Make WordPress Core

Ticket #53668: 53668.4.diff

File 53668.4.diff, 14.0 KB (added by ianmjones, 3 years ago)

More robust handling of versioned files, uppercase extensions including on pre-existing files, and re-analysis of of original upload type previously tested alternate types in response to filename versioning. Inspired by @azaozz's alternate PR, and includes his changes for tests and wp_get_default_mime_type_extension.

  • 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        $ext      = strtolower( $ext );
     2505        $filename = pathinfo( $filename, PATHINFO_FILENAME ) . $ext;
     2506
    25042507        /*
    25052508         * Increment the file number until we have a unique file to save in $dir.
    25062509         * Use callback if supplied.
     
    25112514                $number = '';
    25122515                $fname  = pathinfo( $filename, PATHINFO_FILENAME );
    25132516
     2517                // If filename already versioned, get version and un-versioned filename.
     2518                if ( preg_match( '/-(\d)$/', $fname, $matches ) ) {
     2519                        $fname  = preg_replace( '/' . $matches[0] . '$/', '', $fname );
     2520                        $number = (int) $matches[1];
     2521                }
     2522
    25142523                // Always append a number to file names that can potentially match image sub-size file names.
    25152524                if ( $fname && preg_match( '/-(?:\d+x\d+|scaled|rotated)$/', $fname ) ) {
    2516                         $number = 1;
     2525                        $number = (int) $number + 1;
    25172526
    25182527                        // At this point the file name may not be unique. This is tested below and the $number is incremented.
    25192528                        $filename = str_replace( "{$fname}{$ext}", "{$fname}-{$number}{$ext}", $filename );
    25202529                }
    25212530
    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 );
     2531                // Check for both lower and upper case extension or image sub-sizes may be overwritten.
     2532                $uc_ext          = strtoupper( $ext );
     2533                $uc_ext_filename = preg_replace( '|' . preg_quote( $ext ) . '$|', $uc_ext, $filename );
    25262534
    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                         }
     2535                while ( file_exists( $dir . "/{$filename}" ) || file_exists( $dir . "/{$uc_ext_filename}" ) ) {
     2536                        $new_number      = (int) $number + 1;
     2537                        $filename        = str_replace( array( "{$fname}-{$number}{$ext}", "{$fname}{$number}{$ext}" ), "{$fname}-{$new_number}{$ext}", $filename );
     2538                        $uc_ext_filename = str_replace( array( "{$fname}-{$number}{$uc_ext}", "{$fname}{$number}{$uc_ext}" ), "{$fname}-{$new_number}{$uc_ext}", $uc_ext_filename );
     2539                        $number          = $new_number;
    25482540                }
    25492541
    25502542                // Prevent collisions with existing file names that contain dimension-like strings
     
    25792571                        }
    25802572
    25812573                        if ( ! empty( $files ) ) {
    2582                                 // The extension case may have changed above.
    2583                                 $new_ext = ! empty( $ext2 ) ? $ext2 : $ext;
    2584 
    25852574                                // Ensure this never goes into infinite loop
    25862575                                // as it uses pathinfo() and regex in the check, but string replacement for the changes.
    25872576                                $count = count( $files );
     
    25892578
    25902579                                while ( $i <= $count && _wp_check_existing_file_names( $filename, $files ) ) {
    25912580                                        $new_number = (int) $number + 1;
    2592                                         $filename   = str_replace( array( "-{$number}{$new_ext}", "{$number}{$new_ext}" ), "-{$new_number}{$new_ext}", $filename );
     2581                                        $filename   = str_replace( array( "{$fname}-{$number}{$ext}", "{$fname}{$number}{$ext}" ), "{$fname}-{$new_number}{$ext}", $filename );
    25932582                                        $number     = $new_number;
    25942583                                        $i++;
    25952584                                }
    25962585                        }
    25972586                }
     2587
     2588                // If a different file type might be produced for an image, check filename uniqueness for that format.
     2589                $filename = _wp_check_alternate_output_format_uniqueness( $filename, $ext, $dir );
    25982590        }
    25992591
    26002592        /**
     
    26452637}
    26462638
    26472639/**
     2640 * Helper function for wp_unique_filename to check potential alternate output formats for images.
     2641 *
     2642 * @since 5.8.1
     2643 * @private
     2644 *
     2645 * @param string $filename
     2646 * @param string $ext
     2647 * @param string $dir
     2648 *
     2649 * @return string
     2650 */
     2651function _wp_check_alternate_output_format_uniqueness( $filename, $ext, $dir ) {
     2652        static $checking_alternates;
     2653
     2654        if ( empty( $checking_alternates ) ) {
     2655                $checking_alternates = true;
     2656                $filename_changed    = false;
     2657                $file_type           = wp_check_filetype_and_ext( trailingslashit( $dir ) . $filename, $filename );
     2658                $mime_type           = $file_type['type'];
     2659
     2660                if ( ! empty( $mime_type ) && 0 === strpos( $mime_type, 'image/' ) ) {
     2661                        $output_formats = apply_filters( 'image_editor_output_format', array(), trailingslashit( $dir ) . $filename, $mime_type );
     2662
     2663                        if ( ! empty( $output_formats ) && is_array( $output_formats ) ) {
     2664                                // Temporarily add uploaded type to alts for simpler analysis.
     2665                                $alt_mime_types = array( $mime_type );
     2666
     2667                                // Alternate thumbnail format for uploaded file?
     2668                                if ( ! empty( $output_formats[ $mime_type ] ) ) {
     2669                                        $alt_mime_types[] = $output_formats[ $mime_type ];
     2670                                }
     2671
     2672                                // Any other formats using uploaded or alternate format for thumbnails?
     2673                                $alt_mime_types = array_merge( $alt_mime_types, array_keys( array_intersect( $output_formats, $alt_mime_types ) ) );
     2674
     2675                                // Remove uploaded mime type as we've already tested that.
     2676                                $alt_mime_types = array_diff( $alt_mime_types, array( $mime_type ) );
     2677                        }
     2678
     2679                        if ( ! empty( $alt_mime_types ) ) {
     2680                                $alt_mime_types = array_unique( $alt_mime_types );
     2681
     2682                                foreach ( $alt_mime_types as $alt_mime_type ) {
     2683                                        $alt_ext = wp_get_default_extension_for_mime_type( $alt_mime_type );
     2684
     2685                                        if ( ! empty( $alt_ext ) && ".{$alt_ext}" !== $ext ) {
     2686                                                $alt_filename  = wp_basename( $filename, $ext ) . ".{$alt_ext}";
     2687                                                $alt_filename2 = wp_unique_filename( $dir, $alt_filename );
     2688
     2689                                                // If a potential clash was found for alternate format, use its unique filename.
     2690                                                if ( $alt_filename2 !== $alt_filename ) {
     2691                                                        $filename         = wp_basename( $alt_filename2, ".{$alt_ext}" ) . $ext;
     2692                                                        $filename_changed = true;
     2693                                                }
     2694                                        }
     2695                                }
     2696                        }
     2697                }
     2698                $checking_alternates = false;
     2699
     2700                // Double check that incremented filename for original type or first tested alternate types do not clash.
     2701                if ( $filename_changed ) {
     2702
     2703                        return wp_unique_filename( $dir, $filename );
     2704                }
     2705        }
     2706
     2707        return $filename;
     2708}
     2709
     2710/**
    26482711 * Create a file in the upload folder with given content.
    26492712 *
    26502713 * If there is an error, then the key 'error' will exist with the error message.
     
    30423105}
    30433106
    30443107/**
     3108 * Returns first matched extension from Mime-type,
     3109 * as mapped from wp_get_mime_types()
     3110 *
     3111 * @since 5.8.1
     3112 *
     3113 * @param string $mime_type
     3114 *
     3115 * @return string|false
     3116 *
     3117 */
     3118function wp_get_default_extension_for_mime_type( $mime_type ) {
     3119        $extensions = explode( '|', array_search( $mime_type, wp_get_mime_types(), true ) );
     3120
     3121        if ( empty( $extensions[0] ) ) {
     3122                return false;
     3123        }
     3124
     3125        return $extensions[0];
     3126}
     3127
     3128/**
    30453129 * Returns the real mime type of an image file.
    30463130 *
    30473131 * 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                remove_filter( 'image_editor_output_format', array( $this, 'image_editor_output_format_handler' ) );
     262                remove_filter( 'upload_dir', array( $this, 'upload_dir_patch_basedir' ) );
     263        }
     264
     265        /**
     266         * Changes the output format when editing images. When uploading a PNG file
     267         * it will be converted to JPG (if the image editor in PHP supports it).
     268         *
     269         * @param array $formats
     270         *
     271         * @return array
     272         */
     273        public function image_editor_output_format_handler( $formats ) {
     274                $formats['image/png'] = 'image/jpeg';
     275                $formats['image/gif'] = 'image/jpeg';
     276                $formats['image/pct'] = 'image/bmp';
     277
     278                return $formats;
     279        }
     280
     281        /**
    225282         * @dataProvider data_is_not_serialized
    226283         */
    227284        function test_maybe_serialize( $value ) {
     
    19462003                        array( 'application/activity+json, application/nojson', true ),
    19472004                );
    19482005        }
     2006
     2007        /**
     2008         * @ticket 53668
     2009         */
     2010        public function test_wp_get_default_extension_for_mime_type() {
     2011                $this->assertEquals( 'jpg', wp_get_default_extension_for_mime_type( 'image/jpeg' ), 'jpg not returned as default extension for "image/jpeg"' );
     2012                $this->assertNotEquals( 'jpeg', wp_get_default_extension_for_mime_type( 'image/jpeg' ), 'jpeg should not be returned as default extension for "image/jpeg"' );
     2013                $this->assertEquals( 'png', wp_get_default_extension_for_mime_type( 'image/png' ), 'png not returned as default extension for "image/png"' );
     2014                $this->assertFalse( wp_get_default_extension_for_mime_type( 'wibble/wobble' ), 'false not returned for unrecognized mime type' );
     2015                $this->assertFalse( wp_get_default_extension_for_mime_type( '' ), 'false not returned when empty string as mime type supplied' );
     2016                $this->assertFalse( wp_get_default_extension_for_mime_type( '   ' ), 'false not returned when empty string as mime type supplied' );
     2017                $this->assertFalse( wp_get_default_extension_for_mime_type( 123 ), 'false not returned when int as mime type supplied' );
     2018                $this->assertFalse( wp_get_default_extension_for_mime_type( null ), 'false not returned when null as mime type supplied' );
     2019        }
    19492020}