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