> $themes List of themes returned by wp_get_themes().
* @return string Hash representing the installed themes and their versions.
*/
function wpvulnerability_themes_generate_signature( $themes ) {
$normalized = array();
foreach ( $themes as $slug => $theme_data ) {
$theme_slug = sanitize_text_field( (string) $slug );
$version = '';
if ( $theme_data instanceof WP_Theme ) {
$version = sanitize_text_field( (string) $theme_data->get( 'Version' ) );
} elseif ( isset( $theme_data['Version'] ) ) {
$version = sanitize_text_field( is_scalar( $theme_data['Version'] ) ? (string) $theme_data['Version'] : '' );
}
$normalized[ $theme_slug ] = $version;
}
ksort( $normalized );
$encoded = wp_json_encode( $normalized );
return md5( false !== $encoded ? $encoded : '' );
}
/**
* Retrieve the signature of the currently installed themes.
*
* @since 4.1.2
*
* @return string Hash representing the installed themes and their versions.
*/
function wpvulnerability_themes_get_current_signature() {
return wpvulnerability_themes_generate_signature( wp_get_themes() );
}
/**
* Adds a vulnerability notice under vulnerable themes.
*
* @since 2.0.0
*
* @param string $theme_file Main themes folder/file name.
* @param WP_Theme $theme_data Theme data object.
*
* @return void
*/
function wpvulnerability_theme_info_after( $theme_file, $theme_data ) {
// Retrieve the vulnerabilities for all themes from the options table and decode the JSON.
$raw_themes = is_multisite() ? get_site_option( 'wpvulnerability-themes', '' ) : get_option( 'wpvulnerability-themes', '' );
$theme_vulnerabilities = json_decode( is_string( $raw_themes ) ? $raw_themes : '', true );
if ( ! is_array( $theme_vulnerabilities ) ) {
$theme_vulnerabilities = array();
}
// Determine whether the theme is active and add an appropriate CSS class to the table row.
$current_theme = wp_get_theme();
$tr_class = '';
if ( $theme_file === $current_theme->get_stylesheet() ) {
$tr_class .= 'active';
}
// Generate the vulnerability notice message with the theme 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( (string) $theme_data->get( '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 .= '';
// Loop through all vulnerabilities for the current theme and add their details to the table row HTML markup.
$tf_entry = isset( $theme_vulnerabilities[ $theme_file ] ) && is_array( $theme_vulnerabilities[ $theme_file ] ) ? $theme_vulnerabilities[ $theme_file ] : array();
$tf_wpv = isset( $tf_entry['wpvulnerability'] ) && is_array( $tf_entry['wpvulnerability'] ) ? $tf_entry['wpvulnerability'] : array();
$vulnerabilities = isset( $tf_wpv['vulnerabilities'] ) && is_array( $tf_wpv['vulnerabilities'] ) ? $tf_wpv['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 .= '| ';
$information .= '' !== $version_display
? '' . $version_display . ''
: '—';
$information .= ' | ';
// Details column.
$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 theme is closed. Please replace it with another.', 'wpvulnerability' ) . ' ';
}
if ( $vuln_unfixed ) {
$information .= ' ' . esc_html__( 'This vulnerability appears to be unpatched. Stay tuned for upcoming theme 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 .= ' ';
$information .= ' | ';
$information .= '
';
echo $information; // phpcs:ignore
}
/**
* Retrieves vulnerabilities for a given theme and updates its data.
*
* @since 2.0.0
*
* @param array $theme_data The theme data array (must contain a 'data' key with a WP_Theme object).
* @param string $theme_slug The slug to the theme.
*
* @return array The updated theme data array.
*/
function wpvulnerability_get_fresh_theme_vulnerabilities( $theme_data, $theme_slug ) {
// Get the theme version and slug from the theme data.
$theme_obj = isset( $theme_data['data'] ) && ( $theme_data['data'] instanceof WP_Theme ) ? $theme_data['data'] : null;
$theme_version = null !== $theme_obj ? wp_kses( (string) $theme_obj->get( 'Version' ), 'strip' ) : '';
$theme_data_v = array();
$theme_data_v['slug'] = $theme_slug;
$theme_data_v['name'] = null !== $theme_obj ? (string) $theme_obj->get( 'Name' ) : '';
// Initialize vulnerability related fields.
$theme_data_v['vulnerabilities'] = null;
$theme_data_v['vulnerable'] = 0;
// Retrieve vulnerabilities for the theme using its slug and version.
if ( ! empty( $theme_slug ) ) {
$theme_api_response = wpvulnerability_get_theme( $theme_slug, $theme_version, 0 );
// If vulnerabilities are found, update the theme data accordingly.
if ( ! empty( $theme_api_response ) ) {
$theme_data_v['vulnerabilities'] = $theme_api_response;
$theme_data_v['vulnerable'] = 1;
}
}
return $theme_data_v;
}
/**
* Get Installed Themes
* Retrieves the list of installed themes, 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 themes to detect inventory changes.
*
* @return string JSON-encoded array of theme data with vulnerabilities and vulnerable status, or '[]' on encoding error.
*/
function wpvulnerability_theme_get_installed() {
$wpvulnerability_themes_vulnerable = 0;
$themes_v = array();
$themes = wp_get_themes();
$signature = wpvulnerability_themes_generate_signature( $themes );
foreach ( $themes as $slug => $theme_data ) {
// Store the theme data.
$themes_v[ $slug ]['data'] = $theme_data;
// Get fresh vulnerabilities for the theme.
$themes_v[ $slug ]['wpvulnerability'] = wpvulnerability_get_fresh_theme_vulnerabilities( $themes_v[ $slug ], $slug );
// If the theme is vulnerable, increment the vulnerable themes counter.
if ( isset( $themes_v[ $slug ]['wpvulnerability']['vulnerable'] ) && 1 === ( is_scalar( $themes_v[ $slug ]['wpvulnerability']['vulnerable'] ) ? (int) $themes_v[ $slug ]['wpvulnerability']['vulnerable'] : 0 ) ) {
++$wpvulnerability_themes_vulnerable;
}
}
// Update options for multisite installations.
if ( is_multisite() ) {
update_site_option( 'wpvulnerability-themes', wp_json_encode( $themes_v ) );
update_site_option( 'wpvulnerability-themes-vulnerable', wp_json_encode( number_format( $wpvulnerability_themes_vulnerable, 0, '.', '' ) ) );
update_site_option( 'wpvulnerability-themes-cache', wp_json_encode( number_format( time() + ( 3600 * wpvulnerability_cache_hours() ), 0, '.', '' ) ) );
update_site_option( 'wpvulnerability-themes-signature', wp_json_encode( $signature ) );
} else {
update_option( 'wpvulnerability-themes', wp_json_encode( $themes_v ), false );
update_option( 'wpvulnerability-themes-vulnerable', wp_json_encode( number_format( $wpvulnerability_themes_vulnerable, 0, '.', '' ) ), false );
update_option( 'wpvulnerability-themes-cache', wp_json_encode( number_format( time() + ( 3600 * wpvulnerability_cache_hours() ), 0, '.', '' ) ), false );
update_option( 'wpvulnerability-themes-signature', wp_json_encode( $signature ), false );
}
$encoded = wp_json_encode( $themes_v );
return false !== $encoded ? $encoded : '[]';
}
/**
* Get cached themes vulnerabilities without contacting the API. Data updates via scheduled or manual refreshes.
*
* @since 2.0.0
* @since 4.1.2 Refreshes when the installed themes signature changes.
*
* @return array Array of installed themes with their vulnerabilities.
*/
function wpvulnerability_theme_get_vulnerabilities() {
$raw = is_multisite() ? get_site_option( 'wpvulnerability-themes', '' ) : get_option( 'wpvulnerability-themes', '' );
$theme_data = json_decode( is_string( $raw ) ? $raw : '', true );
return is_array( $theme_data ) ? $theme_data : array();
}
/**
* Update the installed themes cache and remove any old cache data.
*
* @since 2.0.0
*
* @return void
*/
function wpvulnerability_theme_get_vulnerabilities_clean() {
wpvulnerability_clear_cache( 'themes' );
wpvulnerability_theme_get_installed();
}
/**
* Admin Head
* Adds vulnerability information after the theme row and notices on the theme page based on the installed theme cache.
*
* @since 2.0.0
*
* @return void
*/
function wpvulnerability_theme_page() {
// Check if the current page is the themes page.
global $pagenow;
if ( wpvulnerability_analyze_filter( 'themes' ) && 'themes.php' === $pagenow && wpvulnerability_capabilities() ) {
// Get the vulnerabilities for the installed themes.
$themes = wpvulnerability_theme_get_vulnerabilities();
// Loop through the themes and add vulnerability information after the theme row for vulnerable themes.
foreach ( $themes as $theme_file => $theme_data ) {
if ( ! is_array( $theme_data ) ) {
continue;
}
$td_wpv = isset( $theme_data['wpvulnerability'] ) && is_array( $theme_data['wpvulnerability'] ) ? $theme_data['wpvulnerability'] : array();
if ( isset( $td_wpv['vulnerable'] ) && 1 === ( is_scalar( $td_wpv['vulnerable'] ) ? (int) $td_wpv['vulnerable'] : 0 ) ) {
add_action( 'after_theme_row_' . esc_attr( $theme_file ), 'wpvulnerability_theme_info_after', 10, 2 );
}
}
}
}
// Add notices for vulnerable themes on the theme page.
add_action( 'admin_head', 'wpvulnerability_theme_page' );
/**
* Build the vulnerability HTML block for the single-site theme details modal.
*
* Generates a standalone table (no wrapping ) with one row per vulnerability,
* using the same column layout and badges as the theme-row renderer.
*
* @since 5.0.0
*
* @param array $vulnerabilities Array of vulnerability objects from the cache.
* @return string HTML string ready to be injected into the modal, or empty string if none.
*/
function wpvulnerability_theme_modal_html( $vulnerabilities ) {
if ( empty( $vulnerabilities ) ) {
return '';
}
$icon = '
';
$html = '' . $icon . ' ' . esc_html__( 'This theme has known vulnerabilities that may be affecting your installed version.', 'wpvulnerability' ) . '
';
$html .= '';
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 );
$show_active = $kev || 'active' === $exploitation;
$show_poc = 'poc' === $exploitation;
$show_auto = 'yes' === $automatable;
$html .= '';
$html .= '| ';
$html .= '' !== $version_display
? '' . $version_display . ''
: '—';
$html .= ' | ';
$html .= '';
if ( $show_active || $show_poc || $show_auto || '' !== $score_badge ) {
$html .= ' ';
if ( $show_active ) {
$html .= '⚠ ' . esc_html__( 'Actively exploited', 'wpvulnerability' );
if ( $kev && null !== $kev_date ) {
$html .= ' · ' . esc_html( $kev_date );
}
$html .= '';
}
if ( $show_poc ) {
$html .= '⚡ ' . esc_html__( 'Public exploit', 'wpvulnerability' ) . '';
}
if ( $show_auto ) {
$html .= '⚙ ' . esc_html__( 'Automatable', 'wpvulnerability' ) . '';
}
if ( '' !== $score_badge ) {
$html .= $score_badge;
}
$html .= ' ';
}
if ( null !== $description ) {
$html .= '' . esc_html( $description ) . ' ';
}
if ( $vuln_closed || $vuln_unfixed ) {
$html .= '';
if ( $vuln_closed ) {
$html .= ' ' . esc_html__( 'This theme is closed. Please replace it with another.', 'wpvulnerability' ) . ' ';
}
if ( $vuln_unfixed ) {
$html .= ' ' . esc_html__( 'This vulnerability appears to be unpatched. Stay tuned for upcoming theme updates.', 'wpvulnerability' ) . ' ';
}
$html .= ' ';
}
if ( ! empty( $what ) ) {
$html .= '' . implode( '', $what ) . ' ';
}
if ( '' !== $source_pills ) {
$html .= '' . esc_html__( 'References:', 'wpvulnerability' ) . '';
$html .= $source_pills;
$html .= ' ';
}
$html .= ' | ';
$html .= '
';
}
$html .= '
';
return $html;
}
/**
* Inject pre-rendered vulnerability HTML into the theme data passed to the JS modal.
*
* Hooks into wp_prepare_themes_for_js to add a wpvulnerability_html key for each
* vulnerable theme. The JS template patch in wpvulnerability_theme_modal_template_patch()
* then surfaces this data inside the modal.
*
* @since 5.0.0
*
* @param array $prepared_themes Themes data prepared for JS.
* @return array Modified themes data.
*/
function wpvulnerability_filter_prepare_themes_for_js( $prepared_themes ) {
if ( ! wpvulnerability_analyze_filter( 'themes' ) || ! wpvulnerability_capabilities() ) {
return $prepared_themes;
}
$theme_vulns = wpvulnerability_theme_get_vulnerabilities();
foreach ( $prepared_themes as $slug => $theme ) {
if ( ! isset( $theme_vulns[ $slug ] ) || ! is_array( $theme_vulns[ $slug ] ) ) {
continue;
}
$entry = $theme_vulns[ $slug ];
$td_wpv = isset( $entry['wpvulnerability'] ) && is_array( $entry['wpvulnerability'] ) ? $entry['wpvulnerability'] : array();
if ( ! isset( $td_wpv['vulnerable'] ) || 1 !== ( is_scalar( $td_wpv['vulnerable'] ) ? (int) $td_wpv['vulnerable'] : 0 ) ) {
continue;
}
$vulnerabilities = isset( $td_wpv['vulnerabilities'] ) && is_array( $td_wpv['vulnerabilities'] ) ? $td_wpv['vulnerabilities'] : array();
if ( empty( $vulnerabilities ) ) {
continue;
}
if ( is_array( $prepared_themes[ $slug ] ) ) {
$prepared_themes[ $slug ]['wpvulnerability_html'] = wpvulnerability_theme_modal_html( $vulnerabilities );
}
}
return $prepared_themes;
}
add_filter( 'wp_prepare_themes_for_js', 'wpvulnerability_filter_prepare_themes_for_js' );
/**
* Patch the #tmpl-theme-single Underscore template to display vulnerability data.
*
* Injects a conditional block after the tags section so that when a theme's JS data
* contains wpvulnerability_html, that HTML is rendered inside the details modal.
* Only runs on the single-site themes.php page.
*
* @since 5.0.0
*
* @return void
*/
function wpvulnerability_theme_modal_template_patch() {
global $pagenow;
if ( 'themes.php' !== $pagenow || is_multisite() || ! wpvulnerability_capabilities() || ! wpvulnerability_analyze_filter( 'themes' ) ) {
return;
}
?>
items as $theme_file => $theme_data ) {
$tf_entry = isset( $theme_vulnerabilities[ $theme_file ] ) && is_array( $theme_vulnerabilities[ $theme_file ] ) ? $theme_vulnerabilities[ $theme_file ] : array();
$tf_wpv = isset( $tf_entry['wpvulnerability'] ) && is_array( $tf_entry['wpvulnerability'] ) ? $tf_entry['wpvulnerability'] : array();
if ( empty( $tf_wpv['vulnerable'] ) || 0 === ( is_scalar( $tf_wpv['vulnerable'] ) ? (int) $tf_wpv['vulnerable'] : 0 ) ) {
unset( $wp_list_table->items[ $theme_file ] );
}
}
}
}
/**
* Initializes the vulnerability filtering for the themes list in the network admin area of a multisite installation.
*
* This function checks if the current environment is a multisite network and whether the user is in the network
* admin area. If both conditions are met, it hooks into the 'admin_head-themes.php' action to apply a filter that
* shows only vulnerable themes in the themes list.
*
* @since 3.3.5
*
* @return void
*/
function wpvulnerability_themes_filter_init() {
if ( is_multisite() && is_network_admin() ) {
add_action( 'admin_head-themes.php', 'wpvulnerability_themes_filter' );
}
}
add_action( 'network_admin_menu', 'wpvulnerability_themes_filter_init' );
/**
* Adds a "Vulnerable" tab to the WordPress themes page that displays the count of vulnerable themes.
*
* This function checks the cache for the number of vulnerable themes and adds a new tab to the themes
* management page in the WordPress admin area. The tab displays the count of vulnerable themes and highlights it
* if it is currently active. The tab is added only in the network admin area of a multisite installation.
*
* @since 3.3.5
*
* @param array $views An array of existing theme views (tabs) in the WordPress admin themes page.
*
* @return array The modified array of views including the "Vulnerable" tab.
*/
function wpvulnerability_themes_view( $views ) {
if ( ! wpvulnerability_analyze_filter( 'themes' ) ) {
return $views;
}
$raw_count = is_multisite() ? get_site_option( 'wpvulnerability-themes-vulnerable', '0' ) : get_option( 'wpvulnerability-themes-vulnerable', '0' );
$decoded_count = json_decode( is_string( $raw_count ) ? $raw_count : '0', true );
$wpvulnerability_themes_total = ( is_scalar( $decoded_count ) ? (int) $decoded_count : 0 );
if ( is_multisite() && is_network_admin() ) {
$url = network_admin_url( 'themes.php?theme_status=vulnerable' );
// Add nonce for CSRF protection.
$url = esc_url( wp_nonce_url( $url, 'wpvulnerability_filter_themes', 'wpv_nonce' ) );
$views['vulnerable'] = sprintf(
'%s',
$url,
( isset( $_GET['theme_status'] ) && 'vulnerable' === $_GET['theme_status'] ? ' class="current"' : '' ), // phpcs:ignore
// translators: the number of vulnerabilities.
sprintf( __( 'Vulnerabilities (%d)', 'wpvulnerability' ), $wpvulnerability_themes_total )
);
}
return $views;
}
/**
* Adds a custom filter to the themes page in the WordPress admin to display a tab for vulnerable themes.
*
* This function hooks into the 'views_themes-network' filter to add a custom tab or view for displaying vulnerable themes
* on the themes management page in the WordPress network admin area. The tab is added only in a multisite setup
* and specifically in the network admin context.
*
* @since 3.3.5
*
* @return void
*/
function wpvulnerability_themes_add_tab() {
if ( is_multisite() && is_network_admin() ) {
add_filter( 'views_themes-network', 'wpvulnerability_themes_view' );
}
}
add_action( 'admin_head', 'wpvulnerability_themes_add_tab' );