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