Validators.php 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. <?php
  2. /**
  3. * This file is part of the Nette Framework (https://nette.org)
  4. * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
  5. */
  6. declare(strict_types=1);
  7. namespace Nette\Utils;
  8. use Nette;
  9. /**
  10. * Validation utilities.
  11. */
  12. class Validators
  13. {
  14. use Nette\StaticClass;
  15. /** @var array<string,?callable> */
  16. protected static $validators = [
  17. // PHP types
  18. 'array' => 'is_array',
  19. 'bool' => 'is_bool',
  20. 'boolean' => 'is_bool',
  21. 'float' => 'is_float',
  22. 'int' => 'is_int',
  23. 'integer' => 'is_int',
  24. 'null' => 'is_null',
  25. 'object' => 'is_object',
  26. 'resource' => 'is_resource',
  27. 'scalar' => 'is_scalar',
  28. 'string' => 'is_string',
  29. // pseudo-types
  30. 'callable' => [self::class, 'isCallable'],
  31. 'iterable' => 'is_iterable',
  32. 'list' => [Arrays::class, 'isList'],
  33. 'mixed' => [self::class, 'isMixed'],
  34. 'none' => [self::class, 'isNone'],
  35. 'number' => [self::class, 'isNumber'],
  36. 'numeric' => [self::class, 'isNumeric'],
  37. 'numericint' => [self::class, 'isNumericInt'],
  38. // string patterns
  39. 'alnum' => 'ctype_alnum',
  40. 'alpha' => 'ctype_alpha',
  41. 'digit' => 'ctype_digit',
  42. 'lower' => 'ctype_lower',
  43. 'pattern' => null,
  44. 'space' => 'ctype_space',
  45. 'unicode' => [self::class, 'isUnicode'],
  46. 'upper' => 'ctype_upper',
  47. 'xdigit' => 'ctype_xdigit',
  48. // syntax validation
  49. 'email' => [self::class, 'isEmail'],
  50. 'identifier' => [self::class, 'isPhpIdentifier'],
  51. 'uri' => [self::class, 'isUri'],
  52. 'url' => [self::class, 'isUrl'],
  53. // environment validation
  54. 'class' => 'class_exists',
  55. 'interface' => 'interface_exists',
  56. 'directory' => 'is_dir',
  57. 'file' => 'is_file',
  58. 'type' => [self::class, 'isType'],
  59. ];
  60. /** @var array<string,callable> */
  61. protected static $counters = [
  62. 'string' => 'strlen',
  63. 'unicode' => [Strings::class, 'length'],
  64. 'array' => 'count',
  65. 'list' => 'count',
  66. 'alnum' => 'strlen',
  67. 'alpha' => 'strlen',
  68. 'digit' => 'strlen',
  69. 'lower' => 'strlen',
  70. 'space' => 'strlen',
  71. 'upper' => 'strlen',
  72. 'xdigit' => 'strlen',
  73. ];
  74. /**
  75. * Verifies that the value is of expected types separated by pipe.
  76. * @param mixed $value
  77. * @throws AssertionException
  78. */
  79. public static function assert($value, string $expected, string $label = 'variable'): void
  80. {
  81. if (!static::is($value, $expected)) {
  82. $expected = str_replace(['|', ':'], [' or ', ' in range '], $expected);
  83. static $translate = ['boolean' => 'bool', 'integer' => 'int', 'double' => 'float', 'NULL' => 'null'];
  84. $type = $translate[gettype($value)] ?? gettype($value);
  85. if (is_int($value) || is_float($value) || (is_string($value) && strlen($value) < 40)) {
  86. $type .= ' ' . var_export($value, true);
  87. } elseif (is_object($value)) {
  88. $type .= ' ' . get_class($value);
  89. }
  90. throw new AssertionException("The $label expects to be $expected, $type given.");
  91. }
  92. }
  93. /**
  94. * Verifies that element $key in array is of expected types separated by pipe.
  95. * @param mixed[] $array
  96. * @param int|string $key
  97. * @throws AssertionException
  98. */
  99. public static function assertField(
  100. array $array,
  101. $key,
  102. ?string $expected = null,
  103. string $label = "item '%' in array"
  104. ): void {
  105. if (!array_key_exists($key, $array)) {
  106. throw new AssertionException('Missing ' . str_replace('%', $key, $label) . '.');
  107. } elseif ($expected) {
  108. static::assert($array[$key], $expected, str_replace('%', $key, $label));
  109. }
  110. }
  111. /**
  112. * Verifies that the value is of expected types separated by pipe.
  113. * @param mixed $value
  114. */
  115. public static function is($value, string $expected): bool
  116. {
  117. foreach (explode('|', $expected) as $item) {
  118. if (substr($item, -2) === '[]') {
  119. if (is_iterable($value) && self::everyIs($value, substr($item, 0, -2))) {
  120. return true;
  121. }
  122. continue;
  123. } elseif (substr($item, 0, 1) === '?') {
  124. $item = substr($item, 1);
  125. if ($value === null) {
  126. return true;
  127. }
  128. }
  129. [$type] = $item = explode(':', $item, 2);
  130. if (isset(static::$validators[$type])) {
  131. try {
  132. if (!static::$validators[$type]($value)) {
  133. continue;
  134. }
  135. } catch (\TypeError $e) {
  136. continue;
  137. }
  138. } elseif ($type === 'pattern') {
  139. if (Strings::match($value, '|^' . ($item[1] ?? '') . '$|D')) {
  140. return true;
  141. }
  142. continue;
  143. } elseif (!$value instanceof $type) {
  144. continue;
  145. }
  146. if (isset($item[1])) {
  147. $length = $value;
  148. if (isset(static::$counters[$type])) {
  149. $length = static::$counters[$type]($value);
  150. }
  151. $range = explode('..', $item[1]);
  152. if (!isset($range[1])) {
  153. $range[1] = $range[0];
  154. }
  155. if (($range[0] !== '' && $length < $range[0]) || ($range[1] !== '' && $length > $range[1])) {
  156. continue;
  157. }
  158. }
  159. return true;
  160. }
  161. return false;
  162. }
  163. /**
  164. * Finds whether all values are of expected types separated by pipe.
  165. * @param mixed[] $values
  166. */
  167. public static function everyIs(iterable $values, string $expected): bool
  168. {
  169. foreach ($values as $value) {
  170. if (!static::is($value, $expected)) {
  171. return false;
  172. }
  173. }
  174. return true;
  175. }
  176. /**
  177. * Checks if the value is an integer or a float.
  178. * @param mixed $value
  179. */
  180. public static function isNumber($value): bool
  181. {
  182. return is_int($value) || is_float($value);
  183. }
  184. /**
  185. * Checks if the value is an integer or a integer written in a string.
  186. * @param mixed $value
  187. */
  188. public static function isNumericInt($value): bool
  189. {
  190. return is_int($value) || (is_string($value) && preg_match('#^[+-]?[0-9]+$#D', $value));
  191. }
  192. /**
  193. * Checks if the value is a number or a number written in a string.
  194. * @param mixed $value
  195. */
  196. public static function isNumeric($value): bool
  197. {
  198. return is_float($value) || is_int($value) || (is_string($value) && preg_match('#^[+-]?([0-9]++\.?[0-9]*|\.[0-9]+)$#D', $value));
  199. }
  200. /**
  201. * Checks if the value is a syntactically correct callback.
  202. * @param mixed $value
  203. */
  204. public static function isCallable($value): bool
  205. {
  206. return $value && is_callable($value, true);
  207. }
  208. /**
  209. * Checks if the value is a valid UTF-8 string.
  210. * @param mixed $value
  211. */
  212. public static function isUnicode($value): bool
  213. {
  214. return is_string($value) && preg_match('##u', $value);
  215. }
  216. /**
  217. * Checks if the value is 0, '', false or null.
  218. * @param mixed $value
  219. */
  220. public static function isNone($value): bool
  221. {
  222. return $value == null; // intentionally ==
  223. }
  224. /** @internal */
  225. public static function isMixed(): bool
  226. {
  227. return true;
  228. }
  229. /**
  230. * Checks if a variable is a zero-based integer indexed array.
  231. * @param mixed $value
  232. * @deprecated use Nette\Utils\Arrays::isList
  233. */
  234. public static function isList($value): bool
  235. {
  236. return Arrays::isList($value);
  237. }
  238. /**
  239. * Checks if the value is in the given range [min, max], where the upper or lower limit can be omitted (null).
  240. * Numbers, strings and DateTime objects can be compared.
  241. * @param mixed $value
  242. */
  243. public static function isInRange($value, array $range): bool
  244. {
  245. if ($value === null || !(isset($range[0]) || isset($range[1]))) {
  246. return false;
  247. }
  248. $limit = $range[0] ?? $range[1];
  249. if (is_string($limit)) {
  250. $value = (string) $value;
  251. } elseif ($limit instanceof \DateTimeInterface) {
  252. if (!$value instanceof \DateTimeInterface) {
  253. return false;
  254. }
  255. } elseif (is_numeric($value)) {
  256. $value *= 1;
  257. } else {
  258. return false;
  259. }
  260. return (!isset($range[0]) || ($value >= $range[0])) && (!isset($range[1]) || ($value <= $range[1]));
  261. }
  262. /**
  263. * Checks if the value is a valid email address. It does not verify that the domain actually exists, only the syntax is verified.
  264. */
  265. public static function isEmail(string $value): bool
  266. {
  267. $atom = "[-a-z0-9!#$%&'*+/=?^_`{|}~]"; // RFC 5322 unquoted characters in local-part
  268. $alpha = "a-z\x80-\xFF"; // superset of IDN
  269. return (bool) preg_match(<<<XX
  270. (^
  271. ("([ !#-[\\]-~]*|\\\\[ -~])+"|$atom+(\\.$atom+)*) # quoted or unquoted
  272. @
  273. ([0-9$alpha]([-0-9$alpha]{0,61}[0-9$alpha])?\\.)+ # domain - RFC 1034
  274. [$alpha]([-0-9$alpha]{0,17}[$alpha])? # top domain
  275. $)Dix
  276. XX
  277. , $value);
  278. }
  279. /**
  280. * Checks if the value is a valid URL address.
  281. */
  282. public static function isUrl(string $value): bool
  283. {
  284. $alpha = "a-z\x80-\xFF";
  285. return (bool) preg_match(<<<XX
  286. (^
  287. https?://(
  288. (([-_0-9$alpha]+\\.)* # subdomain
  289. [0-9$alpha]([-0-9$alpha]{0,61}[0-9$alpha])?\\.)? # domain
  290. [$alpha]([-0-9$alpha]{0,17}[$alpha])? # top domain
  291. |\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3} # IPv4
  292. |\\[[0-9a-f:]{3,39}\\] # IPv6
  293. )(:\\d{1,5})? # port
  294. (/\\S*)? # path
  295. (\\?\\S*)? # query
  296. (\\#\\S*)? # fragment
  297. $)Dix
  298. XX
  299. , $value);
  300. }
  301. /**
  302. * Checks if the value is a valid URI address, that is, actually a string beginning with a syntactically valid schema.
  303. */
  304. public static function isUri(string $value): bool
  305. {
  306. return (bool) preg_match('#^[a-z\d+\.-]+:\S+$#Di', $value);
  307. }
  308. /**
  309. * Checks whether the input is a class, interface or trait.
  310. */
  311. public static function isType(string $type): bool
  312. {
  313. return class_exists($type) || interface_exists($type) || trait_exists($type);
  314. }
  315. /**
  316. * Checks whether the input is a valid PHP identifier.
  317. */
  318. public static function isPhpIdentifier(string $value): bool
  319. {
  320. return preg_match('#^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$#D', $value) === 1;
  321. }
  322. }