Validation.php 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191
  1. <?php
  2. /**
  3. * @package Grav\Common\Data
  4. *
  5. * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Common\Data;
  9. use ArrayAccess;
  10. use Countable;
  11. use DateTime;
  12. use Grav\Common\Config\Config;
  13. use Grav\Common\Grav;
  14. use Grav\Common\Language\Language;
  15. use Grav\Common\Security;
  16. use Grav\Common\User\Interfaces\UserInterface;
  17. use Grav\Common\Utils;
  18. use Grav\Common\Yaml;
  19. use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
  20. use Traversable;
  21. use function count;
  22. use function is_array;
  23. use function is_bool;
  24. use function is_float;
  25. use function is_int;
  26. use function is_string;
  27. /**
  28. * Class Validation
  29. * @package Grav\Common\Data
  30. */
  31. class Validation
  32. {
  33. /**
  34. * Validate value against a blueprint field definition.
  35. *
  36. * @param mixed $value
  37. * @param array $field
  38. * @return array
  39. */
  40. public static function validate($value, array $field)
  41. {
  42. if (!isset($field['type'])) {
  43. $field['type'] = 'text';
  44. }
  45. $validate = (array)($field['validate'] ?? null);
  46. $type = $validate['type'] ?? $field['type'];
  47. $required = $validate['required'] ?? false;
  48. // If value isn't required, we will stop validation if empty value is given.
  49. if ($required !== true && ($value === null || $value === '' || (($field['type'] === 'checkbox' || $field['type'] === 'switch') && $value == false))
  50. ) {
  51. return [];
  52. }
  53. // Get language class.
  54. $language = Grav::instance()['language'];
  55. $name = ucfirst($field['label'] ?? $field['name']);
  56. $message = (string) isset($field['validate']['message'])
  57. ? $language->translate($field['validate']['message'])
  58. : $language->translate('GRAV.FORM.INVALID_INPUT') . ' "' . $language->translate($name) . '"';
  59. // Validate type with fallback type text.
  60. $method = 'type' . str_replace('-', '_', $type);
  61. // If this is a YAML field validate/filter as such
  62. if (isset($field['yaml']) && $field['yaml'] === true) {
  63. $method = 'typeYaml';
  64. }
  65. $messages = [];
  66. $success = method_exists(__CLASS__, $method) ? self::$method($value, $validate, $field) : true;
  67. if (!$success) {
  68. $messages[$field['name']][] = $message;
  69. }
  70. // Check individual rules.
  71. foreach ($validate as $rule => $params) {
  72. $method = 'validate' . ucfirst(str_replace('-', '_', $rule));
  73. if (method_exists(__CLASS__, $method)) {
  74. $success = self::$method($value, $params);
  75. if (!$success) {
  76. $messages[$field['name']][] = $message;
  77. }
  78. }
  79. }
  80. return $messages;
  81. }
  82. /**
  83. * @param mixed $value
  84. * @param array $field
  85. * @return array
  86. */
  87. public static function checkSafety($value, array $field)
  88. {
  89. $messages = [];
  90. $type = $field['validate']['type'] ?? $field['type'] ?? 'text';
  91. $options = $field['xss_check'] ?? [];
  92. if ($options === false || $type === 'unset') {
  93. return $messages;
  94. }
  95. if (!is_array($options)) {
  96. $options = [];
  97. }
  98. $name = ucfirst($field['label'] ?? $field['name'] ?? 'UNKNOWN');
  99. /** @var UserInterface $user */
  100. $user = Grav::instance()['user'] ?? null;
  101. /** @var Config $config */
  102. $config = Grav::instance()['config'];
  103. $xss_whitelist = $config->get('security.xss_whitelist', 'admin.super');
  104. // Get language class.
  105. /** @var Language $language */
  106. $language = Grav::instance()['language'];
  107. if (!static::authorize($xss_whitelist, $user)) {
  108. $defaults = Security::getXssDefaults();
  109. $options += $defaults;
  110. $options['enabled_rules'] += $defaults['enabled_rules'];
  111. if (!empty($options['safe_protocols'])) {
  112. $options['invalid_protocols'] = array_diff($options['invalid_protocols'], $options['safe_protocols']);
  113. }
  114. if (!empty($options['safe_tags'])) {
  115. $options['dangerous_tags'] = array_diff($options['dangerous_tags'], $options['safe_tags']);
  116. }
  117. if (is_string($value)) {
  118. $violation = Security::detectXss($value, $options);
  119. if ($violation) {
  120. $messages[$name][] = $language->translate(['GRAV.FORM.XSS_ISSUES', $language->translate($name)], null, true);
  121. }
  122. } elseif (is_array($value)) {
  123. $violations = Security::detectXssFromArray($value, "{$name}.", $options);
  124. if ($violations) {
  125. $messages[$name][] = $language->translate(['GRAV.FORM.XSS_ISSUES', $language->translate($name)], null, true);
  126. }
  127. }
  128. }
  129. return $messages;
  130. }
  131. /**
  132. * Checks user authorisation to the action.
  133. *
  134. * @param string|string[] $action
  135. * @param UserInterface|null $user
  136. * @return bool
  137. */
  138. public static function authorize($action, UserInterface $user = null)
  139. {
  140. if (!$user) {
  141. return false;
  142. }
  143. $action = (array)$action;
  144. foreach ($action as $a) {
  145. // Ignore 'admin.super' if it's not the only value to be checked.
  146. if ($a === 'admin.super' && count($action) > 1 && $user instanceof FlexObjectInterface) {
  147. continue;
  148. }
  149. if ($user->authorize($a)) {
  150. return true;
  151. }
  152. }
  153. return false;
  154. }
  155. /**
  156. * Filter value against a blueprint field definition.
  157. *
  158. * @param mixed $value
  159. * @param array $field
  160. * @return mixed Filtered value.
  161. */
  162. public static function filter($value, array $field)
  163. {
  164. $validate = (array)($field['filter'] ?? $field['validate'] ?? null);
  165. // If value isn't required, we will return null if empty value is given.
  166. if (($value === null || $value === '') && empty($validate['required'])) {
  167. return null;
  168. }
  169. if (!isset($field['type'])) {
  170. $field['type'] = 'text';
  171. }
  172. $type = $field['filter']['type'] ?? $field['validate']['type'] ?? $field['type'];
  173. $method = 'filter' . ucfirst(str_replace('-', '_', $type));
  174. // If this is a YAML field validate/filter as such
  175. if (isset($field['yaml']) && $field['yaml'] === true) {
  176. $method = 'filterYaml';
  177. }
  178. if (!method_exists(__CLASS__, $method)) {
  179. $method = isset($field['array']) && $field['array'] === true ? 'filterArray' : 'filterText';
  180. }
  181. return self::$method($value, $validate, $field);
  182. }
  183. /**
  184. * HTML5 input: text
  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 typeText($value, array $params, array $field)
  192. {
  193. if (!is_string($value) && !is_numeric($value)) {
  194. return false;
  195. }
  196. $value = (string)$value;
  197. if (!empty($params['trim'])) {
  198. $value = trim($value);
  199. }
  200. $value = preg_replace("/\r\n|\r/um", "\n", $value);
  201. $len = mb_strlen($value);
  202. $min = (int)($params['min'] ?? 0);
  203. if ($min && $len < $min) {
  204. return false;
  205. }
  206. $max = (int)($params['max'] ?? 0);
  207. if ($max && $len > $max) {
  208. return false;
  209. }
  210. $step = (int)($params['step'] ?? 0);
  211. if ($step && ($len - $min) % $step === 0) {
  212. return false;
  213. }
  214. if ((!isset($params['multiline']) || !$params['multiline']) && preg_match('/\R/um', $value)) {
  215. return false;
  216. }
  217. return true;
  218. }
  219. /**
  220. * @param mixed $value
  221. * @param array $params
  222. * @param array $field
  223. * @return string
  224. */
  225. protected static function filterText($value, array $params, array $field)
  226. {
  227. if (!is_string($value) && !is_numeric($value)) {
  228. return '';
  229. }
  230. $value = (string)$value;
  231. if (!empty($params['trim'])) {
  232. $value = trim($value);
  233. }
  234. return preg_replace("/\r\n|\r/um", "\n", $value);
  235. }
  236. /**
  237. * @param mixed $value
  238. * @param array $params
  239. * @param array $field
  240. * @return string|null
  241. */
  242. protected static function filterCheckbox($value, array $params, array $field)
  243. {
  244. $value = (string)$value;
  245. $field_value = (string)($field['value'] ?? '1');
  246. return $value === $field_value ? $value : null;
  247. }
  248. /**
  249. * @param mixed $value
  250. * @param array $params
  251. * @param array $field
  252. * @return array|array[]|false|string[]
  253. */
  254. protected static function filterCommaList($value, array $params, array $field)
  255. {
  256. return is_array($value) ? $value : preg_split('/\s*,\s*/', $value, -1, PREG_SPLIT_NO_EMPTY);
  257. }
  258. /**
  259. * @param mixed $value
  260. * @param array $params
  261. * @param array $field
  262. * @return bool
  263. */
  264. public static function typeCommaList($value, array $params, array $field)
  265. {
  266. return is_array($value) ? true : self::typeText($value, $params, $field);
  267. }
  268. /**
  269. * @param mixed $value
  270. * @param array $params
  271. * @param array $field
  272. * @return array|array[]|false|string[]
  273. */
  274. protected static function filterLines($value, array $params, array $field)
  275. {
  276. return is_array($value) ? $value : preg_split('/\s*[\r\n]+\s*/', $value, -1, PREG_SPLIT_NO_EMPTY);
  277. }
  278. /**
  279. * @param mixed $value
  280. * @param array $params
  281. * @return string
  282. */
  283. protected static function filterLower($value, array $params)
  284. {
  285. return mb_strtolower($value);
  286. }
  287. /**
  288. * @param mixed $value
  289. * @param array $params
  290. * @return string
  291. */
  292. protected static function filterUpper($value, array $params)
  293. {
  294. return mb_strtoupper($value);
  295. }
  296. /**
  297. * HTML5 input: textarea
  298. *
  299. * @param mixed $value Value to be validated.
  300. * @param array $params Validation parameters.
  301. * @param array $field Blueprint for the field.
  302. * @return bool True if validation succeeded.
  303. */
  304. public static function typeTextarea($value, array $params, array $field)
  305. {
  306. if (!isset($params['multiline'])) {
  307. $params['multiline'] = true;
  308. }
  309. return self::typeText($value, $params, $field);
  310. }
  311. /**
  312. * HTML5 input: password
  313. *
  314. * @param mixed $value Value to be validated.
  315. * @param array $params Validation parameters.
  316. * @param array $field Blueprint for the field.
  317. * @return bool True if validation succeeded.
  318. */
  319. public static function typePassword($value, array $params, array $field)
  320. {
  321. return self::typeText($value, $params, $field);
  322. }
  323. /**
  324. * HTML5 input: hidden
  325. *
  326. * @param mixed $value Value to be validated.
  327. * @param array $params Validation parameters.
  328. * @param array $field Blueprint for the field.
  329. * @return bool True if validation succeeded.
  330. */
  331. public static function typeHidden($value, array $params, array $field)
  332. {
  333. return self::typeText($value, $params, $field);
  334. }
  335. /**
  336. * Custom input: checkbox list
  337. *
  338. * @param mixed $value Value to be validated.
  339. * @param array $params Validation parameters.
  340. * @param array $field Blueprint for the field.
  341. * @return bool True if validation succeeded.
  342. */
  343. public static function typeCheckboxes($value, array $params, array $field)
  344. {
  345. // Set multiple: true so checkboxes can easily use min/max counts to control number of options required
  346. $field['multiple'] = true;
  347. return self::typeArray((array) $value, $params, $field);
  348. }
  349. /**
  350. * @param mixed $value
  351. * @param array $params
  352. * @param array $field
  353. * @return array|null
  354. */
  355. protected static function filterCheckboxes($value, array $params, array $field)
  356. {
  357. return self::filterArray($value, $params, $field);
  358. }
  359. /**
  360. * HTML5 input: checkbox
  361. *
  362. * @param mixed $value Value to be validated.
  363. * @param array $params Validation parameters.
  364. * @param array $field Blueprint for the field.
  365. * @return bool True if validation succeeded.
  366. */
  367. public static function typeCheckbox($value, array $params, array $field)
  368. {
  369. $value = (string)$value;
  370. $field_value = (string)($field['value'] ?? '1');
  371. return $value === $field_value;
  372. }
  373. /**
  374. * HTML5 input: radio
  375. *
  376. * @param mixed $value Value to be validated.
  377. * @param array $params Validation parameters.
  378. * @param array $field Blueprint for the field.
  379. * @return bool True if validation succeeded.
  380. */
  381. public static function typeRadio($value, array $params, array $field)
  382. {
  383. return self::typeArray((array) $value, $params, $field);
  384. }
  385. /**
  386. * Custom input: toggle
  387. *
  388. * @param mixed $value Value to be validated.
  389. * @param array $params Validation parameters.
  390. * @param array $field Blueprint for the field.
  391. * @return bool True if validation succeeded.
  392. */
  393. public static function typeToggle($value, array $params, array $field)
  394. {
  395. if (is_bool($value)) {
  396. $value = (int)$value;
  397. }
  398. return self::typeArray((array) $value, $params, $field);
  399. }
  400. /**
  401. * Custom input: file
  402. *
  403. * @param mixed $value Value to be validated.
  404. * @param array $params Validation parameters.
  405. * @param array $field Blueprint for the field.
  406. * @return bool True if validation succeeded.
  407. */
  408. public static function typeFile($value, array $params, array $field)
  409. {
  410. return self::typeArray((array)$value, $params, $field);
  411. }
  412. /**
  413. * @param mixed $value
  414. * @param array $params
  415. * @param array $field
  416. * @return array
  417. */
  418. protected static function filterFile($value, array $params, array $field)
  419. {
  420. return (array)$value;
  421. }
  422. /**
  423. * HTML5 input: select
  424. *
  425. * @param mixed $value Value to be validated.
  426. * @param array $params Validation parameters.
  427. * @param array $field Blueprint for the field.
  428. * @return bool True if validation succeeded.
  429. */
  430. public static function typeSelect($value, array $params, array $field)
  431. {
  432. return self::typeArray((array) $value, $params, $field);
  433. }
  434. /**
  435. * HTML5 input: number
  436. *
  437. * @param mixed $value Value to be validated.
  438. * @param array $params Validation parameters.
  439. * @param array $field Blueprint for the field.
  440. * @return bool True if validation succeeded.
  441. */
  442. public static function typeNumber($value, array $params, array $field)
  443. {
  444. if (!is_numeric($value)) {
  445. return false;
  446. }
  447. if (isset($params['min']) && $value < $params['min']) {
  448. return false;
  449. }
  450. if (isset($params['max']) && $value > $params['max']) {
  451. return false;
  452. }
  453. $min = $params['min'] ?? 0;
  454. return !(isset($params['step']) && fmod($value - $min, $params['step']) === 0);
  455. }
  456. /**
  457. * @param mixed $value
  458. * @param array $params
  459. * @param array $field
  460. * @return float|int
  461. */
  462. protected static function filterNumber($value, array $params, array $field)
  463. {
  464. return (string)(int)$value !== (string)(float)$value ? (float)$value : (int)$value;
  465. }
  466. /**
  467. * @param mixed $value
  468. * @param array $params
  469. * @param array $field
  470. * @return string
  471. */
  472. protected static function filterDateTime($value, array $params, array $field)
  473. {
  474. $format = Grav::instance()['config']->get('system.pages.dateformat.default');
  475. if ($format) {
  476. $converted = new DateTime($value);
  477. return $converted->format($format);
  478. }
  479. return $value;
  480. }
  481. /**
  482. * HTML5 input: range
  483. *
  484. * @param mixed $value Value to be validated.
  485. * @param array $params Validation parameters.
  486. * @param array $field Blueprint for the field.
  487. * @return bool True if validation succeeded.
  488. */
  489. public static function typeRange($value, array $params, array $field)
  490. {
  491. return self::typeNumber($value, $params, $field);
  492. }
  493. /**
  494. * @param mixed $value
  495. * @param array $params
  496. * @param array $field
  497. * @return float|int
  498. */
  499. protected static function filterRange($value, array $params, array $field)
  500. {
  501. return self::filterNumber($value, $params, $field);
  502. }
  503. /**
  504. * HTML5 input: color
  505. *
  506. * @param mixed $value Value to be validated.
  507. * @param array $params Validation parameters.
  508. * @param array $field Blueprint for the field.
  509. * @return bool True if validation succeeded.
  510. */
  511. public static function typeColor($value, array $params, array $field)
  512. {
  513. return preg_match('/^\#[0-9a-fA-F]{3}[0-9a-fA-F]{3}?$/u', $value);
  514. }
  515. /**
  516. * HTML5 input: email
  517. *
  518. * @param mixed $value Value to be validated.
  519. * @param array $params Validation parameters.
  520. * @param array $field Blueprint for the field.
  521. * @return bool True if validation succeeded.
  522. */
  523. public static function typeEmail($value, array $params, array $field)
  524. {
  525. $values = !is_array($value) ? explode(',', preg_replace('/\s+/', '', $value)) : $value;
  526. foreach ($values as $val) {
  527. if (!(self::typeText($val, $params, $field) && filter_var($val, FILTER_VALIDATE_EMAIL))) {
  528. return false;
  529. }
  530. }
  531. return true;
  532. }
  533. /**
  534. * HTML5 input: url
  535. *
  536. * @param mixed $value Value to be validated.
  537. * @param array $params Validation parameters.
  538. * @param array $field Blueprint for the field.
  539. * @return bool True if validation succeeded.
  540. */
  541. public static function typeUrl($value, array $params, array $field)
  542. {
  543. return self::typeText($value, $params, $field) && filter_var($value, FILTER_VALIDATE_URL);
  544. }
  545. /**
  546. * HTML5 input: datetime
  547. *
  548. * @param mixed $value Value to be validated.
  549. * @param array $params Validation parameters.
  550. * @param array $field Blueprint for the field.
  551. * @return bool True if validation succeeded.
  552. */
  553. public static function typeDatetime($value, array $params, array $field)
  554. {
  555. if ($value instanceof DateTime) {
  556. return true;
  557. }
  558. if (!is_string($value)) {
  559. return false;
  560. }
  561. if (!isset($params['format'])) {
  562. return false !== strtotime($value);
  563. }
  564. $dateFromFormat = DateTime::createFromFormat($params['format'], $value);
  565. return $dateFromFormat && $value === date($params['format'], $dateFromFormat->getTimestamp());
  566. }
  567. /**
  568. * HTML5 input: datetime-local
  569. *
  570. * @param mixed $value Value to be validated.
  571. * @param array $params Validation parameters.
  572. * @param array $field Blueprint for the field.
  573. * @return bool True if validation succeeded.
  574. */
  575. public static function typeDatetimeLocal($value, array $params, array $field)
  576. {
  577. return self::typeDatetime($value, $params, $field);
  578. }
  579. /**
  580. * HTML5 input: date
  581. *
  582. * @param mixed $value Value to be validated.
  583. * @param array $params Validation parameters.
  584. * @param array $field Blueprint for the field.
  585. * @return bool True if validation succeeded.
  586. */
  587. public static function typeDate($value, array $params, array $field)
  588. {
  589. if (!isset($params['format'])) {
  590. $params['format'] = 'Y-m-d';
  591. }
  592. return self::typeDatetime($value, $params, $field);
  593. }
  594. /**
  595. * HTML5 input: time
  596. *
  597. * @param mixed $value Value to be validated.
  598. * @param array $params Validation parameters.
  599. * @param array $field Blueprint for the field.
  600. * @return bool True if validation succeeded.
  601. */
  602. public static function typeTime($value, array $params, array $field)
  603. {
  604. if (!isset($params['format'])) {
  605. $params['format'] = 'H:i';
  606. }
  607. return self::typeDatetime($value, $params, $field);
  608. }
  609. /**
  610. * HTML5 input: month
  611. *
  612. * @param mixed $value Value to be validated.
  613. * @param array $params Validation parameters.
  614. * @param array $field Blueprint for the field.
  615. * @return bool True if validation succeeded.
  616. */
  617. public static function typeMonth($value, array $params, array $field)
  618. {
  619. if (!isset($params['format'])) {
  620. $params['format'] = 'Y-m';
  621. }
  622. return self::typeDatetime($value, $params, $field);
  623. }
  624. /**
  625. * HTML5 input: week
  626. *
  627. * @param mixed $value Value to be validated.
  628. * @param array $params Validation parameters.
  629. * @param array $field Blueprint for the field.
  630. * @return bool True if validation succeeded.
  631. */
  632. public static function typeWeek($value, array $params, array $field)
  633. {
  634. if (!isset($params['format']) && !preg_match('/^\d{4}-W\d{2}$/u', $value)) {
  635. return false;
  636. }
  637. return self::typeDatetime($value, $params, $field);
  638. }
  639. /**
  640. * Custom input: array
  641. *
  642. * @param mixed $value Value to be validated.
  643. * @param array $params Validation parameters.
  644. * @param array $field Blueprint for the field.
  645. * @return bool True if validation succeeded.
  646. */
  647. public static function typeArray($value, array $params, array $field)
  648. {
  649. if (!is_array($value)) {
  650. return false;
  651. }
  652. if (isset($field['multiple'])) {
  653. if (isset($params['min']) && count($value) < $params['min']) {
  654. return false;
  655. }
  656. if (isset($params['max']) && count($value) > $params['max']) {
  657. return false;
  658. }
  659. $min = $params['min'] ?? 0;
  660. if (isset($params['step']) && (count($value) - $min) % $params['step'] === 0) {
  661. return false;
  662. }
  663. }
  664. // If creating new values is allowed, no further checks are needed.
  665. if (!empty($field['selectize']['create'])) {
  666. return true;
  667. }
  668. $options = $field['options'] ?? [];
  669. $use = $field['use'] ?? 'values';
  670. if (empty($field['selectize']) || empty($field['multiple'])) {
  671. $options = array_keys($options);
  672. }
  673. if ($use === 'keys') {
  674. $value = array_keys($value);
  675. }
  676. return !($options && array_diff($value, $options));
  677. }
  678. /**
  679. * @param mixed $value
  680. * @param array $params
  681. * @param array $field
  682. * @return array|null
  683. */
  684. protected static function filterFlatten_array($value, $params, $field)
  685. {
  686. $value = static::filterArray($value, $params, $field);
  687. return Utils::arrayUnflattenDotNotation($value);
  688. }
  689. /**
  690. * @param mixed $value
  691. * @param array $params
  692. * @param array $field
  693. * @return array|null
  694. */
  695. protected static function filterArray($value, $params, $field)
  696. {
  697. $values = (array) $value;
  698. $options = isset($field['options']) ? array_keys($field['options']) : [];
  699. $multi = $field['multiple'] ?? false;
  700. if (count($values) === 1 && isset($values[0]) && $values[0] === '') {
  701. return null;
  702. }
  703. if ($options) {
  704. $useKey = isset($field['use']) && $field['use'] === 'keys';
  705. foreach ($values as $key => $val) {
  706. $values[$key] = $useKey ? (bool) $val : $val;
  707. }
  708. }
  709. if ($multi) {
  710. foreach ($values as $key => $val) {
  711. if (is_array($val)) {
  712. $val = implode(',', $val);
  713. $values[$key] = array_map('trim', explode(',', $val));
  714. } else {
  715. $values[$key] = trim($val);
  716. }
  717. }
  718. }
  719. $ignoreEmpty = isset($field['ignore_empty']) && Utils::isPositive($field['ignore_empty']);
  720. $valueType = $params['value_type'] ?? null;
  721. $keyType = $params['key_type'] ?? null;
  722. if ($ignoreEmpty || $valueType || $keyType) {
  723. $values = static::arrayFilterRecurse($values, ['value_type' => $valueType, 'key_type' => $keyType, 'ignore_empty' => $ignoreEmpty]);
  724. }
  725. return $values;
  726. }
  727. /**
  728. * @param array $values
  729. * @param array $params
  730. * @return array
  731. */
  732. protected static function arrayFilterRecurse(array $values, array $params): array
  733. {
  734. foreach ($values as $key => &$val) {
  735. if ($params['key_type']) {
  736. switch ($params['key_type']) {
  737. case 'int':
  738. $result = is_int($key);
  739. break;
  740. case 'string':
  741. $result = is_string($key);
  742. break;
  743. default:
  744. $result = false;
  745. }
  746. if (!$result) {
  747. unset($values[$key]);
  748. }
  749. }
  750. if (is_array($val)) {
  751. $val = static::arrayFilterRecurse($val, $params);
  752. if ($params['ignore_empty'] && empty($val)) {
  753. unset($values[$key]);
  754. }
  755. } else {
  756. if ($params['value_type'] && $val !== '' && $val !== null) {
  757. switch ($params['value_type']) {
  758. case 'bool':
  759. if (Utils::isPositive($val)) {
  760. $val = true;
  761. } elseif (Utils::isNegative($val)) {
  762. $val = false;
  763. } else {
  764. // Ignore invalid bool values.
  765. $val = null;
  766. }
  767. break;
  768. case 'int':
  769. $val = (int)$val;
  770. break;
  771. case 'float':
  772. $val = (float)$val;
  773. break;
  774. case 'string':
  775. $val = (string)$val;
  776. break;
  777. case 'trim':
  778. $val = trim($val);
  779. break;
  780. }
  781. }
  782. if ($params['ignore_empty'] && ($val === '' || $val === null)) {
  783. unset($values[$key]);
  784. }
  785. }
  786. }
  787. return $values;
  788. }
  789. /**
  790. * @param mixed $value
  791. * @param array $params
  792. * @param array $field
  793. * @return bool
  794. */
  795. public static function typeList($value, array $params, array $field)
  796. {
  797. if (!is_array($value)) {
  798. return false;
  799. }
  800. if (isset($field['fields'])) {
  801. foreach ($value as $key => $item) {
  802. foreach ($field['fields'] as $subKey => $subField) {
  803. $subKey = trim($subKey, '.');
  804. $subValue = $item[$subKey] ?? null;
  805. self::validate($subValue, $subField);
  806. }
  807. }
  808. }
  809. return true;
  810. }
  811. /**
  812. * @param mixed $value
  813. * @param array $params
  814. * @param array $field
  815. * @return array
  816. */
  817. protected static function filterList($value, array $params, array $field)
  818. {
  819. return (array) $value;
  820. }
  821. /**
  822. * @param mixed $value
  823. * @param array $params
  824. * @return array
  825. */
  826. public static function filterYaml($value, $params)
  827. {
  828. if (!is_string($value)) {
  829. return $value;
  830. }
  831. return (array) Yaml::parse($value);
  832. }
  833. /**
  834. * Custom input: ignore (will not validate)
  835. *
  836. * @param mixed $value Value to be validated.
  837. * @param array $params Validation parameters.
  838. * @param array $field Blueprint for the field.
  839. * @return bool True if validation succeeded.
  840. */
  841. public static function typeIgnore($value, array $params, array $field)
  842. {
  843. return true;
  844. }
  845. /**
  846. * @param mixed $value
  847. * @param array $params
  848. * @param array $field
  849. * @return mixed
  850. */
  851. public static function filterIgnore($value, array $params, array $field)
  852. {
  853. return $value;
  854. }
  855. /**
  856. * Input value which can be ignored.
  857. *
  858. * @param mixed $value Value to be validated.
  859. * @param array $params Validation parameters.
  860. * @param array $field Blueprint for the field.
  861. * @return bool True if validation succeeded.
  862. */
  863. public static function typeUnset($value, array $params, array $field)
  864. {
  865. return true;
  866. }
  867. /**
  868. * @param mixed $value
  869. * @param array $params
  870. * @param array $field
  871. * @return null
  872. */
  873. public static function filterUnset($value, array $params, array $field)
  874. {
  875. return null;
  876. }
  877. // HTML5 attributes (min, max and range are handled inside the types)
  878. /**
  879. * @param mixed $value
  880. * @param bool $params
  881. * @return bool
  882. */
  883. public static function validateRequired($value, $params)
  884. {
  885. if (is_scalar($value)) {
  886. return (bool) $params !== true || $value !== '';
  887. }
  888. return (bool) $params !== true || !empty($value);
  889. }
  890. /**
  891. * @param mixed $value
  892. * @param string $params
  893. * @return bool
  894. */
  895. public static function validatePattern($value, $params)
  896. {
  897. return (bool) preg_match("`^{$params}$`u", $value);
  898. }
  899. // Internal types
  900. /**
  901. * @param mixed $value
  902. * @param mixed $params
  903. * @return bool
  904. */
  905. public static function validateAlpha($value, $params)
  906. {
  907. return ctype_alpha($value);
  908. }
  909. /**
  910. * @param mixed $value
  911. * @param mixed $params
  912. * @return bool
  913. */
  914. public static function validateAlnum($value, $params)
  915. {
  916. return ctype_alnum($value);
  917. }
  918. /**
  919. * @param mixed $value
  920. * @param mixed $params
  921. * @return bool
  922. */
  923. public static function typeBool($value, $params)
  924. {
  925. return is_bool($value) || $value == 1 || $value == 0;
  926. }
  927. /**
  928. * @param mixed $value
  929. * @param mixed $params
  930. * @return bool
  931. */
  932. public static function validateBool($value, $params)
  933. {
  934. return is_bool($value) || $value == 1 || $value == 0;
  935. }
  936. /**
  937. * @param mixed $value
  938. * @param mixed $params
  939. * @return bool
  940. */
  941. protected static function filterBool($value, $params)
  942. {
  943. return (bool) $value;
  944. }
  945. /**
  946. * @param mixed $value
  947. * @param mixed $params
  948. * @return bool
  949. */
  950. public static function validateDigit($value, $params)
  951. {
  952. return ctype_digit($value);
  953. }
  954. /**
  955. * @param mixed $value
  956. * @param mixed $params
  957. * @return bool
  958. */
  959. public static function validateFloat($value, $params)
  960. {
  961. return is_float(filter_var($value, FILTER_VALIDATE_FLOAT));
  962. }
  963. /**
  964. * @param mixed $value
  965. * @param mixed $params
  966. * @return float
  967. */
  968. protected static function filterFloat($value, $params)
  969. {
  970. return (float) $value;
  971. }
  972. /**
  973. * @param mixed $value
  974. * @param mixed $params
  975. * @return bool
  976. */
  977. public static function validateHex($value, $params)
  978. {
  979. return ctype_xdigit($value);
  980. }
  981. /**
  982. * Custom input: int
  983. *
  984. * @param mixed $value Value to be validated.
  985. * @param array $params Validation parameters.
  986. * @param array $field Blueprint for the field.
  987. * @return bool True if validation succeeded.
  988. */
  989. public static function typeInt($value, array $params, array $field)
  990. {
  991. $params['step'] = max(1, (int)($params['step'] ?? 0));
  992. return self::typeNumber($value, $params, $field);
  993. }
  994. /**
  995. * @param mixed $value
  996. * @param mixed $params
  997. * @return bool
  998. */
  999. public static function validateInt($value, $params)
  1000. {
  1001. return is_numeric($value) && (int)$value == $value;
  1002. }
  1003. /**
  1004. * @param mixed $value
  1005. * @param mixed $params
  1006. * @return int
  1007. */
  1008. protected static function filterInt($value, $params)
  1009. {
  1010. return (int)$value;
  1011. }
  1012. /**
  1013. * @param mixed $value
  1014. * @param mixed $params
  1015. * @return bool
  1016. */
  1017. public static function validateArray($value, $params)
  1018. {
  1019. return is_array($value) || ($value instanceof ArrayAccess && $value instanceof Traversable && $value instanceof Countable);
  1020. }
  1021. /**
  1022. * @param mixed $value
  1023. * @param mixed $params
  1024. * @return array
  1025. */
  1026. public static function filterItem_List($value, $params)
  1027. {
  1028. return array_values(array_filter($value, function ($v) {
  1029. return !empty($v);
  1030. }));
  1031. }
  1032. /**
  1033. * @param mixed $value
  1034. * @param mixed $params
  1035. * @return bool
  1036. */
  1037. public static function validateJson($value, $params)
  1038. {
  1039. return (bool) (@json_decode($value));
  1040. }
  1041. }