]> git.mxchange.org Git - friendica.git/blob - src/Util/Strings.php
5988cc89bcef708f8c3907c890447d3c6677e17b
[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 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                 $units = ['B', 'KB', 'MB', 'GB', 'TB'];
224                 $bytes = max($bytes, 0);
225                 $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
226                 $pow = min($pow, count($units) - 1);
227                 $bytes /= pow(1024, $pow);
228
229                 return round($bytes, $precision) . ' ' . $units[$pow];
230         }
231
232         /**
233          * Protect percent characters in sprintf calls
234          *
235          * @param string $s String to transform.
236          * @return string       Transformed string.
237          */
238         public static function protectSprintf(string $s): string
239         {
240                 return str_replace('%', '%%', $s);
241         }
242
243         /**
244          * Base64 Encode URL and translate +/ to -_ Optionally strip padding.
245          *
246          * @param string $s                                     URL to encode
247          * @param boolean $strip_padding        Optional. Default false
248          * @return string       Encoded URL
249          * @see https://web.archive.org/web/20160506073138/http://salmon-protocol.googlecode.com:80/svn/trunk/draft-panzer-magicsig-01.html#params
250          */
251         public static function base64UrlEncode(string $s, bool $strip_padding = false): string
252         {
253                 if ($strip_padding) {
254                         $s = Base64::encodeUnpadded($s);
255                 } else {
256                         $s = Base64::encode($s);
257                 }
258
259                 return strtr($s, '+/', '-_');
260         }
261
262         /**
263          * Decode Base64 Encoded URL and translate -_ to +/
264          *
265          * @param string $s URL to decode
266          * @return string       Decoded URL
267          * @throws \Exception
268          * @see https://web.archive.org/web/20160506073138/http://salmon-protocol.googlecode.com:80/svn/trunk/draft-panzer-magicsig-01.html#params
269          */
270         public static function base64UrlDecode(string $s): string
271         {
272                 return Base64::decode(strtr($s, '-_', '+/'));
273         }
274
275         /**
276          * Normalize url
277          *
278          * @param string $url   URL to be normalized.
279          * @return string       Normalized URL.
280          */
281         public static function normaliseLink(string $url): string
282         {
283                 $ret = str_replace(['https:', '//www.'], ['http:', '//'], $url);
284                 return rtrim($ret, '/');
285         }
286
287         /**
288          * Normalize OpenID identity
289          *
290          * @param string $s OpenID Identity
291          * @return string       normalized OpenId Identity
292          */
293         public static function normaliseOpenID(string $s): string
294         {
295                 return trim(str_replace(['http://', 'https://'], ['', ''], $s), '/');
296         }
297
298         /**
299          * Compare two URLs to see if they are the same, but ignore
300          * slight but hopefully insignificant differences such as if one
301          * is https and the other isn't, or if one is www.something and
302          * the other isn't - and also ignore case differences.
303          *
304          * @param string $a first url
305          * @param string $b second url
306          * @return boolean True if the URLs match, otherwise False
307          *
308          */
309         public static function compareLink(string $a, string $b): bool
310         {
311                 return (strcasecmp(self::normaliseLink($a), self::normaliseLink($b)) === 0);
312         }
313
314         /**
315          * Ensures the provided URI has its query string punctuation in order.
316          *
317          * @param string $uri
318          * @return string
319          */
320         public static function ensureQueryParameter(string $uri): string
321         {
322                 if (strpos($uri, '?') === false && ($pos = strpos($uri, '&')) !== false) {
323                         $uri = substr($uri, 0, $pos) . '?' . substr($uri, $pos + 1);
324                 }
325
326                 return $uri;
327         }
328
329         /**
330          * Check if the trimmed provided string is starting with one of the provided characters
331          *
332          * @param string $string
333          * @param array $chars
334          *
335          * @return bool
336          */
337         public static function startsWithChars(string $string, array $chars): bool
338         {
339                 $return = in_array(substr(trim($string), 0, 1), $chars);
340
341                 return $return;
342         }
343
344         /**
345          * Check if the first string starts with the second
346          *
347          * @see http://maettig.com/code/php/php-performance-benchmarks.php#startswith
348          * @param string $string
349          * @param string $start
350          * @return bool
351          */
352         public static function startsWith(string $string, string $start): bool
353         {
354                 $return = substr_compare($string, $start, 0, strlen($start)) === 0;
355
356                 return $return;
357         }
358
359         /**
360          * Checks if the first string ends with the second
361          *
362          * @see http://maettig.com/code/php/php-performance-benchmarks.php#endswith
363          * @param string $string
364          * @param string $end
365          *
366          * @return bool
367          */
368         public static function endsWith(string $string, string $end): bool
369         {
370                 return (substr_compare($string, $end, -strlen($end)) === 0);
371         }
372
373         /**
374          * Returns the regular expression string to match URLs in a given text
375          *
376          * @return string
377          * @see https://daringfireball.net/2010/07/improved_regex_for_matching_urls
378          */
379         public static function autoLinkRegEx(): string
380         {
381                 return '@
382 (?<![=\'\]"/])                  # Not preceded by [, =, \', ], ", /
383 \b
384 (                                                          # Capture 1: entire matched URL
385   https?://                                                        # http or https protocol
386   (?:
387         [^/\s\xA0`!()\[\]{};:\'",<>?«»“”‘’.]    # Domain can\'t start with a .
388         [^/\s\xA0`!()\[\]{};:\'",<>?«»“”‘’]+    # Domain can\'t end with a .
389         \.
390         [^/\s\xA0`!()\[\]{};:\'".,<>?«»“”‘’]+/? # Followed by a slash
391   )
392   (?:                                                              # One or more:
393         [^\s\xA0()<>]+                                             # Run of non-space, non-()<>
394         |                                                                  #   or
395         \(([^\s\xA0()<>]+|(\([^\s()<>]+\)))*\) # balanced parens, up to 2 levels
396         |                                                                  #   or
397         [^\s\xA0`!()\[\]{};:\'".,<>?«»“”‘’]    # not a space or one of these punct chars
398   )*
399 )@xiu';
400         }
401
402         /**
403          * Ensures a single path item doesn't contain any path-traversing characters
404          *
405          * @param string $pathItem
406          *
407          * @see https://stackoverflow.com/a/46097713
408          * @return string
409          */
410         public static function sanitizeFilePathItem(string $pathItem): string
411         {
412                 $pathItem = str_replace('/', '_', $pathItem);
413                 $pathItem = str_replace('\\', '_', $pathItem);
414                 $pathItem = str_replace(DIRECTORY_SEPARATOR, '_', $pathItem); // In case it does not equal the standard values
415
416                 return $pathItem;
417         }
418
419         /**
420          * Multi-byte safe implementation of substr_replace where $start and $length are character offset and count rather
421          * than byte offset and counts.
422          *
423          * Depends on mbstring, use default encoding.
424          *
425          * @param string   $string
426          * @param string   $replacement
427          * @param int      $start
428          * @param int|null $length
429          *
430          * @return string
431          * @see substr_replace()
432          */
433         public static function substringReplace(string $string, string $replacement, int $start, int $length = null): string
434         {
435                 $string_length = mb_strlen($string);
436
437                 $length = $length ?? $string_length;
438
439                 if ($start < 0) {
440                         $start = max(0, $string_length + $start);
441                 } else if ($start > $string_length) {
442                         $start = $string_length;
443                 }
444
445                 if ($length < 0) {
446                         $length = max(0, $string_length - $start + $length);
447                 } else if ($length > $string_length) {
448                         $length = $string_length;
449                 }
450
451                 if (($start + $length) > $string_length) {
452                         $length = $string_length - $start;
453                 }
454
455                 return mb_substr($string, 0, $start) . $replacement . mb_substr($string, $start + $length, $string_length - $start - $length);
456         }
457
458         /**
459          * Perform a custom function on a text after having escaped blocks matched by the provided regular expressions.
460          * Only full matches are used, capturing group are ignored.
461          *
462          * To change the provided text, the callback function needs to return it and this function will return the modified
463          * version as well after having restored the escaped blocks.
464          *
465          * @param string   $text
466          * @param string   $regex
467          * @param callable $callback
468          *
469          * @return string
470          */
471         public static function performWithEscapedBlocks(string $text, string $regex, callable $callback): string
472         {
473                 // Enables nested use
474                 $executionId = random_int(PHP_INT_MAX / 10, PHP_INT_MAX);
475
476                 $blocks = [];
477
478                 $text = preg_replace_callback($regex,
479                         function ($matches) use ($executionId, &$blocks) {
480                                 $return = '«block-' . $executionId . '-' . count($blocks) . '»';
481
482                                 $blocks[] = $matches[0];
483
484                                 return $return;
485                         },
486                         $text
487                 );
488
489                 $text = $callback($text) ?? '';
490
491                 // Restore code blocks
492                 $text = preg_replace_callback('/«block-' . $executionId . '-([0-9]+)»/iU',
493                         function ($matches) use ($blocks) {
494                                 $return = $matches[0];
495                                 if (isset($blocks[intval($matches[1])])) {
496                                         $return = $blocks[$matches[1]];
497                                 }
498                                 return $return;
499                         },
500                         $text
501                 );
502
503                 return $text;
504         }
505
506         /**
507          * This function converts a PHP's shorhand notation string for file sizes in to an integer number of total bytes.
508          * For example: The string for shorthand notation of '2M' (which is 2,097,152 Bytes) is converted to 2097152
509          * @see https://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes
510          * @param string $shorthand
511          * @return int
512          */
513         public static function getBytesFromShorthand(string $shorthand): int
514         {
515                 $shorthand = trim($shorthand);
516
517                 if (is_numeric($shorthand)) {
518                         return $shorthand;
519                 }
520
521                 $last      = strtolower($shorthand[strlen($shorthand)-1]);
522                 $shorthand = substr($shorthand, 0, -1);
523
524                 switch($last) {
525                         case 'g':
526                                 $shorthand *= 1024;
527                         case 'm':
528                                 $shorthand *= 1024;
529                         case 'k':
530                                 $shorthand *= 1024;
531                 }
532
533                 return $shorthand;
534         }
535
536 }