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