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