Validation.php 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214
  1. <?php
  2. /**
  3. * @package Grav\Common\Data
  4. *
  5. * @copyright Copyright (c) 2015 - 2022 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. $value = (float)$value;
  448. $min = 0;
  449. if (isset($params['min'])) {
  450. $min = (float)$params['min'];
  451. if ($value < $min) {
  452. return false;
  453. }
  454. }
  455. if (isset($params['max'])) {
  456. $max = (float)$params['max'];
  457. if ($value > $max) {
  458. return false;
  459. }
  460. }
  461. if (isset($params['step'])) {
  462. $step = (float)$params['step'];
  463. // Count of how many steps we are above/below the minimum value.
  464. $pos = ($value - $min) / $step;
  465. return is_int(static::filterNumber($pos, $params, $field));
  466. }
  467. return true;
  468. }
  469. /**
  470. * @param mixed $value
  471. * @param array $params
  472. * @param array $field
  473. * @return float|int
  474. */
  475. protected static function filterNumber($value, array $params, array $field)
  476. {
  477. return (string)(int)$value !== (string)(float)$value ? (float)$value : (int)$value;
  478. }
  479. /**
  480. * @param mixed $value
  481. * @param array $params
  482. * @param array $field
  483. * @return string
  484. */
  485. protected static function filterDateTime($value, array $params, array $field)
  486. {
  487. $format = Grav::instance()['config']->get('system.pages.dateformat.default');
  488. if ($format) {
  489. $converted = new DateTime($value);
  490. return $converted->format($format);
  491. }
  492. return $value;
  493. }
  494. /**
  495. * HTML5 input: range
  496. *
  497. * @param mixed $value Value to be validated.
  498. * @param array $params Validation parameters.
  499. * @param array $field Blueprint for the field.
  500. * @return bool True if validation succeeded.
  501. */
  502. public static function typeRange($value, array $params, array $field)
  503. {
  504. return self::typeNumber($value, $params, $field);
  505. }
  506. /**
  507. * @param mixed $value
  508. * @param array $params
  509. * @param array $field
  510. * @return float|int
  511. */
  512. protected static function filterRange($value, array $params, array $field)
  513. {
  514. return self::filterNumber($value, $params, $field);
  515. }
  516. /**
  517. * HTML5 input: color
  518. *
  519. * @param mixed $value Value to be validated.
  520. * @param array $params Validation parameters.
  521. * @param array $field Blueprint for the field.
  522. * @return bool True if validation succeeded.
  523. */
  524. public static function typeColor($value, array $params, array $field)
  525. {
  526. return (bool)preg_match('/^\#[0-9a-fA-F]{3}[0-9a-fA-F]{3}?$/u', $value);
  527. }
  528. /**
  529. * HTML5 input: email
  530. *
  531. * @param mixed $value Value to be validated.
  532. * @param array $params Validation parameters.
  533. * @param array $field Blueprint for the field.
  534. * @return bool True if validation succeeded.
  535. */
  536. public static function typeEmail($value, array $params, array $field)
  537. {
  538. $values = !is_array($value) ? explode(',', preg_replace('/\s+/', '', $value)) : $value;
  539. foreach ($values as $val) {
  540. if (!(self::typeText($val, $params, $field) && filter_var($val, FILTER_VALIDATE_EMAIL))) {
  541. return false;
  542. }
  543. }
  544. return true;
  545. }
  546. /**
  547. * HTML5 input: url
  548. *
  549. * @param mixed $value Value to be validated.
  550. * @param array $params Validation parameters.
  551. * @param array $field Blueprint for the field.
  552. * @return bool True if validation succeeded.
  553. */
  554. public static function typeUrl($value, array $params, array $field)
  555. {
  556. return self::typeText($value, $params, $field) && filter_var($value, FILTER_VALIDATE_URL);
  557. }
  558. /**
  559. * HTML5 input: datetime
  560. *
  561. * @param mixed $value Value to be validated.
  562. * @param array $params Validation parameters.
  563. * @param array $field Blueprint for the field.
  564. * @return bool True if validation succeeded.
  565. */
  566. public static function typeDatetime($value, array $params, array $field)
  567. {
  568. if ($value instanceof DateTime) {
  569. return true;
  570. }
  571. if (!is_string($value)) {
  572. return false;
  573. }
  574. if (!isset($params['format'])) {
  575. return false !== strtotime($value);
  576. }
  577. $dateFromFormat = DateTime::createFromFormat($params['format'], $value);
  578. return $dateFromFormat && $value === date($params['format'], $dateFromFormat->getTimestamp());
  579. }
  580. /**
  581. * HTML5 input: datetime-local
  582. *
  583. * @param mixed $value Value to be validated.
  584. * @param array $params Validation parameters.
  585. * @param array $field Blueprint for the field.
  586. * @return bool True if validation succeeded.
  587. */
  588. public static function typeDatetimeLocal($value, array $params, array $field)
  589. {
  590. return self::typeDatetime($value, $params, $field);
  591. }
  592. /**
  593. * HTML5 input: date
  594. *
  595. * @param mixed $value Value to be validated.
  596. * @param array $params Validation parameters.
  597. * @param array $field Blueprint for the field.
  598. * @return bool True if validation succeeded.
  599. */
  600. public static function typeDate($value, array $params, array $field)
  601. {
  602. if (!isset($params['format'])) {
  603. $params['format'] = 'Y-m-d';
  604. }
  605. return self::typeDatetime($value, $params, $field);
  606. }
  607. /**
  608. * HTML5 input: time
  609. *
  610. * @param mixed $value Value to be validated.
  611. * @param array $params Validation parameters.
  612. * @param array $field Blueprint for the field.
  613. * @return bool True if validation succeeded.
  614. */
  615. public static function typeTime($value, array $params, array $field)
  616. {
  617. if (!isset($params['format'])) {
  618. $params['format'] = 'H:i';
  619. }
  620. return self::typeDatetime($value, $params, $field);
  621. }
  622. /**
  623. * HTML5 input: month
  624. *
  625. * @param mixed $value Value to be validated.
  626. * @param array $params Validation parameters.
  627. * @param array $field Blueprint for the field.
  628. * @return bool True if validation succeeded.
  629. */
  630. public static function typeMonth($value, array $params, array $field)
  631. {
  632. if (!isset($params['format'])) {
  633. $params['format'] = 'Y-m';
  634. }
  635. return self::typeDatetime($value, $params, $field);
  636. }
  637. /**
  638. * HTML5 input: week
  639. *
  640. * @param mixed $value Value to be validated.
  641. * @param array $params Validation parameters.
  642. * @param array $field Blueprint for the field.
  643. * @return bool True if validation succeeded.
  644. */
  645. public static function typeWeek($value, array $params, array $field)
  646. {
  647. if (!isset($params['format']) && !preg_match('/^\d{4}-W\d{2}$/u', $value)) {
  648. return false;
  649. }
  650. return self::typeDatetime($value, $params, $field);
  651. }
  652. /**
  653. * Custom input: array
  654. *
  655. * @param mixed $value Value to be validated.
  656. * @param array $params Validation parameters.
  657. * @param array $field Blueprint for the field.
  658. * @return bool True if validation succeeded.
  659. */
  660. public static function typeArray($value, array $params, array $field)
  661. {
  662. if (!is_array($value)) {
  663. return false;
  664. }
  665. if (isset($field['multiple'])) {
  666. if (isset($params['min']) && count($value) < $params['min']) {
  667. return false;
  668. }
  669. if (isset($params['max']) && count($value) > $params['max']) {
  670. return false;
  671. }
  672. $min = $params['min'] ?? 0;
  673. if (isset($params['step']) && (count($value) - $min) % $params['step'] === 0) {
  674. return false;
  675. }
  676. }
  677. // If creating new values is allowed, no further checks are needed.
  678. $validateOptions = $field['validate']['options'] ?? null;
  679. if (!empty($field['selectize']['create']) || $validateOptions === 'ignore') {
  680. return true;
  681. }
  682. $options = $field['options'] ?? [];
  683. $use = $field['use'] ?? 'values';
  684. if ($validateOptions) {
  685. // Use custom options structure.
  686. foreach ($options as &$option) {
  687. $option = $option[$validateOptions] ?? null;
  688. }
  689. unset($option);
  690. $options = array_values($options);
  691. } elseif (empty($field['selectize']) || empty($field['multiple'])) {
  692. $options = array_keys($options);
  693. }
  694. if ($use === 'keys') {
  695. $value = array_keys($value);
  696. }
  697. return !($options && array_diff($value, $options));
  698. }
  699. /**
  700. * @param mixed $value
  701. * @param array $params
  702. * @param array $field
  703. * @return array|null
  704. */
  705. protected static function filterFlatten_array($value, $params, $field)
  706. {
  707. $value = static::filterArray($value, $params, $field);
  708. return is_array($value) ? Utils::arrayUnflattenDotNotation($value) : null;
  709. }
  710. /**
  711. * @param mixed $value
  712. * @param array $params
  713. * @param array $field
  714. * @return array|null
  715. */
  716. protected static function filterArray($value, $params, $field)
  717. {
  718. $values = (array) $value;
  719. $options = isset($field['options']) ? array_keys($field['options']) : [];
  720. $multi = $field['multiple'] ?? false;
  721. if (count($values) === 1 && isset($values[0]) && $values[0] === '') {
  722. return null;
  723. }
  724. if ($options) {
  725. $useKey = isset($field['use']) && $field['use'] === 'keys';
  726. foreach ($values as $key => $val) {
  727. $values[$key] = $useKey ? (bool) $val : $val;
  728. }
  729. }
  730. if ($multi) {
  731. foreach ($values as $key => $val) {
  732. if (is_array($val)) {
  733. $val = implode(',', $val);
  734. $values[$key] = array_map('trim', explode(',', $val));
  735. } else {
  736. $values[$key] = trim($val);
  737. }
  738. }
  739. }
  740. $ignoreEmpty = isset($field['ignore_empty']) && Utils::isPositive($field['ignore_empty']);
  741. $valueType = $params['value_type'] ?? null;
  742. $keyType = $params['key_type'] ?? null;
  743. if ($ignoreEmpty || $valueType || $keyType) {
  744. $values = static::arrayFilterRecurse($values, ['value_type' => $valueType, 'key_type' => $keyType, 'ignore_empty' => $ignoreEmpty]);
  745. }
  746. return $values;
  747. }
  748. /**
  749. * @param array $values
  750. * @param array $params
  751. * @return array
  752. */
  753. protected static function arrayFilterRecurse(array $values, array $params): array
  754. {
  755. foreach ($values as $key => &$val) {
  756. if ($params['key_type']) {
  757. switch ($params['key_type']) {
  758. case 'int':
  759. $result = is_int($key);
  760. break;
  761. case 'string':
  762. $result = is_string($key);
  763. break;
  764. default:
  765. $result = false;
  766. }
  767. if (!$result) {
  768. unset($values[$key]);
  769. }
  770. }
  771. if (is_array($val)) {
  772. $val = static::arrayFilterRecurse($val, $params);
  773. if ($params['ignore_empty'] && empty($val)) {
  774. unset($values[$key]);
  775. }
  776. } else {
  777. if ($params['value_type'] && $val !== '' && $val !== null) {
  778. switch ($params['value_type']) {
  779. case 'bool':
  780. if (Utils::isPositive($val)) {
  781. $val = true;
  782. } elseif (Utils::isNegative($val)) {
  783. $val = false;
  784. } else {
  785. // Ignore invalid bool values.
  786. $val = null;
  787. }
  788. break;
  789. case 'int':
  790. $val = (int)$val;
  791. break;
  792. case 'float':
  793. $val = (float)$val;
  794. break;
  795. case 'string':
  796. $val = (string)$val;
  797. break;
  798. case 'trim':
  799. $val = trim($val);
  800. break;
  801. }
  802. }
  803. if ($params['ignore_empty'] && ($val === '' || $val === null)) {
  804. unset($values[$key]);
  805. }
  806. }
  807. }
  808. return $values;
  809. }
  810. /**
  811. * @param mixed $value
  812. * @param array $params
  813. * @param array $field
  814. * @return bool
  815. */
  816. public static function typeList($value, array $params, array $field)
  817. {
  818. if (!is_array($value)) {
  819. return false;
  820. }
  821. if (isset($field['fields'])) {
  822. foreach ($value as $key => $item) {
  823. foreach ($field['fields'] as $subKey => $subField) {
  824. $subKey = trim($subKey, '.');
  825. $subValue = $item[$subKey] ?? null;
  826. self::validate($subValue, $subField);
  827. }
  828. }
  829. }
  830. return true;
  831. }
  832. /**
  833. * @param mixed $value
  834. * @param array $params
  835. * @param array $field
  836. * @return array
  837. */
  838. protected static function filterList($value, array $params, array $field)
  839. {
  840. return (array) $value;
  841. }
  842. /**
  843. * @param mixed $value
  844. * @param array $params
  845. * @return array
  846. */
  847. public static function filterYaml($value, $params)
  848. {
  849. if (!is_string($value)) {
  850. return $value;
  851. }
  852. return (array) Yaml::parse($value);
  853. }
  854. /**
  855. * Custom input: ignore (will not validate)
  856. *
  857. * @param mixed $value Value to be validated.
  858. * @param array $params Validation parameters.
  859. * @param array $field Blueprint for the field.
  860. * @return bool True if validation succeeded.
  861. */
  862. public static function typeIgnore($value, array $params, array $field)
  863. {
  864. return true;
  865. }
  866. /**
  867. * @param mixed $value
  868. * @param array $params
  869. * @param array $field
  870. * @return mixed
  871. */
  872. public static function filterIgnore($value, array $params, array $field)
  873. {
  874. return $value;
  875. }
  876. /**
  877. * Input value which can be ignored.
  878. *
  879. * @param mixed $value Value to be validated.
  880. * @param array $params Validation parameters.
  881. * @param array $field Blueprint for the field.
  882. * @return bool True if validation succeeded.
  883. */
  884. public static function typeUnset($value, array $params, array $field)
  885. {
  886. return true;
  887. }
  888. /**
  889. * @param mixed $value
  890. * @param array $params
  891. * @param array $field
  892. * @return null
  893. */
  894. public static function filterUnset($value, array $params, array $field)
  895. {
  896. return null;
  897. }
  898. // HTML5 attributes (min, max and range are handled inside the types)
  899. /**
  900. * @param mixed $value
  901. * @param bool $params
  902. * @return bool
  903. */
  904. public static function validateRequired($value, $params)
  905. {
  906. if (is_scalar($value)) {
  907. return (bool) $params !== true || $value !== '';
  908. }
  909. return (bool) $params !== true || !empty($value);
  910. }
  911. /**
  912. * @param mixed $value
  913. * @param string $params
  914. * @return bool
  915. */
  916. public static function validatePattern($value, $params)
  917. {
  918. return (bool) preg_match("`^{$params}$`u", $value);
  919. }
  920. // Internal types
  921. /**
  922. * @param mixed $value
  923. * @param mixed $params
  924. * @return bool
  925. */
  926. public static function validateAlpha($value, $params)
  927. {
  928. return ctype_alpha($value);
  929. }
  930. /**
  931. * @param mixed $value
  932. * @param mixed $params
  933. * @return bool
  934. */
  935. public static function validateAlnum($value, $params)
  936. {
  937. return ctype_alnum($value);
  938. }
  939. /**
  940. * @param mixed $value
  941. * @param mixed $params
  942. * @return bool
  943. */
  944. public static function typeBool($value, $params)
  945. {
  946. return is_bool($value) || $value == 1 || $value == 0;
  947. }
  948. /**
  949. * @param mixed $value
  950. * @param mixed $params
  951. * @return bool
  952. */
  953. public static function validateBool($value, $params)
  954. {
  955. return is_bool($value) || $value == 1 || $value == 0;
  956. }
  957. /**
  958. * @param mixed $value
  959. * @param mixed $params
  960. * @return bool
  961. */
  962. protected static function filterBool($value, $params)
  963. {
  964. return (bool) $value;
  965. }
  966. /**
  967. * @param mixed $value
  968. * @param mixed $params
  969. * @return bool
  970. */
  971. public static function validateDigit($value, $params)
  972. {
  973. return ctype_digit($value);
  974. }
  975. /**
  976. * @param mixed $value
  977. * @param mixed $params
  978. * @return bool
  979. */
  980. public static function validateFloat($value, $params)
  981. {
  982. return is_float(filter_var($value, FILTER_VALIDATE_FLOAT));
  983. }
  984. /**
  985. * @param mixed $value
  986. * @param mixed $params
  987. * @return float
  988. */
  989. protected static function filterFloat($value, $params)
  990. {
  991. return (float) $value;
  992. }
  993. /**
  994. * @param mixed $value
  995. * @param mixed $params
  996. * @return bool
  997. */
  998. public static function validateHex($value, $params)
  999. {
  1000. return ctype_xdigit($value);
  1001. }
  1002. /**
  1003. * Custom input: int
  1004. *
  1005. * @param mixed $value Value to be validated.
  1006. * @param array $params Validation parameters.
  1007. * @param array $field Blueprint for the field.
  1008. * @return bool True if validation succeeded.
  1009. */
  1010. public static function typeInt($value, array $params, array $field)
  1011. {
  1012. $params['step'] = max(1, (int)($params['step'] ?? 0));
  1013. return self::typeNumber($value, $params, $field);
  1014. }
  1015. /**
  1016. * @param mixed $value
  1017. * @param mixed $params
  1018. * @return bool
  1019. */
  1020. public static function validateInt($value, $params)
  1021. {
  1022. return is_numeric($value) && (int)$value == $value;
  1023. }
  1024. /**
  1025. * @param mixed $value
  1026. * @param mixed $params
  1027. * @return int
  1028. */
  1029. protected static function filterInt($value, $params)
  1030. {
  1031. return (int)$value;
  1032. }
  1033. /**
  1034. * @param mixed $value
  1035. * @param mixed $params
  1036. * @return bool
  1037. */
  1038. public static function validateArray($value, $params)
  1039. {
  1040. return is_array($value) || ($value instanceof ArrayAccess && $value instanceof Traversable && $value instanceof Countable);
  1041. }
  1042. /**
  1043. * @param mixed $value
  1044. * @param mixed $params
  1045. * @return array
  1046. */
  1047. public static function filterItem_List($value, $params)
  1048. {
  1049. return array_values(array_filter($value, static function ($v) {
  1050. return !empty($v);
  1051. }));
  1052. }
  1053. /**
  1054. * @param mixed $value
  1055. * @param mixed $params
  1056. * @return bool
  1057. */
  1058. public static function validateJson($value, $params)
  1059. {
  1060. return (bool) (@json_decode($value));
  1061. }
  1062. }