> $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 .= ''; // 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 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 .= ''; 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 .= ''; $html .= ''; } $html .= '
'; $html .= '' !== $version_display ? '' . $version_display . '' : '—'; $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 .= '
'; 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' ) : '0'; $wpvulnerability_themes_total = ( is_scalar( json_decode( is_string( $raw_count ) ? $raw_count : '0', true ) ) ? (int) json_decode( is_string( $raw_count ) ? $raw_count : '0', true ) : 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' );