* @link http://www.vancelucas.com/ */ class Validator { /** * @var string */ const ERROR_DEFAULT = 'Invalid'; /** * @var array */ protected $_fields = array(); /** * @var array */ protected $_errors = array(); /** * @var array */ protected $_validations = array(); /** * @var array */ protected $_labels = array(); /** * Contains all rules that are available to the current valitron instance. * * @var array */ protected $_instanceRules = array(); /** * Contains all rule messages that are available to the current valitron * instance * * @var array */ protected $_instanceRuleMessage = array(); /** * @var string */ protected static $_lang; /** * @var string */ protected static $_langDir; /** * @var array */ protected static $_rules = array(); /** * @var array */ protected static $_ruleMessages = array(); /** * @var array */ protected $validUrlPrefixes = array('http://', 'https://', 'ftp://'); /** * Setup validation * * @param array $data * @param array $fields * @param string $lang * @param string $langDir * @throws \InvalidArgumentException */ public function __construct($data = array(), $fields = array(), $lang = null, $langDir = null) { // Allows filtering of used input fields against optional second array of field names allowed // This is useful for limiting raw $_POST or $_GET data to only known fields $this->_fields = !empty($fields) ? array_intersect_key($data, array_flip($fields)) : $data; // set lang in the follow order: constructor param, static::$_lang, default to en $lang = $lang ?: static::lang(); // set langDir in the follow order: constructor param, static::$_langDir, default to package lang dir $langDir = $langDir ?: static::langDir(); // Load language file in directory $langFile = rtrim($langDir, '/') . '/' . $lang . '.php'; if (stream_resolve_include_path($langFile) ) { $langMessages = include $langFile; static::$_ruleMessages = array_merge(static::$_ruleMessages, $langMessages); } else { throw new \InvalidArgumentException("Fail to load language file '" . $langFile . "'"); } } /** * Get/set language to use for validation messages * * @param string $lang * @return string */ public static function lang($lang = null) { if ($lang !== null) { static::$_lang = $lang; } return static::$_lang ?: 'en'; } /** * Get/set language file path * * @param string $dir * @return string */ public static function langDir($dir = null) { if ($dir !== null) { static::$_langDir = $dir; } return static::$_langDir ?: dirname(dirname(__DIR__)) . '/lang'; } /** * Required field validator * * @param string $field * @param mixed $value * @return bool */ protected function validateRequired($field, $value) { if (is_null($value)) { return false; } elseif (is_string($value) && trim($value) === '') { return false; } return true; } /** * Validate that two values match * * @param string $field * @param mixed $value * @param array $params * @internal param array $fields * @return bool */ protected function validateEquals($field, $value, array $params) { $field2 = $params[0]; return isset($this->_fields[$field2]) && $value == $this->_fields[$field2]; } /** * Validate that a field is different from another field * * @param string $field * @param mixed $value * @param array $params * @internal param array $fields * @return bool */ protected function validateDifferent($field, $value, array $params) { $field2 = $params[0]; return isset($this->_fields[$field2]) && $value != $this->_fields[$field2]; } /** * Validate that a field was "accepted" (based on PHP's string evaluation rules) * * This validation rule implies the field is "required" * * @param string $field * @param mixed $value * @return bool */ protected function validateAccepted($field, $value) { $acceptable = array('yes', 'on', 1, '1', true); return $this->validateRequired($field, $value) && in_array($value, $acceptable, true); } /** * Validate that a field is an array * * @param string $field * @param mixed $value * @return bool */ protected function validateArray($field, $value) { return is_array($value); } /** * Validate that a field is numeric * * @param string $field * @param mixed $value * @return bool */ protected function validateNumeric($field, $value) { return is_numeric($value); } /** * Validate that a field is an integer * * @param string $field * @param mixed $value * @param array $params * @return bool */ protected function validateInteger($field, $value, $params) { if (isset($params[0]) && (bool) $params[0]){ //strict mode return preg_match('/^-?([0-9])+$/i', $value); } return filter_var($value, \FILTER_VALIDATE_INT) !== false; } /** * Validate the length of a string * * @param string $field * @param mixed $value * @param array $params * @internal param array $fields * @return bool */ protected function validateLength($field, $value, $params) { $length = $this->stringLength($value); // Length between if (isset($params[1])) { return $length >= $params[0] && $length <= $params[1]; } // Length same return ($length !== false) && $length == $params[0]; } /** * Validate the length of a string (between) * * @param string $field * @param mixed $value * @param array $params * @return boolean */ protected function validateLengthBetween($field, $value, $params) { $length = $this->stringLength($value); return ($length !== false) && $length >= $params[0] && $length <= $params[1]; } /** * Validate the length of a string (min) * * @param string $field * @param mixed $value * @param array $params * * @return boolean */ protected function validateLengthMin($field, $value, $params) { $length = $this->stringLength($value); return ($length !== false) && $length >= $params[0]; } /** * Validate the length of a string (max) * * @param string $field * @param mixed $value * @param array $params * * @return boolean */ protected function validateLengthMax($field, $value, $params) { $length = $this->stringLength($value); return ($length !== false) && $length <= $params[0]; } /** * Get the length of a string * * @param string $value * @return int|false */ protected function stringLength($value) { if (!is_string($value)) { return false; } elseif (function_exists('mb_strlen')) { return mb_strlen($value); } return strlen($value); } /** * Validate the size of a field is greater than a minimum value. * * @param string $field * @param mixed $value * @param array $params * @internal param array $fields * @return bool */ protected function validateMin($field, $value, $params) { if (!is_numeric($value)) { return false; } elseif (function_exists('bccomp')) { return !(bccomp($params[0], $value, 14) === 1); } else { return $params[0] <= $value; } } /** * Validate the size of a field is less than a maximum value * * @param string $field * @param mixed $value * @param array $params * @internal param array $fields * @return bool */ protected function validateMax($field, $value, $params) { if (!is_numeric($value)) { return false; } elseif (function_exists('bccomp')) { return !(bccomp($value, $params[0], 14) === 1); } else { return $params[0] >= $value; } } /** * Validate the size of a field is between min and max values * * @param string $field * @param mixed $value * @param array $params * @return bool */ protected function validateBetween($field, $value, $params) { if (!is_numeric($value)) { return false; } if (!isset($params[0]) || !is_array($params[0]) || count($params[0]) !== 2) { return false; } list($min, $max) = $params[0]; return $this->validateMin($field, $value, array($min)) && $this->validateMax($field, $value, array($max)); } /** * Validate a field is contained within a list of values * * @param string $field * @param mixed $value * @param array $params * @internal param array $fields * @return bool */ protected function validateIn($field, $value, $params) { $isAssoc = array_values($params[0]) !== $params[0]; if ($isAssoc) { $params[0] = array_keys($params[0]); } $strict = false; if (isset($params[1])) { $strict = $params[1]; } return in_array($value, $params[0], $strict); } /** * Validate a field is not contained within a list of values * * @param string $field * @param mixed $value * @param array $params * @internal param array $fields * @return bool */ protected function validateNotIn($field, $value, $params) { return !$this->validateIn($field, $value, $params); } /** * Validate a field contains a given string * * @param string $field * @param mixed $value * @param array $params * @return bool */ protected function validateContains($field, $value, $params) { if (!isset($params[0])) { return false; } if (!is_string($params[0]) || !is_string($value)) { return false; } $strict = true; if (isset($params[1])) { $strict = (bool) $params[1]; } $isContains = false; if ($strict) { if (function_exists('mb_strpos')) { $isContains = mb_strpos($value, $params[0]) !== false; } else { $isContains = strpos($value, $params[0]) !== false; } } else { if (function_exists('mb_stripos')) { $isContains = mb_stripos($value, $params[0]) !== false; } else { $isContains = stripos($value, $params[0]) !== false; } } return $isContains; } /** * Validate that a field is a valid IP address * * @param string $field * @param mixed $value * @return bool */ protected function validateIp($field, $value) { return filter_var($value, \FILTER_VALIDATE_IP) !== false; } /** * Validate that a field is a valid e-mail address * * @param string $field * @param mixed $value * @return bool */ protected function validateEmail($field, $value) { return filter_var($value, \FILTER_VALIDATE_EMAIL) !== false; } /** * Validate that a field is a valid URL by syntax * * @param string $field * @param mixed $value * @return bool */ protected function validateUrl($field, $value) { foreach ($this->validUrlPrefixes as $prefix) { if (strpos($value, $prefix) !== false) { return filter_var($value, \FILTER_VALIDATE_URL) !== false; } } return false; } /** * Validate that a field is an active URL by verifying DNS record * * @param string $field * @param mixed $value * @return bool */ protected function validateUrlActive($field, $value) { foreach ($this->validUrlPrefixes as $prefix) { if (strpos($value, $prefix) !== false) { $host = parse_url(strtolower($value), PHP_URL_HOST); return checkdnsrr($host, 'A') || checkdnsrr($host, 'AAAA') || checkdnsrr($host, 'CNAME'); } } return false; } /** * Validate that a field contains only alphabetic characters * * @param string $field * @param mixed $value * @return bool */ protected function validateAlpha($field, $value) { return preg_match('/^([a-z])+$/i', $value); } /** * Validate that a field contains only alpha-numeric characters * * @param string $field * @param mixed $value * @return bool */ protected function validateAlphaNum($field, $value) { return preg_match('/^([a-z0-9])+$/i', $value); } /** * Validate that a field contains only alpha-numeric characters, dashes, and underscores * * @param string $field * @param mixed $value * @return bool */ protected function validateSlug($field, $value) { if(is_array($value)) { return false; } return preg_match('/^([-a-z0-9_-])+$/i', $value); } /** * Validate that a field passes a regular expression check * * @param string $field * @param mixed $value * @param array $params * @return bool */ protected function validateRegex($field, $value, $params) { return preg_match($params[0], $value); } /** * Validate that a field is a valid date * * @param string $field * @param mixed $value * @return bool */ protected function validateDate($field, $value) { $isDate = false; if ($value instanceof \DateTime) { $isDate = true; } else { $isDate = strtotime($value) !== false; } return $isDate; } /** * Validate that a field matches a date format * * @param string $field * @param mixed $value * @param array $params * @internal param array $fields * @return bool */ protected function validateDateFormat($field, $value, $params) { $parsed = date_parse_from_format($params[0], $value); return $parsed['error_count'] === 0 && $parsed['warning_count'] === 0; } /** * Validate the date is before a given date * * @param string $field * @param mixed $value * @param array $params * @internal param array $fields * @return bool */ protected function validateDateBefore($field, $value, $params) { $vtime = ($value instanceof \DateTime) ? $value->getTimestamp() : strtotime($value); $ptime = ($params[0] instanceof \DateTime) ? $params[0]->getTimestamp() : strtotime($params[0]); return $vtime < $ptime; } /** * Validate the date is after a given date * * @param string $field * @param mixed $value * @param array $params * @internal param array $fields * @return bool */ protected function validateDateAfter($field, $value, $params) { $vtime = ($value instanceof \DateTime) ? $value->getTimestamp() : strtotime($value); $ptime = ($params[0] instanceof \DateTime) ? $params[0]->getTimestamp() : strtotime($params[0]); return $vtime > $ptime; } /** * Validate that a field contains a boolean. * * @param string $field * @param mixed $value * @return bool */ protected function validateBoolean($field, $value) { return is_bool($value); } /** * Validate that a field contains a valid credit card * optionally filtered by an array * * @param string $field * @param mixed $value * @param array $params * @return bool */ protected function validateCreditCard($field, $value, $params) { /** * I there has been an array of valid cards supplied, or the name of the users card * or the name and an array of valid cards */ if (!empty($params)) { /** * array of valid cards */ if (is_array($params[0])) { $cards = $params[0]; } elseif (is_string($params[0])) { $cardType = $params[0]; if (isset($params[1]) && is_array($params[1])) { $cards = $params[1]; if (!in_array($cardType, $cards)) { return false; } } } } /** * Luhn algorithm * * @return bool */ $numberIsValid = function () use ($value) { $number = preg_replace('/[^0-9]+/', '', $value); $sum = 0; $strlen = strlen($number); if ($strlen < 13) { return false; } for ($i = 0; $i < $strlen; $i++) { $digit = (int) substr($number, $strlen - $i - 1, 1); if ($i % 2 == 1) { $sub_total = $digit * 2; if ($sub_total > 9) { $sub_total = ($sub_total - 10) + 1; } } else { $sub_total = $digit; } $sum += $sub_total; } if ($sum > 0 && $sum % 10 == 0) { return true; } return false; }; if ($numberIsValid()) { if (!isset($cards)) { return true; } else { $cardRegex = array( 'visa' => '#^4[0-9]{12}(?:[0-9]{3})?$#', 'mastercard' => '#^(5[1-5]|2[2-7])[0-9]{14}$#', 'amex' => '#^3[47][0-9]{13}$#', 'dinersclub' => '#^3(?:0[0-5]|[68][0-9])[0-9]{11}$#', 'discover' => '#^6(?:011|5[0-9]{2})[0-9]{12}$#', ); if (isset($cardType)) { // if we don't have any valid cards specified and the card we've been given isn't in our regex array if (!isset($cards) && !in_array($cardType, array_keys($cardRegex))) { return false; } // we only need to test against one card type return (preg_match($cardRegex[$cardType], $value) === 1); } elseif (isset($cards)) { // if we have cards, check our users card against only the ones we have foreach ($cards as $card) { if (in_array($card, array_keys($cardRegex))) { // if the card is valid, we want to stop looping if (preg_match($cardRegex[$card], $value) === 1) { return true; } } } } else { // loop through every card foreach ($cardRegex as $regex) { // until we find a valid one if (preg_match($regex, $value) === 1) { return true; } } } } } // if we've got this far, the card has passed no validation so it's invalid! return false; } protected function validateInstanceOf($field, $value, $params) { $isInstanceOf = false; if (is_object($value)) { if (is_object($params[0]) && $value instanceof $params[0]) { $isInstanceOf = true; } if (get_class($value) === $params[0]) { $isInstanceOf = true; } } if (is_string($value)) { if (is_string($params[0]) && get_class($value) === $params[0]) { $isInstanceOf = true; } } return $isInstanceOf; } //Validate optional field protected function validateOptional($field, $value, $params) { //Always return true return true; } /** * Get array of fields and data * * @return array */ public function data() { return $this->_fields; } /** * Get array of error messages * * @param null|string $field * @return array|bool */ public function errors($field = null) { if ($field !== null) { return isset($this->_errors[$field]) ? $this->_errors[$field] : false; } return $this->_errors; } /** * Add an error to error messages array * * @param string $field * @param string $msg * @param array $params */ public function error($field, $msg, array $params = array()) { $msg = $this->checkAndSetLabel($field, $msg, $params); $values = array(); // Printed values need to be in string format foreach ($params as $param) { if (is_array($param)) { $param = "['" . implode("', '", $param) . "']"; } if ($param instanceof \DateTime) { $param = $param->format('Y-m-d'); } else { if (is_object($param)) { $param = get_class($param); } } // Use custom label instead of field name if set if (is_string($params[0])) { if (isset($this->_labels[$param])) { $param = $this->_labels[$param]; } } $values[] = $param; } $this->_errors[$field][] = vsprintf($msg, $values); } /** * Specify validation message to use for error for the last validation rule * * @param string $msg * @return $this */ public function message($msg) { $this->_validations[count($this->_validations) - 1]['message'] = $msg; return $this; } /** * Reset object properties */ public function reset() { $this->_fields = array(); $this->_errors = array(); $this->_validations = array(); $this->_labels = array(); } protected function getPart($data, $identifiers) { // Catches the case where the field is an array of discrete values if (is_array($identifiers) && count($identifiers) === 0) { return array($data, false); } $identifier = array_shift($identifiers); // Glob match if ($identifier === '*') { $values = array(); foreach ($data as $row) { list($value, $multiple) = $this->getPart($row, $identifiers); if ($multiple) { $values = array_merge($values, $value); } else { $values[] = $value; } } return array($values, true); } // Dead end, abort elseif ($identifier === NULL || ! isset($data[$identifier])) { return array(null, false); } // Match array element elseif (count($identifiers) === 0) { return array($data[$identifier], false); } // We need to go deeper else { return $this->getPart($data[$identifier], $identifiers); } } /** * Run validations and return boolean result * * @return boolean */ public function validate() { foreach ($this->_validations as $v) { foreach ($v['fields'] as $field) { list($values, $multiple) = $this->getPart($this->_fields, explode('.', $field)); // Don't validate if the field is not required and the value is empty if ($this->hasRule('optional', $field) && isset($values)) { //Continue with execution below if statement } elseif ($v['rule'] !== 'required' && !$this->hasRule('required', $field) && (! isset($values) || $values === '' || ($multiple && count($values) == 0))) { continue; } // Callback is user-specified or assumed method on class $errors = $this->getRules(); if (isset($errors[$v['rule']])) { $callback = $errors[$v['rule']]; } else { $callback = array($this, 'validate' . ucfirst($v['rule'])); } if (!$multiple) { $values = array($values); } $result = true; foreach ($values as $value) { $result = $result && call_user_func($callback, $field, $value, $v['params'], $this->_fields); } if (!$result) { $this->error($field, $v['message'], $v['params']); } } } return count($this->errors()) === 0; } /** * Returns all rule callbacks, the static and instance ones. * * @return array */ protected function getRules() { return array_merge($this->_instanceRules, static::$_rules); } /** * Returns all rule message, the static and instance ones. * * @return array */ protected function getRuleMessages() { return array_merge($this->_instanceRuleMessage, static::$_ruleMessages); } /** * Determine whether a field is being validated by the given rule. * * @param string $name The name of the rule * @param string $field The name of the field * @return boolean */ protected function hasRule($name, $field) { foreach ($this->_validations as $validation) { if ($validation['rule'] == $name) { if (in_array($field, $validation['fields'])) { return true; } } } return false; } protected static function assertRuleCallback($callback) { if (!is_callable($callback)) { throw new \InvalidArgumentException('Second argument must be a valid callback. Given argument was not callable.'); } } /** * Adds a new validation rule callback that is tied to the current * instance only. * * @param string $name * @param mixed $callback * @param string $message * @throws \InvalidArgumentException */ public function addInstanceRule($name, $callback, $message = null) { static::assertRuleCallback($callback); $this->_instanceRules[$name] = $callback; $this->_instanceRuleMessage[$name] = $message; } /** * Register new validation rule callback * * @param string $name * @param mixed $callback * @param string $message * @throws \InvalidArgumentException */ public static function addRule($name, $callback, $message = null) { if ($message === null) { $message = static::ERROR_DEFAULT; } static::assertRuleCallback($callback); static::$_rules[$name] = $callback; static::$_ruleMessages[$name] = $message; } public function getUniqueRuleName($fields) { if (is_array($fields)) { $fields = implode("_", $fields); } $orgName = "{$fields}_rule"; $name = $orgName; $rules = $this->getRules(); while (isset($rules[$name])) { $name = $orgName . "_" . rand(0, 10000); } return $name; } /** * Returns true if either a valdiator with the given name has been * registered or there is a default validator by that name. * * @param string $name * @return bool */ public function hasValidator($name) { $rules = $this->getRules(); return method_exists($this, "validate" . ucfirst($name)) || isset($rules[$name]); } /** * Convenience method to add a single validation rule * * @param string|callback $rule * @param array|string $fields * @return $this * @throws \InvalidArgumentException */ public function rule($rule, $fields) { // Get any other arguments passed to function $params = array_slice(func_get_args(), 2); if (is_callable($rule) && !(is_string($rule) && $this->hasValidator($rule))) { $name = $this->getUniqueRuleName($fields); $msg = isset($params[0]) ? $params[0] : null; $this->addInstanceRule($name, $rule, $msg); $rule = $name; } $errors = $this->getRules(); if (!isset($errors[$rule])) { $ruleMethod = 'validate' . ucfirst($rule); if (!method_exists($this, $ruleMethod)) { throw new \InvalidArgumentException("Rule '" . $rule . "' has not been registered with " . __CLASS__ . "::addRule()."); } } // Ensure rule has an accompanying message $msgs = $this->getRuleMessages(); $message = isset($msgs[$rule]) ? $msgs[$rule] : self::ERROR_DEFAULT; $this->_validations[] = array( 'rule' => $rule, 'fields' => (array) $fields, 'params' => (array) $params, 'message' => '{field} ' . $message ); return $this; } /** * @param string $value * @internal param array $labels * @return $this */ public function label($value) { $lastRules = $this->_validations[count($this->_validations) - 1]['fields']; $this->labels(array($lastRules[0] => $value)); return $this; } /** * @param array $labels * @return $this */ public function labels($labels = array()) { $this->_labels = array_merge($this->_labels, $labels); return $this; } /** * @param string $field * @param string $msg * @param array $params * @return array */ protected function checkAndSetLabel($field, $msg, $params) { if (isset($this->_labels[$field])) { $msg = str_replace('{field}', $this->_labels[$field], $msg); if (is_array($params)) { $i = 1; foreach ($params as $k => $v) { $tag = '{field'. $i .'}'; $label = isset($params[$k]) && (is_numeric($params[$k]) || is_string($params[$k])) && isset($this->_labels[$params[$k]]) ? $this->_labels[$params[$k]] : $tag; $msg = str_replace($tag, $label, $msg); $i++; } } } else { $msg = str_replace('{field}', ucwords(str_replace('_', ' ', $field)), $msg); } return $msg; } /** * Convenience method to add multiple validation rules with an array * * @param array $rules */ public function rules($rules) { foreach ($rules as $ruleType => $params) { if (is_array($params)) { foreach ($params as $innerParams) { array_unshift($innerParams, $ruleType); call_user_func_array(array($this, 'rule'), $innerParams); } } else { $this->rule($ruleType, $params); } } } /** * Replace data on cloned instance * * @param array $data * @param array $fields * @return \Valitron\Validator */ public function withData($data, $fields = array()) { $clone = clone $this; $clone->_fields = !empty($fields) ? array_intersect_key($data, array_flip($fields)) : $data; $clone->_errors = array(); return $clone; } /** * Convenience method to add validation rule(s) by field * * @param string field_name * @param array $rules */ public function mapFieldRules($field_name, $rules){ $me = $this; array_map(function($rule) use($field_name, $me){ //rule must be an array $rule = (array)$rule; //First element is the name of the rule $rule_name = array_shift($rule); //find a custom message, if any $message = null; if (isset($rule['message'])){ $message = $rule['message']; unset($rule['message']); } //Add the field and additional parameters to the rule $added = call_user_func_array(array($me, 'rule'), array_merge(array($rule_name, $field_name), $rule)); if (! empty($message)){ $added->message($message); } }, (array) $rules); } /** * Convenience method to add validation rule(s) for multiple fields * * @param array $rules */ public function mapFieldsRules($rules){ $me = $this; array_map(function($field_name) use($rules, $me){ $me->mapFieldRules($field_name, $rules[$field_name]); }, array_keys($rules)); } }