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