settings = $settings; $this->frontend = $frontend; $this->renderer = $renderer; $this->halt = $halt ?? static function (): void { exit; }; $this->header = $header ?? static function (string $line): void { if (!headers_sent()) { header($line); } }; } public function register(): void { add_action('template_redirect', [$this, 'maybe_intercept'], 0); add_action('admin_bar_menu', [$this, 'admin_bar'], 100); add_action('admin_notices', [$this, 'admin_notice']); add_filter('zadex/editor/config', [$this, 'editor_config']); } /** Normalized `site` settings. */ public static function sanitize($input): array { $in = is_array($input) ? $input : []; $mode = isset($in['mode']) && is_string($in['mode']) && in_array($in['mode'], self::MODES, true) ? $in['mode'] : 'off'; $roles = []; foreach ((array) ($in['roles'] ?? []) as $role) { if (is_string($role) || is_int($role)) { $role = sanitize_key((string) $role); if ($role !== '' && $role !== 'administrator' && !in_array($role, $roles, true)) { $roles[] = $role; } } } return [ 'mode' => $mode, 'page' => isset($in['page']) && is_numeric($in['page']) ? max(0, (int) $in['page']) : 0, 'roles' => $roles, ]; } public function site(): array { $global = $this->settings->global(); return self::sanitize($global['site'] ?? []); } public function mode(): string { return $this->site()['mode']; } /** Whether the current user sees the real site while the mode is on. */ public function user_allowed(): bool { if (!is_user_logged_in()) { return false; } if (current_user_can('manage_options')) { return true; } $user = wp_get_current_user(); $roles = is_object($user) && isset($user->roles) ? (array) $user->roles : []; return (bool) array_intersect($roles, $this->site()['roles']); } /** Requests that are never replaced by the maintenance page. */ public static function is_bypassed_request(): bool { if (is_admin() || (function_exists('wp_doing_ajax') && wp_doing_ajax()) || (function_exists('wp_doing_cron') && wp_doing_cron()) || (defined('REST_REQUEST') && REST_REQUEST) || (defined('XMLRPC_REQUEST') && XMLRPC_REQUEST) || (defined('WP_CLI') && WP_CLI) ) { return true; } if (EditorLoader::mode() !== '') { return true; // the editor shell and its preview have their own permission checks } $pagenow = $GLOBALS['pagenow'] ?? ''; if (in_array($pagenow, ['wp-login.php', 'wp-register.php', 'wp-activate.php', 'wp-signup.php'], true)) { return true; } if ((function_exists('is_robots') && is_robots()) || (function_exists('is_favicon') && is_favicon())) { return true; } return false; } public function should_intercept(): bool { if ($this->mode() === 'off' || self::is_bypassed_request()) { return false; } return !$this->user_allowed(); } /** Status code, headers and robots policy for a mode. */ public static function response_for(string $mode): array { if ($mode === 'maintenance') { return ['status' => 503, 'headers' => ['Retry-After' => (string) self::RETRY_AFTER], 'noindex' => true]; } return ['status' => 200, 'headers' => [], 'noindex' => false]; } /** The page shown to visitors, or 0 when it is missing / trashed (the built-in page is used then). */ public function page_id(): int { $id = $this->site()['page']; if ($id <= 0) { return 0; } $post = get_post($id); if (!$post || in_array($post->post_status, ['trash', 'auto-draft', 'inherit'], true)) { return 0; } return (int) $post->ID; } public function maybe_intercept(): void { if (!$this->should_intercept()) { return; } $mode = $this->mode(); $this->send_headers($mode); $page = $this->page_id(); if ($page > 0) { $this->render_page($page); } else { $this->print_builtin_page($mode); } ($this->halt)(); } public function send_headers(string $mode): void { $response = self::response_for($mode); status_header($response['status']); nocache_headers(); foreach ($response['headers'] as $name => $value) { ($this->header)($name . ': ' . $value); } if ($response['noindex']) { ($this->header)('X-Robots-Tag: noindex, nofollow'); } if ($response['noindex']) { add_filter('wp_robots', static fn(array $r): array => array_merge($r, ['noindex' => true, 'nofollow' => true])); } } /** * Renders the chosen page in a blank "Zadex Canvas" document. The main query stays the visitor's request: the * page becomes the current post (PostData, for get_post() / template tags / the frontend bundle) and the * Renderer's dynamic-tag context, its CSS is queued for , and the document title is the page's. */ private function render_page(int $page_id): void { $post = get_post($page_id); if (!$post instanceof \WP_Post || $this->frontend === null || $this->renderer === null) { $this->print_builtin_page($this->mode()); return; } PostData::set($post); $this->renderer->set_context(Context::for_post($page_id)); $renderer = $this->renderer; add_action('wp_enqueue_scripts', static fn() => Styles::document_tree($page_id, $renderer), 30); add_filter('document_title_parts', static fn(array $parts): array => ['title' => get_the_title($post), 'site' => (string) get_bloginfo('name')]); if (Document::is_builder($page_id) && !post_password_required($post)) { // Rendered before wp_head(): its CSS lands in , protected fragments are restored on output. $html = $this->frontend->render_post($page_id); } else { // A page not built with Zadex (or password protected): its content as WordPress prints it. $html = Kses::protect((string) apply_filters('the_content', get_the_content(null, false, $post))); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WordPress core filter. } Layout::open(true, Templates::body_classes(Templates::CANVAS)); echo '
'; Kses::begin(); echo wp_kses($html, Kses::allowed_html()); Kses::end(); echo '
'; Layout::close(); } /** Styles of the built-in page (printed through the style queue, handle `zadex-site-mode`). */ public const BUILTIN_CSS = 'html,body{height:100%;margin:0}body{display:grid;place-items:center;padding:24px;box-sizing:border-box;background:#f8fafc;color:#0f172a;font:16px/1.6 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;text-align:center}main{max-width:520px}h1{margin:0 0 12px;font-size:32px;line-height:1.2}p{margin:0;color:#475569}.zx-site-mode__name{margin-bottom:24px;font-size:14px;font-weight:600;color:#64748b}'; /** Prints the small self-contained page used when no Zadex page is chosen. */ public function print_builtin_page(string $mode): void { $name = (string) get_bloginfo('name'); if ($mode === 'maintenance') { $title = __('Under maintenance', 'zadex'); $text = __('We are performing scheduled maintenance. We will be back shortly.', 'zadex'); } else { $title = __('Coming soon', 'zadex'); $text = __('Something new is on its way. Please check back soon.', 'zadex'); } $lang = function_exists('get_bloginfo') ? (string) get_bloginfo('language') : 'en'; wp_register_style('zadex-site-mode', false, [], ZADEX_VERSION); wp_add_inline_style('zadex-site-mode', self::BUILTIN_CSS); ?><?php echo esc_html($title . ($name !== '' ? ' - ' . $name : '')); ?>

mode(); if ($mode === 'off' || !is_object($bar) || !method_exists($bar, 'add_node') || !$this->user_allowed()) { return; } $bar->add_node([ 'id' => 'zadex-site-mode', 'title' => esc_html($this->notice_text($mode)), 'href' => current_user_can('manage_options') ? admin_url('admin.php?page=zadex') : '', 'meta' => ['class' => 'zadex-site-mode zadex-site-mode--' . $mode, 'title' => __('Visitors see the maintenance / coming soon page. Turn it off in Zadex > Site settings > Site.', 'zadex')], ]); } /** * Reminder that the site is hidden from visitors, for administrators only and only on the Dashboard and on * Zadex's own admin screens (never site-wide); dismissible like any core notice. */ public function admin_notice(): void { $mode = $this->mode(); if ($mode === 'off' || !current_user_can('manage_options') || !self::is_notice_screen()) { return; } echo '

' . esc_html($this->notice_text($mode)) . '. ' . esc_html__('Visitors who are not logged in see the maintenance / coming soon page. Turn it off in the Zadex editor: Site settings > Site.', 'zadex') . '

'; } /** Dashboard (index.php) or one of Zadex's admin pages (admin.php?page=zadex / zadex-*). */ public static function is_notice_screen(): bool { $screen = function_exists('get_current_screen') ? get_current_screen() : null; if (is_object($screen) && isset($screen->id) && $screen->id === 'dashboard') { return true; } // WordPress resolves ?page= into the $plugin_page global on admin.php. $page = isset($GLOBALS['plugin_page']) && is_string($GLOBALS['plugin_page']) ? $GLOBALS['plugin_page'] : ''; return $page === 'zadex' || str_starts_with($page, 'zadex-'); } /** Adds the option lists the Site panel needs (Zadex pages, roles, all pages) to the editor config. */ public function editor_config(array $config): array { $data = isset($config['data']) && is_array($config['data']) ? $config['data'] : []; $data['sitePages'] = self::zadex_page_options(); $data['roles'] = self::role_options(); $data['pages'] = self::page_options(); $config['data'] = $data; return $config; } /** Zadex-built pages (any status but trash) as select options. */ public static function zadex_page_options(): array { $out = [['value' => '0', 'label' => __('Built-in page', 'zadex')]]; if (!function_exists('get_posts')) { return $out; } $posts = get_posts([ 'post_type' => 'page', 'post_status' => ['publish', 'draft', 'private', 'pending', 'future'], 'posts_per_page' => 200, 'orderby' => 'title', 'order' => 'ASC', // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- lists pages built with Zadex (mode meta); bounded admin option list. 'meta_key' => Document::META_MODE, 'meta_value' => 'builder', // phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value ]); foreach ((array) $posts as $post) { $label = $post->post_title !== '' ? $post->post_title : sprintf(/* translators: %d: page id */ __('(no title) #%d', 'zadex'), $post->ID); if ($post->post_status !== 'publish') { $label .= ' (' . $post->post_status . ')'; } $out[] = ['value' => (string) $post->ID, 'label' => $label]; } return $out; } /** Published pages for the floating buttons' "only / except pages" conditions. */ public static function page_options(): array { $out = []; if (!function_exists('get_posts')) { return $out; } foreach ((array) get_posts(['post_type' => 'page', 'post_status' => 'publish', 'posts_per_page' => 200, 'orderby' => 'title', 'order' => 'ASC']) as $post) { $out[] = ['value' => (string) $post->ID, 'label' => $post->post_title !== '' ? $post->post_title : '#' . $post->ID]; } return $out; } /** Editable roles except administrator (always allowed). */ public static function role_options(): array { $out = []; $roles = function_exists('wp_roles') ? wp_roles()->get_names() : []; foreach ((array) $roles as $key => $name) { if ($key === 'administrator') { continue; } $out[] = ['value' => (string) $key, 'label' => function_exists('translate_user_role') ? translate_user_role((string) $name) : (string) $name]; } return $out; } }