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