From 70351dc33d9f97ed9da786c3f3e6b700300c7fc3 Mon Sep 17 00:00:00 2001 From: Joe Maller Date: Sun, 7 Jun 2026 14:09:52 -0400 Subject: [PATCH 1/4] email-token hooks and tests --- providers/class-two-factor-email.php | 28 ++++++- tests/providers/class-two-factor-email.php | 93 ++++++++++++++++++++++ 2 files changed, 119 insertions(+), 2 deletions(-) diff --git a/providers/class-two-factor-email.php b/providers/class-two-factor-email.php index e6ca9bf7..d21b7fe5 100644 --- a/providers/class-two-factor-email.php +++ b/providers/class-two-factor-email.php @@ -312,22 +312,46 @@ public function generate_and_email_token( $user ) { * Filters the token email subject. * * @since 0.5.2 + * @deprecated 0.17.0 Use {@see 'two_factor_email_token_subject'} instead. * * @param string $subject The email subject line. * @param int $user_id The ID of the user. */ - $subject = apply_filters( 'two_factor_token_email_subject', $subject, $user->ID ); + $subject = apply_filters_deprecated( 'two_factor_token_email_subject', array( $subject, $user->ID ), '0.11.0', 'two_factor_email_token_subject' ); + + /** + * Filters the token email subject. + * + * @since 0.17.0 + * + * @param string $subject The email subject line. + * @param string $token The token. + * @param int $user_id The ID of the user. + */ + $subject = apply_filters( 'two_factor_email_token_subject', $subject, $token, $user->ID ); /** * Filters the token email message. * * @since 0.5.2 + * @deprecated 0.17.0 Use {@see 'two_factor_email_token_message'} instead. + * + * @param string $message The email message. + * @param string $token The token. + * @param int $user_id The ID of the user. + */ + $message = apply_filters_deprecated( 'two_factor_token_email_message', array( $message, $token, $user->ID ), '0.11.0', 'two_factor_email_token_message' ); + + /** + * Filters the token email message. + * + * @since 0.17.0 * * @param string $message The email message. * @param string $token The token. * @param int $user_id The ID of the user. */ - $message = apply_filters( 'two_factor_token_email_message', $message, $token, $user->ID ); + $message = apply_filters( 'two_factor_email_token_message', $message, $token, $user->ID ); return wp_mail( $user->user_email, $subject, $message ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_mail_wp_mail } diff --git a/tests/providers/class-two-factor-email.php b/tests/providers/class-two-factor-email.php index 8b37a983..161cd1b9 100644 --- a/tests/providers/class-two-factor-email.php +++ b/tests/providers/class-two-factor-email.php @@ -433,6 +433,99 @@ function () { remove_all_filters( 'two_factor_token_ttl' ); } + /** + * Test that the token email subject can be filtered. + * + * @expectedDeprecated two_factor_token_email_subject + */ + public function test_email_token_subject_filter() { + $user = self::factory()->user->create_and_get(); + $this->provider->generate_and_email_token( $user ); + $default_email = end( self::$mockmailer->mock_sent ); + + // Test deprecated filter + add_filter( + 'two_factor_token_email_subject', + function () { + return 'New Subject'; + } + ); + + $this->provider->generate_and_email_token( $user ); + $custom_email_deprecated = end( self::$mockmailer->mock_sent ); + + $this->assertNotEquals( $default_email['subject'], $custom_email_deprecated['subject'], 'Email subject modified by filter' ); + $this->assertEquals( 'New Subject', $custom_email_deprecated['subject'], 'Email subject matches the filter value' ); + + remove_all_filters( 'two_factor_token_email_subject' ); + + // Test new filter + add_filter( + 'two_factor_email_token_subject', + function ( $subject, $token ) { + return "Your token is: {$token}!"; + }, + 10, + 2 + ); + + $this->provider->generate_and_email_token( $user ); + $custom_email = end( self::$mockmailer->mock_sent ); + + $this->assertNotEquals( $default_email['subject'], $custom_email['subject'], 'Email subject modified by filter' ); + $this->assertMatchesRegularExpression( '/Your token is: [0-9]+!/', $custom_email['subject'], 'Email subject matches the filter value' ); + + remove_all_filters( 'two_factor_email_token_subject' ); + } + + /** + * Test that the token email message can be filtered. + * + * @expectedDeprecated two_factor_token_email_message + */ + public function test_email_token_message_filter() { + $user = self::factory()->user->create_and_get(); + $this->provider->generate_and_email_token( $user ); + $default_email = end( self::$mockmailer->mock_sent ); + + // deprecated filter was renamed, use the same callback to test both filters + $callback = function ( $message, $token ) { + return "$token"; + }; + + // Test deprecated filter + add_filter( + 'two_factor_token_email_message', + $callback, + 10, + 2 + ); + + $this->provider->generate_and_email_token( $user ); + $custom_email_deprecated = end( self::$mockmailer->mock_sent ); + + $this->assertNotEquals( $default_email['body'], $custom_email_deprecated['body'], 'Email message modified by filter' ); + $this->assertMatchesRegularExpression( '/[0-9]+<\/span>/', $custom_email_deprecated['body'], 'Email messages contains the wrapped token' ); + + remove_all_filters( 'two_factor_token_email_message' ); + + // Test new filter + add_filter( + 'two_factor_email_token_message', + $callback, + 10, + 2 + ); + + $this->provider->generate_and_email_token( $user ); + $custom_email = end( self::$mockmailer->mock_sent ); + + $this->assertNotEquals( $default_email['body'], $custom_email['body'], 'Email message modified by filter' ); + $this->assertMatchesRegularExpression( '/[0-9]+<\/span>/', $custom_email['body'], 'Email messages contains the wrapped token' ); + + remove_all_filters( 'two_factor_email_token_message' ); + } + /** * Verify the alternative provider label contains expected text. * From d43c36445b65cd79e8d8751dcabc51c3568fc29b Mon Sep 17 00:00:00 2001 From: Joe Maller Date: Sun, 7 Jun 2026 14:49:54 -0400 Subject: [PATCH 2/4] Add updated filters to readme.txt --- readme.txt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/readme.txt b/readme.txt index 149998a6..71bd36aa 100644 --- a/readme.txt +++ b/readme.txt @@ -96,6 +96,8 @@ Here is a list of action and filter hooks provided by the plugin: - `two_factor_user_api_login_enable` filter restricts authentication for REST API and XML-RPC to application passwords only. Provides the user ID as the second argument. - `two_factor_email_token_ttl` filter overrides the time interval in seconds that an email token is considered after generation. Accepts the time in seconds as the first argument and the ID of the `WP_User` object being authenticated. - `two_factor_email_token_length` filter overrides the default 8 character count for email tokens. +- `two_factor_email_token_subject` filter overrides the subject of messages sent by the email provider. Accepts the login token as the second argument. +- `two_factor_email_token_message` filter overrides the body of messages sent by the email provider. Accepts the message text as the first argument, the login token as the second argument, and the user ID as the third argument. - `two_factor_backup_code_length` filter overrides the default 8 character count for backup codes. Provides the `WP_User` of the associated user as the second argument. - `two_factor_rest_api_can_edit_user` filter overrides whether a user’s Two-Factor settings can be edited via the REST API. First argument is the current `$can_edit` boolean, the second argument is the user ID. - `two_factor_before_authentication_prompt` action which receives the provider object and fires prior to the prompt shown on the authentication input form. @@ -140,15 +142,15 @@ The plugin previously supported FIDO U2F, which was a predecessor to WebAuthn. T Yes. For passkeys and hardware security keys, you can install the [Two-Factor Provider: WebAuthn plugin](https://wordpress.org/plugins/two-factor-provider-webauthn/). It integrates directly with Two-Factor and adds WebAuthn-based authentication as an additional two-factor option for users. = Does this plugin work on WordPress Multisite? = - + Yes. The Two-Factor plugin is compatible with WordPress Multisite. Each user configures their own 2FA settings via their profile, and because authentication codes are stored in WordPress user meta, the configuration is tied to the user account and valid across all sites in the network. However, there are no network-wide settings — a super admin cannot enforce or configure 2FA globally from the Network Admin dashboard. To manage 2FA for a specific user, edit their profile on any site where they have an account. - + = How do I disable 2FA for a user who is locked out? = - + As an administrator, go to **Users → All Users** in the WordPress admin, click **Edit** on the affected user's profile, scroll down to the **Two-Factor Options** section, and uncheck all enabled methods, then click **Update User**. This will remove 2FA for that user, allowing them to log in with their password alone. You can also do this via WP-CLI with `wp user meta delete _two_factor_enabled_providers`. Once they're back in, encourage them to re-enable 2FA and generate fresh backup codes. - + = Can I require 2FA for all users or specific roles? = - + Not through the plugin's interface — there are no built-in enforcement settings. However, developers can use the `two_factor_providers_for_user` filter to control which providers are available per user or role, and combine it with custom logic to redirect users who haven't set up 2FA. Native enforcement support is a known and tracked feature request — follow the discussion at [GitHub issue #255](https://github.com/WordPress/two-factor/issues/255). From c35ff179b92da4b2f6b2a1bffda755334c8c61d3 Mon Sep 17 00:00:00 2001 From: Joe Maller Date: Sat, 1 Aug 2026 11:15:41 -0400 Subject: [PATCH 3/4] update test comments for phpcs/phpstan --- tests/providers/class-two-factor-email.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/providers/class-two-factor-email.php b/tests/providers/class-two-factor-email.php index 2240d10e..d2054d77 100644 --- a/tests/providers/class-two-factor-email.php +++ b/tests/providers/class-two-factor-email.php @@ -437,6 +437,7 @@ function () { /** * Test that the token email subject can be filtered. + * Tests cover current and deprecated filters. * * @expectedDeprecated two_factor_token_email_subject */ @@ -445,7 +446,6 @@ public function test_email_token_subject_filter() { $this->provider->generate_and_email_token( $user ); $default_email = end( self::$mockmailer->mock_sent ); - // Test deprecated filter add_filter( 'two_factor_token_email_subject', function () { @@ -461,7 +461,6 @@ function () { remove_all_filters( 'two_factor_token_email_subject' ); - // Test new filter add_filter( 'two_factor_email_token_subject', function ( $subject, $token ) { @@ -482,6 +481,7 @@ function ( $subject, $token ) { /** * Test that the token email message can be filtered. + * Tests cover current and deprecated filters. * * @expectedDeprecated two_factor_token_email_message */ @@ -490,12 +490,11 @@ public function test_email_token_message_filter() { $this->provider->generate_and_email_token( $user ); $default_email = end( self::$mockmailer->mock_sent ); - // deprecated filter was renamed, use the same callback to test both filters + // Deprecated filter was renamed, use the same callback to test both filters. $callback = function ( $message, $token ) { return "$token"; }; - // Test deprecated filter add_filter( 'two_factor_token_email_message', $callback, @@ -511,7 +510,6 @@ public function test_email_token_message_filter() { remove_all_filters( 'two_factor_token_email_message' ); - // Test new filter add_filter( 'two_factor_email_token_message', $callback, From 7cc24eb3f35b978404a4573f32fbcf7616673b2f Mon Sep 17 00:00:00 2001 From: Joe Maller Date: Sat, 1 Aug 2026 12:10:03 -0400 Subject: [PATCH 4/4] Resolve Copilot feedback --- providers/class-two-factor-email.php | 2 +- readme.txt | 2 +- tests/providers/class-two-factor-email.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/providers/class-two-factor-email.php b/providers/class-two-factor-email.php index 20627a5d..e8f97b90 100644 --- a/providers/class-two-factor-email.php +++ b/providers/class-two-factor-email.php @@ -317,7 +317,7 @@ public function generate_and_email_token( $user ) { * @param string $subject The email subject line. * @param int $user_id The ID of the user. */ - $subject = apply_filters_deprecated( 'two_factor_token_email_subject', array( $subject, $user->ID ), '0.11.0', 'two_factor_email_token_subject' ); + $subject = apply_filters_deprecated( 'two_factor_token_email_subject', array( $subject, $user->ID ), '0.17.0', 'two_factor_email_token_subject' ); /** * Filters the token email subject. diff --git a/readme.txt b/readme.txt index 71bd36aa..f8e67360 100644 --- a/readme.txt +++ b/readme.txt @@ -96,7 +96,7 @@ Here is a list of action and filter hooks provided by the plugin: - `two_factor_user_api_login_enable` filter restricts authentication for REST API and XML-RPC to application passwords only. Provides the user ID as the second argument. - `two_factor_email_token_ttl` filter overrides the time interval in seconds that an email token is considered after generation. Accepts the time in seconds as the first argument and the ID of the `WP_User` object being authenticated. - `two_factor_email_token_length` filter overrides the default 8 character count for email tokens. -- `two_factor_email_token_subject` filter overrides the subject of messages sent by the email provider. Accepts the login token as the second argument. +- `two_factor_email_token_subject` filter overrides the subject of messages sent by the email provider. Accepts the subject text as the first argument, the login token as the second argument, and the user ID as the third argument. - `two_factor_email_token_message` filter overrides the body of messages sent by the email provider. Accepts the message text as the first argument, the login token as the second argument, and the user ID as the third argument. - `two_factor_backup_code_length` filter overrides the default 8 character count for backup codes. Provides the `WP_User` of the associated user as the second argument. - `two_factor_rest_api_can_edit_user` filter overrides whether a user’s Two-Factor settings can be edited via the REST API. First argument is the current `$can_edit` boolean, the second argument is the user ID. diff --git a/tests/providers/class-two-factor-email.php b/tests/providers/class-two-factor-email.php index d2054d77..92ce4799 100644 --- a/tests/providers/class-two-factor-email.php +++ b/tests/providers/class-two-factor-email.php @@ -448,7 +448,7 @@ public function test_email_token_subject_filter() { add_filter( 'two_factor_token_email_subject', - function () { + function ( $subject ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- Required to match filter signature. return 'New Subject'; } );