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..8ea948520f0ca 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" @@ -1115,3 +1116,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/008.phpt b/ext/filter/tests/008.phpt index b43d14603dc1b..984be4c9d8334 100644 --- a/ext/filter/tests/008.phpt +++ b/ext/filter/tests/008.phpt @@ -10,7 +10,7 @@ var_dump(filter_list()); echo "Done\n"; ?> --EXPECT-- -array(21) { +array(22) { [0]=> string(3) "int" [1]=> @@ -30,28 +30,30 @@ array(21) { [8]=> string(12) "validate_mac" [9]=> - string(6) "string" + string(15) "validate_strlen" [10]=> - string(8) "stripped" + string(6) "string" [11]=> - string(7) "encoded" + string(8) "stripped" [12]=> - string(13) "special_chars" + string(7) "encoded" [13]=> - string(18) "full_special_chars" + string(13) "special_chars" [14]=> - string(10) "unsafe_raw" + string(18) "full_special_chars" [15]=> - string(5) "email" + string(10) "unsafe_raw" [16]=> - string(3) "url" + string(5) "email" [17]=> - string(10) "number_int" + string(3) "url" [18]=> - string(12) "number_float" + string(10) "number_int" [19]=> - string(11) "add_slashes" + string(12) "number_float" [20]=> + string(11) "add_slashes" + [21]=> string(8) "callback" } Done diff --git a/ext/filter/tests/033.phpt b/ext/filter/tests/033.phpt index 0f18913a79760..703f02b8b98d4 100644 --- a/ext/filter/tests/033.phpt +++ b/ext/filter/tests/033.phpt @@ -19,6 +19,7 @@ validate_url http://a.b.c validate_email foo@bar.com validate_ip 1.2.3.4 validate_mac aa:bb:cc:dd:ee:ff +validate_strlen PHP 123 하퍼 string PHP 1 foo@bar.com http://a.b.c 1.2.3.4 123 123abc() O'Henry 하퍼 aa:bb:cc:dd:ee:ff stripped PHP 1 foo@bar.com http://a.b.c 1.2.3.4 123 123abc() O'Henry 하퍼 aa:bb:cc:dd:ee:ff encoded PHP 1 foo%40bar.com http%3A%2F%2Fa.b.c 1.2.3.4 123 123abc%3C%3E%28%29 O%27Henry %ED%95%98%ED%8D%BCaa%3Abb%3Acc%3Add%3Aee%3Aff @@ -30,4 +31,4 @@ url PHP 1 foo@bar.com http://a.b.c 1.2.3.4 123 12 number_int 1 1234 123 123 number_float 1 1234 123 123 add_slashes PHP 1 foo@bar.com http://a.b.c 1.2.3.4 123 123abc<>() O\'Henry 하퍼 aa:bb:cc:dd:ee:ff -callback PHP 1 FOO@BAR.COM HTTP://A.B.C 1.2.3.4 123 123ABC<>() O'HENRY 하퍼 AA:BB:CC:DD:EE:FF +callback PHP 1 FOO@BAR.COM HTTP://A.B.C 1.2.3.4 123 123ABC<>() O'HENRY 하퍼 AA:BB:CC:DD:EE:FF \ No newline at end of file diff --git a/ext/filter/tests/033_run.inc b/ext/filter/tests/033_run.inc index 75ce12786836a..39417b90f1cb4 100644 --- a/ext/filter/tests/033_run.inc +++ b/ext/filter/tests/033_run.inc @@ -22,6 +22,10 @@ $data = array( foreach(filter_list() as $filter) { if($filter=="validate_regexp") { foreach($data as $k=>$d) $result[$k] = filter_var($d,filter_id($filter),array("options"=>array("regexp"=>'/^O.*/'))); + } else if($filter=="validate_strlen") { + foreach ($data as $k => $d) { + $result[$k] = filter_var($d, filter_id($filter),array("options" => array("min_len" => 2, "max_len" => 5))); + } } else { foreach($data as $k=>$d) $result[$k] = filter_var($d,filter_id($filter),array("options"=>"test")); } 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