]> git.mxchange.org Git - friendica.git/blob - include/text.php
Stopped using deprecated constants NETWORK_* (#5537)
[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->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 /**
1464  * @return string
1465  */
1466 function generate_user_guid() {
1467         $found = true;
1468         do {
1469                 $guid = System::createGUID(32);
1470                 $x = q("SELECT `uid` FROM `user` WHERE `guid` = '%s' LIMIT 1",
1471                         DBA::escape($guid)
1472                 );
1473                 if (!DBA::isResult($x)) {
1474                         $found = false;
1475                 }
1476         } while ($found == true);
1477
1478         return $guid;
1479 }
1480
1481
1482 /**
1483  * @param string $s
1484  * @param boolean $strip_padding
1485  * @return string
1486  */
1487 function base64url_encode($s, $strip_padding = false) {
1488
1489         $s = strtr(base64_encode($s), '+/', '-_');
1490
1491         if ($strip_padding) {
1492                 $s = str_replace('=','',$s);
1493         }
1494
1495         return $s;
1496 }
1497
1498 /**
1499  * @param string $s
1500  * @return string
1501  */
1502 function base64url_decode($s) {
1503
1504         if (is_array($s)) {
1505                 logger('base64url_decode: illegal input: ' . print_r(debug_backtrace(), true));
1506                 return $s;
1507         }
1508
1509 /*
1510  *  // Placeholder for new rev of salmon which strips base64 padding.
1511  *  // PHP base64_decode handles the un-padded input without requiring this step
1512  *  // Uncomment if you find you need it.
1513  *
1514  *      $l = strlen($s);
1515  *      if (!strpos($s,'=')) {
1516  *              $m = $l % 4;
1517  *              if ($m == 2)
1518  *                      $s .= '==';
1519  *              if ($m == 3)
1520  *                      $s .= '=';
1521  *      }
1522  *
1523  */
1524
1525         return base64_decode(strtr($s,'-_','+/'));
1526 }
1527
1528
1529 /**
1530  * return div element with class 'clear'
1531  * @return string
1532  * @deprecated
1533  */
1534 function cleardiv() {
1535         return '<div class="clear"></div>';
1536 }
1537
1538
1539 function bb_translate_video($s) {
1540
1541         $matches = null;
1542         $r = preg_match_all("/\[video\](.*?)\[\/video\]/ism",$s,$matches,PREG_SET_ORDER);
1543         if ($r) {
1544                 foreach ($matches as $mtch) {
1545                         if ((stristr($mtch[1], 'youtube')) || (stristr($mtch[1], 'youtu.be'))) {
1546                                 $s = str_replace($mtch[0], '[youtube]' . $mtch[1] . '[/youtube]', $s);
1547                         } elseif (stristr($mtch[1], 'vimeo')) {
1548                                 $s = str_replace($mtch[0], '[vimeo]' . $mtch[1] . '[/vimeo]', $s);
1549                         }
1550                 }
1551         }
1552         return $s;
1553 }
1554
1555 function html2bb_video($s) {
1556
1557         $s = preg_replace('#<object[^>]+>(.*?)https?://www.youtube.com/((?:v|cp)/[A-Za-z0-9\-_=]+)(.*?)</object>#ism',
1558                         '[youtube]$2[/youtube]', $s);
1559
1560         $s = preg_replace('#<iframe[^>](.*?)https?://www.youtube.com/embed/([A-Za-z0-9\-_=]+)(.*?)</iframe>#ism',
1561                         '[youtube]$2[/youtube]', $s);
1562
1563         $s = preg_replace('#<iframe[^>](.*?)https?://player.vimeo.com/video/([0-9]+)(.*?)</iframe>#ism',
1564                         '[vimeo]$2[/vimeo]', $s);
1565
1566         return $s;
1567 }
1568
1569 /**
1570  * apply xmlify() to all values of array $val, recursively
1571  * @param array $val
1572  * @return array
1573  */
1574 function array_xmlify($val){
1575         if (is_bool($val)) {
1576                 return $val?"true":"false";
1577         } elseif (is_array($val)) {
1578                 return array_map('array_xmlify', $val);
1579         }
1580         return xmlify((string) $val);
1581 }
1582
1583
1584 /**
1585  * transform link href and img src from relative to absolute
1586  *
1587  * @param string $text
1588  * @param string $base base url
1589  * @return string
1590  */
1591 function reltoabs($text, $base) {
1592         if (empty($base)) {
1593                 return $text;
1594         }
1595
1596         $base = rtrim($base,'/');
1597
1598         $base2 = $base . "/";
1599
1600         // Replace links
1601         $pattern = "/<a([^>]*) href=\"(?!http|https|\/)([^\"]*)\"/";
1602         $replace = "<a\${1} href=\"" . $base2 . "\${2}\"";
1603         $text = preg_replace($pattern, $replace, $text);
1604
1605         $pattern = "/<a([^>]*) href=\"(?!http|https)([^\"]*)\"/";
1606         $replace = "<a\${1} href=\"" . $base . "\${2}\"";
1607         $text = preg_replace($pattern, $replace, $text);
1608
1609         // Replace images
1610         $pattern = "/<img([^>]*) src=\"(?!http|https|\/)([^\"]*)\"/";
1611         $replace = "<img\${1} src=\"" . $base2 . "\${2}\"";
1612         $text = preg_replace($pattern, $replace, $text);
1613
1614         $pattern = "/<img([^>]*) src=\"(?!http|https)([^\"]*)\"/";
1615         $replace = "<img\${1} src=\"" . $base . "\${2}\"";
1616         $text = preg_replace($pattern, $replace, $text);
1617
1618
1619         // Done
1620         return $text;
1621 }
1622
1623 /**
1624  * get translated item type
1625  *
1626  * @param array $itme
1627  * @return string
1628  */
1629 function item_post_type($item) {
1630         if (!empty($item['event-id'])) {
1631                 return L10n::t('event');
1632         } elseif (!empty($item['resource-id'])) {
1633                 return L10n::t('photo');
1634         } elseif (!empty($item['verb']) && $item['verb'] !== ACTIVITY_POST) {
1635                 return L10n::t('activity');
1636         } elseif ($item['id'] != $item['parent']) {
1637                 return L10n::t('comment');
1638         }
1639
1640         return L10n::t('post');
1641 }
1642
1643 // post categories and "save to file" use the same item.file table for storage.
1644 // We will differentiate the different uses by wrapping categories in angle brackets
1645 // and save to file categories in square brackets.
1646 // To do this we need to escape these characters if they appear in our tag.
1647
1648 function file_tag_encode($s) {
1649         return str_replace(['<','>','[',']'],['%3c','%3e','%5b','%5d'],$s);
1650 }
1651
1652 function file_tag_decode($s) {
1653         return str_replace(['%3c', '%3e', '%5b', '%5d'], ['<', '>', '[', ']'], $s);
1654 }
1655
1656 function file_tag_file_query($table,$s,$type = 'file') {
1657
1658         if ($type == 'file') {
1659                 $str = preg_quote('[' . str_replace('%', '%%', file_tag_encode($s)) . ']');
1660         } else {
1661                 $str = preg_quote('<' . str_replace('%', '%%', file_tag_encode($s)) . '>');
1662         }
1663         return " AND " . (($table) ? DBA::escape($table) . '.' : '') . "file regexp '" . DBA::escape($str) . "' ";
1664 }
1665
1666 // ex. given music,video return <music><video> or [music][video]
1667 function file_tag_list_to_file($list, $type = 'file') {
1668         $tag_list = '';
1669         if (strlen($list)) {
1670                 $list_array = explode(",",$list);
1671                 if ($type == 'file') {
1672                         $lbracket = '[';
1673                         $rbracket = ']';
1674                 } else {
1675                         $lbracket = '<';
1676                         $rbracket = '>';
1677                 }
1678
1679                 foreach ($list_array as $item) {
1680                         if (strlen($item)) {
1681                                 $tag_list .= $lbracket . file_tag_encode(trim($item))  . $rbracket;
1682                         }
1683                 }
1684         }
1685         return $tag_list;
1686 }
1687
1688 // ex. given <music><video>[friends], return music,video or friends
1689 function file_tag_file_to_list($file, $type = 'file') {
1690         $matches = false;
1691         $list = '';
1692         if ($type == 'file') {
1693                 $cnt = preg_match_all('/\[(.*?)\]/', $file, $matches, PREG_SET_ORDER);
1694         } else {
1695                 $cnt = preg_match_all('/<(.*?)>/', $file, $matches, PREG_SET_ORDER);
1696         }
1697         if ($cnt) {
1698                 foreach ($matches as $mtch) {
1699                         if (strlen($list)) {
1700                                 $list .= ',';
1701                         }
1702                         $list .= file_tag_decode($mtch[1]);
1703                 }
1704         }
1705
1706         return $list;
1707 }
1708
1709 function file_tag_update_pconfig($uid, $file_old, $file_new, $type = 'file') {
1710         // $file_old - categories previously associated with an item
1711         // $file_new - new list of categories for an item
1712
1713         if (!intval($uid)) {
1714                 return false;
1715         } elseif ($file_old == $file_new) {
1716                 return true;
1717         }
1718
1719         $saved = PConfig::get($uid, 'system', 'filetags');
1720         if (strlen($saved)) {
1721                 if ($type == 'file') {
1722                         $lbracket = '[';
1723                         $rbracket = ']';
1724                         $termtype = TERM_FILE;
1725                 } else {
1726                         $lbracket = '<';
1727                         $rbracket = '>';
1728                         $termtype = TERM_CATEGORY;
1729                 }
1730
1731                 $filetags_updated = $saved;
1732
1733                 // check for new tags to be added as filetags in pconfig
1734                 $new_tags = [];
1735                 $check_new_tags = explode(",",file_tag_file_to_list($file_new,$type));
1736
1737                 foreach ($check_new_tags as $tag) {
1738                         if (!stristr($saved,$lbracket . file_tag_encode($tag) . $rbracket)) {
1739                                 $new_tags[] = $tag;
1740                         }
1741                 }
1742
1743                 $filetags_updated .= file_tag_list_to_file(implode(",",$new_tags),$type);
1744
1745                 // check for deleted tags to be removed from filetags in pconfig
1746                 $deleted_tags = [];
1747                 $check_deleted_tags = explode(",",file_tag_file_to_list($file_old,$type));
1748
1749                 foreach ($check_deleted_tags as $tag) {
1750                         if (!stristr($file_new,$lbracket . file_tag_encode($tag) . $rbracket)) {
1751                                 $deleted_tags[] = $tag;
1752                         }
1753                 }
1754
1755                 foreach ($deleted_tags as $key => $tag) {
1756                         $r = q("SELECT `oid` FROM `term` WHERE `term` = '%s' AND `otype` = %d AND `type` = %d AND `uid` = %d",
1757                                 DBA::escape($tag),
1758                                 intval(TERM_OBJ_POST),
1759                                 intval($termtype),
1760                                 intval($uid));
1761
1762                         if (DBA::isResult($r)) {
1763                                 unset($deleted_tags[$key]);
1764                         } else {
1765                                 $filetags_updated = str_replace($lbracket . file_tag_encode($tag) . $rbracket,'',$filetags_updated);
1766                         }
1767                 }
1768
1769                 if ($saved != $filetags_updated) {
1770                         PConfig::set($uid, 'system', 'filetags', $filetags_updated);
1771                 }
1772                 return true;
1773         } elseif (strlen($file_new)) {
1774                 PConfig::set($uid, 'system', 'filetags', $file_new);
1775         }
1776         return true;
1777 }
1778
1779 function file_tag_save_file($uid, $item_id, $file)
1780 {
1781         if (!intval($uid)) {
1782                 return false;
1783         }
1784
1785         $item = Item::selectFirst(['file'], ['id' => $item_id, 'uid' => $uid]);
1786         if (DBA::isResult($item)) {
1787                 if (!stristr($item['file'],'[' . file_tag_encode($file) . ']')) {
1788                         $fields = ['file' => $item['file'] . '[' . file_tag_encode($file) . ']'];
1789                         Item::update($fields, ['id' => $item_id]);
1790                 }
1791                 $saved = PConfig::get($uid, 'system', 'filetags');
1792                 if (!strlen($saved) || !stristr($saved, '[' . file_tag_encode($file) . ']')) {
1793                         PConfig::set($uid, 'system', 'filetags', $saved . '[' . file_tag_encode($file) . ']');
1794                 }
1795                 info(L10n::t('Item filed'));
1796         }
1797         return true;
1798 }
1799
1800 function file_tag_unsave_file($uid, $item_id, $file, $cat = false)
1801 {
1802         if (!intval($uid)) {
1803                 return false;
1804         }
1805
1806         if ($cat == true) {
1807                 $pattern = '<' . file_tag_encode($file) . '>' ;
1808                 $termtype = TERM_CATEGORY;
1809         } else {
1810                 $pattern = '[' . file_tag_encode($file) . ']' ;
1811                 $termtype = TERM_FILE;
1812         }
1813
1814         $item = Item::selectFirst(['file'], ['id' => $item_id, 'uid' => $uid]);
1815         if (!DBA::isResult($item)) {
1816                 return false;
1817         }
1818
1819         $fields = ['file' => str_replace($pattern,'',$item['file'])];
1820         Item::update($fields, ['id' => $item_id]);
1821
1822         $r = q("SELECT `oid` FROM `term` WHERE `term` = '%s' AND `otype` = %d AND `type` = %d AND `uid` = %d",
1823                 DBA::escape($file),
1824                 intval(TERM_OBJ_POST),
1825                 intval($termtype),
1826                 intval($uid)
1827         );
1828         if (!DBA::isResult($r)) {
1829                 $saved = PConfig::get($uid, 'system', 'filetags');
1830                 PConfig::set($uid, 'system', 'filetags', str_replace($pattern, '', $saved));
1831         }
1832
1833         return true;
1834 }
1835
1836 function normalise_openid($s) {
1837         return trim(str_replace(['http://', 'https://'], ['', ''], $s), '/');
1838 }
1839
1840
1841 function undo_post_tagging($s) {
1842         $matches = null;
1843         $cnt = preg_match_all('/([!#@])\[url=(.*?)\](.*?)\[\/url\]/ism', $s, $matches, PREG_SET_ORDER);
1844         if ($cnt) {
1845                 foreach ($matches as $mtch) {
1846                         if (in_array($mtch[1], ['!', '@'])) {
1847                                 $contact = Contact::getDetailsByURL($mtch[2]);
1848                                 $mtch[3] = empty($contact['addr']) ? $mtch[2] : $contact['addr'];
1849                         }
1850                         $s = str_replace($mtch[0], $mtch[1] . $mtch[3],$s);
1851                 }
1852         }
1853         return $s;
1854 }
1855
1856 function protect_sprintf($s) {
1857         return str_replace('%', '%%', $s);
1858 }
1859
1860 /// @TODO Rewrite this
1861 function is_a_date_arg($s) {
1862         $i = intval($s);
1863
1864         if ($i > 1900) {
1865                 $y = date('Y');
1866
1867                 if ($i <= $y + 1 && strpos($s, '-') == 4) {
1868                         $m = intval(substr($s, 5));
1869
1870                         if ($m > 0 && $m <= 12) {
1871                                 return true;
1872                         }
1873                 }
1874         }
1875
1876         return false;
1877 }
1878
1879 /**
1880  * remove intentation from a text
1881  */
1882 function deindent($text, $chr = "[\t ]", $count = NULL) {
1883         $lines = explode("\n", $text);
1884
1885         if (is_null($count)) {
1886                 $m = [];
1887                 $k = 0;
1888                 while ($k < count($lines) && strlen($lines[$k]) == 0) {
1889                         $k++;
1890                 }
1891                 preg_match("|^" . $chr . "*|", $lines[$k], $m);
1892                 $count = strlen($m[0]);
1893         }
1894
1895         for ($k = 0; $k < count($lines); $k++) {
1896                 $lines[$k] = preg_replace("|^" . $chr . "{" . $count . "}|", "", $lines[$k]);
1897         }
1898
1899         return implode("\n", $lines);
1900 }
1901
1902 function formatBytes($bytes, $precision = 2) {
1903         $units = ['B', 'KB', 'MB', 'GB', 'TB'];
1904
1905         $bytes = max($bytes, 0);
1906         $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
1907         $pow = min($pow, count($units) - 1);
1908
1909         $bytes /= pow(1024, $pow);
1910
1911         return round($bytes, $precision) . ' ' . $units[$pow];
1912 }
1913
1914 /**
1915  * @brief translate and format the networkname of a contact
1916  *
1917  * @param string $network
1918  *      Networkname of the contact (e.g. dfrn, rss and so on)
1919  * @param sting $url
1920  *      The contact url
1921  * @return string
1922  */
1923 function format_network_name($network, $url = 0) {
1924         if ($network != "") {
1925                 if ($url != "") {
1926                         $network_name = '<a href="'.$url.'">'.ContactSelector::networkToName($network, $url)."</a>";
1927                 } else {
1928                         $network_name = ContactSelector::networkToName($network);
1929                 }
1930
1931                 return $network_name;
1932         }
1933 }
1934
1935 /**
1936  * @brief Syntax based code highlighting for popular languages.
1937  * @param string $s Code block
1938  * @param string $lang Programming language
1939  * @return string Formated html
1940  */
1941 function text_highlight($s, $lang) {
1942         if ($lang === 'js') {
1943                 $lang = 'javascript';
1944         }
1945
1946         if ($lang === 'bash') {
1947                 $lang = 'sh';
1948         }
1949
1950         // @TODO: Replace Text_Highlighter_Renderer_Html by scrivo/highlight.php
1951
1952         // Autoload the library to make constants available
1953         class_exists('Text_Highlighter_Renderer_Html');
1954
1955         $options = [
1956                 'numbers' => HL_NUMBERS_LI,
1957                 'tabsize' => 4,
1958         ];
1959
1960         $tag_added = false;
1961         $s = trim(html_entity_decode($s, ENT_COMPAT));
1962         $s = str_replace('    ', "\t", $s);
1963
1964         /*
1965          * The highlighter library insists on an opening php tag for php code blocks. If
1966          * it isn't present, nothing is highlighted. So we're going to see if it's present.
1967          * If not, we'll add it, and then quietly remove it after we get the processed output back.
1968          */
1969         if ($lang === 'php' && strpos($s, '<?php') !== 0) {
1970                 $s = '<?php' . "\n" . $s;
1971                 $tag_added = true;
1972         }
1973
1974         $renderer = new Text_Highlighter_Renderer_Html($options);
1975         $factory = new Text_Highlighter();
1976         $hl = $factory->factory($lang);
1977         $hl->setRenderer($renderer);
1978         $o = $hl->highlight($s);
1979         $o = str_replace("\n", '', $o);
1980
1981         if ($tag_added) {
1982                 $b = substr($o, 0, strpos($o, '<li>'));
1983                 $e = substr($o, strpos($o, '</li>'));
1984                 $o = $b . $e;
1985         }
1986
1987         return '<code>' . $o . '</code>';
1988 }