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