Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion class-two-factor-core.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
16 changes: 14 additions & 2 deletions providers/class-two-factor-backup-codes.php
Original file line number Diff line number Diff line change
Expand Up @@ -257,10 +257,10 @@ 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', 8, $user );
$code_length = (int) apply_filters( 'two_factor_backup_code_length', self::get_code_length(), $user );

return $code_length;
}
Expand Down Expand Up @@ -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
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The @since 0.?.0 version placeholder in the two_factor_autosubmit_length filter documentation needs to be replaced with the actual release version before merging.

Suggested change
* @since 0.?.0
* @since 0.15.0

Copilot uses AI. Check for mistakes.
*
* @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 );

?>
<?php
/**
Expand Down
8 changes: 6 additions & 2 deletions providers/class-two-factor-email.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,10 @@ 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', 8 );
$token_length = (int) apply_filters( 'two_factor_email_token_length', self::get_code_length() );

return $token_length;
}
Expand Down Expand Up @@ -349,6 +350,9 @@ public function authentication_page( $user ) {
$token_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';
?>
<?php
Expand Down
30 changes: 28 additions & 2 deletions providers/class-two-factor-provider.php
Original file line number Diff line number Diff line change
Expand Up @@ -142,15 +142,18 @@ public static function is_supported_for_user( $user = null ) {
}

/**
* Generate a random eight-digit string to send out as an auth code.
* Generate a random string to send out as an auth code. Default is an 8 digit numeric code, but the length and characters can be customized.
*
* @since 0.1-dev
*
* @param int $length The code length.
* @param string|array $chars Valid auth code characters.
* @return string
*/
public static function get_code( $length = 8, $chars = '1234567890' ) {
public static function get_code( $length = null, $chars = '1234567890' ) {
if ( is_null( $length ) ) {
$length = self::get_code_length( 8, static::class );
}
$code = '';
if ( is_array( $chars ) ) {
$chars = implode( '', $chars );
Expand All @@ -161,6 +164,29 @@ public static function get_code( $length = 8, $chars = '1234567890' ) {
return $code;
}

/**
* Get the code length for a provider.
*
* @since 0.?.0
*
* @param int $default Default code length if not filtered.
* @param string|null $provider The provider class name. Null uses the called class.
* @return int Number of characters.
*/
public static function get_code_length( $default = 8, $provider = null ) {
/**
* Filter the default code length for a provider.
*
* @since 0.?.0
Comment on lines +170 to +180
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The @since 0.?.0 version placeholder is present in the get_code_length() method and its filter documentation. This needs to be replaced with the actual version number before the code is released. Based on the codebase, the current version is 0.15.0 (per CHANGELOG.md), so this would be 0.16.0.

Copilot uses AI. Check for mistakes.
*
* @param int $code_length Length of the code. Default 8.
* @param string $provider The provider class name.
*/
$code_length = (int) apply_filters( 'two_factor_code_length', $default, $provider ?: static::class );

return $code_length;
}

/**
* Sanitizes a numeric code to be used as an auth code.
*
Expand Down
10 changes: 7 additions & 3 deletions providers/class-two-factor-totp.php
Original file line number Diff line number Diff line change
Expand Up @@ -720,11 +720,11 @@ public static function pack64( int $value ): string {
if ( 8 === PHP_INT_SIZE ) {
return pack( 'J', $value );
}

// 32-bit PHP fallback
$higher = ( $value >> 32 ) & 0xFFFFFFFF;
$lower = $value & 0xFFFFFFFF;

return pack( 'NN', $higher, $lower );
}

Expand Down Expand Up @@ -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';
?>
<?php
Expand All @@ -847,7 +851,7 @@ public function authentication_page( $user ) {
?>
<p>
<label for="authcode"><?php esc_html_e( 'Authentication Code:', 'two-factor' ); ?></label>
<input type="text" inputmode="numeric" name="authcode" id="authcode" class="input authcode" value="" size="20" pattern="[0-9 ]*" placeholder="123 456" autocomplete="one-time-code" data-digits="<?php echo esc_attr( self::DEFAULT_DIGIT_COUNT ); ?>" />
<input type="text" inputmode="numeric" name="authcode" id="authcode" class="input authcode" value="" size="20" pattern="[0-9 ]*" placeholder="123 456" autocomplete="one-time-code" data-digits="<?php echo esc_attr( $code_length ); ?>" />
</p>
<?php
/** This action is documented in providers/class-two-factor-backup-codes.php */
Expand Down
6 changes: 4 additions & 2 deletions readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,10 @@ 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.
- `two_factor_before_authentication_prompt` action which receives the provider object and fires prior to the prompt shown on the authentication input form.
- `two_factor_after_authentication_prompt` action which receives the provider object and fires after the prompt shown on the authentication input form.
Expand Down
23 changes: 23 additions & 0 deletions tests/providers/class-two-factor-backup-codes.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
}
}
48 changes: 48 additions & 0 deletions tests/providers/class-two-factor-provider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) );
}
}
Loading