settings = $settings; $this->editor = $editor; } private function editor(): EditorLoader { if ($this->editor === null) { $this->editor = Plugin::instance()->editor; } return $this->editor; } public function register(): void { add_action('admin_menu', [$this, 'menu']); add_action('admin_post_' . self::NONCE_ACTION, [$this, 'save_settings']); add_action('add_meta_boxes', [$this, 'meta_box']); add_filter('post_row_actions', [$this, 'row_action'], 10, 2); add_filter('page_row_actions', [$this, 'row_action'], 10, 2); add_action('enqueue_block_editor_assets', [$this, 'block_editor_assets']); add_action('edit_form_after_title', [$this, 'classic_editor_button']); add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']); } /** Delete-confirmation helper (`data-zadex-confirm` links), only on Zadex's own admin screens. */ public function enqueue_admin_assets(string $hook): void { $page = isset($_GET['page']) ? sanitize_key(wp_unslash($_GET['page'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only screen routing, no state change. if ($page !== 'zadex' && strpos($page, 'zadex-') !== 0) { return; } wp_enqueue_script('zadex-admin-confirm', ZADEX_URL . 'includes/Admin/assets/confirm.js', [], ZADEX_VERSION, true); } public function menu(): void { // Other screens add their markers to the menu title (Submissions: the unread count bubble, escaped there). $menu_title = (string) apply_filters('zadex/admin/menu_title', esc_html__('Zadex', 'zadex')); add_menu_page( __('Zadex', 'zadex'), $menu_title, 'manage_options', 'zadex', [$this, 'settings_page'], 'dashicons-layout', 58 ); // Explicit first submenu item, so WordPress does not copy the top-level title (with its bubble) into it. add_submenu_page('zadex', __('Zadex', 'zadex'), esc_html__('Zadex', 'zadex'), 'manage_options', 'zadex', [$this, 'settings_page']); } /** Post being edited on post.php / post-new.php (the global post WordPress sets up there; 0 when unknown). */ private function current_post_id(): int { $post = function_exists('get_post') ? get_post() : null; return $post instanceof \WP_Post && $post->ID ? (int) $post->ID : 0; } /** Data handed to includes/Admin/assets/gutenberg-button.js. */ public function admin_config(int $post_id): array { return [ 'postId' => $post_id, 'editUrl' => $this->editor()->edit_url($post_id), 'label' => __('Edit with Zadex', 'zadex'), 'isBuilder' => Document::is_builder($post_id), 'builtNotice' => self::built_notice(), ]; } /** Block editor: a real registered script (no inline JS) adds the toolbar button. */ public function block_editor_assets(): void { $post_id = $this->current_post_id(); if (!$post_id || !$this->editor()->can_edit($post_id)) { return; } wp_register_script( self::SCRIPT_HANDLE, ZADEX_URL . 'includes/Admin/assets/gutenberg-button.js', ['wp-plugins', 'wp-edit-post', 'wp-editor', 'wp-element', 'wp-data', 'wp-notices'], ZADEX_VERSION, true ); wp_localize_script(self::SCRIPT_HANDLE, 'zadexAdmin', $this->admin_config($post_id)); wp_enqueue_script(self::SCRIPT_HANDLE); } /** Classic editor: prominent button right under the title field. */ public function classic_editor_button(\WP_Post $post): void { if (!$this->editor()->can_edit((int) $post->ID)) { return; } if (function_exists('use_block_editor_for_post') && use_block_editor_for_post($post)) { return; } $built = Document::is_builder((int) $post->ID); echo '
'; echo '' . esc_html__('Edit with Zadex', 'zadex') . ''; echo '' . ($built ? esc_html__('This content is built with Zadex.', 'zadex') : esc_html__('Design this content visually with the live builder.', 'zadex')) . ''; echo '
'; } public function meta_box(): void { foreach ($this->settings->post_types() as $type) { add_meta_box('zadex-editor-box', __('Zadex Builder', 'zadex'), [$this, 'render_meta_box'], $type, 'side', 'high'); } } /** Read-only explanation shown wherever WordPress edits a page that Zadex renders (post_content is untouched). */ public static function built_notice(): string { return __('This page is built with Zadex. The content in the WordPress editor is not shown on the live page; open Zadex to change what visitors see.', 'zadex'); } public function render_meta_box(\WP_Post $post): void { $editor = $this->editor(); if (!$editor->can_edit((int) $post->ID)) { return; } $built = Document::is_builder((int) $post->ID); if ($built) { echo '

' . esc_html(self::built_notice()) . '

'; } else { echo '

' . esc_html__('Design this content visually.', 'zadex') . '

'; } echo '' . esc_html__('Edit with Zadex', 'zadex') . ''; } public function row_action(array $actions, \WP_Post $post): array { $editor = $this->editor(); if ($editor->can_edit((int) $post->ID)) { $actions['zadex'] = '' . esc_html__('Edit with Zadex', 'zadex') . ''; } return $actions; } public function settings_page(): void { if (!current_user_can('manage_options')) { return; } $breakpoints = $this->settings->breakpoints(); $enabled_types = $this->settings->post_types(); $post_types = get_post_types(['public' => true], 'objects'); unset($post_types['attachment']); ?>

$bp) : ?>
> —
settings->recaptcha(); ?>

isset($_POST['post_types']) && is_array($_POST['post_types']) ? array_map('sanitize_key', wp_unslash($_POST['post_types'])) : [], 'breakpoints' => isset($_POST['breakpoints']) && is_array($_POST['breakpoints']) ? map_deep(wp_unslash($_POST['breakpoints']), 'sanitize_text_field') : [], 'integrations' => isset($_POST['integrations']) && is_array($_POST['integrations']) ? map_deep(wp_unslash($_POST['integrations']), 'sanitize_text_field') : [], ]; foreach (['recaptcha_site_key', 'recaptcha_secret', 'recaptcha_secret_clear', 'stripe_secret_key', 'stripe_secret_key_clear'] as $field) { if (isset($_POST[$field]) && is_string($_POST[$field])) { $input[$field] = sanitize_text_field(wp_unslash($_POST[$field])); } } $this->handle_save($input); Notices::add('zadex', __('Settings saved.', 'zadex')); wp_safe_redirect(add_query_arg(['page' => 'zadex'], admin_url('admin.php'))); exit; } /** * Capability + nonce check, then sanitize and persist. Split from save_settings() so it can be * unit-tested (no redirect/exit). Returns the sanitized values that were saved. */ public function handle_save(array $input): array { if (!current_user_can('manage_options')) { wp_die(esc_html__('Not allowed.', 'zadex'), 403); } check_admin_referer(self::NONCE_ACTION); $values = self::sanitize_settings($input); $this->settings->update($values); return $values; } /** * Pure sanitizer for the settings form: post types must exist, breakpoint widths are clamped to * [200, 6000] px, desktop is always enabled, unknown breakpoints are dropped. */ public static function sanitize_settings(array $input): array { $post_types = array_values(array_unique(array_filter( array_map(static fn($t): string => sanitize_key((string) $t), (array) ($input['post_types'] ?? [])), 'post_type_exists' ))); $in_bps = is_array($input['breakpoints'] ?? null) ? $input['breakpoints'] : []; $breakpoints = []; foreach (Settings::default_breakpoints() as $key => $bp) { $in = isset($in_bps[$key]) && is_array($in_bps[$key]) ? $in_bps[$key] : []; $value = isset($in['value']) && is_numeric($in['value']) ? (int) $in['value'] : (int) $bp['value']; $breakpoints[$key] = [ 'value' => $bp['direction'] === 'base' ? 0 : max(self::BP_MIN, min(self::BP_MAX, $value)), 'enabled' => $key === 'desktop' ? true : !empty($in['enabled']), ]; } return ['post_types' => $post_types, 'breakpoints' => $breakpoints] + self::sanitize_recaptcha($input) + \Zadex\Forms\Integrations\Credentials::sanitize($input) + \Zadex\Payments\StripeCheckout::sanitize_settings($input); } /** * reCAPTCHA keys: only keys present in the form are returned (partial saves never wipe them); an empty * secret keeps the saved one unless "Remove" was ticked. */ public static function sanitize_recaptcha(array $input): array { $out = []; $clean = static fn($v): string => substr(preg_replace('/[^A-Za-z0-9_\-]/', '', is_scalar($v) ? (string) $v : '') ?? '', 0, 100); if (array_key_exists('recaptcha_site_key', $input)) { $out['recaptcha_site_key'] = $clean($input['recaptcha_site_key']); } if (!empty($input['recaptcha_secret_clear'])) { $out['recaptcha_secret'] = ''; } elseif (array_key_exists('recaptcha_secret', $input) && $clean($input['recaptcha_secret']) !== '') { $out['recaptcha_secret'] = $clean($input['recaptcha_secret']); } return $out; } }