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 @@ -1603,24 +1603,12 @@ public function prepare_item_for_response( $item, $request ) {
$mime_type = (string) get_post_mime_type( $post );

/*
* Per-file output format for images, evaluated with the real filename
* and MIME type so plugins filtering image_editor_output_format can
* make per-attachment decisions (e.g. JPEG -> WebP). Resolved the same
* way WP_Image_Editor::set_quality() resolves the output format.
* Per-file output format for images, so plugins filtering
* image_editor_output_format can make per-attachment decisions
* (e.g. JPEG -> WebP).
*/
if ( in_array( 'image_output_format', $fields, true ) ) {
$filename = get_attached_file( $post->ID );

/** This filter is documented in wp-includes/media.php */
$output_formats = apply_filters(
'image_editor_output_format',
array( $mime_type => $mime_type ),
$filename ? $filename : '',
$mime_type
);

$output_mime = $output_formats[ $mime_type ] ?? $mime_type;
$data['image_output_format'] = ( $output_mime !== $mime_type ) ? $output_mime : null;
$data['image_output_format'] = $this->get_image_output_format( $post );
}

/*
Expand Down Expand Up @@ -2783,6 +2771,34 @@ private function validate_image_dimensions( int $width, int $height, string $ima
return true;
}

/**
* Gets the MIME type an image attachment's sub-sizes are generated in.
*
* Resolved the same way WP_Image_Editor::set_quality() resolves the output
* format, with the attachment's real file name and MIME type.
*
* @since 7.2.0
*
* @param WP_Post $post Image attachment post object.
* @return string|null The output MIME type, or null if it matches the attachment's own.
*/
private function get_image_output_format( WP_Post $post ): ?string {
$mime_type = (string) get_post_mime_type( $post );
$filename = get_attached_file( $post->ID );

/** This filter is documented in wp-includes/media.php */
$output_formats = apply_filters(
'image_editor_output_format',
array( $mime_type => $mime_type ),
$filename ? $filename : '',
$mime_type
);

$output_mime = is_array( $output_formats ) ? ( $output_formats[ $mime_type ] ?? $mime_type ) : $mime_type;

return ( is_string( $output_mime ) && $output_mime !== $mime_type ) ? $output_mime : null;
}

/**
* Checks whether a dimension exceeds the maximum allowed value.
*
Expand Down Expand Up @@ -2847,6 +2863,25 @@ public function sideload_item( WP_REST_Request $request ) {
);
}

/*
* The server writes sub-sizes in the attachment's output format without
* checking it against the network's allowed upload file types, so a
* sideload in that format is exempt from them too. Resolved before the
* output format filter below is added.
*/
$output_format = wp_attachment_is_image( $post ) ? $this->get_image_output_format( $post ) : null;
$output_extensions = null !== $output_format ? array_search( $output_format, wp_get_mime_types(), true ) : false;
$filter_filetypes = null;

if ( is_string( $output_extensions ) ) {
$filter_filetypes = static function ( $filetypes ) use ( $output_extensions ) {
return is_string( $filetypes ) ? $filetypes . ' ' . str_replace( '|', ' ', $output_extensions ) : $filetypes;
};

add_filter( 'site_option_upload_filetypes', $filter_filetypes );
add_filter( 'default_site_option_upload_filetypes', $filter_filetypes );
}

if ( false === $request['convert_format'] ) {
// Prevent image conversion as that is done client-side.
add_filter( 'image_editor_output_format', '__return_empty_array', 100 );
Expand Down Expand Up @@ -2898,6 +2933,11 @@ public function sideload_item( WP_REST_Request $request ) {
remove_filter( 'image_editor_output_format', '__return_empty_array', 100 );
remove_filter( 'upload_dir', $filter_upload_dir, 100 );

if ( null !== $filter_filetypes ) {
remove_filter( 'site_option_upload_filetypes', $filter_filetypes );
remove_filter( 'default_site_option_upload_filetypes', $filter_filetypes );
}

if ( is_wp_error( $file ) ) {
return $file;
}
Expand Down
143 changes: 143 additions & 0 deletions tests/phpunit/tests/rest-api/rest-attachments-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -2854,6 +2854,149 @@ public function test_create_item_with_data_exceeds_multisite_site_upload_space()
$this->assertErrorResponse( 'rest_upload_limited_space', $response, 400 );
}

/**
* Tests that sub-sizes generated on the server are not limited to the
* network's allowed upload file types.
*
* The image editor writes sub-sizes straight to disk, so a network that
* does not allow WebP uploads still gets WebP sub-sizes when an
* `image_editor_output_format` filter asks for them. This is the
* baseline for test_sideload_sub_size_in_output_format_not_allowed_by_network().
*
* @ticket 66681
*
* @group multisite
* @group ms-required
* @covers ::wp_generate_attachment_metadata
*/
public function test_server_sub_sizes_in_output_format_not_allowed_by_network(): void {
if ( ! wp_image_editor_supports( array( 'mime_type' => 'image/webp' ) ) ) {
$this->markTestSkipped( 'This test requires an image editor that can save WebP.' );
}

/*
* Networks created before WordPress 5.8 have no `webp` in this list,
* and upgrades never add it. This is also the list
* check_upload_mimes() falls back to.
*/
update_site_option( 'upload_filetypes', 'jpg jpeg png gif' );
$this->assertNotContains( 'image/webp', get_allowed_mime_types(), 'The network should not allow WebP uploads.' );

add_filter( 'image_editor_output_format', array( $this, 'filter_output_format_jpeg_to_webp' ) );

$attachment_id = self::factory()->attachment->create_upload_object( DIR_TESTDATA . '/images/canola.jpg' );
$this->assertIsInt( $attachment_id );

$metadata = wp_get_attachment_metadata( $attachment_id );
$this->assertIsArray( $metadata );
$this->assertArrayHasKey( 'sizes', $metadata );

$this->assertArrayHasKey( 'thumbnail', $metadata['sizes'], 'The server should generate a thumbnail.' );
$this->assertSame( 'image/webp', $metadata['sizes']['thumbnail']['mime-type'], 'The thumbnail should be a WebP.' );
}

/**
* Tests that a client-generated sub-size can be sideloaded in an output
* format the network does not allow for upload.
*
* With client-side media processing, the browser transcodes each sub-size
* to the `image_output_format` returned by the upload, then sideloads it.
* The server generates the same sub-size itself on the classic path (see
* test_server_sub_sizes_in_output_format_not_allowed_by_network()), so the
* sideload should not be rejected for its file type.
*
* @ticket 66681
*
* @group multisite
* @group ms-required
* @covers WP_REST_Attachments_Controller::sideload_item
*/
public function test_sideload_sub_size_in_output_format_not_allowed_by_network(): void {
$this->enable_client_side_media_processing();

wp_set_current_user( self::$author_id );

// See test_server_sub_sizes_in_output_format_not_allowed_by_network().
update_site_option( 'upload_filetypes', 'jpg jpeg png gif' );
$this->assertNotContains( 'image/webp', get_allowed_mime_types(), 'The network should not allow WebP uploads.' );

add_filter( 'image_editor_output_format', array( $this, 'filter_output_format_jpeg_to_webp' ) );

$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 );

$this->assertSame( 201, $response->get_status(), 'The original upload should succeed.' );

$data = $response->get_data();
$this->assertIsArray( $data );
$this->assertIsInt( $data['id'] );

$attachment_id = $data['id'];

// A WebP thumbnail, as the client would sideload it.
$request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment_id}/sideload" );
$request->set_header( 'Content-Type', 'image/webp' );
$request->set_header( 'Content-Disposition', 'attachment; filename=canola-50x50.webp' );
$request->set_param( 'image_size', 'thumbnail' );
$request->set_param( 'convert_format', false );
$request->set_body( (string) file_get_contents( DIR_TESTDATA . '/images/test-image.webp' ) );
$response = rest_get_server()->dispatch( $request );

$this->assertSame(
200,
$response->get_status(),
'Sideloading a WebP sub-size should succeed. Response: ' . wp_json_encode( $response->get_data() )
);
}

/**
* Tests that a sideload is still limited to the network's allowed upload
* file types when it is not in the attachment's output format.
*
* @ticket 66681
*
* @group multisite
* @group ms-required
* @covers WP_REST_Attachments_Controller::sideload_item
*/
public function test_sideload_sub_size_not_in_output_format_not_allowed_by_network(): void {
$this->enable_client_side_media_processing();

wp_set_current_user( self::$author_id );

update_site_option( 'upload_filetypes', 'jpg jpeg png gif' );

$attachment_id = self::factory()->attachment->create_upload_object( self::$test_file );
$this->assertIsInt( $attachment_id );

// No output format filter, so the sub-sizes of a JPEG are JPEGs.
$request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment_id}/sideload" );
$request->set_header( 'Content-Type', 'image/webp' );
$request->set_header( 'Content-Disposition', 'attachment; filename=canola-50x50.webp' );
$request->set_param( 'image_size', 'thumbnail' );
$request->set_param( 'convert_format', false );
$request->set_body( (string) file_get_contents( DIR_TESTDATA . '/images/test-image.webp' ) );
$response = rest_get_server()->dispatch( $request );

$this->assertErrorResponse( 'rest_upload_sideload_error', $response, 500 );
$this->assertNotContains( 'image/webp', get_allowed_mime_types(), 'The network should still not allow WebP uploads.' );
}

/**
* Maps JPEG output to WebP, as image format plugins do.
*
* @param array<string, string> $formats Output formats keyed by source mime type.
* @return array<string, string> The filtered output formats.
*/
public function filter_output_format_jpeg_to_webp( $formats ) {
$formats['image/jpeg'] = 'image/webp';
return $formats;
}

/**
* Ensure the `rest_after_insert_attachment` and `rest_insert_attachment` hooks only fire
* once when attachments are created.
Expand Down
Loading