friendship ended with social-app. php is my new best friend
at main 11 kB view raw
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}