diff options
Diffstat (limited to 'kohana/libraries/Validation.php')
-rw-r--r-- | kohana/libraries/Validation.php | 773 |
1 files changed, 773 insertions, 0 deletions
diff --git a/kohana/libraries/Validation.php b/kohana/libraries/Validation.php new file mode 100644 index 00000000..172ff66e --- /dev/null +++ b/kohana/libraries/Validation.php @@ -0,0 +1,773 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Validation library. + * + * $Id$ + * + * @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 $messages = array(); + + // Checks if there is data to validate. + protected $submitted; + + /** + * 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. + * + * @return bool + */ + public function validate() + { + // 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 + * @return object + */ + public function add_error($field, $name) + { + if (isset($this[$field])) + { + $this->errors[$field] = $name; + } + + 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"; + + if (($errors[$input] = Kohana::lang($key)) === $key) + { + // Get the default error message + $errors[$input] = Kohana::lang("$file.$input.default"); + } + } + + 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 |