'feed', 'status' => 'unavailable', 'detail' => __( 'The advisory feed is not part of this version of GuardForge.', 'guardforge' ) ]; } if ( $status === 'clean' ) { /* * §6.4 K1-d review round 1, m1: a bare timestamp used to stand * in for the whole sentence — a reader had no way to tell * whether it was fetched_at, checked_at or generated_at (the * state contract carries all three), and the free tier's own * publication lag (named honestly on the Vulnerabilities * screen, class-vuln-db.php partial) was invisible here. "ran * and succeeded" needs a verb, an explicit reading of WHICH * moment this is, and — for the tier that has one — its delay. */ $detail = $fetched_at !== '' /* translators: %s: the date and time the advisories were fetched */ ? sprintf( __( 'Advisories fetched on %s.', 'guardforge' ), $fetched_at ) : __( 'Advisories fetched.', 'guardforge' ); if ( isset( $state['delay_days'] ) && (int) $state['delay_days'] > 0 ) { $delay_days = (int) $state['delay_days']; $detail .= ' ' . sprintf( /* translators: %d: how many days behind the licensed feed this free one is */ _n( 'This free feed is delayed %d day. GuardForge Pro reads the same advisories in real time.', 'This free feed is delayed %d days. GuardForge Pro reads the same advisories in real time.', $delay_days, 'guardforge' ), $delay_days ); } return [ 'key' => 'feed', 'status' => 'ok', 'detail' => $detail, ]; } /* * Never fetched is not a failure: the feed is an opt-in download * (Guideline 7), so on a fresh site nothing has asked for it yet. * Only a fetch that was attempted and failed, or one that has gone * stale, is "not working". */ if ( $status === 'never_fetched' ) { $since = self::overdue_since( $state ); if ( $since > 0 ) { // Switched on, and a day or more has gone by without one download: the daily task is not coming in. return [ 'key' => 'feed', 'status' => 'not_ok', 'overdue' => true, /* translators: %s: the date the feed download was switched on */ 'detail' => sprintf( __( 'The feed download was switched on %s and nothing has been fetched since.', 'guardforge' ), gmdate( 'Y-m-d', $since ) ), ]; } $check = [ 'key' => 'feed', 'status' => 'not_run', 'detail' => __( 'The advisory feed has not been fetched yet.', 'guardforge' ) ]; if ( empty( $state['enabled'] ) && empty( $state['addon'] ) ) { // Nobody has switched it on: the switch is its card on Settings, which is where to send the owner. $check['where'] = 'settings_card'; } return $check; } $detail = __( 'The advisory feed is not up to date.', 'guardforge' ); if ( $status === 'failed' ) { $detail = $error !== '' /* translators: %s: the reason the other side gave, as it gave it */ ? sprintf( __( 'The last fetch failed: %s', 'guardforge' ), $error ) : __( 'The last fetch attempt failed.', 'guardforge' ); } elseif ( $status === 'stale' ) { $detail = $fetched_at !== '' /* translators: %s: the date and time of the last successful fetch */ ? sprintf( __( 'The last successful fetch (%s) is no longer fresh.', 'guardforge' ), $fetched_at ) : __( 'The feed has not been fetched recently.', 'guardforge' ); } return [ 'key' => 'feed', 'status' => 'not_ok', 'detail' => $detail ]; } public static function scan_check( array $facts ) { $enabled = ! empty( $facts['enabled'] ); $scan_id = (int) ( $facts['scan_id'] ?? 0 ); $scanned_at = (string) ( $facts['scanned_at'] ?? '' ); $now = isset( $facts['now'] ) ? (int) $facts['now'] : 0; $scope = ! empty( $facts['scope_extras'] ) ? __( 'core, plugins and themes', 'guardforge' ) : __( 'core files only', 'guardforge' ); /* * Nothing on record means the scan has never run — whether or not the * daily switch is on (switched on a minute ago, the first scan is * still due). That is "not run yet", not "not working": the integrity * monitor is opt-in (Guideline 7) and ships off, so a fresh site is * here by design. The entry screen offers the one-off scan. * * Until the switch has been on for longer than the grace. A monitor * that was switched on a day or more ago and has not completed one * scan is not waiting for its first run, it is not running: that is * the failure the neutral line used to cover for ("on, then silence"). */ if ( $scan_id <= 0 || $scanned_at === '' ) { $since = self::overdue_since( $facts ); if ( $since > 0 ) { return [ 'key' => 'scan', 'status' => 'not_ok', 'overdue' => true, /* translators: %s: the date daily monitoring was switched on */ 'detail' => sprintf( __( 'Daily monitoring was switched on %s and no scan has completed since.', 'guardforge' ), gmdate( 'Y-m-d', $since ) ), ]; } return [ 'key' => 'scan', 'status' => 'not_run', 'detail' => $enabled ? __( 'Daily monitoring is on, but its first scan has not completed yet.', 'guardforge' ) : __( 'No integrity scan has run yet.', 'guardforge' ), ]; } $stamp = strtotime( $scanned_at ); $age_days = $stamp ? ( $now - $stamp ) / 86400 : PHP_INT_MAX; $recent = $age_days <= self::STALE_SCAN_DAYS; if ( ! $enabled ) { /* * Daily monitoring is off, so no scan is owed on a schedule: what * is on record is a one-off from the "Run scan now" button, which * is consent for that one scan and schedules nothing. A recent one * is a check that ran and succeeded, so it is green — saying * plainly that nothing repeats it. An old one is neither a pass * nor a failure: nothing was promised. */ if ( $recent ) { return [ 'key' => 'scan', 'status' => 'ok', /* translators: 1: the scan's number, 2: the date and time it ran, 3: what it covered, e.g. "core files only" */ 'detail' => sprintf( __( 'Scan #%1$d on %2$s, scope: %3$s.', 'guardforge' ), $scan_id, $scanned_at, $scope ) . ' ' . __( 'Daily monitoring is switched off, so this was a one-off scan.', 'guardforge' ), ]; } return [ 'key' => 'scan', 'status' => 'unavailable', /* translators: %s: the date and time of the last scan */ 'detail' => sprintf( __( 'The last scan was on %s. Daily monitoring is switched off, so it has not been repeated.', 'guardforge' ), $scanned_at ), ]; } if ( ! $recent ) { return [ 'key' => 'scan', 'status' => 'not_ok', 'detail' => sprintf( /* translators: 1: the date and time of the last completed scan, 2: how many days ago is too long */ _n( 'The last completed scan was on %1$s — more than %2$d day ago; the daily check may have failed or been skipped.', 'The last completed scan was on %1$s — more than %2$d days ago; the daily check may have failed or been skipped.', self::STALE_SCAN_DAYS, 'guardforge' ), $scanned_at, self::STALE_SCAN_DAYS ), ]; } return [ 'key' => 'scan', 'status' => 'ok', /* translators: 1: the scan's number, 2: the date and time it ran, 3: what it covered, e.g. "core files only" */ 'detail' => sprintf( __( 'Scan #%1$d on %2$s, scope: %3$s.', 'guardforge' ), $scan_id, $scanned_at, $scope ), ]; } /** * When the switch went on, if it is on and a result is overdue; 0 otherwise. * * Overdue = on for longer than max(FIRST_RUN_GRACE, the schedule's own * interval): a weekly task is not late after three days, and a short * interval never shrinks the day. A switch that is off, a date nobody * recorded (0) or a missing clock can never be called overdue — nothing * says it is late. * * @param array $facts 'enabled', 'enabled_since', 'schedule_interval', 'now'. * @return int Unix timestamp of the switch-on moment, or 0. */ private static function overdue_since( array $facts ) { if ( empty( $facts['enabled'] ) ) { return 0; } $since = isset( $facts['enabled_since'] ) ? (int) $facts['enabled_since'] : 0; $now = isset( $facts['now'] ) ? (int) $facts['now'] : 0; if ( $since <= 0 || $now <= 0 ) { return 0; } $interval = isset( $facts['schedule_interval'] ) ? (int) $facts['schedule_interval'] : 0; $grace = max( self::FIRST_RUN_GRACE, $interval ); return ( $now - $since ) > $grace ? $since : 0; } public static function alerts_check( $available, array $state ) { if ( ! $available ) { return [ 'key' => 'alerts', 'status' => 'unavailable', 'detail' => __( 'Alerts are part of Pro.', 'guardforge' ) ]; } if ( empty( $state['configured'] ) ) { return [ 'key' => 'alerts', 'status' => 'unavailable', 'detail' => __( 'No alert channel is configured yet.', 'guardforge' ) ]; } $checked_at = (string) ( $state['checked_at'] ?? '' ); if ( $checked_at === '' ) { return [ 'key' => 'alerts', 'status' => 'unavailable', 'detail' => __( 'A channel is configured, but no alert has fired yet.', 'guardforge' ) ]; } if ( empty( $state['ok'] ) ) { $error = (string) ( $state['error'] ?? '' ); return [ 'key' => 'alerts', 'status' => 'not_ok', 'detail' => $error !== '' /* translators: 1: the reason the channel gave, as it gave it, 2: the date and time of the attempt */ ? sprintf( __( 'The last alert attempt failed: %1$s (%2$s)', 'guardforge' ), $error, $checked_at ) /* translators: %s: the date and time of the attempt */ : sprintf( __( 'The last alert attempt failed. (%s)', 'guardforge' ), $checked_at ), ]; } /* translators: %s: the date and time the last alert was sent */ return [ 'key' => 'alerts', 'status' => 'ok', 'detail' => sprintf( __( 'Last alert sent successfully on %s.', 'guardforge' ), $checked_at ) ]; } public static function cloudflare_check( $available, $configured, array $state ) { if ( ! $available ) { return [ 'key' => 'cloudflare', 'status' => 'unavailable', 'detail' => __( 'Cloudflare sync is part of Pro.', 'guardforge' ) ]; } if ( ! $configured ) { return [ 'key' => 'cloudflare', 'status' => 'unavailable', 'detail' => __( 'No zone or API token is saved yet.', 'guardforge' ) ]; } $checked_at = (string) ( $state['checked_at'] ?? '' ); if ( $checked_at === '' ) { return [ 'key' => 'cloudflare', 'status' => 'not_ok', 'detail' => __( 'Configured, but the token has never been verified against Cloudflare.', 'guardforge' ) ]; } if ( empty( $state['ok'] ) ) { $error = (string) ( $state['error'] ?? '' ); return [ 'key' => 'cloudflare', 'status' => 'not_ok', 'detail' => $error !== '' /* translators: 1: the reason Cloudflare gave, as it gave it, 2: the date and time of the check */ ? sprintf( __( 'Verification failed: %1$s (%2$s)', 'guardforge' ), $error, $checked_at ) /* translators: %s: the date and time of the check */ : sprintf( __( 'Verification failed. (%s)', 'guardforge' ), $checked_at ), ]; } $zone = (string) ( $state['zone_name'] ?? '' ); return [ 'key' => 'cloudflare', 'status' => 'ok', 'detail' => $zone !== '' /* translators: 1: the date and time of the check, 2: the Cloudflare zone's name */ ? sprintf( __( 'Verified on %1$s (zone "%2$s").', 'guardforge' ), $checked_at, $zone ) /* translators: %s: the date and time of the check */ : sprintf( __( 'Verified on %s.', 'guardforge' ), $checked_at ), ]; } /** * Run all four checks and split them the way the entry screen shows * them: 'ok' into $verified, 'not_ok' into $not_working, 'not_run' into * $not_run (neutral, with the way to run it), 'unavailable' kept * separately so the partial can print it quietly (still honest, never * colored as a failure) rather than dropping it silently. * * @param array $checks * @return array{verified:array,not_working:array,not_run:array,unavailable:array} */ public static function split_checks( array $checks ) { $out = [ 'verified' => [], 'not_working' => [], 'not_run' => [], 'unavailable' => [] ]; foreach ( $checks as $check ) { switch ( (string) ( $check['status'] ?? '' ) ) { case 'ok': $out['verified'][] = $check; break; case 'not_ok': $out['not_working'][] = $check; break; case 'not_run': $out['not_run'][] = $check; break; default: $out['unavailable'][] = $check; } } return $out; } }