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