From de4f49018c92a0cbdefe421c00a174b799a3d274 Mon Sep 17 00:00:00 2001 From: Christoph Daum Date: Mon, 13 Apr 2026 19:57:23 +0200 Subject: [PATCH 1/8] feat(plugins): enrich native plugin list table Add Repository, Last Update, and Tested up to columns to the native Plugins page. Show inline warnings for closed, stale (>2 years), or untested plugins. Data is read from existing transient cache populated by the report page. --- css/plugin-report.css | 17 +++++ rt-plugin-report.php | 160 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+) diff --git a/css/plugin-report.css b/css/plugin-report.css index 27433b8..cedfdb3 100644 --- a/css/plugin-report.css +++ b/css/plugin-report.css @@ -16,6 +16,23 @@ td.pr-risk-medium { font-weight: bold; } +/* Inline risk classes for plugin list table columns */ + +span.pr-risk-low { + color: #007017; + font-weight: 600; +} + +span.pr-risk-high { + color: #b32d2e; + font-weight: 600; +} + +span.pr-risk-medium { + color: #826200; + font-weight: 600; +} + /* additional info in table cells */ span.pr-additional-info { diff --git a/rt-plugin-report.php b/rt-plugin-report.php index 6b76f78..a470244 100644 --- a/rt-plugin-report.php +++ b/rt-plugin-report.php @@ -60,6 +60,14 @@ public function init() { add_action( 'wp_ajax_rt_get_plugin_info', array( $this, 'get_plugin_info' ) ); // Hook into the WP Upgrader to selectively delete cache items. add_action( 'upgrader_process_complete', array( $this, 'upgrade_delete_cache_items' ), 10, 2 ); + // Enrich the native plugin list table with extra columns and warnings. + add_filter( 'manage_plugins_columns', array( $this, 'add_plugin_list_columns' ) ); + add_action( 'manage_plugins_custom_column', array( $this, 'render_plugin_list_column' ), 10, 3 ); + add_action( 'after_plugin_row', array( $this, 'render_plugin_row_warning' ), 10, 3 ); + if ( is_multisite() ) { + add_filter( 'manage_plugins-network_columns', array( $this, 'add_plugin_list_columns' ) ); + add_action( 'manage_plugins-network_custom_column', array( $this, 'render_plugin_list_column' ), 10, 3 ); + } } @@ -172,6 +180,11 @@ public function settings_page() { * @param string $hook Screen hook. */ public function enqueue_assets( $hook ) { + // Load CSS on the native plugin list page too. + if ( 'plugins.php' === $hook ) { + wp_enqueue_style( 'plugin-report-css', plugin_dir_url( __FILE__ ) . 'css/plugin-report.css', array(), self::PLUGIN_VERSION ); + return; + } // Check if we're on the right screen. if ( 'plugins_page_plugin_report' !== $hook ) { return; @@ -406,6 +419,153 @@ private function check_exists_in_svn( $slug ) { } + /** + * Add custom columns to the native plugin list table. + * + * @param array $columns Existing columns. + * + * @return array Modified columns. + */ + public function add_plugin_list_columns( $columns ) { + $columns['pr_repo_status'] = __( 'Repository', 'plugin-report' ); + $columns['pr_last_updated'] = __( 'Last Update', 'plugin-report' ); + $columns['pr_tested'] = __( 'Tested up to', 'plugin-report' ); + return $columns; + } + + + /** + * Render custom column content in the native plugin list table. + * + * @param string $column_name Column identifier. + * @param string $plugin_file Plugin file path relative to plugins dir. + * @param array $plugin_data Plugin header data. + */ + public function render_plugin_list_column( $column_name, $plugin_file, $plugin_data ) { + $slug = $this->get_plugin_slug( $plugin_file ); + $cache = get_site_transient( $this->create_cache_key( $slug ) ); + + if ( empty( $cache ) ) { + echo '—'; + return; + } + + global $wp_version; + $wp_latest = $this->check_core_updates(); + + switch ( $column_name ) { + case 'pr_repo_status': + if ( isset( $cache['repo_error_code'] ) && 'plugins_api_failed' === $cache['repo_error_code'] ) { + if ( isset( $cache['exists_in_svn'] ) && true === $cache['exists_in_svn'] ) { + echo '' . esc_html__( 'Closed', 'plugin-report' ) . ''; + } else { + echo '' . esc_html__( 'Not found', 'plugin-report' ) . ''; + } + } elseif ( isset( $cache['repo_info'] ) ) { + echo 'wordpress.org'; + } else { + $parsed = wp_parse_url( isset( $cache['local_info']['UpdateURI'] ) ? $cache['local_info']['UpdateURI'] : '' ); + if ( isset( $parsed['host'] ) && ! empty( $parsed['host'] ) ) { + echo '' . esc_html( $parsed['host'] ) . ''; + } else { + echo '—'; + } + } + break; + + case 'pr_last_updated': + if ( isset( $cache['repo_info'] ) && isset( $cache['repo_info']->last_updated ) ) { + $time_update = new DateTime( $cache['repo_info']->last_updated ); + $time_diff = human_time_diff( $time_update->getTimestamp(), current_time( 'timestamp' ) ); + $css_class = $this->get_timediff_risk_classname( current_time( 'timestamp' ) - $time_update->getTimestamp() ); + echo '' . esc_html( $time_diff ) . ''; + } else { + echo '—'; + } + break; + + case 'pr_tested': + if ( isset( $cache['repo_info'] ) && isset( $cache['repo_info']->tested ) && ! empty( $cache['repo_info']->tested ) ) { + $css_class = $this->get_version_risk_classname( $cache['repo_info']->tested, $wp_latest, true ); + echo '' . esc_html( $cache['repo_info']->tested ) . ''; + } else { + echo '—'; + } + break; + } + } + + + /** + * Show inline warning rows beneath problematic plugins in the native list. + * + * @param string $plugin_file Plugin file path relative to plugins dir. + * @param array $plugin_data Plugin header data. + * @param string $status Plugin status (active, inactive, etc.). + */ + public function render_plugin_row_warning( $plugin_file, $plugin_data, $status ) { + $slug = $this->get_plugin_slug( $plugin_file ); + $cache = get_site_transient( $this->create_cache_key( $slug ) ); + + if ( empty( $cache ) ) { + return; + } + + $warnings = array(); + + // Closed on wp.org. + if ( isset( $cache['repo_error_code'] ) && 'plugins_api_failed' === $cache['repo_error_code'] && isset( $cache['exists_in_svn'] ) && true === $cache['exists_in_svn'] ) { + $warnings[] = array( + 'class' => self::CSS_CLASS_HIGH, + 'message' => __( 'This plugin has been closed on wordpress.org and will no longer receive updates.', 'plugin-report' ), + ); + } + + // Not updated in over 2 years. + if ( isset( $cache['repo_info'] ) && isset( $cache['repo_info']->last_updated ) ) { + $time_update = new DateTime( $cache['repo_info']->last_updated ); + $days_since = ( current_time( 'timestamp' ) - $time_update->getTimestamp() ) / DAY_IN_SECONDS; + if ( $days_since > 730 ) { + $warnings[] = array( + 'class' => self::CSS_CLASS_MED, + 'message' => __( 'This plugin has not been updated in over 2 years.', 'plugin-report' ), + ); + } + } + + // Not tested with current WP major version. + if ( isset( $cache['repo_info'] ) && isset( $cache['repo_info']->tested ) && ! empty( $cache['repo_info']->tested ) ) { + $wp_latest = $this->check_core_updates(); + if ( version_compare( $this->get_major_version( $cache['repo_info']->tested ), $this->get_major_version( $wp_latest ), '<' ) ) { + $warnings[] = array( + 'class' => self::CSS_CLASS_MED, + /* translators: %s: WordPress version number */ + 'message' => sprintf( __( 'This plugin has not been tested with the current major version of WordPress (%s).', 'plugin-report' ), $this->get_major_version( $wp_latest ) ), + ); + } + } + + if ( empty( $warnings ) ) { + return; + } + + // Count visible columns to span the full width. + $screen = get_current_screen(); + $columns = get_column_headers( $screen ); + $colspan = count( $columns ) + 1; // +1 for the checkbox column. + + foreach ( $warnings as $warning ) { + echo ''; + echo ''; + echo '

'; + echo '' . esc_html( $warning['message'] ) . ''; + echo '

'; + echo ''; + echo ''; + } + } + + /** * From a report, generate an HTML table row with relevant data for the plugin. * From e5e6ff97ea7d8bc1a764951c9294ca55af41325d Mon Sep 17 00:00:00 2001 From: Christoph Daum Date: Mon, 13 Apr 2026 20:04:45 +0200 Subject: [PATCH 2/8] feat(plugins): populate cache and add refresh link Call assemble_plugin_report() for all plugins on the native Plugins page so columns have data on first visit. Add a "Refresh report data" link to the plugin list views with nonce-protected cache clear. --- rt-plugin-report.php | 45 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/rt-plugin-report.php b/rt-plugin-report.php index a470244..214d3c0 100644 --- a/rt-plugin-report.php +++ b/rt-plugin-report.php @@ -68,6 +68,11 @@ public function init() { add_filter( 'manage_plugins-network_columns', array( $this, 'add_plugin_list_columns' ) ); add_action( 'manage_plugins-network_custom_column', array( $this, 'render_plugin_list_column' ), 10, 3 ); } + // Populate cache on the native plugins page so columns have data. + add_action( 'load-plugins.php', array( $this, 'populate_cache_on_plugins_page' ) ); + // Add a cache-clear link to the native plugins page. + add_filter( 'views_plugins', array( $this, 'add_plugins_page_cache_link' ) ); + add_filter( 'views_plugins-network', array( $this, 'add_plugins_page_cache_link' ) ); } @@ -210,6 +215,46 @@ public function enqueue_assets( $hook ) { } + /** + * Ensure all plugins have cached reports when viewing the native plugins page. + * Also handles the cache-clear action for this page. + */ + public function populate_cache_on_plugins_page() { + // Check if get_plugins() function exists. + if ( ! function_exists( 'plugins_api' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; + } + + // Handle cache clear request. + if ( isset( $_GET['pr_clear_cache'] ) && isset( $_GET['_wpnonce'] ) ) { + if ( wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'pr_clear_cache' ) ) { + $this->clear_cache(); + } + } + + // Populate cache for any plugins that don't have it yet. + $plugins = get_plugins(); + foreach ( $plugins as $key => $plugin ) { + $slug = $this->get_plugin_slug( $key ); + $this->assemble_plugin_report( $slug ); + } + } + + + /** + * Add a "Refresh report data" link to the plugin list views. + * + * @param array $views Existing view links. + * + * @return array Modified view links. + */ + public function add_plugins_page_cache_link( $views ) { + $url = wp_nonce_url( add_query_arg( 'pr_clear_cache', '1' ), 'pr_clear_cache' ); + $views['pr_refresh'] = '' . esc_html__( 'Refresh report data', 'plugin-report' ) . ''; + return $views; + } + + /** * Get the slugs for all currently installed plugins */ From aff687f8286e0bc5d0ae03065fd24d2b20be2f3b Mon Sep 17 00:00:00 2001 From: Christoph Daum Date: Mon, 13 Apr 2026 20:16:01 +0200 Subject: [PATCH 3/8] style(warnings): severity-based row styling Use notice-error (red) for closed plugins and notice-warning (orange) for stale/untested. Add dashicons (dismiss, warning, info) for visual distinction between severity levels. --- css/plugin-report.css | 22 ++++++++++++++++++++++ rt-plugin-report.php | 17 +++++++++++------ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/css/plugin-report.css b/css/plugin-report.css index cedfdb3..bfa122b 100644 --- a/css/plugin-report.css +++ b/css/plugin-report.css @@ -33,6 +33,28 @@ span.pr-risk-medium { font-weight: 600; } +/* Warning rows in native plugin list */ + +.pr-warning-icon { + font-size: 16px; + width: 16px; + height: 16px; + vertical-align: text-bottom; +} + +.pr-warning-error .notice-error { + border-left-color: #b32d2e; +} + +.pr-warning-error .notice-error p { + color: #b32d2e; + font-weight: 600; +} + +.pr-warning-warning .notice-warning p { + color: #826200; +} + /* additional info in table cells */ span.pr-additional-info { diff --git a/rt-plugin-report.php b/rt-plugin-report.php index 214d3c0..76ba1c7 100644 --- a/rt-plugin-report.php +++ b/rt-plugin-report.php @@ -561,7 +561,8 @@ public function render_plugin_row_warning( $plugin_file, $plugin_data, $status ) // Closed on wp.org. if ( isset( $cache['repo_error_code'] ) && 'plugins_api_failed' === $cache['repo_error_code'] && isset( $cache['exists_in_svn'] ) && true === $cache['exists_in_svn'] ) { $warnings[] = array( - 'class' => self::CSS_CLASS_HIGH, + 'level' => 'error', + 'icon' => 'dismiss', 'message' => __( 'This plugin has been closed on wordpress.org and will no longer receive updates.', 'plugin-report' ), ); } @@ -572,7 +573,8 @@ public function render_plugin_row_warning( $plugin_file, $plugin_data, $status ) $days_since = ( current_time( 'timestamp' ) - $time_update->getTimestamp() ) / DAY_IN_SECONDS; if ( $days_since > 730 ) { $warnings[] = array( - 'class' => self::CSS_CLASS_MED, + 'level' => 'warning', + 'icon' => 'warning', 'message' => __( 'This plugin has not been updated in over 2 years.', 'plugin-report' ), ); } @@ -583,7 +585,8 @@ public function render_plugin_row_warning( $plugin_file, $plugin_data, $status ) $wp_latest = $this->check_core_updates(); if ( version_compare( $this->get_major_version( $cache['repo_info']->tested ), $this->get_major_version( $wp_latest ), '<' ) ) { $warnings[] = array( - 'class' => self::CSS_CLASS_MED, + 'level' => 'warning', + 'icon' => 'info', /* translators: %s: WordPress version number */ 'message' => sprintf( __( 'This plugin has not been tested with the current major version of WordPress (%s).', 'plugin-report' ), $this->get_major_version( $wp_latest ) ), ); @@ -600,10 +603,12 @@ public function render_plugin_row_warning( $plugin_file, $plugin_data, $status ) $colspan = count( $columns ) + 1; // +1 for the checkbox column. foreach ( $warnings as $warning ) { - echo ''; + $notice_class = 'error' === $warning['level'] ? 'notice-error' : 'notice-warning'; + echo ''; echo ''; - echo '

'; - echo '' . esc_html( $warning['message'] ) . ''; + echo '

'; + echo ' '; + echo esc_html( $warning['message'] ); echo '

'; echo ''; echo ''; From 028a8d24f865e6ae514ee5c0b3fd141208baeea8 Mon Sep 17 00:00:00 2001 From: Christoph Daum Date: Mon, 13 Apr 2026 20:18:36 +0200 Subject: [PATCH 4/8] style(warnings): drop icon from stale warning The orange notice-warning bar is clear enough without the redundant dashicons-warning icon. --- rt-plugin-report.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rt-plugin-report.php b/rt-plugin-report.php index 76ba1c7..ced8c6d 100644 --- a/rt-plugin-report.php +++ b/rt-plugin-report.php @@ -574,7 +574,6 @@ public function render_plugin_row_warning( $plugin_file, $plugin_data, $status ) if ( $days_since > 730 ) { $warnings[] = array( 'level' => 'warning', - 'icon' => 'warning', 'message' => __( 'This plugin has not been updated in over 2 years.', 'plugin-report' ), ); } @@ -607,7 +606,9 @@ public function render_plugin_row_warning( $plugin_file, $plugin_data, $status ) echo ''; echo ''; echo '

'; - echo ' '; + if ( ! empty( $warning['icon'] ) ) { + echo ' '; + } echo esc_html( $warning['message'] ); echo '

'; echo ''; From ad1ea641fffb64725fba194f48ea402805a914a6 Mon Sep 17 00:00:00 2001 From: Christoph Daum Date: Mon, 13 Apr 2026 20:20:58 +0200 Subject: [PATCH 5/8] style(warnings): remove all dashicon spans The notice bar colors (red/orange) are sufficient visual indicators. Dashicon spans were redundant. --- css/plugin-report.css | 7 ------- rt-plugin-report.php | 5 ----- 2 files changed, 12 deletions(-) diff --git a/css/plugin-report.css b/css/plugin-report.css index bfa122b..608656e 100644 --- a/css/plugin-report.css +++ b/css/plugin-report.css @@ -35,13 +35,6 @@ span.pr-risk-medium { /* Warning rows in native plugin list */ -.pr-warning-icon { - font-size: 16px; - width: 16px; - height: 16px; - vertical-align: text-bottom; -} - .pr-warning-error .notice-error { border-left-color: #b32d2e; } diff --git a/rt-plugin-report.php b/rt-plugin-report.php index ced8c6d..9307e5e 100644 --- a/rt-plugin-report.php +++ b/rt-plugin-report.php @@ -562,7 +562,6 @@ public function render_plugin_row_warning( $plugin_file, $plugin_data, $status ) if ( isset( $cache['repo_error_code'] ) && 'plugins_api_failed' === $cache['repo_error_code'] && isset( $cache['exists_in_svn'] ) && true === $cache['exists_in_svn'] ) { $warnings[] = array( 'level' => 'error', - 'icon' => 'dismiss', 'message' => __( 'This plugin has been closed on wordpress.org and will no longer receive updates.', 'plugin-report' ), ); } @@ -585,7 +584,6 @@ public function render_plugin_row_warning( $plugin_file, $plugin_data, $status ) if ( version_compare( $this->get_major_version( $cache['repo_info']->tested ), $this->get_major_version( $wp_latest ), '<' ) ) { $warnings[] = array( 'level' => 'warning', - 'icon' => 'info', /* translators: %s: WordPress version number */ 'message' => sprintf( __( 'This plugin has not been tested with the current major version of WordPress (%s).', 'plugin-report' ), $this->get_major_version( $wp_latest ) ), ); @@ -606,9 +604,6 @@ public function render_plugin_row_warning( $plugin_file, $plugin_data, $status ) echo ''; echo ''; echo '

'; - if ( ! empty( $warning['icon'] ) ) { - echo ' '; - } echo esc_html( $warning['message'] ); echo '

'; echo ''; From 8a9cc7f7d3e15904943a86a18c534a017b7ca215 Mon Sep 17 00:00:00 2001 From: Christoph Daum Date: Mon, 13 Apr 2026 20:28:03 +0200 Subject: [PATCH 6/8] fix(plugins): move cache-clear to page header Place the cache-clear link next to the "Add Plugin" button as a page-title-action instead of in the views filter list. Reuse existing translated string "Clear cached plugin data and reload". --- rt-plugin-report.php | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/rt-plugin-report.php b/rt-plugin-report.php index 9307e5e..04f314a 100644 --- a/rt-plugin-report.php +++ b/rt-plugin-report.php @@ -70,9 +70,6 @@ public function init() { } // Populate cache on the native plugins page so columns have data. add_action( 'load-plugins.php', array( $this, 'populate_cache_on_plugins_page' ) ); - // Add a cache-clear link to the native plugins page. - add_filter( 'views_plugins', array( $this, 'add_plugins_page_cache_link' ) ); - add_filter( 'views_plugins-network', array( $this, 'add_plugins_page_cache_link' ) ); } @@ -185,9 +182,12 @@ public function settings_page() { * @param string $hook Screen hook. */ public function enqueue_assets( $hook ) { - // Load CSS on the native plugin list page too. + // Load CSS and cache-clear button on the native plugin list page. if ( 'plugins.php' === $hook ) { wp_enqueue_style( 'plugin-report-css', plugin_dir_url( __FILE__ ) . 'css/plugin-report.css', array(), self::PLUGIN_VERSION ); + $url = wp_nonce_url( admin_url( 'plugins.php?pr_clear_cache=1' ), 'pr_clear_cache' ); + $label = esc_js( __( 'Clear cached plugin data and reload', 'plugin-report' ) ); + wp_add_inline_script( 'jquery', 'jQuery(function($){var b=document.querySelector(".page-title-action");if(b){var a=document.createElement("a");a.href="' . esc_url( $url ) . '";a.className="page-title-action";a.textContent="' . $label . '";b.parentNode.insertBefore(a,b.nextSibling);}});' ); return; } // Check if we're on the right screen. @@ -241,20 +241,6 @@ public function populate_cache_on_plugins_page() { } - /** - * Add a "Refresh report data" link to the plugin list views. - * - * @param array $views Existing view links. - * - * @return array Modified view links. - */ - public function add_plugins_page_cache_link( $views ) { - $url = wp_nonce_url( add_query_arg( 'pr_clear_cache', '1' ), 'pr_clear_cache' ); - $views['pr_refresh'] = '' . esc_html__( 'Refresh report data', 'plugin-report' ) . ''; - return $views; - } - - /** * Get the slugs for all currently installed plugins */ From ebdf2a917028083f32828c8b5895dbf30b13bbf7 Mon Sep 17 00:00:00 2001 From: Christoph Daum Date: Mon, 13 Apr 2026 20:28:35 +0200 Subject: [PATCH 7/8] fix(plugins): use vanilla JS for cache-clear btn Replace jQuery-based inline script with a vanilla JS IIFE output via admin_print_footer_scripts. No jQuery dependency needed. --- rt-plugin-report.php | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/rt-plugin-report.php b/rt-plugin-report.php index 04f314a..a20cdb4 100644 --- a/rt-plugin-report.php +++ b/rt-plugin-report.php @@ -182,12 +182,12 @@ public function settings_page() { * @param string $hook Screen hook. */ public function enqueue_assets( $hook ) { - // Load CSS and cache-clear button on the native plugin list page. + // Load CSS on the native plugin list page. if ( 'plugins.php' === $hook ) { wp_enqueue_style( 'plugin-report-css', plugin_dir_url( __FILE__ ) . 'css/plugin-report.css', array(), self::PLUGIN_VERSION ); - $url = wp_nonce_url( admin_url( 'plugins.php?pr_clear_cache=1' ), 'pr_clear_cache' ); - $label = esc_js( __( 'Clear cached plugin data and reload', 'plugin-report' ) ); - wp_add_inline_script( 'jquery', 'jQuery(function($){var b=document.querySelector(".page-title-action");if(b){var a=document.createElement("a");a.href="' . esc_url( $url ) . '";a.className="page-title-action";a.textContent="' . $label . '";b.parentNode.insertBefore(a,b.nextSibling);}});' ); + $this->pr_clear_cache_url = wp_nonce_url( admin_url( 'plugins.php?pr_clear_cache=1' ), 'pr_clear_cache' ); + $this->pr_clear_cache_label = __( 'Clear cached plugin data and reload', 'plugin-report' ); + add_action( 'admin_print_footer_scripts', array( $this, 'render_clear_cache_button_script' ) ); return; } // Check if we're on the right screen. @@ -241,6 +241,26 @@ public function populate_cache_on_plugins_page() { } + /** + * Output inline script to place the cache-clear link next to "Add Plugin". + */ + public function render_clear_cache_button_script() { + ?> + + Date: Tue, 14 Apr 2026 08:38:24 +0200 Subject: [PATCH 8/8] fix(phpstan): declare cache-clear properties Add property declarations for pr_clear_cache_url and pr_clear_cache_label to satisfy PHPStan. --- rt-plugin-report.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rt-plugin-report.php b/rt-plugin-report.php index a20cdb4..f2d2691 100644 --- a/rt-plugin-report.php +++ b/rt-plugin-report.php @@ -36,6 +36,12 @@ class RT_Plugin_Report { const CACHE_LIFETIME = DAY_IN_SECONDS; const CACHE_LIFETIME_NOREPO = WEEK_IN_SECONDS; + /** @var string Cache-clear URL for the plugins page. */ + private $pr_clear_cache_url = ''; + + /** @var string Cache-clear button label for the plugins page. */ + private $pr_clear_cache_label = ''; + /** * Constructor */