diff --git a/wp-includes/class-wp-image-editor.php b/wp-includes/class-wp-image-editor.php index 11210180b2..0bc7ee6aee 100644 --- a/wp-includes/class-wp-image-editor.php +++ b/wp-includes/class-wp-image-editor.php @@ -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 ); } } diff --git a/wp-includes/functions.php b/wp-includes/functions.php index 7b01568ad4..02d74e1b70 100644 --- a/wp-includes/functions.php +++ b/wp-includes/functions.php @@ -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. * diff --git a/wp-includes/version.php b/wp-includes/version.php index b670481167..5f70524484 100644 --- a/wp-includes/version.php +++ b/wp-includes/version.php @@ -13,7 +13,7 @@ * * @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.