%PDF-1.4
Directory : /var/www/vhosts/lautnusantara.com/httpdocs/mis/system/Validation/ |
<?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(), $this->customErrors); 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; } }