diff --git a/wp-content/plugins/two-factor/assets/banner-1544x500.png b/wp-content/plugins/two-factor/assets/banner-1544x500.png deleted file mode 100644 index 5b6e2081..00000000 Binary files a/wp-content/plugins/two-factor/assets/banner-1544x500.png and /dev/null differ diff --git a/wp-content/plugins/two-factor/assets/banner-772x250.png b/wp-content/plugins/two-factor/assets/banner-772x250.png deleted file mode 100644 index b955eeeb..00000000 Binary files a/wp-content/plugins/two-factor/assets/banner-772x250.png and /dev/null differ diff --git a/wp-content/plugins/two-factor/assets/icon-128x128.png b/wp-content/plugins/two-factor/assets/icon-128x128.png deleted file mode 100644 index 1f3a3c31..00000000 Binary files a/wp-content/plugins/two-factor/assets/icon-128x128.png and /dev/null differ diff --git a/wp-content/plugins/two-factor/assets/icon-256x256.png b/wp-content/plugins/two-factor/assets/icon-256x256.png deleted file mode 100644 index 3240b832..00000000 Binary files a/wp-content/plugins/two-factor/assets/icon-256x256.png and /dev/null differ diff --git a/wp-content/plugins/two-factor/assets/icon.svg b/wp-content/plugins/two-factor/assets/icon.svg deleted file mode 100644 index cc15690b..00000000 --- a/wp-content/plugins/two-factor/assets/icon.svg +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/wp-content/plugins/two-factor/assets/screenshot-1.png b/wp-content/plugins/two-factor/assets/screenshot-1.png deleted file mode 100644 index 001fb2ae..00000000 Binary files a/wp-content/plugins/two-factor/assets/screenshot-1.png and /dev/null differ diff --git a/wp-content/plugins/two-factor/assets/screenshot-2.png b/wp-content/plugins/two-factor/assets/screenshot-2.png deleted file mode 100644 index 9fb4f742..00000000 Binary files a/wp-content/plugins/two-factor/assets/screenshot-2.png and /dev/null differ diff --git a/wp-content/plugins/two-factor/assets/screenshot-3.png b/wp-content/plugins/two-factor/assets/screenshot-3.png deleted file mode 100644 index b866bbb0..00000000 Binary files a/wp-content/plugins/two-factor/assets/screenshot-3.png and /dev/null differ diff --git a/wp-content/plugins/two-factor/class-two-factor-compat.php b/wp-content/plugins/two-factor/class-two-factor-compat.php index d7b4f46a..94e47f4d 100644 --- a/wp-content/plugins/two-factor/class-two-factor-compat.php +++ b/wp-content/plugins/two-factor/class-two-factor-compat.php @@ -11,11 +11,15 @@ * Should be used with care because ideally we wouldn't need * any integration specific code for this plugin. Everything should * be handled through clever use of hooks and best practices. + * + * @since 0.5.0 */ class Two_Factor_Compat { /** * Initialize all the custom hooks as necessary. * + * @since 0.5.0 + * * @return void */ public function init() { @@ -30,6 +34,8 @@ class Two_Factor_Compat { /** * Jetpack single sign-on wants long-lived sessions for users. * + * @since 0.5.0 + * * @param boolean $rememberme Current state of the "remember me" toggle. * * @return boolean @@ -47,6 +53,8 @@ class Two_Factor_Compat { /** * Helper to detect the presence of the active SSO module. * + * @since 0.5.0 + * * @return boolean */ public function jetpack_is_sso_active() { diff --git a/wp-content/plugins/two-factor/class-two-factor-core.php b/wp-content/plugins/two-factor/class-two-factor-core.php index 5eaa765a..d98cbfe6 100644 --- a/wp-content/plugins/two-factor/class-two-factor-core.php +++ b/wp-content/plugins/two-factor/class-two-factor-core.php @@ -1,6 +1,6 @@ init(); } + /** + * Register login page scripts. + * + * @since 0.10.0 + * + * @codeCoverageIgnore + */ + public static function login_enqueue_scripts() { + $environment_prefix = file_exists( TWO_FACTOR_DIR . '/dist' ) ? '/dist' : ''; + + wp_register_script( + 'two-factor-login', + plugins_url( $environment_prefix . '/providers/js/two-factor-login.js', __FILE__ ), + array(), + TWO_FACTOR_VERSION, + true + ); + + wp_register_script( + 'two-factor-login-authcode', + plugins_url( $environment_prefix . '/providers/js/two-factor-login-authcode.js', __FILE__ ), + array(), + TWO_FACTOR_VERSION, + true + ); + } + /** * Delete all plugin data on uninstall. * + * @since 0.10.0 + * * @return void */ public static function uninstall() { @@ -167,8 +209,7 @@ class Two_Factor_Core { $user_meta_keys, call_user_func( array( $provider_class, 'uninstall_user_meta_keys' ) ) ); - } catch ( Exception $e ) { - // Do nothing. + } catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- Intentionally empty, provider may not implement this method. } } @@ -179,8 +220,7 @@ class Two_Factor_Core { $option_keys, call_user_func( array( $provider_class, 'uninstall_options' ) ) ); - } catch ( Exception $e ) { - // Do nothing. + } catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- Intentionally empty, provider may not implement this method. } } } @@ -200,13 +240,14 @@ class Two_Factor_Core { /** * Get the registered providers of which some might not be enabled. * + * @since 0.11.0 + * * @return array List of provider keys and paths to class files. */ private static function get_default_providers() { return array( 'Two_Factor_Email' => TWO_FACTOR_DIR . 'providers/class-two-factor-email.php', 'Two_Factor_Totp' => TWO_FACTOR_DIR . 'providers/class-two-factor-totp.php', - 'Two_Factor_FIDO_U2F' => TWO_FACTOR_DIR . 'providers/class-two-factor-fido-u2f.php', 'Two_Factor_Backup_Codes' => TWO_FACTOR_DIR . 'providers/class-two-factor-backup-codes.php', 'Two_Factor_Dummy' => TWO_FACTOR_DIR . 'providers/class-two-factor-dummy.php', ); @@ -215,6 +256,8 @@ class Two_Factor_Core { /** * Get the classnames for specific providers. * + * @since 0.10.0 + * * @param array $providers List of paths to provider class files indexed by class names. * * @return array List of provider keys and classnames. @@ -230,6 +273,8 @@ class Two_Factor_Core { /** * Filters the classname for a provider. The dynamic portion of the filter is the defined providers key. * + * @since 0.9.0 + * * @param string $class The PHP Classname of the provider. * @param string $path The provided provider path to be included. */ @@ -255,9 +300,9 @@ class Two_Factor_Core { * @see Two_Factor_Core::get_enabled_providers_for_user() * @see Two_Factor_Core::get_supported_providers_for_user() * - * @since 0.1-dev + * @since 0.2.0 * - * @return array + * @return Two_Factor_Provider[] */ public static function get_providers() { $providers = self::get_default_providers(); @@ -268,23 +313,13 @@ class Two_Factor_Core { * This lets third-parties either remove providers (such as Email), or * add their own providers (such as text message or Clef). * + * @since 0.1-dev + * * @param array $providers A key-value array where the key is the class name, and * the value is the path to the file containing the class. */ $providers = apply_filters( 'two_factor_providers', $providers ); - // FIDO U2F is PHP 5.3+ only. - if ( isset( $providers['Two_Factor_FIDO_U2F'] ) && version_compare( PHP_VERSION, '5.3.0', '<' ) ) { - unset( $providers['Two_Factor_FIDO_U2F'] ); - trigger_error( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error - sprintf( - /* translators: %s: version number */ - __( 'FIDO U2F is not available because you are using PHP %s. (Requires 5.3 or greater)', 'two-factor' ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - PHP_VERSION - ) - ); - } - // Map provider keys to classes so that we can instantiate them. $providers = self::get_providers_classes( $providers ); @@ -303,11 +338,14 @@ class Two_Factor_Core { /** * Get providers available for user which may not be enabled or configured. * + * @since 0.13.0 + * * @see Two_Factor_Core::get_enabled_providers_for_user() * @see Two_Factor_Core::get_available_providers_for_user() * * @param WP_User|int|null $user User ID. - * @return array List of provider instances indexed by provider key. + * + * @return Two_Factor_Provider[] List of provider instances indexed by provider key. */ public static function get_supported_providers_for_user( $user = null ) { $user = self::fetch_user( $user ); @@ -325,6 +363,8 @@ class Two_Factor_Core { /** * Enable the dummy method only during debugging. * + * @since 0.5.2 + * * @param array $methods List of enabled methods. * * @return array @@ -337,9 +377,74 @@ class Two_Factor_Core { return $methods; } + /** + * Add Plugin and User Settings link to the plugin action links on the Plugins screen. + * + * @since 0.14.3 + * + * @param string[] $links An array of plugin action links. + * @return string[] Modified array with the User Settings link added. + */ + public static function add_settings_action_link( $links ) { + $plugin_settings_url = admin_url( 'options-general.php?page=two-factor-settings' ); + $plugin_settings_link = sprintf( + '%s', + esc_url( $plugin_settings_url ), + esc_html__( 'Plugin Settings', 'two-factor' ) + ); + + $user_settings_url = admin_url( 'profile.php#application-passwords-section' ); + $user_settings_link = sprintf( + '%s', + esc_url( $user_settings_url ), + esc_html__( 'User Settings', 'two-factor' ) + ); + + // Show plugin settings first, then user settings. + array_unshift( $links, $user_settings_link ); + + if ( current_user_can( 'manage_options' ) ) { + array_unshift( $links, $plugin_settings_link ); + } + + return $links; + } + + /** + * Register an error associated with the current request. + * + * @param WP_Error $error Error instance. + + * @return void + */ + private static function add_error( WP_Error $error ) { + self::$profile_errors[ $error->get_error_code() ] = $error; + } + + /** + * Attach Two-Factor profile errors to WordPress core profile update errors. + * + * @since NEXT + * + * @param WP_Error $errors WP_Error object passed by core. + * + * @return void + */ + public static function action_user_profile_update_errors( WP_Error $errors ) { + foreach ( self::$profile_errors as $profile_error ) { + foreach ( $profile_error->get_error_codes() as $code ) { + foreach ( $profile_error->get_error_messages( $code ) as $message ) { + $errors->add( $code, $message ); + } + } + } + } + /** * Check if the debug mode is enabled. * + * @since 0.5.2 + * * @return boolean */ protected static function is_wp_debug() { @@ -352,6 +457,8 @@ class Two_Factor_Core { * Fetch this from the plugin core after we introduce proper dependency injection * and get away from the singletons at the provider level (should be handled by core). * + * @since 0.5.2 + * * @param integer $user_id User ID. * * @return string @@ -372,6 +479,8 @@ class Two_Factor_Core { /** * Get the URL for resetting the secret token. * + * @since 0.5.2 + * * @param integer $user_id User ID. * @param string $action Custom two factor action key. * @@ -393,6 +502,8 @@ class Two_Factor_Core { /** * Get the two-factor revalidate URL. * + * @since 0.9.0 + * * @param bool $interim If the URL should load the interim login iframe modal. * @return string */ @@ -410,6 +521,8 @@ class Two_Factor_Core { /** * Check if a user action is valid. * + * @since 0.5.2 + * * @param integer $user_id User ID. * @param string $action User action ID. * @@ -431,6 +544,8 @@ class Two_Factor_Core { /** * Get the ID of the user being edited. * + * @since 0.5.2 + * * @return integer */ public static function current_user_being_edited() { @@ -450,6 +565,8 @@ class Two_Factor_Core { * Trigger our custom update action if a valid * action request is detected and passes the nonce check. * + * @since 0.5.2 + * * @return void */ public static function trigger_user_settings_action() { @@ -472,6 +589,8 @@ class Two_Factor_Core { * Keep track of all the authentication cookies that need to be * invalidated before the second factor authentication. * + * @since 0.5.1 + * * @param string $cookie Cookie string. * * @return void @@ -511,11 +630,14 @@ class Two_Factor_Core { * Get two-factor providers that are enabled for the specified (or current) user * but might not be configured, yet. * + * @since 0.2.0 + * * @see Two_Factor_Core::get_supported_providers_for_user() * @see Two_Factor_Core::get_available_providers_for_user() * * @param int|WP_User $user Optional. User ID, or WP_User object of the the user. Defaults to current user. - * @return array + * + * @return string[] List of keys of enabled providers for the user. */ public static function get_enabled_providers_for_user( $user = null ) { $user = self::fetch_user( $user ); @@ -533,6 +655,8 @@ class Two_Factor_Core { /** * Filter the enabled two-factor authentication providers for this user. * + * @since 0.5.2 + * * @param array $enabled_providers The enabled providers. * @param int $user_id The user ID. */ @@ -543,11 +667,13 @@ class Two_Factor_Core { * Get all two-factor providers that are both enabled and configured * for the specified (or current) user. * + * @since 0.2.0 + * * @see Two_Factor_Core::get_supported_providers_for_user() * @see Two_Factor_Core::get_enabled_providers_for_user() * * @param int|WP_User $user Optional. User ID, or WP_User object of the the user. Defaults to current user. - * @return array List of provider instances. + * @return Two_Factor_Provider[]|WP_Error List of provider instances, or a WP_Error if all configured providers are unavailable. */ public static function get_available_providers_for_user( $user = null ) { $user = self::fetch_user( $user ); @@ -558,6 +684,31 @@ class Two_Factor_Core { $providers = self::get_supported_providers_for_user( $user ); // Returns full objects. $enabled_providers = self::get_enabled_providers_for_user( $user ); // Returns just the keys. $configured_providers = array(); + $user_providers_raw = get_user_meta( $user->ID, self::ENABLED_PROVIDERS_USER_META_KEY, true ); + + /** + * If the user had enabled providers, but none of them exist currently, + * if emailed codes is available force it to be on, so that deprecated + * or removed providers don't result in the two-factor requirement being + * removed and 'failing open'. + * + * Possible enhancement: add a filter to change the fallback method? + */ + if ( empty( $enabled_providers ) && $user_providers_raw ) { + if ( isset( $providers['Two_Factor_Email'] ) ) { + // Force Emailed codes to 'on'. + $enabled_providers[] = 'Two_Factor_Email'; + } else { + return new WP_Error( + 'no_available_2fa_methods', + __( 'Error: You have Two Factor method(s) enabled, but the provider(s) no longer exist. Please contact a site administrator for assistance.', 'two-factor' ), + array( + 'user_providers_raw' => $user_providers_raw, + 'available_providers' => array_keys( $providers ), + ) + ); + } + } foreach ( $providers as $provider_key => $provider ) { if ( in_array( $provider_key, $enabled_providers, true ) && $provider->is_available_for_user( $user ) ) { @@ -571,6 +722,8 @@ class Two_Factor_Core { /** * Fetch the provider for the request based on the user preferences. * + * @since 0.9.0 + * * @param int|WP_User $user Optional. User ID, or WP_User object of the the user. Defaults to current user. * @param null|string|object $preferred_provider Optional. The name of the provider, the provider, or empty. * @return null|object The provider @@ -596,7 +749,7 @@ class Two_Factor_Core { if ( is_string( $preferred_provider ) ) { $providers = self::get_available_providers_for_user( $user ); - if ( isset( $providers[ $preferred_provider ] ) ) { + if ( ! is_wp_error( $providers ) && isset( $providers[ $preferred_provider ] ) ) { return $providers[ $preferred_provider ]; } } @@ -608,6 +761,8 @@ class Two_Factor_Core { * Get the name of the primary provider selected by the user * and enabled for the user. * + * @since 0.12.0 + * * @param WP_User|int $user User ID or instance. * * @return string|null @@ -626,7 +781,7 @@ class Two_Factor_Core { /** * Gets the Two-Factor Auth provider for the specified|current user. * - * @since 0.1-dev + * @since 0.2.0 * * @param int|WP_User $user Optional. User ID, or WP_User object of the the user. Defaults to current user. * @return object|null @@ -643,13 +798,16 @@ class Two_Factor_Core { // If there's only one available provider, force that to be the primary. if ( empty( $available_providers ) ) { return null; + } elseif ( is_wp_error( $available_providers ) ) { + // If it returned an error, the configured methods don't exist, and it couldn't swap in a replacement. + wp_die( $available_providers ); } elseif ( 1 === count( $available_providers ) ) { $provider = key( $available_providers ); } else { $provider = self::get_primary_provider_key_selected_for_user( $user ); // If the provider specified isn't enabled, just grab the first one that is. - if ( ! isset( $available_providers[ $provider ] ) ) { + if ( empty( $provider ) || ! isset( $available_providers[ $provider ] ) ) { $provider = key( $available_providers ); } } @@ -657,6 +815,8 @@ class Two_Factor_Core { /** * Filter the two-factor authentication provider used for this user. * + * @since 0.2.0 + * * @param string $provider The provider currently being used. * @param int $user_id The user ID. */ @@ -672,7 +832,7 @@ class Two_Factor_Core { /** * Quick boolean check for whether a given user is using two-step. * - * @since 0.1-dev + * @since 0.2.0 * * @param int|WP_User $user Optional. User ID, or WP_User object of the the user. Defaults to current user. * @return bool @@ -685,7 +845,9 @@ class Two_Factor_Core { /** * Handle the browser-based login. * - * @since 0.1-dev + * @since 0.2.0 + * + * @see https://developer.wordpress.org/reference/hooks/wp_login/ * * @param string $user_login Username. * @param WP_User $user WP_User object of the logged-in user. @@ -712,6 +874,8 @@ class Two_Factor_Core { * having access to the authentication cookies which are just being set * on the first password-based authentication request. * + * @since 0.5.1 + * * @param \WP_User $user User object. * * @return void @@ -725,63 +889,70 @@ class Two_Factor_Core { } /** - * Prevent login through XML-RPC and REST API for users with at least one - * two-factor method enabled. + * Disable WP core login cookies for users that require second factor. Disable + * authenticated API requests unless explicitly enabled for the user (disabled by default). * - * @param WP_User|WP_Error $user Valid WP_User only if the previous filters + * @since 0.4.0 + * + * @param WP_User|WP_Error $user Valid WP_User only if the previous filters * have verified and confirmed the * authentication credentials. * * @return WP_User|WP_Error */ public static function filter_authenticate( $user ) { - if ( $user instanceof WP_User && self::is_api_request() && self::is_user_using_two_factor( $user->ID ) && ! self::is_user_api_login_enabled( $user->ID ) ) { - return new WP_Error( - 'invalid_application_credentials', - __( 'Error: API login for user disabled.', 'two-factor' ) - ); - } - - return $user; - } - - /** - * Prevent login cookies being set on login for Two Factor users. - * - * This makes it so that Core never sends the auth cookies. `login_form_validate_2fa()` will send them manually once the 2nd factor has been verified. - * - * @param WP_User|WP_Error $user Valid WP_User only if the previous filters - * have verified and confirmed the - * authentication credentials. - * - * @return WP_User|WP_Error - */ - public static function filter_authenticate_block_cookies( $user ) { - /* - * NOTE: The `login_init` action is checked for here to ensure we're within the regular login flow, - * rather than through an unsupported 3rd-party login process which this plugin doesn't support. - */ - if ( $user instanceof WP_User && self::is_user_using_two_factor( $user->ID ) && did_action( 'login_init' ) ) { + if ( $user instanceof WP_User && self::is_user_using_two_factor( $user->ID ) ) { + /** + * Prevent WP core from sending login cookies during `wp_set_auth_cookie()` and + * let two-factor do it after validating the second factor. + */ add_filter( 'send_auth_cookies', '__return_false', PHP_INT_MAX ); + + // Disable authentication requests for API requests for users with two-factor enabled. + if ( self::is_api_request() && ! self::is_user_api_login_enabled( $user->ID ) ) { + return new WP_Error( + 'invalid_application_credentials', + __( 'Error: API login for user disabled.', 'two-factor' ) + ); + } } return $user; } /** - * If the current user can login via API requests such as XML-RPC and REST. + * If the user can login via API requests such as XML-RPC and REST. + * + * Only logins with application passwords are permitted by default. + * + * @since 0.4.0 * * @param integer $user_id User ID. * * @return boolean */ public static function is_user_api_login_enabled( $user_id ) { - return (bool) apply_filters( 'two_factor_user_api_login_enable', false, $user_id ); + /** + * Allow or prevent logins without two-factor during + * API requests such as XML-RPC and REST. + * + * @since 0.4.0 + * + * @param boolean $enabled Whether the user can login via API requests. + * @param integer $user_id User ID. + */ + return (bool) apply_filters( + 'two_factor_user_api_login_enable', + (bool) did_action( 'application_password_did_authenticate' ), + $user_id + ); } /** * Is the current request an XML-RPC or REST request. * + * @since 0.4.0 + * * @return boolean */ public static function is_api_request() { @@ -799,7 +970,7 @@ class Two_Factor_Core { /** * Display the login form. * - * @since 0.1-dev + * @since 0.2.0 * * @param WP_User $user WP_User object of the logged-in user. */ @@ -821,6 +992,8 @@ class Two_Factor_Core { /** * Displays a message informing the user that their account has had failed login attempts. * + * @since 0.8.0 + * * @param WP_User $user WP_User object of the logged-in user. */ public static function maybe_show_last_login_failure_notice( $user ) { @@ -830,14 +1003,17 @@ class Two_Factor_Core { if ( $last_failed_two_factor_login ) { echo '
'; } @@ -849,7 +1025,9 @@ class Two_Factor_Core { * They were also sent an email notification in `send_password_reset_email()`, but email sent from a typical * web server is not reliable enough to trust completely. * - * @param WP_Error $errors + * @since 0.8.0 + * + * @param WP_Error $errors Error object. */ public static function maybe_show_reset_password_notice( $errors ) { if ( 'incorrect_password' !== $errors->get_error_code() ) { @@ -880,6 +1058,7 @@ class Two_Factor_Core { $errors->add( 'two_factor_password_reset', sprintf( + /* translators: %s: URL to create a new password. */ __( 'Your password was reset because of too many failed Two Factor attempts. You will need to create a new password to regain access. Please check your email for more information.', 'two-factor' ), esc_url( add_query_arg( 'action', 'lostpassword', wp_login_url() ) ) ) @@ -891,7 +1070,9 @@ class Two_Factor_Core { /** * Clear the password reset notice after the user resets their password. * - * @param WP_User $user + * @since 0.8.0 + * + * @param WP_User $user User object. */ public static function clear_password_reset_notice( $user ) { delete_user_meta( $user->ID, self::USER_PASSWORD_WAS_RESET_KEY ); @@ -900,18 +1081,19 @@ class Two_Factor_Core { /** * Generates the html form for the second step of the authentication process. * - * @since 0.1-dev + * @since 0.9.0 * * @param WP_User $user WP_User object of the logged-in user. * @param string $login_nonce A string nonce stored in usermeta. * @param string $redirect_to The URL to which the user would like to be redirected. * @param string $error_msg Optional. Login error message. * @param string|object $provider An override to the provider. + * @param string $action Action to perform. */ public static function login_html( $user, $login_nonce, $redirect_to, $error_msg = '', $provider = null, $action = 'validate_2fa' ) { $provider = self::get_provider_for_user( $user, $provider ); if ( ! $provider ) { - wp_die( __( 'Cheatin’ uh?', 'two-factor' ) ); + wp_die( esc_html__( 'Two-factor provider not available for this user.', 'two-factor' ) ); } $provider_key = $provider->get_key(); @@ -921,6 +1103,11 @@ class Two_Factor_Core { $rememberme = intval( self::rememberme() ); + if ( is_wp_error( $available_providers ) ) { + // If it returned an error, the configured methods don't exist, and it couldn't swap in a replacement. + wp_die( $available_providers ); + } + if ( ! function_exists( 'login_header' ) ) { // We really should migrate login_header() out of `wp-login.php` so it can be called from an includes file. require_once TWO_FACTOR_DIR . 'includes/function.login-header.php'; @@ -929,6 +1116,8 @@ class Two_Factor_Core { // Disable the language switcher. add_filter( 'login_display_language_dropdown', '__return_false' ); + wp_enqueue_style( 'user-edit-2fa', plugins_url( 'user-edit.css', __FILE__ ), array(), TWO_FACTOR_VERSION ); + login_header(); if ( ! empty( $error_msg ) ) { @@ -952,7 +1141,10 @@ class Two_Factor_Core { authentication_page( $user ); ?> - $action, 'wp-auth-id' => $user->ID, @@ -967,22 +1159,40 @@ class Two_Factor_Core { if ( $interim_login ) { $backup_link_args['interim-login'] = 1; } - ?> + + foreach ( $backup_providers as $backup_provider_key => $backup_provider ) { + $backup_link_args['provider'] = $backup_provider_key; + $links[] = array( + 'url' => self::login_url( $backup_link_args ), + 'label' => $backup_provider->get_alternative_provider_label(), + ); + } + } + + /** + * Filters the links displayed on the two-factor login form. + * + * Plugins can use this filter to modify or add links to the two-factor authentication + * login form, allowing users to select backup methods for authentication or provide documentation links. + * + * @since 0.16.0 + * + * @param array $links An array of links displayed on the two-factor login form, each with `url` and `label` keys. + */ + $links = apply_filters( 'two_factor_login_backup_links', $links ); + ?> + +