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 Down Expand Up @@ -125,8 +154,12 @@ public function register_routes() {
'type' => 'object',
'properties' => array(
'image_size' => array(
'type' => 'string',
'required' => true,
'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',
Expand Down Expand Up @@ -2127,7 +2160,18 @@ public function sideload_item( WP_REST_Request $request ) {
'image_size' => $image_size,
);

if ( 'original' === $image_size ) {
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 );

$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 ) {
// Record the current attached file as the original.
Expand Down Expand Up @@ -2264,6 +2308,24 @@ public function finalize_item( WP_REST_Request $request ) {
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 ) {
Expand Down
83 changes: 83 additions & 0 deletions tests/phpunit/tests/rest-api/rest-attachments-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -3721,4 +3721,87 @@ public function test_finalize_preserves_image_meta(): void {
$this->assertSame( $original_image_meta['focal_length'], $metadata['image_meta']['focal_length'], 'Focal length should be preserved.' );
$this->assertSame( $original_image_meta['iso'], $metadata['image_meta']['iso'], 'ISO should be preserved.' );
}

/**
* Tests that sideloading with an array of image sizes registers the single
* file under each size name when finalized.
*
* @ticket 64737
* @covers WP_REST_Attachments_Controller::sideload_item
* @covers WP_REST_Attachments_Controller::finalize_item
* @requires function imagejpeg
*/
public function test_sideload_image_size_array() {
$this->enable_client_side_media_processing();

wp_set_current_user( self::$author_id );

// Create an attachment without generating sub-sizes server-side.
$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_param( 'generate_sub_sizes', false );
$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() );

// Sideload a single file registered under multiple sizes.
$request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment_id}/sideload" );
$request->set_header( 'Content-Type', 'image/jpeg' );
$request->set_header( 'Content-Disposition', 'attachment; filename=canola-dup.jpg' );
$request->set_param( 'image_size', array( 'thumbnail', 'medium' ) );
$request->set_body( (string) file_get_contents( self::$test_file ) );
$response = rest_get_server()->dispatch( $request );

$this->assertSame( 200, $response->get_status(), 'Sideloading with an array of sizes should succeed.' );

$sub_size = $response->get_data();
$this->assertSame( array( 'thumbnail', 'medium' ), $sub_size['image_size'], 'Response should echo the array of sizes.' );

// Finalize with the collected sub-size.
$request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment_id}/finalize" );
$request->set_param( 'sub_sizes', array( $sub_size ) );
$response = rest_get_server()->dispatch( $request );

$this->assertSame( 200, $response->get_status(), 'Finalize should succeed.' );

$metadata = wp_get_attachment_metadata( $attachment_id );
$this->assertArrayHasKey( 'thumbnail', $metadata['sizes'], 'Metadata should register the thumbnail size.' );
$this->assertArrayHasKey( 'medium', $metadata['sizes'], 'Metadata should register the medium size.' );
$this->assertSame(
$metadata['sizes']['thumbnail']['file'],
$metadata['sizes']['medium']['file'],
'Both sizes should reference the same physical file.'
);
}

/**
* Tests that the sideload endpoint rejects an invalid image size name.
*
* @ticket 64737
* @requires function imagejpeg
*/
public function test_sideload_image_size_invalid() {
$this->enable_client_side_media_processing();

wp_set_current_user( self::$author_id );

$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'];

$request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment_id}/sideload" );
$request->set_header( 'Content-Type', 'image/jpeg' );
$request->set_header( 'Content-Disposition', 'attachment; filename=canola-x.jpg' );
$request->set_param( 'image_size', array( 'thumbnail', 'not-a-real-size' ) );
$request->set_body( (string) file_get_contents( self::$test_file ) );
$response = rest_get_server()->dispatch( $request );

$this->assertSame( 400, $response->get_status(), 'An unknown size name should be rejected.' );
}
}
Loading