diff --git a/css/plugin-report.css b/css/plugin-report.css index 27433b8..608656e 100644 --- a/css/plugin-report.css +++ b/css/plugin-report.css @@ -16,6 +16,38 @@ 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; +} + +/* Warning rows in native plugin list */ + +.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 6b76f78..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 */ @@ -60,6 +66,16 @@ 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 ); + } + // Populate cache on the native plugins page so columns have data. + add_action( 'load-plugins.php', array( $this, 'populate_cache_on_plugins_page' ) ); } @@ -172,6 +188,14 @@ public function settings_page() { * @param string $hook Screen hook. */ public function enqueue_assets( $hook ) { + // 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 ); + $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. if ( 'plugins_page_plugin_report' !== $hook ) { return; @@ -197,6 +221,52 @@ 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 ); + } + } + + + /** + * Output inline script to place the cache-clear link next to "Add Plugin". + */ + public function render_clear_cache_button_script() { + ?> + + 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( + 'level' => 'error', + '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( + 'level' => 'warning', + '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( + 'level' => 'warning', + /* 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 ) { + $notice_class = 'error' === $warning['level'] ? 'notice-error' : 'notice-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. *