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