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;
28 * This class handles string functions
33 * Generates a pseudo-random string of hexadecimal characters
39 public static function getRandomHex($size = 64)
41 $byte_size = ceil($size / 2);
43 $bytes = random_bytes($byte_size);
45 $return = substr(bin2hex($bytes), 0, $size);
51 * Checks, if the given string is a valid hexadecimal code
53 * @param string $hexCode
57 public static function isHex($hexCode)
59 return !empty($hexCode) ? @preg_match("/^[a-f0-9]{2,}$/i", $hexCode) && !(strlen($hexCode) & 1) : false;
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
69 public static function escapeHtml($string)
71 return htmlspecialchars($string, ENT_COMPAT, 'UTF-8', false);
75 * Generate a string that's random, but usually pronounceable. Used to generate initial passwords
77 * @param int $len length
81 public static function getRandomName($len)
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)
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($bytes, $precision = 2)
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);
229 return round($bytes, $precision) . ' ' . $units[$pow];
233 * Protect percent characters in sprintf calls
235 * @param string $s String to transform.
237 * @return string Transformed string.
239 public static function protectSprintf($s)
241 return str_replace('%', '%%', $s);
245 * Base64 Encode URL and translate +/ to -_ Optionally strip padding.
247 * @param string $s URL to encode
248 * @param boolean $strip_padding Optional. Default false
250 * @return string Encoded URL
252 public static function base64UrlEncode($s, $strip_padding = false)
254 $s = strtr(base64_encode($s), '+/', '-_');
256 if ($strip_padding) {
257 $s = str_replace('=', '', $s);
264 * Decode Base64 Encoded URL and translate -_ to +/
265 * @param string $s URL to decode
267 * @return string Decoded URL
270 public static function base64UrlDecode($s)
273 Logger::notice('base64url_decode: illegal input: ', ['backtrace' => debug_backtrace()]);
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.
283 * if (!strpos($s,'=')) {
293 return base64_decode(strtr($s, '-_', '+/'));
299 * @param string $url URL to be normalized.
301 * @return string Normalized URL.
303 public static function normaliseLink($url)
305 $ret = str_replace(['https:', '//www.'], ['http:', '//'], $url);
306 return rtrim($ret, '/');
310 * Normalize OpenID identity
312 * @param string $s OpenID Identity
314 * @return string normalized OpenId Identity
316 public static function normaliseOpenID($s)
318 return trim(str_replace(['http://', 'https://'], ['', ''], $s), '/');
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.
327 * @param string $a first url
328 * @param string $b second url
329 * @return boolean True if the URLs match, otherwise False
332 public static function compareLink($a, $b)
334 return (strcasecmp(self::normaliseLink($a), self::normaliseLink($b)) === 0);
338 * Ensures the provided URI has its query string punctuation in order.
343 public static function ensureQueryParameter($uri)
345 if (strpos($uri, '?') === false && ($pos = strpos($uri, '&')) !== false) {
346 $uri = substr($uri, 0, $pos) . '?' . substr($uri, $pos + 1);
353 * Check if the trimmed provided string is starting with one of the provided characters
355 * @param string $string
356 * @param array $chars
359 public static function startsWithChars($string, array $chars)
361 $return = in_array(substr(trim($string), 0, 1), $chars);
367 * Check if the first string starts with the second
369 * @see http://maettig.com/code/php/php-performance-benchmarks.php#startswith
370 * @param string $string
371 * @param string $start
374 public static function startsWith(string $string, string $start)
376 $return = substr_compare($string, $start, 0, strlen($start)) === 0;
382 * Checks if the first string ends with the second
384 * @see http://maettig.com/code/php/php-performance-benchmarks.php#endswith
385 * @param string $string
389 public static function endsWith(string $string, string $end)
391 $return = substr_compare($string, $end, -strlen($end)) === 0;
397 * Returns the regular expression string to match URLs in a given text
400 * @see https://daringfireball.net/2010/07/improved_regex_for_matching_urls
402 public static function autoLinkRegEx()
405 (?<![=\'\]"/]) # Not preceded by [, =, \', ], ", /
407 ( # Capture 1: entire matched URL
408 https?:// # http or https protocol
410 [^/\s\xA0`!()\[\]{};:\'",<>?«»“”‘’.] # Domain can\'t start with a .
411 [^/\s\xA0`!()\[\]{};:\'",<>?«»“”‘’]+ # Domain can\'t end with a .
413 [^/\s\xA0`!()\[\]{};:\'".,<>?«»“”‘’]+/? # Followed by a slash
416 [^\s\xA0()<>]+ # Run of non-space, non-()<>
418 \(([^\s\xA0()<>]+|(\([^\s()<>]+\)))*\) # balanced parens, up to 2 levels
420 [^\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 * @see https://stackoverflow.com/a/46097713
429 * @param string $pathItem
432 public static function sanitizeFilePathItem($pathItem)
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
442 * Multi-byte safe implementation of substr_replace where $start and $length are character offset and count rather
443 * than byte offset and counts.
445 * Depends on mbstring, use default encoding.
447 * @param string $string
448 * @param string $replacement
450 * @param int|null $length
452 * @see substr_replace()
454 public static function substringReplace(string $string, string $replacement, int $start, int $length = null)
456 $string_length = mb_strlen($string);
458 $length = $length ?? $string_length;
461 $start = max(0, $string_length + $start);
462 } else if ($start > $string_length) {
463 $start = $string_length;
467 $length = max(0, $string_length - $start + $length);
468 } else if ($length > $string_length) {
469 $length = $string_length;
472 if (($start + $length) > $string_length) {
473 $length = $string_length - $start;
476 return mb_substr($string, 0, $start) . $replacement . mb_substr($string, $start + $length, $string_length - $start - $length);
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.
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.
486 * @param string $text
487 * @param string $regex
488 * @param callable $callback
491 public static function performWithEscapedBlocks(string $text, string $regex, callable $callback): string
493 // Enables nested use
494 $executionId = random_int(PHP_INT_MAX / 10, PHP_INT_MAX);
498 $text = preg_replace_callback($regex,
499 function ($matches) use ($executionId, &$blocks) {
500 $return = '«block-' . $executionId . '-' . count($blocks) . '»';
502 $blocks[] = $matches[0];
509 $text = $callback($text) ?? '';
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]];