diff --git a/src/Assert/Assert.php b/src/Assert/Assert.php index fcaf71f..da2e58d 100644 --- a/src/Assert/Assert.php +++ b/src/Assert/Assert.php @@ -9,33 +9,63 @@ /** * @package simplesamlphp/xml-common * + * @method static void validDateTime(mixed $value, string $message = '', string $exception = '') + * @method static void validDuration(mixed $value, string $message = '', string $exception = '') + * @method static void validEntity(mixed $value, string $message = '', string $exception = '') + * @method static void validEntities(mixed $value, string $message = '', string $exception = '') * @method static void validHexBinary(mixed $value, string $message = '', string $exception = '') + * @method static void validID(mixed $value, string $message = '', string $exception = '') + * @method static void validIDRef(mixed $value, string $message = '', string $exception = '') + * @method static void validIDRefs(mixed $value, string $message = '', string $exception = '') + * @method static void validLang(mixed $value, string $message = '', string $exception = '') + * @method static void validName(mixed $value, string $message = '', string $exception = '') + * @method static void validNCName(mixed $value, string $message = '', string $exception = '') * @method static void validNMToken(mixed $value, string $message = '', string $exception = '') * @method static void validNMTokens(mixed $value, string $message = '', string $exception = '') - * @method static void validDuration(mixed $value, string $message = '', string $exception = '') - * @method static void validDateTime(mixed $value, string $message = '', string $exception = '') - * @method static void validNCName(mixed $value, string $message = '', string $exception = '') * @method static void validQName(mixed $value, string $message = '', string $exception = '') + * @method static void nullOrValidDateTime(mixed $value, string $message = '', string $exception = '') + * @method static void nullOrValidDuration(mixed $value, string $message = '', string $exception = '') + * @method static void nullOrValidEntity(mixed $value, string $message = '', string $exception = '') + * @method static void nullOrValidEntities(mixed $value, string $message = '', string $exception = '') * @method static void nullOrValidHexBinary(mixed $value, string $message = '', string $exception = '') + * @method static void nullOrValidID(mixed $value, string $message = '', string $exception = '') + * @method static void nullOrValidIDRef(mixed $value, string $message = '', string $exception = '') + * @method static void nullOrValidIDRefs(mixed $value, string $message = '', string $exception = '') + * @method static void nullOrValidLang(mixed $value, string $message = '', string $exception = '') + * @method static void nullOrValidName(mixed $value, string $message = '', string $exception = '') + * @method static void nullOrValidNCName(mixed $value, string $message = '', string $exception = '') * @method static void nullOrValidNMToken(mixed $value, string $message = '', string $exception = '') * @method static void nullOrValidNMTokens(mixed $value, string $message = '', string $exception = '') - * @method static void nullOrValidDuration(mixed $value, string $message = '', string $exception = '') - * @method static void nullOrValidDateTime(mixed $value, string $message = '', string $exception = '') - * @method static void nullOrValidNCName(mixed $value, string $message = '', string $exception = '') * @method static void nullOrValidQName(mixed $value, string $message = '', string $exception = '') + * @method static void allValidDateTime(mixed $value, string $message = '', string $exception = '') + * @method static void allValidDuration(mixed $value, string $message = '', string $exception = '') + * @method static void allValidEntity(mixed $value, string $message = '', string $exception = '') + * @method static void allValidEntities(mixed $value, string $message = '', string $exception = '') * @method static void allValidHexBinary(mixed $value, string $message = '', string $exception = '') + * @method static void allValidID(mixed $value, string $message = '', string $exception = '') + * @method static void allValidIDRef(mixed $value, string $message = '', string $exception = '') + * @method static void allValidIDRefs(mixed $value, string $message = '', string $exception = '') + * @method static void allValidLang(mixed $value, string $message = '', string $exception = '') + * @method static void allValidName(mixed $value, string $message = '', string $exception = '') + * @method static void allValidNCName(mixed $value, string $message = '', string $exception = '') * @method static void allValidNMToken(mixed $value, string $message = '', string $exception = '') * @method static void allValidNMTokens(mixed $value, string $message = '', string $exception = '') - * @method static void allValidDuration(mixed $value, string $message = '', string $exception = '') - * @method static void allValidDateTime(mixed $value, string $message = '', string $exception = '') - * @method static void allValidNCName(mixed $value, string $message = '', string $exception = '') * @method static void allValidQName(mixed $value, string $message = '', string $exception = '') */ class Assert extends BaseAssert { use DateTimeTrait; use DurationTrait; - use HexBinTrait; - use NamesTrait; - use TokensTrait; + use HexBinaryTrait; + use EntitiesTrait; + use EntityTrait; + use IDTrait; + use IDRefTrait; + use IDRefsTrait; + use LangTrait; + use NameTrait; + use NCNameTrait; + use NMTokenTrait; + use NMTokensTrait; + use QNameTrait; } diff --git a/src/Assert/EntitiesTrait.php b/src/Assert/EntitiesTrait.php new file mode 100644 index 0000000..76568bb --- /dev/null +++ b/src/Assert/EntitiesTrait.php @@ -0,0 +1,42 @@ + ['regexp' => self::$entities_regex]]) === false) { + throw new InvalidArgumentException(sprintf( + $message ?: '\'%s\' is not a valid xs:ENTITIES', + $value, + )); + } + } +} diff --git a/src/Assert/EntityTrait.php b/src/Assert/EntityTrait.php new file mode 100644 index 0000000..3c5b5eb --- /dev/null +++ b/src/Assert/EntityTrait.php @@ -0,0 +1,35 @@ + ['regexp' => self::$idrefs_regex]]) === false) { + throw new InvalidArgumentException(sprintf( + $message ?: '\'%s\' is not a valid xs:IDREFS', + $value, + )); + } + } +} diff --git a/src/Assert/IDTrait.php b/src/Assert/IDTrait.php new file mode 100644 index 0000000..2c98c6a --- /dev/null +++ b/src/Assert/IDTrait.php @@ -0,0 +1,35 @@ + ['regexp' => self::$lang_regex]]) === false) { + throw new InvalidArgumentException(sprintf( + $message ?: '\'%s\' is not a valid xs:language', + $value, + )); + } + } +} diff --git a/src/Assert/NamesTrait.php b/src/Assert/NCNameTrait.php similarity index 62% rename from src/Assert/NamesTrait.php rename to src/Assert/NCNameTrait.php index 3254913..474edee 100644 --- a/src/Assert/NamesTrait.php +++ b/src/Assert/NCNameTrait.php @@ -12,13 +12,10 @@ /** * @package simplesamlphp/xml-common */ -trait NamesTrait +trait NCNameTrait { /** @var string */ - private static string $ncname_regex = '/^[a-zA-Z_][\w.-]*$/D'; - - /** @var string */ - private static string $qname_regex = '/^[a-zA-Z_][\w.-]*:[a-zA-Z_][\w.-]*$/D'; + private static string $ncname_regex = '/^[\p{L}a-zA-Z-][\w.-]+$/Du'; /*********************************************************************************** * NOTE: Custom assertions may be added below this line. * @@ -42,22 +39,4 @@ protected static function validNCName(string $value, string $message = ''): void )); } } - - - /** - * @param string $value - * @param string $message - */ - protected static function validQName(string $value, string $message = ''): void - { - if ( - filter_var($value, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => self::$qname_regex]]) === false && - filter_var($value, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => self::$ncname_regex]]) === false - ) { - throw new InvalidArgumentException(sprintf( - $message ?: '\'%s\' is not a valid qualified name (QName)', - $value, - )); - } - } } diff --git a/src/Assert/NMTokenTrait.php b/src/Assert/NMTokenTrait.php new file mode 100644 index 0000000..a0c2e9d --- /dev/null +++ b/src/Assert/NMTokenTrait.php @@ -0,0 +1,42 @@ + ['regexp' => self::$nmtoken_regex]]) === false) { + throw new InvalidArgumentException(sprintf( + $message ?: '\'%s\' is not a valid xs:NMTOKEN', + $value, + )); + } + } +} diff --git a/src/Assert/TokensTrait.php b/src/Assert/NMTokensTrait.php similarity index 71% rename from src/Assert/TokensTrait.php rename to src/Assert/NMTokensTrait.php index 9ea6b1e..b1cc459 100644 --- a/src/Assert/TokensTrait.php +++ b/src/Assert/NMTokensTrait.php @@ -12,11 +12,8 @@ /** * @package simplesamlphp/xml-common */ -trait TokensTrait +trait NMTokensTrait { - /** @var string */ - private static string $nmtoken_regex = '/^[\w.:-]+$/Du'; - /** @var string */ private static string $nmtokens_regex = '/^([\w.:-]+)([\s][\w.:-]+)*$/Du'; @@ -29,21 +26,6 @@ trait TokensTrait ***********************************************************************************/ - /** - * @param string $value - * @param string $message - */ - protected static function validNMToken(string $value, string $message = ''): void - { - if (filter_var($value, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => self::$nmtoken_regex]]) === false) { - throw new InvalidArgumentException(sprintf( - $message ?: '\'%s\' is not a valid xs:NMTOKEN', - $value, - )); - } - } - - /** * @param string $value * @param string $message diff --git a/src/Assert/NameTrait.php b/src/Assert/NameTrait.php new file mode 100644 index 0000000..1a7d997 --- /dev/null +++ b/src/Assert/NameTrait.php @@ -0,0 +1,42 @@ + ['regexp' => self::$name_regex]]) === false) { + throw new InvalidArgumentException(sprintf( + $message ?: '\'%s\' is not a valid xs:Name', + $value, + )); + } + } +} diff --git a/src/Assert/QNameTrait.php b/src/Assert/QNameTrait.php new file mode 100644 index 0000000..bb4be8f --- /dev/null +++ b/src/Assert/QNameTrait.php @@ -0,0 +1,44 @@ + ['regexp' => self::$qname_regex]]) === false + ) { + Assert::validNCName( + $value, + $message ?: '\'%s\' is not a valid qualified name (QName)', + InvalidArgumentException::class, + ); + } + } +} diff --git a/src/Type/AbstractValueType.php b/src/Type/AbstractValueType.php new file mode 100644 index 0000000..3ced6c3 --- /dev/null +++ b/src/Type/AbstractValueType.php @@ -0,0 +1,100 @@ +validateValue($value); + } + + + /** + * Get the value. + * + * @return string + */ + public function getValue(): string + { + return $this->sanitizeValue($this->getRawValue()); + } + + + /** + * Get the raw unsanitized value. + * + * @return string + */ + public function getRawValue(): string + { + return $this->value; + } + + + /** + * Sanitize the value. + * + * @param string $value The value + * @throws \Exception on failure + * @return string + */ + protected function sanitizeValue(string $value): string + { + /** + * Perform no sanitation by default. + * Override this method on the implementing class to perform content sanitation. + */ + return $value; + } + + + /** + * Validate the value. + * + * @param string $value The value + * @throws \Exception on failure + * @return void + */ + protected function validateValue(/** @scrutinizer ignore-unused */ string $value): void + { + /** + * Perform no validation by default. + * Override this method on the implementing class to perform validation. + */ + } + + + /** + * @param string $value + * @return \SimpleSAML\XML\Type\ValueTypeInterface + */ + public static function fromString(string $value): ValueTypeInterface + { + return new static($value); + } + + + /** + * Output the value as a string + * + * @return string + */ + public function __toString(): string + { + return $this->getValue(); + } +} diff --git a/src/Type/Base64Value.php b/src/Type/Base64Value.php new file mode 100644 index 0000000..d9e7f1a --- /dev/null +++ b/src/Type/Base64Value.php @@ -0,0 +1,43 @@ +sanitizeValue($value), SchemaViolationException::class); + } +} diff --git a/src/Type/DateTimeValue.php b/src/Type/DateTimeValue.php new file mode 100644 index 0000000..783d442 --- /dev/null +++ b/src/Type/DateTimeValue.php @@ -0,0 +1,26 @@ +sanitizeValue($value), SchemaViolationException::class); + } +} diff --git a/src/Type/EntityValue.php b/src/Type/EntityValue.php new file mode 100644 index 0000000..6970a3e --- /dev/null +++ b/src/Type/EntityValue.php @@ -0,0 +1,12 @@ +sanitizeValue($value), + '/([0-9A-F]{2})*/i', + SchemaViolationException::class, + ); + } +} diff --git a/src/Type/IDRefValue.php b/src/Type/IDRefValue.php new file mode 100644 index 0000000..bc4ebe8 --- /dev/null +++ b/src/Type/IDRefValue.php @@ -0,0 +1,12 @@ +sanitizeValue($value), SchemaViolationException::class); + } +} diff --git a/src/Type/IDValue.php b/src/Type/IDValue.php new file mode 100644 index 0000000..1863014 --- /dev/null +++ b/src/Type/IDValue.php @@ -0,0 +1,12 @@ +sanitizeValue($value), SchemaViolationException::class); + } +} diff --git a/src/Type/NCNameValue.php b/src/Type/NCNameValue.php new file mode 100644 index 0000000..8571249 --- /dev/null +++ b/src/Type/NCNameValue.php @@ -0,0 +1,27 @@ +sanitizeValue($value), SchemaViolationException::class); + } +} diff --git a/src/Type/NMTokenValue.php b/src/Type/NMTokenValue.php new file mode 100644 index 0000000..704f66d --- /dev/null +++ b/src/Type/NMTokenValue.php @@ -0,0 +1,27 @@ +sanitizeValue($value), SchemaViolationException::class); + } +} diff --git a/src/Type/NMTokensValue.php b/src/Type/NMTokensValue.php new file mode 100644 index 0000000..71c8394 --- /dev/null +++ b/src/Type/NMTokensValue.php @@ -0,0 +1,27 @@ +sanitizeValue($value), SchemaViolationException::class); + } +} diff --git a/src/Type/NameValue.php b/src/Type/NameValue.php new file mode 100644 index 0000000..6e8a8ae --- /dev/null +++ b/src/Type/NameValue.php @@ -0,0 +1,27 @@ +sanitizeValue($value), SchemaViolationException::class); + } +} diff --git a/src/Type/NormalizedStringValue.php b/src/Type/NormalizedStringValue.php new file mode 100644 index 0000000..1aa1ee8 --- /dev/null +++ b/src/Type/NormalizedStringValue.php @@ -0,0 +1,24 @@ +assertTrue($shouldPass); + } catch (AssertionFailedException $e) { + $this->assertFalse($shouldPass); + } + } + + + /** + * @return array + */ + public static function provideEntities(): array + { + return [ + [true, 'Snoopy'], + [true, 'CMS'], + [true, 'fööbár'], + [true, '-1950-10-04'], + [false, '0836217462 0836217463'], + [true, 'foo bar'], + // Quotes are forbidden + [false, 'foo "bar" baz'], + // Commas are forbidden + [false, 'foo,bar'], + // Trailing newlines are forbidden + [false, "foobar\n"], + ]; + } +} diff --git a/tests/Assert/EntityTest.php b/tests/Assert/EntityTest.php new file mode 100644 index 0000000..b7488f8 --- /dev/null +++ b/tests/Assert/EntityTest.php @@ -0,0 +1,56 @@ +assertTrue($shouldPass); + } catch (AssertionFailedException $e) { + $this->assertFalse($shouldPass); + } + } + + + /** + * @return array + */ + public static function provideEntity(): array + { + return [ + [true, 'Snoopy'], + [true, 'CMS'], + [true, 'fööbár'], + [true, '-1950-10-04'], + [false, '0836217462'], + // Spaces are forbidden + [false, 'foo bar'], + // Commas are forbidden + [false, 'foo,bar'], + // Trailing newlines are forbidden + [false, "foobar\n"], + ]; + } +} diff --git a/tests/Assert/HexBinTest.php b/tests/Assert/HexBinaryTest.php similarity index 97% rename from tests/Assert/HexBinTest.php rename to tests/Assert/HexBinaryTest.php index 042e7ce..76deff6 100644 --- a/tests/Assert/HexBinTest.php +++ b/tests/Assert/HexBinaryTest.php @@ -7,8 +7,8 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; -use SimpleSAML\Assert\Assert; use SimpleSAML\Assert\AssertionFailedException; +use SimpleSAML\XML\Assert\Assert; /** * Class \SimpleSAML\Test\Assert\HexBinaryTest diff --git a/tests/Assert/IDRefTest.php b/tests/Assert/IDRefTest.php new file mode 100644 index 0000000..c5c25cf --- /dev/null +++ b/tests/Assert/IDRefTest.php @@ -0,0 +1,56 @@ +assertTrue($shouldPass); + } catch (AssertionFailedException $e) { + $this->assertFalse($shouldPass); + } + } + + + /** + * @return array + */ + public static function provideIDRef(): array + { + return [ + [true, 'Snoopy'], + [true, 'CMS'], + [true, 'fööbár'], + [true, '-1950-10-04'], + [false, '0836217462'], + // Spaces are forbidden + [false, 'foo bar'], + // Commas are forbidden + [false, 'foo,bar'], + // Trailing newlines are forbidden + [false, "foobar\n"], + ]; + } +} diff --git a/tests/Assert/IDRefsTest.php b/tests/Assert/IDRefsTest.php new file mode 100644 index 0000000..d12cc73 --- /dev/null +++ b/tests/Assert/IDRefsTest.php @@ -0,0 +1,57 @@ +assertTrue($shouldPass); + } catch (AssertionFailedException $e) { + $this->assertFalse($shouldPass); + } + } + + + /** + * @return array + */ + public static function provideIDRefs(): array + { + return [ + [true, 'Snoopy'], + [true, 'CMS'], + [true, 'fööbár'], + [true, '-1950-10-04'], + [false, '0836217462 0836217463'], + [true, 'foo bar'], + // Quotes are forbidden + [false, 'foo "bar" baz'], + // Commas are forbidden + [false, 'foo,bar'], + // Trailing newlines are forbidden + [false, "foobar\n"], + ]; + } +} diff --git a/tests/Assert/IDTest.php b/tests/Assert/IDTest.php new file mode 100644 index 0000000..d333c2e --- /dev/null +++ b/tests/Assert/IDTest.php @@ -0,0 +1,56 @@ +assertTrue($shouldPass); + } catch (AssertionFailedException $e) { + $this->assertFalse($shouldPass); + } + } + + + /** + * @return array + */ + public static function provideID(): array + { + return [ + [true, 'Snoopy'], + [true, 'CMS'], + [true, 'fööbár'], + [true, '-1950-10-04'], + [false, '0836217462'], + // Spaces are forbidden + [false, 'foo bar'], + // Commas are forbidden + [false, 'foo,bar'], + // Trailing newlines are forbidden + [false, "foobar\n"], + ]; + } +} diff --git a/tests/Assert/LangTest.php b/tests/Assert/LangTest.php new file mode 100644 index 0000000..25468a6 --- /dev/null +++ b/tests/Assert/LangTest.php @@ -0,0 +1,51 @@ +assertTrue($shouldPass); + } catch (AssertionFailedException $e) { + $this->assertFalse($shouldPass); + } + } + + + /** + * @return array + */ + public static function provideLang(): array + { + return [ + 'one part' => [true, 'lang'], + 'two parts' => [true, 'en-US'], + 'too many parts' => [false, 'not-a-lang'], + 'too long' => [false, 'toolonglanguage'], + 'x-case' => [true, 'x-klingon'], + 'i-case' => [true, 'i-sami-no'], + ]; + } +} diff --git a/tests/Assert/NCNameTest.php b/tests/Assert/NCNameTest.php index f3df64e..3d81db4 100644 --- a/tests/Assert/NCNameTest.php +++ b/tests/Assert/NCNameTest.php @@ -41,9 +41,12 @@ public static function provideNCName(): array { return [ [true, 'Test'], - [true, '_Test'], + [true, 'Te.st'], + [false, '_Test'], + [true, '-1950-10-04-10-00'], + [true, 'fööbár'], // Prefixed v4 UUID - [true, '_5425e58e-e799-4884-92cc-ca64ecede32f'], + [false, '_5425e58e-e799-4884-92cc-ca64ecede32f'], // An empty value is not valid, unless xsi:nil is used [false, ''], [false, 'Te*st'], diff --git a/tests/Assert/NameTest.php b/tests/Assert/NameTest.php new file mode 100644 index 0000000..6f3adbc --- /dev/null +++ b/tests/Assert/NameTest.php @@ -0,0 +1,57 @@ +assertTrue($shouldPass); + } catch (AssertionFailedException $e) { + $this->assertFalse($shouldPass); + } + } + + + /** + * @return array + */ + public static function provideName(): array + { + return [ + [true, 'Snoopy'], + [true, ':CMS'], + [true, 'fööbár'], + [true, '-1950-10-04'], + // Must start with a letter, a dash or a colon + [false, '0836217462'], + // Spaces are forbidden + [false, 'foo bar'], + // Commas are forbidden + [false, 'foo,bar'], + // Trailing newlines are forbidden + [false, "foobar\n"], + ]; + } +} diff --git a/tests/Type/LangValueTest.php b/tests/Type/LangValueTest.php new file mode 100644 index 0000000..ec2c401 --- /dev/null +++ b/tests/Type/LangValueTest.php @@ -0,0 +1,54 @@ +assertTrue($shouldPass); + } catch (SchemaViolationException $e) { + $this->assertFalse($shouldPass); + } + } + + + /** + * @return array + */ + public static function provideLang(): array + { + return [ + 'one part' => [true, 'lang'], + 'two parts' => [true, 'en-US'], + 'too many parts' => [false, 'not-a-lang'], + 'too long' => [false, 'toolonglanguage'], + 'x-case' => [true, 'x-klingon'], + 'i-case' => [true, 'i-sami-no'], + 'normalization' => [true, ' en-US '], + ]; + } +}