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
35 * @param int $size Size of string (default: 64)
36 * @return string Pseudo-random string
39 public static function getRandomHex(int $size = 64): string
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
56 public static function isHex(string $hexCode): bool
58 return !empty($hexCode) ? @preg_match("/^[a-f0-9]{2,}$/i", $hexCode) && !(strlen($hexCode) & 1) : false;
62 * Use this on "body" or "content" input where angle chars shouldn't be removed,
63 * and allow them to be safely displayed.
64 * @param string $string
68 public static function escapeHtml($string)
70 return htmlspecialchars($string, ENT_COMPAT, 'UTF-8', false);
74 * Generate a string that's random, but usually pronounceable. Used to generate initial passwords
76 * @param int $len length
79 public static function getRandomName(int $len): string
85 $vowels = ['a', 'a', 'ai', 'au', 'e', 'e', 'e', 'ee', 'ea', 'i', 'ie', 'o', 'ou', 'u'];
87 if (mt_rand(0, 5) == 4) {
93 'c', 'ch', 'cl', 'cr',
96 'g', 'gh', 'gl', 'gr',
99 'k', 'kh', 'kl', 'kr',
103 'p', 'ph', 'pl', 'pr',
106 's', 'sc', 'sh', 'sm', 'sp', 'st',
115 'ck', 'ct', 'gn', 'ld', 'lf', 'lm', 'lt', 'mb', 'mm', 'mn', 'mp',
116 'nd', 'ng', 'nk', 'nt', 'rn', 'rp', 'rt'
120 'bl', 'br', 'cl', 'cr', 'dr', 'fl', 'fr', 'gl', 'gr',
121 'kh', 'kl', 'kr', 'mn', 'pl', 'pr', 'rh', 'tr', 'qu', 'wh', 'q'
124 $start = mt_rand(0, 2);
133 for ($x = 0; $x < $len; $x++) {
134 $r = mt_rand(0, count($table) - 1);
137 if ($table == $vowels) {
138 $table = array_merge($cons, $midcons);
144 $word = substr($word, 0, $len);
146 foreach ($noend as $noe) {
147 $noelen = strlen($noe);
148 if ((strlen($word) > $noelen) && (substr($word, -$noelen) == $noe)) {
149 $word = self::getRandomName($len);
158 * Translate and format the network name of a contact
160 * @param string $network Network name of the contact (e.g. dfrn, rss and so on)
161 * @param string $url The contact url
162 * @return string Formatted network name
163 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
165 public static function formatNetworkName(string $network, string $url = ''): string
167 if ($network != '') {
169 $network_name = '<a href="' . $url . '">' . ContactSelector::networkToName($network, $url) . '</a>';
171 $network_name = ContactSelector::networkToName($network);
174 return $network_name;
181 * Remove indentation from a text
183 * @param string $text String to be transformed.
184 * @param string $chr Optional. Indentation tag. Default tab (\t).
185 * @param int $count Optional. Default null.
187 * @return string Transformed string.
189 public static function deindent(string $text, string $chr = "[\t ]", int $count = null): string
191 $lines = explode("\n", $text);
193 if (is_null($count)) {
196 while ($k < count($lines) && strlen($lines[$k]) == 0) {
199 preg_match("|^" . $chr . "*|", $lines[$k], $m);
200 $count = strlen($m[0]);
203 for ($k = 0; $k < count($lines); $k++) {
204 $lines[$k] = preg_replace("|^" . $chr . "{" . $count . "}|", "", $lines[$k]);
207 return implode("\n", $lines);
211 * Get byte size returned in a Data Measurement (KB, MB, GB)
213 * @param int $bytes The number of bytes to be measured
214 * @param int $precision Optional. Default 2.
216 * @return string Size with measured units.
218 public static function formatBytes(int $bytes, int $precision = 2): string
220 $units = ['B', 'KB', 'MB', 'GB', 'TB'];
221 $bytes = max($bytes, 0);
222 $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
223 $pow = min($pow, count($units) - 1);
224 $bytes /= pow(1024, $pow);
226 return round($bytes, $precision) . ' ' . $units[$pow];
230 * Protect percent characters in sprintf calls
232 * @param string $s String to transform.
233 * @return string Transformed string.
235 public static function protectSprintf(string $s): string
237 return str_replace('%', '%%', $s);
241 * Base64 Encode URL and translate +/ to -_ Optionally strip padding.
243 * @param string $s URL to encode
244 * @param boolean $strip_padding Optional. Default false
245 * @return string Encoded URL
247 public static function base64UrlEncode(string $s, bool $strip_padding = false): string
249 $s = strtr(base64_encode($s), '+/', '-_');
251 if ($strip_padding) {
252 $s = str_replace('=', '', $s);
259 * Decode Base64 Encoded URL and translate -_ to +/
261 * @param string $s URL to decode
262 * @return string Decoded URL
265 public static function base64UrlDecode(string $s): string
268 * // Placeholder for new rev of salmon which strips base64 padding.
269 * // PHP base64_decode handles the un-padded input without requiring this step
270 * // Uncomment if you find you need it.
273 * if (!strpos($s,'=')) {
283 return base64_decode(strtr($s, '-_', '+/'));
289 * @param string $url URL to be normalized.
290 * @return string Normalized URL.
292 public static function normaliseLink(string $url): string
294 $ret = str_replace(['https:', '//www.'], ['http:', '//'], $url);
295 return rtrim($ret, '/');
299 * Normalize OpenID identity
301 * @param string $s OpenID Identity
302 * @return string normalized OpenId Identity
304 public static function normaliseOpenID(string $s): string
306 return trim(str_replace(['http://', 'https://'], ['', ''], $s), '/');
310 * Compare two URLs to see if they are the same, but ignore
311 * slight but hopefully insignificant differences such as if one
312 * is https and the other isn't, or if one is www.something and
313 * the other isn't - and also ignore case differences.
315 * @param string $a first url
316 * @param string $b second url
317 * @return boolean True if the URLs match, otherwise False
320 public static function compareLink(string $a, string $b): bool
322 return (strcasecmp(self::normaliseLink($a), self::normaliseLink($b)) === 0);
326 * Ensures the provided URI has its query string punctuation in order.
331 public static function ensureQueryParameter(string $uri): string
333 if (strpos($uri, '?') === false && ($pos = strpos($uri, '&')) !== false) {
334 $uri = substr($uri, 0, $pos) . '?' . substr($uri, $pos + 1);
341 * Check if the trimmed provided string is starting with one of the provided characters
343 * @param string $string
344 * @param array $chars
347 public static function startsWithChars(string $string, array $chars): bool
349 $return = in_array(substr(trim($string), 0, 1), $chars);
355 * Check if the first string starts with the second
357 * @see http://maettig.com/code/php/php-performance-benchmarks.php#startswith
358 * @param string $string
359 * @param string $start
362 public static function startsWith(string $string, string $start): bool
364 $return = substr_compare($string, $start, 0, strlen($start)) === 0;
370 * Checks if the first string ends with the second
372 * @see http://maettig.com/code/php/php-performance-benchmarks.php#endswith
373 * @param string $string
377 public static function endsWith(string $string, string $end): string
379 $return = substr_compare($string, $end, -strlen($end)) === 0;
385 * Returns the regular expression string to match URLs in a given text
388 * @see https://daringfireball.net/2010/07/improved_regex_for_matching_urls
390 public static function autoLinkRegEx(): string
393 (?<![=\'\]"/]) # Not preceded by [, =, \', ], ", /
395 ( # Capture 1: entire matched URL
396 https?:// # http or https protocol
398 [^/\s\xA0`!()\[\]{};:\'",<>?«»“”‘’.] # Domain can\'t start with a .
399 [^/\s\xA0`!()\[\]{};:\'",<>?«»“”‘’]+ # Domain can\'t end with a .
401 [^/\s\xA0`!()\[\]{};:\'".,<>?«»“”‘’]+/? # Followed by a slash
404 [^\s\xA0()<>]+ # Run of non-space, non-()<>
406 \(([^\s\xA0()<>]+|(\([^\s()<>]+\)))*\) # balanced parens, up to 2 levels
408 [^\s\xA0`!()\[\]{};:\'".,<>?«»“”‘’] # not a space or one of these punct chars
414 * Ensures a single path item doesn't contain any path-traversing characters
416 * @see https://stackoverflow.com/a/46097713
417 * @param string $pathItem
420 public static function sanitizeFilePathItem(string $pathItem): string
422 $pathItem = str_replace('/', '_', $pathItem);
423 $pathItem = str_replace('\\', '_', $pathItem);
424 $pathItem = str_replace(DIRECTORY_SEPARATOR, '_', $pathItem); // In case it does not equal the standard values
430 * Multi-byte safe implementation of substr_replace where $start and $length are character offset and count rather
431 * than byte offset and counts.
433 * Depends on mbstring, use default encoding.
435 * @param string $string
436 * @param string $replacement
438 * @param int|null $length
440 * @see substr_replace()
442 public static function substringReplace(string $string, string $replacement, int $start, int $length = null): string
444 $string_length = mb_strlen($string);
446 $length = $length ?? $string_length;
449 $start = max(0, $string_length + $start);
450 } else if ($start > $string_length) {
451 $start = $string_length;
455 $length = max(0, $string_length - $start + $length);
456 } else if ($length > $string_length) {
457 $length = $string_length;
460 if (($start + $length) > $string_length) {
461 $length = $string_length - $start;
464 return mb_substr($string, 0, $start) . $replacement . mb_substr($string, $start + $length, $string_length - $start - $length);
468 * Perform a custom function on a text after having escaped blocks matched by the provided regular expressions.
469 * Only full matches are used, capturing group are ignored.
471 * To change the provided text, the callback function needs to return it and this function will return the modified
472 * version as well after having restored the escaped blocks.
474 * @param string $text
475 * @param string $regex
476 * @param callable $callback
479 public static function performWithEscapedBlocks(string $text, string $regex, callable $callback): string
481 // Enables nested use
482 $executionId = random_int(PHP_INT_MAX / 10, PHP_INT_MAX);
486 $text = preg_replace_callback($regex,
487 function ($matches) use ($executionId, &$blocks) {
488 $return = '«block-' . $executionId . '-' . count($blocks) . '»';
490 $blocks[] = $matches[0];
497 $text = $callback($text) ?? '';
499 // Restore code blocks
500 $text = preg_replace_callback('/«block-' . $executionId . '-([0-9]+)»/iU',
501 function ($matches) use ($blocks) {
502 $return = $matches[0];
503 if (isset($blocks[intval($matches[1])])) {
504 $return = $blocks[$matches[1]];