Make WordPress Core

Changeset 49744


Ignore:
Timestamp:
12/03/2020 08:37:43 PM (14 months ago)
Author:
iandunn
Message:

Multisite: Cache absolute dirsize paths to avoid PHP 8 fatal.

r49212 greatly improved the performance of get_dirsize(), but also changed the structure of the data stored in the dirsize_cache transient. It stored relative paths instead of absolute ones, and also removed the unnecessary size array.

That difference in data structures led to a fatal error in the following environment:

  • PHP 8
  • Multisite
  • A custom WP_CONTENT_DIR which is not a child of WP's ABSPATH folder (e.g., Bedrock)
  • The upload_space_check_disabled option set to 0

After upgrading to WP 5.6, the dirsize_cache transient still had data in the old format. When wp-admin.php/index.php was visited, get_space_used() received an array instead of an int, and tried to divide it by another int. PHP 7 would silently cast the arguments to match data types, but PHP 8 throws a fatal error:

Uncaught TypeError: Unsupported operand types: array / int

recurse_dirsize() was using ABSPATH to convert the absolute paths to relative ones, but some upload locations are not located under ABSPATH. In those cases, $directory and $cache_path were identical, and that triggered the early return of the old array, instead of the expected int.

In order to avoid that, this commit restores the absolute paths, but without the size array. It also adds a type check when returning cached values. Using absolute paths without size has the result of overwriting the old data, so that it matches the new format. The type check and upgrade routine are additional safety measures.

Props peterwilsoncc, janthiel, helen, hellofromtonya, francina, pbiron.
Fixes #51913. See #19879.

Location:
trunk
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/includes/upgrade.php

    r49671 r49744  
    875875    }
    876876
    877     if ( $wp_current_db_version < 49632 ) {
     877    if ( $wp_current_db_version < 49735 ) {
    878878        upgrade_560();
    879879    }
     
    22752275        save_mod_rewrite_rules();
    22762276    }
     2277
     2278    if ( $wp_current_db_version < 49735 ) {
     2279        delete_transient( 'dirsize_cache' );
     2280    }
    22772281}
    22782282
  • trunk/src/wp-includes/functions.php

    r49693 r49744  
    76277627function recurse_dirsize( $directory, $exclude = null, $max_execution_time = null, &$directory_cache = null ) {
    76287628    $directory  = untrailingslashit( $directory );
    7629     $cache_path = untrailingslashit( str_replace( ABSPATH, '', $directory ) );
    7630 
    76317629    $save_cache = false;
    76327630
     
    76367634    }
    76377635
    7638     if ( isset( $directory_cache[ $cache_path ] ) ) {
    7639         return $directory_cache[ $cache_path ];
     7636    if ( isset( $directory_cache[ $directory ] ) && is_int( $directory_cache[ $directory ] ) ) {
     7637        return $directory_cache[ $directory ];
    76407638    }
    76417639
     
    77067704    }
    77077705
    7708     $directory_cache[ $cache_path ] = $size;
     7706    $directory_cache[ $directory ] = $size;
    77097707
    77107708    // Only write the transient on the top level call and not on recursive calls.
     
    77327730    }
    77337731
    7734     $cache_path = untrailingslashit( str_replace( ABSPATH, '', $path ) );
    7735     unset( $directory_cache[ $cache_path ] );
    7736 
    7737     while ( DIRECTORY_SEPARATOR !== $cache_path && '.' !== $cache_path && '..' !== $cache_path ) {
    7738         $cache_path = dirname( $cache_path );
    7739         unset( $directory_cache[ $cache_path ] );
     7732    $path = untrailingslashit( $path );
     7733    unset( $directory_cache[ $path ] );
     7734
     7735    while ( DIRECTORY_SEPARATOR !== $path && '.' !== $path && '..' !== $path ) {
     7736        $path = dirname( $path );
     7737        unset( $directory_cache[ $path ] );
    77407738    }
    77417739
  • trunk/src/wp-includes/version.php

    r49644 r49744  
    2121 * @global int $wp_db_version
    2222 */
    23 $wp_db_version = 49632;
     23$wp_db_version = 49735;
    2424
    2525/**
  • trunk/tests/phpunit/tests/multisite/cleanDirsizeCache.php

    r49630 r49744  
    9494
    9595            $upload_dir       = wp_upload_dir();
    96             $cache_key_prefix = untrailingslashit( str_replace( ABSPATH, '', $upload_dir['basedir'] ) );
     96            $cache_key_prefix = untrailingslashit( $upload_dir['basedir'] );
    9797
    9898            // Clear the dirsize cache.
     
    142142
    143143            $upload_dir       = wp_upload_dir();
    144             $cache_key_prefix = untrailingslashit( str_replace( ABSPATH, '', $upload_dir['basedir'] ) );
     144            $cache_key_prefix = untrailingslashit( $upload_dir['basedir'] );
    145145
    146146            // Clear the dirsize cache.
     
    206206
    207207            // `dirsize_cache` should now be filled after upload and recurse_dirsize() call.
    208             $cache_path = untrailingslashit( str_replace( ABSPATH, '', $upload_dir['path'] ) );
     208            $cache_path = untrailingslashit( $upload_dir['path'] );
    209209            $this->assertSame( true, is_array( get_transient( 'dirsize_cache' ) ) );
    210210            $this->assertSame( $size, get_transient( 'dirsize_cache' )[ $cache_path ] );
     
    234234
    235235        function _get_mock_dirsize_cache_for_site( $site_id ) {
     236            $prefix = wp_upload_dir()['basedir'];
     237
    236238            return array(
    237                 "wp-content/uploads/sites/$site_id/2/2" => 22,
    238                 "wp-content/uploads/sites/$site_id/2/1" => 21,
    239                 "wp-content/uploads/sites/$site_id/2"   => 2,
    240                 "wp-content/uploads/sites/$site_id/1/3" => 13,
    241                 "wp-content/uploads/sites/$site_id/1/2" => 12,
    242                 "wp-content/uploads/sites/$site_id/1/1" => 11,
    243                 "wp-content/uploads/sites/$site_id/1"   => 1,
    244                 "wp-content/uploads/sites/$site_id/custom_directory" => 42,
     239                "$prefix/2/2"              => 22,
     240                "$prefix/2/1"              => 21,
     241                "$prefix/2"                => 2,
     242                "$prefix/1/3"              => 13,
     243                "$prefix/1/2"              => 12,
     244                "$prefix/1/1"              => 11,
     245                "$prefix/1"                => 1,
     246                "$prefix/custom_directory" => 42,
    245247            );
    246248        }
     249
     250        /*
     251         * Test that 5.6+ gracefully handles the old 5.5 transient structure.
     252         *
     253         * @ticket 51913
     254         */
     255        function test_5_5_transient_structure_compat() {
     256            $blog_id = self::factory()->blog->create();
     257            switch_to_blog( $blog_id );
     258
     259            /*
     260             * Our comparison of space relies on an initial value of 0. If a previous test has failed
     261             * or if the `src` directory already contains a directory with site content, then the initial
     262             * expectation will be polluted. We create sites until an empty one is available.
     263             */
     264            while ( 0 !== get_space_used() ) {
     265                restore_current_blog();
     266                $blog_id = self::factory()->blog->create();
     267                switch_to_blog( $blog_id );
     268            }
     269
     270            // Clear the dirsize cache.
     271            delete_transient( 'dirsize_cache' );
     272
     273            // Set the dirsize cache to our mock.
     274            set_transient( 'dirsize_cache', $this->_get_mock_5_5_dirsize_cache( $blog_id ) );
     275
     276            $upload_dir = wp_upload_dir();
     277
     278            /*
     279             * The cached size should be ignored, because it's in the old format. The function
     280             * will try to fetch a live value, but in this case the folder doesn't actually
     281             * exist on disk, so the function should fail.
     282             */
     283            $this->assertSame( false, recurse_dirsize( $upload_dir['basedir'] . '/2/1' ) );
     284
     285            /*
     286             * Now that it's confirmed that old cached values aren't being returned, create the
     287             * folder on disk, so that the the rest of the function can be tested.
     288             */
     289            wp_mkdir_p( $upload_dir['basedir'] . '/2/1' );
     290            $filename = $upload_dir['basedir'] . '/2/1/this-needs-to-exist.txt';
     291            file_put_contents( $filename, 'this file is 21 bytes' );
     292
     293            // Clear the dirsize cache.
     294            delete_transient( 'dirsize_cache' );
     295
     296            // Set the dirsize cache to our mock.
     297            set_transient( 'dirsize_cache', $this->_get_mock_5_5_dirsize_cache( $blog_id ) );
     298
     299            /*
     300             * Now that the folder exists, the old cached value should be overwritten
     301             * with the size, using the current format.
     302             */
     303            $this->assertSame( 21, recurse_dirsize( $upload_dir['basedir'] . '/2/1' ) );
     304            $this->assertSame( 21, get_transient( 'dirsize_cache' )[ $upload_dir['basedir'] . '/2/1' ] );
     305
     306            // No cache match on non existing directory should return false.
     307            $this->assertSame( false, recurse_dirsize( $upload_dir['basedir'] . '/does_not_exist' ) );
     308
     309            // Cleanup.
     310            $this->remove_added_uploads();
     311            rmdir( $upload_dir['basedir'] . '/2/1' );
     312
     313            restore_current_blog();
     314        }
     315
     316        function _get_mock_5_5_dirsize_cache( $site_id ) {
     317            $prefix = untrailingslashit( wp_upload_dir()['basedir'] );
     318
     319            return array(
     320                "$prefix/2/2"              => array( 'size' => 22 ),
     321                "$prefix/2/1"              => array( 'size' => 21 ),
     322                "$prefix/2"                => array( 'size' => 2 ),
     323                "$prefix/1/3"              => array( 'size' => 13 ),
     324                "$prefix/1/2"              => array( 'size' => 12 ),
     325                "$prefix/1/1"              => array( 'size' => 11 ),
     326                "$prefix/1"                => array( 'size' => 1 ),
     327                "$prefix/custom_directory" => array( 'size' => 42 ),
     328            );
     329        }
    247330    }
    248331
Note: See TracChangeset for help on using the changeset viewer.