/system/src/Grav/Common/Data/Validation.php

https://gitlab.com/x33n/grav · PHP · 597 lines · 315 code · 78 blank · 204 comment · 76 complexity · edaf26885c981f7687f175c6c93f1009 MD5 · raw file

  1. <?php
  2. namespace Grav\Common\Data;
  3. /**
  4. * Data validation.
  5. *
  6. * @author RocketTheme
  7. * @license MIT
  8. */
  9. class Validation
  10. {
  11. /**
  12. * Validate value against a blueprint field definition.
  13. *
  14. * @param mixed $value
  15. * @param array $field
  16. * @throws \RuntimeException
  17. */
  18. public static function validate($value, array $field)
  19. {
  20. $validate = isset($field['validate']) ? (array) $field['validate'] : array();
  21. // If value isn't required, we will stop validation if empty value is given.
  22. if (empty($validate['required']) && ($value === null || $value === '')) {
  23. return;
  24. }
  25. // Validate type with fallback type text.
  26. $type = (string) isset($field['validate']['type']) ? $field['validate']['type'] : $field['type'];
  27. $method = 'type'.strtr($type, '-', '_');
  28. if (method_exists(__CLASS__, $method)) {
  29. $success = self::$method($value, $validate, $field);
  30. } else {
  31. $success = self::typeText($value, $validate, $field);
  32. }
  33. if (!$success) {
  34. $name = $field['label'] ? $field['label'] : $field['name'];
  35. throw new \RuntimeException("invalid input in {$name}");
  36. }
  37. // Check individual rules
  38. foreach ($validate as $rule => $params) {
  39. $method = 'validate'.strtr($rule, '-', '_');
  40. if (method_exists(__CLASS__, $method)) {
  41. $success = self::$method($value, $params);
  42. if (!$success) {
  43. throw new \RuntimeException('Failed');
  44. }
  45. }
  46. }
  47. }
  48. /**
  49. * Filter value against a blueprint field definition.
  50. *
  51. * @param mixed $value
  52. * @param array $field
  53. * @return mixed Filtered value.
  54. */
  55. public static function filter($value, array $field)
  56. {
  57. $validate = isset($field['validate']) ? (array) $field['validate'] : array();
  58. // If value isn't required, we will return null if empty value is given.
  59. if (empty($validate['required']) && ($value === null || $value === '')) {
  60. return null;
  61. }
  62. // Validate type with fallback type text.
  63. $type = (string) isset($field['validate']['type']) ? $field['validate']['type'] : $field['type'];
  64. $method = 'filter'.strtr($type, '-', '_');
  65. if (method_exists(__CLASS__, $method)) {
  66. $value = self::$method($value, $validate, $field);
  67. } else {
  68. $value = self::filterText($value, $validate, $field);
  69. }
  70. return $value;
  71. }
  72. /**
  73. * HTML5 input: text
  74. *
  75. * @param mixed $value Value to be validated.
  76. * @param array $params Validation parameters.
  77. * @param array $field Blueprint for the field.
  78. * @return bool True if validation succeeded.
  79. */
  80. public static function typeText($value, array $params, array $field)
  81. {
  82. if (!is_string($value)) {
  83. return false;
  84. }
  85. if (isset($params['min']) && strlen($value) < $params['min']) {
  86. return false;
  87. }
  88. if (isset($params['max']) && strlen($value) > $params['max']) {
  89. return false;
  90. }
  91. $min = isset($params['min']) ? $params['min'] : 0;
  92. if (isset($params['step']) && (strlen($value) - $min) % $params['step'] == 0) {
  93. return false;
  94. }
  95. if ((!isset($params['multiline']) || !$params['multiline']) && preg_match('/\R/um', $value)) {
  96. return false;
  97. }
  98. return true;
  99. }
  100. protected static function filterText($value, array $params, array $field)
  101. {
  102. return (string) $value;
  103. }
  104. protected static function filterCommaList($value, array $params, array $field)
  105. {
  106. return is_array($value) ? $value : preg_split('/\s*,\s*/', $value, -1, PREG_SPLIT_NO_EMPTY);
  107. }
  108. protected static function typeCommaList($value, array $params, array $field)
  109. {
  110. return is_array($value) ? true : self::typeText($value, $params, $field);
  111. }
  112. /**
  113. * HTML5 input: textarea
  114. *
  115. * @param mixed $value Value to be validated.
  116. * @param array $params Validation parameters.
  117. * @param array $field Blueprint for the field.
  118. * @return bool True if validation succeeded.
  119. */
  120. public static function typeTextarea($value, array $params, array $field)
  121. {
  122. if (!isset($params['multiline'])) {
  123. $params['multiline'] = true;
  124. }
  125. return self::typeText($value, $params, $field);
  126. }
  127. /**
  128. * HTML5 input: password
  129. *
  130. * @param mixed $value Value to be validated.
  131. * @param array $params Validation parameters.
  132. * @param array $field Blueprint for the field.
  133. * @return bool True if validation succeeded.
  134. */
  135. public static function typePassword($value, array $params, array $field)
  136. {
  137. return self::typeText($value, $params, $field);
  138. }
  139. /**
  140. * HTML5 input: hidden
  141. *
  142. * @param mixed $value Value to be validated.
  143. * @param array $params Validation parameters.
  144. * @param array $field Blueprint for the field.
  145. * @return bool True if validation succeeded.
  146. */
  147. public static function typeHidden($value, array $params, array $field)
  148. {
  149. return self::typeText($value, $params, $field);
  150. }
  151. /**
  152. * Custom input: checkbox list
  153. *
  154. * @param mixed $value Value to be validated.
  155. * @param array $params Validation parameters.
  156. * @param array $field Blueprint for the field.
  157. * @return bool True if validation succeeded.
  158. */
  159. public static function typeCheckboxes($value, array $params, array $field)
  160. {
  161. return self::typeArray((array) $value, $params, $field);
  162. }
  163. protected static function filterCheckboxes($value, array $params, array $field)
  164. {
  165. return self::filterArray($value, $params, $field);
  166. }
  167. /**
  168. * HTML5 input: checkbox
  169. *
  170. * @param mixed $value Value to be validated.
  171. * @param array $params Validation parameters.
  172. * @param array $field Blueprint for the field.
  173. * @return bool True if validation succeeded.
  174. */
  175. public static function typeCheckbox($value, array $params, array $field)
  176. {
  177. $value = (string) $value;
  178. if (!isset($field['value'])) {
  179. $field['value'] = 1;
  180. }
  181. if ($value && $value != $field['value']) {
  182. return false;
  183. }
  184. return true;
  185. }
  186. /**
  187. * HTML5 input: radio
  188. *
  189. * @param mixed $value Value to be validated.
  190. * @param array $params Validation parameters.
  191. * @param array $field Blueprint for the field.
  192. * @return bool True if validation succeeded.
  193. */
  194. public static function typeRadio($value, array $params, array $field)
  195. {
  196. return self::typeArray((array) $value, $params, $field);
  197. }
  198. /**
  199. * Custom input: toggle
  200. *
  201. * @param mixed $value Value to be validated.
  202. * @param array $params Validation parameters.
  203. * @param array $field Blueprint for the field.
  204. * @return bool True if validation succeeded.
  205. */
  206. public static function typeToggle($value, array $params, array $field)
  207. {
  208. return self::typeArray((array) $value, $params, $field);
  209. }
  210. /**
  211. * HTML5 input: select
  212. *
  213. * @param mixed $value Value to be validated.
  214. * @param array $params Validation parameters.
  215. * @param array $field Blueprint for the field.
  216. * @return bool True if validation succeeded.
  217. */
  218. public static function typeSelect($value, array $params, array $field)
  219. {
  220. return self::typeArray((array) $value, $params, $field);
  221. }
  222. /**
  223. * HTML5 input: number
  224. *
  225. * @param mixed $value Value to be validated.
  226. * @param array $params Validation parameters.
  227. * @param array $field Blueprint for the field.
  228. * @return bool True if validation succeeded.
  229. */
  230. public static function typeNumber($value, array $params, array $field)
  231. {
  232. if (!is_numeric($value)) {
  233. return false;
  234. }
  235. if (isset($params['min']) && $value < $params['min']) {
  236. return false;
  237. }
  238. if (isset($params['max']) && $value > $params['max']) {
  239. return false;
  240. }
  241. $min = isset($params['min']) ? $params['min'] : 0;
  242. if (isset($params['step']) && fmod($value - $min, $params['step']) == 0) {
  243. return false;
  244. }
  245. return true;
  246. }
  247. protected static function filterNumber($value, array $params, array $field)
  248. {
  249. return (int) $value;
  250. }
  251. /**
  252. * HTML5 input: range
  253. *
  254. * @param mixed $value Value to be validated.
  255. * @param array $params Validation parameters.
  256. * @param array $field Blueprint for the field.
  257. * @return bool True if validation succeeded.
  258. */
  259. public static function typeRange($value, array $params, array $field)
  260. {
  261. return self::typeNumber($value, $params, $field);
  262. }
  263. protected static function filterRange($value, array $params, array $field)
  264. {
  265. return self::filterNumber($value, $params, $field);
  266. }
  267. /**
  268. * HTML5 input: color
  269. *
  270. * @param mixed $value Value to be validated.
  271. * @param array $params Validation parameters.
  272. * @param array $field Blueprint for the field.
  273. * @return bool True if validation succeeded.
  274. */
  275. public static function typeColor($value, array $params, array $field)
  276. {
  277. return preg_match('/^\#[0-9a-fA-F]{3}[0-9a-fA-F]{3}?$/u', $value);
  278. }
  279. /**
  280. * HTML5 input: email
  281. *
  282. * @param mixed $value Value to be validated.
  283. * @param array $params Validation parameters.
  284. * @param array $field Blueprint for the field.
  285. * @return bool True if validation succeeded.
  286. */
  287. public static function typeEmail($value, array $params, array $field)
  288. {
  289. return self::typeText($value, $params, $field) && filter_var($value, FILTER_VALIDATE_EMAIL);
  290. }
  291. /**
  292. * HTML5 input: url
  293. *
  294. * @param mixed $value Value to be validated.
  295. * @param array $params Validation parameters.
  296. * @param array $field Blueprint for the field.
  297. * @return bool True if validation succeeded.
  298. */
  299. public static function typeUrl($value, array $params, array $field)
  300. {
  301. return self::typeText($value, $params, $field) && filter_var($value, FILTER_VALIDATE_URL);
  302. }
  303. /**
  304. * HTML5 input: datetime
  305. *
  306. * @param mixed $value Value to be validated.
  307. * @param array $params Validation parameters.
  308. * @param array $field Blueprint for the field.
  309. * @return bool True if validation succeeded.
  310. */
  311. public static function typeDatetime($value, array $params, array $field)
  312. {
  313. // TODO: add min, max and range.
  314. if ($value instanceof \DateTime) {
  315. return true;
  316. } elseif (!is_string($value)) {
  317. return false;
  318. } elseif (!isset($params['format'])) {
  319. return false !== strtotime($value);
  320. }
  321. $dateFromFormat = \DateTime::createFromFormat($params['format'], $value);
  322. return $dateFromFormat && $value === date($params['format'], $dateFromFormat->getTimestamp());
  323. }
  324. /**
  325. * HTML5 input: datetime-local
  326. *
  327. * @param mixed $value Value to be validated.
  328. * @param array $params Validation parameters.
  329. * @param array $field Blueprint for the field.
  330. * @return bool True if validation succeeded.
  331. */
  332. public static function typeDatetimeLocal($value, array $params, array $field)
  333. {
  334. return self::typeDatetime($value, $params, $field);
  335. }
  336. /**
  337. * HTML5 input: date
  338. *
  339. * @param mixed $value Value to be validated.
  340. * @param array $params Validation parameters.
  341. * @param array $field Blueprint for the field.
  342. * @return bool True if validation succeeded.
  343. */
  344. public static function typeDate($value, array $params, array $field)
  345. {
  346. $params = array($params);
  347. if (!isset($params['format'])) {
  348. $params['format'] = 'Y-m-d';
  349. }
  350. return self::typeDatetime($value, $params, $field);
  351. }
  352. /**
  353. * HTML5 input: time
  354. *
  355. * @param mixed $value Value to be validated.
  356. * @param array $params Validation parameters.
  357. * @param array $field Blueprint for the field.
  358. * @return bool True if validation succeeded.
  359. */
  360. public static function typeTime($value, array $params, array $field)
  361. {
  362. $params = array($params);
  363. if (!isset($params['format'])) {
  364. $params['format'] = 'H:i';
  365. }
  366. return self::typeDatetime($value, $params, $field);
  367. }
  368. /**
  369. * HTML5 input: month
  370. *
  371. * @param mixed $value Value to be validated.
  372. * @param array $params Validation parameters.
  373. * @param array $field Blueprint for the field.
  374. * @return bool True if validation succeeded.
  375. */
  376. public static function typeMonth($value, array $params, array $field)
  377. {
  378. $params = array($params);
  379. if (!isset($params['format'])) {
  380. $params['format'] = 'Y-m';
  381. }
  382. return self::typeDatetime($value, $params, $field);
  383. }
  384. /**
  385. * HTML5 input: week
  386. *
  387. * @param mixed $value Value to be validated.
  388. * @param array $params Validation parameters.
  389. * @param array $field Blueprint for the field.
  390. * @return bool True if validation succeeded.
  391. */
  392. public static function typeWeek($value, array $params, array $field)
  393. {
  394. if (!isset($params['format']) && !preg_match('/^\d{4}-W\d{2}$/u', $value)) {
  395. return false;
  396. }
  397. return self::typeDatetime($value, $params, $field);
  398. }
  399. /**
  400. * Custom input: array
  401. *
  402. * @param mixed $value Value to be validated.
  403. * @param array $params Validation parameters.
  404. * @param array $field Blueprint for the field.
  405. * @return bool True if validation succeeded.
  406. */
  407. public static function typeArray($value, array $params, array $field)
  408. {
  409. if (!is_array($value)) {
  410. return false;
  411. }
  412. if (isset($field['multiple'])) {
  413. if (isset($params['min']) && count($value) < $params['min']) {
  414. return false;
  415. }
  416. if (isset($params['max']) && count($value) > $params['max']) {
  417. return false;
  418. }
  419. $min = isset($params['min']) ? $params['min'] : 0;
  420. if (isset($params['step']) && (count($value) - $min) % $params['step'] == 0) {
  421. return false;
  422. }
  423. }
  424. $options = isset($field['options']) ? array_keys($field['options']) : array();
  425. $values = isset($field['use']) && $field['use'] == 'keys' ? array_keys($value) : $value;
  426. if ($options && array_diff($values, $options)) {
  427. return false;
  428. }
  429. return true;
  430. }
  431. protected static function filterArray($value, $params, $field)
  432. {
  433. $values = (array) $value;
  434. $options = isset($field['options']) ? array_keys($field['options']) : array();
  435. if ($options) {
  436. $useKey = isset($field['use']) && $field['use'] == 'keys';
  437. foreach ($values as $key => $value) {
  438. $values[$key] = $useKey ? (bool) $value : $value;
  439. }
  440. }
  441. return $values;
  442. }
  443. /**
  444. * Custom input: ignore (will not validate)
  445. *
  446. * @param mixed $value Value to be validated.
  447. * @param array $params Validation parameters.
  448. * @param array $field Blueprint for the field.
  449. * @return bool True if validation succeeded.
  450. */
  451. public static function typeIgnore($value, array $params, array $field)
  452. {
  453. return true;
  454. }
  455. // HTML5 attributes (min, max and range are handled inside the types)
  456. public static function validateRequired($value, $params)
  457. {
  458. return (bool) $params !== true || !empty($value);
  459. }
  460. public static function validatePattern($value, $params)
  461. {
  462. return (bool) preg_match("`^{$params}$`u", $value);
  463. }
  464. // Internal types
  465. public static function validateAlpha($value, $params)
  466. {
  467. return ctype_alpha($value);
  468. }
  469. public static function validateAlnum($value, $params)
  470. {
  471. return ctype_alnum($value);
  472. }
  473. public static function typeBool($value, $params)
  474. {
  475. return is_bool($value) || $value == 1 || $value == 0;
  476. }
  477. public static function validateBool($value, $params)
  478. {
  479. return is_bool($value) || $value == 1 || $value == 0;
  480. }
  481. protected static function filterBool($value, $params)
  482. {
  483. return (bool) $value;
  484. }
  485. public static function validateDigit($value, $params)
  486. {
  487. return ctype_digit($value);
  488. }
  489. public static function validateFloat($value, $params)
  490. {
  491. return is_float(filter_var($value, FILTER_VALIDATE_FLOAT));
  492. }
  493. protected static function filterFloat($value, $params)
  494. {
  495. return (float) $value;
  496. }
  497. public static function validateHex($value, $params)
  498. {
  499. return ctype_xdigit($value);
  500. }
  501. public static function validateInt($value, $params)
  502. {
  503. return is_numeric($value) && (int) $value == $value;
  504. }
  505. protected static function filterInt($value, $params)
  506. {
  507. return (int) $value;
  508. }
  509. public static function validateArray($value, $params)
  510. {
  511. return is_array($value) || ($value instanceof \ArrayAccess
  512. && $value instanceof \Traversable
  513. && $value instanceof \Countable);
  514. }
  515. public static function validateJson($value, $params)
  516. {
  517. return (bool) (json_decode($value));
  518. }
  519. }