Skip to content

Commit 2d11f4d

Browse files
Merge branch 'PHP-8.4'
* PHP-8.4: Fix phpGH-11952: better locale strings canonicalization for IntlDateFormatter and NumberFormatter (php#19593)
2 parents ad75c26 + cc4cfbf commit 2d11f4d

File tree

7 files changed

+115
-6
lines changed

7 files changed

+115
-6
lines changed

NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ PHP NEWS
3737
. Fixed bug GH-16993 (filter_var_array with FILTER_VALIDATE_INT|FILTER_NULL_ON_FAILURE
3838
should emit warning for invalid filter usage). (alexandre-daubois)
3939

40+
- Intl:
41+
. Fixed bug GH-11952 (Fix locale strings canonicalization for IntlDateFormatter
42+
and NumberFormatter). (alexandre-daubois)
43+
4044
- Opcache:
4145
. Fixed bug GH-19486 (Incorrect opline after deoptimization). (Arnaud)
4246
. Fixed bug GH-19601 (Wrong JIT stack setup on aarch64/clang). (Arnaud)

ext/intl/dateformat/dateformat_create.cpp

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
extern "C" {
2323
#include <unicode/ustring.h>
2424
#include <unicode/udat.h>
25+
#include <unicode/uloc.h>
2526

2627
#include "php_intl.h"
2728
#include "dateformat_create.h"
@@ -106,7 +107,12 @@ static zend_result datefmt_ctor(INTERNAL_FUNCTION_PARAMETERS)
106107
if (locale_len == 0) {
107108
locale_str = (char *) intl_locale_get_default();
108109
}
109-
locale = Locale::createFromName(locale_str);
110+
111+
char* canonicalized_locale = canonicalize_locale_string(locale_str);
112+
const char* final_locale = canonicalized_locale ? canonicalized_locale : locale_str;
113+
const char* stored_locale = canonicalized_locale ? canonicalized_locale : locale_str;
114+
115+
locale = Locale::createFromName(final_locale);
110116
/* get*Name accessors being set does not preclude being bogus */
111117
if (locale.isBogus() || ((locale_len == 1 && locale_str[0] != 'C') || (locale_len > 1 && strlen(locale.getISO3Language()) == 0))) {
112118
zend_argument_value_error(1, "\"%s\" is invalid", locale_str);
@@ -144,7 +150,7 @@ static zend_result datefmt_ctor(INTERNAL_FUNCTION_PARAMETERS)
144150
}
145151

146152
DATE_FORMAT_OBJECT(dfo) = udat_open((UDateFormatStyle)time_type,
147-
(UDateFormatStyle)date_type, locale_str, NULL, 0, svalue,
153+
(UDateFormatStyle)date_type, final_locale, NULL, 0, svalue,
148154
slength, &INTL_DATA_ERROR_CODE(dfo));
149155

150156
if (pattern_str && pattern_str_len > 0) {
@@ -176,9 +182,13 @@ static zend_result datefmt_ctor(INTERNAL_FUNCTION_PARAMETERS)
176182
dfo->date_type = date_type;
177183
dfo->time_type = time_type;
178184
dfo->calendar = calendar_type;
179-
dfo->requested_locale = estrdup(locale_str);
185+
/* Store the canonicalized locale, or fallback to original if canonicalization failed */
186+
dfo->requested_locale = estrdup(stored_locale);
180187

181188
error:
189+
if (canonicalized_locale) {
190+
efree(canonicalized_locale);
191+
}
182192
if (svalue) {
183193
efree(svalue);
184194
}

ext/intl/formatter/formatter_main.c

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,18 @@ static int numfmt_ctor(INTERNAL_FUNCTION_PARAMETERS)
6464
return FAILURE;
6565
}
6666

67-
/* Create an ICU number formatter. */
68-
FORMATTER_OBJECT(nfo) = unum_open(style, spattern, spattern_len, locale, NULL, &INTL_DATA_ERROR_CODE(nfo));
67+
char* canonicalized_locale = canonicalize_locale_string(locale);
68+
const char* final_locale = canonicalized_locale ? canonicalized_locale : locale;
6969

70-
if(spattern) {
70+
FORMATTER_OBJECT(nfo) = unum_open(style, spattern, spattern_len, final_locale, NULL, &INTL_DATA_ERROR_CODE(nfo));
71+
72+
if (spattern) {
7173
efree(spattern);
7274
}
75+
76+
if (canonicalized_locale) {
77+
efree(canonicalized_locale);
78+
}
7379

7480
INTL_CTOR_CHECK_STATUS(nfo, "number formatter creation failed");
7581
return SUCCESS;

ext/intl/php_intl.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,20 @@ const char *intl_locale_get_default( void )
9898
return INTL_G(default_locale);
9999
}
100100

101+
char* canonicalize_locale_string(const char* locale) {
102+
char canonicalized[ULOC_FULLNAME_CAPACITY];
103+
UErrorCode status = U_ZERO_ERROR;
104+
int32_t canonicalized_len;
105+
106+
canonicalized_len = uloc_canonicalize(locale, canonicalized, sizeof(canonicalized), &status);
107+
108+
if (U_FAILURE(status) || canonicalized_len <= 0) {
109+
return NULL;
110+
}
111+
112+
return estrdup(canonicalized);
113+
}
114+
101115
static PHP_INI_MH(OnUpdateErrorLevel)
102116
{
103117
zend_long *p = (zend_long *) ZEND_INI_GET_ADDR();

ext/intl/php_intl.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ PHP_RSHUTDOWN_FUNCTION(intl);
6868
PHP_MINFO_FUNCTION(intl);
6969

7070
const char *intl_locale_get_default( void );
71+
char *canonicalize_locale_string(const char* locale);
7172

7273
#define PHP_INTL_VERSION PHP_VERSION
7374

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
--TEST--
2+
Fix GH-11942: IntlDateFormatter should canonicalize locale strings
3+
--EXTENSIONS--
4+
intl
5+
--FILE--
6+
<?php
7+
8+
$test_cases = [
9+
['pt', 'pt'],
10+
['pt-PT', 'pt_PT'],
11+
['pt_PT.utf8', 'pt_PT'],
12+
['fr_CA@euro', 'fr_CA'],
13+
];
14+
15+
echo "Testing IntlDateFormatter locale canonicalization:\n";
16+
foreach ($test_cases as $test_case) {
17+
[$input, $expected] = $test_case;
18+
19+
$formatter = new IntlDateFormatter($input, IntlDateFormatter::SHORT, IntlDateFormatter::NONE, 'UTC');
20+
$actual = $formatter->getLocale();
21+
22+
$status = ($actual === $expected) ? 'PASS' : 'FAIL';
23+
echo "Input: $input -> Expected: $expected -> Actual: $actual -> $status\n";
24+
}
25+
26+
$dateFormatter = new IntlDateFormatter('pt_PT.utf8', IntlDateFormatter::SHORT, IntlDateFormatter::NONE, 'UTC');
27+
$dateResult = $dateFormatter->format(1691585260);
28+
echo "\nDateFormatter with pt_PT.utf8: " . $dateResult . "\n";
29+
?>
30+
--EXPECT--
31+
Testing IntlDateFormatter locale canonicalization:
32+
Input: pt -> Expected: pt -> Actual: pt -> PASS
33+
Input: pt-PT -> Expected: pt_PT -> Actual: pt_PT -> PASS
34+
Input: pt_PT.utf8 -> Expected: pt_PT -> Actual: pt_PT -> PASS
35+
Input: fr_CA@euro -> Expected: fr_CA -> Actual: fr_CA -> PASS
36+
37+
DateFormatter with pt_PT.utf8: 09/08/23
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
--TEST--
2+
Fix GH-11942: NumberFormatter should canonicalize locale strings
3+
--EXTENSIONS--
4+
intl
5+
--FILE--
6+
<?php
7+
8+
$test_cases = [
9+
['pt', 'pt'],
10+
['pt-PT', 'pt_PT'],
11+
['pt_PT.utf8', 'pt_PT'],
12+
['fr_CA@euro', 'fr_CA'],
13+
];
14+
15+
echo "Testing NumberFormatter locale canonicalization:\n";
16+
foreach ($test_cases as $test_case) {
17+
[$input, $expected] = $test_case;
18+
19+
$formatter = new NumberFormatter($input, NumberFormatter::DECIMAL);
20+
$actual = $formatter->getLocale();
21+
22+
$status = ($actual === $expected) ? 'PASS' : 'FAIL';
23+
echo "Input: $input -> Expected: $expected -> Actual: $actual -> $status\n";
24+
}
25+
26+
$numFormatter = new NumberFormatter('pt_PT.utf8', NumberFormatter::DECIMAL);
27+
$numResult = $numFormatter->format(1234.56);
28+
echo "\nNumberFormatter with pt_PT.utf8: " . $numResult . "\n";
29+
?>
30+
--EXPECT--
31+
Testing NumberFormatter locale canonicalization:
32+
Input: pt -> Expected: pt -> Actual: pt -> PASS
33+
Input: pt-PT -> Expected: pt_PT -> Actual: pt_PT -> PASS
34+
Input: pt_PT.utf8 -> Expected: pt_PT -> Actual: pt_PT -> PASS
35+
Input: fr_CA@euro -> Expected: fr_CA -> Actual: fr_CA -> PASS
36+
37+
NumberFormatter with pt_PT.utf8: 1 234,56

0 commit comments

Comments
 (0)