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