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