From 567bc5cb431ee912751cf5ce01791b4b26482610 Mon Sep 17 00:00:00 2001 From: Lazizbek Ergashev Date: Mon, 31 Aug 2026 21:45:02 +0500 Subject: [PATCH 1/8] Fix GH-23444: ODBC_ATTR_ASSUME_UTF8 corrupts Unicode data outside Windows (#23445) The attribute is documented as Windows only, and the UTF-8 conversion it relies on is compiled under #ifdef PHP_WIN32. Everywhere else pdo_odbc_sqltype_is_unicode() still reported wide types as Unicode, so parameters and columns were bound SQL_C_BINARY and then passed through unconverted. Raw UTF-8 reached the server for an nvarchar parameter and raw UTF-16 came back for an nvarchar column, and msodbcsql18 rejects a parameter of odd byte length with HY090. Report it as not Unicode outside Windows, which leaves the encoding to the driver as the default already does. On Windows the conversion runs but the data-at-exec branch declared the unconverted byte length in SQL_LEN_DATA_AT_EXEC() while SQLPutData() sent the converted bytes, so binding a non-ASCII parameter failed with 22026. Closes GH-23444 --- NEWS | 4 ++++ ext/pdo_odbc/odbc_stmt.c | 14 +++++++++++++- ext/pdo_odbc/tests/gh23444.phpt | 34 +++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 ext/pdo_odbc/tests/gh23444.phpt diff --git a/NEWS b/NEWS index 608bacc29109..927ebe121e8e 100644 --- a/NEWS +++ b/NEWS @@ -59,6 +59,10 @@ PHP NEWS . Fixed a leak when a persistent connection failed a liveness check with no other live PDO handle. (iliaal) +- PDO_ODBC: + . Fixed bug GH-23444 (ODBC_ATTR_ASSUME_UTF8 corrupts Unicode data outside + Windows). (Calvin Buckley, Lazizbek Ergashev) + - Phar: . Fixed bug GH-23418 (Use-after-free when looking up mounted directories). (Weilin Du) diff --git a/ext/pdo_odbc/odbc_stmt.c b/ext/pdo_odbc/odbc_stmt.c index 8786f2563e52..042c4684a887 100644 --- a/ext/pdo_odbc/odbc_stmt.c +++ b/ext/pdo_odbc/odbc_stmt.c @@ -34,6 +34,7 @@ enum pdo_odbc_conv_result { static int pdo_odbc_sqltype_is_unicode(pdo_odbc_stmt *S, SQLSMALLINT sqltype) { +#ifdef PHP_WIN32 if (!S->assume_utf8) return 0; switch (sqltype) { #ifdef SQL_WCHAR @@ -51,6 +52,9 @@ static int pdo_odbc_sqltype_is_unicode(pdo_odbc_stmt *S, SQLSMALLINT sqltype) default: return 0; } +#else + return 0; +#endif } static int pdo_odbc_utf82ucs2(pdo_stmt_t *stmt, int is_unicode, const char *buf, @@ -548,7 +552,15 @@ static int odbc_stmt_param_hook(pdo_stmt_t *stmt, struct pdo_bound_param_data *p break; } } else { - P->len = SQL_LEN_DATA_AT_EXEC(Z_STRLEN_P(parameter)); + zend_ulong ulen; + if (pdo_odbc_utf82ucs2(stmt, P->is_unicode, + Z_STRVAL_P(parameter), + Z_STRLEN_P(parameter), + &ulen) == PDO_ODBC_CONV_OK) { + P->len = SQL_LEN_DATA_AT_EXEC(ulen); + } else { + P->len = SQL_LEN_DATA_AT_EXEC(Z_STRLEN_P(parameter)); + } } } return 1; diff --git a/ext/pdo_odbc/tests/gh23444.phpt b/ext/pdo_odbc/tests/gh23444.phpt new file mode 100644 index 000000000000..f71fc733839f --- /dev/null +++ b/ext/pdo_odbc/tests/gh23444.phpt @@ -0,0 +1,34 @@ +--TEST-- +GH-23444 (Unicode data is corrupted with ODBC_ATTR_ASSUME_UTF8) +--EXTENSIONS-- +pdo_odbc +--SKIPIF-- + +--FILE-- +exec("CREATE TABLE gh23444 (v NVARCHAR(100))"); + +// 13 bytes as UTF-8, so an unconverted parameter is an odd number of bytes +$string = "\u{6e2c}\u{8a66}\u{4e2d}\u{1f418}"; + +$db->setAttribute(PDO::ODBC_ATTR_ASSUME_UTF8, true); +$stmt = $db->prepare("INSERT INTO gh23444 VALUES(?)"); +$stmt->execute([$string]); + +$stmt = $db->prepare("SELECT v FROM gh23444 WHERE v = ?"); +$stmt->execute([$string]); +var_dump($stmt->fetchColumn() === $string); +?> +--CLEAN-- +exec("DROP TABLE IF EXISTS gh23444"); +?> +--EXPECT-- +bool(true) From 1adadf07e5223b43612afe25e082fe33dd456830 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Mon, 24 Aug 2026 11:12:35 -0400 Subject: [PATCH 2/8] [intl] Fix empty-needle grapheme_strpos offsets grapheme_strpos_utf16() returned raw UTF-16 code-unit positions for an empty needle instead of grapheme counts, so multi-code-unit graphemes made strpos() and strrpos() over-report the offset. Convert the boundary position like the non-empty search path; the ASCII fast paths coincide and grapheme_strstr() consumes the separate raw UTF-16 out-parameter, which stays correct. Closes GH-23519 --- NEWS | 2 ++ ext/intl/grapheme/grapheme_util.c | 6 +++- .../grapheme_empty_offset_multibyte.phpt | 36 +++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 ext/intl/tests/grapheme_empty_offset_multibyte.phpt diff --git a/NEWS b/NEWS index 927ebe121e8e..ee0cf1b9c1df 100644 --- a/NEWS +++ b/NEWS @@ -32,6 +32,8 @@ PHP NEWS . Fixed bug GH-19320 (FPM UID and GID overflow). (Pratik Bhujel) - Intl: + . Fixed grapheme_strpos() and grapheme_strrpos() with an empty needle + returning UTF-16 offsets instead of grapheme offsets. (Ilia Alshanetsky) . Fixed a memory leak when dumping IntlCalendar instances. (Ilia Alshanetsky) . Fixed a memory leak when iterating IntlBreakIterator::getPartsIterator() results. (iliaal) diff --git a/ext/intl/grapheme/grapheme_util.c b/ext/intl/grapheme/grapheme_util.c index 501b9dfb221d..f0c79785483f 100644 --- a/ext/intl/grapheme/grapheme_util.c +++ b/ext/intl/grapheme/grapheme_util.c @@ -131,7 +131,11 @@ int32_t grapheme_strpos_utf16(char *haystack, size_t haystack_len, char *needle, ret_pos = -1; goto finish; } - ret_pos = last && offset >= 0 ? uhaystack_len : offset_pos; + if (last && offset >= 0) { + ret_pos = grapheme_count_graphemes(bi, uhaystack, uhaystack_len); + } else { + ret_pos = grapheme_count_graphemes(bi, uhaystack, offset_pos); + } goto finish; } diff --git a/ext/intl/tests/grapheme_empty_offset_multibyte.phpt b/ext/intl/tests/grapheme_empty_offset_multibyte.phpt new file mode 100644 index 000000000000..caa545c882c8 --- /dev/null +++ b/ext/intl/tests/grapheme_empty_offset_multibyte.phpt @@ -0,0 +1,36 @@ +--TEST-- +grapheme_strpos() family with empty needle and offset on multi-code-unit graphemes +--EXTENSIONS-- +intl +--FILE-- +getMessage(), "\n"; +} + +?> +--EXPECT-- +int(0) +int(0) +int(1) +int(1) +int(1) +int(2) +int(2) +int(2) +int(1) +ValueError: grapheme_strpos(): Argument #3 ($offset) must be contained in argument #1 ($haystack) From 9cce500a5043f2e9827a704a66682089e351a90b Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Tue, 1 Sep 2026 08:33:05 -0400 Subject: [PATCH 3/8] [intl] Record the ICU error code on Spoofchecker failures (#23520) Spoofchecker::isSuspicious(), ::areConfusable(), ::areBidiConfusable(), ::setChecks(), ::setAllowedLocales() and ::setAllowedChars() warn on U_FAILURE but never record the code, so intl_get_error_code() still reads U_ZERO_ERROR after a failed call. They now record it, which is what SPOOFCHECKER_CHECK_STATUS in spoofchecker_class.h already prescribes for this class. Closes GH-23520 --- NEWS | 2 ++ ext/intl/spoofchecker/spoofchecker_main.cpp | 6 ++++++ .../spoofchecker_setchecks_error_code.phpt | 18 ++++++++++++++++++ 3 files changed, 26 insertions(+) create mode 100644 ext/intl/tests/spoofchecker_setchecks_error_code.phpt diff --git a/NEWS b/NEWS index f3a3914a2635..c27354297786 100644 --- a/NEWS +++ b/NEWS @@ -24,6 +24,8 @@ PHP NEWS read. (iliaal) . Fixed a use-after-free when IntlRuleBasedBreakIterator is constructed from compiled rules. (iliaal) + . Fixed Spoofchecker methods not recording the ICU error code when an ICU + call fails. (Ilia Alshanetsky) - PDO_PGSQL: . Added Pdo\Pgsql::ATTR_CHUNK_SIZE to fetch a result set in chunks of the diff --git a/ext/intl/spoofchecker/spoofchecker_main.cpp b/ext/intl/spoofchecker/spoofchecker_main.cpp index d7e1b4e0553d..7a3ffd9b809a 100644 --- a/ext/intl/spoofchecker/spoofchecker_main.cpp +++ b/ext/intl/spoofchecker/spoofchecker_main.cpp @@ -46,6 +46,7 @@ U_CFUNC PHP_METHOD(Spoofchecker, isSuspicious) ret = intl_icu_compat_uspoof_check_utf8(co->uspoof, ZSTR_VAL(text), ZSTR_LEN(text), co->uspoofres, SPOOFCHECKER_ERROR_CODE_P(co)); if (U_FAILURE(SPOOFCHECKER_ERROR_CODE(co))) { + intl_error_set_code(NULL, SPOOFCHECKER_ERROR_CODE(co)); php_error_docref(NULL, E_WARNING, "(%d) %s", SPOOFCHECKER_ERROR_CODE(co), u_errorName(SPOOFCHECKER_ERROR_CODE(co))); if (intl_icu_compat_uspoof_check_result_mismatch(co->uspoofres, ret, &errmask, SPOOFCHECKER_ERROR_CODE_P(co))) { @@ -83,6 +84,7 @@ U_CFUNC PHP_METHOD(Spoofchecker, areConfusable) ret = uspoof_areConfusableUTF8(co->uspoof, ZSTR_VAL(s1), (int32_t)ZSTR_LEN(s1), ZSTR_VAL(s2), (int32_t)ZSTR_LEN(s2), SPOOFCHECKER_ERROR_CODE_P(co)); } if (U_FAILURE(SPOOFCHECKER_ERROR_CODE(co))) { + intl_error_set_code(NULL, SPOOFCHECKER_ERROR_CODE(co)); php_error_docref(NULL, E_WARNING, "(%d) %s", SPOOFCHECKER_ERROR_CODE(co), u_errorName(SPOOFCHECKER_ERROR_CODE(co))); RETURN_TRUE; } @@ -109,6 +111,7 @@ U_CFUNC PHP_METHOD(Spoofchecker, setAllowedLocales) uspoof_setAllowedLocales(co->uspoof, ZSTR_VAL(locales), SPOOFCHECKER_ERROR_CODE_P(co)); if (U_FAILURE(SPOOFCHECKER_ERROR_CODE(co))) { + intl_error_set_code(NULL, SPOOFCHECKER_ERROR_CODE(co)); php_error_docref(NULL, E_WARNING, "(%d) %s", SPOOFCHECKER_ERROR_CODE(co), u_errorName(SPOOFCHECKER_ERROR_CODE(co))); return; } @@ -130,6 +133,7 @@ U_CFUNC PHP_METHOD(Spoofchecker, setChecks) uspoof_setChecks(co->uspoof, checks, SPOOFCHECKER_ERROR_CODE_P(co)); if (U_FAILURE(SPOOFCHECKER_ERROR_CODE(co))) { + intl_error_set_code(NULL, SPOOFCHECKER_ERROR_CODE(co)); php_error_docref(NULL, E_WARNING, "(%d) %s", SPOOFCHECKER_ERROR_CODE(co), u_errorName(SPOOFCHECKER_ERROR_CODE(co))); } } @@ -220,6 +224,7 @@ U_CFUNC PHP_METHOD(Spoofchecker, setAllowedChars) efree(upattern); if (U_FAILURE(SPOOFCHECKER_ERROR_CODE(co))) { + intl_error_set_code(NULL, SPOOFCHECKER_ERROR_CODE(co)); php_error_docref(NULL, E_WARNING, "(%d) %s", SPOOFCHECKER_ERROR_CODE(co), u_errorName(SPOOFCHECKER_ERROR_CODE(co))); } } @@ -355,6 +360,7 @@ U_CFUNC PHP_METHOD(Spoofchecker, areBidiConfusable) ret = uspoof_areBidiConfusableUTF8(co->uspoof, (UBiDiDirection)direction, ZSTR_VAL(s1), (int32_t)ZSTR_LEN(s1), ZSTR_VAL(s2), (int32_t)ZSTR_LEN(s2), SPOOFCHECKER_ERROR_CODE_P(co)); } if (U_FAILURE(SPOOFCHECKER_ERROR_CODE(co))) { + intl_error_set_code(NULL, SPOOFCHECKER_ERROR_CODE(co)); php_error_docref(NULL, E_WARNING, "(%d) %s", SPOOFCHECKER_ERROR_CODE(co), u_errorName(SPOOFCHECKER_ERROR_CODE(co))); RETURN_TRUE; } diff --git a/ext/intl/tests/spoofchecker_setchecks_error_code.phpt b/ext/intl/tests/spoofchecker_setchecks_error_code.phpt new file mode 100644 index 000000000000..a9c1eaeab452 --- /dev/null +++ b/ext/intl/tests/spoofchecker_setchecks_error_code.phpt @@ -0,0 +1,18 @@ +--TEST-- +Spoofchecker::setChecks() records the ICU error code +--EXTENSIONS-- +intl +--SKIPIF-- + +--FILE-- +setChecks(1 << 20); +var_dump(intl_get_error_code(), intl_get_error_message()); + +?> +--EXPECTF-- +Warning: Spoofchecker::setChecks(): (1) U_ILLEGAL_ARGUMENT_ERROR in %s on line %d +int(1) +string(24) "U_ILLEGAL_ARGUMENT_ERROR" From 509e27bae1cd8b5db255cde69a7fc847a85cc6e6 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Sat, 29 Aug 2026 08:00:19 -0400 Subject: [PATCH 4/8] [mbstring] Fix mb_ereg_replace() emitting NUL/garbage for unterminated \k When a \k backref in an mb_ereg_replace() replacement string lacks its closing delimiter, the error path computed p = name_end + 1 past eos and copied the trailing NUL terminator into the output; the multibyte sibling path (p += clen after \k) could overshoot eos and read heap bytes past the string. Both paths are now clamped to eos so verbatim fallback never copies beyond the parsed name. Closes GH-23499 --- NEWS | 5 +++++ ext/mbstring/php_mbregex.c | 5 +++-- ...mb_ereg_replace_kname_unterminated_nul.phpt | 18 ++++++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 ext/mbstring/tests/mb_ereg_replace_kname_unterminated_nul.phpt diff --git a/NEWS b/NEWS index ee0cf1b9c1df..ac08278a8a8b 100644 --- a/NEWS +++ b/NEWS @@ -49,6 +49,11 @@ PHP NEWS . Fixed a use-after-free when IntlRuleBasedBreakIterator is constructed from compiled rules. (iliaal) +- MBString: + . Fixed mb_ereg_replace() emitting a NUL or out-of-bounds bytes in the + replacement when a \k backref has no closing delimiter. + (Ilia Alshanetsky) + - Opcache: . Fixed opcache.protect_memory race under ZTS. (realFlowControl) . Fixed bug GH-23288 (Crash on restart when opcache.interned_strings_buffer diff --git a/ext/mbstring/php_mbregex.c b/ext/mbstring/php_mbregex.c index e823b5529818..3d887aab490b 100644 --- a/ext/mbstring/php_mbregex.c +++ b/ext/mbstring/php_mbregex.c @@ -771,7 +771,7 @@ static inline void mb_regex_substitute( clen = (int) php_mb_mbchar_bytes(++p, enc); if (clen != 1 || p == eos || (p[0] != '<' && p[0] != '\'')) { /* not a backref delimiter */ - p += clen; + p = MIN(p + clen, eos); smart_str_appendl(pbuf, sp, p - sp); continue; } @@ -791,12 +791,13 @@ static inline void mb_regex_substitute( if (maybe_num && !isdigit((unsigned char)name_end[0])) maybe_num = 0; name_end++; } - p = name_end + 1; if (name_end - name < 1 || name_end >= eos) { /* the backref was empty or we failed to find the end delimiter */ + p = MIN(name_end + 1, eos); smart_str_appendl(pbuf, sp, p - sp); continue; } + p = name_end + 1; /* we have either a name or a number */ if (maybe_num) { if (!onig_noname_group_capture_is_active(regexp)) { diff --git a/ext/mbstring/tests/mb_ereg_replace_kname_unterminated_nul.phpt b/ext/mbstring/tests/mb_ereg_replace_kname_unterminated_nul.phpt new file mode 100644 index 000000000000..67a39dc9f95c --- /dev/null +++ b/ext/mbstring/tests/mb_ereg_replace_kname_unterminated_nul.phpt @@ -0,0 +1,18 @@ +--TEST-- +mb_ereg_replace() with unterminated \k backref must not embed a NUL byte +--EXTENSIONS-- +mbstring +--FILE-- + +--EXPECT-- +string(12) "5c6b3c6e756d" +string(12) "5c6b276e756d" +string(10) "615c6b3c6e" +string(8) "615c6bf0" +string(10) "5c5c6be282" From de3436c76e460ca44829cc8234926280d96ceae6 Mon Sep 17 00:00:00 2001 From: Jordi Kroon Date: Mon, 31 Aug 2026 19:16:00 +0200 Subject: [PATCH 5/8] ext/standard: Fix 1-char relative Location redirects after GH-23467 8196275133e changed the relative-Location check from location_len > 1 to > 0, so a single-character Location began resolving against the request path instead of the host root as before. Closes GH-23521 --- ext/standard/http_fopen_wrapper.c | 2 +- .../http_single_char_location_redirect.phpt | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 ext/standard/tests/http/http_single_char_location_redirect.phpt diff --git a/ext/standard/http_fopen_wrapper.c b/ext/standard/http_fopen_wrapper.c index 9bd12ba527ca..891c1b23955f 100644 --- a/ext/standard/http_fopen_wrapper.c +++ b/ext/standard/http_fopen_wrapper.c @@ -1060,7 +1060,7 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, { char *loc_path = NULL; if (*header_info.location != '/') { - if (header_info.location_len > 0 && resource->path) { + if (header_info.location_len > 1 && resource->path) { char *s = strrchr(ZSTR_VAL(resource->path), '/'); if (!s) { s = ZSTR_VAL(resource->path); diff --git a/ext/standard/tests/http/http_single_char_location_redirect.phpt b/ext/standard/tests/http/http_single_char_location_redirect.phpt new file mode 100644 index 000000000000..66af7a432537 --- /dev/null +++ b/ext/standard/tests/http/http_single_char_location_redirect.phpt @@ -0,0 +1,39 @@ +--TEST-- +Single-char relative Location header keeps resolving against the host root (pre-GH-23467 behavior) +--DESCRIPTION-- +Not RFC 3986 compliant ("x" against "/a/b" gives "/a/x"), but matches +PHP's long-standing behavior of resolving against the host root. See GH-23521. +--FILE-- + ['follow_location' => 1]]); +echo @file_get_contents("http://{{ ADDR }}/a/b", false, $ctx), "\n"; +CODE; + +include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--EXPECT-- +uri=/x From 2049c17b9a0c8b68a82ba31254401c0eba80e229 Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Tue, 1 Sep 2026 23:54:41 +0800 Subject: [PATCH 6/8] Tests: Drop ext/openssl/tests/CONFLICTS (#22472) OpenSSL tests use ephemeral ports now, so the directory-level conflict file is no longer needed. --- ext/openssl/tests/CONFLICTS | 1 - 1 file changed, 1 deletion(-) delete mode 100644 ext/openssl/tests/CONFLICTS diff --git a/ext/openssl/tests/CONFLICTS b/ext/openssl/tests/CONFLICTS deleted file mode 100644 index 254defddb53c..000000000000 --- a/ext/openssl/tests/CONFLICTS +++ /dev/null @@ -1 +0,0 @@ -server From 52385c92c66ba9032d7958a34f9ca52f4dc42a4c Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Wed, 2 Sep 2026 00:00:22 +0800 Subject: [PATCH 7/8] CI: Fix tests gh23444 on Windows (#23529) this fixes CI failure introduced in #23444. e.g. https://github.com/php/php-src/actions/runs/33510795665/job/99865746673 --- ext/pdo_odbc/tests/gh23444.phpt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/pdo_odbc/tests/gh23444.phpt b/ext/pdo_odbc/tests/gh23444.phpt index f71fc733839f..a18dd202e732 100644 --- a/ext/pdo_odbc/tests/gh23444.phpt +++ b/ext/pdo_odbc/tests/gh23444.phpt @@ -16,7 +16,7 @@ $db->exec("CREATE TABLE gh23444 (v NVARCHAR(100))"); // 13 bytes as UTF-8, so an unconverted parameter is an odd number of bytes $string = "\u{6e2c}\u{8a66}\u{4e2d}\u{1f418}"; -$db->setAttribute(PDO::ODBC_ATTR_ASSUME_UTF8, true); +$db->setAttribute(Pdo\Odbc::ATTR_ASSUME_UTF8, true); $stmt = $db->prepare("INSERT INTO gh23444 VALUES(?)"); $stmt->execute([$string]); From 48190ac8ed88087583572e6c187a9cfd9c3d5169 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Tue, 1 Sep 2026 17:10:31 +0100 Subject: [PATCH 8/8] ext/date: Throw exceptions directly (#19166) --- ext/date/php_date.c | 67 +++++++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/ext/date/php_date.c b/ext/date/php_date.c index 9f9f0a6159ed..27664623c6b9 100644 --- a/ext/date/php_date.c +++ b/ext/date/php_date.c @@ -3258,7 +3258,12 @@ PHP_FUNCTION(date_format) } /* }}} */ -static bool php_date_modify(zval *object, char *modify, size_t modify_len) /* {{{ */ +typedef enum { + PHP_DATE_MODIFY_WARNING, + PHP_DATE_MODIFY_THROW +} php_date_modify_error_mode; + +static bool php_date_modify(zval *object, char *modify, size_t modify_len, const php_date_modify_error_mode error_mode) /* {{{ */ { php_date_obj *dateobj; timelib_time *tmp_time; @@ -3278,10 +3283,22 @@ static bool php_date_modify(zval *object, char *modify, size_t modify_len) /* {{ if (err && err->error_count) { /* spit out the first library error message, at least */ - php_error_docref(NULL, E_WARNING, "Failed to parse time string (%s) at position %d (%c): %s", modify, - err->error_messages[0].position, - err->error_messages[0].character ? err->error_messages[0].character : ' ', - err->error_messages[0].message); + if (error_mode == PHP_DATE_MODIFY_THROW) { + zend_string *func_name = get_active_function_or_method_name(); + zend_throw_exception_ex(date_ce_date_malformed_string_exception, 0, + "%s(): Failed to parse time string (%s) at position %d (%c): %s", + ZSTR_VAL(func_name), + modify, + err->error_messages[0].position, + err->error_messages[0].character ? err->error_messages[0].character : ' ', + err->error_messages[0].message); + zend_string_release_ex(func_name, false); + } else { + php_error_docref(NULL, E_WARNING, "Failed to parse time string (%s) at position %d (%c): %s", modify, + err->error_messages[0].position, + err->error_messages[0].character ? err->error_messages[0].character : ' ', + err->error_messages[0].message); + } timelib_time_dtor(tmp_time); return false; } @@ -3350,7 +3367,7 @@ PHP_FUNCTION(date_modify) RETURN_THROWS(); } - if (!php_date_modify(object, modify, modify_len)) { + if (!php_date_modify(object, modify, modify_len, PHP_DATE_MODIFY_WARNING)) { RETURN_FALSE; } @@ -3364,21 +3381,16 @@ PHP_METHOD(DateTime, modify) zval *object; char *modify; size_t modify_len; - zend_error_handling zeh; object = ZEND_THIS; ZEND_PARSE_PARAMETERS_START(1, 1) Z_PARAM_STRING(modify, modify_len) ZEND_PARSE_PARAMETERS_END(); - zend_replace_error_handling(EH_THROW, date_ce_date_malformed_string_exception, &zeh); - if (!php_date_modify(object, modify, modify_len)) { - zend_restore_error_handling(&zeh); + if (!php_date_modify(object, modify, modify_len, PHP_DATE_MODIFY_THROW)) { RETURN_THROWS(); } - zend_restore_error_handling(&zeh); - RETURN_OBJ_COPY(Z_OBJ_P(object)); } /* }}} */ @@ -3389,7 +3401,6 @@ PHP_METHOD(DateTimeImmutable, modify) zval *object, new_object; char *modify; size_t modify_len; - zend_error_handling zeh; object = ZEND_THIS; ZEND_PARSE_PARAMETERS_START(1, 1) @@ -3398,15 +3409,11 @@ PHP_METHOD(DateTimeImmutable, modify) date_clone_immutable(object, &new_object); - zend_replace_error_handling(EH_THROW, date_ce_date_malformed_string_exception, &zeh); - if (!php_date_modify(&new_object, modify, modify_len)) { + if (!php_date_modify(&new_object, modify, modify_len, PHP_DATE_MODIFY_THROW)) { zval_ptr_dtor(&new_object); - zend_restore_error_handling(&zeh); RETURN_THROWS(); } - zend_restore_error_handling(&zeh); - RETURN_OBJ(Z_OBJ(new_object)); } /* }}} */ @@ -3463,7 +3470,7 @@ PHP_METHOD(DateTimeImmutable, add) } /* }}} */ -static void php_date_sub(zval *object, zval *interval, zval *return_value) /* {{{ */ +static void php_date_sub(zval *object, zval *interval, zval *return_value, const bool should_throw) /* {{{ */ { php_date_obj *dateobj; php_interval_obj *intobj; @@ -3475,7 +3482,15 @@ static void php_date_sub(zval *object, zval *interval, zval *return_value) /* {{ DATE_CHECK_INITIALIZED(intobj->initialized, Z_OBJCE_P(interval)); if (intobj->diff->have_weekday_relative || intobj->diff->have_special_relative) { - php_error_docref(NULL, E_WARNING, "Only non-special relative time specifications are supported for subtraction"); + if (should_throw) { + zend_string *func_name = get_active_function_or_method_name(); + zend_throw_exception_ex(date_ce_date_invalid_operation_exception, 0, + "%s(): Only non-special relative time specifications are supported for subtraction", + ZSTR_VAL(func_name)); + zend_string_release_ex(func_name, false); + } else { + php_error_docref(NULL, E_WARNING, "Only non-special relative time specifications are supported for subtraction"); + } return; } @@ -3497,7 +3512,7 @@ PHP_FUNCTION(date_sub) RETURN_THROWS(); } - php_date_sub(object, interval, return_value); + php_date_sub(object, interval, return_value, false); RETURN_OBJ_COPY(Z_OBJ_P(object)); } /* }}} */ @@ -3506,15 +3521,12 @@ PHP_FUNCTION(date_sub) PHP_METHOD(DateTime, sub) { zval *object, *interval; - zend_error_handling zeh; if (zend_parse_method_parameters(ZEND_NUM_ARGS(), getThis(), "OO", &object, date_ce_date, &interval, date_ce_interval) == FAILURE) { RETURN_THROWS(); } - zend_replace_error_handling(EH_THROW, date_ce_date_invalid_operation_exception, &zeh); - php_date_sub(object, interval, return_value); - zend_restore_error_handling(&zeh); + php_date_sub(object, interval, return_value, true); RETURN_OBJ_COPY(Z_OBJ_P(object)); } @@ -3524,7 +3536,6 @@ PHP_METHOD(DateTime, sub) PHP_METHOD(DateTimeImmutable, sub) { zval *object, *interval, new_object; - zend_error_handling zeh; object = ZEND_THIS; ZEND_PARSE_PARAMETERS_START(1, 1) @@ -3533,9 +3544,7 @@ PHP_METHOD(DateTimeImmutable, sub) date_clone_immutable(object, &new_object); - zend_replace_error_handling(EH_THROW, date_ce_date_invalid_operation_exception, &zeh); - php_date_sub(&new_object, interval, return_value); - zend_restore_error_handling(&zeh); + php_date_sub(&new_object, interval, return_value, true); RETURN_OBJ(Z_OBJ(new_object)); }