close

Make WordPress Core

Changeset 61982


Ignore:
Timestamp:
03/12/2026 06:01:47 AM (38 hours 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.