123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486 |
- <?php
- /**
- * File for Net\Uri class.
- * @package Phrity > Net > Uri
- * @see https://www.rfc-editor.org/rfc/rfc3986
- * @see https://www.php-fig.org/psr/psr-7/#35-psrhttpmessageuriinterface
- */
- namespace Phrity\Net;
- use InvalidArgumentException;
- use Psr\Http\Message\UriInterface;
- /**
- * Net\Uri class.
- */
- class Uri implements UriInterface
- {
- public const REQUIRE_PORT = 1; // Always include port, explicit or default
- public const ABSOLUTE_PATH = 2; // Enforce absolute path
- public const NORMALIZE_PATH = 4; // Normalize path
- public const IDNA = 8; // IDNA-convert host
- private const RE_MAIN = '!^(?P<schemec>(?P<scheme>[^:/?#]+):)?(?P<authorityc>//(?P<authority>[^/?#]*))?'
- . '(?P<path>[^?#]*)(?P<queryc>\?(?P<query>[^#]*))?(?P<fragmentc>#(?P<fragment>.*))?$!';
- private const RE_AUTH = '!^(?P<userinfoc>(?P<user>[^:/?#]+)(?P<passc>:(?P<pass>[^:/?#]+))?@)?'
- . '(?P<host>[^:/?#]*|\[[^/?#]*\])(?P<portc>:(?P<port>[0-9]*))?$!';
- private static $port_defaults = [
- 'acap' => 674,
- 'afp' => 548,
- 'dict' => 2628,
- 'dns' => 53,
- 'ftp' => 21,
- 'git' => 9418,
- 'gopher' => 70,
- 'http' => 80,
- 'https' => 443,
- 'imap' => 143,
- 'ipp' => 631,
- 'ipps' => 631,
- 'irc' => 194,
- 'ircs' => 6697,
- 'ldap' => 389,
- 'ldaps' => 636,
- 'mms' => 1755,
- 'msrp' => 2855,
- 'mtqp' => 1038,
- 'nfs' => 111,
- 'nntp' => 119,
- 'nntps' => 563,
- 'pop' => 110,
- 'prospero' => 1525,
- 'redis' => 6379,
- 'rsync' => 873,
- 'rtsp' => 554,
- 'rtsps' => 322,
- 'rtspu' => 5005,
- 'sftp' => 22,
- 'smb' => 445,
- 'snmp' => 161,
- 'ssh' => 22,
- 'svn' => 3690,
- 'telnet' => 23,
- 'ventrilo' => 3784,
- 'vnc' => 5900,
- 'wais' => 210,
- 'ws' => 80,
- 'wss' => 443,
- ];
- private $scheme;
- private $authority;
- private $host;
- private $port;
- private $user;
- private $pass;
- private $path;
- private $query;
- private $fragment;
- /**
- * Create new URI instance using a string
- * @param string $uri_string URI as string
- * @throws \InvalidArgumentException If the given URI cannot be parsed
- */
- public function __construct(string $uri_string = '', int $flags = 0)
- {
- $this->parse($uri_string);
- }
- // ---------- PSR-7 getters ---------------------------------------------------------------------------------------
- /**
- * Retrieve the scheme component of the URI.
- * @return string The URI scheme
- */
- public function getScheme(int $flags = 0): string
- {
- return $this->getComponent('scheme') ?? '';
- }
- /**
- * Retrieve the authority component of the URI.
- * @return string The URI authority, in "[user-info@]host[:port]" format
- */
- public function getAuthority(int $flags = 0): string
- {
- $host = $this->formatComponent($this->getHost($flags));
- if ($this->isEmpty($host)) {
- return '';
- }
- $userinfo = $this->formatComponent($this->getUserInfo(), '', '@');
- $port = $this->formatComponent($this->getPort($flags), ':');
- return "{$userinfo}{$host}{$port}";
- }
- /**
- * Retrieve the user information component of the URI.
- * @return string The URI user information, in "username[:password]" format
- */
- public function getUserInfo(int $flags = 0): string
- {
- $user = $this->formatComponent($this->getComponent('user'));
- $pass = $this->formatComponent($this->getComponent('pass'), ':');
- return $this->isEmpty($user) ? '' : "{$user}{$pass}";
- }
- /**
- * Retrieve the host component of the URI.
- * @return string The URI host
- */
- public function getHost(int $flags = 0): string
- {
- $host = $this->getComponent('host') ?? '';
- if ($flags & self::IDNA) {
- $host = $this->idna($host);
- }
- return $host;
- }
- /**
- * Retrieve the port component of the URI.
- * @return null|int The URI port
- */
- public function getPort(int $flags = 0): ?int
- {
- $port = $this->getComponent('port');
- $scheme = $this->getComponent('scheme');
- $default = isset(self::$port_defaults[$scheme]) ? self::$port_defaults[$scheme] : null;
- if ($flags & self::REQUIRE_PORT) {
- return !$this->isEmpty($port) ? $port : $default;
- }
- return $this->isEmpty($port) || $port === $default ? null : $port;
- }
- /**
- * Retrieve the path component of the URI.
- * @return string The URI path
- */
- public function getPath(int $flags = 0): string
- {
- $path = $this->getComponent('path') ?? '';
- if ($flags & self::NORMALIZE_PATH) {
- $path = $this->normalizePath($path);
- }
- if ($flags & self::ABSOLUTE_PATH && substr($path, 0, 1) !== '/') {
- $path = "/{$path}";
- }
- return $path;
- }
- /**
- * Retrieve the query string of the URI.
- * @return string The URI query string
- */
- public function getQuery(int $flags = 0): string
- {
- return $this->getComponent('query') ?? '';
- }
- /**
- * Retrieve the fragment component of the URI.
- * @return string The URI fragment
- */
- public function getFragment(int $flags = 0): string
- {
- return $this->getComponent('fragment') ?? '';
- }
- // ---------- PSR-7 setters ---------------------------------------------------------------------------------------
- /**
- * Return an instance with the specified scheme.
- * @param string $scheme The scheme to use with the new instance
- * @return static A new instance with the specified scheme
- * @throws \InvalidArgumentException for invalid schemes
- * @throws \InvalidArgumentException for unsupported schemes
- */
- public function withScheme($scheme, int $flags = 0): UriInterface
- {
- $clone = clone $this;
- if ($flags & self::REQUIRE_PORT) {
- $clone->setComponent('port', $this->getPort(self::REQUIRE_PORT));
- $default = isset(self::$port_defaults[$scheme]) ? self::$port_defaults[$scheme] : null;
- }
- $clone->setComponent('scheme', $scheme);
- return $clone;
- }
- /**
- * Return an instance with the specified user information.
- * @param string $user The user name to use for authority
- * @param null|string $password The password associated with $user
- * @return static A new instance with the specified user information
- */
- public function withUserInfo($user, $password = null, int $flags = 0): UriInterface
- {
- $clone = clone $this;
- $clone->setComponent('user', $user);
- $clone->setComponent('pass', $password);
- return $clone;
- }
- /**
- * Return an instance with the specified host.
- * @param string $host The hostname to use with the new instance
- * @return static A new instance with the specified host
- * @throws \InvalidArgumentException for invalid hostnames
- */
- public function withHost($host, int $flags = 0): UriInterface
- {
- $clone = clone $this;
- if ($flags & self::IDNA) {
- $host = $this->idna($host);
- }
- $clone->setComponent('host', $host);
- return $clone;
- }
- /**
- * Return an instance with the specified port.
- * @param null|int $port The port to use with the new instance
- * @return static A new instance with the specified port
- * @throws \InvalidArgumentException for invalid ports
- */
- public function withPort($port, int $flags = 0): UriInterface
- {
- $clone = clone $this;
- $clone->setComponent('port', $port);
- return $clone;
- }
- /**
- * Return an instance with the specified path.
- * @param string $path The path to use with the new instance
- * @return static A new instance with the specified path
- * @throws \InvalidArgumentException for invalid paths
- */
- public function withPath($path, int $flags = 0): UriInterface
- {
- $clone = clone $this;
- if ($flags & self::NORMALIZE_PATH) {
- $path = $this->normalizePath($path);
- }
- if ($flags & self::ABSOLUTE_PATH && substr($path, 0, 1) !== '/') {
- $path = "/{$path}";
- }
- $clone->setComponent('path', $path);
- return $clone;
- }
- /**
- * Return an instance with the specified query string.
- * @param string $query The query string to use with the new instance
- * @return static A new instance with the specified query string
- * @throws \InvalidArgumentException for invalid query strings
- */
- public function withQuery($query, int $flags = 0): UriInterface
- {
- $clone = clone $this;
- $clone->setComponent('query', $query);
- return $clone;
- }
- /**
- * Return an instance with the specified URI fragment.
- * @param string $fragment The fragment to use with the new instance
- * @return static A new instance with the specified fragment
- */
- public function withFragment($fragment, int $flags = 0): UriInterface
- {
- $clone = clone $this;
- $clone->setComponent('fragment', $fragment);
- return $clone;
- }
- // ---------- PSR-7 string ----------------------------------------------------------------------------------------
- /**
- * Return the string representation as a URI reference.
- * @return string
- */
- public function __toString(): string
- {
- return $this->toString();
- }
- // ---------- Extensions ------------------------------------------------------------------------------------------
- /**
- * Return the string representation as a URI reference.
- * @return string
- */
- public function toString(int $flags = 0): string
- {
- $scheme = $this->formatComponent($this->getComponent('scheme'), '', ':');
- $authority = $this->authority ? "//{$this->formatComponent($this->getAuthority($flags))}" : '';
- $path_flags = ($this->authority && $this->path ? self::ABSOLUTE_PATH : 0) | $flags;
- $path = $this->formatComponent($this->getPath($path_flags));
- $query = $this->formatComponent($this->getComponent('query'), '?');
- $fragment = $this->formatComponent($this->getComponent('fragment'), '#');
- return "{$scheme}{$authority}{$path}{$query}{$fragment}";
- }
- // ---------- Private helper methods ------------------------------------------------------------------------------
- private function parse(string $uri_string = ''): void
- {
- if ($uri_string === '') {
- return;
- }
- preg_match(self::RE_MAIN, $uri_string, $main);
- $this->authority = !empty($main['authorityc']);
- $this->setComponent('scheme', isset($main['schemec']) ? $main['scheme'] : '');
- $this->setComponent('path', isset($main['path']) ? $main['path'] : '');
- $this->setComponent('query', isset($main['queryc']) ? $main['query'] : '');
- $this->setComponent('fragment', isset($main['fragmentc']) ? $main['fragment'] : '');
- if ($this->authority) {
- preg_match(self::RE_AUTH, $main['authority'], $auth);
- if (empty($auth) && $main['authority'] !== '') {
- throw new InvalidArgumentException("Invalid 'authority'.");
- }
- if ($this->isEmpty($auth['host']) && !$this->isEmpty($auth['user'])) {
- throw new InvalidArgumentException("Invalid 'authority'.");
- }
- $this->setComponent('user', isset($auth['user']) ? $auth['user'] : '');
- $this->setComponent('pass', isset($auth['passc']) ? $auth['pass'] : '');
- $this->setComponent('host', isset($auth['host']) ? $auth['host'] : '');
- $this->setComponent('port', isset($auth['portc']) ? $auth['port'] : '');
- }
- }
- private function encode(string $source, string $keep = ''): string
- {
- $exclude = "[^%\/:=&!\$'()*+,;@{$keep}]+";
- $exp = "/(%{$exclude})|({$exclude})/";
- return preg_replace_callback($exp, function ($matches) {
- if ($e = preg_match('/^(%[0-9a-fA-F]{2})/', $matches[0], $m)) {
- return substr($matches[0], 0, 3) . rawurlencode(substr($matches[0], 3));
- } else {
- return rawurlencode($matches[0]);
- }
- }, $source);
- }
- private function setComponent(string $component, $value): void
- {
- $value = $this->parseCompontent($component, $value);
- $this->$component = $value;
- }
- private function parseCompontent(string $component, $value)
- {
- if ($this->isEmpty($value)) {
- return null;
- }
- switch ($component) {
- case 'scheme':
- $this->assertString($component, $value);
- $this->assertpattern($component, $value, '/^[a-z][a-z0-9-+.]*$/i');
- return mb_strtolower($value);
- case 'host': // IP-literal / IPv4address / reg-name
- $this->assertString($component, $value);
- $this->authority = $this->authority || !$this->isEmpty($value);
- return mb_strtolower($value);
- case 'port':
- $this->assertInteger($component, $value);
- if ($value < 0 || $value > 65535) {
- throw new InvalidArgumentException("Invalid port number");
- }
- return (int)$value;
- case 'path':
- $this->assertString($component, $value);
- $value = $this->encode($value);
- return $value;
- case 'user':
- case 'pass':
- case 'query':
- case 'fragment':
- $this->assertString($component, $value);
- $value = $this->encode($value, '?');
- return $value;
- }
- }
- private function getComponent(string $component)
- {
- return isset($this->$component) ? $this->$component : null;
- }
- private function formatComponent($value, string $before = '', string $after = ''): string
- {
- return $this->isEmpty($value) ? '' : "{$before}{$value}{$after}";
- }
- private function isEmpty($value): bool
- {
- return is_null($value) || $value === '';
- }
- private function assertString(string $component, $value): void
- {
- if (!is_string($value)) {
- throw new InvalidArgumentException("Invalid '{$component}': Should be a string");
- }
- }
- private function assertInteger(string $component, $value): void
- {
- if (!is_numeric($value) || intval($value) != $value) {
- throw new InvalidArgumentException("Invalid '{$component}': Should be an integer");
- }
- }
- private function assertPattern(string $component, string $value, string $pattern): void
- {
- if (preg_match($pattern, $value) == 0) {
- throw new InvalidArgumentException("Invalid '{$component}': Should match {$pattern}");
- }
- }
- private function normalizePath(string $path): string
- {
- $result = [];
- preg_match_all('!([^/]*/|[^/]*$)!', $path, $items);
- foreach ($items[0] as $item) {
- switch ($item) {
- case '':
- case './':
- case '.':
- break; // just skip
- case '/':
- if (empty($result)) {
- array_push($result, $item); // add
- }
- break;
- case '..':
- case '../':
- if (empty($result) || end($result) == '../') {
- array_push($result, $item); // add
- } else {
- array_pop($result); // remove previous
- }
- break;
- default:
- array_push($result, $item); // add
- }
- }
- return implode('', $result);
- }
- private function idna(string $value): string
- {
- if ($value === '' || !is_callable('idn_to_ascii')) {
- return $value; // Can't convert, but don't cause exception
- }
- return idn_to_ascii($value, IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46);
- }
- }
|