From 0229c118e04224a273c5f2ebe90c1244a534ad9f Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Mon, 23 Feb 2026 18:08:28 +0700 Subject: [PATCH 01/17] REST API: Add 'scaled' to sideload route image_size enum When client-side media processing handles big image scaling, the client creates a -scaled version and sideloads it back. The sideload route's image_size enum was missing 'scaled', causing 400 validation errors. This adds 'scaled' to the enum, adds handling in sideload_item() to record the original file and update the attachment to point to the scaled version, and updates the unique filename filter regex to recognize the -scaled suffix. --- .../class-wp-rest-attachments-controller.php | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) 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 1a135ba546779..7e29ceff0ad82 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 @@ -70,6 +70,8 @@ public function register_routes() { $valid_image_sizes[] = 'original'; // Used for PDF thumbnails. $valid_image_sizes[] = 'full'; + // Client-side big image threshold: sideload the scaled version. + $valid_image_sizes[] = 'scaled'; register_rest_route( $this->namespace, @@ -2053,6 +2055,20 @@ public function sideload_item( WP_REST_Request $request ) { if ( 'original' === $image_size ) { $metadata['original_image'] = wp_basename( $path ); + } elseif ( 'scaled' === $image_size ) { + // The current attached file is the original; record it as original_image. + $current_file = get_attached_file( $attachment_id, true ); + $metadata['original_image'] = wp_basename( $current_file ); + + // Update the attached file to point to the scaled version. + update_attached_file( $attachment_id, $path ); + + $size = wp_getimagesize( $path ); + + $metadata['width'] = $size ? $size[0] : 0; + $metadata['height'] = $size ? $size[1] : 0; + $metadata['filesize'] = wp_filesize( $path ); + $metadata['file'] = _wp_relative_upload_path( $path ); } else { $metadata['sizes'] = $metadata['sizes'] ?? array(); @@ -2123,7 +2139,7 @@ private static function filter_wp_unique_filename( $filename, $dir, $number, $at } $matches = array(); - if ( preg_match( '/(.*)(-\d+x\d+)-' . $number . '$/', $name, $matches ) ) { + if ( preg_match( '/(.*)(-\d+x\d+|-scaled)-' . $number . '$/', $name, $matches ) ) { $filename_without_suffix = $matches[1] . $matches[2] . ".$ext"; if ( $matches[1] === $orig_name && ! file_exists( "$dir/$filename_without_suffix" ) ) { return $filename_without_suffix; From 0dcab83e202a0e58c87741d303654b30db0a27ac Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Thu, 26 Feb 2026 18:54:43 +0700 Subject: [PATCH 02/17] REST API: Update auto-generated JS fixture for sideload route. Add 'scaled' to the image_size enum in wp-api-generated.js to match the PHP route registration change, fixing the git diff --exit-code CI check. Co-Authored-By: Claude Opus 4.6 --- tests/qunit/fixtures/wp-api-generated.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index e1a8ffc96995f..83fc6ade67a88 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -3703,7 +3703,8 @@ mockedApiResponse.Schema = { "1536x1536", "2048x2048", "original", - "full" + "full", + "scaled" ], "required": true }, From 2fe2162d26dbf1bef6b20ab862d6384ec986b0fe Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Thu, 26 Feb 2026 18:54:56 +0700 Subject: [PATCH 03/17] Tests: Add unit tests for scaled image sideloading via REST API. Add tests for the new 'scaled' image_size enum value in the sideload endpoint: verifying metadata updates, authentication requirements, route schema, and unique filename handling for the -scaled suffix. Co-Authored-By: Claude Opus 4.6 --- .../rest-api/rest-attachments-controller.php | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index 796e58c45d97d..5d2afc48648a4 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -3154,4 +3154,138 @@ static function ( $data ) use ( &$captured_data ) { // Verify that the data is an array (not an object). $this->assertIsArray( $captured_data, 'Data passed to wp_insert_attachment should be an array' ); } + + /** + * Tests sideloading a scaled image for an existing attachment. + * + * @ticket 63 + * @requires function imagejpeg + */ + public function test_sideload_scaled_image() { + wp_set_current_user( self::$author_id ); + + // First, create an attachment. + $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); + $request->set_header( 'Content-Type', 'image/jpeg' ); + $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' ); + $request->set_body( file_get_contents( self::$test_file ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $attachment_id = $data['id']; + + $this->assertSame( 201, $response->get_status() ); + + $original_file = get_attached_file( $attachment_id, true ); + + // Sideload a "scaled" version of the image. + $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=canola-scaled.jpg' ); + $request->set_param( 'image_size', 'scaled' ); + $request->set_body( file_get_contents( self::$test_file ) ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status(), 'Sideloading scaled image should succeed.' ); + + $metadata = wp_get_attachment_metadata( $attachment_id ); + + // The original file should now be recorded as original_image. + $this->assertArrayHasKey( 'original_image', $metadata, 'Metadata should contain original_image.' ); + $this->assertSame( wp_basename( $original_file ), $metadata['original_image'], 'original_image should be the basename of the original attached file.' ); + + // The attached file should now point to the scaled version. + $new_file = get_attached_file( $attachment_id, true ); + $this->assertStringContainsString( 'scaled', wp_basename( $new_file ), 'Attached file should now be the scaled version.' ); + + // Metadata should have width, height, filesize, and file updated. + $this->assertArrayHasKey( 'width', $metadata, 'Metadata should contain width.' ); + $this->assertArrayHasKey( 'height', $metadata, 'Metadata should contain height.' ); + $this->assertArrayHasKey( 'filesize', $metadata, 'Metadata should contain filesize.' ); + $this->assertArrayHasKey( 'file', $metadata, 'Metadata should contain file.' ); + $this->assertGreaterThan( 0, $metadata['width'], 'Width should be positive.' ); + $this->assertGreaterThan( 0, $metadata['height'], 'Height should be positive.' ); + $this->assertGreaterThan( 0, $metadata['filesize'], 'Filesize should be positive.' ); + } + + /** + * Tests that sideloading scaled image requires authentication. + * + * @ticket 63 + * @requires function imagejpeg + */ + public function test_sideload_scaled_image_requires_auth() { + wp_set_current_user( self::$author_id ); + + // Create an attachment. + $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); + $request->set_header( 'Content-Type', 'image/jpeg' ); + $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' ); + $request->set_body( file_get_contents( self::$test_file ) ); + $response = rest_get_server()->dispatch( $request ); + $attachment_id = $response->get_data()['id']; + + // Try sideloading without authentication. + wp_set_current_user( 0 ); + + $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=canola-scaled.jpg' ); + $request->set_param( 'image_size', 'scaled' ); + $request->set_body( file_get_contents( self::$test_file ) ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_forbidden', $response, 401 ); + } + + /** + * Tests that the sideload endpoint includes 'scaled' in the image_size enum. + * + * @ticket 63 + */ + public function test_sideload_route_includes_scaled_enum() { + $server = rest_get_server(); + $routes = $server->get_routes(); + + $this->assertArrayHasKey( '/wp/v2/media/(?P[\d]+)/sideload', $routes, 'Sideload route should exist.' ); + + $route = $routes['/wp/v2/media/(?P[\d]+)/sideload']; + $endpoint = $route[0]; + $args = $endpoint['args']; + + $this->assertArrayHasKey( 'image_size', $args, 'Route should have image_size arg.' ); + $this->assertContains( 'scaled', $args['image_size']['enum'], 'image_size enum should include scaled.' ); + } + + /** + * Tests the filter_wp_unique_filename method handles the -scaled suffix. + * + * @ticket 63 + * @requires function imagejpeg + */ + public function test_sideload_scaled_unique_filename() { + wp_set_current_user( self::$author_id ); + + // Create an attachment. + $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); + $request->set_header( 'Content-Type', 'image/jpeg' ); + $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' ); + $request->set_body( file_get_contents( self::$test_file ) ); + $response = rest_get_server()->dispatch( $request ); + $attachment_id = $response->get_data()['id']; + + // Sideload with the -scaled 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=canola-scaled.jpg' ); + $request->set_param( 'image_size', 'scaled' ); + $request->set_body( file_get_contents( self::$test_file ) ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status(), 'Sideloading scaled image should succeed.' ); + + // The filename should retain the -scaled suffix without numeric disambiguation. + $new_file = get_attached_file( $attachment_id, true ); + $basename = wp_basename( $new_file ); + $this->assertMatchesRegularExpression( '/canola-scaled\.jpg$/', $basename, 'Scaled filename should not have numeric suffix appended.' ); + } } From 133ec0f61a0186f877b2d9c7389a3fa0087dd2b1 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Thu, 26 Feb 2026 19:09:53 +0700 Subject: [PATCH 04/17] REST API: Validate get_attached_file() in sideload get_attached_file() can return false when no file is attached. Add a guard to return a WP_Error before calling wp_basename() with a falsy value. --- .../endpoints/class-wp-rest-attachments-controller.php | 9 +++++++++ 1 file changed, 9 insertions(+) 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 7e29ceff0ad82..978cbfbebf6f9 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 @@ -2058,6 +2058,15 @@ public function sideload_item( WP_REST_Request $request ) { } elseif ( 'scaled' === $image_size ) { // The current attached file is the original; record it as original_image. $current_file = get_attached_file( $attachment_id, true ); + + if ( ! $current_file ) { + return new WP_Error( + 'rest_sideload_no_attached_file', + __( 'Unable to retrieve the attached file for this attachment.' ), + array( 'status' => 400 ) + ); + } + $metadata['original_image'] = wp_basename( $current_file ); // Update the attached file to point to the scaled version. From a868b57c9807742b75d320e2449d7925c4558482 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Thu, 26 Feb 2026 19:26:30 +0700 Subject: [PATCH 05/17] Tests: Fix expected error code in sideload auth test The sideload route uses edit_media_item_permissions_check which returns rest_cannot_edit_image, not rest_forbidden. --- tests/phpunit/tests/rest-api/rest-attachments-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index 5d2afc48648a4..a974cc167862e 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -3234,7 +3234,7 @@ public function test_sideload_scaled_image_requires_auth() { $request->set_body( file_get_contents( self::$test_file ) ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_forbidden', $response, 401 ); + $this->assertErrorResponse( 'rest_cannot_edit_image', $response, 401 ); } /** From e5baa99971b6cc2cfb97978e95dd186ad42a953b Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Mon, 2 Mar 2026 11:22:56 +0700 Subject: [PATCH 06/17] fix ticket number for test annotations --- .../tests/rest-api/rest-attachments-controller.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index a974cc167862e..8c838f6d2d9e9 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -3158,7 +3158,7 @@ static function ( $data ) use ( &$captured_data ) { /** * Tests sideloading a scaled image for an existing attachment. * - * @ticket 63 + * @ticket 64737 * @requires function imagejpeg */ public function test_sideload_scaled_image() { @@ -3210,7 +3210,7 @@ public function test_sideload_scaled_image() { /** * Tests that sideloading scaled image requires authentication. * - * @ticket 63 + * @ticket 64737 * @requires function imagejpeg */ public function test_sideload_scaled_image_requires_auth() { @@ -3240,7 +3240,7 @@ public function test_sideload_scaled_image_requires_auth() { /** * Tests that the sideload endpoint includes 'scaled' in the image_size enum. * - * @ticket 63 + * @ticket 64737 */ public function test_sideload_route_includes_scaled_enum() { $server = rest_get_server(); @@ -3259,7 +3259,7 @@ public function test_sideload_route_includes_scaled_enum() { /** * Tests the filter_wp_unique_filename method handles the -scaled suffix. * - * @ticket 63 + * @ticket 64737 * @requires function imagejpeg */ public function test_sideload_scaled_unique_filename() { From 458d94c2cb212e019b588bcb82a7439e1c5640f3 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 3 Mar 2026 10:34:43 +0700 Subject: [PATCH 07/17] Add value assertion for metadata file key Addresses review feedback to assert the value of metadata['file'], not just its existence. --- tests/phpunit/tests/rest-api/rest-attachments-controller.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index 8c838f6d2d9e9..33bb972f27623 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -3202,6 +3202,7 @@ public function test_sideload_scaled_image() { $this->assertArrayHasKey( 'height', $metadata, 'Metadata should contain height.' ); $this->assertArrayHasKey( 'filesize', $metadata, 'Metadata should contain filesize.' ); $this->assertArrayHasKey( 'file', $metadata, 'Metadata should contain file.' ); + $this->assertStringContainsString( 'scaled', $metadata['file'], 'Metadata file should reference the scaled version.' ); $this->assertGreaterThan( 0, $metadata['width'], 'Width should be positive.' ); $this->assertGreaterThan( 0, $metadata['height'], 'Height should be positive.' ); $this->assertGreaterThan( 0, $metadata['filesize'], 'Filesize should be positive.' ); From 978ac795a97d50d9104d9d3377386d1274596371 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 3 Mar 2026 10:35:25 +0700 Subject: [PATCH 08/17] Extract image_size key into variable Avoids repeating the string literal for the array key in the enum assertion test. --- tests/phpunit/tests/rest-api/rest-attachments-controller.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index 33bb972f27623..170b462115059 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -3253,8 +3253,9 @@ public function test_sideload_route_includes_scaled_enum() { $endpoint = $route[0]; $args = $endpoint['args']; - $this->assertArrayHasKey( 'image_size', $args, 'Route should have image_size arg.' ); - $this->assertContains( 'scaled', $args['image_size']['enum'], 'image_size enum should include scaled.' ); + $param_name = 'image_size'; + $this->assertArrayHasKey( $param_name, $args, 'Route should have image_size arg.' ); + $this->assertContains( 'scaled', $args[ $param_name ]['enum'], 'image_size enum should include scaled.' ); } /** From eeca10efd955751433eff1c9a9c91f3688a53510 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 3 Mar 2026 10:37:37 +0700 Subject: [PATCH 09/17] Add test for scaled filename numeric suffix on conflict Verifies that sideloading a scaled image retains the numeric suffix when a file with the same name already exists from a different attachment. --- .../rest-api/rest-attachments-controller.php | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index 170b462115059..b634ecf88e3c6 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -3290,4 +3290,56 @@ public function test_sideload_scaled_unique_filename() { $basename = wp_basename( $new_file ); $this->assertMatchesRegularExpression( '/canola-scaled\.jpg$/', $basename, 'Scaled filename should not have numeric suffix appended.' ); } + + /** + * Tests that sideloading a scaled image for a different attachment retains the numeric suffix + * when a file with the same name already exists on disk. + * + * @ticket 64737 + * @requires function imagejpeg + */ + public function test_sideload_scaled_unique_filename_conflict() { + wp_set_current_user( self::$author_id ); + + // Create the first attachment. + $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); + $request->set_header( 'Content-Type', 'image/jpeg' ); + $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' ); + $request->set_body( file_get_contents( self::$test_file ) ); + $response = rest_get_server()->dispatch( $request ); + $attachment_id_a = $response->get_data()['id']; + + // Sideload a scaled image for attachment A, creating canola-scaled.jpg on disk. + $request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment_id_a}/sideload" ); + $request->set_header( 'Content-Type', 'image/jpeg' ); + $request->set_header( 'Content-Disposition', 'attachment; filename=canola-scaled.jpg' ); + $request->set_param( 'image_size', 'scaled' ); + $request->set_body( file_get_contents( self::$test_file ) ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status(), 'First sideload should succeed.' ); + + // Create a second, different attachment. + $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); + $request->set_header( 'Content-Type', 'image/jpeg' ); + $request->set_header( 'Content-Disposition', 'attachment; filename=other.jpg' ); + $request->set_body( file_get_contents( self::$test_file ) ); + $response = rest_get_server()->dispatch( $request ); + $attachment_id_b = $response->get_data()['id']; + + // Sideload scaled for attachment B using the same filename that already exists on disk. + $request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment_id_b}/sideload" ); + $request->set_header( 'Content-Type', 'image/jpeg' ); + $request->set_header( 'Content-Disposition', 'attachment; filename=canola-scaled.jpg' ); + $request->set_param( 'image_size', 'scaled' ); + $request->set_body( file_get_contents( self::$test_file ) ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status(), 'Second sideload should succeed.' ); + + // The filename should have a numeric suffix since the base name does not match this attachment. + $new_file = get_attached_file( $attachment_id_b, true ); + $basename = wp_basename( $new_file ); + $this->assertMatchesRegularExpression( '/canola-scaled-\d+\.jpg$/', $basename, 'Scaled filename should have numeric suffix when file conflicts with a different attachment.' ); + } } From ae029f1ac2619570fab7398bba82b1a5143d38d7 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 3 Mar 2026 10:44:33 +0700 Subject: [PATCH 10/17] Cast $number to int in regex for safety The $number parameter in filter_wp_unique_filename is typed as int|string. Casting to (int) before interpolation into the preg_match pattern ensures regex safety regardless of any future changes to what $number might contain. --- .../rest-api/endpoints/class-wp-rest-attachments-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 978cbfbebf6f9..35d12b2e2f346 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 @@ -2148,7 +2148,7 @@ private static function filter_wp_unique_filename( $filename, $dir, $number, $at } $matches = array(); - if ( preg_match( '/(.*)(-\d+x\d+|-scaled)-' . $number . '$/', $name, $matches ) ) { + if ( preg_match( '/(.*)(-\d+x\d+|-scaled)-' . (int) $number . '$/', $name, $matches ) ) { $filename_without_suffix = $matches[1] . $matches[2] . ".$ext"; if ( $matches[1] === $orig_name && ! file_exists( "$dir/$filename_without_suffix" ) ) { return $filename_without_suffix; From a484e07217ff3df22074e30721ec2e7d185e13a8 Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Tue, 3 Mar 2026 16:58:21 +0700 Subject: [PATCH 11/17] Apply suggestion from @westonruter Co-authored-by: Weston Ruter --- .../rest-api/endpoints/class-wp-rest-attachments-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 35d12b2e2f346..0a6296c58eae7 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 @@ -2063,7 +2063,7 @@ public function sideload_item( WP_REST_Request $request ) { return new WP_Error( 'rest_sideload_no_attached_file', __( 'Unable to retrieve the attached file for this attachment.' ), - array( 'status' => 400 ) + array( 'status' => 404 ) ); } From fbe74584b2482ab706f5d776d91d0debb039ddb5 Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Tue, 3 Mar 2026 17:04:43 +0700 Subject: [PATCH 12/17] Apply suggestion from @westonruter Co-authored-by: Weston Ruter --- tests/phpunit/tests/rest-api/rest-attachments-controller.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index b634ecf88e3c6..93cd4211c93ba 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -3247,9 +3247,10 @@ public function test_sideload_route_includes_scaled_enum() { $server = rest_get_server(); $routes = $server->get_routes(); - $this->assertArrayHasKey( '/wp/v2/media/(?P[\d]+)/sideload', $routes, 'Sideload route should exist.' ); + $endpoint = '/wp/v2/media/(?P[\d]+)/sideload'; + $this->assertArrayHasKey( $endpoint, $routes, 'Sideload route should exist.' ); - $route = $routes['/wp/v2/media/(?P[\d]+)/sideload']; + $route = $routes[ $endpoint ]; $endpoint = $route[0]; $args = $endpoint['args']; From b30edb634ae2aaa5dd62cf90f93180fc69f2fdfe Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 3 Mar 2026 17:08:12 +0700 Subject: [PATCH 13/17] Handle update_attached_file failure in sideload Check whether _wp_attached_file already matches $path before calling update_attached_file(), since a false return could mean the value is unchanged. Return a WP_Error when the meta value differs but the update still fails. --- .../class-wp-rest-attachments-controller.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) 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 0a6296c58eae7..66f32edbecb2a 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 @@ -2070,7 +2070,16 @@ public function sideload_item( WP_REST_Request $request ) { $metadata['original_image'] = wp_basename( $current_file ); // Update the attached file to point to the scaled version. - update_attached_file( $attachment_id, $path ); + if ( + get_post_meta( $attachment_id, '_wp_attached_file', true ) !== $path && + ! update_attached_file( $attachment_id, $path ) + ) { + return new WP_Error( + 'rest_sideload_update_attached_file_failed', + __( 'Unable to update the attached file for this attachment.' ), + array( 'status' => 500 ) + ); + } $size = wp_getimagesize( $path ); From 56c05e43fc80838cfc385202112c406c88acbd2e Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 3 Mar 2026 17:10:34 +0700 Subject: [PATCH 14/17] Use is_int check instead of empty for $number Per review feedback, use ! is_int( $number ) as the guard since $number is either an int or an empty string. This is more precise than empty() and allows removing the (int) cast in the regex since $number is guaranteed to be an int. --- .../endpoints/class-wp-rest-attachments-controller.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 66f32edbecb2a..c800da4a9fa65 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 @@ -2144,7 +2144,7 @@ public function sideload_item( WP_REST_Request $request ) { * @return string Filtered file name. */ private static function filter_wp_unique_filename( $filename, $dir, $number, $attachment_filename ) { - if ( empty( $number ) || ! $attachment_filename ) { + if ( ! is_int( $number ) || ! $attachment_filename ) { return $filename; } @@ -2157,7 +2157,7 @@ private static function filter_wp_unique_filename( $filename, $dir, $number, $at } $matches = array(); - if ( preg_match( '/(.*)(-\d+x\d+|-scaled)-' . (int) $number . '$/', $name, $matches ) ) { + if ( preg_match( '/(.*)(-\d+x\d+|-scaled)-' . $number . '$/', $name, $matches ) ) { $filename_without_suffix = $matches[1] . $matches[2] . ".$ext"; if ( $matches[1] === $orig_name && ! file_exists( "$dir/$filename_without_suffix" ) ) { return $filename_without_suffix; From d558ceb37148c544150716dcd44c2f86a02948e2 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 3 Mar 2026 17:16:42 +0700 Subject: [PATCH 15/17] Validate scaled image before updating attached file Checks image dimensions and filesize before calling update_attached_file() to avoid leaving the attachment in a bad state if the scaled file is unreadable or empty. --- .../class-wp-rest-attachments-controller.php | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) 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 c800da4a9fa65..a7152ad0a6777 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 @@ -2069,6 +2069,18 @@ public function sideload_item( WP_REST_Request $request ) { $metadata['original_image'] = wp_basename( $current_file ); + // Validate the scaled image before updating the attached file. + $size = wp_getimagesize( $path ); + $filesize = wp_filesize( $path ); + + if ( ! $size || ! $filesize ) { + return new WP_Error( + 'rest_sideload_invalid_image', + __( 'Unable to read the scaled image file.' ), + array( 'status' => 500 ) + ); + } + // Update the attached file to point to the scaled version. if ( get_post_meta( $attachment_id, '_wp_attached_file', true ) !== $path && @@ -2081,11 +2093,9 @@ public function sideload_item( WP_REST_Request $request ) { ); } - $size = wp_getimagesize( $path ); - - $metadata['width'] = $size ? $size[0] : 0; - $metadata['height'] = $size ? $size[1] : 0; - $metadata['filesize'] = wp_filesize( $path ); + $metadata['width'] = $size[0]; + $metadata['height'] = $size[1]; + $metadata['filesize'] = $filesize; $metadata['file'] = _wp_relative_upload_path( $path ); } else { $metadata['sizes'] = $metadata['sizes'] ?? array(); From 851f14a907bf7158003862ce70605fec7321faf5 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 3 Mar 2026 17:26:30 +0700 Subject: [PATCH 16/17] Improve regex grouping in filter_wp_unique_filename. Move the literal dash outside the capture group so the regex reads `/(.*)-(\d+x\d+|scaled)-/` instead of alternating `(-\d+x\d+|-scaled)`. This keeps the dash handling consistent and simplifies the filename reconstruction. Props westonruter. Co-Authored-By: Claude Opus 4.6 --- .../endpoints/class-wp-rest-attachments-controller.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 a7152ad0a6777..66d7e32c4b2b4 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 @@ -2167,8 +2167,8 @@ private static function filter_wp_unique_filename( $filename, $dir, $number, $at } $matches = array(); - if ( preg_match( '/(.*)(-\d+x\d+|-scaled)-' . $number . '$/', $name, $matches ) ) { - $filename_without_suffix = $matches[1] . $matches[2] . ".$ext"; + if ( preg_match( '/(.*)-(\d+x\d+|scaled)-' . $number . '$/', $name, $matches ) ) { + $filename_without_suffix = $matches[1] . '-' . $matches[2] . ".$ext"; if ( $matches[1] === $orig_name && ! file_exists( "$dir/$filename_without_suffix" ) ) { return $filename_without_suffix; } From 1fd9dcbe53cb32d1f2f8409a2345483d46a1a23c Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Wed, 4 Mar 2026 09:26:52 +0700 Subject: [PATCH 17/17] Update src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php Co-authored-by: Weston Ruter --- .../rest-api/endpoints/class-wp-rest-attachments-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 66d7e32c4b2b4..848b4e56d75a1 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 @@ -2083,7 +2083,7 @@ public function sideload_item( WP_REST_Request $request ) { // Update the attached file to point to the scaled version. if ( - get_post_meta( $attachment_id, '_wp_attached_file', true ) !== $path && + get_attached_file( $attachment_id, true ) !== $path && ! update_attached_file( $attachment_id, $path ) ) { return new WP_Error(