'', 'shield' => '', 'eye' => '', 'x' => '', 'clock' => '', 'code' => '', 'layout' => '', 'bell' => '', 'user' => '', 'rss' => '', 'lock' => '', 'tool' => '', 'search' => '', 'img' => '', 'chart' => '', 'check' => '', 'alert' => '', 'blog' => '', 'business' => '', 'cart' => '', 'agency' => '', ); /** * Hook suffixes of the plugin's admin screens, as returned by add_menu_page(). * * @var string[] */ private $page_hooks = array(); /** * Register admin hooks. */ public function __construct() { add_action( 'admin_menu', array( $this, 'add_menu' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) ); add_action( 'wp_ajax_ovkit_save_settings', array( $this, 'ajax_save' ) ); add_action( 'wp_ajax_ovkit_scan', array( $this, 'ajax_scan' ) ); add_action( 'wp_ajax_ovkit_apply_safe', array( $this, 'ajax_apply_safe' ) ); add_action( 'wp_ajax_ovkit_rollback', array( $this, 'ajax_rollback' ) ); add_action( 'wp_ajax_ovkit_site_scan', array( $this, 'ajax_site_scan' ) ); add_action( 'wp_ajax_ovkit_health_check', array( $this, 'ajax_health_check' ) ); add_action( 'wp_ajax_ovkit_apply_preset', array( $this, 'ajax_apply_preset' ) ); add_action( 'wp_ajax_ovkit_export_settings', array( $this, 'ajax_export_settings' ) ); add_action( 'wp_ajax_ovkit_import_settings', array( $this, 'ajax_import_settings' ) ); add_action( 'wp_ajax_ovkit_make_login_bypass', array( $this, 'ajax_make_login_bypass' ) ); add_action( 'admin_bar_menu', array( $this, 'admin_bar_indicator' ), 100 ); add_filter( 'admin_body_class', array( $this, 'body_class' ) ); } /** * Mark the toolkit's own screens so their background can match the panel skin. * * @param string $classes Space-separated body classes. * @return string */ public function body_class( $classes ) { $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( $screen && false !== strpos( (string) $screen->id, 'ovkit' ) ) { $classes .= ' ovkit-screen'; } return $classes; } // Menu registration. /** * Add menu. */ public function add_menu(): void { $icon_url = 'data:image/svg+xml;base64,' . base64_encode( $this->logo_svg( 'black' ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Encodes the static menu icon SVG as a data URI, as add_menu_page() expects. $title = __( 'Ovanap Admin Toolkit', 'ovanap-admin-toolkit' ); $pages = array( array( 'ovkit', __( 'Dashboard', 'ovanap-admin-toolkit' ), 'render_dashboard' ), array( 'ovkit-features', __( 'Features', 'ovanap-admin-toolkit' ), 'render_features' ), array( 'ovkit-tracking', __( 'Tracking', 'ovanap-admin-toolkit' ), 'render_tracking' ), array( 'ovkit-scan', __( 'Site Scan', 'ovanap-admin-toolkit' ), 'render_scan' ), array( 'ovkit-log', __( 'Activity Log', 'ovanap-admin-toolkit' ), 'render_log' ), ); $this->page_hooks[] = add_menu_page( $title, __( 'Admin Toolkit', 'ovanap-admin-toolkit' ), 'manage_options', 'ovkit', array( $this, 'render_dashboard' ), $icon_url, 80 ); foreach ( $pages as $page ) { $this->page_hooks[] = add_submenu_page( 'ovkit', $title . ' - ' . $page[1], $page[1], 'manage_options', $page[0], array( $this, $page[2] ) ); } } /** * The OvKit mark (an O and a V with the live dot), as SVG markup. * * @param string $color Mark colour. * @param string $dot Colour of the dot inside the O; defaults to the mark colour. * @return string */ private function logo_svg( string $color, string $dot = '' ): string { // The OvKit mark from ovkit.com: an O (ring) and a V, with the live dot inside the O. $dot = '' !== $dot ? $dot : $color; return '' . '' . '' . ''; } // Asset enqueue. /** * Enqueue admin assets on the plugin's own screens, plus the admin bar badge style everywhere in admin. * * @param string $hook Current admin page hook suffix. */ public function enqueue_assets( string $hook ): void { if ( is_admin_bar_showing() ) { wp_add_inline_style( 'admin-bar', '#wpadminbar .ovkit-ab-label{display:inline-flex;align-items:center;gap:4px}#wpadminbar .ovkit-ab-count{background:#ff5f0f;color:#16181b;font-size:10px;width:20px;height:20px;display:inline-flex;align-items:center;justify-content:center;border-radius:50%;font-weight:600;line-height:1}' ); } if ( ! in_array( $hook, $this->page_hooks, true ) ) { return; } wp_enqueue_media(); wp_enqueue_style( 'ovkit-admin', OVKIT_URL . 'admin/css/toolkit-admin.css', array(), OVKIT_VERSION ); wp_enqueue_script( 'ovkit-admin', OVKIT_URL . 'admin/js/toolkit-admin.js', array(), OVKIT_VERSION, true ); wp_localize_script( 'ovkit-admin', 'ovkitAdmin', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), // Separate nonces by blast radius: settings-save actions, backup I/O, // read-only diagnostics, and the login-bypass issuer each get their // own secret so a leaked nonce from one surface cannot be replayed // against another. 'nonce' => wp_create_nonce( 'ovkit_toolkit_save' ), 'backupNonce' => wp_create_nonce( 'ovkit_backup' ), 'diagNonce' => wp_create_nonce( 'ovkit_diag' ), 'bypassNonce' => wp_create_nonce( 'ovkit_login_bypass' ), 'featuresUrl' => admin_url( 'admin.php?page=ovkit-features' ), 'i18n' => array( 'saving' => __( 'Saving...', 'ovanap-admin-toolkit' ), 'saved' => __( 'Saved! Reloading...', 'ovanap-admin-toolkit' ), 'savedPurged' => __( 'Saved. Page cache cleared:', 'ovanap-admin-toolkit' ), 'errorRetry' => __( 'Error - retry', 'ovanap-admin-toolkit' ), 'applySafeBtn' => __( 'Apply safe defaults', 'ovanap-admin-toolkit' ), 'applying' => __( 'Applying...', 'ovanap-admin-toolkit' ), 'applied' => __( 'Applied! Reloading...', 'ovanap-admin-toolkit' ), /* translators: 1: beginner count, 2: standard count, 3: advanced count */ 'appliedDetail' => __( 'Enabled %1$d beginner + %2$d standard features. %3$d advanced features available for manual review.', 'ovanap-admin-toolkit' ), 'errorPrefix' => __( 'Error: ', 'ovanap-admin-toolkit' ), 'confirmSafe' => __( 'Enable all Safe + Recommended features that were detected? You can rollback anytime.', 'ovanap-admin-toolkit' ), 'confirmRollback' => __( 'Restore settings from this point? This will be logged as a new change and can be undone.', 'ovanap-admin-toolkit' ), 'runHealth' => __( 'Run health check', 'ovanap-admin-toolkit' ), 'runningTests' => __( 'Running tests…', 'ovanap-admin-toolkit' ), 'running' => __( 'Running…', 'ovanap-admin-toolkit' ), 'scanning' => __( 'Scanning environment...', 'ovanap-admin-toolkit' ), 'scanDone' => __( 'Done — reloading...', 'ovanap-admin-toolkit' ), 'scanFailed' => __( 'Scan failed — retry', 'ovanap-admin-toolkit' ), 'refreshScan' => __( 'Refresh scan', 'ovanap-admin-toolkit' ), ), ) ); } // AJAX handlers. /** * Ajax save. */ public function ajax_save(): void { check_ajax_referer( 'ovkit_toolkit_save', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( 'Forbidden' ); } try { $features = OVKIT_Feature_Registry::instance()->get_all(); $old = get_option( OVKIT_OPTION, array() ); $settings = ovkit_read_posted_settings( $features, 'ovkit_toolkit_save' ); $purged = ovkit_commit_settings( $old, $settings ); // Count active. $on = 0; foreach ( $features as $key => $f ) { if ( ! empty( $settings[ $key ] ) ) { ++$on; } } wp_send_json_success( array( 'active' => $on, 'total' => count( $features ), 'purged' => $purged, ) ); } catch ( \Throwable $e ) { wp_send_json_error( 'Save error: ' . $e->getMessage() ); } } /** * Ajax scan. */ public function ajax_scan(): void { check_ajax_referer( 'ovkit_diag', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( 'Forbidden' ); } try { $results = OVKIT_Scanner::instance()->rescan(); $summary = OVKIT_Scanner::instance()->get_summary(); wp_send_json_success( array( 'results' => $results, 'summary' => $summary, ) ); } catch ( \Throwable $e ) { wp_send_json_error( 'Scan error: ' . $e->getMessage() ); } } /** * Ajax apply safe. */ public function ajax_apply_safe(): void { check_ajax_referer( 'ovkit_toolkit_save', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( 'Forbidden' ); } try { $features = OVKIT_Feature_Registry::instance()->get_all(); $scan = OVKIT_Scanner::instance()->get_cached(); $old = get_option( OVKIT_OPTION, array() ); $settings = $old; $enabled_beginner = 0; $enabled_standard = 0; $skipped_advanced = 0; foreach ( $features as $key => $f ) { $risk = $f['risk'] ?? 'safe'; $level = $f['level'] ?? 'standard'; $det = $scan[ $key ] ?? array(); if ( 'advanced' === $level ) { ++$skipped_advanced; continue; // never auto-enable advanced features. } if ( ( 'safe' === $risk || 'recommended' === $risk ) && ! empty( $det['safe'] ) && ! empty( $det['detected'] ) ) { $settings[ $key ] = 1; if ( 'beginner' === $level ) { ++$enabled_beginner; } else { ++$enabled_standard; } } } ovkit_commit_settings( $old, $settings ); wp_send_json_success( array( 'applied' => true, 'enabled_beginner' => $enabled_beginner, 'enabled_standard' => $enabled_standard, 'skipped_advanced' => $skipped_advanced, ) ); } catch ( \Throwable $e ) { wp_send_json_error( 'Apply error: ' . $e->getMessage() ); } } /** * Ajax rollback. */ public function ajax_rollback(): void { check_ajax_referer( 'ovkit_toolkit_save', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( 'Forbidden' ); } if ( ! isset( $_POST['index'] ) ) { wp_send_json_error( 'Missing index' ); } $index = absint( wp_unslash( $_POST['index'] ) ); $ok = OVKIT_Activity_Log::rollback( $index ); wp_send_json_success( array( 'rolled_back' => $ok ) ); } /** * Ajax export settings. */ public function ajax_export_settings(): void { check_ajax_referer( 'ovkit_backup', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( 'Forbidden' ); } $settings = get_option( OVKIT_OPTION, array() ); $export = array( 'plugin' => 'ovkit', 'version' => OVKIT_VERSION, 'exported' => gmdate( 'Y-m-d H:i:s' ), 'site' => home_url(), 'settings' => $settings, ); wp_send_json_success( $export ); } /** * Ajax import settings. */ public function ajax_import_settings(): void { check_ajax_referer( 'ovkit_backup', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( 'Forbidden' ); } // The browser parses the uploaded JSON file and posts it as a normal // settings[key] form, so an import goes through exactly the same // registry allowlist and per-type sanitizers as a regular save. $features = OVKIT_Feature_Registry::instance()->get_all(); $sensitive = $this->import_sensitive_keys(); $clean = ovkit_read_posted_settings( $features, 'ovkit_backup', $sensitive ); // Preserve current sensitive values: an import must not blank them. $old = get_option( OVKIT_OPTION, array() ); foreach ( array_keys( $sensitive ) as $sk ) { if ( array_key_exists( $sk, $old ) ) { $clean[ $sk ] = $old[ $sk ]; } } ovkit_commit_settings( $old, $clean ); wp_send_json_success( array( 'imported' => count( $clean ) ) ); } /** * Settings keys that are site-local and never accepted from an imported file. * * @return array Map of key => true. */ private function import_sensitive_keys(): array { $keys = array(); return $keys; } /** * Mint a one-off login-URL bypass token. Used when an admin locks * themselves out of the hidden login slug — they open Toolkit in the still- * authenticated dashboard tab, click "Generate emergency bypass link", * and get a URL they can paste into a private window or send to another * admin. We store only the SHA-256 hash so the plaintext token exists * client-side for five minutes and nowhere else. */ public function ajax_make_login_bypass(): void { check_ajax_referer( 'ovkit_login_bypass', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( 'Forbidden' ); } // 32-char alphanumeric token — ~190 bits of entropy, plenty against // brute force given the 5-minute, single-use window. $token = wp_generate_password( 32, false ); $hash = hash( 'sha256', $token ); set_transient( 'ovkit_login_bypass_hash', $hash, 5 * MINUTE_IN_SECONDS ); // Point the bypass link at wp-login.php directly rather than the // hidden slug. When the hide-login callback sees a valid token it // returns early without registering its init hook, so wp-login.php // loads natively for that one request — the escape hatch an admin // who has lost the custom slug actually needs. Using wp_login_url() // would resolve back to the hidden slug (because that feature // filters it) and defeat the bypass. $url = add_query_arg( 'ovkit-bypass', $token, site_url( 'wp-login.php' ) ); if ( function_exists( 'ovkit_log' ) ) { ovkit_log( 'Hide login: bypass token minted (hash stored, plaintext returned once)', null, 'info' ); } wp_send_json_success( array( 'url' => $url, 'expires_in' => 5 * MINUTE_IN_SECONDS, ) ); } /** * Ajax site scan. */ public function ajax_site_scan(): void { check_ajax_referer( 'ovkit_diag', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( 'Forbidden' ); } try { $results = OVKIT_Site_Scan::instance()->run(); $summary = OVKIT_Site_Scan::instance()->get_summary(); wp_send_json_success( array( 'results' => $results, 'summary' => $summary, ) ); } catch ( \Throwable $e ) { wp_send_json_error( 'Site scan error: ' . $e->getMessage() ); } } /** * Ajax health check. */ public function ajax_health_check(): void { check_ajax_referer( 'ovkit_diag', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( 'Forbidden' ); } try { require_once OVKIT_PATH . 'includes/class-ovkit-health-check.php'; $results = ( new OVKIT_Health_Check() )->run(); $pass = count( array_filter( $results, fn( $r ) => 'pass' === $r['status'] ) ); $fail = count( array_filter( $results, fn( $r ) => 'fail' === $r['status'] ) ); $warn = count( array_filter( $results, fn( $r ) => 'warn' === $r['status'] ) ); wp_send_json_success( array( 'results' => $results, 'summary' => array( 'pass' => $pass, 'fail' => $fail, 'warn' => $warn, 'total' => count( $results ), ), ) ); } catch ( \Throwable $e ) { wp_send_json_error( 'Health check error: ' . $e->getMessage() ); } } /** * One-click presets: feature keys switched on for each site type. * * @return array */ private static function presets(): array { return array( 'blog' => array( 'disable_emojis', 'remove_dashicons', 'remove_query_strings', 'remove_dns_prefetch', 'disable_oembed', 'disable_self_pingbacks', 'limit_heartbeat', 'hide_login_errors', 'remove_wp_version', 'block_author_enum', 'disable_xmlrpc', 'disable_file_editor', 'disable_app_passwords', 'remove_rsd_link', 'remove_wlw_manifest', 'remove_shortlink', 'remove_rest_api_link', 'disable_attachment_pages', 'disable_admin_email_check', 'clean_dashboard', 'smart_notices', ), 'business' => array( 'disable_emojis', 'remove_dashicons', 'remove_query_strings', 'remove_dns_prefetch', 'disable_oembed', 'disable_self_pingbacks', 'limit_heartbeat', 'hide_login_errors', 'remove_wp_version', 'block_author_enum', 'disable_xmlrpc', 'disable_file_editor', 'disable_app_passwords', 'block_rest_users', 'remove_rsd_link', 'remove_wlw_manifest', 'remove_shortlink', 'remove_rest_api_link', 'disable_attachment_pages', 'disable_author_pages', 'disable_comments', 'disable_admin_email_check', 'clean_dashboard', 'smart_notices', 'hide_update_nag', ), 'woocommerce' => array( 'disable_emojis', 'remove_dashicons', 'remove_query_strings', 'remove_dns_prefetch', 'disable_oembed', 'limit_heartbeat', 'hide_login_errors', 'remove_wp_version', 'block_author_enum', 'disable_xmlrpc', 'disable_file_editor', 'disable_app_passwords', 'block_rest_users', 'remove_rsd_link', 'remove_wlw_manifest', 'remove_shortlink', 'remove_rest_api_link', 'disable_attachment_pages', 'disable_admin_email_check', 'clean_dashboard', 'smart_notices', 'hide_update_nag', 'tracking_codes', 'woo_conversion_tracking', ), 'agency' => array( 'disable_emojis', 'remove_dashicons', 'remove_query_strings', 'remove_dns_prefetch', 'disable_oembed', 'disable_self_pingbacks', 'limit_heartbeat', 'hide_login_errors', 'remove_wp_version', 'block_author_enum', 'disable_xmlrpc', 'disable_file_editor', 'disable_app_passwords', 'block_rest_users', 'security_headers', 'remove_rsd_link', 'remove_wlw_manifest', 'remove_shortlink', 'remove_rest_api_link', 'disable_attachment_pages', 'disable_author_pages', 'disable_comments', 'disable_rss', 'disable_admin_email_check', 'clean_dashboard', 'smart_notices', 'hide_update_nag', 'custom_admin_footer', 'hide_help_tab', 'hide_screen_options', 'custom_login_logo', ), ); } /** * Ajax apply preset. */ public function ajax_apply_preset(): void { check_ajax_referer( 'ovkit_toolkit_save', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( 'Forbidden' ); } $preset = isset( $_POST['preset'] ) ? sanitize_key( wp_unslash( $_POST['preset'] ) ) : ''; // Presets — which features to enable for each site type. $presets = self::presets(); if ( ! isset( $presets[ $preset ] ) ) { wp_send_json_error( 'Unknown preset' ); } $features = OVKIT_Feature_Registry::instance()->get_all(); $old = get_option( OVKIT_OPTION, array() ); $settings = is_array( $old ) ? $old : array(); // Enable preset features, preserving any already-enabled features and their extras. $enabled = 0; foreach ( $presets[ $preset ] as $key ) { if ( isset( $features[ $key ] ) ) { $settings[ $key ] = 1; ++$enabled; } } ovkit_commit_settings( $old, $settings ); wp_send_json_success( array( 'preset' => $preset, 'enabled' => $enabled, ) ); } // Admin bar. /** * Admin bar indicator. * * @param mixed $bar Bar. */ public function admin_bar_indicator( $bar ): void { $features = OVKIT_Feature_Registry::instance()->get_all(); $on = 0; foreach ( $features as $key => $f ) { if ( ovkit_is_enabled( $key ) ) { ++$on; } } if ( $on > 0 ) { $bar->add_node( array( 'id' => 'ovkit', 'title' => '' . (int) $on . ' ' . esc_html__( 'Toolkit', 'ovanap-admin-toolkit' ) . '', 'href' => admin_url( 'admin.php?page=ovkit' ), ) ); } } // SVG helpers. /** * Svg. * * @param string $name Name. * @return string */ private function svg( string $name ): string { $path = $this->icons[ $name ] ?? ''; return '' . $path . ''; } /** * Svg large. * * @param string $name Name. * @return string */ private function svg_large( string $name ): string { $path = $this->icons[ $name ] ?? ''; return '' . $path . ''; } /** * Human-readable relative time: "5 minutes ago", "2 hours ago", or "Apr 11 at 10:00". * * @param int $timestamp Timestamp. */ private function relative_time( int $timestamp ): string { if ( $timestamp <= 0 ) { return ''; } $diff = time() - $timestamp; if ( $diff < 60 ) { return __( 'Just now', 'ovanap-admin-toolkit' ); } if ( $diff < 3600 ) { $m = intdiv( $diff, 60 ); /* translators: %d: number of minutes */ return sprintf( _n( '%d minute ago', '%d minutes ago', $m, 'ovanap-admin-toolkit' ), $m ); } if ( $diff < 86400 ) { $h = intdiv( $diff, 3600 ); /* translators: %d: number of hours */ return sprintf( _n( '%d hour ago', '%d hours ago', $h, 'ovanap-admin-toolkit' ), $h ); } return date_i18n( 'M j \a\t H:i', $timestamp ); } /** * Risk badge. * * @param string $risk Risk. * @return string */ private function risk_badge( string $risk ): string { $map = array( 'safe' => array( __( 'Safe', 'ovanap-admin-toolkit' ), 'ovkit-risk-safe' ), 'recommended' => array( __( 'Recommended', 'ovanap-admin-toolkit' ), 'ovkit-risk-rec' ), 'advanced' => array( __( 'Advanced', 'ovanap-admin-toolkit' ), 'ovkit-risk-adv' ), 'risky' => array( __( 'Careful', 'ovanap-admin-toolkit' ), 'ovkit-risk-risky' ), ); $r = $map[ $risk ] ?? $map['safe']; return '' . $r[0] . ''; } // Shared data helpers. /** * Svg allowlist. * * @return array */ private function svg_allowlist(): array { return array( 'svg' => array( 'width' => true, 'height' => true, 'viewbox' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, 'xmlns' => true, ), 'path' => array( 'd' => true, 'fill' => true, 'fill-rule' => true, ), 'circle' => array( 'cx' => true, 'cy' => true, 'r' => true, 'fill' => true, ), 'rect' => array( 'x' => true, 'y' => true, 'width' => true, 'height' => true, 'rx' => true, ), 'line' => array( 'x1' => true, 'y1' => true, 'x2' => true, 'y2' => true, ), 'polyline' => array( 'points' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, ), ); } /** * Get scan data. * * @return array */ private function get_scan_data(): array { $site_scan = OVKIT_Site_Scan::instance()->get(); $ts = $site_scan['_timestamp'] ?? 0; $groups = array( 'needs_attention' => array( 'label' => __( 'Needs attention', 'ovanap-admin-toolkit' ), 'items' => array(), 'open' => true, ), 'recommended' => array( 'label' => __( 'Recommended', 'ovanap-admin-toolkit' ), 'items' => array(), 'open' => true, ), 'all_good' => array( 'label' => __( 'All good', 'ovanap-admin-toolkit' ), 'items' => array(), 'open' => false, ), 'not_configured' => array( 'label' => __( 'Not configured', 'ovanap-admin-toolkit' ), 'items' => array(), 'open' => false, ), ); if ( $ts ) { foreach ( $site_scan as $key => $item ) { if ( strpos( $key, '_' ) === 0 || ! is_array( $item ) ) { continue; } $st = $item['status'] ?? 'missing'; if ( 'conflict' === $st ) { $groups['needs_attention']['items'][] = $item; } elseif ( 'warning' === $st ) { $groups['recommended']['items'][] = $item; } elseif ( 'ok' === $st ) { $groups['all_good']['items'][] = $item; } else { $groups['not_configured']['items'][] = $item; } } } return array( 'ts' => $ts, 'groups' => $groups, 'conflict_count' => count( $groups['needs_attention']['items'] ), 'warning_count' => count( $groups['recommended']['items'] ), 'ok_count' => count( $groups['all_good']['items'] ), ); } /** * Sorted groups. * * @param array $grouped Grouped. * @param array $scan Scan. * @return array */ private function sorted_groups( array $grouped, array $scan ): array { $risk_order = array( 'risky' => 0, 'advanced' => 1, 'recommended' => 2, 'safe' => 3, ); $sorted = array(); foreach ( $grouped as $section => $features ) { $arr = $features; usort( $arr, function ( $a, $b ) use ( $scan, $risk_order ) { // Primary: priority field (lowest number first). $pa = (int) ( $a['priority'] ?? 99 ); $pb = (int) ( $b['priority'] ?? 99 ); if ( $pa !== $pb ) { return $pa - $pb; } // Tiebreaker 1: detected features first. $det_a = ! empty( $scan[ $a['key'] ]['detected'] ) ? 0 : 1; $det_b = ! empty( $scan[ $b['key'] ]['detected'] ) ? 0 : 1; if ( $det_a !== $det_b ) { return $det_a - $det_b; } // Tiebreaker 2: by risk level. $ra = $risk_order[ $a['risk'] ?? 'safe' ] ?? 3; $rb = $risk_order[ $b['risk'] ?? 'safe' ] ?? 3; return $ra - $rb; } ); $sorted[ $section ] = $arr; } return $sorted; } /** * Audience badge. * * @param string $audience Audience. * @return string */ private function audience_badge( string $audience ): string { if ( 'woocommerce' === $audience ) { return '' . esc_html__( 'WooCommerce', 'ovanap-admin-toolkit' ) . ''; } return ''; } // Shared HTML helpers. /** * Render header. * * @param bool $show_health_check Show health check. */ private function render_header( bool $show_health_check = false ): void { ?>

v
'check', 'warning' => 'alert', 'conflict' => 'x', 'missing' => 'search', ); // Build summary counts for collapsed mode. $conflict_c = count( $groups['needs_attention']['items'] ?? array() ); $warning_c = count( $groups['recommended']['items'] ?? array() ); $ok_c = count( $groups['all_good']['items'] ?? array() ); $sum_parts = array(); if ( $conflict_c ) { /* translators: %d: number of critical issues */ $sum_parts[] = sprintf( _n( '%d critical', '%d critical', $conflict_c, 'ovanap-admin-toolkit' ), $conflict_c ); } if ( $warning_c ) { /* translators: %d: number of recommended items */ $sum_parts[] = sprintf( _n( '%d recommended', '%d recommended', $warning_c, 'ovanap-admin-toolkit' ), $warning_c ); } if ( $ok_c ) { /* translators: %d: number of passing items */ $sum_parts[] = sprintf( _n( '%d passing', '%d passing', $ok_c, 'ovanap-admin-toolkit' ), $ok_c ); } $summary_str = $sum_parts ? implode( ' · ', $sum_parts ) : __( 'No issues found', 'ovanap-admin-toolkit' ); ?>
relative_time( $ts ) ); ?>
$group ) : if ( empty( $group['items'] ) ) { continue; } $open = $all_open ? true : $group['open']; ?>
>
1 ) : ?>

svg_allowlist(); $badge_al = array( 'span' => array( 'class' => true ) ); $wc_active = class_exists( 'WooCommerce' ); foreach ( $sorted_groups as $section => $features ) : $slug = $section_slugs[ $section ] ?? ''; ?>

svg( $f['icon'] ), $svg_al ); ?>
risk_badge( $risk ), $badge_al ); ?> audience_badge( $audience ); if ( $ab ) { echo wp_kses( $ab, $badge_al ); } ?>

array( 'href' => array(), 'target' => array(), 'rel' => array(), ), 'strong' => array(), 'em' => array(), 'code' => array(), 'br' => array(), 'small' => array(), // Inline action buttons inside extras descriptions (e.g. the // "Generate emergency bypass link" button in hide-login-url). // type is locked to "button" to prevent accidental form submit. 'button' => array( 'type' => array(), 'class' => array(), 'data-extra' => array(), ), 'span' => array( 'class' => array(), 'style' => array(), ), ) ); ?>

get_scan_data(); $features = OVKIT_Feature_Registry::instance()->get_all(); $settings = get_option( OVKIT_OPTION, array() ); // Match render_features() visibility: WC-only features hidden when WooCommerce is inactive. $wc_active = class_exists( 'WooCommerce' ); $visible_features = array_filter( $features, function ( $f ) use ( $wc_active ) { $audience = $f['audience'] ?? 'everyone'; return ! ( 'woocommerce' === $audience && ! $wc_active ); } ); $total_features = count( $visible_features ); // Count enabled the same way render_features() does: setting truthy AND key present in registry. $enabled_count = count( array_filter( $settings, function ( $value, $key ) use ( $visible_features ) { return ! empty( $value ) && isset( $visible_features[ $key ] ); }, ARRAY_FILTER_USE_BOTH ) ); $conflict_count = $scan_data['conflict_count']; $scan_url = admin_url( 'admin.php?page=ovkit-scan' ); // Tracking status: GTM configured? $gtm_enabled = ! empty( $settings['track_gtm_enable'] ) && ! empty( trim( (string) ( $settings['track_gtm_id'] ?? '' ) ) ); // Activity status: most recent log entry timestamp (log is stored newest-first). $log = OVKIT_Activity_Log::get_log(); $last_ts = isset( $log[0]['time'] ) ? (int) $log[0]['time'] : 0; $svg_al = $this->svg_allowlist(); ?>
render_header( true ); ?> 0 ) { $state = 'attention'; /* translators: %d: number of issues */ $msg = sprintf( _n( '%d problem needs your attention', '%d problems need your attention', $conflict_count, 'ovanap-admin-toolkit' ), $conflict_count ); $sub = ''; $link = __( 'See what to do →', 'ovanap-admin-toolkit' ); } elseif ( $warning_count > 0 ) { $state = 'recommend'; /* translators: %d: number of recommendations */ $msg = sprintf( _n( 'No problems found, %d recommendation', 'No problems found, %d recommendations', $warning_count, 'ovanap-admin-toolkit' ), $warning_count ); $sub = ''; $link = __( 'See recommendations →', 'ovanap-admin-toolkit' ); } else { $state = 'ok'; $msg = __( 'No problems found in the last site scan', 'ovanap-admin-toolkit' ); $sub = ''; $link = __( 'Scan again →', 'ovanap-admin-toolkit' ); } if ( $scan_data['ts'] ) { /* translators: 1: enabled feature count, 2: total feature count, 3: time of the last scan */ $sub = sprintf( __( '%1$d of %2$d switches on · last scan %3$s', 'ovanap-admin-toolkit' ), $enabled_count, $total_features, $this->relative_time( (int) $scan_data['ts'] ) ); } ?>
0 ) { /* translators: %d: number of issues */ $health_status = sprintf( _n( '%d problem', '%d problems', $conflict_count, 'ovanap-admin-toolkit' ), $conflict_count ); } elseif ( $warning_count > 0 ) { /* translators: %d: number of recommendations */ $health_status = sprintf( _n( '%d recommendation', '%d recommendations', $warning_count, 'ovanap-admin-toolkit' ), $warning_count ); } else { $health_status = __( 'No problems found', 'ovanap-admin-toolkit' ); } $activity_status = $last_ts ? sprintf( /* translators: %s: relative time like "2 hours ago" */ __( 'Last change: %s', 'ovanap-admin-toolkit' ), $this->relative_time( $last_ts ) ) : __( 'No changes yet', 'ovanap-admin-toolkit' ); $nav_cards = array( array( 'url' => admin_url( 'admin.php?page=ovkit-features' ), 'icon' => 'zap', 'title' => __( 'Features', 'ovanap-admin-toolkit' ), 'desc' => __( 'Toggle performance, security, cleanup and more', 'ovanap-admin-toolkit' ), 'status' => $features_status, ), array( 'url' => admin_url( 'admin.php?page=ovkit-tracking' ), 'icon' => 'chart', 'title' => __( 'Tracking', 'ovanap-admin-toolkit' ), 'desc' => __( 'GTM, GA4, Meta Pixel, TikTok Pixel setup', 'ovanap-admin-toolkit' ), 'status' => $tracking_status, ), array( 'url' => $scan_url, 'icon' => 'shield', 'title' => __( 'Site Scan', 'ovanap-admin-toolkit' ), 'desc' => __( 'Full environment scan results', 'ovanap-admin-toolkit' ), 'status' => $health_status, ), array( 'url' => admin_url( 'admin.php?page=ovkit-log' ), 'icon' => 'clock', 'title' => __( 'Activity', 'ovanap-admin-toolkit' ), 'desc' => __( 'Change history and rollback', 'ovanap-admin-toolkit' ), 'status' => $activity_status, ), ); ?>

get_grouped(); $settings = get_option( OVKIT_OPTION, array() ); $scan = OVKIT_Scanner::instance()->get_cached(); $log = OVKIT_Activity_Log::get_formatted(); $section_slugs = array( 'Performance' => 'perf', 'Security' => 'sec', 'Cleanup' => 'clean', 'Admin UI' => 'admin', 'Tracking' => 'track', 'Frontend' => 'front', 'Maintenance' => 'maint', ); $sorted_groups = $this->sorted_groups( $grouped, $scan ); // Compute tab counts using the same visibility rules as render_feature_rows() // (WC-only features hidden when WooCommerce is inactive). $wc_active = class_exists( 'WooCommerce' ); $count_all = 0; $count_quick = 0; $count_det = 0; $count_act = 0; foreach ( $sorted_groups as $features ) { foreach ( $features as $f ) { $audience = $f['audience'] ?? 'everyone'; if ( 'woocommerce' === $audience && ! $wc_active ) { continue; } ++$count_all; $key = $f['key']; $risk = $f['risk'] ?? 'safe'; $level = $f['level'] ?? 'standard'; if ( ( 'safe' === $risk || 'recommended' === $risk ) && 'beginner' === $level ) { ++$count_quick; } if ( ! empty( $scan[ $key ]['detected'] ) ) { ++$count_det; } if ( ! empty( $settings[ $key ] ) ) { ++$count_act; } } } ?>
render_header(); ?>
$slug ) : ?>
render_feature_rows( $sorted_groups, $settings, $scan, $section_slugs ); ?>
render_save_footer( count( $log ) ); ?>
get_grouped(); $settings = get_option( OVKIT_OPTION, array() ); $scan = OVKIT_Scanner::instance()->get_cached(); $log = OVKIT_Activity_Log::get_formatted(); $section_slugs = array( 'Tracking' => 'track' ); $tracking_features = $grouped['Tracking'] ?? array(); $sorted_groups = $this->sorted_groups( array( 'Tracking' => $tracking_features ), $scan ); ?>
render_header(); ?>

render_feature_rows( $sorted_groups, $settings, $scan, $section_slugs ); ?>
render_save_footer( count( $log ) ); ?>
get_scan_data(); ?>
render_header(); ?>

render_scan_panel( $scan_data['groups'], $scan_data['ts'], true ); ?>
render_header(); ?>

5 ) : ?> +