diff --git a/NEWS b/NEWS index 0817b78f6598..5276069926e5 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,10 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ?? ??? ????, PHP 8.6.0beta3 +- CLI: + . Fixed bug GH-23242 (PHP development server does not support Expect + 100-continue flow control). (Sjoerd Langkemper) + 27 Aug 2026, PHP 8.6.0beta2 diff --git a/UPGRADING b/UPGRADING index 4a5d055a57c4..aa6e15aaea6a 100644 --- a/UPGRADING +++ b/UPGRADING @@ -24,9 +24,9 @@ PHP 8.6 UPGRADE NOTES has materialized the property by writing into the property table. The freshly-written value is returned directly. isset() is unaffected. -- COM - . It is no longer possible to clone variant objects, this is because - the cloning behavior was ill defined. +- COM: + . It is no longer possible to clone variant objects because the cloning + behavior was ill-defined. - Curl: . The callback registered with CURLOPT_READFUNCTION now throws a ValueError @@ -35,10 +35,10 @@ PHP 8.6 UPGRADE NOTES - DOM: . Properties previously documented as @readonly (e.g. DOMNode::$nodeType, - DOMDocument::$xmlEncoding, DOMEntity::$actualEncoding, ::$encoding, - ::$version) are now declared with asymmetric visibility - (public private(set)). Attempts to write to them from outside the - class now raise "Cannot modify private(set) property ::$ + DOMDocument::$xmlEncoding, DOMEntity::$actualEncoding, + DOMEntity::$encoding, DOMEntity::$version) are now declared with asymmetric + visibility (public private(set)). Attempts to write to them from outside + the class now raise "Cannot modify private(set) property ::$ from global scope" instead of the prior readonly modification error. ReflectionProperty::isWritable() also reports these properties accurately. @@ -49,8 +49,8 @@ PHP 8.6 UPGRADE NOTES smaller index. - GD: - . imagesetstyle(), imagefilter() and imagecrop() filter their array arguments - types / values and raise a TypeError / ValueError accordingly. + . imagesetstyle(), imagefilter() and imagecrop() filter the types / values of + their array arguments and raise a TypeError / ValueError accordingly. . imageaffinematrixget() now enforces the documented array|float type for the $options parameter, including the corresponding weak and strict typing behavior. @@ -92,7 +92,7 @@ PHP 8.6 UPGRADE NOTES - PCNTL: . pcntl_alarm() now raises a ValueError if the seconds argument is - lower than zero or greater than platform's UINT_MAX. + lower than zero or greater than the platform's UINT_MAX. . pcntl_exec() now raises a ValueError if the $args argument is not a list array. @@ -180,8 +180,8 @@ PHP 8.6 UPGRADE NOTES - SimpleXML: . SimpleXMLElement::__construct() now raises a ValueError when the $data argument contains NUL bytes, matching simplexml_load_file(). With - $dataIsURL set it previously truncated the path at the first NUL byte. - Without it the string went to libxml, which at default options rejects a + $dataIsURL set, it previously truncated the path at the first NUL byte. + Without it, the string went to libxml, which with default options rejects a NUL on current versions but accepts the truncated document on older ones and under LIBXML_RECOVER. @@ -220,7 +220,7 @@ PHP 8.6 UPGRADE NOTES . SplFileObject::next() past EOF no longer increments key() without bound. SplFileObject::seek() past EOF now produces the same key() value as SplTempFileObject; the two previously returned different values. - . DirectoryIterator::key() now returns int|string, + . DirectoryIterator::key() now returns int|string, and DirectoryIterator::current() returns string|SplFileInfo|static. - Standard: @@ -232,14 +232,14 @@ PHP 8.6 UPGRADE NOTES for empty arrays or converting values, so an invalid later argument can suppress conversion side effects from earlier arrays. Values are not converted if any input array is empty. - . Form feed (\f) is now added in the default trimmed characters of trim(), + . Form feed (\f) is now added to the default trimmed characters of trim(), rtrim() and ltrim(). RFC: https://wiki.php.net/rfc/trim_form_feed . array_filter() now raises a ValueError when an invalid $mode argument value is passed. . array_change_key_case() now raises a ValueError when an invalid $case argument value is passed. - . getenv() and putenv() now raises a ValueError when the first argument + . getenv() and putenv() now raise a ValueError when the first argument contains NUL bytes. . dl() now raises a ValueError when the $extension_filename argument contains NUL bytes. @@ -267,7 +267,7 @@ PHP 8.6 UPGRADE NOTES UINT_MAX instead of allowing the value to overflow. . proc_open() now raises a ValueError when the $cwd argument contains NUL bytes. - . base_convert(), bindex(), hexdec() and octdec() now raise a notice when + . base_convert(), bindec(), hexdec() and octdec() now raise a notice when they cannot precisely convert the given number. . The following functions now raise a ValueError when the $filename argument contains NUL bytes: @@ -323,7 +323,7 @@ PHP 8.6 UPGRADE NOTES - Core: . Readonly properties may now declare default values. RFC: https://wiki.php.net/rfc/readonly_property_defaults - . It is now possible to use reference assign on WeakMap without the key + . It is now possible to use reference assignment on WeakMap without the key needing to be present beforehand. . It is now possible to define the __debugInfo() magic method on enums. RFC: https://wiki.php.net/rfc/debugable-enums @@ -371,8 +371,8 @@ PHP 8.6 UPGRADE NOTES IntlDatePatternGenerator::getBaseSkeleton() to generate the unique skeleton and base skeleton for a date/time pattern. . Added Locale::getDisplayKeyword() and Locale::getDisplayKeywordValue(), - with the alias of locale_get_display_keyword() and - locale_get_display_keyword_value() respectively. + with the aliases locale_get_display_keyword() and + locale_get_display_keyword_value(), respectively. RFC: https://wiki.php.net/rfc/getdisplaykeyword_and_getdisplaykeywordvalue . Added IntlNumberRangeFormatter class to format an interval of two numbers with a given skeleton, locale, IntlNumberRangeFormatter::COLLAPSE_AUTO, @@ -383,13 +383,13 @@ PHP 8.6 UPGRADE NOTES IntlNumberRangeFormatter::IDENTITY_FALLBACK_APPROXIMATELY_OR_SINGLE_VALUE, IntlNumberRangeFormatter::IDENTITY_FALLBACK_APPROXIMATELY and IntlNumberRangeFormatter::IDENTITY_FALLBACK_RANGE identity fallbacks. - It is supported from icu 63. + It is supported as of ICU 63. . Added SpoofChecker::areBidiConfusable() to check whether two strings are confusable for a given text direction, along with the SpoofChecker::LTR and SpoofChecker::RTL direction constants. - It is supported from icu 74. + It is supported as of ICU 74. . Added SpoofChecker::getBidiSkeleton() to generate a confusable skeleton for - a given text direction. It is supported from icu 74. + a given text direction. It is supported as of ICU 74. . Added SpoofChecker::getSkeleton() to generate a confusable skeleton for a given string. @@ -423,10 +423,10 @@ PHP 8.6 UPGRADE NOTES This makes it possible to override the timestamp and names of files. - SNMP: - . It is now possible to use the AES192, AES192C, AES256, and AES256C as + . It is now possible to use AES192, AES192C, AES256, and AES256C as SNMPv3 security protocols if the underlying library supports them. RFC: https://wiki.php.net/rfc/snmp_improvements_2026#increase_the_number_of_snmpv3_security_protocols_supported - . It is now possible to reset the MIB tree using the new snmp_read_mib() + . It is now possible to reset the MIB tree using the new snmp_init_mib() function. RFC: https://wiki.php.net/rfc/snmp_improvements_2026#allow_the_snmp_mib_to_be_reset . Additional MIB parsing and output control functionality has been exposed @@ -465,11 +465,11 @@ PHP 8.6 UPGRADE NOTES sockets. A positive value enables lingering for that many seconds, zero or a negative value disables it. Values above 65535 are clamped as the linger time is limited to an unsigned short on some platforms. - . Allowed casting filtered streams as file descriptor for select. - . Added the "write_seek_mode stream" filter parameter for the bz2, iconv, + . Allowed casting filtered streams as file descriptors for select. + . Added the "write_seek_mode" filter parameter for the bz2, iconv, zlib, and string stream filters. This parameter must be set via an - associative array where the key is "write_seek_mode stream" and the - value is one of the following strings "preserve", "reset", or "strict". + associative array where the key is "write_seek_mode" and the + value is one of the following strings: "preserve", "reset", or "strict". - URI: . Added Uri\Rfc3986\Uri::getUriType() and Uri\WhatWg\Url::isSpecialScheme(). @@ -486,7 +486,7 @@ PHP 8.6 UPGRADE NOTES ======================================== - CLI: - . The built-in development server now accepts requests using the QUERY HTTP + . The built-in development server now accepts requests using the HTTP QUERY method instead of returning 501 Not Implemented. ======================================== @@ -509,12 +509,12 @@ PHP 8.6 UPGRADE NOTES . Passing a 3rd argument to define() is now deprecated. RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_define_with_case_insensitive_being_specified -- BZ2 +- BZ2: . Passing an object for the Bzip2 {de}compression stream filter is now deprecated. Use get_object_vars() on the object instead. RFC: https://wiki.php.net/rfc/deprecations_php_8_6#passing_objects_as_parameters_to_the_bzip2decompress_and_bzip2compress_stream_filters -- GMP +- GMP: . The shift (<<, >>) and exponentiation (**) operators on GMP objects now emit a deprecation warning when converting a float right operand to int loses precision. @@ -523,14 +523,15 @@ PHP 8.6 UPGRADE NOTES . Mbregex has been deprecated, because the underlying Oniguruma library is no longer maintained. RFC: https://wiki.php.net/rfc/eol-oniguruma - . Passing objects to mb_convert_variables() is now deprecated + . Passing objects to mb_convert_variables() is now deprecated. RFC: https://wiki.php.net/rfc/deprecations_php_8_6#passing_objects_for_vars_parameter_of_mb_convert_variables - MySQLi: . The mysqli_get_charset() function and mysqli::get_charset() method are now deprecated. RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_mysqli_get_charset - . The mysqli_stmt_init() function, mysqli::stmt_init() method, and calling mysqli_stmt constructor - without providing the $query parameter are now deprecated. + . The mysqli_stmt_init() function, mysqli::stmt_init() method, and calling the + mysqli_stmt constructor without providing the $query parameter are now + deprecated. RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_mysqlistmt_init - Reflection: @@ -555,10 +556,10 @@ PHP 8.6 UPGRADE NOTES RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_passing_a_sessionhandler_object_to_session_set_save_handler_which_does_not_contain_the_create_sid_and_validateid - SPL: - . The spl_classes() function is now deprecated, use + . The spl_classes() function is now deprecated. Use ReflectionExtension::getClassNames() instead. RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_spl_classes - . The spl_object_hash() function is now deprecated, use spl_object_id() + . The spl_object_hash() function is now deprecated. Use spl_object_id() instead. RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_spl_object_hash . The following ArrayIterator methods are now deprecated: @@ -573,7 +574,7 @@ PHP 8.6 UPGRADE NOTES * ArrayIterator::unserialize() * ArrayIterator::serialize() RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_arrayiterator_methods_that_inherit_arrayobject_implementation - . The following SplFileObject methods are now deprecated + . The following SplFileObject methods are now deprecated: * SplFileObject::fgetcsv() * SplFileObject::fputcsv() * SplFileObject::setCsvControl() @@ -591,29 +592,28 @@ PHP 8.6 UPGRADE NOTES . Passing an object to array_walk{_recursive} is now deprecated. Use get_object_vars() on the object instead. RFC: https://wiki.php.net/rfc/deprecations_php_8_6#passing_objects_for_array_parameter_of_array_walk_and_array_walk_recursive - . The is_double() function is now deprecated, use is_float() instead. + . The is_double() function is now deprecated. Use is_float() instead. RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_is_double - . The is_long() and is_integer() functions are now deprecated, use is_int() + . The is_long() and is_integer() functions are now deprecated. Use is_int() instead. RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_is_integer RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_is_long - . The doubleval() function is now deprecated, use floatval() instead. + . The doubleval() function is now deprecated. Use floatval() instead. RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_doubleval - . The strcoll() function is now deprecated, use Collator::compare() instead. + . The strcoll() function is now deprecated. Use Collator::compare() instead. RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_strcoll . The SORT_LOCALE_STRING constant for the family of sort functions is now - deprecated, use one of the following functions instead: + deprecated. Use one of the following functions instead: * Collator::sort() * Collator::asort() - * Collator::sort() * Collator::sortWithSortKeys() RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_sort_locale_string_flag_for_sort_functions -- Zlib +- Zlib: . Passing an object for the zlib deflate and inflate stream filter is now deprecated. Use get_object_vars() on the object instead. RFC: https://wiki.php.net/rfc/deprecations_php_8_6#passing_objects_as_parameters_to_the_zlibinflate_and_zlibdeflate_stream_filters - . Passing an object to the $option argument to deflate_init and inflate_init + . Passing an object as the $option argument to deflate_init and inflate_init is now deprecated. Use get_object_vars() on the object instead. RFC: https://wiki.php.net/rfc/deprecations_php_8_6#passing_objects_for_options_parameter_of_deflate_init_and_inflate_init @@ -622,7 +622,7 @@ PHP 8.6 UPGRADE NOTES ======================================== - GMP: - . gmp_fact() now throws a ValueError() if $num does not fit into a unsigned + . gmp_fact() now throws a ValueError if $num does not fit into an unsigned long. . gmp_pow(), gmp_binomial(), gmp_root() and gmp_rootrem() now throw a ValueError if their second argument does not fit into an unsigned long. @@ -652,9 +652,15 @@ PHP 8.6 UPGRADE NOTES - Phar: . Phar::mungServer() now supports reference values. +- Readline: + . readline_completion_function() now declares true as its return type. The + function assigns a static callback and then tests whether the assignment + landed, which is a tautology; an invalid callback throws a TypeError via + ZPP before the function body is reached. + - Sockets: - . socket_addrinfo_lookup() now has an additional optional argument $error - when not null, and on failure, gives the error code (one of the EAI_* + . socket_addrinfo_lookup() now has an additional optional argument $error_code + that, when not null, receives the error code on failure (one of the EAI_* constants). . socket_cmsg_space() return type has been narrowed from ?int to int. Every failure path has thrown a ValueError since PHP 8.0, so null was never @@ -662,8 +668,11 @@ PHP 8.6 UPGRADE NOTES - Standard: . header_register_callback() now declares true as its return type. It has not - been able to return false since PHP 8.0.0, where passing an invalid + been able to return false since PHP 8.0.0, when passing an invalid callback started throwing a TypeError instead. + . register_tick_function() now declares true as its return type. It has + always returned true on success; an invalid callback throws a TypeError + via ZPP before the function body is reached. . ini_get_all() now includes a "builtin_default_value" element for each directive when $details is true. It holds the built-in default value of the directive (or null if it has none), independent of values set in php.ini, @@ -713,7 +722,7 @@ PHP 8.6 UPGRADE NOTES . snmp_set_string_output_format() - Standard: - . clamp() returns the given value if in range, else return the nearest + . clamp() returns the given value if in range, else returns the nearest bound. RFC: https://wiki.php.net/rfc/clamp_v2 . stream_last_errors() and stream_clear_errors() @@ -800,7 +809,7 @@ PHP 8.6 UPGRADE NOTES - MySQLi: . Added new constant MYSQLI_OPT_COMPRESS. -- Opcache +- Opcache: . JIT is now supported for ZTS builds on Apple Silicon. ======================================== @@ -844,7 +853,7 @@ PHP 8.6 UPGRADE NOTES . EAI_INTR. . EAI_IDN_ENCODE. -- Standard +- Standard: . ARRAY_FILTER_USE_VALUE. . STREAM_CRYPTO_STATUS_NONE. . STREAM_CRYPTO_STATUS_WANT_READ. @@ -863,7 +872,7 @@ PHP 8.6 UPGRADE NOTES - MySQLi: . mysqli.default_port now checks the validity of the value which should be - between 0 and 65535 included. + between 0 and 65535 inclusive. - Opcache: . opcache.jit_debug accepts a new flag: ZEND_JIT_DEBUG_TRACE_EXIT_INFO_SRC. @@ -914,7 +923,7 @@ PHP 8.6 UPGRADE NOTES JIT. . The performance of the TAILCALL VM has been improved. . The TAILCALL VM is now enabled on Windows when compiling with Clang >= 19 - x86_64. + on x86_64. . The performance of ZTS builds has been improved. . Added stateless closure cache. RFC: https://wiki.php.net/rfc/closure-optimizations#stateless_closure_caching @@ -951,7 +960,7 @@ PHP 8.6 UPGRADE NOTES - URI: . Improved performance of Uri\WhatWg\Url::parse() when collecting validation errors by pre-allocating the error array. - . Reduced allocations when reading IPv6/IPFuture hosts and paths with + . Reduced allocations when reading IPv6/IPvFuture hosts and paths with Uri\Rfc3986\Uri. . Improved performance and memory consumption when using normalizing (non-raw) getters on already-normalized URIs with Uri\Rfc3986\Uri. diff --git a/ext/readline/readline.c b/ext/readline/readline.c index cbc0ebdd8713..d9ec274996c7 100644 --- a/ext/readline/readline.c +++ b/ext/readline/readline.c @@ -489,8 +489,8 @@ PHP_FUNCTION(readline_completion_function) /* NOTE: The rl_attempted_completion_function variable (and others) are part of the readline library, not php */ rl_attempted_completion_function = php_readline_completion_cb; - - RETURN_BOOL(rl_attempted_completion_function != NULL); + + RETURN_TRUE; } /* }}} */ diff --git a/ext/readline/readline.stub.php b/ext/readline/readline.stub.php index be445df21415..4dae1237c605 100644 --- a/ext/readline/readline.stub.php +++ b/ext/readline/readline.stub.php @@ -29,7 +29,7 @@ function readline_read_history(?string $filename = null): bool {} function readline_write_history(?string $filename = null): bool {} -function readline_completion_function(callable $callback): bool {} +function readline_completion_function(callable $callback): true {} #ifdef HAVE_RL_CALLBACK_READ_CHAR diff --git a/ext/readline/readline_arginfo.h b/ext/readline/readline_arginfo.h index 689d5f762eef..1ce08c443eeb 100644 --- a/ext/readline/readline_arginfo.h +++ b/ext/readline/readline_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit readline.stub.php instead. - * Stub hash: 7a314f75d9a89a9ea4d525515bb6bacdf7be6746 */ + * Stub hash: 848e798481f62ee09cfd8cc3dfa6b0814cfdd979 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_readline, 0, 0, MAY_BE_STRING|MAY_BE_FALSE) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, prompt, IS_STRING, 1, "null") @@ -28,7 +28,7 @@ ZEND_END_ARG_INFO() #define arginfo_readline_write_history arginfo_readline_read_history -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_readline_completion_function, 0, 1, _IS_BOOL, 0) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_readline_completion_function, 0, 1, IS_TRUE, 0) ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0) ZEND_END_ARG_INFO() diff --git a/ext/standard/basic_functions.stub.php b/ext/standard/basic_functions.stub.php index 42cb711512b8..3e23934cbc78 100644 --- a/ext/standard/basic_functions.stub.php +++ b/ext/standard/basic_functions.stub.php @@ -2063,7 +2063,7 @@ function getprotobyname(string $protocol): int|false {} function getprotobynumber(int $protocol): string|false {} #endif -function register_tick_function(callable $callback, mixed ...$args): bool {} +function register_tick_function(callable $callback, mixed ...$args): true {} function unregister_tick_function(callable $callback): void {} diff --git a/ext/standard/basic_functions_arginfo.h b/ext/standard/basic_functions_arginfo.h index a057d3d48be3..442085e9d6cc 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 basic_functions.stub.php instead. - * Stub hash: 13b6fd340958d1a7c782c8f5f3685517e62e5edc + * Stub hash: c645e310c00d9f4cb3856c94ee60d06071e28de0 * Has decl header: yes */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_set_time_limit, 0, 1, _IS_BOOL, 0) @@ -574,7 +574,7 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_getprotobynumber, 0, 1, MAY_BE_S ZEND_END_ARG_INFO() #endif -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_register_tick_function, 0, 1, _IS_BOOL, 0) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_register_tick_function, 0, 1, IS_TRUE, 0) ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0) ZEND_ARG_VARIADIC_TYPE_INFO(0, args, IS_MIXED, 0) ZEND_END_ARG_INFO() diff --git a/ext/standard/basic_functions_decl.h b/ext/standard/basic_functions_decl.h index ab0a8ec94f7b..f2f234f60cc2 100644 --- a/ext/standard/basic_functions_decl.h +++ b/ext/standard/basic_functions_decl.h @@ -1,8 +1,8 @@ /* This is a generated file, edit basic_functions.stub.php instead. - * Stub hash: 13b6fd340958d1a7c782c8f5f3685517e62e5edc */ + * Stub hash: c645e310c00d9f4cb3856c94ee60d06071e28de0 */ -#ifndef ZEND_BASIC_FUNCTIONS_DECL_13b6fd340958d1a7c782c8f5f3685517e62e5edc_H -#define ZEND_BASIC_FUNCTIONS_DECL_13b6fd340958d1a7c782c8f5f3685517e62e5edc_H +#ifndef ZEND_BASIC_FUNCTIONS_DECL_c645e310c00d9f4cb3856c94ee60d06071e28de0_H +#define ZEND_BASIC_FUNCTIONS_DECL_c645e310c00d9f4cb3856c94ee60d06071e28de0_H typedef enum zend_enum_SortDirection { ZEND_ENUM_SortDirection_Ascending = 1, @@ -20,4 +20,4 @@ typedef enum zend_enum_RoundingMode { ZEND_ENUM_RoundingMode_PositiveInfinity = 8, } zend_enum_RoundingMode; -#endif /* ZEND_BASIC_FUNCTIONS_DECL_13b6fd340958d1a7c782c8f5f3685517e62e5edc_H */ +#endif /* ZEND_BASIC_FUNCTIONS_DECL_c645e310c00d9f4cb3856c94ee60d06071e28de0_H */ diff --git a/sapi/cli/php_cli_server.c b/sapi/cli/php_cli_server.c index 5df12648ca84..9f13f0647700 100644 --- a/sapi/cli/php_cli_server.c +++ b/sapi/cli/php_cli_server.c @@ -176,6 +176,7 @@ typedef struct php_cli_server_client { bool request_read; bool too_large_post; bool headers_written; + bool expect_continue; zend_string *current_header_name; zend_string *current_header_value; enum { HEADER_NONE=0, HEADER_FIELD, HEADER_VALUE } last_header_element; @@ -1794,6 +1795,13 @@ static int php_cli_server_client_read_request_on_headers_complete(php_http_parse return 2; } + zval *expect_val = zend_hash_str_find(&client->request.headers, "expect", sizeof("expect") - 1); + if (expect_val && Z_TYPE_P(expect_val) == IS_STRING + && zend_string_equals_literal_ci(Z_STR_P(expect_val), "100-continue") + && parser->http_major == 1 && parser->http_minor == 1) { + client->expect_continue = true; + } + return 0; } @@ -1901,6 +1909,23 @@ static int php_cli_server_client_read_request(php_cli_server_client *client, cha return -1; } + if (client->expect_continue && !client->request_read) { + /* Parser completed headers with Expect: 100-continue but hasn't + * finished reading the body. Send 100 Continue before the client + * sends the request body. Only supported in HTTP/1.1. */ + static const char continue_response[] = "HTTP/1.1 100 Continue\r\n\r\n"; + bool send_success = false; + client->expect_continue = false; + zend_try { + size_t sent = php_cli_server_client_send_through(client, continue_response, strlen(continue_response)); + send_success = sent == strlen(continue_response); + } zend_end_try(); + if (!send_success) { + *errstr = php_socket_strerror(php_socket_errno(), NULL, 0); + return -1; + } + } + return client->request_read ? 1: 0; } /* }}} */ @@ -1985,6 +2010,7 @@ static void php_cli_server_client_ctor(php_cli_server_client *client, php_cli_se client->request_read = false; client->too_large_post = false; client->headers_written = false; + client->expect_continue = false; client->last_header_element = HEADER_NONE; client->current_header_name = NULL; diff --git a/sapi/cli/tests/php_cli_server.inc b/sapi/cli/tests/php_cli_server.inc index feee2bbb5686..ec370753573c 100644 --- a/sapi/cli/tests/php_cli_server.inc +++ b/sapi/cli/tests/php_cli_server.inc @@ -5,6 +5,7 @@ class CliServerInfo { public function __construct( public string $docRoot, public $processHandle, + public $outputFile, ) {} } @@ -118,7 +119,7 @@ function php_cli_server_start( define("PHP_CLI_SERVER_PORT", $port); define("PHP_CLI_SERVER_ADDRESS", PHP_CLI_SERVER_HOSTNAME.":".PHP_CLI_SERVER_PORT); - return new CliServerInfo($doc_root, $handle); + return new CliServerInfo($doc_root, $handle, $output_file); } function php_cli_server_connect() { diff --git a/sapi/cli/tests/php_cli_server_expect_100_continue_curl.phpt b/sapi/cli/tests/php_cli_server_expect_100_continue_curl.phpt new file mode 100644 index 000000000000..37886fc25e1e --- /dev/null +++ b/sapi/cli/tests/php_cli_server_expect_100_continue_curl.phpt @@ -0,0 +1,35 @@ +--TEST-- +Expect 100-continue behavior in PHP development server (curl) +--SKIPIF-- + +--EXTENSIONS-- +curl +--FILE-- + +--EXPECT-- +int(0) +Did the PHP development server send a HTTP/1.1 100 Continue header? +bool(true) diff --git a/sapi/cli/tests/php_cli_server_expect_100_continue_iua.phpt b/sapi/cli/tests/php_cli_server_expect_100_continue_iua.phpt new file mode 100644 index 000000000000..c792630a9305 --- /dev/null +++ b/sapi/cli/tests/php_cli_server_expect_100_continue_iua.phpt @@ -0,0 +1,33 @@ +--TEST-- +Failure to send "100 Continue" is reported with ignore_user_abort=1 +--SKIPIF-- + +--FILE-- + 1, 'l_linger' => 0]); +fwrite($fp, "POST / HTTP/1.1\r\nExpect: 100-continue\r\nContent-Length: 4\r\n\r\n"); +fclose($fp); + +$output = ''; +for ($i = 0; $i < 100 && !str_contains($output, 'Invalid request'); $i++) { + usleep(50000); + $output = file_get_contents($server->outputFile); +} + +var_dump(str_contains($output, 'Invalid request'), str_contains($output, 'Unexpected EOF')); +?> +--CLEAN-- + +--EXPECT-- +bool(true) +bool(false) diff --git a/sapi/cli/tests/php_cli_server_expect_100_continue_socket.phpt b/sapi/cli/tests/php_cli_server_expect_100_continue_socket.phpt new file mode 100644 index 000000000000..0d386155721d --- /dev/null +++ b/sapi/cli/tests/php_cli_server_expect_100_continue_socket.phpt @@ -0,0 +1,86 @@ +--TEST-- +Expect 100-continue behavior in PHP development server (sockets) +--SKIPIF-- + +--FILE-- + 1, + 'l_linger' => 0, + ] + ); +} +stream_socket_shutdown($fp, STREAM_SHUT_RD); +fwrite($fp, "POST / HTTP/1.1\r\nExpect: 100-continue\r\nContent-Length: 4\r\nConnection: close\r\n\r\n"); +fclose($fp); + +$fp = php_cli_server_connect(); +fwrite($fp, "GET / HTTP/1.1\r\nConnection: close\r\n\r\n"); +echo fgets($fp); +fclose($fp); + +echo "# GET with Expect header (no body).\n"; +$fp = php_cli_server_connect(); +fwrite($fp, "GET / HTTP/1.1\r\nExpect: 100-continue\r\nConnection: close\r\n\r\n"); +echo fgets($fp); +fclose($fp); + +echo "# POST with empty body.\n"; +$fp = php_cli_server_connect(); +fwrite($fp, "POST / HTTP/1.1\r\nExpect: 100-continue\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"); +echo fgets($fp); +fclose($fp); + +echo "# Lower-case expect header.\n"; +$fp = php_cli_server_connect(); +fwrite($fp, "POST / HTTP/1.1\r\nexpect: 100-continue\r\nContent-Length: 4\r\nConnection: close\r\n\r\n"); +echo fgets($fp); +fclose($fp); +?> +--EXPECT-- +# Send Expect: 100-continue header, receive 100 Continue response. +HTTP/1.1 100 Continue + +HTTP/1.1 200 OK +# Send Expect: 100-continue header on HTTP/1.0. +int(0) +HTTP/1.0 200 OK +# Send Expect: 100-continue header and disconnect. +HTTP/1.1 200 OK +# GET with Expect header (no body). +HTTP/1.1 200 OK +# POST with empty body. +HTTP/1.1 200 OK +# Lower-case expect header. +HTTP/1.1 100 Continue