3v4l.org

run code in 150+ php & hhvm versions
Bugs & Features
<?php /** * Class URL * * @package HTTP * @property string $scheme * @property string $user * @property string $pass * @property string $host * @property string $port * @property string $path * @property string $fragment */ class URL { /** * @var string */ private $scheme; /** * @var string */ private $user; /** * @var string */ private $pass; /** * @var string */ private $host; /** * @var int */ private $port; /** * @var string */ private $path; /** * @var array|string * * NOTE: do not attempt to validate this on access because it's too difficult to do sensibly */ public $query = []; /** * @var string */ private $fragment; /** * @var string[] */ private static $stringProperties = ['scheme', 'user', 'pass', 'host', 'path', 'fragment']; /** * Wrapper for parse_url() * * @param string $string * @return URL * @throws \InvalidArgumentException */ public static function createFromString($string) { if (false === $parts = parse_url($string)) { throw new \InvalidArgumentException($string . ' could not be parsed as a valid URL'); } return new static( isset($parts['scheme']) ? $parts['scheme'] : null, isset($parts['user']) ? $parts['user'] : null, isset($parts['pass']) ? $parts['pass'] : null, isset($parts['host']) ? $parts['host'] : null, isset($parts['port']) ? $parts['port'] : null, isset($parts['path']) ? $parts['path'] : null, isset($parts['query']) ? $parts['query'] : null, isset($parts['fragment']) ? $parts['fragment'] : null, true ); } /** * Resolve $target as a relative URL against this, using the same rules as a browser * * @param string|URL $target * @return URL */ public function resolve($target) { if (!($target instanceof static)) { $target = static::createFromString((string) $target); } // returning the same instance sometimes but not others would be confusing $result = clone $target; if (!isset($target->scheme)) { // anything with a scheme is considered absolute if (isset($target->host)) { // similarly anything with a host is absolute, just add a scheme is we have one if (isset($this->scheme)) { $result->scheme = $this->scheme; } } else { // host/scheme portion not specified, inherit from source foreach (['scheme', 'user', 'pass', 'host', 'port'] as $prop) { if (isset($this->{$prop})) { $result->{$prop} = $this->{$prop}; } } if ($target->path === '') { $target->path = '/'; } if ($target->path[0] === '/') { // If the target path is absolute, canonicalize it and use it $resultPath = self::resolveCanonicalPathComponents($target->path); } else { // Target path is relative // First we resolve the source path to a canonical and remove the file name component $sourcePath = self::resolveCanonicalPathComponents($this->path); array_pop($sourcePath); // Now resolve the target path against the source $resultPath = self::resolveCanonicalPathComponents($target->path, $sourcePath); } $result->path = '/' . implode('/', $resultPath); // The query and fragment elements are not inheritable so we don't touch them } } return $result; } /** * Normalise a path, resolving empty, . and .. components, optionally against another path * * @param string $path * @param string[] $target * @return string[] */ private static function resolveCanonicalPathComponents($path, array $target = []) { // strip empty components and resolve . and .. foreach (preg_split('#[\\\\/]+#', $path, -1, PREG_SPLIT_NO_EMPTY) as $component) { switch ($component) { case '.': // current directory - do nothing break; case '..': // up a level array_pop($target); break; default: array_push($target, $component); break; } } // add a trailing empty element if path refers to a directory $lastChar = $path[strlen($path) - 1]; if ($lastChar === '/' || $lastChar === '\\') { array_push($target, ''); } return $target; } /** * Replacement for parse_str() because it sucks * * @param string $str * @return array */ private function parseQueryString($str) { $result = []; foreach (explode('&', $str) as $element) { $parts = explode('=', $element, 2); $key = urldecode(array_shift($parts)); $value = $parts ? urldecode(array_shift($parts)) : ''; if (false === $pos = strpos($key, '[')) { $result[$key] = $value; } else { $base = substr($key, 0, $pos++); if (!isset($result[$base]) || !is_array($result[$base])) { $result[$base] = []; } $target = &$result[$base]; $length = strlen($key); do { $end = strpos($key, ']', $pos); $index = substr($key, $pos, $end - $pos); $pos = $end + 1; if (!isset($key[$pos])) { $target[$index] = $value; break; } if ($key[$pos] !== '[') { $target[$index] = [substr($key, $pos) => $value]; break; } if (!isset($target[$index]) || !is_array($target[$index])) { $target[$index] = []; } $target = &$target[$index]; } while(++$pos < $length); } } return $result; } /** * Build a query string from a set of data * * Assume that any scalar data is a query string literal, cast vectors to array and build as form data * * @param mixed $data * @return string */ private function buildQueryString($data) { return is_scalar($data) ? (string) $data : http_build_query((array) $data); } /** * Normalize slashes in a path and URL-encode without encoding slashes * * @param string $path * @return string */ private function urlEncodePath($path) { return implode('/', array_map('urlencode', preg_split('#[\\\\/]+#', $path))); } /** * Set the port number * * @param int $port * @throws \LogicException */ private function setPort($port) { if ($port !== null) { $port = (string) $port; if (!ctype_digit($port)) { // IMPORTANT NOTE: the range of the port is *not* limited to the bounds of any specific integer type // RFC 3896 sec 3.2.3 simply specifies *DIGIT // DO NOT VALIDATE THIS VALUE ANY MORE THAN THIS! throw new \LogicException('Port can only contain digits'); } } $this->port = $port; } /** * Constructor takes URL-encoded components as individual arguments * * @param string $scheme * @param string $user * @param string $pass * @param string $host * @param int $port * @param string $path * @param string|array $query * @param string $fragment */ public function __construct($scheme = null, $user = null, $pass = null, $host = null, $port = null, $path = null, $query = null, $fragment = null) { foreach (self::$stringProperties as $property) { if (${$property} !== null) { $this->{$property} = urldecode((string) ${$property}); } } // This is a special case. We do not attempt to validate any other component as they can contain more // or less anything due to multilingual transformations, but this *must* be all digits at all times $this->setPort($port); if ($query !== null) { $this->query = is_scalar($query) ? $this->parseQueryString($query) : (array) $query; } } /** * Magic getter * * @param string $name * @return mixed */ public function __get($name) { if (isset($this->{$name})) { return $this->{$name}; } trigger_error('Attempt to access unknown property URL::$' . $name, E_USER_NOTICE); return null; } /** * Magic setter * * @param string $name * @param mixed $value */ public function __set($name, $value) { if (in_array($name, self::$stringProperties)) { $this->{$name} = $value !== null ? (string) $value : null; } else if ($name === 'port') { $this->setPort($value); } else { trigger_error('Attempt to access unknown property URL::' . $name, E_USER_NOTICE); } } /** * Forms all non-null components into a URL * * @return string */ public function __toString() { $result = ''; if (isset($this->host)) { if (isset($this->scheme)) { $result = $this->scheme . ':'; } $result .= '//'; if (isset($this->user)) { $result .= urlencode($this->user); if (isset($this->pass)) { $result .= ':' . urlencode($this->pass); } $result .= '@'; } $result .= $this->host; if (isset($this->port)) { $result .= ':' . $this->port; } if (isset($this->path) && !in_array($this->path[0], ['\\', '/'])) { $result .= '/'; } } if (isset($this->path)) { $result .= $this->urlEncodePath($this->path); } if (isset($this->query) && $this->query !== [] && $this->query !== '') { $result .= '?' . $this->buildQueryString($this->query); } if (isset($this->fragment)) { $result .= '#' . urlencode($this->fragment); } return $result; } } $url = URL::createFromString('http://www.google.com/foo+bar/baz?foo=bar#baz'); echo "Path is " . $url->path . "\n"; echo "Rebuilt is " . $url;
Output for 5.4.0 - 5.6.28, hhvm-3.10.0 - 3.12.0, 7.0.0 - 7.1.0
Path is /foo bar/baz Rebuilt is http://www.google.com/foo+bar/baz?foo=bar#baz
Output for 5.3.0 - 5.3.29
Parse error: syntax error, unexpected '[' in /in/m3f5A on line 52
Process exited with code 255.