PageRenderTime 57ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 0ms

/classes/fValidation.php

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