PageRenderTime 60ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

/base/lib/flourishlib/fValidation.php

https://bitbucket.org/thanhtungnguyenphp/monitos
PHP | 1077 lines | 521 code | 143 blank | 413 comment | 88 complexity | 8138570b7d84fe274a341357d419e778 MD5 | raw file
  1. <?php
  2. /**
  3. * Provides validation routines for standalone forms, such as contact forms
  4. *
  5. * @copyright Copyright (c) 2007-2010 Will Bond
  6. * @author Will Bond [wb] <will@flourishlib.com>
  7. * @license http://flourishlib.com/license
  8. *
  9. * @package Flourish
  10. * @link http://flourishlib.com/fValidation
  11. *
  12. * @version 1.0.0b10
  13. * @changes 1.0.0b10 Fixed ::addRegexRule() to be able to handle multiple rules per field [wb, 2010-08-30]
  14. * @changes 1.0.0b9 Enhanced all of the add fields methods to accept one field per parameter, or an array of fields [wb, 2010-06-24]
  15. * @changes 1.0.0b8 Added/fixed support for array-syntax fields names [wb, 2010-06-09]
  16. * @changes 1.0.0b7 Added the ability to pass an array of replacements to ::addRegexReplacement() and ::addStringReplacement() [wb, 2010-05-31]
  17. * @changes 1.0.0b6 BackwardsCompatibilityBreak - moved one-or-more required fields from ::addRequiredFields() to ::addOneOrMoreRule(), moved conditional required fields from ::addRequiredFields() to ::addConditionalRule(), changed returned messages array to have field name keys - added lots of functionality [wb, 2010-05-26]
  18. * @changes 1.0.0b5 Added the `$return_messages` parameter to ::validate() and updated code for new fValidationException API [wb, 2009-09-17]
  19. * @changes 1.0.0b4 Changed date checking from `strtotime()` to fTimestamp for better localization support [wb, 2009-06-01]
  20. * @changes 1.0.0b3 Updated for new fCore API [wb, 2009-02-16]
  21. * @changes 1.0.0b2 Added support for validating date and URL fields [wb, 2009-01-23]
  22. * @changes 1.0.0b The initial implementation [wb, 2007-06-14]
  23. */
  24. class fValidation
  25. {
  26. /**
  27. * Composes text using fText if loaded
  28. *
  29. * @param string $message The message to compose
  30. * @param mixed $component A string or number to insert into the message
  31. * @param mixed ...
  32. * @return string The composed and possible translated message
  33. */
  34. static protected function compose($message)
  35. {
  36. $args = array_slice(func_get_args(), 1);
  37. if (class_exists('fText', FALSE)) {
  38. return call_user_func_array(
  39. array('fText', 'compose'),
  40. array($message, $args)
  41. );
  42. } else {
  43. return vsprintf($message, $args);
  44. }
  45. }
  46. /**
  47. * Check if a field has a value
  48. *
  49. * @param string $key The key to check for a value
  50. * @return boolean If the key has a value
  51. */
  52. static private function hasValue($key)
  53. {
  54. $value = fRequest::get($key);
  55. if (self::stringlike($value)) {
  56. return TRUE;
  57. }
  58. if (is_array($value)) {
  59. foreach ($value as $individual_value) {
  60. if (self::stringlike($individual_value)) {
  61. return TRUE;
  62. }
  63. }
  64. }
  65. return FALSE;
  66. }
  67. /**
  68. * Compares the message matching strings by longest first so that the longest matches are made first
  69. *
  70. * @param string $a The first string to compare
  71. * @param string $b The second string to compare
  72. * @return integer `-1` if `$a` is longer than `$b`, `0` if they are equal length, `1` if `$a` is shorter than `$b`
  73. */
  74. static private function sortMessageMatches($a, $b)
  75. {
  76. if (strlen($a) == strlen($b)) {
  77. return 0;
  78. }
  79. if (strlen($a) > strlen($b)) {
  80. return -1;
  81. }
  82. return 1;
  83. }
  84. /**
  85. * Returns `TRUE` for non-empty strings, numbers, objects, empty numbers and string-like numbers (such as `0`, `0.0`, `'0'`)
  86. *
  87. * @param mixed $value The value to check
  88. * @return boolean If the value is string-like
  89. */
  90. static protected function stringlike($value)
  91. {
  92. if ((!is_array($value) && !is_string($value) && !is_object($value) && !is_numeric($value)) || (!is_array($value) && !strlen(trim($value)))) {
  93. return FALSE;
  94. }
  95. return TRUE;
  96. }
  97. /**
  98. * Rules that run through a callback
  99. *
  100. * @var array
  101. */
  102. private $callback_rules = array();
  103. /**
  104. * Rules for conditionally requiring fields
  105. *
  106. * @var array
  107. */
  108. private $conditional_rules = array();
  109. /**
  110. * Fields that should be valid dates
  111. *
  112. * @var array
  113. */
  114. private $date_fields = array();
  115. /**
  116. * An array for custom field names
  117. *
  118. * @var array
  119. */
  120. private $field_names = array();
  121. /**
  122. * File upload rules
  123. *
  124. * @var array
  125. */
  126. private $file_upload_rules = array();
  127. /**
  128. * An array for ordering the fields in the resulting message
  129. *
  130. * @var array
  131. */
  132. private $message_order = array();
  133. /**
  134. * Rules for at least one field of multiple having a value
  135. *
  136. * @var array
  137. */
  138. private $one_or_more_rules = array();
  139. /**
  140. * Rules for exactly one field of multiple having a value
  141. *
  142. * @var array
  143. */
  144. private $only_one_rules = array();
  145. /**
  146. * Regular expression replacements for the validation messages
  147. *
  148. * @var array
  149. */
  150. private $regex_replacements = array();
  151. /**
  152. * Rules to validate fields via regular expressions
  153. *
  154. * @var array
  155. */
  156. private $regex_rules = array();
  157. /**
  158. * The fields to be required
  159. *
  160. * @var array
  161. */
  162. private $required_fields = array();
  163. /**
  164. * String replacements for the validation messages
  165. *
  166. * @var array
  167. */
  168. private $string_replacements = array();
  169. /**
  170. * Rules for validating a field against a set of valid values
  171. *
  172. * @var array
  173. */
  174. private $valid_values_rules = array();
  175. /**
  176. * All requests that hit this method should be requests for callbacks
  177. *
  178. * @internal
  179. *
  180. * @param string $method The method to create a callback for
  181. * @return callback The callback for the method requested
  182. */
  183. public function __get($method)
  184. {
  185. return array($this, $method);
  186. }
  187. /**
  188. * Adds fields to be checked for 1/0, t/f, true/false, yes/no
  189. *
  190. * @param string $field A field that should contain a boolean value
  191. * @param string ...
  192. * @param array :$fields Any number of fields that should contain a boolean value
  193. * @return fValidation The validation object, to allow for method chaining
  194. */
  195. public function addBooleanFields($field)
  196. {
  197. $args = func_get_args();
  198. if (count($args) == 1 && is_array($args[0])) {
  199. $args = $args[0];
  200. }
  201. foreach ($args as $arg) {
  202. $this->addRegexRule($arg, '#^0|1|t|f|true|false|yes|no$#iD', 'Please enter Yes or No');
  203. }
  204. return $this;
  205. }
  206. /**
  207. * Adds a callback validation of a field, with a custom error message
  208. *
  209. * @param string $field The field to test with the callback
  210. * @param callback $callback The callback to test the value with - this callback should accept a single string parameter and return a boolean
  211. * @param string $message The error message to return if the regular expression does not match the value
  212. * @return fValidation The validation object, to allow for method chaining
  213. */
  214. public function addCallbackRule($field, $callback, $message)
  215. {
  216. $this->callback_rules[$field] = array(
  217. 'callback' => $callback,
  218. 'message' => $message
  219. );
  220. return $this;
  221. }
  222. /**
  223. * Adds fields to be conditionally required if another field has any value, or specific values
  224. *
  225. * @param string|array $main_fields The fields(s) to check for a value
  226. * @param mixed $conditional_values If `NULL`, any value in the main field(s) will trigger the conditional field(s), otherwise the value must match this scalar value or be present in the array of values
  227. * @param string|array $conditional_fields The field(s) that are to be required
  228. * @return fValidation The validation object, to allow for method chaining
  229. */
  230. public function addConditionalRule($main_fields, $conditional_values, $conditional_fields)
  231. {
  232. settype($main_fields, 'array');
  233. settype($conditional_fields, 'array');
  234. if ($conditional_values !== NULL) {
  235. settype($conditional_values, 'array');
  236. }
  237. $this->conditional_rules[] = array(
  238. 'main_fields' => $main_fields,
  239. 'conditional_values' => $conditional_values,
  240. 'conditional_fields' => $conditional_fields
  241. );
  242. return $this;
  243. }
  244. /**
  245. * Adds form fields to the list of fields to be blank or a valid date
  246. *
  247. * Use ::addRequiredFields() disallow blank values.
  248. *
  249. * @param string $field A field that should contain a valid date
  250. * @param string ...
  251. * @param array :$fields Any number of fields that should contain a valid date
  252. * @return fValidation The validation object, to allow for method chaining
  253. */
  254. public function addDateFields($field)
  255. {
  256. $args = func_get_args();
  257. if (count($args) == 1 && is_array($args[0])) {
  258. $args = $args[0];
  259. }
  260. $this->date_fields = array_merge($this->date_fields, $args);
  261. return $this;
  262. }
  263. /**
  264. * Adds form fields to the list of fields to be blank or a valid email address
  265. *
  266. * Use ::addRequiredFields() disallow blank values.
  267. *
  268. * @param string $field A field that should contain a valid email address
  269. * @param string ...
  270. * @param array :$fields Any number of fields that should contain a valid email address
  271. * @return fValidation The validation object, to allow for method chaining
  272. */
  273. public function addEmailFields($field)
  274. {
  275. $args = func_get_args();
  276. if (count($args) == 1 && is_array($args[0])) {
  277. $args = $args[0];
  278. }
  279. foreach ($args as $arg) {
  280. $this->addRegexRule($arg, fEmail::EMAIL_REGEX, 'Please enter an email address in the form name@example.com');
  281. }
  282. return $this;
  283. }
  284. /**
  285. * Adds form fields to be checked for email injection
  286. *
  287. * Every field that is included in email headers should be passed to this
  288. * method.
  289. *
  290. * @param string $field A field to be checked for email injection
  291. * @param string ...
  292. * @param array :$fields Any number of fields to be checked for email injection
  293. * @return fValidation The validation object, to allow for method chaining
  294. */
  295. public function addEmailHeaderFields($field)
  296. {
  297. $args = func_get_args();
  298. if (count($args) == 1 && is_array($args[0])) {
  299. $args = $args[0];
  300. }
  301. foreach ($args as $arg) {
  302. $this->addRegexRule($arg, '#^[^\r\n]*$#D', 'Line breaks are not allowed');
  303. }
  304. return $this;
  305. }
  306. /**
  307. * Add a file upload field to be validated using an fUpload object
  308. *
  309. * @param string $field The field to validate
  310. * @param mixed $index The index for array file upload fields
  311. * @param fUpload $uploader The uploader to validate the field with
  312. * @param string :$field
  313. * @param fUpload :$uploader
  314. * @return fValidation The validation object, to allow for method chaining
  315. */
  316. public function addFileUploadRule($field, $index, $uploader=NULL)
  317. {
  318. if ($uploader === NULL && $index instanceof fUpload) {
  319. $uploader = $index;
  320. $index = NULL;
  321. }
  322. $this->file_upload_rules[] = array(
  323. 'field' => $field,
  324. 'index' => $index,
  325. 'uploader' => $uploader
  326. );
  327. return $this;
  328. }
  329. /**
  330. * Adds fields to be checked for float values
  331. *
  332. * @param string $field A field that should contain a float value
  333. * @param string ...
  334. * @param array :$fields Any number of fields that should contain a float value
  335. * @return fValidation The validation object, to allow for method chaining
  336. */
  337. public function addFloatFields($field)
  338. {
  339. $args = func_get_args();
  340. if (count($args) == 1 && is_array($args[0])) {
  341. $args = $args[0];
  342. }
  343. foreach ($args as $arg) {
  344. $this->addRegexRule($arg, '#^([+\-]?)(?:\d*\.\d+|\d+\.?)(?:e([+\-]?)(\d+))?$#iD', 'Please enter a number');
  345. }
  346. return $this;
  347. }
  348. /**
  349. * Adds fields to be checked for integer values
  350. *
  351. * @param string $field A field that should contain an integer value
  352. * @param string ...
  353. * @param array :$fields Any number of fields that should contain an integer value
  354. * @return fValidation The validation object, to allow for method chaining
  355. */
  356. public function addIntegerFields($field)
  357. {
  358. $args = func_get_args();
  359. if (count($args) == 1 && is_array($args[0])) {
  360. $args = $args[0];
  361. }
  362. foreach ($args as $arg) {
  363. $this->addRegexRule($arg, '#^[+\-]?\d+(?:e[+]?\d+)?$#iD', 'Please enter a whole number');
  364. }
  365. return $this;
  366. }
  367. /**
  368. * Adds a rule to make sure at least one field of multiple has a value
  369. *
  370. * @param string $field One of the fields to check for a value
  371. * @param string $field_2 Another field to check for a value
  372. * @param string ...
  373. * @return fValidation The validation object, to allow for method chaining
  374. */
  375. public function addOneOrMoreRule($field, $field_2)
  376. {
  377. $fields = func_get_args();
  378. $this->one_or_more_rules[] = $fields;
  379. return $this;
  380. }
  381. /**
  382. * Adds a rule to make sure at exactly one field of multiple has a value
  383. *
  384. * @param string $field One of the fields to check for a value
  385. * @param string $field_2 Another field to check for a value
  386. * @param string ...
  387. * @return fValidation The validation object, to allow for method chaining
  388. */
  389. public function addOnlyOneRule($field, $field_2)
  390. {
  391. $fields = func_get_args();
  392. $this->only_one_rules[] = $fields;
  393. return $this;
  394. }
  395. /**
  396. * Adds a call to [http://php.net/preg_replace `preg_replace()`] for each message
  397. *
  398. * Replacement is done right before the messages are reordered and returned.
  399. *
  400. * If a message is an empty string after replacement, it will be
  401. * removed from the list of messages.
  402. *
  403. * @param string $search The PCRE regex to search for - see http://php.net/pcre for details
  404. * @param string $replace The string to replace with - all $ and \ are used in back references and must be escaped with a \ when meant literally
  405. * @param array :$replacements An associative array with keys being regular expressions to search for and values being the string to replace with
  406. * @return fValidation The validation object, to allow for method chaining
  407. */
  408. public function addRegexReplacement($search, $replace=NULL)
  409. {
  410. if (is_array($search) && $replace === NULL) {
  411. $this->regex_replacements = array_merge($this->regex_replacements, $search);
  412. } else {
  413. $this->regex_replacements[$search] = $replace;
  414. }
  415. return $this;
  416. }
  417. /**
  418. * Adds regular expression validation of a field, with a custom error message
  419. *
  420. * @param string $field The field to test with the regular expression
  421. * @param string $regex The PCRE regex to search for - see http://php.net/pcre for details
  422. * @param string $message The error message to return if the regular expression does not match the value
  423. * @return fValidation The validation object, to allow for method chaining
  424. */
  425. public function addRegexRule($field, $regex, $message)
  426. {
  427. if (!isset($this->regex_rules[$field])) {
  428. $this->regex_rules[$field] = array();
  429. }
  430. $this->regex_rules[$field][] = array(
  431. 'regex' => $regex,
  432. 'message' => $message
  433. );
  434. return $this;
  435. }
  436. /**
  437. * Adds form fields to be required
  438. *
  439. * @param string $field A field to require a value for
  440. * @param string ...
  441. * @param array :$fields Any number of fields to require a value for
  442. * @return fValidation The validation object, to allow for method chaining
  443. */
  444. public function addRequiredFields($field)
  445. {
  446. $args = func_get_args();
  447. if (count($args) == 1 && is_array($args[0])) {
  448. $args = $args[0];
  449. }
  450. $this->required_fields = array_merge($this->required_fields, $args);
  451. return $this;
  452. }
  453. /**
  454. * Adds a call to [http://php.net/str_replace `str_replace()`] for each message
  455. *
  456. * Replacement is done right before the messages are reordered and returned.
  457. *
  458. * If a message is an empty string after replacement, it will be
  459. * removed from the list of messages.
  460. *
  461. * @param string $search The string to search for
  462. * @param string $replace The string to replace with
  463. * @param array :$replacements An associative array with keys being strings to search for and values being the string to replace with
  464. * @return fValidation The validation object, to allow for method chaining
  465. */
  466. public function addStringReplacement($search, $replace=NULL)
  467. {
  468. $this->string_replacements[$search] = $replace;
  469. return $this;
  470. }
  471. /**
  472. * Adds form fields to the list of fields to be blank or a valid URL
  473. *
  474. * Use ::addRequiredFields() disallow blank values.
  475. *
  476. * @param string $field A field that should contain a valid URL
  477. * @param string ...
  478. * @param array :$fields Any number of fields that should contain a valid URL
  479. * @return fValidation The validation object, to allow for method chaining
  480. */
  481. public function addURLFields($field)
  482. {
  483. $args = func_get_args();
  484. if (count($args) == 1 && is_array($args[0])) {
  485. $args = $args[0];
  486. }
  487. $ip_regex = '(?:(?:[01]?\d?\d|2[0-4]\d|25[0-5])\.){3}(?:[01]?\d?\d|2[0-4]\d|25[0-5])';
  488. $hostname_regex = '[a-z]+(?:[a-z0-9\-]*[a-z0-9]\.?|\.)*';
  489. $domain_regex = '([a-z]+([a-z0-9\-]*[a-z0-9])?\.)+[a-z]{2,}';
  490. $regex = '#^(https?://(' . $ip_regex . '|' . $hostname_regex . ')(?=/|$)|' . $domain_regex . '(?=/|$)|/)#i';
  491. foreach ($args as $arg) {
  492. $this->addRegexRule($arg, $regex, 'Please enter a URL in the form http://www.example.com/page');
  493. }
  494. return $this;
  495. }
  496. /**
  497. * Adds a rule to make sure a field has one of the specified valid values
  498. *
  499. * A strict comparison will be made from the string request value to the
  500. * array of valid values.
  501. *
  502. * @param string $field The field to check the value of
  503. * @param array $valid_values The valid values
  504. * @return fValidation The validation object, to allow for method chaining
  505. */
  506. public function addValidValuesRule($field, $valid_values)
  507. {
  508. $this->valid_values_rules[$field] = $this->castToStrings($valid_values);
  509. return $this;
  510. }
  511. /**
  512. * Converts an array of values to string, recursively
  513. *
  514. * @param array $values An array of values to cast to strings
  515. * @return array The values, casted to strings, but preserving multi-dimensional arrays
  516. */
  517. private function castToStrings($values)
  518. {
  519. $casted_values = array();
  520. foreach ($values as $value) {
  521. if (is_object($value)) {
  522. if (method_exists($value, '__toString')) {
  523. $casted_values[] = $value->__toString();
  524. } else {
  525. $casted_values[] = (string) $value;
  526. }
  527. } elseif (!is_array($value)) {
  528. $casted_values[] = (string) $value;
  529. } else {
  530. $casted_values[] = $this->castToStrings($value);
  531. }
  532. }
  533. return $casted_values;
  534. }
  535. /**
  536. * Runs all callback validation rules
  537. *
  538. * @param array &$messages The messages to display to the user
  539. * @return void
  540. */
  541. private function checkCallbackRules(&$messages)
  542. {
  543. foreach ($this->callback_rules as $field => $rule) {
  544. $value = fRequest::get($field);
  545. if (self::stringlike($value) && !call_user_func($rule['callback'], $value)) {
  546. $messages[$field] = self::compose(
  547. '%s' . $rule['message'],
  548. fValidationException::formatField($this->makeFieldName($field))
  549. );
  550. }
  551. }
  552. }
  553. /**
  554. * Checks the conditional validation rules
  555. *
  556. * @param array &$messages The messages to display to the user
  557. * @return void
  558. */
  559. private function checkConditionalRules(&$messages)
  560. {
  561. foreach ($this->conditional_rules as $rule) {
  562. $check_for_missing_values = FALSE;
  563. foreach ($rule['main_fields'] as $main_field) {
  564. $matches_conditional_value = $rule['conditional_values'] !== NULL && in_array(fRequest::get($main_field), $rule['conditional_values']);
  565. $has_some_value = $rule['conditional_values'] === NULL && self::hasValue($main_field);
  566. if ($matches_conditional_value || $has_some_value) {
  567. $check_for_missing_values = TRUE;
  568. break;
  569. }
  570. }
  571. if (!$check_for_missing_values) {
  572. return;
  573. }
  574. foreach ($rule['conditional_fields'] as $conditional_field) {
  575. if (self::hasValue($conditional_field)) { continue; }
  576. $messages[$conditional_field] = self::compose(
  577. '%sPlease enter a value',
  578. fValidationException::formatField($this->makeFieldName($conditional_field))
  579. );
  580. }
  581. }
  582. }
  583. /**
  584. * Validates the date fields, requiring that any date fields that have a value that can be interpreted as a date
  585. *
  586. * @param array &$messages The messages to display to the user
  587. * @return void
  588. */
  589. private function checkDateFields(&$messages)
  590. {
  591. foreach ($this->date_fields as $date_field) {
  592. $value = trim(fRequest::get($date_field));
  593. if (self::stringlike($value)) {
  594. try {
  595. new fTimestamp($value);
  596. } catch (fValidationException $e) {
  597. $messages[$date_field] = self::compose(
  598. '%sPlease enter a date',
  599. fValidationException::formatField($this->makeFieldName($date_field))
  600. );
  601. }
  602. }
  603. }
  604. }
  605. /**
  606. * Checks the file upload validation rules
  607. *
  608. * @param array &$messages The messages to display to the user
  609. * @return void
  610. */
  611. private function checkFileUploadRules(&$messages)
  612. {
  613. foreach ($this->file_upload_rules as $rule) {
  614. $message = $rule['uploader']->validate($rule['field'], $rule['index'], TRUE);
  615. if ($message) {
  616. $field = $rule['index'] === NULL ? $rule['field'] : $rule['field'] . '[' . $rule['index'] . ']';
  617. $messages[$field] = self::compose(
  618. '%s' . $message,
  619. fValidationException::formatField($this->makeFieldName($field))
  620. );
  621. }
  622. }
  623. }
  624. /**
  625. * Ensures all of the one-or-more rules is met
  626. *
  627. * @param array &$messages The messages to display to the user
  628. * @return void
  629. */
  630. private function checkOneOrMoreRules(&$messages)
  631. {
  632. foreach ($this->one_or_more_rules as $fields) {
  633. $found = FALSE;
  634. foreach ($fields as $field) {
  635. if (self::hasValue($field)) {
  636. $found = TRUE;
  637. break;
  638. }
  639. }
  640. if (!$found) {
  641. $messages[join(',', $fields)] = self::compose(
  642. '%sPlease enter a value for at least one',
  643. fValidationException::formatField(join(', ', array_map($this->makeFieldName, $fields)))
  644. );
  645. }
  646. }
  647. }
  648. /**
  649. * Ensures all of the only-one rules is met
  650. *
  651. * @param array &$messages The messages to display to the user
  652. * @return void
  653. */
  654. private function checkOnlyOneRules(&$messages)
  655. {
  656. foreach ($this->only_one_rules as $fields) {
  657. $found = FALSE;
  658. foreach ($fields as $field) {
  659. if (self::hasValue($field)) {
  660. if ($found) {
  661. $messages[join(',', $fields)] = self::compose(
  662. '%sPlease enter a value for only one',
  663. fValidationException::formatField(join(', ', array_map($this->makeFieldName, $fields)))
  664. );
  665. continue 2;
  666. }
  667. $found = TRUE;
  668. }
  669. }
  670. if (!$found) {
  671. $messages[join(',', $fields)] = self::compose(
  672. '%sPlease enter a value for one',
  673. fValidationException::formatField(join(', ', array_map($this->makeFieldName, $fields)))
  674. );
  675. }
  676. }
  677. }
  678. /**
  679. * Runs all regex validation rules
  680. *
  681. * @param array &$messages The messages to display to the user
  682. * @return void
  683. */
  684. private function checkRegexRules(&$messages)
  685. {
  686. foreach ($this->regex_rules as $field => $rules) {
  687. $value = fRequest::get($field);
  688. foreach ($rules as $rule) {
  689. if (self::stringlike($value) && !preg_match($rule['regex'], $value)) {
  690. $messages[$field] = self::compose(
  691. '%s' . $rule['message'],
  692. fValidationException::formatField($this->makeFieldName($field))
  693. );
  694. }
  695. }
  696. }
  697. }
  698. /**
  699. * Validates the required fields, adding any missing fields to the messages array
  700. *
  701. * @param array &$messages The messages to display to the user
  702. * @return void
  703. */
  704. private function checkRequiredFields(&$messages)
  705. {
  706. foreach ($this->required_fields as $required_field) {
  707. if (!self::hasValue($required_field)) {
  708. $messages[$required_field] = self::compose(
  709. '%sPlease enter a value',
  710. fValidationException::formatField($this->makeFieldName($required_field))
  711. );
  712. }
  713. }
  714. }
  715. /**
  716. * Runs all valid-values rules
  717. *
  718. * @param array &$messages The messages to display to the user
  719. * @return void
  720. */
  721. private function checkValidValuesRules(&$messages)
  722. {
  723. foreach ($this->valid_values_rules as $field => $valid_values) {
  724. $value = fRequest::get($field);
  725. if (self::stringlike($value) && !in_array($value, $valid_values, TRUE)) {
  726. $messages[$field] = self::compose(
  727. '%1$sPlease choose from one of the following: %2$s',
  728. fValidationException::formatField($this->makeFieldName($field)),
  729. $this->joinRecursive(', ', $valid_values)
  730. );
  731. }
  732. }
  733. }
  734. /**
  735. * Joins a multi-dimensional array recursively
  736. *
  737. * @param string $glue The string to join the array elements with
  738. * @param array $values The array of values to join together
  739. * @return string The joined array
  740. */
  741. private function joinRecursive($glue, $values)
  742. {
  743. $joined = array();
  744. foreach ($values as $value) {
  745. if (is_array($value)) {
  746. $joined[] = '(' . $this->joinRecursive($glue, $value) . ')';
  747. } else {
  748. $joined[] = $value;
  749. }
  750. }
  751. return join($glue, $joined);
  752. }
  753. /**
  754. * Creates the name for a field taking into account custom field names
  755. *
  756. * @param string $field The field to get the name for
  757. * @return string The field name
  758. */
  759. private function makeFieldName($field)
  760. {
  761. if (isset($this->field_names[$field])) {
  762. return $this->field_names[$field];
  763. }
  764. $suffix = '';
  765. $bracket_pos = strpos($field, '[');
  766. if ($bracket_pos !== FALSE) {
  767. $array_dereference = substr($field, $bracket_pos);
  768. $field = substr($field, 0, $bracket_pos);
  769. preg_match_all('#(?<=\[)[^\[\]]+(?=\])#', $array_dereference, $array_keys, PREG_SET_ORDER);
  770. $array_keys = array_map('current', $array_keys);
  771. foreach ($array_keys as $array_key) {
  772. if (is_numeric($array_key)) {
  773. $suffix .= ' #' . ($array_key+1);
  774. } else {
  775. $suffix .= ' ' . fGrammar::humanize($array_key);
  776. }
  777. }
  778. }
  779. return fGrammar::humanize($field) . $suffix;
  780. }
  781. /**
  782. * Reorders an array of messages based on the requested order
  783. *
  784. * @param array $messages An array of the messages
  785. * @return array The reordered messages
  786. */
  787. private function reorderMessages($messages)
  788. {
  789. if (!$this->message_order) {
  790. return $messages;
  791. }
  792. $ordered_items = array_fill(0, sizeof($this->message_order), array());
  793. $other_items = array();
  794. foreach ($messages as $key => $message) {
  795. foreach ($this->message_order as $num => $match_string) {
  796. if (fUTF8::ipos($message, $match_string) !== FALSE) {
  797. $ordered_items[$num][$key] = $message;
  798. continue 2;
  799. }
  800. }
  801. $other_items[$key] = $message;
  802. }
  803. $final_list = array();
  804. foreach ($ordered_items as $ordered_item) {
  805. $final_list = array_merge($final_list, $ordered_item);
  806. }
  807. return array_merge($final_list, $other_items);
  808. }
  809. /**
  810. * Allows overriding the default name used for a field in the error message
  811. *
  812. * By default, all fields are referred to by the field name run through
  813. * fGrammar::humanize(). This may not be correct for acronyms or complex
  814. * field names.
  815. *
  816. * @param string $field The field to set the custom name for
  817. * @param string $name The custom name for the field
  818. * @param array :$field_names An associative array of custom field names where the keys are the field and the values are the names
  819. * @return fValidation The validation object, to allow for method chaining
  820. */
  821. public function overrideFieldName($field, $name=NULL)
  822. {
  823. if (is_array($field)) {
  824. $this->field_names = array_merge($this->field_names, $field);
  825. } else {
  826. $this->field_names[$field] = $name;
  827. }
  828. return $this;
  829. }
  830. /**
  831. * Allows setting the order that the individual errors in a message will be displayed
  832. *
  833. * All string comparisons during the reordering process are done in a
  834. * case-insensitive manner.
  835. *
  836. * @param string $match The string match to order first
  837. * @param string $match_2 The string match to order second
  838. * @param string ...
  839. * @return fValidation The validation object, to allow for method chaining
  840. */
  841. public function setMessageOrder($match, $match_2=NULL)
  842. {
  843. $args = func_get_args();
  844. if (sizeof($args) == 1 && is_array($args[0])) {
  845. $args = $args[0];
  846. }
  847. uasort($args, array('self', 'sortMessageMatches'));
  848. $this->message_order = $args;
  849. return $this;
  850. }
  851. /**
  852. * Checks for required fields, email field formatting and email header injection using values previously set
  853. *
  854. * @throws fValidationException When one of the options set for the object is violated
  855. *
  856. * @param boolean $return_messages If an array of validation messages should be returned instead of an exception being thrown
  857. * @param boolean $remove_field_names If field names should be removed from the returned messages, leaving just the message itself
  858. * @return void|array If $return_messages is TRUE, an array of validation messages will be returned
  859. */
  860. public function validate($return_messages=FALSE, $remove_field_names=FALSE)
  861. {
  862. if (!$this->callback_rules &&
  863. !$this->conditional_rules &&
  864. !$this->date_fields &&
  865. !$this->file_upload_rules &&
  866. !$this->one_or_more_rules &&
  867. !$this->only_one_rules &&
  868. !$this->regex_rules &&
  869. !$this->required_fields &&
  870. !$this->valid_values_rules) {
  871. throw new fProgrammerException(
  872. 'No fields or rules have been added for validation'
  873. );
  874. }
  875. $messages = array();
  876. $this->checkRequiredFields($messages);
  877. $this->checkFileUploadRules($messages);
  878. $this->checkConditionalRules($messages);
  879. $this->checkOneOrMoreRules($messages);
  880. $this->checkOnlyOneRules($messages);
  881. $this->checkValidValuesRules($messages);
  882. $this->checkDateFields($messages);
  883. $this->checkRegexRules($messages);
  884. $this->checkCallbackRules($messages);
  885. if ($this->regex_replacements) {
  886. $messages = preg_replace(
  887. array_keys($this->regex_replacements),
  888. array_values($this->regex_replacements),
  889. $messages
  890. );
  891. }
  892. if ($this->string_replacements) {
  893. $messages = str_replace(
  894. array_keys($this->string_replacements),
  895. array_values($this->string_replacements),
  896. $messages
  897. );
  898. }
  899. $messages = $this->reorderMessages($messages);
  900. if ($return_messages) {
  901. if ($remove_field_names) {
  902. $messages = fValidationException::removeFieldNames($messages);
  903. }
  904. return $messages;
  905. }
  906. if ($messages) {
  907. throw new fValidationException(
  908. 'The following problems were found:',
  909. $messages
  910. );
  911. }
  912. }
  913. }
  914. /**
  915. * Copyright (c) 2007-2010 Will Bond <will@flourishlib.com>
  916. *
  917. * Permission is hereby granted, free of charge, to any person obtaining a copy
  918. * of this software and associated documentation files (the "Software"), to deal
  919. * in the Software without restriction, including without limitation the rights
  920. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  921. * copies of the Software, and to permit persons to whom the Software is
  922. * furnished to do so, subject to the following conditions:
  923. *
  924. * The above copyright notice and this permission notice shall be included in
  925. * all copies or substantial portions of the Software.
  926. *
  927. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  928. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  929. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  930. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  931. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  932. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  933. * THE SOFTWARE.
  934. */