<?php /** * Slim Framework (https://slimframework.com) * * @license https://github.com/slimphp/Slim-Psr7/blob/master/LICENSE.md (MIT License) */ declare(strict_types=1); namespace Slim\Psr7; use InvalidArgumentException; use Psr\Http\Message\UriInterface; use function filter_var; use function is_integer; use function is_null; use function is_object; use function is_string; use function ltrim; use function method_exists; use function preg_replace_callback; use function rawurlencode; use function str_replace; use function strtolower; use const FILTER_FLAG_IPV6; use const FILTER_VALIDATE_IP; class Uri implements UriInterface { public const SUPPORTED_SCHEMES = [ '' => null, 'http' => 80, 'https' => 443 ]; /** * Uri scheme (without "://" suffix) * * @var string */ protected $scheme = ''; /** * @var string */ protected $user = ''; /** * @var string */ protected $password = ''; /** * @var string */ protected $host = ''; /** * @var null|int */ protected $port; /** * @var string */ protected $path = ''; /** * Uri query string (without "?" prefix) * * @var string */ protected $query = ''; /** * Uri fragment string (without "#" prefix) * * @var string */ protected $fragment = ''; /** * @param string $scheme Uri scheme. * @param string $host Uri host. * @param int $port Uri port number. * @param string $path Uri path. * @param string $query Uri query string. * @param string $fragment Uri fragment. * @param string $user Uri user. * @param string $password Uri password. */ public function __construct( string $scheme, string $host, ?int $port = null, string $path = '/', string $query = '', string $fragment = '', string $user = '', string $password = '' ) { $this->scheme = $this->filterScheme($scheme); $this->host = $this->filterHost($host); $this->port = $this->filterPort($port); $this->path = $this->filterPath($path); $this->query = $this->filterQuery($query); $this->fragment = $this->filterFragment($fragment); $this->user = $user; $this->password = $password; } /** * {@inheritdoc} */ public function getScheme(): string { return $this->scheme; } /** * {@inheritdoc} */ public function withScheme($scheme) { $scheme = $this->filterScheme($scheme); $clone = clone $this; $clone->scheme = $scheme; return $clone; } /** * Filter Uri scheme. * * @param mixed $scheme Raw Uri scheme. * * @return string * * @throws InvalidArgumentException If the Uri scheme is not a string. * @throws InvalidArgumentException If Uri scheme is not exists in SUPPORTED_SCHEMES */ protected function filterScheme($scheme): string { if (!is_string($scheme)) { throw new InvalidArgumentException('Uri scheme must be a string.'); } $scheme = str_replace('://', '', strtolower($scheme)); if (!key_exists($scheme, self::SUPPORTED_SCHEMES)) { throw new InvalidArgumentException( 'Uri scheme must be one of: "' . implode('", "', array_keys(static::SUPPORTED_SCHEMES)) . '"' ); } return $scheme; } /** * {@inheritdoc} */ public function getAuthority(): string { $userInfo = $this->getUserInfo(); $host = $this->getHost(); $port = $this->getPort(); return ($userInfo !== '' ? $userInfo . '@' : '') . $host . ($port !== null ? ':' . $port : ''); } /** * {@inheritdoc} */ public function getUserInfo(): string { $info = $this->user; if (isset($this->password) && $this->password !== '') { $info .= ':' . $this->password; } return $info; } /** * {@inheritdoc} */ public function withUserInfo($user, $password = null) { $clone = clone $this; $clone->user = $this->filterUserInfo($user); if ($clone->user !== '') { $clone->password = $this->filterUserInfo($password); } else { $clone->password = ''; } return $clone; } /** * Filters the user info string. * * Returns the percent-encoded query string. * * @param string|null $info The raw uri query string. * * @return string */ protected function filterUserInfo(?string $info = null): string { if (!is_string($info)) { return ''; } $match = preg_replace_callback( '/(?:[^a-zA-Z0-9_\-\.~!\$&\'\(\)\*\+,;=]+|%(?![A-Fa-f0-9]{2}))/u', function ($match) { return rawurlencode($match[0]); }, $info ); return is_string($match) ? $match : ''; } /** * {@inheritdoc} */ public function getHost(): string { return $this->host; } /** * {@inheritdoc} */ public function withHost($host) { $clone = clone $this; $clone->host = $this->filterHost($host); return $clone; } /** * Filter Uri host. * * If the supplied host is an IPv6 address, then it is converted to a reference * as per RFC 2373. * * @param mixed $host The host to filter. * * @return string * * @throws InvalidArgumentException for invalid host names. */ protected function filterHost($host): string { if (is_object($host) && method_exists($host, '__toString')) { $host = (string) $host; } if (!is_string($host)) { throw new InvalidArgumentException('Uri host must be a string'); } if (filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $host = '[' . $host . ']'; } return strtolower($host); } /** * {@inheritdoc} */ public function getPort(): ?int { return $this->port && !$this->hasStandardPort() ? $this->port : null; } /** * {@inheritdoc} */ public function withPort($port) { $port = $this->filterPort($port); $clone = clone $this; $clone->port = $port; return $clone; } /** * Does this Uri use a standard port? * * @return bool */ protected function hasStandardPort(): bool { return static::SUPPORTED_SCHEMES[$this->scheme] === $this->port; } /** * Filter Uri port. * * @param null|int $port The Uri port number. * * @return null|int * * @throws InvalidArgumentException If the port is invalid. */ protected function filterPort($port): ?int { if (is_null($port) || (is_integer($port) && ($port >= 1 && $port <= 65535))) { return $port; } throw new InvalidArgumentException('Uri port must be null or an integer between 1 and 65535 (inclusive)'); } /** * {@inheritdoc} */ public function getPath(): string { return $this->path; } /** * {@inheritdoc} */ public function withPath($path) { if (!is_string($path)) { throw new InvalidArgumentException('Uri path must be a string'); } $clone = clone $this; $clone->path = $this->filterPath($path); return $clone; } /** * Filter Uri path. * * This method percent-encodes all reserved characters in the provided path string. * This method will NOT double-encode characters that are already percent-encoded. * * @param string $path The raw uri path. * * @return string The RFC 3986 percent-encoded uri path. * * @link http://www.faqs.org/rfcs/rfc3986.html */ protected function filterPath($path): string { $match = preg_replace_callback( '/(?:[^a-zA-Z0-9_\-\.~:@&=\+\$,\/;%]+|%(?![A-Fa-f0-9]{2}))/', function ($match) { return rawurlencode($match[0]); }, $path ); return is_string($match) ? $match : ''; } /** * {@inheritdoc} */ public function getQuery(): string { return $this->query; } /** * {@inheritdoc} */ public function withQuery($query) { $query = ltrim($this->filterQuery($query), '?'); $clone = clone $this; $clone->query = $query; return $clone; } /** * Filters the query string of a URI. * * Returns the percent-encoded query string. * * @param mixed $query The raw uri query string. * * @return string */ protected function filterQuery($query): string { if (is_object($query) && method_exists($query, '__toString')) { $query = (string) $query; } if (!is_string($query)) { throw new InvalidArgumentException('Uri query must be a string.'); } $match = preg_replace_callback( '/(?:[^a-zA-Z0-9_\-\.~!\$&\'\(\)\*\+,;=%:@\/\?]+|%(?![A-Fa-f0-9]{2}))/', function ($match) { return rawurlencode($match[0]); }, $query ); return is_string($match) ? $match : ''; } /** * {@inheritdoc} */ public function getFragment(): string { return $this->fragment; } /** * {@inheritdoc} */ public function withFragment($fragment) { $fragment = $this->filterFragment($fragment); $clone = clone $this; $clone->fragment = $fragment; return $clone; } /** * Filters fragment of a URI. * * Returns the percent-encoded fragment. * * @param mixed $fragment The raw uri query string. * * @return string */ protected function filterFragment($fragment): string { if (is_object($fragment) && method_exists($fragment, '__toString')) { $fragment = (string) $fragment; } if (!is_string($fragment)) { throw new InvalidArgumentException('Uri fragment must be a string.'); } $fragment = ltrim($fragment, '#'); $match = preg_replace_callback( '/(?:[^a-zA-Z0-9_\-\.~!\$&\'\(\)\*\+,;=%:@\/\?]+|%(?![A-Fa-f0-9]{2}))/', function ($match) { return rawurlencode($match[0]); }, $fragment ); return is_string($match) ? $match : ''; } /** * {@inheritdoc} */ public function __toString(): string { $scheme = $this->getScheme(); $authority = $this->getAuthority(); $path = $this->getPath(); $query = $this->getQuery(); $fragment = $this->getFragment(); $path = '/' . ltrim($path, '/'); return ($scheme !== '' ? $scheme . ':' : '') . ($authority !== '' ? '//' . $authority : '') . $path . ($query !== '' ? '?' . $query : '') . ($fragment !== '' ? '#' . $fragment : ''); } }