From 385c8ee43f7c70f475748a03e69b820ad6a456db Mon Sep 17 00:00:00 2001 From: upstream Date: Fri, 4 Sep 2026 09:22:41 +0200 Subject: [PATCH] Upstream nette/utils state of the touched files before dc5e00bdb1 --- src/Utils/Validators.php | 417 ++++++++++++++++++++++++++++++ tests/Utils/Validators.is().phpt | 421 +++++++++++++++++++++++++++++++ 2 files changed, 838 insertions(+) create mode 100644 src/Utils/Validators.php create mode 100644 tests/Utils/Validators.is().phpt diff --git a/src/Utils/Validators.php b/src/Utils/Validators.php new file mode 100644 index 0000000..b11d7e1 --- /dev/null +++ b/src/Utils/Validators.php @@ -0,0 +1,417 @@ + 1, 'int' => 1, 'float' => 1, 'bool' => 1, 'array' => 1, 'object' => 1, + 'callable' => 1, 'iterable' => 1, 'void' => 1, 'null' => 1, 'mixed' => 1, 'false' => 1, + 'never' => 1, 'true' => 1, + ]; + + /** @var array */ + protected static $validators = [ + // PHP types + 'array' => 'is_array', + 'bool' => 'is_bool', + 'boolean' => 'is_bool', + 'float' => 'is_float', + 'int' => 'is_int', + 'integer' => 'is_int', + 'null' => 'is_null', + 'object' => 'is_object', + 'resource' => 'is_resource', + 'scalar' => 'is_scalar', + 'string' => 'is_string', + + // pseudo-types + 'callable' => [self::class, 'isCallable'], + 'iterable' => 'is_iterable', + 'list' => [Arrays::class, 'isList'], + 'mixed' => [self::class, 'isMixed'], + 'none' => [self::class, 'isNone'], + 'number' => [self::class, 'isNumber'], + 'numeric' => [self::class, 'isNumeric'], + 'numericint' => [self::class, 'isNumericInt'], + + // string patterns + 'alnum' => 'ctype_alnum', + 'alpha' => 'ctype_alpha', + 'digit' => 'ctype_digit', + 'lower' => 'ctype_lower', + 'pattern' => null, + 'space' => 'ctype_space', + 'unicode' => [self::class, 'isUnicode'], + 'upper' => 'ctype_upper', + 'xdigit' => 'ctype_xdigit', + + // syntax validation + 'email' => [self::class, 'isEmail'], + 'identifier' => [self::class, 'isPhpIdentifier'], + 'uri' => [self::class, 'isUri'], + 'url' => [self::class, 'isUrl'], + + // environment validation + 'class' => 'class_exists', + 'interface' => 'interface_exists', + 'directory' => 'is_dir', + 'file' => 'is_file', + 'type' => [self::class, 'isType'], + ]; + + /** @var array */ + protected static $counters = [ + 'string' => 'strlen', + 'unicode' => [Strings::class, 'length'], + 'array' => 'count', + 'list' => 'count', + 'alnum' => 'strlen', + 'alpha' => 'strlen', + 'digit' => 'strlen', + 'lower' => 'strlen', + 'space' => 'strlen', + 'upper' => 'strlen', + 'xdigit' => 'strlen', + ]; + + + /** + * Verifies that the value is of expected types separated by pipe. + * @throws AssertionException + */ + public static function assert(mixed $value, string $expected, string $label = 'variable'): void + { + if (!static::is($value, $expected)) { + $expected = str_replace(['|', ':'], [' or ', ' in range '], $expected); + $translate = ['boolean' => 'bool', 'integer' => 'int', 'double' => 'float', 'NULL' => 'null']; + $type = $translate[gettype($value)] ?? gettype($value); + if (is_int($value) || is_float($value) || (is_string($value) && strlen($value) < 40)) { + $type .= ' ' . var_export($value, return: true); + } elseif (is_object($value)) { + $type .= ' ' . $value::class; + } + + throw new AssertionException("The $label expects to be $expected, $type given."); + } + } + + + /** + * Verifies that item $key in array exists and is of expected types separated by pipe. + * @param mixed[] $array + * @throws AssertionException + */ + public static function assertField( + array $array, + int|string $key, + ?string $expected = null, + string $label = "item '%' in array", + ): void + { + if (!array_key_exists($key, $array)) { + throw new AssertionException('Missing ' . str_replace('%', (string) $key, $label) . '.'); + + } elseif ($expected) { + static::assert($array[$key], $expected, str_replace('%', (string) $key, $label)); + } + } + + + /** + * Verifies that the value is of expected types separated by pipe. + */ + public static function is(mixed $value, string $expected): bool + { + foreach (explode('|', $expected) as $item) { + if (str_ends_with($item, '[]')) { + if (is_iterable($value) && self::everyIs($value, substr($item, 0, -2))) { + return true; + } + + continue; + } elseif (str_starts_with($item, '?')) { + $item = substr($item, 1); + if ($value === null) { + return true; + } + } + + [$type] = $item = explode(':', $item, 2); + if (isset(static::$validators[$type])) { + try { + if (!static::$validators[$type]($value)) { + continue; + } + } catch (\TypeError) { + continue; + } + } elseif ($type === 'pattern') { + if (Strings::match($value, '|^' . ($item[1] ?? '') . '$|D')) { + return true; + } + + continue; + } elseif (!$value instanceof $type) { + continue; + } + + if (isset($item[1])) { + $length = $value; + if (isset(static::$counters[$type])) { + $length = static::$counters[$type]($value); + } + + $range = explode('..', $item[1]); + if (!isset($range[1])) { + $range[1] = $range[0]; + } + + if (($range[0] !== '' && $length < $range[0]) || ($range[1] !== '' && $length > $range[1])) { + continue; + } + } + + return true; + } + + return false; + } + + + /** + * Finds whether all values are of expected types separated by pipe. + * @param iterable $values + */ + public static function everyIs(iterable $values, string $expected): bool + { + foreach ($values as $value) { + if (!static::is($value, $expected)) { + return false; + } + } + + return true; + } + + + /** + * Checks if the value is an integer or a float. + * @return ($value is int|float ? true : false) + */ + public static function isNumber(mixed $value): bool + { + return is_int($value) || is_float($value); + } + + + /** + * Checks if the value is an integer or a integer written in a string. + * @return ($value is non-empty-string ? bool : ($value is int ? true : false)) + */ + public static function isNumericInt(mixed $value): bool + { + return is_int($value) || (is_string($value) && preg_match('#^[+-]?[0-9]+$#D', $value)); + } + + + /** + * Checks if the value is a number or a number written in a string. + * @return ($value is non-empty-string ? bool : ($value is int|float ? true : false)) + */ + public static function isNumeric(mixed $value): bool + { + return is_float($value) || is_int($value) || (is_string($value) && preg_match('#^[+-]?([0-9]++\.?[0-9]*|\.[0-9]+)$#D', $value)); + } + + + /** + * Checks if the value is a syntactically correct callback. + */ + public static function isCallable(mixed $value): bool + { + return $value && is_callable($value, syntax_only: true); + } + + + /** + * Checks if the value is a valid UTF-8 string. + */ + public static function isUnicode(mixed $value): bool + { + return is_string($value) && preg_match('##u', $value); + } + + + /** + * Checks if the value is 0, '', false or null. + * @return ($value is 0|0.0|''|false|null ? true : false) + */ + public static function isNone(mixed $value): bool + { + return $value == null; // intentionally == + } + + + /** @internal */ + public static function isMixed(): bool + { + return true; + } + + + /** + * Checks if a variable is a zero-based integer indexed array. + * @deprecated use Nette\Utils\Arrays::isList + * @return ($value is list ? true : false) + */ + public static function isList(mixed $value): bool + { + return Arrays::isList($value); + } + + + /** + * Checks if the value is in the given range [min, max], where the upper or lower limit can be omitted (null). + * Numbers, strings and DateTime objects can be compared. + * @param array{int|float|string|\DateTimeInterface|null, int|float|string|\DateTimeInterface|null} $range + */ + public static function isInRange(mixed $value, array $range): bool + { + if ($value === null || !(isset($range[0]) || isset($range[1]))) { + return false; + } + + $limit = $range[0] ?? $range[1]; + if (is_string($limit)) { + $value = (string) $value; + } elseif ($limit instanceof \DateTimeInterface) { + if (!$value instanceof \DateTimeInterface) { + return false; + } + } elseif (is_numeric($value)) { + $value *= 1; + } else { + return false; + } + + return (!isset($range[0]) || ($value >= $range[0])) && (!isset($range[1]) || ($value <= $range[1])); + } + + + /** + * Checks if the value is a valid email address. It does not verify that the domain actually exists, only the syntax is verified. + */ + public static function isEmail(string $value): bool + { + $atom = "[-a-z0-9!#$%&'*+/=?^_`{|}~]"; // RFC 5322 unquoted characters in local-part + $alpha = "a-z\x80-\xFF"; // superset of IDN + return (bool) preg_match(<< \\? (? [a-zA-Z_\x7f-\xff][\w\x7f-\xff]*) (\\ (?&name))* ) | + (? (?&type) (& (?&type))+ ) | + (? (?&type) | \( (?&intersection) \) ) (\| (?&upart))+ + )$~xAD + XX, $type); + } +} diff --git a/tests/Utils/Validators.is().phpt b/tests/Utils/Validators.is().phpt new file mode 100644 index 0000000..c15eaf4 --- /dev/null +++ b/tests/Utils/Validators.is().phpt @@ -0,0 +1,421 @@ + 1, 2, 3], 'list')); + Assert::false(Validators::is([1 => 'a', 0 => 'b'], 'list')); + Assert::false(Validators::is(['key' => 'value'], 'list')); + $arr = []; + $arr[] = &$arr; + Assert::true(Validators::is($arr, 'list')); + Assert::false(Validators::is([1, 2, 3], 'list:4')); +}); + + +test('confirms value is an object instance', function () { + Assert::false(Validators::is(null, 'object')); + Assert::true(Validators::is(new stdClass, 'object')); +}); + + +test('checks that value is scalar', function () { + Assert::false(Validators::is(null, 'scalar')); + Assert::false(Validators::is([], 'scalar')); + Assert::true(Validators::is(1, 'scalar')); +}); + + +test('validates callable type including array callables', function () { + Assert::false(Validators::is(null, 'callable')); + Assert::false(Validators::is([], 'callable')); + Assert::false(Validators::is(1, 'callable')); + Assert::false(Validators::is('', 'callable')); + Assert::true(Validators::is('hello', 'callable')); + Assert::false(Validators::is(['hello'], 'callable')); + Assert::true(Validators::is(['hello', 'world'], 'callable')); +}); + + +test('ensures value is null', function () { + Assert::false(Validators::is(0, 'null')); + Assert::true(Validators::is(null, 'null')); +}); + + +test('accepts any value for mixed type', function () { + Assert::true(Validators::is([], 'mixed')); + Assert::true(Validators::is(null, 'mixed')); +}); + + +test('validates email format with domain restrictions', function () { + Assert::false(Validators::is('', 'email')); + Assert::false(Validators::is(false, 'email')); + Assert::false(Validators::is('hello', 'email')); + Assert::true(Validators::is('hello@world.cz', 'email')); + Assert::false(Validators::is('hello@localhost', 'email')); + Assert::false(Validators::is('hello@127.0.0.1', 'email')); + Assert::false(Validators::is('hello@localhost.a0', 'email')); + Assert::false(Validators::is('hello@localhost.0a', 'email')); + Assert::true(Validators::is('hello@l.org', 'email')); + Assert::true(Validators::is('hello@1.org', 'email')); + Assert::false(Validators::is('jean.françois@lyotard.fr', 'email')); + Assert::true(Validators::is('jerzy@kosiński.pl', 'email')); + Assert::false(Validators::is('péter@esterházy.hu', 'email')); + Assert::true(Validators::is('hello@1.c0m', 'email')); + Assert::true(Validators::is('hello@1.c', 'email')); +}); + + +test('verifies URL format across various domain cases', function () { + Assert::false(Validators::is('', 'url')); + Assert::false(Validators::is(false, 'url')); + Assert::false(Validators::is('hello', 'url')); + Assert::false(Validators::is('nette.org', 'url')); + Assert::false(Validators::is('http://nette.org0', 'url')); + Assert::false(Validators::is('http://nette.0org', 'url')); + Assert::false(Validators::is('http://_nette.org', 'url')); + Assert::false(Validators::is('http://www._nette.org', 'url')); + Assert::false(Validators::is('http://www.ne_tte.org', 'url')); + Assert::true(Validators::is('http://1.org', 'url')); + Assert::true(Validators::is('http://l.org', 'url')); + Assert::true(Validators::is('http://localhost', 'url')); + Assert::true(Validators::is('http://127.0.0.1', 'url')); + Assert::true(Validators::is('http://255.255.255.255', 'url')); + Assert::false(Validators::is('http://999.999.999.999', 'url')); + Assert::false(Validators::is('http://256.1.1.1', 'url')); + Assert::false(Validators::is('http://1.2.3', 'url')); + Assert::true(Validators::is('http://[::1]', 'url')); + Assert::true(Validators::is('http://[2001:0db8:0000:0000:0000:0000:1428:57AB]', 'url')); + Assert::true(Validators::is('http://nette.org/path', 'url')); + Assert::true(Validators::is('http://nette.org:8080/path', 'url')); + Assert::true(Validators::is('https://www.nette.org/path', 'url')); + Assert::true(Validators::is('https://www.nette.org/path?query#fragment', 'url')); + Assert::true(Validators::is('https://www.nette.org?query', 'url')); + Assert::true(Validators::is('https://www.nette.org#fragment', 'url')); + Assert::true(Validators::is('https://www.nette.org?#', 'url')); + Assert::true(Validators::is('https://example.c0m', 'url')); + Assert::true(Validators::is('https://example.l', 'url')); + Assert::true(Validators::is('http://one_two.example.com', 'url')); + Assert::true(Validators::is('http://_.example.com', 'url')); + Assert::true(Validators::is('http://_e_.example.com', 'url')); +}); + + +test('checks URI format with proper scheme requirements', function () { + Assert::false(Validators::is('', 'uri')); + Assert::false(Validators::is(false, 'uri')); + Assert::false(Validators::is('hello', 'uri')); + Assert::false(Validators::is('nette.org', 'uri')); + Assert::false(Validators::is('mailto: gandalf@example.org', 'uri')); + Assert::false(Validators::is('invalid-scheme :gandalf@example.org', 'uri')); + Assert::false(Validators::is('invalid-scheme~:gandalf@example.org', 'uri')); + Assert::true(Validators::is('mailto:gandalf@example.org', 'uri')); + Assert::true(Validators::is('valid-scheme+.0:lalala', 'uri')); + Assert::true(Validators::is('bitcoin:mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn', 'uri')); +}); + + +test('accepts empty and null values for none type', function () { + Assert::true(Validators::is(0, 'none')); + Assert::true(Validators::is('', 'none')); + Assert::true(Validators::is(null, 'none')); + Assert::true(Validators::is(false, 'none')); + Assert::false(Validators::is('0', 'none')); + Assert::true(Validators::is([], 'none')); +}); + + +test('validates string against a regular expression pattern', function () { + Assert::true(Validators::is('', 'pattern')); + Assert::true(Validators::is(' 123', 'pattern:\s+\d+')); + Assert::false(Validators::is(' 123x', 'pattern:\s+\d+')); +}); + + +test('ensures alphanumeric string meets minimum length', function () { + Assert::false(Validators::is('', 'alnum')); + Assert::false(Validators::is('a-1', 'alnum')); + Assert::true(Validators::is('a1', 'alnum')); + Assert::true(Validators::is('a1', 'alnum:2')); +}); + + +test('checks alphabetic string within a specified length range', function () { + Assert::false(Validators::is('', 'alpha')); + Assert::false(Validators::is('a1', 'alpha')); + Assert::true(Validators::is('aA', 'alpha')); + Assert::true(Validators::is('aA', 'alpha:1..3')); +}); + + +test('validates digit-only string with length limitations', function () { + Assert::false(Validators::is('', 'digit')); + Assert::false(Validators::is('123x', 'digit')); + Assert::true(Validators::is('123', 'digit')); + Assert::false(Validators::is('123', 'digit:..2')); +}); + + +test('ensures string is entirely lowercase with fixed length', function () { + Assert::false(Validators::is('', 'lower')); + Assert::false(Validators::is('Hello', 'lower')); + Assert::true(Validators::is('hello', 'lower')); + Assert::false(Validators::is('hello', 'lower:9')); +}); + + +test('confirms string is entirely uppercase', function () { + Assert::false(Validators::is('', 'upper')); + Assert::false(Validators::is('Hello', 'upper')); + Assert::true(Validators::is('HELLO', 'upper')); +}); + + +test('validates that string contains only whitespace', function () { + Assert::false(Validators::is('', 'space')); + Assert::false(Validators::is(' 1', 'space')); + Assert::true(Validators::is(" \t\r\n", 'space')); +}); + + +test('checks that string consists solely of hexadecimal digits', function () { + Assert::false(Validators::is('', 'xdigit')); + Assert::false(Validators::is('123x', 'xdigit')); + Assert::true(Validators::is('123aA', 'xdigit')); +}); + + +test('validates union type allowing int or float values', function () { + Assert::true(Validators::is(1.0, 'int|float')); + Assert::true(Validators::is(1, 'int|float')); + Assert::false(Validators::is('1', 'int|float')); +}); + + +test('verifies existence of a type by its name', function () { + class rimmer + { + } + interface kryton + { + } + + Assert::true(Validators::is('rimmer', 'type')); + Assert::true(Validators::is('kryton', 'type')); + Assert::false(Validators::is('1', 'type')); +}); + + +test('checks for the existence of a class by name', function () { + Assert::true(Validators::is('rimmer', 'class')); + Assert::false(Validators::is('kryton', 'class')); + Assert::false(Validators::is('1', 'class')); +}); + + +test('determines if an interface exists by its name', function () { + Assert::false(Validators::is('rimmer', 'interface')); + Assert::true(Validators::is('kryton', 'interface')); + Assert::false(Validators::is('1', 'interface')); +}); + + +test('validates file existence based on file type', function () { + Assert::true(Validators::is(__FILE__, 'file')); + Assert::false(Validators::is(__FILE__ . 'xx', 'class')); +}); + + +test('confirms directory existence', function () { + Assert::true(Validators::is(__DIR__, 'directory')); + Assert::false(Validators::is(__DIR__ . 'xx', 'directory')); +}); + + +test('ensures identifier follows naming conventions', function () { + Assert::true(Validators::is('Item', 'identifier')); + Assert::false(Validators::is('0Item', 'identifier')); +}); + +test('validates optional string or email format', function () { + Assert::true(Validators::is('', 'string:0|email')); + Assert::true(Validators::is('foo@bar.com', 'string:0|email')); + Assert::false(Validators::is('foo', 'string:0|email')); +}); + + +test('checks that value is iterable from arrays, iterators, or generators', function () { + $gen = function () { + yield; + }; + + Assert::true(Validators::is([1, 2, 3], 'iterable')); + Assert::true(Validators::is(new ArrayIterator([1, 2, 3]), 'iterable')); + Assert::true(Validators::is($gen(), 'iterable')); + Assert::false(Validators::is(1, 'iterable')); + Assert::false(Validators::is(3.14, 'iterable')); + Assert::false(Validators::is(new stdClass, 'iterable')); +}); + + +test('validates multi-dimensional arrays against element type rules', function () { + class Abc + { + } + + Assert::true(Validators::is([], 'int[]')); + Assert::true(Validators::is(new ArrayIterator([]), 'int[]')); + Assert::false(Validators::is(1, 'int[]')); + Assert::false(Validators::is(2.15, 'int[]')); + Assert::true(Validators::is(2.15, 'float|int[]')); + Assert::true(Validators::is(2.15, 'int[]|float')); + Assert::true(Validators::is([1, 2, 3], 'int[]')); + Assert::false(Validators::is([1, 2, 3], 'int[][]')); + Assert::true(Validators::is([[1], [2, 3]], 'int[][]')); + Assert::false(Validators::is([1, 2.15, 3], 'int[]')); + Assert::true(Validators::is([1, 2.15, 3], 'number[]')); + + Assert::true(Validators::is([new Abc], 'Abc[]')); + Assert::false(Validators::is([new Abc, new stdClass], 'Abc[]')); + + Assert::true(Validators::is(['ABCD', 'EFGH', 'IJKL'], 'string:4[]')); + Assert::false(Validators::is(['ABCD', 'EFGH', 'IJKLM'], 'string:4[]')); + + Assert::true(Validators::is([['ABCD', 'EFGH'], ['IJKL']], 'string:4[][]')); + Assert::false(Validators::is([['ABCD', 'EFGH'], ['IJKLM']], 'string:4[][]')); +}); + + +test('checks nullable string type acceptance', function () { + Assert::true(Validators::is(null, '?string')); + Assert::true(Validators::is('1', '?string')); + Assert::false(Validators::is(true, '?int')); + Assert::false(Validators::is(0, '?string')); +});