['ID']]); foreach ($users as $user) { $old_trusted_devices = get_user_meta($user->ID, 'wrs_sc_trusted_devices', true); if ($old_trusted_devices) { update_user_meta($user->ID, 'scbr_trusted_devices', $old_trusted_devices); delete_user_meta($user->ID, 'wrs_sc_trusted_devices'); } $old_siren_muted = get_user_meta($user->ID, 'wrs_sc_siren_muted', true); if ($old_siren_muted) { update_user_meta($user->ID, 'scbr_siren_muted', $old_siren_muted); delete_user_meta($user->ID, 'wrs_sc_siren_muted'); } } $opts = get_option($this->opt_key); if (!$opts) { try { $this->maybe_bootstrap_options(); $opts = get_option($this->opt_key); } catch (Exception $e) { // Log error silently - don't show admin notice to avoid annoying users } } if (!empty($opts) && empty($opts['master_admin_id'])) { $opts['needs_master_admin'] = 1; update_option($this->opt_key, $opts); } // Initialize blocked users option if (!get_option($this->blocked_users_key)) { update_option($this->blocked_users_key, []); } } public function maybe_bootstrap_options() { $defaults = [ 'master_admin_id' => 0, 'needs_master_admin' => 1, 'status' => 'off', // NOTE: the stop password is NEVER stored in plaintext. Only its // hash (via wp_hash_password) is kept in siren_password_hash. 'siren_password_hash' => '', 'custom_audio_url' => '', 'features' => [ 'siren_enabled' => 1, 'email_alerts' => 1, 'overlay_enabled' => 1, 'broadcast_siren' => 1, ], 'overlay_text' => 'Security Verification: Please enter the Siren Stop Password to continue.', ]; $existing = get_option($this->opt_key); if (!$existing) { $defaults['siren_password_hash'] = wp_hash_password('2210'); update_option($this->opt_key, $defaults); return; } // One-time migration: hash any legacy plaintext password, then drop it. if (empty($existing['siren_password_hash']) && !empty($existing['siren_password'])) { $existing['siren_password_hash'] = wp_hash_password($existing['siren_password']); unset($existing['siren_password']); update_option($this->opt_key, $existing); } elseif (!empty($existing['siren_password'])) { unset($existing['siren_password']); update_option($this->opt_key, $existing); } if (empty($existing['siren_password_hash'])) { $existing['siren_password_hash'] = wp_hash_password('2210'); update_option($this->opt_key, $existing); } if (!isset($existing['custom_audio_url'])) { $existing['custom_audio_url'] = ''; update_option($this->opt_key, $existing); } } /* ---------------------------- Utilities ---------------------------- */ private function get_opts() { return get_option($this->opt_key, []); } private function is_master_admin($user_id = null) { $user_id = $user_id ?: get_current_user_id(); $opts = $this->get_opts(); return !empty($opts['master_admin_id']) && intval($opts['master_admin_id']) === intval($user_id); } private function admins_list() { return get_users([ 'role__in' => ['administrator', 'editor', 'author'], 'fields' => ['ID', 'user_login', 'user_email'] ]); } private function current_device_id() { return isset($_COOKIE[$this->cookie]) ? sanitize_text_field(wp_unslash($_COOKIE[$this->cookie])) : ''; } private function trusted_devices($user_id) { $list = get_user_meta($user_id, 'scbr_trusted_devices', true); return is_array($list) ? $list : []; } private function trust_device($user_id, $device_id) { $list = $this->trusted_devices($user_id); if ($device_id && !in_array($device_id, $list, true)) { $list[] = $device_id; update_user_meta($user_id, 'scbr_trusted_devices', $list); } } private function remove_device($user_id, $device_id) { $list = $this->trusted_devices($user_id); if (($key = array_search($device_id, $list, true)) !== false) { unset($list[$key]); update_user_meta($user_id, 'scbr_trusted_devices', array_values($list)); } } private function broadcast_new_device_event($by_user_id, $device_id) { $token = $this->generate_uuid(); set_transient($this->transient_broadcast, [ 'token' => $token, 'user' => $by_user_id, 'device_id' => $device_id, 'ip' => $this->ip_address(), 'time' => time(), ], 15 * MINUTE_IN_SECONDS); } private function admin_emails_except($exclude_user_id) { $emails = []; foreach ($this->admins_list() as $u) { if (intval($u->ID) !== intval($exclude_user_id) && is_email($u->user_email)) { $emails[] = $u->user_email; } } return $emails; } private function ip_address() { // Only REMOTE_ADDR is trusted. X-Forwarded-For / Client-IP headers are // client-controlled and would let an attacker rotate IPs to evade the // attempt limiter, so they are deliberately ignored. $ip = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : ''; if ($ip && filter_var($ip, FILTER_VALIDATE_IP)) { return $ip; } return 'unknown'; } private function is_ip_blocked($ip) { return get_transient($this->ip_blocked_prefix . $ip) ? true : false; } private function increment_ip_attempts($ip) { $attempts = (int) get_transient($this->ip_attempts_prefix . $ip); $attempts++; set_transient($this->ip_attempts_prefix . $ip, $attempts, HOUR_IN_SECONDS); if ($attempts >= $this->max_attempts) { set_transient($this->ip_blocked_prefix . $ip, 1, HOUR_IN_SECONDS); delete_transient($this->ip_attempts_prefix . $ip); } return $attempts; } private function is_user_blocked($user_id) { $blocked_users = get_option($this->blocked_users_key, []); return in_array(intval($user_id), $blocked_users, true); } private function block_user($user_id) { $blocked_users = get_option($this->blocked_users_key, []); if (!in_array(intval($user_id), $blocked_users, true)) { $blocked_users[] = intval($user_id); update_option($this->blocked_users_key, $blocked_users); } } private function unblock_user($user_id) { $blocked_users = get_option($this->blocked_users_key, []); if (($key = array_search(intval($user_id), $blocked_users, true)) !== false) { unset($blocked_users[$key]); update_option($this->blocked_users_key, array_values($blocked_users)); } } private function generate_uuid() { return wp_generate_uuid4(); } public function set_security_headers() { if (is_admin()) { header('X-Frame-Options: SAMEORIGIN'); header('X-Content-Type-Options: nosniff'); header('Referrer-Policy: no-referrer-when-downgrade'); } } /* ------------------------- Script Enqueue ------------------------- */ public function enqueue_scripts($hook) { $opts = $this->get_opts(); $user_id = get_current_user_id(); $device_id = $this->current_device_id(); // Enqueue styles for relevant admin pages. The settings page and the // dashboard widget always need the v2 styles, even when protection is // off; when protection is on, every admin page needs them (broadcast // banner, overlay, realtime alerts). if ($hook === 'toplevel_page_scbr_settings' || $hook === 'dashboard' || $opts['status'] === 'on' ) { wp_enqueue_style( 'scbr-admin', plugin_dir_url(__FILE__) . 'css/scbr-admin.css', [], '2.0.5' ); } // Enqueue overlay script for new device detection. // NOTE: the stop-password hash is NEVER printed to the page. The // browser only collects the password; verification happens // server-side in ajax_stop_siren() via wp_check_password(). if ($opts['status'] === 'on' && get_transient($this->transient_prefix . $user_id . '_' . $device_id)) { wp_enqueue_script( 'scbr-overlay', plugin_dir_url(__FILE__) . 'js/scbr-overlay.js', [], '2.0.5', true ); wp_localize_script('scbr-overlay', 'scbr_ajax', [ 'ajaxurl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce($this->nonce_action), ]); } // Enqueue settings script. Banner action buttons (trust/block/mute) // render on every admin page via admin_notices, so their handlers must // be present globally whenever protection is on (delegated binding). if ($hook === 'toplevel_page_scbr_settings' || $opts['status'] === 'on') { wp_enqueue_media(); wp_enqueue_script( 'scbr-settings', plugin_dir_url(__FILE__) . 'js/scbr-settings.js', ['jquery', 'wp-mediaelement'], '2.0.5', true ); wp_localize_script('scbr-settings', 'scbr_ajax', [ 'ajaxurl' => admin_url('admin-ajax.php'), ]); } // Enqueue broadcast script for broadcast banner if ($opts['status'] === 'on' && get_transient($this->transient_broadcast)) { wp_enqueue_script( 'scbr-broadcast', plugin_dir_url(__FILE__) . 'js/scbr-broadcast.js', [], '2.0.5', true ); } // Realtime broadcast poller: lets other logged-in admins see and hear // a new-device alert without reloading the page. if ($opts['status'] === 'on' && !empty($opts['features']['broadcast_siren']) && current_user_can('edit_posts')) { wp_enqueue_script( 'scbr-broadcast-poll', plugin_dir_url(__FILE__) . 'js/scbr-broadcast-poll.js', [], '2.0.5', true ); wp_localize_script('scbr-broadcast-poll', 'scbrPoll', [ 'ajaxurl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce($this->nonce_action), ]); } // Enqueue inline style for scroll blocking if ($opts['status'] === 'on' && get_transient($this->transient_prefix . $user_id . '_' . $device_id)) { wp_add_inline_style('scbr-admin', 'html,body{overflow:hidden !important;}'); } } /* ------------------------- Cookie Handling ------------------------- */ public function ensure_device_cookie() { if (!is_admin() || !is_user_logged_in()) return; if (empty($_COOKIE[$this->cookie])) { $uuid = $this->generate_uuid(); $secure = is_ssl(); $httponly = true; $path = defined('ADMIN_COOKIE_PATH') ? ADMIN_COOKIE_PATH : '/'; $domain = COOKIE_DOMAIN ?: ''; setcookie($this->cookie, $uuid, time() + YEAR_IN_SECONDS, $path, $domain, $secure, $httponly); $_COOKIE[$this->cookie] = $uuid; } } /* ----------------------- New Device Detection ----------------------- */ public function maybe_handle_new_device_detection() { $opts = $this->get_opts(); if (empty($opts) || empty($opts['status']) || $opts['status'] !== 'on') { return; } $this->handle_new_device_detection(); } public function handle_new_device_detection() { if (!current_user_can('edit_posts')) return; $opts = $this->get_opts(); if (empty($opts['features']['overlay_enabled'])) return; $user_id = get_current_user_id(); $device_id = $this->current_device_id(); $ip = $this->ip_address(); if (!$device_id || $this->is_ip_blocked($ip)) { return; } if ($this->is_user_blocked($user_id)) { wp_logout(); wp_die('Your account has been blocked. Please contact the master admin.'); } $trusted = $this->trusted_devices($user_id); $is_trusted = in_array($device_id, $trusted, true); if (!$is_trusted) { $trigger_key = $this->transient_prefix . $user_id . '_' . $device_id; $already_triggered = (bool) get_transient($trigger_key); // Keep the overlay active while the user keeps browsing, but only // notify ONCE per new device (not on every admin page load). set_transient($trigger_key, 1, 10 * MINUTE_IN_SECONDS); if (!$already_triggered) { $this->broadcast_new_device_event($user_id, $device_id); if (!empty($opts['features']['email_alerts'])) { $by = wp_get_current_user(); $subject = sprintf('[%s] New Device Login Detected', wp_specialchars_decode(get_bloginfo('name'), ENT_QUOTES)); $body = sprintf( "User: %s (ID %d)\nTime: %s\nIP: %s\nUser Agent: %s\nDevice ID: %s\n\nThis user logged in from a NEW device and must verify with the Siren Stop Password.", $by->user_login, $by->ID, date_i18n('Y-m-d H:i:s'), $ip, isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : 'unknown', $device_id ); foreach ($this->admin_emails_except($user_id) as $email) { wp_mail($email, $subject, $body); } } } } } /* ------------------------- Overlay Handling ------------------------- */ public function maybe_render_new_device_overlay() { $uid = get_current_user_id(); $device_id = $this->current_device_id(); $opts = $this->get_opts(); if (empty($opts) || empty($opts['status']) || $opts['status'] !== 'on') return; if (!$uid || !$device_id || !get_transient($this->transient_prefix . $uid . '_' . $device_id)) return; if (empty($opts['features']['overlay_enabled'])) return; $ip = $this->ip_address(); if ($this->is_ip_blocked($ip)) { wp_die('Your IP has been temporarily blocked due to multiple failed password attempts. Please try again later.'); } $this->render_new_device_overlay(); } public function render_new_device_overlay() { $opts = $this->get_opts(); $message = esc_html($opts['overlay_text'] ?? 'Security Verification: Enter Siren Stop Password.'); $siren_muted = (bool) get_user_meta(get_current_user_id(), 'scbr_siren_muted', true); $audio_src = !empty($opts['custom_audio_url']) ? esc_url($opts['custom_audio_url']) : 'data:audio/mpeg;base64,' . $this->base64_siren(); $audio_type = !empty($opts['custom_audio_url']) ? esc_attr('audio/mpeg') : esc_attr('audio/mpeg'); ?>
get_opts(); if (empty($opts) || empty($opts['status']) || $opts['status'] !== 'on') { return; } if (empty($opts['features']['broadcast_siren'])) { return; } $this->admin_header_siren(); } /** * Siren audio source for the broadcast alert, or '' when it must not play * (feature off, no active alert, user muted, or insufficient caps). */ private function get_broadcast_audio_src() { if (!current_user_can('edit_posts')) return ''; $opts = $this->get_opts(); if (empty($opts['features']['broadcast_siren'])) return ''; if (!get_transient($this->transient_broadcast)) return ''; $muted = get_user_meta(get_current_user_id(), 'scbr_siren_muted', true); if ($muted) return ''; $opts = $this->get_opts(); return !empty($opts['custom_audio_url']) ? esc_url($opts['custom_audio_url']) : 'data:audio/mpeg;base64,' . $this->base64_siren(); } public function admin_header_siren() { $audio_src = $this->get_broadcast_audio_src(); if (!$audio_src) return; // Output audio tag ?> get_opts(); if (empty($opts) || empty($opts['status']) || $opts['status'] !== 'on') { return; } if (empty($opts['features']['broadcast_siren'])) { return; } $this->render_broadcast_banner(); } public function render_broadcast_banner() { if (!current_user_can('edit_posts')) return; $broadcast = get_transient($this->transient_broadcast); if (!$broadcast) { return; } $trigger_user = intval($broadcast['user'] ?? 0); $current_user_id = get_current_user_id(); $is_same_user = ($trigger_user === $current_user_id); $device_id = $this->current_device_id(); $is_trigger_device = ($device_id === ($broadcast['device_id'] ?? '')); if ($is_trigger_device && !$is_same_user) return; $nonce = wp_create_nonce($this->nonce_action); $u = get_user_by('id', $trigger_user); $name = $u ? esc_html($u->user_login) : esc_html('Unknown User'); $is_blocked = $this->is_user_blocked($trigger_user); $action = $is_blocked ? 'unblock' : 'block'; $action_label = $is_blocked ? 'Unblock User' : 'Block User'; $siren_muted = (bool) get_user_meta(get_current_user_id(), 'scbr_siren_muted', true); $mute_class = $siren_muted ? 'scbr-unmute' : 'scbr-mute'; $mute_label = $siren_muted ? __('Unmute Siren', 'defentry') : __('Mute Siren', 'defentry'); ?>

ALERT: New device login detected for your account (Device ID: ). Trust the device or contact the master admin to protect the website.

is_master_admin() && !$is_trigger_device): ?>

ALERT: New device login detected for (ID: ). Siren active until verified or action taken.

is_master_admin()): ?>

nonce_action, 'nonce'); $ip = $this->ip_address(); if ($this->is_ip_blocked($ip)) { wp_send_json_error('Your IP is temporarily blocked due to multiple failed attempts.', 403); } $opts = $this->get_opts(); $hash = !empty($opts['siren_password_hash']) ? $opts['siren_password_hash'] : ''; // The password is verified ONLY here, server-side, with // wp_check_password(). The browser never sees the hash and there is // no client-computed token path (that was a full bypass). $provided = isset($_POST['pass']) ? sanitize_text_field(wp_unslash($_POST['pass'])) : ''; if ($provided !== '' && $hash !== '' && wp_check_password($provided, $hash)) { $user_id = get_current_user_id(); $device_id = $this->current_device_id(); if ($device_id) $this->trust_device($user_id, $device_id); delete_transient($this->transient_prefix . $user_id . '_' . $device_id); delete_user_meta($user_id, $this->attempts_meta); delete_transient($this->ip_attempts_prefix . $ip); delete_transient($this->transient_broadcast); wp_send_json_success(['ok' => 1]); } $attempts = $this->increment_ip_attempts($ip); $remaining = max(0, $this->max_attempts - $attempts); wp_send_json_error('Wrong password. Attempts remaining: ' . $remaining); } public function ajax_block_user() { if (!is_user_logged_in() || !$this->is_master_admin()) { wp_send_json_error('Not allowed. Only Master Admin can block users.', 403); } check_ajax_referer($this->nonce_action, 'nonce'); $target_user_id = isset($_POST['target_user_id']) ? intval($_POST['target_user_id']) : 0; if (!$target_user_id || !user_can($target_user_id, 'edit_posts')) { wp_send_json_error('Invalid user or user does not have required role.', 400); } if ($target_user_id === get_current_user_id()) { wp_send_json_error('Cannot block yourself.', 400); } $this->block_user($target_user_id); $sessions = WP_Session_Tokens::get_instance($target_user_id); $sessions->destroy_all(); delete_user_meta($target_user_id, 'scbr_trusted_devices'); // Instantly block the IP tied to the active new-device alert for this // user, so the suspicious device is locked out immediately. $broadcast = get_transient($this->transient_broadcast); if (!empty($broadcast['ip']) && intval($broadcast['user'] ?? 0) === $target_user_id && filter_var($broadcast['ip'], FILTER_VALIDATE_IP) ) { set_transient($this->ip_blocked_prefix . $broadcast['ip'], 1, HOUR_IN_SECONDS); delete_transient($this->ip_attempts_prefix . $broadcast['ip']); } delete_transient($this->transient_broadcast); $opts = $this->get_opts(); if (!empty($opts['features']['email_alerts'])) { $by = wp_get_current_user(); $target = get_user_by('id', $target_user_id); $subject = sprintf('[%s] User Blocked', wp_specialchars_decode(get_bloginfo('name'), ENT_QUOTES)); $body = sprintf( "User %s (ID %d) has been blocked by %s (ID %d) due to a suspicious login.\nTime: %s\nIP: %s", $target->user_login, $target->ID, $by->user_login, $by->ID, date_i18n('Y-m-d H:i:s'), $this->ip_address() ); foreach ($this->admin_emails_except($by->ID) as $email) { wp_mail($email, $subject, $body); } } wp_send_json_success(['ok' => 1]); } public function ajax_unblock_user() { if (!is_user_logged_in() || !$this->is_master_admin()) { wp_send_json_error('Not allowed. Only Master Admin can unblock users.', 403); } check_ajax_referer($this->nonce_action, 'nonce'); $target_user_id = isset($_POST['target_user_id']) ? intval($_POST['target_user_id']) : 0; if (!$target_user_id || !user_can($target_user_id, 'edit_posts')) { wp_send_json_error('Invalid user or user does not have required role.', 400); } if ($target_user_id === get_current_user_id()) { wp_send_json_error('Cannot unblock yourself.', 400); } $this->unblock_user($target_user_id); $opts = $this->get_opts(); if (!empty($opts['features']['email_alerts'])) { $by = wp_get_current_user(); $target = get_user_by('id', $target_user_id); $subject = sprintf('[%s] User Unblocked', wp_specialchars_decode(get_bloginfo('name'), ENT_QUOTES)); $body = sprintf( "User %s (ID %d) has been unblocked by %s (ID %d).\nTime: %s\nIP: %s", $target->user_login, $target->ID, $by->user_login, $by->ID, date_i18n('Y-m-d H:i:s'), $this->ip_address() ); foreach ($this->admin_emails_except($by->ID) as $email) { wp_mail($email, $subject, $body); } } wp_send_json_success(['ok' => 1]); } public function ajax_trust_device() { if (!is_user_logged_in() || !current_user_can('edit_posts')) { wp_send_json_error('Not allowed.', 403); } check_ajax_referer($this->nonce_action, 'nonce'); $target_user_id = isset($_POST['target_user_id']) ? intval($_POST['target_user_id']) : 0; $device_id = isset($_POST['device_id']) ? sanitize_text_field(wp_unslash($_POST['device_id'])) : ''; if (!$target_user_id || !user_can($target_user_id, 'edit_posts') || !$device_id) { wp_send_json_error('Invalid user or device.', 400); } // A user may trust their OWN device; trusting anyone else's device // requires the Master Admin. if ($target_user_id !== get_current_user_id() && !$this->is_master_admin()) { wp_send_json_error('Not allowed. Only the Master Admin can trust devices for other users.', 403); } $this->trust_device($target_user_id, $device_id); delete_transient($this->transient_prefix . $target_user_id . '_' . $device_id); delete_transient($this->transient_broadcast); $opts = $this->get_opts(); if (!empty($opts['features']['email_alerts'])) { $by = wp_get_current_user(); $target = get_user_by('id', $target_user_id); $subject = sprintf('[%s] Device Trusted', wp_specialchars_decode(get_bloginfo('name'), ENT_QUOTES)); $body = sprintf( "Device for user %s (ID %d) has been trusted by %s (ID %d).\nTime: %s\nIP: %s", $target->user_login, $target->ID, $by->user_login, $by->ID, date_i18n('Y-m-d H:i:s'), $this->ip_address() ); foreach ($this->admin_emails_except($by->ID) as $email) { wp_mail($email, $subject, $body); } } wp_send_json_success(['ok' => 1]); } public function ajax_remove_device() { if (!is_user_logged_in() || !current_user_can('administrator') || !$this->is_master_admin()) { wp_send_json_error('Not allowed.', 403); } check_ajax_referer($this->nonce_action, 'nonce'); $target_user_id = isset($_POST['target_user_id']) ? intval($_POST['target_user_id']) : 0; $device_id = isset($_POST['device_id']) ? sanitize_text_field(wp_unslash($_POST['device_id'])) : ''; if (!$target_user_id || !user_can($target_user_id, 'edit_posts') || !$device_id) { wp_send_json_error('Invalid user or device.', 400); } if ($target_user_id === get_current_user_id()) { wp_send_json_error('Cannot remove your own devices.', 400); } $this->remove_device($target_user_id, $device_id); $opts = $this->get_opts(); if (!empty($opts['features']['email_alerts'])) { $by = wp_get_current_user(); $target = get_user_by('id', $target_user_id); $subject = sprintf('[%s] Device Removed', wp_specialchars_decode(get_bloginfo('name'), ENT_QUOTES)); $body = sprintf( "A trusted device for user %s (ID %d) was removed by %s (ID %d).\nTime: %s\nIP: %s", $target->user_login, $target->ID, $by->user_login, $by->ID, date_i18n('Y-m-d H:i:s'), $this->ip_address() ); foreach ($this->admin_emails_except($by->ID) as $email) { wp_mail($email, $subject, $body); } } wp_send_json_success(['ok' => 1]); } public function ajax_mute_siren() { if (!is_user_logged_in() || !current_user_can('edit_posts')) { wp_send_json_error('Not allowed.', 403); } check_ajax_referer($this->nonce_action, 'nonce'); update_user_meta(get_current_user_id(), 'scbr_siren_muted', 1); wp_send_json_success(['ok' => 1]); } /** * Realtime broadcast check for the admin poller. Returns the rendered * warning banner HTML plus the siren audio source so other logged-in * admins see and hear a new-device alert without reloading the page. */ public function ajax_check_broadcast() { if (!is_user_logged_in() || !current_user_can('edit_posts')) { wp_send_json_error('Not allowed.', 403); } check_ajax_referer($this->nonce_action, 'nonce'); $opts = $this->get_opts(); if (empty($opts['status']) || $opts['status'] !== 'on' || empty($opts['features']['broadcast_siren'])) { wp_send_json_success(['active' => false]); } $broadcast = get_transient($this->transient_broadcast); if (!$broadcast) { wp_send_json_success(['active' => false]); } ob_start(); $this->render_broadcast_banner(); $html = ob_get_clean(); wp_send_json_success([ 'active' => true, 'token' => $broadcast['token'] ?? '', 'html' => $html, 'audio_src' => $this->get_broadcast_audio_src(), ]); } /** * Instantly block the IP tied to the active new-device alert. * Master Admin only. Used by the "Block IP" banner button. */ public function ajax_block_ip() { if (!is_user_logged_in() || !$this->is_master_admin()) { wp_send_json_error('Not allowed. Only Master Admin can block IPs.', 403); } check_ajax_referer($this->nonce_action, 'nonce'); $broadcast = get_transient($this->transient_broadcast); $ip = $broadcast['ip'] ?? ''; if (!$ip || !filter_var($ip, FILTER_VALIDATE_IP)) { wp_send_json_error('No active alert IP to block.', 400); } set_transient($this->ip_blocked_prefix . $ip, 1, HOUR_IN_SECONDS); delete_transient($this->ip_attempts_prefix . $ip); wp_send_json_success(['ok' => 1, 'ip' => $ip]); } /** * Re-enable the broadcast siren after it was muted. Any user who could * mute it can unmute it. */ public function ajax_unmute_siren() { if (!is_user_logged_in() || !current_user_can('edit_posts')) { wp_send_json_error('Not allowed.', 403); } check_ajax_referer($this->nonce_action, 'nonce'); delete_user_meta(get_current_user_id(), 'scbr_siren_muted'); wp_send_json_success(['ok' => 1]); } /* ------------------------------ Settings UI ------------------------------ */ public function register_settings_page() { add_menu_page( 'Defentry', 'Defentry', 'manage_options', 'scbr_settings', [$this, 'render_settings_page'], 'dashicons-shield', 65 ); // The control plane is Master-Admin-exclusive: once setup is done, // secondary admins don't even see the menu. (First-run wizard stays // open to all admins; direct URL access still hits the wp_die guard.) $opts = $this->get_opts(); if (empty($opts['needs_master_admin']) && !$this->is_master_admin()) { remove_menu_page('scbr_settings'); } } public function render_settings_page() { $opts = $this->get_opts(); $needs_setup = !empty($opts['needs_master_admin']); // First run: any administrator may open the setup wizard (the same // audience the existing admin-notice form already serves). Once a // Master Admin exists, only they may open these settings. if ($needs_setup) { if (!current_user_can('manage_options')) { wp_die(esc_html(__('Only an administrator can set up Defentry.', 'defentry'))); } } elseif (!$this->is_master_admin()) { wp_die(esc_html(__('Only the Master Admin can access Defentry settings.', 'defentry'))); } $nonce = wp_create_nonce($this->nonce_action); wp_enqueue_media(); // First-run setup wizard. if ($needs_setup) { $this->render_setup_wizard($nonce); return; } // Wizard steps 2-3, shown right after step 1 was saved. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display-only render-path switch; state changes happen in nonce-verified admin_post handlers. if (isset($_GET['scbr_wizard'])) { $this->render_setup_wizard_steps_23($opts, $nonce); return; } $tabs = [ 'overview' => __('Overview', 'defentry'), 'verification' => __('Verification', 'defentry'), 'devices' => __('Devices', 'defentry'), 'protection' => __('Protection', 'defentry'), 'master' => __('Master Admin', 'defentry'), ]; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display-only tab switch; value is sanitized with sanitize_key() and whitelisted against $tabs. $tab = isset($_GET['scbr_tab']) ? sanitize_key(wp_unslash($_GET['scbr_tab'])) : 'overview'; if (!isset($tabs[$tab])) { $tab = 'overview'; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display-only success notice; the save itself is nonce-verified in handle_save_settings(). if (isset($_GET['updated'])) { echo '

' . esc_html(__('Defentry settings saved.', 'defentry')) . '

'; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display-only success notice; the reset itself is nonce-verified in handle_trust_reset(). if (isset($_GET['reset'])) { echo '

' . esc_html(__('Trusted devices have been reset.', 'defentry')) . '

'; } ?>

> render_overview_tab($opts, $nonce); ?>
> render_verification_tab($opts, $nonce); ?>
> render_devices_tab($nonce); ?>
> render_protection_tab($nonce); ?>
> render_master_tab($opts, $nonce); ?>
admins_list() as $u) { $total += count($this->trusted_devices($u->ID)); } return $total; } /** * Hidden fields that carry the current feature flags and siren audio URL * through partial settings forms (Overview toggle, Master Admin tab, * setup wizard). Without them, saving a partial form would reset the * untouched settings to their defaults. Presentation only. */ private function render_passthrough_fields($opts) { $features = $opts['features'] ?? []; foreach (['siren_enabled', 'email_alerts', 'overlay_enabled', 'broadcast_siren'] as $key) { printf( '' . "\n", esc_attr($key), !empty($features[$key]) ? 1 : 0 ); } printf( '' . "\n", esc_attr($opts['custom_audio_url'] ?? '') ); } private function render_overview_tab($opts, $nonce) { $is_on = !empty($opts['status']) && $opts['status'] === 'on'; $trusted_total = $this->count_trusted_devices(); $blocked_total = count(get_option($this->blocked_users_key, [])); ?>

render_passthrough_fields($opts); ?>
3
[__('Enable Siren Sound', 'defentry'), __('Play the siren when a new device needs verification.', 'defentry')], 'overlay_enabled' => [__('Full-Screen Overlay', 'defentry'), __('Block the screen with the red verification prompt on new devices.', 'defentry')], 'broadcast_siren' => [__('Siren for Other Admins', 'defentry'), __('Other logged-in admins hear the siren and see the blinking alert banner.', 'defentry')], 'email_alerts' => [__('Email Alerts', 'defentry'), __('Notify other admins by email about new device logins.', 'defentry')], ]; ?>

·

$def): ?>

admins_list() as $admin): $devices = $this->trusted_devices($admin->ID); $is_self = intval($admin->ID) === get_current_user_id(); ?>
user_login); ?>

blocked_users_key, []); ?>

user_login); ?> (ID )

max_attempts)); ?>

'administrator', 'fields' => ['ID', 'user_login']]); ?>
render_passthrough_fields($opts); ?>

user_login); ?>

'administrator', 'fields' => ['ID', 'user_login']]); $current_user = get_current_user_id(); $return_url = add_query_arg(['page' => 'scbr_settings', 'scbr_wizard' => 2], admin_url('admin.php')); ?>

  1. 1
  2. 2
  3. 3

is_master_admin()) { $mid = intval($opts['master_admin_id'] ?? 0); $mu = $mid ? get_user_by('id', $mid) : null; $who = $mu ? $mu->user_login : __('the Master Admin', 'defentry'); ?>

' . esc_html($who) . '' ); ?>

  1. ✓
  2. 2
  3. 3
render_passthrough_fields($opts); ?>

get_opts(); if (empty($opts['needs_master_admin']) && !$this->is_master_admin()) { return; } wp_add_dashboard_widget( 'scbr_security_widget', __('Defentry Security', 'defentry'), [$this, 'render_dashboard_widget'] ); } public function render_dashboard_widget() { $opts = $this->get_opts(); $is_on = !empty($opts['status']) && $opts['status'] === 'on'; $trusted_total = $this->count_trusted_devices(); $blocked_total = count(get_option($this->blocked_users_key, [])); ?>

is_master_admin()): ?>

nonce_action, 'nonce'); if (!$this->is_master_admin()) wp_die('Only Master Admin can save settings'); $opts = $this->get_opts(); // Only touch the status when the submitted form manages it (Overview // toggle, setup wizard). Other tabs must never silently flip it. if (isset($_POST['status'])) { $opts['status'] = $_POST['status'] === 'on' ? 'on' : 'off'; } $new_pwd = isset($_POST['siren_password']) ? sanitize_text_field(wp_unslash($_POST['siren_password'])) : ''; if ($new_pwd !== '') { // Hashed with wp_hash_password(); the plaintext is never stored. $opts['siren_password_hash'] = wp_hash_password($new_pwd); } unset($opts['siren_password']); // legacy plaintext key must not survive $opts['custom_audio_url'] = isset($_POST['custom_audio_url']) ? esc_url_raw(wp_unslash($_POST['custom_audio_url'])) : ''; $features = isset($_POST['features']) && is_array($_POST['features']) ? array_map('intval', $_POST['features']) : []; $opts['features'] = [ 'siren_enabled' => !empty($features['siren_enabled']) ? 1 : 0, 'email_alerts' => !empty($features['email_alerts']) ? 1 : 0, 'overlay_enabled' => !empty($features['overlay_enabled']) ? 1 : 0, 'broadcast_siren' => !empty($features['broadcast_siren']) ? 1 : 0, ]; if (isset($_POST['overlay_text'])) { $opts['overlay_text'] = sanitize_text_field(wp_unslash($_POST['overlay_text'])); } if (isset($_POST['master_admin_id'])) { $opts['master_admin_id'] = intval($_POST['master_admin_id']); $opts['needs_master_admin'] = 0; } update_option($this->opt_key, $opts); wp_safe_redirect(add_query_arg(['page' => 'scbr_settings', 'updated' => '1'], admin_url('admin.php'))); exit; } public function handle_trust_reset() { if (!current_user_can('manage_options')) wp_die('No permission'); check_admin_referer($this->nonce_action, 'nonce'); if (!$this->is_master_admin()) wp_die('Only Master Admin can reset'); $target = isset($_POST['target_user_id']) ? intval($_POST['target_user_id']) : 0; if ($target) delete_user_meta($target, 'scbr_trusted_devices'); wp_safe_redirect(add_query_arg(['page' => 'scbr_settings', 'reset' => '1'], admin_url('admin.php'))); exit; } /* ---------------------- Master Admin First-Run UI ---------------------- */ public function maybe_prompt_master_admin() { if (!current_user_can('manage_options')) return; $opts = $this->get_opts(); if (empty($opts['needs_master_admin'])) return; $admins = get_users(['role' => 'administrator', 'fields' => ['ID', 'user_login', 'user_email']]); $nonce = wp_create_nonce($this->nonce_action); echo '
'; echo '

Defentry: Select a Master Admin to finalize setup. Plugin is OFF by default. After setup, change the siren stop password in the Defentry settings.

'; echo '
'; echo ''; echo ''; echo ' '; submit_button('Set Master Admin', 'primary', 'submit', false); echo '
'; echo '
'; } public function handle_set_master_admin() { if (!current_user_can('manage_options')) wp_die('No permission'); check_admin_referer($this->nonce_action, 'nonce'); $opts = $this->get_opts(); $mid = isset($_POST['master_admin_id']) ? intval($_POST['master_admin_id']) : 0; if ($mid > 0) { $opts['master_admin_id'] = $mid; $opts['needs_master_admin'] = 0; update_option($this->opt_key, $opts); } // Optional same-admin return URL (used by the setup wizard). // Restricted to admin URLs only; defaults to the dashboard. $redirect = admin_url('index.php'); if (!empty($_POST['scbr_return'])) { $candidate = esc_url_raw(wp_unslash($_POST['scbr_return'])); if (is_string($candidate) && strpos($candidate, admin_url()) === 0) { $redirect = $candidate; } } wp_safe_redirect($redirect); exit; } /* ----------------------- Embedded Audio (Base64) ----------------------- */ private function base64_siren() { return 'SUQzAwAAAAAAFlRTU0UAAAAPAAADTGF2ZjU2LjQwLjEwMAAAAAAAAAAAAAAA//tQxAADBQAAQAAAABEVmVyc2lvbiB0aW55IGJpcCBhbGFybQAA//uQZAAAAAD6AAAAAAAACQAAABJbmZvAAAACAAAAG1wMyB0aW55AAACAAAB//uQZAAAABkAABp6AAACAAABAAAAP///8AAAP//AAAAP///8AAAAD//wA='; } } new SCBR_Security_Control_Master(); ?>