Viewing file: Validation.php (23.13 KB) -rw-r--r-- Select action/file-type: (+) | (+) | (+) | Code (+) | Session (+) | (+) | SDB (+) | (+) | (+) | (+) | (+) | (+) |
<?php
/** * This file is part of CodeIgniter 4 framework. * * (c) CodeIgniter Foundation <admin@codeigniter.com> * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */
namespace CodeIgniter\Validation;
use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\Validation\Exceptions\ValidationException; use CodeIgniter\View\RendererInterface; use Config\Validation as ValidationConfig; use InvalidArgumentException; use TypeError;
/** * Validator */ class Validation implements ValidationInterface { /** * Files to load with validation functions. * * @var array */ protected $ruleSetFiles;
/** * The loaded instances of our validation files. * * @var array */ protected $ruleSetInstances = [];
/** * Stores the actual rules that should * be ran against $data. * * @var array */ protected $rules = [];
/** * The data that should be validated, * where 'key' is the alias, with value. * * @var array */ protected $data = [];
/** * Any generated errors during validation. * 'key' is the alias, 'value' is the message. * * @var array */ protected $errors = [];
/** * Stores custom error message to use * during validation. Where 'key' is the alias. * * @var array */ protected $customErrors = [];
/** * Our configuration. * * @var ValidationConfig */ protected $config;
/** * The view renderer used to render validation messages. * * @var RendererInterface */ protected $view;
/** * Validation constructor. * * @param ValidationConfig $config */ public function __construct($config, RendererInterface $view) { $this->ruleSetFiles = $config->ruleSets;
$this->config = $config;
$this->view = $view; }
/** * Runs the validation process, returning true/false determining whether * validation was successful or not. * * @param array|null $data The array of data to validate. * @param string|null $group The predefined group of rules to apply. * @param string|null $dbGroup The database group to use. */ public function run(?array $data = null, ?string $group = null, ?string $dbGroup = null): bool { // If there are still validation errors for redirect_with_input request, remove them. // See `getErrors()` method. if (isset($_SESSION, $_SESSION['_ci_validation_errors'])) { unset($_SESSION['_ci_validation_errors']); }
$data ??= $this->data;
// i.e. is_unique $data['DBGroup'] = $dbGroup;
$this->loadRuleSets(); $this->loadRuleGroup($group);
// If no rules exist, we return false to ensure // the developer didn't forget to set the rules. if (empty($this->rules)) { return false; }
// Replace any placeholders (e.g. {id}) in the rules with // the value found in $data, if any. $this->rules = $this->fillPlaceholders($this->rules, $data);
// Need this for searching arrays in validation. helper('array');
// Run through each rule. If we have any field set for // this rule, then we need to run them through! foreach ($this->rules as $field => $setup) { // Blast $rSetup apart, unless it's already an array. $rules = $setup['rules'] ?? $setup;
if (is_string($rules)) { $rules = $this->splitRules($rules); }
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); // if keys not found $values = $values ?: [$field => null]; } else { $values = dot_array_search($field, $data); }
if ($values === []) { // We'll process the values right away if an empty array $this->processRules($field, $setup['label'] ?? $field, $values, $rules, $data);
continue; }
if (strpos($field, '*') !== false) { // Process multiple fields foreach ($values as $dotField => $value) { $this->processRules($dotField, $setup['label'] ?? $field, $value, $rules, $data); } } else { // Process single field $this->processRules($field, $setup['label'] ?? $field, $values, $rules, $data); } }
return $this->getErrors() === []; }
/** * Runs the validation process, returning true or false * determining whether validation was successful or not. * * @param mixed $value * @param string[] $errors */ public function check($value, string $rule, array $errors = []): bool { $this->reset();
return $this->setRule('check', null, $rule, $errors)->run(['check' => $value]); }
/** * Runs all of $rules against $field, until one fails, or * all of them have been processed. If one fails, it adds * the error to $this->errors and moves on to the next, * so that we can collect all of the first errors. * * @param array|string $value * @param array|null $rules * @param array $data */ protected function processRules(string $field, ?string $label, $value, $rules = null, ?array $data = null): bool { if ($data === null) { throw new InvalidArgumentException('You must supply the parameter: data.'); }
if (in_array('if_exist', $rules, true)) { $flattenedData = array_flatten_with_dots($data); $ifExistField = $field;
if (strpos($field, '.*') !== false) { // We'll change the dot notation into a PCRE pattern that can be used later $ifExistField = str_replace('\.\*', '\.(?:[^\.]+)', preg_quote($field, '/')); $dataIsExisting = false; $pattern = sprintf('/%s/u', $ifExistField);
foreach (array_keys($flattenedData) as $item) { if (preg_match($pattern, $item) === 1) { $dataIsExisting = true; break; } } } else { $dataIsExisting = array_key_exists($ifExistField, $flattenedData); }
unset($ifExistField, $flattenedData);
if (! $dataIsExisting) { // we return early if `if_exist` is not satisfied. we have nothing to do here. return true; }
// Otherwise remove the if_exist rule and continue the process $rules = array_diff($rules, ['if_exist']); }
if (in_array('permit_empty', $rules, true)) { if ( ! in_array('required', $rules, true) && (is_array($value) ? $value === [] : trim((string) $value) === '') ) { $passed = true;
foreach ($rules as $rule) { if (preg_match('/(.*?)\[(.*)\]/', $rule, $match)) { $rule = $match[1]; $param = $match[2];
if (! in_array($rule, ['required_with', 'required_without'], true)) { continue; }
// Check in our rulesets foreach ($this->ruleSetInstances as $set) { if (! method_exists($set, $rule)) { continue; }
$passed = $passed && $set->{$rule}($value, $param, $data); break; } } }
if ($passed === true) { return true; } }
$rules = array_diff($rules, ['permit_empty']); }
foreach ($rules as $rule) { $isCallable = is_callable($rule);
$passed = false; $param = false;
if (! $isCallable && preg_match('/(.*?)\[(.*)\]/', $rule, $match)) { $rule = $match[1]; $param = $match[2]; }
// Placeholder for custom errors from the rules. $error = null;
// If it's a callable, call and get out of here. if ($isCallable) { $passed = $param === false ? $rule($value) : $rule($value, $param, $data); } else { $found = false;
// Check in our rulesets foreach ($this->ruleSetInstances as $set) { if (! method_exists($set, $rule)) { continue; }
$found = true; $passed = $param === false ? $set->{$rule}($value, $error) : $set->{$rule}($value, $param, $data, $error);
break; }
// If the rule wasn't found anywhere, we // should throw an exception so the developer can find it. if (! $found) { throw ValidationException::forRuleNotFound($rule); } }
// Set the error message if we didn't survive. if ($passed === false) { // if the $value is an array, convert it to as string representation if (is_array($value)) { $value = '[' . implode(', ', $value) . ']'; } elseif (is_object($value)) { $value = json_encode($value); }
$param = ($param === false) ? '' : $param;
$this->errors[$field] = $error ?? $this->getErrorMessage( $rule, $field, $label, $param, (string) $value );
return false; } }
return true; }
/** * Takes a Request object and grabs the input data to use from its * array values. */ public function withRequest(RequestInterface $request): ValidationInterface { /** @var IncomingRequest $request */ if (strpos($request->getHeaderLine('Content-Type'), 'application/json') !== false) { $this->data = $request->getJSON(true);
return $this; }
if (in_array(strtolower($request->getMethod()), ['put', 'patch', 'delete'], true) && strpos($request->getHeaderLine('Content-Type'), 'multipart/form-data') === false ) { $this->data = $request->getRawInput(); } else { $this->data = $request->getVar() ?? []; }
return $this; }
/** * Sets an individual rule and custom error messages for a single field. * * The custom error message should be just the messages that apply to * this field, like so: * * [ * 'rule' => 'message', * 'rule' => 'message' * ] * * @param array|string $rules * * @throws TypeError * * @return $this */ public function setRule(string $field, ?string $label, $rules, array $errors = []) { if (! is_array($rules) && ! is_string($rules)) { throw new TypeError('$rules must be of type string|array'); }
$ruleSet = [ $field => [ 'label' => $label, 'rules' => $rules, ], ];
if ($errors) { $ruleSet[$field]['errors'] = $errors; }
$this->setRules($ruleSet + $this->getRules());
return $this; }
/** * Stores the rules that should be used to validate the items. * Rules should be an array formatted like: * * [ * 'field' => 'rule1|rule2' * ] * * The $errors array should be formatted like: * [ * 'field' => [ * 'rule' => 'message', * 'rule' => 'message * ], * ] * * @param array $errors // An array of custom error messages */ public function setRules(array $rules, array $errors = []): ValidationInterface { $this->customErrors = $errors;
foreach ($rules as $field => &$rule) { if (is_array($rule)) { if (array_key_exists('errors', $rule)) { $this->customErrors[$field] = $rule['errors']; unset($rule['errors']); }
// if $rule is already a rule collection, just move it to "rules" // transforming [foo => [required, foobar]] to [foo => [rules => [required, foobar]]] if (! array_key_exists('rules', $rule)) { $rule = ['rules' => $rule]; } } }
$this->rules = $rules;
return $this; }
/** * Returns all of the rules currently defined. */ public function getRules(): array { return $this->rules; }
/** * Checks to see if the rule for key $field has been set or not. */ public function hasRule(string $field): bool { return array_key_exists($field, $this->rules); }
/** * Get rule group. * * @param string $group Group. * * @throws InvalidArgumentException If group not found. * * @return string[] Rule group. */ public function getRuleGroup(string $group): array { if (! isset($this->config->{$group})) { throw ValidationException::forGroupNotFound($group); }
if (! is_array($this->config->{$group})) { throw ValidationException::forGroupNotArray($group); }
return $this->config->{$group}; }
/** * Set rule group. * * @param string $group Group. * * @throws InvalidArgumentException If group not found. */ public function setRuleGroup(string $group) { $rules = $this->getRuleGroup($group); $this->setRules($rules);
$errorName = $group . '_errors'; if (isset($this->config->{$errorName})) { $this->customErrors = $this->config->{$errorName}; } }
/** * Returns the rendered HTML of the errors as defined in $template. */ public function listErrors(string $template = 'list'): string { if (! array_key_exists($template, $this->config->templates)) { throw ValidationException::forInvalidTemplate($template); }
return $this->view ->setVar('errors', $this->getErrors()) ->render($this->config->templates[$template]); }
/** * Displays a single error in formatted HTML as defined in the $template view. */ public function showError(string $field, string $template = 'single'): string { if (! array_key_exists($field, $this->getErrors())) { return ''; }
if (! array_key_exists($template, $this->config->templates)) { throw ValidationException::forInvalidTemplate($template); }
return $this->view ->setVar('error', $this->getError($field)) ->render($this->config->templates[$template]); }
/** * Loads all of the rulesets classes that have been defined in the * Config\Validation and stores them locally so we can use them. */ protected function loadRuleSets() { if (empty($this->ruleSetFiles)) { throw ValidationException::forNoRuleSets(); }
foreach ($this->ruleSetFiles as $file) { $this->ruleSetInstances[] = new $file(); } }
/** * Loads custom rule groups (if set) into the current rules. * * Rules can be pre-defined in Config\Validation and can * be any name, but must all still be an array of the * same format used with setRules(). Additionally, check * for {group}_errors for an array of custom error messages. * * @return array|ValidationException|null */ public function loadRuleGroup(?string $group = null) { if (empty($group)) { return null; }
if (! isset($this->config->{$group})) { throw ValidationException::forGroupNotFound($group); }
if (! is_array($this->config->{$group})) { throw ValidationException::forGroupNotArray($group); }
$this->setRules($this->config->{$group});
// If {group}_errors exists in the config file, // then override our custom errors with them. $errorName = $group . '_errors';
if (isset($this->config->{$errorName})) { $this->customErrors = $this->config->{$errorName}; }
return $this->rules; }
/** * Replace any placeholders within the rules with the values that * match the 'key' of any properties being set. For example, if * we had the following $data array: * * [ 'id' => 13 ] * * and the following rule: * * 'required|is_unique[users,email,id,{id}]' * * The value of {id} would be replaced with the actual id in the form data: * * 'required|is_unique[users,email,id,13]' */ protected function fillPlaceholders(array $rules, array $data): array { $replacements = [];
foreach ($data as $key => $value) { $replacements["{{$key}}"] = $value; }
if ($replacements !== []) { foreach ($rules as &$rule) { $ruleSet = $rule['rules'] ?? $rule;
if (is_array($ruleSet)) { foreach ($ruleSet as &$row) { if (is_string($row)) { $row = strtr($row, $replacements); } } }
if (is_string($ruleSet)) { $ruleSet = strtr($ruleSet, $replacements); }
if (isset($rule['rules'])) { $rule['rules'] = $ruleSet; } else { $rule = $ruleSet; } } }
return $rules; }
/** * Checks to see if an error exists for the given field. */ public function hasError(string $field): bool { $pattern = '/^' . str_replace('\.\*', '\..+', preg_quote($field, '/')) . '$/';
return (bool) preg_grep($pattern, array_keys($this->getErrors())); }
/** * Returns the error(s) for a specified $field (or empty string if not * set). */ public function getError(?string $field = null): string { if ($field === null && count($this->rules) === 1) { $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);
return $errors === [] ? '' : implode("\n", $errors); }
/** * Returns the array of errors that were encountered during * a run() call. The array should be in the following format: * * [ * 'field1' => 'error message', * 'field2' => 'error message', * ] * * @return array<string, string> * * @codeCoverageIgnore */ public function getErrors(): array { // If we already have errors, we'll use those. // If we don't, check the session to see if any were // passed along from a redirect_with_input request. if (empty($this->errors) && ! is_cli() && isset($_SESSION, $_SESSION['_ci_validation_errors'])) { $this->errors = unserialize($_SESSION['_ci_validation_errors']); }
return $this->errors ?? []; }
/** * Sets the error for a specific field. Used by custom validation methods. */ public function setError(string $field, string $error): ValidationInterface { $this->errors[$field] = $error;
return $this; }
/** * Attempts to find the appropriate error message * * @param string|null $value The value that caused the validation to fail. */ protected function getErrorMessage(string $rule, string $field, ?string $label = null, ?string $param = null, ?string $value = null): string { $param ??= '';
// Check if custom message has been defined by user if (isset($this->customErrors[$field][$rule])) { $message = lang($this->customErrors[$field][$rule]); } else { // Try to grab a localized version of the message... // lang() will return the rule name back if not found, // so there will always be a string being returned. $message = lang('Validation.' . $rule); }
$message = str_replace('{field}', empty($label) ? $field : lang($label), $message); $message = str_replace( '{param}', empty($this->rules[$param]['label']) ? $param : lang($this->rules[$param]['label']), $message );
return str_replace('{value}', $value ?? '', $message); }
/** * Split rules string by pipe operator. */ protected function splitRules(string $rules): array { if (strpos($rules, '|') === false) { return [$rules]; }
$string = $rules; $rules = []; $length = strlen($string); $cursor = 0;
while ($cursor < $length) { $pos = strpos($string, '|', $cursor);
if ($pos === false) { // we're in the last rule $pos = $length; }
$rule = substr($string, $cursor, $pos - $cursor);
while ( (substr_count($rule, '[') - substr_count($rule, '\[')) !== (substr_count($rule, ']') - substr_count($rule, '\]')) ) { // the pipe is inside the brackets causing the closing bracket to // not be included. so, we adjust the rule to include that portion. $pos = strpos($string, '|', $cursor + strlen($rule) + 1) ?: $length; $rule = substr($string, $cursor, $pos - $cursor); }
$rules[] = $rule; $cursor += strlen($rule) + 1; // +1 to exclude the pipe }
return array_unique($rules); }
/** * Resets the class to a blank slate. Should be called whenever * you need to process more than one array. */ public function reset(): ValidationInterface { $this->data = []; $this->rules = []; $this->errors = []; $this->customErrors = [];
return $this; } }
|