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