Make WordPress Core

Changeset 61809


Ignore:
Timestamp:
03/04/2026 06:57:40 AM (6 months 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.