Finalizes an attachment after client-side media processing.
Description
Applies the sub-size metadata collected from sideload responses in a single metadata update, then 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.
Parameters
$requestWP_REST_Requestrequired- Full details about the request.
Source
public function finalize_item( WP_REST_Request $request ) {
$attachment_id = (int) $request['id'];
$post = $this->get_post( $attachment_id );
if ( is_wp_error( $post ) ) {
return $post;
}
/**
* Sub-size metadata collected from sideload responses. Confirm every
* file name was produced by a prior sideload for this attachment before
* storing it, so a client cannot make finalize record (and later read or
* delete) another attachment's files.
*
* @var list<Image_Sub_Size> $sub_sizes
*/
$sub_sizes = $request['sub_sizes'] ?? array();
$provenance = $this->validate_sub_size_provenance( $attachment_id, $sub_sizes );
if ( is_wp_error( $provenance ) ) {
return $provenance;
}
$metadata = wp_get_attachment_metadata( $attachment_id );
if ( ! is_array( $metadata ) ) {
$metadata = array();
}
// Apply all sub-size metadata collected from sideload responses.
foreach ( $sub_sizes as $sub_size ) {
$image_size = $sub_size['image_size'];
// When multiple size names share identical dimensions the client
// sends a single sub-size entry with an array of names. Register the
// same file under each name.
if ( is_array( $image_size ) ) {
/*
* Arrays carry regular sizes only, as the sideload endpoint
* enforces. Each special size names a single file handled by one
* of the branches below, so grouping one under a shared file
* would write it to the wrong place; reject rather than guess.
*/
if ( array_intersect( $image_size, self::get_special_image_sizes() ) ) {
return new WP_Error(
'rest_invalid_sub_size_name',
__( 'A grouped sub-size entry may only name regular image sizes.' ),
array( 'status' => 400 )
);
}
// As below: `file` is not required by the schema, and a size
// entry that names no file is not worth recording.
if ( empty( $sub_size['file'] ) ) {
continue;
}
$metadata['sizes'] = $metadata['sizes'] ?? array();
foreach ( $image_size as $name ) {
$metadata['sizes'][ $name ] = array(
'width' => $sub_size['width'] ?? 0,
'height' => $sub_size['height'] ?? 0,
'file' => $sub_size['file'],
'mime-type' => $sub_size['mime_type'] ?? '',
'filesize' => $sub_size['filesize'] ?? 0,
);
}
continue;
}
if ( 'original' === $image_size || 'scaled' === $image_size ) {
// Skip malformed entries so a bad payload cannot blank out the
// main file metadata.
if ( empty( $sub_size['file'] ) ) {
continue;
}
/*
* Record the supplied full-size image (from sideload_item()) as
* the main file, keeping the current attached file as
* `original_image`. A 'scaled' image is downsized and an
* 'original' image is rotated; both have any EXIF orientation
* already applied by the client.
*/
if ( ! empty( $sub_size['original_image'] ) ) {
$metadata['original_image'] = $sub_size['original_image'];
}
$metadata['width'] = $sub_size['width'] ?? 0;
$metadata['height'] = $sub_size['height'] ?? 0;
$metadata['filesize'] = $sub_size['filesize'] ?? 0;
$metadata['file'] = $sub_size['file'];
/*
* The supplied image has its orientation applied already, so
* reset the stored value (from the upload) to 1, as
* wp_create_image_subsizes() does for both its scale and rotate
* paths. Otherwise exif_orientation would still report the
* pre-rotation value and the client would rotate the image
* again on a re-fetch.
*/
if ( ! empty( $metadata['image_meta']['orientation'] ) ) {
$metadata['image_meta']['orientation'] = 1;
}
} elseif ( self::IMAGE_SIZE_SOURCE_ORIGINAL === $image_size ) {
// As above: `file` is not required by the schema, and each of
// these sizes is nothing but the file it names.
if ( empty( $sub_size['file'] ) ) {
continue;
}
/*
* Source-format original: stored under its own meta key so the
* scaled-sideload flow (which writes 'original_image') cannot
* clobber it. 'original_image' keeps pointing at the
* web-viewable JPEG derivative. Cleanup on attachment delete
* is handled by wp_delete_attachment_files().
*/
$metadata[ self::META_KEY_SOURCE_IMAGE ] = $sub_size['file'];
} elseif ( 'animated_video' === $image_size ) {
if ( empty( $sub_size['file'] ) ) {
continue;
}
/*
* Converted-video companion of an animated GIF. Stored under its
* own meta key; 'original_image' keeps pointing at the GIF. Cleanup
* on attachment delete is handled by wp_delete_attachment_files().
*/
$metadata['animated_video'] = $sub_size['file'];
} elseif ( 'animated_video_poster' === $image_size ) {
if ( empty( $sub_size['file'] ) ) {
continue;
}
// Static first-frame poster for the converted video.
$metadata['animated_video_poster'] = $sub_size['file'];
} else {
if ( empty( $sub_size['file'] ) ) {
continue;
}
$metadata['sizes'] = $metadata['sizes'] ?? array();
$metadata['sizes'][ $image_size ] = array(
'width' => $sub_size['width'] ?? 0,
'height' => $sub_size['height'] ?? 0,
'file' => $sub_size['file'],
'mime-type' => $sub_size['mime_type'] ?? '',
'filesize' => $sub_size['filesize'] ?? 0,
);
}
}
/** 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 );
/*
* Drop only the provenance rows this request consumed, now that the
* names are recorded in the metadata itself. A row is dropped only once
* its name is recoverable from the stored metadata, so a name the
* 'wp_generate_attachment_metadata' filter removed - or that a failed
* update never persisted - keeps its row and the retried request the
* endpoint documents as idempotent still validates. Rows for sideloads
* that have not been finalized yet survive for a later call, and passing
* the value makes the delete a no-op when the row is already gone, so a
* retried request cleans up without error. Any rows left behind by an
* abandoned upload are removed with the attachment itself.
*
* Retrying is idempotent for the request as it was sent. A name is only
* unavailable to a retry once a later finalize has overwritten the same
* size with a newly sideloaded file, which drops the earlier name from
* the metadata the retry recovers it from.
*
* The names are collected before deleting so a request which repeats
* the same name across many sub-sizes still issues one query per
* distinct name.
*/
$recoverable = $this->get_sideloaded_file_names( $attachment_id, false );
$consumed = array();
foreach ( $sub_sizes as $sub_size ) {
foreach ( array( 'file', 'original_image' ) as $key ) {
// Matches the set validate_sub_size_provenance() checked, so
// every name a request was allowed to store is also cleaned up.
if (
isset( $sub_size[ $key ] ) &&
is_string( $sub_size[ $key ] ) &&
in_array( $sub_size[ $key ], $recoverable, true )
) {
$consumed[] = $sub_size[ $key ];
}
}
}
foreach ( array_unique( $consumed ) as $file_name ) {
delete_post_meta( $attachment_id, self::META_KEY_SIDELOAD_FILE_NAME, wp_slash( $file_name ) );
}
$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 );
}
Hooks
- apply_filters( ‘wp_generate_attachment_metadata’,
array $metadata ,int $attachment_id ,string $context ) Filters the generated attachment meta data.
Changelog
| Version | Description |
|---|---|
| 7.1.0 | Introduced. |
User Contributed Notes
You must log in before being able to contribute a note or feedback.