3 * @copyright Copyright (C) 2010-2022, 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 Friendica\Core\System;
27 use ParagonIE\ConstantTime\Base64;
30 * This class handles string functions
35 * Generates a pseudo-random string of hexadecimal characters
37 * @param int $size Size of string (default: 64)
39 * @return string Pseudo-random string
42 public static function getRandomHex(int $size = 64): string
44 $byte_size = ceil($size / 2);
46 $bytes = random_bytes($byte_size);
48 $return = substr(bin2hex($bytes), 0, $size);
54 * Checks, if the given string is a valid hexadecimal code
56 * @param string $hexCode
59 public static function isHex(string $hexCode): bool
61 return !empty($hexCode) ? @preg_match("/^[a-f0-9]{2,}$/i", $hexCode) && !(strlen($hexCode) & 1) : false;
65 * Use this on "body" or "content" input where angle chars shouldn't be removed,
66 * and allow them to be safely displayed.
67 * @param string $string
71 public static function escapeHtml($string)
73 return htmlspecialchars($string, ENT_COMPAT, 'UTF-8', false);
77 * Generate a string that's random, but usually pronounceable. Used to generate initial passwords
79 * @param int $len length
82 public static function getRandomName(int $len): string
88 $vowels = ['a', 'a', 'ai', 'au', 'e', 'e', 'e', 'ee', 'ea', 'i', 'ie', 'o', 'ou', 'u'];
90 if (mt_rand(0, 5) == 4) {
96 'c', 'ch', 'cl', 'cr',
99 'g', 'gh', 'gl', 'gr',
102 'k', 'kh', 'kl', 'kr',
106 'p', 'ph', 'pl', 'pr',
109 's', 'sc', 'sh', 'sm', 'sp', 'st',
118 'ck', 'ct', 'gn', 'ld', 'lf', 'lm', 'lt', 'mb', 'mm', 'mn', 'mp',
119 'nd', 'ng', 'nk', 'nt', 'rn', 'rp', 'rt'
123 'bl', 'br', 'cl', 'cr', 'dr', 'fl', 'fr', 'gl', 'gr',
124 'kh', 'kl', 'kr', 'mn', 'pl', 'pr', 'rh', 'tr', 'qu', 'wh', 'q'
127 $start = mt_rand(0, 2);
136 for ($x = 0; $x < $len; $x++) {
137 $r = mt_rand(0, count($table) - 1);
140 if ($table == $vowels) {
141 $table = array_merge($cons, $midcons);
147 $word = substr($word, 0, $len);
149 foreach ($noend as $noe) {
150 $noelen = strlen($noe);
151 if ((strlen($word) > $noelen) && (substr($word, -$noelen) == $noe)) {
152 $word = self::getRandomName($len);
161 * Translate and format the network name of a contact
163 * @param string $network Network name of the contact (e.g. dfrn, rss and so on)
164 * @param string $url The contact url
166 * @return string Formatted network name
167 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
169 public static function formatNetworkName(string $network, string $url = ''): string
171 if ($network != '') {
173 $network_name = '<a href="' . $url . '">' . ContactSelector::networkToName($network, $url) . '</a>';
175 $network_name = ContactSelector::networkToName($network);
178 return $network_name;
185 * Remove indentation from a text
187 * @param string $text String to be transformed.
188 * @param string $chr Optional. Indentation tag. Default tab (\t).
189 * @param int $count Optional. Default null.
191 * @return string Transformed string.
193 public static function deindent(string $text, string $chr = "[\t ]", int $count = null): string
195 $lines = explode("\n", $text);
197 if (is_null($count)) {
200 while ($k < count($lines) && strlen($lines[$k]) == 0) {
203 preg_match("|^" . $chr . "*|", $lines[$k], $m);
204 $count = strlen($m[0]);
207 for ($k = 0; $k < count($lines); $k++) {
208 $lines[$k] = preg_replace("|^" . $chr . "{" . $count . "}|", "", $lines[$k]);
211 return implode("\n", $lines);
215 * Get byte size returned in a Data Measurement (KB, MB, GB)
217 * @param int $bytes The number of bytes to be measured
218 * @param int $precision Optional. Default 2.
220 * @return string Size with measured units.
222 public static function formatBytes(int $bytes, int $precision = 2): string
224 // If this method is called for an infinite (== unlimited) amount of bytes:
229 $units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
230 $bytes = max($bytes, 0);
231 $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
232 $pow = min($pow, count($units) - 1);
233 $bytes /= pow(1024, $pow);
235 return round($bytes, $precision) . ' ' . $units[$pow];
239 * Protect percent characters in sprintf calls
241 * @param string $s String to transform.
242 * @return string Transformed string.
244 public static function protectSprintf(string $s): string
246 return str_replace('%', '%%', $s);
250 * Base64 Encode URL and translate +/ to -_ Optionally strip padding.
252 * @param string $s URL to encode
253 * @param boolean $strip_padding Optional. Default false
254 * @return string Encoded URL
255 * @see https://web.archive.org/web/20160506073138/http://salmon-protocol.googlecode.com:80/svn/trunk/draft-panzer-magicsig-01.html#params
257 public static function base64UrlEncode(string $s, bool $strip_padding = false): string
259 if ($strip_padding) {
260 $s = Base64::encodeUnpadded($s);
262 $s = Base64::encode($s);
265 return strtr($s, '+/', '-_');
269 * Decode Base64 Encoded URL and translate -_ to +/
271 * @param string $s URL to decode
272 * @return string Decoded URL
274 * @see https://web.archive.org/web/20160506073138/http://salmon-protocol.googlecode.com:80/svn/trunk/draft-panzer-magicsig-01.html#params
276 public static function base64UrlDecode(string $s): string
278 return Base64::decode(strtr($s, '-_', '+/'));
284 * @param string $url URL to be normalized.
285 * @return string Normalized URL.
287 public static function normaliseLink(string $url): string
289 $ret = str_replace(['https:', '//www.'], ['http:', '//'], $url);
290 return rtrim($ret, '/');
294 * Normalize OpenID identity
296 * @param string $s OpenID Identity
297 * @return string normalized OpenId Identity
299 public static function normaliseOpenID(string $s): string
301 return trim(str_replace(['http://', 'https://'], ['', ''], $s), '/');
305 * Compare two URLs to see if they are the same, but ignore
306 * slight but hopefully insignificant differences such as if one
307 * is https and the other isn't, or if one is www.something and
308 * the other isn't - and also ignore case differences.
310 * @param string $a first url
311 * @param string $b second url
312 * @return boolean True if the URLs match, otherwise False
315 public static function compareLink(string $a, string $b): bool
317 return (strcasecmp(self::normaliseLink($a), self::normaliseLink($b)) === 0);
321 * Ensures the provided URI has its query string punctuation in order.
326 public static function ensureQueryParameter(string $uri): string
328 if (strpos($uri, '?') === false && ($pos = strpos($uri, '&')) !== false) {
329 $uri = substr($uri, 0, $pos) . '?' . substr($uri, $pos + 1);
336 * Check if the trimmed provided string is starting with one of the provided characters
338 * @param string $string
339 * @param array $chars
343 public static function startsWithChars(string $string, array $chars): bool
345 $return = in_array(substr(trim($string), 0, 1), $chars);
351 * Check if the first string starts with the second
353 * @see http://maettig.com/code/php/php-performance-benchmarks.php#startswith
354 * @param string $string
355 * @param string $start
358 public static function startsWith(string $string, string $start): bool
360 $return = substr_compare($string, $start, 0, strlen($start)) === 0;
366 * Checks if the first string ends with the second
368 * @see http://maettig.com/code/php/php-performance-benchmarks.php#endswith
369 * @param string $string
374 public static function endsWith(string $string, string $end): bool
376 return (substr_compare($string, $end, -strlen($end)) === 0);
380 * Returns the regular expression string to match URLs in a given text
384 public static function autoLinkRegEx(): string
387 (?<![=\'\]"/]) # Not preceded by [, =, \', ], ", /
389 ( # Capture 1: entire matched URL
390 ' . self::linkRegEx() . '
395 * Returns the regular expression string to match only an HTTP URL
399 public static function onlyLinkRegEx(): string
401 return '@^' . self::linkRegEx() . '$@xiu';
406 * @see https://daringfireball.net/2010/07/improved_regex_for_matching_urls
408 private static function linkRegEx(): string
410 return 'https?:// # http or https protocol
412 [^/\s\xA0`!()\[\]{};:\'",<>?«»“”‘’.] # Domain can\'t start with a .
413 [^/\s\xA0`!()\[\]{};:\'",<>?«»“”‘’]+ # Domain can\'t end with a .
415 [^/\s\xA0`!()\[\]{};:\'".,<>?«»“”‘’]+/? # Followed by a slash
418 [^\s\xA0()<>]+ # Run of non-space, non-()<>
420 \(([^\s\xA0()<>]+|(\([^\s()<>]+\)))*\) # balanced parens, up to 2 levels
422 [^\s\xA0`!()\[\]{};:\'".,<>?«»“”‘’] # not a space or one of these punct chars
427 * Ensures a single path item doesn't contain any path-traversing characters
429 * @param string $pathItem
431 * @see https://stackoverflow.com/a/46097713
434 public static function sanitizeFilePathItem(string $pathItem): string
436 $pathItem = str_replace('/', '_', $pathItem);
437 $pathItem = str_replace('\\', '_', $pathItem);
438 $pathItem = str_replace(DIRECTORY_SEPARATOR, '_', $pathItem); // In case it does not equal the standard values
444 * Multi-byte safe implementation of substr_replace where $start and $length are character offset and count rather
445 * than byte offset and counts.
447 * Depends on mbstring, use default encoding.
449 * @param string $string
450 * @param string $replacement
452 * @param int|null $length
455 * @see substr_replace()
457 public static function substringReplace(string $string, string $replacement, int $start, int $length = null): string
459 $string_length = mb_strlen($string);
461 $length = $length ?? $string_length;
464 $start = max(0, $string_length + $start);
465 } else if ($start > $string_length) {
466 $start = $string_length;
470 $length = max(0, $string_length - $start + $length);
471 } else if ($length > $string_length) {
472 $length = $string_length;
475 if (($start + $length) > $string_length) {
476 $length = $string_length - $start;
479 return mb_substr($string, 0, $start) . $replacement . mb_substr($string, $start + $length, $string_length - $start - $length);
483 * Perform a custom function on a text after having escaped blocks matched by the provided regular expressions.
484 * Only full matches are used, capturing group are ignored.
486 * To change the provided text, the callback function needs to return it and this function will return the modified
487 * version as well after having restored the escaped blocks.
489 * @param string $text
490 * @param string $regex
491 * @param callable $callback
495 public static function performWithEscapedBlocks(string $text, string $regex, callable $callback): string
497 // Enables nested use
498 $executionId = random_int(PHP_INT_MAX / 10, PHP_INT_MAX);
502 $return = preg_replace_callback($regex,
503 function ($matches) use ($executionId, &$blocks) {
504 $return = '«block-' . $executionId . '-' . count($blocks) . '»';
506 $blocks[] = $matches[0];
513 if (is_null($return)) {
514 Logger::warning('Received null value from preg_replace_callback', ['text' => $text, 'regex' => $regex, 'blocks' => $blocks, 'executionId' => $executionId, 'callstack' => System::callstack(10)]);
517 $text = $callback($return ?? $text) ?? '';
519 // Restore code blocks
520 $text = preg_replace_callback('/«block-' . $executionId . '-([0-9]+)»/iU',
521 function ($matches) use ($blocks) {
522 $return = $matches[0];
523 if (isset($blocks[intval($matches[1])])) {
524 $return = $blocks[$matches[1]];
535 * This function converts a PHP's shorhand notation string for file sizes in to an integer number of total bytes.
536 * For example: The string for shorthand notation of '2M' (which is 2,097,152 Bytes) is converted to 2097152
537 * @see https://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes
538 * @param string $shorthand
541 public static function getBytesFromShorthand(string $shorthand): int
543 $shorthand = trim($shorthand);
545 if (is_numeric($shorthand)) {
549 $last = strtolower($shorthand[strlen($shorthand)-1]);
550 $shorthand = substr($shorthand, 0, -1);