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