]> git.mxchange.org Git - friendica.git/blob - include/text.php
Merge pull request #6092 from JonnyTischbein/issue_drop_item_return_non-frio
[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  * Normalize url
353  *
354  * @param string $url
355  * @return string
356  */
357 function normalise_link($url) {
358         $ret = str_replace(['https:', '//www.'], ['http:', '//'], $url);
359         return rtrim($ret,'/');
360 }
361
362
363 /**
364  * Compare two URLs to see if they are the same, but ignore
365  * slight but hopefully insignificant differences such as if one
366  * is https and the other isn't, or if one is www.something and
367  * the other isn't - and also ignore case differences.
368  *
369  * @param string $a first url
370  * @param string $b second url
371  * @return boolean True if the URLs match, otherwise False
372  *
373  */
374 function link_compare($a, $b) {
375         return (strcasecmp(normalise_link($a), normalise_link($b)) === 0);
376 }
377
378
379 /**
380  * @brief Find any non-embedded images in private items and add redir links to them
381  *
382  * @param App $a
383  * @param array &$item The field array of an item row
384  */
385 function redir_private_images($a, &$item)
386 {
387         $matches = false;
388         $cnt = preg_match_all('|\[img\](http[^\[]*?/photo/[a-fA-F0-9]+?(-[0-9]\.[\w]+?)?)\[\/img\]|', $item['body'], $matches, PREG_SET_ORDER);
389         if ($cnt) {
390                 foreach ($matches as $mtch) {
391                         if (strpos($mtch[1], '/redir') !== false) {
392                                 continue;
393                         }
394
395                         if ((local_user() == $item['uid']) && ($item['private'] == 1) && ($item['contact-id'] != $a->contact['id']) && ($item['network'] == Protocol::DFRN)) {
396                                 $img_url = 'redir?f=1&quiet=1&url=' . urlencode($mtch[1]) . '&conurl=' . urlencode($item['author-link']);
397                                 $item['body'] = str_replace($mtch[0], '[img]' . $img_url . '[/img]', $item['body']);
398                         }
399                 }
400         }
401 }
402
403 /**
404  * Sets the "rendered-html" field of the provided item
405  *
406  * Body is preserved to avoid side-effects as we modify it just-in-time for spoilers and private image links
407  *
408  * @param array $item
409  * @param bool  $update
410  *
411  * @todo Remove reference, simply return "rendered-html" and "rendered-hash"
412  */
413 function put_item_in_cache(&$item, $update = false)
414 {
415         $body = $item["body"];
416
417         $rendered_hash = defaults($item, 'rendered-hash', '');
418         $rendered_html = defaults($item, 'rendered-html', '');
419
420         if ($rendered_hash == ''
421                 || $rendered_html == ""
422                 || $rendered_hash != hash("md5", $item["body"])
423                 || Config::get("system", "ignore_cache")
424         ) {
425                 $a = get_app();
426                 redir_private_images($a, $item);
427
428                 $item["rendered-html"] = prepare_text($item["body"]);
429                 $item["rendered-hash"] = hash("md5", $item["body"]);
430
431                 $hook_data = ['item' => $item, 'rendered-html' => $item['rendered-html'], 'rendered-hash' => $item['rendered-hash']];
432                 Addon::callHooks('put_item_in_cache', $hook_data);
433                 $item['rendered-html'] = $hook_data['rendered-html'];
434                 $item['rendered-hash'] = $hook_data['rendered-hash'];
435                 unset($hook_data);
436
437                 // Force an update if the generated values differ from the existing ones
438                 if ($rendered_hash != $item["rendered-hash"]) {
439                         $update = true;
440                 }
441
442                 // Only compare the HTML when we forcefully ignore the cache
443                 if (Config::get("system", "ignore_cache") && ($rendered_html != $item["rendered-html"])) {
444                         $update = true;
445                 }
446
447                 if ($update && !empty($item["id"])) {
448                         Item::update(['rendered-html' => $item["rendered-html"], 'rendered-hash' => $item["rendered-hash"]],
449                                         ['id' => $item["id"]]);
450                 }
451         }
452
453         $item["body"] = $body;
454 }
455
456 /**
457  * @brief Given an item array, convert the body element from bbcode to html and add smilie icons.
458  * If attach is true, also add icons for item attachments.
459  *
460  * @param array   $item
461  * @param boolean $attach
462  * @param boolean $is_preview
463  * @return string item body html
464  * @hook prepare_body_init item array before any work
465  * @hook prepare_body_content_filter ('item'=>item array, 'filter_reasons'=>string array) before first bbcode to html
466  * @hook prepare_body ('item'=>item array, 'html'=>body string, 'is_preview'=>boolean, 'filter_reasons'=>string array) after first bbcode to html
467  * @hook prepare_body_final ('item'=>item array, 'html'=>body string) after attach icons and blockquote special case handling (spoiler, author)
468  */
469 function prepare_body(array &$item, $attach = false, $is_preview = false)
470 {
471         $a = get_app();
472         Addon::callHooks('prepare_body_init', $item);
473
474         // In order to provide theme developers more possibilities, event items
475         // are treated differently.
476         if ($item['object-type'] === ACTIVITY_OBJ_EVENT && isset($item['event-id'])) {
477                 $ev = Event::getItemHTML($item);
478                 return $ev;
479         }
480
481         $tags = \Friendica\Model\Term::populateTagsFromItem($item);
482
483         $item['tags'] = $tags['tags'];
484         $item['hashtags'] = $tags['hashtags'];
485         $item['mentions'] = $tags['mentions'];
486
487         // Compile eventual content filter reasons
488         $filter_reasons = [];
489         if (!$is_preview && public_contact() != $item['author-id']) {
490                 if (!empty($item['content-warning']) && (!local_user() || !PConfig::get(local_user(), 'system', 'disable_cw', false))) {
491                         $filter_reasons[] = L10n::t('Content warning: %s', $item['content-warning']);
492                 }
493
494                 $hook_data = [
495                         'item' => $item,
496                         'filter_reasons' => $filter_reasons
497                 ];
498                 Addon::callHooks('prepare_body_content_filter', $hook_data);
499                 $filter_reasons = $hook_data['filter_reasons'];
500                 unset($hook_data);
501         }
502
503         // Update the cached values if there is no "zrl=..." on the links.
504         $update = (!local_user() && !remote_user() && ($item["uid"] == 0));
505
506         // Or update it if the current viewer is the intented viewer.
507         if (($item["uid"] == local_user()) && ($item["uid"] != 0)) {
508                 $update = true;
509         }
510
511         put_item_in_cache($item, $update);
512         $s = $item["rendered-html"];
513
514         $hook_data = [
515                 'item' => $item,
516                 'html' => $s,
517                 'preview' => $is_preview,
518                 'filter_reasons' => $filter_reasons
519         ];
520         Addon::callHooks('prepare_body', $hook_data);
521         $s = $hook_data['html'];
522         unset($hook_data);
523
524         if (!$attach) {
525                 // Replace the blockquotes with quotes that are used in mails.
526                 $mailquote = '<blockquote type="cite" class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;">';
527                 $s = str_replace(['<blockquote>', '<blockquote class="spoiler">', '<blockquote class="author">'], [$mailquote, $mailquote, $mailquote], $s);
528                 return $s;
529         }
530
531         $as = '';
532         $vhead = false;
533         $matches = [];
534         preg_match_all('|\[attach\]href=\"(.*?)\" length=\"(.*?)\" type=\"(.*?)\"(?: title=\"(.*?)\")?|', $item['attach'], $matches, PREG_SET_ORDER);
535         foreach ($matches as $mtch) {
536                 $mime = $mtch[3];
537
538                 $the_url = Contact::magicLinkById($item['author-id'], $mtch[1]);
539
540                 if (strpos($mime, 'video') !== false) {
541                         if (!$vhead) {
542                                 $vhead = true;
543                                 $a->page['htmlhead'] .= Renderer::replaceMacros(Renderer::getMarkupTemplate('videos_head.tpl'), [
544                                         '$baseurl' => System::baseUrl(),
545                                 ]);
546                         }
547
548                         $url_parts = explode('/', $the_url);
549                         $id = end($url_parts);
550                         $as .= Renderer::replaceMacros(Renderer::getMarkupTemplate('video_top.tpl'), [
551                                 '$video' => [
552                                         'id'     => $id,
553                                         'title'  => L10n::t('View Video'),
554                                         'src'    => $the_url,
555                                         'mime'   => $mime,
556                                 ],
557                         ]);
558                 }
559
560                 $filetype = strtolower(substr($mime, 0, strpos($mime, '/')));
561                 if ($filetype) {
562                         $filesubtype = strtolower(substr($mime, strpos($mime, '/') + 1));
563                         $filesubtype = str_replace('.', '-', $filesubtype);
564                 } else {
565                         $filetype = 'unkn';
566                         $filesubtype = 'unkn';
567                 }
568
569                 $title = escape_tags(trim(!empty($mtch[4]) ? $mtch[4] : $mtch[1]));
570                 $title .= ' ' . $mtch[2] . ' ' . L10n::t('bytes');
571
572                 $icon = '<div class="attachtype icon s22 type-' . $filetype . ' subtype-' . $filesubtype . '"></div>';
573                 $as .= '<a href="' . strip_tags($the_url) . '" title="' . $title . '" class="attachlink" target="_blank" >' . $icon . '</a>';
574         }
575
576         if ($as != '') {
577                 $s .= '<div class="body-attach">'.$as.'<div class="clear"></div></div>';
578         }
579
580         // Map.
581         if (strpos($s, '<div class="map">') !== false && x($item, 'coord')) {
582                 $x = Map::byCoordinates(trim($item['coord']));
583                 if ($x) {
584                         $s = preg_replace('/\<div class\=\"map\"\>/', '$0' . $x, $s);
585                 }
586         }
587
588
589         // Look for spoiler.
590         $spoilersearch = '<blockquote class="spoiler">';
591
592         // Remove line breaks before the spoiler.
593         while ((strpos($s, "\n" . $spoilersearch) !== false)) {
594                 $s = str_replace("\n" . $spoilersearch, $spoilersearch, $s);
595         }
596         while ((strpos($s, "<br />" . $spoilersearch) !== false)) {
597                 $s = str_replace("<br />" . $spoilersearch, $spoilersearch, $s);
598         }
599
600         while ((strpos($s, $spoilersearch) !== false)) {
601                 $pos = strpos($s, $spoilersearch);
602                 $rnd = random_string(8);
603                 $spoilerreplace = '<br /> <span id="spoiler-wrap-' . $rnd . '" class="spoiler-wrap fakelink" onclick="openClose(\'spoiler-' . $rnd . '\');">' . L10n::t('Click to open/close') . '</span>'.
604                                         '<blockquote class="spoiler" id="spoiler-' . $rnd . '" style="display: none;">';
605                 $s = substr($s, 0, $pos) . $spoilerreplace . substr($s, $pos + strlen($spoilersearch));
606         }
607
608         // Look for quote with author.
609         $authorsearch = '<blockquote class="author">';
610
611         while ((strpos($s, $authorsearch) !== false)) {
612                 $pos = strpos($s, $authorsearch);
613                 $rnd = random_string(8);
614                 $authorreplace = '<br /> <span id="author-wrap-' . $rnd . '" class="author-wrap fakelink" onclick="openClose(\'author-' . $rnd . '\');">' . L10n::t('Click to open/close') . '</span>'.
615                                         '<blockquote class="author" id="author-' . $rnd . '" style="display: block;">';
616                 $s = substr($s, 0, $pos) . $authorreplace . substr($s, $pos + strlen($authorsearch));
617         }
618
619         // Replace friendica image url size with theme preference.
620         if (x($a->theme_info, 'item_image_size')){
621                 $ps = $a->theme_info['item_image_size'];
622                 $s = preg_replace('|(<img[^>]+src="[^"]+/photo/[0-9a-f]+)-[0-9]|', "$1-" . $ps, $s);
623         }
624
625         $s = HTML::applyContentFilter($s, $filter_reasons);
626
627         $hook_data = ['item' => $item, 'html' => $s];
628         Addon::callHooks('prepare_body_final', $hook_data);
629
630         return $hook_data['html'];
631 }
632
633 /**
634  * @brief Given a text string, convert from bbcode to html and add smilie icons.
635  *
636  * @param string $text String with bbcode.
637  * @return string Formattet HTML.
638  */
639 function prepare_text($text) {
640         if (stristr($text, '[nosmile]')) {
641                 $s = BBCode::convert($text);
642         } else {
643                 $s = Smilies::replace(BBCode::convert($text));
644         }
645
646         return trim($s);
647 }
648
649 /**
650  * return array with details for categories and folders for an item
651  *
652  * @param array $item
653  * @return array
654  *
655   * [
656  *      [ // categories array
657  *          {
658  *               'name': 'category name',
659  *               'removeurl': 'url to remove this category',
660  *               'first': 'is the first in this array? true/false',
661  *               'last': 'is the last in this array? true/false',
662  *           } ,
663  *           ....
664  *       ],
665  *       [ //folders array
666  *                      {
667  *               'name': 'folder name',
668  *               'removeurl': 'url to remove this folder',
669  *               'first': 'is the first in this array? true/false',
670  *               'last': 'is the last in this array? true/false',
671  *           } ,
672  *           ....
673  *       ]
674  *  ]
675  */
676 function get_cats_and_terms($item)
677 {
678         $categories = [];
679         $folders = [];
680
681         $matches = false;
682         $first = true;
683         $cnt = preg_match_all('/<(.*?)>/', $item['file'], $matches, PREG_SET_ORDER);
684         if ($cnt) {
685                 foreach ($matches as $mtch) {
686                         $categories[] = [
687                                 'name' => XML::escape(FileTag::decode($mtch[1])),
688                                 'url' =>  "#",
689                                 'removeurl' => ((local_user() == $item['uid'])?'filerm/' . $item['id'] . '?f=&cat=' . XML::escape(FileTag::decode($mtch[1])):""),
690                                 'first' => $first,
691                                 'last' => false
692                         ];
693                         $first = false;
694                 }
695         }
696
697         if (count($categories)) {
698                 $categories[count($categories) - 1]['last'] = true;
699         }
700
701         if (local_user() == $item['uid']) {
702                 $matches = false;
703                 $first = true;
704                 $cnt = preg_match_all('/\[(.*?)\]/', $item['file'], $matches, PREG_SET_ORDER);
705                 if ($cnt) {
706                         foreach ($matches as $mtch) {
707                                 $folders[] = [
708                                         'name' => XML::escape(FileTag::decode($mtch[1])),
709                                         'url' =>  "#",
710                                         'removeurl' => ((local_user() == $item['uid']) ? 'filerm/' . $item['id'] . '?f=&term=' . XML::escape(FileTag::decode($mtch[1])) : ""),
711                                         'first' => $first,
712                                         'last' => false
713                                 ];
714                                 $first = false;
715                         }
716                 }
717         }
718
719         if (count($folders)) {
720                 $folders[count($folders) - 1]['last'] = true;
721         }
722
723         return [$categories, $folders];
724 }
725
726
727 /**
728  * get private link for item
729  * @param array $item
730  * @return boolean|array False if item has not plink, otherwise array('href'=>plink url, 'title'=>translated title)
731  */
732 function get_plink($item) {
733         $a = get_app();
734
735         if ($a->user['nickname'] != "") {
736                 $ret = [
737                                 //'href' => "display/" . $a->user['nickname'] . "/" . $item['id'],
738                                 'href' => "display/" . $item['guid'],
739                                 'orig' => "display/" . $item['guid'],
740                                 'title' => L10n::t('View on separate page'),
741                                 'orig_title' => L10n::t('view on separate page'),
742                         ];
743
744                 if (x($item, 'plink')) {
745                         $ret["href"] = $a->removeBaseURL($item['plink']);
746                         $ret["title"] = L10n::t('link to source');
747                 }
748
749         } elseif (x($item, 'plink') && ($item['private'] != 1)) {
750                 $ret = [
751                                 'href' => $item['plink'],
752                                 'orig' => $item['plink'],
753                                 'title' => L10n::t('link to source'),
754                         ];
755         } else {
756                 $ret = [];
757         }
758
759         return $ret;
760 }
761
762 /**
763  * return number of bytes in size (K, M, G)
764  * @param string $size_str
765  * @return number
766  */
767 function return_bytes($size_str) {
768         switch (substr ($size_str, -1)) {
769                 case 'M': case 'm': return (int)$size_str * 1048576;
770                 case 'K': case 'k': return (int)$size_str * 1024;
771                 case 'G': case 'g': return (int)$size_str * 1073741824;
772                 default: return $size_str;
773         }
774 }
775
776 /**
777  * @param string $s
778  * @param boolean $strip_padding
779  * @return string
780  */
781 function base64url_encode($s, $strip_padding = false) {
782
783         $s = strtr(base64_encode($s), '+/', '-_');
784
785         if ($strip_padding) {
786                 $s = str_replace('=','',$s);
787         }
788
789         return $s;
790 }
791
792 /**
793  * @param string $s
794  * @return string
795  */
796 function base64url_decode($s) {
797
798         if (is_array($s)) {
799                 Logger::log('base64url_decode: illegal input: ' . print_r(debug_backtrace(), true));
800                 return $s;
801         }
802
803 /*
804  *  // Placeholder for new rev of salmon which strips base64 padding.
805  *  // PHP base64_decode handles the un-padded input without requiring this step
806  *  // Uncomment if you find you need it.
807  *
808  *      $l = strlen($s);
809  *      if (!strpos($s,'=')) {
810  *              $m = $l % 4;
811  *              if ($m == 2)
812  *                      $s .= '==';
813  *              if ($m == 3)
814  *                      $s .= '=';
815  *      }
816  *
817  */
818
819         return base64_decode(strtr($s,'-_','+/'));
820 }
821
822
823 function bb_translate_video($s) {
824
825         $matches = null;
826         $r = preg_match_all("/\[video\](.*?)\[\/video\]/ism",$s,$matches,PREG_SET_ORDER);
827         if ($r) {
828                 foreach ($matches as $mtch) {
829                         if ((stristr($mtch[1], 'youtube')) || (stristr($mtch[1], 'youtu.be'))) {
830                                 $s = str_replace($mtch[0], '[youtube]' . $mtch[1] . '[/youtube]', $s);
831                         } elseif (stristr($mtch[1], 'vimeo')) {
832                                 $s = str_replace($mtch[0], '[vimeo]' . $mtch[1] . '[/vimeo]', $s);
833                         }
834                 }
835         }
836         return $s;
837 }
838
839 /**
840  * get translated item type
841  *
842  * @param array $itme
843  * @return string
844  */
845 function item_post_type($item) {
846         if (!empty($item['event-id'])) {
847                 return L10n::t('event');
848         } elseif (!empty($item['resource-id'])) {
849                 return L10n::t('photo');
850         } elseif (!empty($item['verb']) && $item['verb'] !== ACTIVITY_POST) {
851                 return L10n::t('activity');
852         } elseif ($item['id'] != $item['parent']) {
853                 return L10n::t('comment');
854         }
855
856         return L10n::t('post');
857 }
858
859 function normalise_openid($s) {
860         return trim(str_replace(['http://', 'https://'], ['', ''], $s), '/');
861 }
862
863
864 function undo_post_tagging($s) {
865         $matches = null;
866         $cnt = preg_match_all('/([!#@])\[url=(.*?)\](.*?)\[\/url\]/ism', $s, $matches, PREG_SET_ORDER);
867         if ($cnt) {
868                 foreach ($matches as $mtch) {
869                         if (in_array($mtch[1], ['!', '@'])) {
870                                 $contact = Contact::getDetailsByURL($mtch[2]);
871                                 $mtch[3] = empty($contact['addr']) ? $mtch[2] : $contact['addr'];
872                         }
873                         $s = str_replace($mtch[0], $mtch[1] . $mtch[3],$s);
874                 }
875         }
876         return $s;
877 }
878
879 function protect_sprintf($s) {
880         return str_replace('%', '%%', $s);
881 }
882
883 /// @TODO Rewrite this
884 function is_a_date_arg($s) {
885         $i = intval($s);
886
887         if ($i > 1900) {
888                 $y = date('Y');
889
890                 if ($i <= $y + 1 && strpos($s, '-') == 4) {
891                         $m = intval(substr($s, 5));
892
893                         if ($m > 0 && $m <= 12) {
894                                 return true;
895                         }
896                 }
897         }
898
899         return false;
900 }
901
902 /**
903  * remove intentation from a text
904  */
905 function deindent($text, $chr = "[\t ]", $count = NULL) {
906         $lines = explode("\n", $text);
907
908         if (is_null($count)) {
909                 $m = [];
910                 $k = 0;
911                 while ($k < count($lines) && strlen($lines[$k]) == 0) {
912                         $k++;
913                 }
914                 preg_match("|^" . $chr . "*|", $lines[$k], $m);
915                 $count = strlen($m[0]);
916         }
917
918         for ($k = 0; $k < count($lines); $k++) {
919                 $lines[$k] = preg_replace("|^" . $chr . "{" . $count . "}|", "", $lines[$k]);
920         }
921
922         return implode("\n", $lines);
923 }
924
925 function formatBytes($bytes, $precision = 2) {
926         $units = ['B', 'KB', 'MB', 'GB', 'TB'];
927
928         $bytes = max($bytes, 0);
929         $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
930         $pow = min($pow, count($units) - 1);
931
932         $bytes /= pow(1024, $pow);
933
934         return round($bytes, $precision) . ' ' . $units[$pow];
935 }
936
937 /**
938  * @brief translate and format the networkname of a contact
939  *
940  * @param string $network
941  *      Networkname of the contact (e.g. dfrn, rss and so on)
942  * @param sting $url
943  *      The contact url
944  * @return string
945  */
946 function format_network_name($network, $url = 0) {
947         if ($network != "") {
948                 if ($url != "") {
949                         $network_name = '<a href="'.$url.'">'.ContactSelector::networkToName($network, $url)."</a>";
950                 } else {
951                         $network_name = ContactSelector::networkToName($network);
952                 }
953
954                 return $network_name;
955         }
956 }