Skip to content

Commit e26ca28

Browse files
REST API: Fix sideload and finalize for EXIF rotated images.
Fix an issue where client-side media uploads of JPEGs with a quarter-turn EXIF orientation (values 5-8) failed with a 400 `rest_upload_dimension_mismatch` error, because the rotated file's swapped width and height did not match the stored metadata. An `image_size=original` sideload is now handled like a `scaled` one: the rotated file becomes the attachment's main file and the untouched upload is kept as `original_image`, matching what core does when it rotates on upload. Finalize also resets the stored EXIF orientation once rotation has been applied. Follow-up to [61982], [62619]. Props adamsilverstein, andrewserong, ramonjd. Fixes #65643. git-svn-id: https://develop.svn.wordpress.org/trunk@62805 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 9cabe83 commit e26ca28

2 files changed

Lines changed: 364 additions & 29 deletions

File tree

src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php

Lines changed: 57 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2413,14 +2413,23 @@ private function validate_image_dimensions( int $width, int $height, string $ima
24132413
);
24142414
}
24152415

2416-
// 'original' size: should match original attachment dimensions.
2416+
/*
2417+
* 'original' size: the full-size image that replaces the main file (see
2418+
* sideload_item()/finalize_item()). The endpoint expects any EXIF
2419+
* orientation to be applied to the image already, which can swap width
2420+
* and height, so the dimensions must match the stored dimensions or be
2421+
* their transpose.
2422+
*/
24172423
if ( 'original' === $image_size ) {
24182424
$metadata = wp_get_attachment_metadata( $attachment_id, true );
24192425
if ( is_array( $metadata ) && isset( $metadata['width'], $metadata['height'] ) ) {
24202426
$expected_width = (int) $metadata['width'];
24212427
$expected_height = (int) $metadata['height'];
24222428

2423-
if ( $width !== $expected_width || $height !== $expected_height ) {
2429+
$matches_dimensions = $width === $expected_width && $height === $expected_height;
2430+
$transposes_dimensions = $width === $expected_height && $height === $expected_width;
2431+
2432+
if ( ! $matches_dimensions && ! $transposes_dimensions ) {
24242433
return new WP_Error(
24252434
'rest_upload_dimension_mismatch',
24262435
sprintf(
@@ -2665,8 +2674,6 @@ public function sideload_item( WP_REST_Request $request ) {
26652674
$sub_size_data['file'] = wp_basename( $path );
26662675
$sub_size_data['mime_type'] = $type;
26672676
$sub_size_data['filesize'] = wp_filesize( $path );
2668-
} elseif ( 'original' === $image_size ) {
2669-
$sub_size_data['file'] = wp_basename( $path );
26702677
} elseif ( self::IMAGE_SIZE_SOURCE_ORIGINAL === $image_size ) {
26712678
/*
26722679
* Source-format original (e.g. the HEIC kept next to its JPEG
@@ -2681,8 +2688,16 @@ public function sideload_item( WP_REST_Request $request ) {
26812688
* finalize_item can store it under its dedicated meta key.
26822689
*/
26832690
$sub_size_data['file'] = wp_basename( $path );
2684-
} elseif ( 'scaled' === $image_size ) {
2685-
// Record the current attached file as the original.
2691+
} elseif ( 'scaled' === $image_size || 'original' === $image_size ) {
2692+
/*
2693+
* 'scaled' and 'original' both replace the attachment's main file
2694+
* with the supplied image and keep the file being replaced as
2695+
* `original_image`, which is the untouched upload. A 'scaled'
2696+
* image is downsized and an 'original' image has any EXIF
2697+
* orientation already applied. This is the same swap WordPress
2698+
* makes when it scales or rotates an image on upload; see
2699+
* _wp_image_meta_replace_original().
2700+
*/
26862701
$current_file = get_attached_file( $attachment_id, true );
26872702

26882703
if ( ! $current_file ) {
@@ -2695,19 +2710,19 @@ public function sideload_item( WP_REST_Request $request ) {
26952710

26962711
$sub_size_data['original_image'] = wp_basename( $current_file );
26972712

2698-
// Validate the scaled image before updating the attached file.
2713+
// Validate the supplied image before updating the attached file.
26992714
$size = wp_getimagesize( $path );
27002715
$filesize = wp_filesize( $path );
27012716

27022717
if ( ! $size || ! $filesize ) {
27032718
return new WP_Error(
27042719
'rest_sideload_invalid_image',
2705-
__( 'Unable to read the scaled image file.' ),
2720+
__( 'Unable to read the sideloaded image file.' ),
27062721
array( 'status' => 500 )
27072722
);
27082723
}
27092724

2710-
// Update the attached file to point to the scaled version.
2725+
// Update the attached file to point to the supplied image.
27112726
// This writes to _wp_attached_file meta, not _wp_attachment_metadata.
27122727
if (
27132728
get_attached_file( $attachment_id, true ) !== $path &&
@@ -2834,8 +2849,39 @@ public function finalize_item( WP_REST_Request $request ) {
28342849
continue;
28352850
}
28362851

2837-
if ( 'original' === $image_size ) {
2838-
$metadata['original_image'] = $sub_size['file'];
2852+
if ( 'original' === $image_size || 'scaled' === $image_size ) {
2853+
// Skip malformed entries so a bad payload cannot blank out the
2854+
// main file metadata.
2855+
if ( empty( $sub_size['file'] ) ) {
2856+
continue;
2857+
}
2858+
2859+
/*
2860+
* Record the supplied full-size image (from sideload_item()) as
2861+
* the main file, keeping the current attached file as
2862+
* `original_image`. A 'scaled' image is downsized and an
2863+
* 'original' image is rotated; both have any EXIF orientation
2864+
* already applied by the client.
2865+
*/
2866+
if ( ! empty( $sub_size['original_image'] ) ) {
2867+
$metadata['original_image'] = $sub_size['original_image'];
2868+
}
2869+
$metadata['width'] = $sub_size['width'] ?? 0;
2870+
$metadata['height'] = $sub_size['height'] ?? 0;
2871+
$metadata['filesize'] = $sub_size['filesize'] ?? 0;
2872+
$metadata['file'] = $sub_size['file'];
2873+
2874+
/*
2875+
* The supplied image has its orientation applied already, so
2876+
* reset the stored value (from the upload) to 1, as
2877+
* wp_create_image_subsizes() does for both its scale and rotate
2878+
* paths. Otherwise exif_orientation would still report the
2879+
* pre-rotation value and the client would rotate the image
2880+
* again on a re-fetch.
2881+
*/
2882+
if ( ! empty( $metadata['image_meta']['orientation'] ) ) {
2883+
$metadata['image_meta']['orientation'] = 1;
2884+
}
28392885
} elseif ( self::IMAGE_SIZE_SOURCE_ORIGINAL === $image_size ) {
28402886
/*
28412887
* Source-format original: stored under its own meta key so the
@@ -2855,14 +2901,6 @@ public function finalize_item( WP_REST_Request $request ) {
28552901
} elseif ( 'animated_video_poster' === $image_size ) {
28562902
// Static first-frame poster for the converted video.
28572903
$metadata['animated_video_poster'] = $sub_size['file'];
2858-
} elseif ( 'scaled' === $image_size ) {
2859-
if ( ! empty( $sub_size['original_image'] ) ) {
2860-
$metadata['original_image'] = $sub_size['original_image'];
2861-
}
2862-
$metadata['width'] = $sub_size['width'] ?? 0;
2863-
$metadata['height'] = $sub_size['height'] ?? 0;
2864-
$metadata['filesize'] = $sub_size['filesize'] ?? 0;
2865-
$metadata['file'] = $sub_size['file'] ?? '';
28662904
} else {
28672905
$metadata['sizes'] = $metadata['sizes'] ?? array();
28682906

0 commit comments

Comments
 (0)