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