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