Version 5.0.0 beta 1

This commit is contained in:
Neo committed 2025-12-19 16:27:35 -08:00
1 parent 25ddeb65d6
commit 15c7beabc5
6736 files changed
+627902 -497943

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Pdp;
use Throwable;
interface CannotProcessHost extends Throwable
{
}
+162
View File
@@ -0,0 +1,162 @@
<?php
declare(strict_types=1);
namespace Pdp;
use Iterator;
use Stringable;
use const FILTER_FLAG_IPV4;
use const FILTER_VALIDATE_IP;
final class Domain implements DomainName
{
/**
* @throws SyntaxError
*/
private function __construct(private readonly RegisteredName $registeredName)
{
if (false !== filter_var($this->registeredName->value(), FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
throw SyntaxError::dueToUnsupportedType($this->registeredName->toString());
}
}
/**
* @param array{registeredName: RegisteredName} $properties
*/
public static function __set_state(array $properties): self
{
return new self($properties['registeredName']);
}
/**
* @throws CannotProcessHost
*/
public static function fromIDNA2003(DomainNameProvider|Host|Stringable|string|int|null $domain): self
{
return new self(RegisteredName::fromIDNA2003($domain));
}
/**
* @throws CannotProcessHost
*/
public static function fromIDNA2008(DomainNameProvider|Host|Stringable|string|int|null $domain): self
{
return new self(RegisteredName::fromIDNA2008($domain));
}
/**
* @return Iterator<string>
*/
public function getIterator(): Iterator
{
yield from $this->registeredName;
}
public function isAscii(): bool
{
return $this->registeredName->isAscii();
}
public function jsonSerialize(): ?string
{
return $this->registeredName->jsonSerialize();
}
public function count(): int
{
return count($this->registeredName);
}
public function value(): ?string
{
return $this->registeredName->value();
}
public function toString(): string
{
return $this->registeredName->toString();
}
public function label(int $key): ?string
{
return $this->registeredName->label($key);
}
/**
* @return list<int>
*/
public function keys(?string $label = null): array
{
return $this->registeredName->keys($label);
}
/**
* @return list<string>
*/
public function labels(): array
{
return $this->registeredName->labels();
}
private function newInstance(RegisteredName $registeredName): self
{
if ($registeredName->value() === $this->registeredName->value()) {
return $this;
}
return new self($registeredName);
}
public function toAscii(): self
{
return $this->newInstance($this->registeredName->toAscii());
}
public function toUnicode(): self
{
return $this->newInstance($this->registeredName->toUnicode());
}
/**
* @throws CannotProcessHost
*/
public function prepend(DomainNameProvider|Host|Stringable|string|int|null $label): self
{
return $this->newInstance($this->registeredName->prepend($label));
}
/**
* @throws CannotProcessHost
*/
public function append(DomainNameProvider|Host|Stringable|string|int|null $label): self
{
return $this->newInstance($this->registeredName->append($label));
}
public function withLabel(int $key, DomainNameProvider|Host|Stringable|string|int|null $label): self
{
return $this->newInstance($this->registeredName->withLabel($key, $label));
}
public function withoutLabel(int ...$keys): self
{
return $this->newInstance($this->registeredName->withoutLabel(...$keys));
}
/**
* @throws CannotProcessHost
*/
public function clear(): self
{
return $this->newInstance($this->registeredName->clear());
}
/**
* @throws CannotProcessHost
*/
public function slice(int $offset, ?int $length = null): self
{
return $this->newInstance($this->registeredName->slice($offset, $length));
}
}
+123
View File
@@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace Pdp;
use Iterator;
use IteratorAggregate;
use Stringable;
/**
* @see https://tools.ietf.org/html/rfc1034#section-3.5
* @see https://tools.ietf.org/html/rfc1123#section-2.1
* @see https://tools.ietf.org/html/rfc5890
*
* @extends IteratorAggregate<string>
*/
interface DomainName extends Host, IteratorAggregate
{
/**
* Tells whether the current domain is in its ascii form.
*/
public function isAscii(): bool;
/**
* Retrieves a single domain label.
*
* If $key is non-negative, the returned value will be the label at $key position from the start.
* If $key is negative, the returned value will be the label at $key position from the end.
*
* If no label is found the submitted $key the returned value will be null.
*
* @param int $key the label offset
*/
public function label(int $key): ?string;
/**
* Returns the object labels.
*
* @return list<string>
*/
public function labels(): array;
/**
* Returns the associated key for each label.
*
* If a value is specified only the keys associated with
* the given value will be returned
*
* @return list<int>
*/
public function keys(?string $label = null): array;
/**
* The external iterator iterates over the DomainInterface labels
* from the right-most label to the left-most label.
*
* @return Iterator<string>
*/
public function getIterator(): Iterator;
/**
* Prepends a label to the domain.
*
* @see ::withLabel
*/
public function prepend(DomainNameProvider|Host|Stringable|string|int|null $label): self;
/**
* Appends a label to the domain.
*
* @see ::withLabel
*/
public function append(DomainNameProvider|Host|Stringable|string|int|null $label): self;
/**
* Returns an instance with the specified label added at the specified key.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the new label
*
* If $key is non-negative, the added label will be the label at $key position from the start.
* If $key is negative, the added label will be the label at $key position from the end.
*
* @param DomainNameProvider|Host|Stringable|string|int|null $label a domain label
*
* @throws CannotProcessHost If the key is out of bounds
* @throws CannotProcessHost If the label is converted to the NULL value
*/
public function withLabel(int $key, DomainNameProvider|Host|Stringable|string|int|null $label): self;
/**
* Returns an instance with the label at the specified key removed.
*
* This method MUST retain the state of the current instance, and return
* an instance without the specified label
*
* If $key is non-negative, the removed label will be the label at $key position from the start.
* If $key is negative, the removed label will be the label at $key position from the end.
*
*
* @throws CannotProcessHost If the key is out of bounds
*/
public function withoutLabel(int ...$keys): self;
/**
* Returns an instance with all labels removed.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains no label
*/
public function clear(): self;
/**
* Extracts a slice of $length elements starting at position $offset from the Collection.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the selected slice.
*
* If $length is null it returns all elements from $offset to the end of the Domain.
*/
public function slice(int $offset, ?int $length = null): self;
}
+10
View File
@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Pdp;
interface DomainNameProvider
{
public function domain(): DomainName;
}
+16
View File
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Pdp;
interface DomainNameResolver
{
/**
* Returns the effective TLD info for a given domain against an data source.
*
* If the effective TLD can not be resolved it returns a ResolvedDomainName with a null suffix
* If the host is not a valid domain it returns a ResolvedDomainName with a null Domain name
*/
public function resolve(Host $host): ResolvedDomainName;
}
+41
View File
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Pdp;
interface EffectiveTopLevelDomain extends Host, DomainNameProvider
{
/**
* Tells whether the effective TLD has a matching rule in a Public Suffix List.
*/
public function isKnown(): bool;
/**
* Tells whether the effective TLD has a matching rule in the IANA Top Level Domain List.
*/
public function isIANA(): bool;
/**
* Tells whether the effective TLD has a matching rule in the Public Suffix List.
*/
public function isPublicSuffix(): bool;
/**
* Tells whether the effective TLD has a matching rule in a Public Suffix List ICANN Section.
*/
public function isICANN(): bool;
/**
* Tells whether the effective TLD has a matching rule in a Public Suffix List Private Section.
*/
public function isPrivate(): bool;
/**
* Returns an instance with the public suffix normalize to the submitted domain encoding algorithm.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the same domain information normalize against the submitted domainName
*/
public function normalize(DomainName $domain): self;
}
+60
View File
@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Pdp;
use Countable;
use JsonSerializable;
/**
* @see https://tools.ietf.org/html/rfc1034#section-3.5
* @see https://tools.ietf.org/html/rfc1123#section-2.1
* @see https://tools.ietf.org/html/rfc5890
*/
interface Host extends Countable, JsonSerializable
{
/**
* Returns the domain content.
*/
public function value(): ?string;
/**
* Returns the domain content as a string.
*/
public function toString(): string;
/**
* Returns the domain content.
*/
public function jsonSerialize(): ?string;
/**
* The labels total number.
*/
public function count(): int;
/**
* Converts the domain to its IDNA ASCII form.
*
* This method MUST retain the state of the current instance, and return
* an instance with its content converted to its IDNA ASCII form
*
* @throws CannotProcessHost if the domain can not be converted to ASCII using IDN UTS46 algorithm
*
* @return static
*/
public function toAscii(): self;
/**
* Converts the domain to its IDNA UTF8 form.
*
* This method MUST retain the state of the current instance, and return
* an instance with its content converted to its IDNA UTF8 form
*
* @throws CannotProcessHost if the domain can not be converted to Unicode using IDN UTS46 algorithm
*
* @return static
*/
public function toUnicode(): self;
}
+135
View File
@@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
namespace Pdp;
use UnexpectedValueException;
use function defined;
use function function_exists;
use function idn_to_ascii;
use function idn_to_utf8;
use function preg_match;
use function rawurldecode;
use function strtolower;
use const INTL_IDNA_VARIANT_UTS46;
/**
* @internal
* @see https://unicode-org.github.io/icu-docs/apidoc/released/icu4c/uidna_8h.html
*/
final class Idna
{
private const REGEXP_IDNA_PATTERN = '/[^\x20-\x7f]/';
/**
* IDNA options.
*/
public const IDNA_DEFAULT = 0;
public const IDNA_ALLOW_UNASSIGNED = 1;
public const IDNA_USE_STD3_RULES = 2;
public const IDNA_CHECK_BIDI = 4;
public const IDNA_CHECK_CONTEXTJ = 8;
public const IDNA_NONTRANSITIONAL_TO_ASCII = 0x10;
public const IDNA_NONTRANSITIONAL_TO_UNICODE = 0x20;
public const IDNA_CHECK_CONTEXTO = 0x40;
/**
* IDNA errors.
*/
public const ERROR_EMPTY_LABEL = 1;
public const ERROR_LABEL_TOO_LONG = 2;
public const ERROR_DOMAIN_NAME_TOO_LONG = 4;
public const ERROR_LEADING_HYPHEN = 8;
public const ERROR_TRAILING_HYPHEN = 0x10;
public const ERROR_HYPHEN_3_4 = 0x20;
public const ERROR_LEADING_COMBINING_MARK = 0x40;
public const ERROR_DISALLOWED = 0x80;
public const ERROR_PUNYCODE = 0x100;
public const ERROR_LABEL_HAS_DOT = 0x200;
public const ERROR_INVALID_ACE_LABEL = 0x400;
public const ERROR_BIDI = 0x800;
public const ERROR_CONTEXTJ = 0x1000;
public const ERROR_CONTEXTO_PUNCTUATION = 0x2000;
public const ERROR_CONTEXTO_DIGITS = 0x4000;
/**
* IDNA default options.
*/
public const IDNA2008_ASCII = self::IDNA_NONTRANSITIONAL_TO_ASCII
| self::IDNA_CHECK_BIDI
| self::IDNA_USE_STD3_RULES
| self::IDNA_CHECK_CONTEXTJ;
public const IDNA2008_UNICODE = self::IDNA_NONTRANSITIONAL_TO_UNICODE
| self::IDNA_CHECK_BIDI
| self::IDNA_USE_STD3_RULES
| self::IDNA_CHECK_CONTEXTJ;
public const IDNA2003_ASCII = self::IDNA_DEFAULT;
public const IDNA2003_UNICODE = self::IDNA_DEFAULT;
/**
* @codeCoverageIgnore
*/
private static function supportsIdna(): void
{
static $idnSupport;
if (null === $idnSupport) {
$idnSupport = function_exists('\idn_to_ascii') && defined('\INTL_IDNA_VARIANT_UTS46');
}
if (!$idnSupport) {
throw new UnexpectedValueException('IDN host can not be processed. Verify that ext/intl is installed for IDN support and that ICU is at least version 4.6.');
}
}
/**
* Converts the input to its IDNA ASCII form.
*
* This method returns the string converted to IDN ASCII form
*
* @throws SyntaxError if the string can not be converted to ASCII using IDN UTS46 algorithm
*/
public static function toAscii(string $domain, int $options): IdnaInfo
{
$domain = rawurldecode($domain);
if (1 !== preg_match(self::REGEXP_IDNA_PATTERN, $domain)) {
return IdnaInfo::fromIntl(['result' => strtolower($domain), 'isTransitionalDifferent' => false, 'errors' => 0]);
}
self::supportsIdna();
/* @param-out array{errors: int, isTransitionalDifferent: bool, result: string} $idnaInfo */
idn_to_ascii($domain, $options, INTL_IDNA_VARIANT_UTS46, $idnaInfo);
/** @var array{result:string, isTransitionalDifferent:bool, errors:int} $idnaInfo */
$info = IdnaInfo::fromIntl($idnaInfo);
if (0 !== $info->errors()) {
throw SyntaxError::dueToIDNAError($domain, $info);
}
return $info;
}
/**
* Converts the input to its IDNA UNICODE form.
*
* This method returns the string converted to IDN UNICODE form
*/
public static function toUnicode(string $domain, int $options): IdnaInfo
{
if (!str_contains($domain, 'xn--')) {
return IdnaInfo::fromIntl(['result' => $domain, 'isTransitionalDifferent' => false, 'errors' => 0]);
}
self::supportsIdna();
/* @param-out array{errors: int, isTransitionalDifferent: bool, result: string} $idnaInfo */
idn_to_utf8($domain, $options, INTL_IDNA_VARIANT_UTS46, $idnaInfo);
/** @var array{result:string, isTransitionalDifferent:bool, errors:int} $idnaInfo */
$info = IdnaInfo::fromIntl($idnaInfo);
if (0 !== $info->errors()) {
return IdnaInfo::fromIntl(['result' => $domain, 'isTransitionalDifferent' => false, 'errors' => 0]);
}
return $info;
}
}
+92
View File
@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace Pdp;
use function array_filter;
use const ARRAY_FILTER_USE_KEY;
/**
* @internal
* @see https://unicode-org.github.io/icu-docs/apidoc/released/icu4c/uidna_8h.html
*/
final class IdnaInfo
{
private const ERRORS = [
Idna::ERROR_EMPTY_LABEL => 'a non-final domain name label (or the whole domain name) is empty',
Idna::ERROR_LABEL_TOO_LONG => 'a domain name label is longer than 63 bytes',
Idna::ERROR_DOMAIN_NAME_TOO_LONG => 'a domain name is longer than 255 bytes in its storage form',
Idna::ERROR_LEADING_HYPHEN => 'a label starts with a hyphen-minus ("-")',
Idna::ERROR_TRAILING_HYPHEN => 'a label ends with a hyphen-minus ("-")',
Idna::ERROR_HYPHEN_3_4 => 'a label contains hyphen-minus ("-") in the third and fourth positions',
Idna::ERROR_LEADING_COMBINING_MARK => 'a label starts with a combining mark',
Idna::ERROR_DISALLOWED => 'a label or domain name contains disallowed characters',
Idna::ERROR_PUNYCODE => 'a label starts with "xn--" but does not contain valid Punycode',
Idna::ERROR_LABEL_HAS_DOT => 'a label contains a dot=full stop',
Idna::ERROR_INVALID_ACE_LABEL => 'An ACE label does not contain a valid label string',
Idna::ERROR_BIDI => 'a label does not meet the IDNA BiDi requirements (for right-to-left characters)',
Idna::ERROR_CONTEXTJ => 'a label does not meet the IDNA CONTEXTJ requirements',
Idna::ERROR_CONTEXTO_DIGITS => 'a label does not meet the IDNA CONTEXTO requirements for digits',
Idna::ERROR_CONTEXTO_PUNCTUATION => 'a label does not meet the IDNA CONTEXTO requirements for punctuation characters. Some punctuation characters "Would otherwise have been DISALLOWED" but are allowed in certain contexts',
];
/** @var array<int, string> */
public readonly array $errorList;
private function __construct(
public readonly string $result,
public readonly bool $isTransitionalDifferent,
public readonly int $errors
) {
$this->errorList = array_filter(
self::ERRORS,
fn (int $error): bool => 0 !== ($error & $this->errors),
ARRAY_FILTER_USE_KEY
);
}
/**
* @param array{result:string, isTransitionalDifferent:bool, errors:int} $infos
*/
public static function fromIntl(array $infos): self
{
return new self($infos['result'], $infos['isTransitionalDifferent'], $infos['errors']);
}
/**
* @param array{result:string, isTransitionalDifferent:bool, errors:int} $properties
*/
public static function __set_state(array $properties): self
{
return self::fromIntl($properties);
}
public function result(): string
{
return $this->result;
}
public function isTransitionalDifferent(): bool
{
return $this->isTransitionalDifferent;
}
public function errors(): int
{
return $this->errors;
}
public function error(int $error): ?string
{
return $this->errorList[$error] ?? null;
}
/**
* @return array<int, string>
*/
public function errorList(): array
{
return $this->errorList;
}
}
+20
View File
@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (C) 2013 Jeremy Kendall, http://about.me/jeremykendall
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+32
View File
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Pdp;
interface PublicSuffixList extends DomainNameResolver
{
/**
* Returns PSL info for a given domain against the PSL rules for cookie domain detection.
*
* @throws SyntaxError if the domain is invalid
* @throws UnableToResolveDomain if the effective TLD can not be resolved
*/
public function getCookieDomain(Host $host): ResolvedDomainName;
/**
* Returns PSL info for a given domain against the PSL rules for ICANN domain detection.
*
* @throws SyntaxError if the domain is invalid
* @throws UnableToResolveDomain if the domain does not contain a ICANN Effective TLD
*/
public function getICANNDomain(Host $host): ResolvedDomainName;
/**
* Returns PSL info for a given domain against the PSL rules for private domain detection.
*
* @throws SyntaxError if the domain is invalid
* @throws UnableToResolveDomain if the domain does not contain a private Effective TLD
*/
public function getPrivateDomain(Host $host): ResolvedDomainName;
}
+362
View File
@@ -0,0 +1,362 @@
<?php
declare(strict_types=1);
namespace Pdp;
use Iterator;
use Stringable;
use function array_count_values;
use function array_keys;
use function array_reverse;
use function array_slice;
use function count;
use function explode;
use function filter_var;
use function implode;
use function in_array;
use function ksort;
use function preg_match;
use function rawurldecode;
use function strtolower;
use const FILTER_FLAG_IPV4;
use const FILTER_VALIDATE_IP;
final class RegisteredName implements DomainName
{
private const IDNA_2003 = 'IDNA_2003';
private const IDNA_2008 = 'IDNA_2008';
private const REGEXP_IDN_PATTERN = '/[^\x20-\x7f]/';
// Note that unreserved is purposely missing . as it is used to separate labels.
private const REGEXP_REGISTERED_NAME = '/(?(DEFINE)
(?<unreserved>[a-z0-9_~\-])
(?<sub_delims>[!$&\'()*+,;=])
(?<encoded>%[A-F0-9]{2})
(?<reg_name>(?:(?&unreserved)|(?&sub_delims)|(?&encoded)){1,63})
)
^(?:(?&reg_name)\.){0,126}(?&reg_name)\.?$/ix';
private const REGEXP_URI_DELIMITERS = '/[:\/?#\[\]@ ]/';
/** @var list<string> */
private readonly array $labels;
private readonly ?string $domain;
/**
* @throws CannotProcessHost
*/
private function __construct(private readonly string $type, DomainNameProvider|Host|Stringable|string|int|null $domain)
{
$this->domain = $this->parseDomain($domain);
$this->labels = null === $this->domain ? [] : array_reverse(explode('.', $this->domain));
}
/**
* @param array{domain:string|null, type:string} $properties
*
* @throws CannotProcessHost
*/
public static function __set_state(array $properties): self
{
return new self($properties['type'], $properties['domain']);
}
/**
* @throws CannotProcessHost
*/
public static function fromIDNA2003(DomainNameProvider|Host|Stringable|string|int|null $domain): self
{
return new self(self::IDNA_2003, $domain);
}
/**
* @throws CannotProcessHost
*/
public static function fromIDNA2008(DomainNameProvider|Host|Stringable|string|int|null $domain): self
{
return new self(self::IDNA_2008, $domain);
}
/**
* @throws CannotProcessHost
*/
private function parseDomain(DomainNameProvider|Host|Stringable|string|int|null $domain): ?string
{
return $this->parseValue(match (true) {
$domain instanceof DomainNameProvider => $domain->domain()->value(),
$domain instanceof Host => $domain->toUnicode()->value(),
default => $domain,
});
}
/**
* Parse and format the domain to ensure it is valid.
* Returns an array containing the formatted domain name labels
* and the domain transitional information.
*
* For example: parse('wWw.uLb.Ac.be') should return ['www.ulb.ac.be', ['be', 'ac', 'ulb', 'www']];.
*
* @throws SyntaxError If the host is not a domain
* @throws SyntaxError If the domain is not a host
*/
private function parseValue(Stringable|string|int|null $domain): ?string
{
if (null === $domain) {
return null;
}
if ($domain instanceof Stringable) {
$domain = (string) $domain;
}
$domain = (string) $domain;
if ('' === $domain) {
return '';
}
$res = filter_var($domain, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
if (false !== $res) {
return $res;
}
$formattedDomain = rawurldecode($domain);
return match (true) {
1 === preg_match(self::REGEXP_REGISTERED_NAME, $formattedDomain) => strtolower($formattedDomain),
// a domain name can not contain URI delimiters or space
1 === preg_match(self::REGEXP_URI_DELIMITERS, $formattedDomain) => throw SyntaxError::dueToInvalidCharacters($domain),
// if the domain name does not contain UTF-8 chars then it is malformed
1 !== preg_match(self::REGEXP_IDN_PATTERN, $formattedDomain) => throw SyntaxError::dueToMalformedValue($domain),
default => $this->domainToUnicode($this->domainToAscii($formattedDomain)),
};
}
private function domainToAscii(string $domain): string
{
return Idna::toAscii(
$domain,
self::IDNA_2003 === $this->type ? Idna::IDNA2003_ASCII : Idna::IDNA2008_ASCII
)->result();
}
private function domainToUnicode(string $domain): string
{
return Idna::toUnicode(
$domain,
self::IDNA_2003 === $this->type ? Idna::IDNA2003_UNICODE : Idna::IDNA2008_UNICODE
)->result();
}
/**
* @return Iterator<string>
*/
public function getIterator(): Iterator
{
yield from $this->labels;
}
public function isAscii(): bool
{
return null === $this->domain || 1 !== preg_match(self::REGEXP_IDN_PATTERN, $this->domain);
}
public function jsonSerialize(): ?string
{
return $this->domain;
}
public function count(): int
{
return count($this->labels);
}
public function value(): ?string
{
return $this->domain;
}
public function toString(): string
{
return (string) $this->domain;
}
public function label(int $key): ?string
{
if ($key < 0) {
$key += count($this->labels);
}
return $this->labels[$key] ?? null;
}
/**
* @return list<int>
*/
public function keys(?string $label = null): array
{
if (null === $label) {
return array_keys($this->labels);
}
return array_keys($this->labels, $label, true);
}
/**
* @return list<string>
*/
public function labels(): array
{
return $this->labels;
}
public function toAscii(): self
{
if (null === $this->domain) {
return $this;
}
$domain = $this->domainToAscii($this->domain);
if ($domain === $this->domain) {
return $this;
}
return new self($this->type, $domain);
}
public function toUnicode(): self
{
if (null === $this->domain) {
return $this;
}
$domain = $this->domainToUnicode($this->domain);
if ($domain === $this->domain) {
return $this;
}
return new self($this->type, $domain);
}
/**
* Filter a subdomain to update the domain part.
*/
private function normalize(DomainNameProvider|Host|Stringable|string|int|null $domain): ?string
{
if ($domain instanceof DomainNameProvider) {
$domain = $domain->domain();
}
if ($domain instanceof Host) {
$domain = $domain->value();
}
if (null === $domain) {
return null;
}
$domain = (string) $domain;
return match (true) {
null === $this->domain => $domain,
$this->isAscii() => $this->domainToAscii($domain),
default => $this->domainToUnicode($domain),
};
}
/**
* @throws CannotProcessHost
*/
public function prepend(DomainNameProvider|Host|Stringable|string|int|null $label): self
{
return $this->withLabel(count($this->labels), $label);
}
/**
* @throws CannotProcessHost
*/
public function append(DomainNameProvider|Host|Stringable|string|int|null $label): self
{
return $this->withLabel(- count($this->labels) - 1, $label);
}
public function withLabel(int $key, DomainNameProvider|Host|Stringable|string|int|null $label): self
{
$nbLabels = count($this->labels);
if ($key < - $nbLabels - 1 || $key > $nbLabels) {
throw SyntaxError::dueToInvalidLabelKey($this, $key);
}
if (0 > $key) {
$key = $nbLabels + $key;
}
$label = $this->normalize($label);
if (($this->labels[$key] ?? null) === $label) {
return $this;
}
$labels = $this->labels;
$labels[$key] = $label;
ksort($labels);
return new self($this->type, implode('.', array_reverse($labels)));
}
public function withoutLabel(int ...$keys): self
{
$nbLabels = count($this->labels);
foreach ($keys as &$offset) {
if (- $nbLabels > $offset || $nbLabels - 1 < $offset) {
throw SyntaxError::dueToInvalidLabelKey($this, $offset);
}
if (0 > $offset) {
$offset += $nbLabels;
}
}
unset($offset);
$deletedKeys = array_keys(array_count_values($keys));
$labels = [];
foreach ($this->labels as $offset => $label) {
if (!in_array($offset, $deletedKeys, true)) {
$labels[] = $label;
}
}
if ($labels === $this->labels) {
return $this;
}
return new self($this->type, [] === $labels ? null : implode('.', array_reverse($labels)));
}
/**
* @throws CannotProcessHost
*/
public function clear(): self
{
if (null === $this->domain) {
return $this;
}
return new self($this->type, null);
}
/**
* @throws CannotProcessHost
*/
public function slice(int $offset, ?int $length = null): self
{
$nbLabels = count($this->labels);
if ($offset < - $nbLabels || $offset > $nbLabels) {
throw SyntaxError::dueToInvalidLabelKey($this, $offset);
}
$labels = array_slice($this->labels, $offset, $length, true);
if ($labels === $this->labels) {
return $this;
}
return new self($this->type, [] === $labels ? null : implode('.', array_reverse($labels)));
}
}
+237
View File
@@ -0,0 +1,237 @@
<?php
declare(strict_types=1);
namespace Pdp;
use Stringable;
use function count;
final class ResolvedDomain implements ResolvedDomainName
{
private readonly DomainName $secondLevelDomain;
private readonly DomainName $registrableDomain;
private readonly DomainName $subDomain;
/**
* @throws CannotProcessHost
*/
private function __construct(
private readonly DomainName $domain,
private readonly EffectiveTopLevelDomain $suffix
) {
[
'registrableDomain' => $this->registrableDomain,
'secondLevelDomain' => $this->secondLevelDomain,
'subDomain' => $this->subDomain,
] = $this->parse();
}
/**
* @throws CannotProcessHost
*/
public static function fromICANN(DomainNameProvider|Host|Stringable|string|int|null $domain, int $suffixLength): self
{
$domain = self::setDomainName($domain);
return new self($domain, Suffix::fromICANN($domain->slice(0, $suffixLength)));
}
/**
* @throws CannotProcessHost
*/
public static function fromPrivate(DomainNameProvider|Host|Stringable|string|int|null $domain, int $suffixLength): self
{
$domain = self::setDomainName($domain);
return new self($domain, Suffix::fromPrivate($domain->slice(0, $suffixLength)));
}
/**
* @throws CannotProcessHost
*/
public static function fromIANA(DomainNameProvider|Host|Stringable|string|int|null $domain): self
{
$domain = self::setDomainName($domain);
return new self($domain, Suffix::fromIANA($domain->label(0)));
}
/**
* @throws CannotProcessHost
*/
public static function fromUnknown(DomainNameProvider|Host|Stringable|string|int|null $domain, int $suffixLength = 0): self
{
$domain = self::setDomainName($domain);
return new self($domain, Suffix::fromUnknown($domain->slice(0, $suffixLength)));
}
/**
* @param array{domain:DomainName, suffix:EffectiveTopLevelDomain} $properties
*
* @throws CannotProcessHost
*/
public static function __set_state(array $properties): self
{
return new self($properties['domain'], $properties['suffix']);
}
/**
* @throws CannotProcessHost
*/
private static function setDomainName(DomainNameProvider|Host|Stringable|string|int|null $domain): DomainName
{
return match (true) {
$domain instanceof DomainNameProvider => $domain->domain(),
$domain instanceof DomainName => $domain,
default => RegisteredName::fromIDNA2008($domain),
};
}
/**
* Make sure the Value Object is always in a valid state.
*
* @throws UnableToResolveDomain|CannotProcessHost If the suffix can not be attached to the domain
*
* @return array{registrableDomain: DomainName, secondLevelDomain: DomainName, subDomain: DomainName}
*/
private function parse(): array
{
$suffixValue = $this->suffix->value();
if (null === $suffixValue) {
$nullDomain = $this->domain->clear();
return [
'registrableDomain' => $nullDomain,
'secondLevelDomain' => $nullDomain,
'subDomain' => $nullDomain,
];
}
if (2 > count($this->domain)) {
throw UnableToResolveDomain::dueToUnresolvableDomain($this->domain);
}
if ($this->domain->value() === $suffixValue) {
throw UnableToResolveDomain::dueToIdenticalValue($this->domain);
}
$length = count($this->suffix);
return [
'registrableDomain' => $this->domain->slice(0, $length + 1),
'secondLevelDomain' => $this->domain->slice($length, 1),
'subDomain' => RegisteredName::fromIDNA2008($this->domain->value())->slice($length + 1),
];
}
public function count(): int
{
return count($this->domain);
}
public function domain(): DomainName
{
return $this->domain;
}
public function registrableDomain(): DomainName
{
return $this->registrableDomain;
}
public function secondLevelDomain(): DomainName
{
return $this->secondLevelDomain;
}
public function subDomain(): DomainName
{
return $this->subDomain;
}
public function suffix(): EffectiveTopLevelDomain
{
return $this->suffix;
}
public function jsonSerialize(): ?string
{
return $this->domain->jsonSerialize();
}
public function value(): ?string
{
return $this->domain->value();
}
public function toString(): string
{
return $this->domain->toString();
}
public function toAscii(): self
{
return new self($this->domain->toAscii(), $this->suffix->toAscii());
}
public function toUnicode(): self
{
return new self($this->domain->toUnicode(), $this->suffix->toUnicode());
}
/**
* @throws CannotProcessHost
*/
public function withSuffix(DomainNameProvider|Host|Stringable|string|int|null $suffix): self
{
if (!$suffix instanceof EffectiveTopLevelDomain) {
$suffix = Suffix::fromUnknown($suffix);
}
return new self(
$this->domain->slice(count($this->suffix))->append($suffix),
$suffix->normalize($this->domain)
);
}
/**
* @throws CannotProcessHost|UnableToResolveDomain
*/
public function withSubDomain(DomainNameProvider|Host|Stringable|string|int|null $subDomain): self
{
if (null === $this->suffix->value()) {
throw UnableToResolveDomain::dueToMissingRegistrableDomain($this->domain);
}
$subDomain = RegisteredName::fromIDNA2008($subDomain);
if ($this->subDomain->value() === $subDomain->value()) {
return $this;
}
return new self($this->registrableDomain->prepend($subDomain), $this->suffix);
}
/**
* @throws CannotProcessHost|UnableToResolveDomain
*/
public function withSecondLevelDomain(DomainNameProvider|Host|Stringable|string|int|null $label): self
{
if (null === $this->suffix->value()) {
throw UnableToResolveDomain::dueToMissingRegistrableDomain($this->domain);
}
$label = RegisteredName::fromIDNA2008($label);
if (1 !== count($label)) {
throw UnableToResolveDomain::dueToInvalidSecondLevelDomain($label);
}
$newRegistrableDomain = $this->registrableDomain->withoutLabel(-1)->prepend($label);
if ($newRegistrableDomain->value() === $this->registrableDomain->value()) {
return $this;
}
return new self($newRegistrableDomain->prepend($this->subDomain), $this->suffix);
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Pdp;
interface ResolvedDomainName extends Host, DomainNameProvider
{
/**
* Returns the domain effective tld component.
*/
public function suffix(): EffectiveTopLevelDomain;
/**
* Returns the second level domain component.
*/
public function secondLevelDomain(): DomainName;
/**
* Returns the registrable domain component.
*/
public function registrableDomain(): DomainName;
/**
* Returns the subdomain component.
*/
public function subDomain(): DomainName;
/**
* Returns an instance with the specified subdomain added.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the new subdomain
*
* @throws CannotProcessHost If the Sub domain can not be added to the current Domain
*/
public function withSubDomain(DomainName $subDomain): self;
/**
* Returns an instance with the specified second level domain label added.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the second level domain label
*
* @throws CannotProcessHost If the second level domain label can not be added
*/
public function withSecondLevelDomain(DomainName $label): self;
/**
* Returns an instance with the specified public suffix added.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the new public suffix
*
* If the domain already has a public suffix it will be replaced by the new value
* otherwise the public suffix content is added to or remove from the current domain.
*/
public function withSuffix(EffectiveTopLevelDomain $suffix): self;
}
+12
View File
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Pdp;
interface ResourceUri
{
public const PUBLIC_SUFFIX_LIST_URI = 'https://publicsuffix.org/list/public_suffix_list.dat';
public const TOP_LEVEL_DOMAIN_LIST_URI = 'https://data.iana.org/TLD/tlds-alpha-by-domain.txt';
}
+306
View File
@@ -0,0 +1,306 @@
<?php
declare(strict_types=1);
namespace Pdp;
use SplFileObject;
use SplTempFileObject;
use Stringable;
use function array_pop;
use function explode;
use function preg_match;
use function substr;
final class Rules implements PublicSuffixList
{
private const ICANN_DOMAINS = 'ICANN_DOMAINS';
private const PRIVATE_DOMAINS = 'PRIVATE_DOMAINS';
private const UNKNOWN_DOMAINS = 'UNKNOWN_DOMAINS';
private const REGEX_PSL_SECTION = ',^// ===(?<point>BEGIN|END) (?<type>ICANN|PRIVATE) DOMAINS===,';
private const PSL_SECTION = [
'ICANN' => [
'BEGIN' => self::ICANN_DOMAINS,
'END' => '',
],
'PRIVATE' => [
'BEGIN' => self::PRIVATE_DOMAINS,
'END' => '',
],
];
/**
* @param array{ICANN_DOMAINS: array<array>, PRIVATE_DOMAINS: array<array>} $rules
*/
private function __construct(private readonly array $rules)
{
}
/**
* Returns a new instance from a file path.
*
* @param null|resource $context
*
* @throws UnableToLoadResource If the rules can not be loaded from the path
* @throws UnableToLoadPublicSuffixList If the rules contain in the resource are invalid
*/
public static function fromPath(string $path, $context = null): self
{
return self::fromString(Stream::getContentAsString($path, $context));
}
/**
* Returns a new instance from a string.
*
* @throws UnableToLoadPublicSuffixList If the rules contain in the resource are invalid
*/
public static function fromString(Stringable|string $content): self
{
return new self(self::parse((string) $content));
}
/**
* Convert the Public Suffix List into an associative, multidimensional array.
*
* @return array{ICANN_DOMAINS: array<array>, PRIVATE_DOMAINS: array<array>}
*/
private static function parse(string $content): array
{
$rules = [self::ICANN_DOMAINS => [], self::PRIVATE_DOMAINS => []];
$section = '';
$file = new SplTempFileObject();
$file->fwrite($content);
$file->setFlags(SplFileObject::DROP_NEW_LINE | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY);
/** @var string $line */
foreach ($file as $line) {
$section = self::getSection($section, $line);
if (in_array($section, [self::PRIVATE_DOMAINS, self::ICANN_DOMAINS], true) && !str_contains($line, '//')) {
$rules[$section] = self::addRule($rules[$section], explode('.', $line));
}
}
return $rules;
}
/**
* Returns the section type for a given line.
*/
private static function getSection(string $section, string $line): string
{
if (1 === preg_match(self::REGEX_PSL_SECTION, $line, $matches)) {
return self::PSL_SECTION[$matches['type']][$matches['point']];
}
return $section;
}
/**
* Recursive method to build the array representation of the Public Suffix List.
*
* This method is based heavily on the code found in generateEffectiveTLDs.php
*
* @see https://github.com/usrflo/registered-domain-libs/blob/master/generateEffectiveTLDs.php
* A copy of the Apache License, Version 2.0, is provided with this
* distribution
*
* @param array<array> $list Initially an empty array, this eventually becomes the array representation of a
* Public Suffix List section
* @param array<string> $ruleParts One line (rule) from the Public Suffix List exploded on '.', or the remaining
* portion of that array during recursion
*
* @throws UnableToLoadPublicSuffixList if the domain name can not be converted using IDN to ASCII algorithm
*
* @return array<array>
*/
private static function addRule(array $list, array $ruleParts): array
{
// Adheres to canonicalization rule from the "Formal Algorithm" section
// of https://publicsuffix.org/list/
// "The domain and all rules must be canonicalized in the normal way
// for hostnames - lower-case, Punycode (RFC 3492)."
try {
/** @var string $line */
$line = array_pop($ruleParts);
$rule = Idna::toAscii($line, Idna::IDNA2008_ASCII)->result();
} catch (CannotProcessHost $exception) {
throw UnableToLoadPublicSuffixList::dueToInvalidRule($line ?? null, $exception);
}
$isDomain = true;
if (str_starts_with($rule, '!')) {
$rule = substr($rule, 1);
$isDomain = false;
}
$list[$rule] = $list[$rule] ?? ($isDomain ? [] : ['!' => '']);
if ($isDomain && [] !== $ruleParts) {
/** @var array<array-key, array> $tmpList */
$tmpList = $list[$rule];
$list[$rule] = self::addRule($tmpList, $ruleParts);
}
return $list;
}
/**
* @param array{rules:array{ICANN_DOMAINS: array<array>, PRIVATE_DOMAINS: array<array>}} $properties
*/
public static function __set_state(array $properties): self
{
return new self($properties['rules']);
}
/**
* @param int|DomainNameProvider|Host|string|Stringable|null $host a type that supports instantiating a Domain from.
*/
public function resolve($host): ResolvedDomainName
{
try {
return $this->getCookieDomain($host);
} catch (UnableToResolveDomain $exception) {
return ResolvedDomain::fromUnknown($exception->domain());
} catch (SyntaxError) {
return ResolvedDomain::fromUnknown(Domain::fromIDNA2008(null));
}
}
/**
* @param int|DomainNameProvider|Host|string|Stringable|null $host the domain value
*/
public function getCookieDomain($host): ResolvedDomainName
{
$domain = $this->validateDomain($host);
[$suffixLength, $section] = $this->resolveSuffix($domain, self::UNKNOWN_DOMAINS);
return match (true) {
self::ICANN_DOMAINS === $section => ResolvedDomain::fromICANN($domain, $suffixLength),
self::PRIVATE_DOMAINS === $section => ResolvedDomain::fromPrivate($domain, $suffixLength),
default => ResolvedDomain::fromUnknown($domain, $suffixLength),
};
}
/**
* @param int|DomainNameProvider|Host|string|Stringable|null $host a type that supports instantiating a Domain from.
*/
public function getICANNDomain($host): ResolvedDomainName
{
$domain = $this->validateDomain($host);
[$suffixLength, $section] = $this->resolveSuffix($domain, self::ICANN_DOMAINS);
if (self::ICANN_DOMAINS !== $section) {
throw UnableToResolveDomain::dueToMissingSuffix($domain, 'ICANN');
}
return ResolvedDomain::fromICANN($domain, $suffixLength);
}
/**
* @param int|DomainNameProvider|Host|string|Stringable|null $host a type that supports instantiating a Domain from.
*/
public function getPrivateDomain($host): ResolvedDomainName
{
$domain = $this->validateDomain($host);
[$suffixLength, $section] = $this->resolveSuffix($domain, self::PRIVATE_DOMAINS);
if (self::PRIVATE_DOMAINS !== $section) {
throw UnableToResolveDomain::dueToMissingSuffix($domain, 'private');
}
return ResolvedDomain::fromPrivate($domain, $suffixLength);
}
/**
* Assert the domain is valid and is resolvable.
*
* @throws SyntaxError If the domain is invalid
* @throws UnableToResolveDomain If the domain can not be resolved
*/
private function validateDomain(int|DomainNameProvider|Host|string|Stringable|null $domain): DomainName
{
if ($domain instanceof DomainNameProvider) {
$domain = $domain->domain();
}
if (!$domain instanceof DomainName) {
$domain = Domain::fromIDNA2008($domain);
}
if ('' === $domain->label(0)) {
throw UnableToResolveDomain::dueToUnresolvableDomain($domain);
}
return $domain;
}
/**
* Returns the length and the section of the resolved effective top level domain.
*
* @param Rules::UNKNOWN_DOMAINS|Rules::ICANN_DOMAINS|Rules::PRIVATE_DOMAINS $section
*
* @return array{0: int, 1:Rules::UNKNOWN_DOMAINS|Rules::ICANN_DOMAINS|Rules::PRIVATE_DOMAINS}
*/
private function resolveSuffix(DomainName $domain, string $section): array
{
$icannSuffixLength = $this->getPublicSuffixLengthFromSection($domain, self::ICANN_DOMAINS);
if (1 > $icannSuffixLength) {
return [1, self::UNKNOWN_DOMAINS];
}
if (self::ICANN_DOMAINS === $section) {
return [$icannSuffixLength, self::ICANN_DOMAINS];
}
$privateSuffixLength = $this->getPublicSuffixLengthFromSection($domain, self::PRIVATE_DOMAINS);
if ($privateSuffixLength > $icannSuffixLength) {
return [$privateSuffixLength, self::PRIVATE_DOMAINS];
}
return [$icannSuffixLength, self::ICANN_DOMAINS];
}
/**
* Returns the public suffix label count for a domain name according to a PSL section.
*/
private function getPublicSuffixLengthFromSection(DomainName $domain, string $section): int
{
$rules = $this->rules[$section];
$labelCount = 0;
foreach ($domain->toAscii() as $label) {
//match exception rule
if (isset($rules[$label]['!'])) {
break;
}
//match wildcard rule
if (array_key_exists('*', $rules)) {
++$labelCount;
break;
}
//no match found
if (!array_key_exists($label, $rules)) {
// Suffix MUST be fully matched else no suffix is found for private domain
if (self::PRIVATE_DOMAINS === $section && self::hasRemainingRules($rules)) {
$labelCount = 0;
}
break;
}
++$labelCount;
/** @var array<array-key, mixed> $rules */
$rules = $rules[$label];
}
return $labelCount;
}
private static function hasRemainingRules(array $rules): bool
{
foreach ($rules as $rule) {
if ([] !== $rule) {
return true;
}
}
return false;
}
}
+48
View File
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Pdp\Storage;
use DateInterval;
use DateTimeInterface;
use Pdp\ResourceUri;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\SimpleCache\CacheInterface;
use Stringable;
final class PsrStorageFactory implements
ResourceUri,
PublicSuffixListStorageFactory,
TopLevelDomainListStorageFactory
{
public function __construct(
private readonly CacheInterface $cache,
private readonly ClientInterface $client,
private readonly RequestFactoryInterface $requestFactory
) {
}
/**
* @param DateInterval|DateTimeInterface|Stringable|int|string|null $cacheTtl
*/
public function createPublicSuffixListStorage(string $cachePrefix = '', $cacheTtl = null): PublicSuffixListStorage
{
return new RulesStorage(
new PublicSuffixListPsr16Cache($this->cache, $cachePrefix, $cacheTtl),
new PublicSuffixListPsr18Client($this->client, $this->requestFactory)
);
}
/**
* @param DateInterval|DateTimeInterface|Stringable|int|string|null $cacheTtl
*/
public function createTopLevelDomainListStorage(string $cachePrefix = '', $cacheTtl = null): TopLevelDomainListStorage
{
return new TopLevelDomainsStorage(
new TopLevelDomainListPsr16Cache($this->cache, $cachePrefix, $cacheTtl),
new TopLevelDomainListPsr18Client($this->client, $this->requestFactory)
);
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Pdp\Storage;
use Pdp\PublicSuffixList;
interface PublicSuffixListCache
{
/**
* Retrieves the Public Suffix List from Cache.
*/
public function fetch(string $uri): ?PublicSuffixList;
/**
* Caches the Public Suffix List.
*
* If a local cache already exists, it will be overwritten.
*
* Returns true if the action was successful, false otherwise
*/
public function remember(string $uri, PublicSuffixList $publicSuffixList): bool;
/**
* Deletes the Public Suffix List entry.
*
* Returns true if the action was successful, false otherwise
*/
public function forget(string $uri): bool;
}
+16
View File
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Pdp\Storage;
use Pdp\PublicSuffixList;
use Pdp\UnableToLoadPublicSuffixList;
interface PublicSuffixListClient
{
/**
* @throws UnableToLoadPublicSuffixList
*/
public function get(string $uri): PublicSuffixList;
}
@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Pdp\Storage;
use DateInterval;
use DateTimeInterface;
use Pdp\PublicSuffixList;
use Psr\SimpleCache\CacheException;
use Psr\SimpleCache\CacheInterface;
use Stringable;
use Throwable;
use function md5;
use function strtolower;
final class PublicSuffixListPsr16Cache implements PublicSuffixListCache
{
private readonly ?DateInterval $cacheTtl;
public function __construct(
private readonly CacheInterface $cache,
private readonly string $cachePrefix = '',
DateInterval|DateTimeInterface|Stringable|int|string|null $cacheTtl = null
) {
$this->cacheTtl = TimeToLive::convert($cacheTtl);
}
public function fetch(string $uri): ?PublicSuffixList
{
$cacheKey = $this->cacheKey($uri);
$publicSuffixList = $this->cache->get($cacheKey);
if (null === $publicSuffixList) {
return null;
}
if (!$publicSuffixList instanceof PublicSuffixList) {
$this->cache->delete($cacheKey);
return null;
}
return $publicSuffixList;
}
/**
* Returns the cache key according to the source URL.
*/
private function cacheKey(string $str): string
{
return $this->cachePrefix.md5(strtolower($str));
}
public function remember(string $uri, PublicSuffixList $publicSuffixList): bool
{
try {
return $this->cache->set($this->cacheKey($uri), $publicSuffixList, $this->cacheTtl);
} catch (Throwable $exception) {
if ($exception instanceof CacheException) {
return false;
}
throw $exception;
}
}
public function forget(string $uri): bool
{
return $this->cache->delete($this->cacheKey($uri));
}
}
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Pdp\Storage;
use Pdp\PublicSuffixList;
use Pdp\Rules;
use Pdp\UnableToLoadResource;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
final class PublicSuffixListPsr18Client implements PublicSuffixListClient
{
public function __construct(
private readonly ClientInterface $client,
private readonly RequestFactoryInterface $requestFactory
) {
}
public function get(string $uri): PublicSuffixList
{
$request = $this->requestFactory->createRequest('GET', $uri);
try {
$response = $this->client->sendRequest($request);
} catch (ClientExceptionInterface $exception) {
throw UnableToLoadResource::dueToUnavailableService($uri, $exception);
}
if (400 <= $response->getStatusCode()) {
throw UnableToLoadResource::dueToUnexpectedStatusCode($uri, $response->getStatusCode());
}
return Rules::fromString($response->getBody());
}
}
@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Pdp\Storage;
interface PublicSuffixListStorage extends PublicSuffixListClient
{
public function delete(string $uri): bool;
}
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Pdp\Storage;
use DateInterval;
use DateTimeInterface;
use Stringable;
interface PublicSuffixListStorageFactory
{
/**
* @param DateInterval|DateTimeInterface|Stringable|int|string|null $cacheTtl storage TTL object should implement the __toString method
*/
public function createPublicSuffixListStorage(string $cachePrefix = '', $cacheTtl = null): PublicSuffixListStorage;
}
+35
View File
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Pdp\Storage;
use Pdp\PublicSuffixList;
final class RulesStorage implements PublicSuffixListStorage
{
public function __construct(
private readonly PublicSuffixListCache $cache,
private readonly PublicSuffixListClient $client
) {
}
public function get(string $uri): PublicSuffixList
{
$publicSuffixList = $this->cache->fetch($uri);
if (null !== $publicSuffixList) {
return $publicSuffixList;
}
$publicSuffixList = $this->client->get($uri);
$this->cache->remember($uri, $publicSuffixList);
return $publicSuffixList;
}
public function delete(string $uri): bool
{
return $this->cache->forget($uri);
}
}
+103
View File
@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace Pdp\Storage;
use DateInterval;
use DateTimeImmutable;
use DateTimeInterface;
use InvalidArgumentException;
use Stringable;
use Throwable;
use function filter_var;
use const FILTER_VALIDATE_INT;
/**
* @internal
*/
final class TimeToLive
{
public static function fromDurationString(string $duration): DateInterval
{
try {
set_error_handler(fn () => true);
$interval = DateInterval::createFromDateString($duration);
restore_error_handler();
if (!$interval instanceof DateInterval) {
throw new InvalidArgumentException(
'The ttl value "'.$duration.'" can not be parsable by `DateInterval::createFromDateString`.'
);
}
} catch (Throwable $exception) {
if (!$exception instanceof InvalidArgumentException) {
throw new InvalidArgumentException(
'The ttl value "'.$duration.'" can not be parsable by `DateInterval::createFromDateString`.',
0,
$exception
);
}
throw $exception;
}
return $interval;
}
public static function until(DateTimeInterface $date): DateInterval
{
return (new DateTimeImmutable('NOW', $date->getTimezone()))->diff($date, false);
}
/**
* Returns a DateInterval relative to the current date and time.
*
* DEPRECATION WARNING! This method will be removed in the next major point release
*
* @deprecated 6.1.0 deprecated
* @codeCoverageIgnore
* @see TimeToLive::until
*/
public static function fromDateTimeInterface(DateTimeInterface $date): DateInterval
{
return self::until($date);
}
/**
* Returns a DateInterval from string parsing.
*
* DEPRECATION WARNING! This method will be removed in the next major point release
*
* @deprecated 6.1.0 deprecated
* @see TimeToLive::fromDurationString
* @codeCoverageIgnore
*
* @throws InvalidArgumentException if the value can not be parsable
*
*/
public static function fromScalar(Stringable|int|string $duration): DateInterval
{
return self::fromDurationString((string) $duration);
}
/**
* Convert the input data into a DateInterval object or null.
*
* @throws InvalidArgumentException if the value can not be computed
*/
public static function convert(DateInterval|DateTimeInterface|Stringable|int|string|null $ttl): ?DateInterval
{
if ($ttl instanceof Stringable) {
$ttl = (string) $ttl;
}
return match (true) {
$ttl instanceof DateInterval || null === $ttl => $ttl,
$ttl instanceof DateTimeInterface => self::until($ttl),
is_int($ttl) => self::fromDurationString($ttl.' seconds'),
false !== ($seconds = filter_var($ttl, FILTER_VALIDATE_INT)) => self::fromDurationString($seconds.' seconds'),
default => self::fromDurationString($ttl),
};
}
}
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Pdp\Storage;
use Pdp\TopLevelDomainList;
interface TopLevelDomainListCache
{
/**
* Retrieves the Top Level Domain List from the Cache.
*/
public function fetch(string $uri): ?TopLevelDomainList;
/**
* Cache the Top Level Domain List.
*
* If a local cache already exists, it will be overwritten.
*
* Returns true if the action was successful, false otherwise
*/
public function remember(string $uri, TopLevelDomainList $topLevelDomainList): bool;
/**
* Deletes the Top Level Domain List entry.
*
* Returns true if the action was successful, false otherwise
*/
public function forget(string $uri): bool;
}
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Pdp\Storage;
use Pdp\TopLevelDomainList;
use Pdp\UnableToLoadTopLevelDomainList;
interface TopLevelDomainListClient
{
/**
* @throws UnableToLoadTopLevelDomainList
*/
public function get(string $uri): TopLevelDomainList;
}
@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Pdp\Storage;
use DateInterval;
use DateTimeInterface;
use Pdp\TopLevelDomainList;
use Psr\SimpleCache\CacheException;
use Psr\SimpleCache\CacheInterface;
use Stringable;
use Throwable;
use function md5;
use function strtolower;
final class TopLevelDomainListPsr16Cache implements TopLevelDomainListCache
{
private readonly ?DateInterval $cacheTtl;
public function __construct(
private readonly CacheInterface $cache,
private readonly string $cachePrefix = '',
DateInterval|DateTimeInterface|Stringable|int|string|null $cacheTtl = null
) {
$this->cacheTtl = TimeToLive::convert($cacheTtl);
}
public function fetch(string $uri): ?TopLevelDomainList
{
$cacheKey = $this->cacheKey($uri);
$topLevelDomainList = $this->cache->get($cacheKey);
if (null === $topLevelDomainList) {
return null;
}
if (!$topLevelDomainList instanceof TopLevelDomainList) {
$this->cache->delete($cacheKey);
return null;
}
return $topLevelDomainList;
}
/**
* Returns the cache key according to the source URL.
*/
private function cacheKey(string $str): string
{
return $this->cachePrefix.md5(strtolower($str));
}
public function remember(string $uri, TopLevelDomainList $topLevelDomainList): bool
{
try {
return $this->cache->set($this->cacheKey($uri), $topLevelDomainList, $this->cacheTtl);
} catch (Throwable $exception) {
if ($exception instanceof CacheException) {
return false;
}
throw $exception;
}
}
public function forget(string $uri): bool
{
return $this->cache->delete($this->cacheKey($uri));
}
}
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Pdp\Storage;
use Pdp\TopLevelDomainList;
use Pdp\TopLevelDomains;
use Pdp\UnableToLoadResource;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
final class TopLevelDomainListPsr18Client implements TopLevelDomainListClient
{
public function __construct(
private readonly ClientInterface $client,
private readonly RequestFactoryInterface $requestFactory
) {
}
public function get(string $uri): TopLevelDomainList
{
$request = $this->requestFactory->createRequest('GET', $uri);
try {
$response = $this->client->sendRequest($request);
} catch (ClientExceptionInterface $exception) {
throw UnableToLoadResource::dueToUnavailableService($uri, $exception);
}
if (400 <= $response->getStatusCode()) {
throw UnableToLoadResource::dueToUnexpectedStatusCode($uri, $response->getStatusCode());
}
return TopLevelDomains::fromString($response->getBody());
}
}
@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Pdp\Storage;
interface TopLevelDomainListStorage extends TopLevelDomainListClient
{
public function delete(string $uri): bool;
}
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Pdp\Storage;
use DateInterval;
use DateTimeInterface;
use Stringable;
interface TopLevelDomainListStorageFactory
{
/**
* @param DateInterval|DateTimeInterface|Stringable|int|string|null $cacheTtl storage TTL object should implement the __toString method
*/
public function createTopLevelDomainListStorage(string $cachePrefix = '', $cacheTtl = null): TopLevelDomainListStorage;
}
+35
View File
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Pdp\Storage;
use Pdp\TopLevelDomainList;
final class TopLevelDomainsStorage implements TopLevelDomainListStorage
{
public function __construct(
private readonly TopLevelDomainListCache $cache,
private readonly TopLevelDomainListClient $client
) {
}
public function get(string $uri): TopLevelDomainList
{
$topLevelDomains = $this->cache->fetch($uri);
if (null !== $topLevelDomains) {
return $topLevelDomains;
}
$topLevelDomains = $this->client->get($uri);
$this->cache->remember($uri, $topLevelDomains);
return $topLevelDomains;
}
public function delete(string $uri): bool
{
return $this->cache->forget($uri);
}
}
+54
View File
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Pdp;
use function fclose;
use function fopen;
use function stream_get_contents;
/**
* @internal
*/
final class Stream
{
/**
* Returns the content as string from a path.
*
* @param null|resource $context
*
* @throws UnableToLoadPublicSuffixList If the rules can not be loaded from the path
*/
public static function getContentAsString(string $path, $context = null): string
{
$stream = self::fromPath($path, $context);
if (false === $stream) {
throw UnableToLoadResource::dueToInvalidUri($path);
}
/** @var string $content */
$content = stream_get_contents($stream);
fclose($stream);
return $content;
}
/**
* @param null|resource $context
*
* @return false|resource
*/
private static function fromPath(string $path, $context = null)
{
$args = [$path, 'r'];
if (null !== $context) {
$args = [...$args, false, $context];
}
set_error_handler(fn (int $errno, string $errstr, string $errfile, int $errline) => true);
$stream = fopen(...$args);
restore_error_handler();
return $stream;
}
}
+171
View File
@@ -0,0 +1,171 @@
<?php
declare(strict_types=1);
namespace Pdp;
use Stringable;
use function count;
use function in_array;
final class Suffix implements EffectiveTopLevelDomain
{
private const ICANN = 'ICANN';
private const PRIVATE = 'PRIVATE';
private const IANA = 'IANA';
private const UNKNOWN = 'UNKNOWN';
private function __construct(
private readonly DomainName $domain,
private readonly string $section
) {
}
/**
* @param array{domain:DomainName, section:Suffix::ICANN|Suffix::PRIVATE|Suffix::IANA|Suffix::UNKNOWN} $properties
*/
public static function __set_state(array $properties): self
{
return new self($properties['domain'], $properties['section']);
}
/**
* @throws CannotProcessHost
*/
public static function fromICANN(DomainNameProvider|Host|Stringable|string|int|null $domain): self
{
$domain = self::setDomainName($domain);
if (1 > count($domain)) {
throw SyntaxError::dueToInvalidSuffix($domain, self::ICANN);
}
return new self($domain, self::ICANN);
}
/**
* @throws CannotProcessHost
*/
public static function fromPrivate(DomainNameProvider|Host|Stringable|string|int|null $domain): self
{
$domain = self::setDomainName($domain);
if (1 > count($domain)) {
throw SyntaxError::dueToInvalidSuffix($domain, self::PRIVATE);
}
return new self($domain, self::PRIVATE);
}
/**
* @throws CannotProcessHost
*/
public static function fromIANA(DomainNameProvider|Host|Stringable|string|int|null $domain): self
{
$domain = self::setDomainName($domain);
if (1 !== count($domain)) {
throw SyntaxError::dueToInvalidSuffix($domain, self::IANA);
}
return new self($domain, self::IANA);
}
/**
* @throws CannotProcessHost
*/
public static function fromUnknown(DomainNameProvider|Host|Stringable|string|int|null $domain): self
{
return new self(self::setDomainName($domain), self::UNKNOWN);
}
/**
* @throws CannotProcessHost
*/
private static function setDomainName(int|DomainNameProvider|Host|string|Stringable|null $domain): DomainName
{
if ($domain instanceof DomainNameProvider) {
$domain = $domain->domain();
}
if (!$domain instanceof DomainName) {
$domain = RegisteredName::fromIDNA2008($domain);
}
if ('' === $domain->label(0)) {
throw SyntaxError::dueToInvalidSuffix($domain);
}
return $domain;
}
public function isKnown(): bool
{
return self::UNKNOWN !== $this->section;
}
public function isIANA(): bool
{
return self::IANA === $this->section;
}
public function isPublicSuffix(): bool
{
return in_array($this->section, [self::ICANN, self::PRIVATE], true);
}
public function isICANN(): bool
{
return self::ICANN === $this->section;
}
public function isPrivate(): bool
{
return self::PRIVATE === $this->section;
}
public function domain(): DomainName
{
return $this->domain;
}
public function count(): int
{
return count($this->domain);
}
public function jsonSerialize(): ?string
{
return $this->domain->jsonSerialize();
}
public function value(): ?string
{
return $this->domain->value();
}
public function toString(): string
{
return $this->domain->toString();
}
public function toAscii(): self
{
return new self($this->domain->toAscii(), $this->section);
}
public function toUnicode(): self
{
return new self($this->domain->toUnicode(), $this->section);
}
/**
* @throws CannotProcessHost
*/
public function normalize(DomainName $domain): self
{
$newDomain = $domain->clear()->append($this->toUnicode());
if ($domain->isAscii()) {
$newDomain = $newDomain->toAscii();
}
return new self($newDomain, $this->section);
}
}
+54
View File
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Pdp;
use InvalidArgumentException;
final class SyntaxError extends InvalidArgumentException implements CannotProcessHost
{
private function __construct(string $message, private readonly ?IdnaInfo $idnaInfo = null)
{
parent::__construct($message);
}
public static function dueToInvalidCharacters(string $domain): self
{
return new self('The host `'.$domain.'` is invalid: it contains invalid characters.');
}
public static function dueToMalformedValue(string $domain): self
{
return new self('The host `'.$domain.'` is malformed; Verify its length and/or characters.');
}
public static function dueToIDNAError(string $domain, IdnaInfo $idnaInfo): self
{
return new self('The host `'.$domain.'` is invalid for IDN conversion.', $idnaInfo);
}
public static function dueToInvalidSuffix(Host $publicSuffix, string $type = ''): self
{
if ('' === $type) {
return new self('The suffix `"'.($publicSuffix->value() ?? 'NULL').'"` is invalid.');
}
return new self('The suffix `"'.($publicSuffix->value() ?? 'NULL').'"` is an invalid `'.$type.'` suffix.');
}
public static function dueToUnsupportedType(string $domain): self
{
return new self('The domain `'.$domain.'` is invalid: this is an IPv4 host.');
}
public static function dueToInvalidLabelKey(Host $domain, int $key): self
{
return new self('the given key `'.$key.'` is invalid for the domain `'.($domain->value() ?? 'NULL').'`.');
}
public function idnaInfo(): ?IdnaInfo
{
return $this->idnaInfo;
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Pdp;
use Countable;
use DateTimeImmutable;
use Iterator;
use IteratorAggregate;
/**
* @extends IteratorAggregate<string>
*/
interface TopLevelDomainList extends Countable, DomainNameResolver, IteratorAggregate
{
/**
* Returns the Version ID.
*/
public function version(): string;
/**
* Returns the List Last Modified Date.
*/
public function lastUpdated(): DateTimeImmutable;
public function count(): int;
/**
* Tells whether the list is empty.
*/
public function isEmpty(): bool;
/**
* @return Iterator<string>
*/
public function getIterator(): Iterator;
/**
* Returns PSL info for a given domain against the PSL rules for ICANN domain detection.
*
* @throws SyntaxError if the domain is invalid
* @throws UnableToResolveDomain if the domain does not contain a IANA Effective TLD
*/
public function getIANADomain(Host $host): ResolvedDomainName;
}
+226
View File
@@ -0,0 +1,226 @@
<?php
declare(strict_types=1);
namespace Pdp;
use DateTimeImmutable;
use Iterator;
use SplFileObject;
use SplTempFileObject;
use Stringable;
use function count;
use function in_array;
use function preg_match;
use function trim;
final class TopLevelDomains implements TopLevelDomainList
{
private const IANA_DATE_FORMAT = 'D M d H:i:s Y e';
private const REGEXP_HEADER_LINE = '/^\# Version (?<version>\d+), Last Updated (?<date>.*?)$/';
/**
* @param array<string, int> $records
*/
private function __construct(
private readonly array $records,
private readonly string $version,
private readonly DateTimeImmutable $lastUpdated
) {
}
/**
* Returns a new instance from a file path.
*
* @param null|resource $context
*
* @throws UnableToLoadResource If the rules can not be loaded from the path
* @throws UnableToLoadTopLevelDomainList If the content is invalid or can not be correctly parsed and converted
*/
public static function fromPath(string $path, $context = null): self
{
return self::fromString(Stream::getContentAsString($path, $context));
}
/**
* Returns a new instance from a string.
*
* @throws UnableToLoadTopLevelDomainList if the content is invalid or can not be correctly parsed and converted
*/
public static function fromString(Stringable|string $content): self
{
$data = self::parse((string) $content);
return new self($data['records'], $data['version'], $data['lastUpdated']);
}
/**
* Converts the IANA Top Level Domain List into a TopLevelDomains associative array.
*
* @throws UnableToLoadTopLevelDomainList if the content is invalid or can not be correctly parsed and converted
*
* @return array{version:string, lastUpdated:DateTimeImmutable, records:array<string,int>}
*/
public static function parse(string $content): array
{
$data = [];
$file = new SplTempFileObject();
$file->fwrite($content);
$file->setFlags(SplFileObject::DROP_NEW_LINE | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY);
/** @var string $line */
foreach ($file as $line) {
$line = trim($line);
if ([] === $data) {
$data = self::extractHeader($line) + ['records' => []];
continue;
}
if (!str_contains($line, '#')) {
$data['records'][self::extractRootZone($line)] = 1;
continue;
}
throw UnableToLoadTopLevelDomainList::dueToInvalidLine($line);
}
if (isset($data['version'], $data['lastUpdated'], $data['records'])) {
return $data;
}
throw UnableToLoadTopLevelDomainList::dueToFailedConversion();
}
/**
* Extract IANA Top Level Domain List header info.
*
* @throws UnableToLoadTopLevelDomainList if the Header line is invalid
*
* @return array{version:string, lastUpdated:DateTimeImmutable}
*/
private static function extractHeader(string $content): array
{
if (1 !== preg_match(self::REGEXP_HEADER_LINE, $content, $matches)) {
throw UnableToLoadTopLevelDomainList::dueToInvalidVersionLine($content);
}
/** @var DateTimeImmutable $date */
$date = DateTimeImmutable::createFromFormat(self::IANA_DATE_FORMAT, $matches['date']);
return [
'version' => $matches['version'],
'lastUpdated' => $date,
];
}
/**
* Extract IANA Root Zone.
*
* @throws UnableToLoadTopLevelDomainList If the Top Level Domain is invalid
*/
private static function extractRootZone(string $content): string
{
try {
$tld = Suffix::fromIANA($content);
} catch (CannotProcessHost $exception) {
throw UnableToLoadTopLevelDomainList::dueToInvalidTopLevelDomain($content, $exception);
}
return $tld->toAscii()->toString();
}
/**
* @param array{records:array<string, int>, version:string, lastUpdated:DateTimeImmutable} $properties
*/
public static function __set_state(array $properties): self
{
return new self($properties['records'], $properties['version'], $properties['lastUpdated']);
}
public function version(): string
{
return $this->version;
}
public function lastUpdated(): DateTimeImmutable
{
return $this->lastUpdated;
}
public function count(): int
{
return count($this->records);
}
public function isEmpty(): bool
{
return [] === $this->records;
}
/**
* @return Iterator<string>
*/
public function getIterator(): Iterator
{
yield from array_keys($this->records);
}
/**
* @param int|DomainNameProvider|Host|string|Stringable|null $host a type that supports instantiating a Domain from.
*/
public function resolve($host): ResolvedDomainName
{
try {
$domain = $this->validateDomain($host);
if ($this->containsTopLevelDomain($domain)) {
return ResolvedDomain::fromIANA($domain);
}
return ResolvedDomain::fromUnknown($domain);
} catch (UnableToResolveDomain $exception) {
return ResolvedDomain::fromUnknown($exception->domain());
} catch (SyntaxError $exception) {
return ResolvedDomain::fromUnknown(null);
}
}
/**
* Assert the domain is valid and is resolvable.
*
* @throws SyntaxError If the domain is invalid
* @throws UnableToResolveDomain If the domain can not be resolved
*/
private function validateDomain(int|DomainNameProvider|Host|string|Stringable|null $domain): DomainName
{
if ($domain instanceof DomainNameProvider) {
$domain = $domain->domain();
}
if (!$domain instanceof DomainName) {
$domain = Domain::fromIDNA2008($domain);
}
$label = $domain->label(0);
if (in_array($label, [null, ''], true)) {
throw UnableToResolveDomain::dueToUnresolvableDomain($domain);
}
return $domain;
}
private function containsTopLevelDomain(DomainName $domain): bool
{
return isset($this->records[$domain->toAscii()->label(0)]);
}
/**
* @param int|DomainNameProvider|Host|string|Stringable|null $host a domain in a type that can be converted into a DomainInterface instance
*/
public function getIANADomain($host): ResolvedDomainName
{
$domain = $this->validateDomain($host);
if (!$this->containsTopLevelDomain($domain)) {
throw UnableToResolveDomain::dueToMissingSuffix($domain, 'IANA');
}
return ResolvedDomain::fromIANA($domain);
}
}
+16
View File
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Pdp;
use InvalidArgumentException;
use Throwable;
final class UnableToLoadPublicSuffixList extends InvalidArgumentException implements CannotProcessHost
{
public static function dueToInvalidRule(?string $line, Throwable $exception): self
{
return new self('The following rule "'.($line ?? 'NULL').'" could not be processed because it is invalid.', 0, $exception);
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Pdp;
use RuntimeException;
use Throwable;
final class UnableToLoadResource extends RuntimeException implements CannotProcessHost
{
public static function dueToInvalidUri(string $uri): self
{
return new self('`'.$uri.'`: failed to open stream; No such URI, file or directory.');
}
public static function dueToUnavailableService(string $uri, Throwable $exception): self
{
return new self('Could not access the URI: `'.$uri.'`.', 0, $exception);
}
public static function dueToUnexpectedStatusCode(string $uri, int $statusCode): self
{
return new self('Invalid response from URI: `'.$uri.'`.', $statusCode);
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Pdp;
use InvalidArgumentException;
use Throwable;
final class UnableToLoadTopLevelDomainList extends InvalidArgumentException implements CannotProcessHost
{
public static function dueToInvalidTopLevelDomain(string $content, ?Throwable $exception = null): self
{
return new self('Invalid Top Level Domain: '.$content, 0, $exception);
}
public static function dueToInvalidVersionLine(string $line): self
{
return new self('Invalid Version line: '.$line);
}
public static function dueToFailedConversion(): self
{
return new self('Invalid content: Top Level Domain List conversion failed.');
}
public static function dueToInvalidLine(string $line): self
{
return new self('Invalid line content: '.$line);
}
}
+48
View File
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Pdp;
use InvalidArgumentException;
final class UnableToResolveDomain extends InvalidArgumentException implements CannotProcessHost
{
private ?DomainName $domain;
private function __construct(string $message, ?DomainName $domain = null)
{
parent::__construct($message);
$this->domain = $domain;
}
public static function dueToInvalidSecondLevelDomain(DomainName $domain): self
{
return new self('The submitted Second Level Domain is invalid `'.($domain->value() ?? 'NULL').'`; it should contains only one label.', $domain);
}
public static function dueToIdenticalValue(DomainName $domain): self
{
return new self('The public suffix and the domain name are identical `'.$domain->toString().'`.', $domain);
}
public static function dueToMissingSuffix(DomainName $domain, string $type): self
{
return new self('The domain "'.($domain->value() ?? 'NULL').'" does not contain a "'.$type.'" TLD.', $domain);
}
public static function dueToUnresolvableDomain(DomainName $domain): self
{
return new self('The domain "'.($domain->value() ?? 'NULL').'" can not contain a public suffix.', $domain);
}
public static function dueToMissingRegistrableDomain(DomainName $domain): self
{
return new self('A subdomain can not be added to a domain "'.($domain->value() ?? 'NULL').'" without a registrable domain part.', $domain);
}
public function domain(): ?DomainName
{
return $this->domain;
}
}
+97
View File
@@ -0,0 +1,97 @@
{
"name": "jeremykendall/php-domain-parser",
"description": "Public Suffix List and IANA Root Zone Database based Domain parsing implemented in PHP.",
"homepage": "https://github.com/jeremykendall/php-domain-parser",
"support": {
"issues": "https://github.com/jeremykendall/php-domain-parser/issues",
"source": "https://github.com/jeremykendall/php-domain-parser"
},
"license": "MIT",
"authors": [
{
"name": "Jeremy Kendall",
"homepage": "https://about.me/jeremykendall",
"role": "Developer"
},
{
"name": "Ignace Nyamagana Butera",
"homepage": "https://nyamsprod.com",
"role": "Developer"
},
{
"name": "Contributors",
"homepage": "https://github.com/jeremykendall/php-domain-parser/graphs/contributors"
}
],
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/nyamsprod"
}
],
"keywords": [
"Public Suffix List",
"Top Level Domains",
"domain parsing",
"icann",
"iana",
"idn",
"tld",
"psl"
],
"require": {
"php": "^8.1",
"ext-filter": "*",
"ext-intl": "*",
"ext-json": "*"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.53.0",
"guzzlehttp/guzzle": "^7.8.1",
"guzzlehttp/psr7": "^1.6 || ^2.6.2",
"phpstan/phpstan": "^1.10.66",
"phpstan/phpstan-phpunit": "^1.3.16",
"phpstan/phpstan-strict-rules": "^1.5.3",
"phpunit/phpunit": "^10.5.15 || ^11.1.1",
"psr/http-factory": "^1.0.2",
"psr/simple-cache": "^1.0.1",
"symfony/cache": "^v5.0.0 || ^6.4.6"
},
"suggest": {
"psr/http-client-implementation": "To use the storage functionnality which depends on PSR-18",
"psr/http-factory-implementation": "To use the storage functionnality which depends on PSR-17",
"psr/simple-cache-implementation": "To use the storage functionnality which depends on PSR-16",
"league/uri": "To parse URL and validate host"
},
"autoload": {
"psr-4": {
"Pdp\\": "src/"
}
},
"scripts": {
"phpcs": "PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer fix -vvv --diff --dry-run --allow-risky=yes --ansi",
"phpcs:fix": "php-cs-fixer fix -vvv --allow-risky=yes --ansi",
"phpstan": "phpstan analyse -l max -c phpstan.neon src --memory-limit=256M --ansi",
"phpunit": "XDEBUG_MODE=coverage phpunit --coverage-text",
"test": [
"@phpunit",
"@phpstan",
"@phpcs"
]
},
"scripts-descriptions": {
"phpcs": "Runs coding style test suite",
"phpcs:fix": "Fix the package coding style",
"phpstan": "Runs complete codebase static analysis",
"phpunit": "Runs unit and functional testing",
"test": "Runs the complete test suite"
},
"extra": {
"branch-alias": {
"dev-master": "6.x-dev"
}
},
"config": {
"sort-packages": true
}
}
+1449
View File
File diff suppressed because it is too large. Load diff