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