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