@@ -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