494 lines
12 KiB
PHP
494 lines
12 KiB
PHP
<?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)
|
|
*/
|
|
protected string $scheme = '';
|
|
|
|
protected string $user = '';
|
|
|
|
protected string $password = '';
|
|
|
|
protected string $host = '';
|
|
|
|
protected ?int $port;
|
|
|
|
protected string $path = '';
|
|
|
|
/**
|
|
* Uri query string (without "?" prefix)
|
|
*/
|
|
protected string $query = '';
|
|
|
|
/**
|
|
* Uri fragment string (without "#" prefix)
|
|
*/
|
|
protected string $fragment = '';
|
|
|
|
/**
|
|
* @param string $scheme Uri scheme.
|
|
* @param string $host Uri host.
|
|
* @param int|null $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 = $this->filterUserInfo($user);
|
|
$this->password = $this->filterUserInfo($password);
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function getScheme(): string
|
|
{
|
|
return $this->scheme;
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
* @return static
|
|
*/
|
|
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, static::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 ($this->password !== '') {
|
|
$info .= ':' . $this->password;
|
|
}
|
|
|
|
return $info;
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
* @return static
|
|
*/
|
|
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}
|
|
* @return static
|
|
*/
|
|
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}
|
|
* @return static
|
|
*/
|
|
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 int|null $port The Uri port number.
|
|
*
|
|
* @return int|null
|
|
*
|
|
* @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}
|
|
* @return static
|
|
*/
|
|
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}
|
|
* @return static
|
|
*/
|
|
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}
|
|
* @return static
|
|
*/
|
|
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();
|
|
|
|
if ($path !== '') {
|
|
if ($path[0] !== '/') {
|
|
if ($authority !== '') {
|
|
// If the path is rootless and an authority is present, the path MUST be prefixed by "/".
|
|
$path = '/' . $path;
|
|
}
|
|
} elseif (isset($path[1]) && $path[1] === '/') {
|
|
if ($authority === '') {
|
|
// If the path is starting with more than one "/" and no authority is present,
|
|
// the starting slashes MUST be reduced to one.
|
|
$path = '/' . ltrim($path, '/');
|
|
}
|
|
}
|
|
}
|
|
|
|
return ($scheme !== '' ? $scheme . ':' : '')
|
|
. ($authority !== '' ? '//' . $authority : '')
|
|
. $path
|
|
. ($query !== '' ? '?' . $query : '')
|
|
. ($fragment !== '' ? '#' . $fragment : '');
|
|
}
|
|
}
|