PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.2.0
Defender Security – Malware Scanner, Login Security & Firewall v6.2.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 6.2.0, at src/controller/class-two-factor.php

1,449 lines 46.2 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 // Set active user.
343 wp_set_current_user( $user_id, $user->user_login );
344 // Todo: add code for 'rememberme'-option.
345 wp_set_auth_cookie( $user_id, true );
346
347 /**
348 * Fires after successful login via 2fa.
349 *
350 * @param int $user_id @since 2.6.1
351 * @param string $auth_method @since 3.4.0
352 */
353 do_action( 'wpmu_2fa_login', $user_id, $auth_method );
354
355 $interim_login = defender_get_data_from_request( 'interim-login', 'r' );
356 if ( is_string( $interim_login ) && '' !== trim( $interim_login ) ) {
357 $params['interim_login'] = 'success';
358 $params['message'] = '<p class="message">' . esc_html__( 'You have logged in successfully.', 'defender-security' ) . '</p>';
359 $this->render_otp_screen( $params );
360 exit;
361 } else {
362 // Usual success.
363 $redirect = apply_filters(
364 'login_redirect',
365 HTTP::post( 'redirect_to', admin_url() ),
366 $this->redirect_url(),
367 $user
368 );
369 wp_safe_redirect( $redirect );
370 exit;
371 }
372 }
373 }
374 $lockout_message = $this->service->verify_attempt( $user->ID, Totp::$slug );
375
376 $params['error'] = new WP_Error(
377 'opt_fail',
378 '' === trim( $lockout_message )
379 ? esc_html__( 'Whoops, the passcode you entered was incorrect or expired.', 'defender-security' )
380 : $lockout_message
381 );
382 $this->render_otp_screen( $params );
383 exit;
384 }
385
386 /**
387 * Generate a unique token for user.
388 *
389 * @param int $user_id User ID.
390 *
391 * @return string Unique token.
392 */
393 public function get_token( int $user_id ): string {
394 $token = bin2hex( Crypt::random_bytes( 32 ) );
395 update_user_meta( $user_id, Two_Fa_Component::TOKEN_USER_KEY, wp_hash( $user_id . $token ) );
396
397 return $token;
398 }
399
400 /**
401 * Render otp form. Required conditions for the current user:
402 * - is not logged in,
403 * - user data is not empty,
404 * - password matches the user,
405 * - user role is checked on 2FA settings,
406 * - user has at least one 2FA auth method available.
407 *
408 * @param null|WP_User|WP_Error $user Object of the logged-in user.
409 * @param string $username Username or email address.
410 * @param string $password Plain password string.
411 */
412 public function maybe_show_otp_form( $user, string $username, string $password ) {
413 if (
414 ! is_user_logged_in()
415 && '' !== trim( $password ) && $user instanceof WP_User
416 && wp_check_password( $password, $user->data->user_pass, $user->ID )
417 && $this->service->is_auth_enable_for( $user, $this->model->user_roles )
418 && array() !== $this->service->get_available_providers_for_user( $user )
419 ) {
420 $params = array();
421 $cookie = Array_Cache::get( 'auth_cookie', 'two_fa' );
422 if ( null !== $cookie ) {
423 // Clear all session data if any.
424 $session = WP_Session_Tokens::get_instance( $user->ID );
425 $session->destroy( $cookie['token'] );
426 }
427 // Prevent user to login, and show otp screen.
428 wp_clear_auth_cookie();
429 // All goods, we'll need to create a unique token to mark this user.
430 $params['token'] = $this->get_token( $user->ID );
431 $params['password'] = $password;
432 $params['user_id'] = $user->ID;
433 // Get default provider.
434 $params['default_slug'] = $this->service->get_default_provider_slug_for_user( $user->ID );
435 if ( Fallback_Email::$slug === $params['default_slug'] ) {
436 $user_id = isset( $user->ID ) ? $user->ID : 0;
437 $user_id = is_int( $user_id ) ? $user_id : (int) $user_id;
438 $result = $this->service->send_otp_to_email( $params['token'], $user_id );
439 if ( is_wp_error( $result ) ) {
440 $params['error'] = $result;
441 $this->render_otp_screen( $params );
442 }
443 }
444 $this->render_otp_screen( $params );
445 }
446
447 return $user;
448 }
449
450 /**
451 * Render the OTP screen after login successful.
452 *
453 * @param array $params Additional parameters.
454 *
455 * @return void|null
456 */
457 private function render_otp_screen( array $params = array() ) {
458 // Add common styles and scripts to enqueue.
459 wp_enqueue_script( 'jquery' );
460 wp_enqueue_style( 'defender-otp-screen', defender_asset_url( '/assets/css/otp.css' ), array(), DEFENDER_VERSION );
461
462 $params['redirect_to'] = $this->redirect_url();
463 if ( ! isset( $params['error'] ) ) {
464 $params['error'] = null;
465 }
466
467 $this->attach_behavior( WPMUDEV::class, WPMUDEV::class );
468 $params['custom_graphic'] = '';
469 $params['custom_graphic_type'] = $this->model->custom_graphic_type;
470 $custom_graphic = '';
471 $custom_graphic_type = $this->model->custom_graphic_type;
472 $custom_graphic_url = trim( $this->model->custom_graphic_url );
473 $custom_graphic_link = trim( $this->model->custom_graphic_link );
474 if ( $this->model->custom_graphic ) {
475 if ( Two_Fa::CUSTOM_GRAPHIC_TYPE_UPLOAD === $custom_graphic_type && '' !== $custom_graphic_url ) {
476 $custom_graphic = $custom_graphic_url;
477 } elseif ( Two_Fa::CUSTOM_GRAPHIC_TYPE_LINK === $custom_graphic_type && '' !== $custom_graphic_link ) {
478 $custom_graphic = $custom_graphic_link;
479 }
480 }
481
482 $params['custom_graphic'] = $custom_graphic;
483 $params['custom_graphic_type'] = $custom_graphic_type;
484
485 $collection = $this->dump_routes_and_nonces();
486 $routes = $collection['routes'];
487 $nonces = $collection['nonces'];
488
489 $params['providers'] = array();
490 $user = null;
491 if ( isset( $params['user_id'] ) ) {
492 $user = get_user_by( 'id', $params['user_id'] );
493 if ( is_object( $user ) ) {
494 $params['providers'] = $this->service->get_available_providers_for_user( $user );
495 // Get default provider.
496 if ( ! isset( $params['default_slug'] ) || ! is_string( $params['default_slug'] ) || '' === trim( $params['default_slug'] ) ) {
497 $params['default_slug'] = $this->service->get_default_provider_slug_for_user( $user->ID );
498 }
499 }
500 }
501
502 $this->service->remove_actions_for_2fa_screen();
503
504 if (
505 isset( $params['providers'][ Webauthn::$slug ] ) &&
506 false === $params['providers'][ Webauthn::$slug ]->is_otp_screen_available( $user )
507 ) {
508 unset( $params['providers'][ Webauthn::$slug ] );
509 $params['default_slug'] = Webauthn::$slug !== $params['default_slug'] ? $params['default_slug'] : null;
510 }
511
512 if ( 0 === count( $params['providers'] ) ) {
513 // Since 3.5.0.
514 $error_msg = esc_html__( 'No providers.', 'defender-security' );
515 $params['error'] = new WP_Error( 'opt_fail', $error_msg );
516 do_action( 'wd_2fa_otp_params', $params );
517
518 wp_die( esc_html( $error_msg ) );
519 }
520 // Add WebAuthn styles and scripts to enqueue.
521 if ( true === array_key_exists( Webauthn::$slug, $params['providers'] ) ) {
522 wp_enqueue_style(
523 'defender-biometric-login-screen',
524 defender_asset_url( '/assets/css/biometric.css' ),
525 array(),
526 DEFENDER_VERSION
527 );
528 wp_enqueue_script(
529 'wpdef_webauthn_common_script',
530 plugins_url( 'assets/js/webauthn-common.js', WP_DEFENDER_FILE ),
531 array(),
532 DEFENDER_VERSION,
533 true
534 );
535 wp_enqueue_script(
536 'defender-biometric-login-script',
537 plugins_url( 'assets/js/biometric-login.js', WP_DEFENDER_FILE ),
538 array(
539 'jquery',
540 'wpdef_webauthn_common_script',
541 ),
542 DEFENDER_VERSION,
543 true
544 );
545 $webauthn_controller = wd_di()->get( Webauthn_Controller::class );
546 wp_localize_script(
547 'defender-biometric-login-script',
548 'webauthn',
549 array(
550 'admin_url' => admin_url( 'admin-ajax.php' ),
551 'nonce' => wp_create_nonce( 'wpdef_webauthn' ),
552 'i18n' => $webauthn_controller->get_translations(),
553 'username' => isset( $user->user_login ) && is_string( $user->user_login ) && '' !== trim( $user->user_login ) ? $user->user_login : '',
554 'provider_slug' => Webauthn::$slug,
555 )
556 );
557 }
558 // Prepare data.
559 $args = array(
560 'action' => defender_base_action(),
561 '_def_nonce' => $nonces['send_backup_code'],
562 // Add a dummy values to avoid displaying errors, e.g. for the case with null.
563 'route' => $this->check_route( $routes['send_backup_code'] ?? 'test' ),
564 );
565 // If user's session has expired add a new 'interimlogin'-arg.
566 $interim_login = defender_get_data_from_request( 'interim-login', 'r' );
567 if ( is_string( $interim_login ) && '' !== trim( $interim_login ) ) {
568 $args['interimlogin'] = 'yes';
569 }
570 $params['action_fallback_email'] = add_query_arg( $args, admin_url( 'admin-ajax.php' ) );
571
572 // Since 3.5.0.
573 do_action( 'wd_2fa_otp_params', $params );
574
575 $this->render_partial( 'two-fa/otp', $params );
576 exit;
577 }
578
579 /**
580 * Store the session key in the array cache for two-factor authentication.
581 *
582 * @param string $cookie The authentication cookie.
583 *
584 * @return void
585 */
586 public function store_session_key( $cookie ): void {
587 // Clear login cookie to ensure nonce consistency.
588 if ( ! is_user_logged_in() && isset( $_COOKIE[ LOGGED_IN_COOKIE ] ) ) {
589 unset( $_COOKIE[ LOGGED_IN_COOKIE ] );
590 }
591
592 $cookie = wp_parse_auth_cookie( $cookie, 'logged_in' );
593 Array_Cache::set( 'auth_cookie', $cookie, 'two_fa' );
594 }
595
596 /**
597 * Disable 2FA TOTP method for the current user. It's not from the list of routes.
598 *
599 * @return Response
600 * @defender_route
601 * @is_public
602 */
603 public function disable_totp(): Response {
604 $user_id = get_current_user_id();
605 // Remove TOTP flag.
606 delete_user_meta( $user_id, Totp::TOTP_AUTH_KEY );
607 // Remove old secret key.
608 delete_user_meta( $user_id, Totp::TOTP_SECRET_KEY );
609 // Remove new secret key.
610 delete_user_meta( $user_id, Totp::TOTP_SODIUM_SECRET_KEY );
611 // Remove TOTP from enabled providers.
612 $enabled_providers = get_user_meta( $user_id, Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY, true );
613 if ( is_array( $enabled_providers ) && array() !== $enabled_providers ) {
614 foreach ( $enabled_providers as $key => $slug ) {
615 if ( Totp::$slug === $slug ) {
616 unset( $enabled_providers[ $key ] );
617 break;
618 }
619 }
620 } else {
621 $enabled_providers = '';
622 }
623 update_user_meta( $user_id, Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY, $enabled_providers );
624 // Check the default provider. If it's TOTP then clear the value.
625 $default_provider = get_user_meta( $user_id, Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY, true );
626 if ( is_string( $default_provider ) && '' !== trim( $default_provider ) && Totp::$slug === $default_provider ) {
627 update_user_meta( $user_id, Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY, '' );
628 }
629
630 return new Response( true, array() );
631 }
632
633 /**
634 * Verify the OTP and enable 2-factor authentication for the currently logged in user.
635 *
636 * @param Request $request The request object containing the OTP and setup key.
637 *
638 * @return Response A response object indicating the success or failure of the operation.
639 * If successful, the response does not contain any data.
640 * If failed, the response contains an error message.
641 * @defender_route
642 * @is_public
643 * @throws SodiumException Exceptions thrown by the sodium functions.
644 */
645 public function verify_otp_for_enabling( Request $request ): Response {
646 if ( is_user_logged_in() ) {
647 $data = $request->get_data();
648 $otp = isset( $data['otp'] ) ? sanitize_text_field( $data['otp'] ) : false;
649 if ( false === $otp || strlen( $otp ) < 6 ) {
650 return new Response(
651 false,
652 array( 'message' => esc_html__( 'Please input a valid OTP code.', 'defender-security' ) )
653 );
654 }
655 // Get the setup key.
656 $setup_key = $data['setup_key'] ?? false;
657 if ( ! $setup_key ) {
658 return new Response(
659 false,
660 array( 'message' => esc_html__( 'The setup key is incorrect.', 'defender-security' ) )
661 );
662 }
663 $user_id = get_current_user_id();
664 $result = TOTP::verify_otp( $otp, $user_id, $setup_key );
665 // OTP result can be a boolean value or WP error.
666 if ( is_wp_error( $result ) ) {
667 return new Response(
668 false,
669 array( 'message' => $result->get_error_message() )
670 );
671 }
672 if ( $result ) {
673 // Save a setup key.
674 $result = Totp::save_setup_key( $user_id, $setup_key );
675 if ( is_wp_error( $result ) ) {
676 return new Response(
677 false,
678 array( 'message' => $result->get_error_message() )
679 );
680 }
681 // Enable OTP.
682 $this->service->enable_otp( $user_id );
683 $totp_slug = Totp::$slug;
684 // Add TOTP to enabled providers.
685 $enabled_providers = get_user_meta( $user_id, Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY, true );
686 if ( is_array( $enabled_providers ) && array() !== $enabled_providers ) {
687 // Array of enabled providers is not empty now.
688 if ( ! in_array( Totp::$slug, $enabled_providers, true ) ) {
689 $enabled_providers[] = $totp_slug;
690 update_user_meta( $user_id, Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY, $enabled_providers );
691 }
692 } else {
693 // Array of enabled providers is empty now.
694 update_user_meta( $user_id, Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY, array( $totp_slug ) );
695 }
696 // If no default provider then add TOTP as it.
697 $default_provider = get_user_meta( $user_id, Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY, true );
698 if ( ! is_string( $default_provider ) || '' === trim( $default_provider ) ) {
699 update_user_meta( $user_id, Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY, $totp_slug );
700 }
701
702 return new Response( true, array() );
703 } else {
704 return new Response(
705 false,
706 array( 'message' => esc_html__( 'Your OTP code is incorrect. Please try again.', 'defender-security' ) )
707 );
708 }
709 }
710 return new Response( true, array() );
711 }
712
713 /**
714 * Clear 2FA providers for the given user.
715 *
716 * @param int $user_id User ID.
717 */
718 protected function clear_providers( int $user_id ): void {
719 update_user_meta( $user_id, Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY, '' );
720 update_user_meta( $user_id, Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY, '' );
721 }
722
723 /**
724 * Updates the user's 2FA profile with the selected providers and default provider.
725 *
726 * @param int $user_id The ID of the user whose profile is being updated.
727 *
728 * @return void
729 */
730 public function profile_update( int $user_id ) {
731 $post_data = defender_get_data_from_request( null, 'p' );
732 if ( isset( $post_data['_wpdef_2fa_nonce_user_options'] ) ) {
733 check_admin_referer( 'wpdef_2fa_user_options', '_wpdef_2fa_nonce_user_options' );
734
735 if (
736 ! isset( $post_data[ Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY ] )
737 || ! is_array( $post_data[ Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY ] )
738 ) {
739 return;
740 }
741 // Remove empty elements.
742 $checked_providers = array_diff( $post_data[ Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY ], array( '' ) );
743 // If no option is checked then the values for default provider and enabled providers are cleared.
744 if ( array() === $checked_providers ) {
745 $this->clear_providers( $user_id );
746
747 return;
748 }
749
750 $providers = $this->service->get_providers();
751 // For Fallback-Email method: the email value should be not empty and valid.
752 if ( in_array( Fallback_Email::$slug, $checked_providers, true ) ) {
753 $email = HTTP::post( 'def_2fa_backup_email' );
754 if ( is_string( $email ) && '' !== trim( $email ) && filter_var( $email, FILTER_VALIDATE_EMAIL ) ) {
755 update_user_meta( $user_id, Fallback_Email::FALLBACK_EMAIL_KEY, $email );
756 } else {
757 unset( $checked_providers[ Fallback_Email::$slug ] );
758 }
759 }
760
761 // For Webauthn method: a user must have at least once device registered.
762 $key = array_search( Webauthn::$slug, $checked_providers, true );
763 if ( false !== $key ) {
764 $user_authenticators = wd_di()->get( Webauthn_Controller::class )->get_current_user_authenticators();
765 if ( 0 === count( $user_authenticators ) ) {
766 unset( $checked_providers[ $key ] );
767 }
768 }
769 // Case when WebAuthn is checked but no registered devices OR Fallback_Email has an invalid email value.
770 if ( array() === $checked_providers ) {
771 $this->clear_providers( $user_id );
772
773 return;
774 }
775
776 // Current user.
777 $user = get_user_by( 'id', $user_id );
778 // Enable only the available providers.
779 $enabled_providers = array();
780 foreach ( $providers as $slug => $provider ) {
781 if ( in_array( $slug, $checked_providers, true ) && $provider->is_available_for_user( $user ) ) {
782 $enabled_providers[] = $slug;
783 }
784 }
785 update_user_meta( $user_id, Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY, $enabled_providers );
786 /**
787 * Fires when 2fa providers are enabled.
788 *
789 * @since 4.3.0
790 */
791 do_action( 'wd_2fa_enabled_provider_slugs', $enabled_providers );
792 // Default provider must be enabled.
793 $default_provider = $post_data[ Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY ] ?? '';
794 // The case#1 when all 2fa providers were deactivated before.
795 if ( ! is_string( $default_provider ) || '' === trim( $default_provider ) ) {
796 $default_provider = $enabled_providers[0];
797 }
798 // The case#2 when prev default provider is deactivated and another one is activated.
799 if ( ! in_array( $default_provider, $checked_providers, true ) ) {
800 $default_provider = $enabled_providers[0];
801 }
802 // Save default provider.
803 update_user_meta( $user_id, Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY, $default_provider );
804 }
805 }
806
807 /**
808 * A simple filter to show activate 2fa screen on profile page.
809 *
810 * @param WP_User $user The current WP_User object.
811 *
812 * @return void
813 */
814 public function show_user_profile( WP_User $user ): void {
815 $user_roles = $this->get_roles( $user );
816 // This method is better than is_intersected_arrays() because it is flexibly controlled with a nested hook.
817 if ( is_array( $user_roles ) && array() !== $user_roles && $this->service->is_auth_enable_for( $user, $this->model->user_roles ) ) {
818 wp_enqueue_style( 'defender-profile-2fa', defender_asset_url( '/assets/css/two-factor.css' ), array(), DEFENDER_VERSION );
819
820 $webauthn_controller = wd_di()->get( Webauthn_Controller::class );
821 $webauthn_requirements = $this->check_webauthn_requirements();
822 if ( $this->service->is_checked_enabled_provider_by_slug(
823 $user,
824 Webauthn::$slug
825 ) && ! $webauthn_requirements ) {
826 $this->service->remove_enabled_provider_for_user( Webauthn::$slug, $user );
827 }
828
829 $webauthn_user_handle_match_failed = wd_di()->get( Webauthn_Component::class )->getUserHandleMatchFailed( $user->ID );
830
831 wp_enqueue_script(
832 'wpdef_webauthn_common_script',
833 plugins_url( 'assets/js/webauthn-common.js', WP_DEFENDER_FILE ),
834 array(),
835 DEFENDER_VERSION,
836 true
837 );
838 wp_enqueue_script(
839 'wpdef_webauthn_script',
840 plugins_url( 'assets/js/webauthn.js', WP_DEFENDER_FILE ),
841 array(
842 'jquery',
843 'wpdef_webauthn_common_script',
844 'wp-i18n',
845 ),
846 DEFENDER_VERSION,
847 true
848 );
849 wp_localize_script(
850 'wpdef_webauthn_script',
851 'webauthn',
852 array(
853 'admin_url' => admin_url( 'admin-ajax.php' ),
854 'nonce' => wp_create_nonce( 'wpdef_webauthn' ),
855 'i18n' => $webauthn_controller->get_translations(),
856 'registered_auths' => $webauthn_controller->get_current_user_authenticators(),
857 'username' => isset( $user->user_login ) && is_string( $user->user_login ) ? $user->user_login : '',
858 'user_handle_match_failed' => $webauthn_user_handle_match_failed,
859 )
860 );
861
862 $forced_auth = $this->service->is_intersected_arrays(
863 $user_roles,
864 $this->model->force_auth_roles
865 );
866 $default_values = $this->model->get_default_values();
867 $enabled_providers = $this->service->get_available_providers_for_user( $user );
868 $enabled_provider_slugs = array() !== $enabled_providers ? array_keys( $enabled_providers ) : array();
869 $default_provider_slug = $this->service->get_default_provider_slug_for_user( $user->ID );
870 $webauthn_enabled = $this->service->is_checked_enabled_provider_by_slug( $user, Webauthn::$slug );
871
872 $this->render_partial(
873 'two-fa/user-options',
874 array(
875 'is_force_auth' => $forced_auth && $this->model->force_auth && array() === $enabled_providers,
876 'force_auth_message' => $this->model->force_auth_mess,
877 'default_message' => $default_values['message'],
878 'user' => $user,
879 'all_providers' => $this->service->get_providers(),
880 'enabled_providers_key' => Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY,
881 'default_provider_key' => Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY,
882 'checked_provider_slugs' => $enabled_provider_slugs,
883 'checked_def_provider_slug' => is_string( $default_provider_slug ) && '' !== trim( $default_provider_slug ) ? $default_provider_slug : null,
884 'webauthn_requirements' => $webauthn_requirements,
885 'webauthn_enabled' => $webauthn_enabled,
886 'webauthn_slug' => Webauthn::$slug,
887 'is_admin' => is_admin(),
888 )
889 );
890 }
891 }
892
893 /**
894 * Save settings.
895 *
896 * @param Request $request The request object containing new settings data.
897 *
898 * @return Response
899 * @defender_route
900 */
901 public function save_settings( Request $request ): Response {
902 $model = $this->model;
903 $data = $request->get_data();
904 $old_detect_woo = $model->detect_woo;
905
906 $model->import( $data );
907 if ( $model->validate() ) {
908 $model->save();
909
910 if ( $old_detect_woo !== $model->detect_woo ) {
911 if ( $model->detect_woo ) {
912 $this->wp_defender_2fa_endpoint();
913 }
914
915 flush_rewrite_rules();
916 }
917
918 Config_Hub_Helper::set_clear_active_flag();
919
920 return new Response(
921 true,
922 array_merge(
923 array(
924 'message' => esc_html__( 'Your settings have been updated.', 'defender-security' ),
925 'auto_close' => true,
926 ),
927 $this->data_frontend()
928 )
929 );
930 }
931
932 return new Response(
933 false,
934 array( 'message' => $model->get_formatted_errors() )
935 );
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 * Removes settings for all submodules.
1042 */
1043 public function remove_settings(): void {
1044 ( new Two_Fa() )->delete();
1045 }
1046
1047 /**
1048 * Delete all the data & the cache.
1049 */
1050 public function remove_data(): void {
1051 global $wpdb;
1052
1053 $keys = array(
1054 Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY,
1055 Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY,
1056 // From Totp.
1057 'wd_2fa_attempt_' . TOTP::$slug,
1058 TOTP::TOTP_AUTH_KEY,
1059 // For backward compatible with the def.key file. We'll remove this key in future versions and use the key for Sodium.
1060 TOTP::TOTP_SECRET_KEY,
1061 TOTP::TOTP_SODIUM_SECRET_KEY,
1062 TOTP::TOTP_FORCE_KEY,
1063 // From Backup_Codes.
1064 'wd_2fa_attempt_' . Backup_Codes::$slug,
1065 Backup_Codes::BACKUP_CODE_START,
1066 Backup_Codes::BACKUP_CODE_VALUES,
1067 // From Fallback_Email.
1068 'wd_2fa_attempt_' . Fallback_Email::$slug,
1069 Fallback_Email::FALLBACK_EMAIL_KEY,
1070 Fallback_Email::FALLBACK_BACKUP_CODE_KEY,
1071 );
1072 $sql = "DELETE FROM {$wpdb->usermeta} WHERE meta_key IN (" . implode(
1073 ',',
1074 array_fill( 0, count( $keys ), '%s' )
1075 ) . ');';
1076 $query = call_user_func_array( array( $wpdb, 'prepare' ), array_merge( array( $sql ), $keys ) );
1077 $wpdb->query( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery
1078 // From Webauthn.
1079 wd_di()->get( Webauthn_Controller::class )->remove_data();
1080 // Check if 2fa file exists.
1081 $file = $this->get_2fa_lock_path();
1082 if ( is_file( $file ) && is_readable( $file ) ) {
1083 // Delete 2fa file. It's actual for prev v3.3.1.
1084 wp_delete_file( $file );
1085 }
1086 // Check if the file with a random key exists.
1087 $file = Crypt::get_path_to_key_file();
1088 if ( is_file( $file ) && is_readable( $file ) ) {
1089 wp_delete_file( $file );
1090 }
1091 // Remove cached data.
1092 Array_Cache::remove( 'auth_cookie', 'two_fa' );
1093 Array_Cache::remove( 'providers', 'two_fa' );
1094 }
1095
1096 /**
1097 * Filters users by 2FA option.
1098 *
1099 * @param WP_User_Query $query The user query object.
1100 *
1101 * @return void
1102 */
1103 public function filter_users_by_2fa( $query ): void {
1104 global $pagenow;
1105
1106 $is_enabled = defender_get_data_from_request( 'wpdef_two_fa', 'g' );
1107 if ( is_admin() && 'users.php' === $pagenow && 'enabled' === $is_enabled ) {
1108 $query->set(
1109 'meta_query',
1110 array(
1111 array(
1112 'key' => Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY,
1113 'value' => array_keys( $this->service->get_providers() ),
1114 'compare' => 'IN',
1115 ),
1116 )
1117 );
1118 }
1119 }
1120
1121 /**
1122 * Provides data for the frontend.
1123 *
1124 * @return array An array of data for the frontend.
1125 */
1126 public function data_frontend(): array {
1127 return array_merge(
1128 array(
1129 'model' => $this->model->export(),
1130 'defaults' => $this->model->get_default_values(),
1131 'all_roles' => $this->get_all_editable_roles(),
1132 'count' => $this->service->count_users_with_enabled_2fa(),
1133 'notices' => $this->compatibility_notices,
1134 'count_checked_roles' => count( $this->model->user_roles ),
1135 'is_woo_active' => $this->is_woo_activated,
1136 // The multisite check is an isolated case now. If it will be needed for several modules, then a more global scope is needed.
1137 'is_multisite' => is_multisite(),
1138 'module_name' => Two_Fa::get_module_name(),
1139 'hub_connector' => wd_di()->get( Hub_Connector::class )->data_frontend(),
1140 'antibot' => wd_di()->get( Antibot_Global_Firewall::class )->data_frontend(),
1141 ),
1142 $this->dump_routes_and_nonces()
1143 );
1144 }
1145
1146 /**
1147 * Imports data into the model.
1148 *
1149 * @param array $data Data to be imported into the model.
1150 *
1151 * @throws Exception If table is not defined.
1152 */
1153 public function import_data( array $data ) {
1154 $model = new Two_Fa();
1155
1156 $model->import( $data );
1157 /**
1158 * Sometime, the custom image broken on import. When that happen, we will revert to the default image.
1159 */
1160 $model->custom_graphic_url = $this->service->get_custom_graphic_url( $model->custom_graphic_url );
1161 if ( $model->validate() ) {
1162 $model->save();
1163 }
1164 }
1165
1166 /**
1167 * Exports strings.
1168 *
1169 * @return array An array of strings.
1170 */
1171 public function export_strings(): array {
1172 $settings = new Two_Fa();
1173
1174 return array(
1175 $settings->enabled ? esc_html__( 'Active', 'defender-security' ) : esc_html__( 'Inactive', 'defender-security' ),
1176 );
1177 }
1178
1179 /**
1180 * Generates configuration strings based on the provided configuration.
1181 *
1182 * @param array $config Configuration data.
1183 *
1184 * @return array Returns an array of configuration strings.
1185 */
1186 public function config_strings( array $config ): array {
1187 return array(
1188 $config['enabled'] ? esc_html__( 'Active', 'defender-security' ) : esc_html__( 'Inactive', 'defender-security' ),
1189 );
1190 }
1191
1192 /**
1193 * WooCommerce prevents any user who cannot 'edit_posts' (subscribers, customers etc.) from accessing admin.
1194 * Here we are disabling WooCommerce default behavior, if force 2FA is enabled.
1195 *
1196 * @param bool $prevent Prevent admin access.
1197 *
1198 * @return bool|null
1199 */
1200 public function handle_woocommerce_prevent_admin_access( bool $prevent ) {
1201 $user = $this->current_user;
1202 if ( ! is_object( $user ) ) {
1203 return;
1204 }
1205 // Is User role from common list checked?
1206 if ( false === $this->service->is_auth_enable_for( $user, $this->model->user_roles ) ) {
1207 return $prevent;
1208 }
1209 // Is 'Force Authentication' checked?
1210 if ( false === $this->model->force_auth ) {
1211 return $prevent;
1212 }
1213 // Is User role from forced list checked?
1214 if ( $this->service->is_force_auth_enable_for( $user->ID, $this->model->force_auth_roles ) ) {
1215 return false;
1216 }
1217 // Is TOTP saved with a passcode?
1218 if ( array() !== $this->service->get_available_providers_for_user( $user ) ) {
1219 return $prevent;
1220 }
1221
1222 return $prevent;
1223 }
1224
1225 /**
1226 * WooCommerce specific hooks.
1227 *
1228 * @return void
1229 */
1230 private function woocommerce_hooks(): void {
1231 // This filter added only for disable WooCommerce default behavior.
1232 add_filter(
1233 'woocommerce_prevent_admin_access',
1234 array(
1235 $this,
1236 'handle_woocommerce_prevent_admin_access',
1237 ),
1238 10,
1239 1
1240 );
1241 // Handle WooCommerce MyAccount page login redirect.
1242 add_filter( 'woocommerce_login_redirect', array( $this, 'handle_woocommerce_login_redirect' ), 10, 2 );
1243 // Add field.
1244 add_action( 'woocommerce_login_form_end', array( $this, 'add_redirect_to_input' ) );
1245 }
1246
1247 /**
1248 * WooCommerce by default redirect users to My-account page.
1249 * Here we are checking force 2FA is enabled or not.
1250 *
1251 * @param string $redirect Redirect URL.
1252 * @param WP_User $user Logged-in user.
1253 *
1254 * @return string
1255 */
1256 public function handle_woocommerce_login_redirect( string $redirect, WP_User $user ) {
1257 // Is User role from common list checked?
1258 if ( false === $this->service->is_auth_enable_for( $user, $this->model->user_roles ) ) {
1259 return $redirect;
1260 }
1261 // Is 'Force Authentication' checked?
1262 if ( false === $this->model->force_auth ) {
1263 return $redirect;
1264 }
1265 // Is User role from forced list checked?
1266 if ( ! $this->service->is_force_auth_enable_for( $user->ID, $this->model->force_auth_roles ) ) {
1267 return $redirect;
1268 }
1269 // Is TOTP saved with a passcode?
1270 if ( array() === $this->service->get_available_providers_for_user( $user ) ) {
1271 return admin_url( 'profile.php' ) . '#defender-security';
1272 }
1273
1274 return $redirect;
1275 }
1276
1277 /**
1278 * Return redirect URL after 2FA submit.
1279 */
1280 private function redirect_url() {
1281 return HTTP::post( 'redirect_to', defender_get_request_url() );
1282 }
1283
1284 /**
1285 * Adds redirect_to hidden input to Woo login.
1286 *
1287 * @return void
1288 */
1289 public function add_redirect_to_input(): void {
1290 echo '<input type="hidden" name="redirect_to" value="' . esc_url_raw( defender_get_request_url() ) . '">';
1291 }
1292
1293 /**
1294 * Generate Backup codes on Profile page.
1295 *
1296 * @return Response
1297 * @defender_route
1298 * @is_public
1299 */
1300 public function generate_backup_codes(): Response {
1301 $user = wp_get_current_user();
1302
1303 return new Response(
1304 true,
1305 array(
1306 'codes' => Backup_Codes::generate_codes( $user ),
1307 'count' => Backup_Codes::display_number_of_codes( Backup_Codes::get_unused_codes_for_user( $user ) ),
1308 'title' => sprintf(
1309 /* translators: %s: count */
1310 esc_html__( '2FA Backup Codes for %s:', 'defender-security' ),
1311 get_bloginfo( 'url' )
1312 ),
1313 'button_text' => esc_html__( 'Get New Codes', 'defender-security' ),
1314 'description' => esc_html__( 'Each backup code can only be used to log in once.', 'defender-security' ),
1315 )
1316 );
1317 }
1318
1319 /**
1320 * Shortcode to display 2FA user settings.
1321 *
1322 * @return void
1323 * @since 3.2.0
1324 */
1325 public function display_2fa_user_settings(): void {
1326 if ( ( ! is_admin() || defined( 'DOING_AJAX' ) || defined( 'DOING_CRON' ) ) ) {
1327 wp_enqueue_script( 'wp-i18n' );
1328
1329 do_action( 'wd_2fa_form_before' );
1330
1331 echo '<form class="wpdef-2fa-wrap" action="" method="post">';
1332
1333 $this->show_user_profile( $this->current_user );
1334
1335 echo '<input type="hidden" name="action" value="save_def_2fa_user_settings" />';
1336 echo '<button type="submit" class="button" name="save_def_2fa_user_settings" value="' . esc_attr__(
1337 'Save changes',
1338 'defender-security'
1339 ) . '">'
1340 . esc_html__( 'Save changes', 'defender-security' ) . '</button>';
1341 echo '</form>';
1342
1343 do_action( 'wd_2fa_form_after' );
1344 } else {
1345 apply_filters( 'wd_2fa_form_when_not_logged_in', '' );
1346 }
1347 }
1348
1349 /**
1350 * 1. Register new endpoint (URL) for My Account page. Re-save Permalinks or it will give 404 error.
1351 *
1352 * @return void
1353 */
1354 public function wp_defender_2fa_endpoint(): void {
1355 add_rewrite_endpoint( $this->slug, EP_PERMALINK | EP_PAGES );
1356 }
1357
1358 /**
1359 * 2. dds the slug of the current instance to the given array of query variables.
1360 *
1361 * @param array $vars The array of query variables.
1362 *
1363 * @return array The updated array of query variables.
1364 */
1365 public function wp_defender_2fa_query_vars( $vars ) {
1366 $vars[] = $this->slug;
1367
1368 return $vars;
1369 }
1370
1371 /**
1372 * 3. Inserts the new endpoint into the My Account menu.
1373 *
1374 * @param array $items The array of items in the My Account menu.
1375 *
1376 * @return array The updated array of items with the new endpoint inserted.
1377 */
1378 public function wp_defender_2fa_link_my_account( $items ) {
1379 $needed_place = is_array( $items ) && array() !== $items ? ( count( $items ) - 1 ) : 0;
1380
1381 return array_slice( $items, 0, $needed_place, true )
1382 + array( $this->slug => esc_html__( '2FA', 'defender-security' ) )
1383 + array_slice( $items, $needed_place, null, true );
1384 }
1385
1386 /**
1387 * 4. Add content to the new tab.
1388 *
1389 * @return void
1390 */
1391 public function wp_defender_2fa_content(): void {
1392 echo do_shortcode( '[wp_defender_2fa_user_settings]' );
1393 }
1394
1395 /**
1396 * Save the 2fa details and redirect back to 'My Account' page.
1397 *
1398 * @return void
1399 */
1400 public function save_2fa_details() {
1401 $action = defender_get_data_from_request( 'action', 'p' );
1402 if ( ! is_string( $action ) || '' === trim( $action ) || 'save_def_2fa_user_settings' !== $action ) {
1403 return;
1404 }
1405
1406 wc_nocache_headers();
1407
1408 $user_id = $this->current_user->ID;
1409 if ( $user_id <= 0 ) {
1410 return;
1411 }
1412 // Verify nonce and other two-factor arguments passed.
1413 $this->profile_update( $user_id );
1414
1415 wc_add_notice( esc_html__( 'Two-Factor settings updated successfully.', 'defender-security' ) );
1416 // @since 3.2.0
1417 do_action( 'wd_woocommerce_save_2fa_details', $user_id );
1418
1419 wp_safe_redirect( wc_get_endpoint_url( $this->slug, '', wc_get_page_permalink( 'myaccount' ) ) );
1420 exit;
1421 }
1422
1423 /**
1424 * Enable provider slugs.
1425 *
1426 * @param array $provider_slugs The array of provider slugs to enable.
1427 *
1428 * @return void
1429 */
1430 public function enable_provider_slugs( array $provider_slugs ) {
1431 // Track conditions.
1432 if ( array() !== $provider_slugs ) {
1433 $methods = array();
1434 foreach ( $this->service->get_providers() as $slug => $object ) {
1435 if ( in_array( $slug, $provider_slugs, true ) ) {
1436 $methods[] = $object->get_label();
1437 }
1438 }
1439 // Run track.
1440 $this->track_feature(
1441 'def_2fa_method_activated',
1442 array(
1443 'Method name' => $methods,
1444 )
1445 );
1446 }
1447 }
1448 }
1449