settings = $settings; $this->mailer = $mailer; $this->conflicts = $conflicts; add_action( 'admin_menu', array( $this, 'register_page' ) ); add_action( 'admin_init', array( $this, 'maybe_redirect_after_activate' ) ); add_action( 'admin_post_wpcev_wizard_step', array( $this, 'handle_step_submit' ) ); add_action( 'admin_post_wpcev_wizard_skip', array( $this, 'handle_skip' ) ); } /** * Register the wizard as a page with no menu entry (empty parent slug), so * it is only reachable by its URL or the buttons that link to it. * * It used to be registered under Dashboard and then removed from the menu * with remove_submenu_page(). WordPress decides whether a user may open an * admin page by looking the page up in the menu, so a page removed from it * answers "Sorry, you are not allowed to access this page" and the wizard * could not be opened at all, neither on activation nor from the settings * page. A page registered without a parent is allowed and stays hidden. */ public function register_page() { $hook = add_submenu_page( '', __( 'Email Verify Setup', 'wpcoretools-email-verify' ), __( 'Email Verify Setup', 'wpcoretools-email-verify' ), 'manage_options', self::PAGE_SLUG, array( $this, 'render' ) ); if ( $hook ) { add_action( 'load-' . $hook, array( $this, 'set_page_title' ) ); } } /** * Give the page its title. WordPress looks a page's title up in the menu * and only finds it for a page that has a parent; for this one the global * stays null, which makes admin-header.php call strip_tags( null ) (a PHP 8.1+ * deprecation on every load) and leaves the browser tab without a title. */ public function set_page_title() { global $title; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- the page title, set before admin-header.php reads it. $title = __( 'Email Verify Setup', 'wpcoretools-email-verify' ); } public function maybe_redirect_after_activate() { if ( ! get_transient( self::RUN_TRANSIENT_KEY ) ) { return; } if ( get_option( self::COMPLETED_OPTION ) ) { delete_transient( self::RUN_TRANSIENT_KEY ); return; } /* * Every bail-out below is "not now", not "never": an AJAX call, a cron * tick, or a request from a user without manage_options can all happen * before the admin's own first pageload. Consuming the transient there * would swallow the redirect and the wizard would never appear. It is * only deleted once it has actually been acted on, or once the wizard * is known to be complete. The 5-minute TTL cleans up on its own. */ if ( wp_doing_ajax() || ( defined( 'DOING_CRON' ) && DOING_CRON ) || wp_doing_cron() ) { return; } if ( ! current_user_can( 'manage_options' ) ) { return; } // Skip when bulk-activating (multiple plugins activated together) — // redirecting would strand the other activations. // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( isset( $_GET['activate-multi'] ) ) { delete_transient( self::RUN_TRANSIENT_KEY ); return; } delete_transient( self::RUN_TRANSIENT_KEY ); wp_safe_redirect( $this->base_url() ); exit; } /** * Canonical wizard URL — uses `menu_page_url()` so it stays correct * regardless of which admin parent we registered under. */ private function base_url() { $url = function_exists( 'menu_page_url' ) ? menu_page_url( self::PAGE_SLUG, false ) : ''; if ( ! $url ) { $url = admin_url( 'admin.php?page=' . self::PAGE_SLUG ); } return $url; } /* ─── Render ─────────────────────────────────────────────────────────── */ public function render() { if ( ! current_user_can( 'manage_options' ) ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only step indicator. $step = isset( $_GET['step'] ) ? max( 1, min( self::STEP_COUNT, (int) $_GET['step'] ) ) : self::STEP_WELCOME; ?>

render_progress( $step ); ?>
render_step_post_notices(); ?>
step_gates(); break; case self::STEP_ROLES: $this->step_roles(); break; case self::STEP_EMAIL: $this->step_email(); break; case self::STEP_CONFLICTS: $this->step_conflicts(); break; case self::STEP_WELCOME: default: $this->step_welcome(); } ?>
__( 'Welcome', 'wpcoretools-email-verify' ), self::STEP_GATES => __( 'Restrictions', 'wpcoretools-email-verify' ), self::STEP_ROLES => __( 'Exempt roles', 'wpcoretools-email-verify' ), self::STEP_EMAIL => __( 'Email', 'wpcoretools-email-verify' ), self::STEP_CONFLICTS => __( 'Safety check', 'wpcoretools-email-verify' ), ); echo '
    '; foreach ( $labels as $i => $label ) { $state = $i < $step ? 'done' : ( $i === $step ? 'active' : 'pending' ); echo '
  1. '; echo '' . (int) $i . ''; echo '' . esc_html( $label ) . ''; echo '
  2. '; } echo '
'; } private function render_step_post_notices() { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- read-only flag. if ( ! empty( $_GET['wpcev_test'] ) ) { $res = sanitize_key( wp_unslash( $_GET['wpcev_test'] ) ); if ( 'sent' === $res ) { echo '

' . esc_html__( 'Test email sent — check your inbox.', 'wpcoretools-email-verify' ) . '

'; } else { echo '

' . esc_html__( 'Test email failed to send. Check your wp_mail / SMTP setup.', 'wpcoretools-email-verify' ) . '

'; } } if ( ! empty( $_GET['wpcev_preset'] ) ) { $slug = sanitize_key( wp_unslash( $_GET['wpcev_preset'] ) ); $cards = WPCEV_Presets::cards( class_exists( 'WooCommerce' ) ); if ( isset( $cards[ $slug ] ) && null !== $cards[ $slug ]['values'] ) { echo '

' . esc_html( sprintf( /* translators: %s: preset name, for example Shop */ __( '%s applied. The switches below show what it set; change any of them if you like, then continue.', 'wpcoretools-email-verify' ), $cards[ $slug ]['title'] ) ) . '

'; } } if ( ! empty( $_GET['wpcev_scanned'] ) ) { echo '

' . esc_html__( 'Code scan finished. The results are in the list below.', 'wpcoretools-email-verify' ) . '

'; } // phpcs:enable WordPress.Security.NonceVerification.Recommended } /* ─── Step 1: Welcome ────────────────────────────────────────────────── */ private function step_welcome() { ?>

render_nav( self::STEP_WELCOME, false, true ); ?>
settings->all(); $wc = class_exists( 'WooCommerce' ); $cards = WPCEV_Presets::cards( $wc ); $current = WPCEV_Presets::match( $s ); $recommended = WPCEV_Presets::recommended( $wc ); $descriptors = WPCEV_Presets::descriptors( $wc ); ?>

$card ) : ?>

$descriptor ) : ?>

render_nav( self::STEP_GATES ); ?>
settings->all(); $roles = wp_roles()->roles; ?>

render_nav( self::STEP_ROLES ); ?>
settings->all(); ?>

render_nav( self::STEP_EMAIL, true, false, true ); ?>
conflicts->detect( true ); $guard_active = defined( 'WPCDEG_VERSION' ); ?>

conflicts->render_scan_panel( 'wizard' ); ?>

render_nav( self::STEP_CONFLICTS, true, false, false, true ); ?>

base_url() ); } private function skip_url() { return wp_nonce_url( add_query_arg( 'action', 'wpcev_wizard_skip', admin_url( 'admin-post.php' ) ), 'wpcev_wizard' ); } /* ─── Submit handlers ────────────────────────────────────────────────── */ public function handle_step_submit() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to do this.', 'wpcoretools-email-verify' ) ); } check_admin_referer( 'wpcev_wizard' ); $step = isset( $_POST['step'] ) ? max( 1, min( self::STEP_COUNT, (int) $_POST['step'] ) ) : self::STEP_WELCOME; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- per-key sanitization in Settings::sanitize(). $raw = isset( $_POST['wpcev'] ) && is_array( $_POST['wpcev'] ) ? wp_unslash( $_POST['wpcev'] ) : array(); $next = isset( $_POST['wpcev_next'] ) ? sanitize_key( wp_unslash( $_POST['wpcev_next'] ) ) : 'save'; // A preset card was pressed: apply that card on its own and stay on the // step so the switches below show what it set. The posted switches are // ignored on purpose: picking a card replaces what was ticked by hand. if ( self::STEP_GATES === $step && ! empty( $raw['preset'] ) ) { $slug = sanitize_key( $raw['preset'] ); $this->settings->save( array( 'preset' => $slug ), true ); wp_safe_redirect( add_query_arg( array( 'step' => self::STEP_GATES, 'wpcev_preset' => $slug, ), $this->base_url() ) ); exit; } // Welcome step has no settings to save; just advance. if ( self::STEP_WELCOME !== $step && $raw ) { $this->settings->save( $raw, true ); } // "Send test email" button — save current values, send to admin email, // stay on this step so the admin sees the result inline. if ( 'test' === $next ) { $user = wp_get_current_user(); $res = $this->mailer->send_test( $user->ID, $user->user_email ); wp_safe_redirect( add_query_arg( array( 'step' => $step, 'wpcev_test' => $res, ), $this->base_url() ) ); exit; } $finish = ! empty( $_POST['finish'] ); if ( $finish || self::STEP_CONFLICTS === $step ) { update_option( self::COMPLETED_OPTION, time(), false ); wp_safe_redirect( add_query_arg( array( 'page' => WPCEV_Admin::PAGE_SLUG, 'wpcev_done' => 1, ), admin_url( 'options-general.php' ) ) ); exit; } $next_step = min( self::STEP_COUNT, $step + 1 ); wp_safe_redirect( $this->step_url( $next_step ) ); exit; } public function handle_skip() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to do this.', 'wpcoretools-email-verify' ) ); } check_admin_referer( 'wpcev_wizard' ); update_option( self::COMPLETED_OPTION, time(), false ); wp_safe_redirect( admin_url( 'options-general.php?page=' . WPCEV_Admin::PAGE_SLUG ) ); exit; } }