PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / trunk
Defender Security – Malware Scanner, Login Security & Firewall vtrunk
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 3.1.1 All 140 releases
defender-security / src / controller / class-two-factor.php

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

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