` and plain-text selectors. */ final class Settings { public const LAYOUTS = ['center', 'slide_in', 'fullscreen', 'top_bar', 'bottom_bar']; public const POSITIONS = ['center', 'top', 'bottom']; public const SIDES = ['start', 'end']; public const ANIMATIONS = ['fade', 'zoom', 'slide_up', 'slide_down', 'slide_start', 'slide_end', 'none']; public const DEVICES = ['desktop', 'tablet', 'mobile']; /** * @param array $page page settings (bare settings or the `{settings, css}` envelope) * @param array{tablet?: int, mobile?: int} $breakpoints max-width breakpoints for the device rule * @return array */ public static function from_page(array $page, array $breakpoints = []): array { $p = isset($page['settings']) && is_array($page['settings']) ? $page['settings'] : $page; $devices = isset($p['popup_rule_devices']) && is_array($p['popup_rule_devices']) ? array_values(array_intersect(self::DEVICES, array_map('strval', array_filter($p['popup_rule_devices'], 'is_scalar')))) : self::DEVICES; return [ 'layout' => self::pick($p['popup_layout'] ?? null, self::LAYOUTS, 'center'), 'position' => self::pick($p['popup_position'] ?? null, self::POSITIONS, 'center'), 'side' => self::pick($p['popup_side'] ?? null, self::SIDES, 'end'), 'width' => self::length($p['popup_width'] ?? null, '600px'), 'height' => self::length($p['popup_height'] ?? null, ''), 'background' => self::color($p['popup_background'] ?? null, '#ffffff'), 'radius' => self::num($p['popup_radius'] ?? null, 8, 0, 200), 'padding' => self::num($p['popup_padding'] ?? null, 24, 0, 200), 'overlay' => [ 'show' => self::bool($p['popup_overlay'] ?? null, true), 'color' => self::color($p['popup_overlay_color'] ?? null, 'rgba(15, 23, 42, 0.6)'), 'blur' => self::num($p['popup_overlay_blur'] ?? null, 0, 0, 40), 'close' => self::bool($p['popup_overlay_close'] ?? null, true), ], 'close' => [ 'show' => self::bool($p['popup_close'] ?? null, true), 'position' => self::pick($p['popup_close_position'] ?? null, ['inside', 'outside'], 'inside'), 'color' => self::color($p['popup_close_color'] ?? null, '#334155'), 'delay' => self::num($p['popup_close_delay'] ?? null, 0, 0, 600), ], 'animation' => [ 'in' => self::pick($p['popup_anim_in'] ?? null, self::ANIMATIONS, 'fade'), 'out' => self::pick($p['popup_anim_out'] ?? null, self::ANIMATIONS, 'fade'), 'duration' => self::num($p['popup_anim_duration'] ?? null, 300, 0, 5000), ], 'preventScroll' => self::bool($p['popup_prevent_scroll'] ?? null, true), 'autoClose' => self::num($p['popup_auto_close'] ?? null, 0, 0, 3600), 'triggers' => [ 'load' => self::bool($p['popup_trigger_load'] ?? null, false) ? self::num($p['popup_trigger_load_delay'] ?? null, 0, 0, 3600) : null, 'scroll' => self::bool($p['popup_trigger_scroll'] ?? null, false) ? self::num($p['popup_trigger_scroll_percent'] ?? null, 50, 0, 100) : null, 'scrollTo' => self::bool($p['popup_trigger_scroll_to'] ?? null, false) ? self::selector($p['popup_trigger_scroll_to_selector'] ?? null) : null, 'click' => self::bool($p['popup_trigger_click'] ?? null, false) ? self::selector($p['popup_trigger_click_selector'] ?? null) : null, 'inactivity' => self::bool($p['popup_trigger_inactivity'] ?? null, false) ? self::num($p['popup_trigger_inactivity_seconds'] ?? null, 30, 1, 3600) : null, 'exit' => self::bool($p['popup_trigger_exit'] ?? null, false), 'adblock' => self::bool($p['popup_trigger_adblock'] ?? null, false), ], 'rules' => [ 'views' => (int) self::num($p['popup_rule_views'] ?? null, 0, 0, 1000), 'sessions' => (int) self::num($p['popup_rule_sessions'] ?? null, 0, 0, 1000), 'referrer' => [ 'mode' => self::pick($p['popup_rule_referrer'] ?? null, ['any', 'contains', 'regex'], 'any'), 'value' => self::text($p['popup_rule_referrer_value'] ?? null), ], 'login' => self::pick($p['popup_rule_login'] ?? null, ['any', 'in', 'out'], 'any'), 'devices' => $devices, 'maxTotal' => (int) self::num($p['popup_rule_max_total'] ?? null, 0, 0, 1000), 'maxSession' => (int) self::num($p['popup_rule_max_session'] ?? null, 0, 0, 1000), 'cooldown' => self::num($p['popup_rule_cooldown'] ?? null, 0, 0, 3650), 'hideAfterSubmit' => self::bool($p['popup_rule_hide_after_submit'] ?? null, false), ], 'breakpoints' => [ 'tablet' => (int) ($breakpoints['tablet'] ?? 1024), 'mobile' => (int) ($breakpoints['mobile'] ?? 767), ], ]; } /** Server-side part of the rules: logged in / out (the popup is not printed when it fails). */ public static function login_allows(array $settings, bool $logged_in): bool { $rule = $settings['rules']['login'] ?? 'any'; return $rule === 'any' || ($rule === 'in' && $logged_in) || ($rule === 'out' && !$logged_in); } /** Whether any automatic trigger is enabled. */ public static function has_triggers(array $settings): bool { foreach ((array) ($settings['triggers'] ?? []) as $value) { if ($value !== null && $value !== false) { return true; } } return false; } private static function pick($value, array $list, string $fallback): string { return is_string($value) && in_array($value, $list, true) ? $value : $fallback; } /** @return int|float */ private static function num($value, $fallback, $min, $max) { if (is_bool($value) || !is_numeric($value)) { return $fallback; } $n = $value + 0; $n = max($min, min($max, $n)); return is_float($n) && floor($n) === $n ? (int) $n : $n; } private static function bool($value, bool $fallback): bool { if (is_bool($value)) { return $value; } if ($value === 1 || $value === '1' || $value === 'true' || $value === 'yes') { return true; } if ($value === 0 || $value === '0' || $value === 'false' || $value === '' || $value === 'no') { return false; } return $fallback; } public static function color($value, string $fallback): string { if (!is_string($value)) { return $fallback; } $value = trim($value); return preg_match('/^(#[0-9a-f]{3,8}|(rgb|rgba|hsl|hsla)\([0-9.,%\s\/+\-]+\)|var\(--[a-z0-9_\-]+\)|transparent|currentColor|[a-z]+)$/i', $value) ? $value : $fallback; } /** Slider `{size, unit}`, number (px) or `` -> CSS length; '' = auto. */ public static function length($value, string $fallback): string { if ($value === null) { return $fallback; } if (is_int($value) || is_float($value)) { return $value >= 0 ? self::fmt($value) . 'px' : $fallback; } if (is_array($value)) { $size = $value['size'] ?? ''; if ($size === '' || $size === null) { return ''; } if (!is_numeric($size) || $size < 0) { return $fallback; } $unit = in_array($value['unit'] ?? 'px', ['px', '%', 'vw', 'vh', 'em', 'rem'], true) ? $value['unit'] : 'px'; return self::fmt($size + 0) . $unit; } if (!is_string($value)) { return $fallback; } $value = trim($value); if ($value === '') { return ''; } return preg_match('/^\d+(\.\d+)?(px|%|vw|vh|em|rem)$/', $value) ? $value : $fallback; } private static function fmt($n): string { return rtrim(rtrim(number_format((float) $n, 3, '.', ''), '0'), '.'); } /** A CSS selector typed by the editor: plain text, no markup, max 300 chars (null = none). */ private static function selector($value): ?string { $s = self::text($value); return $s === '' ? null : $s; } private static function text($value): string { if (!is_string($value)) { return ''; } // `>` stays (child combinator); the JSON is attribute-escaped when printed. $s = trim(wp_strip_all_tags($value)); return function_exists('mb_substr') ? mb_substr($s, 0, 300) : substr($s, 0, 300); } }