PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.83
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.83
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.83, at includes/extensions/Maintenance_Mode/Maintenance_Mode.php

2,085 lines 70.4 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 /**
1264 * Whether the current user may see this post's body, including status and password.
1265 */
1266 private function user_can_view_post_content(\WP_Post $post): bool
1267 {
1268 if (!current_user_can('read_post', $post->ID)) {
1269 return false;
1270 }
1271
1272 return !post_password_required($post);
1273 }
1274
1275 private function get_page_content(int $page_id): string
1276 {
1277 $page = get_post($page_id);
1278 if (!$page) {
1279 return $this->render_builtin_template('minimal');
1280 }
1281
1282 if (!$this->user_can_view_post_content($page)) {
1283 if (post_password_required($page) && current_user_can('read_post', $page_id)) {
1284 return get_the_password_form($page);
1285 }
1286
1287 return $this->render_builtin_template('minimal');
1288 }
1289
1290 $old_post = $GLOBALS['post'] ?? null;
1291 $GLOBALS['post'] = $page;
1292 setup_postdata($page);
1293
1294 $content = apply_filters('the_content', $page->post_content);
1295
1296 wp_reset_postdata();
1297 $GLOBALS['post'] = $old_post;
1298
1299 return $content;
1300 }
1301
1302 private function get_elementor_content(int $template_id): string
1303 {
1304 $template = get_post($template_id);
1305 if (!$template || !$this->user_can_view_post_content($template)) {
1306 if ($template && post_password_required($template) && current_user_can('read_post', $template_id)) {
1307 return get_the_password_form($template);
1308 }
1309
1310 return $this->render_builtin_template('minimal');
1311 }
1312
1313 if (!class_exists('\\Elementor\\Plugin')) {
1314 return $this->render_builtin_template('minimal');
1315 }
1316
1317 $plugin = \Elementor\Plugin::instance();
1318 if (!$plugin->frontend) {
1319 return $this->render_builtin_template('minimal');
1320 }
1321
1322 $plugin->frontend->enqueue_styles();
1323 $plugin->frontend->enqueue_scripts();
1324
1325 return $plugin->frontend->get_builder_content_for_display($template_id, true);
1326 }
1327
1328 private function render_builtin_template(string $template_id): string
1329 {
1330 $templates = $this->get_builtin_templates();
1331 $template_id = isset($templates[$template_id]) ? $template_id : 'minimal';
1332 $template_id = $this->ensure_template_allowed($template_id);
1333
1334 $mode = $this->settings['mode'] ?? 'coming_soon';
1335 $site_name = get_bloginfo('name');
1336 $tagline = get_bloginfo('description');
1337 $schedule_end = $this->settings['schedule_end'] ?? '';
1338 $content = $this->get_template_content($template_id);
1339
1340 $headline = $mode === 'maintenance'
1341 ? __('We are upgrading the experience.', 'king-addons')
1342 : __('A new experience is on the horizon.', 'king-addons');
1343
1344 $subhead = $tagline !== '' ? $tagline : __('Our team is preparing something beautiful. Stay tuned.', 'king-addons');
1345
1346 $mode_label = $mode === 'maintenance' ? __('Maintenance Mode', 'king-addons') : __('Coming Soon', 'king-addons');
1347
1348 $badge = $content['badge'] !== '' ? $content['badge'] : $mode_label;
1349 $headline = $content['headline'] !== '' ? $content['headline'] : $headline;
1350 $subhead = $content['subhead'] !== '' ? $content['subhead'] : $subhead;
1351
1352 $launch_label = '';
1353 if ($content['launch_label'] !== '') {
1354 $launch_label = $content['launch_label'];
1355 }
1356 if ($schedule_end !== '') {
1357 $local_end = get_date_from_gmt($schedule_end, 'Y-m-d H:i');
1358 if ($launch_label === '') {
1359 $launch_label = sprintf(__('Estimated return: %s', 'king-addons'), esc_html($local_end));
1360 }
1361 }
1362
1363 $footer_left = $content['footer_left'] !== '' ? $content['footer_left'] : $site_name;
1364 $footer_right = $content['footer_right'] !== '' ? $content['footer_right'] : __('Powered by King Addons', 'king-addons');
1365
1366 $countdown_days = str_pad((string) (int) ($content['countdown_days'] ?? 0), 2, '0', STR_PAD_LEFT);
1367 $countdown_hours = str_pad((string) (int) ($content['countdown_hours'] ?? 0), 2, '0', STR_PAD_LEFT);
1368 $countdown_minutes = str_pad((string) (int) ($content['countdown_minutes'] ?? 0), 2, '0', STR_PAD_LEFT);
1369 $progress_percent = isset($content['progress_percent']) ? min(100, max(0, (int) $content['progress_percent'])) : 0;
1370 $progress_label = $content['progress_label'] ?? '';
1371 if ($progress_label === '') {
1372 $progress_label = sprintf(__('%d%% complete', 'king-addons'), $progress_percent);
1373 }
1374 $form_placeholder = $content['form_placeholder'] ?? '';
1375 if ($form_placeholder === '') {
1376 $form_placeholder = __('Email address', 'king-addons');
1377 }
1378 $form_button = $content['form_button'] ?? '';
1379 if ($form_button === '') {
1380 $form_button = __('Notify me', 'king-addons');
1381 }
1382
1383 ob_start();
1384 ?>
1385 <section class="kng-maintenance-shell kng-maintenance-template-<?php echo esc_attr($template_id); ?>">
1386 <div class="kng-maintenance-card">
1387 <div class="kng-maintenance-badge"><?php echo esc_html($badge); ?></div>
1388 <h1><?php echo esc_html($headline); ?></h1>
1389 <p class="kng-maintenance-subhead"><?php echo esc_html($subhead); ?></p>
1390
1391 <?php if ($launch_label !== '') : ?>
1392 <p class="kng-maintenance-launch"><?php echo esc_html($launch_label); ?></p>
1393 <?php endif; ?>
1394
1395 <?php if ($template_id === 'countdown') : ?>
1396 <div class="kng-maintenance-countdown">
1397 <div><strong><?php echo esc_html($countdown_days); ?></strong><span><?php esc_html_e('Days', 'king-addons'); ?></span></div>
1398 <div><strong><?php echo esc_html($countdown_hours); ?></strong><span><?php esc_html_e('Hours', 'king-addons'); ?></span></div>
1399 <div><strong><?php echo esc_html($countdown_minutes); ?></strong><span><?php esc_html_e('Minutes', 'king-addons'); ?></span></div>
1400 </div>
1401 <?php endif; ?>
1402
1403 <?php if ($template_id === 'progress') : ?>
1404 <div class="kng-maintenance-progress">
1405 <div class="kng-maintenance-progress-bar">
1406 <span style="width: <?php echo esc_attr($progress_percent); ?>%;"></span>
1407 </div>
1408 <div class="kng-maintenance-progress-label"><?php echo esc_html($progress_label); ?></div>
1409 </div>
1410 <?php endif; ?>
1411
1412 <?php if (in_array($template_id, ['subscribe', 'product-launch'], true)) : ?>
1413 <form class="kng-maintenance-form">
1414 <input type="email" placeholder="<?php echo esc_attr($form_placeholder); ?>" aria-label="<?php echo esc_attr($form_placeholder); ?>">
1415 <button type="button"><?php echo esc_html($form_button); ?></button>
1416 </form>
1417 <?php endif; ?>
1418
1419 <?php if ($template_id === 'split') : ?>
1420 <div class="kng-maintenance-split">
1421 <div>
1422 <h3><?php echo esc_html($content['split_title_a']); ?></h3>
1423 <p><?php echo esc_html($content['split_text_a']); ?></p>
1424 </div>
1425 <div>
1426 <h3><?php echo esc_html($content['split_title_b']); ?></h3>
1427 <p><?php echo esc_html($content['split_text_b']); ?></p>
1428 </div>
1429 </div>
1430 <?php endif; ?>
1431
1432 <div class="kng-maintenance-footer">
1433 <span><?php echo esc_html($footer_left); ?></span>
1434 <span class="kng-maintenance-dot"></span>
1435 <span><?php echo esc_html($footer_right); ?></span>
1436 </div>
1437 </div>
1438 <div class="kng-maintenance-orb"></div>
1439 <div class="kng-maintenance-orb secondary"></div>
1440 </section>
1441 <?php
1442 return ob_get_clean();
1443 }
1444
1445 private function get_builtin_templates(): array
1446 {
1447 return [
1448 'minimal' => __('Minimal', 'king-addons'),
1449 'dark' => __('Dark', 'king-addons'),
1450 'gradient' => __('Gradient', 'king-addons'),
1451 'aurora' => __('Aurora Glow', 'king-addons'),
1452 'neon' => __('Neon Tech', 'king-addons'),
1453 'paper' => __('Paper Light', 'king-addons'),
1454 'grid' => __('Tech Grid', 'king-addons'),
1455 'mono' => __('Mono Minimal', 'king-addons'),
1456 'spotlight' => __('Spotlight', 'king-addons'),
1457 'poster' => __('Poster', 'king-addons'),
1458 'ribbon' => __('Ribbon', 'king-addons'),
1459 'countdown' => __('Coming Soon Countdown', 'king-addons'),
1460 'progress' => __('Maintenance Progress', 'king-addons'),
1461 'subscribe' => __('Simple Subscribe', 'king-addons'),
1462 'product-launch' => __('Product Launch', 'king-addons'),
1463 'construction' => __('Under Construction', 'king-addons'),
1464 'split' => __('Split Layout', 'king-addons'),
1465 'logo' => __('Centered Logo', 'king-addons'),
1466 ];
1467 }
1468
1469 public function get_pro_templates(): array
1470 {
1471 return [
1472 'countdown',
1473 'progress',
1474 'product-launch',
1475 'split',
1476 ];
1477 }
1478
1479 public function handle_export(): void
1480 {
1481 if (!current_user_can('manage_options')) {
1482 wp_die(esc_html__('Unauthorized request.', 'king-addons'));
1483 }
1484
1485 check_admin_referer('kng_maintenance_export');
1486
1487 $settings = $this->get_settings();
1488
1489 header('Content-Type: application/json; charset=utf-8');
1490 header('Content-Disposition: attachment; filename=maintenance-mode-settings.json');
1491 echo wp_json_encode($settings);
1492 exit;
1493 }
1494
1495 public function handle_import(): void
1496 {
1497 if (!current_user_can('manage_options')) {
1498 wp_die(esc_html__('Unauthorized request.', 'king-addons'));
1499 }
1500
1501 check_admin_referer('kng_maintenance_import');
1502
1503 if (empty($_FILES['import_file']) || !isset($_FILES['import_file']['tmp_name'])) {
1504 $this->redirect_with_message('import-export', 'error_import');
1505 }
1506
1507 $file = $_FILES['import_file'];
1508 if (!empty($file['error'])) {
1509 $this->redirect_with_message('import-export', 'error_import');
1510 }
1511
1512 $contents = file_get_contents($file['tmp_name']);
1513 if ($contents === false) {
1514 $this->redirect_with_message('import-export', 'error_import');
1515 }
1516
1517 $decoded = json_decode($contents, true);
1518 if (!is_array($decoded)) {
1519 $this->redirect_with_message('import-export', 'error_import');
1520 }
1521
1522 $sanitized = $this->sanitize_settings($decoded);
1523 update_option(self::OPTION_NAME, $sanitized);
1524
1525 $this->redirect_with_message('import-export', 'imported');
1526 }
1527
1528 private function get_default_settings(): array
1529 {
1530 return [
1531 'enabled' => false,
1532 'mode' => 'coming_soon',
1533 'template_source' => 'built_in',
1534 'template_id' => 'minimal',
1535 'template_content' => [],
1536 'page_id' => 0,
1537 'elementor_id' => 0,
1538 'theme' => 'dark',
1539 'noindex' => true,
1540 'retry_after' => 3600,
1541 'whitelist_ips' => [],
1542 'whitelist_paths' => [],
1543 'exclude_admin' => true,
1544 'allowed_roles' => [],
1545 'schedule_enabled' => false,
1546 'schedule_start' => '',
1547 'schedule_end' => '',
1548 'schedule_windows' => [],
1549 'recurring_enabled' => false,
1550 'recurring_rules' => [],
1551 'allow_rest' => true,
1552 'allow_admin_ajax' => true,
1553 'disable_elementor_editor' => true,
1554 'private_password_hash' => '',
1555 'private_token' => '',
1556 'custom_css' => '',
1557 'custom_js' => '',
1558 ];
1559 }
1560
1561 public function get_settings(): array
1562 {
1563 $defaults = $this->get_default_settings();
1564 $saved = get_option(self::OPTION_NAME, []);
1565
1566 $settings = wp_parse_args($saved, $defaults);
1567 if (!is_array($settings['template_content'])) {
1568 $settings['template_content'] = $defaults['template_content'];
1569 }
1570 if (!is_array($settings['whitelist_ips'])) {
1571 $settings['whitelist_ips'] = $defaults['whitelist_ips'];
1572 }
1573 if (!is_array($settings['whitelist_paths'])) {
1574 $settings['whitelist_paths'] = $defaults['whitelist_paths'];
1575 }
1576 if (!is_array($settings['allowed_roles'])) {
1577 $settings['allowed_roles'] = $defaults['allowed_roles'];
1578 }
1579
1580 if (!is_array($settings['schedule_windows'])) {
1581 $settings['schedule_windows'] = $defaults['schedule_windows'];
1582 }
1583
1584 if (!is_array($settings['recurring_rules'])) {
1585 $settings['recurring_rules'] = $defaults['recurring_rules'];
1586 }
1587
1588 return $settings;
1589 }
1590
1591 public function sanitize_settings(array $settings): array
1592 {
1593 $defaults = $this->get_default_settings();
1594 $existing = $this->get_settings();
1595 $settings = wp_parse_args($settings, $existing);
1596 $templates = array_keys($this->get_builtin_templates());
1597 $pro_templates = $this->get_pro_templates();
1598
1599 $source = isset($settings['template_source']) ? sanitize_key($settings['template_source']) : $defaults['template_source'];
1600 if (!in_array($source, ['built_in', 'page', 'elementor'], true)) {
1601 $source = 'built_in';
1602 }
1603
1604 $template_id = isset($settings['template_id']) ? sanitize_key($settings['template_id']) : $defaults['template_id'];
1605 if (!in_array($template_id, $templates, true)) {
1606 $template_id = $defaults['template_id'];
1607 }
1608 if (!$this->is_pro() && in_array($template_id, $pro_templates, true)) {
1609 $template_id = 'minimal';
1610 }
1611
1612 $mode = isset($settings['mode']) ? sanitize_key($settings['mode']) : $defaults['mode'];
1613 if (!in_array($mode, ['coming_soon', 'maintenance'], true)) {
1614 $mode = $defaults['mode'];
1615 }
1616
1617 $theme = isset($settings['theme']) ? sanitize_key($settings['theme']) : $defaults['theme'];
1618 $theme = $theme === 'light' ? 'light' : 'dark';
1619
1620 $whitelist_ips = $this->sanitize_list($settings['whitelist_ips'] ?? []);
1621 $whitelist_paths = $this->sanitize_list($settings['whitelist_paths'] ?? []);
1622
1623 if (!$this->is_pro()) {
1624 $whitelist_ips = array_slice($whitelist_ips, 0, 10);
1625 $whitelist_paths = array_slice($whitelist_paths, 0, 10);
1626 }
1627
1628 $legacyScheduleStart = $this->sanitize_datetime($settings['schedule_start'] ?? '');
1629 $legacyScheduleEnd = $this->sanitize_datetime($settings['schedule_end'] ?? '');
1630
1631 $scheduleWindows = $this->sanitize_schedule_windows($settings['schedule_windows'] ?? []);
1632 if ($scheduleWindows === [] && ($legacyScheduleStart !== '' || $legacyScheduleEnd !== '')) {
1633 $scheduleWindows[] = [
1634 'start' => $legacyScheduleStart,
1635 'end' => $legacyScheduleEnd,
1636 'timezone' => self::DEFAULT_TIMEZONE,
1637 ];
1638 }
1639
1640 if ($scheduleWindows !== []) {
1641 $legacyScheduleStart = (string) ($scheduleWindows[0]['start'] ?? $legacyScheduleStart);
1642 $legacyScheduleEnd = (string) ($scheduleWindows[0]['end'] ?? $legacyScheduleEnd);
1643 }
1644
1645 $recurringRules = $this->sanitize_recurring_rules($settings['recurring_rules'] ?? []);
1646
1647 $clean = [
1648 'enabled' => !empty($settings['enabled']),
1649 'mode' => $mode,
1650 'template_source' => $source,
1651 'template_id' => $template_id,
1652 'template_content' => $this->sanitize_template_content($settings['template_content'] ?? []),
1653 'page_id' => absint($settings['page_id'] ?? 0),
1654 'elementor_id' => absint($settings['elementor_id'] ?? 0),
1655 'theme' => $theme,
1656 'noindex' => !empty($settings['noindex']),
1657 'retry_after' => max(0, absint($settings['retry_after'] ?? $defaults['retry_after'])),
1658 'whitelist_ips' => $whitelist_ips,
1659 'whitelist_paths' => $this->normalize_paths($whitelist_paths),
1660 'exclude_admin' => !empty($settings['exclude_admin']),
1661 'allowed_roles' => $this->sanitize_list($settings['allowed_roles'] ?? []),
1662 'schedule_enabled' => !empty($settings['schedule_enabled']),
1663 'schedule_start' => $legacyScheduleStart,
1664 'schedule_end' => $legacyScheduleEnd,
1665 'schedule_windows' => $scheduleWindows,
1666 'recurring_enabled' => !empty($settings['recurring_enabled']),
1667 'recurring_rules' => $recurringRules,
1668 'allow_rest' => !empty($settings['allow_rest']),
1669 'allow_admin_ajax' => !empty($settings['allow_admin_ajax']),
1670 'disable_elementor_editor' => !empty($settings['disable_elementor_editor']),
1671 'private_password_hash' => (string) ($existing['private_password_hash'] ?? ''),
1672 'private_token' => (string) ($existing['private_token'] ?? ''),
1673 'custom_css' => '',
1674 'custom_js' => '',
1675 ];
1676
1677 if ($this->is_pro()) {
1678 $remove_password = !empty($settings['private_password_remove']);
1679 $plain = isset($settings['private_password']) ? (string) $settings['private_password'] : '';
1680 $plain = trim($plain);
1681
1682 if ($remove_password) {
1683 $clean['private_password_hash'] = '';
1684 } elseif ($plain !== '') {
1685 $clean['private_password_hash'] = function_exists('wp_hash_password') ? wp_hash_password($plain) : $clean['private_password_hash'];
1686 }
1687
1688 $token = isset($settings['private_token']) ? sanitize_text_field((string) $settings['private_token']) : (string) ($existing['private_token'] ?? '');
1689 $clean['private_token'] = $token;
1690 }
1691
1692 if (!$this->is_pro()) {
1693 $clean['allowed_roles'] = [];
1694 }
1695
1696 return $clean;
1697 }
1698
1699 private function sanitize_schedule_windows($windows): array
1700 {
1701 if (!is_array($windows)) {
1702 return [];
1703 }
1704
1705 $clean = [];
1706 foreach ($windows as $window) {
1707 if (!is_array($window)) {
1708 continue;
1709 }
1710
1711 $tz = isset($window['timezone']) ? sanitize_text_field((string) $window['timezone']) : self::DEFAULT_TIMEZONE;
1712 if ($tz === '') {
1713 $tz = self::DEFAULT_TIMEZONE;
1714 }
1715
1716 $start = $this->sanitize_datetime_with_timezone((string) ($window['start'] ?? ''), $tz);
1717 $end = $this->sanitize_datetime_with_timezone((string) ($window['end'] ?? ''), $tz);
1718
1719 if ($start === '' && $end === '') {
1720 continue;
1721 }
1722
1723 $clean[] = [
1724 'start' => $start,
1725 'end' => $end,
1726 'timezone' => $tz,
1727 ];
1728 }
1729
1730 return $clean;
1731 }
1732
1733 private function sanitize_recurring_rules($rules): array
1734 {
1735 if (!is_array($rules)) {
1736 return [];
1737 }
1738
1739 $clean = [];
1740 foreach ($rules as $rule) {
1741 if (!is_array($rule)) {
1742 continue;
1743 }
1744
1745 $freq = isset($rule['frequency']) ? sanitize_key((string) $rule['frequency']) : '';
1746 if (!in_array($freq, ['daily', 'weekly', 'monthly'], true)) {
1747 continue;
1748 }
1749
1750 $tz = isset($rule['timezone']) ? sanitize_text_field((string) $rule['timezone']) : self::DEFAULT_TIMEZONE;
1751 if ($tz === '') {
1752 $tz = self::DEFAULT_TIMEZONE;
1753 }
1754
1755 $startTime = sanitize_text_field((string) ($rule['start_time'] ?? ''));
1756 $endTime = sanitize_text_field((string) ($rule['end_time'] ?? ''));
1757 if ($this->parse_time_minutes($startTime) < 0 || $this->parse_time_minutes($endTime) < 0) {
1758 continue;
1759 }
1760
1761 $daysOfWeek = [];
1762 if ($freq === 'weekly') {
1763 $raw = $rule['days_of_week'] ?? [];
1764 if (!is_array($raw)) {
1765 $raw = preg_split('/\s*,\s*/', (string) $raw);
1766 }
1767
1768 $daysOfWeek = array_values(array_unique(array_filter(array_map(static function ($v) {
1769 $n = absint($v);
1770 return ($n >= 1 && $n <= 7) ? $n : 0;
1771 }, (array) $raw))));
1772 }
1773
1774 $daysOfMonth = [];
1775 if ($freq === 'monthly') {
1776 $raw = $rule['days_of_month'] ?? [];
1777 if (!is_array($raw)) {
1778 $raw = preg_split('/\s*,\s*/', (string) $raw);
1779 }
1780
1781 $daysOfMonth = array_values(array_unique(array_filter(array_map(static function ($v) {
1782 $n = absint($v);
1783 return ($n >= 1 && $n <= 31) ? $n : 0;
1784 }, (array) $raw))));
1785 }
1786
1787 $item = [
1788 'frequency' => $freq,
1789 'timezone' => $tz,
1790 'start_time' => $startTime,
1791 'end_time' => $endTime,
1792 'days_of_week' => $daysOfWeek,
1793 'days_of_month' => $daysOfMonth,
1794 ];
1795
1796 $clean[] = $item;
1797 }
1798
1799 return $clean;
1800 }
1801
1802 public function get_template_content(string $template_id): array
1803 {
1804 $templates = $this->get_builtin_templates();
1805 $template_id = isset($templates[$template_id]) ? $template_id : 'minimal';
1806 $template_id = $this->ensure_template_allowed($template_id);
1807
1808 $defaults = $this->get_template_content_defaults($template_id);
1809 $saved = $this->settings['template_content'][$template_id] ?? [];
1810 if (!is_array($saved)) {
1811 $saved = [];
1812 }
1813
1814 return wp_parse_args($saved, $defaults);
1815 }
1816
1817 private function get_template_content_defaults(string $template_id): array
1818 {
1819 $mode = $this->settings['mode'] ?? 'coming_soon';
1820 $site_name = get_bloginfo('name');
1821 $tagline = get_bloginfo('description');
1822
1823 $headline = $mode === 'maintenance'
1824 ? __('We are upgrading the experience.', 'king-addons')
1825 : __('A new experience is on the horizon.', 'king-addons');
1826
1827 $subhead = $tagline !== '' ? $tagline : __('Our team is preparing something beautiful. Stay tuned.', 'king-addons');
1828
1829 $base = [
1830 'badge' => '',
1831 'headline' => $headline,
1832 'subhead' => $subhead,
1833 'launch_label' => '',
1834 'footer_left' => $site_name,
1835 'footer_right' => __('Powered by King Addons', 'king-addons'),
1836 ];
1837
1838 if ($template_id === 'countdown') {
1839 $base['countdown_days'] = '14';
1840 $base['countdown_hours'] = '06';
1841 $base['countdown_minutes'] = '42';
1842 }
1843
1844 if ($template_id === 'progress') {
1845 $base['progress_percent'] = 68;
1846 $base['progress_label'] = __('68% complete', 'king-addons');
1847 }
1848
1849 if (in_array($template_id, ['subscribe', 'product-launch'], true)) {
1850 $base['form_placeholder'] = __('Email address', 'king-addons');
1851 $base['form_button'] = __('Notify me', 'king-addons');
1852 }
1853
1854 if ($template_id === 'split') {
1855 $base['split_title_a'] = __('What is happening', 'king-addons');
1856 $base['split_text_a'] = __('We are refining the experience with faster performance and new visuals.', 'king-addons');
1857 $base['split_title_b'] = __('Stay connected', 'king-addons');
1858 $base['split_text_b'] = __('Follow our updates while we prepare the launch.', 'king-addons');
1859 }
1860
1861 return $base;
1862 }
1863
1864 private function sanitize_template_content(array $content): array
1865 {
1866 $fields = $this->get_template_content_fields();
1867 $clean = [];
1868
1869 foreach ($fields as $template_id => $template_fields) {
1870 $values = isset($content[$template_id]) && is_array($content[$template_id]) ? $content[$template_id] : [];
1871 $template_clean = [];
1872
1873 foreach ($template_fields as $field => $type) {
1874 $value = $values[$field] ?? '';
1875 if ($type === 'int') {
1876 $int = absint($value);
1877 if ($field === 'progress_percent') {
1878 $int = min(100, max(0, $int));
1879 }
1880 if ($field === 'countdown_hours') {
1881 $int = min(23, max(0, $int));
1882 }
1883 if ($field === 'countdown_minutes') {
1884 $int = min(59, max(0, $int));
1885 }
1886 $template_clean[$field] = $int;
1887 } else {
1888 $template_clean[$field] = sanitize_text_field((string) $value);
1889 }
1890 }
1891
1892 $clean[$template_id] = $template_clean;
1893 }
1894
1895 return $clean;
1896 }
1897
1898 private function get_template_content_fields(): array
1899 {
1900 $base = [
1901 'badge' => 'text',
1902 'headline' => 'text',
1903 'subhead' => 'text',
1904 'launch_label' => 'text',
1905 'footer_left' => 'text',
1906 'footer_right' => 'text',
1907 ];
1908
1909 return [
1910 'minimal' => $base,
1911 'dark' => $base,
1912 'gradient' => $base,
1913 'aurora' => $base,
1914 'neon' => $base,
1915 'paper' => $base,
1916 'grid' => $base,
1917 'mono' => $base,
1918 'spotlight' => $base,
1919 'poster' => $base,
1920 'ribbon' => $base,
1921 'construction' => $base,
1922 'logo' => $base,
1923 'countdown' => $base + [
1924 'countdown_days' => 'int',
1925 'countdown_hours' => 'int',
1926 'countdown_minutes' => 'int',
1927 ],
1928 'progress' => $base + [
1929 'progress_percent' => 'int',
1930 'progress_label' => 'text',
1931 ],
1932 'subscribe' => $base + [
1933 'form_placeholder' => 'text',
1934 'form_button' => 'text',
1935 ],
1936 'product-launch' => $base + [
1937 'form_placeholder' => 'text',
1938 'form_button' => 'text',
1939 ],
1940 'split' => $base + [
1941 'split_title_a' => 'text',
1942 'split_text_a' => 'text',
1943 'split_title_b' => 'text',
1944 'split_text_b' => 'text',
1945 ],
1946 ];
1947 }
1948
1949 private function ensure_template_allowed(string $template_id): string
1950 {
1951 if (!$this->is_pro() && in_array($template_id, $this->get_pro_templates(), true)) {
1952 return 'minimal';
1953 }
1954
1955 return $template_id;
1956 }
1957
1958 private function sanitize_list($value): array
1959 {
1960 if (is_array($value)) {
1961 $items = $value;
1962 } else {
1963 $items = preg_split('/\\r\\n|\\r|\\n|,/', (string) $value);
1964 }
1965
1966 $items = array_map('trim', $items);
1967 $items = array_filter($items, static function ($item) {
1968 return $item !== '';
1969 });
1970 $items = array_map('sanitize_text_field', $items);
1971
1972 return array_values(array_unique($items));
1973 }
1974
1975 private function normalize_paths(array $paths): array
1976 {
1977 $normalized = [];
1978 foreach ($paths as $path) {
1979 $path = wp_parse_url($path, PHP_URL_PATH) ?: $path;
1980 $path = '/' . ltrim((string) $path, '/');
1981 $normalized[] = $path === '' ? '/' : $path;
1982 }
1983
1984 return array_values(array_unique($normalized));
1985 }
1986
1987 private function sanitize_datetime(string $value): string
1988 {
1989 $value = sanitize_text_field($value);
1990 if ($value === '') {
1991 return '';
1992 }
1993
1994 $value = str_replace('T', ' ', $value);
1995 if (strlen($value) === 16) {
1996 $value .= ':00';
1997 }
1998
1999 $timestamp = strtotime($value);
2000 if (!$timestamp) {
2001 return '';
2002 }
2003
2004 return get_gmt_from_date($value, 'Y-m-d H:i:s');
2005 }
2006
2007 private function sanitize_datetime_with_timezone(string $value, string $timezone): string
2008 {
2009 $value = sanitize_text_field($value);
2010 if ($value === '') {
2011 return '';
2012 }
2013
2014 $value = str_replace('T', ' ', $value);
2015 if (strlen($value) === 16) {
2016 $value .= ':00';
2017 }
2018
2019 if (!preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $value)) {
2020 return '';
2021 }
2022
2023 $tz = $this->resolve_timezone($timezone);
2024 $dt = \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $value, $tz);
2025 if (!$dt) {
2026 return '';
2027 }
2028
2029 return $dt->setTimezone(new \DateTimeZone('UTC'))->format('Y-m-d H:i:s');
2030 }
2031
2032 private function parse_schedule_time(string $value): int
2033 {
2034 if ($value === '') {
2035 return 0;
2036 }
2037
2038 return (int) strtotime($value . ' UTC');
2039 }
2040
2041 private function get_client_ip(): string
2042 {
2043 if (!empty($_SERVER['REMOTE_ADDR']) && filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP)) {
2044 return (string) $_SERVER['REMOTE_ADDR'];
2045 }
2046
2047 return '';
2048 }
2049
2050 private function is_preview_request(): bool
2051 {
2052 if (empty($_GET['kng_maintenance_preview'])) {
2053 return false;
2054 }
2055
2056 if (!current_user_can('manage_options')) {
2057 return false;
2058 }
2059
2060 $nonce = isset($_GET['_kng_preview_nonce']) ? sanitize_text_field(wp_unslash($_GET['_kng_preview_nonce'])) : '';
2061 if ($nonce === '' || !wp_verify_nonce($nonce, 'kng_maintenance_preview')) {
2062 return false;
2063 }
2064
2065 return true;
2066 }
2067
2068 private function is_pro(): bool
2069 {
2070 return function_exists('king_addons_freemius') && king_addons_freemius()->can_use_premium_code__premium_only();
2071 }
2072
2073 private function redirect_with_message(string $view, string $message): void
2074 {
2075 $args = [
2076 'page' => 'king-addons-maintenance-mode',
2077 'view' => $view,
2078 'message' => $message,
2079 ];
2080
2081 wp_safe_redirect(add_query_arg($args, admin_url('admin.php')));
2082 exit;
2083 }
2084 }
2085