Initial Commit

This commit is contained in:
David Stone
2024-11-30 18:24:12 -07:00
commit e8f7955c1c
5432 changed files with 1397750 additions and 0 deletions

View File

@ -0,0 +1,177 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare (strict_types=1);
namespace WP_Ultimo\Dependencies\League\Uri\Idna;
use WP_Ultimo\Dependencies\League\Uri\Exceptions\ConversionFailed;
use WP_Ultimo\Dependencies\League\Uri\Exceptions\SyntaxError;
use WP_Ultimo\Dependencies\League\Uri\FeatureDetection;
use Stringable;
use function idn_to_ascii;
use function idn_to_utf8;
use function rawurldecode;
use const INTL_IDNA_VARIANT_UTS46;
/**
* @see https://unicode-org.github.io/icu-docs/apidoc/released/icu4c/uidna_8h.html
*/
final class Converter
{
private const REGEXP_IDNA_PATTERN = '/[^\\x20-\\x7f]/';
private const MAX_DOMAIN_LENGTH = 253;
private const MAX_LABEL_LENGTH = 63;
/**
* General registered name regular expression.
*
* @see https://tools.ietf.org/html/rfc3986#section-3.2.2
* @see https://regex101.com/r/fptU8V/1
*/
private const REGEXP_REGISTERED_NAME = '/
(?(DEFINE)
(?<unreserved>[a-z0-9_~\\-]) # . is missing as it is used to separate labels
(?<sub_delims>[!$&\'()*+,;=])
(?<encoded>%[A-F0-9]{2})
(?<reg_name>(?:(?&unreserved)|(?&sub_delims)|(?&encoded))*)
)
^(?:(?&reg_name)\\.)*(?&reg_name)\\.?$
/ix';
/**
* Converts the input to its IDNA ASCII form or throw on failure.
*
* @see Converter::toAscii()
*
* @throws SyntaxError if the string cannot be converted to UNICODE using IDN UTS46 algorithm
* @throws ConversionFailed if the conversion returns error
*/
public static function toAsciiOrFail(Stringable|string $domain, Option|int|null $options = null) : string
{
$result = self::toAscii($domain, $options);
return match (\true) {
$result->hasErrors() => throw ConversionFailed::dueToIdnError($domain, $result),
default => $result->domain(),
};
}
/**
* Converts the input to its IDNA ASCII form.
*
* This method returns the string converted to IDN ASCII form
*
* @throws SyntaxError if the string cannot be converted to ASCII using IDN UTS46 algorithm
*/
public static function toAscii(Stringable|string $domain, Option|int|null $options = null) : Result
{
$domain = rawurldecode((string) $domain);
if (1 === \preg_match(self::REGEXP_IDNA_PATTERN, $domain)) {
FeatureDetection::supportsIdn();
$flags = match (\true) {
null === $options => Option::forIDNA2008Ascii(),
$options instanceof Option => $options,
default => Option::new($options),
};
idn_to_ascii($domain, $flags->toBytes(), INTL_IDNA_VARIANT_UTS46, $idnaInfo);
if ([] === $idnaInfo) {
return Result::fromIntl(['result' => \strtolower($domain), 'isTransitionalDifferent' => \false, 'errors' => self::validateDomainAndLabelLength($domain)]);
}
return Result::fromIntl($idnaInfo);
}
$error = Error::NONE->value;
if (1 !== \preg_match(self::REGEXP_REGISTERED_NAME, $domain)) {
$error |= Error::DISALLOWED->value;
}
return Result::fromIntl(['result' => \strtolower($domain), 'isTransitionalDifferent' => \false, 'errors' => self::validateDomainAndLabelLength($domain) | $error]);
}
/**
* Converts the input to its IDNA UNICODE form or throw on failure.
*
* @see Converter::toUnicode()
*
* @throws ConversionFailed if the conversion returns error
*/
public static function toUnicodeOrFail(Stringable|string $domain, Option|int|null $options = null) : string
{
$result = self::toUnicode($domain, $options);
return match (\true) {
$result->hasErrors() => throw ConversionFailed::dueToIdnError($domain, $result),
default => $result->domain(),
};
}
/**
* Converts the input to its IDNA UNICODE form.
*
* This method returns the string converted to IDN UNICODE form
*
* @throws SyntaxError if the string cannot be converted to UNICODE using IDN UTS46 algorithm
*/
public static function toUnicode(Stringable|string $domain, Option|int|null $options = null) : Result
{
$domain = rawurldecode((string) $domain);
if (\false === \stripos($domain, 'xn--')) {
return Result::fromIntl(['result' => $domain, 'isTransitionalDifferent' => \false, 'errors' => Error::NONE->value]);
}
FeatureDetection::supportsIdn();
$flags = match (\true) {
null === $options => Option::forIDNA2008Unicode(),
$options instanceof Option => $options,
default => Option::new($options),
};
idn_to_utf8($domain, $flags->toBytes(), INTL_IDNA_VARIANT_UTS46, $idnaInfo);
if ([] === $idnaInfo) {
return Result::fromIntl(['result' => $domain, 'isTransitionalDifferent' => \false, 'errors' => Error::NONE->value]);
}
return Result::fromIntl($idnaInfo);
}
/**
* Tells whether the submitted host is a valid IDN regardless of its format.
*
* Returns false if the host is invalid or if its conversion yield the same result
*/
public static function isIdn(Stringable|string|null $domain) : bool
{
$domain = \strtolower(rawurldecode((string) $domain));
$result = match (1) {
\preg_match(self::REGEXP_IDNA_PATTERN, $domain) => self::toAscii($domain),
default => self::toUnicode($domain),
};
return match (\true) {
$result->hasErrors() => \false,
default => $result->domain() !== $domain,
};
}
/**
* Adapted from https://github.com/TRowbotham/idna.
*
* @see https://github.com/TRowbotham/idna/blob/master/src/Idna.php#L236
*/
private static function validateDomainAndLabelLength(string $domain) : int
{
$error = Error::NONE->value;
$labels = \explode('.', $domain);
$maxDomainSize = self::MAX_DOMAIN_LENGTH;
$length = \count($labels);
// If the last label is empty, and it is not the first label, then it is the root label.
// Increase the max size by 1, making it 254, to account for the root label's "."
// delimiter. This also means we don't need to check the last label's length for being too
// long.
if ($length > 1 && '' === $labels[$length - 1]) {
++$maxDomainSize;
\array_pop($labels);
}
if (\strlen($domain) > $maxDomainSize) {
$error |= Error::DOMAIN_NAME_TOO_LONG->value;
}
foreach ($labels as $label) {
if (\strlen($label) > self::MAX_LABEL_LENGTH) {
$error |= Error::LABEL_TOO_LONG->value;
break;
}
}
return $error;
}
}

View File

@ -0,0 +1,56 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace WP_Ultimo\Dependencies\League\Uri\Idna;
enum Error : int
{
case NONE = 0;
case EMPTY_LABEL = 1;
case LABEL_TOO_LONG = 2;
case DOMAIN_NAME_TOO_LONG = 4;
case LEADING_HYPHEN = 8;
case TRAILING_HYPHEN = 0x10;
case HYPHEN_3_4 = 0x20;
case LEADING_COMBINING_MARK = 0x40;
case DISALLOWED = 0x80;
case PUNYCODE = 0x100;
case LABEL_HAS_DOT = 0x200;
case INVALID_ACE_LABEL = 0x400;
case BIDI = 0x800;
case CONTEXTJ = 0x1000;
case CONTEXTO_PUNCTUATION = 0x2000;
case CONTEXTO_DIGITS = 0x4000;
public function description() : string
{
return match ($this) {
self::NONE => 'No error has occurred',
self::EMPTY_LABEL => 'a non-final domain name label (or the whole domain name) is empty',
self::LABEL_TOO_LONG => 'a domain name label is longer than 63 bytes',
self::DOMAIN_NAME_TOO_LONG => 'a domain name is longer than 255 bytes in its storage form',
self::LEADING_HYPHEN => 'a label starts with a hyphen-minus ("-")',
self::TRAILING_HYPHEN => 'a label ends with a hyphen-minus ("-")',
self::HYPHEN_3_4 => 'a label contains hyphen-minus ("-") in the third and fourth positions',
self::LEADING_COMBINING_MARK => 'a label starts with a combining mark',
self::DISALLOWED => 'a label or domain name contains disallowed characters',
self::PUNYCODE => 'a label starts with "xn--" but does not contain valid Punycode',
self::LABEL_HAS_DOT => 'a label contains a dot=full stop',
self::INVALID_ACE_LABEL => 'An ACE label does not contain a valid label string',
self::BIDI => 'a label does not meet the IDNA BiDi requirements (for right-to-left characters)',
self::CONTEXTJ => 'a label does not meet the IDNA CONTEXTJ requirements',
self::CONTEXTO_DIGITS => 'a label does not meet the IDNA CONTEXTO requirements for digits',
self::CONTEXTO_PUNCTUATION => 'a label does not meet the IDNA CONTEXTO requirements for punctuation characters. Some punctuation characters "Would otherwise have been DISALLOWED" but are allowed in certain contexts',
};
}
public static function filterByErrorBytes(int $errors) : array
{
return \array_values(\array_filter(self::cases(), fn(self $error): bool => 0 !== ($error->value & $errors)));
}
}

View File

@ -0,0 +1,137 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare (strict_types=1);
namespace WP_Ultimo\Dependencies\League\Uri\Idna;
use ReflectionClass;
use ReflectionClassConstant;
/**
* @see https://unicode-org.github.io/icu-docs/apidoc/released/icu4c/uidna_8h.html
*/
final class Option
{
private const DEFAULT = 0;
private const ALLOW_UNASSIGNED = 1;
private const USE_STD3_RULES = 2;
private const CHECK_BIDI = 4;
private const CHECK_CONTEXTJ = 8;
private const NONTRANSITIONAL_TO_ASCII = 0x10;
private const NONTRANSITIONAL_TO_UNICODE = 0x20;
private const CHECK_CONTEXTO = 0x40;
private function __construct(private readonly int $value)
{
}
private static function cases() : array
{
static $assoc;
if (null === $assoc) {
$assoc = [];
$fooClass = new ReflectionClass(self::class);
foreach ($fooClass->getConstants(ReflectionClassConstant::IS_PRIVATE) as $name => $value) {
$assoc[$name] = $value;
}
}
return $assoc;
}
public static function new(int $bytes = self::DEFAULT) : self
{
return new self(\array_reduce(self::cases(), fn(int $value, int $option) => 0 !== ($option & $bytes) ? $value | $option : $value, self::DEFAULT));
}
public static function forIDNA2008Ascii() : self
{
return self::new()->nonTransitionalToAscii()->checkBidi()->useSTD3Rules()->checkContextJ();
}
public static function forIDNA2008Unicode() : self
{
return self::new()->nonTransitionalToUnicode()->checkBidi()->useSTD3Rules()->checkContextJ();
}
public function toBytes() : int
{
return $this->value;
}
/** array<string, int> */
public function list() : array
{
return \array_keys(\array_filter(self::cases(), fn(int $value) => 0 !== ($value & $this->value)));
}
public function allowUnassigned() : self
{
return $this->add(self::ALLOW_UNASSIGNED);
}
public function disallowUnassigned() : self
{
return $this->remove(self::ALLOW_UNASSIGNED);
}
public function useSTD3Rules() : self
{
return $this->add(self::USE_STD3_RULES);
}
public function prohibitSTD3Rules() : self
{
return $this->remove(self::USE_STD3_RULES);
}
public function checkBidi() : self
{
return $this->add(self::CHECK_BIDI);
}
public function ignoreBidi() : self
{
return $this->remove(self::CHECK_BIDI);
}
public function checkContextJ() : self
{
return $this->add(self::CHECK_CONTEXTJ);
}
public function ignoreContextJ() : self
{
return $this->remove(self::CHECK_CONTEXTJ);
}
public function checkContextO() : self
{
return $this->add(self::CHECK_CONTEXTO);
}
public function ignoreContextO() : self
{
return $this->remove(self::CHECK_CONTEXTO);
}
public function nonTransitionalToAscii() : self
{
return $this->add(self::NONTRANSITIONAL_TO_ASCII);
}
public function transitionalToAscii() : self
{
return $this->remove(self::NONTRANSITIONAL_TO_ASCII);
}
public function nonTransitionalToUnicode() : self
{
return $this->add(self::NONTRANSITIONAL_TO_UNICODE);
}
public function transitionalToUnicode() : self
{
return $this->remove(self::NONTRANSITIONAL_TO_UNICODE);
}
public function add(Option|int|null $option = null) : self
{
return match (\true) {
null === $option => $this,
$option instanceof self => self::new($this->value | $option->value),
default => self::new($this->value | $option),
};
}
public function remove(Option|int|null $option = null) : self
{
return match (\true) {
null === $option => $this,
$option instanceof self => self::new($this->value & ~$option->value),
default => self::new($this->value & ~$option),
};
}
}

View File

@ -0,0 +1,57 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare (strict_types=1);
namespace WP_Ultimo\Dependencies\League\Uri\Idna;
/**
* @see https://unicode-org.github.io/icu-docs/apidoc/released/icu4c/uidna_8h.html
*/
final class Result
{
private function __construct(
private readonly string $domain,
private readonly bool $isTransitionalDifferent,
/** @var array<Error> */
private readonly array $errors
)
{
}
/**
* @param array{result:string, isTransitionalDifferent:bool, errors:int} $infos
*/
public static function fromIntl(array $infos) : self
{
return new self($infos['result'], $infos['isTransitionalDifferent'], Error::filterByErrorBytes($infos['errors']));
}
public function domain() : string
{
return $this->domain;
}
public function isTransitionalDifferent() : bool
{
return $this->isTransitionalDifferent;
}
/**
* @return array<Error>
*/
public function errors() : array
{
return $this->errors;
}
public function hasErrors() : bool
{
return [] !== $this->errors;
}
public function hasError(Error $error) : bool
{
return \in_array($error, $this->errors, \true);
}
}