Skip to content

Commit f7a8413

Browse files
REST API: Support registering one sideloaded file under multiple image sizes.
When multiple registered sizes resolve to identical dimensions (`width`, `height`, `crop`), the client can now group them, upload one file, and reference it from every matching size name instead of encoding, transferring, and storing duplicates. The sideload endpoint's `image_size` parameter and the finalize endpoint's `sub_sizes[].image_size` now accept either a string or an array of strings. See related Gutenberg work: WordPress/gutenberg#77036. Props adamsilverstein, westonruter, swissspidy, sachinrajcp123, sanayasir. Fixes #65481. git-svn-id: https://develop.svn.wordpress.org/trunk@62609 602fd350-edb4-49c9-b593-d223f7449a82
1 parent f5ee2b2 commit f7a8413

3 files changed

Lines changed: 512 additions & 74 deletions

File tree

src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php

Lines changed: 167 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -65,14 +65,6 @@ public function register_routes() {
6565
);
6666

6767
if ( wp_is_client_side_media_processing_enabled() ) {
68-
$valid_image_sizes = array_keys( wp_get_registered_image_subsizes() );
69-
// Special case to set 'original_image' in attachment metadata.
70-
$valid_image_sizes[] = 'original';
71-
// Used for PDF thumbnails.
72-
$valid_image_sizes[] = 'full';
73-
// Client-side big image threshold: sideload the scaled version.
74-
$valid_image_sizes[] = 'scaled';
75-
7668
register_rest_route(
7769
$this->namespace,
7870
'/' . $this->rest_base . '/(?P<id>[\d]+)/sideload',
@@ -87,10 +79,47 @@ public function register_routes() {
8779
'type' => 'integer',
8880
),
8981
'image_size' => array(
90-
'description' => __( 'Image size.' ),
91-
'type' => 'string',
92-
'enum' => $valid_image_sizes,
93-
'required' => true,
82+
'description' => __( 'Image size. Can be a single size name or an array of size names to register the same file under multiple sizes.' ),
83+
'type' => array( 'string', 'array' ),
84+
'items' => array(
85+
'type' => 'string',
86+
),
87+
'required' => true,
88+
/*
89+
* A custom callback is used instead of the default enum validation
90+
* because rest_is_array() treats scalar strings as single-element
91+
* lists (via wp_parse_list()), so a [ 'string', 'array' ] type alone
92+
* cannot enforce the enum. The callback validates each item against
93+
* the current list of registered sizes, which reflects sizes added
94+
* after route registration (e.g. via add_image_size()).
95+
*/
96+
'validate_callback' => static function ( $value, $request, $param ) {
97+
$valid_sizes = array_keys( wp_get_registered_image_subsizes() );
98+
$valid_sizes[] = 'original';
99+
$valid_sizes[] = 'scaled';
100+
$valid_sizes[] = 'full';
101+
102+
$items = is_string( $value ) ? array( $value ) : ( is_array( $value ) ? $value : null );
103+
if ( null === $items ) {
104+
return new WP_Error(
105+
'rest_invalid_type',
106+
/* translators: %s: Parameter name. */
107+
sprintf( __( '%s must be a string or an array of strings.' ), $param )
108+
);
109+
}
110+
111+
foreach ( $items as $item ) {
112+
if ( ! is_string( $item ) || ! in_array( $item, $valid_sizes, true ) ) {
113+
return new WP_Error(
114+
'rest_not_in_enum',
115+
/* translators: %s: Parameter name. */
116+
sprintf( __( '%s contains an invalid image size.' ), $param )
117+
);
118+
}
119+
}
120+
121+
return true;
122+
},
94123
),
95124
'convert_format' => array(
96125
'type' => 'boolean',
@@ -113,10 +142,50 @@ public function register_routes() {
113142
'callback' => array( $this, 'finalize_item' ),
114143
'permission_callback' => array( $this, 'edit_media_item_permissions_check' ),
115144
'args' => array(
116-
'id' => array(
145+
'id' => array(
117146
'description' => __( 'Unique identifier for the attachment.' ),
118147
'type' => 'integer',
119148
),
149+
'sub_sizes' => array(
150+
'description' => __( 'Array of sub-size metadata collected from sideload responses.' ),
151+
'type' => 'array',
152+
'default' => array(),
153+
'items' => array(
154+
'type' => 'object',
155+
'properties' => array(
156+
'image_size' => array(
157+
'description' => __( 'Size name, or an array of size names when a single file is registered under multiple sizes with matching dimensions.' ),
158+
'type' => array( 'string', 'array' ),
159+
'items' => array(
160+
'type' => 'string',
161+
),
162+
'required' => true,
163+
),
164+
'width' => array(
165+
'type' => 'integer',
166+
'minimum' => 1,
167+
),
168+
'height' => array(
169+
'type' => 'integer',
170+
'minimum' => 1,
171+
),
172+
'file' => array(
173+
'type' => 'string',
174+
),
175+
'mime_type' => array(
176+
'type' => 'string',
177+
'pattern' => '^image/.*',
178+
),
179+
'filesize' => array(
180+
'type' => 'integer',
181+
'minimum' => 1,
182+
),
183+
'original_image' => array(
184+
'type' => 'string',
185+
),
186+
),
187+
),
188+
),
120189
),
121190
),
122191
'allow_batch' => $this->allow_batch,
@@ -2082,16 +2151,30 @@ public function sideload_item( WP_REST_Request $request ) {
20822151

20832152
$image_size = $request['image_size'];
20842153

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

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

2091-
if ( 'original' === $image_size ) {
2092-
$metadata['original_image'] = wp_basename( $path );
2169+
$sub_size_data['width'] = $size ? $size[0] : 0;
2170+
$sub_size_data['height'] = $size ? $size[1] : 0;
2171+
$sub_size_data['file'] = wp_basename( $path );
2172+
$sub_size_data['mime_type'] = $type;
2173+
$sub_size_data['filesize'] = wp_filesize( $path );
2174+
} elseif ( 'original' === $image_size ) {
2175+
$sub_size_data['file'] = wp_basename( $path );
20932176
} elseif ( 'scaled' === $image_size ) {
2094-
// The current attached file is the original; record it as original_image.
2177+
// Record the current attached file as the original.
20952178
$current_file = get_attached_file( $attachment_id, true );
20962179

20972180
if ( ! $current_file ) {
@@ -2102,7 +2185,7 @@ public function sideload_item( WP_REST_Request $request ) {
21022185
);
21032186
}
21042187

2105-
$metadata['original_image'] = wp_basename( $current_file );
2188+
$sub_size_data['original_image'] = wp_basename( $current_file );
21062189

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

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

2131-
$metadata['width'] = $size[0];
2132-
$metadata['height'] = $size[1];
2133-
$metadata['filesize'] = $filesize;
2134-
$metadata['file'] = _wp_relative_upload_path( $path );
2215+
$sub_size_data['width'] = $size[0];
2216+
$sub_size_data['height'] = $size[1];
2217+
$sub_size_data['filesize'] = $filesize;
2218+
$sub_size_data['file'] = _wp_relative_upload_path( $path );
21352219
} else {
2136-
$metadata['sizes'] = $metadata['sizes'] ?? array();
2137-
21382220
$size = wp_getimagesize( $path );
21392221

2140-
$metadata['sizes'][ $image_size ] = array(
2141-
'width' => $size ? $size[0] : 0,
2142-
'height' => $size ? $size[1] : 0,
2143-
'file' => wp_basename( $path ),
2144-
'mime-type' => $type,
2145-
'filesize' => wp_filesize( $path ),
2146-
);
2147-
}
2148-
2149-
wp_update_attachment_metadata( $attachment_id, $metadata );
2150-
2151-
$response_request = new WP_REST_Request(
2152-
WP_REST_Server::READABLE,
2153-
rest_get_route_for_post( $attachment_id )
2154-
);
2155-
2156-
$response_request['context'] = 'edit';
2157-
2158-
if ( isset( $request['_fields'] ) ) {
2159-
$response_request['_fields'] = $request['_fields'];
2222+
$sub_size_data['width'] = $size ? $size[0] : 0;
2223+
$sub_size_data['height'] = $size ? $size[1] : 0;
2224+
$sub_size_data['file'] = wp_basename( $path );
2225+
$sub_size_data['mime_type'] = $type;
2226+
$sub_size_data['filesize'] = wp_filesize( $path );
21602227
}
21612228

2162-
$response = $this->prepare_item_for_response( get_post( $attachment_id ), $response_request );
2163-
2164-
$response->header( 'Location', rest_url( rest_get_route_for_post( $attachment_id ) ) );
2165-
2166-
return $response;
2229+
return rest_ensure_response( $sub_size_data );
21672230
}
21682231

21692232
/**
@@ -2215,9 +2278,11 @@ private static function filter_wp_unique_filename( $filename, $dir, $number, $at
22152278
/**
22162279
* Finalizes an attachment after client-side media processing.
22172280
*
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.
2281+
* Applies the sub-size metadata collected from sideload responses in a
2282+
* single metadata update, then triggers the 'wp_generate_attachment_metadata'
2283+
* filter so that server-side plugins can process the attachment after all
2284+
* client-side operations (upload, thumbnail generation, sideloads) are
2285+
* complete.
22212286
*
22222287
* @since 7.1.0
22232288
*
@@ -2237,6 +2302,53 @@ public function finalize_item( WP_REST_Request $request ) {
22372302
$metadata = array();
22382303
}
22392304

2305+
// Apply all sub-size metadata collected from sideload responses.
2306+
$sub_sizes = $request['sub_sizes'] ?? array();
2307+
2308+
foreach ( $sub_sizes as $sub_size ) {
2309+
$image_size = $sub_size['image_size'];
2310+
2311+
// When multiple size names share identical dimensions the client
2312+
// sends a single sub-size entry with an array of names. Register the
2313+
// same file under each name. Arrays only contain regular sizes.
2314+
if ( is_array( $image_size ) ) {
2315+
$metadata['sizes'] = $metadata['sizes'] ?? array();
2316+
2317+
foreach ( $image_size as $name ) {
2318+
$metadata['sizes'][ $name ] = array(
2319+
'width' => $sub_size['width'] ?? 0,
2320+
'height' => $sub_size['height'] ?? 0,
2321+
'file' => $sub_size['file'] ?? '',
2322+
'mime-type' => $sub_size['mime_type'] ?? '',
2323+
'filesize' => $sub_size['filesize'] ?? 0,
2324+
);
2325+
}
2326+
continue;
2327+
}
2328+
2329+
if ( 'original' === $image_size ) {
2330+
$metadata['original_image'] = $sub_size['file'];
2331+
} elseif ( 'scaled' === $image_size ) {
2332+
if ( ! empty( $sub_size['original_image'] ) ) {
2333+
$metadata['original_image'] = $sub_size['original_image'];
2334+
}
2335+
$metadata['width'] = $sub_size['width'] ?? 0;
2336+
$metadata['height'] = $sub_size['height'] ?? 0;
2337+
$metadata['filesize'] = $sub_size['filesize'] ?? 0;
2338+
$metadata['file'] = $sub_size['file'] ?? '';
2339+
} else {
2340+
$metadata['sizes'] = $metadata['sizes'] ?? array();
2341+
2342+
$metadata['sizes'][ $image_size ] = array(
2343+
'width' => $sub_size['width'] ?? 0,
2344+
'height' => $sub_size['height'] ?? 0,
2345+
'file' => $sub_size['file'] ?? '',
2346+
'mime-type' => $sub_size['mime_type'] ?? '',
2347+
'filesize' => $sub_size['filesize'] ?? 0,
2348+
);
2349+
}
2350+
}
2351+
22402352
/** This filter is documented in wp-admin/includes/image.php */
22412353
$metadata = apply_filters( 'wp_generate_attachment_metadata', $metadata, $attachment_id, 'update' );
22422354

0 commit comments

Comments
 (0)