Skip to content

Commit cc4cfbf

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

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
@@ -32,6 +32,10 @@ PHP NEWS
3232
- FPM:
3333
. Fixed failed debug assertion when php_admin_value setting fails. (ilutov)
3434

35+
- Intl:
36+
. Fixed bug GH-11952 (Fix locale strings canonicalization for IntlDateFormatter
37+
and NumberFormatter). (alexandre-daubois)
38+
3539
- Opcache:
3640
. Fixed bug GH-19493 (JIT variable not stored before YIELD). (Arnaud)
3741

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"
@@ -110,7 +111,12 @@ static zend_result datefmt_ctor(INTERNAL_FUNCTION_PARAMETERS, zend_error_handlin
110111
if (locale_len == 0) {
111112
locale_str = (char *) intl_locale_get_default();
112113
}
113-
locale = Locale::createFromName(locale_str);
114+
115+
char* canonicalized_locale = canonicalize_locale_string(locale_str);
116+
const char* final_locale = canonicalized_locale ? canonicalized_locale : locale_str;
117+
const char* stored_locale = canonicalized_locale ? canonicalized_locale : locale_str;
118+
119+
locale = Locale::createFromName(final_locale);
114120
/* get*Name accessors being set does not preclude being bogus */
115121
if (locale.isBogus() || ((locale_len == 1 && locale_str[0] != 'C') || (locale_len > 1 && strlen(locale.getISO3Language()) == 0))) {
116122
zend_argument_value_error(1, "\"%s\" is invalid", locale_str);
@@ -149,7 +155,7 @@ static zend_result datefmt_ctor(INTERNAL_FUNCTION_PARAMETERS, zend_error_handlin
149155
}
150156

151157
DATE_FORMAT_OBJECT(dfo) = udat_open((UDateFormatStyle)time_type,
152-
(UDateFormatStyle)date_type, locale_str, NULL, 0, svalue,
158+
(UDateFormatStyle)date_type, final_locale, NULL, 0, svalue,
153159
slength, &INTL_DATA_ERROR_CODE(dfo));
154160

155161
if (pattern_str && pattern_str_len > 0) {
@@ -182,9 +188,13 @@ static zend_result datefmt_ctor(INTERNAL_FUNCTION_PARAMETERS, zend_error_handlin
182188
dfo->date_type = date_type;
183189
dfo->time_type = time_type;
184190
dfo->calendar = calendar_type;
185-
dfo->requested_locale = estrdup(locale_str);
191+
/* Store the canonicalized locale, or fallback to original if canonicalization failed */
192+
dfo->requested_locale = estrdup(stored_locale);
186193

187194
error:
195+
if (canonicalized_locale) {
196+
efree(canonicalized_locale);
197+
}
188198
if (svalue) {
189199
efree(svalue);
190200
}

ext/intl/formatter/formatter_main.c

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,18 @@ static int numfmt_ctor(INTERNAL_FUNCTION_PARAMETERS, zend_error_handling *error_
6969
return FAILURE;
7070
}
7171

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

75-
if(spattern) {
75+
FORMATTER_OBJECT(nfo) = unum_open(style, spattern, spattern_len, final_locale, NULL, &INTL_DATA_ERROR_CODE(nfo));
76+
77+
if (spattern) {
7678
efree(spattern);
7779
}
80+
81+
if (canonicalized_locale) {
82+
efree(canonicalized_locale);
83+
}
7884

7985
INTL_CTOR_CHECK_STATUS(nfo, "numfmt_create: number formatter creation failed");
8086
return SUCCESS;

ext/intl/php_intl.c

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

99+
char* canonicalize_locale_string(const char* locale) {
100+
char canonicalized[ULOC_FULLNAME_CAPACITY];
101+
UErrorCode status = U_ZERO_ERROR;
102+
int32_t canonicalized_len;
103+
104+
canonicalized_len = uloc_canonicalize(locale, canonicalized, sizeof(canonicalized), &status);
105+
106+
if (U_FAILURE(status) || canonicalized_len <= 0) {
107+
return NULL;
108+
}
109+
110+
return estrdup(canonicalized);
111+
}
112+
99113
/* {{{ INI Settings */
100114
PHP_INI_BEGIN()
101115
STD_PHP_INI_ENTRY(LOCALE_INI_NAME, NULL, PHP_INI_ALL, OnUpdateStringUnempty, default_locale, zend_intl_globals, intl_globals)

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)