From 9c4ee2b9aab124a0f9c4a1319d383fcbd534da06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20H=C3=A4drich?= Date: Sat, 2 Nov 2024 02:20:15 +0100 Subject: [PATCH 1/4] feat: :sparkles: Implement class-based UUID validation rule with more constraints than just the version --- src/Illuminate/Validation/Rule.php | 12 ++ src/Illuminate/Validation/Rules/Uuid.php | 203 ++++++++++++++++++++ tests/Validation/ValidationUuidRuleTest.php | 98 ++++++++++ 3 files changed, 313 insertions(+) create mode 100644 src/Illuminate/Validation/Rules/Uuid.php create mode 100644 tests/Validation/ValidationUuidRuleTest.php diff --git a/src/Illuminate/Validation/Rule.php b/src/Illuminate/Validation/Rule.php index f0483842d99c..91c37b5b1896 100644 --- a/src/Illuminate/Validation/Rule.php +++ b/src/Illuminate/Validation/Rule.php @@ -17,6 +17,7 @@ use Illuminate\Validation\Rules\ProhibitedIf; use Illuminate\Validation\Rules\RequiredIf; use Illuminate\Validation\Rules\Unique; +use Illuminate\Validation\Rules\Uuid; class Rule { @@ -210,4 +211,15 @@ public static function dimensions(array $constraints = []) { return new Dimensions($constraints); } + + /** + * Get an uuid rule builder instance. + * + * @param class-string $type + * @return \Illuminate\Validation\Rules\Uuid + */ + public static function uuid($type) + { + return new Uuid($type); + } } diff --git a/src/Illuminate/Validation/Rules/Uuid.php b/src/Illuminate/Validation/Rules/Uuid.php new file mode 100644 index 000000000000..e8af4b45e8e7 --- /dev/null +++ b/src/Illuminate/Validation/Rules/Uuid.php @@ -0,0 +1,203 @@ +|'max' + */ + protected string|int $version; + + /** + * Usually, the MAC address (can be random) + * Supported by v1, v2, and v6 + */ + protected string $node; + + /** + * @var ':'|'-' $separator + */ + protected string $separator = ':'; + + /** + * Usually, the namespace + * Supported by v3 and v5 + */ + protected string $domain; + + /** + * Usually, the namespace ID + * Supported by v3 and v5 + */ + protected string $identifier; + + /** + * Supported by v1, v2, v6, and v7 + * @var callable $dateTimeCallback + */ + protected $dateTimeCallback; + + public function passes($attribute, $value) + { + if (! Str::isUuid($value)) { + return false; + } + + $factory = new UuidFactory; + + try { + $factoryUuid = $factory->fromString($value); + } catch (InvalidUuidStringException $ex) { + return false; + } + + $fields = $factoryUuid->getFields(); + + if (! ($fields instanceof FieldsInterface)) { + return false; + } + + foreach ([ 'version', 'node', 'domain', 'identifier', 'dateTimeCallback' ] as $prop) { + if (! $this->isInitialized($prop)) { + continue; + } + + if ( + $prop === 'version' && ( + (in_array($this->version, [ 0, 'nil' ], true) && ! $fields->isNil()) || + ($this->version === 'max' && ! $fields->isMax()) || + (! in_array($this->version, [ 0, 'nil', 'max' ]) && $fields->getVersion() !== $this->version) + ) + ) { + return false; + } + + if ($prop === 'node' && in_array($fields->getVersion(), [ + UuidUuid::UUID_TYPE_TIME, + UuidUuid::UUID_TYPE_DCE_SECURITY, + UuidUuid::UUID_TYPE_REORDERED_TIME, + ]) && $this->node !== $this->formatMacAddress($fields->getNode()->toString())) { + return false; + } + + if ( + $prop === 'domain' && + $fields->getVersion() === UuidUuid::UUID_TYPE_DCE_SECURITY && + $this->toV2($fields, $factory)->getLocalDomainName() !== $this->domain + ) { + return false; + } + + if ( + $prop === 'identifier' && + $fields->getVersion() === UuidUuid::UUID_TYPE_DCE_SECURITY && + $this->toV2($fields, $factory)->getLocalIdentifier()->toString() !== $this->identifier + ) { + return false; + } + + if ( + $prop === 'dateTimeCallback' && + $this->dateTimeCallback !== null && + in_array($fields->getVersion(), [ + UuidUuid::UUID_TYPE_TIME, + UuidUuid::UUID_TYPE_DCE_SECURITY, + UuidUuid::UUID_TYPE_REORDERED_TIME, + UuidUuid::UUID_TYPE_UNIX_TIME, + ]) && + ! call_user_func($this->dateTimeCallback, Carbon::createFromId($factoryUuid)) + ) { + return false; + } + } + + return true; + } + + private function toV2(FieldsInterface $fields, UuidFactory $factory) + { + $features = new FeatureSet(); + return new UuidV2($fields, $factory->getNumberConverter(), $factory->getCodec(), $features->getTimeConverter()); + } + + private function formatMacAddress(string $macAddress): string + { + return Str::upper(implode($this->separator, str_split($macAddress, 2))); + } + + /** + * Get the validation error message. + * + * @return array + */ + public function message() + { + $message = $this->validator->getTranslator()->get('validation.uuid'); + + return $message === 'validation.uuid' + ? ['The selected UUID (:attribute) is invalid.'] + : $message; + } + + /** + * Set the current validator. + * + * @param \Illuminate\Validation\Validator $validator + * @return $this + */ + public function setValidator($validator) + { + return tap($this, fn () => $this->validator = $validator); + } + + public function version(string|int $version) + { + return tap($this, fn () => $this->version = $version); + } + + public function node(string $node) + { + return tap($this, fn () => $this->node = Str::upper($node)); + } + + public function domain(string|int $domain) + { + return tap($this, fn () => $this->domain = is_int($domain) ? UuidUuid::DCE_DOMAIN_NAMES[$domain] : $domain); + } + + public function identifier(string|int $identifier) + { + return tap($this, fn () => $this->identifier = (string)$identifier); + } + + public function dateTime(callable $callback) + { + return tap($this, fn () => $this->dateTimeCallback = $callback); + } + + private function isInitialized(string $property) + { + return (new ReflectionProperty($this, $property))->isInitialized($this); + } +} diff --git a/tests/Validation/ValidationUuidRuleTest.php b/tests/Validation/ValidationUuidRuleTest.php new file mode 100644 index 000000000000..e566c2aea5a8 --- /dev/null +++ b/tests/Validation/ValidationUuidRuleTest.php @@ -0,0 +1,98 @@ + $value, + ], + [ + 'uuid' => $rule, + ] + ); + + $this->assertFalse($v->fails()); + } + + public static function providesValidValidationDataAndRules(): array + { + return [ + [ '00000000-0000-0000-0000-000000000000', new Uuid ], + [ '145a1e72-d11d-11e8-a8d5-f2801f1b9fd1', (new Uuid())->version(1) ], + [ '85962c6a-98a5-11ef-8d2a-0242fe50b4b4', (new Uuid())->version(1)->node('02:42:fe:50:b4:b4') ], + [ '000001f5-5e9a-21ea-9e00-0242ac130003', (new Uuid())->version(2)->domain(UuidUuid::DCE_DOMAIN_PERSON)->identifier(501) ], + [ '85962c6a-98a5-11ef-8d2a-0242fe50b4b4', (new Uuid())->version(1)->dateTime(fn (Carbon $dateTime) => $dateTime->isSameDay('2024-11-01')) ], + ]; + } + + #[DataProvider('providesInvalidValidationDataAndRules')] + public function testValidationPassesWhenPassingIncorrectUuid(string $value, Uuid $rule) + { + $v = new Validator( + resolve('translator'), + [ + 'uuid' => $value, + ], + [ + 'uuid' => $rule, + ] + ); + + $this->assertTrue($v->fails()); + } + + public static function providesInvalidValidationDataAndRules(): array + { + return [ + [ 'ZZZZZZZZ-ZZZZ-ZZZZ-ZZZZ-ZZZZZZZZZZZZ', new Uuid ], + [ '145a1e72-d11d-11e8-a8d5-f2801f1b9fd1', (new Uuid())->version(2) ], + [ '145a1e72-d11d-11e8-a8d5-f2801f1b9fd1', (new Uuid())->version(42) ], + [ '85962c6a-98a5-11ef-8d2a-0242fe50b4b4', (new Uuid())->version(1)->node('ZZ:ZZ:ZZ:ZZ:ZZ:ZZ') ], + [ '000001f5-5e9a-21ea-9e00-0242ac130003', (new Uuid())->version(2)->domain(UuidUuid::DCE_DOMAIN_GROUP)->identifier(501) ], + [ '000001f5-5e9a-21ea-9e00-0242ac130003', (new Uuid())->version(2)->domain(UuidUuid::DCE_DOMAIN_PERSON)->identifier(42) ], + [ '85962c6a-98a5-11ef-8d2a-0242fe50b4b4', (new Uuid())->version(1)->dateTime(fn (Carbon $dateTime) => $dateTime->isSameDay('2042-01-11')) ], + ]; + } + + protected function setUp(): void + { + $container = Container::getInstance(); + + $container->bind('translator', function () { + return new Translator( + new ArrayLoader, 'en' + ); + }); + + Facade::setFacadeApplication($container); + + (new ValidationServiceProvider($container))->register(); + } + + protected function tearDown(): void + { + Container::setInstance(null); + + Facade::clearResolvedInstances(); + + Facade::setFacadeApplication(null); + } +} From f6af0e0caa3385177164a1971d028a98ccad2a2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20H=C3=A4drich?= Date: Sat, 2 Nov 2024 02:26:47 +0100 Subject: [PATCH 2/4] style: Fix StyleCI errors --- src/Illuminate/Validation/Rules/Uuid.php | 20 ++++++++--------- tests/Validation/ValidationUuidRuleTest.php | 24 ++++++++++----------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/Illuminate/Validation/Rules/Uuid.php b/src/Illuminate/Validation/Rules/Uuid.php index e8af4b45e8e7..52097d5a33e7 100644 --- a/src/Illuminate/Validation/Rules/Uuid.php +++ b/src/Illuminate/Validation/Rules/Uuid.php @@ -31,30 +31,30 @@ class Uuid implements Rule, ValidatorAwareRule /** * Usually, the MAC address (can be random) - * Supported by v1, v2, and v6 + * Supported by v1, v2, and v6. */ protected string $node; /** - * @var ':'|'-' $separator + * @var ':'|'-' */ protected string $separator = ':'; /** * Usually, the namespace - * Supported by v3 and v5 + * Supported by v3 and v5. */ protected string $domain; /** * Usually, the namespace ID - * Supported by v3 and v5 + * Supported by v3 and v5. */ protected string $identifier; /** - * Supported by v1, v2, v6, and v7 - * @var callable $dateTimeCallback + * Supported by v1, v2, v6, and v7. + * @var callable */ protected $dateTimeCallback; @@ -78,16 +78,16 @@ public function passes($attribute, $value) return false; } - foreach ([ 'version', 'node', 'domain', 'identifier', 'dateTimeCallback' ] as $prop) { + foreach (['version', 'node', 'domain', 'identifier', 'dateTimeCallback'] as $prop) { if (! $this->isInitialized($prop)) { continue; } if ( $prop === 'version' && ( - (in_array($this->version, [ 0, 'nil' ], true) && ! $fields->isNil()) || + (in_array($this->version, [0, 'nil'], true) && ! $fields->isNil()) || ($this->version === 'max' && ! $fields->isMax()) || - (! in_array($this->version, [ 0, 'nil', 'max' ]) && $fields->getVersion() !== $this->version) + (! in_array($this->version, [0, 'nil', 'max']) && $fields->getVersion() !== $this->version) ) ) { return false; @@ -188,7 +188,7 @@ public function domain(string|int $domain) public function identifier(string|int $identifier) { - return tap($this, fn () => $this->identifier = (string)$identifier); + return tap($this, fn () => $this->identifier = (string) $identifier); } public function dateTime(callable $callback) diff --git a/tests/Validation/ValidationUuidRuleTest.php b/tests/Validation/ValidationUuidRuleTest.php index e566c2aea5a8..fe89dffcff3f 100644 --- a/tests/Validation/ValidationUuidRuleTest.php +++ b/tests/Validation/ValidationUuidRuleTest.php @@ -35,11 +35,11 @@ public function testValidationPassesWhenPassingCorrectUuid(string $value, Uuid $ public static function providesValidValidationDataAndRules(): array { return [ - [ '00000000-0000-0000-0000-000000000000', new Uuid ], - [ '145a1e72-d11d-11e8-a8d5-f2801f1b9fd1', (new Uuid())->version(1) ], - [ '85962c6a-98a5-11ef-8d2a-0242fe50b4b4', (new Uuid())->version(1)->node('02:42:fe:50:b4:b4') ], - [ '000001f5-5e9a-21ea-9e00-0242ac130003', (new Uuid())->version(2)->domain(UuidUuid::DCE_DOMAIN_PERSON)->identifier(501) ], - [ '85962c6a-98a5-11ef-8d2a-0242fe50b4b4', (new Uuid())->version(1)->dateTime(fn (Carbon $dateTime) => $dateTime->isSameDay('2024-11-01')) ], + ['00000000-0000-0000-0000-000000000000', new Uuid], + ['145a1e72-d11d-11e8-a8d5-f2801f1b9fd1', (new Uuid())->version(1)], + ['85962c6a-98a5-11ef-8d2a-0242fe50b4b4', (new Uuid())->version(1)->node('02:42:fe:50:b4:b4')], + ['000001f5-5e9a-21ea-9e00-0242ac130003', (new Uuid())->version(2)->domain(UuidUuid::DCE_DOMAIN_PERSON)->identifier(501)], + ['85962c6a-98a5-11ef-8d2a-0242fe50b4b4', (new Uuid())->version(1)->dateTime(fn (Carbon $dateTime) => $dateTime->isSameDay('2024-11-01'))], ]; } @@ -62,13 +62,13 @@ public function testValidationPassesWhenPassingIncorrectUuid(string $value, Uuid public static function providesInvalidValidationDataAndRules(): array { return [ - [ 'ZZZZZZZZ-ZZZZ-ZZZZ-ZZZZ-ZZZZZZZZZZZZ', new Uuid ], - [ '145a1e72-d11d-11e8-a8d5-f2801f1b9fd1', (new Uuid())->version(2) ], - [ '145a1e72-d11d-11e8-a8d5-f2801f1b9fd1', (new Uuid())->version(42) ], - [ '85962c6a-98a5-11ef-8d2a-0242fe50b4b4', (new Uuid())->version(1)->node('ZZ:ZZ:ZZ:ZZ:ZZ:ZZ') ], - [ '000001f5-5e9a-21ea-9e00-0242ac130003', (new Uuid())->version(2)->domain(UuidUuid::DCE_DOMAIN_GROUP)->identifier(501) ], - [ '000001f5-5e9a-21ea-9e00-0242ac130003', (new Uuid())->version(2)->domain(UuidUuid::DCE_DOMAIN_PERSON)->identifier(42) ], - [ '85962c6a-98a5-11ef-8d2a-0242fe50b4b4', (new Uuid())->version(1)->dateTime(fn (Carbon $dateTime) => $dateTime->isSameDay('2042-01-11')) ], + ['ZZZZZZZZ-ZZZZ-ZZZZ-ZZZZ-ZZZZZZZZZZZZ', new Uuid], + ['145a1e72-d11d-11e8-a8d5-f2801f1b9fd1', (new Uuid())->version(2)], + ['145a1e72-d11d-11e8-a8d5-f2801f1b9fd1', (new Uuid())->version(42)], + ['85962c6a-98a5-11ef-8d2a-0242fe50b4b4', (new Uuid())->version(1)->node('ZZ:ZZ:ZZ:ZZ:ZZ:ZZ')], + ['000001f5-5e9a-21ea-9e00-0242ac130003', (new Uuid())->version(2)->domain(UuidUuid::DCE_DOMAIN_GROUP)->identifier(501)], + ['000001f5-5e9a-21ea-9e00-0242ac130003', (new Uuid())->version(2)->domain(UuidUuid::DCE_DOMAIN_PERSON)->identifier(42)], + ['85962c6a-98a5-11ef-8d2a-0242fe50b4b4', (new Uuid())->version(1)->dateTime(fn (Carbon $dateTime) => $dateTime->isSameDay('2042-01-11'))], ]; } From e25e6c04c06bb728c0181a91edfd9c110fa83d3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20H=C3=A4drich?= Date: Sat, 2 Nov 2024 02:28:01 +0100 Subject: [PATCH 3/4] style: :art: More style fixes --- src/Illuminate/Validation/Rules/Uuid.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Illuminate/Validation/Rules/Uuid.php b/src/Illuminate/Validation/Rules/Uuid.php index 52097d5a33e7..e2210a3e9407 100644 --- a/src/Illuminate/Validation/Rules/Uuid.php +++ b/src/Illuminate/Validation/Rules/Uuid.php @@ -54,6 +54,7 @@ class Uuid implements Rule, ValidatorAwareRule /** * Supported by v1, v2, v6, and v7. + * * @var callable */ protected $dateTimeCallback; @@ -138,6 +139,7 @@ public function passes($attribute, $value) private function toV2(FieldsInterface $fields, UuidFactory $factory) { $features = new FeatureSet(); + return new UuidV2($fields, $factory->getNumberConverter(), $factory->getCodec(), $features->getTimeConverter()); } From 4f6ed5e61b0a103412181d882a440724027524dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20H=C3=A4drich?= Date: Sat, 2 Nov 2024 02:31:30 +0100 Subject: [PATCH 4/4] fix: :bug: Remove leftover --- src/Illuminate/Validation/Rule.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Validation/Rule.php b/src/Illuminate/Validation/Rule.php index 91c37b5b1896..e49fc71d5272 100644 --- a/src/Illuminate/Validation/Rule.php +++ b/src/Illuminate/Validation/Rule.php @@ -215,11 +215,10 @@ public static function dimensions(array $constraints = []) /** * Get an uuid rule builder instance. * - * @param class-string $type * @return \Illuminate\Validation\Rules\Uuid */ - public static function uuid($type) + public static function uuid() { - return new Uuid($type); + return new Uuid(); } }