Version 5.0.0 beta 1
This commit is contained in:
1 parent
25ddeb65d6
commit
15c7beabc5
6736 files changed
+627902
-497943
No files matched your search
+226
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user