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 @@ -34,6 +34,17 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
*/
protected $allow_batch = false;

/**
* Whether check_upload_size() skips the multisite upload space checks.
*
* Set while a file is sideloaded for an existing attachment. The maximum
* upload file size still applies.
*
* @since 7.2.0
* @var bool
*/
private bool $skip_upload_space_check = false;

/**
* Image size token for the source-format original preserved alongside a
* client-generated derivative (e.g. the HEIC file kept next to its JPEG).
Expand Down Expand Up @@ -2284,7 +2295,7 @@ protected function check_upload_size( $file ) {

$file_size = filesize( $file['tmp_name'] );

if ( $space_left < $file_size ) {
if ( ! $this->skip_upload_space_check && $space_left < $file_size ) {
return new WP_Error(
'rest_upload_limited_space',
/* translators: %s: Required disk space in kilobytes. */
Expand All @@ -2305,7 +2316,7 @@ protected function check_upload_size( $file ) {
// Include multisite admin functions to get access to upload_is_user_over_quota().
require_once ABSPATH . 'wp-admin/includes/ms.php';

if ( upload_is_user_over_quota( false ) ) {
if ( ! $this->skip_upload_space_check && upload_is_user_over_quota( false ) ) {
return new WP_Error(
'rest_upload_user_quota_exceeded',
__( 'You have used your space quota. Please delete files before uploading.' ),
Expand Down Expand Up @@ -2803,6 +2814,8 @@ private function dimension_exceeds_max( int $value, int $max, int $tolerance ):
* Side-loads a media file without creating a new attachment.
*
* @since 7.1.0
* @since 7.2.0 The file is no longer checked against the multisite upload space.
* The maximum upload file size still applies.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure.
Expand Down Expand Up @@ -2888,12 +2901,22 @@ public function sideload_item( WP_REST_Request $request ) {

add_filter( 'upload_dir', $filter_upload_dir, 100 );

/*
* On multisite, only the original upload counts against the site's
* upload space: the sub-sizes the server generates from it are written
* without a check. A sideloaded file is such a sub-size, so skip the
* check rather than leave the attachment with some of its sizes missing.
*/
$this->skip_upload_space_check = true;

if ( ! empty( $files ) ) {
$file = $this->upload_from_file( $files, $headers );
} else {
$file = $this->upload_from_data( $request->get_body(), $headers );
}

$this->skip_upload_space_check = false;

remove_filter( 'wp_unique_filename', $filter_filename );
remove_filter( 'image_editor_output_format', '__return_empty_array', 100 );
remove_filter( 'upload_dir', $filter_upload_dir, 100 );
Expand Down
186 changes: 186 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,192 @@ 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 by the
* site's upload space.
*
* On the classic path, only the original upload is checked against the
* space quota (check_upload_size() on `wp_handle_upload_prefilter`). The
* image editor then writes sub-sizes even when that upload used the last
* of the space. This is the baseline for
* test_sideload_sub_size_after_multisite_upload_space_is_used().
*
* @ticket 66680
* @group multisite
* @group ms-required
* @covers ::wp_update_image_subsizes
*/
public function test_server_sub_sizes_after_multisite_upload_space_is_used(): void {
// Upload the original without sub-sizes, while there is space left.
add_filter( 'intermediate_image_sizes_advanced', '__return_empty_array' );
$attachment_id = self::factory()->attachment->create_upload_object( DIR_TESTDATA . '/images/canola.jpg' );
remove_filter( 'intermediate_image_sizes_advanced', '__return_empty_array' );

$this->assertIsInt( $attachment_id, 'The original upload should succeed.' );

$metadata = wp_get_attachment_metadata( $attachment_id );

$this->assertIsArray( $metadata, 'The original should have metadata.' );
$this->assertEmpty( $metadata['sizes'] ?? array(), 'The original should have no sub-sizes yet.' );

// The original used the last of the space.
add_filter( 'get_space_allowed', '__return_zero' );
update_site_option( 'upload_space_check_disabled', false );
$this->assertSame( 0, get_upload_space_available(), 'The site should have no upload space left.' );

$metadata = wp_update_image_subsizes( $attachment_id );

$this->assertIsArray( $metadata, 'Generating sub-sizes should succeed.' );
$this->assertIsArray( $metadata['sizes'], 'The metadata should list the sub-sizes.' );
$this->assertArrayHasKey( 'thumbnail', $metadata['sizes'], 'The server should generate a thumbnail.' );
}

/**
* Tests that a client-generated sub-size can be sideloaded after the
* original upload used the last of the site's upload space.
*
* With client-side media processing, the browser uploads the original,
* then sideloads each sub-size. The server generates the same sub-sizes
* regardless of the quota on the classic path (see
* test_server_sub_sizes_after_multisite_upload_space_is_used()), so the
* sideload should not be rejected for lack of space.
*
* @ticket 66680
* @group multisite
* @group ms-required
* @covers WP_REST_Attachments_Controller::sideload_item
*/
public function test_sideload_sub_size_after_multisite_upload_space_is_used(): void {
$this->enable_client_side_media_processing();

wp_set_current_user( self::$author_id );
update_site_option( 'upload_space_check_disabled', false );

// Upload the original while there is space left.
$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, 'The upload response should describe the attachment.' );
$this->assertIsInt( $data['id'], 'The upload response should include the attachment ID.' );

$attachment_id = $data['id'];

// The original used the last of the space.
add_filter( 'get_space_allowed', '__return_zero' );
$this->assertSame( 0, get_upload_space_available(), 'The site should have no upload space left.' );

// A 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/jpeg' );
$request->set_header( 'Content-Disposition', 'attachment; filename=canola-50x50.jpg' );
$request->set_param( 'image_size', 'thumbnail' );
$request->set_body( (string) file_get_contents( DIR_TESTDATA . '/images/test-image.jpg' ) );
$response = rest_get_server()->dispatch( $request );

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

/**
* Tests that a sub-size sideloaded as a multipart file, the way the browser
* sends it, skips the upload space check without disabling it for the
* uploads that follow.
*
* @ticket 66680
* @group multisite
* @group ms-required
* @covers WP_REST_Attachments_Controller::sideload_item
*/
public function test_sideload_sub_size_file_skips_multisite_upload_space_check_once(): void {
$this->enable_client_side_media_processing();

wp_set_current_user( self::$author_id );
update_site_option( 'upload_space_check_disabled', false );

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

$this->assertIsInt( $attachment_id, 'The original upload should succeed.' );

// The original used the last of the space.
add_filter( 'get_space_allowed', '__return_zero' );

// wp_handle_upload() removes the temporary file, so sideload a copy.
$sub_size_file = wp_tempnam( 'canola-50x50.jpg' );
copy( DIR_TESTDATA . '/images/test-image.jpg', $sub_size_file );

$request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment_id}/sideload" );
$request->set_file_params(
array(
'file' => array(
'error' => UPLOAD_ERR_OK,
'name' => 'canola-50x50.jpg',
'type' => 'image/jpeg',
'size' => (int) filesize( $sub_size_file ),
'tmp_name' => $sub_size_file,
),
)
);
$request->set_param( 'image_size', 'thumbnail' );
$response = rest_get_server()->dispatch( $request );

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

// A new upload is still checked against the upload space.
$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 );

$this->assertErrorResponse( 'rest_upload_limited_space', $response, 400 );
}

/**
* Tests that a sideloaded sub-size is still held to the network's maximum
* upload file size.
*
* @ticket 66680
* @group multisite
* @group ms-required
* @covers WP_REST_Attachments_Controller::sideload_item
*/
public function test_sideload_sub_size_exceeds_multisite_max_filesize(): void {
$this->enable_client_side_media_processing();

wp_set_current_user( self::$author_id );
update_site_option( 'upload_space_check_disabled', false );

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

$this->assertIsInt( $attachment_id, 'The original upload should succeed.' );

update_site_option( 'fileupload_maxk', 1 );

$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-50x50.jpg' );
$request->set_param( 'image_size', 'thumbnail' );
$request->set_body( (string) file_get_contents( DIR_TESTDATA . '/images/test-image.jpg' ) );
$response = rest_get_server()->dispatch( $request );

$this->assertErrorResponse( 'rest_upload_file_too_big', $response, 400 );
}

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