]> git.mxchange.org Git - friendica.git/blob - include/text.php
Merge pull request #16 from MrPetovan/imgbot
[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\System;
16 use Friendica\Database\DBM;
17 use Friendica\Model\Contact;
18 use Friendica\Model\Event;
19 use Friendica\Model\Item;
20 use Friendica\Model\Profile;
21 use Friendica\Render\FriendicaSmarty;
22 use Friendica\Util\DateTimeFormat;
23 use Friendica\Util\Map;
24
25 require_once "mod/proxy.php";
26 require_once "include/conversation.php";
27
28 /**
29  * This is our template processor
30  *
31  * @param string|FriendicaSmarty $s the string requiring macro substitution,
32  *                              or an instance of FriendicaSmarty
33  * @param array $r key value pairs (search => replace)
34  * @return string substituted string
35  */
36 function replace_macros($s, $r) {
37
38         $stamp1 = microtime(true);
39
40         $a = get_app();
41
42         // pass $baseurl to all templates
43         $r['$baseurl'] = System::baseUrl();
44
45         $t = $a->template_engine();
46         try {
47                 $output = $t->replaceMacros($s, $r);
48         } catch (Exception $e) {
49                 echo "<pre><b>" . __FUNCTION__ . "</b>: " . $e->getMessage() . "</pre>";
50                 killme();
51         }
52
53         $a->save_timestamp($stamp1, "rendering");
54
55         return $output;
56 }
57
58 /**
59  * @brief Generates a pseudo-random string of hexadecimal characters
60  *
61  * @param int $size
62  * @return string
63  */
64 function random_string($size = 64)
65 {
66         $byte_size = ceil($size / 2);
67
68         $bytes = random_bytes($byte_size);
69
70         $return = substr(bin2hex($bytes), 0, $size);
71
72         return $return;
73 }
74
75 /**
76  * This is our primary input filter.
77  *
78  * The high bit hack only involved some old IE browser, forget which (IE5/Mac?)
79  * that had an XSS attack vector due to stripping the high-bit on an 8-bit character
80  * after cleansing, and angle chars with the high bit set could get through as markup.
81  *
82  * This is now disabled because it was interfering with some legitimate unicode sequences
83  * and hopefully there aren't a lot of those browsers left.
84  *
85  * Use this on any text input where angle chars are not valid or permitted
86  * They will be replaced with safer brackets. This may be filtered further
87  * if these are not allowed either.
88  *
89  * @param string $string Input string
90  * @return string Filtered string
91  */
92 function notags($string) {
93         return str_replace(["<", ">"], ['[', ']'], $string);
94
95 //  High-bit filter no longer used
96 //      return str_replace(array("<",">","\xBA","\xBC","\xBE"), array('[',']','','',''), $string);
97 }
98
99
100 /**
101  * use this on "body" or "content" input where angle chars shouldn't be removed,
102  * and allow them to be safely displayed.
103  * @param string $string
104  * @return string
105  */
106 function escape_tags($string) {
107         return htmlspecialchars($string, ENT_COMPAT, 'UTF-8', false);
108 }
109
110
111 /**
112  * generate a string that's random, but usually pronounceable.
113  * used to generate initial passwords
114  * @param int $len
115  * @return string
116  */
117 function autoname($len) {
118
119         if ($len <= 0) {
120                 return '';
121         }
122
123         $vowels = ['a','a','ai','au','e','e','e','ee','ea','i','ie','o','ou','u'];
124         if (mt_rand(0, 5) == 4) {
125                 $vowels[] = 'y';
126         }
127
128         $cons = [
129                         'b','bl','br',
130                         'c','ch','cl','cr',
131                         'd','dr',
132                         'f','fl','fr',
133                         'g','gh','gl','gr',
134                         'h',
135                         'j',
136                         'k','kh','kl','kr',
137                         'l',
138                         'm',
139                         'n',
140                         'p','ph','pl','pr',
141                         'qu',
142                         'r','rh',
143                         's','sc','sh','sm','sp','st',
144                         't','th','tr',
145                         'v',
146                         'w','wh',
147                         'x',
148                         'z','zh'
149                         ];
150
151         $midcons = ['ck','ct','gn','ld','lf','lm','lt','mb','mm', 'mn','mp',
152                                 'nd','ng','nk','nt','rn','rp','rt'];
153
154         $noend = ['bl', 'br', 'cl','cr','dr','fl','fr','gl','gr',
155                                 'kh', 'kl','kr','mn','pl','pr','rh','tr','qu','wh','q'];
156
157         $start = mt_rand(0,2);
158         if ($start == 0) {
159                 $table = $vowels;
160         } else {
161                 $table = $cons;
162         }
163
164         $word = '';
165
166         for ($x = 0; $x < $len; $x ++) {
167                 $r = mt_rand(0,count($table) - 1);
168                 $word .= $table[$r];
169
170                 if ($table == $vowels) {
171                         $table = array_merge($cons,$midcons);
172                 } else {
173                         $table = $vowels;
174                 }
175
176         }
177
178         $word = substr($word,0,$len);
179
180         foreach ($noend as $noe) {
181                 $noelen = strlen($noe);
182                 if ((strlen($word) > $noelen) && (substr($word, -$noelen) == $noe)) {
183                         $word = autoname($len);
184                         break;
185                 }
186         }
187
188         return $word;
189 }
190
191
192 /**
193  * escape text ($str) for XML transport
194  * @param string $str
195  * @return string Escaped text.
196  */
197 function xmlify($str) {
198         /// @TODO deprecated code found?
199 /*      $buffer = '';
200
201         $len = mb_strlen($str);
202         for ($x = 0; $x < $len; $x ++) {
203                 $char = mb_substr($str,$x,1);
204
205                 switch($char) {
206
207                         case "\r" :
208                                 break;
209                         case "&" :
210                                 $buffer .= '&amp;';
211                                 break;
212                         case "'" :
213                                 $buffer .= '&apos;';
214                                 break;
215                         case "\"" :
216                                 $buffer .= '&quot;';
217                                 break;
218                         case '<' :
219                                 $buffer .= '&lt;';
220                                 break;
221                         case '>' :
222                                 $buffer .= '&gt;';
223                                 break;
224                         case "\n" :
225                                 $buffer .= "\n";
226                                 break;
227                         default :
228                                 $buffer .= $char;
229                                 break;
230                 }
231         }*/
232         /*
233         $buffer = mb_ereg_replace("&", "&amp;", $str);
234         $buffer = mb_ereg_replace("'", "&apos;", $buffer);
235         $buffer = mb_ereg_replace('"', "&quot;", $buffer);
236         $buffer = mb_ereg_replace("<", "&lt;", $buffer);
237         $buffer = mb_ereg_replace(">", "&gt;", $buffer);
238         */
239         $buffer = htmlspecialchars($str, ENT_QUOTES, "UTF-8");
240         $buffer = trim($buffer);
241
242         return $buffer;
243 }
244
245
246 /**
247  * undo an xmlify
248  * @param string $s xml escaped text
249  * @return string unescaped text
250  */
251 function unxmlify($s) {
252         /// @TODO deprecated code found?
253 //      $ret = str_replace('&amp;','&', $s);
254 //      $ret = str_replace(array('&lt;','&gt;','&quot;','&apos;'),array('<','>','"',"'"),$ret);
255         /*$ret = mb_ereg_replace('&amp;', '&', $s);
256         $ret = mb_ereg_replace('&apos;', "'", $ret);
257         $ret = mb_ereg_replace('&quot;', '"', $ret);
258         $ret = mb_ereg_replace('&lt;', "<", $ret);
259         $ret = mb_ereg_replace('&gt;', ">", $ret);
260         */
261         $ret = htmlspecialchars_decode($s, ENT_QUOTES);
262         return $ret;
263 }
264
265
266 /**
267  * @brief Paginator function. Pushes relevant links in a pager array structure.
268  *
269  * Links are generated depending on the current page and the total number of items.
270  * Inactive links (like "first" and "prev" on page 1) are given the "disabled" class.
271  * Current page link is given the "active" CSS class
272  *
273  * @param App $a App instance
274  * @param int $count [optional] item count (used with minimal pager)
275  * @return Array data for pagination template
276  */
277 function paginate_data(App $a, $count = null) {
278         $stripped = preg_replace('/([&?]page=[0-9]*)/', '', $a->query_string);
279
280         $stripped = str_replace('q=', '', $stripped);
281         $stripped = trim($stripped, '/');
282         $pagenum = $a->pager['page'];
283
284         if (($a->page_offset != '') && !preg_match('/[?&].offset=/', $stripped)) {
285                 $stripped .= '&offset=' . urlencode($a->page_offset);
286         }
287
288         $url = $stripped;
289         $data = [];
290
291         function _l(&$d, $name, $url, $text, $class = '') {
292                 if (strpos($url, '?') === false && ($pos = strpos($url, '&')) !== false) {
293                         $url = substr($url, 0, $pos) . '?' . substr($url, $pos + 1);
294                 }
295
296                 $d[$name] = ['url' => $url, 'text' => $text, 'class' => $class];
297         }
298
299         if (!is_null($count)) {
300                 // minimal pager (newer / older)
301                 $data['class'] = 'pager';
302                 _l($data, 'prev', $url . '&page=' . ($a->pager['page'] - 1), L10n::t('newer'), 'previous' . ($a->pager['page'] == 1 ? ' disabled' : ''));
303                 _l($data, 'next', $url . '&page=' . ($a->pager['page'] + 1), L10n::t('older'), 'next' . ($count <= 0 ? ' disabled' : ''));
304         } else {
305                 // full pager (first / prev / 1 / 2 / ... / 14 / 15 / next / last)
306                 $data['class'] = 'pagination';
307                 if ($a->pager['total'] > $a->pager['itemspage']) {
308                         _l($data, 'first', $url . '&page=1', L10n::t('first'), $a->pager['page'] == 1 ? 'disabled' : '');
309                         _l($data, 'prev', $url . '&page=' . ($a->pager['page'] - 1), L10n::t('prev'), $a->pager['page'] == 1 ? 'disabled' : '');
310
311                         $numpages = $a->pager['total'] / $a->pager['itemspage'];
312
313                         $numstart = 1;
314                         $numstop = $numpages;
315
316                         // Limit the number of displayed page number buttons.
317                         if ($numpages > 8) {
318                                 $numstart = (($pagenum > 4) ? ($pagenum - 4) : 1);
319                                 $numstop = (($pagenum > ($numpages - 7)) ? $numpages : ($numstart + 8));
320                         }
321
322                         $pages = [];
323
324                         for ($i = $numstart; $i <= $numstop; $i++) {
325                                 if ($i == $a->pager['page']) {
326                                         _l($pages, $i, '#',  $i, 'current active');
327                                 } else {
328                                         _l($pages, $i, $url . '&page='. $i, $i, 'n');
329                                 }
330                         }
331
332                         if (($a->pager['total'] % $a->pager['itemspage']) != 0) {
333                                 if ($i == $a->pager['page']) {
334                                         _l($pages, $i, '#',  $i, 'current active');
335                                 } else {
336                                         _l($pages, $i, $url . '&page=' . $i, $i, 'n');
337                                 }
338                         }
339
340                         $data['pages'] = $pages;
341
342                         $lastpage = (($numpages > intval($numpages)) ? intval($numpages)+1 : $numpages);
343                         _l($data, 'next', $url . '&page=' . ($a->pager['page'] + 1), L10n::t('next'), $a->pager['page'] == $lastpage ? 'disabled' : '');
344                         _l($data, 'last', $url . '&page=' . $lastpage, L10n::t('last'), $a->pager['page'] == $lastpage ? 'disabled' : '');
345                 }
346         }
347
348         return $data;
349 }
350
351
352 /**
353  * Automatic pagination.
354  *
355  *  To use, get the count of total items.
356  * Then call $a->set_pager_total($number_items);
357  * Optionally call $a->set_pager_itemspage($n) to the number of items to display on each page
358  * Then call paginate($a) after the end of the display loop to insert the pager block on the page
359  * (assuming there are enough items to paginate).
360  * When using with SQL, the setting LIMIT %d, %d => $a->pager['start'],$a->pager['itemspage']
361  * will limit the results to the correct items for the current page.
362  * The actual page handling is then accomplished at the application layer.
363  *
364  * @param App $a App instance
365  * @return string html for pagination #FIXME remove html
366  */
367 function paginate(App $a) {
368
369         $data = paginate_data($a);
370         $tpl = get_markup_template("paginate.tpl");
371         return replace_macros($tpl, ["pager" => $data]);
372
373 }
374
375
376 /**
377  * Alternative pager
378  * @param App $a App instance
379  * @param int $i
380  * @return string html for pagination #FIXME remove html
381  */
382 function alt_pager(App $a, $i) {
383
384         $data = paginate_data($a, $i);
385         $tpl = get_markup_template("paginate.tpl");
386         return replace_macros($tpl, ['pager' => $data]);
387
388 }
389
390
391 /**
392  * Loader for infinite scrolling
393  * @return string html for loader
394  */
395 function scroll_loader() {
396         $tpl = get_markup_template("scroll_loader.tpl");
397         return replace_macros($tpl, [
398                 'wait' => L10n::t('Loading more entries...'),
399                 'end' => L10n::t('The end')
400         ]);
401 }
402
403
404 /**
405  * Turn user/group ACLs stored as angle bracketed text into arrays
406  *
407  * @param string $s
408  * @return array
409  */
410 function expand_acl($s) {
411         // turn string array of angle-bracketed elements into numeric array
412         // e.g. "<1><2><3>" => array(1,2,3);
413         $ret = [];
414
415         if (strlen($s)) {
416                 $t = str_replace('<', '', $s);
417                 $a = explode('>', $t);
418                 foreach ($a as $aa) {
419                         if (intval($aa)) {
420                                 $ret[] = intval($aa);
421                         }
422                 }
423         }
424         return $ret;
425 }
426
427
428 /**
429  * Wrap ACL elements in angle brackets for storage
430  * @param string $item
431  */
432 function sanitise_acl(&$item) {
433         if (intval($item)) {
434                 $item = '<' . intval(notags(trim($item))) . '>';
435         } else {
436                 unset($item);
437         }
438 }
439
440
441 /**
442  * Convert an ACL array to a storable string
443  *
444  * Normally ACL permissions will be an array.
445  * We'll also allow a comma-separated string.
446  *
447  * @param string|array $p
448  * @return string
449  */
450 function perms2str($p) {
451         $ret = '';
452         if (is_array($p)) {
453                 $tmp = $p;
454         } else {
455                 $tmp = explode(',', $p);
456         }
457
458         if (is_array($tmp)) {
459                 array_walk($tmp, 'sanitise_acl');
460                 $ret = implode('', $tmp);
461         }
462         return $ret;
463 }
464
465 /**
466  * @deprecated
467  * wrapper to load a view template, checking for alternate
468  * languages before falling back to the default
469  *
470  * @global string $lang
471  * @global App $a
472  * @param string $s view name
473  * @return string
474  */
475 function load_view_file($s) {
476         global $lang, $a;
477         if (!isset($lang)) {
478                 $lang = 'en';
479         }
480         $b = basename($s);
481         $d = dirname($s);
482         if (file_exists("$d/$lang/$b")) {
483                 $stamp1 = microtime(true);
484                 $content = file_get_contents("$d/$lang/$b");
485                 $a->save_timestamp($stamp1, "file");
486                 return $content;
487         }
488
489         $theme = $a->getCurrentTheme();
490
491         if (file_exists("$d/theme/$theme/$b")) {
492                 $stamp1 = microtime(true);
493                 $content = file_get_contents("$d/theme/$theme/$b");
494                 $a->save_timestamp($stamp1, "file");
495                 return $content;
496         }
497
498         $stamp1 = microtime(true);
499         $content = file_get_contents($s);
500         $a->save_timestamp($stamp1, "file");
501         return $content;
502 }
503
504
505 /**
506  * load a view template, checking for alternate
507  * languages before falling back to the default
508  *
509  * @global string $lang
510  * @param string $s view path
511  * @return string
512  */
513 function get_intltext_template($s) {
514         global $lang;
515
516         $a = get_app();
517         $engine = '';
518         if ($a->theme['template_engine'] === 'smarty3') {
519                 $engine = "/smarty3";
520         }
521
522         if (!isset($lang)) {
523                 $lang = 'en';
524         }
525
526         if (file_exists("view/lang/$lang$engine/$s")) {
527                 $stamp1 = microtime(true);
528                 $content = file_get_contents("view/lang/$lang$engine/$s");
529                 $a->save_timestamp($stamp1, "file");
530                 return $content;
531         } elseif (file_exists("view/lang/en$engine/$s")) {
532                 $stamp1 = microtime(true);
533                 $content = file_get_contents("view/lang/en$engine/$s");
534                 $a->save_timestamp($stamp1, "file");
535                 return $content;
536         } else {
537                 $stamp1 = microtime(true);
538                 $content = file_get_contents("view$engine/$s");
539                 $a->save_timestamp($stamp1, "file");
540                 return $content;
541         }
542 }
543
544
545 /**
546  * load template $s
547  *
548  * @param string $s
549  * @param string $root
550  * @return string
551  */
552 function get_markup_template($s, $root = '') {
553         $stamp1 = microtime(true);
554
555         $a = get_app();
556         $t = $a->template_engine();
557         try {
558                 $template = $t->getTemplateFile($s, $root);
559         } catch (Exception $e) {
560                 echo "<pre><b>" . __FUNCTION__ . "</b>: " . $e->getMessage() . "</pre>";
561                 killme();
562         }
563
564         $a->save_timestamp($stamp1, "file");
565
566         return $template;
567 }
568
569 /**
570  *  for html,xml parsing - let's say you've got
571  *  an attribute foobar="class1 class2 class3"
572  *  and you want to find out if it contains 'class3'.
573  *  you can't use a normal sub string search because you
574  *  might match 'notclass3' and a regex to do the job is
575  *  possible but a bit complicated.
576  *  pass the attribute string as $attr and the attribute you
577  *  are looking for as $s - returns true if found, otherwise false
578  *
579  * @param string $attr attribute value
580  * @param string $s string to search
581  * @return boolean True if found, False otherwise
582  */
583 function attribute_contains($attr, $s) {
584         $a = explode(' ', $attr);
585         return (count($a) && in_array($s,$a));
586 }
587
588
589 /* setup int->string log level map */
590 $LOGGER_LEVELS = [];
591
592 /**
593  * @brief Logs the given message at the given log level
594  *
595  * log levels:
596  * LOGGER_NORMAL (default)
597  * LOGGER_TRACE
598  * LOGGER_DEBUG
599  * LOGGER_DATA
600  * LOGGER_ALL
601  *
602  * @global App $a
603  * @global array $LOGGER_LEVELS
604  * @param string $msg
605  * @param int $level
606  */
607 function logger($msg, $level = 0) {
608         $a = get_app();
609         global $LOGGER_LEVELS;
610
611         // turn off logger in install mode
612         if (
613                 $a->mode == App::MODE_INSTALL
614                 || !dba::$connected
615         ) {
616                 return;
617         }
618
619         $debugging = Config::get('system','debugging');
620         $logfile   = Config::get('system','logfile');
621         $loglevel = intval(Config::get('system','loglevel'));
622
623         if (
624                 !$debugging
625                 || !$logfile
626                 || $level > $loglevel
627         ) {
628                 return;
629         }
630
631         if (count($LOGGER_LEVELS) == 0) {
632                 foreach (get_defined_constants() as $k => $v) {
633                         if (substr($k, 0, 7) == "LOGGER_") {
634                                 $LOGGER_LEVELS[$v] = substr($k, 7, 7);
635                         }
636                 }
637         }
638
639         $process_id = session_id();
640
641         if ($process_id == '') {
642                 $process_id = get_app()->process_id;
643         }
644
645         $callers = debug_backtrace();
646
647         if (count($callers) > 1) {
648                 $function = $callers[1]['function'];
649         } else {
650                 $function = '';
651         }
652
653         $logline = sprintf("%s@%s\t[%s]:%s:%s:%s\t%s\n",
654                         DateTimeFormat::utcNow(DateTimeFormat::ATOM),
655                         $process_id,
656                         $LOGGER_LEVELS[$level],
657                         basename($callers[0]['file']),
658                         $callers[0]['line'],
659                         $function,
660                         $msg
661                 );
662
663         $stamp1 = microtime(true);
664         @file_put_contents($logfile, $logline, FILE_APPEND);
665         $a->save_timestamp($stamp1, "file");
666 }
667
668 /**
669  * @brief An alternative logger for development.
670  * Works largely as logger() but allows developers
671  * to isolate particular elements they are targetting
672  * personally without background noise
673  *
674  * log levels:
675  * LOGGER_NORMAL (default)
676  * LOGGER_TRACE
677  * LOGGER_DEBUG
678  * LOGGER_DATA
679  * LOGGER_ALL
680  *
681  * @global App $a
682  * @global array $LOGGER_LEVELS
683  * @param string $msg
684  * @param int $level
685  */
686
687 function dlogger($msg, $level = 0) {
688         $a = get_app();
689
690         // turn off logger in install mode
691         if (
692                 $a->mode == App::MODE_INSTALL
693                 || !dba::$connected
694         ) {
695                 return;
696         }
697
698         $logfile = Config::get('system', 'dlogfile');
699         if (!$logfile) {
700                 return;
701         }
702
703         $dlogip = Config::get('system', 'dlogip');
704         if (!is_null($dlogip) && $_SERVER['REMOTE_ADDR'] != $dlogip) {
705                 return;
706         }
707
708         if (count($LOGGER_LEVELS) == 0) {
709                 foreach (get_defined_constants() as $k => $v) {
710                         if (substr($k, 0, 7) == "LOGGER_") {
711                                 $LOGGER_LEVELS[$v] = substr($k, 7, 7);
712                         }
713                 }
714         }
715
716         $process_id = session_id();
717
718         if ($process_id == '') {
719                 $process_id = get_app()->process_id;
720         }
721
722         $callers = debug_backtrace();
723         $logline = sprintf("%s@\t%s:\t%s:\t%s\t%s\t%s\n",
724                         DateTimeFormat::utcNow(),
725                         $process_id,
726                         basename($callers[0]['file']),
727                         $callers[0]['line'],
728                         $callers[1]['function'],
729                         $msg
730                 );
731
732         $stamp1 = microtime(true);
733         @file_put_contents($logfile, $logline, FILE_APPEND);
734         $a->save_timestamp($stamp1, "file");
735 }
736
737
738 /**
739  * Compare activity uri. Knows about activity namespace.
740  *
741  * @param string $haystack
742  * @param string $needle
743  * @return boolean
744  */
745 function activity_match($haystack,$needle) {
746         return (($haystack === $needle) || ((basename($needle) === $haystack) && strstr($needle, NAMESPACE_ACTIVITY_SCHEMA)));
747 }
748
749
750 /**
751  * @brief Pull out all #hashtags and @person tags from $string.
752  *
753  * We also get @person@domain.com - which would make
754  * the regex quite complicated as tags can also
755  * end a sentence. So we'll run through our results
756  * and strip the period from any tags which end with one.
757  * Returns array of tags found, or empty array.
758  *
759  * @param string $string Post content
760  * @return array List of tag and person names
761  */
762 function get_tags($string) {
763         $ret = [];
764
765         // Convert hashtag links to hashtags
766         $string = preg_replace('/#\[url\=([^\[\]]*)\](.*?)\[\/url\]/ism', '#$2', $string);
767
768         // ignore anything in a code block
769         $string = preg_replace('/\[code\](.*?)\[\/code\]/sm', '', $string);
770
771         // Force line feeds at bbtags
772         $string = str_replace(['[', ']'], ["\n[", "]\n"], $string);
773
774         // ignore anything in a bbtag
775         $string = preg_replace('/\[(.*?)\]/sm', '', $string);
776
777         // Match full names against @tags including the space between first and last
778         // We will look these up afterward to see if they are full names or not recognisable.
779
780         if (preg_match_all('/(@[^ \x0D\x0A,:?]+ [^ \x0D\x0A@,:?]+)([ \x0D\x0A@,:?]|$)/', $string, $matches)) {
781                 foreach ($matches[1] as $match) {
782                         if (strstr($match, ']')) {
783                                 // we might be inside a bbcode color tag - leave it alone
784                                 continue;
785                         }
786                         if (substr($match, -1, 1) === '.') {
787                                 $ret[] = substr($match, 0, -1);
788                         } else {
789                                 $ret[] = $match;
790                         }
791                 }
792         }
793
794         // Otherwise pull out single word tags. These can be @nickname, @first_last
795         // and #hash tags.
796
797         if (preg_match_all('/([!#@][^\^ \x0D\x0A,;:?]+)([ \x0D\x0A,;:?]|$)/', $string, $matches)) {
798                 foreach ($matches[1] as $match) {
799                         if (strstr($match, ']')) {
800                                 // we might be inside a bbcode color tag - leave it alone
801                                 continue;
802                         }
803                         if (substr($match, -1, 1) === '.') {
804                                 $match = substr($match,0,-1);
805                         }
806                         // ignore strictly numeric tags like #1
807                         if ((strpos($match, '#') === 0) && ctype_digit(substr($match, 1))) {
808                                 continue;
809                         }
810                         // try not to catch url fragments
811                         if (strpos($string, $match) && preg_match('/[a-zA-z0-9\/]/', substr($string, strpos($string, $match) - 1, 1))) {
812                                 continue;
813                         }
814                         $ret[] = $match;
815                 }
816         }
817         return $ret;
818 }
819
820
821 /**
822  * quick and dirty quoted_printable encoding
823  *
824  * @param string $s
825  * @return string
826  */
827 function qp($s) {
828         return str_replace("%", "=", rawurlencode($s));
829 }
830
831
832 /**
833  * Get html for contact block.
834  *
835  * @template contact_block.tpl
836  * @hook contact_block_end (contacts=>array, output=>string)
837  * @return string
838  */
839 function contact_block() {
840         $o = '';
841         $a = get_app();
842
843         $shown = PConfig::get($a->profile['uid'], 'system', 'display_friend_count', 24);
844         if ($shown == 0) {
845                 return;
846         }
847
848         if (!is_array($a->profile) || $a->profile['hide-friends']) {
849                 return $o;
850         }
851         $r = q("SELECT COUNT(*) AS `total` FROM `contact`
852                         WHERE `uid` = %d AND NOT `self` AND NOT `blocked`
853                                 AND NOT `pending` AND NOT `hidden` AND NOT `archive`
854                                 AND `network` IN ('%s', '%s', '%s')",
855                         intval($a->profile['uid']),
856                         dbesc(NETWORK_DFRN),
857                         dbesc(NETWORK_OSTATUS),
858                         dbesc(NETWORK_DIASPORA)
859         );
860         if (DBM::is_result($r)) {
861                 $total = intval($r[0]['total']);
862         }
863         if (!$total) {
864                 $contacts = L10n::t('No contacts');
865                 $micropro = null;
866         } else {
867                 // Splitting the query in two parts makes it much faster
868                 $r = q("SELECT `id` FROM `contact`
869                                 WHERE `uid` = %d AND NOT `self` AND NOT `blocked`
870                                         AND NOT `pending` AND NOT `hidden` AND NOT `archive`
871                                         AND `network` IN ('%s', '%s', '%s')
872                                 ORDER BY RAND() LIMIT %d",
873                                 intval($a->profile['uid']),
874                                 dbesc(NETWORK_DFRN),
875                                 dbesc(NETWORK_OSTATUS),
876                                 dbesc(NETWORK_DIASPORA),
877                                 intval($shown)
878                 );
879                 if (DBM::is_result($r)) {
880                         $contacts = [];
881                         foreach ($r AS $contact) {
882                                 $contacts[] = $contact["id"];
883                         }
884                         $r = q("SELECT `id`, `uid`, `addr`, `url`, `name`, `thumb`, `network` FROM `contact` WHERE `id` IN (%s)",
885                                 dbesc(implode(",", $contacts)));
886
887                         if (DBM::is_result($r)) {
888                                 $contacts = L10n::tt('%d Contact', '%d Contacts', $total);
889                                 $micropro = [];
890                                 foreach ($r as $rr) {
891                                         $micropro[] = micropro($rr, true, 'mpfriend');
892                                 }
893                         }
894                 }
895         }
896
897         $tpl = get_markup_template('contact_block.tpl');
898         $o = replace_macros($tpl, [
899                 '$contacts' => $contacts,
900                 '$nickname' => $a->profile['nickname'],
901                 '$viewcontacts' => L10n::t('View Contacts'),
902                 '$micropro' => $micropro,
903         ]);
904
905         $arr = ['contacts' => $r, 'output' => $o];
906
907         Addon::callHooks('contact_block_end', $arr);
908         return $o;
909
910 }
911
912
913 /**
914  * @brief Format contacts as picture links or as texxt links
915  *
916  * @param array $contact Array with contacts which contains an array with
917  *      int 'id' => The ID of the contact
918  *      int 'uid' => The user ID of the user who owns this data
919  *      string 'name' => The name of the contact
920  *      string 'url' => The url to the profile page of the contact
921  *      string 'addr' => The webbie of the contact (e.g.) username@friendica.com
922  *      string 'network' => The network to which the contact belongs to
923  *      string 'thumb' => The contact picture
924  *      string 'click' => js code which is performed when clicking on the contact
925  * @param boolean $redirect If true try to use the redir url if it's possible
926  * @param string $class CSS class for the
927  * @param boolean $textmode If true display the contacts as text links
928  *      if false display the contacts as picture links
929
930  * @return string Formatted html
931  */
932 function micropro($contact, $redirect = false, $class = '', $textmode = false) {
933
934         // Use the contact URL if no address is available
935         if (!x($contact, "addr")) {
936                 $contact["addr"] = $contact["url"];
937         }
938
939         $url = $contact['url'];
940         $sparkle = '';
941         $redir = false;
942
943         if ($redirect) {
944                 $url = Contact::magicLink($contact['url']);
945                 if (strpos($url, 'redir/') === 0) {
946                         $sparkle = ' sparkle';
947                 }
948         }
949
950         // If there is some js available we don't need the url
951         if (x($contact, 'click')) {
952                 $url = '';
953         }
954
955         return replace_macros(get_markup_template(($textmode)?'micropro_txt.tpl':'micropro_img.tpl'),[
956                 '$click' => defaults($contact, 'click', ''),
957                 '$class' => $class,
958                 '$url' => $url,
959                 '$photo' => proxy_url($contact['thumb'], false, PROXY_SIZE_THUMB),
960                 '$name' => $contact['name'],
961                 'title' => $contact['name'] . ' [' . $contact['addr'] . ']',
962                 '$parkle' => $sparkle,
963                 '$redir' => $redir,
964
965         ]);
966 }
967
968 /**
969  * Search box.
970  *
971  * @param string $s     Search query.
972  * @param string $id    HTML id
973  * @param string $url   Search url.
974  * @param bool   $save  Show save search button.
975  * @param bool   $aside Display the search widgit aside.
976  *
977  * @return string Formatted HTML.
978  */
979 function search($s, $id = 'search-box', $url = 'search', $save = false, $aside = true)
980 {
981         $mode = 'text';
982
983         if (strpos($s, '#') === 0) {
984                 $mode = 'tag';
985         }
986         $save_label = $mode === 'text' ? L10n::t('Save') : L10n::t('Follow');
987
988         $values = [
989                         '$s' => htmlspecialchars($s),
990                         '$id' => $id,
991                         '$action_url' => $url,
992                         '$search_label' => L10n::t('Search'),
993                         '$save_label' => $save_label,
994                         '$savedsearch' => local_user() && Feature::isEnabled(local_user(),'savedsearch'),
995                         '$search_hint' => L10n::t('@name, !forum, #tags, content'),
996                         '$mode' => $mode
997                 ];
998
999         if (!$aside) {
1000                 $values['$searchoption'] = [
1001                                         L10n::t("Full Text"),
1002                                         L10n::t("Tags"),
1003                                         L10n::t("Contacts")];
1004
1005                 if (Config::get('system','poco_local_search')) {
1006                         $values['$searchoption'][] = L10n::t("Forums");
1007                 }
1008         }
1009
1010         return replace_macros(get_markup_template('searchbox.tpl'), $values);
1011 }
1012
1013 /**
1014  * @brief Check for a valid email string
1015  *
1016  * @param string $email_address
1017  * @return boolean
1018  */
1019 function valid_email($email_address)
1020 {
1021         return preg_match('/^[_a-zA-Z0-9\-\+]+(\.[_a-zA-Z0-9\-\+]+)*@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+$/', $email_address);
1022 }
1023
1024
1025 /**
1026  * Replace naked text hyperlink with HTML formatted hyperlink
1027  *
1028  * @param string $s
1029  */
1030 function linkify($s) {
1031         $s = preg_replace("/(https?\:\/\/[a-zA-Z0-9\:\/\-\?\&\;\.\=\_\~\#\'\%\$\!\+]*)/", ' <a href="$1" target="_blank">$1</a>', $s);
1032         $s = preg_replace("/\<(.*?)(src|href)=(.*?)\&amp\;(.*?)\>/ism",'<$1$2=$3&$4>',$s);
1033         return $s;
1034 }
1035
1036
1037 /**
1038  * Load poke verbs
1039  *
1040  * @return array index is present tense verb
1041  *                               value is array containing past tense verb, translation of present, translation of past
1042  * @hook poke_verbs pokes array
1043  */
1044 function get_poke_verbs() {
1045
1046         // index is present tense verb
1047         // value is array containing past tense verb, translation of present, translation of past
1048
1049         $arr = [
1050                 'poke' => ['poked', L10n::t('poke'), L10n::t('poked')],
1051                 'ping' => ['pinged', L10n::t('ping'), L10n::t('pinged')],
1052                 'prod' => ['prodded', L10n::t('prod'), L10n::t('prodded')],
1053                 'slap' => ['slapped', L10n::t('slap'), L10n::t('slapped')],
1054                 'finger' => ['fingered', L10n::t('finger'), L10n::t('fingered')],
1055                 'rebuff' => ['rebuffed', L10n::t('rebuff'), L10n::t('rebuffed')],
1056         ];
1057         Addon::callHooks('poke_verbs', $arr);
1058         return $arr;
1059 }
1060
1061 /**
1062  * @brief Translate days and months names.
1063  *
1064  * @param string $s String with day or month name.
1065  * @return string Translated string.
1066  */
1067 function day_translate($s) {
1068         $ret = str_replace(['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'],
1069                 [L10n::t('Monday'), L10n::t('Tuesday'), L10n::t('Wednesday'), L10n::t('Thursday'), L10n::t('Friday'), L10n::t('Saturday'), L10n::t('Sunday')],
1070                 $s);
1071
1072         $ret = str_replace(['January','February','March','April','May','June','July','August','September','October','November','December'],
1073                 [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')],
1074                 $ret);
1075
1076         return $ret;
1077 }
1078
1079 /**
1080  * @brief Translate short days and months names.
1081  *
1082  * @param string $s String with short day or month name.
1083  * @return string Translated string.
1084  */
1085 function day_short_translate($s) {
1086         $ret = str_replace(['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
1087                 [L10n::t('Mon'), L10n::t('Tue'), L10n::t('Wed'), L10n::t('Thu'), L10n::t('Fri'), L10n::t('Sat'), L10n::t('Sun')],
1088                 $s);
1089         $ret = str_replace(['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov','Dec'],
1090                 [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')],
1091                 $ret);
1092         return $ret;
1093 }
1094
1095
1096 /**
1097  * Normalize url
1098  *
1099  * @param string $url
1100  * @return string
1101  */
1102 function normalise_link($url) {
1103         $ret = str_replace(['https:', '//www.'], ['http:', '//'], $url);
1104         return rtrim($ret,'/');
1105 }
1106
1107
1108 /**
1109  * Compare two URLs to see if they are the same, but ignore
1110  * slight but hopefully insignificant differences such as if one
1111  * is https and the other isn't, or if one is www.something and
1112  * the other isn't - and also ignore case differences.
1113  *
1114  * @param string $a first url
1115  * @param string $b second url
1116  * @return boolean True if the URLs match, otherwise False
1117  *
1118  */
1119 function link_compare($a, $b) {
1120         return (strcasecmp(normalise_link($a), normalise_link($b)) === 0);
1121 }
1122
1123
1124 /**
1125  * @brief Find any non-embedded images in private items and add redir links to them
1126  *
1127  * @param App $a
1128  * @param array &$item The field array of an item row
1129  */
1130 function redir_private_images($a, &$item)
1131 {
1132         $matches = false;
1133         $cnt = preg_match_all('|\[img\](http[^\[]*?/photo/[a-fA-F0-9]+?(-[0-9]\.[\w]+?)?)\[\/img\]|', $item['body'], $matches, PREG_SET_ORDER);
1134         if ($cnt) {
1135                 foreach ($matches as $mtch) {
1136                         if (strpos($mtch[1], '/redir') !== false) {
1137                                 continue;
1138                         }
1139
1140                         if ((local_user() == $item['uid']) && ($item['private'] != 0) && ($item['contact-id'] != $a->contact['id']) && ($item['network'] == NETWORK_DFRN)) {
1141                                 $img_url = 'redir?f=1&quiet=1&url=' . urlencode($mtch[1]) . '&conurl=' . urlencode($item['author-link']);
1142                                 $item['body'] = str_replace($mtch[0], '[img]' . $img_url . '[/img]', $item['body']);
1143                         }
1144                 }
1145         }
1146 }
1147
1148 /**
1149  * Sets the "rendered-html" field of the provided item
1150  *
1151  * Body is preserved to avoid side-effects as we modify it just-in-time for spoilers and private image links
1152  *
1153  * @param array $item
1154  * @param bool  $update
1155  *
1156  * @todo Remove reference, simply return "rendered-html" and "rendered-hash"
1157  */
1158 function put_item_in_cache(&$item, $update = false)
1159 {
1160         $body = $item["body"];
1161
1162         $rendered_hash = defaults($item, 'rendered-hash', '');
1163         $rendered_html = defaults($item, 'rendered-html', '');
1164
1165         if ($rendered_hash == ''
1166                 || $rendered_html == ""
1167                 || $rendered_hash != hash("md5", $item["body"])
1168                 || Config::get("system", "ignore_cache")
1169         ) {
1170                 $a = get_app();
1171                 redir_private_images($a, $item);
1172
1173                 $item["rendered-html"] = prepare_text($item["body"]);
1174                 $item["rendered-hash"] = hash("md5", $item["body"]);
1175
1176                 // Force an update if the generated values differ from the existing ones
1177                 if ($rendered_hash != $item["rendered-hash"]) {
1178                         $update = true;
1179                 }
1180
1181                 // Only compare the HTML when we forcefully ignore the cache
1182                 if (Config::get("system", "ignore_cache") && ($rendered_html != $item["rendered-html"])) {
1183                         $update = true;
1184                 }
1185
1186                 if ($update && !empty($item["id"])) {
1187                         Item::update(['rendered-html' => $item["rendered-html"], 'rendered-hash' => $item["rendered-hash"]],
1188                                         ['id' => $item["id"]]);
1189                 }
1190         }
1191
1192         $item["body"] = $body;
1193 }
1194
1195 /**
1196  * @brief Given an item array, convert the body element from bbcode to html and add smilie icons.
1197  * If attach is true, also add icons for item attachments.
1198  *
1199  * @param array   $item
1200  * @param boolean $attach
1201  * @param boolean $is_preview
1202  * @return string item body html
1203  * @hook prepare_body_init item array before any work
1204  * @hook prepare_body_content_filter ('item'=>item array, 'filter_reasons'=>string array) before first bbcode to html
1205  * @hook prepare_body ('item'=>item array, 'html'=>body string, 'is_preview'=>boolean, 'filter_reasons'=>string array) after first bbcode to html
1206  * @hook prepare_body_final ('item'=>item array, 'html'=>body string) after attach icons and blockquote special case handling (spoiler, author)
1207  */
1208 function prepare_body(array &$item, $attach = false, $is_preview = false)
1209 {
1210         $a = get_app();
1211         Addon::callHooks('prepare_body_init', $item);
1212
1213         // In order to provide theme developers more possibilities, event items
1214         // are treated differently.
1215         if ($item['object-type'] === ACTIVITY_OBJ_EVENT && isset($item['event-id'])) {
1216                 $ev = Event::getItemHTML($item);
1217                 return $ev;
1218         }
1219
1220         $tags = \Friendica\Model\Term::populateTagsFromItem($item);
1221
1222         $item['tags'] = $tags['tags'];
1223         $item['hashtags'] = $tags['hashtags'];
1224         $item['mentions'] = $tags['mentions'];
1225
1226         // Compile eventual content filter reasons
1227         $filter_reasons = [];
1228         if (!$is_preview && public_contact() != $item['author-id']) {
1229                 if (!empty($item['content-warning']) && (!local_user() || !PConfig::get(local_user(), 'system', 'disable_cw', false))) {
1230                         $filter_reasons[] = L10n::t('Content warning: %s', $item['content-warning']);
1231                 }
1232
1233                 $hook_data = [
1234                         'item' => $item,
1235                         'filter_reasons' => $filter_reasons
1236                 ];
1237                 Addon::callHooks('prepare_body_content_filter', $hook_data);
1238                 $filter_reasons = $hook_data['filter_reasons'];
1239                 unset($hook_data);
1240         }
1241
1242         // Update the cached values if there is no "zrl=..." on the links.
1243         $update = (!local_user() && !remote_user() && ($item["uid"] == 0));
1244
1245         // Or update it if the current viewer is the intented viewer.
1246         if (($item["uid"] == local_user()) && ($item["uid"] != 0)) {
1247                 $update = true;
1248         }
1249
1250         put_item_in_cache($item, $update);
1251         $s = $item["rendered-html"];
1252
1253         $hook_data = [
1254                 'item' => $item,
1255                 'html' => $s,
1256                 'preview' => $is_preview,
1257                 'filter_reasons' => $filter_reasons
1258         ];
1259         Addon::callHooks('prepare_body', $hook_data);
1260         $s = $hook_data['html'];
1261         unset($hook_data);
1262
1263         if (!$attach) {
1264                 // Replace the blockquotes with quotes that are used in mails.
1265                 $mailquote = '<blockquote type="cite" class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;">';
1266                 $s = str_replace(['<blockquote>', '<blockquote class="spoiler">', '<blockquote class="author">'], [$mailquote, $mailquote, $mailquote], $s);
1267                 return $s;
1268         }
1269
1270         $as = '';
1271         $vhead = false;
1272         $matches = [];
1273         preg_match_all('|\[attach\]href=\"(.*?)\" length=\"(.*?)\" type=\"(.*?)\"(?: title=\"(.*?)\")?|', $item['attach'], $matches, PREG_SET_ORDER);
1274         foreach ($matches as $mtch) {
1275                 $mime = $mtch[3];
1276
1277                 $the_url = Contact::magicLinkById($item['author-id'], $mtch[1]);
1278
1279                 if (strpos($mime, 'video') !== false) {
1280                         if (!$vhead) {
1281                                 $vhead = true;
1282                                 $a->page['htmlhead'] .= replace_macros(get_markup_template('videos_head.tpl'), [
1283                                         '$baseurl' => System::baseUrl(),
1284                                 ]);
1285                                 $a->page['end'] .= replace_macros(get_markup_template('videos_end.tpl'), [
1286                                         '$baseurl' => System::baseUrl(),
1287                                 ]);
1288                         }
1289
1290                         $url_parts = explode('/', $the_url);
1291                         $id = end($url_parts);
1292                         $as .= replace_macros(get_markup_template('video_top.tpl'), [
1293                                 '$video' => [
1294                                         'id'     => $id,
1295                                         'title'  => L10n::t('View Video'),
1296                                         'src'    => $the_url,
1297                                         'mime'   => $mime,
1298                                 ],
1299                         ]);
1300                 }
1301
1302                 $filetype = strtolower(substr($mime, 0, strpos($mime, '/')));
1303                 if ($filetype) {
1304                         $filesubtype = strtolower(substr($mime, strpos($mime, '/') + 1));
1305                         $filesubtype = str_replace('.', '-', $filesubtype);
1306                 } else {
1307                         $filetype = 'unkn';
1308                         $filesubtype = 'unkn';
1309                 }
1310
1311                 $title = escape_tags(trim(!empty($mtch[4]) ? $mtch[4] : $mtch[1]));
1312                 $title .= ' ' . $mtch[2] . ' ' . L10n::t('bytes');
1313
1314                 $icon = '<div class="attachtype icon s22 type-' . $filetype . ' subtype-' . $filesubtype . '"></div>';
1315                 $as .= '<a href="' . strip_tags($the_url) . '" title="' . $title . '" class="attachlink" target="_blank" >' . $icon . '</a>';
1316         }
1317
1318         if ($as != '') {
1319                 $s .= '<div class="body-attach">'.$as.'<div class="clear"></div></div>';
1320         }
1321
1322         // Map.
1323         if (strpos($s, '<div class="map">') !== false && x($item, 'coord')) {
1324                 $x = Map::byCoordinates(trim($item['coord']));
1325                 if ($x) {
1326                         $s = preg_replace('/\<div class\=\"map\"\>/', '$0' . $x, $s);
1327                 }
1328         }
1329
1330
1331         // Look for spoiler.
1332         $spoilersearch = '<blockquote class="spoiler">';
1333
1334         // Remove line breaks before the spoiler.
1335         while ((strpos($s, "\n" . $spoilersearch) !== false)) {
1336                 $s = str_replace("\n" . $spoilersearch, $spoilersearch, $s);
1337         }
1338         while ((strpos($s, "<br />" . $spoilersearch) !== false)) {
1339                 $s = str_replace("<br />" . $spoilersearch, $spoilersearch, $s);
1340         }
1341
1342         while ((strpos($s, $spoilersearch) !== false)) {
1343                 $pos = strpos($s, $spoilersearch);
1344                 $rnd = random_string(8);
1345                 $spoilerreplace = '<br /> <span id="spoiler-wrap-' . $rnd . '" class="spoiler-wrap fakelink" onclick="openClose(\'spoiler-' . $rnd . '\');">' . L10n::t('Click to open/close') . '</span>'.
1346                                         '<blockquote class="spoiler" id="spoiler-' . $rnd . '" style="display: none;">';
1347                 $s = substr($s, 0, $pos) . $spoilerreplace . substr($s, $pos + strlen($spoilersearch));
1348         }
1349
1350         // Look for quote with author.
1351         $authorsearch = '<blockquote class="author">';
1352
1353         while ((strpos($s, $authorsearch) !== false)) {
1354                 $pos = strpos($s, $authorsearch);
1355                 $rnd = random_string(8);
1356                 $authorreplace = '<br /> <span id="author-wrap-' . $rnd . '" class="author-wrap fakelink" onclick="openClose(\'author-' . $rnd . '\');">' . L10n::t('Click to open/close') . '</span>'.
1357                                         '<blockquote class="author" id="author-' . $rnd . '" style="display: block;">';
1358                 $s = substr($s, 0, $pos) . $authorreplace . substr($s, $pos + strlen($authorsearch));
1359         }
1360
1361         // Replace friendica image url size with theme preference.
1362         if (x($a->theme_info, 'item_image_size')){
1363                 $ps = $a->theme_info['item_image_size'];
1364                 $s = preg_replace('|(<img[^>]+src="[^"]+/photo/[0-9a-f]+)-[0-9]|', "$1-" . $ps, $s);
1365         }
1366
1367         $s = apply_content_filter($s, $filter_reasons);
1368
1369         $hook_data = ['item' => $item, 'html' => $s];
1370         Addon::callHooks('prepare_body_final', $hook_data);
1371
1372         return $hook_data['html'];
1373 }
1374
1375 /**
1376  * Given a HTML text and a set of filtering reasons, adds a content hiding header with the provided reasons
1377  *
1378  * Reasons are expected to have been translated already.
1379  *
1380  * @param string $html
1381  * @param array  $reasons
1382  * @return string
1383  */
1384 function apply_content_filter($html, array $reasons)
1385 {
1386         if (count($reasons)) {
1387                 $tpl = get_markup_template('wall/content_filter.tpl');
1388                 $html = replace_macros($tpl, [
1389                         '$reasons'   => $reasons,
1390                         '$rnd'       => random_string(8),
1391                         '$openclose' => L10n::t('Click to open/close'),
1392                         '$html'      => $html
1393                 ]);
1394         }
1395
1396         return $html;
1397 }
1398
1399 /**
1400  * @brief Given a text string, convert from bbcode to html and add smilie icons.
1401  *
1402  * @param string $text String with bbcode.
1403  * @return string Formattet HTML.
1404  */
1405 function prepare_text($text) {
1406         if (stristr($text, '[nosmile]')) {
1407                 $s = BBCode::convert($text);
1408         } else {
1409                 $s = Smilies::replace(BBCode::convert($text));
1410         }
1411
1412         return trim($s);
1413 }
1414
1415 /**
1416  * return array with details for categories and folders for an item
1417  *
1418  * @param array $item
1419  * @return array
1420  *
1421   * [
1422  *      [ // categories array
1423  *          {
1424  *               'name': 'category name',
1425  *               'removeurl': 'url to remove this category',
1426  *               'first': 'is the first in this array? true/false',
1427  *               'last': 'is the last in this array? true/false',
1428  *           } ,
1429  *           ....
1430  *       ],
1431  *       [ //folders array
1432  *                      {
1433  *               'name': 'folder name',
1434  *               'removeurl': 'url to remove this folder',
1435  *               'first': 'is the first in this array? true/false',
1436  *               'last': 'is the last in this array? true/false',
1437  *           } ,
1438  *           ....
1439  *       ]
1440  *  ]
1441  */
1442 function get_cats_and_terms($item)
1443 {
1444         $categories = [];
1445         $folders = [];
1446
1447         $matches = false;
1448         $first = true;
1449         $cnt = preg_match_all('/<(.*?)>/', $item['file'], $matches, PREG_SET_ORDER);
1450         if ($cnt) {
1451                 foreach ($matches as $mtch) {
1452                         $categories[] = [
1453                                 'name' => xmlify(file_tag_decode($mtch[1])),
1454                                 'url' =>  "#",
1455                                 'removeurl' => ((local_user() == $item['uid'])?'filerm/' . $item['id'] . '?f=&cat=' . xmlify(file_tag_decode($mtch[1])):""),
1456                                 'first' => $first,
1457                                 'last' => false
1458                         ];
1459                         $first = false;
1460                 }
1461         }
1462
1463         if (count($categories)) {
1464                 $categories[count($categories) - 1]['last'] = true;
1465         }
1466
1467         if (local_user() == $item['uid']) {
1468                 $matches = false;
1469                 $first = true;
1470                 $cnt = preg_match_all('/\[(.*?)\]/', $item['file'], $matches, PREG_SET_ORDER);
1471                 if ($cnt) {
1472                         foreach ($matches as $mtch) {
1473                                 $folders[] = [
1474                                         'name' => xmlify(file_tag_decode($mtch[1])),
1475                                         'url' =>  "#",
1476                                         'removeurl' => ((local_user() == $item['uid']) ? 'filerm/' . $item['id'] . '?f=&term=' . xmlify(file_tag_decode($mtch[1])) : ""),
1477                                         'first' => $first,
1478                                         'last' => false
1479                                 ];
1480                                 $first = false;
1481                         }
1482                 }
1483         }
1484
1485         if (count($folders)) {
1486                 $folders[count($folders) - 1]['last'] = true;
1487         }
1488
1489         return [$categories, $folders];
1490 }
1491
1492
1493 /**
1494  * get private link for item
1495  * @param array $item
1496  * @return boolean|array False if item has not plink, otherwise array('href'=>plink url, 'title'=>translated title)
1497  */
1498 function get_plink($item) {
1499         $a = get_app();
1500
1501         if ($a->user['nickname'] != "") {
1502                 $ret = [
1503                                 //'href' => "display/" . $a->user['nickname'] . "/" . $item['id'],
1504                                 'href' => "display/" . $item['guid'],
1505                                 'orig' => "display/" . $item['guid'],
1506                                 'title' => L10n::t('View on separate page'),
1507                                 'orig_title' => L10n::t('view on separate page'),
1508                         ];
1509
1510                 if (x($item, 'plink')) {
1511                         $ret["href"] = $a->remove_baseurl($item['plink']);
1512                         $ret["title"] = L10n::t('link to source');
1513                 }
1514
1515         } elseif (x($item, 'plink') && ($item['private'] != 1)) {
1516                 $ret = [
1517                                 'href' => $item['plink'],
1518                                 'orig' => $item['plink'],
1519                                 'title' => L10n::t('link to source'),
1520                         ];
1521         } else {
1522                 $ret = [];
1523         }
1524
1525         return $ret;
1526 }
1527
1528
1529 /**
1530  * replace html amp entity with amp char
1531  * @param string $s
1532  * @return string
1533  */
1534 function unamp($s) {
1535         return str_replace('&amp;', '&', $s);
1536 }
1537
1538
1539 /**
1540  * return number of bytes in size (K, M, G)
1541  * @param string $size_str
1542  * @return number
1543  */
1544 function return_bytes($size_str) {
1545         switch (substr ($size_str, -1)) {
1546                 case 'M': case 'm': return (int)$size_str * 1048576;
1547                 case 'K': case 'k': return (int)$size_str * 1024;
1548                 case 'G': case 'g': return (int)$size_str * 1073741824;
1549                 default: return $size_str;
1550         }
1551 }
1552
1553
1554 /**
1555  * @return string
1556  */
1557 function generate_user_guid() {
1558         $found = true;
1559         do {
1560                 $guid = System::createGUID(32);
1561                 $x = q("SELECT `uid` FROM `user` WHERE `guid` = '%s' LIMIT 1",
1562                         dbesc($guid)
1563                 );
1564                 if (!DBM::is_result($x)) {
1565                         $found = false;
1566                 }
1567         } while ($found == true);
1568
1569         return $guid;
1570 }
1571
1572
1573 /**
1574  * @param string $s
1575  * @param boolean $strip_padding
1576  * @return string
1577  */
1578 function base64url_encode($s, $strip_padding = false) {
1579
1580         $s = strtr(base64_encode($s), '+/', '-_');
1581
1582         if ($strip_padding) {
1583                 $s = str_replace('=','',$s);
1584         }
1585
1586         return $s;
1587 }
1588
1589 /**
1590  * @param string $s
1591  * @return string
1592  */
1593 function base64url_decode($s) {
1594
1595         if (is_array($s)) {
1596                 logger('base64url_decode: illegal input: ' . print_r(debug_backtrace(), true));
1597                 return $s;
1598         }
1599
1600 /*
1601  *  // Placeholder for new rev of salmon which strips base64 padding.
1602  *  // PHP base64_decode handles the un-padded input without requiring this step
1603  *  // Uncomment if you find you need it.
1604  *
1605  *      $l = strlen($s);
1606  *      if (!strpos($s,'=')) {
1607  *              $m = $l % 4;
1608  *              if ($m == 2)
1609  *                      $s .= '==';
1610  *              if ($m == 3)
1611  *                      $s .= '=';
1612  *      }
1613  *
1614  */
1615
1616         return base64_decode(strtr($s,'-_','+/'));
1617 }
1618
1619
1620 /**
1621  * return div element with class 'clear'
1622  * @return string
1623  * @deprecated
1624  */
1625 function cleardiv() {
1626         return '<div class="clear"></div>';
1627 }
1628
1629
1630 function bb_translate_video($s) {
1631
1632         $matches = null;
1633         $r = preg_match_all("/\[video\](.*?)\[\/video\]/ism",$s,$matches,PREG_SET_ORDER);
1634         if ($r) {
1635                 foreach ($matches as $mtch) {
1636                         if ((stristr($mtch[1], 'youtube')) || (stristr($mtch[1], 'youtu.be'))) {
1637                                 $s = str_replace($mtch[0], '[youtube]' . $mtch[1] . '[/youtube]', $s);
1638                         } elseif (stristr($mtch[1], 'vimeo')) {
1639                                 $s = str_replace($mtch[0], '[vimeo]' . $mtch[1] . '[/vimeo]', $s);
1640                         }
1641                 }
1642         }
1643         return $s;
1644 }
1645
1646 function html2bb_video($s) {
1647
1648         $s = preg_replace('#<object[^>]+>(.*?)https?://www.youtube.com/((?:v|cp)/[A-Za-z0-9\-_=]+)(.*?)</object>#ism',
1649                         '[youtube]$2[/youtube]', $s);
1650
1651         $s = preg_replace('#<iframe[^>](.*?)https?://www.youtube.com/embed/([A-Za-z0-9\-_=]+)(.*?)</iframe>#ism',
1652                         '[youtube]$2[/youtube]', $s);
1653
1654         $s = preg_replace('#<iframe[^>](.*?)https?://player.vimeo.com/video/([0-9]+)(.*?)</iframe>#ism',
1655                         '[vimeo]$2[/vimeo]', $s);
1656
1657         return $s;
1658 }
1659
1660 /**
1661  * apply xmlify() to all values of array $val, recursively
1662  * @param array $val
1663  * @return array
1664  */
1665 function array_xmlify($val){
1666         if (is_bool($val)) {
1667                 return $val?"true":"false";
1668         } elseif (is_array($val)) {
1669                 return array_map('array_xmlify', $val);
1670         }
1671         return xmlify((string) $val);
1672 }
1673
1674
1675 /**
1676  * transform link href and img src from relative to absolute
1677  *
1678  * @param string $text
1679  * @param string $base base url
1680  * @return string
1681  */
1682 function reltoabs($text, $base) {
1683         if (empty($base)) {
1684                 return $text;
1685         }
1686
1687         $base = rtrim($base,'/');
1688
1689         $base2 = $base . "/";
1690
1691         // Replace links
1692         $pattern = "/<a([^>]*) href=\"(?!http|https|\/)([^\"]*)\"/";
1693         $replace = "<a\${1} href=\"" . $base2 . "\${2}\"";
1694         $text = preg_replace($pattern, $replace, $text);
1695
1696         $pattern = "/<a([^>]*) href=\"(?!http|https)([^\"]*)\"/";
1697         $replace = "<a\${1} href=\"" . $base . "\${2}\"";
1698         $text = preg_replace($pattern, $replace, $text);
1699
1700         // Replace images
1701         $pattern = "/<img([^>]*) src=\"(?!http|https|\/)([^\"]*)\"/";
1702         $replace = "<img\${1} src=\"" . $base2 . "\${2}\"";
1703         $text = preg_replace($pattern, $replace, $text);
1704
1705         $pattern = "/<img([^>]*) src=\"(?!http|https)([^\"]*)\"/";
1706         $replace = "<img\${1} src=\"" . $base . "\${2}\"";
1707         $text = preg_replace($pattern, $replace, $text);
1708
1709
1710         // Done
1711         return $text;
1712 }
1713
1714 /**
1715  * get translated item type
1716  *
1717  * @param array $itme
1718  * @return string
1719  */
1720 function item_post_type($item) {
1721         if (!empty($item['event-id'])) {
1722                 return L10n::t('event');
1723         } elseif (!empty($item['resource-id'])) {
1724                 return L10n::t('photo');
1725         } elseif (!empty($item['verb']) && $item['verb'] !== ACTIVITY_POST) {
1726                 return L10n::t('activity');
1727         } elseif ($item['id'] != $item['parent']) {
1728                 return L10n::t('comment');
1729         }
1730
1731         return L10n::t('post');
1732 }
1733
1734 // post categories and "save to file" use the same item.file table for storage.
1735 // We will differentiate the different uses by wrapping categories in angle brackets
1736 // and save to file categories in square brackets.
1737 // To do this we need to escape these characters if they appear in our tag.
1738
1739 function file_tag_encode($s) {
1740         return str_replace(['<','>','[',']'],['%3c','%3e','%5b','%5d'],$s);
1741 }
1742
1743 function file_tag_decode($s) {
1744         return str_replace(['%3c', '%3e', '%5b', '%5d'], ['<', '>', '[', ']'], $s);
1745 }
1746
1747 function file_tag_file_query($table,$s,$type = 'file') {
1748
1749         if ($type == 'file') {
1750                 $str = preg_quote('[' . str_replace('%', '%%', file_tag_encode($s)) . ']');
1751         } else {
1752                 $str = preg_quote('<' . str_replace('%', '%%', file_tag_encode($s)) . '>');
1753         }
1754         return " AND " . (($table) ? dbesc($table) . '.' : '') . "file regexp '" . dbesc($str) . "' ";
1755 }
1756
1757 // ex. given music,video return <music><video> or [music][video]
1758 function file_tag_list_to_file($list, $type = 'file') {
1759         $tag_list = '';
1760         if (strlen($list)) {
1761                 $list_array = explode(",",$list);
1762                 if ($type == 'file') {
1763                         $lbracket = '[';
1764                         $rbracket = ']';
1765                 } else {
1766                         $lbracket = '<';
1767                         $rbracket = '>';
1768                 }
1769
1770                 foreach ($list_array as $item) {
1771                         if (strlen($item)) {
1772                                 $tag_list .= $lbracket . file_tag_encode(trim($item))  . $rbracket;
1773                         }
1774                 }
1775         }
1776         return $tag_list;
1777 }
1778
1779 // ex. given <music><video>[friends], return music,video or friends
1780 function file_tag_file_to_list($file, $type = 'file') {
1781         $matches = false;
1782         $list = '';
1783         if ($type == 'file') {
1784                 $cnt = preg_match_all('/\[(.*?)\]/', $file, $matches, PREG_SET_ORDER);
1785         } else {
1786                 $cnt = preg_match_all('/<(.*?)>/', $file, $matches, PREG_SET_ORDER);
1787         }
1788         if ($cnt) {
1789                 foreach ($matches as $mtch) {
1790                         if (strlen($list)) {
1791                                 $list .= ',';
1792                         }
1793                         $list .= file_tag_decode($mtch[1]);
1794                 }
1795         }
1796
1797         return $list;
1798 }
1799
1800 function file_tag_update_pconfig($uid, $file_old, $file_new, $type = 'file') {
1801         // $file_old - categories previously associated with an item
1802         // $file_new - new list of categories for an item
1803
1804         if (!intval($uid)) {
1805                 return false;
1806         } elseif ($file_old == $file_new) {
1807                 return true;
1808         }
1809
1810         $saved = PConfig::get($uid, 'system', 'filetags');
1811         if (strlen($saved)) {
1812                 if ($type == 'file') {
1813                         $lbracket = '[';
1814                         $rbracket = ']';
1815                         $termtype = TERM_FILE;
1816                 } else {
1817                         $lbracket = '<';
1818                         $rbracket = '>';
1819                         $termtype = TERM_CATEGORY;
1820                 }
1821
1822                 $filetags_updated = $saved;
1823
1824                 // check for new tags to be added as filetags in pconfig
1825                 $new_tags = [];
1826                 $check_new_tags = explode(",",file_tag_file_to_list($file_new,$type));
1827
1828                 foreach ($check_new_tags as $tag) {
1829                         if (!stristr($saved,$lbracket . file_tag_encode($tag) . $rbracket)) {
1830                                 $new_tags[] = $tag;
1831                         }
1832                 }
1833
1834                 $filetags_updated .= file_tag_list_to_file(implode(",",$new_tags),$type);
1835
1836                 // check for deleted tags to be removed from filetags in pconfig
1837                 $deleted_tags = [];
1838                 $check_deleted_tags = explode(",",file_tag_file_to_list($file_old,$type));
1839
1840                 foreach ($check_deleted_tags as $tag) {
1841                         if (!stristr($file_new,$lbracket . file_tag_encode($tag) . $rbracket)) {
1842                                 $deleted_tags[] = $tag;
1843                         }
1844                 }
1845
1846                 foreach ($deleted_tags as $key => $tag) {
1847                         $r = q("SELECT `oid` FROM `term` WHERE `term` = '%s' AND `otype` = %d AND `type` = %d AND `uid` = %d",
1848                                 dbesc($tag),
1849                                 intval(TERM_OBJ_POST),
1850                                 intval($termtype),
1851                                 intval($uid));
1852
1853                         if (DBM::is_result($r)) {
1854                                 unset($deleted_tags[$key]);
1855                         } else {
1856                                 $filetags_updated = str_replace($lbracket . file_tag_encode($tag) . $rbracket,'',$filetags_updated);
1857                         }
1858                 }
1859
1860                 if ($saved != $filetags_updated) {
1861                         PConfig::set($uid, 'system', 'filetags', $filetags_updated);
1862                 }
1863                 return true;
1864         } elseif (strlen($file_new)) {
1865                 PConfig::set($uid, 'system', 'filetags', $file_new);
1866         }
1867         return true;
1868 }
1869
1870 function file_tag_save_file($uid, $item_id, $file)
1871 {
1872         if (!intval($uid)) {
1873                 return false;
1874         }
1875
1876         $item = Item::selectFirst(['file'], ['id' => $item_id, 'uid' => $uid]);
1877         if (DBM::is_result($item)) {
1878                 if (!stristr($item['file'],'[' . file_tag_encode($file) . ']')) {
1879                         $fields = ['file' => $item['file'] . '[' . file_tag_encode($file) . ']'];
1880                         Item::update($fields, ['id' => $item_id]);
1881                 }
1882                 $saved = PConfig::get($uid, 'system', 'filetags');
1883                 if (!strlen($saved) || !stristr($saved, '[' . file_tag_encode($file) . ']')) {
1884                         PConfig::set($uid, 'system', 'filetags', $saved . '[' . file_tag_encode($file) . ']');
1885                 }
1886                 info(L10n::t('Item filed'));
1887         }
1888         return true;
1889 }
1890
1891 function file_tag_unsave_file($uid, $item_id, $file, $cat = false)
1892 {
1893         if (!intval($uid)) {
1894                 return false;
1895         }
1896
1897         if ($cat == true) {
1898                 $pattern = '<' . file_tag_encode($file) . '>' ;
1899                 $termtype = TERM_CATEGORY;
1900         } else {
1901                 $pattern = '[' . file_tag_encode($file) . ']' ;
1902                 $termtype = TERM_FILE;
1903         }
1904
1905         $item = Item::selectFirst(['file'], ['id' => $item_id, 'uid' => $uid]);
1906         if (!DBM::is_result($item)) {
1907                 return false;
1908         }
1909
1910         $fields = ['file' => str_replace($pattern,'',$item['file'])];
1911         Item::update($fields, ['id' => $item_id]);
1912
1913         $r = q("SELECT `oid` FROM `term` WHERE `term` = '%s' AND `otype` = %d AND `type` = %d AND `uid` = %d",
1914                 dbesc($file),
1915                 intval(TERM_OBJ_POST),
1916                 intval($termtype),
1917                 intval($uid)
1918         );
1919         if (!DBM::is_result($r)) {
1920                 $saved = PConfig::get($uid, 'system', 'filetags');
1921                 PConfig::set($uid, 'system', 'filetags', str_replace($pattern, '', $saved));
1922         }
1923
1924         return true;
1925 }
1926
1927 function normalise_openid($s) {
1928         return trim(str_replace(['http://', 'https://'], ['', ''], $s), '/');
1929 }
1930
1931
1932 function undo_post_tagging($s) {
1933         $matches = null;
1934         $cnt = preg_match_all('/([!#@])\[url=(.*?)\](.*?)\[\/url\]/ism', $s, $matches, PREG_SET_ORDER);
1935         if ($cnt) {
1936                 foreach ($matches as $mtch) {
1937                         if (in_array($mtch[1], ['!', '@'])) {
1938                                 $contact = Contact::getDetailsByURL($mtch[2]);
1939                                 $mtch[3] = empty($contact['addr']) ? $mtch[2] : $contact['addr'];
1940                         }
1941                         $s = str_replace($mtch[0], $mtch[1] . $mtch[3],$s);
1942                 }
1943         }
1944         return $s;
1945 }
1946
1947 function protect_sprintf($s) {
1948         return str_replace('%', '%%', $s);
1949 }
1950
1951 /// @TODO Rewrite this
1952 function is_a_date_arg($s) {
1953         $i = intval($s);
1954
1955         if ($i > 1900) {
1956                 $y = date('Y');
1957
1958                 if ($i <= $y + 1 && strpos($s, '-') == 4) {
1959                         $m = intval(substr($s, 5));
1960
1961                         if ($m > 0 && $m <= 12) {
1962                                 return true;
1963                         }
1964                 }
1965         }
1966
1967         return false;
1968 }
1969
1970 /**
1971  * remove intentation from a text
1972  */
1973 function deindent($text, $chr = "[\t ]", $count = NULL) {
1974         $lines = explode("\n", $text);
1975
1976         if (is_null($count)) {
1977                 $m = [];
1978                 $k = 0;
1979                 while ($k < count($lines) && strlen($lines[$k]) == 0) {
1980                         $k++;
1981                 }
1982                 preg_match("|^" . $chr . "*|", $lines[$k], $m);
1983                 $count = strlen($m[0]);
1984         }
1985
1986         for ($k = 0; $k < count($lines); $k++) {
1987                 $lines[$k] = preg_replace("|^" . $chr . "{" . $count . "}|", "", $lines[$k]);
1988         }
1989
1990         return implode("\n", $lines);
1991 }
1992
1993 function formatBytes($bytes, $precision = 2) {
1994         $units = ['B', 'KB', 'MB', 'GB', 'TB'];
1995
1996         $bytes = max($bytes, 0);
1997         $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
1998         $pow = min($pow, count($units) - 1);
1999
2000         $bytes /= pow(1024, $pow);
2001
2002         return round($bytes, $precision) . ' ' . $units[$pow];
2003 }
2004
2005 /**
2006  * @brief translate and format the networkname of a contact
2007  *
2008  * @param string $network
2009  *      Networkname of the contact (e.g. dfrn, rss and so on)
2010  * @param sting $url
2011  *      The contact url
2012  * @return string
2013  */
2014 function format_network_name($network, $url = 0) {
2015         if ($network != "") {
2016                 if ($url != "") {
2017                         $network_name = '<a href="'.$url.'">'.ContactSelector::networkToName($network, $url)."</a>";
2018                 } else {
2019                         $network_name = ContactSelector::networkToName($network);
2020                 }
2021
2022                 return $network_name;
2023         }
2024 }
2025
2026 /**
2027  * @brief Syntax based code highlighting for popular languages.
2028  * @param string $s Code block
2029  * @param string $lang Programming language
2030  * @return string Formated html
2031  */
2032 function text_highlight($s, $lang) {
2033         if ($lang === 'js') {
2034                 $lang = 'javascript';
2035         }
2036
2037         if ($lang === 'bash') {
2038                 $lang = 'sh';
2039         }
2040
2041         // @TODO: Replace Text_Highlighter_Renderer_Html by scrivo/highlight.php
2042
2043         // Autoload the library to make constants available
2044         class_exists('Text_Highlighter_Renderer_Html');
2045
2046         $options = [
2047                 'numbers' => HL_NUMBERS_LI,
2048                 'tabsize' => 4,
2049         ];
2050
2051         $tag_added = false;
2052         $s = trim(html_entity_decode($s, ENT_COMPAT));
2053         $s = str_replace('    ', "\t", $s);
2054
2055         /*
2056          * The highlighter library insists on an opening php tag for php code blocks. If
2057          * it isn't present, nothing is highlighted. So we're going to see if it's present.
2058          * If not, we'll add it, and then quietly remove it after we get the processed output back.
2059          */
2060         if ($lang === 'php' && strpos($s, '<?php') !== 0) {
2061                 $s = '<?php' . "\n" . $s;
2062                 $tag_added = true;
2063         }
2064
2065         $renderer = new Text_Highlighter_Renderer_Html($options);
2066         $factory = new Text_Highlighter();
2067         $hl = $factory->factory($lang);
2068         $hl->setRenderer($renderer);
2069         $o = $hl->highlight($s);
2070         $o = str_replace("\n", '', $o);
2071
2072         if ($tag_added) {
2073                 $b = substr($o, 0, strpos($o, '<li>'));
2074                 $e = substr($o, strpos($o, '</li>'));
2075                 $o = $b . $e;
2076         }
2077
2078         return '<code>' . $o . '</code>';
2079 }