$replacement) { preg_match_all($pattern, $str, $matches); if (!empty($matches[0])) { if (empty($totalMatches)) { $totalMatches = $matches[0]; } else { $totalMatches = array_merge($totalMatches, $matches[0]); } $match = array_flip($totalMatches); } } } if (!empty($match)) { foreach ($match as $matchKey => & $dontCare) { foreach ($regexRules as $pattern => $replacement) { if (preg_match($pattern, $matchKey)) { $dontCare = $replacement . substr(md5($matchKey), 0, 5); break; } } } return $match; } return []; } /** * @param $str * @param $stringRules * * @return mixed */ public static function redaction($str, $stringRules) { // redact the strings if (!empty($stringRules)) { foreach ($stringRules as $match => $replace) { $str = str_ireplace($match, $replace, $str); } } // return the redacted output return $str; } /** * Determine if a string is composed only of utf8 characters * * @param string $str * Input string. * * @return bool */ public static function isUtf8($str) { $enc = mb_detect_encoding($str, ['UTF-8'], TRUE); return ($enc !== FALSE); } /** * Determine if two hrefs are equivalent (fuzzy match) * * @param string $url1 * The first url to be matched. * @param string $url2 * The second url to be matched against. * * @return bool * true if the urls match, else false */ public static function match($url1, $url2) { $url1 = strtolower($url1); $url2 = strtolower($url2); $url1Str = parse_url($url1); $url2Str = parse_url($url2); if ($url1Str['path'] == $url2Str['path'] && self::extractURLVarValue(CRM_Utils_Array::value('query', $url1Str)) == self::extractURLVarValue(CRM_Utils_Array::value('query', $url2Str)) ) { return TRUE; } return FALSE; } /** * Extract the civicrm path from the url. * * @param string $query * A url string. * * @return string|null * civicrm url (eg: civicrm/contact/search) */ public static function extractURLVarValue($query) { $config = CRM_Core_Config::singleton(); $urlVar = $config->userFrameworkURLVar; $params = explode('&', $query); foreach ($params as $p) { if (strpos($p, '=')) { list($k, $v) = explode('=', $p); if ($k == $urlVar) { return $v; } } } return NULL; } /** * Translate a true/false/yes/no string to a 0 or 1 value * * @param string $str * The string to be translated. * * @return bool */ public static function strtobool($str) { if (!is_scalar($str)) { return FALSE; } if (preg_match('/^(y(es)?|t(rue)?|1)$/i', $str)) { return TRUE; } return FALSE; } /** * Returns string '1' for a true/yes/1 string, and '0' for no/false/0 else returns false * * @param string $str * The string to be translated. * * @return string|false */ public static function strtoboolstr($str) { if (!is_scalar($str)) { return FALSE; } if (preg_match('/^(y(es)?|t(rue)?|1)$/i', $str)) { return '1'; } elseif (preg_match('/^(n(o)?|f(alse)?|0)$/i', $str)) { return '0'; } else { return FALSE; } } /** * Convert a HTML string into a text one using html2text * * @param string $html * The string to be converted. * * @return string * the converted string */ public static function htmlToText($html) { $token_html = preg_replace('!\{([a-z_.]+)\}!i', 'token:{$1}', $html); $converter = new \Html2Text\Html2Text($token_html, ['do_links' => 'table', 'width' => 75]); $token_text = $converter->getText(); $text = preg_replace('!token\:\{([a-z_.]+)\}!i', '{$1}', $token_text); return $text; } /** * @param $string * @param array $params */ public static function extractName($string, &$params) { $name = trim($string); if (empty($name)) { return; } // strip out quotes $name = str_replace('"', '', $name); $name = str_replace('\'', '', $name); // check for comma in name if (strpos($name, ',') !== FALSE) { // name has a comma - assume lname, fname [mname] $names = explode(',', $name); if (count($names) > 1) { $params['last_name'] = trim($names[0]); // check for space delim $fnames = explode(' ', trim($names[1])); if (count($fnames) > 1) { $params['first_name'] = trim($fnames[0]); $params['middle_name'] = trim($fnames[1]); } else { $params['first_name'] = trim($fnames[0]); } } else { $params['first_name'] = trim($names[0]); } } else { // name has no comma - assume fname [mname] fname $names = explode(' ', $name); if (count($names) == 1) { $params['first_name'] = $names[0]; } elseif (count($names) == 2) { $params['first_name'] = $names[0]; $params['last_name'] = $names[1]; } else { $params['first_name'] = $names[0]; $params['middle_name'] = $names[1]; $params['last_name'] = $names[2]; } } } /** * @param $string * * @return array */ public static function &makeArray($string) { $string = trim($string); $values = explode("\n", $string); $result = []; foreach ($values as $value) { list($n, $v) = CRM_Utils_System::explode('=', $value, 2); if (!empty($v)) { $result[trim($n)] = trim($v); } } return $result; } /** * Given an ezComponents-parsed representation of * a text with alternatives return only the first one * * @param string $full * All alternatives as a long string (or some other text). * * @return string * only the first alternative found (or the text without alternatives) */ public static function stripAlternatives($full) { $matches = []; preg_match('/-ALTERNATIVE ITEM 0-(.*?)-ALTERNATIVE ITEM 1-.*-ALTERNATIVE END-/s', ($full ?? ''), $matches); if (isset($matches[1]) && trim(strip_tags($matches[1])) != '' ) { return $matches[1]; } else { return $full; } } /** * Strip leading, trailing, double spaces from string * used for postal/greeting/addressee * * @param string $string * Input string to be cleaned. * * @return string * the cleaned string */ public static function stripSpaces($string) { return (empty($string)) ? $string : preg_replace("/\s{2,}/", " ", trim($string)); } /** * clean the URL 'path' variable that we use * to construct CiviCRM urls by removing characters from the path variable * * @param string $string * The input string to be sanitized. * @param array $search * The characters to be sanitized. * @param string $replace * The character to replace it with. * * @return string * the sanitized string */ public static function stripPathChars( $string, $search = NULL, $replace = NULL ) { static $_searchChars = NULL; static $_replaceChar = NULL; if (empty($string)) { return $string; } if ($_searchChars == NULL) { $_searchChars = [ '&', ';', ',', '=', '$', '"', "'", '\\', '<', '>', '(', ')', ' ', "\r", "\r\n", "\n", "\t", ]; $_replaceChar = '_'; } if ($search == NULL) { $search = $_searchChars; } if ($replace == NULL) { $replace = $_replaceChar; } return str_replace($search, $replace, $string); } /** * Use HTMLPurifier to clean up a text string and remove any potential * xss attacks. This is primarily used in public facing pages which * accept html as the input string * * @param string $string * The input string. * * @return string * the cleaned up string */ public static function purifyHTML($string) { static $_filter = NULL; if (!$_filter) { $config = HTMLPurifier_Config::createDefault(); $config->set('Core.Encoding', 'UTF-8'); $config->set('Attr.AllowedFrameTargets', ['_blank', '_self', '_parent', '_top']); // Disable the cache entirely $config->set('Cache.DefinitionImpl', NULL); $_filter = new HTMLPurifier($config); } return $_filter->purify($string ?? ''); } /** * Truncate $string; if $string exceeds $maxLen, place "..." at the end * * @param string $string * @param int $maxLen * * @return string */ public static function ellipsify($string, $maxLen) { if (mb_strlen($string, 'UTF-8') <= $maxLen) { return $string; } return mb_substr($string, 0, $maxLen - 3, 'UTF-8') . '...'; } /** * Generate a random string. * * @param $len * @param $alphabet * @return string */ public static function createRandom($len, $alphabet) { $alphabetSize = strlen($alphabet); $result = ''; for ($i = 0; $i < $len; $i++) { $result .= $alphabet[rand(1, $alphabetSize) - 1]; } return $result; } /** * Examples: * "admin foo" => array(NULL,"admin foo") * "cms:admin foo" => array("cms", "admin foo") * * @param string $delim * @param string $string * E.g. "view all contacts". Syntax: "[prefix:]name". * @param string|null $defaultPrefix * * @return array * (0 => string|NULL $prefix, 1 => string $value) */ public static function parsePrefix($delim, $string, $defaultPrefix = NULL) { $pos = strpos($string, $delim); if ($pos === FALSE) { return [$defaultPrefix, $string]; } else { return [substr($string, 0, $pos), substr($string, 1 + $pos)]; } } /** * This function will mask part of the the user portion of an Email address (everything before the @) * * @param string $email * The email address to be masked. * @param string $maskChar * The character used for masking. * @param int $percent * The percentage of the user portion to be masked. * * @return string * returns the masked Email address */ public static function maskEmail($email, $maskChar = '*', $percent = 50) { list($user, $domain) = preg_split("/@/", $email); $len = strlen($user); $maskCount = floor($len * $percent / 100); $offset = floor(($len - $maskCount) / 2); $masked = substr($user, 0, $offset) . str_repeat($maskChar, $maskCount) . substr($user, $maskCount + $offset); return ($masked . '@' . $domain); } /** * This function compares two strings. * * @param string $strOne * String one. * @param string $strTwo * String two. * @param bool $case * Boolean indicating whether you want the comparison to be case sensitive or not. * * @return bool * TRUE (string are identical); FALSE (strings are not identical) */ public static function compareStr($strOne, $strTwo, $case) { if ($case == TRUE) { // Convert to lowercase and trim white spaces if (strtolower(trim($strOne)) == strtolower(trim($strTwo))) { // yes - they are identical return TRUE; } else { // not identical return FALSE; } } if ($case == FALSE) { // Trim white spaces if (trim($strOne) == trim($strTwo)) { // yes - they are identical return TRUE; } else { // not identical return FALSE; } } } /** * Many parts of the codebase have a convention of internally passing around * HTML-encoded URLs. This effectively means that "&" is replaced by "&" * (because most other odd characters are %-escaped in URLs; and %-escaped * strings don't need any extra escaping in HTML). * * @param string $htmlUrl * URL with HTML entities. * @return string * URL without HTML entities */ public static function unstupifyUrl($htmlUrl) { return str_replace('&', '&', $htmlUrl); } /** * When a user supplies a URL (e.g. to an image), we'd like to: * - Remove the protocol and domain name if the URL points to the current * site. * - Keep the domain name for remote URLs. * - Optionally, force remote URLs to use https instead of http (which is * useful for images) * * @param string $url * The URL to simplify. Examples: * "https://example.org/sites/default/files/coffee-mug.jpg" * "sites/default/files/coffee-mug.jpg" * "http://i.stack.imgur.com/9jb2ial01b.png" * @param bool $forceHttps = FALSE * If TRUE, ensure that remote URLs use https. If a URL with * http is supplied, then we'll change it to https. * This is useful for situations like showing a premium product on a * contribution, because (as reported in CRM-14283) if the user gets a * browser warning like "page contains insecure elements" on a contribution * page, that's a very bad thing. Thus, even if changing http to https * breaks the image, that's better than leaving http content in a * contribution page. * * @return string * The simplified URL. Examples: * "/sites/default/files/coffee-mug.jpg" * "https://i.stack.imgur.com/9jb2ial01b.png" */ public static function simplifyURL($url, $forceHttps = FALSE) { $config = CRM_Core_Config::singleton(); $siteURLParts = self::simpleParseUrl($config->userFrameworkBaseURL); $urlParts = self::simpleParseUrl($url); // If the image is locally hosted, then only give the path to the image $urlIsLocal = ($urlParts['host+port'] == '') | ($urlParts['host+port'] == $siteURLParts['host+port']); if ($urlIsLocal) { // and make sure it begins with one forward slash return preg_replace('_^/*(?=.)_', '/', $urlParts['path+query']); } // If the URL is external, then keep the full URL as supplied else { return $forceHttps ? preg_replace('_^http://_', 'https://', $url) : $url; } } /** * A simplified version of PHP's parse_url() function. * * @param string $url * e.g. "https://example.com:8000/foo/bar/?id=1#fragment" * * @return array * Will always contain keys 'host+port' and 'path+query', even if they're * empty strings. Example: * [ * 'host+port' => "example.com:8000", * 'path+query' => "/foo/bar/?id=1", * ] */ public static function simpleParseUrl($url) { $parts = parse_url($url); $host = $parts['host'] ?? ''; $port = isset($parts['port']) ? ':' . $parts['port'] : ''; $path = $parts['path'] ?? ''; $query = isset($parts['query']) ? '?' . $parts['query'] : ''; return [ 'host+port' => "$host$port", 'path+query' => "$path$query", ]; } /** * Formats a string of attributes for insertion in an html tag. * * @param array $attributes * * @return string */ public static function htmlAttributes($attributes) { $output = ''; foreach ($attributes as $name => $vals) { $output .= " $name=\"" . htmlspecialchars(implode(' ', (array) $vals)) . '"'; } return ltrim($output); } /** * Determine if $string starts with $fragment. * * @param string $string * The long string. * @param string $fragment * The fragment to look for. * @return bool */ public static function startsWith($string, $fragment) { if ($fragment === '') { return TRUE; } $len = strlen($fragment ?? ''); return substr(($string ?? ''), 0, $len) === $fragment; } /** * Determine if $string ends with $fragment. * * @param string $string * The long string. * @param string $fragment * The fragment to look for. * @return bool */ public static function endsWith($string, $fragment) { if ($fragment === '') { return TRUE; } $len = strlen($fragment ?? ''); return substr(($string ?? ''), -1 * $len) === $fragment; } /** * @param string|array $patterns * @param array $allStrings * @param bool $allowNew * Whether to return new, unrecognized names. * @return array */ public static function filterByWildcards($patterns, $allStrings, $allowNew = FALSE) { $patterns = (array) $patterns; $result = []; foreach ($patterns as $pattern) { if (!\CRM_Utils_String::endsWith($pattern, '*')) { if ($allowNew || in_array($pattern, $allStrings)) { $result[] = $pattern; } } else { $prefix = rtrim($pattern, '*'); foreach ($allStrings as $key) { if (\CRM_Utils_String::startsWith($key, $prefix)) { $result[] = $key; } } } } return array_values(array_unique($result)); } /** * Safely unserialize a string of scalar or array values (but not objects!) * * Use `xkerman/restricted-unserialize` to unserialize strings using PHP's * serialization format. `restricted-unserialize` works like PHP's built-in * `unserialize` function except that it does not deserialize object instances, * making it immune to PHP Object Injection {@see https://www.owasp.org/index.php/PHP_Object_Injection} * vulnerabilities. * * Note: When dealing with user inputs, it is generally recommended to use * safe, standard data interchange formats such as JSON rather than PHP's * serialization format when dealing with user input. * * @param string|null $string * * @return mixed */ public static function unserialize($string) { if (!is_string($string)) { return FALSE; } try { return unserialize($string); } catch (UnserializeFailedException $e) { return FALSE; } } /** * Returns the plural form of an English word. * * @param string $str * @return string */ public static function pluralize($str) { $lastLetter = substr($str, -1); $lastTwo = substr($str, -2); if ($lastLetter == 's' || $lastLetter == 'x' || $lastTwo == 'ch') { return $str . 'es'; } if ($lastLetter == 'y' && !in_array($lastTwo, ['ay', 'ey', 'iy', 'oy', 'uy'])) { return substr($str, 0, -1) . 'ies'; } return $str . 's'; } /** * Generic check as to whether any tokens are in the given string. * * It might be a smarty token OR a CiviCRM token. In both cases the * absence of a '{' indicates no token is present. * * @param string $string * * @return bool */ public static function stringContainsTokens(string $string) { return strpos($string, '{') !== FALSE; } /** * Parse a string through smarty without creating a smarty template file per string. * * This function is for swapping out any smarty tokens that appear in a string * and are not re-used much if at all. For example parsing a contact's greeting * does not need to be cached are there are some minor security / data privacy benefits * to not caching them per file. We also save disk space, reduce I/O and disk clearing time. * * Doing this is cleaning in Smarty3 which we are alas not using * https://www.smarty.net/docs/en/resources.string.tpl * * However, it highlights that smarty-eval is not evil-eval and still have the security applied. * * In order to replicate that in Smarty2 I'm using {eval} per * https://www.smarty.net/docsv2/en/language.function.eval.tpl#id2820446 * From the above: * - Evaluated variables are treated the same as templates. They follow the same escapement and security features just as if they were templates. * - Evaluated variables are compiled on every invocation, the compiled versions are not saved! However if you have caching enabled, the output * will be cached with the rest of the template. * * Our set up does not have caching enabled and my testing suggests this still works fine with it * enabled so turning it off before running this is out of caution based on the above. * * When this function is run only one template file is created (for the eval) tag no matter how * many times it is run. This compares to it otherwise creating one file for every parsed string. * * @param string $templateString * * @return string */ public static function parseOneOffStringThroughSmarty($templateString) { if (!CRM_Utils_String::stringContainsTokens($templateString)) { // Skip expensive smarty processing. return $templateString; } $smarty = CRM_Core_Smarty::singleton(); $cachingValue = $smarty->caching; $smarty->caching = 0; $smarty->assign('smartySingleUseString', $templateString); // Do not escape the smartySingleUseString as that is our smarty template // and is likely to contain html. $templateString = (string) $smarty->fetch('string:{eval var=$smartySingleUseString|smarty:nodefaults}'); $smarty->caching = $cachingValue; $smarty->assign('smartySingleUseString', NULL); return $templateString; } }