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('/(? 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