PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.9.0
Defender Security – Malware Scanner, Login Security & Firewall v5.9.0
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
defender-security / src / controller / class-two-factor.php

class-two-factor.php in Defender Security – Malware Scanner, Login Security & Firewall 5.9.0, at src/controller/class-two-factor.php

1,458 lines 46.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * This class handles all the routes related to two-factor authentication.
4 *
5 * @package WP_Defender\Controller
6 */
7
8 namespace WP_Defender\Controller;
9
10 use WP_User;
11 use WP_Error;
12 use Exception;
13 use WP_User_Query;
14 use SodiumException;
15 use WP_Defender\Event;
16 use WP_Session_Tokens;
17 use Calotes\Helper\HTTP;
18 use Calotes\Helper\Route;
19 use Calotes\Component\Request;
20 use Calotes\Component\Response;
21 use Calotes\Helper\Array_Cache;
22 use WP_Defender\Component\Crypt;
23 use WP_Defender\Behavior\WPMUDEV;
24 use WP_Defender\Model\Setting\Two_Fa;
25 use WP_Defender\Integrations\Woocommerce;
26 use WP_Defender\Traits\Webauthn as Webauthn_Trait;
27 use WP_Defender\Component\Config\Config_Hub_Helper;
28 use WP_Defender\Component\Two_Fa as Two_Fa_Component;
29 use WP_Defender\Component\Two_Factor\Providers\Totp;
30 use WP_Defender\Component\Two_Factor\Providers\Webauthn;
31 use WP_Defender\Component\Webauthn as Webauthn_Component;
32 use WP_Defender\Controller\Webauthn as Webauthn_Controller;
33 use WP_Defender\Component\Two_Factor\Providers\Backup_Codes;
34 use WP_Defender\Component\Two_Factor\Providers\Fallback_Email;
35 use WP_Defender\Component\Password_Protection as Password_Protection_Service;
36
37 /**
38 * Handles all the routes related to two-factor authentication.
39 */
40 class Two_Factor extends Event {
41
42 use Webauthn_Trait;
43
44 /**
45 * The slug identifier for this controller.
46 *
47 * @var string
48 */
49 public $slug = 'wdf-2fa';
50
51 /**
52 * The model for handling the data.
53 *
54 * @var Two_Fa
55 */
56 protected $model;
57
58 /**
59 * Service for handling logic.
60 *
61 * @var Two_Fa_Component
62 */
63 protected $service;
64
65 /**
66 * Compatibility notices for the frontend.
67 *
68 * @var array
69 */
70 protected $compatibility_notices = array();
71
72 /**
73 * Logic for handling the password protection.
74 *
75 * @var Password_Protection_Service
76 */
77 protected $password_protection_service;
78
79 /**
80 * Is the woocommerce plugin activated.
81 *
82 * @var bool
83 */
84 protected $is_woo_activated;
85 /**
86 * The current logged-in user.
87 *
88 * @var WP_User
89 */
90 protected $current_user;
91
92 /**
93 * Defender flush rules slug.
94 *
95 * @var string
96 */
97 private $flush_slug = 'defender_flush_rules';
98
99 /**
100 * Initializes the model and service, registers routes, and sets up scheduled events if the model is active.
101 */
102 public function __construct() {
103 $this->register_page(
104 esc_html__( '2FA', 'defender-security' ),
105 $this->slug,
106 array( $this, 'main_view' ),
107 $this->parent_slug
108 );
109 add_action( 'defender_enqueue_assets', array( $this, 'enqueue_assets' ) );
110 $this->register_routes();
111 $this->service = wd_di()->get( Two_Fa_Component::class );
112 $this->model = wd_di()->get( Two_Fa::class );
113 $this->password_protection_service = wd_di()->get( Password_Protection_Service::class );
114 $this->is_woo_activated = wd_di()->get( Woocommerce::class )->is_activated();
115
116 add_action( 'update_option_jetpack_active_modules', array( $this, 'listen_for_jetpack_option' ), 10, 2 );
117
118 if ( $this->model->is_active() ) {
119 require_once ABSPATH . 'wp-admin/includes/plugin.php';
120 $is_jetpack_sso = $this->service->is_jetpack_sso();
121 $is_tml = $this->service->is_tml();
122 add_action( 'admin_init', array( $this->service, 'get_providers' ) );
123 add_action( 'pre_get_users', array( $this, 'filter_users_by_2fa' ) );
124 add_action( 'show_user_profile', array( $this, 'show_user_profile' ) );
125 add_action( 'profile_update', array( $this, 'profile_update' ) );
126
127 if ( ! defined( 'DOING_AJAX' ) && ! $is_jetpack_sso && ! $is_tml ) {
128 add_filter( 'authenticate', array( $this, 'maybe_show_otp_form' ), 30, 3 );
129 add_action( 'set_logged_in_cookie', array( $this, 'store_session_key' ) );
130 add_action( 'login_form_defender-verify-otp', array( $this, 'verify_otp_login_time' ) );
131 } else {
132 if ( $is_jetpack_sso ) {
133 $this->compatibility_notices[] = esc_html__(
134 'We`ve detected a conflict with Jetpack`s Wordpress.com Log In feature. Please disable it and return to this page to continue setup.',
135 'defender-security'
136 );
137 }
138 if ( $is_tml ) {
139 $this->compatibility_notices[] = esc_html__(
140 'We`ve detected a conflict with Theme my login. Please disable it and return to this page to continue setup.',
141 'defender-security'
142 );
143 }
144 }
145 // Force auth redirect for admin area.
146 add_action( 'current_screen', array( $this, 'maybe_redirect_to_show_2fa_enabler' ), 1 );
147
148 $this->service->add_hooks();
149
150 // Todo: add the verify for filter 'login_redirect'.
151 if ( $this->is_woo_activated ) {
152 // Todo: move to Woocommerce class.
153 $this->current_user = wp_get_current_user();
154 $this->woocommerce_hooks();
155
156 // Display 2FA content on Woo My Account page for enabled user roles.
157 if ( $this->model->detect_woo && is_object( $this->current_user ) && $this->current_user->exists()
158 && $this->service->is_auth_enable_for( $this->current_user, $this->model->user_roles )
159 ) {
160 // Show a new Woo submenu.
161 add_action( 'init', array( $this, 'wp_defender_2fa_endpoint' ) );
162 add_filter( 'query_vars', array( $this, 'wp_defender_2fa_query_vars' ), 0 );
163 add_filter( 'woocommerce_account_menu_items', array( $this, 'wp_defender_2fa_link_my_account' ) );
164 add_action(
165 "woocommerce_account_{$this->slug}_endpoint",
166 array(
167 &
168 $this,
169 'wp_defender_2fa_content',
170 )
171 );
172 // Display Woo content for 2FA user settings.
173 add_shortcode( 'wp_defender_2fa_user_settings', array( $this, 'display_2fa_user_settings' ) );
174 // Form processing.
175 add_action( 'template_redirect', array( $this, 'save_2fa_details' ) );
176 }
177 }
178 // Fires when 2FA methods are enabled.
179 add_action( 'wd_2fa_enabled_provider_slugs', array( $this, 'enable_provider_slugs' ) );
180 }
181 }
182
183 /**
184 * Checks if WooCommerce integration is enabled.
185 *
186 * @return bool Whether WooCommerce integration is enabled.
187 */
188 public function woo_integration_enabled(): bool {
189 return $this->is_woo_activated && $this->model->detect_woo;
190 }
191
192 /**
193 * We have some feature conflict with jetpack, so listen to know when Defender can on.
194 *
195 * @param mixed $old_value Old value of `jetpack_active_modules`.
196 * @param mixed $value New value of `jetpack_active_modules`.
197 *
198 * @return void
199 */
200 public function listen_for_jetpack_option( $old_value, $value ): void {
201 if ( false !== array_search( 'sso', $value, true ) ) {
202 $this->model->mark_as_conflict( 'jetpack/jetpack.php' );
203 } else {
204 $this->model->mark_as_un_conflict( 'jetpack/jetpack.php' );
205 }
206 }
207
208 /**
209 * If force redirect enabled, then we should check and redirect to profile page until the 2FA enabled.
210 *
211 * @return void
212 */
213 public function maybe_redirect_to_show_2fa_enabler() {
214 $user = wp_get_current_user();
215 if ( ! is_object( $user ) ) {
216 return;
217 }
218 // Is User role from common list checked?
219 if ( false === $this->service->is_auth_enable_for( $user, $this->model->user_roles ) ) {
220 return;
221 }
222 // Is 'Force Authentication' checked?
223 if ( false === $this->model->force_auth ) {
224 return;
225 }
226 // Is User role from forced list checked?
227 if ( ! $this->service->is_force_auth_enable_for( $user->ID, $this->model->force_auth_roles ) ) {
228 return;
229 }
230 // Is TOTP saved with a passcode?
231 if ( array() !== $this->service->get_available_providers_for_user( $user ) ) {
232 return;
233 }
234 $screen = get_current_screen();
235 if ( 'profile' !== $screen->id ) {
236 wp_safe_redirect( admin_url( 'profile.php' ) . '#defender-security' );
237 exit;
238 }
239 }
240
241 /**
242 * Retrieve the backup code if lost phone.
243 *
244 * @param Request $request Request object.
245 *
246 * @return Response
247 * @defender_route
248 * @is_public
249 */
250 public function send_backup_code( Request $request ): Response {
251 $data = $request->get_data();
252 $token = $data['token'];
253 $user_id = (int) $data['requested_user'];
254 $ret = $this->service->send_otp_to_email( $token, $user_id );
255 if ( false === $ret ) {
256 return new Response(
257 false,
258 array( 'message' => esc_html__( 'Please try again.', 'defender-security' ) )
259 );
260 }
261
262 if ( is_wp_error( $ret ) ) {
263 return new Response(
264 false,
265 array( 'message' => $ret->get_error_message() )
266 );
267 }
268
269 return new Response(
270 true,
271 array( 'message' => esc_html__( 'Your code has been sent to your email.', 'defender-security' ) )
272 );
273 }
274
275 /**
276 * Verify the OTP after user login successful.
277 *
278 * @return void
279 */
280 public function verify_otp_login_time() {
281 if ( 'POST' !== defender_get_data_from_request( 'REQUEST_METHOD', 's' ) ) {
282 return;
283 }
284
285 $post = defender_get_data_from_request( null, 'p' );
286 if ( '' === $post['_wpnonce'] || ! wp_verify_nonce( $post['_wpnonce'], 'verify_otp' ) ) {
287 wp_die( esc_html__( 'Nonce verification failed.', 'defender-security' ) );
288 }
289
290 $token = HTTP::post( 'login_token' );
291 $user_id = (int) HTTP::post( 'requested_user', 0 );
292 $auth_method = HTTP::post( 'auth_method' );
293 $password = HTTP::post( 'password' );
294 if ( '' === $token || 0 === $user_id || '' === $auth_method || '' === $password ) {
295 wp_die( esc_html__( 'Missing parameter(s)', 'defender-security' ) );
296 }
297
298 $user = get_user_by( 'id', $user_id );
299 // Spoofed data? E.g. a hidden field user is changed.
300 if ( ! is_object( $user ) ) {
301 wp_die( esc_html__( 'Invalid user.', 'defender-security' ) );
302 }
303
304 $hashed_token = get_user_meta( $user_id, Two_Fa_Component::TOKEN_USER_KEY, true );
305 // Spoofed data again?
306 if ( ! Crypt::compare_lines( $hashed_token, wp_hash( $user_id . $token ) ) ) {
307 wp_die( esc_html__( 'Invalid request.', 'defender-security' ) );
308 }
309
310 // Base params.
311 $params = array(
312 'password' => $this->password_protection_service->get_submitted_password(),
313 'user_id' => $user->ID,
314 'token' => $this->get_token( $user_id ),
315 'default_slug' => $auth_method,
316 );
317
318 // Get provider object.
319 $provider = $this->service->get_provider_by_slug( $auth_method );
320 if ( is_wp_error( $provider ) ) {
321 $params['error'] = $provider;
322 $this->render_otp_screen( $params );
323 }
324 $result = $provider->validate_authentication( $user );
325 if ( is_wp_error( $result ) ) {
326 $params['error'] = $result;
327 $this->render_otp_screen( $params );
328 }
329 if ( $result ) {
330 // Clean token.
331 delete_user_meta( $user->ID, Two_Fa_Component::TOKEN_USER_KEY );
332
333 $is_weak_password = $this->password_protection_service->is_weak_password( $user, $password );
334 if ( true === $is_weak_password ) {
335 $this->password_protection_service->do_weak_reset( $user, $password );
336 } elseif ( $this->password_protection_service->is_force_reset( $user ) ) {
337 $this->password_protection_service->do_force_reset( $user, $password );
338 } else {
339 $user_id = $user->ID;
340 // Set active user.
341 wp_set_current_user( $user_id, $user->user_login );
342 // Todo: add code for 'rememberme'-option.
343 wp_set_auth_cookie( $user_id, true );
344
345 /**
346 * Fires after successful login via 2fa.
347 *
348 * @param int $user_id @since 2.6.1
349 * @param string $auth_method @since 3.4.0
350 */
351 do_action( 'wpmu_2fa_login', $user_id, $auth_method );
352
353 $interim_login = defender_get_data_from_request( 'interim-login', 'r' );
354 if ( is_string( $interim_login ) && '' !== trim( $interim_login ) ) {
355 $params['interim_login'] = 'success';
356 $params['message'] = '<p class="message">' . esc_html__( 'You have logged in successfully.', 'defender-security' ) . '</p>';
357 $this->render_otp_screen( $params );
358 exit;
359 } else {
360 // Usual success.
361 $redirect = apply_filters(
362 'login_redirect',
363 HTTP::post( 'redirect_to', admin_url() ),
364 $this->redirect_url(),
365 $user
366 );
367 wp_safe_redirect( $redirect );
368 exit;
369 }
370 }
371 }
372 $lockout_message = $this->service->verify_attempt( $user->ID, Totp::$slug );
373
374 $params['error'] = new WP_Error(
375 'opt_fail',
376 '' === trim( $lockout_message )
377 ? esc_html__( 'Whoops, the passcode you entered was incorrect or expired.', 'defender-security' )
378 : $lockout_message
379 );
380 $this->render_otp_screen( $params );
381 exit;
382 }
383
384 /**
385 * Generate a unique token for user.
386 *
387 * @param int $user_id User ID.
388 *
389 * @return string Unique token.
390 */
391 public function get_token( int $user_id ): string {
392 $token = bin2hex( Crypt::random_bytes( 32 ) );
393 update_user_meta( $user_id, Two_Fa_Component::TOKEN_USER_KEY, wp_hash( $user_id . $token ) );
394
395 return $token;
396 }
397
398 /**
399 * Render otp form. Required conditions for the current user:
400 * - is not logged in,
401 * - user data is not empty,
402 * - password matches the user,
403 * - user role is checked on 2FA settings,
404 * - user has at least one 2FA auth method available.
405 *
406 * @param null|WP_User|WP_Error $user Object of the logged-in user.
407 * @param string $username Username or email address.
408 * @param string $password Plain password string.
409 */
410 public function maybe_show_otp_form( $user, string $username, string $password ) {
411 if (
412 ! is_user_logged_in()
413 && '' !== trim( $password ) && $user instanceof WP_User
414 && wp_check_password( $password, $user->data->user_pass, $user->ID )
415 && $this->service->is_auth_enable_for( $user, $this->model->user_roles )
416 && array() !== $this->service->get_available_providers_for_user( $user )
417 ) {
418 $params = array();
419 $cookie = Array_Cache::get( 'auth_cookie', 'two_fa' );
420 if ( null !== $cookie ) {
421 // Clear all session data if any.
422 $session = WP_Session_Tokens::get_instance( $user->ID );
423 $session->destroy( $cookie['token'] );
424 }
425 // Prevent user to login, and show otp screen.
426 wp_clear_auth_cookie();
427 // All goods, we'll need to create a unique token to mark this user.
428 $params['token'] = $this->get_token( $user->ID );
429 $params['password'] = $password;
430 $params['user_id'] = $user->ID;
431 // Get default provider.
432 $params['default_slug'] = $this->service->get_default_provider_slug_for_user( $user->ID );
433 if ( Fallback_Email::$slug === $params['default_slug'] ) {
434 $user_id = isset( $user->ID ) ? $user->ID : 0;
435 $user_id = is_int( $user_id ) ? $user_id : (int) $user_id;
436 $result = $this->service->send_otp_to_email( $params['token'], $user_id );
437 if ( is_wp_error( $result ) ) {
438 $params['error'] = $result;
439 $this->render_otp_screen( $params );
440 }
441 }
442 $this->render_otp_screen( $params );
443 }
444
445 return $user;
446 }
447
448 /**
449 * Render the OTP screen after login successful.
450 *
451 * @param array $params Additional parameters.
452 *
453 * @return void|null
454 */
455 private function render_otp_screen( array $params = array() ) {
456 // Add common styles and scripts to enqueue.
457 wp_enqueue_script( 'jquery' );
458 wp_enqueue_style( 'defender-otp-screen', defender_asset_url( '/assets/css/otp.css' ), array(), DEFENDER_VERSION );
459
460 $params['redirect_to'] = $this->redirect_url();
461 if ( ! isset( $params['error'] ) ) {
462 $params['error'] = null;
463 }
464
465 $this->attach_behavior( WPMUDEV::class, WPMUDEV::class );
466 $custom_graphic = '';
467 $custom_graphic_type = '';
468 $custom_graphic_url = trim( $this->model->custom_graphic_url );
469 $custom_graphic_link = trim( $this->model->custom_graphic_link );
470 if ( $this->is_pro() && $this->model->custom_graphic ) {
471 $custom_graphic_type = $this->model->custom_graphic_type;
472 if ( Two_Fa::CUSTOM_GRAPHIC_TYPE_UPLOAD === $custom_graphic_type && '' !== $custom_graphic_url ) {
473 $custom_graphic = $custom_graphic_url;
474 } elseif ( Two_Fa::CUSTOM_GRAPHIC_TYPE_LINK === $custom_graphic_type && '' !== $custom_graphic_link ) {
475 $custom_graphic = $custom_graphic_link;
476 }
477 }
478
479 $params['custom_graphic'] = $custom_graphic;
480 $params['custom_graphic_type'] = $custom_graphic_type;
481
482 $collection = $this->dump_routes_and_nonces();
483 $routes = $collection['routes'];
484 $nonces = $collection['nonces'];
485
486 $params['providers'] = array();
487 $user = null;
488 if ( isset( $params['user_id'] ) ) {
489 $user = get_user_by( 'id', $params['user_id'] );
490 if ( is_object( $user ) ) {
491 $params['providers'] = $this->service->get_available_providers_for_user( $user );
492 // Get default provider.
493 if ( ! isset( $params['default_slug'] ) || ! is_string( $params['default_slug'] ) || '' === trim( $params['default_slug'] ) ) {
494 $params['default_slug'] = $this->service->get_default_provider_slug_for_user( $user->ID );
495 }
496 }
497 }
498
499 $this->service->remove_actions_for_2fa_screen();
500
501 if (
502 isset( $params['providers'][ Webauthn::$slug ] ) &&
503 false === $params['providers'][ Webauthn::$slug ]->is_otp_screen_available( $user )
504 ) {
505 unset( $params['providers'][ Webauthn::$slug ] );
506 $params['default_slug'] = Webauthn::$slug !== $params['default_slug'] ? $params['default_slug'] : null;
507 }
508
509 if ( 0 === count( $params['providers'] ) ) {
510 // Since 3.5.0.
511 $error_msg = esc_html__( 'No providers.', 'defender-security' );
512 $params['error'] = new WP_Error( 'opt_fail', $error_msg );
513 do_action( 'wd_2fa_otp_params', $params );
514
515 wp_die( esc_html( $error_msg ) );
516 }
517 // Add WebAuthn styles and scripts to enqueue.
518 if ( true === array_key_exists( Webauthn::$slug, $params['providers'] ) ) {
519 wp_enqueue_style(
520 'defender-biometric-login-screen',
521 defender_asset_url( '/assets/css/biometric.css' ),
522 array(),
523 DEFENDER_VERSION
524 );
525 wp_enqueue_script(
526 'wpdef_webauthn_common_script',
527 plugins_url( 'assets/js/webauthn-common.js', WP_DEFENDER_FILE ),
528 array(),
529 DEFENDER_VERSION,
530 true
531 );
532 wp_enqueue_script(
533 'defender-biometric-login-script',
534 plugins_url( 'assets/js/biometric-login.js', WP_DEFENDER_FILE ),
535 array(
536 'jquery',
537 'wpdef_webauthn_common_script',
538 ),
539 DEFENDER_VERSION,
540 true
541 );
542 $webauthn_controller = wd_di()->get( Webauthn_Controller::class );
543 wp_localize_script(
544 'defender-biometric-login-script',
545 'webauthn',
546 array(
547 'admin_url' => admin_url( 'admin-ajax.php' ),
548 'nonce' => wp_create_nonce( 'wpdef_webauthn' ),
549 'i18n' => $webauthn_controller->get_translations(),
550 'username' => isset( $user->user_login ) && is_string( $user->user_login ) && '' !== trim( $user->user_login ) ? $user->user_login : '',
551 'provider_slug' => Webauthn::$slug,
552 )
553 );
554 }
555 // Prepare data.
556 $args = array(
557 'action' => defender_base_action(),
558 '_def_nonce' => $nonces['send_backup_code'],
559 // Add a dummy values to avoid displaying errors, e.g. for the case with null.
560 'route' => $this->check_route( $routes['send_backup_code'] ?? 'test' ),
561 );
562 // If user's session has expired add a new 'interimlogin'-arg.
563 $interim_login = defender_get_data_from_request( 'interim-login', 'r' );
564 if ( is_string( $interim_login ) && '' !== trim( $interim_login ) ) {
565 $args['interimlogin'] = 'yes';
566 }
567 $params['action_fallback_email'] = add_query_arg( $args, admin_url( 'admin-ajax.php' ) );
568
569 // Since 3.5.0.
570 do_action( 'wd_2fa_otp_params', $params );
571
572 $this->render_partial( 'two-fa/otp', $params );
573 exit;
574 }
575
576 /**
577 * Store the session key in the array cache for two-factor authentication.
578 *
579 * @param string $cookie The authentication cookie.
580 *
581 * @return void
582 */
583 public function store_session_key( $cookie ): void {
584 // Clear login cookie to ensure nonce consistency.
585 if ( ! is_user_logged_in() && isset( $_COOKIE[ LOGGED_IN_COOKIE ] ) ) {
586 unset( $_COOKIE[ LOGGED_IN_COOKIE ] );
587 }
588
589 $cookie = wp_parse_auth_cookie( $cookie, 'logged_in' );
590 Array_Cache::set( 'auth_cookie', $cookie, 'two_fa' );
591 }
592
593 /**
594 * Disable 2FA TOTP method for the current user. It's not from the list of routes.
595 *
596 * @return Response
597 * @defender_route
598 * @is_public
599 */
600 public function disable_totp(): Response {
601 $user_id = get_current_user_id();
602 // Remove TOTP flag.
603 delete_user_meta( $user_id, Totp::TOTP_AUTH_KEY );
604 // Remove old secret key.
605 delete_user_meta( $user_id, Totp::TOTP_SECRET_KEY );
606 // Remove new secret key.
607 delete_user_meta( $user_id, Totp::TOTP_SODIUM_SECRET_KEY );
608 // Remove TOTP from enabled providers.
609 $enabled_providers = get_user_meta( $user_id, Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY, true );
610 if ( is_array( $enabled_providers ) && array() !== $enabled_providers ) {
611 foreach ( $enabled_providers as $key => $slug ) {
612 if ( Totp::$slug === $slug ) {
613 unset( $enabled_providers[ $key ] );
614 break;
615 }
616 }
617 } else {
618 $enabled_providers = '';
619 }
620 update_user_meta( $user_id, Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY, $enabled_providers );
621 // Check the default provider. If it's TOTP then clear the value.
622 $default_provider = get_user_meta( $user_id, Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY, true );
623 if ( is_string( $default_provider ) && '' !== trim( $default_provider ) && Totp::$slug === $default_provider ) {
624 update_user_meta( $user_id, Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY, '' );
625 }
626
627 return new Response( true, array() );
628 }
629
630 /**
631 * Verify the OTP and enable 2-factor authentication for the currently logged in user.
632 *
633 * @param Request $request The request object containing the OTP and setup key.
634 *
635 * @return Response A response object indicating the success or failure of the operation.
636 * If successful, the response does not contain any data.
637 * If failed, the response contains an error message.
638 * @defender_route
639 * @is_public
640 * @throws SodiumException Exceptions thrown by the sodium functions.
641 */
642 public function verify_otp_for_enabling( Request $request ): Response {
643 if ( is_user_logged_in() ) {
644 $data = $request->get_data();
645 $otp = isset( $data['otp'] ) ? sanitize_text_field( $data['otp'] ) : false;
646 if ( false === $otp || strlen( $otp ) < 6 ) {
647 return new Response(
648 false,
649 array( 'message' => esc_html__( 'Please input a valid OTP code.', 'defender-security' ) )
650 );
651 }
652 // Get the setup key.
653 $setup_key = $data['setup_key'] ?? false;
654 if ( ! $setup_key ) {
655 return new Response(
656 false,
657 array( 'message' => esc_html__( 'The setup key is incorrect.', 'defender-security' ) )
658 );
659 }
660 $user_id = get_current_user_id();
661 $result = TOTP::verify_otp( $otp, $user_id, $setup_key );
662 // OTP result can be a boolean value or WP error.
663 if ( is_wp_error( $result ) ) {
664 return new Response(
665 false,
666 array( 'message' => $result->get_error_message() )
667 );
668 }
669 if ( $result ) {
670 // Save a setup key.
671 $result = Totp::save_setup_key( $user_id, $setup_key );
672 if ( is_wp_error( $result ) ) {
673 return new Response(
674 false,
675 array( 'message' => $result->get_error_message() )
676 );
677 }
678 // Enable OTP.
679 $this->service->enable_otp( $user_id );
680 $totp_slug = Totp::$slug;
681 // Add TOTP to enabled providers.
682 $enabled_providers = get_user_meta( $user_id, Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY, true );
683 if ( is_array( $enabled_providers ) && array() !== $enabled_providers ) {
684 // Array of enabled providers is not empty now.
685 if ( ! in_array( Totp::$slug, $enabled_providers, true ) ) {
686 $enabled_providers[] = $totp_slug;
687 update_user_meta( $user_id, Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY, $enabled_providers );
688 }
689 } else {
690 // Array of enabled providers is empty now.
691 update_user_meta( $user_id, Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY, array( $totp_slug ) );
692 }
693 // If no default provider then add TOTP as it.
694 $default_provider = get_user_meta( $user_id, Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY, true );
695 if ( ! is_string( $default_provider ) || '' === trim( $default_provider ) ) {
696 update_user_meta( $user_id, Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY, $totp_slug );
697 }
698
699 return new Response( true, array() );
700 } else {
701 return new Response(
702 false,
703 array( 'message' => esc_html__( 'Your OTP code is incorrect. Please try again.', 'defender-security' ) )
704 );
705 }
706 }
707 return new Response( true, array() );
708 }
709
710 /**
711 * Clear 2FA providers for the given user.
712 *
713 * @param int $user_id User ID.
714 */
715 protected function clear_providers( int $user_id ): void {
716 update_user_meta( $user_id, Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY, '' );
717 update_user_meta( $user_id, Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY, '' );
718 }
719
720 /**
721 * Updates the user's 2FA profile with the selected providers and default provider.
722 *
723 * @param int $user_id The ID of the user whose profile is being updated.
724 *
725 * @return void
726 */
727 public function profile_update( int $user_id ) {
728 $post_data = defender_get_data_from_request( null, 'p' );
729 if ( isset( $post_data['_wpdef_2fa_nonce_user_options'] ) ) {
730 check_admin_referer( 'wpdef_2fa_user_options', '_wpdef_2fa_nonce_user_options' );
731
732 if (
733 ! isset( $post_data[ Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY ] )
734 || ! is_array( $post_data[ Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY ] )
735 ) {
736 return;
737 }
738 // Remove empty elements.
739 $checked_providers = array_diff( $post_data[ Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY ], array( '' ) );
740 // If no option is checked then the values for default provider and enabled providers are cleared.
741 if ( array() === $checked_providers ) {
742 $this->clear_providers( $user_id );
743
744 return;
745 }
746
747 $providers = $this->service->get_providers();
748 // For Fallback-Email method: the email value should be not empty and valid.
749 if ( in_array( Fallback_Email::$slug, $checked_providers, true ) ) {
750 $email = HTTP::post( 'def_2fa_backup_email' );
751 if ( is_string( $email ) && '' !== trim( $email ) && filter_var( $email, FILTER_VALIDATE_EMAIL ) ) {
752 update_user_meta( $user_id, Fallback_Email::FALLBACK_EMAIL_KEY, $email );
753 } else {
754 unset( $checked_providers[ Fallback_Email::$slug ] );
755 }
756 }
757
758 // For Webauthn method: a user must have at least once device registered.
759 $key = array_search( Webauthn::$slug, $checked_providers, true );
760 if ( false !== $key ) {
761 $user_authenticators = wd_di()->get( Webauthn_Controller::class )->get_current_user_authenticators();
762 if ( 0 === count( $user_authenticators ) ) {
763 unset( $checked_providers[ $key ] );
764 }
765 }
766 // Case when WebAuthn is checked but no registered devices OR Fallback_Email has an invalid email value.
767 if ( array() === $checked_providers ) {
768 $this->clear_providers( $user_id );
769
770 return;
771 }
772
773 // Current user.
774 $user = get_user_by( 'id', $user_id );
775 // Enable only the available providers.
776 $enabled_providers = array();
777 foreach ( $providers as $slug => $provider ) {
778 if ( in_array( $slug, $checked_providers, true ) && $provider->is_available_for_user( $user ) ) {
779 $enabled_providers[] = $slug;
780 }
781 }
782 update_user_meta( $user_id, Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY, $enabled_providers );
783 /**
784 * Fires when 2fa providers are enabled.
785 *
786 * @since 4.3.0
787 */
788 do_action( 'wd_2fa_enabled_provider_slugs', $enabled_providers );
789 // Default provider must be enabled.
790 $default_provider = $post_data[ Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY ] ?? '';
791 // The case#1 when all 2fa providers were deactivated before.
792 if ( ! is_string( $default_provider ) || '' === trim( $default_provider ) ) {
793 $default_provider = $enabled_providers[0];
794 }
795 // The case#2 when prev default provider is deactivated and another one is activated.
796 if ( ! in_array( $default_provider, $checked_providers, true ) ) {
797 $default_provider = $enabled_providers[0];
798 }
799 // Save default provider.
800 update_user_meta( $user_id, Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY, $default_provider );
801 }
802 }
803
804 /**
805 * A simple filter to show activate 2fa screen on profile page.
806 *
807 * @param WP_User $user The current WP_User object.
808 *
809 * @return void
810 */
811 public function show_user_profile( WP_User $user ): void {
812 $user_roles = $this->get_roles( $user );
813 // This method is better than is_intersected_arrays() because it is flexibly controlled with a nested hook.
814 if ( is_array( $user_roles ) && array() !== $user_roles && $this->service->is_auth_enable_for( $user, $this->model->user_roles ) ) {
815 wp_enqueue_style( 'defender-profile-2fa', defender_asset_url( '/assets/css/two-factor.css' ), array(), DEFENDER_VERSION );
816
817 $webauthn_controller = wd_di()->get( Webauthn_Controller::class );
818 $webauthn_requirements = $this->check_webauthn_requirements();
819 if ( $this->service->is_checked_enabled_provider_by_slug(
820 $user,
821 Webauthn::$slug
822 ) && ! $webauthn_requirements ) {
823 $this->service->remove_enabled_provider_for_user( Webauthn::$slug, $user );
824 }
825
826 $webauthn_user_handle_match_failed = wd_di()->get( Webauthn_Component::class )->getUserHandleMatchFailed( $user->ID );
827
828 wp_enqueue_script(
829 'wpdef_webauthn_common_script',
830 plugins_url( 'assets/js/webauthn-common.js', WP_DEFENDER_FILE ),
831 array(),
832 DEFENDER_VERSION,
833 true
834 );
835 wp_enqueue_script(
836 'wpdef_webauthn_script',
837 plugins_url( 'assets/js/webauthn.js', WP_DEFENDER_FILE ),
838 array(
839 'jquery',
840 'wpdef_webauthn_common_script',
841 ),
842 DEFENDER_VERSION,
843 true
844 );
845 wp_localize_script(
846 'wpdef_webauthn_script',
847 'webauthn',
848 array(
849 'admin_url' => admin_url( 'admin-ajax.php' ),
850 'nonce' => wp_create_nonce( 'wpdef_webauthn' ),
851 'i18n' => $webauthn_controller->get_translations(),
852 'registered_auths' => $webauthn_controller->get_current_user_authenticators(),
853 'username' => isset( $user->user_login ) && is_string( $user->user_login ) ? $user->user_login : '',
854 'user_handle_match_failed' => $webauthn_user_handle_match_failed,
855 )
856 );
857
858 $forced_auth = $this->service->is_intersected_arrays(
859 $user_roles,
860 $this->model->force_auth_roles
861 );
862 $default_values = $this->model->get_default_values();
863 $enabled_providers = $this->service->get_available_providers_for_user( $user );
864 $enabled_provider_slugs = array() !== $enabled_providers ? array_keys( $enabled_providers ) : array();
865 $default_provider_slug = $this->service->get_default_provider_slug_for_user( $user->ID );
866 $webauthn_enabled = $this->service->is_checked_enabled_provider_by_slug( $user, Webauthn::$slug );
867
868 $this->render_partial(
869 'two-fa/user-options',
870 array(
871 'is_force_auth' => $forced_auth && $this->model->force_auth && array() === $enabled_providers,
872 'force_auth_message' => $this->model->force_auth_mess,
873 'default_message' => $default_values['message'],
874 'user' => $user,
875 'all_providers' => $this->service->get_providers(),
876 'enabled_providers_key' => Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY,
877 'default_provider_key' => Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY,
878 'checked_provider_slugs' => $enabled_provider_slugs,
879 'checked_def_provider_slug' => is_string( $default_provider_slug ) && '' !== trim( $default_provider_slug ) ? $default_provider_slug : null,
880 'webauthn_requirements' => $webauthn_requirements,
881 'webauthn_enabled' => $webauthn_enabled,
882 'webauthn_slug' => Webauthn::$slug,
883 'is_admin' => is_admin(),
884 )
885 );
886 }
887 }
888
889 /**
890 * Save settings.
891 *
892 * @param Request $request The request object containing new settings data.
893 *
894 * @return Response
895 * @defender_route
896 */
897 public function save_settings( Request $request ): Response {
898 $model = $this->model;
899 $data = $request->get_data();
900 $model->import( $data );
901 if ( $model->validate() ) {
902 $model->save();
903 Config_Hub_Helper::set_clear_active_flag();
904
905 return new Response(
906 true,
907 array_merge(
908 array(
909 'message' => esc_html__( 'Your settings have been updated.', 'defender-security' ),
910 'auto_close' => true,
911 ),
912 $this->data_frontend()
913 )
914 );
915 }
916
917 return new Response(
918 false,
919 array( 'message' => $model->get_formatted_errors() )
920 );
921 }
922
923 /**
924 * Enqueues scripts and styles for this page.
925 * Only enqueues assets if the page is active.
926 */
927 public function enqueue_assets() {
928 if ( ! $this->is_page_active() ) {
929 return;
930 }
931 wp_enqueue_script( 'clipboard' );
932 wp_enqueue_media();
933 wp_localize_script( 'def-2fa', 'two_fa', $this->data_frontend() );
934 wp_enqueue_script( 'def-2fa' );
935 $this->enqueue_main_assets();
936 }
937
938 /**
939 * Send test email, use in settings screen.
940 *
941 * @param Request $request Request object.
942 *
943 * @return Response
944 * @defender_route
945 */
946 public function send_test_email( Request $request ): Response {
947 $data = $request->get_data(
948 array(
949 'email_subject' => array(
950 'type' => 'string',
951 'sanitize' => 'sanitize_text_field',
952 ),
953 'email_sender' => array(
954 'type' => 'string',
955 'sanitize' => 'sanitize_text_field',
956 ),
957 'email_body' => array(
958 'type' => 'string',
959 'sanitize' => 'wp_kses_post',
960 ),
961 )
962 );
963
964 $subject = $data['email_subject'];
965 $sender = $data['email_sender'];
966 $body = $this->render_partial(
967 'email/2fa-lost-phone',
968 array(
969 'body' => $data['email_body'],
970 ),
971 false
972 );
973
974 $params = array(
975 'passcode' => '[a-sample-passcode]',
976 'display_name' => $this->get_user_display( get_current_user_id() ),
977 );
978
979 foreach ( $params as $key => $param ) {
980 if ( 'passcode' === $key ) {
981 $body = str_replace( "{{{$key}}}", '<span class="defender-otp">' . $param . '</span>', $body );
982 } else {
983 $body = str_replace( "{{{$key}}}", $param, $body );
984 }
985 }
986 $headers = array( 'Content-Type: text/html; charset=UTF-8' );
987 if ( is_string( $sender ) && '' !== trim( $sender ) ) {
988 // Since v5.2.0.
989 $from_email = defender_noreply_email( 'wd_two_fa_totp_noreply_email' );
990 $headers[] = sprintf( 'From: %s <%s>', $sender, $from_email );
991 } else {
992 return new Response(
993 false,
994 array( 'message' => esc_html__( 'Sender value cannot be empty.', 'defender-security' ) )
995 );
996 }
997 // Main email template.
998 $body = $this->render_partial(
999 'email/index',
1000 array(
1001 'title' => Two_Fa::get_module_name(),
1002 'content_body' => $body,
1003 // An empty value because 2FA-email is sent after a manual click from the user.
1004 'unsubscribe_link' => '',
1005 ),
1006 false
1007 );
1008
1009 $send_mail = wp_mail( Fallback_Email::get_backup_email(), $subject, $body, $headers );
1010 if ( $send_mail ) {
1011 return new Response(
1012 true,
1013 array( 'message' => esc_html__( 'Test email has been sent to your email.', 'defender-security' ) )
1014 );
1015 } else {
1016 return new Response(
1017 false,
1018 array( 'message' => esc_html__( 'Test email failed.', 'defender-security' ) )
1019 );
1020 }
1021 }
1022
1023 /**
1024 * Converts the current object state to an array.
1025 *
1026 * @return array The array representation of the object.
1027 */
1028 public function to_array(): array {
1029 $settings = new Two_Fa();
1030 [ $routes, $nonces ] = Route::export_routes( 'two_fa' );
1031
1032 return array(
1033 'enabled' => $settings->enabled,
1034 'useable' => $settings->enabled && count( $settings->user_roles ),
1035 'nonces' => $nonces,
1036 'endpoints' => $routes,
1037 );
1038 }
1039
1040 /**
1041 * Renders the main view.
1042 */
1043 public function main_view(): void {
1044 $this->render( 'main' );
1045 }
1046
1047 /**
1048 * Removes settings for all submodules.
1049 */
1050 public function remove_settings(): void {
1051 ( new Two_Fa() )->delete();
1052 }
1053
1054 /**
1055 * Delete all the data & the cache.
1056 */
1057 public function remove_data(): void {
1058 global $wpdb;
1059
1060 $keys = array(
1061 Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY,
1062 Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY,
1063 // From Totp.
1064 'wd_2fa_attempt_' . TOTP::$slug,
1065 TOTP::TOTP_AUTH_KEY,
1066 // For backward compatible with the def.key file. We'll remove this key in future versions and use the key for Sodium.
1067 TOTP::TOTP_SECRET_KEY,
1068 TOTP::TOTP_SODIUM_SECRET_KEY,
1069 TOTP::TOTP_FORCE_KEY,
1070 // From Backup_Codes.
1071 'wd_2fa_attempt_' . Backup_Codes::$slug,
1072 Backup_Codes::BACKUP_CODE_START,
1073 Backup_Codes::BACKUP_CODE_VALUES,
1074 // From Fallback_Email.
1075 'wd_2fa_attempt_' . Fallback_Email::$slug,
1076 Fallback_Email::FALLBACK_EMAIL_KEY,
1077 Fallback_Email::FALLBACK_BACKUP_CODE_KEY,
1078 );
1079 $sql = "DELETE FROM {$wpdb->usermeta} WHERE meta_key IN (" . implode(
1080 ',',
1081 array_fill( 0, count( $keys ), '%s' )
1082 ) . ');';
1083 $query = call_user_func_array( array( $wpdb, 'prepare' ), array_merge( array( $sql ), $keys ) );
1084 $wpdb->query( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery
1085 // From Webauthn.
1086 wd_di()->get( Webauthn_Controller::class )->remove_data();
1087 // Check if 2fa file exists.
1088 $file = $this->get_2fa_lock_path();
1089 if ( is_file( $file ) && is_readable( $file ) ) {
1090 // Delete 2fa file. It's actual for prev v3.3.1.
1091 wp_delete_file( $file );
1092 }
1093 // Check if the file with a random key exists.
1094 $file = Crypt::get_path_to_key_file();
1095 if ( is_file( $file ) && is_readable( $file ) ) {
1096 wp_delete_file( $file );
1097 }
1098 // Remove cached data.
1099 Array_Cache::remove( 'auth_cookie', 'two_fa' );
1100 Array_Cache::remove( 'providers', 'two_fa' );
1101 }
1102
1103 /**
1104 * Filters users by 2FA option.
1105 *
1106 * @param WP_User_Query $query The user query object.
1107 *
1108 * @return void
1109 */
1110 public function filter_users_by_2fa( $query ): void {
1111 global $pagenow;
1112
1113 $is_enabled = defender_get_data_from_request( 'wpdef_two_fa', 'g' );
1114 if ( is_admin() && 'users.php' === $pagenow && 'enabled' === $is_enabled ) {
1115 $query->set(
1116 'meta_query',
1117 array(
1118 array(
1119 'key' => Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY,
1120 'value' => array_keys( $this->service->get_providers() ),
1121 'compare' => 'IN',
1122 ),
1123 )
1124 );
1125 }
1126 }
1127
1128 /**
1129 * Provides data for the frontend.
1130 *
1131 * @return array An array of data for the frontend.
1132 */
1133 public function data_frontend(): array {
1134 return array_merge(
1135 array(
1136 'model' => $this->model->export(),
1137 'all_roles' => $this->get_all_editable_roles(),
1138 'count' => $this->service->count_users_with_enabled_2fa(),
1139 'notices' => $this->compatibility_notices,
1140 'count_checked_roles' => count( $this->model->user_roles ),
1141 'is_woo_active' => $this->is_woo_activated,
1142 // The multisite check is an isolated case now. If it will be needed for several modules, then a more global scope is needed.
1143 'is_multisite' => is_multisite(),
1144 'module_name' => Two_Fa::get_module_name(),
1145 'hub_connector' => wd_di()->get( Hub_Connector::class )->data_frontend(),
1146 'antibot' => wd_di()->get( Antibot_Global_Firewall::class )->data_frontend(),
1147 ),
1148 $this->dump_routes_and_nonces()
1149 );
1150 }
1151
1152 /**
1153 * Imports data into the model.
1154 *
1155 * @param array $data Data to be imported into the model.
1156 *
1157 * @throws Exception If table is not defined.
1158 */
1159 public function import_data( array $data ) {
1160 $model = new Two_Fa();
1161
1162 $model->import( $data );
1163 /**
1164 * Sometime, the custom image broken on import. When that happen, we will revert to the default image.
1165 */
1166 $model->custom_graphic_url = $this->service->get_custom_graphic_url( $model->custom_graphic_url );
1167 if ( $model->validate() ) {
1168 $model->save();
1169 }
1170 }
1171
1172 /**
1173 * Exports strings.
1174 *
1175 * @return array An array of strings.
1176 */
1177 public function export_strings(): array {
1178 $settings = new Two_Fa();
1179
1180 return array(
1181 $settings->enabled ? esc_html__( 'Active', 'defender-security' ) : esc_html__( 'Inactive', 'defender-security' ),
1182 );
1183 }
1184
1185 /**
1186 * Generates configuration strings based on the provided configuration and
1187 * whether the product is a pro version.
1188 *
1189 * @param array $config Configuration data.
1190 * @param bool $is_pro Indicates if the product is a pro version.
1191 *
1192 * @return array Returns an array of configuration strings.
1193 */
1194 public function config_strings( array $config, bool $is_pro ): array {
1195 return array(
1196 $config['enabled'] ? esc_html__( 'Active', 'defender-security' ) : esc_html__( 'Inactive', 'defender-security' ),
1197 );
1198 }
1199
1200 /**
1201 * WooCommerce prevents any user who cannot 'edit_posts' (subscribers, customers etc.) from accessing admin.
1202 * Here we are disabling WooCommerce default behavior, if force 2FA is enabled.
1203 *
1204 * @param bool $prevent Prevent admin access.
1205 *
1206 * @return bool|null
1207 */
1208 public function handle_woocommerce_prevent_admin_access( bool $prevent ) {
1209 $user = $this->current_user;
1210 if ( ! is_object( $user ) ) {
1211 return;
1212 }
1213 // Is User role from common list checked?
1214 if ( false === $this->service->is_auth_enable_for( $user, $this->model->user_roles ) ) {
1215 return $prevent;
1216 }
1217 // Is 'Force Authentication' checked?
1218 if ( false === $this->model->force_auth ) {
1219 return $prevent;
1220 }
1221 // Is User role from forced list checked?
1222 if ( $this->service->is_force_auth_enable_for( $user->ID, $this->model->force_auth_roles ) ) {
1223 return false;
1224 }
1225 // Is TOTP saved with a passcode?
1226 if ( array() !== $this->service->get_available_providers_for_user( $user ) ) {
1227 return $prevent;
1228 }
1229
1230 return $prevent;
1231 }
1232
1233 /**
1234 * WooCommerce specific hooks.
1235 *
1236 * @return void
1237 */
1238 private function woocommerce_hooks(): void {
1239 // This filter added only for disable WooCommerce default behavior.
1240 add_filter(
1241 'woocommerce_prevent_admin_access',
1242 array(
1243 $this,
1244 'handle_woocommerce_prevent_admin_access',
1245 ),
1246 10,
1247 1
1248 );
1249 // Handle WooCommerce MyAccount page login redirect.
1250 add_filter( 'woocommerce_login_redirect', array( $this, 'handle_woocommerce_login_redirect' ), 10, 2 );
1251 // Add field.
1252 add_action( 'woocommerce_login_form_end', array( $this, 'add_redirect_to_input' ) );
1253 }
1254
1255 /**
1256 * WooCommerce by default redirect users to My-account page.
1257 * Here we are checking force 2FA is enabled or not.
1258 *
1259 * @param string $redirect Redirect URL.
1260 * @param WP_User $user Logged-in user.
1261 *
1262 * @return string
1263 */
1264 public function handle_woocommerce_login_redirect( string $redirect, WP_User $user ) {
1265 // Is User role from common list checked?
1266 if ( false === $this->service->is_auth_enable_for( $user, $this->model->user_roles ) ) {
1267 return $redirect;
1268 }
1269 // Is 'Force Authentication' checked?
1270 if ( false === $this->model->force_auth ) {
1271 return $redirect;
1272 }
1273 // Is User role from forced list checked?
1274 if ( ! $this->service->is_force_auth_enable_for( $user->ID, $this->model->force_auth_roles ) ) {
1275 return $redirect;
1276 }
1277 // Is TOTP saved with a passcode?
1278 if ( array() === $this->service->get_available_providers_for_user( $user ) ) {
1279 return admin_url( 'profile.php' ) . '#defender-security';
1280 }
1281
1282 return $redirect;
1283 }
1284
1285 /**
1286 * Return redirect URL after 2FA submit.
1287 */
1288 private function redirect_url() {
1289 return HTTP::post( 'redirect_to', defender_get_request_url() );
1290 }
1291
1292 /**
1293 * Adds redirect_to hidden input to Woo login.
1294 *
1295 * @return void
1296 */
1297 public function add_redirect_to_input(): void {
1298 echo '<input type="hidden" name="redirect_to" value="' . esc_url_raw( defender_get_request_url() ) . '">';
1299 }
1300
1301 /**
1302 * Generate Backup codes on Profile page.
1303 *
1304 * @return Response
1305 * @defender_route
1306 * @is_public
1307 */
1308 public function generate_backup_codes(): Response {
1309 $user = wp_get_current_user();
1310
1311 return new Response(
1312 true,
1313 array(
1314 'codes' => Backup_Codes::generate_codes( $user ),
1315 'count' => Backup_Codes::display_number_of_codes( Backup_Codes::get_unused_codes_for_user( $user ) ),
1316 'title' => sprintf(
1317 /* translators: %s: count */
1318 esc_html__( '2FA Backup Codes for %s:', 'defender-security' ),
1319 get_bloginfo( 'url' )
1320 ),
1321 'button_text' => esc_html__( 'Get New Codes', 'defender-security' ),
1322 'description' => esc_html__( 'Each backup code can only be used to log in once.', 'defender-security' ),
1323 )
1324 );
1325 }
1326
1327 /**
1328 * Shortcode to display 2FA user settings.
1329 *
1330 * @return void
1331 * @since 3.2.0
1332 */
1333 public function display_2fa_user_settings(): void {
1334 if ( ( ! is_admin() || defined( 'DOING_AJAX' ) || defined( 'DOING_CRON' ) ) ) {
1335 wp_enqueue_script( 'wp-i18n' );
1336
1337 do_action( 'wd_2fa_form_before' );
1338
1339 echo '<form class="wpdef-2fa-wrap" action="" method="post">';
1340
1341 $this->show_user_profile( $this->current_user );
1342
1343 echo '<input type="hidden" name="action" value="save_def_2fa_user_settings" />';
1344 echo '<button type="submit" class="button" name="save_def_2fa_user_settings" value="' . esc_attr__(
1345 'Save changes',
1346 'defender-security'
1347 ) . '">'
1348 . esc_html__( 'Save changes', 'defender-security' ) . '</button>';
1349 echo '</form>';
1350
1351 do_action( 'wd_2fa_form_after' );
1352 } else {
1353 apply_filters( 'wd_2fa_form_when_not_logged_in', '' );
1354 }
1355 }
1356
1357 /**
1358 * 1. Register new endpoint (URL) for My Account page. Re-save Permalinks or it will give 404 error.
1359 *
1360 * @return void
1361 */
1362 public function wp_defender_2fa_endpoint(): void {
1363 add_rewrite_endpoint( $this->slug, EP_PERMALINK | EP_PAGES );
1364 flush_rewrite_rules();
1365 }
1366
1367 /**
1368 * 2. dds the slug of the current instance to the given array of query variables.
1369 *
1370 * @param array $vars The array of query variables.
1371 *
1372 * @return array The updated array of query variables.
1373 */
1374 public function wp_defender_2fa_query_vars( $vars ) {
1375 $vars[] = $this->slug;
1376
1377 return $vars;
1378 }
1379
1380 /**
1381 * 3. Inserts the new endpoint into the My Account menu.
1382 *
1383 * @param array $items The array of items in the My Account menu.
1384 *
1385 * @return array The updated array of items with the new endpoint inserted.
1386 */
1387 public function wp_defender_2fa_link_my_account( $items ) {
1388 $needed_place = is_array( $items ) && array() !== $items ? ( count( $items ) - 1 ) : 0;
1389
1390 return array_slice( $items, 0, $needed_place, true )
1391 + array( $this->slug => esc_html__( '2FA', 'defender-security' ) )
1392 + array_slice( $items, $needed_place, null, true );
1393 }
1394
1395 /**
1396 * 4. Add content to the new tab.
1397 *
1398 * @return void
1399 */
1400 public function wp_defender_2fa_content(): void {
1401 echo do_shortcode( '[wp_defender_2fa_user_settings]' );
1402 }
1403
1404 /**
1405 * Save the 2fa details and redirect back to 'My Account' page.
1406 *
1407 * @return void
1408 */
1409 public function save_2fa_details() {
1410 $action = defender_get_data_from_request( 'action', 'p' );
1411 if ( ! is_string( $action ) || '' === trim( $action ) || 'save_def_2fa_user_settings' !== $action ) {
1412 return;
1413 }
1414
1415 wc_nocache_headers();
1416
1417 $user_id = $this->current_user->ID;
1418 if ( $user_id <= 0 ) {
1419 return;
1420 }
1421 // Verify nonce and other two-factor arguments passed.
1422 $this->profile_update( $user_id );
1423
1424 wc_add_notice( esc_html__( 'Two-Factor settings updated successfully.', 'defender-security' ) );
1425 // @since 3.2.0
1426 do_action( 'wd_woocommerce_save_2fa_details', $user_id );
1427
1428 wp_safe_redirect( wc_get_endpoint_url( $this->slug, '', wc_get_page_permalink( 'myaccount' ) ) );
1429 exit;
1430 }
1431
1432 /**
1433 * Enable provider slugs.
1434 *
1435 * @param array $provider_slugs The array of provider slugs to enable.
1436 *
1437 * @return void
1438 */
1439 public function enable_provider_slugs( array $provider_slugs ) {
1440 // Track conditions.
1441 if ( array() !== $provider_slugs ) {
1442 $methods = array();
1443 foreach ( $this->service->get_providers() as $slug => $object ) {
1444 if ( in_array( $slug, $provider_slugs, true ) ) {
1445 $methods[] = $object->get_label();
1446 }
1447 }
1448 // Run track.
1449 $this->track_feature(
1450 'def_2fa_method_activated',
1451 array(
1452 'Method name' => $methods,
1453 )
1454 );
1455 }
1456 }
1457 }
1458