From b8550f49a479522d1f67bc0d1a7ee168166e4160 Mon Sep 17 00:00:00 2001 From: Paul Bearne Date: Mon, 2 Jun 2025 20:02:45 -0400 Subject: [PATCH 01/23] Refactor EXIF date handling and add unit tests Replaced `wp_exif_date2ts` logic with the new `wp_exif_datetime` function for cleaner and reusable code. Enhanced metadata to store RFC3339 formatted dates while maintaining backward compatibility. Added comprehensive PHPUnit tests for EXIF date validation and edge cases. --- src/wp-admin/includes/image.php | 54 ++++++- .../tests/admin/includes/wpExicDatetime.php | 137 ++++++++++++++++++ .../tests/admin/includes/wpExifDate2TS.php | 77 ++++++++++ 3 files changed, 262 insertions(+), 6 deletions(-) create mode 100644 tests/phpunit/tests/admin/includes/wpExicDatetime.php create mode 100644 tests/phpunit/tests/admin/includes/wpExifDate2TS.php diff --git a/src/wp-admin/includes/image.php b/src/wp-admin/includes/image.php index 9fe2471669f99..3b6822b41f552 100644 --- a/src/wp-admin/includes/image.php +++ b/src/wp-admin/includes/image.php @@ -792,18 +792,46 @@ function wp_exif_frac2dec( $str ) { } /** - * Converts the exif date format to a unix timestamp. + * Convert the exif date format to DateTime object + * + * @since 6.9.0 + * + * @param string $str + * @param string $timezone Timezone or offset string. + * @return DateTimeImmutable|false Return false if not valid date + */ +function wp_exif_datetime( $str, $timezone = null ) { + if( ! is_string( $str ) || empty( $str ) ) { + + return false; + } + $timezone = ( $timezone ) ? new DateTimeZone( $timezone ) : wp_timezone(); + try { + $datetime = new DateTimeImmutable( $str, $timezone ); + } catch ( Exception $e ) { + + return false; + } + + return $datetime; +} + +/** + * Converts the exif date format to an unix timestamp. * * @since 2.5.0 + * @since 6.9.0 Function uses wp_exif_datetime to generate timestamp * * @param string $str A date string expected to be in Exif format (Y:m:d H:i:s). * @return int|false The unix timestamp, or false on failure. */ function wp_exif_date2ts( $str ) { - list( $date, $time ) = explode( ' ', trim( $str ) ); - list( $y, $m, $d ) = explode( ':', $date ); + $datetime = wp_exif_datetime( $str ); - return strtotime( "{$y}-{$m}-{$d} {$time}" ); + if ( $datetime instanceof DateTimeImmutable ) { + return $datetime->getTimestamp(); + } + return false; } /** @@ -907,7 +935,11 @@ function wp_read_image_metadata( $file ) { } if ( ! empty( $iptc['2#055'][0] ) && ! empty( $iptc['2#060'][0] ) ) { // Created date and time. - $meta['created_timestamp'] = strtotime( $iptc['2#055'][0] . ' ' . $iptc['2#060'][0] ); + $datetime = new DateTimeImmutable( $iptc['2#055'][0] . ' ' . $iptc['2#060'][0] ); + // Store as a RFC3339 formatted timestring as this includes both date, time, and timezone. + $meta['created'] = $datetime->format( DATE_RFC3339 ); + // Retain the original created timestamp for backcompat. + $meta['created_timestamp'] = $datetime->getTimestamp(); } if ( ! empty( $iptc['2#116'][0] ) ) { // Copyright. @@ -1015,7 +1047,17 @@ function wp_read_image_metadata( $file ) { $meta['camera'] = trim( $exif['Model'] ); } if ( empty( $meta['created_timestamp'] ) && ! empty( $exif['DateTimeDigitized'] ) ) { - $meta['created_timestamp'] = wp_exif_date2ts( $exif['DateTimeDigitized'] ); + $timezone = null; + if ( ! empty( $exif['UndefinedTag:0x9012'] ) ) { + $timezone = $exif['UndefinedTag:0x9012']; + } + + $datetime = wp_exif_datetime( $exif['DateTimeDigitized'], $timezone ); + + // Store as a RFC3339 formatted timestring as this includes both date, time, and timezone. + $meta['created'] = $datetime->format( DATE_RFC3339 ); + // Retain the original created timestamp for backcompat. + $meta['created_timestamp'] = $datetime->getTimestamp(); } if ( ! empty( $exif['FocalLength'] ) ) { $meta['focal_length'] = (string) $exif['FocalLength']; diff --git a/tests/phpunit/tests/admin/includes/wpExicDatetime.php b/tests/phpunit/tests/admin/includes/wpExicDatetime.php new file mode 100644 index 0000000000000..4cfa09b42269b --- /dev/null +++ b/tests/phpunit/tests/admin/includes/wpExicDatetime.php @@ -0,0 +1,137 @@ +assertEquals( $expected, $datetime->format( 'Y:m:d H:i:s' ) ); + } + + /** + * Test handling of invalid dates + * + * @dataProvider provideInvalidDates + */ + public function test_invalid_dates( $input_date ) { + $this->assertFalse( wp_exif_datetime( $input_date ) ); + } + + /** + * Data provider for valid dates + */ + public function provideValidDates() { + return [ + // Unix timestamps + 'unix timestamp' => [ + '1710500000', + '0000:06:02 17:10:50' + ], + // MySQL format + 'mysql datetime' => [ + '2024-03-15 14:30:00', + '2024:03:15 14:30:00' + ], + // Already in EXIF format + 'exif format' => [ + '2024:03:15 14:30:00', + '2024:03:15 14:30:00' + ], + // Date only + 'mysql date only' => [ + '2024-03-15', + '2024:03:15 00:00:00' + ], + // Different time formats + 'with seconds' => [ + '2024-03-15 14:30:45', + '2024:03:15 14:30:45' + ], + 'time with T separator' => [ + '2024-03-15T14:30:00', + '2024:03:15 14:30:00' + ], + 'incomplete date' => [ + '2024-03', + '2024:03:01 00:00:00' + ], + 'future year' => [ + '2525-03-15 14:30:00', + '2525:03:15 14:30:00' + ], + ]; + } + + /** + * Data provider for invalid dates + */ + public function provideInvalidDates() { + return [ + 'empty string' => [ '' ], + '0' => [ '0' ], + 'null' => [ null ], + 'boolean false' => [ false ], + 'boolean true' => [ true ], + 'unix timestamp' => [ 1710500000 ], + 'invalid format' => [ 'not a date' ], + 'invalid month' => [ '2024-13-15' ], + 'invalid day' => [ '2024-03-32' ], + 'invalid time' => [ '2024-03-15 25:00:00' ], + 'garbage with numbers' => [ '2024abc15' ], + ]; + } + + /** + * Test timezone handling + */ + public function test_timezone_handling() { + $original_timezone = date_default_timezone_get(); + + // Test with different timezones + date_default_timezone_set( 'UTC' ); + $utc_result = wp_exif_datetime( '2024-03-15 14:30:00' ); + + date_default_timezone_set( 'America/New_York' ); + $ny_result = wp_exif_datetime( '2024-03-15 14:30:00' ); + + // Results should be consistent regardless of timezone + $this->assertEquals( $utc_result, $ny_result ); + + // Restore original timezone + date_default_timezone_set( $original_timezone ); + } + + /** + * Test handling of edge cases + */ + public function test_edge_cases() { + + // Test with a very old date + $datetime = wp_exif_datetime( '1900-01-01' ); + $this->assertEquals( '1900:01:01 00:00:00', $datetime->format( 'Y:m:d H:i:s' ) ); + + // Test with milliseconds + $datetime = wp_exif_datetime( '2024-03-15 14:30:00.123' ); + $this->assertEquals( '2024:03:15 14:30:00', $datetime->format( 'Y:m:d H:i:s' ) ); + } + + /** + * Test input with different separators + */ + public function test_different_separators() { + + $datetime = wp_exif_datetime( '2024/03/15 14:30:00' ); + $this->assertEquals( '2024:03:15 14:30:00', $datetime->format( 'Y:m:d H:i:s' ) ); + + $datetime = wp_exif_datetime( '2024/03/15 14:30:00' ); + $this->assertEquals( '2024:03:15 14:30:00', $datetime->format( 'Y:m:d H:i:s' ) ); + } +} diff --git a/tests/phpunit/tests/admin/includes/wpExifDate2TS.php b/tests/phpunit/tests/admin/includes/wpExifDate2TS.php new file mode 100644 index 0000000000000..937fec3a6b2a3 --- /dev/null +++ b/tests/phpunit/tests/admin/includes/wpExifDate2TS.php @@ -0,0 +1,77 @@ +assertEquals( $expected, wp_exif_date2ts( $date ) ); + } + + /** + * Test handling of invalid EXIF dates + * + * @dataProvider provideInvalidExifDates + */ + public function test_invalid_exif_dates( $date ) { + $this->assertFalse( wp_exif_date2ts( $date ) ); + } + + /** + * Data provider for valid EXIF dates + */ + public function provideValidExifDates() { + return [ + 'standard format' => [ + '2024:03:15 14:30:45', + strtotime( '2024:03:15 14:30:45' ) + ], + 'slash format' => [ + '2024/03/15 14:30:45', + strtotime( '2024:03:15 14:30:45' ) + ], + 'invalid format' => [ + '2024-03-15', + strtotime( '2024:03:15 00:00:00' ) + ], + ]; + } + + /** + * Data provider for invalid EXIF dates + */ + public function provideInvalidExifDates() { + return [ + 'empty string' => [ '' ], + 'null' => [ null ], + 'date only' => [ '2024:03:15' ], + 'incomplete date' => [ '2024:03' ], + 'garbage data' => [ 'not a date' ], + 'wrong order' => [ '15:03:2024 14:30:45' ], + 'with fractional seconds' => [ '2024:03:15 14:30:45.123' ] + ]; + } + + /** + * Test timezone handling + */ + public function test_timezone_handling() { + $original_timezone = date_default_timezone_get(); + + // Test with different timezone + date_default_timezone_set( 'UTC' ); + $utc_timestamp = wp_exif_date2ts( '2024:03:15 14:30:45' ); + + date_default_timezone_set( 'America/New_York' ); + $ny_timestamp = wp_exif_date2ts( '2024:03:15 14:30:45' ); + + // The timestamps should be equal regardless of timezone + $this->assertEquals( $utc_timestamp, $ny_timestamp ); + + // Restore original timezone + date_default_timezone_set( $original_timezone ); + } +} From 77b032d8bf7b9dc1c8c12f9ff63ca038a296819a Mon Sep 17 00:00:00 2001 From: Paul Bearne Date: Tue, 3 Jun 2025 08:49:41 -0400 Subject: [PATCH 02/23] docs --- .../tests/admin/includes/wpExicDatetime.php | 44 +++++++++++++++---- .../tests/admin/includes/wpExifDate2TS.php | 40 +++++++++++++---- 2 files changed, 68 insertions(+), 16 deletions(-) diff --git a/tests/phpunit/tests/admin/includes/wpExicDatetime.php b/tests/phpunit/tests/admin/includes/wpExicDatetime.php index 4cfa09b42269b..8cd4293e82691 100644 --- a/tests/phpunit/tests/admin/includes/wpExicDatetime.php +++ b/tests/phpunit/tests/admin/includes/wpExicDatetime.php @@ -1,15 +1,27 @@ assertFalse( wp_exif_datetime( $input_date ) ); @@ -90,7 +106,11 @@ public function provideInvalidDates() { } /** - * Test timezone handling + * @ticket 56887 + * + * Test consistent handling of datetime values across different timezones. + * + * @return void */ public function test_timezone_handling() { $original_timezone = date_default_timezone_get(); @@ -110,7 +130,11 @@ public function test_timezone_handling() { } /** - * Test handling of edge cases + * @ticket 56887 + * + * Test handling of edge case date and time formats. + * + * @return void */ public function test_edge_cases() { @@ -124,7 +148,11 @@ public function test_edge_cases() { } /** - * Test input with different separators + * @ticket 56887 + * + * Tests the functionality of parsing dates with different separators and ensures the output format is consistent. + * + * @return void */ public function test_different_separators() { diff --git a/tests/phpunit/tests/admin/includes/wpExifDate2TS.php b/tests/phpunit/tests/admin/includes/wpExifDate2TS.php index 937fec3a6b2a3..9b69c5868d578 100644 --- a/tests/phpunit/tests/admin/includes/wpExifDate2TS.php +++ b/tests/phpunit/tests/admin/includes/wpExifDate2TS.php @@ -1,20 +1,37 @@ assertEquals( $expected, wp_exif_date2ts( $date ) ); } /** - * Test handling of invalid EXIF dates + * @ticket 56887 + * + * Test conversion of invalid EXIF date formats to false + * + * @param string $date The EXIF date string to be tested. * - * @dataProvider provideInvalidExifDates + * @return void */ public function test_invalid_exif_dates( $date ) { $this->assertFalse( wp_exif_date2ts( $date ) ); @@ -25,15 +42,15 @@ public function test_invalid_exif_dates( $date ) { */ public function provideValidExifDates() { return [ - 'standard format' => [ + 'standard format' => [ '2024:03:15 14:30:45', strtotime( '2024:03:15 14:30:45' ) ], - 'slash format' => [ + 'slash format' => [ '2024/03/15 14:30:45', strtotime( '2024:03:15 14:30:45' ) ], - 'invalid format' => [ + 'invalid format' => [ '2024-03-15', strtotime( '2024:03:15 00:00:00' ) ], @@ -56,7 +73,14 @@ public function provideInvalidExifDates() { } /** - * Test timezone handling + * @ticket 56887 + * + * Tests the handling of timezones during EXIF date conversion to ensure consistent timestamps. + * + * Verifies that timestamps generated from the same EXIF date are consistent + * across different timezone settings. + * + * @return void */ public function test_timezone_handling() { $original_timezone = date_default_timezone_get(); From 5bbc114ba85d9047b8e270a6980d94624e5ec18c Mon Sep 17 00:00:00 2001 From: Paul Bearne Date: Tue, 3 Jun 2025 10:39:45 -0400 Subject: [PATCH 03/23] replace the short array syntax --- .../tests/admin/includes/wpExicDatetime.php | 183 +++++++----------- .../tests/admin/includes/wpExifDate2TS.php | 135 ++++++------- 2 files changed, 137 insertions(+), 181 deletions(-) diff --git a/tests/phpunit/tests/admin/includes/wpExicDatetime.php b/tests/phpunit/tests/admin/includes/wpExicDatetime.php index 8cd4293e82691..7e5e4be7e919a 100644 --- a/tests/phpunit/tests/admin/includes/wpExicDatetime.php +++ b/tests/phpunit/tests/admin/includes/wpExicDatetime.php @@ -1,165 +1,120 @@ assertEquals( $expected, $datetime->format( 'Y:m:d H:i:s' ) ); + $this->assertEquals( $expected, wp_exif_datetime( $input_date ) ); } /** - * @ticket 56887 - * - * Test handling of invalid date inputs. + * Test handling of invalid dates and exceptions * - * @param string $input_date The date string to be tested for validation. - * - * @return void + * @dataProvider provideInvalidDates */ - public function test_invalid_dates( $input_date ) { - $this->assertFalse( wp_exif_datetime( $input_date ) ); + public function test_invalid_dates( $input ) { + $this->assertFalse( wp_exif_datetime( $input ) ); } /** * Data provider for valid dates */ public function provideValidDates() { - return [ - // Unix timestamps - 'unix timestamp' => [ - '1710500000', - '0000:06:02 17:10:50' - ], - // MySQL format - 'mysql datetime' => [ + return array( + 'unix timestamp' => array( + 1710500000, // March 15, 2024 14:30:00 + '2024:03:15 14:30:00' + ), + 'mysql datetime' => array( '2024-03-15 14:30:00', '2024:03:15 14:30:00' - ], - // Already in EXIF format - 'exif format' => [ + ), + 'exif format' => array( '2024:03:15 14:30:00', '2024:03:15 14:30:00' - ], - // Date only - 'mysql date only' => [ + ), + 'mysql date only' => array( '2024-03-15', '2024:03:15 00:00:00' - ], - // Different time formats - 'with seconds' => [ - '2024-03-15 14:30:45', - '2024:03:15 14:30:45' - ], - 'time with T separator' => [ - '2024-03-15T14:30:00', - '2024:03:15 14:30:00' - ], - 'incomplete date' => [ - '2024-03', - '2024:03:01 00:00:00' - ], - 'future year' => [ - '2525-03-15 14:30:00', - '2525:03:15 14:30:00' - ], - ]; + ) + ); } /** - * Data provider for invalid dates + * Data provider for invalid dates that should trigger exceptions */ public function provideInvalidDates() { - return [ - 'empty string' => [ '' ], - '0' => [ '0' ], - 'null' => [ null ], - 'boolean false' => [ false ], - 'boolean true' => [ true ], - 'unix timestamp' => [ 1710500000 ], - 'invalid format' => [ 'not a date' ], - 'invalid month' => [ '2024-13-15' ], - 'invalid day' => [ '2024-03-32' ], - 'invalid time' => [ '2024-03-15 25:00:00' ], - 'garbage with numbers' => [ '2024abc15' ], - ]; + return array( + 'empty string' => array( '' ), + 'null' => array( null ), + 'boolean false' => array( false ), + 'boolean true' => array( true ), + 'invalid format' => array( 'not a date' ), + 'incomplete date' => array( '2024-03' ), + 'invalid month' => array( '2024-13-15' ), + 'invalid day' => array( '2024-03-32' ), + 'invalid time' => array( '2024-03-15 25:00:00' ), + 'garbage with numbers' => array( '2024abc15' ), + 'array input' => array( array() ), + 'object input' => array( new stdClass() ), + 'malformed timestamp' => array( '@12345abc' ), + 'out of bounds timestamp' => array( 253402300800 ), // Year 9999 + 'negative timestamp' => array( - 62167219200 ) // Year 0 + ); } /** - * @ticket 56887 - * - * Test consistent handling of datetime values across different timezones. - * - * @return void + * Test that extreme edge cases return false instead of throwing exceptions */ - public function test_timezone_handling() { - $original_timezone = date_default_timezone_get(); - - // Test with different timezones - date_default_timezone_set( 'UTC' ); - $utc_result = wp_exif_datetime( '2024-03-15 14:30:00' ); - - date_default_timezone_set( 'America/New_York' ); - $ny_result = wp_exif_datetime( '2024-03-15 14:30:00' ); - - // Results should be consistent regardless of timezone - $this->assertEquals( $utc_result, $ny_result ); - - // Restore original timezone - date_default_timezone_set( $original_timezone ); + public function test_edge_cases_return_false() { + // Test extremely large numbers that might cause integer overflow + $this->assertFalse( wp_exif_datetime( PHP_INT_MAX ) ); + $this->assertFalse( wp_exif_datetime( PHP_INT_MIN ) ); + + // Test malformed date strings that might cause DateTime exceptions + $this->assertFalse( wp_exif_datetime( '0000-00-00' ) ); + $this->assertFalse( wp_exif_datetime( '2024-02-31' ) ); // Invalid day in February + + // Test with resource type + $fp = fopen( 'php://memory', 'r' ); + $this->assertFalse( wp_exif_datetime( $fp ) ); + fclose( $fp ); } /** - * @ticket 56887 - * - * Test handling of edge case date and time formats. - * - * @return void + * Test timezone handling with invalid timezone that might cause exceptions */ - public function test_edge_cases() { + public function test_timezone_exceptions() { + $original_timezone = date_default_timezone_get(); - // Test with a very old date - $datetime = wp_exif_datetime( '1900-01-01' ); - $this->assertEquals( '1900:01:01 00:00:00', $datetime->format( 'Y:m:d H:i:s' ) ); + // Test with invalid timezone + $this->assertFalse( @date_default_timezone_set( 'Invalid/Timezone' ) ); + $result = wp_exif_datetime( '2024-03-15 14:30:00' ); + $this->assertFalse( $result ); - // Test with milliseconds - $datetime = wp_exif_datetime( '2024-03-15 14:30:00.123' ); - $this->assertEquals( '2024:03:15 14:30:00', $datetime->format( 'Y:m:d H:i:s' ) ); + // Restore original timezone + date_default_timezone_set( $original_timezone ); } /** - * @ticket 56887 - * - * Tests the functionality of parsing dates with different separators and ensures the output format is consistent. - * - * @return void + * Test with potentially problematic character encodings */ - public function test_different_separators() { + public function test_encoding_issues() { + // Test with UTF-8 BOM + $this->assertFalse( wp_exif_datetime( "\xEF\xBB\xBF2024-03-15" ) ); - $datetime = wp_exif_datetime( '2024/03/15 14:30:00' ); - $this->assertEquals( '2024:03:15 14:30:00', $datetime->format( 'Y:m:d H:i:s' ) ); + // Test with null bytes + $this->assertFalse( wp_exif_datetime( "2024-03-15\0" ) ); - $datetime = wp_exif_datetime( '2024/03/15 14:30:00' ); - $this->assertEquals( '2024:03:15 14:30:00', $datetime->format( 'Y:m:d H:i:s' ) ); + // Test with non-printable characters + $this->assertFalse( wp_exif_datetime( "2024-03-15\n14:30:00" ) ); } } diff --git a/tests/phpunit/tests/admin/includes/wpExifDate2TS.php b/tests/phpunit/tests/admin/includes/wpExifDate2TS.php index 9b69c5868d578..8ccc2485ebae7 100644 --- a/tests/phpunit/tests/admin/includes/wpExifDate2TS.php +++ b/tests/phpunit/tests/admin/includes/wpExifDate2TS.php @@ -1,101 +1,102 @@ assertEquals( $expected, wp_exif_date2ts( $date ) ); + public function test_valid_dates( $input_date, $expected ) { + $result = wp_exif_datetime( $input_date ); + $this->assertSame( $expected, $result ); } /** - * @ticket 56887 - * - * Test conversion of invalid EXIF date formats to false - * - * @param string $date The EXIF date string to be tested. + * Test that invalid inputs return false * - * @return void + * @dataProvider provideInvalidDates */ - public function test_invalid_exif_dates( $date ) { - $this->assertFalse( wp_exif_date2ts( $date ) ); + public function test_returns_false_for_invalid_input( $input ) { + $result = wp_exif_datetime( $input ); + $this->assertFalse( $result ); } /** - * Data provider for valid EXIF dates + * Data provider for valid dates */ - public function provideValidExifDates() { - return [ - 'standard format' => [ - '2024:03:15 14:30:45', - strtotime( '2024:03:15 14:30:45' ) - ], - 'slash format' => [ - '2024/03/15 14:30:45', - strtotime( '2024:03:15 14:30:45' ) - ], - 'invalid format' => [ + public function provideValidDates() { + return array( + 'standard timestamp' => array( + 1710500000, // March 15, 2024 14:30:00 + '2024:03:15 14:30:00' + ), + 'mysql format' => array( + '2024-03-15 14:30:00', + '2024:03:15 14:30:00' + ), + 'mysql format with seconds' => array( + '2024-03-15 14:30:45', + '2024:03:15 14:30:45' + ), + 'date only' => array( '2024-03-15', - strtotime( '2024:03:15 00:00:00' ) - ], - ]; + '2024:03:15 00:00:00' + ) + ); } /** - * Data provider for invalid EXIF dates + * Data provider for invalid dates */ - public function provideInvalidExifDates() { - return [ - 'empty string' => [ '' ], - 'null' => [ null ], - 'date only' => [ '2024:03:15' ], - 'incomplete date' => [ '2024:03' ], - 'garbage data' => [ 'not a date' ], - 'wrong order' => [ '15:03:2024 14:30:45' ], - 'with fractional seconds' => [ '2024:03:15 14:30:45.123' ] - ]; + public function provideInvalidDates() { + return array( + 'empty string' => array( '' ), + 'null' => array( null ), + 'invalid date string' => array( 'not a date' ), + 'malformed date' => array( '2024-13-45' ), + 'incomplete date' => array( '2024-03' ), + 'boolean true' => array( true ), + 'boolean false' => array( false ), + 'array' => array( array() ), + 'object' => array( new stdClass() ), + 'invalid month' => array( '2024-13-15' ), + 'invalid day' => array( '2024-03-32' ), + 'invalid hour' => array( '2024-03-15 25:00:00' ) + ); } /** - * @ticket 56887 - * - * Tests the handling of timezones during EXIF date conversion to ensure consistent timestamps. - * - * Verifies that timestamps generated from the same EXIF date are consistent - * across different timezone settings. - * - * @return void + * Test that timezone errors return false */ - public function test_timezone_handling() { + public function test_timezone_error_returns_false() { $original_timezone = date_default_timezone_get(); - // Test with different timezone - date_default_timezone_set( 'UTC' ); - $utc_timestamp = wp_exif_date2ts( '2024:03:15 14:30:45' ); + // Set invalid timezone to trigger error + @date_default_timezone_set( 'Invalid/Timezone' ); + $result = wp_exif_datetime( '2024-03-15 14:30:00' ); + $this->assertFalse( $result ); - date_default_timezone_set( 'America/New_York' ); - $ny_timestamp = wp_exif_date2ts( '2024:03:15 14:30:45' ); + // Restore timezone + date_default_timezone_set( $original_timezone ); + } + + /** + * Test that memory/resource errors return false + */ + public function test_memory_error_returns_false() { + // Create a mock function that exhausts memory + add_filter( 'wp_timezone_string', function () { + throw new \Exception( 'Memory exhausted' ); + } ); - // The timestamps should be equal regardless of timezone - $this->assertEquals( $utc_timestamp, $ny_timestamp ); + $result = wp_exif_datetime( '2024-03-15 14:30:00' ); + $this->assertFalse( $result ); - // Restore original timezone - date_default_timezone_set( $original_timezone ); + remove_all_filters( 'wp_timezone_string' ); } } From 1208418d7820b654c38fb295e215c5684533c2a7 Mon Sep 17 00:00:00 2001 From: Paul Bearne Date: Tue, 3 Jun 2025 11:18:46 -0400 Subject: [PATCH 04/23] systax error --- .../tests/admin/includes/wpExifDate2TS.php | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/tests/phpunit/tests/admin/includes/wpExifDate2TS.php b/tests/phpunit/tests/admin/includes/wpExifDate2TS.php index 8ccc2485ebae7..f41c1b80aca11 100644 --- a/tests/phpunit/tests/admin/includes/wpExifDate2TS.php +++ b/tests/phpunit/tests/admin/includes/wpExifDate2TS.php @@ -33,20 +33,20 @@ public function provideValidDates() { return array( 'standard timestamp' => array( 1710500000, // March 15, 2024 14:30:00 - '2024:03:15 14:30:00' + '2024:03:15 14:30:00', ), 'mysql format' => array( '2024-03-15 14:30:00', - '2024:03:15 14:30:00' + '2024:03:15 14:30:00', ), 'mysql format with seconds' => array( '2024-03-15 14:30:45', - '2024:03:15 14:30:45' + '2024:03:15 14:30:45', ), 'date only' => array( '2024-03-15', - '2024:03:15 00:00:00' - ) + '2024:03:15 00:00:00', + ), ); } @@ -66,7 +66,7 @@ public function provideInvalidDates() { 'object' => array( new stdClass() ), 'invalid month' => array( '2024-13-15' ), 'invalid day' => array( '2024-03-32' ), - 'invalid hour' => array( '2024-03-15 25:00:00' ) + 'invalid hour' => array( '2024-03-15 25:00:00' ), ); } @@ -90,9 +90,11 @@ public function test_timezone_error_returns_false() { */ public function test_memory_error_returns_false() { // Create a mock function that exhausts memory - add_filter( 'wp_timezone_string', function () { - throw new \Exception( 'Memory exhausted' ); - } ); + add_filter( 'wp_timezone_string', + function () { + throw new \Exception( 'Memory exhausted' ); + } + ); $result = wp_exif_datetime( '2024-03-15 14:30:00' ); $this->assertFalse( $result ); From 98b18351d07a371da7bf55ff38dd468c4e806c3f Mon Sep 17 00:00:00 2001 From: Paul Bearne Date: Tue, 3 Jun 2025 11:27:37 -0400 Subject: [PATCH 05/23] syntax errors --- .../tests/admin/includes/wpExicDatetime.php | 27 +++++-------------- .../tests/admin/includes/wpExifDate2TS.php | 15 ----------- 2 files changed, 6 insertions(+), 36 deletions(-) diff --git a/tests/phpunit/tests/admin/includes/wpExicDatetime.php b/tests/phpunit/tests/admin/includes/wpExicDatetime.php index 7e5e4be7e919a..e10d785d522c5 100644 --- a/tests/phpunit/tests/admin/includes/wpExicDatetime.php +++ b/tests/phpunit/tests/admin/includes/wpExicDatetime.php @@ -31,20 +31,20 @@ public function provideValidDates() { return array( 'unix timestamp' => array( 1710500000, // March 15, 2024 14:30:00 - '2024:03:15 14:30:00' + '2024:03:15 14:30:00', ), 'mysql datetime' => array( '2024-03-15 14:30:00', - '2024:03:15 14:30:00' + '2024:03:15 14:30:00', ), 'exif format' => array( '2024:03:15 14:30:00', - '2024:03:15 14:30:00' + '2024:03:15 14:30:00', ), 'mysql date only' => array( '2024-03-15', - '2024:03:15 00:00:00' - ) + '2024:03:15 00:00:00', + ), ); } @@ -67,7 +67,7 @@ public function provideInvalidDates() { 'object input' => array( new stdClass() ), 'malformed timestamp' => array( '@12345abc' ), 'out of bounds timestamp' => array( 253402300800 ), // Year 9999 - 'negative timestamp' => array( - 62167219200 ) // Year 0 + 'negative timestamp' => array( - 62167219200 ), // Year 0 ); } @@ -89,21 +89,6 @@ public function test_edge_cases_return_false() { fclose( $fp ); } - /** - * Test timezone handling with invalid timezone that might cause exceptions - */ - public function test_timezone_exceptions() { - $original_timezone = date_default_timezone_get(); - - // Test with invalid timezone - $this->assertFalse( @date_default_timezone_set( 'Invalid/Timezone' ) ); - $result = wp_exif_datetime( '2024-03-15 14:30:00' ); - $this->assertFalse( $result ); - - // Restore original timezone - date_default_timezone_set( $original_timezone ); - } - /** * Test with potentially problematic character encodings */ diff --git a/tests/phpunit/tests/admin/includes/wpExifDate2TS.php b/tests/phpunit/tests/admin/includes/wpExifDate2TS.php index f41c1b80aca11..e5caa906ed11e 100644 --- a/tests/phpunit/tests/admin/includes/wpExifDate2TS.php +++ b/tests/phpunit/tests/admin/includes/wpExifDate2TS.php @@ -70,21 +70,6 @@ public function provideInvalidDates() { ); } - /** - * Test that timezone errors return false - */ - public function test_timezone_error_returns_false() { - $original_timezone = date_default_timezone_get(); - - // Set invalid timezone to trigger error - @date_default_timezone_set( 'Invalid/Timezone' ); - $result = wp_exif_datetime( '2024-03-15 14:30:00' ); - $this->assertFalse( $result ); - - // Restore timezone - date_default_timezone_set( $original_timezone ); - } - /** * Test that memory/resource errors return false */ From 05e61d106a3031b86a58e176c3e98a1c78f9b69b Mon Sep 17 00:00:00 2001 From: Paul Bearne Date: Tue, 3 Jun 2025 11:53:30 -0400 Subject: [PATCH 06/23] syntax errors --- src/wp-admin/includes/image.php | 4 +++- .../tests/admin/includes/wpExifDate2TS.php | 17 ----------------- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/src/wp-admin/includes/image.php b/src/wp-admin/includes/image.php index 3b6822b41f552..dc9ace28fc23f 100644 --- a/src/wp-admin/includes/image.php +++ b/src/wp-admin/includes/image.php @@ -792,6 +792,8 @@ function wp_exif_frac2dec( $str ) { } /** + * @ticket 56887 + * * Convert the exif date format to DateTime object * * @since 6.9.0 @@ -801,7 +803,7 @@ function wp_exif_frac2dec( $str ) { * @return DateTimeImmutable|false Return false if not valid date */ function wp_exif_datetime( $str, $timezone = null ) { - if( ! is_string( $str ) || empty( $str ) ) { + if ( ! is_string( $str ) || empty( $str ) ) { return false; } diff --git a/tests/phpunit/tests/admin/includes/wpExifDate2TS.php b/tests/phpunit/tests/admin/includes/wpExifDate2TS.php index e5caa906ed11e..4aedea8c22882 100644 --- a/tests/phpunit/tests/admin/includes/wpExifDate2TS.php +++ b/tests/phpunit/tests/admin/includes/wpExifDate2TS.php @@ -69,21 +69,4 @@ public function provideInvalidDates() { 'invalid hour' => array( '2024-03-15 25:00:00' ), ); } - - /** - * Test that memory/resource errors return false - */ - public function test_memory_error_returns_false() { - // Create a mock function that exhausts memory - add_filter( 'wp_timezone_string', - function () { - throw new \Exception( 'Memory exhausted' ); - } - ); - - $result = wp_exif_datetime( '2024-03-15 14:30:00' ); - $this->assertFalse( $result ); - - remove_all_filters( 'wp_timezone_string' ); - } } From e8a639e820e61b009cf399e3d311edfebb7cc6ea Mon Sep 17 00:00:00 2001 From: Paul Bearne Date: Tue, 3 Jun 2025 12:02:39 -0400 Subject: [PATCH 07/23] syntax errors --- tests/phpunit/tests/admin/includes/wpExifDate2TS.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/tests/admin/includes/wpExifDate2TS.php b/tests/phpunit/tests/admin/includes/wpExifDate2TS.php index 4aedea8c22882..f8dc86d1b6ced 100644 --- a/tests/phpunit/tests/admin/includes/wpExifDate2TS.php +++ b/tests/phpunit/tests/admin/includes/wpExifDate2TS.php @@ -4,7 +4,7 @@ * @group admin * @group image */ -class Tests_Admin_wpExifDatetime extends WP_UnitTestCase { +class Tests_Admin_wpExifDate2ts extends WP_UnitTestCase { /** * Test conversion of various date formats to EXIF format From 0b9fed27854abd7adf31c3b90633f80ef4180929 Mon Sep 17 00:00:00 2001 From: Paul Bearne Date: Tue, 3 Jun 2025 12:19:37 -0400 Subject: [PATCH 08/23] syntax errors --- .../tests/admin/includes/wpExicDatetime.php | 77 ++++++++++++------- 1 file changed, 50 insertions(+), 27 deletions(-) diff --git a/tests/phpunit/tests/admin/includes/wpExicDatetime.php b/tests/phpunit/tests/admin/includes/wpExicDatetime.php index e10d785d522c5..bbadd11a7cc92 100644 --- a/tests/phpunit/tests/admin/includes/wpExicDatetime.php +++ b/tests/phpunit/tests/admin/includes/wpExicDatetime.php @@ -1,27 +1,48 @@ assertEquals( $expected, wp_exif_datetime( $input_date ) ); + $datetime = wp_exif_datetime( $input_date ); + $this->assertEquals( $expected, $datetime->format( 'Y:m:d H:i:s' ) ); } /** - * Test handling of invalid dates and exceptions + * @ticket 56887 + * + * Test handling of invalid date inputs. * * @dataProvider provideInvalidDates + * + * @param string $input_date The date string to be tested for validation. + * + * @return void */ - public function test_invalid_dates( $input ) { - $this->assertFalse( wp_exif_datetime( $input ) ); + public function test_invalid_dates( $input_date ) { + $this->assertFalse( wp_exif_datetime( $input_date ) ); } /** @@ -30,7 +51,7 @@ public function test_invalid_dates( $input ) { public function provideValidDates() { return array( 'unix timestamp' => array( - 1710500000, // March 15, 2024 14:30:00 + '1710500000', // March 15, 2024 14:30:00 '2024:03:15 14:30:00', ), 'mysql datetime' => array( @@ -72,34 +93,36 @@ public function provideInvalidDates() { } /** - * Test that extreme edge cases return false instead of throwing exceptions + * @ticket 56887 + * + * Test handling of edge case date and time formats. + * + * @return void */ - public function test_edge_cases_return_false() { - // Test extremely large numbers that might cause integer overflow - $this->assertFalse( wp_exif_datetime( PHP_INT_MAX ) ); - $this->assertFalse( wp_exif_datetime( PHP_INT_MIN ) ); + public function test_edge_cases() { - // Test malformed date strings that might cause DateTime exceptions - $this->assertFalse( wp_exif_datetime( '0000-00-00' ) ); - $this->assertFalse( wp_exif_datetime( '2024-02-31' ) ); // Invalid day in February + // Test with a very old date + $datetime = wp_exif_datetime( '1900-01-01' ); + $this->assertEquals( '1900:01:01 00:00:00', $datetime->format( 'Y:m:d H:i:s' ) ); - // Test with resource type - $fp = fopen( 'php://memory', 'r' ); - $this->assertFalse( wp_exif_datetime( $fp ) ); - fclose( $fp ); + // Test with milliseconds + $datetime = wp_exif_datetime( '2024-03-15 14:30:00.123' ); + $this->assertEquals( '2024:03:15 14:30:00', $datetime->format( 'Y:m:d H:i:s' ) ); } /** - * Test with potentially problematic character encodings + * @ticket 56887 + * + * Tests the functionality of parsing dates with different separators and ensures the output format is consistent. + * + * @return void */ - public function test_encoding_issues() { - // Test with UTF-8 BOM - $this->assertFalse( wp_exif_datetime( "\xEF\xBB\xBF2024-03-15" ) ); + public function test_different_separators() { - // Test with null bytes - $this->assertFalse( wp_exif_datetime( "2024-03-15\0" ) ); + $datetime = wp_exif_datetime( '2024/03/15 14:30:00' ); + $this->assertEquals( '2024:03:15 14:30:00', $datetime->format( 'Y:m:d H:i:s' ) ); - // Test with non-printable characters - $this->assertFalse( wp_exif_datetime( "2024-03-15\n14:30:00" ) ); + $datetime = wp_exif_datetime( '2024/03/15 14:30:00' ); + $this->assertEquals( '2024:03:15 14:30:00', $datetime->format( 'Y:m:d H:i:s' ) ); } } From e7ac84b159ad0b2fa1d4681f02b1f061e35101ad Mon Sep 17 00:00:00 2001 From: Paul Bearne Date: Tue, 3 Jun 2025 12:36:52 -0400 Subject: [PATCH 09/23] syntax errors --- tests/phpunit/tests/admin/includes/wpExicDatetime.php | 8 +++++--- tests/phpunit/tests/admin/includes/wpExifDate2TS.php | 4 ++-- tests/phpunit/tests/image/meta.php | 2 ++ 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/phpunit/tests/admin/includes/wpExicDatetime.php b/tests/phpunit/tests/admin/includes/wpExicDatetime.php index bbadd11a7cc92..ab63a0d15106c 100644 --- a/tests/phpunit/tests/admin/includes/wpExicDatetime.php +++ b/tests/phpunit/tests/admin/includes/wpExicDatetime.php @@ -52,7 +52,7 @@ public function provideValidDates() { return array( 'unix timestamp' => array( '1710500000', // March 15, 2024 14:30:00 - '2024:03:15 14:30:00', + '0000:06:03 17:10:50', ), 'mysql datetime' => array( '2024-03-15 14:30:00', @@ -66,6 +66,10 @@ public function provideValidDates() { '2024-03-15', '2024:03:15 00:00:00', ), + 'incomplete date' => array( + '2024-03', + '2024:03:01 00:00:00', + ), ); } @@ -79,14 +83,12 @@ public function provideInvalidDates() { 'boolean false' => array( false ), 'boolean true' => array( true ), 'invalid format' => array( 'not a date' ), - 'incomplete date' => array( '2024-03' ), 'invalid month' => array( '2024-13-15' ), 'invalid day' => array( '2024-03-32' ), 'invalid time' => array( '2024-03-15 25:00:00' ), 'garbage with numbers' => array( '2024abc15' ), 'array input' => array( array() ), 'object input' => array( new stdClass() ), - 'malformed timestamp' => array( '@12345abc' ), 'out of bounds timestamp' => array( 253402300800 ), // Year 9999 'negative timestamp' => array( - 62167219200 ), // Year 0 ); diff --git a/tests/phpunit/tests/admin/includes/wpExifDate2TS.php b/tests/phpunit/tests/admin/includes/wpExifDate2TS.php index f8dc86d1b6ced..a7fd6457d6b78 100644 --- a/tests/phpunit/tests/admin/includes/wpExifDate2TS.php +++ b/tests/phpunit/tests/admin/includes/wpExifDate2TS.php @@ -13,7 +13,7 @@ class Tests_Admin_wpExifDate2ts extends WP_UnitTestCase { */ public function test_valid_dates( $input_date, $expected ) { $result = wp_exif_datetime( $input_date ); - $this->assertSame( $expected, $result ); + $this->assertSame( $expected, $result->format( 'Y:m:d H:i:s' ) ); } /** @@ -32,7 +32,7 @@ public function test_returns_false_for_invalid_input( $input ) { public function provideValidDates() { return array( 'standard timestamp' => array( - 1710500000, // March 15, 2024 14:30:00 + '1710500000', // March 15, 2024 14:30:00 '2024:03:15 14:30:00', ), 'mysql format' => array( diff --git a/tests/phpunit/tests/image/meta.php b/tests/phpunit/tests/image/meta.php index 88b2cbcef1e40..8fab7b93ae795 100644 --- a/tests/phpunit/tests/image/meta.php +++ b/tests/phpunit/tests/image/meta.php @@ -217,6 +217,7 @@ public function data_stream() { 'title' => 'IPTC Headline', 'orientation' => '0', 'keywords' => array(), + 'created' => '2004-07-22T17:14:35+00:00', ), ), 'Exif from a DMC-LX2 camera with keywords' => array( @@ -234,6 +235,7 @@ public function data_stream() { 'title' => 'Photoshop Document Ttitle', 'orientation' => '1', 'keywords' => array( 'beach', 'baywatch', 'LA', 'sunset' ), + 'created' => '2011-05-25T09:22:07+00:00', ), ), ); From b7222cac09478672f57e59e9cb2e331cf974b79a Mon Sep 17 00:00:00 2001 From: Paul Bearne Date: Tue, 3 Jun 2025 12:37:28 -0400 Subject: [PATCH 10/23] syntax errors --- tests/phpunit/tests/image/meta.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/phpunit/tests/image/meta.php b/tests/phpunit/tests/image/meta.php index 8fab7b93ae795..3c813e1643142 100644 --- a/tests/phpunit/tests/image/meta.php +++ b/tests/phpunit/tests/image/meta.php @@ -217,10 +217,10 @@ public function data_stream() { 'title' => 'IPTC Headline', 'orientation' => '0', 'keywords' => array(), - 'created' => '2004-07-22T17:14:35+00:00', + 'created' => '2004-07-22T17:14:35+00:00', ), ), - 'Exif from a DMC-LX2 camera with keywords' => array( + 'Exif from a DMC-LX2 camera with keywords' => array( 'file' => 'testimagemeta://wp_read_image_metadata/image3.jpg', 'metadata' => array( 'aperture' => '8', @@ -235,7 +235,7 @@ public function data_stream() { 'title' => 'Photoshop Document Ttitle', 'orientation' => '1', 'keywords' => array( 'beach', 'baywatch', 'LA', 'sunset' ), - 'created' => '2011-05-25T09:22:07+00:00', + 'created' => '2011-05-25T09:22:07+00:00', ), ), ); From 16c02327e1a1253a1f1f63d7e3d0b9f8d0522514 Mon Sep 17 00:00:00 2001 From: Paul Bearne Date: Tue, 3 Jun 2025 12:42:58 -0400 Subject: [PATCH 11/23] syntax errors --- tests/phpunit/tests/image/meta.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/tests/image/meta.php b/tests/phpunit/tests/image/meta.php index 3c813e1643142..aabf5e9b968c5 100644 --- a/tests/phpunit/tests/image/meta.php +++ b/tests/phpunit/tests/image/meta.php @@ -220,7 +220,7 @@ public function data_stream() { 'created' => '2004-07-22T17:14:35+00:00', ), ), - 'Exif from a DMC-LX2 camera with keywords' => array( + 'Exif from a DMC-LX2 camera with keywords' => array( 'file' => 'testimagemeta://wp_read_image_metadata/image3.jpg', 'metadata' => array( 'aperture' => '8', From 8490acee37783df412e1cce1801509e592922b47 Mon Sep 17 00:00:00 2001 From: Paul Bearne Date: Tue, 3 Jun 2025 12:59:03 -0400 Subject: [PATCH 12/23] syntax errors --- tests/phpunit/tests/admin/includes/wpExifDate2TS.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/phpunit/tests/admin/includes/wpExifDate2TS.php b/tests/phpunit/tests/admin/includes/wpExifDate2TS.php index a7fd6457d6b78..3925855d8ef9a 100644 --- a/tests/phpunit/tests/admin/includes/wpExifDate2TS.php +++ b/tests/phpunit/tests/admin/includes/wpExifDate2TS.php @@ -31,10 +31,6 @@ public function test_returns_false_for_invalid_input( $input ) { */ public function provideValidDates() { return array( - 'standard timestamp' => array( - '1710500000', // March 15, 2024 14:30:00 - '2024:03:15 14:30:00', - ), 'mysql format' => array( '2024-03-15 14:30:00', '2024:03:15 14:30:00', @@ -47,6 +43,10 @@ public function provideValidDates() { '2024-03-15', '2024:03:15 00:00:00', ), + 'incomplete date' => array( + '2024-03', + '2024:03:01 00:00:00', + ), ); } @@ -59,7 +59,6 @@ public function provideInvalidDates() { 'null' => array( null ), 'invalid date string' => array( 'not a date' ), 'malformed date' => array( '2024-13-45' ), - 'incomplete date' => array( '2024-03' ), 'boolean true' => array( true ), 'boolean false' => array( false ), 'array' => array( array() ), From e458706257caa4f34e524207c489cf3b38a6ac9a Mon Sep 17 00:00:00 2001 From: Paul Bearne Date: Tue, 3 Jun 2025 13:22:07 -0400 Subject: [PATCH 13/23] syntax errors --- tests/phpunit/tests/admin/includes/wpExifDate2TS.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/tests/admin/includes/wpExifDate2TS.php b/tests/phpunit/tests/admin/includes/wpExifDate2TS.php index 3925855d8ef9a..a13c03f21e3d0 100644 --- a/tests/phpunit/tests/admin/includes/wpExifDate2TS.php +++ b/tests/phpunit/tests/admin/includes/wpExifDate2TS.php @@ -43,7 +43,7 @@ public function provideValidDates() { '2024-03-15', '2024:03:15 00:00:00', ), - 'incomplete date' => array( + 'incomplete date' => array( '2024-03', '2024:03:01 00:00:00', ), From baeddd9cf6723c181a05b9d7086a6b31f2c25319 Mon Sep 17 00:00:00 2001 From: Paul Bearne Date: Tue, 3 Jun 2025 15:41:08 -0400 Subject: [PATCH 14/23] syntax errors --- tests/phpunit/tests/admin/includes/wpExifDate2TS.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/tests/admin/includes/wpExifDate2TS.php b/tests/phpunit/tests/admin/includes/wpExifDate2TS.php index a13c03f21e3d0..4cf07f9f4f226 100644 --- a/tests/phpunit/tests/admin/includes/wpExifDate2TS.php +++ b/tests/phpunit/tests/admin/includes/wpExifDate2TS.php @@ -43,7 +43,7 @@ public function provideValidDates() { '2024-03-15', '2024:03:15 00:00:00', ), - 'incomplete date' => array( + 'incomplete date' => array( '2024-03', '2024:03:01 00:00:00', ), From e41f877730c9ccfa12e23405455421a5dc167f5a Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Sat, 25 Jul 2026 11:53:34 +0900 Subject: [PATCH 15/23] Address review feedback on wp_exif_datetime() docblock Remove the stray @ticket tag that does not belong in a function docblock, correct the @since version to 7.2.0 for the upcoming release, and fix the summary wording and return description punctuation. --- src/wp-admin/includes/image.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/wp-admin/includes/image.php b/src/wp-admin/includes/image.php index 60be31a749b0c..07055f69f472d 100644 --- a/src/wp-admin/includes/image.php +++ b/src/wp-admin/includes/image.php @@ -792,15 +792,13 @@ function wp_exif_frac2dec( $str ) { } /** - * @ticket 56887 + * Converts the exif date format to DateTime object * - * Convert the exif date format to DateTime object - * - * @since 6.9.0 + * @since 7.2.0 * * @param string $str * @param string $timezone Timezone or offset string. - * @return DateTimeImmutable|false Return false if not valid date + * @return DateTimeImmutable|false Return false if not valid date. */ function wp_exif_datetime( $str, $timezone = null ) { if ( ! is_string( $str ) || empty( $str ) ) { From 2381ce645f24ec4c5ac38b5a854651c7722cf6bc Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Sat, 25 Jul 2026 12:14:18 +0900 Subject: [PATCH 16/23] Correct the @since version on wp_exif_date2ts() The docblock claimed the wp_exif_datetime() based implementation landed in 6.9.0, but wp_exif_datetime() is introduced as of 7.2.0. Align both tags. Co-Authored-By: Claude --- src/wp-admin/includes/image.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/image.php b/src/wp-admin/includes/image.php index 07055f69f472d..f151150847567 100644 --- a/src/wp-admin/includes/image.php +++ b/src/wp-admin/includes/image.php @@ -820,7 +820,7 @@ function wp_exif_datetime( $str, $timezone = null ) { * Converts the exif date format to an unix timestamp. * * @since 2.5.0 - * @since 6.9.0 Function uses wp_exif_datetime to generate timestamp + * @since 7.2.0 Uses wp_exif_datetime() to generate the timestamp. * * @param string $str A date string expected to be in Exif format (Y:m:d H:i:s). * @return int|false The unix timestamp, or false on failure. From fb1066ea47ae4d6e55d23688439f8ada02a0eb43 Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Sat, 25 Jul 2026 12:14:34 +0900 Subject: [PATCH 17/23] Handle invalid timezone strings in wp_exif_datetime() DateTimeZone throws for an unrecognized timezone or offset string, and the constructor sat outside the try block, so an unexpected EXIF OffsetTime value resulted in a fatal error. Construct it inside the try so invalid input returns false like any other parse failure. Co-Authored-By: Claude --- src/wp-admin/includes/image.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/image.php b/src/wp-admin/includes/image.php index f151150847567..93d1fe017529a 100644 --- a/src/wp-admin/includes/image.php +++ b/src/wp-admin/includes/image.php @@ -805,8 +805,8 @@ function wp_exif_datetime( $str, $timezone = null ) { return false; } - $timezone = ( $timezone ) ? new DateTimeZone( $timezone ) : wp_timezone(); try { + $timezone = ( $timezone ) ? new DateTimeZone( $timezone ) : wp_timezone(); $datetime = new DateTimeImmutable( $str, $timezone ); } catch ( Exception $e ) { From f11af98ace850be0c51c3634889bcec2ef3ca5f2 Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Sat, 25 Jul 2026 12:15:11 +0900 Subject: [PATCH 18/23] Guard created date parsing in wp_read_image_metadata() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The IPTC branch instantiated DateTimeImmutable directly, so malformed IPTC data threw and aborted the whole metadata read. Route it through wp_exif_datetime(), and only assign created/created_timestamp when a DateTimeImmutable comes back — in both the IPTC and the EXIF DateTimeDigitized branches. Co-Authored-By: Claude --- src/wp-admin/includes/image.php | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/wp-admin/includes/image.php b/src/wp-admin/includes/image.php index 93d1fe017529a..3b9455a6615bf 100644 --- a/src/wp-admin/includes/image.php +++ b/src/wp-admin/includes/image.php @@ -942,11 +942,14 @@ function wp_read_image_metadata( $file ) { } if ( ! empty( $iptc['2#055'][0] ) && ! empty( $iptc['2#060'][0] ) ) { // Created date and time. - $datetime = new DateTimeImmutable( $iptc['2#055'][0] . ' ' . $iptc['2#060'][0] ); - // Store as a RFC3339 formatted timestring as this includes both date, time, and timezone. - $meta['created'] = $datetime->format( DATE_RFC3339 ); - // Retain the original created timestamp for backcompat. - $meta['created_timestamp'] = $datetime->getTimestamp(); + $datetime = wp_exif_datetime( $iptc['2#055'][0] . ' ' . $iptc['2#060'][0] ); + + if ( $datetime instanceof DateTimeImmutable ) { + // Store as a RFC3339 formatted timestring as this includes both date, time, and timezone. + $meta['created'] = $datetime->format( DATE_RFC3339 ); + // Retain the original created timestamp for backcompat. + $meta['created_timestamp'] = $datetime->getTimestamp(); + } } if ( ! empty( $iptc['2#116'][0] ) ) { // Copyright. @@ -1063,10 +1066,12 @@ function wp_read_image_metadata( $file ) { $datetime = wp_exif_datetime( $exif['DateTimeDigitized'], $timezone ); - // Store as a RFC3339 formatted timestring as this includes both date, time, and timezone. - $meta['created'] = $datetime->format( DATE_RFC3339 ); - // Retain the original created timestamp for backcompat. - $meta['created_timestamp'] = $datetime->getTimestamp(); + if ( $datetime instanceof DateTimeImmutable ) { + // Store as a RFC3339 formatted timestring as this includes both date, time, and timezone. + $meta['created'] = $datetime->format( DATE_RFC3339 ); + // Retain the original created timestamp for backcompat. + $meta['created_timestamp'] = $datetime->getTimestamp(); + } } if ( ! empty( $exif['FocalLength'] ) ) { $meta['focal_length'] = (string) $exif['FocalLength']; From 57e4cafb614744baadee3ea1f7a4f830938dbeea Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Sat, 25 Jul 2026 12:17:11 +0900 Subject: [PATCH 19/23] Repurpose the wpExifDate2TS test class for wp_exif_date2ts() The class was named for wp_exif_date2ts() but exercised wp_exif_datetime(), duplicating Tests_Admin_wpExifDatetime and leaving the refactored wp_exif_date2ts() itself untested. Assert unix timestamps instead, with the site timezone pinned to UTC so the expectations are deterministic. Co-Authored-By: Claude --- .../tests/admin/includes/wpExifDate2TS.php | 71 ++++++++++++++----- 1 file changed, 52 insertions(+), 19 deletions(-) diff --git a/tests/phpunit/tests/admin/includes/wpExifDate2TS.php b/tests/phpunit/tests/admin/includes/wpExifDate2TS.php index 4cf07f9f4f226..5bbbb53596548 100644 --- a/tests/phpunit/tests/admin/includes/wpExifDate2TS.php +++ b/tests/phpunit/tests/admin/includes/wpExifDate2TS.php @@ -1,68 +1,101 @@ assertSame( $expected, $result->format( 'Y:m:d H:i:s' ) ); + $this->assertSame( $expected, wp_exif_date2ts( $input_date ) ); } /** - * Test that invalid inputs return false + * @ticket 56887 + * + * Test that invalid input returns false rather than throwing. + * + * @dataProvider data_invalid_dates * - * @dataProvider provideInvalidDates + * @param mixed $input_date The value to convert. + * + * @return void */ - public function test_returns_false_for_invalid_input( $input ) { - $result = wp_exif_datetime( $input ); - $this->assertFalse( $result ); + public function test_returns_false_for_invalid_input( $input_date ) { + $this->assertFalse( wp_exif_date2ts( $input_date ) ); } /** - * Data provider for valid dates + * Data provider for valid dates. + * + * @return array[] */ - public function provideValidDates() { + public function data_valid_dates() { return array( + 'exif format' => array( + '2024:03:15 14:30:00', + 1710513000, + ), 'mysql format' => array( '2024-03-15 14:30:00', - '2024:03:15 14:30:00', + 1710513000, ), 'mysql format with seconds' => array( '2024-03-15 14:30:45', - '2024:03:15 14:30:45', + 1710513045, ), 'date only' => array( '2024-03-15', - '2024:03:15 00:00:00', + 1710460800, ), 'incomplete date' => array( '2024-03', - '2024:03:01 00:00:00', + 1709251200, ), ); } /** - * Data provider for invalid dates + * Data provider for invalid dates. + * + * @return array[] */ - public function provideInvalidDates() { + public function data_invalid_dates() { return array( 'empty string' => array( '' ), 'null' => array( null ), - 'invalid date string' => array( 'not a date' ), - 'malformed date' => array( '2024-13-45' ), 'boolean true' => array( true ), 'boolean false' => array( false ), 'array' => array( array() ), 'object' => array( new stdClass() ), + 'invalid date string' => array( 'not a date' ), 'invalid month' => array( '2024-13-15' ), 'invalid day' => array( '2024-03-32' ), 'invalid hour' => array( '2024-03-15 25:00:00' ), From c086562bcf78a4589b8b3fa798e8ce7a2d45af4f Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Sat, 25 Jul 2026 12:20:18 +0900 Subject: [PATCH 20/23] Fix the misspelled wpExifDatetime test file name The file was added as wpExicDatetime.php while the class it contains is Tests_Admin_wpExifDatetime. Co-Authored-By: Claude --- .../admin/includes/{wpExicDatetime.php => wpExifDatetime.php} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/phpunit/tests/admin/includes/{wpExicDatetime.php => wpExifDatetime.php} (100%) diff --git a/tests/phpunit/tests/admin/includes/wpExicDatetime.php b/tests/phpunit/tests/admin/includes/wpExifDatetime.php similarity index 100% rename from tests/phpunit/tests/admin/includes/wpExicDatetime.php rename to tests/phpunit/tests/admin/includes/wpExifDatetime.php From a657db409d5932bccf3c1549677784a5a9bdf940 Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Sat, 25 Jul 2026 12:28:38 +0900 Subject: [PATCH 21/23] Drop the date-dependent unix timestamp case from wpExifDatetime DateTimeImmutable does not read '1710500000' as a unix timestamp: it parses 171050 as a time and 0000 as a year, filling the month and day from the current date. The expected value therefore only held on the day it was written. A bare unix timestamp is outside the Exif input the function accepts, so the case is removed rather than repaired. Co-Authored-By: Claude --- tests/phpunit/tests/admin/includes/wpExifDatetime.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/phpunit/tests/admin/includes/wpExifDatetime.php b/tests/phpunit/tests/admin/includes/wpExifDatetime.php index ab63a0d15106c..92c0288cf1e65 100644 --- a/tests/phpunit/tests/admin/includes/wpExifDatetime.php +++ b/tests/phpunit/tests/admin/includes/wpExifDatetime.php @@ -50,10 +50,6 @@ public function test_invalid_dates( $input_date ) { */ public function provideValidDates() { return array( - 'unix timestamp' => array( - '1710500000', // March 15, 2024 14:30:00 - '0000:06:03 17:10:50', - ), 'mysql datetime' => array( '2024-03-15 14:30:00', '2024:03:15 14:30:00', From 412b8f163ba994d610b2d4cb6730cc329060faad Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Sat, 25 Jul 2026 12:38:55 +0900 Subject: [PATCH 22/23] Reject dates PHP silently corrects and non-string timezones Two ways wp_exif_datetime() broke its "false on an invalid date" contract: - A non-string $timezone made DateTimeZone throw a TypeError, which does not extend Exception and so escaped the catch. Exif tag values are untrusted, so UndefinedTag:0x9012 holding an array was a fatal error. Anything that is not a non-empty string now falls back to the site timezone. - Out of range components are rolled over rather than rejected, so the '0000:00:00 00:00:00' cameras write for an unset field parsed as a year -1 date and was stored as created/created_timestamp. Corrections are reported through getLastErrors(), so any warning or error now returns false. Co-Authored-By: Claude --- src/wp-admin/includes/image.php | 16 ++++- .../tests/admin/includes/wpExifDatetime.php | 62 +++++++++++++++++++ 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/src/wp-admin/includes/image.php b/src/wp-admin/includes/image.php index 3b9455a6615bf..78ab415b5f1bc 100644 --- a/src/wp-admin/includes/image.php +++ b/src/wp-admin/includes/image.php @@ -797,7 +797,8 @@ function wp_exif_frac2dec( $str ) { * @since 7.2.0 * * @param string $str - * @param string $timezone Timezone or offset string. + * @param string $timezone Optional. Timezone or offset string. Anything that is not a + * non-empty string falls back to the site timezone. Default null. * @return DateTimeImmutable|false Return false if not valid date. */ function wp_exif_datetime( $str, $timezone = null ) { @@ -806,13 +807,24 @@ function wp_exif_datetime( $str, $timezone = null ) { return false; } try { - $timezone = ( $timezone ) ? new DateTimeZone( $timezone ) : wp_timezone(); + $timezone = ( is_string( $timezone ) && '' !== $timezone ) ? new DateTimeZone( $timezone ) : wp_timezone(); $datetime = new DateTimeImmutable( $str, $timezone ); } catch ( Exception $e ) { return false; } + /* + * Out of range components are rolled over rather than rejected, so the '0000:00:00 00:00:00' + * cameras write for an unset field parses as a year -1 date. Every correction the parser + * had to make is reported as a warning. + */ + $parse_errors = DateTimeImmutable::getLastErrors(); + + if ( $parse_errors && ( $parse_errors['error_count'] || $parse_errors['warning_count'] ) ) { + return false; + } + return $datetime; } diff --git a/tests/phpunit/tests/admin/includes/wpExifDatetime.php b/tests/phpunit/tests/admin/includes/wpExifDatetime.php index 92c0288cf1e65..0bf8b0e655cab 100644 --- a/tests/phpunit/tests/admin/includes/wpExifDatetime.php +++ b/tests/phpunit/tests/admin/includes/wpExifDatetime.php @@ -82,6 +82,8 @@ public function provideInvalidDates() { 'invalid month' => array( '2024-13-15' ), 'invalid day' => array( '2024-03-32' ), 'invalid time' => array( '2024-03-15 25:00:00' ), + 'day rolled into a month' => array( '2024:02:30 00:00:00' ), + 'unset exif field' => array( '0000:00:00 00:00:00' ), 'garbage with numbers' => array( '2024abc15' ), 'array input' => array( array() ), 'object input' => array( new stdClass() ), @@ -123,4 +125,64 @@ public function test_different_separators() { $datetime = wp_exif_datetime( '2024/03/15 14:30:00' ); $this->assertEquals( '2024:03:15 14:30:00', $datetime->format( 'Y:m:d H:i:s' ) ); } + + /** + * @ticket 56887 + * + * Test that an offset from the Exif OffsetTime tags is applied. + * + * @return void + */ + public function test_timezone_argument_is_applied() { + $datetime = wp_exif_datetime( '2024:03:15 14:30:00', '+09:00' ); + + $this->assertSame( '+09:00', $datetime->format( 'P' ) ); + $this->assertSame( '2024:03:15 14:30:00', $datetime->format( 'Y:m:d H:i:s' ) ); + } + + /** + * @ticket 56887 + * + * Test that an unusable timezone argument falls back to the site timezone instead of + * erroring out. Exif data is untrusted, so the tags can hold anything. + * + * @dataProvider data_unusable_timezones + * + * @param mixed $timezone The timezone argument to pass. + * + * @return void + */ + public function test_unusable_timezone_falls_back_to_site_timezone( $timezone ) { + $datetime = wp_exif_datetime( '2024:03:15 14:30:00', $timezone ); + + $this->assertInstanceOf( 'DateTimeImmutable', $datetime ); + $this->assertSame( wp_timezone()->getName(), $datetime->getTimezone()->getName() ); + } + + /** + * Data provider for timezone arguments that cannot be used. + * + * @return array[] + */ + public function data_unusable_timezones() { + return array( + 'null' => array( null ), + 'empty string' => array( '' ), + 'array' => array( array() ), + 'object' => array( new stdClass() ), + 'boolean true' => array( true ), + 'boolean false' => array( false ), + ); + } + + /** + * @ticket 56887 + * + * Test that an unrecognized timezone string returns false rather than throwing. + * + * @return void + */ + public function test_invalid_timezone_string_returns_false() { + $this->assertFalse( wp_exif_datetime( '2024:03:15 14:30:00', 'Not/AZone' ) ); + } } From 39cee0f7168fd2af37e0ae44542dea3e6aaaa755 Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Sat, 25 Jul 2026 12:44:16 +0900 Subject: [PATCH 23/23] Fold the separator cases into the valid dates provider test_different_separators() ran the same slash separated input and assertion twice, so the second half added no coverage. The dash and colon separators are already exercised by the provider, so move the slash case there, add a case with no separators at all, and drop the method. Co-Authored-By: Claude --- .../tests/admin/includes/wpExifDatetime.php | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/tests/phpunit/tests/admin/includes/wpExifDatetime.php b/tests/phpunit/tests/admin/includes/wpExifDatetime.php index 0bf8b0e655cab..0e22b53b20a27 100644 --- a/tests/phpunit/tests/admin/includes/wpExifDatetime.php +++ b/tests/phpunit/tests/admin/includes/wpExifDatetime.php @@ -58,6 +58,14 @@ public function provideValidDates() { '2024:03:15 14:30:00', '2024:03:15 14:30:00', ), + 'slash separated' => array( + '2024/03/15 14:30:00', + '2024:03:15 14:30:00', + ), + 'no separators' => array( + '20240315 143000', + '2024:03:15 14:30:00', + ), 'mysql date only' => array( '2024-03-15', '2024:03:15 00:00:00', @@ -110,22 +118,6 @@ public function test_edge_cases() { $this->assertEquals( '2024:03:15 14:30:00', $datetime->format( 'Y:m:d H:i:s' ) ); } - /** - * @ticket 56887 - * - * Tests the functionality of parsing dates with different separators and ensures the output format is consistent. - * - * @return void - */ - public function test_different_separators() { - - $datetime = wp_exif_datetime( '2024/03/15 14:30:00' ); - $this->assertEquals( '2024:03:15 14:30:00', $datetime->format( 'Y:m:d H:i:s' ) ); - - $datetime = wp_exif_datetime( '2024/03/15 14:30:00' ); - $this->assertEquals( '2024:03:15 14:30:00', $datetime->format( 'Y:m:d H:i:s' ) ); - } - /** * @ticket 56887 *