@@ -24,6 +24,17 @@ class WP_Plugins_List_Table extends WP_List_Table {
2424 */
2525 protected $ show_autoupdates = true ;
2626
27+ /**
28+ * Plugin authors for the "Filter by author" control, keyed by author key.
29+ *
30+ * Each value is an array containing a 'label' and a 'count'.
31+ *
32+ * @since 7.1.0
33+ *
34+ * @var array
35+ */
36+ protected $ plugin_authors = array ();
37+
2738 /**
2839 * Constructor.
2940 *
@@ -311,6 +322,36 @@ public function prepare_items() {
311322 $ totals [ $ type ] = count ( $ list );
312323 }
313324
325+ // Build the list of plugin authors for the "Filter by author" control.
326+ $ this ->plugin_authors = array ();
327+ foreach ( $ plugins ['all ' ] as $ plugin_file => $ plugin_data ) {
328+ $ author_key = $ this ->get_plugin_author_key ( $ plugin_file , $ plugin_data );
329+ if ( '' === $ author_key ) {
330+ continue ;
331+ }
332+ if ( ! isset ( $ this ->plugin_authors [ $ author_key ] ) ) {
333+ $ this ->plugin_authors [ $ author_key ] = array (
334+ 'label ' => ( isset ( $ plugin_data ['AuthorName ' ] ) && '' !== $ plugin_data ['AuthorName ' ] ) ? $ plugin_data ['AuthorName ' ] : $ author_key ,
335+ 'count ' => 0 ,
336+ );
337+ }
338+ ++$ this ->plugin_authors [ $ author_key ]['count ' ];
339+ }
340+
341+ /**
342+ * Filters the authors shown in the Plugins list table "Filter by author" control.
343+ *
344+ * The array is keyed by author key; each value is an array with a 'label' and a 'count'.
345+ * Returning the same key for several plugins merges author-name variants, while removing
346+ * an entry hides that author from the control.
347+ *
348+ * @since 7.1.0
349+ *
350+ * @param array $plugin_authors Map of author key to an array with 'label' and 'count'.
351+ * @param array $all_plugins The full list of installed plugins, keyed by plugin file.
352+ */
353+ $ this ->plugin_authors = apply_filters ( 'plugins_list_authors ' , $ this ->plugin_authors , $ plugins ['all ' ] );
354+
314355 if ( empty ( $ plugins [ $ status ] ) && ! in_array ( $ status , array ( 'all ' , 'search ' ), true ) ) {
315356 $ status = 'all ' ;
316357 }
@@ -321,7 +362,17 @@ public function prepare_items() {
321362 $ this ->items [ $ plugin_file ] = _get_plugin_data_markup_translate ( $ plugin_file , $ plugin_data , false , true );
322363 }
323364
324- $ total_this_page = $ totals [ $ status ];
365+ // Narrow the current view by author when the "Filter by author" control is used.
366+ $ plugin_author = isset ( $ _REQUEST ['plugin_author ' ] ) ? sanitize_key ( $ _REQUEST ['plugin_author ' ] ) : '' ;
367+ if ( '' !== $ plugin_author ) {
368+ foreach ( $ this ->items as $ plugin_file => $ plugin_data ) {
369+ if ( $ this ->get_plugin_author_key ( $ plugin_file , $ plugin_data ) !== $ plugin_author ) {
370+ unset( $ this ->items [ $ plugin_file ] );
371+ }
372+ }
373+ }
374+
375+ $ total_this_page = ( '' !== $ plugin_author ) ? count ( $ this ->items ) : $ totals [ $ status ];
325376
326377 $ js_plugins = array ();
327378 foreach ( $ plugins as $ key => $ list ) {
@@ -363,6 +414,34 @@ public function prepare_items() {
363414 );
364415 }
365416
417+ /**
418+ * Returns the author grouping key used to bucket a plugin in the "Filter by author" control.
419+ *
420+ * @since 7.1.0
421+ *
422+ * @param string $plugin_file Path to the plugin file relative to the plugins directory.
423+ * @param array $plugin_data An array of plugin data.
424+ * @return string The author grouping key, or an empty string when no author is set.
425+ */
426+ protected function get_plugin_author_key ( $ plugin_file , $ plugin_data ) {
427+ $ author_name = isset ( $ plugin_data ['AuthorName ' ] ) ? $ plugin_data ['AuthorName ' ] : '' ;
428+ $ author_key = ( '' === $ author_name ) ? '' : sanitize_title ( $ author_name );
429+
430+ /**
431+ * Filters the author grouping key for a plugin in the Plugins list "Filter by author" control.
432+ *
433+ * Returning the same key for several plugins groups them under a single author, which allows
434+ * author-name header variants (for example "Team Yoast" and "Yoast") to be merged.
435+ *
436+ * @since 7.1.0
437+ *
438+ * @param string $author_key Author grouping key derived from the plugin's AuthorName header.
439+ * @param string $plugin_file Path to the plugin file relative to the plugins directory.
440+ * @param array $plugin_data An array of plugin data.
441+ */
442+ return apply_filters ( 'plugins_list_plugin_author ' , $ author_key , $ plugin_file , $ plugin_data );
443+ }
444+
366445 /**
367446 * @global string $s URL encoded search term.
368447 *
@@ -675,6 +754,11 @@ public function bulk_actions( $which = '' ) {
675754 protected function extra_tablenav ( $ which ) {
676755 global $ status ;
677756
757+ // Offer a single "Filter by author" control whenever more than one author is installed.
758+ if ( 'top ' === $ which && count ( $ this ->plugin_authors ) > 1 ) {
759+ $ this ->author_filter ();
760+ }
761+
678762 if ( ! in_array ( $ status , array ( 'recently_activated ' , 'mustuse ' , 'dropins ' ), true ) ) {
679763 return ;
680764 }
@@ -699,6 +783,92 @@ protected function extra_tablenav( $which ) {
699783 echo '</div> ' ;
700784 }
701785
786+ /**
787+ * Displays the "Filter by author" controls in the table navigation.
788+ *
789+ * The plugins list table is rendered inside the bulk-actions POST form, which
790+ * cannot contain a nested filter form. The native <select> and submit button are
791+ * therefore associated, via the HTML5 "form" attribute, with the separate GET form
792+ * printed by author_filter_form(). Filtering is a plain GET request: it needs no
793+ * JavaScript, and nothing is inlined for a Content Security Policy to block.
794+ *
795+ * @since 7.1.0
796+ */
797+ protected function author_filter () {
798+ $ current = isset ( $ _REQUEST ['plugin_author ' ] ) ? sanitize_key ( $ _REQUEST ['plugin_author ' ] ) : '' ;
799+
800+ echo '<div class="alignleft actions"> ' ;
801+ printf (
802+ '<label for="plugin-author-filter" class="screen-reader-text">%s</label> ' ,
803+ esc_html__ ( 'Filter by author ' )
804+ );
805+ echo '<select name="plugin_author" id="plugin-author-filter" form="plugin-author-filter-form"> ' ;
806+ printf (
807+ '<option value=""%s>%s</option> ' ,
808+ selected ( $ current , '' , false ),
809+ esc_html__ ( 'All authors ' )
810+ );
811+ foreach ( $ this ->plugin_authors as $ author_key => $ author ) {
812+ $ label = sprintf (
813+ /* translators: 1: Plugin author name. 2: Number of plugins by this author. */
814+ _x ( '%1$s (%2$s) ' , 'plugins ' ),
815+ $ author ['label ' ],
816+ number_format_i18n ( $ author ['count ' ] )
817+ );
818+ printf (
819+ '<option value="%s"%s>%s</option> ' ,
820+ esc_attr ( $ author_key ),
821+ selected ( $ current , $ author_key , false ),
822+ esc_html ( $ label )
823+ );
824+ }
825+ echo '</select> ' ;
826+ submit_button (
827+ __ ( 'Filter ' ),
828+ '' ,
829+ '' ,
830+ false ,
831+ array (
832+ 'id ' => 'plugin-author-filter-submit ' ,
833+ 'form ' => 'plugin-author-filter-form ' ,
834+ )
835+ );
836+ echo '</div> ' ;
837+ }
838+
839+ /**
840+ * Prints the GET form that the "Filter by author" controls submit.
841+ *
842+ * This is printed outside the bulk-actions POST form (see wp-admin/plugins.php),
843+ * because a form cannot be nested in another form. The author <select> and its
844+ * submit button reference this form through their "form" attribute, so choosing an
845+ * author and pressing Filter performs a plain GET request. The hidden fields keep
846+ * the current status view and search term.
847+ *
848+ * @since 7.1.0
849+ *
850+ * @global string $status Current plugin status view.
851+ */
852+ public function author_filter_form () {
853+ global $ status ;
854+
855+ if ( count ( $ this ->plugin_authors ) < 2 ) {
856+ return ;
857+ }
858+
859+ echo '<form id="plugin-author-filter-form" method="get"> ' ;
860+ if ( 'all ' !== $ status ) {
861+ printf ( '<input type="hidden" name="plugin_status" value="%s" /> ' , esc_attr ( $ status ) );
862+ }
863+ if ( isset ( $ _REQUEST ['s ' ] ) && '' !== $ _REQUEST ['s ' ] ) {
864+ printf (
865+ '<input type="hidden" name="s" value="%s" /> ' ,
866+ esc_attr ( sanitize_text_field ( wp_unslash ( $ _REQUEST ['s ' ] ) ) )
867+ );
868+ }
869+ echo '</form> ' ;
870+ }
871+
702872 /**
703873 * @return string
704874 */
0 commit comments