['label' => 'Mobile', 'value' => 767, 'direction' => 'max', 'enabled' => true, 'icon' => 'smartphone'], 'mobile_large' => ['label' => 'Mobile Large', 'value' => 880, 'direction' => 'max', 'enabled' => false, 'icon' => 'smartphone-large'], 'tablet' => ['label' => 'Tablet', 'value' => 1024, 'direction' => 'max', 'enabled' => true, 'icon' => 'tablet'], 'tablet_large' => ['label' => 'Tablet Large', 'value' => 1200, 'direction' => 'max', 'enabled' => false, 'icon' => 'tablet-large'], 'laptop' => ['label' => 'Laptop', 'value' => 1366, 'direction' => 'max', 'enabled' => false, 'icon' => 'laptop'], 'desktop' => ['label' => 'Desktop', 'value' => 0, 'direction' => 'base', 'enabled' => true, 'icon' => 'monitor'], 'widescreen' => ['label' => 'Widescreen', 'value' => 2400, 'direction' => 'min', 'enabled' => false, 'icon' => 'tv'], ]; } public static function defaults(): array { return [ 'post_types' => ['post', 'page'], 'breakpoints' => self::default_breakpoints(), 'features' => [ 'dark_mode_default' => false, 'autosave_interval' => 60, ], // Google reCAPTCHA keys for the Form widget (v2 checkbox or v3). The secret never leaves the server. 'recaptcha_site_key' => '', 'recaptcha_secret' => '', // Stripe secret key for the Stripe button's Checkout mode (T193). Server-side only, never printed. 'stripe_secret_key' => '', ]; } /** reCAPTCHA keys (Form widget). @return array{site_key: string, secret: string} */ public function recaptcha(): array { return [ 'site_key' => trim((string) $this->get('recaptcha_site_key', '')), 'secret' => trim((string) $this->get('recaptcha_secret', '')), ]; } public static function default_global(): array { return [ 'colors' => [ ['id' => 'primary', 'label' => 'Primary', 'value' => '#2563eb'], ['id' => 'secondary', 'label' => 'Secondary', 'value' => '#0f172a'], ['id' => 'text', 'label' => 'Text', 'value' => '#334155'], ['id' => 'accent', 'label' => 'Accent', 'value' => '#f59e0b'], ], 'fonts' => [ ['id' => 'primary', 'label' => 'Primary', 'value' => ['family' => 'Inter', 'weight' => '600']], ['id' => 'secondary', 'label' => 'Secondary', 'value' => ['family' => 'Inter', 'weight' => '400']], ['id' => 'text', 'label' => 'Text', 'value' => ['family' => 'Inter', 'weight' => '400']], ], 'spacing' => [ ['id' => 'xs', 'label' => 'XS', 'value' => ['size' => 4, 'unit' => 'px']], ['id' => 'sm', 'label' => 'SM', 'value' => ['size' => 8, 'unit' => 'px']], ['id' => 'md', 'label' => 'MD', 'value' => ['size' => 16, 'unit' => 'px']], ['id' => 'lg', 'label' => 'LG', 'value' => ['size' => 32, 'unit' => 'px']], ['id' => 'xl', 'label' => 'XL', 'value' => ['size' => 64, 'unit' => 'px']], ], 'classes' => [], 'themeStyle' => [], 'layout' => ['contentWidth' => ['size' => 1140, 'unit' => 'px'], 'gap' => ['size' => 20, 'unit' => 'px']], ]; } public static function ensure_defaults(): void { if (get_option(self::OPTION) === false) { add_option(self::OPTION, self::defaults()); } if (get_option(self::OPTION_GLOBAL) === false) { add_option(self::OPTION_GLOBAL, self::default_global()); } $saved_css = get_option(self::OPTION_GLOBAL_CSS); if ($saved_css === false || $saved_css === '') { $default_vars = self::build_default_vars(self::default_global()); if ($saved_css === false) { add_option(self::OPTION_GLOBAL_CSS, $default_vars); } else { update_option(self::OPTION_GLOBAL_CSS, $default_vars); } } } public function get(string $key, $default = null) { $all = wp_parse_args((array) get_option(self::OPTION, []), self::defaults()); return $all[$key] ?? $default; } public function update(array $values): void { $all = wp_parse_args((array) get_option(self::OPTION, []), self::defaults()); update_option(self::OPTION, array_merge($all, $values)); } public function breakpoints(): array { $saved = (array) $this->get('breakpoints', []); $out = []; foreach (self::default_breakpoints() as $key => $bp) { $out[$key] = array_merge($bp, isset($saved[$key]) && is_array($saved[$key]) ? $saved[$key] : []); $out[$key]['value'] = (int) $out[$key]['value']; $out[$key]['enabled'] = $key === 'desktop' ? true : (bool) $out[$key]['enabled']; } return $out; } public function post_types(): array { return array_values(array_filter((array) $this->get('post_types', ['post', 'page']), 'post_type_exists')); } public function global(): array { $global = wp_parse_args((array) get_option(self::OPTION_GLOBAL, []), self::default_global()); unset($global['customCss']); // Removed feature (arbitrary site-wide CSS): old values are ignored. return $global; } public function update_global(array $settings, ?string $css = null): void { // Site features (T102/T103) get strict shapes instead of the generic sanitizer's. if (array_key_exists('site', $settings)) { $settings['site'] = \Zadex\Site\MaintenanceMode::sanitize($settings['site']); } if (array_key_exists('floatingButtons', $settings)) { $settings['floatingButtons'] = \Zadex\Site\FloatingButtons::sanitize($settings['floatingButtons']); } if (array_key_exists('pageTransitions', $settings)) { $settings['pageTransitions'] = \Zadex\Site\PageTransitions::sanitize($settings['pageTransitions']); } unset($settings['customCss']); // Arbitrary site-wide CSS is not supported. update_option(self::OPTION_GLOBAL, $settings); if ($css !== null) { update_option(self::OPTION_GLOBAL_CSS, $css); } } public function global_css(): string { $saved = (string) get_option(self::OPTION_GLOBAL_CSS, ''); if ($saved !== '') { return self::strip_legacy_custom_css($saved, get_option(self::OPTION_GLOBAL, [])); } return self::build_default_vars($this->global()); } /** * Older versions let admins type site-wide custom CSS (`zadex_global.customCss`) that the editor appended to the * end of the generated global CSS. That feature is removed: until the global settings are saved again (which * regenerates the CSS without it), the stored tail is cut off so old custom CSS is never printed. * * @param mixed $global The raw `zadex_global` option. */ public static function strip_legacy_custom_css(string $css, $global): string { $custom = is_array($global) && isset($global['customCss']) && is_string($global['customCss']) ? trim($global['customCss']) : ''; if ($custom === '') { return $css; } $trimmed = rtrim($css); foreach ([$custom, trim(\Zadex\Documents\Document::sanitize_css($custom))] as $tail) { if ($tail !== '' && str_ends_with($trimmed, $tail)) { return rtrim(substr($trimmed, 0, -strlen($tail))); } } return $css; } /** * Builds default CSS custom properties from global settings so that variables * like --zx-color-* and --zx-font-* exist even before the user visits Site Settings in the editor. */ public static function build_default_vars(array $global): string { $vars = []; foreach ($global['colors'] ?? [] as $c) { if (!empty($c['id']) && isset($c['value']) && is_string($c['value']) && $c['value'] !== '') { $vars[] = '--zx-color-' . preg_replace('/[^a-zA-Z0-9_\-]/', '', (string) $c['id']) . ':' . sanitize_text_field($c['value']); } } foreach ($global['fonts'] ?? [] as $f) { if (!empty($f['id']) && !empty($f['value']['family']) && is_string($f['value']['family'])) { $family = trim($f['value']['family']); if (!str_contains($family, ',') && !str_starts_with($family, '"') && !str_starts_with($family, "'") && preg_match('/\s/', $family)) { $family = '"' . $family . '"'; } $vars[] = '--zx-font-' . preg_replace('/[^a-zA-Z0-9_\-]/', '', (string) $f['id']) . ':' . $family; } if (!empty($f['id']) && !empty($f['value']['weight'])) { $vars[] = '--zx-font-' . preg_replace('/[^a-zA-Z0-9_\-]/', '', (string) $f['id']) . '-weight:' . sanitize_text_field((string) $f['value']['weight']); } } foreach ($global['spacing'] ?? [] as $s) { if (!empty($s['id']) && isset($s['value'])) { $css = self::slider_to_css($s['value']); if ($css !== null) { $vars[] = '--zx-space-' . preg_replace('/[^a-zA-Z0-9_\-]/', '', (string) $s['id']) . ':' . $css; } } } if (isset($global['layout']['contentWidth'])) { $cw = self::slider_to_css($global['layout']['contentWidth']); if ($cw !== null) { $vars[] = '--zx-content-width:' . $cw; } } if (isset($global['layout']['gap'])) { $gap = self::slider_to_css($global['layout']['gap']); if ($gap !== null) { $vars[] = '--zx-gap:' . $gap; } } if (isset($global['layout']['containerPadding'])) { $cp = self::dimensions_to_css($global['layout']['containerPadding']); if ($cp !== null) { $vars[] = '--zx-container-padding:' . $cp; } } $lb = $global['layout']['lightbox'] ?? null; if (is_array($lb)) { if (!empty($lb['bg'])) $vars[] = '--zx-lightbox-bg:' . sanitize_text_field($lb['bg']); if (!empty($lb['color'])) $vars[] = '--zx-lightbox-color:' . sanitize_text_field($lb['color']); if (isset($lb['counter']) && $lb['counter'] === false) $vars[] = '--zx-lightbox-counter:none'; if (isset($lb['captions']) && $lb['captions'] === false) $vars[] = '--zx-lightbox-caption:none'; if (isset($lb['zoom']) && $lb['zoom'] === false) $vars[] = '--zx-lightbox-zoom:default'; } if (empty($vars)) { return ''; } return ':root{' . implode(';', $vars) . '}'; } private static function slider_to_css($value): ?string { if (is_numeric($value)) { return $value . 'px'; } if (is_string($value)) { $trimmed = trim($value); return $trimmed === '' ? null : $trimmed; } if (is_array($value) && isset($value['size']) && $value['size'] !== '' && $value['size'] !== null) { $unit = $value['unit'] ?? 'px'; if ($unit === 'custom') { return (string) $value['size']; } if (is_numeric($value['size'])) { return (float) $value['size'] . preg_replace('/[^a-zA-Z%]/', '', (string) $unit); } } return null; } private static function dimensions_to_css($value): ?string { if (!is_array($value)) { return null; } $unit = $value['unit'] ?? 'px'; $sides = ['top', 'right', 'bottom', 'left']; $has_any = false; foreach ($sides as $s) { if (isset($value[$s]) && $value[$s] !== '' && $value[$s] !== null) { $has_any = true; break; } } if (!$has_any) { return null; } $out = []; foreach ($sides as $s) { $v = $value[$s] ?? null; if ($v === null || $v === '') { $out[] = '0'; } elseif ($unit === 'custom') { $out[] = (string) $v; } elseif (is_numeric($v)) { $n = (float) $v; $out[] = ($n === 0.0) ? '0' : ($n . preg_replace('/[^a-zA-Z%]/', '', (string) $unit)); } else { $out[] = (string) $v; } } return implode(' ', $out); } }