]> git.mxchange.org Git - friendica.git/blob - include/text.php
cfa6c0675363bb996caed8c4e049d51a77d3cc58
[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 use Friendica\Util\XML;
30 use Friendica\Content\Text\HTML;
31
32 require_once "include/conversation.php";
33
34 /**
35  * @brief Generates a pseudo-random string of hexadecimal characters
36  *
37  * @param int $size
38  * @return string
39  */
40 function random_string($size = 64)
41 {
42         $byte_size = ceil($size / 2);
43
44         $bytes = random_bytes($byte_size);
45
46         $return = substr(bin2hex($bytes), 0, $size);
47
48         return $return;
49 }
50
51 /**
52  * This is our primary input filter.
53  *
54  * The high bit hack only involved some old IE browser, forget which (IE5/Mac?)
55  * that had an XSS attack vector due to stripping the high-bit on an 8-bit character
56  * after cleansing, and angle chars with the high bit set could get through as markup.
57  *
58  * This is now disabled because it was interfering with some legitimate unicode sequences
59  * and hopefully there aren't a lot of those browsers left.
60  *
61  * Use this on any text input where angle chars are not valid or permitted
62  * They will be replaced with safer brackets. This may be filtered further
63  * if these are not allowed either.
64  *
65  * @param string $string Input string
66  * @return string Filtered string
67  */
68 function notags($string) {
69         return str_replace(["<", ">"], ['[', ']'], $string);
70
71 //  High-bit filter no longer used
72 //      return str_replace(array("<",">","\xBA","\xBC","\xBE"), array('[',']','','',''), $string);
73 }
74
75
76 /**
77  * use this on "body" or "content" input where angle chars shouldn't be removed,
78  * and allow them to be safely displayed.
79  * @param string $string
80  * @return string
81  */
82 function escape_tags($string) {
83         return htmlspecialchars($string, ENT_COMPAT, 'UTF-8', false);
84 }
85
86
87 /**
88  * generate a string that's random, but usually pronounceable.
89  * used to generate initial passwords
90  * @param int $len
91  * @return string
92  */
93 function autoname($len) {
94
95         if ($len <= 0) {
96                 return '';
97         }
98
99         $vowels = ['a','a','ai','au','e','e','e','ee','ea','i','ie','o','ou','u'];
100         if (mt_rand(0, 5) == 4) {
101                 $vowels[] = 'y';
102         }
103
104         $cons = [
105                         'b','bl','br',
106                         'c','ch','cl','cr',
107                         'd','dr',
108                         'f','fl','fr',
109                         'g','gh','gl','gr',
110                         'h',
111                         'j',
112                         'k','kh','kl','kr',
113                         'l',
114                         'm',
115                         'n',
116                         'p','ph','pl','pr',
117                         'qu',
118                         'r','rh',
119                         's','sc','sh','sm','sp','st',
120                         't','th','tr',
121                         'v',
122                         'w','wh',
123                         'x',
124                         'z','zh'
125                         ];
126
127         $midcons = ['ck','ct','gn','ld','lf','lm','lt','mb','mm', 'mn','mp',
128                                 'nd','ng','nk','nt','rn','rp','rt'];
129
130         $noend = ['bl', 'br', 'cl','cr','dr','fl','fr','gl','gr',
131                                 'kh', 'kl','kr','mn','pl','pr','rh','tr','qu','wh','q'];
132
133         $start = mt_rand(0,2);
134         if ($start == 0) {
135                 $table = $vowels;
136         } else {
137                 $table = $cons;
138         }
139
140         $word = '';
141
142         for ($x = 0; $x < $len; $x ++) {
143                 $r = mt_rand(0,count($table) - 1);
144                 $word .= $table[$r];
145
146                 if ($table == $vowels) {
147                         $table = array_merge($cons,$midcons);
148                 } else {
149                         $table = $vowels;
150                 }
151
152         }
153
154         $word = substr($word,0,$len);
155
156         foreach ($noend as $noe) {
157                 $noelen = strlen($noe);
158                 if ((strlen($word) > $noelen) && (substr($word, -$noelen) == $noe)) {
159                         $word = autoname($len);
160                         break;
161                 }
162         }
163
164         return $word;
165 }
166
167 /**
168  * Turn user/group ACLs stored as angle bracketed text into arrays
169  *
170  * @param string $s
171  * @return array
172  */
173 function expand_acl($s) {
174         // turn string array of angle-bracketed elements into numeric array
175         // e.g. "<1><2><3>" => array(1,2,3);
176         $ret = [];
177
178         if (strlen($s)) {
179                 $t = str_replace('<', '', $s);
180                 $a = explode('>', $t);
181                 foreach ($a as $aa) {
182                         if (intval($aa)) {
183                                 $ret[] = intval($aa);
184                         }
185                 }
186         }
187         return $ret;
188 }
189
190
191 /**
192  * Wrap ACL elements in angle brackets for storage
193  * @param string $item
194  */
195 function sanitise_acl(&$item) {
196         if (intval($item)) {
197                 $item = '<' . intval(notags(trim($item))) . '>';
198         } else {
199                 unset($item);
200         }
201 }
202
203
204 /**
205  * Convert an ACL array to a storable string
206  *
207  * Normally ACL permissions will be an array.
208  * We'll also allow a comma-separated string.
209  *
210  * @param string|array $p
211  * @return string
212  */
213 function perms2str($p) {
214         $ret = '';
215         if (is_array($p)) {
216                 $tmp = $p;
217         } else {
218                 $tmp = explode(',', $p);
219         }
220
221         if (is_array($tmp)) {
222                 array_walk($tmp, 'sanitise_acl');
223                 $ret = implode('', $tmp);
224         }
225         return $ret;
226 }
227
228 /**
229  *  for html,xml parsing - let's say you've got
230  *  an attribute foobar="class1 class2 class3"
231  *  and you want to find out if it contains 'class3'.
232  *  you can't use a normal sub string search because you
233  *  might match 'notclass3' and a regex to do the job is
234  *  possible but a bit complicated.
235  *  pass the attribute string as $attr and the attribute you
236  *  are looking for as $s - returns true if found, otherwise false
237  *
238  * @param string $attr attribute value
239  * @param string $s string to search
240  * @return boolean True if found, False otherwise
241  */
242 function attribute_contains($attr, $s) {
243         $a = explode(' ', $attr);
244         return (count($a) && in_array($s,$a));
245 }
246
247 /**
248  * Compare activity uri. Knows about activity namespace.
249  *
250  * @param string $haystack
251  * @param string $needle
252  * @return boolean
253  */
254 function activity_match($haystack,$needle) {
255         return (($haystack === $needle) || ((basename($needle) === $haystack) && strstr($needle, NAMESPACE_ACTIVITY_SCHEMA)));
256 }
257
258
259 /**
260  * @brief Pull out all #hashtags and @person tags from $string.
261  *
262  * We also get @person@domain.com - which would make
263  * the regex quite complicated as tags can also
264  * end a sentence. So we'll run through our results
265  * and strip the period from any tags which end with one.
266  * Returns array of tags found, or empty array.
267  *
268  * @param string $string Post content
269  * @return array List of tag and person names
270  */
271 function get_tags($string) {
272         $ret = [];
273
274         // Convert hashtag links to hashtags
275         $string = preg_replace('/#\[url\=([^\[\]]*)\](.*?)\[\/url\]/ism', '#$2', $string);
276
277         // ignore anything in a code block
278         $string = preg_replace('/\[code\](.*?)\[\/code\]/sm', '', $string);
279
280         // Force line feeds at bbtags
281         $string = str_replace(['[', ']'], ["\n[", "]\n"], $string);
282
283         // ignore anything in a bbtag
284         $string = preg_replace('/\[(.*?)\]/sm', '', $string);
285
286         // Match full names against @tags including the space between first and last
287         // We will look these up afterward to see if they are full names or not recognisable.
288
289         if (preg_match_all('/(@[^ \x0D\x0A,:?]+ [^ \x0D\x0A@,:?]+)([ \x0D\x0A@,:?]|$)/', $string, $matches)) {
290                 foreach ($matches[1] as $match) {
291                         if (strstr($match, ']')) {
292                                 // we might be inside a bbcode color tag - leave it alone
293                                 continue;
294                         }
295                         if (substr($match, -1, 1) === '.') {
296                                 $ret[] = substr($match, 0, -1);
297                         } else {
298                                 $ret[] = $match;
299                         }
300                 }
301         }
302
303         // Otherwise pull out single word tags. These can be @nickname, @first_last
304         // and #hash tags.
305
306         if (preg_match_all('/([!#@][^\^ \x0D\x0A,;:?]+)([ \x0D\x0A,;:?]|$)/', $string, $matches)) {
307                 foreach ($matches[1] as $match) {
308                         if (strstr($match, ']')) {
309                                 // we might be inside a bbcode color tag - leave it alone
310                                 continue;
311                         }
312                         if (substr($match, -1, 1) === '.') {
313                                 $match = substr($match,0,-1);
314                         }
315                         // ignore strictly numeric tags like #1
316                         if ((strpos($match, '#') === 0) && ctype_digit(substr($match, 1))) {
317                                 continue;
318                         }
319                         // try not to catch url fragments
320                         if (strpos($string, $match) && preg_match('/[a-zA-z0-9\/]/', substr($string, strpos($string, $match) - 1, 1))) {
321                                 continue;
322                         }
323                         $ret[] = $match;
324                 }
325         }
326         return $ret;
327 }
328
329
330 /**
331  * quick and dirty quoted_printable encoding
332  *
333  * @param string $s
334  * @return string
335  */
336 function qp($s) {
337         return str_replace("%", "=", rawurlencode($s));
338 }
339
340 /**
341  * @brief Check for a valid email string
342  *
343  * @param string $email_address
344  * @return boolean
345  */
346 function valid_email($email_address)
347 {
348         return preg_match('/^[_a-zA-Z0-9\-\+]+(\.[_a-zA-Z0-9\-\+]+)*@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+$/', $email_address);
349 }
350
351 /**
352  * Load poke verbs
353  *
354  * @return array index is present tense verb
355  *                               value is array containing past tense verb, translation of present, translation of past
356  * @hook poke_verbs pokes array
357  */
358 function get_poke_verbs() {
359
360         // index is present tense verb
361         // value is array containing past tense verb, translation of present, translation of past
362
363         $arr = [
364                 'poke' => ['poked', L10n::t('poke'), L10n::t('poked')],
365                 'ping' => ['pinged', L10n::t('ping'), L10n::t('pinged')],
366                 'prod' => ['prodded', L10n::t('prod'), L10n::t('prodded')],
367                 'slap' => ['slapped', L10n::t('slap'), L10n::t('slapped')],
368                 'finger' => ['fingered', L10n::t('finger'), L10n::t('fingered')],
369                 'rebuff' => ['rebuffed', L10n::t('rebuff'), L10n::t('rebuffed')],
370         ];
371         Addon::callHooks('poke_verbs', $arr);
372         return $arr;
373 }
374
375 /**
376  * @brief Translate days and months names.
377  *
378  * @param string $s String with day or month name.
379  * @return string Translated string.
380  */
381 function day_translate($s) {
382         $ret = str_replace(['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'],
383                 [L10n::t('Monday'), L10n::t('Tuesday'), L10n::t('Wednesday'), L10n::t('Thursday'), L10n::t('Friday'), L10n::t('Saturday'), L10n::t('Sunday')],
384                 $s);
385
386         $ret = str_replace(['January','February','March','April','May','June','July','August','September','October','November','December'],
387                 [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')],
388                 $ret);
389
390         return $ret;
391 }
392
393 /**
394  * @brief Translate short days and months names.
395  *
396  * @param string $s String with short day or month name.
397  * @return string Translated string.
398  */
399 function day_short_translate($s) {
400         $ret = str_replace(['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
401                 [L10n::t('Mon'), L10n::t('Tue'), L10n::t('Wed'), L10n::t('Thu'), L10n::t('Fri'), L10n::t('Sat'), L10n::t('Sun')],
402                 $s);
403         $ret = str_replace(['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov','Dec'],
404                 [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')],
405                 $ret);
406         return $ret;
407 }
408
409
410 /**
411  * Normalize url
412  *
413  * @param string $url
414  * @return string
415  */
416 function normalise_link($url) {
417         $ret = str_replace(['https:', '//www.'], ['http:', '//'], $url);
418         return rtrim($ret,'/');
419 }
420
421
422 /**
423  * Compare two URLs to see if they are the same, but ignore
424  * slight but hopefully insignificant differences such as if one
425  * is https and the other isn't, or if one is www.something and
426  * the other isn't - and also ignore case differences.
427  *
428  * @param string $a first url
429  * @param string $b second url
430  * @return boolean True if the URLs match, otherwise False
431  *
432  */
433 function link_compare($a, $b) {
434         return (strcasecmp(normalise_link($a), normalise_link($b)) === 0);
435 }
436
437
438 /**
439  * @brief Find any non-embedded images in private items and add redir links to them
440  *
441  * @param App $a
442  * @param array &$item The field array of an item row
443  */
444 function redir_private_images($a, &$item)
445 {
446         $matches = false;
447         $cnt = preg_match_all('|\[img\](http[^\[]*?/photo/[a-fA-F0-9]+?(-[0-9]\.[\w]+?)?)\[\/img\]|', $item['body'], $matches, PREG_SET_ORDER);
448         if ($cnt) {
449                 foreach ($matches as $mtch) {
450                         if (strpos($mtch[1], '/redir') !== false) {
451                                 continue;
452                         }
453
454                         if ((local_user() == $item['uid']) && ($item['private'] == 1) && ($item['contact-id'] != $a->contact['id']) && ($item['network'] == Protocol::DFRN)) {
455                                 $img_url = 'redir?f=1&quiet=1&url=' . urlencode($mtch[1]) . '&conurl=' . urlencode($item['author-link']);
456                                 $item['body'] = str_replace($mtch[0], '[img]' . $img_url . '[/img]', $item['body']);
457                         }
458                 }
459         }
460 }
461
462 /**
463  * Sets the "rendered-html" field of the provided item
464  *
465  * Body is preserved to avoid side-effects as we modify it just-in-time for spoilers and private image links
466  *
467  * @param array $item
468  * @param bool  $update
469  *
470  * @todo Remove reference, simply return "rendered-html" and "rendered-hash"
471  */
472 function put_item_in_cache(&$item, $update = false)
473 {
474         $body = $item["body"];
475
476         $rendered_hash = defaults($item, 'rendered-hash', '');
477         $rendered_html = defaults($item, 'rendered-html', '');
478
479         if ($rendered_hash == ''
480                 || $rendered_html == ""
481                 || $rendered_hash != hash("md5", $item["body"])
482                 || Config::get("system", "ignore_cache")
483         ) {
484                 $a = get_app();
485                 redir_private_images($a, $item);
486
487                 $item["rendered-html"] = prepare_text($item["body"]);
488                 $item["rendered-hash"] = hash("md5", $item["body"]);
489
490                 $hook_data = ['item' => $item, 'rendered-html' => $item['rendered-html'], 'rendered-hash' => $item['rendered-hash']];
491                 Addon::callHooks('put_item_in_cache', $hook_data);
492                 $item['rendered-html'] = $hook_data['rendered-html'];
493                 $item['rendered-hash'] = $hook_data['rendered-hash'];
494                 unset($hook_data);
495
496                 // Force an update if the generated values differ from the existing ones
497                 if ($rendered_hash != $item["rendered-hash"]) {
498                         $update = true;
499                 }
500
501                 // Only compare the HTML when we forcefully ignore the cache
502                 if (Config::get("system", "ignore_cache") && ($rendered_html != $item["rendered-html"])) {
503                         $update = true;
504                 }
505
506                 if ($update && !empty($item["id"])) {
507                         Item::update(['rendered-html' => $item["rendered-html"], 'rendered-hash' => $item["rendered-hash"]],
508                                         ['id' => $item["id"]]);
509                 }
510         }
511
512         $item["body"] = $body;
513 }
514
515 /**
516  * @brief Given an item array, convert the body element from bbcode to html and add smilie icons.
517  * If attach is true, also add icons for item attachments.
518  *
519  * @param array   $item
520  * @param boolean $attach
521  * @param boolean $is_preview
522  * @return string item body html
523  * @hook prepare_body_init item array before any work
524  * @hook prepare_body_content_filter ('item'=>item array, 'filter_reasons'=>string array) before first bbcode to html
525  * @hook prepare_body ('item'=>item array, 'html'=>body string, 'is_preview'=>boolean, 'filter_reasons'=>string array) after first bbcode to html
526  * @hook prepare_body_final ('item'=>item array, 'html'=>body string) after attach icons and blockquote special case handling (spoiler, author)
527  */
528 function prepare_body(array &$item, $attach = false, $is_preview = false)
529 {
530         $a = get_app();
531         Addon::callHooks('prepare_body_init', $item);
532
533         // In order to provide theme developers more possibilities, event items
534         // are treated differently.
535         if ($item['object-type'] === ACTIVITY_OBJ_EVENT && isset($item['event-id'])) {
536                 $ev = Event::getItemHTML($item);
537                 return $ev;
538         }
539
540         $tags = \Friendica\Model\Term::populateTagsFromItem($item);
541
542         $item['tags'] = $tags['tags'];
543         $item['hashtags'] = $tags['hashtags'];
544         $item['mentions'] = $tags['mentions'];
545
546         // Compile eventual content filter reasons
547         $filter_reasons = [];
548         if (!$is_preview && public_contact() != $item['author-id']) {
549                 if (!empty($item['content-warning']) && (!local_user() || !PConfig::get(local_user(), 'system', 'disable_cw', false))) {
550                         $filter_reasons[] = L10n::t('Content warning: %s', $item['content-warning']);
551                 }
552
553                 $hook_data = [
554                         'item' => $item,
555                         'filter_reasons' => $filter_reasons
556                 ];
557                 Addon::callHooks('prepare_body_content_filter', $hook_data);
558                 $filter_reasons = $hook_data['filter_reasons'];
559                 unset($hook_data);
560         }
561
562         // Update the cached values if there is no "zrl=..." on the links.
563         $update = (!local_user() && !remote_user() && ($item["uid"] == 0));
564
565         // Or update it if the current viewer is the intented viewer.
566         if (($item["uid"] == local_user()) && ($item["uid"] != 0)) {
567                 $update = true;
568         }
569
570         put_item_in_cache($item, $update);
571         $s = $item["rendered-html"];
572
573         $hook_data = [
574                 'item' => $item,
575                 'html' => $s,
576                 'preview' => $is_preview,
577                 'filter_reasons' => $filter_reasons
578         ];
579         Addon::callHooks('prepare_body', $hook_data);
580         $s = $hook_data['html'];
581         unset($hook_data);
582
583         if (!$attach) {
584                 // Replace the blockquotes with quotes that are used in mails.
585                 $mailquote = '<blockquote type="cite" class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;">';
586                 $s = str_replace(['<blockquote>', '<blockquote class="spoiler">', '<blockquote class="author">'], [$mailquote, $mailquote, $mailquote], $s);
587                 return $s;
588         }
589
590         $as = '';
591         $vhead = false;
592         $matches = [];
593         preg_match_all('|\[attach\]href=\"(.*?)\" length=\"(.*?)\" type=\"(.*?)\"(?: title=\"(.*?)\")?|', $item['attach'], $matches, PREG_SET_ORDER);
594         foreach ($matches as $mtch) {
595                 $mime = $mtch[3];
596
597                 $the_url = Contact::magicLinkById($item['author-id'], $mtch[1]);
598
599                 if (strpos($mime, 'video') !== false) {
600                         if (!$vhead) {
601                                 $vhead = true;
602                                 $a->page['htmlhead'] .= Renderer::replaceMacros(Renderer::getMarkupTemplate('videos_head.tpl'), [
603                                         '$baseurl' => System::baseUrl(),
604                                 ]);
605                         }
606
607                         $url_parts = explode('/', $the_url);
608                         $id = end($url_parts);
609                         $as .= Renderer::replaceMacros(Renderer::getMarkupTemplate('video_top.tpl'), [
610                                 '$video' => [
611                                         'id'     => $id,
612                                         'title'  => L10n::t('View Video'),
613                                         'src'    => $the_url,
614                                         'mime'   => $mime,
615                                 ],
616                         ]);
617                 }
618
619                 $filetype = strtolower(substr($mime, 0, strpos($mime, '/')));
620                 if ($filetype) {
621                         $filesubtype = strtolower(substr($mime, strpos($mime, '/') + 1));
622                         $filesubtype = str_replace('.', '-', $filesubtype);
623                 } else {
624                         $filetype = 'unkn';
625                         $filesubtype = 'unkn';
626                 }
627
628                 $title = escape_tags(trim(!empty($mtch[4]) ? $mtch[4] : $mtch[1]));
629                 $title .= ' ' . $mtch[2] . ' ' . L10n::t('bytes');
630
631                 $icon = '<div class="attachtype icon s22 type-' . $filetype . ' subtype-' . $filesubtype . '"></div>';
632                 $as .= '<a href="' . strip_tags($the_url) . '" title="' . $title . '" class="attachlink" target="_blank" >' . $icon . '</a>';
633         }
634
635         if ($as != '') {
636                 $s .= '<div class="body-attach">'.$as.'<div class="clear"></div></div>';
637         }
638
639         // Map.
640         if (strpos($s, '<div class="map">') !== false && x($item, 'coord')) {
641                 $x = Map::byCoordinates(trim($item['coord']));
642                 if ($x) {
643                         $s = preg_replace('/\<div class\=\"map\"\>/', '$0' . $x, $s);
644                 }
645         }
646
647
648         // Look for spoiler.
649         $spoilersearch = '<blockquote class="spoiler">';
650
651         // Remove line breaks before the spoiler.
652         while ((strpos($s, "\n" . $spoilersearch) !== false)) {
653                 $s = str_replace("\n" . $spoilersearch, $spoilersearch, $s);
654         }
655         while ((strpos($s, "<br />" . $spoilersearch) !== false)) {
656                 $s = str_replace("<br />" . $spoilersearch, $spoilersearch, $s);
657         }
658
659         while ((strpos($s, $spoilersearch) !== false)) {
660                 $pos = strpos($s, $spoilersearch);
661                 $rnd = random_string(8);
662                 $spoilerreplace = '<br /> <span id="spoiler-wrap-' . $rnd . '" class="spoiler-wrap fakelink" onclick="openClose(\'spoiler-' . $rnd . '\');">' . L10n::t('Click to open/close') . '</span>'.
663                                         '<blockquote class="spoiler" id="spoiler-' . $rnd . '" style="display: none;">';
664                 $s = substr($s, 0, $pos) . $spoilerreplace . substr($s, $pos + strlen($spoilersearch));
665         }
666
667         // Look for quote with author.
668         $authorsearch = '<blockquote class="author">';
669
670         while ((strpos($s, $authorsearch) !== false)) {
671                 $pos = strpos($s, $authorsearch);
672                 $rnd = random_string(8);
673                 $authorreplace = '<br /> <span id="author-wrap-' . $rnd . '" class="author-wrap fakelink" onclick="openClose(\'author-' . $rnd . '\');">' . L10n::t('Click to open/close') . '</span>'.
674                                         '<blockquote class="author" id="author-' . $rnd . '" style="display: block;">';
675                 $s = substr($s, 0, $pos) . $authorreplace . substr($s, $pos + strlen($authorsearch));
676         }
677
678         // Replace friendica image url size with theme preference.
679         if (x($a->theme_info, 'item_image_size')){
680                 $ps = $a->theme_info['item_image_size'];
681                 $s = preg_replace('|(<img[^>]+src="[^"]+/photo/[0-9a-f]+)-[0-9]|', "$1-" . $ps, $s);
682         }
683
684         $s = HTML::applyContentFilter($s, $filter_reasons);
685
686         $hook_data = ['item' => $item, 'html' => $s];
687         Addon::callHooks('prepare_body_final', $hook_data);
688
689         return $hook_data['html'];
690 }
691
692 /**
693  * @brief Given a text string, convert from bbcode to html and add smilie icons.
694  *
695  * @param string $text String with bbcode.
696  * @return string Formattet HTML.
697  */
698 function prepare_text($text) {
699         if (stristr($text, '[nosmile]')) {
700                 $s = BBCode::convert($text);
701         } else {
702                 $s = Smilies::replace(BBCode::convert($text));
703         }
704
705         return trim($s);
706 }
707
708 /**
709  * return array with details for categories and folders for an item
710  *
711  * @param array $item
712  * @return array
713  *
714   * [
715  *      [ // categories array
716  *          {
717  *               'name': 'category name',
718  *               'removeurl': 'url to remove this category',
719  *               'first': 'is the first in this array? true/false',
720  *               'last': 'is the last in this array? true/false',
721  *           } ,
722  *           ....
723  *       ],
724  *       [ //folders array
725  *                      {
726  *               'name': 'folder name',
727  *               'removeurl': 'url to remove this folder',
728  *               'first': 'is the first in this array? true/false',
729  *               'last': 'is the last in this array? true/false',
730  *           } ,
731  *           ....
732  *       ]
733  *  ]
734  */
735 function get_cats_and_terms($item)
736 {
737         $categories = [];
738         $folders = [];
739
740         $matches = false;
741         $first = true;
742         $cnt = preg_match_all('/<(.*?)>/', $item['file'], $matches, PREG_SET_ORDER);
743         if ($cnt) {
744                 foreach ($matches as $mtch) {
745                         $categories[] = [
746                                 'name' => XML::escape(FileTag::decode($mtch[1])),
747                                 'url' =>  "#",
748                                 'removeurl' => ((local_user() == $item['uid'])?'filerm/' . $item['id'] . '?f=&cat=' . XML::escape(FileTag::decode($mtch[1])):""),
749                                 'first' => $first,
750                                 'last' => false
751                         ];
752                         $first = false;
753                 }
754         }
755
756         if (count($categories)) {
757                 $categories[count($categories) - 1]['last'] = true;
758         }
759
760         if (local_user() == $item['uid']) {
761                 $matches = false;
762                 $first = true;
763                 $cnt = preg_match_all('/\[(.*?)\]/', $item['file'], $matches, PREG_SET_ORDER);
764                 if ($cnt) {
765                         foreach ($matches as $mtch) {
766                                 $folders[] = [
767                                         'name' => XML::escape(FileTag::decode($mtch[1])),
768                                         'url' =>  "#",
769                                         'removeurl' => ((local_user() == $item['uid']) ? 'filerm/' . $item['id'] . '?f=&term=' . XML::escape(FileTag::decode($mtch[1])) : ""),
770                                         'first' => $first,
771                                         'last' => false
772                                 ];
773                                 $first = false;
774                         }
775                 }
776         }
777
778         if (count($folders)) {
779                 $folders[count($folders) - 1]['last'] = true;
780         }
781
782         return [$categories, $folders];
783 }
784
785
786 /**
787  * get private link for item
788  * @param array $item
789  * @return boolean|array False if item has not plink, otherwise array('href'=>plink url, 'title'=>translated title)
790  */
791 function get_plink($item) {
792         $a = get_app();
793
794         if ($a->user['nickname'] != "") {
795                 $ret = [
796                                 //'href' => "display/" . $a->user['nickname'] . "/" . $item['id'],
797                                 'href' => "display/" . $item['guid'],
798                                 'orig' => "display/" . $item['guid'],
799                                 'title' => L10n::t('View on separate page'),
800                                 'orig_title' => L10n::t('view on separate page'),
801                         ];
802
803                 if (x($item, 'plink')) {
804                         $ret["href"] = $a->removeBaseURL($item['plink']);
805                         $ret["title"] = L10n::t('link to source');
806                 }
807
808         } elseif (x($item, 'plink') && ($item['private'] != 1)) {
809                 $ret = [
810                                 'href' => $item['plink'],
811                                 'orig' => $item['plink'],
812                                 'title' => L10n::t('link to source'),
813                         ];
814         } else {
815                 $ret = [];
816         }
817
818         return $ret;
819 }
820
821 /**
822  * return number of bytes in size (K, M, G)
823  * @param string $size_str
824  * @return number
825  */
826 function return_bytes($size_str) {
827         switch (substr ($size_str, -1)) {
828                 case 'M': case 'm': return (int)$size_str * 1048576;
829                 case 'K': case 'k': return (int)$size_str * 1024;
830                 case 'G': case 'g': return (int)$size_str * 1073741824;
831                 default: return $size_str;
832         }
833 }
834
835 /**
836  * @param string $s
837  * @param boolean $strip_padding
838  * @return string
839  */
840 function base64url_encode($s, $strip_padding = false) {
841
842         $s = strtr(base64_encode($s), '+/', '-_');
843
844         if ($strip_padding) {
845                 $s = str_replace('=','',$s);
846         }
847
848         return $s;
849 }
850
851 /**
852  * @param string $s
853  * @return string
854  */
855 function base64url_decode($s) {
856
857         if (is_array($s)) {
858                 Logger::log('base64url_decode: illegal input: ' . print_r(debug_backtrace(), true));
859                 return $s;
860         }
861
862 /*
863  *  // Placeholder for new rev of salmon which strips base64 padding.
864  *  // PHP base64_decode handles the un-padded input without requiring this step
865  *  // Uncomment if you find you need it.
866  *
867  *      $l = strlen($s);
868  *      if (!strpos($s,'=')) {
869  *              $m = $l % 4;
870  *              if ($m == 2)
871  *                      $s .= '==';
872  *              if ($m == 3)
873  *                      $s .= '=';
874  *      }
875  *
876  */
877
878         return base64_decode(strtr($s,'-_','+/'));
879 }
880
881
882 function bb_translate_video($s) {
883
884         $matches = null;
885         $r = preg_match_all("/\[video\](.*?)\[\/video\]/ism",$s,$matches,PREG_SET_ORDER);
886         if ($r) {
887                 foreach ($matches as $mtch) {
888                         if ((stristr($mtch[1], 'youtube')) || (stristr($mtch[1], 'youtu.be'))) {
889                                 $s = str_replace($mtch[0], '[youtube]' . $mtch[1] . '[/youtube]', $s);
890                         } elseif (stristr($mtch[1], 'vimeo')) {
891                                 $s = str_replace($mtch[0], '[vimeo]' . $mtch[1] . '[/vimeo]', $s);
892                         }
893                 }
894         }
895         return $s;
896 }
897
898 /**
899  * get translated item type
900  *
901  * @param array $itme
902  * @return string
903  */
904 function item_post_type($item) {
905         if (!empty($item['event-id'])) {
906                 return L10n::t('event');
907         } elseif (!empty($item['resource-id'])) {
908                 return L10n::t('photo');
909         } elseif (!empty($item['verb']) && $item['verb'] !== ACTIVITY_POST) {
910                 return L10n::t('activity');
911         } elseif ($item['id'] != $item['parent']) {
912                 return L10n::t('comment');
913         }
914
915         return L10n::t('post');
916 }
917
918 function normalise_openid($s) {
919         return trim(str_replace(['http://', 'https://'], ['', ''], $s), '/');
920 }
921
922
923 function undo_post_tagging($s) {
924         $matches = null;
925         $cnt = preg_match_all('/([!#@])\[url=(.*?)\](.*?)\[\/url\]/ism', $s, $matches, PREG_SET_ORDER);
926         if ($cnt) {
927                 foreach ($matches as $mtch) {
928                         if (in_array($mtch[1], ['!', '@'])) {
929                                 $contact = Contact::getDetailsByURL($mtch[2]);
930                                 $mtch[3] = empty($contact['addr']) ? $mtch[2] : $contact['addr'];
931                         }
932                         $s = str_replace($mtch[0], $mtch[1] . $mtch[3],$s);
933                 }
934         }
935         return $s;
936 }
937
938 function protect_sprintf($s) {
939         return str_replace('%', '%%', $s);
940 }
941
942 /// @TODO Rewrite this
943 function is_a_date_arg($s) {
944         $i = intval($s);
945
946         if ($i > 1900) {
947                 $y = date('Y');
948
949                 if ($i <= $y + 1 && strpos($s, '-') == 4) {
950                         $m = intval(substr($s, 5));
951
952                         if ($m > 0 && $m <= 12) {
953                                 return true;
954                         }
955                 }
956         }
957
958         return false;
959 }
960
961 /**
962  * remove intentation from a text
963  */
964 function deindent($text, $chr = "[\t ]", $count = NULL) {
965         $lines = explode("\n", $text);
966
967         if (is_null($count)) {
968                 $m = [];
969                 $k = 0;
970                 while ($k < count($lines) && strlen($lines[$k]) == 0) {
971                         $k++;
972                 }
973                 preg_match("|^" . $chr . "*|", $lines[$k], $m);
974                 $count = strlen($m[0]);
975         }
976
977         for ($k = 0; $k < count($lines); $k++) {
978                 $lines[$k] = preg_replace("|^" . $chr . "{" . $count . "}|", "", $lines[$k]);
979         }
980
981         return implode("\n", $lines);
982 }
983
984 function formatBytes($bytes, $precision = 2) {
985         $units = ['B', 'KB', 'MB', 'GB', 'TB'];
986
987         $bytes = max($bytes, 0);
988         $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
989         $pow = min($pow, count($units) - 1);
990
991         $bytes /= pow(1024, $pow);
992
993         return round($bytes, $precision) . ' ' . $units[$pow];
994 }
995
996 /**
997  * @brief translate and format the networkname of a contact
998  *
999  * @param string $network
1000  *      Networkname of the contact (e.g. dfrn, rss and so on)
1001  * @param sting $url
1002  *      The contact url
1003  * @return string
1004  */
1005 function format_network_name($network, $url = 0) {
1006         if ($network != "") {
1007                 if ($url != "") {
1008                         $network_name = '<a href="'.$url.'">'.ContactSelector::networkToName($network, $url)."</a>";
1009                 } else {
1010                         $network_name = ContactSelector::networkToName($network);
1011                 }
1012
1013                 return $network_name;
1014         }
1015 }