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