🆙 Add cms i using 🆙

This commit is contained in:
Remco
2025-11-25 22:42:56 +01:00
parent 94704e0925
commit d44196149e
35591 changed files with 3601123 additions and 0 deletions
@@ -0,0 +1,319 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Components;
use Deprecated;
use League\Uri\Contracts\AuthorityInterface;
use League\Uri\Contracts\HostInterface;
use League\Uri\Contracts\PortInterface;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Contracts\UriException;
use League\Uri\Contracts\UriInterface;
use League\Uri\Contracts\UserInfoInterface;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\UriString;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use SensitiveParameter;
use Stringable;
use Uri\Rfc3986\Uri as Rfc3986Uri;
use Uri\WhatWg\Url as WhatWgUrl;
use function is_string;
final class Authority extends Component implements AuthorityInterface
{
private readonly HostInterface $host;
private readonly PortInterface $port;
private readonly UserInfoInterface $userInfo;
public function __construct(
Stringable|string|null $host,
Stringable|string|int|null $port = null,
#[SensitiveParameter] Stringable|string|null $userInfo = null
) {
$this->host = !$host instanceof HostInterface ? Host::new($host) : $host;
$this->port = !$port instanceof PortInterface ? Port::new($port) : $port;
$this->userInfo = !$userInfo instanceof UserInfoInterface ? UserInfo::new($userInfo) : $userInfo;
if (null === $this->host->value() && null !== $this->value()) {
throw new SyntaxError('A non-empty authority must contains a non null host.');
}
}
/**
* @throws SyntaxError If the component contains invalid HostInterface part.
*/
public static function new(Stringable|string|null $value = null): self
{
$components = UriString::parseAuthority(self::filterComponent($value));
return new self(
Host::new($components['host']),
Port::new($components['port']),
new UserInfo(
$components['user'],
$components['pass']
)
);
}
/**
* Create a new instance from a string.or a stringable structure or returns null on failure.
*/
public static function tryNew(Stringable|string|null $uri = null): ?self
{
try {
return self::new($uri);
} catch (UriException) {
return null;
}
}
/**
* Create a new instance from a URI object.
*/
public static function fromUri(WhatwgUrl|Rfc3986Uri|Stringable|string $uri): self
{
$uri = self::filterUri($uri);
if ($uri instanceof Rfc3986Uri) {
return new self($uri->getHost(), $uri->getPort(), $uri->getUserInfo());
}
if ($uri instanceof WhatWgUrl) {
$userInfo = $uri->getUsername();
if (null !== ($password = $uri->getPassword())) {
$userInfo .= ':'.$password;
}
return new self($uri->getUnicodeHost(), $uri->getPort(), $userInfo);
}
if ($uri instanceof Psr7UriInterface) {
$components = UriString::parse($uri);
$userInfo = $components['user'];
if (null !== ($password = $components['pass'])) {
$userInfo .= ':'.$password;
}
return new self($components['host'], $components['port'], $userInfo);
}
return self::new($uri->getAuthority());
}
/**
* Create a new instance from a hash of parse_url parts.
*
* Create a new instance from a hash representation of the URI similar
* to PHP parse_url function result
*
* @param array{
* user? : ?string,
* pass? : ?string,
* host? : ?string,
* port? : ?int
* } $components
*/
public static function fromComponents(array $components): self
{
$components += ['user' => null, 'pass' => null, 'host' => null, 'port' => null];
return match (true) {
null === $components['user'] => new self($components['host'], $components['port']),
null === $components['pass'] => new self($components['host'], $components['port'], $components['user']),
default => new self($components['host'], $components['port'], $components['user'].':'.$components['pass']),
};
}
public function value(): ?string
{
return self::getAuthorityValue($this->userInfo, $this->host, $this->port);
}
private static function getAuthorityValue(
UserInfoInterface $userInfo,
HostInterface $host,
PortInterface $port
): ?string {
$auth = $host->value();
$port = $port->value();
if (null !== $port) {
$auth .= ':'.$port;
}
$userInfo = $userInfo->value();
return match (null) {
$userInfo => $auth,
default => $userInfo.'@'.$auth,
};
}
public function getUriComponent(): string
{
return match (null) {
$this->host->value() => $this->toString(),
default => '//'.$this->toString(),
};
}
public function getHost(): ?string
{
return $this->host->value();
}
public function getPort(): ?int
{
return $this->port->toInt();
}
public function getUserInfo(): ?string
{
return $this->userInfo->value();
}
public function equals(mixed $value): bool
{
if (!$value instanceof Stringable && !is_string($value) && null !== $value) {
return false;
}
if (!$value instanceof UriComponentInterface) {
$value = self::tryNew($value);
if (null === $value) {
return false;
}
}
return $value->getUriComponent() === $this->getUriComponent();
}
/**
* @return array{user: ?string, pass: ?string, host: ?string, port: ?int}
*/
public function components(): array
{
return $this->userInfo->components() + [
'host' => $this->host->value(),
'port' => $this->port->toInt(),
];
}
public function withHost(Stringable|string|null $host): AuthorityInterface
{
if (!$host instanceof HostInterface) {
$host = Host::new($host);
}
return match ($this->host->value()) {
$host->value() => $this,
default => new self($host, $this->port, $this->userInfo),
};
}
public function withPort(Stringable|string|int|null $port): AuthorityInterface
{
if (!$port instanceof PortInterface) {
$port = Port::new($port);
}
return match ($this->port->value()) {
$port->value() => $this,
default => new self($this->host, $port, $this->userInfo),
};
}
public function withUserInfo(Stringable|string|null $user, #[SensitiveParameter] Stringable|string|null $password = null): AuthorityInterface
{
$userInfo = new UserInfo($user, $password);
return match ($this->userInfo->value()) {
$userInfo->value() => $this,
default => new self($this->host, $this->port, $userInfo),
};
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Authority::fromUri()
*
* @codeCoverageIgnore
*
* Create a new instance from a URI object.
*/
#[Deprecated(message:'use League\Uri\Components\Authority::fromUri() instead', since:'league/uri-components:7.0.0')]
public static function createFromUri(UriInterface|Psr7UriInterface $uri): self
{
return self::fromUri($uri);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Authority::new()
*
* @codeCoverageIgnore
*
* Returns a new instance from a string or a stringable object.
*/
#[Deprecated(message:'use League\Uri\Components\Authority::new() instead', since:'league/uri-components:7.0.0')]
public static function createFromString(Stringable|string $authority): self
{
return self::new($authority);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Authority::new()
*
* @codeCoverageIgnore
*
* Returns a new instance from null.
*/
#[Deprecated(message:'use League\Uri\Components\Authority::new() instead', since:'league/uri-components:7.0.0')]
public static function createFromNull(): self
{
return self::new();
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Authority::fromComponents()
*
* @codeCoverageIgnore
*
* Create a new instance from a hash of parse_url parts.
*
* Create a new instance from a hash representation of the URI similar
* to PHP parse_url function result
*
* @param array{
* user? : ?string,
* pass? : ?string,
* host? : ?string,
* port? : ?int
* } $components
*/
#[Deprecated(message:'use League\Uri\Components\Authority::fromComponents() instead', since:'league/uri-components:7.0.0')]
public static function createFromComponents(array $components): self
{
return self::fromComponents($components);
}
}
@@ -0,0 +1,110 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Components;
use League\Uri\Contracts\Conditionable;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Contracts\UriInterface;
use League\Uri\Encoder;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\Modifier;
use League\Uri\Uri;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use Stringable;
use Uri\Rfc3986\Uri as Rfc3986Uri;
use Uri\WhatWg\Url as WhatWgUrl;
use function is_bool;
use function preg_match;
use function sprintf;
abstract class Component implements UriComponentInterface, Conditionable
{
protected const REGEXP_INVALID_URI_CHARS = '/[\x00-\x1f\x7f]/';
abstract public function value(): ?string;
public function jsonSerialize(): ?string
{
return $this->value();
}
public function toString(): string
{
return $this->value() ?? '';
}
public function __toString(): string
{
return $this->toString();
}
public function getUriComponent(): string
{
return $this->toString();
}
final protected static function filterUri(WhatWgUrl|Rfc3986Uri|Stringable|string $uri): WhatWgUrl|Rfc3986Uri|UriInterface|Psr7UriInterface
{
if ($uri instanceof Modifier) {
return $uri->unwrap();
}
if ($uri instanceof Rfc3986Uri
|| $uri instanceof WhatWgUrl
|| $uri instanceof PSR7UriInterface
|| $uri instanceof UriInterface
) {
return $uri;
}
return Uri::new($uri);
}
/**
* Validate the component content.
*/
protected function validateComponent(Stringable|int|string|null $component): ?string
{
return Encoder::decodeNecessary($component);
}
/**
* Filter the input component.
*
* @throws SyntaxError If the component cannot be converted to a string or null
*/
final protected static function filterComponent(Stringable|int|string|null $component): ?string
{
return match (true) {
$component instanceof UriComponentInterface => $component->value(),
null === $component => null,
1 === preg_match(self::REGEXP_INVALID_URI_CHARS, (string) $component) => throw new SyntaxError(sprintf('Invalid component string: %s.', $component)),
default => (string) $component,
};
}
final public function when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null): static
{
if (!is_bool($condition)) {
$condition = $condition($this);
}
return match (true) {
$condition => $onSuccess($this),
null !== $onFail => $onFail($this),
default => $this,
} ?? $this;
}
}
@@ -0,0 +1,453 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Components;
use Deprecated;
use finfo;
use League\Uri\Contracts\DataPathInterface;
use League\Uri\Contracts\PathInterface;
use League\Uri\Contracts\UriInterface;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\FeatureDetection;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use SplFileObject;
use Stringable;
use Throwable;
use Uri\Rfc3986\Uri as Rfc3986Uri;
use Uri\WhatWg\Url as WhatWgUrl;
use function base64_decode;
use function base64_encode;
use function count;
use function explode;
use function file_get_contents;
use function implode;
use function preg_match;
use function preg_replace_callback;
use function rawurldecode;
use function rawurlencode;
use function sprintf;
use function str_replace;
use function strlen;
use function strtolower;
use const FILEINFO_MIME;
final class DataPath extends Component implements DataPathInterface
{
/**
* All ASCII letters sorted by typical frequency of occurrence.
*/
private const ASCII = "\x20\x65\x69\x61\x73\x6E\x74\x72\x6F\x6C\x75\x64\x5D\x5B\x63\x6D\x70\x27\x0A\x67\x7C\x68\x76\x2E\x66\x62\x2C\x3A\x3D\x2D\x71\x31\x30\x43\x32\x2A\x79\x78\x29\x28\x4C\x39\x41\x53\x2F\x50\x22\x45\x6A\x4D\x49\x6B\x33\x3E\x35\x54\x3C\x44\x34\x7D\x42\x7B\x38\x46\x77\x52\x36\x37\x55\x47\x4E\x3B\x4A\x7A\x56\x23\x48\x4F\x57\x5F\x26\x21\x4B\x3F\x58\x51\x25\x59\x5C\x09\x5A\x2B\x7E\x5E\x24\x40\x60\x7F\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F";
private const BINARY_PARAMETER = 'base64';
private const DEFAULT_MIMETYPE = 'text/plain';
private const DEFAULT_PARAMETER = 'charset=us-ascii';
private const REGEXP_MIMETYPE = ',^\w+/[-.\w]+(?:\+[-.\w]+)?$,';
private const REGEXP_DATAPATH = '/^\w+\/[-.\w]+(?:\+[-.\w]+)?;,$/';
private const REGEXP_DATAPATH_ENCODING = '/[^A-Za-z0-9_\-.~!$&\'()*+,;=%:\/@]+|%(?![A-Fa-f0-9]{2})/x';
private readonly PathInterface $path;
private readonly string $mimetype;
/** @var string[] */
private readonly array $parameters;
private readonly bool $isBinaryData;
private readonly string $document;
/**
* New instance.
*/
private function __construct(Stringable|string $path)
{
/** @var string $path */
$path = self::filterComponent($path);
$this->path = Path::new($this->filterPath($path));
[$mediaType, $this->document] = explode(',', $this->path->toString(), 2) + [1 => ''];
[$mimetype, $parameters] = explode(';', $mediaType, 2) + [1 => ''];
$this->mimetype = $this->filterMimeType($mimetype);
[$this->parameters, $this->isBinaryData] = $this->filterParameters($parameters);
$this->validateDocument();
}
/**
* Filter the data path.
*
* @throws SyntaxError If the path is null
* @throws SyntaxError If the path is not valid according to RFC2937
*/
private function filterPath(string $path): string
{
if ('' === $path || ',' === $path) {
return 'text/plain;charset=us-ascii,';
}
if (1 === preg_match(self::REGEXP_DATAPATH, $path)) {
$path = substr($path, 0, -1).'charset=us-ascii,';
}
if (strlen($path) !== strspn($path, self::ASCII) || !str_contains($path, ',')) {
throw new SyntaxError(sprintf('The path `%s` is invalid according to RFC2937.', $path));
}
return $path;
}
/**
* Filter the mimeType property.
*
* @throws SyntaxError If the mimetype is invalid
*/
private function filterMimeType(string $mimetype): string
{
return match (true) {
'' === $mimetype => self::DEFAULT_MIMETYPE,
1 === preg_match(self::REGEXP_MIMETYPE, $mimetype) => $mimetype,
default => throw new SyntaxError(sprintf('Invalid mimeType, `%s`.', $mimetype)),
};
}
/**
* Extract and set the binary flag from the parameters if it exists.
*
* @throws SyntaxError If the mediatype parameters contain invalid data
*
* @return array{0:array<string>, 1:bool}
*/
private function filterParameters(string $parameters): array
{
if ('' === $parameters) {
return [[self::DEFAULT_PARAMETER], false];
}
$isBinaryData = false;
if (1 === preg_match(',(;|^)'.self::BINARY_PARAMETER.'$,', $parameters, $matches)) {
$parameters = substr($parameters, 0, - strlen($matches[0]));
$isBinaryData = true;
}
$params = array_filter(explode(';', $parameters), fn (string $param) => '' !== $param);
if ([] !== array_filter($params, $this->validateParameter(...))) {
throw new SyntaxError(sprintf('Invalid mediatype parameters, `%s`.', $parameters));
}
return [$params, $isBinaryData];
}
/**
* Validate mediatype parameter.
*/
private function validateParameter(string $parameter): bool
{
$properties = explode('=', $parameter);
return 2 !== count($properties) || self::BINARY_PARAMETER === strtolower($properties[0]);
}
/**
* Validate the path document string representation.
*
* @throws SyntaxError If the data is invalid
*/
private function validateDocument(): void
{
if (!$this->isBinaryData) {
return;
}
$res = base64_decode($this->document, true);
if (false === $res || $this->document !== base64_encode($res)) {
throw new SyntaxError(sprintf('Invalid document, `%s`.', $this->document));
}
}
/**
* Returns a new instance from a string or a stringable object.
*/
public static function new(Stringable|string $value = ''): self
{
return new self($value);
}
/**
* Create a new instance from a string.or a stringable structure or returns null on failure.
*/
public static function tryNew(Stringable|string $uri = ''): ?self
{
try {
return self::new($uri);
} catch (Throwable) {
return null;
}
}
/**
* Creates a new instance from a file path.
*
* @param null|resource $context
*
* @throws SyntaxError If the File is not readable
*/
public static function fromFileContents(string $path, $context = null): self
{
FeatureDetection::supportsFileDetection();
$fileArgs = [$path, false];
$mimeArgs = [$path, FILEINFO_MIME];
if (null !== $context) {
$fileArgs[] = $context;
$mimeArgs[] = $context;
}
$content = @file_get_contents(...$fileArgs);
if (false === $content) {
throw new SyntaxError(sprintf('`%s` failed to open stream: No such file or directory.', $path));
}
$mimetype = (string) (new finfo(FILEINFO_MIME))->file(...$mimeArgs);
return new self(
str_replace(' ', '', $mimetype)
.';base64,'.base64_encode($content)
);
}
/**
* Create a new instance from a URI object.
*/
public static function fromUri(WhatWgUrl|Rfc3986Uri|Stringable|string $uri): self
{
return self::new(Path::fromUri($uri)->toString());
}
public function value(): ?string
{
return $this->path->value();
}
public function equals(mixed $value): bool
{
return $this->path->equals($value);
}
public function getData(): string
{
return $this->document;
}
public function isBinaryData(): bool
{
return $this->isBinaryData;
}
public function getMimeType(): string
{
return $this->mimetype;
}
public function getParameters(): string
{
return implode(';', $this->parameters);
}
public function getMediaType(): string
{
return $this->getMimeType().';'.$this->getParameters();
}
public function isAbsolute(): bool
{
return $this->path->isAbsolute();
}
public function hasTrailingSlash(): bool
{
return $this->path->hasTrailingSlash();
}
public function decoded(): string
{
return $this->path->decoded();
}
public function normalize(): self
{
return new self((string) $this->path->normalize()->value());
}
/**
* @param ?resource $context
*/
public function save(string $path, string $mode = 'w', $context = null): SplFileObject
{
$data = $this->isBinaryData ? base64_decode($this->document, true) : rawurldecode($this->document);
$file = new SplFileObject($path, $mode, context: $context);
$file->fwrite((string) $data);
return $file;
}
public function toBinary(): DataPathInterface
{
if ($this->isBinaryData) {
return $this;
}
return new self($this->formatComponent(
$this->mimetype,
$this->getParameters(),
true,
base64_encode(rawurldecode($this->document))
));
}
/**
* Format the DataURI string.
*/
private function formatComponent(
string $mimetype,
string $parameters,
bool $isBinaryData,
string $data
): string {
if ('' !== $parameters) {
$parameters = ';'.$parameters;
}
if ($isBinaryData) {
$parameters .= ';base64';
}
$path = $mimetype.$parameters.','.$data;
return preg_replace_callback(
self::REGEXP_DATAPATH_ENCODING,
static fn (array $matches): string => rawurlencode($matches[0]),
$path
) ?? $path;
}
public function toAscii(): DataPathInterface
{
return match (false) {
$this->isBinaryData => $this,
default => new self($this->formatComponent(
$this->mimetype,
$this->getParameters(),
false,
rawurlencode((string)base64_decode($this->document, true))
)),
};
}
public function withoutDotSegments(): PathInterface
{
return $this;
}
public function withLeadingSlash(): PathInterface
{
return new self($this->path->withLeadingSlash());
}
public function withoutLeadingSlash(): PathInterface
{
return $this;
}
public function withoutTrailingSlash(): PathInterface
{
$path = $this->path->withoutTrailingSlash();
return match ($this->path) {
$path => $this,
default => new self($path),
};
}
public function withTrailingSlash(): PathInterface
{
$path = $this->path->withTrailingSlash();
return match ($this->path) {
$path => $this,
default => new self($path),
};
}
public function withParameters(Stringable|string $parameters): DataPathInterface
{
$parameters = (string) $parameters;
return match ($this->getParameters()) {
$parameters => $this,
default => new self($this->formatComponent(
$this->mimetype,
$parameters,
$this->isBinaryData,
$this->document
)),
};
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see DataPath::new()
*
* @codeCoverageIgnore
*
* Returns a new instance from a string or a stringable object.
*/
#[Deprecated(message:'use League\Uri\Components\DataPath::new() instead', since:'league/uri-components:7.0.0')]
public static function createFromString(Stringable|string $path): self
{
return self::new($path);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see DataPath::fromFilePath()
*
* @codeCoverageIgnore
*
* Creates a new instance from a file path.
*
* @param null|resource $context
*
* @throws SyntaxError If the File is not readable
*/
#[Deprecated(message:'use League\Uri\Components\DataPath::fromFilePath() instead', since:'league/uri-components:7.0.0')]
public static function createFromFilePath(string $path, $context = null): self
{
return self::fromFileContents($path, $context);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see DataPath::fromUri()
*
* @codeCoverageIgnore
*
* Create a new instance from a URI object.
*/
#[Deprecated(message:'use League\Uri\Components\DataPath::fromUri() instead', since:'league/uri-components:7.0.0')]
public static function createFromUri(Psr7UriInterface|UriInterface $uri): self
{
return self::fromUri($uri);
}
}
@@ -0,0 +1,420 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Components;
use Deprecated;
use Iterator;
use League\Uri\Contracts\AuthorityInterface;
use League\Uri\Contracts\DomainHostInterface;
use League\Uri\Contracts\HostInterface;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Contracts\UriException;
use League\Uri\Contracts\UriInterface;
use League\Uri\Exceptions\OffsetOutOfBounds;
use League\Uri\Exceptions\SyntaxError;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use Stringable;
use TypeError;
use Uri\Rfc3986\Uri as Rfc3986Uri;
use Uri\WhatWg\Url as WhatWgUrl;
use function array_count_values;
use function array_filter;
use function array_keys;
use function array_reverse;
use function array_shift;
use function count;
use function explode;
use function implode;
use function sprintf;
final class Domain extends Component implements DomainHostInterface
{
private const SEPARATOR = '.';
private readonly HostInterface $host;
/** @var string[] */
private readonly array $labels;
private function __construct(Stringable|string|null $host)
{
$host = match (true) {
$host instanceof HostInterface => $host,
$host instanceof UriComponentInterface => Host::new($host->value()),
default => Host::new($host),
};
if (!$host->isDomain()) {
throw new SyntaxError(sprintf('`%s` is an invalid domain name.', $host->value() ?? 'null'));
}
$this->host = $host;
$this->labels = array_reverse(explode(self::SEPARATOR, $this->host->value() ?? ''));
}
/**
* Returns a new instance from a string or a stringable object.
*/
public static function new(Stringable|string|null $value = null): self
{
return new self($value);
}
/**
* Create a new instance from a string.or a stringable structure or returns null on failure.
*/
public static function tryNew(Stringable|string|null $uri = null): ?self
{
try {
return self::new($uri);
} catch (UriException) {
return null;
}
}
/**
* Returns a new instance from an iterable structure.
*/
public static function fromLabels(Stringable|string ...$labels): self
{
return new self(match ([]) {
$labels => null,
default => implode(self::SEPARATOR, array_reverse(array_map(
fn ($label) => self::filterComponent($label),
$labels
))),
});
}
/**
* Create a new instance from a URI object.
*/
public static function fromUri(WhatWgUrl|Rfc3986Uri|Stringable|string $uri): self
{
return self::new(Host::fromUri($uri));
}
/**
* Create a new instance from an Authority object.
*/
public static function fromAuthority(Stringable|string $authority): self
{
return self::new(Host::fromAuthority($authority));
}
public function value(): ?string
{
return $this->host->value();
}
public function equals(mixed $value): bool
{
return $this->host->equals($value);
}
public function toAscii(): ?string
{
return $this->host->toAscii();
}
public function toUnicode(): ?string
{
return $this->host->toUnicode();
}
public function encoded(): ?string
{
return $this->host->encoded();
}
public function isIp(): bool
{
return false;
}
public function isDomain(): bool
{
return true;
}
public function isRegisteredName(): bool
{
return true;
}
public function getIpVersion(): ?string
{
return null;
}
public function getIp(): ?string
{
return null;
}
public function count(): int
{
return count($this->labels);
}
public function getIterator(): Iterator
{
yield from $this->labels;
}
public function get(int $offset): ?string
{
if ($offset < 0) {
$offset += count($this->labels);
}
return $this->labels[$offset] ?? null;
}
public function keys(?string $label = null): array
{
return match (null) {
$label => array_keys($this->labels),
default => array_keys($this->labels, $label, true),
};
}
public function isAbsolute(): bool
{
return count($this->labels) > 1 && '' === $this->labels[array_key_first($this->labels)];
}
public function prepend(Stringable|string|int|null $label): DomainHostInterface
{
$label = self::filterComponent($label);
$value = $this->value();
return match (true) {
null === $label => $this,
null === $value => new self($label),
str_ends_with($label, self::SEPARATOR) => new self($label.$value),
default => new self($label.self::SEPARATOR.$value),
};
}
public function append(Stringable|string|int|null $label): DomainHostInterface
{
$label = self::filterComponent($label);
$value = $this->value();
return match (true) {
null === $label => $this,
null === $value => new self($label),
!$this->isAbsolute() => new self($value.self::SEPARATOR.$label),
str_ends_with($label, self::SEPARATOR) => new self($value.$label),
default => new self($value.$label.self::SEPARATOR),
};
}
public function withRootLabel(): DomainHostInterface
{
$key = array_key_first($this->labels);
return match ($this->labels[$key]) {
'' => $this,
default => $this->append(''),
};
}
public function slice(int $offset, ?int $length = null): self
{
$nbLabels = count($this->labels);
if ($offset < -$nbLabels || $offset > $nbLabels) {
throw new OffsetOutOfBounds(sprintf('No label can be found with at : `%s`.', $offset));
}
$labels = array_slice($this->labels, $offset, $length, true);
return match ($labels) {
$this->labels => $this,
default => self::fromLabels(...$labels),
};
}
public function withoutRootLabel(): DomainHostInterface
{
$key = array_key_first($this->labels);
if ('' !== $this->labels[$key]) {
return $this;
}
$labels = $this->labels;
array_shift($labels);
return self::fromLabels(...$labels);
}
/**
* @throws OffsetOutOfBounds
*/
public function withLabel(int $key, Stringable|string|int|null $label): DomainHostInterface
{
$nbLabels = count($this->labels);
if ($key < - $nbLabels - 1 || $key > $nbLabels) {
throw new OffsetOutOfBounds(sprintf('No label can be added with the submitted key : `%s`.', $key));
}
if (0 > $key) {
$key += $nbLabels;
}
if ($nbLabels === $key) {
return $this->append($label);
}
if (-1 === $key) {
return $this->prepend($label);
}
if (!$label instanceof HostInterface && null !== $label) {
$label = Host::new((string) $label)->value();
}
if ($label === $this->labels[$key]) {
return $this;
}
$labels = $this->labels;
$labels[$key] = $label;
return new self(implode(self::SEPARATOR, array_reverse($labels)));
}
public function withoutLabel(int ...$keys): DomainHostInterface
{
if ([] === $keys) {
return $this;
}
$nb_labels = count($this->labels);
foreach ($keys as &$offset) {
if (- $nb_labels > $offset || $nb_labels - 1 < $offset) {
throw new OffsetOutOfBounds(sprintf('No label can be removed with the submitted key : `%s`.', $offset));
}
if (0 > $offset) {
$offset += $nb_labels;
}
}
unset($offset);
$deleted_keys = array_keys(array_count_values($keys));
$filter = static fn ($key): bool => !in_array($key, $deleted_keys, true);
return self::fromLabels(...array_filter($this->labels, $filter, ARRAY_FILTER_USE_KEY));
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Domain::getIterator()
*
* @codeCoverageIgnore
*
* Returns a new instance from a string or a stringable object.
*/
#[Deprecated(message:'use League\Uri\Components\Domain::getIterator() instead', since:'league/uri-components:7.0.0')]
public function labels(): array
{
return $this->labels;
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Domain::new()
*
* @codeCoverageIgnore
*
* Returns a new instance from a string or a stringable object.
*/
#[Deprecated(message:'use League\Uri\Components\Domain::new() instead', since:'league/uri-components:7.0.0')]
public static function createFromString(Stringable|string $host): self
{
return self::new($host);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Domain::fromLabels()
*
* @codeCoverageIgnore
*
* Returns a new instance from an iterable structure.
*
* @throws TypeError If a label is the null value
*/
#[Deprecated(message:'use League\Uri\Components\Domain::fromLabels() instead', since:'league/uri-components:7.0.0')]
public static function createFromLabels(iterable $labels): self
{
return self::fromLabels(...$labels);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Domain::fromUri()
*
* @codeCoverageIgnore
*
* Create a new instance from a URI object.
*/
#[Deprecated(message:'use League\Uri\Components\Domain::fromUri() instead', since:'league/uri-components:7.0.0')]
public static function createFromUri(Psr7UriInterface|UriInterface $uri): self
{
return self::fromUri($uri);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Domain::fromAuthority()
*
* @codeCoverageIgnore
*
* Create a new instance from an Authority object.
*/
#[Deprecated(message:'use League\Uri\Components\Domain::fromAuthority() instead', since:'league/uri-components:7.0.0')]
public static function createFromAuthority(AuthorityInterface|Stringable|string $authority): self
{
return self::fromAuthority($authority);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Domain::new()
*
* @codeCoverageIgnore
*
* Returns a new instance from an iterable structure.
*/
#[Deprecated(message:'use League\Uri\Components\Domain::new() instead', since:'league/uri-components:7.0.0')]
public static function createFromHost(HostInterface $host): self
{
return self::new($host);
}
}
@@ -0,0 +1,146 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Components;
use Deprecated;
use League\Uri\Contracts\FragmentInterface;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Contracts\UriException;
use League\Uri\Contracts\UriInterface;
use League\Uri\Encoder;
use League\Uri\UriString;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use Stringable;
use Uri\Rfc3986\Uri as Rfc3986Uri;
use Uri\WhatWg\Url as WhatWgUrl;
use function is_string;
use function str_replace;
final class Fragment extends Component implements FragmentInterface
{
private readonly ?string $fragment;
/**
* New instance.
*/
private function __construct(Stringable|string|null $fragment)
{
$this->fragment = $this->validateComponent($fragment);
}
public static function new(Stringable|string|null $value = null): self
{
return new self($value);
}
/**
* Create a new instance from a string.or a stringable structure or returns null on failure.
*/
public static function tryNew(Stringable|string|null $uri = null): ?self
{
try {
return self::new($uri);
} catch (UriException) {
return null;
}
}
/**
* Create a new instance from a URI object.
*/
public static function fromUri(WhatWgUrl|Rfc3986Uri|Stringable|string $uri): self
{
$uri = self::filterUri($uri);
return match (true) {
$uri instanceof Rfc3986Uri => new self($uri->getRawFragment()),
$uri instanceof Psr7UriInterface => new self(UriString::parse($uri)['fragment']),
default => new self($uri->getFragment()),
};
}
public function value(): ?string
{
return Encoder::encodeQueryOrFragment($this->fragment);
}
public function getUriComponent(): string
{
return (null === $this->fragment ? '' : '#').$this->value();
}
/**
* Returns the decoded fragment.
*/
public function decoded(): ?string
{
if (null === $this->fragment) {
return null;
}
return str_replace('%20', ' ', $this->fragment);
}
public function equals(mixed $value): bool
{
if (!$value instanceof Stringable && !is_string($value) && null !== $value) {
return false;
}
if (!$value instanceof UriComponentInterface) {
$value = self::tryNew($value);
if (null === $value) {
return false;
}
}
return $value->getUriComponent() === $this->getUriComponent();
}
public function normalize(): self
{
return new self(Encoder::normalizeFragment($this->value()));
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Fragment::new()
*
* @codeCoverageIgnore
*/
#[Deprecated(message:'use League\Uri\Components\Fragment::new() instead', since:'league/uri-components:7.0.0')]
public static function createFromString(Stringable|string $fragment): self
{
return self::new($fragment);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Fragment::fromUri()
*
* @codeCoverageIgnore
*
* Create a new instance from a URI object.
*/
#[Deprecated(message:'use League\Uri\Components\Fragment::fromUri() instead', since:'league/uri-components:7.0.0')]
public static function createFromUri(Psr7UriInterface|UriInterface $uri): self
{
return self::fromUri($uri);
}
}
@@ -0,0 +1,391 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Components;
use Countable;
use IteratorAggregate;
use League\Uri\Components\FragmentDirectives\DirectiveString;
use League\Uri\Contracts\FragmentDirective;
use League\Uri\Contracts\FragmentInterface;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Contracts\UriInterface;
use League\Uri\Encoder;
use League\Uri\Exceptions\OffsetOutOfBounds;
use League\Uri\Modifier;
use League\Uri\Uri;
use League\Uri\UriString;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use Stringable;
use Throwable;
use Traversable;
use Uri\Rfc3986\Uri as Rfc3986Uri;
use Uri\WhatWg\Url as WhatWgUrl;
use function array_count_values;
use function array_filter;
use function array_keys;
use function array_map;
use function array_slice;
use function array_values;
use function count;
use function explode;
use function implode;
use function in_array;
use function is_bool;
use function is_string;
use function sprintf;
use function str_replace;
use function strpos;
use function substr;
use const ARRAY_FILTER_USE_BOTH;
/**
* @see https://wicg.github.io/scroll-to-text-fragment/
*
* @implements IteratorAggregate<int, FragmentDirective>
*/
final class FragmentDirectives implements FragmentInterface, IteratorAggregate, Countable
{
public const DELIMITER = ':~:';
public const SEPARATOR = '&';
/** @var list<FragmentDirective> */
private readonly array $directives;
public function __construct(FragmentDirective|Stringable|string ...$directives)
{
$this->directives = array_values(array_map(self::filterDirective(...), $directives));
}
/**
* Create a new instance from a Fragment.
*
* If no delimiter is found, an empty collection is returned
*/
public static function fromFragment(Stringable|string|null $fragment): self
{
if ($fragment instanceof UriComponentInterface) {
$fragment = $fragment->value();
}
if (null === $fragment) {
return new self();
}
$fragment = (string) $fragment;
$pos = strpos($fragment, self::DELIMITER);
if (false === $pos) {
return new self();
}
return self::new(substr($fragment, $pos + 3));
}
/**
* Create a new instance from a string which only contains directives.
*/
public static function new(Stringable|string|null $value): self
{
return null === $value
? new self()
: new self(...explode(self::SEPARATOR, (string) $value));
}
private static function filterDirective(FragmentDirective|Stringable|string $directive): FragmentDirective
{
return $directive instanceof FragmentDirective ? $directive : DirectiveString::resolve($directive);
}
public static function tryNew(Stringable|string|null $value): ?self
{
try {
return self::new($value);
} catch (Throwable) {
return null;
}
}
/**
* Create a new instance from a URI string or object.
*/
public static function fromUri(WhatWgUrl|Rfc3986Uri|Stringable|string $uri): self
{
if ($uri instanceof Modifier) {
$uri = $uri->unwrap();
}
return self::fromFragment(match (true) {
$uri instanceof Psr7UriInterface => UriString::parse($uri)['fragment'],
$uri instanceof Rfc3986Uri => $uri->getRawFragment(),
$uri instanceof UriInterface, $uri instanceof WhatWgUrl => $uri->getFragment(),
default => Uri::new($uri)->getFragment(),
});
}
public function count(): int
{
return count($this->directives);
}
public function getIterator(): Traversable
{
yield from $this->directives;
}
public function __toString(): string
{
return $this->toString();
}
public function jsonSerialize(): string
{
return $this->toString();
}
public function value(): ?string
{
return [] === $this->directives
? null
: self::DELIMITER.implode(
self::SEPARATOR,
array_map(fn (FragmentDirective $directive): string => $directive->toString(), $this->directives)
);
}
public function toString(): string
{
return (string) $this->value();
}
public function getUriComponent(): string
{
$fragment = $this->value();
return (null === $fragment ? '' : '#').$fragment;
}
public function decoded(): ?string
{
return [] === $this->directives
? null
: str_replace('%20', ' ', (string) Encoder::decodeFragment($this->toString()));
}
/**
* Returns the Directive at a specified offset or null if none is defined.
*
* Negative offsets are supported.
*/
public function nth(int $offset): ?FragmentDirective
{
if ($offset < 0) {
$offset += count($this->directives);
}
return $this->directives[$offset] ?? null;
}
/**
* The first Directive defined on the fragment or null if none are defined.
*/
public function first(): ?FragmentDirective
{
return $this->nth(0);
}
/**
* The last Directive defined on the fragment or null if none are defined.
*/
public function last(): ?FragmentDirective
{
return $this->nth(-1);
}
/**
* Tells whether all the submitted keys are present in the collection.
*
* Negative offsets are supported.
*/
public function has(int ...$offsets): bool
{
$nbDirectives = count($this->directives);
foreach ($offsets as $offset) {
if ($offset < 0) {
$offset += $nbDirectives;
}
if (! isset($this->directives[$offset])) {
return false;
}
}
return [] !== $offsets;
}
public function isEmpty(): bool
{
return [] === $this->directives;
}
public function equals(mixed $value): bool
{
if (!$value instanceof Stringable && !is_string($value) && null !== $value) {
return false;
}
if (!$value instanceof UriComponentInterface) {
$value = self::tryNew($value);
if (null === $value) {
return false;
}
}
return $value->getUriComponent() === $this->getUriComponent();
}
public function indexOf(FragmentDirective|Stringable|string $directive): ?int
{
$directive = self::filterDirective($directive);
foreach ($this->directives as $offset => $innerDirective) {
if ($innerDirective->equals($directive)) {
return $offset;
}
}
return null;
}
public function contains(FragmentDirective|Stringable|string $directive): bool
{
return null !== $this->indexOf($directive);
}
/**
* Append one or more Directives to the fragment.
*/
public function append(FragmentDirectives|FragmentDirective|Stringable|string ...$directives): self
{
$items = self::implodeDirectives(...$directives);
return [] === $items ? $this : new self(...$this->directives, ...$items);
}
/**
* Prepend one or more Directives to the fragment.
*/
public function prepend(FragmentDirectives|FragmentDirective|Stringable|string ...$directives): self
{
$items = self::implodeDirectives(...$directives);
return [] === $items ? $this : new self(...$items, ...$this->directives);
}
/**
* @return list<FragmentDirective|Stringable|string>
*/
private static function implodeDirectives(FragmentDirectives|FragmentDirective|Stringable|string ...$directives): array
{
return array_merge(...array_map(fn ($d) => $d instanceof FragmentDirectives ? [...$d] : [$d], $directives));
}
/**
* Removes one or more Directives by offset from the fragment.
*/
public function remove(int ...$keys): self
{
if ([] === $keys) {
return $this;
}
$nbDirectives = count($this->directives);
$deletedKeys = [];
foreach ($keys as $key) {
$value = $key;
if ($value < 0) {
$value += $nbDirectives;
}
isset($this->directives[$value]) || throw new OffsetOutOfBounds(sprintf('The key `%s` is invalid.', $key));
$deletedKeys[] = $value;
}
$deletedKeys = array_keys(array_count_values($deletedKeys));
return $this->filter(fn (FragmentDirective $directive, int $offset): bool => !in_array($offset, $deletedKeys, true)); /* @phpstan-ignore-line */
}
/**
* Slices the fragment to remove Directives portions.
*/
public function slice(int $offset, ?int $length = null): self
{
$nbDirectives = count($this->directives);
($offset >= -$nbDirectives && $offset <= $nbDirectives) || throw new OffsetOutOfBounds(sprintf('No directive can be found at : `%s`.', $offset));
$directives = array_slice($this->directives, $offset, $length);
return $directives === $this->directives ? $this : new self(...$directives);
}
/**
* Filter the Directives to return a new instance based on the callback.
*
* @param callable(FragmentDirective, int=): bool $callback
*/
public function filter(callable $callback): self
{
$directives = array_filter($this->directives, $callback, ARRAY_FILTER_USE_BOTH);
return $directives === $this->directives ? $this : new self(...$directives);
}
/**
* Replace the Directive define at a specific offset.
* Negative offsets are supported.
*
* If no Directive is found to the specified offset, an exception is thrown
*/
public function replace(int $offset, FragmentDirective|Stringable|string $directive): self
{
$currentDirective = $this->nth($offset);
null !== $currentDirective || throw new OffsetOutOfBounds(sprintf('The key `%s` is invalid.', $offset));
$directive = self::filterDirective($directive);
if ($directive::class === $currentDirective::class && $currentDirective->equals($directive)) {
return $this;
}
if ($offset < 0) {
$offset += count($this->directives);
}
$directives = $this->directives;
$directives[$offset] = $directive;
return new self(...$directives);
}
public function when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null): self
{
if (!is_bool($condition)) {
$condition = $condition($this);
}
return match (true) {
$condition => $onSuccess($this),
null !== $onFail => $onFail($this),
default => $this,
} ?? $this;
}
}
@@ -0,0 +1,39 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Components\FragmentDirectives;
use League\Uri\Contracts\FragmentDirective;
use Stringable;
use function preg_match;
final class DirectiveString
{
/**
* Parse a Directive string representation.
*
* A Directive syntax is name[=value] where the
* separator `=` is not present when no value
* is attached to it
*/
public static function resolve(Stringable|string $directive): FragmentDirective
{
$directive = (string) $directive;
return match (true) {
1 === preg_match('/^text(?:=|$)/i', $directive) => TextDirective::fromString($directive),
default => GenericDirective::fromString($directive),
};
}
}
@@ -0,0 +1,96 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Components\FragmentDirectives;
use League\Uri\Contracts\FragmentDirective;
use League\Uri\Encoder;
use League\Uri\Exceptions\SyntaxError;
use Stringable;
use Throwable;
use function explode;
use function str_replace;
final class GenericDirective implements FragmentDirective
{
/**
* @param non-empty-string $name
*/
private function __construct(
private readonly string $name,
private readonly ?string $value = null,
) {
}
/**
* Create a new instance from a string without the Directive delimiter (:~:) or a separator (&).
*/
public static function fromString(Stringable|string $value): self
{
[$name, $value] = explode('=', (string) $value, 2) + [1 => null];
(null !== $name && '' !== $name && !str_contains($name, '&')) || throw new SyntaxError('The submitted text is not a valid directive.');
return new self($name, $value);
}
private static function decode(?string $value): ?string
{
return null !== $value ? str_replace('%20', ' ', (string) Encoder::decodeFragment($value)) : null;
}
public function name(): string
{
/** @var non-empty-string $name */
$name = (string) self::decode($this->name);
return $name;
}
public function value(): ?string
{
return self::decode($this->value);
}
public function toString(): string
{
$str = $this->name;
if (null === $this->value) {
return $str;
}
return $str.'='.$this->value;
}
public function __toString(): string
{
return $this->toString();
}
public function equals(mixed $directive): bool
{
if (!$directive instanceof Stringable && !is_string($directive)) {
return false;
}
if (!$directive instanceof FragmentDirective) {
try {
$directive = self::fromString($directive);
} catch (Throwable) {
return false;
}
}
return $directive->toString() === $this->toString();
}
}
@@ -0,0 +1,246 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Components\FragmentDirectives;
use League\Uri\Contracts\FragmentDirective;
use League\Uri\Encoder;
use League\Uri\Exceptions\SyntaxError;
use Stringable;
use Throwable;
use function explode;
use function is_string;
use function preg_match;
use function str_replace;
final class TextDirective implements FragmentDirective
{
private const NAME = 'text';
private const REGEXP_PATTERN = '/^
(?:(?<prefix>.+?)-,)? # optional prefix up to first "-,"
(?<start>[^,]+?) # required start (up to "," or end)
(?:,(?<end>[^,-]*),?)? # optional end, stop before ",-" if present
(?:,-(?<suffix>.+))? # optional suffix (to end)
$/x';
/**
* @param non-empty-string $start
* @param ?non-empty-string $end
* @param ?non-empty-string $prefix
* @param ?non-empty-string $suffix
*/
public function __construct(
public readonly string $start,
public readonly ?string $end = null,
public readonly ?string $prefix = null,
public readonly ?string $suffix = null,
) {
('' !== $this->start && '' !== $this->end && '' !== $this->prefix && '' !== $this->suffix)
|| throw new SyntaxError('The start part can not be the empty string.');
}
/**
* Create a new instance from a string without the Directive delimiter (:~:) or a separator (&).
*/
public static function fromString(Stringable|string $value): self
{
[$name, $value] = explode('=', (string) $value, 2) + [1 => ''];
self::NAME === $name || throw new SyntaxError('The submitted text is not a text directive.');
return self::fromValue($value);
}
/**
* Create a new instance from a string without the Directive name and the separator (=).
*/
public static function fromValue(Stringable|string $text): self
{
'' !== $text || throw new SyntaxError('The text directive value can not be the empty string.');
1 === preg_match(self::REGEXP_PATTERN, (string) $text, $matches) || throw new SyntaxError('The text directive is malformed.');
if ('' === $matches['prefix']) {
$matches['prefix'] = null;
}
/** @var non-empty-string $start */
$start = (string) self::decode($matches['start']);
/** @var ?non-empty-string $prefix */
$prefix = self::decode($matches['prefix']);
/** @var ?non-empty-string $suffix */
$suffix = self::decode($matches['suffix'] ?? null);
$matches['end'] ??= null;
if ('' === $matches['end']) {
$matches['end'] = null;
}
/** @var ?non-empty-string $end */
$end = self::decode($matches['end']);
return new self($start, $end, $prefix, $suffix);
}
private static function encode(?string $value): ?string
{
return null !== $value ? strtr((string) Encoder::encodeQueryOrFragment($value), ['-' => '%2D', ',' => '%2C', '&' => '%26']) : null;
}
private static function decode(?string $value): ?string
{
if (null === $value) {
return null;
}
return str_replace('%20', ' ', (string) Encoder::decodeFragment($value));
}
public function name(): string
{
return self::NAME;
}
public function value(): string
{
$str = $this->start;
if (null !== $this->prefix) {
$str = $this->prefix.'-,'.$str;
}
if (null !== $this->end) {
$str .= ','.$this->end;
}
if (null !== $this->suffix) {
$str .= ',-'.$this->suffix;
}
return $str;
}
public function toString(): string
{
$encodedValue = (string) self::encode($this->start);
$prefix = self::encode($this->prefix);
if (null !== $prefix) {
$encodedValue = $prefix.'-,'.$encodedValue;
}
$end = self::encode($this->end);
if (null !== $end) {
$encodedValue .= ','.$end;
}
$suffix = self::encode($this->suffix);
if (null !== $suffix) {
$encodedValue .= ',-'.$suffix;
}
return self::NAME.'='.$encodedValue;
}
public function __toString(): string
{
return $this->toString();
}
public function equals(mixed $directive): bool
{
if (!$directive instanceof Stringable && !is_string($directive)) {
return false;
}
if (!$directive instanceof FragmentDirective) {
try {
$directive = self::fromString($directive);
} catch (Throwable) {
return false;
}
}
return $directive->toString() === $this->toString();
}
/**
* Returns a new instance with a new start portion.
*
* The submitted string must be in its decoded form
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the new start portion.
*
* @param non-empty-string $text
*/
public function startsWith(string $text): self
{
if ($this->start === $text) {
return $this;
}
return new self($text, $this->end, $this->prefix, $this->suffix);
}
/**
* Returns a new instance with a new end portion.
*
* The submitted string must be in its decoded form
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the new end portion.
*
* @param ?non-empty-string $text
*/
public function endsWith(?string $text): self
{
if ($this->end === $text) {
return $this;
}
return new self($this->start, $text, $this->prefix, $this->suffix);
}
/**
* Returns a new instance with a new suffix portion.
*
* The submitted string must be in its decoded form
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the new suffix portion.
*
* @param ?non-empty-string $text
*/
public function followedBy(?string $text): self
{
if ($this->suffix === $text) {
return $this;
}
return new self($this->start, $this->end, $this->prefix, $text);
}
/**
* Returns a new instance with a new prefix portion.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the new prefix portion.
*
* @param ?non-empty-string $text
*/
public function precededBy(?string $text): self
{
if ($this->prefix === $text) {
return $this;
}
return new self($this->start, $this->end, $text, $this->suffix);
}
}
@@ -0,0 +1,569 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Components;
use Deprecated;
use Iterator;
use League\Uri\Contracts\PathInterface;
use League\Uri\Contracts\SegmentedPathInterface;
use League\Uri\Contracts\UriException;
use League\Uri\Contracts\UriInterface;
use League\Uri\Encoder;
use League\Uri\Exceptions\OffsetOutOfBounds;
use League\Uri\Exceptions\SyntaxError;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use Stringable;
use TypeError;
use Uri\Rfc3986\Uri as Rfc3986Uri;
use Uri\WhatWg\Url as WhatWgUrl;
use function array_count_values;
use function array_filter;
use function array_keys;
use function array_pop;
use function array_unshift;
use function count;
use function dirname;
use function explode;
use function implode;
use function ltrim;
use function rtrim;
use function sprintf;
use function str_contains;
use function str_replace;
use function str_starts_with;
use function strrpos;
use function substr;
use const ARRAY_FILTER_USE_KEY;
use const FILTER_VALIDATE_INT;
use const PATHINFO_EXTENSION;
final class HierarchicalPath extends Component implements SegmentedPathInterface
{
private const SEPARATOR = '/';
private const IS_ABSOLUTE = 1;
private const IS_RELATIVE = 0;
private readonly PathInterface $path;
/** @var array<string> */
private readonly array $segments;
private function __construct(Stringable|string $path)
{
if (!$path instanceof PathInterface) {
$path = Path::new($path);
}
$this->path = $path;
$segments = $this->path->decoded();
if ($this->path->isAbsolute()) {
$segments = substr($segments, 1);
}
$this->segments = explode(self::SEPARATOR, $segments);
}
/**
* Returns a new instance from a string or a stringable object.
*/
public static function new(Stringable|string $value = ''): self
{
return new self($value);
}
/**
* Create a new instance from a string.or a stringable structure or returns null on failure.
*/
public static function tryNew(Stringable|string $uri = ''): ?self
{
try {
return self::new($uri);
} catch (UriException) {
return null;
}
}
/**
* Create a new instance from a URI object.
*/
public static function fromUri(WhatWgUrl|Rfc3986Uri|Stringable|string $uri): self
{
return new self(Path::fromUri($uri));
}
/**
* Returns a new instance from an iterable structure.
*
* @throws TypeError If the segments are malformed
*/
public static function fromRelative(string ...$segments): self
{
return self::fromSegments(self::IS_RELATIVE, $segments);
}
/**
* Returns a new instance from an iterable structure.
*
* @throws TypeError If the segments are malformed
*/
public static function fromAbsolute(string ...$segments): self
{
return self::fromSegments(self::IS_ABSOLUTE, $segments);
}
/**
* @param array<string> $segments
*/
private static function fromSegments(int $pathType, array $segments): self
{
$path = implode(self::SEPARATOR, $segments);
return match (true) {
self::IS_RELATIVE === $pathType => new self(ltrim($path, self::SEPARATOR)),
self::SEPARATOR !== ($path[0] ?? '') => new self(self::SEPARATOR.$path),
default => new self($path),
};
}
public function count(): int
{
return count($this->segments);
}
public function getIterator(): Iterator
{
yield from $this->segments;
}
public function isAbsolute(): bool
{
return $this->path->isAbsolute();
}
public function hasTrailingSlash(): bool
{
return $this->path->hasTrailingSlash();
}
public function value(): ?string
{
return $this->path->value();
}
public function equals(mixed $value): bool
{
return $this->path->equals($value);
}
public function decoded(): string
{
return $this->path->decoded();
}
public function normalize(): self
{
return new self((string) $this->path->normalize()->value());
}
public function getDirname(): string
{
$path = $this->path->decoded();
return str_replace(
['\\', "\0"],
[self::SEPARATOR, '\\'],
dirname(str_replace('\\', "\0", $path))
);
}
public function getBasename(): string
{
$data = $this->segments;
$basename = (string) array_pop($data);
$pos = strpos($basename, ';');
return match (false) {
$pos => $basename,
default => substr($basename, 0, $pos),
};
}
public function getExtension(): string
{
[$basename] = explode(';', $this->getBasename(), 2);
return pathinfo($basename, PATHINFO_EXTENSION);
}
public function get(int $offset): ?string
{
if ($offset < 0) {
$offset += count($this->segments);
}
return $this->segments[$offset] ?? null;
}
public function keys(Stringable|string|null $segment = null): array
{
$segment = self::filterComponent($segment);
return match (null) {
$segment => array_keys($this->segments),
default => array_keys($this->segments, $segment, true),
};
}
public function withoutDotSegments(): PathInterface
{
$path = $this->path->withoutDotSegments();
return match ($this->path) {
$path => $this,
default => new self($path),
};
}
public function withLeadingSlash(): PathInterface
{
$path = $this->path->withLeadingSlash();
return match ($this->path) {
$path => $this,
default => new self($path),
};
}
public function withoutLeadingSlash(): PathInterface
{
$path = $this->path->withoutLeadingSlash();
return match ($this->path) {
$path => $this,
default => new self($path),
};
}
public function withoutTrailingSlash(): PathInterface
{
$path = $this->path->withoutTrailingSlash();
return match ($this->path) {
$path => $this,
default => new self($path),
};
}
public function withTrailingSlash(): PathInterface
{
$path = $this->path->withTrailingSlash();
return match ($this->path) {
$path => $this,
default => new self($path),
};
}
public function append(Stringable|string $segment): SegmentedPathInterface
{
/** @var string $segment */
$segment = self::filterComponent($segment);
return new self(
rtrim($this->path->toString(), self::SEPARATOR)
.self::SEPARATOR
.ltrim($segment, self::SEPARATOR)
);
}
public function prepend(Stringable|string $segment): SegmentedPathInterface
{
/** @var string $segment */
$segment = self::filterComponent($segment);
return new self(
rtrim($segment, self::SEPARATOR)
.self::SEPARATOR
.ltrim($this->path->toString(), self::SEPARATOR)
);
}
public function withSegment(int $key, Stringable|string $segment): SegmentedPathInterface
{
$nbSegments = count($this->segments);
if ($key < - $nbSegments - 1 || $key > $nbSegments) {
throw new OffsetOutOfBounds(sprintf('The given key `%s` is invalid.', $key));
}
if (0 > $key) {
$key += $nbSegments;
}
if ($nbSegments === $key) {
return $this->append($segment);
}
if (-1 === $key) {
return $this->prepend($segment);
}
if (!$segment instanceof PathInterface) {
$segment = new self($segment);
}
$segment = Encoder::decodeAll($segment);
if ($segment === $this->segments[$key]) {
return $this;
}
$segments = $this->segments;
$segments[$key] = $segment;
if ($this->isAbsolute()) {
array_unshift($segments, '');
}
return new self(implode(self::SEPARATOR, $segments));
}
public function withoutEmptySegments(): SegmentedPathInterface
{
/** @var string $path */
$path = preg_replace(',/+,', self::SEPARATOR, $this->toString());
return new self($path);
}
public function withoutSegment(int ...$keys): SegmentedPathInterface
{
if ([] === $keys) {
return $this;
}
$nb_segments = count($this->segments);
$options = ['options' => ['min_range' => - $nb_segments, 'max_range' => $nb_segments - 1]];
$deleted_keys = [];
foreach ($keys as $value) {
/** @var false|int $offset */
$offset = filter_var($value, FILTER_VALIDATE_INT, $options);
if (false === $offset) {
throw new OffsetOutOfBounds(sprintf('The key `%s` is invalid.', $value));
}
if ($offset < 0) {
$offset += $nb_segments;
}
$deleted_keys[] = $offset;
}
$deleted_keys = array_keys(array_count_values($deleted_keys));
$filter = static fn ($key): bool => !in_array($key, $deleted_keys, true);
$path = implode(self::SEPARATOR, array_filter($this->segments, $filter, ARRAY_FILTER_USE_KEY));
if ($this->isAbsolute()) {
return new self(self::SEPARATOR.$path);
}
return new self($path);
}
public function slice(int $offset, ?int $length = null): self
{
$nbSegments = count($this->segments);
if ($offset < -$nbSegments || $offset > $nbSegments) {
throw new OffsetOutOfBounds(sprintf('No segment can be found with at : `%s`.', $offset));
}
$segments = array_slice($this->segments, $offset, $length, true);
if ($this->hasTrailingSlash()) {
$segments[] = '';
}
return match (true) {
$segments === $this->segments => $this,
$this->isAbsolute() => self::fromAbsolute(...$segments),
default => self::fromRelative(...$segments),
};
}
public function withDirname(Stringable|string $path): SegmentedPathInterface
{
if (!$path instanceof PathInterface) {
$path = Path::new($path);
}
if ($path->value() === $this->getDirname()) {
return $this;
}
$segments = $this->segments;
return new self(
rtrim($path->toString(), self::SEPARATOR)
.self::SEPARATOR
.array_pop($segments)
);
}
public function withBasename(Stringable|string $basename): SegmentedPathInterface
{
/** @var string $basename */
$basename = $this->validateComponent($basename);
return match (true) {
str_contains($basename, self::SEPARATOR) => throw new SyntaxError('The basename cannot contain the path separator.'),
default => $this->withSegment(count($this->segments) - 1, $basename),
};
}
public function withExtension(Stringable|string $extension): SegmentedPathInterface
{
/** @var string $extension */
$extension = $this->validateComponent($extension);
if (str_contains($extension, self::SEPARATOR)) {
throw new SyntaxError('An extension sequence cannot contain a path delimiter.');
}
if (str_starts_with($extension, '.')) {
throw new SyntaxError('An extension sequence cannot contain a leading `.` character.');
}
/** @var string $basename */
$basename = $this->segments[array_key_last($this->segments)];
[$ext, $param] = explode(';', $basename, 2) + [1 => null];
if ('' === $ext) {
return $this;
}
return $this->withBasename($this->buildBasename($extension, (string) $ext, $param));
}
/**
* Creates a new basename with a new extension.
*/
private function buildBasename(string $extension, string $ext, ?string $param = null): string
{
$length = strrpos($ext, '.'.pathinfo($ext, PATHINFO_EXTENSION));
if (false !== $length) {
$ext = substr($ext, 0, $length);
}
if (null !== $param && '' !== $param) {
$param = ';'.$param;
}
$extension = trim($extension);
if ('' === $extension) {
return $ext.$param;
}
return $ext.'.'.$extension.$param;
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see HierarchicalPath::getIterator()
*
* @codeCoverageIgnore
*
* Returns a new instance from a string or a stringable object.
*/
#[Deprecated(message:'use League\Uri\Components\HierarchicalPath::getIterator() instead', since:'league/uri-components:7.0.0')]
public function segments(): array
{
return $this->segments;
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see HierarchicalPath::new()
*
* @codeCoverageIgnore
*
* Returns a new instance from a string or a stringable object.
*/
#[Deprecated(message:'use League\Uri\Components\HierarchicalPath::new() instead', since:'league/uri-components:7.0.0')]
public static function createFromString(Stringable|string $path): self
{
return self::new($path);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see HierarchicalPath::new()
*
* @codeCoverageIgnore
*/
#[Deprecated(message:'use League\Uri\Components\HierarchicalPath::new() instead', since:'league/uri-components:7.0.0')]
public static function createFromPath(PathInterface $path): self
{
return self::new($path);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @throws TypeError If the segments are malformed
*@see HierarchicalPath::fromRelative()
*
* @codeCoverageIgnore
*
* Returns a new instance from an iterable structure.
*
* @deprecated Since version 7.0.0
*/
#[Deprecated(message:'use League\Uri\Components\HierarchicalPath::fromRelative() instead', since:'league/uri-components:7.0.0')]
public static function createRelativeFromSegments(iterable $segments): self
{
return self::fromRelative(...$segments);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @throws TypeError If the segments are malformed
*@see HierarchicalPath::fromAbsolute()
*
* @codeCoverageIgnore
*
* Returns a new instance from an iterable structure.
*
* @deprecated Since version 7.0.0
*/
#[Deprecated(message:'use League\Uri\Components\HierarchicalPath::fromAbsolute() instead', since:'league/uri-components:7.0.0')]
public static function createAbsoluteFromSegments(iterable $segments): self
{
return self::fromAbsolute(...$segments);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see HierarchicalPath::fromUri()
*
* @codeCoverageIgnore
*
* Create a new instance from a URI object.
*/
#[Deprecated(message:'use League\Uri\Components\HierarchicalPath::fromUri() instead', since:'league/uri-components:7.0.0')]
public static function createFromUri(Psr7UriInterface|UriInterface $uri): self
{
return self::fromUri($uri);
}
}
@@ -0,0 +1,541 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Components;
use Deprecated;
use League\Uri\Contracts\AuthorityInterface;
use League\Uri\Contracts\IpHostInterface;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Contracts\UriException;
use League\Uri\Contracts\UriInterface;
use League\Uri\Exceptions\ConversionFailed;
use League\Uri\Exceptions\MissingFeature;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\Idna\Converter as IdnConverter;
use League\Uri\IPv4\Converter as IPv4Converter;
use League\Uri\IPv4Normalizer;
use League\Uri\UriString;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use Stringable;
use Uri\Rfc3986\Uri as Rfc3986Uri;
use Uri\WhatWg\Url as WhatWgUrl;
use function explode;
use function filter_var;
use function in_array;
use function inet_pton;
use function is_string;
use function preg_match;
use function preg_replace_callback;
use function rawurldecode;
use function rawurlencode;
use function sprintf;
use function strpos;
use function strtolower;
use function strtoupper;
use function substr;
use const FILTER_FLAG_IPV4;
use const FILTER_FLAG_IPV6;
use const FILTER_VALIDATE_IP;
final class Host extends Component implements IpHostInterface
{
protected const REGEXP_NON_ASCII_PATTERN = '/[^\x20-\x7f]/';
/**
* @see https://tools.ietf.org/html/rfc3986#section-3.2.2
*
* invalid characters in host regular expression
*/
private const REGEXP_INVALID_HOST_CHARS = '/
[:\/?#\[\]@ ] # gen-delims characters as well as the space character
/ix';
/**
* General registered name regular expression.
*
* @see https://tools.ietf.org/html/rfc3986#section-3.2.2
* @see https://regex101.com/r/fptU8V/1
*/
private const REGEXP_REGISTERED_NAME = '/
(?(DEFINE)
(?<unreserved>[a-z0-9_~\-]) # . is missing as it is used to separate labels
(?<sub_delims>[!$&\'()*+,;=])
(?<encoded>%[A-F0-9]{2})
(?<reg_name>(?:(?&unreserved)|(?&sub_delims)|(?&encoded))*)
)
^(?:(?&reg_name)\.)*(?&reg_name)\.?$
/ix';
/**
* Domain name regular expression.
*
* Everything but the domain name length is validated
*
* @see https://tools.ietf.org/html/rfc1034#section-3.5
* @see https://tools.ietf.org/html/rfc1123#section-2.1
* @see https://regex101.com/r/71j6rt/1
*/
private const REGEXP_DOMAIN_NAME = '/
(?(DEFINE)
(?<let_dig> [a-z0-9]) # alpha digit
(?<let_dig_hyp> [a-z0-9-]) # alpha digit and hyphen
(?<ldh_str> (?&let_dig_hyp){0,61}(?&let_dig)) # domain label end
(?<label> (?&let_dig)((?&ldh_str))?) # domain label
(?<domain> (?&label)(\.(?&label)){0,126}\.?) # domain name
)
^(?&domain)$
/ix';
/**
* @see https://tools.ietf.org/html/rfc3986#section-3.2.2
*
* IPvFuture regular expression
*/
private const REGEXP_IP_FUTURE = '/^
v(?<version>[A-F0-9]+)\.
(?:
(?<unreserved>[a-z0-9_~\-\.])|
(?<sub_delims>[!$&\'()*+,;=:]) # also include the : character
)+
$/ix';
private const REGEXP_GEN_DELIMS = '/[:\/?#\[\]@]/';
private const ADDRESS_BLOCK = "\xfe\x80";
private readonly ?string $host;
private readonly bool $isDomain;
private readonly ?string $ipVersion;
private readonly bool $hasZoneIdentifier;
private function __construct(Stringable|int|string|null $host)
{
[
'host' => $this->host,
'is_domain' => $this->isDomain,
'ip_version' => $this->ipVersion,
'has_zone_identifier' => $this->hasZoneIdentifier,
] = $this->parse($host);
}
/**
* @throws ConversionFailed if the submitted IDN host cannot be converted to a valid ascii form
*
* @return array{host:string|null, is_domain:bool, ip_version:string|null, has_zone_identifier:bool}
*/
private function parse(Stringable|int|string|null $host): array
{
$host = self::filterComponent($host);
if (null === $host) {
return [
'host' => null,
'is_domain' => true,
'ip_version' => null,
'has_zone_identifier' => false,
];
}
if ('' === $host) {
return [
'host' => '',
'is_domain' => false,
'ip_version' => null,
'has_zone_identifier' => false,
];
}
static $inMemoryCache = [];
if (isset($inMemoryCache[$host])) {
return $inMemoryCache[$host];
}
if (100 < count($inMemoryCache)) {
unset($inMemoryCache[array_key_first($inMemoryCache)]);
}
if (false !== filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
return $inMemoryCache[$host] = [
'host' => $host,
'is_domain' => false,
'ip_version' => '4',
'has_zone_identifier' => false,
];
}
if ('[' === $host[0] && str_ends_with($host, ']')) {
$ip_host = substr($host, 1, -1);
if ($this->isValidIpv6Hostname($ip_host)) {
return $inMemoryCache[$host] = [
'host' => $host,
'is_domain' => false,
'ip_version' => '6',
'has_zone_identifier' => str_contains($ip_host, '%'),
];
}
if (1 === preg_match(self::REGEXP_IP_FUTURE, $ip_host, $matches) && !in_array($matches['version'], ['4', '6'], true)) {
return $inMemoryCache[$host] = [
'host' => $host,
'is_domain' => false,
'ip_version' => $matches['version'],
'has_zone_identifier' => false,
];
}
throw new SyntaxError(sprintf('`%s` is an invalid IP literal format.', $host));
}
$domainName = rawurldecode($host);
$isAscii = false;
if (1 !== preg_match(self::REGEXP_NON_ASCII_PATTERN, $domainName)) {
$domainName = strtolower($domainName);
$isAscii = true;
}
if (1 === preg_match(self::REGEXP_REGISTERED_NAME, $domainName)) {
return $inMemoryCache[$domainName] = [
'host' => $domainName,
'is_domain' => $this->isValidDomain($domainName),
'ip_version' => null,
'has_zone_identifier' => false,
];
}
if ($isAscii || 1 === preg_match(self::REGEXP_INVALID_HOST_CHARS, $domainName)) {
throw new SyntaxError(sprintf('`%s` is an invalid domain name : the host contains invalid characters.', $host));
}
$host = IdnConverter::toAsciiOrFail($domainName);
return $inMemoryCache[$host] = [
'host' => $host,
'is_domain' => $this->isValidDomain($host),
'ip_version' => null,
'has_zone_identifier' => false,
];
}
/**
* Tells whether the registered name is a valid domain name according to RFC1123.
*
* @see http://man7.org/linux/man-pages/man7/hostname.7.html
* @see https://tools.ietf.org/html/rfc1123#section-2.1
*/
private function isValidDomain(string $hostname): bool
{
$domainMaxLength = str_ends_with($hostname, '.') ? 254 : 253;
return !isset($hostname[$domainMaxLength])
&& 1 === preg_match(self::REGEXP_DOMAIN_NAME, $hostname);
}
/**
* Validates an Ipv6 as Host.
*
* @see http://tools.ietf.org/html/rfc6874#section-2
* @see http://tools.ietf.org/html/rfc6874#section-4
*/
private function isValidIpv6Hostname(string $host): bool
{
[$ipv6, $scope] = explode('%', $host, 2) + [1 => null];
if (null === $scope) {
return (bool) filter_var($ipv6, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
}
$scope = rawurldecode('%'.$scope);
return 1 !== preg_match(self::REGEXP_NON_ASCII_PATTERN, $scope)
&& 1 !== preg_match(self::REGEXP_GEN_DELIMS, $scope)
&& false !== filter_var($ipv6, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)
&& str_starts_with((string)inet_pton((string)$ipv6), self::ADDRESS_BLOCK);
}
public static function new(Stringable|string|null $value = null): self
{
return new self($value);
}
/**
* Create a new instance from a string.or a stringable structure or returns null on failure.
*/
public static function tryNew(Stringable|string|null $uri = null): ?self
{
try {
return self::new($uri);
} catch (UriException) {
return null;
}
}
/**
* Returns a host from an IP address.
*
* @throws MissingFeature If detecting IPv4 is not possible
* @throws SyntaxError If the $ip cannot be converted into a Host
*/
public static function fromIp(Stringable|string $ip, string $version = ''): self
{
if ('' !== $version) {
return new self('[v'.$version.'.'.$ip.']');
}
$ip = (string) $ip;
if (false !== filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
return new self('['.$ip.']');
}
if (str_contains($ip, '%')) {
[$ipv6, $zoneId] = explode('%', rawurldecode($ip), 2) + [1 => ''];
return new self('['.$ipv6.'%25'.rawurlencode($zoneId).']');
}
$host = IPv4Converter::fromEnvironment()->toDecimal($ip);
if (null === $host) {
throw new SyntaxError(sprintf('`%s` is an invalid IP Host.', $ip));
}
return new self($host);
}
/**
* Create a new instance from a URI object.
*/
public static function fromUri(WhatWgUrl|Rfc3986Uri|Stringable|string $uri): self
{
$uri = self::filterUri($uri);
return match (true) {
$uri instanceof Rfc3986Uri => new self($uri->getRawHost()),
$uri instanceof WhatWgUrl => new self($uri->getAsciiHost()),
$uri instanceof Psr7UriInterface => new self(UriString::parse($uri)['host']),
default => new self($uri->getHost()),
};
}
/**
* Create a new instance from an Authority object.
*/
public static function fromAuthority(Stringable|string $authority): self
{
return match (true) {
$authority instanceof AuthorityInterface => new self($authority->getHost()),
default => new self(Authority::new($authority)->getHost()),
};
}
public function value(): ?string
{
return $this->host;
}
public function equals(mixed $value): bool
{
if (!$value instanceof Stringable && !is_string($value) && null !== $value) {
return false;
}
if (!$value instanceof UriComponentInterface) {
$value = self::tryNew($value);
if (null === $value) {
return false;
}
}
return $value->getUriComponent() === $this->getUriComponent();
}
public function toAscii(): ?string
{
return $this->value();
}
public function toUnicode(): ?string
{
return match (true) {
null !== $this->ipVersion,
null === $this->host => $this->host,
default => IdnConverter::toUnicode($this->host)->domain(),
};
}
public function encoded(): ?string
{
return match (true) {
null !== $this->ipVersion,
null === $this->host => $this->host,
default => (string) preg_replace_callback(
'/%[0-9A-F]{2}/i',
fn (array $matches) => strtoupper($matches[0]),
strtolower(rawurlencode(IdnConverter::toUnicode($this->host)->domain()))
),
};
}
public function getIpVersion(): ?string
{
return $this->ipVersion;
}
public function getIp(): ?string
{
if (null === $this->ipVersion) {
return null;
}
if ('4' === $this->ipVersion) {
return $this->host;
}
$ip = substr((string) $this->host, 1, -1);
if ('6' !== $this->ipVersion) {
return substr($ip, (int) strpos($ip, '.') + 1);
}
$pos = strpos($ip, '%');
if (false === $pos) {
return $ip;
}
return substr($ip, 0, $pos).'%'.rawurldecode(substr($ip, $pos + 3));
}
public function isRegisteredName(): bool
{
return !$this->isIp();
}
public function isDomain(): bool
{
return $this->isDomain;
}
public function isIp(): bool
{
return null !== $this->ipVersion;
}
public function isIpv4(): bool
{
return '4' === $this->ipVersion;
}
public function isIpv6(): bool
{
return '6' === $this->ipVersion;
}
public function isIpFuture(): bool
{
return !in_array($this->ipVersion, [null, '4', '6'], true);
}
public function hasZoneIdentifier(): bool
{
return $this->hasZoneIdentifier;
}
public function withoutZoneIdentifier(): IpHostInterface
{
if (!$this->hasZoneIdentifier) {
return $this;
}
[$ipv6] = explode('%', substr((string) $this->host, 1, -1));
return self::fromIp($ipv6);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Host::new()
*
* @codeCoverageIgnore
*/
#[Deprecated(message:'use League\Uri\Components\Host::new() instead', since:'league/uri-components:7.0.0')]
public static function createFromString(Stringable|string|null $host): self
{
return self::new($host);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Host::new()
*
* @codeCoverageIgnore
*
* Returns a new instance from null.
*/
#[Deprecated(message:'use League\Uri\Components\Host::new() instead', since:'league/uri-components:7.0.0')]
public static function createFromNull(): self
{
return self::new();
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @throws MissingFeature If detecting IPv4 is not possible
* @throws SyntaxError If the $ip cannot be converted into a Host
* @deprecated Since version 7.0.0
* @see Host::fromIp()
*
* @codeCoverageIgnore
*
* Returns a host from an IP address.
*
*/
#[Deprecated(message:'use League\Uri\Components\Host::fromIp() instead', since:'league/uri-components:7.0.0')]
public static function createFromIp(string $ip, string $version = '', ?IPv4Normalizer $normalizer = null): self
{
return self::fromIp($ip, $version);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Host::fromUri()
*
* @codeCoverageIgnore
*
* Create a new instance from a URI object.
*/
#[Deprecated(message:'use League\Uri\Components\Host::fromUri() instead', since:'league/uri-components:7.0.0')]
public static function createFromUri(Psr7UriInterface|UriInterface $uri): self
{
return self::fromUri($uri);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Host::fromAuthority()
*
* @codeCoverageIgnore
*
* Create a new instance from an Authority object.
*/
#[Deprecated(message:'use League\Uri\Components\Host::fromAuthority() instead', since:'league/uri-components:7.0.0')]
public static function createFromAuthority(Stringable|string $authority): self
{
return self::fromAuthority($authority);
}
}
@@ -0,0 +1,205 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Components;
use Deprecated;
use League\Uri\Contracts\PathInterface;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Contracts\UriInterface;
use League\Uri\Encoder;
use League\Uri\UriString;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use Stringable;
use Throwable;
use Uri\Rfc3986\Uri as Rfc3986Uri;
use Uri\WhatWg\Url as WhatWgUrl;
use function is_string;
use function substr;
final class Path extends Component implements PathInterface
{
private const SEPARATOR = '/';
private readonly string $path;
/**
* New instance.
*/
private function __construct(Stringable|string $path)
{
$this->path = $this->validate($path);
}
/**
* Validate the component content.
*/
private function validate(Stringable|string $path): string
{
return (string) $this->validateComponent($path);
}
/**
* Returns a new instance from a string or a stringable object.
*/
public static function new(Stringable|string $value = ''): self
{
return new self($value);
}
/**
* Create a new instance from a string or a stringable structure or returns null on failure.
*/
public static function tryNew(Stringable|string $uri = ''): ?self
{
try {
return self::new($uri);
} catch (Throwable) {
return null;
}
}
/**
* Create a new instance from a URI object.
*/
public static function fromUri(WhatwgUrl|Rfc3986Uri|Stringable|string $uri): self
{
$uri = self::filterUri($uri);
if ($uri instanceof Rfc3986Uri) {
return self::new($uri->getRawPath());
}
if ($uri instanceof WhatwgUrl) {
return self::new($uri->getPath());
}
$path = $uri->getPath();
$authority = $uri->getAuthority();
return match (true) {
null === $authority, '' === $authority, '' === $path, '/' === $path[0] => new self($path),
default => new self('/'.$path),
};
}
public function value(): string
{
return Encoder::encodePath($this->path);
}
public function equals(mixed $value): bool
{
if (!$value instanceof Stringable && !is_string($value)) {
return false;
}
if (!$value instanceof UriComponentInterface) {
$value = self::tryNew($value);
if (null === $value) {
return false;
}
}
return $value->getUriComponent() === $this->getUriComponent();
}
public function decoded(): string
{
return $this->path;
}
public function normalize(): self
{
return new self((string) Encoder::normalizePath($this->withoutDotSegments()));
}
public function isAbsolute(): bool
{
return self::SEPARATOR === ($this->path[0] ?? '');
}
public function hasTrailingSlash(): bool
{
return '' !== $this->path && self::SEPARATOR === substr($this->path, -1);
}
public function withoutDotSegments(): PathInterface
{
$current = $this->toString();
$new = UriString::removeDotSegments($current);
if ($current === $new) {
return $this;
}
return new self($new);
}
/**
* Returns an instance with a trailing slash.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the path component with a trailing slash
*/
public function withTrailingSlash(): PathInterface
{
return $this->hasTrailingSlash() ? $this : new self($this->toString().self::SEPARATOR);
}
public function withoutTrailingSlash(): PathInterface
{
return !$this->hasTrailingSlash() ? $this : new self(substr($this->toString(), 0, -1));
}
public function withLeadingSlash(): PathInterface
{
return $this->isAbsolute() ? $this : new self(self::SEPARATOR.$this->toString());
}
public function withoutLeadingSlash(): PathInterface
{
return !$this->isAbsolute() ? $this : new self(substr($this->toString(), 1));
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see HierarchicalPath::new()
*
* @codeCoverageIgnore
*
* Returns a new instance from a string or a stringable object.
*/
#[Deprecated(message:'use League\Uri\Components\HierarchicalPath::new() instead', since:'league/uri-components:7.0.0')]
public static function createFromString(Stringable|string|int $path): self
{
return self::new((string) $path);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see HierarchicalPath::fromUri()
*
* @codeCoverageIgnore
*
* Create a new instance from a URI object.
*/
#[Deprecated(message:'use League\Uri\Components\HierarchicalPath::fromUri() instead', since:'league/uri-components:7.0.0')]
public static function createFromUri(Psr7UriInterface|UriInterface $uri): self
{
return self::fromUri($uri);
}
}
@@ -0,0 +1,201 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Components;
use Deprecated;
use League\Uri\Contracts\AuthorityInterface;
use League\Uri\Contracts\PortInterface;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Contracts\UriException;
use League\Uri\Contracts\UriInterface;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\UriScheme;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use Stringable;
use Uri\Rfc3986\Uri as Rfc3986Uri;
use Uri\WhatWg\Url as WhatWgUrl;
use function filter_var;
use function is_string;
use const FILTER_VALIDATE_INT;
final class Port extends Component implements PortInterface
{
private readonly ?int $port;
private ?array $cachedDefaultSchemes = null;
/**
* New instance.
*/
private function __construct(Stringable|string|int|null $port = null)
{
$this->port = $this->validate($port);
}
public static function new(Stringable|string|int|null $value = null): self
{
return new self($value);
}
/**
* Create a new instance from a string.or a stringable structure or returns null on failure.
*/
public static function tryNew(Stringable|string|null $uri = null): ?self
{
try {
return self::new($uri);
} catch (UriException) {
return null;
}
}
/**
* Create a new instance from a URI object.
*/
public static function fromUri(WhatWgUrl|Rfc3986Uri|Stringable|string $uri): self
{
return new self(self::filterUri($uri)->getPort());
}
/**
* Create a new instance from an Authority object.
*/
public static function fromAuthority(Stringable|string $authority): self
{
return match (true) {
$authority instanceof AuthorityInterface => new self($authority->getPort()),
default => new self(Authority::new($authority)->getPort()),
};
}
/**
* Validate a port.
*
* @throws SyntaxError if the port is invalid
*/
private function validate(Stringable|int|string|null $port): ?int
{
$port = self::filterComponent($port);
if (null === $port) {
return null;
}
$fport = filter_var($port, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0]]);
if (false !== $fport) {
return $fport;
}
throw new SyntaxError('Expected port to be a positive integer or 0; received '.$port.'.');
}
public function value(): ?string
{
return match (null) {
$this->port => $this->port,
default => (string) $this->port,
};
}
public function equals(mixed $value): bool
{
if (!$value instanceof Stringable && !is_string($value) && null !== $value) {
return false;
}
if (!$value instanceof UriComponentInterface) {
$value = self::tryNew($value);
if (null === $value) {
return false;
}
}
return $value->getUriComponent() === $this->getUriComponent();
}
public function getUriComponent(): string
{
return match (null) {
$this->port => '',
default => ':'.$this->value(),
};
}
public function toInt(): ?int
{
return $this->port;
}
public function defaultScheme(): ?Scheme
{
return $this->defaultSchemes()[0] ?? null;
}
/**
* @return list<Scheme>
*/
public function defaultSchemes(): array
{
return $this->cachedDefaultSchemes ??= array_map(
fn (UriScheme $schemePort): Scheme => Scheme::new($schemePort->value),
UriScheme::fromPort($this->port)
);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Port::fromUri()
*
* @codeCoverageIgnore
*
* Create a new instance from a URI object.
*/
#[Deprecated(message:'use League\Uri\Components\Port::fromUri() instead', since:'league/uri-components:7.0.0')]
public static function createFromUri(Psr7UriInterface|UriInterface $uri): self
{
return self::fromUri($uri);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Port::fromAuthority()
*
* @codeCoverageIgnore
*
* Create a new instance from an Authority object.
*/
#[Deprecated(message:'use League\Uri\Components\Port::fromAuthority() instead', since:'league/uri-components:7.0.0')]
public static function createFromAuthority(AuthorityInterface|Stringable|string $authority): self
{
return self::fromAuthority($authority);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Port::new()
*
* @codeCoverageIgnore
*/
#[Deprecated(message:'use League\Uri\Components\Port::new() instead', since:'league/uri-components:7.0.0')]
public static function fromInt(int $port): self
{
return self::new($port);
}
}
@@ -0,0 +1,868 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Components;
use Deprecated;
use Iterator;
use League\Uri\Contracts\QueryInterface;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Contracts\UriException;
use League\Uri\Contracts\UriInterface;
use League\Uri\Encoder;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\KeyValuePair\Converter;
use League\Uri\QueryString;
use League\Uri\UriString;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use Stringable;
use Traversable;
use Uri\Rfc3986\Uri as Rfc3986Uri;
use Uri\WhatWg\Url as WhatWgUrl;
use ValueError;
use function array_column;
use function array_count_values;
use function array_filter;
use function array_flip;
use function array_intersect;
use function array_is_list;
use function array_map;
use function array_merge;
use function count;
use function get_object_vars;
use function http_build_query;
use function implode;
use function in_array;
use function is_int;
use function is_object;
use function is_string;
use function preg_match;
use function preg_quote;
use function preg_replace;
use const JSON_PRESERVE_ZERO_FRACTION;
use const PREG_SPLIT_NO_EMPTY;
final class Query extends Component implements QueryInterface
{
private const REGEXP_NON_ASCII_PATTERN = '/[^\x20-\x7f]/';
/** @var array<int, array{0:string, 1:string|null}> */
private readonly array $pairs;
/** @var non-empty-string */
private readonly string $separator;
private readonly array $parameters;
/**
* Returns a new instance.
*/
private function __construct(Stringable|string|null $query, ?Converter $converter = null)
{
$converter ??= Converter::fromRFC3986();
$this->pairs = QueryString::parseFromValue($query, $converter);
$this->parameters = QueryString::extractFromValue($query, $converter);
$this->separator = $converter->separator();
}
public static function new(Stringable|string|null $value = null): self
{
return self::fromRFC3986($value);
}
/**
* Create a new instance from a string.or a stringable structure or returns null on failure.
*/
public static function tryNew(Stringable|string|null $uri = null): ?self
{
try {
return self::new($uri);
} catch (UriException) {
return null;
}
}
/**
* Returns a new instance from the input of http_build_query.
*
* @param non-empty-string $separator
*/
public static function fromVariable(object|array $parameters, string $separator = '&', string $prefix = ''): self
{
$params = is_object($parameters) ? get_object_vars($parameters) : $parameters;
$data = [];
foreach ($params as $name => $value) {
$data[$prefix.$name] = $value;
}
return new self(http_build_query(data: $data, arg_separator: $separator), Converter::fromRFC1738($separator));
}
/**
* Returns a new instance from the result of QueryString::parse.
*
* @param iterable<int, array{0:string, 1:string|null}> $pairs
* @param non-empty-string $separator
*/
public static function fromPairs(iterable $pairs, string $separator = '&', string $prefix = ''): self
{
$data = [];
foreach ($pairs as $pair) {
if (!is_array($pair) || !array_is_list($pair) || 2 !== count($pair)) {
throw new SyntaxError('A pair must be a sequential array starting at `0` and containing two elements.');
}
$data[] = [$prefix.$pair[0], $pair[1]];
}
$converter = Converter::fromRFC3986($separator);
return new self(QueryString::buildFromPairs($data, $converter), $converter);
}
/**
* Create a new instance from a URI object.
*/
public static function fromUri(WhatWgUrl|Rfc3986Uri|Stringable|string $uri): self
{
$uri = self::filterUri($uri);
return match (true) {
$uri instanceof Rfc3986Uri => new self($uri->getRawQuery()),
$uri instanceof Psr7UriInterface => new self(UriString::parse($uri)['query']),
default => new self($uri->getQuery()),
};
}
/**
* Returns a new instance.
*
* @param non-empty-string $separator
*/
public static function fromRFC3986(Stringable|string|null $query = null, string $separator = '&'): self
{
return new self($query, Converter::fromRFC3986($separator));
}
/**
* Returns a new instance.
*
* @param non-empty-string $separator
*/
public static function fromRFC1738(Stringable|string|null $query = null, string $separator = '&'): self
{
return new self($query, Converter::fromRFC1738($separator));
}
/**
* Returns a new instance.
*
* @param non-empty-string $separator
*/
public static function fromFormData(Stringable|string|null $query = null, string $separator = '&'): self
{
return new self($query, Converter::fromFormData($separator));
}
public function getSeparator(): string
{
return $this->separator;
}
public function toRFC3986(): ?string
{
return QueryString::buildFromPairs($this->pairs, Converter::fromRFC3986($this->separator));
}
public function toRFC1738(): ?string
{
return QueryString::buildFromPairs($this->pairs, Converter::fromRFC1738($this->separator));
}
public function toFormData(): ?string
{
return QueryString::buildFromPairs($this->pairs, Converter::fromFormData($this->separator));
}
public function decoded(): ?string
{
return Converter::new($this->separator)->toValue($this);
}
public function normalize(): self
{
return self::new(Encoder::normalizeQuery($this->value()));
}
public function value(): ?string
{
return $this->toRFC3986();
}
public function getUriComponent(): string
{
return match ([]) {
$this->pairs => '',
default => '?'.$this->value(),
};
}
public function jsonSerialize(): ?string
{
return $this->toFormData();
}
public function count(): int
{
return count($this->pairs);
}
public function getIterator(): Iterator
{
yield from $this->pairs;
}
public function pairs(): iterable
{
foreach ($this->pairs as $pair) {
yield $pair[0] => $pair[1];
}
}
public function has(string ...$keys): bool
{
foreach ($keys as $key) {
if (!isset(array_flip(array_column($this->pairs, 0))[$key])) {
return false;
}
}
return [] !== $keys;
}
public function hasPair(string $key, ?string $value): bool
{
return in_array([$key, $value], $this->pairs, true);
}
public function get(string $key): ?string
{
foreach ($this->pairs as $pair) {
if ($key === $pair[0]) {
return $pair[1];
}
}
return null;
}
public function getAll(string $key): array
{
return array_column(array_filter($this->pairs, fn (array $pair): bool => $key === $pair[0]), 1);
}
public function equals(mixed $value): bool
{
if (!$value instanceof Stringable && !is_string($value) && null !== $value) {
return false;
}
if (!$value instanceof UriComponentInterface) {
$value = self::tryNew($value);
if (null === $value) {
return false;
}
}
return $value->getUriComponent() === $this->getUriComponent();
}
public function parameters(): array
{
return $this->parameters;
}
public function parameter(string $name): mixed
{
return $this->parameters[$name] ?? null;
}
public function hasParameter(string ...$names): bool
{
foreach ($names as $name) {
if (!isset($this->parameters[$name])) {
return false;
}
}
return [] !== $names;
}
public function mergeParameters(object|array $parameter, string $prefix = ''): self
{
$params = is_object($parameter) ? get_object_vars($parameter) : $parameter;
$data = [];
foreach ($params as $name => $value) {
$data[$prefix.$name] = $value;
}
return in_array($data, [$this->parameters, []], true) ? $this : new self(
http_build_query(data: array_merge($this->parameters, $data), arg_separator: $this->separator),
Converter::fromRFC1738($this->separator)
);
}
public function replaceParameter(string $name, mixed $parameter): self
{
$this->hasParameter($name) || throw new ValueError('The specified name does not exist');
if ($parameter === $this->parameters[$name]) {
return $this;
}
$parameters = $this->parameters;
$parameters[$name] = $parameter;
return new self(http_build_query(data: $parameters, arg_separator: $this->separator), Converter::fromRFC1738($this->separator));
}
public function withSeparator(string $separator): self
{
return match ($separator) {
$this->separator => $this,
'' => throw new SyntaxError('The separator character cannot be the empty string.'),
default => self::fromPairs($this->pairs, $separator),
};
}
public function sort(): self
{
$codepoints = fn (?string $str): string => in_array($str, ['', null], true) ? '' : implode('.', array_map(
mb_ord(...), /* @phpstan-ignore-line */
(array) preg_split(pattern:'//u', subject: $str, flags: PREG_SPLIT_NO_EMPTY)
));
$compare = fn (string $name1, string $name2): int => match (1) {
preg_match(self::REGEXP_NON_ASCII_PATTERN, $name1.$name2) => strcmp($codepoints($name1), $codepoints($name2)),
default => strcmp($name1, $name2),
};
$parameters = array_reduce($this->pairs, function (array $carry, array $pair) {
$carry[$pair[0]] ??= [];
$carry[$pair[0]][] = $pair[1];
return $carry;
}, []);
uksort($parameters, $compare);
$pairs = [];
foreach ($parameters as $key => $values) {
$pairs = [...$pairs, ...array_map(fn ($value) => [$key, $value], $values)];
}
return match ($this->pairs) {
$pairs => $this,
default => self::fromPairs($pairs),
};
}
public function withoutDuplicates(): self
{
if (count($this->pairs) === count(array_count_values(array_column($this->pairs, 0)))) {
return $this;
}
$pairs = array_reduce($this->pairs, $this->removeDuplicates(...), []);
if ($pairs === $this->pairs) {
return $this;
}
return self::fromPairs($pairs, $this->separator);
}
/**
* @template TInitial
*
* @param callable(TInitial|null, array{0:array-key, 1:mixed}, array-key=): TInitial $callback
* @param TInitial|null $initial
*
* @return TInitial|null
*/
public function reduce(callable $callback, mixed $initial = null): mixed
{
foreach ($this->pairs as $offset => $pair) {
$initial = $callback($initial, $pair, $offset);
}
return $initial;
}
/**
* Adds a query pair only if it is not already present in a given array.
*/
private function removeDuplicates(array $pairs, array $pair): array
{
return match (true) {
in_array($pair, $pairs, true) => $pairs,
default => [...$pairs, $pair],
};
}
public function withoutEmptyPairs(): self
{
$pairs = array_filter($this->pairs, $this->filterEmptyPair(...));
return match ($this->pairs) {
$pairs => $this,
default => self::fromPairs($pairs),
};
}
/**
* Empty Pair filtering.
*/
private function filterEmptyPair(array $pair): bool
{
return '' !== $pair[0] && null !== $pair[1] && '' !== $pair[1];
}
public function withoutNumericIndices(): self
{
$pairs = array_map($this->encodeNumericIndices(...), $this->pairs);
return match ($this->pairs) {
$pairs => $this,
default => self::fromPairs($pairs, $this->separator),
};
}
/**
* Remove numeric indices from pairs.
*
* @param array{0:string, 1:string|null} $pair
*
* @return array{0:string, 1:string|null}
*/
private function encodeNumericIndices(array $pair): array
{
static $regexp = ',\[\d+],';
$pair[0] = (string) preg_replace($regexp, '[]', $pair[0]);
return $pair;
}
public function withPair(string $key, Stringable|string|int|float|bool|null $value): QueryInterface
{
$pairs = $this->addPair($this->pairs, [$key, $this->filterPair($value)]);
return match ($this->pairs) {
$pairs => $this,
default => self::fromPairs($pairs, $this->separator),
};
}
/**
* Add a new pair to the query key/value list.
*
* If there are any key/value pair whose kay is kay, in the list,
* set the value of the first such key/value pair to value and remove the others.
* Otherwise, append a new key/value pair whose key is key and value is value, to the list.
*/
private function addPair(array $list, array $pair): array
{
$found = false;
$reducer = static function (array $pairs, array $srcPair) use ($pair, &$found): array {
if ($pair[0] !== $srcPair[0]) {
$pairs[] = $srcPair;
return $pairs;
}
if (!$found) {
$pairs[] = $pair;
$found = true;
return $pairs;
}
return $pairs;
};
$pairs = array_reduce($list, $reducer, []);
if (!$found) {
$pairs[] = $pair;
}
return $pairs;
}
public function merge(Stringable|string|null $query): QueryInterface
{
$pairs = $this->pairs;
foreach (QueryString::parse(self::filterComponent($query), $this->separator) as $pair) {
$pairs = $this->addPair($pairs, $pair);
}
return match ($this->pairs) {
$pairs => $this,
default => self::fromPairs($pairs, $this->separator),
};
}
/**
* Validate the given pair.
*
* To be valid, the pair must be the null value, a scalar or a collection of scalar and null values.
*/
private function filterPair(Stringable|string|int|float|bool|null $value): ?string
{
return match (true) {
$value instanceof UriComponentInterface => $value->value(),
null === $value => null,
true === $value => 'true',
false === $value => 'false',
is_float($value) => (string) json_encode($value, JSON_PRESERVE_ZERO_FRACTION),
default => (string) $value,
};
}
public function withoutPairByKey(string ...$keys): QueryInterface
{
if ([] === $keys) {
return $this;
}
$keysToRemove = array_intersect($keys, array_column($this->pairs, 0));
return match ([]) {
$keysToRemove => $this,
default => self::fromPairs(
array_filter($this->pairs, static fn (array $pair): bool => !in_array($pair[0], $keysToRemove, true)),
$this->separator
),
};
}
public function withoutPairByValue(Stringable|string|int|float|bool|null ...$values): self
{
if ([] === $values) {
return $this;
}
$values = array_map($this->filterPair(...), $values);
$newPairs = array_filter($this->pairs, fn (array $pair) => !in_array($pair[1], $values, true));
return match ($this->pairs) {
$newPairs => $this,
default => self::fromPairs($newPairs, $this->separator),
};
}
public function withoutPairByKeyValue(string $key, Stringable|string|int|float|bool|null $value): self
{
$pair = [$key, $this->filterPair($value)];
$newPairs = array_filter($this->pairs, fn (array $currentPair) => $currentPair !== $pair);
return match ($this->pairs) {
$newPairs => $this,
default => self::fromPairs($newPairs, $this->separator),
};
}
public function appendTo(string $key, Stringable|string|int|float|bool|null $value): QueryInterface
{
return self::fromPairs([...$this->pairs, [$key, $this->filterPair($value)]], $this->separator);
}
public function append(Stringable|string|null $query): QueryInterface
{
if ($query instanceof UriComponentInterface) {
$query = $query->value();
}
$pairs = array_merge($this->pairs, QueryString::parse($query, $this->separator));
return match ($this->pairs) {
$pairs => $this,
default => self::fromPairs(array_filter($pairs, $this->filterEmptyValue(...)), $this->separator),
};
}
public function prepend(Stringable|string|null $query): QueryInterface
{
return Query::new($query)->append($this);
}
/**
* Replace a pair based on its offset.
*/
public function replace(int $offset, string $key, Stringable|string|int|float|bool|null $value): QueryInterface
{
$index = $offset < 0 ? count($this->pairs) + $offset : $offset;
$pair = $this->pairs[$index] ?? [];
[] !== $pair || throw new ValueError('The given offset "'.$offset.'" does not exist');
$newPair = [$key, $this->filterPair($value)];
if ($pair === $newPair) {
return $this;
}
$newPairs = $this->pairs;
$newPairs[$index] = $newPair;
return self::fromPairs($newPairs, $this->separator);
}
/**
* Returns the offset of the pair based on its key and its nth occurrence.
*
* negative occurrences are supported
*/
public function indexOf(string $key, int $nth = 0): ?int
{
if ([] === $this->pairs) {
return null;
}
if ($nth < 0) {
$matchCount = 0;
for ($offset = count($this->pairs) - 1; $offset >= 0; --$offset) {
if ($this->pairs[$offset][0] === $key) {
if (++$matchCount === -$nth) {
return $offset;
}
}
}
return null;
}
$matchCount = 0;
foreach ($this->pairs as $offset => $pair) {
if ($pair[0] === $key) {
if ($nth === $matchCount) {
return $offset;
}
++$matchCount;
}
}
return null;
}
/**
* Empty Pair filtering.
*/
private function filterEmptyValue(array $pair): bool
{
return '' !== $pair[0] || null !== $pair[1];
}
public function withoutParameters(string ...$names): QueryInterface
{
if ([] === $names) {
return $this;
}
$mapper = static fn (string $offset): string => preg_quote($offset, ',').'(\[.*\].*)?';
$regexp = ',^('.implode('|', array_map($mapper, $names)).')?$,';
$filter = fn (array $pair): bool => 1 !== preg_match($regexp, $pair[0]);
$pairs = array_filter($this->pairs, $filter);
return match ($this->pairs) {
$pairs => $this,
default => self::fromPairs($pairs, $this->separator),
};
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Query::fromParameters()
*
* @codeCoverageIgnore
*
* @param non-empty-string $separator
*
* Returns a new instance from the result of PHP's parse_str.
*
* @deprecated Since version 7.0.0
*/
#[Deprecated(message:'use League\Uri\Components\Query::fromVariables() instead', since:'league/uri-components:7.0.0')]
public static function createFromParams(iterable|object $params, string $separator = '&'): self
{
return self::fromParameters($params, $separator);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Query::fromPairs()
*
* @codeCoverageIgnore
*
*
* Returns a new instance from the result of QueryString::parse.
*
* @param iterable<int, array{0:string, 1:string|null}> $pairs
* @param non-empty-string $separator
*/
#[Deprecated(message:'use League\Uri\Components\Query::fromPairs() instead', since:'league/uri-components:7.0.0')]
public static function createFromPairs(iterable $pairs, string $separator = '&'): self
{
return self::fromPairs($pairs, $separator);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Query::fromUri()
*
* @codeCoverageIgnore
*
* Create a new instance from a URI object.
*/
#[Deprecated(message:'use League\Uri\Components\Query::fromUri() instead', since:'league/uri-components:7.0.0')]
public static function createFromUri(Psr7UriInterface|UriInterface $uri): self
{
return self::fromUri($uri);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Query::fromRFC3986()
*
* @codeCoverageIgnore
*
* Returns a new instance.
*
* @param non-empty-string $separator
*/
#[Deprecated(message:'use League\Uri\Components\Query::fromRFC3986() instead', since:'league/uri-components:7.0.0')]
public static function createFromRFC3986(Stringable|string|int|null $query = '', string $separator = '&'): self
{
if (null !== $query) {
$query = (string) $query;
}
return self::fromRFC3986($query, $separator);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Query::fromRFC1738()
*
* @codeCoverageIgnore
*
* Returns a new instance.
*
* @param non-empty-string $separator
*/
#[Deprecated(message:'use League\Uri\Components\Query::fromRFC1738() instead', since:'league/uri-components:7.0.0')]
public static function createFromRFC1738(Stringable|string|int|null $query = '', string $separator = '&'): self
{
if (is_int($query)) {
$query = (string) $query;
}
return self::fromRFC1738($query, $separator);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Query::parameters()
* @see Query::parameter()
*
* @codeCoverageIgnore
*
* Returns the query as a collection of PHP variables or a single variable assign to a specific key
*/
#[Deprecated(message:'use League\Uri\Components\Query::parameter() or League\Uri\Components\Query::parameters() instead', since:'league/uri-components:7.0.0')]
public function params(?string $key = null): mixed
{
return match (null) {
$key => $this->parameters(),
default => $this->parameter($key),
};
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Query::withoutParameters()
*
* @codeCoverageIgnore
*/
#[Deprecated(message:'use League\Uri\Components\Query::withoutParameters() instead', since:'league/uri-components:7.0.0')]
public function withoutParams(string ...$names): QueryInterface
{
return $this->withoutParameters(...$names);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.3.0
* @see Query::withoutPairByKey()
*
* @codeCoverageIgnore
*/
#[Deprecated(message:'use League\Uri\Components\Query::withoutPairByKey() instead', since:'league/uri-components:7.3.0')]
public function withoutPair(string ...$keys): QueryInterface
{
return $this->withoutPairByKey(...$keys);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @param non-empty-string $separator
*
* @see Query::fromVariable()
*
* @codeCoverageIgnore
* Returns a new instance from the result of PHP's parse_str.
*
* @deprecated Since version 7.0.0
*/
#[Deprecated(message:'use League\Uri\Components\Query::fromVariable() instead', since:'league/uri-components:7.0.0')]
public static function fromParameters(object|array $parameters, string $separator = '&'): self
{
if ($parameters instanceof QueryInterface) {
return self::fromPairs($parameters, $separator);
}
$parameters = match (true) {
$parameters instanceof Traversable => iterator_to_array($parameters),
default => $parameters,
};
$query = match ([]) {
$parameters => null,
default => http_build_query(data: $parameters, arg_separator: $separator),
};
return new self($query, Converter::fromRFC1738($separator));
}
}
@@ -0,0 +1,209 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Components;
use Deprecated;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Contracts\UriInterface;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\SchemeType;
use League\Uri\UriScheme;
use League\Uri\UriString;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use Stringable;
use Throwable;
use Uri\Rfc3986\Uri as Rfc3986Uri;
use Uri\WhatWg\Url as WhatWgUrl;
use function in_array;
use function is_string;
use function preg_match;
use function sprintf;
use function strtolower;
final class Scheme extends Component
{
private const REGEXP_SCHEME = ',^[a-z]([-a-z0-9+.]+)?$,i';
private readonly ?string $scheme;
private readonly ?UriScheme $uriScheme;
private function __construct(Stringable|string|null $scheme)
{
$this->scheme = $this->validate($scheme);
$this->uriScheme = UriScheme::tryFrom((string) $this->scheme);
}
public function isWebsocket(): bool
{
return in_array($this->scheme, ['ws', 'wss'], true);
}
public function isHttp(): bool
{
return in_array($this->scheme, ['http', 'https'], true);
}
public function isSsl(): bool
{
return in_array($this->scheme, ['https', 'wss'], true);
}
public function isSpecial(): bool
{
return $this->isWhatWgSpecial() || in_array($this->scheme, ['data', 'file'], true);
}
public function isWhatWgSpecial(): bool
{
return $this->uriScheme?->isWhatWgSpecial() ?? false;
}
public function defaultPort(): Port
{
return Port::new($this->uriScheme?->port());
}
public function hasDefaultPort(): bool
{
static $emptyPort = null;
$emptyPort ??= Port::new();
return !$emptyPort->equals($this->defaultPort());
}
public function type(): SchemeType
{
return $this->uriScheme?->type() ?? SchemeType::Unknown;
}
/**
* Validate a scheme.
*
* @throws SyntaxError if the scheme is invalid
*/
private function validate(Stringable|string|null $scheme): ?string
{
$scheme = self::filterComponent($scheme);
if (null === $scheme) {
return null;
}
$fScheme = strtolower($scheme);
/** @var array<string> $inMemoryCache */
static $inMemoryCache = [];
if (isset($inMemoryCache[$fScheme])) {
return $fScheme;
}
if (1 !== preg_match(self::REGEXP_SCHEME, $fScheme)) {
throw new SyntaxError(sprintf("The scheme '%s' is invalid.", $scheme));
}
if (100 < count($inMemoryCache)) {
unset($inMemoryCache[array_key_first($inMemoryCache)]);
}
$inMemoryCache[$fScheme] = 1;
return $fScheme;
}
public static function new(Stringable|string|null $value = null): self
{
return new self($value);
}
/**
* Create a new instance from a string.or a stringable structure or returns null on failure.
*/
public static function tryNew(Stringable|string|null $uri = null): ?self
{
try {
return self::new($uri);
} catch (Throwable) {
return null;
}
}
/**
* Create a new instance from a URI object.
*/
public static function fromUri(WhatWgUrl|Rfc3986Uri|Stringable|string $uri): self
{
$uri = self::filterUri($uri);
return new self(
$uri instanceof Psr7UriInterface
? UriString::parse($uri)['scheme']
: $uri->getScheme()
);
}
public function value(): ?string
{
return $this->scheme;
}
public function getUriComponent(): string
{
return $this->value().(null === $this->scheme ? '' : ':');
}
public function equals(mixed $value): bool
{
if (!$value instanceof Stringable && !is_string($value) && null !== $value) {
return false;
}
if (!$value instanceof UriComponentInterface) {
$value = self::tryNew($value);
if (null === $value) {
return false;
}
}
return $value->getUriComponent() === $this->getUriComponent();
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Scheme::new()
*
* @codeCoverageIgnore
*/
#[Deprecated(message:'use League\Uri\Components\Scheme::new() instead', since:'league/uri-components:7.0.0')]
public static function createFromString(Stringable|string $scheme): self
{
return self::new($scheme);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see Scheme::fromUri()
*
* @codeCoverageIgnore
*
* Create a new instance from a URI object.
*/
#[Deprecated(message:'use League\Uri\Components\Scheme::fromUri() instead', since:'league/uri-components:7.0.0')]
public static function createFromUri(Psr7UriInterface|UriInterface $uri): self
{
return self::fromUri($uri);
}
}
@@ -0,0 +1,553 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Components;
use ArgumentCountError;
use Closure;
use Countable;
use Deprecated;
use Iterator;
use IteratorAggregate;
use League\Uri\Contracts\QueryInterface;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Contracts\UriException;
use League\Uri\Contracts\UriInterface;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\KeyValuePair\Converter;
use League\Uri\QueryString;
use League\Uri\Uri;
use League\Uri\UriString;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use Stringable;
use Uri\Rfc3986\Uri as Rfc3986Uri;
use Uri\WhatWg\Url as WhatWgUrl;
use function array_is_list;
use function array_key_exists;
use function array_keys;
use function array_map;
use function count;
use function func_get_arg;
use function func_num_args;
use function get_object_vars;
use function is_array;
use function is_bool;
use function is_iterable;
use function is_object;
use function is_scalar;
use function iterator_to_array;
use function json_encode;
use function spl_object_hash;
use function str_starts_with;
use const JSON_PRESERVE_ZERO_FRACTION;
/**
* @see https://url.spec.whatwg.org/#interface-urlsearchparams
*
* @implements IteratorAggregate<array{0:string, 1:string}>
*/
final class URLSearchParams implements Countable, IteratorAggregate, UriComponentInterface
{
private QueryInterface $pairs;
/**
* New instance.
*
* A string, which will be parsed from application/x-www-form-urlencoded format. A leading '?' character is ignored.
* A literal sequence of name-value string pairs, or any object with an iterator that produces a sequence of string pairs.
* A record of string keys and string values. Note that nesting is not supported.
*/
public function __construct(object|array|string|null $init = '')
{
$pairs = self::filterPairs(match (true) {
$init instanceof self,
$init instanceof QueryInterface => $init,
$init instanceof UriComponentInterface => self::parsePairs($init->value()),
is_iterable($init) => self::formatIterable($init),
$init instanceof Stringable, !is_object($init) => self::parsePairs(self::formatQuery($init)),
default => self::yieldPairs($init),
});
$this->pairs = Query::fromPairs($pairs);
}
/**
* @return array<int, array{0:string, 1:string|null}>
*/
private static function parsePairs(string|null $query): array
{
return QueryString::parseFromValue($query, Converter::fromFormData());
}
/**
* @return iterable<array{0:string, 1:string}>
*/
private static function formatIterable(iterable $iterable): iterable
{
if (!is_array($iterable)) {
$iterable = iterator_to_array($iterable);
}
return match (true) {
array_is_list($iterable) => $iterable,
default => self::yieldPairs($iterable)
};
}
/**
* Generates an Iterator containing pairs as items from an object or array.
*
* If an iterable is given, foreach will loop over the iterable structure
* If an object is give, foreach will loop over the object public properties if they are defined
*
* @param object|iterable<array-key, Stringable|string|float|int|bool|null> $associative
*
* @return Iterator<int, array{0:string, 1:string}>
*/
private static function yieldPairs(object|array $associative): Iterator
{
foreach ($associative as $key => $value) { /* @phpstan-ignore-line */
yield [self::uvString($key), self::uvString($value)];
}
}
/**
* @return Iterator<int, array{0:string, 1:string}>
*/
private static function filterPairs(iterable $pairs): iterable
{
$filter = static fn ($pair): ?array => match (true) {
!is_array($pair),
[0, 1] !== array_keys($pair) => throw new SyntaxError('A pair must be a sequential array starting at `0` and containing two elements.'),
null !== $pair[1] => [self::uvString($pair[0]), self::uvString($pair[1])],
'' !== $pair[0] => [self::uvString($pair[0]), ''],
default => null,
};
foreach ($pairs as $pair) {
if (null !== ($filteredPair = $filter($pair))) {
yield $filteredPair;
}
}
}
private static function formatQuery(Stringable|string|null $query): string
{
return match (true) {
null === $query => '',
str_starts_with((string) $query, '?') => substr((string) $query, 1),
default => (string) $query,
};
}
/**
* Normalizes type to UVString.
*
* @see https://webidl.spec.whatwg.org/#idl-USVString
*/
private static function uvString(Stringable|string|float|int|bool|null $value): string
{
return match (true) {
null === $value => 'null',
false === $value => 'false',
true === $value => 'true',
is_float($value) => (string) json_encode($value, JSON_PRESERVE_ZERO_FRACTION),
default => (string) $value,
};
}
/**
* Returns a new instance from a string or a stringable object.
*
* The input will be parsed from application/x-www-form-urlencoded format.
* The leading '?' character if present is ignored.
*/
public static function new(Stringable|string|null $query = null): self
{
return new self(Query::fromFormData(self::formatQuery($query)));
}
/**
* Create a new instance from a string.or a stringable structure or returns null on failure.
*/
public static function tryNew(Stringable|string|null $uri = null): ?self
{
try {
return self::new($uri);
} catch (UriException) {
return null;
}
}
/**
* Returns a new instance from a literal sequence of name-value string pairs,
* or any object with an iterator that produces a sequence of string pairs.
*
* @param iterable<int, array{0:string, 1:string|null}> $pairs
*/
public static function fromPairs(iterable $pairs): self
{
return new self(Query::fromPairs($pairs));
}
/**
* Returns a new instance from a record of string keys and string values.
*
* A record can be, an iterable or any object with scalar or null public properties. Nesting is not supported.
*
* @param object|iterable<array-key, Stringable|string|float|int|bool|null> $associative
*/
public static function fromAssociative(object|array $associative): self
{
return new self(Query::fromPairs(self::yieldPairs($associative)));
}
/**
* Returns a new instance from a URI.
*/
public static function fromUri(WhatWgUrl|Rfc3986Uri|Stringable|string $uri): self
{
$query = match (true) {
$uri instanceof Rfc3986Uri => $uri->getRawQuery(),
$uri instanceof WhatWgUrl, $uri instanceof UriInterface => $uri->getQuery(),
$uri instanceof Psr7UriInterface => UriString::parse($uri)['query'],
default => Uri::new($uri)->getQuery(),
};
return new self(Query::fromPairs(QueryString::parseFromValue($query, Converter::fromFormData())));
}
/**
* Returns a new instance from the input of PHP's http_build_query.
*/
public static function fromVariable(object|array $parameters): self
{
return self::fromPairs(self::parametersToPairs($parameters));
}
private static function parametersToPairs(array|object $data, string|int $prefix = '', array &$recursive = []): array
{
$yieldParameters = static fn (object|array $data): array => is_array($data) ? $data : get_object_vars($data);
$pairs = [];
foreach ($yieldParameters($data) as $name => $value) {
if (is_object($data)) {
$id = spl_object_hash($data);
if (!array_key_exists($id, $recursive)) {
$recursive[$id] = 1;
}
}
if (is_object($value)) {
$id = spl_object_hash($value);
if (array_key_exists($id, $recursive)) {
return [];
}
$recursive[$id] = 1;
}
if ('' !== $prefix) {
$name = $prefix.'['.$name.']';
}
$pairs = match (true) {
is_array($value),
is_object($value) => [...$pairs, ...self::parametersToPairs($value, $name, $recursive)],
is_scalar($value) => [...$pairs, [$name, self::uvString($value)]],
default => $pairs,
};
}
return $pairs;
}
public function value(): ?string
{
return $this->pairs->toFormData();
}
public function equals(mixed $value): bool
{
return $this->pairs->equals($value);
}
/**
* Returns a query string suitable for use in a URL.
*/
public function toString(): string
{
return $this->value() ?? '';
}
public function decoded(): string
{
return (string) Query::fromPairs($this->pairs)->decoded();
}
public function __toString(): string
{
return $this->toString();
}
public function jsonSerialize(): string
{
return $this->toString();
}
public function getUriComponent(): string
{
$value = $this->value() ?? '';
return match ('') {
$value => $value,
default => '?'.$value,
};
}
/**
* Returns an iterator allowing iteration through all keys contained in this object.
*
* @return iterable<string>
*/
public function keys(): iterable
{
foreach ($this->pairs as [$key, $__]) {
yield $key;
}
}
/**
* Returns an iterator allowing iteration through all values contained in this object.
*
* @return iterable<string>
*/
public function values(): iterable
{
foreach ($this->pairs as [$__, $value]) {
yield $value ?? '';
}
}
/**
* Tells whether the specified parameter is in the search parameters.
*
* The method requires at least one parameter as the pair name (string or null)
* and an optional second and last parameter as the pair value (Stringable|string|float|int|bool|null)
* <code>
* $params = new URLSearchParams('a=b&c);
* $params->has('c'); // return true
* $params->has('a', 'b'); // return true
* $params->has('a', 'c'); // return false
* </code>
*/
public function has(?string $name): bool
{
$name = self::uvString($name);
return match (func_num_args()) {
1 => $this->pairs->has($name),
2 => $this->pairs->hasPair($name, self::uvString(func_get_arg(1))), /* @phpstan-ignore-line */
default => throw new ArgumentCountError(__METHOD__.' requires at least one argument as the pair name and a second optional argument as the pair value.'),
};
}
/**
* Returns the first value associated to the given search parameter or null if none exists.
*/
public function get(?string $name): ?string
{
return match (true) {
$this->has($name) => $this->pairs->get(self::uvString($name)) ?? '',
default => null,
};
}
/**
* Returns all the values associated with a given search parameter as an array.
*
* @return array<string>
*/
public function getAll(?string $name): array
{
return array_map(
fn (?string $value): string => $value ?? '',
$this->pairs->getAll(self::uvString($name))
);
}
/**
* Tells whether the instance has some parameters.
*/
public function isNotEmpty(): bool
{
return ! $this->isEmpty();
}
/**
* Tells whether the instance has no parameters.
*/
public function isEmpty(): bool
{
return 0 === $this->size();
}
/**
* Returns the total number of distinct search parameter keys.
*/
public function uniqueKeyCount(): int
{
return count(
array_count_values(
array_column([...$this->pairs], 0)
)
);
}
/**
* Returns the total number of search parameter entries.
*/
public function size(): int
{
return count($this->pairs);
}
/**
* @see URLSearchParams::size()
*/
public function count(): int
{
return $this->size();
}
/**
* Allowing iteration through all key/value pairs contained in this object.
*
* The iterator returns key/value pairs in the same order as they appear in the query string.
* The key and value of each pair are string objects.
*/
public function entries(): Iterator
{
yield from $this->pairs;
}
/**
* @see URLSearchParams::entries()
*/
public function getIterator(): Iterator
{
return $this->entries();
}
/**
* Allows iteration through all values contained in this object via a callback function.
*
* @param Closure(string $value, string $key): void $callback
*/
public function each(Closure $callback): void
{
foreach ($this->pairs->pairs() as $key => $value) {
$callback($value ?? '', $key);
}
}
private function updateQuery(QueryInterface $query): void
{
if ($query->value() !== $this->pairs->value()) {
$this->pairs = $query;
}
}
/**
* Sets the value associated with a given search parameter to the given value.
*
* If there were several matching values, this method deletes the others.
* If the search parameter doesn't exist, this method creates it.
*/
public function set(?string $name, Stringable|string|float|int|bool|null $value): void
{
$this->updateQuery($this->pairs->withPair(self::uvString($name), self::uvString($value)));
}
/**
* Appends a specified key/value pair as a new search parameter.
*/
public function append(?string $name, Stringable|string|float|int|bool|null $value): void
{
$this->updateQuery($this->pairs->appendTo(self::uvString($name), self::uvString($value)));
}
/**
* Deletes specified parameters and their associated value(s) from the list of all search parameters.
*
* The method requires at least one parameter as the pair name (string or null)
* and an optional second and last parameter as the pair value (Stringable|string|float|int|bool|null)
* <code>
* $params = new URLSearchParams('a=b&c);
* $params->delete('c'); //delete all parameters with the key 'c'
* $params->delete('a', 'b') //delete all pairs with the key 'a' and the value 'b'
* </code>
*/
public function delete(?string $name): void
{
$name = self::uvString($name);
$this->updateQuery(match (func_num_args()) {
1 => $this->pairs->withoutPairByKey($name),
2 => $this->pairs->withoutPairByKeyValue($name, self::uvString(func_get_arg(1))), /* @phpstan-ignore-line */
default => throw new ArgumentCountError(__METHOD__.' requires at least one argument as the pair name and a second optional argument as the pair value.'),
});
}
/**
* Sorts all key/value pairs contained in this object in place and returns undefined.
*
* The sort order is according to Unicode code points of the keys. This method
* uses a stable sorting algorithm (i.e. the relative order between
* key/value pairs with equal keys will be preserved).
*/
public function sort(): void
{
$this->updateQuery($this->pairs->sort());
}
public function when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null): static
{
if (!is_bool($condition)) {
$condition = $condition($this);
}
return match (true) {
$condition => $onSuccess($this) ?? $this,
null !== $onFail => $onFail($this) ?? $this,
default => $this,
};
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.4.0
* @see URLSearchParams::fromVariable()
*
* @codeCoverageIgnore
*
*/
#[Deprecated(message:'use League\Uri\Components\URLSearchParams::fromVariable() instead', since:'league/uri-components:7.4.0')]
public static function fromParameters(object|array $parameters): self
{
return new self(Query::fromParameters($parameters));
}
}
@@ -0,0 +1,256 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Components;
use Deprecated;
use League\Uri\Contracts\AuthorityInterface;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Contracts\UriException;
use League\Uri\Contracts\UriInterface;
use League\Uri\Contracts\UserInfoInterface;
use League\Uri\Encoder;
use League\Uri\UriString;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use SensitiveParameter;
use Stringable;
use Uri\Rfc3986\Uri as Rfc3986Uri;
use Uri\WhatWg\Url as WhatWgUrl;
use function explode;
use function is_string;
final class UserInfo extends Component implements UserInfoInterface
{
private readonly ?string $username;
private readonly ?string $password;
/**
* New instance.
*/
public function __construct(
Stringable|string|null $username,
#[SensitiveParameter] Stringable|string|null $password = null,
) {
$this->username = $this->validateComponent($username);
$this->password = $this->validateComponent($password);
}
/**
* Create a new instance from a URI object.
*/
public static function fromUri(Rfc3986Uri|WhatWgUrl|Stringable|string $uri): self
{
$uri = self::filterUri($uri);
if ($uri instanceof Rfc3986Uri) {
return new self($uri->getRawUsername(), $uri->getRawPassword());
}
if ($uri instanceof WhatWgUrl || $uri instanceof UriInterface) {
return new self($uri->getUsername(), $uri->getPassword());
}
$components = UriString::parse($uri);
return new self($components['user'], $components['pass']);
}
/**
* Create a new instance from an Authority object.
*/
public static function fromAuthority(Stringable|string|null $authority): self
{
return match (true) {
$authority instanceof AuthorityInterface => self::new($authority->getUserInfo()),
default => self::new(Authority::new($authority)->getUserInfo()),
};
}
/**
* Create a new instance from a hash of parse_url parts.
*
* Create a new instance from a hash representation of the URI similar
* to PHP parse_url function result
*
* @param array{user? : ?string, pass? : ?string} $components
*/
public static function fromComponents(array $components): self
{
$components += ['user' => null, 'pass' => null];
return match (null) {
$components['user'] => new self(null),
default => new self($components['user'], $components['pass']),
};
}
/**
* Creates a new instance from an encoded string.
*/
public static function new(Stringable|string|null $value = null): self
{
if ($value instanceof UriComponentInterface) {
$value = $value->value();
}
if (null === $value) {
return new self(null);
}
$value = (string) $value;
[$user, $pass] = explode(':', $value, 2) + [1 => null];
return new self(Encoder::decodeAll($user), Encoder::decodeAll($pass));
}
/**
* Create a new instance from a string or a stringable structure or returns null on failure.
*/
public static function tryNew(Stringable|string|null $uri = null): ?self
{
try {
return self::new($uri);
} catch (UriException) {
return null;
}
}
public function value(): ?string
{
return match (true) {
null === $this->password => $this->getUsername(),
default => $this->getUsername().':'.$this->getPassword(),
};
}
public function equals(mixed $value): bool
{
if (!$value instanceof Stringable && !is_string($value) && null !== $value) {
return false;
}
if (!$value instanceof UriComponentInterface) {
$value = self::tryNew($value);
if (null === $value) {
return false;
}
}
return $value->getUriComponent() === $this->getUriComponent();
}
public function getUriComponent(): string
{
return $this->value().(null === $this->username ? '' : '@');
}
public function getUser(): ?string
{
return $this->username;
}
public function getPass(): ?string
{
return $this->password;
}
public function getUsername(): ?string
{
return Encoder::encodeUser($this->username);
}
public function getPassword(): ?string
{
return Encoder::encodePassword($this->password);
}
/**
* @return array{user: ?string, pass: ?string}
*/
public function components(): array
{
return [
'user' => $this->username,
'pass' => $this->password,
];
}
public function withUser(Stringable|string|null $username): self
{
$username = $this->validateComponent($username);
if ($this->username === $username) {
return $this;
}
return new self($username, $this->password);
}
public function withPass(#[SensitiveParameter] Stringable|string|null $password): self
{
$password = $this->validateComponent($password);
if ($password === $this->password) {
return $this;
}
return new self($this->username, $password);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see UserInfo::fromUri()
*
* @codeCoverageIgnore
*
* Create a new instance from a URI object.
*/
#[Deprecated(message:'use League\Uri\Components\UserInfo::fromUri() instead', since:'league/uri-components:7.0.0')]
public static function createFromUri(Rfc3986Uri|WhatWgUrl|Psr7UriInterface|UriInterface $uri): self
{
return self::fromUri($uri);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see UserInfo::fromAuthority()
*
* @codeCoverageIgnore
*
* Create a new instance from an Authority object.
*/
#[Deprecated(message:'use League\Uri\Components\UserInfo::fromAuthority() instead', since:'league/uri-components:7.0.0')]
public static function createFromAuthority(AuthorityInterface|Stringable|string $authority): self
{
return self::fromAuthority($authority);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @see UserInfo::new()
*
* @codeCoverageIgnore
*
* Creates a new instance from an encoded string.
*/
#[Deprecated(message:'use League\Uri\Components\UserInfo::new() instead', since:'league/uri-components:7.0.0')]
public static function createFromString(Stringable|string $userInfo): self
{
return self::new($userInfo);
}
}