]> git.mxchange.org Git - friendica.git/blob - include/text.php
Fix uses in include/dba
[friendica.git] / include / text.php
1 <?php
2 /**
3  * @file include/text.php
4  */
5
6 use Friendica\App;
7 use Friendica\Content\ContactSelector;
8 use Friendica\Content\Feature;
9 use Friendica\Content\Smilies;
10 use Friendica\Content\Text\BBCode;
11 use Friendica\Core\Addon;
12 use Friendica\Core\Config;
13 use Friendica\Core\L10n;
14 use Friendica\Core\PConfig;
15 use Friendica\Core\System;
16 use Friendica\Database\DBM;
17 use Friendica\Model\Contact;
18 use Friendica\Model\Event;
19 use Friendica\Model\Item;
20 use Friendica\Model\Profile;
21 use Friendica\Render\FriendicaSmarty;
22 use Friendica\Util\DateTimeFormat;
23 use Friendica\Util\Map;
24
25 require_once "mod/proxy.php";
26 require_once "include/conversation.php";
27
28 /**
29  * This is our template processor
30  *
31  * @param string|FriendicaSmarty $s the string requiring macro substitution,
32  *                              or an instance of FriendicaSmarty
33  * @param array $r key value pairs (search => replace)
34  * @return string substituted string
35  */
36 function replace_macros($s, $r) {
37
38         $stamp1 = microtime(true);
39
40         $a = get_app();
41
42         // pass $baseurl to all templates
43         $r['$baseurl'] = System::baseUrl();
44
45         $t = $a->template_engine();
46         try {
47                 $output = $t->replaceMacros($s, $r);
48         } catch (Exception $e) {
49                 echo "<pre><b>" . __FUNCTION__ . "</b>: " . $e->getMessage() . "</pre>";
50                 killme();
51         }
52
53         $a->save_timestamp($stamp1, "rendering");
54
55         return $output;
56 }
57
58 /**
59  * @brief Generates a pseudo-random string of hexadecimal characters
60  *
61  * @param int $size
62  * @return string
63  */
64 function random_string($size = 64)
65 {
66         $byte_size = ceil($size / 2);
67
68         $bytes = random_bytes($byte_size);
69
70         $return = substr(bin2hex($bytes), 0, $size);
71
72         return $return;
73 }
74
75 /**
76  * This is our primary input filter.
77  *
78  * The high bit hack only involved some old IE browser, forget which (IE5/Mac?)
79  * that had an XSS attack vector due to stripping the high-bit on an 8-bit character
80  * after cleansing, and angle chars with the high bit set could get through as markup.
81  *
82  * This is now disabled because it was interfering with some legitimate unicode sequences
83  * and hopefully there aren't a lot of those browsers left.
84  *
85  * Use this on any text input where angle chars are not valid or permitted
86  * They will be replaced with safer brackets. This may be filtered further
87  * if these are not allowed either.
88  *
89  * @param string $string Input string
90  * @return string Filtered string
91  */
92 function notags($string) {
93         return str_replace(["<", ">"], ['[', ']'], $string);
94
95 //  High-bit filter no longer used
96 //      return str_replace(array("<",">","\xBA","\xBC","\xBE"), array('[',']','','',''), $string);
97 }
98
99
100 /**
101  * use this on "body" or "content" input where angle chars shouldn't be removed,
102  * and allow them to be safely displayed.
103  * @param string $string
104  * @return string
105  */
106 function escape_tags($string) {
107         return htmlspecialchars($string, ENT_COMPAT, 'UTF-8', false);
108 }
109
110
111 /**
112  * generate a string that's random, but usually pronounceable.
113  * used to generate initial passwords
114  * @param int $len
115  * @return string
116  */
117 function autoname($len) {
118
119         if ($len <= 0) {
120                 return '';
121         }
122
123         $vowels = ['a','a','ai','au','e','e','e','ee','ea','i','ie','o','ou','u'];
124         if (mt_rand(0, 5) == 4) {
125                 $vowels[] = 'y';
126         }
127
128         $cons = [
129                         'b','bl','br',
130                         'c','ch','cl','cr',
131                         'd','dr',
132                         'f','fl','fr',
133                         'g','gh','gl','gr',
134                         'h',
135                         'j',
136                         'k','kh','kl','kr',
137                         'l',
138                         'm',
139                         'n',
140                         'p','ph','pl','pr',
141                         'qu',
142                         'r','rh',
143                         's','sc','sh','sm','sp','st',
144                         't','th','tr',
145                         'v',
146                         'w','wh',
147                         'x',
148                         'z','zh'
149                         ];
150
151         $midcons = ['ck','ct','gn','ld','lf','lm','lt','mb','mm', 'mn','mp',
152                                 'nd','ng','nk','nt','rn','rp','rt'];
153
154         $noend = ['bl', 'br', 'cl','cr','dr','fl','fr','gl','gr',
155                                 'kh', 'kl','kr','mn','pl','pr','rh','tr','qu','wh','q'];
156
157         $start = mt_rand(0,2);
158         if ($start == 0) {
159                 $table = $vowels;
160         } else {
161                 $table = $cons;
162         }
163
164         $word = '';
165
166         for ($x = 0; $x < $len; $x ++) {
167                 $r = mt_rand(0,count($table) - 1);
168                 $word .= $table[$r];
169
170                 if ($table == $vowels) {
171                         $table = array_merge($cons,$midcons);
172                 } else {
173                         $table = $vowels;
174                 }
175
176         }
177
178         $word = substr($word,0,$len);
179
180         foreach ($noend as $noe) {
181                 $noelen = strlen($noe);
182                 if ((strlen($word) > $noelen) && (substr($word, -$noelen) == $noe)) {
183                         $word = autoname($len);
184                         break;
185                 }
186         }
187
188         return $word;
189 }
190
191
192 /**
193  * escape text ($str) for XML transport
194  * @param string $str
195  * @return string Escaped text.
196  */
197 function xmlify($str) {
198         /// @TODO deprecated code found?
199 /*      $buffer = '';
200
201         $len = mb_strlen($str);
202         for ($x = 0; $x < $len; $x ++) {
203                 $char = mb_substr($str,$x,1);
204
205                 switch($char) {
206
207                         case "\r" :
208                                 break;
209                         case "&" :
210                                 $buffer .= '&amp;';
211                                 break;
212                         case "'" :
213                                 $buffer .= '&apos;';
214                                 break;
215                         case "\"" :
216                                 $buffer .= '&quot;';
217                                 break;
218                         case '<' :
219                                 $buffer .= '&lt;';
220                                 break;
221                         case '>' :
222                                 $buffer .= '&gt;';
223                                 break;
224                         case "\n" :
225                                 $buffer .= "\n";
226                                 break;
227                         default :
228                                 $buffer .= $char;
229                                 break;
230                 }
231         }*/
232         /*
233         $buffer = mb_ereg_replace("&", "&amp;", $str);
234         $buffer = mb_ereg_replace("'", "&apos;", $buffer);
235         $buffer = mb_ereg_replace('"', "&quot;", $buffer);
236         $buffer = mb_ereg_replace("<", "&lt;", $buffer);
237         $buffer = mb_ereg_replace(">", "&gt;", $buffer);
238         */
239         $buffer = htmlspecialchars($str, ENT_QUOTES, "UTF-8");
240         $buffer = trim($buffer);
241
242         return $buffer;
243 }
244
245
246 /**
247  * undo an xmlify
248  * @param string $s xml escaped text
249  * @return string unescaped text
250  */
251 function unxmlify($s) {
252         /// @TODO deprecated code found?
253 //      $ret = str_replace('&amp;','&', $s);
254 //      $ret = str_replace(array('&lt;','&gt;','&quot;','&apos;'),array('<','>','"',"'"),$ret);
255         /*$ret = mb_ereg_replace('&amp;', '&', $s);
256         $ret = mb_ereg_replace('&apos;', "'", $ret);
257         $ret = mb_ereg_replace('&quot;', '"', $ret);
258         $ret = mb_ereg_replace('&lt;', "<", $ret);
259         $ret = mb_ereg_replace('&gt;', ">", $ret);
260         */
261         $ret = htmlspecialchars_decode($s, ENT_QUOTES);
262         return $ret;
263 }
264
265
266 /**
267  * @brief Paginator function. Pushes relevant links in a pager array structure.
268  *
269  * Links are generated depending on the current page and the total number of items.
270  * Inactive links (like "first" and "prev" on page 1) are given the "disabled" class.
271  * Current page link is given the "active" CSS class
272  *
273  * @param App $a App instance
274  * @param int $count [optional] item count (used with minimal pager)
275  * @return Array data for pagination template
276  */
277 function paginate_data(App $a, $count = null) {
278         $stripped = preg_replace('/([&?]page=[0-9]*)/', '', $a->query_string);
279
280         $stripped = str_replace('q=', '', $stripped);
281         $stripped = trim($stripped, '/');
282         $pagenum = $a->pager['page'];
283
284         if (($a->page_offset != '') && !preg_match('/[?&].offset=/', $stripped)) {
285                 $stripped .= '&offset=' . urlencode($a->page_offset);
286         }
287
288         $url = $stripped;
289         $data = [];
290
291         function _l(&$d, $name, $url, $text, $class = '') {
292                 if (strpos($url, '?') === false && ($pos = strpos($url, '&')) !== false) {
293                         $url = substr($url, 0, $pos) . '?' . substr($url, $pos + 1);
294                 }
295
296                 $d[$name] = ['url' => $url, 'text' => $text, 'class' => $class];
297         }
298
299         if (!is_null($count)) {
300                 // minimal pager (newer / older)
301                 $data['class'] = 'pager';
302                 _l($data, 'prev', $url . '&page=' . ($a->pager['page'] - 1), L10n::t('newer'), 'previous' . ($a->pager['page'] == 1 ? ' disabled' : ''));
303                 _l($data, 'next', $url . '&page=' . ($a->pager['page'] + 1), L10n::t('older'), 'next' . ($count <= 0 ? ' disabled' : ''));
304         } else {
305                 // full pager (first / prev / 1 / 2 / ... / 14 / 15 / next / last)
306                 $data['class'] = 'pagination';
307                 if ($a->pager['total'] > $a->pager['itemspage']) {
308                         _l($data, 'first', $url . '&page=1', L10n::t('first'), $a->pager['page'] == 1 ? 'disabled' : '');
309                         _l($data, 'prev', $url . '&page=' . ($a->pager['page'] - 1), L10n::t('prev'), $a->pager['page'] == 1 ? 'disabled' : '');
310
311                         $numpages = $a->pager['total'] / $a->pager['itemspage'];
312
313                         $numstart = 1;
314                         $numstop = $numpages;
315
316                         // Limit the number of displayed page number buttons.
317                         if ($numpages > 8) {
318                                 $numstart = (($pagenum > 4) ? ($pagenum - 4) : 1);
319                                 $numstop = (($pagenum > ($numpages - 7)) ? $numpages : ($numstart + 8));
320                         }
321
322                         $pages = [];
323
324                         for ($i = $numstart; $i <= $numstop; $i++) {
325                                 if ($i == $a->pager['page']) {
326                                         _l($pages, $i, '#',  $i, 'current active');
327                                 } else {
328                                         _l($pages, $i, $url . '&page='. $i, $i, 'n');
329                                 }
330                         }
331
332                         if (($a->pager['total'] % $a->pager['itemspage']) != 0) {
333                                 if ($i == $a->pager['page']) {
334                                         _l($pages, $i, '#',  $i, 'current active');
335                                 } else {
336                                         _l($pages, $i, $url . '&page=' . $i, $i, 'n');
337                                 }
338                         }
339
340                         $data['pages'] = $pages;
341
342                         $lastpage = (($numpages > intval($numpages)) ? intval($numpages)+1 : $numpages);
343                         _l($data, 'next', $url . '&page=' . ($a->pager['page'] + 1), L10n::t('next'), $a->pager['page'] == $lastpage ? 'disabled' : '');
344                         _l($data, 'last', $url . '&page=' . $lastpage, L10n::t('last'), $a->pager['page'] == $lastpage ? 'disabled' : '');
345                 }
346         }
347
348         return $data;
349 }
350
351
352 /**
353  * Automatic pagination.
354  *
355  *  To use, get the count of total items.
356  * Then call $a->set_pager_total($number_items);
357  * Optionally call $a->set_pager_itemspage($n) to the number of items to display on each page
358  * Then call paginate($a) after the end of the display loop to insert the pager block on the page
359  * (assuming there are enough items to paginate).
360  * When using with SQL, the setting LIMIT %d, %d => $a->pager['start'],$a->pager['itemspage']
361  * will limit the results to the correct items for the current page.
362  * The actual page handling is then accomplished at the application layer.
363  *
364  * @param App $a App instance
365  * @return string html for pagination #FIXME remove html
366  */
367 function paginate(App $a) {
368
369         $data = paginate_data($a);
370         $tpl = get_markup_template("paginate.tpl");
371         return replace_macros($tpl, ["pager" => $data]);
372
373 }
374
375
376 /**
377  * Alternative pager
378  * @param App $a App instance
379  * @param int $i
380  * @return string html for pagination #FIXME remove html
381  */
382 function alt_pager(App $a, $i) {
383
384         $data = paginate_data($a, $i);
385         $tpl = get_markup_template("paginate.tpl");
386         return replace_macros($tpl, ['pager' => $data]);
387
388 }
389
390
391 /**
392  * Loader for infinite scrolling
393  * @return string html for loader
394  */
395 function scroll_loader() {
396         $tpl = get_markup_template("scroll_loader.tpl");
397         return replace_macros($tpl, [
398                 'wait' => L10n::t('Loading more entries...'),
399                 'end' => L10n::t('The end')
400         ]);
401 }
402
403
404 /**
405  * Turn user/group ACLs stored as angle bracketed text into arrays
406  *
407  * @param string $s
408  * @return array
409  */
410 function expand_acl($s) {
411         // turn string array of angle-bracketed elements into numeric array
412         // e.g. "<1><2><3>" => array(1,2,3);
413         $ret = [];
414
415         if (strlen($s)) {
416                 $t = str_replace('<', '', $s);
417                 $a = explode('>', $t);
418                 foreach ($a as $aa) {
419                         if (intval($aa)) {
420                                 $ret[] = intval($aa);
421                         }
422                 }
423         }
424         return $ret;
425 }
426
427
428 /**
429  * Wrap ACL elements in angle brackets for storage
430  * @param string $item
431  */
432 function sanitise_acl(&$item) {
433         if (intval($item)) {
434                 $item = '<' . intval(notags(trim($item))) . '>';
435         } else {
436                 unset($item);
437         }
438 }
439
440
441 /**
442  * Convert an ACL array to a storable string
443  *
444  * Normally ACL permissions will be an array.
445  * We'll also allow a comma-separated string.
446  *
447  * @param string|array $p
448  * @return string
449  */
450 function perms2str($p) {
451         $ret = '';
452         if (is_array($p)) {
453                 $tmp = $p;
454         } else {
455                 $tmp = explode(',', $p);
456         }
457
458         if (is_array($tmp)) {
459                 array_walk($tmp, 'sanitise_acl');
460                 $ret = implode('', $tmp);
461         }
462         return $ret;
463 }
464
465 /**
466  * @deprecated
467  * wrapper to load a view template, checking for alternate
468  * languages before falling back to the default
469  *
470  * @global string $lang
471  * @global App $a
472  * @param string $s view name
473  * @return string
474  */
475 function load_view_file($s) {
476         global $lang, $a;
477         if (!isset($lang)) {
478                 $lang = 'en';
479         }
480         $b = basename($s);
481         $d = dirname($s);
482         if (file_exists("$d/$lang/$b")) {
483                 $stamp1 = microtime(true);
484                 $content = file_get_contents("$d/$lang/$b");
485                 $a->save_timestamp($stamp1, "file");
486                 return $content;
487         }
488
489         $theme = $a->getCurrentTheme();
490
491         if (file_exists("$d/theme/$theme/$b")) {
492                 $stamp1 = microtime(true);
493                 $content = file_get_contents("$d/theme/$theme/$b");
494                 $a->save_timestamp($stamp1, "file");
495                 return $content;
496         }
497
498         $stamp1 = microtime(true);
499         $content = file_get_contents($s);
500         $a->save_timestamp($stamp1, "file");
501         return $content;
502 }
503
504
505 /**
506  * load a view template, checking for alternate
507  * languages before falling back to the default
508  *
509  * @global string $lang
510  * @param string $s view path
511  * @return string
512  */
513 function get_intltext_template($s) {
514         global $lang;
515
516         $a = get_app();
517         $engine = '';
518         if ($a->theme['template_engine'] === 'smarty3') {
519                 $engine = "/smarty3";
520         }
521
522         if (!isset($lang)) {
523                 $lang = 'en';
524         }
525
526         if (file_exists("view/lang/$lang$engine/$s")) {
527                 $stamp1 = microtime(true);
528                 $content = file_get_contents("view/lang/$lang$engine/$s");
529                 $a->save_timestamp($stamp1, "file");
530                 return $content;
531         } elseif (file_exists("view/lang/en$engine/$s")) {
532                 $stamp1 = microtime(true);
533                 $content = file_get_contents("view/lang/en$engine/$s");
534                 $a->save_timestamp($stamp1, "file");
535                 return $content;
536         } else {
537                 $stamp1 = microtime(true);
538                 $content = file_get_contents("view$engine/$s");
539                 $a->save_timestamp($stamp1, "file");
540                 return $content;
541         }
542 }
543
544
545 /**
546  * load template $s
547  *
548  * @param string $s
549  * @param string $root
550  * @return string
551  */
552 function get_markup_template($s, $root = '') {
553         $stamp1 = microtime(true);
554
555         $a = get_app();
556         $t = $a->template_engine();
557         try {
558                 $template = $t->getTemplateFile($s, $root);
559         } catch (Exception $e) {
560                 echo "<pre><b>" . __FUNCTION__ . "</b>: " . $e->getMessage() . "</pre>";
561                 killme();
562         }
563
564         $a->save_timestamp($stamp1, "file");
565
566         return $template;
567 }
568
569 /**
570  *  for html,xml parsing - let's say you've got
571  *  an attribute foobar="class1 class2 class3"
572  *  and you want to find out if it contains 'class3'.
573  *  you can't use a normal sub string search because you
574  *  might match 'notclass3' and a regex to do the job is
575  *  possible but a bit complicated.
576  *  pass the attribute string as $attr and the attribute you
577  *  are looking for as $s - returns true if found, otherwise false
578  *
579  * @param string $attr attribute value
580  * @param string $s string to search
581  * @return boolean True if found, False otherwise
582  */
583 function attribute_contains($attr, $s) {
584         $a = explode(' ', $attr);
585         return (count($a) && in_array($s,$a));
586 }
587
588
589 /* setup int->string log level map */
590 $LOGGER_LEVELS = [];
591
592 /**
593  * @brief Logs the given message at the given log level
594  *
595  * log levels:
596  * LOGGER_NORMAL (default)
597  * LOGGER_TRACE
598  * LOGGER_DEBUG
599  * LOGGER_DATA
600  * LOGGER_ALL
601  *
602  * @global App $a
603  * @global array $LOGGER_LEVELS
604  * @param string $msg
605  * @param int $level
606  */
607 function logger($msg, $level = 0) {
608         $a = get_app();
609         global $LOGGER_LEVELS;
610
611         $debugging = Config::get('system', 'debugging');
612         $logfile   = Config::get('system', 'logfile');
613         $loglevel = intval(Config::get('system', 'loglevel'));
614
615         if (
616                 !$debugging
617                 || !$logfile
618                 || $level > $loglevel
619         ) {
620                 return;
621         }
622
623         if (count($LOGGER_LEVELS) == 0) {
624                 foreach (get_defined_constants() as $k => $v) {
625                         if (substr($k, 0, 7) == "LOGGER_") {
626                                 $LOGGER_LEVELS[$v] = substr($k, 7, 7);
627                         }
628                 }
629         }
630
631         $process_id = session_id();
632
633         if ($process_id == '') {
634                 $process_id = get_app()->process_id;
635         }
636
637         $callers = debug_backtrace();
638
639         if (count($callers) > 1) {
640                 $function = $callers[1]['function'];
641         } else {
642                 $function = '';
643         }
644
645         $logline = sprintf("%s@%s\t[%s]:%s:%s:%s\t%s\n",
646                         DateTimeFormat::utcNow(DateTimeFormat::ATOM),
647                         $process_id,
648                         $LOGGER_LEVELS[$level],
649                         basename($callers[0]['file']),
650                         $callers[0]['line'],
651                         $function,
652                         $msg
653                 );
654
655         $stamp1 = microtime(true);
656         @file_put_contents($logfile, $logline, FILE_APPEND);
657         $a->save_timestamp($stamp1, "file");
658 }
659
660 /**
661  * @brief An alternative logger for development.
662  * Works largely as logger() but allows developers
663  * to isolate particular elements they are targetting
664  * personally without background noise
665  *
666  * log levels:
667  * LOGGER_NORMAL (default)
668  * LOGGER_TRACE
669  * LOGGER_DEBUG
670  * LOGGER_DATA
671  * LOGGER_ALL
672  *
673  * @global App $a
674  * @global array $LOGGER_LEVELS
675  * @param string $msg
676  * @param int $level
677  */
678
679 function dlogger($msg, $level = 0) {
680         $a = get_app();
681
682         $logfile = Config::get('system', 'dlogfile');
683         if (!$logfile) {
684                 return;
685         }
686
687         $dlogip = Config::get('system', 'dlogip');
688         if (!is_null($dlogip) && $_SERVER['REMOTE_ADDR'] != $dlogip) {
689                 return;
690         }
691
692         if (count($LOGGER_LEVELS) == 0) {
693                 foreach (get_defined_constants() as $k => $v) {
694                         if (substr($k, 0, 7) == "LOGGER_") {
695                                 $LOGGER_LEVELS[$v] = substr($k, 7, 7);
696                         }
697                 }
698         }
699
700         $process_id = session_id();
701
702         if ($process_id == '') {
703                 $process_id = $a->process_id;
704         }
705
706         $callers = debug_backtrace();
707         $logline = sprintf("%s@\t%s:\t%s:\t%s\t%s\t%s\n",
708                         DateTimeFormat::utcNow(),
709                         $process_id,
710                         basename($callers[0]['file']),
711                         $callers[0]['line'],
712                         $callers[1]['function'],
713                         $msg
714                 );
715
716         $stamp1 = microtime(true);
717         @file_put_contents($logfile, $logline, FILE_APPEND);
718         $a->save_timestamp($stamp1, "file");
719 }
720
721
722 /**
723  * Compare activity uri. Knows about activity namespace.
724  *
725  * @param string $haystack
726  * @param string $needle
727  * @return boolean
728  */
729 function activity_match($haystack,$needle) {
730         return (($haystack === $needle) || ((basename($needle) === $haystack) && strstr($needle, NAMESPACE_ACTIVITY_SCHEMA)));
731 }
732
733
734 /**
735  * @brief Pull out all #hashtags and @person tags from $string.
736  *
737  * We also get @person@domain.com - which would make
738  * the regex quite complicated as tags can also
739  * end a sentence. So we'll run through our results
740  * and strip the period from any tags which end with one.
741  * Returns array of tags found, or empty array.
742  *
743  * @param string $string Post content
744  * @return array List of tag and person names
745  */
746 function get_tags($string) {
747         $ret = [];
748
749         // Convert hashtag links to hashtags
750         $string = preg_replace('/#\[url\=([^\[\]]*)\](.*?)\[\/url\]/ism', '#$2', $string);
751
752         // ignore anything in a code block
753         $string = preg_replace('/\[code\](.*?)\[\/code\]/sm', '', $string);
754
755         // Force line feeds at bbtags
756         $string = str_replace(['[', ']'], ["\n[", "]\n"], $string);
757
758         // ignore anything in a bbtag
759         $string = preg_replace('/\[(.*?)\]/sm', '', $string);
760
761         // Match full names against @tags including the space between first and last
762         // We will look these up afterward to see if they are full names or not recognisable.
763
764         if (preg_match_all('/(@[^ \x0D\x0A,:?]+ [^ \x0D\x0A@,:?]+)([ \x0D\x0A@,:?]|$)/', $string, $matches)) {
765                 foreach ($matches[1] as $match) {
766                         if (strstr($match, ']')) {
767                                 // we might be inside a bbcode color tag - leave it alone
768                                 continue;
769                         }
770                         if (substr($match, -1, 1) === '.') {
771                                 $ret[] = substr($match, 0, -1);
772                         } else {
773                                 $ret[] = $match;
774                         }
775                 }
776         }
777
778         // Otherwise pull out single word tags. These can be @nickname, @first_last
779         // and #hash tags.
780
781         if (preg_match_all('/([!#@][^\^ \x0D\x0A,;:?]+)([ \x0D\x0A,;:?]|$)/', $string, $matches)) {
782                 foreach ($matches[1] as $match) {
783                         if (strstr($match, ']')) {
784                                 // we might be inside a bbcode color tag - leave it alone
785                                 continue;
786                         }
787                         if (substr($match, -1, 1) === '.') {
788                                 $match = substr($match,0,-1);
789                         }
790                         // ignore strictly numeric tags like #1
791                         if ((strpos($match, '#') === 0) && ctype_digit(substr($match, 1))) {
792                                 continue;
793                         }
794                         // try not to catch url fragments
795                         if (strpos($string, $match) && preg_match('/[a-zA-z0-9\/]/', substr($string, strpos($string, $match) - 1, 1))) {
796                                 continue;
797                         }
798                         $ret[] = $match;
799                 }
800         }
801         return $ret;
802 }
803
804
805 /**
806  * quick and dirty quoted_printable encoding
807  *
808  * @param string $s
809  * @return string
810  */
811 function qp($s) {
812         return str_replace("%", "=", rawurlencode($s));
813 }
814
815
816 /**
817  * Get html for contact block.
818  *
819  * @template contact_block.tpl
820  * @hook contact_block_end (contacts=>array, output=>string)
821  * @return string
822  */
823 function contact_block() {
824         $o = '';
825         $a = get_app();
826
827         $shown = PConfig::get($a->profile['uid'], 'system', 'display_friend_count', 24);
828         if ($shown == 0) {
829                 return;
830         }
831
832         if (!is_array($a->profile) || $a->profile['hide-friends']) {
833                 return $o;
834         }
835         $r = q("SELECT COUNT(*) AS `total` FROM `contact`
836                         WHERE `uid` = %d AND NOT `self` AND NOT `blocked`
837                                 AND NOT `pending` AND NOT `hidden` AND NOT `archive`
838                                 AND `network` IN ('%s', '%s', '%s')",
839                         intval($a->profile['uid']),
840                         dbesc(NETWORK_DFRN),
841                         dbesc(NETWORK_OSTATUS),
842                         dbesc(NETWORK_DIASPORA)
843         );
844         if (DBM::is_result($r)) {
845                 $total = intval($r[0]['total']);
846         }
847         if (!$total) {
848                 $contacts = L10n::t('No contacts');
849                 $micropro = null;
850         } else {
851                 // Splitting the query in two parts makes it much faster
852                 $r = q("SELECT `id` FROM `contact`
853                                 WHERE `uid` = %d AND NOT `self` AND NOT `blocked`
854                                         AND NOT `pending` AND NOT `hidden` AND NOT `archive`
855                                         AND `network` IN ('%s', '%s', '%s')
856                                 ORDER BY RAND() LIMIT %d",
857                                 intval($a->profile['uid']),
858                                 dbesc(NETWORK_DFRN),
859                                 dbesc(NETWORK_OSTATUS),
860                                 dbesc(NETWORK_DIASPORA),
861                                 intval($shown)
862                 );
863                 if (DBM::is_result($r)) {
864                         $contacts = [];
865                         foreach ($r AS $contact) {
866                                 $contacts[] = $contact["id"];
867                         }
868                         $r = q("SELECT `id`, `uid`, `addr`, `url`, `name`, `thumb`, `network` FROM `contact` WHERE `id` IN (%s)",
869                                 dbesc(implode(",", $contacts)));
870
871                         if (DBM::is_result($r)) {
872                                 $contacts = L10n::tt('%d Contact', '%d Contacts', $total);
873                                 $micropro = [];
874                                 foreach ($r as $rr) {
875                                         $micropro[] = micropro($rr, true, 'mpfriend');
876                                 }
877                         }
878                 }
879         }
880
881         $tpl = get_markup_template('contact_block.tpl');
882         $o = replace_macros($tpl, [
883                 '$contacts' => $contacts,
884                 '$nickname' => $a->profile['nickname'],
885                 '$viewcontacts' => L10n::t('View Contacts'),
886                 '$micropro' => $micropro,
887         ]);
888
889         $arr = ['contacts' => $r, 'output' => $o];
890
891         Addon::callHooks('contact_block_end', $arr);
892         return $o;
893
894 }
895
896
897 /**
898  * @brief Format contacts as picture links or as texxt links
899  *
900  * @param array $contact Array with contacts which contains an array with
901  *      int 'id' => The ID of the contact
902  *      int 'uid' => The user ID of the user who owns this data
903  *      string 'name' => The name of the contact
904  *      string 'url' => The url to the profile page of the contact
905  *      string 'addr' => The webbie of the contact (e.g.) username@friendica.com
906  *      string 'network' => The network to which the contact belongs to
907  *      string 'thumb' => The contact picture
908  *      string 'click' => js code which is performed when clicking on the contact
909  * @param boolean $redirect If true try to use the redir url if it's possible
910  * @param string $class CSS class for the
911  * @param boolean $textmode If true display the contacts as text links
912  *      if false display the contacts as picture links
913
914  * @return string Formatted html
915  */
916 function micropro($contact, $redirect = false, $class = '', $textmode = false) {
917
918         // Use the contact URL if no address is available
919         if (!x($contact, "addr")) {
920                 $contact["addr"] = $contact["url"];
921         }
922
923         $url = $contact['url'];
924         $sparkle = '';
925         $redir = false;
926
927         if ($redirect) {
928                 $url = Contact::magicLink($contact['url']);
929                 if (strpos($url, 'redir/') === 0) {
930                         $sparkle = ' sparkle';
931                 }
932         }
933
934         // If there is some js available we don't need the url
935         if (x($contact, 'click')) {
936                 $url = '';
937         }
938
939         return replace_macros(get_markup_template(($textmode)?'micropro_txt.tpl':'micropro_img.tpl'),[
940                 '$click' => defaults($contact, 'click', ''),
941                 '$class' => $class,
942                 '$url' => $url,
943                 '$photo' => proxy_url($contact['thumb'], false, PROXY_SIZE_THUMB),
944                 '$name' => $contact['name'],
945                 'title' => $contact['name'] . ' [' . $contact['addr'] . ']',
946                 '$parkle' => $sparkle,
947                 '$redir' => $redir,
948
949         ]);
950 }
951
952 /**
953  * Search box.
954  *
955  * @param string $s     Search query.
956  * @param string $id    HTML id
957  * @param string $url   Search url.
958  * @param bool   $save  Show save search button.
959  * @param bool   $aside Display the search widgit aside.
960  *
961  * @return string Formatted HTML.
962  */
963 function search($s, $id = 'search-box', $url = 'search', $save = false, $aside = true)
964 {
965         $mode = 'text';
966
967         if (strpos($s, '#') === 0) {
968                 $mode = 'tag';
969         }
970         $save_label = $mode === 'text' ? L10n::t('Save') : L10n::t('Follow');
971
972         $values = [
973                         '$s' => htmlspecialchars($s),
974                         '$id' => $id,
975                         '$action_url' => $url,
976                         '$search_label' => L10n::t('Search'),
977                         '$save_label' => $save_label,
978                         '$savedsearch' => local_user() && Feature::isEnabled(local_user(),'savedsearch'),
979                         '$search_hint' => L10n::t('@name, !forum, #tags, content'),
980                         '$mode' => $mode
981                 ];
982
983         if (!$aside) {
984                 $values['$searchoption'] = [
985                                         L10n::t("Full Text"),
986                                         L10n::t("Tags"),
987                                         L10n::t("Contacts")];
988
989                 if (Config::get('system','poco_local_search')) {
990                         $values['$searchoption'][] = L10n::t("Forums");
991                 }
992         }
993
994         return replace_macros(get_markup_template('searchbox.tpl'), $values);
995 }
996
997 /**
998  * @brief Check for a valid email string
999  *
1000  * @param string $email_address
1001  * @return boolean
1002  */
1003 function valid_email($email_address)
1004 {
1005         return preg_match('/^[_a-zA-Z0-9\-\+]+(\.[_a-zA-Z0-9\-\+]+)*@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+$/', $email_address);
1006 }
1007
1008
1009 /**
1010  * Replace naked text hyperlink with HTML formatted hyperlink
1011  *
1012  * @param string $s
1013  */
1014 function linkify($s) {
1015         $s = preg_replace("/(https?\:\/\/[a-zA-Z0-9\:\/\-\?\&\;\.\=\_\~\#\'\%\$\!\+]*)/", ' <a href="$1" target="_blank">$1</a>', $s);
1016         $s = preg_replace("/\<(.*?)(src|href)=(.*?)\&amp\;(.*?)\>/ism",'<$1$2=$3&$4>',$s);
1017         return $s;
1018 }
1019
1020
1021 /**
1022  * Load poke verbs
1023  *
1024  * @return array index is present tense verb
1025  *                               value is array containing past tense verb, translation of present, translation of past
1026  * @hook poke_verbs pokes array
1027  */
1028 function get_poke_verbs() {
1029
1030         // index is present tense verb
1031         // value is array containing past tense verb, translation of present, translation of past
1032
1033         $arr = [
1034                 'poke' => ['poked', L10n::t('poke'), L10n::t('poked')],
1035                 'ping' => ['pinged', L10n::t('ping'), L10n::t('pinged')],
1036                 'prod' => ['prodded', L10n::t('prod'), L10n::t('prodded')],
1037                 'slap' => ['slapped', L10n::t('slap'), L10n::t('slapped')],
1038                 'finger' => ['fingered', L10n::t('finger'), L10n::t('fingered')],
1039                 'rebuff' => ['rebuffed', L10n::t('rebuff'), L10n::t('rebuffed')],
1040         ];
1041         Addon::callHooks('poke_verbs', $arr);
1042         return $arr;
1043 }
1044
1045 /**
1046  * @brief Translate days and months names.
1047  *
1048  * @param string $s String with day or month name.
1049  * @return string Translated string.
1050  */
1051 function day_translate($s) {
1052         $ret = str_replace(['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'],
1053                 [L10n::t('Monday'), L10n::t('Tuesday'), L10n::t('Wednesday'), L10n::t('Thursday'), L10n::t('Friday'), L10n::t('Saturday'), L10n::t('Sunday')],
1054                 $s);
1055
1056         $ret = str_replace(['January','February','March','April','May','June','July','August','September','October','November','December'],
1057                 [L10n::t('January'), L10n::t('February'), L10n::t('March'), L10n::t('April'), L10n::t('May'), L10n::t('June'), L10n::t('July'), L10n::t('August'), L10n::t('September'), L10n::t('October'), L10n::t('November'), L10n::t('December')],
1058                 $ret);
1059
1060         return $ret;
1061 }
1062
1063 /**
1064  * @brief Translate short days and months names.
1065  *
1066  * @param string $s String with short day or month name.
1067  * @return string Translated string.
1068  */
1069 function day_short_translate($s) {
1070         $ret = str_replace(['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
1071                 [L10n::t('Mon'), L10n::t('Tue'), L10n::t('Wed'), L10n::t('Thu'), L10n::t('Fri'), L10n::t('Sat'), L10n::t('Sun')],
1072                 $s);
1073         $ret = str_replace(['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov','Dec'],
1074                 [L10n::t('Jan'), L10n::t('Feb'), L10n::t('Mar'), L10n::t('Apr'), L10n::t('May'), ('Jun'), L10n::t('Jul'), L10n::t('Aug'), L10n::t('Sep'), L10n::t('Oct'), L10n::t('Nov'), L10n::t('Dec')],
1075                 $ret);
1076         return $ret;
1077 }
1078
1079
1080 /**
1081  * Normalize url
1082  *
1083  * @param string $url
1084  * @return string
1085  */
1086 function normalise_link($url) {
1087         $ret = str_replace(['https:', '//www.'], ['http:', '//'], $url);
1088         return rtrim($ret,'/');
1089 }
1090
1091
1092 /**
1093  * Compare two URLs to see if they are the same, but ignore
1094  * slight but hopefully insignificant differences such as if one
1095  * is https and the other isn't, or if one is www.something and
1096  * the other isn't - and also ignore case differences.
1097  *
1098  * @param string $a first url
1099  * @param string $b second url
1100  * @return boolean True if the URLs match, otherwise False
1101  *
1102  */
1103 function link_compare($a, $b) {
1104         return (strcasecmp(normalise_link($a), normalise_link($b)) === 0);
1105 }
1106
1107
1108 /**
1109  * @brief Find any non-embedded images in private items and add redir links to them
1110  *
1111  * @param App $a
1112  * @param array &$item The field array of an item row
1113  */
1114 function redir_private_images($a, &$item)
1115 {
1116         $matches = false;
1117         $cnt = preg_match_all('|\[img\](http[^\[]*?/photo/[a-fA-F0-9]+?(-[0-9]\.[\w]+?)?)\[\/img\]|', $item['body'], $matches, PREG_SET_ORDER);
1118         if ($cnt) {
1119                 foreach ($matches as $mtch) {
1120                         if (strpos($mtch[1], '/redir') !== false) {
1121                                 continue;
1122                         }
1123
1124                         if ((local_user() == $item['uid']) && ($item['private'] == 1) && ($item['contact-id'] != $a->contact['id']) && ($item['network'] == NETWORK_DFRN)) {
1125                                 $img_url = 'redir?f=1&quiet=1&url=' . urlencode($mtch[1]) . '&conurl=' . urlencode($item['author-link']);
1126                                 $item['body'] = str_replace($mtch[0], '[img]' . $img_url . '[/img]', $item['body']);
1127                         }
1128                 }
1129         }
1130 }
1131
1132 /**
1133  * Sets the "rendered-html" field of the provided item
1134  *
1135  * Body is preserved to avoid side-effects as we modify it just-in-time for spoilers and private image links
1136  *
1137  * @param array $item
1138  * @param bool  $update
1139  *
1140  * @todo Remove reference, simply return "rendered-html" and "rendered-hash"
1141  */
1142 function put_item_in_cache(&$item, $update = false)
1143 {
1144         $body = $item["body"];
1145
1146         $rendered_hash = defaults($item, 'rendered-hash', '');
1147         $rendered_html = defaults($item, 'rendered-html', '');
1148
1149         if ($rendered_hash == ''
1150                 || $rendered_html == ""
1151                 || $rendered_hash != hash("md5", $item["body"])
1152                 || Config::get("system", "ignore_cache")
1153         ) {
1154                 $a = get_app();
1155                 redir_private_images($a, $item);
1156
1157                 $item["rendered-html"] = prepare_text($item["body"]);
1158                 $item["rendered-hash"] = hash("md5", $item["body"]);
1159
1160                 $hook_data = ['item' => $item, 'rendered-html' => $item['rendered-html'], 'rendered-hash' => $item['rendered-hash']];
1161                 Addon::callHooks('put_item_in_cache', $hook_data);
1162                 $item['rendered-html'] = $hook_data['rendered-html'];
1163                 $item['rendered-hash'] = $hook_data['rendered-hash'];
1164                 unset($hook_data);
1165
1166                 // Force an update if the generated values differ from the existing ones
1167                 if ($rendered_hash != $item["rendered-hash"]) {
1168                         $update = true;
1169                 }
1170
1171                 // Only compare the HTML when we forcefully ignore the cache
1172                 if (Config::get("system", "ignore_cache") && ($rendered_html != $item["rendered-html"])) {
1173                         $update = true;
1174                 }
1175
1176                 if ($update && !empty($item["id"])) {
1177                         Item::update(['rendered-html' => $item["rendered-html"], 'rendered-hash' => $item["rendered-hash"]],
1178                                         ['id' => $item["id"]]);
1179                 }
1180         }
1181
1182         $item["body"] = $body;
1183 }
1184
1185 /**
1186  * @brief Given an item array, convert the body element from bbcode to html and add smilie icons.
1187  * If attach is true, also add icons for item attachments.
1188  *
1189  * @param array   $item
1190  * @param boolean $attach
1191  * @param boolean $is_preview
1192  * @return string item body html
1193  * @hook prepare_body_init item array before any work
1194  * @hook prepare_body_content_filter ('item'=>item array, 'filter_reasons'=>string array) before first bbcode to html
1195  * @hook prepare_body ('item'=>item array, 'html'=>body string, 'is_preview'=>boolean, 'filter_reasons'=>string array) after first bbcode to html
1196  * @hook prepare_body_final ('item'=>item array, 'html'=>body string) after attach icons and blockquote special case handling (spoiler, author)
1197  */
1198 function prepare_body(array &$item, $attach = false, $is_preview = false)
1199 {
1200         $a = get_app();
1201         Addon::callHooks('prepare_body_init', $item);
1202
1203         // In order to provide theme developers more possibilities, event items
1204         // are treated differently.
1205         if ($item['object-type'] === ACTIVITY_OBJ_EVENT && isset($item['event-id'])) {
1206                 $ev = Event::getItemHTML($item);
1207                 return $ev;
1208         }
1209
1210         $tags = \Friendica\Model\Term::populateTagsFromItem($item);
1211
1212         $item['tags'] = $tags['tags'];
1213         $item['hashtags'] = $tags['hashtags'];
1214         $item['mentions'] = $tags['mentions'];
1215
1216         // Compile eventual content filter reasons
1217         $filter_reasons = [];
1218         if (!$is_preview && public_contact() != $item['author-id']) {
1219                 if (!empty($item['content-warning']) && (!local_user() || !PConfig::get(local_user(), 'system', 'disable_cw', false))) {
1220                         $filter_reasons[] = L10n::t('Content warning: %s', $item['content-warning']);
1221                 }
1222
1223                 $hook_data = [
1224                         'item' => $item,
1225                         'filter_reasons' => $filter_reasons
1226                 ];
1227                 Addon::callHooks('prepare_body_content_filter', $hook_data);
1228                 $filter_reasons = $hook_data['filter_reasons'];
1229                 unset($hook_data);
1230         }
1231
1232         // Update the cached values if there is no "zrl=..." on the links.
1233         $update = (!local_user() && !remote_user() && ($item["uid"] == 0));
1234
1235         // Or update it if the current viewer is the intented viewer.
1236         if (($item["uid"] == local_user()) && ($item["uid"] != 0)) {
1237                 $update = true;
1238         }
1239
1240         put_item_in_cache($item, $update);
1241         $s = $item["rendered-html"];
1242
1243         $hook_data = [
1244                 'item' => $item,
1245                 'html' => $s,
1246                 'preview' => $is_preview,
1247                 'filter_reasons' => $filter_reasons
1248         ];
1249         Addon::callHooks('prepare_body', $hook_data);
1250         $s = $hook_data['html'];
1251         unset($hook_data);
1252
1253         if (!$attach) {
1254                 // Replace the blockquotes with quotes that are used in mails.
1255                 $mailquote = '<blockquote type="cite" class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;">';
1256                 $s = str_replace(['<blockquote>', '<blockquote class="spoiler">', '<blockquote class="author">'], [$mailquote, $mailquote, $mailquote], $s);
1257                 return $s;
1258         }
1259
1260         $as = '';
1261         $vhead = false;
1262         $matches = [];
1263         preg_match_all('|\[attach\]href=\"(.*?)\" length=\"(.*?)\" type=\"(.*?)\"(?: title=\"(.*?)\")?|', $item['attach'], $matches, PREG_SET_ORDER);
1264         foreach ($matches as $mtch) {
1265                 $mime = $mtch[3];
1266
1267                 $the_url = Contact::magicLinkById($item['author-id'], $mtch[1]);
1268
1269                 if (strpos($mime, 'video') !== false) {
1270                         if (!$vhead) {
1271                                 $vhead = true;
1272                                 $a->page['htmlhead'] .= replace_macros(get_markup_template('videos_head.tpl'), [
1273                                         '$baseurl' => System::baseUrl(),
1274                                 ]);
1275                                 $a->page['end'] .= replace_macros(get_markup_template('videos_end.tpl'), [
1276                                         '$baseurl' => System::baseUrl(),
1277                                 ]);
1278                         }
1279
1280                         $url_parts = explode('/', $the_url);
1281                         $id = end($url_parts);
1282                         $as .= replace_macros(get_markup_template('video_top.tpl'), [
1283                                 '$video' => [
1284                                         'id'     => $id,
1285                                         'title'  => L10n::t('View Video'),
1286                                         'src'    => $the_url,
1287                                         'mime'   => $mime,
1288                                 ],
1289                         ]);
1290                 }
1291
1292                 $filetype = strtolower(substr($mime, 0, strpos($mime, '/')));
1293                 if ($filetype) {
1294                         $filesubtype = strtolower(substr($mime, strpos($mime, '/') + 1));
1295                         $filesubtype = str_replace('.', '-', $filesubtype);
1296                 } else {
1297                         $filetype = 'unkn';
1298                         $filesubtype = 'unkn';
1299                 }
1300
1301                 $title = escape_tags(trim(!empty($mtch[4]) ? $mtch[4] : $mtch[1]));
1302                 $title .= ' ' . $mtch[2] . ' ' . L10n::t('bytes');
1303
1304                 $icon = '<div class="attachtype icon s22 type-' . $filetype . ' subtype-' . $filesubtype . '"></div>';
1305                 $as .= '<a href="' . strip_tags($the_url) . '" title="' . $title . '" class="attachlink" target="_blank" >' . $icon . '</a>';
1306         }
1307
1308         if ($as != '') {
1309                 $s .= '<div class="body-attach">'.$as.'<div class="clear"></div></div>';
1310         }
1311
1312         // Map.
1313         if (strpos($s, '<div class="map">') !== false && x($item, 'coord')) {
1314                 $x = Map::byCoordinates(trim($item['coord']));
1315                 if ($x) {
1316                         $s = preg_replace('/\<div class\=\"map\"\>/', '$0' . $x, $s);
1317                 }
1318         }
1319
1320
1321         // Look for spoiler.
1322         $spoilersearch = '<blockquote class="spoiler">';
1323
1324         // Remove line breaks before the spoiler.
1325         while ((strpos($s, "\n" . $spoilersearch) !== false)) {
1326                 $s = str_replace("\n" . $spoilersearch, $spoilersearch, $s);
1327         }
1328         while ((strpos($s, "<br />" . $spoilersearch) !== false)) {
1329                 $s = str_replace("<br />" . $spoilersearch, $spoilersearch, $s);
1330         }
1331
1332         while ((strpos($s, $spoilersearch) !== false)) {
1333                 $pos = strpos($s, $spoilersearch);
1334                 $rnd = random_string(8);
1335                 $spoilerreplace = '<br /> <span id="spoiler-wrap-' . $rnd . '" class="spoiler-wrap fakelink" onclick="openClose(\'spoiler-' . $rnd . '\');">' . L10n::t('Click to open/close') . '</span>'.
1336                                         '<blockquote class="spoiler" id="spoiler-' . $rnd . '" style="display: none;">';
1337                 $s = substr($s, 0, $pos) . $spoilerreplace . substr($s, $pos + strlen($spoilersearch));
1338         }
1339
1340         // Look for quote with author.
1341         $authorsearch = '<blockquote class="author">';
1342
1343         while ((strpos($s, $authorsearch) !== false)) {
1344                 $pos = strpos($s, $authorsearch);
1345                 $rnd = random_string(8);
1346                 $authorreplace = '<br /> <span id="author-wrap-' . $rnd . '" class="author-wrap fakelink" onclick="openClose(\'author-' . $rnd . '\');">' . L10n::t('Click to open/close') . '</span>'.
1347                                         '<blockquote class="author" id="author-' . $rnd . '" style="display: block;">';
1348                 $s = substr($s, 0, $pos) . $authorreplace . substr($s, $pos + strlen($authorsearch));
1349         }
1350
1351         // Replace friendica image url size with theme preference.
1352         if (x($a->theme_info, 'item_image_size')){
1353                 $ps = $a->theme_info['item_image_size'];
1354                 $s = preg_replace('|(<img[^>]+src="[^"]+/photo/[0-9a-f]+)-[0-9]|', "$1-" . $ps, $s);
1355         }
1356
1357         $s = apply_content_filter($s, $filter_reasons);
1358
1359         $hook_data = ['item' => $item, 'html' => $s];
1360         Addon::callHooks('prepare_body_final', $hook_data);
1361
1362         return $hook_data['html'];
1363 }
1364
1365 /**
1366  * Given a HTML text and a set of filtering reasons, adds a content hiding header with the provided reasons
1367  *
1368  * Reasons are expected to have been translated already.
1369  *
1370  * @param string $html
1371  * @param array  $reasons
1372  * @return string
1373  */
1374 function apply_content_filter($html, array $reasons)
1375 {
1376         if (count($reasons)) {
1377                 $tpl = get_markup_template('wall/content_filter.tpl');
1378                 $html = replace_macros($tpl, [
1379                         '$reasons'   => $reasons,
1380                         '$rnd'       => random_string(8),
1381                         '$openclose' => L10n::t('Click to open/close'),
1382                         '$html'      => $html
1383                 ]);
1384         }
1385
1386         return $html;
1387 }
1388
1389 /**
1390  * @brief Given a text string, convert from bbcode to html and add smilie icons.
1391  *
1392  * @param string $text String with bbcode.
1393  * @return string Formattet HTML.
1394  */
1395 function prepare_text($text) {
1396         if (stristr($text, '[nosmile]')) {
1397                 $s = BBCode::convert($text);
1398         } else {
1399                 $s = Smilies::replace(BBCode::convert($text));
1400         }
1401
1402         return trim($s);
1403 }
1404
1405 /**
1406  * return array with details for categories and folders for an item
1407  *
1408  * @param array $item
1409  * @return array
1410  *
1411   * [
1412  *      [ // categories array
1413  *          {
1414  *               'name': 'category name',
1415  *               'removeurl': 'url to remove this category',
1416  *               'first': 'is the first in this array? true/false',
1417  *               'last': 'is the last in this array? true/false',
1418  *           } ,
1419  *           ....
1420  *       ],
1421  *       [ //folders array
1422  *                      {
1423  *               'name': 'folder name',
1424  *               'removeurl': 'url to remove this folder',
1425  *               'first': 'is the first in this array? true/false',
1426  *               'last': 'is the last in this array? true/false',
1427  *           } ,
1428  *           ....
1429  *       ]
1430  *  ]
1431  */
1432 function get_cats_and_terms($item)
1433 {
1434         $categories = [];
1435         $folders = [];
1436
1437         $matches = false;
1438         $first = true;
1439         $cnt = preg_match_all('/<(.*?)>/', $item['file'], $matches, PREG_SET_ORDER);
1440         if ($cnt) {
1441                 foreach ($matches as $mtch) {
1442                         $categories[] = [
1443                                 'name' => xmlify(file_tag_decode($mtch[1])),
1444                                 'url' =>  "#",
1445                                 'removeurl' => ((local_user() == $item['uid'])?'filerm/' . $item['id'] . '?f=&cat=' . xmlify(file_tag_decode($mtch[1])):""),
1446                                 'first' => $first,
1447                                 'last' => false
1448                         ];
1449                         $first = false;
1450                 }
1451         }
1452
1453         if (count($categories)) {
1454                 $categories[count($categories) - 1]['last'] = true;
1455         }
1456
1457         if (local_user() == $item['uid']) {
1458                 $matches = false;
1459                 $first = true;
1460                 $cnt = preg_match_all('/\[(.*?)\]/', $item['file'], $matches, PREG_SET_ORDER);
1461                 if ($cnt) {
1462                         foreach ($matches as $mtch) {
1463                                 $folders[] = [
1464                                         'name' => xmlify(file_tag_decode($mtch[1])),
1465                                         'url' =>  "#",
1466                                         'removeurl' => ((local_user() == $item['uid']) ? 'filerm/' . $item['id'] . '?f=&term=' . xmlify(file_tag_decode($mtch[1])) : ""),
1467                                         'first' => $first,
1468                                         'last' => false
1469                                 ];
1470                                 $first = false;
1471                         }
1472                 }
1473         }
1474
1475         if (count($folders)) {
1476                 $folders[count($folders) - 1]['last'] = true;
1477         }
1478
1479         return [$categories, $folders];
1480 }
1481
1482
1483 /**
1484  * get private link for item
1485  * @param array $item
1486  * @return boolean|array False if item has not plink, otherwise array('href'=>plink url, 'title'=>translated title)
1487  */
1488 function get_plink($item) {
1489         $a = get_app();
1490
1491         if ($a->user['nickname'] != "") {
1492                 $ret = [
1493                                 //'href' => "display/" . $a->user['nickname'] . "/" . $item['id'],
1494                                 'href' => "display/" . $item['guid'],
1495                                 'orig' => "display/" . $item['guid'],
1496                                 'title' => L10n::t('View on separate page'),
1497                                 'orig_title' => L10n::t('view on separate page'),
1498                         ];
1499
1500                 if (x($item, 'plink')) {
1501                         $ret["href"] = $a->remove_baseurl($item['plink']);
1502                         $ret["title"] = L10n::t('link to source');
1503                 }
1504
1505         } elseif (x($item, 'plink') && ($item['private'] != 1)) {
1506                 $ret = [
1507                                 'href' => $item['plink'],
1508                                 'orig' => $item['plink'],
1509                                 'title' => L10n::t('link to source'),
1510                         ];
1511         } else {
1512                 $ret = [];
1513         }
1514
1515         return $ret;
1516 }
1517
1518
1519 /**
1520  * replace html amp entity with amp char
1521  * @param string $s
1522  * @return string
1523  */
1524 function unamp($s) {
1525         return str_replace('&amp;', '&', $s);
1526 }
1527
1528
1529 /**
1530  * return number of bytes in size (K, M, G)
1531  * @param string $size_str
1532  * @return number
1533  */
1534 function return_bytes($size_str) {
1535         switch (substr ($size_str, -1)) {
1536                 case 'M': case 'm': return (int)$size_str * 1048576;
1537                 case 'K': case 'k': return (int)$size_str * 1024;
1538                 case 'G': case 'g': return (int)$size_str * 1073741824;
1539                 default: return $size_str;
1540         }
1541 }
1542
1543
1544 /**
1545  * @return string
1546  */
1547 function generate_user_guid() {
1548         $found = true;
1549         do {
1550                 $guid = System::createGUID(32);
1551                 $x = q("SELECT `uid` FROM `user` WHERE `guid` = '%s' LIMIT 1",
1552                         dbesc($guid)
1553                 );
1554                 if (!DBM::is_result($x)) {
1555                         $found = false;
1556                 }
1557         } while ($found == true);
1558
1559         return $guid;
1560 }
1561
1562
1563 /**
1564  * @param string $s
1565  * @param boolean $strip_padding
1566  * @return string
1567  */
1568 function base64url_encode($s, $strip_padding = false) {
1569
1570         $s = strtr(base64_encode($s), '+/', '-_');
1571
1572         if ($strip_padding) {
1573                 $s = str_replace('=','',$s);
1574         }
1575
1576         return $s;
1577 }
1578
1579 /**
1580  * @param string $s
1581  * @return string
1582  */
1583 function base64url_decode($s) {
1584
1585         if (is_array($s)) {
1586                 logger('base64url_decode: illegal input: ' . print_r(debug_backtrace(), true));
1587                 return $s;
1588         }
1589
1590 /*
1591  *  // Placeholder for new rev of salmon which strips base64 padding.
1592  *  // PHP base64_decode handles the un-padded input without requiring this step
1593  *  // Uncomment if you find you need it.
1594  *
1595  *      $l = strlen($s);
1596  *      if (!strpos($s,'=')) {
1597  *              $m = $l % 4;
1598  *              if ($m == 2)
1599  *                      $s .= '==';
1600  *              if ($m == 3)
1601  *                      $s .= '=';
1602  *      }
1603  *
1604  */
1605
1606         return base64_decode(strtr($s,'-_','+/'));
1607 }
1608
1609
1610 /**
1611  * return div element with class 'clear'
1612  * @return string
1613  * @deprecated
1614  */
1615 function cleardiv() {
1616         return '<div class="clear"></div>';
1617 }
1618
1619
1620 function bb_translate_video($s) {
1621
1622         $matches = null;
1623         $r = preg_match_all("/\[video\](.*?)\[\/video\]/ism",$s,$matches,PREG_SET_ORDER);
1624         if ($r) {
1625                 foreach ($matches as $mtch) {
1626                         if ((stristr($mtch[1], 'youtube')) || (stristr($mtch[1], 'youtu.be'))) {
1627                                 $s = str_replace($mtch[0], '[youtube]' . $mtch[1] . '[/youtube]', $s);
1628                         } elseif (stristr($mtch[1], 'vimeo')) {
1629                                 $s = str_replace($mtch[0], '[vimeo]' . $mtch[1] . '[/vimeo]', $s);
1630                         }
1631                 }
1632         }
1633         return $s;
1634 }
1635
1636 function html2bb_video($s) {
1637
1638         $s = preg_replace('#<object[^>]+>(.*?)https?://www.youtube.com/((?:v|cp)/[A-Za-z0-9\-_=]+)(.*?)</object>#ism',
1639                         '[youtube]$2[/youtube]', $s);
1640
1641         $s = preg_replace('#<iframe[^>](.*?)https?://www.youtube.com/embed/([A-Za-z0-9\-_=]+)(.*?)</iframe>#ism',
1642                         '[youtube]$2[/youtube]', $s);
1643
1644         $s = preg_replace('#<iframe[^>](.*?)https?://player.vimeo.com/video/([0-9]+)(.*?)</iframe>#ism',
1645                         '[vimeo]$2[/vimeo]', $s);
1646
1647         return $s;
1648 }
1649
1650 /**
1651  * apply xmlify() to all values of array $val, recursively
1652  * @param array $val
1653  * @return array
1654  */
1655 function array_xmlify($val){
1656         if (is_bool($val)) {
1657                 return $val?"true":"false";
1658         } elseif (is_array($val)) {
1659                 return array_map('array_xmlify', $val);
1660         }
1661         return xmlify((string) $val);
1662 }
1663
1664
1665 /**
1666  * transform link href and img src from relative to absolute
1667  *
1668  * @param string $text
1669  * @param string $base base url
1670  * @return string
1671  */
1672 function reltoabs($text, $base) {
1673         if (empty($base)) {
1674                 return $text;
1675         }
1676
1677         $base = rtrim($base,'/');
1678
1679         $base2 = $base . "/";
1680
1681         // Replace links
1682         $pattern = "/<a([^>]*) href=\"(?!http|https|\/)([^\"]*)\"/";
1683         $replace = "<a\${1} href=\"" . $base2 . "\${2}\"";
1684         $text = preg_replace($pattern, $replace, $text);
1685
1686         $pattern = "/<a([^>]*) href=\"(?!http|https)([^\"]*)\"/";
1687         $replace = "<a\${1} href=\"" . $base . "\${2}\"";
1688         $text = preg_replace($pattern, $replace, $text);
1689
1690         // Replace images
1691         $pattern = "/<img([^>]*) src=\"(?!http|https|\/)([^\"]*)\"/";
1692         $replace = "<img\${1} src=\"" . $base2 . "\${2}\"";
1693         $text = preg_replace($pattern, $replace, $text);
1694
1695         $pattern = "/<img([^>]*) src=\"(?!http|https)([^\"]*)\"/";
1696         $replace = "<img\${1} src=\"" . $base . "\${2}\"";
1697         $text = preg_replace($pattern, $replace, $text);
1698
1699
1700         // Done
1701         return $text;
1702 }
1703
1704 /**
1705  * get translated item type
1706  *
1707  * @param array $itme
1708  * @return string
1709  */
1710 function item_post_type($item) {
1711         if (!empty($item['event-id'])) {
1712                 return L10n::t('event');
1713         } elseif (!empty($item['resource-id'])) {
1714                 return L10n::t('photo');
1715         } elseif (!empty($item['verb']) && $item['verb'] !== ACTIVITY_POST) {
1716                 return L10n::t('activity');
1717         } elseif ($item['id'] != $item['parent']) {
1718                 return L10n::t('comment');
1719         }
1720
1721         return L10n::t('post');
1722 }
1723
1724 // post categories and "save to file" use the same item.file table for storage.
1725 // We will differentiate the different uses by wrapping categories in angle brackets
1726 // and save to file categories in square brackets.
1727 // To do this we need to escape these characters if they appear in our tag.
1728
1729 function file_tag_encode($s) {
1730         return str_replace(['<','>','[',']'],['%3c','%3e','%5b','%5d'],$s);
1731 }
1732
1733 function file_tag_decode($s) {
1734         return str_replace(['%3c', '%3e', '%5b', '%5d'], ['<', '>', '[', ']'], $s);
1735 }
1736
1737 function file_tag_file_query($table,$s,$type = 'file') {
1738
1739         if ($type == 'file') {
1740                 $str = preg_quote('[' . str_replace('%', '%%', file_tag_encode($s)) . ']');
1741         } else {
1742                 $str = preg_quote('<' . str_replace('%', '%%', file_tag_encode($s)) . '>');
1743         }
1744         return " AND " . (($table) ? dbesc($table) . '.' : '') . "file regexp '" . dbesc($str) . "' ";
1745 }
1746
1747 // ex. given music,video return <music><video> or [music][video]
1748 function file_tag_list_to_file($list, $type = 'file') {
1749         $tag_list = '';
1750         if (strlen($list)) {
1751                 $list_array = explode(",",$list);
1752                 if ($type == 'file') {
1753                         $lbracket = '[';
1754                         $rbracket = ']';
1755                 } else {
1756                         $lbracket = '<';
1757                         $rbracket = '>';
1758                 }
1759
1760                 foreach ($list_array as $item) {
1761                         if (strlen($item)) {
1762                                 $tag_list .= $lbracket . file_tag_encode(trim($item))  . $rbracket;
1763                         }
1764                 }
1765         }
1766         return $tag_list;
1767 }
1768
1769 // ex. given <music><video>[friends], return music,video or friends
1770 function file_tag_file_to_list($file, $type = 'file') {
1771         $matches = false;
1772         $list = '';
1773         if ($type == 'file') {
1774                 $cnt = preg_match_all('/\[(.*?)\]/', $file, $matches, PREG_SET_ORDER);
1775         } else {
1776                 $cnt = preg_match_all('/<(.*?)>/', $file, $matches, PREG_SET_ORDER);
1777         }
1778         if ($cnt) {
1779                 foreach ($matches as $mtch) {
1780                         if (strlen($list)) {
1781                                 $list .= ',';
1782                         }
1783                         $list .= file_tag_decode($mtch[1]);
1784                 }
1785         }
1786
1787         return $list;
1788 }
1789
1790 function file_tag_update_pconfig($uid, $file_old, $file_new, $type = 'file') {
1791         // $file_old - categories previously associated with an item
1792         // $file_new - new list of categories for an item
1793
1794         if (!intval($uid)) {
1795                 return false;
1796         } elseif ($file_old == $file_new) {
1797                 return true;
1798         }
1799
1800         $saved = PConfig::get($uid, 'system', 'filetags');
1801         if (strlen($saved)) {
1802                 if ($type == 'file') {
1803                         $lbracket = '[';
1804                         $rbracket = ']';
1805                         $termtype = TERM_FILE;
1806                 } else {
1807                         $lbracket = '<';
1808                         $rbracket = '>';
1809                         $termtype = TERM_CATEGORY;
1810                 }
1811
1812                 $filetags_updated = $saved;
1813
1814                 // check for new tags to be added as filetags in pconfig
1815                 $new_tags = [];
1816                 $check_new_tags = explode(",",file_tag_file_to_list($file_new,$type));
1817
1818                 foreach ($check_new_tags as $tag) {
1819                         if (!stristr($saved,$lbracket . file_tag_encode($tag) . $rbracket)) {
1820                                 $new_tags[] = $tag;
1821                         }
1822                 }
1823
1824                 $filetags_updated .= file_tag_list_to_file(implode(",",$new_tags),$type);
1825
1826                 // check for deleted tags to be removed from filetags in pconfig
1827                 $deleted_tags = [];
1828                 $check_deleted_tags = explode(",",file_tag_file_to_list($file_old,$type));
1829
1830                 foreach ($check_deleted_tags as $tag) {
1831                         if (!stristr($file_new,$lbracket . file_tag_encode($tag) . $rbracket)) {
1832                                 $deleted_tags[] = $tag;
1833                         }
1834                 }
1835
1836                 foreach ($deleted_tags as $key => $tag) {
1837                         $r = q("SELECT `oid` FROM `term` WHERE `term` = '%s' AND `otype` = %d AND `type` = %d AND `uid` = %d",
1838                                 dbesc($tag),
1839                                 intval(TERM_OBJ_POST),
1840                                 intval($termtype),
1841                                 intval($uid));
1842
1843                         if (DBM::is_result($r)) {
1844                                 unset($deleted_tags[$key]);
1845                         } else {
1846                                 $filetags_updated = str_replace($lbracket . file_tag_encode($tag) . $rbracket,'',$filetags_updated);
1847                         }
1848                 }
1849
1850                 if ($saved != $filetags_updated) {
1851                         PConfig::set($uid, 'system', 'filetags', $filetags_updated);
1852                 }
1853                 return true;
1854         } elseif (strlen($file_new)) {
1855                 PConfig::set($uid, 'system', 'filetags', $file_new);
1856         }
1857         return true;
1858 }
1859
1860 function file_tag_save_file($uid, $item_id, $file)
1861 {
1862         if (!intval($uid)) {
1863                 return false;
1864         }
1865
1866         $item = Item::selectFirst(['file'], ['id' => $item_id, 'uid' => $uid]);
1867         if (DBM::is_result($item)) {
1868                 if (!stristr($item['file'],'[' . file_tag_encode($file) . ']')) {
1869                         $fields = ['file' => $item['file'] . '[' . file_tag_encode($file) . ']'];
1870                         Item::update($fields, ['id' => $item_id]);
1871                 }
1872                 $saved = PConfig::get($uid, 'system', 'filetags');
1873                 if (!strlen($saved) || !stristr($saved, '[' . file_tag_encode($file) . ']')) {
1874                         PConfig::set($uid, 'system', 'filetags', $saved . '[' . file_tag_encode($file) . ']');
1875                 }
1876                 info(L10n::t('Item filed'));
1877         }
1878         return true;
1879 }
1880
1881 function file_tag_unsave_file($uid, $item_id, $file, $cat = false)
1882 {
1883         if (!intval($uid)) {
1884                 return false;
1885         }
1886
1887         if ($cat == true) {
1888                 $pattern = '<' . file_tag_encode($file) . '>' ;
1889                 $termtype = TERM_CATEGORY;
1890         } else {
1891                 $pattern = '[' . file_tag_encode($file) . ']' ;
1892                 $termtype = TERM_FILE;
1893         }
1894
1895         $item = Item::selectFirst(['file'], ['id' => $item_id, 'uid' => $uid]);
1896         if (!DBM::is_result($item)) {
1897                 return false;
1898         }
1899
1900         $fields = ['file' => str_replace($pattern,'',$item['file'])];
1901         Item::update($fields, ['id' => $item_id]);
1902
1903         $r = q("SELECT `oid` FROM `term` WHERE `term` = '%s' AND `otype` = %d AND `type` = %d AND `uid` = %d",
1904                 dbesc($file),
1905                 intval(TERM_OBJ_POST),
1906                 intval($termtype),
1907                 intval($uid)
1908         );
1909         if (!DBM::is_result($r)) {
1910                 $saved = PConfig::get($uid, 'system', 'filetags');
1911                 PConfig::set($uid, 'system', 'filetags', str_replace($pattern, '', $saved));
1912         }
1913
1914         return true;
1915 }
1916
1917 function normalise_openid($s) {
1918         return trim(str_replace(['http://', 'https://'], ['', ''], $s), '/');
1919 }
1920
1921
1922 function undo_post_tagging($s) {
1923         $matches = null;
1924         $cnt = preg_match_all('/([!#@])\[url=(.*?)\](.*?)\[\/url\]/ism', $s, $matches, PREG_SET_ORDER);
1925         if ($cnt) {
1926                 foreach ($matches as $mtch) {
1927                         if (in_array($mtch[1], ['!', '@'])) {
1928                                 $contact = Contact::getDetailsByURL($mtch[2]);
1929                                 $mtch[3] = empty($contact['addr']) ? $mtch[2] : $contact['addr'];
1930                         }
1931                         $s = str_replace($mtch[0], $mtch[1] . $mtch[3],$s);
1932                 }
1933         }
1934         return $s;
1935 }
1936
1937 function protect_sprintf($s) {
1938         return str_replace('%', '%%', $s);
1939 }
1940
1941 /// @TODO Rewrite this
1942 function is_a_date_arg($s) {
1943         $i = intval($s);
1944
1945         if ($i > 1900) {
1946                 $y = date('Y');
1947
1948                 if ($i <= $y + 1 && strpos($s, '-') == 4) {
1949                         $m = intval(substr($s, 5));
1950
1951                         if ($m > 0 && $m <= 12) {
1952                                 return true;
1953                         }
1954                 }
1955         }
1956
1957         return false;
1958 }
1959
1960 /**
1961  * remove intentation from a text
1962  */
1963 function deindent($text, $chr = "[\t ]", $count = NULL) {
1964         $lines = explode("\n", $text);
1965
1966         if (is_null($count)) {
1967                 $m = [];
1968                 $k = 0;
1969                 while ($k < count($lines) && strlen($lines[$k]) == 0) {
1970                         $k++;
1971                 }
1972                 preg_match("|^" . $chr . "*|", $lines[$k], $m);
1973                 $count = strlen($m[0]);
1974         }
1975
1976         for ($k = 0; $k < count($lines); $k++) {
1977                 $lines[$k] = preg_replace("|^" . $chr . "{" . $count . "}|", "", $lines[$k]);
1978         }
1979
1980         return implode("\n", $lines);
1981 }
1982
1983 function formatBytes($bytes, $precision = 2) {
1984         $units = ['B', 'KB', 'MB', 'GB', 'TB'];
1985
1986         $bytes = max($bytes, 0);
1987         $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
1988         $pow = min($pow, count($units) - 1);
1989
1990         $bytes /= pow(1024, $pow);
1991
1992         return round($bytes, $precision) . ' ' . $units[$pow];
1993 }
1994
1995 /**
1996  * @brief translate and format the networkname of a contact
1997  *
1998  * @param string $network
1999  *      Networkname of the contact (e.g. dfrn, rss and so on)
2000  * @param sting $url
2001  *      The contact url
2002  * @return string
2003  */
2004 function format_network_name($network, $url = 0) {
2005         if ($network != "") {
2006                 if ($url != "") {
2007                         $network_name = '<a href="'.$url.'">'.ContactSelector::networkToName($network, $url)."</a>";
2008                 } else {
2009                         $network_name = ContactSelector::networkToName($network);
2010                 }
2011
2012                 return $network_name;
2013         }
2014 }
2015
2016 /**
2017  * @brief Syntax based code highlighting for popular languages.
2018  * @param string $s Code block
2019  * @param string $lang Programming language
2020  * @return string Formated html
2021  */
2022 function text_highlight($s, $lang) {
2023         if ($lang === 'js') {
2024                 $lang = 'javascript';
2025         }
2026
2027         if ($lang === 'bash') {
2028                 $lang = 'sh';
2029         }
2030
2031         // @TODO: Replace Text_Highlighter_Renderer_Html by scrivo/highlight.php
2032
2033         // Autoload the library to make constants available
2034         class_exists('Text_Highlighter_Renderer_Html');
2035
2036         $options = [
2037                 'numbers' => HL_NUMBERS_LI,
2038                 'tabsize' => 4,
2039         ];
2040
2041         $tag_added = false;
2042         $s = trim(html_entity_decode($s, ENT_COMPAT));
2043         $s = str_replace('    ', "\t", $s);
2044
2045         /*
2046          * The highlighter library insists on an opening php tag for php code blocks. If
2047          * it isn't present, nothing is highlighted. So we're going to see if it's present.
2048          * If not, we'll add it, and then quietly remove it after we get the processed output back.
2049          */
2050         if ($lang === 'php' && strpos($s, '<?php') !== 0) {
2051                 $s = '<?php' . "\n" . $s;
2052                 $tag_added = true;
2053         }
2054
2055         $renderer = new Text_Highlighter_Renderer_Html($options);
2056         $factory = new Text_Highlighter();
2057         $hl = $factory->factory($lang);
2058         $hl->setRenderer($renderer);
2059         $o = $hl->highlight($s);
2060         $o = str_replace("\n", '', $o);
2061
2062         if ($tag_added) {
2063                 $b = substr($o, 0, strpos($o, '<li>'));
2064                 $e = substr($o, strpos($o, '</li>'));
2065                 $o = $b . $e;
2066         }
2067
2068         return '<code>' . $o . '</code>';
2069 }