Skip to content

Commit e465beb

Browse files
Fix DateTimeValueCleaner for years in the far future
Since PHP 8.3.25, cal_days_in_month() refuses to operate outside of what we previously called the “sane range” here, throwing a ValueError instead of silently giving a slightly wrong result (28 instead of 29 days for February 92000000200). To avoid this error, bring the year into a PHP-safe range using string operations (so we don’t introduce any numerical imprecision / overflow issues). Note that this *changes* the result for affected dates (year 2^31 or higher, precision day or higher) in all PHP versions, but in a way that I think is more correct. (And, really, I very much doubt anybody will notice in practice. As of this writing, there are only 14 distinct time values in Wikidata in excess of 2^31 years, and none of them are in February or have a precision higher than year: https://w.wiki/Fwqd.) Bug: T409269 Change-Id: Ibde22c1e8832b0c46301529eb613fe45dc6f4703
1 parent 8bcf275 commit e465beb

File tree

2 files changed

+16
-3
lines changed

2 files changed

+16
-3
lines changed

repo/includes/Rdf/DateTimeValueCleaner.php

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,16 +76,24 @@ protected function cleanupGregorianValue( $dateValue, $precision ) {
7676

7777
// check if the date "looks safe". If not, we do deeper check
7878
if ( !( $d <= 28 || ( $m != 2 && $d <= 30 ) ) ) {
79+
// clamp the day to the last day in the month according to PHP
80+
// (note: $safeYear is only used for cal_days_in_month())
81+
7982
// PHP source docs say PHP gregorian calendar can work down to 4714 BC
8083
// Use float conversion here since we don't care about precision but don't want overflows.
8184
if ( $minus && (float)$y >= 4714 ) {
8285
$safeYear = -4713;
8386
} else {
84-
$safeYear = (int)$y * ( $minus ? -1 : 1 );
87+
// since PHP 8.3.25, PHP also requires a year less than 2147483646 (2^31), see T409269;
88+
// as the Gregorian calendar has a 400-year period and 10000 is a clean multiple of 400,
89+
// shift overlong years into the safe range [100000, 199999] without losing integer precision
90+
$safeYear = (int)(
91+
strlen( $y ) >= 10 ? '1' . substr( $y, -5 ) : $y
92+
) * (
93+
$minus ? -1 : 1
94+
);
8595
}
8696

87-
// This will convert $y to int. If it's not within sane range,
88-
// Feb 29 may be mangled, but this will be rare.
8997
$max = cal_days_in_month( CAL_GREGORIAN, $m, $safeYear );
9098
// We just put it as the last day in month, won't bother further
9199
if ( $d > $max ) {

repo/tests/phpunit/includes/Rdf/DateTimeValueCleanerTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ public static function provideDates() {
5858
[ '+02000000200-00-00T00:00:00Z', $greg, '2000000200-01-01T00:00:00Z' ],
5959
[ '+92000000200-05-31T00:00:00Z', $greg, '92000000200-01-01T00:00:00Z', $year1m ],
6060
[ '+92000000200-05-31T00:00:00Z', $greg, '92000000200-05-31T00:00:00Z' ],
61+
[ '+92000000200-02-31T00:00:00Z', $greg, '92000000200-02-28T00:00:00Z' ],
62+
[ '+92000000201-02-31T00:00:00Z', $greg, '92000000201-02-28T00:00:00Z' ],
63+
[ '+92000000204-02-31T00:00:00Z', $greg, '92000000204-02-29T00:00:00Z' ],
64+
[ '+92000000400-02-31T00:00:00Z', $greg, '92000000400-02-29T00:00:00Z' ],
65+
[ '+92000000000-02-31T00:00:00Z', $greg, '92000000000-02-29T00:00:00Z' ],
6166
[ '-02000000200-05-22T00:00:00Z', $greg, '-2000000200-05-22T00:00:00Z' ],
6267
[ '-02000000200-02-31T00:00:00Z', $greg, '-2000000200-02-29T00:00:00Z' ],
6368
[ '+00000000200-02-31T00:00:00Z', $greg, '0200-02-28T00:00:00Z' ],

0 commit comments

Comments
 (0)