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