Make WordPress Core

Changeset 61809


Ignore:
Timestamp:
03/04/2026 06:57:40 AM (4 weeks ago)
Author:
adamsilverstein
Message:

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.

Location:
trunk
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php

    r61703 r61809  
    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(
     
    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();
     
    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        }
     
    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;
  • trunk/tests/phpunit/tests/rest-api/rest-attachments-controller.php

    r61703 r61809  
    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}
  • trunk/tests/qunit/fixtures/wp-api-generated.js

    r61749 r61809  
    37043704                                "2048x2048",
    37053705                                "original",
    3706                                 "full"
     3706                                "full",
     3707                                "scaled"
    37073708                            ],
    37083709                            "required": true
Note: See TracChangeset for help on using the changeset viewer.