*/
private array $settings = [];
private string $private_access_error = '';
private string $private_access_redirect_to = '';
public static function instance(): Maintenance_Mode
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct()
{
$this->settings = $this->get_settings();
add_action('admin_menu', [$this, 'register_admin_menu']);
add_action('admin_init', [$this, 'register_settings']);
add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']);
add_action('template_redirect', [$this, 'maybe_render_maintenance'], 1);
add_action('admin_post_kng_maintenance_export', [$this, 'handle_export']);
add_action('admin_post_kng_maintenance_import', [$this, 'handle_import']);
add_action('admin_post_kng_maintenance_reset_analytics', [$this, 'handle_reset_analytics']);
add_action('admin_post_kng_maintenance_generate_token', [$this, 'handle_generate_private_token']);
add_action('admin_post_kng_maintenance_revoke_token', [$this, 'handle_revoke_private_token']);
add_action('admin_post_kng_maintenance_revoke_password', [$this, 'handle_revoke_private_password']);
add_shortcode('kng_maintenance_page', [$this, 'render_shortcode']);
}
public function register_admin_menu(): void
{
add_submenu_page(
'king-addons',
__('Maintenance Mode', 'king-addons'),
__('Maintenance Mode', 'king-addons'),
'manage_options',
'king-addons-maintenance-mode',
[$this, 'render_admin_page']
);
}
public function register_settings(): void
{
register_setting(
'kng_maintenance_settings_group',
self::OPTION_NAME,
[
'type' => 'array',
'sanitize_callback' => [$this, 'sanitize_settings'],
'default' => $this->get_default_settings(),
]
);
}
public function enqueue_admin_assets(string $hook): void
{
if ($hook !== 'king-addons_page_king-addons-maintenance-mode') {
return;
}
$shared_css = KING_ADDONS_URL . 'includes/admin/layouts/shared/admin-v3-styles.css';
$shared_path = KING_ADDONS_PATH . 'includes/admin/layouts/shared/admin-v3-styles.css';
$shared_version = file_exists($shared_path) ? filemtime($shared_path) : KING_ADDONS_VERSION;
wp_enqueue_style('king-addons-admin-v3', $shared_css, [], $shared_version);
$admin_css = KING_ADDONS_URL . 'includes/extensions/Maintenance_Mode/assets/admin.css';
$admin_path = KING_ADDONS_PATH . 'includes/extensions/Maintenance_Mode/assets/admin.css';
$admin_version = file_exists($admin_path) ? filemtime($admin_path) : KING_ADDONS_VERSION;
wp_enqueue_style('king-addons-maintenance-admin', $admin_css, ['king-addons-admin-v3'], $admin_version);
$admin_js = KING_ADDONS_URL . 'includes/extensions/Maintenance_Mode/assets/admin.js';
$admin_path_js = KING_ADDONS_PATH . 'includes/extensions/Maintenance_Mode/assets/admin.js';
$admin_js_version = file_exists($admin_path_js) ? filemtime($admin_path_js) : KING_ADDONS_VERSION;
wp_enqueue_script('king-addons-maintenance-admin', $admin_js, ['jquery'], $admin_js_version, true);
wp_localize_script('king-addons-maintenance-admin', 'KNGMaintenance', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'themeNonce' => wp_create_nonce('king_addons_dashboard_ui'),
]);
}
public function render_admin_page(): void
{
if (!current_user_can('manage_options')) {
return;
}
$view = isset($_GET['view']) ? sanitize_key($_GET['view']) : 'dashboard';
$is_pro = $this->is_pro();
$settings = $this->get_settings();
$templates = $this->get_builtin_templates();
include __DIR__ . '/templates/admin-page.php';
}
public function maybe_render_maintenance(): void
{
if ($this->is_preview_request()) {
$this->render_maintenance_response(true);
exit;
}
if (!$this->is_mode_active()) {
return;
}
$bypass_reason = $this->get_bypass_reason();
if ($bypass_reason !== '') {
$this->track_bypass($bypass_reason);
if ($this->private_access_redirect_to !== '') {
wp_safe_redirect($this->private_access_redirect_to);
exit;
}
return;
}
$this->track_blocked_visit();
$this->render_maintenance_response(false);
exit;
}
public function handle_reset_analytics(): void
{
if (!current_user_can('manage_options')) {
wp_die(esc_html__('Unauthorized request.', 'king-addons'));
}
check_admin_referer('kng_maintenance_reset_analytics');
delete_option(self::ANALYTICS_OPTION);
delete_transient(self::ANALYTICS_TRANSIENT_24H);
$this->redirect_with_message('analytics', 'analytics_reset');
}
private function is_mode_active(): bool
{
if (empty($this->settings['enabled'])) {
return false;
}
if (empty($this->settings['schedule_enabled'])) {
return true;
}
return $this->is_any_schedule_window_active() || $this->is_any_recurring_rule_active();
}
private function is_any_schedule_window_active(): bool
{
$now = current_time('timestamp', true);
$windows = $this->settings['schedule_windows'] ?? [];
if (!is_array($windows)) {
$windows = [];
}
foreach ($windows as $window) {
if (!is_array($window)) {
continue;
}
$start = $this->parse_schedule_time((string) ($window['start'] ?? ''));
$end = $this->parse_schedule_time((string) ($window['end'] ?? ''));
if ($this->is_time_range_active($now, $start, $end)) {
return true;
}
}
$start = $this->parse_schedule_time($this->settings['schedule_start'] ?? '');
$end = $this->parse_schedule_time($this->settings['schedule_end'] ?? '');
return $this->is_time_range_active($now, $start, $end);
}
private function is_time_range_active(int $now, int $start, int $end): bool
{
if ($start && $end) {
return $now >= $start && $now <= $end;
}
if ($start && !$end) {
return $now >= $start;
}
if (!$start && $end) {
return $now <= $end;
}
return false;
}
private function is_any_recurring_rule_active(): bool
{
if (empty($this->settings['recurring_enabled'])) {
return false;
}
$rules = $this->settings['recurring_rules'] ?? [];
if (!is_array($rules) || $rules === []) {
return false;
}
$nowTs = current_time('timestamp', true);
$nowUtc = (new \DateTimeImmutable('@' . $nowTs))->setTimezone(new \DateTimeZone('UTC'));
foreach ($rules as $rule) {
if (!is_array($rule)) {
continue;
}
if ($this->is_recurring_rule_active($rule, $nowUtc)) {
return true;
}
}
return false;
}
private function is_recurring_rule_active(array $rule, \DateTimeImmutable $nowUtc): bool
{
$freq = isset($rule['frequency']) ? sanitize_key((string) $rule['frequency']) : '';
if (!in_array($freq, ['daily', 'weekly', 'monthly'], true)) {
return false;
}
$tzString = isset($rule['timezone']) ? sanitize_text_field((string) $rule['timezone']) : self::DEFAULT_TIMEZONE;
$tz = $this->resolve_timezone($tzString);
$local = $nowUtc->setTimezone($tz);
$startMinutes = $this->parse_time_minutes((string) ($rule['start_time'] ?? ''));
$endMinutes = $this->parse_time_minutes((string) ($rule['end_time'] ?? ''));
if ($startMinutes < 0 || $endMinutes < 0) {
return false;
}
$nowMinutes = ((int) $local->format('G')) * 60 + (int) $local->format('i');
$inTime = $this->is_minutes_in_range($nowMinutes, $startMinutes, $endMinutes);
if (!$inTime) {
return false;
}
if ($freq === 'daily') {
return true;
}
if ($freq === 'weekly') {
$days = $rule['days_of_week'] ?? [];
if (!is_array($days) || $days === []) {
return false;
}
$dow = (int) $local->format('N');
return in_array($dow, array_map('intval', $days), true);
}
$days = $rule['days_of_month'] ?? [];
if (!is_array($days) || $days === []) {
return false;
}
$dom = (int) $local->format('j');
return in_array($dom, array_map('intval', $days), true);
}
private function is_minutes_in_range(int $value, int $start, int $end): bool
{
if ($start === $end) {
return false;
}
if ($start < $end) {
return $value >= $start && $value <= $end;
}
return $value >= $start || $value <= $end;
}
private function parse_time_minutes(string $value): int
{
$value = trim($value);
if (!preg_match('/^(\d{1,2}):(\d{2})$/', $value, $m)) {
return -1;
}
$h = (int) $m[1];
$i = (int) $m[2];
if ($h < 0 || $h > 23 || $i < 0 || $i > 59) {
return -1;
}
return $h * 60 + $i;
}
private function resolve_timezone(string $timezone): \DateTimeZone
{
$timezone = $timezone !== '' ? $timezone : self::DEFAULT_TIMEZONE;
if ($timezone === self::DEFAULT_TIMEZONE) {
return function_exists('wp_timezone') ? wp_timezone() : new \DateTimeZone('UTC');
}
try {
return new \DateTimeZone($timezone);
} catch (\Exception $e) {
return function_exists('wp_timezone') ? wp_timezone() : new \DateTimeZone('UTC');
}
}
private function should_bypass_request(): bool
{
return $this->get_bypass_reason() !== '';
}
private function get_bypass_reason(): string
{
if (defined('WP_CLI') && WP_CLI) {
return 'wp_cli';
}
if (wp_doing_cron()) {
return 'cron';
}
if (is_admin()) {
return 'wp_admin';
}
if (wp_doing_ajax() && !empty($this->settings['allow_admin_ajax'])) {
return 'admin_ajax_allowed';
}
if ($this->is_login_request()) {
return 'login_page';
}
if (!empty($this->settings['disable_elementor_editor']) && $this->is_elementor_editor()) {
return 'elementor_editor';
}
$private_reason = $this->get_private_access_bypass_reason();
if ($private_reason !== '') {
return $private_reason;
}
if ($this->is_rest_request()) {
if (is_user_logged_in()) {
return 'rest_logged_in';
}
return !empty($this->settings['allow_rest']) ? 'rest_allowed' : '';
}
if ($this->is_user_allowed()) {
return 'user_allowed';
}
if ($this->is_ip_whitelisted()) {
return 'ip_whitelist';
}
if ($this->is_path_whitelisted()) {
return 'path_whitelist';
}
return '';
}
private function is_private_access_enabled(): bool
{
if (!$this->is_pro()) {
return false;
}
return !empty($this->settings['private_password_hash']) || !empty($this->settings['private_token']);
}
private function get_private_access_bypass_reason(): string
{
if (!$this->is_private_access_enabled()) {
return '';
}
if ($this->has_private_access_cookie()) {
return 'private_cookie';
}
$token = isset($_GET[self::PRIVATE_ACCESS_QUERY_PARAM])
? sanitize_text_field(wp_unslash($_GET[self::PRIVATE_ACCESS_QUERY_PARAM]))
: '';
if ($token !== '' && $this->is_private_token_valid($token)) {
$this->set_private_access_cookie();
$this->private_access_redirect_to = $this->get_current_url_without_private_params();
return 'private_token';
}
if ($this->maybe_accept_private_password()) {
$this->private_access_redirect_to = $this->get_current_url_without_private_params();
return 'private_password';
}
return '';
}
private function maybe_accept_private_password(): bool
{
if (empty($this->settings['private_password_hash'])) {
return false;
}
if (!isset($_POST[self::PRIVATE_ACCESS_POST_FLAG])) {
return false;
}
$nonce = isset($_POST['_kng_private_nonce']) ? sanitize_text_field(wp_unslash($_POST['_kng_private_nonce'])) : '';
if ($nonce === '' || !wp_verify_nonce($nonce, 'kng_maintenance_private_access')) {
$this->private_access_error = 'invalid_nonce';
return false;
}
$password = isset($_POST['kng_maintenance_private_password'])
? (string) wp_unslash($_POST['kng_maintenance_private_password'])
: '';
$password = trim($password);
if ($password === '') {
$this->private_access_error = 'invalid_password';
return false;
}
$hash = (string) ($this->settings['private_password_hash'] ?? '');
if ($hash === '' || !function_exists('wp_check_password')) {
$this->private_access_error = 'invalid_password';
return false;
}
if (!wp_check_password($password, $hash)) {
$this->private_access_error = 'invalid_password';
return false;
}
$this->set_private_access_cookie();
return true;
}
private function is_private_token_valid(string $token): bool
{
$saved = (string) ($this->settings['private_token'] ?? '');
if ($saved === '' || $token === '') {
return false;
}
return hash_equals($saved, $token);
}
private function get_private_access_cookie_expected(): string
{
$key = defined('AUTH_SALT') && AUTH_SALT ? AUTH_SALT : (defined('NONCE_SALT') && NONCE_SALT ? NONCE_SALT : 'kng');
$hash = (string) ($this->settings['private_password_hash'] ?? '');
$token = (string) ($this->settings['private_token'] ?? '');
$material = $hash . '|' . $token;
return hash_hmac('sha256', 'private_access|' . $material, $key);
}
private function has_private_access_cookie(): bool
{
$expected = $this->get_private_access_cookie_expected();
if ($expected === '') {
return false;
}
$cookie = isset($_COOKIE[self::PRIVATE_ACCESS_COOKIE]) ? (string) wp_unslash($_COOKIE[self::PRIVATE_ACCESS_COOKIE]) : '';
if ($cookie === '') {
return false;
}
return hash_equals($expected, $cookie);
}
private function set_private_access_cookie(): void
{
$value = $this->get_private_access_cookie_expected();
if ($value === '') {
return;
}
$expires = time() + (int) apply_filters('kng_maintenance_private_cookie_ttl', 7 * DAY_IN_SECONDS);
$path = defined('COOKIEPATH') ? (string) COOKIEPATH : '/';
if ($path === '') {
$path = '/';
}
$args = [
'expires' => $expires,
'path' => $path,
'secure' => is_ssl(),
'httponly' => true,
'samesite' => 'Lax',
];
$domain = defined('COOKIE_DOMAIN') ? (string) COOKIE_DOMAIN : '';
if ($domain !== '') {
$args['domain'] = $domain;
}
setcookie(self::PRIVATE_ACCESS_COOKIE, $value, $args);
$_COOKIE[self::PRIVATE_ACCESS_COOKIE] = $value;
}
private function get_current_url_without_private_params(): string
{
$request_uri = isset($_SERVER['REQUEST_URI']) ? (string) wp_unslash($_SERVER['REQUEST_URI']) : '/';
$url = home_url($request_uri);
$url = remove_query_arg([self::PRIVATE_ACCESS_QUERY_PARAM], $url);
return $url;
}
private function track_blocked_visit(): void
{
if ($this->is_preview_request()) {
return;
}
if (is_admin() || wp_doing_cron() || (defined('WP_CLI') && WP_CLI)) {
return;
}
$analytics = $this->get_analytics();
$analytics['blocked_total'] = (int) ($analytics['blocked_total'] ?? 0) + 1;
$analytics['updated_at'] = time();
update_option(self::ANALYTICS_OPTION, $analytics, false);
$this->update_24h_analytics('blocked', '', $this->get_masked_request_path());
$ip = $this->get_client_ip();
if ($ip !== '') {
$this->update_24h_unique_ip($ip);
}
}
private function track_bypass(string $reason = ''): void
{
if ($this->is_preview_request()) {
return;
}
if (is_admin() || wp_doing_cron() || (defined('WP_CLI') && WP_CLI)) {
return;
}
$analytics = $this->get_analytics();
$analytics['bypass_total'] = (int) ($analytics['bypass_total'] ?? 0) + 1;
if ($reason !== '') {
if (!isset($analytics['bypass_by_reason']) || !is_array($analytics['bypass_by_reason'])) {
$analytics['bypass_by_reason'] = [];
}
$analytics['bypass_by_reason'][$reason] = (int) ($analytics['bypass_by_reason'][$reason] ?? 0) + 1;
}
$analytics['updated_at'] = time();
update_option(self::ANALYTICS_OPTION, $analytics, false);
$this->update_24h_analytics('bypass', $reason, $this->get_masked_request_path());
}
private function get_analytics(): array
{
$saved = get_option(self::ANALYTICS_OPTION, []);
if (!is_array($saved)) {
$saved = [];
}
$defaults = [
'blocked_total' => 0,
'bypass_total' => 0,
'bypass_by_reason' => [],
'created_at' => time(),
'updated_at' => time(),
];
$data = wp_parse_args($saved, $defaults);
if (!is_array($data['bypass_by_reason'])) {
$data['bypass_by_reason'] = [];
}
return $data;
}
private function get_analytics_24h(): array
{
$saved = get_transient(self::ANALYTICS_TRANSIENT_24H);
if (!is_array($saved)) {
$saved = [];
}
$defaults = [
'blocked' => 0,
'bypass' => 0,
'bypass_by_reason' => [],
'unique' => [],
'paths' => [
'blocked' => [],
'bypass' => [],
],
];
$data = wp_parse_args($saved, $defaults);
if (!is_array($data['unique'])) {
$data['unique'] = [];
}
if (!is_array($data['bypass_by_reason'])) {
$data['bypass_by_reason'] = [];
}
if (!isset($data['paths']) || !is_array($data['paths'])) {
$data['paths'] = ['blocked' => [], 'bypass' => []];
}
if (!isset($data['paths']['blocked']) || !is_array($data['paths']['blocked'])) {
$data['paths']['blocked'] = [];
}
if (!isset($data['paths']['bypass']) || !is_array($data['paths']['bypass'])) {
$data['paths']['bypass'] = [];
}
$this->prune_24h_unique($data);
$this->prune_24h_paths($data);
return $data;
}
private function save_analytics_24h(array $data): void
{
$this->prune_24h_unique($data);
$this->prune_24h_paths($data);
set_transient(self::ANALYTICS_TRANSIENT_24H, $data, DAY_IN_SECONDS + HOUR_IN_SECONDS);
}
private function prune_24h_unique(array &$data): void
{
$cutoff = time() - DAY_IN_SECONDS;
if (!isset($data['unique']) || !is_array($data['unique'])) {
$data['unique'] = [];
return;
}
foreach ($data['unique'] as $hash => $ts) {
if ((int) $ts < $cutoff) {
unset($data['unique'][$hash]);
}
}
$max = 10000;
if (count($data['unique']) > $max) {
$data['unique'] = array_slice($data['unique'], -$max, null, true);
}
}
private function prune_24h_paths(array &$data): void
{
$cutoff = time() - DAY_IN_SECONDS;
if (!isset($data['paths']) || !is_array($data['paths'])) {
$data['paths'] = ['blocked' => [], 'bypass' => []];
return;
}
foreach (['blocked', 'bypass'] as $bucket) {
if (!isset($data['paths'][$bucket]) || !is_array($data['paths'][$bucket])) {
$data['paths'][$bucket] = [];
continue;
}
foreach ($data['paths'][$bucket] as $hash => $row) {
$ts = is_array($row) ? (int) ($row['t'] ?? 0) : 0;
if ($ts < $cutoff) {
unset($data['paths'][$bucket][$hash]);
}
}
$max = 400;
if (count($data['paths'][$bucket]) > $max) {
uasort($data['paths'][$bucket], static function ($a, $b) {
$ta = is_array($a) ? (int) ($a['t'] ?? 0) : 0;
$tb = is_array($b) ? (int) ($b['t'] ?? 0) : 0;
return $ta <=> $tb;
});
$data['paths'][$bucket] = array_slice($data['paths'][$bucket], -$max, null, true);
}
}
}
private function update_24h_analytics(string $type, string $reason = '', string $masked_path = ''): void
{
$data = $this->get_analytics_24h();
if ($type === 'blocked') {
$data['blocked'] = (int) ($data['blocked'] ?? 0) + 1;
} elseif ($type === 'bypass') {
$data['bypass'] = (int) ($data['bypass'] ?? 0) + 1;
if ($reason !== '') {
if (!isset($data['bypass_by_reason'][$reason])) {
$data['bypass_by_reason'][$reason] = 0;
}
$data['bypass_by_reason'][$reason] = (int) $data['bypass_by_reason'][$reason] + 1;
}
}
if ($masked_path !== '' && in_array($type, ['blocked', 'bypass'], true)) {
if (!isset($data['paths']) || !is_array($data['paths'])) {
$data['paths'] = ['blocked' => [], 'bypass' => []];
}
if (!isset($data['paths'][$type]) || !is_array($data['paths'][$type])) {
$data['paths'][$type] = [];
}
$hash = $this->hash_string($masked_path);
if ($hash !== '') {
if (!isset($data['paths'][$type][$hash]) || !is_array($data['paths'][$type][$hash])) {
$data['paths'][$type][$hash] = ['c' => 0, 'm' => $masked_path, 't' => time()];
}
$data['paths'][$type][$hash]['c'] = (int) ($data['paths'][$type][$hash]['c'] ?? 0) + 1;
$data['paths'][$type][$hash]['m'] = $masked_path;
$data['paths'][$type][$hash]['t'] = time();
}
}
$this->save_analytics_24h($data);
}
private function hash_string(string $value): string
{
$value = trim($value);
if ($value === '') {
return '';
}
$key = defined('AUTH_SALT') && AUTH_SALT ? AUTH_SALT : (defined('NONCE_SALT') && NONCE_SALT ? NONCE_SALT : 'kng');
return hash_hmac('sha256', $value, $key);
}
private function get_request_path(): string
{
$request_uri = isset($_SERVER['REQUEST_URI']) ? (string) wp_unslash($_SERVER['REQUEST_URI']) : '';
$path = wp_parse_url($request_uri, PHP_URL_PATH);
$path = $path ? '/' . ltrim((string) $path, '/') : '/';
return $path;
}
private function get_masked_request_path(): string
{
return $this->mask_path($this->get_request_path());
}
private function mask_path(string $path): string
{
$path = $path !== '' ? '/' . ltrim($path, '/') : '/';
$trim = trim($path, '/');
if ($trim === '') {
return '/';
}
$segments = array_values(array_filter(explode('/', $trim), static fn($s) => $s !== ''));
$maxSegments = 2;
$shown = array_slice($segments, 0, $maxSegments);
$out = [];
foreach ($shown as $seg) {
$seg = rawurldecode((string) $seg);
$seg = preg_replace('/\s+/', '-', $seg);
if ($seg === null || $seg === '') {
continue;
}
if (preg_match('/^[0-9]+$/', $seg)) {
$out[] = '{n}';
continue;
}
if (preg_match('/^[a-f0-9]{16,}$/i', $seg)) {
$out[] = '{hash}';
continue;
}
if (preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i', $seg)) {
$out[] = '{uuid}';
continue;
}
$seg = preg_replace('/\d+/', '{n}', $seg);
$seg = preg_replace('/[^a-zA-Z0-9._\-{}]+/', '', (string) $seg);
$seg = substr((string) $seg, 0, 24);
if ($seg === '') {
$seg = '{seg}';
}
$out[] = $seg;
}
$masked = '/' . implode('/', $out);
if (count($segments) > $maxSegments) {
$masked .= '/…';
}
return substr($masked, 0, 80);
}
private function update_24h_unique_ip(string $ip): void
{
$hash = $this->hash_ip($ip);
if ($hash === '') {
return;
}
$data = $this->get_analytics_24h();
$data['unique'][$hash] = time();
$this->save_analytics_24h($data);
}
private function hash_ip(string $ip): string
{
$ip = trim($ip);
if ($ip === '') {
return '';
}
$key = defined('AUTH_SALT') && AUTH_SALT ? AUTH_SALT : (defined('NONCE_SALT') && NONCE_SALT ? NONCE_SALT : 'kng');
return hash_hmac('sha256', $ip, $key);
}
private function get_analytics_overview(): array
{
$all = $this->get_analytics();
$h24 = $this->get_analytics_24h();
$bypassAll = $all['bypass_by_reason'] ?? [];
if (!is_array($bypassAll)) {
$bypassAll = [];
}
$bypass24 = $h24['bypass_by_reason'] ?? [];
if (!is_array($bypass24)) {
$bypass24 = [];
}
arsort($bypassAll);
arsort($bypass24);
$topBlocked = $this->get_top_paths_24h($h24, 'blocked');
$topBypass = $this->get_top_paths_24h($h24, 'bypass');
return [
'blocked_total' => (int) ($all['blocked_total'] ?? 0),
'bypass_total' => (int) ($all['bypass_total'] ?? 0),
'blocked_24h' => (int) ($h24['blocked'] ?? 0),
'bypass_24h' => (int) ($h24['bypass'] ?? 0),
'unique_24h' => is_array($h24['unique'] ?? null) ? count($h24['unique']) : 0,
'bypass_by_reason_total' => $bypassAll,
'bypass_by_reason_24h' => $bypass24,
'top_paths_24h_blocked' => $topBlocked,
'top_paths_24h_bypass' => $topBypass,
];
}
private function get_top_paths_24h(array $h24, string $bucket, int $limit = 10): array
{
if (!isset($h24['paths']) || !is_array($h24['paths'])) {
return [];
}
if (!isset($h24['paths'][$bucket]) || !is_array($h24['paths'][$bucket])) {
return [];
}
$rows = [];
foreach ($h24['paths'][$bucket] as $row) {
if (!is_array($row)) {
continue;
}
$count = (int) ($row['c'] ?? 0);
$mask = isset($row['m']) ? (string) $row['m'] : '';
if ($count <= 0 || $mask === '') {
continue;
}
$rows[] = ['mask' => $mask, 'count' => $count];
}
usort($rows, static function ($a, $b) {
$ca = (int) ($a['count'] ?? 0);
$cb = (int) ($b['count'] ?? 0);
if ($ca === $cb) {
return strcmp((string) ($a['mask'] ?? ''), (string) ($b['mask'] ?? ''));
}
return $cb <=> $ca;
});
return array_slice($rows, 0, $limit);
}
private function is_user_allowed(): bool
{
if (!is_user_logged_in()) {
return false;
}
$user = wp_get_current_user();
if (!$user || !$user->ID) {
return false;
}
if (!empty($this->settings['exclude_admin']) && user_can($user, 'manage_options')) {
return true;
}
if ($this->is_pro()) {
$allowed_roles = $this->settings['allowed_roles'] ?? [];
if (!empty($allowed_roles) && array_intersect((array) $user->roles, $allowed_roles)) {
return true;
}
}
return false;
}
private function is_ip_whitelisted(): bool
{
$whitelist = $this->settings['whitelist_ips'] ?? [];
if (empty($whitelist)) {
return false;
}
$ip = $this->get_client_ip();
if ($ip === '') {
return false;
}
return in_array($ip, $whitelist, true);
}
private function is_path_whitelisted(): bool
{
$paths = $this->settings['whitelist_paths'] ?? [];
if (empty($paths)) {
return false;
}
$request_uri = isset($_SERVER['REQUEST_URI']) ? (string) wp_unslash($_SERVER['REQUEST_URI']) : '';
$path = wp_parse_url($request_uri, PHP_URL_PATH);
$path = $path ? '/' . ltrim($path, '/') : '/';
foreach ($paths as $allowed) {
if ($allowed !== '/' && strpos($path, $allowed) === 0) {
return true;
}
if ($allowed === $path) {
return true;
}
}
return false;
}
private function is_login_request(): bool
{
$request_uri = isset($_SERVER['REQUEST_URI']) ? (string) wp_unslash($_SERVER['REQUEST_URI']) : '';
return strpos($request_uri, 'wp-login.php') !== false || strpos($request_uri, 'wp-register.php') !== false;
}
private function is_rest_request(): bool
{
return defined('REST_REQUEST') && REST_REQUEST;
}
private function is_elementor_editor(): bool
{
if (isset($_GET['elementor-preview'])) {
return true;
}
if (class_exists('\\Elementor\\Plugin')) {
$plugin = \Elementor\Plugin::instance();
if ($plugin->editor && $plugin->editor->is_edit_mode()) {
return true;
}
}
return false;
}
private function render_maintenance_response(bool $is_preview): void
{
if (!defined('KING_ADDONS_IS_MAINTENANCE_PAGE')) {
define('KING_ADDONS_IS_MAINTENANCE_PAGE', true);
}
$mode = $this->settings['mode'] ?? 'coming_soon';
$status = $mode === 'maintenance' ? 503 : 200;
if ($is_preview) {
$status = 200;
}
status_header($status);
nocache_headers();
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
$retry_after = (int) ($this->settings['retry_after'] ?? 0);
if ($status === 503 && $retry_after > 0) {
header('Retry-After: ' . $retry_after);
}
if (!empty($this->settings['noindex'])) {
header('X-Robots-Tag: noindex, nofollow', true);
}
$title = $mode === 'maintenance' ? __('Maintenance Mode', 'king-addons') : __('Coming Soon', 'king-addons');
$content = $this->get_rendered_page_content();
$this->enqueue_frontend_assets();
echo '';
echo '';
echo '
';
echo '';
echo '';
echo '' . esc_html($title) . '';
if (!empty($this->settings['noindex'])) {
echo '';
}
wp_head();
echo '';
echo '';
echo $content;
wp_footer();
echo '';
}
private function enqueue_frontend_assets(): void
{
wp_enqueue_style(
'king-addons-maintenance-frontend',
KING_ADDONS_URL . 'includes/extensions/Maintenance_Mode/assets/frontend.css',
[],
KING_ADDONS_VERSION
);
wp_enqueue_script(
'king-addons-maintenance-frontend',
KING_ADDONS_URL . 'includes/extensions/Maintenance_Mode/assets/frontend.js',
[],
KING_ADDONS_VERSION,
true
);
}
private function get_rendered_page_content(): string
{
$theme = $this->settings['theme'] ?? 'dark';
$mode = $this->settings['mode'] ?? 'coming_soon';
$template_source = $this->settings['template_source'] ?? 'built_in';
$wrapper_classes = [
'kng-maintenance',
$theme === 'light' ? 'kng-maintenance-theme-light' : 'kng-maintenance-theme-dark',
$mode === 'maintenance' ? 'kng-maintenance-mode-maintenance' : 'kng-maintenance-mode-coming-soon',
];
$content = '';
if ($template_source === 'page' && !empty($this->settings['page_id'])) {
$content = $this->get_page_content((int) $this->settings['page_id']);
} elseif ($template_source === 'elementor' && !empty($this->settings['elementor_id'])) {
$content = $this->get_elementor_content((int) $this->settings['elementor_id']);
} else {
$content = $this->render_builtin_template((string) $this->settings['template_id']);
}
$private_panel = $this->render_private_access_panel();
return '' . $content . $private_panel . '';
}
private function render_private_access_panel(): string
{
if (!$this->is_private_access_enabled()) {
return '';
}
if (empty($this->settings['private_password_hash'])) {
return '';
}
$error = $this->private_access_error;
ob_start();
?>
>
get_settings();
try {
$settings['private_token'] = bin2hex(random_bytes(16));
} catch (\Exception $e) {
$settings['private_token'] = wp_generate_password(32, false, false);
}
update_option(self::OPTION_NAME, $settings, false);
$this->redirect_with_message('mode', 'token_generated');
}
public function handle_revoke_private_token(): void
{
if (!current_user_can('manage_options')) {
wp_die(esc_html__('Unauthorized request.', 'king-addons'));
}
check_admin_referer('kng_maintenance_private_token');
$settings = $this->get_settings();
$settings['private_token'] = '';
update_option(self::OPTION_NAME, $settings, false);
$this->redirect_with_message('mode', 'token_revoked');
}
public function handle_revoke_private_password(): void
{
if (!current_user_can('manage_options')) {
wp_die(esc_html__('Unauthorized request.', 'king-addons'));
}
check_admin_referer('kng_maintenance_private_password');
$settings = $this->get_settings();
// update_option() will call the registered sanitize callback.
// Ensure we provide the same intent flag used by sanitize_settings().
$settings['private_password_remove'] = 1;
$settings['private_password'] = '';
update_option(self::OPTION_NAME, $settings, false);
$this->redirect_with_message('mode', 'password_revoked');
}
public function render_shortcode(array $atts): string
{
$atts = shortcode_atts([
'id' => '',
'type' => '',
'theme' => '',
'full_height' => 'false',
], $atts, 'kng_maintenance_page');
$theme = $atts['theme'] !== '' ? sanitize_key($atts['theme']) : ($this->settings['theme'] ?? 'dark');
$theme = $theme === 'light' ? 'light' : 'dark';
$template_source = $this->settings['template_source'] ?? 'built_in';
$template_id = $this->settings['template_id'] ?? 'minimal';
$page_id = (int) ($this->settings['page_id'] ?? 0);
$elementor_id = (int) ($this->settings['elementor_id'] ?? 0);
if ($atts['id'] !== '') {
if (is_numeric($atts['id'])) {
$template_source = 'page';
$page_id = absint($atts['id']);
} else {
$template_source = 'built_in';
$template_id = sanitize_key($atts['id']);
}
}
if ($atts['type'] !== '') {
$template_source = sanitize_key($atts['type']);
if ($template_source === 'elementor' && is_numeric($atts['id'])) {
$elementor_id = absint($atts['id']);
}
}
$content = '';
if ($template_source === 'page' && $page_id) {
$content = $this->get_page_content($page_id);
} elseif ($template_source === 'elementor' && $elementor_id) {
$content = $this->get_elementor_content($elementor_id);
} else {
$content = $this->render_builtin_template($this->ensure_template_allowed($template_id));
}
$this->enqueue_frontend_assets();
$classes = [
'kng-maintenance',
$theme === 'light' ? 'kng-maintenance-theme-light' : 'kng-maintenance-theme-dark',
];
if (filter_var($atts['full_height'], FILTER_VALIDATE_BOOLEAN)) {
$classes[] = 'kng-maintenance-full';
}
return '' . $content . '
';
}
/**
* Whether the current user may see this post's body, including status and password.
*/
private function user_can_view_post_content(\WP_Post $post): bool
{
if (!current_user_can('read_post', $post->ID)) {
return false;
}
return !post_password_required($post);
}
private function get_page_content(int $page_id): string
{
$page = get_post($page_id);
if (!$page) {
return $this->render_builtin_template('minimal');
}
if (!$this->user_can_view_post_content($page)) {
if (post_password_required($page) && current_user_can('read_post', $page_id)) {
return get_the_password_form($page);
}
return $this->render_builtin_template('minimal');
}
$old_post = $GLOBALS['post'] ?? null;
$GLOBALS['post'] = $page;
setup_postdata($page);
$content = apply_filters('the_content', $page->post_content);
wp_reset_postdata();
$GLOBALS['post'] = $old_post;
return $content;
}
private function get_elementor_content(int $template_id): string
{
$template = get_post($template_id);
if (!$template || !$this->user_can_view_post_content($template)) {
if ($template && post_password_required($template) && current_user_can('read_post', $template_id)) {
return get_the_password_form($template);
}
return $this->render_builtin_template('minimal');
}
if (!class_exists('\\Elementor\\Plugin')) {
return $this->render_builtin_template('minimal');
}
$plugin = \Elementor\Plugin::instance();
if (!$plugin->frontend) {
return $this->render_builtin_template('minimal');
}
$plugin->frontend->enqueue_styles();
$plugin->frontend->enqueue_scripts();
return $plugin->frontend->get_builder_content_for_display($template_id, true);
}
private function render_builtin_template(string $template_id): string
{
$templates = $this->get_builtin_templates();
$template_id = isset($templates[$template_id]) ? $template_id : 'minimal';
$template_id = $this->ensure_template_allowed($template_id);
$mode = $this->settings['mode'] ?? 'coming_soon';
$site_name = get_bloginfo('name');
$tagline = get_bloginfo('description');
$schedule_end = $this->settings['schedule_end'] ?? '';
$content = $this->get_template_content($template_id);
$headline = $mode === 'maintenance'
? __('We are upgrading the experience.', 'king-addons')
: __('A new experience is on the horizon.', 'king-addons');
$subhead = $tagline !== '' ? $tagline : __('Our team is preparing something beautiful. Stay tuned.', 'king-addons');
$mode_label = $mode === 'maintenance' ? __('Maintenance Mode', 'king-addons') : __('Coming Soon', 'king-addons');
$badge = $content['badge'] !== '' ? $content['badge'] : $mode_label;
$headline = $content['headline'] !== '' ? $content['headline'] : $headline;
$subhead = $content['subhead'] !== '' ? $content['subhead'] : $subhead;
$launch_label = '';
if ($content['launch_label'] !== '') {
$launch_label = $content['launch_label'];
}
if ($schedule_end !== '') {
$local_end = get_date_from_gmt($schedule_end, 'Y-m-d H:i');
if ($launch_label === '') {
$launch_label = sprintf(__('Estimated return: %s', 'king-addons'), esc_html($local_end));
}
}
$footer_left = $content['footer_left'] !== '' ? $content['footer_left'] : $site_name;
$footer_right = $content['footer_right'] !== '' ? $content['footer_right'] : __('Powered by King Addons', 'king-addons');
$countdown_days = str_pad((string) (int) ($content['countdown_days'] ?? 0), 2, '0', STR_PAD_LEFT);
$countdown_hours = str_pad((string) (int) ($content['countdown_hours'] ?? 0), 2, '0', STR_PAD_LEFT);
$countdown_minutes = str_pad((string) (int) ($content['countdown_minutes'] ?? 0), 2, '0', STR_PAD_LEFT);
$progress_percent = isset($content['progress_percent']) ? min(100, max(0, (int) $content['progress_percent'])) : 0;
$progress_label = $content['progress_label'] ?? '';
if ($progress_label === '') {
$progress_label = sprintf(__('%d%% complete', 'king-addons'), $progress_percent);
}
$form_placeholder = $content['form_placeholder'] ?? '';
if ($form_placeholder === '') {
$form_placeholder = __('Email address', 'king-addons');
}
$form_button = $content['form_button'] ?? '';
if ($form_button === '') {
$form_button = __('Notify me', 'king-addons');
}
ob_start();
?>
__('Minimal', 'king-addons'),
'dark' => __('Dark', 'king-addons'),
'gradient' => __('Gradient', 'king-addons'),
'aurora' => __('Aurora Glow', 'king-addons'),
'neon' => __('Neon Tech', 'king-addons'),
'paper' => __('Paper Light', 'king-addons'),
'grid' => __('Tech Grid', 'king-addons'),
'mono' => __('Mono Minimal', 'king-addons'),
'spotlight' => __('Spotlight', 'king-addons'),
'poster' => __('Poster', 'king-addons'),
'ribbon' => __('Ribbon', 'king-addons'),
'countdown' => __('Coming Soon Countdown', 'king-addons'),
'progress' => __('Maintenance Progress', 'king-addons'),
'subscribe' => __('Simple Subscribe', 'king-addons'),
'product-launch' => __('Product Launch', 'king-addons'),
'construction' => __('Under Construction', 'king-addons'),
'split' => __('Split Layout', 'king-addons'),
'logo' => __('Centered Logo', 'king-addons'),
];
}
public function get_pro_templates(): array
{
return [
'countdown',
'progress',
'product-launch',
'split',
];
}
public function handle_export(): void
{
if (!current_user_can('manage_options')) {
wp_die(esc_html__('Unauthorized request.', 'king-addons'));
}
check_admin_referer('kng_maintenance_export');
$settings = $this->get_settings();
header('Content-Type: application/json; charset=utf-8');
header('Content-Disposition: attachment; filename=maintenance-mode-settings.json');
echo wp_json_encode($settings);
exit;
}
public function handle_import(): void
{
if (!current_user_can('manage_options')) {
wp_die(esc_html__('Unauthorized request.', 'king-addons'));
}
check_admin_referer('kng_maintenance_import');
if (empty($_FILES['import_file']) || !isset($_FILES['import_file']['tmp_name'])) {
$this->redirect_with_message('import-export', 'error_import');
}
$file = $_FILES['import_file'];
if (!empty($file['error'])) {
$this->redirect_with_message('import-export', 'error_import');
}
$contents = file_get_contents($file['tmp_name']);
if ($contents === false) {
$this->redirect_with_message('import-export', 'error_import');
}
$decoded = json_decode($contents, true);
if (!is_array($decoded)) {
$this->redirect_with_message('import-export', 'error_import');
}
$sanitized = $this->sanitize_settings($decoded);
update_option(self::OPTION_NAME, $sanitized);
$this->redirect_with_message('import-export', 'imported');
}
private function get_default_settings(): array
{
return [
'enabled' => false,
'mode' => 'coming_soon',
'template_source' => 'built_in',
'template_id' => 'minimal',
'template_content' => [],
'page_id' => 0,
'elementor_id' => 0,
'theme' => 'dark',
'noindex' => true,
'retry_after' => 3600,
'whitelist_ips' => [],
'whitelist_paths' => [],
'exclude_admin' => true,
'allowed_roles' => [],
'schedule_enabled' => false,
'schedule_start' => '',
'schedule_end' => '',
'schedule_windows' => [],
'recurring_enabled' => false,
'recurring_rules' => [],
'allow_rest' => true,
'allow_admin_ajax' => true,
'disable_elementor_editor' => true,
'private_password_hash' => '',
'private_token' => '',
'custom_css' => '',
'custom_js' => '',
];
}
public function get_settings(): array
{
$defaults = $this->get_default_settings();
$saved = get_option(self::OPTION_NAME, []);
$settings = wp_parse_args($saved, $defaults);
if (!is_array($settings['template_content'])) {
$settings['template_content'] = $defaults['template_content'];
}
if (!is_array($settings['whitelist_ips'])) {
$settings['whitelist_ips'] = $defaults['whitelist_ips'];
}
if (!is_array($settings['whitelist_paths'])) {
$settings['whitelist_paths'] = $defaults['whitelist_paths'];
}
if (!is_array($settings['allowed_roles'])) {
$settings['allowed_roles'] = $defaults['allowed_roles'];
}
if (!is_array($settings['schedule_windows'])) {
$settings['schedule_windows'] = $defaults['schedule_windows'];
}
if (!is_array($settings['recurring_rules'])) {
$settings['recurring_rules'] = $defaults['recurring_rules'];
}
return $settings;
}
public function sanitize_settings(array $settings): array
{
$defaults = $this->get_default_settings();
$existing = $this->get_settings();
$settings = wp_parse_args($settings, $existing);
$templates = array_keys($this->get_builtin_templates());
$pro_templates = $this->get_pro_templates();
$source = isset($settings['template_source']) ? sanitize_key($settings['template_source']) : $defaults['template_source'];
if (!in_array($source, ['built_in', 'page', 'elementor'], true)) {
$source = 'built_in';
}
$template_id = isset($settings['template_id']) ? sanitize_key($settings['template_id']) : $defaults['template_id'];
if (!in_array($template_id, $templates, true)) {
$template_id = $defaults['template_id'];
}
if (!$this->is_pro() && in_array($template_id, $pro_templates, true)) {
$template_id = 'minimal';
}
$mode = isset($settings['mode']) ? sanitize_key($settings['mode']) : $defaults['mode'];
if (!in_array($mode, ['coming_soon', 'maintenance'], true)) {
$mode = $defaults['mode'];
}
$theme = isset($settings['theme']) ? sanitize_key($settings['theme']) : $defaults['theme'];
$theme = $theme === 'light' ? 'light' : 'dark';
$whitelist_ips = $this->sanitize_list($settings['whitelist_ips'] ?? []);
$whitelist_paths = $this->sanitize_list($settings['whitelist_paths'] ?? []);
if (!$this->is_pro()) {
$whitelist_ips = array_slice($whitelist_ips, 0, 10);
$whitelist_paths = array_slice($whitelist_paths, 0, 10);
}
$legacyScheduleStart = $this->sanitize_datetime($settings['schedule_start'] ?? '');
$legacyScheduleEnd = $this->sanitize_datetime($settings['schedule_end'] ?? '');
$scheduleWindows = $this->sanitize_schedule_windows($settings['schedule_windows'] ?? []);
if ($scheduleWindows === [] && ($legacyScheduleStart !== '' || $legacyScheduleEnd !== '')) {
$scheduleWindows[] = [
'start' => $legacyScheduleStart,
'end' => $legacyScheduleEnd,
'timezone' => self::DEFAULT_TIMEZONE,
];
}
if ($scheduleWindows !== []) {
$legacyScheduleStart = (string) ($scheduleWindows[0]['start'] ?? $legacyScheduleStart);
$legacyScheduleEnd = (string) ($scheduleWindows[0]['end'] ?? $legacyScheduleEnd);
}
$recurringRules = $this->sanitize_recurring_rules($settings['recurring_rules'] ?? []);
$clean = [
'enabled' => !empty($settings['enabled']),
'mode' => $mode,
'template_source' => $source,
'template_id' => $template_id,
'template_content' => $this->sanitize_template_content($settings['template_content'] ?? []),
'page_id' => absint($settings['page_id'] ?? 0),
'elementor_id' => absint($settings['elementor_id'] ?? 0),
'theme' => $theme,
'noindex' => !empty($settings['noindex']),
'retry_after' => max(0, absint($settings['retry_after'] ?? $defaults['retry_after'])),
'whitelist_ips' => $whitelist_ips,
'whitelist_paths' => $this->normalize_paths($whitelist_paths),
'exclude_admin' => !empty($settings['exclude_admin']),
'allowed_roles' => $this->sanitize_list($settings['allowed_roles'] ?? []),
'schedule_enabled' => !empty($settings['schedule_enabled']),
'schedule_start' => $legacyScheduleStart,
'schedule_end' => $legacyScheduleEnd,
'schedule_windows' => $scheduleWindows,
'recurring_enabled' => !empty($settings['recurring_enabled']),
'recurring_rules' => $recurringRules,
'allow_rest' => !empty($settings['allow_rest']),
'allow_admin_ajax' => !empty($settings['allow_admin_ajax']),
'disable_elementor_editor' => !empty($settings['disable_elementor_editor']),
'private_password_hash' => (string) ($existing['private_password_hash'] ?? ''),
'private_token' => (string) ($existing['private_token'] ?? ''),
'custom_css' => '',
'custom_js' => '',
];
if ($this->is_pro()) {
$remove_password = !empty($settings['private_password_remove']);
$plain = isset($settings['private_password']) ? (string) $settings['private_password'] : '';
$plain = trim($plain);
if ($remove_password) {
$clean['private_password_hash'] = '';
} elseif ($plain !== '') {
$clean['private_password_hash'] = function_exists('wp_hash_password') ? wp_hash_password($plain) : $clean['private_password_hash'];
}
$token = isset($settings['private_token']) ? sanitize_text_field((string) $settings['private_token']) : (string) ($existing['private_token'] ?? '');
$clean['private_token'] = $token;
}
if (!$this->is_pro()) {
$clean['allowed_roles'] = [];
}
return $clean;
}
private function sanitize_schedule_windows($windows): array
{
if (!is_array($windows)) {
return [];
}
$clean = [];
foreach ($windows as $window) {
if (!is_array($window)) {
continue;
}
$tz = isset($window['timezone']) ? sanitize_text_field((string) $window['timezone']) : self::DEFAULT_TIMEZONE;
if ($tz === '') {
$tz = self::DEFAULT_TIMEZONE;
}
$start = $this->sanitize_datetime_with_timezone((string) ($window['start'] ?? ''), $tz);
$end = $this->sanitize_datetime_with_timezone((string) ($window['end'] ?? ''), $tz);
if ($start === '' && $end === '') {
continue;
}
$clean[] = [
'start' => $start,
'end' => $end,
'timezone' => $tz,
];
}
return $clean;
}
private function sanitize_recurring_rules($rules): array
{
if (!is_array($rules)) {
return [];
}
$clean = [];
foreach ($rules as $rule) {
if (!is_array($rule)) {
continue;
}
$freq = isset($rule['frequency']) ? sanitize_key((string) $rule['frequency']) : '';
if (!in_array($freq, ['daily', 'weekly', 'monthly'], true)) {
continue;
}
$tz = isset($rule['timezone']) ? sanitize_text_field((string) $rule['timezone']) : self::DEFAULT_TIMEZONE;
if ($tz === '') {
$tz = self::DEFAULT_TIMEZONE;
}
$startTime = sanitize_text_field((string) ($rule['start_time'] ?? ''));
$endTime = sanitize_text_field((string) ($rule['end_time'] ?? ''));
if ($this->parse_time_minutes($startTime) < 0 || $this->parse_time_minutes($endTime) < 0) {
continue;
}
$daysOfWeek = [];
if ($freq === 'weekly') {
$raw = $rule['days_of_week'] ?? [];
if (!is_array($raw)) {
$raw = preg_split('/\s*,\s*/', (string) $raw);
}
$daysOfWeek = array_values(array_unique(array_filter(array_map(static function ($v) {
$n = absint($v);
return ($n >= 1 && $n <= 7) ? $n : 0;
}, (array) $raw))));
}
$daysOfMonth = [];
if ($freq === 'monthly') {
$raw = $rule['days_of_month'] ?? [];
if (!is_array($raw)) {
$raw = preg_split('/\s*,\s*/', (string) $raw);
}
$daysOfMonth = array_values(array_unique(array_filter(array_map(static function ($v) {
$n = absint($v);
return ($n >= 1 && $n <= 31) ? $n : 0;
}, (array) $raw))));
}
$item = [
'frequency' => $freq,
'timezone' => $tz,
'start_time' => $startTime,
'end_time' => $endTime,
'days_of_week' => $daysOfWeek,
'days_of_month' => $daysOfMonth,
];
$clean[] = $item;
}
return $clean;
}
public function get_template_content(string $template_id): array
{
$templates = $this->get_builtin_templates();
$template_id = isset($templates[$template_id]) ? $template_id : 'minimal';
$template_id = $this->ensure_template_allowed($template_id);
$defaults = $this->get_template_content_defaults($template_id);
$saved = $this->settings['template_content'][$template_id] ?? [];
if (!is_array($saved)) {
$saved = [];
}
return wp_parse_args($saved, $defaults);
}
private function get_template_content_defaults(string $template_id): array
{
$mode = $this->settings['mode'] ?? 'coming_soon';
$site_name = get_bloginfo('name');
$tagline = get_bloginfo('description');
$headline = $mode === 'maintenance'
? __('We are upgrading the experience.', 'king-addons')
: __('A new experience is on the horizon.', 'king-addons');
$subhead = $tagline !== '' ? $tagline : __('Our team is preparing something beautiful. Stay tuned.', 'king-addons');
$base = [
'badge' => '',
'headline' => $headline,
'subhead' => $subhead,
'launch_label' => '',
'footer_left' => $site_name,
'footer_right' => __('Powered by King Addons', 'king-addons'),
];
if ($template_id === 'countdown') {
$base['countdown_days'] = '14';
$base['countdown_hours'] = '06';
$base['countdown_minutes'] = '42';
}
if ($template_id === 'progress') {
$base['progress_percent'] = 68;
$base['progress_label'] = __('68% complete', 'king-addons');
}
if (in_array($template_id, ['subscribe', 'product-launch'], true)) {
$base['form_placeholder'] = __('Email address', 'king-addons');
$base['form_button'] = __('Notify me', 'king-addons');
}
if ($template_id === 'split') {
$base['split_title_a'] = __('What is happening', 'king-addons');
$base['split_text_a'] = __('We are refining the experience with faster performance and new visuals.', 'king-addons');
$base['split_title_b'] = __('Stay connected', 'king-addons');
$base['split_text_b'] = __('Follow our updates while we prepare the launch.', 'king-addons');
}
return $base;
}
private function sanitize_template_content(array $content): array
{
$fields = $this->get_template_content_fields();
$clean = [];
foreach ($fields as $template_id => $template_fields) {
$values = isset($content[$template_id]) && is_array($content[$template_id]) ? $content[$template_id] : [];
$template_clean = [];
foreach ($template_fields as $field => $type) {
$value = $values[$field] ?? '';
if ($type === 'int') {
$int = absint($value);
if ($field === 'progress_percent') {
$int = min(100, max(0, $int));
}
if ($field === 'countdown_hours') {
$int = min(23, max(0, $int));
}
if ($field === 'countdown_minutes') {
$int = min(59, max(0, $int));
}
$template_clean[$field] = $int;
} else {
$template_clean[$field] = sanitize_text_field((string) $value);
}
}
$clean[$template_id] = $template_clean;
}
return $clean;
}
private function get_template_content_fields(): array
{
$base = [
'badge' => 'text',
'headline' => 'text',
'subhead' => 'text',
'launch_label' => 'text',
'footer_left' => 'text',
'footer_right' => 'text',
];
return [
'minimal' => $base,
'dark' => $base,
'gradient' => $base,
'aurora' => $base,
'neon' => $base,
'paper' => $base,
'grid' => $base,
'mono' => $base,
'spotlight' => $base,
'poster' => $base,
'ribbon' => $base,
'construction' => $base,
'logo' => $base,
'countdown' => $base + [
'countdown_days' => 'int',
'countdown_hours' => 'int',
'countdown_minutes' => 'int',
],
'progress' => $base + [
'progress_percent' => 'int',
'progress_label' => 'text',
],
'subscribe' => $base + [
'form_placeholder' => 'text',
'form_button' => 'text',
],
'product-launch' => $base + [
'form_placeholder' => 'text',
'form_button' => 'text',
],
'split' => $base + [
'split_title_a' => 'text',
'split_text_a' => 'text',
'split_title_b' => 'text',
'split_text_b' => 'text',
],
];
}
private function ensure_template_allowed(string $template_id): string
{
if (!$this->is_pro() && in_array($template_id, $this->get_pro_templates(), true)) {
return 'minimal';
}
return $template_id;
}
private function sanitize_list($value): array
{
if (is_array($value)) {
$items = $value;
} else {
$items = preg_split('/\\r\\n|\\r|\\n|,/', (string) $value);
}
$items = array_map('trim', $items);
$items = array_filter($items, static function ($item) {
return $item !== '';
});
$items = array_map('sanitize_text_field', $items);
return array_values(array_unique($items));
}
private function normalize_paths(array $paths): array
{
$normalized = [];
foreach ($paths as $path) {
$path = wp_parse_url($path, PHP_URL_PATH) ?: $path;
$path = '/' . ltrim((string) $path, '/');
$normalized[] = $path === '' ? '/' : $path;
}
return array_values(array_unique($normalized));
}
private function sanitize_datetime(string $value): string
{
$value = sanitize_text_field($value);
if ($value === '') {
return '';
}
$value = str_replace('T', ' ', $value);
if (strlen($value) === 16) {
$value .= ':00';
}
$timestamp = strtotime($value);
if (!$timestamp) {
return '';
}
return get_gmt_from_date($value, 'Y-m-d H:i:s');
}
private function sanitize_datetime_with_timezone(string $value, string $timezone): string
{
$value = sanitize_text_field($value);
if ($value === '') {
return '';
}
$value = str_replace('T', ' ', $value);
if (strlen($value) === 16) {
$value .= ':00';
}
if (!preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $value)) {
return '';
}
$tz = $this->resolve_timezone($timezone);
$dt = \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $value, $tz);
if (!$dt) {
return '';
}
return $dt->setTimezone(new \DateTimeZone('UTC'))->format('Y-m-d H:i:s');
}
private function parse_schedule_time(string $value): int
{
if ($value === '') {
return 0;
}
return (int) strtotime($value . ' UTC');
}
private function get_client_ip(): string
{
if (!empty($_SERVER['REMOTE_ADDR']) && filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP)) {
return (string) $_SERVER['REMOTE_ADDR'];
}
return '';
}
private function is_preview_request(): bool
{
if (empty($_GET['kng_maintenance_preview'])) {
return false;
}
if (!current_user_can('manage_options')) {
return false;
}
$nonce = isset($_GET['_kng_preview_nonce']) ? sanitize_text_field(wp_unslash($_GET['_kng_preview_nonce'])) : '';
if ($nonce === '' || !wp_verify_nonce($nonce, 'kng_maintenance_preview')) {
return false;
}
return true;
}
private function is_pro(): bool
{
return function_exists('king_addons_freemius') && king_addons_freemius()->can_use_premium_code__premium_only();
}
private function redirect_with_message(string $view, string $message): void
{
$args = [
'page' => 'king-addons-maintenance-mode',
'view' => $view,
'message' => $message,
];
wp_safe_redirect(add_query_arg($args, admin_url('admin.php')));
exit;
}
}