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