PageRenderTime 65ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

/base/lib/flourishlib/fORMValidation.php

https://bitbucket.org/thanhtungnguyenphp/monitos
PHP | 1597 lines | 866 code | 231 blank | 500 comment | 150 complexity | f12070cb500374c71e67eac1db88801d MD5 | raw file

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. /**
  3. * Handles validation for fActiveRecord classes
  4. *
  5. * @copyright Copyright (c) 2007-2011 Will Bond, others
  6. * @author Will Bond [wb] <will@flourishlib.com>
  7. * @author Jeff Turcotte [jt] <jeff.turcotte@gmail.com>
  8. * @license http://flourishlib.com/license
  9. *
  10. * @package Flourish
  11. * @link http://flourishlib.com/fORMValidation
  12. *
  13. * @version 1.0.0b30
  14. * @changes 1.0.0b30 Fixed a bug with ::setMessageOrder() not accepting a variable number of parameters like fValidation::setMessageOrder() does [wb, 2011-03-07]
  15. * @changes 1.0.0b29 Updated ::addManyToManyRule() and ::addOneToManyRule() to prefix any namespace from `$class` to `$related_class` if not already present [wb, 2010-11-24]
  16. * @changes 1.0.0b28 Updated the class to work with the new nested array structure for validation messages [wb, 2010-10-03]
  17. * @changes 1.0.0b27 Fixed ::hasValue() to properly detect zero-value floats, made ::hasValue() internal public [wb, 2010-07-26]
  18. * @changes 1.0.0b26 Improved the error message for integers to say `whole number` instead of just `number` [wb, 2010-05-29]
  19. * @changes 1.0.0b25 Added ::addRegexRule(), changed validation messages array to use column name keys [wb, 2010-05-26]
  20. * @changes 1.0.0b24 Added ::addRequiredRule() for required columns that aren't automatically handled via schema detection [wb, 2010-04-06]
  21. * @changes 1.0.0b23 Added support for checking integers and floats to ensure they fit within the range imposed by the database schema [wb, 2010-03-17]
  22. * @changes 1.0.0b22 Made the value checking for one-or-more and only-one rules more robust when detecting the absence of a value [wb, 2009-12-17]
  23. * @changes 1.0.0b21 Fixed a bug affecting where conditions with columns that are not null but have a default value [wb, 2009-11-03]
  24. * @changes 1.0.0b20 Updated code for the new fORMDatabase and fORMSchema APIs [wb, 2009-10-28]
  25. * @changes 1.0.0b19 Changed SQL statements to use value placeholders, identifier escaping and schema support [wb, 2009-10-22]
  26. * @changes 1.0.0b18 Fixed ::checkOnlyOneRule() and ::checkOneOrMoreRule() to consider blank strings as NULL [wb, 2009-08-21]
  27. * @changes 1.0.0b17 Added @internal methods ::removeStringReplacement() and ::removeRegexReplacement() [wb, 2009-07-29]
  28. * @changes 1.0.0b16 Backwards Compatibility Break - renamed ::addConditionalValidationRule() to ::addConditionalRule(), ::addManyToManyValidationRule() to ::addManyToManyRule(), ::addOneOrMoreValidationRule() to ::addOneOrMoreRule(), ::addOneToManyValidationRule() to ::addOneToManyRule(), ::addOnlyOneValidationRule() to ::addOnlyOneRule(), ::addValidValuesValidationRule() to ::addValidValuesRule() [wb, 2009-07-13]
  29. * @changes 1.0.0b15 Added ::addValidValuesValidationRule() [wb/jt, 2009-07-13]
  30. * @changes 1.0.0b14 Added ::addStringReplacement() and ::addRegexReplacement() for simple validation message modification [wb, 2009-07-01]
  31. * @changes 1.0.0b13 Changed ::reorderMessages() to compare string in a case-insensitive manner [wb, 2009-06-30]
  32. * @changes 1.0.0b12 Updated ::addConditionalValidationRule() to allow any number of `$main_columns`, and if any of those have a matching value, the condtional columns will be required [wb, 2009-06-30]
  33. * @changes 1.0.0b11 Fixed a couple of bugs with validating related records [wb, 2009-06-26]
  34. * @changes 1.0.0b10 Fixed UNIQUE constraint checking so it is only done once per constraint, fixed some UTF-8 case sensitivity issues [wb, 2009-06-17]
  35. * @changes 1.0.0b9 Updated code for new fORM API [wb, 2009-06-15]
  36. * @changes 1.0.0b8 Updated code to use new fValidationException::formatField() method [wb, 2009-06-04]
  37. * @changes 1.0.0b7 Updated ::validateRelated() to use new fORMRelated::validate() method and ::checkRelatedOneOrMoreRule() to use new `$related_records` structure [wb, 2009-06-02]
  38. * @changes 1.0.0b6 Changed date/time/timestamp checking from `strtotime()` to fDate/fTime/fTimestamp for better localization support [wb, 2009-06-01]
  39. * @changes 1.0.0b5 Fixed a bug in ::checkOnlyOneRule() where no values would not be flagged as an error [wb, 2009-04-23]
  40. * @changes 1.0.0b4 Fixed a bug in ::checkUniqueConstraints() related to case-insensitive columns [wb, 2009-02-15]
  41. * @changes 1.0.0b3 Implemented proper fix for ::addManyToManyValidationRule() [wb, 2008-12-12]
  42. * @changes 1.0.0b2 Fixed a bug with ::addManyToManyValidationRule() [wb, 2008-12-08]
  43. * @changes 1.0.0b The initial implementation [wb, 2007-08-04]
  44. */
  45. class fORMValidation
  46. {
  47. // The following constants allow for nice looking callbacks to static methods
  48. const addConditionalRule = 'fORMValidation::addConditionalRule';
  49. const addManyToManyRule = 'fORMValidation::addManyToManyRule';
  50. const addOneOrMoreRule = 'fORMValidation::addOneOrMoreRule';
  51. const addOneToManyRule = 'fORMValidation::addOneToManyRule';
  52. const addOnlyOneRule = 'fORMValidation::addOnlyOneRule';
  53. const addRegexReplacement = 'fORMValidation::addRegexReplacement';
  54. const addRegexRule = 'fORMValidation::addRegexRule';
  55. const addRequiredRule = 'fORMValidation::addRequiredRule';
  56. const addStringReplacement = 'fORMValidation::addStringReplacement';
  57. const addValidValuesRule = 'fORMValidation::addValidValuesRule';
  58. const hasValue = 'fORMValidation::hasValue';
  59. const inspect = 'fORMValidation::inspect';
  60. const removeStringReplacement = 'fORMValidation::removeStringReplacement';
  61. const removeRegexReplacement = 'fORMValidation::removeRegexReplacement';
  62. const reorderMessages = 'fORMValidation::reorderMessages';
  63. const replaceMessages = 'fORMValidation::replaceMessages';
  64. const reset = 'fORMValidation::reset';
  65. const setColumnCaseInsensitive = 'fORMValidation::setColumnCaseInsensitive';
  66. const setMessageOrder = 'fORMValidation::setMessageOrder';
  67. const validate = 'fORMValidation::validate';
  68. const validateRelated = 'fORMValidation::validateRelated';
  69. /**
  70. * Columns that should be treated as case insensitive when checking uniqueness
  71. *
  72. * @var array
  73. */
  74. static private $case_insensitive_columns = array();
  75. /**
  76. * Conditional rules
  77. *
  78. * @var array
  79. */
  80. static private $conditional_rules = array();
  81. /**
  82. * Ordering rules for messages
  83. *
  84. * @var array
  85. */
  86. static private $message_orders = array();
  87. /**
  88. * One or more rules
  89. *
  90. * @var array
  91. */
  92. static private $one_or_more_rules = array();
  93. /**
  94. * Only one rules
  95. *
  96. * @var array
  97. */
  98. static private $only_one_rules = array();
  99. /**
  100. * Regular expression replacements performed on each message
  101. *
  102. * @var array
  103. */
  104. static private $regex_replacements = array();
  105. /**
  106. * Rules that require at least one or more *-to-many related records to be associated
  107. *
  108. * @var array
  109. */
  110. static private $related_one_or_more_rules = array();
  111. /**
  112. * Rules that require a value to match a regular expression
  113. *
  114. * @var array
  115. */
  116. static private $regex_rules = array();
  117. /**
  118. * Rules that require a value be present in a column even if the database schema doesn't require it
  119. *
  120. * @var array
  121. */
  122. static private $required_rules = array();
  123. /**
  124. * String replacements performed on each message
  125. *
  126. * @var array
  127. */
  128. static private $string_replacements = array();
  129. /**
  130. * Valid values rules
  131. *
  132. * @var array
  133. */
  134. static private $valid_values_rules = array();
  135. /**
  136. * Adds a conditional rule
  137. *
  138. * If a non-empty value is found in one of the `$main_columns`, or if
  139. * specified, a value from the `$conditional_values` array, all of the
  140. * `$conditional_columns` will also be required to have a value.
  141. *
  142. * @param mixed $class The class name or instance of the class this rule applies to
  143. * @param string|array $main_columns The column(s) to check for a value
  144. * @param mixed $conditional_values If `NULL`, any value in the main column will trigger the conditional column(s), otherwise the value must match this scalar value or be present in the array of values
  145. * @param string|array $conditional_columns The column(s) that are to be required
  146. * @return void
  147. */
  148. static public function addConditionalRule($class, $main_columns, $conditional_values, $conditional_columns)
  149. {
  150. $class = fORM::getClass($class);
  151. if (!isset(self::$conditional_rules[$class])) {
  152. self::$conditional_rules[$class] = array();
  153. }
  154. settype($main_columns, 'array');
  155. settype($conditional_columns, 'array');
  156. if ($conditional_values !== NULL) {
  157. settype($conditional_values, 'array');
  158. }
  159. $rule = array();
  160. $rule['main_columns'] = $main_columns;
  161. $rule['conditional_values'] = $conditional_values;
  162. $rule['conditional_columns'] = $conditional_columns;
  163. self::$conditional_rules[$class][] = $rule;
  164. }
  165. /**
  166. * Add a many-to-many rule that requires at least one related record is associated with the current record
  167. *
  168. * @param mixed $class The class name or instance of the class to add the rule for
  169. * @param string $related_class The name of the related class
  170. * @param string $route The route to the related class
  171. * @return void
  172. */
  173. static public function addManyToManyRule($class, $related_class, $route=NULL)
  174. {
  175. $class = fORM::getClass($class);
  176. $related_class = fORM::getRelatedClass($class, $related_class);
  177. if (!isset(self::$related_one_or_more_rules[$class])) {
  178. self::$related_one_or_more_rules[$class] = array();
  179. }
  180. if (!isset(self::$related_one_or_more_rules[$class][$related_class])) {
  181. self::$related_one_or_more_rules[$class][$related_class] = array();
  182. }
  183. $route = fORMSchema::getRouteName(
  184. fORMSchema::retrieve($class),
  185. fORM::tablize($class),
  186. fORM::tablize($related_class),
  187. $route,
  188. 'many-to-many'
  189. );
  190. self::$related_one_or_more_rules[$class][$related_class][$route] = TRUE;
  191. }
  192. /**
  193. * Adds a one-or-more rule that requires at least one of the columns specified has a value
  194. *
  195. * @param mixed $class The class name or instance of the class the columns exists in
  196. * @param array $columns The columns to check
  197. * @return void
  198. */
  199. static public function addOneOrMoreRule($class, $columns)
  200. {
  201. $class = fORM::getClass($class);
  202. settype($columns, 'array');
  203. if (!isset(self::$one_or_more_rules[$class])) {
  204. self::$one_or_more_rules[$class] = array();
  205. }
  206. $rule = array();
  207. $rule['columns'] = $columns;
  208. self::$one_or_more_rules[$class][] = $rule;
  209. }
  210. /**
  211. * Add a one-to-many rule that requires at least one related record is associated with the current record
  212. *
  213. * @param mixed $class The class name or instance of the class to add the rule for
  214. * @param string $related_class The name of the related class
  215. * @param string $route The route to the related class
  216. * @return void
  217. */
  218. static public function addOneToManyRule($class, $related_class, $route=NULL)
  219. {
  220. $class = fORM::getClass($class);
  221. $related_class = fORM::getRelatedClass($class, $related_class);
  222. if (!isset(self::$related_one_or_more_rules[$class])) {
  223. self::$related_one_or_more_rules[$class] = array();
  224. }
  225. if (!isset(self::$related_one_or_more_rules[$class][$related_class])) {
  226. self::$related_one_or_more_rules[$class][$related_class] = array();
  227. }
  228. $route = fORMSchema::getRouteName(
  229. fORMSchema::retrieve($class),
  230. fORM::tablize($class),
  231. fORM::tablize($related_class),
  232. $route,
  233. 'one-to-many'
  234. );
  235. self::$related_one_or_more_rules[$class][$related_class][$route] = TRUE;
  236. }
  237. /**
  238. * Add an only-one rule that requires exactly one of the columns must have a value
  239. *
  240. * @param mixed $class The class name or instance of the class the columns exists in
  241. * @param array $columns The columns to check
  242. * @return void
  243. */
  244. static public function addOnlyOneRule($class, $columns)
  245. {
  246. $class = fORM::getClass($class);
  247. settype($columns, 'array');
  248. if (!isset(self::$only_one_rules[$class])) {
  249. self::$only_one_rules[$class] = array();
  250. }
  251. $rule = array();
  252. $rule['columns'] = $columns;
  253. self::$only_one_rules[$class][] = $rule;
  254. }
  255. /**
  256. * Adds a call to [http://php.net/preg_replace `preg_replace()`] for each message
  257. *
  258. * Regex replacement is done after the `post::validate()` hook, and right
  259. * before the messages are reordered.
  260. *
  261. * If a message is an empty string after replacement, it will be
  262. * removed from the list of messages.
  263. *
  264. * @param mixed $class The class name or instance of the class the columns exists in
  265. * @param string $search The PCRE regex to search for - see http://php.net/pcre for details
  266. * @param string $replace The string to replace with - all $ and \ are used in back references and must be escaped with a \ when meant literally
  267. * @return void
  268. */
  269. static public function addRegexReplacement($class, $search, $replace)
  270. {
  271. $class = fORM::getClass($class);
  272. if (!isset(self::$regex_replacements[$class])) {
  273. self::$regex_replacements[$class] = array(
  274. 'search' => array(),
  275. 'replace' => array()
  276. );
  277. }
  278. self::$regex_replacements[$class]['search'][] = $search;
  279. self::$regex_replacements[$class]['replace'][] = $replace;
  280. }
  281. /**
  282. * Adds a rule to validate a column against a PCRE regular expression - the rule is not run if the value is `NULL`
  283. *
  284. * @param mixed $class The class name or instance of the class the columns exists in
  285. * @param string $column The column to match with the regex
  286. * @param string $regex The PCRE regex to match against - see http://php.net/pcre for details
  287. * @param string $message The message to use if the value does not match the regular expression
  288. * @return void
  289. */
  290. static public function addRegexRule($class, $column, $regex, $message)
  291. {
  292. $class = fORM::getClass($class);
  293. if (!isset(self::$regex_rules[$class])) {
  294. self::$regex_rules[$class] = array();
  295. }
  296. self::$regex_rules[$class][$column] = array(
  297. 'regex' => $regex,
  298. 'message' => $message
  299. );
  300. }
  301. /**
  302. * Requires that a column have a non-`NULL` value
  303. *
  304. * Before using this method, try setting the database column to `NOT NULL`
  305. * and remove any default value. Such a configuration will trigger the same
  306. * functionality as this method, and will enforce the rule on the database
  307. * level for any other code that queries it.
  308. *
  309. * @param mixed $class The class name or instance of the class the column(s) exists in
  310. * @param array $columns The column or columns to check - each column will require a value
  311. * @return void
  312. */
  313. static public function addRequiredRule($class, $columns)
  314. {
  315. $class = fORM::getClass($class);
  316. settype($columns, 'array');
  317. if (!isset(self::$required_rules[$class])) {
  318. self::$required_rules[$class] = array();
  319. }
  320. foreach ($columns as $column) {
  321. self::$required_rules[$class][$column] = TRUE;
  322. }
  323. }
  324. /**
  325. * Adds a call to [http://php.net/str_replace `str_replace()`] for each message
  326. *
  327. * String replacement is done after the `post::validate()` hook, and right
  328. * before the messages are reordered.
  329. *
  330. * If a message is an empty string after replacement, it will be
  331. * removed from the list of messages.
  332. *
  333. * @param mixed $class The class name or instance of the class the columns exists in
  334. * @param string $search The string to search for
  335. * @param string $replace The string to replace with
  336. * @return void
  337. */
  338. static public function addStringReplacement($class, $search, $replace)
  339. {
  340. $class = fORM::getClass($class);
  341. if (!isset(self::$string_replacements[$class])) {
  342. self::$string_replacements[$class] = array(
  343. 'search' => array(),
  344. 'replace' => array()
  345. );
  346. }
  347. self::$string_replacements[$class]['search'][] = $search;
  348. self::$string_replacements[$class]['replace'][] = $replace;
  349. }
  350. /**
  351. * Restricts a column to having only a value from the list of valid values
  352. *
  353. * Please note that `NULL` values are always allowed, even if not listed in
  354. * the `$valid_values` array, if the column is not set as `NOT NULL`.
  355. *
  356. * This functionality can also be accomplished by added a `CHECK` constraint
  357. * on the column in the database, or using a MySQL `ENUM` data type.
  358. *
  359. * @param mixed $class The class name or instance of the class this rule applies to
  360. * @param string $column The column to validate
  361. * @param array $valid_values The valid values to check - `NULL` values are always allows if the column is not set to `NOT NULL`
  362. * @return void
  363. */
  364. static public function addValidValuesRule($class, $column, $valid_values)
  365. {
  366. $class = fORM::getClass($class);
  367. if (!isset(self::$valid_values_rules[$class])) {
  368. self::$valid_values_rules[$class] = array();
  369. }
  370. settype($valid_values, 'array');
  371. self::$valid_values_rules[$class][$column] = $valid_values;
  372. fORM::registerInspectCallback($class, $column, self::inspect);
  373. }
  374. /**
  375. * Validates a value against the database schema
  376. *
  377. * @param fActiveRecord $object The instance of the class the column is part of
  378. * @param string $column The column to check
  379. * @param array &$values An associative array of all values going into the row (needs all for multi-field unique constraint checking)
  380. * @param array &$old_values The old values from the record
  381. * @return string An error message for the column specified
  382. */
  383. static private function checkAgainstSchema($object, $column, &$values, &$old_values)
  384. {
  385. $class = get_class($object);
  386. $table = fORM::tablize($class);
  387. $schema = fORMSchema::retrieve($class);
  388. $info = $schema->getColumnInfo($table, $column);
  389. // Make sure a value is provided for required columns
  390. $schema_not_null = $info['not_null'] && $info['default'] === NULL && $info['auto_increment'] === FALSE;
  391. $rule_not_null = isset(self::$required_rules[$class][$column]);
  392. if ($values[$column] === NULL && ($schema_not_null || $rule_not_null)) {
  393. return self::compose(
  394. '%sPlease enter a value',
  395. fValidationException::formatField(fORM::getColumnName($class, $column))
  396. );
  397. }
  398. $message = self::checkDataType($class, $column, $values[$column]);
  399. if ($message) { return $message; }
  400. // Make sure a valid value is chosen
  401. if (isset($info['valid_values']) && $values[$column] !== NULL && !in_array($values[$column], $info['valid_values'])) {
  402. return self::compose(
  403. '%1$sPlease choose from one of the following: %2$s',
  404. fValidationException::formatField(fORM::getColumnName($class, $column)),
  405. join(', ', $info['valid_values'])
  406. );
  407. }
  408. // Make sure the value isn't too long
  409. if ($info['type'] == 'varchar' && isset($info['max_length']) && $values[$column] !== NULL && is_string($values[$column]) && fUTF8::len($values[$column]) > $info['max_length']) {
  410. return self::compose(
  411. '%1$sPlease enter a value no longer than %2$s characters',
  412. fValidationException::formatField(fORM::getColumnName($class, $column)),
  413. $info['max_length']
  414. );
  415. }
  416. // Make sure the value is the proper length
  417. if ($info['type'] == 'char' && isset($info['max_length']) && $values[$column] !== NULL && is_string($values[$column]) && fUTF8::len($values[$column]) != $info['max_length']) {
  418. return self::compose(
  419. '%1$sPlease enter exactly %2$s characters',
  420. fValidationException::formatField(fORM::getColumnName($class, $column)),
  421. $info['max_length']
  422. );
  423. }
  424. // Make sure the value fits in the numeric range
  425. if (self::stringlike($values[$column]) && in_array($info['type'], array('integer', 'float')) && $info['min_value'] && $info['max_value'] && ($info['min_value']->gt($values[$column]) || $info['max_value']->lt($values[$column]))) {
  426. return self::compose(
  427. '%1$sPlease enter a number between %2$s and %3$s',
  428. fValidationException::formatField(fORM::getColumnName($class, $column)),
  429. $info['min_value']->__toString(),
  430. $info['max_value']->__toString()
  431. );
  432. }
  433. $message = self::checkForeignKeyConstraints($class, $column, $values);
  434. if ($message) { return $message; }
  435. }
  436. /**
  437. * Validates against a conditional rule
  438. *
  439. * @param string $class The class this rule applies to
  440. * @param array &$values An associative array of all values for the record
  441. * @param array $main_columns The columns to check for a value
  442. * @param array $conditional_values If `NULL`, any value in the main column will trigger the conditional columns, otherwise the value must match one of these
  443. * @param array $conditional_columns The columns that are to be required
  444. * @return array The error messages for the rule specified
  445. */
  446. static private function checkConditionalRule($class, &$values, $main_columns, $conditional_values, $conditional_columns)
  447. {
  448. $check_for_missing_values = FALSE;
  449. foreach ($main_columns as $main_column) {
  450. $matches_conditional_value = $conditional_values !== NULL && in_array($values[$main_column], $conditional_values);
  451. $has_some_value = $conditional_values === NULL && strlen((string) $values[$main_column]);
  452. if ($matches_conditional_value || $has_some_value) {
  453. $check_for_missing_values = TRUE;
  454. break;
  455. }
  456. }
  457. if (!$check_for_missing_values) {
  458. return;
  459. }
  460. $messages = array();
  461. foreach ($conditional_columns as $conditional_column) {
  462. if ($values[$conditional_column] !== NULL) { continue; }
  463. $messages[$conditional_column] = self::compose(
  464. '%sPlease enter a value',
  465. fValidationException::formatField(fORM::getColumnName($class, $conditional_column))
  466. );
  467. }
  468. if ($messages) {
  469. return $messages;
  470. }
  471. }
  472. /**
  473. * Validates a value against the database data type
  474. *
  475. * @param string $class The class the column is part of
  476. * @param string $column The column to check
  477. * @param mixed $value The value to check
  478. * @return string An error message for the column specified
  479. */
  480. static private function checkDataType($class, $column, $value)
  481. {
  482. $table = fORM::tablize($class);
  483. $schema = fORMSchema::retrieve($class);
  484. $column_info = $schema->getColumnInfo($table, $column);
  485. if ($value !== NULL) {
  486. switch ($column_info['type']) {
  487. case 'varchar':
  488. case 'char':
  489. case 'text':
  490. case 'blob':
  491. if (!is_string($value) && !is_numeric($value)) {
  492. return self::compose(
  493. '%sPlease enter a string',
  494. fValidationException::formatField(fORM::getColumnName($class, $column))
  495. );
  496. }
  497. break;
  498. case 'integer':
  499. if (!is_numeric($value)) {
  500. return self::compose(
  501. '%sPlease enter a whole number',
  502. fValidationException::formatField(fORM::getColumnName($class, $column))
  503. );
  504. }
  505. break;
  506. case 'float':
  507. if (!is_numeric($value)) {
  508. return self::compose(
  509. '%sPlease enter a number',
  510. fValidationException::formatField(fORM::getColumnName($class, $column))
  511. );
  512. }
  513. break;
  514. case 'timestamp':
  515. try {
  516. new fTimestamp($value);
  517. } catch (fValidationException $e) {
  518. return self::compose(
  519. '%sPlease enter a date/time',
  520. fValidationException::formatField(fORM::getColumnName($class, $column))
  521. );
  522. }
  523. break;
  524. case 'date':
  525. try {
  526. new fDate($value);
  527. } catch (fValidationException $e) {
  528. return self::compose(
  529. '%sPlease enter a date',
  530. fValidationException::formatField(fORM::getColumnName($class, $column))
  531. );
  532. }
  533. break;
  534. case 'time':
  535. try {
  536. new fTime($value);
  537. } catch (fValidationException $e) {
  538. return self::compose(
  539. '%sPlease enter a time',
  540. fValidationException::formatField(fORM::getColumnName($class, $column))
  541. );
  542. }
  543. break;
  544. }
  545. }
  546. }
  547. /**
  548. * Validates values against foreign key constraints
  549. *
  550. * @param string $class The class to check the foreign keys for
  551. * @param string $column The column to check
  552. * @param array &$values The values to check
  553. * @return string An error message for the column specified
  554. */
  555. static private function checkForeignKeyConstraints($class, $column, &$values)
  556. {
  557. if ($values[$column] === NULL) {
  558. return;
  559. }
  560. $db = fORMDatabase::retrieve($class, 'read');
  561. $schema = fORMSchema::retrieve($class);
  562. $table = fORM::tablize($class);
  563. $foreign_keys = $schema->getKeys($table, 'foreign');
  564. foreach ($foreign_keys AS $foreign_key) {
  565. if ($foreign_key['column'] == $column) {
  566. try {
  567. $params = array(
  568. "SELECT %r FROM %r WHERE " . fORMDatabase::makeCondition($schema, $table, $column, '=', $values[$column]),
  569. $foreign_key['foreign_column'],
  570. $foreign_key['foreign_table'],
  571. $foreign_key['foreign_column'],
  572. $values[$column]
  573. );
  574. $result = call_user_func_array($db->translatedQuery, $params);
  575. $result->tossIfNoRows();
  576. } catch (fNoRowsException $e) {
  577. return self::compose(
  578. '%sThe value specified is invalid',
  579. fValidationException::formatField(fORM::getColumnName($class, $column))
  580. );
  581. }
  582. }
  583. }
  584. }
  585. /**
  586. * Validates against a one-or-more rule
  587. *
  588. * @param fSchema $schema The schema object for the table
  589. * @param string $class The class the columns are part of
  590. * @param array &$values An associative array of all values for the record
  591. * @param array $columns The columns to check
  592. * @return string An error message for the rule
  593. */
  594. static private function checkOneOrMoreRule($schema, $class, &$values, $columns)
  595. {
  596. settype($columns, 'array');
  597. $found_value = FALSE;
  598. foreach ($columns as $column) {
  599. if (self::hasValue($schema, $class, $values, $column)) {
  600. $found_value = TRUE;
  601. }
  602. }
  603. if (!$found_value) {
  604. $column_names = array();
  605. foreach ($columns as $column) {
  606. $column_names[] = fORM::getColumnName($class, $column);
  607. }
  608. return self::compose(
  609. '%sPlease enter a value for at least one',
  610. fValidationException::formatField(join(', ', $column_names))
  611. );
  612. }
  613. }
  614. /**
  615. * Validates against an only-one rule
  616. *
  617. * @param fSchema $schema The schema object for the table
  618. * @param string $class The class the columns are part of
  619. * @param array &$values An associative array of all values for the record
  620. * @param array $columns The columns to check
  621. * @return string An error message for the rule
  622. */
  623. static private function checkOnlyOneRule($schema, $class, &$values, $columns)
  624. {
  625. settype($columns, 'array');
  626. $column_names = array();
  627. foreach ($columns as $column) {
  628. $column_names[] = fORM::getColumnName($class, $column);
  629. }
  630. $found_value = FALSE;
  631. foreach ($columns as $column) {
  632. if (self::hasValue($schema, $class, $values, $column)) {
  633. if ($found_value) {
  634. return self::compose(
  635. '%sPlease enter a value for only one',
  636. fValidationException::formatField(join(', ', $column_names))
  637. );
  638. }
  639. $found_value = TRUE;
  640. }
  641. }
  642. if (!$found_value) {
  643. return self::compose(
  644. '%sPlease enter a value for one',
  645. fValidationException::formatField(join(', ', $column_names))
  646. );
  647. }
  648. }
  649. /**
  650. * Makes sure a record with the same primary keys is not already in the database
  651. *
  652. * @param fActiveRecord $object The instance of the class to check
  653. * @param array &$values An associative array of all values going into the row (needs all for multi-field unique constraint checking)
  654. * @param array &$old_values The old values for the record
  655. * @return array A single element associative array with the key being the primary keys joined by ,s and the value being the error message
  656. */
  657. static private function checkPrimaryKeys($object, &$values, &$old_values)
  658. {
  659. $class = get_class($object);
  660. $table = fORM::tablize($class);
  661. $db = fORMDatabase::retrieve($class, 'read');
  662. $schema = fORMSchema::retrieve($class);
  663. $pk_columns = $schema->getKeys($table, 'primary');
  664. $columns = array();
  665. $found_value = FALSE;
  666. foreach ($pk_columns as $pk_column) {
  667. $columns[] = fORM::getColumnName($class, $pk_column);
  668. if ($values[$pk_column]) {
  669. $found_value = TRUE;
  670. }
  671. }
  672. if (!$found_value) {
  673. return;
  674. }
  675. $different = FALSE;
  676. foreach ($pk_columns as $pk_column) {
  677. if (!fActiveRecord::hasOld($old_values, $pk_column)) {
  678. continue;
  679. }
  680. $old_value = fActiveRecord::retrieveOld($old_values, $pk_column);
  681. $value = $values[$pk_column];
  682. if (self::isCaseInsensitive($class, $pk_column) && self::stringlike($value) && self::stringlike($old_value)) {
  683. if (fUTF8::lower($value) != fUTF8::lower($old_value)) {
  684. $different = TRUE;
  685. }
  686. } elseif ($old_value != $value) {
  687. $different = TRUE;
  688. }
  689. }
  690. if (!$different) {
  691. return;
  692. }
  693. try {
  694. $params = array(
  695. "SELECT %r FROM %r WHERE ",
  696. $pk_columns,
  697. $table
  698. );
  699. $column_info = $schema->getColumnInfo($table);
  700. $conditions = array();
  701. foreach ($pk_columns as $pk_column) {
  702. $value = $values[$pk_column];
  703. // This makes sure the query performs the way an insert will
  704. if ($value === NULL && $column_info[$pk_column]['not_null'] && $column_info[$pk_column]['default'] !== NULL) {
  705. $value = $column_info[$pk_column]['default'];
  706. }
  707. if (self::isCaseInsensitive($class, $pk_column) && self::stringlike($value)) {
  708. $condition = fORMDatabase::makeCondition($schema, $table, $pk_column, '=', $value);
  709. $conditions[] = str_replace('%r', 'LOWER(%r)', $condition);
  710. $params[] = $pk_column;
  711. $params[] = fUTF8::lower($value);
  712. } else {
  713. $conditions[] = fORMDatabase::makeCondition($schema, $table, $pk_column, '=', $value);
  714. $params[] = $pk_column;
  715. $params[] = $value;
  716. }
  717. }
  718. $params[0] .= join(' AND ', $conditions);
  719. $result = call_user_func_array($db->translatedQuery, $params);
  720. $result->tossIfNoRows();
  721. return array(join(',', $pk_columns) => self::compose(
  722. 'Another %1$s with the same %2$s already exists',
  723. fORM::getRecordName($class),
  724. fGrammar::joinArray($columns, 'and')
  725. ));
  726. } catch (fNoRowsException $e) { }
  727. }
  728. /**
  729. * Validates against a regex rule
  730. *
  731. * @param string $class The class the column is part of
  732. * @param array &$values An associative array of all values for the record
  733. * @param string $column The column to check
  734. * @param string $regex The PCRE regular expression
  735. * @param string $message The message to use if the value does not match the regular expression
  736. * @return string An error message for the rule
  737. */
  738. static private function checkRegexRule($class, &$values, $column, $regex, $message)
  739. {
  740. if ($values[$column] === NULL) {
  741. return;
  742. }
  743. if (preg_match($regex, $values[$column])) {
  744. return;
  745. }
  746. return self::compose(
  747. '%s' . str_replace('%', '%%', $message),
  748. fValidationException::formatField(fORM::getColumnName($class, $column))
  749. );
  750. }
  751. /**
  752. * Validates against a *-to-many one or more rule
  753. *
  754. * @param fActiveRecord $object The object being checked
  755. * @param array &$values The values for the object
  756. * @param array &$related_records The related records for the object
  757. * @param string $related_class The name of the related class
  758. * @param string $route The name of the route from the class to the related class
  759. * @return string An error message for the rule
  760. */
  761. static private function checkRelatedOneOrMoreRule($object, &$values, &$related_records, $related_class, $route)
  762. {
  763. $related_table = fORM::tablize($related_class);
  764. $class = get_class($object);
  765. $exists = $object->exists();
  766. $records_are_set = isset($related_records[$related_table][$route]);
  767. $has_records = $records_are_set && $related_records[$related_table][$route]['count'];
  768. if ($exists && (!$records_are_set || $has_records)) {
  769. return;
  770. }
  771. if (!$exists && $has_records) {
  772. return;
  773. }
  774. return self::compose(
  775. '%sPlease select at least one',
  776. fValidationException::formatField(fGrammar::pluralize(fORMRelated::getRelatedRecordName($class, $related_class, $route)))
  777. );
  778. }
  779. /**
  780. * Validates values against unique constraints
  781. *
  782. * @param fActiveRecord $object The instance of the class to check
  783. * @param array &$values The values to check
  784. * @param array &$old_values The old values for the record
  785. * @return array An aray of error messages for the unique constraints
  786. */
  787. static private function checkUniqueConstraints($object, &$values, &$old_values)
  788. {
  789. $class = get_class($object);
  790. $table = fORM::tablize($class);
  791. $db = fORMDatabase::retrieve($class, 'read');
  792. $schema = fORMSchema::retrieve($class);
  793. $key_info = $schema->getKeys($table);
  794. $pk_columns = $key_info['primary'];
  795. $unique_keys = $key_info['unique'];
  796. $messages = array();
  797. foreach ($unique_keys AS $unique_columns) {
  798. settype($unique_columns, 'array');
  799. // NULL values are unique
  800. $found_not_null = FALSE;
  801. foreach ($unique_columns as $unique_column) {
  802. if ($values[$unique_column] !== NULL) {
  803. $found_not_null = TRUE;
  804. }
  805. }
  806. if (!$found_not_null) {
  807. continue;
  808. }
  809. $params = array(
  810. "SELECT %r FROM %r WHERE ",
  811. $key_info['primary'],
  812. $table
  813. );
  814. $column_info = $schema->getColumnInfo($table);
  815. $conditions = array();
  816. foreach ($unique_columns as $unique_column) {
  817. $value = $values[$unique_column];
  818. // This makes sure the query performs the way an insert will
  819. if ($value === NULL && $column_info[$unique_column]['not_null'] && $column_info[$unique_column]['default'] !== NULL) {
  820. $value = $column_info[$unique_column]['default'];
  821. }
  822. if (self::isCaseInsensitive($class, $unique_column) && self::stringlike($value)) {
  823. $condition = fORMDatabase::makeCondition($schema, $table, $unique_column, '=', $value);
  824. $conditions[] = str_replace('%r', 'LOWER(%r)', $condition);
  825. $params[] = $table . '.' . $unique_column;
  826. $params[] = fUTF8::lower($value);
  827. } else {
  828. $conditions[] = fORMDatabase::makeCondition($schema, $table, $unique_column, '=', $value);
  829. $params[] = $table . '.' . $unique_column;
  830. $params[] = $value;
  831. }
  832. }
  833. $params[0] .= join(' AND ', $conditions);
  834. if ($object->exists()) {
  835. foreach ($pk_columns as $pk_column) {
  836. $value = fActiveRecord::retrieveOld($old_values, $pk_column, $values[$pk_column]);
  837. $params[0] .= ' AND ' . fORMDatabase::makeCondition($schema, $table, $pk_column, '<>', $value);
  838. $params[] = $table . '.' . $pk_column;
  839. $params[] = $value;
  840. }
  841. }
  842. try {
  843. $result = call_user_func_array($db->translatedQuery, $params);
  844. $result->tossIfNoRows();
  845. // If an exception was not throw, we have existing values
  846. $column_names = array();
  847. foreach ($unique_columns as $unique_column) {
  848. $column_names[] = fORM::getColumnName($class, $unique_column);
  849. }
  850. if (sizeof($column_names) == 1) {
  851. $messages[join('', $unique_columns)] = self::compose(
  852. '%sThe value specified must be unique, however it already exists',
  853. fValidationException::formatField(join('', $column_names))
  854. );
  855. } else {
  856. $messages[join(',', $unique_columns)] = self::compose(
  857. '%sThe values specified must be a unique combination, however the specified combination already exists',
  858. fValidationException::formatField(join(', ', $column_names))
  859. );
  860. }
  861. } catch (fNoRowsException $e) { }
  862. }
  863. return $messages;
  864. }
  865. /**
  866. * Validates against a valid values rule
  867. *
  868. * @param string $class The class this rule applies to
  869. * @param array &$values An associative array of all values for the record
  870. * @param string $column The column the rule applies to
  871. * @param array $valid_values An array of valid values to check the column against
  872. * @return string The error message for the rule specified
  873. */
  874. static private function checkValidValuesRule($class, &$values, $column, $valid_values)
  875. {
  876. if ($values[$column] === NULL) {
  877. return;
  878. }
  879. if (!in_array($values[$column], $valid_values)) {
  880. return self::compose(
  881. '%1$sPlease choose from one of the following: %2$s',
  882. fValidationException::formatField(fORM::getColumnName($class, $column)),
  883. join(', ', $valid_values)
  884. );
  885. }
  886. }
  887. /**
  888. * Composes text using fText if loaded
  889. *
  890. * @param string $message The message to compose
  891. * @param mixed $component A string or number to insert into the message
  892. * @param mixed ...
  893. * @return string The composed and possible translated message
  894. */
  895. static private function compose($message)
  896. {
  897. $args = array_slice(func_get_args(), 1);
  898. if (class_exists('fText', FALSE)) {
  899. return call_user_func_array(
  900. array('fText', 'compose'),
  901. array($message, $args)
  902. );
  903. } else {
  904. return vsprintf($message, $args);
  905. }
  906. }
  907. /**
  908. * Makes sure each rule array is set to at least an empty array
  909. *
  910. * @internal
  911. *
  912. * @param string $class The class to initilize the arrays for
  913. * @return void
  914. */
  915. static private function initializeRuleArrays($class)
  916. {
  917. self::$conditional_rules[$class] = (isset(self::$conditional_rules[$class])) ? self::$conditional_rules[$class] : array();
  918. self::$one_or_more_rules[$class] = (isset(self::$one_or_more_rules[$class])) ? self::$one_or_more_rules[$class] : array();
  919. self::$only_one_rules[$class] = (isset(self::$only_one_rules[$class])) ? self::$only_one_rules[$class] : array();
  920. self::$regex_rules[$class] = (isset(self::$regex_rules[$class])) ? self::$regex_rules[$class] : array();
  921. self::$related_one_or_more_rules[$class] = (isset(self::$related_one_or_more_rules[$class])) ? self::$related_one_or_more_rules[$class] : array();
  922. self::$valid_values_rules[$class] = (isset(self::$valid_values_rules[$class])) ? self::$valid_values_rules[$class] : array();
  923. }
  924. /**
  925. * Adds metadata about features added by this class
  926. *
  927. * @internal
  928. *
  929. * @param string $class The class being inspected
  930. * @param string $column The column being inspected
  931. * @param array &$metadata The array of metadata about a column
  932. * @return void
  933. */
  934. static public function inspect($class, $column, &$metadata)
  935. {
  936. if (!empty(self::$valid_values_rules[$class][$column])) {
  937. $metadata['valid_values'] = self::$valid_values_rules[$class][$column];
  938. }
  939. }
  940. /**
  941. * Checks to see if a columns has a value, but based on the schema and if the column allows NULL
  942. *
  943. * If the columns allows NULL values, than anything other than NULL
  944. * will be returned as TRUE. If the column does not allow NULL and
  945. * the value is anything other than the "empty" value for that data type,
  946. * then TRUE will be returned.
  947. *
  948. * The values that are considered "empty" for each data type are as follows.
  949. * Please note that there is no "empty" value for dates, times or
  950. * timestamps.
  951. *
  952. * - Blob: ''
  953. * - Boolean: FALSE
  954. * - Float: 0.0
  955. * - Integer: 0
  956. * - String: ''
  957. *
  958. * @internal
  959. *
  960. * @param fSchema $schema The schema object for the table
  961. * @param string $class The class the column is part of
  962. * @param array &$values An associative array of all values for the record
  963. * @param array $columns The column to check
  964. * @return string An error message for the rule
  965. */
  966. static public function hasValue($schema, $class, &$values, $column)
  967. {
  968. $value = $values[$column];
  969. if ($value === NULL) {
  970. return FALSE;
  971. }
  972. $table = fORM::tablize($class);
  973. $data_type = $schema->getColumnInfo($table, $column, 'type');
  974. $allows_null = !$schema->getColumnInfo($table, $column, 'not_null');
  975. if ($allows_null) {
  976. return TRUE;
  977. }
  978. switch ($data_type) {
  979. case 'blob':
  980. case 'char':
  981. case 'text':
  982. case 'varchar':
  983. if ($value === '') {
  984. return FALSE;
  985. }
  986. break;
  987. case 'boolean':
  988. if ($value === FALSE) {
  989. return FALSE;
  990. }
  991. break;
  992. case 'integer':
  993. if ($value === 0 || $value === '0') {
  994. return FALSE;
  995. }
  996. break;
  997. case 'float':
  998. if (preg_match('#^0(\.0*)?$|^\.0+$#D', $value)) {
  999. return FALSE;
  1000. }
  1001. break;
  1002. }
  1003. return TRUE;
  1004. }
  1005. /**
  1006. * Checks to see if a column has been set as case insensitive
  1007. *
  1008. * @internal
  1009. *
  1010. * @param string $class The class to check
  1011. * @param string $column The column to check
  1012. * @return boolean If the column is set to be case insensitive
  1013. */
  1014. static private function isCaseInsensitive($class, $column)
  1015. {
  1016. return isset(self::$case_insensitive_columns[$class][$column]);
  1017. }
  1018. /**
  1019. * Returns FALSE if the string is empty - used for array filtering
  1020. *
  1021. * @param string $string The string to check
  1022. * @return boolean If the string is not blank
  1023. */
  1024. static private function isNonBlankString($string)
  1025. {
  1026. return ((string) $string) !== '';
  1027. }
  1028. /**
  1029. * Removes a regex replacement
  1030. *
  1031. * @internal
  1032. *
  1033. * @param mixed $class The class name or instance of the class the columns exists in
  1034. * @param string $search The string to search for
  1035. * @param string $replace The string to replace with
  1036. * @return void
  1037. */
  1038. static public function removeRegexReplacement($class, $search, $replace)
  1039. {
  1040. $class = fORM::getClass($class);
  1041. if (!isset(self::$regex_replacements[$class])) {
  1042. self::$regex_replacements[$class] = array(
  1043. 'search' => array(),
  1044. 'replace' => array()
  1045. );
  1046. }
  1047. $replacements = count(self::$regex_replacements[$class]['search']);
  1048. for ($i = 0; $i < $replacements; $i++) {
  1049. $match_search = self::$regex_replacements[$class]['search'][$i] == $search;
  1050. $match_replace = self::$regex_replacements[$class]['replace'][$i] == $replace;
  1051. if ($match_search && $match_replace) {
  1052. unset(self::$regex_replacements[$class]['search'][$i]);
  1053. unset(self::$regex_replacements[$class]['replace'][$i]);
  1054. }
  1055. }
  1056. // Remove the any gaps in the arrays
  1057. self::$regex_replacements[$class]['search'] = array_merge(self::$regex_replacements[$class]['search']);
  1058. self::$regex_replacements[$class]['replace'] = array_merge(self::$regex_replacements[$class]['replace']);
  1059. }
  1060. /**
  1061. * Removes a string replacement
  1062. *
  1063. * @internal
  1064. *
  1065. * @param mixed $class The class name or instance of the class the columns exists in
  1066. * @param string $search The string to search for
  1067. * @param string $replace The string to replace with
  1068. * @return void
  1069. */
  1070. static public function removeStringReplacement($class, $search, $replace)
  1071. {
  1072. $class = fORM::getClass($class);
  1073. if (!isset(self::$string_replacements[$class])) {
  1074. self::$string_replacements[$class] = array(
  1075. 'search' => array(),
  1076. 'replace' => array()
  1077. );
  1078. }
  1079. $replacements = count(self::$string_replacements[$class]['search']);
  1080. for ($i = 0; $i < $replacements; $i++) {
  1081. $match_search = self::$string_replacements[$class]['search'][$i] == $search;
  1082. $match_replace = self::$string_replacements[$class]['replace'][$i] == $replace;
  1083. if ($match_search && $match_replace) {
  1084. unset(self::$string_replacements[$class]['search'][$i]);
  1085. unset(self::$string_replacements[$class]['replace'][$i]);
  1086. }
  1087. }
  1088. // Remove the any gaps in the arrays
  1089. self::$string_replacements[$class]['search'] = array_merge(self::$string_replacements[$class]['search']);
  1090. self::$string_replacements[$class]['replace'] = array_merge(self::$string_replacements[$class]['replace']);
  1091. }
  1092. /**
  1093. * Reorders list items in an html string based on their contents
  1094. *
  1095. * @internal
  1096. *
  1097. * @param string $class The class to reorder messages for
  1098. * @param array $messages An array of the messages
  1099. * @return array The reordered messages
  1100. */
  1101. static public function reorderMessages($class, $messages)
  1102. {
  1103. if (!isset(self::$message_orders[$class])) {
  1104. return $messages;
  1105. }
  1106. $matches = self::$message_orders[$class];
  1107. $ordered_items = array_fill(0, sizeof($matches), array());
  1108. $other_items = array();
  1109. foreach ($messages as $key => $message) {
  1110. foreach ($matches as $num => $match_string) {
  1111. $string = is_array($message) ? $message['name'] : $message;
  1112. if (fUTF8::ipos($string, $match_string) !== FALSE) {
  1113. $ordered_items[$num][$key] = $message;
  1114. continue 2;
  1115. }
  1116. }
  1117. $other_items[$key] = $message;
  1118. }
  1119. $final_list = array();
  1120. foreach ($ordered_items as $ordered_item) {
  1121. $final_list = array_merge($final_list, $ordered_item);
  1122. }
  1123. return array_merge($final_list, $other_items);
  1124. }
  1125. /**
  1126. * Takes a list of messages and performs string and regex replacements on them
  1127. *
  1128. * @internal
  1129. *
  1130. * @param string $class The class to reorder messages for
  1131. * @param array $messages The array of messages
  1132. * @return array The new array of messages
  1133. */
  1134. static public function replaceMessages($class, $messages)
  1135. {
  1136. if (isset(self::$string_replacements[$class])) {
  1137. foreach ($messages as $key => $message) {
  1138. if (is_array($message)) {
  1139. continue;
  1140. }
  1141. $messages[$key] = str_replace(
  1142. self::$string_replacements[$class]['search'],
  1143. self::$string_replacements[$class]['replace'],
  1144. $message
  1145. );
  1146. }
  1147. }
  1148. if (isset(self::$regex_replacements[$class])) {
  1149. foreach ($messages as $key => $message) {
  1150. if (is_array($message)) {
  1151. continue;
  1152. }
  1153. $messages[$key] = preg_replace(
  1154. self::$regex_replacements[$class]['search'],
  1155. self::$regex_replacements[$class]['replace'],
  1156. $message
  1157. );
  1158. }
  1159. }
  1160. return array_filter($messages, array('fORMValidation', 'isNonBlankString'));
  1161. }
  1162. /**
  1163. * Resets the configuration of the class
  1164. *
  1165. * @internal
  1166. *
  1167. * @return void
  1168. */
  1169. static public function reset()
  1170. {
  1171. self::$case_insensitive_columns = array();
  1172. self::$conditional_rules = array();
  1173. self::$message_orders = array();
  1174. self::$one_or_more_rules = array();
  1175. self::$only_one_rules = array();
  1176. self::$regex_replacements = array();
  1177. self::$related_one_or_more_rules = array();
  1178. self::$regex_rules = array();
  1179. self::$required_rules = array();
  1180. self::$string_replacements = array();
  1181. self::$valid_values_rules = array();
  1182. }
  1183. /**
  1184. * Sets a column to be compared in a case-insensitive manner when checking `UNIQUE` and `PRIMARY KEY` constraints
  1185. *
  1186. * @param mixed $class The class name or instance of the class the column is located in
  1187. * @param string $column The column to set as case-insensitive
  1188. * @return void
  1189. */
  1190. static public function setColumnCaseInsensitive($class, $column)
  1191. {
  1192. $class = fORM::getClass($class);
  1193. $table = fORM::tablize($class);
  1194. $schema = fORMSchema::retrieve($class);
  1195. $type = $schema->getColumnInfo($table, $column, 'type');
  1196. $valid_types = array('varchar', 'char', 'text');
  1197. if (!in_array($type, $valid_types)) {
  1198. throw new fProgrammerException(
  1199. 'The column specified, %1$s, is of the data type %2$s. Must be one of %3$s to be treated as case insensitive.',
  1200. $column,
  1201. $type,
  1202. join(', ', $valid_types)
  1203. );
  1204. }
  1205. if (!isset(self::$case_insensitive_columns[$class])) {
  1206. self::$case_insensitive_columns[$class] = array();
  1207. }
  1208. self::$case_insensitive_columns[$class][$column] = TRUE;
  1209. }
  1210. /**
  1211. * Allows setting the order that the list items in a message will be displayed
  1212. *
  1213. * All string comparisons during the reordering process are done in a
  1214. * case-insensitive manner.
  1215. *
  1216. * @param mixed $class The class name or an instance of the class to set the message order for
  1217. * @param array $matches This should be an ordered array of strings. If a line contains the string it will be displayed in the relative order it occurs in this array.
  1218. * @return void
  1219. */
  1220. static public function setMessageOrder($class, $matches)
  1221. {
  1222. $class = fORM::getClass($class);
  1223. // Handle the alternate form allowed with fValidation::setMessageOrder()
  1224. $args = func_get_args();
  1225. array_shift($args);
  1226. if (count($args) != 1) {
  1227. $matches = $args;
  1228. }
  1229. uasort($matches, array('self', 'sortMessageMatches'));
  1230. self::$message_orders[$class] = $matches;
  1231. }
  1232. /**
  1233. * Compares the message matching strings by longest first so that the longest matches are made first
  1234. *
  1235. * @param string $a The first string to compare
  1236. * @param string $b The second string to compare
  1237. * @return integer `-1` if `$a` is longer than `$b`, `0` if they are equal length, `1` if `$a` is shorter than `$b`
  1238. */
  1239. static private function sortMessageMatches($a, $b)
  1240. {
  1241. if (strlen($a) == strlen($b)) {
  1242. return 0;
  1243. }
  1244. if (strlen($a) > strlen($b)) {
  1245. return -1;
  1246. }
  1247. return 1;
  1248. }
  1249. /**
  1250. * Returns `TRUE` for non-empty strings, numbers, objects, empty numbers and string-like numbers (such as `0`, `0.0`, `'0'`)
  1251. *
  1252. * @param mixed $value The value to check
  1253. * @return boolean If the value is string-like
  1254. */
  1255. static private function stringlike($value)
  1256. {
  1257. if ((!is_string($value) && !is_object($value) && !is_numeric($value)) || !strlen(trim($value))) {
  1258. return FALSE;
  1259. }
  1260. return TRUE;
  1261. }
  1262. /**
  1263. * Validates values for an fActiveRecord object against the database schema and any additional rules that have been added
  1264. *
  1265. * @internal
  1266. *
  1267. * @param fActiveRecord $object The instance of the class to validate
  1268. * @param array $values The values to validate
  1269. * @param array $old_values The old values for the record
  1270. * @return array An array of messages
  1271. */
  1272. static public function validate($object,

Large files files are truncated, but you can click here to view the full file