get(self::ID); if (!$row) { return ['enabled' => false, 'grace_days' => 7, 'started' => 0, 'epoch' => '', '_revision' => 0]; } if (!isset($row['enabled'], $row['grace_days'], $row['started'], $row['epoch']) || !is_bool($row['enabled']) || !is_int($row['grace_days']) || $row['grace_days'] < 1 || $row['grace_days'] > 30 || !is_int($row['started']) || !is_string($row['epoch'])) { throw new \RuntimeException('The administrator protection policy could not be read. Restore its retained record or use documented host recovery.'); } return $row; } /** Uses the unfiltered role/capability map to avoid recursion through user_has_cap. */ public static function targeted(int $uid): bool { $user = get_userdata($uid); return $user instanceof \WP_User && (in_array('administrator', (array)$user->roles, true) || !empty($user->allcaps['manage_options'])); } private static function rolesTargeted(array $roles): bool { foreach ($roles as $role) { $definition = get_role($role); if ($role === 'administrator' || ($definition && !empty($definition->capabilities['manage_options']))) { return true; } } return false; } public static function status(int $uid): array { $policy = self::settings(); $targeted = self::targeted($uid); $enrolled = $uid > 0 && AccountSecurity::enrolled($uid); $start = (int)$policy['started']; if ($policy['enabled'] && $targeted) { $assignment = (new Store())->get('auth_policy_user_' . $uid); if ($assignment && ($assignment['epoch'] ?? '') === $policy['epoch']) { $start = max($start, (int)$assignment['started']); } } $deadline = $start + (int)$policy['grace_days'] * DAY_IN_SECONDS; $required = $policy['enabled'] && $targeted && !self::recovery(); return ['enabled' => $policy['enabled'], 'targeted' => $targeted, 'required' => $required, 'enrolled' => $enrolled, 'deadline' => $deadline, 'restricted' => $required && !$enrolled && time() >= $deadline, 'grace_days' => $policy['grace_days'], 'host_recovery' => self::recovery()]; } public static function restricted(int $uid): bool { if ($uid < 1 || self::recovery() || !self::targeted($uid)) { return false; } if (!array_key_exists($uid, self::$requestStatus)) { try { self::$requestStatus[$uid] = self::status($uid)['restricted']; } catch (\Throwable $error) { self::$requestStatus[$uid] = true; } } return self::$requestStatus[$uid]; } public static function capabilities(array $allcaps, array $caps, array $args, \WP_User $user): array { if (self::restricted((int)$user->ID)) { // Retain the account setup/recovery page. No setting, content, user or plugin administration. foreach ($allcaps as $name => $allowed) { $allcaps[$name] = in_array($name, ['read', 'exist'], true) && $allowed; } $allcaps['read'] = true; $allcaps['exist'] = true; $allcaps['do_not_allow'] = false; } return $allcaps; } public static function rest($result) { if (is_wp_error($result)) { return $result; } return self::restricted(get_current_user_id()) ? new \WP_Error('antivirus_ai_enrollment_required', 'Set up your administrator second factor on the WordPress Account protection page before using the API.', ['status' => 403]) : $result; } public static function adminGate(): void { if (!self::restricted(get_current_user_id())) { return; } if (DeviceTrust::recoveryRequest()) { return; } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Only size or unchanged-byte validation uses the original input here; the accepted value is unslashed and sanitized, and normalization cannot create an allowed method, action or credential. $method = isset( $_SERVER['REQUEST_METHOD'] ) && is_string( $_SERVER['REQUEST_METHOD'] ) && strlen( $_SERVER['REQUEST_METHOD'] ) <= 16 ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Only size or unchanged-byte validation uses the original input here; the accepted value is unslashed and sanitized, and normalization cannot create an allowed method, action or credential. if ( wp_doing_ajax() || 'GET' !== $method || $method !== wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) { wp_die(esc_html__('Set up your administrator second factor on Account protection before continuing.', 'shieldlume-security'), '', ['response' => 403]); } wp_safe_redirect(AccountSecurity::pageUrl()); exit; } public static function loginRedirect(string $redirect, string $requested, $user): string { return $user instanceof \WP_User && self::restricted((int)$user->ID) ? admin_url('profile.php?page=antivirus-ai-account') : $redirect; } public static function notice(): void { try { $state = self::status(get_current_user_id()); } catch (\Throwable $error) { return; } if (!$state['required'] || $state['enrolled']) { return; } $text = $state['restricted'] ? 'Administrator actions are restricted until you finish second-factor setup. Your existing sessions have not been deleted.' : 'Your administrator account needs a second factor by ' . wp_date('M j, Y H:i T', $state['deadline']) . '. Set it up now and save your recovery codes.'; echo '
' . esc_html($text) . ' Account protection