Validation.php 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232
  1. <?php
  2. /**
  3. * @package Grav\Common\Data
  4. *
  5. * @copyright Copyright (c) 2015 - 2023 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. $multiline = isset($params['multiline']) && $params['multiline'];
  207. $max = (int)($params['max'] ?? ($multiline ? 65536 : 2048));
  208. if ($max && $len > $max) {
  209. return false;
  210. }
  211. $step = (int)($params['step'] ?? 0);
  212. if ($step && ($len - $min) % $step === 0) {
  213. return false;
  214. }
  215. if (!$multiline && preg_match('/\R/um', $value)) {
  216. return false;
  217. }
  218. return true;
  219. }
  220. /**
  221. * @param mixed $value
  222. * @param array $params
  223. * @param array $field
  224. * @return string
  225. */
  226. protected static function filterText($value, array $params, array $field)
  227. {
  228. if (!is_string($value) && !is_numeric($value)) {
  229. return '';
  230. }
  231. $value = (string)$value;
  232. if (!empty($params['trim'])) {
  233. $value = trim($value);
  234. }
  235. return preg_replace("/\r\n|\r/um", "\n", $value);
  236. }
  237. /**
  238. * @param mixed $value
  239. * @param array $params
  240. * @param array $field
  241. * @return string|null
  242. */
  243. protected static function filterCheckbox($value, array $params, array $field)
  244. {
  245. $value = (string)$value;
  246. $field_value = (string)($field['value'] ?? '1');
  247. return $value === $field_value ? $value : null;
  248. }
  249. /**
  250. * @param mixed $value
  251. * @param array $params
  252. * @param array $field
  253. * @return array|array[]|false|string[]
  254. */
  255. protected static function filterCommaList($value, array $params, array $field)
  256. {
  257. return is_array($value) ? $value : preg_split('/\s*,\s*/', $value, -1, PREG_SPLIT_NO_EMPTY);
  258. }
  259. /**
  260. * @param mixed $value
  261. * @param array $params
  262. * @param array $field
  263. * @return bool
  264. */
  265. public static function typeCommaList($value, array $params, array $field)
  266. {
  267. if (!isset($params['max'])) {
  268. $params['max'] = 2048;
  269. }
  270. return is_array($value) ? true : self::typeText($value, $params, $field);
  271. }
  272. /**
  273. * @param mixed $value
  274. * @param array $params
  275. * @param array $field
  276. * @return array|array[]|false|string[]
  277. */
  278. protected static function filterLines($value, array $params, array $field)
  279. {
  280. return is_array($value) ? $value : preg_split('/\s*[\r\n]+\s*/', $value, -1, PREG_SPLIT_NO_EMPTY);
  281. }
  282. /**
  283. * @param mixed $value
  284. * @param array $params
  285. * @return string
  286. */
  287. protected static function filterLower($value, array $params)
  288. {
  289. return mb_strtolower($value);
  290. }
  291. /**
  292. * @param mixed $value
  293. * @param array $params
  294. * @return string
  295. */
  296. protected static function filterUpper($value, array $params)
  297. {
  298. return mb_strtoupper($value);
  299. }
  300. /**
  301. * HTML5 input: textarea
  302. *
  303. * @param mixed $value Value to be validated.
  304. * @param array $params Validation parameters.
  305. * @param array $field Blueprint for the field.
  306. * @return bool True if validation succeeded.
  307. */
  308. public static function typeTextarea($value, array $params, array $field)
  309. {
  310. if (!isset($params['multiline'])) {
  311. $params['multiline'] = true;
  312. }
  313. return self::typeText($value, $params, $field);
  314. }
  315. /**
  316. * HTML5 input: password
  317. *
  318. * @param mixed $value Value to be validated.
  319. * @param array $params Validation parameters.
  320. * @param array $field Blueprint for the field.
  321. * @return bool True if validation succeeded.
  322. */
  323. public static function typePassword($value, array $params, array $field)
  324. {
  325. if (!isset($params['max'])) {
  326. $params['max'] = 256;
  327. }
  328. return self::typeText($value, $params, $field);
  329. }
  330. /**
  331. * HTML5 input: hidden
  332. *
  333. * @param mixed $value Value to be validated.
  334. * @param array $params Validation parameters.
  335. * @param array $field Blueprint for the field.
  336. * @return bool True if validation succeeded.
  337. */
  338. public static function typeHidden($value, array $params, array $field)
  339. {
  340. return self::typeText($value, $params, $field);
  341. }
  342. /**
  343. * Custom input: checkbox list
  344. *
  345. * @param mixed $value Value to be validated.
  346. * @param array $params Validation parameters.
  347. * @param array $field Blueprint for the field.
  348. * @return bool True if validation succeeded.
  349. */
  350. public static function typeCheckboxes($value, array $params, array $field)
  351. {
  352. // Set multiple: true so checkboxes can easily use min/max counts to control number of options required
  353. $field['multiple'] = true;
  354. return self::typeArray((array) $value, $params, $field);
  355. }
  356. /**
  357. * @param mixed $value
  358. * @param array $params
  359. * @param array $field
  360. * @return array|null
  361. */
  362. protected static function filterCheckboxes($value, array $params, array $field)
  363. {
  364. return self::filterArray($value, $params, $field);
  365. }
  366. /**
  367. * HTML5 input: checkbox
  368. *
  369. * @param mixed $value Value to be validated.
  370. * @param array $params Validation parameters.
  371. * @param array $field Blueprint for the field.
  372. * @return bool True if validation succeeded.
  373. */
  374. public static function typeCheckbox($value, array $params, array $field)
  375. {
  376. $value = (string)$value;
  377. $field_value = (string)($field['value'] ?? '1');
  378. return $value === $field_value;
  379. }
  380. /**
  381. * HTML5 input: radio
  382. *
  383. * @param mixed $value Value to be validated.
  384. * @param array $params Validation parameters.
  385. * @param array $field Blueprint for the field.
  386. * @return bool True if validation succeeded.
  387. */
  388. public static function typeRadio($value, array $params, array $field)
  389. {
  390. return self::typeArray((array) $value, $params, $field);
  391. }
  392. /**
  393. * Custom input: toggle
  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 typeToggle($value, array $params, array $field)
  401. {
  402. if (is_bool($value)) {
  403. $value = (int)$value;
  404. }
  405. return self::typeArray((array) $value, $params, $field);
  406. }
  407. /**
  408. * Custom input: file
  409. *
  410. * @param mixed $value Value to be validated.
  411. * @param array $params Validation parameters.
  412. * @param array $field Blueprint for the field.
  413. * @return bool True if validation succeeded.
  414. */
  415. public static function typeFile($value, array $params, array $field)
  416. {
  417. return self::typeArray((array)$value, $params, $field);
  418. }
  419. /**
  420. * @param mixed $value
  421. * @param array $params
  422. * @param array $field
  423. * @return array
  424. */
  425. protected static function filterFile($value, array $params, array $field)
  426. {
  427. return (array)$value;
  428. }
  429. /**
  430. * HTML5 input: select
  431. *
  432. * @param mixed $value Value to be validated.
  433. * @param array $params Validation parameters.
  434. * @param array $field Blueprint for the field.
  435. * @return bool True if validation succeeded.
  436. */
  437. public static function typeSelect($value, array $params, array $field)
  438. {
  439. return self::typeArray((array) $value, $params, $field);
  440. }
  441. /**
  442. * HTML5 input: number
  443. *
  444. * @param mixed $value Value to be validated.
  445. * @param array $params Validation parameters.
  446. * @param array $field Blueprint for the field.
  447. * @return bool True if validation succeeded.
  448. */
  449. public static function typeNumber($value, array $params, array $field)
  450. {
  451. if (!is_numeric($value)) {
  452. return false;
  453. }
  454. $value = (float)$value;
  455. $min = 0;
  456. if (isset($params['min'])) {
  457. $min = (float)$params['min'];
  458. if ($value < $min) {
  459. return false;
  460. }
  461. }
  462. if (isset($params['max'])) {
  463. $max = (float)$params['max'];
  464. if ($value > $max) {
  465. return false;
  466. }
  467. }
  468. if (isset($params['step'])) {
  469. $step = (float)$params['step'];
  470. // Count of how many steps we are above/below the minimum value.
  471. $pos = ($value - $min) / $step;
  472. return is_int(static::filterNumber($pos, $params, $field));
  473. }
  474. return true;
  475. }
  476. /**
  477. * @param mixed $value
  478. * @param array $params
  479. * @param array $field
  480. * @return float|int
  481. */
  482. protected static function filterNumber($value, array $params, array $field)
  483. {
  484. return (string)(int)$value !== (string)(float)$value ? (float)$value : (int)$value;
  485. }
  486. /**
  487. * @param mixed $value
  488. * @param array $params
  489. * @param array $field
  490. * @return string
  491. */
  492. protected static function filterDateTime($value, array $params, array $field)
  493. {
  494. $format = Grav::instance()['config']->get('system.pages.dateformat.default');
  495. if ($format) {
  496. $converted = new DateTime($value);
  497. return $converted->format($format);
  498. }
  499. return $value;
  500. }
  501. /**
  502. * HTML5 input: range
  503. *
  504. * @param mixed $value Value to be validated.
  505. * @param array $params Validation parameters.
  506. * @param array $field Blueprint for the field.
  507. * @return bool True if validation succeeded.
  508. */
  509. public static function typeRange($value, array $params, array $field)
  510. {
  511. return self::typeNumber($value, $params, $field);
  512. }
  513. /**
  514. * @param mixed $value
  515. * @param array $params
  516. * @param array $field
  517. * @return float|int
  518. */
  519. protected static function filterRange($value, array $params, array $field)
  520. {
  521. return self::filterNumber($value, $params, $field);
  522. }
  523. /**
  524. * HTML5 input: color
  525. *
  526. * @param mixed $value Value to be validated.
  527. * @param array $params Validation parameters.
  528. * @param array $field Blueprint for the field.
  529. * @return bool True if validation succeeded.
  530. */
  531. public static function typeColor($value, array $params, array $field)
  532. {
  533. return (bool)preg_match('/^\#[0-9a-fA-F]{3}[0-9a-fA-F]{3}?$/u', $value);
  534. }
  535. /**
  536. * HTML5 input: email
  537. *
  538. * @param mixed $value Value to be validated.
  539. * @param array $params Validation parameters.
  540. * @param array $field Blueprint for the field.
  541. * @return bool True if validation succeeded.
  542. */
  543. public static function typeEmail($value, array $params, array $field)
  544. {
  545. if (!isset($params['max'])) {
  546. $params['max'] = 320;
  547. }
  548. $values = !is_array($value) ? explode(',', preg_replace('/\s+/', '', $value)) : $value;
  549. foreach ($values as $val) {
  550. if (!(self::typeText($val, $params, $field) && filter_var($val, FILTER_VALIDATE_EMAIL))) {
  551. return false;
  552. }
  553. }
  554. return true;
  555. }
  556. /**
  557. * HTML5 input: url
  558. *
  559. * @param mixed $value Value to be validated.
  560. * @param array $params Validation parameters.
  561. * @param array $field Blueprint for the field.
  562. * @return bool True if validation succeeded.
  563. */
  564. public static function typeUrl($value, array $params, array $field)
  565. {
  566. if (!isset($params['max'])) {
  567. $params['max'] = 2048;
  568. }
  569. return self::typeText($value, $params, $field) && filter_var($value, FILTER_VALIDATE_URL);
  570. }
  571. /**
  572. * HTML5 input: datetime
  573. *
  574. * @param mixed $value Value to be validated.
  575. * @param array $params Validation parameters.
  576. * @param array $field Blueprint for the field.
  577. * @return bool True if validation succeeded.
  578. */
  579. public static function typeDatetime($value, array $params, array $field)
  580. {
  581. if ($value instanceof DateTime) {
  582. return true;
  583. }
  584. if (!is_string($value)) {
  585. return false;
  586. }
  587. if (!isset($params['format'])) {
  588. return false !== strtotime($value);
  589. }
  590. $dateFromFormat = DateTime::createFromFormat($params['format'], $value);
  591. return $dateFromFormat && $value === date($params['format'], $dateFromFormat->getTimestamp());
  592. }
  593. /**
  594. * HTML5 input: datetime-local
  595. *
  596. * @param mixed $value Value to be validated.
  597. * @param array $params Validation parameters.
  598. * @param array $field Blueprint for the field.
  599. * @return bool True if validation succeeded.
  600. */
  601. public static function typeDatetimeLocal($value, array $params, array $field)
  602. {
  603. return self::typeDatetime($value, $params, $field);
  604. }
  605. /**
  606. * HTML5 input: date
  607. *
  608. * @param mixed $value Value to be validated.
  609. * @param array $params Validation parameters.
  610. * @param array $field Blueprint for the field.
  611. * @return bool True if validation succeeded.
  612. */
  613. public static function typeDate($value, array $params, array $field)
  614. {
  615. if (!isset($params['format'])) {
  616. $params['format'] = 'Y-m-d';
  617. }
  618. return self::typeDatetime($value, $params, $field);
  619. }
  620. /**
  621. * HTML5 input: time
  622. *
  623. * @param mixed $value Value to be validated.
  624. * @param array $params Validation parameters.
  625. * @param array $field Blueprint for the field.
  626. * @return bool True if validation succeeded.
  627. */
  628. public static function typeTime($value, array $params, array $field)
  629. {
  630. if (!isset($params['format'])) {
  631. $params['format'] = 'H:i';
  632. }
  633. return self::typeDatetime($value, $params, $field);
  634. }
  635. /**
  636. * HTML5 input: month
  637. *
  638. * @param mixed $value Value to be validated.
  639. * @param array $params Validation parameters.
  640. * @param array $field Blueprint for the field.
  641. * @return bool True if validation succeeded.
  642. */
  643. public static function typeMonth($value, array $params, array $field)
  644. {
  645. if (!isset($params['format'])) {
  646. $params['format'] = 'Y-m';
  647. }
  648. return self::typeDatetime($value, $params, $field);
  649. }
  650. /**
  651. * HTML5 input: week
  652. *
  653. * @param mixed $value Value to be validated.
  654. * @param array $params Validation parameters.
  655. * @param array $field Blueprint for the field.
  656. * @return bool True if validation succeeded.
  657. */
  658. public static function typeWeek($value, array $params, array $field)
  659. {
  660. if (!isset($params['format']) && !preg_match('/^\d{4}-W\d{2}$/u', $value)) {
  661. return false;
  662. }
  663. return self::typeDatetime($value, $params, $field);
  664. }
  665. /**
  666. * Custom input: array
  667. *
  668. * @param mixed $value Value to be validated.
  669. * @param array $params Validation parameters.
  670. * @param array $field Blueprint for the field.
  671. * @return bool True if validation succeeded.
  672. */
  673. public static function typeArray($value, array $params, array $field)
  674. {
  675. if (!is_array($value)) {
  676. return false;
  677. }
  678. if (isset($field['multiple'])) {
  679. if (isset($params['min']) && count($value) < $params['min']) {
  680. return false;
  681. }
  682. if (isset($params['max']) && count($value) > $params['max']) {
  683. return false;
  684. }
  685. $min = $params['min'] ?? 0;
  686. if (isset($params['step']) && (count($value) - $min) % $params['step'] === 0) {
  687. return false;
  688. }
  689. }
  690. // If creating new values is allowed, no further checks are needed.
  691. $validateOptions = $field['validate']['options'] ?? null;
  692. if (!empty($field['selectize']['create']) || $validateOptions === 'ignore') {
  693. return true;
  694. }
  695. $options = $field['options'] ?? [];
  696. $use = $field['use'] ?? 'values';
  697. if ($validateOptions) {
  698. // Use custom options structure.
  699. foreach ($options as &$option) {
  700. $option = $option[$validateOptions] ?? null;
  701. }
  702. unset($option);
  703. $options = array_values($options);
  704. } elseif (empty($field['selectize']) || empty($field['multiple'])) {
  705. $options = array_keys($options);
  706. }
  707. if ($use === 'keys') {
  708. $value = array_keys($value);
  709. }
  710. return !($options && array_diff($value, $options));
  711. }
  712. /**
  713. * @param mixed $value
  714. * @param array $params
  715. * @param array $field
  716. * @return array|null
  717. */
  718. protected static function filterFlatten_array($value, $params, $field)
  719. {
  720. $value = static::filterArray($value, $params, $field);
  721. return is_array($value) ? Utils::arrayUnflattenDotNotation($value) : null;
  722. }
  723. /**
  724. * @param mixed $value
  725. * @param array $params
  726. * @param array $field
  727. * @return array|null
  728. */
  729. protected static function filterArray($value, $params, $field)
  730. {
  731. $values = (array) $value;
  732. $options = isset($field['options']) ? array_keys($field['options']) : [];
  733. $multi = $field['multiple'] ?? false;
  734. if (count($values) === 1 && isset($values[0]) && $values[0] === '') {
  735. return null;
  736. }
  737. if ($options) {
  738. $useKey = isset($field['use']) && $field['use'] === 'keys';
  739. foreach ($values as $key => $val) {
  740. $values[$key] = $useKey ? (bool) $val : $val;
  741. }
  742. }
  743. if ($multi) {
  744. foreach ($values as $key => $val) {
  745. if (is_array($val)) {
  746. $val = implode(',', $val);
  747. $values[$key] = array_map('trim', explode(',', $val));
  748. } else {
  749. $values[$key] = trim($val);
  750. }
  751. }
  752. }
  753. $ignoreEmpty = isset($field['ignore_empty']) && Utils::isPositive($field['ignore_empty']);
  754. $valueType = $params['value_type'] ?? null;
  755. $keyType = $params['key_type'] ?? null;
  756. if ($ignoreEmpty || $valueType || $keyType) {
  757. $values = static::arrayFilterRecurse($values, ['value_type' => $valueType, 'key_type' => $keyType, 'ignore_empty' => $ignoreEmpty]);
  758. }
  759. return $values;
  760. }
  761. /**
  762. * @param array $values
  763. * @param array $params
  764. * @return array
  765. */
  766. protected static function arrayFilterRecurse(array $values, array $params): array
  767. {
  768. foreach ($values as $key => &$val) {
  769. if ($params['key_type']) {
  770. switch ($params['key_type']) {
  771. case 'int':
  772. $result = is_int($key);
  773. break;
  774. case 'string':
  775. $result = is_string($key);
  776. break;
  777. default:
  778. $result = false;
  779. }
  780. if (!$result) {
  781. unset($values[$key]);
  782. }
  783. }
  784. if (is_array($val)) {
  785. $val = static::arrayFilterRecurse($val, $params);
  786. if ($params['ignore_empty'] && empty($val)) {
  787. unset($values[$key]);
  788. }
  789. } else {
  790. if ($params['value_type'] && $val !== '' && $val !== null) {
  791. switch ($params['value_type']) {
  792. case 'bool':
  793. if (Utils::isPositive($val)) {
  794. $val = true;
  795. } elseif (Utils::isNegative($val)) {
  796. $val = false;
  797. } else {
  798. // Ignore invalid bool values.
  799. $val = null;
  800. }
  801. break;
  802. case 'int':
  803. $val = (int)$val;
  804. break;
  805. case 'float':
  806. $val = (float)$val;
  807. break;
  808. case 'string':
  809. $val = (string)$val;
  810. break;
  811. case 'trim':
  812. $val = trim($val);
  813. break;
  814. }
  815. }
  816. if ($params['ignore_empty'] && ($val === '' || $val === null)) {
  817. unset($values[$key]);
  818. }
  819. }
  820. }
  821. return $values;
  822. }
  823. /**
  824. * @param mixed $value
  825. * @param array $params
  826. * @param array $field
  827. * @return bool
  828. */
  829. public static function typeList($value, array $params, array $field)
  830. {
  831. if (!is_array($value)) {
  832. return false;
  833. }
  834. if (isset($field['fields'])) {
  835. foreach ($value as $key => $item) {
  836. foreach ($field['fields'] as $subKey => $subField) {
  837. $subKey = trim($subKey, '.');
  838. $subValue = $item[$subKey] ?? null;
  839. self::validate($subValue, $subField);
  840. }
  841. }
  842. }
  843. return true;
  844. }
  845. /**
  846. * @param mixed $value
  847. * @param array $params
  848. * @param array $field
  849. * @return array
  850. */
  851. protected static function filterList($value, array $params, array $field)
  852. {
  853. return (array) $value;
  854. }
  855. /**
  856. * @param mixed $value
  857. * @param array $params
  858. * @return array
  859. */
  860. public static function filterYaml($value, $params)
  861. {
  862. if (!is_string($value)) {
  863. return $value;
  864. }
  865. return (array) Yaml::parse($value);
  866. }
  867. /**
  868. * Custom input: ignore (will not validate)
  869. *
  870. * @param mixed $value Value to be validated.
  871. * @param array $params Validation parameters.
  872. * @param array $field Blueprint for the field.
  873. * @return bool True if validation succeeded.
  874. */
  875. public static function typeIgnore($value, array $params, array $field)
  876. {
  877. return true;
  878. }
  879. /**
  880. * @param mixed $value
  881. * @param array $params
  882. * @param array $field
  883. * @return mixed
  884. */
  885. public static function filterIgnore($value, array $params, array $field)
  886. {
  887. return $value;
  888. }
  889. /**
  890. * Input value which can be ignored.
  891. *
  892. * @param mixed $value Value to be validated.
  893. * @param array $params Validation parameters.
  894. * @param array $field Blueprint for the field.
  895. * @return bool True if validation succeeded.
  896. */
  897. public static function typeUnset($value, array $params, array $field)
  898. {
  899. return true;
  900. }
  901. /**
  902. * @param mixed $value
  903. * @param array $params
  904. * @param array $field
  905. * @return null
  906. */
  907. public static function filterUnset($value, array $params, array $field)
  908. {
  909. return null;
  910. }
  911. // HTML5 attributes (min, max and range are handled inside the types)
  912. /**
  913. * @param mixed $value
  914. * @param bool $params
  915. * @return bool
  916. */
  917. public static function validateRequired($value, $params)
  918. {
  919. if (is_scalar($value)) {
  920. return (bool) $params !== true || $value !== '';
  921. }
  922. return (bool) $params !== true || !empty($value);
  923. }
  924. /**
  925. * @param mixed $value
  926. * @param string $params
  927. * @return bool
  928. */
  929. public static function validatePattern($value, $params)
  930. {
  931. return (bool) preg_match("`^{$params}$`u", $value);
  932. }
  933. // Internal types
  934. /**
  935. * @param mixed $value
  936. * @param mixed $params
  937. * @return bool
  938. */
  939. public static function validateAlpha($value, $params)
  940. {
  941. return ctype_alpha($value);
  942. }
  943. /**
  944. * @param mixed $value
  945. * @param mixed $params
  946. * @return bool
  947. */
  948. public static function validateAlnum($value, $params)
  949. {
  950. return ctype_alnum($value);
  951. }
  952. /**
  953. * @param mixed $value
  954. * @param mixed $params
  955. * @return bool
  956. */
  957. public static function typeBool($value, $params)
  958. {
  959. return is_bool($value) || $value == 1 || $value == 0;
  960. }
  961. /**
  962. * @param mixed $value
  963. * @param mixed $params
  964. * @return bool
  965. */
  966. public static function validateBool($value, $params)
  967. {
  968. return is_bool($value) || $value == 1 || $value == 0;
  969. }
  970. /**
  971. * @param mixed $value
  972. * @param mixed $params
  973. * @return bool
  974. */
  975. protected static function filterBool($value, $params)
  976. {
  977. return (bool) $value;
  978. }
  979. /**
  980. * @param mixed $value
  981. * @param mixed $params
  982. * @return bool
  983. */
  984. public static function validateDigit($value, $params)
  985. {
  986. return ctype_digit($value);
  987. }
  988. /**
  989. * @param mixed $value
  990. * @param mixed $params
  991. * @return bool
  992. */
  993. public static function validateFloat($value, $params)
  994. {
  995. return is_float(filter_var($value, FILTER_VALIDATE_FLOAT));
  996. }
  997. /**
  998. * @param mixed $value
  999. * @param mixed $params
  1000. * @return float
  1001. */
  1002. protected static function filterFloat($value, $params)
  1003. {
  1004. return (float) $value;
  1005. }
  1006. /**
  1007. * @param mixed $value
  1008. * @param mixed $params
  1009. * @return bool
  1010. */
  1011. public static function validateHex($value, $params)
  1012. {
  1013. return ctype_xdigit($value);
  1014. }
  1015. /**
  1016. * Custom input: int
  1017. *
  1018. * @param mixed $value Value to be validated.
  1019. * @param array $params Validation parameters.
  1020. * @param array $field Blueprint for the field.
  1021. * @return bool True if validation succeeded.
  1022. */
  1023. public static function typeInt($value, array $params, array $field)
  1024. {
  1025. $params['step'] = max(1, (int)($params['step'] ?? 0));
  1026. return self::typeNumber($value, $params, $field);
  1027. }
  1028. /**
  1029. * @param mixed $value
  1030. * @param mixed $params
  1031. * @return bool
  1032. */
  1033. public static function validateInt($value, $params)
  1034. {
  1035. return is_numeric($value) && (int)$value == $value;
  1036. }
  1037. /**
  1038. * @param mixed $value
  1039. * @param mixed $params
  1040. * @return int
  1041. */
  1042. protected static function filterInt($value, $params)
  1043. {
  1044. return (int)$value;
  1045. }
  1046. /**
  1047. * @param mixed $value
  1048. * @param mixed $params
  1049. * @return bool
  1050. */
  1051. public static function validateArray($value, $params)
  1052. {
  1053. return is_array($value) || ($value instanceof ArrayAccess && $value instanceof Traversable && $value instanceof Countable);
  1054. }
  1055. /**
  1056. * @param mixed $value
  1057. * @param mixed $params
  1058. * @return array
  1059. */
  1060. public static function filterItem_List($value, $params)
  1061. {
  1062. return array_values(array_filter($value, static function ($v) {
  1063. return !empty($v);
  1064. }));
  1065. }
  1066. /**
  1067. * @param mixed $value
  1068. * @param mixed $params
  1069. * @return bool
  1070. */
  1071. public static function validateJson($value, $params)
  1072. {
  1073. return (bool) (@json_decode($value));
  1074. }
  1075. }