Validation.php 14 KB

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