]> git.mxchange.org Git - friendica.git/blob - include/text.php
Merge pull request #5372 from MrPetovan/task/add-mastodoncustomemojis-addon
[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                 $hook_data = ['item' => $item, 'rendered-html' => $item['rendered-html'], 'rendered-hash' => $item['rendered-hash']];
1177                 Addon::callHooks('put_item_in_cache', $hook_data);
1178                 $item['rendered-html'] = $hook_data['rendered-html'];
1179                 $item['rendered-hash'] = $hook_data['rendered-hash'];
1180                 unset($hook_data);
1181
1182                 // Force an update if the generated values differ from the existing ones
1183                 if ($rendered_hash != $item["rendered-hash"]) {
1184                         $update = true;
1185                 }
1186
1187                 // Only compare the HTML when we forcefully ignore the cache
1188                 if (Config::get("system", "ignore_cache") && ($rendered_html != $item["rendered-html"])) {
1189                         $update = true;
1190                 }
1191
1192                 if ($update && !empty($item["id"])) {
1193                         Item::update(['rendered-html' => $item["rendered-html"], 'rendered-hash' => $item["rendered-hash"]],
1194                                         ['id' => $item["id"]]);
1195                 }
1196         }
1197
1198         $item["body"] = $body;
1199 }
1200
1201 /**
1202  * @brief Given an item array, convert the body element from bbcode to html and add smilie icons.
1203  * If attach is true, also add icons for item attachments.
1204  *
1205  * @param array   $item
1206  * @param boolean $attach
1207  * @param boolean $is_preview
1208  * @return string item body html
1209  * @hook prepare_body_init item array before any work
1210  * @hook prepare_body_content_filter ('item'=>item array, 'filter_reasons'=>string array) before first bbcode to html
1211  * @hook prepare_body ('item'=>item array, 'html'=>body string, 'is_preview'=>boolean, 'filter_reasons'=>string array) after first bbcode to html
1212  * @hook prepare_body_final ('item'=>item array, 'html'=>body string) after attach icons and blockquote special case handling (spoiler, author)
1213  */
1214 function prepare_body(array &$item, $attach = false, $is_preview = false)
1215 {
1216         $a = get_app();
1217         Addon::callHooks('prepare_body_init', $item);
1218
1219         // In order to provide theme developers more possibilities, event items
1220         // are treated differently.
1221         if ($item['object-type'] === ACTIVITY_OBJ_EVENT && isset($item['event-id'])) {
1222                 $ev = Event::getItemHTML($item);
1223                 return $ev;
1224         }
1225
1226         $tags = \Friendica\Model\Term::populateTagsFromItem($item);
1227
1228         $item['tags'] = $tags['tags'];
1229         $item['hashtags'] = $tags['hashtags'];
1230         $item['mentions'] = $tags['mentions'];
1231
1232         // Compile eventual content filter reasons
1233         $filter_reasons = [];
1234         if (!$is_preview && public_contact() != $item['author-id']) {
1235                 if (!empty($item['content-warning']) && (!local_user() || !PConfig::get(local_user(), 'system', 'disable_cw', false))) {
1236                         $filter_reasons[] = L10n::t('Content warning: %s', $item['content-warning']);
1237                 }
1238
1239                 $hook_data = [
1240                         'item' => $item,
1241                         'filter_reasons' => $filter_reasons
1242                 ];
1243                 Addon::callHooks('prepare_body_content_filter', $hook_data);
1244                 $filter_reasons = $hook_data['filter_reasons'];
1245                 unset($hook_data);
1246         }
1247
1248         // Update the cached values if there is no "zrl=..." on the links.
1249         $update = (!local_user() && !remote_user() && ($item["uid"] == 0));
1250
1251         // Or update it if the current viewer is the intented viewer.
1252         if (($item["uid"] == local_user()) && ($item["uid"] != 0)) {
1253                 $update = true;
1254         }
1255
1256         put_item_in_cache($item, $update);
1257         $s = $item["rendered-html"];
1258
1259         $hook_data = [
1260                 'item' => $item,
1261                 'html' => $s,
1262                 'preview' => $is_preview,
1263                 'filter_reasons' => $filter_reasons
1264         ];
1265         Addon::callHooks('prepare_body', $hook_data);
1266         $s = $hook_data['html'];
1267         unset($hook_data);
1268
1269         if (!$attach) {
1270                 // Replace the blockquotes with quotes that are used in mails.
1271                 $mailquote = '<blockquote type="cite" class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;">';
1272                 $s = str_replace(['<blockquote>', '<blockquote class="spoiler">', '<blockquote class="author">'], [$mailquote, $mailquote, $mailquote], $s);
1273                 return $s;
1274         }
1275
1276         $as = '';
1277         $vhead = false;
1278         $matches = [];
1279         preg_match_all('|\[attach\]href=\"(.*?)\" length=\"(.*?)\" type=\"(.*?)\"(?: title=\"(.*?)\")?|', $item['attach'], $matches, PREG_SET_ORDER);
1280         foreach ($matches as $mtch) {
1281                 $mime = $mtch[3];
1282
1283                 $the_url = Contact::magicLinkById($item['author-id'], $mtch[1]);
1284
1285                 if (strpos($mime, 'video') !== false) {
1286                         if (!$vhead) {
1287                                 $vhead = true;
1288                                 $a->page['htmlhead'] .= replace_macros(get_markup_template('videos_head.tpl'), [
1289                                         '$baseurl' => System::baseUrl(),
1290                                 ]);
1291                                 $a->page['end'] .= replace_macros(get_markup_template('videos_end.tpl'), [
1292                                         '$baseurl' => System::baseUrl(),
1293                                 ]);
1294                         }
1295
1296                         $url_parts = explode('/', $the_url);
1297                         $id = end($url_parts);
1298                         $as .= replace_macros(get_markup_template('video_top.tpl'), [
1299                                 '$video' => [
1300                                         'id'     => $id,
1301                                         'title'  => L10n::t('View Video'),
1302                                         'src'    => $the_url,
1303                                         'mime'   => $mime,
1304                                 ],
1305                         ]);
1306                 }
1307
1308                 $filetype = strtolower(substr($mime, 0, strpos($mime, '/')));
1309                 if ($filetype) {
1310                         $filesubtype = strtolower(substr($mime, strpos($mime, '/') + 1));
1311                         $filesubtype = str_replace('.', '-', $filesubtype);
1312                 } else {
1313                         $filetype = 'unkn';
1314                         $filesubtype = 'unkn';
1315                 }
1316
1317                 $title = escape_tags(trim(!empty($mtch[4]) ? $mtch[4] : $mtch[1]));
1318                 $title .= ' ' . $mtch[2] . ' ' . L10n::t('bytes');
1319
1320                 $icon = '<div class="attachtype icon s22 type-' . $filetype . ' subtype-' . $filesubtype . '"></div>';
1321                 $as .= '<a href="' . strip_tags($the_url) . '" title="' . $title . '" class="attachlink" target="_blank" >' . $icon . '</a>';
1322         }
1323
1324         if ($as != '') {
1325                 $s .= '<div class="body-attach">'.$as.'<div class="clear"></div></div>';
1326         }
1327
1328         // Map.
1329         if (strpos($s, '<div class="map">') !== false && x($item, 'coord')) {
1330                 $x = Map::byCoordinates(trim($item['coord']));
1331                 if ($x) {
1332                         $s = preg_replace('/\<div class\=\"map\"\>/', '$0' . $x, $s);
1333                 }
1334         }
1335
1336
1337         // Look for spoiler.
1338         $spoilersearch = '<blockquote class="spoiler">';
1339
1340         // Remove line breaks before the spoiler.
1341         while ((strpos($s, "\n" . $spoilersearch) !== false)) {
1342                 $s = str_replace("\n" . $spoilersearch, $spoilersearch, $s);
1343         }
1344         while ((strpos($s, "<br />" . $spoilersearch) !== false)) {
1345                 $s = str_replace("<br />" . $spoilersearch, $spoilersearch, $s);
1346         }
1347
1348         while ((strpos($s, $spoilersearch) !== false)) {
1349                 $pos = strpos($s, $spoilersearch);
1350                 $rnd = random_string(8);
1351                 $spoilerreplace = '<br /> <span id="spoiler-wrap-' . $rnd . '" class="spoiler-wrap fakelink" onclick="openClose(\'spoiler-' . $rnd . '\');">' . L10n::t('Click to open/close') . '</span>'.
1352                                         '<blockquote class="spoiler" id="spoiler-' . $rnd . '" style="display: none;">';
1353                 $s = substr($s, 0, $pos) . $spoilerreplace . substr($s, $pos + strlen($spoilersearch));
1354         }
1355
1356         // Look for quote with author.
1357         $authorsearch = '<blockquote class="author">';
1358
1359         while ((strpos($s, $authorsearch) !== false)) {
1360                 $pos = strpos($s, $authorsearch);
1361                 $rnd = random_string(8);
1362                 $authorreplace = '<br /> <span id="author-wrap-' . $rnd . '" class="author-wrap fakelink" onclick="openClose(\'author-' . $rnd . '\');">' . L10n::t('Click to open/close') . '</span>'.
1363                                         '<blockquote class="author" id="author-' . $rnd . '" style="display: block;">';
1364                 $s = substr($s, 0, $pos) . $authorreplace . substr($s, $pos + strlen($authorsearch));
1365         }
1366
1367         // Replace friendica image url size with theme preference.
1368         if (x($a->theme_info, 'item_image_size')){
1369                 $ps = $a->theme_info['item_image_size'];
1370                 $s = preg_replace('|(<img[^>]+src="[^"]+/photo/[0-9a-f]+)-[0-9]|', "$1-" . $ps, $s);
1371         }
1372
1373         $s = apply_content_filter($s, $filter_reasons);
1374
1375         $hook_data = ['item' => $item, 'html' => $s];
1376         Addon::callHooks('prepare_body_final', $hook_data);
1377
1378         return $hook_data['html'];
1379 }
1380
1381 /**
1382  * Given a HTML text and a set of filtering reasons, adds a content hiding header with the provided reasons
1383  *
1384  * Reasons are expected to have been translated already.
1385  *
1386  * @param string $html
1387  * @param array  $reasons
1388  * @return string
1389  */
1390 function apply_content_filter($html, array $reasons)
1391 {
1392         if (count($reasons)) {
1393                 $tpl = get_markup_template('wall/content_filter.tpl');
1394                 $html = replace_macros($tpl, [
1395                         '$reasons'   => $reasons,
1396                         '$rnd'       => random_string(8),
1397                         '$openclose' => L10n::t('Click to open/close'),
1398                         '$html'      => $html
1399                 ]);
1400         }
1401
1402         return $html;
1403 }
1404
1405 /**
1406  * @brief Given a text string, convert from bbcode to html and add smilie icons.
1407  *
1408  * @param string $text String with bbcode.
1409  * @return string Formattet HTML.
1410  */
1411 function prepare_text($text) {
1412         if (stristr($text, '[nosmile]')) {
1413                 $s = BBCode::convert($text);
1414         } else {
1415                 $s = Smilies::replace(BBCode::convert($text));
1416         }
1417
1418         return trim($s);
1419 }
1420
1421 /**
1422  * return array with details for categories and folders for an item
1423  *
1424  * @param array $item
1425  * @return array
1426  *
1427   * [
1428  *      [ // categories array
1429  *          {
1430  *               'name': 'category name',
1431  *               'removeurl': 'url to remove this category',
1432  *               'first': 'is the first in this array? true/false',
1433  *               'last': 'is the last in this array? true/false',
1434  *           } ,
1435  *           ....
1436  *       ],
1437  *       [ //folders array
1438  *                      {
1439  *               'name': 'folder name',
1440  *               'removeurl': 'url to remove this folder',
1441  *               'first': 'is the first in this array? true/false',
1442  *               'last': 'is the last in this array? true/false',
1443  *           } ,
1444  *           ....
1445  *       ]
1446  *  ]
1447  */
1448 function get_cats_and_terms($item)
1449 {
1450         $categories = [];
1451         $folders = [];
1452
1453         $matches = false;
1454         $first = true;
1455         $cnt = preg_match_all('/<(.*?)>/', $item['file'], $matches, PREG_SET_ORDER);
1456         if ($cnt) {
1457                 foreach ($matches as $mtch) {
1458                         $categories[] = [
1459                                 'name' => xmlify(file_tag_decode($mtch[1])),
1460                                 'url' =>  "#",
1461                                 'removeurl' => ((local_user() == $item['uid'])?'filerm/' . $item['id'] . '?f=&cat=' . xmlify(file_tag_decode($mtch[1])):""),
1462                                 'first' => $first,
1463                                 'last' => false
1464                         ];
1465                         $first = false;
1466                 }
1467         }
1468
1469         if (count($categories)) {
1470                 $categories[count($categories) - 1]['last'] = true;
1471         }
1472
1473         if (local_user() == $item['uid']) {
1474                 $matches = false;
1475                 $first = true;
1476                 $cnt = preg_match_all('/\[(.*?)\]/', $item['file'], $matches, PREG_SET_ORDER);
1477                 if ($cnt) {
1478                         foreach ($matches as $mtch) {
1479                                 $folders[] = [
1480                                         'name' => xmlify(file_tag_decode($mtch[1])),
1481                                         'url' =>  "#",
1482                                         'removeurl' => ((local_user() == $item['uid']) ? 'filerm/' . $item['id'] . '?f=&term=' . xmlify(file_tag_decode($mtch[1])) : ""),
1483                                         'first' => $first,
1484                                         'last' => false
1485                                 ];
1486                                 $first = false;
1487                         }
1488                 }
1489         }
1490
1491         if (count($folders)) {
1492                 $folders[count($folders) - 1]['last'] = true;
1493         }
1494
1495         return [$categories, $folders];
1496 }
1497
1498
1499 /**
1500  * get private link for item
1501  * @param array $item
1502  * @return boolean|array False if item has not plink, otherwise array('href'=>plink url, 'title'=>translated title)
1503  */
1504 function get_plink($item) {
1505         $a = get_app();
1506
1507         if ($a->user['nickname'] != "") {
1508                 $ret = [
1509                                 //'href' => "display/" . $a->user['nickname'] . "/" . $item['id'],
1510                                 'href' => "display/" . $item['guid'],
1511                                 'orig' => "display/" . $item['guid'],
1512                                 'title' => L10n::t('View on separate page'),
1513                                 'orig_title' => L10n::t('view on separate page'),
1514                         ];
1515
1516                 if (x($item, 'plink')) {
1517                         $ret["href"] = $a->remove_baseurl($item['plink']);
1518                         $ret["title"] = L10n::t('link to source');
1519                 }
1520
1521         } elseif (x($item, 'plink') && ($item['private'] != 1)) {
1522                 $ret = [
1523                                 'href' => $item['plink'],
1524                                 'orig' => $item['plink'],
1525                                 'title' => L10n::t('link to source'),
1526                         ];
1527         } else {
1528                 $ret = [];
1529         }
1530
1531         return $ret;
1532 }
1533
1534
1535 /**
1536  * replace html amp entity with amp char
1537  * @param string $s
1538  * @return string
1539  */
1540 function unamp($s) {
1541         return str_replace('&amp;', '&', $s);
1542 }
1543
1544
1545 /**
1546  * return number of bytes in size (K, M, G)
1547  * @param string $size_str
1548  * @return number
1549  */
1550 function return_bytes($size_str) {
1551         switch (substr ($size_str, -1)) {
1552                 case 'M': case 'm': return (int)$size_str * 1048576;
1553                 case 'K': case 'k': return (int)$size_str * 1024;
1554                 case 'G': case 'g': return (int)$size_str * 1073741824;
1555                 default: return $size_str;
1556         }
1557 }
1558
1559
1560 /**
1561  * @return string
1562  */
1563 function generate_user_guid() {
1564         $found = true;
1565         do {
1566                 $guid = System::createGUID(32);
1567                 $x = q("SELECT `uid` FROM `user` WHERE `guid` = '%s' LIMIT 1",
1568                         dbesc($guid)
1569                 );
1570                 if (!DBM::is_result($x)) {
1571                         $found = false;
1572                 }
1573         } while ($found == true);
1574
1575         return $guid;
1576 }
1577
1578
1579 /**
1580  * @param string $s
1581  * @param boolean $strip_padding
1582  * @return string
1583  */
1584 function base64url_encode($s, $strip_padding = false) {
1585
1586         $s = strtr(base64_encode($s), '+/', '-_');
1587
1588         if ($strip_padding) {
1589                 $s = str_replace('=','',$s);
1590         }
1591
1592         return $s;
1593 }
1594
1595 /**
1596  * @param string $s
1597  * @return string
1598  */
1599 function base64url_decode($s) {
1600
1601         if (is_array($s)) {
1602                 logger('base64url_decode: illegal input: ' . print_r(debug_backtrace(), true));
1603                 return $s;
1604         }
1605
1606 /*
1607  *  // Placeholder for new rev of salmon which strips base64 padding.
1608  *  // PHP base64_decode handles the un-padded input without requiring this step
1609  *  // Uncomment if you find you need it.
1610  *
1611  *      $l = strlen($s);
1612  *      if (!strpos($s,'=')) {
1613  *              $m = $l % 4;
1614  *              if ($m == 2)
1615  *                      $s .= '==';
1616  *              if ($m == 3)
1617  *                      $s .= '=';
1618  *      }
1619  *
1620  */
1621
1622         return base64_decode(strtr($s,'-_','+/'));
1623 }
1624
1625
1626 /**
1627  * return div element with class 'clear'
1628  * @return string
1629  * @deprecated
1630  */
1631 function cleardiv() {
1632         return '<div class="clear"></div>';
1633 }
1634
1635
1636 function bb_translate_video($s) {
1637
1638         $matches = null;
1639         $r = preg_match_all("/\[video\](.*?)\[\/video\]/ism",$s,$matches,PREG_SET_ORDER);
1640         if ($r) {
1641                 foreach ($matches as $mtch) {
1642                         if ((stristr($mtch[1], 'youtube')) || (stristr($mtch[1], 'youtu.be'))) {
1643                                 $s = str_replace($mtch[0], '[youtube]' . $mtch[1] . '[/youtube]', $s);
1644                         } elseif (stristr($mtch[1], 'vimeo')) {
1645                                 $s = str_replace($mtch[0], '[vimeo]' . $mtch[1] . '[/vimeo]', $s);
1646                         }
1647                 }
1648         }
1649         return $s;
1650 }
1651
1652 function html2bb_video($s) {
1653
1654         $s = preg_replace('#<object[^>]+>(.*?)https?://www.youtube.com/((?:v|cp)/[A-Za-z0-9\-_=]+)(.*?)</object>#ism',
1655                         '[youtube]$2[/youtube]', $s);
1656
1657         $s = preg_replace('#<iframe[^>](.*?)https?://www.youtube.com/embed/([A-Za-z0-9\-_=]+)(.*?)</iframe>#ism',
1658                         '[youtube]$2[/youtube]', $s);
1659
1660         $s = preg_replace('#<iframe[^>](.*?)https?://player.vimeo.com/video/([0-9]+)(.*?)</iframe>#ism',
1661                         '[vimeo]$2[/vimeo]', $s);
1662
1663         return $s;
1664 }
1665
1666 /**
1667  * apply xmlify() to all values of array $val, recursively
1668  * @param array $val
1669  * @return array
1670  */
1671 function array_xmlify($val){
1672         if (is_bool($val)) {
1673                 return $val?"true":"false";
1674         } elseif (is_array($val)) {
1675                 return array_map('array_xmlify', $val);
1676         }
1677         return xmlify((string) $val);
1678 }
1679
1680
1681 /**
1682  * transform link href and img src from relative to absolute
1683  *
1684  * @param string $text
1685  * @param string $base base url
1686  * @return string
1687  */
1688 function reltoabs($text, $base) {
1689         if (empty($base)) {
1690                 return $text;
1691         }
1692
1693         $base = rtrim($base,'/');
1694
1695         $base2 = $base . "/";
1696
1697         // Replace links
1698         $pattern = "/<a([^>]*) href=\"(?!http|https|\/)([^\"]*)\"/";
1699         $replace = "<a\${1} href=\"" . $base2 . "\${2}\"";
1700         $text = preg_replace($pattern, $replace, $text);
1701
1702         $pattern = "/<a([^>]*) href=\"(?!http|https)([^\"]*)\"/";
1703         $replace = "<a\${1} href=\"" . $base . "\${2}\"";
1704         $text = preg_replace($pattern, $replace, $text);
1705
1706         // Replace images
1707         $pattern = "/<img([^>]*) src=\"(?!http|https|\/)([^\"]*)\"/";
1708         $replace = "<img\${1} src=\"" . $base2 . "\${2}\"";
1709         $text = preg_replace($pattern, $replace, $text);
1710
1711         $pattern = "/<img([^>]*) src=\"(?!http|https)([^\"]*)\"/";
1712         $replace = "<img\${1} src=\"" . $base . "\${2}\"";
1713         $text = preg_replace($pattern, $replace, $text);
1714
1715
1716         // Done
1717         return $text;
1718 }
1719
1720 /**
1721  * get translated item type
1722  *
1723  * @param array $itme
1724  * @return string
1725  */
1726 function item_post_type($item) {
1727         if (!empty($item['event-id'])) {
1728                 return L10n::t('event');
1729         } elseif (!empty($item['resource-id'])) {
1730                 return L10n::t('photo');
1731         } elseif (!empty($item['verb']) && $item['verb'] !== ACTIVITY_POST) {
1732                 return L10n::t('activity');
1733         } elseif ($item['id'] != $item['parent']) {
1734                 return L10n::t('comment');
1735         }
1736
1737         return L10n::t('post');
1738 }
1739
1740 // post categories and "save to file" use the same item.file table for storage.
1741 // We will differentiate the different uses by wrapping categories in angle brackets
1742 // and save to file categories in square brackets.
1743 // To do this we need to escape these characters if they appear in our tag.
1744
1745 function file_tag_encode($s) {
1746         return str_replace(['<','>','[',']'],['%3c','%3e','%5b','%5d'],$s);
1747 }
1748
1749 function file_tag_decode($s) {
1750         return str_replace(['%3c', '%3e', '%5b', '%5d'], ['<', '>', '[', ']'], $s);
1751 }
1752
1753 function file_tag_file_query($table,$s,$type = 'file') {
1754
1755         if ($type == 'file') {
1756                 $str = preg_quote('[' . str_replace('%', '%%', file_tag_encode($s)) . ']');
1757         } else {
1758                 $str = preg_quote('<' . str_replace('%', '%%', file_tag_encode($s)) . '>');
1759         }
1760         return " AND " . (($table) ? dbesc($table) . '.' : '') . "file regexp '" . dbesc($str) . "' ";
1761 }
1762
1763 // ex. given music,video return <music><video> or [music][video]
1764 function file_tag_list_to_file($list, $type = 'file') {
1765         $tag_list = '';
1766         if (strlen($list)) {
1767                 $list_array = explode(",",$list);
1768                 if ($type == 'file') {
1769                         $lbracket = '[';
1770                         $rbracket = ']';
1771                 } else {
1772                         $lbracket = '<';
1773                         $rbracket = '>';
1774                 }
1775
1776                 foreach ($list_array as $item) {
1777                         if (strlen($item)) {
1778                                 $tag_list .= $lbracket . file_tag_encode(trim($item))  . $rbracket;
1779                         }
1780                 }
1781         }
1782         return $tag_list;
1783 }
1784
1785 // ex. given <music><video>[friends], return music,video or friends
1786 function file_tag_file_to_list($file, $type = 'file') {
1787         $matches = false;
1788         $list = '';
1789         if ($type == 'file') {
1790                 $cnt = preg_match_all('/\[(.*?)\]/', $file, $matches, PREG_SET_ORDER);
1791         } else {
1792                 $cnt = preg_match_all('/<(.*?)>/', $file, $matches, PREG_SET_ORDER);
1793         }
1794         if ($cnt) {
1795                 foreach ($matches as $mtch) {
1796                         if (strlen($list)) {
1797                                 $list .= ',';
1798                         }
1799                         $list .= file_tag_decode($mtch[1]);
1800                 }
1801         }
1802
1803         return $list;
1804 }
1805
1806 function file_tag_update_pconfig($uid, $file_old, $file_new, $type = 'file') {
1807         // $file_old - categories previously associated with an item
1808         // $file_new - new list of categories for an item
1809
1810         if (!intval($uid)) {
1811                 return false;
1812         } elseif ($file_old == $file_new) {
1813                 return true;
1814         }
1815
1816         $saved = PConfig::get($uid, 'system', 'filetags');
1817         if (strlen($saved)) {
1818                 if ($type == 'file') {
1819                         $lbracket = '[';
1820                         $rbracket = ']';
1821                         $termtype = TERM_FILE;
1822                 } else {
1823                         $lbracket = '<';
1824                         $rbracket = '>';
1825                         $termtype = TERM_CATEGORY;
1826                 }
1827
1828                 $filetags_updated = $saved;
1829
1830                 // check for new tags to be added as filetags in pconfig
1831                 $new_tags = [];
1832                 $check_new_tags = explode(",",file_tag_file_to_list($file_new,$type));
1833
1834                 foreach ($check_new_tags as $tag) {
1835                         if (!stristr($saved,$lbracket . file_tag_encode($tag) . $rbracket)) {
1836                                 $new_tags[] = $tag;
1837                         }
1838                 }
1839
1840                 $filetags_updated .= file_tag_list_to_file(implode(",",$new_tags),$type);
1841
1842                 // check for deleted tags to be removed from filetags in pconfig
1843                 $deleted_tags = [];
1844                 $check_deleted_tags = explode(",",file_tag_file_to_list($file_old,$type));
1845
1846                 foreach ($check_deleted_tags as $tag) {
1847                         if (!stristr($file_new,$lbracket . file_tag_encode($tag) . $rbracket)) {
1848                                 $deleted_tags[] = $tag;
1849                         }
1850                 }
1851
1852                 foreach ($deleted_tags as $key => $tag) {
1853                         $r = q("SELECT `oid` FROM `term` WHERE `term` = '%s' AND `otype` = %d AND `type` = %d AND `uid` = %d",
1854                                 dbesc($tag),
1855                                 intval(TERM_OBJ_POST),
1856                                 intval($termtype),
1857                                 intval($uid));
1858
1859                         if (DBM::is_result($r)) {
1860                                 unset($deleted_tags[$key]);
1861                         } else {
1862                                 $filetags_updated = str_replace($lbracket . file_tag_encode($tag) . $rbracket,'',$filetags_updated);
1863                         }
1864                 }
1865
1866                 if ($saved != $filetags_updated) {
1867                         PConfig::set($uid, 'system', 'filetags', $filetags_updated);
1868                 }
1869                 return true;
1870         } elseif (strlen($file_new)) {
1871                 PConfig::set($uid, 'system', 'filetags', $file_new);
1872         }
1873         return true;
1874 }
1875
1876 function file_tag_save_file($uid, $item_id, $file)
1877 {
1878         if (!intval($uid)) {
1879                 return false;
1880         }
1881
1882         $item = Item::selectFirst(['file'], ['id' => $item_id, 'uid' => $uid]);
1883         if (DBM::is_result($item)) {
1884                 if (!stristr($item['file'],'[' . file_tag_encode($file) . ']')) {
1885                         $fields = ['file' => $item['file'] . '[' . file_tag_encode($file) . ']'];
1886                         Item::update($fields, ['id' => $item_id]);
1887                 }
1888                 $saved = PConfig::get($uid, 'system', 'filetags');
1889                 if (!strlen($saved) || !stristr($saved, '[' . file_tag_encode($file) . ']')) {
1890                         PConfig::set($uid, 'system', 'filetags', $saved . '[' . file_tag_encode($file) . ']');
1891                 }
1892                 info(L10n::t('Item filed'));
1893         }
1894         return true;
1895 }
1896
1897 function file_tag_unsave_file($uid, $item_id, $file, $cat = false)
1898 {
1899         if (!intval($uid)) {
1900                 return false;
1901         }
1902
1903         if ($cat == true) {
1904                 $pattern = '<' . file_tag_encode($file) . '>' ;
1905                 $termtype = TERM_CATEGORY;
1906         } else {
1907                 $pattern = '[' . file_tag_encode($file) . ']' ;
1908                 $termtype = TERM_FILE;
1909         }
1910
1911         $item = Item::selectFirst(['file'], ['id' => $item_id, 'uid' => $uid]);
1912         if (!DBM::is_result($item)) {
1913                 return false;
1914         }
1915
1916         $fields = ['file' => str_replace($pattern,'',$item['file'])];
1917         Item::update($fields, ['id' => $item_id]);
1918
1919         $r = q("SELECT `oid` FROM `term` WHERE `term` = '%s' AND `otype` = %d AND `type` = %d AND `uid` = %d",
1920                 dbesc($file),
1921                 intval(TERM_OBJ_POST),
1922                 intval($termtype),
1923                 intval($uid)
1924         );
1925         if (!DBM::is_result($r)) {
1926                 $saved = PConfig::get($uid, 'system', 'filetags');
1927                 PConfig::set($uid, 'system', 'filetags', str_replace($pattern, '', $saved));
1928         }
1929
1930         return true;
1931 }
1932
1933 function normalise_openid($s) {
1934         return trim(str_replace(['http://', 'https://'], ['', ''], $s), '/');
1935 }
1936
1937
1938 function undo_post_tagging($s) {
1939         $matches = null;
1940         $cnt = preg_match_all('/([!#@])\[url=(.*?)\](.*?)\[\/url\]/ism', $s, $matches, PREG_SET_ORDER);
1941         if ($cnt) {
1942                 foreach ($matches as $mtch) {
1943                         if (in_array($mtch[1], ['!', '@'])) {
1944                                 $contact = Contact::getDetailsByURL($mtch[2]);
1945                                 $mtch[3] = empty($contact['addr']) ? $mtch[2] : $contact['addr'];
1946                         }
1947                         $s = str_replace($mtch[0], $mtch[1] . $mtch[3],$s);
1948                 }
1949         }
1950         return $s;
1951 }
1952
1953 function protect_sprintf($s) {
1954         return str_replace('%', '%%', $s);
1955 }
1956
1957 /// @TODO Rewrite this
1958 function is_a_date_arg($s) {
1959         $i = intval($s);
1960
1961         if ($i > 1900) {
1962                 $y = date('Y');
1963
1964                 if ($i <= $y + 1 && strpos($s, '-') == 4) {
1965                         $m = intval(substr($s, 5));
1966
1967                         if ($m > 0 && $m <= 12) {
1968                                 return true;
1969                         }
1970                 }
1971         }
1972
1973         return false;
1974 }
1975
1976 /**
1977  * remove intentation from a text
1978  */
1979 function deindent($text, $chr = "[\t ]", $count = NULL) {
1980         $lines = explode("\n", $text);
1981
1982         if (is_null($count)) {
1983                 $m = [];
1984                 $k = 0;
1985                 while ($k < count($lines) && strlen($lines[$k]) == 0) {
1986                         $k++;
1987                 }
1988                 preg_match("|^" . $chr . "*|", $lines[$k], $m);
1989                 $count = strlen($m[0]);
1990         }
1991
1992         for ($k = 0; $k < count($lines); $k++) {
1993                 $lines[$k] = preg_replace("|^" . $chr . "{" . $count . "}|", "", $lines[$k]);
1994         }
1995
1996         return implode("\n", $lines);
1997 }
1998
1999 function formatBytes($bytes, $precision = 2) {
2000         $units = ['B', 'KB', 'MB', 'GB', 'TB'];
2001
2002         $bytes = max($bytes, 0);
2003         $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
2004         $pow = min($pow, count($units) - 1);
2005
2006         $bytes /= pow(1024, $pow);
2007
2008         return round($bytes, $precision) . ' ' . $units[$pow];
2009 }
2010
2011 /**
2012  * @brief translate and format the networkname of a contact
2013  *
2014  * @param string $network
2015  *      Networkname of the contact (e.g. dfrn, rss and so on)
2016  * @param sting $url
2017  *      The contact url
2018  * @return string
2019  */
2020 function format_network_name($network, $url = 0) {
2021         if ($network != "") {
2022                 if ($url != "") {
2023                         $network_name = '<a href="'.$url.'">'.ContactSelector::networkToName($network, $url)."</a>";
2024                 } else {
2025                         $network_name = ContactSelector::networkToName($network);
2026                 }
2027
2028                 return $network_name;
2029         }
2030 }
2031
2032 /**
2033  * @brief Syntax based code highlighting for popular languages.
2034  * @param string $s Code block
2035  * @param string $lang Programming language
2036  * @return string Formated html
2037  */
2038 function text_highlight($s, $lang) {
2039         if ($lang === 'js') {
2040                 $lang = 'javascript';
2041         }
2042
2043         if ($lang === 'bash') {
2044                 $lang = 'sh';
2045         }
2046
2047         // @TODO: Replace Text_Highlighter_Renderer_Html by scrivo/highlight.php
2048
2049         // Autoload the library to make constants available
2050         class_exists('Text_Highlighter_Renderer_Html');
2051
2052         $options = [
2053                 'numbers' => HL_NUMBERS_LI,
2054                 'tabsize' => 4,
2055         ];
2056
2057         $tag_added = false;
2058         $s = trim(html_entity_decode($s, ENT_COMPAT));
2059         $s = str_replace('    ', "\t", $s);
2060
2061         /*
2062          * The highlighter library insists on an opening php tag for php code blocks. If
2063          * it isn't present, nothing is highlighted. So we're going to see if it's present.
2064          * If not, we'll add it, and then quietly remove it after we get the processed output back.
2065          */
2066         if ($lang === 'php' && strpos($s, '<?php') !== 0) {
2067                 $s = '<?php' . "\n" . $s;
2068                 $tag_added = true;
2069         }
2070
2071         $renderer = new Text_Highlighter_Renderer_Html($options);
2072         $factory = new Text_Highlighter();
2073         $hl = $factory->factory($lang);
2074         $hl->setRenderer($renderer);
2075         $o = $hl->highlight($s);
2076         $o = str_replace("\n", '', $o);
2077
2078         if ($tag_added) {
2079                 $b = substr($o, 0, strpos($o, '<li>'));
2080                 $e = substr($o, strpos($o, '</li>'));
2081                 $o = $b . $e;
2082         }
2083
2084         return '<code>' . $o . '</code>';
2085 }