/application/libraries/Validation.php
PHP | 859 lines | 481 code | 122 blank | 256 comment | 68 complexity | 33dbd788aa7ff72828bc9c589dd806a2 MD5 | raw file
Possible License(s): LGPL-2.1
- <?php defined('SYSPATH') or die('No direct script access.');
- /**
- * Validation library.
- *
- * $Id: Validation.php 3127 2008-07-16 14:43:52Z Shadowhand $
- *
- * @package Validation
- * @author Kohana Team
- * @copyright (c) 2007-2008 Kohana Team
- * @license http://kohanaphp.com/license.html
- */
- class Validation_Core extends ArrayObject {
- // Unique "any field" key
- protected $any_field;
- // Array fields
- protected $array_fields = array();
- // Filters
- protected $pre_filters = array();
- protected $post_filters = array();
- // Rules and callbacks
- protected $rules = array();
- protected $callbacks = array();
- // Rules that are allowed to run on empty fields
- protected $empty_rules = array('required', 'matches');
- // Errors
- protected $errors = array();
- protected $error_message_args = array();
- protected $messages = array();
- // Checks if there is data to validate.
- protected $submitted;
- /**
- * Whether CSRF validation has succeeded
- * @var bool
- */
- protected $csrf_validation_failed = FALSE;
- public static $is_api_request = FALSE;
- /**
- * Creates a new Validation instance.
- *
- * @param array array to use for validation
- * @return object
- */
- public static function factory($array = NULL)
- {
- return new Validation( ! is_array($array) ? $_POST : $array);
- }
- /**
- * Sets the unique "any field" key and creates an ArrayObject from the
- * passed array.
- *
- * @param array array to validate
- * @return void
- */
- public function __construct(array $array)
- {
- // Set a dynamic, unique "any field" key
- $this->any_field = uniqid(NULL, TRUE);
- // Test if there is any actual data
- $this->submitted = (count($array) > 0);
- parent::__construct($array, ArrayObject::ARRAY_AS_PROPS | ArrayObject::STD_PROP_LIST);
- }
- /**
- * Test if the data has been submitted.
- *
- * @return boolean
- */
- public function submitted($value = NULL)
- {
- if (is_bool($value))
- {
- $this->submitted = $value;
- }
- return $this->submitted;
- }
- /**
- * Returns the ArrayObject values.
- *
- * @return array
- */
- public function as_array()
- {
- return $this->getArrayCopy();
- }
- /**
- * Returns the ArrayObject values, removing all inputs without rules.
- * To choose specific inputs, list the field name as arguments.
- *
- * @return array
- */
- public function safe_array()
- {
- // All the fields that are being validated
- $all_fields = array_unique(array_merge
- (
- array_keys($this->pre_filters),
- array_keys($this->rules),
- array_keys($this->callbacks),
- array_keys($this->post_filters)
- ));
- // Load choices
- $choices = func_get_args();
- $choices = empty($choices) ? NULL : array_combine($choices, $choices);
- $safe = array();
- foreach ($all_fields as $i => $field)
- {
- // Ignore "any field" key
- if ($field === $this->any_field) continue;
- if (isset($this->array_fields[$field]))
- {
- // Use the key field
- $field = $this->array_fields[$field];
- }
- if ($choices === NULL OR isset($choices[$field]))
- {
- // Make sure all fields are defined
- $safe[$field] = isset($this[$field]) ? $this[$field] : NULL;
- }
- }
- return $safe;
- }
- /**
- * Add additional rules that will forced, even for empty fields. All arguments
- * passed will be appended to the list.
- *
- * @chainable
- * @param string rule name
- * @return object
- */
- public function allow_empty_rules($rules)
- {
- // Any number of args are supported
- $rules = func_get_args();
- // Merge the allowed rules
- $this->empty_rules = array_merge($this->empty_rules, $rules);
- return $this;
- }
- /**
- * Add a pre-filter to one or more inputs.
- *
- * @chainable
- * @param callback filter
- * @param string fields to apply filter to, use TRUE for all fields
- * @return object
- */
- public function pre_filter($filter, $field = TRUE)
- {
- if ( ! is_callable($filter))
- throw new Kohana_Exception('validation.filter_not_callable');
- $filter = (is_string($filter) AND strpos($filter, '::') !== FALSE) ? explode('::', $filter) : $filter;
- if ($field === TRUE)
- {
- // Handle "any field" filters
- $fields = array($this->any_field);
- }
- else
- {
- // Add the filter to specific inputs
- $fields = func_get_args();
- $fields = array_slice($fields, 1);
- }
- foreach ($fields as $field)
- {
- if (strpos($field, '.') > 0)
- {
- // Field keys
- $keys = explode('.', $field);
- // Add to array fields
- $this->array_fields[$field] = $keys[0];
- }
- // Add the filter to specified field
- $this->pre_filters[$field][] = $filter;
- }
- return $this;
- }
- /**
- * Add a post-filter to one or more inputs.
- *
- * @chainable
- * @param callback filter
- * @param string fields to apply filter to, use TRUE for all fields
- * @return object
- */
- public function post_filter($filter, $field = TRUE)
- {
- if ( ! is_callable($filter, TRUE))
- throw new Kohana_Exception('validation.filter_not_callable');
- $filter = (is_string($filter) AND strpos($filter, '::') !== FALSE) ? explode('::', $filter) : $filter;
- if ($field === TRUE)
- {
- // Handle "any field" filters
- $fields = array($this->any_field);
- }
- else
- {
- // Add the filter to specific inputs
- $fields = func_get_args();
- $fields = array_slice($fields, 1);
- }
- foreach ($fields as $field)
- {
- if (strpos($field, '.') > 0)
- {
- // Field keys
- $keys = explode('.', $field);
- // Add to array fields
- $this->array_fields[$field] = $keys[0];
- }
- // Add the filter to specified field
- $this->post_filters[$field][] = $filter;
- }
- return $this;
- }
- /**
- * Add rules to a field. Rules are callbacks or validation methods. Rules can
- * only return TRUE or FALSE.
- *
- * @chainable
- * @param string field name
- * @param callback rules (unlimited number)
- * @return object
- */
- public function add_rules($field, $rules)
- {
- // Handle "any field" filters
- ($field === TRUE) and $field = $this->any_field;
- // Get the rules
- $rules = func_get_args();
- $rules = array_slice($rules, 1);
- foreach ($rules as $rule)
- {
- // Rule arguments
- $args = NULL;
- if (is_string($rule))
- {
- if (preg_match('/^([^\[]++)\[(.+)\]$/', $rule, $matches))
- {
- // Split the rule into the function and args
- $rule = $matches[1];
- $args = preg_split('/(?<!\\\\),\s*/', $matches[2]);
- // Replace escaped comma with comma
- $args = str_replace('\,', ',', $args);
- }
- if (method_exists($this, $rule))
- {
- // Make the rule a valid callback
- $rule = array($this, $rule);
- }
- elseif (method_exists('valid', $rule))
- {
- // Make the rule a callback for the valid:: helper
- $rule = array('valid', $rule);
- }
- }
- if ( ! is_callable($rule, TRUE))
- throw new Kohana_Exception('validation.rule_not_callable');
- $rule = (is_string($rule) AND strpos($rule, '::') !== FALSE) ? explode('::', $rule) : $rule;
- if (strpos($field, '.') > 0)
- {
- // Field keys
- $keys = explode('.', $field);
- // Add to array fields
- $this->array_fields[$field] = $keys[0];
- }
- // Add the rule to specified field
- $this->rules[$field][] = array($rule, $args);
- }
- return $this;
- }
- /**
- * Add callbacks to a field. Callbacks must accept the Validation object
- * and the input name. Callback returns are not processed.
- *
- * @chainable
- * @param string field name
- * @param callbacks callbacks (unlimited number)
- * @return object
- */
- public function add_callbacks($field, $callbacks)
- {
- // Handle "any field" filters
- ($field === TRUE) and $field = $this->any_field;
- if (func_get_args() > 2)
- {
- // Multiple callback
- $callbacks = array_slice(func_get_args(), 1);
- }
- else
- {
- // Only one callback
- $callbacks = array($callbacks);
- }
- foreach ($callbacks as $callback)
- {
- if ( ! is_callable($callback, TRUE))
- throw new Kohana_Exception('validation.callback_not_callable');
- $callback = (is_string($callback) AND strpos($callback, '::') !== FALSE) ? explode('::', $callback) : $callback;
- if (strpos($field, '.') > 0)
- {
- // Field keys
- $keys = explode('.', $field);
- // Add to array fields
- $this->array_fields[$field] = $keys[0];
- }
- // Add the callback to specified field
- $this->callbacks[$field][] = $callback;
- }
- return $this;
- }
- /**
- * Validate by processing pre-filters, rules, callbacks, and post-filters.
- * All fields that have filters, rules, or callbacks will be initialized if
- * they are undefined. Validation will only be run if there is data already
- * in the array.
- *
- * @param bool $validate_csrf When TRUE, performs CSRF token validation
- * @return bool
- */
- public function validate($validate_csrf = TRUE)
- {
- // CSRF token field
- $csrf_token_key = 'form_auth_token';
- if (array_key_exists($csrf_token_key, $this))
- {
- unset ($this[$csrf_token_key]);
- }
- // Delete the CSRF token field if it's in the validation
- // rules
- if (array_key_exists($csrf_token_key, $this->callbacks))
- {
- unset ($this->callbacks[$csrf_token_key]);
- }
- elseif (array_key_exists($csrf_token_key, $this->rules))
- {
- unset ($this->rules[$csrf_token_key]);
- }
- // Disable CSRF for XHR
- // Same method as django CSRF protection:
- // http://michael-coates.blogspot.co.nz/2010/12/djangos-built-in-csrf-defense-for-ajax.html
- if (request::is_ajax())
- {
- $validate_csrf = FALSE;
- }
- // Perform CSRF validation for all HTTP POST requests
- // where CSRF validation is enabled and the request
- // was not submitted via the API
- if ($_POST AND $validate_csrf AND ! Validation::$is_api_request)
- {
- // Check if CSRF module is loaded
- if (in_array(MODPATH.'csrf', Kohana::config('config.modules')))
- {
- // Check for presence of CSRF token in HTTP POST payload
- $form_auth_token = (isset($_POST[$csrf_token_key]))
- ? $_POST[$csrf_token_key]
- // Generate invalid token
- : text::random('alnum', 10);
-
- // Validate the token
- if ( ! csrf::valid($form_auth_token))
- {
- Kohana::log('debug', 'Invalid CSRF token: '.$form_auth_token);
- Kohana::log('debug', 'Actual CSRF token: '.csrf::token());
- // Flag CSRF validation as having failed
- $this->csrf_validation_failed = TRUE;
- // Set the error message
- $this->errors[$csrf_token_key] = Kohana::lang('csrf.form_auth_token.error');
- return FALSE;
- }
- }
- }
- // All the fields that are being validated
- $all_fields = array_unique(array_merge
- (
- array_keys($this->pre_filters),
- array_keys($this->rules),
- array_keys($this->callbacks),
- array_keys($this->post_filters)
- ));
- // Copy the array from the object, to optimize multiple sets
- $object_array = $this->getArrayCopy();
- foreach ($all_fields as $i => $field)
- {
- if ($field === $this->any_field)
- {
- // Remove "any field" from the list of fields
- unset($all_fields[$i]);
- continue;
- }
- if (substr($field, -2) === '.*')
- {
- // Set the key to be an array
- Kohana::key_string_set($object_array, substr($field, 0, -2), array());
- }
- else
- {
- // Set the key to be NULL
- Kohana::key_string_set($object_array, $field, NULL);
- }
- }
- // Swap the array back into the object
- $this->exchangeArray($object_array);
- // Reset all fields to ALL defined fields
- $all_fields = array_keys($this->getArrayCopy());
- foreach ($this->pre_filters as $field => $calls)
- {
- foreach ($calls as $func)
- {
- if ($field === $this->any_field)
- {
- foreach ($all_fields as $f)
- {
- // Process each filter
- $this[$f] = is_array($this[$f]) ? arr::map_recursive($func, $this[$f]) : call_user_func($func, $this[$f]);
- }
- }
- else
- {
- // Process each filter
- $this[$field] = is_array($this[$field]) ? arr::map_recursive($func, $this[$field]) : call_user_func($func, $this[$field]);
- }
- }
- }
- if ($this->submitted === FALSE)
- return FALSE;
- foreach ($this->rules as $field => $calls)
- {
- foreach ($calls as $call)
- {
- // Split the rule into function and args
- list($func, $args) = $call;
- if ($field === $this->any_field)
- {
- foreach ($all_fields as $f)
- {
- if (isset($this->array_fields[$f]))
- {
- // Use the field key
- $f_key = $this->array_fields[$f];
- // Prevent other rules from running when this field already has errors
- if ( ! empty($this->errors[$f_key])) break;
- // Don't process rules on empty fields
- if ( ! in_array($func[1], $this->empty_rules, TRUE) AND $this[$f_key] == NULL)
- continue;
- foreach ($this[$f_key] as $k => $v)
- {
- if ( ! call_user_func($func, $this[$f_key][$k], $args))
- {
- // Run each rule
- $this->errors[$f_key] = is_array($func) ? $func[1] : $func;
- }
- }
- }
- else
- {
- // Prevent other rules from running when this field already has errors
- if ( ! empty($this->errors[$f])) break;
- // Don't process rules on empty fields
- if ( ! in_array($func[1], $this->empty_rules, TRUE) AND $this[$f] == NULL)
- continue;
- if ( ! call_user_func($func, $this[$f], $args))
- {
- // Run each rule
- $this->errors[$f] = is_array($func) ? $func[1] : $func;
- }
- }
- }
- }
- else
- {
- if (isset($this->array_fields[$field]))
- {
- // Use the field key
- $field_key = $this->array_fields[$field];
- // Prevent other rules from running when this field already has errors
- if ( ! empty($this->errors[$field_key])) break;
- // Don't process rules on empty fields
- if ( ! in_array($func[1], $this->empty_rules, TRUE) AND $this[$field_key] == NULL)
- continue;
- foreach ($this[$field_key] as $k => $val)
- {
- if ( ! call_user_func($func, $this[$field_key][$k], $args))
- {
- // Run each rule
- $this->errors[$field_key] = is_array($func) ? $func[1] : $func;
- // Stop after an error is found
- break 2;
- }
- }
- }
- else
- {
- // Prevent other rules from running when this field already has errors
- if ( ! empty($this->errors[$field])) break;
- // Don't process rules on empty fields
- if ( ! in_array($func[1], $this->empty_rules, TRUE) AND $this[$field] == NULL)
- continue;
- if ( ! call_user_func($func, $this[$field], $args))
- {
- // Run each rule
- $this->errors[$field] = is_array($func) ? $func[1] : $func;
- // Stop after an error is found
- break;
- }
- }
- }
- }
- }
- foreach ($this->callbacks as $field => $calls)
- {
- foreach ($calls as $func)
- {
- if ($field === $this->any_field)
- {
- foreach ($all_fields as $f)
- {
- // Execute the callback
- call_user_func($func, $this, $f);
- // Stop after an error is found
- if ( ! empty($errors[$f])) break 2;
- }
- }
- else
- {
- // Execute the callback
- call_user_func($func, $this, $field);
- // Stop after an error is found
- if ( ! empty($errors[$field])) break;
- }
- }
- }
- foreach ($this->post_filters as $field => $calls)
- {
- foreach ($calls as $func)
- {
- if ($field === $this->any_field)
- {
- foreach ($all_fields as $f)
- {
- if (isset($this->array_fields[$f]))
- {
- // Use the field key
- $f = $this->array_fields[$f];
- }
- // Process each filter
- $this[$f] = is_array($this[$f]) ? array_map($func, $this[$f]) : call_user_func($func, $this[$f]);
- }
- }
- else
- {
- if (isset($this->array_fields[$field]))
- {
- // Use the field key
- $field = $this->array_fields[$field];
- }
- // Process each filter
- $this[$field] = is_array($this[$field]) ? array_map($func, $this[$field]) : call_user_func($func, $this[$field]);
- }
- }
- }
- // Return TRUE if there are no errors
- return (count($this->errors) === 0);
- }
- /**
- * Add an error to an input.
- *
- * @chainable
- * @param string input name
- * @param string unique error name
- * @param array extra vars to pass to kohana::lang()
- * @return object
- */
- public function add_error($field, $name, $lang_vars = FALSE)
- {
- if (isset($this[$field]) OR $field == 'custom')
- {
- $this->errors[$field] = $name;
- // Save error message vars
- if ($lang_vars)
- {
- $this->error_message_args[$field] = $lang_vars;
- }
- }
- return $this;
- }
- /**
- * Sets or returns the message for an input.
- *
- * @chainable
- * @param string input key
- * @param string message to set
- * @return string|object
- */
- public function message($input = NULL, $message = NULL)
- {
- if ($message === NULL)
- {
- if ($input === NULL)
- {
- $messages = array();
- $keys = array_keys($this->messages);
- foreach ($keys as $input)
- {
- $messages[] = $this->message($input);
- }
- return implode("\n", $messages);
- }
- // Return nothing if no message exists
- if (empty($this->messages[$input]))
- return '';
- // Return the HTML message string
- return $this->messages[$input];
- }
- else
- {
- $this->messages[$input] = $message;
- }
- return $this;
- }
- /**
- * Return the errors array.
- *
- * @param boolean load errors from a lang file
- * @return array
- */
- public function errors($file = NULL)
- {
- if ($file === NULL)
- {
- return $this->errors;
- }
- else
- {
- $errors = array();
- foreach ($this->errors as $input => $error)
- {
- // Key for this input error
- $key = "$file.$input.$error";
-
- $message_vars = isset($this->error_message_args[$input]) ? $this->error_message_args[$input] : array();
- if (($errors[$input] = Kohana::lang($key, $message_vars)) === $key)
- {
- // Get the default error message. Note: commented out by BH
- //$errors[$input] = Kohana::lang("$file.$input.default");
- // Don't get the default message because we rarely set it. Pass the key since it will
- // provide more clues as to the problem than what we are currently providing. Also,
- // this allows "custom" inputs to pass through, bypassing localization by design
- //$errors[$input] = $key;
- }
- }
- // CSRF validation errors MUST always be returned
- if ($this->csrf_validation_failed)
- {
- $errors['form_auth_token'] = $this->errors['form_auth_token'];
- }
- return $errors;
- }
- }
- /**
- * Rule: required. Generates an error if the field has an empty value.
- *
- * @param mixed input value
- * @return bool
- */
- public function required($str)
- {
- return ! ($str === '' OR $str === NULL OR $str === FALSE OR (is_array($str) AND empty($str)));
- }
- /**
- * Rule: matches. Generates an error if the field does not match one or more
- * other fields.
- *
- * @param mixed input value
- * @param array input names to match against
- * @return bool
- */
- public function matches($str, array $inputs)
- {
- foreach ($inputs as $key)
- {
- if ($str !== (isset($this[$key]) ? $this[$key] : NULL))
- return FALSE;
- }
- return TRUE;
- }
- /**
- * Rule: length. Generates an error if the field is too long or too short.
- *
- * @param mixed input value
- * @param array minimum, maximum, or exact length to match
- * @return bool
- */
- public function length($str, array $length)
- {
- if ( ! is_string($str))
- return FALSE;
- $size = utf8::strlen($str);
- $status = FALSE;
- if (count($length) > 1)
- {
- list ($min, $max) = $length;
- if ($size >= $min AND $size <= $max)
- {
- $status = TRUE;
- }
- }
- else
- {
- $status = ($size === (int) $length[0]);
- }
- return $status;
- }
- /**
- * Rule: depends_on. Generates an error if the field does not depend on one
- * or more other fields.
- *
- * @param mixed field name
- * @param array field names to check dependency
- * @return bool
- */
- public function depends_on($field, array $fields)
- {
- foreach ($fields as $depends_on)
- {
- if ( ! isset($this[$depends_on]) OR $this[$depends_on] == NULL)
- return FALSE;
- }
- return TRUE;
- }
- /**
- * Rule: chars. Generates an error if the field contains characters outside of the list.
- *
- * @param string field value
- * @param array allowed characters
- * @return bool
- */
- public function chars($value, array $chars)
- {
- return ! preg_match('![^'.preg_quote(implode(',', $chars)).']!', $value);
- }
- } // End Validation