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