diff --git a/inc/Blocks/Calendar/Grouping/MultiDayResolver.php b/inc/Blocks/Calendar/Grouping/MultiDayResolver.php index 71ef2b10..dd76de7a 100644 --- a/inc/Blocks/Calendar/Grouping/MultiDayResolver.php +++ b/inc/Blocks/Calendar/Grouping/MultiDayResolver.php @@ -44,6 +44,13 @@ public static function is_multi_day( array $event_data ): bool { return false; } + // Placeholder / TBD dates (e.g. "2026-07-??") are not parseable and + // cannot be expanded into a date range. Treat them as single-day so + // the calendar still renders rather than throwing a fatal. + if ( ! self::is_valid_date( $start_date ) || ! self::is_valid_date( $end_date ) ) { + return false; + } + $start = new DateTime( $start_date ); $end = new DateTime( $end_date ); $diff = $start->diff( $end )->days; @@ -75,6 +82,17 @@ public static function is_multi_day( array $event_data ): bool { public static function get_date_range( string $start_date, string $end_date, DateTimeZone $event_tz ): array { $dates = array(); + // Guard against placeholder / malformed dates that would throw a + // DateMalformedStringException. is_multi_day() already filters these, + // but get_date_range() is a public entry point in its own right. + if ( ! self::is_valid_date( $start_date ) || ! self::is_valid_date( $end_date ) ) { + if ( self::is_valid_date( $start_date ) ) { + $dates[] = $start_date; + } + + return $dates; + } + $start = new DateTime( $start_date, $event_tz ); $end = new DateTime( $end_date, $event_tz ); @@ -89,4 +107,23 @@ public static function get_date_range( string $start_date, string $end_date, Dat return $dates; } + + /** + * Strictly validate a Y-m-d date string. + * + * Rejects placeholder / TBD values (e.g. "2026-07-??") and any string + * that does not represent a real calendar date, so callers can avoid + * passing unparseable input into DateTime and triggering a + * DateMalformedStringException. + * + * @param string $date Date string to validate. + * @return bool True when $date is a valid Y-m-d calendar date. + */ + private static function is_valid_date( string $date ): bool { + if ( ! preg_match( '/^(\d{4})-(\d{2})-(\d{2})$/', $date, $matches ) ) { + return false; + } + + return checkdate( (int) $matches[2], (int) $matches[3], (int) $matches[1] ); + } } diff --git a/tests/Unit/MultiDayResolverTest.php b/tests/Unit/MultiDayResolverTest.php new file mode 100644 index 00000000..696980a4 --- /dev/null +++ b/tests/Unit/MultiDayResolverTest.php @@ -0,0 +1,105 @@ + '2026-07-01', + 'endDate' => '2026-07-??', + ) + ); + + $this->assertFalse( $result, 'Placeholder dates cannot form a range and must be treated as single-day.' ); + } + + public function test_is_multi_day_handles_placeholder_start_date_without_throwing() { + $result = MultiDayResolver::is_multi_day( + array( + 'startDate' => '2026-??-??', + 'endDate' => '2026-07-05', + ) + ); + + $this->assertFalse( $result ); + } + + public function test_is_multi_day_returns_false_for_invalid_calendar_dates() { + $result = MultiDayResolver::is_multi_day( + array( + 'startDate' => '2026-02-30', + 'endDate' => '2026-03-05', + ) + ); + + $this->assertFalse( $result ); + } + + public function test_is_multi_day_true_for_genuine_multi_day_span() { + $result = MultiDayResolver::is_multi_day( + array( + 'startDate' => '2026-07-01', + 'endDate' => '2026-07-05', + ) + ); + + $this->assertTrue( $result ); + } + + public function test_is_multi_day_false_for_same_day() { + $result = MultiDayResolver::is_multi_day( + array( + 'startDate' => '2026-07-01', + 'endDate' => '2026-07-01', + ) + ); + + $this->assertFalse( $result ); + } + + public function test_get_date_range_returns_empty_for_malformed_dates() { + $tz = new DateTimeZone( 'America/New_York' ); + $range = MultiDayResolver::get_date_range( '2026-07-??', '2026-07-??', $tz ); + + $this->assertSame( array(), $range ); + } + + public function test_get_date_range_returns_single_start_when_end_is_malformed() { + $tz = new DateTimeZone( 'America/New_York' ); + $range = MultiDayResolver::get_date_range( '2026-07-01', '2026-07-??', $tz ); + + $this->assertSame( array( '2026-07-01' ), $range ); + } + + public function test_get_date_range_expands_valid_span() { + $tz = new DateTimeZone( 'America/New_York' ); + $range = MultiDayResolver::get_date_range( '2026-07-01', '2026-07-03', $tz ); + + $this->assertSame( + array( '2026-07-01', '2026-07-02', '2026-07-03' ), + $range + ); + } +}