WokerTask/vendor/phrity/net-uri/src/Uri.php

487 lines
16 KiB
PHP

<?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);
}
}