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: 32 additions & 0 deletions css/plugin-report.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
218 changes: 218 additions & 0 deletions rt-plugin-report.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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' ) );
}


Expand Down Expand Up @@ -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;
Expand All @@ -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() {
?>
<script>
(function() {
var btn = document.querySelector( '.page-title-action' );
if ( ! btn ) return;
var a = document.createElement( 'a' );
a.href = <?php echo wp_json_encode( $this->pr_clear_cache_url ); ?>;
a.className = 'page-title-action';
a.textContent = <?php echo wp_json_encode( $this->pr_clear_cache_label ); ?>;
btn.parentNode.insertBefore( a, btn.nextSibling );
})();
</script>
<?php
}


/**
* Get the slugs for all currently installed plugins
*/
Expand Down Expand Up @@ -406,6 +476,154 @@ 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 '<span class="' . self::CSS_CLASS_HIGH . '">' . esc_html__( 'Closed', 'plugin-report' ) . '</span>';
} else {
echo '<span class="' . self::CSS_CLASS_HIGH . '">' . esc_html__( 'Not found', 'plugin-report' ) . '</span>';
}
} elseif ( isset( $cache['repo_info'] ) ) {
echo '<span class="' . self::CSS_CLASS_LOW . '">wordpress.org</span>';
} else {
$parsed = wp_parse_url( isset( $cache['local_info']['UpdateURI'] ) ? $cache['local_info']['UpdateURI'] : '' );
if ( isset( $parsed['host'] ) && ! empty( $parsed['host'] ) ) {
echo '<span class="' . self::CSS_CLASS_MED . '">' . esc_html( $parsed['host'] ) . '</span>';
} 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 '<span class="' . $css_class . '">' . esc_html( $time_diff ) . '</span>';
} 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 '<span class="' . $css_class . '">' . esc_html( $cache['repo_info']->tested ) . '</span>';
} 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 '<tr class="plugin-update-tr pr-warning-row pr-warning-' . esc_attr( $warning['level'] ) . '">';
echo '<td colspan="' . (int) $colspan . '" class="plugin-update colspanchange">';
echo '<div class="update-message notice inline ' . $notice_class . ' notice-alt"><p>';
echo esc_html( $warning['message'] );
echo '</p></div>';
echo '</td>';
echo '</tr>';
}
}


/**
* From a report, generate an HTML table row with relevant data for the plugin.
*
Expand Down
Loading