Skip to content

Commit e4f8435

Browse files
authored
#361 Adding the ability to use absolute domain (#362)
Allow absolute domain name to be resolved
1 parent 2d0caea commit e4f8435

32 files changed

+197
-143
lines changed

.github/workflows/build.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ on:
77
jobs:
88
linux_tests:
99
name: PHP on ${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.composer-flags }}
10-
runs-on: ubuntu-20.04
10+
runs-on: ubuntu-22.04
1111
strategy:
1212
matrix:
1313
php: ['8.1', '8.2', '8.3', '8.4']
@@ -32,7 +32,7 @@ jobs:
3232
id: composer-cache
3333
run: |
3434
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
35-
- uses: actions/cache@v3
35+
- uses: actions/cache@v4
3636
with:
3737
path: ${{ steps.composer-cache.outputs.dir }}
3838
key: ${{ runner.os }}-composer-${{ matrix.stability }}-${{ matrix.flags }}-${{ hashFiles('**/composer.lock') }}

.php-cs-fixer.php

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,36 @@
11
<?php
22

3+
use PhpCsFixer\Runner\Parallel\ParallelConfigFactory;
4+
35
$finder = PhpCsFixer\Finder::create()
46
->in(__DIR__.'/src')
57
;
68

79
$config = new PhpCsFixer\Config();
810

911
return $config
12+
->setParallelConfig(ParallelConfigFactory::detect())
1013
->setRules([
11-
'@PSR2' => true,
14+
'@PSR12' => true,
1215
'array_syntax' => ['syntax' => 'short'],
1316
'concat_space' => ['spacing' => 'none'],
1417
'global_namespace_import' => [
1518
'import_classes' => true,
1619
'import_constants' => true,
1720
'import_functions' => true,
1821
],
19-
'list_syntax' => ['syntax' => 'short'],
2022
'new_with_parentheses' => true,
2123
'no_blank_lines_after_phpdoc' => true,
2224
'no_empty_phpdoc' => true,
2325
'no_empty_comment' => true,
2426
'no_leading_import_slash' => true,
25-
'no_superfluous_phpdoc_tags' => [
26-
'allow_mixed' => true,
27-
'remove_inheritdoc' => true,
28-
'allow_unused_params' => false,
29-
],
27+
'no_superfluous_phpdoc_tags' => true,
3028
'no_trailing_comma_in_singleline' => true,
3129
'no_unused_imports' => true,
3230
'nullable_type_declaration_for_default_null_value' => true,
3331
'ordered_imports' => ['imports_order' => ['class', 'function', 'const'], 'sort_algorithm' => 'alpha'],
3432
'phpdoc_add_missing_param_annotation' => ['only_untyped' => true],
35-
'phpdoc_align' => true,
33+
'phpdoc_align' => ['align' => 'left'],
3634
'phpdoc_no_empty_return' => true,
3735
'phpdoc_order' => true,
3836
'phpdoc_scalar' => true,
@@ -47,5 +45,6 @@
4745
'trailing_comma_in_multiline' => true,
4846
'trim_array_spaces' => true,
4947
'whitespace_after_comma_in_array' => true,
48+
'yoda_style' => true,
5049
])
5150
->setFinder($finder);

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,25 @@
22

33
All Notable changes to `PHP Domain Parser` starting from the **5.x** series will be documented in this file
44

5+
## [6.4.0] - 2025-04-22
6+
7+
### Added
8+
9+
- `DomainName::withRootLabel`, `DomainName::withoutRootLabel`, `DomainName::isAbsolute` methods to handle absolute domain names.
10+
11+
### Fixed
12+
13+
- 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.
14+
- Since we no longer support PHP7 type hint and return type are improved.
15+
16+
### Deprecated
17+
18+
- None
19+
20+
### Removed
21+
22+
- None
23+
524
## [6.3.0] - 2023-02-25
625

726
### Added

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,14 @@ composer require jeremykendall/php-domain-parser:^6.0
3636

3737
You need:
3838

39-
- **PHP >= 7.4** but the latest stable version of PHP is recommended
40-
- the `intl` extension
39+
- **PHP >= 8.1** but the latest stable version of PHP is recommended
4140
- 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.
4241

42+
Handling of an IDN host requires the presence of the `intl` extension or
43+
a polyfill for the `intl` IDN functions like the `symfony/polyfill-intl-idn`
44+
otherwise an exception will be thrown when attempting to validate or interact
45+
with such a host.
46+
4347
## Usage
4448

4549
> [!WARNING]

composer.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,7 @@
4141
],
4242
"require": {
4343
"php": "^8.1",
44-
"ext-filter": "*",
45-
"ext-intl": "*"
44+
"ext-filter": "*"
4645
},
4746
"require-dev": {
4847
"friendsofphp/php-cs-fixer": "^3.65.0",
@@ -54,13 +53,16 @@
5453
"phpunit/phpunit": "^10.5.15 || ^11.5.1",
5554
"psr/http-factory": "^1.1.0",
5655
"psr/simple-cache": "^1.0.1 || ^2.0.0",
57-
"symfony/cache": "^v5.0.0 || ^6.4.16"
56+
"symfony/cache": "^v5.0.0 || ^6.4.16",
57+
"symfony/var-dumper": "^v6.4.18 || ^7.2"
5858
},
5959
"suggest": {
6060
"psr/http-client-implementation": "To use the storage functionality which depends on PSR-18",
6161
"psr/http-factory-implementation": "To use the storage functionality which depends on PSR-17",
6262
"psr/simple-cache-implementation": "To use the storage functionality which depends on PSR-16",
63-
"league/uri": "To parse URL and validate host"
63+
"league/uri": "To parse and extract the host from an URL using a RFC3986/RFC3987 URI parser",
64+
"rowbot/url": "To parse and extract the host from an URL using a WHATWG URL parser",
65+
"symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present"
6466
},
6567
"autoload": {
6668
"psr-4": {

src/Domain.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Iterator;
88
use Stringable;
9+
910
use const FILTER_FLAG_IPV4;
1011
use const FILTER_VALIDATE_IP;
1112

@@ -159,4 +160,27 @@ public function slice(int $offset, ?int $length = null): self
159160
{
160161
return $this->newInstance($this->registeredName->slice($offset, $length));
161162
}
163+
164+
public function withRootLabel(): self
165+
{
166+
if ('' === $this->label(0)) {
167+
return $this;
168+
}
169+
170+
return $this->append('');
171+
}
172+
173+
public function withoutRootLabel(): self
174+
{
175+
if ('' === $this->label(0)) {
176+
return $this->slice(1);
177+
}
178+
179+
return $this;
180+
}
181+
182+
public function isAbsolute(): bool
183+
{
184+
return '' === $this->label(0);
185+
}
162186
}

src/DomainName.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
* @see https://tools.ietf.org/html/rfc5890
1515
*
1616
* @extends IteratorAggregate<string>
17+
*
18+
* @method bool isAbsolute() tells whether the domain is absolute or not.
19+
* @method self withRootLabel() returns an instance with its Root label. (see https://tools.ietf.org/html/rfc3986#section-3.2.2)
20+
* @method self withoutRootLabel() returns an instance without its Root label. (see https://tools.ietf.org/html/rfc3986#section-3.2.2)
1721
*/
1822
interface DomainName extends Host, IteratorAggregate
1923
{
@@ -120,4 +124,6 @@ public function clear(): self;
120124
* If $length is null it returns all elements from $offset to the end of the Domain.
121125
*/
122126
public function slice(int $offset, ?int $length = null): self;
127+
128+
123129
}

src/DomainTest.php

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66

77
use PHPUnit\Framework\Attributes\DataProvider;
88
use PHPUnit\Framework\TestCase;
9-
use stdClass;
10-
use TypeError;
119

1210
final class DomainTest extends TestCase
1311
{
@@ -290,12 +288,6 @@ public static function withLabelWorksProvider(): iterable
290288
];
291289
}
292290

293-
public function testWithLabelFailsWithTypeError(): void
294-
{
295-
$this->expectException(TypeError::class);
296-
Domain::fromIDNA2008('example.com')->withLabel(1, new stdClass()); /* @phpstan-ignore-line */
297-
}
298-
299291
public function testWithLabelFailsWithInvalidKey(): void
300292
{
301293
$this->expectException(SyntaxError::class);

src/Idna.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
namespace Pdp;
66

77
use UnexpectedValueException;
8+
89
use function defined;
910
use function function_exists;
1011
use function idn_to_ascii;
1112
use function idn_to_utf8;
1213
use function preg_match;
1314
use function rawurldecode;
1415
use function strtolower;
16+
1517
use const INTL_IDNA_VARIANT_UTS46;
1618

1719
/**

src/IdnaInfo.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Pdp;
66

77
use function array_filter;
8+
89
use const ARRAY_FILTER_USE_KEY;
910

1011
/**

src/IdnaInfoTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Pdp;
66

77
use PHPUnit\Framework\TestCase;
8+
89
use function var_export;
910

1011
final class IdnaInfoTest extends TestCase

src/PublicSuffixList.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,23 @@ interface PublicSuffixList extends DomainNameResolver
99
/**
1010
* Returns PSL info for a given domain against the PSL rules for cookie domain detection.
1111
*
12-
* @throws SyntaxError if the domain is invalid
12+
* @throws SyntaxError if the domain is invalid
1313
* @throws UnableToResolveDomain if the effective TLD can not be resolved
1414
*/
1515
public function getCookieDomain(Host $host): ResolvedDomainName;
1616

1717
/**
1818
* Returns PSL info for a given domain against the PSL rules for ICANN domain detection.
1919
*
20-
* @throws SyntaxError if the domain is invalid
20+
* @throws SyntaxError if the domain is invalid
2121
* @throws UnableToResolveDomain if the domain does not contain a ICANN Effective TLD
2222
*/
2323
public function getICANNDomain(Host $host): ResolvedDomainName;
2424

2525
/**
2626
* Returns PSL info for a given domain against the PSL rules for private domain detection.
2727
*
28-
* @throws SyntaxError if the domain is invalid
28+
* @throws SyntaxError if the domain is invalid
2929
* @throws UnableToResolveDomain if the domain does not contain a private Effective TLD
3030
*/
3131
public function getPrivateDomain(Host $host): ResolvedDomainName;

src/RegisteredName.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Iterator;
88
use Stringable;
9+
910
use function array_count_values;
1011
use function array_keys;
1112
use function array_reverse;
@@ -19,6 +20,7 @@
1920
use function preg_match;
2021
use function rawurldecode;
2122
use function strtolower;
23+
2224
use const FILTER_FLAG_IPV4;
2325
use const FILTER_VALIDATE_IP;
2426

@@ -359,4 +361,27 @@ public function slice(int $offset, ?int $length = null): self
359361

360362
return new self($this->type, [] === $labels ? null : implode('.', array_reverse($labels)));
361363
}
364+
365+
public function withRootLabel(): self
366+
{
367+
if ('' === $this->label(0)) {
368+
return $this;
369+
}
370+
371+
return $this->append('');
372+
}
373+
374+
public function withoutRootLabel(): self
375+
{
376+
if ('' === $this->label(0)) {
377+
return $this->slice(1);
378+
}
379+
380+
return $this;
381+
}
382+
383+
public function isAbsolute(): bool
384+
{
385+
return '' === $this->label(0);
386+
}
362387
}

src/ResolvedDomain.php

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Pdp;
66

77
use Stringable;
8+
89
use function count;
910

1011
final class ResolvedDomain implements ResolvedDomainName
@@ -34,7 +35,7 @@ public static function fromICANN(DomainNameProvider|Host|Stringable|string|int|n
3435
{
3536
$domain = self::setDomainName($domain);
3637

37-
return new self($domain, Suffix::fromICANN($domain->slice(0, $suffixLength)));
38+
return new self($domain, Suffix::fromICANN($domain->withoutRootLabel()->slice(0, $suffixLength)));
3839
}
3940

4041
/**
@@ -44,7 +45,7 @@ public static function fromPrivate(DomainNameProvider|Host|Stringable|string|int
4445
{
4546
$domain = self::setDomainName($domain);
4647

47-
return new self($domain, Suffix::fromPrivate($domain->slice(0, $suffixLength)));
48+
return new self($domain, Suffix::fromPrivate($domain->withoutRootLabel()->slice(0, $suffixLength)));
4849
}
4950

5051
/**
@@ -54,7 +55,7 @@ public static function fromIANA(DomainNameProvider|Host|Stringable|string|int|nu
5455
{
5556
$domain = self::setDomainName($domain);
5657

57-
return new self($domain, Suffix::fromIANA($domain->label(0)));
58+
return new self($domain, Suffix::fromIANA($domain->withoutRootLabel()->label(0)));
5859
}
5960

6061
/**
@@ -118,11 +119,15 @@ private function parse(): array
118119
}
119120

120121
$length = count($this->suffix);
122+
$offset = 0;
123+
if ($this->domain->isAbsolute()) {
124+
$offset = 1;
125+
}
121126

122127
return [
123-
'registrableDomain' => $this->domain->slice(0, $length + 1),
124-
'secondLevelDomain' => $this->domain->slice($length, 1),
125-
'subDomain' => RegisteredName::fromIDNA2008($this->domain->value())->slice($length + 1),
128+
'registrableDomain' => $this->domain->slice($offset, $length + 1),
129+
'secondLevelDomain' => $this->domain->slice($length + $offset, 1),
130+
'subDomain' => RegisteredName::fromIDNA2008($this->domain->value())->slice($length + 1 + $offset),
126131
];
127132
}
128133

0 commit comments

Comments
 (0)