From e17c5b3c22010fd45a6d9963b109e132aac59ee3 Mon Sep 17 00:00:00 2001 From: George Stephanis Date: Wed, 4 Mar 2026 10:29:23 -0500 Subject: [PATCH 1/7] Add configurable code length and autosubmit filters Introduce provider-wide code length controls and an autosubmit length filter. - Add Two_Factor_Provider::get_code_length() and update get_code() to accept a null length and fall back to the provider-specific length. Providers can now use two_factor_code_length to customize default code lengths per provider. - Use self::get_code_length() in backup-codes and email providers so their default token/backup lengths are filterable. - Add two_factor_autosubmit_length filter and apply it in backup-codes, email and TOTP providers to control the data-digits attribute (setting it to 0 disables autosubmit). - Update readme.txt to document the new two_factor_code_length and two_factor_autosubmit_length filters. - Minor whitespace cleanups in TOTP packing code. These changes centralize code-length behavior across providers and make the client-side autosubmit behavior configurable. --- providers/class-two-factor-backup-codes.php | 14 +++++++++- providers/class-two-factor-email.php | 5 +++- providers/class-two-factor-provider.php | 30 +++++++++++++++++++-- providers/class-two-factor-totp.php | 10 ++++--- readme.txt | 2 ++ 5 files changed, 54 insertions(+), 7 deletions(-) diff --git a/providers/class-two-factor-backup-codes.php b/providers/class-two-factor-backup-codes.php index 6d6896bb..2b5fbafd 100644 --- a/providers/class-two-factor-backup-codes.php +++ b/providers/class-two-factor-backup-codes.php @@ -260,7 +260,7 @@ private function get_backup_code_length( $user ) { * @param int $code_length Length of the backup code. Default 8. * @param WP_User $user User object. */ - $code_length = (int) apply_filters( 'two_factor_backup_code_length', 8, $user ); + $code_length = (int) apply_filters( 'two_factor_backup_code_length', self::get_code_length(), $user ); return $code_length; } @@ -387,6 +387,18 @@ public function authentication_page( $user ) { $code_length = $this->get_backup_code_length( $user ); $code_placeholder = str_repeat( 'X', $code_length ); + /** + * Filters the `digits` dataset attribute of the backup code input field on the authentication screen. + * + * To disable autosubmit, set the digits to `0` via the core method `__return_zero`. + * + * @since 0.?.0 + * + * @param int $code_length The length of the backup code. + * @param Two_Factor_Provider $provider The two-factor provider instance. + */ + $code_length = apply_filters( 'two_factor_autosubmit_length', $code_length, $this ); + ?> get_token_length(); $token_placeholder = str_repeat( 'X', $token_length ); + /** This filter is documented in providers/class-two-factor-backup-codes.php */ + $token_length = apply_filters( 'two_factor_autosubmit_length', $token_length, $this ); + require_once ABSPATH . '/wp-admin/includes/template.php'; ?> > 32 ) & 0xFFFFFFFF; $lower = $value & 0xFFFFFFFF; - + return pack( 'NN', $higher, $lower ); } @@ -832,6 +832,10 @@ public function is_available_for_user( $user ) { * @codeCoverageIgnore */ public function authentication_page( $user ) { + + /** This filter is documented in providers/class-two-factor-backup-codes.php */ + $code_length = apply_filters( 'two_factor_autosubmit_length', self::DEFAULT_DIGIT_COUNT, $this ); + require_once ABSPATH . '/wp-admin/includes/template.php'; ?>

- +

Date: Wed, 4 Mar 2026 10:33:46 -0500 Subject: [PATCH 2/7] Parse expectedLength as integer Ensure the expectedLength value is treated as a number by parsing inputEl.dataset.digits with parseInt(..., 10). This prevents string-based comparisons or unexpected behavior when dataset values are present (or missing), while preserving the optional chaining and defaulting to 0. --- class-two-factor-core.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/class-two-factor-core.php b/class-two-factor-core.php index c034c53d..6580339e 100644 --- a/class-two-factor-core.php +++ b/class-two-factor-core.php @@ -1124,7 +1124,7 @@ public static function login_html( $user, $login_nonce, $redirect_to, $error_msg // Enforce numeric-only input for numeric inputmode elements. const form = document.querySelector( '#loginform' ), inputEl = document.querySelector( 'input.authcode[inputmode="numeric"]' ), - expectedLength = inputEl?.dataset.digits || 0; + expectedLength = parseInt( ( inputEl?.dataset.digits || 0 ), 10 ); if ( inputEl ) { let spaceInserted = false; From 23b8907500ce44a6fd9de0f064bfc2e3aa9733f1 Mon Sep 17 00:00:00 2001 From: George Stephanis Date: Wed, 4 Mar 2026 10:49:31 -0500 Subject: [PATCH 3/7] Update readme.txt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- readme.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.txt b/readme.txt index 1c9f742e..210c571d 100644 --- a/readme.txt +++ b/readme.txt @@ -95,8 +95,8 @@ Here is a list of action and filter hooks provided by the plugin: - `two_factor_user_authenticated` action which receives the logged in `WP_User` object as the first argument for determining the logged in user right after the authentication workflow. - `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_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_email_token_length` filter overrides the default email token length determined by the `two_factor_code_length` filter (which defaults to 8 characters if not filtered). +- `two_factor_backup_code_length` filter overrides the default backup code length determined by the `two_factor_code_length` filter (which defaults to 8 characters if not filtered). Provides the `WP_User` of the associated user as the second argument. - `two_factor_code_length` filter sets the default for all providers that invoke `self::get_code_length()`. Provides the called class as the second argument. - `two_factor_autosubmit_length` filter sets the input length at which the form will auto-submit. Set to `0` via `__return_zero` to disable autosubmit. Provides the provider's object 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. From 698c7a5f218b238cd32d6af90dc43ff752f34816 Mon Sep 17 00:00:00 2001 From: George Stephanis Date: Wed, 4 Mar 2026 10:50:31 -0500 Subject: [PATCH 4/7] Update providers/class-two-factor-provider.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- providers/class-two-factor-provider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/class-two-factor-provider.php b/providers/class-two-factor-provider.php index 059aa1e1..4085e877 100644 --- a/providers/class-two-factor-provider.php +++ b/providers/class-two-factor-provider.php @@ -175,7 +175,7 @@ public static function get_code( $length = null, $chars = '1234567890' ) { */ public static function get_code_length( $default = 8, $provider = null ) { /** - * Filter the length of the code for a user. + * Filter the default code length for a provider. * * @since 0.?.0 * From e9b935d37f40ed31c7d3649a3fa1d37f7f43c0d8 Mon Sep 17 00:00:00 2001 From: George Stephanis Date: Wed, 4 Mar 2026 10:50:59 -0500 Subject: [PATCH 5/7] Update providers/class-two-factor-email.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- providers/class-two-factor-email.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/providers/class-two-factor-email.php b/providers/class-two-factor-email.php index 1309bd99..ed9056ed 100644 --- a/providers/class-two-factor-email.php +++ b/providers/class-two-factor-email.php @@ -76,7 +76,8 @@ private function get_token_length() { * * @since 0.11.0 * - * @param int $token_length Number of characters in the email token. Default 8. + * @param int $token_length Number of characters in the email token. Defaults to the value of the + * `two_factor_code_length` filter (8 if not filtered). */ $token_length = (int) apply_filters( 'two_factor_email_token_length', self::get_code_length() ); From 4659913fa5cb94bae97a10ba86303164f63bbbce Mon Sep 17 00:00:00 2001 From: George Stephanis Date: Wed, 4 Mar 2026 10:51:12 -0500 Subject: [PATCH 6/7] Update providers/class-two-factor-backup-codes.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- providers/class-two-factor-backup-codes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/class-two-factor-backup-codes.php b/providers/class-two-factor-backup-codes.php index 2b5fbafd..e20c1e4d 100644 --- a/providers/class-two-factor-backup-codes.php +++ b/providers/class-two-factor-backup-codes.php @@ -257,7 +257,7 @@ private function get_backup_code_length( $user ) { * * @since 0.11.0 * - * @param int $code_length Length of the backup code. Default 8. + * @param int $code_length Length of the backup code. Default is taken from the `two_factor_code_length` filter (which defaults to 8 if not filtered). * @param WP_User $user User object. */ $code_length = (int) apply_filters( 'two_factor_backup_code_length', self::get_code_length(), $user ); From 18732c99fb276131f9d5a13ebc1a363b16071756 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 11:06:38 -0500 Subject: [PATCH 7/7] Add test coverage for `get_code_length()` and autosubmit filters (#821) * Initial plan * Add test coverage for get_code_length() and two_factor_autosubmit_length filter Co-authored-by: georgestephanis <941023+georgestephanis@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: georgestephanis <941023+georgestephanis@users.noreply.github.com> --- .../class-two-factor-backup-codes.php | 23 +++++++++ tests/providers/class-two-factor-provider.php | 48 +++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/tests/providers/class-two-factor-backup-codes.php b/tests/providers/class-two-factor-backup-codes.php index 8d976eca..d3eec34c 100644 --- a/tests/providers/class-two-factor-backup-codes.php +++ b/tests/providers/class-two-factor-backup-codes.php @@ -218,4 +218,27 @@ function () { remove_all_filters( 'two_factor_backup_code_length' ); } + + /** + * Test that the two_factor_autosubmit_length filter changes the data-digits attribute on the authentication page. + * + * @covers Two_Factor_Backup_Codes::authentication_page + */ + public function test_autosubmit_length_filter_affects_authentication_page() { + // Default: data-digits should reflect the default backup code length (8). + // Pass false as the user since no user-specific code length is needed for this test. + ob_start(); + $this->provider->authentication_page( false ); + $default_output = ob_get_clean(); + $this->assertStringContainsString( 'data-digits="8"', $default_output ); + + // With filter: data-digits should be overridden to 0 (disables autosubmit). + add_filter( 'two_factor_autosubmit_length', '__return_zero' ); + ob_start(); + $this->provider->authentication_page( false ); + $filtered_output = ob_get_clean(); + remove_filter( 'two_factor_autosubmit_length', '__return_zero' ); + + $this->assertStringContainsString( 'data-digits="0"', $filtered_output ); + } } diff --git a/tests/providers/class-two-factor-provider.php b/tests/providers/class-two-factor-provider.php index dcce6c6f..93520ddc 100644 --- a/tests/providers/class-two-factor-provider.php +++ b/tests/providers/class-two-factor-provider.php @@ -87,4 +87,52 @@ public function test_get_instance() { $this->assertSame( $instance_one, $instance_two ); } + + /** + * Test that get_code_length() returns the default value when no filter is applied. + * + * @covers Two_Factor_Provider::get_code_length + */ + public function test_get_code_length_returns_default() { + $this->assertSame( 8, Two_Factor_Provider::get_code_length( 8 ) ); + $this->assertSame( 6, Two_Factor_Provider::get_code_length( 6 ) ); + } + + /** + * Test that the two_factor_code_length filter can override the default code length. + * + * @covers Two_Factor_Provider::get_code_length + */ + public function test_get_code_length_filter_overrides_default() { + $set_length_to_4 = function() { + return 4; + }; + add_filter( 'two_factor_code_length', $set_length_to_4 ); + $this->assertSame( 4, Two_Factor_Provider::get_code_length( 8 ) ); + remove_filter( 'two_factor_code_length', $set_length_to_4 ); + + $set_length_to_12 = function() { + return 12; + }; + add_filter( 'two_factor_code_length', $set_length_to_12 ); + $this->assertSame( 12, Two_Factor_Provider::get_code_length( 8 ) ); + remove_filter( 'two_factor_code_length', $set_length_to_12 ); + } + + /** + * Test that get_code( null ) uses the filtered code length from two_factor_code_length. + * + * @covers Two_Factor_Provider::get_code + * @covers Two_Factor_Provider::get_code_length + */ + public function test_get_code_with_null_uses_filtered_length() { + $set_length_to_5 = function() { + return 5; + }; + add_filter( 'two_factor_code_length', $set_length_to_5 ); + $code = Two_Factor_Provider::get_code( null ); + remove_filter( 'two_factor_code_length', $set_length_to_5 ); + + $this->assertSame( 5, strlen( $code ) ); + } }