diff --git a/src/wp-admin/includes/image.php b/src/wp-admin/includes/image.php index 8471c999c8ec5..8e5fe3ccfe095 100644 --- a/src/wp-admin/includes/image.php +++ b/src/wp-admin/includes/image.php @@ -805,6 +805,25 @@ function wp_exif_frac2dec( $str ) { return $numerator / $denominator; } +/** + * Convert the exif date format to DateTime object. + * + * @since 6.8.0 + * + * @param string $str A date string expected to be in Exif format (Y:m:d H:i:s). + * @param string $timezone Optional. Timezone or offset string. + * @return DateTimeImmutable|false Return Date and time object, otherwise false. + */ +function wp_exif_datetime( $str, $timezone = null ) { + try { + $timezone = ( $timezone ) ? new DateTimeZone( $timezone ) : wp_timezone(); + $datetime = new DateTimeImmutable( $str, $timezone ); + return $datetime; + } catch ( Exception $e ) { + return false; + } +} + /** * Converts the exif date format to a unix timestamp. * @@ -814,10 +833,11 @@ function wp_exif_frac2dec( $str ) { * @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 ); - - return strtotime( "{$y}-{$m}-{$d} {$time}" ); + $datetime = wp_exif_datetime( $str ); + if ( $datetime ) { + return $datetime->getTimestamp(); + } + return false; } /** @@ -921,7 +941,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. @@ -1029,7 +1053,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/date/exifDate.php b/tests/phpunit/tests/date/exifDate.php new file mode 100644 index 0000000000000..aab1b6464ac8d --- /dev/null +++ b/tests/phpunit/tests/date/exifDate.php @@ -0,0 +1,78 @@ +assertFalse( $datetime ); + } else { + $this->assertInstanceOf( 'DateTimeImmutable', $datetime ); + $this->assertSame( $expected, $datetime->format( DATE_RFC3339 ) ); + } + } + + public function data_wp_exif_datetime() { + return array( + 'valid date without timezone' => array( + '2004:07:22 17:14:35', + null, + '2004-07-22T17:14:35+00:00', + ), + 'valid date with timezone' => array( + '2004:07:22 17:14:35', + '+05:30', + '2004-07-22T17:14:35+05:30', + ), + 'invalid date format' => array( + 'not a date', + null, + false, + ), + 'invalid timezone' => array( + '2004:07:22 17:14:35', + 'Invalid/Timezone', + false, + ), + ); + } + + /** + * @ticket 49413 + * + * @dataProvider data_wp_exif_date2ts + * + * @param string $date_string Date string in EXIF format (Y:m:d H:i:s). + * @param int|bool $expected Expected Unix timestamp or false for invalid input. + */ + public function test_wp_exif_date2ts( $date_string, $expected ) { + $timestamp = wp_exif_date2ts( $date_string ); + $this->assertSame( $expected, $timestamp ); + } + + public function data_wp_exif_date2ts() { + return array( + 'valid date' => array( + '2004:07:22 17:14:35', + 1090516475, + ), + 'invalid date format' => array( + 'not a date', + false, + ), + ); + } +} diff --git a/tests/phpunit/tests/image/meta.php b/tests/phpunit/tests/image/meta.php index 88b2cbcef1e40..aabf5e9b968c5 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', ), ), );