Skip to content

Commit 7d5b42e

Browse files
[RFC} FILTER_THROW_ON_FAILURE
1 parent 27e485d commit 7d5b42e

File tree

9 files changed

+379
-10
lines changed

9 files changed

+379
-10
lines changed

ext/filter/filter.c

Lines changed: 102 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ ZEND_DECLARE_MODULE_GLOBALS(filter)
2929

3030
#include "filter_private.h"
3131
#include "filter_arginfo.h"
32+
#include "zend_exceptions.h"
3233

3334
typedef struct filter_list_entry {
3435
const char *name;
@@ -76,6 +77,9 @@ static const filter_list_entry filter_list[] = {
7677
static unsigned int php_sapi_filter(int arg, const char *var, char **val, size_t val_len, size_t *new_val_len);
7778
static unsigned int php_sapi_filter_init(void);
7879

80+
zend_class_entry *php_filter_exception_ce;
81+
zend_class_entry *php_filter_failed_exception_ce;
82+
7983
/* {{{ filter_module_entry */
8084
zend_module_entry filter_module_entry = {
8185
STANDARD_MODULE_HEADER,
@@ -159,6 +163,9 @@ PHP_MINIT_FUNCTION(filter)
159163

160164
sapi_register_input_filter(php_sapi_filter, php_sapi_filter_init);
161165

166+
php_filter_exception_ce = register_class_Filter_FilterException(zend_ce_exception);
167+
php_filter_failed_exception_ce = register_class_Filter_FilterFailedException(php_filter_exception_ce);
168+
162169
return SUCCESS;
163170
}
164171
/* }}} */
@@ -250,6 +257,15 @@ static void php_zval_filter(zval *value, zend_long filter, zend_long flags, zval
250257
ce = Z_OBJCE_P(value);
251258
if (!ce->__tostring) {
252259
zval_ptr_dtor(value);
260+
if (flags & FILTER_THROW_ON_FAILURE) {
261+
zend_throw_exception_ex(
262+
php_filter_failed_exception_ce,
263+
0,
264+
"filter validation failed: object of type %s has no __toString() method",
265+
ZSTR_VAL(ce->name)
266+
);
267+
return;
268+
}
253269
/* #67167: doesn't return null on failure for objects */
254270
if (flags & FILTER_NULL_ON_FAILURE) {
255271
ZVAL_NULL(value);
@@ -263,8 +279,30 @@ static void php_zval_filter(zval *value, zend_long filter, zend_long flags, zval
263279
/* Here be strings */
264280
convert_to_string(value);
265281

282+
zend_string *copy_for_throwing = NULL;
283+
if (flags & FILTER_THROW_ON_FAILURE) {
284+
copy_for_throwing = zend_string_copy(Z_STR_P(value));
285+
}
286+
266287
filter_func.function(value, flags, options, charset);
267288

289+
if (flags & FILTER_THROW_ON_FAILURE) {
290+
ZEND_ASSERT(copy_for_throwing != NULL);
291+
if (Z_ISERROR_P(value)) {
292+
zend_throw_exception_ex(
293+
php_filter_failed_exception_ce,
294+
0,
295+
"filter validation failed: filter %s not satisfied by %s",
296+
filter_func.name,
297+
ZSTR_VAL(copy_for_throwing)
298+
);
299+
zend_string_delref(copy_for_throwing);
300+
return;
301+
}
302+
zend_string_delref(copy_for_throwing);
303+
copy_for_throwing = NULL;
304+
}
305+
268306
handle_default:
269307
if (options && Z_TYPE_P(options) == IS_ARRAY &&
270308
((flags & FILTER_NULL_ON_FAILURE && Z_TYPE_P(value) == IS_NULL) ||
@@ -449,7 +487,8 @@ PHP_FUNCTION(filter_has_var)
449487

450488
static void php_filter_call(
451489
zval *filtered, zend_long filter, HashTable *filter_args_ht, zend_long filter_args_long,
452-
zend_long filter_flags
490+
zend_long filter_flags,
491+
uint32_t options_arg_num
453492
) /* {{{ */ {
454493
zval *options = NULL;
455494
char *charset = NULL;
@@ -491,10 +530,25 @@ static void php_filter_call(
491530
}
492531
}
493532

533+
/* Cannot use both FILTER_NULL_ON_FAILURE and FILTER_THROW_ON_FAILURE */
534+
if ((filter_flags & FILTER_NULL_ON_FAILURE) && (filter_flags & FILTER_THROW_ON_FAILURE)) {
535+
zend_argument_value_error(
536+
options_arg_num,
537+
"cannot use both FILTER_NULL_ON_FAILURE and FILTER_THROW_ON_FAILURE"
538+
);
539+
return;
540+
}
541+
494542
if (Z_TYPE_P(filtered) == IS_ARRAY) {
495543
if (filter_flags & FILTER_REQUIRE_SCALAR) {
496544
zval_ptr_dtor(filtered);
497-
if (filter_flags & FILTER_NULL_ON_FAILURE) {
545+
if (filter_flags & FILTER_THROW_ON_FAILURE) {
546+
zend_throw_exception(
547+
php_filter_failed_exception_ce,
548+
"filter validation failed: not a scalar value (got an array)",
549+
0
550+
);
551+
} else if (filter_flags & FILTER_NULL_ON_FAILURE) {
498552
ZVAL_NULL(filtered);
499553
} else {
500554
ZVAL_FALSE(filtered);
@@ -505,8 +559,16 @@ static void php_filter_call(
505559
return;
506560
}
507561
if (filter_flags & FILTER_REQUIRE_ARRAY) {
562+
const char *got_type = zend_zval_value_name(filtered);
508563
zval_ptr_dtor(filtered);
509-
if (filter_flags & FILTER_NULL_ON_FAILURE) {
564+
if (filter_flags & FILTER_THROW_ON_FAILURE) {
565+
zend_throw_exception_ex(
566+
php_filter_failed_exception_ce,
567+
0,
568+
"filter validation failed: not an array (got %s)",
569+
got_type
570+
);
571+
} else if (filter_flags & FILTER_NULL_ON_FAILURE) {
510572
ZVAL_NULL(filtered);
511573
} else {
512574
ZVAL_FALSE(filtered);
@@ -515,6 +577,10 @@ static void php_filter_call(
515577
}
516578

517579
php_zval_filter(filtered, filter, filter_flags, options, charset);
580+
// Don't wrap in an array if we are throwing an exception
581+
if (EG(exception)) {
582+
return;
583+
}
518584
if (filter_flags & FILTER_FORCE_ARRAY) {
519585
zval tmp;
520586
ZVAL_COPY_VALUE(&tmp, filtered);
@@ -529,7 +595,7 @@ static void php_filter_array_handler(zval *input, HashTable *op_ht, zend_long op
529595
) /* {{{ */ {
530596
if (!op_ht) {
531597
ZVAL_DUP(return_value, input);
532-
php_filter_call(return_value, -1, NULL, op_long, FILTER_REQUIRE_ARRAY);
598+
php_filter_call(return_value, -1, NULL, op_long, FILTER_REQUIRE_ARRAY, 2);
533599
} else {
534600
array_init(return_value);
535601
zend_string *arg_key;
@@ -556,8 +622,13 @@ static void php_filter_array_handler(zval *input, HashTable *op_ht, zend_long op
556622
php_filter_call(&nval, -1,
557623
Z_TYPE_P(arg_elm) == IS_ARRAY ? Z_ARRVAL_P(arg_elm) : NULL,
558624
Z_TYPE_P(arg_elm) == IS_ARRAY ? 0 : zval_get_long(arg_elm),
559-
FILTER_REQUIRE_SCALAR
625+
FILTER_REQUIRE_SCALAR,
626+
2
560627
);
628+
if (EG(exception)) {
629+
zval_ptr_dtor(&nval);
630+
RETURN_THROWS();
631+
}
561632
zend_hash_update(Z_ARRVAL_P(return_value), arg_key, &nval);
562633
}
563634
} ZEND_HASH_FOREACH_END();
@@ -597,11 +668,34 @@ PHP_FUNCTION(filter_input)
597668
if (!filter_args_ht) {
598669
filter_flags = filter_args_long;
599670
} else {
600-
zval *option, *opt, *def;
671+
zval *option;
601672
if ((option = zend_hash_str_find(filter_args_ht, "flags", sizeof("flags") - 1)) != NULL) {
602673
filter_flags = zval_get_long(option);
603674
}
675+
}
676+
677+
/* Cannot use both FILTER_NULL_ON_FAILURE and FILTER_THROW_ON_FAILURE */
678+
if ((filter_flags & FILTER_NULL_ON_FAILURE) && (filter_flags & FILTER_THROW_ON_FAILURE)) {
679+
zend_argument_value_error(
680+
4,
681+
"cannot use both FILTER_NULL_ON_FAILURE and FILTER_THROW_ON_FAILURE"
682+
);
683+
RETURN_THROWS();
684+
}
685+
686+
if (filter_flags & FILTER_THROW_ON_FAILURE) {
687+
zend_throw_exception(
688+
php_filter_failed_exception_ce,
689+
"input value not found",
690+
0
691+
);
692+
RETURN_THROWS();
693+
}
604694

695+
/* FILTER_THROW_ON_FAILURE overrides defaults, needs to be checked
696+
* before the default is used. */
697+
if (filter_args_ht) {
698+
zval *opt, *def;
605699
if ((opt = zend_hash_str_find_deref(filter_args_ht, "options", sizeof("options") - 1)) != NULL &&
606700
Z_TYPE_P(opt) == IS_ARRAY &&
607701
(def = zend_hash_str_find_deref(Z_ARRVAL_P(opt), "default", sizeof("default") - 1)) != NULL
@@ -625,7 +719,7 @@ PHP_FUNCTION(filter_input)
625719

626720
ZVAL_DUP(return_value, tmp);
627721

628-
php_filter_call(return_value, filter, filter_args_ht, filter_args_long, FILTER_REQUIRE_SCALAR);
722+
php_filter_call(return_value, filter, filter_args_ht, filter_args_long, FILTER_REQUIRE_SCALAR, 4);
629723
}
630724
/* }}} */
631725

@@ -651,7 +745,7 @@ PHP_FUNCTION(filter_var)
651745

652746
ZVAL_DUP(return_value, data);
653747

654-
php_filter_call(return_value, filter, filter_args_ht, filter_args_long, FILTER_REQUIRE_SCALAR);
748+
php_filter_call(return_value, filter, filter_args_ht, filter_args_long, FILTER_REQUIRE_SCALAR, 3);
655749
}
656750
/* }}} */
657751

ext/filter/filter.stub.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
/** @generate-class-entries */
44

5+
namespace {
56
/**
67
* @var int
78
* @cvalue PARSE_POST
@@ -54,6 +55,11 @@
5455
* @cvalue FILTER_NULL_ON_FAILURE
5556
*/
5657
const FILTER_NULL_ON_FAILURE = UNKNOWN;
58+
/**
59+
* @var int
60+
* @cvalue FILTER_THROW_ON_FAILURE
61+
*/
62+
const FILTER_THROW_ON_FAILURE = UNKNOWN;
5763

5864
/**
5965
* @var int
@@ -313,3 +319,13 @@ function filter_var_array(array $array, array|int $options = FILTER_DEFAULT, boo
313319
function filter_list(): array {}
314320

315321
function filter_id(string $name): int|false {}
322+
323+
}
324+
325+
namespace Filter {
326+
327+
class FilterException extends \Exception {}
328+
329+
class FilterFailedException extends FilterException {}
330+
331+
}

ext/filter/filter_arginfo.h

Lines changed: 22 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ext/filter/filter_private.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
#define FILTER_FORCE_ARRAY 0x4000000
2626
#define FILTER_NULL_ON_FAILURE 0x8000000
27+
#define FILTER_THROW_ON_FAILURE 0x10000000
2728

2829
#define FILTER_FLAG_ALLOW_OCTAL 0x0001
2930
#define FILTER_FLAG_ALLOW_HEX 0x0002
@@ -50,7 +51,7 @@
5051
#define FILTER_FLAG_IPV6 0x00200000
5152
#define FILTER_FLAG_NO_RES_RANGE 0x00400000
5253
#define FILTER_FLAG_NO_PRIV_RANGE 0x00800000
53-
#define FILTER_FLAG_GLOBAL_RANGE 0x10000000
54+
#define FILTER_FLAG_GLOBAL_RANGE 0x20000000
5455

5556
#define FILTER_FLAG_HOSTNAME 0x100000
5657

@@ -93,9 +94,16 @@
9394
|| (id >= FILTER_VALIDATE_ALL && id <= FILTER_VALIDATE_LAST) \
9495
|| id == FILTER_CALLBACK)
9596

97+
98+
/* When using FILTER_THROW_ON_FAILURE, we can't actually throw the error here
99+
* because we don't have access to the name of the filter. Use ZVAL_ERROR()
100+
* so that we can easily identify places where we want to throw. */
96101
#define RETURN_VALIDATION_FAILED \
97102
if (EG(exception)) { \
98103
return; \
104+
} else if (flags & FILTER_THROW_ON_FAILURE) { \
105+
zval_ptr_dtor(value); \
106+
ZVAL_ERROR(value); \
99107
} else if (flags & FILTER_NULL_ON_FAILURE) { \
100108
zval_ptr_dtor(value); \
101109
ZVAL_NULL(value); \
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
--TEST--
2+
FILTER_THROW_ON_FAILURE: filter_input_array() failure
3+
--EXTENSIONS--
4+
filter
5+
--GET--
6+
a=1
7+
--FILE--
8+
<?php
9+
10+
echo "\nvalidation fails (array type check)\n";
11+
try {
12+
filter_input_array(INPUT_GET, ['a' => ['flags' => FILTER_REQUIRE_ARRAY | FILTER_THROW_ON_FAILURE]]);
13+
} catch (Filter\FilterFailedException $e) {
14+
echo get_class($e) . ": " . $e->getMessage() . "\n";
15+
}
16+
17+
echo "\nvalidation fails (filter value)\n";
18+
try {
19+
filter_input_array(INPUT_GET, ['a' => ['filter' => FILTER_VALIDATE_EMAIL, 'flags' => FILTER_THROW_ON_FAILURE]]);
20+
} catch (Filter\FilterFailedException $e) {
21+
echo get_class($e) . ": " . $e->getMessage() . "\n";
22+
}
23+
?>
24+
--EXPECT--
25+
validation fails (array type check)
26+
Filter\FilterFailedException: filter validation failed: not an array (got string)
27+
28+
validation fails (filter value)
29+
Filter\FilterFailedException: filter validation failed: filter validate_email not satisfied by 1
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
--TEST--
2+
FILTER_THROW_ON_FAILURE: filter_input() failure
3+
--EXTENSIONS--
4+
filter
5+
--GET--
6+
a=1
7+
--FILE--
8+
<?php
9+
10+
echo "missing value\n";
11+
try {
12+
filter_input(INPUT_GET, 'b', FILTER_DEFAULT, FILTER_THROW_ON_FAILURE);
13+
} catch (Filter\FilterFailedException $e) {
14+
echo get_class($e) . ": " . $e->getMessage() . "\n";
15+
}
16+
17+
echo "\nvalidation fails (array type check)\n";
18+
try {
19+
filter_input(INPUT_GET, 'a', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY | FILTER_THROW_ON_FAILURE);
20+
} catch (Filter\FilterFailedException $e) {
21+
echo get_class($e) . ": " . $e->getMessage() . "\n";
22+
}
23+
24+
echo "\nvalidation fails (filter value)\n";
25+
try {
26+
filter_input(INPUT_GET, 'a', FILTER_VALIDATE_EMAIL, FILTER_THROW_ON_FAILURE);
27+
} catch (Filter\FilterFailedException $e) {
28+
echo get_class($e) . ": " . $e->getMessage() . "\n";
29+
}
30+
?>
31+
--EXPECT--
32+
missing value
33+
Filter\FilterFailedException: input value not found
34+
35+
validation fails (array type check)
36+
Filter\FilterFailedException: filter validation failed: not an array (got string)
37+
38+
validation fails (filter value)
39+
Filter\FilterFailedException: filter validation failed: filter validate_email not satisfied by 1

0 commit comments

Comments
 (0)