wpvulnerability/wpvulnerability-plugins.php
2026-08-23 07:08:42 +00:00

791 lines
32 KiB
PHP

<?php
/**
* Plugin functions
*
* @package WPVulnerability
*
* @version 2.0.0
*/
defined( 'ABSPATH' ) || die( 'No script kiddies please!' );
/**
* Enqueues the admin JavaScript for plugin update interactions.
*
* @since 4.1.0
*
* @param string $hook Current admin page hook.
*
* @return void
*/
function wpvulnerability_plugins_admin_enqueue_scripts( $hook ) {
if ( 'plugins.php' !== $hook && 'plugins-network' !== $hook ) {
return;
}
wp_enqueue_script(
'wpvulnerability-admin-js',
WPVULNERABILITY_PLUGIN_URL . 'assets/admin.js',
array( 'jquery' ),
WPVULNERABILITY_PLUGIN_VERSION,
true
);
}
add_action( 'admin_enqueue_scripts', 'wpvulnerability_plugins_admin_enqueue_scripts' );
/**
* Generate a deterministic signature for the installed plugins list.
*
* @since 4.1.2
*
* @param array<string,array<string,mixed>> $plugins List of plugins returned by get_plugins().
* @return string Hash representing the installed plugins and their versions.
*/
function wpvulnerability_plugins_generate_signature( $plugins ) {
$normalized = array();
foreach ( $plugins as $file_path => $plugin_data ) {
$plugin_file = sanitize_text_field( (string) $file_path );
$version = '';
if ( isset( $plugin_data['Version'] ) ) {
$v_raw = $plugin_data['Version'];
$version = sanitize_text_field( is_scalar( $v_raw ) ? (string) $v_raw : '' );
}
$normalized[ $plugin_file ] = $version;
}
ksort( $normalized );
$encoded = wp_json_encode( $normalized );
return md5( false !== $encoded ? $encoded : '' );
}
/**
* Retrieve the signature of the currently installed plugins.
*
* @since 4.1.2
*
* @return string Hash representing the installed plugins and their versions.
*/
function wpvulnerability_plugins_get_current_signature() {
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
return wpvulnerability_plugins_generate_signature( get_plugins() );
}
/**
* Adds a vulnerability notice under vulnerable plugins.
*
* This function retrieves the vulnerability data for the specified plugin from the WordPress options table
* and displays a detailed notice below the plugin's row on the plugins management page in the WordPress admin area.
* The notice includes information about the plugin's vulnerabilities, such as affected versions, severity, CVSS scores,
* and links to sources.
*
* The function is applicable both in single-site and multisite installations. In a multisite setup, the notice
* is displayed only in the network admin area or in the site admin area of individual sites.
*
* @since 2.0.0
*
* @param string $plugin_file Main plugin folder/file name.
* @param array<string, mixed> $plugin_data Plugin data array containing information about the plugin.
*
* @return void
*/
function wpvulnerability_plugin_info_after( $plugin_file, $plugin_data ) {
// Retrieve the vulnerabilities for all plugins from the options table and decode the JSON.
$raw_plugins = is_multisite() ? get_site_option( 'wpvulnerability-plugins', '' ) : get_option( 'wpvulnerability-plugins', '' );
$plugin_vulnerabilities = json_decode( is_string( $raw_plugins ) ? $raw_plugins : '', true );
if ( ! is_array( $plugin_vulnerabilities ) ) {
$plugin_vulnerabilities = array();
}
if ( ( is_multisite() && is_network_admin() ) || ! is_multisite() ) {
// Determine whether the plugin is active and add an appropriate CSS class to the table row.
$tr_class = is_plugin_active( $plugin_file ) ? 'active' : '';
// Generate the vulnerability notice message with the plugin name.
$message = sprintf(
/* translators: 1: Plugin or theme name. */
__( '%1$s has a known vulnerability that may be affecting your installed version.', 'wpvulnerability' ),
wp_kses( is_scalar( $plugin_data['Name'] ) ? (string) $plugin_data['Name'] : '', 'strip' )
);
// Begin generating the table row HTML markup with appropriate CSS classes and the vulnerability notice message.
$information = '<tr class="wpvulnerability ' . esc_attr( $tr_class ) . '">';
$information .= '<td colspan="4">';
$information .= '<p class="text-red"><img src="' . esc_url( WPVULNERABILITY_PLUGIN_URL ) . 'assets/icon.svg" style="height: 16px; vertical-align: text-top; width: 16px;" alt="" title="WPVulnerability"> <strong>' . esc_html( $message ) . '</strong>';
$information .= '</p>';
$information .= '<table>';
// Loop through all vulnerabilities for the current plugin and add their details to the table row HTML markup.
$pf_entry = isset( $plugin_vulnerabilities[ $plugin_file ] ) && is_array( $plugin_vulnerabilities[ $plugin_file ] ) ? $plugin_vulnerabilities[ $plugin_file ] : array();
$vulnerabilities = isset( $pf_entry['vulnerabilities'] ) && is_array( $pf_entry['vulnerabilities'] ) ? $pf_entry['vulnerabilities'] : array();
foreach ( $vulnerabilities as $vulnerability ) {
if ( ! is_array( $vulnerability ) ) {
continue;
}
$vuln_versions_raw = $vulnerability['versions'] ?? '';
$vuln_versions = is_scalar( $vuln_versions_raw ) ? (string) $vuln_versions_raw : '';
$vuln_closed_raw = $vulnerability['closed'] ?? 0;
$vuln_closed = is_scalar( $vuln_closed_raw ) ? intval( $vuln_closed_raw ) : 0;
$vuln_unfixed_raw = $vulnerability['unfixed'] ?? 0;
$vuln_unfixed = is_scalar( $vuln_unfixed_raw ) ? intval( $vuln_unfixed_raw ) : 0;
$vuln_impact = isset( $vulnerability['impact'] ) && is_array( $vulnerability['impact'] ) ? $vulnerability['impact'] : array();
$vuln_cvss = isset( $vuln_impact['cvss'] ) && is_array( $vuln_impact['cvss'] ) ? $vuln_impact['cvss'] : array();
$vuln_cvss2 = isset( $vuln_impact['cvss2'] ) && is_array( $vuln_impact['cvss2'] ) ? $vuln_impact['cvss2'] : array();
$vuln_cvss3 = isset( $vuln_impact['cvss3'] ) && is_array( $vuln_impact['cvss3'] ) ? $vuln_impact['cvss3'] : array();
$vuln_cvss4 = isset( $vuln_impact['cvss4'] ) && is_array( $vuln_impact['cvss4'] ) ? $vuln_impact['cvss4'] : array();
$vuln_ssvc = isset( $vuln_impact['ssvc'] ) && is_array( $vuln_impact['ssvc'] ) ? $vuln_impact['ssvc'] : array();
$vuln_cwe = isset( $vuln_impact['cwe'] ) && is_array( $vuln_impact['cwe'] ) ? $vuln_impact['cwe'] : array();
$vuln_sources = isset( $vulnerability['source'] ) && is_array( $vulnerability['source'] ) ? $vulnerability['source'] : array();
$kev = ( isset( $vuln_ssvc['kev'] ) && true === $vuln_ssvc['kev'] );
$exploitation = isset( $vuln_ssvc['exploitation'] ) && is_string( $vuln_ssvc['exploitation'] ) ? $vuln_ssvc['exploitation'] : '';
$automatable = isset( $vuln_ssvc['automatable'] ) && is_string( $vuln_ssvc['automatable'] ) ? $vuln_ssvc['automatable'] : '';
$kev_date_raw = $vuln_ssvc['kev_date'] ?? null;
$kev_date = is_string( $kev_date_raw ) && '' !== $kev_date_raw ? $kev_date_raw : null;
$epss_raw = $vuln_impact['epss'] ?? null;
$epss = is_numeric( $epss_raw ) ? (float) $epss_raw : null;
$description = wpvulnerability_get_source_description( $vuln_sources );
// Best available CVSS score and severity: cvss4 > cvss3 > cvss2 > legacy cvss.
$score_raw = null;
$sev_raw = null;
foreach ( array( $vuln_cvss4, $vuln_cvss3, $vuln_cvss2, $vuln_cvss ) as $cvss_c ) {
if ( empty( $cvss_c ) ) {
continue;
}
$s_raw = $cvss_c['score'] ?? null;
$v_raw = $cvss_c['severity'] ?? null;
$s = is_numeric( $s_raw ) ? number_format( (float) $s_raw, 1, '.', '' ) : null;
$v = is_string( $v_raw ) && '' !== $v_raw ? $v_raw : null;
if ( null !== $s || null !== $v ) {
$score_raw = $s;
$sev_raw = $v;
break;
}
}
$what = array();
foreach ( $vuln_cwe as $vulnerability_cwe ) {
if ( ! is_array( $vulnerability_cwe ) ) {
continue;
}
$cwe_name = $vulnerability_cwe['name'] ?? '';
$cwe_desc = $vulnerability_cwe['description'] ?? '';
$what[] = '<div><b>' . wp_kses( is_scalar( $cwe_name ) ? (string) $cwe_name : '', 'strip' ) . '</b></div><div><i>' . esc_html( is_scalar( $cwe_desc ) ? (string) $cwe_desc : '' ) . '</i></div>';
}
$version_display = wpvulnerability_clean_version_range( $vuln_versions );
$source_pills = wpvulnerability_render_source_pills( $vuln_sources );
$score_badge = wpvulnerability_render_score_badge( $score_raw, $sev_raw, $epss );
$information .= '<tr>';
// Version range column.
$information .= '<td style="max-width: 256px; min-width: 96px; vertical-align: top; padding-top: 6px;">';
$information .= '' !== $version_display
? '<span class="wpvuln-versions">' . $version_display . '</span>'
: '&mdash;';
$information .= '</td>';
// Details column.
$information .= '<td>';
$show_active = $kev || 'active' === $exploitation;
$show_poc = 'poc' === $exploitation;
$show_auto = 'yes' === $automatable;
if ( $show_active || $show_poc || $show_auto || '' !== $score_badge ) {
$information .= '<div style="display:flex; align-items:center; gap:6px; flex-wrap:wrap; margin-bottom:5px;">';
if ( $show_active ) {
$information .= '<span class="wpvuln-kev-label">&#9888; ' . esc_html__( 'Actively exploited', 'wpvulnerability' );
if ( $kev && null !== $kev_date ) {
$information .= ' &middot; ' . esc_html( $kev_date );
}
$information .= '</span>';
}
if ( $show_poc ) {
$information .= '<span class="wpvuln-poc-label">&#9889; ' . esc_html__( 'Public exploit', 'wpvulnerability' ) . '</span>';
}
if ( $show_auto ) {
$information .= '<span class="wpvuln-auto-label">&#9881; ' . esc_html__( 'Automatable', 'wpvulnerability' ) . '</span>';
}
if ( '' !== $score_badge ) {
$information .= $score_badge;
}
$information .= '</div>';
}
if ( null !== $description ) {
$information .= '<div style="padding-bottom: 5px;">' . esc_html( $description ) . '</div>';
}
if ( $vuln_closed || $vuln_unfixed ) {
$information .= '<div style="padding-bottom: 5px;">';
if ( $vuln_closed ) {
$information .= '<div class="text-red">' . esc_html__( 'This plugin is closed. Please replace it with another.', 'wpvulnerability' ) . '</div>';
}
if ( $vuln_unfixed ) {
$information .= '<div class="text-red">' . esc_html__( 'This vulnerability appears to be unpatched. Stay tuned for upcoming plugin updates.', 'wpvulnerability' ) . '</div>';
}
$information .= '</div>';
}
if ( ! empty( $what ) ) {
$information .= '<div style="padding-bottom: 5px;">';
foreach ( $what as $w ) {
$information .= $w;
}
$information .= '</div>';
}
if ( '' !== $source_pills ) {
$information .= '<div class="wpvuln-refs-row"><span class="wpvuln-refs-label">' . esc_html__( 'References:', 'wpvulnerability' ) . '</span>';
$information .= $source_pills;
$information .= '</div>';
}
$information .= '</td>';
$information .= '</tr>';
}
$information .= '</table>';
$information .= '</td>';
$information .= '</tr>';
echo $information; // phpcs:ignore
}
}
/**
* Retrieves vulnerabilities for a given plugin and updates its data.
*
* @since 2.0.0
*
* @param array<string, mixed> $plugin_data The plugin data array.
* @param string $file_path The path to the plugin file.
*
* @return array<string, mixed> The updated plugin data array.
*/
function wpvulnerability_get_fresh_plugin_vulnerabilities( $plugin_data, $file_path ) {
$plugin_slug = null;
// Extract the folder name from the file path.
$folder_name = explode( '/', $file_path );
// Use the first folder segment as the plugin slug.
$plugin_slug = wp_kses( trim( (string) $folder_name[0] ), 'strip' );
unset( $folder_name );
// If the plugin slug is empty, fall back to the TextDomain key.
if ( empty( $plugin_slug ) && isset( $plugin_data['TextDomain'] ) ) {
$td_raw = $plugin_data['TextDomain'];
$plugin_slug = wp_kses( is_scalar( $td_raw ) ? (string) $td_raw : '', 'strip' );
}
// Get the plugin version from the plugin data.
$plugin_version_raw = $plugin_data['Version'] ?? '';
$plugin_version = wp_kses( is_scalar( $plugin_version_raw ) ? (string) $plugin_version_raw : '', 'strip' );
// Initialize vulnerability-related fields.
$plugin_data['vulnerabilities'] = null;
$plugin_data['vulnerable'] = 0;
// Retrieve vulnerabilities for the plugin using its slug and version.
if ( ! empty( $plugin_slug ) ) {
$plugin_api_response = wpvulnerability_get_plugin( $plugin_slug, $plugin_version, 0, 0 );
// If vulnerabilities are found, update the plugin data accordingly.
if ( ! empty( $plugin_api_response ) ) {
$plugin_data['slug'] = $plugin_slug;
$plugin_data['vulnerabilities'] = $plugin_api_response;
$plugin_data['vulnerable'] = 1;
}
}
return $plugin_data;
}
/**
* Retrieves updated data for a specified plugin, potentially including vulnerability information.
*
* @since 3.1.0
*
* @param array<string, mixed> $plugin_data The original plugin data array, expected to contain keys like 'TextDomain' and 'Version'.
* @param string $file_path The file path of the plugin, used to determine the plugin's slug if 'TextDomain' is not specified in `$plugin_data`.
*
* @return array<mixed>|null Updated plugin data array with fresh information or null if the plugin slug cannot be determined or no updated information is available.
*/
function wpvulnerability_get_fresh_plugin_data( $plugin_data, $file_path ) {
$plugin_slug = '';
// Extract the folder name from the file path.
$folder_name = explode( '/', $file_path );
// Use the first folder segment as the plugin slug.
$plugin_slug = wp_kses( trim( (string) $folder_name[0] ), 'strip' );
unset( $folder_name );
// If the plugin slug is still empty, use the TextDomain key from the plugin data if it exists.
if ( '' === $plugin_slug && isset( $plugin_data['TextDomain'] ) ) {
$td_raw2 = $plugin_data['TextDomain'];
$plugin_slug = wp_kses( is_scalar( $td_raw2 ) ? (string) $td_raw2 : '', 'strip' );
}
// Get the plugin version from the plugin data if it exists.
$plugin_version_raw2 = $plugin_data['Version'] ?? '';
$plugin_version = wp_kses( is_scalar( $plugin_version_raw2 ) ? (string) $plugin_version_raw2 : '', 'strip' );
// Retrieve vulnerabilities for the plugin using its slug and version.
if ( ! empty( $plugin_slug ) ) {
$plugin_api_response = wpvulnerability_get_plugin( $plugin_slug, $plugin_version, 1, 1 );
// If vulnerabilities are found, return the updated plugin data.
if ( ! empty( $plugin_api_response ) ) {
return $plugin_api_response;
}
}
return null; // Return null if no valid data is found.
}
/**
* Get Installed Plugins
* Retrieves the list of installed plugins, checks for vulnerabilities in each of them, caches the data, and sends an email notification if vulnerabilities are detected.
*
* @since 2.0.0
* @since 4.1.2 Stores a signature of the installed plugins to detect inventory changes.
*
* @return string JSON-encoded array of plugin data with vulnerabilities and vulnerable status, or '[]' on encoding error.
*/
function wpvulnerability_plugin_get_installed() {
$wpvulnerability_plugins_vulnerable = 0;
// Ensure the get_plugins() function is available.
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
// Retrieve the list of installed plugins.
$plugins = get_plugins();
$signature = wpvulnerability_plugins_generate_signature( $plugins );
// Iterate through each plugin and check for vulnerabilities.
foreach ( $plugins as $file_path => $plugin_data ) {
$plugins[ $file_path ] = wpvulnerability_get_fresh_plugin_vulnerabilities( $plugin_data, $file_path );
// Increment the vulnerable plugin counter if vulnerabilities are found.
$vuln_flag = $plugins[ $file_path ]['vulnerable'] ?? null;
if ( is_scalar( $vuln_flag ) && (int) $vuln_flag ) {
++$wpvulnerability_plugins_vulnerable;
}
}
// Update site options for multisite installations.
if ( is_multisite() ) {
update_site_option( 'wpvulnerability-plugins', wp_json_encode( $plugins ) );
update_site_option( 'wpvulnerability-plugins-vulnerable', wp_json_encode( number_format( $wpvulnerability_plugins_vulnerable, 0, '.', '' ) ) );
update_site_option( 'wpvulnerability-plugins-cache', wp_json_encode( number_format( time() + ( 3600 * wpvulnerability_cache_hours() ), 0, '.', '' ) ) );
update_site_option( 'wpvulnerability-plugins-signature', wp_json_encode( $signature ) );
} else {
// Update options for single site installations.
update_option( 'wpvulnerability-plugins', wp_json_encode( $plugins ), false );
update_option( 'wpvulnerability-plugins-vulnerable', wp_json_encode( number_format( $wpvulnerability_plugins_vulnerable, 0, '.', '' ) ), false );
update_option( 'wpvulnerability-plugins-cache', wp_json_encode( number_format( time() + ( 3600 * wpvulnerability_cache_hours() ), 0, '.', '' ) ), false );
update_option( 'wpvulnerability-plugins-signature', wp_json_encode( $signature ), false );
}
// Return the JSON-encoded array of plugin data.
$encoded = wp_json_encode( $plugins );
return false !== $encoded ? $encoded : '[]';
}
/**
* Retrieves cached data for installed plugins, optionally refreshing when forced.
*
* @since 3.1.0
* @since 4.1.2 Refreshes automatically when the installed plugins signature changes.
*
* @param bool $clean Optional. Whether to force a refresh of the plugin data cache. Default false.
*
* @return string JSON-encoded array of plugin data, or '[]' on encoding error.
*/
function wpvulnerability_plugin_get_data( $clean = false ) {
if ( true === $clean ) {
// Ensure the get_plugins() function is available.
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
// Retrieve the list of installed plugins.
$plugins = get_plugins();
$pluginsdata = array();
$signature = wpvulnerability_plugins_generate_signature( $plugins );
// Iterate through each plugin and get fresh data.
foreach ( $plugins as $file_path => $plugin_data ) {
$pluginsdata[ $file_path ] = wpvulnerability_get_fresh_plugin_data( $plugin_data, $file_path );
}
// Update site options for multisite installations.
if ( is_multisite() ) {
update_site_option( 'wpvulnerability-plugins-data', wp_json_encode( $pluginsdata ) );
update_site_option( 'wpvulnerability-plugins-cache-data', wp_json_encode( number_format( time() + ( 3600 * wpvulnerability_cache_hours() ), 0, '.', '' ) ) );
update_site_option( 'wpvulnerability-plugins-cache', wp_json_encode( number_format( time() + ( 3600 * wpvulnerability_cache_hours() ), 0, '.', '' ) ) );
update_site_option( 'wpvulnerability-plugins-signature', wp_json_encode( $signature ) );
} else {
// Update options for single site installations.
update_option( 'wpvulnerability-plugins-data', wp_json_encode( $pluginsdata ), false );
update_option( 'wpvulnerability-plugins-cache-data', wp_json_encode( number_format( time() + ( 3600 * wpvulnerability_cache_hours() ), 0, '.', '' ) ), false );
update_option( 'wpvulnerability-plugins-cache', wp_json_encode( number_format( time() + ( 3600 * wpvulnerability_cache_hours() ), 0, '.', '' ) ), false );
update_option( 'wpvulnerability-plugins-signature', wp_json_encode( $signature ), false );
}
$encoded_data = wp_json_encode( $pluginsdata );
return false !== $encoded_data ? $encoded_data : '[]';
}
$raw_pd = is_multisite() ? get_site_option( 'wpvulnerability-plugins-data', '' ) : get_option( 'wpvulnerability-plugins-data', '' );
$plugin_data = json_decode( is_string( $raw_pd ) ? $raw_pd : '', true );
if ( ! is_array( $plugin_data ) ) {
$plugin_data = array();
}
$encoded = wp_json_encode( $plugin_data );
return false !== $encoded ? $encoded : '[]';
}
/**
* Get cached plugin vulnerabilities without contacting the API. Data is refreshed by scheduled or manual updates.
*
* @since 2.0.0
* @since 4.1.2 Refreshes when the installed plugins signature changes.
*
* @return array<string, mixed> Array of installed plugins with their vulnerabilities.
*/
function wpvulnerability_plugin_get_vulnerabilities() {
$raw_data = is_multisite() ? get_site_option( 'wpvulnerability-plugins', '' ) : get_option( 'wpvulnerability-plugins', '' );
$plugin_data = json_decode( is_string( $raw_data ) ? $raw_data : '', true );
return is_array( $plugin_data ) ? $plugin_data : array();
}
/**
* Update the installed plugins cache and remove any old cache data.
*
* @since 2.0.0
*
* @return void
*/
function wpvulnerability_plugin_get_vulnerabilities_clean() {
wpvulnerability_clear_cache( 'plugins' );
wpvulnerability_plugin_get_installed();
wpvulnerability_plugin_get_data( true );
}
/**
* Displays information in the 'Last Updated' column for each plugin in the plugins list table.
*
* This function is triggered for each row in the plugins list table when the 'Last Updated' column is rendered.
* It retrieves the last update date from stored plugin data, compares it against the current date to highlight
* plugins not updated in over a year or those marked as closed, and displays this information.
*
* @since 3.1.0 Introduced.
*
* @param string $column_name The name of the current column being rendered.
* @param string $plugin_file Path to the plugin file, relative to the plugins directory.
* @param array<string, mixed> $plugin_data Array of plugin data, such as the plugin's name, version, and description.
*
* @return void Outputs the last updated information directly to the browser, including any warnings for plugins
* not updated in over a year or marked as closed.
*/
function wpvulnerability_plugin_show_lastupdated( $column_name, $plugin_file, $plugin_data ) {
static $plugins_data = null;
$now = time();
$year = strtotime( '-1 year', $now );
if ( 'last_updated' === $column_name && $plugin_file ) {
$plugin_slug = '';
// Extract the plugin slug from the file path.
$folder_name = explode( '/', $plugin_file );
// Use the first folder segment as the plugin slug.
$plugin_slug = wp_kses( trim( (string) $folder_name[0] ), 'strip' );
unset( $folder_name );
// If the plugin slug is empty, extract it from the plugin data.
if ( '' === $plugin_slug && isset( $plugin_data['TextDomain'] ) ) {
$td_raw3 = $plugin_data['TextDomain'];
$plugin_slug = wp_kses( is_scalar( $td_raw3 ) ? (string) $td_raw3 : '', 'strip' );
}
if ( '' !== $plugin_slug ) {
// Retrieve the vulnerabilities for all plugins once per request; this
// callback runs for every row of the plugins list table.
if ( null === $plugins_data ) {
$raw_plugins_data = is_multisite() ? get_site_option( 'wpvulnerability-plugins-data', '' ) : get_option( 'wpvulnerability-plugins-data', '' );
$plugins_data = json_decode( is_string( $raw_plugins_data ) ? $raw_plugins_data : '', true );
if ( ! is_array( $plugins_data ) ) {
$plugins_data = array();
}
}
// Get the plugin data from the stored data.
if ( isset( $plugins_data[ $plugin_file ] ) && is_array( $plugins_data[ $plugin_file ] ) ) {
$pd = $plugins_data[ $plugin_file ];
$pd_latest_raw = $pd['latest'] ?? 0;
$pd_latest = is_scalar( $pd_latest_raw ) ? intval( $pd_latest_raw ) : 0;
if ( $pd_latest > 0 ) {
$timestamp = $pd_latest;
$df_raw = get_option( 'date_format', 'Y-m-d' );
$date_format = is_scalar( $df_raw ) ? (string) $df_raw : 'Y-m-d';
$plugin_data_updated = (string) wp_date( $date_format, $timestamp );
$plugin_data_ago = human_time_diff( $timestamp );
$warning_date = $pd_latest < $year;
$pd_closed_raw = $pd['closed'] ?? 0;
$warning_closed = isset( $pd['closed'] ) && ( is_scalar( $pd_closed_raw ) ? intval( $pd_closed_raw ) : 0 );
echo '<p>' . wp_kses( $plugin_data_updated, 'strip' ) . ' (' . wp_kses( (string) $plugin_data_ago, 'strip' ) . ')</p>';
if ( $warning_date ) {
echo '<p><strong>⚠️ ';
esc_html_e( 'It hasn\'t been updated in over a year.', 'wpvulnerability' );
echo '</strong></p>';
}
if ( $warning_closed ) {
echo '<p><strong>⚠️ ';
esc_html_e( 'It may no longer be available (closed?).', 'wpvulnerability' );
echo '</strong></p>';
}
} else {
echo '<p></p>';
}
}
}
}
}
/**
* Adds a 'Last Updated' column to the plugins table list in the WordPress admin area.
*
* This function iterates over the existing columns in the plugins table and inserts a new column titled 'Last Updated'
* just after the 'description' column if it exists. If the 'description' column is not found, the 'Last Updated'
* column is appended at the end. The function is typically hooked to the 'manage_plugins_columns' filter in WordPress
* to modify the columns of the plugins table.
*
* @since 3.1.0 Introduced.
*
* @param array<string, string> $columns An associative array of column names and titles for the plugins table.
*
* @return array<string, string> An associative array containing the modified list of columns, including the new 'Last Updated' column.
*/
function wpvulnerability_plugin_add_lastupdated_column( $columns ) {
$toadd = true;
$new_columns = array();
// Loop through each existing column and add it to the new columns array.
foreach ( $columns as $key => $title ) {
// Add the existing column to the new columns array.
$new_columns[ $key ] = $title;
// Insert your custom column before the 'auto-updates' column.
if ( 'description' === $key && $toadd ) {
$new_columns['last_updated'] = __( 'Last updated on', 'wpvulnerability' );
$toadd = false;
}
}
// If 'auto-updates' column is not found, add 'last_updated' column at the end.
if ( $toadd ) {
$new_columns['last_updated'] = __( 'Last updated on', 'wpvulnerability' );
}
// Return the modified columns array.
return $new_columns;
}
/**
* Admin Head
* Adds vulnerability information after the plugin row and notices on the plugin page based on the installed plugins cache.
*
* @since 2.0.0
*
* @return void
*/
function wpvulnerability_plugin_page() {
// Check if the current page is the plugins page.
global $pagenow;
if ( wpvulnerability_analyze_filter( 'plugins' ) && 'plugins.php' === $pagenow && wpvulnerability_capabilities() ) {
// Get the vulnerabilities for the installed plugins.
$plugins = wpvulnerability_plugin_get_vulnerabilities();
// Loop through the plugins and add vulnerability information after the plugin row for vulnerable plugins.
foreach ( $plugins as $file_path => $plugin_data ) {
if ( is_array( $plugin_data ) && isset( $plugin_data['vulnerable'] ) ) {
$vulnerable_raw = $plugin_data['vulnerable'];
if ( 1 === ( is_scalar( $vulnerable_raw ) ? intval( $vulnerable_raw ) : 0 ) ) {
add_action( 'after_plugin_row_' . $file_path, 'wpvulnerability_plugin_info_after', 10, 2 );
}
}
}
// Add 'Last Updated' column to the plugins table based on user capabilities.
if ( is_multisite() ) {
add_filter( 'manage_plugins-network_columns', 'wpvulnerability_plugin_add_lastupdated_column' );
} else {
add_filter( 'manage_plugins_columns', 'wpvulnerability_plugin_add_lastupdated_column' );
}
add_action( 'manage_plugins_custom_column', 'wpvulnerability_plugin_show_lastupdated', 10, 3 );
}
}
// Add notices for vulnerable plugins on the plugin page.
add_action( 'admin_head', 'wpvulnerability_plugin_page' );
/**
* Filters the plugins list to show only vulnerable plugins when the "Vulnerable" tab is selected.
*
* This function hooks into the WordPress plugins listing to filter the displayed plugins based on their
* vulnerability status. When the "Vulnerable" tab is selected (identified by the `plugin_status=vulnerable`
* query parameter), it filters the plugins list to include only those plugins with known vulnerabilities.
*
* The function retrieves the vulnerabilities for all plugins from the WordPress options table and compares
* them against the active list of plugins. Plugins without vulnerabilities are removed from the list, leaving
* only those that are considered vulnerable.
*
* @since 3.3.5
*
* @return void
*/
function wpvulnerability_plugins_filter() {
if ( isset( $_GET['plugin_status'] ) && 'vulnerable' === $_GET['plugin_status'] ) { // phpcs:ignore
// Verify nonce for CSRF protection.
$nonce_raw = isset( $_GET['wpv_nonce'] ) && is_string( $_GET['wpv_nonce'] ) ? $_GET['wpv_nonce'] : ''; // phpcs:ignore
$nonce = sanitize_text_field( wp_unslash( $nonce_raw ) );
if ( ! wp_verify_nonce( $nonce, 'wpvulnerability_filter_plugins' ) ) {
// If nonce verification fails, silently return without filtering.
// This provides graceful degradation - users simply see all plugins instead of an error.
return;
}
global $wp_list_table;
// Retrieve the vulnerabilities for all plugins from the options table and decode the JSON.
$raw_pv = is_multisite() ? get_site_option( 'wpvulnerability-plugins', '' ) : get_option( 'wpvulnerability-plugins', '' );
$plugin_vulnerabilities = json_decode( is_string( $raw_pv ) ? $raw_pv : '', true );
if ( ! is_array( $plugin_vulnerabilities ) ) {
$plugin_vulnerabilities = array();
}
foreach ( $wp_list_table->items as $plugin_file => $plugin_data ) {
$pf_data = isset( $plugin_vulnerabilities[ $plugin_file ] ) && is_array( $plugin_vulnerabilities[ $plugin_file ] ) ? $plugin_vulnerabilities[ $plugin_file ] : array();
$pf_vulns = isset( $pf_data['vulnerabilities'] ) && is_array( $pf_data['vulnerabilities'] ) ? $pf_data['vulnerabilities'] : array();
if ( empty( $pf_vulns ) ) {
unset( $wp_list_table->items[ $plugin_file ] );
}
}
}
}
add_action( 'pre_current_active_plugins', 'wpvulnerability_plugins_filter' );
/**
* Adds a "Vulnerable" tab to the WordPress plugins page that displays the count of vulnerable plugins.
*
* This function checks the cache for the number of vulnerable plugins and adds a new tab to the plugins
* management page in the WordPress admin area. The tab displays the count of vulnerable plugins and highlights it
* if it is currently active.
*
* @since 3.3.5
*
* @param array<string, string> $views An array of existing plugin views (tabs) in the WordPress admin plugins page.
*
* @return array<string, string> The modified array of views including the "Vulnerable" tab.
*/
function wpvulnerability_plugins_view( $views ) {
if ( ! wpvulnerability_analyze_filter( 'plugins' ) ) {
return $views;
}
// Retrieve the number of plugins vulnerabilities from cache.
$raw_count = is_multisite()
? get_site_option( 'wpvulnerability-plugins-vulnerable', '0' )
: get_option( 'wpvulnerability-plugins-vulnerable', '0' );
$decoded_count = json_decode( is_string( $raw_count ) ? $raw_count : '0', true );
$wpvulnerability_plugins_total = is_scalar( $decoded_count ) ? intval( $decoded_count ) : 0;
$current_class = ( isset( $_GET['plugin_status'] ) && 'vulnerable' === $_GET['plugin_status'] ) ? ' class="current"' : ''; // phpcs:ignore
$url = is_multisite()
? network_admin_url( 'plugins.php?plugin_status=vulnerable' )
: admin_url( 'plugins.php?plugin_status=vulnerable' );
// Add nonce for CSRF protection.
$url = esc_url( wp_nonce_url( $url, 'wpvulnerability_filter_plugins', 'wpv_nonce' ) );
$views['vulnerable'] = sprintf(
'<a href="%s"%s>%s</a>',
$url,
$current_class,
// translators: the number of vulnerabilities.
sprintf( __( 'Vulnerabilities (%d)', 'wpvulnerability' ), $wpvulnerability_plugins_total )
);
return $views;
}
/**
* Adds a custom filter to the plugins page in the WordPress admin to display a tab for vulnerable plugins.
*
* This function hooks into the 'views_plugins' filter to add a custom tab or view for displaying vulnerable plugins
* on the plugins management page in the WordPress admin area. The tab is added in both single-site and multisite
* installations, but in a multisite setup, it is only added to the network admin area.
*
* @since 3.3.5
*
* @return void
*/
function wpvulnerability_plugins_add_tab() {
if ( is_multisite() ) {
if ( is_network_admin() ) {
add_filter( 'views_plugins-network', 'wpvulnerability_plugins_view' );
}
} else {
add_filter( 'views_plugins', 'wpvulnerability_plugins_view' );
}
}
add_action( 'admin_head', 'wpvulnerability_plugins_add_tab' );