Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -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
========================================
Expand Down
2 changes: 1 addition & 1 deletion ext/intl/locale/locale.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion ext/intl/locale/locale_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

60 changes: 57 additions & 3 deletions ext/intl/locale/locale_methods.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
#include <unicode/udata.h>
#include <unicode/putil.h>
#include <unicode/ures.h>
#include <unicode/uenum.h>
#include <unicode/locid.h>

extern "C" {
#include "php_intl.h"
Expand Down Expand Up @@ -1661,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;
Expand All @@ -1670,10 +1672,52 @@ 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<const char *[]> available_list;
std::unique_ptr<icu::Locale[]> available_canonical;
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.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) {
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) > 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);
} 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;
Expand All @@ -1693,7 +1737,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);
Expand All @@ -1702,6 +1750,12 @@ 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]);
}
}
RETURN_STRINGL(resultLocale, len);
}
/* }}} */
Expand Down
2 changes: 1 addition & 1 deletion ext/intl/php_intl.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}

Expand Down
3 changes: 2 additions & 1 deletion ext/intl/php_intl_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

60 changes: 60 additions & 0 deletions ext/intl/tests/gh24074.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
--TEST--
GH-24074 (Locale::acceptFromHttp() with a list of available locales)
--EXTENSIONS--
intl
--SKIPIF--
<?php if (version_compare(INTL_ICU_VERSION, '67.1') < 0) die('skip for ICU >= 67.1'); ?>
--FILE--
<?php

$header = 'fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7';

var_dump(Locale::acceptFromHttp($header, ['en_US', 'de_DE']));
var_dump(Locale::acceptFromHttp($header, ['de', 'en']));
var_dump(Locale::acceptFromHttp($header, ['de']));
var_dump(Locale::acceptFromHttp('de-CH, en;q=0.5', ['en', 'de']));
var_dump(Locale::acceptFromHttp('en-us', ['de', 'en_US']));
var_dump(Locale::acceptFromHttp('en-us', ['de', 'en-US']));
var_dump(Locale::acceptFromHttp('ja', ['en', 'de']));
var_dump(Locale::acceptFromHttp('en-us,en;q=0.5', null));
var_dump(locale_accept_from_http($header, ['de', 'en']));

var_dump(Locale::acceptFromHttp(str_repeat('a', 200), ['en']));
var_dump(Locale::acceptFromHttp('en', ['en']));
var_dump(intl_get_error_message());

$invalid = [
[],
[1],
["e\0n"],
[str_repeat('a', 200)],
['en', str_repeat('a', 20)],
];

foreach ($invalid as $locales) {
try {
Locale::acceptFromHttp('en', $locales);
} catch (\Throwable $e) {
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
}
}

?>
--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 156 characters
ValueError: Locale::acceptFromHttp(): Argument #2 ($availableLocales) must only contain valid locales
32 changes: 32 additions & 0 deletions ext/intl/tests/locale_accept_from_http_error_reset.phpt
Original file line number Diff line number Diff line change
@@ -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--
<?php
foreach (['Locale::acceptFromHttp', 'locale_accept_from_http'] as $func) {
$func(str_repeat('a', 200));
var_dump(intl_get_error_code() === U_ILLEGAL_ARGUMENT_ERROR);

try {
$func('en', 123);
} catch (\Throwable $e) {
echo $e::class, "\n";
}

var_dump(intl_get_error_code() === U_ZERO_ERROR);
var_dump(intl_get_error_message());
}
?>
--EXPECT--
bool(true)
TypeError
bool(true)
string(12) "U_ZERO_ERROR"
bool(true)
TypeError
bool(true)
string(12) "U_ZERO_ERROR"
2 changes: 1 addition & 1 deletion ext/intl/tests/locale_bug74439.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading