*/ private array $settings = []; private string $private_access_error = ''; private string $private_access_redirect_to = ''; public static function instance(): Maintenance_Mode { if (self::$instance === null) { self::$instance = new self(); } return self::$instance; } private function __construct() { $this->settings = $this->get_settings(); add_action('admin_menu', [$this, 'register_admin_menu']); add_action('admin_init', [$this, 'register_settings']); add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']); add_action('template_redirect', [$this, 'maybe_render_maintenance'], 1); add_action('admin_post_kng_maintenance_export', [$this, 'handle_export']); add_action('admin_post_kng_maintenance_import', [$this, 'handle_import']); add_action('admin_post_kng_maintenance_reset_analytics', [$this, 'handle_reset_analytics']); add_action('admin_post_kng_maintenance_generate_token', [$this, 'handle_generate_private_token']); add_action('admin_post_kng_maintenance_revoke_token', [$this, 'handle_revoke_private_token']); add_action('admin_post_kng_maintenance_revoke_password', [$this, 'handle_revoke_private_password']); add_shortcode('kng_maintenance_page', [$this, 'render_shortcode']); } public function register_admin_menu(): void { add_submenu_page( 'king-addons', __('Maintenance Mode', 'king-addons'), __('Maintenance Mode', 'king-addons'), 'manage_options', 'king-addons-maintenance-mode', [$this, 'render_admin_page'] ); } public function register_settings(): void { register_setting( 'kng_maintenance_settings_group', self::OPTION_NAME, [ 'type' => 'array', 'sanitize_callback' => [$this, 'sanitize_settings'], 'default' => $this->get_default_settings(), ] ); } public function enqueue_admin_assets(string $hook): void { if ($hook !== 'king-addons_page_king-addons-maintenance-mode') { return; } $shared_css = KING_ADDONS_URL . 'includes/admin/layouts/shared/admin-v3-styles.css'; $shared_path = KING_ADDONS_PATH . 'includes/admin/layouts/shared/admin-v3-styles.css'; $shared_version = file_exists($shared_path) ? filemtime($shared_path) : KING_ADDONS_VERSION; wp_enqueue_style('king-addons-admin-v3', $shared_css, [], $shared_version); $admin_css = KING_ADDONS_URL . 'includes/extensions/Maintenance_Mode/assets/admin.css'; $admin_path = KING_ADDONS_PATH . 'includes/extensions/Maintenance_Mode/assets/admin.css'; $admin_version = file_exists($admin_path) ? filemtime($admin_path) : KING_ADDONS_VERSION; wp_enqueue_style('king-addons-maintenance-admin', $admin_css, ['king-addons-admin-v3'], $admin_version); $admin_js = KING_ADDONS_URL . 'includes/extensions/Maintenance_Mode/assets/admin.js'; $admin_path_js = KING_ADDONS_PATH . 'includes/extensions/Maintenance_Mode/assets/admin.js'; $admin_js_version = file_exists($admin_path_js) ? filemtime($admin_path_js) : KING_ADDONS_VERSION; wp_enqueue_script('king-addons-maintenance-admin', $admin_js, ['jquery'], $admin_js_version, true); wp_localize_script('king-addons-maintenance-admin', 'KNGMaintenance', [ 'ajaxUrl' => admin_url('admin-ajax.php'), 'themeNonce' => wp_create_nonce('king_addons_dashboard_ui'), ]); } public function render_admin_page(): void { if (!current_user_can('manage_options')) { return; } $view = isset($_GET['view']) ? sanitize_key($_GET['view']) : 'dashboard'; $is_pro = $this->is_pro(); $settings = $this->get_settings(); $templates = $this->get_builtin_templates(); include __DIR__ . '/templates/admin-page.php'; } public function maybe_render_maintenance(): void { if ($this->is_preview_request()) { $this->render_maintenance_response(true); exit; } if (!$this->is_mode_active()) { return; } $bypass_reason = $this->get_bypass_reason(); if ($bypass_reason !== '') { $this->track_bypass($bypass_reason); if ($this->private_access_redirect_to !== '') { wp_safe_redirect($this->private_access_redirect_to); exit; } return; } $this->track_blocked_visit(); $this->render_maintenance_response(false); exit; } public function handle_reset_analytics(): void { if (!current_user_can('manage_options')) { wp_die(esc_html__('Unauthorized request.', 'king-addons')); } check_admin_referer('kng_maintenance_reset_analytics'); delete_option(self::ANALYTICS_OPTION); delete_transient(self::ANALYTICS_TRANSIENT_24H); $this->redirect_with_message('analytics', 'analytics_reset'); } private function is_mode_active(): bool { if (empty($this->settings['enabled'])) { return false; } if (empty($this->settings['schedule_enabled'])) { return true; } return $this->is_any_schedule_window_active() || $this->is_any_recurring_rule_active(); } private function is_any_schedule_window_active(): bool { $now = current_time('timestamp', true); $windows = $this->settings['schedule_windows'] ?? []; if (!is_array($windows)) { $windows = []; } foreach ($windows as $window) { if (!is_array($window)) { continue; } $start = $this->parse_schedule_time((string) ($window['start'] ?? '')); $end = $this->parse_schedule_time((string) ($window['end'] ?? '')); if ($this->is_time_range_active($now, $start, $end)) { return true; } } $start = $this->parse_schedule_time($this->settings['schedule_start'] ?? ''); $end = $this->parse_schedule_time($this->settings['schedule_end'] ?? ''); return $this->is_time_range_active($now, $start, $end); } private function is_time_range_active(int $now, int $start, int $end): bool { if ($start && $end) { return $now >= $start && $now <= $end; } if ($start && !$end) { return $now >= $start; } if (!$start && $end) { return $now <= $end; } return false; } private function is_any_recurring_rule_active(): bool { if (empty($this->settings['recurring_enabled'])) { return false; } $rules = $this->settings['recurring_rules'] ?? []; if (!is_array($rules) || $rules === []) { return false; } $nowTs = current_time('timestamp', true); $nowUtc = (new \DateTimeImmutable('@' . $nowTs))->setTimezone(new \DateTimeZone('UTC')); foreach ($rules as $rule) { if (!is_array($rule)) { continue; } if ($this->is_recurring_rule_active($rule, $nowUtc)) { return true; } } return false; } private function is_recurring_rule_active(array $rule, \DateTimeImmutable $nowUtc): bool { $freq = isset($rule['frequency']) ? sanitize_key((string) $rule['frequency']) : ''; if (!in_array($freq, ['daily', 'weekly', 'monthly'], true)) { return false; } $tzString = isset($rule['timezone']) ? sanitize_text_field((string) $rule['timezone']) : self::DEFAULT_TIMEZONE; $tz = $this->resolve_timezone($tzString); $local = $nowUtc->setTimezone($tz); $startMinutes = $this->parse_time_minutes((string) ($rule['start_time'] ?? '')); $endMinutes = $this->parse_time_minutes((string) ($rule['end_time'] ?? '')); if ($startMinutes < 0 || $endMinutes < 0) { return false; } $nowMinutes = ((int) $local->format('G')) * 60 + (int) $local->format('i'); $inTime = $this->is_minutes_in_range($nowMinutes, $startMinutes, $endMinutes); if (!$inTime) { return false; } if ($freq === 'daily') { return true; } if ($freq === 'weekly') { $days = $rule['days_of_week'] ?? []; if (!is_array($days) || $days === []) { return false; } $dow = (int) $local->format('N'); return in_array($dow, array_map('intval', $days), true); } $days = $rule['days_of_month'] ?? []; if (!is_array($days) || $days === []) { return false; } $dom = (int) $local->format('j'); return in_array($dom, array_map('intval', $days), true); } private function is_minutes_in_range(int $value, int $start, int $end): bool { if ($start === $end) { return false; } if ($start < $end) { return $value >= $start && $value <= $end; } return $value >= $start || $value <= $end; } private function parse_time_minutes(string $value): int { $value = trim($value); if (!preg_match('/^(\d{1,2}):(\d{2})$/', $value, $m)) { return -1; } $h = (int) $m[1]; $i = (int) $m[2]; if ($h < 0 || $h > 23 || $i < 0 || $i > 59) { return -1; } return $h * 60 + $i; } private function resolve_timezone(string $timezone): \DateTimeZone { $timezone = $timezone !== '' ? $timezone : self::DEFAULT_TIMEZONE; if ($timezone === self::DEFAULT_TIMEZONE) { return function_exists('wp_timezone') ? wp_timezone() : new \DateTimeZone('UTC'); } try { return new \DateTimeZone($timezone); } catch (\Exception $e) { return function_exists('wp_timezone') ? wp_timezone() : new \DateTimeZone('UTC'); } } private function should_bypass_request(): bool { return $this->get_bypass_reason() !== ''; } private function get_bypass_reason(): string { if (defined('WP_CLI') && WP_CLI) { return 'wp_cli'; } if (wp_doing_cron()) { return 'cron'; } if (is_admin()) { return 'wp_admin'; } if (wp_doing_ajax() && !empty($this->settings['allow_admin_ajax'])) { return 'admin_ajax_allowed'; } if ($this->is_login_request()) { return 'login_page'; } if (!empty($this->settings['disable_elementor_editor']) && $this->is_elementor_editor()) { return 'elementor_editor'; } $private_reason = $this->get_private_access_bypass_reason(); if ($private_reason !== '') { return $private_reason; } if ($this->is_rest_request()) { if (is_user_logged_in()) { return 'rest_logged_in'; } return !empty($this->settings['allow_rest']) ? 'rest_allowed' : ''; } if ($this->is_user_allowed()) { return 'user_allowed'; } if ($this->is_ip_whitelisted()) { return 'ip_whitelist'; } if ($this->is_path_whitelisted()) { return 'path_whitelist'; } return ''; } private function is_private_access_enabled(): bool { if (!$this->is_pro()) { return false; } return !empty($this->settings['private_password_hash']) || !empty($this->settings['private_token']); } private function get_private_access_bypass_reason(): string { if (!$this->is_private_access_enabled()) { return ''; } if ($this->has_private_access_cookie()) { return 'private_cookie'; } $token = isset($_GET[self::PRIVATE_ACCESS_QUERY_PARAM]) ? sanitize_text_field(wp_unslash($_GET[self::PRIVATE_ACCESS_QUERY_PARAM])) : ''; if ($token !== '' && $this->is_private_token_valid($token)) { $this->set_private_access_cookie(); $this->private_access_redirect_to = $this->get_current_url_without_private_params(); return 'private_token'; } if ($this->maybe_accept_private_password()) { $this->private_access_redirect_to = $this->get_current_url_without_private_params(); return 'private_password'; } return ''; } private function maybe_accept_private_password(): bool { if (empty($this->settings['private_password_hash'])) { return false; } if (!isset($_POST[self::PRIVATE_ACCESS_POST_FLAG])) { return false; } $nonce = isset($_POST['_kng_private_nonce']) ? sanitize_text_field(wp_unslash($_POST['_kng_private_nonce'])) : ''; if ($nonce === '' || !wp_verify_nonce($nonce, 'kng_maintenance_private_access')) { $this->private_access_error = 'invalid_nonce'; return false; } $password = isset($_POST['kng_maintenance_private_password']) ? (string) wp_unslash($_POST['kng_maintenance_private_password']) : ''; $password = trim($password); if ($password === '') { $this->private_access_error = 'invalid_password'; return false; } $hash = (string) ($this->settings['private_password_hash'] ?? ''); if ($hash === '' || !function_exists('wp_check_password')) { $this->private_access_error = 'invalid_password'; return false; } if (!wp_check_password($password, $hash)) { $this->private_access_error = 'invalid_password'; return false; } $this->set_private_access_cookie(); return true; } private function is_private_token_valid(string $token): bool { $saved = (string) ($this->settings['private_token'] ?? ''); if ($saved === '' || $token === '') { return false; } return hash_equals($saved, $token); } private function get_private_access_cookie_expected(): string { $key = defined('AUTH_SALT') && AUTH_SALT ? AUTH_SALT : (defined('NONCE_SALT') && NONCE_SALT ? NONCE_SALT : 'kng'); $hash = (string) ($this->settings['private_password_hash'] ?? ''); $token = (string) ($this->settings['private_token'] ?? ''); $material = $hash . '|' . $token; return hash_hmac('sha256', 'private_access|' . $material, $key); } private function has_private_access_cookie(): bool { $expected = $this->get_private_access_cookie_expected(); if ($expected === '') { return false; } $cookie = isset($_COOKIE[self::PRIVATE_ACCESS_COOKIE]) ? (string) wp_unslash($_COOKIE[self::PRIVATE_ACCESS_COOKIE]) : ''; if ($cookie === '') { return false; } return hash_equals($expected, $cookie); } private function set_private_access_cookie(): void { $value = $this->get_private_access_cookie_expected(); if ($value === '') { return; } $expires = time() + (int) apply_filters('kng_maintenance_private_cookie_ttl', 7 * DAY_IN_SECONDS); $path = defined('COOKIEPATH') ? (string) COOKIEPATH : '/'; if ($path === '') { $path = '/'; } $args = [ 'expires' => $expires, 'path' => $path, 'secure' => is_ssl(), 'httponly' => true, 'samesite' => 'Lax', ]; $domain = defined('COOKIE_DOMAIN') ? (string) COOKIE_DOMAIN : ''; if ($domain !== '') { $args['domain'] = $domain; } setcookie(self::PRIVATE_ACCESS_COOKIE, $value, $args); $_COOKIE[self::PRIVATE_ACCESS_COOKIE] = $value; } private function get_current_url_without_private_params(): string { $request_uri = isset($_SERVER['REQUEST_URI']) ? (string) wp_unslash($_SERVER['REQUEST_URI']) : '/'; $url = home_url($request_uri); $url = remove_query_arg([self::PRIVATE_ACCESS_QUERY_PARAM], $url); return $url; } private function track_blocked_visit(): void { if ($this->is_preview_request()) { return; } if (is_admin() || wp_doing_cron() || (defined('WP_CLI') && WP_CLI)) { return; } $analytics = $this->get_analytics(); $analytics['blocked_total'] = (int) ($analytics['blocked_total'] ?? 0) + 1; $analytics['updated_at'] = time(); update_option(self::ANALYTICS_OPTION, $analytics, false); $this->update_24h_analytics('blocked', '', $this->get_masked_request_path()); $ip = $this->get_client_ip(); if ($ip !== '') { $this->update_24h_unique_ip($ip); } } private function track_bypass(string $reason = ''): void { if ($this->is_preview_request()) { return; } if (is_admin() || wp_doing_cron() || (defined('WP_CLI') && WP_CLI)) { return; } $analytics = $this->get_analytics(); $analytics['bypass_total'] = (int) ($analytics['bypass_total'] ?? 0) + 1; if ($reason !== '') { if (!isset($analytics['bypass_by_reason']) || !is_array($analytics['bypass_by_reason'])) { $analytics['bypass_by_reason'] = []; } $analytics['bypass_by_reason'][$reason] = (int) ($analytics['bypass_by_reason'][$reason] ?? 0) + 1; } $analytics['updated_at'] = time(); update_option(self::ANALYTICS_OPTION, $analytics, false); $this->update_24h_analytics('bypass', $reason, $this->get_masked_request_path()); } private function get_analytics(): array { $saved = get_option(self::ANALYTICS_OPTION, []); if (!is_array($saved)) { $saved = []; } $defaults = [ 'blocked_total' => 0, 'bypass_total' => 0, 'bypass_by_reason' => [], 'created_at' => time(), 'updated_at' => time(), ]; $data = wp_parse_args($saved, $defaults); if (!is_array($data['bypass_by_reason'])) { $data['bypass_by_reason'] = []; } return $data; } private function get_analytics_24h(): array { $saved = get_transient(self::ANALYTICS_TRANSIENT_24H); if (!is_array($saved)) { $saved = []; } $defaults = [ 'blocked' => 0, 'bypass' => 0, 'bypass_by_reason' => [], 'unique' => [], 'paths' => [ 'blocked' => [], 'bypass' => [], ], ]; $data = wp_parse_args($saved, $defaults); if (!is_array($data['unique'])) { $data['unique'] = []; } if (!is_array($data['bypass_by_reason'])) { $data['bypass_by_reason'] = []; } if (!isset($data['paths']) || !is_array($data['paths'])) { $data['paths'] = ['blocked' => [], 'bypass' => []]; } if (!isset($data['paths']['blocked']) || !is_array($data['paths']['blocked'])) { $data['paths']['blocked'] = []; } if (!isset($data['paths']['bypass']) || !is_array($data['paths']['bypass'])) { $data['paths']['bypass'] = []; } $this->prune_24h_unique($data); $this->prune_24h_paths($data); return $data; } private function save_analytics_24h(array $data): void { $this->prune_24h_unique($data); $this->prune_24h_paths($data); set_transient(self::ANALYTICS_TRANSIENT_24H, $data, DAY_IN_SECONDS + HOUR_IN_SECONDS); } private function prune_24h_unique(array &$data): void { $cutoff = time() - DAY_IN_SECONDS; if (!isset($data['unique']) || !is_array($data['unique'])) { $data['unique'] = []; return; } foreach ($data['unique'] as $hash => $ts) { if ((int) $ts < $cutoff) { unset($data['unique'][$hash]); } } $max = 10000; if (count($data['unique']) > $max) { $data['unique'] = array_slice($data['unique'], -$max, null, true); } } private function prune_24h_paths(array &$data): void { $cutoff = time() - DAY_IN_SECONDS; if (!isset($data['paths']) || !is_array($data['paths'])) { $data['paths'] = ['blocked' => [], 'bypass' => []]; return; } foreach (['blocked', 'bypass'] as $bucket) { if (!isset($data['paths'][$bucket]) || !is_array($data['paths'][$bucket])) { $data['paths'][$bucket] = []; continue; } foreach ($data['paths'][$bucket] as $hash => $row) { $ts = is_array($row) ? (int) ($row['t'] ?? 0) : 0; if ($ts < $cutoff) { unset($data['paths'][$bucket][$hash]); } } $max = 400; if (count($data['paths'][$bucket]) > $max) { uasort($data['paths'][$bucket], static function ($a, $b) { $ta = is_array($a) ? (int) ($a['t'] ?? 0) : 0; $tb = is_array($b) ? (int) ($b['t'] ?? 0) : 0; return $ta <=> $tb; }); $data['paths'][$bucket] = array_slice($data['paths'][$bucket], -$max, null, true); } } } private function update_24h_analytics(string $type, string $reason = '', string $masked_path = ''): void { $data = $this->get_analytics_24h(); if ($type === 'blocked') { $data['blocked'] = (int) ($data['blocked'] ?? 0) + 1; } elseif ($type === 'bypass') { $data['bypass'] = (int) ($data['bypass'] ?? 0) + 1; if ($reason !== '') { if (!isset($data['bypass_by_reason'][$reason])) { $data['bypass_by_reason'][$reason] = 0; } $data['bypass_by_reason'][$reason] = (int) $data['bypass_by_reason'][$reason] + 1; } } if ($masked_path !== '' && in_array($type, ['blocked', 'bypass'], true)) { if (!isset($data['paths']) || !is_array($data['paths'])) { $data['paths'] = ['blocked' => [], 'bypass' => []]; } if (!isset($data['paths'][$type]) || !is_array($data['paths'][$type])) { $data['paths'][$type] = []; } $hash = $this->hash_string($masked_path); if ($hash !== '') { if (!isset($data['paths'][$type][$hash]) || !is_array($data['paths'][$type][$hash])) { $data['paths'][$type][$hash] = ['c' => 0, 'm' => $masked_path, 't' => time()]; } $data['paths'][$type][$hash]['c'] = (int) ($data['paths'][$type][$hash]['c'] ?? 0) + 1; $data['paths'][$type][$hash]['m'] = $masked_path; $data['paths'][$type][$hash]['t'] = time(); } } $this->save_analytics_24h($data); } private function hash_string(string $value): string { $value = trim($value); if ($value === '') { return ''; } $key = defined('AUTH_SALT') && AUTH_SALT ? AUTH_SALT : (defined('NONCE_SALT') && NONCE_SALT ? NONCE_SALT : 'kng'); return hash_hmac('sha256', $value, $key); } private function get_request_path(): string { $request_uri = isset($_SERVER['REQUEST_URI']) ? (string) wp_unslash($_SERVER['REQUEST_URI']) : ''; $path = wp_parse_url($request_uri, PHP_URL_PATH); $path = $path ? '/' . ltrim((string) $path, '/') : '/'; return $path; } private function get_masked_request_path(): string { return $this->mask_path($this->get_request_path()); } private function mask_path(string $path): string { $path = $path !== '' ? '/' . ltrim($path, '/') : '/'; $trim = trim($path, '/'); if ($trim === '') { return '/'; } $segments = array_values(array_filter(explode('/', $trim), static fn($s) => $s !== '')); $maxSegments = 2; $shown = array_slice($segments, 0, $maxSegments); $out = []; foreach ($shown as $seg) { $seg = rawurldecode((string) $seg); $seg = preg_replace('/\s+/', '-', $seg); if ($seg === null || $seg === '') { continue; } if (preg_match('/^[0-9]+$/', $seg)) { $out[] = '{n}'; continue; } if (preg_match('/^[a-f0-9]{16,}$/i', $seg)) { $out[] = '{hash}'; continue; } if (preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i', $seg)) { $out[] = '{uuid}'; continue; } $seg = preg_replace('/\d+/', '{n}', $seg); $seg = preg_replace('/[^a-zA-Z0-9._\-{}]+/', '', (string) $seg); $seg = substr((string) $seg, 0, 24); if ($seg === '') { $seg = '{seg}'; } $out[] = $seg; } $masked = '/' . implode('/', $out); if (count($segments) > $maxSegments) { $masked .= '/…'; } return substr($masked, 0, 80); } private function update_24h_unique_ip(string $ip): void { $hash = $this->hash_ip($ip); if ($hash === '') { return; } $data = $this->get_analytics_24h(); $data['unique'][$hash] = time(); $this->save_analytics_24h($data); } private function hash_ip(string $ip): string { $ip = trim($ip); if ($ip === '') { return ''; } $key = defined('AUTH_SALT') && AUTH_SALT ? AUTH_SALT : (defined('NONCE_SALT') && NONCE_SALT ? NONCE_SALT : 'kng'); return hash_hmac('sha256', $ip, $key); } private function get_analytics_overview(): array { $all = $this->get_analytics(); $h24 = $this->get_analytics_24h(); $bypassAll = $all['bypass_by_reason'] ?? []; if (!is_array($bypassAll)) { $bypassAll = []; } $bypass24 = $h24['bypass_by_reason'] ?? []; if (!is_array($bypass24)) { $bypass24 = []; } arsort($bypassAll); arsort($bypass24); $topBlocked = $this->get_top_paths_24h($h24, 'blocked'); $topBypass = $this->get_top_paths_24h($h24, 'bypass'); return [ 'blocked_total' => (int) ($all['blocked_total'] ?? 0), 'bypass_total' => (int) ($all['bypass_total'] ?? 0), 'blocked_24h' => (int) ($h24['blocked'] ?? 0), 'bypass_24h' => (int) ($h24['bypass'] ?? 0), 'unique_24h' => is_array($h24['unique'] ?? null) ? count($h24['unique']) : 0, 'bypass_by_reason_total' => $bypassAll, 'bypass_by_reason_24h' => $bypass24, 'top_paths_24h_blocked' => $topBlocked, 'top_paths_24h_bypass' => $topBypass, ]; } private function get_top_paths_24h(array $h24, string $bucket, int $limit = 10): array { if (!isset($h24['paths']) || !is_array($h24['paths'])) { return []; } if (!isset($h24['paths'][$bucket]) || !is_array($h24['paths'][$bucket])) { return []; } $rows = []; foreach ($h24['paths'][$bucket] as $row) { if (!is_array($row)) { continue; } $count = (int) ($row['c'] ?? 0); $mask = isset($row['m']) ? (string) $row['m'] : ''; if ($count <= 0 || $mask === '') { continue; } $rows[] = ['mask' => $mask, 'count' => $count]; } usort($rows, static function ($a, $b) { $ca = (int) ($a['count'] ?? 0); $cb = (int) ($b['count'] ?? 0); if ($ca === $cb) { return strcmp((string) ($a['mask'] ?? ''), (string) ($b['mask'] ?? '')); } return $cb <=> $ca; }); return array_slice($rows, 0, $limit); } private function is_user_allowed(): bool { if (!is_user_logged_in()) { return false; } $user = wp_get_current_user(); if (!$user || !$user->ID) { return false; } if (!empty($this->settings['exclude_admin']) && user_can($user, 'manage_options')) { return true; } if ($this->is_pro()) { $allowed_roles = $this->settings['allowed_roles'] ?? []; if (!empty($allowed_roles) && array_intersect((array) $user->roles, $allowed_roles)) { return true; } } return false; } private function is_ip_whitelisted(): bool { $whitelist = $this->settings['whitelist_ips'] ?? []; if (empty($whitelist)) { return false; } $ip = $this->get_client_ip(); if ($ip === '') { return false; } return in_array($ip, $whitelist, true); } private function is_path_whitelisted(): bool { $paths = $this->settings['whitelist_paths'] ?? []; if (empty($paths)) { return false; } $request_uri = isset($_SERVER['REQUEST_URI']) ? (string) wp_unslash($_SERVER['REQUEST_URI']) : ''; $path = wp_parse_url($request_uri, PHP_URL_PATH); $path = $path ? '/' . ltrim($path, '/') : '/'; foreach ($paths as $allowed) { if ($allowed !== '/' && strpos($path, $allowed) === 0) { return true; } if ($allowed === $path) { return true; } } return false; } private function is_login_request(): bool { $request_uri = isset($_SERVER['REQUEST_URI']) ? (string) wp_unslash($_SERVER['REQUEST_URI']) : ''; return strpos($request_uri, 'wp-login.php') !== false || strpos($request_uri, 'wp-register.php') !== false; } private function is_rest_request(): bool { return defined('REST_REQUEST') && REST_REQUEST; } private function is_elementor_editor(): bool { if (isset($_GET['elementor-preview'])) { return true; } if (class_exists('\\Elementor\\Plugin')) { $plugin = \Elementor\Plugin::instance(); if ($plugin->editor && $plugin->editor->is_edit_mode()) { return true; } } return false; } private function render_maintenance_response(bool $is_preview): void { if (!defined('KING_ADDONS_IS_MAINTENANCE_PAGE')) { define('KING_ADDONS_IS_MAINTENANCE_PAGE', true); } $mode = $this->settings['mode'] ?? 'coming_soon'; $status = $mode === 'maintenance' ? 503 : 200; if ($is_preview) { $status = 200; } status_header($status); nocache_headers(); header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); $retry_after = (int) ($this->settings['retry_after'] ?? 0); if ($status === 503 && $retry_after > 0) { header('Retry-After: ' . $retry_after); } if (!empty($this->settings['noindex'])) { header('X-Robots-Tag: noindex, nofollow', true); } $title = $mode === 'maintenance' ? __('Maintenance Mode', 'king-addons') : __('Coming Soon', 'king-addons'); $content = $this->get_rendered_page_content(); $this->enqueue_frontend_assets(); echo ''; echo ''; echo '
'; echo ''; echo ''; echo '