> $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 $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 = ''; $information .= ''; $information .= '

' . esc_html( $message ) . ''; $information .= '

'; $information .= ''; // 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[] = '
' . wp_kses( is_scalar( $cwe_name ) ? (string) $cwe_name : '', 'strip' ) . '
' . esc_html( is_scalar( $cwe_desc ) ? (string) $cwe_desc : '' ) . '
'; } $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 .= ''; // Version range column. $information .= ''; // Details column. $information .= ''; $information .= ''; } $information .= '
'; $information .= '' !== $version_display ? '' . $version_display . '' : '—'; $information .= ''; $show_active = $kev || 'active' === $exploitation; $show_poc = 'poc' === $exploitation; $show_auto = 'yes' === $automatable; if ( $show_active || $show_poc || $show_auto || '' !== $score_badge ) { $information .= '
'; if ( $show_active ) { $information .= '⚠ ' . esc_html__( 'Actively exploited', 'wpvulnerability' ); if ( $kev && null !== $kev_date ) { $information .= ' · ' . esc_html( $kev_date ); } $information .= ''; } if ( $show_poc ) { $information .= '⚡ ' . esc_html__( 'Public exploit', 'wpvulnerability' ) . ''; } if ( $show_auto ) { $information .= '⚙ ' . esc_html__( 'Automatable', 'wpvulnerability' ) . ''; } if ( '' !== $score_badge ) { $information .= $score_badge; } $information .= '
'; } if ( null !== $description ) { $information .= '
' . esc_html( $description ) . '
'; } if ( $vuln_closed || $vuln_unfixed ) { $information .= '
'; if ( $vuln_closed ) { $information .= '
' . esc_html__( 'This plugin is closed. Please replace it with another.', 'wpvulnerability' ) . '
'; } if ( $vuln_unfixed ) { $information .= '
' . esc_html__( 'This vulnerability appears to be unpatched. Stay tuned for upcoming plugin updates.', 'wpvulnerability' ) . '
'; } $information .= '
'; } if ( ! empty( $what ) ) { $information .= '
'; foreach ( $what as $w ) { $information .= $w; } $information .= '
'; } if ( '' !== $source_pills ) { $information .= '
' . esc_html__( 'References:', 'wpvulnerability' ) . ''; $information .= $source_pills; $information .= '
'; } $information .= '
'; $information .= ''; $information .= ''; echo $information; // phpcs:ignore } } /** * Retrieves vulnerabilities for a given plugin and updates its data. * * @since 2.0.0 * * @param array $plugin_data The plugin data array. * @param string $file_path The path to the plugin file. * * @return array 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 $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|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 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 $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 ) { $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 from the options table and decode the JSON. $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'; if ( function_exists( 'wp_date' ) ) { $plugin_data_updated = (string) wp_date( $date_format, $timestamp ); } else { $plugin_data_updated = gmdate( $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 '

' . wp_kses( $plugin_data_updated, 'strip' ) . ' (' . wp_kses( (string) $plugin_data_ago, 'strip' ) . ')

'; if ( $warning_date ) { echo '

⚠️ '; esc_html_e( 'It hasn\'t been updated in over a year.', 'wpvulnerability' ); echo '

'; } if ( $warning_closed ) { echo '

⚠️ '; esc_html_e( 'It may no longer be available (closed?).', 'wpvulnerability' ); echo '

'; } } else { echo '

'; } } } } } /** * 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 before the 'auto-updates' column if it exists. If the 'auto-updates' 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 $columns An associative array of column names and titles for the plugins table. * * @return array 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 $views An array of existing plugin views (tabs) in the WordPress admin plugins page. * * @return array 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( '%s', $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' );