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