]> git.mxchange.org Git - friendica.git/blob - include/text.php
Commented out the functionality
[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
13 require_once "include/friendica_smarty.php";
14 require_once "include/map.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  * Check if $x is a valid email string
1072  *
1073  * @param string $x
1074  * @return boolean
1075  */
1076 function valid_email($x){
1077
1078         /// @TODO Removed because Fabio told me so.
1079         //if (Config::get('system','disable_email_validation'))
1080         //      return true;
1081         return preg_match('/^[_a-zA-Z0-9\-\+]+(\.[_a-zA-Z0-9\-\+]+)*@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+$/', $x);
1082 }
1083
1084
1085 /**
1086  * Replace naked text hyperlink with HTML formatted hyperlink
1087  *
1088  * @param string $s
1089  */
1090 function linkify($s) {
1091         $s = preg_replace("/(https?\:\/\/[a-zA-Z0-9\:\/\-\?\&\;\.\=\_\~\#\'\%\$\!\+]*)/", ' <a href="$1" target="_blank">$1</a>', $s);
1092         $s = preg_replace("/\<(.*?)(src|href)=(.*?)\&amp\;(.*?)\>/ism",'<$1$2=$3&$4>',$s);
1093         return $s;
1094 }
1095
1096
1097 /**
1098  * Load poke verbs
1099  *
1100  * @return array index is present tense verb
1101                                  value is array containing past tense verb, translation of present, translation of past
1102  * @hook poke_verbs pokes array
1103  */
1104 function get_poke_verbs() {
1105
1106         // index is present tense verb
1107         // value is array containing past tense verb, translation of present, translation of past
1108
1109         $arr = array(
1110                 'poke' => array('poked', t('poke'), t('poked')),
1111                 'ping' => array('pinged', t('ping'), t('pinged')),
1112                 'prod' => array('prodded', t('prod'), t('prodded')),
1113                 'slap' => array('slapped', t('slap'), t('slapped')),
1114                 'finger' => array('fingered', t('finger'), t('fingered')),
1115                 'rebuff' => array('rebuffed', t('rebuff'), t('rebuffed')),
1116         );
1117         call_hooks('poke_verbs', $arr);
1118         return $arr;
1119 }
1120
1121 /**
1122  * @brief Translate days and months names.
1123  *
1124  * @param string $s String with day or month name.
1125  * @return string Translated string.
1126  */
1127 function day_translate($s) {
1128         $ret = str_replace(array('Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'),
1129                 array(t('Monday'), t('Tuesday'), t('Wednesday'), t('Thursday'), t('Friday'), t('Saturday'), t('Sunday')),
1130                 $s);
1131
1132         $ret = str_replace(array('January','February','March','April','May','June','July','August','September','October','November','December'),
1133                 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')),
1134                 $ret);
1135
1136         return $ret;
1137 }
1138
1139 /**
1140  * @brief Translate short days and months names.
1141  *
1142  * @param string $s String with short day or month name.
1143  * @return string Translated string.
1144  */
1145 function day_short_translate($s) {
1146         $ret = str_replace(array('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'),
1147                 array(t('Mon'), t('Tue'), t('Wed'), t('Thu'), t('Fri'), t('Sat'), t('Sun')),
1148                 $s);
1149         $ret = str_replace(array('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov','Dec'),
1150                 array(t('Jan'), t('Feb'), t('Mar'), t('Apr'), t('May'), ('Jun'), t('Jul'), t('Aug'), t('Sep'), t('Oct'), t('Nov'), t('Dec')),
1151                 $ret);
1152         return $ret;
1153 }
1154
1155
1156 /**
1157  * Normalize url
1158  *
1159  * @param string $url
1160  * @return string
1161  */
1162 function normalise_link($url) {
1163         $ret = str_replace(array('https:', '//www.'), array('http:', '//'), $url);
1164         return rtrim($ret,'/');
1165 }
1166
1167
1168 /**
1169  * Compare two URLs to see if they are the same, but ignore
1170  * slight but hopefully insignificant differences such as if one
1171  * is https and the other isn't, or if one is www.something and
1172  * the other isn't - and also ignore case differences.
1173  *
1174  * @param string $a first url
1175  * @param string $b second url
1176  * @return boolean True if the URLs match, otherwise False
1177  *
1178  */
1179 function link_compare($a, $b) {
1180         return (strcasecmp(normalise_link($a), normalise_link($b)) === 0);
1181 }
1182
1183
1184 /**
1185  * @brief Find any non-embedded images in private items and add redir links to them
1186  *
1187  * @param App $a
1188  * @param array &$item The field array of an item row
1189  */
1190 function redir_private_images($a, &$item)
1191 {
1192         $matches = false;
1193         $cnt = preg_match_all('|\[img\](http[^\[]*?/photo/[a-fA-F0-9]+?(-[0-9]\.[\w]+?)?)\[\/img\]|', $item['body'], $matches, PREG_SET_ORDER);
1194         if ($cnt) {
1195                 foreach ($matches as $mtch) {
1196                         if (strpos($mtch[1], '/redir') !== false) {
1197                                 continue;
1198                         }
1199
1200                         if ((local_user() == $item['uid']) && ($item['private'] != 0) && ($item['contact-id'] != $a->contact['id']) && ($item['network'] == NETWORK_DFRN)) {
1201                                 $img_url = 'redir?f=1&quiet=1&url=' . urlencode($mtch[1]) . '&conurl=' . urlencode($item['author-link']);
1202                                 $item['body'] = str_replace($mtch[0], '[img]' . $img_url . '[/img]', $item['body']);
1203                         }
1204                 }
1205         }
1206 }
1207
1208 function put_item_in_cache(&$item, $update = false) {
1209
1210         if (($item["rendered-hash"] != hash("md5", $item["body"])) || ($item["rendered-hash"] == "") ||
1211                 ($item["rendered-html"] == "") || Config::get("system", "ignore_cache")) {
1212
1213                 // The function "redir_private_images" changes the body.
1214                 // I'm not sure if we should store it permanently, so we save the old value.
1215                 $body = $item["body"];
1216
1217                 $a = get_app();
1218                 redir_private_images($a, $item);
1219
1220                 $item["rendered-html"] = prepare_text($item["body"]);
1221                 $item["rendered-hash"] = hash("md5", $item["body"]);
1222                 $item["body"] = $body;
1223
1224                 if ($update && ($item["id"] > 0)) {
1225                         dba::update('item', array('rendered-html' => $item["rendered-html"], 'rendered-hash' => $item["rendered-hash"]),
1226                                         array('id' => $item["id"]), false);
1227                 }
1228         }
1229 }
1230
1231 /**
1232  * @brief Given an item array, convert the body element from bbcode to html and add smilie icons.
1233  * If attach is true, also add icons for item attachments.
1234  *
1235  * @param array $item
1236  * @param boolean $attach
1237  * @return string item body html
1238  * @hook prepare_body_init item array before any work
1239  * @hook prepare_body ('item'=>item array, 'html'=>body string) after first bbcode to html
1240  * @hook prepare_body_final ('item'=>item array, 'html'=>body string) after attach icons and blockquote special case handling (spoiler, author)
1241  */
1242 function prepare_body(&$item, $attach = false, $preview = false) {
1243
1244         $a = get_app();
1245         call_hooks('prepare_body_init', $item);
1246
1247         $searchpath = System::baseUrl() . "/search?tag=";
1248
1249         $tags = array();
1250         $hashtags = array();
1251         $mentions = array();
1252
1253         // In order to provide theme developers more possibilities, event items
1254         // are treated differently.
1255         if ($item['object-type'] === ACTIVITY_OBJ_EVENT && isset($item['event-id'])) {
1256                 $ev = format_event_item($item);
1257                 return $ev;
1258         }
1259
1260         if (!Config::get('system','suppress_tags')) {
1261                 $taglist = dba::p("SELECT `type`, `term`, `url` FROM `term` WHERE `otype` = ? AND `oid` = ? AND `type` IN (?, ?) ORDER BY `tid`",
1262                                 intval(TERM_OBJ_POST), intval($item['id']), intval(TERM_HASHTAG), intval(TERM_MENTION));
1263
1264                 while ($tag = dba::fetch($taglist)) {
1265                         if ($tag["url"] == "") {
1266                                 $tag["url"] = $searchpath.strtolower($tag["term"]);
1267                         }
1268
1269                         $orig_tag = $tag["url"];
1270
1271                         $tag["url"] = best_link_url($item, $sp, $tag["url"]);
1272
1273                         if ($tag["type"] == TERM_HASHTAG) {
1274                                 if ($orig_tag != $tag["url"]) {
1275                                         $item['body'] = str_replace($orig_tag, $tag["url"], $item['body']);
1276                                 }
1277                                 $hashtags[] = "#<a href=\"".$tag["url"]."\" target=\"_blank\">".$tag["term"]."</a>";
1278                                 $prefix = "#";
1279                         } elseif ($tag["type"] == TERM_MENTION) {
1280                                 $mentions[] = "@<a href=\"".$tag["url"]."\" target=\"_blank\">".$tag["term"]."</a>";
1281                                 $prefix = "@";
1282                         }
1283                         $tags[] = $prefix."<a href=\"".$tag["url"]."\" target=\"_blank\">".$tag["term"]."</a>";
1284                 }
1285                 dba::close($taglist);
1286         }
1287
1288         $item['tags'] = $tags;
1289         $item['hashtags'] = $hashtags;
1290         $item['mentions'] = $mentions;
1291
1292         // Update the cached values if there is no "zrl=..." on the links.
1293         $update = (!local_user() && !remote_user() && ($item["uid"] == 0));
1294
1295         // Or update it if the current viewer is the intented viewer.
1296         if (($item["uid"] == local_user()) && ($item["uid"] != 0)) {
1297                 $update = true;
1298         }
1299
1300         put_item_in_cache($item, $update);
1301         $s = $item["rendered-html"];
1302
1303         $prep_arr = array('item' => $item, 'html' => $s, 'preview' => $preview);
1304         call_hooks('prepare_body', $prep_arr);
1305         $s = $prep_arr['html'];
1306
1307         if (! $attach) {
1308                 // Replace the blockquotes with quotes that are used in mails.
1309                 $mailquote = '<blockquote type="cite" class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;">';
1310                 $s = str_replace(array('<blockquote>', '<blockquote class="spoiler">', '<blockquote class="author">'), array($mailquote, $mailquote, $mailquote), $s);
1311                 return $s;
1312         }
1313
1314         $as = '';
1315         $vhead = false;
1316         $arr = explode('[/attach],', $item['attach']);
1317         if (count($arr)) {
1318                 foreach ($arr as $r) {
1319                         $matches = false;
1320                         $icon = '';
1321                         $cnt = preg_match_all('|\[attach\]href=\"(.*?)\" length=\"(.*?)\" type=\"(.*?)\" title=\"(.*?)\"|',$r ,$matches, PREG_SET_ORDER);
1322                         if ($cnt) {
1323                                 foreach ($matches as $mtch) {
1324                                         $mime = $mtch[3];
1325
1326                                         if ((local_user() == $item['uid']) && ($item['contact-id'] != $a->contact['id']) && ($item['network'] == NETWORK_DFRN)) {
1327                                                 $the_url = 'redir/' . $item['contact-id'] . '?f=1&url=' . $mtch[1];
1328                                         } else {
1329                                                 $the_url = $mtch[1];
1330                                         }
1331
1332                                         if (strpos($mime, 'video') !== false) {
1333                                                 if (!$vhead) {
1334                                                         $vhead = true;
1335                                                         $a->page['htmlhead'] .= replace_macros(get_markup_template('videos_head.tpl'), array(
1336                                                                 '$baseurl' => System::baseUrl(),
1337                                                         ));
1338                                                         $a->page['end'] .= replace_macros(get_markup_template('videos_end.tpl'), array(
1339                                                                 '$baseurl' => System::baseUrl(),
1340                                                         ));
1341                                                 }
1342
1343                                                 $id = end(explode('/', $the_url));
1344                                                 $as .= replace_macros(get_markup_template('video_top.tpl'), array(
1345                                                         '$video' => array(
1346                                                                 'id'     => $id,
1347                                                                 'title'  => t('View Video'),
1348                                                                 'src'    => $the_url,
1349                                                                 'mime'   => $mime,
1350                                                         ),
1351                                                 ));
1352                                         }
1353
1354                                         $filetype = strtolower(substr($mime, 0, strpos($mime, '/')));
1355                                         if ($filetype) {
1356                                                 $filesubtype = strtolower(substr($mime, strpos($mime, '/') + 1));
1357                                                 $filesubtype = str_replace('.', '-', $filesubtype);
1358                                         } else {
1359                                                 $filetype = 'unkn';
1360                                                 $filesubtype = 'unkn';
1361                                         }
1362
1363                                         $title = ((strlen(trim($mtch[4]))) ? escape_tags(trim($mtch[4])) : escape_tags($mtch[1]));
1364                                         $title .= ' ' . $mtch[2] . ' ' . t('bytes');
1365
1366                                         $icon = '<div class="attachtype icon s22 type-' . $filetype . ' subtype-' . $filesubtype . '"></div>';
1367                                         $as .= '<a href="' . strip_tags($the_url) . '" title="' . $title . '" class="attachlink" target="_blank" >' . $icon . '</a>';
1368                                 }
1369                         }
1370                 }
1371         }
1372         if ($as != '') {
1373                 $s .= '<div class="body-attach">'.$as.'<div class="clear"></div></div>';
1374         }
1375
1376         // Map.
1377         if (strpos($s, '<div class="map">') !== false && x($item, 'coord')) {
1378                 $x = generate_map(trim($item['coord']));
1379                 if ($x) {
1380                         $s = preg_replace('/\<div class\=\"map\"\>/', '$0' . $x, $s);
1381                 }
1382         }
1383
1384
1385         // Look for spoiler.
1386         $spoilersearch = '<blockquote class="spoiler">';
1387
1388         // Remove line breaks before the spoiler.
1389         while ((strpos($s, "\n" . $spoilersearch) !== false)) {
1390                 $s = str_replace("\n" . $spoilersearch, $spoilersearch, $s);
1391         }
1392         while ((strpos($s, "<br />" . $spoilersearch) !== false)) {
1393                 $s = str_replace("<br />" . $spoilersearch, $spoilersearch, $s);
1394         }
1395
1396         while ((strpos($s, $spoilersearch) !== false)) {
1397                 $pos = strpos($s, $spoilersearch);
1398                 $rnd = random_string(8);
1399                 $spoilerreplace = '<br /> <span id="spoiler-wrap-' . $rnd . '" class="spoiler-wrap fakelink" onclick="openClose(\'spoiler-' . $rnd . '\');">' . sprintf(t('Click to open/close')) . '</span>'.
1400                                         '<blockquote class="spoiler" id="spoiler-' . $rnd . '" style="display: none;">';
1401                 $s = substr($s, 0, $pos) . $spoilerreplace . substr($s, $pos + strlen($spoilersearch));
1402         }
1403
1404         // Look for quote with author.
1405         $authorsearch = '<blockquote class="author">';
1406
1407         while ((strpos($s, $authorsearch) !== false)) {
1408                 $pos = strpos($s, $authorsearch);
1409                 $rnd = random_string(8);
1410                 $authorreplace = '<br /> <span id="author-wrap-' . $rnd . '" class="author-wrap fakelink" onclick="openClose(\'author-' . $rnd . '\');">' . sprintf(t('Click to open/close')) . '</span>'.
1411                                         '<blockquote class="author" id="author-' . $rnd . '" style="display: block;">';
1412                 $s = substr($s, 0, $pos) . $authorreplace . substr($s, $pos + strlen($authorsearch));
1413         }
1414
1415         // Replace friendica image url size with theme preference.
1416         if (x($a->theme_info, 'item_image_size')){
1417                 $ps = $a->theme_info['item_image_size'];
1418                 $s = preg_replace('|(<img[^>]+src="[^"]+/photo/[0-9a-f]+)-[0-9]|', "$1-" . $ps, $s);
1419         }
1420
1421         $prep_arr = array('item' => $item, 'html' => $s);
1422         call_hooks('prepare_body_final', $prep_arr);
1423
1424         return $prep_arr['html'];
1425 }
1426
1427 /**
1428  * @brief Given a text string, convert from bbcode to html and add smilie icons.
1429  *
1430  * @param string $text String with bbcode.
1431  * @return string Formattet HTML.
1432  */
1433 function prepare_text($text) {
1434
1435         require_once 'include/bbcode.php';
1436
1437         if (stristr($text, '[nosmile]')) {
1438                 $s = bbcode($text);
1439         } else {
1440                 $s = Smilies::replace(bbcode($text));
1441         }
1442
1443         return trim($s);
1444 }
1445
1446 /**
1447  * return array with details for categories and folders for an item
1448  *
1449  * @param array $item
1450  * @return array
1451  *
1452   * [
1453  *      [ // categories array
1454  *          {
1455  *               'name': 'category name',
1456  *               'removeurl': 'url to remove this category',
1457  *               'first': 'is the first in this array? true/false',
1458  *               'last': 'is the last in this array? true/false',
1459  *           } ,
1460  *           ....
1461  *       ],
1462  *       [ //folders array
1463  *                      {
1464  *               'name': 'folder name',
1465  *               'removeurl': 'url to remove this folder',
1466  *               'first': 'is the first in this array? true/false',
1467  *               'last': 'is the last in this array? true/false',
1468  *           } ,
1469  *           ....
1470  *       ]
1471  *  ]
1472  */
1473 function get_cats_and_terms($item) {
1474
1475         $a = get_app();
1476         $categories = array();
1477         $folders = array();
1478
1479         $matches = false;
1480         $first = true;
1481         $cnt = preg_match_all('/<(.*?)>/', $item['file'], $matches, PREG_SET_ORDER);
1482         if ($cnt) {
1483                 foreach ($matches as $mtch) {
1484                         $categories[] = array(
1485                                 'name' => xmlify(file_tag_decode($mtch[1])),
1486                                 'url' =>  "#",
1487                                 'removeurl' => ((local_user() == $item['uid'])?'filerm/' . $item['id'] . '?f=&cat=' . xmlify(file_tag_decode($mtch[1])):""),
1488                                 'first' => $first,
1489                                 'last' => false
1490                         );
1491                         $first = false;
1492                 }
1493         }
1494
1495         if (count($categories)) {
1496                 $categories[count($categories) - 1]['last'] = true;
1497         }
1498
1499         if (local_user() == $item['uid']) {
1500                 $matches = false;
1501                 $first = true;
1502                 $cnt = preg_match_all('/\[(.*?)\]/', $item['file'], $matches, PREG_SET_ORDER);
1503                 if ($cnt) {
1504                         foreach ($matches as $mtch) {
1505                                 $folders[] = array(
1506                                         'name' => xmlify(file_tag_decode($mtch[1])),
1507                                         'url' =>  "#",
1508                                         'removeurl' => ((local_user() == $item['uid']) ? 'filerm/' . $item['id'] . '?f=&term=' . xmlify(file_tag_decode($mtch[1])) : ""),
1509                                         'first' => $first,
1510                                         'last' => false
1511                                 );
1512                                 $first = false;
1513                         }
1514                 }
1515         }
1516
1517         if (count($folders)) {
1518                 $folders[count($folders) - 1]['last'] = true;
1519         }
1520
1521         return array($categories, $folders);
1522 }
1523
1524
1525 /**
1526  * get private link for item
1527  * @param array $item
1528  * @return boolean|array False if item has not plink, otherwise array('href'=>plink url, 'title'=>translated title)
1529  */
1530 function get_plink($item) {
1531         $a = get_app();
1532
1533         if ($a->user['nickname'] != "") {
1534                 $ret = array(
1535                                 //'href' => "display/" . $a->user['nickname'] . "/" . $item['id'],
1536                                 'href' => "display/" . $item['guid'],
1537                                 'orig' => "display/" . $item['guid'],
1538                                 'title' => t('View on separate page'),
1539                                 'orig_title' => t('view on separate page'),
1540                         );
1541
1542                 if (x($item, 'plink')) {
1543                         $ret["href"] = $a->remove_baseurl($item['plink']);
1544                         $ret["title"] = t('link to source');
1545                 }
1546
1547         } elseif (x($item, 'plink') && ($item['private'] != 1)) {
1548                 $ret = array(
1549                                 'href' => $item['plink'],
1550                                 'orig' => $item['plink'],
1551                                 'title' => t('link to source'),
1552                         );
1553         } else {
1554                 $ret = array();
1555         }
1556
1557         return $ret;
1558 }
1559
1560
1561 /**
1562  * replace html amp entity with amp char
1563  * @param string $s
1564  * @return string
1565  */
1566 function unamp($s) {
1567         return str_replace('&amp;', '&', $s);
1568 }
1569
1570
1571 /**
1572  * return number of bytes in size (K, M, G)
1573  * @param string $size_str
1574  * @return number
1575  */
1576 function return_bytes($size_str) {
1577         switch (substr ($size_str, -1)) {
1578                 case 'M': case 'm': return (int)$size_str * 1048576;
1579                 case 'K': case 'k': return (int)$size_str * 1024;
1580                 case 'G': case 'g': return (int)$size_str * 1073741824;
1581                 default: return $size_str;
1582         }
1583 }
1584
1585
1586 /**
1587  * @return string
1588  */
1589 function generate_user_guid() {
1590         $found = true;
1591         do {
1592                 $guid = get_guid(32);
1593                 $x = q("SELECT `uid` FROM `user` WHERE `guid` = '%s' LIMIT 1",
1594                         dbesc($guid)
1595                 );
1596                 if (! DBM::is_result($x)) {
1597                         $found = false;
1598                 }
1599         } while ($found == true);
1600
1601         return $guid;
1602 }
1603
1604
1605 /**
1606  * @param string $s
1607  * @param boolean $strip_padding
1608  * @return string
1609  */
1610 function base64url_encode($s, $strip_padding = false) {
1611
1612         $s = strtr(base64_encode($s), '+/', '-_');
1613
1614         if ($strip_padding) {
1615                 $s = str_replace('=','',$s);
1616         }
1617
1618         return $s;
1619 }
1620
1621 /**
1622  * @param string $s
1623  * @return string
1624  */
1625 function base64url_decode($s) {
1626
1627         if (is_array($s)) {
1628                 logger('base64url_decode: illegal input: ' . print_r(debug_backtrace(), true));
1629                 return $s;
1630         }
1631
1632 /*
1633  *  // Placeholder for new rev of salmon which strips base64 padding.
1634  *  // PHP base64_decode handles the un-padded input without requiring this step
1635  *  // Uncomment if you find you need it.
1636  *
1637  *      $l = strlen($s);
1638  *      if (! strpos($s,'=')) {
1639  *              $m = $l % 4;
1640  *              if ($m == 2)
1641  *                      $s .= '==';
1642  *              if ($m == 3)
1643  *                      $s .= '=';
1644  *      }
1645  *
1646  */
1647
1648         return base64_decode(strtr($s,'-_','+/'));
1649 }
1650
1651
1652 /**
1653  * return div element with class 'clear'
1654  * @return string
1655  * @deprecated
1656  */
1657 function cleardiv() {
1658         return '<div class="clear"></div>';
1659 }
1660
1661
1662 function bb_translate_video($s) {
1663
1664         $matches = null;
1665         $r = preg_match_all("/\[video\](.*?)\[\/video\]/ism",$s,$matches,PREG_SET_ORDER);
1666         if ($r) {
1667                 foreach ($matches as $mtch) {
1668                         if ((stristr($mtch[1],'youtube')) || (stristr($mtch[1],'youtu.be')))
1669                                 $s = str_replace($mtch[0],'[youtube]' . $mtch[1] . '[/youtube]',$s);
1670                         elseif (stristr($mtch[1],'vimeo'))
1671                                 $s = str_replace($mtch[0],'[vimeo]' . $mtch[1] . '[/vimeo]',$s);
1672                 }
1673         }
1674         return $s;
1675 }
1676
1677 function html2bb_video($s) {
1678
1679         $s = preg_replace('#<object[^>]+>(.*?)https?://www.youtube.com/((?:v|cp)/[A-Za-z0-9\-_=]+)(.*?)</object>#ism',
1680                         '[youtube]$2[/youtube]', $s);
1681
1682         $s = preg_replace('#<iframe[^>](.*?)https?://www.youtube.com/embed/([A-Za-z0-9\-_=]+)(.*?)</iframe>#ism',
1683                         '[youtube]$2[/youtube]', $s);
1684
1685         $s = preg_replace('#<iframe[^>](.*?)https?://player.vimeo.com/video/([0-9]+)(.*?)</iframe>#ism',
1686                         '[vimeo]$2[/vimeo]', $s);
1687
1688         return $s;
1689 }
1690
1691 /**
1692  * apply xmlify() to all values of array $val, recursively
1693  * @param array $val
1694  * @return array
1695  */
1696 function array_xmlify($val){
1697         if (is_bool($val)) {
1698                 return $val?"true":"false";
1699         } elseif (is_array($val)) {
1700                 return array_map('array_xmlify', $val);
1701         }
1702         return xmlify((string) $val);
1703 }
1704
1705
1706 /**
1707  * transorm link href and img src from relative to absolute
1708  *
1709  * @param string $text
1710  * @param string $base base url
1711  * @return string
1712  */
1713 function reltoabs($text, $base) {
1714         if (empty($base)) {
1715                 return $text;
1716         }
1717
1718         $base = rtrim($base,'/');
1719
1720         $base2 = $base . "/";
1721
1722         // Replace links
1723         $pattern = "/<a([^>]*) href=\"(?!http|https|\/)([^\"]*)\"/";
1724         $replace = "<a\${1} href=\"" . $base2 . "\${2}\"";
1725         $text = preg_replace($pattern, $replace, $text);
1726
1727         $pattern = "/<a([^>]*) href=\"(?!http|https)([^\"]*)\"/";
1728         $replace = "<a\${1} href=\"" . $base . "\${2}\"";
1729         $text = preg_replace($pattern, $replace, $text);
1730
1731         // Replace images
1732         $pattern = "/<img([^>]*) src=\"(?!http|https|\/)([^\"]*)\"/";
1733         $replace = "<img\${1} src=\"" . $base2 . "\${2}\"";
1734         $text = preg_replace($pattern, $replace, $text);
1735
1736         $pattern = "/<img([^>]*) src=\"(?!http|https)([^\"]*)\"/";
1737         $replace = "<img\${1} src=\"" . $base . "\${2}\"";
1738         $text = preg_replace($pattern, $replace, $text);
1739
1740
1741         // Done
1742         return $text;
1743 }
1744
1745 /**
1746  * get translated item type
1747  *
1748  * @param array $itme
1749  * @return string
1750  */
1751 function item_post_type($item) {
1752         if (intval($item['event-id'])) {
1753                 return t('event');
1754         } elseif (strlen($item['resource-id'])) {
1755                 return t('photo');
1756         } elseif (strlen($item['verb']) && $item['verb'] !== ACTIVITY_POST) {
1757                 return t('activity');
1758         } elseif ($item['id'] != $item['parent']) {
1759                 return t('comment');
1760         }
1761
1762         return t('post');
1763 }
1764
1765 // post categories and "save to file" use the same item.file table for storage.
1766 // We will differentiate the different uses by wrapping categories in angle brackets
1767 // and save to file categories in square brackets.
1768 // To do this we need to escape these characters if they appear in our tag.
1769
1770 function file_tag_encode($s) {
1771         return str_replace(array('<','>','[',']'),array('%3c','%3e','%5b','%5d'),$s);
1772 }
1773
1774 function file_tag_decode($s) {
1775         return str_replace(array('%3c', '%3e', '%5b', '%5d'), array('<', '>', '[', ']'), $s);
1776 }
1777
1778 function file_tag_file_query($table,$s,$type = 'file') {
1779
1780         if ($type == 'file') {
1781                 $str = preg_quote('[' . str_replace('%', '%%', file_tag_encode($s)) . ']');
1782         } else {
1783                 $str = preg_quote('<' . str_replace('%', '%%', file_tag_encode($s)) . '>');
1784         }
1785         return " AND " . (($table) ? dbesc($table) . '.' : '') . "file regexp '" . dbesc($str) . "' ";
1786 }
1787
1788 // ex. given music,video return <music><video> or [music][video]
1789 function file_tag_list_to_file($list,$type = 'file') {
1790         $tag_list = '';
1791         if (strlen($list)) {
1792                 $list_array = explode(",",$list);
1793                 if ($type == 'file') {
1794                         $lbracket = '[';
1795                         $rbracket = ']';
1796                 } else {
1797                         $lbracket = '<';
1798                         $rbracket = '>';
1799                 }
1800
1801                 foreach ($list_array as $item) {
1802                         if (strlen($item)) {
1803                                 $tag_list .= $lbracket . file_tag_encode(trim($item))  . $rbracket;
1804                         }
1805                 }
1806         }
1807         return $tag_list;
1808 }
1809
1810 // ex. given <music><video>[friends], return music,video or friends
1811 function file_tag_file_to_list($file,$type = 'file') {
1812         $matches = false;
1813         $list = '';
1814         if ($type == 'file') {
1815                 $cnt = preg_match_all('/\[(.*?)\]/', $file, $matches, PREG_SET_ORDER);
1816         } else {
1817                 $cnt = preg_match_all('/<(.*?)>/', $file, $matches, PREG_SET_ORDER);
1818         }
1819         if ($cnt) {
1820                 foreach ($matches as $mtch) {
1821                         if (strlen($list)) {
1822                                 $list .= ',';
1823                         }
1824                         $list .= file_tag_decode($mtch[1]);
1825                 }
1826         }
1827
1828         return $list;
1829 }
1830
1831 function file_tag_update_pconfig($uid, $file_old, $file_new, $type = 'file') {
1832         // $file_old - categories previously associated with an item
1833         // $file_new - new list of categories for an item
1834
1835         if (!intval($uid)) {
1836                 return false;
1837         }
1838         if ($file_old == $file_new) {
1839                 return true;
1840         }
1841
1842         $saved = PConfig::get($uid, 'system', 'filetags');
1843         if (strlen($saved)) {
1844                 if ($type == 'file') {
1845                         $lbracket = '[';
1846                         $rbracket = ']';
1847                         $termtype = TERM_FILE;
1848                 } else {
1849                         $lbracket = '<';
1850                         $rbracket = '>';
1851                         $termtype = TERM_CATEGORY;
1852                 }
1853
1854                 $filetags_updated = $saved;
1855
1856                 // check for new tags to be added as filetags in pconfig
1857                 $new_tags = array();
1858                 $check_new_tags = explode(",",file_tag_file_to_list($file_new,$type));
1859
1860                 foreach ($check_new_tags as $tag) {
1861                         if (! stristr($saved,$lbracket . file_tag_encode($tag) . $rbracket))
1862                                 $new_tags[] = $tag;
1863                 }
1864
1865                 $filetags_updated .= file_tag_list_to_file(implode(",",$new_tags),$type);
1866
1867                 // check for deleted tags to be removed from filetags in pconfig
1868                 $deleted_tags = array();
1869                 $check_deleted_tags = explode(",",file_tag_file_to_list($file_old,$type));
1870
1871                 foreach ($check_deleted_tags as $tag) {
1872                         if (! stristr($file_new,$lbracket . file_tag_encode($tag) . $rbracket))
1873                                 $deleted_tags[] = $tag;
1874                 }
1875
1876                 foreach ($deleted_tags as $key => $tag) {
1877                         $r = q("SELECT `oid` FROM `term` WHERE `term` = '%s' AND `otype` = %d AND `type` = %d AND `uid` = %d",
1878                                 dbesc($tag),
1879                                 intval(TERM_OBJ_POST),
1880                                 intval($termtype),
1881                                 intval($uid));
1882
1883                         if (DBM::is_result($r)) {
1884                                 unset($deleted_tags[$key]);
1885                         } else {
1886                                 $filetags_updated = str_replace($lbracket . file_tag_encode($tag) . $rbracket,'',$filetags_updated);
1887                         }
1888                 }
1889
1890                 if ($saved != $filetags_updated) {
1891                         PConfig::set($uid, 'system', 'filetags', $filetags_updated);
1892                 }
1893                 return true;
1894         } elseif (strlen($file_new)) {
1895                 PConfig::set($uid, 'system', 'filetags', $file_new);
1896         }
1897         return true;
1898 }
1899
1900 function file_tag_save_file($uid, $item, $file) {
1901         require_once "include/files.php";
1902
1903         $result = false;
1904         if (! intval($uid))
1905                 return false;
1906         $r = q("SELECT `file` FROM `item` WHERE `id` = %d AND `uid` = %d LIMIT 1",
1907                 intval($item),
1908                 intval($uid)
1909         );
1910         if (DBM::is_result($r)) {
1911                 if (! stristr($r[0]['file'],'[' . file_tag_encode($file) . ']')) {
1912                         q("UPDATE `item` SET `file` = '%s' WHERE `id` = %d AND `uid` = %d",
1913                                 dbesc($r[0]['file'] . '[' . file_tag_encode($file) . ']'),
1914                                 intval($item),
1915                                 intval($uid)
1916                         );
1917                 }
1918
1919                 create_files_from_item($item);
1920
1921                 $saved = PConfig::get($uid, 'system', 'filetags');
1922                 if (!strlen($saved) || !stristr($saved, '[' . file_tag_encode($file) . ']')) {
1923                         PConfig::set($uid, 'system', 'filetags', $saved . '[' . file_tag_encode($file) . ']');
1924                 }
1925                 info(t('Item filed'));
1926         }
1927         return true;
1928 }
1929
1930 function file_tag_unsave_file($uid, $item, $file, $cat = false) {
1931         require_once "include/files.php";
1932
1933         $result = false;
1934         if (! intval($uid))
1935                 return false;
1936
1937         if ($cat == true) {
1938                 $pattern = '<' . file_tag_encode($file) . '>' ;
1939                 $termtype = TERM_CATEGORY;
1940         } else {
1941                 $pattern = '[' . file_tag_encode($file) . ']' ;
1942                 $termtype = TERM_FILE;
1943         }
1944
1945
1946         $r = q("SELECT `file` FROM `item` WHERE `id` = %d AND `uid` = %d LIMIT 1",
1947                 intval($item),
1948                 intval($uid)
1949         );
1950         if (! DBM::is_result($r)) {
1951                 return false;
1952         }
1953
1954         q("UPDATE `item` SET `file` = '%s' WHERE `id` = %d AND `uid` = %d",
1955                 dbesc(str_replace($pattern,'',$r[0]['file'])),
1956                 intval($item),
1957                 intval($uid)
1958         );
1959
1960         create_files_from_item($item);
1961
1962         $r = q("SELECT `oid` FROM `term` WHERE `term` = '%s' AND `otype` = %d AND `type` = %d AND `uid` = %d",
1963                 dbesc($file),
1964                 intval(TERM_OBJ_POST),
1965                 intval($termtype),
1966                 intval($uid));
1967
1968         if (!DBM::is_result($r)) {
1969                 $saved = PConfig::get($uid, 'system', 'filetags');
1970                 PConfig::set($uid, 'system', 'filetags', str_replace($pattern, '', $saved));
1971         }
1972
1973         return true;
1974 }
1975
1976 function normalise_openid($s) {
1977         return trim(str_replace(array('http://', 'https://'), array('', ''), $s), '/');
1978 }
1979
1980
1981 function undo_post_tagging($s) {
1982         $matches = null;
1983         $cnt = preg_match_all('/([!#@])\[url=(.*?)\](.*?)\[\/url\]/ism', $s, $matches, PREG_SET_ORDER);
1984         if ($cnt) {
1985                 foreach ($matches as $mtch) {
1986                         $s = str_replace($mtch[0], $mtch[1] . $mtch[3],$s);
1987                 }
1988         }
1989         return $s;
1990 }
1991
1992 function protect_sprintf($s) {
1993         return str_replace('%', '%%', $s);
1994 }
1995
1996
1997 function is_a_date_arg($s) {
1998         $i = intval($s);
1999         if ($i > 1900) {
2000                 $y = date('Y');
2001                 if ($i <= $y + 1 && strpos($s, '-') == 4) {
2002                         $m = intval(substr($s,5));
2003                         if ($m > 0 && $m <= 12)
2004                                 return true;
2005                 }
2006         }
2007         return false;
2008 }
2009
2010 /**
2011  * remove intentation from a text
2012  */
2013 function deindent($text, $chr = "[\t ]", $count = NULL) {
2014         $lines = explode("\n", $text);
2015         if (is_null($count)) {
2016                 $m = array();
2017                 $k = 0;
2018                 while ($k < count($lines) && strlen($lines[$k]) == 0) {
2019                         $k++;
2020                 }
2021                 preg_match("|^" . $chr . "*|", $lines[$k], $m);
2022                 $count = strlen($m[0]);
2023         }
2024         for ($k = 0; $k < count($lines); $k++) {
2025                 $lines[$k] = preg_replace("|^" . $chr . "{" . $count . "}|", "", $lines[$k]);
2026         }
2027
2028         return implode("\n", $lines);
2029 }
2030
2031 function formatBytes($bytes, $precision = 2) {
2032          $units = array('B', 'KB', 'MB', 'GB', 'TB');
2033
2034         $bytes = max($bytes, 0);
2035         $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
2036         $pow = min($pow, count($units) - 1);
2037
2038         $bytes /= pow(1024, $pow);
2039
2040         return round($bytes, $precision) . ' ' . $units[$pow];
2041 }
2042
2043 /**
2044  * @brief translate and format the networkname of a contact
2045  *
2046  * @param string $network
2047  *      Networkname of the contact (e.g. dfrn, rss and so on)
2048  * @param sting $url
2049  *      The contact url
2050  * @return string
2051  */
2052 function format_network_name($network, $url = 0) {
2053         if ($network != "") {
2054                 require_once 'include/contact_selectors.php';
2055                 if ($url != "") {
2056                         $network_name = '<a href="'.$url.'">'.network_to_name($network, $url)."</a>";
2057                 } else {
2058                         $network_name = network_to_name($network);
2059                 }
2060
2061                 return $network_name;
2062         }
2063
2064 }
2065
2066 /**
2067  * @brief Syntax based code highlighting for popular languages.
2068  * @param string $s Code block
2069  * @param string $lang Programming language
2070  * @return string Formated html
2071  */
2072 function text_highlight($s, $lang) {
2073         if ($lang === 'js') {
2074                 $lang = 'javascript';
2075         }
2076
2077         // @TODO: Replace Text_Highlighter_Renderer_Html by scrivo/highlight.php
2078
2079         // Autoload the library to make constants available
2080         class_exists('Text_Highlighter_Renderer_Html');
2081
2082         $options = array(
2083                 'numbers' => HL_NUMBERS_LI,
2084                 'tabsize' => 4,
2085         );
2086
2087         $tag_added = false;
2088         $s = trim(html_entity_decode($s, ENT_COMPAT));
2089         $s = str_replace('    ', "\t", $s);
2090
2091         /*
2092          * The highlighter library insists on an opening php tag for php code blocks. If
2093          * it isn't present, nothing is highlighted. So we're going to see if it's present.
2094          * If not, we'll add it, and then quietly remove it after we get the processed output back.
2095          */
2096         if ($lang === 'php' && strpos($s, '<?php') !== 0) {
2097                 $s = '<?php' . "\n" . $s;
2098                 $tag_added = true;
2099         }
2100
2101         $renderer = new Text_Highlighter_Renderer_Html($options);
2102         $hl = Text_Highlighter::factory($lang);
2103         $hl->setRenderer($renderer);
2104         $o = $hl->highlight($s);
2105         $o = str_replace("\n", '', $o);
2106
2107         if ($tag_added) {
2108                 $b = substr($o, 0, strpos($o, '<li>'));
2109                 $e = substr($o, strpos($o, '</li>'));
2110                 $o = $b . $e;
2111         }
2112
2113         return '<code>' . $o . '</code>';
2114 }