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