]> git.mxchange.org Git - friendica.git/blob - include/text.php
adapted 300px image size for default avatar
[friendica.git] / include / text.php
1 <?php
2 /**
3  * @file include/text.php
4  */
5
6 use Friendica\App;
7 use Friendica\Content\ContactSelector;
8 use Friendica\Content\Feature;
9 use Friendica\Content\Smilies;
10 use Friendica\Content\Text\BBCode;
11 use Friendica\Core\Addon;
12 use Friendica\Core\Config;
13 use Friendica\Core\L10n;
14 use Friendica\Core\PConfig;
15 use Friendica\Core\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->getTemplateEngine();
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->saveTimestamp($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->getTemplateEngine();
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->saveTimestamp($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->saveTimestamp($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->saveTimestamp($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                         }
1195
1196                         $url_parts = explode('/', $the_url);
1197                         $id = end($url_parts);
1198                         $as .= replace_macros(get_markup_template('video_top.tpl'), [
1199                                 '$video' => [
1200                                         'id'     => $id,
1201                                         'title'  => L10n::t('View Video'),
1202                                         'src'    => $the_url,
1203                                         'mime'   => $mime,
1204                                 ],
1205                         ]);
1206                 }
1207
1208                 $filetype = strtolower(substr($mime, 0, strpos($mime, '/')));
1209                 if ($filetype) {
1210                         $filesubtype = strtolower(substr($mime, strpos($mime, '/') + 1));
1211                         $filesubtype = str_replace('.', '-', $filesubtype);
1212                 } else {
1213                         $filetype = 'unkn';
1214                         $filesubtype = 'unkn';
1215                 }
1216
1217                 $title = escape_tags(trim(!empty($mtch[4]) ? $mtch[4] : $mtch[1]));
1218                 $title .= ' ' . $mtch[2] . ' ' . L10n::t('bytes');
1219
1220                 $icon = '<div class="attachtype icon s22 type-' . $filetype . ' subtype-' . $filesubtype . '"></div>';
1221                 $as .= '<a href="' . strip_tags($the_url) . '" title="' . $title . '" class="attachlink" target="_blank" >' . $icon . '</a>';
1222         }
1223
1224         if ($as != '') {
1225                 $s .= '<div class="body-attach">'.$as.'<div class="clear"></div></div>';
1226         }
1227
1228         // Map.
1229         if (strpos($s, '<div class="map">') !== false && x($item, 'coord')) {
1230                 $x = Map::byCoordinates(trim($item['coord']));
1231                 if ($x) {
1232                         $s = preg_replace('/\<div class\=\"map\"\>/', '$0' . $x, $s);
1233                 }
1234         }
1235
1236
1237         // Look for spoiler.
1238         $spoilersearch = '<blockquote class="spoiler">';
1239
1240         // Remove line breaks before the spoiler.
1241         while ((strpos($s, "\n" . $spoilersearch) !== false)) {
1242                 $s = str_replace("\n" . $spoilersearch, $spoilersearch, $s);
1243         }
1244         while ((strpos($s, "<br />" . $spoilersearch) !== false)) {
1245                 $s = str_replace("<br />" . $spoilersearch, $spoilersearch, $s);
1246         }
1247
1248         while ((strpos($s, $spoilersearch) !== false)) {
1249                 $pos = strpos($s, $spoilersearch);
1250                 $rnd = random_string(8);
1251                 $spoilerreplace = '<br /> <span id="spoiler-wrap-' . $rnd . '" class="spoiler-wrap fakelink" onclick="openClose(\'spoiler-' . $rnd . '\');">' . L10n::t('Click to open/close') . '</span>'.
1252                                         '<blockquote class="spoiler" id="spoiler-' . $rnd . '" style="display: none;">';
1253                 $s = substr($s, 0, $pos) . $spoilerreplace . substr($s, $pos + strlen($spoilersearch));
1254         }
1255
1256         // Look for quote with author.
1257         $authorsearch = '<blockquote class="author">';
1258
1259         while ((strpos($s, $authorsearch) !== false)) {
1260                 $pos = strpos($s, $authorsearch);
1261                 $rnd = random_string(8);
1262                 $authorreplace = '<br /> <span id="author-wrap-' . $rnd . '" class="author-wrap fakelink" onclick="openClose(\'author-' . $rnd . '\');">' . L10n::t('Click to open/close') . '</span>'.
1263                                         '<blockquote class="author" id="author-' . $rnd . '" style="display: block;">';
1264                 $s = substr($s, 0, $pos) . $authorreplace . substr($s, $pos + strlen($authorsearch));
1265         }
1266
1267         // Replace friendica image url size with theme preference.
1268         if (x($a->theme_info, 'item_image_size')){
1269                 $ps = $a->theme_info['item_image_size'];
1270                 $s = preg_replace('|(<img[^>]+src="[^"]+/photo/[0-9a-f]+)-[0-9]|', "$1-" . $ps, $s);
1271         }
1272
1273         $s = apply_content_filter($s, $filter_reasons);
1274
1275         $hook_data = ['item' => $item, 'html' => $s];
1276         Addon::callHooks('prepare_body_final', $hook_data);
1277
1278         return $hook_data['html'];
1279 }
1280
1281 /**
1282  * Given a HTML text and a set of filtering reasons, adds a content hiding header with the provided reasons
1283  *
1284  * Reasons are expected to have been translated already.
1285  *
1286  * @param string $html
1287  * @param array  $reasons
1288  * @return string
1289  */
1290 function apply_content_filter($html, array $reasons)
1291 {
1292         if (count($reasons)) {
1293                 $tpl = get_markup_template('wall/content_filter.tpl');
1294                 $html = replace_macros($tpl, [
1295                         '$reasons'   => $reasons,
1296                         '$rnd'       => random_string(8),
1297                         '$openclose' => L10n::t('Click to open/close'),
1298                         '$html'      => $html
1299                 ]);
1300         }
1301
1302         return $html;
1303 }
1304
1305 /**
1306  * @brief Given a text string, convert from bbcode to html and add smilie icons.
1307  *
1308  * @param string $text String with bbcode.
1309  * @return string Formattet HTML.
1310  */
1311 function prepare_text($text) {
1312         if (stristr($text, '[nosmile]')) {
1313                 $s = BBCode::convert($text);
1314         } else {
1315                 $s = Smilies::replace(BBCode::convert($text));
1316         }
1317
1318         return trim($s);
1319 }
1320
1321 /**
1322  * return array with details for categories and folders for an item
1323  *
1324  * @param array $item
1325  * @return array
1326  *
1327   * [
1328  *      [ // categories array
1329  *          {
1330  *               'name': 'category name',
1331  *               'removeurl': 'url to remove this category',
1332  *               'first': 'is the first in this array? true/false',
1333  *               'last': 'is the last in this array? true/false',
1334  *           } ,
1335  *           ....
1336  *       ],
1337  *       [ //folders array
1338  *                      {
1339  *               'name': 'folder name',
1340  *               'removeurl': 'url to remove this folder',
1341  *               'first': 'is the first in this array? true/false',
1342  *               'last': 'is the last in this array? true/false',
1343  *           } ,
1344  *           ....
1345  *       ]
1346  *  ]
1347  */
1348 function get_cats_and_terms($item)
1349 {
1350         $categories = [];
1351         $folders = [];
1352
1353         $matches = false;
1354         $first = true;
1355         $cnt = preg_match_all('/<(.*?)>/', $item['file'], $matches, PREG_SET_ORDER);
1356         if ($cnt) {
1357                 foreach ($matches as $mtch) {
1358                         $categories[] = [
1359                                 'name' => xmlify(file_tag_decode($mtch[1])),
1360                                 'url' =>  "#",
1361                                 'removeurl' => ((local_user() == $item['uid'])?'filerm/' . $item['id'] . '?f=&cat=' . xmlify(file_tag_decode($mtch[1])):""),
1362                                 'first' => $first,
1363                                 'last' => false
1364                         ];
1365                         $first = false;
1366                 }
1367         }
1368
1369         if (count($categories)) {
1370                 $categories[count($categories) - 1]['last'] = true;
1371         }
1372
1373         if (local_user() == $item['uid']) {
1374                 $matches = false;
1375                 $first = true;
1376                 $cnt = preg_match_all('/\[(.*?)\]/', $item['file'], $matches, PREG_SET_ORDER);
1377                 if ($cnt) {
1378                         foreach ($matches as $mtch) {
1379                                 $folders[] = [
1380                                         'name' => xmlify(file_tag_decode($mtch[1])),
1381                                         'url' =>  "#",
1382                                         'removeurl' => ((local_user() == $item['uid']) ? 'filerm/' . $item['id'] . '?f=&term=' . xmlify(file_tag_decode($mtch[1])) : ""),
1383                                         'first' => $first,
1384                                         'last' => false
1385                                 ];
1386                                 $first = false;
1387                         }
1388                 }
1389         }
1390
1391         if (count($folders)) {
1392                 $folders[count($folders) - 1]['last'] = true;
1393         }
1394
1395         return [$categories, $folders];
1396 }
1397
1398
1399 /**
1400  * get private link for item
1401  * @param array $item
1402  * @return boolean|array False if item has not plink, otherwise array('href'=>plink url, 'title'=>translated title)
1403  */
1404 function get_plink($item) {
1405         $a = get_app();
1406
1407         if ($a->user['nickname'] != "") {
1408                 $ret = [
1409                                 //'href' => "display/" . $a->user['nickname'] . "/" . $item['id'],
1410                                 'href' => "display/" . $item['guid'],
1411                                 'orig' => "display/" . $item['guid'],
1412                                 'title' => L10n::t('View on separate page'),
1413                                 'orig_title' => L10n::t('view on separate page'),
1414                         ];
1415
1416                 if (x($item, 'plink')) {
1417                         $ret["href"] = $a->removeBaseURL($item['plink']);
1418                         $ret["title"] = L10n::t('link to source');
1419                 }
1420
1421         } elseif (x($item, 'plink') && ($item['private'] != 1)) {
1422                 $ret = [
1423                                 'href' => $item['plink'],
1424                                 'orig' => $item['plink'],
1425                                 'title' => L10n::t('link to source'),
1426                         ];
1427         } else {
1428                 $ret = [];
1429         }
1430
1431         return $ret;
1432 }
1433
1434
1435 /**
1436  * replace html amp entity with amp char
1437  * @param string $s
1438  * @return string
1439  */
1440 function unamp($s) {
1441         return str_replace('&amp;', '&', $s);
1442 }
1443
1444
1445 /**
1446  * return number of bytes in size (K, M, G)
1447  * @param string $size_str
1448  * @return number
1449  */
1450 function return_bytes($size_str) {
1451         switch (substr ($size_str, -1)) {
1452                 case 'M': case 'm': return (int)$size_str * 1048576;
1453                 case 'K': case 'k': return (int)$size_str * 1024;
1454                 case 'G': case 'g': return (int)$size_str * 1073741824;
1455                 default: return $size_str;
1456         }
1457 }
1458
1459 /**
1460  * @param string $s
1461  * @param boolean $strip_padding
1462  * @return string
1463  */
1464 function base64url_encode($s, $strip_padding = false) {
1465
1466         $s = strtr(base64_encode($s), '+/', '-_');
1467
1468         if ($strip_padding) {
1469                 $s = str_replace('=','',$s);
1470         }
1471
1472         return $s;
1473 }
1474
1475 /**
1476  * @param string $s
1477  * @return string
1478  */
1479 function base64url_decode($s) {
1480
1481         if (is_array($s)) {
1482                 logger('base64url_decode: illegal input: ' . print_r(debug_backtrace(), true));
1483                 return $s;
1484         }
1485
1486 /*
1487  *  // Placeholder for new rev of salmon which strips base64 padding.
1488  *  // PHP base64_decode handles the un-padded input without requiring this step
1489  *  // Uncomment if you find you need it.
1490  *
1491  *      $l = strlen($s);
1492  *      if (!strpos($s,'=')) {
1493  *              $m = $l % 4;
1494  *              if ($m == 2)
1495  *                      $s .= '==';
1496  *              if ($m == 3)
1497  *                      $s .= '=';
1498  *      }
1499  *
1500  */
1501
1502         return base64_decode(strtr($s,'-_','+/'));
1503 }
1504
1505
1506 /**
1507  * return div element with class 'clear'
1508  * @return string
1509  * @deprecated
1510  */
1511 function cleardiv() {
1512         return '<div class="clear"></div>';
1513 }
1514
1515
1516 function bb_translate_video($s) {
1517
1518         $matches = null;
1519         $r = preg_match_all("/\[video\](.*?)\[\/video\]/ism",$s,$matches,PREG_SET_ORDER);
1520         if ($r) {
1521                 foreach ($matches as $mtch) {
1522                         if ((stristr($mtch[1], 'youtube')) || (stristr($mtch[1], 'youtu.be'))) {
1523                                 $s = str_replace($mtch[0], '[youtube]' . $mtch[1] . '[/youtube]', $s);
1524                         } elseif (stristr($mtch[1], 'vimeo')) {
1525                                 $s = str_replace($mtch[0], '[vimeo]' . $mtch[1] . '[/vimeo]', $s);
1526                         }
1527                 }
1528         }
1529         return $s;
1530 }
1531
1532 function html2bb_video($s) {
1533
1534         $s = preg_replace('#<object[^>]+>(.*?)https?://www.youtube.com/((?:v|cp)/[A-Za-z0-9\-_=]+)(.*?)</object>#ism',
1535                         '[youtube]$2[/youtube]', $s);
1536
1537         $s = preg_replace('#<iframe[^>](.*?)https?://www.youtube.com/embed/([A-Za-z0-9\-_=]+)(.*?)</iframe>#ism',
1538                         '[youtube]$2[/youtube]', $s);
1539
1540         $s = preg_replace('#<iframe[^>](.*?)https?://player.vimeo.com/video/([0-9]+)(.*?)</iframe>#ism',
1541                         '[vimeo]$2[/vimeo]', $s);
1542
1543         return $s;
1544 }
1545
1546 /**
1547  * apply xmlify() to all values of array $val, recursively
1548  * @param array $val
1549  * @return array
1550  */
1551 function array_xmlify($val){
1552         if (is_bool($val)) {
1553                 return $val?"true":"false";
1554         } elseif (is_array($val)) {
1555                 return array_map('array_xmlify', $val);
1556         }
1557         return xmlify((string) $val);
1558 }
1559
1560
1561 /**
1562  * transform link href and img src from relative to absolute
1563  *
1564  * @param string $text
1565  * @param string $base base url
1566  * @return string
1567  */
1568 function reltoabs($text, $base) {
1569         if (empty($base)) {
1570                 return $text;
1571         }
1572
1573         $base = rtrim($base,'/');
1574
1575         $base2 = $base . "/";
1576
1577         // Replace links
1578         $pattern = "/<a([^>]*) href=\"(?!http|https|\/)([^\"]*)\"/";
1579         $replace = "<a\${1} href=\"" . $base2 . "\${2}\"";
1580         $text = preg_replace($pattern, $replace, $text);
1581
1582         $pattern = "/<a([^>]*) href=\"(?!http|https)([^\"]*)\"/";
1583         $replace = "<a\${1} href=\"" . $base . "\${2}\"";
1584         $text = preg_replace($pattern, $replace, $text);
1585
1586         // Replace images
1587         $pattern = "/<img([^>]*) src=\"(?!http|https|\/)([^\"]*)\"/";
1588         $replace = "<img\${1} src=\"" . $base2 . "\${2}\"";
1589         $text = preg_replace($pattern, $replace, $text);
1590
1591         $pattern = "/<img([^>]*) src=\"(?!http|https)([^\"]*)\"/";
1592         $replace = "<img\${1} src=\"" . $base . "\${2}\"";
1593         $text = preg_replace($pattern, $replace, $text);
1594
1595
1596         // Done
1597         return $text;
1598 }
1599
1600 /**
1601  * get translated item type
1602  *
1603  * @param array $itme
1604  * @return string
1605  */
1606 function item_post_type($item) {
1607         if (!empty($item['event-id'])) {
1608                 return L10n::t('event');
1609         } elseif (!empty($item['resource-id'])) {
1610                 return L10n::t('photo');
1611         } elseif (!empty($item['verb']) && $item['verb'] !== ACTIVITY_POST) {
1612                 return L10n::t('activity');
1613         } elseif ($item['id'] != $item['parent']) {
1614                 return L10n::t('comment');
1615         }
1616
1617         return L10n::t('post');
1618 }
1619
1620 // post categories and "save to file" use the same item.file table for storage.
1621 // We will differentiate the different uses by wrapping categories in angle brackets
1622 // and save to file categories in square brackets.
1623 // To do this we need to escape these characters if they appear in our tag.
1624
1625 function file_tag_encode($s) {
1626         return str_replace(['<','>','[',']'],['%3c','%3e','%5b','%5d'],$s);
1627 }
1628
1629 function file_tag_decode($s) {
1630         return str_replace(['%3c', '%3e', '%5b', '%5d'], ['<', '>', '[', ']'], $s);
1631 }
1632
1633 function file_tag_file_query($table,$s,$type = 'file') {
1634
1635         if ($type == 'file') {
1636                 $str = preg_quote('[' . str_replace('%', '%%', file_tag_encode($s)) . ']');
1637         } else {
1638                 $str = preg_quote('<' . str_replace('%', '%%', file_tag_encode($s)) . '>');
1639         }
1640         return " AND " . (($table) ? DBA::escape($table) . '.' : '') . "file regexp '" . DBA::escape($str) . "' ";
1641 }
1642
1643 // ex. given music,video return <music><video> or [music][video]
1644 function file_tag_list_to_file($list, $type = 'file') {
1645         $tag_list = '';
1646         if (strlen($list)) {
1647                 $list_array = explode(",",$list);
1648                 if ($type == 'file') {
1649                         $lbracket = '[';
1650                         $rbracket = ']';
1651                 } else {
1652                         $lbracket = '<';
1653                         $rbracket = '>';
1654                 }
1655
1656                 foreach ($list_array as $item) {
1657                         if (strlen($item)) {
1658                                 $tag_list .= $lbracket . file_tag_encode(trim($item))  . $rbracket;
1659                         }
1660                 }
1661         }
1662         return $tag_list;
1663 }
1664
1665 // ex. given <music><video>[friends], return music,video or friends
1666 function file_tag_file_to_list($file, $type = 'file') {
1667         $matches = false;
1668         $list = '';
1669         if ($type == 'file') {
1670                 $cnt = preg_match_all('/\[(.*?)\]/', $file, $matches, PREG_SET_ORDER);
1671         } else {
1672                 $cnt = preg_match_all('/<(.*?)>/', $file, $matches, PREG_SET_ORDER);
1673         }
1674         if ($cnt) {
1675                 foreach ($matches as $mtch) {
1676                         if (strlen($list)) {
1677                                 $list .= ',';
1678                         }
1679                         $list .= file_tag_decode($mtch[1]);
1680                 }
1681         }
1682
1683         return $list;
1684 }
1685
1686 function file_tag_update_pconfig($uid, $file_old, $file_new, $type = 'file') {
1687         // $file_old - categories previously associated with an item
1688         // $file_new - new list of categories for an item
1689
1690         if (!intval($uid)) {
1691                 return false;
1692         } elseif ($file_old == $file_new) {
1693                 return true;
1694         }
1695
1696         $saved = PConfig::get($uid, 'system', 'filetags');
1697         if (strlen($saved)) {
1698                 if ($type == 'file') {
1699                         $lbracket = '[';
1700                         $rbracket = ']';
1701                         $termtype = TERM_FILE;
1702                 } else {
1703                         $lbracket = '<';
1704                         $rbracket = '>';
1705                         $termtype = TERM_CATEGORY;
1706                 }
1707
1708                 $filetags_updated = $saved;
1709
1710                 // check for new tags to be added as filetags in pconfig
1711                 $new_tags = [];
1712                 $check_new_tags = explode(",",file_tag_file_to_list($file_new,$type));
1713
1714                 foreach ($check_new_tags as $tag) {
1715                         if (!stristr($saved,$lbracket . file_tag_encode($tag) . $rbracket)) {
1716                                 $new_tags[] = $tag;
1717                         }
1718                 }
1719
1720                 $filetags_updated .= file_tag_list_to_file(implode(",",$new_tags),$type);
1721
1722                 // check for deleted tags to be removed from filetags in pconfig
1723                 $deleted_tags = [];
1724                 $check_deleted_tags = explode(",",file_tag_file_to_list($file_old,$type));
1725
1726                 foreach ($check_deleted_tags as $tag) {
1727                         if (!stristr($file_new,$lbracket . file_tag_encode($tag) . $rbracket)) {
1728                                 $deleted_tags[] = $tag;
1729                         }
1730                 }
1731
1732                 foreach ($deleted_tags as $key => $tag) {
1733                         $r = q("SELECT `oid` FROM `term` WHERE `term` = '%s' AND `otype` = %d AND `type` = %d AND `uid` = %d",
1734                                 DBA::escape($tag),
1735                                 intval(TERM_OBJ_POST),
1736                                 intval($termtype),
1737                                 intval($uid));
1738
1739                         if (DBA::isResult($r)) {
1740                                 unset($deleted_tags[$key]);
1741                         } else {
1742                                 $filetags_updated = str_replace($lbracket . file_tag_encode($tag) . $rbracket,'',$filetags_updated);
1743                         }
1744                 }
1745
1746                 if ($saved != $filetags_updated) {
1747                         PConfig::set($uid, 'system', 'filetags', $filetags_updated);
1748                 }
1749                 return true;
1750         } elseif (strlen($file_new)) {
1751                 PConfig::set($uid, 'system', 'filetags', $file_new);
1752         }
1753         return true;
1754 }
1755
1756 function file_tag_save_file($uid, $item_id, $file)
1757 {
1758         if (!intval($uid)) {
1759                 return false;
1760         }
1761
1762         $item = Item::selectFirst(['file'], ['id' => $item_id, 'uid' => $uid]);
1763         if (DBA::isResult($item)) {
1764                 if (!stristr($item['file'],'[' . file_tag_encode($file) . ']')) {
1765                         $fields = ['file' => $item['file'] . '[' . file_tag_encode($file) . ']'];
1766                         Item::update($fields, ['id' => $item_id]);
1767                 }
1768                 $saved = PConfig::get($uid, 'system', 'filetags');
1769                 if (!strlen($saved) || !stristr($saved, '[' . file_tag_encode($file) . ']')) {
1770                         PConfig::set($uid, 'system', 'filetags', $saved . '[' . file_tag_encode($file) . ']');
1771                 }
1772                 info(L10n::t('Item filed'));
1773         }
1774         return true;
1775 }
1776
1777 function file_tag_unsave_file($uid, $item_id, $file, $cat = false)
1778 {
1779         if (!intval($uid)) {
1780                 return false;
1781         }
1782
1783         if ($cat == true) {
1784                 $pattern = '<' . file_tag_encode($file) . '>' ;
1785                 $termtype = TERM_CATEGORY;
1786         } else {
1787                 $pattern = '[' . file_tag_encode($file) . ']' ;
1788                 $termtype = TERM_FILE;
1789         }
1790
1791         $item = Item::selectFirst(['file'], ['id' => $item_id, 'uid' => $uid]);
1792         if (!DBA::isResult($item)) {
1793                 return false;
1794         }
1795
1796         $fields = ['file' => str_replace($pattern,'',$item['file'])];
1797         Item::update($fields, ['id' => $item_id]);
1798
1799         $r = q("SELECT `oid` FROM `term` WHERE `term` = '%s' AND `otype` = %d AND `type` = %d AND `uid` = %d",
1800                 DBA::escape($file),
1801                 intval(TERM_OBJ_POST),
1802                 intval($termtype),
1803                 intval($uid)
1804         );
1805         if (!DBA::isResult($r)) {
1806                 $saved = PConfig::get($uid, 'system', 'filetags');
1807                 PConfig::set($uid, 'system', 'filetags', str_replace($pattern, '', $saved));
1808         }
1809
1810         return true;
1811 }
1812
1813 function normalise_openid($s) {
1814         return trim(str_replace(['http://', 'https://'], ['', ''], $s), '/');
1815 }
1816
1817
1818 function undo_post_tagging($s) {
1819         $matches = null;
1820         $cnt = preg_match_all('/([!#@])\[url=(.*?)\](.*?)\[\/url\]/ism', $s, $matches, PREG_SET_ORDER);
1821         if ($cnt) {
1822                 foreach ($matches as $mtch) {
1823                         if (in_array($mtch[1], ['!', '@'])) {
1824                                 $contact = Contact::getDetailsByURL($mtch[2]);
1825                                 $mtch[3] = empty($contact['addr']) ? $mtch[2] : $contact['addr'];
1826                         }
1827                         $s = str_replace($mtch[0], $mtch[1] . $mtch[3],$s);
1828                 }
1829         }
1830         return $s;
1831 }
1832
1833 function protect_sprintf($s) {
1834         return str_replace('%', '%%', $s);
1835 }
1836
1837 /// @TODO Rewrite this
1838 function is_a_date_arg($s) {
1839         $i = intval($s);
1840
1841         if ($i > 1900) {
1842                 $y = date('Y');
1843
1844                 if ($i <= $y + 1 && strpos($s, '-') == 4) {
1845                         $m = intval(substr($s, 5));
1846
1847                         if ($m > 0 && $m <= 12) {
1848                                 return true;
1849                         }
1850                 }
1851         }
1852
1853         return false;
1854 }
1855
1856 /**
1857  * remove intentation from a text
1858  */
1859 function deindent($text, $chr = "[\t ]", $count = NULL) {
1860         $lines = explode("\n", $text);
1861
1862         if (is_null($count)) {
1863                 $m = [];
1864                 $k = 0;
1865                 while ($k < count($lines) && strlen($lines[$k]) == 0) {
1866                         $k++;
1867                 }
1868                 preg_match("|^" . $chr . "*|", $lines[$k], $m);
1869                 $count = strlen($m[0]);
1870         }
1871
1872         for ($k = 0; $k < count($lines); $k++) {
1873                 $lines[$k] = preg_replace("|^" . $chr . "{" . $count . "}|", "", $lines[$k]);
1874         }
1875
1876         return implode("\n", $lines);
1877 }
1878
1879 function formatBytes($bytes, $precision = 2) {
1880         $units = ['B', 'KB', 'MB', 'GB', 'TB'];
1881
1882         $bytes = max($bytes, 0);
1883         $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
1884         $pow = min($pow, count($units) - 1);
1885
1886         $bytes /= pow(1024, $pow);
1887
1888         return round($bytes, $precision) . ' ' . $units[$pow];
1889 }
1890
1891 /**
1892  * @brief translate and format the networkname of a contact
1893  *
1894  * @param string $network
1895  *      Networkname of the contact (e.g. dfrn, rss and so on)
1896  * @param sting $url
1897  *      The contact url
1898  * @return string
1899  */
1900 function format_network_name($network, $url = 0) {
1901         if ($network != "") {
1902                 if ($url != "") {
1903                         $network_name = '<a href="'.$url.'">'.ContactSelector::networkToName($network, $url)."</a>";
1904                 } else {
1905                         $network_name = ContactSelector::networkToName($network);
1906                 }
1907
1908                 return $network_name;
1909         }
1910 }