From 61229313e7afa680716c6a9f10805a78941365ae Mon Sep 17 00:00:00 2001 From: David Carlier Date: Sat, 3 Oct 2026 12:05:25 +0100 Subject: [PATCH 1/4] Implement GH-24074: Locale::acceptFromHttp() available locales list Add an optional $availableLocales argument so the Accept-Language header is matched against the locales the application supports rather than every locale known to ICU. --- ext/intl/locale/locale.stub.php | 2 +- ext/intl/locale/locale_arginfo.h | 3 +- ext/intl/locale/locale_methods.cpp | 44 +++++++++++++++++++++++++-- ext/intl/php_intl.stub.php | 2 +- ext/intl/php_intl_arginfo.h | 3 +- ext/intl/tests/gh24074.phpt | 47 +++++++++++++++++++++++++++++ ext/intl/tests/locale_bug74439.phpt | 2 +- 7 files changed, 96 insertions(+), 7 deletions(-) create mode 100644 ext/intl/tests/gh24074.phpt diff --git a/ext/intl/locale/locale.stub.php b/ext/intl/locale/locale.stub.php index fc012768de9f..3fd11034d272 100644 --- a/ext/intl/locale/locale.stub.php +++ b/ext/intl/locale/locale.stub.php @@ -142,7 +142,7 @@ public static function canonicalize(string $locale): ?string {} * @tentative-return-type * @alias locale_accept_from_http */ - public static function acceptFromHttp(string $header): string|false {} + public static function acceptFromHttp(string $header, ?array $availableLocales = null): string|false {} /** * @alias locale_is_right_to_left diff --git a/ext/intl/locale/locale_arginfo.h b/ext/intl/locale/locale_arginfo.h index a01f583b0451..a95494580c0f 100644 --- a/ext/intl/locale/locale_arginfo.h +++ b/ext/intl/locale/locale_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit locale.stub.php instead. - * Stub hash: 43dbc3aa294044e9cef923a5f39249dae86b67f0 */ + * Stub hash: 2f2de79772630fd0a2399b88120bd968bfed1144 */ ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_Locale_getDefault, 0, 0, IS_STRING, 0) ZEND_END_ARG_INFO() @@ -71,6 +71,7 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_MASK_EX(arginfo_class_Locale_acceptFromHttp, 0, 1, MAY_BE_STRING|MAY_BE_FALSE) ZEND_ARG_TYPE_INFO(0, header, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, availableLocales, IS_ARRAY, 1, "null") ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Locale_isRightToLeft, 0, 1, _IS_BOOL, 0) diff --git a/ext/intl/locale/locale_methods.cpp b/ext/intl/locale/locale_methods.cpp index 911abfdc5c11..f043723293fd 100644 --- a/ext/intl/locale/locale_methods.cpp +++ b/ext/intl/locale/locale_methods.cpp @@ -22,6 +22,7 @@ #include #include #include +#include extern "C" { #include "php_intl.h" @@ -1670,10 +1671,45 @@ U_CFUNC PHP_FUNCTION(locale_accept_from_http) int len; char resultLocale[INTL_MAX_LOCALE_LEN+1]; UAcceptResult outResult; + HashTable *available_locales = nullptr; + std::unique_ptr available_list; + uint32_t available_count = 0; - ZEND_PARSE_PARAMETERS_START(1, 1) + ZEND_PARSE_PARAMETERS_START(1, 2) Z_PARAM_STRING(http_accept, http_accept_len) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_HT_OR_NULL(available_locales) ZEND_PARSE_PARAMETERS_END(); + + if (available_locales) { + uint32_t i = 0; + zval *entry; + + available_count = zend_hash_num_elements(available_locales); + if (available_count == 0) { + zend_argument_must_not_be_empty_error(2); + RETURN_THROWS(); + } + + available_list = std::make_unique(available_count); + ZEND_HASH_FOREACH_VAL(available_locales, entry) { + ZVAL_DEREF(entry); + if (Z_TYPE_P(entry) != IS_STRING) { + zend_argument_type_error(2, "must only contain string values"); + RETURN_THROWS(); + } + if (zend_str_has_nul_byte(Z_STR_P(entry))) { + zend_argument_value_error(2, "must not contain any null bytes"); + RETURN_THROWS(); + } + if (Z_STRLEN_P(entry) > ULOC_FULLNAME_CAPACITY) { + zend_argument_value_error(2, "must not contain locales longer than %d characters", ULOC_FULLNAME_CAPACITY); + RETURN_THROWS(); + } + available_list[i++] = Z_STRVAL_P(entry); + } ZEND_HASH_FOREACH_END(); + } + if (UNEXPECTED(http_accept_len > ULOC_FULLNAME_CAPACITY)) { /* check each fragment, if any bigger than capacity, can't do it due to bug #72533 */ char *start = http_accept; @@ -1693,7 +1729,11 @@ U_CFUNC PHP_FUNCTION(locale_accept_from_http) } while(end != nullptr); } - available = ures_openAvailableLocales(nullptr, &status); + if (available_list) { + available = uenum_openCharStringsEnumeration(available_list.get(), available_count, &status); + } else { + available = ures_openAvailableLocales(nullptr, &status); + } INTL_CHECK_STATUS(status, "failed to retrieve locale list"); len = uloc_acceptLanguageFromHTTP(resultLocale, INTL_MAX_LOCALE_LEN, &outResult, http_accept, available, &status); diff --git a/ext/intl/php_intl.stub.php b/ext/intl/php_intl.stub.php index 1e6c5cacf203..a30c2d987608 100644 --- a/ext/intl/php_intl.stub.php +++ b/ext/intl/php_intl.stub.php @@ -503,7 +503,7 @@ function locale_canonicalize(string $locale): ?string {} function locale_lookup(array $languageTag, string $locale, bool $canonicalize = false, ?string $defaultLocale = null): ?string {} -function locale_accept_from_http(string $header): string|false {} +function locale_accept_from_http(string $header, ?array $availableLocales = null): string|false {} function locale_is_right_to_left(string $locale): bool {} diff --git a/ext/intl/php_intl_arginfo.h b/ext/intl/php_intl_arginfo.h index f46192618b91..9478a818d383 100644 --- a/ext/intl/php_intl_arginfo.h +++ b/ext/intl/php_intl_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit php_intl.stub.php instead. - * Stub hash: f94e7c9cc372878f1f8bd0e948092ea72076e687 */ + * Stub hash: bb8c86f1c7f0dcc264e5968fe9548f20ac20908b */ #include "zend_attributes.h" #include "zend_constants.h" @@ -594,6 +594,7 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_locale_accept_from_http, 0, 1, MAY_BE_STRING|MAY_BE_FALSE) ZEND_ARG_TYPE_INFO(0, header, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, availableLocales, IS_ARRAY, 1, "null") ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_locale_is_right_to_left, 0, 1, _IS_BOOL, 0) diff --git a/ext/intl/tests/gh24074.phpt b/ext/intl/tests/gh24074.phpt new file mode 100644 index 000000000000..e3602c5a2dfa --- /dev/null +++ b/ext/intl/tests/gh24074.phpt @@ -0,0 +1,47 @@ +--TEST-- +GH-24074 (Locale::acceptFromHttp() with a list of available locales) +--EXTENSIONS-- +intl +--FILE-- +getMessage(), "\n"; + } +} + +?> +--EXPECTF-- +string(5) "en_US" +string(2) "en" +string(2) "de" +string(2) "de" +string(5) "en_US" +bool(false) +string(5) "en_US" +string(2) "en" +ValueError: Locale::acceptFromHttp(): Argument #2 ($availableLocales) must not be empty +TypeError: Locale::acceptFromHttp(): Argument #2 ($availableLocales) must only contain string values +ValueError: Locale::acceptFromHttp(): Argument #2 ($availableLocales) must not contain any null bytes +ValueError: Locale::acceptFromHttp(): Argument #2 ($availableLocales) must not contain locales longer than %d characters diff --git a/ext/intl/tests/locale_bug74439.phpt b/ext/intl/tests/locale_bug74439.phpt index 8fe1f2645b9e..5d9a8086be6d 100644 --- a/ext/intl/tests/locale_bug74439.phpt +++ b/ext/intl/tests/locale_bug74439.phpt @@ -33,7 +33,7 @@ foreach ($methods as $method) { ?> --EXPECT-- getDefault: 0, 0 -acceptFromHttp: 1, 1 +acceptFromHttp: 2, 1 canonicalize: 1, 1 composeLocale: 1, 1 getAllVariants: 1, 1 From 00f15de1e84e279544e796eada0b751f75619767 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Sat, 3 Oct 2026 13:05:28 +0100 Subject: [PATCH 2/4] fix edge cases --- ext/intl/locale/locale_methods.cpp | 19 +++++++++++++++++-- ext/intl/tests/gh24074.phpt | 19 ++++++++++++++++--- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/ext/intl/locale/locale_methods.cpp b/ext/intl/locale/locale_methods.cpp index f043723293fd..89d662f360b7 100644 --- a/ext/intl/locale/locale_methods.cpp +++ b/ext/intl/locale/locale_methods.cpp @@ -23,6 +23,7 @@ #include #include #include +#include extern "C" { #include "php_intl.h" @@ -1673,6 +1674,7 @@ U_CFUNC PHP_FUNCTION(locale_accept_from_http) UAcceptResult outResult; HashTable *available_locales = nullptr; std::unique_ptr available_list; + std::unique_ptr available_canonical; uint32_t available_count = 0; ZEND_PARSE_PARAMETERS_START(1, 2) @@ -1681,6 +1683,8 @@ U_CFUNC PHP_FUNCTION(locale_accept_from_http) Z_PARAM_ARRAY_HT_OR_NULL(available_locales) ZEND_PARSE_PARAMETERS_END(); + intl_error_reset( nullptr ); + if (available_locales) { uint32_t i = 0; zval *entry; @@ -1692,6 +1696,7 @@ U_CFUNC PHP_FUNCTION(locale_accept_from_http) } available_list = std::make_unique(available_count); + available_canonical = std::make_unique(available_count); ZEND_HASH_FOREACH_VAL(available_locales, entry) { ZVAL_DEREF(entry); if (Z_TYPE_P(entry) != IS_STRING) { @@ -1702,8 +1707,13 @@ U_CFUNC PHP_FUNCTION(locale_accept_from_http) zend_argument_value_error(2, "must not contain any null bytes"); RETURN_THROWS(); } - if (Z_STRLEN_P(entry) > ULOC_FULLNAME_CAPACITY) { - zend_argument_value_error(2, "must not contain locales longer than %d characters", ULOC_FULLNAME_CAPACITY); + if (Z_STRLEN_P(entry) > INTL_MAX_LOCALE_LEN) { + zend_argument_value_error(2, "must not contain locales longer than %d characters", INTL_MAX_LOCALE_LEN); + RETURN_THROWS(); + } + available_canonical[i] = icu::Locale(Z_STRVAL_P(entry)); + if (available_canonical[i].isBogus()) { + zend_argument_value_error(2, "must only contain valid locales"); RETURN_THROWS(); } available_list[i++] = Z_STRVAL_P(entry); @@ -1742,6 +1752,11 @@ U_CFUNC PHP_FUNCTION(locale_accept_from_http) if (UNEXPECTED(len < 0 || outResult == ULOC_ACCEPT_FAILED)) { RETURN_FALSE; } + for (uint32_t i = 0; i < available_count; i++) { + if (strcmp(available_list[i], resultLocale) == 0 || strcmp(available_canonical[i].getName(), resultLocale) == 0) { + RETURN_STRING(available_list[i]); + } + } RETURN_STRINGL(resultLocale, len); } /* }}} */ diff --git a/ext/intl/tests/gh24074.phpt b/ext/intl/tests/gh24074.phpt index e3602c5a2dfa..f9a734b0dc0c 100644 --- a/ext/intl/tests/gh24074.phpt +++ b/ext/intl/tests/gh24074.phpt @@ -2,6 +2,8 @@ GH-24074 (Locale::acceptFromHttp() with a list of available locales) --EXTENSIONS-- intl +--SKIPIF-- += 67.1'); ?> --FILE-- getMessage(), "\n"; + echo $e::class, ': ', $e->getMessage(), PHP_EOL; } } ?> ---EXPECTF-- +--EXPECT-- string(5) "en_US" string(2) "en" string(2) "de" string(2) "de" string(5) "en_US" +string(5) "en-US" bool(false) string(5) "en_US" string(2) "en" +bool(false) +string(2) "en" +string(12) "U_ZERO_ERROR" ValueError: Locale::acceptFromHttp(): Argument #2 ($availableLocales) must not be empty TypeError: Locale::acceptFromHttp(): Argument #2 ($availableLocales) must only contain string values ValueError: Locale::acceptFromHttp(): Argument #2 ($availableLocales) must not contain any null bytes -ValueError: Locale::acceptFromHttp(): Argument #2 ($availableLocales) must not contain locales longer than %d characters +ValueError: Locale::acceptFromHttp(): Argument #2 ($availableLocales) must not contain locales longer than 156 characters +ValueError: Locale::acceptFromHttp(): Argument #2 ($availableLocales) must only contain valid locales From b85411858acaf216d9452e5ece9a9c17977c56ee Mon Sep 17 00:00:00 2001 From: David Carlier Date: Sat, 3 Oct 2026 13:42:18 +0100 Subject: [PATCH 3/4] dowbgrade C++ spec usage / UPGRADING note --- UPGRADING | 8 ++++++++ ext/intl/locale/locale_methods.cpp | 5 +++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/UPGRADING b/UPGRADING index 9692bfd9b2cc..2095fdb03839 100644 --- a/UPGRADING +++ b/UPGRADING @@ -49,6 +49,14 @@ PHP 8.7 UPGRADE NOTES 5. Changed Functions ======================================== +- Intl: + . Locale::acceptFromHttp() and locale_accept_from_http() now accept an + optional $availableLocales array argument. When given, the + Accept-Language header is matched against these locales instead of all + locales known to ICU, and the matching entry is returned as passed. + A ValueError is thrown if the array is empty or contains an invalid + locale, and a TypeError if it contains a non-string value. + ======================================== 6. New Functions ======================================== diff --git a/ext/intl/locale/locale_methods.cpp b/ext/intl/locale/locale_methods.cpp index 89d662f360b7..f1228adb6e08 100644 --- a/ext/intl/locale/locale_methods.cpp +++ b/ext/intl/locale/locale_methods.cpp @@ -1695,8 +1695,8 @@ U_CFUNC PHP_FUNCTION(locale_accept_from_http) RETURN_THROWS(); } - available_list = std::make_unique(available_count); - available_canonical = std::make_unique(available_count); + available_list.reset(new const char *[available_count]); + available_canonical.reset(new icu::Locale[available_count]); ZEND_HASH_FOREACH_VAL(available_locales, entry) { ZVAL_DEREF(entry); if (Z_TYPE_P(entry) != IS_STRING) { @@ -1752,6 +1752,7 @@ U_CFUNC PHP_FUNCTION(locale_accept_from_http) if (UNEXPECTED(len < 0 || outResult == ULOC_ACCEPT_FAILED)) { RETURN_FALSE; } + resultLocale[len] = '\0'; for (uint32_t i = 0; i < available_count; i++) { if (strcmp(available_list[i], resultLocale) == 0 || strcmp(available_canonical[i].getName(), resultLocale) == 0) { RETURN_STRING(available_list[i]); From 1418a272ed5b3aea4c857b685ab725795d516e20 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Sat, 3 Oct 2026 15:56:27 +0100 Subject: [PATCH 4/4] feedback --- ext/intl/locale/locale_methods.cpp | 4 +-- .../locale_accept_from_http_error_reset.phpt | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 ext/intl/tests/locale_accept_from_http_error_reset.phpt diff --git a/ext/intl/locale/locale_methods.cpp b/ext/intl/locale/locale_methods.cpp index f1228adb6e08..8130593b8938 100644 --- a/ext/intl/locale/locale_methods.cpp +++ b/ext/intl/locale/locale_methods.cpp @@ -1663,7 +1663,7 @@ PHP_INTL_FUNCTION_WITH_ERROR_RESET(locale_lookup) /* }}} */ /* {{{ Tries to find out best available locale based on HTTP "Accept-Language" header */ -U_CFUNC PHP_FUNCTION(locale_accept_from_http) +PHP_INTL_FUNCTION_WITH_ERROR_RESET(locale_accept_from_http) { UEnumeration *available; char *http_accept = nullptr; @@ -1683,8 +1683,6 @@ U_CFUNC PHP_FUNCTION(locale_accept_from_http) Z_PARAM_ARRAY_HT_OR_NULL(available_locales) ZEND_PARSE_PARAMETERS_END(); - intl_error_reset( nullptr ); - if (available_locales) { uint32_t i = 0; zval *entry; diff --git a/ext/intl/tests/locale_accept_from_http_error_reset.phpt b/ext/intl/tests/locale_accept_from_http_error_reset.phpt new file mode 100644 index 000000000000..f4472cf9f49f --- /dev/null +++ b/ext/intl/tests/locale_accept_from_http_error_reset.phpt @@ -0,0 +1,32 @@ +--TEST-- +Locale::acceptFromHttp() resets intl error before argument parsing +--EXTENSIONS-- +intl +--INI-- +intl.error_level=0 +intl.use_exceptions=0 +--FILE-- + +--EXPECT-- +bool(true) +TypeError +bool(true) +string(12) "U_ZERO_ERROR" +bool(true) +TypeError +bool(true) +string(12) "U_ZERO_ERROR"