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