Skip to content

Commit db6e7ac

Browse files
REST API: Add 'scaled' to sideload route image_size enum
Fix an issue where sideloaded images with a ‘-scaled’ suffix would respond with an error. When users upload a very large image in the editor, the client-side media processing sideloads a scaled version of that image with a ‘-scaled’ suffix. Props adamsilverstein, huzaifaalmesbah, westonruter. Fixes #64737. git-svn-id: https://develop.svn.wordpress.org/trunk@61809 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 0738208 commit db6e7ac

3 files changed

Lines changed: 238 additions & 4 deletions

File tree

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

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ public function register_routes() {
7070
$valid_image_sizes[] = 'original';
7171
// Used for PDF thumbnails.
7272
$valid_image_sizes[] = 'full';
73+
// Client-side big image threshold: sideload the scaled version.
74+
$valid_image_sizes[] = 'scaled';
7375

7476
register_rest_route(
7577
$this->namespace,
@@ -2053,6 +2055,48 @@ public function sideload_item( WP_REST_Request $request ) {
20532055

20542056
if ( 'original' === $image_size ) {
20552057
$metadata['original_image'] = wp_basename( $path );
2058+
} elseif ( 'scaled' === $image_size ) {
2059+
// The current attached file is the original; record it as original_image.
2060+
$current_file = get_attached_file( $attachment_id, true );
2061+
2062+
if ( ! $current_file ) {
2063+
return new WP_Error(
2064+
'rest_sideload_no_attached_file',
2065+
__( 'Unable to retrieve the attached file for this attachment.' ),
2066+
array( 'status' => 404 )
2067+
);
2068+
}
2069+
2070+
$metadata['original_image'] = wp_basename( $current_file );
2071+
2072+
// Validate the scaled image before updating the attached file.
2073+
$size = wp_getimagesize( $path );
2074+
$filesize = wp_filesize( $path );
2075+
2076+
if ( ! $size || ! $filesize ) {
2077+
return new WP_Error(
2078+
'rest_sideload_invalid_image',
2079+
__( 'Unable to read the scaled image file.' ),
2080+
array( 'status' => 500 )
2081+
);
2082+
}
2083+
2084+
// Update the attached file to point to the scaled version.
2085+
if (
2086+
get_attached_file( $attachment_id, true ) !== $path &&
2087+
! update_attached_file( $attachment_id, $path )
2088+
) {
2089+
return new WP_Error(
2090+
'rest_sideload_update_attached_file_failed',
2091+
__( 'Unable to update the attached file for this attachment.' ),
2092+
array( 'status' => 500 )
2093+
);
2094+
}
2095+
2096+
$metadata['width'] = $size[0];
2097+
$metadata['height'] = $size[1];
2098+
$metadata['filesize'] = $filesize;
2099+
$metadata['file'] = _wp_relative_upload_path( $path );
20562100
} else {
20572101
$metadata['sizes'] = $metadata['sizes'] ?? array();
20582102

@@ -2110,7 +2154,7 @@ public function sideload_item( WP_REST_Request $request ) {
21102154
* @return string Filtered file name.
21112155
*/
21122156
private static function filter_wp_unique_filename( $filename, $dir, $number, $attachment_filename ) {
2113-
if ( empty( $number ) || ! $attachment_filename ) {
2157+
if ( ! is_int( $number ) || ! $attachment_filename ) {
21142158
return $filename;
21152159
}
21162160

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

21252169
$matches = array();
2126-
if ( preg_match( '/(.*)(-\d+x\d+)-' . $number . '$/', $name, $matches ) ) {
2127-
$filename_without_suffix = $matches[1] . $matches[2] . ".$ext";
2170+
if ( preg_match( '/(.*)-(\d+x\d+|scaled)-' . $number . '$/', $name, $matches ) ) {
2171+
$filename_without_suffix = $matches[1] . '-' . $matches[2] . ".$ext";
21282172
if ( $matches[1] === $orig_name && ! file_exists( "$dir/$filename_without_suffix" ) ) {
21292173
return $filename_without_suffix;
21302174
}

tests/phpunit/tests/rest-api/rest-attachments-controller.php

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3154,4 +3154,193 @@ static function ( $data ) use ( &$captured_data ) {
31543154
// Verify that the data is an array (not an object).
31553155
$this->assertIsArray( $captured_data, 'Data passed to wp_insert_attachment should be an array' );
31563156
}
3157+
3158+
/**
3159+
* Tests sideloading a scaled image for an existing attachment.
3160+
*
3161+
* @ticket 64737
3162+
* @requires function imagejpeg
3163+
*/
3164+
public function test_sideload_scaled_image() {
3165+
wp_set_current_user( self::$author_id );
3166+
3167+
// First, create an attachment.
3168+
$request = new WP_REST_Request( 'POST', '/wp/v2/media' );
3169+
$request->set_header( 'Content-Type', 'image/jpeg' );
3170+
$request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' );
3171+
$request->set_body( file_get_contents( self::$test_file ) );
3172+
$response = rest_get_server()->dispatch( $request );
3173+
$data = $response->get_data();
3174+
$attachment_id = $data['id'];
3175+
3176+
$this->assertSame( 201, $response->get_status() );
3177+
3178+
$original_file = get_attached_file( $attachment_id, true );
3179+
3180+
// Sideload a "scaled" version of the image.
3181+
$request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment_id}/sideload" );
3182+
$request->set_header( 'Content-Type', 'image/jpeg' );
3183+
$request->set_header( 'Content-Disposition', 'attachment; filename=canola-scaled.jpg' );
3184+
$request->set_param( 'image_size', 'scaled' );
3185+
$request->set_body( file_get_contents( self::$test_file ) );
3186+
$response = rest_get_server()->dispatch( $request );
3187+
3188+
$this->assertSame( 200, $response->get_status(), 'Sideloading scaled image should succeed.' );
3189+
3190+
$metadata = wp_get_attachment_metadata( $attachment_id );
3191+
3192+
// The original file should now be recorded as original_image.
3193+
$this->assertArrayHasKey( 'original_image', $metadata, 'Metadata should contain original_image.' );
3194+
$this->assertSame( wp_basename( $original_file ), $metadata['original_image'], 'original_image should be the basename of the original attached file.' );
3195+
3196+
// The attached file should now point to the scaled version.
3197+
$new_file = get_attached_file( $attachment_id, true );
3198+
$this->assertStringContainsString( 'scaled', wp_basename( $new_file ), 'Attached file should now be the scaled version.' );
3199+
3200+
// Metadata should have width, height, filesize, and file updated.
3201+
$this->assertArrayHasKey( 'width', $metadata, 'Metadata should contain width.' );
3202+
$this->assertArrayHasKey( 'height', $metadata, 'Metadata should contain height.' );
3203+
$this->assertArrayHasKey( 'filesize', $metadata, 'Metadata should contain filesize.' );
3204+
$this->assertArrayHasKey( 'file', $metadata, 'Metadata should contain file.' );
3205+
$this->assertStringContainsString( 'scaled', $metadata['file'], 'Metadata file should reference the scaled version.' );
3206+
$this->assertGreaterThan( 0, $metadata['width'], 'Width should be positive.' );
3207+
$this->assertGreaterThan( 0, $metadata['height'], 'Height should be positive.' );
3208+
$this->assertGreaterThan( 0, $metadata['filesize'], 'Filesize should be positive.' );
3209+
}
3210+
3211+
/**
3212+
* Tests that sideloading scaled image requires authentication.
3213+
*
3214+
* @ticket 64737
3215+
* @requires function imagejpeg
3216+
*/
3217+
public function test_sideload_scaled_image_requires_auth() {
3218+
wp_set_current_user( self::$author_id );
3219+
3220+
// Create an attachment.
3221+
$request = new WP_REST_Request( 'POST', '/wp/v2/media' );
3222+
$request->set_header( 'Content-Type', 'image/jpeg' );
3223+
$request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' );
3224+
$request->set_body( file_get_contents( self::$test_file ) );
3225+
$response = rest_get_server()->dispatch( $request );
3226+
$attachment_id = $response->get_data()['id'];
3227+
3228+
// Try sideloading without authentication.
3229+
wp_set_current_user( 0 );
3230+
3231+
$request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment_id}/sideload" );
3232+
$request->set_header( 'Content-Type', 'image/jpeg' );
3233+
$request->set_header( 'Content-Disposition', 'attachment; filename=canola-scaled.jpg' );
3234+
$request->set_param( 'image_size', 'scaled' );
3235+
$request->set_body( file_get_contents( self::$test_file ) );
3236+
$response = rest_get_server()->dispatch( $request );
3237+
3238+
$this->assertErrorResponse( 'rest_cannot_edit_image', $response, 401 );
3239+
}
3240+
3241+
/**
3242+
* Tests that the sideload endpoint includes 'scaled' in the image_size enum.
3243+
*
3244+
* @ticket 64737
3245+
*/
3246+
public function test_sideload_route_includes_scaled_enum() {
3247+
$server = rest_get_server();
3248+
$routes = $server->get_routes();
3249+
3250+
$endpoint = '/wp/v2/media/(?P<id>[\d]+)/sideload';
3251+
$this->assertArrayHasKey( $endpoint, $routes, 'Sideload route should exist.' );
3252+
3253+
$route = $routes[ $endpoint ];
3254+
$endpoint = $route[0];
3255+
$args = $endpoint['args'];
3256+
3257+
$param_name = 'image_size';
3258+
$this->assertArrayHasKey( $param_name, $args, 'Route should have image_size arg.' );
3259+
$this->assertContains( 'scaled', $args[ $param_name ]['enum'], 'image_size enum should include scaled.' );
3260+
}
3261+
3262+
/**
3263+
* Tests the filter_wp_unique_filename method handles the -scaled suffix.
3264+
*
3265+
* @ticket 64737
3266+
* @requires function imagejpeg
3267+
*/
3268+
public function test_sideload_scaled_unique_filename() {
3269+
wp_set_current_user( self::$author_id );
3270+
3271+
// Create an attachment.
3272+
$request = new WP_REST_Request( 'POST', '/wp/v2/media' );
3273+
$request->set_header( 'Content-Type', 'image/jpeg' );
3274+
$request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' );
3275+
$request->set_body( file_get_contents( self::$test_file ) );
3276+
$response = rest_get_server()->dispatch( $request );
3277+
$attachment_id = $response->get_data()['id'];
3278+
3279+
// Sideload with the -scaled suffix.
3280+
$request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment_id}/sideload" );
3281+
$request->set_header( 'Content-Type', 'image/jpeg' );
3282+
$request->set_header( 'Content-Disposition', 'attachment; filename=canola-scaled.jpg' );
3283+
$request->set_param( 'image_size', 'scaled' );
3284+
$request->set_body( file_get_contents( self::$test_file ) );
3285+
$response = rest_get_server()->dispatch( $request );
3286+
3287+
$this->assertSame( 200, $response->get_status(), 'Sideloading scaled image should succeed.' );
3288+
3289+
// The filename should retain the -scaled suffix without numeric disambiguation.
3290+
$new_file = get_attached_file( $attachment_id, true );
3291+
$basename = wp_basename( $new_file );
3292+
$this->assertMatchesRegularExpression( '/canola-scaled\.jpg$/', $basename, 'Scaled filename should not have numeric suffix appended.' );
3293+
}
3294+
3295+
/**
3296+
* Tests that sideloading a scaled image for a different attachment retains the numeric suffix
3297+
* when a file with the same name already exists on disk.
3298+
*
3299+
* @ticket 64737
3300+
* @requires function imagejpeg
3301+
*/
3302+
public function test_sideload_scaled_unique_filename_conflict() {
3303+
wp_set_current_user( self::$author_id );
3304+
3305+
// Create the first attachment.
3306+
$request = new WP_REST_Request( 'POST', '/wp/v2/media' );
3307+
$request->set_header( 'Content-Type', 'image/jpeg' );
3308+
$request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' );
3309+
$request->set_body( file_get_contents( self::$test_file ) );
3310+
$response = rest_get_server()->dispatch( $request );
3311+
$attachment_id_a = $response->get_data()['id'];
3312+
3313+
// Sideload a scaled image for attachment A, creating canola-scaled.jpg on disk.
3314+
$request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment_id_a}/sideload" );
3315+
$request->set_header( 'Content-Type', 'image/jpeg' );
3316+
$request->set_header( 'Content-Disposition', 'attachment; filename=canola-scaled.jpg' );
3317+
$request->set_param( 'image_size', 'scaled' );
3318+
$request->set_body( file_get_contents( self::$test_file ) );
3319+
$response = rest_get_server()->dispatch( $request );
3320+
3321+
$this->assertSame( 200, $response->get_status(), 'First sideload should succeed.' );
3322+
3323+
// Create a second, different attachment.
3324+
$request = new WP_REST_Request( 'POST', '/wp/v2/media' );
3325+
$request->set_header( 'Content-Type', 'image/jpeg' );
3326+
$request->set_header( 'Content-Disposition', 'attachment; filename=other.jpg' );
3327+
$request->set_body( file_get_contents( self::$test_file ) );
3328+
$response = rest_get_server()->dispatch( $request );
3329+
$attachment_id_b = $response->get_data()['id'];
3330+
3331+
// Sideload scaled for attachment B using the same filename that already exists on disk.
3332+
$request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment_id_b}/sideload" );
3333+
$request->set_header( 'Content-Type', 'image/jpeg' );
3334+
$request->set_header( 'Content-Disposition', 'attachment; filename=canola-scaled.jpg' );
3335+
$request->set_param( 'image_size', 'scaled' );
3336+
$request->set_body( file_get_contents( self::$test_file ) );
3337+
$response = rest_get_server()->dispatch( $request );
3338+
3339+
$this->assertSame( 200, $response->get_status(), 'Second sideload should succeed.' );
3340+
3341+
// The filename should have a numeric suffix since the base name does not match this attachment.
3342+
$new_file = get_attached_file( $attachment_id_b, true );
3343+
$basename = wp_basename( $new_file );
3344+
$this->assertMatchesRegularExpression( '/canola-scaled-\d+\.jpg$/', $basename, 'Scaled filename should have numeric suffix when file conflicts with a different attachment.' );
3345+
}
31573346
}

tests/qunit/fixtures/wp-api-generated.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3703,7 +3703,8 @@ mockedApiResponse.Schema = {
37033703
"1536x1536",
37043704
"2048x2048",
37053705
"original",
3706-
"full"
3706+
"full",
3707+
"scaled"
37073708
],
37083709
"required": true
37093710
},

0 commit comments

Comments
 (0)