3 * @copyright Copyright (C) 2010-2023, the Friendica project
5 * @license GNU AGPL version 3 or any later version
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.
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.
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/>.
22 namespace Friendica\Util;
24 use Friendica\Content\ContactSelector;
25 use Friendica\Core\Logger;
26 use ParagonIE\ConstantTime\Base64;
29 * This class handles string functions
34 * Generates a pseudo-random string of hexadecimal characters
36 * @param int $size Size of string (default: 64)
38 * @return string Pseudo-random string
41 public static function getRandomHex(int $size = 64): string
43 $byte_size = ceil($size / 2);
45 $bytes = random_bytes($byte_size);
47 $return = substr(bin2hex($bytes), 0, $size);
53 * Checks, if the given string is a valid hexadecimal code
55 * @param string $hexCode
58 public static function isHex(string $hexCode): bool
60 return !empty($hexCode) ? @preg_match("/^[a-f0-9]{2,}$/i", $hexCode) && !(strlen($hexCode) & 1) : false;
64 * Use this on "body" or "content" input where angle chars shouldn't be removed,
65 * and allow them to be safely displayed.
66 * @param string $string
70 public static function escapeHtml($string)
72 return htmlspecialchars($string, ENT_COMPAT, 'UTF-8', false);
76 * Generate a string that's random, but usually pronounceable. Used to generate initial passwords
78 * @param int $len length
81 public static function getRandomName(int $len): string
87 $vowels = ['a', 'a', 'ai', 'au', 'e', 'e', 'e', 'ee', 'ea', 'i', 'ie', 'o', 'ou', 'u'];
89 if (mt_rand(0, 5) == 4) {
95 'c', 'ch', 'cl', 'cr',
98 'g', 'gh', 'gl', 'gr',
101 'k', 'kh', 'kl', 'kr',
105 'p', 'ph', 'pl', 'pr',
108 's', 'sc', 'sh', 'sm', 'sp', 'st',
117 'ck', 'ct', 'gn', 'ld', 'lf', 'lm', 'lt', 'mb', 'mm', 'mn', 'mp',
118 'nd', 'ng', 'nk', 'nt', 'rn', 'rp', 'rt'
122 'bl', 'br', 'cl', 'cr', 'dr', 'fl', 'fr', 'gl', 'gr',
123 'kh', 'kl', 'kr', 'mn', 'pl', 'pr', 'rh', 'tr', 'qu', 'wh', 'q'
126 $start = mt_rand(0, 2);
135 for ($x = 0; $x < $len; $x++) {
136 $r = mt_rand(0, count($table) - 1);
139 if ($table == $vowels) {
140 $table = array_merge($cons, $midcons);
146 $word = substr($word, 0, $len);
148 foreach ($noend as $noe) {
149 $noelen = strlen($noe);
150 if ((strlen($word) > $noelen) && (substr($word, -$noelen) == $noe)) {
151 $word = self::getRandomName($len);
160 * Translate and format the network name of a contact
162 * @param string $network Network name of the contact (e.g. dfrn, rss and so on)
163 * @param string $url The contact url
165 * @return string Formatted network name
166 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
168 public static function formatNetworkName(string $network, string $url = ''): string
170 if ($network != '') {
172 $network_name = '<a href="' . $url . '">' . ContactSelector::networkToName($network, $url) . '</a>';
174 $network_name = ContactSelector::networkToName($network);
177 return $network_name;
184 * Remove indentation from a text
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.
190 * @return string Transformed string.
192 public static function deindent(string $text, string $chr = "[\t ]", int $count = null): string
194 $lines = explode("\n", $text);
196 if (is_null($count)) {
199 while ($k < count($lines) && strlen($lines[$k]) == 0) {
202 preg_match("|^" . $chr . "*|", $lines[$k], $m);
203 $count = strlen($m[0]);
206 for ($k = 0; $k < count($lines); $k++) {
207 $lines[$k] = preg_replace("|^" . $chr . "{" . $count . "}|", "", $lines[$k]);
210 return implode("\n", $lines);
214 * Get byte size returned in a Data Measurement (KB, MB, GB)
216 * @param int $bytes The number of bytes to be measured
217 * @param int $precision Optional. Default 2.
219 * @return string Size with measured units.
221 public static function formatBytes(int $bytes, int $precision = 2): string
223 // If this method is called for an infinite (== unlimited) amount of bytes:
228 $units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
229 $bytes = max($bytes, 0);
230 $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
231 $pow = min($pow, count($units) - 1);
232 $bytes /= pow(1024, $pow);
234 return round($bytes, $precision) . ' ' . $units[$pow];
238 * Protect percent characters in sprintf calls
240 * @param string $s String to transform.
241 * @return string Transformed string.
243 public static function protectSprintf(string $s): string
245 return str_replace('%', '%%', $s);
249 * Base64 Encode URL and translate +/ to -_ Optionally strip padding.
251 * @param string $s URL to encode
252 * @param boolean $strip_padding Optional. Default false
253 * @return string Encoded URL
254 * @see https://web.archive.org/web/20160506073138/http://salmon-protocol.googlecode.com:80/svn/trunk/draft-panzer-magicsig-01.html#params
256 public static function base64UrlEncode(string $s, bool $strip_padding = false): string
258 if ($strip_padding) {
259 $s = Base64::encodeUnpadded($s);
261 $s = Base64::encode($s);
264 return strtr($s, '+/', '-_');
268 * Decode Base64 Encoded URL and translate -_ to +/
270 * @param string $s URL to decode
271 * @return string Decoded URL
273 * @see https://web.archive.org/web/20160506073138/http://salmon-protocol.googlecode.com:80/svn/trunk/draft-panzer-magicsig-01.html#params
275 public static function base64UrlDecode(string $s): string
277 return Base64::decode(strtr($s, '-_', '+/'));
283 * @param string $url URL to be normalized.
284 * @return string Normalized URL.
286 public static function normaliseLink(string $url): string
288 $ret = str_replace(['https:', '//www.'], ['http:', '//'], $url);
289 return rtrim($ret, '/');
293 * Normalize OpenID identity
295 * @param string $s OpenID Identity
296 * @return string normalized OpenId Identity
298 public static function normaliseOpenID(string $s): string
300 return trim(str_replace(['http://', 'https://'], ['', ''], $s), '/');
304 * Compare two URLs to see if they are the same, but ignore
305 * slight but hopefully insignificant differences such as if one
306 * is https and the other isn't, or if one is www.something and
307 * the other isn't - and also ignore case differences.
309 * @param string $a first url
310 * @param string $b second url
311 * @return boolean True if the URLs match, otherwise False
314 public static function compareLink(string $a, string $b): bool
316 return (strcasecmp(self::normaliseLink($a), self::normaliseLink($b)) === 0);
320 * Ensures the provided URI has its query string punctuation in order.
325 public static function ensureQueryParameter(string $uri): string
327 if (strpos($uri, '?') === false && ($pos = strpos($uri, '&')) !== false) {
328 $uri = substr($uri, 0, $pos) . '?' . substr($uri, $pos + 1);
335 * Check if the trimmed provided string is starting with one of the provided characters
337 * @param string $string
338 * @param array $chars
342 public static function startsWithChars(string $string, array $chars): bool
344 $return = in_array(substr(trim($string), 0, 1), $chars);
350 * Check if the first string starts with the second
352 * @see http://maettig.com/code/php/php-performance-benchmarks.php#startswith
353 * @param string $string
354 * @param string $start
357 public static function startsWith(string $string, string $start): bool
359 $return = substr_compare($string, $start, 0, strlen($start)) === 0;
365 * Checks if the first string ends with the second
367 * @see http://maettig.com/code/php/php-performance-benchmarks.php#endswith
368 * @param string $string
373 public static function endsWith(string $string, string $end): bool
375 return (substr_compare($string, $end, -strlen($end)) === 0);
379 * Returns the regular expression string to match URLs in a given text
383 public static function autoLinkRegEx(): string
386 (?<![=\'\]"/]) # Not preceded by [, =, \', ], ", /
388 ( # Capture 1: entire matched URL
389 ' . self::linkRegEx() . '
394 * Returns the regular expression string to match only an HTTP URL
398 public static function onlyLinkRegEx(): string
400 return '@^' . self::linkRegEx() . '$@xiu';
405 * @see https://daringfireball.net/2010/07/improved_regex_for_matching_urls
407 private static function linkRegEx(): string
409 return 'https?:// # http or https protocol
411 [^/\s\xA0`!()\[\]{};:\'",<>?«»“”‘’.] # Domain can\'t start with a .
412 [^/\s\xA0`!()\[\]{};:\'",<>?«»“”‘’]+ # Domain can\'t end with a .
414 [^/\s\xA0`!()\[\]{};:\'".,<>?«»“”‘’]+/? # Followed by a slash
417 [^\s\xA0()<>]+ # Run of non-space, non-()<>
419 \(([^\s\xA0()<>]+|(\([^\s()<>]+\)))*\) # balanced parens, up to 2 levels
421 [^\s\xA0`!()\[\]{};:\'".,<>?«»“”‘’] # not a space or one of these punct chars
426 * Ensures a single path item doesn't contain any path-traversing characters
428 * @param string $pathItem
430 * @see https://stackoverflow.com/a/46097713
433 public static function sanitizeFilePathItem(string $pathItem): string
435 $pathItem = str_replace('/', '_', $pathItem);
436 $pathItem = str_replace('\\', '_', $pathItem);
437 $pathItem = str_replace(DIRECTORY_SEPARATOR, '_', $pathItem); // In case it does not equal the standard values
443 * Multi-byte safe implementation of substr_replace where $start and $length are character offset and count rather
444 * than byte offset and counts.
446 * Depends on mbstring, use default encoding.
448 * @param string $string
449 * @param string $replacement
451 * @param int|null $length
454 * @see substr_replace()
456 public static function substringReplace(string $string, string $replacement, int $start, int $length = null): string
458 $string_length = mb_strlen($string);
460 $length = $length ?? $string_length;
463 $start = max(0, $string_length + $start);
464 } else if ($start > $string_length) {
465 $start = $string_length;
469 $length = max(0, $string_length - $start + $length);
470 } else if ($length > $string_length) {
471 $length = $string_length;
474 if (($start + $length) > $string_length) {
475 $length = $string_length - $start;
478 return mb_substr($string, 0, $start) . $replacement . mb_substr($string, $start + $length, $string_length - $start - $length);
482 * Perform a custom function on a text after having escaped blocks matched by the provided regular expressions.
483 * Only full matches are used, capturing group are ignored.
485 * To change the provided text, the callback function needs to return it and this function will return the modified
486 * version as well after having restored the escaped blocks.
488 * @param string $text
489 * @param string $regex
490 * @param callable $callback
494 public static function performWithEscapedBlocks(string $text, string $regex, callable $callback): string
496 // Enables nested use
497 $executionId = random_int(PHP_INT_MAX / 10, PHP_INT_MAX);
501 $return = preg_replace_callback($regex,
502 function ($matches) use ($executionId, &$blocks) {
503 $return = '«block-' . $executionId . '-' . count($blocks) . '»';
505 $blocks[] = $matches[0];
512 if (is_null($return)) {
513 Logger::notice('Received null value from preg_replace_callback', ['text' => $text, 'regex' => $regex, 'blocks' => $blocks, 'executionId' => $executionId]);
516 $text = $callback($return ?? $text) ?? '';
518 // Restore code blocks
519 $text = preg_replace_callback('/«block-' . $executionId . '-([0-9]+)»/iU',
520 function ($matches) use ($blocks) {
521 $return = $matches[0];
522 if (isset($blocks[intval($matches[1])])) {
523 $return = $blocks[$matches[1]];
534 * This function converts a file size string written in PHP's shorthand notation to an integer number of total bytes.
535 * For example: The string for shorthand notation of '2M' (which is 2,097,152 Bytes) is converted to 2097152
536 * @see https://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes
537 * @param string $shorthand
540 public static function getBytesFromShorthand(string $shorthand): int
542 $shorthand = trim($shorthand);
544 if (is_numeric($shorthand)) {
548 $last = strtolower($shorthand[strlen($shorthand)-1]);
549 $shorthand = substr($shorthand, 0, -1);
564 * Converts an URL in a nicer format (without the scheme and possibly shortened)
566 * @param string $url URL that is about to be reformatted
567 * @return string reformatted link
569 public static function getStyledURL(string $url): string
571 $parts = parse_url($url);
572 $scheme = [$parts['scheme'] . '://www.', $parts['scheme'] . '://'];
573 $styled_url = str_replace($scheme, '', $url);
575 if (strlen($styled_url) > 30) {
576 $styled_url = substr($styled_url, 0, 30) . "…";