Skip to content

Commit 6ae86c2

Browse files
committedJul 22, 2022
Fixed bug #80022: Support ISO 8601 years outside 0000-9999 range better
1 parent 8ea587a commit 6ae86c2

9 files changed

+122
-39
lines changed
 

‎ext/date/php_date.c

+42-5
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ PHPAPI time_t php_time(void)
134134
* zone = (( "+" / "-" ) 4DIGIT)
135135
*/
136136
#define DATE_FORMAT_RFC2822 "D, d M Y H:i:s O"
137+
137138
/*
138139
* RFC3339, Section 5.6: http://www.ietf.org/rfc/rfc3339.txt
139140
* date-fullyear = 4DIGIT
@@ -156,8 +157,42 @@ PHPAPI time_t php_time(void)
156157
*/
157158
#define DATE_FORMAT_RFC3339 "Y-m-d\\TH:i:sP"
158159

160+
/*
161+
* This format does not technically match the ISO 8601 standard, as it does not
162+
* use : in the UTC offset format specifier. This is kept for BC reasons. The
163+
* DATE_FORMAT_ISO8601_EXPANDED format does correct this, as well as adding
164+
* support for years out side of the traditional 0000-9999 range.
165+
*/
159166
#define DATE_FORMAT_ISO8601 "Y-m-d\\TH:i:sO"
160167

168+
/* ISO 8601:2004(E)
169+
*
170+
* Section 3.5 Expansion:
171+
* By mutual agreement of the partners in information interchange, it is
172+
* permitted to expand the component identifying the calendar year, which is
173+
* otherwise limited to four digits. This enables reference to dates and times
174+
* in calendar years outside the range supported by complete representations,
175+
* i.e. before the start of the year [0000] or after the end of the year
176+
* [9999]."
177+
*
178+
* Section 4.1.2.4 Expanded representations:
179+
* If, by agreement, expanded representations are used, the formats shall be as
180+
* specified below. The interchange parties shall agree the additional number of
181+
* digits in the time element year. In the examples below it has been agreed to
182+
* expand the time element year with two digits.
183+
* Extended format: ±YYYYY-MM-DD
184+
* Example: +001985-04-12
185+
*
186+
* PHP's year expansion digits are variable.
187+
*/
188+
#define DATE_FORMAT_ISO8601_EXPANDED "X-m-d\\TH:i:sP"
189+
190+
/* Internal Only
191+
* This format only extends the year when needed, keeping the 'P' format with
192+
* colon for UTC offsets
193+
*/
194+
#define DATE_FORMAT_ISO8601_LARGE_YEAR "x-m-d\\TH:i:sP"
195+
161196
/*
162197
* RFC3339, Appendix A: http://www.ietf.org/rfc/rfc3339.txt
163198
* ISO 8601 also requires (in section 5.3.1.3) that a decimal fraction
@@ -659,6 +694,8 @@ static zend_string *date_format(const char *format, size_t format_len, timelib_t
659694
case 'L': length = slprintf(buffer, sizeof(buffer), "%d", timelib_is_leap((int) t->y)); break;
660695
case 'y': length = slprintf(buffer, sizeof(buffer), "%02d", (int) (t->y % 100)); break;
661696
case 'Y': length = slprintf(buffer, sizeof(buffer), "%s%04lld", t->y < 0 ? "-" : "", php_date_llabs((timelib_sll) t->y)); break;
697+
case 'x': length = slprintf(buffer, sizeof(buffer), "%s%04lld", t->y < 0 ? "-" : (t->y >= 10000 ? "+" : ""), php_date_llabs((timelib_sll) t->y)); break;
698+
case 'X': length = slprintf(buffer, sizeof(buffer), "%s%04lld", t->y < 0 ? "-" : "+", php_date_llabs((timelib_sll) t->y)); break;
662699

663700
/* time */
664701
case 'a': length = slprintf(buffer, sizeof(buffer), "%s", t->h >= 12 ? "pm" : "am"); break;
@@ -1810,7 +1847,7 @@ static void date_object_to_hash(php_date_obj *dateobj, HashTable *props)
18101847
zval zv;
18111848

18121849
/* first we add the date and time in ISO format */
1813-
ZVAL_STR(&zv, date_format("Y-m-d H:i:s.u", sizeof("Y-m-d H:i:s.u")-1, dateobj->time, 1));
1850+
ZVAL_STR(&zv, date_format("x-m-d H:i:s.u", sizeof("x-m-d H:i:s.u")-1, dateobj->time, 1));
18141851
zend_hash_str_update(props, "date", sizeof("date")-1, &zv);
18151852

18161853
/* then we add the timezone name (or similar) */
@@ -3800,7 +3837,7 @@ PHP_FUNCTION(timezone_transitions_get)
38003837
#define add_nominal() \
38013838
array_init(&element); \
38023839
add_assoc_long(&element, "ts", timestamp_begin); \
3803-
add_assoc_str(&element, "time", php_format_date(DATE_FORMAT_ISO8601, 13, timestamp_begin, 0)); \
3840+
add_assoc_str(&element, "time", php_format_date(DATE_FORMAT_ISO8601_LARGE_YEAR, 13, timestamp_begin, 0)); \
38043841
add_assoc_long(&element, "offset", tzobj->tzi.tz->type[0].offset); \
38053842
add_assoc_bool(&element, "isdst", tzobj->tzi.tz->type[0].isdst); \
38063843
add_assoc_string(&element, "abbr", &tzobj->tzi.tz->timezone_abbr[tzobj->tzi.tz->type[0].abbr_idx]); \
@@ -3809,7 +3846,7 @@ PHP_FUNCTION(timezone_transitions_get)
38093846
#define add(i,ts) \
38103847
array_init(&element); \
38113848
add_assoc_long(&element, "ts", ts); \
3812-
add_assoc_str(&element, "time", php_format_date(DATE_FORMAT_ISO8601, 13, ts, 0)); \
3849+
add_assoc_str(&element, "time", php_format_date(DATE_FORMAT_ISO8601_LARGE_YEAR, 13, ts, 0)); \
38133850
add_assoc_long(&element, "offset", tzobj->tzi.tz->type[tzobj->tzi.tz->trans_idx[i]].offset); \
38143851
add_assoc_bool(&element, "isdst", tzobj->tzi.tz->type[tzobj->tzi.tz->trans_idx[i]].isdst); \
38153852
add_assoc_string(&element, "abbr", &tzobj->tzi.tz->timezone_abbr[tzobj->tzi.tz->type[tzobj->tzi.tz->trans_idx[i]].abbr_idx]); \
@@ -3818,7 +3855,7 @@ PHP_FUNCTION(timezone_transitions_get)
38183855
#define add_by_index(i,ts) \
38193856
array_init(&element); \
38203857
add_assoc_long(&element, "ts", ts); \
3821-
add_assoc_str(&element, "time", php_format_date(DATE_FORMAT_ISO8601, 13, ts, 0)); \
3858+
add_assoc_str(&element, "time", php_format_date(DATE_FORMAT_ISO8601_LARGE_YEAR, 13, ts, 0)); \
38223859
add_assoc_long(&element, "offset", tzobj->tzi.tz->type[i].offset); \
38233860
add_assoc_bool(&element, "isdst", tzobj->tzi.tz->type[i].isdst); \
38243861
add_assoc_string(&element, "abbr", &tzobj->tzi.tz->timezone_abbr[tzobj->tzi.tz->type[i].abbr_idx]); \
@@ -3827,7 +3864,7 @@ PHP_FUNCTION(timezone_transitions_get)
38273864
#define add_from_tto(to,ts) \
38283865
array_init(&element); \
38293866
add_assoc_long(&element, "ts", ts); \
3830-
add_assoc_str(&element, "time", php_format_date(DATE_FORMAT_ISO8601, 13, ts, 0)); \
3867+
add_assoc_str(&element, "time", php_format_date(DATE_FORMAT_ISO8601_LARGE_YEAR, 13, ts, 0)); \
38313868
add_assoc_long(&element, "offset", (to)->offset); \
38323869
add_assoc_bool(&element, "isdst", (to)->is_dst); \
38333870
add_assoc_string(&element, "abbr", (to)->abbr); \

‎ext/date/php_date.stub.php

+8
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@
2020
*/
2121
const DATE_ISO8601 = "Y-m-d\\TH:i:sO";
2222

23+
/**
24+
* @var string
25+
* @cvalue DATE_FORMAT_ISO8601_EXPANDED
26+
*/
27+
const DATE_ISO8601_EXPANDED = "X-m-d\\TH:i:sP";
28+
2329
/**
2430
* @var string
2531
* @cvalue DATE_FORMAT_RFC822
@@ -285,6 +291,8 @@ interface DateTimeInterface
285291
/** @var string */
286292
public const ISO8601 = DATE_ISO8601;
287293
/** @var string */
294+
public const ISO8601_EXPANDED = DATE_ISO8601_EXPANDED;
295+
/** @var string */
288296
public const RFC822 = DATE_RFC822;
289297
/** @var string */
290298
public const RFC850 = DATE_RFC850;

‎ext/date/php_date_arginfo.h

+10-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎ext/date/tests/DateTimeZone_getTransitions_basic1.phpt

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ array(5) {
3232
["ts"]=>
3333
int(-213228000)
3434
["time"]=>
35-
string(24) "1963-03-31T02:00:00+0000"
35+
string(25) "1963-03-31T02:00:00+00:00"
3636
["offset"]=>
3737
int(3600)
3838
["isdst"]=>

‎ext/date/tests/DateTimeZone_getTransitions_bug1.phpt

+23-23
Original file line numberDiff line numberDiff line change
@@ -24,35 +24,35 @@ showTransitions('Europe/Paris', 1645095600); // GH Issue 8108
2424
--EXPECT--
2525
Europe/London from @1648342200-@1727398200:
2626

27-
1648342200 2022-03-27T00:50:00+0000 0 x GMT
28-
1648342800 2022-03-27T01:00:00+0000 3600 DST BST
29-
1667091600 2022-10-30T01:00:00+0000 0 x GMT
30-
1679792400 2023-03-26T01:00:00+0000 3600 DST BST
31-
1698541200 2023-10-29T01:00:00+0000 0 x GMT
32-
1711846800 2024-03-31T01:00:00+0000 3600 DST BST
27+
1648342200 2022-03-27T00:50:00+00:00 0 x GMT
28+
1648342800 2022-03-27T01:00:00+00:00 3600 DST BST
29+
1667091600 2022-10-30T01:00:00+00:00 0 x GMT
30+
1679792400 2023-03-26T01:00:00+00:00 3600 DST BST
31+
1698541200 2023-10-29T01:00:00+00:00 0 x GMT
32+
1711846800 2024-03-31T01:00:00+00:00 3600 DST BST
3333

3434
America/Los_Angeles from @1648557596-@1727613596:
3535

36-
1648557596 2022-03-29T12:39:56+0000 -25200 DST PDT
37-
1667725200 2022-11-06T09:00:00+0000 -28800 x PST
38-
1678615200 2023-03-12T10:00:00+0000 -25200 DST PDT
39-
1699174800 2023-11-05T09:00:00+0000 -28800 x PST
40-
1710064800 2024-03-10T10:00:00+0000 -25200 DST PDT
36+
1648557596 2022-03-29T12:39:56+00:00 -25200 DST PDT
37+
1667725200 2022-11-06T09:00:00+00:00 -28800 x PST
38+
1678615200 2023-03-12T10:00:00+00:00 -25200 DST PDT
39+
1699174800 2023-11-05T09:00:00+00:00 -28800 x PST
40+
1710064800 2024-03-10T10:00:00+00:00 -25200 DST PDT
4141

4242
America/Chicago from @1293861600-@1372917600:
4343

44-
1293861600 2011-01-01T06:00:00+0000 -21600 x CST
45-
1300003200 2011-03-13T08:00:00+0000 -18000 DST CDT
46-
1320562800 2011-11-06T07:00:00+0000 -21600 x CST
47-
1331452800 2012-03-11T08:00:00+0000 -18000 DST CDT
48-
1352012400 2012-11-04T07:00:00+0000 -21600 x CST
49-
1362902400 2013-03-10T08:00:00+0000 -18000 DST CDT
44+
1293861600 2011-01-01T06:00:00+00:00 -21600 x CST
45+
1300003200 2011-03-13T08:00:00+00:00 -18000 DST CDT
46+
1320562800 2011-11-06T07:00:00+00:00 -21600 x CST
47+
1331452800 2012-03-11T08:00:00+00:00 -18000 DST CDT
48+
1352012400 2012-11-04T07:00:00+00:00 -21600 x CST
49+
1362902400 2013-03-10T08:00:00+00:00 -18000 DST CDT
5050

5151
Europe/Paris from @1645095600-@1724151600:
5252

53-
1645095600 2022-02-17T11:00:00+0000 3600 x CET
54-
1648342800 2022-03-27T01:00:00+0000 7200 DST CEST
55-
1667091600 2022-10-30T01:00:00+0000 3600 x CET
56-
1679792400 2023-03-26T01:00:00+0000 7200 DST CEST
57-
1698541200 2023-10-29T01:00:00+0000 3600 x CET
58-
1711846800 2024-03-31T01:00:00+0000 7200 DST CEST
53+
1645095600 2022-02-17T11:00:00+00:00 3600 x CET
54+
1648342800 2022-03-27T01:00:00+00:00 7200 DST CEST
55+
1667091600 2022-10-30T01:00:00+00:00 3600 x CET
56+
1679792400 2023-03-26T01:00:00+00:00 7200 DST CEST
57+
1698541200 2023-10-29T01:00:00+00:00 3600 x CET
58+
1711846800 2024-03-31T01:00:00+00:00 7200 DST CEST

‎ext/date/tests/bug75035.phpt

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
--TEST--
2+
Bug #75035 (Datetime fails to unserialize "extreme" dates)
3+
--INI--
4+
date.timezone=UTC
5+
--FILE--
6+
<?php
7+
var_dump('PHP version', PHP_VERSION);
8+
9+
foreach ([PHP_INT_MIN, PHP_INT_MAX] as $extreme) {
10+
$i = 64;
11+
while ($i --> 0) {
12+
$d = new DateTime('@' . ($extreme >> $i));
13+
$s = serialize($d);
14+
try {
15+
$u = unserialize($s);
16+
} catch (Error $e) {
17+
$u = "failed unserialization: " . $e->getMessage() . ' : ' . $s;
18+
}
19+
$original = $d->format('Y-m-d H:i:s');
20+
$serializedUnserialized = is_string($u) ? $u : $u->format('Y-m-d H:i:s');
21+
if ($original !== $serializedUnserialized) {
22+
var_dump('[' . ($extreme >> $i) . '] ' . $original . ' => ' . $serializedUnserialized);
23+
}
24+
}
25+
}
26+
?>
27+
--EXPECTF--
28+
string(11) "PHP version"
29+
string(%d) "%s"

‎ext/date/tests/bug80963.phpt

+3-3
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ array(5) {
1919
["ts"]=>
2020
int(2140045200)
2121
["time"]=>
22-
string(24) "2037-10-25T01:00:00+0000"
22+
string(25) "2037-10-25T01:00:00+00:00"
2323
["offset"]=>
2424
int(0)
2525
["isdst"]=>
@@ -32,7 +32,7 @@ array(5) {
3232
["ts"]=>
3333
int(2140668000)
3434
["time"]=>
35-
string(24) "2037-11-01T06:00:00+0000"
35+
string(25) "2037-11-01T06:00:00+00:00"
3636
["offset"]=>
3737
int(-18000)
3838
["isdst"]=>
@@ -45,7 +45,7 @@ array(5) {
4545
["ts"]=>
4646
int(2140045200)
4747
["time"]=>
48-
string(24) "2037-10-25T01:00:00+0000"
48+
string(25) "2037-10-25T01:00:00+00:00"
4949
["offset"]=>
5050
int(3600)
5151
["isdst"]=>

‎ext/date/tests/bug81504.phpt

+5-5
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ foreach ($tz->getTransitions(strtotime("1996-01-01"), strtotime("1997-12-31")) a
1111
}
1212
?>
1313
--EXPECT--
14-
1996-01-01T00:00:00+0000 3600 CET
15-
1996-03-31T01:00:00+0000 7200 CEST
16-
1996-10-27T01:00:00+0000 3600 CET
17-
1997-03-30T01:00:00+0000 7200 CEST
18-
1997-10-26T01:00:00+0000 3600 CET
14+
1996-01-01T00:00:00+00:00 3600 CET
15+
1996-03-31T01:00:00+00:00 7200 CEST
16+
1996-10-27T01:00:00+00:00 3600 CET
17+
1997-03-30T01:00:00+00:00 7200 CEST
18+
1997-10-26T01:00:00+00:00 3600 CET

‎ext/date/tests/timezone_transitions_get_basic1.phpt

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ array(5) {
3535
["ts"]=>
3636
int(-213228000)
3737
["time"]=>
38-
string(24) "1963-03-31T02:00:00+0000"
38+
string(25) "1963-03-31T02:00:00+00:00"
3939
["offset"]=>
4040
int(3600)
4141
["isdst"]=>

0 commit comments

Comments
 (0)
Please sign in to comment.