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

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