diff --git a/src/wp-admin/includes/media.php b/src/wp-admin/includes/media.php index c4fe301bebb3b..32603227dd0c8 100644 --- a/src/wp-admin/includes/media.php +++ b/src/wp-admin/includes/media.php @@ -1056,24 +1056,32 @@ function media_sideload_image( $file, $post_id = 0, $desc = null, $return_type = $allowed_extensions = apply_filters( 'image_sideload_extensions', $allowed_extensions, $file ); $allowed_extensions = array_map( 'preg_quote', $allowed_extensions ); + // Download file to temp location. + $tmp = download_url( $file ); + + // If error storing temporarily, return the error. + if ( is_wp_error( $tmp ) ) { + return $tmp; + } + // Set variables for storage, fix file filename for query strings. preg_match( '/[^\?]+\.(' . implode( '|', $allowed_extensions ) . ')\b/i', $file, $matches ); + // If the URL has no recognizable extension, fall back to the temp filename. + // download_url() may have added one based on the Content-Type header. if ( ! $matches ) { - return new WP_Error( 'image_sideload_failed', __( 'Invalid image URL.' ) ); + preg_match( '/[^\?]+\.(' . implode( '|', $allowed_extensions ) . ')\b/i', $tmp, $matches ); } - $file_array = array(); - $file_array['name'] = wp_basename( $matches[0] ); - - // Download file to temp location. - $file_array['tmp_name'] = download_url( $file ); - - // If error storing temporarily, return the error. - if ( is_wp_error( $file_array['tmp_name'] ) ) { - return $file_array['tmp_name']; + if ( ! $matches ) { + @unlink( $tmp ); + return new WP_Error( 'image_sideload_failed', __( 'Invalid image URL.' ) ); } + $file_array = array(); + $file_array['name'] = wp_basename( $matches[0] ); + $file_array['tmp_name'] = $tmp; + // Do the validation and storage stuff. $id = media_handle_sideload( $file_array, $post_id, $desc ); diff --git a/tests/phpunit/tests/media/mediaSideloadImage.php b/tests/phpunit/tests/media/mediaSideloadImage.php new file mode 100644 index 0000000000000..18a77ea7f3191 --- /dev/null +++ b/tests/phpunit/tests/media/mediaSideloadImage.php @@ -0,0 +1,99 @@ +remove_added_uploads(); + parent::tear_down(); + } + + /** + * Mocks an HTTP response returning a JPEG Content-Type for use with the `pre_http_request` filter. + * + * @param false|array|WP_Error $preempt Whether to preempt an HTTP request. + * @param array $args HTTP request arguments. + * @return array Mocked HTTP response. + */ + public function mock_jpeg_http_response( $preempt, $args ) { + if ( ! empty( $args['filename'] ) ) { + copy( DIR_TESTDATA . '/images/test-image.jpg', $args['filename'] ); + } + + return array( + 'headers' => array( 'content-type' => 'image/jpeg' ), + 'body' => '', + 'response' => array( + 'code' => 200, + 'message' => 'OK', + ), + 'cookies' => array(), + 'filename' => $args['filename'] ?? '', + ); + } + + /** + * Mocks an HTTP response returning an HTML Content-Type for use with the `pre_http_request` filter. + * + * @param false|array|WP_Error $preempt Whether to preempt an HTTP request. + * @param array $args HTTP request arguments. + * @return array Mocked HTTP response. + */ + public function mock_html_http_response( $preempt, $args ) { + if ( ! empty( $args['filename'] ) ) { + copy( DIR_TESTDATA . '/images/test-image.jpg', $args['filename'] ); + } + + return array( + 'headers' => array( 'content-type' => 'text/html' ), + 'body' => '', + 'response' => array( + 'code' => 200, + 'message' => 'OK', + ), + 'cookies' => array(), + 'filename' => $args['filename'] ?? '', + ); + } + + /** + * Tests that an image at a URL without a file extension is sideloaded when the server returns a valid image Content-Type. + * + * @ticket 18730 + */ + public function test_sideload_extensionless_url_succeeds_when_content_type_is_valid_image() { + add_filter( 'pre_http_request', array( $this, 'mock_jpeg_http_response' ), 10, 2 ); + + $result = media_sideload_image( 'http://' . WP_TESTS_DOMAIN . '/photo/1280/10464566223/1/tumblr_lrum2xzkpC1r3z8e3', 0, null, 'id' ); + + remove_filter( 'pre_http_request', array( $this, 'mock_jpeg_http_response' ), 10 ); + + $this->assertNotWPError( $result ); + $this->assertIsInt( $result ); + $this->assertGreaterThan( 0, $result ); + } + + /** + * Tests that sideloading an extensionless URL fails when the server returns a non-image Content-Type. + * + * @ticket 18730 + */ + public function test_sideload_extensionless_url_fails_when_content_type_is_not_image() { + add_filter( 'pre_http_request', array( $this, 'mock_html_http_response' ), 10, 2 ); + + $result = media_sideload_image( 'http://' . WP_TESTS_DOMAIN . '/dynamic-resource', 0, null, 'id' ); + + remove_filter( 'pre_http_request', array( $this, 'mock_html_http_response' ), 10 ); + + $this->assertWPError( $result ); + $this->assertSame( 'image_sideload_failed', $result->get_error_code() ); + } +}