Changeset 63018
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php
r63015 r63018 238 238 $args = parent::get_endpoint_args_for_item_schema( $method ); 239 239 240 if ( WP_REST_Server::CREATABLE === $method && wp_is_client_side_media_processing_enabled() ) { 241 $args['generate_sub_sizes'] = array( 242 'type' => 'boolean', 243 'default' => true, 244 'description' => __( 'Whether to generate image sub sizes.' ), 245 ); 246 $args['convert_format'] = array( 247 'type' => 'boolean', 248 'default' => true, 249 'description' => __( 'Whether to convert image formats.' ), 250 ); 251 $args['url'] = array( 252 'type' => 'string', 253 'format' => 'uri', 254 'description' => __( 'URL of an external image to sideload into the media library, instead of uploading a file.' ), 255 'sanitize_callback' => 'sanitize_url', 256 'validate_callback' => static function ( $url, $request, $param ) { 257 /* 258 * A custom validate_callback replaces the default 259 * rest_validate_request_arg(), so re-apply it first to keep 260 * the schema checks (string type, uri format) enforced. 261 */ 262 $valid = rest_validate_request_arg( $url, $request, $param ); 263 if ( is_wp_error( $valid ) ) { 264 return $valid; 265 } 266 267 /* 268 * Reject URLs that are not safe to request server-side. wp_http_validate_url() 269 * enforces an HTTP(S) scheme and blocks private, local, and otherwise 270 * disallowed hosts, guarding the sideload against SSRF. 271 */ 272 if ( false === wp_http_validate_url( $url ) ) { 273 return new WP_Error( 274 'rest_invalid_url', 275 __( 'Invalid URL. Provide a valid, publicly reachable HTTP or HTTPS image URL.' ), 276 array( 'status' => 400 ) 277 ); 278 } 279 280 return true; 281 }, 282 ); 283 } 240 if ( WP_REST_Server::CREATABLE !== $method ) { 241 return $args; 242 } 243 244 $args['generate_sub_sizes'] = array( 245 'type' => 'boolean', 246 'default' => true, 247 'description' => __( 'Whether to generate image sub sizes.' ), 248 ); 249 250 $args['convert_format'] = array( 251 'type' => 'boolean', 252 'default' => true, 253 'description' => __( 'Whether to convert image formats.' ), 254 ); 255 256 $args['url'] = array( 257 'type' => 'string', 258 'format' => 'uri', 259 'description' => __( 'URL of an external image to sideload into the media library, instead of uploading a file.' ), 260 'sanitize_callback' => 'sanitize_url', 261 'validate_callback' => static function ( $url, $request, $param ) { 262 /* 263 * A custom validate_callback replaces the default 264 * rest_validate_request_arg(), so re-apply it first to keep 265 * the schema checks (string type, uri format) enforced. 266 */ 267 $valid = rest_validate_request_arg( $url, $request, $param ); 268 if ( is_wp_error( $valid ) ) { 269 return $valid; 270 } 271 272 /* 273 * Reject URLs that are not safe to request server-side. wp_http_validate_url() 274 * enforces an HTTP(S) scheme and blocks private, local, and otherwise 275 * disallowed hosts, guarding the sideload against SSRF. 276 */ 277 if ( false === wp_http_validate_url( $url ) ) { 278 return new WP_Error( 279 'rest_invalid_url', 280 __( 'Invalid URL. Provide a valid, publicly reachable HTTP or HTTPS image URL.' ), 281 array( 'status' => 400 ) 282 ); 283 } 284 285 return true; 286 }, 287 ); 284 288 285 289 return $args; … … 382 386 $prevent_unsupported_uploads = apply_filters( 'wp_prevent_unsupported_mime_type_uploads', true, $files['file']['type'] ?? null ); 383 387 384 // When the client handles image processing (generate_sub_sizes is false), 385 // skip the server-side image editor support check. 386 if ( false === $request['generate_sub_sizes'] ) { 388 /* 389 * When the client handles image processing (generate_sub_sizes is false), 390 * skip the server-side image editor support check. This check exists 391 * because the server cannot process the image, so it is only relaxed when 392 * client side media processing is enabled and something else can. Asking 393 * to skip sub sizes on a site without it does not make an unsupported 394 * image type any more usable. 395 */ 396 if ( wp_is_client_side_media_processing_enabled() && false === $request['generate_sub_sizes'] ) { 387 397 $prevent_unsupported_uploads = false; 388 398 } -
trunk/tests/phpunit/tests/rest-api/rest-attachments-controller.php
r63015 r63018 202 202 private function enable_client_side_media_processing(): void { 203 203 add_filter( 'wp_client_side_media_processing_enabled', '__return_true' ); 204 205 global $wp_rest_server; 206 $wp_rest_server = new Spy_REST_Server(); 207 do_action( 'rest_api_init', $wp_rest_server ); 208 } 209 210 /** 211 * Turns client-side media processing off and rebuilds the REST server so the 212 * routes are registered with the feature disabled. 213 */ 214 private function disable_client_side_media_processing(): void { 215 add_filter( 'wp_client_side_media_processing_enabled', '__return_false' ); 204 216 205 217 global $wp_rest_server; … … 3413 3425 * check uses get_file_params() which is only populated for multipart uploads. 3414 3426 * 3427 * The check is only relaxed when client-side media processing is enabled, 3428 * since that is what makes the client able to handle the image, so the 3429 * feature is enabled here. 3430 * 3415 3431 * @ticket 64836 3432 * @ticket 65517 3416 3433 */ 3417 3434 public function test_upload_unsupported_image_type_skipped_when_not_generating_sub_sizes() { 3435 $this->enable_client_side_media_processing(); 3436 3418 3437 wp_set_current_user( self::$author_id ); 3419 3438 … … 5832 5851 5833 5852 /** 5834 * Verifies that the `url` argument rejects values that are not safe to 5835 * request server-side, guarding the sideload against SSRF. 5853 * Verifies that the media creation arguments are registered even when 5854 * client-side media processing is disabled. 5855 * 5856 * The feature is determined per request, from the scheme and host, so gating 5857 * the schema on it would advertise different arguments for the same site 5858 * depending on how it was reached. 5836 5859 * 5837 5860 * @ticket 65517 … … 5839 5862 * @covers WP_REST_Attachments_Controller::get_endpoint_args_for_item_schema 5840 5863 */ 5841 public function test_ url_arg_rejects_unsafe_urls() {5842 $this-> enable_client_side_media_processing();5864 public function test_creatable_args_registered_without_client_side_media_processing() { 5865 $this->disable_client_side_media_processing(); 5843 5866 5844 5867 $routes = rest_get_server()->get_routes(); … … 5852 5875 5853 5876 $this->assertNotNull( $creatable, 'The media route should register a CREATABLE handler.' ); 5877 $this->assertArrayHasKey( 'url', $creatable['args'] ); 5878 $this->assertArrayHasKey( 'generate_sub_sizes', $creatable['args'] ); 5879 $this->assertArrayHasKey( 'convert_format', $creatable['args'] ); 5880 } 5881 5882 /** 5883 * Verifies that sideloading an external image works when client-side media 5884 * processing is disabled. 5885 * 5886 * @ticket 65517 5887 * 5888 * @covers WP_REST_Attachments_Controller::create_item 5889 * @covers WP_REST_Attachments_Controller::create_item_from_url 5890 */ 5891 public function test_create_item_from_url_without_client_side_media_processing() { 5892 $this->disable_client_side_media_processing(); 5893 5894 wp_set_current_user( self::$superadmin_id ); 5895 5896 add_filter( 'pre_http_request', array( $this, 'mock_image_download' ), 10, 3 ); 5897 5898 $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); 5899 $request->set_param( 'url', 'https://example.com/photo.jpg' ); 5900 5901 $response = rest_get_server()->dispatch( $request ); 5902 5903 remove_filter( 'pre_http_request', array( $this, 'mock_image_download' ), 10 ); 5904 5905 $data = $response->get_data(); 5906 5907 $this->assertSame( 201, $response->get_status() ); 5908 $this->assertSame( 'image', $data['media_type'] ); 5909 $this->assertSame( 'https://example.com/photo.jpg', $this->last_download_url ); 5910 } 5911 5912 /** 5913 * Verifies that the `url` argument's validation runs when client-side media 5914 * processing is disabled, so an unsafe URL is rejected with a 400 rather than 5915 * reaching the download. 5916 * 5917 * @ticket 65517 5918 * 5919 * @covers WP_REST_Attachments_Controller::get_endpoint_args_for_item_schema 5920 */ 5921 public function test_url_arg_rejects_unsafe_urls_without_client_side_media_processing() { 5922 $this->disable_client_side_media_processing(); 5923 5924 wp_set_current_user( self::$superadmin_id ); 5925 5926 $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); 5927 $request->set_param( 'url', 'http://127.0.0.1/private.jpg' ); 5928 5929 $response = rest_get_server()->dispatch( $request ); 5930 5931 $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); 5932 } 5933 5934 /** 5935 * Verifies that `generate_sub_sizes` is honored when client-side media 5936 * processing is disabled. 5937 * 5938 * Skipping sub-size generation is a request the server can carry out on its 5939 * own, so it does not depend on the feature. Sub-sizes can still be added 5940 * later with wp_update_image_subsizes(). 5941 * 5942 * @ticket 65517 5943 * 5944 * @covers WP_REST_Attachments_Controller::create_item 5945 */ 5946 public function test_generate_sub_sizes_honored_without_client_side_media_processing() { 5947 $this->disable_client_side_media_processing(); 5948 5949 wp_set_current_user( self::$superadmin_id ); 5950 5951 add_filter( 'pre_http_request', array( $this, 'mock_image_download' ), 10, 3 ); 5952 5953 $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); 5954 $request->set_param( 'url', 'https://example.com/photo.jpg' ); 5955 $request->set_param( 'generate_sub_sizes', false ); 5956 5957 $response = rest_get_server()->dispatch( $request ); 5958 5959 remove_filter( 'pre_http_request', array( $this, 'mock_image_download' ), 10 ); 5960 5961 $data = $response->get_data(); 5962 5963 $this->assertSame( 201, $response->get_status() ); 5964 5965 $metadata = wp_get_attachment_metadata( $data['id'], true ); 5966 $this->assertEmpty( 5967 $metadata['sizes'] ?? array(), 5968 'Sub-sizes should not be generated when generate_sub_sizes is false.' 5969 ); 5970 } 5971 5972 /** 5973 * Verifies that `generate_sub_sizes` does not relax the unsupported image 5974 * type check when client-side media processing is disabled. 5975 * 5976 * That check exists because the server cannot process the image, so it should 5977 * only be relaxed when the client can process it instead. Otherwise the 5978 * upload is stored unprocessable. 5979 * 5980 * @ticket 65517 5981 * 5982 * @covers WP_REST_Attachments_Controller::create_item_permissions_check 5983 */ 5984 public function test_unsupported_image_type_still_checked_without_client_side_media_processing() { 5985 $this->disable_client_side_media_processing(); 5986 5987 wp_set_current_user( self::$author_id ); 5988 5989 add_filter( 'wp_image_editors', '__return_empty_array' ); 5990 5991 $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); 5992 $request->set_file_params( 5993 array( 5994 'file' => array( 5995 'name' => 'avif-lossy.avif', 5996 'type' => 'image/avif', 5997 'tmp_name' => self::$test_avif_file, 5998 'error' => 0, 5999 'size' => filesize( self::$test_avif_file ), 6000 ), 6001 ) 6002 ); 6003 $request->set_param( 'generate_sub_sizes', false ); 6004 6005 $controller = new WP_REST_Attachments_Controller( 'attachment' ); 6006 $result = $controller->create_item_permissions_check( $request ); 6007 6008 $this->assertWPError( $result ); 6009 $this->assertSame( 'rest_upload_image_type_not_supported', $result->get_error_code() ); 6010 } 6011 6012 /** 6013 * Verifies that the `url` argument rejects values that are not safe to 6014 * request server-side, guarding the sideload against SSRF. 6015 * 6016 * @ticket 65517 6017 * 6018 * @covers WP_REST_Attachments_Controller::get_endpoint_args_for_item_schema 6019 */ 6020 public function test_url_arg_rejects_unsafe_urls() { 6021 $this->enable_client_side_media_processing(); 6022 6023 $routes = rest_get_server()->get_routes(); 6024 $creatable = null; 6025 foreach ( $routes['/wp/v2/media'] as $route ) { 6026 if ( ! empty( $route['methods'][ WP_REST_Server::CREATABLE ] ) ) { 6027 $creatable = $route; 6028 break; 6029 } 6030 } 6031 6032 $this->assertNotNull( $creatable, 'The media route should register a CREATABLE handler.' ); 5854 6033 $this->assertArrayHasKey( 'validate_callback', $creatable['args']['url'] ); 5855 6034
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)