diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 8bb4293..12ce07f 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -212,7 +212,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-versions }} - extensions: ctype, date, filter, pcre, spl + extensions: ctype, date, filter, intl, pcre, spl tools: composer ini-values: error_reporting=E_ALL coverage: none diff --git a/composer.json b/composer.json index 14474cd..aa5ae8a 100644 --- a/composer.json +++ b/composer.json @@ -19,10 +19,11 @@ "ext-pcre": "*", "ext-spl": "*", + "league/uri-interfaces": "^7.4", "webmozart/assert": "^1.11" }, "require-dev": { - "simplesamlphp/simplesamlphp-test-framework": "^1.5.5" + "simplesamlphp/simplesamlphp-test-framework": "^1.7" }, "autoload": { "psr-4": { @@ -44,7 +45,8 @@ "allow-plugins": { "composer/package-versions-deprecated": true, "dealerdirect/phpcodesniffer-composer-installer": true, - "simplesamlphp/composer-module-installer": false + "simplesamlphp/composer-module-installer": false, + "phpstan/extension-installer": true } } } diff --git a/src/CustomAssertionTrait.php b/src/CustomAssertionTrait.php index 8b37917..7d67ce3 100644 --- a/src/CustomAssertionTrait.php +++ b/src/CustomAssertionTrait.php @@ -4,9 +4,9 @@ namespace SimpleSAML\Assert; -use DateTimeImmutable; // Requires ext-date -use DateTimeInterface; // Requires ext-date use InvalidArgumentException; +use League\Uri\Exceptions\SyntaxError; +use League\Uri\UriString; use function array_map; use function base64_decode; @@ -37,15 +37,6 @@ trait CustomAssertionTrait /** @var string */ private static string $base64_regex = '/^(?:[a-z0-9+\/]{4})*(?:[a-z0-9+\/]{2}==|[a-z0-9+\/]{3}=)?$/i'; - /** @var string */ - private static string $uri_same_document_regex = '#^(?:\#([A-Za-z][A-Za-z0-9+\-.]*:(?:\/\/(?:(?:[A-Za-z0-9\-._~!$&\'()*+,;=:]|%[0-9A-Fa-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9A-Fa-f]{1,4}:){6}|::(?:[0-9A-Fa-f]{1,4}:){5}|(?:[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){4}|(?:(?:[0-9A-Fa-f]{1,4}:){0,1}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){3}|(?:(?:[0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){2}|(?:(?:[0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:|(?:(?:[0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})?::)(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))|(?:(?:[0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(?:(?:[0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})?::)|[Vv][0-9A-Fa-f]+\.[A-Za-z0-9\-._~!$&\'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|(?:[A-Za-z0-9\-._~!$&\'()*+,;=]|%[0-9A-Fa-f]{2})*)(?::[0-9]*)?(?:\/(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*|\/(?:(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})+(?:\/(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*)?|(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})+(?:\/(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*|)(?:\?(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@/?]|%[0-9A-Fa-f]{2})*)?(?:\#(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@/?]|%[0-9A-Fa-f]{2})*)?|(?:\/\/(?:(?:[A-Za-z0-9\-._~!$&\'()*+,;=:]|%[0-9A-Fa-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9A-Fa-f]{1,4}:){6}|::(?:[0-9A-Fa-f]{1,4}:){5}|(?:[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){4}|(?:(?:[0-9A-Fa-f]{1,4}:){0,1}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){3}|(?:(?:[0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){2}|(?:(?:[0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:|(?:(?:[0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})?::)(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))|(?:(?:[0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(?:(?:[0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})?::)|[Vv][0-9A-Fa-f]+\.[A-Za-z0-9\-._~!$&\'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|(?:[A-Za-z0-9\-._~!$&\'()*+,;=]|%[0-9A-Fa-f]{2})*)(?::[0-9]*)?(?:\/(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*|\/(?:(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})+(?:\/(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*)?|(?:[A-Za-z0-9\-._~!$&\'()*+,;=@]|%[0-9A-Fa-f]{2})+(?:\/(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*|)(?:\?(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@/?]|%[0-9A-Fa-f]{2})*)?(?:\#(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@/?]|%[0-9A-Fa-f]{2})*)?))$#'; - - /** @var string */ - private static string $urn_regex = '/\A(?i:urn:(?!urn:)(?[a-z0-9][a-z0-9-]{1,31}):(?(?:[-a-z0-9()+,.:=@;$_!*\'&~\/]|%[0-9a-f]{2})+)(?:\?\+(?.*?))?(?:\?=(?.*?))?(?:#(?.*?))?)\z/'; - - /** @var string */ - private static string $uri_regex = '#[A-Za-z][A-Za-z0-9+\-.]*:(?:\/\/(?:(?:[A-Za-z0-9\-._~!$&\'()*+,;=:]|%[0-9A-Fa-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9A-Fa-f]{1,4}:){6}|::(?:[0-9A-Fa-f]{1,4}:){5}|(?:[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){4}|(?:(?:[0-9A-Fa-f]{1,4}:){0,1}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){3}|(?:(?:[0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){2}|(?:(?:[0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:|(?:(?:[0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})?::)(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))|(?:(?:[0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(?:(?:[0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})?::)|[Vv][0-9A-Fa-f]+\.[A-Za-z0-9\-._~!$&\'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|(?:[A-Za-z0-9\-._~!$&\'()*+,;=]|%[0-9A-Fa-f]{2})*)(?::[0-9]*)?(?:\/(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*|\/(?:(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})+(?:\/(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*)?|(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})+(?:\/(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*|)(?:\?(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@/?]|%[0-9A-Fa-f]{2})*)?(?:\#(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@/?]|%[0-9A-Fa-f]{2})*)?#'; - /** @var string */ private static string $hostname_regex = '/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/'; @@ -168,7 +159,19 @@ private static function notInArray($value, array $values, string $message = ''): */ private static function validURN(string $value, string $message = ''): void { - if (filter_var($value, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => self::$urn_regex]]) === false) { + try { + $uri = UriString::parse($value); + } catch (SyntaxError $e) { + throw new InvalidArgumentException(sprintf( + $message ?: '\'%s\' is not a valid RFC3986 compliant URI', + $value, + )); + } + + if ( + $uri['scheme'] !== 'urn' + || (($uri['scheme'] !== null) && $uri['path'] !== substr($value, strlen($uri['scheme']) + 1)) + ) { throw new InvalidArgumentException(sprintf( $message ?: '\'%s\' is not a valid RFC8141 compliant URN', $value, @@ -183,7 +186,16 @@ private static function validURN(string $value, string $message = ''): void */ private static function validURL(string $value, string $message = ''): void { - if (filter_var($value, FILTER_VALIDATE_URL) === false) { + try { + $uri = UriString::parse($value); + } catch (SyntaxError $e) { + throw new InvalidArgumentException(sprintf( + $message ?: '\'%s\' is not a valid RFC3986 compliant URI', + $value, + )); + } + + if ($uri['scheme'] !== 'http' && $uri['scheme'] !== 'https') { throw new InvalidArgumentException(sprintf( $message ?: '\'%s\' is not a valid RFC2396 compliant URL', $value, @@ -198,12 +210,9 @@ private static function validURL(string $value, string $message = ''): void */ private static function validURI(string $value, string $message = ''): void { - if ( - filter_var($value, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => self::$uri_regex]]) === false && - // We're very lenient here to accept DNS hostnames without a scheme - filter_var($value, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => self::$hostname_regex]]) === false && - filter_var($value, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => self::$uri_same_document_regex]]) === false - ) { + try { + UriString::parse($value); + } catch (SyntaxError $e) { throw new InvalidArgumentException(sprintf( $message ?: '\'%s\' is not a valid RFC3986 compliant URI', $value, diff --git a/tests/Assert/URITest.php b/tests/Assert/URITest.php index 85391f0..83d64cd 100644 --- a/tests/Assert/URITest.php +++ b/tests/Assert/URITest.php @@ -75,8 +75,10 @@ public static function provideURI(): array 'urn' => [true, 'urn:x-simplesamlphp:phpunit'], 'same-doc' => [true, '#_53d830ab1be17291a546c95c7f1cdf8d3d23c959e6'], 'url' => [true, 'https://www.simplesamlphp.org'], - 'bogus' => [false, 'stupid value'], + 'invalid_char' => [false, 'https://a⒈com'], + 'intl' => [true, 'https://niño.com'], 'spn' => [true, 'spn:a4cf592f-a64c-46ff-a788-b260f474525b'], + 'typos' => [true, 'https//www.uni.l/en/'], ]; } @@ -90,8 +92,10 @@ public static function provideURL(): array 'url' => [true, 'https://www.simplesamlphp.org'], 'same-doc' => [false, '#_53d830ab1be17291a546c95c7f1cdf8d3d23c959e6'], 'urn' => [false, 'urn:x-simplesamlphp:phpunit'], - 'bogus' => [false, 'stupid value'], + 'invalid_char' => [false, 'https://a⒈com'], + 'intl' => [true, 'https://niño.com'], 'spn' => [false, 'spn:a4cf592f-a64c-46ff-a788-b260f474525b'], + 'typos' => [false, 'https//www.uni.l/en/'], ]; } @@ -105,8 +109,10 @@ public static function provideURN(): array 'urn' => [true, 'urn:x-simplesamlphp:phpunit'], 'url' => [false, 'https://www.simplesamlphp.org'], 'same-doc' => [false, '#_53d830ab1be17291a546c95c7f1cdf8d3d23c959e6'], - 'bogus' => [false, 'stupid value'], + 'invalid_char' => [false, 'https://a⒈com'], + 'intl' => [false, 'https://niño.com'], 'spn' => [false, 'spn:a4cf592f-a64c-46ff-a788-b260f474525b'], + 'typos' => [false, 'https//www.uni.l/en/'], ]; } }