From 0229c118e04224a273c5f2ebe90c1244a534ad9f Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Mon, 23 Feb 2026 18:08:28 +0700 Subject: [PATCH 01/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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 6a95756503b779c99270e0f20a5e29f1765ad7ad Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Sun, 1 Mar 2026 17:42:17 +0700 Subject: [PATCH 17/21] REST API: Add dimension validation to sideload endpoint. Validates uploaded image dimensions against expected size constraints in the wp/v2/media//sideload endpoint. This prevents users from uploading incorrectly-sized images for a specified image size. Validation rules: - 'original' size: must match original attachment dimensions exactly. - 'full' and 'scaled' sizes: requires positive dimensions only. - Regular sizes: dimensions must not exceed registered size maximums (with 1px tolerance for rounding differences). Also adds two new test cases for dimension validation. Co-Authored-By: Claude Opus 4.6 --- .../class-wp-rest-attachments-controller.php | 122 +++++++++++++++++- .../rest-api/rest-attachments-controller.php | 58 +++++++++ 2 files changed, 178 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 66d7e32c4b2b4..45ef658a39d2e 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 @@ -1968,6 +1968,114 @@ public function sideload_item_permissions_check( $request ) { return $this->edit_media_item_permissions_check( $request ); } + /** + * Validates that uploaded image dimensions are appropriate for the specified image size. + * + * @since 7.0.0 + * + * @param int $width Uploaded image width. + * @param int $height Uploaded image height. + * @param string $image_size The target image size name. + * @param int $attachment_id The attachment ID. + * @return true|WP_Error True if valid, WP_Error if invalid. + */ + private function validate_image_dimensions( int $width, int $height, string $image_size, int $attachment_id ) { + // 'original' size: should match original attachment dimensions. + if ( 'original' === $image_size ) { + $metadata = wp_get_attachment_metadata( $attachment_id, true ); + if ( is_array( $metadata ) && isset( $metadata['width'], $metadata['height'] ) ) { + $expected_width = (int) $metadata['width']; + $expected_height = (int) $metadata['height']; + + if ( $width !== $expected_width || $height !== $expected_height ) { + return new WP_Error( + 'rest_upload_dimension_mismatch', + sprintf( + /* translators: 1: Expected width, 2: expected height, 3: actual width, 4: actual height. */ + __( 'Uploaded image dimensions (%3$dx%4$d) do not match original image dimensions (%1$dx%2$d).' ), + $expected_width, + $expected_height, + $width, + $height + ), + array( 'status' => 400 ) + ); + } + } + return true; + } + + // 'full' size (PDF thumbnails) and 'scaled': dimensions must be positive. + if ( 'full' === $image_size || 'scaled' === $image_size ) { + if ( $width <= 0 || $height <= 0 ) { + return new WP_Error( + 'rest_upload_invalid_dimensions', + __( 'Uploaded image must have positive dimensions.' ), + array( 'status' => 400 ) + ); + } + return true; + } + + // Regular image sizes: validate against registered size constraints. + $registered_sizes = wp_get_registered_image_subsizes(); + + if ( ! isset( $registered_sizes[ $image_size ] ) ) { + return new WP_Error( + 'rest_upload_unknown_size', + __( 'Unknown image size.' ), + array( 'status' => 400 ) + ); + } + + $size_data = $registered_sizes[ $image_size ]; + $max_width = (int) $size_data['width']; + $max_height = (int) $size_data['height']; + + // Dimensions must be positive. + if ( $width <= 0 || $height <= 0 ) { + return new WP_Error( + 'rest_upload_invalid_dimensions', + __( 'Uploaded image must have positive dimensions.' ), + array( 'status' => 400 ) + ); + } + + // Validate dimensions don't exceed the registered size maximums. + // Allow 1px tolerance for rounding differences. + $tolerance = 1; + + if ( $max_width > 0 && $width > $max_width + $tolerance ) { + return new WP_Error( + 'rest_upload_dimension_mismatch', + sprintf( + /* translators: 1: Image size name, 2: maximum width, 3: actual width. */ + __( 'Uploaded image width (%3$d) exceeds maximum for "%1$s" size (%2$d).' ), + $image_size, + $max_width, + $width + ), + array( 'status' => 400 ) + ); + } + + if ( $max_height > 0 && $height > $max_height + $tolerance ) { + return new WP_Error( + 'rest_upload_dimension_mismatch', + sprintf( + /* translators: 1: Image size name, 2: maximum height, 3: actual height. */ + __( 'Uploaded image height (%3$d) exceeds maximum for "%1$s" size (%2$d).' ), + $image_size, + $max_height, + $height + ), + array( 'status' => 400 ) + ); + } + + return true; + } + /** * Side-loads a media file without creating a new attachment. * @@ -2047,6 +2155,18 @@ public function sideload_item( WP_REST_Request $request ) { $image_size = $request['image_size']; + $size = wp_getimagesize( $path ); + + // Validate dimensions match expected size. + if ( $size ) { + $validation = $this->validate_image_dimensions( $size[0], $size[1], $image_size, $attachment_id ); + if ( is_wp_error( $validation ) ) { + // Clean up the uploaded file. + wp_delete_file( $path ); + return $validation; + } + } + $metadata = wp_get_attachment_metadata( $attachment_id, true ); if ( ! $metadata ) { @@ -2100,8 +2220,6 @@ public function sideload_item( WP_REST_Request $request ) { } else { $metadata['sizes'] = $metadata['sizes'] ?? array(); - $size = wp_getimagesize( $path ); - $metadata['sizes'][ $image_size ] = array( 'width' => $size ? $size[0] : 0, 'height' => $size ? $size[1] : 0, diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index 93cd4211c93ba..c8c7fb18ae159 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -3343,4 +3343,62 @@ public function test_sideload_scaled_unique_filename_conflict() { $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.' ); } + + /** + * Tests that sideloading an oversized image for a registered size is rejected. + * + * @ticket 63 + * @requires function imagejpeg + */ + public function test_sideload_item_rejects_oversized_dimensions() { + 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']; + + // canola.jpg is 640x480, which exceeds the default thumbnail size (150x150). + $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-150x150.jpg' ); + $request->set_param( 'image_size', 'thumbnail' ); + $request->set_body( file_get_contents( self::$test_file ) ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 400, $response->get_status(), 'Oversized image should be rejected.' ); + $this->assertSame( 'rest_upload_dimension_mismatch', $response->get_data()['code'], 'Error code should be rest_upload_dimension_mismatch.' ); + } + + /** + * Tests that sideloading a correctly sized image for a registered size succeeds. + * + * @ticket 63 + * @requires function imagejpeg + */ + public function test_sideload_item_accepts_valid_dimensions() { + 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']; + + // test-image.jpg is 50x50, which fits within the default thumbnail size (150x150). + $test_image = DIR_TESTDATA . '/images/test-image.jpg'; + $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-50x50.jpg' ); + $request->set_param( 'image_size', 'thumbnail' ); + $request->set_body( file_get_contents( $test_image ) ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status(), 'Valid-sized image should be accepted.' ); + } } From 2cb24541955f45e5f0c8ad3fd52009c469ba3308 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Mon, 2 Mar 2026 10:51:41 +0700 Subject: [PATCH 18/21] REST API: Refactor dimension validation in sideload endpoint. Move the positive-dimensions check to the top of validate_image_dimensions() to apply it universally and eliminate duplication. Reorder sprintf args in the dimension mismatch message to match display order, reducing cognitive load. Props apermo. Co-Authored-By: Claude Opus 4.6 --- .../class-wp-rest-attachments-controller.php | 37 ++++++++----------- 1 file changed, 15 insertions(+), 22 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 45ef658a39d2e..cbd4c05396edb 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 @@ -1980,6 +1980,15 @@ public function sideload_item_permissions_check( $request ) { * @return true|WP_Error True if valid, WP_Error if invalid. */ private function validate_image_dimensions( int $width, int $height, string $image_size, int $attachment_id ) { + // All image sizes require positive dimensions. + if ( $width <= 0 || $height <= 0 ) { + return new WP_Error( + 'rest_upload_invalid_dimensions', + __( 'Uploaded image must have positive dimensions.' ), + array( 'status' => 400 ) + ); + } + // 'original' size: should match original attachment dimensions. if ( 'original' === $image_size ) { $metadata = wp_get_attachment_metadata( $attachment_id, true ); @@ -1991,12 +2000,12 @@ private function validate_image_dimensions( int $width, int $height, string $ima return new WP_Error( 'rest_upload_dimension_mismatch', sprintf( - /* translators: 1: Expected width, 2: expected height, 3: actual width, 4: actual height. */ - __( 'Uploaded image dimensions (%3$dx%4$d) do not match original image dimensions (%1$dx%2$d).' ), - $expected_width, - $expected_height, + /* translators: 1: Actual width, 2: actual height, 3: expected width, 4: expected height. */ + __( 'Uploaded image dimensions (%1$dx%2$d) do not match original image dimensions (%3$dx%4$d).' ), $width, - $height + $height, + $expected_width, + $expected_height ), array( 'status' => 400 ) ); @@ -2005,15 +2014,8 @@ private function validate_image_dimensions( int $width, int $height, string $ima return true; } - // 'full' size (PDF thumbnails) and 'scaled': dimensions must be positive. + // 'full' size (PDF thumbnails) and 'scaled': no further constraints. if ( 'full' === $image_size || 'scaled' === $image_size ) { - if ( $width <= 0 || $height <= 0 ) { - return new WP_Error( - 'rest_upload_invalid_dimensions', - __( 'Uploaded image must have positive dimensions.' ), - array( 'status' => 400 ) - ); - } return true; } @@ -2032,15 +2034,6 @@ private function validate_image_dimensions( int $width, int $height, string $ima $max_width = (int) $size_data['width']; $max_height = (int) $size_data['height']; - // Dimensions must be positive. - if ( $width <= 0 || $height <= 0 ) { - return new WP_Error( - 'rest_upload_invalid_dimensions', - __( 'Uploaded image must have positive dimensions.' ), - array( 'status' => 400 ) - ); - } - // Validate dimensions don't exceed the registered size maximums. // Allow 1px tolerance for rounding differences. $tolerance = 1; From 5474fcd983d2a8c329c8d220eb610a6ad93f7a29 Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Tue, 3 Mar 2026 09:36:16 +0700 Subject: [PATCH 19/21] Update src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php Co-authored-by: Christoph Daum --- .../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 cbd4c05396edb..022221e6cf75b 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 @@ -2151,7 +2151,7 @@ public function sideload_item( WP_REST_Request $request ) { $size = wp_getimagesize( $path ); // Validate dimensions match expected size. - if ( $size ) { + if ( is_array( $size ) ) { $validation = $this->validate_image_dimensions( $size[0], $size[1], $image_size, $attachment_id ); if ( is_wp_error( $validation ) ) { // Clean up the uploaded file. From 0cec0b0e9677c9057dd5076451f8d6cd30f17bdf Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 3 Mar 2026 17:45:02 +0700 Subject: [PATCH 20/21] revert unrelated --- .../rest-api/rest-attachments-controller.php | 247 ------------------ tests/qunit/fixtures/wp-api-generated.js | 3 +- 2 files changed, 1 insertion(+), 249 deletions(-) diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index c8c7fb18ae159..796e58c45d97d 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -3154,251 +3154,4 @@ 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 64737 - * @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->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.' ); - } - - /** - * Tests that sideloading scaled image requires authentication. - * - * @ticket 64737 - * @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_cannot_edit_image', $response, 401 ); - } - - /** - * Tests that the sideload endpoint includes 'scaled' in the image_size enum. - * - * @ticket 64737 - */ - public function test_sideload_route_includes_scaled_enum() { - $server = rest_get_server(); - $routes = $server->get_routes(); - - $endpoint = '/wp/v2/media/(?P[\d]+)/sideload'; - $this->assertArrayHasKey( $endpoint, $routes, 'Sideload route should exist.' ); - - $route = $routes[ $endpoint ]; - $endpoint = $route[0]; - $args = $endpoint['args']; - - $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.' ); - } - - /** - * Tests the filter_wp_unique_filename method handles the -scaled suffix. - * - * @ticket 64737 - * @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.' ); - } - - /** - * 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.' ); - } - - /** - * Tests that sideloading an oversized image for a registered size is rejected. - * - * @ticket 63 - * @requires function imagejpeg - */ - public function test_sideload_item_rejects_oversized_dimensions() { - 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']; - - // canola.jpg is 640x480, which exceeds the default thumbnail size (150x150). - $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-150x150.jpg' ); - $request->set_param( 'image_size', 'thumbnail' ); - $request->set_body( file_get_contents( self::$test_file ) ); - $response = rest_get_server()->dispatch( $request ); - - $this->assertSame( 400, $response->get_status(), 'Oversized image should be rejected.' ); - $this->assertSame( 'rest_upload_dimension_mismatch', $response->get_data()['code'], 'Error code should be rest_upload_dimension_mismatch.' ); - } - - /** - * Tests that sideloading a correctly sized image for a registered size succeeds. - * - * @ticket 63 - * @requires function imagejpeg - */ - public function test_sideload_item_accepts_valid_dimensions() { - 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']; - - // test-image.jpg is 50x50, which fits within the default thumbnail size (150x150). - $test_image = DIR_TESTDATA . '/images/test-image.jpg'; - $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-50x50.jpg' ); - $request->set_param( 'image_size', 'thumbnail' ); - $request->set_body( file_get_contents( $test_image ) ); - $response = rest_get_server()->dispatch( $request ); - - $this->assertSame( 200, $response->get_status(), 'Valid-sized image should be accepted.' ); - } } diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 40e20d10d4dc4..ef59ce5ca5074 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -3703,8 +3703,7 @@ mockedApiResponse.Schema = { "1536x1536", "2048x2048", "original", - "full", - "scaled" + "full" ], "required": true }, From 2566f7c9e0052088350984943ea3678d232746ef Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 3 Mar 2026 17:46:12 +0700 Subject: [PATCH 21/21] revert dupicate change --- .../rest-api/endpoints/class-wp-rest-attachments-controller.php | 2 -- 1 file changed, 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 022221e6cf75b..e91eb4c694b4b 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,8 +70,6 @@ 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,