Make WordPress Core

Ticket #53668: 53668.5.diff

File 53668.5.diff, 15.9 KB (added by ianmjones, 3 years ago)

Ensure original extension is supplied to exit filter, add test case that exercises the exit filter to ensure known but temporarily unavailable filenames are checked.

  • src/wp-includes/class-wp-image-editor.php

     
    591591         * @return string|false
    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}
    603601
  • src/wp-includes/functions.php

     
    24862486function wp_unique_filename( $dir, $filename, $unique_filename_callback = null ) {
    24872487        // Sanitize the file name before we begin processing.
    24882488        $filename = sanitize_file_name( $filename );
    2489         $ext2     = null;
    24902489
    24912490        // Separate the filename into a name and extension.
    24922491        $ext  = pathinfo( $filename, PATHINFO_EXTENSION );
     
    25012500                $name = '';
    25022501        }
    25032502
     2503        // Reconstruct sanitized filename with lower case extension.
     2504        $orig_ext = $ext;
     2505        $ext      = strtolower( $ext );
     2506        $filename = pathinfo( $filename, PATHINFO_FILENAME ) . $ext;
     2507
    25042508        /*
    25052509         * Increment the file number until we have a unique file to save in $dir.
    25062510         * Use callback if supplied.
     
    25112515                $number = '';
    25122516                $fname  = pathinfo( $filename, PATHINFO_FILENAME );
    25132517
     2518                // If filename already versioned, get version and un-versioned filename.
     2519                if ( preg_match( '/-(\d)$/', $fname, $matches ) ) {
     2520                        $fname  = preg_replace( '/' . $matches[0] . '$/', '', $fname );
     2521                        $number = (int) $matches[1];
     2522                }
     2523
    25142524                // Always append a number to file names that can potentially match image sub-size file names.
    25152525                if ( $fname && preg_match( '/-(?:\d+x\d+|scaled|rotated)$/', $fname ) ) {
    2516                         $number = 1;
     2526                        $number = (int) $number + 1;
    25172527
    25182528                        // At this point the file name may not be unique. This is tested below and the $number is incremented.
    25192529                        $filename = str_replace( "{$fname}{$ext}", "{$fname}-{$number}{$ext}", $filename );
    25202530                }
    25212531
    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 );
     2532                // Check for both lower and upper case extension or image sub-sizes may be overwritten.
     2533                $uc_ext          = strtoupper( $ext );
     2534                $uc_ext_filename = preg_replace( '|' . preg_quote( $ext ) . '$|', $uc_ext, $filename );
    25262535
    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;
    2533                         }
    2534 
    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;
    2547                         }
     2536                while ( file_exists( $dir . "/{$filename}" ) || file_exists( $dir . "/{$uc_ext_filename}" ) ) {
     2537                        $new_number      = (int) $number + 1;
     2538                        $filename        = str_replace( array( "{$fname}-{$number}{$ext}", "{$fname}{$number}{$ext}" ), "{$fname}-{$new_number}{$ext}", $filename );
     2539                        $uc_ext_filename = str_replace( array( "{$fname}-{$number}{$uc_ext}", "{$fname}{$number}{$uc_ext}" ), "{$fname}-{$new_number}{$uc_ext}", $uc_ext_filename );
     2540                        $number          = $new_number;
    25482541                }
    25492542
    25502543                // Prevent collisions with existing file names that contain dimension-like strings
     
    25792572                        }
    25802573
    25812574                        if ( ! empty( $files ) ) {
    2582                                 // The extension case may have changed above.
    2583                                 $new_ext = ! empty( $ext2 ) ? $ext2 : $ext;
    2584 
    25852575                                // Ensure this never goes into infinite loop
    25862576                                // as it uses pathinfo() and regex in the check, but string replacement for the changes.
    25872577                                $count = count( $files );
     
    25892579
    25902580                                while ( $i <= $count && _wp_check_existing_file_names( $filename, $files ) ) {
    25912581                                        $new_number = (int) $number + 1;
    2592                                         $filename   = str_replace( array( "-{$number}{$new_ext}", "{$number}{$new_ext}" ), "-{$new_number}{$new_ext}", $filename );
     2582                                        $filename   = str_replace( array( "{$fname}-{$number}{$ext}", "{$fname}{$number}{$ext}" ), "{$fname}-{$new_number}{$ext}", $filename );
    25932583                                        $number     = $new_number;
    25942584                                        $i++;
    25952585                                }
    25962586                        }
    25972587                }
     2588
     2589                // If a different file type might be produced for an image, check filename uniqueness for that format.
     2590                $filename = _wp_check_alternate_output_format_uniqueness( $filename, $ext, $dir );
    25982591        }
    25992592
    26002593        /**
     
    26032596         * @since 4.5.0
    26042597         *
    26052598         * @param string        $filename                 Unique file name.
    2606          * @param string        $ext                      File extension, eg. ".png".
     2599         * @param string        $orig_ext                 File extension using original case, e.g. ".png" or ".PNG".
    26072600         * @param string        $dir                      Directory path.
    26082601         * @param callable|null $unique_filename_callback Callback function that generates the unique file name.
    26092602         */
    2610         return apply_filters( 'wp_unique_filename', $filename, $ext, $dir, $unique_filename_callback );
     2603        return apply_filters( 'wp_unique_filename', $filename, $orig_ext, $dir, $unique_filename_callback );
    26112604}
    26122605
    26132606/**
     
    26452638}
    26462639
    26472640/**
     2641 * Helper function for wp_unique_filename to check potential alternate output formats for images.
     2642 *
     2643 * @since 5.8.1
     2644 * @private
     2645 *
     2646 * @param string $filename
     2647 * @param string $ext
     2648 * @param string $dir
     2649 *
     2650 * @return string
     2651 */
     2652function _wp_check_alternate_output_format_uniqueness( $filename, $ext, $dir ) {
     2653        static $checking_alternates;
     2654
     2655        if ( empty( $checking_alternates ) ) {
     2656                $checking_alternates = true;
     2657                $filename_changed    = false;
     2658                $file_type           = wp_check_filetype_and_ext( trailingslashit( $dir ) . $filename, $filename );
     2659                $mime_type           = $file_type['type'];
     2660
     2661                if ( ! empty( $mime_type ) && 0 === strpos( $mime_type, 'image/' ) ) {
     2662                        $output_formats = apply_filters( 'image_editor_output_format', array(), trailingslashit( $dir ) . $filename, $mime_type );
     2663
     2664                        if ( ! empty( $output_formats ) && is_array( $output_formats ) ) {
     2665                                // Temporarily add uploaded type to alts for simpler analysis.
     2666                                $alt_mime_types = array( $mime_type );
     2667
     2668                                // Alternate thumbnail format for uploaded file?
     2669                                if ( ! empty( $output_formats[ $mime_type ] ) ) {
     2670                                        $alt_mime_types[] = $output_formats[ $mime_type ];
     2671                                }
     2672
     2673                                // Any other formats using uploaded or alternate format for thumbnails?
     2674                                $alt_mime_types = array_merge( $alt_mime_types, array_keys( array_intersect( $output_formats, $alt_mime_types ) ) );
     2675
     2676                                // Remove uploaded mime type as we've already tested that.
     2677                                $alt_mime_types = array_diff( $alt_mime_types, array( $mime_type ) );
     2678                        }
     2679
     2680                        if ( ! empty( $alt_mime_types ) ) {
     2681                                $alt_mime_types = array_unique( $alt_mime_types );
     2682
     2683                                foreach ( $alt_mime_types as $alt_mime_type ) {
     2684                                        $alt_ext = wp_get_default_extension_for_mime_type( $alt_mime_type );
     2685
     2686                                        if ( ! empty( $alt_ext ) && ".{$alt_ext}" !== $ext ) {
     2687                                                $alt_filename  = wp_basename( $filename, $ext ) . ".{$alt_ext}";
     2688                                                $alt_filename2 = wp_unique_filename( $dir, $alt_filename );
     2689
     2690                                                // If a potential clash was found for alternate format, use its unique filename.
     2691                                                if ( $alt_filename2 !== $alt_filename ) {
     2692                                                        $filename         = wp_basename( $alt_filename2, ".{$alt_ext}" ) . $ext;
     2693                                                        $filename_changed = true;
     2694                                                }
     2695                                        }
     2696                                }
     2697                        }
     2698                }
     2699                $checking_alternates = false;
     2700
     2701                // Double check that incremented filename for original type or first tested alternate types do not clash.
     2702                if ( $filename_changed ) {
     2703
     2704                        return wp_unique_filename( $dir, $filename );
     2705                }
     2706        }
     2707
     2708        return $filename;
     2709}
     2710
     2711/**
    26482712 * Create a file in the upload folder with given content.
    26492713 *
    26502714 * If there is an error, then the key 'error' will exist with the error message.
     
    30423106}
    30433107
    30443108/**
     3109 * Returns first matched extension from Mime-type,
     3110 * as mapped from wp_get_mime_types()
     3111 *
     3112 * @since 5.8.1
     3113 *
     3114 * @param string $mime_type
     3115 *
     3116 * @return string|false
     3117 *
     3118 */
     3119function wp_get_default_extension_for_mime_type( $mime_type ) {
     3120        $extensions = explode( '|', array_search( $mime_type, wp_get_mime_types(), true ) );
     3121
     3122        if ( empty( $extensions[0] ) ) {
     3123                return false;
     3124        }
     3125
     3126        return $extensions[0];
     3127}
     3128
     3129/**
    30453130 * Returns the real mime type of an image file.
    30463131 *
    30473132 * This depends on exif_imagetype() or getimagesize() to determine real mime types.
  • tests/phpunit/tests/functions.php

     
    222222        }
    223223
    224224        /**
     225         * @ticket 53668
     226         */
     227        function test__wp_check_alternate_output_format_uniqueness() {
     228                $testdir = DIR_TESTDATA . '/images/';
     229
     230                add_filter( 'upload_dir', array( $this, 'upload_dir_patch_basedir' ) );
     231
     232                // Standard test that wp_unique_filename allows usage if file does not exist yet.
     233                $this->assertSame( 'abcdef.png', wp_unique_filename( $testdir, 'abcdef.png' ), 'The abcdef.png image does not exist. The name does not need to be made unique.' );
     234                // Difference in extension does not affect wp_unique_filename by default (canola.jpg exists).
     235                $this->assertSame( 'canola.png', wp_unique_filename( $testdir, 'canola.png' ), 'The canola.jpg image exists. Clashing base filename but not extension should not have name changed.' );
     236                // Run again with upper case extension.
     237                $this->assertSame( 'canola.png', wp_unique_filename( $testdir, 'canola.PNG' ), 'The canola.jpg image exists. Clashing base filename but not extension should not have name changed.' );
     238                // Actual clash recognized.
     239                $this->assertSame( 'canola-1.jpg', wp_unique_filename( $testdir, 'canola.jpg' ), 'The canola.jpg image exists. Uploading canola.jpg again should have unique name.' );
     240                // Future clash by regenerated thumbnails not applicable.
     241                $this->assertSame( 'codeispoetry.jpg', wp_unique_filename( $testdir, 'codeispoetry.jpg' ), 'The codeispoetry.png image exists. Uploading codeispoetry.jpg does not need unique name.' );
     242
     243                // When creating sub-sizes convert uploaded PNG images to JPG.
     244                add_filter( 'image_editor_output_format', array( $this, 'image_editor_output_format_handler' ) );
     245
     246                // Standard test that wp_unique_filename allows usage if file does not exist yet.
     247                $this->assertSame( 'abcdef.png', wp_unique_filename( $testdir, 'abcdef.png' ), 'The abcdef.png image does not exist. Its name should not be changed.' );
     248                // Standard test that wp_unique_filename allows usage if file does not exist yet.
     249                $this->assertSame( 'abcdef.bmp', wp_unique_filename( $testdir, 'abcdef.bmp' ), 'The abcdef.bmp and abcdef.pct images do not exist. When uploading abcdef.bmp its name should not be changed.' );
     250                // Difference in extension does affect wp_unique_filename when thumbnails use existing file's type.
     251                $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.' );
     252                // Run again with upper case extension.
     253                $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.' );
     254                // Actual clash recognized.
     255                $this->assertSame( 'canola-1.jpg', wp_unique_filename( $testdir, 'canola.jpg' ), 'Existing file should have name changed.' );
     256                // Actual clash with images with different extensions.
     257                $this->assertSame( 'test-image-3.png', wp_unique_filename( $testdir, 'test-image.png' ), 'The test-image.png, test-image-1-100x100.jpg, and test-image-2.gif images exist. All of them may be intersected when creating sub-sizes for the new image: test-image.png, so its filename should be unique.' );
     258                // Future clash by regenerated thumbnails recognized.
     259                $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.' );
     260
     261                // Ensure plugins take part in analysis of alternate extensions.
     262                add_filter( 'wp_unique_filename', array( $this, 'wp_unique_filename_handler' ), 10, 4 );
     263
     264                // Actual clash with images with different extensions.
     265                $this->assertSame( 'test-image-4.png', wp_unique_filename( $testdir, 'test-image.png' ), 'The test-image.png, test-image-1-100x100.jpg, test-image-2.gif and offloaded test-image-3.jpg images exist. All of them may be intersected when creating sub-sizes for the new image: test-image.png, so its filename should be unique.' );
     266
     267                remove_filter( 'wp_unique_filename', array( $this, 'wp_unique_filename_handler' ) );
     268                remove_filter( 'image_editor_output_format', array( $this, 'image_editor_output_format_handler' ) );
     269                remove_filter( 'upload_dir', array( $this, 'upload_dir_patch_basedir' ) );
     270        }
     271
     272        /**
     273         * Changes the output format when editing images. When uploading a PNG file
     274         * it will be converted to JPG (if the image editor in PHP supports it).
     275         *
     276         * @param array $formats
     277         *
     278         * @return array
     279         */
     280        public function image_editor_output_format_handler( $formats ) {
     281                $formats['image/png'] = 'image/jpeg';
     282                $formats['image/gif'] = 'image/jpeg';
     283                $formats['image/pct'] = 'image/bmp';
     284
     285                return $formats;
     286        }
     287
     288        /**
     289         * Pretend that a filename has been uploaded but removed from the server's filesystem
     290         * e.g. by an offloader plugin on a cluster with ephemeral filesystem.
     291         *
     292         * @param string   $filename
     293         * @param string   $orig_ext
     294         * @param string   $dir
     295         * @param callable $unique_filename_callback
     296         *
     297         * @return string
     298         */
     299        public function wp_unique_filename_handler( $filename, $orig_ext, $dir, $unique_filename_callback ) {
     300                if ( 'test-image-3.jpg' === $filename ) {
     301                        // Bump the filename version and re-check.
     302                        $filename = 'test-image-4.jpg';
     303                        $filename = wp_unique_filename( $dir, $filename );
     304                }
     305
     306                return $filename;
     307        }
     308
     309        /**
    225310         * @dataProvider data_is_not_serialized
    226311         */
    227312        function test_maybe_serialize( $value ) {
     
    19462031                        array( 'application/activity+json, application/nojson', true ),
    19472032                );
    19482033        }
     2034
     2035        /**
     2036         * @ticket 53668
     2037         */
     2038        public function test_wp_get_default_extension_for_mime_type() {
     2039                $this->assertEquals( 'jpg', wp_get_default_extension_for_mime_type( 'image/jpeg' ), 'jpg not returned as default extension for "image/jpeg"' );
     2040                $this->assertNotEquals( 'jpeg', wp_get_default_extension_for_mime_type( 'image/jpeg' ), 'jpeg should not be returned as default extension for "image/jpeg"' );
     2041                $this->assertEquals( 'png', wp_get_default_extension_for_mime_type( 'image/png' ), 'png not returned as default extension for "image/png"' );
     2042                $this->assertFalse( wp_get_default_extension_for_mime_type( 'wibble/wobble' ), 'false not returned for unrecognized mime type' );
     2043                $this->assertFalse( wp_get_default_extension_for_mime_type( '' ), 'false not returned when empty string as mime type supplied' );
     2044                $this->assertFalse( wp_get_default_extension_for_mime_type( '   ' ), 'false not returned when empty string as mime type supplied' );
     2045                $this->assertFalse( wp_get_default_extension_for_mime_type( 123 ), 'false not returned when int as mime type supplied' );
     2046                $this->assertFalse( wp_get_default_extension_for_mime_type( null ), 'false not returned when null as mime type supplied' );
     2047        }
    19492048}