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;
?>
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();
}
?>
' . 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']
)
) . '