From b4f0fc916ef58c0285f119e24f56e90831fcfc48 Mon Sep 17 00:00:00 2001 From: Joe Dolson Date: Mon, 3 Mar 2025 00:17:08 +0000 Subject: [PATCH] Media: Allow uploading images from URLs without extensions. Enable `download_url()` to fetch and verify file types if the URL does not contain a file extension. This allows URL downloads to handle media endpoints like istockphoto.com that use file IDs and formatting arguments to deliver images. Props masteradhoc, mitogh, joedolson, hellofromTonya, antpb, audrasjb, navi161, dmsnell. Fixes #54738. git-svn-id: https://develop.svn.wordpress.org/trunk@59902 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/file.php | 18 ++++++ tests/phpunit/tests/admin/includesFile.php | 74 ++++++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/src/wp-admin/includes/file.php b/src/wp-admin/includes/file.php index 152a1e2f21..40059273ba 100644 --- a/src/wp-admin/includes/file.php +++ b/src/wp-admin/includes/file.php @@ -1241,6 +1241,24 @@ function download_url( $url, $timeout = 300, $signature_verification = false ) { } } + $mime_type = wp_remote_retrieve_header( $response, 'content-type' ); + if ( $mime_type && 'tmp' === pathinfo( $tmpfname, PATHINFO_EXTENSION ) ) { + $valid_mime_types = array_flip( get_allowed_mime_types() ); + if ( ! empty( $valid_mime_types[ $mime_type ] ) ) { + $extensions = explode( '|', $valid_mime_types[ $mime_type ] ); + $new_image_name = substr( $tmpfname, 0, -4 ) . ".{$extensions[0]}"; + if ( 0 === validate_file( $new_image_name ) ) { + if ( rename( $tmpfname, $new_image_name ) ) { + $tmpfname = $new_image_name; + } + + if ( ( $tmpfname !== $new_image_name ) && file_exists( $new_image_name ) ) { + unlink( $new_image_name ); + } + } + } + } + $content_md5 = wp_remote_retrieve_header( $response, 'Content-MD5' ); if ( $content_md5 ) { diff --git a/tests/phpunit/tests/admin/includesFile.php b/tests/phpunit/tests/admin/includesFile.php index 943cabb7c0..20ced72821 100644 --- a/tests/phpunit/tests/admin/includesFile.php +++ b/tests/phpunit/tests/admin/includesFile.php @@ -389,4 +389,78 @@ class Tests_Admin_IncludesFile extends WP_UnitTestCase { return $response; } + + /** + * Test that `download_url()` properly handles setting the file name when set using + * the content type header on URLs with no file extension. + * + * @dataProvider data_download_url_should_use_the_content_type_header_to_set_extension_of_a_file_if_extension_was_not_determined + * + * @covers ::download_url + * @ticket 54738 + * + * @param string $filter A callback containing a fake Content-Type header. + * @param string $ext The expected file extension to match. + */ + public function test_download_url_should_use_the_content_type_header_to_set_extension_of_a_file_if_extension_was_not_determined( $filter, $extension ) { + add_filter( 'pre_http_request', $filter ); + + $filename = download_url( 'url_with_content_type_header' ); + $this->assertStringEndsWith( $extension, $filename ); + $this->assertFileExists( $filename ); + $this->unlink( $filename ); + } + + /** + * Data provider for test_download_url_should_use_the_content_type_header_to_set_extension_of_a_file_if_extension_was_not_determined + * + * @see test_download_url_should_use_the_content_type_header_to_set_extension_of_a_file_if_extension_was_not_determined() + * @test + * @ticket 54738 + * + * @return Generator + */ + public function data_download_url_should_use_the_content_type_header_to_set_extension_of_a_file_if_extension_was_not_determined() { + yield 'Content-Type header in the response' => array( + function () { + return array( + 'response' => array( + 'code' => 200, + ), + 'headers' => array( + 'content-type' => 'image/jpeg', + ), + ); + }, + '.jpg', + ); + + yield 'Invalid Content-Type header' => array( + function () { + return array( + 'response' => array( + 'code' => 200, + ), + 'headers' => array( + 'content-type' => '../../filename-from-content-disposition-header.txt', + ), + ); + }, + '.tmp', + ); + + yield 'Valid content type but not supported mime type' => array( + function () { + return array( + 'response' => array( + 'code' => 200, + ), + 'headers' => array( + 'content-type' => 'image/x-xbm', + ), + ); + }, + '.tmp', + ); + } }