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