From 50da2138d553bf01d7cad8e1ea9fcc6f31cf34b0 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 31 Oct 2023 09:03:40 +0900 Subject: [PATCH 1/7] fix!: Validation rule with * gets incorrect values as dot array syntax --- system/Validation/Validation.php | 45 ++++++++++++++++------ tests/system/Validation/ValidationTest.php | 12 +++--- 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/system/Validation/Validation.php b/system/Validation/Validation.php index ad95cde7bc6b..f8c64e63ab3c 100644 --- a/system/Validation/Validation.php +++ b/system/Validation/Validation.php @@ -168,12 +168,21 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup } if (strpos($field, '*') !== false) { - $values = array_filter(array_flatten_with_dots($data), static fn ($key) => preg_match( - '/^' - . str_replace(['\.\*', '\*\.'], ['\..+', '.+\.'], preg_quote($field, '/')) - . '$/', - $key - ), ARRAY_FILTER_USE_KEY); + $flattenedArray = array_flatten_with_dots($data); + + $pattern = '/\A' + . str_replace( + ['\.\*', '\*\.'], + ['\.[^.]+', '[^.]+\.'], + preg_quote($field, '/') + ) + . '\z/'; + $values = array_filter( + $flattenedArray, + static fn ($key) => preg_match($pattern, $key), + ARRAY_FILTER_USE_KEY + ); + // if keys not found $values = $values ?: [$field => null]; } else { @@ -814,7 +823,13 @@ private function retrievePlaceholders(string $rule, array $data): array */ public function hasError(string $field): bool { - $pattern = '/^' . str_replace('\.\*', '\..+', preg_quote($field, '/')) . '$/'; + $pattern = '/\A' + . str_replace( + ['\.\*', '\*\.'], + ['\.[^.]+', '[^.]+\.'], + preg_quote($field, '/') + ) + . '\z/'; return (bool) preg_grep($pattern, array_keys($this->getErrors())); } @@ -829,10 +844,18 @@ public function getError(?string $field = null): string $field = array_key_first($this->rules); } - $errors = array_filter($this->getErrors(), static fn ($key) => preg_match( - '/^' . str_replace(['\.\*', '\*\.'], ['\..+', '.+\.'], preg_quote($field, '/')) . '$/', - $key - ), ARRAY_FILTER_USE_KEY); + $pattern = '/\A' + . str_replace( + ['\.\*', '\*\.'], + ['\.[^.]+', '[^.]+\.'], + preg_quote($field, '/') + ) + . '\z/'; + $errors = array_filter( + $this->getErrors(), + static fn ($key) => preg_match($pattern, $key), + ARRAY_FILTER_USE_KEY + ); return $errors === [] ? '' : implode("\n", $errors); } diff --git a/tests/system/Validation/ValidationTest.php b/tests/system/Validation/ValidationTest.php index e015c8b21de9..1c42b263b499 100644 --- a/tests/system/Validation/ValidationTest.php +++ b/tests/system/Validation/ValidationTest.php @@ -1111,9 +1111,9 @@ public function testRulesForSingleRuleWithAsteriskWillReturnError(): void $request = new IncomingRequest($config, new URI(), 'php://input', new UserAgent()); $this->validation->setRules([ - 'id_user.*' => 'numeric', - 'name_user.*' => 'alpha', - 'contacts.*.name' => 'required', + 'id_user.*' => 'numeric', + 'name_user.*' => 'alpha', + 'contacts.friends.*.name' => 'required', ]); $this->validation->withRequest($request->withMethod('post'))->run(); @@ -1121,7 +1121,7 @@ public function testRulesForSingleRuleWithAsteriskWillReturnError(): void 'id_user.0' => 'The id_user.* field must contain only numbers.', 'name_user.0' => 'The name_user.* field may only contain alphabetical characters.', 'name_user.2' => 'The name_user.* field may only contain alphabetical characters.', - 'contacts.friends.0.name' => 'The contacts.*.name field is required.', + 'contacts.friends.0.name' => 'The contacts.friends.*.name field is required.', ], $this->validation->getErrors()); $this->assertSame( @@ -1130,8 +1130,8 @@ public function testRulesForSingleRuleWithAsteriskWillReturnError(): void $this->validation->getError('name_user.*') ); $this->assertSame( - 'The contacts.*.name field is required.', - $this->validation->getError('contacts.*.name') + 'The contacts.friends.*.name field is required.', + $this->validation->getError('contacts.friends.*.name') ); } From 340e28efb49f4a2e053e4dfe9a076be57a4d24c0 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 31 Oct 2023 09:12:27 +0900 Subject: [PATCH 2/7] docs: fix incorrect dot array syntax keys --- user_guide_src/source/libraries/validation.rst | 6 +++--- user_guide_src/source/libraries/validation/009.php | 7 +------ user_guide_src/source/libraries/validation/010.php | 2 +- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/user_guide_src/source/libraries/validation.rst b/user_guide_src/source/libraries/validation.rst index 0288090b6f6f..7300fa88ff56 100644 --- a/user_guide_src/source/libraries/validation.rst +++ b/user_guide_src/source/libraries/validation.rst @@ -591,7 +591,7 @@ If you need to retrieve all error messages for failed fields, you can use the `` If no errors exist, an empty array will be returned. -When using a wildcard, the error will point to a specific field, replacing the asterisk with the appropriate key/keys:: +When using a wildcard (``*``), the error will point to a specific field, replacing the asterisk with the appropriate key/keys:: // for data 'contacts' => [ @@ -606,10 +606,10 @@ When using a wildcard, the error will point to a specific field, replacing the a ] // rule - 'contacts.*.name' => 'required' + 'contacts.friends.*.name' => 'required' // error will be - 'contacts.friends.1.name' => 'The contacts.*.name field is required.' + 'contacts.friends.1.name' => 'The contacts.friends.*.name field is required.' Getting a Single Error ====================== diff --git a/user_guide_src/source/libraries/validation/009.php b/user_guide_src/source/libraries/validation/009.php index 7e321e9664ff..85be52b7abb2 100644 --- a/user_guide_src/source/libraries/validation/009.php +++ b/user_guide_src/source/libraries/validation/009.php @@ -4,7 +4,7 @@ * The data to test: * [ * 'contacts' => [ - * 'name' => 'Joe Smith', + * 'name' => 'Joe Smith', * 'friends' => [ * [ * 'name' => 'Fred Flinstone', @@ -21,8 +21,3 @@ $validation->setRules([ 'contacts.name' => 'required|max_length[60]', ]); - -// Fred Flintsone & Wilma -$validation->setRules([ - 'contacts.friends.name' => 'required|max_length[60]', -]); diff --git a/user_guide_src/source/libraries/validation/010.php b/user_guide_src/source/libraries/validation/010.php index 0951f582c882..2646fcc32b99 100644 --- a/user_guide_src/source/libraries/validation/010.php +++ b/user_guide_src/source/libraries/validation/010.php @@ -2,5 +2,5 @@ // Fred Flintsone & Wilma $validation->setRules([ - 'contacts.*.name' => 'required|max_length[60]', + 'contacts.friends.*.name' => 'required|max_length[60]', ]); From 1b48e13064f95b817aac9e6088227797fdb774e0 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 31 Oct 2023 09:12:55 +0900 Subject: [PATCH 3/7] docs: fix text decoration --- user_guide_src/source/libraries/validation.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/user_guide_src/source/libraries/validation.rst b/user_guide_src/source/libraries/validation.rst index 7300fa88ff56..e1b0161ff714 100644 --- a/user_guide_src/source/libraries/validation.rst +++ b/user_guide_src/source/libraries/validation.rst @@ -830,7 +830,8 @@ alpha_numeric_punct No Fails if field contains anything other than alphanumeric, space, or this limited set of punctuation characters: ``~`` (tilde), ``!`` (exclamation), ``#`` (number), - ``$`` (dollar), ``% (percent), & (ampersand), + ``$`` (dollar), ``%`` (percent), + ``&`` (ampersand), ``*`` (asterisk), ``-`` (dash), ``_`` (underscore), ``+`` (plus), ``=`` (equals), ``|`` (vertical bar), From 77a17aac29c76c66019b61e31a5906072909d089 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 31 Oct 2023 09:27:04 +0900 Subject: [PATCH 4/7] test: rename test method We use the word asterisk here. --- tests/system/Validation/ValidationTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/system/Validation/ValidationTest.php b/tests/system/Validation/ValidationTest.php index 1c42b263b499..7f11b86d916a 100644 --- a/tests/system/Validation/ValidationTest.php +++ b/tests/system/Validation/ValidationTest.php @@ -1228,17 +1228,17 @@ public function testTranslatedLabelTagReplacement(): void } /** - * @dataProvider provideDotNotationOnIfExistRule + * @dataProvider provideIfExistRuleWithAsterisk * * @see https://github.com/codeigniter4/CodeIgniter4/issues/4521 */ - public function testDotNotationOnIfExistRule(bool $expected, array $rules, array $data): void + public function testIfExistRuleWithAsterisk(bool $expected, array $rules, array $data): void { $actual = $this->validation->setRules($rules)->run($data); $this->assertSame($expected, $actual); } - public static function provideDotNotationOnIfExistRule(): iterable + public static function provideIfExistRuleWithAsterisk(): iterable { yield 'dot-on-end-fail' => [ false, @@ -1613,7 +1613,7 @@ public function testRuleWithLeadingAsterisk(): void /** * @see https://github.com/codeigniter4/CodeIgniter4/issues/5942 */ - public function testRequireWithoutWithWildCard(): void + public function testRequireWithoutWithAsterisk(): void { $data = [ 'a' => [ From 09e194dc591633dbac0aad96a3987ff671e4c3f4 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 31 Oct 2023 09:43:51 +0900 Subject: [PATCH 5/7] test: add test for multidimensinal array and `*` --- tests/system/Validation/ValidationTest.php | 46 ++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/system/Validation/ValidationTest.php b/tests/system/Validation/ValidationTest.php index 7f11b86d916a..7cc6f99c40d5 100644 --- a/tests/system/Validation/ValidationTest.php +++ b/tests/system/Validation/ValidationTest.php @@ -1631,4 +1631,50 @@ public function testRequireWithoutWithAsterisk(): void $this->validation->getError('a.1.c') ); } + + /** + * @see https://github.com/codeigniter4/CodeIgniter4/issues/8128 + */ + public function testRuleWithAsteriskToMultiDimensionalArray(): void + { + $data = [ + 'contacts' => [ + 'name' => 'Joe Smith', + 'just' => [ + 'friends' => [ + [ + 'name' => 'Fred Flinstone', + ], + [ + 'name' => 'Wilma', + ], + ], + ], + ], + ]; + + $this->validation->setRules( + ['contacts.just.friends.*.name' => 'required|max_length[1]'] + ); + $this->assertFalse($this->validation->run($data)); + $this->assertSame( + [ + 'contacts.just.friends.0.name' => 'The contacts.just.friends.*.name field cannot exceed 1 characters in length.', + 'contacts.just.friends.1.name' => 'The contacts.just.friends.*.name field cannot exceed 1 characters in length.', + ], + $this->validation->getErrors() + ); + + $this->validation->reset(); + $this->validation->setRules( + ['contacts.*.name' => 'required|max_length[1]'] + ); + $this->assertFalse($this->validation->run($data)); + $this->assertSame( + // The data for `contacts.*.name` does not exist. So it is interpreted + // as `null`, and this error message returns. + ['contacts.*.name' => 'The contacts.*.name field is required.'], + $this->validation->getErrors() + ); + } } From 410b7a9041bd33001122079f312e592a6b74683f Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 31 Oct 2023 10:54:51 +0900 Subject: [PATCH 6/7] docs: add changelog and upgrade --- user_guide_src/source/changelogs/v4.4.4.rst | 7 ++++ .../source/installation/upgrade_444.rst | 19 ++++++++++ .../source/installation/upgrade_444/001.php | 38 +++++++++++++++++++ .../source/libraries/validation.rst | 6 +++ 4 files changed, 70 insertions(+) create mode 100644 user_guide_src/source/installation/upgrade_444/001.php diff --git a/user_guide_src/source/changelogs/v4.4.4.rst b/user_guide_src/source/changelogs/v4.4.4.rst index 218c56e738d7..cfb0aeb4f121 100644 --- a/user_guide_src/source/changelogs/v4.4.4.rst +++ b/user_guide_src/source/changelogs/v4.4.4.rst @@ -14,6 +14,13 @@ Release Date: Unreleased BREAKING ******** +Validation with Dot Array Syntax +================================ + +A validation rule with the wildcard ``*`` now validates only data in correct +dimensions as "dot array syntax". +See :ref:`Upgrading ` for details. + *************** Message Changes *************** diff --git a/user_guide_src/source/installation/upgrade_444.rst b/user_guide_src/source/installation/upgrade_444.rst index 6dbfa8e7baac..c0b4def6e34d 100644 --- a/user_guide_src/source/installation/upgrade_444.rst +++ b/user_guide_src/source/installation/upgrade_444.rst @@ -20,6 +20,25 @@ Mandatory File Changes Breaking Changes **************** +.. _upgrade-444-validation-with-dot-array-syntax: + +Validation with Dot Array Syntax +================================ + +If you are using :ref:`dot array syntax ` in validation +rules, a bug where ``*`` would validate data in incorrect dimensions has been fixed. + +In previous versions, the rule key ``contacts.*.name`` captured data with any +level like ``contacts.*.name``, ``contacts.*.*.name``, ``contacts.*.*.*.name``, +etc., incorrectly. + +The following code explains details: + +.. literalinclude:: upgrade_444/001.php + :lines: 2- + +If you have code that depends on the bug, fix the the rule key. + ********************* Breaking Enhancements ********************* diff --git a/user_guide_src/source/installation/upgrade_444/001.php b/user_guide_src/source/installation/upgrade_444/001.php new file mode 100644 index 000000000000..9bcc6412141f --- /dev/null +++ b/user_guide_src/source/installation/upgrade_444/001.php @@ -0,0 +1,38 @@ + [ + 'name' => 'Joe Smith', + 'just' => [ + 'friends' => [ + ['name' => 'SATO Taro'], + ['name' => 'Li Ming'], + ['name' => 'Heinz Müller'], + ], + ], + ], +]; + +$validation->setRules( + ['contacts.*.name' => 'required|max_length[8]'] +); + +$validation->run($data); // false + +d($validation->getErrors()); +/* + Before: Captured `contacts.*.*.*.name` incorrectly. + [ + contacts.just.friends.0.name => "The contacts.*.name field cannot exceed 8 characters in length.", + contacts.just.friends.2.name => "The contacts.*.name field cannot exceed 8 characters in length.", + ] + + After: Captures no data for `contacts.*.name`. + [ + contacts.*.name => string (38) "The contacts.*.name field is required.", + ] +*/ diff --git a/user_guide_src/source/libraries/validation.rst b/user_guide_src/source/libraries/validation.rst index e1b0161ff714..8ebec266c0bf 100644 --- a/user_guide_src/source/libraries/validation.rst +++ b/user_guide_src/source/libraries/validation.rst @@ -314,6 +314,8 @@ To give a labeled error message you can set up as: .. note:: ``setRules()`` will overwrite any rules that were set previously. To add more than one rule to an existing set of rules, use ``setRule()`` multiple times. +.. _validation-dot-array-syntax: + Setting Rules for Array Data ============================ @@ -328,6 +330,10 @@ You can use the ``*`` wildcard symbol to match any one level of the array: .. literalinclude:: validation/010.php :lines: 2- +.. note:: Prior to v4.4.4, due to a bug, the wildcard ``*`` validated data in incorrect + dimensions. See :ref:`Upgrading ` + for details. + "dot array syntax" can also be useful when you have single dimension array data. For example, data returned by multi select dropdown: From 4913acc1d6bab342e45a95fe14afd1866e3b23c9 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 31 Oct 2023 11:22:33 +0900 Subject: [PATCH 7/7] refactor: extract getRegex() --- system/Validation/Validation.php | 42 +++++++++++++------------------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/system/Validation/Validation.php b/system/Validation/Validation.php index f8c64e63ab3c..b1612481b97b 100644 --- a/system/Validation/Validation.php +++ b/system/Validation/Validation.php @@ -170,16 +170,9 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup if (strpos($field, '*') !== false) { $flattenedArray = array_flatten_with_dots($data); - $pattern = '/\A' - . str_replace( - ['\.\*', '\*\.'], - ['\.[^.]+', '[^.]+\.'], - preg_quote($field, '/') - ) - . '\z/'; $values = array_filter( $flattenedArray, - static fn ($key) => preg_match($pattern, $key), + static fn ($key) => preg_match(self::getRegex($field), $key), ARRAY_FILTER_USE_KEY ); @@ -220,6 +213,20 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup return false; } + /** + * Returns regex pattern for key with dot array syntax. + */ + private static function getRegex(string $field): string + { + return '/\A' + . str_replace( + ['\.\*', '\*\.'], + ['\.[^.]+', '[^.]+\.'], + preg_quote($field, '/') + ) + . '\z/'; + } + /** * Runs the validation process, returning true or false determining whether * validation was successful or not. @@ -823,15 +830,7 @@ private function retrievePlaceholders(string $rule, array $data): array */ public function hasError(string $field): bool { - $pattern = '/\A' - . str_replace( - ['\.\*', '\*\.'], - ['\.[^.]+', '[^.]+\.'], - preg_quote($field, '/') - ) - . '\z/'; - - return (bool) preg_grep($pattern, array_keys($this->getErrors())); + return (bool) preg_grep(self::getRegex($field), array_keys($this->getErrors())); } /** @@ -844,16 +843,9 @@ public function getError(?string $field = null): string $field = array_key_first($this->rules); } - $pattern = '/\A' - . str_replace( - ['\.\*', '\*\.'], - ['\.[^.]+', '[^.]+\.'], - preg_quote($field, '/') - ) - . '\z/'; $errors = array_filter( $this->getErrors(), - static fn ($key) => preg_match($pattern, $key), + static fn ($key) => preg_match(self::getRegex($field), $key), ARRAY_FILTER_USE_KEY );