Make WordPress Core


Ignore:
Timestamp:
12/03/2020 08:43:53 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.
Reviewed by SergeyBiryukov, iandunn.
Merges [49744] to the 5.6 branch.
Fixes #51913. See #19879.

Location:
branches/5.6
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • branches/5.6

  • branches/5.6/tests/phpunit/tests/multisite/cleanDirsizeCache.php

    r49630 r49745  
    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.