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