Make WordPress Core

Changeset 51653


Ignore:
Timestamp:
08/24/2021 08:50:21 PM (5 years ago)
Author:
azaozz
Message:

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.

Location:
trunk
Files:
4 added
3 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-image-editor.php

    r51442 r51653  
    592592         */
    593593        protected static function get_extension( $mime_type = null ) {
    594                 $extensions = explode( '|', array_search( $mime_type, wp_get_mime_types(), true ) );
    595 
    596                 if ( empty( $extensions[0] ) ) {
     594                if ( empty( $mime_type ) ) {
    597595                        return false;
    598596                }
    599597
    600                 return $extensions[0];
     598                return wp_get_default_extension_for_mime_type( $mime_type );
    601599        }
    602600}
  • trunk/src/wp-includes/functions.php

    r51625 r51653  
    24892489        $ext2     = null;
    24902490
     2491        // Initialize vars used in the wp_unique_filename filter.
     2492        $number        = '';
     2493        $alt_filenames = array();
     2494
    24912495        // Separate the filename into a name and extension.
    24922496        $ext  = pathinfo( $filename, PATHINFO_EXTENSION );
     
    25092513                $filename = call_user_func( $unique_filename_callback, $dir, $name, $ext );
    25102514        } else {
    2511                 $number = '';
    2512                 $fname  = pathinfo( $filename, PATHINFO_FILENAME );
     2515                $fname = pathinfo( $filename, PATHINFO_FILENAME );
    25132516
    25142517                // Always append a number to file names that can potentially match image sub-size file names.
     
    25202523                }
    25212524
    2522                 // Change '.ext' to lower case.
    2523                 if ( $ext && strtolower( $ext ) != $ext ) {
    2524                         $ext2      = strtolower( $ext );
    2525                         $filename2 = preg_replace( '|' . preg_quote( $ext ) . '$|', $ext2, $filename );
    2526 
    2527                         // Check for both lower and upper case extension or image sub-sizes may be overwritten.
    2528                         while ( file_exists( $dir . "/{$filename}" ) || file_exists( $dir . "/{$filename2}" ) ) {
    2529                                 $new_number = (int) $number + 1;
    2530                                 $filename   = str_replace( array( "-{$number}{$ext}", "{$number}{$ext}" ), "-{$new_number}{$ext}", $filename );
    2531                                 $filename2  = str_replace( array( "-{$number}{$ext2}", "{$number}{$ext2}" ), "-{$new_number}{$ext2}", $filename2 );
    2532                                 $number     = $new_number;
     2525                // Get the mime type. Uploaded files were already checked with wp_check_filetype_and_ext()
     2526                // in _wp_handle_upload(). Using wp_check_filetype() would be sufficient here.
     2527                $file_type = wp_check_filetype( $filename );
     2528                $mime_type = $file_type['type'];
     2529
     2530                $is_image    = ( ! empty( $mime_type ) && 0 === strpos( $mime_type, 'image/' ) );
     2531                $upload_dir  = wp_get_upload_dir();
     2532                $lc_filename = null;
     2533
     2534                $lc_ext = strtolower( $ext );
     2535                $_dir   = trailingslashit( $dir );
     2536
     2537                // If the extension is uppercase add an alternate file name with lowercase extension. Both need to be tested
     2538                // for uniqueness as the extension will be changed to lowercase for better compatibility with different filesystems.
     2539                // Fixes an inconsistency in WP < 2.9 where uppercase extensions were allowed but image sub-sizes were created with
     2540                // lowercase extensions.
     2541                if ( $ext && $lc_ext !== $ext ) {
     2542                        $lc_filename = preg_replace( '|' . preg_quote( $ext ) . '$|', $lc_ext, $filename );
     2543                }
     2544
     2545                // Increment the number added to the file name if there are any files in $dir whose names match one of the
     2546                // possible name variations.
     2547                while ( file_exists( $_dir . $filename ) || ( $lc_filename && file_exists( $_dir . $lc_filename ) ) ) {
     2548                        $new_number = (int) $number + 1;
     2549
     2550                        if ( $lc_filename ) {
     2551                                $lc_filename = str_replace( array( "-{$number}{$lc_ext}", "{$number}{$lc_ext}" ), "-{$new_number}{$lc_ext}", $lc_filename );
    25332552                        }
    25342553
    2535                         $filename = $filename2;
    2536                 } else {
    2537                         while ( file_exists( $dir . "/{$filename}" ) ) {
    2538                                 $new_number = (int) $number + 1;
    2539 
    2540                                 if ( '' === "{$number}{$ext}" ) {
    2541                                         $filename = "{$filename}-{$new_number}";
    2542                                 } else {
    2543                                         $filename = str_replace( array( "-{$number}{$ext}", "{$number}{$ext}" ), "-{$new_number}{$ext}", $filename );
    2544                                 }
    2545 
    2546                                 $number = $new_number;
     2554                        if ( '' === "{$number}{$ext}" ) {
     2555                                $filename = "{$filename}-{$new_number}";
     2556                        } else {
     2557                                $filename = str_replace( array( "-{$number}{$ext}", "{$number}{$ext}" ), "-{$new_number}{$ext}", $filename );
    25472558                        }
     2559
     2560                        $number = $new_number;
     2561                }
     2562
     2563                // Change the extension to lowercase if needed.
     2564                if ( $lc_filename ) {
     2565                        $filename = $lc_filename;
    25482566                }
    25492567
    25502568                // Prevent collisions with existing file names that contain dimension-like strings
    25512569                // (whether they are subsizes or originals uploaded prior to #42437).
    2552                 $upload_dir = wp_get_upload_dir();
     2570
     2571                $files = array();
     2572                $count = 10000;
    25532573
    25542574                // The (resized) image files would have name and extension, and will be in the uploads dir.
     
    25802600
    25812601                        if ( ! empty( $files ) ) {
    2582                                 // The extension case may have changed above.
    2583                                 $new_ext = ! empty( $ext2 ) ? $ext2 : $ext;
     2602                                $count = count( $files );
    25842603
    25852604                                // Ensure this never goes into infinite loop
    25862605                                // as it uses pathinfo() and regex in the check, but string replacement for the changes.
    2587                                 $count = count( $files );
    2588                                 $i     = 0;
     2606                                $i = 0;
    25892607
    25902608                                while ( $i <= $count && _wp_check_existing_file_names( $filename, $files ) ) {
    25912609                                        $new_number = (int) $number + 1;
    2592                                         $filename   = str_replace( array( "-{$number}{$new_ext}", "{$number}{$new_ext}" ), "-{$new_number}{$new_ext}", $filename );
    2593                                         $number     = $new_number;
     2610
     2611                                        // If $ext is uppercase it was replaced with the lowercase version after the previous loop.
     2612                                        $filename = str_replace( array( "-{$number}{$lc_ext}", "{$number}{$lc_ext}" ), "-{$new_number}{$lc_ext}", $filename );
     2613
     2614                                        $number = $new_number;
    25942615                                        $i++;
    25952616                                }
    25962617                        }
    25972618                }
     2619
     2620                // Check if an image will be converted after uploading or some existing images sub-sizes file names may conflict
     2621                // when regenerated. If yes, ensure the new file name will be unique and will produce unique sub-sizes.
     2622                if ( $is_image ) {
     2623                        $output_formats = apply_filters( 'image_editor_output_format', array(), $_dir . $filename, $mime_type );
     2624                        $alt_types      = array();
     2625
     2626                        if ( ! empty( $output_formats[ $mime_type ] ) ) {
     2627                                // The image will be converted to this format/mime type.
     2628                                $alt_mime_type = $output_formats[ $mime_type ];
     2629
     2630                                // Other types of images whose names may conflict if their sub-sizes are regenerated.
     2631                                $alt_types = array_keys( array_intersect( $output_formats, array( $mime_type, $alt_mime_type ) ) );
     2632                                $alt_types[] = $alt_mime_type;
     2633                        } elseif ( ! empty( $output_formats ) ) {
     2634                                $alt_types = array_keys( array_intersect( $output_formats, array( $mime_type ) ) );
     2635                        }
     2636
     2637                        // Remove duplicates and the original mime type. It will be added later if needed.
     2638                        $alt_types = array_unique( array_diff( $alt_types, array( $mime_type ) ) );
     2639
     2640                        foreach ( $alt_types as $alt_type ) {
     2641                                $alt_ext = wp_get_default_extension_for_mime_type( $alt_type );
     2642
     2643                                if ( ! $alt_ext ) {
     2644                                        continue;
     2645                                }
     2646
     2647                                $alt_ext      = ".{$alt_ext}";
     2648                                $alt_filename = preg_replace( '|' . preg_quote( $lc_ext ) . '$|', $alt_ext, $filename );
     2649
     2650                                $alt_filenames[ $alt_ext ] = $alt_filename;
     2651                        }
     2652
     2653                        if ( ! empty( $alt_filenames ) ) {
     2654                                // Add the original filename. It needs to be checked again together with the alternate filenames
     2655                                // when $number is incremented.
     2656                                $alt_filenames[ $lc_ext ] = $filename;
     2657
     2658                                // Ensure no infinite loop.
     2659                                $i = 0;
     2660
     2661                                while ( $i <= $count && _wp_check_alternate_file_names( $alt_filenames, $_dir, $files ) ) {
     2662                                        $new_number = (int) $number + 1;
     2663
     2664                                        foreach ( $alt_filenames as $alt_ext => $alt_filename ) {
     2665                                                $alt_filenames[ $alt_ext ] = str_replace( array( "-{$number}{$alt_ext}", "{$number}{$alt_ext}" ), "-{$new_number}{$alt_ext}", $alt_filename );
     2666                                        }
     2667
     2668                                        // Also update the $number in (the output) $filename.
     2669                                        // If the extension was uppercase it was already replaced with the lowercase version.
     2670                                        $filename = str_replace( array( "-{$number}{$lc_ext}", "{$number}{$lc_ext}" ), "-{$new_number}{$lc_ext}", $filename );
     2671
     2672                                        $number = $new_number;
     2673                                        $i++;
     2674                                }
     2675                        }
     2676                }
    25982677        }
    25992678
     
    26022681         *
    26032682         * @since 4.5.0
     2683         * @since 5.8.1 The `$alt_filenames` and `$number` parameters were added.
    26042684         *
    26052685         * @param string        $filename                 Unique file name.
     
    26072687         * @param string        $dir                      Directory path.
    26082688         * @param callable|null $unique_filename_callback Callback function that generates the unique file name.
     2689         * @param string[]      $alt_filenames            Array of alternate file names that were checked for collisions.
     2690         * @param int|string    $number                   The highest number that was used to make the file name unique
     2691         *                                                or an empty string if unused.
    26092692         */
    2610         return apply_filters( 'wp_unique_filename', $filename, $ext, $dir, $unique_filename_callback );
     2693        return apply_filters( 'wp_unique_filename', $filename, $ext, $dir, $unique_filename_callback, $alt_filenames, $number );
     2694}
     2695
     2696/**
     2697 * Helper function to test if each of an array of file names could conflict with existing files.
     2698 *
     2699 * @since 5.8.1
     2700 * @access private
     2701 *
     2702 * @param string[] $filenames Array of file names to check.
     2703 * @param string   $dir       The directory containing the files.
     2704 * @param array    $files     An array of existing files in the directory. May be empty.
     2705 * @return bool True if the tested file name could match an existing file, false otherwise.
     2706 */
     2707function _wp_check_alternate_file_names( $filenames, $dir, $files ) {
     2708        foreach ( $filenames as $filename ) {
     2709                if ( file_exists( $dir . $filename ) ) {
     2710                        return true;
     2711                }
     2712
     2713                if ( ! empty( $files ) && _wp_check_existing_file_names( $filename, $files ) ) {
     2714                        return true;
     2715                }
     2716        }
     2717
     2718        return false;
    26112719}
    26122720
     
    27922900                }
    27932901        }
     2902}
     2903
     2904/**
     2905 * Returns first matched extension for the mime-type,
     2906 * as mapped from wp_get_mime_types().
     2907 *
     2908 * @since 5.8.1
     2909 *
     2910 * @param string $mime_type
     2911 *
     2912 * @return string|false
     2913 */
     2914function wp_get_default_extension_for_mime_type( $mime_type ) {
     2915        $extensions = explode( '|', array_search( $mime_type, wp_get_mime_types(), true ) );
     2916
     2917        if ( empty( $extensions[0] ) ) {
     2918                return false;
     2919        }
     2920
     2921        return $extensions[0];
    27942922}
    27952923
  • trunk/tests/phpunit/tests/functions.php

    r51599 r51653  
    169169
    170170                // Sanity check.
    171                 $this->assertSame( 'abcdefg.png', wp_unique_filename( $testdir, 'abcdefg.png' ), 'Sanitiy check failed' );
    172 
    173                 // Check number is appended for file already exists.
     171                $this->assertSame( 'abcdefg.png', wp_unique_filename( $testdir, 'abcdefg.png' ), 'Test non-existing file, file name should be unchanged.' );
     172
     173                // Ensure correct images exist.
    174174                $this->assertFileExists( $testdir . 'test-image.png', 'Test image does not exist' );
    175                 $this->assertSame( 'test-image-1.png', wp_unique_filename( $testdir, 'test-image.png' ), 'Number not appended correctly' );
    176175                $this->assertFileDoesNotExist( $testdir . 'test-image-1.png' );
     176
     177                // Check number is appended if file already exists.
     178                $this->assertSame( 'test-image-1.png', wp_unique_filename( $testdir, 'test-image.png' ), 'File name not unique, number not appended.' );
     179
     180                // Check file with uppercase extension.
     181                $this->assertSame( 'test-image-1.png', wp_unique_filename( $testdir, 'test-image.PNG' ), 'File name with uppercase extension not unique, number not appended.' );
     182
     183                // Check file name with already added number.
     184                $this->assertSame( 'test-image-2-1.gif', wp_unique_filename( $testdir, 'test-image-2.gif' ), 'File name not unique, number not appended correctly.' );
    177185
    178186                // Check special chars.
     
    220228                $upload_dir['basedir'] = DIR_TESTDATA . '/images/';
    221229                return $upload_dir;
     230        }
     231
     232        /**
     233         * @ticket 53668
     234         */
     235        function test_wp_unique_filename_with_additional_image_extension() {
     236                $testdir = DIR_TESTDATA . '/images/';
     237
     238                add_filter( 'upload_dir', array( $this, 'upload_dir_patch_basedir' ) );
     239
     240                // Set conversions for uploaded images.
     241                add_filter( 'image_editor_output_format', array( $this, 'image_editor_output_format_handler' ) );
     242
     243                // Ensure the test images exist.
     244                $this->assertFileExists( $testdir . 'test-image-1-100x100.jpg', 'test-image-1-100x100.jpg does not exist' );
     245                $this->assertFileExists( $testdir . 'test-image-2.gif', 'test-image-2.gif does not exist' );
     246                $this->assertFileExists( $testdir . 'test-image-3.jpg', 'test-image-3.jpg does not exist' );
     247                $this->assertFileExists( $testdir . 'test-image-4.png', 'test-image-4.png does not exist' );
     248
     249                // Standard test: file does not exist and there are no possible intersections with other files.
     250                $this->assertSame(
     251                        'abcdef.png',
     252                        wp_unique_filename( $testdir, 'abcdef.png' ),
     253                        'The abcdef.png, abcdef.gif, and abcdef.jpg images do not exist. The file name should not be changed.'
     254                );
     255
     256                // Actual clash recognized.
     257                $this->assertSame(
     258                        'canola-1.jpg',
     259                        wp_unique_filename( $testdir, 'canola.jpg' ),
     260                        'The canola.jpg image exists. The file name should be unique.'
     261                );
     262
     263                // Same name with different extension and the image will be converted.
     264                $this->assertSame(
     265                        'canola-1.png',
     266                        wp_unique_filename( $testdir, 'canola.png' ),
     267                        'The canola.jpg image exists. Uploading canola.png that will be converted to canola.jpg should produce unique file name.'
     268                );
     269
     270                // Same name with different uppercase extension and the image will be converted.
     271                $this->assertSame(
     272                        'canola-1.png',
     273                        wp_unique_filename( $testdir, 'canola.PNG' ),
     274                        'The canola.jpg image exists. Uploading canola.PNG that will be converted to canola.jpg should produce unique file name.'
     275                );
     276
     277                // Actual clash with several images with different extensions.
     278                $this->assertSame(
     279                        'test-image-5.png',
     280                        wp_unique_filename( $testdir, 'test-image.png' ),
     281                        'The test-image.png, test-image-1-100x100.jpg, test-image-2.gif, test-image-3.jpg, and test-image-4.png images exist.' .
     282                        'All of them may clash when creating sub-sizes or regenerating thumbnails in the future. The filename should be unique.'
     283                );
     284
     285                // Possible clash with regenerated thumbnails in the future.
     286                $this->assertSame(
     287                        'codeispoetry-1.jpg',
     288                        wp_unique_filename( $testdir, 'codeispoetry.jpg' ),
     289                        'The codeispoetry.png image exists. When regenerating thumbnails for it they will be converted to JPG.' .
     290                        'The name of the newly uploaded codeispoetry.jpg should be made unique.'
     291                );
     292
     293                remove_filter( 'image_editor_output_format', array( $this, 'image_editor_output_format_handler' ) );
     294                remove_filter( 'upload_dir', array( $this, 'upload_dir_patch_basedir' ) );
     295        }
     296
     297        /**
     298         * Changes the output format when editing images. When uploading a PNG file
     299         * it will be converted to JPEG, GIF to JPEG, and PICT to BMP
     300         * (if the image editor in PHP supports it).
     301         *
     302         * @param array $formats
     303         *
     304         * @return array
     305         */
     306        public function image_editor_output_format_handler( $formats ) {
     307                $formats['image/png'] = 'image/jpeg';
     308                $formats['image/gif'] = 'image/jpeg';
     309                $formats['image/pct'] = 'image/bmp';
     310
     311                return $formats;
    222312        }
    223313
     
    19472037                );
    19482038        }
     2039
     2040        /**
     2041         * @ticket 53668
     2042         */
     2043        public function test_wp_get_default_extension_for_mime_type() {
     2044                $this->assertEquals( 'jpg', wp_get_default_extension_for_mime_type( 'image/jpeg' ), 'jpg not returned as default extension for "image/jpeg"' );
     2045                $this->assertNotEquals( 'jpeg', wp_get_default_extension_for_mime_type( 'image/jpeg' ), 'jpeg should not be returned as default extension for "image/jpeg"' );
     2046                $this->assertEquals( 'png', wp_get_default_extension_for_mime_type( 'image/png' ), 'png not returned as default extension for "image/png"' );
     2047                $this->assertFalse( wp_get_default_extension_for_mime_type( 'wibble/wobble' ), 'false not returned for unrecognized mime type' );
     2048                $this->assertFalse( wp_get_default_extension_for_mime_type( '' ), 'false not returned when empty string as mime type supplied' );
     2049                $this->assertFalse( wp_get_default_extension_for_mime_type( '   ' ), 'false not returned when empty string as mime type supplied' );
     2050                $this->assertFalse( wp_get_default_extension_for_mime_type( 123 ), 'false not returned when int as mime type supplied' );
     2051                $this->assertFalse( wp_get_default_extension_for_mime_type( null ), 'false not returned when null as mime type supplied' );
     2052        }
    19492053}
Note: See TracChangeset for help on using the changeset viewer.