diff --git a/ext/filter/filter.c b/ext/filter/filter.c index 03f0f7887f924..056ad95b750cf 100644 --- a/ext/filter/filter.c +++ b/ext/filter/filter.c @@ -50,6 +50,7 @@ static const filter_list_entry filter_list[] = { { "validate_email", FILTER_VALIDATE_EMAIL, php_filter_validate_email }, { "validate_ip", FILTER_VALIDATE_IP, php_filter_validate_ip }, { "validate_mac", FILTER_VALIDATE_MAC, php_filter_validate_mac }, + { "validate_strlen", FILTER_VALIDATE_STRLEN, php_filter_validate_strlen }, { "string", FILTER_SANITIZE_STRING, php_filter_string }, { "stripped", FILTER_SANITIZE_STRING, php_filter_string }, diff --git a/ext/filter/filter.stub.php b/ext/filter/filter.stub.php index 4332f9261e982..ec8cf135e9409 100644 --- a/ext/filter/filter.stub.php +++ b/ext/filter/filter.stub.php @@ -112,6 +112,11 @@ * @cvalue FILTER_VALIDATE_MAC */ const FILTER_VALIDATE_MAC = UNKNOWN; +/** + * @var int + * @cvalue FILTER_VALIDATE_STRLEN + */ +const FILTER_VALIDATE_STRLEN = UNKNOWN; /** * @var int diff --git a/ext/filter/filter_arginfo.h b/ext/filter/filter_arginfo.h index 4e24ede41a633..f638fef91e71f 100644 --- a/ext/filter/filter_arginfo.h +++ b/ext/filter/filter_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit filter.stub.php instead. - * Stub hash: c3eb55dfec619af1e46be206f51a2b0893ed399f */ + * Stub hash: 50101ce180f08678a6d921a5d6dcb2fc94e97a17 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_filter_has_var, 0, 2, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO(0, input_type, IS_LONG, 0) @@ -80,6 +80,7 @@ static void register_filter_symbols(int module_number) REGISTER_LONG_CONSTANT("FILTER_VALIDATE_EMAIL", FILTER_VALIDATE_EMAIL, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("FILTER_VALIDATE_IP", FILTER_VALIDATE_IP, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("FILTER_VALIDATE_MAC", FILTER_VALIDATE_MAC, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("FILTER_VALIDATE_STRLEN", FILTER_VALIDATE_STRLEN, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("FILTER_DEFAULT", FILTER_DEFAULT, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("FILTER_UNSAFE_RAW", FILTER_UNSAFE_RAW, CONST_PERSISTENT); zend_constant *const_FILTER_SANITIZE_STRING = REGISTER_LONG_CONSTANT("FILTER_SANITIZE_STRING", FILTER_SANITIZE_STRING, CONST_PERSISTENT | CONST_DEPRECATED); diff --git a/ext/filter/filter_private.h b/ext/filter/filter_private.h index 709b7fbc45edc..dac410bb27c24 100644 --- a/ext/filter/filter_private.h +++ b/ext/filter/filter_private.h @@ -67,7 +67,8 @@ #define FILTER_VALIDATE_IP 0x0113 #define FILTER_VALIDATE_MAC 0x0114 #define FILTER_VALIDATE_DOMAIN 0x0115 -#define FILTER_VALIDATE_LAST 0x0115 +#define FILTER_VALIDATE_STRLEN 0x0116 +#define FILTER_VALIDATE_LAST 0x0116 #define FILTER_VALIDATE_ALL 0x0100 diff --git a/ext/filter/logical_filters.c b/ext/filter/logical_filters.c index 20760e656e763..474dd99acac52 100644 --- a/ext/filter/logical_filters.c +++ b/ext/filter/logical_filters.c @@ -21,6 +21,7 @@ #include "filter_private.h" #include "ext/pcre/php_pcre.h" #include "ext/uri/php_uri.h" +#include "ext/standard/html.h" #include "zend_multiply.h" @@ -32,6 +33,14 @@ # define INADDR_NONE ((unsigned long int) -1) #endif +#ifdef HAVE_ARPA_INET_H +# include +#endif + +#ifndef INADDR_NONE +# define INADDR_NONE ((unsigned long int) -1) +#endif + /* {{{ FETCH_DOUBLE_OPTION(var_name, option_name) */ #define FETCH_DOUBLE_OPTION(var_name, option_name) \ @@ -1115,3 +1124,63 @@ zend_result php_filter_validate_mac(PHP_INPUT_FILTER_PARAM_DECL) /* {{{ */ return SUCCESS; } /* }}} */ + +/** + * Returns the number of Unicode code points in a UTF-8 encoded string. + * Invalid UTF-8 byte sequences (U+FFFD) are counted as one replacement character. + */ +static size_t php_utf8_strlen(const unsigned char *str, size_t str_len) +{ + size_t len = 0, cursor = 0; + zend_result status; + + while (cursor < str_len) { + php_next_utf8_char(str, str_len, &cursor, &status); + len++; + } + + return len; +} + + +zend_result php_filter_validate_strlen(PHP_INPUT_FILTER_PARAM_DECL) /* {{{ */ +{ + int min_len_set, max_len_set; + zval *option_val; + zend_long min_len, max_len; + size_t len; + const char *str = Z_STRVAL_P(value); + size_t str_size = Z_STRLEN_P(value); + + FETCH_LONG_OPTION(min_len, "min_len"); + FETCH_LONG_OPTION(max_len, "max_len"); + + if (min_len_set && min_len < 0) { + php_error_docref(NULL, E_WARNING, + "min_len must be greater than or equal to 0"); + RETURN_VALIDATION_FAILED; + } + + if (max_len_set && max_len < 0) { + php_error_docref(NULL, E_WARNING, + "max_len must be greater than or equal to 0"); + RETURN_VALIDATION_FAILED; + } + + if (min_len_set && max_len_set && min_len > max_len) { + php_error_docref(NULL, E_WARNING, + "min_len must be less than or equal to max_len"); + RETURN_VALIDATION_FAILED; + } + + len = php_utf8_strlen((const unsigned char *)str, str_size); + + if (min_len_set && len < min_len) { + RETURN_VALIDATION_FAILED; + } + if (max_len_set && max_len < len) { + RETURN_VALIDATION_FAILED; + } + return SUCCESS; +} +/* }}} */ \ No newline at end of file diff --git a/ext/filter/php_filter.h b/ext/filter/php_filter.h index 48ad5cc07943e..e5035be2abefb 100644 --- a/ext/filter/php_filter.h +++ b/ext/filter/php_filter.h @@ -62,6 +62,7 @@ zend_result php_filter_validate_url(PHP_INPUT_FILTER_PARAM_DECL); zend_result php_filter_validate_email(PHP_INPUT_FILTER_PARAM_DECL); zend_result php_filter_validate_ip(PHP_INPUT_FILTER_PARAM_DECL); zend_result php_filter_validate_mac(PHP_INPUT_FILTER_PARAM_DECL); +zend_result php_filter_validate_strlen(PHP_INPUT_FILTER_PARAM_DECL); zend_result php_filter_string(PHP_INPUT_FILTER_PARAM_DECL); zend_result php_filter_encoded(PHP_INPUT_FILTER_PARAM_DECL); diff --git a/ext/filter/tests/validate_str_basic.phpt b/ext/filter/tests/validate_str_basic.phpt new file mode 100644 index 0000000000000..4b5230113ebf9 --- /dev/null +++ b/ext/filter/tests/validate_str_basic.phpt @@ -0,0 +1,26 @@ +--TEST-- +FILTER_VALIDATE_STR: Emoji string length +--SKIPIF-- + +--FILE-- + ['min_len' => 2, 'max_len' => 2]]; +$options2 = ['options' => ['max_len' => 2, 'default' => 'error']]; +$options3 = ['options' => ['min_len' => 0]]; + +var_dump( + filter_var('ab', filter_id('validate_strlen'), $options1), + filter_var('🐘🐘', FILTER_VALIDATE_STRLEN, $options1), + filter_var('🐘', FILTER_VALIDATE_STRLEN, $options1), + filter_var('🐘🐘🐘', FILTER_VALIDATE_STRLEN, $options1), + filter_var('🐘🐘🐘', FILTER_VALIDATE_STRLEN, $options2), + filter_var('', FILTER_VALIDATE_STRLEN, $options3), +); +?> +--EXPECT-- +string(2) "ab" +string(8) "🐘🐘" +bool(false) +bool(false) +string(5) "error" +string(0) "" diff --git a/ext/filter/tests/validate_str_invalid_options.phpt b/ext/filter/tests/validate_str_invalid_options.phpt new file mode 100644 index 0000000000000..098f8d0509eb1 --- /dev/null +++ b/ext/filter/tests/validate_str_invalid_options.phpt @@ -0,0 +1,37 @@ +--TEST-- +FILTER_VALIDATE_STR: invalid min_len/max_len options +--FILE-- + ["min_len" => -1] +])); + +echo "--- max_len negative ---\n"; +var_dump(filter_var("abc", FILTER_VALIDATE_STRLEN, [ + "options" => ["max_len" => -1] +])); + +echo "--- min_len greater than max_len ---\n"; +var_dump(filter_var("abc", FILTER_VALIDATE_STRLEN, [ + "options" => [ + "min_len" => 10, + "max_len" => 5 + ] +])); + +?> +--EXPECTF-- +--- min_len negative --- + +Warning: filter_var(): min_len must be greater than or equal to 0 in %s on line %d +bool(false) +--- max_len negative --- + +Warning: filter_var(): max_len must be greater than or equal to 0 in %s on line %d +bool(false) +--- min_len greater than max_len --- + +Warning: filter_var(): min_len must be less than or equal to max_len in %s on line %d +bool(false) diff --git a/ext/filter/tests/validate_str_invalid_type.phpt b/ext/filter/tests/validate_str_invalid_type.phpt new file mode 100644 index 0000000000000..ca513cc1cae83 --- /dev/null +++ b/ext/filter/tests/validate_str_invalid_type.phpt @@ -0,0 +1,43 @@ +--TEST-- +FILTER_VALIDATE_STR: Invalid input types +--SKIPIF-- + +--FILE-- + ['min_len' => 2, 'max_len' => 4]]; +$handle = fopen("php://memory", "r"); +class Dummy { public $x = 1; } + +var_dump( + filter_var(1234, FILTER_VALIDATE_STRLEN, $options), + filter_var(3.14, FILTER_VALIDATE_STRLEN, $options), + filter_var(['a', 'b'], FILTER_VALIDATE_STRLEN, $options), + filter_var(new Dummy(), FILTER_VALIDATE_STRLEN, $options), + filter_var(NULL, FILTER_VALIDATE_STRLEN, $options), + filter_var(true, FILTER_VALIDATE_STRLEN, $options), + filter_var(false, FILTER_VALIDATE_STRLEN, $options), + filter_var($handle, FILTER_VALIDATE_STRLEN, $options) +); + +fclose($handle); + +?> +--EXPECT-- +string(4) "1234" +string(4) "3.14" +bool(false) +bool(false) +bool(false) +bool(false) +bool(false) +bool(false) diff --git a/ext/filter/tests/validate_str_unicode.phpt b/ext/filter/tests/validate_str_unicode.phpt new file mode 100644 index 0000000000000..efb7cd1fdfb56 --- /dev/null +++ b/ext/filter/tests/validate_str_unicode.phpt @@ -0,0 +1,35 @@ +--TEST-- +FILTER_VALIDATE_STR: Unicode maximal subpart validation (Table 3-11 reference) +--SKIPIF-- + +--FILE-- + ['min_range' => 2, 'max_range' => 2]]; +$options2 = ['options' => ['max_range' => 5]]; + +echo bin2hex(filter_var("a\x80", FILTER_VALIDATE_STRLEN, $options1)), "\n"; +echo bin2hex(filter_var("\xE1\x80\xE2\xF0\x91\x92\xF1\xBF\x41", FILTER_VALIDATE_STRLEN, $options2)), "\n"; +?> +--EXPECT-- +6180 +e180e2f09192f1bf41