Skip to content

Commit d1cd73a

Browse files
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. git-svn-id: https://develop.svn.wordpress.org/trunk@64492 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 4f9f74c commit d1cd73a

2 files changed

Lines changed: 319 additions & 76 deletions

File tree

‎src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php‎

Lines changed: 67 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,29 @@ public function create_item( $request ) {
516516
);
517517
}
518518

519+
/*
520+
* A request names the file to attach either as an upload or as a URL to
521+
* sideload, never as both: the two are alternative sources for the same
522+
* attachment, so honoring one would silently discard the other. An
523+
* upload arrives as a multipart file parameter or as the raw request
524+
* body, which upload_from_data() identifies by its Content-Disposition
525+
* filename.
526+
*/
527+
if ( ! empty( $request['url'] ) ) {
528+
$headers = $request->get_headers();
529+
$disposition = empty( $headers['content_disposition'] )
530+
? null
531+
: self::get_filename_from_disposition( $headers['content_disposition'] );
532+
533+
if ( ! empty( $request->get_file_params() ) || ! empty( $disposition ) ) {
534+
return new WP_Error(
535+
'rest_invalid_param',
536+
__( 'The url parameter cannot be combined with an uploaded file.' ),
537+
array( 'status' => 400 )
538+
);
539+
}
540+
}
541+
519542
// Handle generate_sub_sizes parameter.
520543
if ( false === $request['generate_sub_sizes'] ) {
521544
add_filter( 'intermediate_image_sizes_advanced', '__return_empty_array', 100 );
@@ -534,18 +557,6 @@ public function create_item( $request ) {
534557
add_filter( 'image_editor_output_format', '__return_empty_array', 100 );
535558
}
536559

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

551562
if ( is_wp_error( $insert ) ) {
@@ -640,33 +651,23 @@ public function create_item( $request ) {
640651
}
641652

642653
/**
643-
* Sideloads an external image from a URL into the media library.
654+
* Handles an image sideloaded from a URL.
644655
*
645656
* Downloads the remote file on the server, avoiding a cross-origin browser
646-
* fetch that fails under cross-origin isolation. Whether sub-sizes are
647-
* generated is governed by the filters applied in create_item().
657+
* fetch that fails under cross-origin isolation. The downloaded file is
658+
* handed to wp_handle_sideload(), so this returns the same data as the
659+
* uploaded-file handlers and the attachment is inserted by the shared code
660+
* in insert_attachment().
648661
*
649662
* @since 7.1.0
650663
*
651-
* @param WP_REST_Request $request Full details about the request.
652-
* @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure.
664+
* @param string $url URL of the image to sideload.
665+
* @param string|null $time Optional. Time formatted in 'yyyy/mm'. Default null.
666+
* @return array|WP_Error Data from wp_handle_sideload().
653667
*/
654-
protected function create_item_from_url( WP_REST_Request $request ) {
655-
// Sideloading downloads and stores a file, so require the upload capability.
656-
if ( ! current_user_can( 'upload_files' ) ) {
657-
return new WP_Error(
658-
'rest_cannot_create',
659-
__( 'Sorry, you are not allowed to upload media on this site.' ),
660-
array( 'status' => rest_authorization_required_code() )
661-
);
662-
}
663-
668+
protected function upload_from_url( $url, $time = null ) {
669+
// Include filesystem functions to get access to download_url() and wp_handle_sideload().
664670
require_once ABSPATH . 'wp-admin/includes/file.php';
665-
require_once ABSPATH . 'wp-admin/includes/media.php';
666-
require_once ABSPATH . 'wp-admin/includes/image.php';
667-
668-
$url = $request['url'];
669-
$post_id = ! empty( $request['post'] ) ? (int) $request['post'] : 0;
670671

671672
// Derive the filename from the URL path before downloading anything.
672673
$url_path = wp_parse_url( $url, PHP_URL_PATH );
@@ -759,36 +760,34 @@ protected function create_item_from_url( WP_REST_Request $request ) {
759760
);
760761
}
761762

762-
$attachment_id = media_handle_sideload( $file_array, $post_id );
763+
$sideloaded = wp_handle_sideload( $file_array, array( 'test_form' => false ), $time );
763764

764-
if ( is_wp_error( $attachment_id ) ) {
765+
if ( isset( $sideloaded['error'] ) ) {
765766
/*
766-
* media_handle_sideload() deletes the temp file on success; remove
767-
* it explicitly when the sideload fails.
767+
* wp_handle_sideload() moves the temp file on success; remove it
768+
* explicitly when it fails.
768769
*/
769770
if ( file_exists( $tmp_file ) ) {
770771
wp_delete_file( $tmp_file );
771772
}
772-
return $attachment_id;
773-
}
774-
775-
$attachment = get_post( $attachment_id );
776-
777-
$request->set_param( 'context', 'edit' );
778773

779-
/*
780-
* media_handle_sideload() fires the standard insert hooks (including
781-
* wp_after_insert_post), but not the REST-specific action, so fire it
782-
* here for parity with the uploaded-file path in create_item().
783-
*/
784-
/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php */
785-
do_action( 'rest_after_insert_attachment', $attachment, $request, true );
774+
// Content that does not match an allowed type is a client error, not a server one.
775+
if ( __( 'Sorry, you are not allowed to upload this file type.' ) === $sideloaded['error'] ) {
776+
return new WP_Error(
777+
'rest_upload_invalid_file_type',
778+
$sideloaded['error'],
779+
array( 'status' => 400 )
780+
);
781+
}
786782

787-
$response = $this->prepare_item_for_response( $attachment, $request );
788-
$response->set_status( 201 );
789-
$response->header( 'Location', rest_url( rest_get_route_for_post( $attachment_id ) ) );
783+
return new WP_Error(
784+
'rest_upload_sideload_error',
785+
$sideloaded['error'],
786+
array( 'status' => 500 )
787+
);
788+
}
790789

791-
return $response;
790+
return $sideloaded;
792791
}
793792

794793
/**
@@ -828,7 +827,16 @@ protected function insert_attachment( $request ) {
828827
}
829828
}
830829

831-
if ( ! empty( $files ) ) {
830+
/*
831+
* A URL is the third way to name the file, alongside a multipart upload
832+
* and a raw request body. All three produce the same data, so the rest
833+
* of this method is shared: the URL path gets the same title and caption
834+
* defaults, the same hooks, and the same terms and meta handling as an
835+
* uploaded file.
836+
*/
837+
if ( ! empty( $request['url'] ) ) {
838+
$file = $this->upload_from_url( $request['url'], $time );
839+
} elseif ( ! empty( $files ) ) {
832840
$file = $this->upload_from_file( $files, $headers, $time );
833841
} else {
834842
$file = $this->upload_from_data( $request->get_body(), $headers, $time );
@@ -869,6 +877,11 @@ protected function insert_attachment( $request ) {
869877

870878
$attachment = $this->prepare_item_for_database( $request );
871879

880+
// The rest_pre_insert_attachment filter can return an error.
881+
if ( is_wp_error( $attachment ) ) {
882+
return $attachment;
883+
}
884+
872885
$attachment->post_mime_type = $type;
873886
$attachment->guid = $url;
874887

0 commit comments

Comments
 (0)