snapshot(); $staging = GUARDFORGE_Staging::snapshot(); // §6.4 K1-d: the first screen's "needs a decision" / "verified" case // buckets. A pure read — no sync here (that stays wired to its own // daily cron and to the Cases screen's own render, see class-cases.php) // so the entry screen never writes to the case table on every load. $gf_verified_window_days = 30; $gf_cases_summary = class_exists( 'GUARDFORGE_Cases' ) ? GUARDFORGE_Cases::instance()->first_screen_summary( $gf_verified_window_days ) : [ 'needs_decision' => [], 'needs_decision_total' => 0, 'verified_count' => 0, 'as_of' => '' ]; $gf_checks = self::first_screen_checks(); require GUARDFORGE_DIR . 'admin/partials/settings.php'; } /** * §6.4 K1-d: the four "is protection actually working" checks, sorted the * way the entry screen shows them. Every fact is read from the exact * contract GF-G1..G3 already built — nothing here fetches a feed, runs a * scan, sends an alert, or calls Cloudflare; the render must not have side * effects of its own. Public and static so what is handed to the pure * rules can be tested without rendering the screen. * * @return array{verified:array,not_working:array,not_run:array,unavailable:array} */ public static function first_screen_checks() { // The rules run on the site's clock (the one the stored scan and fetch // stamps are written in); the switch-on dates are UTC, so they are // moved onto that clock by the site's offset. $gf_now = (int) current_time( 'timestamp' ); $gf_shift = $gf_now - time(); // Asked the way every other add-on feature is (class_exists is the floor, the add-on answers for its // licence) and not through GUARDFORGE_Vuln_Feed: the wp.org package has no such class, and an add-on that // brings the feed the package itself lacks is still a feed that works. $gf_pro_feed = GUARDFORGE_Pro_Bridge::feature_available( 'vuln_db', 'GUARDFORGE_Vuln_Db' ); if ( $gf_pro_feed ) { // The licensed feed has no consent switch of its own and no date: nothing to measure a silence against. $gf_feed_state = GUARDFORGE_Vuln_Db::state() + [ 'addon' => true ]; } elseif ( class_exists( 'GUARDFORGE_Vuln_Feed' ) ) { $gf_feed_since = (int) get_option( GUARDFORGE_Vuln_Feed::ENABLED_SINCE_OPTION, 0 ); $gf_feed_state = GUARDFORGE_Vuln_Feed::state() + [ 'enabled' => GUARDFORGE_Vuln_Feed::consent_given(), 'enabled_since' => $gf_feed_since > 0 ? $gf_feed_since + $gf_shift : 0, 'schedule_interval' => self::schedule_interval( GUARDFORGE_Vuln_Feed::CRON_HOOK ), 'now' => $gf_now, ]; } else { // The wp.org package carries no feed (and no class behind it) and no licensed add-on brings one: // nothing to run, nothing to link to. $gf_feed_state = [ 'status' => 'unavailable' ]; } $gf_scan_since = (int) get_option( GUARDFORGE_Integrity::ENABLED_SINCE_OPTION, 0 ); $gf_scan_facts = [ 'enabled' => (bool) GUARDFORGE_Options::get( 'integrity_monitor' ), 'scan_id' => (int) get_option( GUARDFORGE_Integrity::SCAN_ID_OPTION, 0 ), 'scanned_at' => (string) ( get_option( GUARDFORGE_Integrity::LAST_RESULT_OPTION, [] )['scanned_at'] ?? '' ), 'scope_extras' => (bool) GUARDFORGE_Options::get( 'integrity_scope_extras' ), 'enabled_since' => $gf_scan_since > 0 ? $gf_scan_since + $gf_shift : 0, 'schedule_interval' => self::schedule_interval( GUARDFORGE_Integrity::CRON_HOOK ), 'now' => $gf_now, ]; $gf_alerts_available = GUARDFORGE_Pro_Bridge::feature_available( 'alerts_webhook', 'GUARDFORGE_Alerts' ); $gf_alerts_state = $gf_alerts_available ? GUARDFORGE_Alerts::state() : []; $gf_cf_available = GUARDFORGE_Pro_Bridge::feature_available( 'cloudflare_sync', 'GUARDFORGE_Cloudflare_Sync' ); $gf_cf_configured = $gf_cf_available && GUARDFORGE_Options::get_secret( 'cf_api_token' ) !== '' && (string) GUARDFORGE_Options::get( 'cf_zone_id', '' ) !== ''; $gf_cf_state = $gf_cf_available ? GUARDFORGE_Cloudflare_Sync::state() : []; return GUARDFORGE_First_Screen::split_checks( [ GUARDFORGE_First_Screen::feed_check( $gf_feed_state ), GUARDFORGE_First_Screen::scan_check( $gf_scan_facts ), GUARDFORGE_First_Screen::alerts_check( $gf_alerts_available, $gf_alerts_state ), GUARDFORGE_First_Screen::cloudflare_check( $gf_cf_available, $gf_cf_configured, $gf_cf_state ), ] ); } /** * How often a scheduled task runs, in seconds; 0 when it is not on the * schedule (or WordPress does not know the recurrence), which the first * screen reads as "a day". */ private static function schedule_interval( $hook ) { $recurrence = wp_get_schedule( $hook ); if ( ! $recurrence ) { return 0; } $schedules = wp_get_schedules(); return isset( $schedules[ $recurrence ]['interval'] ) ? (int) $schedules[ $recurrence ]['interval'] : 0; } public function render_hardening_score() { if ( ! current_user_can( 'manage_options' ) ) { return; } require GUARDFORGE_DIR . 'admin/partials/hardening-score.php'; } public function render_login_log() { if ( ! current_user_can( 'manage_options' ) ) { return; } require GUARDFORGE_DIR . 'admin/partials/login-log.php'; } public function render_audit_log() { if ( ! current_user_can( 'manage_options' ) ) { return; } require GUARDFORGE_DIR . 'admin/partials/audit-log.php'; } public function render_integrity() { if ( ! current_user_can( 'manage_options' ) ) { return; } require GUARDFORGE_DIR . 'admin/partials/integrity.php'; } public function render_unblock() { if ( ! current_user_can( 'manage_options' ) ) { return; } require GUARDFORGE_DIR . 'admin/partials/unblock.php'; } /** * The one screen in this plugin that is not administrators-only: it * enrols the caller's own account. Reads the same capability the menu * registered with, so the two can never drift apart. */ public function render_two_factor() { if ( ! current_user_can( GUARDFORGE_Core::two_factor_capability() ) ) { return; } require GUARDFORGE_DIR . 'admin/partials/two-factor.php'; } /** §6.4 K1-a: the case list built from integrity/vuln/login-log observations. */ public function render_cases() { if ( ! current_user_can( 'manage_options' ) ) { return; } if ( class_exists( 'GUARDFORGE_Cases' ) ) { GUARDFORGE_Cases::instance()->sync(); } require GUARDFORGE_DIR . 'admin/partials/cases.php'; } public function render_vuln_db() { if ( ! current_user_can( 'manage_options' ) ) { return; } require GUARDFORGE_DIR . 'admin/partials/vuln-db.php'; } /** * What a "Fetch now" press leaves behind: "Feed refreshed (N records)." for a pull that went through, and for one * that did not, the reason it gave. * * The button is on two screens, Vulnerabilities and the overdue feed row on the first card of Settings, and the * handler behind it (the free feed's, and the add-on's the same way) sends the owner back to the screen they * pressed it on with two flags in the address: gf_vulndb_ok and gf_vulndb_rows. Vulnerabilities read them; * Settings read them nowhere, so a press there came back to a screen that looked as it had before the press. * Both screens print the notice from here, so the sentence, its count and its markup cannot drift apart. * * gf_vulndb_ok is the outcome, 1 or 0, and its value is read, not only its presence: it used to be 1 for every * press, and a download that failed was announced as a refresh. A 0 prints the red notice, built from the * messages the first card of Settings already uses ("The last fetch failed: %s", and "The last fetch attempt * failed." when no reason was kept). The reason is read from the stored state of the feed whose handler judged the * press, and never from the address, which carries no text: the way back names that feed in gf_vulndb_feed, "free" * or "pro" (a name from a list of two). The feed behind the screen is not always the one that was pulled: a press * that reaches the free handler while the add-on is in charge (a Settings tab opened before the add-on was switched * on) is judged on the free feed's state, and the add-on's holds another reason, or none. An address without a name * (an older add-on, a bookmark) or with any other value keeps the choice made before the name existed, the feed * behind the screen. Any other value of gf_vulndb_ok picks no notice at all. * * Public and static because the two partials call it by the class name, from whatever scope they were included * into. The class is in the wordpress.org package, which renders Vulnerabilities for the add-on's sake; the free * feed, and with it the button on Settings, is not in that package. * * Display only: the flags are added by our own redirect after a nonce-checked action, they only pick the * notice, and the count is cast to a non-negative integer before it is printed. */ public static function render_feed_refreshed_notice() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- display-only: a flag our own redirect adds after a nonce-checked action; it is only compared with '0' and '1', never printed. $flag = ( isset( $_GET['gf_vulndb_ok'] ) && is_string( $_GET['gf_vulndb_ok'] ) ) ? wp_unslash( $_GET['gf_vulndb_ok'] ) : ''; if ( '0' === $flag ) { self::render_feed_failed_notice(); return; } if ( '1' !== $flag ) { return; } ?>