From bf4783dfff8961eb9373bcf73d845ab0fee1dc54 Mon Sep 17 00:00:00 2001 From: ignace nyamagana butera Date: Fri, 22 Mar 2024 11:46:15 +0100 Subject: [PATCH 01/23] Handles PHP8.4 nullable type deprecations --- .php-cs-fixer.php | 1 + composer.json | 16 ++++++++-------- src/Domain.php | 4 ++-- src/DomainName.php | 4 ++-- src/RegisteredName.php | 4 ++-- src/ResolvedDomainTest.php | 2 +- src/UnableToLoadTopLevelDomainList.php | 2 +- 7 files changed, 17 insertions(+), 16 deletions(-) diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 0b41a31..99023a2 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -29,6 +29,7 @@ ], 'no_trailing_comma_in_singleline' => true, 'no_unused_imports' => true, + 'nullable_type_declaration_for_default_null_value' => true, 'ordered_imports' => ['imports_order' => ['class', 'function', 'const'], 'sort_algorithm' => 'alpha'], 'phpdoc_add_missing_param_annotation' => ['only_untyped' => true], 'phpdoc_align' => true, diff --git a/composer.json b/composer.json index 25138d2..c2be172 100644 --- a/composer.json +++ b/composer.json @@ -46,16 +46,16 @@ "ext-json": "*" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^v3.35.1", - "guzzlehttp/guzzle": "^7.8", - "guzzlehttp/psr7": "^1.6 || ^2.6.1", - "phpstan/phpstan": "^1.10.39", - "phpstan/phpstan-phpunit": "^1.3.15", - "phpstan/phpstan-strict-rules": "^1.5.1", - "phpunit/phpunit": "^10.4.1", + "friendsofphp/php-cs-fixer": "^3.52.1", + "guzzlehttp/guzzle": "^7.8.1", + "guzzlehttp/psr7": "^1.6 || ^2.6.2", + "phpstan/phpstan": "^1.10.64", + "phpstan/phpstan-phpunit": "^1.3.16", + "phpstan/phpstan-strict-rules": "^1.5.2", + "phpunit/phpunit": "^10.5.15", "psr/http-factory": "^1.0.2", "psr/simple-cache": "^1.0.1", - "symfony/cache": "^v5.0.0 || ^v6.3.5" + "symfony/cache": "^v5.0.0 || ^6.4.4" }, "suggest": { "psr/http-client-implementation": "To use the storage functionnality which depends on PSR-18", diff --git a/src/Domain.php b/src/Domain.php index 358ebdc..1d0bbf7 100644 --- a/src/Domain.php +++ b/src/Domain.php @@ -86,7 +86,7 @@ public function label(int $key): ?string /** * @return list */ - public function keys(string $label = null): array + public function keys(?string $label = null): array { return $this->registeredName->keys($label); } @@ -155,7 +155,7 @@ public function clear(): self /** * @throws CannotProcessHost */ - public function slice(int $offset, int $length = null): self + public function slice(int $offset, ?int $length = null): self { return $this->newInstance($this->registeredName->slice($offset, $length)); } diff --git a/src/DomainName.php b/src/DomainName.php index c4a4aaf..fd6e3b9 100644 --- a/src/DomainName.php +++ b/src/DomainName.php @@ -49,7 +49,7 @@ public function labels(): array; * * @return list */ - public function keys(string $label = null): array; + public function keys(?string $label = null): array; /** * The external iterator iterates over the DomainInterface labels @@ -119,5 +119,5 @@ public function clear(): self; * * 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; + public function slice(int $offset, ?int $length = null): self; } diff --git a/src/RegisteredName.php b/src/RegisteredName.php index 4350932..cd12eea 100644 --- a/src/RegisteredName.php +++ b/src/RegisteredName.php @@ -190,7 +190,7 @@ public function label(int $key): ?string /** * @return list */ - public function keys(string $label = null): array + public function keys(?string $label = null): array { if (null === $label) { return array_keys($this->labels); @@ -345,7 +345,7 @@ public function clear(): self /** * @throws CannotProcessHost */ - public function slice(int $offset, int $length = null): self + public function slice(int $offset, ?int $length = null): self { $nbLabels = count($this->labels); if ($offset < - $nbLabels || $offset > $nbLabels) { diff --git a/src/ResolvedDomainTest.php b/src/ResolvedDomainTest.php index e6f15f2..e0b1466 100644 --- a/src/ResolvedDomainTest.php +++ b/src/ResolvedDomainTest.php @@ -236,7 +236,7 @@ public static function toAsciiProvider(): iterable } #[DataProvider('withSubDomainWorksProvider')] - public function testItCanHaveItsSubDomainChanged(ResolvedDomain $domain, DomainName|string|null $subdomain, string $expected = null): void + public function testItCanHaveItsSubDomainChanged(ResolvedDomain $domain, DomainName|string|null $subdomain, ?string $expected = null): void { $result = $domain->withSubDomain($subdomain); diff --git a/src/UnableToLoadTopLevelDomainList.php b/src/UnableToLoadTopLevelDomainList.php index 94ed824..d2f4e93 100644 --- a/src/UnableToLoadTopLevelDomainList.php +++ b/src/UnableToLoadTopLevelDomainList.php @@ -9,7 +9,7 @@ final class UnableToLoadTopLevelDomainList extends InvalidArgumentException implements CannotProcessHost { - public static function dueToInvalidTopLevelDomain(string $content, Throwable $exception = null): self + public static function dueToInvalidTopLevelDomain(string $content, ?Throwable $exception = null): self { return new self('Invalid Top Level Domain: '.$content, 0, $exception); } From c4c8528f4fe71a86b5305aabfdc57cf5c49adbd2 Mon Sep 17 00:00:00 2001 From: ignace nyamagana butera Date: Sat, 13 Apr 2024 12:39:44 +0200 Subject: [PATCH 02/23] Add support for PHPUnit 11 --- composer.json | 10 ++--- phpstan.neon | 1 + src/DomainTest.php | 4 +- src/ResolvedDomainTest.php | 8 ++-- .../PublicSuffixListPsr16CacheTest.php | 35 ++++++++++++++++++ src/Storage/TimeToLiveTest.php | 4 +- .../TopLevelDomainListPsr16CacheTest.php | 37 +++++++++++++++++++ 7 files changed, 85 insertions(+), 14 deletions(-) diff --git a/composer.json b/composer.json index c2be172..65f30e6 100644 --- a/composer.json +++ b/composer.json @@ -46,16 +46,16 @@ "ext-json": "*" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.52.1", + "friendsofphp/php-cs-fixer": "^3.53.0", "guzzlehttp/guzzle": "^7.8.1", "guzzlehttp/psr7": "^1.6 || ^2.6.2", - "phpstan/phpstan": "^1.10.64", + "phpstan/phpstan": "^1.10.66", "phpstan/phpstan-phpunit": "^1.3.16", - "phpstan/phpstan-strict-rules": "^1.5.2", - "phpunit/phpunit": "^10.5.15", + "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.4" + "symfony/cache": "^v5.0.0 || ^6.4.6" }, "suggest": { "psr/http-client-implementation": "To use the storage functionnality which depends on PSR-18", diff --git a/phpstan.neon b/phpstan.neon index caa678b..0332b88 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -12,4 +12,5 @@ parameters: path: src/Rules.php - message: '#Variable \$line on left side of \?\? always exists and is not nullable.#' path: src/Rules.php + - '#^Parameter \#1 \$callback of function set_error_handler expects#' reportUnmatchedIgnoredErrors: true diff --git a/src/DomainTest.php b/src/DomainTest.php index 3ea5315..e3ea7c0 100644 --- a/src/DomainTest.php +++ b/src/DomainTest.php @@ -168,14 +168,14 @@ public static function toUnicodeProvider(): iterable public function testToAscii( ?string $domain, ?string $expectedDomain, - ?string $expectedAsciiDomain + ?string $expectedIDNDomain ): void { $domain = Domain::fromIDNA2008($domain); self::assertSame($expectedDomain, $domain->value()); /** @var Domain $domainIDN */ $domainIDN = $domain->toAscii(); - self::assertSame($expectedAsciiDomain, $domainIDN->value()); + self::assertSame($expectedIDNDomain, $domainIDN->value()); } /** diff --git a/src/ResolvedDomainTest.php b/src/ResolvedDomainTest.php index e0b1466..12c777b 100644 --- a/src/ResolvedDomainTest.php +++ b/src/ResolvedDomainTest.php @@ -173,8 +173,8 @@ public function testItCanBeConvertedToAscii( ?string $publicSuffix, ?string $expectedDomain, ?string $expectedSuffix, - ?string $expectedAsciiDomain, - ?string $expectedAsciiSuffix + ?string $expectedIDNDomain, + ?string $expectedIDNSuffix ): void { $domain = ResolvedDomain::fromUnknown(Domain::fromIDNA2003($domain), count(Domain::fromIDNA2003($publicSuffix))); self::assertSame($expectedDomain, $domain->value()); @@ -182,8 +182,8 @@ public function testItCanBeConvertedToAscii( /** @var ResolvedDomain $domainIDN */ $domainIDN = $domain->toAscii(); - self::assertSame($expectedAsciiDomain, $domainIDN->value()); - self::assertSame($expectedAsciiSuffix, $domainIDN->suffix()->value()); + self::assertSame($expectedIDNDomain, $domainIDN->value()); + self::assertSame($expectedIDNSuffix, $domainIDN->suffix()->value()); } /** diff --git a/src/Storage/PublicSuffixListPsr16CacheTest.php b/src/Storage/PublicSuffixListPsr16CacheTest.php index 18b0cf3..f41cfb5 100644 --- a/src/Storage/PublicSuffixListPsr16CacheTest.php +++ b/src/Storage/PublicSuffixListPsr16CacheTest.php @@ -9,6 +9,7 @@ use InvalidArgumentException; use Pdp\Rules; use PHPUnit\Framework\TestCase; +use PHPUnit\Runner\ErrorHandler; use Psr\SimpleCache\CacheException; use Psr\SimpleCache\CacheInterface; use RuntimeException; @@ -128,4 +129,38 @@ public function testItWillThrowIfTheTTLIsNotParsable(): void $cache = self::createStub(CacheInterface::class); new PublicSuffixListPsr16Cache($cache, 'pdp_', 'foobar'); } + + protected function restoreExceptionHandler(): void + { + while (true) { + $previousHandler = set_exception_handler(static fn () => null); + restore_exception_handler(); + if (null === $previousHandler) { + break; + } + + restore_exception_handler(); + } + } + + protected function restoreErrorHandler(): void + { + while (true) { + $previousHandler = set_error_handler(static fn (int $errno, string $errstr, ?string $errfile = null, ?int $errline = null) => null); + restore_error_handler(); + $isPhpUnitErrorHandler = ($previousHandler instanceof ErrorHandler); + if (null === $previousHandler || $isPhpUnitErrorHandler) { + break; + } + restore_error_handler(); + } + } + + protected function tearDown(): void + { + parent::tearDown(); + + $this->restoreErrorHandler(); + $this->restoreExceptionHandler(); + } } diff --git a/src/Storage/TimeToLiveTest.php b/src/Storage/TimeToLiveTest.php index 1604d3d..9ca00b7 100644 --- a/src/Storage/TimeToLiveTest.php +++ b/src/Storage/TimeToLiveTest.php @@ -22,9 +22,7 @@ public function testItDoesNotReturnTheAbsoluteInterval(): void self::assertSame(0, TimeToLive::until($tomorrow)->invert); } - /** - * @dataProvider validDurationString - */ + #[DataProvider('validDurationString')] public function testItCanBeInstantiatedFromDurationInput(string $input, DateInterval $expected): void { $now = new DateTimeImmutable(); diff --git a/src/Storage/TopLevelDomainListPsr16CacheTest.php b/src/Storage/TopLevelDomainListPsr16CacheTest.php index 9d4275b..2eb8cb4 100644 --- a/src/Storage/TopLevelDomainListPsr16CacheTest.php +++ b/src/Storage/TopLevelDomainListPsr16CacheTest.php @@ -9,6 +9,7 @@ use InvalidArgumentException; use Pdp\TopLevelDomains; use PHPUnit\Framework\TestCase; +use PHPUnit\Runner\ErrorHandler; use Psr\SimpleCache\CacheException; use Psr\SimpleCache\CacheInterface; use RuntimeException; @@ -125,4 +126,40 @@ public function testItWillThrowIfTheTTLIsNotParsable(): void $cache = self::createStub(CacheInterface::class); new TopLevelDomainListPsr16Cache($cache, 'pdp_', 'foobar'); } + + + + protected function restoreExceptionHandler(): void + { + while (true) { + $previousHandler = set_exception_handler(static fn () => null); + restore_exception_handler(); + if (null === $previousHandler) { + break; + } + + restore_exception_handler(); + } + } + + protected function restoreErrorHandler(): void + { + while (true) { + $previousHandler = set_error_handler(static fn (int $errno, string $errstr, ?string $errfile = null, ?int $errline = null) => null); + restore_error_handler(); + $isPhpUnitErrorHandler = ($previousHandler instanceof ErrorHandler); + if (null === $previousHandler || $isPhpUnitErrorHandler) { + break; + } + restore_error_handler(); + } + } + + protected function tearDown(): void + { + parent::tearDown(); + + $this->restoreErrorHandler(); + $this->restoreExceptionHandler(); + } } From 4ae7743be46d657a4f7d27c5a358c2d06b8356f5 Mon Sep 17 00:00:00 2001 From: ignace nyamagana butera Date: Sat, 9 Nov 2024 21:50:33 +0100 Subject: [PATCH 03/23] Update codebase test suite --- .github/workflows/build.yaml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 9444048..31e3a97 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -12,6 +12,11 @@ jobs: matrix: php: ['8.1', '8.2', '8.3'] stability: [prefer-lowest, prefer-stable] + include: + - php: '8.4' + flags: "--ignore-platform-req=php" + phpunit-flags: '--no-coverage' + stability: prefer-stable steps: - name: Checkout code uses: actions/checkout@v3 @@ -45,11 +50,16 @@ jobs: - name: Run Unit tests with coverage run: composer phpunit -- ${{ matrix.phpunit-flags }} + if: ${{ matrix.php == '8.3' || matrix.php == '8.2' || matrix.php == '8.1'}} + + - name: Run Unit tests without coverage + run: composer phpunit:min + if: ${{ matrix.php == '8.4'}} - name: Run static analysis run: composer phpstan - if: ${{ matrix.php == '8.2' && matrix.stability == 'prefer-stable'}} + if: ${{ matrix.php == '8.3' && matrix.stability == 'prefer-stable'}} - name: Run Coding style rules run: composer phpcs:fix - if: ${{ matrix.php == '8.2' && matrix.stability == 'prefer-stable'}} + if: ${{ matrix.php == '8.3' && matrix.stability == 'prefer-stable'}} From 26cbafd2baf9fab6a92d2d7ec60333a59cc1c602 Mon Sep 17 00:00:00 2001 From: ignace nyamagana butera Date: Sun, 10 Nov 2024 10:10:02 +0100 Subject: [PATCH 04/23] Update codebase test suite --- composer.json | 3 ++- phpstan.neon | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 65f30e6..558f769 100644 --- a/composer.json +++ b/composer.json @@ -71,8 +71,9 @@ "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", + "phpstan": "phpstan analyse -c phpstan.neon --ansi --memory-limit=192M", "phpunit": "XDEBUG_MODE=coverage phpunit --coverage-text", + "phpunit:min": "phpunit --no-coverage", "test": [ "@phpunit", "@phpstan", diff --git a/phpstan.neon b/phpstan.neon index 0332b88..19ea5e3 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -3,6 +3,9 @@ includes: - vendor/phpstan/phpstan-phpunit/extension.neon - vendor/phpstan/phpstan-phpunit/rules.neon parameters: + level: max + paths: + - src ignoreErrors: - message: '#has no value type specified in iterable type array.#' path: src/Rules.php From aa1f9bf2617c9863f2286595403f7dbffe2786ea Mon Sep 17 00:00:00 2001 From: ignace nyamagana butera Date: Sun, 10 Nov 2024 10:18:36 +0100 Subject: [PATCH 05/23] Prepare PHP8.4 compatible release (#359) Add support for PHP8.3+ and clean up documentation --- .github/workflows/build.yaml | 16 ++++- .php-cs-fixer.php | 5 +- README.md | 70 ++++++++++--------- composer.json | 21 +++--- phpstan.neon | 4 ++ phpunit.xml.dist | 26 +++---- src/Domain.php | 4 +- src/DomainName.php | 4 +- src/DomainTest.php | 4 +- src/RegisteredName.php | 4 +- src/ResolvedDomainTest.php | 10 +-- src/Storage/PsrStorageFactoryTest.php | 6 +- .../PublicSuffixListPsr16CacheTest.php | 55 ++++++++++++--- src/Storage/TimeToLive.php | 28 ++++++-- src/Storage/TimeToLiveTest.php | 4 +- .../TopLevelDomainListPsr16CacheTest.php | 57 ++++++++++++--- src/UnableToLoadTopLevelDomainList.php | 2 +- 17 files changed, 208 insertions(+), 112 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index c5e55a2..31e3a97 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -10,8 +10,13 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - php: ['8.1', '8.2'] + php: ['8.1', '8.2', '8.3'] stability: [prefer-lowest, prefer-stable] + include: + - php: '8.4' + flags: "--ignore-platform-req=php" + phpunit-flags: '--no-coverage' + stability: prefer-stable steps: - name: Checkout code uses: actions/checkout@v3 @@ -45,11 +50,16 @@ jobs: - name: Run Unit tests with coverage run: composer phpunit -- ${{ matrix.phpunit-flags }} + if: ${{ matrix.php == '8.3' || matrix.php == '8.2' || matrix.php == '8.1'}} + + - name: Run Unit tests without coverage + run: composer phpunit:min + if: ${{ matrix.php == '8.4'}} - name: Run static analysis run: composer phpstan - if: ${{ matrix.php == '8.2' && matrix.stability == 'prefer-stable'}} + if: ${{ matrix.php == '8.3' && matrix.stability == 'prefer-stable'}} - name: Run Coding style rules run: composer phpcs:fix - if: ${{ matrix.php == '8.1' && matrix.stability == 'prefer-stable'}} + if: ${{ matrix.php == '8.3' && matrix.stability == 'prefer-stable'}} diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index e771f58..99023a2 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -17,7 +17,7 @@ 'import_functions' => true, ], 'list_syntax' => ['syntax' => 'short'], - 'new_with_braces' => true, + 'new_with_parentheses' => true, 'no_blank_lines_after_phpdoc' => true, 'no_empty_phpdoc' => true, 'no_empty_comment' => true, @@ -29,6 +29,7 @@ ], 'no_trailing_comma_in_singleline' => true, 'no_unused_imports' => true, + 'nullable_type_declaration_for_default_null_value' => true, 'ordered_imports' => ['imports_order' => ['class', 'function', 'const'], 'sort_algorithm' => 'alpha'], 'phpdoc_add_missing_param_annotation' => ['only_untyped' => true], 'phpdoc_align' => true, @@ -39,7 +40,7 @@ 'phpdoc_summary' => true, 'psr_autoloading' => true, 'return_type_declaration' => ['space_before' => 'none'], - 'single_blank_line_before_namespace' => true, + 'blank_lines_before_namespace' => true, 'single_quote' => true, 'space_after_semicolon' => true, 'ternary_operator_spaces' => true, diff --git a/README.md b/README.md index 8cd8756..68dc1b3 100644 --- a/README.md +++ b/README.md @@ -38,10 +38,12 @@ You need: - **PHP >= 7.4** but the latest stable version of PHP is recommended - the `intl` extension +- a copy of the [Public Suffix List](https://publicsuffix.org/) data and/or a copy of the [IANA Top Level Domain List](https://www.iana.org/domains/root/files). Please refer to the [Managing external data source section](#managing-the-package-external-resources) for more information when using this package in production. ## Usage -**If you are upgrading from version 5 please check the [upgrading guide](UPGRADING.md) for known issues.** +> [!WARNING] +> If you are upgrading from version 5 please check the [upgrading guide](UPGRADING.md) for known issues. ### Resolving Domains @@ -115,9 +117,10 @@ These methods resolve the domain against their respective data source using the same rules as the `resolve` method but will instead throw an exception if no valid effective TLD is found or if the submitted domain is invalid. -**All these methods expect as their sole argument a `Pdp\Host` implementing +> [!CAUTION] +> All these methods expect as their sole argument a `Pdp\Host` implementing object, but other types (ie: `string`, `null` and stringable objects) are -supported with predefined conditions as explained in the remaining document.** +supported with predefined conditions as explained in the remaining document. ~~~php [!WARNING] +> You SHOULD never resolve domain name this way in production, without, at +least, a caching mechanism to reduce external resource downloads. +> Using the Public Suffix List to determine what is a valid domain name and what +isn't is dangerous, and MAY lead to errors because of new gTLDs being registered +on a regular basis. +> If you are looking to know the validity of a Top Level Domain, you MUST use +the IANA Top Level Domain List as the proper source for this information or +alternatively the DNS. +> If you MUST use this library for any of the above purposes, you SHOULD consider +integrating an updating mechanism into your software. +> For more information go to the [Managing external data source section](#managing-the-package-external-resources)** ### Resolved domain information. @@ -220,10 +218,11 @@ echo $altResult->domain()->toString(); //display 'foo.bar.test.example'; $altResult->suffix()->isKnown(); //return false; ~~~ -**TIP: Always favor submitting a `Pdp\Suffix` object rather that any other +> [!TIP] +> Always favor submitting a `Pdp\Suffix` object rather that any other supported type to avoid unexpected results. By default, if the input is not a `Pdp\Suffix` instance, the resulting public suffix will be labelled as -being unknown. For more information go to the [Public Suffix section](#public-suffix)** +being unknown. For more information go to the [Public Suffix section](#public-suffix) ### Domain Suffix @@ -339,7 +338,8 @@ $newDomain->clear()->labels(); //return [] echo $domain->slice(2)->toString(); //display 'www' ~~~ -**WARNING: Because of its definition, a domain name can be `null` or a string.** +> [!WARNING] +> Because of its definition, a domain name can be `null` or a string. To distinguish this possibility the object exposes two (2) formatting methods `Domain::value` which can be `null` or a `string` and `Domain::toString` which @@ -396,8 +396,9 @@ is done via two (2) named constructors: At any given moment the `Pdp\Domain` instance can tell you whether it is in `ASCII` mode or not. -**Once instantiated there's no way to tell which algorithm is used to convert -the object from ascii to unicode and vice-versa** +> [!WARNING] +> Once instantiated there's no way to tell which algorithm is used to convert +the object from ascii to unicode and vice-versa ~~~php use Pdp\Domain; @@ -419,10 +420,11 @@ echo $asciiDomain->value(); // display 'fass.de' $asciiDomain->isAscii(); // returns true ~~~ -**TIP: Always favor submitting a `Pdp\Domain` object for resolution rather that a +> [!TIP] +> Always favor submitting a `Pdp\Domain` object for resolution rather that a string or an object that can be cast to a string to avoid unexpected format conversion errors/results. By default, and with lack of information conversion -is done using IDNA 2008 rules.** +is done using IDNA 2008 rules. ### Managing the package external resources @@ -469,7 +471,8 @@ on packagist. #### Refreshing the resource using the provided factories -**THIS IS THE RECOMMENDED WAY OF USING THE LIBRARY** +> [!NOTE] +> THIS IS THE RECOMMENDED WAY OF USING THE LIBRARY For the purpose of this example we will use our PSR powered solution with: @@ -526,12 +529,14 @@ $publicSuffixList = $pslStorage->get(PsrStorageFactory::PUBLIC_SUFFIX_LIST_URI); $topLevelDomains = $rzdStorage->get(PsrStorageFactory::TOP_LEVEL_DOMAIN_LIST_URI); ~~~ -**Be sure to adapt the following code to your own application. +> [!NOTE] +> Be sure to adapt the following code to your own application. The following code is an example given without warranty of it working -out of the box.** +out of the box. -**You should use your dependency injection container to avoid repeating this -code in your application.** +> [!WARNING] +> You should use your dependency injection container to avoid repeating this +code in your application. ### Automatic Updates @@ -560,7 +565,6 @@ Testing - a [PHPUnit](https://phpunit.de) test suite - a code analysis compliance test suite using [PHPStan](https://phpstan.org). -- a code analysis compliance test suite using [Psalm](https://psalm.dev). - a coding style compliance test suite using [PHP CS Fixer](https://cs.symfony.com). To run the tests, run the following command from the project folder. diff --git a/composer.json b/composer.json index 66aafde..558f769 100644 --- a/composer.json +++ b/composer.json @@ -46,16 +46,16 @@ "ext-json": "*" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^v3.13.2", - "guzzlehttp/guzzle": "^7.5", - "guzzlehttp/psr7": "^1.6 || ^2.4.3", - "phpstan/phpstan": "^1.10.3", - "phpstan/phpstan-phpunit": "^1.3.8", - "phpstan/phpstan-strict-rules": "^1.5.0", - "phpunit/phpunit": "^10.0.12", - "psr/http-factory": "^1.0.1", + "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 || ^v6.0.0" + "symfony/cache": "^v5.0.0 || ^6.4.6" }, "suggest": { "psr/http-client-implementation": "To use the storage functionnality which depends on PSR-18", @@ -71,8 +71,9 @@ "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 --xdebug --memory-limit=256M --ansi", + "phpstan": "phpstan analyse -c phpstan.neon --ansi --memory-limit=192M", "phpunit": "XDEBUG_MODE=coverage phpunit --coverage-text", + "phpunit:min": "phpunit --no-coverage", "test": [ "@phpunit", "@phpstan", diff --git a/phpstan.neon b/phpstan.neon index caa678b..19ea5e3 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -3,6 +3,9 @@ includes: - vendor/phpstan/phpstan-phpunit/extension.neon - vendor/phpstan/phpstan-phpunit/rules.neon parameters: + level: max + paths: + - src ignoreErrors: - message: '#has no value type specified in iterable type array.#' path: src/Rules.php @@ -12,4 +15,5 @@ parameters: path: src/Rules.php - message: '#Variable \$line on left side of \?\? always exists and is not nullable.#' path: src/Rules.php + - '#^Parameter \#1 \$callback of function set_error_handler expects#' reportUnmatchedIgnoredErrors: true diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 4a02ebf..2403d4e 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,22 +1,6 @@ - + - - src - - - src - @@ -31,4 +15,12 @@ + + + src + + + src + + diff --git a/src/Domain.php b/src/Domain.php index 358ebdc..1d0bbf7 100644 --- a/src/Domain.php +++ b/src/Domain.php @@ -86,7 +86,7 @@ public function label(int $key): ?string /** * @return list */ - public function keys(string $label = null): array + public function keys(?string $label = null): array { return $this->registeredName->keys($label); } @@ -155,7 +155,7 @@ public function clear(): self /** * @throws CannotProcessHost */ - public function slice(int $offset, int $length = null): self + public function slice(int $offset, ?int $length = null): self { return $this->newInstance($this->registeredName->slice($offset, $length)); } diff --git a/src/DomainName.php b/src/DomainName.php index c4a4aaf..fd6e3b9 100644 --- a/src/DomainName.php +++ b/src/DomainName.php @@ -49,7 +49,7 @@ public function labels(): array; * * @return list */ - public function keys(string $label = null): array; + public function keys(?string $label = null): array; /** * The external iterator iterates over the DomainInterface labels @@ -119,5 +119,5 @@ public function clear(): self; * * 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; + public function slice(int $offset, ?int $length = null): self; } diff --git a/src/DomainTest.php b/src/DomainTest.php index 3ea5315..e3ea7c0 100644 --- a/src/DomainTest.php +++ b/src/DomainTest.php @@ -168,14 +168,14 @@ public static function toUnicodeProvider(): iterable public function testToAscii( ?string $domain, ?string $expectedDomain, - ?string $expectedAsciiDomain + ?string $expectedIDNDomain ): void { $domain = Domain::fromIDNA2008($domain); self::assertSame($expectedDomain, $domain->value()); /** @var Domain $domainIDN */ $domainIDN = $domain->toAscii(); - self::assertSame($expectedAsciiDomain, $domainIDN->value()); + self::assertSame($expectedIDNDomain, $domainIDN->value()); } /** diff --git a/src/RegisteredName.php b/src/RegisteredName.php index 4350932..cd12eea 100644 --- a/src/RegisteredName.php +++ b/src/RegisteredName.php @@ -190,7 +190,7 @@ public function label(int $key): ?string /** * @return list */ - public function keys(string $label = null): array + public function keys(?string $label = null): array { if (null === $label) { return array_keys($this->labels); @@ -345,7 +345,7 @@ public function clear(): self /** * @throws CannotProcessHost */ - public function slice(int $offset, int $length = null): self + public function slice(int $offset, ?int $length = null): self { $nbLabels = count($this->labels); if ($offset < - $nbLabels || $offset > $nbLabels) { diff --git a/src/ResolvedDomainTest.php b/src/ResolvedDomainTest.php index e6f15f2..12c777b 100644 --- a/src/ResolvedDomainTest.php +++ b/src/ResolvedDomainTest.php @@ -173,8 +173,8 @@ public function testItCanBeConvertedToAscii( ?string $publicSuffix, ?string $expectedDomain, ?string $expectedSuffix, - ?string $expectedAsciiDomain, - ?string $expectedAsciiSuffix + ?string $expectedIDNDomain, + ?string $expectedIDNSuffix ): void { $domain = ResolvedDomain::fromUnknown(Domain::fromIDNA2003($domain), count(Domain::fromIDNA2003($publicSuffix))); self::assertSame($expectedDomain, $domain->value()); @@ -182,8 +182,8 @@ public function testItCanBeConvertedToAscii( /** @var ResolvedDomain $domainIDN */ $domainIDN = $domain->toAscii(); - self::assertSame($expectedAsciiDomain, $domainIDN->value()); - self::assertSame($expectedAsciiSuffix, $domainIDN->suffix()->value()); + self::assertSame($expectedIDNDomain, $domainIDN->value()); + self::assertSame($expectedIDNSuffix, $domainIDN->suffix()->value()); } /** @@ -236,7 +236,7 @@ public static function toAsciiProvider(): iterable } #[DataProvider('withSubDomainWorksProvider')] - public function testItCanHaveItsSubDomainChanged(ResolvedDomain $domain, DomainName|string|null $subdomain, string $expected = null): void + public function testItCanHaveItsSubDomainChanged(ResolvedDomain $domain, DomainName|string|null $subdomain, ?string $expected = null): void { $result = $domain->withSubDomain($subdomain); diff --git a/src/Storage/PsrStorageFactoryTest.php b/src/Storage/PsrStorageFactoryTest.php index b423fb4..efa782f 100644 --- a/src/Storage/PsrStorageFactoryTest.php +++ b/src/Storage/PsrStorageFactoryTest.php @@ -15,9 +15,9 @@ final class PsrStorageFactoryTest extends TestCase public function setUp(): void { - $cache = $this->createStub(CacheInterface::class); - $requestFactory = $this->createStub(RequestFactoryInterface::class); - $client = $this->createStub(ClientInterface::class); + $cache = self::createStub(CacheInterface::class); + $requestFactory = self::createStub(RequestFactoryInterface::class); + $client = self::createStub(ClientInterface::class); $this->factory = new PsrStorageFactory($cache, $client, $requestFactory); } diff --git a/src/Storage/PublicSuffixListPsr16CacheTest.php b/src/Storage/PublicSuffixListPsr16CacheTest.php index c9c8989..f41cfb5 100644 --- a/src/Storage/PublicSuffixListPsr16CacheTest.php +++ b/src/Storage/PublicSuffixListPsr16CacheTest.php @@ -9,6 +9,7 @@ use InvalidArgumentException; use Pdp\Rules; use PHPUnit\Framework\TestCase; +use PHPUnit\Runner\ErrorHandler; use Psr\SimpleCache\CacheException; use Psr\SimpleCache\CacheInterface; use RuntimeException; @@ -18,7 +19,7 @@ final class PublicSuffixListPsr16CacheTest extends TestCase { public function testItReturnsNullIfTheCacheDoesNotExists(): void { - $cache = $this->createStub(CacheInterface::class); + $cache = self::createStub(CacheInterface::class); $cache->method('get')->willReturn(null); $pslCache = new PublicSuffixListPsr16Cache($cache, 'pdp_', '1 DAY'); @@ -29,7 +30,7 @@ public function testItReturnsNullIfTheCacheDoesNotExists(): void public function testItReturnsAnInstanceIfTheCorrectCacheExists(): void { $rules = Rules::fromPath(dirname(__DIR__, 2).'/test_data/public_suffix_list.dat'); - $cache = $this->createStub(CacheInterface::class); + $cache = self::createStub(CacheInterface::class); $cache->method('get')->willReturn($rules); $pslCache = new PublicSuffixListPsr16Cache($cache, 'pdp_', 86400); @@ -39,7 +40,7 @@ public function testItReturnsAnInstanceIfTheCorrectCacheExists(): void public function testItReturnsNullIfTheCacheContentContainsInvalidJsonData(): void { - $cache = $this->createStub(CacheInterface::class); + $cache = self::createStub(CacheInterface::class); $cache->method('get')->willReturn('foobar'); $pslCache = new PublicSuffixListPsr16Cache($cache, 'pdp_', 86400); @@ -48,7 +49,7 @@ public function testItReturnsNullIfTheCacheContentContainsInvalidJsonData(): voi public function testItReturnsNullIfTheCacheContentCannotBeConvertedToTheCorrectInstance(): void { - $cache = $this->createStub(CacheInterface::class); + $cache = self::createStub(CacheInterface::class); $cache->method('get')->willReturn('{"foo":"bar"}'); $pslCache = new PublicSuffixListPsr16Cache($cache, 'pdp_', new DateTimeImmutable('+1 DAY')); @@ -58,7 +59,7 @@ public function testItReturnsNullIfTheCacheContentCannotBeConvertedToTheCorrectI public function testItCanStoreAPublicSuffixListInstance(): void { - $cache = $this->createStub(CacheInterface::class); + $cache = self::createStub(CacheInterface::class); $cache->method('set')->willReturn(true); $psl = Rules::fromPath(dirname(__DIR__, 2).'/test_data/public_suffix_list.dat'); @@ -69,7 +70,7 @@ public function testItCanStoreAPublicSuffixListInstance(): void public function testItReturnsFalseIfItCantStoreAPublicSuffixListInstance(): void { - $cache = $this->createStub(CacheInterface::class); + $cache = self::createStub(CacheInterface::class); $cache->method('set')->willReturn(false); $psl = Rules::fromPath(dirname(__DIR__, 2).'/test_data/public_suffix_list.dat'); @@ -82,7 +83,7 @@ public function testItReturnsFalseIfItCantCacheAPublicSuffixListInstance(): void { $exception = new class('Something went wrong.', 0) extends RuntimeException implements CacheException { }; - $cache = $this->createStub(CacheInterface::class); + $cache = self::createStub(CacheInterface::class); $cache->method('set')->will(self::throwException($exception)); $psl = Rules::fromPath(dirname(__DIR__, 2).'/test_data/public_suffix_list.dat'); @@ -95,7 +96,7 @@ public function testItWillThrowIfItCantCacheAPublicSuffixListInstance(): void { $exception = new class('Something went wrong.', 0) extends RuntimeException { }; - $cache = $this->createStub(CacheInterface::class); + $cache = self::createStub(CacheInterface::class); $cache->method('set')->will(self::throwException($exception)); $psl = Rules::fromPath(dirname(__DIR__, 2).'/test_data/public_suffix_list.dat'); @@ -114,7 +115,7 @@ public function testItCanDeleteTheCachedDatabase(): void { $uri = 'http://www.example.com'; - $cache = $this->createStub(CacheInterface::class); + $cache = self::createStub(CacheInterface::class); $cache->method('delete')->willReturn(true); $instance = new PublicSuffixListPsr16Cache($cache, 'pdp_', new DateInterval('P1D')); @@ -125,7 +126,41 @@ public function testItWillThrowIfTheTTLIsNotParsable(): void { $this->expectException(InvalidArgumentException::class); - $cache = $this->createStub(CacheInterface::class); + $cache = self::createStub(CacheInterface::class); new PublicSuffixListPsr16Cache($cache, 'pdp_', 'foobar'); } + + protected function restoreExceptionHandler(): void + { + while (true) { + $previousHandler = set_exception_handler(static fn () => null); + restore_exception_handler(); + if (null === $previousHandler) { + break; + } + + restore_exception_handler(); + } + } + + protected function restoreErrorHandler(): void + { + while (true) { + $previousHandler = set_error_handler(static fn (int $errno, string $errstr, ?string $errfile = null, ?int $errline = null) => null); + restore_error_handler(); + $isPhpUnitErrorHandler = ($previousHandler instanceof ErrorHandler); + if (null === $previousHandler || $isPhpUnitErrorHandler) { + break; + } + restore_error_handler(); + } + } + + protected function tearDown(): void + { + parent::tearDown(); + + $this->restoreErrorHandler(); + $this->restoreExceptionHandler(); + } } diff --git a/src/Storage/TimeToLive.php b/src/Storage/TimeToLive.php index 5182f09..3bbe04a 100644 --- a/src/Storage/TimeToLive.php +++ b/src/Storage/TimeToLive.php @@ -9,6 +9,7 @@ use DateTimeInterface; use InvalidArgumentException; use Stringable; +use Throwable; use function filter_var; use const FILTER_VALIDATE_INT; @@ -19,13 +20,26 @@ final class TimeToLive { public static function fromDurationString(string $duration): DateInterval { - 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`.' - ); + 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; diff --git a/src/Storage/TimeToLiveTest.php b/src/Storage/TimeToLiveTest.php index 1604d3d..9ca00b7 100644 --- a/src/Storage/TimeToLiveTest.php +++ b/src/Storage/TimeToLiveTest.php @@ -22,9 +22,7 @@ public function testItDoesNotReturnTheAbsoluteInterval(): void self::assertSame(0, TimeToLive::until($tomorrow)->invert); } - /** - * @dataProvider validDurationString - */ + #[DataProvider('validDurationString')] public function testItCanBeInstantiatedFromDurationInput(string $input, DateInterval $expected): void { $now = new DateTimeImmutable(); diff --git a/src/Storage/TopLevelDomainListPsr16CacheTest.php b/src/Storage/TopLevelDomainListPsr16CacheTest.php index 59a0a76..2eb8cb4 100644 --- a/src/Storage/TopLevelDomainListPsr16CacheTest.php +++ b/src/Storage/TopLevelDomainListPsr16CacheTest.php @@ -9,6 +9,7 @@ use InvalidArgumentException; use Pdp\TopLevelDomains; use PHPUnit\Framework\TestCase; +use PHPUnit\Runner\ErrorHandler; use Psr\SimpleCache\CacheException; use Psr\SimpleCache\CacheInterface; use RuntimeException; @@ -18,7 +19,7 @@ final class TopLevelDomainListPsr16CacheTest extends TestCase { public function testItReturnsNullIfTheCacheDoesNotExists(): void { - $cache = $this->createStub(CacheInterface::class); + $cache = self::createStub(CacheInterface::class); $cache->method('get')->willReturn(null); $instance = new TopLevelDomainListPsr16Cache($cache, 'pdp_', '1 DAY'); @@ -29,7 +30,7 @@ public function testItReturnsNullIfTheCacheDoesNotExists(): void public function testItReturnsAnInstanceIfTheCorrectCacheExists(): void { $topLevelDomainList = TopLevelDomains::fromPath(dirname(__DIR__, 2).'/test_data/tlds-alpha-by-domain.txt'); - $cache = $this->createStub(CacheInterface::class); + $cache = self::createStub(CacheInterface::class); $cache->method('get')->willReturn($topLevelDomainList); $instance = new TopLevelDomainListPsr16Cache($cache, 'pdp_', 86400); @@ -39,7 +40,7 @@ public function testItReturnsAnInstanceIfTheCorrectCacheExists(): void public function testItReturnsNullIfTheCacheContentContainsInvalidJsonData(): void { - $cache = $this->createStub(CacheInterface::class); + $cache = self::createStub(CacheInterface::class); $cache->method('get')->willReturn('foobar'); $instance = new TopLevelDomainListPsr16Cache($cache, 'pdp_', new DateInterval('P1D')); @@ -49,7 +50,7 @@ public function testItReturnsNullIfTheCacheContentContainsInvalidJsonData(): voi public function testItReturnsNullIfTheCacheContentCannotBeConvertedToTheCorrectInstance(): void { - $cache = $this->createStub(CacheInterface::class); + $cache = self::createStub(CacheInterface::class); $cache->method('get')->willReturn('{"foo":"bar"}'); $instance = new TopLevelDomainListPsr16Cache($cache, 'pdp_', new DateTimeImmutable('+1 DAY')); @@ -59,7 +60,7 @@ public function testItReturnsNullIfTheCacheContentCannotBeConvertedToTheCorrectI public function testItCanStoreAPublicSuffixListInstance(): void { - $cache = $this->createStub(CacheInterface::class); + $cache = self::createStub(CacheInterface::class); $cache->method('set')->willReturn(true); $rzd = TopLevelDomains::fromPath(dirname(__DIR__, 2).'/test_data/tlds-alpha-by-domain.txt'); @@ -70,7 +71,7 @@ public function testItCanStoreAPublicSuffixListInstance(): void public function testItReturnsFalseIfItCantStoreAPublicSuffixListInstance(): void { - $cache = $this->createStub(CacheInterface::class); + $cache = self::createStub(CacheInterface::class); $cache->method('set')->willReturn(false); $rzd = TopLevelDomains::fromPath(dirname(__DIR__, 2).'/test_data/tlds-alpha-by-domain.txt'); @@ -83,7 +84,7 @@ public function testItReturnsFalseIfItCantCacheATopLevelDomainListInstance(): vo { $exception = new class('Something went wrong.', 0) extends RuntimeException implements CacheException { }; - $cache = $this->createStub(CacheInterface::class); + $cache = self::createStub(CacheInterface::class); $cache->method('set')->will(self::throwException($exception)); $rzd = TopLevelDomains::fromPath(dirname(__DIR__, 2).'/test_data/tlds-alpha-by-domain.txt'); @@ -96,7 +97,7 @@ public function testItThrowsIfItCantCacheATopLevelDomainListInstance(): void { $exception = new class('Something went wrong.', 0) extends RuntimeException { }; - $cache = $this->createStub(CacheInterface::class); + $cache = self::createStub(CacheInterface::class); $cache->method('set')->will(self::throwException($exception)); $rzd = TopLevelDomains::fromPath(dirname(__DIR__, 2).'/test_data/tlds-alpha-by-domain.txt'); @@ -111,7 +112,7 @@ public function testItCanDeleteTheCachedDatabase(): void { $uri = 'http://www.example.com'; - $cache = $this->createStub(CacheInterface::class); + $cache = self::createStub(CacheInterface::class); $cache->method('delete')->willReturn(true); $instance = new TopLevelDomainListPsr16Cache($cache, 'pdp_', new DateInterval('P1D')); @@ -122,7 +123,43 @@ public function testItWillThrowIfTheTTLIsNotParsable(): void { $this->expectException(InvalidArgumentException::class); - $cache = $this->createStub(CacheInterface::class); + $cache = self::createStub(CacheInterface::class); new TopLevelDomainListPsr16Cache($cache, 'pdp_', 'foobar'); } + + + + protected function restoreExceptionHandler(): void + { + while (true) { + $previousHandler = set_exception_handler(static fn () => null); + restore_exception_handler(); + if (null === $previousHandler) { + break; + } + + restore_exception_handler(); + } + } + + protected function restoreErrorHandler(): void + { + while (true) { + $previousHandler = set_error_handler(static fn (int $errno, string $errstr, ?string $errfile = null, ?int $errline = null) => null); + restore_error_handler(); + $isPhpUnitErrorHandler = ($previousHandler instanceof ErrorHandler); + if (null === $previousHandler || $isPhpUnitErrorHandler) { + break; + } + restore_error_handler(); + } + } + + protected function tearDown(): void + { + parent::tearDown(); + + $this->restoreErrorHandler(); + $this->restoreExceptionHandler(); + } } diff --git a/src/UnableToLoadTopLevelDomainList.php b/src/UnableToLoadTopLevelDomainList.php index 94ed824..d2f4e93 100644 --- a/src/UnableToLoadTopLevelDomainList.php +++ b/src/UnableToLoadTopLevelDomainList.php @@ -9,7 +9,7 @@ final class UnableToLoadTopLevelDomainList extends InvalidArgumentException implements CannotProcessHost { - public static function dueToInvalidTopLevelDomain(string $content, Throwable $exception = null): self + public static function dueToInvalidTopLevelDomain(string $content, ?Throwable $exception = null): self { return new self('Invalid Top Level Domain: '.$content, 0, $exception); } From c6fb031fe52ee1b3943867eb079c91476e28c9e1 Mon Sep 17 00:00:00 2001 From: ignace nyamagana butera Date: Thu, 19 Dec 2024 11:12:35 +0100 Subject: [PATCH 06/23] Update develop dependencies --- composer.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/composer.json b/composer.json index 558f769..e497097 100644 --- a/composer.json +++ b/composer.json @@ -46,16 +46,16 @@ "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" + "friendsofphp/php-cs-fixer": "^3.65.0", + "guzzlehttp/guzzle": "^7.9.2", + "guzzlehttp/psr7": "^1.6 || ^2.7.0", + "phpstan/phpstan": "^1.12.13", + "phpstan/phpstan-phpunit": "^1.4.2", + "phpstan/phpstan-strict-rules": "^1.6.1", + "phpunit/phpunit": "^10.5.15 || ^11.5.1", + "psr/http-factory": "^1.1.0", + "psr/simple-cache": "^1.0.1 || ^2.0.0", + "symfony/cache": "^v5.0.0 || ^6.4.16" }, "suggest": { "psr/http-client-implementation": "To use the storage functionnality which depends on PSR-18", From 5a5cb792d3b897382832cd30d63f72b5f9313a27 Mon Sep 17 00:00:00 2001 From: ignace nyamagana butera Date: Thu, 19 Dec 2024 11:14:37 +0100 Subject: [PATCH 07/23] Update github actions and dependencies --- .github/workflows/build.yaml | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 31e3a97..d0f89e8 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -10,13 +10,8 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - php: ['8.1', '8.2', '8.3'] + php: ['8.1', '8.2', '8.3', '8.4'] stability: [prefer-lowest, prefer-stable] - include: - - php: '8.4' - flags: "--ignore-platform-req=php" - phpunit-flags: '--no-coverage' - stability: prefer-stable steps: - name: Checkout code uses: actions/checkout@v3 @@ -50,11 +45,6 @@ jobs: - name: Run Unit tests with coverage run: composer phpunit -- ${{ matrix.phpunit-flags }} - if: ${{ matrix.php == '8.3' || matrix.php == '8.2' || matrix.php == '8.1'}} - - - name: Run Unit tests without coverage - run: composer phpunit:min - if: ${{ matrix.php == '8.4'}} - name: Run static analysis run: composer phpstan From ac15896fdcb868eba4d366d820531bdc9baaed74 Mon Sep 17 00:00:00 2001 From: ignace nyamagana butera Date: Thu, 19 Dec 2024 20:59:47 +0100 Subject: [PATCH 08/23] Fix typo in composer manifest --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index e497097..b258858 100644 --- a/composer.json +++ b/composer.json @@ -58,9 +58,9 @@ "symfony/cache": "^v5.0.0 || ^6.4.16" }, "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", + "psr/http-client-implementation": "To use the storage functionality which depends on PSR-18", + "psr/http-factory-implementation": "To use the storage functionality which depends on PSR-17", + "psr/simple-cache-implementation": "To use the storage functionality which depends on PSR-16", "league/uri": "To parse URL and validate host" }, "autoload": { From 2d0caeac505ba7ecf5a56cc0b7c2d1653ed4608c Mon Sep 17 00:00:00 2001 From: ignace nyamagana butera Date: Thu, 19 Dec 2024 21:00:26 +0100 Subject: [PATCH 09/23] remove ext-json requirement which is always loaded in PHP8.1+ --- composer.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/composer.json b/composer.json index b258858..b76532d 100644 --- a/composer.json +++ b/composer.json @@ -42,8 +42,7 @@ "require": { "php": "^8.1", "ext-filter": "*", - "ext-intl": "*", - "ext-json": "*" + "ext-intl": "*" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.65.0", From e4f8435650bb4b8bc1ef9136a34c568d6938972b Mon Sep 17 00:00:00 2001 From: ignace nyamagana butera Date: Wed, 23 Apr 2025 00:12:08 +0200 Subject: [PATCH 10/23] #361 Adding the ability to use absolute domain (#362) Allow absolute domain name to be resolved --- .github/workflows/build.yaml | 4 +- .php-cs-fixer.php | 15 ++++---- CHANGELOG.md | 19 ++++++++++ README.md | 8 +++- composer.json | 10 +++-- src/Domain.php | 24 ++++++++++++ src/DomainName.php | 6 +++ src/DomainTest.php | 8 ---- src/Idna.php | 2 + src/IdnaInfo.php | 1 + src/IdnaInfoTest.php | 1 + src/PublicSuffixList.php | 6 +-- src/RegisteredName.php | 25 +++++++++++++ src/ResolvedDomain.php | 17 ++++++--- src/ResolvedDomainTest.php | 11 +----- src/Rules.php | 37 ++++++------------- src/RulesTest.php | 17 +++------ src/Storage/PublicSuffixListPsr16Cache.php | 1 + .../PublicSuffixListPsr16CacheTest.php | 7 ++-- .../PublicSuffixListPsr18ClientTest.php | 13 ++++--- src/Storage/RulesStorageTest.php | 13 ++++--- src/Storage/TimeToLive.php | 2 + src/Storage/TimeToLiveTest.php | 2 +- src/Storage/TopLevelDomainListPsr16Cache.php | 1 + .../TopLevelDomainListPsr16CacheTest.php | 5 ++- .../TopLevelDomainListPsr18ClientTest.php | 13 ++++--- src/Storage/TopLevelDomainsStorageTest.php | 13 ++++--- src/Suffix.php | 1 + src/SuffixTest.php | 1 + src/TopLevelDomainList.php | 4 +- src/TopLevelDomains.php | 25 +++++-------- src/TopLevelDomainsTest.php | 28 +++++++------- 32 files changed, 197 insertions(+), 143 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index d0f89e8..6fbc4a2 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -7,7 +7,7 @@ on: jobs: linux_tests: name: PHP on ${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.composer-flags }} - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: matrix: php: ['8.1', '8.2', '8.3', '8.4'] @@ -32,7 +32,7 @@ jobs: id: composer-cache run: | echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ matrix.stability }}-${{ matrix.flags }}-${{ hashFiles('**/composer.lock') }} diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 99023a2..c17d0c9 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -1,5 +1,7 @@ in(__DIR__.'/src') ; @@ -7,8 +9,9 @@ $config = new PhpCsFixer\Config(); return $config + ->setParallelConfig(ParallelConfigFactory::detect()) ->setRules([ - '@PSR2' => true, + '@PSR12' => true, 'array_syntax' => ['syntax' => 'short'], 'concat_space' => ['spacing' => 'none'], 'global_namespace_import' => [ @@ -16,23 +19,18 @@ 'import_constants' => true, 'import_functions' => true, ], - 'list_syntax' => ['syntax' => 'short'], 'new_with_parentheses' => true, 'no_blank_lines_after_phpdoc' => true, 'no_empty_phpdoc' => true, 'no_empty_comment' => true, 'no_leading_import_slash' => true, - 'no_superfluous_phpdoc_tags' => [ - 'allow_mixed' => true, - 'remove_inheritdoc' => true, - 'allow_unused_params' => false, - ], + 'no_superfluous_phpdoc_tags' => true, 'no_trailing_comma_in_singleline' => true, 'no_unused_imports' => true, 'nullable_type_declaration_for_default_null_value' => true, 'ordered_imports' => ['imports_order' => ['class', 'function', 'const'], 'sort_algorithm' => 'alpha'], 'phpdoc_add_missing_param_annotation' => ['only_untyped' => true], - 'phpdoc_align' => true, + 'phpdoc_align' => ['align' => 'left'], 'phpdoc_no_empty_return' => true, 'phpdoc_order' => true, 'phpdoc_scalar' => true, @@ -47,5 +45,6 @@ 'trailing_comma_in_multiline' => true, 'trim_array_spaces' => true, 'whitespace_after_comma_in_array' => true, + 'yoda_style' => true, ]) ->setFinder($finder); diff --git a/CHANGELOG.md b/CHANGELOG.md index 965c392..12cf2a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ All Notable changes to `PHP Domain Parser` starting from the **5.x** series will be documented in this file +## [6.4.0] - 2025-04-22 + +### Added + +- `DomainName::withRootLabel`, `DomainName::withoutRootLabel`, `DomainName::isAbsolute` methods to handle absolute domain names. + +### Fixed + +- Absolute domain name can now also be resolved by the package see issue [#361](https://github.com/jeremykendall/php-domain-parser/issues/361) prior to this release an exception was thrown. +- Since we no longer support PHP7 type hint and return type are improved. + +### Deprecated + +- None + +### Removed + +- None + ## [6.3.0] - 2023-02-25 ### Added diff --git a/README.md b/README.md index 68dc1b3..80f892d 100644 --- a/README.md +++ b/README.md @@ -36,10 +36,14 @@ composer require jeremykendall/php-domain-parser:^6.0 You need: -- **PHP >= 7.4** but the latest stable version of PHP is recommended -- the `intl` extension +- **PHP >= 8.1** but the latest stable version of PHP is recommended - a copy of the [Public Suffix List](https://publicsuffix.org/) data and/or a copy of the [IANA Top Level Domain List](https://www.iana.org/domains/root/files). Please refer to the [Managing external data source section](#managing-the-package-external-resources) for more information when using this package in production. +Handling of an IDN host requires the presence of the `intl` extension or +a polyfill for the `intl` IDN functions like the `symfony/polyfill-intl-idn` +otherwise an exception will be thrown when attempting to validate or interact +with such a host. + ## Usage > [!WARNING] diff --git a/composer.json b/composer.json index b76532d..245e9d2 100644 --- a/composer.json +++ b/composer.json @@ -41,8 +41,7 @@ ], "require": { "php": "^8.1", - "ext-filter": "*", - "ext-intl": "*" + "ext-filter": "*" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.65.0", @@ -54,13 +53,16 @@ "phpunit/phpunit": "^10.5.15 || ^11.5.1", "psr/http-factory": "^1.1.0", "psr/simple-cache": "^1.0.1 || ^2.0.0", - "symfony/cache": "^v5.0.0 || ^6.4.16" + "symfony/cache": "^v5.0.0 || ^6.4.16", + "symfony/var-dumper": "^v6.4.18 || ^7.2" }, "suggest": { "psr/http-client-implementation": "To use the storage functionality which depends on PSR-18", "psr/http-factory-implementation": "To use the storage functionality which depends on PSR-17", "psr/simple-cache-implementation": "To use the storage functionality which depends on PSR-16", - "league/uri": "To parse URL and validate host" + "league/uri": "To parse and extract the host from an URL using a RFC3986/RFC3987 URI parser", + "rowbot/url": "To parse and extract the host from an URL using a WHATWG URL parser", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "autoload": { "psr-4": { diff --git a/src/Domain.php b/src/Domain.php index 1d0bbf7..6764ff2 100644 --- a/src/Domain.php +++ b/src/Domain.php @@ -6,6 +6,7 @@ use Iterator; use Stringable; + use const FILTER_FLAG_IPV4; use const FILTER_VALIDATE_IP; @@ -159,4 +160,27 @@ public function slice(int $offset, ?int $length = null): self { return $this->newInstance($this->registeredName->slice($offset, $length)); } + + public function withRootLabel(): self + { + if ('' === $this->label(0)) { + return $this; + } + + return $this->append(''); + } + + public function withoutRootLabel(): self + { + if ('' === $this->label(0)) { + return $this->slice(1); + } + + return $this; + } + + public function isAbsolute(): bool + { + return '' === $this->label(0); + } } diff --git a/src/DomainName.php b/src/DomainName.php index fd6e3b9..d2610b2 100644 --- a/src/DomainName.php +++ b/src/DomainName.php @@ -14,6 +14,10 @@ * @see https://tools.ietf.org/html/rfc5890 * * @extends IteratorAggregate + * + * @method bool isAbsolute() tells whether the domain is absolute or not. + * @method self withRootLabel() returns an instance with its Root label. (see https://tools.ietf.org/html/rfc3986#section-3.2.2) + * @method self withoutRootLabel() returns an instance without its Root label. (see https://tools.ietf.org/html/rfc3986#section-3.2.2) */ interface DomainName extends Host, IteratorAggregate { @@ -120,4 +124,6 @@ public function clear(): self; * 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; + + } diff --git a/src/DomainTest.php b/src/DomainTest.php index e3ea7c0..da13679 100644 --- a/src/DomainTest.php +++ b/src/DomainTest.php @@ -6,8 +6,6 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; -use stdClass; -use TypeError; final class DomainTest extends TestCase { @@ -290,12 +288,6 @@ public static function withLabelWorksProvider(): iterable ]; } - public function testWithLabelFailsWithTypeError(): void - { - $this->expectException(TypeError::class); - Domain::fromIDNA2008('example.com')->withLabel(1, new stdClass()); /* @phpstan-ignore-line */ - } - public function testWithLabelFailsWithInvalidKey(): void { $this->expectException(SyntaxError::class); diff --git a/src/Idna.php b/src/Idna.php index cf33ca0..f09f72a 100644 --- a/src/Idna.php +++ b/src/Idna.php @@ -5,6 +5,7 @@ namespace Pdp; use UnexpectedValueException; + use function defined; use function function_exists; use function idn_to_ascii; @@ -12,6 +13,7 @@ use function preg_match; use function rawurldecode; use function strtolower; + use const INTL_IDNA_VARIANT_UTS46; /** diff --git a/src/IdnaInfo.php b/src/IdnaInfo.php index 5724c6f..7afd6b2 100644 --- a/src/IdnaInfo.php +++ b/src/IdnaInfo.php @@ -5,6 +5,7 @@ namespace Pdp; use function array_filter; + use const ARRAY_FILTER_USE_KEY; /** diff --git a/src/IdnaInfoTest.php b/src/IdnaInfoTest.php index cd2fcde..1ecd93b 100644 --- a/src/IdnaInfoTest.php +++ b/src/IdnaInfoTest.php @@ -5,6 +5,7 @@ namespace Pdp; use PHPUnit\Framework\TestCase; + use function var_export; final class IdnaInfoTest extends TestCase diff --git a/src/PublicSuffixList.php b/src/PublicSuffixList.php index b63b64c..ecbd75a 100644 --- a/src/PublicSuffixList.php +++ b/src/PublicSuffixList.php @@ -9,7 +9,7 @@ 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 SyntaxError if the domain is invalid * @throws UnableToResolveDomain if the effective TLD can not be resolved */ public function getCookieDomain(Host $host): ResolvedDomainName; @@ -17,7 +17,7 @@ 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 SyntaxError if the domain is invalid * @throws UnableToResolveDomain if the domain does not contain a ICANN Effective TLD */ public function getICANNDomain(Host $host): ResolvedDomainName; @@ -25,7 +25,7 @@ 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 SyntaxError if the domain is invalid * @throws UnableToResolveDomain if the domain does not contain a private Effective TLD */ public function getPrivateDomain(Host $host): ResolvedDomainName; diff --git a/src/RegisteredName.php b/src/RegisteredName.php index cd12eea..ac6d68a 100644 --- a/src/RegisteredName.php +++ b/src/RegisteredName.php @@ -6,6 +6,7 @@ use Iterator; use Stringable; + use function array_count_values; use function array_keys; use function array_reverse; @@ -19,6 +20,7 @@ use function preg_match; use function rawurldecode; use function strtolower; + use const FILTER_FLAG_IPV4; use const FILTER_VALIDATE_IP; @@ -359,4 +361,27 @@ public function slice(int $offset, ?int $length = null): self return new self($this->type, [] === $labels ? null : implode('.', array_reverse($labels))); } + + public function withRootLabel(): self + { + if ('' === $this->label(0)) { + return $this; + } + + return $this->append(''); + } + + public function withoutRootLabel(): self + { + if ('' === $this->label(0)) { + return $this->slice(1); + } + + return $this; + } + + public function isAbsolute(): bool + { + return '' === $this->label(0); + } } diff --git a/src/ResolvedDomain.php b/src/ResolvedDomain.php index f4fb87c..1994531 100644 --- a/src/ResolvedDomain.php +++ b/src/ResolvedDomain.php @@ -5,6 +5,7 @@ namespace Pdp; use Stringable; + use function count; final class ResolvedDomain implements ResolvedDomainName @@ -34,7 +35,7 @@ public static function fromICANN(DomainNameProvider|Host|Stringable|string|int|n { $domain = self::setDomainName($domain); - return new self($domain, Suffix::fromICANN($domain->slice(0, $suffixLength))); + return new self($domain, Suffix::fromICANN($domain->withoutRootLabel()->slice(0, $suffixLength))); } /** @@ -44,7 +45,7 @@ public static function fromPrivate(DomainNameProvider|Host|Stringable|string|int { $domain = self::setDomainName($domain); - return new self($domain, Suffix::fromPrivate($domain->slice(0, $suffixLength))); + return new self($domain, Suffix::fromPrivate($domain->withoutRootLabel()->slice(0, $suffixLength))); } /** @@ -54,7 +55,7 @@ public static function fromIANA(DomainNameProvider|Host|Stringable|string|int|nu { $domain = self::setDomainName($domain); - return new self($domain, Suffix::fromIANA($domain->label(0))); + return new self($domain, Suffix::fromIANA($domain->withoutRootLabel()->label(0))); } /** @@ -118,11 +119,15 @@ private function parse(): array } $length = count($this->suffix); + $offset = 0; + if ($this->domain->isAbsolute()) { + $offset = 1; + } return [ - 'registrableDomain' => $this->domain->slice(0, $length + 1), - 'secondLevelDomain' => $this->domain->slice($length, 1), - 'subDomain' => RegisteredName::fromIDNA2008($this->domain->value())->slice($length + 1), + 'registrableDomain' => $this->domain->slice($offset, $length + 1), + 'secondLevelDomain' => $this->domain->slice($length + $offset, 1), + 'subDomain' => RegisteredName::fromIDNA2008($this->domain->value())->slice($length + 1 + $offset), ]; } diff --git a/src/ResolvedDomainTest.php b/src/ResolvedDomainTest.php index 12c777b..0c391c9 100644 --- a/src/ResolvedDomainTest.php +++ b/src/ResolvedDomainTest.php @@ -6,8 +6,6 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; -use TypeError; -use function date_create; final class ResolvedDomainTest extends TestCase { @@ -300,13 +298,6 @@ public function testItCanThrowsDuringSubDomainChangesIfTheSubDomainIsTheEmptyStr ResolvedDomain::fromICANN('www.example.com', 1)->withSubDomain(''); } - public function testItCanThrowsDuringSubDomainChangesIfTheSubDomainIsNotStringable(): void - { - $this->expectException(TypeError::class); - - ResolvedDomain::fromICANN('www.example.com', 1)->withSubDomain(date_create()); /* @phpstan-ignore-line */ - } - #[DataProvider('withPublicSuffixWorksProvider')] public function testItCanChangeItsSuffix( ResolvedDomain $domain, @@ -397,7 +388,7 @@ public static function withPublicSuffixWorksProvider(): iterable 'isICANN' => false, 'isPrivate' => false, ], - 'with custom IDNA domain options' =>[ + 'with custom IDNA domain options' => [ 'domain' => ResolvedDomain::fromICANN('www.bébé.be', 1), 'publicSuffix' => null, 'expected' => null, diff --git a/src/Rules.php b/src/Rules.php index 6c26b49..cdd4d4b 100644 --- a/src/Rules.php +++ b/src/Rules.php @@ -7,6 +7,7 @@ use SplFileObject; use SplTempFileObject; use Stringable; + use function array_pop; use function explode; use function preg_match; @@ -42,7 +43,7 @@ private function __construct(private readonly array $rules) * * @param null|resource $context * - * @throws UnableToLoadResource If the rules can not be loaded from the path + * @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 @@ -104,8 +105,8 @@ private static function getSection(string $section, string $line): string * A copy of the Apache License, Version 2.0, is provided with this * distribution * - * @param array $list Initially an empty array, this eventually becomes the array representation of a - * Public Suffix List section + * @param array $list Initially an empty array, this eventually becomes the array representation of a + * Public Suffix List section * @param array $ruleParts One line (rule) from the Public Suffix List exploded on '.', or the remaining * portion of that array during recursion * @@ -151,10 +152,7 @@ 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 + public function resolve(DomainNameProvider|Host|Stringable|string|int|null $host): ResolvedDomainName { try { return $this->getCookieDomain($host); @@ -165,13 +163,10 @@ public function resolve($host): ResolvedDomainName } } - /** - * @param int|DomainNameProvider|Host|string|Stringable|null $host the domain value - */ - public function getCookieDomain($host): ResolvedDomainName + public function getCookieDomain(DomainNameProvider|Host|Stringable|string|int|null $host): ResolvedDomainName { $domain = $this->validateDomain($host); - [$suffixLength, $section] = $this->resolveSuffix($domain, self::UNKNOWN_DOMAINS); + [$suffixLength, $section] = $this->resolveSuffix($domain->withoutRootLabel(), self::UNKNOWN_DOMAINS); return match (true) { self::ICANN_DOMAINS === $section => ResolvedDomain::fromICANN($domain, $suffixLength), @@ -180,10 +175,7 @@ public function getCookieDomain($host): ResolvedDomainName }; } - /** - * @param int|DomainNameProvider|Host|string|Stringable|null $host a type that supports instantiating a Domain from. - */ - public function getICANNDomain($host): ResolvedDomainName + public function getICANNDomain(DomainNameProvider|Host|Stringable|string|int|null $host): ResolvedDomainName { $domain = $this->validateDomain($host); [$suffixLength, $section] = $this->resolveSuffix($domain, self::ICANN_DOMAINS); @@ -194,10 +186,7 @@ public function getICANNDomain($host): ResolvedDomainName 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 + public function getPrivateDomain(DomainNameProvider|Host|Stringable|string|int|null $host): ResolvedDomainName { $domain = $this->validateDomain($host); [$suffixLength, $section] = $this->resolveSuffix($domain, self::PRIVATE_DOMAINS); @@ -211,10 +200,10 @@ public function getPrivateDomain($host): ResolvedDomainName /** * Assert the domain is valid and is resolvable. * - * @throws SyntaxError If the domain is invalid + * @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 + private function validateDomain(DomainNameProvider|Host|Stringable|string|int|null $domain): DomainName { if ($domain instanceof DomainNameProvider) { $domain = $domain->domain(); @@ -224,10 +213,6 @@ private function validateDomain(int|DomainNameProvider|Host|string|Stringable|nu $domain = Domain::fromIDNA2008($domain); } - if ('' === $domain->label(0)) { - throw UnableToResolveDomain::dueToUnresolvableDomain($domain); - } - return $domain; } diff --git a/src/RulesTest.php b/src/RulesTest.php index b8e4838..be4f25e 100644 --- a/src/RulesTest.php +++ b/src/RulesTest.php @@ -6,7 +6,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; -use TypeError; + use function array_fill; use function dirname; use function file_get_contents; @@ -27,7 +27,7 @@ public static function setUpBeforeClass(): void public function testCreateFromPath(): void { $context = stream_context_create([ - 'http'=> [ + 'http' => [ 'method' => 'GET', 'header' => "Accept-language: en\r\nCookie: foo=bar\r\n", ], @@ -66,13 +66,6 @@ public function testNullWillReturnNullDomain(): void self::assertFalse($domain->suffix()->isKnown()); } - public function testThrowsTypeErrorOnWrongInput(): void - { - $this->expectException(TypeError::class); - - self::$rules->resolve(date_create()); /* @phpstan-ignore-line */ - } - public function testIsSuffixValidFalse(): void { $domain = self::$rules->resolve('www.example.faketld'); @@ -129,10 +122,10 @@ public function testWithAbsoluteHostInvalid(): void $domain = self::$rules->resolve('private.ulb.ac.be.'); self::assertSame('private.ulb.ac.be.', $domain->value()); - self::assertFalse($domain->suffix()->isKnown()); - self::assertFalse($domain->suffix()->isICANN()); + self::assertTrue($domain->suffix()->isKnown()); + self::assertTrue($domain->suffix()->isICANN()); self::assertFalse($domain->suffix()->isPrivate()); - self::assertNull($domain->suffix()->value()); + self::assertSame('ac.be', $domain->suffix()->value()); } public function testWithICANNDomainInvalid(): void diff --git a/src/Storage/PublicSuffixListPsr16Cache.php b/src/Storage/PublicSuffixListPsr16Cache.php index b8753af..676c164 100644 --- a/src/Storage/PublicSuffixListPsr16Cache.php +++ b/src/Storage/PublicSuffixListPsr16Cache.php @@ -11,6 +11,7 @@ use Psr\SimpleCache\CacheInterface; use Stringable; use Throwable; + use function md5; use function strtolower; diff --git a/src/Storage/PublicSuffixListPsr16CacheTest.php b/src/Storage/PublicSuffixListPsr16CacheTest.php index f41cfb5..686294b 100644 --- a/src/Storage/PublicSuffixListPsr16CacheTest.php +++ b/src/Storage/PublicSuffixListPsr16CacheTest.php @@ -13,6 +13,7 @@ use Psr\SimpleCache\CacheException; use Psr\SimpleCache\CacheInterface; use RuntimeException; + use function dirname; final class PublicSuffixListPsr16CacheTest extends TestCase @@ -81,7 +82,7 @@ public function testItReturnsFalseIfItCantStoreAPublicSuffixListInstance(): void public function testItReturnsFalseIfItCantCacheAPublicSuffixListInstance(): void { - $exception = new class('Something went wrong.', 0) extends RuntimeException implements CacheException { + $exception = new class ('Something went wrong.', 0) extends RuntimeException implements CacheException { }; $cache = self::createStub(CacheInterface::class); $cache->method('set')->will(self::throwException($exception)); @@ -94,13 +95,13 @@ public function testItReturnsFalseIfItCantCacheAPublicSuffixListInstance(): void public function testItWillThrowIfItCantCacheAPublicSuffixListInstance(): void { - $exception = new class('Something went wrong.', 0) extends RuntimeException { + $exception = new class ('Something went wrong.', 0) extends RuntimeException { }; $cache = self::createStub(CacheInterface::class); $cache->method('set')->will(self::throwException($exception)); $psl = Rules::fromPath(dirname(__DIR__, 2).'/test_data/public_suffix_list.dat'); - $pslCache = new PublicSuffixListPsr16Cache($cache, 'pdp_', new class() { + $pslCache = new PublicSuffixListPsr16Cache($cache, 'pdp_', new class () { public function __toString(): string { return '1 DAY'; diff --git a/src/Storage/PublicSuffixListPsr18ClientTest.php b/src/Storage/PublicSuffixListPsr18ClientTest.php index 1117abb..eb4b237 100644 --- a/src/Storage/PublicSuffixListPsr18ClientTest.php +++ b/src/Storage/PublicSuffixListPsr18ClientTest.php @@ -14,6 +14,7 @@ use Psr\Http\Message\RequestFactoryInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; + use function dirname; use function file_get_contents; @@ -21,7 +22,7 @@ final class PublicSuffixListPsr18ClientTest extends TestCase { public function testIsCanReturnAPublicSuffixListInstance(): void { - $client = new class() implements ClientInterface { + $client = new class () implements ClientInterface { public function sendRequest(RequestInterface $request): ResponseInterface { /** @var string $body */ @@ -30,7 +31,7 @@ public function sendRequest(RequestInterface $request): ResponseInterface } }; - $requestFactory = new class() implements RequestFactoryInterface { + $requestFactory = new class () implements RequestFactoryInterface { public function createRequest(string $method, $uri): RequestInterface { return new Request($method, $uri); @@ -45,14 +46,14 @@ public function createRequest(string $method, $uri): RequestInterface public function testItWillThrowIfTheClientCanNotConnectToTheRemoteURI(): void { - $client = new class() implements ClientInterface { + $client = new class () implements ClientInterface { public function sendRequest(RequestInterface $request): ResponseInterface { throw new ConnectException('foobar', $request, null); } }; - $requestFactory = new class() implements RequestFactoryInterface { + $requestFactory = new class () implements RequestFactoryInterface { public function createRequest(string $method, $uri): RequestInterface { return new Request($method, $uri); @@ -68,14 +69,14 @@ public function createRequest(string $method, $uri): RequestInterface public function testItWillThrowIfTheReturnedStatusCodeIsNotOK(): void { - $client = new class() implements ClientInterface { + $client = new class () implements ClientInterface { public function sendRequest(RequestInterface $request): ResponseInterface { return new Response(404); } }; - $requestFactory = new class() implements RequestFactoryInterface { + $requestFactory = new class () implements RequestFactoryInterface { public function createRequest(string $method, $uri): RequestInterface { return new Request($method, $uri); diff --git a/src/Storage/RulesStorageTest.php b/src/Storage/RulesStorageTest.php index 723628e..8606ffe 100644 --- a/src/Storage/RulesStorageTest.php +++ b/src/Storage/RulesStorageTest.php @@ -7,13 +7,14 @@ use Pdp\PublicSuffixList; use Pdp\Rules; use PHPUnit\Framework\TestCase; + use function dirname; final class RulesStorageTest extends TestCase { public function testIsCanReturnAPublicSuffixListInstanceFromCache(): void { - $cache = new class() implements PublicSuffixListCache { + $cache = new class () implements PublicSuffixListCache { public function fetch(string $uri): ?PublicSuffixList { return Rules::fromPath(dirname(__DIR__, 2).'/test_data/public_suffix_list.dat'); @@ -30,7 +31,7 @@ public function forget(string $uri): bool } }; - $client = new class() implements PublicSuffixListClient { + $client = new class () implements PublicSuffixListClient { public function get(string $uri): PublicSuffixList { return Rules::fromPath(dirname(__DIR__, 2).'/test_data/public_suffix_list.dat'); @@ -45,7 +46,7 @@ public function get(string $uri): PublicSuffixList public function testIsCanReturnAPublicSuffixListInstanceFromTheInnerStorage(): void { - $cache = new class() implements PublicSuffixListCache { + $cache = new class () implements PublicSuffixListCache { public function fetch(string $uri): ?PublicSuffixList { return null; @@ -62,7 +63,7 @@ public function forget(string $uri): bool } }; - $client = new class() implements PublicSuffixListClient { + $client = new class () implements PublicSuffixListClient { public function get(string $uri): PublicSuffixList { return Rules::fromPath(dirname(__DIR__, 2).'/test_data/public_suffix_list.dat'); @@ -77,7 +78,7 @@ public function get(string $uri): PublicSuffixList public function testIsCanDeleteAPublicSuffixListInstanceFromTheInnerStorage(): void { - $cache = new class() implements PublicSuffixListCache { + $cache = new class () implements PublicSuffixListCache { public function fetch(string $uri): ?PublicSuffixList { return null; @@ -94,7 +95,7 @@ public function forget(string $uri): bool } }; - $client = new class() implements PublicSuffixListClient { + $client = new class () implements PublicSuffixListClient { public function get(string $uri): PublicSuffixList { return Rules::fromPath(dirname(__DIR__, 2).'/test_data/public_suffix_list.dat'); diff --git a/src/Storage/TimeToLive.php b/src/Storage/TimeToLive.php index 3bbe04a..6d0289f 100644 --- a/src/Storage/TimeToLive.php +++ b/src/Storage/TimeToLive.php @@ -10,7 +10,9 @@ use InvalidArgumentException; use Stringable; use Throwable; + use function filter_var; + use const FILTER_VALIDATE_INT; /** diff --git a/src/Storage/TimeToLiveTest.php b/src/Storage/TimeToLiveTest.php index 9ca00b7..43d7bff 100644 --- a/src/Storage/TimeToLiveTest.php +++ b/src/Storage/TimeToLiveTest.php @@ -76,7 +76,7 @@ public static function validDurationInt(): iterable ]; yield 'stringable object' => [ - 'input' => new class() { + 'input' => new class () { public function __toString(): string { return '2345'; diff --git a/src/Storage/TopLevelDomainListPsr16Cache.php b/src/Storage/TopLevelDomainListPsr16Cache.php index e2b2c6b..2e76284 100644 --- a/src/Storage/TopLevelDomainListPsr16Cache.php +++ b/src/Storage/TopLevelDomainListPsr16Cache.php @@ -11,6 +11,7 @@ use Psr\SimpleCache\CacheInterface; use Stringable; use Throwable; + use function md5; use function strtolower; diff --git a/src/Storage/TopLevelDomainListPsr16CacheTest.php b/src/Storage/TopLevelDomainListPsr16CacheTest.php index 2eb8cb4..fc91144 100644 --- a/src/Storage/TopLevelDomainListPsr16CacheTest.php +++ b/src/Storage/TopLevelDomainListPsr16CacheTest.php @@ -13,6 +13,7 @@ use Psr\SimpleCache\CacheException; use Psr\SimpleCache\CacheInterface; use RuntimeException; + use function dirname; final class TopLevelDomainListPsr16CacheTest extends TestCase @@ -82,7 +83,7 @@ public function testItReturnsFalseIfItCantStoreAPublicSuffixListInstance(): void public function testItReturnsFalseIfItCantCacheATopLevelDomainListInstance(): void { - $exception = new class('Something went wrong.', 0) extends RuntimeException implements CacheException { + $exception = new class ('Something went wrong.', 0) extends RuntimeException implements CacheException { }; $cache = self::createStub(CacheInterface::class); $cache->method('set')->will(self::throwException($exception)); @@ -95,7 +96,7 @@ public function testItReturnsFalseIfItCantCacheATopLevelDomainListInstance(): vo public function testItThrowsIfItCantCacheATopLevelDomainListInstance(): void { - $exception = new class('Something went wrong.', 0) extends RuntimeException { + $exception = new class ('Something went wrong.', 0) extends RuntimeException { }; $cache = self::createStub(CacheInterface::class); $cache->method('set')->will(self::throwException($exception)); diff --git a/src/Storage/TopLevelDomainListPsr18ClientTest.php b/src/Storage/TopLevelDomainListPsr18ClientTest.php index 9f4b0b6..eb5a5b4 100644 --- a/src/Storage/TopLevelDomainListPsr18ClientTest.php +++ b/src/Storage/TopLevelDomainListPsr18ClientTest.php @@ -14,6 +14,7 @@ use Psr\Http\Message\RequestFactoryInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; + use function dirname; use function file_get_contents; @@ -21,7 +22,7 @@ final class TopLevelDomainListPsr18ClientTest extends TestCase { public function testIsCanReturnARootZoneDatabaseInstance(): void { - $client = new class() implements ClientInterface { + $client = new class () implements ClientInterface { public function sendRequest(RequestInterface $request): ResponseInterface { /** @var string $body */ @@ -31,7 +32,7 @@ public function sendRequest(RequestInterface $request): ResponseInterface } }; - $requestFactory = new class() implements RequestFactoryInterface { + $requestFactory = new class () implements RequestFactoryInterface { public function createRequest(string $method, $uri): RequestInterface { return new Request($method, $uri); @@ -46,14 +47,14 @@ public function createRequest(string $method, $uri): RequestInterface public function testItWillThrowIfTheClientCanNotConnectToTheRemoteURI(): void { - $client = new class() implements ClientInterface { + $client = new class () implements ClientInterface { public function sendRequest(RequestInterface $request): ResponseInterface { throw new ConnectException('foobar', $request, null); } }; - $requestFactory = new class() implements RequestFactoryInterface { + $requestFactory = new class () implements RequestFactoryInterface { public function createRequest(string $method, $uri): RequestInterface { return new Request($method, $uri); @@ -69,14 +70,14 @@ public function createRequest(string $method, $uri): RequestInterface public function testItWillThrowIfTheReturnedStatusCodeIsNotOK(): void { - $client = new class() implements ClientInterface { + $client = new class () implements ClientInterface { public function sendRequest(RequestInterface $request): ResponseInterface { return new Response(404); } }; - $requestFactory = new class() implements RequestFactoryInterface { + $requestFactory = new class () implements RequestFactoryInterface { public function createRequest(string $method, $uri): RequestInterface { return new Request($method, $uri); diff --git a/src/Storage/TopLevelDomainsStorageTest.php b/src/Storage/TopLevelDomainsStorageTest.php index 0e299e9..2da00d1 100644 --- a/src/Storage/TopLevelDomainsStorageTest.php +++ b/src/Storage/TopLevelDomainsStorageTest.php @@ -7,13 +7,14 @@ use Pdp\TopLevelDomainList; use Pdp\TopLevelDomains; use PHPUnit\Framework\TestCase; + use function dirname; final class TopLevelDomainsStorageTest extends TestCase { public function testIsCanReturnARootZoneDatabaseInstanceFromCache(): void { - $cache = new class() implements TopLevelDomainListCache { + $cache = new class () implements TopLevelDomainListCache { public function fetch(string $uri): ?TopLevelDomainList { return TopLevelDomains::fromPath(dirname(__DIR__, 2).'/test_data/tlds-alpha-by-domain.txt'); @@ -30,7 +31,7 @@ public function forget(string $uri): bool } }; - $client = new class() implements TopLevelDomainListClient { + $client = new class () implements TopLevelDomainListClient { public function get(string $uri): TopLevelDomainList { return TopLevelDomains::fromPath(dirname(__DIR__, 2).'/test_data/tlds-alpha-by-domain.txt'); @@ -44,7 +45,7 @@ public function get(string $uri): TopLevelDomainList public function testIsCanReturnARootZoneDatabaseInstanceFromTheInnerStorage(): void { - $cache = new class() implements TopLevelDomainListCache { + $cache = new class () implements TopLevelDomainListCache { public function fetch(string $uri): ?TopLevelDomainList { return null; @@ -61,7 +62,7 @@ public function forget(string $uri): bool } }; - $client = new class() implements TopLevelDomainListClient { + $client = new class () implements TopLevelDomainListClient { public function get(string $uri): TopLevelDomainList { return TopLevelDomains::fromPath(dirname(__DIR__, 2).'/test_data/tlds-alpha-by-domain.txt'); @@ -75,7 +76,7 @@ public function get(string $uri): TopLevelDomainList public function testIsCanDeleteARootZoneDatabaseInstanceFromTheInnerStorage(): void { - $cache = new class() implements TopLevelDomainListCache { + $cache = new class () implements TopLevelDomainListCache { public function fetch(string $uri): ?TopLevelDomainList { return null; @@ -92,7 +93,7 @@ public function forget(string $uri): bool } }; - $client = new class() implements TopLevelDomainListClient { + $client = new class () implements TopLevelDomainListClient { public function get(string $uri): TopLevelDomainList { return TopLevelDomains::fromPath(dirname(__DIR__, 2).'/test_data/tlds-alpha-by-domain.txt'); diff --git a/src/Suffix.php b/src/Suffix.php index da39728..3f0a615 100644 --- a/src/Suffix.php +++ b/src/Suffix.php @@ -5,6 +5,7 @@ namespace Pdp; use Stringable; + use function count; use function in_array; diff --git a/src/SuffixTest.php b/src/SuffixTest.php index b3e6d56..bf47266 100644 --- a/src/SuffixTest.php +++ b/src/SuffixTest.php @@ -6,6 +6,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; + use function json_encode; final class SuffixTest extends TestCase diff --git a/src/TopLevelDomainList.php b/src/TopLevelDomainList.php index 51e9b0f..1ca3eb5 100644 --- a/src/TopLevelDomainList.php +++ b/src/TopLevelDomainList.php @@ -24,7 +24,7 @@ public function version(): string; */ public function lastUpdated(): DateTimeImmutable; - + public function count(): int; /** @@ -40,7 +40,7 @@ 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 SyntaxError if the domain is invalid * @throws UnableToResolveDomain if the domain does not contain a IANA Effective TLD */ public function getIANADomain(Host $host): ResolvedDomainName; diff --git a/src/TopLevelDomains.php b/src/TopLevelDomains.php index 8f956cf..98384c2 100644 --- a/src/TopLevelDomains.php +++ b/src/TopLevelDomains.php @@ -9,8 +9,8 @@ use SplFileObject; use SplTempFileObject; use Stringable; + use function count; -use function in_array; use function preg_match; use function trim; @@ -34,7 +34,7 @@ private function __construct( * * @param null|resource $context * - * @throws UnableToLoadResource If the rules can not be loaded from the path + * @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 @@ -164,14 +164,11 @@ 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 + public function resolve(DomainNameProvider|Host|Stringable|string|int|null $host): ResolvedDomainName { try { $domain = $this->validateDomain($host); - if ($this->containsTopLevelDomain($domain)) { + if ($this->containsTopLevelDomain($domain->withoutRootLabel())) { return ResolvedDomain::fromIANA($domain); } return ResolvedDomain::fromUnknown($domain); @@ -185,10 +182,10 @@ public function resolve($host): ResolvedDomainName /** * Assert the domain is valid and is resolvable. * - * @throws SyntaxError If the domain is invalid + * @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 + private function validateDomain(DomainNameProvider|Host|Stringable|string|int|null $domain): DomainName { if ($domain instanceof DomainNameProvider) { $domain = $domain->domain(); @@ -198,8 +195,7 @@ private function validateDomain(int|DomainNameProvider|Host|string|Stringable|nu $domain = Domain::fromIDNA2008($domain); } - $label = $domain->label(0); - if (in_array($label, [null, ''], true)) { + if (null === $domain->label(0)) { throw UnableToResolveDomain::dueToUnresolvableDomain($domain); } @@ -211,13 +207,10 @@ 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 + public function getIANADomain(DomainNameProvider|Host|Stringable|string|int|null $host): ResolvedDomainName { $domain = $this->validateDomain($host); - if (!$this->containsTopLevelDomain($domain)) { + if (!$this->containsTopLevelDomain($domain->withoutRootLabel())) { throw UnableToResolveDomain::dueToMissingSuffix($domain, 'IANA'); } diff --git a/src/TopLevelDomainsTest.php b/src/TopLevelDomainsTest.php index ca86e11..e756c6c 100644 --- a/src/TopLevelDomainsTest.php +++ b/src/TopLevelDomainsTest.php @@ -9,7 +9,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Stringable; -use TypeError; + use function dirname; final class TopLevelDomainsTest extends TestCase @@ -24,7 +24,7 @@ public static function setUpBeforeClass(): void public function testCreateFromPath(): void { $context = stream_context_create([ - 'http'=> [ + 'http' => [ 'method' => 'GET', 'header' => "Accept-language: en\r\nCookie: foo=bar\r\n", ], @@ -176,7 +176,7 @@ public static function validDomainProvider(): iterable 'Unicode domain (1)' => ['الاعلى-للاتصالات.قطر'], 'Unicode domain (2)' => ['кто.рф'], 'Unicode domain (3)' => ['Deutsche.Vermögensberatung.vermögensberater'], - 'object with __toString method' => [new class() { + 'object with __toString method' => [new class () { public function __toString(): string { return 'www.இந.இந்தியா'; @@ -186,13 +186,6 @@ public function __toString(): string ]; } - public function testTopLevelDomainThrowsTypeError(): void - { - $this->expectException(TypeError::class); - - self::$topLevelDomains->getIANADomain(new DateTimeImmutable()); /* @phpstan-ignore-line */ - } - public function testTopLevelDomainWithInvalidDomain(): void { $this->expectException(SyntaxError::class); @@ -211,8 +204,8 @@ public function testResolveWithAbsoluteDomainName(): void { $result = self::$topLevelDomains->resolve('example.com.'); self::assertSame('example.com.', $result->value()); - self::assertFalse($result->suffix()->isIANA()); - self::assertNull($result->suffix()->value()); + self::assertTrue($result->suffix()->isIANA()); + self::assertSame('com', $result->suffix()->value()); } public function testTopLevelDomainWithUnResolvableDomain(): void @@ -222,6 +215,13 @@ public function testTopLevelDomainWithUnResolvableDomain(): void self::$topLevelDomains->getIANADomain('localhost'); } + public function testTopLevelDomainWithUnResolvableDomain2(): void + { + $this->expectException(UnableToResolveDomain::class); + + self::$topLevelDomains->getIANADomain('localhost.'); + } + public function testResolveWithUnResolvableDomain(): void { $result = self::$topLevelDomains->resolve('localhost'); @@ -269,7 +269,7 @@ public static function validTldProvider(): iterable 'Unicode TLD (3)' => ['рф'], 'Unicode TLD (4)' => ['இந்தியா'], 'Unicode TLD (5)' => ['vermögensberater'], - 'object with __toString method' => [new class() { + 'object with __toString method' => [new class () { public function __toString(): string { return 'COM'; @@ -296,7 +296,7 @@ public static function invalidTldProvider(): iterable 'invalid IDN to ASCII' => ['XN--TTT'], 'invalid IDN to ASCII with leading dot' => ['.XN--TTT'], 'null' => [null], - 'object with __toString method' => [new class() { + 'object with __toString method' => [new class () { public function __toString(): string { return 'COMMM'; From 504828ee1dab2ab81a0d1719a02ab3c6f6295140 Mon Sep 17 00:00:00 2001 From: ignace nyamagana butera Date: Wed, 23 Apr 2025 09:32:59 +0200 Subject: [PATCH 11/23] Update documentation to add support for absolute domain --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.md b/README.md index 80f892d..8e85738 100644 --- a/README.md +++ b/README.md @@ -228,6 +228,13 @@ supported type to avoid unexpected results. By default, if the input is not a `Pdp\Suffix` instance, the resulting public suffix will be labelled as being unknown. For more information go to the [Public Suffix section](#public-suffix) +> [!INFO] +> Since version `6.4` Domain resolution is also adapted so that absolute domain +can effectively be resolved. Prior to version `6.4` an exception was thrown. +The `ResolvedDomain` stays the same as with the equivalent non-absolute domain +but with the only difference being that the resolved domain will be absolute. +Everything else stays the same. + ### Domain Suffix The domain effective TLD is represented using the `Pdp\Suffix`. Depending on @@ -342,6 +349,26 @@ $newDomain->clear()->labels(); //return [] echo $domain->slice(2)->toString(); //display 'www' ~~~ +Starting with version `6.4` it is possible to specify and handle absolute domain +with the following methods. + +~~~php +resolve(Domain::from2008('www.ExAmpLE.cOM'))->domain(); +$newDomain = $domain->withRootLabel(); + +echo $domain->toString(); //display 'www.example.com' +echo $newDomain->toString(); //display 'www.example.com.www.' +$domain->isAbsolute(); //return false +$newDomain->isAbsolute() //return true + +$domain->value() === $newDomain->withoutRootLabel()->value(); +~~~ + > [!WARNING] > Because of its definition, a domain name can be `null` or a string. From 0a3902e9199705c339c5c20fc60347e8aebac53b Mon Sep 17 00:00:00 2001 From: ignace nyamagana butera Date: Wed, 23 Apr 2025 09:36:57 +0200 Subject: [PATCH 12/23] Update documentation to add support for absolute domain --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index 8e85738..3dbc5db 100644 --- a/README.md +++ b/README.md @@ -228,12 +228,9 @@ supported type to avoid unexpected results. By default, if the input is not a `Pdp\Suffix` instance, the resulting public suffix will be labelled as being unknown. For more information go to the [Public Suffix section](#public-suffix) -> [!INFO] +> [!TIP] > Since version `6.4` Domain resolution is also adapted so that absolute domain can effectively be resolved. Prior to version `6.4` an exception was thrown. -The `ResolvedDomain` stays the same as with the equivalent non-absolute domain -but with the only difference being that the resolved domain will be absolute. -Everything else stays the same. ### Domain Suffix From f6581fd7770eb9fe62d7db1d9b3ad210cdaf1ca4 Mon Sep 17 00:00:00 2001 From: ignace nyamagana butera Date: Wed, 23 Apr 2025 17:49:35 +0200 Subject: [PATCH 13/23] ResolvedDomain should handle domain with root label --- src/DomainName.php | 2 -- src/ResolvedDomain.php | 16 +++++++++++++--- src/ResolvedDomainTest.php | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/src/DomainName.php b/src/DomainName.php index d2610b2..b5c736d 100644 --- a/src/DomainName.php +++ b/src/DomainName.php @@ -124,6 +124,4 @@ public function clear(): self; * 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; - - } diff --git a/src/ResolvedDomain.php b/src/ResolvedDomain.php index 1994531..70d4644 100644 --- a/src/ResolvedDomain.php +++ b/src/ResolvedDomain.php @@ -10,17 +10,21 @@ final class ResolvedDomain implements ResolvedDomainName { + private readonly DomainName $domain; private readonly DomainName $secondLevelDomain; private readonly DomainName $registrableDomain; private readonly DomainName $subDomain; + private readonly EffectiveTopLevelDomain $suffix; /** * @throws CannotProcessHost */ private function __construct( - private readonly DomainName $domain, - private readonly EffectiveTopLevelDomain $suffix + DomainName $domain, + EffectiveTopLevelDomain $suffix ) { + $this->domain = $domain; + $this->suffix = $suffix; [ 'registrableDomain' => $this->registrableDomain, 'secondLevelDomain' => $this->secondLevelDomain, @@ -195,8 +199,10 @@ public function withSuffix(DomainNameProvider|Host|Stringable|string|int|null $s $suffix = Suffix::fromUnknown($suffix); } + $newDomain = $this->domain->withoutRootLabel()->slice(count($this->suffix))->append($suffix); + return new self( - $this->domain->slice(count($this->suffix))->append($suffix), + $this->domain->isAbsolute() ? $newDomain->withRootLabel() : $newDomain, $suffix->normalize($this->domain) ); } @@ -211,6 +217,10 @@ public function withSubDomain(DomainNameProvider|Host|Stringable|string|int|null } $subDomain = RegisteredName::fromIDNA2008($subDomain); + if ('' === $subDomain->withoutRootLabel()->value()) { + throw SyntaxError::dueToMalformedValue($subDomain->toString()); + } + if ($this->subDomain->value() === $subDomain->value()) { return $this; } diff --git a/src/ResolvedDomainTest.php b/src/ResolvedDomainTest.php index 0c391c9..050a2a9 100644 --- a/src/ResolvedDomainTest.php +++ b/src/ResolvedDomainTest.php @@ -322,6 +322,7 @@ public function testItCanChangeItsSuffix( public static function withPublicSuffixWorksProvider(): iterable { $baseDomain = ResolvedDomain::fromICANN('example.com', 1); + $baseRootDomain = ResolvedDomain::fromICANN('example.com.', 1); return [ 'simple update (1)' => [ @@ -332,6 +333,14 @@ public static function withPublicSuffixWorksProvider(): iterable 'isICANN' => false, 'isPrivate' => false, ], + 'simple update with root-label (1)' => [ + 'domain' => $baseRootDomain, + 'publicSuffix' => 'be', + 'expected' => 'be', + 'isKnown' => false, + 'isICANN' => false, + 'isPrivate' => false, + ], 'simple update (2)' => [ 'domain' => $baseDomain, 'publicSuffix' => Suffix::fromPrivate('github.io'), @@ -340,6 +349,14 @@ public static function withPublicSuffixWorksProvider(): iterable 'isICANN' => false, 'isPrivate' => true, ], + 'simple update with root-label (2)' => [ + 'domain' => $baseRootDomain, + 'publicSuffix' => Suffix::fromPrivate('github.io'), + 'expected' => 'github.io', + 'isKnown' => true, + 'isICANN' => false, + 'isPrivate' => true, + ], 'same public suffix but PSL info is changed' => [ 'domain' => $baseDomain, 'publicSuffix' => Suffix::fromPrivate('com'), @@ -348,6 +365,14 @@ public static function withPublicSuffixWorksProvider(): iterable 'isICANN' => false, 'isPrivate' => true, ], + 'same public suffix but PSL info is changed with root domain' => [ + 'domain' => $baseRootDomain, + 'publicSuffix' => Suffix::fromPrivate('com'), + 'expected' => 'com', + 'isKnown' => true, + 'isICANN' => false, + 'isPrivate' => true, + ], 'same public suffix but PSL info does not changed' => [ 'domain' => $baseDomain, 'publicSuffix' => Suffix::fromICANN('com'), @@ -372,6 +397,14 @@ public static function withPublicSuffixWorksProvider(): iterable 'isICANN' => true, 'isPrivate' => false, ], + 'simple update IDN (2) with root domains' => [ + 'domain' => ResolvedDomain::fromICANN(Domain::fromIDNA2003('www.bébé.be.'), 1), + 'publicSuffix' => Suffix::fromICANN(Domain::fromIDNA2003('xn--p1ai')), + 'expected' => 'рф', + 'isKnown' => true, + 'isICANN' => true, + 'isPrivate' => false, + ], 'adding the public suffix to a single label domain' => [ 'domain' => ResolvedDomain::fromUnknown('localhost'), 'publicSuffix' => 'www', From d8069179ef40576f45c912de0a8e3e1e2bf0c5fc Mon Sep 17 00:00:00 2001 From: ignace nyamagana butera Date: Wed, 23 Apr 2025 23:01:41 +0200 Subject: [PATCH 14/23] ResolvedDomain should handle domain with root label --- src/ResolvedDomain.php | 22 +++++++++++++++++----- src/ResolvedDomainTest.php | 35 +++++++++++++++++++++++++++++++++++ src/Suffix.php | 2 +- 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/src/ResolvedDomain.php b/src/ResolvedDomain.php index 70d4644..0d19bbc 100644 --- a/src/ResolvedDomain.php +++ b/src/ResolvedDomain.php @@ -199,10 +199,10 @@ public function withSuffix(DomainNameProvider|Host|Stringable|string|int|null $s $suffix = Suffix::fromUnknown($suffix); } - $newDomain = $this->domain->withoutRootLabel()->slice(count($this->suffix))->append($suffix); + $domain = $this->domain->withoutRootLabel()->slice(count($this->suffix))->append($suffix); return new self( - $this->domain->isAbsolute() ? $newDomain->withRootLabel() : $newDomain, + $this->domain->isAbsolute() ? $domain->withRootLabel() : $domain, $suffix->normalize($this->domain) ); } @@ -217,15 +217,20 @@ public function withSubDomain(DomainNameProvider|Host|Stringable|string|int|null } $subDomain = RegisteredName::fromIDNA2008($subDomain); - if ('' === $subDomain->withoutRootLabel()->value()) { - throw SyntaxError::dueToMalformedValue($subDomain->toString()); + if ($subDomain->isAbsolute()) { + $subDomain = $subDomain->withoutRootLabel(); + if (null === $subDomain->value()) { + throw SyntaxError::dueToMalformedValue($subDomain->withRootLabel()->toString()); + } } if ($this->subDomain->value() === $subDomain->value()) { return $this; } - return new self($this->registrableDomain->prepend($subDomain), $this->suffix); + $domain = $this->registrableDomain->prepend($subDomain); + + return new self($this->domain->isAbsolute() ? $domain->withRootLabel() : $domain, $this->suffix); } /** @@ -238,6 +243,13 @@ public function withSecondLevelDomain(DomainNameProvider|Host|Stringable|string| } $label = RegisteredName::fromIDNA2008($label); + if ($label->isAbsolute()) { + if (2 !== count($label)) { + throw UnableToResolveDomain::dueToInvalidSecondLevelDomain($label); + } + $label = $label->withoutRootLabel(); + } + if (1 !== count($label)) { throw UnableToResolveDomain::dueToInvalidSecondLevelDomain($label); } diff --git a/src/ResolvedDomainTest.php b/src/ResolvedDomainTest.php index 050a2a9..532531d 100644 --- a/src/ResolvedDomainTest.php +++ b/src/ResolvedDomainTest.php @@ -533,6 +533,13 @@ public static function withSldWorksProvider(): iterable 'expectedSld' => 'www', 'expectedHost' => 'www.com', ], + [ + 'host' => 'example.com', + 'publicSuffix' => 'com', + 'sld' => 'www.', + 'expectedSld' => 'www', + 'expectedHost' => 'www.com', + ], [ 'host' => 'www.example.com', 'publicSuffix' => 'com', @@ -547,6 +554,13 @@ public static function withSldWorksProvider(): iterable 'expectedSld' => 'hamburger', 'expectedHost' => 'www.hamburger.co.uk', ], + [ + 'host' => 'www.bbc.co.uk', + 'publicSuffix' => 'co.uk', + 'sld' => 'hamburger.', + 'expectedSld' => 'hamburger', + 'expectedHost' => 'www.hamburger.co.uk', + ], ]; } @@ -557,6 +571,20 @@ public function testItCanNotAppendAnEmptySLD(): void ResolvedDomain::fromICANN('private.ulb.ac.be', 2)->withSecondLevelDomain(null); } + public function testItCanNotAppendAnEmptyRootLabelSLD(): void + { + $this->expectException(SyntaxError::class); + + ResolvedDomain::fromICANN('private.ulb.ac.be', 2)->withSecondLevelDomain('.'); + } + + public function testItCanNotAppendAnInvalidRootLabelSLD(): void + { + $this->expectException(UnableToResolveDomain::class); + + ResolvedDomain::fromICANN('private.ulb.ac.be', 2)->withSecondLevelDomain('toto.foo.'); + } + public function testItCanNotAppendASLDToAResolvedDomainWithoutSuffix(): void { $this->expectException(UnableToResolveDomain::class); @@ -571,6 +599,13 @@ public function testItCanNotAppendAnInvalidSLDToAResolvedDomain(): void ResolvedDomain::fromIANA('private.ulb.ac.be')->withSecondLevelDomain('foo.bar'); } + public function testItCanNotAppendAnInvalidSubDomainToAResolvedDomain(): void + { + $this->expectException(SyntaxError::class); + + ResolvedDomain::fromIANA('private.ulb.ac.be')->withSubDomain(''); + } + public function testItReturnsTheInstanceWhenTheSLDIsEqual(): void { $domain = ResolvedDomain::fromICANN('private.ulb.ac.be', 2); diff --git a/src/Suffix.php b/src/Suffix.php index 3f0a615..6a4989d 100644 --- a/src/Suffix.php +++ b/src/Suffix.php @@ -90,7 +90,7 @@ private static function setDomainName(int|DomainNameProvider|Host|string|Stringa $domain = RegisteredName::fromIDNA2008($domain); } - if ('' === $domain->label(0)) { + if ($domain->isAbsolute()) { throw SyntaxError::dueToInvalidSuffix($domain); } From f06c740e9622336e6ddd21e3cffed23542b50d01 Mon Sep 17 00:00:00 2001 From: ignace nyamagana butera Date: Thu, 24 Apr 2025 08:09:00 +0200 Subject: [PATCH 15/23] ResolvedDomain should handle domain with root label --- README.md | 2 +- src/DomainName.php | 4 ---- src/Host.php | 4 ++++ src/ResolvedDomain.php | 15 +++++++++++++++ src/ResolvedDomainTest.php | 15 +++++++++++++++ 5 files changed, 35 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3dbc5db..9a5fb53 100644 --- a/README.md +++ b/README.md @@ -228,7 +228,7 @@ supported type to avoid unexpected results. By default, if the input is not a `Pdp\Suffix` instance, the resulting public suffix will be labelled as being unknown. For more information go to the [Public Suffix section](#public-suffix) -> [!TIP] +> [!NOTE] > Since version `6.4` Domain resolution is also adapted so that absolute domain can effectively be resolved. Prior to version `6.4` an exception was thrown. diff --git a/src/DomainName.php b/src/DomainName.php index b5c736d..fd6e3b9 100644 --- a/src/DomainName.php +++ b/src/DomainName.php @@ -14,10 +14,6 @@ * @see https://tools.ietf.org/html/rfc5890 * * @extends IteratorAggregate - * - * @method bool isAbsolute() tells whether the domain is absolute or not. - * @method self withRootLabel() returns an instance with its Root label. (see https://tools.ietf.org/html/rfc3986#section-3.2.2) - * @method self withoutRootLabel() returns an instance without its Root label. (see https://tools.ietf.org/html/rfc3986#section-3.2.2) */ interface DomainName extends Host, IteratorAggregate { diff --git a/src/Host.php b/src/Host.php index 87945e9..589e3be 100644 --- a/src/Host.php +++ b/src/Host.php @@ -11,6 +11,10 @@ * @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 + * + * @method bool isAbsolute() tells whether the domain is absolute or not. + * @method static withRootLabel() returns an instance with its Root label. (see https://tools.ietf.org/html/rfc3986#section-3.2.2) + * @method static withoutRootLabel() returns an instance without its Root label. (see https://tools.ietf.org/html/rfc3986#section-3.2.2) */ interface Host extends Countable, JsonSerializable { diff --git a/src/ResolvedDomain.php b/src/ResolvedDomain.php index 0d19bbc..de64b6e 100644 --- a/src/ResolvedDomain.php +++ b/src/ResolvedDomain.php @@ -190,6 +190,21 @@ public function toUnicode(): self return new self($this->domain->toUnicode(), $this->suffix->toUnicode()); } + public function isAbsolute(): bool + { + return $this->domain->isAbsolute(); + } + + public function withoutRootLabel(): self + { + return new self($this->domain->withoutRootLabel(), $this->suffix); + } + + public function withRootLabel(): self + { + return new self($this->domain->withRootLabel(), $this->suffix); + } + /** * @throws CannotProcessHost */ diff --git a/src/ResolvedDomainTest.php b/src/ResolvedDomainTest.php index 532531d..5f22839 100644 --- a/src/ResolvedDomainTest.php +++ b/src/ResolvedDomainTest.php @@ -628,4 +628,19 @@ public function testSuffixCanHandleIpLikeValue(): void ResolvedDomain::fromICANN('cloudflare-dns.com.1.1.1.1', 4)->toString() ); } + + public function testWithAndWithoutRootLabelResult(): void + { + $withoutRootLabelResult = ResolvedDomain::fromICANN('cloudflare-dns.com', 1); + $withRootLabelResult = $withoutRootLabelResult->withRootLabel(); + + self::assertTrue($withRootLabelResult->isAbsolute()); + self::assertFalse($withoutRootLabelResult->isAbsolute()); + self::assertEquals($withoutRootLabelResult, $withRootLabelResult->withoutRootLabel()); + self::assertSame($withoutRootLabelResult->suffix()->value(), $withRootLabelResult->suffix()->value()); + self::assertSame($withoutRootLabelResult->subDomain()->value(), $withRootLabelResult->subDomain()->value()); + self::assertSame($withoutRootLabelResult->registrableDomain()->value(), $withRootLabelResult->registrableDomain()->value()); + self::assertSame($withoutRootLabelResult->secondLevelDomain()->value(), $withRootLabelResult->secondLevelDomain()->value()); + self::assertNotSame($withoutRootLabelResult->domain()->value(), $withRootLabelResult->domain()->value()); + } } From 77878b8f4a4357f2a5455d551e4d6b43ec6654b4 Mon Sep 17 00:00:00 2001 From: ignace nyamagana butera Date: Thu, 24 Apr 2025 08:19:36 +0200 Subject: [PATCH 16/23] Adding conditionable on Host interface --- src/Domain.php | 23 +++++++++++++++++++++++ src/Host.php | 1 + src/RegisteredName.php | 22 ++++++++++++++++++++++ src/ResolvedDomain.php | 26 ++++++++++++++++++++++++-- 4 files changed, 70 insertions(+), 2 deletions(-) diff --git a/src/Domain.php b/src/Domain.php index 6764ff2..39fb020 100644 --- a/src/Domain.php +++ b/src/Domain.php @@ -7,6 +7,8 @@ use Iterator; use Stringable; +use function is_bool; + use const FILTER_FLAG_IPV4; use const FILTER_VALIDATE_IP; @@ -183,4 +185,25 @@ public function isAbsolute(): bool { return '' === $this->label(0); } + + /** + * Apply the callback if the given "condition" is (or resolves to) true. + * + * @param (callable($this): bool)|bool $condition + * @param callable($this): (self|null) $onSuccess + * @param ?callable($this): (self|null) $onFail + * + */ + public function when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null): self + { + if (!is_bool($condition)) { + $condition = $condition($this); + } + + return match (true) { + $condition => $onSuccess($this), + null !== $onFail => $onFail($this), + default => $this, + } ?? $this; + } } diff --git a/src/Host.php b/src/Host.php index 589e3be..6b4de6b 100644 --- a/src/Host.php +++ b/src/Host.php @@ -15,6 +15,7 @@ * @method bool isAbsolute() tells whether the domain is absolute or not. * @method static withRootLabel() returns an instance with its Root label. (see https://tools.ietf.org/html/rfc3986#section-3.2.2) * @method static withoutRootLabel() returns an instance without its Root label. (see https://tools.ietf.org/html/rfc3986#section-3.2.2) + * @method static when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null) apply the callback if the given "condition" is (or resolves to) true. */ interface Host extends Countable, JsonSerializable { diff --git a/src/RegisteredName.php b/src/RegisteredName.php index ac6d68a..4a08b41 100644 --- a/src/RegisteredName.php +++ b/src/RegisteredName.php @@ -16,6 +16,7 @@ use function filter_var; use function implode; use function in_array; +use function is_bool; use function ksort; use function preg_match; use function rawurldecode; @@ -384,4 +385,25 @@ public function isAbsolute(): bool { return '' === $this->label(0); } + + /** + * Apply the callback if the given "condition" is (or resolves to) true. + * + * @param (callable($this): bool)|bool $condition + * @param callable($this): (self|null) $onSuccess + * @param ?callable($this): (self|null) $onFail + * + */ + public function when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null): self + { + if (!is_bool($condition)) { + $condition = $condition($this); + } + + return match (true) { + $condition => $onSuccess($this), + null !== $onFail => $onFail($this), + default => $this, + } ?? $this; + } } diff --git a/src/ResolvedDomain.php b/src/ResolvedDomain.php index de64b6e..d687146 100644 --- a/src/ResolvedDomain.php +++ b/src/ResolvedDomain.php @@ -7,6 +7,7 @@ use Stringable; use function count; +use function is_bool; final class ResolvedDomain implements ResolvedDomainName { @@ -217,7 +218,7 @@ public function withSuffix(DomainNameProvider|Host|Stringable|string|int|null $s $domain = $this->domain->withoutRootLabel()->slice(count($this->suffix))->append($suffix); return new self( - $this->domain->isAbsolute() ? $domain->withRootLabel() : $domain, + $domain->when($this->domain->isAbsolute(), fn (DomainName $domainName) => $domain->withRootLabel()), $suffix->normalize($this->domain) ); } @@ -245,7 +246,7 @@ public function withSubDomain(DomainNameProvider|Host|Stringable|string|int|null $domain = $this->registrableDomain->prepend($subDomain); - return new self($this->domain->isAbsolute() ? $domain->withRootLabel() : $domain, $this->suffix); + return new self($domain->when($this->domain->isAbsolute(), fn (DomainName $domainName) => $domain->withRootLabel()), $this->suffix); } /** @@ -276,4 +277,25 @@ public function withSecondLevelDomain(DomainNameProvider|Host|Stringable|string| return new self($newRegistrableDomain->prepend($this->subDomain), $this->suffix); } + + /** + * Apply the callback if the given "condition" is (or resolves to) true. + * + * @param (callable($this): bool)|bool $condition + * @param callable($this): (self|null) $onSuccess + * @param ?callable($this): (self|null) $onFail + * + */ + public function when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null): self + { + if (!is_bool($condition)) { + $condition = $condition($this); + } + + return match (true) { + $condition => $onSuccess($this), + null !== $onFail => $onFail($this), + default => $this, + } ?? $this; + } } From fdec722844aaf8040af2fce88f2d77736236c1fd Mon Sep 17 00:00:00 2001 From: Clemens Krack <73299324+wikando-ck@users.noreply.github.com> Date: Thu, 24 Apr 2025 15:11:07 +0200 Subject: [PATCH 17/23] Fix #363 - domain and subdomain wildcard (#364) * test: private domain with subdomain wildcard When a private domain is listed, with an additional wildcard on a subdomain, the private domain itself is not resolved. Confirms #363 * fix: private domain with subdomain wildcard When a private domain is listed, with an additional wildcard on a subdomain, the private domain itself is not resolved. Fixes #363 --- src/Rules.php | 15 ++++++++++++++- src/RulesTest.php | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/Rules.php b/src/Rules.php index cdd4d4b..331df48 100644 --- a/src/Rules.php +++ b/src/Rules.php @@ -8,8 +8,10 @@ use SplTempFileObject; use Stringable; +use function array_key_exists; use function array_pop; use function explode; +use function in_array; use function preg_match; use function substr; @@ -19,6 +21,8 @@ final class Rules implements PublicSuffixList private const PRIVATE_DOMAINS = 'PRIVATE_DOMAINS'; private const UNKNOWN_DOMAINS = 'UNKNOWN_DOMAINS'; + private const DOMAIN_RULE_MARKER = '?'; + private const REGEX_PSL_SECTION = ',^// ===(?BEGIN|END) (?ICANN|PRIVATE) DOMAINS===,'; private const PSL_SECTION = [ 'ICANN' => [ @@ -134,7 +138,13 @@ private static function addRule(array $list, array $ruleParts): array $isDomain = false; } - $list[$rule] = $list[$rule] ?? ($isDomain ? [] : ['!' => '']); + if (isset($list[$rule]) && [] === $list[$rule]) { + $list[$rule] = [self::DOMAIN_RULE_MARKER => '']; + } + if (!isset($list[$rule])) { + $list[$rule] = $isDomain ? [] : ['!' => '']; + } + if ($isDomain && [] !== $ruleParts) { /** @var array $tmpList */ $tmpList = $list[$rule]; @@ -264,6 +274,9 @@ private function getPublicSuffixLengthFromSection(DomainName $domain, string $se //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 && array_key_exists(self::DOMAIN_RULE_MARKER, $rules)) { + return $labelCount; + } if (self::PRIVATE_DOMAINS === $section && self::hasRemainingRules($rules)) { $labelCount = 0; } diff --git a/src/RulesTest.php b/src/RulesTest.php index be4f25e..2b0bda9 100644 --- a/src/RulesTest.php +++ b/src/RulesTest.php @@ -565,4 +565,39 @@ public function testWithMultiLevelPrivateDomain(): void self::assertTrue($domain->suffix()->isPrivate()); self::assertSame('lt.eu.org', $domain->suffix()->value()); } + + #[DataProvider('privateDomainWithWildcardProvider')] + public function testWithPrivateDomainThatHasWildcardSubdomain(string $inputDomain, string $expectedSuffix): void + { + $domain = self::$rules->getPrivateDomain($inputDomain); + + self::assertSame($expectedSuffix, $domain->suffix()->value()); + self::assertFalse($domain->suffix()->isICANN()); + self::assertTrue($domain->suffix()->isPrivate()); + } + + /** + * @return iterable + */ + public static function privateDomainWithWildcardProvider(): iterable + { + return [ + 'appspot subdomain' => [ + 'inputDomain' => 'test-domain.de.r.appspot.com', + 'expectedSuffix' => 'de.r.appspot.com', + ], + 'appspot root domain' => [ + 'inputDomain' => 'test-domain.appspot.com', + 'expectedSuffix' => 'appspot.com', + ], + 'qcx subdomain' => [ + 'inputDomain' => 'test-domain.de.sys.qcx.io', + 'expectedSuffix' => 'de.sys.qcx.io', + ], + 'qcx root domain' => [ + 'inputDomain' => 'test-domain.qcx.io', + 'expectedSuffix' => 'qcx.io', + ], + ]; + } } From 6d0878acf978df357f0898ad107a50da6c6435d5 Mon Sep 17 00:00:00 2001 From: ignace nyamagana butera Date: Thu, 24 Apr 2025 21:02:57 +0200 Subject: [PATCH 18/23] Adding information about 6.4 cache refresh requirement --- CHANGELOG.md | 3 +++ README.md | 4 ++++ src/Rules.php | 15 +++++++++++---- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12cf2a8..b488d79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,14 @@ All Notable changes to `PHP Domain Parser` starting from the **5.x** series will ### Added - `DomainName::withRootLabel`, `DomainName::withoutRootLabel`, `DomainName::isAbsolute` methods to handle absolute domain names. +- `DomainName::when` to allow conditionable when building the domain. ### Fixed - Absolute domain name can now also be resolved by the package see issue [#361](https://github.com/jeremykendall/php-domain-parser/issues/361) prior to this release an exception was thrown. - Since we no longer support PHP7 type hint and return type are improved. +- Resolving private suffixes that have a wildcarded subdomain suffix [#363](https://github.com/jeremykendall/php-domain-parser/issues/363) by [@wikando-ck](https://github.com/wikando-ck) + (Once downloaded and installed you MUST refresh your cache to enable the fix to work.) ### Deprecated diff --git a/README.md b/README.md index 9a5fb53..96d3519 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,10 @@ a polyfill for the `intl` IDN functions like the `symfony/polyfill-intl-idn` otherwise an exception will be thrown when attempting to validate or interact with such a host. +> [!WARNING] +> When upgrading to version `6.4` you MUST refresh your local cache for the fix on +> private domain resolution to take effect. + ## Usage > [!WARNING] diff --git a/src/Rules.php b/src/Rules.php index 331df48..f91cd08 100644 --- a/src/Rules.php +++ b/src/Rules.php @@ -273,13 +273,20 @@ private function getPublicSuffixLengthFromSection(DomainName $domain, string $se //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 && array_key_exists(self::DOMAIN_RULE_MARKER, $rules)) { + if (self::PRIVATE_DOMAINS !== $section) { + break; + } + + // Suffix MATCHES default domain + if (array_key_exists(self::DOMAIN_RULE_MARKER, $rules)) { return $labelCount; } - if (self::PRIVATE_DOMAINS === $section && self::hasRemainingRules($rules)) { - $labelCount = 0; + + // Suffix MUST be fully matched else no suffix is found for private domain + if (self::hasRemainingRules($rules)) { + return 0; } + break; } From 548e1c854ee61da692c158fd8920caf1bd04e136 Mon Sep 17 00:00:00 2001 From: ignace nyamagana butera Date: Thu, 24 Apr 2025 21:08:17 +0200 Subject: [PATCH 19/23] Update changelog and prepare release --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b488d79..64b25ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All Notable changes to `PHP Domain Parser` starting from the **5.x** series will be documented in this file -## [6.4.0] - 2025-04-22 +## [6.4.0] - 2025-04-24 ### Added @@ -412,6 +412,7 @@ All Notable changes to `PHP Domain Parser` starting from the **5.x** series will - `Pdp\HttpAdapter\HttpAdapterInterface` interface replaced by the `Pdp\HttpClient` interface - `Pdp\HttpAdapter\CurlHttpAdapter` class replaced by the `Pdp\CurlHttpClient` class +[6.4.0]: https://github.com/jeremykendall/php-domain-parser/compare/6.3.0...6.4.0 [6.3.0]: https://github.com/jeremykendall/php-domain-parser/compare/6.2.0...6.3.0 [6.2.0]: https://github.com/jeremykendall/php-domain-parser/compare/6.1.2...6.2.0 [6.1.2]: https://github.com/jeremykendall/php-domain-parser/compare/6.1.1...6.1.2 From 55246c1fe7f1e3493862aa5511fbd20fa42f9971 Mon Sep 17 00:00:00 2001 From: ignace nyamagana butera Date: Thu, 24 Apr 2025 21:26:10 +0200 Subject: [PATCH 20/23] Improve codebase --- src/Rules.php | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Rules.php b/src/Rules.php index f91cd08..a954844 100644 --- a/src/Rules.php +++ b/src/Rules.php @@ -20,9 +20,7 @@ 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 DOMAIN_RULE_MARKER = '?'; - private const REGEX_PSL_SECTION = ',^// ===(?BEGIN|END) (?ICANN|PRIVATE) DOMAINS===,'; private const PSL_SECTION = [ 'ICANN' => [ @@ -106,8 +104,7 @@ private static function getSection(string $section, string $line): string * 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 + * A copy of the Apache License, Version 2.0, is provided with this distribution * * @param array $list Initially an empty array, this eventually becomes the array representation of a * Public Suffix List section @@ -141,14 +138,13 @@ private static function addRule(array $list, array $ruleParts): array if (isset($list[$rule]) && [] === $list[$rule]) { $list[$rule] = [self::DOMAIN_RULE_MARKER => '']; } + if (!isset($list[$rule])) { $list[$rule] = $isDomain ? [] : ['!' => '']; } if ($isDomain && [] !== $ruleParts) { - /** @var array $tmpList */ - $tmpList = $list[$rule]; - $list[$rule] = self::addRule($tmpList, $ruleParts); + $list[$rule] = self::addRule($list[$rule], $ruleParts); } return $list; From 9b344cf39488fdbe6cf713caad5d4dd4ff4e695f Mon Sep 17 00:00:00 2001 From: ignace nyamagana butera Date: Fri, 25 Apr 2025 12:22:30 +0200 Subject: [PATCH 21/23] Improve Rules::getPublicSuffixLengthFromSection codebase with early return --- CHANGELOG.md | 2 ++ src/Rules.php | 9 +++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64b25ee..1068eb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ All Notable changes to `PHP Domain Parser` starting from the **5.x** series will - `DomainName::withRootLabel`, `DomainName::withoutRootLabel`, `DomainName::isAbsolute` methods to handle absolute domain names. - `DomainName::when` to allow conditionable when building the domain. +- `Rules` can now be serialize and unserialize. +- `TopLevelDomains` can now be serialize and unserialize. ### Fixed diff --git a/src/Rules.php b/src/Rules.php index a954844..edee031 100644 --- a/src/Rules.php +++ b/src/Rules.php @@ -258,19 +258,20 @@ private function getPublicSuffixLengthFromSection(DomainName $domain, string $se foreach ($domain->toAscii() as $label) { //match exception rule if (isset($rules[$label]['!'])) { - break; + return $labelCount; } //match wildcard rule if (array_key_exists('*', $rules)) { ++$labelCount; - break; + + return $labelCount; } //no match found if (!array_key_exists($label, $rules)) { if (self::PRIVATE_DOMAINS !== $section) { - break; + return $labelCount; } // Suffix MATCHES default domain @@ -283,7 +284,7 @@ private function getPublicSuffixLengthFromSection(DomainName $domain, string $se return 0; } - break; + return $labelCount; } ++$labelCount; From d93d26400aefe4d56a4169abe97212a80684f894 Mon Sep 17 00:00:00 2001 From: ignace nyamagana butera Date: Sat, 26 Apr 2025 13:15:15 +0200 Subject: [PATCH 22/23] prepare 6.4.0 release --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1068eb5..044e9c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All Notable changes to `PHP Domain Parser` starting from the **5.x** series will be documented in this file -## [6.4.0] - 2025-04-24 +## [6.4.0] - 2025-04-26 ### Added From e07be63a9e647efa0e4e79a5d5a944e8ae841bf4 Mon Sep 17 00:00:00 2001 From: ignace nyamagana butera Date: Sat, 26 Apr 2025 13:15:51 +0200 Subject: [PATCH 23/23] prepare 6.4.0 release --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 044e9c4..50dc06f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,6 @@ All Notable changes to `PHP Domain Parser` starting from the **5.x** series will - `DomainName::withRootLabel`, `DomainName::withoutRootLabel`, `DomainName::isAbsolute` methods to handle absolute domain names. - `DomainName::when` to allow conditionable when building the domain. -- `Rules` can now be serialize and unserialize. -- `TopLevelDomains` can now be serialize and unserialize. ### Fixed