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