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