From 9c1eb3e87b5c9c0cbe173e68a14cf4f90f117395 Mon Sep 17 00:00:00 2001 From: Dhrupo Nil Date: Sat, 27 Jun 2026 10:35:08 +0600 Subject: [PATCH 1/3] Plugins: Match translated plugin data in the Installed Plugins search. The Installed Plugins list table displays translated plugin names, descriptions, and authors, but the search filtered against the raw (untranslated) plugin headers only. As a result, searching by the displayed (translated) name returned no results, even though the Add Plugins screen already matches translated names. Pass the plugin file to WP_Plugins_List_Table::_search_callback() via ARRAY_FILTER_USE_BOTH and also match against the translated plugin data from _get_plugin_data_markup_translate(), while still matching the original headers so searches by the untranslated value keep working. Adds regression tests covering search by translated name, original name, and a non-matching term. Fixes #64188. --- .../includes/class-wp-plugins-list-table.php | 31 +++- .../tests/admin/wpPluginsListTable.php | 148 ++++++++++++++++++ 2 files changed, 173 insertions(+), 6 deletions(-) diff --git a/src/wp-admin/includes/class-wp-plugins-list-table.php b/src/wp-admin/includes/class-wp-plugins-list-table.php index 08b2e982e702f..e5a05b79b51ff 100644 --- a/src/wp-admin/includes/class-wp-plugins-list-table.php +++ b/src/wp-admin/includes/class-wp-plugins-list-table.php @@ -306,7 +306,7 @@ public function prepare_items() { if ( strlen( $s ) ) { $status = 'search'; - $plugins['search'] = array_filter( $plugins['all'], array( $this, '_search_callback' ) ); + $plugins['search'] = array_filter( $plugins['all'], array( $this, '_search_callback' ), ARRAY_FILTER_USE_BOTH ); } /** @@ -382,15 +382,34 @@ public function prepare_items() { * * @global string $s URL encoded search term. * - * @param array $plugin Plugin data array to check against the search term. + * @param array $plugin Plugin data array to check against the search term. + * @param string $plugin_file Optional. Plugin file path relative to the plugins + * directory. When provided, the plugin's translated data + * is also searched so that the search matches the + * translated name, description, and author shown in the + * list table. Default empty string. * @return bool True if the plugin matches the search term, false otherwise. */ - public function _search_callback( $plugin ) { + public function _search_callback( $plugin, $plugin_file = '' ) { global $s; - foreach ( $plugin as $value ) { - if ( is_string( $value ) && false !== stripos( strip_tags( $value ), urldecode( $s ) ) ) { - return true; + $term = urldecode( $s ); + + /* + * Search the original (untranslated) plugin data as well as the translated + * data shown in the list table, so searches match either the value in the + * plugin's file header or the localized value displayed to the user. + */ + $plugin_data_sets = array( $plugin ); + if ( '' !== $plugin_file ) { + $plugin_data_sets[] = _get_plugin_data_markup_translate( $plugin_file, $plugin, false, true ); + } + + foreach ( $plugin_data_sets as $plugin_data ) { + foreach ( $plugin_data as $value ) { + if ( is_string( $value ) && false !== stripos( strip_tags( $value ), $term ) ) { + return true; + } } } diff --git a/tests/phpunit/tests/admin/wpPluginsListTable.php b/tests/phpunit/tests/admin/wpPluginsListTable.php index daa54750fdf9e..9e56c6aeb5d0a 100644 --- a/tests/phpunit/tests/admin/wpPluginsListTable.php +++ b/tests/phpunit/tests/admin/wpPluginsListTable.php @@ -337,4 +337,152 @@ public function plugins_list_filter( $plugins_list ) { return $plugins_list; } + + /** + * Tests that the Installed Plugins search matches the translated plugin + * data shown in the list table, not only the original (untranslated) headers. + * + * The list table displays translated plugin names, but the search used to + * filter against the raw plugin headers only, so searching by the displayed + * (translated) name returned no results. + * + * @ticket 64188 + * + * @covers WP_Plugins_List_Table::prepare_items + * @covers WP_Plugins_List_Table::_search_callback + */ + public function test_search_matches_translated_plugin_data() { + global $status, $s; + + wp_set_current_user( self::$admin_id ); + + $old_status = $status; + $status = 'all'; + + // The search term only appears in the translated name, not the raw header. + $s = 'Wtyczka'; + + add_filter( 'all_plugins', array( $this, 'inject_translatable_plugin' ) ); + add_filter( 'gettext', array( $this, 'filter_translate_plugin_name' ), 10, 3 ); + + $this->table->prepare_items(); + $items = $this->table->items; + + remove_filter( 'gettext', array( $this, 'filter_translate_plugin_name' ), 10 ); + remove_filter( 'all_plugins', array( $this, 'inject_translatable_plugin' ) ); + + $status = $old_status; + + $this->assertArrayHasKey( + 'fake-translated-plugin/fake-translated-plugin.php', + $items, + 'The plugin should be found when searching by its translated name.' + ); + } + + /** + * Tests that the Installed Plugins search still matches the original + * (untranslated) plugin headers, and does not match unrelated terms. + * + * @ticket 64188 + * + * @covers WP_Plugins_List_Table::prepare_items + * @covers WP_Plugins_List_Table::_search_callback + * + * @dataProvider data_search_terms_against_translatable_plugin + * + * @param string $search_term The value of the `$s` search global. + * @param bool $should_match Whether the injected plugin should be in the results. + */ + public function test_search_against_original_plugin_data( $search_term, $should_match ) { + global $status, $s; + + wp_set_current_user( self::$admin_id ); + + $old_status = $status; + $status = 'all'; + $s = $search_term; + + add_filter( 'all_plugins', array( $this, 'inject_translatable_plugin' ) ); + add_filter( 'gettext', array( $this, 'filter_translate_plugin_name' ), 10, 3 ); + + $this->table->prepare_items(); + $items = $this->table->items; + + remove_filter( 'gettext', array( $this, 'filter_translate_plugin_name' ), 10 ); + remove_filter( 'all_plugins', array( $this, 'inject_translatable_plugin' ) ); + + $status = $old_status; + + if ( $should_match ) { + $this->assertArrayHasKey( + 'fake-translated-plugin/fake-translated-plugin.php', + $items, + "The plugin should be found when searching for '{$search_term}'." + ); + } else { + $this->assertArrayNotHasKey( + 'fake-translated-plugin/fake-translated-plugin.php', + $items, + "The plugin should not be found when searching for '{$search_term}'." + ); + } + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_search_terms_against_translatable_plugin() { + return array( + 'original (untranslated) name' => array( 'Fake Translated Plugin', true ), + 'translated name' => array( 'Wtyczka', true ), + 'unrelated term' => array( 'NoSuchPluginNameHere', false ), + ); + } + + /** + * Injects a plugin whose translated name differs from its file header name. + * + * Used as a callback for the 'all_plugins' hook. + * + * @param array $all_plugins Array of plugin data keyed by plugin file. + * @return array + */ + public function inject_translatable_plugin( $all_plugins ) { + $all_plugins['fake-translated-plugin/fake-translated-plugin.php'] = array( + 'Name' => 'Fake Translated Plugin', + 'PluginURI' => 'https://wordpress.org/', + 'Version' => '1.0.0', + 'Description' => 'A fake plugin for testing translated search.', + 'Author' => 'WordPress', + 'AuthorURI' => 'https://wordpress.org/', + 'TextDomain' => 'fake-translated-plugin', + 'DomainPath' => '', + 'Network' => false, + 'Title' => 'Fake Translated Plugin', + 'AuthorName' => 'WordPress', + ); + + return $all_plugins; + } + + /** + * Simulates a translation of the fake plugin's name, as a loaded .mo file would. + * + * Used as a callback for the 'gettext' hook. + * + * @param string $translation Translated text. + * @param string $text Text to translate. + * @param string $domain Text domain. + * @return string + */ + public function filter_translate_plugin_name( $translation, $text, $domain ) { + if ( 'fake-translated-plugin' === $domain && 'Fake Translated Plugin' === $text ) { + return 'Wtyczka testowa'; + } + + return $translation; + } } From a26c5ccd97c1ccb0ebd55525cb6fdc2c0a2d54bc Mon Sep 17 00:00:00 2001 From: Dhrupo Nil Date: Sun, 28 Jun 2026 16:19:03 +0600 Subject: [PATCH 2/3] Plugins: Use str_contains() in the Installed Plugins search callback. Replace the false !== stripos() idiom with str_contains() per review feedback. The search term and each value are lowercased first so the search stays case-insensitive, and tests are added to cover matching across different letter cases. --- src/wp-admin/includes/class-wp-plugins-list-table.php | 5 +++-- tests/phpunit/tests/admin/wpPluginsListTable.php | 9 ++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/wp-admin/includes/class-wp-plugins-list-table.php b/src/wp-admin/includes/class-wp-plugins-list-table.php index e5a05b79b51ff..0e8eaa53a3622 100644 --- a/src/wp-admin/includes/class-wp-plugins-list-table.php +++ b/src/wp-admin/includes/class-wp-plugins-list-table.php @@ -393,7 +393,8 @@ public function prepare_items() { public function _search_callback( $plugin, $plugin_file = '' ) { global $s; - $term = urldecode( $s ); + // Lowercase the term so the search stays case-insensitive. + $term = strtolower( urldecode( $s ) ); /* * Search the original (untranslated) plugin data as well as the translated @@ -407,7 +408,7 @@ public function _search_callback( $plugin, $plugin_file = '' ) { foreach ( $plugin_data_sets as $plugin_data ) { foreach ( $plugin_data as $value ) { - if ( is_string( $value ) && false !== stripos( strip_tags( $value ), $term ) ) { + if ( is_string( $value ) && str_contains( strtolower( strip_tags( $value ) ), $term ) ) { return true; } } diff --git a/tests/phpunit/tests/admin/wpPluginsListTable.php b/tests/phpunit/tests/admin/wpPluginsListTable.php index 9e56c6aeb5d0a..4eea54428049d 100644 --- a/tests/phpunit/tests/admin/wpPluginsListTable.php +++ b/tests/phpunit/tests/admin/wpPluginsListTable.php @@ -436,9 +436,12 @@ public function test_search_against_original_plugin_data( $search_term, $should_ */ public function data_search_terms_against_translatable_plugin() { return array( - 'original (untranslated) name' => array( 'Fake Translated Plugin', true ), - 'translated name' => array( 'Wtyczka', true ), - 'unrelated term' => array( 'NoSuchPluginNameHere', false ), + 'original (untranslated) name' => array( 'Fake Translated Plugin', true ), + 'translated name' => array( 'Wtyczka', true ), + 'original name, different case' => array( 'fake translated plugin', true ), + 'translated name, different case' => array( 'wtyczka', true ), + 'translated name, upper case' => array( 'WTYCZKA', true ), + 'unrelated term' => array( 'NoSuchPluginNameHere', false ), ); } From 0696e1a7020b8ca2d312d1f8b804c2b86852176c Mon Sep 17 00:00:00 2001 From: Dhrupo Nil Date: Sun, 28 Jun 2026 16:27:21 +0600 Subject: [PATCH 3/3] Plugins: Align data provider arrows in plugins list table test. Fixes a WordPress.Arrays.MultipleStatementAlignment warning flagged by the test suite coding-standards scan. --- tests/phpunit/tests/admin/wpPluginsListTable.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/phpunit/tests/admin/wpPluginsListTable.php b/tests/phpunit/tests/admin/wpPluginsListTable.php index 4eea54428049d..23ce3c10b5451 100644 --- a/tests/phpunit/tests/admin/wpPluginsListTable.php +++ b/tests/phpunit/tests/admin/wpPluginsListTable.php @@ -436,12 +436,12 @@ public function test_search_against_original_plugin_data( $search_term, $should_ */ public function data_search_terms_against_translatable_plugin() { return array( - 'original (untranslated) name' => array( 'Fake Translated Plugin', true ), - 'translated name' => array( 'Wtyczka', true ), - 'original name, different case' => array( 'fake translated plugin', true ), - 'translated name, different case' => array( 'wtyczka', true ), - 'translated name, upper case' => array( 'WTYCZKA', true ), - 'unrelated term' => array( 'NoSuchPluginNameHere', false ), + 'original (untranslated) name' => array( 'Fake Translated Plugin', true ), + 'translated name' => array( 'Wtyczka', true ), + 'original name, different case' => array( 'fake translated plugin', true ), + 'translated name, different case' => array( 'wtyczka', true ), + 'translated name, upper case' => array( 'WTYCZKA', true ), + 'unrelated term' => array( 'NoSuchPluginNameHere', false ), ); }