Validator.php 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677
  1. <?php
  2. /**
  3. * Lithium: the most rad php framework
  4. *
  5. * @copyright Copyright 2012, Union of RAD (http://union-of-rad.org)
  6. * @license http://opensource.org/licenses/bsd-license.php The BSD License
  7. */
  8. namespace lithium\util;
  9. use lithium\util\Set;
  10. use InvalidArgumentException;
  11. /**
  12. * The `Validator` class provides static access to commonly used data validation logic. These common
  13. * routines cover HTML form input data such as phone and credit card numbers, dates and postal
  14. * codes, but also include general checks for regular expressions and booleans and numericality.
  15. *
  16. * General data checking is done by using `Validator` statically. Rules can be specified as a
  17. * parameter to the `rule()` method or accessed directly via the `is[RuleName]()` method name
  18. * convention:
  19. *
  20. * {{{
  21. * use lithium\util\Validator;
  22. *
  23. * // The following are equivalent:
  24. * Validator::rule('email', '[email protected]'); // true
  25. * Validator::isEmail('foo-at-example.com'); // false
  26. * }}}
  27. *
  28. * Data can also be validated against multiple rules, each having their own associated error
  29. * message. The rule structure is array-based and hierarchical based on rule names and
  30. * messages. Responses match the keys present in the `$data` parameter of `check()` up with an array
  31. * of rules which they violate.
  32. *
  33. * {{{ embed:lithium\tests\cases\util\ValidatorTest::testCheckMultipleHasFirstError(1-15) }}}
  34. *
  35. * See the `check()` method for more information an multi-value datasets. Custom validation rules
  36. * can also be added to `Validator` at runtime. These can either take the form of regular expression
  37. * strings or functions supplied to the `add()` method.
  38. *
  39. * ### Rules
  40. *
  41. * The `Validator` class includes a series of commonly-used rules by default, any of which may be
  42. * used in calls to `rule()` or `check()`, or called directly as a method. Additionally, many rules
  43. * have a variety of different _formats_ in which they may be specified. The following is the list
  44. * of the built-in rules, but keep in mind that none of them are hard-coded. Any rule may be
  45. * overridden by adding a new rule of the same name using the `add()` method.
  46. *
  47. * - `notEmpty`: Checks that a string contains at least one non-whitespace character.
  48. *
  49. * - `alphaNumeric`: Checks that a string contains only integer or letters.
  50. *
  51. * - `lengthBetween`: Checks that a string length is within a specified range. Spaces are included
  52. * in the character count. The available options are `'min'` and `'max'`, which designate the
  53. * minimum and maximum length of the string.
  54. *
  55. * - `blank`: Checks that a field is left blank **OR** only whitespace characters are present in its
  56. * value. Whitespace characters include spaces, tabs, carriage returns and newlines.
  57. *
  58. * - `creditCard`: Checks that a value is a valid credit card number. This rule is divided into a
  59. * series of formats: `'amex'`, `'bankcard'`, `'diners'`, `'disc'`, `'electron'`, `'enroute'`,
  60. * `'jcb'`, `'maestro'`, `'mc'`, `'solo'`, `'switch'`, `'visa'`, `'voyager'`, `'fast'`. If no
  61. * format value is specified, the value defaults to `'any'`, which will validate the value if
  62. * _any_ of the available formats match. You can also use the `'fast'` format, which does a
  63. * high-speed, low-fidelity check to ensure that the value looks like a real credit card number.
  64. * This rule includes one option, `'deep'`, which (if set to `true`) validates the value using the
  65. * [Luhn algorithm](http://en.wikipedia.org/wiki/Luhn_algorithm) if the format validation is
  66. * successful. See the `luhn` validator below for more details.
  67. *
  68. * - `date`: Checks that a value is a valid date that complies with one or more formats. Also
  69. * validates leap years. Possible formats are `'dmy'` (27-12-2010 or 27-12-10 separators can be a
  70. * space, period, dash, forward slash), `'mdy'` (12-27-2010 or 12-27-10 separators can be a space,
  71. * period, dash, forward slash), `'ymd'` (2010-12-27 or 10-12-27 separators can be a space,
  72. * period, dash, forward slash), `'dMy'` (27 December 2010 or 27 Dec 2010), `'Mdy'` (December 27,
  73. * 2010 or Dec 27, 2010 comma is optional), `'My'` (December 2010 or Dec 2010) or `'my'` (12/2010
  74. * separators can be a space, period, dash, forward slash).
  75. *
  76. * - `time`: Checks that a value is a valid time. Validates time as 24hr (HH:MM) or am/pm
  77. * ([ H]H:MM[a|p]m). Does not allow / validate seconds.
  78. *
  79. * - `boolean`: Checks that the value is or looks like a boolean value. The following types of
  80. * values are interpreted as boolean and will pass the check.
  81. * - boolean (`true`, `false`, `'true'`, `'false'`)
  82. * - boolean number (`1`, `0`, `'1'`, `'0'`)
  83. * - boolean text string (`'on'`, `'off'`, `'yes'`, `'no'`)
  84. *
  85. * - `decimal`: Checks that a value is a valid decimal. Takes one option, `'precision'`, which is
  86. * an optional integer value defining the level of precision the decimal number must match.
  87. *
  88. * - `email`: Checks that a value is (probably) a valid email address. The subject of validating
  89. * an actual email address is complicated and problematic. A regular expression that correctly
  90. * validates addresses against [RFC 5322](http://tools.ietf.org/html/rfc5322) would be several
  91. * pages long, with the drawback of being unable to keep up as new top-level domains are added.
  92. * Instead, this validator uses PHP's internal input filtering API to check the format, and
  93. * provides an option, `'deep'` ( _boolean_) which, if set to `true`, will validate that the email
  94. * address' domain contains a valid MX record. Keep in mind, this is just one of the many ways to
  95. * validate an email address in the overall context of an application. For other ideas or
  96. * examples, [ask Sean](http://seancoates.com/).
  97. *
  98. * - `ip`: Validates a string as a valid IPv4 or IPv6 address.
  99. *
  100. * - `money`: Checks that a value is a valid monetary amount. This rule has two formats, `'right'`
  101. * and `'left'`, which indicates which side the monetary symbol (i.e. $) appears on.
  102. *
  103. * - `numeric`: Checks that a value is numeric.
  104. *
  105. * - `phone`: Check that a value is a valid phone number, non-locale-specific phone number.
  106. *
  107. * - `postalCode`: Checks that a given value is a valid US postal code.
  108. *
  109. * - `inRange`: Checks that a numeric value is within a specified range. This value has two options,
  110. * `'upper'` and `'lower'`, which specify the boundary of the value.
  111. *
  112. * - `url`: Checks that a value is a valid URL according to
  113. * [RFC 2395](http://www.faqs.org/rfcs/rfc2396.html). Uses PHP's filter API, and accepts any
  114. * options accepted for
  115. * [the validation URL filter](http://www.php.net/manual/en/filter.filters.validate.php).
  116. *
  117. * - `luhn`: Checks that a value is a valid credit card number according to the
  118. * [Luhn algorithm](http://en.wikipedia.org/wiki/Luhn_algorithm). (See also: the `creditCard`
  119. * validator).
  120. *
  121. * - `inList`: Checks that a value is in a pre-defined list of values. This validator accepts one
  122. * option, `'list'`, which is an array containing acceptable values.
  123. *
  124. * - `regex`: Checks that a value appears to be a valid regular expression, possibly
  125. * containing PCRE-compatible options flags.
  126. *
  127. * - `uuid`: Checks that a value is a valid UUID.
  128. *
  129. * ### UTF-8 encoded input strings
  130. *
  131. * All rules operating on strings have been created with the possibility of
  132. * UTF-8 encoded input in mind. A default PHP binary and an enabled Lithium
  133. * g11n bootstrap will make these rules work correctly in any case. Should you
  134. * ever experience odd behavior following paragraph with implementation
  135. * details might help you to track to the cause.
  136. *
  137. * The rules `alphaNumeric` and `money` rely on additional functionality of
  138. * PCRE to validate UTF-8 encoded strings. As no PCRE feature detection is
  139. * done, having this feature enabled in PCRE isn't optional. Please ensure
  140. * you've got PCRE compiled with UTF-8 support.
  141. */
  142. class Validator extends \lithium\core\StaticObject {
  143. /**
  144. * An array of validation rules. May contain a single regular expression, an array of regular
  145. * expressions (where the array keys define various possible 'formats' of the same rule), or a
  146. * closure which accepts a value to be validated, and an array of options, and returns a
  147. * boolean value, indicating whether the validation succeeded or failed.
  148. *
  149. * @var array
  150. * @see lithium\util\Validator::add()
  151. * @see lithium\util\Validator::rule()
  152. */
  153. protected static $_rules = array();
  154. /**
  155. * Default options used when defining a new validator rule. Each key contains method-specific
  156. * options that should always be applied, or options that should be applied to all rules in the
  157. * `'defaults'` key.
  158. *
  159. * @see lithium\util\Validator::add()
  160. * @see lithium\util\Validator::rule()
  161. * @var array
  162. */
  163. protected static $_options = array(
  164. 'defaults' => array('contains' => true)
  165. );
  166. /**
  167. * Initializes the list of default validation rules.
  168. *
  169. * @return void
  170. */
  171. public static function __init() {
  172. $alnum = '[A-Fa-f0-9]';
  173. $class = get_called_class();
  174. static::$_methodFilters[$class] = array();
  175. static::$_rules = array(
  176. 'alphaNumeric' => '/^[\p{Ll}\p{Lm}\p{Lo}\p{Lt}\p{Lu}\p{Nd}]+$/mu',
  177. 'blank' => '/[^\\s]/',
  178. 'creditCard' => array(
  179. 'amex' => '/^3[4|7]\\d{13}$/',
  180. 'bankcard' => '/^56(10\\d\\d|022[1-5])\\d{10}$/',
  181. 'diners' => '/^(?:3(0[0-5]|[68]\\d)\\d{11})|(?:5[1-5]\\d{14})$/',
  182. 'disc' => '/^(?:6011|650\\d)\\d{12}$/',
  183. 'electron' => '/^(?:417500|4917\\d{2}|4913\\d{2})\\d{10}$/',
  184. 'enroute' => '/^2(?:014|149)\\d{11}$/',
  185. 'jcb' => '/^(3\\d{4}|2100|1800)\\d{11}$/',
  186. 'maestro' => '/^(?:5020|6\\d{3})\\d{12}$/',
  187. 'mc' => '/^5[1-5]\\d{14}$/',
  188. 'solo' => '/^(6334[5-9][0-9]|6767[0-9]{2})\\d{10}(\\d{2,3})?$/',
  189. 'switch' => '/^(?:49(03(0[2-9]|3[5-9])|11(0[1-2]|7[4-9]|8[1-2])|36[0-9]{2})' .
  190. '\\d{10}(\\d{2,3})?)|(?:564182\\d{10}(\\d{2,3})?)|(6(3(33[0-4]' .
  191. '[0-9])|759[0-9]{2})\\d{10}(\\d{2,3})?)$/',
  192. 'visa' => '/^4\\d{12}(\\d{3})?$/',
  193. 'voyager' => '/^8699[0-9]{11}$/',
  194. 'fast' => '/^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6011[0-9]{12}|3' .
  195. '(?:0[0-5]|[68][0-9])[0-9]{11}|3[47][0-9]{13})$/'
  196. ),
  197. 'date' => array(
  198. 'dmy' => '%^(?:(?:31(\\/|-|\\.|\\x20)(?:0?[13578]|1[02]))\\1|(?:(?:29|30)' .
  199. '(\\/|-|\\.|\\x20)(?:0?[1,3-9]|1[0-2])\\2))(?:(?:1[6-9]|[2-9]\\d)?' .
  200. '\\d{2})$|^(?:29(\\/|-|\\.|\\x20)0?2\\3(?:(?:(?:1[6-9]|[2-9]\\d)?' .
  201. '(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])' .
  202. '00))))$|^(?:0?[1-9]|1\\d|2[0-8])(\\/|-|\\.|\\x20)(?:(?:0?[1-9])|' .
  203. '(?:1[0-2]))\\4(?:(?:1[6-9]|[2-9]\\d)?\\d{2})$%',
  204. 'mdy' => '%^(?:(?:(?:0?[13578]|1[02])(\\/|-|\\.|\\x20)31)\\1|(?:(?:0?[13-9]|' .
  205. '1[0-2])(\\/|-|\\.|\\x20)(?:29|30)\\2))(?:(?:1[6-9]|[2-9]\\d)?\\d' .
  206. '{2})$|^(?:0?2(\\/|-|\\.|\\x20)29\\3(?:(?:(?:1[6-9]|[2-9]\\d)?' .
  207. '(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])' .
  208. '00))))$|^(?:(?:0?[1-9])|(?:1[0-2]))(\\/|-|\\.|\\x20)(?:0?[1-9]|1' .
  209. '\\d|2[0-8])\\4(?:(?:1[6-9]|[2-9]\\d)?\\d{2})$%',
  210. 'ymd' => '%^(?:(?:(?:(?:(?:1[6-9]|[2-9]\\d)?(?:0[48]|[2468][048]|[13579]' .
  211. '[26])|(?:(?:16|[2468][048]|[3579][26])00)))(\\/|-|\\.|\\x20)' .
  212. '(?:0?2\\1(?:29)))|(?:(?:(?:1[6-9]|[2-9]\\d)?\\d{2})(\\/|-|\\.|' .
  213. '\\x20)(?:(?:(?:0?[13578]|1[02])\\2(?:31))|(?:(?:0?[1,3-9]|1[0-2])' .
  214. '\\2(29|30))|(?:(?:0?[1-9])|(?:1[0-2]))\\2(?:0?[1-9]|1\\d|2[0-8]' .
  215. '))))$%',
  216. 'dMy' => '/^((31(?!\\ (Feb(ruary)?|Apr(il)?|June?|(Sep(?=\\b|t)t?|Nov)' .
  217. '(ember)?)))|((30|29)(?!\\ Feb(ruary)?))|(29(?=\\ Feb(ruary)?\\ ' .
  218. '(((1[6-9]|[2-9]\\d)(0[48]|[2468][048]|[13579][26])|((16|[2468]' .
  219. '[048]|[3579][26])00)))))|(0?[1-9])|1\\d|2[0-8])\\ (Jan(uary)?|' .
  220. 'Feb(ruary)?|Ma(r(ch)?|y)|Apr(il)?|Ju((ly?)|(ne?))|Aug(ust)?|' .
  221. 'Oct(ober)?|(Sep(?=\\b|t)t?|Nov|Dec)(ember)?)\\ ((1[6-9]|[2-9]' .
  222. '\\d)\\d{2})$/',
  223. 'Mdy' => '/^(?:(((Jan(uary)?|Ma(r(ch)?|y)|Jul(y)?|Aug(ust)?|Oct(ober)?' .
  224. '|Dec(ember)?)\\ 31)|((Jan(uary)?|Ma(r(ch)?|y)|Apr(il)?|Ju((ly?)' .
  225. '|(ne?))|Aug(ust)?|Oct(ober)?|(Sept|Nov|Dec)(ember)?)\\ (0?[1-9]' .
  226. '|([12]\\d)|30))|(Feb(ruary)?\\ (0?[1-9]|1\\d|2[0-8]|(29(?=,?\\ ' .
  227. '((1[6-9]|[2-9]\\d)(0[48]|[2468][048]|[13579][26])|((16|[2468]' .
  228. '[048]|[3579][26])00)))))))\\,?\\ ((1[6-9]|[2-9]\\d)\\d{2}))$/',
  229. 'My' => '%^(Jan(uary)?|Feb(ruary)?|Ma(r(ch)?|y)|Apr(il)?|Ju((ly?)|(ne?))|' .
  230. 'Aug(ust)?|Oct(ober)?|(Sep(?=\\b|t)t?|Nov|Dec)(ember)?)[ /]((1[6-9]' .
  231. '|[2-9]\\d)\\d{2})$%',
  232. 'my' => '%^(((0[123456789]|10|11|12)([- /.])(([1][9][0-9][0-9])|([2][0-9]' .
  233. '[0-9][0-9]))))$%'
  234. ),
  235. 'ip' => function($value, $format = null, array $options = array()) {
  236. $options += array('flags' => array());
  237. return (boolean) filter_var($value, FILTER_VALIDATE_IP, $options);
  238. },
  239. 'money' => array(
  240. 'right' => '/^(?!0,?\d)(?:\d{1,3}(?:([, .])\d{3})?(?:\1\d{3})*|(?:\d+))' .
  241. '((?!\1)[,.]\d{2})?(?<!\x{00a2})\p{Sc}?$/u',
  242. 'left' => '/^(?!\x{00a2})\p{Sc}?(?!0,?\d)(?:\d{1,3}(?:([, .])\d{3})?' .
  243. '(?:\1\d{3})*|(?:\d+))((?!\1)[,.]\d{2})?$/u'
  244. ),
  245. 'notEmpty' => '/[^\s]+/m',
  246. 'phone' => '/^\+?[0-9\(\)\-]{10,20}$/',
  247. 'postalCode' => '/(^|\A\b)[A-Z0-9\s\-]{5,}($|\b\z)/i',
  248. 'regex' => '/^(?:([^[:alpha:]\\\\{<\[\(])(.+)(?:\1))|(?:{(.+)})|(?:<(.+)>)|' .
  249. '(?:\[(.+)\])|(?:\((.+)\))[gimsxu]*$/',
  250. 'time' => '%^((0?[1-9]|1[012])(:[0-5]\d){0,2}([AP]M|[ap]m))$|^([01]\d|2[0-3])' .
  251. '(:[0-5]\d){0,2}$%',
  252. 'boolean' => function($value) {
  253. $bool = is_bool($value);
  254. $filter = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
  255. return ($bool || $filter !== null || empty($value));
  256. },
  257. 'decimal' => function($value, $format = null, array $options = array()) {
  258. if (isset($options['precision'])) {
  259. $precision = strlen($value) - strrpos($value, '.') - 1;
  260. if ($precision !== (int) $options['precision']) {
  261. return false;
  262. }
  263. }
  264. return (filter_var($value, FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE) !== null);
  265. },
  266. 'inList' => function($value, $format, $options) {
  267. $options += array('list' => array());
  268. $strict = is_bool($value) || $value === '';
  269. return in_array($value, $options['list'], $strict);
  270. },
  271. 'lengthBetween' => function($value, $format, $options) {
  272. $length = strlen($value);
  273. $options += array('min' => 1, 'max' => 255);
  274. return ($length >= $options['min'] && $length <= $options['max']);
  275. },
  276. 'luhn' => function($value) {
  277. if (empty($value) || !is_string($value)) {
  278. return false;
  279. }
  280. $sum = 0;
  281. $length = strlen($value);
  282. for ($position = 1 - ($length % 2); $position < $length; $position += 2) {
  283. $sum += $value[$position];
  284. }
  285. for ($position = ($length % 2); $position < $length; $position += 2) {
  286. $number = $value[$position] * 2;
  287. $sum += ($number < 10) ? $number : $number - 9;
  288. }
  289. return ($sum % 10 === 0);
  290. },
  291. 'numeric' => function($value) {
  292. return is_numeric($value);
  293. },
  294. 'inRange' => function($value, $format, $options) {
  295. $defaults = array('upper' => null, 'lower' => null);
  296. $options += $defaults;
  297. if (!is_numeric($value)) {
  298. return false;
  299. }
  300. switch (true) {
  301. case (!is_null($options['upper']) && !is_null($options['lower'])):
  302. return ($value >= $options['lower'] && $value <= $options['upper']);
  303. case (!is_null($options['upper'])):
  304. return ($value <= $options['upper']);
  305. case (!is_null($options['lower'])):
  306. return ($value >= $options['lower']);
  307. }
  308. return is_finite($value);
  309. },
  310. 'uuid' => "/^{$alnum}{8}-{$alnum}{4}-{$alnum}{4}-{$alnum}{4}-{$alnum}{12}$/",
  311. 'email' => function($value) {
  312. return filter_var($value, FILTER_VALIDATE_EMAIL);
  313. },
  314. 'url' => function($value, $format = null, array $options = array()) {
  315. $options += array('flags' => array());
  316. return (boolean) filter_var($value, FILTER_VALIDATE_URL, $options);
  317. }
  318. );
  319. $isEmpty = function($self, $params, $chain) {
  320. extract($params);
  321. return (empty($value) && $value !== '0') ? false : $chain->next($self, $params, $chain);
  322. };
  323. static::$_methodFilters[$class]['alphaNumeric'] = array($isEmpty);
  324. static::$_methodFilters[$class]['notEmpty'] = array($isEmpty);
  325. static::$_methodFilters[$class]['creditCard'] = array(function($self, $params, $chain) {
  326. extract($params);
  327. $options += array('deep' => false);
  328. if (strlen($value = str_replace(array('-', ' '), '', $value)) < 13) {
  329. return false;
  330. }
  331. if (!$chain->next($self, compact('value') + $params, $chain)) {
  332. return false;
  333. }
  334. return $options['deep'] ? Validator::isLuhn($value) : true;
  335. });
  336. static::$_methodFilters[$class]['email'] = array(function($self, $params, $chain) {
  337. extract($params);
  338. $defaults = array('deep' => false);
  339. $options += $defaults;
  340. if (!$chain->next($self, $params, $chain)) {
  341. return false;
  342. }
  343. if (!$options['deep']) {
  344. return true;
  345. }
  346. list($prefix, $host) = explode('@', $params['value']);
  347. if (getmxrr($host, $mxhosts)) {
  348. return is_array($mxhosts);
  349. }
  350. return false;
  351. });
  352. }
  353. /**
  354. * Maps method calls to validation rule names. For example, a validation rule that would
  355. * normally be called as `Validator::rule('email', '[email protected]')` can also be called as
  356. * `Validator::isEmail('[email protected]')`.
  357. *
  358. * @param string $method The name of the method called, i.e. `'isEmail'` or `'isCreditCard'`.
  359. * @param array $args
  360. * @return boolean
  361. */
  362. public static function __callStatic($method, $args = array()) {
  363. if (!isset($args[0])) {
  364. return false;
  365. }
  366. $args = array_filter($args) + array(0 => $args[0], 1 => 'any', 2 => array());
  367. $rule = preg_replace("/^is([A-Z][A-Za-z0-9]+)$/", '$1', $method);
  368. $rule[0] = strtolower($rule[0]);
  369. return static::rule($rule, $args[0], $args[1], $args[2]);
  370. }
  371. /**
  372. * Custom check to determine if our given magic methods can be responded to.
  373. *
  374. * @param string $method Method name.
  375. * @param bool $internal Interal call or not.
  376. * @return bool
  377. */
  378. public static function respondsTo($method, $internal = false) {
  379. $rule = preg_replace("/^is([A-Z][A-Za-z0-9]+)$/", '$1', $method);
  380. $rule[0] = strtolower($rule[0]);
  381. return isset(static::$_rules[$rule]) || parent::respondsTo($method, $internal);
  382. }
  383. /**
  384. * Checks a set of values against a specified rules list. This method may be used to validate
  385. * any arbitrary array of data against a set of validation rules.
  386. *
  387. * @param array $values An array of key/value pairs, where the values are to be checked.
  388. * @param array $rules An array of rules to check the values in `$values` against. Each key in
  389. * `$rules` should match a key contained in `$values`, and each value should be a
  390. * validation rule in one of the allowable formats. For example, if you are
  391. * validating a data set containing a `'credit_card'` key, possible values for
  392. * `$rules` would be as follows:
  393. * - `array('credit_card' => 'You must include a credit card number')`: This is the
  394. * simplest form of validation rule, in which the value is simply a message to
  395. * display if the rule fails. Using this format, all other validation settings
  396. * inherit from the defaults, including the validation rule itself, which only
  397. * checks to see that the corresponding key in `$values` is present and contains
  398. * a value that is not empty. _Please note when globalizing validation messages:_
  399. * When specifying messages, it may be preferable to use a code string (i.e.
  400. * `'ERR_NO_TITLE'`) instead of the full text of the validation error. These code
  401. * strings may then be translated by the appropriate tools in the templating
  402. * layer.
  403. * - `array('credit_card' => array('creditCard', 'message' => 'Invalid CC #'))`:
  404. * In the second format, the validation rule (in this case `creditCard`) and
  405. * associated configuration are specified as an array, where the rule to use is
  406. * the first value in the array (no key), and additional settings are specified
  407. * as other keys in the array. Please see the list below for more information on
  408. * allowed keys.
  409. * - The final format allows you to apply multiple validation rules to a single
  410. * value, and it is specified as follows:
  411. *
  412. * `array('credit_card' => array(
  413. * array('notEmpty', 'message' => 'You must include credit card number'),
  414. * array('creditCard', 'message' => 'Your credit card number must be valid')
  415. * ));`
  416. * @param array $options Validator-specific options.
  417. *
  418. * Each rule defined as an array can contain any of the following settings (in addition to the
  419. * first value, which represents the rule to be used):
  420. * - `'message'` _string_: The error message to be returned if the validation rule fails. See
  421. * the note above regarding globalization of error messages.
  422. * - `'required`' _boolean_: Represents whether the value is required to be present in
  423. * `$values`. If `'required'` is set to `false`, the validation rule will be skipped if the
  424. * corresponding key is not present. Defaults to `true`.
  425. * - `'skipEmpty'` _boolean_: Similar to `'required'`, this setting (if `true`) will cause the
  426. * validation rule to be skipped if the corresponding value is empty (an empty string or
  427. * `null`). Defaults to `false`.
  428. * - `'format'` _string_: If the validation rule has multiple format definitions (see the
  429. * `add()` or `__init()` methods), the name of the format to be used can be specified here.
  430. * Additionally, two special values can be used: either `'any'`, which means that all formats
  431. * will be checked and the rule will pass if any format passes, or `'all'`, which requires
  432. * all formats to pass in order for the rule check to succeed.
  433. * @return array Returns an array containing all validation failures for data in `$values`,
  434. * where each key matches a key in `$values`, and each value is an array of that
  435. * element's validation errors.
  436. * @filter
  437. */
  438. public static function check(array $values, array $rules, array $options = array()) {
  439. $defaults = array(
  440. 'notEmpty',
  441. 'message' => null,
  442. 'required' => true,
  443. 'skipEmpty' => false,
  444. 'format' => 'any',
  445. 'on' => null,
  446. 'last' => false
  447. );
  448. $options += $defaults;
  449. $params = compact('values', 'rules', 'options');
  450. return static::_filter(__FUNCTION__, $params, function($self, $params) {
  451. $values = $params['values'];
  452. $rules = $params['rules'];
  453. $options = $params['options'];
  454. $errors = array();
  455. $events = (array) (isset($options['events']) ? $options['events'] : null);
  456. $values = Set::flatten($values);
  457. foreach ($rules as $field => $rules) {
  458. $rules = is_string($rules) ? array('message' => $rules) : $rules;
  459. $rules = is_array(current($rules)) ? $rules : array($rules);
  460. $errors[$field] = array();
  461. $options['field'] = $field;
  462. foreach ($rules as $key => $rule) {
  463. $rule += $options + compact('values');
  464. list($name) = $rule;
  465. if ($events && $rule['on'] && !array_intersect($events, (array) $rule['on'])) {
  466. continue;
  467. }
  468. if (!array_key_exists($field, $values)) {
  469. if ($rule['required']) {
  470. $errors[$field][] = $rule['message'] ?: $key;
  471. }
  472. if ($rule['last']) {
  473. break;
  474. }
  475. continue;
  476. }
  477. if (empty($values[$field]) && $rule['skipEmpty']) {
  478. continue;
  479. }
  480. if (!$self::rule($name, $values[$field], $rule['format'], $rule + $options)) {
  481. $errors[$field][] = $rule['message'] ?: $key;
  482. if ($rule['last']) {
  483. break;
  484. }
  485. }
  486. }
  487. }
  488. return array_filter($errors);
  489. });
  490. }
  491. /**
  492. * Adds to or replaces built-in validation rules specified in `Validator::$_rules`. Any new
  493. * validation rules created are automatically callable as validation methods.
  494. *
  495. * For example:
  496. * {{{
  497. * Validator::add('zeroToNine', '/^[0-9]$/');
  498. * $isValid = Validator::isZeroToNine("5"); // true
  499. * $isValid = Validator::isZeroToNine("20"); // false
  500. * }}}
  501. *
  502. * Alternatively, the first parameter may be an array of rules expressed as key/value pairs,
  503. * as in the following:
  504. * {{{
  505. * Validator::add(array(
  506. * 'zeroToNine' => '/^[0-9]$/',
  507. * 'tenToNineteen' => '/^1[0-9]$/',
  508. * ));
  509. * }}}
  510. *
  511. * In addition to regular expressions, validation rules can also be defined as full anonymous
  512. * functions:
  513. * {{{
  514. * use app\models\Account;
  515. *
  516. * Validator::add('accountActive', function($value) {
  517. * $value = is_int($value) ? Account::find($value) : $value;
  518. * return (boolean) $value->is_active;
  519. * });
  520. *
  521. * $testAccount = Account::create(array('is_active' => false));
  522. * Validator::isAccountActive($testAccount); // returns false
  523. * }}}
  524. *
  525. * These functions can take up to 3 parameters:
  526. * - `$value` _mixed_: This is the actual value to be validated (as in the above example).
  527. * - `$format` _string_: Often, validation rules come in multiple "formats", for example:
  528. * postal codes, which vary by country or region. Defining multiple formats allows you to
  529. * retian flexibility in how you validate data. In cases where a user's country of origin
  530. * is known, the appropriate validation rule may be selected. In cases where it is not
  531. * known, the value of `$format` may be `'any'`, which should pass if any format matches.
  532. * In cases where validation rule formats are not mutually exclusive, the value may be
  533. * `'all'`, in which case all must match.
  534. * - `$options` _array_: This parameter allows a validation rule to implement custom
  535. * options.
  536. *
  537. * @see lithium\util\Validator::$_rules
  538. * @param mixed $name The name of the validation rule (string), or an array of key/value pairs
  539. * of names and rules.
  540. * @param string $rule If $name is a string, this should be a string regular expression, or a
  541. * closure that returns a boolean indicating success. Should be left blank if
  542. * `$name` is an array.
  543. * @param array $options The default options for validating this rule. An option which applies
  544. * to all regular expression rules is `'contains'` which, if set to true, allows
  545. * validated values to simply _contain_ a match to a rule, rather than exactly
  546. * matching it in whole.
  547. * @return void
  548. */
  549. public static function add($name, $rule = null, array $options = array()) {
  550. if (!is_array($name)) {
  551. $name = array($name => $rule);
  552. }
  553. static::$_rules = Set::merge(static::$_rules, $name);
  554. if (!empty($options)) {
  555. $options = array_combine(array_keys($name), array_fill(0, count($name), $options));
  556. static::$_options = Set::merge(static::$_options, $options);
  557. }
  558. }
  559. /**
  560. * Checks a single value against a single validation rule in one or more formats.
  561. *
  562. * @param string $rule
  563. * @param mixed $value
  564. * @param string $format
  565. * @param array $options
  566. * @return boolean Returns `true` or `false` indicating whether the validation rule check
  567. * succeeded or failed.
  568. * @filter
  569. */
  570. public static function rule($rule, $value, $format = 'any', array $options = array()) {
  571. if (!isset(static::$_rules[$rule])) {
  572. throw new InvalidArgumentException("Rule `{$rule}` is not a validation rule.");
  573. }
  574. $defaults = isset(static::$_options[$rule]) ? static::$_options[$rule] : array();
  575. $options = (array) $options + $defaults + static::$_options['defaults'];
  576. $ruleCheck = static::$_rules[$rule];
  577. $ruleCheck = is_array($ruleCheck) ? $ruleCheck : array($ruleCheck);
  578. if (!$options['contains'] && !empty($ruleCheck)) {
  579. foreach ($ruleCheck as $key => $item) {
  580. $ruleCheck[$key] = is_string($item) ? "/^{$item}$/" : $item;
  581. }
  582. }
  583. $params = compact('value', 'format', 'options');
  584. return static::_filter($rule, $params, static::_checkFormats($ruleCheck));
  585. }
  586. /**
  587. * Returns a list of available validation rules, or the configuration details of a single rule.
  588. *
  589. * @param string $name Optional name of a rule to get the details of. If not specified, an array
  590. * of all available rule names is returned. Otherwise, returns the details of a
  591. * single rule. This can be a regular expression string, a closure object, or an
  592. * array of available rule formats made up of string regular expressions,
  593. * closures, or both.
  594. * @return mixed Returns either an single array of rule names, or the details of a single rule.
  595. */
  596. public static function rules($name = null) {
  597. if (!$name) {
  598. return array_keys(static::$_rules);
  599. }
  600. return isset(static::$_rules[$name]) ? static::$_rules[$name] : null;
  601. }
  602. /**
  603. * Perform validation checks against a value using an array of all possible formats for a rule,
  604. * and an array specifying which formats within the rule to use.
  605. *
  606. * @param array $rules All available rules.
  607. * @return closure Function returning boolean `true` if validation succeeded, `false` otherwise.
  608. */
  609. protected static function _checkFormats($rules) {
  610. return function($self, $params, $chain) use ($rules) {
  611. $value = $params['value'];
  612. $format = $params['format'];
  613. $options = $params['options'];
  614. $defaults = array('all' => true);
  615. $options += $defaults;
  616. $formats = (array) $format;
  617. $options['all'] = ($format === 'any');
  618. foreach ($rules as $index => $check) {
  619. if (!$options['all'] && !(in_array($index, $formats) || isset($formats[$index]))) {
  620. continue;
  621. }
  622. $regexPassed = (is_string($check) && preg_match($check, $value));
  623. $closurePassed = (is_object($check) && $check($value, $format, $options));
  624. if (!$options['all'] && ($regexPassed || $closurePassed)) {
  625. return true;
  626. }
  627. if ($options['all'] && (!$regexPassed && !$closurePassed)) {
  628. return false;
  629. }
  630. }
  631. return $options['all'];
  632. };
  633. }
  634. }
  635. ?>