: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
* REST API: WP_REST_Attachments_Controller class
* Core controller used to access attachments via the REST API.
* @see WP_REST_Posts_Controller
class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
* Whether the controller supports batching.
protected $allow_batch = false;
* Registers the routes for attachments.
* @see register_rest_route()
public function register_routes() {
parent::register_routes();
'/' . $this->rest_base . '/(?P<id>[\d]+)/post-process',
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'post_process_item' ),
'permission_callback' => array( $this, 'post_process_item_permissions_check' ),
'description' => __( 'Unique identifier for the attachment.' ),
'enum' => array( 'create-image-subsizes' ),
'/' . $this->rest_base . '/(?P<id>[\d]+)/edit',
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'edit_media_item' ),
'permission_callback' => array( $this, 'edit_media_item_permissions_check' ),
'args' => $this->get_edit_media_item_args(),
* Determines the allowed query_vars for a get_items() response and
* @param array $prepared_args Optional. Array of prepared arguments. Default empty array.
* @param WP_REST_Request $request Optional. Request to prepare items for.
* @return array Array of query arguments.
protected function prepare_items_query( $prepared_args = array(), $request = null ) {
$query_args = parent::prepare_items_query( $prepared_args, $request );
if ( empty( $query_args['post_status'] ) ) {
$query_args['post_status'] = 'inherit';
$media_types = $this->get_media_types();
if ( ! empty( $request['media_type'] ) && isset( $media_types[ $request['media_type'] ] ) ) {
$query_args['post_mime_type'] = $media_types[ $request['media_type'] ];
if ( ! empty( $request['mime_type'] ) ) {
$parts = explode( '/', $request['mime_type'] );
if ( isset( $media_types[ $parts[0] ] ) && in_array( $request['mime_type'], $media_types[ $parts[0] ], true ) ) {
$query_args['post_mime_type'] = $request['mime_type'];
// Filter query clauses to include filenames.
if ( isset( $query_args['s'] ) ) {
add_filter( 'wp_allow_query_attachment_by_filename', '__return_true' );
* Checks if a given request has access to create an attachment.
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error Boolean true if the attachment may be created, or a WP_Error if not.
public function create_item_permissions_check( $request ) {
$ret = parent::create_item_permissions_check( $request );
if ( ! $ret || is_wp_error( $ret ) ) {
if ( ! current_user_can( 'upload_files' ) ) {
__( 'Sorry, you are not allowed to upload media on this site.' ),
// Attaching media to a post requires ability to edit said post.
if ( ! empty( $request['post'] ) && ! current_user_can( 'edit_post', (int) $request['post'] ) ) {
__( 'Sorry, you are not allowed to upload media to this post.' ),
array( 'status' => rest_authorization_required_code() )
* Creates a single attachment.
* @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.
public function create_item( $request ) {
if ( ! empty( $request['post'] ) && in_array( get_post_type( $request['post'] ), array( 'revision', 'attachment' ), true ) ) {
__( 'Invalid parent type.' ),
$insert = $this->insert_attachment( $request );
if ( is_wp_error( $insert ) ) {
$schema = $this->get_item_schema();
$attachment_id = $insert['attachment_id'];
if ( isset( $request['alt_text'] ) ) {
update_post_meta( $attachment_id, '_wp_attachment_image_alt', sanitize_text_field( $request['alt_text'] ) );
if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) {
$thumbnail_update = $this->handle_featured_media( $request['featured_media'], $attachment_id );
if ( is_wp_error( $thumbnail_update ) ) {
return $thumbnail_update;
if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
$meta_update = $this->meta->update_value( $request['meta'], $attachment_id );
if ( is_wp_error( $meta_update ) ) {
$attachment = get_post( $attachment_id );
$fields_update = $this->update_additional_fields_for_object( $attachment, $request );
if ( is_wp_error( $fields_update ) ) {
$terms_update = $this->handle_terms( $attachment_id, $request );
if ( is_wp_error( $terms_update ) ) {
$request->set_param( 'context', 'edit' );
* Fires after a single attachment is completely created or updated via the REST API.
* @param WP_Post $attachment Inserted or updated attachment object.
* @param WP_REST_Request $request Request object.
* @param bool $creating True when creating an attachment, false when updating.
do_action( 'rest_after_insert_attachment', $attachment, $request, true );
wp_after_insert_post( $attachment, false, null );
if ( wp_is_serving_rest_request() ) {
* Set a custom header with the attachment_id.
* Used by the browser/client to resume creating image sub-sizes after a PHP fatal error.
header( 'X-WP-Upload-Attachment-ID: ' . $attachment_id );
// Include media and image functions to get access to wp_generate_attachment_metadata().
require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/image.php';
* Post-process the upload (create image sub-sizes, make PDF thumbnails, etc.) and insert attachment meta.
* At this point the server may run out of resources and post-processing of uploaded images may fail.
wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $file ) );
$response = $this->prepare_item_for_response( $attachment, $request );
$response = rest_ensure_response( $response );
$response->set_status( 201 );
$response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $attachment_id ) ) );
* Inserts the attachment post in the database. Does not update the attachment meta.
* @param WP_REST_Request $request
protected function insert_attachment( $request ) {
// Get the file via $_FILES or raw data.
$files = $request->get_file_params();
$headers = $request->get_headers();
// Matches logic in media_handle_upload().
if ( ! empty( $request['post'] ) ) {
$post = get_post( $request['post'] );
// The post date doesn't usually matter for pages, so don't backdate this upload.
if ( $post && 'page' !== $post->post_type && substr( $post->post_date, 0, 4 ) > 0 ) {
$time = $post->post_date;
if ( ! empty( $files ) ) {
$file = $this->upload_from_file( $files, $headers, $time );
$file = $this->upload_from_data( $request->get_body(), $headers, $time );
if ( is_wp_error( $file ) ) {
$name = wp_basename( $file['file'] );
$name_parts = pathinfo( $name );
$name = trim( substr( $name, 0, -( 1 + strlen( $name_parts['extension'] ) ) ) );
// Include image functions to get access to wp_read_image_metadata().
require_once ABSPATH . 'wp-admin/includes/image.php';
// Use image exif/iptc data for title and caption defaults if possible.
$image_meta = wp_read_image_metadata( $file );
if ( ! empty( $image_meta ) ) {
if ( empty( $request['title'] ) && trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) {
$request['title'] = $image_meta['title'];
if ( empty( $request['caption'] ) && trim( $image_meta['caption'] ) ) {
$request['caption'] = $image_meta['caption'];
$attachment = $this->prepare_item_for_database( $request );
$attachment->post_mime_type = $type;
$attachment->guid = $url;
// If the title was not set, use the original filename.
if ( empty( $attachment->post_title ) && ! empty( $files['file']['name'] ) ) {
// Remove the file extension (after the last `.`)
$tmp_title = substr( $files['file']['name'], 0, strrpos( $files['file']['name'], '.' ) );
if ( ! empty( $tmp_title ) ) {
$attachment->post_title = $tmp_title;
// Fall back to the original approach.
if ( empty( $attachment->post_title ) ) {
$attachment->post_title = preg_replace( '/\.[^.]+$/', '', wp_basename( $file ) );
// $post_parent is inherited from $attachment['post_parent'].
$id = wp_insert_attachment( wp_slash( (array) $attachment ), $file, 0, true, false );
if ( is_wp_error( $id ) ) {
if ( 'db_update_error' === $id->get_error_code() ) {
$id->add_data( array( 'status' => 500 ) );
$id->add_data( array( 'status' => 400 ) );
$attachment = get_post( $id );
* Fires after a single attachment is created or updated via the REST API.
* @param WP_Post $attachment Inserted or updated attachment
* @param WP_REST_Request $request The request sent to the API.
* @param bool $creating True when creating an attachment, false when updating.
do_action( 'rest_insert_attachment', $attachment, $request, true );
* Determines the featured media based on a request param.
* @param int $featured_media Featured Media ID.
* @param int $post_id Post ID.
* @return bool|WP_Error Whether the post thumbnail was successfully deleted, otherwise WP_Error.
protected function handle_featured_media( $featured_media, $post_id ) {
$post_type = get_post_type( $post_id );
$thumbnail_support = current_theme_supports( 'post-thumbnails', $post_type ) && post_type_supports( $post_type, 'thumbnail' );
// Similar check as in wp_insert_post().
if ( ! $thumbnail_support && get_post_mime_type( $post_id ) ) {
if ( wp_attachment_is( 'audio', $post_id ) ) {
$thumbnail_support = post_type_supports( 'attachment:audio', 'thumbnail' ) || current_theme_supports( 'post-thumbnails', 'attachment:audio' );
} elseif ( wp_attachment_is( 'video', $post_id ) ) {
$thumbnail_support = post_type_supports( 'attachment:video', 'thumbnail' ) || current_theme_supports( 'post-thumbnails', 'attachment:video' );
if ( $thumbnail_support ) {
return parent::handle_featured_media( $featured_media, $post_id );
'rest_no_featured_media',
/* translators: %s: attachment mime type */
__( 'This site does not support post thumbnails on attachments with MIME type %s.' ),
get_post_mime_type( $post_id )
* Updates a single attachment.
* @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.
public function update_item( $request ) {
if ( ! empty( $request['post'] ) && in_array( get_post_type( $request['post'] ), array( 'revision', 'attachment' ), true ) ) {
__( 'Invalid parent type.' ),
$attachment_before = get_post( $request['id'] );
$response = parent::update_item( $request );
if ( is_wp_error( $response ) ) {
$response = rest_ensure_response( $response );
$data = $response->get_data();
if ( isset( $request['alt_text'] ) ) {
update_post_meta( $data['id'], '_wp_attachment_image_alt', $request['alt_text'] );
$attachment = get_post( $request['id'] );
if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) {
$thumbnail_update = $this->handle_featured_media( $request['featured_media'], $attachment->ID );
if ( is_wp_error( $thumbnail_update ) ) {
return $thumbnail_update;
$fields_update = $this->update_additional_fields_for_object( $attachment, $request );
if ( is_wp_error( $fields_update ) ) {
$request->set_param( 'context', 'edit' );
/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php */
do_action( 'rest_after_insert_attachment', $attachment, $request, false );
wp_after_insert_post( $attachment, true, $attachment_before );
$response = $this->prepare_item_for_response( $attachment, $request );
$response = rest_ensure_response( $response );
* Performs post processing on an attachment.
* @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.
public function post_process_item( $request ) {
switch ( $request['action'] ) {
case 'create-image-subsizes':
require_once ABSPATH . 'wp-admin/includes/image.php';
wp_update_image_subsizes( $request['id'] );
$request['context'] = 'edit';
return $this->prepare_item_for_response( get_post( $request['id'] ), $request );
* Checks if a given request can perform post processing on an attachment.
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise.
public function post_process_item_permissions_check( $request ) {
return $this->update_item_permissions_check( $request );
* Checks if a given request has access to editing media.
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
public function edit_media_item_permissions_check( $request ) {
if ( ! current_user_can( 'upload_files' ) ) {
'rest_cannot_edit_image',
__( 'Sorry, you are not allowed to upload media on this site.' ),
array( 'status' => rest_authorization_required_code() )