Skip to content
Closed
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 @@ -248,6 +248,38 @@ 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',
'validate_callback' => static function ( $url, $request, $param ) {
/*
* A custom validate_callback replaces the default
* rest_validate_request_arg(), so re-apply it first to keep
* the schema checks (string type, uri format) enforced.
*/
$valid = rest_validate_request_arg( $url, $request, $param );
if ( is_wp_error( $valid ) ) {
return $valid;
}

/*
* Reject URLs that are not safe to request server-side. wp_http_validate_url()
* enforces an HTTP(S) scheme and blocks private, local, and otherwise
* disallowed hosts, guarding the sideload against SSRF.
*/
if ( false === wp_http_validate_url( $url ) ) {
return new WP_Error(
'rest_invalid_url',
__( 'Invalid URL. Provide a valid, publicly reachable HTTP or HTTPS image URL.' ),
array( 'status' => 400 )
);
}

return true;
},
);
}

return $args;
Expand Down Expand Up @@ -406,7 +438,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 @@ -434,6 +466,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 @@ -527,6 +571,108 @@ 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 )
);
}

/*
* Only download URLs whose extension maps to an allowed image MIME type.
* The sideload handler would reject other types anyway (via
* wp_check_filetype_and_ext()), but checking first avoids downloading
* files that can never be accepted, such as PHP scripts.
*/
$filetype = wp_check_filetype( $filename );
if ( ! $filetype['type'] || ! str_starts_with( $filetype['type'], 'image/' ) ) {
return new WP_Error(
'rest_invalid_url',
__( 'The provided URL does not point to a supported image file.' ),
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 ) ) );

return $response;
}

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