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