From f180a0865e8fbd152a21d61a9fa162b692d5f98f Mon Sep 17 00:00:00 2001 From: Tonya Mork Date: Fri, 15 Oct 2021 22:52:43 +0000 Subject: [PATCH] FileSystem API: Fix autovivification deprecation notice in `recurse_dirsize()`. >PHP natively allows for autovivification (auto-creation of arrays from falsey values). This feature is very useful and used in a lot of PHP projects, especially if the variable is undefined. However, there is a little oddity that allows creating an array from a `false` and `null` value. The above quote is from the PHP 8.1 RFC and the (accepted) RFC changes the behaviour described above to deprecated auto creation of arrays from `false`. As it is deprecated, it _will_ still work for the time being, but as of PHP 9.0, this will become a Fatal Error, so we may as well fix it now. The `recurse_dirsize()` function retrieves a transient and places it in the `$directory_cache` variable, but the `get_transient()` function in WP returns `false` when the transient doesn't exist, which subsequently can lead to the above mentioned deprecation notice. By verifying that the `$directory_cache` variable is an array before assigning to it and initializing it to an empty array, if it's not, we prevent the deprecation notice, as well as harden the function against potentially corrupted transients where this transient would not return the expected array format, but some other variable type. Includes adding dedicated unit tests for both the PHP 8.1 issue, as well as the hardening against corrupted transients. Includes some girl-scouting: touching up a parameter description and some code layout. Refs: * https://wiki.php.net/rfc/autovivification_false * https://developer.wordpress.org/reference/functions/get_transient/ Follow-up to [49212], [49744]. Props jrf, hellofromTonya. See #53635. git-svn-id: https://develop.svn.wordpress.org/trunk@51911 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/functions.php | 13 ++++++++++--- tests/phpunit/tests/functions/cleanDirsizeCache.php | 2 +- tests/phpunit/tests/functions/fixtures/dummy.txt | 1 + 3 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 tests/phpunit/tests/functions/fixtures/dummy.txt diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 81409e9382..86a21a8e78 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -8120,8 +8120,9 @@ function get_dirsize( $directory, $max_execution_time = null ) { * @param string $directory Full path of a directory. * @param string|array $exclude Optional. Full path of a subdirectory to exclude from the total, * or array of paths. Expected without trailing slash(es). - * @param int $max_execution_time Maximum time to run before giving up. In seconds. The timeout is global - * and is measured from the moment WordPress started to load. + * @param int $max_execution_time Optional. Maximum time to run before giving up. In seconds. + * The timeout is global and is measured from the moment + * WordPress started to load. * @param array $directory_cache Optional. Array of cached directory paths. * * @return int|false|null Size in bytes if a valid directory. False if not. Null if timeout. @@ -8194,7 +8195,9 @@ function recurse_dirsize( $directory, $exclude = null, $max_execution_time = nul } } - if ( $max_execution_time > 0 && microtime( true ) - WP_START_TIMESTAMP > $max_execution_time ) { + if ( $max_execution_time > 0 && + ( microtime( true ) - WP_START_TIMESTAMP ) > $max_execution_time + ) { // Time exceeded. Give up instead of risking a fatal timeout. $size = null; break; @@ -8205,6 +8208,10 @@ function recurse_dirsize( $directory, $exclude = null, $max_execution_time = nul } } + if ( ! is_array( $directory_cache ) ) { + $directory_cache = array(); + } + $directory_cache[ $directory ] = $size; // Only write the transient on the top level call and not on recursive calls. diff --git a/tests/phpunit/tests/functions/cleanDirsizeCache.php b/tests/phpunit/tests/functions/cleanDirsizeCache.php index 6ae7d6d2dc..d600bdca54 100644 --- a/tests/phpunit/tests/functions/cleanDirsizeCache.php +++ b/tests/phpunit/tests/functions/cleanDirsizeCache.php @@ -1 +1 @@ -expectNotice(); $this->expectNoticeMessage( $expected_message ); clean_dirsize_cache( $path ); } /** * Data provider. * * @return array */ public function data_clean_dirsize_cache_with_invalid_inputs() { return array( 'null' => array( 'path' => null, 'expected_message' => 'clean_dirsize_cache() only accepts a non-empty path string, received NULL.', ), 'bool false' => array( 'path' => false, 'expected_message' => 'clean_dirsize_cache() only accepts a non-empty path string, received boolean.', ), 'empty string' => array( 'path' => '', 'expected_message' => 'clean_dirsize_cache() only accepts a non-empty path string, received string.', ), 'array' => array( 'path' => array( '.', './second/path/' ), 'expected_message' => 'clean_dirsize_cache() only accepts a non-empty path string, received array.', ), ); } /** * Test the handling of a non-path text string passed as the $path parameter. * * @ticket 52241 * * @dataProvider data_clean_dirsize_cache_with_non_path_string * * @param string $path Path input to use in the test. * @param int $expected_count Expected number of paths in the cache after cleaning. */ public function test_clean_dirsize_cache_with_non_path_string( $path, $expected_count ) { // Set the dirsize cache to our mock. set_transient( 'dirsize_cache', $this->mock_dirsize_cache_with_non_path_string() ); clean_dirsize_cache( $path ); $cache = get_transient( 'dirsize_cache' ); $this->assertIsArray( $cache ); $this->assertCount( $expected_count, $cache ); } /** * Data provider. * * @return array */ public function data_clean_dirsize_cache_with_non_path_string() { return array( 'single dot' => array( 'path' => '.', 'expected_count' => 1, ), 'non-path' => array( 'path' => 'string', 'expected_count' => 1, ), 'non-existant string, but non-path' => array( 'path' => 'doesnotexist', 'expected_count' => 2, ), ); } private function mock_dirsize_cache_with_non_path_string() { return array( '.' => array( 'size' => 50 ), 'string' => array( 'size' => 42 ), ); } } \ No newline at end of file +expectNotice(); $this->expectNoticeMessage( $expected_message ); clean_dirsize_cache( $path ); } /** * Data provider. * * @return array */ public function data_clean_dirsize_cache_with_invalid_inputs() { return array( 'null' => array( 'path' => null, 'expected_message' => 'clean_dirsize_cache() only accepts a non-empty path string, received NULL.', ), 'bool false' => array( 'path' => false, 'expected_message' => 'clean_dirsize_cache() only accepts a non-empty path string, received boolean.', ), 'empty string' => array( 'path' => '', 'expected_message' => 'clean_dirsize_cache() only accepts a non-empty path string, received string.', ), 'array' => array( 'path' => array( '.', './second/path/' ), 'expected_message' => 'clean_dirsize_cache() only accepts a non-empty path string, received array.', ), ); } /** * Test the handling of a non-path text string passed as the $path parameter. * * @ticket 52241 * * @covers ::clean_dirsize_cache * * @dataProvider data_clean_dirsize_cache_with_non_path_string * * @param string $path Path input to use in the test. * @param int $expected_count Expected number of paths in the cache after cleaning. */ public function test_clean_dirsize_cache_with_non_path_string( $path, $expected_count ) { // Set the dirsize cache to our mock. set_transient( 'dirsize_cache', $this->mock_dirsize_cache_with_non_path_string() ); clean_dirsize_cache( $path ); $cache = get_transient( 'dirsize_cache' ); $this->assertIsArray( $cache ); $this->assertCount( $expected_count, $cache ); } /** * Data provider. * * @return array */ public function data_clean_dirsize_cache_with_non_path_string() { return array( 'single dot' => array( 'path' => '.', 'expected_count' => 1, ), 'non-path' => array( 'path' => 'string', 'expected_count' => 1, ), 'non-existant string, but non-path' => array( 'path' => 'doesnotexist', 'expected_count' => 2, ), ); } private function mock_dirsize_cache_with_non_path_string() { return array( '.' => array( 'size' => 50 ), 'string' => array( 'size' => 42 ), ); } /** * Test the behaviour of the function when the transient doesn't exist. * * @ticket 52241 * @ticket 53635 * * @covers ::recurse_dirsize */ public function test_recurse_dirsize_without_transient() { delete_transient( 'dirsize_cache' ); $size = recurse_dirsize( __DIR__ . '/fixtures' ); $this->assertGreaterThan( 10, $size ); } /** * Test the behaviour of the function when the transient does exist, but is not an array. * * In particular, this tests that no PHP TypeErrors are being thrown. * * @ticket 52241 * @ticket 53635 * * @covers ::recurse_dirsize */ public function test_recurse_dirsize_with_invalid_transient() { set_transient( 'dirsize_cache', 'this is not a valid transient for dirsize cache' ); $size = recurse_dirsize( __DIR__ . '/fixtures' ); $this->assertGreaterThan( 10, $size ); } } \ No newline at end of file diff --git a/tests/phpunit/tests/functions/fixtures/dummy.txt b/tests/phpunit/tests/functions/fixtures/dummy.txt new file mode 100644 index 0000000000..7cc0f577ee --- /dev/null +++ b/tests/phpunit/tests/functions/fixtures/dummy.txt @@ -0,0 +1 @@ +This is a dummy text file which is only used by the `Tests_Multisite_CleanDirsizeCache::test_recurse_dirsize_without_transient()` test. \ No newline at end of file