Move autolink regex in Util\Strings
[friendica.git/.git] / src / Util / Strings.php
1 <?php
2 /**
3  * @file src/Util/Strings.php
4  */
5
6 namespace Friendica\Util;
7
8 use Friendica\Content\ContactSelector;
9 use Friendica\Core\Logger;
10
11 /**
12  * @brief This class handles string functions
13  */
14 class Strings
15 {
16         /**
17          * @brief Generates a pseudo-random string of hexadecimal characters
18          *
19          * @param int $size
20          * @return string
21          * @throws \Exception
22          */
23     public static function getRandomHex($size = 64)
24     {
25         $byte_size = ceil($size / 2);
26
27         $bytes = random_bytes($byte_size);
28
29         $return = substr(bin2hex($bytes), 0, $size);
30
31         return $return;
32     }
33
34     /**
35      * @brief This is our primary input filter.
36      *
37      * Use this on any text input where angle chars are not valid or permitted
38      * They will be replaced with safer brackets. This may be filtered further
39      * if these are not allowed either.
40      *
41      * @param string $string Input string
42      * @return string Filtered string
43      */
44     public static function escapeTags($string)
45     {
46         return str_replace(["<", ">"], ['[', ']'], $string);
47     }
48
49     /**
50      * @brief Use this on "body" or "content" input where angle chars shouldn't be removed,
51      * and allow them to be safely displayed.
52      * @param string $string
53      * 
54      * @return string
55      */
56     public static function escapeHtml($string)
57     {
58         return htmlspecialchars($string, ENT_COMPAT, 'UTF-8', false);
59     }
60
61     /**
62      * @brief Generate a string that's random, but usually pronounceable. Used to generate initial passwords
63      * 
64      * @param int $len  length
65      * 
66      * @return string
67      */
68     public static function getRandomName($len)
69     {
70         if ($len <= 0) {
71             return '';
72         }
73
74         $vowels = ['a', 'a', 'ai', 'au', 'e', 'e', 'e', 'ee', 'ea', 'i', 'ie', 'o', 'ou', 'u'];
75
76         if (mt_rand(0, 5) == 4) {
77             $vowels[] = 'y';
78         }
79
80         $cons = [
81                 'b', 'bl', 'br',
82                 'c', 'ch', 'cl', 'cr',
83                 'd', 'dr',
84                 'f', 'fl', 'fr',
85                 'g', 'gh', 'gl', 'gr',
86                 'h',
87                 'j',
88                 'k', 'kh', 'kl', 'kr',
89                 'l',
90                 'm',
91                 'n',
92                 'p', 'ph', 'pl', 'pr',
93                 'qu',
94                 'r', 'rh',
95                 's' ,'sc', 'sh', 'sm', 'sp', 'st',
96                 't', 'th', 'tr',
97                 'v',
98                 'w', 'wh',
99                 'x',
100                 'z', 'zh'
101             ];
102
103         $midcons = ['ck', 'ct', 'gn', 'ld', 'lf', 'lm', 'lt', 'mb', 'mm', 'mn', 'mp',
104                     'nd', 'ng', 'nk', 'nt', 'rn', 'rp', 'rt'];
105
106         $noend = ['bl', 'br', 'cl', 'cr', 'dr', 'fl', 'fr', 'gl', 'gr',
107                     'kh', 'kl', 'kr', 'mn', 'pl', 'pr', 'rh', 'tr', 'qu', 'wh', 'q'];
108
109         $start = mt_rand(0, 2);
110         if ($start == 0) {
111             $table = $vowels;
112         } else {
113             $table = $cons;
114         }
115
116         $word = '';
117
118         for ($x = 0; $x < $len; $x ++) {
119             $r = mt_rand(0, count($table) - 1);
120             $word .= $table[$r];
121
122             if ($table == $vowels) {
123                 $table = array_merge($cons, $midcons);
124             } else {
125                 $table = $vowels;
126             }
127
128         }
129
130         $word = substr($word, 0, $len);
131
132         foreach ($noend as $noe) {
133             $noelen = strlen($noe);
134             if ((strlen($word) > $noelen) && (substr($word, -$noelen) == $noe)) {
135                 $word = self::getRandomName($len);
136                 break;
137             }
138         }
139
140         return $word;
141     }
142
143         /**
144          * Translate and format the network name of a contact
145          *
146          * @param string $network Network name of the contact (e.g. dfrn, rss and so on)
147          * @param string $url     The contact url
148          *
149          * @return string Formatted network name
150          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
151          */
152     public static function formatNetworkName($network, $url = '')
153     {
154         if ($network != '') {
155             if ($url != '') {
156                 $network_name = '<a href="' . $url  .'">' . ContactSelector::networkToName($network, $url) . '</a>';
157             } else {
158                 $network_name = ContactSelector::networkToName($network);
159             }
160
161             return $network_name;
162         }
163     }
164
165     /**
166      * @brief Remove indentation from a text
167      * 
168      * @param string $text  String to be transformed.
169      * @param string $chr   Optional. Indentation tag. Default tab (\t).
170      * @param int    $count Optional. Default null.
171      * 
172      * @return string       Transformed string.
173      */
174     public static function deindent($text, $chr = "[\t ]", $count = NULL)
175     {
176         $lines = explode("\n", $text);
177
178         if (is_null($count)) {
179             $m = [];
180             $k = 0;
181             while ($k < count($lines) && strlen($lines[$k]) == 0) {
182                 $k++;
183             }
184             preg_match("|^" . $chr . "*|", $lines[$k], $m);
185             $count = strlen($m[0]);
186         }
187
188         for ($k = 0; $k < count($lines); $k++) {
189             $lines[$k] = preg_replace("|^" . $chr . "{" . $count . "}|", "", $lines[$k]);
190         }
191
192         return implode("\n", $lines);
193     }
194
195     /**
196      * @brief Get byte size returned in a Data Measurement (KB, MB, GB)
197      * 
198      * @param int $bytes    The number of bytes to be measured
199      * @param int $precision    Optional. Default 2.
200      * 
201      * @return string   Size with measured units.
202      */
203     public static function formatBytes($bytes, $precision = 2)
204     {
205         $units = ['B', 'KB', 'MB', 'GB', 'TB'];
206         $bytes = max($bytes, 0);
207         $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
208         $pow = min($pow, count($units) - 1);
209         $bytes /= pow(1024, $pow);
210
211         return round($bytes, $precision) . ' ' . $units[$pow];
212     }
213
214     /**
215      * @brief Protect percent characters in sprintf calls
216      * 
217      * @param string $s String to transform.
218      * 
219      * @return string   Transformed string.
220      */
221     public static function protectSprintf($s)
222     {
223         return str_replace('%', '%%', $s);
224     }
225
226     /**
227      * @brief Base64 Encode URL and translate +/ to -_ Optionally strip padding.
228      * 
229      * @param string $s                 URL to encode
230      * @param boolean $strip_padding    Optional. Default false
231      * 
232      * @return string   Encoded URL
233      */
234     public static function base64UrlEncode($s, $strip_padding = false)
235     {
236         $s = strtr(base64_encode($s), '+/', '-_');
237
238         if ($strip_padding) {
239             $s = str_replace('=', '', $s);
240         }
241
242         return $s;
243     }
244
245         /**
246          * @brief Decode Base64 Encoded URL and translate -_ to +/
247          * @param string $s URL to decode
248          *
249          * @return string   Decoded URL
250          * @throws \Exception
251          */
252     public static function base64UrlDecode($s)
253     {
254         if (is_array($s)) {
255             Logger::log('base64url_decode: illegal input: ' . print_r(debug_backtrace(), true));
256             return $s;
257         }
258
259         /*
260         *  // Placeholder for new rev of salmon which strips base64 padding.
261         *  // PHP base64_decode handles the un-padded input without requiring this step
262         *  // Uncomment if you find you need it.
263         *
264         *       $l = strlen($s);
265         *       if (!strpos($s,'=')) {
266         *               $m = $l % 4;
267         *               if ($m == 2)
268         *                       $s .= '==';
269         *               if ($m == 3)
270         *                       $s .= '=';
271         *       }
272         *
273         */
274
275         return base64_decode(strtr($s, '-_', '+/'));
276     }
277
278     /**
279      * @brief Normalize url
280      *
281      * @param string $url   URL to be normalized.
282      * 
283      * @return string   Normalized URL.
284      */
285     public static function normaliseLink($url)
286     {
287         $ret = str_replace(['https:', '//www.'], ['http:', '//'], $url);
288         return rtrim($ret, '/');
289     }
290
291     /**
292      * @brief Normalize OpenID identity
293      * 
294      * @param string $s OpenID Identity
295      * 
296      * @return string   normalized OpenId Identity
297      */
298     public static function normaliseOpenID($s)
299     {
300         return trim(str_replace(['http://', 'https://'], ['', ''], $s), '/');
301     }
302
303     /**
304      * @brief Compare two URLs to see if they are the same, but ignore
305      * slight but hopefully insignificant differences such as if one
306      * is https and the other isn't, or if one is www.something and
307      * the other isn't - and also ignore case differences.
308      *
309      * @param string $a first url
310      * @param string $b second url
311      * @return boolean True if the URLs match, otherwise False
312      *
313      */
314     public static function compareLink($a, $b)
315     {
316         return (strcasecmp(self::normaliseLink($a), self::normaliseLink($b)) === 0);
317     }
318
319
320         /**
321          * Ensures the provided URI has its query string punctuation in order.
322          *
323          * @param string $uri
324          * @return string
325          */
326         public static function ensureQueryParameter($uri)
327         {
328                 if (strpos($uri, '?') === false && ($pos = strpos($uri, '&')) !== false) {
329                         $uri = substr($uri, 0, $pos) . '?' . substr($uri, $pos + 1);
330                 }
331
332                 return $uri;
333         }
334
335
336         /**
337          * Check if the trimmed provided string is starting with one of the provided characters
338          *
339          * @param string $string
340          * @param array  $chars
341          * @return bool
342          */
343         public static function startsWith($string, array $chars)
344         {
345                 $return = in_array(substr(trim($string), 0, 1), $chars);
346
347                 return $return;
348         }
349
350         /**
351          * Returns the regular expression string to match URLs in a given text
352          *
353          * @return string
354          * @see https://daringfireball.net/2010/07/improved_regex_for_matching_urls
355          */
356         public static function autoLinkRegEx()
357         {
358                 return '@(?xi)
359 (?<![=\'\]"/])          # Not preceded by [, =, \', ], ", /
360 \b
361 (                       # Capture 1: entire matched URL
362   https?://                 # http or https protocol
363   (?:
364     [^/.][^/]+[.][^/]+/?    # looks like domain name followed by a slash
365   )
366   (?:                       # One or more:
367     [^\s()<>]+                  # Run of non-space, non-()<>
368     |                           #   or
369     \(([^\s()<>]+|(\([^\s()<>]+\)))*\)  # balanced parens, up to 2 levels
370     |                               #   or
371     [^\s`!()\[\]{};:\'".,<>?«»“”‘’]        # not a space or one of these punct chars
372   )*
373 )@';
374         }
375 }