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