File: /var/www/karjerosdiena.lt/wp-content/plugins/sucuri-scanner/src/topt.lib.php
<?php
// Abort if the file is loaded out of context.
if (!defined('SUCURISCAN_INIT') || SUCURISCAN_INIT !== true) {
if (!headers_sent()) {
/* Report invalid access if possible. */
header('HTTP/1.1 403 Forbidden');
}
exit(1);
}
/**
* This class implements Two-Factor Authentication (2FA) using TOTP (Time-based One-Time Password).
*/
class SucuriScanTwoFactor extends SucuriScan
{
const OPTION_PREFIX = 'sucuriscan_totp_';
const SECRET_META_KEY = 'sucuriscan_topt_secret_key';
const LAST_SUCCESS_META_KEY = 'sucuriscan_topt_last_success';
const LOGIN_TOKEN_TTL = 600;
const LOGIN_TOKEN_MAX_ATTEMPTS = 5;
const DEFAULT_CODE_ERROR = 'sucuriscan_profile_error';
const LOGIN_TRANSIENT_PREFIX = 'sucuri_2fa_';
const LOGIN_TOKEN_PATTERN = '[A-Za-z0-9]{10,128}';
const LOGIN_TOKEN_MIN_LENGTH = 10;
const LOGIN_TOKEN_MAX_LENGTH = 128;
public static function add_hooks()
{
add_filter('authenticate', array(__CLASS__, 'authenticate'), 30, 3);
add_action('login_form_sucuri-2fa', array(__CLASS__, 'login_form_2fa'));
add_action('login_form_sucuri-2fa-setup', array(__CLASS__, 'login_form_2fa_setup'));
add_action('login_head', array(__CLASS__, 'brand_login_logo'));
add_action('show_user_profile', array(__CLASS__, 'render_user_profile_section'));
add_action('edit_user_profile', array(__CLASS__, 'render_user_profile_section'));
add_action('personal_options_update', array(__CLASS__, 'save_user_profile_section'));
add_action('edit_user_profile_update', array(__CLASS__, 'save_user_profile_section'));
add_action('admin_enqueue_scripts', array(__CLASS__, 'enqueue_profile_assets'));
add_action('wp_ajax_sucuri_profile_2fa_enable', array(__CLASS__, 'ajax_profile_enable'));
add_action('wp_ajax_sucuri_profile_2fa_reset', array(__CLASS__, 'ajax_profile_reset'));
}
protected static $profile_error_queue = array();
protected static $profile_error_hook_registered = false;
/**
* Adds an error to be displayed on the user profile page.
*
* @param mixed $code
* @param mixed $message
*
* @return void
*/
protected static function add_profile_error($code, $message)
{
$code = (string) (is_scalar($code) ? $code : self::DEFAULT_CODE_ERROR);
$code = trim($code) !== '' ? $code : self::DEFAULT_CODE_ERROR;
$message = (string) (is_scalar($message) ? $message : '');
$message = trim($message);
if ($message === '') {
// Do not enqueue empty messages.
return;
}
if (!is_array(self::$profile_error_queue)) {
self::$profile_error_queue = array();
}
self::$profile_error_queue[] = array('code' => $code, 'message' => $message);
if (!self::$profile_error_hook_registered) {
add_action('user_profile_update_errors', array(__CLASS__, 'on_profile_update_errors'), 10, 3);
self::$profile_error_hook_registered = true;
}
}
/**
* Flush queued profile errors into the WP_Error object provided by WordPress.
* WordPress passes a WP_Error instance by reference as the first argument of the
* 'user_profile_update_errors' action. If for any reason it's not a WP_Error we just drop.
*
* @param mixed $errors Expected WP_Error instance.
*
* @return void
*/
public static function on_profile_update_errors($errors)
{
if (empty(self::$profile_error_queue) || !is_array(self::$profile_error_queue)) {
self::$profile_error_queue = array();
return;
}
if (!class_exists('WP_Error') || !($errors instanceof WP_Error)) {
// Can't safely add errors; clear queue to avoid leakage into later requests.
self::$profile_error_queue = array();
return;
}
foreach (self::$profile_error_queue as $item) {
if (!is_array($item)) {
continue;
}
$code = isset($item['code']) ? (string) $item['code'] : '';
$message = isset($item['message']) ? (string) $item['message'] : '';
if ($code === '' || $message === '') {
continue;
}
$isDuplicate = false;
$existing = $errors->get_error_messages($code);
if (!empty($existing) && in_array($message, $existing, true)) {
$isDuplicate = true;
}
if (!$isDuplicate) {
$errors->add($code, $message);
}
}
self::$profile_error_queue = array();
}
/**
* Determine whether Two-Factor is enforced for a given user.
*
* Modes (stored in :twofactor_mode):
* - disabled => never enforce
* - all_users => enforce for every valid user id > 0
* - selected_users => enforce only for IDs listed in :twofactor_users (array)
* Any other/unknown mode safely falls back to 'disabled'.
*
* Security / Defensive Notes:
* - We normalize/sanitize user lists to integers and strictly compare.
* - We bound extremely large lists (> 25k) to a safe failure (returns false) to avoid memory pressure from malformed options.
*
* @param int $user_id User ID to evaluate.
*
* @return bool Whether enforcement applies.
*/
protected static function is_enforced_for_user($user_id)
{
$user_id = (int) $user_id;
if ($user_id <= 0) {
return false; // Invalid target user.
}
$mode = (string) SucuriScanOption::getOption(':twofactor_mode');
if ($mode === '' || $mode === null) {
$mode = 'disabled';
}
// Whitelist allowed modes; unknown values become 'disabled' to fail closed.
static $allowed_modes = array('disabled', 'all_users', 'selected_users');
if (!in_array($mode, $allowed_modes, true)) {
$mode = 'disabled';
}
switch ($mode) {
case 'disabled':
return false;
case 'all_users':
return true;
case 'selected_users':
$list = SucuriScanOption::getOption(':twofactor_users');
if (!is_array($list) || empty($list)) {
return false;
}
$normalized = array();
foreach ($list as $id) {
$id = (int) $id;
if ($id > 0) {
$normalized[$id] = true;
}
}
if (empty($normalized)) {
return false;
}
if (count($normalized) > 25000) {
return false; // List too large / suspicious.
}
return isset($normalized[$user_id]);
}
return false; // Fallback.
}
/**
* This function enqueues the necessary assets for the user profile page if 2FA is enforced.
*
* @param mixed $hook
*
* @return void
*/
public static function enqueue_profile_assets($hook)
{
if (!is_admin()) {
return;
}
$is_profile_context = ($hook === 'profile.php' || $hook === 'user-edit.php');
$is_plugin_twofactor_page = false;
if (is_string($hook) && strpos($hook, 'sucuriscan_2fa') !== false) {
$is_plugin_twofactor_page = true;
}
if (!$is_profile_context && !$is_plugin_twofactor_page) {
return; // Not a target page.
}
$target_user = 0;
if ($is_profile_context) {
if ($hook === 'profile.php') {
$target_user = get_current_user_id();
} elseif ($hook === 'user-edit.php') {
$req_user = SucuriScanRequest::get('user_id', '[0-9]+');
if ($req_user !== false) {
$target_user = (int) $req_user;
}
}
} else {
$target_user = get_current_user_id();
}
if (!self::is_enforced_for_user($target_user) && !$is_plugin_twofactor_page) {
return;
}
self::ensure_qr_script();
}
protected static function create_login_token($user_id, $remember, $redirect_to, $secret_for_setup = '')
{
$token = wp_generate_password(64, false, false);
$data = array(
'user_id' => (int) $user_id,
'remember' => (bool) $remember,
'redirect' => (string) $redirect_to,
'secret' => (string) $secret_for_setup,
'created' => time(),
'attempts' => 0,
'ua' => isset($_SERVER['HTTP_USER_AGENT']) ? (string) $_SERVER['HTTP_USER_AGENT'] : '',
);
set_transient(self::transient_key($token), $data, self::LOGIN_TOKEN_TTL);
return $token;
}
/**
* This function retrieves the login session data associated with a given token.
*
* @param mixed $token
*/
protected static function get_login_session($token)
{
if (!$token) {
return false;
}
$data = get_transient(self::transient_key($token));
return is_array($data) ? $data : false;
}
/**
* This function clears the login session associated with a given token.
*/
protected static function clear_login_session($token)
{
if ($token) {
delete_transient(self::transient_key($token));
}
}
/**
* This function updates the login session associated with a given token.
*
* @param mixed $token
* @param mixed $data
*
* @return void
*/
protected static function update_login_session($token, $data)
{
if (!$token || !is_array($data)) {
return;
}
$created = isset($data['created']) ? (int) $data['created'] : time();
$elapsed = max(0, time() - $created);
$remaining = self::LOGIN_TOKEN_TTL - $elapsed;
if ($remaining <= 0) {
self::clear_login_session($token);
return;
}
set_transient(self::transient_key($token), $data, $remaining);
}
/**
* Build the transient key for a given raw token.
*
* @param string $token Raw random token value.
* @return string Transient key name.
*/
protected static function transient_key($token)
{
return self::LOGIN_TRANSIENT_PREFIX . $token;
}
/**
* Fetch and normalize the 2FA login token from either GET or POST (param: token).
* Performs both pattern-based filtering (through SucuriScanRequest) and an
* explicit secondary validation for defense-in-depth.
*
* @return string Normalized, validated token or empty string if invalid/absent.
*/
protected static function fetch_request_token()
{
$raw = SucuriScanRequest::getOrPost('token', self::LOGIN_TOKEN_PATTERN);
if ($raw === false) {
return '';
}
$token = (string) $raw;
$len = strlen($token);
if ($len < self::LOGIN_TOKEN_MIN_LENGTH || $len > self::LOGIN_TOKEN_MAX_LENGTH) {
return '';
}
if (preg_match('/^[A-Za-z0-9]{10,128}$/', $token) !== 1) {
return '';
}
return $token;
}
/**
* Bootstrap a 2FA session from request token.
* Redirects to wp_login_url() and exits on any invalid precondition.
*
* @param bool $require_secret If true, session must include a non-empty 'secret'.
*
* @return array Array with keys: token, session, user_id, redirect_to, remember, secret (may be '').
*/
protected static function bootstrap_session($require_secret = false)
{
$token = self::fetch_request_token();
if (empty($token)) {
wp_safe_redirect(wp_login_url());
exit;
}
$session = self::get_login_session($token);
if (!$session || empty($session['user_id'])) {
wp_safe_redirect(wp_login_url());
exit;
}
if ($require_secret && empty($session['secret'])) {
wp_safe_redirect(wp_login_url());
exit;
}
$user_id = (int) $session['user_id'];
$redirect_to = (string) (isset($session['redirect']) ? $session['redirect'] : admin_url());
$remember = (bool) (isset($session['remember']) ? $session['remember'] : false);
$secret = isset($session['secret']) ? (string) $session['secret'] : '';
return compact('token', 'session', 'user_id', 'redirect_to', 'remember', 'secret');
}
/**
* Enforce user-agent binding. If mismatch, clear session and redirect to login.
*
* @param array $session
* @param string $token
*
* @return void
*/
protected static function enforce_user_agent($session, $token)
{
$ua = isset($_SERVER['HTTP_USER_AGENT']) ? (string) $_SERVER['HTTP_USER_AGENT'] : '';
if (!empty($session['ua']) && $session['ua'] !== $ua) {
self::clear_login_session($token);
wp_safe_redirect(wp_login_url());
exit;
}
}
/**
* Extract and normalize a submitted numeric TOTP code from POST.
* Returns empty string if absent.
* @param string $field
*
* @return string
*/
protected static function extract_submitted_code($field)
{
$code_raw = SucuriScanRequest::post($field, '[0-9 ]+');
return $code_raw !== false ? preg_replace('/\D+/', '', (string) $code_raw) : '';
}
/**
* Record a failed attempt. If lockout threshold reached the session is cleared and user redirected.
* Otherwise session is updated with incremented attempts.
*
* @param string $token
* @param array $session (by reference)
*
* @return void (never returns on lockout)
*/
protected static function record_failed_attempt($token, &$session)
{
$session['attempts'] = isset($session['attempts']) ? ((int) $session['attempts'] + 1) : 1;
if ($session['attempts'] >= self::LOGIN_TOKEN_MAX_ATTEMPTS) {
self::clear_login_session($token);
wp_safe_redirect(wp_login_url());
exit;
}
self::update_login_session($token, $session);
}
/**
* Complete a successful standard verification login.
* Clears session, sets auth cookie and redirects.
*/
protected static function complete_success_login($user_id, $remember, $redirect_to, $token, $valid_ts)
{
if ($valid_ts) {
update_user_meta($user_id, self::LAST_SUCCESS_META_KEY, $valid_ts);
}
self::clear_login_session($token);
wp_set_current_user($user_id);
wp_set_auth_cookie($user_id, $remember);
wp_safe_redirect($redirect_to);
exit;
}
/**
* Handle successful setup flow: persist secret, update policy (selected_users) as needed,
* finalize authentication and redirect.
*/
protected static function process_successful_setup($user_id, $secret_key, $valid_ts, $remember, $redirect_to, $token)
{
self::store_user_totp_key($user_id, $secret_key);
if ($valid_ts) {
update_user_meta($user_id, self::LAST_SUCCESS_META_KEY, $valid_ts);
}
$current_mode = SucuriScanOption::getOption(':twofactor_mode');
if ($current_mode !== 'all_users') {
$list = SucuriScanOption::getOption(':twofactor_users');
$list = is_array($list) ? array_map('intval', $list) : array();
if (!in_array((int) $user_id, $list, true)) {
$list[] = (int) $user_id;
}
$list = array_values(array_unique(array_filter($list, function ($v) {
return $v > 0;
})));
SucuriScanOption::updateOption(':twofactor_users', $list);
SucuriScanOption::updateOption(':twofactor_mode', 'selected_users');
}
self::clear_login_session($token);
wp_set_current_user($user_id);
wp_set_auth_cookie($user_id, $remember);
wp_safe_redirect($redirect_to);
exit;
}
/*
* WordPress calls the 'authenticate' filter multiple times during the login pipeline.
*
* We only act when:
* - A previous authentication step has produced a concrete WP_User instance; AND
* - There is a non-empty username credential (protects against cookie/interim flows where username may be blank);
* - Two-Factor enforcement policy applies to this user.
*
* @param mixed $user
* @param mixed $username
* @param mixed $password
*
* @return mixed WP_User instance on success, WP_Error on failure, or original $user to pass through.
*/
public static function authenticate($user, $username, $password)
{
if ($user instanceof WP_Error) {
return $user;
}
// Security note: If username is empty we avoid triggering 2FA so we do not
// inadvertently enforce during non-standard auth flows (e.g., XML-RPC / cookie).
if (empty($username)) {
return $user;
}
if (!$user instanceof WP_User) {
return $user; // Not a fully authenticated user yet.
}
$user_id = (int) $user->ID;
if ($user_id <= 0) {
return $user;
}
if (!self::is_enforced_for_user($user_id)) {
return $user; // 2FA not required; allow core to continue normally.
}
$remember = (SucuriScanRequest::post('rememberme') !== false);
$redirect_raw = SucuriScanRequest::getOrPost('redirect_to');
$redirect_to = $redirect_raw !== false ? (string) $redirect_raw : admin_url();
$redirect_to = wp_validate_redirect($redirect_to, admin_url());
$secret_key = self::get_user_totp_key($user_id);
if (empty($secret_key)) {
try {
$setup_secret = SucuriScanTOTP::generate_key();
} catch (Exception $e) {
$setup_secret = '';
}
if (empty($setup_secret)) {
// Rare failure: generation failed; surface explicit error so login flow can show feedback.
return new WP_Error(
'sucuriscan_2fa_error',
esc_html__('Unable to initialize two-factor setup.', 'sucuri-scanner')
);
}
$token = self::create_login_token($user_id, $remember, $redirect_to, $setup_secret);
$setup_url = add_query_arg(
array(
'action' => 'sucuri-2fa-setup',
'token' => rawurlencode($token),
),
wp_login_url()
);
wp_safe_redirect($setup_url);
exit;
}
// Proceed to verification challenge screen.
$token = self::create_login_token($user_id, $remember, $redirect_to, '');
$verify_url = add_query_arg(
array(
'action' => 'sucuri-2fa',
'token' => rawurlencode($token),
),
wp_login_url()
);
wp_safe_redirect($verify_url);
exit;
}
public static function login_form_2fa()
{
$error = '';
$boot = self::bootstrap_session(false);
$token = $boot['token'];
$session = $boot['session'];
$user_id = $boot['user_id'];
$redirect_to = $boot['redirect_to'];
$remember = $boot['remember'];
$nonce_action = 'sucuri_2fa_verify_' . $token;
self::enforce_user_agent($session, $token);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
check_admin_referer($nonce_action);
$submitted_code = self::extract_submitted_code('sucuriscan_totp_code');
$invalid_message = esc_html__('Invalid two-factor authentication code.', 'sucuri-scanner');
if (strlen($submitted_code) !== SucuriScanTOTP::DEFAULT_DIGIT_COUNT) {
self::record_failed_attempt($token, $session);
$error = $invalid_message;
} else {
$secret_key = self::get_user_totp_key($user_id);
if (empty($secret_key)) {
self::clear_login_session($token);
wp_safe_redirect(add_query_arg(array('action' => 'sucuri-2fa-setup'), wp_login_url()));
exit;
}
$valid_ts = false;
try {
$valid_ts = SucuriScanTOTP::get_authcode_valid_ticktime($secret_key, $submitted_code);
} catch (Exception $e) {
$valid_ts = false;
}
if ($valid_ts) {
$last = (int) get_user_meta($user_id, self::LAST_SUCCESS_META_KEY, true);
if ($last && $last >= $valid_ts) {
$valid_ts = false;
}
}
if ($valid_ts) {
self::complete_success_login($user_id, $remember, $redirect_to, $token, $valid_ts);
}
self::record_failed_attempt($token, $session);
$error = $invalid_message;
}
}
$message_html = SucuriScanTemplate::getSnippet('login-message', array(
'Message' => esc_html__('Enter the 6-digit code from your authenticator app to continue.', 'sucuri-scanner'),
));
if (!empty($error)) {
$message_html = SucuriScanTemplate::getSnippet('login-error', array(
'Error' => esc_html($error),
)) . $message_html;
}
login_header(esc_html__('Two-Factor Authentication', 'sucuri-scanner'), $message_html);
$params = array(
'ActionURL' => add_query_arg(array('action' => 'sucuri-2fa', 'token' => rawurlencode($token)), wp_login_url()),
'NonceField' => wp_nonce_field($nonce_action, '_wpnonce', true, false),
);
echo SucuriScanTemplate::getSection('login-2fa', $params);
login_footer();
exit;
}
/*
* Render the 2FA setup form.
*
* @return void
*/
public static function login_form_2fa_setup()
{
$error = '';
$boot = self::bootstrap_session(true);
$token = $boot['token'];
$session = $boot['session'];
$user_id = $boot['user_id'];
$redirect_to = $boot['redirect_to'];
$remember = $boot['remember'];
$secret_key = $boot['secret'];
$nonce_action = 'sucuri_2fa_setup_' . $token;
self::enforce_user_agent($session, $token);
$user = get_user_by('id', $user_id);
$otpauth = SucuriScanTOTP::generate_qr_code_url($user, $secret_key);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
check_admin_referer($nonce_action);
$submitted_code = self::extract_submitted_code('sucuriscan_totp_code');
$invalid_message = esc_html__('Invalid code. Make sure you scanned the QR and your device time is correct.', 'sucuri-scanner');
if (strlen($submitted_code) !== SucuriScanTOTP::DEFAULT_DIGIT_COUNT) {
self::record_failed_attempt($token, $session);
$error = $invalid_message;
} else {
$valid_ts = false;
try {
$valid_ts = SucuriScanTOTP::get_authcode_valid_ticktime($secret_key, $submitted_code);
} catch (Exception $e) {
$valid_ts = false;
}
if ($valid_ts) {
self::process_successful_setup($user_id, $secret_key, $valid_ts, $remember, $redirect_to, $token);
}
self::record_failed_attempt($token, $session);
$error = $invalid_message;
}
}
$message_html = SucuriScanTemplate::getSnippet('login-message', array(
'Message' => esc_html__('Set up two-factor authentication. Scan the QR code with your authenticator app, then enter the 6-digit code to continue.', 'sucuri-scanner'),
));
if (!empty($error)) {
$message_html = SucuriScanTemplate::getSnippet('login-error', array(
'Error' => esc_html($error),
)) . $message_html;
}
login_header(esc_html__('Set up Two-Factor Authentication', 'sucuri-scanner'), $message_html);
$params = array(
'ActionURL' => add_query_arg(array('action' => 'sucuri-2fa-setup', 'token' => rawurlencode($token)), wp_login_url()),
'NonceField' => wp_nonce_field($nonce_action, '_wpnonce', true, false),
'SecretManual' => $secret_key,
'OtpauthURI' => $otpauth,
);
echo SucuriScanTemplate::getSection('login-2fa-setup', $params);
login_footer();
exit;
}
/**
* Brand the 2FA login and setup pages with the Sucuri logo.
* Only applies to the 2FA-specific actions.
*
* @return void
*/
public static function brand_login_logo()
{
$action_raw = SucuriScanRequest::getOrPost('action', '[a-z0-9\-_]+');
$action = $action_raw !== false ? (string) $action_raw : '';
if ($action !== 'sucuri-2fa' && $action !== 'sucuri-2fa-setup') {
return;
}
self::ensure_qr_script();
$logo = trailingslashit(SUCURISCAN_URL) . 'inc/images/pluginlogo.png';
echo SucuriScanTemplate::getSnippet('login-brand', array(
'LogoURL' => esc_url($logo),
));
}
/**
* Ensure the QR code script for 2FA (qr.js) is registered and enqueued once.
*
* @return void
*/
protected static function ensure_qr_script()
{
if (!function_exists('wp_script_is') || !function_exists('wp_register_script') || !function_exists('wp_enqueue_script')) {
return;
}
if (!wp_script_is('sucuriscan-qrcode', 'registered')) {
wp_register_script(
'sucuriscan-qrcode',
trailingslashit(SUCURISCAN_URL) . 'inc/js/qr.js',
array(),
method_exists('SucuriScan', 'fileVersion') ? SucuriScan::fileVersion('inc/js/qr.js') : false
);
}
if (!wp_script_is('sucuriscan-qrcode', 'enqueued')) {
wp_enqueue_script('sucuriscan-qrcode');
}
}
/**
* Retrieve the stored TOTP secret key for a given user.
*/
public static function get_user_totp_key($user_id)
{
return (string) get_user_meta($user_id, self::SECRET_META_KEY, true);
}
/**
* Store or update the TOTP secret key for a given user.
*/
public static function store_user_totp_key($user_id, $key)
{
$existingKey = self::get_user_totp_key($user_id);
if (empty($existingKey)) {
return (bool) add_user_meta($user_id, self::SECRET_META_KEY, $key);
}
return (bool) update_user_meta($user_id, self::SECRET_META_KEY, $key);
}
/**
* Resolve and authorize the target user for profile AJAX endpoints.
* Sends json error & exits on failure; always returns an array on success.
*
* @return array [current => int, target => int, is_self => bool]
*/
protected static function resolve_ajax_target_user()
{
$current = get_current_user_id();
if (!$current || $current <= 0) {
wp_send_json_error(array('message' => 'Forbidden'), 403);
}
$raw_user = SucuriScanRequest::post('user_id', '[0-9]+');
if ($raw_user === false || $raw_user === '') {
$user_id = $current;
} else {
$user_id = (int) $raw_user;
if ($user_id <= 0) {
wp_send_json_error(array('message' => 'Invalid user.'), 400);
}
}
$is_self = ($current === $user_id);
$provided_nonce = SucuriScanRequest::post('nonce');
$nonce_ok = false;
if ($provided_nonce !== '') {
if (wp_verify_nonce($provided_nonce, 'sucuri_profile_2fa_' . $user_id)) {
$nonce_ok = true;
} elseif ($is_self && wp_verify_nonce($provided_nonce, 'sucuri_profile_2fa')) {
$nonce_ok = true;
}
}
if (!$nonce_ok) {
wp_send_json_error(array('message' => 'Invalid security token.'), 403);
}
if (!$is_self && !current_user_can('edit_users')) {
wp_send_json_error(array('message' => 'Not allowed'), 403);
}
if (!self::is_enforced_for_user($user_id)) {
wp_send_json_error(array('message' => 'Two-Factor not enforced for this user'), 400);
}
return array('current' => $current, 'target' => $user_id, 'is_self' => $is_self);
}
/**
* Generate a new setup key + otpauth URL for a user; returns array(key, otpauth) or empty array on failure.
*
* @param int $user_id
*
* @return array
*/
protected static function generate_setup_key_and_otpauth($user_id)
{
$key = '';
try {
$key = SucuriScanTOTP::generate_key();
} catch (Exception $e) {
$key = '';
}
if ($key === '') {
return array();
}
$user = get_user_by('id', $user_id);
if (!$user) {
return array();
}
$otpauth = SucuriScanTOTP::generate_qr_code_url($user, $key);
return array($key, $otpauth);
}
/**
* Build status snippet HTML (enabled state actions) for a user.
*
* @param int $user_id
*
* @return string
*/
protected static function profile_status_snippet($user_id)
{
return SucuriScanTemplate::getSnippet('profile-2fa-status', array(
'ajax_url' => admin_url('admin-ajax.php'),
'ajax_nonce' => wp_create_nonce('sucuri_profile_2fa_' . (int) $user_id),
'user_id' => (int) $user_id,
));
}
/**
* Build setup snippet HTML for a user (expects pre-generated key + otpauth).
*
* @param int $user_id
* @param string $key
* @param string $otpauth
*
* @return string
*/
protected static function profile_setup_snippet($user_id, $key, $otpauth)
{
return SucuriScanTemplate::getSnippet('profile-2fa-setup', array(
'totp_key' => $key,
'topt_url' => $otpauth,
'ajax_url' => admin_url('admin-ajax.php'),
'ajax_nonce' => wp_create_nonce('sucuri_profile_2fa_' . (int) $user_id),
'user_id' => (int) $user_id,
));
}
/**
* Internal generic TOTP verification utility (non-AJAX). Returns array with:
* [ 'valid' => bool, 'valid_ts' => int|0, 'error' => string ]
*
* Performs:
* - Code length check
* - Secret format validation
* - TOTP computation + replay prevention (LAST_SUCCESS_META_KEY)
*
* Does NOT throw/exit; caller decides how to surface errors.
*
* @param int $user_id
* @param string $secret
* @param string $code
*
* @return array
*/
protected static function verify_totp_code($user_id, $secret, $code)
{
$user_id = (int) $user_id;
$result = array('valid' => false, 'valid_ts' => 0, 'error' => '');
if (strlen($code) !== SucuriScanTOTP::DEFAULT_DIGIT_COUNT) {
$result['error'] = __('Please enter the 6-digit verification code.', 'sucuri-scanner');
return $result;
}
if (empty($secret) || !SucuriScanTOTP::is_valid_key($secret)) {
$result['error'] = __('Invalid secret. Reload the page and try again.', 'sucuri-scanner');
return $result;
}
$valid_ts = false;
try {
$valid_ts = SucuriScanTOTP::get_authcode_valid_ticktime($secret, $code);
} catch (Exception $e) {
$valid_ts = false;
}
if ($valid_ts) {
$last = (int) get_user_meta($user_id, self::LAST_SUCCESS_META_KEY, true);
if ($last && $last >= $valid_ts) {
// Replay / stale timestep
$valid_ts = false;
}
}
if (!$valid_ts) {
$result['error'] = __('Incorrect code. Check your authenticator app and device time.', 'sucuri-scanner');
return $result;
}
$result['valid'] = true;
$result['valid_ts'] = (int) $valid_ts;
return $result;
}
/**
* Validate secret & code and return valid tick timestamp or send JSON error.
* Performs length & format validation plus replay prevention.
*
* @param int $user_id
* @param string $secret
* @param string $code
* @return int $valid_ts
*/
protected static function validate_secret_and_code_or_error($user_id, $secret, $code)
{
if (strlen($code) !== SucuriScanTOTP::DEFAULT_DIGIT_COUNT) {
wp_send_json_error(array('message' => __('Please enter the 6-digit verification code.', 'sucuri-scanner')));
}
if (empty($secret) || !SucuriScanTOTP::is_valid_key($secret)) {
wp_send_json_error(array('message' => __('Invalid secret.', 'sucuri-scanner')));
}
$valid_ts = false;
try {
$valid_ts = SucuriScanTOTP::get_authcode_valid_ticktime($secret, $code);
} catch (Exception $e) {
wp_send_json_error(array('message' => __('Verification failed.', 'sucuri-scanner')));
}
if ($valid_ts) {
$last = (int) get_user_meta($user_id, self::LAST_SUCCESS_META_KEY, true);
if ($last && $last >= $valid_ts) {
$valid_ts = false; // Replay within same or older window.
}
}
if (!$valid_ts) {
wp_send_json_error(array('message' => __('Incorrect code. Check your authenticator app and device time.', 'sucuri-scanner')));
}
return (int) $valid_ts;
}
/**
* This function saves the user's 2FA settings when their profile is updated.
*
* @param WP_User $user
*
* @return void
*/
public static function render_user_profile_section($user)
{
if (!($user instanceof WP_User)) {
return;
}
$current_id = get_current_user_id();
$is_self = ((int) $user->ID === (int) $current_id);
$can_manage_users = current_user_can('edit_users');
if (!self::is_enforced_for_user((int) $user->ID)) {
return;
}
$existing = self::get_user_totp_key((int) $user->ID);
$enabled = !empty($existing);
$status_html = $enabled
? '<span class="dashicons dashicons-yes" style="color:#46b450"></span> ' . esc_html__('Enabled', 'sucuri-scanner')
: '<span class="dashicons dashicons-dismiss" style="color:#dc3232"></span> ' . esc_html__('Disabled', 'sucuri-scanner');
wp_nonce_field('sucuri_2fa_profile_action', 'sucuri_2fa_profile_nonce');
$uid = (int) $user->ID;
if ($enabled) {
$actions_html = self::profile_status_snippet($uid);
} else {
$actions_html = '';
if ($is_self) {
list($key, $otpauth) = array('', '');
$data = self::generate_setup_key_and_otpauth($uid);
if (!empty($data)) {
list($key, $otpauth) = $data;
$actions_html = self::profile_setup_snippet($uid, $key, $otpauth);
}
} elseif ($can_manage_users) {
$actions_html = '<p class="description">' . esc_html__('Two-Factor is not enabled for this user. Ask the user to enable it from their own Profile page.', 'sucuri-scanner') . '</p>';
}
}
echo SucuriScanTemplate::getSection('profile-2fa-section', array(
'StatusHTML' => $status_html,
'ActionsHTML' => $actions_html,
));
}
/**
* AJAX handler to enable 2FA for a user.
* Expects POST with: user_id (int, optional), code (string, required), secret (string, required), nonce (string, required).
*
* @return void (sends JSON response and exits)
*/
public static function ajax_profile_enable()
{
if (!is_user_logged_in()) {
wp_send_json_error(array('message' => 'Forbidden'), 403);
}
$resolved = self::resolve_ajax_target_user();
$user_id = $resolved['target'];
$is_self = $resolved['is_self'];
if (!$is_self) {
wp_send_json_error(array('message' => __('You can only enable two-factor for your own account.', 'sucuri-scanner')), 403);
}
$code_raw = SucuriScanRequest::post('code', '[0-9 ]+');
$code = $code_raw !== false ? preg_replace('/\D+/', '', $code_raw) : '';
$secret = (string) SucuriScanRequest::post('secret', '[A-Za-z0-9=]+');
$valid_ts = self::validate_secret_and_code_or_error($user_id, $secret, $code);
self::store_user_totp_key($user_id, $secret);
update_user_meta($user_id, self::LAST_SUCCESS_META_KEY, $valid_ts);
if (class_exists('SucuriScanEvent')) {
SucuriScanEvent::reportInfoEvent('Two-factor authentication enabled for user ID ' . (int) $user_id);
}
$html = self::profile_status_snippet($user_id);
wp_send_json_success(array('html' => $html));
}
/**
* AJAX handler to reset (disable) 2FA for a user.
* Expects POST with: user_id (int, optional), nonce (string, required).
*
* @return void (sends JSON response and exits)
*/
public static function ajax_profile_reset()
{
if (!is_user_logged_in()) {
wp_send_json_error(array('message' => 'Forbidden'), 403);
}
$resolved = self::resolve_ajax_target_user();
$user_id = $resolved['target'];
$is_self = $resolved['is_self'];
delete_user_meta($user_id, self::SECRET_META_KEY);
delete_user_meta($user_id, self::LAST_SUCCESS_META_KEY);
if (class_exists('SucuriScanEvent')) {
SucuriScanEvent::reportInfoEvent('Two-factor authentication reset for user ID ' . (int) $user_id);
}
$html = '';
if ($is_self) {
$data = self::generate_setup_key_and_otpauth($user_id);
if (!empty($data)) {
list($key, $otpauth) = $data;
$html = self::profile_setup_snippet($user_id, $key, $otpauth);
}
}
if ($html === '') {
$html = SucuriScanTemplate::getSnippet('profile-2fa-disabled', array());
}
wp_send_json_success(array('html' => $html));
}
/**
* Process and save 2FA settings when a user profile is updated.
*
* @param int $user_id
*
* @return void
*/
public static function save_user_profile_section($user_id)
{
$user_id = (int) $user_id;
if ($user_id <= 0) {
return;
}
$profile_nonce = SucuriScanRequest::post('sucuri_2fa_profile_nonce', '_nonce');
if (!$profile_nonce || !wp_verify_nonce($profile_nonce, 'sucuri_2fa_profile_action')) {
return;
}
$action_raw = SucuriScanRequest::post('sucuri_2fa_action', '[a-z_]+');
$action = $action_raw !== false ? sanitize_text_field((string) $action_raw) : '';
if ($action !== 'enable' && $action !== 'reset') {
return;
}
$current_id = get_current_user_id();
$is_self = ($current_id && (int) $current_id === $user_id);
if ($action === 'enable') {
if (!$is_self) {
return;
}
if (!self::is_enforced_for_user($user_id)) {
self::add_profile_error('sucuri_2fa_policy', esc_html__('Two-Factor is not enforced for your account.', 'sucuri-scanner'));
return;
}
$existing = self::get_user_totp_key($user_id);
if (!empty($existing)) {
self::add_profile_error('sucuri_2fa_already', esc_html__('Two-Factor is already enabled.', 'sucuri-scanner'));
return;
}
$code = SucuriScanRequest::post('sucuriscan_totp_code', '[0-9 ]+');
$secret = SucuriScanRequest::post('sucuri_2fa_secret', '[A-Za-z0-9=]+');
$code = $code ? preg_replace('/\D+/', '', $code) : '';
$secret = $secret ? (string) $secret : '';
$verified = self::verify_totp_code($user_id, $secret, $code);
if (!$verified['valid']) {
self::add_profile_error('sucuri_2fa_code', esc_html($verified['error']));
return;
}
self::store_user_totp_key($user_id, $secret);
update_user_meta($user_id, self::LAST_SUCCESS_META_KEY, $verified['valid_ts']);
if (class_exists('SucuriScanEvent')) {
SucuriScanEvent::reportInfoEvent('Two-factor authentication enabled for user ID ' . (int) $user_id);
}
return;
}
if ($action === 'reset') {
if (!$is_self && !current_user_can('edit_users')) {
return; // Capability required to reset others.
}
if (!self::is_enforced_for_user($user_id)) {
// Silently ignore: not enforced.
return;
}
delete_user_meta($user_id, self::SECRET_META_KEY);
delete_user_meta($user_id, self::LAST_SUCCESS_META_KEY);
if (class_exists('SucuriScanEvent')) {
SucuriScanEvent::reportInfoEvent('Two-factor authentication reset for user ID ' . (int) $user_id);
}
return;
}
}
/**
* Render the 2FA setup block for the current user via AJAX.
*
* @return string HTML (error message if not logged in or other failure).
*/
public static function topt()
{
if (!SucuriScanInterface::checkNonce()) {
return SucuriScanInterface::error(__('Incorrect nonce.', 'sucuri-scanner'));
}
$user = wp_get_current_user();
if (!$user->ID) {
return SucuriScanInterface::error(__('Incorrect user.', 'sucuri-scanner'));
}
$existing = self::get_user_totp_key((int) $user->ID);
if (!empty($existing)) {
return SucuriScanTemplate::getSnippet('2fa-current-user-status', array(
'Message' => __('Two-Factor Authentication is already enabled for your account.', 'sucuri-scanner'),
));
}
$data = self::generate_setup_key_and_otpauth((int) $user->ID);
if (empty($data)) {
return SucuriScanInterface::error(__('Unable to generate secret.', 'sucuri-scanner'));
}
list($key, $otpauth) = $data;
$params = array(
'totp_key' => $key,
'topt_url' => $otpauth,
'2FA.Status' => false,
'SecretManual' => $key,
);
return SucuriScanTemplate::getSnippet('2fa-setup', $params);
}
/**
* Render the 2FA block for the current user profile page.
*
* @return string HTML (error message if not logged in or other failure).
*/
public static function current_user_block()
{
$user = wp_get_current_user();
if (!$user || !$user->ID) {
return SucuriScanInterface::error(__('Incorrect user.', 'sucuri-scanner'));
}
$existing = self::get_user_totp_key((int) $user->ID);
if (!empty($existing)) {
return SucuriScanTemplate::getSnippet('2fa-current-user-status', array(
'Message' => __('Two-Factor Authentication is enabled for your account.', 'sucuri-scanner'),
));
}
$data = self::generate_setup_key_and_otpauth((int) $user->ID);
if (empty($data)) {
return SucuriScanInterface::error(__('Unable to generate secret.', 'sucuri-scanner'));
}
list($key, $otpauth) = $data;
return SucuriScanTemplate::getSnippet('2fa-setup', array(
'totp_key' => $key,
'topt_url' => $otpauth,
'SecretManual' => $key
));
}
/**
* Render the 2FA users admin section (list of users with status and bulk actions).
* Only shown if current user can 'list_users'.
*
* @return string HTML (empty if no permission)
*/
public static function users_admin_section()
{
if (!current_user_can('list_users')) {
return '';
}
$rows = '';
$users = get_users(array('fields' => array('ID', 'user_login', 'user_email', 'roles')));
$total_users = is_array($users) ? count($users) : 0;
$activated_count = 0;
if (is_array($users)) {
foreach ($users as $user) {
$uid = (int) $user->ID;
$secret = self::get_user_totp_key($uid);
$status = empty($secret) ? __('Deactivated', 'sucuri-scanner') : __('Activated', 'sucuri-scanner');
if (!empty($secret)) {
$activated_count++;
}
$rows .= SucuriScanTemplate::getSnippet('2fa-user-row', array(
'ID' => $uid,
'Login' => $user->user_login,
'Email' => $user->user_email,
'Status' => $status,
));
}
}
$bulkOptions = '';
$bulkMap = array(
'activate_all' => __('Activate two factor for all users', 'sucuri-scanner'),
'activate_selected' => __('Activate two factor for selected users', 'sucuri-scanner'),
'deactivate_all' => __('Deactivate two factor for all users', 'sucuri-scanner'),
'deactivate_selected' => __('Deactivate two factor for selected users', 'sucuri-scanner'),
'reset_selected' => __('Reset two factor for selected users (keep enforcement)', 'sucuri-scanner'),
'reset_all' => __('Reset two factor for all users (keep enforcement)', 'sucuri-scanner'),
'reset_everything' => __('Delete all two-factor data and disable enforcement', 'sucuri-scanner'),
);
foreach ($bulkMap as $val => $label) {
$bulkOptions .= sprintf('<option value="%s">%s</option>', esc_attr($val), esc_html($label));
}
$status_id = 0;
$status_text = __('Deactivated', 'sucuri-scanner');
if ($activated_count > 0) {
if ($total_users > 0 && $activated_count >= $total_users) {
$status_id = 1;
$status_text = __('Activated for all users', 'sucuri-scanner');
} else {
$status_id = 2;
$status_text = __('Activated for some users', 'sucuri-scanner');
}
}
return SucuriScanTemplate::getSection('2fa-users', array(
'Rows' => $rows,
'BulkOptions' => $bulkOptions,
'TwoFactor.Status' => (string) $status_id,
'TwoFactor.StatusText' => $status_text,
));
}
/**
* Retrieve all valid user IDs (>0).
*
* @return int[]
*/
protected static function get_all_user_ids()
{
$users = get_users(array('fields' => array('ID')));
if (!is_array($users)) {
return array();
}
$ids = array();
foreach ($users as $user) {
if (is_object($user) && isset($user->ID)) {
$id = (int) $user->ID;
if ($id > 0) {
$ids[$id] = true;
}
} elseif (is_array($user) && isset($user['ID'])) {
$id = (int) $user['ID'];
if ($id > 0) {
$ids[$id] = true;
}
}
}
return array_keys($ids);
}
/**
* Normalize an array of user IDs, optionally intersect with full set.
* Returns a deduplicated, sorted list.
*
* @param array $ids
* @param array $universe Optional array of allowed IDs; if provided we intersect.
*
* @return int[]
*/
protected static function normalize_user_ids($ids, $universe = null)
{
if (!is_array($ids)) {
return array();
}
$output = array();
foreach ($ids as $id) {
$id = (int) $id;
if ($id > 0) {
$output[$id] = true;
}
}
$output = array_keys($output);
if (is_array($universe)) {
$allowed = array();
$flip = array();
foreach ($universe as $uid) {
$flip[(int) $uid] = true;
}
foreach ($output as $uid) {
if (isset($flip[$uid])) {
$allowed[] = $uid;
}
}
$output = $allowed;
}
sort($output, SORT_NUMERIC);
return $output;
}
/**
* Bulk administrative action handler for Two-Factor policy management.
*
* Supported actions:
* - activate_all
* - deactivate_all
* - activate_selected
* - deactivate_selected
* - reset_selected
* - reset_all
*
* Returns structured response array for easier testing & UI handling:
* [ success => bool, code => string, message => string, affected => array, mode => string ]
*
* Capability (manage_options) is enforced here for defense-in-depth.
*
* @param string $action
* @param array $selected User IDs (untrusted input)
*
* @return array
*/
public static function process_admin_bulk_action($action, $selected)
{
$result = array(
'success' => false,
'code' => 'invalid',
'message' => '',
'affected' => array(),
'mode' => (string) SucuriScanOption::getOption(':twofactor_mode'),
);
if (!is_admin() || !current_user_can('manage_options')) {
$result['message'] = __('You are not allowed to modify Two-Factor settings.', 'sucuri-scanner');
return $result;
}
$action = is_string($action) ? trim($action) : '';
$all_ids = self::get_all_user_ids();
$selected = self::normalize_user_ids($selected, $all_ids);
$current_mode = (string) SucuriScanOption::getOption(':twofactor_mode');
switch ($action) {
case 'activate_all':
SucuriScanOption::updateOption(':twofactor_mode', 'all_users');
SucuriScanOption::updateOption(':twofactor_users', array());
$result['success'] = true;
$result['code'] = 'activate_all';
$result['message'] = __('Two-Factor enforced for all users.', 'sucuri-scanner');
$result['mode'] = 'all_users';
$result['affected'] = $all_ids;
break;
case 'deactivate_all':
SucuriScanOption::updateOption(':twofactor_mode', 'disabled');
SucuriScanOption::updateOption(':twofactor_users', array());
$result['success'] = true;
$result['code'] = 'deactivate_all';
$result['message'] = __('Two-Factor deactivated for all users.', 'sucuri-scanner');
$result['mode'] = 'disabled';
$result['affected'] = $all_ids;
break;
case 'activate_selected':
if (empty($selected)) {
$result['message'] = __('No users selected.', 'sucuri-scanner');
break;
}
$list = SucuriScanOption::getOption(':twofactor_users');
$list = is_array($list) ? array_map('intval', $list) : array();
$list = self::normalize_user_ids($list);
$merged = self::normalize_user_ids(array_merge($list, $selected), $all_ids);
SucuriScanOption::updateOption(':twofactor_mode', 'selected_users');
SucuriScanOption::updateOption(':twofactor_users', $merged);
$result['success'] = true;
$result['code'] = 'activate_selected';
$result['message'] = __('Two-Factor enforced for selected users.', 'sucuri-scanner');
$result['mode'] = 'selected_users';
$result['affected'] = $selected;
break;
case 'deactivate_selected':
if (empty($selected)) {
$result['message'] = __('No users selected.', 'sucuri-scanner');
break;
}
if ($current_mode === 'all_users') {
$remaining = array_values(array_diff($all_ids, $selected));
if (empty($remaining)) {
SucuriScanOption::updateOption(':twofactor_mode', 'disabled');
SucuriScanOption::updateOption(':twofactor_users', array());
$result['mode'] = 'disabled';
} else {
SucuriScanOption::updateOption(':twofactor_mode', 'selected_users');
SucuriScanOption::updateOption(':twofactor_users', $remaining);
$result['mode'] = 'selected_users';
}
} else {
$list = SucuriScanOption::getOption(':twofactor_users');
$list = is_array($list) ? array_map('intval', $list) : array();
$list = self::normalize_user_ids($list, $all_ids);
$remaining = array_values(array_diff($list, $selected));
if (empty($remaining)) {
SucuriScanOption::updateOption(':twofactor_mode', 'disabled');
SucuriScanOption::updateOption(':twofactor_users', array());
$result['mode'] = 'disabled';
} else {
SucuriScanOption::updateOption(':twofactor_mode', 'selected_users');
SucuriScanOption::updateOption(':twofactor_users', $remaining);
$result['mode'] = 'selected_users';
}
}
$result['success'] = true;
$result['code'] = 'deactivate_selected';
$result['message'] = __('Two-Factor deactivated for selected users.', 'sucuri-scanner');
$result['affected'] = $selected;
break;
case 'reset_selected':
if (empty($selected)) {
$result['message'] = __('No users selected.', 'sucuri-scanner');
break;
}
foreach ($selected as $uid) {
delete_user_meta($uid, self::SECRET_META_KEY);
delete_user_meta($uid, self::LAST_SUCCESS_META_KEY);
}
$result['success'] = true;
$result['code'] = 'reset_selected';
$result['message'] = __('Two-Factor settings reset for selected users.', 'sucuri-scanner');
$result['affected'] = $selected;
$result['mode'] = (string) SucuriScanOption::getOption(':twofactor_mode');
break;
case 'reset_all':
foreach ($all_ids as $uid) {
delete_user_meta($uid, self::SECRET_META_KEY);
delete_user_meta($uid, self::LAST_SUCCESS_META_KEY);
}
$result['success'] = true;
$result['code'] = 'reset_all';
$result['message'] = __('Two-Factor settings reset for all users.', 'sucuri-scanner');
$result['affected'] = $all_ids;
$result['mode'] = (string) SucuriScanOption::getOption(':twofactor_mode');
break;
case 'reset_everything':
foreach ($all_ids as $uid) {
delete_user_meta($uid, self::SECRET_META_KEY);
delete_user_meta($uid, self::LAST_SUCCESS_META_KEY);
}
SucuriScanOption::updateOption(':twofactor_mode', 'disabled');
SucuriScanOption::updateOption(':twofactor_users', array());
$result['success'] = true;
$result['code'] = 'reset_everything';
$result['message'] = __('All Two-Factor data deleted and enforcement disabled.', 'sucuri-scanner');
$result['affected'] = $all_ids;
$result['mode'] = 'disabled';
break;
default:
$result['message'] = __('Invalid two-factor action selected.', 'sucuri-scanner');
break;
}
return $result;
}
/**
* AJAX handler to verify and enable 2FA for the current user.
* Expects POST with: form_action=totp_verify, topt_code (string, required),
* topt_key (string, required if no existing key), enforce_all (0|1, optional),
* _wpnonce (string, required).
*
* @return void (sends JSON response and exits)
*/
public static function totp_verify()
{
if (SucuriScanRequest::post('form_action') !== 'totp_verify') {
return;
}
if (!SucuriScanInterface::checkNonce()) {
return SucuriScanInterface::error(__('Incorrect nonce.', 'sucuri-scanner'));
}
$user = wp_get_current_user();
if (!$user || !$user->ID) {
return SucuriScanInterface::error(__('Incorrect user.', 'sucuri-scanner'));
}
$user_id = (int) $user->ID;
$existingKey = self::get_user_totp_key($user_id);
$code_raw = SucuriScanRequest::post('topt_code', '[0-9 ]+');
$code = $code_raw ? preg_replace('/\D+/', '', $code_raw) : '';
$secret_input = SucuriScanRequest::post('topt_key', '[A-Za-z0-9=]+');
$secret_input = $secret_input ? (string) $secret_input : '';
$secret = !empty($existingKey) ? $existingKey : $secret_input;
$verified = self::verify_totp_code($user_id, $secret, $code);
if (!$verified['valid']) {
wp_send_json(array('data' => '', 'error' => $verified['error']), 200);
}
self::store_user_totp_key($user_id, $secret);
update_user_meta($user_id, self::LAST_SUCCESS_META_KEY, $verified['valid_ts']);
$enforce_all = false;
$enforce_all_raw = SucuriScanRequest::post('enforce_all', '[01]');
if (current_user_can('manage_options')) {
$enforce_all = ($enforce_all_raw !== false) && ((string) $enforce_all_raw === '1');
}
if ($enforce_all) {
SucuriScanOption::updateOption(':twofactor_mode', 'all_users');
SucuriScanOption::updateOption(':twofactor_users', array());
} else {
$list = SucuriScanOption::getOption(':twofactor_users');
$list = is_array($list) ? array_map('intval', $list) : array();
if (!in_array($user_id, $list, true)) {
$list[] = $user_id;
}
$list = array_values(array_unique(array_filter($list, function ($v) {
return $v > 0;
})));
SucuriScanOption::updateOption(':twofactor_users', $list);
SucuriScanOption::updateOption(':twofactor_mode', 'selected_users');
}
wp_send_json(array('data' => 'activated', 'error' => ''), 200);
}
}