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