Refactor EXIF date handling and add unit tests#8884
Conversation
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.
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the Unlinked AccountsThe following contributors have not linked their GitHub and WordPress.org accounts: @pbearne@git.wordpress.org. Contributors, please read how to link your accounts to ensure your work is properly credited in WordPress releases. Core Committers: Use this line as a base for the props when committing in SVN: To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
Test using WordPress PlaygroundThe changes in this pull request can previewed and tested using a WordPress Playground instance. WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser. Some things to be aware of
For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation. |
audrasjb
left a comment
There was a problem hiding this comment.
some small improvements
There was a problem hiding this comment.
Pull request overview
This PR refactors EXIF/IPTC date handling by introducing wp_exif_datetime() and updating image metadata parsing to store a RFC3339 created datetime string while keeping the legacy created_timestamp for back-compat. It also updates/introduces PHPUnit tests to cover date parsing scenarios.
Changes:
- Added
wp_exif_datetime()and refactoredwp_exif_date2ts()to use it. - Updated
wp_read_image_metadata()to store RFC3339createdin addition tocreated_timestamp. - Added/updated PHPUnit expectations and new unit tests for EXIF datetime parsing.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
src/wp-admin/includes/image.php |
Adds wp_exif_datetime(), refactors timestamp conversion, and stores RFC3339 created metadata. |
tests/phpunit/tests/image/meta.php |
Updates expected metadata arrays to include the new created field for applicable fixtures. |
tests/phpunit/tests/admin/includes/wpExifDate2TS.php |
Adds a new test class intended for EXIF date conversion behavior (currently misaligned with what it tests). |
tests/phpunit/tests/admin/includes/wpExicDatetime.php |
Adds tests for wp_exif_datetime() across valid/invalid/edge inputs (contains some inconsistent vectors). |
Comments suppressed due to low confidence (2)
src/wp-admin/includes/image.php:1068
wp_exif_datetime()can returnfalsefor invalid EXIF values (or invalid timezone strings), but the result is used unconditionally. Calling->format()/->getTimestamp()onfalsewill fatal. Guard the assignments so they only run when aDateTimeImmutableis returned.
$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.
tests/phpunit/tests/admin/includes/wpExicDatetime.php:129
- This test repeats the exact same input and assertion twice, so it doesn’t add coverage and makes failures noisier to diagnose. Remove the duplicate assertion or change the second one to exercise a different separator format.
$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' ) );
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
The file was added as wpExicDatetime.php while the class it contains is Tests_Admin_wpExifDatetime. Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (3)
tests/phpunit/tests/admin/includes/wpExifDatetime.php:56
- The "unix timestamp" test case is internally inconsistent: the input/comment say "March 15, 2024 14:30:00" but the expected value is
0000:06:03 17:10:50. If the intent is to support Unix timestamps, the input should be the matching timestamp and the expected formatted output should reflect that date/time.
'unix timestamp' => array(
'1710500000', // March 15, 2024 14:30:00
'0000:06:03 17:10:50',
),
tests/phpunit/tests/admin/includes/wpExifDatetime.php:31
test_valid_dates()calls$datetime->format()without asserting thatwp_exif_datetime()actually returned a DateTime instance. If the function returnsfalsefor any provider row, this will trigger a fatal error and obscure the real failure.
public function test_valid_dates( $input_date, $expected ) {
$datetime = wp_exif_datetime( $input_date );
$this->assertEquals( $expected, $datetime->format( 'Y:m:d H:i:s' ) );
}
tests/phpunit/tests/admin/includes/wpExifDatetime.php:128
test_different_separators()repeats the exact same input twice, so it doesn’t actually validate multiple 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' ) );
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 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (5)
src/wp-admin/includes/image.php:812
wp_exif_datetime()currently relies onnew DateTimeImmutable( $str, $timezone )and only returnsfalsewhen an exception is thrown. The DateTime parser can normalize out-of-range components (e.g. month/day/time overflow) instead of throwing, so inputs like2024-13-15may be treated as valid and produce incorrect timestamps. Also, defaulting towp_timezone()changes the interpretation vs the historicalstrtotime()behavior (WordPress sets the PHP default timezone to UTC inwp-settings.php:73), which risks breaking back-compat forcreated_timestamp. Consider strict parsing against the accepted formats and defaulting to UTC unless an explicit timezone/offset is provided.
try {
$timezone = ( $timezone ) ? new DateTimeZone( $timezone ) : wp_timezone();
$datetime = new DateTimeImmutable( $str, $timezone );
} catch ( Exception $e ) {
src/wp-admin/includes/image.php:797
- The new
@sincetag is set to7.2.0, but the current development version in this branch is7.1(seesrc/wp-includes/version.php). The@sincevalue should reflect the first core version that will ship this function.
* @since 7.2.0
src/wp-admin/includes/image.php:823
- The
@sincetag here is set to7.2.0, but this change is being introduced while core is on7.1(seesrc/wp-includes/version.php). The@sincetag should match the release that will first include this behavior change.
* @since 7.2.0 Uses wp_exif_datetime() to generate the timestamp.
tests/phpunit/tests/admin/includes/wpExifDatetime.php:15
- These tests call
wp_exif_datetime()without providing a timezone, but the function defaults to the site timezone (wp_timezone()). To avoid order-dependent/flaky tests, pintimezone_stringto a known value and restore it intear_down()(similar to other date/datetime tests).
class Tests_Admin_wpExifDatetime extends WP_UnitTestCase {
tests/phpunit/tests/admin/includes/wpExifDate2TS.php:21
set_up()changes thetimezone_stringoption but never restores it, which can leak state into later tests. Store the original value and restore it intear_down()(as done in other datetime-related tests).
public function set_up() {
parent::set_up();
// Pin the site timezone so the expected timestamps are deterministic.
update_option( 'timezone_string', 'UTC' );
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (1)
tests/phpunit/tests/admin/includes/wpExifDatetime.php:126
test_different_separators()repeats the exact same input/assertion twice, which adds noise without increasing coverage. Remove the duplicate block (or change it to a genuinely different separator if that was the intent).
* @ticket 56887
*
* Test that an offset from the Exif OffsetTime tags is applied.
*
* @return void
| function wp_exif_date2ts( $str ) { | ||
| list( $date, $time ) = explode( ' ', trim( $str ) ); | ||
| list( $y, $m, $d ) = explode( ':', $date ); | ||
| $datetime = wp_exif_datetime( $str ); |
| * | ||
| * @dataProvider provideInvalidDates | ||
| * | ||
| * @param string $input_date The date string to be tested for validation. |
| /** | ||
| * Data provider for invalid dates that should trigger exceptions | ||
| */ |
| 'array input' => array( array() ), | ||
| 'object input' => array( new stdClass() ), | ||
| 'out of bounds timestamp' => array( 253402300800 ), // Year 9999 | ||
| 'negative timestamp' => array( - 62167219200 ), // Year 0 |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (3)
src/wp-admin/includes/image.php:846
- wp_exif_date2ts() previously used strtotime() while WordPress runs with date_default_timezone_set('UTC') (see wp-settings.php:73), so its output was effectively UTC regardless of the site timezone setting. The refactor now defaults to wp_timezone() via wp_exif_datetime(), which changes timestamps on sites that are not set to UTC. If backward compatibility is a goal, pass an explicit 'UTC' timezone here.
function wp_exif_date2ts( $str ) {
$datetime = wp_exif_datetime( $str );
if ( $datetime instanceof DateTimeImmutable ) {
return $datetime->getTimestamp();
}
return false;
tests/phpunit/tests/admin/includes/wpExifDatetime.php:83
- The provideInvalidDates() doc comment says these inputs “should trigger exceptions”, but wp_exif_datetime() is explicitly designed to return false rather than throw. Updating the comment avoids confusion about the intended contract.
/**
* Data provider for invalid dates that should trigger exceptions
*/
public function provideInvalidDates() {
tests/phpunit/tests/admin/includes/wpExifDatetime.php:42
- test_invalid_dates() documents $input_date as a string, but the provider passes null, booleans, arrays, and objects. Updating the
@paramtype makes the test intent clearer.
* Test handling of invalid date inputs.
*
* @dataProvider provideInvalidDates
*
* @param string $input_date The date string to be tested for validation.
*
* @return void
| * Converts the exif date format to DateTime object | ||
| * | ||
| * @since 7.2.0 | ||
| * | ||
| * @param string $str |
| 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 = 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( $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 ); | ||
|
|
||
| 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(); | ||
| } |
| public function test_valid_dates( $input_date, $expected ) { | ||
| $datetime = wp_exif_datetime( $input_date ); | ||
| $this->assertEquals( $expected, $datetime->format( 'Y:m:d H:i:s' ) ); | ||
| } |
Replaced
wp_exif_date2tslogic with the newwp_exif_datetimefunction 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.Fixes 48204 as well
Trac ticket: https://core.trac.wordpress.org/ticket/56887