diff --git a/UPGRADING b/UPGRADING index 4d968ef4daf8..8661d3317446 100644 --- a/UPGRADING +++ b/UPGRADING @@ -258,6 +258,13 @@ PHP 8.5 UPGRADE NOTES 4. Deprecated Functionality ======================================== +- Core: + . Returning a non-string from a user output handler is deprecated. The + deprecation warning will bypass the handler with the bad return to ensure + it is visible; if there are nested output handlers the next one will still + be used. + RFC: https://wiki.php.net/rfc/deprecations_php_8_4 + - Hash: . The MHASH_* constants have been deprecated. These have been overlooked when the mhash*() function family has been deprecated per diff --git a/Zend/tests/concat/bug79836.phpt b/Zend/tests/concat/bug79836.phpt index 5fb07396762f..0383bd0fb636 100644 --- a/Zend/tests/concat/bug79836.phpt +++ b/Zend/tests/concat/bug79836.phpt @@ -14,5 +14,10 @@ $c .= []; ob_end_clean(); echo $counter . "\n"; ?> ---EXPECT-- +--EXPECTF-- +Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d + +Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d + +Deprecated: ob_end_clean(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d 3 diff --git a/Zend/tests/concat/bug79836_1.phpt b/Zend/tests/concat/bug79836_1.phpt index 86e7f4767184..28a04ad47f5c 100644 --- a/Zend/tests/concat/bug79836_1.phpt +++ b/Zend/tests/concat/bug79836_1.phpt @@ -14,5 +14,8 @@ $x = $c . $x; ob_end_clean(); echo "Done\n"; ?> ---EXPECT-- +--EXPECTF-- +Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d + +Deprecated: ob_end_clean(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d Done diff --git a/Zend/tests/concat/bug79836_2.phpt b/Zend/tests/concat/bug79836_2.phpt index b02fcc13ea11..eeba0135468c 100644 --- a/Zend/tests/concat/bug79836_2.phpt +++ b/Zend/tests/concat/bug79836_2.phpt @@ -21,5 +21,8 @@ $x = $c . $xxx; ob_end_clean(); echo $x . "\n"; ?> ---EXPECT-- +--EXPECTF-- +Deprecated: X::__toString(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d + +Deprecated: ob_end_clean(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabc diff --git a/Zend/tests/declare/gh18033_2.phpt b/Zend/tests/declare/gh18033_2.phpt index 8fdcff1b51e6..45d506b9b1d8 100644 --- a/Zend/tests/declare/gh18033_2.phpt +++ b/Zend/tests/declare/gh18033_2.phpt @@ -13,4 +13,5 @@ ob_start(function() { ); }); ?> ---EXPECT-- +--EXPECTF-- +Deprecated: PHP Request Shutdown: Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d diff --git a/Zend/tests/gh11189.phpt b/Zend/tests/gh11189.phpt index f1c877f20ee4..84174023dee0 100644 --- a/Zend/tests/gh11189.phpt +++ b/Zend/tests/gh11189.phpt @@ -26,4 +26,6 @@ while (1) { ?> --EXPECTF-- Success +Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d + Fatal error: Allowed memory size of %s bytes exhausted%s(tried to allocate %s bytes) in %s on line %d diff --git a/Zend/tests/gh11189_1.phpt b/Zend/tests/gh11189_1.phpt index 53727908e5e2..0d8b84d2667f 100644 --- a/Zend/tests/gh11189_1.phpt +++ b/Zend/tests/gh11189_1.phpt @@ -26,4 +26,6 @@ while (1) { ?> --EXPECTF-- Success +Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d + Fatal error: Allowed memory size of %s bytes exhausted%s(tried to allocate %s bytes) in %s on line %d diff --git a/Zend/tests/gh16408.phpt b/Zend/tests/gh16408.phpt index f28c6435bfe7..de09c84803de 100644 --- a/Zend/tests/gh16408.phpt +++ b/Zend/tests/gh16408.phpt @@ -12,5 +12,10 @@ $c .= []; ob_end_clean(); echo $counter . "\n"; ?> ---EXPECT-- +--EXPECTF-- +Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d + +Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d + +Deprecated: ob_end_clean(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d 3 diff --git a/ext/session/tests/user_session_module/bug61728.phpt b/ext/session/tests/user_session_module/bug61728.phpt index fd79fa6b2bce..7bc042e0116f 100644 --- a/ext/session/tests/user_session_module/bug61728.phpt +++ b/ext/session/tests/user_session_module/bug61728.phpt @@ -40,5 +40,6 @@ class MySessionHandler implements SessionHandlerInterface { session_set_save_handler(new MySessionHandler()); session_start(); ?> ---EXPECT-- +--EXPECTF-- +Deprecated: ob_end_flush(): Returning a non-string result from user output handler output_html is deprecated in %s on line %d 8 diff --git a/main/output.c b/main/output.c index ef6be672d1c1..1437eb37273b 100644 --- a/main/output.c +++ b/main/output.c @@ -934,6 +934,7 @@ static inline php_output_handler_status_t php_output_handler_op(php_output_handl return PHP_OUTPUT_HANDLER_FAILURE; } + bool still_have_handler = true; /* storable? */ if (php_output_handler_append(handler, &context->in) && !context->op) { context->op = original_op; @@ -948,6 +949,7 @@ static inline php_output_handler_status_t php_output_handler_op(php_output_handl if (handler->flags & PHP_OUTPUT_HANDLER_USER) { zval ob_args[2]; zval retval; + ZVAL_UNDEF(&retval); /* ob_data */ ZVAL_STRINGL(&ob_args[0], handler->buffer.data, handler->buffer.used); @@ -959,17 +961,48 @@ static inline php_output_handler_status_t php_output_handler_op(php_output_handl handler->func.user->fci.params = ob_args; handler->func.user->fci.retval = &retval; -#define PHP_OUTPUT_USER_SUCCESS(retval) ((Z_TYPE(retval) != IS_UNDEF) && !(Z_TYPE(retval) == IS_FALSE)) - if (SUCCESS == zend_call_function(&handler->func.user->fci, &handler->func.user->fcc) && PHP_OUTPUT_USER_SUCCESS(retval)) { - /* user handler may have returned TRUE */ - status = PHP_OUTPUT_HANDLER_NO_DATA; - if (Z_TYPE(retval) != IS_FALSE && Z_TYPE(retval) != IS_TRUE) { - convert_to_string(&retval); - if (Z_STRLEN(retval)) { - context->out.data = estrndup(Z_STRVAL(retval), Z_STRLEN(retval)); - context->out.used = Z_STRLEN(retval); - context->out.free = 1; - status = PHP_OUTPUT_HANDLER_SUCCESS; + if (SUCCESS == zend_call_function(&handler->func.user->fci, &handler->func.user->fcc) && Z_TYPE(retval) != IS_UNDEF) { + if (Z_TYPE(retval) != IS_STRING) { + // Make sure that we don't get lost in the current output buffer + // by disabling it + handler->flags |= PHP_OUTPUT_HANDLER_DISABLED; + php_error_docref( + NULL, + E_DEPRECATED, + "Returning a non-string result from user output handler %s is deprecated", + ZSTR_VAL(handler->name) + ); + // Check if the handler is still in the list of handlers to + // determine if the PHP_OUTPUT_HANDLER_DISABLED flag can + // be removed + still_have_handler = false; + int handler_count = php_output_get_level(); + if (handler_count) { + php_output_handler **handlers = (php_output_handler **) zend_stack_base(&OG(handlers)); + for (int iii = 0; iii < handler_count; ++iii) { + php_output_handler *curr_handler = handlers[iii]; + if (curr_handler == handler) { + handler->flags &= (~PHP_OUTPUT_HANDLER_DISABLED); + still_have_handler = true; + break; + } + } + } + } + if (Z_TYPE(retval) == IS_FALSE) { + /* call failed, pass internal buffer along */ + status = PHP_OUTPUT_HANDLER_FAILURE; + } else { + /* user handler may have returned TRUE */ + status = PHP_OUTPUT_HANDLER_NO_DATA; + if (Z_TYPE(retval) != IS_FALSE && Z_TYPE(retval) != IS_TRUE) { + convert_to_string(&retval); + if (Z_STRLEN(retval)) { + context->out.data = estrndup(Z_STRVAL(retval), Z_STRLEN(retval)); + context->out.used = Z_STRLEN(retval); + context->out.free = 1; + status = PHP_OUTPUT_HANDLER_SUCCESS; + } } } } else { @@ -996,10 +1029,17 @@ static inline php_output_handler_status_t php_output_handler_op(php_output_handl status = PHP_OUTPUT_HANDLER_FAILURE; } } - handler->flags |= PHP_OUTPUT_HANDLER_STARTED; + if (still_have_handler) { + handler->flags |= PHP_OUTPUT_HANDLER_STARTED; + } OG(running) = NULL; } + if (!still_have_handler) { + // Handler and context will have both already been freed + return status; + } + switch (status) { case PHP_OUTPUT_HANDLER_FAILURE: /* disable this handler */ @@ -1225,6 +1265,19 @@ static int php_output_stack_pop(int flags) } php_output_handler_op(orphan, &context); } + // If it isn't still in the stack, cannot free it + bool still_have_handler = false; + int handler_count = php_output_get_level(); + if (handler_count) { + php_output_handler **handlers = (php_output_handler **) zend_stack_base(&OG(handlers)); + for (int iii = 0; iii < handler_count; ++iii) { + php_output_handler *curr_handler = handlers[iii]; + if (curr_handler == orphan) { + still_have_handler = true; + break; + } + } + } /* pop it off the stack */ zend_stack_del_top(&OG(handlers)); @@ -1240,7 +1293,9 @@ static int php_output_stack_pop(int flags) } /* destroy the handler (after write!) */ - php_output_handler_free(&orphan); + if (still_have_handler) { + php_output_handler_free(&orphan); + } php_output_context_dtor(&context); return 1; diff --git a/sapi/cli/tests/gh8827-002.phpt b/sapi/cli/tests/gh8827-002.phpt index 00fd5cfa78f7..72e2aeb077c3 100644 --- a/sapi/cli/tests/gh8827-002.phpt +++ b/sapi/cli/tests/gh8827-002.phpt @@ -32,10 +32,20 @@ print "STDOUT:\n"; fclose(STDOUT); var_dump(@fopen('php://stdout', 'a')); ?> ---EXPECT-- +--EXPECTF-- STDIN: + +Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d bool(false) + +Deprecated: var_dump(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d STDERR: + +Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d bool(false) + +Deprecated: var_dump(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d STDOUT: + +Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d bool(false) diff --git a/sapi/cli/tests/gh8827-003.phpt b/sapi/cli/tests/gh8827-003.phpt index 11f7880770ed..12b6798a57c2 100644 --- a/sapi/cli/tests/gh8827-003.phpt +++ b/sapi/cli/tests/gh8827-003.phpt @@ -34,6 +34,7 @@ file_put_contents('php://fd/2', "Goes to stderrFile\n"); ob_start(function ($buffer) use ($stdoutStream) { fwrite($stdoutStream, $buffer); + return ''; }, 1); print "stdoutFile:\n"; diff --git a/tests/output/bug60768.phpt b/tests/output/bug60768.phpt index 4de7fbd69ff4..1ab702fa6468 100644 --- a/tests/output/bug60768.phpt +++ b/tests/output/bug60768.phpt @@ -5,7 +5,7 @@ Bug #60768 Output buffer not discarded global $storage; -ob_start(function($buffer) use (&$storage) { $storage .= $buffer; }, 20); +ob_start(function($buffer) use (&$storage) { $storage .= $buffer; return ''; }, 20); echo str_repeat("0", 20); // fill in the buffer diff --git a/tests/output/ob_start_basic_002.phpt b/tests/output/ob_start_basic_002.phpt index 700dab5d3c38..e9af2b5e1904 100644 --- a/tests/output/ob_start_basic_002.phpt +++ b/tests/output/ob_start_basic_002.phpt @@ -35,19 +35,24 @@ foreach ($callbacks as $callback) { } ?> ---EXPECT-- +--EXPECTF-- --> Use callback 'return_empty_string': --> Use callback 'return_false': + +Deprecated: ob_end_flush(): Returning a non-string result from user output handler return_false is deprecated in %s on line %d My output. --> Use callback 'return_null': +Deprecated: ob_end_flush(): Returning a non-string result from user output handler return_null is deprecated in %s on line %d + --> Use callback 'return_string': I stole your output. --> Use callback 'return_zero': -0 +Deprecated: ob_end_flush(): Returning a non-string result from user output handler return_zero is deprecated in %s on line %d +0 diff --git a/tests/output/ob_start_callback_bad_return/exception_handler.phpt b/tests/output/ob_start_callback_bad_return/exception_handler.phpt new file mode 100644 index 000000000000..efe9ffc4516c --- /dev/null +++ b/tests/output/ob_start_callback_bad_return/exception_handler.phpt @@ -0,0 +1,96 @@ +--TEST-- +ob_start(): Check behaviour with deprecation converted to exception +--FILE-- +>>"; + return null; +} + +function return_false($string) { + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + return false; +} + +function return_true($string) { + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + return true; +} + +function return_zero($string) { + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + return 0; +} + +$cases = ['return_null', 'return_false', 'return_true', 'return_zero']; +foreach ($cases as $case) { + $log = []; + echo "\n\nTesting: $case\n"; + ob_start($case); + echo "Inside of $case\n"; + try { + ob_end_flush(); + } catch (\ErrorException $e) { + echo $e . "\n"; + } + echo "\nEnd of $case, log was:\n"; + echo implode("\n", $log); +} + +?> +--EXPECTF-- +Testing: return_null +ErrorException: ob_end_flush(): Returning a non-string result from user output handler return_null is deprecated in %s:%d +Stack trace: +#0 [internal function]: {closure:%s:%d}(8192, 'ob_end_flush():...', %s, %d) +#1 %s(%d): ob_end_flush() +#2 {main} + +End of return_null, log was: +return_null: <<>> + +Testing: return_false +Inside of return_false +ErrorException: ob_end_flush(): Returning a non-string result from user output handler return_false is deprecated in %s:%d +Stack trace: +#0 [internal function]: {closure:%s:%d}(8192, 'ob_end_flush():...', %s, %d) +#1 %s(%d): ob_end_flush() +#2 {main} + +End of return_false, log was: +return_false: <<>> + +Testing: return_true +ErrorException: ob_end_flush(): Returning a non-string result from user output handler return_true is deprecated in %s:%d +Stack trace: +#0 [internal function]: {closure:%s:%d}(8192, 'ob_end_flush():...', %s, %d) +#1 %s(%d): ob_end_flush() +#2 {main} + +End of return_true, log was: +return_true: <<>> + +Testing: return_zero +0ErrorException: ob_end_flush(): Returning a non-string result from user output handler return_zero is deprecated in %s:%d +Stack trace: +#0 [internal function]: {closure:%s:%d}(8192, 'ob_end_flush():...', %s, %d) +#1 %s(%d): ob_end_flush() +#2 {main} + +End of return_zero, log was: +return_zero: <<>> diff --git a/tests/output/ob_start_callback_bad_return/exception_handler_nested.phpt b/tests/output/ob_start_callback_bad_return/exception_handler_nested.phpt new file mode 100644 index 000000000000..29f17f99b649 --- /dev/null +++ b/tests/output/ob_start_callback_bad_return/exception_handler_nested.phpt @@ -0,0 +1,97 @@ +--TEST-- +ob_start(): Check behaviour with deprecation converted to exception +--FILE-- +>>"; + return null; +} + +function return_false($string) { + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + return false; +} + +function return_true($string) { + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + return true; +} + +function return_zero($string) { + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + return 0; +} + +ob_start('return_null'); +ob_start('return_false'); +ob_start('return_true'); +ob_start('return_zero'); + +echo "In all of them\n\n"; +try { + ob_end_flush(); +} catch (\ErrorException $e) { + echo $e->getMessage() . "\n"; +} +echo "Ended return_zero handler\n\n"; + +try { + ob_end_flush(); +} catch (\ErrorException $e) { + echo $e->getMessage() . "\n"; +} +echo "Ended return_true handler\n\n"; + +try { + ob_end_flush(); +} catch (\ErrorException $e) { + echo $e->getMessage() . "\n"; +} +echo "Ended return_false handler\n\n"; + +try { + ob_end_flush(); +} catch (\ErrorException $e) { + echo $e->getMessage() . "\n"; +} +echo "Ended return_null handler\n\n"; + +echo "All handlers are over\n\n"; +echo implode("\n", $log); + +?> +--EXPECT-- +ob_end_flush(): Returning a non-string result from user output handler return_null is deprecated +Ended return_null handler + +All handlers are over + +return_zero: <<>> +return_true: <<<0ob_end_flush(): Returning a non-string result from user output handler return_zero is deprecated +Ended return_zero handler + +>>> +return_false: <<>> +return_null: <<>> diff --git a/tests/output/ob_start_callback_bad_return/handler_false_removed.phpt b/tests/output/ob_start_callback_bad_return/handler_false_removed.phpt new file mode 100644 index 000000000000..1614e7f6e4d7 --- /dev/null +++ b/tests/output/ob_start_callback_bad_return/handler_false_removed.phpt @@ -0,0 +1,21 @@ +--TEST-- +ob_start(): Check behaviour with deprecation when OOM triggers handler removal (handler returns false) +--FILE-- + +--EXPECTF-- +Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d + +Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d diff --git a/tests/output/ob_start_callback_bad_return/handler_true_removed.phpt b/tests/output/ob_start_callback_bad_return/handler_true_removed.phpt new file mode 100644 index 000000000000..af8f5fe48a40 --- /dev/null +++ b/tests/output/ob_start_callback_bad_return/handler_true_removed.phpt @@ -0,0 +1,21 @@ +--TEST-- +ob_start(): Check behaviour with deprecation when OOM triggers handler removal (handler returns true) +--FILE-- + +--EXPECTF-- +Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d + +Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d diff --git a/tests/output/ob_start_callback_bad_return/handler_zero_removed.phpt b/tests/output/ob_start_callback_bad_return/handler_zero_removed.phpt new file mode 100644 index 000000000000..f9bbd7d37927 --- /dev/null +++ b/tests/output/ob_start_callback_bad_return/handler_zero_removed.phpt @@ -0,0 +1,21 @@ +--TEST-- +ob_start(): Check behaviour with deprecation when OOM triggers handler removal (handler returns zero) +--FILE-- + +--EXPECTF-- +Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d + +Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d diff --git a/tests/output/ob_start_callback_bad_return/multiple_handlers.phpt b/tests/output/ob_start_callback_bad_return/multiple_handlers.phpt new file mode 100644 index 000000000000..fef8daf8783a --- /dev/null +++ b/tests/output/ob_start_callback_bad_return/multiple_handlers.phpt @@ -0,0 +1,89 @@ +--TEST-- +ob_start(): Check behaviour with multiple nested handlers with had return values +--FILE-- +>>"; + return $string; +} + +function return_empty_string($string) { + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + return ""; +} + +function return_false($string) { + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + return false; +} + +function return_true($string) { + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + return true; +} + +function return_null($string) { + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + return null; +} + +function return_string($string) { + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + return "I stole your output."; +} + +function return_zero($string) { + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + return 0; +} + +ob_start('return_given_string'); +ob_start('return_empty_string'); +ob_start('return_false'); +ob_start('return_true'); +ob_start('return_null'); +ob_start('return_string'); +ob_start('return_zero'); + +echo "Testing..."; + +ob_end_flush(); +ob_end_flush(); +ob_end_flush(); +ob_end_flush(); +ob_end_flush(); +ob_end_flush(); +ob_end_flush(); + +echo "\n\nLog:\n"; +echo implode("\n", $log); +?> +--EXPECTF-- +Log: +return_zero: <<>> +return_string: <<< +Deprecated: ob_end_flush(): Returning a non-string result from user output handler return_zero is deprecated in %s on line %d +0>>> +return_null: <<>> +return_true: <<< +Deprecated: ob_end_flush(): Returning a non-string result from user output handler return_null is deprecated in %s on line %d +>>> +return_false: <<< +Deprecated: ob_end_flush(): Returning a non-string result from user output handler return_true is deprecated in %s on line %d +>>> +return_empty_string: <<< +Deprecated: ob_end_flush(): Returning a non-string result from user output handler return_false is deprecated in %s on line %d + +Deprecated: ob_end_flush(): Returning a non-string result from user output handler return_true is deprecated in %s on line %d +>>> +return_given_string: <<<>>>