Side-loads a media file without creating a new attachment.
Parameters
$requestWP_REST_Requestrequired- Full details about the request.
Source
public function sideload_item( WP_REST_Request $request ) {
$attachment_id = (int) $request['id'];
$post = $this->get_post( $attachment_id );
if ( is_wp_error( $post ) ) {
return $post;
}
if (
! wp_attachment_is_image( $post ) &&
! wp_attachment_is( 'pdf', $post )
) {
return new WP_Error(
'rest_post_invalid_id',
__( 'Invalid post ID. Only images and PDFs can be sideloaded.' ),
array( 'status' => 400 )
);
}
/*
* Sideloaded files are placed in the same directory as the attachment
* they extend, because the file names produced here are later resolved
* against that directory. An attachment stored outside the uploads
* directory has no such directory to use, so there is nowhere the names
* this would produce could resolve.
*/
$attached_file = get_attached_file( $attachment_id, true );
$subdir = is_string( $attached_file ) && '' !== $attached_file
? $this->get_attachment_upload_subdir( $attached_file )
: null;
if ( ! is_string( $attached_file ) || '' === $attached_file || null === $subdir ) {
return new WP_Error(
'rest_sideload_attachment_not_in_uploads',
__( 'The attachment is not stored in the uploads directory, so a file cannot be sideloaded for it.' ),
array( 'status' => 403 )
);
}
if ( false === $request['convert_format'] ) {
// Prevent image conversion as that is done client-side.
add_filter( 'image_editor_output_format', '__return_empty_array', 100 );
}
// Get the file via $_FILES or raw data.
$files = $request->get_file_params();
$headers = $request->get_headers();
/*
* wp_unique_filename() will always add numeric suffix if the name looks like a sub-size to avoid conflicts.
* See /wp-includes/functions.php.
* With the following filter we can work around this safeguard.
*/
$attachment_filename = wp_basename( $attached_file );
$filter_filename = static function ( $filename, $ext, $dir, $unique_filename_callback, $alt_filenames, $number ) use ( $attachment_filename ) {
return self::filter_wp_unique_filename( $filename, $dir, $number, $attachment_filename );
};
add_filter( 'wp_unique_filename', $filter_filename, 10, 6 );
// Pin the upload to the attachment's own directory, rather than deriving
// it from the parent post's date as media_handle_upload() does for a
// brand new upload. See the note above where $subdir is resolved.
$filter_upload_dir = static function ( $uploads ) use ( $subdir ) {
if (
is_array( $uploads ) &&
isset( $uploads['basedir'], $uploads['baseurl'] ) &&
is_string( $uploads['basedir'] ) &&
is_string( $uploads['baseurl'] )
) {
$uploads['subdir'] = $subdir;
$uploads['path'] = $uploads['basedir'] . $subdir;
$uploads['url'] = $uploads['baseurl'] . $subdir;
}
return $uploads;
};
add_filter( 'upload_dir', $filter_upload_dir, 100 );
if ( ! empty( $files ) ) {
$file = $this->upload_from_file( $files, $headers );
} else {
$file = $this->upload_from_data( $request->get_body(), $headers );
}
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 );
if ( is_wp_error( $file ) ) {
return $file;
}
$type = $file['type'];
$path = $file['file'];
/** @var non-empty-string|non-empty-list<non-empty-string> $image_size */
$image_size = $request['image_size'];
/*
* Validate raster sub-sizes before storing them. Two companion sizes
* are exempt because wp_getimagesize() may not be able to read the
* file at all: the 'animated_video' companion of an animated GIF is a
* video (MP4/WebM), and a source-format original (e.g. a HEIC or JXL
* kept next to its JPEG derivative) may be an unreadable format. Their
* dimensions are neither validated nor recorded. The
* 'animated_video_poster' companion is a real image, so it is still
* read and rejected if unreadable; validate_image_dimensions() skips
* only the registered-size constraint for it.
*/
$skip_dimension_read = self::IMAGE_SIZE_SOURCE_ORIGINAL === $image_size || 'animated_video' === $image_size;
$size = false;
if ( ! $skip_dimension_read ) {
/*
* Read the dimensions up front. A file whose dimensions cannot be
* read is corrupted or an unsupported format and must be rejected
* rather than silently stored with zero dimensions.
*/
$size = wp_getimagesize( $path );
if ( ! $size ) {
// Clean up the uploaded file.
wp_delete_file( $path );
return new WP_Error(
'rest_upload_invalid_image',
__( 'Could not read image dimensions. The file may be corrupted or an unsupported format.' ),
array( 'status' => 400 )
);
}
/*
* Validate the dimensions against every size the file is being
* registered under. An array $image_size shares one file among
* several registered sizes, so the file has to satisfy each of
* them; validating only the scalar case would let a name wrapped
* in a one-element array skip the constraint entirely.
*/
foreach ( (array) $image_size as $size_name ) {
$validation = $this->validate_image_dimensions( $size[0], $size[1], $size_name, $attachment_id );
if ( is_wp_error( $validation ) ) {
// Clean up the uploaded file.
wp_delete_file( $path );
return $validation;
}
}
}
// 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 ( 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
* (ref. WP_REST_Attachments_Controller::get_special_image_sizes()). Those never skip
* the read above, so $size already holds the dimensions.
*/
$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 ( self::IMAGE_SIZE_SOURCE_ORIGINAL === $image_size ) {
/*
* Source-format original (e.g. the HEIC kept next to its JPEG
* derivative). Record the filename so finalize_item can store it
* under the dedicated source-image meta key.
*/
$sub_size_data['file'] = wp_basename( $path );
} elseif ( 'animated_video' === $image_size || 'animated_video_poster' === $image_size ) {
/*
* Converted-video companion of an animated GIF (the MP4/WebM or
* its static first-frame poster). Record the filename so
* finalize_item can store it under its dedicated meta key.
*/
$sub_size_data['file'] = wp_basename( $path );
} elseif ( 'scaled' === $image_size || 'original' === $image_size ) {
/*
* 'scaled' and 'original' both replace the attachment's main file
* with the supplied image and keep the file being replaced as
* `original_image`, which is the untouched upload. A 'scaled'
* image is downsized and an 'original' image has any EXIF
* orientation already applied. This is the same swap WordPress
* makes when it scales or rotates an image on upload; see
* _wp_image_meta_replace_original().
*/
$sub_size_data['original_image'] = $attachment_filename;
// Validate the supplied image before updating the attached file.
// $size was read above: neither of these sizes skips that read.
$filesize = wp_filesize( $path );
if ( ! $size || ! $filesize ) {
// Clean up the uploaded file, which nothing references yet.
wp_delete_file( $path );
return new WP_Error(
'rest_sideload_invalid_image',
__( 'Unable to read the sideloaded image file.' ),
array( 'status' => 500 )
);
}
// Update the attached file to point to the supplied image.
// This writes to _wp_attached_file meta, not _wp_attachment_metadata.
if (
$attached_file !== $path &&
! update_attached_file( $attachment_id, $path )
) {
// Clean up the uploaded file, which nothing references yet.
wp_delete_file( $path );
return new WP_Error(
'rest_sideload_update_attached_file_failed',
__( 'Unable to update the attached file for this attachment.' ),
array( 'status' => 500 )
);
}
$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 {
// As above, $size was already read for every size reaching here.
$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 );
}
/*
* Record the file names produced for this attachment so finalize can
* confirm every stored sub-size was actually sideloaded here. The
* values recorded are exactly the ones handed back to the client, so
* finalize accepts a submission only when it echoes what was produced.
*/
foreach ( array( 'file', 'original_image' ) as $provenance_key ) {
if (
isset( $sub_size_data[ $provenance_key ] ) &&
is_string( $sub_size_data[ $provenance_key ] ) &&
'' !== $sub_size_data[ $provenance_key ]
) {
add_post_meta( $attachment_id, self::META_KEY_SIDELOAD_FILE_NAME, wp_slash( $sub_size_data[ $provenance_key ] ) );
}
}
return rest_ensure_response( $sub_size_data );
}
Changelog
| Version | Description |
|---|---|
| 7.1.0 | Introduced. |
User Contributed Notes
You must log in before being able to contribute a note or feedback.