From 22fba6e0381e7f8e99f10f771478877a76294b05 Mon Sep 17 00:00:00 2001 From: Joseph Scott Date: Mon, 7 Sep 2026 13:40:43 -0600 Subject: [PATCH 1/4] Check cache return value in WP_Textdomain_Registry::get_language_files_from_path() https://core.trac.wordpress.org/ticket/66063 I came across this because I ran into a PHP fatal error from the lack of checking the return value from the cache: > Uncaught TypeError: str_starts_with(): Argument #1 ($haystack) must be of type string, array given Inside of `get_path_from_lang_dir()`. --- src/wp-includes/class-wp-textdomain-registry.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/wp-includes/class-wp-textdomain-registry.php b/src/wp-includes/class-wp-textdomain-registry.php index 7d924d5abdf1d..73ce2e7ee81c5 100644 --- a/src/wp-includes/class-wp-textdomain-registry.php +++ b/src/wp-includes/class-wp-textdomain-registry.php @@ -209,6 +209,13 @@ public function get_language_files_from_path( $path ) { $cache_key = md5( $path ); $files = wp_cache_get( $cache_key, 'translation_files' ); + /* Verify that the cached value is an array and that all entries in that array + * are strings. + */ + if ( ! is_array( $files ) || array_filter( $files, 'is_string' ) !== $files ) { + $files = false; + } + if ( false === $files ) { $files = glob( $path . '*.mo' ); if ( false === $files ) { From 86630fb528a4ad75aec550625de0aac7fc550743 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 9 Sep 2026 09:44:10 +0400 Subject: [PATCH 2/4] Add tests --- .../tests/l10n/wpTextdomainRegistry.php | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/tests/phpunit/tests/l10n/wpTextdomainRegistry.php b/tests/phpunit/tests/l10n/wpTextdomainRegistry.php index 1402a86ce0ad4..446d995c63ec7 100644 --- a/tests/phpunit/tests/l10n/wpTextdomainRegistry.php +++ b/tests/phpunit/tests/l10n/wpTextdomainRegistry.php @@ -111,6 +111,73 @@ public function test_get_language_files_from_path_short_circuit() { $this->assertFalse( $cache ); } + /** + * A cached value that is not an array of strings is discarded and replaced + * by a fresh lookup. + * + * @ticket 66063 + * + * @covers ::get_language_files_from_path + * + * @dataProvider data_get_language_files_from_path_ignores_invalid_cached_values + * + * @param mixed $cached_value Value seeded into the cache. + */ + public function test_get_language_files_from_path_ignores_invalid_cached_values( $cached_value ): void { + $path = WP_LANG_DIR . '/plugins/'; + $cache_key = md5( $path ); + + wp_cache_set( $cache_key, $cached_value, 'translation_files' ); + + $result = $this->instance->get_language_files_from_path( $path ); + + $this->assertIsArray( $result, 'An array should be returned' ); + $this->assertNotEmpty( $result, 'The files should have been looked up instead of using the cached value' ); + $this->assertContainsOnly( 'string', $result, null, 'All returned entries should be strings' ); + $this->assertSame( + $result, + wp_cache_get( $cache_key, 'translation_files' ), + 'The invalid cached value should have been replaced' + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_get_language_files_from_path_ignores_invalid_cached_values(): array { + return array( + 'string' => array( 'not-an-array' ), + 'integer' => array( 1 ), + 'null' => array( null ), + 'object' => array( (object) array( WP_LANG_DIR . '/plugins/foo-de_DE.mo' ) ), + 'array with an array' => array( array( WP_LANG_DIR . '/plugins/foo-de_DE.mo', array( 'bar-de_DE.mo' ) ) ), + 'array with an integer' => array( array( WP_LANG_DIR . '/plugins/foo-de_DE.mo', 1 ) ), + 'array with null' => array( array( null ) ), + ); + } + + /** + * An empty array is a valid cached result for a directory without translation + * files and must not cause a new lookup. + * + * @ticket 66063 + * + * @covers ::get_language_files_from_path + */ + public function test_get_language_files_from_path_keeps_cached_empty_array(): void { + $path = WP_LANG_DIR . '/plugins/'; + + wp_cache_set( md5( $path ), array(), 'translation_files' ); + + $this->assertSame( + array(), + $this->instance->get_language_files_from_path( $path ), + 'A cached empty array should be returned without looking up the files' + ); + } + /** * @covers ::invalidate_mo_files_cache */ From 22e1ad1a85ad15b0dced70905ad73336e44d0c53 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 9 Sep 2026 08:10:26 +0200 Subject: [PATCH 3/4] Fix multiline comment style --- src/wp-includes/class-wp-textdomain-registry.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/class-wp-textdomain-registry.php b/src/wp-includes/class-wp-textdomain-registry.php index 73ce2e7ee81c5..8387d382ec1c0 100644 --- a/src/wp-includes/class-wp-textdomain-registry.php +++ b/src/wp-includes/class-wp-textdomain-registry.php @@ -209,7 +209,8 @@ public function get_language_files_from_path( $path ) { $cache_key = md5( $path ); $files = wp_cache_get( $cache_key, 'translation_files' ); - /* Verify that the cached value is an array and that all entries in that array + /* + * Verify that the cached value is an array and that all entries in that array * are strings. */ if ( ! is_array( $files ) || array_filter( $files, 'is_string' ) !== $files ) { From f3b532811a3c0fc47cd209d7e427c7255574416e Mon Sep 17 00:00:00 2001 From: Joseph Scott Date: Wed, 9 Sep 2026 06:57:53 -0600 Subject: [PATCH 4/4] Use assertIsString instead of assertContainsOnly ``` Deprecation: assertContainsOnly() and assertNotContainsOnly() are deprecated As of PHPUnit 11.5, the assertContainsOnly() and assertNotContainsOnly() methods are hard-deprecated. Using these methods will trigger a deprecation warning. The methods will be removed in PHPUnit 13. ``` From https://docs.phpunit.de/en/12.5/assertions.html --- tests/phpunit/tests/l10n/wpTextdomainRegistry.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/phpunit/tests/l10n/wpTextdomainRegistry.php b/tests/phpunit/tests/l10n/wpTextdomainRegistry.php index 446d995c63ec7..60661830c464c 100644 --- a/tests/phpunit/tests/l10n/wpTextdomainRegistry.php +++ b/tests/phpunit/tests/l10n/wpTextdomainRegistry.php @@ -133,7 +133,9 @@ public function test_get_language_files_from_path_ignores_invalid_cached_values( $this->assertIsArray( $result, 'An array should be returned' ); $this->assertNotEmpty( $result, 'The files should have been looked up instead of using the cached value' ); - $this->assertContainsOnly( 'string', $result, null, 'All returned entries should be strings' ); + foreach ( $result as $file ) { + $this->assertIsString( $file, 'All returned entries should be strings' ); + } $this->assertSame( $result, wp_cache_get( $cache_key, 'translation_files' ),