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