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