parseFragment($target); $target = $this->parseQuery($target); $this->path = $this->parsePath($target); } public function __toString(): string { $out = ""; $path = []; foreach ($this->path as $segment) { if (is_null($segment)) { if (!$path) { $path[] = ".."; } else { continue; } } elseif ($segment === ".") { $path[] = "%2E"; } elseif ($segment === "..") { $path[] = "%2E%2E"; } else { $path[] = rawurlencode(ValueInfo::normalize($segment, ValueInfo::T_STRING)); } } $path = implode("/", $path); if (!$this->relative) { $out .= "/"; } $out .= $path; if ($this->index && strlen($path)) { $out .= "/"; } if (strlen($this->query)) { $out .= "?".$this->query; } if (strlen($this->fragment)) { $out .= "#".rawurlencode($this->fragment); } return $out; } public static function normalize(string $target): string { return (string) new self($target); } protected function parseFragment(string $target): string { // store and strip off any fragment identifier and return the target without a fragment $pos = strpos($target, "#"); if ($pos !== false) { $this->fragment = rawurldecode(substr($target, $pos + 1)); $target = substr($target, 0, $pos); } return $target; } protected function parseQuery(string $target): string { // store and strip off any query string and return the target without a query // note that the function assumes any fragment identifier has already been stripped off // unlike the other parts the query string is currently neither parsed nor normalized $pos = strpos($target, "?"); if ($pos !== false) { $this->query = substr($target, $pos + 1); $target = substr($target, 0, $pos); } return $target; } protected function parsePath(string $target): array { // note that the function assumes any fragment identifier or query has already been stripped off // syntax-based normalization is applied to the path segments (see RFC 3986 sec. 6.2.2) // duplicate slashes are NOT collapsed if (substr($target, 0, 1) === "/") { // if the path starts with a slash, strip it off $target = substr($target, 1); } else { // otherwise this is a relative target $this->relative = true; } if (!strlen($target)) { // if the target is an empty string, this is an index target $this->index = true; } elseif (substr($target, -1, 1) === "/") { // if the path ends in a slash, this is an index target and the slash should be stripped off $this->index = true; $target = substr($target, 0, strlen($target) -1); } // after stripping, explode the path parts if (strlen($target)) { $target = explode("/", $target); $out = []; // resolve relative path segments and decode each retained segment foreach ($target as $index => $segment) { if ($segment === ".") { // self-referential segments can be ignored continue; } elseif ($segment === "..") { if ($index == 0) { // if the first path segment refers to its parent (which we don't know about) we cannot output a correct path, so we do the best we can $out[] = null; } else { // for any other segments after the first we pop off the last stored segment array_pop($out); } } else { // any other segment is decoded and retained $out[] = rawurldecode($segment); } } return $out; } else { return []; } } }