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