From e4f3252df05933d60b838da36c99843444d15989 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 25 Oct 2026 09:10:58 -0700 Subject: [PATCH 1/6] Date/Time: Clamp the start of the week in get_calendar(). The `start_of_week` option is cast to an int but otherwise unchecked, so a negative value makes `( $wdcount + $week_begins ) % 7` negative, and a value above 6 is out of range for `WP_Locale::get_weekday()`. Either leads to an undefined array key. Clamping it to 0-6 keeps the weekday headers valid and satisfies the new `int<0, 6>` type of `WP_Locale::get_weekday()`. Co-Authored-By: Claude Opus 5.5 --- src/wp-includes/general-template.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/general-template.php b/src/wp-includes/general-template.php index 5d6786db99bc8..660963dce0299 100644 --- a/src/wp-includes/general-template.php +++ b/src/wp-includes/general-template.php @@ -2666,7 +2666,7 @@ function get_calendar( $args = array() ) { } // week_begins = 0 stands for Sunday. - $week_begins = (int) get_option( 'start_of_week' ); + $week_begins = clamp( (int) get_option( 'start_of_week' ), 0, 6 ); // Read the current date. list( $current_year, $current_month, $current_day ) = array_map( 'intval', explode( '-', current_time( 'Y-m-j' ) ) ); From 01816068b5c018a23946d85b86c01c044d0728ea Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 25 Oct 2026 09:12:32 -0700 Subject: [PATCH 2/6] Date/Time: Handle a missing post weekday in the_weekday() and the_weekday_date(). `get_post_time()` returns false when the post date cannot be parsed. Both functions passed that straight to `WP_Locale::get_weekday()`, where it was coerced to the array key 0, so the post was silently labeled Sunday. Now nothing is output in that case, and the filterable string result is cast and clamped to 0-6 to match the `int<0, 6>` type of `WP_Locale::get_weekday()`. This removes the corresponding entry from the `argument.type` PHPStan baseline. Co-Authored-By: Claude Opus 5.5 --- src/wp-includes/general-template.php | 13 ++++++++++--- tests/phpstan/baselines/argument.type.neon | 5 ----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/wp-includes/general-template.php b/src/wp-includes/general-template.php index 660963dce0299..5d36449834005 100644 --- a/src/wp-includes/general-template.php +++ b/src/wp-includes/general-template.php @@ -3436,7 +3436,13 @@ function the_weekday() { return; } - $the_weekday = $wp_locale->get_weekday( get_post_time( 'w', false, $post ) ); + $weekday_number = get_post_time( 'w', false, $post ); + + if ( false === $weekday_number ) { + return; + } + + $the_weekday = $wp_locale->get_weekday( clamp( (int) $weekday_number, 0, 6 ) ); /** * Filters the localized weekday of the post, for display. @@ -3473,10 +3479,11 @@ function the_weekday_date( $before = '', $after = '' ) { } $the_weekday_date = ''; + $weekday_number = get_post_time( 'w', false, $post ); - if ( $currentday !== $previousweekday ) { + if ( false !== $weekday_number && $currentday !== $previousweekday ) { $the_weekday_date .= $before; - $the_weekday_date .= $wp_locale->get_weekday( get_post_time( 'w', false, $post ) ); + $the_weekday_date .= $wp_locale->get_weekday( clamp( (int) $weekday_number, 0, 6 ) ); $the_weekday_date .= $after; $previousweekday = $currentday; } diff --git a/tests/phpstan/baselines/argument.type.neon b/tests/phpstan/baselines/argument.type.neon index df374a6cd9c83..32a807efa68df 100644 --- a/tests/phpstan/baselines/argument.type.neon +++ b/tests/phpstan/baselines/argument.type.neon @@ -953,11 +953,6 @@ parameters: identifier: argument.type count: 2 path: ../../../src/wp-includes/general-template.php - - - message: '#^Parameter \#1 \$weekday_number of method WP_Locale\:\:get_weekday\(\) expects int\<0, 6\>, string\|false given\.$#' - identifier: argument.type - count: 2 - path: ../../../src/wp-includes/general-template.php - message: '#^Parameter \#2 \$replace of function str_replace expects array\\|string, int\<1, max\> given\.$#' identifier: argument.type From 96af5015a650f7cfaf1b84e4bb3889336097b5f5 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 25 Oct 2026 09:22:43 -0700 Subject: [PATCH 3/6] Docs: Narrow the get_post_time() return type by date format. For each format character whose output has a fixed range, such as `w` for `'0'` through `'6'` or `m` for `'01'` through `'12'`, the conditional return type now gives that range, mirroring what PHPStan infers for `DateTimeInterface::format()`. Formats with an open-ended numeric output give `numeric-string`. Formats that `wp_date()` translates (`D`, `l`, `M`, `F`, `a`, and `A`) and the PHP 8.2+ `X` and `x` remain `string`. With `w` narrowed, the weekday number passed to `WP_Locale::get_weekday()` in the_weekday() and the_weekday_date() already satisfies `int<0, 6>` after an int cast, so the `clamp()` calls are no longer needed. Co-Authored-By: Claude Opus 5.5 --- src/wp-includes/general-template.php | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/general-template.php b/src/wp-includes/general-template.php index 5d36449834005..df15e864e8c44 100644 --- a/src/wp-includes/general-template.php +++ b/src/wp-includes/general-template.php @@ -3181,7 +3181,21 @@ function get_the_time( $format = '', $post = null ) { * @return string|int|false Formatted date string or Unix timestamp if `$format` is 'U' or 'G'. * False on failure. * - * @phpstan-return ( $format is 'U'|'G' ? int|false : string|false ) + * @phpstan-return ( + * $format is 'U'|'G' ? int|false : ( + * $format is 'w' ? '0'|'1'|'2'|'3'|'4'|'5'|'6'|false : ( + * $format is 'N' ? '1'|'2'|'3'|'4'|'5'|'6'|'7'|false : ( + * $format is 'L'|'I' ? '0'|'1'|false : ( + * $format is 't' ? '28'|'29'|'30'|'31'|false : ( + * $format is 'm'|'h' ? '01'|'02'|'03'|'04'|'05'|'06'|'07'|'08'|'09'|'10'|'11'|'12'|false : ( + * $format is 'n'|'g' ? '1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'|'10'|'11'|'12'|false : ( + * $format is 'H' ? '00'|'01'|'02'|'03'|'04'|'05'|'06'|'07'|'08'|'09'|'10'|'11'|'12'|'13'|'14'|'15'|'16'|'17'|'18'|'19'|'20'|'21'|'22'|'23'|false : ( + * $format is 'd' ? '01'|'02'|'03'|'04'|'05'|'06'|'07'|'08'|'09'|'10'|'11'|'12'|'13'|'14'|'15'|'16'|'17'|'18'|'19'|'20'|'21'|'22'|'23'|'24'|'25'|'26'|'27'|'28'|'29'|'30'|'31'|false : ( + * $format is 'j' ? '1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'|'10'|'11'|'12'|'13'|'14'|'15'|'16'|'17'|'18'|'19'|'20'|'21'|'22'|'23'|'24'|'25'|'26'|'27'|'28'|'29'|'30'|'31'|false : ( + * $format is 'z'|'W'|'o'|'Y'|'y'|'B'|'i'|'s'|'u'|'v'|'Z' ? numeric-string|false : + * string|false + * ) ) ) ) ) ) ) ) ) ) + * ) */ function get_post_time( $format = 'U', $gmt = false, $post = null, $translate = false ) { $post = get_post( $post ); @@ -3442,7 +3456,7 @@ function the_weekday() { return; } - $the_weekday = $wp_locale->get_weekday( clamp( (int) $weekday_number, 0, 6 ) ); + $the_weekday = $wp_locale->get_weekday( (int) $weekday_number ); /** * Filters the localized weekday of the post, for display. @@ -3483,7 +3497,7 @@ function the_weekday_date( $before = '', $after = '' ) { if ( false !== $weekday_number && $currentday !== $previousweekday ) { $the_weekday_date .= $before; - $the_weekday_date .= $wp_locale->get_weekday( clamp( (int) $weekday_number, 0, 6 ) ); + $the_weekday_date .= $wp_locale->get_weekday( (int) $weekday_number ); $the_weekday_date .= $after; $previousweekday = $currentday; } From fbd1a8e04f7ccd30b2e50f92991ddc1f696bdb7b Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 25 Oct 2026 09:55:40 -0700 Subject: [PATCH 4/6] Date/Time: Pass the weekday number to WP_Locale::get_weekday() as an int in wp_date(). `DateTimeInterface::format( 'w' )` returns a string from `'0'` to `'6'`, which worked only because the string was coerced to an integer array key. Casting it to an int matches the `int<0, 6>` parameter type and removes the last `get_weekday()` entry from the `argument.type` PHPStan baseline. Co-Authored-By: Claude Opus 5.5 --- src/wp-includes/functions.php | 2 +- tests/phpstan/baselines/argument.type.neon | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 3b9e247fd44fd..33e6ba878893f 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -269,7 +269,7 @@ function wp_date( $format, $timestamp = null, $timezone = null ) { $new_format = ''; $format_length = strlen( $format ); $month = $wp_locale->get_month( $datetime->format( 'm' ) ); - $weekday = $wp_locale->get_weekday( $datetime->format( 'w' ) ); + $weekday = $wp_locale->get_weekday( (int) $datetime->format( 'w' ) ); for ( $i = 0; $i < $format_length; $i++ ) { switch ( $format[ $i ] ) { diff --git a/tests/phpstan/baselines/argument.type.neon b/tests/phpstan/baselines/argument.type.neon index 32a807efa68df..03306758d5130 100644 --- a/tests/phpstan/baselines/argument.type.neon +++ b/tests/phpstan/baselines/argument.type.neon @@ -903,11 +903,6 @@ parameters: identifier: argument.type count: 2 path: ../../../src/wp-includes/functions.php - - - message: '#^Parameter \#1 \$weekday_number of method WP_Locale\:\:get_weekday\(\) expects int\<0, 6\>, ''0''\|''1''\|''2''\|''3''\|''4''\|''5''\|''6'' given\.$#' - identifier: argument.type - count: 1 - path: ../../../src/wp-includes/functions.php - message: '#^Parameter \#2 \$fallback_url of function wp_validate_redirect expects string, false given\.$#' identifier: argument.type From bed0a3a82a372507b5c811b6731dbfe62c88f3eb Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 25 Oct 2026 10:11:26 -0700 Subject: [PATCH 5/6] Date/Time: Return false from wp_date() when the timestamp cannot be parsed. `wp_date()` only checked the timestamp with `is_numeric()`, which accepts values that `date_create( '@' . $timestamp )` rejects, such as `'1e3'`, `1e20`, `' 5'`, and `'.5'`. For those, `date_create()` returned false and the following `setTimezone()` call caused a fatal error. Now false is returned, as documented for invalid timestamp input and as already done for non-numeric input. This also resolves the PHPStan `method.nonObject` errors on every use of `$datetime` in the function. Co-Authored-By: Claude Opus 5.5 --- src/wp-includes/functions.php | 5 +++++ tests/phpunit/tests/date/wpDate.php | 26 ++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 33e6ba878893f..afa066aa8c8a7 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -258,6 +258,11 @@ function wp_date( $format, $timestamp = null, $timezone = null ) { } $datetime = date_create( '@' . $timestamp ); + + if ( false === $datetime ) { + return false; + } + $datetime->setTimezone( $timezone ); if ( empty( $wp_locale->month ) || empty( $wp_locale->weekday ) ) { diff --git a/tests/phpunit/tests/date/wpDate.php b/tests/phpunit/tests/date/wpDate.php index cff2f11967f3b..ef051fe8bd058 100644 --- a/tests/phpunit/tests/date/wpDate.php +++ b/tests/phpunit/tests/date/wpDate.php @@ -34,6 +34,32 @@ public function test_should_return_false_on_invalid_timestamp() { $this->assertFalse( wp_date( DATE_RFC3339, 'invalid' ) ); } + /** + * Tests that a numeric timestamp which {@see date_create()} cannot parse returns false. + * + * @dataProvider data_should_return_false_on_unparsable_numeric_timestamp + * + * @param int|float|string $timestamp Numeric timestamp. + */ + public function test_should_return_false_on_unparsable_numeric_timestamp( $timestamp ): void { + $this->assertTrue( is_numeric( $timestamp ), 'Expected the timestamp to pass the is_numeric() check.' ); + $this->assertFalse( wp_date( DATE_RFC3339, $timestamp ) ); // @phpstan-ignore argument.type (Intentionally invalid input.) + } + + /** + * Data provider for {@see self::test_should_return_false_on_unparsable_numeric_timestamp()}. + * + * @return array + */ + public static function data_should_return_false_on_unparsable_numeric_timestamp(): array { + return array( + 'exponent notation' => array( '1e3' ), + 'out-of-range float' => array( 1e20 ), + 'leading whitespace' => array( ' 5' ), + 'leading decimal' => array( '.5' ), + ); + } + /** * @ticket 48319 */ From e26b0b9e18e4c0571e4690c1180bd8d547cbdc99 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 25 Oct 2026 10:18:04 -0700 Subject: [PATCH 6/6] Docs: Keep the full month number type for WP_Locale::get_month(). Unlike the other `WP_Locale` getters, `get_month()` returns an empty string for a month number that is not found, which was added so that callers like `single_month_title()` can pass unvalidated query var values. Narrowing its parameter to valid month numbers told callers to validate input the method already handles, surfacing errors at call sites that pass `zeroise()` results, query vars, or database values. Co-Authored-By: Claude Opus 5.5 --- src/wp-includes/class-wp-locale.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/wp-includes/class-wp-locale.php b/src/wp-includes/class-wp-locale.php index 841007f15a6c1..20931182e5b39 100644 --- a/src/wp-includes/class-wp-locale.php +++ b/src/wp-includes/class-wp-locale.php @@ -318,8 +318,6 @@ public function get_weekday_abbrev( $weekday_name ) { * * @param string|int $month_number '01' through '12'. * @return string Translated full month name. If the month number is not found, an empty string is returned. - * - * @phpstan-param int<1, 12>|'1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'|'10'|'11'|'12'|'01'|'02'|'03'|'04'|'05'|'06'|'07'|'08'|'09' $month_number */ public function get_month( $month_number ) { $month_number = zeroise( $month_number, 2 );