From dd65fe9a9a1521d7772ebb577a4931415fe85a65 Mon Sep 17 00:00:00 2001 From: Nikita Popov Date: Fri, 21 May 2021 12:15:49 +0200 Subject: [PATCH 1/3] Add headers_send_early_and_clear() function This is intended to be used in conjunction with HTTP Early Hints and other 1xx status codes, which require sending multiple sets of headers. Usage: header('HTTP/1.1 103 Early Hints'); header('Link: ; rel=preload; as=style'); headers_send_early_and_clear(); // Normal code goes here. You can send more headers and body // here. --- ext/phar/phar_object.c | 8 ++-- ext/standard/basic_functions.stub.php | 2 + ext/standard/basic_functions_arginfo.h | 6 ++- ext/standard/head.c | 20 +++++++- .../headers_send_early_hint.phpt | 20 ++++++++ .../headers_send_early_hint_2.phpt | 21 +++++++++ .../headers_send_early_hint_3.phpt | 14 ++++++ main/SAPI.c | 6 +-- main/SAPI.h | 2 +- run-tests.php | 2 +- sapi/cli/php_cli_server.c | 2 +- .../cli/tests/php_cli_server_early_hints.phpt | 47 +++++++++++++++++++ 12 files changed, 138 insertions(+), 12 deletions(-) create mode 100644 ext/standard/tests/general_functions/headers_send_early_hint.phpt create mode 100644 ext/standard/tests/general_functions/headers_send_early_hint_2.phpt create mode 100644 ext/standard/tests/general_functions/headers_send_early_hint_3.phpt create mode 100644 sapi/cli/tests/php_cli_server_early_hints.phpt diff --git a/ext/phar/phar_object.c b/ext/phar/phar_object.c index c012ac763b841..970a955956fe8 100644 --- a/ext/phar/phar_object.c +++ b/ext/phar/phar_object.c @@ -175,7 +175,7 @@ static int phar_file_action(phar_archive_data *phar, phar_entry_info *info, char sapi_header_op(SAPI_HEADER_REPLACE, &ctr); efree((void *) ctr.line); - if (FAILURE == sapi_send_headers()) { + if (FAILURE == sapi_send_headers(/* last_headers */ true)) { zend_bailout(); } @@ -300,7 +300,7 @@ static void phar_do_403(char *entry, size_t entry_len) /* {{{ */ ctr.line_len = sizeof("HTTP/1.0 403 Access Denied")-1; ctr.line = "HTTP/1.0 403 Access Denied"; sapi_header_op(SAPI_HEADER_REPLACE, &ctr); - sapi_send_headers(); + sapi_send_headers(/* last_headers */ true); PHPWRITE("\n \n Access Denied\n \n \n

403 - File ", sizeof("\n \n Access Denied\n \n \n

403 - File ") - 1); PHPWRITE("Access Denied

\n \n", sizeof("Access Denied\n \n") - 1); } @@ -324,7 +324,7 @@ static void phar_do_404(phar_archive_data *phar, char *fname, size_t fname_len, ctr.line_len = sizeof("HTTP/1.0 404 Not Found")-1; ctr.line = "HTTP/1.0 404 Not Found"; sapi_header_op(SAPI_HEADER_REPLACE, &ctr); - sapi_send_headers(); + sapi_send_headers(/* last_headers */ true); PHPWRITE("\n \n File Not Found\n \n \n

404 - File ", sizeof("\n \n File Not Found\n \n \n

404 - File ") - 1); PHPWRITE("Not Found

\n \n", sizeof("Not Found\n \n") - 1); } @@ -783,7 +783,7 @@ PHP_METHOD(Phar, webPhar) } sapi_header_op(SAPI_HEADER_REPLACE, &ctr); - sapi_send_headers(); + sapi_send_headers(/* last_headers */ true); efree((void *) ctr.line); zend_bailout(); } diff --git a/ext/standard/basic_functions.stub.php b/ext/standard/basic_functions.stub.php index 15abe44a0f2b9..4bb1740af7dba 100755 --- a/ext/standard/basic_functions.stub.php +++ b/ext/standard/basic_functions.stub.php @@ -520,6 +520,8 @@ function headers_sent(&$filename = null, &$line = null): bool {} function headers_list(): array {} +function headers_send_early_and_clear(): bool {} + /* {{{ html.c */ function htmlspecialchars(string $string, int $flags = ENT_QUOTES | ENT_SUBSTITUTE, ?string $encoding = null, bool $double_encode = true): string {} diff --git a/ext/standard/basic_functions_arginfo.h b/ext/standard/basic_functions_arginfo.h index 8e06402c0c536..43e9da96512ed 100644 --- a/ext/standard/basic_functions_arginfo.h +++ b/ext/standard/basic_functions_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 810b8bfbdf037702fcaec2ff81998c2bc2cefae8 */ + * Stub hash: f09f8df1b2704720615642414ea106471b139e33 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_set_time_limit, 0, 1, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO(0, seconds, IS_LONG, 0) @@ -770,6 +770,8 @@ ZEND_END_ARG_INFO() #define arginfo_headers_list arginfo_ob_list_handlers +#define arginfo_headers_send_early_and_clear arginfo_ob_flush + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_htmlspecialchars, 0, 1, IS_STRING, 0) ZEND_ARG_TYPE_INFO(0, string, IS_STRING, 0) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, flags, IS_LONG, 0, "ENT_QUOTES | ENT_SUBSTITUTE") @@ -2445,6 +2447,7 @@ ZEND_FUNCTION(setcookie); ZEND_FUNCTION(http_response_code); ZEND_FUNCTION(headers_sent); ZEND_FUNCTION(headers_list); +ZEND_FUNCTION(headers_send_early_and_clear); ZEND_FUNCTION(htmlspecialchars); ZEND_FUNCTION(htmlspecialchars_decode); ZEND_FUNCTION(html_entity_decode); @@ -3080,6 +3083,7 @@ static const zend_function_entry ext_functions[] = { ZEND_FE(http_response_code, arginfo_http_response_code) ZEND_FE(headers_sent, arginfo_headers_sent) ZEND_FE(headers_list, arginfo_headers_list) + ZEND_FE(headers_send_early_and_clear, arginfo_headers_send_early_and_clear) ZEND_FE(htmlspecialchars, arginfo_htmlspecialchars) ZEND_FE(htmlspecialchars_decode, arginfo_htmlspecialchars_decode) ZEND_FE(html_entity_decode, arginfo_html_entity_decode) diff --git a/ext/standard/head.c b/ext/standard/head.c index ab64fecf381aa..2311a0e7e36e9 100644 --- a/ext/standard/head.c +++ b/ext/standard/head.c @@ -69,7 +69,7 @@ PHP_FUNCTION(header_remove) PHPAPI int php_header(void) { - if (sapi_send_headers()==FAILURE || SG(request_info).headers_only) { + if (sapi_send_headers(/* last_headers */ true) == FAILURE || SG(request_info).headers_only) { return 0; /* don't allow output */ } else { return 1; /* allow output */ @@ -383,3 +383,21 @@ PHP_FUNCTION(http_response_code) RETURN_LONG(SG(sapi_headers).http_response_code); } /* }}} */ + +PHP_FUNCTION(headers_send_early_and_clear) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + if (SG(headers_sent)) { + php_error_docref(NULL, E_WARNING, "Headers already sent"); + RETURN_FALSE; + } + + if (sapi_send_headers(/* last_header */ false) == FAILURE) { + RETURN_FALSE; + } + + zend_llist_clean(&SG(sapi_headers).headers); + SG(sapi_headers).http_response_code = 200; + RETURN_TRUE; +} diff --git a/ext/standard/tests/general_functions/headers_send_early_hint.phpt b/ext/standard/tests/general_functions/headers_send_early_hint.phpt new file mode 100644 index 0000000000000..169a5482c2ed6 --- /dev/null +++ b/ext/standard/tests/general_functions/headers_send_early_hint.phpt @@ -0,0 +1,20 @@ +--TEST-- +Using headers_send_early_and_clear() for HTTP early hints (no extra headers) +--CGI-- +--FILE-- +; rel=preload; as=style'); +headers_send_early_and_clear(); +// Headers should be cleared. +var_dump(headers_list()); +?> +--EXPECTHEADERS-- +Status: 103 Early Hints +Link: ; rel=preload; as=style +--EXPECT-- +Content-type: text/html; charset=UTF-8 + +array(0) { +} diff --git a/ext/standard/tests/general_functions/headers_send_early_hint_2.phpt b/ext/standard/tests/general_functions/headers_send_early_hint_2.phpt new file mode 100644 index 0000000000000..2b4b43629d8c5 --- /dev/null +++ b/ext/standard/tests/general_functions/headers_send_early_hint_2.phpt @@ -0,0 +1,21 @@ +--TEST-- +Using headers_send_early_and_clear() for HTTP early hints (extra headers) +--CGI-- +--FILE-- +; rel=preload; as=style'); +headers_send_early_and_clear(); +header('Location: http://example.com/'); +echo "Foo\n"; +?> +--EXPECTHEADERS-- +Status: 103 Early Hints +Link: ; rel=preload; as=style +--EXPECT-- +Status: 302 Found +Location: http://example.com/ +Content-type: text/html; charset=UTF-8 + +Foo diff --git a/ext/standard/tests/general_functions/headers_send_early_hint_3.phpt b/ext/standard/tests/general_functions/headers_send_early_hint_3.phpt new file mode 100644 index 0000000000000..dd0caa117c083 --- /dev/null +++ b/ext/standard/tests/general_functions/headers_send_early_hint_3.phpt @@ -0,0 +1,14 @@ +--TEST-- +Using headers_send_early_and_clear() for HTTP early hints (after output) +--CGI-- +--FILE-- + +--EXPECTF-- +Foo + +Warning: headers_send_early_and_clear(): Headers already sent in %s on line %d +bool(false) diff --git a/main/SAPI.c b/main/SAPI.c index d3cc29ec02394..bf5b8bda03dee 100644 --- a/main/SAPI.c +++ b/main/SAPI.c @@ -821,7 +821,7 @@ SAPI_API int sapi_header_op(sapi_header_op_enum op, void *arg) } -SAPI_API int sapi_send_headers(void) +SAPI_API int sapi_send_headers(bool last_headers) { int retval; int ret = FAILURE; @@ -833,7 +833,7 @@ SAPI_API int sapi_send_headers(void) /* Success-oriented. We set headers_sent to 1 here to avoid an infinite loop * in case of an error situation. */ - if (SG(sapi_headers).send_default_content_type && sapi_module.send_headers) { + if (SG(sapi_headers).send_default_content_type && sapi_module.send_headers && last_headers) { uint32_t len = 0; char *default_mimetype = get_default_content_type(0, &len); @@ -863,7 +863,7 @@ SAPI_API int sapi_send_headers(void) zval_ptr_dtor(&cb); } - SG(headers_sent) = 1; + SG(headers_sent) = last_headers; if (sapi_module.send_headers) { retval = sapi_module.send_headers(&SG(sapi_headers)); diff --git a/main/SAPI.h b/main/SAPI.h index 97c52c41eccce..258c16b42a6c2 100644 --- a/main/SAPI.h +++ b/main/SAPI.h @@ -181,7 +181,7 @@ SAPI_API int sapi_add_header_ex(const char *header_line, size_t header_line_len, #define sapi_add_header(a, b, c) sapi_add_header_ex((a),(b),(c),1) -SAPI_API int sapi_send_headers(void); +SAPI_API int sapi_send_headers(bool last_headers); SAPI_API void sapi_free_header(sapi_header_struct *sapi_header); SAPI_API void sapi_handle_post(void *arg); SAPI_API size_t sapi_read_post_block(char *buffer, size_t buflen); diff --git a/run-tests.php b/run-tests.php index e046857c66e82..e1a88cc65e99d 100755 --- a/run-tests.php +++ b/run-tests.php @@ -2513,7 +2513,7 @@ function run_test(string $php, $file, array $env): string $headers = []; if (!empty($uses_cgi) && preg_match("/^(.*?)\r?\n\r?\n(.*)/s", $out, $match)) { - $output = trim($match[2]); + $output = str_replace("\r\n", "\n", trim($match[2])); $rh = preg_split("/[\n\r]+/", $match[1]); foreach ($rh as $line) { diff --git a/sapi/cli/php_cli_server.c b/sapi/cli/php_cli_server.c index 32f14f3cfbfd1..062c7892f57a1 100644 --- a/sapi/cli/php_cli_server.c +++ b/sapi/cli/php_cli_server.c @@ -524,7 +524,7 @@ static void sapi_cli_server_flush(void *server_context) /* {{{ */ } if (!SG(headers_sent)) { - sapi_send_headers(); + sapi_send_headers(/* last_headers */ true); SG(headers_sent) = 1; } } /* }}} */ diff --git a/sapi/cli/tests/php_cli_server_early_hints.phpt b/sapi/cli/tests/php_cli_server_early_hints.phpt new file mode 100644 index 0000000000000..2ba0b85cc4336 --- /dev/null +++ b/sapi/cli/tests/php_cli_server_early_hints.phpt @@ -0,0 +1,47 @@ +--TEST-- +PHP CLI server HTTP early hints +--SKIPIF-- + +--FILE-- +; rel=preload; as=style'); + headers_send_early_and_clear(); + header('Location: http://example.com/'); + echo "Foo\n"; + PHP); + +$host = PHP_CLI_SERVER_HOSTNAME; +$fp = php_cli_server_connect(); + +if (fwrite($fp, <<
+--EXPECTF-- +HTTP/1.1 103 Early Hints +Host: %s +Date: %s +Connection: close +X-Powered-By: PHP/%s +Link: ; rel=preload; as=style + +HTTP/1.1 302 Found +Host: localhost +Date: Fri, 21 May 2021 13:25:48 GMT +Connection: close +Location: http://example.com/ +Content-type: text/html; charset=UTF-8 + +Foo From abd62f03bab7748ac6158281e1ca378879255552 Mon Sep 17 00:00:00 2001 From: Nikita Popov Date: Fri, 21 May 2021 15:59:50 +0200 Subject: [PATCH 2/3] Don't include Date in test... --- sapi/cli/tests/php_cli_server_early_hints.phpt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sapi/cli/tests/php_cli_server_early_hints.phpt b/sapi/cli/tests/php_cli_server_early_hints.phpt index 2ba0b85cc4336..0467393841d6b 100644 --- a/sapi/cli/tests/php_cli_server_early_hints.phpt +++ b/sapi/cli/tests/php_cli_server_early_hints.phpt @@ -39,7 +39,7 @@ Link: ; rel=preload; as=style HTTP/1.1 302 Found Host: localhost -Date: Fri, 21 May 2021 13:25:48 GMT +Date: %s Connection: close Location: http://example.com/ Content-type: text/html; charset=UTF-8 From 1d03a7699ebcb8cd6fa46904afaf25dda48d0fb6 Mon Sep 17 00:00:00 2001 From: Nikita Popov Date: Fri, 21 May 2021 16:56:27 +0200 Subject: [PATCH 3/3] fix apache build --- sapi/apache2handler/sapi_apache2.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sapi/apache2handler/sapi_apache2.c b/sapi/apache2handler/sapi_apache2.c index f569e79a848e7..78020d5b9fcdb 100644 --- a/sapi/apache2handler/sapi_apache2.c +++ b/sapi/apache2handler/sapi_apache2.c @@ -295,7 +295,7 @@ php_apache_sapi_flush(void *server_context) r = ctx->r; - sapi_send_headers(); + sapi_send_headers(/* last_headers */ true); r->status = SG(sapi_headers).http_response_code; SG(headers_sent) = 1;