]> git.mxchange.org Git - friendica.git/blob - include/text.php
Function calls
[friendica.git] / include / text.php
1 <?php
2 /**
3  * @file include/text.php
4  */
5
6 use Friendica\App;
7 use Friendica\Content\ContactSelector;
8 use Friendica\Content\Feature;
9 use Friendica\Content\Smilies;
10 use Friendica\Content\Text\BBCode;
11 use Friendica\Core\Addon;
12 use Friendica\Core\Config;
13 use Friendica\Core\L10n;
14 use Friendica\Core\PConfig;
15 use Friendica\Core\Protocol;
16 use Friendica\Core\System;
17 use Friendica\Database\DBA;
18 use Friendica\Model\Contact;
19 use Friendica\Model\Event;
20 use Friendica\Model\Item;
21 use Friendica\Render\FriendicaSmarty;
22 use Friendica\Util\DateTimeFormat;
23 use Friendica\Util\Map;
24 use Friendica\Util\Proxy as ProxyUtils;
25
26 use Friendica\Core\Logger;
27 use Friendica\Core\Renderer;
28 use Friendica\Model\FileTag;
29 use Friendica\Util\XML;
30 use Friendica\Content\Text\HTML;
31
32 require_once "include/conversation.php";
33
34 /**
35  * @brief Generates a pseudo-random string of hexadecimal characters
36  *
37  * @param int $size
38  * @return string
39  */
40 function random_string($size = 64)
41 {
42         $byte_size = ceil($size / 2);
43
44         $bytes = random_bytes($byte_size);
45
46         $return = substr(bin2hex($bytes), 0, $size);
47
48         return $return;
49 }
50
51 /**
52  * This is our primary input filter.
53  *
54  * The high bit hack only involved some old IE browser, forget which (IE5/Mac?)
55  * that had an XSS attack vector due to stripping the high-bit on an 8-bit character
56  * after cleansing, and angle chars with the high bit set could get through as markup.
57  *
58  * This is now disabled because it was interfering with some legitimate unicode sequences
59  * and hopefully there aren't a lot of those browsers left.
60  *
61  * Use this on any text input where angle chars are not valid or permitted
62  * They will be replaced with safer brackets. This may be filtered further
63  * if these are not allowed either.
64  *
65  * @param string $string Input string
66  * @return string Filtered string
67  */
68 function notags($string) {
69         return str_replace(["<", ">"], ['[', ']'], $string);
70
71 //  High-bit filter no longer used
72 //      return str_replace(array("<",">","\xBA","\xBC","\xBE"), array('[',']','','',''), $string);
73 }
74
75
76 /**
77  * use this on "body" or "content" input where angle chars shouldn't be removed,
78  * and allow them to be safely displayed.
79  * @param string $string
80  * @return string
81  */
82 function escape_tags($string) {
83         return htmlspecialchars($string, ENT_COMPAT, 'UTF-8', false);
84 }
85
86
87 /**
88  * generate a string that's random, but usually pronounceable.
89  * used to generate initial passwords
90  * @param int $len
91  * @return string
92  */
93 function autoname($len) {
94
95         if ($len <= 0) {
96                 return '';
97         }
98
99         $vowels = ['a','a','ai','au','e','e','e','ee','ea','i','ie','o','ou','u'];
100         if (mt_rand(0, 5) == 4) {
101                 $vowels[] = 'y';
102         }
103
104         $cons = [
105                         'b','bl','br',
106                         'c','ch','cl','cr',
107                         'd','dr',
108                         'f','fl','fr',
109                         'g','gh','gl','gr',
110                         'h',
111                         'j',
112                         'k','kh','kl','kr',
113                         'l',
114                         'm',
115                         'n',
116                         'p','ph','pl','pr',
117                         'qu',
118                         'r','rh',
119                         's','sc','sh','sm','sp','st',
120                         't','th','tr',
121                         'v',
122                         'w','wh',
123                         'x',
124                         'z','zh'
125                         ];
126
127         $midcons = ['ck','ct','gn','ld','lf','lm','lt','mb','mm', 'mn','mp',
128                                 'nd','ng','nk','nt','rn','rp','rt'];
129
130         $noend = ['bl', 'br', 'cl','cr','dr','fl','fr','gl','gr',
131                                 'kh', 'kl','kr','mn','pl','pr','rh','tr','qu','wh','q'];
132
133         $start = mt_rand(0,2);
134         if ($start == 0) {
135                 $table = $vowels;
136         } else {
137                 $table = $cons;
138         }
139
140         $word = '';
141
142         for ($x = 0; $x < $len; $x ++) {
143                 $r = mt_rand(0,count($table) - 1);
144                 $word .= $table[$r];
145
146                 if ($table == $vowels) {
147                         $table = array_merge($cons,$midcons);
148                 } else {
149                         $table = $vowels;
150                 }
151
152         }
153
154         $word = substr($word,0,$len);
155
156         foreach ($noend as $noe) {
157                 $noelen = strlen($noe);
158                 if ((strlen($word) > $noelen) && (substr($word, -$noelen) == $noe)) {
159                         $word = autoname($len);
160                         break;
161                 }
162         }
163
164         return $word;
165 }
166
167 /**
168  * Turn user/group ACLs stored as angle bracketed text into arrays
169  *
170  * @param string $s
171  * @return array
172  */
173 function expand_acl($s) {
174         // turn string array of angle-bracketed elements into numeric array
175         // e.g. "<1><2><3>" => array(1,2,3);
176         $ret = [];
177
178         if (strlen($s)) {
179                 $t = str_replace('<', '', $s);
180                 $a = explode('>', $t);
181                 foreach ($a as $aa) {
182                         if (intval($aa)) {
183                                 $ret[] = intval($aa);
184                         }
185                 }
186         }
187         return $ret;
188 }
189
190
191 /**
192  * Wrap ACL elements in angle brackets for storage
193  * @param string $item
194  */
195 function sanitise_acl(&$item) {
196         if (intval($item)) {
197                 $item = '<' . intval(notags(trim($item))) . '>';
198         } else {
199                 unset($item);
200         }
201 }
202
203
204 /**
205  * Convert an ACL array to a storable string
206  *
207  * Normally ACL permissions will be an array.
208  * We'll also allow a comma-separated string.
209  *
210  * @param string|array $p
211  * @return string
212  */
213 function perms2str($p) {
214         $ret = '';
215         if (is_array($p)) {
216                 $tmp = $p;
217         } else {
218                 $tmp = explode(',', $p);
219         }
220
221         if (is_array($tmp)) {
222                 array_walk($tmp, 'sanitise_acl');
223                 $ret = implode('', $tmp);
224         }
225         return $ret;
226 }
227
228 /**
229  *  for html,xml parsing - let's say you've got
230  *  an attribute foobar="class1 class2 class3"
231  *  and you want to find out if it contains 'class3'.
232  *  you can't use a normal sub string search because you
233  *  might match 'notclass3' and a regex to do the job is
234  *  possible but a bit complicated.
235  *  pass the attribute string as $attr and the attribute you
236  *  are looking for as $s - returns true if found, otherwise false
237  *
238  * @param string $attr attribute value
239  * @param string $s string to search
240  * @return boolean True if found, False otherwise
241  */
242 function attribute_contains($attr, $s) {
243         $a = explode(' ', $attr);
244         return (count($a) && in_array($s,$a));
245 }
246
247 /**
248  * Compare activity uri. Knows about activity namespace.
249  *
250  * @param string $haystack
251  * @param string $needle
252  * @return boolean
253  */
254 function activity_match($haystack,$needle) {
255         return (($haystack === $needle) || ((basename($needle) === $haystack) && strstr($needle, NAMESPACE_ACTIVITY_SCHEMA)));
256 }
257
258
259 /**
260  * @brief Pull out all #hashtags and @person tags from $string.
261  *
262  * We also get @person@domain.com - which would make
263  * the regex quite complicated as tags can also
264  * end a sentence. So we'll run through our results
265  * and strip the period from any tags which end with one.
266  * Returns array of tags found, or empty array.
267  *
268  * @param string $string Post content
269  * @return array List of tag and person names
270  */
271 function get_tags($string) {
272         $ret = [];
273
274         // Convert hashtag links to hashtags
275         $string = preg_replace('/#\[url\=([^\[\]]*)\](.*?)\[\/url\]/ism', '#$2', $string);
276
277         // ignore anything in a code block
278         $string = preg_replace('/\[code\](.*?)\[\/code\]/sm', '', $string);
279
280         // Force line feeds at bbtags
281         $string = str_replace(['[', ']'], ["\n[", "]\n"], $string);
282
283         // ignore anything in a bbtag
284         $string = preg_replace('/\[(.*?)\]/sm', '', $string);
285
286         // Match full names against @tags including the space between first and last
287         // We will look these up afterward to see if they are full names or not recognisable.
288
289         if (preg_match_all('/(@[^ \x0D\x0A,:?]+ [^ \x0D\x0A@,:?]+)([ \x0D\x0A@,:?]|$)/', $string, $matches)) {
290                 foreach ($matches[1] as $match) {
291                         if (strstr($match, ']')) {
292                                 // we might be inside a bbcode color tag - leave it alone
293                                 continue;
294                         }
295                         if (substr($match, -1, 1) === '.') {
296                                 $ret[] = substr($match, 0, -1);
297                         } else {
298                                 $ret[] = $match;
299                         }
300                 }
301         }
302
303         // Otherwise pull out single word tags. These can be @nickname, @first_last
304         // and #hash tags.
305
306         if (preg_match_all('/([!#@][^\^ \x0D\x0A,;:?]+)([ \x0D\x0A,;:?]|$)/', $string, $matches)) {
307                 foreach ($matches[1] as $match) {
308                         if (strstr($match, ']')) {
309                                 // we might be inside a bbcode color tag - leave it alone
310                                 continue;
311                         }
312                         if (substr($match, -1, 1) === '.') {
313                                 $match = substr($match,0,-1);
314                         }
315                         // ignore strictly numeric tags like #1
316                         if ((strpos($match, '#') === 0) && ctype_digit(substr($match, 1))) {
317                                 continue;
318                         }
319                         // try not to catch url fragments
320                         if (strpos($string, $match) && preg_match('/[a-zA-z0-9\/]/', substr($string, strpos($string, $match) - 1, 1))) {
321                                 continue;
322                         }
323                         $ret[] = $match;
324                 }
325         }
326         return $ret;
327 }
328
329
330 /**
331  * quick and dirty quoted_printable encoding
332  *
333  * @param string $s
334  * @return string
335  */
336 function qp($s) {
337         return str_replace("%", "=", rawurlencode($s));
338 }
339
340 /**
341  * @brief Check for a valid email string
342  *
343  * @param string $email_address
344  * @return boolean
345  */
346 function valid_email($email_address)
347 {
348         return preg_match('/^[_a-zA-Z0-9\-\+]+(\.[_a-zA-Z0-9\-\+]+)*@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+$/', $email_address);
349 }
350
351 /**
352  * Normalize url
353  *
354  * @param string $url
355  * @return string
356  */
357 function normalise_link($url) {
358         $ret = str_replace(['https:', '//www.'], ['http:', '//'], $url);
359         return rtrim($ret,'/');
360 }
361
362
363 /**
364  * Compare two URLs to see if they are the same, but ignore
365  * slight but hopefully insignificant differences such as if one
366  * is https and the other isn't, or if one is www.something and
367  * the other isn't - and also ignore case differences.
368  *
369  * @param string $a first url
370  * @param string $b second url
371  * @return boolean True if the URLs match, otherwise False
372  *
373  */
374 function link_compare($a, $b) {
375         return (strcasecmp(normalise_link($a), normalise_link($b)) === 0);
376 }
377
378
379 /**
380  * @brief Find any non-embedded images in private items and add redir links to them
381  *
382  * @param App $a
383  * @param array &$item The field array of an item row
384  */
385 function redir_private_images($a, &$item)
386 {
387         $matches = false;
388         $cnt = preg_match_all('|\[img\](http[^\[]*?/photo/[a-fA-F0-9]+?(-[0-9]\.[\w]+?)?)\[\/img\]|', $item['body'], $matches, PREG_SET_ORDER);
389         if ($cnt) {
390                 foreach ($matches as $mtch) {
391                         if (strpos($mtch[1], '/redir') !== false) {
392                                 continue;
393                         }
394
395                         if ((local_user() == $item['uid']) && ($item['private'] == 1) && ($item['contact-id'] != $a->contact['id']) && ($item['network'] == Protocol::DFRN)) {
396                                 $img_url = 'redir?f=1&quiet=1&url=' . urlencode($mtch[1]) . '&conurl=' . urlencode($item['author-link']);
397                                 $item['body'] = str_replace($mtch[0], '[img]' . $img_url . '[/img]', $item['body']);
398                         }
399                 }
400         }
401 }
402
403 /**
404  * @brief Given a text string, convert from bbcode to html and add smilie icons.
405  *
406  * @param string $text String with bbcode.
407  * @return string Formattet HTML.
408  */
409 function prepare_text($text) {
410         if (stristr($text, '[nosmile]')) {
411                 $s = BBCode::convert($text);
412         } else {
413                 $s = Smilies::replace(BBCode::convert($text));
414         }
415
416         return trim($s);
417 }
418
419 /**
420  * return array with details for categories and folders for an item
421  *
422  * @param array $item
423  * @return array
424  *
425   * [
426  *      [ // categories array
427  *          {
428  *               'name': 'category name',
429  *               'removeurl': 'url to remove this category',
430  *               'first': 'is the first in this array? true/false',
431  *               'last': 'is the last in this array? true/false',
432  *           } ,
433  *           ....
434  *       ],
435  *       [ //folders array
436  *                      {
437  *               'name': 'folder name',
438  *               'removeurl': 'url to remove this folder',
439  *               'first': 'is the first in this array? true/false',
440  *               'last': 'is the last in this array? true/false',
441  *           } ,
442  *           ....
443  *       ]
444  *  ]
445  */
446 function get_cats_and_terms($item)
447 {
448         $categories = [];
449         $folders = [];
450
451         $matches = false;
452         $first = true;
453         $cnt = preg_match_all('/<(.*?)>/', $item['file'], $matches, PREG_SET_ORDER);
454         if ($cnt) {
455                 foreach ($matches as $mtch) {
456                         $categories[] = [
457                                 'name' => XML::escape(FileTag::decode($mtch[1])),
458                                 'url' =>  "#",
459                                 'removeurl' => ((local_user() == $item['uid'])?'filerm/' . $item['id'] . '?f=&cat=' . XML::escape(FileTag::decode($mtch[1])):""),
460                                 'first' => $first,
461                                 'last' => false
462                         ];
463                         $first = false;
464                 }
465         }
466
467         if (count($categories)) {
468                 $categories[count($categories) - 1]['last'] = true;
469         }
470
471         if (local_user() == $item['uid']) {
472                 $matches = false;
473                 $first = true;
474                 $cnt = preg_match_all('/\[(.*?)\]/', $item['file'], $matches, PREG_SET_ORDER);
475                 if ($cnt) {
476                         foreach ($matches as $mtch) {
477                                 $folders[] = [
478                                         'name' => XML::escape(FileTag::decode($mtch[1])),
479                                         'url' =>  "#",
480                                         'removeurl' => ((local_user() == $item['uid']) ? 'filerm/' . $item['id'] . '?f=&term=' . XML::escape(FileTag::decode($mtch[1])) : ""),
481                                         'first' => $first,
482                                         'last' => false
483                                 ];
484                                 $first = false;
485                         }
486                 }
487         }
488
489         if (count($folders)) {
490                 $folders[count($folders) - 1]['last'] = true;
491         }
492
493         return [$categories, $folders];
494 }
495
496
497 /**
498  * get private link for item
499  * @param array $item
500  * @return boolean|array False if item has not plink, otherwise array('href'=>plink url, 'title'=>translated title)
501  */
502 function get_plink($item) {
503         $a = get_app();
504
505         if ($a->user['nickname'] != "") {
506                 $ret = [
507                                 //'href' => "display/" . $a->user['nickname'] . "/" . $item['id'],
508                                 'href' => "display/" . $item['guid'],
509                                 'orig' => "display/" . $item['guid'],
510                                 'title' => L10n::t('View on separate page'),
511                                 'orig_title' => L10n::t('view on separate page'),
512                         ];
513
514                 if (x($item, 'plink')) {
515                         $ret["href"] = $a->removeBaseURL($item['plink']);
516                         $ret["title"] = L10n::t('link to source');
517                 }
518
519         } elseif (x($item, 'plink') && ($item['private'] != 1)) {
520                 $ret = [
521                                 'href' => $item['plink'],
522                                 'orig' => $item['plink'],
523                                 'title' => L10n::t('link to source'),
524                         ];
525         } else {
526                 $ret = [];
527         }
528
529         return $ret;
530 }
531
532 /**
533  * return number of bytes in size (K, M, G)
534  * @param string $size_str
535  * @return number
536  */
537 function return_bytes($size_str) {
538         switch (substr ($size_str, -1)) {
539                 case 'M': case 'm': return (int)$size_str * 1048576;
540                 case 'K': case 'k': return (int)$size_str * 1024;
541                 case 'G': case 'g': return (int)$size_str * 1073741824;
542                 default: return $size_str;
543         }
544 }
545
546 /**
547  * @param string $s
548  * @param boolean $strip_padding
549  * @return string
550  */
551 function base64url_encode($s, $strip_padding = false) {
552
553         $s = strtr(base64_encode($s), '+/', '-_');
554
555         if ($strip_padding) {
556                 $s = str_replace('=','',$s);
557         }
558
559         return $s;
560 }
561
562 /**
563  * @param string $s
564  * @return string
565  */
566 function base64url_decode($s) {
567
568         if (is_array($s)) {
569                 Logger::log('base64url_decode: illegal input: ' . print_r(debug_backtrace(), true));
570                 return $s;
571         }
572
573 /*
574  *  // Placeholder for new rev of salmon which strips base64 padding.
575  *  // PHP base64_decode handles the un-padded input without requiring this step
576  *  // Uncomment if you find you need it.
577  *
578  *      $l = strlen($s);
579  *      if (!strpos($s,'=')) {
580  *              $m = $l % 4;
581  *              if ($m == 2)
582  *                      $s .= '==';
583  *              if ($m == 3)
584  *                      $s .= '=';
585  *      }
586  *
587  */
588
589         return base64_decode(strtr($s,'-_','+/'));
590 }
591
592
593 function bb_translate_video($s) {
594
595         $matches = null;
596         $r = preg_match_all("/\[video\](.*?)\[\/video\]/ism",$s,$matches,PREG_SET_ORDER);
597         if ($r) {
598                 foreach ($matches as $mtch) {
599                         if ((stristr($mtch[1], 'youtube')) || (stristr($mtch[1], 'youtu.be'))) {
600                                 $s = str_replace($mtch[0], '[youtube]' . $mtch[1] . '[/youtube]', $s);
601                         } elseif (stristr($mtch[1], 'vimeo')) {
602                                 $s = str_replace($mtch[0], '[vimeo]' . $mtch[1] . '[/vimeo]', $s);
603                         }
604                 }
605         }
606         return $s;
607 }
608
609 function normalise_openid($s) {
610         return trim(str_replace(['http://', 'https://'], ['', ''], $s), '/');
611 }
612
613
614 function undo_post_tagging($s) {
615         $matches = null;
616         $cnt = preg_match_all('/([!#@])\[url=(.*?)\](.*?)\[\/url\]/ism', $s, $matches, PREG_SET_ORDER);
617         if ($cnt) {
618                 foreach ($matches as $mtch) {
619                         if (in_array($mtch[1], ['!', '@'])) {
620                                 $contact = Contact::getDetailsByURL($mtch[2]);
621                                 $mtch[3] = empty($contact['addr']) ? $mtch[2] : $contact['addr'];
622                         }
623                         $s = str_replace($mtch[0], $mtch[1] . $mtch[3],$s);
624                 }
625         }
626         return $s;
627 }
628
629 function protect_sprintf($s) {
630         return str_replace('%', '%%', $s);
631 }
632
633 /// @TODO Rewrite this
634 function is_a_date_arg($s) {
635         $i = intval($s);
636
637         if ($i > 1900) {
638                 $y = date('Y');
639
640                 if ($i <= $y + 1 && strpos($s, '-') == 4) {
641                         $m = intval(substr($s, 5));
642
643                         if ($m > 0 && $m <= 12) {
644                                 return true;
645                         }
646                 }
647         }
648
649         return false;
650 }
651
652 /**
653  * remove intentation from a text
654  */
655 function deindent($text, $chr = "[\t ]", $count = NULL) {
656         $lines = explode("\n", $text);
657
658         if (is_null($count)) {
659                 $m = [];
660                 $k = 0;
661                 while ($k < count($lines) && strlen($lines[$k]) == 0) {
662                         $k++;
663                 }
664                 preg_match("|^" . $chr . "*|", $lines[$k], $m);
665                 $count = strlen($m[0]);
666         }
667
668         for ($k = 0; $k < count($lines); $k++) {
669                 $lines[$k] = preg_replace("|^" . $chr . "{" . $count . "}|", "", $lines[$k]);
670         }
671
672         return implode("\n", $lines);
673 }
674
675 function formatBytes($bytes, $precision = 2) {
676         $units = ['B', 'KB', 'MB', 'GB', 'TB'];
677
678         $bytes = max($bytes, 0);
679         $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
680         $pow = min($pow, count($units) - 1);
681
682         $bytes /= pow(1024, $pow);
683
684         return round($bytes, $precision) . ' ' . $units[$pow];
685 }
686
687 /**
688  * @brief translate and format the networkname of a contact
689  *
690  * @param string $network
691  *      Networkname of the contact (e.g. dfrn, rss and so on)
692  * @param sting $url
693  *      The contact url
694  * @return string
695  */
696 function format_network_name($network, $url = 0) {
697         if ($network != "") {
698                 if ($url != "") {
699                         $network_name = '<a href="'.$url.'">'.ContactSelector::networkToName($network, $url)."</a>";
700                 } else {
701                         $network_name = ContactSelector::networkToName($network);
702                 }
703
704                 return $network_name;
705         }
706 }