| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446 |
- <?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 BackedEnum;
- use Exception;
- use JsonSerializable;
- use League\Uri\Contracts\UriComponentInterface;
- use League\Uri\Exceptions\SyntaxError;
- use League\Uri\Idna\Converter as IdnConverter;
- use Stringable;
- use Throwable;
- use function array_key_first;
- use function count;
- use function explode;
- use function filter_var;
- use function get_object_vars;
- use function in_array;
- use function inet_pton;
- use function is_object;
- use function preg_match;
- use function rawurldecode;
- use function strpos;
- use function strtolower;
- use function substr;
- use const FILTER_FLAG_IPV4;
- use const FILTER_FLAG_IPV6;
- use const FILTER_VALIDATE_IP;
- /**
- * @phpstan-type HostRecordSerializedShape array{0: array{host: ?string}, 1: array{}}
- */
- final class HostRecord implements JsonSerializable
- {
- /**
- * Maximum number of host cached.
- *
- * @var int
- */
- private const MAXIMUM_HOST_CACHED = 100;
- private 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))*)
- )
- ^(?:(?®_name)\.)*(?®_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-F\d])+\.
- (?:
- (?<unreserved>[a-z\d_~\-\.])|
- (?<sub_delims>[!$&\'()*+,;=:]) # also include the : character
- )+
- $/ix';
- private const REGEXP_GEN_DELIMS = '/[:\/?#\[\]@ ]/';
- private const ADDRESS_BLOCK = "\xfe\x80";
- private ?bool $isDomainName = null;
- private ?bool $hasZoneIdentifier = null;
- private bool $asciiIsLoaded = false;
- private ?string $hostAsAscii = null;
- private bool $unicodeIsLoaded = false;
- private ?string $hostAsUnicode = null;
- private bool $isIpVersionLoaded = false;
- private ?string $ipVersion = null;
- private bool $isIpValueLoaded = false;
- private ?string $ipValue = null;
- private function __construct(
- public readonly ?string $value,
- public readonly HostType $type,
- public readonly HostFormat $format
- ) {
- }
- public function hasZoneIdentifier(): bool
- {
- return $this->hasZoneIdentifier ??= HostType::Ipv6 === $this->type && str_contains((string) $this->value, '%');
- }
- public function toAscii(): ?string
- {
- if (!$this->asciiIsLoaded) {
- $this->asciiIsLoaded = true;
- $this->hostAsAscii = (function (): ?string {
- if (HostType::RegisteredName !== $this->type || null === $this->value) {
- return $this->value;
- }
- $formattedHost = rawurldecode($this->value);
- if ($formattedHost === $this->value) {
- return $this->isDomainType() ? IdnConverter::toAscii($this->value)->domain() : strtolower($formattedHost);
- }
- return Encoder::normalizeHost($this->value);
- })();
- }
- return $this->hostAsAscii;
- }
- public function toUnicode(): ?string
- {
- if (!$this->unicodeIsLoaded) {
- $this->unicodeIsLoaded = true;
- $this->hostAsUnicode = $this->isDomainType() && null !== $this->value ? IdnConverter::toUnicode($this->value)->domain() : $this->value;
- }
- return $this->hostAsUnicode;
- }
- public function isDomainType(): bool
- {
- return $this->isDomainName ??= match (true) {
- HostType::RegisteredName !== $this->type, '' === $this->value => false,
- null === $this->value => true,
- default => is_object($result = IdnConverter::toAscii($this->value))
- && !$result->hasErrors()
- && self::isValidDomain($result->domain()),
- };
- }
- public function ipVersion(): ?string
- {
- if (!$this->isIpVersionLoaded) {
- $this->isIpVersionLoaded = true;
- $this->ipVersion = match (true) {
- HostType::Ipv4 === $this->type => '4',
- HostType::Ipv6 === $this->type => '6',
- 1 === preg_match(self::REGEXP_IP_FUTURE, substr((string) $this->value, 1, -1), $matches) => $matches['version'],
- default => null,
- };
- }
- return $this->ipVersion;
- }
- public function ipValue(): ?string
- {
- if (!$this->isIpValueLoaded) {
- $this->isIpValueLoaded = true;
- $this->ipValue = (function (): ?string {
- if (HostType::RegisteredName === $this->type) {
- return null;
- }
- if (HostType::Ipv4 === $this->type) {
- return $this->value;
- }
- $ip = substr((string) $this->value, 1, -1);
- if (HostType::Ipv6 !== $this->type) {
- 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));
- })();
- }
- return $this->ipValue;
- }
- public static function isValid(BackedEnum|Stringable|string|null $host): bool
- {
- try {
- HostRecord::from($host);
- return true;
- } catch (Throwable) {
- return false;
- }
- }
- public static function isIpv4(Stringable|string|null $host): bool
- {
- try {
- return HostType::Ipv4 === HostRecord::from($host)->type;
- } catch (Throwable) {
- return false;
- }
- }
- public static function isIpv6(Stringable|string|null $host): bool
- {
- try {
- return HostType::Ipv6 === HostRecord::from($host)->type;
- } catch (Throwable) {
- return false;
- }
- }
- public static function isIpvFuture(Stringable|string|null $host): bool
- {
- try {
- return HostType::IpvFuture === HostRecord::from($host)->type;
- } catch (Throwable) {
- return false;
- }
- }
- public static function isIp(Stringable|string|null $host): bool
- {
- return !self::isRegisteredName($host);
- }
- public static function isRegisteredName(Stringable|string|null $host): bool
- {
- try {
- return HostType::RegisteredName === HostRecord::from($host)->type;
- } catch (Throwable) {
- return false;
- }
- }
- public static function isDomain(Stringable|string|null $host): bool
- {
- try {
- return HostRecord::from($host)->isDomainType();
- } catch (Throwable) {
- return false;
- }
- }
- /**
- * @throws SyntaxError
- */
- public static function from(BackedEnum|Stringable|string|null $host): self
- {
- if ($host instanceof BackedEnum) {
- $host = $host->value;
- }
- if ($host instanceof UriComponentInterface) {
- $host = $host->value();
- }
- if (null === $host) {
- return new self(
- value: null,
- type: HostType::RegisteredName,
- format: HostFormat::Ascii,
- );
- }
- $host = (string) $host;
- if ('' === $host) {
- return new self(
- value: '',
- type: HostType::RegisteredName,
- format: HostFormat::Ascii,
- );
- }
- static $inMemoryCache = [];
- if (isset($inMemoryCache[$host])) {
- return $inMemoryCache[$host];
- }
- if (self::MAXIMUM_HOST_CACHED < count($inMemoryCache)) {
- unset($inMemoryCache[array_key_first($inMemoryCache)]);
- }
- if ($host === filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
- return $inMemoryCache[$host] = new self(
- value: $host,
- type: HostType::Ipv4,
- format: HostFormat::Ascii,
- );
- }
- if (str_starts_with($host, '[')) {
- str_ends_with($host, ']') || throw new SyntaxError('The host '.$host.' is not a valid IPv6 host.');
- $ipHost = substr($host, 1, -1);
- if (1 === preg_match(self::REGEXP_IP_FUTURE, $ipHost, $matches)) {
- return !in_array($matches['version'], ['4', '6'], true) ? ($inMemoryCache[$host] = new self(
- value: $host,
- type: HostType::IpvFuture,
- format: HostFormat::Ascii,
- )) : throw new SyntaxError('The host '.$host.' is not a valid IPvFuture host.');
- }
- if (self::isValidIpv6Hostname($ipHost)) {
- return $inMemoryCache[$host] = new self(
- value: $host,
- type: HostType::Ipv6,
- format: HostFormat::Ascii,
- );
- }
- throw new SyntaxError('The host '.$host.' is not a valid IPv6 host.');
- }
- $domainName = rawurldecode($host);
- $format = HostFormat::Unicode;
- if (1 !== preg_match(self::REGEXP_NON_ASCII_PATTERN, $domainName)) {
- $domainName = strtolower($domainName);
- $format = HostFormat::Ascii;
- }
- if (1 === preg_match(self::REGEXP_REGISTERED_NAME, $domainName)) {
- return $inMemoryCache[$host] = new self(
- value: $host,
- type: HostType::RegisteredName,
- format: $format,
- );
- }
- (HostFormat::Ascii !== $format && 1 !== preg_match(self::REGEXP_INVALID_HOST_CHARS, $domainName)) || throw new SyntaxError('`'.$host.'` is an invalid domain name : the host contains invalid characters.');
- IdnConverter::toAsciiOrFail($domainName);
- return $inMemoryCache[$host] = new self(
- value: $host,
- type: HostType::RegisteredName,
- format: $format,
- );
- }
- /**
- * 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 static 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 static 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 function jsonSerialize(): ?string
- {
- return $this->value;
- }
- /**
- * @return HostRecordSerializedShape
- */
- public function __serialize(): array
- {
- return [['host' => $this->value], []];
- }
- /**
- * @param HostRecordSerializedShape $data
- *
- * @throws Exception|SyntaxError
- */
- public function __unserialize(array $data): void
- {
- [$properties] = $data;
- $record = self::from($properties['host'] ?? throw new Exception('The `host` property is missing from the serialized object.'));
- //if the Host computed value are already cache this avoid recomputing them
- foreach (get_object_vars($record) as $prop => $value) {
- /* @phpstan-ignore-next-line */
- $this->{$prop} = $value;
- }
- }
- }
|