PluginProbe ʕ •ᴥ•ʔ
WP 2FA – Two-factor authentication for WordPress / 4.0.0
WP 2FA – Two-factor authentication for WordPress v4.0.0
4.0.0 1.7.1 2.0.0 2.0.1 2.1.0 2.2.0 2.2.1 2.3.0 2.4.0 2.4.1 2.4.2 2.5.0 2.6.0 2.6.1 2.6.2 2.6.3 2.6.4 2.7.0 2.8.0 2.9.0 2.9.1 2.9.2 2.9.3 3.0.0 3.0.1 3.1.0 3.1.1 3.1.1.2 trunk 1.2.0 1.3.0 1.4.0 1.4.1 1.4.2 1.5.0 1.5.1 1.5.2 1.6.0 1.6.1 1.6.2 1.7.0
wp-2fa / includes / classes / Admin / SettingsPages / class-setup-wizard-new.php
wp-2fa / includes / classes / Admin / SettingsPages Last commit date
class-settings-page-email.php 1 day ago class-settings-page-general.php 1 day ago class-settings-page-new.php 1 day ago class-settings-page-passkeys.php 1 day ago class-settings-page-policies-new.php 1 day ago class-settings-page-policies.php 1 day ago class-settings-page-render.php 1 day ago class-settings-page-white-label.php 1 day ago class-settings-page-white-labeling-new.php 1 day ago class-setup-wizard-new.php 1 day ago index.php 1 day ago
class-setup-wizard-new.php
473 lines
1 <?php
2 /**
3 * New first-time setup wizard – static class.
4 *
5 * Renders a three-screen wizard (Welcome → Steps → Finish) that reuses the
6 * Settings_Builder components and CSS variables from the new settings UI.
7 * Step content is rendered by First_Time_Wizard_Steps_New. Data is submitted
8 * via AJAX so the page never reloads.
9 *
10 * @package wp2fa
11 * @subpackage settings-pages
12 * @since 4.0.0
13 */
14
15 namespace WP2FA\Admin\SettingsPages;
16
17 use WP2FA\WP2FA;
18 use WP2FA\Utils\Settings_Utils;
19 use WP2FA\Admin\Settings_Page;
20 use WP2FA\Admin\Helpers\User_Helper;
21 use WP2FA\Admin\Helpers\WP_Helper;
22 use WP2FA\Admin\Controllers\Settings;
23 use WP2FA\Admin\Views\First_Time_Wizard_Steps_New;
24 use WP2FA\Extensions\RoleSettings\Role_Settings_Controller;
25 use WP2FA\Methods\TOTP;
26 use WP2FA\Methods\Email;
27 use WP2FA\Methods\Backup_Codes;
28
29 if ( ! class_exists( '\WP2FA\Admin\SettingsPages\Setup_Wizard_New' ) ) {
30
31 /**
32 * New first-time wizard rendered as a standalone page.
33 *
34 * @since 4.0.0
35 */
36 class Setup_Wizard_New {
37
38 /**
39 * AJAX action name.
40 *
41 * @since 4.0.0
42 */
43 private const AJAX_ACTION = 'wp2fa_wizard_save';
44
45 /**
46 * Nonce action string.
47 *
48 * @since 4.0.0
49 */
50 private const NONCE_ACTION = 'wp2fa_wizard_new_nonce';
51
52 /**
53 * Whether this class should handle the wizard request.
54 *
55 * @return bool
56 *
57 * @since 4.0.0
58 */
59 public static function should_render(): bool {
60 return Settings_Page::is_new_interface_enabled();
61 }
62
63 /**
64 * Render the full wizard page (called from Setup_Wizard::setup_page).
65 *
66 * Outputs a complete HTML document and calls exit().
67 *
68 * @since 4.0.0
69 */
70 public static function render() {
71 self::render_page();
72 exit();
73 }
74
75 /**
76 * Register the AJAX save handler. Called once from WP2FA::init().
77 *
78 * @since 4.0.0
79 */
80 public static function init() {
81 \add_action( 'wp_ajax_' . self::AJAX_ACTION, array( __CLASS__, 'ajax_save' ) );
82 }
83
84 /**
85 * Output the full HTML page with three screens.
86 *
87 * @since 4.0.0
88 */
89 private static function render_page() {
90 // Register dialog assets (normally done on admin_enqueue_scripts,
91 // but the wizard renders and exits during admin_init).
92 \WP2FA\Core\register_dialog_assets();
93
94 // Enqueue assets.
95 \wp_enqueue_style(
96 'wp_2fa_settings_new_css',
97 WP_2FA_URL . 'includes/assets/css/settings.css',
98 array(),
99 WP_2FA_VERSION
100 );
101
102 \wp_enqueue_style(
103 'wp_2fa_wizard_new_css',
104 WP_2FA_URL . 'includes/assets/css/wizard-new.css',
105 array( 'wp_2fa_settings_new_css' ),
106 WP_2FA_VERSION
107 );
108
109 \wp_enqueue_script( 'wp2fa-dialog' );
110 \wp_enqueue_style( 'wp2fa-dialog' );
111
112 \wp_enqueue_script(
113 'wp_2fa_wizard_new_js',
114 WP_2FA_URL . 'includes/assets/js/wizard-new.js',
115 array( 'wp2fa-dialog' ),
116 WP_2FA_VERSION,
117 true
118 );
119
120 \wp_enqueue_script(
121 'wp_2fa_settings_new',
122 WP_2FA_URL . 'includes/assets/js/settings-design-logic.js',
123 array(),
124 WP_2FA_VERSION,
125 true
126 );
127
128 // "Exclude yourself?" prompt when zero-setup email is toggled on.
129 \wp_enqueue_script(
130 'wp_2fa_zero_email_exclude_self',
131 WP_2FA_URL . 'extensions/0-setup-email/js/zero-email-exclude-self.js',
132 array( 'wp_2fa_wizard_new_js' ),
133 WP_2FA_VERSION,
134 true
135 );
136
137 $user = \wp_get_current_user();
138 if ( $user && $user->exists() ) {
139 \wp_localize_script(
140 'wp_2fa_zero_email_exclude_self',
141 'wp2faZeroEmailExcludeSelf',
142 array(
143 'currentUserLogin' => \esc_js( $user->user_login ),
144 'modalTitle' => \esc_html__( 'Exclude yourself?', 'wp-2fa' ),
145 'modalBody' => \esc_html__( 'You are enabling Zero-setup email 2FA. This will enforce 2FA for all users, including your own account, replacing any current 2FA method enabled. That means you will be asked for a one-time code sent by email the next time you log in.', 'wp-2fa' )
146 . '<br><br>'
147 . \esc_html__( "If you don't want this applied to your account, you can exclude yourself from the 2FA policies before continuing.", 'wp-2fa' ),
148 'excludeBtnText' => \esc_html__( 'Exclude myself from 2FA policies', 'wp-2fa' ),
149 'continueBtnText' => \esc_html__( 'Continue anyway', 'wp-2fa' ),
150 'noteText' => \esc_html__( "Note: After closing this prompt, don't forget to save your settings for the changes to take effect.", 'wp-2fa' ),
151 )
152 );
153 }
154
155 \wp_localize_script(
156 'wp_2fa_wizard_new_js',
157 'wp2faWizardNew',
158 array(
159 'ajaxUrl' => \admin_url( 'admin-ajax.php' ),
160 'nonce' => \wp_create_nonce( self::NONCE_ACTION ),
161 'action' => self::AJAX_ACTION,
162 'searchAction' => 'wp2fa_search_policy_items',
163 'searchNonce' => \wp_create_nonce( 'wp2fa_save_policies_new_nonce' ),
164 'savingText' => \esc_html__( 'Saving…', 'wp-2fa' ),
165 'errorText' => \esc_html__( 'An error occurred. Please try again.', 'wp-2fa' ),
166 'methodsRequiredText' => \esc_html__( 'Please select at least one 2FA method', 'wp-2fa' ),
167 'configure2fa' => \esc_url( Settings::get_setup_page_link() ),
168 'settingsUrl' => \esc_url( Settings::get_settings_page_link() ),
169 'skipConfirmMessage' => \__( 'If you cancel this wizard, the default plugin settings will be applied. You can always configure the plugin settings and two-factor authentication policies at a later stage from the <b>WP 2FA</b> entry in your WordPress dashboard menu.', 'wp-2fa' ),
170 'skipConfirmOk' => \esc_html__( 'OK, close the wizard', 'wp-2fa' ),
171 'skipConfirmCancel' => \esc_html__( 'Continue with the wizard', 'wp-2fa' ),
172 )
173 );
174
175 $skip_url = \esc_url( Settings::get_settings_page_link() );
176 $logo_url = \esc_url( WP_2FA_URL . 'dist/images/wizard-logo.png' );
177
178 $is_excluded = User_Helper::is_excluded( User_Helper::get_user_object()->ID );
179 ?>
180 <!DOCTYPE html>
181 <html <?php \language_attributes(); ?>>
182 <head>
183 <meta charset="utf-8">
184 <meta name="viewport" content="width=device-width, initial-scale=1">
185 <title><?php \esc_html_e( 'WP 2FA — Setup Wizard', 'wp-2fa' ); ?></title>
186 <?php
187 \wp_enqueue_style( 'common' );
188 \wp_enqueue_style( 'forms' );
189 \wp_enqueue_style( 'buttons' );
190 \remove_action( 'admin_print_styles', 'print_emoji_styles' );
191 \do_action( 'admin_print_styles' );
192 \wp_print_scripts( 'wp2fa-dialog' );
193 \wp_print_scripts( 'wp_2fa_settings_new' );
194 \wp_print_scripts( 'wp_2fa_wizard_new_js' );
195 \wp_print_scripts( 'wp_2fa_zero_email_exclude_self' );
196 ?>
197 </head>
198 <body class="wp2fa-wizard-new wp-core-ui">
199
200 <!-- ============ Screen 1: Welcome ============ -->
201 <div id="wp2fa-wizard-welcome" class="wp2fa-wizard-screen wp2fa-wizard-screen--active">
202 <div class="wp2fa-wizard-welcome-card">
203 <div class="wp2fa-wizard-logo">
204 <img src="<?php echo $logo_url; // phpcs:ignore ?>" alt="WP 2FA">
205 <span class="wp2fa-wizard-logo-text"><?php \esc_html_e( 'WP 2FA', 'wp-2fa' ); ?></span>
206 </div>
207 <h2><?php \esc_html_e( 'Let\'s get you started', 'wp-2fa' ); ?></h2>
208 <p><?php \esc_html_e( 'Thank you for installing WP 2FA. This quick setup wizard will guide you through configuring the plugin and setting up two-factor authentication (2FA) for your user account and the users on this website.', 'wp-2fa' ); ?></p>
209 <div class="wp2fa-wizard-welcome-actions">
210 <button type="button" class="button button-primary button-hero js-wizard-start"><?php \esc_html_e( "Let's get started!", 'wp-2fa' ); ?></button>
211 <a href="<?php echo $skip_url; // phpcs:ignore ?>" class="wp2fa-wizard-skip-link"><?php \esc_html_e( 'Skip Wizard', 'wp-2fa' ); ?></a>
212 </div>
213 </div>
214 </div>
215
216 <!-- ============ Screen 2: Steps ============ -->
217 <div id="wp2fa-wizard-steps-screen" class="wp2fa-wizard-screen">
218 <main class="wp2fa-wizard-main">
219 <header class="wp2fa-wizard-header">
220 <div class="wp2fa-wizard-header-logo">
221 <img src="<?php echo $logo_url; // phpcs:ignore ?>" alt="WP 2FA">
222 <span><?php \esc_html_e( 'WP 2FA', 'wp-2fa' ); ?></span>
223 </div>
224 <nav class="wp2fa-wizard-nav">
225 <ol>
226 <li data-step="methods" class="is-active">
227 <span class="step-indicator"></span>
228 <span class="step-label"><?php \esc_html_e( '2FA METHODS', 'wp-2fa' ); ?></span>
229 </li>
230 <li data-step="alternative">
231 <span class="step-indicator"></span>
232 <span class="step-label"><?php \esc_html_e( 'ALTERNATIVE METHODS', 'wp-2fa' ); ?></span>
233 </li>
234 <li data-step="enforcement">
235 <span class="step-indicator"></span>
236 <span class="step-label"><?php \esc_html_e( '2FA POLICY', 'wp-2fa' ); ?></span>
237 </li>
238 <li data-step="exclusions">
239 <span class="step-indicator"></span>
240 <span class="step-label"><?php \esc_html_e( 'EXCLUDE USERS', 'wp-2fa' ); ?></span>
241 </li>
242 <?php if ( WP_Helper::is_multisite() ) : ?>
243 <li data-step="exclude-sites">
244 <span class="step-indicator"></span>
245 <span class="step-label"><?php \esc_html_e( 'EXCLUDE SITES', 'wp-2fa' ); ?></span>
246 </li>
247 <?php endif; ?>
248 <li data-step="grace">
249 <span class="step-indicator"></span>
250 <span class="step-label"><?php \esc_html_e( 'SET GRACE PERIOD', 'wp-2fa' ); ?></span>
251 </li>
252 </ol>
253 </nav>
254 </header>
255
256 <form id="wp2fa-wizard-form" class="wp2fa-wizard-form" autocomplete="off">
257
258 <div class="wp2fa-wizard-panel" data-panel="methods">
259 <?php First_Time_Wizard_Steps_New::step_methods(); ?>
260 </div>
261
262 <div class="wp2fa-wizard-panel" data-panel="alternative" style="display:none;">
263 <?php First_Time_Wizard_Steps_New::step_alternative_methods(); ?>
264 </div>
265
266 <div class="wp2fa-wizard-panel" data-panel="enforcement" style="display:none;">
267 <?php First_Time_Wizard_Steps_New::step_enforcement_policy(); ?>
268 </div>
269
270 <div class="wp2fa-wizard-panel" data-panel="exclusions" style="display:none;">
271 <?php First_Time_Wizard_Steps_New::step_exclude_users(); ?>
272 </div>
273
274 <?php if ( WP_Helper::is_multisite() ) : ?>
275 <div class="wp2fa-wizard-panel" data-panel="exclude-sites" style="display:none;">
276 <?php First_Time_Wizard_Steps_New::step_exclude_sites(); ?>
277 </div>
278 <?php endif; ?>
279
280 <div class="wp2fa-wizard-panel" data-panel="grace" style="display:none;">
281 <?php First_Time_Wizard_Steps_New::step_grace_period(); ?>
282 </div>
283
284 <footer class="wp2fa-wizard-footer">
285 <div class="wp2fa-wizard-footer-inner">
286 <button type="button" class="button button-primary js-wizard-continue"><?php \esc_html_e( 'Continue', 'wp-2fa' ); ?></button>
287 <button type="button" class="button button-primary js-wizard-finish" style="display:none;"><?php \esc_html_e( 'Finish Setup', 'wp-2fa' ); ?></button>
288 <a href="<?php echo $skip_url; // phpcs:ignore ?>" class="wp2fa-wizard-skip-link"><?php \esc_html_e( 'Skip Wizard', 'wp-2fa' ); ?></a>
289 </div>
290 </footer>
291 </form>
292 </main>
293 </div>
294
295 <!-- ============ Screen 3: Finish ============ -->
296 <div id="wp2fa-wizard-finish" class="wp2fa-wizard-screen">
297 <div class="wp2fa-wizard-welcome-card">
298 <div class="wp2fa-wizard-logo">
299 <img src="<?php echo $logo_url; // phpcs:ignore ?>" alt="WP 2FA">
300 <span class="wp2fa-wizard-logo-text"><?php \esc_html_e( 'WP 2FA', 'wp-2fa' ); ?></span>
301 </div>
302 <div data-finish-excluded <?php echo ( $is_excluded ) ? '' : 'style="display:none;"'; // phpcs:ignore ?> >
303 <h2><?php \esc_html_e( 'Congratulations.', 'wp-2fa' ); ?></h2>
304 <p><?php \esc_html_e( 'Great job, the plugin and 2FA policies are now configured. You can always change the plugin settings and 2FA policies at a later stage from the WP 2FA entry in the WordPress menu.', 'wp-2fa' ); ?></p>
305 <div class="wp2fa-wizard-welcome-actions">
306 <a href="<?php echo $skip_url; // phpcs:ignore ?>" class="wp2fa-wizard-skip-link"><?php \esc_html_e( 'Close wizard', 'wp-2fa' ); ?></a>
307 </div>
308 </div>
309
310 <div data-finish-normal <?php echo ( $is_excluded ) ? 'style="display:none;"' : ''; // phpcs:ignore ?> >
311 <h2><?php \esc_html_e( "Congratulations, you're almost there…", 'wp-2fa' ); ?></h2>
312 <p><?php \esc_html_e( 'Great job, the plugin and 2FA policies are now configured. You can always change the plugin settings and 2FA policies at a later stage from the WP 2FA entry in the WordPress menu.', 'wp-2fa' ); ?></p>
313 <p><?php \esc_html_e( 'Now you need to configure 2FA for your own user account. You can do this now (recommended) or later.', 'wp-2fa' ); ?></p>
314 <div class="wp2fa-wizard-welcome-actions">
315 <a href="<?php echo \esc_url( Settings::get_setup_page_link() ); ?>" class="button button-primary button-hero"><?php \esc_html_e( 'Configure 2FA Now', 'wp-2fa' ); ?></a>
316 <a href="<?php echo $skip_url; // phpcs:ignore ?>" class="wp2fa-wizard-skip-link"><?php \esc_html_e( 'Close wizard and configure 2FA Later', 'wp-2fa' ); ?></a>
317 </div>
318 </div>
319 </div>
320 </div>
321
322 </body>
323 </html>
324 <?php
325 }
326
327 /**
328 * Handle the wizard save via AJAX.
329 *
330 * Collects the policy fields from the form, validates via the existing
331 * policies validator, merges with existing settings, and marks the
332 * wizard as finished.
333 *
334 * @since 4.0.0
335 */
336 public static function ajax_save() {
337 // 1. Nonce.
338 $nonce = isset( $_POST['nonce'] ) ? \sanitize_text_field( \wp_unslash( $_POST['nonce'] ) ) : '';
339 if ( ! \wp_verify_nonce( $nonce, self::NONCE_ACTION ) ) {
340 \wp_send_json_error(
341 array( 'message' => \esc_html__( 'Security check failed. Please refresh and try again.', 'wp-2fa' ) ),
342 403
343 );
344 }
345
346 // 2. Capability.
347 if ( ! \current_user_can( 'manage_options' ) ) {
348 \wp_send_json_error(
349 array( 'message' => \esc_html__( 'Permission denied.', 'wp-2fa' ) ),
350 403
351 );
352 }
353
354 // 3. Collect fields.
355 if ( ! isset( $_POST[ WP_2FA_POLICY_SETTINGS_NAME ] ) || ! \is_array( $_POST[ WP_2FA_POLICY_SETTINGS_NAME ] ) ) {
356 \wp_send_json_error(
357 array( 'message' => \esc_html__( 'No data received.', 'wp-2fa' ) ),
358 400
359 );
360 }
361
362 $raw = \wp_unslash( $_POST[ WP_2FA_POLICY_SETTINGS_NAME ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
363
364 // Ensure method toggles are always present so unchecked boxes can disable methods.
365 $method_toggle_keys = array(
366 TOTP::POLICY_SETTINGS_NAME,
367 Email::POLICY_SETTINGS_NAME,
368 Backup_Codes::get_settings_name(),
369 'enable-email-backup',
370 'specify-backup-email',
371 );
372
373 $all_methods = \apply_filters( WP_2FA_PREFIX . 'methods_policy_settings_new', array(), null );
374 if ( \is_array( $all_methods ) ) {
375 foreach ( $all_methods as $method ) {
376 if ( isset( $method['setting'] ) && \is_string( $method['setting'] ) && '' !== $method['setting'] ) {
377 $method_toggle_keys[] = $method['setting'];
378 }
379 }
380 }
381
382 $method_toggle_keys = \array_values( \array_unique( $method_toggle_keys ) );
383 foreach ( $method_toggle_keys as $method_key ) {
384 if ( ! \array_key_exists( $method_key, $raw ) ) {
385 $raw[ $method_key ] = '';
386 }
387 }
388
389 // Backup email sub-option must be disabled if the parent method is disabled.
390 if ( empty( $raw['enable-email-backup'] ) ) {
391 $raw['specify-backup-email'] = '';
392 }
393
394 // Sanitize array fields (multi-select-ajax sends comma-separated strings).
395 $array_fields = array( 'excluded_users', 'excluded_roles', 'enforced_users', 'enforced_roles', 'included_sites', 'excluded_sites' );
396 foreach ( $array_fields as $field ) {
397 if ( isset( $raw[ $field ] ) ) {
398 $values = \is_array( $raw[ $field ] )
399 ? \array_map( 'sanitize_text_field', \array_map( 'trim', $raw[ $field ] ) )
400 : \array_map( 'sanitize_text_field', \array_map( 'trim', explode( ',', $raw[ $field ] ) ) );
401
402 $raw[ $field ] = \array_values( \array_filter( $values ) );
403 }
404 }
405
406 // Prevent Role_Settings_Controller from wiping role-specific data
407 // during a global-scope wizard save.
408 if ( \class_exists( Role_Settings_Controller::class ) ) {
409 \remove_filter( WP_2FA_PREFIX . 'filter_output_content', array( Role_Settings_Controller::class, 'validate_and_sanitize' ), 10 );
410 }
411
412 // 4. Validate via the existing policies validator.
413 $validated = Settings_Page_Policies::validate_and_sanitize( $raw, 'setup_wizard' );
414
415 // 5. Merge with existing settings so the wizard doesn't wipe
416 // values from fields it doesn't display.
417 $existing = Settings_Utils::get_option( WP_2FA_POLICY_SETTINGS_NAME, array() );
418
419 if ( \is_array( $validated ) && \is_array( $existing ) ) {
420 $submitted_keys = \array_keys( $raw );
421
422 // Some keys are derived/computed from other submitted keys.
423 $derived_from = array(
424 'grace-period-expiry-time' => array( 'grace-period', 'grace-period-denominator' ),
425 'custom-user-page-id' => array( 'create-custom-user-page', 'custom-user-page-url' ),
426 );
427
428 $merged = $existing;
429 foreach ( $validated as $key => $value ) {
430 if ( \in_array( $key, $submitted_keys, true ) ) {
431 $merged[ $key ] = $value;
432 } elseif ( isset( $derived_from[ $key ] ) && \array_intersect( $derived_from[ $key ], $submitted_keys ) ) {
433 $merged[ $key ] = $value;
434 } elseif ( ! \array_key_exists( $key, $existing ) ) {
435 $merged[ $key ] = $value;
436 }
437 }
438
439 // Guarantee method toggles reflect submitted wizard state even if not present in validator output.
440 foreach ( $method_toggle_keys as $method_key ) {
441 if ( \array_key_exists( $method_key, $validated ) ) {
442 $merged[ $method_key ] = $validated[ $method_key ];
443 } else {
444 $merged[ $method_key ] = ! empty( $raw[ $method_key ] ) ? \sanitize_text_field( (string) $raw[ $method_key ] ) : false;
445 }
446 }
447 } else {
448 $merged = \is_array( $validated ) ? $validated : $existing;
449 }
450
451 WP2FA::update_plugin_settings( $merged, false, WP_2FA_POLICY_SETTINGS_NAME );
452
453 // 6. Mark wizard as finished.
454 Settings_Utils::delete_option( WP_2FA_PREFIX . 'default_settings_applied' );
455 Settings_Utils::delete_option( 'wizard_not_finished' );
456
457 // 7. Fire extension hook.
458 \do_action( WP_2FA_PREFIX . 'policies_new_ajax_save', \wp_unslash( $_POST ) ); // phpcs:ignore
459
460 $current_user_id = User_Helper::get_user_object()->ID;
461 User_Helper::update_user_state( $current_user_id );
462 $is_excluded = User_Helper::is_excluded( $current_user_id );
463
464 \wp_send_json_success(
465 array(
466 'message' => \esc_html__( 'Setup complete.', 'wp-2fa' ),
467 'isCurrentUserExcluded' => (bool) $is_excluded,
468 )
469 );
470 }
471 }
472 }
473