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-user-profile.php
wp-2fa / includes / classes / Admin Last commit date
AdminSettings 18 hours ago Controllers 18 hours ago Fly-Out 18 hours ago Helpers 18 hours ago Methods 18 hours ago Settings 18 hours ago SettingsPages 18 hours ago Views 18 hours ago class-about-us.php 18 hours ago class-docs-and-support.php 18 hours ago class-free-support-notice.php 18 hours ago class-help-contact-us.php 18 hours ago class-license-page.php 18 hours ago class-new-interface-notice.php 18 hours ago class-plugin-updated-notice.php 18 hours ago class-premium-features.php 18 hours ago class-profile-section-renderer.php 18 hours ago class-settings-page.php 18 hours ago class-setup-wizard.php 18 hours ago class-top-bar-banner.php 18 hours ago class-user-listing.php 18 hours ago class-user-notices.php 18 hours ago class-user-profile.php 18 hours ago class-user-registered.php 18 hours ago class-wizard-integration.php 18 hours ago index.php 18 hours ago
class-user-profile.php
936 lines
1 <?php
2 /**
3 * Responsible for WP2FA user's profile settings.
4 *
5 * @package wp2fa
6 * @subpackage user-utils
7 * @copyright 2026 Melapress
8 * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
9 * @link https://wordpress.org/plugins/wp-2fa/
10 */
11
12 namespace WP2FA\Admin;
13
14 use WP2FA\WP2FA;
15 use WP2FA\Methods\TOTP;
16 use WP2FA\Methods\Email;
17 use WP2FA\Utils\User_Utils;
18 use WP2FA\Extensions_Loader;
19 use WP2FA\Methods\Backup_Codes;
20 use WP2FA\Utils\Generate_Modal;
21 use WP2FA\Utils\Settings_Utils;
22 use WP2FA\Authenticator\Open_SSL;
23 use WP2FA\Admin\Helpers\WP_Helper;
24 use WP2FA\Freemius\User_Licensing;
25 use WP2FA\Admin\Views\Wizard_Steps;
26 use WP2FA\Admin\Controllers\Methods;
27 use WP2FA\Admin\Helpers\User_Helper;
28 use WP2FA\Admin\Controllers\Settings;
29 use WP2FA\Authenticator\Authentication;
30 use WP2FA\Extensions\OutOfBand\Out_Of_Band;
31 use WP2FA\Extensions\Zero_Setup_Email\Zero_Setup_Email;
32
33 /**
34 * User_Profile class responsible for the profile page operations
35 *
36 * @since 2.4.0
37 */
38 if ( ! class_exists( '\WP2FA\Admin\User_Profile' ) ) {
39 /**
40 * User_Profile - Class for handling user things such as profile settings and admin list views.
41 *
42 * @since 2.7.0
43 */
44 class User_Profile {
45
46 /**
47 * Add our buttons to the user profile editing screen.
48 *
49 * @param object $user User data.
50 * @param array $additional_args - Array with extra parameters for the method.
51 *
52 * @since 2.7.0
53 */
54 public static function user_2fa_options( $user, $additional_args = array() ) {
55
56 \wp_enqueue_script(
57 'jquery-ui-dialog'
58 );
59
60 \wp_enqueue_style( 'wp-jquery-ui-dialog' );
61
62 if ( isset( $_GET['user_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
63 $user_id = (int) $_GET['user_id']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
64 if ( ! \current_user_can( 'edit_user', $user_id ) ) {
65 return;
66 }
67 $user = \get_user_by( 'id', $user_id );
68 } else {
69 // Get current user, we're going to need this regardless.
70 $user = \wp_get_current_user();
71 }
72
73 if ( ! is_a( $user, '\WP_User' ) ) {
74 return;
75 }
76
77 // Ensure we have something in the settings — but only bail if the plugin
78 // was never configured (no settings hash). If settings got wiped after
79 // initial configuration, the init() fallback restores them.
80 if ( empty( Settings_Utils::get_option( WP_2FA_POLICY_SETTINGS_NAME ) ) && ! Settings_Utils::get_option( WP_2FA_PREFIX . 'settings_hash' ) ) {
81 return;
82 }
83
84 $show_preamble = true;
85 if ( isset( $additional_args['show_preamble'] ) ) {
86 $show_preamble = \filter_var( $additional_args['show_preamble'], FILTER_VALIDATE_BOOLEAN );
87 }
88
89 $user_type = User_Utils::determine_user_2fa_status( $user );
90
91 $form_output = '<style> .disabled{ opacity: 0.5; pointer-events: none;}</style>';
92 $form_content = '';
93 $description = WP2FA::get_wp2fa_white_label_setting( 'user-profile-form-preamble-desc', true );
94 $show_form_table = true;
95 $page_url = ( WP_Helper::is_multisite() ) ? 'index.php' : 'options-general.php';
96
97 // Orphan user (a user with no role or capabilities).
98 if ( in_array( 'orphan_user', $user_type, true ) ) {
99 // We want to use the same form/buttons used in the shortcode.
100 $additional_args['is_shortcode'] = true;
101
102 // Create useful message for admin.
103 if ( User_Utils::in_array_all( array( 'user_needs_to_setup_2fa', 'can_manage_options' ), $user_type ) ) {
104 $description = \esc_html__( 'This user is required to setup 2FA but has not yet done so.', 'wp-2fa' );
105 }
106
107 if ( User_Utils::in_array_all( array( 'user_is_excluded', 'can_manage_options' ), $user_type ) ) {
108 $description = \esc_html__( 'This user is excluded from configuring 2FA.', 'wp-2fa' );
109 }
110 }
111
112 // Excluded user.
113 if ( in_array( 'user_is_excluded', $user_type, true ) ) {
114 /**
115 * Gives the ability to add more content to the profile page.
116 *
117 * @param string $form_content - The parsed HTML of the form.
118 * @param \WP_USER $user - The user object.
119 *
120 * @since 3.0.0
121 */
122 $form_content = \wp_kses_post( \apply_filters( WP_2FA_PREFIX . 'append_to_profile_form_content', $form_content, $user ) );
123
124 if ( ! empty( $form_content ) ) {
125 $form_output .= '<h2>' . WP2FA::get_wp2fa_white_label_setting( 'user-profile-form-preamble-title', true ) . '</h2>';
126
127 if ( $description ) {
128 $form_output .= '<p class="description">' . $description . '</p>';
129 }
130
131 $form_output .= '
132 <table id="wp-2fa-user-global-configuration" class="form-table wp-2fa-user-profile-form" role="presentation">
133 <tbody>
134 <tr>
135 <th><label>' . \esc_html__( '2FA Setup:', 'wp-2fa' ) . '</label></th>
136 <td>
137 ' . $form_content . '
138 </td>
139 </tr>
140 </tbody>
141 </table>';
142
143 echo $form_output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
144 }
145
146 return;
147 }
148
149 // A user viewing their own profile AND has a 2FA method configured.
150 if ( User_Utils::in_array_all( array( 'viewing_own_profile' ), $user_type ) ) {
151 if (
152 User_Utils::in_array_all( array( 'has_enabled_methods' ), $user_type ) ||
153 User_Utils::in_array_all( array( 'no_required_has_enabled' ), $user_type )
154 ) {
155
156 if ( isset( $additional_args['is_shortcode'] ) && $additional_args['is_shortcode'] ) {
157 $form_content = '';
158
159 /**
160 * Gives the ability to remove the user's settings.
161 *
162 * @param bool - The status of the settings.
163 *
164 * @since 2.2.2
165 * @since 2.9.2 - Added user ID parameter.
166 */
167 $show_enable2fa = \apply_filters( WP_2FA_PREFIX . 'enable_2fa_user_setting', true, $user->ID );
168
169 /**
170 * Gives the ability to change the user profile description message.
171 *
172 * @param bool - The status of the settings.
173 *
174 * @since 2.4.0
175 */
176 $description = \apply_filters( WP_2FA_PREFIX . 'enable_2fa_user_setting_description', $description );
177
178 // $disabled_class = Email::is_enforced() ? 'disabled' : '';
179
180 $styling_class = ( empty( WP2FA::get_wp2fa_white_label_setting( 'enable_wizard_styling' ) ) ) ? 'default_styling' : 'enable_styling';
181
182 if ( $show_enable2fa ) {
183 $form_content = '<a href="#" class="button button-primary remove-2fa ' . \esc_attr( $styling_class ) . '" data-open-configure-2fa-wizard>' . \esc_html__( 'Change 2FA settings', 'wp-2fa' ) . '</a>';
184 }
185
186 if ( self::can_user_remove_2fa( $user->ID ) ) {
187 $form_content .= '<a href="#" class="button button-primary remove-2fa ' . \esc_attr( $styling_class ) . '" onclick="MicroModal.show(\'confirm-remove-2fa\');">' . \esc_html__( 'Remove 2FA', 'wp-2fa' ) . '</a>';
188 }
189
190 $form_content .= '</td><tr><th class="backup-methods-label">';
191 $backup_codes_desc = '';
192 if ( Backup_Codes::are_backup_codes_enabled_for_role( User_Helper::get_user_role( $user ) ) ) {
193 $codes_remaining = Backup_Codes::codes_remaining_for_user( $user );
194 if ( $codes_remaining > 0 ) {
195 $backup_codes_desc = '<span class="description mt-5px">' . \esc_attr( (int) $codes_remaining ) . ' ' . \esc_html__( 'unused backup codes remaining.', 'wp-2fa' ) . '</span>';
196 } elseif ( 0 === $codes_remaining ) {
197 $backup_codes_desc = WP2FA::get_wp2fa_white_label_setting( 'backup_codes_learn_more', true );
198 }
199
200 if ( ! empty( $backup_codes_desc ) ) {
201 $backup_codes_desc = Wizard_Steps::get_backup_codes_link() . $backup_codes_desc;
202 }
203 }
204
205 /**
206 * Add an option for external providers to add their own user form buttons.
207 *
208 * @since 2.0.0
209 */
210 $backup_codes_desc = apply_filters( WP_2FA_PREFIX . 'additional_form_buttons', $backup_codes_desc );
211
212 if ( ! empty( $backup_codes_desc ) ) {
213 $form_content .= Wizard_Steps::get_generate_codes_label() . $backup_codes_desc;
214 }
215
216 $form_content .= '</th></tr>';
217 }
218 }
219
220 $show_if_user_is_not_in = array(
221 'user_is_excluded',
222 'has_enabled_methods',
223 'no_required_has_enabled',
224 );
225
226 // User viewing own profile and needs to enable 2FA.
227 if (
228 User_Utils::in_array_all( array( 'user_needs_to_setup_2fa' ), $user_type ) ||
229 User_Utils::role_is_not( $show_if_user_is_not_in, $user_type )
230 ) {
231
232 $first_time_setup_url = Settings::get_setup_page_link();
233
234 /**
235 * Gives the ability to remove the user's settings.
236 *
237 * @param bool - The status of the settings.
238 *
239 * @since 2.2.2
240 * @since 2.9.2 - Added user ID parameter.
241 */
242 $show_enable2fa = \apply_filters( WP_2FA_PREFIX . 'enable_2fa_user_setting', true, $user->ID );
243
244
245 /**
246 * Gives the ability to change the user profile description message.
247 *
248 * @param bool - The status of the settings.
249 *
250 * @since 2.4.0
251 */
252 $description = \apply_filters( WP_2FA_PREFIX . 'enable_2fa_user_setting_description', $description );
253
254 // $disabled_class = Email::is_enforced() ? 'disabled' : '';
255
256 $styling_class = ( empty( WP2FA::get_wp2fa_white_label_setting( 'enable_wizard_styling' ) ) ) ? 'default_styling' : 'enable_styling';
257
258 if ( $show_enable2fa ) {
259
260 if ( isset( $additional_args['is_shortcode'] ) && $additional_args['is_shortcode'] ) {
261 $form_content .= '<a href="#" class="button button-primary ' . \esc_attr( $styling_class ) . '" data-open-configure-2fa-wizard>' . \esc_html__( 'Configure 2FA', 'wp-2fa' ) . '</a>';
262 }
263
264 if ( empty( $additional_args ) ) {
265 $form_content .= '<a href="' . \esc_url( $first_time_setup_url ) . '" class="button button-primary ' . \esc_attr( $styling_class ) . '">' . \esc_html__( 'Configure Two-factor authentication (2FA)', 'wp-2fa' ) . '</a>';
266 }
267 }
268 }
269 }
270
271 // Admin viewing users profile AND user has a configured 2FA method.
272 if ( User_Utils::in_array_all( array( 'can_manage_options', 'has_enabled_methods' ), $user_type ) && ! in_array( 'viewing_own_profile', $user_type, true ) ) {
273 $description = \esc_html__( 'The user has already configured 2FA. When you reset the user\'s current 2FA configuration, the user can log back in with just the username and password.', 'wp-2fa' );
274
275 $remove_users_2fa_url = add_query_arg(
276 array(
277 'action' => 'remove_user_2fa',
278 'user_id' => $user->ID,
279 'wp_2fa_nonce' => \wp_create_nonce( 'wp-2fa-remove-user-2fa-nonce' ),
280 'admin_reset' => 'yes',
281 ),
282 \admin_url( 'user-edit.php' )
283 );
284
285 $form_content .= '<a href="' . \esc_url( $remove_users_2fa_url ) . '" class="button button-primary">' . \esc_html__( 'Reset 2FA configuration', 'wp-2fa' ) . '</a>';
286 }
287
288
289 // Admin viewing users profile AND users grace period has expired.
290 if ( User_Utils::in_array_all( array( 'can_manage_options', 'grace_has_expired' ), $user_type ) ) {
291 $unlock_user_url = \add_query_arg(
292 array(
293 'action' => 'unlock_account',
294 'user_id' => $user->ID,
295 'wp_2fa_nonce' => \wp_create_nonce( 'wp-2fa-unlock-account-nonce' ),
296 ),
297 \admin_url( 'user-edit.php' )
298 );
299 $form_content .= '<a href="' . \esc_url( $unlock_user_url ) . '" class="button button-primary">' . \esc_html__( 'Unlock user and reset the grace period', 'wp-2fa' ) . '</a>';
300 }
301
302 if ( $show_preamble ) {
303 $form_output .= '<h2>' . WP2FA::get_wp2fa_white_label_setting( 'user-profile-form-preamble-title', true ) . '</h2>';
304
305 if ( $description ) {
306 $form_output .= '<p class="description">' . $description . '</p>';
307 }
308 }
309 /**
310 * Gives the ability to add more content to the profile page.
311 *
312 * @param string $form_content - The parsed HTML of the form.
313 * @param \WP_USER $user - The user object.
314 *
315 * @since 3.0.0
316 */
317 $form_content = \wp_kses_post( \apply_filters( WP_2FA_PREFIX . 'append_to_profile_form_content', $form_content, $user ) );
318
319 if ( $show_form_table && ! empty( $form_content ) ) {
320
321
322 $enabled_methods = User_Helper::get_enabled_method_for_user( $user );
323 $primary_label = \esc_html__( 'No enabled primary method', 'wp-2fa' );
324 if ( isset( $enabled_methods ) && ! empty( $enabled_methods ) ) {
325 $translated_methods = Settings::get_providers_translate_names();
326 $primary_label = ( isset( $translated_methods[ $enabled_methods ] ) ) ? $translated_methods[ $enabled_methods ] : \esc_html__( 'No enabled primary method', 'wp-2fa' );
327 }
328 $enabled_backup_methods = User_Helper::get_enabled_backup_methods_for_user( $user );
329 $backup_methods_enabled = \esc_html__( 'No enabled backup methods', 'wp-2fa' );
330
331 if ( isset( $enabled_backup_methods ) && ! empty( $enabled_backup_methods ) ) {
332 $backup_methods_enabled = \implode( ', ', $enabled_backup_methods );
333 }
334
335 $show_enabled = true;
336
337 if ( isset( $additional_args ) && ! empty( $additional_args ) && isset( $additional_args['options'] ) && ! empty( $additional_args['options'] ) ) {
338 if ( isset( $additional_args['options']['do_not_show_enabled'] ) && false !== (bool) $additional_args['options']['do_not_show_enabled'] ) {
339 $show_enabled = false;
340 }
341 }
342
343 if ( $show_enabled ) {
344
345 $form_output .= '<h3 style="font-size: 1.1em;">' . \esc_html__( 'Currently configured:', 'wp-2fa' ) . '</h3>';
346
347 $form_output .= '
348 <table id="wp-2fa-currently-configured-methods" class="form-table wp-2fa-user-profile-form" role="presentation">
349 <tbody>
350 <th><label>' . \esc_html__( 'Primary method:', 'wp-2fa' ) . '</label></th>
351 <td>
352 ' . $primary_label . '
353 </td>
354 </tr>';
355
356 $form_output .= '
357 <tr>
358 <th><label>' . \esc_html__( 'Secondary method(s):', 'wp-2fa' ) . '</label></th>
359 <td>
360 ' . $backup_methods_enabled . '
361 </td>
362 </tr>';
363
364 $form_output .= '
365 </tbody>
366 </table>';
367 }
368
369 $form_output .= '<h3 style="font-size: 1.1em;">' . \esc_html__( '2FA configuration:', 'wp-2fa' ) . '</h3>';
370
371 if ( User_Utils::in_array_all( array( 'has_enabled_methods', 'viewing_own_profile' ), $user_type ) && isset( $enabled_methods ) && TOTP::METHOD_NAME === $enabled_methods ) {
372 $form_output .= '
373 <table id="wp-2fa-configuration-options" class="form-table wp-2fa-user-profile-form remove-tr-padding" role="presentation">
374 <tbody>
375 <tr>
376 <th><label>' . Settings::get_providers_translate_names()[ $enabled_methods ] . '</label></th>
377 <td>
378 <details>
379 <summary class="qr-btn">' . \esc_html__( 'Show QR code', 'wp-2fa' ) . '</summary>
380 <p><img class="qr-code" src="' . ( TOTP::get_qr_code() ) . '" /></p>
381 <div class="app-key-wrapper">
382 <input type="text" id="app-key-input" readonly value="' . \esc_html( TOTP::get_totp_decrypted() ) . '" class="app-key">
383 ' .
384 ( ( is_ssl() ) ?
385 '<span class="click-to-copy">' . \esc_html__( 'COPY', 'wp-2fa' ) . '</span>' : '' ) . '
386 </div>
387 </details>
388 </td>
389 </tr>
390 </tbody>
391 </table>';
392 }
393
394 $form_output .= '
395 <table id="wp-2fa-user-global-configuration" class="form-table wp-2fa-user-profile-form" role="presentation">
396 <tbody>
397 <tr>
398 <th><label>' . \esc_html__( '2FA Setup:', 'wp-2fa' ) . '</label></th>
399 <td>
400 ' . $form_content . '
401 </td>
402 </tr>
403 </tbody>
404 </table>';
405
406 if ( ( ( isset( $_GET['show'] ) && 'wp-2fa-setup' === $_GET['show'] ) || User_Helper::get_user_enforced_instantly( $user ) ) && $show_enable2fa ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
407 $form_output .= '
408 <script>
409 window.addEventListener("load", function() {
410 wp2fa_fireWizard();
411 });
412 </script>
413 ';
414 }
415 }
416
417 echo $form_output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
418
419 self::generate_inline_modals( $user_type );
420 }
421
422 /**
423 * Responsible for the building of all the modals.
424 *
425 * @param array $user_type - The WP user type.
426 *
427 * @return void
428 *
429 * @since 2.7.0
430 */
431 public static function generate_inline_modals( $user_type = array() ) {
432
433 ob_start();
434
435 $user = \wp_get_current_user();
436
437 $styling_class = ( empty( WP2FA::get_wp2fa_white_label_setting( 'enable_wizard_styling' ) ) ) ? 'default_styling' : 'enable_styling';
438
439 if ( User_Utils::in_array_all( array( 'user_needs_to_setup_2fa', 'viewing_own_profile' ), $user_type ) || User_Utils::in_array_all( array( 'has_enabled_methods', 'viewing_own_profile' ), $user_type ) || User_Utils::in_array_all( array( 'no_required_not_enabled', 'viewing_own_profile' ), $user_type ) || User_Utils::in_array_all( array( User_Helper::USER_UNDETERMINED_STATUS, 'viewing_own_profile' ), $user_type ) ) {
440 ?>
441 <div>
442 <div class="wp2fa-modal micromodal-slide <?php echo \esc_attr( $styling_class ); ?>" id="configure-2fa" aria-hidden="true">
443 <div class="modal__overlay" tabindex="-1">
444 <div class="modal__container" role="dialog" aria-dialog aria-labelledby="modal-1-title">
445 <?php
446 echo Generate_Modal::generate_modal(
447 'notify-users',
448 '',
449 \wp_kses_post( WP2FA::get_wp2fa_white_label_setting( 'wp-2fa_wizard_cancel', true ) ),
450 array(
451 '<button type="button" class="button wp-2fa-button-primary button-primary button-confirm" aria-label="Close this dialog window and the wizard">' . \esc_html__( 'Yes', 'wp-2fa' ) . '</button>',
452 '<button type="button" class="button wp-2fa-button-secondary button-secondary button-decline" data-micromodal-close aria-label="Close this dialog window">' . \esc_html__( 'No', 'wp-2fa' ) . '</button>',
453 ),
454 '',
455 '430px'
456 );
457 ?>
458 <?php
459 echo Generate_Modal::generate_modal(
460 'confirm-logout',
461 '',
462 \wp_kses_post( WP2FA::get_wp2fa_white_label_setting( '2fa_wizard_logout', true ) ),
463 array(
464 '<button type="button" class="button wp-2fa-button-primary button-primary button-confirm-logout" aria-label="Close this dialog window and the wizard">' . \esc_html__( 'Yes', 'wp-2fa' ) . '</button>',
465 '<button type="button" class="button wp-2fa-button-secondary button-secondary button-decline" data-micromodal-close aria-label="Close this dialog window">' . \esc_html__( 'No', 'wp-2fa' ) . '</button>',
466 ),
467 '',
468 '430px'
469 );
470 ?>
471 <button class="modal__close modal_cancel" aria-label="Close modal"></button>
472 <main class="modal__content wp2fa-form-styles" id="modal-1-content">
473 <?php
474 $logo = WP2FA::get_wp2fa_white_label_setting( 'logo-code-page', false );
475 $logo_section = ( $logo ) ? '<p class="modal-logo-wrapper"><img style="max-height: 60px;margin: 0 auto 30px;" src="' . \esc_url( $logo ) . '" /></p>' : '';
476 $enable_logo = WP2FA::get_wp2fa_white_label_setting( 'enable_wizard_logo', false );
477
478 if ( $enable_logo ) {
479 echo $logo_section; // phpcs:ignore */
480 }
481
482 if ( User_Utils::in_array_all( array( 'user_needs_to_setup_2fa', 'viewing_own_profile' ), $user_type ) || User_Utils::in_array_all( array( 'no_required_not_enabled', 'viewing_own_profile' ), $user_type ) || User_Utils::in_array_all( array( User_Helper::USER_UNDETERMINED_STATUS, 'viewing_own_profile' ), $user_type ) ) {
483
484 $available_methods = Methods::get_enabled_methods( User_Helper::get_user_role( $user ) );
485 $optional_welcome = WP2FA::get_wp2fa_white_label_setting( 'welcome', false );
486 $enable_welcome = WP2FA::get_wp2fa_white_label_setting( 'enable_welcome', false );
487
488 $intro_text = '';
489 if ( count( $available_methods[ User_Helper::get_user_role( $user ) ] ) > 1 ) {
490 $intro_text = WP2FA::replace_wizard_strings( WP2FA::get_wp2fa_white_label_setting( 'method_selection', true ), $user );
491 } elseif ( 1 === count( $available_methods[ User_Helper::get_user_role( $user ) ] ) ) {
492 $intro_text = WP2FA::replace_wizard_strings( WP2FA::get_wp2fa_white_label_setting( 'method_selection', true ), $user );
493 } else {
494 $intro_text = '<h3 style="font-size: 1.1em;">' . esc_html__( 'No available 2FA methods set', 'wp-2fa' ) . '</h3><p>' . esc_html__( 'Ask your administrator to enable 2FA methods', 'wp-2fa' ) . '</p>';
495 }
496
497 if ( ! empty( $optional_welcome ) && $enable_welcome ) {
498 Wizard_Steps::optional_user_welcome_step();
499 }
500 ?>
501
502 <div class="wizard-step <?php echo ( empty( $optional_welcome ) ) ? 'active' : ''; ?>" id="choose-2fa-method">
503 <div class="mb-20"><?php echo \wp_kses_post( $intro_text ); ?></div>
504 <fieldset class="radio-cells">
505 <?php
506 /**
507 * Adds an option for external providers to add their own 2fa methods options. And sorts them (our logic).
508 *
509 * @since 2.0.0
510 */
511 \do_action( WP_2FA_PREFIX . 'methods_options' );
512 ?>
513 </fieldset>
514 <br>
515 <?php
516 if ( 0 !== count( $available_methods[ User_Helper::get_user_role( $user ) ] ) ) {
517 ?>
518 <a href="#" class="button wp-2fa-button-primary button-primary wp-2fa-choose-method" data-name="next_step_setting_modal_wizard" ><?php \esc_html_e( 'Next Step', 'wp-2fa' ); ?></a>
519 <?php
520 }
521 ?>
522 <button class="button wp-2fa-button-secondary button-secondary" data-close-2fa-modal aria-label="Close this dialog window"><?php \esc_html_e( 'Cancel', 'wp-2fa' ); ?></button>
523 <?php
524 /*
525 if ( User_Helper::is_enforced( User_Helper::get_user_object()->ID ) ) {
526 ?>
527 <a class="button button-primary wp-2fa-button-primary modal_logout" <?php echo WP_Helper::create_data_nonce( 'wp-2fa-logout' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>><?php \esc_attr_e( 'Log out', 'wp-2fa' ); ?></a>
528 <?php
529 }
530 */
531 ?>
532 </div>
533 <?php } ?>
534
535 <?php if ( User_Utils::in_array_all( array( 'has_enabled_methods', 'viewing_own_profile' ), $user_type ) ) { ?>
536 <div class="wizard-step active">
537 <fieldset class="radio-cells max-3">
538 <?php
539 /**
540 * Add an option for external providers to add their own reconfigure methods options.
541 *
542 * @since 2.0.0
543 */
544 \do_action( WP_2FA_PREFIX . 'methods_reconfigure_options' );
545 ?>
546 </fieldset>
547 </div>
548 <?php } ?>
549
550 <?php Wizard_Steps::show_modal_methods( $user ); ?>
551 <?php
552
553 $backup_methods = Settings::get_enabled_backup_methods_for_user_role( $user );
554
555 if ( count( $backup_methods ) > 1 ) {
556 Wizard_Steps::choose_backup_method();
557 }
558
559 /**
560 * Add an option for external providers to add their own wizard steps.
561 *
562 * @since 2.0.0
563 */
564 \do_action( WP_2FA_PREFIX . 'additional_settings_steps' );
565
566 // Create a nonce for use in ajax call to generate codes.
567 if ( Backup_Codes::are_backup_codes_enabled_for_role( User_Helper::get_user_role( $user ) ) ) {
568 ?>
569 <div class="wizard-step" id="wp-2fa-wizard-config-backup-codes">
570 <?php Wizard_Steps::backup_codes_configure(); ?>
571 <?php Wizard_Steps::generated_backup_codes(); ?>
572 </div>
573 <?php } else { ?>
574 <div class="wizard-step" id="wp-2fa-wizard-config-backup-codes">
575 <?php Wizard_Steps::congratulations_step(); ?>
576 </div>
577 <?php } ?>
578 </main>
579 </div>
580 </div>
581 </div>
582 </div>
583 <?php } ?>
584
585 <?php
586 /**
587 * Add an option for external providers to add their own 2fa methods options.
588 *
589 * @since 2.0.0
590 */
591 \do_action( WP_2FA_PREFIX . 'methods_wizards' );
592 ?>
593
594 <?php if ( Backup_Codes::are_backup_codes_enabled_for_role( User_Helper::get_user_role( $user ) ) ) { ?>
595 <div>
596 <div class="wp2fa-modal micromodal-slide <?php echo \esc_attr( $styling_class ); ?>" id="configure-2fa-backup-codes" aria-hidden="true">
597 <input type="hidden" id="wp-2fa-manual-backup-codes" />
598 <div class="modal__overlay" tabindex="-1">
599 <div class="modal__container" role="dialog" aria-dialog aria-labelledby="modal-1-title">
600 <button class="modal__close modal_cancel" aria-label="Close modal" data-close-2fa-modal></button>
601 <main class="modal__content wp2fa-form-styles" id="modal-1-content">
602 <?php Wizard_Steps::generated_backup_codes( true ); ?>
603 </main>
604 </div>
605 </div>
606 </div>
607 </div>
608 <?php } ?>
609 <div>
610 <?php
611
612 if ( self::can_user_remove_2fa( $user->ID ) ) :
613 echo Generate_Modal::generate_modal( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
614 'confirm-remove-2fa',
615 esc_html__( 'Remove 2FA?', 'wp-2fa' ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
616 esc_html__( 'Are you sure you want to remove two-factor authentication and lower the security of your user account?', 'wp-2fa' ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
617 array(
618 '<a href="#" class="button wp-2fa-button-primary" data-trigger-remove-2fa data-user-id="' . \esc_attr( $user->ID ) . '" ' . WP_Helper::create_data_nonce( 'wp-2fa-remove-user-2fa-nonce' ) . '>' . \esc_html__( 'Yes', 'wp-2fa' ) . '</a>', // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
619 '<button class="modal__btn wp-2fa-button-secondary button" data-close-2fa-modal aria-label="Close this dialog window">' . \esc_html__( 'No', 'wp-2fa' ) . '</button>',
620 )
621 );
622 endif;
623 ?>
624 </div>
625 <?php
626
627 $output = ob_get_contents();
628 ob_end_clean();
629
630 echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
631 }
632
633 /**
634 * Produces the 2FA configuration form for network users, or any user with no roles.
635 *
636 * @param string $is_shortcode - Current logic expects that to be set always.
637 * @param boolean $show_preamble - Show / hide preamble.
638 * @param array $options - Array with additional options.
639 *
640 * @return void
641 *
642 * @since 2.7.0
643 */
644 public static function inline_2fa_profile_form( $is_shortcode = 'true', $show_preamble = true, array $options = array() ) {
645
646 if ( isset( $_GET['user_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
647 $user_id = (int) $_GET['user_id']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
648 if ( ! \current_user_can( 'edit_user', $user_id ) ) {
649 return;
650 }
651 $user = \get_user_by( 'id', $user_id );
652 } else {
653 $user = \wp_get_current_user();
654 }
655
656 // Get current user, we going to need this regardless.
657 $current_user = \wp_get_current_user();
658
659 if ( \is_multisite() ) {
660 if ( '' === trim( (string) User_Helper::get_user_role( $user ) ) ) {
661 return;
662 }
663 }
664
665 // Bail if we still dont have an object.
666 if ( ! is_a( $user, '\WP_User' ) || ! is_a( $current_user, '\WP_User' ) ) {
667 return;
668 }
669
670 $additional_args = array(
671 'is_shortcode' => $is_shortcode,
672 'show_preamble' => $show_preamble,
673 'options' => $options,
674 );
675
676 self::user_2fa_options( $user, $additional_args );
677 }
678
679 /**
680 * Add custom unlock account link to user edit admin list.
681 *
682 * @param string $actions Default actions.
683 * @param object $user_object User data.
684 * @return string Appended actions.
685 *
686 * @since 2.7.0
687 */
688 public static function user_2fa_row_actions( $actions, $user_object ) {
689 $nonce = wp_create_nonce( 'wp-2fa-unlock-account-nonce' );
690 $grace_period_expired = User_Helper::get_grace_period( $user_object );
691 $url = add_query_arg(
692 array(
693 'action' => 'unlock_account',
694 'user_id' => $user_object->ID,
695 'wp_2fa_nonce' => $nonce,
696 ),
697 admin_url( 'users.php' )
698 );
699
700 if ( $grace_period_expired ) {
701 $actions['edit_badges'] = '<a href="' . \esc_url( $url ) . '">' . \esc_html__( 'Unlock user', 'wp-2fa' ) . '</a>';
702 }
703
704 return $actions;
705 }
706
707 /**
708 * Save user profile information.
709 *
710 * @param array $input - The array with values to process.
711 *
712 * @return void
713 *
714 * @since 2.7.0
715 */
716 public static function save_user_2fa_options( $input ) {
717
718 // Ensure we have the inputs we want before we process.
719 // To avoid causing issues with the rest of the user profile.
720 if ( ! is_array( $input ) || empty( $input ) ) {
721 return;
722 }
723
724 // Grab current user.
725 $user = wp_get_current_user();
726
727 // Grab authcode and ensure its a number.
728 if ( isset( $input['wp-2fa-totp-authcode'] ) ) {
729 $input['wp-2fa-totp-authcode'] = (int) $input['wp-2fa-totp-authcode'];
730 }
731 if ( ( ! isset( $input['custom-email-address'] ) || isset( $input['custom-email-address'] ) && empty( $input['custom-email-address'] ) ) &&
732 ( ! isset( $input['custom-oob-email-address'] ) || isset( $input['custom-oob-email-address'] ) && empty( $input['custom-oob-email-address'] ) ) ) {
733 if ( isset( $input['email'] ) ) {
734 User_Helper::set_nominated_email_for_user( sanitize_email( $input['email'] ), $user );
735 } elseif ( isset( $input['wp_2fa_email_address'] ) && isset( $input['wp-2fa-totp-authcode'] ) && ! empty( $input['wp-2fa-totp-authcode'] ) ) {
736 User_Helper::set_nominated_email_for_user( sanitize_email( $input['wp_2fa_email_address'] ), $user );
737 } elseif ( isset( $input['wp_2fa_email_oob_address'] ) && isset( $input['wp-2fa-oob-authcode'] ) && ! empty( $input['wp-2fa-oob-authcode'] ) ) {
738 if ( 'use_custom_email' !== $input['wp_2fa_email_oob_address'] ) {
739 User_Helper::set_nominated_email_for_user( sanitize_email( $input['wp_2fa_email_oob_address'] ), $user );
740 }
741 }
742 } elseif ( isset( $input['custom-email-address'] ) && ! empty( $input['custom-email-address'] ) ) {
743 User_Helper::set_nominated_email_for_user( sanitize_email( $input['custom-email-address'] ), $user );
744 } elseif ( isset( $input['custom-oob-email-address'] ) && ! empty( $input['custom-oob-email-address'] ) ) {
745 User_Helper::set_nominated_email_for_user( sanitize_email( $input['custom-oob-email-address'] ), $user );
746 }
747
748 // Check its one of our options.
749 if ( ( isset( $input['wp_2fa_enabled_methods'] ) && TOTP::METHOD_NAME === $input['wp_2fa_enabled_methods'] ) ||
750 ( isset( $input['wp_2fa_enabled_methods'] ) && Email::METHOD_NAME === $input['wp_2fa_enabled_methods'] ) ||
751 ( isset( $input['wp_2fa_enabled_methods'] ) && ( class_exists( '\WP2FA\Extensions\OutOfBand\Out_Of_Band', false ) && Out_Of_Band::METHOD_NAME === $input['wp_2fa_enabled_methods'] ) ) ) {
752 User_Helper::set_enabled_method_for_user( sanitize_text_field( wp_unslash( $input['wp_2fa_enabled_methods'] ) ), $user );
753 self::delete_expire_and_enforced_keys( $user->ID );
754 User_Helper::set_user_status( $user );
755 }
756
757 if ( isset( $input['wp-2fa-email-authcode'] ) && ! empty( $input['wp-2fa-email-authcode'] ) ) {
758 User_Helper::set_enabled_method_for_user( Email::METHOD_NAME, $user );
759 self::delete_expire_and_enforced_keys( $user->ID );
760 User_Helper::set_user_status( $user );
761 }
762
763 if ( isset( $input['wp-2fa-totp-authcode'] ) && ! empty( $input['wp-2fa-totp-authcode'] ) ) {
764 $totp_key = sanitize_text_field( $input['wp-2fa-totp-key'] );
765 if ( Authentication::is_valid_key( $totp_key ) ) {
766 if ( Open_SSL::is_ssl_available() ) {
767 $totp_key = Open_SSL::SECRET_KEY_PREFIX . Open_SSL::encrypt( $totp_key );
768 }
769
770 TOTP::set_user_method( $user, $totp_key );
771 }
772 }
773 }
774
775 /**
776 * Utility function to remove user expiry and enforced data.
777 *
778 * @param int $user_id User id to process.
779 *
780 * @since 2.7.0
781 */
782 public static function delete_expire_and_enforced_keys( $user_id ) {
783 User_Helper::remove_user_expiry_date( $user_id );
784 User_Helper::remove_user_enforced_instantly( $user_id );
785 User_Helper::remove_grace_period( $user_id );
786 User_Helper::remove_meta( User_Helper::USER_LOCKED_STATUS, $user_id );
787 }
788
789 /**
790 * Validate a user's code when setting up 2fa via the inline form.
791 *
792 * @return void
793 *
794 * @since 2.7.0
795 */
796 public static function validate_authcode_via_ajax() {
797 check_ajax_referer( 'wp-2fa-validate-authcode' );
798
799 if ( isset( $_POST['form'] ) ) {
800 $input = wp_unslash( $_POST['form'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
801 } else {
802 wp_send_json_error(
803 array(
804 'error' => \esc_html__( 'No form', 'wp-2fa' ),
805 )
806 );
807 }
808
809 $user = wp_get_current_user();
810
811 $our_errors = '';
812
813 // Grab key from the $_POST.
814 if ( isset( $input['wp-2fa-totp-key'] ) ) {
815 $current_key = sanitize_text_field( wp_unslash( $input['wp-2fa-totp-key'] ) );
816 }
817
818 // Grab authcode and validate format.
819 if ( isset( $input['wp-2fa-totp-authcode'] ) ) {
820 $raw_authcode = trim( (string) $input['wp-2fa-totp-authcode'] );
821 if ( '' !== $raw_authcode && ! preg_match( '/^\d{6}$/', $raw_authcode ) ) {
822 wp_send_json_error(
823 array(
824 'error' => \esc_html__( 'Invalid code. Please enter the correct OTP code generated by your Authenticator app.', 'wp-2fa' ),
825 )
826 );
827 }
828 $input['wp-2fa-totp-authcode'] = (int) $input['wp-2fa-totp-authcode'];
829 }
830
831 // Check if we are dealing with totp or email, if totp validate and store a new secret key.
832 if ( ! empty( $input['wp-2fa-totp-authcode'] ) && ! empty( $current_key ) ) {
833 if ( Authentication::is_valid_key( $current_key ) || ! is_numeric( $input['wp-2fa-totp-authcode'] ) ) {
834 if ( ! Authentication::is_valid_authcode( $current_key, \sanitize_text_field( \wp_unslash( $input['wp-2fa-totp-authcode'] ) ) ) ) {
835 $our_errors = \esc_html__( 'Invalid or expired OTP code. Please try again.', 'wp-2fa' );
836 }
837 } else {
838 $our_errors = \esc_html__( 'Invalid Two Factor Authentication secret key.', 'wp-2fa' );
839 }
840
841 // If its not totp, is it email.
842 } elseif ( ! empty( $input['wp-2fa-email-authcode'] ) ) {
843 if ( ! Authentication::validate_token( $user, sanitize_text_field( wp_unslash( $input['wp-2fa-email-authcode'] ) ) ) ) {
844 $our_errors = \esc_html__( 'Invalid Email Authentication code.', 'wp-2fa' );
845 }
846 } else {
847 $our_errors = \esc_html__( 'Please enter the code to finalize the 2FA setup.', 'wp-2fa' );
848 }
849
850 if ( ! empty( $our_errors ) ) {
851 // Send the response.
852 wp_send_json_error(
853 array(
854 'error' => $our_errors,
855 )
856 );
857 } else {
858 self::save_user_2fa_options( $input );
859 // Send the response.
860 wp_send_json_success();
861 }
862
863 wp_send_json_error(
864 array(
865 'error' => \esc_html__( 'Error processing form', 'wp-2fa' ),
866 )
867 );
868 }
869
870 /**
871 * Checks the user for remove 2FA capabilities.
872 *
873 * @param int $user_id User ID.
874 *
875 * @return bool True if the user can remove 2FA from their account.
876 *
877 * @since 2.7.0
878 */
879 public static function can_user_remove_2fa( $user_id ) {
880 // check the "Hide the Remove 2FA button" setting.
881 if ( Settings_Utils::get_setting_role( User_Helper::get_user_role( $user_id ), 'hide_remove_button' ) ) {
882 return false;
883 }
884
885 // check grace period policy.
886 $grace_policy = Settings_Utils::get_setting_role( User_Helper::get_user_role( $user_id ), 'grace-policy' );
887 if ( 'no-grace-period' === $grace_policy ) {
888 // we only need to run further checks to find out if the 2FA is enforced for the user in question if there
889 // is no grace period.
890 $enforcement_policy = Settings_Utils::get_setting_role( User_Helper::get_user_role( $user_id ), 'enforcement-policy' );
891
892 if ( 'all-users' === $enforcement_policy ) {
893 // enforced for all users, target user is definitely included.
894 return false;
895 }
896
897 if ( 'certain-roles-only' === $enforcement_policy && ! User_Helper::is_enforced( $user_id ) ) {
898 // Users specific role is not enforced, allow removal.
899 return true;
900 }
901
902 if ( 'certain-roles-only' === $enforcement_policy && User_Helper::is_enforced( $user_id ) ) {
903 // Users specific role is not enforced, allow removal.
904 return false;
905 }
906
907 if ( 'do-not-enforce' !== $enforcement_policy ) {
908 // one of possible enforcement options is set, check the target user.
909 return User_Helper::is_enforced( $user_id );
910 }
911 }
912
913 /**
914 * Gives the ability to remove the "Remove" button from the user profile page.
915 *
916 * @param bool - The status of the settings.
917 *
918 * @since 2.9.2
919 */
920 return \apply_filters( WP_2FA_PREFIX . 'can_user_remove_2fa', \true, $user_id );
921 }
922
923 /**
924 * Legacy stub: this method existed in older versions and may still be
925 * registered as a hook callback in the database after an upgrade.
926 *
927 * @return void
928 *
929 * @since 2.10.0
930 */
931 public static function dismiss_nag_notice() {
932 // No-op: retained for backward compatibility during upgrades.
933 }
934 }
935 }
936