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