diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index 6e06f1563c50c..78225e87e23da 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -459,6 +459,10 @@ public function create_item( $request ) { // Disable server-side EXIF rotation so the client can handle it. // This preserves the original orientation value in the metadata. add_filter( 'wp_image_maybe_exif_rotate', '__return_false', 100 ); + // Disable server-side "big image" downscaling; the client supplies its + // own scaled version via the sideload endpoint. Scaling here would + // create a conflicting "-scaled" file and orphan the full-size upload. + add_filter( 'big_image_size_threshold', '__return_false', 100 ); } // Handle convert_format parameter. @@ -691,6 +695,7 @@ private function remove_client_side_media_processing_filters(): void { remove_filter( 'fallback_intermediate_image_sizes', '__return_empty_array', 100 ); remove_filter( 'wp_image_maybe_exif_rotate', '__return_false', 100 ); remove_filter( 'image_editor_output_format', '__return_empty_array', 100 ); + remove_filter( 'big_image_size_threshold', '__return_false', 100 ); } /** diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index 90899df850d47..b79ee71e5ee71 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -3875,6 +3875,71 @@ public function test_sideload_scaled_image() { $this->assertGreaterThan( 0, $metadata['filesize'], 'Filesize should be positive.' ); } + /** + * When the client generates sub-sizes (generate_sub_sizes is false), the + * server must not perform its own "big image" downscaling on upload. + * + * Otherwise the server creates a `-scaled` file and records the upload as + * `original_image`. The client's subsequent scaled sideload then collides + * with that `-scaled` file and is renamed `-scaled-1`, the thumbnails + * inherit the numbered name, and the server-generated full-size file is + * left orphaned on disk. + * + * @ticket 65708 + * @requires function imagejpeg + */ + public function test_create_item_skips_big_image_scaling_when_client_generates_sub_sizes() { + $this->enable_client_side_media_processing(); + + wp_set_current_user( self::$author_id ); + + // Force the threshold below the image's dimensions so scaling would be + // triggered were it not suppressed for client-side processing. + add_filter( + 'big_image_size_threshold', + static function () { + return 1000; + } + ); + + // Upload a large image with the client handling sub-size generation. + $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); + $request->set_header( 'Content-Type', 'image/jpeg' ); + $request->set_header( 'Content-Disposition', 'attachment; filename=33772.jpg' ); + $request->set_param( 'generate_sub_sizes', false ); + $request->set_body( file_get_contents( DIR_TESTDATA . '/images/33772.jpg' ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $attachment_id = $data['id']; + + $this->assertSame( 201, $response->get_status(), 'Uploading the image should succeed.' ); + + // The uploaded full-size image should be stored untouched: no + // server-side "-scaled" file and no original_image swap. + $original_file = get_attached_file( $attachment_id, true ); + $original_basename = wp_basename( $original_file ); + $original_name_stem = pathinfo( $original_basename, PATHINFO_FILENAME ); + $this->assertStringNotContainsString( '-scaled', $original_basename, 'The server should not create a -scaled file when the client generates sub-sizes.' ); + + $metadata = wp_get_attachment_metadata( $attachment_id ); + $this->assertArrayNotHasKey( 'original_image', $metadata, 'The server should not record an original_image when it does not scale the upload.' ); + + // The client's scaled sideload should now record the untouched upload as + // original_image and keep the -scaled name without a numeric suffix. + $request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment_id}/sideload" ); + $request->set_header( 'Content-Type', 'image/jpeg' ); + $request->set_header( 'Content-Disposition', "attachment; filename={$original_name_stem}-scaled.jpg" ); + $request->set_param( 'image_size', 'scaled' ); + $request->set_body( file_get_contents( DIR_TESTDATA . '/images/33772.jpg' ) ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status(), 'Sideloading the scaled image should succeed.' ); + + $sub_size = $response->get_data(); + $this->assertSame( $original_basename, $sub_size['original_image'], 'The untouched upload should be recorded as original_image.' ); + $this->assertSame( "{$original_name_stem}-scaled.jpg", wp_basename( $sub_size['file'] ), 'The scaled sideload should keep the -scaled name without a numeric collision suffix.' ); + } + /** * Tests that sideloading scaled image requires authentication. *