]> git.mxchange.org Git - friendica.git/blob - src/Util/Strings.php
Catch exceptions for Worker::AddContact()
[friendica.git] / src / Util / Strings.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2022, the Friendica project
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as
9  * published by the Free Software Foundation, either version 3 of the
10  * License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19  *
20  */
21
22 namespace Friendica\Util;
23
24 use Friendica\Content\ContactSelector;
25 use Friendica\Core\Logger;
26
27 /**
28  * This class handles string functions
29  */
30 class Strings
31 {
32         /**
33          * Generates a pseudo-random string of hexadecimal characters
34          *
35          * @param int $size
36          * @return string
37          * @throws \Exception
38          */
39         public static function getRandomHex($size = 64)
40         {
41                 $byte_size = ceil($size / 2);
42
43                 $bytes = random_bytes($byte_size);
44
45                 $return = substr(bin2hex($bytes), 0, $size);
46
47                 return $return;
48         }
49
50         /**
51          * Checks, if the given string is a valid hexadecimal code
52          *
53          * @param string $hexCode
54          *
55          * @return bool
56          */
57         public static function isHex($hexCode)
58         {
59                 return !empty($hexCode) ? @preg_match("/^[a-f0-9]{2,}$/i", $hexCode) && !(strlen($hexCode) & 1) : false;
60         }
61
62         /**
63          * Use this on "body" or "content" input where angle chars shouldn't be removed,
64          * and allow them to be safely displayed.
65          * @param string $string
66          *
67          * @return string
68          */
69         public static function escapeHtml($string)
70         {
71                 return htmlspecialchars($string, ENT_COMPAT, 'UTF-8', false);
72         }
73
74         /**
75          * Generate a string that's random, but usually pronounceable. Used to generate initial passwords
76          *
77          * @param int $len      length
78          *
79          * @return string
80          */
81         public static function getRandomName($len)
82         {
83                 if ($len <= 0) {
84                         return '';
85                 }
86
87                 $vowels = ['a', 'a', 'ai', 'au', 'e', 'e', 'e', 'ee', 'ea', 'i', 'ie', 'o', 'ou', 'u'];
88
89                 if (mt_rand(0, 5) == 4) {
90                         $vowels[] = 'y';
91                 }
92
93                 $cons = [
94                         'b', 'bl', 'br',
95                         'c', 'ch', 'cl', 'cr',
96                         'd', 'dr',
97                         'f', 'fl', 'fr',
98                         'g', 'gh', 'gl', 'gr',
99                         'h',
100                         'j',
101                         'k', 'kh', 'kl', 'kr',
102                         'l',
103                         'm',
104                         'n',
105                         'p', 'ph', 'pl', 'pr',
106                         'qu',
107                         'r', 'rh',
108                         's', 'sc', 'sh', 'sm', 'sp', 'st',
109                         't', 'th', 'tr',
110                         'v',
111                         'w', 'wh',
112                         'x',
113                         'z', 'zh'
114                 ];
115
116                 $midcons = [
117                         'ck', 'ct', 'gn', 'ld', 'lf', 'lm', 'lt', 'mb', 'mm', 'mn', 'mp',
118                         'nd', 'ng', 'nk', 'nt', 'rn', 'rp', 'rt'
119                 ];
120
121                 $noend = [
122                         'bl', 'br', 'cl', 'cr', 'dr', 'fl', 'fr', 'gl', 'gr',
123                         'kh', 'kl', 'kr', 'mn', 'pl', 'pr', 'rh', 'tr', 'qu', 'wh', 'q'
124                 ];
125
126                 $start = mt_rand(0, 2);
127                 if ($start == 0) {
128                         $table = $vowels;
129                 } else {
130                         $table = $cons;
131                 }
132
133                 $word = '';
134
135                 for ($x = 0; $x < $len; $x++) {
136                         $r = mt_rand(0, count($table) - 1);
137                         $word .= $table[$r];
138
139                         if ($table == $vowels) {
140                                 $table = array_merge($cons, $midcons);
141                         } else {
142                                 $table = $vowels;
143                         }
144                 }
145
146                 $word = substr($word, 0, $len);
147
148                 foreach ($noend as $noe) {
149                         $noelen = strlen($noe);
150                         if ((strlen($word) > $noelen) && (substr($word, -$noelen) == $noe)) {
151                                 $word = self::getRandomName($len);
152                                 break;
153                         }
154                 }
155
156                 return $word;
157         }
158
159         /**
160          * Translate and format the network name of a contact
161          *
162          * @param string $network Network name of the contact (e.g. dfrn, rss and so on)
163          * @param string $url     The contact url
164          *
165          * @return string Formatted network name
166          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
167          */
168         public static function formatNetworkName($network, $url = '')
169         {
170                 if ($network != '') {
171                         if ($url != '') {
172                                 $network_name = '<a href="' . $url . '">' . ContactSelector::networkToName($network, $url) . '</a>';
173                         } else {
174                                 $network_name = ContactSelector::networkToName($network);
175                         }
176
177                         return $network_name;
178                 }
179         }
180
181         /**
182          * Remove indentation from a text
183          *
184          * @param string $text  String to be transformed.
185          * @param string $chr   Optional. Indentation tag. Default tab (\t).
186          * @param int    $count Optional. Default null.
187          *
188          * @return string               Transformed string.
189          */
190         public static function deindent($text, $chr = "[\t ]", $count = NULL)
191         {
192                 $lines = explode("\n", $text);
193
194                 if (is_null($count)) {
195                         $m = [];
196                         $k = 0;
197                         while ($k < count($lines) && strlen($lines[$k]) == 0) {
198                                 $k++;
199                         }
200                         preg_match("|^" . $chr . "*|", $lines[$k], $m);
201                         $count = strlen($m[0]);
202                 }
203
204                 for ($k = 0; $k < count($lines); $k++) {
205                         $lines[$k] = preg_replace("|^" . $chr . "{" . $count . "}|", "", $lines[$k]);
206                 }
207
208                 return implode("\n", $lines);
209         }
210
211         /**
212          * Get byte size returned in a Data Measurement (KB, MB, GB)
213          *
214          * @param int $bytes    The number of bytes to be measured
215          * @param int $precision        Optional. Default 2.
216          *
217          * @return string       Size with measured units.
218          */
219         public static function formatBytes($bytes, $precision = 2)
220         {
221                 $units = ['B', 'KB', 'MB', 'GB', 'TB'];
222                 $bytes = max($bytes, 0);
223                 $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
224                 $pow = min($pow, count($units) - 1);
225                 $bytes /= pow(1024, $pow);
226
227                 return round($bytes, $precision) . ' ' . $units[$pow];
228         }
229
230         /**
231          * Protect percent characters in sprintf calls
232          *
233          * @param string $s String to transform.
234          *
235          * @return string       Transformed string.
236          */
237         public static function protectSprintf($s)
238         {
239                 return str_replace('%', '%%', $s);
240         }
241
242         /**
243          * Base64 Encode URL and translate +/ to -_ Optionally strip padding.
244          *
245          * @param string $s                                     URL to encode
246          * @param boolean $strip_padding        Optional. Default false
247          *
248          * @return string       Encoded URL
249          */
250         public static function base64UrlEncode($s, $strip_padding = false)
251         {
252                 $s = strtr(base64_encode($s), '+/', '-_');
253
254                 if ($strip_padding) {
255                         $s = str_replace('=', '', $s);
256                 }
257
258                 return $s;
259         }
260
261         /**
262          * Decode Base64 Encoded URL and translate -_ to +/
263          * @param string $s URL to decode
264          *
265          * @return string       Decoded URL
266          * @throws \Exception
267          */
268         public static function base64UrlDecode($s)
269         {
270                 if (is_array($s)) {
271                         Logger::notice('base64url_decode: illegal input: ', ['backtrace' => debug_backtrace()]);
272                         return $s;
273                 }
274
275                 /*
276                 *  // Placeholder for new rev of salmon which strips base64 padding.
277                 *  // PHP base64_decode handles the un-padded input without requiring this step
278                 *  // Uncomment if you find you need it.
279                 *
280                 *       $l = strlen($s);
281                 *       if (!strpos($s,'=')) {
282                 *               $m = $l % 4;
283                 *               if ($m == 2)
284                 *                       $s .= '==';
285                 *               if ($m == 3)
286                 *                       $s .= '=';
287                 *       }
288                 *
289                 */
290
291                 return base64_decode(strtr($s, '-_', '+/'));
292         }
293
294         /**
295          * Normalize url
296          *
297          * @param string $url   URL to be normalized.
298          *
299          * @return string       Normalized URL.
300          */
301         public static function normaliseLink($url)
302         {
303                 $ret = str_replace(['https:', '//www.'], ['http:', '//'], $url);
304                 return rtrim($ret, '/');
305         }
306
307         /**
308          * Normalize OpenID identity
309          *
310          * @param string $s OpenID Identity
311          *
312          * @return string       normalized OpenId Identity
313          */
314         public static function normaliseOpenID($s)
315         {
316                 return trim(str_replace(['http://', 'https://'], ['', ''], $s), '/');
317         }
318
319         /**
320          * Compare two URLs to see if they are the same, but ignore
321          * slight but hopefully insignificant differences such as if one
322          * is https and the other isn't, or if one is www.something and
323          * the other isn't - and also ignore case differences.
324          *
325          * @param string $a first url
326          * @param string $b second url
327          * @return boolean True if the URLs match, otherwise False
328          *
329          */
330         public static function compareLink($a, $b)
331         {
332                 return (strcasecmp(self::normaliseLink($a), self::normaliseLink($b)) === 0);
333         }
334
335         /**
336          * Ensures the provided URI has its query string punctuation in order.
337          *
338          * @param string $uri
339          * @return string
340          */
341         public static function ensureQueryParameter($uri)
342         {
343                 if (strpos($uri, '?') === false && ($pos = strpos($uri, '&')) !== false) {
344                         $uri = substr($uri, 0, $pos) . '?' . substr($uri, $pos + 1);
345                 }
346
347                 return $uri;
348         }
349
350         /**
351          * Check if the trimmed provided string is starting with one of the provided characters
352          *
353          * @param string $string
354          * @param array  $chars
355          * @return bool
356          */
357         public static function startsWithChars($string, array $chars)
358         {
359                 $return = in_array(substr(trim($string), 0, 1), $chars);
360
361                 return $return;
362         }
363
364         /**
365          * Check if the first string starts with the second
366          *
367          * @see http://maettig.com/code/php/php-performance-benchmarks.php#startswith
368          * @param string $string
369          * @param string $start
370          * @return bool
371          */
372         public static function startsWith(string $string, string $start)
373         {
374                 $return = substr_compare($string, $start, 0, strlen($start)) === 0;
375
376                 return $return;
377         }
378
379         /**
380          * Checks if the first string ends with the second
381          *
382          * @see http://maettig.com/code/php/php-performance-benchmarks.php#endswith
383          * @param string $string
384          * @param string $end
385          * @return bool
386          */
387         public static function endsWith(string $string, string $end)
388         {
389                 $return = substr_compare($string, $end, -strlen($end)) === 0;
390
391                 return $return;
392         }
393
394         /**
395          * Returns the regular expression string to match URLs in a given text
396          *
397          * @return string
398          * @see https://daringfireball.net/2010/07/improved_regex_for_matching_urls
399          */
400         public static function autoLinkRegEx()
401         {
402                 return '@
403 (?<![=\'\]"/])                  # Not preceded by [, =, \', ], ", /
404 \b
405 (                                                          # Capture 1: entire matched URL
406   https?://                                                        # http or https protocol
407   (?:
408         [^/\s\xA0`!()\[\]{};:\'",<>?«»“”‘’.]    # Domain can\'t start with a .
409         [^/\s\xA0`!()\[\]{};:\'",<>?«»“”‘’]+    # Domain can\'t end with a .
410         \.
411         [^/\s\xA0`!()\[\]{};:\'".,<>?«»“”‘’]+/? # Followed by a slash
412   )
413   (?:                                                              # One or more:
414         [^\s\xA0()<>]+                                             # Run of non-space, non-()<>
415         |                                                                  #   or
416         \(([^\s\xA0()<>]+|(\([^\s()<>]+\)))*\) # balanced parens, up to 2 levels
417         |                                                                  #   or
418         [^\s\xA0`!()\[\]{};:\'".,<>?«»“”‘’]    # not a space or one of these punct chars
419   )*
420 )@xiu';
421         }
422
423         /**
424          * Ensures a single path item doesn't contain any path-traversing characters
425          *
426          * @see https://stackoverflow.com/a/46097713
427          * @param string $pathItem
428          * @return string
429          */
430         public static function sanitizeFilePathItem($pathItem)
431         {
432                 $pathItem = str_replace('/', '_', $pathItem);
433                 $pathItem = str_replace('\\', '_', $pathItem);
434                 $pathItem = str_replace(DIRECTORY_SEPARATOR, '_', $pathItem); // In case it does not equal the standard values
435
436                 return $pathItem;
437         }
438
439         /**
440          * Multi-byte safe implementation of substr_replace where $start and $length are character offset and count rather
441          * than byte offset and counts.
442          *
443          * Depends on mbstring, use default encoding.
444          *
445          * @param string   $string
446          * @param string   $replacement
447          * @param int      $start
448          * @param int|null $length
449          * @return string
450          * @see substr_replace()
451          */
452         public static function substringReplace(string $string, string $replacement, int $start, int $length = null)
453         {
454                 $string_length = mb_strlen($string);
455
456                 $length = $length ?? $string_length;
457
458                 if ($start < 0) {
459                         $start = max(0, $string_length + $start);
460                 } else if ($start > $string_length) {
461                         $start = $string_length;
462                 }
463
464                 if ($length < 0) {
465                         $length = max(0, $string_length - $start + $length);
466                 } else if ($length > $string_length) {
467                         $length = $string_length;
468                 }
469
470                 if (($start + $length) > $string_length) {
471                         $length = $string_length - $start;
472                 }
473
474                 return mb_substr($string, 0, $start) . $replacement . mb_substr($string, $start + $length, $string_length - $start - $length);
475         }
476
477         /**
478          * Perform a custom function on a text after having escaped blocks matched by the provided regular expressions.
479          * Only full matches are used, capturing group are ignored.
480          *
481          * To change the provided text, the callback function needs to return it and this function will return the modified
482          * version as well after having restored the escaped blocks.
483          *
484          * @param string   $text
485          * @param string   $regex
486          * @param callable $callback
487          * @return string
488          */
489         public static function performWithEscapedBlocks(string $text, string $regex, callable $callback): string
490         {
491                 // Enables nested use
492                 $executionId = random_int(PHP_INT_MAX / 10, PHP_INT_MAX);
493
494                 $blocks = [];
495
496                 $text = preg_replace_callback($regex,
497                         function ($matches) use ($executionId, &$blocks) {
498                                 $return = '«block-' . $executionId . '-' . count($blocks) . '»';
499
500                                 $blocks[] = $matches[0];
501
502                                 return $return;
503                         },
504                         $text
505                 );
506
507                 $text = $callback($text) ?? '';
508
509                 // Restore code blocks
510                 $text = preg_replace_callback('/«block-' . $executionId . '-([0-9]+)»/iU',
511                         function ($matches) use ($blocks) {
512                                 $return = $matches[0];
513                                 if (isset($blocks[intval($matches[1])])) {
514                                         $return = $blocks[$matches[1]];
515                                 }
516                                 return $return;
517                         },
518                         $text
519                 );
520
521                 return $text;
522         }
523 }