3 * @copyright Copyright (C) 2010-2021, 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 * This is our primary input filter.
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.
69 * @param string $string Input string
70 * @return string Filtered string
71 * @deprecated since 2020.09 Please use Smarty default HTML escaping for templates or htmlspecialchars() otherwise
73 public static function escapeTags($string)
75 return str_replace(["<", ">"], ['[', ']'], $string);
79 * Use this on "body" or "content" input where angle chars shouldn't be removed,
80 * and allow them to be safely displayed.
81 * @param string $string
85 public static function escapeHtml($string)
87 return htmlspecialchars($string, ENT_COMPAT, 'UTF-8', false);
91 * Generate a string that's random, but usually pronounceable. Used to generate initial passwords
93 * @param int $len length
97 public static function getRandomName($len)
103 $vowels = ['a', 'a', 'ai', 'au', 'e', 'e', 'e', 'ee', 'ea', 'i', 'ie', 'o', 'ou', 'u'];
105 if (mt_rand(0, 5) == 4) {
111 'c', 'ch', 'cl', 'cr',
114 'g', 'gh', 'gl', 'gr',
117 'k', 'kh', 'kl', 'kr',
121 'p', 'ph', 'pl', 'pr',
124 's', 'sc', 'sh', 'sm', 'sp', 'st',
133 'ck', 'ct', 'gn', 'ld', 'lf', 'lm', 'lt', 'mb', 'mm', 'mn', 'mp',
134 'nd', 'ng', 'nk', 'nt', 'rn', 'rp', 'rt'
138 'bl', 'br', 'cl', 'cr', 'dr', 'fl', 'fr', 'gl', 'gr',
139 'kh', 'kl', 'kr', 'mn', 'pl', 'pr', 'rh', 'tr', 'qu', 'wh', 'q'
142 $start = mt_rand(0, 2);
151 for ($x = 0; $x < $len; $x++) {
152 $r = mt_rand(0, count($table) - 1);
155 if ($table == $vowels) {
156 $table = array_merge($cons, $midcons);
162 $word = substr($word, 0, $len);
164 foreach ($noend as $noe) {
165 $noelen = strlen($noe);
166 if ((strlen($word) > $noelen) && (substr($word, -$noelen) == $noe)) {
167 $word = self::getRandomName($len);
176 * Translate and format the network name of a contact
178 * @param string $network Network name of the contact (e.g. dfrn, rss and so on)
179 * @param string $url The contact url
181 * @return string Formatted network name
182 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
184 public static function formatNetworkName($network, $url = '')
186 if ($network != '') {
188 $network_name = '<a href="' . $url . '">' . ContactSelector::networkToName($network, $url) . '</a>';
190 $network_name = ContactSelector::networkToName($network);
193 return $network_name;
198 * Remove indentation from a text
200 * @param string $text String to be transformed.
201 * @param string $chr Optional. Indentation tag. Default tab (\t).
202 * @param int $count Optional. Default null.
204 * @return string Transformed string.
206 public static function deindent($text, $chr = "[\t ]", $count = NULL)
208 $lines = explode("\n", $text);
210 if (is_null($count)) {
213 while ($k < count($lines) && strlen($lines[$k]) == 0) {
216 preg_match("|^" . $chr . "*|", $lines[$k], $m);
217 $count = strlen($m[0]);
220 for ($k = 0; $k < count($lines); $k++) {
221 $lines[$k] = preg_replace("|^" . $chr . "{" . $count . "}|", "", $lines[$k]);
224 return implode("\n", $lines);
228 * Get byte size returned in a Data Measurement (KB, MB, GB)
230 * @param int $bytes The number of bytes to be measured
231 * @param int $precision Optional. Default 2.
233 * @return string Size with measured units.
235 public static function formatBytes($bytes, $precision = 2)
237 $units = ['B', 'KB', 'MB', 'GB', 'TB'];
238 $bytes = max($bytes, 0);
239 $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
240 $pow = min($pow, count($units) - 1);
241 $bytes /= pow(1024, $pow);
243 return round($bytes, $precision) . ' ' . $units[$pow];
247 * Protect percent characters in sprintf calls
249 * @param string $s String to transform.
251 * @return string Transformed string.
253 public static function protectSprintf($s)
255 return str_replace('%', '%%', $s);
259 * Base64 Encode URL and translate +/ to -_ Optionally strip padding.
261 * @param string $s URL to encode
262 * @param boolean $strip_padding Optional. Default false
264 * @return string Encoded URL
266 public static function base64UrlEncode($s, $strip_padding = false)
268 $s = strtr(base64_encode($s), '+/', '-_');
270 if ($strip_padding) {
271 $s = str_replace('=', '', $s);
278 * Decode Base64 Encoded URL and translate -_ to +/
279 * @param string $s URL to decode
281 * @return string Decoded URL
284 public static function base64UrlDecode($s)
287 Logger::log('base64url_decode: illegal input: ' . print_r(debug_backtrace(), true));
292 * // Placeholder for new rev of salmon which strips base64 padding.
293 * // PHP base64_decode handles the un-padded input without requiring this step
294 * // Uncomment if you find you need it.
297 * if (!strpos($s,'=')) {
307 return base64_decode(strtr($s, '-_', '+/'));
313 * @param string $url URL to be normalized.
315 * @return string Normalized URL.
317 public static function normaliseLink($url)
319 $ret = str_replace(['https:', '//www.'], ['http:', '//'], $url);
320 return rtrim($ret, '/');
324 * Normalize OpenID identity
326 * @param string $s OpenID Identity
328 * @return string normalized OpenId Identity
330 public static function normaliseOpenID($s)
332 return trim(str_replace(['http://', 'https://'], ['', ''], $s), '/');
336 * Compare two URLs to see if they are the same, but ignore
337 * slight but hopefully insignificant differences such as if one
338 * is https and the other isn't, or if one is www.something and
339 * the other isn't - and also ignore case differences.
341 * @param string $a first url
342 * @param string $b second url
343 * @return boolean True if the URLs match, otherwise False
346 public static function compareLink($a, $b)
348 return (strcasecmp(self::normaliseLink($a), self::normaliseLink($b)) === 0);
352 * Ensures the provided URI has its query string punctuation in order.
357 public static function ensureQueryParameter($uri)
359 if (strpos($uri, '?') === false && ($pos = strpos($uri, '&')) !== false) {
360 $uri = substr($uri, 0, $pos) . '?' . substr($uri, $pos + 1);
367 * Check if the trimmed provided string is starting with one of the provided characters
369 * @param string $string
370 * @param array $chars
373 public static function startsWithChars($string, array $chars)
375 $return = in_array(substr(trim($string), 0, 1), $chars);
381 * Check if the first string starts with the second
383 * @see http://maettig.com/code/php/php-performance-benchmarks.php#startswith
384 * @param string $string
385 * @param string $start
388 public static function startsWith(string $string, string $start)
390 $return = substr_compare($string, $start, 0, strlen($start)) === 0;
396 * Checks if the first string ends with the second
398 * @see http://maettig.com/code/php/php-performance-benchmarks.php#endswith
399 * @param string $string
403 public static function endsWith(string $string, string $end)
405 $return = substr_compare($string, $end, -strlen($end)) === 0;
411 * Returns the regular expression string to match URLs in a given text
414 * @see https://daringfireball.net/2010/07/improved_regex_for_matching_urls
416 public static function autoLinkRegEx()
419 (?<![=\'\]"/]) # Not preceded by [, =, \', ], ", /
421 ( # Capture 1: entire matched URL
422 https?:// # http or https protocol
424 [^/\s\xA0`!()\[\]{};:\'",<>?«»“”‘’.] # Domain can\'t start with a .
425 [^/\s\xA0`!()\[\]{};:\'",<>?«»“”‘’]+ # Domain can\'t end with a .
427 [^/\s\xA0`!()\[\]{};:\'".,<>?«»“”‘’]+/? # Followed by a slash
430 [^\s\xA0()<>]+ # Run of non-space, non-()<>
432 \(([^\s\xA0()<>]+|(\([^\s()<>]+\)))*\) # balanced parens, up to 2 levels
434 [^\s\xA0`!()\[\]{};:\'".,<>?«»“”‘’] # not a space or one of these punct chars
440 * Ensures a single path item doesn't contain any path-traversing characters
442 * @see https://stackoverflow.com/a/46097713
443 * @param string $pathItem
446 public static function sanitizeFilePathItem($pathItem)
448 $pathItem = str_replace('/', '_', $pathItem);
449 $pathItem = str_replace('\\', '_', $pathItem);
450 $pathItem = str_replace(DIRECTORY_SEPARATOR, '_', $pathItem); // In case it does not equal the standard values
456 * Multi-byte safe implementation of substr_replace where $start and $length are character offset and count rather
457 * than byte offset and counts.
459 * Depends on mbstring, use default encoding.
461 * @param string $string
462 * @param string $replacement
464 * @param int|null $length
466 * @see substr_replace()
468 public static function substringReplace(string $string, string $replacement, int $start, int $length = null)
470 $string_length = mb_strlen($string);
472 $length = $length ?? $string_length;
475 $start = max(0, $string_length + $start);
476 } else if ($start > $string_length) {
477 $start = $string_length;
481 $length = max(0, $string_length - $start + $length);
482 } else if ($length > $string_length) {
483 $length = $string_length;
486 if (($start + $length) > $string_length) {
487 $length = $string_length - $start;
490 return mb_substr($string, 0, $start) . $replacement . mb_substr($string, $start + $length, $string_length - $start - $length);
494 * Perform a custom function on a text after having escaped blocks matched by the provided regular expressions.
495 * Only full matches are used, capturing group are ignored.
497 * To change the provided text, the callback function needs to return it and this function will return the modified
498 * version as well after having restored the escaped blocks.
500 * @param string $text
501 * @param string $regex
502 * @param callable $callback
506 public static function performWithEscapedBlocks(string $text, string $regex, callable $callback)
508 // Enables nested use
509 $executionId = random_int(PHP_INT_MAX / 10, PHP_INT_MAX);
513 $text = preg_replace_callback($regex,
514 function ($matches) use ($executionId, &$blocks) {
515 $return = '«block-' . $executionId . '-' . count($blocks) . '»';
517 $blocks[] = $matches[0];
524 $text = $callback($text) ?? '';
526 // Restore code blocks
527 $text = preg_replace_callback('/«block-' . $executionId . '-([0-9]+)»/iU',
528 function ($matches) use ($blocks) {
529 $return = $matches[0];
530 if (isset($blocks[intval($matches[1])])) {
531 $return = $blocks[$matches[1]];