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

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

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