From 400ada3fb5cdd067d447b2a9a759eebd18ae1e7f Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Mon, 22 Jun 2026 12:24:52 -0700 Subject: [PATCH 1/5] Media: Sideload external images on the server via a `url` REST parameter. Extend the attachments REST endpoint (`POST /wp/v2/media`) to accept an optional `url` parameter. When present, the server downloads the remote image with `download_url()` and sideloads it with `media_handle_sideload()`, instead of the browser fetching the bytes and posting a blob. A browser cross-origin fetch is subject to CORS, so uploading an externally-hosted image to the media library fails for any host that does not send permissive headers. This breaks entirely once the editor is cross-origin isolated, which client-side media processing requires. Letting the server fetch the URL avoids browser CORS, so external uploads work regardless of isolation. The existing sub-size and scaling filters continue to govern derivative generation, the `upload_files` capability is required, and a URL without a usable filename is rejected before any download is attempted. --- .../class-wp-rest-attachments-controller.php | 95 ++++++++ .../rest-api/rest-attachments-controller.php | 226 ++++++++++++++++++ 2 files changed, 321 insertions(+) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index 21805778ba659..5aaea47082636 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -151,6 +151,12 @@ 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', + ); } return $args; @@ -290,6 +296,7 @@ public function create_item_permissions_check( $request ) { * * @since 4.7.0 * @since 7.1.0 Added `generate_sub_sizes` and `convert_format` parameters. + * @since 7.1.0 Added the `url` parameter to sideload an external image on the server. * * @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. @@ -317,6 +324,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 ) ) { @@ -410,6 +429,82 @@ 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 ) + ); + } + + /* + * 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; + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( get_post( $attachment_id ), $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. * diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index 79e9d23cf9dd3..3757f26f8ec8a 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -3541,4 +3541,230 @@ public function test_finalize_item_invalid_id(): void { $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 ); } + + /** + * The URL requested by the most recent mocked HTTP download. + * + * @var string|null + */ + protected $last_download_url = null; + + /** + * Short-circuits download_url()'s HTTP request, writing a local fixture into + * the streamed temp file so media_handle_sideload() has a real image to process. + * + * Mirrors the approach core's media_sideload_image() tests use: returning a + * non-false value from `pre_http_request` skips the network, so the mock must + * copy the fixture into the `filename` the request would have streamed to. + * + * @param false|array|WP_Error $response A preempted response, or false to continue. + * @param array $args HTTP request arguments. + * @param string $url The request URL. + * @return array A faked 200 response. + */ + public function mock_image_download( $response, $args, $url ) { + $this->last_download_url = $url; + + if ( ! empty( $args['filename'] ) ) { + copy( DIR_TESTDATA . '/images/canola.jpg', $args['filename'] ); + } + + return array( + 'response' => array( + 'code' => 200, + 'message' => 'OK', + ), + 'headers' => array(), + 'cookies' => array(), + 'body' => '', + ); + } + + /** + * Verifies that supplying a `url` to the create endpoint sideloads the remote + * image on the server and, with generate_sub_sizes=false, creates no sub-sizes. + * + * This is the cross-origin-isolation fallback path: the server fetches the + * remote image so the browser does not have to, and only the original is kept. + * + * @covers WP_REST_Attachments_Controller::create_item + * @covers WP_REST_Attachments_Controller::create_item_from_url + */ + public function test_create_item_from_url_sideloads_without_subsizes() { + $this->enable_client_side_media_processing(); + + wp_set_current_user( self::$superadmin_id ); + + add_filter( 'pre_http_request', array( $this, 'mock_image_download' ), 10, 3 ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); + $request->set_param( 'url', 'https://example.com/photo.jpg' ); + $request->set_param( 'generate_sub_sizes', false ); + + $response = rest_get_server()->dispatch( $request ); + + remove_filter( 'pre_http_request', array( $this, 'mock_image_download' ), 10 ); + + $data = $response->get_data(); + + $this->assertSame( 201, $response->get_status() ); + $this->assertSame( 'image', $data['media_type'] ); + $this->assertSame( 'https://example.com/photo.jpg', $this->last_download_url ); + + // No sub-sizes should have been generated; only the original is stored. + $metadata = wp_get_attachment_metadata( $data['id'], true ); + $this->assertEmpty( $metadata['sizes'] ?? array(), 'Sideloaded external image should have no sub-sizes.' ); + } + + /** + * Verifies that a sideloaded external image is attached to the post passed in + * the `post` parameter. + * + * @covers WP_REST_Attachments_Controller::create_item + * @covers WP_REST_Attachments_Controller::create_item_from_url + */ + public function test_create_item_from_url_attaches_to_post() { + $this->enable_client_side_media_processing(); + + wp_set_current_user( self::$superadmin_id ); + + $parent_post = self::factory()->post->create(); + + add_filter( 'pre_http_request', array( $this, 'mock_image_download' ), 10, 3 ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); + $request->set_param( 'url', 'https://example.com/attached.jpg' ); + $request->set_param( 'generate_sub_sizes', false ); + $request->set_param( 'post', $parent_post ); + + $response = rest_get_server()->dispatch( $request ); + + remove_filter( 'pre_http_request', array( $this, 'mock_image_download' ), 10 ); + + $data = $response->get_data(); + + $this->assertSame( 201, $response->get_status() ); + $this->assertSame( $parent_post, get_post( $data['id'] )->post_parent ); + } + + /** + * Verifies that a failed download propagates the WP_Error from download_url() + * rather than creating an attachment. + * + * @covers WP_REST_Attachments_Controller::create_item + * @covers WP_REST_Attachments_Controller::create_item_from_url + */ + public function test_create_item_from_url_returns_error_on_download_failure() { + $this->enable_client_side_media_processing(); + + wp_set_current_user( self::$superadmin_id ); + + $fail_download = static function () { + return new WP_Error( 'http_request_failed', 'Could not resolve host.' ); + }; + add_filter( 'pre_http_request', $fail_download ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); + $request->set_param( 'url', 'https://example.com/missing.jpg' ); + $request->set_param( 'generate_sub_sizes', false ); + + $response = rest_get_server()->dispatch( $request ); + + remove_filter( 'pre_http_request', $fail_download ); + + $this->assertSame( 'http_request_failed', $response->get_data()['code'] ); + $this->assertSame( 500, $response->get_status() ); + } + + /** + * Verifies that a URL with no usable path bails with a 400 before any + * download is attempted, rather than handing an empty filename to the + * sideload handler. + * + * @covers WP_REST_Attachments_Controller::create_item_from_url + */ + public function test_create_item_from_url_rejects_url_without_filename() { + $this->enable_client_side_media_processing(); + + wp_set_current_user( self::$superadmin_id ); + + // Fail loudly if the guard does not bail and a download is attempted. + $downloaded = false; + $track = static function () use ( &$downloaded ) { + $downloaded = true; + return new WP_Error( 'http_request_failed', 'Should not be reached.' ); + }; + add_filter( 'pre_http_request', $track ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); + $request->set_param( 'url', 'https://example.com/?img=123' ); + + $response = rest_get_server()->dispatch( $request ); + + remove_filter( 'pre_http_request', $track ); + + $this->assertSame( 'rest_invalid_url', $response->get_data()['code'] ); + $this->assertSame( 400, $response->get_status() ); + $this->assertFalse( $downloaded, 'No download should be attempted for a URL without a filename.' ); + } + + /** + * Verifies that a user without the `upload_files` capability cannot sideload + * an external image and that the request bails before any download happens. + * + * @covers WP_REST_Attachments_Controller::create_item_from_url + */ + public function test_create_item_from_url_requires_upload_capability() { + $subscriber_id = self::factory()->user->create( array( 'role' => 'subscriber' ) ); + wp_set_current_user( $subscriber_id ); + + // Fail loudly if the guard does not bail and a download is attempted. + $downloaded = false; + $track = static function () use ( &$downloaded ) { + $downloaded = true; + return new WP_Error( 'http_request_failed', 'Should not be reached.' ); + }; + add_filter( 'pre_http_request', $track ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); + $request->set_param( 'url', 'https://example.com/denied.jpg' ); + + $controller = new WP_REST_Attachments_Controller( 'attachment' ); + $method = new ReflectionMethod( $controller, 'create_item_from_url' ); + $method->setAccessible( true ); + $result = $method->invoke( $controller, $request ); + + remove_filter( 'pre_http_request', $track ); + + $this->assertWPError( $result ); + $this->assertSame( 'rest_cannot_create', $result->get_error_code() ); + $this->assertSame( 403, $result->get_error_data()['status'] ); + $this->assertFalse( $downloaded, 'No download should be attempted without upload_files.' ); + } + + /** + * Verifies that the `url` argument is registered on the creatable media route + * so requests can supply an external image URL to sideload. + * + * @covers WP_REST_Attachments_Controller::get_endpoint_args_for_item_schema + */ + public function test_url_registered_as_creatable_arg() { + $this->enable_client_side_media_processing(); + + $routes = rest_get_server()->get_routes(); + $this->assertArrayHasKey( '/wp/v2/media', $routes ); + + $creatable = null; + foreach ( $routes['/wp/v2/media'] as $route ) { + if ( ! empty( $route['methods'][ WP_REST_Server::CREATABLE ] ) ) { + $creatable = $route; + break; + } + } + + $this->assertNotNull( $creatable, 'The media route should register a CREATABLE handler.' ); + $this->assertArrayHasKey( 'url', $creatable['args'] ); + $this->assertSame( 'string', $creatable['args']['url']['type'] ); + $this->assertSame( 'uri', $creatable['args']['url']['format'] ); + } } From 4de4f088a06fb7ed40009cfd89d2f314bcf1fefd Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Mon, 22 Jun 2026 15:35:40 -0700 Subject: [PATCH 2/5] Tests: Regenerate REST API client fixtures for the media `url` parameter. The sideload `url` REST parameter added to the `POST /wp/v2/media` endpoint changes the generated API schema. Update the QUnit fixture so the `test_build_wp_api_client_fixtures` git-diff check passes. --- tests/qunit/fixtures/wp-api-generated.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index fa03d9751fe99..7dcb7f7f54734 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -3160,6 +3160,12 @@ mockedApiResponse.Schema = { "default": true, "description": "Whether to convert image formats.", "required": false + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL of an external image to sideload into the media library, instead of uploading a file.", + "required": false } } } From 105ea30da059fdd6f6c0ac29cddb900962f6ba9b Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Mon, 22 Jun 2026 16:03:27 -0700 Subject: [PATCH 3/5] Tests: Guard ReflectionMethod::setAccessible() for PHP 8.5 compatibility. `ReflectionMethod::setAccessible()` is deprecated as of PHP 8.5 (it has had no effect since PHP 8.1), and the test suite converts deprecations to exceptions, so the unconditional call errored on PHP 8.5. Wrap it in the established `PHP_VERSION_ID < 80100` guard used elsewhere in core tests. --- tests/phpunit/tests/rest-api/rest-attachments-controller.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index 3757f26f8ec8a..8fe766928a44e 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -3731,7 +3731,9 @@ public function test_create_item_from_url_requires_upload_capability() { $controller = new WP_REST_Attachments_Controller( 'attachment' ); $method = new ReflectionMethod( $controller, 'create_item_from_url' ); - $method->setAccessible( true ); + if ( PHP_VERSION_ID < 80100 ) { + $method->setAccessible( true ); + } $result = $method->invoke( $controller, $request ); remove_filter( 'pre_http_request', $track ); From 93c911e3caad94c32b174e5ea9b8652feb11b71d Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Mon, 22 Jun 2026 17:57:51 -0700 Subject: [PATCH 4/5] Media: Fire rest_after_insert_attachment on the URL sideload path. media_handle_sideload() fires the standard insert hooks (including wp_after_insert_post), but not the REST-specific rest_after_insert_attachment action. Fire it on the sideload path for parity with the uploaded-file path in create_item(), so extensions hooking the documented REST action also run for attachments created from a URL. Also consolidate the duplicate `@since 7.1.0` docblock line, and add tests covering sub-size generation under the default generate_sub_sizes and the rest_after_insert_attachment firing. --- .../class-wp-rest-attachments-controller.php | 18 ++++- .../rest-api/rest-attachments-controller.php | 69 +++++++++++++++++++ 2 files changed, 84 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index 5aaea47082636..4b492d3b9af40 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -295,8 +295,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 `url` parameter to sideload an external image on the server. + * @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. @@ -497,8 +496,21 @@ protected function create_item_from_url( $request ) { return $attachment_id; } + $attachment = get_post( $attachment_id ); + $request->set_param( 'context', 'edit' ); - $response = $this->prepare_item_for_response( get_post( $attachment_id ), $request ); + + /* + * 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 ) ) ); diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index 8fe766928a44e..6d3503d56b0db 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -3616,6 +3616,75 @@ public function test_create_item_from_url_sideloads_without_subsizes() { $this->assertEmpty( $metadata['sizes'] ?? array(), 'Sideloaded external image should have no sub-sizes.' ); } + /** + * Verifies that, with the default generate_sub_sizes (true), sideloading an + * external image generates sub-sizes, so the filters applied in create_item() + * still govern derivative generation on the URL path. + * + * @covers WP_REST_Attachments_Controller::create_item + * @covers WP_REST_Attachments_Controller::create_item_from_url + */ + public function test_create_item_from_url_generates_subsizes_by_default() { + $this->enable_client_side_media_processing(); + + wp_set_current_user( self::$superadmin_id ); + + add_filter( 'pre_http_request', array( $this, 'mock_image_download' ), 10, 3 ); + + // Note: generate_sub_sizes is intentionally not set, so it defaults to true. + $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); + $request->set_param( 'url', 'https://example.com/full.jpg' ); + + $response = rest_get_server()->dispatch( $request ); + + remove_filter( 'pre_http_request', array( $this, 'mock_image_download' ), 10 ); + + $data = $response->get_data(); + + $this->assertSame( 201, $response->get_status() ); + + $metadata = wp_get_attachment_metadata( $data['id'], true ); + $this->assertNotEmpty( $metadata['sizes'] ?? array(), 'Sub-sizes should be generated when generate_sub_sizes is true.' ); + } + + /** + * Verifies that the REST-specific rest_after_insert_attachment action fires on + * the URL sideload path, for parity with the uploaded-file path. + * + * @covers WP_REST_Attachments_Controller::create_item_from_url + */ + public function test_create_item_from_url_fires_rest_after_insert_attachment() { + $this->enable_client_side_media_processing(); + + wp_set_current_user( self::$superadmin_id ); + + $fired = array(); + $spy = static function ( $attachment, $request, $creating ) use ( &$fired ) { + $fired = array( + 'id' => $attachment->ID, + 'creating' => $creating, + ); + }; + + add_filter( 'pre_http_request', array( $this, 'mock_image_download' ), 10, 3 ); + add_action( 'rest_after_insert_attachment', $spy, 10, 3 ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); + $request->set_param( 'url', 'https://example.com/hooked.jpg' ); + $request->set_param( 'generate_sub_sizes', false ); + + $response = rest_get_server()->dispatch( $request ); + + remove_action( 'rest_after_insert_attachment', $spy, 10 ); + remove_filter( 'pre_http_request', array( $this, 'mock_image_download' ), 10 ); + + $data = $response->get_data(); + + $this->assertSame( 201, $response->get_status() ); + $this->assertSame( $data['id'], $fired['id'] ?? null, 'rest_after_insert_attachment should fire with the new attachment.' ); + $this->assertTrue( $fired['creating'] ?? null, 'rest_after_insert_attachment should report creating=true.' ); + } + /** * Verifies that a sideloaded external image is attached to the post passed in * the `post` parameter. From 039354ca2c82c66a1fd8f56d052ef72695718373 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 23 Jun 2026 15:39:03 -0700 Subject: [PATCH 5/5] Tests: Add @ticket 65517 annotations to the URL sideload tests. --- .../rest-api/rest-attachments-controller.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index 6d3503d56b0db..4a75a65d42fe5 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -3587,6 +3587,8 @@ public function mock_image_download( $response, $args, $url ) { * This is the cross-origin-isolation fallback path: the server fetches the * remote image so the browser does not have to, and only the original is kept. * + * @ticket 65517 + * * @covers WP_REST_Attachments_Controller::create_item * @covers WP_REST_Attachments_Controller::create_item_from_url */ @@ -3621,6 +3623,8 @@ public function test_create_item_from_url_sideloads_without_subsizes() { * external image generates sub-sizes, so the filters applied in create_item() * still govern derivative generation on the URL path. * + * @ticket 65517 + * * @covers WP_REST_Attachments_Controller::create_item * @covers WP_REST_Attachments_Controller::create_item_from_url */ @@ -3651,6 +3655,8 @@ public function test_create_item_from_url_generates_subsizes_by_default() { * Verifies that the REST-specific rest_after_insert_attachment action fires on * the URL sideload path, for parity with the uploaded-file path. * + * @ticket 65517 + * * @covers WP_REST_Attachments_Controller::create_item_from_url */ public function test_create_item_from_url_fires_rest_after_insert_attachment() { @@ -3689,6 +3695,8 @@ public function test_create_item_from_url_fires_rest_after_insert_attachment() { * Verifies that a sideloaded external image is attached to the post passed in * the `post` parameter. * + * @ticket 65517 + * * @covers WP_REST_Attachments_Controller::create_item * @covers WP_REST_Attachments_Controller::create_item_from_url */ @@ -3720,6 +3728,8 @@ public function test_create_item_from_url_attaches_to_post() { * Verifies that a failed download propagates the WP_Error from download_url() * rather than creating an attachment. * + * @ticket 65517 + * * @covers WP_REST_Attachments_Controller::create_item * @covers WP_REST_Attachments_Controller::create_item_from_url */ @@ -3750,6 +3760,8 @@ public function test_create_item_from_url_returns_error_on_download_failure() { * download is attempted, rather than handing an empty filename to the * sideload handler. * + * @ticket 65517 + * * @covers WP_REST_Attachments_Controller::create_item_from_url */ public function test_create_item_from_url_rejects_url_without_filename() { @@ -3781,6 +3793,8 @@ public function test_create_item_from_url_rejects_url_without_filename() { * Verifies that a user without the `upload_files` capability cannot sideload * an external image and that the request bails before any download happens. * + * @ticket 65517 + * * @covers WP_REST_Attachments_Controller::create_item_from_url */ public function test_create_item_from_url_requires_upload_capability() { @@ -3817,6 +3831,8 @@ public function test_create_item_from_url_requires_upload_capability() { * Verifies that the `url` argument is registered on the creatable media route * so requests can supply an external image URL to sideload. * + * @ticket 65517 + * * @covers WP_REST_Attachments_Controller::get_endpoint_args_for_item_schema */ public function test_url_registered_as_creatable_arg() {