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