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 @@ -65,14 +65,6 @@ public function register_routes() {
);

if ( wp_is_client_side_media_processing_enabled() ) {
$valid_image_sizes = array_keys( wp_get_registered_image_subsizes() );
// Special case to set 'original_image' in attachment metadata.
$valid_image_sizes[] = 'original';
// Used for PDF thumbnails.
$valid_image_sizes[] = 'full';
// Client-side big image threshold: sideload the scaled version.
$valid_image_sizes[] = 'scaled';

register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\d]+)/sideload',
Expand All @@ -87,10 +79,47 @@ public function register_routes() {
'type' => 'integer',
),
'image_size' => array(
'description' => __( 'Image size.' ),
'type' => 'string',
'enum' => $valid_image_sizes,
'required' => true,
'description' => __( 'Image size. Can be a single size name or an array of size names to register the same file under multiple sizes.' ),
'type' => array( 'string', 'array' ),
'items' => array(
'type' => 'string',
),
'required' => true,
/*
* A custom callback is used instead of the default enum validation
* because rest_is_array() treats scalar strings as single-element
* lists (via wp_parse_list()), so a [ 'string', 'array' ] type alone
* cannot enforce the enum. The callback validates each item against
* the current list of registered sizes, which reflects sizes added
* after route registration (e.g. via add_image_size()).
*/
'validate_callback' => static function ( $value, $request, $param ) {
$valid_sizes = array_keys( wp_get_registered_image_subsizes() );
$valid_sizes[] = 'original';
$valid_sizes[] = 'scaled';
$valid_sizes[] = 'full';

$items = is_string( $value ) ? array( $value ) : ( is_array( $value ) ? $value : null );
if ( null === $items ) {
return new WP_Error(
'rest_invalid_type',
/* translators: %s: Parameter name. */
sprintf( __( '%s must be a string or an array of strings.' ), $param )
);
}

foreach ( $items as $item ) {
if ( ! is_string( $item ) || ! in_array( $item, $valid_sizes, true ) ) {
return new WP_Error(
'rest_not_in_enum',
/* translators: %s: Parameter name. */
sprintf( __( '%s contains an invalid image size.' ), $param )
);
}
}

return true;
},
),
'convert_format' => array(
'type' => 'boolean',
Expand All @@ -113,10 +142,50 @@ public function register_routes() {
'callback' => array( $this, 'finalize_item' ),
'permission_callback' => array( $this, 'edit_media_item_permissions_check' ),
'args' => array(
'id' => array(
'id' => array(
'description' => __( 'Unique identifier for the attachment.' ),
'type' => 'integer',
),
'sub_sizes' => array(
'description' => __( 'Array of sub-size metadata collected from sideload responses.' ),
'type' => 'array',
'default' => array(),
'items' => array(
'type' => 'object',
'properties' => array(
'image_size' => array(
'description' => __( 'Size name, or an array of size names when a single file is registered under multiple sizes with matching dimensions.' ),
'type' => array( 'string', 'array' ),
'items' => array(
'type' => 'string',
),
'required' => true,
),
'width' => array(
'type' => 'integer',
'minimum' => 1,
),
'height' => array(
'type' => 'integer',
'minimum' => 1,
),
'file' => array(
'type' => 'string',
),
'mime_type' => array(
'type' => 'string',
'pattern' => '^image/.*',
),
'filesize' => array(
'type' => 'integer',
'minimum' => 1,
),
'original_image' => array(
'type' => 'string',
),
),
),
),
),
),
'allow_batch' => $this->allow_batch,
Expand Down Expand Up @@ -2082,16 +2151,30 @@ public function sideload_item( WP_REST_Request $request ) {

$image_size = $request['image_size'];

$metadata = wp_get_attachment_metadata( $attachment_id, true );
// Build sub-size data to return to the client.
// The client accumulates these and sends them all to the finalize
// endpoint, which writes the metadata in a single operation. This
// avoids the read-modify-write race that concurrent sideloads for the
// same attachment would otherwise hit.
$sub_size_data = array(
'image_size' => $image_size,
);

if ( ! $metadata ) {
$metadata = array();
}
if ( is_array( $image_size ) ) {
// Multiple registered sizes share these dimensions, so a single
// sideloaded file is reused for all of them. Arrays only carry
// regular sub-sizes; the special keys below are always scalar.
$size = wp_getimagesize( $path );

if ( 'original' === $image_size ) {
$metadata['original_image'] = wp_basename( $path );
$sub_size_data['width'] = $size ? $size[0] : 0;
$sub_size_data['height'] = $size ? $size[1] : 0;
$sub_size_data['file'] = wp_basename( $path );
$sub_size_data['mime_type'] = $type;
$sub_size_data['filesize'] = wp_filesize( $path );
} elseif ( 'original' === $image_size ) {
$sub_size_data['file'] = wp_basename( $path );
} elseif ( 'scaled' === $image_size ) {
// The current attached file is the original; record it as original_image.
// Record the current attached file as the original.
$current_file = get_attached_file( $attachment_id, true );

if ( ! $current_file ) {
Expand All @@ -2102,7 +2185,7 @@ public function sideload_item( WP_REST_Request $request ) {
);
}

$metadata['original_image'] = wp_basename( $current_file );
$sub_size_data['original_image'] = wp_basename( $current_file );

// Validate the scaled image before updating the attached file.
$size = wp_getimagesize( $path );
Expand All @@ -2117,6 +2200,7 @@ public function sideload_item( WP_REST_Request $request ) {
}

// Update the attached file to point to the scaled version.
// This writes to _wp_attached_file meta, not _wp_attachment_metadata.
if (
get_attached_file( $attachment_id, true ) !== $path &&
! update_attached_file( $attachment_id, $path )
Expand All @@ -2128,42 +2212,21 @@ public function sideload_item( WP_REST_Request $request ) {
);
}

$metadata['width'] = $size[0];
$metadata['height'] = $size[1];
$metadata['filesize'] = $filesize;
$metadata['file'] = _wp_relative_upload_path( $path );
$sub_size_data['width'] = $size[0];
$sub_size_data['height'] = $size[1];
$sub_size_data['filesize'] = $filesize;
$sub_size_data['file'] = _wp_relative_upload_path( $path );
} else {
$metadata['sizes'] = $metadata['sizes'] ?? array();

$size = wp_getimagesize( $path );

$metadata['sizes'][ $image_size ] = array(
'width' => $size ? $size[0] : 0,
'height' => $size ? $size[1] : 0,
'file' => wp_basename( $path ),
'mime-type' => $type,
'filesize' => wp_filesize( $path ),
);
}

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'];
$sub_size_data['width'] = $size ? $size[0] : 0;
$sub_size_data['height'] = $size ? $size[1] : 0;
$sub_size_data['file'] = wp_basename( $path );
$sub_size_data['mime_type'] = $type;
$sub_size_data['filesize'] = wp_filesize( $path );
}

$response = $this->prepare_item_for_response( get_post( $attachment_id ), $response_request );

$response->header( 'Location', rest_url( rest_get_route_for_post( $attachment_id ) ) );

return $response;
return rest_ensure_response( $sub_size_data );
}

/**
Expand Down Expand Up @@ -2215,9 +2278,11 @@ private static function filter_wp_unique_filename( $filename, $dir, $number, $at
/**
* 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.
* 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.
*
* @since 7.1.0
*
Expand All @@ -2237,6 +2302,53 @@ public function finalize_item( WP_REST_Request $request ) {
$metadata = array();
}

// Apply all sub-size metadata collected from sideload responses.
$sub_sizes = $request['sub_sizes'] ?? array();

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. Arrays only contain regular sizes.
if ( is_array( $image_size ) ) {
$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 ) {
$metadata['original_image'] = $sub_size['file'];
} elseif ( 'scaled' === $image_size ) {
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'] ?? '';
} else {
$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' );

Expand Down
Loading
Loading