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.
Built from https://develop.svn.wordpress.org/trunk@51653


git-svn-id: http://core.svn.wordpress.org/trunk@51259 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
Andrew Ozz 2021-08-24 20:52:03 +00:00
parent e53ffbaf95
commit 94a990de99
3 changed files with 163 additions and 37 deletions

View File

@ -591,13 +591,11 @@ abstract class WP_Image_Editor {
* @return string|false * @return string|false
*/ */
protected static function get_extension( $mime_type = null ) { protected static function get_extension( $mime_type = null ) {
$extensions = explode( '|', array_search( $mime_type, wp_get_mime_types(), true ) ); if ( empty( $mime_type ) ) {
if ( empty( $extensions[0] ) ) {
return false; return false;
} }
return $extensions[0]; return wp_get_default_extension_for_mime_type( $mime_type );
} }
} }

View File

@ -2488,6 +2488,10 @@ function wp_unique_filename( $dir, $filename, $unique_filename_callback = null )
$filename = sanitize_file_name( $filename ); $filename = sanitize_file_name( $filename );
$ext2 = null; $ext2 = null;
// Initialize vars used in the wp_unique_filename filter.
$number = '';
$alt_filenames = array();
// Separate the filename into a name and extension. // Separate the filename into a name and extension.
$ext = pathinfo( $filename, PATHINFO_EXTENSION ); $ext = pathinfo( $filename, PATHINFO_EXTENSION );
$name = pathinfo( $filename, PATHINFO_BASENAME ); $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 ) ) { if ( $unique_filename_callback && is_callable( $unique_filename_callback ) ) {
$filename = call_user_func( $unique_filename_callback, $dir, $name, $ext ); $filename = call_user_func( $unique_filename_callback, $dir, $name, $ext );
} else { } 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. // 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 ) ) { 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 ); $filename = str_replace( "{$fname}{$ext}", "{$fname}-{$number}{$ext}", $filename );
} }
// Change '.ext' to lower case. // Get the mime type. Uploaded files were already checked with wp_check_filetype_and_ext()
if ( $ext && strtolower( $ext ) != $ext ) { // in _wp_handle_upload(). Using wp_check_filetype() would be sufficient here.
$ext2 = strtolower( $ext ); $file_type = wp_check_filetype( $filename );
$filename2 = preg_replace( '|' . preg_quote( $ext ) . '$|', $ext2, $filename ); $mime_type = $file_type['type'];
// Check for both lower and upper case extension or image sub-sizes may be overwritten. $is_image = ( ! empty( $mime_type ) && 0 === strpos( $mime_type, 'image/' ) );
while ( file_exists( $dir . "/{$filename}" ) || file_exists( $dir . "/{$filename2}" ) ) { $upload_dir = wp_get_upload_dir();
$new_number = (int) $number + 1; $lc_filename = null;
$filename = str_replace( array( "-{$number}{$ext}", "{$number}{$ext}" ), "-{$new_number}{$ext}", $filename );
$filename2 = str_replace( array( "-{$number}{$ext2}", "{$number}{$ext2}" ), "-{$new_number}{$ext2}", $filename2 ); $lc_ext = strtolower( $ext );
$number = $new_number; $_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; if ( '' === "{$number}{$ext}" ) {
} else { $filename = "{$filename}-{$new_number}";
while ( file_exists( $dir . "/{$filename}" ) ) { } else {
$new_number = (int) $number + 1; $filename = str_replace( array( "-{$number}{$ext}", "{$number}{$ext}" ), "-{$new_number}{$ext}", $filename );
if ( '' === "{$number}{$ext}" ) {
$filename = "{$filename}-{$new_number}";
} else {
$filename = str_replace( array( "-{$number}{$ext}", "{$number}{$ext}" ), "-{$new_number}{$ext}", $filename );
}
$number = $new_number;
} }
$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 // Prevent collisions with existing file names that contain dimension-like strings
// (whether they are subsizes or originals uploaded prior to #42437). // (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. // 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'] ) ) { 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 ) ) { if ( ! empty( $files ) ) {
// The extension case may have changed above. $count = count( $files );
$new_ext = ! empty( $ext2 ) ? $ext2 : $ext;
// Ensure this never goes into infinite loop // Ensure this never goes into infinite loop
// as it uses pathinfo() and regex in the check, but string replacement for the changes. // 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 ) ) { while ( $i <= $count && _wp_check_existing_file_names( $filename, $files ) ) {
$new_number = (int) $number + 1; $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++; $i++;
} }
} }
@ -2601,13 +2680,42 @@ function wp_unique_filename( $dir, $filename, $unique_filename_callback = null )
* Filters the result when generating a unique file name. * Filters the result when generating a unique file name.
* *
* @since 4.5.0 * @since 4.5.0
* @since 5.8.1 The `$alt_filenames` and `$number` parameters were added.
* *
* @param string $filename Unique file name. * @param string $filename Unique file name.
* @param string $ext File extension, eg. ".png". * @param string $ext File extension, eg. ".png".
* @param string $dir Directory path. * @param string $dir Directory path.
* @param callable|null $unique_filename_callback Callback function that generates the unique file name. * @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. * Retrieve the file type from the file name.
* *

View File

@ -13,7 +13,7 @@
* *
* @global string $wp_version * @global string $wp_version
*/ */
$wp_version = '5.9-alpha-51652'; $wp_version = '5.9-alpha-51653';
/** /**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema. * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.