]> git.mxchange.org Git - friendica.git/blob - include/text.php
Merge pull request #6098 from annando/worker-speed
[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  * return number of bytes in size (K, M, G)
498  * @param string $size_str
499  * @return number
500  */
501 function return_bytes($size_str) {
502         switch (substr ($size_str, -1)) {
503                 case 'M': case 'm': return (int)$size_str * 1048576;
504                 case 'K': case 'k': return (int)$size_str * 1024;
505                 case 'G': case 'g': return (int)$size_str * 1073741824;
506                 default: return $size_str;
507         }
508 }
509
510 /**
511  * @param string $s
512  * @param boolean $strip_padding
513  * @return string
514  */
515 function base64url_encode($s, $strip_padding = false) {
516
517         $s = strtr(base64_encode($s), '+/', '-_');
518
519         if ($strip_padding) {
520                 $s = str_replace('=','',$s);
521         }
522
523         return $s;
524 }
525
526 /**
527  * @param string $s
528  * @return string
529  */
530 function base64url_decode($s) {
531
532         if (is_array($s)) {
533                 Logger::log('base64url_decode: illegal input: ' . print_r(debug_backtrace(), true));
534                 return $s;
535         }
536
537 /*
538  *  // Placeholder for new rev of salmon which strips base64 padding.
539  *  // PHP base64_decode handles the un-padded input without requiring this step
540  *  // Uncomment if you find you need it.
541  *
542  *      $l = strlen($s);
543  *      if (!strpos($s,'=')) {
544  *              $m = $l % 4;
545  *              if ($m == 2)
546  *                      $s .= '==';
547  *              if ($m == 3)
548  *                      $s .= '=';
549  *      }
550  *
551  */
552
553         return base64_decode(strtr($s,'-_','+/'));
554 }
555
556
557 function bb_translate_video($s) {
558
559         $matches = null;
560         $r = preg_match_all("/\[video\](.*?)\[\/video\]/ism",$s,$matches,PREG_SET_ORDER);
561         if ($r) {
562                 foreach ($matches as $mtch) {
563                         if ((stristr($mtch[1], 'youtube')) || (stristr($mtch[1], 'youtu.be'))) {
564                                 $s = str_replace($mtch[0], '[youtube]' . $mtch[1] . '[/youtube]', $s);
565                         } elseif (stristr($mtch[1], 'vimeo')) {
566                                 $s = str_replace($mtch[0], '[vimeo]' . $mtch[1] . '[/vimeo]', $s);
567                         }
568                 }
569         }
570         return $s;
571 }
572
573 function normalise_openid($s) {
574         return trim(str_replace(['http://', 'https://'], ['', ''], $s), '/');
575 }
576
577
578 function undo_post_tagging($s) {
579         $matches = null;
580         $cnt = preg_match_all('/([!#@])\[url=(.*?)\](.*?)\[\/url\]/ism', $s, $matches, PREG_SET_ORDER);
581         if ($cnt) {
582                 foreach ($matches as $mtch) {
583                         if (in_array($mtch[1], ['!', '@'])) {
584                                 $contact = Contact::getDetailsByURL($mtch[2]);
585                                 $mtch[3] = empty($contact['addr']) ? $mtch[2] : $contact['addr'];
586                         }
587                         $s = str_replace($mtch[0], $mtch[1] . $mtch[3],$s);
588                 }
589         }
590         return $s;
591 }
592
593 function protect_sprintf($s) {
594         return str_replace('%', '%%', $s);
595 }
596
597 /// @TODO Rewrite this
598 function is_a_date_arg($s) {
599         $i = intval($s);
600
601         if ($i > 1900) {
602                 $y = date('Y');
603
604                 if ($i <= $y + 1 && strpos($s, '-') == 4) {
605                         $m = intval(substr($s, 5));
606
607                         if ($m > 0 && $m <= 12) {
608                                 return true;
609                         }
610                 }
611         }
612
613         return false;
614 }
615
616 /**
617  * remove intentation from a text
618  */
619 function deindent($text, $chr = "[\t ]", $count = NULL) {
620         $lines = explode("\n", $text);
621
622         if (is_null($count)) {
623                 $m = [];
624                 $k = 0;
625                 while ($k < count($lines) && strlen($lines[$k]) == 0) {
626                         $k++;
627                 }
628                 preg_match("|^" . $chr . "*|", $lines[$k], $m);
629                 $count = strlen($m[0]);
630         }
631
632         for ($k = 0; $k < count($lines); $k++) {
633                 $lines[$k] = preg_replace("|^" . $chr . "{" . $count . "}|", "", $lines[$k]);
634         }
635
636         return implode("\n", $lines);
637 }
638
639 function formatBytes($bytes, $precision = 2) {
640         $units = ['B', 'KB', 'MB', 'GB', 'TB'];
641
642         $bytes = max($bytes, 0);
643         $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
644         $pow = min($pow, count($units) - 1);
645
646         $bytes /= pow(1024, $pow);
647
648         return round($bytes, $precision) . ' ' . $units[$pow];
649 }
650
651 /**
652  * @brief translate and format the networkname of a contact
653  *
654  * @param string $network
655  *      Networkname of the contact (e.g. dfrn, rss and so on)
656  * @param sting $url
657  *      The contact url
658  * @return string
659  */
660 function format_network_name($network, $url = 0) {
661         if ($network != "") {
662                 if ($url != "") {
663                         $network_name = '<a href="'.$url.'">'.ContactSelector::networkToName($network, $url)."</a>";
664                 } else {
665                         $network_name = ContactSelector::networkToName($network);
666                 }
667
668                 return $network_name;
669         }
670 }