'; echo esc_html( sprintf( /* translators: 1: installed Pro version, 2: required Pro version */ __( 'GuardForge Pro %1$s is too old for this version of GuardForge, so it is switched off rather than left running. Install GuardForge Pro %2$s or newer.', 'guardforge' ), $installed, $required ) ); // The second line carries no placeholder, because the machine translator drops %1$s and %2$s when it rewrites a sentence this long. echo '
'; echo esc_html( __( 'Until then alerts to Telegram, Slack or a webhook, the Cloudflare firewall push, automatic lockdown, patch by update, the WAF, malware scanning, AI threat analysis, required 2FA and audit export are unavailable. Your logs, baselines and settings are kept.', 'guardforge' ) ); echo '
'; echo esc_html( __( 'If Dashboard → Updates lists no update for it, download the add-on from your Avakode dashboard and upload it over this copy in Plugins → Add New Plugin → Upload Plugin.', 'guardforge' ) ); echo ' ' . esc_html( __( 'Open Downloads', 'guardforge' ) ) . ''; echo '
'; } /** * The capability the 2FA enrolment screen registers at. * * Read through defined rather than referenced directly, because * guardforge_init stands down on a site where GuardForge Pro 1.0.x still * carries its own copy of class-two-factor.php: on that site * GUARDFORGE_Two_Factor is the old vintage, which has no ENROL_CAP, and a * bare reference would raise "Undefined constant" on every admin page — * reopening the exact fatal that bootstrap guard exists to prevent. * * The fallback degrades the right way. When the class that will receive * the POST is not ours, the menu stays at manage_options: shut, rather * than a form that dies on submit. * * @return string */ public static function two_factor_capability() { return defined( 'GUARDFORGE_Two_Factor::ENROL_CAP' ) ? GUARDFORGE_Two_Factor::ENROL_CAP : 'manage_options'; } public function admin_menu() { add_menu_page( __( 'GuardForge', 'guardforge' ), __( 'GuardForge', 'guardforge' ), 'manage_options', 'guardforge', [ GUARDFORGE_Admin::instance(), 'render_settings' ], 'dashicons-shield-alt', 80 ); // [ slug, label, render method, capability? ]. The fourth element is // optional and defaults to manage_options — only 2FA sets it, because // only 2FA acts on the caller's own account and nobody else's. // Order (GF-G4): Settings first — it now carries the "at a glance" // summary (needs a decision / verified / not working). Directly under // it sit the five screens that summary is actually a reading of, and // whose own honesty this Gate 0 wave rebuilt — the hardening score, // the vulnerability feed, the malware scanner, file integrity and // Cloudflare token verification — so opening any row on the entry // screen is at most one click from its detail. Logs, response tools // and pure configuration follow, in that order. The Alerts and License // rows are the add-on's since 1.6.0 (GuardForge Pro registers them // after these thirteen): both screens only ever ran against the // add-on's classes, and without it there is nothing behind them. $sub = [ [ 'guardforge', __( 'Settings', 'guardforge' ), 'render_settings' ], [ 'guardforge-score', __( 'Hardening score', 'guardforge' ), 'render_hardening_score' ], [ 'guardforge-vulndb', __( 'Vulnerabilities', 'guardforge' ), 'render_vuln_db' ], [ 'guardforge-scanner', __( 'Malware scanner', 'guardforge' ), 'render_scanner' ], [ 'guardforge-integrity', __( 'File integrity', 'guardforge' ), 'render_integrity' ], [ 'guardforge-cloudflare', __( 'Cloudflare', 'guardforge' ), 'render_cloudflare' ], // §6.4 K1-a: cases built FROM the five screens above (an // unexplained integrity finding, an advisory match, a login // series) — placed right after them, ahead of the raw logs they // are drawn from. [ 'guardforge-cases', __( 'Cases', 'guardforge' ), 'render_cases' ], [ 'guardforge-audit', __( 'Audit log', 'guardforge' ), 'render_audit_log' ], [ 'guardforge-log', __( 'Login log', 'guardforge' ), 'render_login_log' ], [ 'guardforge-unblock', __( 'Unblock IP', 'guardforge' ), 'render_unblock' ], [ 'guardforge-lockdown', __( 'Lockdown', 'guardforge' ), 'render_lockdown' ], [ 'guardforge-geo', __( 'Geo-IP', 'guardforge' ), 'render_geo_ip' ], [ 'guardforge-2fa', __( '2FA', 'guardforge' ), 'render_two_factor', self::two_factor_capability() ], ]; foreach ( $sub as $row ) { add_submenu_page( 'guardforge', $row[1], $row[1], $row[3] ?? 'manage_options', $row[0], [ GUARDFORGE_Admin::instance(), $row[2] ] ); } } public function add_body_class( $classes ) { if ( $this->is_guardforge_screen() ) { $classes .= ' gf-admin'; } return $classes; } public function enqueue_assets( $hook ) { if ( ! $this->is_guardforge_screen() ) { return; } wp_enqueue_style( 'guardforge-admin', GUARDFORGE_URL . 'assets/css/admin.css', [], GUARDFORGE_VERSION ); wp_enqueue_script( 'guardforge-admin', GUARDFORGE_URL . 'assets/js/admin.js', [ 'jquery' ], GUARDFORGE_VERSION, true ); wp_localize_script( 'guardforge-admin', 'guardforgeAdmin', [ 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'guardforge_admin' ), 'i18n' => [ 'confirm_clear' => __( 'Clear the log? This cannot be undone.', 'guardforge' ), 'scanning' => __( 'Scanning…', 'guardforge' ), 'unblock' => __( 'Unblock', 'guardforge' ), ], ] ); } /** * Where a notice of ours may appear: our own screens, the plugin list and * the dashboard. * * Guideline 11 — "plugins should not hijack the admin dashboard" — and the * T1 review of 20.09.2026 flagged the pattern. A notice about this plugin * on every screen of somebody else's admin is noise; on the three screens * where the reader can act on it, it is information. */ public static function notice_screen_allows() { // admin_notices fires inside wp-admin and nowhere else, so a missing // screen is an unusual context rather than a wrong one: hiding a // notice we cannot place would lose it altogether. if ( ! function_exists( 'get_current_screen' ) ) { return true; } $screen = get_current_screen(); if ( ! $screen ) { return true; } $id = (string) $screen->id; return in_array( $id, [ 'dashboard', 'plugins', 'plugins-network' ], true ) || strpos( $id, 'guardforge' ) !== false; } private function is_guardforge_screen() { if ( ! function_exists( 'get_current_screen' ) ) { return false; } $screen = get_current_screen(); if ( ! $screen ) { return false; } return strpos( $screen->id, 'guardforge' ) !== false; } }