Commit 7824f9a7b3 for wordpress.org
commit 7824f9a7b349268c35b81211df22046385fa4c55
Author: adamsilverstein <adamsilverstein@git.wordpress.org>
Date: Sun Oct 18 22:57:45 2026 +0000
REST API: Send URL sideloads through the shared attachment insert path.
Since [62659], a `POST /wp/v2/media` request with a `url` built its own response in `create_item_from_url()` and returned before `create_item()` did any of its work. Fields the schema accepts (`title`, `caption`, `description`, `alt_text`, `featured_media`, `meta`, terms) were silently dropped. The `rest_pre_insert_attachment`, `rest_insert_attachment` and `wp_after_insert_post` hooks never fired. A request with both an uploaded file and a `url` discarded the file without an error.
This treats a URL as a third source of the file, next to a multipart upload and a raw request body. `create_item_from_url()` becomes `upload_from_url()`, a sibling of `upload_from_file()` and `upload_from_data()`. It downloads the remote file, passes it to `wp_handle_sideload()`, and returns the same data. `insert_attachment()` picks the handler, so every path now shares the same insert, hooks and field handling. A request with both a file and a `url` now returns a 400. An error returned from `rest_pre_insert_attachment` is now respected on every path instead of being used as if it were an attachment.
Follow-up to [62659], [62841].
Props jeremyfelt, andrewserong, khokansardar.
Fixes #65810.
Built from https://develop.svn.wordpress.org/trunk@64492
git-svn-id: http://core.svn.wordpress.org/trunk@63642 1a063a9b-81f0-0310-95a4-ce76da25c4cd
diff --git a/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php
index f0f1d0e49d..7b3ae9d160 100644
--- a/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php
+++ b/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php
@@ -516,6 +516,29 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
);
}
+ /*
+ * A request names the file to attach either as an upload or as a URL to
+ * sideload, never as both: the two are alternative sources for the same
+ * attachment, so honoring one would silently discard the other. An
+ * upload arrives as a multipart file parameter or as the raw request
+ * body, which upload_from_data() identifies by its Content-Disposition
+ * filename.
+ */
+ if ( ! empty( $request['url'] ) ) {
+ $headers = $request->get_headers();
+ $disposition = empty( $headers['content_disposition'] )
+ ? null
+ : self::get_filename_from_disposition( $headers['content_disposition'] );
+
+ if ( ! empty( $request->get_file_params() ) || ! empty( $disposition ) ) {
+ return new WP_Error(
+ 'rest_invalid_param',
+ __( 'The url parameter cannot be combined with an uploaded file.' ),
+ array( 'status' => 400 )
+ );
+ }
+ }
+
// Handle generate_sub_sizes parameter.
if ( false === $request['generate_sub_sizes'] ) {
add_filter( 'intermediate_image_sizes_advanced', '__return_empty_array', 100 );
@@ -534,18 +557,6 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
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 ) ) {
@@ -640,33 +651,23 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
}
/**
- * Sideloads an external image from a URL into the media library.
+ * Handles an image sideloaded from a URL.
*
* 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().
+ * fetch that fails under cross-origin isolation. The downloaded file is
+ * handed to wp_handle_sideload(), so this returns the same data as the
+ * uploaded-file handlers and the attachment is inserted by the shared code
+ * in insert_attachment().
*
* @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.
+ * @param string $url URL of the image to sideload.
+ * @param string|null $time Optional. Time formatted in 'yyyy/mm'. Default null.
+ * @return array|WP_Error Data from wp_handle_sideload().
*/
- 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() )
- );
- }
-
+ protected function upload_from_url( $url, $time = null ) {
+ // Include filesystem functions to get access to download_url() and wp_handle_sideload().
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 );
@@ -759,36 +760,34 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
);
}
- $attachment_id = media_handle_sideload( $file_array, $post_id );
+ $sideloaded = wp_handle_sideload( $file_array, array( 'test_form' => false ), $time );
- if ( is_wp_error( $attachment_id ) ) {
+ if ( isset( $sideloaded['error'] ) ) {
/*
- * media_handle_sideload() deletes the temp file on success; remove
- * it explicitly when the sideload fails.
+ * wp_handle_sideload() moves the temp file on success; remove it
+ * explicitly when it 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 );
+ // Content that does not match an allowed type is a client error, not a server one.
+ if ( __( 'Sorry, you are not allowed to upload this file type.' ) === $sideloaded['error'] ) {
+ return new WP_Error(
+ 'rest_upload_invalid_file_type',
+ $sideloaded['error'],
+ array( 'status' => 400 )
+ );
+ }
- $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 new WP_Error(
+ 'rest_upload_sideload_error',
+ $sideloaded['error'],
+ array( 'status' => 500 )
+ );
+ }
- return $response;
+ return $sideloaded;
}
/**
@@ -828,7 +827,16 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
}
}
- if ( ! empty( $files ) ) {
+ /*
+ * A URL is the third way to name the file, alongside a multipart upload
+ * and a raw request body. All three produce the same data, so the rest
+ * of this method is shared: the URL path gets the same title and caption
+ * defaults, the same hooks, and the same terms and meta handling as an
+ * uploaded file.
+ */
+ if ( ! empty( $request['url'] ) ) {
+ $file = $this->upload_from_url( $request['url'], $time );
+ } elseif ( ! empty( $files ) ) {
$file = $this->upload_from_file( $files, $headers, $time );
} else {
$file = $this->upload_from_data( $request->get_body(), $headers, $time );
@@ -869,6 +877,11 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
$attachment = $this->prepare_item_for_database( $request );
+ // The rest_pre_insert_attachment filter can return an error.
+ if ( is_wp_error( $attachment ) ) {
+ return $attachment;
+ }
+
$attachment->post_mime_type = $type;
$attachment->guid = $url;
diff --git a/wp-includes/version.php b/wp-includes/version.php
index 99c1adcdfb..1519a07a27 100644
--- a/wp-includes/version.php
+++ b/wp-includes/version.php
@@ -16,7 +16,7 @@
*
* @global string $wp_version
*/
-$wp_version = '7.2-alpha-64491';
+$wp_version = '7.2-alpha-64492';
/**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.