Skip to content

Commit 07f1cfd

Browse files
Deprecate producing output in a user output handler (#19067)
https://wiki.php.net/rfc/deprecations_php_8_4
1 parent 964a404 commit 07f1cfd

21 files changed

+1071
-16
lines changed

UPGRADING

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,12 @@ PHP 8.5 UPGRADE NOTES
265265
it is visible; if there are nested output handlers the next one will still
266266
be used.
267267
RFC: https://wiki.php.net/rfc/deprecations_php_8_4
268+
. Trying to produce output (e.g. with `echo`) within a user output handler
269+
is deprecated. The deprecation warning will bypass the handler producing the
270+
output to ensure it is visible; if there are nested output handlers the next
271+
one will still be used. If a user output handler returns a non-string and
272+
produces output, the warning about producing an output is emitted first.
273+
RFC: https://wiki.php.net/rfc/deprecations_php_8_4
268274

269275
- Hash:
270276
. The MHASH_* constants have been deprecated. These have been overlooked

ext/mbstring/mbstring.c

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1584,10 +1584,22 @@ PHP_FUNCTION(mb_output_handler)
15841584
if (SG(sapi_headers).send_default_content_type || free_mimetype) {
15851585
const char *charset = encoding->mime_name;
15861586
if (charset) {
1587-
char *p;
1588-
size_t len = spprintf(&p, 0, "Content-Type: %s; charset=%s", mimetype, charset);
1589-
if (sapi_add_header(p, len, 0) != FAILURE) {
1590-
SG(sapi_headers).send_default_content_type = 0;
1587+
/* Don't try to add a header if we are in an output handler;
1588+
* we aren't supposed to directly access the output globals
1589+
* from outside of main/output.c, so just try to get the flags
1590+
* for the currently running handler, will only succeed if
1591+
* there is a handler running. */
1592+
int unused;
1593+
bool in_handler = php_output_handler_hook(
1594+
PHP_OUTPUT_HANDLER_HOOK_GET_FLAGS,
1595+
&unused
1596+
) == SUCCESS;
1597+
if (!in_handler) {
1598+
char *p;
1599+
size_t len = spprintf(&p, 0, "Content-Type: %s; charset=%s", mimetype, charset);
1600+
if (sapi_add_header(p, len, 0) != FAILURE) {
1601+
SG(sapi_headers).send_default_content_type = 0;
1602+
}
15911603
}
15921604
}
15931605

main/output.c

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -934,6 +934,13 @@ static inline php_output_handler_status_t php_output_handler_op(php_output_handl
934934
return PHP_OUTPUT_HANDLER_FAILURE;
935935
}
936936

937+
/* php_output_lock_error() doesn't fail for PHP_OUTPUT_HANDLER_WRITE but
938+
* anything that gets written will silently be discarded, remember that we
939+
* tried to write so a deprecation warning can be emitted at the end. */
940+
if (context->op == PHP_OUTPUT_HANDLER_WRITE && OG(active) && OG(running)) {
941+
handler->flags |= PHP_OUTPUT_HANDLER_PRODUCED_OUTPUT;
942+
}
943+
937944
bool still_have_handler = true;
938945
/* storable? */
939946
if (php_output_handler_append(handler, &context->in) && !context->op) {
@@ -962,16 +969,37 @@ static inline php_output_handler_status_t php_output_handler_op(php_output_handl
962969
handler->func.user->fci.retval = &retval;
963970

964971
if (SUCCESS == zend_call_function(&handler->func.user->fci, &handler->func.user->fcc) && Z_TYPE(retval) != IS_UNDEF) {
965-
if (Z_TYPE(retval) != IS_STRING) {
972+
if (Z_TYPE(retval) != IS_STRING || handler->flags & PHP_OUTPUT_HANDLER_PRODUCED_OUTPUT) {
966973
// Make sure that we don't get lost in the current output buffer
967974
// by disabling it
968975
handler->flags |= PHP_OUTPUT_HANDLER_DISABLED;
969-
php_error_docref(
970-
NULL,
971-
E_DEPRECATED,
972-
"Returning a non-string result from user output handler %s is deprecated",
973-
ZSTR_VAL(handler->name)
974-
);
976+
// Make sure we keep a reference to the handler name in
977+
// case
978+
// * The handler produced output *and* returned a non-string
979+
// * The first deprecation message causes the handler to
980+
// be removed
981+
zend_string *handler_name = handler->name;
982+
zend_string_addref(handler_name);
983+
if (handler->flags & PHP_OUTPUT_HANDLER_PRODUCED_OUTPUT) {
984+
// The handler might not always produce output
985+
handler->flags &= ~PHP_OUTPUT_HANDLER_PRODUCED_OUTPUT;
986+
php_error_docref(
987+
NULL,
988+
E_DEPRECATED,
989+
"Producing output from user output handler %s is deprecated",
990+
ZSTR_VAL(handler_name)
991+
);
992+
}
993+
if (Z_TYPE(retval) != IS_STRING) {
994+
php_error_docref(
995+
NULL,
996+
E_DEPRECATED,
997+
"Returning a non-string result from user output handler %s is deprecated",
998+
ZSTR_VAL(handler_name)
999+
);
1000+
}
1001+
zend_string_release(handler_name);
1002+
9751003
// Check if the handler is still in the list of handlers to
9761004
// determine if the PHP_OUTPUT_HANDLER_DISABLED flag can
9771005
// be removed

main/php_output.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
#define PHP_OUTPUT_HANDLER_STARTED 0x1000
4343
#define PHP_OUTPUT_HANDLER_DISABLED 0x2000
4444
#define PHP_OUTPUT_HANDLER_PROCESSED 0x4000
45+
#define PHP_OUTPUT_HANDLER_PRODUCED_OUTPUT 0x8000
4546

4647
#define PHP_OUTPUT_HANDLER_ABILITY_FLAGS(bitmask) ((bitmask) & ~0xf00f)
4748

tests/output/ob_start_callback_bad_return/exception_handler.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
--TEST--
2-
ob_start(): Check behaviour with deprecation converted to exception
2+
ob_start(): Check behaviour with deprecation converted to exception [bad return]
33
--FILE--
44
<?php
55

tests/output/ob_start_callback_bad_return/exception_handler_nested.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
--TEST--
2-
ob_start(): Check behaviour with deprecation converted to exception
2+
ob_start(): Check behaviour with nested deprecation converted to exception [bad return]
33
--FILE--
44
<?php
55

tests/output/ob_start_callback_bad_return/multiple_handlers.phpt

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
--TEST--
2-
ob_start(): Check behaviour with multiple nested handlers with had return values
2+
ob_start(): Check behaviour with multiple nested handlers with bad return values
33
--FILE--
44
<?php
55

@@ -69,21 +69,38 @@ echo "\n\nLog:\n";
6969
echo implode("\n", $log);
7070
?>
7171
--EXPECTF--
72+
Deprecated: ob_end_flush(): Producing output from user output handler return_given_string is deprecated in %s on line %d3
73+
74+
Deprecated: ob_end_flush(): Producing output from user output handler return_empty_string is deprecated in %s on line %d2
75+
76+
7277
Log:
7378
return_zero: <<<Testing...>>>
7479
return_string: <<<
7580
Deprecated: ob_end_flush(): Returning a non-string result from user output handler return_zero is deprecated in %s on line %d
7681
0>>>
77-
return_null: <<<I stole your output.>>>
82+
return_null: <<<
83+
Deprecated: ob_end_flush(): Producing output from user output handler return_string is deprecated in %s on line %d
84+
I stole your output.>>>
7885
return_true: <<<
86+
Deprecated: ob_end_flush(): Producing output from user output handler return_null is deprecated in %s on line %d
87+
7988
Deprecated: ob_end_flush(): Returning a non-string result from user output handler return_null is deprecated in %s on line %d
8089
>>>
8190
return_false: <<<
91+
Deprecated: ob_end_flush(): Producing output from user output handler return_true is deprecated in %s on line %d
92+
8293
Deprecated: ob_end_flush(): Returning a non-string result from user output handler return_true is deprecated in %s on line %d
8394
>>>
8495
return_empty_string: <<<
96+
Deprecated: ob_end_flush(): Producing output from user output handler return_false is deprecated in %s on line %d
97+
8598
Deprecated: ob_end_flush(): Returning a non-string result from user output handler return_false is deprecated in %s on line %d
8699

100+
Deprecated: ob_end_flush(): Producing output from user output handler return_true is deprecated in %s on line %d
101+
87102
Deprecated: ob_end_flush(): Returning a non-string result from user output handler return_true is deprecated in %s on line %d
88103
>>>
89-
return_given_string: <<<>>>
104+
return_given_string: <<<
105+
Deprecated: ob_end_flush(): Producing output from user output handler return_empty_string is deprecated in %s on line %d2
106+
>>>
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
--TEST--
2+
ob_start(): Check behaviour with deprecation converted to exception [produce output]
3+
--FILE--
4+
<?php
5+
6+
$log = [];
7+
8+
set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline) {
9+
throw new \ErrorException($errstr, 0, $errno, $errfile, $errline);
10+
});
11+
12+
function first_handler($string) {
13+
global $log;
14+
$log[] = __FUNCTION__ . ": <<<" . $string . ">>>";
15+
echo __FUNCTION__;
16+
return "FIRST\n";
17+
}
18+
19+
function second_handler($string) {
20+
global $log;
21+
$log[] = __FUNCTION__ . ": <<<" . $string . ">>>";
22+
echo __FUNCTION__;
23+
return "SECOND\n";
24+
}
25+
26+
function third_handler($string) {
27+
global $log;
28+
$log[] = __FUNCTION__ . ": <<<" . $string . ">>>";
29+
echo __FUNCTION__;
30+
return "THIRD\n";
31+
}
32+
33+
$cases = [
34+
'first_handler',
35+
'second_handler',
36+
'third_handler',
37+
];
38+
foreach ($cases as $case) {
39+
$log = [];
40+
echo "\n\nTesting: $case\n";
41+
ob_start($case);
42+
echo "Inside of $case\n";
43+
try {
44+
ob_end_flush();
45+
} catch (\ErrorException $e) {
46+
echo $e . "\n";
47+
}
48+
echo "\nEnd of $case, log was:\n";
49+
echo implode("\n", $log);
50+
}
51+
52+
?>
53+
--EXPECTF--
54+
Testing: first_handler
55+
FIRST
56+
ErrorException: ob_end_flush(): Producing output from user output handler first_handler is deprecated in %s:%d
57+
Stack trace:
58+
#0 [internal function]: {closure:%s:%d}(8192, 'ob_end_flush():...', %s, 41)
59+
#1 %s(%d): ob_end_flush()
60+
#2 {main}
61+
62+
End of first_handler, log was:
63+
first_handler: <<<Inside of first_handler
64+
>>>
65+
66+
Testing: second_handler
67+
SECOND
68+
ErrorException: ob_end_flush(): Producing output from user output handler second_handler is deprecated in %s:%d
69+
Stack trace:
70+
#0 [internal function]: {closure:%s:%d}(8192, 'ob_end_flush():...', %s, 41)
71+
#1 %s(%d): ob_end_flush()
72+
#2 {main}
73+
74+
End of second_handler, log was:
75+
second_handler: <<<Inside of second_handler
76+
>>>
77+
78+
Testing: third_handler
79+
THIRD
80+
ErrorException: ob_end_flush(): Producing output from user output handler third_handler is deprecated in %s:%d
81+
Stack trace:
82+
#0 [internal function]: {closure:%s:%d}(8192, 'ob_end_flush():...', %s, 41)
83+
#1 %s(%d): ob_end_flush()
84+
#2 {main}
85+
86+
End of third_handler, log was:
87+
third_handler: <<<Inside of third_handler
88+
>>>
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
--TEST--
2+
ob_start(): Check behaviour with nested deprecation converted to exception [produce output]
3+
--FILE--
4+
<?php
5+
6+
$log = [];
7+
8+
set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline) {
9+
throw new \ErrorException($errstr, 0, $errno, $errfile, $errline);
10+
});
11+
12+
function first_handler($string) {
13+
global $log;
14+
$log[] = __FUNCTION__ . ": <<<" . $string . ">>>";
15+
echo __FUNCTION__;
16+
return "FIRST\n";
17+
}
18+
19+
function second_handler($string) {
20+
global $log;
21+
$log[] = __FUNCTION__ . ": <<<" . $string . ">>>";
22+
echo __FUNCTION__;
23+
return "SECOND\n";
24+
}
25+
26+
function third_handler($string) {
27+
global $log;
28+
$log[] = __FUNCTION__ . ": <<<" . $string . ">>>";
29+
echo __FUNCTION__;
30+
return "THIRD\n";
31+
}
32+
33+
ob_start('first_handler');
34+
ob_start('second_handler');
35+
ob_start('third_handler');
36+
37+
echo "In all of them\n\n";
38+
try {
39+
ob_end_flush();
40+
} catch (\ErrorException $e) {
41+
echo $e->getMessage() . "\n";
42+
}
43+
echo "Ended third_handler\n\n";
44+
45+
try {
46+
ob_end_flush();
47+
} catch (\ErrorException $e) {
48+
echo $e->getMessage() . "\n";
49+
}
50+
echo "Ended second_handler\n\n";
51+
52+
try {
53+
ob_end_flush();
54+
} catch (\ErrorException $e) {
55+
echo $e->getMessage() . "\n";
56+
}
57+
echo "Ended first_handler handler\n\n";
58+
59+
echo "All handlers are over\n\n";
60+
echo implode("\n", $log);
61+
62+
?>
63+
--EXPECT--
64+
FIRST
65+
ob_end_flush(): Producing output from user output handler first_handler is deprecated
66+
Ended first_handler handler
67+
68+
All handlers are over
69+
70+
third_handler: <<<In all of them
71+
72+
>>>
73+
second_handler: <<<THIRD
74+
ob_end_flush(): Producing output from user output handler third_handler is deprecated
75+
Ended third_handler
76+
77+
>>>
78+
first_handler: <<<SECOND
79+
ob_end_flush(): Producing output from user output handler second_handler is deprecated
80+
Ended second_handler
81+
82+
>>>

0 commit comments

Comments
 (0)