mirror of
git://develop.git.wordpress.org/
synced 2025-04-29 16:41:00 +02:00
Media: Fix wp_unique_filename()
to check for name collisions with all alternate file names when an image may be converted after uploading. This includes possible collinions with pre-existing images whose sub-sizes/thumbnails are regenerated.
Props ianmjones, azaozz. Fixes #53668. git-svn-id: https://develop.svn.wordpress.org/trunk@51653 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
parent
d4abd5ca4a
commit
67f8702477
@ -591,13 +591,11 @@ abstract class WP_Image_Editor {
|
||||
* @return string|false
|
||||
*/
|
||||
protected static function get_extension( $mime_type = null ) {
|
||||
$extensions = explode( '|', array_search( $mime_type, wp_get_mime_types(), true ) );
|
||||
|
||||
if ( empty( $extensions[0] ) ) {
|
||||
if ( empty( $mime_type ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $extensions[0];
|
||||
return wp_get_default_extension_for_mime_type( $mime_type );
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2488,6 +2488,10 @@ function wp_unique_filename( $dir, $filename, $unique_filename_callback = null )
|
||||
$filename = sanitize_file_name( $filename );
|
||||
$ext2 = null;
|
||||
|
||||
// Initialize vars used in the wp_unique_filename filter.
|
||||
$number = '';
|
||||
$alt_filenames = array();
|
||||
|
||||
// Separate the filename into a name and extension.
|
||||
$ext = pathinfo( $filename, PATHINFO_EXTENSION );
|
||||
$name = pathinfo( $filename, PATHINFO_BASENAME );
|
||||
@ -2508,8 +2512,7 @@ function wp_unique_filename( $dir, $filename, $unique_filename_callback = null )
|
||||
if ( $unique_filename_callback && is_callable( $unique_filename_callback ) ) {
|
||||
$filename = call_user_func( $unique_filename_callback, $dir, $name, $ext );
|
||||
} else {
|
||||
$number = '';
|
||||
$fname = pathinfo( $filename, PATHINFO_FILENAME );
|
||||
$fname = pathinfo( $filename, PATHINFO_FILENAME );
|
||||
|
||||
// Always append a number to file names that can potentially match image sub-size file names.
|
||||
if ( $fname && preg_match( '/-(?:\d+x\d+|scaled|rotated)$/', $fname ) ) {
|
||||
@ -2519,37 +2522,54 @@ function wp_unique_filename( $dir, $filename, $unique_filename_callback = null )
|
||||
$filename = str_replace( "{$fname}{$ext}", "{$fname}-{$number}{$ext}", $filename );
|
||||
}
|
||||
|
||||
// Change '.ext' to lower case.
|
||||
if ( $ext && strtolower( $ext ) != $ext ) {
|
||||
$ext2 = strtolower( $ext );
|
||||
$filename2 = preg_replace( '|' . preg_quote( $ext ) . '$|', $ext2, $filename );
|
||||
// Get the mime type. Uploaded files were already checked with wp_check_filetype_and_ext()
|
||||
// in _wp_handle_upload(). Using wp_check_filetype() would be sufficient here.
|
||||
$file_type = wp_check_filetype( $filename );
|
||||
$mime_type = $file_type['type'];
|
||||
|
||||
// Check for both lower and upper case extension or image sub-sizes may be overwritten.
|
||||
while ( file_exists( $dir . "/{$filename}" ) || file_exists( $dir . "/{$filename2}" ) ) {
|
||||
$new_number = (int) $number + 1;
|
||||
$filename = str_replace( array( "-{$number}{$ext}", "{$number}{$ext}" ), "-{$new_number}{$ext}", $filename );
|
||||
$filename2 = str_replace( array( "-{$number}{$ext2}", "{$number}{$ext2}" ), "-{$new_number}{$ext2}", $filename2 );
|
||||
$number = $new_number;
|
||||
$is_image = ( ! empty( $mime_type ) && 0 === strpos( $mime_type, 'image/' ) );
|
||||
$upload_dir = wp_get_upload_dir();
|
||||
$lc_filename = null;
|
||||
|
||||
$lc_ext = strtolower( $ext );
|
||||
$_dir = trailingslashit( $dir );
|
||||
|
||||
// If the extension is uppercase add an alternate file name with lowercase extension. Both need to be tested
|
||||
// for uniqueness as the extension will be changed to lowercase for better compatibility with different filesystems.
|
||||
// Fixes an inconsistency in WP < 2.9 where uppercase extensions were allowed but image sub-sizes were created with
|
||||
// lowercase extensions.
|
||||
if ( $ext && $lc_ext !== $ext ) {
|
||||
$lc_filename = preg_replace( '|' . preg_quote( $ext ) . '$|', $lc_ext, $filename );
|
||||
}
|
||||
|
||||
// Increment the number added to the file name if there are any files in $dir whose names match one of the
|
||||
// possible name variations.
|
||||
while ( file_exists( $_dir . $filename ) || ( $lc_filename && file_exists( $_dir . $lc_filename ) ) ) {
|
||||
$new_number = (int) $number + 1;
|
||||
|
||||
if ( $lc_filename ) {
|
||||
$lc_filename = str_replace( array( "-{$number}{$lc_ext}", "{$number}{$lc_ext}" ), "-{$new_number}{$lc_ext}", $lc_filename );
|
||||
}
|
||||
|
||||
$filename = $filename2;
|
||||
} else {
|
||||
while ( file_exists( $dir . "/{$filename}" ) ) {
|
||||
$new_number = (int) $number + 1;
|
||||
|
||||
if ( '' === "{$number}{$ext}" ) {
|
||||
$filename = "{$filename}-{$new_number}";
|
||||
} else {
|
||||
$filename = str_replace( array( "-{$number}{$ext}", "{$number}{$ext}" ), "-{$new_number}{$ext}", $filename );
|
||||
}
|
||||
|
||||
$number = $new_number;
|
||||
if ( '' === "{$number}{$ext}" ) {
|
||||
$filename = "{$filename}-{$new_number}";
|
||||
} else {
|
||||
$filename = str_replace( array( "-{$number}{$ext}", "{$number}{$ext}" ), "-{$new_number}{$ext}", $filename );
|
||||
}
|
||||
|
||||
$number = $new_number;
|
||||
}
|
||||
|
||||
// Change the extension to lowercase if needed.
|
||||
if ( $lc_filename ) {
|
||||
$filename = $lc_filename;
|
||||
}
|
||||
|
||||
// Prevent collisions with existing file names that contain dimension-like strings
|
||||
// (whether they are subsizes or originals uploaded prior to #42437).
|
||||
$upload_dir = wp_get_upload_dir();
|
||||
|
||||
$files = array();
|
||||
$count = 10000;
|
||||
|
||||
// The (resized) image files would have name and extension, and will be in the uploads dir.
|
||||
if ( $name && $ext && @is_dir( $dir ) && false !== strpos( $dir, $upload_dir['basedir'] ) ) {
|
||||
@ -2579,18 +2599,77 @@ function wp_unique_filename( $dir, $filename, $unique_filename_callback = null )
|
||||
}
|
||||
|
||||
if ( ! empty( $files ) ) {
|
||||
// The extension case may have changed above.
|
||||
$new_ext = ! empty( $ext2 ) ? $ext2 : $ext;
|
||||
$count = count( $files );
|
||||
|
||||
// Ensure this never goes into infinite loop
|
||||
// as it uses pathinfo() and regex in the check, but string replacement for the changes.
|
||||
$count = count( $files );
|
||||
$i = 0;
|
||||
$i = 0;
|
||||
|
||||
while ( $i <= $count && _wp_check_existing_file_names( $filename, $files ) ) {
|
||||
$new_number = (int) $number + 1;
|
||||
$filename = str_replace( array( "-{$number}{$new_ext}", "{$number}{$new_ext}" ), "-{$new_number}{$new_ext}", $filename );
|
||||
$number = $new_number;
|
||||
|
||||
// If $ext is uppercase it was replaced with the lowercase version after the previous loop.
|
||||
$filename = str_replace( array( "-{$number}{$lc_ext}", "{$number}{$lc_ext}" ), "-{$new_number}{$lc_ext}", $filename );
|
||||
|
||||
$number = $new_number;
|
||||
$i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if an image will be converted after uploading or some existing images sub-sizes file names may conflict
|
||||
// when regenerated. If yes, ensure the new file name will be unique and will produce unique sub-sizes.
|
||||
if ( $is_image ) {
|
||||
$output_formats = apply_filters( 'image_editor_output_format', array(), $_dir . $filename, $mime_type );
|
||||
$alt_types = array();
|
||||
|
||||
if ( ! empty( $output_formats[ $mime_type ] ) ) {
|
||||
// The image will be converted to this format/mime type.
|
||||
$alt_mime_type = $output_formats[ $mime_type ];
|
||||
|
||||
// Other types of images whose names may conflict if their sub-sizes are regenerated.
|
||||
$alt_types = array_keys( array_intersect( $output_formats, array( $mime_type, $alt_mime_type ) ) );
|
||||
$alt_types[] = $alt_mime_type;
|
||||
} elseif ( ! empty( $output_formats ) ) {
|
||||
$alt_types = array_keys( array_intersect( $output_formats, array( $mime_type ) ) );
|
||||
}
|
||||
|
||||
// Remove duplicates and the original mime type. It will be added later if needed.
|
||||
$alt_types = array_unique( array_diff( $alt_types, array( $mime_type ) ) );
|
||||
|
||||
foreach ( $alt_types as $alt_type ) {
|
||||
$alt_ext = wp_get_default_extension_for_mime_type( $alt_type );
|
||||
|
||||
if ( ! $alt_ext ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$alt_ext = ".{$alt_ext}";
|
||||
$alt_filename = preg_replace( '|' . preg_quote( $lc_ext ) . '$|', $alt_ext, $filename );
|
||||
|
||||
$alt_filenames[ $alt_ext ] = $alt_filename;
|
||||
}
|
||||
|
||||
if ( ! empty( $alt_filenames ) ) {
|
||||
// Add the original filename. It needs to be checked again together with the alternate filenames
|
||||
// when $number is incremented.
|
||||
$alt_filenames[ $lc_ext ] = $filename;
|
||||
|
||||
// Ensure no infinite loop.
|
||||
$i = 0;
|
||||
|
||||
while ( $i <= $count && _wp_check_alternate_file_names( $alt_filenames, $_dir, $files ) ) {
|
||||
$new_number = (int) $number + 1;
|
||||
|
||||
foreach ( $alt_filenames as $alt_ext => $alt_filename ) {
|
||||
$alt_filenames[ $alt_ext ] = str_replace( array( "-{$number}{$alt_ext}", "{$number}{$alt_ext}" ), "-{$new_number}{$alt_ext}", $alt_filename );
|
||||
}
|
||||
|
||||
// Also update the $number in (the output) $filename.
|
||||
// If the extension was uppercase it was already replaced with the lowercase version.
|
||||
$filename = str_replace( array( "-{$number}{$lc_ext}", "{$number}{$lc_ext}" ), "-{$new_number}{$lc_ext}", $filename );
|
||||
|
||||
$number = $new_number;
|
||||
$i++;
|
||||
}
|
||||
}
|
||||
@ -2601,13 +2680,42 @@ function wp_unique_filename( $dir, $filename, $unique_filename_callback = null )
|
||||
* Filters the result when generating a unique file name.
|
||||
*
|
||||
* @since 4.5.0
|
||||
* @since 5.8.1 The `$alt_filenames` and `$number` parameters were added.
|
||||
*
|
||||
* @param string $filename Unique file name.
|
||||
* @param string $ext File extension, eg. ".png".
|
||||
* @param string $dir Directory path.
|
||||
* @param callable|null $unique_filename_callback Callback function that generates the unique file name.
|
||||
* @param string[] $alt_filenames Array of alternate file names that were checked for collisions.
|
||||
* @param int|string $number The highest number that was used to make the file name unique
|
||||
* or an empty string if unused.
|
||||
*/
|
||||
return apply_filters( 'wp_unique_filename', $filename, $ext, $dir, $unique_filename_callback );
|
||||
return apply_filters( 'wp_unique_filename', $filename, $ext, $dir, $unique_filename_callback, $alt_filenames, $number );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to test if each of an array of file names could conflict with existing files.
|
||||
*
|
||||
* @since 5.8.1
|
||||
* @access private
|
||||
*
|
||||
* @param string[] $filenames Array of file names to check.
|
||||
* @param string $dir The directory containing the files.
|
||||
* @param array $files An array of existing files in the directory. May be empty.
|
||||
* @return bool True if the tested file name could match an existing file, false otherwise.
|
||||
*/
|
||||
function _wp_check_alternate_file_names( $filenames, $dir, $files ) {
|
||||
foreach ( $filenames as $filename ) {
|
||||
if ( file_exists( $dir . $filename ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( ! empty( $files ) && _wp_check_existing_file_names( $filename, $files ) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2793,6 +2901,26 @@ function wp_ext2type( $ext ) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns first matched extension for the mime-type,
|
||||
* as mapped from wp_get_mime_types().
|
||||
*
|
||||
* @since 5.8.1
|
||||
*
|
||||
* @param string $mime_type
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
function wp_get_default_extension_for_mime_type( $mime_type ) {
|
||||
$extensions = explode( '|', array_search( $mime_type, wp_get_mime_types(), true ) );
|
||||
|
||||
if ( empty( $extensions[0] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $extensions[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the file type from the file name.
|
||||
*
|
||||
|
BIN
tests/phpunit/data/images/test-image-1-100x100.jpg
Normal file
BIN
tests/phpunit/data/images/test-image-1-100x100.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.0 KiB |
BIN
tests/phpunit/data/images/test-image-2.gif
Normal file
BIN
tests/phpunit/data/images/test-image-2.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 524 B |
BIN
tests/phpunit/data/images/test-image-3.jpg
Normal file
BIN
tests/phpunit/data/images/test-image-3.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.0 KiB |
BIN
tests/phpunit/data/images/test-image-4.png
Normal file
BIN
tests/phpunit/data/images/test-image-4.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1011 B |
@ -168,13 +168,21 @@ class Tests_Functions extends WP_UnitTestCase {
|
||||
$testdir = DIR_TESTDATA . '/images/';
|
||||
|
||||
// Sanity check.
|
||||
$this->assertSame( 'abcdefg.png', wp_unique_filename( $testdir, 'abcdefg.png' ), 'Sanitiy check failed' );
|
||||
$this->assertSame( 'abcdefg.png', wp_unique_filename( $testdir, 'abcdefg.png' ), 'Test non-existing file, file name should be unchanged.' );
|
||||
|
||||
// Check number is appended for file already exists.
|
||||
// Ensure correct images exist.
|
||||
$this->assertFileExists( $testdir . 'test-image.png', 'Test image does not exist' );
|
||||
$this->assertSame( 'test-image-1.png', wp_unique_filename( $testdir, 'test-image.png' ), 'Number not appended correctly' );
|
||||
$this->assertFileDoesNotExist( $testdir . 'test-image-1.png' );
|
||||
|
||||
// Check number is appended if file already exists.
|
||||
$this->assertSame( 'test-image-1.png', wp_unique_filename( $testdir, 'test-image.png' ), 'File name not unique, number not appended.' );
|
||||
|
||||
// Check file with uppercase extension.
|
||||
$this->assertSame( 'test-image-1.png', wp_unique_filename( $testdir, 'test-image.PNG' ), 'File name with uppercase extension not unique, number not appended.' );
|
||||
|
||||
// Check file name with already added number.
|
||||
$this->assertSame( 'test-image-2-1.gif', wp_unique_filename( $testdir, 'test-image-2.gif' ), 'File name not unique, number not appended correctly.' );
|
||||
|
||||
// Check special chars.
|
||||
$this->assertSame( 'testtest-image.png', wp_unique_filename( $testdir, 'testtést-imagé.png' ), 'Filename with special chars failed' );
|
||||
|
||||
@ -221,6 +229,88 @@ class Tests_Functions extends WP_UnitTestCase {
|
||||
return $upload_dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* @ticket 53668
|
||||
*/
|
||||
function test_wp_unique_filename_with_additional_image_extension() {
|
||||
$testdir = DIR_TESTDATA . '/images/';
|
||||
|
||||
add_filter( 'upload_dir', array( $this, 'upload_dir_patch_basedir' ) );
|
||||
|
||||
// Set conversions for uploaded images.
|
||||
add_filter( 'image_editor_output_format', array( $this, 'image_editor_output_format_handler' ) );
|
||||
|
||||
// Ensure the test images exist.
|
||||
$this->assertFileExists( $testdir . 'test-image-1-100x100.jpg', 'test-image-1-100x100.jpg does not exist' );
|
||||
$this->assertFileExists( $testdir . 'test-image-2.gif', 'test-image-2.gif does not exist' );
|
||||
$this->assertFileExists( $testdir . 'test-image-3.jpg', 'test-image-3.jpg does not exist' );
|
||||
$this->assertFileExists( $testdir . 'test-image-4.png', 'test-image-4.png does not exist' );
|
||||
|
||||
// Standard test: file does not exist and there are no possible intersections with other files.
|
||||
$this->assertSame(
|
||||
'abcdef.png',
|
||||
wp_unique_filename( $testdir, 'abcdef.png' ),
|
||||
'The abcdef.png, abcdef.gif, and abcdef.jpg images do not exist. The file name should not be changed.'
|
||||
);
|
||||
|
||||
// Actual clash recognized.
|
||||
$this->assertSame(
|
||||
'canola-1.jpg',
|
||||
wp_unique_filename( $testdir, 'canola.jpg' ),
|
||||
'The canola.jpg image exists. The file name should be unique.'
|
||||
);
|
||||
|
||||
// Same name with different extension and the image will be converted.
|
||||
$this->assertSame(
|
||||
'canola-1.png',
|
||||
wp_unique_filename( $testdir, 'canola.png' ),
|
||||
'The canola.jpg image exists. Uploading canola.png that will be converted to canola.jpg should produce unique file name.'
|
||||
);
|
||||
|
||||
// Same name with different uppercase extension and the image will be converted.
|
||||
$this->assertSame(
|
||||
'canola-1.png',
|
||||
wp_unique_filename( $testdir, 'canola.PNG' ),
|
||||
'The canola.jpg image exists. Uploading canola.PNG that will be converted to canola.jpg should produce unique file name.'
|
||||
);
|
||||
|
||||
// Actual clash with several images with different extensions.
|
||||
$this->assertSame(
|
||||
'test-image-5.png',
|
||||
wp_unique_filename( $testdir, 'test-image.png' ),
|
||||
'The test-image.png, test-image-1-100x100.jpg, test-image-2.gif, test-image-3.jpg, and test-image-4.png images exist.' .
|
||||
'All of them may clash when creating sub-sizes or regenerating thumbnails in the future. The filename should be unique.'
|
||||
);
|
||||
|
||||
// Possible clash with regenerated thumbnails in the future.
|
||||
$this->assertSame(
|
||||
'codeispoetry-1.jpg',
|
||||
wp_unique_filename( $testdir, 'codeispoetry.jpg' ),
|
||||
'The codeispoetry.png image exists. When regenerating thumbnails for it they will be converted to JPG.' .
|
||||
'The name of the newly uploaded codeispoetry.jpg should be made unique.'
|
||||
);
|
||||
|
||||
remove_filter( 'image_editor_output_format', array( $this, 'image_editor_output_format_handler' ) );
|
||||
remove_filter( 'upload_dir', array( $this, 'upload_dir_patch_basedir' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the output format when editing images. When uploading a PNG file
|
||||
* it will be converted to JPEG, GIF to JPEG, and PICT to BMP
|
||||
* (if the image editor in PHP supports it).
|
||||
*
|
||||
* @param array $formats
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function image_editor_output_format_handler( $formats ) {
|
||||
$formats['image/png'] = 'image/jpeg';
|
||||
$formats['image/gif'] = 'image/jpeg';
|
||||
$formats['image/pct'] = 'image/bmp';
|
||||
|
||||
return $formats;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider data_is_not_serialized
|
||||
*/
|
||||
@ -1946,4 +2036,18 @@ class Tests_Functions extends WP_UnitTestCase {
|
||||
array( 'application/activity+json, application/nojson', true ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @ticket 53668
|
||||
*/
|
||||
public function test_wp_get_default_extension_for_mime_type() {
|
||||
$this->assertEquals( 'jpg', wp_get_default_extension_for_mime_type( 'image/jpeg' ), 'jpg not returned as default extension for "image/jpeg"' );
|
||||
$this->assertNotEquals( 'jpeg', wp_get_default_extension_for_mime_type( 'image/jpeg' ), 'jpeg should not be returned as default extension for "image/jpeg"' );
|
||||
$this->assertEquals( 'png', wp_get_default_extension_for_mime_type( 'image/png' ), 'png not returned as default extension for "image/png"' );
|
||||
$this->assertFalse( wp_get_default_extension_for_mime_type( 'wibble/wobble' ), 'false not returned for unrecognized mime type' );
|
||||
$this->assertFalse( wp_get_default_extension_for_mime_type( '' ), 'false not returned when empty string as mime type supplied' );
|
||||
$this->assertFalse( wp_get_default_extension_for_mime_type( ' ' ), 'false not returned when empty string as mime type supplied' );
|
||||
$this->assertFalse( wp_get_default_extension_for_mime_type( 123 ), 'false not returned when int as mime type supplied' );
|
||||
$this->assertFalse( wp_get_default_extension_for_mime_type( null ), 'false not returned when null as mime type supplied' );
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user