Make WordPress Core

Changeset 49744


Ignore:
Timestamp:
12/03/2020 08:37:43 PM (6 years 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.