Admin
5 years ago
Authenticator
5 years ago
BackgroundProcessing
5 years ago
Cron
5 years ago
Shortcodes
5 years ago
Utils
5 years ago
.gitkeep
6 years ago
EmailTemplate.php
5 years ago
WP2FA.php
5 years ago
WP2FA.php
957 lines
| 1 | <?php // phpcs:ignore |
| 2 | |
| 3 | namespace WP2FA; |
| 4 | |
| 5 | use WP2FA\Admin\UserListing; |
| 6 | use WP2FA\Admin\SettingsPage; |
| 7 | use WP2FA\Utils\DateTimeUtils; |
| 8 | |
| 9 | /** |
| 10 | * Main WP2FA Class. |
| 11 | */ |
| 12 | class WP2FA { |
| 13 | |
| 14 | /** |
| 15 | * Plugin version. |
| 16 | * |
| 17 | * @var string |
| 18 | */ |
| 19 | public $version = WP_2FA_VERSION; |
| 20 | |
| 21 | /** |
| 22 | * Options variables. |
| 23 | * |
| 24 | * @var array |
| 25 | */ |
| 26 | protected static $wp_2fa_options; |
| 27 | protected static $wp_2fa_email_templates; |
| 28 | |
| 29 | /** |
| 30 | * Count of the available methods |
| 31 | * |
| 32 | * @var integer |
| 33 | */ |
| 34 | public static $methodsCount = 2; |
| 35 | |
| 36 | /** |
| 37 | * Instance wrapper. |
| 38 | * |
| 39 | * @var object |
| 40 | */ |
| 41 | private static $instance = null; |
| 42 | |
| 43 | /** |
| 44 | * Holds array with all the sites in multisite WP installation |
| 45 | * |
| 46 | * @var array |
| 47 | */ |
| 48 | private static $sites = []; |
| 49 | |
| 50 | /** |
| 51 | * Return plugin instance. |
| 52 | */ |
| 53 | public static function get_instance() { |
| 54 | |
| 55 | if ( null === self::$instance ) { |
| 56 | self::$instance = new self(); |
| 57 | } |
| 58 | |
| 59 | return self::$instance; |
| 60 | } |
| 61 | |
| 62 | /** |
| 63 | * Contructor. |
| 64 | */ |
| 65 | private function __construct() { |
| 66 | if ( self::is_this_multisite() ) { |
| 67 | self::$wp_2fa_options = get_network_option( null, 'wp_2fa_settings' ); |
| 68 | self::$wp_2fa_email_templates = get_network_option( null, 'wp_2fa_email_settings' ); |
| 69 | } else { |
| 70 | self::$wp_2fa_options = get_option( 'wp_2fa_settings' ); |
| 71 | self::$wp_2fa_email_templates = get_option( 'wp_2fa_email_settings' ); |
| 72 | } |
| 73 | |
| 74 | // Activation/Deactivation. |
| 75 | register_activation_hook( WP_2FA_FILE, '\WP2FA\Core\activate' ); |
| 76 | register_deactivation_hook( WP_2FA_FILE, '\WP2FA\Core\deactivate' ); |
| 77 | // Register our uninstallation hook. |
| 78 | register_uninstall_hook( WP_2FA_FILE, '\WP2FA\Core\uninstall' ); |
| 79 | } |
| 80 | |
| 81 | /** |
| 82 | * Fire up classes. |
| 83 | */ |
| 84 | public function init() { |
| 85 | // Bootstrap. |
| 86 | Core\setup(); |
| 87 | |
| 88 | $this->settings = new Admin\SettingsPage(); |
| 89 | $this->wizard = new Admin\SetupWizard(); |
| 90 | $this->authentication = new Authenticator\Authentication(); |
| 91 | $this->backupcodes = new Authenticator\BackupCodes(); |
| 92 | $this->login = new Authenticator\Login(); |
| 93 | $this->user_notices = new Admin\UserNotices(); |
| 94 | $this->crontasks = new Cron\CronTasks(); |
| 95 | $this->user_registered = new Admin\UserRegistered(); |
| 96 | $this->shortcodes = new Shortcodes\Shortcodes(); |
| 97 | // BG Processors. |
| 98 | $this->bg_enforce_2fa = new BackgroundProcessing\Enforce2FA(); |
| 99 | $this->bg_delete_grace = new BackgroundProcessing\DeleteGracePeriod(); |
| 100 | $this->bg_remove_methods = new BackgroundProcessing\RemoveEnabledMethods(); |
| 101 | $this->bg_wipe_2fa = new BackgroundProcessing\RemoveAllUserData(); |
| 102 | |
| 103 | global $pagenow; |
| 104 | if ( 'profile.php' !== $pagenow || 'user-edit.php' !== $pagenow ) { |
| 105 | $this->user_profiles = new Admin\UserProfile(); |
| 106 | } |
| 107 | |
| 108 | if ( is_admin() ) { |
| 109 | UserListing::init(); |
| 110 | } |
| 111 | |
| 112 | $this->add_actions(); |
| 113 | } |
| 114 | |
| 115 | /** |
| 116 | * Add our plugins actions. |
| 117 | */ |
| 118 | public function add_actions() { |
| 119 | // Plugin redirect on activation, only if we have no settings currently saved. |
| 120 | if ( empty( self::$wp_2fa_options ) || ! isset( self::$wp_2fa_options ) ) { |
| 121 | add_action( 'admin_init', array( $this, 'setup_redirect' ), 10 ); |
| 122 | } |
| 123 | |
| 124 | // SettingsPage. |
| 125 | if ( self::is_this_multisite() ) { |
| 126 | add_action( 'network_admin_menu', array( $this->settings, 'create_settings_admin_menu_multisite' ) ); |
| 127 | add_action( 'network_admin_edit_update_wp2fa_network_options', array( $this->settings, 'update_wp2fa_network_options' ) ); |
| 128 | add_action( 'network_admin_edit_update_wp2fa_network_email_options', array( $this->settings, 'update_wp2fa_network_email_options' ) ); |
| 129 | add_action( 'network_admin_notices', array( $this->settings, 'settings_saved_network_admin_notice' ) ); |
| 130 | } else { |
| 131 | add_action( 'admin_menu', array( $this->settings, 'create_settings_admin_menu' ) ); |
| 132 | } |
| 133 | add_action( 'wp_ajax_get_all_users', array( $this->settings, 'get_all_users' ) ); |
| 134 | add_action( 'wp_ajax_get_all_network_sites', array( $this->settings, 'get_all_network_sites' ) ); |
| 135 | add_action( 'wp_ajax_unlock_account', array( $this->settings, 'unlock_account' ), 10, 1 ); |
| 136 | add_action( 'admin_action_unlock_account', array( $this->settings, 'unlock_account' ), 10, 1 ); |
| 137 | add_action( 'admin_action_remove_user_2fa', array( $this->settings, 'remove_user_2fa' ), 10, 1 ); |
| 138 | add_action( 'wp_ajax_remove_user_2fa', array( $this->settings, 'remove_user_2fa' ), 10, 1 ); |
| 139 | add_action( 'admin_menu', array( $this->settings, 'hide_settings' ), 999 ); |
| 140 | add_action( 'plugin_action_links_' . WP_2FA_BASE, array( $this->settings, 'add_plugin_action_links' ) ); |
| 141 | add_filter( 'display_post_states', array( $this->settings, 'add_display_post_states' ), 10, 2 ); |
| 142 | add_action( 'wp_ajax_wp_2fa_cancel_bg_processes', array( $this->settings, 'cancel_bg_processes' ) ); |
| 143 | |
| 144 | // SetupWizard. |
| 145 | if ( self::is_this_multisite() ) { |
| 146 | add_action( 'network_admin_menu', array( $this->wizard, 'network_admin_menus' ), 10 ); |
| 147 | add_action( 'admin_menu', array( $this->wizard, 'admin_menus' ), 10 ); |
| 148 | } else { |
| 149 | add_action( 'admin_menu', array( $this->wizard, 'admin_menus' ), 10 ); |
| 150 | } |
| 151 | add_action( 'plugins_loaded', array( $this, 'add_wizard_actions' ), 10 ); |
| 152 | add_action( 'wp_ajax_send_authentication_setup_email', array( $this->wizard, 'send_authentication_setup_email' ) ); |
| 153 | add_action( 'wp_ajax_regenerate_authentication_key', array( $this->wizard, 'regenerate_authentication_key' ) ); |
| 154 | |
| 155 | // UserNotices. |
| 156 | add_action( 'wp_ajax_dismiss_nag', array( $this->user_notices, 'dismiss_nag' ) ); |
| 157 | add_action( 'wp_ajax_dismiss_nag', array( $this->user_notices, 'dismiss_reconfigure_nag' ) ); |
| 158 | add_action( 'clear_auth_cookie', array( $this->user_notices, 'reset_nag' ) ); |
| 159 | |
| 160 | // UserProfile. |
| 161 | global $pagenow; |
| 162 | if ( 'profile.php' !== $pagenow || 'user-edit.php' !== $pagenow ) { |
| 163 | add_action( 'show_user_profile', array( $this->user_profiles, 'user_2fa_options' ) ); |
| 164 | add_action( 'edit_user_profile', array( $this->user_profiles, 'user_2fa_options' ) ); |
| 165 | if ( self::is_this_multisite() ) { |
| 166 | add_action( 'personal_options_update', array( $this->user_profiles, 'save_user_2fa_options' ) ); |
| 167 | } |
| 168 | } |
| 169 | add_filter( 'user_row_actions', array( $this->user_profiles, 'user_2fa_row_actions' ), 10, 2 ); |
| 170 | if ( self::is_this_multisite() ) { |
| 171 | add_filter( 'ms_user_row_actions', array( $this->user_profiles, 'user_2fa_row_actions' ), 10, 2 ); |
| 172 | } |
| 173 | add_action( 'wp_ajax_validate_authcode_via_ajax', array( $this->user_profiles, 'validate_authcode_via_ajax' ) ); |
| 174 | add_action( 'wp_ajax_wp2fa_test_email', array( $this, 'handle_send_test_email_ajax' ) ); |
| 175 | |
| 176 | // Login. |
| 177 | add_action( 'init', array( $this->login, 'get_providers' ) ); |
| 178 | add_action( 'wp_login', array( $this->login, 'wp_login' ), 20, 2 ); |
| 179 | add_action( 'login_form_validate_2fa', array( $this->login, 'login_form_validate_2fa' ) ); |
| 180 | add_action( 'login_form_backup_2fa', array( $this->login, 'backup_2fa' ) ); |
| 181 | |
| 182 | /** |
| 183 | * Keep track of all the user sessions for which we need to invalidate the |
| 184 | * authentication cookies set during the initial password check. |
| 185 | */ |
| 186 | add_action( 'set_auth_cookie', array( $this->login, 'collect_auth_cookie_tokens' ) ); |
| 187 | add_action( 'set_logged_in_cookie', array( $this->login, 'collect_auth_cookie_tokens' ) ); |
| 188 | |
| 189 | // Run only after the core wp_authenticate_username_password() check. |
| 190 | add_filter( 'authenticate', array( $this->login, 'filter_authenticate' ), 50 ); |
| 191 | add_filter( 'wp_authenticate_user', array( $this->login, 'run_authentication_check' ), 10, 2 ); |
| 192 | |
| 193 | // User Register. |
| 194 | add_action( 'set_user_role', array( $this->user_registered, 'check_user_upon_role_change' ), 10, 3 ); |
| 195 | |
| 196 | // Block users from admin if needed. |
| 197 | $user_block_hook = is_admin() || is_network_admin() ? 'init' : 'wp'; |
| 198 | add_action( $user_block_hook, array( $this, 'block_unconfigured_users_from_admin' ), 10 ); |
| 199 | } |
| 200 | |
| 201 | /** |
| 202 | * Add actions specific to the wizard. |
| 203 | */ |
| 204 | public function add_wizard_actions() { |
| 205 | new BackgroundProcessing\Enforce2FA(); |
| 206 | if ( function_exists( 'wp_get_current_user' ) && current_user_can( 'read' ) ) { |
| 207 | add_action( 'admin_init', array( $this->wizard, 'setup_page' ), 10 ); |
| 208 | } |
| 209 | } |
| 210 | |
| 211 | /** |
| 212 | * Redirect user to 1st time setup. |
| 213 | */ |
| 214 | public function setup_redirect() { |
| 215 | |
| 216 | // Bail early before the redirect if the user can't manage options. |
| 217 | if ( ! current_user_can( 'manage_options' ) ) { |
| 218 | return; |
| 219 | } |
| 220 | |
| 221 | if ( get_option( 'wp_2fa_redirect_on_activate', false ) ) { |
| 222 | // Delete redirect option. |
| 223 | delete_option( 'wp_2fa_redirect_on_activate' ); |
| 224 | |
| 225 | $page = ( self::is_this_multisite() && is_super_admin() ) ? network_admin_url( 'index.php' ) : admin_url( 'options-general.php' ); |
| 226 | $redirect = add_query_arg( |
| 227 | array( |
| 228 | 'page' => 'wp-2fa-setup', |
| 229 | 'is_initial_setup' => 'true', |
| 230 | ), |
| 231 | admin_url( 'user-edit.php' ) |
| 232 | ); |
| 233 | |
| 234 | wp_safe_redirect( $redirect ); |
| 235 | exit(); |
| 236 | } |
| 237 | } |
| 238 | |
| 239 | /** |
| 240 | * Check is this is a multisite setup. |
| 241 | */ |
| 242 | public static function is_this_multisite() { |
| 243 | return function_exists( 'is_multisite' ) && is_multisite(); |
| 244 | } |
| 245 | |
| 246 | /** |
| 247 | * Return user roles. |
| 248 | * |
| 249 | * @return array User roles. |
| 250 | */ |
| 251 | public static function wp_2fa_get_roles() { |
| 252 | global $wp_roles; |
| 253 | $roles = $wp_roles->role_names; |
| 254 | return $roles; |
| 255 | } |
| 256 | |
| 257 | /** |
| 258 | * Check to see if the user or user role is excluded. |
| 259 | * |
| 260 | * @param int $user_id User id. |
| 261 | * @return boolean Is user excluded or not. |
| 262 | */ |
| 263 | public static function is_user_excluded( $user_id, $excluded_users = '', $excluded_roles = '', $excluded_sites = '', $included_sites = [] ) { |
| 264 | $user = false; |
| 265 | $user_roles = false; |
| 266 | |
| 267 | // If we have been passed an object, handle accordingly. |
| 268 | if ( is_a( $user_id, '\WP_User' ) ) { |
| 269 | $user = $user_id; |
| 270 | $user_roles = $user->roles; |
| 271 | } |
| 272 | |
| 273 | // If we have an int instead, lets get the user data for that ID. |
| 274 | if ( is_numeric( $user_id ) || isset( $_GET['user_id'] ) && is_numeric( $user_id ) ) { |
| 275 | $user = get_user_by( 'id', $user_id ); |
| 276 | $user_roles = $user->roles; |
| 277 | } |
| 278 | |
| 279 | // Finally, we could have an array consisting of ID or user_login. |
| 280 | if ( is_array( $user_id ) && isset( $user_id['ID'] ) ) { |
| 281 | $user = get_user_by( 'id', $user_id['ID'] ); |
| 282 | $user_roles = $user->roles; |
| 283 | } |
| 284 | |
| 285 | // Finally, if we reach this point with no $user or $user_roles lets get the current user. |
| 286 | if ( ! $user || ! $user_roles ) { |
| 287 | $user = wp_get_current_user(); |
| 288 | $user_roles = $user->roles; |
| 289 | } |
| 290 | |
| 291 | $user_excluded = false; |
| 292 | |
| 293 | if ( isset( $excluded_users ) && ! empty( $excluded_users ) ) { |
| 294 | $excluded_users = $excluded_users; |
| 295 | } else { |
| 296 | $excluded_users = WP2FA::get_wp2fa_setting( 'excluded_users' ); |
| 297 | } |
| 298 | |
| 299 | if ( ! empty( $excluded_users ) ) { |
| 300 | // Turn it into an array. |
| 301 | $excluded_users_array = $excluded_users; |
| 302 | // Compare our roles with the users and see if we get a match. |
| 303 | $result = in_array( $user->user_login, $excluded_users_array, true ); |
| 304 | if ( $result ) { |
| 305 | $user_excluded = true; |
| 306 | return true; |
| 307 | } |
| 308 | } |
| 309 | |
| 310 | if ( isset( $excluded_roles ) && ! empty( $excluded_roles ) ) { |
| 311 | $excluded_roles = $excluded_roles; |
| 312 | } else { |
| 313 | $excluded_roles = WP2FA::get_wp2fa_setting( 'excluded_roles' ); |
| 314 | } |
| 315 | |
| 316 | if ( ! empty( $excluded_roles ) ) { |
| 317 | // Turn it into an array. |
| 318 | $excluded_roles_array = array_map('strtolower', $excluded_roles ); |
| 319 | // Compare our roles with the users and see if we get a match. |
| 320 | $result = array_intersect( $excluded_roles_array, $user->roles ); |
| 321 | if ( $result ) { |
| 322 | $user_excluded = true; |
| 323 | return true; |
| 324 | } |
| 325 | } |
| 326 | |
| 327 | if ( self::is_this_multisite() ) { |
| 328 | if ( isset( $excluded_sites ) && ! empty( $excluded_sites ) ) { |
| 329 | $excluded_sites = $excluded_sites; |
| 330 | } else { |
| 331 | $excluded_sites = WP2FA::get_wp2fa_setting( 'excluded_sites' ); |
| 332 | } |
| 333 | |
| 334 | if ( ! empty( $excluded_sites ) ) { |
| 335 | // Turn it into an array. |
| 336 | $excluded_site_array = explode( ',', strtolower( $excluded_sites ) ); |
| 337 | $site_ids_only = array(); |
| 338 | // Remove everything but the excluded sites ID, which we will use to check if user is a member. |
| 339 | foreach ( $excluded_site_array as $excluded_site ) { |
| 340 | if ( isset( explode( ':', $excluded_site )[1] ) ) { |
| 341 | $id = trim( explode( ':', $excluded_site )[1] ); |
| 342 | $site_ids_only[] = $id; |
| 343 | } |
| 344 | } |
| 345 | foreach ( $site_ids_only as $site_id ) { |
| 346 | if ( is_user_member_of_blog( $user->ID, $site_id ) ) { |
| 347 | // User is a member of the a blog we are excluding from 2FA. |
| 348 | $user_excluded = true; |
| 349 | return true; |
| 350 | } else { |
| 351 | // User is NOT a member of the a blog we are excluding. |
| 352 | $user_excluded = false; |
| 353 | } |
| 354 | } |
| 355 | } |
| 356 | |
| 357 | if ( ! isset( $included_sites ) || empty( $included_sites ) ) { |
| 358 | $included_sites = WP2FA::get_wp2fa_setting( 'included_sites' ); |
| 359 | } |
| 360 | |
| 361 | foreach ( $included_sites as $siteId ) { |
| 362 | if ( is_user_member_of_blog( $user->ID, $siteId ) ) { |
| 363 | $user_excluded = false; |
| 364 | } |
| 365 | } |
| 366 | } |
| 367 | |
| 368 | if ( true === $user_excluded ) { |
| 369 | return true; |
| 370 | } |
| 371 | |
| 372 | return false; |
| 373 | } |
| 374 | |
| 375 | /** |
| 376 | * Checks if user is enforced |
| 377 | * |
| 378 | * @since 1.6 |
| 379 | * |
| 380 | * @param [type] $user_id |
| 381 | * @param string $current_policy |
| 382 | * @param string $excluded_users |
| 383 | * @param string $excluded_roles |
| 384 | * @param string $enforced_users |
| 385 | * @param string $enforced_roles |
| 386 | * |
| 387 | * @return boolean |
| 388 | */ |
| 389 | public static function isUserEnforced( $user_id, $current_policy = '', $excluded_users = '', $excluded_roles = '', $enforced_users = '', $enforced_roles = '' ) { |
| 390 | if ( isset( $user_id ) ) { |
| 391 | $user = get_user_by( 'id', $user_id ); |
| 392 | $user_roles = $user->roles; |
| 393 | } else { |
| 394 | $user = wp_get_current_user(); |
| 395 | $user_roles = $user->roles; |
| 396 | } |
| 397 | |
| 398 | if ( $current_policy ) { |
| 399 | $current_policy = $current_policy; |
| 400 | } else { |
| 401 | $current_policy = self::get_wp2fa_setting( 'enforcement-policy' ); |
| 402 | } |
| 403 | |
| 404 | $enabled_method = get_user_meta( $user->ID, 'wp_2fa_enabled_methods', true ); |
| 405 | $user_eligable = false; |
| 406 | |
| 407 | // Lets check the policy settings and if the user has setup totp/email by checking for the usermeta. |
| 408 | if ( empty( $enabled_method ) && self::is_this_multisite() && 'superadmins-only' === $current_policy ) { |
| 409 | return is_super_admin( $user->ID ); |
| 410 | } else if ( 'all-users' === $current_policy && empty( $enabled_method ) ) { |
| 411 | |
| 412 | if ( isset( $excluded_users ) ) { |
| 413 | $excluded_users = $excluded_users; |
| 414 | } else { |
| 415 | $excluded_users = self::get_wp2fa_setting( 'excluded_users' ); |
| 416 | } |
| 417 | |
| 418 | if ( ! empty( $excluded_users ) ) { |
| 419 | // Turn it into an array. |
| 420 | $excluded_users_array = explode( ',', $excluded_users ); |
| 421 | // Compare our roles with the users and see if we get a match. |
| 422 | $result = in_array( $user->user_login, $excluded_users_array, true ); |
| 423 | if ( ! $result ) { |
| 424 | $user_eligable = true; |
| 425 | } |
| 426 | } |
| 427 | |
| 428 | if ( isset( $excluded_roles ) ) { |
| 429 | $excluded_roles = $excluded_roles; |
| 430 | } else { |
| 431 | $excluded_roles = self::get_wp2fa_setting( 'excluded_roles' ); |
| 432 | } |
| 433 | |
| 434 | if ( ! empty( $excluded_roles ) ) { |
| 435 | // Turn it into an array. |
| 436 | $excluded_roles_array = explode( ',', strtolower( $excluded_roles ) ); |
| 437 | // Compare our roles with the users and see if we get a match. |
| 438 | $result = array_intersect( $excluded_roles_array, $user->roles ); |
| 439 | |
| 440 | if ( ! empty( $result ) ) { |
| 441 | $user_eligable = true; |
| 442 | } |
| 443 | |
| 444 | if ( self::is_this_multisite() ) { |
| 445 | $users_caps = array(); |
| 446 | $subsites = get_sites(); |
| 447 | // Check each site and add to our array so we know each users actual roles. |
| 448 | foreach ( $subsites as $subsite ) { |
| 449 | $subsite_id = get_object_vars( $subsite )['blog_id']; |
| 450 | $users_caps[] = get_user_meta( $user->ID, 'wp_' .$subsite_id .'_capabilities', true ); |
| 451 | } |
| 452 | // Strip the top layer ready. |
| 453 | $users_caps = $users_caps; |
| 454 | foreach ( $users_caps as $key => $value ) { |
| 455 | if ( ! empty( $value ) ) { |
| 456 | foreach ( $value as $key => $value ) { |
| 457 | $result = in_array( $key, $excluded_roles_array, true ); |
| 458 | } |
| 459 | } |
| 460 | } |
| 461 | if ( ! empty( $result ) ) { |
| 462 | return false; |
| 463 | } |
| 464 | } |
| 465 | } |
| 466 | |
| 467 | if ( true === $user_eligable || empty( $enabled_method ) ) { |
| 468 | return true; |
| 469 | } |
| 470 | } elseif ( 'certain-roles-only' === $current_policy && empty( $enabled_method ) ) { |
| 471 | |
| 472 | if ( isset( $enforced_users ) && ! empty( $enforced_users ) ) { |
| 473 | $enforced_users = $enforced_users; |
| 474 | } else { |
| 475 | $enforced_users = self::get_wp2fa_setting( 'enforced_users' ); |
| 476 | } |
| 477 | |
| 478 | if ( ! empty( $enforced_users )) { |
| 479 | // Turn it into an array. |
| 480 | $enforced_users_array = $enforced_users; |
| 481 | // Compare our roles with the users and see if we get a match. |
| 482 | $result = in_array( $user->user_login, $enforced_users_array, true ); |
| 483 | // The user is one of the chosen roles we are forcing 2FA onto, so lets show the nag. |
| 484 | if ( ! empty( $result ) ) { |
| 485 | return true; |
| 486 | } |
| 487 | } |
| 488 | |
| 489 | if ( isset( $enforced_roles ) && ! empty( $enforced_roles ) ) { |
| 490 | $enforced_roles = $enforced_roles; |
| 491 | } else { |
| 492 | $enforced_roles = self::get_wp2fa_setting( 'enforced_roles' ); |
| 493 | } |
| 494 | |
| 495 | if ( ! empty( $enforced_roles ) ) { |
| 496 | // Turn it into an array. |
| 497 | $enforced_roles_array = SettingsPage::extract_roles_from_input( $enforced_roles ); |
| 498 | // Compare our roles with the users and see if we get a match. |
| 499 | $result = array_intersect( $enforced_roles_array, $user->roles ); |
| 500 | // The user is one of the chosen roles we are forcing 2FA onto, so lets show the nag. |
| 501 | if ( ! empty( $result ) ) { |
| 502 | return true; |
| 503 | } |
| 504 | |
| 505 | if ( self::is_this_multisite() ) { |
| 506 | $users_caps = array(); |
| 507 | $subsites = get_sites(); |
| 508 | // Check each site and add to our array so we know each users actual roles. |
| 509 | foreach ( $subsites as $subsite ) { |
| 510 | $subsite_id = get_object_vars( $subsite )['blog_id']; |
| 511 | $users_caps[] = get_user_meta( $user->ID, 'wp_' .$subsite_id .'_capabilities', true ); |
| 512 | } |
| 513 | // Strip the top layer ready. |
| 514 | $users_caps = $users_caps; |
| 515 | foreach ( $users_caps as $key => $value ) { |
| 516 | if ( ! empty( $value ) ) { |
| 517 | foreach ( $value as $key => $value ) { |
| 518 | $result = in_array( $key, $enforced_roles_array, true ); |
| 519 | } |
| 520 | } |
| 521 | } |
| 522 | if ( ! empty( $result ) ) { |
| 523 | return true; |
| 524 | } |
| 525 | } |
| 526 | } |
| 527 | |
| 528 | } elseif ( 'certain-users-only' === $current_policy && empty( $enabled_method ) ) { |
| 529 | |
| 530 | if ( isset( $enforced_users ) && ! empty( $enforced_users ) ) { |
| 531 | $enforced_users = $enforced_users; |
| 532 | } else { |
| 533 | $enforced_users = self::get_wp2fa_setting( 'enforced_users' ); |
| 534 | } |
| 535 | |
| 536 | if ( ! empty( $enforced_users ) ) { |
| 537 | // Turn it into an array. |
| 538 | $enforced_users_array = explode( ',', $enforced_users ); |
| 539 | // Compare our roles with the users and see if we get a match. |
| 540 | $result = in_array( $user->user_login, $enforced_users_array, true ); |
| 541 | // The user is one of the chosen roles we are forcing 2FA onto, so lets show the nag. |
| 542 | if ( ! empty( $result ) ) { |
| 543 | return true; |
| 544 | } |
| 545 | } |
| 546 | } elseif ( 'enforce-on-multisite' === $current_policy ) { |
| 547 | $includedSites = self::get_wp2fa_setting( 'included_sites' ); |
| 548 | |
| 549 | foreach ( $includedSites as $site_id ) { |
| 550 | if ( is_user_member_of_blog( $user_id, $site_id ) ) { |
| 551 | return true; |
| 552 | } |
| 553 | } |
| 554 | } |
| 555 | |
| 556 | return false; |
| 557 | } |
| 558 | |
| 559 | /** |
| 560 | * Util function to grab settings or apply defaults if no settings are saved into the db. |
| 561 | * |
| 562 | * @param string $setting_name Settings to grab value of. |
| 563 | * @param boolean $getDefaultOnEmpty return default setting value if current one is empty |
| 564 | * @param boolean $getDefaultValue return default value setting (ignore the stored ones) |
| 565 | * @return string Settings value or default value. |
| 566 | */ |
| 567 | public static function get_wp2fa_setting( $setting_name = '', $getDefaultOnEmpty = false, $getDefaultValue = false ) { |
| 568 | $default_settings = array( |
| 569 | 'enable_totp' => 'enable_totp', |
| 570 | 'enable_email' => 'enable_email', |
| 571 | 'backup_codes_enabled' => 'yes', |
| 572 | 'enforcement-policy' => 'do-not-enforce', |
| 573 | 'excluded_users' => [], |
| 574 | 'excluded_roles' => [], |
| 575 | 'enforced_users' => [], |
| 576 | 'enforced_roles' => [], |
| 577 | 'grace-period' => 3, |
| 578 | 'grace-period-denominator' => 'days', |
| 579 | 'enable_grace_cron' => '', |
| 580 | 'enable_destroy_session' => '', |
| 581 | 'limit_access' => '', |
| 582 | '2fa_settings_last_updated_by' => '', |
| 583 | '2fa_main_user' => '', |
| 584 | 'grace-period-expiry-time' => '', |
| 585 | 'plugin_version' => WP_2FA_VERSION, |
| 586 | 'delete_data_upon_uninstall' => '', |
| 587 | 'excluded_sites' => '', |
| 588 | 'included_sites' => [], |
| 589 | 'create-custom-user-page' => 'no', |
| 590 | 'redirect-user-custom-page' => '', |
| 591 | 'redirect-user-custom-page-global' => '', |
| 592 | 'custom-user-page-url' => '', |
| 593 | 'custom-user-page-id' => '', |
| 594 | 'hide_remove_button' => '', |
| 595 | 'grace-policy' => 'use-grace-period', |
| 596 | 'default-text-code-page' => __( 'Please enter the two-factor authentication (2FA) verification code below to login. Depending on your 2FA setup, you can get the code from the 2FA app or it was sent to you by email.', 'wp-2fa' ), |
| 597 | ); |
| 598 | |
| 599 | if ( true === $getDefaultValue ) { |
| 600 | if ( isset( $default_settings[ $setting_name ] ) ) { |
| 601 | return $default_settings[ $setting_name ]; |
| 602 | } |
| 603 | |
| 604 | return false; |
| 605 | } |
| 606 | |
| 607 | $apply_defaults = false; |
| 608 | |
| 609 | // If we have no setting name, return them all. |
| 610 | if ( empty( $setting_name ) ) { |
| 611 | return self::$wp_2fa_options; |
| 612 | } |
| 613 | |
| 614 | // First lets check if any options have been saved. |
| 615 | if ( empty( self::$wp_2fa_options ) || ! isset( self::$wp_2fa_options ) ) { |
| 616 | $apply_defaults = true; |
| 617 | } |
| 618 | |
| 619 | if ( $apply_defaults ) { |
| 620 | return $default_settings[ $setting_name ]; |
| 621 | } elseif ( ! isset( self::$wp_2fa_options[ $setting_name ] ) ) { |
| 622 | if ( true === $getDefaultOnEmpty ) { |
| 623 | if ( isset( $default_settings[ $setting_name ] ) ) { |
| 624 | return $default_settings[ $setting_name ]; |
| 625 | } |
| 626 | } |
| 627 | return false; |
| 628 | } else { |
| 629 | return self::$wp_2fa_options[ $setting_name ]; |
| 630 | } |
| 631 | } |
| 632 | |
| 633 | /** |
| 634 | * Util function to grab EMAIL settings or apply defaults if no settings are saved into the db. |
| 635 | * |
| 636 | * @param string $setting_name Settings to grab value of. |
| 637 | * @return string Settings value or default value. |
| 638 | */ |
| 639 | public static function get_wp2fa_email_templates( $setting_name = '', $grab_default = '' ) { |
| 640 | |
| 641 | $apply_defaults = false; |
| 642 | |
| 643 | // First lets check if any options have been saved. |
| 644 | if ( empty( self::$wp_2fa_email_templates ) || ! isset( self::$wp_2fa_email_templates ) ) { |
| 645 | $apply_defaults = true; |
| 646 | } |
| 647 | |
| 648 | // If we have no setting name, return what ever is saved. |
| 649 | if ( empty( $setting_name ) ) { |
| 650 | return self::$wp_2fa_email_templates; |
| 651 | } |
| 652 | |
| 653 | // If we have a saved setting, return it. |
| 654 | if ( $setting_name && isset( self::$wp_2fa_email_templates[ $setting_name ] ) ) { |
| 655 | return self::$wp_2fa_email_templates[ $setting_name ]; |
| 656 | } |
| 657 | |
| 658 | // If we reach this point, we have nothing saved and we are looking for defaults. |
| 659 | // Create Enable 2FA Message. |
| 660 | $enable_2fa_subject = __( 'Please enable 2FA on {site_url}', 'wp-2fa' ); |
| 661 | $enable_2fa_body = ''; |
| 662 | |
| 663 | $enable_2fa_body .= '<p>'. __( 'The administrator of the website', 'wp-2fa' ) .' {site_url} '. esc_html__( 'enforced two-factor authentication. You have', 'wp-2fa' ) .' <strong>{grace_period}</strong> '. esc_html__( 'to enable and configure 2FA on your WordPress user', 'wp-2fa' ) .' <strong>{user_login_name}</strong></p>'; |
| 664 | |
| 665 | if ( ! empty( WP2FA::get_wp2fa_setting( 'custom-user-page-id' ) ) ) { |
| 666 | $enable_2fa_body .= '<p>'. esc_html__( 'You can configure 2FA from this page:', 'wp-2fa' ) .' <a href="{2fa_settings_page_url}" target="_blank">{2fa_settings_page_url}.</a></p>'; |
| 667 | } |
| 668 | |
| 669 | $enable_2fa_body .= '<p>'. __( 'Failing to enable 2FA within the grace period will result in a locked account. In case that happens contact the website’s administrator.', 'wp-2fa' ) .'</p><p>'. esc_html__( 'Thank you.', 'wp-2fa' ) .'</p><p>'. esc_html__( 'Email sent by', 'wp-2fa' ).' <a href="https://www.wpwhitesecurity.com/wordpress-plugins/wp-2fa/" target="_blank">'. esc_html__( 'WP 2FA plugin.', 'wp-2fa' ) .'</a></p>'; |
| 670 | |
| 671 | // Create Login Code Message. |
| 672 | $login_code_subject = __( 'Your login confirmation code for {site_name}', 'wp-2fa' ); |
| 673 | |
| 674 | |
| 675 | $login_code_body = '<p>' . sprintf( |
| 676 | esc_html__( 'Enter %1$s to log in.', 'wp-2fa' ), |
| 677 | '<strong>{login_code}</strong>' |
| 678 | ); |
| 679 | $login_code_body .= '</p>'; |
| 680 | $login_code_body .= '<p>' . esc_html__( 'Thank you.', 'wp-2fa' ) . '</p>'; |
| 681 | $login_code_body .= '<p>' . esc_html__( 'Email sent by', 'wp-2fa' ); |
| 682 | $login_code_body .= ' <a href="https://www.wpwhitesecurity.com/wordpress-plugins/wp-2fa/" target="_blank">' . esc_html__( 'WP 2FA plugin.', 'wp-2fa' ) . '</a>'; |
| 683 | $login_code_body .= '</p>'; |
| 684 | |
| 685 | // Create User Locked Message. |
| 686 | $user_locked_subject = __( 'Your user on {site_name} has been locked', 'wp-2fa' ); |
| 687 | |
| 688 | $user_locked_body = '<p>' . esc_html__( 'Hello.', 'wp-2fa' ) . '</p>'; |
| 689 | $user_locked_body .= '<p>' . sprintf( |
| 690 | esc_html__( 'Since you have not enabled two-factor authentication for the user %1$s on the website %2$s within the grace period, your account has been locked.', 'wp-2fa' ), |
| 691 | '{user_login_name}', |
| 692 | '{site_name}' |
| 693 | ); |
| 694 | $user_locked_body .= '</p>'; |
| 695 | $user_locked_body .= '<p>' . esc_html__( 'Contact your website administrator to unlock your account.', 'wp-2fa' ) . '</p>'; |
| 696 | $user_locked_body .= '<p>' . esc_html__( 'Thank you.', 'wp-2fa' ) . '</p>'; |
| 697 | $user_locked_body .= '<p>' . esc_html__( 'Email sent by', 'wp-2fa' ); |
| 698 | $user_locked_body .= ' <a href="https://www.wpwhitesecurity.com/wordpress-plugins/wp-2fa/" target="_blank">' . esc_html__( 'WP 2FA plugin.', 'wp-2fa' ) . '</a>'; |
| 699 | $user_locked_body .= '</p>'; |
| 700 | |
| 701 | // Create User unlocked Message. |
| 702 | $user_unlocked_subject = __( 'Your user on {site_name} has been unlocked', 'wp-2fa' ); |
| 703 | $user_unlocked_body = ''; |
| 704 | |
| 705 | $user_unlocked_body .= '<p>'. __( 'Hello,', 'wp-2fa' ) .'</p><p>'. esc_html__( 'Your user', 'wp-2fa' ) .' <strong>{user_login_name}</strong> '. esc_html__( 'on the website', 'wp-2fa' ) .' {site_url} '. __( 'has been unlocked. Please configure two-factor authentication within the grace period, otherwise your account will be locked again.', 'wp-2fa' ) .'</p>'; |
| 706 | |
| 707 | if ( ! empty( WP2FA::get_wp2fa_setting( 'custom-user-page-id' ) ) ) { |
| 708 | $user_unlocked_body .= '<p>'. __( 'You can configure 2FA from this page:', 'wp-2fa' ) .' <a href="{2fa_settings_page_url}" target="_blank">{2fa_settings_page_url}.</a></p>'; |
| 709 | } |
| 710 | |
| 711 | $user_unlocked_body .='<p>'. __( 'Thank you.', 'wp-2fa' ) .'</p><p>'. __( 'Email sent by', 'wp-2fa' ) .' <a href="https://www.wpwhitesecurity.com/wordpress-plugins/wp-2fa/" target="_blank">'. __( 'WP 2FA plugin', 'wp-2fa' ) .'</a></p>'; |
| 712 | |
| 713 | // Array of defaults, now we have things setup above. |
| 714 | $default_settings = array( |
| 715 | 'email_from_setting' => 'use-defaults', |
| 716 | 'custom_from_email_address' => '', |
| 717 | 'custom_from_display_name' => '', |
| 718 | 'enforced_email_subject' => $enable_2fa_subject, |
| 719 | 'enforced_email_body' => $enable_2fa_body, |
| 720 | 'login_code_email_subject' => $login_code_subject, |
| 721 | 'login_code_email_body' => $login_code_body, |
| 722 | 'user_account_locked_email_subject' => $user_locked_subject, |
| 723 | 'user_account_locked_email_body' => $user_locked_body, |
| 724 | 'user_account_unlocked_email_subject' => $user_unlocked_subject, |
| 725 | 'user_account_unlocked_email_body' => $user_unlocked_body, |
| 726 | 'send_enforced_email' => 'enable_enforced_email', |
| 727 | 'send_account_locked_email' => 'enable_account_locked_email', |
| 728 | 'send_account_unlocked_email' => 'enable_account_unlocked_email', |
| 729 | ); |
| 730 | |
| 731 | if ( $apply_defaults || ! empty( $grab_default ) ) { |
| 732 | return $default_settings[ $setting_name ]; |
| 733 | } |
| 734 | } |
| 735 | |
| 736 | /** |
| 737 | * Util which we use to replace our {strings} with actual, useful stuff. |
| 738 | * |
| 739 | * @param string $input Text we are working on. |
| 740 | * @param int|string $user_id User id, if its needed. |
| 741 | * @param string $token Login code, if its needed.. |
| 742 | * @return string The output, with all the {strings} swapped out. |
| 743 | */ |
| 744 | public static function replace_email_strings( $input = '', $user_id = '', $token = '', $override_grace_period = '' ) { |
| 745 | |
| 746 | // Gather grace period. |
| 747 | $grace_period_string = ''; |
| 748 | if ( isset( $override_grace_period ) && ! empty( $override_grace_period ) ) { |
| 749 | $grace_period_string = $override_grace_period; |
| 750 | } else { |
| 751 | $grace_policy = self::get_wp2fa_setting( 'grace-policy' ); |
| 752 | $grace_period_string = DateTimeUtils::format_grace_period_expiration_string( $grace_policy ); |
| 753 | } |
| 754 | |
| 755 | // Setup user data. |
| 756 | if ( isset( $user_id ) && ! empty( $user_id ) ) { |
| 757 | $user = get_userdata( $user_id ); |
| 758 | } else { |
| 759 | $user = wp_get_current_user(); |
| 760 | } |
| 761 | |
| 762 | // Setup token. |
| 763 | if ( isset( $token ) && ! empty( $token ) ) { |
| 764 | $login_code = $token; |
| 765 | } else { |
| 766 | $login_code = ''; |
| 767 | } |
| 768 | |
| 769 | $new_page_id = WP2FA::get_wp2fa_setting( 'custom-user-page-id' ); |
| 770 | if ( ! empty( $new_page_id ) ) { |
| 771 | $new_page_permalink = get_permalink( $new_page_id ); |
| 772 | } else { |
| 773 | $new_page_permalink = ''; |
| 774 | } |
| 775 | |
| 776 | // These are the strings we are going to search for, as well as there respective replacements. |
| 777 | $replacements = array( |
| 778 | '{site_url}' => esc_url( get_bloginfo( 'url' ) ), |
| 779 | '{site_name}' => sanitize_text_field( get_bloginfo( 'name' ) ), |
| 780 | '{grace_period}' => sanitize_text_field( $grace_period_string ), |
| 781 | '{user_login_name}' => sanitize_text_field( $user->user_login ), |
| 782 | '{login_code}' => sanitize_text_field( $login_code ), |
| 783 | '{2fa_settings_page_url}' => esc_url( $new_page_permalink ), |
| 784 | ); |
| 785 | |
| 786 | $replacements = apply_filters( |
| 787 | 'wp_2fa_replacement_email_strings', |
| 788 | $replacements |
| 789 | ); |
| 790 | |
| 791 | $final_output = str_replace( array_keys( $replacements ), array_values( $replacements ), $input ); |
| 792 | return $final_output; |
| 793 | } |
| 794 | |
| 795 | /** |
| 796 | * If a user is trying to access anywhere other than the 2FA config area, this blocks them. |
| 797 | */ |
| 798 | public function block_unconfigured_users_from_admin() { |
| 799 | global $pagenow; |
| 800 | |
| 801 | if ( 'use-grace-period' !== WP2FA::get_wp2fa_setting( 'grace-policy' ) ) { |
| 802 | $user = wp_get_current_user(); |
| 803 | $is_user_instantly_enforced = get_user_meta( $user->ID, 'wp_2fa_user_enforced_instantly', true ); |
| 804 | $grace_period_expiry_time = get_user_meta( $user->ID, 'wp_2fa_grace_period_expiry', true ); |
| 805 | $time_now = time(); |
| 806 | if ( $is_user_instantly_enforced && ! empty( $grace_period_expiry_time ) && $grace_period_expiry_time < $time_now ) { |
| 807 | |
| 808 | /* |
| 809 | * We should only allow: |
| 810 | * - 2FA setup wizard in the administration |
| 811 | * - custom 2FA page if enabled and created |
| 812 | * - AJAX requests originating from these 2FA setup UIs |
| 813 | */ |
| 814 | if ( wp_doing_ajax() && isset( $_REQUEST['action'] ) && in_array( $_REQUEST['action'], [ 'send_authentication_setup_email', 'validate_authcode_via_ajax' ] )) { |
| 815 | return; |
| 816 | } |
| 817 | |
| 818 | if ( is_admin() || is_network_admin() ) { |
| 819 | $allowed_admin_page = self::is_this_multisite() ? 'index.php' : 'options-general.php'; |
| 820 | if ( $pagenow === $allowed_admin_page && ( isset( $_GET['page'] ) && $_GET['page'] === 'wp-2fa-setup' ) ) { |
| 821 | return; |
| 822 | } |
| 823 | } |
| 824 | |
| 825 | if ( is_page() ) { |
| 826 | $custom_user_page_id = \WP2FA\WP2FA::get_wp2fa_setting( 'custom-user-page-id' ); |
| 827 | if ( !empty( $custom_user_page_id ) && get_the_ID() == (int) $custom_user_page_id ) { |
| 828 | return; |
| 829 | } |
| 830 | } |
| 831 | |
| 832 | // force a redirect to the 2FA set-up page if it exists |
| 833 | $custom_user_page_id = \WP2FA\WP2FA::get_wp2fa_setting( 'custom-user-page-id' ); |
| 834 | if ( !empty( $custom_user_page_id ) ) { |
| 835 | wp_redirect( get_permalink($custom_user_page_id) ); |
| 836 | exit; |
| 837 | } |
| 838 | |
| 839 | // custom 2FA page is not set-up, force redirect to the wizard in administration |
| 840 | $url = self::is_this_multisite() ? 'index.php' : 'options-general.php'; |
| 841 | wp_redirect( add_query_arg( [ |
| 842 | 'page' => 'wp-2fa-setup' |
| 843 | ], admin_url( $url ) ) ); |
| 844 | exit; |
| 845 | } |
| 846 | } |
| 847 | } |
| 848 | |
| 849 | /** |
| 850 | * Handles AJAX calls for sending test emails. |
| 851 | */ |
| 852 | public function handle_send_test_email_ajax() { |
| 853 | |
| 854 | // check user permissions |
| 855 | if ( ! current_user_can( 'manage_options' ) ) { |
| 856 | wp_send_json_error(); |
| 857 | } |
| 858 | |
| 859 | // check email id |
| 860 | $email_id = filter_input(INPUT_POST, 'email_id', FILTER_SANITIZE_STRING); |
| 861 | if ($email_id === null || $email_id === false) { |
| 862 | wp_send_json_error(); |
| 863 | } |
| 864 | |
| 865 | // check nonce |
| 866 | $nonce = filter_input(INPUT_POST, '_wpnonce', FILTER_SANITIZE_STRING); |
| 867 | if ($nonce === null || $nonce === false || ! wp_verify_nonce($nonce, 'wp-2fa-email-test-' . $email_id)) { |
| 868 | wp_send_json_error(); |
| 869 | } |
| 870 | |
| 871 | $user_id = get_current_user_id(); |
| 872 | // Grab user data. |
| 873 | $user = get_userdata( $user_id ); |
| 874 | // Grab user email. |
| 875 | $email = $user->user_email; |
| 876 | |
| 877 | if ('config_test' === $email_id) { |
| 878 | $email_sent = SettingsPage::send_email( |
| 879 | $email, |
| 880 | esc_html__('Test email from WP 2FA', 'wp-2fa'), |
| 881 | esc_html__('This email was sent by the WP 2FA plugin to test the email delivery.', 'wp-2fa') |
| 882 | ); |
| 883 | if ( $email_sent ) { |
| 884 | wp_send_json_success('Test email was successfully sent to <strong>' . $email . '</strong>' ); |
| 885 | } |
| 886 | |
| 887 | wp_send_json_error(); |
| 888 | } |
| 889 | |
| 890 | /** @var EmailTemplate[] $email_templates */ |
| 891 | $email_templates = $this->settings->get_email_notification_definitions(); |
| 892 | foreach ($email_templates as $email_template) { |
| 893 | if ($email_id === $email_template->getEmailContentId()) { |
| 894 | // send the test email |
| 895 | |
| 896 | |
| 897 | |
| 898 | // Setup the email contents. |
| 899 | $subject = wp_strip_all_tags( WP2FA::get_wp2fa_email_templates( $email_id . '_email_subject' ) ); |
| 900 | $message = wpautop( WP2FA::get_wp2fa_email_templates( $email_id . '_email_body' ), $user_id ); |
| 901 | |
| 902 | $email_sent = SettingsPage::send_email( $email, $subject, $message ); |
| 903 | if ( $email_sent ) { |
| 904 | wp_send_json_success('Test email <strong>' . $email_template->getTitle() . '</strong> was successfully sent to <strong>' . $email . '</strong>' ); |
| 905 | } |
| 906 | |
| 907 | wp_send_json_error(); |
| 908 | } |
| 909 | } |
| 910 | } |
| 911 | |
| 912 | /** |
| 913 | * Collects all the sites from multisite WP installation |
| 914 | * |
| 915 | * @return array |
| 916 | */ |
| 917 | public static function getMultiSites() { |
| 918 | if ( self::is_this_multisite() ) { |
| 919 | if ( empty( self::$sites ) ) { |
| 920 | |
| 921 | self::$sites = \get_sites(); |
| 922 | } |
| 923 | |
| 924 | return self::$sites; |
| 925 | } |
| 926 | |
| 927 | return []; |
| 928 | } |
| 929 | |
| 930 | /** |
| 931 | * Returns text with the number of plugins supported |
| 932 | * |
| 933 | * @since 1.6 |
| 934 | * |
| 935 | * @return string |
| 936 | */ |
| 937 | public static function getNumberOfPluginsText() { |
| 938 | $methodsCount = self::$methodsCount; |
| 939 | |
| 940 | if ( \class_exists('NumberFormatter') ) { |
| 941 | $number_formatter = new \NumberFormatter( get_locale(), \NumberFormatter::SPELLOUT ); |
| 942 | $methodsCount = $number_formatter->format( self::$methodsCount ); |
| 943 | } |
| 944 | |
| 945 | return |
| 946 | sprintf( |
| 947 | \_n( |
| 948 | 'There is %s method available from which you can choose for 2FA:', |
| 949 | 'There are %s methods available from which you can choose for 2FA:', |
| 950 | self::$methodsCount, |
| 951 | 'wp-2fa' |
| 952 | ), |
| 953 | $methodsCount |
| 954 | ); |
| 955 | } |
| 956 | } |
| 957 |