-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Refactor EXIF date handling and add unit tests #8884
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
Changes from all commits
b8550f4
77b032d
5bbc114
1208418
98b1835
05e61d1
e8a639e
0b9fed2
e7ac84b
b7222ca
16c0232
8490ace
e458706
baeddd9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -792,18 +792,48 @@ function wp_exif_frac2dec( $str ) { | |
| } | ||
|
|
||
| /** | ||
| * Converts the exif date format to a unix timestamp. | ||
| * @ticket 56887 | ||
| * | ||
| * Convert the exif date format to DateTime object | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "Converts" |
||
| * | ||
| * @since 6.9.0 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will Need to be updated to the correct version |
||
| * | ||
| * @param string $str | ||
| * @param string $timezone Timezone or offset string. | ||
| * @return DateTimeImmutable|false Return false if not valid date | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. missing period at the end of the comment |
||
| */ | ||
| 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 +937,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 +1049,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']; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| <?php | ||
|
|
||
| /** | ||
| * Class Tests_Admin_wpExifDatetime | ||
| * | ||
| * Contains unit tests for validating the functionality of the wp_exif_datetime function, | ||
| * which is responsible for formatting datetime strings to the EXIF-compliant format. | ||
| * | ||
| * @group datetime | ||
| * @group image | ||
| * | ||
| * @covers ::wp_exif_datetime | ||
| */ | ||
| class Tests_Admin_wpExifDatetime extends WP_UnitTestCase { | ||
|
|
||
| /** | ||
| * @ticket 56887 | ||
| * | ||
| * Test valid date inputs and their expected formatted outputs. | ||
| * | ||
| * @dataProvider provideValidDates | ||
| * | ||
| * @param string $input_date The input date string to be formatted. | ||
| * @param string $expected The expected formatted date string. | ||
| * | ||
| * @return void | ||
| */ | ||
| 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' ) ); | ||
| } | ||
|
|
||
| /** | ||
| * @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_date ) { | ||
| $this->assertFalse( wp_exif_datetime( $input_date ) ); | ||
| } | ||
|
|
||
| /** | ||
| * Data provider for valid dates | ||
| */ | ||
| 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', | ||
| ), | ||
| 'exif format' => array( | ||
| '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', | ||
| ), | ||
| 'incomplete date' => array( | ||
| '2024-03', | ||
| '2024:03:01 00:00:00', | ||
| ), | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Data provider for invalid dates that should trigger exceptions | ||
| */ | ||
| public function provideInvalidDates() { | ||
| return array( | ||
| 'empty string' => array( '' ), | ||
| 'null' => array( null ), | ||
| 'boolean false' => array( false ), | ||
| 'boolean true' => array( true ), | ||
| 'invalid format' => array( 'not a date' ), | ||
| '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() ), | ||
| 'out of bounds timestamp' => array( 253402300800 ), // Year 9999 | ||
| 'negative timestamp' => array( - 62167219200 ), // Year 0 | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * @ticket 56887 | ||
| * | ||
| * Test handling of edge case date and time formats. | ||
| * | ||
| * @return void | ||
| */ | ||
| 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' ) ); | ||
| } | ||
|
|
||
| /** | ||
| * @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' ) ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| <?php | ||
|
|
||
| /** | ||
| * @group admin | ||
| * @group image | ||
| */ | ||
| class Tests_Admin_wpExifDate2ts extends WP_UnitTestCase { | ||
|
|
||
| /** | ||
| * Test conversion of various date formats to EXIF format | ||
| * | ||
| * @dataProvider provideValidDates | ||
| */ | ||
| public function test_valid_dates( $input_date, $expected ) { | ||
| $result = wp_exif_datetime( $input_date ); | ||
| $this->assertSame( $expected, $result->format( 'Y:m:d H:i:s' ) ); | ||
| } | ||
|
|
||
| /** | ||
| * Test that invalid inputs return false | ||
| * | ||
| * @dataProvider provideInvalidDates | ||
| */ | ||
| public function test_returns_false_for_invalid_input( $input ) { | ||
| $result = wp_exif_datetime( $input ); | ||
| $this->assertFalse( $result ); | ||
| } | ||
|
|
||
| /** | ||
| * Data provider for valid dates | ||
| */ | ||
| public function provideValidDates() { | ||
| return array( | ||
| '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', | ||
| '2024:03:15 00:00:00', | ||
| ), | ||
| 'incomplete date' => array( | ||
| '2024-03', | ||
| '2024:03:01 00:00:00', | ||
| ), | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Data provider for invalid dates | ||
| */ | ||
| public function provideInvalidDates() { | ||
| 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 month' => array( '2024-13-15' ), | ||
| 'invalid day' => array( '2024-03-32' ), | ||
| 'invalid hour' => array( '2024-03-15 25:00:00' ), | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We usually don't mention tickets.
Note: this can be removed during commit by the committer.