Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,26 @@ public function register_routes() {
'schema' => array( $this, 'get_public_item_schema' ),
)
);

register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\d]+)/finalize',
array(
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'finalize_item' ),
'permission_callback' => array( $this, 'finalize_item_permissions_check' ),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shell we use edit_media_item_permissions_check directly as the new method class that function and return the value?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like the wrapper approach is pretty standard here? or maybe I'm not understanding your suggestion completely?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes but

public function sideload_item_permissions_check( $request ) {
return $this->edit_media_item_permissions_check( $request );
}
use wrapper method.

Can we use single method for both?

'args' => array(
'id' => array(
'description' => __( 'Unique identifier for the attachment.' ),
'type' => 'integer',
),
),
),
'allow_batch' => $this->allow_batch,
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
}

Expand Down Expand Up @@ -2176,4 +2196,60 @@ private static function filter_wp_unique_filename( $filename, $dir, $number, $at

return $filename;
}

/**
* Checks if a given request has access to finalize an attachment.
*
* @since 7.0.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access, WP_Error object otherwise.
*/
public function finalize_item_permissions_check( WP_REST_Request $request ) {
return $this->edit_media_item_permissions_check( $request );
}

/**
* Finalizes an attachment after client-side media processing.
*
* Triggers the 'wp_generate_attachment_metadata' filter so that
* server-side plugins can process the attachment after all client-side
* operations (upload, thumbnail generation, sideloads) are complete.
*
* @since 7.0.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure.
*/
public function finalize_item( WP_REST_Request $request ) {
$attachment_id = $request['id'];

$post = $this->get_post( $attachment_id );
if ( is_wp_error( $post ) ) {
return $post;
}

$metadata = wp_get_attachment_metadata( $attachment_id );
if ( ! is_array( $metadata ) ) {
$metadata = array();
}

/** This filter is documented in wp-admin/includes/image.php */
$metadata = apply_filters( 'wp_generate_attachment_metadata', $metadata, $attachment_id, 'update' );

wp_update_attachment_metadata( $attachment_id, $metadata );

$response_request = new WP_REST_Request(
WP_REST_Server::READABLE,
rest_get_route_for_post( $attachment_id )
);

$response_request['context'] = 'edit';

if ( isset( $request['_fields'] ) ) {
$response_request['_fields'] = $request['_fields'];
}

return $this->prepare_item_for_response( $post, $response_request );
}
}
96 changes: 96 additions & 0 deletions tests/phpunit/tests/rest-api/rest-attachments-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -3343,4 +3343,100 @@ public function test_sideload_scaled_unique_filename_conflict() {
$basename = wp_basename( $new_file );
$this->assertMatchesRegularExpression( '/canola-scaled-\d+\.jpg$/', $basename, 'Scaled filename should have numeric suffix when file conflicts with a different attachment.' );
}

/**
* Tests that the finalize endpoint triggers wp_generate_attachment_metadata.
*
* @ticket 62243
* @covers WP_REST_Attachments_Controller::finalize_item
* @requires function imagejpeg
*/
public function test_finalize_item(): void {
wp_set_current_user( self::$author_id );

// Create an attachment.
$request = new WP_REST_Request( 'POST', '/wp/v2/media' );
$request->set_header( 'Content-Type', 'image/jpeg' );
$request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' );
$request->set_body( (string) file_get_contents( self::$test_file ) );
$response = rest_get_server()->dispatch( $request );
$attachment_id = $response->get_data()['id'];

$this->assertSame( 201, $response->get_status() );

// Track whether wp_generate_attachment_metadata filter fires.
$filter_metadata = null;
$filter_id = null;
$filter_context = null;
add_filter(
'wp_generate_attachment_metadata',
function ( array $metadata, int $id, string $context ) use ( &$filter_metadata, &$filter_id, &$filter_context ) {
$filter_metadata = $metadata;
$filter_id = $id;
$filter_context = $context;
$metadata['foo'] = 'bar';
return $metadata;
},
10,
3
);

// Call the finalize endpoint.
$request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment_id}/finalize" );
$response = rest_get_server()->dispatch( $request );

$this->assertSame( 200, $response->get_status(), 'Finalize endpoint should return 200.' );
$this->assertIsArray( $filter_metadata );
$this->assertStringContainsString( 'canola', $filter_metadata['file'], 'Expected the canola image to have been had its metadata updated.' );
$this->assertSame( $attachment_id, $filter_id, 'Expected the post ID to be passed to the filter.' );
$this->assertSame( 'update', $filter_context, 'Filter context should be "update".' );
$resulting_metadata = wp_get_attachment_metadata( $attachment_id );
$this->assertIsArray( $resulting_metadata );
$this->assertArrayHasKey( 'foo', $resulting_metadata, 'Expected new metadata key to have been added.' );
$this->assertSame( 'bar', $resulting_metadata['foo'], 'Expected filtered metadata to be updated.' );
}

/**
* Tests that the finalize endpoint requires authentication.
*
* @ticket 62243
* @covers WP_REST_Attachments_Controller::finalize_item
* @requires function imagejpeg
*/
public function test_finalize_item_requires_auth(): void {
wp_set_current_user( self::$author_id );

// Create an attachment.
$request = new WP_REST_Request( 'POST', '/wp/v2/media' );
$request->set_header( 'Content-Type', 'image/jpeg' );
$request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' );
$request->set_body( (string) file_get_contents( self::$test_file ) );
$response = rest_get_server()->dispatch( $request );
$attachment_id = $response->get_data()['id'];

// Try finalizing without authentication.
wp_set_current_user( 0 );

$request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment_id}/finalize" );
$response = rest_get_server()->dispatch( $request );

$this->assertErrorResponse( 'rest_cannot_edit_image', $response, 401 );
}

/**
* Tests that the finalize endpoint returns error for invalid attachment ID.
*
* @ticket 62243
* @covers WP_REST_Attachments_Controller::finalize_item
*/
public function test_finalize_item_invalid_id(): void {
wp_set_current_user( self::$author_id );

$invalid_id = PHP_INT_MAX;
$this->assertNull( get_post( $invalid_id ), 'Expected invalid ID to not exist for an existing post.' );
$request = new WP_REST_Request( 'POST', "/wp/v2/media/$invalid_id/finalize" );
$response = rest_get_server()->dispatch( $request );

$this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 );
}
}
1 change: 1 addition & 0 deletions tests/phpunit/tests/rest-api/rest-schema-setup.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ public function test_expected_routes_in_schema() {
'/wp/v2/media/(?P<id>[\\d]+)/post-process',
'/wp/v2/media/(?P<id>[\\d]+)/edit',
'/wp/v2/media/(?P<id>[\\d]+)/sideload',
'/wp/v2/media/(?P<id>[\\d]+)/finalize',
'/wp/v2/blocks',
'/wp/v2/blocks/(?P<id>[\d]+)',
'/wp/v2/blocks/(?P<id>[\d]+)/autosaves',
Expand Down
20 changes: 20 additions & 0 deletions tests/qunit/fixtures/wp-api-generated.js
Original file line number Diff line number Diff line change
Expand Up @@ -3718,6 +3718,26 @@ mockedApiResponse.Schema = {
}
]
},
"/wp/v2/media/(?P<id>[\\d]+)/finalize": {
"namespace": "wp/v2",
"methods": [
"POST"
],
"endpoints": [
{
"methods": [
"POST"
],
"args": {
"id": {
"description": "Unique identifier for the attachment.",
"type": "integer",
"required": false
}
}
}
]
},
"/wp/v2/menu-items": {
"namespace": "wp/v2",
"methods": [
Expand Down
Loading