Validation.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622
  1. <?php
  2. /**
  3. * Array and variable validation.
  4. *
  5. * @package Kohana
  6. * @category Security
  7. * @author Kohana Team
  8. * @copyright (c) Kohana Team
  9. * @license https://koseven.ga/LICENSE.md
  10. */
  11. class Kohana_Validation implements ArrayAccess {
  12. /**
  13. * Creates a new Validation instance.
  14. *
  15. * @param array $array array to use for validation
  16. * @return Validation
  17. */
  18. public static function factory(array $array)
  19. {
  20. return new Validation($array);
  21. }
  22. // Bound values
  23. protected $_bound = [];
  24. // Field rules
  25. protected $_rules = [];
  26. // Field labels
  27. protected $_labels = [];
  28. // Rules that are executed even when the value is empty
  29. protected $_empty_rules = ['not_empty', 'matches'];
  30. // Error list, field => rule
  31. protected $_errors = [];
  32. // Array to validate
  33. protected $_data = [];
  34. /**
  35. * Sets the unique "any field" key and creates an ArrayObject from the
  36. * passed array.
  37. *
  38. * @param array $array array to validate
  39. * @return void
  40. */
  41. public function __construct(array $array)
  42. {
  43. $this->_data = $array;
  44. }
  45. /**
  46. * Throws an exception because Validation is read-only.
  47. * Implements ArrayAccess method.
  48. *
  49. * @throws Kohana_Exception
  50. * @param string $offset key to set
  51. * @param mixed $value value to set
  52. * @return void
  53. */
  54. #[\ReturnTypeWillChange]
  55. public function offsetSet($offset, $value)
  56. {
  57. throw new Kohana_Exception('Validation objects are read-only.');
  58. }
  59. /**
  60. * Checks if key is set in array data.
  61. * Implements ArrayAccess method.
  62. *
  63. * @param string $offset key to check
  64. * @return bool whether the key is set
  65. */
  66. #[\ReturnTypeWillChange]
  67. public function offsetExists($offset)
  68. {
  69. return isset($this->_data[$offset]);
  70. }
  71. /**
  72. * Throws an exception because Validation is read-only.
  73. * Implements ArrayAccess method.
  74. *
  75. * @throws Kohana_Exception
  76. * @param string $offset key to unset
  77. * @return void
  78. */
  79. #[\ReturnTypeWillChange]
  80. public function offsetUnset($offset)
  81. {
  82. throw new Kohana_Exception('Validation objects are read-only.');
  83. }
  84. /**
  85. * Gets a value from the array data.
  86. * Implements ArrayAccess method.
  87. *
  88. * @param string $offset key to return
  89. * @return mixed value from array
  90. */
  91. #[\ReturnTypeWillChange]
  92. public function offsetGet($offset)
  93. {
  94. return $this->_data[$offset];
  95. }
  96. /**
  97. * Copies the current rules to a new array.
  98. *
  99. * $copy = $array->copy($new_data);
  100. *
  101. * @param array $array new data set
  102. * @return Validation
  103. * @since 3.0.5
  104. */
  105. public function copy(array $array)
  106. {
  107. // Create a copy of the current validation set
  108. $copy = clone $this;
  109. // Replace the data set
  110. $copy->_data = $array;
  111. return $copy;
  112. }
  113. /**
  114. * Returns the array representation of the current object.
  115. * Deprecated in favor of [Validation::data]
  116. *
  117. * @deprecated
  118. * @return array
  119. */
  120. public function as_array()
  121. {
  122. return $this->_data;
  123. }
  124. /**
  125. * Returns the array of data to be validated.
  126. *
  127. * @return array
  128. */
  129. public function data()
  130. {
  131. return $this->_data;
  132. }
  133. /**
  134. * Sets or overwrites the label name for a field.
  135. *
  136. * @param string $field field name
  137. * @param string $label label
  138. * @return $this
  139. */
  140. public function label($field, $label)
  141. {
  142. // Set the label for this field
  143. $this->_labels[$field] = $label;
  144. return $this;
  145. }
  146. /**
  147. * Sets labels using an array.
  148. *
  149. * @param array $labels list of field => label names
  150. * @return $this
  151. */
  152. public function labels(array $labels)
  153. {
  154. $this->_labels = $labels + $this->_labels;
  155. return $this;
  156. }
  157. /**
  158. * Overwrites or appends rules to a field. Each rule will be executed once.
  159. * All rules must be string names of functions method names. Parameters must
  160. * match the parameters of the callback function exactly
  161. *
  162. * Aliases you can use in callback parameters:
  163. * - :validation - the validation object
  164. * - :field - the field name
  165. * - :value - the value of the field
  166. *
  167. * // The "username" must not be empty and have a minimum length of 4
  168. * $validation->rule('username', 'not_empty')
  169. * ->rule('username', 'min_length', array(':value', 4));
  170. *
  171. * // The "password" field must match the "password_repeat" field
  172. * $validation->rule('password', 'matches', array(':validation', 'password', 'password_repeat'));
  173. *
  174. * // Using closure (anonymous function)
  175. * $validation->rule('index',
  176. * function(Validation $array, $field, $value)
  177. * {
  178. * if ($value > 6 AND $value < 10)
  179. * {
  180. * $array->error($field, 'custom');
  181. * }
  182. * }
  183. * , array(':validation', ':field', ':value')
  184. * );
  185. *
  186. * [!!] Errors must be added manually when using closures!
  187. *
  188. * @param string $field field name
  189. * @param callback $rule valid PHP callback or closure
  190. * @param array $params extra parameters for the rule
  191. * @return $this
  192. */
  193. public function rule($field, $rule, array $params = NULL)
  194. {
  195. if ($params === NULL)
  196. {
  197. // Default to array(':value')
  198. $params = [':value'];
  199. }
  200. if ($field !== TRUE AND ! isset($this->_labels[$field]))
  201. {
  202. // Set the field label to the field name
  203. $this->_labels[$field] = $field;
  204. }
  205. // Store the rule and params for this rule
  206. $this->_rules[$field][] = [$rule, $params];
  207. return $this;
  208. }
  209. /**
  210. * Add rules using an array.
  211. *
  212. * @param string $field field name
  213. * @param array $rules list of callbacks
  214. * @return $this
  215. */
  216. public function rules($field, array $rules)
  217. {
  218. foreach ($rules as $rule)
  219. {
  220. $this->rule($field, $rule[0], Arr::get($rule, 1));
  221. }
  222. return $this;
  223. }
  224. /**
  225. * Bind a value to a parameter definition.
  226. *
  227. * // This allows you to use :model in the parameter definition of rules
  228. * $validation->bind(':model', $model)
  229. * ->rule('status', 'valid_status', array(':model'));
  230. *
  231. * @param string $key variable name or an array of variables
  232. * @param mixed $value value
  233. * @return $this
  234. */
  235. public function bind($key, $value = NULL)
  236. {
  237. if (is_array($key))
  238. {
  239. foreach ($key as $name => $value)
  240. {
  241. $this->_bound[$name] = $value;
  242. }
  243. }
  244. else
  245. {
  246. $this->_bound[$key] = $value;
  247. }
  248. return $this;
  249. }
  250. /**
  251. * Executes all validation rules. This should
  252. * typically be called within an if/else block.
  253. *
  254. * if ($validation->check())
  255. * {
  256. * // The data is valid, do something here
  257. * }
  258. *
  259. * @return boolean
  260. */
  261. public function check()
  262. {
  263. if (Kohana::$profiling === TRUE)
  264. {
  265. // Start a new benchmark
  266. $benchmark = Profiler::start('Validation', __FUNCTION__);
  267. }
  268. // New data set
  269. $data = $this->_errors = [];
  270. // Store the original data because this class should not modify it post-validation
  271. $original = $this->_data;
  272. // Get a list of the expected fields
  273. $expected = Arr::merge(array_keys($original), array_keys($this->_labels));
  274. // Import the rules locally
  275. $rules = $this->_rules;
  276. foreach ($expected as $field)
  277. {
  278. // Use the submitted value or NULL if no data exists
  279. $data[$field] = Arr::get($this, $field);
  280. if (isset($rules[TRUE]))
  281. {
  282. if ( ! isset($rules[$field]))
  283. {
  284. // Initialize the rules for this field
  285. $rules[$field] = [];
  286. }
  287. // Append the rules
  288. $rules[$field] = array_merge($rules[$field], $rules[TRUE]);
  289. }
  290. }
  291. // Overload the current array with the new one
  292. $this->_data = $data;
  293. // Remove the rules that apply to every field
  294. unset($rules[TRUE]);
  295. // Bind the validation object to :validation
  296. $this->bind(':validation', $this);
  297. // Bind the data to :data
  298. $this->bind(':data', $this->_data);
  299. // Execute the rules
  300. foreach ($rules as $field => $set)
  301. {
  302. // Get the field value
  303. $value = $this[$field];
  304. // Bind the field name and value to :field and :value respectively
  305. $this->bind([
  306. ':field' => $field,
  307. ':value' => $value,
  308. ]);
  309. foreach ($set as $array)
  310. {
  311. // Rules are defined as array($rule, $params)
  312. list($rule, $params) = $array;
  313. foreach ($params as $key => $param)
  314. {
  315. if (is_string($param) AND array_key_exists($param, $this->_bound))
  316. {
  317. // Replace with bound value
  318. $params[$key] = $this->_bound[$param];
  319. }
  320. }
  321. // Default the error name to be the rule (except array and lambda rules)
  322. $error_name = $rule;
  323. if (is_array($rule))
  324. {
  325. // Allows rule('field', array(':model', 'some_rule'));
  326. if (is_string($rule[0]) AND array_key_exists($rule[0], $this->_bound))
  327. {
  328. // Replace with bound value
  329. $rule[0] = $this->_bound[$rule[0]];
  330. }
  331. // This is an array callback, the method name is the error name
  332. $error_name = $rule[1];
  333. $passed = call_user_func_array($rule, $params);
  334. }
  335. elseif ( ! is_string($rule))
  336. {
  337. // This is a lambda function, there is no error name (errors must be added manually)
  338. $error_name = FALSE;
  339. $passed = call_user_func_array($rule, $params);
  340. }
  341. elseif (method_exists('Valid', $rule))
  342. {
  343. // Use a method in this object
  344. $method = new ReflectionMethod('Valid', $rule);
  345. // Call static::$rule($this[$field], $param, ...) with Reflection
  346. $passed = $method->invokeArgs(NULL, $params);
  347. }
  348. elseif (strpos($rule, '::') === FALSE)
  349. {
  350. // Use a function call
  351. $function = new ReflectionFunction($rule);
  352. // Call $function($this[$field], $param, ...) with Reflection
  353. $passed = $function->invokeArgs($params);
  354. }
  355. else
  356. {
  357. // Split the class and method of the rule
  358. list($class, $method) = explode('::', $rule, 2);
  359. // Use a static method call
  360. $method = new ReflectionMethod($class, $method);
  361. // Call $Class::$method($this[$field], $param, ...) with Reflection
  362. $passed = $method->invokeArgs(NULL, $params);
  363. }
  364. // Ignore return values from rules when the field is empty
  365. if ( ! in_array($rule, $this->_empty_rules) AND ! Valid::not_empty($value))
  366. continue;
  367. if ($passed === FALSE AND $error_name !== FALSE)
  368. {
  369. // Add the rule to the errors
  370. $this->error($field, $error_name, $params);
  371. // This field has an error, stop executing rules
  372. break;
  373. }
  374. elseif (isset($this->_errors[$field]))
  375. {
  376. // The callback added the error manually, stop checking rules
  377. break;
  378. }
  379. }
  380. }
  381. // Unbind all the automatic bindings to avoid memory leaks.
  382. unset($this->_bound[':validation']);
  383. unset($this->_bound[':data']);
  384. unset($this->_bound[':field']);
  385. unset($this->_bound[':value']);
  386. // Restore the data to its original form
  387. $this->_data = $original;
  388. if (isset($benchmark))
  389. {
  390. // Stop benchmarking
  391. Profiler::stop($benchmark);
  392. }
  393. return empty($this->_errors);
  394. }
  395. /**
  396. * Add an error to a field.
  397. *
  398. * @param string $field field name
  399. * @param string $error error message
  400. * @param array $params
  401. * @return $this
  402. */
  403. public function error($field, $error, array $params = NULL)
  404. {
  405. $this->_errors[$field] = [$error, $params];
  406. return $this;
  407. }
  408. /**
  409. * Returns the error messages. If no file is specified, the error message
  410. * will be the name of the rule that failed. When a file is specified, the
  411. * message will be loaded from "field/rule", or if no rule-specific message
  412. * exists, "field/default" will be used. If neither is set, the returned
  413. * message will be "file/field/rule".
  414. *
  415. * By default all messages are translated using the default language.
  416. * A string can be used as the second parameter to specified the language
  417. * that the message was written in.
  418. *
  419. * // Get errors from messages/forms/login.php
  420. * $errors = $Validation->errors('forms/login');
  421. *
  422. * @uses Kohana::message
  423. * @param string $file file to load error messages from
  424. * @param mixed $translate translate the message
  425. * @return array
  426. */
  427. public function errors($file = NULL, $translate = TRUE)
  428. {
  429. if ($file === NULL)
  430. {
  431. // Return the error list
  432. return $this->_errors;
  433. }
  434. // Create a new message list
  435. $messages = [];
  436. foreach ($this->_errors as $field => $set)
  437. {
  438. list($error, $params) = $set;
  439. // Get the label for this field
  440. $label = $this->_labels[$field];
  441. if ($translate)
  442. {
  443. if (is_string($translate))
  444. {
  445. // Translate the label using the specified language
  446. $label = __($label, NULL, $translate);
  447. }
  448. else
  449. {
  450. // Translate the label
  451. $label = __($label);
  452. }
  453. }
  454. // Start the translation values list
  455. $values = [
  456. ':field' => $label,
  457. ':value' => Arr::get($this, $field),
  458. ];
  459. if (is_array($values[':value']))
  460. {
  461. // All values must be strings
  462. $values[':value'] = implode(', ', Arr::flatten($values[':value']));
  463. }
  464. if ($params)
  465. {
  466. foreach ($params as $key => $value)
  467. {
  468. if (is_array($value))
  469. {
  470. // All values must be strings
  471. $value = implode(', ', Arr::flatten($value));
  472. }
  473. elseif (is_object($value))
  474. {
  475. // Objects cannot be used in message files
  476. continue;
  477. }
  478. // Check if a label for this parameter exists
  479. if (isset($this->_labels[$value]))
  480. {
  481. // Use the label as the value, eg: related field name for "matches"
  482. $value = $this->_labels[$value];
  483. if ($translate)
  484. {
  485. if (is_string($translate))
  486. {
  487. // Translate the value using the specified language
  488. $value = __($value, NULL, $translate);
  489. }
  490. else
  491. {
  492. // Translate the value
  493. $value = __($value);
  494. }
  495. }
  496. }
  497. // Add each parameter as a numbered value, starting from 1
  498. $values[':param'.($key + 1)] = $value;
  499. }
  500. }
  501. if ($message = Kohana::message($file, "{$field}.{$error}") AND is_string($message))
  502. {
  503. // Found a message for this field and error
  504. }
  505. elseif ($message = Kohana::message($file, "{$field}.default") AND is_string($message))
  506. {
  507. // Found a default message for this field
  508. }
  509. elseif ($message = Kohana::message($file, $error) AND is_string($message))
  510. {
  511. // Found a default message for this error
  512. }
  513. elseif ($message = Kohana::message('validation', $error) AND is_string($message))
  514. {
  515. // Found a default message for this error
  516. }
  517. else
  518. {
  519. // No message exists, display the path expected
  520. $message = "{$file}.{$field}.{$error}";
  521. }
  522. if ($translate)
  523. {
  524. if (is_string($translate))
  525. {
  526. // Translate the message using specified language
  527. $message = __($message, $values, $translate);
  528. }
  529. else
  530. {
  531. // Translate the message using the default language
  532. $message = __($message, $values);
  533. }
  534. }
  535. else
  536. {
  537. // Do not translate, just replace the values
  538. $message = strtr($message, $values);
  539. }
  540. // Set the message for this field
  541. $messages[$field] = $message;
  542. }
  543. return $messages;
  544. }
  545. }