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