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