0x7E || $d == 0x25) { // these characters are always encoded $out .= "%".strtoupper(dechex($d)); } elseif (preg_match("/[a-zA-Z0-9\._~-]/", $dc)) { // these characters are never encoded $out .= $dc; } else { // these characters are passed through as-is if ($c === "%") { $out .= "%".strtoupper(dechex($d)); } else { $out .= $c; } } $pos++; } return $out; } /** Normalizes a hostname per IDNA:2008 */ protected static function normalizeHost(string $host): string { if ($host[0] === "[" && substr($host, -1) === "]") { // normalize IPv6 addresses $addr = @inet_pton(substr($host, 1, strlen($host) - 2)); if ($addr !== false) { return "[".inet_ntop($addr)."]"; } } $idn = idn_to_ascii($host, \IDNA_NONTRANSITIONAL_TO_ASCII, \INTL_IDNA_VARIANT_UTS46); return $idn !== false ? idn_to_utf8($idn, \IDNA_NONTRANSITIONAL_TO_UNICODE, \INTL_IDNA_VARIANT_UTS46) : $host; } /** Normalizes the whole path segment to remove empty segments and relative segments */ protected static function normalizePath(string $path, bool $hasHost): string { $parts = explode("/", self::normalizeEncoding($path)); $absolute = ($hasHost || $path[0] === "/"); $index = (substr($path, -1) === "/"); $out = []; foreach ($parts as $p) { switch ($p) { case "": case ".": break; case "..": array_pop($out); break; default: $out[] = $p; } } $out = implode("/", $out); $out = ($absolute ? "/" : "").$out.($index ? "/" : ""); return str_replace("//", "/", $out); } /** Appends data to a URL's query component * * @param string $url The input URL * @param string $data The data to append. This should already be escaped where necessary and not start with any delimiter * @param string $glue The query subcomponent delimiter, usually "&". If the URL has no query, "?" will be prepended instead */ public static function queryAppend(string $url, string $data, string $glue = "&"): string { if (!strlen($data)) { return $url; } $insPos = strpos($url, "#"); $insPos = $insPos === false ? strlen($url) : $insPos; $qPos = strpos($url, "?"); $hasQuery = $qPos !== false; $glue = $hasQuery ? $glue : "?"; if ($hasQuery && $insPos > 0) { if ($url[$insPos - 1] === $glue || ($insPos - 1) == $qPos) { // if the URL already has excess glue, use it $glue = ""; } } return substr($url, 0, $insPos).$glue.$data.substr($url, $insPos); } }