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