Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions src/Phaseolies/Http/Support/ValidationRules.php
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ protected function sanitizeUserRequest(
if ($this->isNullable($input, $fieldName) && $rule !== 'null') {
return null;
} else {
if ($this->rejectsNonScalarValue($rule, $input[$fieldName] ?? null)) {
return $this->getErrorMessage('string', $fieldName);
}

switch ($rule) {
case 'required':
if ($this->isEmptyFieldRequired($input, $fieldName)) {
Expand Down Expand Up @@ -1308,6 +1312,10 @@ protected function isDateFormat(array $input, string $fieldName, string $format)
{
$value = $input[$fieldName] ?? '';

if (is_string($value) && str_contains($value, "\0")) {
return false;
}

$date = \DateTime::createFromFormat($format, $value);

return $date !== false && $date->format($format) === $value;
Expand Down Expand Up @@ -1404,6 +1412,86 @@ protected function _removeRuleSuffix(string $string): string
return explode(":", $string)[0];
}

/**
* Split a "required|min:3|regex:/a|b/" rule string into its rules.
*
* @param string $ruleString
* @return array<int, string>
*/
protected function splitRules(string $ruleString): array
{
$pieces = explode('|', $ruleString);
$count = count($pieces);
$rules = [];

for ($i = 0; $i < $count; $i++) {
$rule = $pieces[$i];

if (str_starts_with($rule, 'regex:')) {
$joined = $rule;
$next = $i;

while (! $this->isRegexClosed(substr($joined, 6)) && $next + 1 < $count) {
$joined .= '|' . $pieces[++$next];
}

// An unterminated pattern keeps its own piece instead of swallowing
// the rules that follow it.
if ($this->isRegexClosed(substr($joined, 6))) {
$rule = $joined;
$i = $next;
}
}

$rules[] = $rule;
}

return $rules;
}

/**
* Determine if a regex pattern has its closing delimiter (and optional flags).
*
* @param string $pattern
* @return bool
*/
protected function isRegexClosed(string $pattern): bool
{
if (strlen($pattern) < 2) {
return false;
}

$closing = ['(' => ')', '[' => ']', '{' => '}', '<' => '>'][$pattern[0]] ?? $pattern[0];

// The closing delimiter must not be escaped, i.e. be preceded by an even
// number of backslashes.
return (bool) preg_match(
'/(?<!\\\\)(?:\\\\\\\\)*' . preg_quote($closing, '/') . '[a-zA-Z]*$/',
substr($pattern, 1)
);
}

/**
* Determine if a rule cannot be applied to the given array or object value.
*
* @param string $rule
* @param mixed $value
* @return bool
*/
protected function rejectsNonScalarValue(string $rule, mixed $value): bool
{
if (is_object($value)) {
return ! in_array($rule, ['required', 'array', 'string', 'null'], true);
}

return is_array($value) && in_array($rule, [
'min', 'max', 'date', 'gte', 'lte', 'gt', 'lt',
'alpha', 'alpha_num', 'alpha_dash', 'regex', 'phone', 'uuid',
'starts_with', 'ends_with', 'date_format', 'uppercase', 'lowercase', 'slug',
'digits', 'min_digits', 'max_digits', 'same_as', 'unique', 'exists_in',
], true);
}

/**
* Get the suffix from a rule string.
*
Expand Down
4 changes: 2 additions & 2 deletions src/Phaseolies/Http/Validation/Rule.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,12 @@ public function sanitize(array $rules): array|Response
continue;
}

$fieldRules = explode("|", $value);
$fieldRules = $this->splitRules($value);
foreach ($fieldRules as $rule) {
$ruleValue = $this->_getRuleSuffix($rule);
$rule = $this->_removeRuleSuffix($rule);
$errorMessage = $this->sanitizeUserRequest($input, $fieldName, $rule, $ruleValue);
if ($errorMessage) {
if ($errorMessage && ! in_array($errorMessage, $errors[$fieldName] ?? [], true)) {
$errors[$fieldName][] = $errorMessage;
}
}
Expand Down
9 changes: 7 additions & 2 deletions src/Phaseolies/Support/Validation/Sanitizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ public function request(array $data, array $rules): self
*/
public function validate(): bool
{
// Validating again must not stack the same errors on top of the old ones.
$this->errors = [];

foreach ($this->rules as $field => $ruleString) {
// Custom rule class bound via Bind::to(new Rule())->context([...])
if ($ruleString instanceof Bind) {
Expand All @@ -78,7 +81,7 @@ public function validate(): bool
continue;
}

$rulesArray = explode('|', $ruleString);
$rulesArray = $this->splitRules($ruleString);

foreach ($rulesArray as $rule) {
$this->applyRule($field, $rule);
Expand Down Expand Up @@ -170,6 +173,8 @@ protected function addError(string $field, string $message)
$this->errors[$field] = [];
}

$this->errors[$field][] = $message;
if (!in_array($message, $this->errors[$field], true)) {
$this->errors[$field][] = $message;
}
}
}
Loading
Loading