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