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