Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
0229c11
REST API: Add 'scaled' to sideload route image_size enum
adamsilverstein Feb 23, 2026
3832e25
Merge branch 'trunk' into add-scaled-to-sideload-route
adamsilverstein Feb 26, 2026
0dcab83
REST API: Update auto-generated JS fixture for sideload route.
adamsilverstein Feb 26, 2026
2fe2162
Tests: Add unit tests for scaled image sideloading via REST API.
adamsilverstein Feb 26, 2026
133ec0f
REST API: Validate get_attached_file() in sideload
adamsilverstein Feb 26, 2026
a868b57
Tests: Fix expected error code in sideload auth test
adamsilverstein Feb 26, 2026
e5baa99
fix ticket number for test annotations
adamsilverstein Mar 2, 2026
5405e38
Merge branch 'trunk' into add-scaled-to-sideload-route
adamsilverstein Mar 2, 2026
02a9794
Merge branch 'trunk' into add-scaled-to-sideload-route
adamsilverstein Mar 3, 2026
458d94c
Add value assertion for metadata file key
adamsilverstein Mar 3, 2026
978ac79
Extract image_size key into variable
adamsilverstein Mar 3, 2026
eeca10e
Add test for scaled filename numeric suffix on conflict
adamsilverstein Mar 3, 2026
ae029f1
Cast $number to int in regex for safety
adamsilverstein Mar 3, 2026
a484e07
Apply suggestion from @westonruter
adamsilverstein Mar 3, 2026
fbe7458
Apply suggestion from @westonruter
adamsilverstein Mar 3, 2026
b30edb6
Handle update_attached_file failure in sideload
adamsilverstein Mar 3, 2026
56c05e4
Use is_int check instead of empty for $number
adamsilverstein Mar 3, 2026
d558ceb
Validate scaled image before updating attached file
adamsilverstein Mar 3, 2026
851f14a
Improve regex grouping in filter_wp_unique_filename.
adamsilverstein Mar 3, 2026
0743a97
Merge branch 'trunk' into add-scaled-to-sideload-route
adamsilverstein Mar 3, 2026
1fd9dcb
Update src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-c…
adamsilverstein Mar 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -2053,6 +2055,48 @@ 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 );

if ( ! $current_file ) {
return new WP_Error(
'rest_sideload_no_attached_file',
__( 'Unable to retrieve the attached file for this attachment.' ),
array( 'status' => 404 )
);
}

$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_attached_file( $attachment_id, 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 )
);
}

$metadata['width'] = $size[0];
$metadata['height'] = $size[1];
$metadata['filesize'] = $filesize;
$metadata['file'] = _wp_relative_upload_path( $path );
} else {
$metadata['sizes'] = $metadata['sizes'] ?? array();

Expand Down Expand Up @@ -2110,7 +2154,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;
}

Expand All @@ -2123,8 +2167,8 @@ private static function filter_wp_unique_filename( $filename, $dir, $number, $at
}

$matches = array();
if ( preg_match( '/(.*)(-\d+x\d+)-' . $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;
}
Expand Down
189 changes: 189 additions & 0 deletions tests/phpunit/tests/rest-api/rest-attachments-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -3154,4 +3154,193 @@ 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<id>[\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.' );
}
}
3 changes: 2 additions & 1 deletion tests/qunit/fixtures/wp-api-generated.js
Original file line number Diff line number Diff line change
Expand Up @@ -3703,7 +3703,8 @@ mockedApiResponse.Schema = {
"1536x1536",
"2048x2048",
"original",
"full"
"full",
"scaled"
],
"required": true
},
Expand Down
Loading