diff --git a/.gitattributes b/.gitattributes index 8a21992..9f6bf2f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -28,4 +28,5 @@ qodana.yaml export-ignore renovate.json export-ignore .coderabbit.yaml export-ignore sonar-project.properties export-ignore +CHANGELOG.md export-ignore CLAUDE.md export-ignore diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index ba4c492..bdb3d3f 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -14,7 +14,7 @@ body: attributes: label: PHP Version description: Provide the PHP version that you are using. - options: [ '7.4', '8.0','8.1','8.2','8.3', '8.4' ] + options: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5'] multiple: true validations: required: true diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 8b9841d..9359ecd 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -6,7 +6,7 @@ RegDom is a XOOPS library that parses domain names using the Mozilla Public Suff ## Project Layout -``` +```text src/ # Library source code RegisteredDomain.php # Main class: extracts registrable domains, validates cookie domains PublicSuffixList.php # PSL cache loader and query engine (NORMAL/WILDCARD/EXCEPTION rules) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c300b5..6b577c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,9 @@ name: CI on: [push, pull_request] +permissions: + contents: read + jobs: tests: name: PHP ${{ matrix.php }}${{ matrix.deps && format(' / {0}', matrix.deps) || '' }}${{ matrix.coverage && ' / Coverage' || '' }} @@ -73,11 +76,15 @@ jobs: run: mkdir -p build/logs - name: Run all CI checks + if: ${{ !matrix.coverage }} run: composer ci - - name: Generate coverage report + - name: Run CI checks with coverage if: ${{ matrix.coverage }} - run: ./vendor/bin/phpunit --colors=always --coverage-clover build/logs/clover.xml + run: | + composer lint + composer analyse + ./vendor/bin/phpunit --colors=always --coverage-clover build/logs/clover.xml - name: Upload coverage to Codecov if: ${{ matrix.coverage }} diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 8182179..57a141b 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -10,6 +10,8 @@ jobs: name: SonarCloud Analysis runs-on: ubuntu-latest if: github.repository == 'XOOPS/RegDom' + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} steps: - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 @@ -36,7 +38,7 @@ jobs: run: ./vendor/bin/phpunit --colors=always --coverage-clover build/logs/clover.xml - name: SonarCloud Scan - if: ${{ secrets.SONAR_TOKEN != '' }} + if: env.SONAR_TOKEN != '' uses: SonarSource/sonarqube-scan-action@a31c9398be7ace6bbfaf30c0bd5d415f843d45e9 # v7 env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.gitignore b/.gitignore index e6587de..7844798 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,9 @@ build/ # Downloaded PSL data (can be re-downloaded) data/public_suffix_list.dat +# Composer patches plugin (not used by this project) +patches.lock.json + # Claude local files CLAUDE.md .claude/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c551e7a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,181 @@ +# RegDom ChangeLog + +All notable changes to this project will be documented in this file. +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). + +## [2.0.2-beta3] - 2026-02-08 + +### Bug Fixes +* Fix `getPublicSuffix()` PSL exception-rule handling — exception branch returned one label too many; now correctly returns the exception rule minus its leftmost label (e.g. `!city.kawasaki.jp` yields public suffix `kawasaki.jp`, not `city.kawasaki.jp`) +* Fix `normalizeHost()` corrupting IPv6 addresses — the port-stripping regex `/:\d+$/` mistakenly treated the last hextet of addresses like `::1` as a port, truncating them; IPv6 literals are now parsed bracket-aware +* Fix PSL cache storing IDN rules as Unicode — `bin/update-psl.php` now normalizes all rules to ASCII/punycode via `idn_to_ascii()`, matching the form used by `normalizeDomain()` at runtime; previously 457 Unicode keys were unreachable when `ext-intl` was loaded +* Fix `composer.json` PHP constraint from `"^7.4 || ^8.5"` to `"^7.4 || ^8.0"` (previously excluded PHP 8.0–8.4) +* Fix `normalizeDomain()` crash on empty string — `idn_to_ascii('')` throws `ValueError` on PHP 8.4+; added `$domain !== ''` guard +* Fix PSL exception tests referencing `parliament.uk` (removed from the PSL); replaced with stable entries (`www.ck`, `city.kawasaki.jp`) +* Fix `bin/update-psl.php` failing when run standalone on PHP 7.4 — `str_starts_with()` was called without loading the polyfill autoloader +* Fix `bin/reloadpsl` calling removed methods (`clearDataDirectory()`, `getTree()`); rewritten as a simple wrapper +* Fix `getMetadata()` unreachable statement + +### Security +* Add `opcache_invalidate()` after atomic cache writes in `bin/update-psl.php` +* Add `is_array()` validation for all three PSL cache keys (`NORMAL`, `WILDCARD`, `EXCEPTION`) in `loadRules()` +* Add file size sanity check (100KB–10MB) before `include` in `loadRules()` to reject corrupt or tampered cache files +* Replace suppressed `@unlink()` with explicit `file_exists()` guard on temp-file cleanup in `bin/update-psl.php` + +### Changed +* Make `normalizeDomain()` a `private static` method (was instance method) +* Replace `strpos()`/`substr()` patterns with `str_contains()`/`str_starts_with()`/`str_ends_with()` via `symfony/polyfill-php80` +* Add `@throws PslCacheNotFoundException` PHPDoc tags to `PublicSuffixList::__construct()` and `loadRules()` +* Add XOOPS copyright headers to all source files +* Clarify `README.md` license section — library code is Apache-2.0, bundled PSL data is MPL-2.0 + +### Added +* Add `symfony/polyfill-php80` dependency for `str_contains`, `str_starts_with`, `str_ends_with` on PHP 7.4 +* Add `ext-intl` to `suggest` in `composer.json` + +### Tests +* Add PSL exception-rule regression tests — `sub.city.kawasaki.jp`, `city.kawasaki.jp`, `sub.www.ck`, `www.ck` for both `getPublicSuffix()` and `getRegisteredDomain()` +* Add IPv6 normalization tests — `[::1]`, `[::1]:443`, `[2001:db8::1]:8080` for `getRegisteredDomain()` and `domainMatches()` +* Add IDN/punycode PSL cache tests — `公司.cn`, `xn--55qx5d.cn` for `isPublicSuffix()` and `getPublicSuffix()` +* Add IDN integration tests — `test.公司.cn`, `test.xn--55qx5d.cn`, `公司.cn` through the full `RegisteredDomain` stack +* Add `PslCacheNotFoundExceptionTest` — exception class, message, code, previous exception +* Add edge case tests for `getRegisteredDomain()` — trailing dots, URLs with ports +* Add `PublicSuffixList` tests — empty string, IP addresses, `isException`, normalization (dots, case) +* Add `domainMatches()` tests for RFC 6265 cookie domain validation +* Expand `getMetadata()` assertions — `needs_update`, wildcard/exception counts + +### Infrastructure +* Add `declare(strict_types=1)` to `bin/update-psl.php` +* Add Composer autoloader bootstrap to `bin/update-psl.php` for standalone execution +* Add `composer ci` script chaining lint + analyse + test +* Add `composer update-psl` and `auto-update-psl` scripts with `XOOPS_SKIP_PSL_UPDATE` support +* Add GitHub Actions CI workflow — PHP 7.4–8.5 matrix, lowest-deps on 7.4, coverage on 8.3 +* Fix CI coverage matrix entry running tests twice — added `if: !matrix.coverage` guard +* Fix SonarCloud workflow using `secrets` context in step-level `if:` — moved to job-level `env` +* Add GitHub Copilot custom instructions (`.github/copilot-instructions.md`) +* Add Dependabot configuration for Composer and GitHub Actions +* Add Qodana static analysis workflow +* Add `.editorconfig` (UTF-8, LF, PSR-12 indentation) +* Remove `xsi:noNamespaceSchemaLocation` from `phpunit.xml.dist` to avoid XSD validation warnings on PHPUnit 9; `` element works natively on PHPUnit 10+/11+ +* Standardize `.gitattributes` with LF enforcement and export-ignore list +* Standardize `.gitignore` with local config overrides, build artifacts, PHPUnit cache +* Track `phpcs.xml` in version control (was previously untracked, causing CI lint failure) +* Fix PSR-4 autoload-dev mappings for `tests/unit/` and `tests/integration/` +* Rewrite `README.md` with current API documentation and usage examples +* Remove legacy config files (`.travis0.yml`, `phpcs.xml.dist`, `phpcs.xml0.dist`, `phpunit.xml0.dist`, `.scrutinizer0.yml`, `composer0.json`, `composer1.json`) + +## [2.0.2-beta2] - 2025-10-01 + +### Changed +* Rewrite `PublicSuffixList` — flat-file PSL cache (`data/psl.cache.php`) replaces tree-based data structure; three-category rule lookup (`NORMAL`, `WILDCARD`, `EXCEPTION`) with `O(1)` hash lookups +* Rewrite `RegisteredDomain` — simplified API using `PublicSuffixList` for all PSL queries; added `domainMatches()` for RFC 6265 cookie domain validation +* Replace `bin/reloadpsl` with `bin/update-psl.php` — HTTP conditional downloads (ETag/Last-Modified), atomic file writes, metadata tracking +* Add custom exception `PslCacheNotFoundException` for missing/invalid cache files +* Require `symfony/polyfill-mbstring` `^1.33` + +### Added +* Add `getMetadata()` to `PublicSuffixList` — cache age, rule counts, staleness warning +* Add `isException()` to `PublicSuffixList` — check if a domain is a PSL exception entry +* Add `isPublicSuffix()` and `getPublicSuffix()` public methods to `PublicSuffixList` +* Add integration test suite with real PSL data + +### Infrastructure +* Add PHPStan static analysis (`phpstan.neon`, level max) +* Add PHP_CodeSniffer with PSR-12 standard +* Add `.scrutinizer.yml` configuration +* Add `phpunit.xml.dist` with unit and integration test suites +* Require PHPUnit `^9.6 || ^10.0 || ^11.0` + +## [2.0.2-beta1] - 2025-09-10 + +### Added +* Add `.gitattributes` with export-ignore rules +* Add PHPStan configuration (`phpstan.neon`) +* Add PHP_CodeSniffer as a dev dependency + +### Changed +* Add `phpstan/phpstan` and `squizlabs/php_codesniffer` to `require-dev` +* Remove backslash prefix from `isset()` calls in `PublicSuffixList` +* Update `composer.json` dependencies + +## [2.0.1] - 2024-11-27 + +### Added +* Add `.github/CONTRIBUTING.md` with contribution guidelines +* Add `.github/ISSUE_TEMPLATE/bug-report.yml` with PHP version dropdown (7.4–8.4) +* Add `.github/ISSUE_TEMPLATE/feature-request.yml` + +### Changed +* Add PHP 8.4 to Travis CI test matrix +* Update `composer.json` PHP constraint + +## [2.0.0] - 2024-07-26 + +### Changed +* Bump `symfony/polyfill-mbstring` constraint from `^1.29.0` to `^1.30.0` + +## [2.0.0-Alpha] - 2024-07-08 + +### Changed +* Rename namespace from `Geekwright\RegDom` to `Xoops\RegDom` +* Modernise codebase for PHP 7.4+ — short array syntax, type hints, Yoda conditions removed +* Replace `PHPUnit_Framework_TestCase` with `PHPUnit\Framework\TestCase` +* Improve `decodePunycode()` implementation +* Add `is_string()` guard to curl return value +* Remove redundant type casts and `else` keywords +* Remove `PHP_VERSION_ID < 70000` compatibility code +* Update Public Suffix List data + +### Added +* Add GitHub Actions workflow for CI (`pr_tests.yml`) +* Add Scrutinizer CI configuration +* Add `symfony/polyfill-mbstring` `^1.29.0` as a runtime dependency +* Add PSR-4 autoloading via Composer + +### Removed +* Remove `/archive` directory with legacy files +* Remove Travis CI configuration (`.travis.yml`) + +## Pre-release History + +### 2023-04-30 +* Tweak PHP version check for pre-7.0 compatibility (geekwright) +* Fix `unserialize()` — limit allowed classes for security (geekwright) + +### 2022-04-08 +* Limit `unserialize()` to prevent object injection (geekwright) +* Add Scrutinizer CI configuration (geekwright) + +### 2019-08-30 +* Remove array access with curly brackets for PHP 7.4 compatibility (geekwright) +* Fix build for PHP 5.4 and 5.5 (geekwright) + +### 2018-02-07 +* Restructure unit tests (geekwright) +* Simplify Travis CI configuration (geekwright) + +### 2017-10-03 +* Add PHP 7.2 support (geekwright) +* Update Public Suffix List data (geekwright) +* Fix CI configuration (geekwright) + +### 2017-02-03 — Initial OO Rewrite +* Rewrite as OO library under `Geekwright\RegDom` namespace (geekwright) +* Add Composer support with PSR-4 autoloading (geekwright) +* Add `PublicSuffixList` class for PSL data management (geekwright) +* Add `RegisteredDomain` class for domain extraction (geekwright) +* Add `bin/reloadpsl` script for PSL updates (geekwright) +* Add unit tests with PHPUnit (geekwright) +* Add Scrutinizer CI and Travis CI configurations (geekwright) +* Add workaround for missing `ext-intl` (geekwright) + +### 2012-10-02 — Original Import +* Import Florian Sager's regdom-php library (Synchro/Marcus Bointon) +* PHP include file for domain registration data (Synchro/Marcus Bointon) + +[2.0.2-beta3]: https://github.com/XOOPS/RegDom/compare/v2.0.2-beta2...HEAD +[2.0.2-beta2]: https://github.com/XOOPS/RegDom/compare/v2.0.2-beta1...v2.0.2-beta2 +[2.0.2-beta1]: https://github.com/XOOPS/RegDom/compare/v2.0.1...v2.0.2-beta1 +[2.0.1]: https://github.com/XOOPS/RegDom/compare/v2.0.0...v2.0.1 +[2.0.0]: https://github.com/XOOPS/RegDom/compare/v2.0.0-Alpha...v2.0.0 +[2.0.0-Alpha]: https://github.com/XOOPS/RegDom/releases/tag/v2.0.0-Alpha diff --git a/README.md b/README.md index 8545611..069f22c 100644 --- a/README.md +++ b/README.md @@ -117,5 +117,9 @@ Marcus Bointon's adapted code: https://github.com/Synchro/regdom-php ## License -Licensed under Apache License 2.0 or Mozilla Public License 2.0 (dual-licensed). -See [LICENSE.txt](LICENSE.txt) for details. +The PHP library code in this repository is licensed under the Apache License 2.0. +See [LICENSE.txt](LICENSE.txt) for the full Apache 2.0 license text. + +The bundled Public Suffix List data/cache is derived from Mozilla's Public Suffix List +and is available under the Mozilla Public License 2.0 (MPL-2.0). For details, see +https://www.mozilla.org/en-US/MPL/2.0/ and https://publicsuffix.org/. diff --git a/bin/update-psl.php b/bin/update-psl.php index 2f23776..c41175b 100644 --- a/bin/update-psl.php +++ b/bin/update-psl.php @@ -49,15 +49,23 @@ // --- HTTP Conditional Download --- $headers = ['User-Agent: XOOPS-RegDom/1.1 (https://xoops.org)']; $meta = file_exists($metaPath) ? json_decode(file_get_contents($metaPath), true) : []; -if (!empty($meta['etag'])) $headers[] = "If-None-Match: {$meta['etag']}"; -if (!empty($meta['last_modified'])) $headers[] = "If-Modified-Since: {$meta['last_modified']}"; +if (!empty($meta['etag'])) { + $headers[] = "If-None-Match: {$meta['etag']}"; +} +if (!empty($meta['last_modified'])) { + $headers[] = "If-Modified-Since: {$meta['last_modified']}"; +} echo "Downloading from publicsuffix.org...\n"; $context = stream_context_create(['http' => ['method' => 'GET', 'timeout' => 20, 'header' => implode("\r\n", $headers), 'ignore_errors' => true]]); $latestList = @file_get_contents($sourceUrl, false, $context); $responseHeaders = $http_response_header ?? []; $statusCode = 0; -foreach ($responseHeaders as $header) if (preg_match('/^HTTP\/\d\.\d\s+(\d+)/', $header, $m)) $statusCode = (int)$m[1]; +foreach ($responseHeaders as $header) { + if (preg_match('/^HTTP\/\d\.\d\s+(\d+)/', $header, $m)) { + $statusCode = (int)$m[1]; + } +} if ($statusCode === 304) { echo "SUCCESS: Public Suffix List is already up-to-date (304 Not Modified).\n"; @@ -71,14 +79,44 @@ // --- Parse and Generate Cache --- echo "Parsing rules...\n"; + +// Normalize rule keys to ASCII/punycode so they match the form used by +// PublicSuffixList::normalizeDomain() at runtime (which calls idn_to_ascii). +$hasIntl = function_exists('idn_to_ascii'); +if (!$hasIntl) { + echo "WARNING: ext-intl not available — IDN rules will be stored as Unicode.\n"; + echo " Install ext-intl for correct internationalized domain handling.\n"; +} + +/** + * @param string $rule Raw rule text from the PSL (may be Unicode) + * @return string Normalised key (punycode when ext-intl is available) + */ +$normalizeRule = static function (string $rule) use ($hasIntl): string { + $rule = strtolower($rule); + if ($hasIntl) { + $ascii = idn_to_ascii($rule, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46); + if ($ascii !== false) { + return $ascii; + } + } + return $rule; +}; + $lines = explode("\n", $latestList); $rules = ['NORMAL' => [], 'WILDCARD' => [], 'EXCEPTION' => []]; foreach ($lines as $line) { $line = trim($line); - if (empty($line) || str_starts_with($line, '//')) continue; - if (str_starts_with($line, '!')) $rules['EXCEPTION'][substr($line, 1)] = true; - elseif (str_starts_with($line, '*.')) $rules['WILDCARD'][substr($line, 2)] = true; - else $rules['NORMAL'][$line] = true; + if (empty($line) || str_starts_with($line, '//')) { + continue; + } + if (str_starts_with($line, '!')) { + $rules['EXCEPTION'][$normalizeRule(substr($line, 1))] = true; + } elseif (str_starts_with($line, '*.')) { + $rules['WILDCARD'][$normalizeRule(substr($line, 2))] = true; + } else { + $rules['NORMAL'][$normalizeRule($line)] = true; + } } $totalRules = count($rules['NORMAL']) + count($rules['WILDCARD']) + count($rules['EXCEPTION']); @@ -92,7 +130,9 @@ // --- Atomic Write to Caches --- $writePaths = ['bundled' => $bundledCachePath]; -if ($runtimeCachePath) $writePaths['runtime'] = $runtimeCachePath; +if ($runtimeCachePath) { + $writePaths['runtime'] = $runtimeCachePath; +} foreach ($writePaths as $type => $cachePath) { $tmpPath = $cachePath . '.tmp.' . getmypid(); @@ -103,15 +143,21 @@ echo "SUCCESS: {$type} cache updated with {$totalRules} rules.\n"; } else { echo "WARNING: Could not write {$type} cache to {$cachePath}.\n"; - @unlink($tmpPath); + if (file_exists($tmpPath)) { + unlink($tmpPath); + } } } // --- Save Metadata --- $newMeta = ['updated' => date('c'), 'etag' => null, 'last_modified' => null]; foreach ($responseHeaders as $header) { - if (stripos($header, 'ETag:') === 0) $newMeta['etag'] = trim(substr($header, 5)); - if (stripos($header, 'Last-Modified:') === 0) $newMeta['last_modified'] = trim(substr($header, 14)); + if (stripos($header, 'ETag:') === 0) { + $newMeta['etag'] = trim(substr($header, 5)); + } + if (stripos($header, 'Last-Modified:') === 0) { + $newMeta['last_modified'] = trim(substr($header, 14)); + } } file_put_contents($metaPath, json_encode($newMeta, JSON_PRETTY_PRINT)); diff --git a/data/psl.cache.php b/data/psl.cache.php index 8608063..75f31ae 100644 --- a/data/psl.cache.php +++ b/data/psl.cache.php @@ -1,7 +1,7 @@ true, 'ci' => true, 'ac.ci' => true, - 'aéroport.ci' => true, + 'xn--aroport-bya.ci' => true, 'asso.ci' => true, 'co.ci' => true, 'com.ci' => true, @@ -659,9 +659,9 @@ 'mil.cn' => true, 'net.cn' => true, 'org.cn' => true, - '公司.cn' => true, - '網絡.cn' => true, - '网络.cn' => true, + 'xn--55qx5d.cn' => true, + 'xn--od0alg.cn' => true, + 'xn--io0a7i.cn' => true, 'ah.cn' => true, 'bj.cn' => true, 'cq.cn' => true, @@ -1007,21 +1007,21 @@ 'idv.hk' => true, 'net.hk' => true, 'org.hk' => true, - '个人.hk' => true, - '個人.hk' => true, - '公司.hk' => true, - '政府.hk' => true, - '敎育.hk' => true, - '教育.hk' => true, - '箇人.hk' => true, - '組織.hk' => true, - '組织.hk' => true, - '網絡.hk' => true, - '網络.hk' => true, - '组織.hk' => true, - '组织.hk' => true, - '网絡.hk' => true, - '网络.hk' => true, + 'xn--ciqpn.hk' => true, + 'xn--gmqw5a.hk' => true, + 'xn--55qx5d.hk' => true, + 'xn--mxtq1m.hk' => true, + 'xn--lcvr32d.hk' => true, + 'xn--wcvs22d.hk' => true, + 'xn--gmq050i.hk' => true, + 'xn--uc0atv.hk' => true, + 'xn--uc0ay4a.hk' => true, + 'xn--od0alg.hk' => true, + 'xn--zf0avx.hk' => true, + 'xn--mk0axi.hk' => true, + 'xn--tn0ag.hk' => true, + 'xn--od0aq3b.hk' => true, + 'xn--io0a7i.hk' => true, 'hm' => true, 'hn' => true, 'com.hn' => true, @@ -1099,7 +1099,7 @@ 'ponpes.id' => true, 'sch.id' => true, 'web.id' => true, - 'ᬩᬮᬶ.id' => true, + 'xn--9tfky.id' => true, 'ie' => true, 'gov.ie' => true, 'il' => true, @@ -1111,11 +1111,11 @@ 'muni.il' => true, 'net.il' => true, 'org.il' => true, - 'ישראל' => true, - 'אקדמיה.ישראל' => true, - 'ישוב.ישראל' => true, - 'צהל.ישראל' => true, - 'ממשל.ישראל' => true, + 'xn--4dbrk0ce' => true, + 'xn--4dbgdty6c.xn--4dbrk0ce' => true, + 'xn--5dbhl8d.xn--4dbrk0ce' => true, + 'xn--8dbq2a.xn--4dbrk0ce' => true, + 'xn--hebda8b.xn--4dbrk0ce' => true, 'im' => true, 'ac.im' => true, 'co.im' => true, @@ -1197,8 +1197,8 @@ 'net.ir' => true, 'org.ir' => true, 'sch.ir' => true, - 'ایران.ir' => true, - 'ايران.ir' => true, + 'xn--mgba3a4f16a.ir' => true, + 'xn--mgba3a4fra.ir' => true, 'is' => true, 'it' => true, 'edu.it' => true, @@ -1256,9 +1256,9 @@ 'tos.it' => true, 'toscana.it' => true, 'trentin-sud-tirol.it' => true, - 'trentin-süd-tirol.it' => true, + 'xn--trentin-sd-tirol-rzb.it' => true, 'trentin-sudtirol.it' => true, - 'trentin-südtirol.it' => true, + 'xn--trentin-sdtirol-7vb.it' => true, 'trentin-sued-tirol.it' => true, 'trentin-suedtirol.it' => true, 'trentino.it' => true, @@ -1269,9 +1269,9 @@ 'trentino-s-tirol.it' => true, 'trentino-stirol.it' => true, 'trentino-sud-tirol.it' => true, - 'trentino-süd-tirol.it' => true, + 'xn--trentino-sd-tirol-c3b.it' => true, 'trentino-sudtirol.it' => true, - 'trentino-südtirol.it' => true, + 'xn--trentino-sdtirol-szb.it' => true, 'trentino-sued-tirol.it' => true, 'trentino-suedtirol.it' => true, 'trentinoa-adige.it' => true, @@ -1281,15 +1281,15 @@ 'trentinos-tirol.it' => true, 'trentinostirol.it' => true, 'trentinosud-tirol.it' => true, - 'trentinosüd-tirol.it' => true, + 'xn--trentinosd-tirol-rzb.it' => true, 'trentinosudtirol.it' => true, - 'trentinosüdtirol.it' => true, + 'xn--trentinosdtirol-7vb.it' => true, 'trentinosued-tirol.it' => true, 'trentinosuedtirol.it' => true, 'trentinsud-tirol.it' => true, - 'trentinsüd-tirol.it' => true, + 'xn--trentinsd-tirol-6vb.it' => true, 'trentinsudtirol.it' => true, - 'trentinsüdtirol.it' => true, + 'xn--trentinsdtirol-nsb.it' => true, 'trentinsued-tirol.it' => true, 'trentinsuedtirol.it' => true, 'tuscany.it' => true, @@ -1306,13 +1306,13 @@ 'valled-aosta.it' => true, 'valledaosta.it' => true, 'vallee-aoste.it' => true, - 'vallée-aoste.it' => true, + 'xn--valle-aoste-ebb.it' => true, 'vallee-d-aoste.it' => true, - 'vallée-d-aoste.it' => true, + 'xn--valle-d-aoste-ehb.it' => true, 'valleeaoste.it' => true, - 'valléeaoste.it' => true, + 'xn--valleaoste-e7a.it' => true, 'valleedaoste.it' => true, - 'valléedaoste.it' => true, + 'xn--valledaoste-ebb.it' => true, 'vao.it' => true, 'vda.it' => true, 'ven.it' => true, @@ -1346,7 +1346,7 @@ 'ba.it' => true, 'balsan.it' => true, 'balsan-sudtirol.it' => true, - 'balsan-südtirol.it' => true, + 'xn--balsan-sdtirol-nsb.it' => true, 'balsan-suedtirol.it' => true, 'bari.it' => true, 'barletta-trani-andria.it' => true, @@ -1365,7 +1365,7 @@ 'bolzano-altoadige.it' => true, 'bozen.it' => true, 'bozen-sudtirol.it' => true, - 'bozen-südtirol.it' => true, + 'xn--bozen-sdtirol-2ob.it' => true, 'bozen-suedtirol.it' => true, 'br.it' => true, 'brescia.it' => true, @@ -1374,7 +1374,7 @@ 'bt.it' => true, 'bulsan.it' => true, 'bulsan-sudtirol.it' => true, - 'bulsan-südtirol.it' => true, + 'xn--bulsan-sdtirol-nsb.it' => true, 'bulsan-suedtirol.it' => true, 'bz.it' => true, 'ca.it' => true, @@ -1393,9 +1393,9 @@ 'cb.it' => true, 'ce.it' => true, 'cesena-forli.it' => true, - 'cesena-forlì.it' => true, + 'xn--cesena-forl-mcb.it' => true, 'cesenaforli.it' => true, - 'cesenaforlì.it' => true, + 'xn--cesenaforl-i8a.it' => true, 'ch.it' => true, 'chieti.it' => true, 'ci.it' => true, @@ -1426,9 +1426,9 @@ 'fm.it' => true, 'foggia.it' => true, 'forli-cesena.it' => true, - 'forlì-cesena.it' => true, + 'xn--forl-cesena-fcb.it' => true, 'forlicesena.it' => true, - 'forlìcesena.it' => true, + 'xn--forlcesena-c8a.it' => true, 'fr.it' => true, 'frosinone.it' => true, 'ge.it' => true, @@ -1558,7 +1558,7 @@ 'sp.it' => true, 'sr.it' => true, 'ss.it' => true, - 'südtirol.it' => true, + 'xn--sdtirol-n2a.it' => true, 'suedtirol.it' => true, 'sv.it' => true, 'ta.it' => true, @@ -1684,53 +1684,53 @@ 'yamagata.jp' => true, 'yamaguchi.jp' => true, 'yamanashi.jp' => true, - '三重.jp' => true, - '京都.jp' => true, - '佐賀.jp' => true, - '兵庫.jp' => true, - '北海道.jp' => true, - '千葉.jp' => true, - '和歌山.jp' => true, - '埼玉.jp' => true, - '大分.jp' => true, - '大阪.jp' => true, - '奈良.jp' => true, - '宮城.jp' => true, - '宮崎.jp' => true, - '富山.jp' => true, - '山口.jp' => true, - '山形.jp' => true, - '山梨.jp' => true, - '岐阜.jp' => true, - '岡山.jp' => true, - '岩手.jp' => true, - '島根.jp' => true, - '広島.jp' => true, - '徳島.jp' => true, - '愛媛.jp' => true, - '愛知.jp' => true, - '新潟.jp' => true, - '東京.jp' => true, - '栃木.jp' => true, - '沖縄.jp' => true, - '滋賀.jp' => true, - '熊本.jp' => true, - '石川.jp' => true, - '神奈川.jp' => true, - '福井.jp' => true, - '福岡.jp' => true, - '福島.jp' => true, - '秋田.jp' => true, - '群馬.jp' => true, - '茨城.jp' => true, - '長崎.jp' => true, - '長野.jp' => true, - '青森.jp' => true, - '静岡.jp' => true, - '香川.jp' => true, - '高知.jp' => true, - '鳥取.jp' => true, - '鹿児島.jp' => true, + 'xn--ehqz56n.jp' => true, + 'xn--1lqs03n.jp' => true, + 'xn--qqqt11m.jp' => true, + 'xn--f6qx53a.jp' => true, + 'xn--djrs72d6uy.jp' => true, + 'xn--mkru45i.jp' => true, + 'xn--0trq7p7nn.jp' => true, + 'xn--5js045d.jp' => true, + 'xn--kbrq7o.jp' => true, + 'xn--pssu33l.jp' => true, + 'xn--ntsq17g.jp' => true, + 'xn--uisz3g.jp' => true, + 'xn--6btw5a.jp' => true, + 'xn--1ctwo.jp' => true, + 'xn--6orx2r.jp' => true, + 'xn--rht61e.jp' => true, + 'xn--rht27z.jp' => true, + 'xn--nit225k.jp' => true, + 'xn--rht3d.jp' => true, + 'xn--djty4k.jp' => true, + 'xn--klty5x.jp' => true, + 'xn--kltx9a.jp' => true, + 'xn--kltp7d.jp' => true, + 'xn--c3s14m.jp' => true, + 'xn--vgu402c.jp' => true, + 'xn--efvn9s.jp' => true, + 'xn--1lqs71d.jp' => true, + 'xn--4pvxs.jp' => true, + 'xn--uuwu58a.jp' => true, + 'xn--zbx025d.jp' => true, + 'xn--8pvr4u.jp' => true, + 'xn--5rtp49c.jp' => true, + 'xn--ntso0iqx3a.jp' => true, + 'xn--elqq16h.jp' => true, + 'xn--4it168d.jp' => true, + 'xn--klt787d.jp' => true, + 'xn--rny31h.jp' => true, + 'xn--7t0a264c.jp' => true, + 'xn--uist22h.jp' => true, + 'xn--8ltr62k.jp' => true, + 'xn--2m4a15e.jp' => true, + 'xn--32vp30h.jp' => true, + 'xn--4it797k.jp' => true, + 'xn--5rtq34k.jp' => true, + 'xn--k7yn95e.jp' => true, + 'xn--tor131o.jp' => true, + 'xn--d5qv7z876c.jp' => true, 'aisai.aichi.jp' => true, 'ama.aichi.jp' => true, 'anjo.aichi.jp' => true, @@ -3836,119 +3836,119 @@ 'gs.va.no' => true, 'gs.vf.no' => true, 'akrehamn.no' => true, - 'åkrehamn.no' => true, + 'xn--krehamn-dxa.no' => true, 'algard.no' => true, - 'ålgård.no' => true, + 'xn--lgrd-poac.no' => true, 'arna.no' => true, 'bronnoysund.no' => true, - 'brønnøysund.no' => true, + 'xn--brnnysund-m8ac.no' => true, 'brumunddal.no' => true, 'bryne.no' => true, 'drobak.no' => true, - 'drøbak.no' => true, + 'xn--drbak-wua.no' => true, 'egersund.no' => true, 'fetsund.no' => true, 'floro.no' => true, - 'florø.no' => true, + 'xn--flor-jra.no' => true, 'fredrikstad.no' => true, 'hokksund.no' => true, 'honefoss.no' => true, - 'hønefoss.no' => true, + 'xn--hnefoss-q1a.no' => true, 'jessheim.no' => true, 'jorpeland.no' => true, - 'jørpeland.no' => true, + 'xn--jrpeland-54a.no' => true, 'kirkenes.no' => true, 'kopervik.no' => true, 'krokstadelva.no' => true, 'langevag.no' => true, - 'langevåg.no' => true, + 'xn--langevg-jxa.no' => true, 'leirvik.no' => true, 'mjondalen.no' => true, - 'mjøndalen.no' => true, + 'xn--mjndalen-64a.no' => true, 'mo-i-rana.no' => true, 'mosjoen.no' => true, - 'mosjøen.no' => true, + 'xn--mosjen-eya.no' => true, 'nesoddtangen.no' => true, 'orkanger.no' => true, 'osoyro.no' => true, - 'osøyro.no' => true, + 'xn--osyro-wua.no' => true, 'raholt.no' => true, - 'råholt.no' => true, + 'xn--rholt-mra.no' => true, 'sandnessjoen.no' => true, - 'sandnessjøen.no' => true, + 'xn--sandnessjen-ogb.no' => true, 'skedsmokorset.no' => true, 'slattum.no' => true, 'spjelkavik.no' => true, 'stathelle.no' => true, 'stavern.no' => true, 'stjordalshalsen.no' => true, - 'stjørdalshalsen.no' => true, + 'xn--stjrdalshalsen-sqb.no' => true, 'tananger.no' => true, 'tranby.no' => true, 'vossevangen.no' => true, 'aarborte.no' => true, 'aejrie.no' => true, 'afjord.no' => true, - 'åfjord.no' => true, + 'xn--fjord-lra.no' => true, 'agdenes.no' => true, 'nes.akershus.no' => true, 'aknoluokta.no' => true, - 'ákŋoluokta.no' => true, + 'xn--koluokta-7ya57h.no' => true, 'al.no' => true, - 'ål.no' => true, + 'xn--l-1fa.no' => true, 'alaheadju.no' => true, - 'álaheadju.no' => true, + 'xn--laheadju-7ya.no' => true, 'alesund.no' => true, - 'ålesund.no' => true, + 'xn--lesund-hua.no' => true, 'alstahaug.no' => true, 'alta.no' => true, - 'áltá.no' => true, + 'xn--lt-liac.no' => true, 'alvdal.no' => true, 'amli.no' => true, - 'åmli.no' => true, + 'xn--mli-tla.no' => true, 'amot.no' => true, - 'åmot.no' => true, + 'xn--mot-tla.no' => true, 'andasuolo.no' => true, 'andebu.no' => true, 'andoy.no' => true, - 'andøy.no' => true, + 'xn--andy-ira.no' => true, 'ardal.no' => true, - 'årdal.no' => true, + 'xn--rdal-poa.no' => true, 'aremark.no' => true, 'arendal.no' => true, - 'ås.no' => true, + 'xn--s-1fa.no' => true, 'aseral.no' => true, - 'åseral.no' => true, + 'xn--seral-lra.no' => true, 'asker.no' => true, 'askim.no' => true, 'askoy.no' => true, - 'askøy.no' => true, + 'xn--asky-ira.no' => true, 'askvoll.no' => true, 'asnes.no' => true, - 'åsnes.no' => true, + 'xn--snes-poa.no' => true, 'audnedaln.no' => true, 'aukra.no' => true, 'aure.no' => true, 'aurland.no' => true, 'aurskog-holand.no' => true, - 'aurskog-høland.no' => true, + 'xn--aurskog-hland-jnb.no' => true, 'austevoll.no' => true, 'austrheim.no' => true, 'averoy.no' => true, - 'averøy.no' => true, + 'xn--avery-yua.no' => true, 'badaddja.no' => true, - 'bådåddjå.no' => true, - 'bærum.no' => true, + 'xn--bdddj-mrabd.no' => true, + 'xn--brum-voa.no' => true, 'bahcavuotna.no' => true, - 'báhcavuotna.no' => true, + 'xn--bhcavuotna-s4a.no' => true, 'bahccavuotna.no' => true, - 'báhccavuotna.no' => true, + 'xn--bhccavuotna-k7a.no' => true, 'baidar.no' => true, - 'báidár.no' => true, + 'xn--bidr-5nac.no' => true, 'bajddar.no' => true, - 'bájddar.no' => true, + 'xn--bjddar-pta.no' => true, 'balat.no' => true, - 'bálát.no' => true, + 'xn--blt-elab.no' => true, 'balestrand.no' => true, 'ballangen.no' => true, 'balsfjord.no' => true, @@ -3956,49 +3956,49 @@ 'bardu.no' => true, 'barum.no' => true, 'batsfjord.no' => true, - 'båtsfjord.no' => true, + 'xn--btsfjord-9za.no' => true, 'bearalvahki.no' => true, - 'bearalváhki.no' => true, + 'xn--bearalvhki-y4a.no' => true, 'beardu.no' => true, 'beiarn.no' => true, 'berg.no' => true, 'bergen.no' => true, 'berlevag.no' => true, - 'berlevåg.no' => true, + 'xn--berlevg-jxa.no' => true, 'bievat.no' => true, - 'bievát.no' => true, + 'xn--bievt-0qa.no' => true, 'bindal.no' => true, 'birkenes.no' => true, 'bjerkreim.no' => true, 'bjugn.no' => true, 'bodo.no' => true, - 'bodø.no' => true, + 'xn--bod-2na.no' => true, 'bokn.no' => true, 'bomlo.no' => true, - 'bømlo.no' => true, + 'xn--bmlo-gra.no' => true, 'bremanger.no' => true, 'bronnoy.no' => true, - 'brønnøy.no' => true, + 'xn--brnny-wuac.no' => true, 'budejju.no' => true, 'nes.buskerud.no' => true, 'bygland.no' => true, 'bykle.no' => true, 'cahcesuolo.no' => true, - 'čáhcesuolo.no' => true, + 'xn--hcesuolo-7ya35b.no' => true, 'davvenjarga.no' => true, - 'davvenjárga.no' => true, + 'xn--davvenjrga-y4a.no' => true, 'davvesiida.no' => true, 'deatnu.no' => true, 'dielddanuorri.no' => true, 'divtasvuodna.no' => true, 'divttasvuotna.no' => true, 'donna.no' => true, - 'dønna.no' => true, + 'xn--dnna-gra.no' => true, 'dovre.no' => true, 'drammen.no' => true, 'drangedal.no' => true, 'dyroy.no' => true, - 'dyrøy.no' => true, + 'xn--dyry-ira.no' => true, 'eid.no' => true, 'eidfjord.no' => true, 'eidsberg.no' => true, @@ -4011,7 +4011,7 @@ 'etne.no' => true, 'etnedal.no' => true, 'evenassi.no' => true, - 'evenášši.no' => true, + 'xn--eveni-0qa01ga.no' => true, 'evenes.no' => true, 'evje-og-hornnes.no' => true, 'farsund.no' => true, @@ -4019,12 +4019,12 @@ 'fedje.no' => true, 'fet.no' => true, 'finnoy.no' => true, - 'finnøy.no' => true, + 'xn--finny-yua.no' => true, 'fitjar.no' => true, 'fjaler.no' => true, 'fjell.no' => true, 'fla.no' => true, - 'flå.no' => true, + 'xn--fl-zia.no' => true, 'flakstad.no' => true, 'flatanger.no' => true, 'flekkefjord.no' => true, @@ -4032,40 +4032,40 @@ 'flora.no' => true, 'folldal.no' => true, 'forde.no' => true, - 'førde.no' => true, + 'xn--frde-gra.no' => true, 'forsand.no' => true, 'fosnes.no' => true, - 'fræna.no' => true, + 'xn--frna-woa.no' => true, 'frana.no' => true, 'frei.no' => true, 'frogn.no' => true, 'froland.no' => true, 'frosta.no' => true, 'froya.no' => true, - 'frøya.no' => true, + 'xn--frya-hra.no' => true, 'fuoisku.no' => true, 'fuossko.no' => true, 'fusa.no' => true, 'fyresdal.no' => true, 'gaivuotna.no' => true, - 'gáivuotna.no' => true, + 'xn--givuotna-8ya.no' => true, 'galsa.no' => true, - 'gálsá.no' => true, + 'xn--gls-elac.no' => true, 'gamvik.no' => true, 'gangaviika.no' => true, - 'gáŋgaviika.no' => true, + 'xn--ggaviika-8ya47h.no' => true, 'gaular.no' => true, 'gausdal.no' => true, 'giehtavuoatna.no' => true, 'gildeskal.no' => true, - 'gildeskål.no' => true, + 'xn--gildeskl-g0a.no' => true, 'giske.no' => true, 'gjemnes.no' => true, 'gjerdrum.no' => true, 'gjerstad.no' => true, 'gjesdal.no' => true, 'gjovik.no' => true, - 'gjøvik.no' => true, + 'xn--gjvik-wua.no' => true, 'gloppen.no' => true, 'gol.no' => true, 'gran.no' => true, @@ -4078,21 +4078,21 @@ 'gulen.no' => true, 'guovdageaidnu.no' => true, 'ha.no' => true, - 'hå.no' => true, + 'xn--h-2fa.no' => true, 'habmer.no' => true, - 'hábmer.no' => true, + 'xn--hbmer-xqa.no' => true, 'hadsel.no' => true, - 'hægebostad.no' => true, + 'xn--hgebostad-g3a.no' => true, 'hagebostad.no' => true, 'halden.no' => true, 'halsa.no' => true, 'hamar.no' => true, 'hamaroy.no' => true, 'hammarfeasta.no' => true, - 'hámmárfeasta.no' => true, + 'xn--hmmrfeasta-s4ac.no' => true, 'hammerfest.no' => true, 'hapmir.no' => true, - 'hápmir.no' => true, + 'xn--hpmir-xqa.no' => true, 'haram.no' => true, 'hareid.no' => true, 'harstad.no' => true, @@ -4101,7 +4101,7 @@ 'haugesund.no' => true, 'os.hedmark.no' => true, 'valer.hedmark.no' => true, - 'våler.hedmark.no' => true, + 'xn--vler-qoa.hedmark.no' => true, 'hemne.no' => true, 'hemnes.no' => true, 'hemsedal.no' => true, @@ -4109,57 +4109,57 @@ 'hjartdal.no' => true, 'hjelmeland.no' => true, 'hobol.no' => true, - 'hobøl.no' => true, + 'xn--hobl-ira.no' => true, 'hof.no' => true, 'hol.no' => true, 'hole.no' => true, 'holmestrand.no' => true, 'holtalen.no' => true, - 'holtålen.no' => true, + 'xn--holtlen-hxa.no' => true, 'os.hordaland.no' => true, 'hornindal.no' => true, 'horten.no' => true, 'hoyanger.no' => true, - 'høyanger.no' => true, + 'xn--hyanger-q1a.no' => true, 'hoylandet.no' => true, - 'høylandet.no' => true, + 'xn--hylandet-54a.no' => true, 'hurdal.no' => true, 'hurum.no' => true, 'hvaler.no' => true, 'hyllestad.no' => true, 'ibestad.no' => true, 'inderoy.no' => true, - 'inderøy.no' => true, + 'xn--indery-fya.no' => true, 'iveland.no' => true, 'ivgu.no' => true, 'jevnaker.no' => true, 'jolster.no' => true, - 'jølster.no' => true, + 'xn--jlster-bya.no' => true, 'jondal.no' => true, 'kafjord.no' => true, - 'kåfjord.no' => true, + 'xn--kfjord-iua.no' => true, 'karasjohka.no' => true, - 'kárášjohka.no' => true, + 'xn--krjohka-hwab49j.no' => true, 'karasjok.no' => true, 'karlsoy.no' => true, 'karmoy.no' => true, - 'karmøy.no' => true, + 'xn--karmy-yua.no' => true, 'kautokeino.no' => true, 'klabu.no' => true, - 'klæbu.no' => true, + 'xn--klbu-woa.no' => true, 'klepp.no' => true, 'kongsberg.no' => true, 'kongsvinger.no' => true, 'kraanghke.no' => true, - 'kråanghke.no' => true, + 'xn--kranghke-b0a.no' => true, 'kragero.no' => true, - 'kragerø.no' => true, + 'xn--krager-gya.no' => true, 'kristiansand.no' => true, 'kristiansund.no' => true, 'krodsherad.no' => true, - 'krødsherad.no' => true, - 'kvæfjord.no' => true, - 'kvænangen.no' => true, + 'xn--krdsherad-m8a.no' => true, + 'xn--kvfjord-nxa.no' => true, + 'xn--kvnangen-k0a.no' => true, 'kvafjord.no' => true, 'kvalsund.no' => true, 'kvam.no' => true, @@ -4168,17 +4168,17 @@ 'kvinnherad.no' => true, 'kviteseid.no' => true, 'kvitsoy.no' => true, - 'kvitsøy.no' => true, + 'xn--kvitsy-fya.no' => true, 'laakesvuemie.no' => true, - 'lærdal.no' => true, + 'xn--lrdal-sra.no' => true, 'lahppi.no' => true, - 'láhppi.no' => true, + 'xn--lhppi-xqa.no' => true, 'lardal.no' => true, 'larvik.no' => true, 'lavagis.no' => true, 'lavangen.no' => true, 'leangaviika.no' => true, - 'leaŋgaviika.no' => true, + 'xn--leagaviika-52b.no' => true, 'lebesby.no' => true, 'leikanger.no' => true, 'leirfjord.no' => true, @@ -4193,63 +4193,63 @@ 'lillehammer.no' => true, 'lillesand.no' => true, 'lindas.no' => true, - 'lindås.no' => true, + 'xn--linds-pra.no' => true, 'lindesnes.no' => true, 'loabat.no' => true, - 'loabát.no' => true, + 'xn--loabt-0qa.no' => true, 'lodingen.no' => true, - 'lødingen.no' => true, + 'xn--ldingen-q1a.no' => true, 'lom.no' => true, 'loppa.no' => true, 'lorenskog.no' => true, - 'lørenskog.no' => true, + 'xn--lrenskog-54a.no' => true, 'loten.no' => true, - 'løten.no' => true, + 'xn--lten-gra.no' => true, 'lund.no' => true, 'lunner.no' => true, 'luroy.no' => true, - 'lurøy.no' => true, + 'xn--lury-ira.no' => true, 'luster.no' => true, 'lyngdal.no' => true, 'lyngen.no' => true, 'malatvuopmi.no' => true, - 'málatvuopmi.no' => true, + 'xn--mlatvuopmi-s4a.no' => true, 'malselv.no' => true, - 'målselv.no' => true, + 'xn--mlselv-iua.no' => true, 'malvik.no' => true, 'mandal.no' => true, 'marker.no' => true, 'marnardal.no' => true, 'masfjorden.no' => true, 'masoy.no' => true, - 'måsøy.no' => true, + 'xn--msy-ula0h.no' => true, 'matta-varjjat.no' => true, - 'mátta-várjjat.no' => true, + 'xn--mtta-vrjjat-k7af.no' => true, 'meland.no' => true, 'meldal.no' => true, 'melhus.no' => true, 'meloy.no' => true, - 'meløy.no' => true, + 'xn--mely-ira.no' => true, 'meraker.no' => true, - 'meråker.no' => true, + 'xn--merker-kua.no' => true, 'midsund.no' => true, 'midtre-gauldal.no' => true, 'moareke.no' => true, - 'moåreke.no' => true, + 'xn--moreke-jua.no' => true, 'modalen.no' => true, 'modum.no' => true, 'molde.no' => true, 'heroy.more-og-romsdal.no' => true, 'sande.more-og-romsdal.no' => true, - 'herøy.møre-og-romsdal.no' => true, - 'sande.møre-og-romsdal.no' => true, + 'xn--hery-ira.xn--mre-og-romsdal-qqb.no' => true, + 'sande.xn--mre-og-romsdal-qqb.no' => true, 'moskenes.no' => true, 'moss.no' => true, 'muosat.no' => true, - 'muosát.no' => true, + 'xn--muost-0qa.no' => true, 'naamesjevuemie.no' => true, - 'nååmesjevuemie.no' => true, - 'nærøy.no' => true, + 'xn--nmesjevuemie-tcba.no' => true, + 'xn--nry-yla5g.no' => true, 'namdalseid.no' => true, 'namsos.no' => true, 'namsskogan.no' => true, @@ -4259,7 +4259,7 @@ 'narvik.no' => true, 'naustdal.no' => true, 'navuotna.no' => true, - 'návuotna.no' => true, + 'xn--nvuotna-hwa.no' => true, 'nedre-eiker.no' => true, 'nesna.no' => true, 'nesodden.no' => true, @@ -4273,58 +4273,58 @@ 'norddal.no' => true, 'nordkapp.no' => true, 'bo.nordland.no' => true, - 'bø.nordland.no' => true, + 'xn--b-5ga.nordland.no' => true, 'heroy.nordland.no' => true, - 'herøy.nordland.no' => true, + 'xn--hery-ira.nordland.no' => true, 'nordre-land.no' => true, 'nordreisa.no' => true, 'nore-og-uvdal.no' => true, 'notodden.no' => true, 'notteroy.no' => true, - 'nøtterøy.no' => true, + 'xn--nttery-byae.no' => true, 'odda.no' => true, 'oksnes.no' => true, - 'øksnes.no' => true, + 'xn--ksnes-uua.no' => true, 'omasvuotna.no' => true, 'oppdal.no' => true, 'oppegard.no' => true, - 'oppegård.no' => true, + 'xn--oppegrd-ixa.no' => true, 'orkdal.no' => true, 'orland.no' => true, - 'ørland.no' => true, + 'xn--rland-uua.no' => true, 'orskog.no' => true, - 'ørskog.no' => true, + 'xn--rskog-uua.no' => true, 'orsta.no' => true, - 'ørsta.no' => true, + 'xn--rsta-fra.no' => true, 'osen.no' => true, 'osteroy.no' => true, - 'osterøy.no' => true, + 'xn--ostery-fya.no' => true, 'valer.ostfold.no' => true, - 'våler.østfold.no' => true, + 'xn--vler-qoa.xn--stfold-9xa.no' => true, 'ostre-toten.no' => true, - 'østre-toten.no' => true, + 'xn--stre-toten-zcb.no' => true, 'overhalla.no' => true, 'ovre-eiker.no' => true, - 'øvre-eiker.no' => true, + 'xn--vre-eiker-k8a.no' => true, 'oyer.no' => true, - 'øyer.no' => true, + 'xn--yer-zna.no' => true, 'oygarden.no' => true, - 'øygarden.no' => true, + 'xn--ygarden-p1a.no' => true, 'oystre-slidre.no' => true, - 'øystre-slidre.no' => true, + 'xn--ystre-slidre-ujb.no' => true, 'porsanger.no' => true, 'porsangu.no' => true, - 'porsáŋgu.no' => true, + 'xn--porsgu-sta26f.no' => true, 'porsgrunn.no' => true, 'rade.no' => true, - 'råde.no' => true, + 'xn--rde-ula.no' => true, 'radoy.no' => true, - 'radøy.no' => true, - 'rælingen.no' => true, + 'xn--rady-ira.no' => true, + 'xn--rlingen-mxa.no' => true, 'rahkkeravju.no' => true, - 'ráhkkerávju.no' => true, + 'xn--rhkkervju-01af.no' => true, 'raisa.no' => true, - 'ráisa.no' => true, + 'xn--risa-5na.no' => true, 'rakkestad.no' => true, 'ralingen.no' => true, 'rana.no' => true, @@ -4333,41 +4333,41 @@ 'rendalen.no' => true, 'rennebu.no' => true, 'rennesoy.no' => true, - 'rennesøy.no' => true, + 'xn--rennesy-v1a.no' => true, 'rindal.no' => true, 'ringebu.no' => true, 'ringerike.no' => true, 'ringsaker.no' => true, 'risor.no' => true, - 'risør.no' => true, + 'xn--risr-ira.no' => true, 'rissa.no' => true, 'roan.no' => true, 'rodoy.no' => true, - 'rødøy.no' => true, + 'xn--rdy-0nab.no' => true, 'rollag.no' => true, 'romsa.no' => true, 'romskog.no' => true, - 'rømskog.no' => true, + 'xn--rmskog-bya.no' => true, 'roros.no' => true, - 'røros.no' => true, + 'xn--rros-gra.no' => true, 'rost.no' => true, - 'røst.no' => true, + 'xn--rst-0na.no' => true, 'royken.no' => true, - 'røyken.no' => true, + 'xn--ryken-vua.no' => true, 'royrvik.no' => true, - 'røyrvik.no' => true, + 'xn--ryrvik-bya.no' => true, 'ruovat.no' => true, 'rygge.no' => true, 'salangen.no' => true, 'salat.no' => true, - 'sálat.no' => true, - 'sálát.no' => true, + 'xn--slat-5na.no' => true, + 'xn--slt-elab.no' => true, 'saltdal.no' => true, 'samnanger.no' => true, 'sandefjord.no' => true, 'sandnes.no' => true, 'sandoy.no' => true, - 'sandøy.no' => true, + 'xn--sandy-yua.no' => true, 'sarpsborg.no' => true, 'sauda.no' => true, 'sauherad.no' => true, @@ -4380,62 +4380,62 @@ 'siljan.no' => true, 'sirdal.no' => true, 'skanit.no' => true, - 'skánit.no' => true, + 'xn--sknit-yqa.no' => true, 'skanland.no' => true, - 'skånland.no' => true, + 'xn--sknland-fxa.no' => true, 'skaun.no' => true, 'skedsmo.no' => true, 'ski.no' => true, 'skien.no' => true, 'skierva.no' => true, - 'skiervá.no' => true, + 'xn--skierv-uta.no' => true, 'skiptvet.no' => true, 'skjak.no' => true, - 'skjåk.no' => true, + 'xn--skjk-soa.no' => true, 'skjervoy.no' => true, - 'skjervøy.no' => true, + 'xn--skjervy-v1a.no' => true, 'skodje.no' => true, 'smola.no' => true, - 'smøla.no' => true, + 'xn--smla-hra.no' => true, 'snaase.no' => true, - 'snåase.no' => true, + 'xn--snase-nra.no' => true, 'snasa.no' => true, - 'snåsa.no' => true, + 'xn--snsa-roa.no' => true, 'snillfjord.no' => true, 'snoasa.no' => true, 'sogndal.no' => true, 'sogne.no' => true, - 'søgne.no' => true, + 'xn--sgne-gra.no' => true, 'sokndal.no' => true, 'sola.no' => true, 'solund.no' => true, 'somna.no' => true, - 'sømna.no' => true, + 'xn--smna-gra.no' => true, 'sondre-land.no' => true, - 'søndre-land.no' => true, + 'xn--sndre-land-0cb.no' => true, 'songdalen.no' => true, 'sor-aurdal.no' => true, - 'sør-aurdal.no' => true, + 'xn--sr-aurdal-l8a.no' => true, 'sor-fron.no' => true, - 'sør-fron.no' => true, + 'xn--sr-fron-q1a.no' => true, 'sor-odal.no' => true, - 'sør-odal.no' => true, + 'xn--sr-odal-q1a.no' => true, 'sor-varanger.no' => true, - 'sør-varanger.no' => true, + 'xn--sr-varanger-ggb.no' => true, 'sorfold.no' => true, - 'sørfold.no' => true, + 'xn--srfold-bya.no' => true, 'sorreisa.no' => true, - 'sørreisa.no' => true, + 'xn--srreisa-q1a.no' => true, 'sortland.no' => true, 'sorum.no' => true, - 'sørum.no' => true, + 'xn--srum-gra.no' => true, 'spydeberg.no' => true, 'stange.no' => true, 'stavanger.no' => true, 'steigen.no' => true, 'steinkjer.no' => true, 'stjordal.no' => true, - 'stjørdal.no' => true, + 'xn--stjrdal-s1a.no' => true, 'stokke.no' => true, 'stor-elvdal.no' => true, 'stord.no' => true, @@ -4454,28 +4454,28 @@ 'sykkylven.no' => true, 'tana.no' => true, 'bo.telemark.no' => true, - 'bø.telemark.no' => true, + 'xn--b-5ga.telemark.no' => true, 'time.no' => true, 'tingvoll.no' => true, 'tinn.no' => true, 'tjeldsund.no' => true, 'tjome.no' => true, - 'tjøme.no' => true, + 'xn--tjme-hra.no' => true, 'tokke.no' => true, 'tolga.no' => true, 'tonsberg.no' => true, - 'tønsberg.no' => true, + 'xn--tnsberg-q1a.no' => true, 'torsken.no' => true, - 'træna.no' => true, + 'xn--trna-woa.no' => true, 'trana.no' => true, 'tranoy.no' => true, - 'tranøy.no' => true, + 'xn--trany-yua.no' => true, 'troandin.no' => true, 'trogstad.no' => true, - 'trøgstad.no' => true, + 'xn--trgstad-r1a.no' => true, 'tromsa.no' => true, 'tromso.no' => true, - 'tromsø.no' => true, + 'xn--troms-zua.no' => true, 'trondheim.no' => true, 'trysil.no' => true, 'tvedestrand.no' => true, @@ -4483,37 +4483,37 @@ 'tynset.no' => true, 'tysfjord.no' => true, 'tysnes.no' => true, - 'tysvær.no' => true, + 'xn--tysvr-vra.no' => true, 'tysvar.no' => true, 'ullensaker.no' => true, 'ullensvang.no' => true, 'ulvik.no' => true, 'unjarga.no' => true, - 'unjárga.no' => true, + 'xn--unjrga-rta.no' => true, 'utsira.no' => true, 'vaapste.no' => true, 'vadso.no' => true, - 'vadsø.no' => true, - 'værøy.no' => true, + 'xn--vads-jra.no' => true, + 'xn--vry-yla5g.no' => true, 'vaga.no' => true, - 'vågå.no' => true, + 'xn--vg-yiab.no' => true, 'vagan.no' => true, - 'vågan.no' => true, + 'xn--vgan-qoa.no' => true, 'vagsoy.no' => true, - 'vågsøy.no' => true, + 'xn--vgsy-qoa0j.no' => true, 'vaksdal.no' => true, 'valle.no' => true, 'vang.no' => true, 'vanylven.no' => true, 'vardo.no' => true, - 'vardø.no' => true, + 'xn--vard-jra.no' => true, 'varggat.no' => true, - 'várggát.no' => true, + 'xn--vrggt-xqad.no' => true, 'varoy.no' => true, 'vefsn.no' => true, 'vega.no' => true, 'vegarshei.no' => true, - 'vegårshei.no' => true, + 'xn--vegrshei-c0a.no' => true, 'vennesla.no' => true, 'verdal.no' => true, 'verran.no' => true, @@ -4523,7 +4523,7 @@ 'vestre-slidre.no' => true, 'vestre-toten.no' => true, 'vestvagoy.no' => true, - 'vestvågøy.no' => true, + 'xn--vestvgy-ixa6o.no' => true, 'vevelstad.no' => true, 'vik.no' => true, 'vikna.no' => true, @@ -4551,7 +4551,7 @@ 'iwi.nz' => true, 'kiwi.nz' => true, 'maori.nz' => true, - 'māori.nz' => true, + 'xn--mori-qsa.nz' => true, 'mil.nz' => true, 'net.nz' => true, 'org.nz' => true, @@ -5674,93 +5674,93 @@ 'net.ws' => true, 'org.ws' => true, 'yt' => true, - 'امارات' => true, - 'հայ' => true, - 'বাংলা' => true, - 'бг' => true, - 'البحرين' => true, - 'бел' => true, - '中国' => true, - '中國' => true, - 'الجزائر' => true, - 'مصر' => true, - 'ею' => true, - 'ευ' => true, - 'موريتانيا' => true, - 'გე' => true, - 'ελ' => true, - '香港' => true, - '個人.香港' => true, - '公司.香港' => true, - '政府.香港' => true, - '教育.香港' => true, - '組織.香港' => true, - '網絡.香港' => true, - 'ಭಾರತ' => true, - 'ଭାରତ' => true, - 'ভাৰত' => true, - 'भारतम्' => true, - 'भारोत' => true, - 'ڀارت' => true, - 'ഭാരതം' => true, - 'भारत' => true, - 'بارت' => true, - 'بھارت' => true, - 'భారత్' => true, - 'ભારત' => true, - 'ਭਾਰਤ' => true, - 'ভারত' => true, - 'இந்தியா' => true, - 'ایران' => true, - 'ايران' => true, - 'عراق' => true, - 'الاردن' => true, - '한국' => true, - 'қаз' => true, - 'ລາວ' => true, - 'ලංකා' => true, - 'இலங்கை' => true, - 'المغرب' => true, - 'мкд' => true, - 'мон' => true, - '澳門' => true, - '澳门' => true, - 'مليسيا' => true, - 'عمان' => true, - 'پاکستان' => true, - 'پاكستان' => true, - 'فلسطين' => true, - 'срб' => true, - 'ак.срб' => true, - 'обр.срб' => true, - 'од.срб' => true, - 'орг.срб' => true, - 'пр.срб' => true, - 'упр.срб' => true, - 'рф' => true, - 'قطر' => true, - 'السعودية' => true, - 'السعودیة' => true, - 'السعودیۃ' => true, - 'السعوديه' => true, - 'سودان' => true, - '新加坡' => true, - 'சிங்கப்பூர்' => true, - 'سورية' => true, - 'سوريا' => true, - 'ไทย' => true, - 'ทหาร.ไทย' => true, - 'ธุรกิจ.ไทย' => true, - 'เน็ต.ไทย' => true, - 'รัฐบาล.ไทย' => true, - 'ศึกษา.ไทย' => true, - 'องค์กร.ไทย' => true, - 'تونس' => true, - '台灣' => true, - '台湾' => true, - '臺灣' => true, - 'укр' => true, - 'اليمن' => true, + 'xn--mgbaam7a8h' => true, + 'xn--y9a3aq' => true, + 'xn--54b7fta0cc' => true, + 'xn--90ae' => true, + 'xn--mgbcpq6gpa1a' => true, + 'xn--90ais' => true, + 'xn--fiqs8s' => true, + 'xn--fiqz9s' => true, + 'xn--lgbbat1ad8j' => true, + 'xn--wgbh1c' => true, + 'xn--e1a4c' => true, + 'xn--qxa6a' => true, + 'xn--mgbah1a3hjkrd' => true, + 'xn--node' => true, + 'xn--qxam' => true, + 'xn--j6w193g' => true, + 'xn--gmqw5a.xn--j6w193g' => true, + 'xn--55qx5d.xn--j6w193g' => true, + 'xn--mxtq1m.xn--j6w193g' => true, + 'xn--wcvs22d.xn--j6w193g' => true, + 'xn--uc0atv.xn--j6w193g' => true, + 'xn--od0alg.xn--j6w193g' => true, + 'xn--2scrj9c' => true, + 'xn--3hcrj9c' => true, + 'xn--45br5cyl' => true, + 'xn--h2breg3eve' => true, + 'xn--h2brj9c8c' => true, + 'xn--mgbgu82a' => true, + 'xn--rvc1e0am3e' => true, + 'xn--h2brj9c' => true, + 'xn--mgbbh1a' => true, + 'xn--mgbbh1a71e' => true, + 'xn--fpcrj9c3d' => true, + 'xn--gecrj9c' => true, + 'xn--s9brj9c' => true, + 'xn--45brj9c' => true, + 'xn--xkc2dl3a5ee0h' => true, + 'xn--mgba3a4f16a' => true, + 'xn--mgba3a4fra' => true, + 'xn--mgbtx2b' => true, + 'xn--mgbayh7gpa' => true, + 'xn--3e0b707e' => true, + 'xn--80ao21a' => true, + 'xn--q7ce6a' => true, + 'xn--fzc2c9e2c' => true, + 'xn--xkc2al3hye2a' => true, + 'xn--mgbc0a9azcg' => true, + 'xn--d1alf' => true, + 'xn--l1acc' => true, + 'xn--mix891f' => true, + 'xn--mix082f' => true, + 'xn--mgbx4cd0ab' => true, + 'xn--mgb9awbf' => true, + 'xn--mgbai9azgqp6j' => true, + 'xn--mgbai9a5eva00b' => true, + 'xn--ygbi2ammx' => true, + 'xn--90a3ac' => true, + 'xn--80au.xn--90a3ac' => true, + 'xn--90azh.xn--90a3ac' => true, + 'xn--d1at.xn--90a3ac' => true, + 'xn--c1avg.xn--90a3ac' => true, + 'xn--o1ac.xn--90a3ac' => true, + 'xn--o1ach.xn--90a3ac' => true, + 'xn--p1ai' => true, + 'xn--wgbl6a' => true, + 'xn--mgberp4a5d4ar' => true, + 'xn--mgberp4a5d4a87g' => true, + 'xn--mgbqly7c0a67fbc' => true, + 'xn--mgbqly7cvafr' => true, + 'xn--mgbpl2fh' => true, + 'xn--yfro4i67o' => true, + 'xn--clchc0ea0b2g2a9gcd' => true, + 'xn--ogbpf8fl' => true, + 'xn--mgbtf8fl' => true, + 'xn--o3cw4h' => true, + 'xn--o3cyx2a.xn--o3cw4h' => true, + 'xn--12co0c3b4eva.xn--o3cw4h' => true, + 'xn--m3ch0j3a.xn--o3cw4h' => true, + 'xn--h3cuzk1di.xn--o3cw4h' => true, + 'xn--12c1fe0br.xn--o3cw4h' => true, + 'xn--12cfi8ixb8l.xn--o3cw4h' => true, + 'xn--pgbs0dh' => true, + 'xn--kpry57d' => true, + 'xn--kprw13d' => true, + 'xn--nnx388a' => true, + 'xn--j1amh' => true, + 'xn--mgb2ddes' => true, 'xxx' => true, 'ye' => true, 'com.ye' => true, @@ -6199,7 +6199,6 @@ 'gold' => true, 'goldpoint' => true, 'golf' => true, - 'goo' => true, 'goodyear' => true, 'goog' => true, 'google' => true, @@ -6807,96 +6806,96 @@ 'xerox' => true, 'xihuan' => true, 'xin' => true, - 'कॉम' => true, - 'セール' => true, - '佛山' => true, - '慈善' => true, - '集团' => true, - '在线' => true, - '点看' => true, - 'คอม' => true, - '八卦' => true, - 'موقع' => true, - '公益' => true, - '公司' => true, - '香格里拉' => true, - '网站' => true, - '移动' => true, - '我爱你' => true, - 'москва' => true, - 'католик' => true, - 'онлайн' => true, - 'сайт' => true, - '联通' => true, - 'קום' => true, - '时尚' => true, - '微博' => true, - '淡马锡' => true, - 'ファッション' => true, - 'орг' => true, - 'नेट' => true, - 'ストア' => true, - 'アマゾン' => true, - '삼성' => true, - '商标' => true, - '商店' => true, - '商城' => true, - 'дети' => true, - 'ポイント' => true, - '新闻' => true, - '家電' => true, - 'كوم' => true, - '中文网' => true, - '中信' => true, - '娱乐' => true, - '谷歌' => true, - '電訊盈科' => true, - '购物' => true, - 'クラウド' => true, - '通販' => true, - '网店' => true, - 'संगठन' => true, - '餐厅' => true, - '网络' => true, - 'ком' => true, - '亚马逊' => true, - '食品' => true, - '飞利浦' => true, - '手机' => true, - 'ارامكو' => true, - 'العليان' => true, - 'بازار' => true, - 'ابوظبي' => true, - 'كاثوليك' => true, - 'همراه' => true, - '닷컴' => true, - '政府' => true, - 'شبكة' => true, - 'بيتك' => true, - 'عرب' => true, - '机构' => true, - '组织机构' => true, - '健康' => true, - '招聘' => true, - 'рус' => true, - '大拿' => true, - 'みんな' => true, - 'グーグル' => true, - '世界' => true, - '書籍' => true, - '网址' => true, - '닷넷' => true, - 'コム' => true, - '天主教' => true, - '游戏' => true, - 'vermögensberater' => true, - 'vermögensberatung' => true, - '企业' => true, - '信息' => true, - '嘉里大酒店' => true, - '嘉里' => true, - '广东' => true, - '政务' => true, + 'xn--11b4c3d' => true, + 'xn--1ck2e1b' => true, + 'xn--1qqw23a' => true, + 'xn--30rr7y' => true, + 'xn--3bst00m' => true, + 'xn--3ds443g' => true, + 'xn--3pxu8k' => true, + 'xn--42c2d9a' => true, + 'xn--45q11c' => true, + 'xn--4gbrim' => true, + 'xn--55qw42g' => true, + 'xn--55qx5d' => true, + 'xn--5su34j936bgsg' => true, + 'xn--5tzm5g' => true, + 'xn--6frz82g' => true, + 'xn--6qq986b3xl' => true, + 'xn--80adxhks' => true, + 'xn--80aqecdr1a' => true, + 'xn--80asehdb' => true, + 'xn--80aswg' => true, + 'xn--8y0a063a' => true, + 'xn--9dbq2a' => true, + 'xn--9et52u' => true, + 'xn--9krt00a' => true, + 'xn--b4w605ferd' => true, + 'xn--bck1b9a5dre4c' => true, + 'xn--c1avg' => true, + 'xn--c2br7g' => true, + 'xn--cck2b3b' => true, + 'xn--cckwcxetd' => true, + 'xn--cg4bki' => true, + 'xn--czr694b' => true, + 'xn--czrs0t' => true, + 'xn--czru2d' => true, + 'xn--d1acj3b' => true, + 'xn--eckvdtc9d' => true, + 'xn--efvy88h' => true, + 'xn--fct429k' => true, + 'xn--fhbei' => true, + 'xn--fiq228c5hs' => true, + 'xn--fiq64b' => true, + 'xn--fjq720a' => true, + 'xn--flw351e' => true, + 'xn--fzys8d69uvgm' => true, + 'xn--g2xx48c' => true, + 'xn--gckr3f0f' => true, + 'xn--gk3at1e' => true, + 'xn--hxt814e' => true, + 'xn--i1b6b1a6a2e' => true, + 'xn--imr513n' => true, + 'xn--io0a7i' => true, + 'xn--j1aef' => true, + 'xn--jlq480n2rg' => true, + 'xn--jvr189m' => true, + 'xn--kcrx77d1x4a' => true, + 'xn--kput3i' => true, + 'xn--mgba3a3ejt' => true, + 'xn--mgba7c0bbn0a' => true, + 'xn--mgbab2bd' => true, + 'xn--mgbca7dzdo' => true, + 'xn--mgbi4ecexp' => true, + 'xn--mgbt3dhd' => true, + 'xn--mk1bu44c' => true, + 'xn--mxtq1m' => true, + 'xn--ngbc5azd' => true, + 'xn--ngbe9e0a' => true, + 'xn--ngbrx' => true, + 'xn--nqv7f' => true, + 'xn--nqv7fs00ema' => true, + 'xn--nyqy26a' => true, + 'xn--otu796d' => true, + 'xn--p1acf' => true, + 'xn--pssy2u' => true, + 'xn--q9jyb4c' => true, + 'xn--qcka1pmc' => true, + 'xn--rhqv96g' => true, + 'xn--rovu88b' => true, + 'xn--ses554g' => true, + 'xn--t60b56a' => true, + 'xn--tckwe' => true, + 'xn--tiq49xqyj' => true, + 'xn--unup4y' => true, + 'xn--vermgensberater-ctb' => true, + 'xn--vermgensberatung-pwb' => true, + 'xn--vhquv' => true, + 'xn--vuq861b' => true, + 'xn--w4r85el8fhu5dnra' => true, + 'xn--w4rs40l' => true, + 'xn--xhq521b' => true, + 'xn--zfr164b' => true, 'xyz' => true, 'yachts' => true, 'yahoo' => true, @@ -8691,11 +8690,11 @@ 'grafana-dev.net' => true, 'grayjayleagues.com' => true, 'grebedoc.dev' => true, - 'günstigbestellen.de' => true, - 'günstigliefern.de' => true, + 'xn--gnstigbestellen-zvb.de' => true, + 'xn--gnstigliefern-wob.de' => true, 'gv.uy' => true, 'hackclub.app' => true, - 'häkkinen.fi' => true, + 'xn--hkkinen-5wa.fi' => true, 'hashbang.sh' => true, 'hasura.app' => true, 'hasura-app.io' => true, @@ -9417,16 +9416,16 @@ 'rub.de' => true, 'ruhr-uni-bochum.de' => true, 'io.noc.ruhr-uni-bochum.de' => true, - 'биз.рус' => true, - 'ком.рус' => true, - 'крым.рус' => true, - 'мир.рус' => true, - 'мск.рус' => true, - 'орг.рус' => true, - 'самара.рус' => true, - 'сочи.рус' => true, - 'спб.рус' => true, - 'я.рус' => true, + 'xn--90amc.xn--p1acf' => true, + 'xn--j1aef.xn--p1acf' => true, + 'xn--j1ael8b.xn--p1acf' => true, + 'xn--h1ahn.xn--p1acf' => true, + 'xn--j1adp.xn--p1acf' => true, + 'xn--c1avg.xn--p1acf' => true, + 'xn--80aaa0cvac.xn--p1acf' => true, + 'xn--h1aliz.xn--p1acf' => true, + 'xn--90a1af.xn--p1acf' => true, + 'xn--41a.xn--p1acf' => true, 'ras.ru' => true, 'nyat.app' => true, '180r.com' => true, diff --git a/data/psl.meta.json b/data/psl.meta.json index c1a1510..04a542f 100644 --- a/data/psl.meta.json +++ b/data/psl.meta.json @@ -1,5 +1,5 @@ { - "updated": "2026-02-07T09:24:29+00:00", - "etag": "\"408f4d04d52ae35dc914f139a81bdbbc\"", - "last_modified": "Fri, 06 Feb 2026 07:37:00 GMT" + "updated": "2026-02-08T13:14:03+00:00", + "etag": "\"fd24f5772abcadcaa6e8511ac27235d9\"", + "last_modified": "Sat, 07 Feb 2026 17:37:09 GMT" } \ No newline at end of file diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..ae70297 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,226 @@ +# RegDom Architecture + +This document describes the internal architecture of the RegDom library, including +the PSL cache format, rule matching logic, domain normalization, and the update pipeline. + +## Project Structure + +```text +src/ + RegisteredDomain.php Main API class + PublicSuffixList.php PSL cache loader and query engine + Exception/ + PslCacheNotFoundException.php Thrown when no valid cache is found +bin/ + update-psl.php Downloads PSL from publicsuffix.org, builds cache + reloadpsl Convenience wrapper for update-psl.php +data/ + psl.cache.php Bundled PSL cache (PHP array, ~200 KB) + psl.meta.json Download metadata (ETag, Last-Modified) +``` + +## Class Overview + +### `RegisteredDomain` + +The main API class. Provides two public methods: + +- **`getRegisteredDomain(string $host, bool $utf8 = true): ?string`** + Extracts the registrable domain from a hostname or URL. + Accepts bare hosts (`www.example.co.uk`), full URLs (`https://example.com/path`), + hosts with ports (`example.com:8080`), and IDN labels (`www.munchen.de`). + Returns `null` for IP addresses, localhost, public suffixes, and unrecognized TLDs. + +- **`domainMatches(string $host, string $domain): bool`** *(static)* + Validates whether a cookie domain is appropriate for a given host, following + RFC 6265 rules and PSL conventions. Rejects public suffixes, IP addresses, + localhost, and cross-domain mismatches. + +### `PublicSuffixList` + +Manages and queries the PSL data loaded from a pre-generated cache. Provides: + +- **`isPublicSuffix(string $domain): bool`** -- Checks if a domain is a public suffix. +- **`getPublicSuffix(string $domain): ?string`** -- Returns the public suffix portion + of a domain (e.g. `www.example.co.uk` returns `co.uk`). +- **`isException(string $domain): bool`** -- Checks if a domain is a PSL exception + entry (e.g. `www.ck`, `city.kawasaki.jp`). +- **`getMetadata(): array`** -- Returns cache age, rule counts, and a staleness flag. + +The `$rules` property is **static** -- shared across all instances within a single +PHP request. This avoids reloading the ~200 KB cache file on every instantiation. + +## PSL Cache Format + +The cache file (`data/psl.cache.php`) is a plain PHP `return` statement that yields +an associative array with three keys: + +```php +return [ + 'NORMAL' => ['com' => true, 'co.uk' => true, ...], + 'WILDCARD' => ['ck' => true, 'kawasaki.jp' => true, ...], + 'EXCEPTION' => ['www.ck' => true, 'city.kawasaki.jp' => true, ...], +]; +``` + +| Key | Meaning | Example PSL Entry | Stored Key | +|-------------|--------------------------------------|---------------------|----------------| +| `NORMAL` | Exact public suffix | `com` | `com` | +| `WILDCARD` | Wildcard rule (all children are PS) | `*.ck` | `ck` | +| `EXCEPTION` | Exception to a wildcard rule | `!www.ck` | `www.ck` | + +All keys are stored in ASCII/punycode form (e.g. `xn--55qx5d.cn` instead of +Unicode). This matches the normalization applied at query time, so lookups +are simple `isset()` hash checks -- O(1). + +### Dual Cache Paths + +The library checks two locations, in order: + +1. **Runtime cache** (preferred): `XOOPS_VAR_PATH/cache/regdom/psl.cache.php` + Used in a full XOOPS installation where the constant is defined. +2. **Bundled fallback**: `data/psl.cache.php` + Shipped with the package for standalone or non-XOOPS usage. + +Both files undergo validation before use: +- File size must be between 100 KB and 10 MB (rejects corrupt/empty files) +- Must return an array with all three keys (`NORMAL`, `WILDCARD`, `EXCEPTION`) +- Each key must be an array +- Total rule count must be between 1,000 and 100,000 + +## Rule Matching Priority + +The PSL specification defines a strict priority order: + +1. **Exception rules** -- checked first; if a domain matches an exception entry + (e.g. `city.kawasaki.jp`), it is *not* a public suffix, even though a wildcard + rule (`*.kawasaki.jp`) would otherwise match. +2. **Normal rules** -- exact suffix match (e.g. `com`, `co.uk`). +3. **Wildcard rules** -- if the parent domain matches a wildcard key + (e.g. `anything.ck` matches wildcard `ck`). + +### Exception Rule Semantics + +PSL exception rules are prefixed with `!` in the raw data (e.g. `!city.kawasaki.jp`). +They mean: "this specific label is *not* part of the public suffix, even though a +wildcard would otherwise claim it." + +For `getPublicSuffix()`, the public suffix of an exception domain is the exception +entry minus its leftmost label: + +```text +PSL entry: !city.kawasaki.jp +Exception key: city.kawasaki.jp +Public suffix: kawasaki.jp (one label removed) +``` + +This means `city.kawasaki.jp` is itself a registrable domain, not a public suffix. + +## Domain Normalization + +Both classes normalize input before processing: + +### `RegisteredDomain::normalizeHost()` + +Handles the full range of inputs: +- Strips scheme and path from URLs via `parse_url()` +- Lowercases via `mb_strtolower()` (UTF-8 aware) +- Strips trailing port numbers (`:8080`) +- Strips trailing dots (`example.com.`) +- Handles bracketed IPv6 literals (`[::1]`, `[::1]:443`) +- Detects unbracketed IPv6 (from `parse_url()`) and skips port stripping to + avoid corrupting the address + +### `PublicSuffixList::normalizeDomain()` + +Simpler normalization for PSL queries: +- Lowercases and trims +- Strips leading/trailing dots +- Converts Unicode labels to punycode via `idn_to_ascii()` (when `ext-intl` is available) + +## IDN / Punycode Handling + +Internationalized domain names (IDN) are handled at two levels: + +1. **Cache generation** (`bin/update-psl.php`): All PSL rules are normalized to + ASCII/punycode before storage. This ensures that Unicode rules like + `xn--55qx5d.cn` are reachable regardless of input form. + +2. **Query time**: Input domains are converted to punycode via `idn_to_ascii()` + before lookup. Results can be converted back to UTF-8 via `idn_to_utf8()` for + display (controlled by the `$utf8` parameter on `getRegisteredDomain()`). + +Both conversions require `ext-intl`. When the extension is not available: +- Punycode input (e.g. `xn--55qx5d.cn`) still works correctly +- Unicode input (e.g. `公司.cn`) cannot be converted and may not match + +## PSL Update Pipeline + +The `bin/update-psl.php` script downloads the latest PSL from +[publicsuffix.org](https://publicsuffix.org/list/public_suffix_list.dat), +parses the rules, and writes the cache atomically. + +```mermaid +sequenceDiagram + participant CLI as PSL Update Script + participant Intl as ext-intl Extension + participant Parser as Rule Parser + participant Validator as Validator + participant Writer as Cache Writer + + CLI->>Intl: Check if ext-intl available + alt ext-intl available + Intl-->>CLI: Extension loaded + CLI->>Parser: Parse PSL rules with normalization + Parser->>Intl: Convert Unicode labels to punycode (xn--...) + Intl-->>Parser: Normalized ASCII/punycode form + Parser-->>CLI: Normalized rules (EXCEPTION, WILDCARD, NORMAL) + else ext-intl unavailable + Intl-->>CLI: Extension not found + CLI->>CLI: Emit warning + CLI->>Parser: Parse PSL rules (Unicode retained) + Parser-->>CLI: Unicode rules + end + + CLI->>Validator: Validate total rule count >= 1000 + alt Rules count valid + Validator-->>CLI: Validation passed + CLI->>Writer: Write normalized rules to cache + Writer->>Writer: Clear leftover temp files on error + Writer-->>CLI: Cache written successfully + else Rules count too low + Validator-->>CLI: Abort update (suspicious count) + end +``` + +### Update Details + +- **HTTP conditional downloads**: Uses `ETag` and `Last-Modified` headers from + previous downloads (stored in `data/psl.meta.json`) to avoid re-downloading + unchanged data (HTTP 304 Not Modified). + +- **Rule parsing**: Each line of the raw PSL is classified as: + - `!prefix` -> `EXCEPTION` (stored without the `!`) + - `*.suffix` -> `WILDCARD` (stored without the `*.`) + - Everything else -> `NORMAL` + +- **Atomic writes**: Cache content is written to a temp file first, then renamed + into place via `rename()`. This prevents partial reads if the cache is loaded + concurrently. After writing, `opcache_invalidate()` is called to ensure PHP + picks up the new file. + +- **Dual targets**: The script writes to both the bundled path (`data/psl.cache.php`) + and, if available, the runtime path (`XOOPS_VAR_PATH/cache/regdom/psl.cache.php`). + +## XOOPS Integration + +When used within a XOOPS installation, three constants affect behavior: + +| Constant | Purpose | +|--------------------------------|-------------------------------------------------| +| `XOOPS_VAR_PATH` | Base path for the runtime PSL cache directory | +| `XOOPS_COOKIE_DOMAIN_USE_PSL` | Set to `false` to disable PSL validation in `domainMatches()` | +| `XOOPS_SKIP_PSL_UPDATE` | Environment variable; skips auto-update during Composer hooks | + +When none of these constants are defined, the library operates in standalone mode +using only the bundled cache file. diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..08fcd82 --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,20 @@ + + + XOOPS Coding Standards (PSR-12) + + src + + + + + + + 0 + + + + */_archive/* + */docs/* + */tests/* + */vendor/* + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 2e07fb9..0443f4f 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,7 +1,14 @@ - (PHPUnit 10.1+) for code-coverage source filtering. + PHPUnit 9.6 (PHP 7.4/8.0) ignores this element harmlessly; coverage is only + collected on PHP 8.3 (PHPUnit 10+) in CI, so the filter is always effective + when it matters. + + The xsi:noNamespaceSchemaLocation attribute is intentionally omitted to avoid + XSD validation warnings on PHPUnit 9, which does not recognise . +--> + diff --git a/src/PublicSuffixList.php b/src/PublicSuffixList.php index 17f538e..d1e8416 100644 --- a/src/PublicSuffixList.php +++ b/src/PublicSuffixList.php @@ -130,7 +130,9 @@ public function getPublicSuffix(string $domain): ?string for ($i = 0; $i < $n; $i++) { $testSuffix = implode('.', array_slice($parts, $i)); if (isset(self::$rules['EXCEPTION'][$testSuffix])) { - return $i > 0 ? implode('.', array_slice($parts, $i - 1)) : null; + // PSL exception: the public suffix is the exception rule minus its leftmost label. + // e.g. exception "!city.kawasaki.jp" → public suffix is "kawasaki.jp" + return ($i + 1 < $n) ? implode('.', array_slice($parts, $i + 1)) : null; } if (isset(self::$rules['NORMAL'][$testSuffix])) { return $testSuffix; diff --git a/src/RegisteredDomain.php b/src/RegisteredDomain.php index caf2c1a..1b27d6d 100644 --- a/src/RegisteredDomain.php +++ b/src/RegisteredDomain.php @@ -119,9 +119,21 @@ private static function normalizeHost(string $input): string } $host = trim(mb_strtolower($host, 'UTF-8')); if ($host !== '' && $host[0] === '[') { - $host = trim($host, '[]'); + // Bracketed IPv6 literal, e.g. [::1] or [::1]:443 + // Extract the address between brackets, discarding any trailing port. + if (preg_match('/^\[([^\]]+)]/', $host, $m)) { + $host = $m[1]; + } else { + // Malformed bracket expression — strip brackets as fallback + $host = trim($host, '[]'); + } + } elseif ($host !== '' && filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + // Unbracketed IPv6 — parse_url() returns IPv6 without brackets. + // Skip port stripping to avoid corrupting the address. + } else { + // Non-IPv6: strip trailing :port (e.g. example.com:8080 → example.com) + $host = preg_replace('/:\d+$/', '', $host) ?? $host; } - $host = preg_replace('/:\d+$/', '', $host) ?? $host; return rtrim($host, '.'); } diff --git a/tests/integration/IntegrationTest.php b/tests/integration/IntegrationTest.php index 24261a0..268a3a1 100644 --- a/tests/integration/IntegrationTest.php +++ b/tests/integration/IntegrationTest.php @@ -70,6 +70,11 @@ public static function realWorldDataProvider(): array 'Unicode' => ['www.münchen.de', 'münchen.de'], 'Punycode' => ['www.xn--mnchen-3ya.de', 'münchen.de'], 'Public Suffix' => ['co.uk', null], + 'IDN PS Unicode' => ['test.公司.cn', 'test.公司.cn'], // 公司.cn is a PS + 'IDN PS punycode' => ['test.xn--55qx5d.cn', 'test.公司.cn'], // punycode PS + 'IDN PS itself' => ['公司.cn', null], // PS alone → null + 'Japan exception sub' => ['sub.city.kawasaki.jp', 'city.kawasaki.jp'], + 'CK exception sub' => ['sub.www.ck', 'www.ck'], ]; } } diff --git a/tests/integration/PublicSuffixListIntegrationTest.php b/tests/integration/PublicSuffixListIntegrationTest.php new file mode 100644 index 0000000..443cb4c --- /dev/null +++ b/tests/integration/PublicSuffixListIntegrationTest.php @@ -0,0 +1,124 @@ +markTestSkipped('Bundled PSL cache not found. Run: composer update-psl'); + } + $this->psl = new PublicSuffixList(); + } + + protected function tearDown(): void + { + // Reset static PSL cache to prevent cross-test leakage + $ref = new ReflectionProperty(PublicSuffixList::class, 'rules'); + $ref->setAccessible(true); + $ref->setValue(null, null); + } + + public function testIsPublicSuffix(): void + { + $this->assertTrue($this->psl->isPublicSuffix('com')); + $this->assertTrue($this->psl->isPublicSuffix('co.uk')); + $this->assertFalse($this->psl->isPublicSuffix('example.com')); + $this->assertFalse($this->psl->isPublicSuffix('parliament.uk')); // Regular domain, not a public suffix + $this->assertTrue($this->psl->isPublicSuffix('anything.ck')); // Wildcard rule + } + + public function testGetPublicSuffix(): void + { + $this->assertSame('com', $this->psl->getPublicSuffix('example.com')); + $this->assertSame('co.uk', $this->psl->getPublicSuffix('www.example.co.uk')); + $this->assertSame('uk', $this->psl->getPublicSuffix('example.parliament.uk')); + $this->assertSame('something.ck', $this->psl->getPublicSuffix('sub.something.ck')); + $this->assertSame('com', $this->psl->getPublicSuffix('com')); + } + + public function testGetPublicSuffixWithExceptionRules(): void + { + // PSL exception: !city.kawasaki.jp -> public suffix is kawasaki.jp + $this->assertSame('kawasaki.jp', $this->psl->getPublicSuffix('sub.city.kawasaki.jp')); + $this->assertSame('kawasaki.jp', $this->psl->getPublicSuffix('city.kawasaki.jp')); + + // PSL exception: !www.ck -> public suffix is ck + $this->assertSame('ck', $this->psl->getPublicSuffix('sub.www.ck')); + $this->assertSame('ck', $this->psl->getPublicSuffix('www.ck')); + } + + public function testGetMetadata(): void + { + $metadata = $this->psl->getMetadata(); + $this->assertIsArray($metadata); + $this->assertArrayHasKey('active_cache', $metadata); + $this->assertArrayHasKey('last_updated', $metadata); + $this->assertArrayHasKey('days_old', $metadata); + $this->assertArrayHasKey('rule_counts', $metadata); + $this->assertArrayHasKey('needs_update', $metadata); + $this->assertGreaterThan(1000, $metadata['rule_counts']['normal']); + $this->assertGreaterThan(0, $metadata['rule_counts']['wildcard']); + $this->assertGreaterThan(0, $metadata['rule_counts']['exception']); + } + + public function testIsException(): void + { + $this->assertTrue($this->psl->isException('www.ck')); + $this->assertTrue($this->psl->isException('city.kawasaki.jp')); + $this->assertFalse($this->psl->isException('com')); + $this->assertFalse($this->psl->isException('example.com')); + $this->assertFalse($this->psl->isException('')); + } + + public function testIsPublicSuffixWithPunycodeDomains(): void + { + // Punycode form works regardless of ext-intl + $this->assertTrue($this->psl->isPublicSuffix('xn--55qx5d.cn')); // 公司.cn in punycode + } + + /** + * @requires extension intl + */ + public function testIsPublicSuffixWithUnicodeIdnDomains(): void + { + // Unicode input requires ext-intl for idn_to_ascii() conversion + $this->assertTrue($this->psl->isPublicSuffix('公司.cn')); // xn--55qx5d.cn + $this->assertFalse($this->psl->isPublicSuffix('test.公司.cn')); // not a PS itself + } + + public function testGetPublicSuffixWithPunycodeDomains(): void + { + // Punycode form works regardless of ext-intl + $this->assertSame('xn--55qx5d.cn', $this->psl->getPublicSuffix('test.xn--55qx5d.cn')); + } + + /** + * @requires extension intl + */ + public function testGetPublicSuffixWithUnicodeIdnDomains(): void + { + // Unicode input requires ext-intl for idn_to_ascii() conversion + $this->assertSame('xn--55qx5d.cn', $this->psl->getPublicSuffix('test.公司.cn')); + } + + public function testNormalizeDomainHandlesLeadingAndTrailingDots(): void + { + // Leading/trailing dots should be stripped during normalization + $this->assertTrue($this->psl->isPublicSuffix('.com.')); + $this->assertTrue($this->psl->isPublicSuffix('COM')); + } +} diff --git a/tests/unit/PublicSuffixListTest.php b/tests/unit/PublicSuffixListTest.php index ef59700..abbd4ca 100644 --- a/tests/unit/PublicSuffixListTest.php +++ b/tests/unit/PublicSuffixListTest.php @@ -4,7 +4,15 @@ use Xoops\RegDom\PublicSuffixList; use PHPUnit\Framework\TestCase; - +use ReflectionProperty; + +/** + * Unit tests for PublicSuffixList edge-case behavior. + * + * These tests validate input handling (empty strings, IP addresses) + * rather than real PSL data. For tests that verify behavior against actual PSL + * entries, see tests/integration/PublicSuffixListIntegrationTest.php. + */ class PublicSuffixListTest extends TestCase { private PublicSuffixList $psl; @@ -14,36 +22,12 @@ protected function setUp(): void $this->psl = new PublicSuffixList(); } - public function testIsPublicSuffix(): void - { - $this->assertTrue($this->psl->isPublicSuffix('com')); - $this->assertTrue($this->psl->isPublicSuffix('co.uk')); - $this->assertFalse($this->psl->isPublicSuffix('example.com')); - $this->assertFalse($this->psl->isPublicSuffix('parliament.uk')); // Regular domain, not a public suffix - $this->assertTrue($this->psl->isPublicSuffix('anything.ck')); // Wildcard rule - } - - public function testGetPublicSuffix(): void - { - $this->assertSame('com', $this->psl->getPublicSuffix('example.com')); - $this->assertSame('co.uk', $this->psl->getPublicSuffix('www.example.co.uk')); - $this->assertSame('uk', $this->psl->getPublicSuffix('example.parliament.uk')); - $this->assertSame('something.ck', $this->psl->getPublicSuffix('sub.something.ck')); - $this->assertSame('com', $this->psl->getPublicSuffix('com')); - } - - public function testGetMetadata(): void + protected function tearDown(): void { - $metadata = $this->psl->getMetadata(); - $this->assertIsArray($metadata); - $this->assertArrayHasKey('active_cache', $metadata); - $this->assertArrayHasKey('last_updated', $metadata); - $this->assertArrayHasKey('days_old', $metadata); - $this->assertArrayHasKey('rule_counts', $metadata); - $this->assertArrayHasKey('needs_update', $metadata); - $this->assertGreaterThan(1000, $metadata['rule_counts']['normal']); - $this->assertGreaterThan(0, $metadata['rule_counts']['wildcard']); - $this->assertGreaterThan(0, $metadata['rule_counts']['exception']); + // Reset static PSL cache to prevent cross-test leakage + $ref = new ReflectionProperty(PublicSuffixList::class, 'rules'); + $ref->setAccessible(true); + $ref->setValue(null, null); } public function testIsPublicSuffixWithEmptyString(): void @@ -65,20 +49,4 @@ public function testGetPublicSuffixWithIpAddress(): void { $this->assertNull($this->psl->getPublicSuffix('192.168.1.1')); } - - public function testIsException(): void - { - $this->assertTrue($this->psl->isException('www.ck')); - $this->assertTrue($this->psl->isException('city.kawasaki.jp')); - $this->assertFalse($this->psl->isException('com')); - $this->assertFalse($this->psl->isException('example.com')); - $this->assertFalse($this->psl->isException('')); - } - - public function testNormalizeDomainHandlesLeadingAndTrailingDots(): void - { - // Leading/trailing dots should be stripped during normalization - $this->assertTrue($this->psl->isPublicSuffix('.com.')); - $this->assertTrue($this->psl->isPublicSuffix('COM')); - } } diff --git a/tests/unit/RegisteredDomainTest.php b/tests/unit/RegisteredDomainTest.php index 88f2929..784fc86 100644 --- a/tests/unit/RegisteredDomainTest.php +++ b/tests/unit/RegisteredDomainTest.php @@ -37,12 +37,12 @@ public function testGetRegisteredDomain(string $host, ?string $expected): void return null; } - // Exception rules: return suffix one level up - if ($h === 'city.kawasaki.jp') { - return 'kawasaki.jp'; // So city.kawasaki.jp is registrable + // Exception rules: the public suffix is the exception minus its leftmost label + if ($h === 'city.kawasaki.jp' || $h === 'sub.city.kawasaki.jp') { + return 'kawasaki.jp'; } - if ($h === 'www.ck') { - return 'ck'; // So www.ck is registrable + if ($h === 'www.ck' || $h === 'sub.www.ck') { + return 'ck'; } // Standard suffix map @@ -100,6 +100,8 @@ public static function getRegisteredDomainProvider(): array 'unlisted tld' => ['example.example', null], 'wildcard exception' => ['www.ck', 'www.ck'], 'PSL exception rule' => ['city.kawasaki.jp', 'city.kawasaki.jp'], + 'PSL exception subdomain' => ['sub.city.kawasaki.jp', 'city.kawasaki.jp'], + 'PSL exception subdomain ck' => ['sub.www.ck', 'www.ck'], 'IDN simple' => ['食狮.com.cn', '食狮.com.cn'], 'IDN multi-level' => ['www.食狮.公司.cn', '食狮.公司.cn'], 'IDN punycode' => ['www.xn--85x722f.xn--55qx5d.cn', '食狮.公司.cn'], @@ -111,6 +113,12 @@ public static function getRegisteredDomainProvider(): array 'url with port' => ['https://example.com:8080/path', 'example.com'], 'host with port no scheme' => ['example.com:443', 'example.com'], 'IPv6 bracketed' => ['[::1]', null], + 'IPv6 bracketed with port' => ['[::1]:443', null], + 'IPv6 full form' => ['[2001:db8::1]', null], + 'IPv6 full form with port' => ['[2001:db8::1]:8080', null], + 'IPv6 URL with port' => ['http://[::1]:443/path', null], + 'IPv6 URL no port' => ['http://[::1]/path', null], + 'IPv6 URL full form' => ['https://[2001:db8::1]:8080/path', null], ]; } @@ -129,6 +137,8 @@ public static function domainMatchesProvider(): array 'leading dot ignored' => ['example.com', '.example.com', true], 'port stripped' => ['example.com:8080', 'example.com', true], 'idn match' => ['münchen.de', 'münchen.de', true], + 'IPv6 host rejected' => ['[::1]', '::1', false], + 'IPv6 host with port rejected' => ['[::1]:443', '::1', false], ]; } }