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