]> git.mxchange.org Git - friendica.git/blob - src/Util/Strings.php
Changes:
[friendica.git] / src / Util / Strings.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2024, 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 ParagonIE\ConstantTime\Base64;
27
28 /**
29  * This class handles string functions
30  */
31 class Strings
32 {
33         /**
34          * Generates a pseudo-random string of hexadecimal characters
35          *
36          * @param int $size Size of string (default: 64)
37          *
38          * @return string Pseudo-random string
39          * @throws \Exception
40          */
41         public static function getRandomHex(int $size = 64): string
42         {
43                 $byte_size = ceil($size / 2);
44
45                 $bytes = random_bytes($byte_size);
46
47                 $return = substr(bin2hex($bytes), 0, $size);
48
49                 return $return;
50         }
51
52         /**
53          * Checks, if the given string is a valid hexadecimal code
54          *
55          * @param string $hexCode
56          * @return bool
57          */
58         public static function isHex(string $hexCode): bool
59         {
60                 return !empty($hexCode) ? @preg_match("/^[a-f0-9]{2,}$/i", $hexCode) && !(strlen($hexCode) & 1) : false;
61         }
62
63         /**
64          * Use this on "body" or "content" input where angle chars shouldn't be removed,
65          * and allow them to be safely displayed.
66          * @param string $string
67          *
68          * @return string
69          */
70         public static function escapeHtml($string)
71         {
72                 return htmlspecialchars($string, ENT_COMPAT, 'UTF-8', false);
73         }
74
75         /**
76          * Generate a string that's random, but usually pronounceable. Used to generate initial passwords
77          *
78          * @param int $len      length
79          * @return string
80          */
81         public static function getRandomName(int $len): string
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(string $network, string $url = ''): string
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                 return '';
181         }
182
183         /**
184          * Remove indentation from a text
185          *
186          * @param string $text  String to be transformed.
187          * @param string $chr   Optional. Indentation tag. Default tab (\t).
188          * @param int    $count Optional. Default null.
189          *
190          * @return string               Transformed string.
191          */
192         public static function deindent(string $text, string $chr = "[\t ]", int $count = null): string
193         {
194                 $lines = explode("\n", $text);
195
196                 if (is_null($count)) {
197                         $m = [];
198                         $k = 0;
199                         while ($k < count($lines) && strlen($lines[$k]) == 0) {
200                                 $k++;
201                         }
202                         preg_match("|^" . $chr . "*|", $lines[$k], $m);
203                         $count = strlen($m[0]);
204                 }
205
206                 for ($k = 0; $k < count($lines); $k++) {
207                         $lines[$k] = preg_replace("|^" . $chr . "{" . $count . "}|", "", $lines[$k]);
208                 }
209
210                 return implode("\n", $lines);
211         }
212
213         /**
214          * Get byte size returned in a Data Measurement (KB, MB, GB)
215          *
216          * @param int $bytes    The number of bytes to be measured
217          * @param int $precision        Optional. Default 2.
218          *
219          * @return string       Size with measured units.
220          */
221         public static function formatBytes(int $bytes, int $precision = 2): string
222         {
223                 // If this method is called for an infinite (== unlimited) amount of bytes:
224                 if ($bytes == INF) {
225                         return INF;
226                 }
227
228                 $units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
229                 $bytes = max($bytes, 0);
230                 $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
231                 $pow = min($pow, count($units) - 1);
232                 $bytes /= pow(1024, $pow);
233
234                 return round($bytes, $precision) . ' ' . $units[$pow];
235         }
236
237         /**
238          * Protect percent characters in sprintf calls
239          *
240          * @param string $s String to transform.
241          * @return string       Transformed string.
242          */
243         public static function protectSprintf(string $s): string
244         {
245                 return str_replace('%', '%%', $s);
246         }
247
248         /**
249          * Base64 Encode URL and translate +/ to -_ Optionally strip padding.
250          *
251          * @param string $s                                     URL to encode
252          * @param boolean $strip_padding        Optional. Default false
253          * @return string       Encoded URL
254          * @see https://web.archive.org/web/20160506073138/http://salmon-protocol.googlecode.com:80/svn/trunk/draft-panzer-magicsig-01.html#params
255          */
256         public static function base64UrlEncode(string $s, bool $strip_padding = false): string
257         {
258                 if ($strip_padding) {
259                         $s = Base64::encodeUnpadded($s);
260                 } else {
261                         $s = Base64::encode($s);
262                 }
263
264                 return strtr($s, '+/', '-_');
265         }
266
267         /**
268          * Decode Base64 Encoded URL and translate -_ to +/
269          *
270          * @param string $s URL to decode
271          * @return string       Decoded URL
272          * @throws \Exception
273          * @see https://web.archive.org/web/20160506073138/http://salmon-protocol.googlecode.com:80/svn/trunk/draft-panzer-magicsig-01.html#params
274          */
275         public static function base64UrlDecode(string $s): string
276         {
277                 return Base64::decode(strtr($s, '-_', '+/'));
278         }
279
280         /**
281          * Normalize url
282          *
283          * @param string $url   URL to be normalized.
284          * @return string       Normalized URL.
285          */
286         public static function normaliseLink(string $url): string
287         {
288                 $ret = str_replace(['https:', '//www.'], ['http:', '//'], $url);
289                 return rtrim($ret, '/');
290         }
291
292         /**
293          * Normalize OpenID identity
294          *
295          * @param string $s OpenID Identity
296          * @return string       normalized OpenId Identity
297          */
298         public static function normaliseOpenID(string $s): string
299         {
300                 return trim(str_replace(['http://', 'https://'], ['', ''], $s), '/');
301         }
302
303         /**
304          * 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(string $a, string $b): bool
315         {
316                 return (strcasecmp(self::normaliseLink($a), self::normaliseLink($b)) === 0);
317         }
318
319         /**
320          * Ensures the provided URI has its query string punctuation in order.
321          *
322          * @param string $uri
323          * @return string
324          */
325         public static function ensureQueryParameter(string $uri): string
326         {
327                 if (strpos($uri, '?') === false && ($pos = strpos($uri, '&')) !== false) {
328                         $uri = substr($uri, 0, $pos) . '?' . substr($uri, $pos + 1);
329                 }
330
331                 return $uri;
332         }
333
334         /**
335          * Check if the trimmed provided string is starting with one of the provided characters
336          *
337          * @param string $string
338          * @param array $chars
339          *
340          * @return bool
341          */
342         public static function startsWithChars(string $string, array $chars): bool
343         {
344                 $return = in_array(substr(trim($string), 0, 1), $chars);
345
346                 return $return;
347         }
348
349         /**
350          * Check if the first string starts with the second
351          *
352          * @see http://maettig.com/code/php/php-performance-benchmarks.php#startswith
353          * @param string $string
354          * @param string $start
355          * @return bool
356          */
357         public static function startsWith(string $string, string $start): bool
358         {
359                 $return = substr_compare($string, $start, 0, strlen($start)) === 0;
360
361                 return $return;
362         }
363
364         /**
365          * Checks if the first string ends with the second
366          *
367          * @see http://maettig.com/code/php/php-performance-benchmarks.php#endswith
368          * @param string $string
369          * @param string $end
370          *
371          * @return bool
372          */
373         public static function endsWith(string $string, string $end): bool
374         {
375                 return (substr_compare($string, $end, -strlen($end)) === 0);
376         }
377
378         /**
379          * Returns the regular expression string to match URLs in a given text
380          *
381          * @return string
382          */
383         public static function autoLinkRegEx(): string
384         {
385                 return '@
386 (?<![=\'\]"/]) # Not preceded by [, =, \', ], ", /
387 \b
388 (              # Capture 1: entire matched URL
389   ' . self::linkRegEx() . '
390 )@xiu';
391         }
392
393         /**
394          * Returns the regular expression string to match only an HTTP URL
395          *
396          * @return string
397          */
398         public static function onlyLinkRegEx(): string
399         {
400                 return '@^' . self::linkRegEx() . '$@xiu';
401         }
402
403         /**
404          * @return string
405          * @see https://daringfireball.net/2010/07/improved_regex_for_matching_urls
406          */
407         private static function linkRegEx(): string
408         {
409                 return 'https?://                   # http or https protocol
410   (?:
411         [^/\s\xA0`!()\[\]{};:\'",<>?«»“”‘’.]    # Domain can\'t start with a .
412         [^/\s\xA0`!()\[\]{};:\'",<>?«»“”‘’]+    # Domain can\'t end with a .
413         \.
414         [^/\s\xA0`!()\[\]{};:\'".,<>?«»“”‘’]+/? # Followed by a slash
415   )
416   (?:                                       # One or more:
417         [^\s\xA0()<>]+                            # Run of non-space, non-()<>
418         |                                         #   or
419         \(([^\s\xA0()<>]+|(\([^\s()<>]+\)))*\)    # balanced parens, up to 2 levels
420         |                                                                         #   or
421         [^\s\xA0`!()\[\]{};:\'".,<>?«»“”‘’]         # not a space or one of these punct chars
422   )*';
423         }
424
425         /**
426          * Ensures a single path item doesn't contain any path-traversing characters
427          *
428          * @param string $pathItem
429          *
430          * @see https://stackoverflow.com/a/46097713
431          * @return string
432          */
433         public static function sanitizeFilePathItem(string $pathItem): string
434         {
435                 $pathItem = str_replace('/', '_', $pathItem);
436                 $pathItem = str_replace('\\', '_', $pathItem);
437                 $pathItem = str_replace(DIRECTORY_SEPARATOR, '_', $pathItem); // In case it does not equal the standard values
438
439                 return $pathItem;
440         }
441
442         /**
443          * Multi-byte safe implementation of substr_replace where $start and $length are character offset and count rather
444          * than byte offset and counts.
445          *
446          * Depends on mbstring, use default encoding.
447          *
448          * @param string   $string
449          * @param string   $replacement
450          * @param int      $start
451          * @param int|null $length
452          *
453          * @return string
454          * @see substr_replace()
455          */
456         public static function substringReplace(string $string, string $replacement, int $start, int $length = null): string
457         {
458                 $string_length = mb_strlen($string);
459
460                 $length = $length ?? $string_length;
461
462                 if ($start < 0) {
463                         $start = max(0, $string_length + $start);
464                 } else if ($start > $string_length) {
465                         $start = $string_length;
466                 }
467
468                 if ($length < 0) {
469                         $length = max(0, $string_length - $start + $length);
470                 } else if ($length > $string_length) {
471                         $length = $string_length;
472                 }
473
474                 if (($start + $length) > $string_length) {
475                         $length = $string_length - $start;
476                 }
477
478                 return mb_substr($string, 0, $start) . $replacement . mb_substr($string, $start + $length, $string_length - $start - $length);
479         }
480
481         /**
482          * Perform a custom function on a text after having escaped blocks matched by the provided regular expressions.
483          * Only full matches are used, capturing group are ignored.
484          *
485          * To change the provided text, the callback function needs to return it and this function will return the modified
486          * version as well after having restored the escaped blocks.
487          *
488          * @param string   $text
489          * @param string   $regex
490          * @param callable $callback
491          *
492          * @return string
493          */
494         public static function performWithEscapedBlocks(string $text, string $regex, callable $callback): string
495         {
496                 // Enables nested use
497                 $executionId = random_int(PHP_INT_MAX / 10, PHP_INT_MAX);
498
499                 $blocks = [];
500
501                 $return = preg_replace_callback($regex,
502                         function ($matches) use ($executionId, &$blocks) {
503                                 $return = '«block-' . $executionId . '-' . count($blocks) . '»';
504
505                                 $blocks[] = $matches[0];
506
507                                 return $return;
508                         },
509                         $text
510                 );
511
512                 if (is_null($return)) {
513                         Logger::notice('Received null value from preg_replace_callback', ['text' => $text, 'regex' => $regex, 'blocks' => $blocks, 'executionId' => $executionId]);
514                 }
515
516                 $text = $callback($return ?? $text) ?? '';
517
518                 // Restore code blocks
519                 $text = preg_replace_callback('/«block-' . $executionId . '-([0-9]+)»/iU',
520                         function ($matches) use ($blocks) {
521                                 $return = $matches[0];
522                                 if (isset($blocks[intval($matches[1])])) {
523                                         $return = $blocks[$matches[1]];
524                                 }
525                                 return $return;
526                         },
527                         $text
528                 );
529
530                 return $text;
531         }
532
533         /**
534          * This function converts a file size string written in PHP's shorthand notation to an integer number of total bytes.
535          * For example: The string for shorthand notation of '2M' (which is 2,097,152 Bytes) is converted to 2097152
536          * @see https://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes
537          * @param string $shorthand
538          * @return int
539          */
540         public static function getBytesFromShorthand(string $shorthand): int
541         {
542                 $shorthand = trim($shorthand);
543
544                 if (is_numeric($shorthand)) {
545                         return $shorthand;
546                 }
547
548                 $last      = strtolower($shorthand[strlen($shorthand)-1]);
549                 $shorthand = substr($shorthand, 0, -1);
550
551                 switch($last) {
552                         case 'g':
553                                 $shorthand *= 1024;
554                         case 'm':
555                                 $shorthand *= 1024;
556                         case 'k':
557                                 $shorthand *= 1024;
558                 }
559
560                 return $shorthand;
561         }
562
563         /**
564          * Converts an URL in a nicer format (without the scheme and possibly shortened)
565          *
566          * @param string $url URL that is about to be reformatted
567          * @return string reformatted link
568          */
569         public static function getStyledURL(string $url): string
570         {
571                 $parts = parse_url($url);
572                 $scheme = [$parts['scheme'] . '://www.', $parts['scheme'] . '://'];
573                 $styled_url = str_replace($scheme, '', $url);
574
575                 if (strlen($styled_url) > 30) {
576                         $styled_url = substr($styled_url, 0, 30) . "…";
577                 }
578
579                 return $styled_url;
580         }
581 }