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 @@ -151,6 +151,12 @@ public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CRE
'default' => true,
'description' => __( 'Whether to convert image formats.' ),
);
$args['url'] = array(
'type' => 'string',
'format' => 'uri',
'description' => __( 'URL of an external image to sideload into the media library, instead of uploading a file.' ),
'sanitize_callback' => 'sanitize_url',
);
}

return $args;
Expand Down Expand Up @@ -289,7 +295,7 @@ public function create_item_permissions_check( $request ) {
* Creates a single attachment.
*
* @since 4.7.0
* @since 7.1.0 Added `generate_sub_sizes` and `convert_format` parameters.
* @since 7.1.0 Added the `generate_sub_sizes`, `convert_format`, and `url` parameters.
*
* @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 @@ -317,6 +323,18 @@ public function create_item( $request ) {
add_filter( 'image_editor_output_format', '__return_empty_array', 100 );
}

/*
* When a URL is supplied instead of an uploaded file, sideload the
* remote image on the server. This avoids a cross-origin browser fetch,
* which fails under cross-origin isolation. The sub-size and scaling
* filters applied above still govern whether derivatives are generated.
*/
if ( ! empty( $request['url'] ) ) {
$response = $this->create_item_from_url( $request );
$this->remove_client_side_media_processing_filters();
return $response;
}

$insert = $this->insert_attachment( $request );

if ( is_wp_error( $insert ) ) {
Expand Down Expand Up @@ -410,6 +428,95 @@ public function create_item( $request ) {
return $response;
}

/**
* Sideloads an external image from a URL into the media library.
*
* Downloads the remote file on the server, avoiding a cross-origin browser
* fetch that fails under cross-origin isolation. Whether sub-sizes are
* generated is governed by the filters applied in create_item().
*
* @since 7.1.0
*
* @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.
*/
protected function create_item_from_url( $request ) {
// Sideloading downloads and stores a file, so require the upload capability.
if ( ! current_user_can( 'upload_files' ) ) {
return new WP_Error(
'rest_cannot_create',
__( 'Sorry, you are not allowed to upload media on this site.' ),
array( 'status' => rest_authorization_required_code() )
);
}

require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/image.php';

$url = $request['url'];
$post_id = ! empty( $request['post'] ) ? (int) $request['post'] : 0;

// Derive the filename from the URL path before downloading anything.
$url_path = wp_parse_url( $url, PHP_URL_PATH );
$filename = $url_path ? wp_basename( $url_path ) : '';
if ( '' === $filename ) {
return new WP_Error(
'rest_invalid_url',
__( 'Could not determine a filename from the provided URL.' ),
array( 'status' => 400 )
);
}

/*
* Download the remote file with WordPress's HTTP API, which validates
* the host and blocks requests to private or local addresses. This is
* the same primitive core's media_sideload_image() relies on.
*/
$tmp_file = download_url( $url );
if ( is_wp_error( $tmp_file ) ) {
return $tmp_file;
}

$file_array = array(
'name' => $filename,
'tmp_name' => $tmp_file,
);

$attachment_id = media_handle_sideload( $file_array, $post_id );

if ( is_wp_error( $attachment_id ) ) {
/*
* media_handle_sideload() deletes the temp file on success; remove
* it explicitly when the sideload fails.
*/
if ( file_exists( $tmp_file ) ) {
wp_delete_file( $tmp_file );
}
return $attachment_id;
}

$attachment = get_post( $attachment_id );

$request->set_param( 'context', 'edit' );

/*
* media_handle_sideload() fires the standard insert hooks (including
* wp_after_insert_post), but not the REST-specific action, so fire it
* here for parity with the uploaded-file path in create_item().
*
* This action is documented in
* wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php
*/
do_action( 'rest_after_insert_attachment', $attachment, $request, true );

$response = $this->prepare_item_for_response( $attachment, $request );
$response->set_status( 201 );
$response->header( 'Location', rest_url( rest_get_route_for_post( $attachment_id ) ) );

Comment on lines +501 to +516
return $response;
}

/**
* Removes filters added for client-side media processing.
*
Expand Down
Loading
Loading