Pandabot/vendor/league/uri-components/Modifier.php

810 lines
25 KiB
PHP
Executable file

<?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;
use JsonSerializable;
use League\Uri\Components\DataPath;
use League\Uri\Components\Domain;
use League\Uri\Components\HierarchicalPath;
use League\Uri\Components\Host;
use League\Uri\Components\Path;
use League\Uri\Components\Query;
use League\Uri\Contracts\PathInterface;
use League\Uri\Contracts\UriAccess;
use League\Uri\Contracts\UriInterface;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\Idna\Converter as IdnConverter;
use League\Uri\IPv4\Converter as IPv4Converter;
use League\Uri\KeyValuePair\Converter as KeyValuePairConverter;
use Psr\Http\Message\UriFactoryInterface;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use Stringable;
use function get_object_vars;
use function ltrim;
use function rtrim;
use function str_ends_with;
use function str_starts_with;
class Modifier implements Stringable, JsonSerializable, UriAccess
{
final public function __construct(protected readonly Psr7UriInterface|UriInterface $uri)
{
}
public static function from(Stringable|string $uri, ?UriFactoryInterface $uriFactory = null): static
{
return new static(match (true) {
$uri instanceof UriAccess => $uri->getUri(),
$uri instanceof Psr7UriInterface, $uri instanceof UriInterface => $uri,
$uriFactory instanceof UriFactoryInterface => $uriFactory->createUri((string) $uri),
default => Uri::new($uri),
});
}
public function getUri(): Psr7UriInterface|UriInterface
{
return $this->uri;
}
public function getUriString(): string
{
return $this->uri->__toString();
}
public function jsonSerialize(): string
{
return $this->uri->__toString();
}
public function __toString(): string
{
return $this->uri->__toString();
}
/*********************************
* Query modifier methods
*********************************/
/**
* Change the encoding of the query.
*
*/
public function encodeQuery(KeyValuePairConverter|int $to, KeyValuePairConverter|int|null $from = null): static
{
$to = match (true) {
!$to instanceof KeyValuePairConverter => KeyValuePairConverter::fromEncodingType($to),
default => $to,
};
$from = match (true) {
null === $from => KeyValuePairConverter::fromRFC3986(),
!$from instanceof KeyValuePairConverter => KeyValuePairConverter::fromEncodingType($from),
default => $from,
};
if ($to == $from) {
return $this;
}
$originalQuery = $this->uri->getQuery();
$query = QueryString::buildFromPairs(QueryString::parseFromValue($originalQuery, $from), $to);
return match (true) {
null === $query,
'' === $query,
$originalQuery === $query => $this,
default => new static($this->uri->withQuery($query)),
};
}
/**
* Sort the URI query by keys.
*/
public function sortQuery(): static
{
return new static($this->uri->withQuery(
static::normalizeComponent(
Query::fromUri($this->uri)->sort()->value(),
$this->uri
)
));
}
/**
* Add the new query data to the existing URI query.
*/
public function appendQuery(Stringable|string|null $query): static
{
return new static($this->uri->withQuery(
static::normalizeComponent(
Query::fromUri($this->uri)->append($query)->value(),
$this->uri
)
));
}
/**
* Merge query paris with the existing URI query.
*
* @param iterable<int, array{0:string, 1:string|null}> $pairs
*/
public function appendQueryPairs(iterable $pairs): self
{
return $this->appendQuery(Query::fromPairs($pairs)->value());
}
/**
* Append PHP query parameters to the existing URI query.
*/
public function appendQueryParameters(object|array $parameters): self
{
return $this->appendQuery(Query::fromVariable($parameters)->value());
}
/**
* Merge a new query with the existing URI query.
*/
public function mergeQuery(Stringable|string|null $query): static
{
return new static($this->uri->withQuery(
static::normalizeComponent(
Query::fromUri($this->uri)->merge($query)->value(),
$this->uri
)
));
}
/**
* Merge query paris with the existing URI query.
*
* @param iterable<int, array{0:string, 1:string|null}> $pairs
*/
public function mergeQueryPairs(iterable $pairs): self
{
$currentPairs = [...Query::fromUri($this->uri)->pairs()];
$pairs = [...$pairs];
return match (true) {
[] === $pairs,
$currentPairs === $pairs => $this,
default => $this->mergeQuery(Query::fromPairs($pairs)->value()),
};
}
/**
* Merge PHP query parameters with the existing URI query.
*/
public function mergeQueryParameters(object|array $parameters): self
{
$parameters = match (true) {
is_object($parameters) => get_object_vars($parameters),
default => $parameters,
};
$currentParameters = Query::fromUri($this->uri)->parameters();
return match (true) {
[] === $parameters,
$currentParameters === $parameters => $this,
default => new static($this->uri->withQuery(
self::normalizeComponent(
Query::fromVariable([...$currentParameters, ...$parameters])->value(),
$this->uri
)
)),
};
}
/**
* Remove query data according to their key name.
*/
public function removeQueryPairsByKey(string ...$keys): static
{
$query = Query::fromUri($this->uri);
$newQuery = $query->withoutPairByKey(...$keys)->value();
return match ($query->value()) {
$newQuery => $this,
default => new static($this->uri->withQuery(static::normalizeComponent($newQuery, $this->uri))),
};
}
/**
* Remove query pair according to their value.
*/
public function removeQueryPairsByValue(Stringable|string|int|float|bool|null ...$values): static
{
$query = Query::fromUri($this->uri);
$newQuery = $query->withoutPairByValue(...$values)->value();
return match ($query->value()) {
$newQuery => $this,
default => new static($this->uri->withQuery(static::normalizeComponent($newQuery, $this->uri))),
};
}
/**
* Remove query pair according to their key/value name.
*/
public function removeQueryPairsByKeyValue(string $key, Stringable|string|int|bool|null $value): static
{
$query = Query::fromUri($this->uri);
$newQuery = $query->withoutPairByKeyValue($key, $value)->value();
return match ($newQuery) {
$query->value() => $this,
default => new static($this->uri->withQuery(static::normalizeComponent($newQuery, $this->uri))),
};
}
/**
* Remove query data according to their PHP parameter key name.
*/
public function removeQueryParameters(string ...$keys): static
{
$query = Query::fromUri($this->uri);
$newQuery = $query->withoutParameters(...$keys)->value();
return match ($newQuery) {
$query->value() => $this,
default => new static($this->uri->withQuery(static::normalizeComponent($newQuery, $this->uri))),
};
}
/**
* Remove empty pairs from the URL query component.
*
* A pair is considered empty if it's name is the empty string
* and its value is either the empty string or the null value
*/
public function removeEmptyQueryPairs(): static
{
return new static($this->uri->withQuery(
static::normalizeComponent(
Query::fromUri($this->uri)->withoutEmptyPairs()->value(),
$this->uri
)
));
}
/**
* Returns an instance where numeric indices associated to PHP's array like key are removed.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the query component normalized so that numeric indexes
* are removed from the pair key value.
*
* ie.: toto[3]=bar[3]&foo=bar becomes toto[]=bar[3]&foo=bar
*/
public function removeQueryParameterIndices(): static
{
$query = Query::fromUri($this->uri);
$newQuery = $query->withoutNumericIndices()->value();
return match ($newQuery) {
$query->value() => $this,
default => new static($this->uri->withQuery(static::normalizeComponent($newQuery, $this->uri))),
};
}
/*********************************
* Host modifier methods
*********************************/
/**
* Add the root label to the URI.
*/
public function addRootLabel(): static
{
$host = $this->uri->getHost();
return match (true) {
null === $host,
str_ends_with($host, '.') => $this,
default => new static($this->uri->withHost($host.'.')),
};
}
/**
* Append a label or a host to the current URI host.
*
* @throws SyntaxError If the host cannot be appended
*/
public function appendLabel(Stringable|string|null $label): static
{
$host = Host::fromUri($this->uri);
$label = Host::new($label);
return match (true) {
null === $label->value() => $this,
$host->isDomain() => new static($this->uri->withHost(static::normalizeComponent(Domain::new($host)->append($label)->toUnicode(), $this->uri))),
$host->isIpv4() => new static($this->uri->withHost($host->value().'.'.ltrim($label->value(), '.'))),
default => throw new SyntaxError('The URI host '.$host->toString().' cannot be appended.'),
};
}
/**
* Convert the URI host part to its ascii value.
*/
public function hostToAscii(): static
{
$currentHost = $this->uri->getHost();
$host = IdnConverter::toAsciiOrFail((string) $currentHost);
return match (true) {
null === $currentHost,
'' === $currentHost,
$host === $currentHost => $this,
default => new static($this->uri->withHost($host)),
};
}
/**
* Convert the URI host part to its unicode value.
*/
public function hostToUnicode(): static
{
$currentHost = $this->uri->getHost();
$host = IdnConverter::toUnicode((string) $currentHost)->domain();
return match (true) {
null === $currentHost,
'' === $currentHost,
$host === $currentHost => $this,
default => new static($this->uri->withHost($host)),
};
}
/**
* Normalizes the URI host content to a IPv4 dot-decimal notation if possible
* otherwise returns the uri instance unchanged.
*
* @see https://url.spec.whatwg.org/#concept-ipv4-parser
*/
public function hostToDecimal(): static
{
$currentHost = $this->uri->getHost();
$hostIp = self::ipv4Converter()->toDecimal($currentHost);
return match (true) {
null === $currentHost,
'' === $currentHost,
null === $hostIp,
$currentHost === $hostIp => $this,
default => new static($this->uri->withHost($hostIp)),
};
}
/**
* Normalizes the URI host content to a IPv4 octal notation if possible
* otherwise returns the uri instance unchanged.
*
* @see https://url.spec.whatwg.org/#concept-ipv4-parser
*/
public function hostToOctal(): static
{
$currentHost = $this->uri->getHost();
$hostIp = self::ipv4Converter()->toOctal($currentHost);
return match (true) {
null === $currentHost,
'' === $currentHost,
null === $hostIp,
$currentHost === $hostIp => $this,
default => new static($this->uri->withHost($hostIp)),
};
}
/**
* Normalizes the URI host content to a IPv4 octal notation if possible
* otherwise returns the uri instance unchanged.
*
* @see https://url.spec.whatwg.org/#concept-ipv4-parser
*/
public function hostToHexadecimal(): static
{
$currentHost = $this->uri->getHost();
$hostIp = self::ipv4Converter()->toHexadecimal($currentHost);
return match (true) {
null === $currentHost,
'' === $currentHost,
null === $hostIp,
$currentHost === $hostIp => $this,
default => new static($this->uri->withHost($hostIp)),
};
}
/**
* Prepend a label or a host to the current URI host.
*
* @throws SyntaxError If the host cannot be prepended
*/
public function prependLabel(Stringable|string|null $label): static
{
$host = Host::fromUri($this->uri);
$label = Host::new($label);
return match (true) {
null === $label->value() => $this,
$host->isIpv4() => new static($this->uri->withHost(rtrim($label->value(), '.').'.'.$host->value())),
$host->isDomain() => new static($this->uri->withHost(static::normalizeComponent(Domain::new($host)->prepend($label)->toUnicode(), $this->uri))),
default => throw new SyntaxError('The URI host '.$host->toString().' cannot be prepended.'),
};
}
/**
* Remove host labels according to their offset.
*/
public function removeLabels(int ...$keys): static
{
return new static($this->uri->withHost(
static::normalizeComponent(
Domain::fromUri($this->uri)->withoutLabel(...$keys)->toUnicode(),
$this->uri
)
));
}
/**
* Remove the root label to the URI.
*/
public function removeRootLabel(): static
{
$host = $this->uri->getHost();
return match (true) {
null === $host,
'' === $host,
!str_ends_with($host, '.') => $this,
default => new static($this->uri->withHost(substr($host, 0, -1))),
};
}
/**
* Slice the host from the URI.
*/
public function sliceLabels(int $offset, ?int $length = null): static
{
$currentHost = $this->uri->getHost();
$host = Domain::new($currentHost)->slice($offset, $length);
return match (true) {
$host->value() === $currentHost,
$host->toUnicode() === $currentHost => $this,
default => new static($this->uri->withHost($host->toUnicode())),
};
}
/**
* Remove the host zone identifier.
*/
public function removeZoneId(): static
{
$host = Host::fromUri($this->uri);
return match (true) {
$host->hasZoneIdentifier() => new static($this->uri->withHost(
static::normalizeComponent(
Host::fromUri($this->uri)->withoutZoneIdentifier()->value(),
$this->uri
)
)),
default => $this,
};
}
/**
* Replace a label of the current URI host.
*/
public function replaceLabel(int $offset, Stringable|string|null $label): static
{
return new static($this->uri->withHost(
static::normalizeComponent(
Domain::fromUri($this->uri)->withLabel($offset, $label)->toUnicode(),
$this->uri
)
));
}
/*********************************
* Path modifier methods
*********************************/
/**
* Add a new base path to the URI path.
*/
public function addBasePath(Stringable|string $path): static
{
/** @var HierarchicalPath $path */
$path = HierarchicalPath::new($path)->withLeadingSlash();
/** @var HierarchicalPath $currentPath */
$currentPath = HierarchicalPath::fromUri($this->uri)->withLeadingSlash();
return new static(match (true) {
!str_starts_with($currentPath->toString(), $path->toString()) => $this->uri->withPath($path->append($currentPath)->toString()),
default => static::normalizePath($this->uri, $currentPath),
});
}
/**
* Add a leading slash to the URI path.
*/
public function addLeadingSlash(): static
{
$path = $this->uri->getPath();
return match (true) {
str_starts_with($path, '/') => $this,
default => new static($this->uri->withPath('/'.$path)),
};
}
/**
* Add a trailing slash to the URI path.
*/
public function addTrailingSlash(): static
{
$path = $this->uri->getPath();
return match (true) {
str_ends_with($path, '/') => $this,
default => new static($this->uri->withPath($path.'/')),
};
}
/**
* Append a new segment or a new path to the URI path.
*/
public function appendSegment(Stringable|string $segment): static
{
return new static(static::normalizePath($this->uri, HierarchicalPath::fromUri($this->uri)->append($segment)));
}
/**
* Convert the Data URI path to its ascii form.
*/
public function dataPathToAscii(): static
{
return new static($this->uri->withPath(DataPath::fromUri($this->uri)->toAscii()->toString()));
}
/**
* Convert the Data URI path to its binary (base64encoded) form.
*/
public function dataPathToBinary(): static
{
return new static($this->uri->withPath(DataPath::fromUri($this->uri)->toBinary()->toString()));
}
/**
* Prepend a new segment or a new path to the URI path.
*/
public function prependSegment(Stringable|string $segment): static
{
return new static(static::normalizePath($this->uri, HierarchicalPath::fromUri($this->uri)->prepend($segment)));
}
/**
* Remove a base path from the URI path.
*/
public function removeBasePath(Stringable|string $path): static
{
$basePath = HierarchicalPath::new($path)->withLeadingSlash()->toString();
$currentPath = HierarchicalPath::fromUri($this->uri)->withLeadingSlash()->toString();
$newPath = substr($currentPath, strlen($basePath));
return match (true) {
'/' === $basePath,
!str_starts_with($currentPath, $basePath),
!str_starts_with($newPath, '/') => $this,
default => new static($this->uri->withPath($newPath)),
};
}
/**
* Remove dot segments from the URI path.
*/
public function removeDotSegments(): static
{
return new static($this->uri->withPath(Path::fromUri($this->uri)->withoutDotSegments()->toString()));
}
/**
* Remove empty segments from the URI path.
*/
public function removeEmptySegments(): static
{
return new static($this->uri->withPath(HierarchicalPath::fromUri($this->uri)->withoutEmptySegments()->toString()));
}
/**
* Remove the leading slash from the URI path.
*/
public function removeLeadingSlash(): static
{
return new static(static::normalizePath($this->uri, Path::fromUri($this->uri)->withoutLeadingSlash()));
}
/**
* Remove the trailing slash from the URI path.
*/
public function removeTrailingSlash(): static
{
$path = $this->uri->getPath();
return match (true) {
!str_ends_with($path, '/') => $this,
default => new static($this->uri->withPath(substr($path, 0, -1))),
};
}
/**
* Remove path segments from the URI path according to their offsets.
*/
public function removeSegments(int ...$keys): static
{
return new static($this->uri->withPath(HierarchicalPath::fromUri($this->uri)->withoutSegment(...$keys)->toString()));
}
/**
* Replace the URI path basename.
*/
public function replaceBasename(Stringable|string $basename): static
{
return new static(static::normalizePath($this->uri, HierarchicalPath::fromUri($this->uri)->withBasename($basename)));
}
/**
* Replace the data URI path parameters.
*/
public function replaceDataUriParameters(Stringable|string $parameters): static
{
return new static($this->uri->withPath(DataPath::fromUri($this->uri)->withParameters($parameters)->toString()));
}
/**
* Replace the URI path dirname.
*/
public function replaceDirname(Stringable|string $dirname): static
{
return new static(static::normalizePath($this->uri, HierarchicalPath::fromUri($this->uri)->withDirname($dirname)));
}
/**
* Replace the URI path basename extension.
*/
public function replaceExtension(Stringable|string $extension): static
{
return new static($this->uri->withPath(HierarchicalPath::fromUri($this->uri)->withExtension($extension)->toString()));
}
/**
* Replace a segment from the URI path according its offset.
*/
public function replaceSegment(int $offset, Stringable|string $segment): static
{
return new static($this->uri->withPath(HierarchicalPath::fromUri($this->uri)->withSegment($offset, $segment)->toString()));
}
/**
* Slice the host from the URI.
*/
public function sliceSegments(int $offset, ?int $length = null): static
{
return new static(static::normalizePath($this->uri, HierarchicalPath::fromUri($this->uri)->slice($offset, $length)));
}
/**
* Normalize a URI path.
*
* Make sure the path always has a leading slash if an authority is present
* and the path is not the empty string.
*/
final protected static function normalizePath(Psr7UriInterface|UriInterface $uri, PathInterface $path): Psr7UriInterface|UriInterface
{
$pathString = $path->toString();
$authority = $uri->getAuthority();
return match (true) {
'' === $pathString,
'/' === $pathString[0],
null === $authority,
'' === $authority => $uri->withPath($pathString),
default => $uri->withPath('/'.$pathString),
};
}
/**
* Normalize the URI component value depending on the subject interface.
*
* null value MUST be converted to the empty string if a Psr7 UriInterface is being manipulated.
*/
final protected static function normalizeComponent(?string $component, Psr7UriInterface|UriInterface $uri): ?string
{
return match (true) {
$uri instanceof Psr7UriInterface => (string) $component,
default => $component,
};
}
final protected static function ipv4Converter(): IPv4Converter
{
static $converter;
$converter = $converter ?? IPv4Converter::fromEnvironment();
return $converter;
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.2.0
* @codeCoverageIgnore
* @see Modifier::removeQueryParameters()
*
* Remove query data according to their key name.
*/
public function removeParams(string ...$keys): static
{
return $this->removeQueryParameters(...$keys);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.2.0
* @codeCoverageIgnore
* @see Modifier::removeEmptyQueryPairs()
*
* Remove empty pairs from the URL query component.
*
* A pair is considered empty if it's name is the empty string
* and its value is either the empty string or the null value
*/
public function removeEmptyPairs(): static
{
return $this->removeEmptyQueryPairs();
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.2.0
* @codeCoverageIgnore
* @see Modifier::removeQueryPairsByKey()
*
* Remove query data according to their key name.
*/
public function removePairs(string ...$keys): static
{
return $this->removeQueryPairsByKey(...$keys);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.2.0
* @codeCoverageIgnore
* @see Modifier::removeQueryPairsByKey()
*
* Remove query data according to their key name.
*/
public function removeQueryPairs(string ...$keys): static
{
return $this->removeQueryPairsByKey(...$keys);
}
}