Make WordPress Core

Changeset 61982


Ignore:
Timestamp:
03/12/2026 06:01:47 AM (5 months ago)
Author:
adamsilverstein
Message:

REST API: Add finalize endpoint to WP_REST_Attachments_Controller.

Introduce a POST /wp/v2/media/{id}/finalize REST API endpoint that re-triggers the wp_generate_attachment_metadata filter with context 'update' after client-side media processing completes. This ensures server-side plugins (watermarking, CDN sync, custom sizes, etc.) can post-process attachments when client-side processing is active.

The endpoint reuses edit_media_item_permissions_check for authorization and is only registered when wp_is_client_side_media_processing_enabled() returns true.

See https://github.com/WordPress/gutenberg/pull/74913.
See https://github.com/WordPress/gutenberg/issues/74358.

Props adamsilverstein, westonruter, mukesh27, divyeshpatel01.
Fixes #64804.

Location:
trunk
Files:
4 edited

Legend:

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

    r61980 r61982  
    9797                                                                'default'     => true,
    9898                                                                'description' => __( 'Whether to convert image formats.' ),
     99                                                        ),
     100                                                ),
     101                                        ),
     102                                        'allow_batch' => $this->allow_batch,
     103                                        'schema'      => array( $this, 'get_public_item_schema' ),
     104                                )
     105                        );
     106
     107                        register_rest_route(
     108                                $this->namespace,
     109                                '/' . $this->rest_base . '/(?P<id>[\d]+)/finalize',
     110                                array(
     111                                        array(
     112                                                'methods'             => WP_REST_Server::CREATABLE,
     113                                                'callback'            => array( $this, 'finalize_item' ),
     114                                                'permission_callback' => array( $this, 'edit_media_item_permissions_check' ),
     115                                                'args'                => array(
     116                                                        'id' => array(
     117                                                                'description' => __( 'Unique identifier for the attachment.' ),
     118                                                                'type'        => 'integer',
    99119                                                        ),
    100120                                                ),
     
    21922212                return $filename;
    21932213        }
     2214
     2215        /**
     2216         * Finalizes an attachment after client-side media processing.
     2217         *
     2218         * Triggers the 'wp_generate_attachment_metadata' filter so that
     2219         * server-side plugins can process the attachment after all client-side
     2220         * operations (upload, thumbnail generation, sideloads) are complete.
     2221         *
     2222         * @since 7.0.0
     2223         *
     2224         * @param WP_REST_Request $request Full details about the request.
     2225         * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure.
     2226         */
     2227        public function finalize_item( WP_REST_Request $request ) {
     2228                $attachment_id = $request['id'];
     2229
     2230                $post = $this->get_post( $attachment_id );
     2231                if ( is_wp_error( $post ) ) {
     2232                        return $post;
     2233                }
     2234
     2235                $metadata = wp_get_attachment_metadata( $attachment_id );
     2236                if ( ! is_array( $metadata ) ) {
     2237                        $metadata = array();
     2238                }
     2239
     2240                /** This filter is documented in wp-admin/includes/image.php */
     2241                $metadata = apply_filters( 'wp_generate_attachment_metadata', $metadata, $attachment_id, 'update' );
     2242
     2243                wp_update_attachment_metadata( $attachment_id, $metadata );
     2244
     2245                $response_request = new WP_REST_Request(
     2246                        WP_REST_Server::READABLE,
     2247                        rest_get_route_for_post( $attachment_id )
     2248                );
     2249
     2250                $response_request['context'] = 'edit';
     2251
     2252                if ( isset( $request['_fields'] ) ) {
     2253                        $response_request['_fields'] = $request['_fields'];
     2254                }
     2255
     2256                return $this->prepare_item_for_response( $post, $response_request );
     2257        }
    21942258}
  • trunk/tests/phpunit/tests/rest-api/rest-attachments-controller.php

    r61980 r61982  
    34483448                $this->assertMatchesRegularExpression( '/canola-scaled-\d+\.jpg$/', $basename, 'Scaled filename should have numeric suffix when file conflicts with a different attachment.' );
    34493449        }
     3450
     3451        /**
     3452         * Tests that the finalize endpoint triggers wp_generate_attachment_metadata.
     3453         *
     3454         * @ticket 62243
     3455         * @covers WP_REST_Attachments_Controller::finalize_item
     3456         * @requires function imagejpeg
     3457         */
     3458        public function test_finalize_item(): void {
     3459                wp_set_current_user( self::$author_id );
     3460
     3461                // Create an attachment.
     3462                $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
     3463                $request->set_header( 'Content-Type', 'image/jpeg' );
     3464                $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' );
     3465                $request->set_body( (string) file_get_contents( self::$test_file ) );
     3466                $response      = rest_get_server()->dispatch( $request );
     3467                $attachment_id = $response->get_data()['id'];
     3468
     3469                $this->assertSame( 201, $response->get_status() );
     3470
     3471                // Track whether wp_generate_attachment_metadata filter fires.
     3472                $filter_metadata = null;
     3473                $filter_id       = null;
     3474                $filter_context  = null;
     3475                add_filter(
     3476                        'wp_generate_attachment_metadata',
     3477                        function ( array $metadata, int $id, string $context ) use ( &$filter_metadata, &$filter_id, &$filter_context ) {
     3478                                $filter_metadata = $metadata;
     3479                                $filter_id       = $id;
     3480                                $filter_context  = $context;
     3481                                $metadata['foo'] = 'bar';
     3482                                return $metadata;
     3483                        },
     3484                        10,
     3485                        3
     3486                );
     3487
     3488                // Call the finalize endpoint.
     3489                $request  = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment_id}/finalize" );
     3490                $response = rest_get_server()->dispatch( $request );
     3491
     3492                $this->assertSame( 200, $response->get_status(), 'Finalize endpoint should return 200.' );
     3493                $this->assertIsArray( $filter_metadata );
     3494                $this->assertStringContainsString( 'canola', $filter_metadata['file'], 'Expected the canola image to have been had its metadata updated.' );
     3495                $this->assertSame( $attachment_id, $filter_id, 'Expected the post ID to be passed to the filter.' );
     3496                $this->assertSame( 'update', $filter_context, 'Filter context should be "update".' );
     3497                $resulting_metadata = wp_get_attachment_metadata( $attachment_id );
     3498                $this->assertIsArray( $resulting_metadata );
     3499                $this->assertArrayHasKey( 'foo', $resulting_metadata, 'Expected new metadata key to have been added.' );
     3500                $this->assertSame( 'bar', $resulting_metadata['foo'], 'Expected filtered metadata to be updated.' );
     3501        }
     3502
     3503        /**
     3504         * Tests that the finalize endpoint requires authentication.
     3505         *
     3506         * @ticket 62243
     3507         * @covers WP_REST_Attachments_Controller::finalize_item
     3508         * @requires function imagejpeg
     3509         */
     3510        public function test_finalize_item_requires_auth(): void {
     3511                wp_set_current_user( self::$author_id );
     3512
     3513                // Create an attachment.
     3514                $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
     3515                $request->set_header( 'Content-Type', 'image/jpeg' );
     3516                $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' );
     3517                $request->set_body( (string) file_get_contents( self::$test_file ) );
     3518                $response      = rest_get_server()->dispatch( $request );
     3519                $attachment_id = $response->get_data()['id'];
     3520
     3521                // Try finalizing without authentication.
     3522                wp_set_current_user( 0 );
     3523
     3524                $request  = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment_id}/finalize" );
     3525                $response = rest_get_server()->dispatch( $request );
     3526
     3527                $this->assertErrorResponse( 'rest_cannot_edit_image', $response, 401 );
     3528        }
     3529
     3530        /**
     3531         * Tests that the finalize endpoint returns error for invalid attachment ID.
     3532         *
     3533         * @ticket 62243
     3534         * @covers WP_REST_Attachments_Controller::finalize_item
     3535         */
     3536        public function test_finalize_item_invalid_id(): void {
     3537                wp_set_current_user( self::$author_id );
     3538
     3539                $invalid_id = PHP_INT_MAX;
     3540                $this->assertNull( get_post( $invalid_id ), 'Expected invalid ID to not exist for an existing post.' );
     3541                $request  = new WP_REST_Request( 'POST', "/wp/v2/media/$invalid_id/finalize" );
     3542                $response = rest_get_server()->dispatch( $request );
     3543
     3544                $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 );
     3545        }
    34503546}
  • trunk/tests/phpunit/tests/rest-api/rest-schema-setup.php

    r61947 r61982  
    114114                        '/wp/v2/media/(?P<id>[\\d]+)/edit',
    115115                        '/wp/v2/media/(?P<id>[\\d]+)/sideload',
     116                        '/wp/v2/media/(?P<id>[\\d]+)/finalize',
    116117                        '/wp/v2/blocks',
    117118                        '/wp/v2/blocks/(?P<id>[\d]+)',
  • trunk/tests/qunit/fixtures/wp-api-generated.js

    r61943 r61982  
    37203720            ]
    37213721        },
     3722        "/wp/v2/media/(?P<id>[\\d]+)/finalize": {
     3723            "namespace": "wp/v2",
     3724            "methods": [
     3725                "POST"
     3726            ],
     3727            "endpoints": [
     3728                {
     3729                    "methods": [
     3730                        "POST"
     3731                    ],
     3732                    "args": {
     3733                        "id": {
     3734                            "description": "Unique identifier for the attachment.",
     3735                            "type": "integer",
     3736                            "required": false
     3737                        }
     3738                    }
     3739                }
     3740            ]
     3741        },
    37223742        "/wp/v2/menu-items": {
    37233743            "namespace": "wp/v2",
Note: See TracChangeset for help on using the changeset viewer.