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 / class-wizard-integration.php
wp-2fa / includes / classes / Admin Last commit date
AdminSettings 17 hours ago Controllers 17 hours ago Fly-Out 17 hours ago Helpers 17 hours ago Methods 17 hours ago Settings 17 hours ago SettingsPages 17 hours ago Views 17 hours ago class-about-us.php 17 hours ago class-docs-and-support.php 17 hours ago class-free-support-notice.php 17 hours ago class-help-contact-us.php 17 hours ago class-license-page.php 17 hours ago class-new-interface-notice.php 17 hours ago class-plugin-updated-notice.php 17 hours ago class-premium-features.php 17 hours ago class-profile-section-renderer.php 17 hours ago class-settings-page.php 17 hours ago class-setup-wizard.php 17 hours ago class-top-bar-banner.php 17 hours ago class-user-listing.php 17 hours ago class-user-notices.php 17 hours ago class-user-profile.php 17 hours ago class-user-registered.php 17 hours ago class-wizard-integration.php 17 hours ago index.php 17 hours ago
class-wizard-integration.php
1429 lines
1 <?php
2 /**
3 * WP 2FA - New Wizard Integration
4 *
5 * Enqueues the new JS/CSS wizard assets on user profile pages and
6 * provides the localization data (enabled methods, TOTP data, etc.)
7 * via wp_localize_script.
8 *
9 * Premium / extension methods should NOT be referenced here directly.
10 * Instead they hook into the following WP filters and actions:
11 *
12 * Actions:
13 * wp_2fa_wizard_enqueue_assets - Fired during asset enqueue so extensions
14 * can register their own JS/CSS files.
15 * Args: (string $plugin_url, string $version, string $core_handle)
16 *
17 * Filters:
18 * wp_2fa_wizard_enabled_methods - Register method as enabled. Args: (array, WP_User, string $role)
19 * wp_2fa_wizard_method_labels - Add/override method labels. Args: (array $labels, WP_User, string $role)
20 * wp_2fa_wizard_method_descriptions - Add/override method descriptions. Args: (array $descs, WP_User, string $role)
21 * wp_2fa_wizard_localized_data - Final filter on the full localized data array.
22 * Args: (array $data, WP_User, string $role)
23 * wp_2fa_wizard_i18n_strings - Add/override translatable strings. Args: (array $strings)
24 *
25 * @package wp2fa
26 * @subpackage wizard
27 * @since 4.0.0
28 * @copyright 2026 Melapress
29 * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
30 */
31
32 declare( strict_types=1 );
33
34 namespace WP2FA\Admin;
35
36 use WP2FA\WP2FA;
37 use WP2FA\Methods\TOTP;
38 use WP2FA\Utils\Settings_Utils;
39 use WP2FA\Admin\Controllers\Methods;
40 use WP2FA\Admin\Helpers\User_Helper;
41 use WP2FA\Admin\Controllers\Settings;
42 use WP2FA\Admin\Helpers\Email_Templates;
43 use WP2FA\Methods\Backup_Codes;
44 use WP2FA\Admin\Helpers\Methods_Helper;
45 use WP2FA\Admin\Views\Re_Login_2FA;
46
47 defined( 'ABSPATH' ) || exit;
48
49 if ( ! class_exists( '\WP2FA\Admin\Wizard_Integration' ) ) {
50
51 /**
52 * Handles the new JS-driven 2FA setup wizard for user profiles.
53 *
54 * @since 4.0.0
55 */
56 class Wizard_Integration {
57
58 /**
59 * Script handle for the wizard core.
60 *
61 * @var string
62 */
63 const SCRIPT_HANDLE = 'wp2fa-setup-wizard';
64
65 /**
66 * Registers hooks.
67 *
68 * @return void
69 */
70 public static function init(): void {
71 \add_action( 'show_user_profile', array( __CLASS__, 'render_profile_section' ), 25 );
72 \add_action( 'edit_user_profile', array( __CLASS__, 'render_profile_section' ), 25 );
73 \add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ) );
74 \add_action( 'admin_footer', array( __CLASS__, 'print_wizard_templates' ) );
75 \add_action( 'wp_ajax_wp2fa_dismiss_required_intro', array( __CLASS__, 'ajax_dismiss_required_intro' ) );
76 }
77
78 /**
79 * Decide whether to load assets on the current screen.
80 *
81 * @return bool
82 */
83 private static function is_profile_page(): bool {
84 $screen = \function_exists( 'get_current_screen' ) ? \get_current_screen() : null;
85 if ( ! $screen ) {
86 return false;
87 }
88
89 return in_array( $screen->id, array( 'profile', 'user-edit', 'profile-network', 'user-edit-network' ), true );
90 }
91
92 /**
93 * Enqueue CSS and JS on user profile pages.
94 *
95 * @return void
96 */
97 public static function enqueue_assets(): void {
98 if ( ! self::is_profile_page() ) {
99 return;
100 }
101
102 // Profile section assets (cards, buttons, etc.).
103 Profile_Section_Renderer::enqueue_assets();
104
105 $plugin_url = WP_2FA_URL;
106 $version = WP_2FA_VERSION;
107
108 \wp_enqueue_style(
109 'wp2fa-setup-wizard',
110 $plugin_url . 'css/wp2fa-wizard.css',
111 array(),
112 $version
113 );
114
115 // Core wizard JS (depends on wp-hooks and wp-util for wp.template).
116 \wp_enqueue_script(
117 self::SCRIPT_HANDLE,
118 $plugin_url . 'js/wp2fa-wizard.js',
119 array( 'wp-hooks', 'wp-util' ),
120 $version,
121 true
122 );
123
124 // Core method modules (always present).
125 \wp_enqueue_script(
126 'wp2fa-wizard-totp',
127 $plugin_url . 'js/wp2fa-wizard-totp.js',
128 array( self::SCRIPT_HANDLE, 'wp-hooks' ),
129 $version,
130 true
131 );
132
133 \wp_enqueue_script(
134 'wp2fa-wizard-email',
135 $plugin_url . 'js/wp2fa-wizard-email.js',
136 array( self::SCRIPT_HANDLE, 'wp-hooks' ),
137 $version,
138 true
139 );
140
141 // Backup codes module.
142 \wp_enqueue_script(
143 'wp2fa-wizard-backup-codes',
144 $plugin_url . 'js/wp2fa-wizard-backup-codes.js',
145 array( self::SCRIPT_HANDLE, 'wp-hooks' ),
146 $version,
147 true
148 );
149
150 /**
151 * Action: wp_2fa_wizard_enqueue_assets
152 *
153 * Allows premium / extension methods to enqueue their own wizard
154 * JS and CSS files conditionally.
155 *
156 * @param string $plugin_url The plugin root URL (with trailing slash).
157 * @param string $version The plugin version string.
158 * @param string $core_handle The script handle of the wizard core JS.
159 *
160 * @since 4.0.0
161 */
162 \do_action( WP_2FA_PREFIX . 'wizard_enqueue_assets', $plugin_url, $version, self::SCRIPT_HANDLE );
163
164 // Localized data – attached to the core handle.
165 \wp_localize_script( self::SCRIPT_HANDLE, 'wp2faWizardData', self::get_localized_data() );
166 }
167
168 /**
169 * Build the localization payload for the wizard scripts.
170 *
171 * @return array<string,mixed>
172 */
173 private static function get_localized_data(): array {
174 // $user = self::get_target_user();
175 User_Helper::set_user( \wp_get_current_user() );
176 $role = User_Helper::get_user_role();
177
178 $user = User_Helper::get_user();
179
180 // Determine which methods are enabled for this user's role.
181 $enabled_methods = array();
182 $method_labels = array();
183 $method_descs = array();
184
185 $available_methods = Methods::get_enabled_methods( User_Helper::get_user_role() );
186
187 // Use methods for the user's role if available, otherwise fall back to global.
188 $role_methods = array();
189 if ( is_array( $available_methods ) && ! empty( $available_methods ) ) {
190 if ( \array_key_exists( $role, $available_methods ) && ! empty( $available_methods[ $role ] ) ) {
191 $role_methods = $available_methods[ $role ];
192 } elseif ( \array_key_exists( 'global', $available_methods ) ) {
193 $role_methods = $available_methods['global'];
194 }
195 }
196
197 foreach ( $role_methods as $method_id => $method_data ) {
198 $enabled_methods[ $method_id ] = true;
199 }
200
201 /**
202 * Filter: wp_2fa_wizard_enabled_methods
203 *
204 * Allows external classes to register themselves as enabled for the given user (should never be used).
205 * Each method should add its key (e.g. 'totp', 'email') with a truthy value.
206 *
207 * @param array $enabled_methods Associative array of method_id => bool.
208 * @param \WP_User $user The target user.
209 * @param string $role The user's role.
210 *
211 * @since 4.0.0
212 */
213 $enabled_methods = \apply_filters( WP_2FA_PREFIX . 'wizard_enabled_methods', $enabled_methods, $user, $role );
214
215 // White label / translatable labels.
216 $method_labels['totp'] = WP2FA::get_wp2fa_white_label_setting( 'totp-option-label', true ) ?: __( 'One-time code via 2FA App (TOTP)', 'wp-2fa' );
217 $method_labels['email'] = WP2FA::get_wp2fa_white_label_setting( 'email-option-label', true ) ?: __( 'One-time code via email (HOTP)', 'wp-2fa' );
218
219 /**
220 * Filter: wp_2fa_wizard_method_labels
221 *
222 * Allows methods to add or override their display labels.
223 *
224 * @param array $method_labels Associative array of method_id => label string.
225 * @param \WP_User $user The target user.
226 * @param string $role The user's role.
227 *
228 * @since 4.0.0
229 */
230 $method_labels = \apply_filters( WP_2FA_PREFIX . 'wizard_method_labels', $method_labels, $user, $role );
231
232 $method_descs = array(
233 'totp' => __( 'Refer to the guide on how to set up 2FA apps for more information on how to setup these apps and which apps are supported.', 'wp-2fa' ),
234 'email' => '',
235 );
236
237 /**
238 * Filter: wp_2fa_wizard_method_descriptions
239 *
240 * Allows methods to add or override their descriptions.
241 *
242 * @param array $method_descs Associative array of method_id => description string.
243 * @param \WP_User $user The target user.
244 * @param string $role The user's role.
245 *
246 * @since 4.0.0
247 */
248 $method_descs = \apply_filters( WP_2FA_PREFIX . 'wizard_method_descriptions', $method_descs, $user, $role );
249 $method_descs = \array_map( 'wp_kses_post', $method_descs );
250
251 // -------------------------------------------------------
252 // Method hints (info icon tooltip text).
253 // Only populated when the "show_help_text" white-label
254 // setting is enabled. Keys match method IDs.
255 // -------------------------------------------------------
256 $method_hints = array();
257 $show_help_text = ( 'show_help_text' === WP2FA::get_wp2fa_white_label_setting( 'show_help_text' ) );
258
259 if ( $show_help_text ) {
260 foreach ( \array_keys( $enabled_methods ) as $method_id ) {
261 $hint = WP2FA::get_wp2fa_white_label_setting( $method_id . '-option-label-hint', true );
262 if ( ! empty( $hint ) ) {
263 $method_hints[ $method_id ] = \wp_kses_post( $hint );
264 }
265 }
266
267 /**
268 * Filter: wp_2fa_wizard_method_hints
269 *
270 * Allows methods to add or override their hint texts shown via the info icon.
271 *
272 * @param array $method_hints Associative array of method_id => hint HTML string.
273 * @param \WP_User $user The target user.
274 * @param string $role The user's role.
275 *
276 * @since 4.0.0
277 */
278 $method_hints = \apply_filters( WP_2FA_PREFIX . 'wizard_method_hints', $method_hints, $user, $role );
279 $method_hints = \array_map( 'wp_kses_post', $method_hints );
280 }
281
282 // -------------------------------------------------------
283 // TOTP-specific data.
284 // -------------------------------------------------------
285 $totp_data = array();
286 if ( ! empty( $enabled_methods['totp'] ) && class_exists( '\WP2FA\Methods\TOTP' ) ) {
287 User_Helper::set_user( \wp_get_current_user() );
288 $totp_data = array(
289 'qrCodeUrl' => TOTP::get_qr_code(),
290 'totpKey' => TOTP::get_totp_decrypted(),
291 );
292 }
293
294 // -------------------------------------------------------
295 // Email-specific data.
296 // -------------------------------------------------------
297 $email_data = array();
298 if ( ! empty( $enabled_methods['email'] ) ) {
299 $from_email = \get_option( 'admin_email' );
300 $custom_mail = Email_Templates::get_wp2fa_email_templates( 'custom_from_email_address' );
301
302 if ( ! empty( $custom_mail ) ) {
303 $from_email = $custom_mail;
304 } else {
305 $from_email = Settings_Page::get_default_email_address();
306 }
307
308 $nominated_email = User_Helper::get_nominated_email_for_user( $user );
309 $email_data = array(
310 'userEmail' => $user->user_email,
311 'allowCustomEmail' => ! empty( Settings_Utils::get_setting_role( $role, 'specify-email_hotp' ) ),
312 'fromEmail' => $from_email,
313 'nominatedEmail' => $nominated_email !== $user->user_email ? $nominated_email : '',
314 );
315 }
316
317 // -------------------------------------------------------
318 // Backup methods.
319 // -------------------------------------------------------
320 $backup_methods = array();
321 $backup_method_labels = array();
322 $backup_codes_data = array();
323
324 if ( class_exists( '\WP2FA\Methods\Backup_Codes' ) && Backup_Codes::are_backup_codes_enabled_for_role( $role ) ) {
325 $backup_methods['backup_codes'] = true;
326 $backup_method_labels['backup_codes'] = Backup_Codes::get_select_method_label();
327 $backup_codes_data = array(
328 'nonce' => \wp_create_nonce( 'wp-2fa-backup-codes-generate-json-' . $user->ID ),
329 'emailNonce' => \wp_create_nonce( 'wp-2fa-send-backup-codes-email-nonce' ),
330 );
331 }
332
333 $role_backup_methods = Settings::get_enabled_backup_methods_for_user_role( $user );
334 if ( ! empty( $role_backup_methods ) ) {
335 foreach ( $role_backup_methods as $method_id => $method_data ) {
336 if ( ! isset( $backup_methods[ $method_id ] ) ) {
337 $backup_methods[ $method_id ] = true;
338 if ( is_array( $method_data ) && ! empty( $method_data['button_name'] ) ) {
339 $backup_method_labels[ $method_id ] = $method_data['button_name'];
340 }
341 }
342 }
343 }
344 $backup_method_labels = \array_map( 'wp_kses_post', $backup_method_labels );
345
346 // -------------------------------------------------------
347 // Method ordering.
348 // -------------------------------------------------------
349 $method_orders = array();
350 $methods_order_setting = Settings_Utils::get_setting_role( $role, Methods_Helper::POLICY_SETTINGS_NAME, true );
351 if ( \is_array( $methods_order_setting ) ) {
352 $flipped = \array_flip( $methods_order_setting );
353 foreach ( $flipped as $method_name => $order ) {
354 $method_orders[ $method_name ] = (int) $order;
355 }
356 }
357
358 // -------------------------------------------------------
359 // Welcome screen (optional white-label setting).
360 // -------------------------------------------------------
361 $welcome_html = '';
362 $enable_welcome = WP2FA::get_wp2fa_white_label_setting( 'enable_welcome', false );
363 if ( 'enable_welcome' === $enable_welcome ) {
364 $welcome_content = WP2FA::get_wp2fa_white_label_setting( 'welcome', false );
365 if ( ! empty( trim( (string) $welcome_content ) ) ) {
366 $welcome_html = \wp_kses_post( $welcome_content );
367 }
368 }
369
370 // -------------------------------------------------------
371 // Required intro screen (shown before everything for enforced users).
372 // -------------------------------------------------------
373 $required_intro_html = '';
374 $is_user_enforced = User_Helper::is_enforced( $user->ID );
375 if ( $is_user_enforced ) {
376 $intro_content = WP2FA::get_wp2fa_white_label_setting( 'wp-2fa_required_intro', true );
377 $intro_seen = \get_user_meta( $user->ID, WP_2FA_PREFIX . 'required_intro_seen', true );
378 if ( ! empty( trim( (string) $intro_content ) ) && empty( $intro_seen ) ) {
379 $required_intro_html = \wp_kses_post( $intro_content );
380 }
381 }
382
383 // -------------------------------------------------------
384 // Redirect URL after successful 2FA setup.
385 // Priority: redirect-user-custom-page > redirect-user-custom-page-global > none (reload).
386 // -------------------------------------------------------
387 $redirect_url = '';
388 $role_redirect = \sanitize_text_field( (string) Settings_Utils::get_setting_role( $role, 'redirect-user-custom-page' ) );
389 $global_redirect = \sanitize_text_field( (string) Settings_Utils::get_setting_role( null, 'redirect-user-custom-page' ) );
390
391 if ( '' !== trim( $role_redirect ) ) {
392 $redirect_url = \trailingslashit( \get_site_url() ) . $role_redirect;
393 } elseif ( '' !== trim( $global_redirect ) ) {
394 $redirect_url = \trailingslashit( \get_site_url() ) . $global_redirect;
395 } else {
396 // Fallback: check redirect-user-custom-page-global.
397 $global_page_redirect = \sanitize_text_field( (string) Settings_Utils::get_setting_role( $role, 'redirect-user-custom-page-global' ) );
398 if ( '' !== trim( $global_page_redirect ) ) {
399 $redirect_url = \trailingslashit( \get_site_url() ) . $global_page_redirect;
400 }
401 }
402
403 // -------------------------------------------------------
404 // Reconfigure intro texts (shown when user already has 2FA).
405 // -------------------------------------------------------
406 $current_method = User_Helper::get_enabled_method_for_user( $user );
407 $is_reconfiguration = ! empty( $current_method );
408 $reconfigure_intros = array();
409 $reconfigure_intros_unavailable = array();
410 $unavailable_methods = array();
411
412 if ( $is_reconfiguration ) {
413 $totp_reconfigure = WP2FA::get_wp2fa_white_label_setting( 'totp_reconfigure_intro', true );
414 if ( ! empty( $totp_reconfigure ) ) {
415 $reconfigure_intros['totp'] = \wp_kses_post( $totp_reconfigure );
416 }
417
418 $hotp_reconfigure = WP2FA::get_wp2fa_white_label_setting( 'hotp_reconfigure_intro', true );
419 if ( ! empty( $hotp_reconfigure ) ) {
420 $reconfigure_intros['email'] = \wp_kses_post( $hotp_reconfigure );
421 }
422
423 /**
424 * Filter: wp_2fa_wizard_reconfigure_intros
425 *
426 * Allows extensions to add their reconfigure intro texts.
427 *
428 * @param array $reconfigure_intros Associative array of method_id => raw HTML text with placeholders.
429 * @param \WP_User $user The target user.
430 * @param string $role The user's role.
431 *
432 * @since 4.0.0
433 */
434 $reconfigure_intros = \apply_filters( WP_2FA_PREFIX . 'wizard_reconfigure_intros', $reconfigure_intros, $user, $role );
435
436 /**
437 * Filter: wp_2fa_wizard_reconfigure_intros_unavailable
438 *
439 * Allows extensions to add their unavailable reconfigure intro texts.
440 *
441 * @param array $reconfigure_intros_unavailable Associative array of method_id => raw HTML text.
442 * @param \WP_User $user The target user.
443 * @param string $role The user's role.
444 *
445 * @since 4.0.0
446 */
447 $reconfigure_intros_unavailable = \apply_filters( WP_2FA_PREFIX . 'wizard_reconfigure_intros_unavailable', $reconfigure_intros_unavailable, $user, $role );
448
449 /**
450 * Filter: wp_2fa_wizard_unavailable_methods
451 *
452 * Allows extensions to mark methods as unavailable (service down, API key invalid, etc).
453 *
454 * @param array $unavailable_methods Array of method IDs that are unavailable.
455 * @param \WP_User $user The target user.
456 * @param string $role The user's role.
457 *
458 * @since 4.0.0
459 */
460 $unavailable_methods = \apply_filters( WP_2FA_PREFIX . 'wizard_unavailable_methods', $unavailable_methods, $user, $role );
461 }
462
463 // -------------------------------------------------------
464 // Assemble the data array.
465 // -------------------------------------------------------
466 $data = array(
467 'ajaxUrl' => \admin_url( 'admin-ajax.php' ),
468 'nonce' => \wp_create_nonce( 'wp-2fa-validate-authcode' ),
469 'sendEmailNonce' => \wp_create_nonce( 'wp-2fa-send-setup-email' ),
470 'userId' => $user->ID,
471 'userRole' => $role,
472 'currentMethod' => $current_method,
473 'isReconfiguration' => $is_reconfiguration,
474 'reconfigureIntros' => $reconfigure_intros,
475 'reconfigureIntrosUnavailable' => $reconfigure_intros_unavailable,
476 'unavailableMethods' => $unavailable_methods,
477 'enabledMethods' => $enabled_methods,
478 'methodLabels' => $method_labels,
479 'methodDescriptions' => $method_descs,
480 'methodHints' => $method_hints,
481 'totpData' => $totp_data,
482 'emailData' => $email_data,
483 'backupMethods' => $backup_methods,
484 'backupMethodLabels' => $backup_method_labels,
485 'backupCodesData' => $backup_codes_data,
486 'methodOrders' => $method_orders,
487 'reloadOnSuccess' => true,
488 'redirectToUrl' => $redirect_url,
489 'reLogin' => Settings_Utils::get_setting_role( $role, Re_Login_2FA::RE_LOGIN_SETTINGS_NAME ),
490 'reLoginEnabled' => Re_Login_2FA::ENABLED_SETTING_VALUE,
491 'loginUrl' => \wp_login_url(),
492 'autoOpenWizard' => self::should_auto_open_wizard( $user ),
493 'welcomeHtml' => $welcome_html,
494 'requiredIntroHtml' => $required_intro_html,
495 'dismissIntroNonce' => \wp_create_nonce( 'wp-2fa-dismiss-required-intro' ),
496 'stylingClass' => ( empty( WP2FA::get_wp2fa_white_label_setting( 'enable_wizard_styling' ) ) ) ? 'default_styling' : 'enable_styling',
497 'i18n' => self::get_i18n_strings(),
498 );
499
500 /**
501 * Filter: wp_2fa_wizard_localized_data
502 *
503 * Final filter on the complete localized data array passed to JS.
504 * Extensions can add their own method-specific data keys here.
505 *
506 * @param array $data The complete wp2faWizardData array.
507 * @param \WP_User $user The target user.
508 * @param string $role The user's role.
509 *
510 * @since 4.0.0
511 */
512 return \apply_filters( WP_2FA_PREFIX . 'wizard_localized_data', $data, $user, $role );
513 }
514
515 /**
516 * Translatable strings for the JS wizard.
517 *
518 * @return array<string,string>
519 */
520 private static function get_i18n_strings(): array {
521 $strings = array(
522 'chooseMethod' => \wp_kses_post( WP2FA::get_wp2fa_white_label_setting( 'method_selection', true ) ),
523 'goBack' => \__( 'Go back', 'wp-2fa' ),
524 'continueBtn' => \__( 'Continue', 'wp-2fa' ),
525 'close' => \__( 'Close', 'wp-2fa' ),
526 'cancel' => \__( 'Cancel', 'wp-2fa' ),
527 'imReady' => \__( "I'm Ready", 'wp-2fa' ),
528 'validateSave' => \__( 'Validate & Save', 'wp-2fa' ),
529 'emailVerifyIntro' => \wp_kses_post( WP2FA::get_wp2fa_white_label_setting( 'method_verification_hotp_pre', true ) ) ?: \__( 'Please type in the one-time code sent to your email address to finalize the setup', 'wp-2fa' ),
530 'verificationCode' => \__( 'Verification Code:', 'wp-2fa' ),
531 'enterCode' => \__( 'Please enter the verification code.', 'wp-2fa' ),
532 'invalidCode' => \__( 'Invalid or expired OTP code. Please try again.', 'wp-2fa' ),
533 'invalidCodeFormat' => \__( 'Invalid code. Please enter the correct OTP code generated by your Authenticator app.', 'wp-2fa' ),
534 'networkError' => \__( 'Network error. Please try again.', 'wp-2fa' ),
535 'verifyConfig' => \__( 'Verify configuration', 'wp-2fa' ),
536
537 // TOTP.
538 'settingUpTotp' => \wp_kses_post( WP2FA::get_wp2fa_white_label_setting( 'method_help_totp_intro', true ) ) ?: \__( 'Setting up TOTP (one-time code via app)', 'wp-2fa' ),
539 'totpStep1' => \wp_kses_post( WP2FA::get_wp2fa_white_label_setting( 'method_help_totp_step_1', true ) ) ?: \__( 'Download and start the application of your choice', 'wp-2fa' ),
540 'totpStep2' => \wp_kses_post( WP2FA::get_wp2fa_white_label_setting( 'method_help_totp_step_2', true ) ) ?: \__( 'From within the application scan the QR code provided on the left. Otherwise, enter the following code manually in the application:', 'wp-2fa' ),
541 'totpStep3' => \wp_kses_post( WP2FA::get_wp2fa_white_label_setting( 'method_help_totp_step_3', true ) ) ?: \__( 'Click the "I\'m ready" button below when you complete the application setup process to proceed with the wizard.', 'wp-2fa' ),
542 'totpVerifyIntro' => \wp_kses_post( WP2FA::get_wp2fa_white_label_setting( 'method_verification_totp_pre', true ) ) ?: \__( 'Please type in the one-time code from your authenticator app to finalize setup.', 'wp-2fa' ),
543 'totpSuccess' => \__( 'TOTP configured successfully!', 'wp-2fa' ),
544 'copyKey' => \__( 'Copy key', 'wp-2fa' ),
545
546 // Email.
547 'settingUpEmail' => \__( 'Setting up HOTP (one-time code via email)', 'wp-2fa' ),
548 'emailSetupIntro' => \wp_kses_post( WP2FA::get_wp2fa_white_label_setting( 'method_help_hotp_intro', true ) ) ?: \__( 'To complete the 2FA configuration you will be sent a one-time code over email, therefore you should have access to the mailbox of this email address. If you do not receive the email with the one-time code please check your spam folder and contact your administrator', 'wp-2fa' ),
549 'useMyEmail' => \__( 'Use my user email', 'wp-2fa' ),
550 'useAnotherEmail' => \__( 'Use another email address', 'wp-2fa' ),
551 'emailPlaceholder' => \__( 'email@example.com', 'wp-2fa' ),
552 'emailHelpText' => \wp_kses_post( WP2FA::get_wp2fa_white_label_setting( 'method_help_hotp_help', true ) ),
553 'emailWhitelistNotice' => \wp_kses_post( WP2FA::get_wp2fa_white_label_setting( 'method_help_hotp_help_email', true ) ),
554 'emailSuccess' => \__( 'Email 2FA configured successfully!', 'wp-2fa' ),
555 'emailSendError' => \__( 'Failed to send setup email.', 'wp-2fa' ),
556 'codeSent' => \__( 'A new code has been sent.', 'wp-2fa' ),
557 'resendCode' => \__( 'Send me another code', 'wp-2fa' ),
558
559 // Backup codes.
560 'backupCodesTitle' => '', //\__( 'Backup Codes', 'wp-2fa' ),
561 'backupCodesIntro' => Backup_Codes::user_needs_to_setup_backup_codes( \wp_get_current_user() )
562 ? \wp_kses_post( WP2FA::get_wp2fa_white_label_setting( 'backup_codes_intro', true ) )
563 : \wp_kses_post( WP2FA::get_wp2fa_white_label_setting( 'backup_codes_intro_continue', true ) ),
564 'generateBackupCodes' => \__( 'Generate list of backup codes', 'wp-2fa' ),
565 'backupCodesError' => \__( 'Failed to generate backup codes.', 'wp-2fa' ),
566 'yourBackupCodes' => '', // \__( 'Your backup codes', 'wp-2fa' ),
567 'backupCodesGenerated' => \wp_kses_post( WP2FA::get_wp2fa_white_label_setting( 'backup_codes_generated', true ) ) ?: \esc_html__( 'Store these codes somewhere safe. Each code can only be used once.', 'wp-2fa' ),
568 'copyBackupCodes' => \__( 'Copy', 'wp-2fa' ),
569 'copied' => \__( 'Copied!', 'wp-2fa' ),
570 'downloadBackupCodes' => \__( 'Download', 'wp-2fa' ),
571 'printBackupCodes' => \__( 'Print', 'wp-2fa' ),
572 'imReadyClose' => \__( 'I\'m ready, close the wizard', 'wp-2fa' ),
573 'skipBackupMethod' => \__( 'I\'ll set this up later', 'wp-2fa' ),
574
575 // Backup method selection step (shown when >1 backup method is available).
576 'backupSelectTitle' => '', // WP2FA::get_wp2fa_white_label_setting( 'backup_codes_intro_multi_title', true ) ?: \__( 'Your login just got more secure', 'wp-2fa' ),
577 'backupSelectDescription' => \wp_kses_post( WP2FA::get_wp2fa_white_label_setting( 'backup_codes_intro_multi', true ) ) ?: \__( 'Congratulations! You have enabled two-factor authentication for your user. You\'ve just helped towards making this website more secure!', 'wp-2fa' ),
578 'backupSelectSectionTitle' => \__( 'Select backup method', 'wp-2fa' ),
579 'closeLater' => \__( 'Close wizard', 'wp-2fa' ),
580 'configureBackup' => \__( 'Continue', 'wp-2fa' ),
581
582 // Completion step (no backup methods available).
583 'noFurtherAction' => \wp_kses_post( WP2FA::get_wp2fa_white_label_setting( 'no_further_action', true ) ),
584 'closeWizard' => \__( 'Close wizard', 'wp-2fa' ),
585
586 // Close confirmation dialog.
587 'wizardCancelBody' => \wp_kses_post( WP2FA::get_wp2fa_white_label_setting( 'wp-2fa_wizard_cancel', true ) ) ?: ( '<h3>' . \esc_html__( 'Are you sure?', 'wp-2fa' ) . '</h3><p>' . \esc_html__( 'Any unsaved changes will be lost!', 'wp-2fa' ) . '</p>' ),
588 'confirmYes' => \__( 'Yes', 'wp-2fa' ),
589 'confirmNo' => \__( 'No', 'wp-2fa' ),
590
591 // Reconfiguration placeholder replacements.
592 'reconfigureCapitalized' => \__( 'Reconfigure', 'wp-2fa' ),
593 'configureCapitalized' => \__( 'Configure', 'wp-2fa' ),
594 'reconfigureLower' => \__( 'reconfigure', 'wp-2fa' ),
595 'configureLower' => \__( 'configure', 'wp-2fa' ),
596 );
597
598 /**
599 * Filter: wp_2fa_wizard_i18n_strings
600 *
601 * Allows extensions to add their own translatable strings.
602 *
603 * @param array $strings Associative array of key => translated string.
604 *
605 * @since 4.0.0
606 */
607 return \apply_filters( WP_2FA_PREFIX . 'wizard_i18n_strings', $strings );
608 }
609
610 /**
611 * Determine if the wizard should auto-open for the given user.
612 *
613 * @param \WP_User $user The target user.
614 *
615 * @return bool
616 *
617 * @since 4.0.0
618 */
619 private static function should_auto_open_wizard( \WP_User $user ): bool {
620 // Admin profile page with show=wp-2fa-setup parameter.
621 if ( isset( $_GET['show'] ) && 'wp-2fa-setup' === $_GET['show'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
622 return true;
623 }
624
625 // User is enforced instantly (grace period expired, must configure now).
626 if ( User_Helper::get_user_enforced_instantly( $user ) ) {
627 return true;
628 }
629
630 return false;
631 }
632
633 /**
634 * AJAX handler: mark the required intro as seen for the current user.
635 *
636 * @return void
637 *
638 * @since 4.0.0
639 */
640 public static function ajax_dismiss_required_intro(): void {
641 \check_ajax_referer( 'wp-2fa-dismiss-required-intro', 'nonce' );
642
643 $user = \wp_get_current_user();
644 if ( ! $user || ! $user->ID ) {
645 \wp_send_json_error( 'not_logged_in', 403 );
646 }
647
648 \update_user_meta( $user->ID, WP_2FA_PREFIX . 'required_intro_seen', 1 );
649 \wp_send_json_success();
650 }
651
652 /**
653 * Get the user being viewed/edited.
654 *
655 * @return \WP_User
656 */
657 private static function get_target_user(): \WP_User {
658 if ( isset( $_GET['user_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
659 $user_id = (int) $_GET['user_id']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
660 $user = \get_user_by( 'id', $user_id );
661 if ( $user ) {
662 return $user;
663 }
664 }
665
666 return \wp_get_current_user();
667 }
668
669 /**
670 * Print wizard underscore.js templates in the admin footer.
671 *
672 * Templates are rendered as <script type="text/html" id="tmpl-…"> blocks
673 * and consumed by JS via wp.template().
674 *
675 * To override a template, use the {@see 'wp_2fa_wizard_templates'} filter
676 * and replace the file path for any template key. Extensions can add their
677 * own templates via the {@see 'wp_2fa_wizard_print_templates'} action.
678 *
679 * @return void
680 *
681 * @since 4.0.0
682 */
683 public static function print_wizard_templates(): void {
684 if ( ! self::is_profile_page() ) {
685 return;
686 }
687
688 $template_dir = WP_2FA_PATH . 'templates' . DIRECTORY_SEPARATOR . 'wizard' . DIRECTORY_SEPARATOR;
689
690 $templates = array(
691 'modal' => $template_dir . 'modal.php',
692 'confirm-close' => $template_dir . 'confirm-close.php',
693 'required-intro' => $template_dir . 'required-intro.php',
694 'welcome' => $template_dir . 'welcome.php',
695 'method-selection' => $template_dir . 'method-selection.php',
696 'backup-method-footer' => $template_dir . 'backup-method-footer.php',
697 'backup-method-selection' => $template_dir . 'backup-method-selection.php',
698 'totp-setup' => $template_dir . 'totp-setup.php',
699 'totp-verify' => $template_dir . 'totp-verify.php',
700 'email-setup' => $template_dir . 'email-setup.php',
701 'email-verify' => $template_dir . 'email-verify.php',
702 'backup-codes-generate' => $template_dir . 'backup-codes-generate.php',
703 'backup-codes-display' => $template_dir . 'backup-codes-display.php',
704 'completion' => $template_dir . 'completion.php',
705 );
706
707 /**
708 * Filter: wp_2fa_wizard_templates
709 *
710 * Allows overriding wizard template file paths. Each key is a template
711 * name, each value is the absolute file path. Replace a path to use a
712 * custom template file instead of the bundled one.
713 *
714 * @param array<string,string> $templates Template name => absolute path.
715 *
716 * @since 4.0.0
717 */
718 $templates = \apply_filters( WP_2FA_PREFIX . 'wizard_templates', $templates );
719
720 foreach ( $templates as $name => $path ) {
721 if ( \file_exists( $path ) ) {
722 include $path;
723 }
724 }
725
726 /**
727 * Action: wp_2fa_wizard_print_templates
728 *
729 * Allows extensions to print their own wizard underscore templates.
730 * Extensions should include their template PHP files inside this hook.
731 *
732 * @since 4.0.0
733 */
734 \do_action( WP_2FA_PREFIX . 'wizard_print_templates' );
735
736 self::print_wizard_styling_script();
737 }
738
739 /**
740 * Render the full profile 2FA section using the new template-based renderer.
741 *
742 * Replaces the old render_wizard_button method. This renders
743 * the complete profile section with summary, cards, and actions.
744 *
745 * @param \WP_User $user The user whose profile is being displayed.
746 *
747 * @return void
748 */
749 public static function render_profile_section( $user ): void {
750 $role = User_Helper::get_user_role( $user );
751 $enabled_methods = Methods::get_enabled_methods( $role );
752
753 // Only show the section if there are enabled methods for this role.
754 if ( empty( $enabled_methods ) || ( isset( $enabled_methods[ $role ] ) && empty( $enabled_methods[ $role ] ) ) ) {
755 return;
756 }
757
758 Profile_Section_Renderer::render( $user );
759 }
760
761 /**
762 * Render the profile section for shortcode / frontend context.
763 *
764 * Called from the shortcode handler or custom pages.
765 *
766 * @param \WP_User $user The target user.
767 * @param array $additional_args Extra arguments (show_preamble, options, etc.).
768 *
769 * @return void
770 */
771 public static function render_profile_section_frontend( \WP_User $user, array $additional_args = array() ): void {
772 // Enqueue frontend-specific assets.
773 Profile_Section_Renderer::enqueue_assets( true );
774
775 // Enqueue the wizard assets for the frontend context.
776 self::enqueue_frontend_wizard_assets();
777
778 Profile_Section_Renderer::render( $user, $additional_args );
779 }
780
781 /**
782 * Enqueue wizard JS/CSS and print templates for the frontend context.
783 *
784 * This mirrors what enqueue_assets() does for the admin profile page,
785 * adapted for frontend shortcode / WooCommerce pages.
786 *
787 * @return void
788 *
789 * @since 4.0.0
790 */
791 public static function enqueue_frontend_wizard_assets(): void {
792 static $enqueued = false;
793 if ( $enqueued ) {
794 return;
795 }
796 $enqueued = true;
797
798 $plugin_url = WP_2FA_URL;
799 $version = WP_2FA_VERSION;
800
801 \wp_enqueue_style(
802 'wp2fa-setup-wizard',
803 $plugin_url . 'css/wp2fa-wizard.css',
804 array(),
805 $version
806 );
807
808 // Core wizard JS (depends on wp-hooks and wp-util for wp.template).
809 \wp_enqueue_script(
810 self::SCRIPT_HANDLE,
811 $plugin_url . 'js/wp2fa-wizard.js',
812 array( 'wp-hooks', 'wp-util' ),
813 $version,
814 true
815 );
816
817 // Core method modules.
818 \wp_enqueue_script(
819 'wp2fa-wizard-totp',
820 $plugin_url . 'js/wp2fa-wizard-totp.js',
821 array( self::SCRIPT_HANDLE, 'wp-hooks' ),
822 $version,
823 true
824 );
825
826 \wp_enqueue_script(
827 'wp2fa-wizard-email',
828 $plugin_url . 'js/wp2fa-wizard-email.js',
829 array( self::SCRIPT_HANDLE, 'wp-hooks' ),
830 $version,
831 true
832 );
833
834 // Backup codes module.
835 \wp_enqueue_script(
836 'wp2fa-wizard-backup-codes',
837 $plugin_url . 'js/wp2fa-wizard-backup-codes.js',
838 array( self::SCRIPT_HANDLE, 'wp-hooks' ),
839 $version,
840 true
841 );
842
843 /**
844 * Action: wp_2fa_wizard_enqueue_assets
845 *
846 * Allows premium / extension methods to enqueue their own wizard
847 * JS and CSS files conditionally.
848 *
849 * @param string $plugin_url The plugin root URL (with trailing slash).
850 * @param string $version The plugin version string.
851 * @param string $core_handle The script handle of the wizard core JS.
852 *
853 * @since 4.0.0
854 */
855 \do_action( WP_2FA_PREFIX . 'wizard_enqueue_assets', $plugin_url, $version, self::SCRIPT_HANDLE );
856
857 // Localized data – attached to the core handle.
858 \wp_localize_script( self::SCRIPT_HANDLE, 'wp2faWizardData', self::get_localized_data() );
859
860 // Print wizard templates in the footer.
861 \add_action( 'wp_footer', array( __CLASS__, 'print_wizard_templates_frontend' ) );
862 }
863
864 /**
865 * Print wizard underscore.js templates in the frontend footer.
866 *
867 * @return void
868 *
869 * @since 4.0.0
870 */
871 public static function print_wizard_templates_frontend(): void {
872 $template_dir = WP_2FA_PATH . 'templates' . DIRECTORY_SEPARATOR . 'wizard' . DIRECTORY_SEPARATOR;
873
874 $templates = array(
875 'modal' => $template_dir . 'modal.php',
876 'confirm-close' => $template_dir . 'confirm-close.php',
877 'required-intro' => $template_dir . 'required-intro.php',
878 'welcome' => $template_dir . 'welcome.php',
879 'method-selection' => $template_dir . 'method-selection.php',
880 'backup-method-footer' => $template_dir . 'backup-method-footer.php',
881 'backup-method-selection' => $template_dir . 'backup-method-selection.php',
882 'totp-setup' => $template_dir . 'totp-setup.php',
883 'totp-verify' => $template_dir . 'totp-verify.php',
884 'email-setup' => $template_dir . 'email-setup.php',
885 'email-verify' => $template_dir . 'email-verify.php',
886 'backup-codes-generate' => $template_dir . 'backup-codes-generate.php',
887 'backup-codes-display' => $template_dir . 'backup-codes-display.php',
888 'completion' => $template_dir . 'completion.php',
889 );
890
891 /** This filter is documented in self::print_wizard_templates() */
892 $templates = \apply_filters( WP_2FA_PREFIX . 'wizard_templates', $templates );
893
894 foreach ( $templates as $name => $path ) {
895 if ( \file_exists( $path ) ) {
896 include $path;
897 }
898 }
899
900 /** This action is documented in self::print_wizard_templates() */
901 \do_action( WP_2FA_PREFIX . 'wizard_print_templates' );
902
903 self::print_wizard_styling_script();
904 }
905
906 /**
907 * Output inline script and custom CSS for backward-compatible wizard styling.
908 *
909 * Adds the enable_styling/default_styling class to the wizard modal via the
910 * wp2fa_wizard_modal_classes JS filter, and outputs any user-defined custom CSS.
911 *
912 * @return void
913 *
914 * @since 4.0.0
915 */
916 private static function print_wizard_styling_script(): void {
917 // When enable_wizard_styling is checked, use the plugin's built-in styling (no custom CSS).
918 // When unchecked (empty), apply the custom CSS from white-label settings.
919 // if ( ! empty( WP2FA::get_wp2fa_white_label_setting( 'enable_wizard_styling' ) ) ) {
920 // return;
921 // }
922
923 // Always output custom CSS if present — it applies regardless of the
924 // enable_wizard_styling toggle (matching legacy v3 behaviour where
925 // custom_wizard_css fired unconditionally via the methods_wizards action).
926 $custom_css = WP2FA::get_wp2fa_white_label_setting( 'custom_css', true );
927 if ( ! empty( $custom_css ) ) {
928 $modal_css = <<<'CSS'
929 .wp2fa-modal input[type=radio]:checked:before {
930 display:none;
931 }
932 .wp2fa-modal.enable_styling h4,.wp2fa-modal.enable_styling h3 {
933 font-family:helvetica !important;
934 }
935 .wp2fa-modal fieldset {
936 padding:0;
937 border:0;
938 }
939 .wp2fa-modal ol {
940 margin:0 0 15px;
941 padding-inline-start:17px;
942 }
943 .wp2fa-modal ol li {
944 position:relative;
945 padding:6px;
946 }
947 .wp2fa-modal .modal__content code.app-key {
948 font-size:16px;
949 padding:7px 12px;
950 word-break:break-all;
951 background:#efefef;
952 color:#222;
953 }
954 .wp2fa-modal.enable_styling .modal__content .wp2fa-setup-actions .button:focus,
955 .wp2fa-modal.enable_styling .modal__content .modal__btn:focus,
956 .wp2fa-modal.enable_styling .modal__content .modal__btn.button-confirm:focus,
957 .wp2fa-modal.enable_styling .modal__content .modal__btn.button-decline:focus {
958 box-shadow:none;
959 }
960 .wp2fa-modal.enable_styling .button+.button {
961 margin-inline-start:10px;
962 }
963 @media screen and (max-width:1299px) {
964 .wp2fa-modal .modal__container {
965 max-width:96vw;
966 }
967 .wp2fa-modal .modal__content code br {
968 display:block !important;
969 }
970 .wp2fa-modal .modal__content code.app-key {
971 width:100%;
972 display:block;
973 text-align:center;
974 font-size:16px;
975 margin-bottom:30px;
976 background:#efefef;
977 padding:5px;
978 word-break:break-all;
979 }
980 .wp2fa-modal .modal__content .apps-wrapper {
981 margin:10px 0;
982 }
983 .wp2fa-modal .modal__content a.app-logo {
984 padding:0 5px;
985 }
986 .wp2fa-modal .modal__content .wizard-tooltip {
987 background:#3e6bff;
988 color:white;
989 height:18px;
990 display:inline-block;
991 width:18px;
992 border-radius:50%;
993 position:absolute;
994 overflow:hidden;
995 text-align:center;
996 line-height:18px;
997 font-weight:700;
998 margin-inline-start:4px;
999 margin-top:3px;
1000 bottom:10px;
1001 inset-inline-end:17px;
1002 }
1003 .wp2fa-modal .modal__content .inline-helper {
1004 background:#eee;
1005 padding:10px;
1006 border-radius:10px;
1007 display:block;
1008 margin-top:10px;
1009 font-size:12px;
1010 line-height:24px;
1011 display:none;
1012 }
1013 .wp2fa-modal .modal__content .click-to-copy {
1014 border:2px solid #3e6bff;
1015 border-radius:14px;
1016 display:inline-block;
1017 font-size:10px;
1018 padding:0 9px;
1019 font-weight:700;
1020 line-height:16px;
1021 }
1022 .wp2fa-modal .modal__content .click-to-copy.done {
1023 background-color:green !important;
1024 border:2px solid green;
1025 color:white;
1026 }
1027 .wp2fa-modal .modal__content .click-to-copy:hover {
1028 background-color:#3e6bff;
1029 color:white;
1030 cursor:pointer;
1031 opacity:0.5;
1032 }
1033 .wp2fa-modal .modal__content .app-key-wrapper {
1034 background:#eee;
1035 border-radius:4px;
1036 margin:10px 0 5px;
1037 overflow:hidden;
1038 padding:3px 7px;
1039 }
1040 .wp2fa-modal .modal__content .app-key-wrapper input {
1041 background:transparent;
1042 border:none;
1043 display:inline-block;
1044 font-size:9pt;
1045 margin:0 10px 0 0;
1046 max-width:15pc;
1047 min-height:0;
1048 min-width:200px;
1049 padding:0;
1050 color:#888;
1051 }
1052 .wp2fa-modal .modal__content .app-key-wrapper input:focus-visible,
1053 .wp2fa-modal .modal__content .app-key-wrapper input:focus {
1054 border:none;
1055 background-color:transparent;
1056 outline:none;
1057 }
1058 .wp2fa-modal .modal__content .wp2fa-setup-actions .button,
1059 .wp2fa-modal .modal__content .modal__btn {
1060 width:auto;
1061 display:inline-block;
1062 margin-bottom:10px;
1063 margin-inline-start:0px;
1064 margin-inline-end:0px;
1065 text-align:center;
1066 }
1067 .wp2fa-modal h4,.wp2fa-modal h3 {
1068 font-size:20px;
1069 }
1070 .wp2fa-modal h4 {
1071 clear:both;
1072 }
1073 }
1074 @media screen and (min-width:1300px) {
1075 .wp2fa-modal .clear-both {
1076 clear:both;
1077 display:block;
1078 overflow:hidden;
1079 }
1080 .wp2fa-modal .modal-50 {
1081 width:50%;
1082 display:inline-block;
1083 float:left;
1084 }
1085 .wp2fa-modal .modal-60 {
1086 width:70%;
1087 display:inline-block;
1088 float:left;
1089 }
1090 .wp2fa-modal .modal-60 .radio-cells {
1091 width:100%;
1092 padding-inline-start:10px;
1093 }
1094 .wp2fa-modal .modal-40 {
1095 width:30%;
1096 float:left;
1097 display:flex;
1098 justify-content:center;
1099 }
1100 .wp2fa-modal .mb-20 {
1101 margin-bottom:20px;
1102 }
1103 .wp2fa-modal.enable_styling p+.radio-cells,
1104 .wp2fa-modal.enable_styling p+fieldset {
1105 margin:20px 0;
1106 display:flex;
1107 flex-wrap:wrap;
1108 }
1109 .wp2fa-modal.enable_styling .radio-cells {
1110 flex-wrap:wrap;
1111 margin:0px 0 20px;
1112 width:calc(100% + 10px);
1113 position:relative;
1114 inset-inline-start:-5px;
1115 }
1116 .wp2fa-modal.enable_styling .radio-cells input:not([type="radio"]) {
1117 margin-top:5px;
1118 }
1119 .wp2fa-modal.enable_styling .radio-cells .option-pill {
1120 flex-basis:0;
1121 flex-grow:1;
1122 border:3px solid #eee;
1123 border-radius:10px;
1124 padding:10px;
1125 margin:5px;
1126 font-size:11px;
1127 position:relative;
1128 }
1129 .wp2fa-modal.enable_styling .radio-cells .option-pill p {
1130 height:100%;
1131 }
1132 .wp2fa-modal.enable_styling .radio-cells.max-3 .option-pill {
1133 flex:33.333%;
1134 display:flex;
1135 flex-direction:column;
1136 }
1137 .wp2fa-modal.enable_styling .radio-cells.max-3 .option-pill p {
1138 flex:1;
1139 }
1140 .wp2fa-modal.enable_styling .radio-cells .option-pill.isSelected {
1141 border:3px solid #007cba;
1142 }
1143 .wp2fa-modal.enable_styling .radio-cells .option-pill label {
1144 display:block;
1145 font-size:13px;
1146 line-height:24px;
1147 font-weight:500;
1148 margin-bottom:2px;
1149 height:100%;
1150 max-width:calc(100% - 20px);
1151 }
1152 .wp2fa-modal.enable_styling .radio-cells .option-pill p {
1153 font-size:12px;
1154 line-height:23px;
1155 opacity:0.9;
1156 height:auto;
1157 }
1158 .wp2fa-modal .wizard-tooltip {
1159 background:#3e6bff;
1160 color:white;
1161 height:18px;
1162 display:inline-block;
1163 width:18px;
1164 border-radius:50%;
1165 position:absolute;
1166 overflow:hidden;
1167 text-align:center;
1168 line-height:18px;
1169 font-weight:700;
1170 margin-inline-start:4px;
1171 margin-top:3px;
1172 bottom:10px;
1173 inset-inline-end:17px;
1174 }
1175 .wp2fa-modal .inline-helper {
1176 background:#eee;
1177 padding:10px;
1178 border-radius:10px;
1179 display:block;
1180 margin-top:10px;
1181 font-size:12px;
1182 line-height:24px;
1183 display:none;
1184 }
1185 .wp2fa-modal.enable_styling .tooltip-content-wrapper {
1186 display:none;
1187 }
1188 .wp2fa-modal .click-to-copy {
1189 border:2px solid #3e6bff;
1190 border-radius:14px;
1191 display:inline-block;
1192 font-size:10px;
1193 padding:0 9px;
1194 font-weight:700;
1195 line-height:16px;
1196 }
1197 .wp2fa-modal .click-to-copy.done {
1198 background-color:green !important;
1199 border:2px solid green;
1200 color:white;
1201 }
1202 .wp2fa-modal .click-to-copy:hover {
1203 background-color:#3e6bff;
1204 color:white;
1205 cursor:pointer;
1206 opacity:0.5;
1207 }
1208 .wp2fa-modal .app-key-wrapper {
1209 background:#eee;
1210 border-radius:4px;
1211 margin:10px 0 5px;
1212 overflow:hidden;
1213 padding:3px 7px;
1214 }
1215 .wp2fa-modal .app-key-wrapper input {
1216 background:transparent;
1217 border:none;
1218 display:inline-block;
1219 font-size:9pt;
1220 margin:0 10px 0 0;
1221 max-width:15pc;
1222 min-height:0;
1223 min-width:200px;
1224 padding:0;
1225 color:#888;
1226 }
1227 .wp2fa-modal .app-key-wrapper input:focus-visible,
1228 .wp2fa-modal .app-key-wrapper input:focus {
1229 border:none;
1230 background-color:transparent;
1231 outline:none;
1232 }
1233 .wp2fa-modal .option-pill {
1234 margin-bottom:15px;
1235 }
1236 .wp2fa-modal .qr-code {
1237 float:left;
1238 width:100%;
1239 position:relative;
1240 inset-inline-start:-2%;
1241 }
1242 .wp2fa-modal .mb-30 {
1243 margin-bottom:30px;
1244 }
1245 }
1246 @media (max-width:500px) {
1247 .wp2fa-modal .modal__content .apps-wrapper {
1248 display:block;
1249 }
1250 .wp2fa-modal .modal__content a.app-logo {
1251 width:calc(33.33333% - 13px);
1252 display:inline-block;
1253 margin-bottom:5px;
1254 }
1255 .wp2fa-modal h4,.wp2fa-modal h3 {
1256 font-size:18px;
1257 margin-bottom:9px;
1258 }
1259 .wp2fa-modal .modal__content .modal__btn,
1260 .wp2fa-modal .modal__content .wp2fa-setup-actions .button {
1261 display:block;
1262 margin-bottom:10px;
1263 margin-inline-start:0;
1264 margin-inline-end:0;
1265 text-align:center;
1266 width:auto;
1267 padding:10px 15px;
1268 font-size:12px;
1269 }
1270 .wp2fa-modal .modal__content .radio-cells .option-pill {
1271 flex-basis:unset;
1272 flex-grow:1;
1273 }
1274 .wp2fa-modal .modal__content p+.radio-cells {
1275 margin-top:20px;
1276 }
1277 }
1278 @media screen and (min-width:801px) and (max-width:1299px) {
1279 .wp2fa-modal .modal__content p+.radio-cells {
1280 margin-top:20px;
1281 }
1282 }
1283 @media screen and (min-width:1024px) {
1284 .wp2fa-modal .modal__container {
1285 min-width:768px;
1286 }
1287 }
1288 @media screen and (max-width:800px) {
1289 .wp2fa-modal .modal__container {
1290 max-width:95vw;
1291 min-width:80vw;
1292 }
1293 .wp2fa-modal .modal__content .wp2fa-setup-actions .button,
1294 .wp2fa-modal .modal__content .modal__btn {
1295 width:auto;
1296 display:inline-block;
1297 margin-bottom:10px;
1298 margin-inline-start:0px;
1299 margin-inline-end:0px;
1300 text-align:center;
1301 }
1302 .wp2fa-modal .modal__close {
1303 inset-inline-end:15px;
1304 top:15px;
1305 }
1306 }
1307 @media screen and (max-width:500px) {
1308 .wp2fa-modal .modal__content .app-key-wrapper input {
1309 max-width:200px;
1310 }
1311 .wp2fa-modal .modal__content .wizard-tooltip {
1312 inset-inline-end:5px;
1313 bottom:5px;
1314 }
1315 }
1316 CSS;
1317 echo '<style id="wp2fa-custom-wizard-css">' . $modal_css . \wp_strip_all_tags( $custom_css ) . '</style>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1318 }
1319 ?>
1320 <script>
1321 ( function() {
1322 function wp2faRegisterStylingFilter() {
1323 if ( typeof wp !== 'undefined' && wp.hooks && typeof wp2faWizardData !== 'undefined' ) {
1324 wp.hooks.addFilter( 'wp2fa_wizard_modal_classes', 'wp2fa/white-label-styling', function( classes ) {
1325 var stylingClass = wp2faWizardData.stylingClass || 'default_styling';
1326 return ( classes ? classes + ' ' : '' ) + stylingClass + ' wp2fa-modal';
1327 } );
1328
1329 // When styling is disabled, swap our custom button/element classes
1330 // for WP-native ones so the theme's styles apply directly.
1331 if ( ( wp2faWizardData.stylingClass || 'default_styling' ) === 'default_styling' ) {
1332
1333 // Selectors covering wizard buttons and profile section buttons.
1334 var WIZARD_BTN_SEL = '.wp2fa-wizard-btn, .wp2fa-wizard-btn-primary, .wp2fa-wizard-btn-secondary, .wp-2fa-button-primary, .wp-2fa-button-secondary';
1335 var PROFILE_BTN_SEL = '.wp2fa-profile__btn';
1336
1337 function tagWizardButtons( root ) {
1338 root.querySelectorAll( WIZARD_BTN_SEL ).forEach( function( el ) {
1339 var isPrimary = el.classList.contains( 'wp2fa-wizard-btn-primary' ) || el.classList.contains( 'wp-2fa-button-primary' );
1340 el.classList.remove(
1341 'wp2fa-wizard-btn',
1342 'wp2fa-wizard-btn-primary',
1343 'wp2fa-wizard-btn-secondary',
1344 'wp2fa-wizard-btn-close-later',
1345 'wp-2fa-button-primary',
1346 'wp-2fa-button-secondary'
1347 );
1348 el.classList.add( 'button' );
1349 if ( isPrimary ) {
1350 el.classList.add( 'button-primary' );
1351 }
1352 } );
1353 }
1354
1355 function tagProfileButtons( root ) {
1356 root.querySelectorAll( PROFILE_BTN_SEL ).forEach( function( el ) {
1357 var isPrimary = el.classList.contains( 'wp2fa-profile__btn--primary' );
1358 var isDanger = el.classList.contains( 'wp2fa-profile__btn--danger' );
1359 el.classList.remove(
1360 'wp2fa-profile__btn',
1361 'wp2fa-profile__btn--primary',
1362 'wp2fa-profile__btn--secondary',
1363 'wp2fa-profile__btn--danger',
1364 'wp2fa-profile__btn--disabled'
1365 );
1366 el.classList.add( 'button' );
1367 if ( isPrimary || isDanger ) {
1368 el.classList.add( 'button-primary' );
1369 }
1370 } );
1371 }
1372
1373 // Tag profile buttons on the page immediately.
1374 function initProfileButtons() {
1375 tagProfileButtons( document );
1376 }
1377
1378 if ( document.readyState === 'complete' || document.readyState === 'interactive' ) {
1379 initProfileButtons();
1380 } else {
1381 document.addEventListener( 'DOMContentLoaded', initProfileButtons );
1382 }
1383
1384 // Tag wizard buttons each time a step is shown.
1385 wp.hooks.addAction( 'wp2fa_wizard_step_shown', 'wp2fa/default-button-classes', function() {
1386 var modal = document.getElementById( 'wp2fa-setup-wizard-modal' );
1387 if ( modal ) {
1388 tagWizardButtons( modal );
1389 }
1390 } );
1391
1392 // Observe for modal insertion and dynamic content inside it.
1393 var modalObserver = null;
1394 var bodyObserver = new MutationObserver( function( mutations ) {
1395 for ( var i = 0; i < mutations.length; i++ ) {
1396 for ( var j = 0; j < mutations[i].addedNodes.length; j++ ) {
1397 var node = mutations[i].addedNodes[j];
1398 if ( node.nodeType === 1 && ( node.id === 'wp2fa-setup-wizard-overlay' || ( node.querySelector && node.querySelector( '#wp2fa-setup-wizard-modal' ) ) ) ) {
1399 var modal = document.getElementById( 'wp2fa-setup-wizard-modal' ) || node.querySelector( '#wp2fa-setup-wizard-modal' ) || node;
1400 tagWizardButtons( modal );
1401 bodyObserver.disconnect();
1402
1403 // Watch inside the modal for dynamically added buttons.
1404 modalObserver = new MutationObserver( function() {
1405 tagWizardButtons( modal );
1406 } );
1407 modalObserver.observe( modal, { childList: true, subtree: true } );
1408 return;
1409 }
1410 }
1411 }
1412 } );
1413 bodyObserver.observe( document.body || document.documentElement, { childList: true, subtree: true } );
1414 }
1415 }
1416 }
1417 if ( document.readyState === 'complete' || document.readyState === 'interactive' ) {
1418 wp2faRegisterStylingFilter();
1419 } else {
1420 document.addEventListener( 'DOMContentLoaded', wp2faRegisterStylingFilter );
1421 }
1422 } )();
1423 </script>
1424 <?php
1425 }
1426 }
1427 }
1428
1429