PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.8.1
Defender Security – Malware Scanner, Login Security & Firewall v5.8.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 / component / class-two-fa.php

class-two-fa.php in Defender Security – Malware Scanner, Login Security & Firewall 5.8.1, at src/component/class-two-fa.php

888 lines 26.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles all operations related to two-factor authentication (2FA) for users.
4 *
5 * @package WP_Defender\Component
6 */
7
8 namespace WP_Defender\Component;
9
10 use WP_User;
11 use WP_Error;
12 use Countable;
13 use WP_User_Query;
14 use SodiumException;
15 use WP_Defender\Traits\IP;
16 use Calotes\Base\Component;
17 use WP_Defender\Traits\User;
18 use Calotes\Helper\Array_Cache;
19 use WP_Defender\Controller\Two_Factor;
20 use WordfenceLS\Controller_WordfenceLS;
21 use WP_Defender\Model\Setting\Login_Lockout;
22 use WP_Defender\Component\Two_Factor\Providers\Totp;
23 use WP_Defender\Model\Setting\Two_Fa as Two_Fa_Model;
24 use WP_Defender\Component\Two_Factor\Providers\Webauthn;
25 use WP_Defender\Component\Two_Factor\Providers\Backup_Codes;
26 use WP_Defender\Component\Two_Factor\Providers\Fallback_Email;
27
28 /**
29 * Handles all operations related to two-factor authentication (2FA) for users.
30 */
31 class Two_Fa extends Component {
32
33 use User;
34 use IP;
35
36 /**
37 * The user meta key for the default provider.
38 *
39 * @var string
40 */
41 public const DEFAULT_PROVIDER_USER_KEY = 'wd_2fa_default_provider';
42
43 /**
44 * The user meta key for enabled providers.
45 *
46 * @var string
47 */
48 public const ENABLED_PROVIDERS_USER_KEY = 'wd_2fa_enabled_providers';
49
50 /**
51 * Default limit for failed 2FA attempts.
52 *
53 * @var int
54 */
55 public const ATTEMPT_LIMIT = 5;
56
57 /**
58 * Default time limit for 2FA attempt window in seconds.
59 *
60 * @var int
61 */
62 public const TIME_LIMIT = 30 * MINUTE_IN_SECONDS;
63
64 /**
65 * User meta key for 2FA tokens.
66 *
67 * @var string
68 */
69 public const TOKEN_USER_KEY = 'defender_two_fa_token';
70
71 /**
72 * Get the limit of failed attempts.
73 *
74 * @return int
75 * @since 3.3.0
76 */
77 public function get_attempt_limit(): int {
78 return (int) apply_filters( 'wd_2fa_attempt_limit', self::ATTEMPT_LIMIT );
79 }
80
81 /**
82 * Get the limit of time.
83 *
84 * @return int
85 */
86 public function get_time_limit() {
87 return (int) apply_filters( 'wd_2fa_time_limit', self::TIME_LIMIT );
88 }
89
90 /**
91 * Enable OTP for a user.
92 *
93 * @param int $user_id The ID of the user.
94 *
95 * @return void
96 */
97 public function enable_otp( int $user_id ): void {
98 update_user_meta( $user_id, Totp::TOTP_AUTH_KEY, 1 );
99 update_user_meta( $user_id, Totp::TOTP_FORCE_KEY, 0 );
100 }
101
102 /**
103 * Gradually move on to using the method is_enabled_otp_for_user(). Used only by the Forminator plugin.
104 *
105 * @param int $user_id User ID to check.
106 *
107 * @return mixed
108 */
109 public function is_user_enabled_otp( int $user_id ) {
110 return get_user_meta( $user_id, Totp::TOTP_AUTH_KEY, true );
111 }
112
113 /**
114 * Checks if forced authentication is enabled for specific user roles.
115 *
116 * @param int $user_id The user ID.
117 * @param array $roles The user roles to check.
118 *
119 * @return bool True if forced authentication is enabled, false otherwise.
120 */
121 public function is_force_auth_enable_for( int $user_id, array $roles ): bool {
122 $user = get_user_by( 'id', $user_id );
123 if ( ! is_object( $user ) ) {
124 return false;
125 }
126
127 $check = array_intersect( $this->get_roles( $user ), $roles );
128
129 return count( $check ) > 0;
130 }
131
132 /**
133 * Count the total of users, who enables any 2FA method.
134 *
135 * @return int
136 */
137 public function count_users_with_enabled_2fa(): int {
138 $slugs = array_keys( $this->get_providers() );
139 $query = new WP_User_Query(
140 array(
141 // Look over the network.
142 'blog_id' => 0,
143 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
144 array(
145 'key' => self::DEFAULT_PROVIDER_USER_KEY,
146 'value' => $slugs,
147 'compare' => 'IN',
148 ),
149 ),
150 )
151 );
152
153 return $query->get_total();
154 }
155
156 /**
157 * Checks if Jetpack SSO is in conflict with 2FA settings.
158 *
159 * @return bool|int True if there is a conflict, false otherwise, or 0 if undetermined.
160 */
161 public function is_jetpack_sso() {
162 $settings = new Two_Fa_Model();
163 if ( is_plugin_active_for_network( 'jetpack/jetpack.php' ) ) {
164 // Loop through all sites.
165 $is_conflict = $settings->is_conflict( 'jetpack/jetpack.php' );
166 if ( 0 === $is_conflict ) {
167 $blog_ids = get_sites( array( 'fields' => 'ids' ) );
168 if ( is_array( $blog_ids ) ) {
169 foreach ( $blog_ids as $id ) {
170 $options = get_blog_option( $id, 'jetpack_active_modules', array() );
171 if ( array_search( 'sso', $options, true ) ) {
172 $settings->mark_as_conflict( 'jetpack/jetpack.php' );
173
174 return true;
175 }
176 }
177 }
178 } else {
179 // Get the data from cache.
180 return $is_conflict;
181 }
182 } elseif ( is_plugin_active( 'jetpack/jetpack.php' ) ) {
183 $is_conflict = $settings->is_conflict( 'jetpack/jetpack.php' );
184 if ( 0 === $is_conflict ) {
185 $options = get_option( 'jetpack_active_modules', array() );
186 if ( array_search( 'sso', $options, true ) ) {
187 $settings->mark_as_conflict( 'jetpack/jetpack.php' );
188
189 return true;
190 }
191 } else {
192 return $is_conflict;
193 }
194 }
195
196 return false;
197 }
198
199 /**
200 * Checks if Theme My Login plugin is active and marks it as a conflict.
201 *
202 * @return bool True if Theme My Login is active, false otherwise.
203 */
204 public function is_tml(): bool {
205 if (
206 is_plugin_active( 'theme-my-login/theme-my-login.php' )
207 || is_plugin_active_for_network( 'theme-my-login/theme-my-login.php' )
208 ) {
209 $settings = new Two_Fa_Model();
210 $settings->mark_as_conflict( 'theme-my-login/theme-my-login.php' );
211
212 return true;
213 }
214
215 return false;
216 }
217
218 /**
219 * Retrieves the custom graphic URL for 2FA, with fallback to a default image if necessary.
220 *
221 * @param string $url Optional. The custom URL to validate.
222 *
223 * @return string The URL to the graphic.
224 */
225 public function get_custom_graphic_url( string $url = '' ): string {
226 if ( '' === $url ) {
227 // Nothing here, surely it will cause broken, fall back to default.
228 return defender_asset_url( '/assets/img/2factor-disabled.svg' );
229 } else {
230 // Image should be under wp-content/.., so we catch that part.
231 if ( preg_match( '/(\/wp-content\/.+)/', $url, $matches ) ) {
232 $rel_path = $matches[1];
233 $rel_path = ltrim( $rel_path, '/' );
234 $abs_path = ABSPATH . $rel_path;
235 if ( ! file_exists( $abs_path ) ) {
236 // Fallback.
237 return defender_asset_url( '/assets/img/2factor-disabled.svg' );
238 } else {
239 // Should replace with our site url.
240 return get_site_url( null, $rel_path );
241 }
242 }
243
244 return defender_asset_url( '/assets/img/2factor-disabled.svg' );
245 }
246 }
247
248 /**
249 * Checks if 2FA is enabled for the current role of the specified user.
250 *
251 * @param WP_User $user The user to check.
252 *
253 * @return bool True if 2FA is enabled for the user's role, false otherwise.
254 */
255 public function is_enable_for_current_role( WP_User $user ): bool {
256 if ( 0 === ( is_array( $user->roles ) || $user->roles instanceof Countable ? count( $user->roles ) : 0 ) ) {
257 return true;
258 }
259
260 $settings = new Two_Fa_Model();
261 if ( ! is_multisite() ) {
262 $allowed_for_this_role = array_intersect( $settings->user_roles, $user->roles );
263 if ( ! is_array( $allowed_for_this_role ) ) {
264 $allowed_for_this_role = array();
265 }
266
267 return count( $allowed_for_this_role ) > 0;
268 } else {
269 $blogs = get_blogs_of_user( $user->ID );
270 $user_roles = array();
271 foreach ( $blogs as $blog ) {
272 // Get user roles for this blog.
273 $u = new WP_User( $user->ID, '', $blog->userblog_id );
274 $user_roles = array_merge( $u->roles, $user_roles );
275 }
276 $allowed_for_this_role = array_intersect( $settings->user_roles, $user_roles );
277
278 return count( $allowed_for_this_role ) > 0;
279 }
280 }
281
282 /**
283 * Checks if OTP is enabled for a specific user.
284 *
285 * @param int|WP_User $user_value The user ID or WP_User object.
286 *
287 * @return bool True if OTP is enabled, false otherwise.
288 */
289 public function is_enabled_otp_for_user( $user_value ): bool {
290 if ( is_numeric( $user_value ) ) {
291 $user = get_user_by( 'id', $user_value );
292 } elseif ( $user_value instanceof WP_User ) {
293 $user = $user_value;
294 } else {
295 return false;
296 }
297
298 if ( ! $this->is_enable_for_current_role( $user ) ) {
299 return false;
300 }
301
302 return (bool) get_user_meta( $user->ID, Totp::TOTP_AUTH_KEY, true );
303 }
304
305 /**
306 * Checks if the specified user roles intersect with the plugin's user roles.
307 *
308 * @param array $current_user_roles The current user roles.
309 * @param array $plugin_user_roles The plugin's user roles.
310 *
311 * @return bool True if there is an intersection, false otherwise.
312 */
313 public function is_intersected_arrays( array $current_user_roles, array $plugin_user_roles ): bool {
314 return array() !== array_intersect( $current_user_roles, $plugin_user_roles );
315 }
316
317 /**
318 * Retrieves all 2FA providers.
319 *
320 * @return array An array of 2FA providers.
321 */
322 public function get_providers(): array {
323 $providers = Array_Cache::get( 'providers', 'two_fa' );
324 if ( ! is_array( $providers ) ) {
325 $classes = array(
326 Totp::class,
327 Backup_Codes::class,
328 Fallback_Email::class,
329 Webauthn::class,
330 );
331 /**
332 * Filter the supplied providers.
333 *
334 * @param array $classes
335 *
336 * @since 2.8.0
337 */
338 $classes = apply_filters( 'wd_2fa_providers', $classes );
339 foreach ( $classes as $class ) {
340 $providers[ $class::$slug ] = new $class();
341 }
342 Array_Cache::set( 'providers', $providers, 'two_fa' );
343 }
344
345 return $providers;
346 }
347
348 /**
349 * Get all 2FA Auth providers that are enabled for the specified|current user.
350 *
351 * @param WP_User|null $user The user to check.
352 *
353 * @return array
354 */
355 public function get_enabled_providers_for_user( $user = null ): array {
356 if ( ! ( $user instanceof WP_User ) ) {
357 $user = wp_get_current_user();
358 }
359
360 $providers = $this->get_providers();
361 $enabled_providers = get_user_meta( $user->ID, self::ENABLED_PROVIDERS_USER_KEY, true );
362 if ( ! is_array( $enabled_providers ) || array() === $enabled_providers ) {
363 $enabled_providers = array();
364 }
365 $enabled_providers = array_intersect( $enabled_providers, array_keys( $providers ) );
366
367 /**
368 * Filter the enabled 2FA providers for this user.
369 *
370 * @param array $enabled_providers The enabled providers.
371 * @param int $user_id The user ID.
372 *
373 * @since 2.8.0
374 */
375 return apply_filters( 'wd_2fa_enabled_providers_for_user', $enabled_providers, $user->ID );
376 }
377
378 /**
379 * Retrieves all available 2FA providers for the specified or current user.
380 *
381 * @param WP_User|null $user The user to check. If null, the current user is used.
382 *
383 * @return array An array of available 2FA providers.
384 */
385 public function get_available_providers_for_user( $user = null ): array {
386 if ( ! ( $user instanceof WP_User ) ) {
387 $user = wp_get_current_user();
388 }
389
390 $providers = $this->get_providers();
391 $enabled_providers = $this->get_enabled_providers_for_user( $user );
392 $configured_providers = array();
393
394 foreach ( $providers as $slug => $provider ) {
395 if ( in_array( $slug, $enabled_providers, true ) && $provider->is_available_for_user( $user ) ) {
396 $configured_providers[ $slug ] = $provider;
397 }
398 }
399
400 return $configured_providers;
401 }
402
403 /**
404 * Gets the 2FA provider's slug for the specified or current user.
405 *
406 * @param int $user_id Optional. User ID. Default is 'null'.
407 *
408 * @return string|null
409 */
410 public function get_default_provider_slug_for_user( $user_id = null ) {
411 if ( is_null( $user_id ) || ! is_numeric( $user_id ) ) {
412 $user_id = get_current_user_id();
413 }
414
415 $available_providers = $this->get_available_providers_for_user( get_userdata( $user_id ) );
416 // If there's only one available provider, force that to be the primary.
417 if ( array() === $available_providers ) {
418 return null;
419 } elseif ( 1 === count( $available_providers ) ) {
420 $provider_slug = key( $available_providers );
421 } else {
422 $provider_slug = get_user_meta( $user_id, self::DEFAULT_PROVIDER_USER_KEY, true );
423
424 // If the provider specified isn't enabled, just grab the first one that is.
425 if ( ! isset( $available_providers[ $provider_slug ] ) ) {
426 $provider_slug = key( $available_providers );
427 }
428 }
429
430 /**
431 * Filter the 2FA provider slug used for this user.
432 *
433 * @param string $provider_slug The provider slug currently being used.
434 * @param int $user_id The user ID.
435 */
436 return apply_filters( 'wd_2fa_default_provider_for_user', $provider_slug, $user_id );
437 }
438
439 /**
440 * Checks if a specific 2FA provider is enabled for a user by its slug.
441 *
442 * @param WP_User $user The user to check.
443 * @param string $slug The provider slug.
444 *
445 * @return bool True if the provider is enabled, false otherwise.
446 */
447 public function is_checked_enabled_provider_by_slug( WP_User $user, string $slug ): bool {
448 $enabled_providers = $this->get_enabled_providers_for_user( $user );
449
450 return in_array( $slug, $enabled_providers, true );
451 }
452
453 /**
454 * Send emergency email to users.
455 *
456 * @param string $login_token This will be generated randomly on frontend each time user refresh, an internal
457 * OTP.
458 * @param int $user_id User ID to send email to.
459 *
460 * @return boolean|WP_Error
461 */
462 public function send_otp_to_email( string $login_token, int $user_id ) {
463 if ( $user_id <= 0 ) {
464 return new WP_Error( Error_Code::INVALID, esc_html__( 'The user is invalid.', 'defender-security' ) );
465 }
466 $settings = new Two_Fa_Model();
467 $hashed_token = get_user_meta( $user_id, self::TOKEN_USER_KEY, true );
468 if ( ! Crypt::compare_lines( $hashed_token, wp_hash( $user_id . $login_token ) ) ) {
469 return new WP_Error( Error_Code::INVALID, esc_html__( 'Your token is invalid.', 'defender-security' ) );
470 }
471
472 $code = wp_generate_password( 20, false );
473 update_user_meta(
474 $user_id,
475 Fallback_Email::FALLBACK_BACKUP_CODE_KEY,
476 array(
477 'code' => wp_hash( $code ),
478 'time' => time(),
479 )
480 );
481 $user = get_user_by( 'id', $user_id );
482 $params = array(
483 'display_name' => $user->display_name,
484 'passcode' => $code,
485 );
486 $two_fa = wd_di()->get( Two_Factor::class );
487 $body = $two_fa->render_partial(
488 'email/2fa-lost-phone',
489 array(
490 'body' => $settings->email_body,
491 ),
492 false
493 );
494
495 foreach ( $params as $key => $val ) {
496 if ( 'passcode' === $key ) {
497 $body = str_replace( "{{{$key}}}", '<span class="defender-otp">' . $val . '</span>', $body );
498 } else {
499 $body = str_replace( "{{{$key}}}", $val, $body );
500 }
501 }
502 // Main email template.
503 $body = $two_fa->render_partial(
504 'email/index',
505 array(
506 'title' => esc_html__( 'Two-Factor Authentication', 'defender-security' ),
507 'content_body' => $body,
508 // An empty value because 2FA-email is sent after a manual click from the user.
509 'unsubscribe_link' => '',
510 ),
511 false
512 );
513 // Since v5.2.0.
514 $from_email = defender_noreply_email( 'wd_two_fa_totp_noreply_email' );
515 $headers = array( 'Content-Type: text/html; charset=UTF-8' );
516 $headers[] = sprintf( 'From: %s <%s>', $settings->email_sender, $from_email );
517
518 return wp_mail( Fallback_Email::get_backup_email( $user->ID ), $settings->email_subject, $body, $headers );
519 }
520
521 /**
522 * Retrieves a 2FA provider by its slug.
523 *
524 * @param string $slug The provider slug.
525 *
526 * @return object|WP_Error The provider object if found, WP_Error otherwise.
527 */
528 public function get_provider_by_slug( $slug ) {
529 foreach ( $this->get_providers() as $key => $provider ) {
530 if ( $slug === $key ) {
531 return $provider;
532 }
533 }
534
535 return new WP_Error( 'opt_fail', esc_html__( 'ERROR: Cheatin&#8217; uh?', 'defender-security' ) );
536 }
537
538 /**
539 * Remove enabled provider for the specified|current user.
540 *
541 * @param string $provider A provider which needs to be removed.
542 * @param WP_User|null $user The current user to be removed.
543 *
544 * @return void
545 * @since 3.0.0
546 */
547 public function remove_enabled_provider_for_user( string $provider, $user = null ): void {
548 if ( ! ( $user instanceof WP_User ) ) {
549 $user = wp_get_current_user();
550 }
551
552 $enabled_providers = get_user_meta( $user->ID, self::ENABLED_PROVIDERS_USER_KEY, true );
553 if ( ! is_array( $enabled_providers ) || array() === $enabled_providers ) {
554 return;
555 }
556
557 $pos = array_search( $provider, $enabled_providers, true );
558 if ( false !== $pos ) {
559 unset( $enabled_providers[ $pos ] );
560 update_user_meta( $user->ID, self::ENABLED_PROVIDERS_USER_KEY, $enabled_providers );
561
562 $default_provider = get_user_meta( $user->ID, self::DEFAULT_PROVIDER_USER_KEY, true );
563 if ( $provider === $default_provider ) {
564 delete_user_meta( $user->ID, self::DEFAULT_PROVIDER_USER_KEY );
565 }
566 }
567 }
568
569 /**
570 * Verifies the attempt for two-factor authentication.
571 *
572 * @param int $user_id The ID of the user.
573 * @param string $slug The slug of the two-factor authentication method.
574 *
575 * @return string The lockout message, if the attempt is invalid.
576 * @since 3.3.0
577 */
578 public function verify_attempt( int $user_id, string $slug ): string {
579 $lockout_message = '';
580 $login_settings = wd_di()->get( Login_Lockout::class );
581
582 $firewall_component = wd_di()->get( Firewall::class );
583
584 $skip_priority_lockout_checks = true;
585 foreach ( $this->get_user_ip() as $ip ) {
586 if ( ! $firewall_component->skip_priority_lockout_checks( $ip ) ) {
587 $skip_priority_lockout_checks = false;
588 break;
589 }
590 }
591
592 /**
593 * Required rules:
594 * hook returns true (since v3.3.0),
595 * if Login Protection is enabled,
596 * the current IP is not in priority points: no allowlisted, no staff and the country is not from allowlisted.
597 */
598 if (
599 apply_filters( 'wd_2fa_enable_attempts', true )
600 && $login_settings->enabled
601 && ! $skip_priority_lockout_checks
602 ) {
603 $line = get_user_meta( $user_id, 'wd_2fa_attempt_' . $slug, true );
604 // Fresh start or there's a record.
605 if ( in_array( $line, array( false, '', array() ), true ) ) {
606 $count = $this->get_attempt_limit();
607 $start_time = time();
608 } else {
609 [ $count, $start_time ] = explode( '::', $line );
610 }
611 $end_time = time();
612 $time_limit = $this->get_time_limit();
613 // If the time difference between attempts is greater than the limit, clear the attempt counter.
614 if ( $end_time - $start_time >= $time_limit ) {
615 delete_user_meta( $user_id, 'wd_2fa_attempt_' . $slug );
616 $count = $this->get_attempt_limit();
617 $start_time = $end_time;
618 }
619
620 $count = (int) $count - 1;
621 if ( 1 > $count ) {
622 $message = sprintf(
623 /* translators: %s: 2FA method slug. */
624 esc_html__( 'Lockout occurred: Too many failed 2fa attempts for %s method.', 'defender-security' ),
625 $slug
626 );
627 // since 3.3.0.
628 do_action( 'wd_2fa_lockout', $user_id, $message, $time_limit );
629
630 delete_user_meta( $user_id, 'wd_2fa_attempt_' . $slug );
631
632 wp_safe_redirect( home_url() );
633 exit;
634 }
635 // Save number of attempts.
636 update_user_meta( $user_id, 'wd_2fa_attempt_' . $slug, $count . '::' . $start_time );
637 $lockout_message = esc_html__( 'INVALID CODE: ', 'defender-security' );
638 $lockout_message .= '<span class="two-factor-attempt">' . esc_html__(
639 'The two-factor authentication code you entered is incorrect or has expired. Please try again.',
640 'defender-security'
641 );
642 $lockout_message .= '<br/><br/>';
643 $lockout_message .= sprintf(
644 /* translators: %d: Count. */
645 _n( 'You have %d login attempt remaining.', 'You have %d login attempts remaining.', $count, 'defender-security' ),
646 $count
647 );
648 $lockout_message .= '</span>';
649 }
650
651 return $lockout_message;
652 }
653
654 /**
655 * Encrypts user data with a new key.
656 *
657 * @param int $user_id The ID of the user.
658 * @param string $plaintext The plaintext data to be encrypted.
659 * @param string $state The state of the data before encryption.
660 *
661 * @return bool True if the data was successfully encrypted, false otherwise.
662 * @throws SodiumException On failure.
663 */
664 protected function reencrypt_data( $user_id, $plaintext, $state = 'plaintext' ) {
665 $new_key = Crypt::get_encrypted_data( $plaintext );
666 if ( ! is_wp_error( $new_key ) ) {
667 // If everything is successful, remove an old key.
668 delete_user_meta( $user_id, TOTP::TOTP_SECRET_KEY );
669 // Update the value with a new key.
670 update_user_meta( $user_id, TOTP::TOTP_SODIUM_SECRET_KEY, $new_key );
671 // Logging.
672 $this->log( 'Update UID: ' . $user_id . '. Previous state: ' . $state, 'internal' );
673
674 return true;
675 }
676 $this->log( 'Encryption error for UID: ' . $user_id . '. State: ' . $state, 'internal' );
677
678 return false;
679 }
680
681 /**
682 * Old states with plaintext or pub key are exist?
683 *
684 * @param int $user_id The ID of the user.
685 *
686 * @return bool
687 * @throws SodiumException On failure.
688 */
689 public function maybe_update( $user_id ) {
690 $old_key = get_user_meta( $user_id, Totp::TOTP_SECRET_KEY, true );
691 if ( is_string( $old_key ) && '' !== $old_key ) {
692 // Is it a plaintext? It was before v3.3.1.
693 if ( TOTP::TOTP_LENGTH === mb_strlen( $old_key, '8bit' ) ) {
694 return $this->reencrypt_data( $user_id, $old_key );
695 }
696 // Is it encrypted via a pub key? It was before v3.4.0.
697 $decrypted_data = Legacy_Versions::get_decrypted_data_with_pub_key( $old_key );
698 if ( false !== $decrypted_data ) {
699 return $this->reencrypt_data( $user_id, $decrypted_data, 'pub-key' );
700 }
701 }
702
703 return false;
704 }
705
706 /**
707 * Finds whether at least anyone user role in array of the enabled 2FA user roles.
708 *
709 * @param WP_User $user User instance object.
710 * @param array $roles User roles.
711 *
712 * @return bool Return true for at least one role matches else false return.
713 */
714 public function is_auth_enable_for( WP_User $user, array $roles ): bool {
715 if ( false === apply_filters( 'wp_defender_2fa_user_enabled', true, $user->ID ) ) {
716 return false;
717 }
718
719 $common_roles = array_intersect( $this->get_roles( $user ), $roles );
720 return array() !== $common_roles;
721 }
722
723 /**
724 * Remove actions when rendering 2FA screen during login.
725 *
726 * @return void
727 * @since 3.5.0
728 */
729 public function remove_actions_for_2fa_screen(): void {
730 if (
731 class_exists( '\WordfenceLS\Controller_WordfenceLS' ) &&
732 method_exists( '\WordfenceLS\Controller_WordfenceLS', 'shared' ) &&
733 is_object( Controller_WordfenceLS::shared() ) &&
734 method_exists( Controller_WordfenceLS::shared(), '_login_enqueue_scripts' )
735 ) {
736 remove_action(
737 'login_enqueue_scripts',
738 array(
739 Controller_WordfenceLS::shared(),
740 '_login_enqueue_scripts',
741 )
742 );
743 }
744 }
745
746 /**
747 * Queue hooks when this class init.
748 *
749 * @return void
750 */
751 public function add_hooks(): void {
752 // This will be only displayed on a single site and the main site of MU.
753 if ( is_multisite() ) {
754 add_filter( 'wpmu_users_columns', array( $this, 'alter_users_table' ) );
755 add_action( 'network_admin_notices', array( $this, 'admin_notices' ) );
756 add_filter( 'ms_user_row_actions', array( $this, 'display_user_actions' ), 10, 2 );
757 } else {
758 add_filter( 'manage_users_columns', array( $this, 'alter_users_table' ) );
759 add_action( 'admin_notices', array( $this, 'admin_notices' ) );
760 add_filter( 'user_row_actions', array( $this, 'display_user_actions' ), 10, 2 );
761 }
762 add_filter( 'manage_users_custom_column', array( $this, 'alter_user_table_row' ), 10, 3 );
763 add_filter( 'ms_shortcode_ajax_login', array( $this, 'm2_no_ajax' ) );
764 }
765
766 /**
767 * Add a column in the users table, column will be last.
768 *
769 * @param array $columns an array of columns.
770 *
771 * @return array
772 */
773 public function alter_users_table( array $columns ) {
774 $columns = array_slice( $columns, 0, count( $columns ) - 1 )
775 + array( 'defender-two-fa' => esc_html__( 'Two Factor', 'defender-security' ) )
776 + array_slice( $columns, count( $columns ) - 1 );
777
778 return $columns;
779 }
780
781 /**
782 * Reset 2FA methods for specific user and display notice.
783 *
784 * @return void
785 */
786 public function admin_notices() {
787 $screen = get_current_screen();
788 $screen_id = is_multisite() ? 'users-network' : 'users';
789
790 if ( $screen_id !== $screen->id || ! current_user_can( 'edit_users' ) ) {
791 return;
792 }
793 $action = defender_get_data_from_request( 'action', 'g' );
794 $user_id = defender_get_data_from_request( 'user', 'g' );
795 if ( ! isset( $action, $user_id ) ) {
796 return;
797 }
798 if ( 'disable_2fa_methods' !== $action ) {
799 return;
800 }
801 $user = get_user_by( 'id', $user_id );
802 if ( ! is_object( $user ) ) {
803 return;
804 }
805 // Maybe the value has already been cleared.
806 $default_provider = get_user_meta( $user_id, self::DEFAULT_PROVIDER_USER_KEY, true );
807 if ( in_array( $default_provider, array( false, '', array() ), true ) ) {
808 return;
809 }
810 // Default and enabled 2fa providers are cleared for user.
811 update_user_meta( $user_id, self::DEFAULT_PROVIDER_USER_KEY, '' );
812 update_user_meta( $user_id, self::ENABLED_PROVIDERS_USER_KEY, '' );
813 ?>
814 <div class="notice notice-success is-dismissible">
815 <p>
816 <?php
817 printf(
818 /* translators: %s: User name. */
819 esc_html__( 'Two factor authentication has been reset for %s.', 'defender-security' ),
820 '<b>' . esc_html( $user->display_name ) . '</b>'
821 );
822 ?>
823 </p>
824 </div>
825 <?php
826 }
827
828 /**
829 * Add the link to reset 2FA settings for specific user. It's without bulk actions.
830 *
831 * @param string[] $actions The array of user actions.
832 * @param WP_User $user WP_User object for the currently listed user.
833 *
834 * @return array
835 */
836 public function display_user_actions( $actions, WP_User $user ) {
837 // Only for users that have one enabled 2fa method at least.
838 $default_provider = get_user_meta( $user->ID, self::DEFAULT_PROVIDER_USER_KEY, true );
839 if ( in_array( $default_provider, array( false, '', array() ), true ) ) {
840 return $actions;
841 }
842
843 $cap = is_multisite() ? 'manage_network_options' : 'manage_options';
844 if ( current_user_can( $cap ) ) {
845 $actions['disable_wpdef_2fa_methods'] = sprintf(
846 '<a href="%s">%s</a>',
847 wp_nonce_url( "users.php?action=disable_2fa_methods&amp;user={$user->ID}", 'bulk-users' ),
848 esc_html__( 'Reset two factor', 'defender-security' )
849 );
850 }
851
852 return $actions;
853 }
854
855 /**
856 * This method is part of the Two_Fa class and is used to alter the user table row value for the specified column.
857 *
858 * @param mixed $val The current value of the user table row.
859 * @param string $column_name The name of the column.
860 * @param int $user_id The ID of the user.
861 *
862 * @return string The altered value of the user table row.
863 */
864 public function alter_user_table_row( $val, string $column_name, int $user_id ) {
865 // @since 3.3.0. Fix an error from other plugins.
866 $val = (string) $val;
867 if ( 'defender-two-fa' !== $column_name ) {
868 return $val;
869 }
870 $provider_slug = get_user_meta( $user_id, self::DEFAULT_PROVIDER_USER_KEY, true );
871 $provider = $this->get_provider_by_slug( $provider_slug );
872 if ( is_wp_error( $provider ) ) {
873 return '';
874 }
875
876 return $provider->get_user_label();
877 }
878
879 /**
880 * Stop ajax login on membership 2.
881 *
882 * @return bool
883 */
884 public function m2_no_ajax() {
885 return false;
886 }
887 }
888