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