WP_REST_Attachments_Controller::create_item_from_url( WP_REST_Request $request ): WP_REST_Response|WP_Error

Sideloads an external image from a URL into the media library.

Description

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().

Parameters

$requestWP_REST_Requestrequired
Full details about the request.

Return

WP_REST_Response|WP_Error Response object on success, WP_Error object on failure.

Source

protected function create_item_from_url( WP_REST_Request $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 )
		);
	}

	/*
	 * Cap the download at the same size the site would accept as a direct
	 * upload. check_upload_size() only applies on multisite, so without a
	 * ceiling here a single site has no limit at all on this path: the
	 * `upload_max_filesize` and `post_max_size` directives bound a request
	 * body, not a fetch the server makes itself.
	 *
	 * When `wp_max_upload_size` returns 0, no ceiling is applied.
	 */
	$max_size = (int) wp_max_upload_size();

	/*
	 * 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.
	 *
	 * `limit_response_size` stops the transfer once the limit is passed,
	 * so an oversized remote file is never written to disk in full. One
	 * byte over the ceiling is enough to fail the size check below.
	 */
	$limit_response_size = static function ( $args ) use ( $max_size ) {
		$args['limit_response_size'] = $max_size + 1;
		return $args;
	};

	if ( $max_size > 0 ) {
		add_filter( 'http_request_args', $limit_response_size );
	}

	$tmp_file = download_url( $url );

	if ( $max_size > 0 ) {
		remove_filter( 'http_request_args', $limit_response_size );
	}

	if ( is_wp_error( $tmp_file ) ) {
		return $tmp_file;
	}

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

	$size_check = self::check_upload_size( $file_array );
	if ( is_wp_error( $size_check ) ) {
		if ( file_exists( $tmp_file ) ) {
			wp_delete_file( $tmp_file );
		}
		return $size_check;
	}

	if ( $max_size > 0 && wp_filesize( $tmp_file ) > $max_size ) {
		if ( file_exists( $tmp_file ) ) {
			wp_delete_file( $tmp_file );
		}

		return new WP_Error(
			'rest_upload_file_too_big',
			/* translators: %s: Maximum allowed file size in kilobytes. */
			sprintf( __( 'This file is too big. Files must be less than %s KB in size.' ), number_format( $max_size / KB_IN_BYTES ) ),
			array( 'status' => 400 )
		);
	}

	$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;
}

Hooks

do_action( ‘rest_after_insert_attachment’, WP_Post $attachment, WP_REST_Request $request, bool $creating )

Fires after a single attachment is completely created or updated via the REST API.

Changelog

VersionDescription
7.1.0Introduced.

User Contributed Notes

You must log in before being able to contribute a note or feedback.