PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.65
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.65
51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 51.1.46 51.1.47 51.1.49 All 37 releases
king-addons / includes / extensions / Maintenance_Mode / Maintenance_Mode.php

Maintenance_Mode.php in King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder 51.1.65, at includes/extensions/Maintenance_Mode/Maintenance_Mode.php

2,056 lines 69.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Maintenance Mode extension.
4 *
5 * @package King_Addons
6 */
7
8 namespace King_Addons\Maintenance_Mode;
9
10 if (!defined('ABSPATH')) {
11 exit;
12 }
13
14 class Maintenance_Mode
15 {
16 private const OPTION_NAME = 'kng_maintenance_settings';
17 private const ANALYTICS_OPTION = 'kng_maintenance_analytics';
18 private const ANALYTICS_TRANSIENT_24H = 'kng_maintenance_analytics_24h';
19
20 private const PRIVATE_ACCESS_COOKIE = 'kng_maintenance_private_access';
21 private const PRIVATE_ACCESS_QUERY_PARAM = 'kng_maintenance_token';
22 private const PRIVATE_ACCESS_POST_FLAG = 'kng_maintenance_private_submit';
23
24 private const DEFAULT_TIMEZONE = 'site';
25
26 private static ?Maintenance_Mode $instance = null;
27
28 /**
29 * Cached settings.
30 *
31 * @var array<string, mixed>
32 */
33 private array $settings = [];
34
35 private string $private_access_error = '';
36 private string $private_access_redirect_to = '';
37
38 public static function instance(): Maintenance_Mode
39 {
40 if (self::$instance === null) {
41 self::$instance = new self();
42 }
43
44 return self::$instance;
45 }
46
47 private function __construct()
48 {
49 $this->settings = $this->get_settings();
50
51 add_action('admin_menu', [$this, 'register_admin_menu']);
52 add_action('admin_init', [$this, 'register_settings']);
53 add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']);
54 add_action('template_redirect', [$this, 'maybe_render_maintenance'], 1);
55
56 add_action('admin_post_kng_maintenance_export', [$this, 'handle_export']);
57 add_action('admin_post_kng_maintenance_import', [$this, 'handle_import']);
58 add_action('admin_post_kng_maintenance_reset_analytics', [$this, 'handle_reset_analytics']);
59
60 add_action('admin_post_kng_maintenance_generate_token', [$this, 'handle_generate_private_token']);
61 add_action('admin_post_kng_maintenance_revoke_token', [$this, 'handle_revoke_private_token']);
62 add_action('admin_post_kng_maintenance_revoke_password', [$this, 'handle_revoke_private_password']);
63
64 add_shortcode('kng_maintenance_page', [$this, 'render_shortcode']);
65 }
66
67 public function register_admin_menu(): void
68 {
69 add_submenu_page(
70 'king-addons',
71 __('Maintenance Mode', 'king-addons'),
72 __('Maintenance Mode', 'king-addons'),
73 'manage_options',
74 'king-addons-maintenance-mode',
75 [$this, 'render_admin_page']
76 );
77 }
78
79 public function register_settings(): void
80 {
81 register_setting(
82 'kng_maintenance_settings_group',
83 self::OPTION_NAME,
84 [
85 'type' => 'array',
86 'sanitize_callback' => [$this, 'sanitize_settings'],
87 'default' => $this->get_default_settings(),
88 ]
89 );
90 }
91
92 public function enqueue_admin_assets(string $hook): void
93 {
94 if ($hook !== 'king-addons_page_king-addons-maintenance-mode') {
95 return;
96 }
97
98 $shared_css = KING_ADDONS_URL . 'includes/admin/layouts/shared/admin-v3-styles.css';
99 $shared_path = KING_ADDONS_PATH . 'includes/admin/layouts/shared/admin-v3-styles.css';
100 $shared_version = file_exists($shared_path) ? filemtime($shared_path) : KING_ADDONS_VERSION;
101 wp_enqueue_style('king-addons-admin-v3', $shared_css, [], $shared_version);
102
103 $admin_css = KING_ADDONS_URL . 'includes/extensions/Maintenance_Mode/assets/admin.css';
104 $admin_path = KING_ADDONS_PATH . 'includes/extensions/Maintenance_Mode/assets/admin.css';
105 $admin_version = file_exists($admin_path) ? filemtime($admin_path) : KING_ADDONS_VERSION;
106 wp_enqueue_style('king-addons-maintenance-admin', $admin_css, ['king-addons-admin-v3'], $admin_version);
107
108 $admin_js = KING_ADDONS_URL . 'includes/extensions/Maintenance_Mode/assets/admin.js';
109 $admin_path_js = KING_ADDONS_PATH . 'includes/extensions/Maintenance_Mode/assets/admin.js';
110 $admin_js_version = file_exists($admin_path_js) ? filemtime($admin_path_js) : KING_ADDONS_VERSION;
111 wp_enqueue_script('king-addons-maintenance-admin', $admin_js, ['jquery'], $admin_js_version, true);
112
113 wp_localize_script('king-addons-maintenance-admin', 'KNGMaintenance', [
114 'ajaxUrl' => admin_url('admin-ajax.php'),
115 'themeNonce' => wp_create_nonce('king_addons_dashboard_ui'),
116 ]);
117 }
118
119 public function render_admin_page(): void
120 {
121 if (!current_user_can('manage_options')) {
122 return;
123 }
124
125 $view = isset($_GET['view']) ? sanitize_key($_GET['view']) : 'dashboard';
126 $is_pro = $this->is_pro();
127 $settings = $this->get_settings();
128 $templates = $this->get_builtin_templates();
129
130 include __DIR__ . '/templates/admin-page.php';
131 }
132
133 public function maybe_render_maintenance(): void
134 {
135 if ($this->is_preview_request()) {
136 $this->render_maintenance_response(true);
137 exit;
138 }
139
140 if (!$this->is_mode_active()) {
141 return;
142 }
143
144 $bypass_reason = $this->get_bypass_reason();
145 if ($bypass_reason !== '') {
146 $this->track_bypass($bypass_reason);
147
148 if ($this->private_access_redirect_to !== '') {
149 wp_safe_redirect($this->private_access_redirect_to);
150 exit;
151 }
152 return;
153 }
154
155 $this->track_blocked_visit();
156 $this->render_maintenance_response(false);
157 exit;
158 }
159
160 public function handle_reset_analytics(): void
161 {
162 if (!current_user_can('manage_options')) {
163 wp_die(esc_html__('Unauthorized request.', 'king-addons'));
164 }
165
166 check_admin_referer('kng_maintenance_reset_analytics');
167
168 delete_option(self::ANALYTICS_OPTION);
169 delete_transient(self::ANALYTICS_TRANSIENT_24H);
170
171 $this->redirect_with_message('analytics', 'analytics_reset');
172 }
173
174 private function is_mode_active(): bool
175 {
176 if (empty($this->settings['enabled'])) {
177 return false;
178 }
179
180 if (empty($this->settings['schedule_enabled'])) {
181 return true;
182 }
183
184 return $this->is_any_schedule_window_active() || $this->is_any_recurring_rule_active();
185 }
186
187 private function is_any_schedule_window_active(): bool
188 {
189 $now = current_time('timestamp', true);
190
191 $windows = $this->settings['schedule_windows'] ?? [];
192 if (!is_array($windows)) {
193 $windows = [];
194 }
195
196 foreach ($windows as $window) {
197 if (!is_array($window)) {
198 continue;
199 }
200
201 $start = $this->parse_schedule_time((string) ($window['start'] ?? ''));
202 $end = $this->parse_schedule_time((string) ($window['end'] ?? ''));
203
204 if ($this->is_time_range_active($now, $start, $end)) {
205 return true;
206 }
207 }
208
209 $start = $this->parse_schedule_time($this->settings['schedule_start'] ?? '');
210 $end = $this->parse_schedule_time($this->settings['schedule_end'] ?? '');
211
212 return $this->is_time_range_active($now, $start, $end);
213 }
214
215 private function is_time_range_active(int $now, int $start, int $end): bool
216 {
217 if ($start && $end) {
218 return $now >= $start && $now <= $end;
219 }
220
221 if ($start && !$end) {
222 return $now >= $start;
223 }
224
225 if (!$start && $end) {
226 return $now <= $end;
227 }
228
229 return false;
230 }
231
232 private function is_any_recurring_rule_active(): bool
233 {
234 if (empty($this->settings['recurring_enabled'])) {
235 return false;
236 }
237
238 $rules = $this->settings['recurring_rules'] ?? [];
239 if (!is_array($rules) || $rules === []) {
240 return false;
241 }
242
243 $nowTs = current_time('timestamp', true);
244 $nowUtc = (new \DateTimeImmutable('@' . $nowTs))->setTimezone(new \DateTimeZone('UTC'));
245
246 foreach ($rules as $rule) {
247 if (!is_array($rule)) {
248 continue;
249 }
250
251 if ($this->is_recurring_rule_active($rule, $nowUtc)) {
252 return true;
253 }
254 }
255
256 return false;
257 }
258
259 private function is_recurring_rule_active(array $rule, \DateTimeImmutable $nowUtc): bool
260 {
261 $freq = isset($rule['frequency']) ? sanitize_key((string) $rule['frequency']) : '';
262 if (!in_array($freq, ['daily', 'weekly', 'monthly'], true)) {
263 return false;
264 }
265
266 $tzString = isset($rule['timezone']) ? sanitize_text_field((string) $rule['timezone']) : self::DEFAULT_TIMEZONE;
267 $tz = $this->resolve_timezone($tzString);
268 $local = $nowUtc->setTimezone($tz);
269
270 $startMinutes = $this->parse_time_minutes((string) ($rule['start_time'] ?? ''));
271 $endMinutes = $this->parse_time_minutes((string) ($rule['end_time'] ?? ''));
272 if ($startMinutes < 0 || $endMinutes < 0) {
273 return false;
274 }
275
276 $nowMinutes = ((int) $local->format('G')) * 60 + (int) $local->format('i');
277 $inTime = $this->is_minutes_in_range($nowMinutes, $startMinutes, $endMinutes);
278 if (!$inTime) {
279 return false;
280 }
281
282 if ($freq === 'daily') {
283 return true;
284 }
285
286 if ($freq === 'weekly') {
287 $days = $rule['days_of_week'] ?? [];
288 if (!is_array($days) || $days === []) {
289 return false;
290 }
291
292 $dow = (int) $local->format('N');
293 return in_array($dow, array_map('intval', $days), true);
294 }
295
296 $days = $rule['days_of_month'] ?? [];
297 if (!is_array($days) || $days === []) {
298 return false;
299 }
300
301 $dom = (int) $local->format('j');
302 return in_array($dom, array_map('intval', $days), true);
303 }
304
305 private function is_minutes_in_range(int $value, int $start, int $end): bool
306 {
307 if ($start === $end) {
308 return false;
309 }
310
311 if ($start < $end) {
312 return $value >= $start && $value <= $end;
313 }
314
315 return $value >= $start || $value <= $end;
316 }
317
318 private function parse_time_minutes(string $value): int
319 {
320 $value = trim($value);
321 if (!preg_match('/^(\d{1,2}):(\d{2})$/', $value, $m)) {
322 return -1;
323 }
324
325 $h = (int) $m[1];
326 $i = (int) $m[2];
327 if ($h < 0 || $h > 23 || $i < 0 || $i > 59) {
328 return -1;
329 }
330
331 return $h * 60 + $i;
332 }
333
334 private function resolve_timezone(string $timezone): \DateTimeZone
335 {
336 $timezone = $timezone !== '' ? $timezone : self::DEFAULT_TIMEZONE;
337 if ($timezone === self::DEFAULT_TIMEZONE) {
338 return function_exists('wp_timezone') ? wp_timezone() : new \DateTimeZone('UTC');
339 }
340
341 try {
342 return new \DateTimeZone($timezone);
343 } catch (\Exception $e) {
344 return function_exists('wp_timezone') ? wp_timezone() : new \DateTimeZone('UTC');
345 }
346 }
347
348 private function should_bypass_request(): bool
349 {
350 return $this->get_bypass_reason() !== '';
351 }
352
353 private function get_bypass_reason(): string
354 {
355 if (defined('WP_CLI') && WP_CLI) {
356 return 'wp_cli';
357 }
358
359 if (wp_doing_cron()) {
360 return 'cron';
361 }
362
363 if (is_admin()) {
364 return 'wp_admin';
365 }
366
367 if (wp_doing_ajax() && !empty($this->settings['allow_admin_ajax'])) {
368 return 'admin_ajax_allowed';
369 }
370
371 if ($this->is_login_request()) {
372 return 'login_page';
373 }
374
375 if (!empty($this->settings['disable_elementor_editor']) && $this->is_elementor_editor()) {
376 return 'elementor_editor';
377 }
378
379 $private_reason = $this->get_private_access_bypass_reason();
380 if ($private_reason !== '') {
381 return $private_reason;
382 }
383
384 if ($this->is_rest_request()) {
385 if (is_user_logged_in()) {
386 return 'rest_logged_in';
387 }
388 return !empty($this->settings['allow_rest']) ? 'rest_allowed' : '';
389 }
390
391 if ($this->is_user_allowed()) {
392 return 'user_allowed';
393 }
394
395 if ($this->is_ip_whitelisted()) {
396 return 'ip_whitelist';
397 }
398
399 if ($this->is_path_whitelisted()) {
400 return 'path_whitelist';
401 }
402
403 return '';
404 }
405
406 private function is_private_access_enabled(): bool
407 {
408 if (!$this->is_pro()) {
409 return false;
410 }
411
412 return !empty($this->settings['private_password_hash']) || !empty($this->settings['private_token']);
413 }
414
415 private function get_private_access_bypass_reason(): string
416 {
417 if (!$this->is_private_access_enabled()) {
418 return '';
419 }
420
421 if ($this->has_private_access_cookie()) {
422 return 'private_cookie';
423 }
424
425 $token = isset($_GET[self::PRIVATE_ACCESS_QUERY_PARAM])
426 ? sanitize_text_field(wp_unslash($_GET[self::PRIVATE_ACCESS_QUERY_PARAM]))
427 : '';
428
429 if ($token !== '' && $this->is_private_token_valid($token)) {
430 $this->set_private_access_cookie();
431 $this->private_access_redirect_to = $this->get_current_url_without_private_params();
432 return 'private_token';
433 }
434
435 if ($this->maybe_accept_private_password()) {
436 $this->private_access_redirect_to = $this->get_current_url_without_private_params();
437 return 'private_password';
438 }
439
440 return '';
441 }
442
443 private function maybe_accept_private_password(): bool
444 {
445 if (empty($this->settings['private_password_hash'])) {
446 return false;
447 }
448
449 if (!isset($_POST[self::PRIVATE_ACCESS_POST_FLAG])) {
450 return false;
451 }
452
453 $nonce = isset($_POST['_kng_private_nonce']) ? sanitize_text_field(wp_unslash($_POST['_kng_private_nonce'])) : '';
454 if ($nonce === '' || !wp_verify_nonce($nonce, 'kng_maintenance_private_access')) {
455 $this->private_access_error = 'invalid_nonce';
456 return false;
457 }
458
459 $password = isset($_POST['kng_maintenance_private_password'])
460 ? (string) wp_unslash($_POST['kng_maintenance_private_password'])
461 : '';
462
463 $password = trim($password);
464 if ($password === '') {
465 $this->private_access_error = 'invalid_password';
466 return false;
467 }
468
469 $hash = (string) ($this->settings['private_password_hash'] ?? '');
470 if ($hash === '' || !function_exists('wp_check_password')) {
471 $this->private_access_error = 'invalid_password';
472 return false;
473 }
474
475 if (!wp_check_password($password, $hash)) {
476 $this->private_access_error = 'invalid_password';
477 return false;
478 }
479
480 $this->set_private_access_cookie();
481 return true;
482 }
483
484 private function is_private_token_valid(string $token): bool
485 {
486 $saved = (string) ($this->settings['private_token'] ?? '');
487 if ($saved === '' || $token === '') {
488 return false;
489 }
490
491 return hash_equals($saved, $token);
492 }
493
494 private function get_private_access_cookie_expected(): string
495 {
496 $key = defined('AUTH_SALT') && AUTH_SALT ? AUTH_SALT : (defined('NONCE_SALT') && NONCE_SALT ? NONCE_SALT : 'kng');
497 $hash = (string) ($this->settings['private_password_hash'] ?? '');
498 $token = (string) ($this->settings['private_token'] ?? '');
499 $material = $hash . '|' . $token;
500 return hash_hmac('sha256', 'private_access|' . $material, $key);
501 }
502
503 private function has_private_access_cookie(): bool
504 {
505 $expected = $this->get_private_access_cookie_expected();
506 if ($expected === '') {
507 return false;
508 }
509
510 $cookie = isset($_COOKIE[self::PRIVATE_ACCESS_COOKIE]) ? (string) wp_unslash($_COOKIE[self::PRIVATE_ACCESS_COOKIE]) : '';
511 if ($cookie === '') {
512 return false;
513 }
514
515 return hash_equals($expected, $cookie);
516 }
517
518 private function set_private_access_cookie(): void
519 {
520 $value = $this->get_private_access_cookie_expected();
521 if ($value === '') {
522 return;
523 }
524
525 $expires = time() + (int) apply_filters('kng_maintenance_private_cookie_ttl', 7 * DAY_IN_SECONDS);
526
527 $path = defined('COOKIEPATH') ? (string) COOKIEPATH : '/';
528 if ($path === '') {
529 $path = '/';
530 }
531
532 $args = [
533 'expires' => $expires,
534 'path' => $path,
535 'secure' => is_ssl(),
536 'httponly' => true,
537 'samesite' => 'Lax',
538 ];
539
540 $domain = defined('COOKIE_DOMAIN') ? (string) COOKIE_DOMAIN : '';
541 if ($domain !== '') {
542 $args['domain'] = $domain;
543 }
544
545 setcookie(self::PRIVATE_ACCESS_COOKIE, $value, $args);
546 $_COOKIE[self::PRIVATE_ACCESS_COOKIE] = $value;
547 }
548
549 private function get_current_url_without_private_params(): string
550 {
551 $request_uri = isset($_SERVER['REQUEST_URI']) ? (string) wp_unslash($_SERVER['REQUEST_URI']) : '/';
552 $url = home_url($request_uri);
553 $url = remove_query_arg([self::PRIVATE_ACCESS_QUERY_PARAM], $url);
554 return $url;
555 }
556
557 private function track_blocked_visit(): void
558 {
559 if ($this->is_preview_request()) {
560 return;
561 }
562
563 if (is_admin() || wp_doing_cron() || (defined('WP_CLI') && WP_CLI)) {
564 return;
565 }
566
567 $analytics = $this->get_analytics();
568 $analytics['blocked_total'] = (int) ($analytics['blocked_total'] ?? 0) + 1;
569 $analytics['updated_at'] = time();
570 update_option(self::ANALYTICS_OPTION, $analytics, false);
571
572 $this->update_24h_analytics('blocked', '', $this->get_masked_request_path());
573
574 $ip = $this->get_client_ip();
575 if ($ip !== '') {
576 $this->update_24h_unique_ip($ip);
577 }
578 }
579
580 private function track_bypass(string $reason = ''): void
581 {
582 if ($this->is_preview_request()) {
583 return;
584 }
585
586 if (is_admin() || wp_doing_cron() || (defined('WP_CLI') && WP_CLI)) {
587 return;
588 }
589
590 $analytics = $this->get_analytics();
591 $analytics['bypass_total'] = (int) ($analytics['bypass_total'] ?? 0) + 1;
592
593 if ($reason !== '') {
594 if (!isset($analytics['bypass_by_reason']) || !is_array($analytics['bypass_by_reason'])) {
595 $analytics['bypass_by_reason'] = [];
596 }
597 $analytics['bypass_by_reason'][$reason] = (int) ($analytics['bypass_by_reason'][$reason] ?? 0) + 1;
598 }
599
600 $analytics['updated_at'] = time();
601 update_option(self::ANALYTICS_OPTION, $analytics, false);
602
603 $this->update_24h_analytics('bypass', $reason, $this->get_masked_request_path());
604 }
605
606 private function get_analytics(): array
607 {
608 $saved = get_option(self::ANALYTICS_OPTION, []);
609 if (!is_array($saved)) {
610 $saved = [];
611 }
612
613 $defaults = [
614 'blocked_total' => 0,
615 'bypass_total' => 0,
616 'bypass_by_reason' => [],
617 'created_at' => time(),
618 'updated_at' => time(),
619 ];
620
621 $data = wp_parse_args($saved, $defaults);
622 if (!is_array($data['bypass_by_reason'])) {
623 $data['bypass_by_reason'] = [];
624 }
625
626 return $data;
627 }
628
629 private function get_analytics_24h(): array
630 {
631 $saved = get_transient(self::ANALYTICS_TRANSIENT_24H);
632 if (!is_array($saved)) {
633 $saved = [];
634 }
635
636 $defaults = [
637 'blocked' => 0,
638 'bypass' => 0,
639 'bypass_by_reason' => [],
640 'unique' => [],
641 'paths' => [
642 'blocked' => [],
643 'bypass' => [],
644 ],
645 ];
646
647 $data = wp_parse_args($saved, $defaults);
648 if (!is_array($data['unique'])) {
649 $data['unique'] = [];
650 }
651
652 if (!is_array($data['bypass_by_reason'])) {
653 $data['bypass_by_reason'] = [];
654 }
655
656 if (!isset($data['paths']) || !is_array($data['paths'])) {
657 $data['paths'] = ['blocked' => [], 'bypass' => []];
658 }
659 if (!isset($data['paths']['blocked']) || !is_array($data['paths']['blocked'])) {
660 $data['paths']['blocked'] = [];
661 }
662 if (!isset($data['paths']['bypass']) || !is_array($data['paths']['bypass'])) {
663 $data['paths']['bypass'] = [];
664 }
665
666 $this->prune_24h_unique($data);
667 $this->prune_24h_paths($data);
668
669 return $data;
670 }
671
672 private function save_analytics_24h(array $data): void
673 {
674 $this->prune_24h_unique($data);
675 $this->prune_24h_paths($data);
676 set_transient(self::ANALYTICS_TRANSIENT_24H, $data, DAY_IN_SECONDS + HOUR_IN_SECONDS);
677 }
678
679 private function prune_24h_unique(array &$data): void
680 {
681 $cutoff = time() - DAY_IN_SECONDS;
682 if (!isset($data['unique']) || !is_array($data['unique'])) {
683 $data['unique'] = [];
684 return;
685 }
686
687 foreach ($data['unique'] as $hash => $ts) {
688 if ((int) $ts < $cutoff) {
689 unset($data['unique'][$hash]);
690 }
691 }
692
693 $max = 10000;
694 if (count($data['unique']) > $max) {
695 $data['unique'] = array_slice($data['unique'], -$max, null, true);
696 }
697 }
698
699 private function prune_24h_paths(array &$data): void
700 {
701 $cutoff = time() - DAY_IN_SECONDS;
702 if (!isset($data['paths']) || !is_array($data['paths'])) {
703 $data['paths'] = ['blocked' => [], 'bypass' => []];
704 return;
705 }
706
707 foreach (['blocked', 'bypass'] as $bucket) {
708 if (!isset($data['paths'][$bucket]) || !is_array($data['paths'][$bucket])) {
709 $data['paths'][$bucket] = [];
710 continue;
711 }
712
713 foreach ($data['paths'][$bucket] as $hash => $row) {
714 $ts = is_array($row) ? (int) ($row['t'] ?? 0) : 0;
715 if ($ts < $cutoff) {
716 unset($data['paths'][$bucket][$hash]);
717 }
718 }
719
720 $max = 400;
721 if (count($data['paths'][$bucket]) > $max) {
722 uasort($data['paths'][$bucket], static function ($a, $b) {
723 $ta = is_array($a) ? (int) ($a['t'] ?? 0) : 0;
724 $tb = is_array($b) ? (int) ($b['t'] ?? 0) : 0;
725 return $ta <=> $tb;
726 });
727 $data['paths'][$bucket] = array_slice($data['paths'][$bucket], -$max, null, true);
728 }
729 }
730 }
731
732 private function update_24h_analytics(string $type, string $reason = '', string $masked_path = ''): void
733 {
734 $data = $this->get_analytics_24h();
735 if ($type === 'blocked') {
736 $data['blocked'] = (int) ($data['blocked'] ?? 0) + 1;
737 } elseif ($type === 'bypass') {
738 $data['bypass'] = (int) ($data['bypass'] ?? 0) + 1;
739 if ($reason !== '') {
740 if (!isset($data['bypass_by_reason'][$reason])) {
741 $data['bypass_by_reason'][$reason] = 0;
742 }
743 $data['bypass_by_reason'][$reason] = (int) $data['bypass_by_reason'][$reason] + 1;
744 }
745 }
746
747 if ($masked_path !== '' && in_array($type, ['blocked', 'bypass'], true)) {
748 if (!isset($data['paths']) || !is_array($data['paths'])) {
749 $data['paths'] = ['blocked' => [], 'bypass' => []];
750 }
751 if (!isset($data['paths'][$type]) || !is_array($data['paths'][$type])) {
752 $data['paths'][$type] = [];
753 }
754
755 $hash = $this->hash_string($masked_path);
756 if ($hash !== '') {
757 if (!isset($data['paths'][$type][$hash]) || !is_array($data['paths'][$type][$hash])) {
758 $data['paths'][$type][$hash] = ['c' => 0, 'm' => $masked_path, 't' => time()];
759 }
760 $data['paths'][$type][$hash]['c'] = (int) ($data['paths'][$type][$hash]['c'] ?? 0) + 1;
761 $data['paths'][$type][$hash]['m'] = $masked_path;
762 $data['paths'][$type][$hash]['t'] = time();
763 }
764 }
765
766 $this->save_analytics_24h($data);
767 }
768
769 private function hash_string(string $value): string
770 {
771 $value = trim($value);
772 if ($value === '') {
773 return '';
774 }
775
776 $key = defined('AUTH_SALT') && AUTH_SALT ? AUTH_SALT : (defined('NONCE_SALT') && NONCE_SALT ? NONCE_SALT : 'kng');
777 return hash_hmac('sha256', $value, $key);
778 }
779
780 private function get_request_path(): string
781 {
782 $request_uri = isset($_SERVER['REQUEST_URI']) ? (string) wp_unslash($_SERVER['REQUEST_URI']) : '';
783 $path = wp_parse_url($request_uri, PHP_URL_PATH);
784 $path = $path ? '/' . ltrim((string) $path, '/') : '/';
785 return $path;
786 }
787
788 private function get_masked_request_path(): string
789 {
790 return $this->mask_path($this->get_request_path());
791 }
792
793 private function mask_path(string $path): string
794 {
795 $path = $path !== '' ? '/' . ltrim($path, '/') : '/';
796 $trim = trim($path, '/');
797 if ($trim === '') {
798 return '/';
799 }
800
801 $segments = array_values(array_filter(explode('/', $trim), static fn($s) => $s !== ''));
802 $maxSegments = 2;
803 $shown = array_slice($segments, 0, $maxSegments);
804
805 $out = [];
806 foreach ($shown as $seg) {
807 $seg = rawurldecode((string) $seg);
808 $seg = preg_replace('/\s+/', '-', $seg);
809
810 if ($seg === null || $seg === '') {
811 continue;
812 }
813
814 if (preg_match('/^[0-9]+$/', $seg)) {
815 $out[] = '{n}';
816 continue;
817 }
818
819 if (preg_match('/^[a-f0-9]{16,}$/i', $seg)) {
820 $out[] = '{hash}';
821 continue;
822 }
823
824 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)) {
825 $out[] = '{uuid}';
826 continue;
827 }
828
829 $seg = preg_replace('/\d+/', '{n}', $seg);
830 $seg = preg_replace('/[^a-zA-Z0-9._\-{}]+/', '', (string) $seg);
831 $seg = substr((string) $seg, 0, 24);
832 if ($seg === '') {
833 $seg = '{seg}';
834 }
835 $out[] = $seg;
836 }
837
838 $masked = '/' . implode('/', $out);
839 if (count($segments) > $maxSegments) {
840 $masked .= '/…';
841 }
842
843 return substr($masked, 0, 80);
844 }
845
846 private function update_24h_unique_ip(string $ip): void
847 {
848 $hash = $this->hash_ip($ip);
849 if ($hash === '') {
850 return;
851 }
852
853 $data = $this->get_analytics_24h();
854 $data['unique'][$hash] = time();
855 $this->save_analytics_24h($data);
856 }
857
858 private function hash_ip(string $ip): string
859 {
860 $ip = trim($ip);
861 if ($ip === '') {
862 return '';
863 }
864
865 $key = defined('AUTH_SALT') && AUTH_SALT ? AUTH_SALT : (defined('NONCE_SALT') && NONCE_SALT ? NONCE_SALT : 'kng');
866 return hash_hmac('sha256', $ip, $key);
867 }
868
869 private function get_analytics_overview(): array
870 {
871 $all = $this->get_analytics();
872 $h24 = $this->get_analytics_24h();
873
874 $bypassAll = $all['bypass_by_reason'] ?? [];
875 if (!is_array($bypassAll)) {
876 $bypassAll = [];
877 }
878
879 $bypass24 = $h24['bypass_by_reason'] ?? [];
880 if (!is_array($bypass24)) {
881 $bypass24 = [];
882 }
883
884 arsort($bypassAll);
885 arsort($bypass24);
886
887 $topBlocked = $this->get_top_paths_24h($h24, 'blocked');
888 $topBypass = $this->get_top_paths_24h($h24, 'bypass');
889
890 return [
891 'blocked_total' => (int) ($all['blocked_total'] ?? 0),
892 'bypass_total' => (int) ($all['bypass_total'] ?? 0),
893 'blocked_24h' => (int) ($h24['blocked'] ?? 0),
894 'bypass_24h' => (int) ($h24['bypass'] ?? 0),
895 'unique_24h' => is_array($h24['unique'] ?? null) ? count($h24['unique']) : 0,
896 'bypass_by_reason_total' => $bypassAll,
897 'bypass_by_reason_24h' => $bypass24,
898 'top_paths_24h_blocked' => $topBlocked,
899 'top_paths_24h_bypass' => $topBypass,
900 ];
901 }
902
903 private function get_top_paths_24h(array $h24, string $bucket, int $limit = 10): array
904 {
905 if (!isset($h24['paths']) || !is_array($h24['paths'])) {
906 return [];
907 }
908 if (!isset($h24['paths'][$bucket]) || !is_array($h24['paths'][$bucket])) {
909 return [];
910 }
911
912 $rows = [];
913 foreach ($h24['paths'][$bucket] as $row) {
914 if (!is_array($row)) {
915 continue;
916 }
917 $count = (int) ($row['c'] ?? 0);
918 $mask = isset($row['m']) ? (string) $row['m'] : '';
919 if ($count <= 0 || $mask === '') {
920 continue;
921 }
922 $rows[] = ['mask' => $mask, 'count' => $count];
923 }
924
925 usort($rows, static function ($a, $b) {
926 $ca = (int) ($a['count'] ?? 0);
927 $cb = (int) ($b['count'] ?? 0);
928 if ($ca === $cb) {
929 return strcmp((string) ($a['mask'] ?? ''), (string) ($b['mask'] ?? ''));
930 }
931 return $cb <=> $ca;
932 });
933
934 return array_slice($rows, 0, $limit);
935 }
936
937 private function is_user_allowed(): bool
938 {
939 if (!is_user_logged_in()) {
940 return false;
941 }
942
943 $user = wp_get_current_user();
944 if (!$user || !$user->ID) {
945 return false;
946 }
947
948 if (!empty($this->settings['exclude_admin']) && user_can($user, 'manage_options')) {
949 return true;
950 }
951
952 if ($this->is_pro()) {
953 $allowed_roles = $this->settings['allowed_roles'] ?? [];
954 if (!empty($allowed_roles) && array_intersect((array) $user->roles, $allowed_roles)) {
955 return true;
956 }
957 }
958
959 return false;
960 }
961
962 private function is_ip_whitelisted(): bool
963 {
964 $whitelist = $this->settings['whitelist_ips'] ?? [];
965 if (empty($whitelist)) {
966 return false;
967 }
968
969 $ip = $this->get_client_ip();
970 if ($ip === '') {
971 return false;
972 }
973
974 return in_array($ip, $whitelist, true);
975 }
976
977 private function is_path_whitelisted(): bool
978 {
979 $paths = $this->settings['whitelist_paths'] ?? [];
980 if (empty($paths)) {
981 return false;
982 }
983
984 $request_uri = isset($_SERVER['REQUEST_URI']) ? (string) wp_unslash($_SERVER['REQUEST_URI']) : '';
985 $path = wp_parse_url($request_uri, PHP_URL_PATH);
986 $path = $path ? '/' . ltrim($path, '/') : '/';
987
988 foreach ($paths as $allowed) {
989 if ($allowed !== '/' && strpos($path, $allowed) === 0) {
990 return true;
991 }
992 if ($allowed === $path) {
993 return true;
994 }
995 }
996
997 return false;
998 }
999
1000 private function is_login_request(): bool
1001 {
1002 $request_uri = isset($_SERVER['REQUEST_URI']) ? (string) wp_unslash($_SERVER['REQUEST_URI']) : '';
1003 return strpos($request_uri, 'wp-login.php') !== false || strpos($request_uri, 'wp-register.php') !== false;
1004 }
1005
1006 private function is_rest_request(): bool
1007 {
1008 return defined('REST_REQUEST') && REST_REQUEST;
1009 }
1010
1011 private function is_elementor_editor(): bool
1012 {
1013 if (isset($_GET['elementor-preview'])) {
1014 return true;
1015 }
1016
1017 if (class_exists('\\Elementor\\Plugin')) {
1018 $plugin = \Elementor\Plugin::instance();
1019 if ($plugin->editor && $plugin->editor->is_edit_mode()) {
1020 return true;
1021 }
1022 }
1023
1024 return false;
1025 }
1026
1027 private function render_maintenance_response(bool $is_preview): void
1028 {
1029 if (!defined('KING_ADDONS_IS_MAINTENANCE_PAGE')) {
1030 define('KING_ADDONS_IS_MAINTENANCE_PAGE', true);
1031 }
1032
1033 $mode = $this->settings['mode'] ?? 'coming_soon';
1034 $status = $mode === 'maintenance' ? 503 : 200;
1035
1036 if ($is_preview) {
1037 $status = 200;
1038 }
1039
1040 status_header($status);
1041 nocache_headers();
1042 header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
1043
1044 $retry_after = (int) ($this->settings['retry_after'] ?? 0);
1045 if ($status === 503 && $retry_after > 0) {
1046 header('Retry-After: ' . $retry_after);
1047 }
1048
1049 if (!empty($this->settings['noindex'])) {
1050 header('X-Robots-Tag: noindex, nofollow', true);
1051 }
1052
1053 $title = $mode === 'maintenance' ? __('Maintenance Mode', 'king-addons') : __('Coming Soon', 'king-addons');
1054 $content = $this->get_rendered_page_content();
1055
1056 $this->enqueue_frontend_assets();
1057
1058 echo '<!doctype html>';
1059 echo '<html ' . get_language_attributes() . '>';
1060 echo '<head>';
1061 echo '<meta charset="' . esc_attr(get_bloginfo('charset')) . '">';
1062 echo '<meta name="viewport" content="width=device-width, initial-scale=1">';
1063 echo '<title>' . esc_html($title) . '</title>';
1064 if (!empty($this->settings['noindex'])) {
1065 echo '<meta name="robots" content="noindex, nofollow">';
1066 }
1067 wp_head();
1068 echo '</head>';
1069 echo '<body class="kng-maintenance-body">';
1070 echo $content;
1071 wp_footer();
1072 echo '</body></html>';
1073 }
1074
1075 private function enqueue_frontend_assets(): void
1076 {
1077 wp_enqueue_style(
1078 'king-addons-maintenance-frontend',
1079 KING_ADDONS_URL . 'includes/extensions/Maintenance_Mode/assets/frontend.css',
1080 [],
1081 KING_ADDONS_VERSION
1082 );
1083
1084 wp_enqueue_script(
1085 'king-addons-maintenance-frontend',
1086 KING_ADDONS_URL . 'includes/extensions/Maintenance_Mode/assets/frontend.js',
1087 [],
1088 KING_ADDONS_VERSION,
1089 true
1090 );
1091 }
1092
1093 private function get_rendered_page_content(): string
1094 {
1095 $theme = $this->settings['theme'] ?? 'dark';
1096 $mode = $this->settings['mode'] ?? 'coming_soon';
1097 $template_source = $this->settings['template_source'] ?? 'built_in';
1098
1099 $wrapper_classes = [
1100 'kng-maintenance',
1101 $theme === 'light' ? 'kng-maintenance-theme-light' : 'kng-maintenance-theme-dark',
1102 $mode === 'maintenance' ? 'kng-maintenance-mode-maintenance' : 'kng-maintenance-mode-coming-soon',
1103 ];
1104
1105 $content = '';
1106 if ($template_source === 'page' && !empty($this->settings['page_id'])) {
1107 $content = $this->get_page_content((int) $this->settings['page_id']);
1108 } elseif ($template_source === 'elementor' && !empty($this->settings['elementor_id'])) {
1109 $content = $this->get_elementor_content((int) $this->settings['elementor_id']);
1110 } else {
1111 $content = $this->render_builtin_template((string) $this->settings['template_id']);
1112 }
1113
1114 $private_panel = $this->render_private_access_panel();
1115
1116 return '<main class="' . esc_attr(implode(' ', $wrapper_classes)) . '">' . $content . $private_panel . '</main>';
1117 }
1118
1119 private function render_private_access_panel(): string
1120 {
1121 if (!$this->is_private_access_enabled()) {
1122 return '';
1123 }
1124
1125 if (empty($this->settings['private_password_hash'])) {
1126 return '';
1127 }
1128
1129 $error = $this->private_access_error;
1130
1131 ob_start();
1132 ?>
1133 <details class="kng-maintenance-private-access" <?php echo $error !== '' ? 'open' : ''; ?>>
1134 <summary><?php esc_html_e('Private access', 'king-addons'); ?></summary>
1135 <div class="kng-maintenance-private-access-body">
1136 <?php if ($error === 'invalid_password') : ?>
1137 <div class="kng-maintenance-private-access-error"><?php esc_html_e('Incorrect password. Please try again.', 'king-addons'); ?></div>
1138 <?php elseif ($error === 'invalid_nonce') : ?>
1139 <div class="kng-maintenance-private-access-error"><?php esc_html_e('Session expired. Please try again.', 'king-addons'); ?></div>
1140 <?php endif; ?>
1141
1142 <form method="post" class="kng-maintenance-private-access-form">
1143 <?php wp_nonce_field('kng_maintenance_private_access', '_kng_private_nonce'); ?>
1144 <input type="password" name="kng_maintenance_private_password" placeholder="<?php echo esc_attr__('Password', 'king-addons'); ?>" autocomplete="current-password">
1145 <button type="submit" name="<?php echo esc_attr(self::PRIVATE_ACCESS_POST_FLAG); ?>" value="1"><?php esc_html_e('Enter', 'king-addons'); ?></button>
1146 </form>
1147 </div>
1148 </details>
1149 <?php
1150 return (string) ob_get_clean();
1151 }
1152
1153 public function handle_generate_private_token(): void
1154 {
1155 if (!current_user_can('manage_options')) {
1156 wp_die(esc_html__('Unauthorized request.', 'king-addons'));
1157 }
1158
1159 check_admin_referer('kng_maintenance_private_token');
1160
1161 $settings = $this->get_settings();
1162 try {
1163 $settings['private_token'] = bin2hex(random_bytes(16));
1164 } catch (\Exception $e) {
1165 $settings['private_token'] = wp_generate_password(32, false, false);
1166 }
1167
1168 update_option(self::OPTION_NAME, $settings, false);
1169 $this->redirect_with_message('mode', 'token_generated');
1170 }
1171
1172 public function handle_revoke_private_token(): void
1173 {
1174 if (!current_user_can('manage_options')) {
1175 wp_die(esc_html__('Unauthorized request.', 'king-addons'));
1176 }
1177
1178 check_admin_referer('kng_maintenance_private_token');
1179
1180 $settings = $this->get_settings();
1181 $settings['private_token'] = '';
1182 update_option(self::OPTION_NAME, $settings, false);
1183
1184 $this->redirect_with_message('mode', 'token_revoked');
1185 }
1186
1187 public function handle_revoke_private_password(): void
1188 {
1189 if (!current_user_can('manage_options')) {
1190 wp_die(esc_html__('Unauthorized request.', 'king-addons'));
1191 }
1192
1193 check_admin_referer('kng_maintenance_private_password');
1194
1195 $settings = $this->get_settings();
1196
1197 // update_option() will call the registered sanitize callback.
1198 // Ensure we provide the same intent flag used by sanitize_settings().
1199 $settings['private_password_remove'] = 1;
1200 $settings['private_password'] = '';
1201 update_option(self::OPTION_NAME, $settings, false);
1202
1203 $this->redirect_with_message('mode', 'password_revoked');
1204 }
1205
1206 public function render_shortcode(array $atts): string
1207 {
1208 $atts = shortcode_atts([
1209 'id' => '',
1210 'type' => '',
1211 'theme' => '',
1212 'full_height' => 'false',
1213 ], $atts, 'kng_maintenance_page');
1214
1215 $theme = $atts['theme'] !== '' ? sanitize_key($atts['theme']) : ($this->settings['theme'] ?? 'dark');
1216 $theme = $theme === 'light' ? 'light' : 'dark';
1217
1218 $template_source = $this->settings['template_source'] ?? 'built_in';
1219 $template_id = $this->settings['template_id'] ?? 'minimal';
1220 $page_id = (int) ($this->settings['page_id'] ?? 0);
1221 $elementor_id = (int) ($this->settings['elementor_id'] ?? 0);
1222
1223 if ($atts['id'] !== '') {
1224 if (is_numeric($atts['id'])) {
1225 $template_source = 'page';
1226 $page_id = absint($atts['id']);
1227 } else {
1228 $template_source = 'built_in';
1229 $template_id = sanitize_key($atts['id']);
1230 }
1231 }
1232
1233 if ($atts['type'] !== '') {
1234 $template_source = sanitize_key($atts['type']);
1235 if ($template_source === 'elementor' && is_numeric($atts['id'])) {
1236 $elementor_id = absint($atts['id']);
1237 }
1238 }
1239
1240 $content = '';
1241 if ($template_source === 'page' && $page_id) {
1242 $content = $this->get_page_content($page_id);
1243 } elseif ($template_source === 'elementor' && $elementor_id) {
1244 $content = $this->get_elementor_content($elementor_id);
1245 } else {
1246 $content = $this->render_builtin_template($this->ensure_template_allowed($template_id));
1247 }
1248
1249 $this->enqueue_frontend_assets();
1250
1251 $classes = [
1252 'kng-maintenance',
1253 $theme === 'light' ? 'kng-maintenance-theme-light' : 'kng-maintenance-theme-dark',
1254 ];
1255
1256 if (filter_var($atts['full_height'], FILTER_VALIDATE_BOOLEAN)) {
1257 $classes[] = 'kng-maintenance-full';
1258 }
1259
1260 return '<div class="' . esc_attr(implode(' ', $classes)) . '">' . $content . '</div>';
1261 }
1262
1263 private function get_page_content(int $page_id): string
1264 {
1265 $page = get_post($page_id);
1266 if (!$page) {
1267 return $this->render_builtin_template('minimal');
1268 }
1269
1270 $old_post = $GLOBALS['post'] ?? null;
1271 $GLOBALS['post'] = $page;
1272 setup_postdata($page);
1273
1274 $content = apply_filters('the_content', $page->post_content);
1275
1276 wp_reset_postdata();
1277 $GLOBALS['post'] = $old_post;
1278
1279 return $content;
1280 }
1281
1282 private function get_elementor_content(int $template_id): string
1283 {
1284 if (!class_exists('\\Elementor\\Plugin')) {
1285 return $this->render_builtin_template('minimal');
1286 }
1287
1288 $plugin = \Elementor\Plugin::instance();
1289 if (!$plugin->frontend) {
1290 return $this->render_builtin_template('minimal');
1291 }
1292
1293 $plugin->frontend->enqueue_styles();
1294 $plugin->frontend->enqueue_scripts();
1295
1296 return $plugin->frontend->get_builder_content_for_display($template_id, true);
1297 }
1298
1299 private function render_builtin_template(string $template_id): string
1300 {
1301 $templates = $this->get_builtin_templates();
1302 $template_id = isset($templates[$template_id]) ? $template_id : 'minimal';
1303 $template_id = $this->ensure_template_allowed($template_id);
1304
1305 $mode = $this->settings['mode'] ?? 'coming_soon';
1306 $site_name = get_bloginfo('name');
1307 $tagline = get_bloginfo('description');
1308 $schedule_end = $this->settings['schedule_end'] ?? '';
1309 $content = $this->get_template_content($template_id);
1310
1311 $headline = $mode === 'maintenance'
1312 ? __('We are upgrading the experience.', 'king-addons')
1313 : __('A new experience is on the horizon.', 'king-addons');
1314
1315 $subhead = $tagline !== '' ? $tagline : __('Our team is preparing something beautiful. Stay tuned.', 'king-addons');
1316
1317 $mode_label = $mode === 'maintenance' ? __('Maintenance Mode', 'king-addons') : __('Coming Soon', 'king-addons');
1318
1319 $badge = $content['badge'] !== '' ? $content['badge'] : $mode_label;
1320 $headline = $content['headline'] !== '' ? $content['headline'] : $headline;
1321 $subhead = $content['subhead'] !== '' ? $content['subhead'] : $subhead;
1322
1323 $launch_label = '';
1324 if ($content['launch_label'] !== '') {
1325 $launch_label = $content['launch_label'];
1326 }
1327 if ($schedule_end !== '') {
1328 $local_end = get_date_from_gmt($schedule_end, 'Y-m-d H:i');
1329 if ($launch_label === '') {
1330 $launch_label = sprintf(__('Estimated return: %s', 'king-addons'), esc_html($local_end));
1331 }
1332 }
1333
1334 $footer_left = $content['footer_left'] !== '' ? $content['footer_left'] : $site_name;
1335 $footer_right = $content['footer_right'] !== '' ? $content['footer_right'] : __('Powered by King Addons', 'king-addons');
1336
1337 $countdown_days = str_pad((string) (int) ($content['countdown_days'] ?? 0), 2, '0', STR_PAD_LEFT);
1338 $countdown_hours = str_pad((string) (int) ($content['countdown_hours'] ?? 0), 2, '0', STR_PAD_LEFT);
1339 $countdown_minutes = str_pad((string) (int) ($content['countdown_minutes'] ?? 0), 2, '0', STR_PAD_LEFT);
1340 $progress_percent = isset($content['progress_percent']) ? min(100, max(0, (int) $content['progress_percent'])) : 0;
1341 $progress_label = $content['progress_label'] ?? '';
1342 if ($progress_label === '') {
1343 $progress_label = sprintf(__('%d%% complete', 'king-addons'), $progress_percent);
1344 }
1345 $form_placeholder = $content['form_placeholder'] ?? '';
1346 if ($form_placeholder === '') {
1347 $form_placeholder = __('Email address', 'king-addons');
1348 }
1349 $form_button = $content['form_button'] ?? '';
1350 if ($form_button === '') {
1351 $form_button = __('Notify me', 'king-addons');
1352 }
1353
1354 ob_start();
1355 ?>
1356 <section class="kng-maintenance-shell kng-maintenance-template-<?php echo esc_attr($template_id); ?>">
1357 <div class="kng-maintenance-card">
1358 <div class="kng-maintenance-badge"><?php echo esc_html($badge); ?></div>
1359 <h1><?php echo esc_html($headline); ?></h1>
1360 <p class="kng-maintenance-subhead"><?php echo esc_html($subhead); ?></p>
1361
1362 <?php if ($launch_label !== '') : ?>
1363 <p class="kng-maintenance-launch"><?php echo esc_html($launch_label); ?></p>
1364 <?php endif; ?>
1365
1366 <?php if ($template_id === 'countdown') : ?>
1367 <div class="kng-maintenance-countdown">
1368 <div><strong><?php echo esc_html($countdown_days); ?></strong><span><?php esc_html_e('Days', 'king-addons'); ?></span></div>
1369 <div><strong><?php echo esc_html($countdown_hours); ?></strong><span><?php esc_html_e('Hours', 'king-addons'); ?></span></div>
1370 <div><strong><?php echo esc_html($countdown_minutes); ?></strong><span><?php esc_html_e('Minutes', 'king-addons'); ?></span></div>
1371 </div>
1372 <?php endif; ?>
1373
1374 <?php if ($template_id === 'progress') : ?>
1375 <div class="kng-maintenance-progress">
1376 <div class="kng-maintenance-progress-bar">
1377 <span style="width: <?php echo esc_attr($progress_percent); ?>%;"></span>
1378 </div>
1379 <div class="kng-maintenance-progress-label"><?php echo esc_html($progress_label); ?></div>
1380 </div>
1381 <?php endif; ?>
1382
1383 <?php if (in_array($template_id, ['subscribe', 'product-launch'], true)) : ?>
1384 <form class="kng-maintenance-form">
1385 <input type="email" placeholder="<?php echo esc_attr($form_placeholder); ?>" aria-label="<?php echo esc_attr($form_placeholder); ?>">
1386 <button type="button"><?php echo esc_html($form_button); ?></button>
1387 </form>
1388 <?php endif; ?>
1389
1390 <?php if ($template_id === 'split') : ?>
1391 <div class="kng-maintenance-split">
1392 <div>
1393 <h3><?php echo esc_html($content['split_title_a']); ?></h3>
1394 <p><?php echo esc_html($content['split_text_a']); ?></p>
1395 </div>
1396 <div>
1397 <h3><?php echo esc_html($content['split_title_b']); ?></h3>
1398 <p><?php echo esc_html($content['split_text_b']); ?></p>
1399 </div>
1400 </div>
1401 <?php endif; ?>
1402
1403 <div class="kng-maintenance-footer">
1404 <span><?php echo esc_html($footer_left); ?></span>
1405 <span class="kng-maintenance-dot"></span>
1406 <span><?php echo esc_html($footer_right); ?></span>
1407 </div>
1408 </div>
1409 <div class="kng-maintenance-orb"></div>
1410 <div class="kng-maintenance-orb secondary"></div>
1411 </section>
1412 <?php
1413 return ob_get_clean();
1414 }
1415
1416 private function get_builtin_templates(): array
1417 {
1418 return [
1419 'minimal' => __('Minimal', 'king-addons'),
1420 'dark' => __('Dark', 'king-addons'),
1421 'gradient' => __('Gradient', 'king-addons'),
1422 'aurora' => __('Aurora Glow', 'king-addons'),
1423 'neon' => __('Neon Tech', 'king-addons'),
1424 'paper' => __('Paper Light', 'king-addons'),
1425 'grid' => __('Tech Grid', 'king-addons'),
1426 'mono' => __('Mono Minimal', 'king-addons'),
1427 'spotlight' => __('Spotlight', 'king-addons'),
1428 'poster' => __('Poster', 'king-addons'),
1429 'ribbon' => __('Ribbon', 'king-addons'),
1430 'countdown' => __('Coming Soon Countdown', 'king-addons'),
1431 'progress' => __('Maintenance Progress', 'king-addons'),
1432 'subscribe' => __('Simple Subscribe', 'king-addons'),
1433 'product-launch' => __('Product Launch', 'king-addons'),
1434 'construction' => __('Under Construction', 'king-addons'),
1435 'split' => __('Split Layout', 'king-addons'),
1436 'logo' => __('Centered Logo', 'king-addons'),
1437 ];
1438 }
1439
1440 public function get_pro_templates(): array
1441 {
1442 return [
1443 'countdown',
1444 'progress',
1445 'product-launch',
1446 'split',
1447 ];
1448 }
1449
1450 public function handle_export(): void
1451 {
1452 if (!current_user_can('manage_options')) {
1453 wp_die(esc_html__('Unauthorized request.', 'king-addons'));
1454 }
1455
1456 check_admin_referer('kng_maintenance_export');
1457
1458 $settings = $this->get_settings();
1459
1460 header('Content-Type: application/json; charset=utf-8');
1461 header('Content-Disposition: attachment; filename=maintenance-mode-settings.json');
1462 echo wp_json_encode($settings);
1463 exit;
1464 }
1465
1466 public function handle_import(): void
1467 {
1468 if (!current_user_can('manage_options')) {
1469 wp_die(esc_html__('Unauthorized request.', 'king-addons'));
1470 }
1471
1472 check_admin_referer('kng_maintenance_import');
1473
1474 if (empty($_FILES['import_file']) || !isset($_FILES['import_file']['tmp_name'])) {
1475 $this->redirect_with_message('import-export', 'error_import');
1476 }
1477
1478 $file = $_FILES['import_file'];
1479 if (!empty($file['error'])) {
1480 $this->redirect_with_message('import-export', 'error_import');
1481 }
1482
1483 $contents = file_get_contents($file['tmp_name']);
1484 if ($contents === false) {
1485 $this->redirect_with_message('import-export', 'error_import');
1486 }
1487
1488 $decoded = json_decode($contents, true);
1489 if (!is_array($decoded)) {
1490 $this->redirect_with_message('import-export', 'error_import');
1491 }
1492
1493 $sanitized = $this->sanitize_settings($decoded);
1494 update_option(self::OPTION_NAME, $sanitized);
1495
1496 $this->redirect_with_message('import-export', 'imported');
1497 }
1498
1499 private function get_default_settings(): array
1500 {
1501 return [
1502 'enabled' => false,
1503 'mode' => 'coming_soon',
1504 'template_source' => 'built_in',
1505 'template_id' => 'minimal',
1506 'template_content' => [],
1507 'page_id' => 0,
1508 'elementor_id' => 0,
1509 'theme' => 'dark',
1510 'noindex' => true,
1511 'retry_after' => 3600,
1512 'whitelist_ips' => [],
1513 'whitelist_paths' => [],
1514 'exclude_admin' => true,
1515 'allowed_roles' => [],
1516 'schedule_enabled' => false,
1517 'schedule_start' => '',
1518 'schedule_end' => '',
1519 'schedule_windows' => [],
1520 'recurring_enabled' => false,
1521 'recurring_rules' => [],
1522 'allow_rest' => true,
1523 'allow_admin_ajax' => true,
1524 'disable_elementor_editor' => true,
1525 'private_password_hash' => '',
1526 'private_token' => '',
1527 'custom_css' => '',
1528 'custom_js' => '',
1529 ];
1530 }
1531
1532 public function get_settings(): array
1533 {
1534 $defaults = $this->get_default_settings();
1535 $saved = get_option(self::OPTION_NAME, []);
1536
1537 $settings = wp_parse_args($saved, $defaults);
1538 if (!is_array($settings['template_content'])) {
1539 $settings['template_content'] = $defaults['template_content'];
1540 }
1541 if (!is_array($settings['whitelist_ips'])) {
1542 $settings['whitelist_ips'] = $defaults['whitelist_ips'];
1543 }
1544 if (!is_array($settings['whitelist_paths'])) {
1545 $settings['whitelist_paths'] = $defaults['whitelist_paths'];
1546 }
1547 if (!is_array($settings['allowed_roles'])) {
1548 $settings['allowed_roles'] = $defaults['allowed_roles'];
1549 }
1550
1551 if (!is_array($settings['schedule_windows'])) {
1552 $settings['schedule_windows'] = $defaults['schedule_windows'];
1553 }
1554
1555 if (!is_array($settings['recurring_rules'])) {
1556 $settings['recurring_rules'] = $defaults['recurring_rules'];
1557 }
1558
1559 return $settings;
1560 }
1561
1562 public function sanitize_settings(array $settings): array
1563 {
1564 $defaults = $this->get_default_settings();
1565 $existing = $this->get_settings();
1566 $settings = wp_parse_args($settings, $existing);
1567 $templates = array_keys($this->get_builtin_templates());
1568 $pro_templates = $this->get_pro_templates();
1569
1570 $source = isset($settings['template_source']) ? sanitize_key($settings['template_source']) : $defaults['template_source'];
1571 if (!in_array($source, ['built_in', 'page', 'elementor'], true)) {
1572 $source = 'built_in';
1573 }
1574
1575 $template_id = isset($settings['template_id']) ? sanitize_key($settings['template_id']) : $defaults['template_id'];
1576 if (!in_array($template_id, $templates, true)) {
1577 $template_id = $defaults['template_id'];
1578 }
1579 if (!$this->is_pro() && in_array($template_id, $pro_templates, true)) {
1580 $template_id = 'minimal';
1581 }
1582
1583 $mode = isset($settings['mode']) ? sanitize_key($settings['mode']) : $defaults['mode'];
1584 if (!in_array($mode, ['coming_soon', 'maintenance'], true)) {
1585 $mode = $defaults['mode'];
1586 }
1587
1588 $theme = isset($settings['theme']) ? sanitize_key($settings['theme']) : $defaults['theme'];
1589 $theme = $theme === 'light' ? 'light' : 'dark';
1590
1591 $whitelist_ips = $this->sanitize_list($settings['whitelist_ips'] ?? []);
1592 $whitelist_paths = $this->sanitize_list($settings['whitelist_paths'] ?? []);
1593
1594 if (!$this->is_pro()) {
1595 $whitelist_ips = array_slice($whitelist_ips, 0, 10);
1596 $whitelist_paths = array_slice($whitelist_paths, 0, 10);
1597 }
1598
1599 $legacyScheduleStart = $this->sanitize_datetime($settings['schedule_start'] ?? '');
1600 $legacyScheduleEnd = $this->sanitize_datetime($settings['schedule_end'] ?? '');
1601
1602 $scheduleWindows = $this->sanitize_schedule_windows($settings['schedule_windows'] ?? []);
1603 if ($scheduleWindows === [] && ($legacyScheduleStart !== '' || $legacyScheduleEnd !== '')) {
1604 $scheduleWindows[] = [
1605 'start' => $legacyScheduleStart,
1606 'end' => $legacyScheduleEnd,
1607 'timezone' => self::DEFAULT_TIMEZONE,
1608 ];
1609 }
1610
1611 if ($scheduleWindows !== []) {
1612 $legacyScheduleStart = (string) ($scheduleWindows[0]['start'] ?? $legacyScheduleStart);
1613 $legacyScheduleEnd = (string) ($scheduleWindows[0]['end'] ?? $legacyScheduleEnd);
1614 }
1615
1616 $recurringRules = $this->sanitize_recurring_rules($settings['recurring_rules'] ?? []);
1617
1618 $clean = [
1619 'enabled' => !empty($settings['enabled']),
1620 'mode' => $mode,
1621 'template_source' => $source,
1622 'template_id' => $template_id,
1623 'template_content' => $this->sanitize_template_content($settings['template_content'] ?? []),
1624 'page_id' => absint($settings['page_id'] ?? 0),
1625 'elementor_id' => absint($settings['elementor_id'] ?? 0),
1626 'theme' => $theme,
1627 'noindex' => !empty($settings['noindex']),
1628 'retry_after' => max(0, absint($settings['retry_after'] ?? $defaults['retry_after'])),
1629 'whitelist_ips' => $whitelist_ips,
1630 'whitelist_paths' => $this->normalize_paths($whitelist_paths),
1631 'exclude_admin' => !empty($settings['exclude_admin']),
1632 'allowed_roles' => $this->sanitize_list($settings['allowed_roles'] ?? []),
1633 'schedule_enabled' => !empty($settings['schedule_enabled']),
1634 'schedule_start' => $legacyScheduleStart,
1635 'schedule_end' => $legacyScheduleEnd,
1636 'schedule_windows' => $scheduleWindows,
1637 'recurring_enabled' => !empty($settings['recurring_enabled']),
1638 'recurring_rules' => $recurringRules,
1639 'allow_rest' => !empty($settings['allow_rest']),
1640 'allow_admin_ajax' => !empty($settings['allow_admin_ajax']),
1641 'disable_elementor_editor' => !empty($settings['disable_elementor_editor']),
1642 'private_password_hash' => (string) ($existing['private_password_hash'] ?? ''),
1643 'private_token' => (string) ($existing['private_token'] ?? ''),
1644 'custom_css' => '',
1645 'custom_js' => '',
1646 ];
1647
1648 if ($this->is_pro()) {
1649 $remove_password = !empty($settings['private_password_remove']);
1650 $plain = isset($settings['private_password']) ? (string) $settings['private_password'] : '';
1651 $plain = trim($plain);
1652
1653 if ($remove_password) {
1654 $clean['private_password_hash'] = '';
1655 } elseif ($plain !== '') {
1656 $clean['private_password_hash'] = function_exists('wp_hash_password') ? wp_hash_password($plain) : $clean['private_password_hash'];
1657 }
1658
1659 $token = isset($settings['private_token']) ? sanitize_text_field((string) $settings['private_token']) : (string) ($existing['private_token'] ?? '');
1660 $clean['private_token'] = $token;
1661 }
1662
1663 if (!$this->is_pro()) {
1664 $clean['allowed_roles'] = [];
1665 }
1666
1667 return $clean;
1668 }
1669
1670 private function sanitize_schedule_windows($windows): array
1671 {
1672 if (!is_array($windows)) {
1673 return [];
1674 }
1675
1676 $clean = [];
1677 foreach ($windows as $window) {
1678 if (!is_array($window)) {
1679 continue;
1680 }
1681
1682 $tz = isset($window['timezone']) ? sanitize_text_field((string) $window['timezone']) : self::DEFAULT_TIMEZONE;
1683 if ($tz === '') {
1684 $tz = self::DEFAULT_TIMEZONE;
1685 }
1686
1687 $start = $this->sanitize_datetime_with_timezone((string) ($window['start'] ?? ''), $tz);
1688 $end = $this->sanitize_datetime_with_timezone((string) ($window['end'] ?? ''), $tz);
1689
1690 if ($start === '' && $end === '') {
1691 continue;
1692 }
1693
1694 $clean[] = [
1695 'start' => $start,
1696 'end' => $end,
1697 'timezone' => $tz,
1698 ];
1699 }
1700
1701 return $clean;
1702 }
1703
1704 private function sanitize_recurring_rules($rules): array
1705 {
1706 if (!is_array($rules)) {
1707 return [];
1708 }
1709
1710 $clean = [];
1711 foreach ($rules as $rule) {
1712 if (!is_array($rule)) {
1713 continue;
1714 }
1715
1716 $freq = isset($rule['frequency']) ? sanitize_key((string) $rule['frequency']) : '';
1717 if (!in_array($freq, ['daily', 'weekly', 'monthly'], true)) {
1718 continue;
1719 }
1720
1721 $tz = isset($rule['timezone']) ? sanitize_text_field((string) $rule['timezone']) : self::DEFAULT_TIMEZONE;
1722 if ($tz === '') {
1723 $tz = self::DEFAULT_TIMEZONE;
1724 }
1725
1726 $startTime = sanitize_text_field((string) ($rule['start_time'] ?? ''));
1727 $endTime = sanitize_text_field((string) ($rule['end_time'] ?? ''));
1728 if ($this->parse_time_minutes($startTime) < 0 || $this->parse_time_minutes($endTime) < 0) {
1729 continue;
1730 }
1731
1732 $daysOfWeek = [];
1733 if ($freq === 'weekly') {
1734 $raw = $rule['days_of_week'] ?? [];
1735 if (!is_array($raw)) {
1736 $raw = preg_split('/\s*,\s*/', (string) $raw);
1737 }
1738
1739 $daysOfWeek = array_values(array_unique(array_filter(array_map(static function ($v) {
1740 $n = absint($v);
1741 return ($n >= 1 && $n <= 7) ? $n : 0;
1742 }, (array) $raw))));
1743 }
1744
1745 $daysOfMonth = [];
1746 if ($freq === 'monthly') {
1747 $raw = $rule['days_of_month'] ?? [];
1748 if (!is_array($raw)) {
1749 $raw = preg_split('/\s*,\s*/', (string) $raw);
1750 }
1751
1752 $daysOfMonth = array_values(array_unique(array_filter(array_map(static function ($v) {
1753 $n = absint($v);
1754 return ($n >= 1 && $n <= 31) ? $n : 0;
1755 }, (array) $raw))));
1756 }
1757
1758 $item = [
1759 'frequency' => $freq,
1760 'timezone' => $tz,
1761 'start_time' => $startTime,
1762 'end_time' => $endTime,
1763 'days_of_week' => $daysOfWeek,
1764 'days_of_month' => $daysOfMonth,
1765 ];
1766
1767 $clean[] = $item;
1768 }
1769
1770 return $clean;
1771 }
1772
1773 public function get_template_content(string $template_id): array
1774 {
1775 $templates = $this->get_builtin_templates();
1776 $template_id = isset($templates[$template_id]) ? $template_id : 'minimal';
1777 $template_id = $this->ensure_template_allowed($template_id);
1778
1779 $defaults = $this->get_template_content_defaults($template_id);
1780 $saved = $this->settings['template_content'][$template_id] ?? [];
1781 if (!is_array($saved)) {
1782 $saved = [];
1783 }
1784
1785 return wp_parse_args($saved, $defaults);
1786 }
1787
1788 private function get_template_content_defaults(string $template_id): array
1789 {
1790 $mode = $this->settings['mode'] ?? 'coming_soon';
1791 $site_name = get_bloginfo('name');
1792 $tagline = get_bloginfo('description');
1793
1794 $headline = $mode === 'maintenance'
1795 ? __('We are upgrading the experience.', 'king-addons')
1796 : __('A new experience is on the horizon.', 'king-addons');
1797
1798 $subhead = $tagline !== '' ? $tagline : __('Our team is preparing something beautiful. Stay tuned.', 'king-addons');
1799
1800 $base = [
1801 'badge' => '',
1802 'headline' => $headline,
1803 'subhead' => $subhead,
1804 'launch_label' => '',
1805 'footer_left' => $site_name,
1806 'footer_right' => __('Powered by King Addons', 'king-addons'),
1807 ];
1808
1809 if ($template_id === 'countdown') {
1810 $base['countdown_days'] = '14';
1811 $base['countdown_hours'] = '06';
1812 $base['countdown_minutes'] = '42';
1813 }
1814
1815 if ($template_id === 'progress') {
1816 $base['progress_percent'] = 68;
1817 $base['progress_label'] = __('68% complete', 'king-addons');
1818 }
1819
1820 if (in_array($template_id, ['subscribe', 'product-launch'], true)) {
1821 $base['form_placeholder'] = __('Email address', 'king-addons');
1822 $base['form_button'] = __('Notify me', 'king-addons');
1823 }
1824
1825 if ($template_id === 'split') {
1826 $base['split_title_a'] = __('What is happening', 'king-addons');
1827 $base['split_text_a'] = __('We are refining the experience with faster performance and new visuals.', 'king-addons');
1828 $base['split_title_b'] = __('Stay connected', 'king-addons');
1829 $base['split_text_b'] = __('Follow our updates while we prepare the launch.', 'king-addons');
1830 }
1831
1832 return $base;
1833 }
1834
1835 private function sanitize_template_content(array $content): array
1836 {
1837 $fields = $this->get_template_content_fields();
1838 $clean = [];
1839
1840 foreach ($fields as $template_id => $template_fields) {
1841 $values = isset($content[$template_id]) && is_array($content[$template_id]) ? $content[$template_id] : [];
1842 $template_clean = [];
1843
1844 foreach ($template_fields as $field => $type) {
1845 $value = $values[$field] ?? '';
1846 if ($type === 'int') {
1847 $int = absint($value);
1848 if ($field === 'progress_percent') {
1849 $int = min(100, max(0, $int));
1850 }
1851 if ($field === 'countdown_hours') {
1852 $int = min(23, max(0, $int));
1853 }
1854 if ($field === 'countdown_minutes') {
1855 $int = min(59, max(0, $int));
1856 }
1857 $template_clean[$field] = $int;
1858 } else {
1859 $template_clean[$field] = sanitize_text_field((string) $value);
1860 }
1861 }
1862
1863 $clean[$template_id] = $template_clean;
1864 }
1865
1866 return $clean;
1867 }
1868
1869 private function get_template_content_fields(): array
1870 {
1871 $base = [
1872 'badge' => 'text',
1873 'headline' => 'text',
1874 'subhead' => 'text',
1875 'launch_label' => 'text',
1876 'footer_left' => 'text',
1877 'footer_right' => 'text',
1878 ];
1879
1880 return [
1881 'minimal' => $base,
1882 'dark' => $base,
1883 'gradient' => $base,
1884 'aurora' => $base,
1885 'neon' => $base,
1886 'paper' => $base,
1887 'grid' => $base,
1888 'mono' => $base,
1889 'spotlight' => $base,
1890 'poster' => $base,
1891 'ribbon' => $base,
1892 'construction' => $base,
1893 'logo' => $base,
1894 'countdown' => $base + [
1895 'countdown_days' => 'int',
1896 'countdown_hours' => 'int',
1897 'countdown_minutes' => 'int',
1898 ],
1899 'progress' => $base + [
1900 'progress_percent' => 'int',
1901 'progress_label' => 'text',
1902 ],
1903 'subscribe' => $base + [
1904 'form_placeholder' => 'text',
1905 'form_button' => 'text',
1906 ],
1907 'product-launch' => $base + [
1908 'form_placeholder' => 'text',
1909 'form_button' => 'text',
1910 ],
1911 'split' => $base + [
1912 'split_title_a' => 'text',
1913 'split_text_a' => 'text',
1914 'split_title_b' => 'text',
1915 'split_text_b' => 'text',
1916 ],
1917 ];
1918 }
1919
1920 private function ensure_template_allowed(string $template_id): string
1921 {
1922 if (!$this->is_pro() && in_array($template_id, $this->get_pro_templates(), true)) {
1923 return 'minimal';
1924 }
1925
1926 return $template_id;
1927 }
1928
1929 private function sanitize_list($value): array
1930 {
1931 if (is_array($value)) {
1932 $items = $value;
1933 } else {
1934 $items = preg_split('/\\r\\n|\\r|\\n|,/', (string) $value);
1935 }
1936
1937 $items = array_map('trim', $items);
1938 $items = array_filter($items, static function ($item) {
1939 return $item !== '';
1940 });
1941 $items = array_map('sanitize_text_field', $items);
1942
1943 return array_values(array_unique($items));
1944 }
1945
1946 private function normalize_paths(array $paths): array
1947 {
1948 $normalized = [];
1949 foreach ($paths as $path) {
1950 $path = wp_parse_url($path, PHP_URL_PATH) ?: $path;
1951 $path = '/' . ltrim((string) $path, '/');
1952 $normalized[] = $path === '' ? '/' : $path;
1953 }
1954
1955 return array_values(array_unique($normalized));
1956 }
1957
1958 private function sanitize_datetime(string $value): string
1959 {
1960 $value = sanitize_text_field($value);
1961 if ($value === '') {
1962 return '';
1963 }
1964
1965 $value = str_replace('T', ' ', $value);
1966 if (strlen($value) === 16) {
1967 $value .= ':00';
1968 }
1969
1970 $timestamp = strtotime($value);
1971 if (!$timestamp) {
1972 return '';
1973 }
1974
1975 return get_gmt_from_date($value, 'Y-m-d H:i:s');
1976 }
1977
1978 private function sanitize_datetime_with_timezone(string $value, string $timezone): string
1979 {
1980 $value = sanitize_text_field($value);
1981 if ($value === '') {
1982 return '';
1983 }
1984
1985 $value = str_replace('T', ' ', $value);
1986 if (strlen($value) === 16) {
1987 $value .= ':00';
1988 }
1989
1990 if (!preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $value)) {
1991 return '';
1992 }
1993
1994 $tz = $this->resolve_timezone($timezone);
1995 $dt = \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $value, $tz);
1996 if (!$dt) {
1997 return '';
1998 }
1999
2000 return $dt->setTimezone(new \DateTimeZone('UTC'))->format('Y-m-d H:i:s');
2001 }
2002
2003 private function parse_schedule_time(string $value): int
2004 {
2005 if ($value === '') {
2006 return 0;
2007 }
2008
2009 return (int) strtotime($value . ' UTC');
2010 }
2011
2012 private function get_client_ip(): string
2013 {
2014 if (!empty($_SERVER['REMOTE_ADDR']) && filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP)) {
2015 return (string) $_SERVER['REMOTE_ADDR'];
2016 }
2017
2018 return '';
2019 }
2020
2021 private function is_preview_request(): bool
2022 {
2023 if (empty($_GET['kng_maintenance_preview'])) {
2024 return false;
2025 }
2026
2027 if (!current_user_can('manage_options')) {
2028 return false;
2029 }
2030
2031 $nonce = isset($_GET['_kng_preview_nonce']) ? sanitize_text_field(wp_unslash($_GET['_kng_preview_nonce'])) : '';
2032 if ($nonce === '' || !wp_verify_nonce($nonce, 'kng_maintenance_preview')) {
2033 return false;
2034 }
2035
2036 return true;
2037 }
2038
2039 private function is_pro(): bool
2040 {
2041 return function_exists('king_addons_freemius') && king_addons_freemius()->can_use_premium_code__premium_only();
2042 }
2043
2044 private function redirect_with_message(string $view, string $message): void
2045 {
2046 $args = [
2047 'page' => 'king-addons-maintenance-mode',
2048 'view' => $view,
2049 'message' => $message,
2050 ];
2051
2052 wp_safe_redirect(add_query_arg($args, admin_url('admin.php')));
2053 exit;
2054 }
2055 }
2056