]> git.mxchange.org Git - friendica.git/blob - include/text.php
Use already fetched data for magiclink
[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         $logline = sprintf("%s@%s\t[%s]:%s:%s:%s\t%s\n",
647                         DateTimeFormat::utcNow(DateTimeFormat::ATOM),
648                         $process_id,
649                         $LOGGER_LEVELS[$level],
650                         basename($callers[0]['file']),
651                         $callers[0]['line'],
652                         $callers[1]['function'],
653                         $msg
654                 );
655
656         $stamp1 = microtime(true);
657         @file_put_contents($logfile, $logline, FILE_APPEND);
658         $a->save_timestamp($stamp1, "file");
659 }
660
661 /**
662  * @brief An alternative logger for development.
663  * Works largely as logger() but allows developers
664  * to isolate particular elements they are targetting
665  * personally without background noise
666  *
667  * log levels:
668  * LOGGER_NORMAL (default)
669  * LOGGER_TRACE
670  * LOGGER_DEBUG
671  * LOGGER_DATA
672  * LOGGER_ALL
673  *
674  * @global App $a
675  * @global array $LOGGER_LEVELS
676  * @param string $msg
677  * @param int $level
678  */
679
680 function dlogger($msg, $level = 0) {
681         $a = get_app();
682
683         // turn off logger in install mode
684         if (
685                 $a->mode == App::MODE_INSTALL
686                 || !dba::$connected
687         ) {
688                 return;
689         }
690
691         $logfile = Config::get('system', 'dlogfile');
692         if (!$logfile) {
693                 return;
694         }
695
696         $dlogip = Config::get('system', 'dlogip');
697         if (!is_null($dlogip) && $_SERVER['REMOTE_ADDR'] != $dlogip) {
698                 return;
699         }
700
701         if (count($LOGGER_LEVELS) == 0) {
702                 foreach (get_defined_constants() as $k => $v) {
703                         if (substr($k, 0, 7) == "LOGGER_") {
704                                 $LOGGER_LEVELS[$v] = substr($k, 7, 7);
705                         }
706                 }
707         }
708
709         $process_id = session_id();
710
711         if ($process_id == '') {
712                 $process_id = get_app()->process_id;
713         }
714
715         $callers = debug_backtrace();
716         $logline = sprintf("%s@\t%s:\t%s:\t%s\t%s\t%s\n",
717                         DateTimeFormat::utcNow(),
718                         $process_id,
719                         basename($callers[0]['file']),
720                         $callers[0]['line'],
721                         $callers[1]['function'],
722                         $msg
723                 );
724
725         $stamp1 = microtime(true);
726         @file_put_contents($logfile, $logline, FILE_APPEND);
727         $a->save_timestamp($stamp1, "file");
728 }
729
730
731 /**
732  * Compare activity uri. Knows about activity namespace.
733  *
734  * @param string $haystack
735  * @param string $needle
736  * @return boolean
737  */
738 function activity_match($haystack,$needle) {
739         return (($haystack === $needle) || ((basename($needle) === $haystack) && strstr($needle, NAMESPACE_ACTIVITY_SCHEMA)));
740 }
741
742
743 /**
744  * @brief Pull out all #hashtags and @person tags from $string.
745  *
746  * We also get @person@domain.com - which would make
747  * the regex quite complicated as tags can also
748  * end a sentence. So we'll run through our results
749  * and strip the period from any tags which end with one.
750  * Returns array of tags found, or empty array.
751  *
752  * @param string $string Post content
753  * @return array List of tag and person names
754  */
755 function get_tags($string) {
756         $ret = [];
757
758         // Convert hashtag links to hashtags
759         $string = preg_replace('/#\[url\=([^\[\]]*)\](.*?)\[\/url\]/ism', '#$2', $string);
760
761         // ignore anything in a code block
762         $string = preg_replace('/\[code\](.*?)\[\/code\]/sm', '', $string);
763
764         // Force line feeds at bbtags
765         $string = str_replace(['[', ']'], ["\n[", "]\n"], $string);
766
767         // ignore anything in a bbtag
768         $string = preg_replace('/\[(.*?)\]/sm', '', $string);
769
770         // Match full names against @tags including the space between first and last
771         // We will look these up afterward to see if they are full names or not recognisable.
772
773         if (preg_match_all('/(@[^ \x0D\x0A,:?]+ [^ \x0D\x0A@,:?]+)([ \x0D\x0A@,:?]|$)/', $string, $matches)) {
774                 foreach ($matches[1] as $match) {
775                         if (strstr($match, ']')) {
776                                 // we might be inside a bbcode color tag - leave it alone
777                                 continue;
778                         }
779                         if (substr($match, -1, 1) === '.') {
780                                 $ret[] = substr($match, 0, -1);
781                         } else {
782                                 $ret[] = $match;
783                         }
784                 }
785         }
786
787         // Otherwise pull out single word tags. These can be @nickname, @first_last
788         // and #hash tags.
789
790         if (preg_match_all('/([!#@][^\^ \x0D\x0A,;:?]+)([ \x0D\x0A,;:?]|$)/', $string, $matches)) {
791                 foreach ($matches[1] as $match) {
792                         if (strstr($match, ']')) {
793                                 // we might be inside a bbcode color tag - leave it alone
794                                 continue;
795                         }
796                         if (substr($match, -1, 1) === '.') {
797                                 $match = substr($match,0,-1);
798                         }
799                         // ignore strictly numeric tags like #1
800                         if ((strpos($match, '#') === 0) && ctype_digit(substr($match, 1))) {
801                                 continue;
802                         }
803                         // try not to catch url fragments
804                         if (strpos($string, $match) && preg_match('/[a-zA-z0-9\/]/', substr($string, strpos($string, $match) - 1, 1))) {
805                                 continue;
806                         }
807                         $ret[] = $match;
808                 }
809         }
810         return $ret;
811 }
812
813
814 /**
815  * quick and dirty quoted_printable encoding
816  *
817  * @param string $s
818  * @return string
819  */
820 function qp($s) {
821         return str_replace("%", "=", rawurlencode($s));
822 }
823
824
825 /**
826  * Get html for contact block.
827  *
828  * @template contact_block.tpl
829  * @hook contact_block_end (contacts=>array, output=>string)
830  * @return string
831  */
832 function contact_block() {
833         $o = '';
834         $a = get_app();
835
836         $shown = PConfig::get($a->profile['uid'], 'system', 'display_friend_count', 24);
837         if ($shown == 0) {
838                 return;
839         }
840
841         if (!is_array($a->profile) || $a->profile['hide-friends']) {
842                 return $o;
843         }
844         $r = q("SELECT COUNT(*) AS `total` FROM `contact`
845                         WHERE `uid` = %d AND NOT `self` AND NOT `blocked`
846                                 AND NOT `pending` AND NOT `hidden` AND NOT `archive`
847                                 AND `network` IN ('%s', '%s', '%s')",
848                         intval($a->profile['uid']),
849                         dbesc(NETWORK_DFRN),
850                         dbesc(NETWORK_OSTATUS),
851                         dbesc(NETWORK_DIASPORA)
852         );
853         if (DBM::is_result($r)) {
854                 $total = intval($r[0]['total']);
855         }
856         if (!$total) {
857                 $contacts = L10n::t('No contacts');
858                 $micropro = null;
859         } else {
860                 // Splitting the query in two parts makes it much faster
861                 $r = q("SELECT `id` FROM `contact`
862                                 WHERE `uid` = %d AND NOT `self` AND NOT `blocked`
863                                         AND NOT `pending` AND NOT `hidden` AND NOT `archive`
864                                         AND `network` IN ('%s', '%s', '%s')
865                                 ORDER BY RAND() LIMIT %d",
866                                 intval($a->profile['uid']),
867                                 dbesc(NETWORK_DFRN),
868                                 dbesc(NETWORK_OSTATUS),
869                                 dbesc(NETWORK_DIASPORA),
870                                 intval($shown)
871                 );
872                 if (DBM::is_result($r)) {
873                         $contacts = [];
874                         foreach ($r AS $contact) {
875                                 $contacts[] = $contact["id"];
876                         }
877                         $r = q("SELECT `id`, `uid`, `addr`, `url`, `name`, `thumb`, `network` FROM `contact` WHERE `id` IN (%s)",
878                                 dbesc(implode(",", $contacts)));
879
880                         if (DBM::is_result($r)) {
881                                 $contacts = L10n::tt('%d Contact', '%d Contacts', $total);
882                                 $micropro = [];
883                                 foreach ($r as $rr) {
884                                         $micropro[] = micropro($rr, true, 'mpfriend');
885                                 }
886                         }
887                 }
888         }
889
890         $tpl = get_markup_template('contact_block.tpl');
891         $o = replace_macros($tpl, [
892                 '$contacts' => $contacts,
893                 '$nickname' => $a->profile['nickname'],
894                 '$viewcontacts' => L10n::t('View Contacts'),
895                 '$micropro' => $micropro,
896         ]);
897
898         $arr = ['contacts' => $r, 'output' => $o];
899
900         Addon::callHooks('contact_block_end', $arr);
901         return $o;
902
903 }
904
905
906 /**
907  * @brief Format contacts as picture links or as texxt links
908  *
909  * @param array $contact Array with contacts which contains an array with
910  *      int 'id' => The ID of the contact
911  *      int 'uid' => The user ID of the user who owns this data
912  *      string 'name' => The name of the contact
913  *      string 'url' => The url to the profile page of the contact
914  *      string 'addr' => The webbie of the contact (e.g.) username@friendica.com
915  *      string 'network' => The network to which the contact belongs to
916  *      string 'thumb' => The contact picture
917  *      string 'click' => js code which is performed when clicking on the contact
918  * @param boolean $redirect If true try to use the redir url if it's possible
919  * @param string $class CSS class for the
920  * @param boolean $textmode If true display the contacts as text links
921  *      if false display the contacts as picture links
922
923  * @return string Formatted html
924  */
925 function micropro($contact, $redirect = false, $class = '', $textmode = false) {
926
927         // Use the contact URL if no address is available
928         if (!x($contact, "addr")) {
929                 $contact["addr"] = $contact["url"];
930         }
931
932         $url = $contact['url'];
933         $sparkle = '';
934         $redir = false;
935
936         if ($redirect) {
937                 $url = Contact::magicLink($contact['url']);
938                 if (strpos($url, 'redir/') === 0) {
939                         $sparkle = ' sparkle';
940                 }
941         }
942
943         // If there is some js available we don't need the url
944         if (x($contact, 'click')) {
945                 $url = '';
946         }
947
948         return replace_macros(get_markup_template(($textmode)?'micropro_txt.tpl':'micropro_img.tpl'),[
949                 '$click' => defaults($contact, 'click', ''),
950                 '$class' => $class,
951                 '$url' => $url,
952                 '$photo' => proxy_url($contact['thumb'], false, PROXY_SIZE_THUMB),
953                 '$name' => $contact['name'],
954                 'title' => $contact['name'] . ' [' . $contact['addr'] . ']',
955                 '$parkle' => $sparkle,
956                 '$redir' => $redir,
957
958         ]);
959 }
960
961 /**
962  * Search box.
963  *
964  * @param string $s     Search query.
965  * @param string $id    HTML id
966  * @param string $url   Search url.
967  * @param bool   $save  Show save search button.
968  * @param bool   $aside Display the search widgit aside.
969  *
970  * @return string Formatted HTML.
971  */
972 function search($s, $id = 'search-box', $url = 'search', $save = false, $aside = true)
973 {
974         $mode = 'text';
975
976         if (strpos($s, '#') === 0) {
977                 $mode = 'tag';
978         }
979         $save_label = $mode === 'text' ? L10n::t('Save') : L10n::t('Follow');
980
981         $values = [
982                         '$s' => htmlspecialchars($s),
983                         '$id' => $id,
984                         '$action_url' => $url,
985                         '$search_label' => L10n::t('Search'),
986                         '$save_label' => $save_label,
987                         '$savedsearch' => local_user() && Feature::isEnabled(local_user(),'savedsearch'),
988                         '$search_hint' => L10n::t('@name, !forum, #tags, content'),
989                         '$mode' => $mode
990                 ];
991
992         if (!$aside) {
993                 $values['$searchoption'] = [
994                                         L10n::t("Full Text"),
995                                         L10n::t("Tags"),
996                                         L10n::t("Contacts")];
997
998                 if (Config::get('system','poco_local_search')) {
999                         $values['$searchoption'][] = L10n::t("Forums");
1000                 }
1001         }
1002
1003         return replace_macros(get_markup_template('searchbox.tpl'), $values);
1004 }
1005
1006 /**
1007  * @brief Check for a valid email string
1008  *
1009  * @param string $email_address
1010  * @return boolean
1011  */
1012 function valid_email($email_address)
1013 {
1014         return preg_match('/^[_a-zA-Z0-9\-\+]+(\.[_a-zA-Z0-9\-\+]+)*@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+$/', $email_address);
1015 }
1016
1017
1018 /**
1019  * Replace naked text hyperlink with HTML formatted hyperlink
1020  *
1021  * @param string $s
1022  */
1023 function linkify($s) {
1024         $s = preg_replace("/(https?\:\/\/[a-zA-Z0-9\:\/\-\?\&\;\.\=\_\~\#\'\%\$\!\+]*)/", ' <a href="$1" target="_blank">$1</a>', $s);
1025         $s = preg_replace("/\<(.*?)(src|href)=(.*?)\&amp\;(.*?)\>/ism",'<$1$2=$3&$4>',$s);
1026         return $s;
1027 }
1028
1029
1030 /**
1031  * Load poke verbs
1032  *
1033  * @return array index is present tense verb
1034  *                               value is array containing past tense verb, translation of present, translation of past
1035  * @hook poke_verbs pokes array
1036  */
1037 function get_poke_verbs() {
1038
1039         // index is present tense verb
1040         // value is array containing past tense verb, translation of present, translation of past
1041
1042         $arr = [
1043                 'poke' => ['poked', L10n::t('poke'), L10n::t('poked')],
1044                 'ping' => ['pinged', L10n::t('ping'), L10n::t('pinged')],
1045                 'prod' => ['prodded', L10n::t('prod'), L10n::t('prodded')],
1046                 'slap' => ['slapped', L10n::t('slap'), L10n::t('slapped')],
1047                 'finger' => ['fingered', L10n::t('finger'), L10n::t('fingered')],
1048                 'rebuff' => ['rebuffed', L10n::t('rebuff'), L10n::t('rebuffed')],
1049         ];
1050         Addon::callHooks('poke_verbs', $arr);
1051         return $arr;
1052 }
1053
1054 /**
1055  * @brief Translate days and months names.
1056  *
1057  * @param string $s String with day or month name.
1058  * @return string Translated string.
1059  */
1060 function day_translate($s) {
1061         $ret = str_replace(['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'],
1062                 [L10n::t('Monday'), L10n::t('Tuesday'), L10n::t('Wednesday'), L10n::t('Thursday'), L10n::t('Friday'), L10n::t('Saturday'), L10n::t('Sunday')],
1063                 $s);
1064
1065         $ret = str_replace(['January','February','March','April','May','June','July','August','September','October','November','December'],
1066                 [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')],
1067                 $ret);
1068
1069         return $ret;
1070 }
1071
1072 /**
1073  * @brief Translate short days and months names.
1074  *
1075  * @param string $s String with short day or month name.
1076  * @return string Translated string.
1077  */
1078 function day_short_translate($s) {
1079         $ret = str_replace(['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
1080                 [L10n::t('Mon'), L10n::t('Tue'), L10n::t('Wed'), L10n::t('Thu'), L10n::t('Fri'), L10n::t('Sat'), L10n::t('Sun')],
1081                 $s);
1082         $ret = str_replace(['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov','Dec'],
1083                 [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')],
1084                 $ret);
1085         return $ret;
1086 }
1087
1088
1089 /**
1090  * Normalize url
1091  *
1092  * @param string $url
1093  * @return string
1094  */
1095 function normalise_link($url) {
1096         $ret = str_replace(['https:', '//www.'], ['http:', '//'], $url);
1097         return rtrim($ret,'/');
1098 }
1099
1100
1101 /**
1102  * Compare two URLs to see if they are the same, but ignore
1103  * slight but hopefully insignificant differences such as if one
1104  * is https and the other isn't, or if one is www.something and
1105  * the other isn't - and also ignore case differences.
1106  *
1107  * @param string $a first url
1108  * @param string $b second url
1109  * @return boolean True if the URLs match, otherwise False
1110  *
1111  */
1112 function link_compare($a, $b) {
1113         return (strcasecmp(normalise_link($a), normalise_link($b)) === 0);
1114 }
1115
1116
1117 /**
1118  * @brief Find any non-embedded images in private items and add redir links to them
1119  *
1120  * @param App $a
1121  * @param array &$item The field array of an item row
1122  */
1123 function redir_private_images($a, &$item)
1124 {
1125         $matches = false;
1126         $cnt = preg_match_all('|\[img\](http[^\[]*?/photo/[a-fA-F0-9]+?(-[0-9]\.[\w]+?)?)\[\/img\]|', $item['body'], $matches, PREG_SET_ORDER);
1127         if ($cnt) {
1128                 foreach ($matches as $mtch) {
1129                         if (strpos($mtch[1], '/redir') !== false) {
1130                                 continue;
1131                         }
1132
1133                         if ((local_user() == $item['uid']) && ($item['private'] != 0) && ($item['contact-id'] != $a->contact['id']) && ($item['network'] == NETWORK_DFRN)) {
1134                                 $img_url = 'redir?f=1&quiet=1&url=' . urlencode($mtch[1]) . '&conurl=' . urlencode($item['author-link']);
1135                                 $item['body'] = str_replace($mtch[0], '[img]' . $img_url . '[/img]', $item['body']);
1136                         }
1137                 }
1138         }
1139 }
1140
1141 /**
1142  * Sets the "rendered-html" field of the provided item
1143  *
1144  * Body is preserved to avoid side-effects as we modify it just-in-time for spoilers and private image links
1145  *
1146  * @param array $item
1147  * @param bool  $update
1148  *
1149  * @todo Remove reference, simply return "rendered-html" and "rendered-hash"
1150  */
1151 function put_item_in_cache(&$item, $update = false)
1152 {
1153         $body = $item["body"];
1154
1155         $rendered_hash = defaults($item, 'rendered-hash', '');
1156         $rendered_html = defaults($item, 'rendered-html', '');
1157
1158         if ($rendered_hash == ''
1159                 || $item["rendered-html"] == ""
1160                 || $rendered_hash != hash("md5", $item["body"])
1161                 || Config::get("system", "ignore_cache")
1162         ) {
1163                 $a = get_app();
1164                 redir_private_images($a, $item);
1165
1166                 $item["rendered-html"] = prepare_text($item["body"]);
1167                 $item["rendered-hash"] = hash("md5", $item["body"]);
1168
1169                 // Force an update if the generated values differ from the existing ones
1170                 if ($rendered_hash != $item["rendered-hash"]) {
1171                         $update = true;
1172                 }
1173
1174                 // Only compare the HTML when we forcefully ignore the cache
1175                 if (Config::get("system", "ignore_cache") && ($rendered_html != $item["rendered-html"])) {
1176                         $update = true;
1177                 }
1178
1179                 if ($update && ($item["id"] > 0)) {
1180                         Item::update(['rendered-html' => $item["rendered-html"], 'rendered-hash' => $item["rendered-hash"]],
1181                                         ['id' => $item["id"]]);
1182                 }
1183         }
1184
1185         $item["body"] = $body;
1186 }
1187
1188 /**
1189  * @brief Given an item array, convert the body element from bbcode to html and add smilie icons.
1190  * If attach is true, also add icons for item attachments.
1191  *
1192  * @param array   $item
1193  * @param boolean $attach
1194  * @param boolean $is_preview
1195  * @return string item body html
1196  * @hook prepare_body_init item array before any work
1197  * @hook prepare_body_content_filter ('item'=>item array, 'filter_reasons'=>string array) before first bbcode to html
1198  * @hook prepare_body ('item'=>item array, 'html'=>body string, 'is_preview'=>boolean, 'filter_reasons'=>string array) after first bbcode to html
1199  * @hook prepare_body_final ('item'=>item array, 'html'=>body string) after attach icons and blockquote special case handling (spoiler, author)
1200  */
1201 function prepare_body(array &$item, $attach = false, $is_preview = false)
1202 {
1203         $a = get_app();
1204         Addon::callHooks('prepare_body_init', $item);
1205
1206         // In order to provide theme developers more possibilities, event items
1207         // are treated differently.
1208         if ($item['object-type'] === ACTIVITY_OBJ_EVENT && isset($item['event-id'])) {
1209                 $ev = Event::getItemHTML($item);
1210                 return $ev;
1211         }
1212
1213         $tags = \Friendica\Model\Term::populateTagsFromItem($item);
1214
1215         $item['tags'] = $tags['tags'];
1216         $item['hashtags'] = $tags['hashtags'];
1217         $item['mentions'] = $tags['mentions'];
1218
1219         // Compile eventual content filter reasons
1220         $filter_reasons = [];
1221         if (!$is_preview && public_contact() != $item['author-id']) {
1222                 if (!empty($item['content-warning']) && (!local_user() || !PConfig::get(local_user(), 'system', 'disable_cw', false))) {
1223                         $filter_reasons[] = L10n::t('Content warning: %s', $item['content-warning']);
1224                 }
1225
1226                 $hook_data = [
1227                         'item' => $item,
1228                         'filter_reasons' => $filter_reasons
1229                 ];
1230                 Addon::callHooks('prepare_body_content_filter', $hook_data);
1231                 $filter_reasons = $hook_data['filter_reasons'];
1232                 unset($hook_data);
1233         }
1234
1235         // Update the cached values if there is no "zrl=..." on the links.
1236         $update = (!local_user() && !remote_user() && ($item["uid"] == 0));
1237
1238         // Or update it if the current viewer is the intented viewer.
1239         if (($item["uid"] == local_user()) && ($item["uid"] != 0)) {
1240                 $update = true;
1241         }
1242
1243         put_item_in_cache($item, $update);
1244         $s = $item["rendered-html"];
1245
1246         $hook_data = [
1247                 'item' => $item,
1248                 'html' => $s,
1249                 'preview' => $is_preview,
1250                 'filter_reasons' => $filter_reasons
1251         ];
1252         Addon::callHooks('prepare_body', $hook_data);
1253         $s = $hook_data['html'];
1254         unset($hook_data);
1255
1256         if (!$attach) {
1257                 // Replace the blockquotes with quotes that are used in mails.
1258                 $mailquote = '<blockquote type="cite" class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;">';
1259                 $s = str_replace(['<blockquote>', '<blockquote class="spoiler">', '<blockquote class="author">'], [$mailquote, $mailquote, $mailquote], $s);
1260                 return $s;
1261         }
1262
1263         $as = '';
1264         $vhead = false;
1265         $matches = [];
1266         preg_match_all('|\[attach\]href=\"(.*?)\" length=\"(.*?)\" type=\"(.*?)\"(?: title=\"(.*?)\")?|', $item['attach'], $matches, PREG_SET_ORDER);
1267         foreach ($matches as $mtch) {
1268                 $mime = $mtch[3];
1269
1270                 $the_url = Contact::magicLinkById($item['author-id'], $mtch[1]);
1271
1272                 if (strpos($mime, 'video') !== false) {
1273                         if (!$vhead) {
1274                                 $vhead = true;
1275                                 $a->page['htmlhead'] .= replace_macros(get_markup_template('videos_head.tpl'), [
1276                                         '$baseurl' => System::baseUrl(),
1277                                 ]);
1278                                 $a->page['end'] .= replace_macros(get_markup_template('videos_end.tpl'), [
1279                                         '$baseurl' => System::baseUrl(),
1280                                 ]);
1281                         }
1282
1283                         $id = end(explode('/', $the_url));
1284                         $as .= replace_macros(get_markup_template('video_top.tpl'), [
1285                                 '$video' => [
1286                                         'id'     => $id,
1287                                         'title'  => L10n::t('View Video'),
1288                                         'src'    => $the_url,
1289                                         'mime'   => $mime,
1290                                 ],
1291                         ]);
1292                 }
1293
1294                 $filetype = strtolower(substr($mime, 0, strpos($mime, '/')));
1295                 if ($filetype) {
1296                         $filesubtype = strtolower(substr($mime, strpos($mime, '/') + 1));
1297                         $filesubtype = str_replace('.', '-', $filesubtype);
1298                 } else {
1299                         $filetype = 'unkn';
1300                         $filesubtype = 'unkn';
1301                 }
1302
1303                 $title = escape_tags(trim(!empty($mtch[4]) ? $mtch[4] : $mtch[1]));
1304                 $title .= ' ' . $mtch[2] . ' ' . L10n::t('bytes');
1305
1306                 $icon = '<div class="attachtype icon s22 type-' . $filetype . ' subtype-' . $filesubtype . '"></div>';
1307                 $as .= '<a href="' . strip_tags($the_url) . '" title="' . $title . '" class="attachlink" target="_blank" >' . $icon . '</a>';
1308         }
1309
1310         if ($as != '') {
1311                 $s .= '<div class="body-attach">'.$as.'<div class="clear"></div></div>';
1312         }
1313
1314         // Map.
1315         if (strpos($s, '<div class="map">') !== false && x($item, 'coord')) {
1316                 $x = Map::byCoordinates(trim($item['coord']));
1317                 if ($x) {
1318                         $s = preg_replace('/\<div class\=\"map\"\>/', '$0' . $x, $s);
1319                 }
1320         }
1321
1322
1323         // Look for spoiler.
1324         $spoilersearch = '<blockquote class="spoiler">';
1325
1326         // Remove line breaks before the spoiler.
1327         while ((strpos($s, "\n" . $spoilersearch) !== false)) {
1328                 $s = str_replace("\n" . $spoilersearch, $spoilersearch, $s);
1329         }
1330         while ((strpos($s, "<br />" . $spoilersearch) !== false)) {
1331                 $s = str_replace("<br />" . $spoilersearch, $spoilersearch, $s);
1332         }
1333
1334         while ((strpos($s, $spoilersearch) !== false)) {
1335                 $pos = strpos($s, $spoilersearch);
1336                 $rnd = random_string(8);
1337                 $spoilerreplace = '<br /> <span id="spoiler-wrap-' . $rnd . '" class="spoiler-wrap fakelink" onclick="openClose(\'spoiler-' . $rnd . '\');">' . L10n::t('Click to open/close') . '</span>'.
1338                                         '<blockquote class="spoiler" id="spoiler-' . $rnd . '" style="display: none;">';
1339                 $s = substr($s, 0, $pos) . $spoilerreplace . substr($s, $pos + strlen($spoilersearch));
1340         }
1341
1342         // Look for quote with author.
1343         $authorsearch = '<blockquote class="author">';
1344
1345         while ((strpos($s, $authorsearch) !== false)) {
1346                 $pos = strpos($s, $authorsearch);
1347                 $rnd = random_string(8);
1348                 $authorreplace = '<br /> <span id="author-wrap-' . $rnd . '" class="author-wrap fakelink" onclick="openClose(\'author-' . $rnd . '\');">' . L10n::t('Click to open/close') . '</span>'.
1349                                         '<blockquote class="author" id="author-' . $rnd . '" style="display: block;">';
1350                 $s = substr($s, 0, $pos) . $authorreplace . substr($s, $pos + strlen($authorsearch));
1351         }
1352
1353         // Replace friendica image url size with theme preference.
1354         if (x($a->theme_info, 'item_image_size')){
1355                 $ps = $a->theme_info['item_image_size'];
1356                 $s = preg_replace('|(<img[^>]+src="[^"]+/photo/[0-9a-f]+)-[0-9]|', "$1-" . $ps, $s);
1357         }
1358
1359         $s = apply_content_filter($s, $filter_reasons);
1360
1361         $hook_data = ['item' => $item, 'html' => $s];
1362         Addon::callHooks('prepare_body_final', $hook_data);
1363
1364         return $hook_data['html'];
1365 }
1366
1367 /**
1368  * Given a HTML text and a set of filtering reasons, adds a content hiding header with the provided reasons
1369  *
1370  * Reasons are expected to have been translated already.
1371  *
1372  * @param string $html
1373  * @param array  $reasons
1374  * @return string
1375  */
1376 function apply_content_filter($html, array $reasons)
1377 {
1378         if (count($reasons)) {
1379                 $tpl = get_markup_template('wall/content_filter.tpl');
1380                 $html = replace_macros($tpl, [
1381                         '$reasons'   => $reasons,
1382                         '$rnd'       => random_string(8),
1383                         '$openclose' => L10n::t('Click to open/close'),
1384                         '$html'      => $html
1385                 ]);
1386         }
1387
1388         return $html;
1389 }
1390
1391 /**
1392  * @brief Given a text string, convert from bbcode to html and add smilie icons.
1393  *
1394  * @param string $text String with bbcode.
1395  * @return string Formattet HTML.
1396  */
1397 function prepare_text($text) {
1398         if (stristr($text, '[nosmile]')) {
1399                 $s = BBCode::convert($text);
1400         } else {
1401                 $s = Smilies::replace(BBCode::convert($text));
1402         }
1403
1404         return trim($s);
1405 }
1406
1407 /**
1408  * return array with details for categories and folders for an item
1409  *
1410  * @param array $item
1411  * @return array
1412  *
1413   * [
1414  *      [ // categories array
1415  *          {
1416  *               'name': 'category name',
1417  *               'removeurl': 'url to remove this category',
1418  *               'first': 'is the first in this array? true/false',
1419  *               'last': 'is the last in this array? true/false',
1420  *           } ,
1421  *           ....
1422  *       ],
1423  *       [ //folders array
1424  *                      {
1425  *               'name': 'folder name',
1426  *               'removeurl': 'url to remove this folder',
1427  *               'first': 'is the first in this array? true/false',
1428  *               'last': 'is the last in this array? true/false',
1429  *           } ,
1430  *           ....
1431  *       ]
1432  *  ]
1433  */
1434 function get_cats_and_terms($item)
1435 {
1436         $categories = [];
1437         $folders = [];
1438
1439         $matches = false;
1440         $first = true;
1441         $cnt = preg_match_all('/<(.*?)>/', $item['file'], $matches, PREG_SET_ORDER);
1442         if ($cnt) {
1443                 foreach ($matches as $mtch) {
1444                         $categories[] = [
1445                                 'name' => xmlify(file_tag_decode($mtch[1])),
1446                                 'url' =>  "#",
1447                                 'removeurl' => ((local_user() == $item['uid'])?'filerm/' . $item['id'] . '?f=&cat=' . xmlify(file_tag_decode($mtch[1])):""),
1448                                 'first' => $first,
1449                                 'last' => false
1450                         ];
1451                         $first = false;
1452                 }
1453         }
1454
1455         if (count($categories)) {
1456                 $categories[count($categories) - 1]['last'] = true;
1457         }
1458
1459         if (local_user() == $item['uid']) {
1460                 $matches = false;
1461                 $first = true;
1462                 $cnt = preg_match_all('/\[(.*?)\]/', $item['file'], $matches, PREG_SET_ORDER);
1463                 if ($cnt) {
1464                         foreach ($matches as $mtch) {
1465                                 $folders[] = [
1466                                         'name' => xmlify(file_tag_decode($mtch[1])),
1467                                         'url' =>  "#",
1468                                         'removeurl' => ((local_user() == $item['uid']) ? 'filerm/' . $item['id'] . '?f=&term=' . xmlify(file_tag_decode($mtch[1])) : ""),
1469                                         'first' => $first,
1470                                         'last' => false
1471                                 ];
1472                                 $first = false;
1473                         }
1474                 }
1475         }
1476
1477         if (count($folders)) {
1478                 $folders[count($folders) - 1]['last'] = true;
1479         }
1480
1481         return [$categories, $folders];
1482 }
1483
1484
1485 /**
1486  * get private link for item
1487  * @param array $item
1488  * @return boolean|array False if item has not plink, otherwise array('href'=>plink url, 'title'=>translated title)
1489  */
1490 function get_plink($item) {
1491         $a = get_app();
1492
1493         if ($a->user['nickname'] != "") {
1494                 $ret = [
1495                                 //'href' => "display/" . $a->user['nickname'] . "/" . $item['id'],
1496                                 'href' => "display/" . $item['guid'],
1497                                 'orig' => "display/" . $item['guid'],
1498                                 'title' => L10n::t('View on separate page'),
1499                                 'orig_title' => L10n::t('view on separate page'),
1500                         ];
1501
1502                 if (x($item, 'plink')) {
1503                         $ret["href"] = $a->remove_baseurl($item['plink']);
1504                         $ret["title"] = L10n::t('link to source');
1505                 }
1506
1507         } elseif (x($item, 'plink') && ($item['private'] != 1)) {
1508                 $ret = [
1509                                 'href' => $item['plink'],
1510                                 'orig' => $item['plink'],
1511                                 'title' => L10n::t('link to source'),
1512                         ];
1513         } else {
1514                 $ret = [];
1515         }
1516
1517         return $ret;
1518 }
1519
1520
1521 /**
1522  * replace html amp entity with amp char
1523  * @param string $s
1524  * @return string
1525  */
1526 function unamp($s) {
1527         return str_replace('&amp;', '&', $s);
1528 }
1529
1530
1531 /**
1532  * return number of bytes in size (K, M, G)
1533  * @param string $size_str
1534  * @return number
1535  */
1536 function return_bytes($size_str) {
1537         switch (substr ($size_str, -1)) {
1538                 case 'M': case 'm': return (int)$size_str * 1048576;
1539                 case 'K': case 'k': return (int)$size_str * 1024;
1540                 case 'G': case 'g': return (int)$size_str * 1073741824;
1541                 default: return $size_str;
1542         }
1543 }
1544
1545
1546 /**
1547  * @return string
1548  */
1549 function generate_user_guid() {
1550         $found = true;
1551         do {
1552                 $guid = get_guid(32);
1553                 $x = q("SELECT `uid` FROM `user` WHERE `guid` = '%s' LIMIT 1",
1554                         dbesc($guid)
1555                 );
1556                 if (!DBM::is_result($x)) {
1557                         $found = false;
1558                 }
1559         } while ($found == true);
1560
1561         return $guid;
1562 }
1563
1564
1565 /**
1566  * @param string $s
1567  * @param boolean $strip_padding
1568  * @return string
1569  */
1570 function base64url_encode($s, $strip_padding = false) {
1571
1572         $s = strtr(base64_encode($s), '+/', '-_');
1573
1574         if ($strip_padding) {
1575                 $s = str_replace('=','',$s);
1576         }
1577
1578         return $s;
1579 }
1580
1581 /**
1582  * @param string $s
1583  * @return string
1584  */
1585 function base64url_decode($s) {
1586
1587         if (is_array($s)) {
1588                 logger('base64url_decode: illegal input: ' . print_r(debug_backtrace(), true));
1589                 return $s;
1590         }
1591
1592 /*
1593  *  // Placeholder for new rev of salmon which strips base64 padding.
1594  *  // PHP base64_decode handles the un-padded input without requiring this step
1595  *  // Uncomment if you find you need it.
1596  *
1597  *      $l = strlen($s);
1598  *      if (!strpos($s,'=')) {
1599  *              $m = $l % 4;
1600  *              if ($m == 2)
1601  *                      $s .= '==';
1602  *              if ($m == 3)
1603  *                      $s .= '=';
1604  *      }
1605  *
1606  */
1607
1608         return base64_decode(strtr($s,'-_','+/'));
1609 }
1610
1611
1612 /**
1613  * return div element with class 'clear'
1614  * @return string
1615  * @deprecated
1616  */
1617 function cleardiv() {
1618         return '<div class="clear"></div>';
1619 }
1620
1621
1622 function bb_translate_video($s) {
1623
1624         $matches = null;
1625         $r = preg_match_all("/\[video\](.*?)\[\/video\]/ism",$s,$matches,PREG_SET_ORDER);
1626         if ($r) {
1627                 foreach ($matches as $mtch) {
1628                         if ((stristr($mtch[1], 'youtube')) || (stristr($mtch[1], 'youtu.be'))) {
1629                                 $s = str_replace($mtch[0], '[youtube]' . $mtch[1] . '[/youtube]', $s);
1630                         } elseif (stristr($mtch[1], 'vimeo')) {
1631                                 $s = str_replace($mtch[0], '[vimeo]' . $mtch[1] . '[/vimeo]', $s);
1632                         }
1633                 }
1634         }
1635         return $s;
1636 }
1637
1638 function html2bb_video($s) {
1639
1640         $s = preg_replace('#<object[^>]+>(.*?)https?://www.youtube.com/((?:v|cp)/[A-Za-z0-9\-_=]+)(.*?)</object>#ism',
1641                         '[youtube]$2[/youtube]', $s);
1642
1643         $s = preg_replace('#<iframe[^>](.*?)https?://www.youtube.com/embed/([A-Za-z0-9\-_=]+)(.*?)</iframe>#ism',
1644                         '[youtube]$2[/youtube]', $s);
1645
1646         $s = preg_replace('#<iframe[^>](.*?)https?://player.vimeo.com/video/([0-9]+)(.*?)</iframe>#ism',
1647                         '[vimeo]$2[/vimeo]', $s);
1648
1649         return $s;
1650 }
1651
1652 /**
1653  * apply xmlify() to all values of array $val, recursively
1654  * @param array $val
1655  * @return array
1656  */
1657 function array_xmlify($val){
1658         if (is_bool($val)) {
1659                 return $val?"true":"false";
1660         } elseif (is_array($val)) {
1661                 return array_map('array_xmlify', $val);
1662         }
1663         return xmlify((string) $val);
1664 }
1665
1666
1667 /**
1668  * transform link href and img src from relative to absolute
1669  *
1670  * @param string $text
1671  * @param string $base base url
1672  * @return string
1673  */
1674 function reltoabs($text, $base) {
1675         if (empty($base)) {
1676                 return $text;
1677         }
1678
1679         $base = rtrim($base,'/');
1680
1681         $base2 = $base . "/";
1682
1683         // Replace links
1684         $pattern = "/<a([^>]*) href=\"(?!http|https|\/)([^\"]*)\"/";
1685         $replace = "<a\${1} href=\"" . $base2 . "\${2}\"";
1686         $text = preg_replace($pattern, $replace, $text);
1687
1688         $pattern = "/<a([^>]*) href=\"(?!http|https)([^\"]*)\"/";
1689         $replace = "<a\${1} href=\"" . $base . "\${2}\"";
1690         $text = preg_replace($pattern, $replace, $text);
1691
1692         // Replace images
1693         $pattern = "/<img([^>]*) src=\"(?!http|https|\/)([^\"]*)\"/";
1694         $replace = "<img\${1} src=\"" . $base2 . "\${2}\"";
1695         $text = preg_replace($pattern, $replace, $text);
1696
1697         $pattern = "/<img([^>]*) src=\"(?!http|https)([^\"]*)\"/";
1698         $replace = "<img\${1} src=\"" . $base . "\${2}\"";
1699         $text = preg_replace($pattern, $replace, $text);
1700
1701
1702         // Done
1703         return $text;
1704 }
1705
1706 /**
1707  * get translated item type
1708  *
1709  * @param array $itme
1710  * @return string
1711  */
1712 function item_post_type($item) {
1713         if (intval($item['event-id'])) {
1714                 return L10n::t('event');
1715         } elseif (strlen($item['resource-id'])) {
1716                 return L10n::t('photo');
1717         } elseif (strlen($item['verb']) && $item['verb'] !== ACTIVITY_POST) {
1718                 return L10n::t('activity');
1719         } elseif ($item['id'] != $item['parent']) {
1720                 return L10n::t('comment');
1721         }
1722
1723         return L10n::t('post');
1724 }
1725
1726 // post categories and "save to file" use the same item.file table for storage.
1727 // We will differentiate the different uses by wrapping categories in angle brackets
1728 // and save to file categories in square brackets.
1729 // To do this we need to escape these characters if they appear in our tag.
1730
1731 function file_tag_encode($s) {
1732         return str_replace(['<','>','[',']'],['%3c','%3e','%5b','%5d'],$s);
1733 }
1734
1735 function file_tag_decode($s) {
1736         return str_replace(['%3c', '%3e', '%5b', '%5d'], ['<', '>', '[', ']'], $s);
1737 }
1738
1739 function file_tag_file_query($table,$s,$type = 'file') {
1740
1741         if ($type == 'file') {
1742                 $str = preg_quote('[' . str_replace('%', '%%', file_tag_encode($s)) . ']');
1743         } else {
1744                 $str = preg_quote('<' . str_replace('%', '%%', file_tag_encode($s)) . '>');
1745         }
1746         return " AND " . (($table) ? dbesc($table) . '.' : '') . "file regexp '" . dbesc($str) . "' ";
1747 }
1748
1749 // ex. given music,video return <music><video> or [music][video]
1750 function file_tag_list_to_file($list, $type = 'file') {
1751         $tag_list = '';
1752         if (strlen($list)) {
1753                 $list_array = explode(",",$list);
1754                 if ($type == 'file') {
1755                         $lbracket = '[';
1756                         $rbracket = ']';
1757                 } else {
1758                         $lbracket = '<';
1759                         $rbracket = '>';
1760                 }
1761
1762                 foreach ($list_array as $item) {
1763                         if (strlen($item)) {
1764                                 $tag_list .= $lbracket . file_tag_encode(trim($item))  . $rbracket;
1765                         }
1766                 }
1767         }
1768         return $tag_list;
1769 }
1770
1771 // ex. given <music><video>[friends], return music,video or friends
1772 function file_tag_file_to_list($file, $type = 'file') {
1773         $matches = false;
1774         $list = '';
1775         if ($type == 'file') {
1776                 $cnt = preg_match_all('/\[(.*?)\]/', $file, $matches, PREG_SET_ORDER);
1777         } else {
1778                 $cnt = preg_match_all('/<(.*?)>/', $file, $matches, PREG_SET_ORDER);
1779         }
1780         if ($cnt) {
1781                 foreach ($matches as $mtch) {
1782                         if (strlen($list)) {
1783                                 $list .= ',';
1784                         }
1785                         $list .= file_tag_decode($mtch[1]);
1786                 }
1787         }
1788
1789         return $list;
1790 }
1791
1792 function file_tag_update_pconfig($uid, $file_old, $file_new, $type = 'file') {
1793         // $file_old - categories previously associated with an item
1794         // $file_new - new list of categories for an item
1795
1796         if (!intval($uid)) {
1797                 return false;
1798         } elseif ($file_old == $file_new) {
1799                 return true;
1800         }
1801
1802         $saved = PConfig::get($uid, 'system', 'filetags');
1803         if (strlen($saved)) {
1804                 if ($type == 'file') {
1805                         $lbracket = '[';
1806                         $rbracket = ']';
1807                         $termtype = TERM_FILE;
1808                 } else {
1809                         $lbracket = '<';
1810                         $rbracket = '>';
1811                         $termtype = TERM_CATEGORY;
1812                 }
1813
1814                 $filetags_updated = $saved;
1815
1816                 // check for new tags to be added as filetags in pconfig
1817                 $new_tags = [];
1818                 $check_new_tags = explode(",",file_tag_file_to_list($file_new,$type));
1819
1820                 foreach ($check_new_tags as $tag) {
1821                         if (!stristr($saved,$lbracket . file_tag_encode($tag) . $rbracket)) {
1822                                 $new_tags[] = $tag;
1823                         }
1824                 }
1825
1826                 $filetags_updated .= file_tag_list_to_file(implode(",",$new_tags),$type);
1827
1828                 // check for deleted tags to be removed from filetags in pconfig
1829                 $deleted_tags = [];
1830                 $check_deleted_tags = explode(",",file_tag_file_to_list($file_old,$type));
1831
1832                 foreach ($check_deleted_tags as $tag) {
1833                         if (!stristr($file_new,$lbracket . file_tag_encode($tag) . $rbracket)) {
1834                                 $deleted_tags[] = $tag;
1835                         }
1836                 }
1837
1838                 foreach ($deleted_tags as $key => $tag) {
1839                         $r = q("SELECT `oid` FROM `term` WHERE `term` = '%s' AND `otype` = %d AND `type` = %d AND `uid` = %d",
1840                                 dbesc($tag),
1841                                 intval(TERM_OBJ_POST),
1842                                 intval($termtype),
1843                                 intval($uid));
1844
1845                         if (DBM::is_result($r)) {
1846                                 unset($deleted_tags[$key]);
1847                         } else {
1848                                 $filetags_updated = str_replace($lbracket . file_tag_encode($tag) . $rbracket,'',$filetags_updated);
1849                         }
1850                 }
1851
1852                 if ($saved != $filetags_updated) {
1853                         PConfig::set($uid, 'system', 'filetags', $filetags_updated);
1854                 }
1855                 return true;
1856         } elseif (strlen($file_new)) {
1857                 PConfig::set($uid, 'system', 'filetags', $file_new);
1858         }
1859         return true;
1860 }
1861
1862 function file_tag_save_file($uid, $item_id, $file)
1863 {
1864         if (!intval($uid)) {
1865                 return false;
1866         }
1867
1868         $item = Item::selectFirst(['file'], ['id' => $item_id, 'uid' => $uid]);
1869         if (DBM::is_result($item)) {
1870                 if (!stristr($item['file'],'[' . file_tag_encode($file) . ']')) {
1871                         $fields = ['file' => $item['file'] . '[' . file_tag_encode($file) . ']'];
1872                         Item::update($fields, ['id' => $item_id]);
1873                 }
1874                 $saved = PConfig::get($uid, 'system', 'filetags');
1875                 if (!strlen($saved) || !stristr($saved, '[' . file_tag_encode($file) . ']')) {
1876                         PConfig::set($uid, 'system', 'filetags', $saved . '[' . file_tag_encode($file) . ']');
1877                 }
1878                 info(L10n::t('Item filed'));
1879         }
1880         return true;
1881 }
1882
1883 function file_tag_unsave_file($uid, $item_id, $file, $cat = false)
1884 {
1885         if (!intval($uid)) {
1886                 return false;
1887         }
1888
1889         if ($cat == true) {
1890                 $pattern = '<' . file_tag_encode($file) . '>' ;
1891                 $termtype = TERM_CATEGORY;
1892         } else {
1893                 $pattern = '[' . file_tag_encode($file) . ']' ;
1894                 $termtype = TERM_FILE;
1895         }
1896
1897         $item = Item::selectFirst(['file'], ['id' => $item_id, 'uid' => $uid]);
1898         if (!DBM::is_result($item)) {
1899                 return false;
1900         }
1901
1902         $fields = ['file' => str_replace($pattern,'',$item['file'])];
1903         Item::update($fields, ['id' => $item_id]);
1904
1905         $r = q("SELECT `oid` FROM `term` WHERE `term` = '%s' AND `otype` = %d AND `type` = %d AND `uid` = %d",
1906                 dbesc($file),
1907                 intval(TERM_OBJ_POST),
1908                 intval($termtype),
1909                 intval($uid)
1910         );
1911         if (!DBM::is_result($r)) {
1912                 $saved = PConfig::get($uid, 'system', 'filetags');
1913                 PConfig::set($uid, 'system', 'filetags', str_replace($pattern, '', $saved));
1914         }
1915
1916         return true;
1917 }
1918
1919 function normalise_openid($s) {
1920         return trim(str_replace(['http://', 'https://'], ['', ''], $s), '/');
1921 }
1922
1923
1924 function undo_post_tagging($s) {
1925         $matches = null;
1926         $cnt = preg_match_all('/([!#@])\[url=(.*?)\](.*?)\[\/url\]/ism', $s, $matches, PREG_SET_ORDER);
1927         if ($cnt) {
1928                 foreach ($matches as $mtch) {
1929                         if (in_array($mtch[1], ['!', '@'])) {
1930                                 $contact = Contact::getDetailsByURL($mtch[2]);
1931                                 $mtch[3] = empty($contact['addr']) ? $mtch[2] : $contact['addr'];
1932                         }
1933                         $s = str_replace($mtch[0], $mtch[1] . $mtch[3],$s);
1934                 }
1935         }
1936         return $s;
1937 }
1938
1939 function protect_sprintf($s) {
1940         return str_replace('%', '%%', $s);
1941 }
1942
1943 /// @TODO Rewrite this
1944 function is_a_date_arg($s) {
1945         $i = intval($s);
1946
1947         if ($i > 1900) {
1948                 $y = date('Y');
1949
1950                 if ($i <= $y + 1 && strpos($s, '-') == 4) {
1951                         $m = intval(substr($s, 5));
1952
1953                         if ($m > 0 && $m <= 12) {
1954                                 return true;
1955                         }
1956                 }
1957         }
1958
1959         return false;
1960 }
1961
1962 /**
1963  * remove intentation from a text
1964  */
1965 function deindent($text, $chr = "[\t ]", $count = NULL) {
1966         $lines = explode("\n", $text);
1967
1968         if (is_null($count)) {
1969                 $m = [];
1970                 $k = 0;
1971                 while ($k < count($lines) && strlen($lines[$k]) == 0) {
1972                         $k++;
1973                 }
1974                 preg_match("|^" . $chr . "*|", $lines[$k], $m);
1975                 $count = strlen($m[0]);
1976         }
1977
1978         for ($k = 0; $k < count($lines); $k++) {
1979                 $lines[$k] = preg_replace("|^" . $chr . "{" . $count . "}|", "", $lines[$k]);
1980         }
1981
1982         return implode("\n", $lines);
1983 }
1984
1985 function formatBytes($bytes, $precision = 2) {
1986         $units = ['B', 'KB', 'MB', 'GB', 'TB'];
1987
1988         $bytes = max($bytes, 0);
1989         $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
1990         $pow = min($pow, count($units) - 1);
1991
1992         $bytes /= pow(1024, $pow);
1993
1994         return round($bytes, $precision) . ' ' . $units[$pow];
1995 }
1996
1997 /**
1998  * @brief translate and format the networkname of a contact
1999  *
2000  * @param string $network
2001  *      Networkname of the contact (e.g. dfrn, rss and so on)
2002  * @param sting $url
2003  *      The contact url
2004  * @return string
2005  */
2006 function format_network_name($network, $url = 0) {
2007         if ($network != "") {
2008                 if ($url != "") {
2009                         $network_name = '<a href="'.$url.'">'.ContactSelector::networkToName($network, $url)."</a>";
2010                 } else {
2011                         $network_name = ContactSelector::networkToName($network);
2012                 }
2013
2014                 return $network_name;
2015         }
2016 }
2017
2018 /**
2019  * @brief Syntax based code highlighting for popular languages.
2020  * @param string $s Code block
2021  * @param string $lang Programming language
2022  * @return string Formated html
2023  */
2024 function text_highlight($s, $lang) {
2025         if ($lang === 'js') {
2026                 $lang = 'javascript';
2027         }
2028
2029         if ($lang === 'bash') {
2030                 $lang = 'sh';
2031         }
2032
2033         // @TODO: Replace Text_Highlighter_Renderer_Html by scrivo/highlight.php
2034
2035         // Autoload the library to make constants available
2036         class_exists('Text_Highlighter_Renderer_Html');
2037
2038         $options = [
2039                 'numbers' => HL_NUMBERS_LI,
2040                 'tabsize' => 4,
2041         ];
2042
2043         $tag_added = false;
2044         $s = trim(html_entity_decode($s, ENT_COMPAT));
2045         $s = str_replace('    ', "\t", $s);
2046
2047         /*
2048          * The highlighter library insists on an opening php tag for php code blocks. If
2049          * it isn't present, nothing is highlighted. So we're going to see if it's present.
2050          * If not, we'll add it, and then quietly remove it after we get the processed output back.
2051          */
2052         if ($lang === 'php' && strpos($s, '<?php') !== 0) {
2053                 $s = '<?php' . "\n" . $s;
2054                 $tag_added = true;
2055         }
2056
2057         $renderer = new Text_Highlighter_Renderer_Html($options);
2058         $factory = new Text_Highlighter();
2059         $hl = $factory->factory($lang);
2060         $hl->setRenderer($renderer);
2061         $o = $hl->highlight($s);
2062         $o = str_replace("\n", '', $o);
2063
2064         if ($tag_added) {
2065                 $b = substr($o, 0, strpos($o, '<li>'));
2066                 $e = substr($o, strpos($o, '</li>'));
2067                 $o = $b . $e;
2068         }
2069
2070         return '<code>' . $o . '</code>';
2071 }