Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 26 additions & 6 deletions src/wp-admin/includes/class-wp-plugins-list-table.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
}

/**
Expand Down Expand Up @@ -382,15 +382,35 @@ public function prepare_items() {
*
* @global string $s URL encoded search term.
*
* @param array<string, mixed> $plugin Plugin data array to check against the search term.
* @param array<string, mixed> $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;
// Lowercase the term so the search stays case-insensitive.
$term = strtolower( 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 ) && str_contains( strtolower( strip_tags( $value ) ), $term ) ) {
return true;
}
}
}

Expand Down
151 changes: 151 additions & 0 deletions tests/phpunit/tests/admin/wpPluginsListTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -337,4 +337,155 @@ 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 ),
'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 ),
);
}

/**
* 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;
}
}
Loading