/** * Replaces any reference to one of the framework's special directories in a path with the directory's actual path * and returns the usable path. * * A framework's directory is referenced in a path by wrapping its ID into double curly braces, as in * "{{PHRED_PATH_TO_FRAMEWORK_ROOT}}", optionally with "/" after the reference. * * @param string $path The path to the file or directory (can be absolute or relative). * * @return CUStringObject The usable path. */ public static function frameworkPath($path) { assert('!isset($path) || is_cstring($path)', vs(isset($this), get_defined_vars())); if (!isset($path)) { return null; } // Replace every "{{EXAMPLE_PATH}}" in the path string with the value of "EXAMPLE_PATH" key from $GLOBALS // variable if such key exists in the variable. $modified = false; $path = CRegex::replaceWithCallback($path, "/\\{\\{\\w+\\}\\}/", function ($matches) use(&$modified) { $pathVarName = CString::substr($matches[0], 2, CString::length($matches[0]) - 4); if (isset($GLOBALS[$pathVarName])) { $modified = true; return $GLOBALS[$pathVarName] . "/"; } else { assert('false', vs(isset($this), get_defined_vars())); return $matches[0]; } }); if ($modified) { $path = CRegex::replace($path, "/\\/{2,}/", "/"); } return $path; }
public function testReplaceWithCallback() { // ASCII. $res = CRegex::replaceWithCallback("Hello there!", "/[eo]/", function ($matches) { return CString::equals($matches[0], "e") ? "3" : $matches[0]; }); $this->assertTrue(CString::equals($res, "H3llo th3r3!")); $quantity; $res = CRegex::replaceWithCallback("Hello there!", "/[eo]/", function ($matches) { return CString::equals($matches[0], "e") ? "3" : $matches[0]; }, $quantity); $this->assertTrue(CString::equals($res, "H3llo th3r3!") && $quantity == 4); // Unicode. $res = CRegex::replaceWithCallback("¡Hello señor!", "/[eoñ]/u", function ($matches) { return CUString::equals($matches[0], "ñ") ? "n" : $matches[0]; }); $this->assertTrue(CUString::equals($res, "¡Hello senor!")); $quantity; $res = CRegex::replaceWithCallback("¡Hello señor!", "/[eoñ]/u", function ($matches) { return CUString::equals($matches[0], "ñ") ? "n" : $matches[0]; }, $quantity); $this->assertTrue(CUString::equals($res, "¡Hello senor!") && $quantity == 5); }
/** * Creates a parsed URL from a URL string. * * The URL string is expected to be by all means valid, with characters being percent-encoded where it is required * by the URL standard and without any leading or trailing whitespace. It is only when the URL string does not * indicate any protocol that the URL may still be considered valid and the default protocol is assigned to the * URL, which is "http". * * @param string $url The URL string. */ public function __construct($url) { assert('is_cstring($url)', vs(isset($this), get_defined_vars())); assert('self::isValid($url, true)', vs(isset($this), get_defined_vars())); $this->m_url = $url; $parsedUrl = parse_url($url); assert('is_cmap($parsedUrl)', vs(isset($this), get_defined_vars())); // should not rise for a valid URL // Protocol (scheme). if (CMap::hasKey($parsedUrl, "scheme")) { $this->m_hasProtocol = true; $this->m_protocol = $parsedUrl["scheme"]; // Normalize by converting to lowercase. $this->m_normProtocol = CString::toLowerCase($this->m_protocol); } else { $this->m_hasProtocol = false; $this->m_normProtocol = self::DEFAULT_PROTOCOL; if (!CMap::hasKey($parsedUrl, "host")) { // Most likely, `parse_url` function has not parsed the host because the protocol (scheme) is absent // and there are no "//" in the front, so try parsing the host with the default protocol in the URL. $parsedUrl = parse_url(self::ensureProtocol($url)); assert('is_cmap($parsedUrl)', vs(isset($this), get_defined_vars())); CMap::remove($parsedUrl, "scheme"); } } // Host (domain). $this->m_hostIsInBrackets = false; if (CMap::hasKey($parsedUrl, "host")) { $this->m_host = $parsedUrl["host"]; if (CRegex::find($this->m_host, "/^\\[.*\\]\\z/")) { // Most likely, an IPv6 enclosed in "[]". $this->m_hostIsInBrackets = true; $this->m_host = CString::substr($this->m_host, 1, CString::length($this->m_host) - 2); } // Normalize by converting to lowercase. $this->m_normHost = CString::toLowerCase($this->m_host); } else { // Same as invalid. assert('false', vs(isset($this), get_defined_vars())); } // Port. if (CMap::hasKey($parsedUrl, "port")) { $this->m_hasPort = true; $this->m_port = $parsedUrl["port"]; // Should be `int`, but look into the type just in case. if (is_cstring($this->m_port)) { $this->m_port = CString::toInt($this->m_port); } } else { $this->m_hasPort = false; } // Path. if (CMap::hasKey($parsedUrl, "path")) { $this->m_hasPath = true; $this->m_path = $parsedUrl["path"]; // Normalize by replacing percent-encoded bytes of unreserved characters with their literal equivalents and // ensuring that all percent-encoded parts are in uppercase. $pathDelimitersReEsc = CRegex::enterTd(self::$ms_delimiters); $this->m_normPath = CRegex::replaceWithCallback($this->m_path, "/[^{$pathDelimitersReEsc}]+/", function ($matches) { return CUrl::enterTdNew(CUrl::leaveTdNew($matches[0])); }); } else { $this->m_hasPath = false; $this->m_normPath = "/"; } $this->m_urlPath = new CUrlPath($this->m_normPath); // Query string. $this->m_hasQuery = false; if (CMap::hasKey($parsedUrl, "query")) { $this->m_hasQuery = true; $this->m_queryString = $parsedUrl["query"]; $parsingWasFruitful; $this->m_urlQuery = new CUrlQuery($this->m_queryString, $parsingWasFruitful); if ($parsingWasFruitful) { $this->m_hasQuery = true; $this->m_normQueryString = $this->m_urlQuery->queryString(true); } } // Fragment ID. if (CMap::hasKey($parsedUrl, "fragment")) { $this->m_hasFragmentId = true; $this->m_fragmentId = $parsedUrl["fragment"]; // Normalize by replacing percent-encoded bytes of unreserved characters with their literal equivalents and // ensuring that all percent-encoded parts are in uppercase. $fiDelimitersReEsc = CRegex::enterTd(self::$ms_delimiters); $this->m_normFragmentId = CRegex::replaceWithCallback($this->m_fragmentId, "/[^{$fiDelimitersReEsc}]+/", function ($matches) { // Use the newer flavor of percent-encoding. return CUrl::enterTdNew(CUrl::leaveTdNew($matches[0])); }); } else { $this->m_hasFragmentId = false; } // User. if (CMap::hasKey($parsedUrl, "user")) { $this->m_hasUser = true; $this->m_user = $parsedUrl["user"]; } else { $this->m_hasUser = false; } // Password. if (CMap::hasKey($parsedUrl, "pass")) { $this->m_hasPassword = true; $this->m_password = $parsedUrl["pass"]; } else { $this->m_hasPassword = false; } // Compose the normalized URL string. $this->m_normUrl = ""; $this->m_normUrl .= $this->m_normProtocol . "://"; if ($this->m_hasUser) { $this->m_normUrl .= $this->m_user; } if ($this->m_hasPassword) { $this->m_normUrl .= ":" . $this->m_password; } if ($this->m_hasUser || $this->m_hasPassword) { $this->m_normUrl .= "@"; } if (!$this->m_hostIsInBrackets) { $this->m_normUrl .= $this->m_normHost; } else { $this->m_normUrl .= "[" . $this->m_normHost . "]"; } if ($this->m_hasPort) { // Normalize by skipping port indication if the port is the default one for the protocol. if (!(CMap::hasKey(self::$ms_knownProtocolToDefaultPort, $this->m_normProtocol) && self::$ms_knownProtocolToDefaultPort[$this->m_normProtocol] == $this->m_port)) { $this->m_normUrl .= ":" . $this->m_port; } } $this->m_normUrl .= $this->m_normPath; if ($this->m_hasQuery) { $this->m_normUrl .= "?" . $this->m_normQueryString; } $this->m_normUrlWithoutFragmentId = $this->m_normUrl; if ($this->m_hasFragmentId) { $this->m_normUrl .= "#" . $this->m_normFragmentId; } }
/** * Replaces any occurrence of a regular expression pattern in a string with the string returned by a function or * method called on the matching substring and returns the new string, optionally reporting the number of * replacements made. * * @param string $whatPattern The pattern to be replaced. * @param callable $callback A function or method that, as imposed by PHP's PCRE, takes a map as a parameter, * which contains the matching substring under the key of `0`, the substring that matched the first group, if any, * under the key of `1`, and so on for groups, and returns the string with which the matching substring should be * replaced. * @param reference $quantity **OPTIONAL. OUTPUT.** After the method is called with this parameter provided, the * parameter's value, which is of type `int`, indicates the number of replacements made. * * @return CUStringObject The resulting string. */ public function reReplaceWithCallback($whatPattern, $callback, &$quantity = null) { $whatPattern = self::ensureUModifier($whatPattern); $useCallback = function ($matches) use($callback) { $matches = to_oop($matches); return call_user_func($callback, $matches); }; return CRegex::replaceWithCallback($this, $whatPattern, $useCallback, $quantity); }
/** * Composes a URL query into a query string ready to be used as a part of a URL and returns it. * * Any characters that cannot be represented literally in a valid query string come out percent-encoded. The * resulting query string never starts with "?". * * Because the characters in field named and field values are stored in their literal representations, the * resulting query string is always normalized, with only those characters appearing percent-encoded that really * require it for the query string to be valid and with the hexadecimal letters in percent-encoded characters * appearing uppercased. Also, no duplicate fields are produced in the resulting query string (even if the object * was constructed from a query string with duplicate fields in it) and "=" is added after any field name that goes * without a value and is not followed by "=". * * @param bool $sortFields **OPTIONAL. Default is** `false`. Tells whether the fields in the query string should * appear sorted in the ascending order, case-insensitively, and with natural order comparison used for sorting. * * @return CUStringObject The query string. */ public function queryString($sortFields = false) { assert('is_bool($sortFields)', vs(isset($this), get_defined_vars())); if (!CMap::isEmpty($this->m_query)) { $useQuery = CMap::makeCopy($this->m_query); // Recursively convert any CArray into a CMap for `http_build_query` function to accept the query. $useQuery = self::recurseQueryValueBeforeComposingQs($useQuery, 0); // Compose a preliminary query string. $queryString = http_build_query($useQuery, "", self::$ms_fieldDelimiters[0], PHP_QUERY_RFC1738); if (!is_cstring($queryString)) { return ""; } // Break the string into fields. $fields = CString::split($queryString, self::$ms_fieldDelimiters[0]); // Adjust the result of `http_build_query` function. $len = CArray::length($fields); for ($i = 0; $i < $len; $i++) { if (CString::find($fields[$i], "=")) { // Revert excessive percent-encoding of the square brackets next to the identifiers of // multidimensional data. $fields[$i] = CRegex::replaceWithCallback($fields[$i], "/(?:%5B(?:[^%]++|%(?!5B|5D))*+%5D)+?=/i", function ($matches) { $value = $matches[0]; $value = CString::replace($value, "%5B", "["); $value = CString::replace($value, "%5D", "]"); return $value; }); // Remove redundant indexing next to the identifiers of simple arrays. $fields[$i] = CRegex::replaceWithCallback($fields[$i], "/^.+?=/", function ($matches) { return CRegex::replace($matches[0], "/\\[\\d+\\]/", "[]"); }); } } if ($sortFields) { // Normalize the order of fields. CArray::sortStringsNatCi($fields); } $queryString = CArray::join($fields, self::$ms_fieldDelimiters[0]); return $queryString; } else { return ""; } }
/** * Decodes the JSON-encoded string provided earlier to the decoder and returns the result. * * @param reference $success **OPTIONAL. OUTPUT.** After the method is called with this parameter provided, the * parameter's value tells whether the decoding was successful. * * @return mixed The decoded value of type `CMapObject` or `CArrayObject`. */ public function decode(&$success = null) { assert('is_cstring($this->m_source)', vs(isset($this), get_defined_vars())); $success = true; $source = $this->m_source; if ($this->m_decodingStrictness == self::LENIENT && !CUString::isValid($source)) { // Change the character encoding or try fixing it. if (CEString::looksLikeLatin1($source)) { $source = CEString::convertLatin1ToUtf8($source); } else { $source = CEString::fixUtf8($source); } } if ($this->m_decodingStrictness == self::STRICT_WITH_COMMENTS || $this->m_decodingStrictness == self::LENIENT) { if (CRegex::find($source, "/\\/\\/|\\/\\*/u")) { // Remove "//..." and "/*...*/" comments. $source = CRegex::remove($source, "/(?<!\\\\)\"(?:[^\\\\\"]++|\\\\{2}|\\\\\\C)*\"(*SKIP)(*FAIL)|" . "\\/\\/.*|\\/\\*\\C*?\\*\\//u"); } } if ($this->m_decodingStrictness == self::LENIENT) { if (CRegex::find($source, "/[:\\[,]\\s*'([^\\\\']++|\\\\{2}|\\\\\\C)*'(?=\\s*[,}\\]])/u")) { // Convert single-quoted string values into double-quoted, taking care of double quotes within such // strings before and single quotes after. This needs to go in front of the rest of the leniency fixes. while (true) { $prevSource = $source; $source = CRegex::replace($source, "/(?<!\\\\)\"(?:[^\\\\\"]++|\\\\{2}|\\\\\\C)*\"(*SKIP)(*FAIL)|" . "([:\\[,]\\s*'(?:[^\\\\'\"]++|\\\\{2}|\\\\\\C)*)\"((?:[^\\\\']++|\\\\{2}|\\\\\\C)*')/u", "\$1\\\"\$2"); if (CString::equals($source, $prevSource) || is_null($source)) { break; } } if (is_null($source)) { $source = ""; } $source = CRegex::replace($source, "/(?<!\\\\)\"(?:[^\\\\\"]++|\\\\{2}|\\\\\\C)*\"(*SKIP)(*FAIL)|" . "([:\\[,]\\s*)'((?:[^\\\\']++|\\\\{2}|\\\\\\C)*)'(?=\\s*[,}\\]])/u", "\$1\"\$2\""); while (true) { $prevSource = $source; $source = CRegex::replace($source, "/([:\\[,]\\s*\"(?:[^\\\\\"]++|\\\\{2}|\\\\[^'])*)\\\\'((?:[^\\\\\"]++|\\\\{2}|\\\\\\C)*\")" . "(?=\\s*[,}\\]])/u", "\$1'\$2"); if (CString::equals($source, $prevSource) || is_null($source)) { break; } } if (is_null($source)) { $source = ""; } } if (CRegex::find($source, "/[{,]\\s*[\\w\\-.]+\\s*:/u")) { // Put property names in double quotes. $source = CRegex::replace($source, "/(?<!\\\\)\"(?:[^\\\\\"]++|\\\\{2}|\\\\\\C)*\"(*SKIP)(*FAIL)|" . "([{,]\\s*)([\\w\\-.]+)(\\s*:)/u", "\$1\"\$2\"\$3"); } if (CRegex::find($source, "/[{,]\\s*'[\\w\\-.]+'\\s*:/u")) { // Put property names that are in single quotes in double quotes. $source = CRegex::replace($source, "/(?<!\\\\)\"(?:[^\\\\\"]++|\\\\{2}|\\\\\\C)*\"(*SKIP)(*FAIL)|" . "([{,]\\s*)'([\\w\\-.]+)'(\\s*:)/u", "\$1\"\$2\"\$3"); } if (CRegex::find($source, "/,\\s*[}\\]]/u")) { // Remove trailing commas. $source = CRegex::remove($source, "/(?<!\\\\)\"(?:[^\\\\\"]++|\\\\{2}|\\\\\\C)*\"(*SKIP)(*FAIL)|" . ",(?=\\s*[}\\]])/u"); } // Within string values, convert byte values for BS, FF, LF, CR, and HT, which are prohibited in JSON, // to their escaped equivalents. $stringValueSubjectRe = "/(?<!\\\\)\"(?:[^\\\\\"]++|\\\\{2}|\\\\\\C)*\"/u"; $source = CRegex::replaceWithCallback($source, $stringValueSubjectRe, function ($matches) { return CRegex::replace($matches[0], "/\\x{0008}/u", "\\b"); }); $source = CRegex::replaceWithCallback($source, $stringValueSubjectRe, function ($matches) { return CRegex::replace($matches[0], "/\\x{000C}/u", "\\f"); }); $source = CRegex::replaceWithCallback($source, $stringValueSubjectRe, function ($matches) { return CRegex::replace($matches[0], "/\\x{000A}/u", "\\n"); }); $source = CRegex::replaceWithCallback($source, $stringValueSubjectRe, function ($matches) { return CRegex::replace($matches[0], "/\\x{000D}/u", "\\r"); }); $source = CRegex::replaceWithCallback($source, $stringValueSubjectRe, function ($matches) { return CRegex::replace($matches[0], "/\\x{0009}/u", "\\t"); }); } $decodedValue = @json_decode($source, false, self::$ms_maxRecursionDepth); if (is_null($decodedValue)) { if ($this->m_decodingStrictness == self::STRICT || $this->m_decodingStrictness == self::STRICT_WITH_COMMENTS) { $success = false; } else { if (CRegex::find($source, "/^\\s*[\\w.]+\\s*\\(/u")) { // The source string appears to be a JSONP. Extract the function's argument and try decoding again. $source = CRegex::replace($source, "/^\\s*[\\w.]+\\s*\\((\\C+)\\)/u", "\$1"); $decodedValue = @json_decode($source, false, self::$ms_maxRecursionDepth); if (is_null($decodedValue)) { $success = false; } } } } if (!$success) { return; } if ($this->m_decodingStrictness == self::STRICT || $this->m_decodingStrictness == self::STRICT_WITH_COMMENTS) { if (!is_object($decodedValue) && !is_array($decodedValue)) { $success = false; return; } } // Recursively convert any object into a CMapObject/CMap and any PHP array into a CArrayObject/CArray. $decodedValue = self::recurseValueAfterDecoding($decodedValue, 0); return $decodedValue; }