settings = new OCVMSettings(); $this->noticeHTML = $this->settings->templateDir . '/partials/notice.html'; } /** * Get FQN of plugin by slug * @param $slug string Theme's stylesheet or Plugin's PHP dir name * @return string Name of the plugin/theme */ public function get_name_for_slug( $slug, $type ): string { // If core, return empty string if ( 'wp' === $type ) { return ''; } if ( 'themes' === $type ) { // theme headers $themeData = wp_get_theme( $slug ); return $themeData->get( 'Name' ); } require_once ABSPATH . 'wp-admin/includes/plugin.php'; $plugin_files = get_plugins( '/' . $slug ); if ( ! $plugin_files ) { return ''; } $plugin_files = array_keys( $plugin_files ); $plugin_dir = $slug . '/' . reset( $plugin_files ); $pluginData = get_plugin_data( trailingslashit( WP_PLUGIN_DIR ) . $plugin_dir ); return $pluginData['Name']; } /** * Check is vuls is single or multiple * @return bool */ public function isSingleVul(): bool { $groupedByType = []; // Group by type and ensure unique slugs foreach ($this->highCriticalNotices as $item) { $groupedByType[$item['type']] = $groupedByType[$item['type']] ?? []; $groupedByType[$item['type']][] = $item['slug']; } // Calculate total count of unique slugs across all types $totalCount = 0; foreach (['plugins', 'themes', 'wp'] as $type) { if (isset($groupedByType[$type])) { $totalCount += count(array_unique($groupedByType[$type])); } } //if total count is greater than 1, then its multiple vuls notices return ($totalCount === 1); } /** * Validate before notice show * @return bool */ public function isHighCriticalNotificationsExist(): bool { $filteredData = array_values(array_filter($this->notices, function ($item) { return $item['notice_tag'] !== 'Low' && $item['notice_tag'] !== 'Medium'; })); $this->highCriticalNotices = $filteredData; return count($this->highCriticalNotices); } /** * Get only high notification * @return array */ public function getAjaxHighNotifications(): bool { // Mandatory to get the notices, need to call during ajax call, $this->prepareNotifications(); return $this->isHighCriticalNotificationsExist(); } /** * Helper function to prepare and return the warning content for multiple vuls * @param $severity * @param $templateFile * @param $warning_img_url * @param $multipleVuls * @param $notice_cta * @param $notice_close_img * @return string */ public function prepareNotice($severity, $templateFile, $warning_img_url, $multipleVuls, $notice_cta, $notice_close_img = ''): string { $warning_content = strtr($multipleVuls, array( '{{tagName}}' => $severity, )); $placeholders = array( '{{warning_img_url}}' => $warning_img_url, '{{warning_content}}' => __($warning_content, 'onecom-wp'), '{{notice_cta}}' => $notice_cta, '{{button_text}}' => __('See details', 'onecom-wp'), ); if (!empty($notice_close_img)) { $placeholders['{{notice_close_img}}'] = $notice_close_img; } return strtr( file_get_contents($templateFile, true), $placeholders ); } /** * Get prev and existing high notification diff * @param $prevHighCriticalNotices * @return array */ public function isNewHighVMExist($prevHighCriticalNotices): array { //if empty simply return if(empty($prevHighCriticalNotices)) { $prevHighCriticalNotices = []; } //get all new ids for high and critical $new_vm_ids = array_merge(...array_column($this->highCriticalNotices, 'high_vuls_ids')); return array_diff($new_vm_ids, $prevHighCriticalNotices); } /** * Show only critical and high vuls notification * @return string */ public function criticalAndHighNoticeShow(): string { // First check is notification eligible to show or not // Need to show when the high and critical vuls exist if(!$this->isHighCriticalNotificationsExist()){ return ''; } //Note:Below we have only high and critical vuls list, no need to check for low and medium $singleVul = '{{tagName}} vulnerability risk, caused by the {{vul_type}}: %s.'; $coreVul = '{{tagName}} vulnerability risk, caused by %s.'; $multipleVuls = '{{tagName}} vulnerability risk on your site, caused by several themes, plugins or core files.'; //notification template path $highNotice = $this->settings->templateDir . '/partials/notice-high-vul.html'; $criticalNotice = $this->settings->templateDir . '/partials/notice-critical-vul.html'; $warning_img_url = OCVM_PLUGIN_DIR_URL. "/assets/images/warning.svg"; $notice_close_img = OCVM_PLUGIN_DIR_URL. "/assets/images/close.svg"; $notice_cta = is_multisite() ? get_admin_url( 'admin.php?page=onecom-wp-health-monitor#vm-page' ) : admin_url( 'admin.php?page=onecom-wp-health-monitor#vm-page' ); $isSingleVul = $this->isSingleVul(); //get prev high $prev_high_notices = json_decode(get_user_meta(get_current_user_id(),'vm_prev_high_notices', true), true); $newVM = $this->isNewHighVMExist($prev_high_notices); $notifications = ''; //Show single notification if($isSingleVul){ $notice = $this->highCriticalNotices[0]; //skip for similar high notices if($notice['notice_tag'] === 'High' && count($newVM) < 1){ return ''; } $noticeTemplate = ($notice['notice_tag'] === 'High') ? $highNotice : $criticalNotice; //message prepare $replacements = [ '{{tagName}}' => $notice['notice_tag'], '{{vul_type}}' => rtrim($notice['type'], "s") ]; $warning_content = ($notice['type'] === 'wp') ? strtr($coreVul, $replacements) : strtr($singleVul, $replacements); $translation = __($warning_content, 'onecom-wp'); $notice_name = $this->get_name_for_slug($notice['slug'], $notice['type']); //decide notice $notice_translation = ($notice['type'] === 'wp') ? str_replace('%s', 'WordPress Core', $translation) : str_replace('%s', $notice_name, $translation); // Replace template variables with dynamic text/translation return strtr( file_get_contents($noticeTemplate, true), array( '{{warning_img_url}}' => $warning_img_url, '{{warning_content}}' => __($notice_translation, 'onecom-wp'), '{{notice_cta}}' => $notice_cta, '{{notice_close_img}}' => $notice_close_img, '{{button_text}}' => __('See details', 'onecom-wp'), ) ); } //Show multiple vuls notification $groupVulSev = []; foreach ( $this->highCriticalNotices as $notice ) { $groupVulSev[] = $notice['notice_tag']; } //if critical vuls exits then show critical if (in_array('Critical', $groupVulSev)) { return $this->prepareNotice('Critical', $criticalNotice, $warning_img_url, $multipleVuls, $notice_cta); } //if high vuls exist then show high, // high and critical for multiple vuls will not show at same time if (in_array('High', $groupVulSev)) { //return if no new high vm found if(count($newVM) < 1){ return ''; } return $this->prepareNotice('High', $highNotice, $warning_img_url, $multipleVuls, $notice_cta, $notice_close_img); } return $notifications; } /** * Prepare notifications * @param int $expanded * @return string notice html */ public function notificationHTML( $expanded = 0 ) { $allPageClass = ( 0 === $expanded ) ? 'allpageNotification' : ''; $alinkOpen = ''; $notifications = '
'; return $notifications; } /** * Returns vulnerability severity level: critical, high, medium or low */ public function get_vul_severity( $cvss ): array { $vul_severity = array(); if ( $cvss < 4 ) { $vul_severity['label'] = 'Low'; $vul_severity['class'] = 'vm_severity_low'; } elseif ( $cvss < 7 ) { $vul_severity['label'] = 'Medium'; $vul_severity['class'] = 'vm_severity_medium'; } elseif ( $cvss < 9 ) { $vul_severity['label'] = 'High'; $vul_severity['class'] = 'vm_severity_high'; } else { $vul_severity['label'] = 'Critical'; $vul_severity['class'] = 'vm_severity_critical'; } return $vul_severity; } /** * Determines if a vulnerability is exploitable or not. * * @param bool $exploited Whether the vulnerability is exploited or not. * * @return array An associative array containing information about the vulnerability's exploitability: * - 'label' (string) The label indicating exploitability ('Exploitable' or empty string). * - 'class' (string) The CSS class to apply ('oc_vm_exploitable' or 'oc_vm_none'). * - 'tooltip' (string) The tooltip message describing the exploitability status. */ public function vul_is_exploitable( $exploited ): array { $exploitable = array(); if ( $exploited ) { $exploitable['label'] = 'Exploitable'; $exploitable['class'] = 'oc_vm_exploitable'; $exploitable['tooltip'] = __( 'Vulnerability is known to be exploited', 'onecom-wp' ); } else { $exploitable['label'] = ''; $exploitable['class'] = 'oc_vm_none'; $exploitable['tooltip'] = ''; } return $exploitable; } /** * Check if a notice is dismissed */ public function isDismissed( $slug ): int { return get_site_transient( sprintf( self::dismissFlag, $slug, get_current_user_id() ) ); } /** * Is item still active */ public function isActive( $item, $vuls ): bool { //seat belt if ( empty( $item ) ) { return false; } if ( 'plugins' === $item['type'] ) { // exit if the plugin is not installed if ( ! $this->isInstalled( $item ) ) { return false; } if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } // get details of installed plugin by its slug $plugin_files = get_plugins( '/' . $item['slug'] ); // get plugin name (array) $single_plugin = array_keys( $plugin_files ); $plugin_path = $item['slug'] . '/' . reset( $single_plugin ); // exit if the plugin is not active if ( false === is_plugin_active( $plugin_path ) ) { return false; } // check if plugin's current active version matches the vulnerable version if ( ! empty( $vuls ) && ! empty( $vuls[ $item['type'] ][ $item['slug'] ] ) ) { // get plugin version of first plugin by its name $plugin_installed_ver = $plugin_files[ reset( $single_plugin ) ]['Version']; $plugin_vulnerable_ver = $vuls[ $item['type'] ][ $item['slug'] ]['installed_version']; if ( version_compare( $plugin_installed_ver, $plugin_vulnerable_ver ) > 0 ) { return false; } } return true; } elseif ( 'themes' === $item['type'] ) { // exit if the theme is not installed if ( ! $this->isInstalled( $item ) ) { return false; } return $item['slug'] === get_option( 'template' ); } elseif ( 'wp' === $item['type'] ) { // exit if core is already on fixed or higher version if ( version_compare( $item['installed_version'], $item['fixed_in'] ) >= 0 ) { return false; } return true; } else { // type == wp return true; } } /** * WPIN-3470: Is item (theme/plugin) installed */ public function isInstalled( $item ): bool { if ( 'plugins' === $item['type'] ) { if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } // Get all installed plugins $plugins = get_plugins(); // Check if the plugin is installed $plugin_installed = false; foreach ( $plugins as $plugin_file => $plugin_data ) { $folder_name = dirname( $plugin_file ); if ( $folder_name === $item['slug'] ) { $plugin_installed = true; break; } } // Return false if plugin is not installed if ( ! $plugin_installed ) { return false; } } elseif ( 'themes' === $item['type'] ) { // Return false if the theme is not installed $installed_themes = wp_get_themes(); if ( ! isset( $installed_themes[ $item['slug'] ] ) ) { return false; } } // Default true (assuming plugin or theme is installed) return true; } /** * Get id of high vulnerability * @param array $vulnerabilities * @return array */ public function getHighVulnerabilityIds(array $vulnerabilities): array { $filtered = array_filter($vulnerabilities, function ($item) { return isset($item['cvss_score']) && $item['cvss_score'] > 7 && $item['cvss_score'] < 9; }); return array_column($filtered, 'id'); } /** * Array iterator * @param $items array * @return void */ public function arrayIterate( $items, $type = '', $vuls = array() ): void { if ( ! empty( $items ) ) { foreach ( $items as $slug ) { // Get highest cvss_score for all vulnerabilities of the item, if not found or non-numeric, set default 10 if ( ! empty( array_column( $vuls[ $type ][ $slug ]['vulnerabilities'], 'cvss_score' ) ) ) { $cvss_score = max( array_column( $vuls[ $type ][ $slug ]['vulnerabilities'], 'cvss_score' ) ); } else { $cvss_score = 10; } //filter high vulnerability ids only $highVuls_ids = $this->getHighVulnerabilityIds($vuls[$type][$slug]['vulnerabilities']); $is_exploited = false; if ( $this->settings->isPremium() && isset( $vuls[ $type ][ $slug ]['vulnerabilities'] ) && is_array( $vuls[ $type ][ $slug ]['vulnerabilities'] ) ) { foreach ( $vuls[ $type ][ $slug ]['vulnerabilities'] as $vuln ) { if ( isset( $vuln['is_exploited'] ) && $vuln['is_exploited'] == 1 ) { $is_exploited = true; break; // Exit the loop as soon as an exploited vulnerability is found } } } $noticeScore = is_numeric( $cvss_score ) ? $cvss_score : 10; $vul_severity = $this->get_vul_severity($noticeScore); $this->notices[] = array( 'slug' => $slug, 'dismissed' => (int) $this->isDismissed( $slug ), 'type' => $type, 'fixed_in' => isset( $vuls[ $type ][ $slug ]['fixed_in'] ) ? $vuls[ $type ][ $slug ]['fixed_in'] : '', 'installed_version' => isset( $vuls[ $type ][ $slug ]['installed_version'] ) ? $vuls[ $type ][ $slug ]['installed_version'] : '', 'cvss_score' => $noticeScore, 'is_exploited' => $is_exploited, 'notice_tag' => $vul_severity['label'], 'high_vuls_ids' => $highVuls_ids ); } } } /** * Prepare notifications to be displayed * @return void */ public function prepareNotifications( $show_dismissed = 0 ): void { $settings = $this->settings->get(); $vuls = empty( $settings['vulnerabilities'] ) ? array() : $settings['vulnerabilities']; // in order to follow a default structure $vuls = array_merge( array( 'themes' => array(), 'plugins' => array(), 'wp' => array(), ), $vuls ); // collect wp vulnerabilities if ( ! empty( $vuls['wp']['vulnerabilities'] ) ) { global $wp_version; // Get highest cvss_score for all vulnerabilities of the item, if not found, set default 10 if ( ! empty( array_column( $vuls['wp']['vulnerabilities'], 'cvss_score' ) ) ) { $cvss_score = max( array_column( $vuls['wp']['vulnerabilities'], 'cvss_score' ) ); } else { $cvss_score = 10; } $noticeScore = is_numeric( $cvss_score ) ? $cvss_score : 10; $vul_severity = $this->get_vul_severity($noticeScore); //filter high vulnerability ids only $highVuls_ids = $this->getHighVulnerabilityIds($vuls['wp']['vulnerabilities']); $this->notices[] = array( 'slug' => 'wp', 'dismissed' => $this->isDismissed( 'wp' ), 'type' => 'wp', 'fixed_in' => max( array_column( $vuls['wp']['vulnerabilities'], 'fixed_in' ) ), 'installed_version' => $wp_version, 'cvss_score' => $noticeScore, 'notice_tag' => $vul_severity['label'], 'high_vuls_ids' => $highVuls_ids ); } // collect plugins vulnerabilities if ( ! empty( $vuls['plugins'] ) ) { $this->arrayIterate( array_keys( (array) $vuls['plugins'] ), 'plugins', $vuls ); } // collect themes vulnerabilities if ( ! empty( $vuls['themes'] ) ) { $this->arrayIterate( array_keys( (array) $vuls['themes'] ), 'themes', $vuls ); } // filter out the dismissed items if ( ! $show_dismissed ) { $this->notices = array_filter( $this->notices, function ( $v ) { return ( 0 === $v['dismissed'] ); }, ARRAY_FILTER_USE_BOTH ); } // filter out inactive items $this->notices = array_filter( $this->notices, function ( $v ) use ( $vuls ) { return $this->isActive( $v, $vuls ); } ); } /** * Show admin notice * @return void */ public function showNotifications(): void { $user = get_user_by( 'id', get_current_user_id() ); if ( ! $user->has_cap( 'update_core' ) || ! $user->has_cap( 'update_themes' ) || ! $user->has_cap( 'update_plugins' ) ) { return; } // check screen $screen = get_current_screen(); if ( $screen->id === 'one-com_page_onecom-wp-health-monitor' ) { return; } // get notices to display $this->prepareNotifications(); // exit if no notice found if ( empty( $this->notices ) ) { return; } // render notices on top of all pages //echo $this->notificationHTML(); //old notices show echo '
' . $this->criticalAndHighNoticeShow() . '
'; } /** * Dismiss notifications */ public function dismissNotifications() { if ( isset( $_POST['action'] ) && $_POST['action'] === 'ocvm_dismissNotification' ) { $response = array(); //set WP transient as per if ( isset( $_POST['dismisstype'] ) ) { $dismisstype = $_POST['dismisstype']; set_site_transient( $dismisstype, 1, 0 ); $response['dismissSetFor'] = $_POST['dismisstype']; $response['success'] = true; } else { $response['dismissSetFor'] = ''; $response['success'] = false; } wp_send_json( $response ); } } /** * Dismiss High notifications */ public function dismissHighNotifications() { if ( isset( $_POST['action'] ) && $_POST['action'] === 'ocvm_dismissNotificationHigh' ) { $response = array(); if ( isset( $_POST['dismisstype'] ) ) { //get only high and critical risk notices $isHighCriticalNotifications = $this->getAjaxHighNotifications(); if($isHighCriticalNotifications){ $highVulsIds = array_merge(...array_column($this->highCriticalNotices, 'high_vuls_ids')); update_user_meta(get_current_user_id(),'vm_prev_high_notices', json_encode($highVulsIds)); } $response['dismissSetFor'] = $_POST['dismisstype']; $response['success'] = true; } else { $response['dismissSetFor'] = ''; $response['success'] = false; } wp_send_json( $response ); } } }