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