Skip to content

Commit 23052bc

Browse files
committed
fix #234
1 parent 6e326c5 commit 23052bc

10 files changed

+482
-68
lines changed

src/Domain.php

Lines changed: 117 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Pdp\Exception\CouldNotResolveSubDomain;
2121
use Pdp\Exception\InvalidLabel;
2222
use Pdp\Exception\InvalidLabelKey;
23+
use Throwable;
2324
use TypeError;
2425
use function array_count_values;
2526
use function array_keys;
@@ -82,32 +83,59 @@ final class Domain implements DomainInterface, JsonSerializable
8283
* @var string|null
8384
*/
8485
private $subDomain;
85-
86+
87+
/**
88+
* @var int
89+
*/
90+
private $asciiIDNAOption = IDNA_DEFAULT;
91+
92+
/**
93+
* @var int
94+
*/
95+
private $unicodeIDNAOption = IDNA_DEFAULT;
96+
97+
/**
98+
* @var bool
99+
*/
100+
private $isTransitionalDifferent;
86101
/**
87102
* {@inheritdoc}
88103
*/
89104
public static function __set_state(array $properties): self
90105
{
91-
return new self($properties['domain'], $properties['publicSuffix']);
106+
return new self(
107+
$properties['domain'],
108+
$properties['publicSuffix'],
109+
$properties['asciiIDNAOption'],
110+
$properties['unicodeIDNAOption']
111+
);
92112
}
93-
113+
94114
/**
95115
* New instance.
96-
*
97116
* @param null|mixed $domain
98117
* @param null|PublicSuffix $publicSuffix
99-
*/
100-
public function __construct($domain = null, PublicSuffix $publicSuffix = null)
101-
{
102-
$this->labels = $this->setLabels($domain);
118+
* @param int $asciiIDNAOption
119+
* @param int $unicodeIDNAOption
120+
*/
121+
public function __construct(
122+
$domain = null,
123+
PublicSuffix $publicSuffix = null,
124+
int $asciiIDNAOption = IDNA_DEFAULT,
125+
int $unicodeIDNAOption = IDNA_DEFAULT
126+
) {
127+
$this->setIDNAOptions($asciiIDNAOption, $unicodeIDNAOption);
128+
$this->labels = $this->setLabels($domain, $asciiIDNAOption, $unicodeIDNAOption);
103129
if ([] !== $this->labels) {
104130
$this->domain = implode('.', array_reverse($this->labels));
105131
}
106-
$this->publicSuffix = $this->setPublicSuffix($publicSuffix ?? new PublicSuffix());
132+
$this->publicSuffix = $this->setPublicSuffix(
133+
$publicSuffix ?? new PublicSuffix(null, '', $asciiIDNAOption, $unicodeIDNAOption)
134+
);
107135
$this->registrableDomain = $this->setRegistrableDomain();
108136
$this->subDomain = $this->setSubDomain();
109137
}
110-
138+
111139
/**
112140
* Sets the public suffix domain part.
113141
*
@@ -224,7 +252,7 @@ public function jsonSerialize()
224252
*/
225253
public function __debugInfo()
226254
{
227-
return [
255+
return [
228256
'domain' => $this->domain,
229257
'registrableDomain' => $this->registrableDomain,
230258
'subDomain' => $this->subDomain,
@@ -346,7 +374,7 @@ public function getPublicSuffix()
346374
*/
347375
public function isResolvable(): bool
348376
{
349-
return 1 < count($this->labels) && '.' !== substr($this->domain, -1, 1);
377+
return 1 < count($this->labels ?? []) && '.' !== substr($this->domain, -1, 1);
350378
}
351379

352380
/**
@@ -388,12 +416,12 @@ public function toAscii()
388416
return $this;
389417
}
390418

391-
$domain = $this->idnToAscii($this->domain);
419+
$domain = $this->idnToAscii($this->domain, $this->asciiIDNAOption);
392420
if ($domain === $this->domain) {
393421
return $this;
394422
}
395423

396-
return new self($domain, $this->publicSuffix);
424+
return new self($domain, $this->publicSuffix, ...$this->getIDNAOptions());
397425
}
398426

399427
/**
@@ -405,7 +433,11 @@ public function toUnicode()
405433
return $this;
406434
}
407435

408-
return new self($this->idnToUnicode($this->domain), $this->publicSuffix);
436+
return new self(
437+
$this->idnToUnicode($this->domain, $this->unicodeIDNAOption),
438+
$this->publicSuffix,
439+
...$this->getIDNAOptions()
440+
);
409441
}
410442

411443
/**
@@ -426,15 +458,15 @@ public function toUnicode()
426458
public function resolve($publicSuffix): self
427459
{
428460
if (!$publicSuffix instanceof PublicSuffix) {
429-
$publicSuffix = new PublicSuffix($publicSuffix);
461+
$publicSuffix = new PublicSuffix($publicSuffix, '', ...$this->getIDNAOptions());
430462
}
431463

432464
$publicSuffix = $this->normalize($publicSuffix);
433465
if ($this->publicSuffix == $publicSuffix) {
434466
return $this;
435467
}
436468

437-
return new self($this->domain, $publicSuffix);
469+
return new self($this->domain, $publicSuffix, ...$this->getIDNAOptions());
438470
}
439471

440472
/**
@@ -453,7 +485,7 @@ public function resolve($publicSuffix): self
453485
public function withPublicSuffix($publicSuffix): self
454486
{
455487
if (!$publicSuffix instanceof PublicSuffix) {
456-
$publicSuffix = new PublicSuffix($publicSuffix);
488+
$publicSuffix = new PublicSuffix($publicSuffix, '', ...$this->getIDNAOptions());
457489
}
458490

459491
$publicSuffix = $this->normalize($publicSuffix);
@@ -463,10 +495,10 @@ public function withPublicSuffix($publicSuffix): self
463495

464496
$domain = implode('.', array_reverse(array_slice($this->labels, count($this->publicSuffix))));
465497
if (null === $publicSuffix->getContent()) {
466-
return new self($domain);
498+
return new self($domain, null, ...$this->getIDNAOptions());
467499
}
468500

469-
return new self($domain.'.'.$publicSuffix->getContent(), $publicSuffix);
501+
return new self($domain.'.'.$publicSuffix->getContent(), $publicSuffix, ...$this->getIDNAOptions());
470502
}
471503

472504

@@ -494,10 +526,14 @@ public function withSubDomain($subDomain): self
494526
}
495527

496528
if (null === $subDomain) {
497-
return new self($this->registrableDomain, $this->publicSuffix);
529+
return new self($this->registrableDomain, $this->publicSuffix, ...$this->getIDNAOptions());
498530
}
499531

500-
return new self($subDomain.'.'.$this->registrableDomain, $this->publicSuffix);
532+
return new self(
533+
$subDomain.'.'.$this->registrableDomain,
534+
$this->publicSuffix,
535+
...$this->getIDNAOptions()
536+
);
501537
}
502538

503539
/**
@@ -529,10 +565,10 @@ private function normalizeContent($domain)
529565
}
530566

531567
if (preg_match(self::REGEXP_IDN_PATTERN, $this->domain)) {
532-
return $this->idnToUnicode($domain);
568+
return $this->idnToUnicode($domain, $this->unicodeIDNAOption);
533569
}
534570

535-
return $this->idnToAscii($domain);
571+
return $this->idnToAscii($domain, $this->asciiIDNAOption);
536572
}
537573

538574
/**
@@ -605,10 +641,14 @@ public function withLabel(int $key, $label): self
605641
ksort($labels);
606642

607643
if (null !== $this->publicSuffix->getLabel($key)) {
608-
return new self(implode('.', array_reverse($labels)));
644+
return new self(implode('.', array_reverse($labels)), null, ...$this->getIDNAOptions());
609645
}
610646

611-
return new self(implode('.', array_reverse($labels)), $this->publicSuffix);
647+
return new self(
648+
implode('.', array_reverse($labels)),
649+
$this->publicSuffix,
650+
...$this->getIDNAOptions()
651+
);
612652
}
613653

614654
/**
@@ -651,15 +691,63 @@ public function withoutLabel(int $key, int ...$keys): self
651691
}
652692

653693
if ([] === $labels) {
654-
return new self();
694+
return new self(null, null, ...$this->getIDNAOptions());
655695
}
656696

657697
$domain = implode('.', array_reverse($labels));
658698
$psContent = $this->publicSuffix->getContent();
659699
if (null === $psContent || '.'.$psContent !== substr($domain, - strlen($psContent) - 1)) {
660-
return new self($domain);
700+
return new self($domain, null, ...$this->getIDNAOptions());
661701
}
662702

663-
return new self($domain, $this->publicSuffix);
703+
return new self($domain, $this->publicSuffix, ...$this->getIDNAOptions());
704+
}
705+
706+
/**
707+
* @return array
708+
*/
709+
public function getIDNAOptions(): array
710+
{
711+
return [$this->asciiIDNAOption, $this->unicodeIDNAOption];
712+
}
713+
714+
public function getAsciiIDNAOption(): int
715+
{
716+
return $this->asciiIDNAOption;
717+
}
718+
719+
public function getUnicodeIDNAOption(): int
720+
{
721+
return $this->unicodeIDNAOption;
722+
}
723+
/**
724+
* Set IDNA_* options for functions idn_to_ascii, idn_to_utf.
725+
* @see https://www.php.net/manual/en/intl.constants.php
726+
* @param int $forAscii
727+
* @param int $forUnicode
728+
* @return $this
729+
*/
730+
public function setIDNAOptions(int $forAscii, int $forUnicode)
731+
{
732+
$this->asciiIDNAOption = $forAscii;
733+
$this->unicodeIDNAOption = $forUnicode;
734+
return $this;
735+
}
736+
737+
/**
738+
* return true if domain contains deviation characters.
739+
* @see http://unicode.org/reports/tr46/#Transition_Considerations
740+
* @return bool
741+
**/
742+
public function isTransitionalDifferent(): bool
743+
{
744+
if ($this->isTransitionalDifferent === null) {
745+
try {
746+
$this->idnToAscii($this->getContent(), $this->asciiIDNAOption);
747+
} catch (Throwable $e) {
748+
$this->isTransitionalDifferent = false;
749+
}
750+
}
751+
return $this->isTransitionalDifferent;
664752
}
665753
}

src/DomainInterface.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,4 +99,11 @@ public function toUnicode();
9999
* from the right-most label to the left-most label.
100100
*/
101101
public function getIterator();
102+
103+
/**
104+
* return true if domain contains deviation characters.
105+
* @see http://unicode.org/reports/tr46/#Transition_Considerations
106+
* @return bool
107+
**/
108+
public function isTransitionalDifferent(): bool;
102109
}

src/IDNAConverterTrait.php

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
use function iterator_to_array;
2929
use function method_exists;
3030
use function preg_match;
31+
use function property_exists;
3132
use function rawurldecode;
3233
use function sprintf;
3334
use function strpos;
@@ -103,19 +104,22 @@ private static function getIdnErrors(int $error_bit): string
103104
*
104105
* @param string $domain
105106
*
107+
* @param int $IDNAOption
108+
*
106109
* @throws InvalidDomain if the string can not be converted to ASCII using IDN UTS46 algorithm
107110
*
108111
* @return string
109112
*/
110-
private function idnToAscii(string $domain): string
113+
private function idnToAscii(string $domain, int $IDNAOption = IDNA_DEFAULT): string
111114
{
112115
$domain = rawurldecode($domain);
113116
static $pattern = '/[^\x20-\x7f]/';
114117
if (!preg_match($pattern, $domain)) {
118+
$this->isTransitionalDifferent = false;
115119
return strtolower($domain);
116120
}
117121

118-
$output = idn_to_ascii($domain, 0, INTL_IDNA_VARIANT_UTS46, $arr);
122+
$output = idn_to_ascii($domain, $IDNAOption, INTL_IDNA_VARIANT_UTS46, $arr);
119123
if (0 !== $arr['errors']) {
120124
throw new InvalidDomain(sprintf('The host `%s` is invalid : %s', $domain, self::getIdnErrors($arr['errors'])));
121125
}
@@ -125,7 +129,11 @@ private function idnToAscii(string $domain): string
125129
throw new UnexpectedValueException(sprintf('The Intl extension is misconfigured for %s, please correct this issue before proceeding.', PHP_OS));
126130
}
127131
// @codeCoverageIgnoreEnd
128-
132+
133+
if (property_exists($this, 'isTransitionalDifferent')) {
134+
$this->isTransitionalDifferent = isset($arr['isTransitionalDifferent'])
135+
&& (bool)$arr['isTransitionalDifferent'] === true;
136+
}
129137
if (false === strpos($output, '%')) {
130138
return $output;
131139
}
@@ -140,13 +148,15 @@ private function idnToAscii(string $domain): string
140148
*
141149
* @param string $domain
142150
*
151+
* @param int $IDNAOption
152+
*
143153
* @throws InvalidDomain if the string can not be converted to UNICODE using IDN UTS46 algorithm
144154
*
145155
* @return string
146156
*/
147-
private function idnToUnicode(string $domain): string
157+
private function idnToUnicode(string $domain, int $IDNAOption = IDNA_DEFAULT): string
148158
{
149-
$output = idn_to_utf8($domain, 0, INTL_IDNA_VARIANT_UTS46, $arr);
159+
$output = idn_to_utf8($domain, $IDNAOption, INTL_IDNA_VARIANT_UTS46, $arr);
150160
if (0 !== $arr['errors']) {
151161
throw new InvalidDomain(sprintf('The host `%s` is invalid : %s', $domain, self::getIdnErrors($arr['errors'])));
152162
}
@@ -162,19 +172,16 @@ private function idnToUnicode(string $domain): string
162172

163173
/**
164174
* Filter and format the domain to ensure it is valid.
165-
*
166175
* Returns an array containing the formatted domain name in lowercase
167176
* with its associated labels in reverse order
168-
*
169-
* For example: setLabels('wWw.uLb.Ac.be') should return ['www.ulb.ac.be', ['be', 'ac', 'ulb', 'www']];
170-
*
171-
* @param mixed $domain
172-
*
177+
* For example: setLabels('wWw.uLb.Ac.be') should return ['www.ulb.ac.be', ['be', 'ac', 'ulb', 'www']];.
178+
* @param mixed $domain
179+
* @param int $asciiOption
180+
* @param int $unicodeOption
173181
* @throws InvalidDomain If the domain is invalid
174-
*
175182
* @return string[]
176183
*/
177-
private function setLabels($domain = null): array
184+
private function setLabels($domain = null, int $asciiOption = 0, int $unicodeOption = 0): array
178185
{
179186
if ($domain instanceof DomainInterface) {
180187
return iterator_to_array($domain, false);
@@ -222,9 +229,9 @@ private function setLabels($domain = null): array
222229
if (!preg_match($pattern, $formatted_domain)) {
223230
throw new InvalidDomain(sprintf('The domain `%s` is invalid: the labels are malformed', $domain));
224231
}
232+
233+
$ascii_domain = $this->idnToAscii($domain, $asciiOption);
225234

226-
$ascii_domain = $this->idnToAscii($domain);
227-
228-
return array_reverse(explode('.', $this->idnToUnicode($ascii_domain)));
235+
return array_reverse(explode('.', $this->idnToUnicode($ascii_domain, $unicodeOption)));
229236
}
230237
}

0 commit comments

Comments
 (0)