Validation.php 20 KB

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