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