Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[9.x] Patch regex rule parsing due to Rule::forEach() #40941

Merged
merged 12 commits into from
Feb 11, 2022
23 changes: 18 additions & 5 deletions src/Illuminate/Validation/ValidationRuleParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,12 @@ protected function explodeRules($rules)
protected function explodeExplicitRule($rule, $attribute)
{
if (is_string($rule)) {
return explode('|', $rule);
} elseif (is_object($rule)) {
[$name] = static::parseStringRule($rule);

return explode('|', $rule, ...array_filter([static::ruleIsRegex($name)]));
Copy link
Contributor Author

@stevebauman stevebauman Feb 10, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've used the spread operator to completely omit the third ($limit) parameter if the rule is not regex.

If the rule is not a regex:

  1. false will be returned from ruleIsRegex()
  2. array_filter will then clear false out of the array, resulting in an empty array
  3. Spread operator will do nothing, as there are no elements of the array

I've added this, because explode() will take a null, false, or 0 parameter and set the $limit to 1.

We want $limit to be 1 when the rule is a regular expression, or $limit to be completely omitted when the rule is not.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we adjust this code to actually explicitly pass an integer to explode?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes absolutely, one moment.

Copy link
Contributor Author

@stevebauman stevebauman Feb 10, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've simplified it to:

return static::ruleIsRegex($name) ? [$rule] : explode('|', $rule);

Since the explode('|', $rule, 1) will simply wrap the rule in an array. Let me know if you want me to change this. If I wanted to explicitly pass in an integer value, I'd have to use PHP_INT_MAX for the default value (which looked a bit off -- not sure of your opinion on this):

return explode('|', $rule, static::ruleIsRegex($name) ? 1 : PHP_INT_MAX);

}

if (is_object($rule)) {
return Arr::wrap($this->prepareRule($rule, $attribute));
}

Expand Down Expand Up @@ -272,15 +276,24 @@ protected static function parseStringRule($rule)
*/
protected static function parseParameters($rule, $parameter)
{
$rule = strtolower($rule);

if (in_array($rule, ['regex', 'not_regex', 'notregex'], true)) {
if (static::ruleIsRegex($rule)) {
return [$parameter];
}

return str_getcsv($parameter);
}

/**
* Determine if the rule is a regular expression.
*
* @param string $rule
* @return bool
*/
protected static function ruleIsRegex($rule)
{
return in_array(strtolower($rule), ['regex', 'not_regex', 'notregex'], true);
}

/**
* Normalizes a rule so that we can accept short types.
*
Expand Down
56 changes: 56 additions & 0 deletions tests/Validation/ValidationForEachTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,62 @@ public function testForEachCallbacksCanReturnDifferentRules()
], $v->getMessageBag()->toArray());
}

public function testForEachCallbacksDoNotBreakRegexRules()
{
$data = [
'items' => [
['users' => [['type' => 'super'], ['type' => 'invalid']]],
],
];

$rules = [
'items.*' => Rule::forEach(function () {
return ['users.*.type' => 'regex:/^(super|admin)$/i'];
}),
];

$trans = $this->getIlluminateArrayTranslator();

$v = new Validator($trans, $data, $rules);

$this->assertFalse($v->passes());

$this->assertEquals([
'items.0.users.1.type' => ['validation.regex'],
], $v->getMessageBag()->toArray());
}

public function testForEachCallbacksCanContainMultipleRegexRules()
{
$data = [
'items' => [
['users' => [['type' => 'super'], ['type' => 'invalid']]],
],
];

$rules = [
'items.*' => Rule::forEach(function () {
return ['users.*.type' => [
'regex:/^(super)$/i',
'notregex:/^(invalid)$/i',
]];
}),
];

$trans = $this->getIlluminateArrayTranslator();

$v = new Validator($trans, $data, $rules);

$this->assertFalse($v->passes());

$this->assertEquals([
'items.0.users.1.type' => [
'validation.regex',
'validation.notregex',
],
], $v->getMessageBag()->toArray());
}

protected function getTranslator()
{
return m::mock(TranslatorContract::class);
Expand Down
36 changes: 36 additions & 0 deletions tests/Validation/ValidationRuleParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,42 @@ public function testEmptyConditionalRulesArePreserved()
], $rules);
}

public function testExplodeProperlyParsesSingleRegexRule()
{
$data = ['items' => [['type' => 'foo']]];

$exploded = (new ValidationRuleParser($data))->explode(
['items.*.type' => 'regex:/^(super|admin)$/i']
);

$this->assertEquals('regex:/^(super|admin)$/i', $exploded->rules['items.0.type'][0]);
}

public function testExplodeProperlyParsesRegexWithArrayOfRules()
{
$data = ['items' => [['type' => 'foo']]];

$exploded = (new ValidationRuleParser($data))->explode(
['items.*.type' => ['in:foo', 'regex:/^(super|admin)$/i']]
);

$this->assertEquals('in:foo', $exploded->rules['items.0.type'][0]);
$this->assertEquals('regex:/^(super|admin)$/i', $exploded->rules['items.0.type'][1]);
}

public function testExplodeFailsParsingRegexWithOtherRulesInSingleString()
{
$data = ['items' => [['type' => 'foo']]];

$exploded = (new ValidationRuleParser($data))->explode(
['items.*.type' => 'in:foo|regex:/^(super|admin)$/i']
);

$this->assertEquals('in:foo', $exploded->rules['items.0.type'][0]);
$this->assertEquals('regex:/^(super', $exploded->rules['items.0.type'][1]);
$this->assertEquals('admin)$/i', $exploded->rules['items.0.type'][2]);
}

public function testExplodeGeneratesNestedRules()
{
$parser = (new ValidationRuleParser([
Expand Down
3 changes: 3 additions & 0 deletions tests/Validation/ValidationValidatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3820,6 +3820,9 @@ public function testValidateRegex()

$v = new Validator($trans, ['x' => 12], ['x' => 'Regex:/^12$/i']);
$this->assertTrue($v->passes());

$v = new Validator($trans, ['x' => ['y' => ['z' => 'james']]], ['x.*.z' => ['Regex:/^(taylor|james)$/i']]);
$this->assertTrue($v->passes());
}

public function testValidateNotRegex()
Expand Down