. * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License version 3. * * In accordance with Section 7(b) of the GNU Affero General Public License version 3, * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ namespace Espo\Core\Utils; use Exception; use RuntimeException; use stdClass; class Util { /** @var string */ protected static $separator = DIRECTORY_SEPARATOR; /** @var string[] */ protected static $reservedWordList = ['Case']; /** * Get a folder separator. */ public static function getSeparator(): string { return static::$separator; } /** * Convert camelCase to under_score. */ public static function camelCaseToUnderscore(string $string): string { return static::toUnderScore($string); } /** * Convert hyphen-string to camelCase. */ public static function hyphenToCamelCase(string $string): string { return self::toCamelCase($string, '-'); } /** * Convert camelCase to hyphen-string. */ public static function camelCaseToHyphen(string $string): string { return static::fromCamelCase($string, '-'); } /** * Replace slashes and backslashes with a delimiter. E.g. Espo\Utils to Espo/Utils. * * @param string $name * @param string $delimiter * @return string */ public static function toFormat(string $name, string $delimiter = '/'): string { /** @var string */ return preg_replace("/[\/\\\]/", $delimiter, $name); } /** * Convert a string of a specific format to camelCase. * * @param string $input An input string. * @param string $symbol A formatting symbol. * @param bool $capitaliseFirstChar Capitalise the first character. * @return string */ public static function toCamelCase($input, string $symbol = '_', bool $capitaliseFirstChar = false) { if (is_array($input)) { /** @phpstan-ignore-line */ foreach ($input as &$value) { $value = static::toCamelCase($value, $symbol, $capitaliseFirstChar); } return $input; /** @phpstan-ignore-line */ } $input = lcfirst($input); if ($capitaliseFirstChar) { $input = ucfirst($input); } /** @var string */ return preg_replace_callback( '/' . $symbol . '([a-zA-Z])/', /** * @param string[] $matches */ function ($matches): string { return strtoupper($matches[1]); }, $input ); } /** * Convert a string from camelCase to a format defined by a symbol. * * @param string $input * @return string */ public static function fromCamelCase($input, string $symbol = '_') { if (is_array($input)) { /** @phpstan-ignore-line */ foreach ($input as &$value) { $value = static::fromCamelCase($value, $symbol); } return $input; /** @phpstan-ignore-line */ } $input[0] = strtolower($input[0]); /** @var string */ return preg_replace_callback( '/([A-Z])/', function ($matches) use ($symbol) { return $symbol . strtolower($matches[1]); }, $input ); } /** * Convert a string from camelCase to under_score. * * @param string $input * @return string */ public static function toUnderScore($input) { return static::fromCamelCase($input, '_'); } /** * Merge arrays recursively. $newArray overrides $currentArray. * * @template T of array * @param T $currentArray A source array. * @param T $newArray A merge-array (priority is same as for array_merge()). * @return T */ public static function merge($currentArray, $newArray) { /** @phpstan-var mixed $currentArray */ /** @phpstan-var mixed $newArray */ $mergeIdentifier = '__APPEND__'; if (is_array($currentArray) && !is_array($newArray)) { /** @var T */ return $currentArray; } else if (!is_array($currentArray) && is_array($newArray)) { /** @var T */ return $newArray; /** @phpstan-ignore-line */ } else if ( (!is_array($currentArray) || empty($currentArray)) && (!is_array($newArray) || empty($newArray)) ) { /** @var T */ return []; } foreach ($newArray as $newName => $newValue) { if ( is_array($newValue) && array_key_exists($newName, $currentArray) && is_array($currentArray[$newName]) ) { // check __APPEND__ identifier $appendKey = array_search($mergeIdentifier, $newValue, true); if ($appendKey !== false) { unset($newValue[$appendKey]); $newValue = array_merge($currentArray[$newName], $newValue); } else if ( !static::isSingleArray($newValue) || !static::isSingleArray($currentArray[$newName]) ) { $newValue = static::merge($currentArray[$newName], $newValue); } } // check if exists __APPEND__ identifier and remove its if (!isset($currentArray[$newName]) && is_array($newValue)) { $newValue = static::unsetInArrayByValue($mergeIdentifier, $newValue); } $currentArray[$newName] = $newValue; } return $currentArray; } /** * Unset a value in an array recursively. * * @param string $needle A needle. * @param array $haystack A haystack. * @return array */ public static function unsetInArrayByValue($needle, array $haystack, bool $reIndex = true): array { $doReindex = false; foreach ($haystack as $key => $value) { if (is_array($value)) { $haystack[$key] = static::unsetInArrayByValue($needle, $value); } else if ($needle === $value) { unset($haystack[$key]); if ($reIndex) { $doReindex = true; } } } if ($doReindex) { $haystack = array_values($haystack); } return $haystack; } /** * Get a full path of a file. * * @param string|string[] $folderPath A folder path. */ public static function concatPath($folderPath, ?string $filePath = null): string { if (is_array($folderPath)) { $fullPath = ''; foreach ($folderPath as $path) { $fullPath = static::concatPath($fullPath, $path); } return static::fixPath($fullPath); } if (empty($filePath)) { return static::fixPath($folderPath); } if (empty($folderPath)) { return static::fixPath($filePath); } if (substr($folderPath, -1) == static::getSeparator() || str_ends_with($folderPath, '/')) { return static::fixPath($folderPath . $filePath); } return static::fixPath($folderPath) . static::getSeparator() . $filePath; } /** * Fix a path separator. */ public static function fixPath(string $path): string { return str_replace('/', static::getSeparator(), $path); } /** * Convert an array to stdClass recursively. * * @param array $array * @return stdClass */ public static function arrayToObject($array) { /** @var stdClass */ return self::arrayToObjectInternal($array); } /** * @param mixed $value * @return mixed */ private static function arrayToObjectInternal($value) { if (!is_array($value)) { return $value; } // @todo Change to `array_is_list` when PHP 8.1 is the min supported. $isList = $value === array_values($value); $value = array_map(fn($v) => self::arrayToObjectInternal($v), $value); if (!$isList) { $value = (object) $value; } return $value; } /** * Convert stdClass to array recursively. * * @param object $object * @return array */ public static function objectToArray($object) { /** @var array */ return self::objectToArrayInternal($object); } /** * @param mixed $value * @return mixed */ private static function objectToArrayInternal($value) { if (is_object($value)) { $value = (array) $value; } if (is_array($value)) { return array_map(fn($v) => self::objectToArrayInternal($v), $value); } return $value; } /** * Appends 'Obj' if a name is a PHP reserved word. * * @param string $name A name. * @return string */ public static function normalizeClassName($name) { if (in_array($name, self::$reservedWordList)) { $name .= 'Obj'; } return $name; } /** * Remove 'Obj' if a name is a PHP reserved word. * * @param string $name * @return string */ public static function normalizeScopeName($name) { foreach (self::$reservedWordList as $reservedWord) { if ($reservedWord.'Obj' == $name) { return $reservedWord; } } return $name; } /** * Get naming according to a prefix or postfix type. */ public static function getNaming( string $name, string $prePostFix, string $type = 'prefix', string $symbol = '_' ): ?string { if ($type == 'prefix') { return static::toCamelCase($prePostFix.$symbol.$name, $symbol); } if ($type == 'postfix') { return static::toCamelCase($name.$symbol.$prePostFix, $symbol); } return null; } /** * Replace a search-string in an array recursively. * * @param string $search * @param string $replace * @param string[]|string $array * @param bool $isKeys * @return string|array * * @todo Maybe to remove the method. * @deprecated */ public static function replaceInArray($search = '', $replace = '', $array = [], $isKeys = true) { if (!is_array($array)) { return str_replace($search, $replace, $array); } $newArr = []; foreach ($array as $key => $value) { $addKey = $key; if ($isKeys) { $addKey = str_replace($search, $replace, $key); } $newArr[$addKey] = static::replaceInArray($search, $replace, $value, $isKeys); } return $newArr; } /** * Unset content items defined in the unset.json. * * @param array $content * @param string|array $unsets in format * [ * 'EntityType1' => [ 'unset1', 'unset2'], * 'EntityType2' => ['unset1', 'unset2'], * ] * OR * ['EntityType1.unset1', 'EntityType2.unset2', ...] * OR * 'EntityType1.unset1' * @param bool $unsetParentEmptyArray If unset empty parent array after unsets * @return array */ public static function unsetInArray(array $content, $unsets, bool $unsetParentEmptyArray = false) { if (empty($unsets)) { return $content; } if (is_string($unsets)) { $unsets = (array) $unsets; } foreach ($unsets as $rootKey => $unsetItem) { $unsetItem = is_array($unsetItem) ? $unsetItem : (array) $unsetItem; foreach ($unsetItem as $unsetString) { if (is_string($rootKey)) { $unsetString = $rootKey . '.' . $unsetString; } $keyArr = explode('.', $unsetString); $keyChainCount = count($keyArr) - 1; $elem = &$content; $elementArr = []; $elementArr[] = &$elem; for ($i = 0; $i <= $keyChainCount; $i++) { if (!array_key_exists($keyArr[$i], $elem)) { continue; } if ($i == $keyChainCount) { unset($elem[$keyArr[$i]]); if ($unsetParentEmptyArray) { for ($j = count($elementArr); $j > 0; $j--) { $pointer =& $elementArr[$j]; if (empty($pointer)) { $previous =& $elementArr[$j - 1]; unset($previous[$keyArr[$j - 1]]); } } } } else if (is_array($elem[$keyArr[$i]])) { $elem = &$elem[$keyArr[$i]]; $elementArr[] = &$elem; } } } } return $content; } /** * Get class name from a file path. * * @return class-string */ public static function getClassName(string $filePath): string { /** @var string $className */ $className = preg_replace('/\.php$/i', '', $filePath); /** @var string $className */ $className = preg_replace('/^(application|custom)(\/|\\\)/i', '', $className); /** @var class-string */ return static::toFormat($className, '\\'); } /** * Get a value of a key. * * @param stdClass|array $data * @param string[]|string $key Ex. of key is "entityDefs", "entityDefs.User". * @param mixed $default * @return mixed */ public static function getValueByKey($data, $key = null, $default = null) { if (empty($key)) { return $data; } if (is_array($key)) { $keys = $key; } else { $keys = explode('.', $key); } $item = $data; foreach ($keys as $keyName) { if (is_array($item)) { if (isset($item[$keyName])) { $item = $item[$keyName]; } else { return $default; } } else if (is_object($item)) { if (isset($item->$keyName)) { $item = $item->$keyName; } else { return $default; } } } return $item; } /** * Check if two variables are equal. * * @param mixed $var1 * @param mixed $var2 */ public static function areEqual($var1, $var2): bool { if (is_array($var1)) { static::ksortRecursive($var1); } if (is_array($var2)) { static::ksortRecursive($var2); } return ($var1 === $var2); } /** * Sort array recursively. * * @param array $array */ public static function ksortRecursive(&$array): bool { if (!is_array($array)) { return false; } ksort($array); foreach ($array as $key => $value) { static::ksortRecursive($array[$key]); } return true; } /** * @param array $array * @deprecated * @todo Make private. */ public static function isSingleArray(array $array): bool { foreach ($array as $key => $value) { if (!is_int($key)) { return false; } } return true; } /** * Generate a UUID v4. */ public static function generateUuid4(): string { try { $data = random_bytes(16); $data[6] = chr(ord($data[6]) & 0x0f | 0x40); $data[8] = chr(ord($data[8]) & 0x3f | 0x80); $hex = bin2hex($data); } catch (Exception) { throw new RuntimeException("Could not generate UUID."); } return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split($hex, 4)); } /** * Generate a 17-character hex ID. */ public static function generateId(): string { return uniqid() . substr(md5((string) rand()), 0, 4); } /** * Generate an ID with more entropy. */ public static function generateMoreEntropyId(): string { return substr(md5(uniqid((string) rand(), true)), 0, 16) . substr(md5((string) rand()), 0, 4); } /** * Generate a crypt ID. * * @return string */ public static function generateCryptId(): string { return bin2hex(random_bytes(16)); } /** * Generate an API key. */ public static function generateApiKey(): string { return self::generateCryptId(); } /** * Generate a secret key. */ public static function generateSecretKey(): string { return self::generateCryptId(); } /** * Generate a 32-character hex key. */ public static function generateKey(): string { return md5(uniqid((string) rand(), true)); } /** * Sanitize a file name. */ public static function sanitizeFileName(string $fileName): string { /** @var string */ return preg_replace("/([^\w\d\-_~,;:\[\]\(\).])/u", '_', $fileName); } /** * Improved computing the difference of arrays. * * @deprecated As of v7.4. * @param array $array1 * @param array $array2 * @return array */ public static function arrayDiff(array $array1, array $array2) { $diff = []; foreach ($array1 as $key1 => $value1) { if (array_key_exists($key1, $array2)) { if ($value1 !== $array2[$key1]) { $diff[$key1] = $array2[$key1]; } continue; } $diff[$key1] = $value1; } return array_merge($diff, array_diff_key($array2, $array1)); } /** * Fill an array with specific keys. * * @param mixed[]|mixed $keys * @param mixed $value * @return array */ public static function fillArrayKeys($keys, $value) { $arrayKeys = is_array($keys) ? $keys : explode('.', $keys); $array = []; foreach (array_reverse($arrayKeys) as $i => $key) { $array = [ $key => ($i == 0) ? $value : $array, ]; } return $array; } /** * Array keys exists. * * @param mixed[] $keys * @param array $array * @return bool */ public static function arrayKeysExists(array $keys, array $array) { return !array_diff_key(array_flip($keys), $array); } /** * Convert a numeric a memory amount value to a number of bytes it represents. E.g. 1K => 1024. */ public static function convertToByte(string $value): int { $valueTrimmed = trim($value); $last = strtoupper(substr($valueTrimmed, -1)); return match ($last) { 'G' => (int) $valueTrimmed * 1024 * 1024 * 1024 , 'M' => (int) $valueTrimmed * 1024 * 1024, 'K' => (int) $valueTrimmed * 1024, default => (int) $valueTrimmed, }; } /** * Check whether values are equal. * * @param mixed $v1 * @param mixed $v2 */ public static function areValuesEqual($v1, $v2, bool $isUnordered = false): bool { if (is_array($v1) && is_array($v2)) { if ($isUnordered) { sort($v1); sort($v2); } if ($v1 != $v2) { return false; } foreach ($v1 as $i => $itemValue) { if (is_object($itemValue) && is_object($v2[$i])) { if (!self::areValuesEqual($itemValue, $v2[$i])) { return false; } continue; } if ($itemValue !== $v2[$i]) { return false; } } return true; } if (is_object($v1) && is_object($v2)) { if ($v1 != $v2) { return false; } $a1 = get_object_vars($v1); $a2 = get_object_vars($v2); foreach ($a1 as $key => $itemValue) { if (is_object($itemValue) && is_object($a2[$key])) { if (!self::areValuesEqual($itemValue, $a2[$key])) { return false; } continue; } if (is_array($itemValue) && is_array($a2[$key])) { if (!self::areValuesEqual($itemValue, $a2[$key])) { return false; } continue; } if ($itemValue !== $a2[$key]) { return false; } } return true; } return $v1 === $v2; } /** * Upper-case a first letter for multibyte strings. */ public static function mbUpperCaseFirst(string $string): string { if (!$string) { return $string; } $length = mb_strlen($string); $firstChar = mb_substr($string, 0, 1); $then = mb_substr($string, 1, $length - 1); return mb_strtoupper($firstChar) . $then; } /** * Lower-case a first letter for multibyte strings. */ public static function mbLowerCaseFirst(string $string): string { if (!$string) { return $string; } $length = mb_strlen($string); $firstChar = mb_substr($string, 0, 1); $then = mb_substr($string, 1, $length - 1); return mb_strtolower($firstChar) . $then; } /** * Sanitize HTML code. * * @param string|array $text A source. * @param string[] $permittedHtmlTags Allows only HTML tags without parameters like

,
, etc. * @return string|array */ public static function sanitizeHtml($text, $permittedHtmlTags = ['p', 'br', 'b', 'strong', 'pre']) { if (is_array($text)) { foreach ($text as &$value) { $value = self::sanitizeHtml($value, $permittedHtmlTags); } return $text; } $sanitized = htmlspecialchars($text, \ENT_QUOTES | \ENT_HTML5, 'UTF-8'); foreach ($permittedHtmlTags as $htmlTag) { /** @var string $sanitized */ $sanitized = preg_replace('/<(\/)?(' . $htmlTag . ')>/i', '<$1$2>', $sanitized); } return $sanitized; } /** * @deprecated As of v7.4. * @param mixed $paramValue */ public static function urlAddParam(string $url, string $paramName, $paramValue): string { $urlQuery = parse_url($url, \PHP_URL_QUERY); if (!$urlQuery) { $params = [ $paramName => $paramValue ]; $url = trim($url); /** @var string $url */ $url = preg_replace('/\/\?$/', '', $url); /** @var string $url */ $url = preg_replace('/\/$/', '', $url); return $url . '/?' . http_build_query($params); } parse_str($urlQuery, $params); if (!isset($params[$paramName]) || $params[$paramName] != $paramValue) { $params[$paramName] = $paramValue; return str_replace($urlQuery, http_build_query($params), $url); } return $url; } /** * @deprecated As of v7.4. */ public static function urlRemoveParam(string $url, string $paramName, string $suffix = ''): string { $urlQuery = parse_url($url, \PHP_URL_QUERY); if ($urlQuery) { parse_str($urlQuery, $params); if (isset($params[$paramName])) { unset($params[$paramName]); $newUrl = str_replace($urlQuery, http_build_query($params), $url); if (empty($params)) { /** @var string $newUrl */ $newUrl = preg_replace('/\/\?$/', '', $newUrl); /** @var string $newUrl */ $newUrl = preg_replace('/\/$/', '', $newUrl); $newUrl .= $suffix; } return $newUrl; } } return $url; } /** * Generate a password. * * @param int $length A length. * @param int $letters A number of letters. * @param int $digits A number of digits. * @param bool $bothCases Use upper and lower case letters. */ public static function generatePassword( int $length = 8, int $letters = 5, int $digits = 3, bool $bothCases = false, int $specialCharacters = 0 ): string { $chars = [ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', '0123456789', 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz', "'-!\"#$%&()*,./:;?@[]^_`{|}~+<=>", ]; $shuffle = function ($array) { $currentIndex = count($array); while (0 !== $currentIndex) { $rand = (0 + (1 - 0) * (mt_rand() / mt_getrandmax())); $randomIndex = intval(floor($rand * $currentIndex)); $currentIndex -= 1; $temporaryValue = $array[$currentIndex]; $array[$currentIndex] = $array[$randomIndex]; $array[$randomIndex] = $temporaryValue; } return $array; }; $upperCase = 0; $lowerCase = 0; if ($bothCases) { $upperCase = 1; $lowerCase = 1; if ($letters >= 2) { $letters = $letters - 2; } else { $letters = 0; } } $either = $length - ($letters + $digits + $upperCase + $lowerCase + $specialCharacters); if ($either < 0) { $either = 0; } $array = []; foreach ([$letters, $digits, $either, $upperCase, $lowerCase, $specialCharacters] as $i => $len) { $set = $chars[$i]; $subArray = []; $j = 0; while ($j < $len) { $rand = (0 + (1 - 0) * (mt_rand() / mt_getrandmax())); $index = intval(floor($rand * strlen($set))); $subArray[] = $set[$index]; $j++; } $array = array_merge($array, $subArray); } return implode('', $shuffle($array)); } /** * @deprecated Use `normalizeScopeName`. * * @param string $name * @return string */ public static function normilizeScopeName($name) { return self::normalizeScopeName($name); } /** * @deprecated Use `normalizeClassName`. * * @param string $name * @return string */ public static function normilizeClassName($name) { return self::normalizeClassName($name); } }