summaryrefslogtreecommitdiff
path: root/kohana/libraries/Validation.php
diff options
context:
space:
mode:
Diffstat (limited to 'kohana/libraries/Validation.php')
-rw-r--r--kohana/libraries/Validation.php773
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