From 9004456132d8411eda79197ca05a7e0aeee45522 Mon Sep 17 00:00:00 2001 From: Brian Date: Fri, 3 Apr 2026 15:30:27 +0200 Subject: [PATCH 1/5] add settings, raise level to 1 --- phpstan.dist.neon | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/phpstan.dist.neon b/phpstan.dist.neon index fc02e7c0..ed2a00ea 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -1,10 +1,11 @@ includes: - vendor/szepeviktor/phpstan-wordpress/extension.neon parameters: - level: 0 + level: 1 paths: - includes - providers + - settings - class-two-factor-compat.php - class-two-factor-core.php - two-factor.php From 5c945c5b1299f9a3ab4b722e2ec9cb73500083ed Mon Sep 17 00:00:00 2001 From: Brian Date: Tue, 7 Jul 2026 14:18:30 +0200 Subject: [PATCH 2/5] remove check as can never be undefined or empty at that point --- class-two-factor-core.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/class-two-factor-core.php b/class-two-factor-core.php index 7c0ad9a0..de927f22 100644 --- a/class-two-factor-core.php +++ b/class-two-factor-core.php @@ -238,10 +238,8 @@ public static function uninstall() { } // Delete options first since that is faster. - if ( ! empty( $option_keys ) ) { - foreach ( $option_keys as $option_key ) { - delete_option( $option_key ); - } + foreach ( $option_keys as $option_key ) { + delete_option( $option_key ); } foreach ( $user_meta_keys as $meta_key ) { From 88a8827cb952f37a6885ae399cfa388702dbbd44 Mon Sep 17 00:00:00 2001 From: Brian Date: Tue, 7 Jul 2026 14:37:40 +0200 Subject: [PATCH 3/5] raise to level 2 --- phpstan.dist.neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan.dist.neon b/phpstan.dist.neon index ed2a00ea..23ef496e 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -1,7 +1,7 @@ includes: - vendor/szepeviktor/phpstan-wordpress/extension.neon parameters: - level: 1 + level: 2 paths: - includes - providers From 3fd2ef10cc144c4b4aa8b34b6324f10eb7d1d0d3 Mon Sep 17 00:00:00 2001 From: Brian Date: Tue, 7 Jul 2026 14:45:34 +0200 Subject: [PATCH 4/5] enforce php lint on workflows --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 308a2620..029a9709 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -64,7 +64,7 @@ jobs: run: npm install - name: Lint PHP - run: npm run lint:php || true # Ignore for now. + run: npm run lint:php - name: Lint PHP Compatibility run: composer lint-compat From 4f606313f8484816ba84bfb6ff086f01193068df Mon Sep 17 00:00:00 2001 From: Brian Date: Tue, 7 Jul 2026 13:53:44 +0000 Subject: [PATCH 5/5] fix phpstand issue to go to level2 --- class-two-factor-core.php | 86 ++++++++++++++----- providers/class-two-factor-provider.php | 2 +- providers/class-two-factor-totp.php | 4 +- settings/class-two-factor-settings.php | 2 +- tests/bootstrap.php | 1 + tests/class-two-factor-core.php | 25 +++--- tests/class-two-factor-redirect-exception.php | 11 +++ tests/providers/class-two-factor-email.php | 2 + tests/providers/class-two-factor-totp.php | 30 ++++++- two-factor.php | 5 +- 10 files changed, 124 insertions(+), 44 deletions(-) create mode 100644 tests/class-two-factor-redirect-exception.php diff --git a/class-two-factor-core.php b/class-two-factor-core.php index de927f22..5fa4da59 100644 --- a/class-two-factor-core.php +++ b/class-two-factor-core.php @@ -586,7 +586,7 @@ public static function current_user_being_edited() { */ public static function trigger_user_settings_action() { $action_raw = isset( $_REQUEST[ self::USER_SETTINGS_ACTION_QUERY_VAR ] ) ? wp_unslash( $_REQUEST[ self::USER_SETTINGS_ACTION_QUERY_VAR ] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Value sanitized below; nonce verified in is_valid_user_action() before do_action. - $action = ( is_scalar( $action_raw ) && (string) $action_raw !== '' ) ? sanitize_key( (string) $action_raw ) : ''; + $action = ( is_scalar( $action_raw ) && '' !== (string) $action_raw ) ? sanitize_key( (string) $action_raw ) : ''; $user_id = self::current_user_being_edited(); if ( self::is_valid_user_action( $user_id, $action ) ) { @@ -816,7 +816,7 @@ public static function get_primary_provider_for_user( $user = null ) { 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 ); + wp_die( esc_html( $available_providers->get_error_message() ) ); } elseif ( 1 === count( $available_providers ) ) { $provider = key( $available_providers ); } else { @@ -1127,7 +1127,7 @@ public static function login_html( $user, $login_nonce, $redirect_to, $error_msg 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 ); + wp_die( esc_html( $available_providers->get_error_message() ) ); } if ( ! function_exists( 'login_header' ) ) { @@ -1184,7 +1184,7 @@ public static function login_html( $user, $login_nonce, $redirect_to, $error_msg foreach ( $backup_providers as $backup_provider_key => $backup_provider ) { $backup_link_args['provider'] = $backup_provider_key; - $links[] = array( + $links[] = array( 'url' => self::login_url( $backup_link_args ), 'label' => $backup_provider->get_alternative_provider_label(), ); @@ -1580,10 +1580,10 @@ public static function rest_api_can_edit_user_and_update_two_factor_options( $us * @since 0.2.0 */ public static function login_form_validate_2fa() { - $wp_auth_id = ! empty( $_REQUEST['wp-auth-id'] ) ? absint( $_REQUEST['wp-auth-id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in _login_form_validate_2fa() via verify_login_nonce() before any use. - $nonce = ( isset( $_REQUEST['wp-auth-nonce'] ) && is_scalar( $_REQUEST['wp-auth-nonce'] ) ) ? sanitize_text_field( wp_unslash( (string) $_REQUEST['wp-auth-nonce'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in _login_form_validate_2fa() before any use. - $provider = ! empty( $_REQUEST['provider'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['provider'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in _login_form_validate_2fa() before any use. - $redirect_to = ! empty( $_REQUEST['redirect_to'] ) ? esc_url_raw( wp_unslash( $_REQUEST['redirect_to'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in _login_form_validate_2fa() before any use. + $wp_auth_id = ! empty( $_REQUEST['wp-auth-id'] ) ? absint( $_REQUEST['wp-auth-id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in validate_login_form_2fa() before any use. + $nonce = ( isset( $_REQUEST['wp-auth-nonce'] ) && is_scalar( $_REQUEST['wp-auth-nonce'] ) ) ? sanitize_text_field( wp_unslash( (string) $_REQUEST['wp-auth-nonce'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in validate_login_form_2fa() before any use. + $provider = ! empty( $_REQUEST['provider'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['provider'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in validate_login_form_2fa() before any use. + $redirect_to = ! empty( $_REQUEST['redirect_to'] ) ? esc_url_raw( wp_unslash( $_REQUEST['redirect_to'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in validate_login_form_2fa() before any use. $is_post_request = isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' === strtoupper( $_SERVER['REQUEST_METHOD'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- REQUEST_METHOD is not user input. $user = get_user_by( 'id', $wp_auth_id ); @@ -1591,7 +1591,7 @@ public static function login_form_validate_2fa() { return; } - self::_login_form_validate_2fa( $user, $nonce, $provider, $redirect_to, $is_post_request ); + self::validate_login_form_2fa( $user, $nonce, $provider, $redirect_to, $is_post_request ); exit; } @@ -1602,6 +1602,7 @@ public static function login_form_validate_2fa() { * This function expects the caller exiting after calling. * * @since 0.9.0 + * @since 0.17.0 Renamed from `_login_form_validate_2fa()`. * * @param WP_User $user The WP_User instance. * @param string $nonce The nonce provided. @@ -1610,7 +1611,7 @@ public static function login_form_validate_2fa() { * @param bool $is_post_request Whether the incoming request was a POST request or not. * @return void */ - public static function _login_form_validate_2fa( $user, $nonce = '', $provider = '', $redirect_to = '', $is_post_request = false ) { + public static function validate_login_form_2fa( $user, $nonce = '', $provider = '', $redirect_to = '', $is_post_request = false ) { // Validate the request. if ( true !== self::verify_login_nonce( $user->ID, $nonce ) ) { wp_safe_redirect( home_url() ); @@ -1714,6 +1715,26 @@ public static function _login_form_validate_2fa( $user, $nonce = '', $provider = exit; } + /** + * Backward-compatible wrapper for the old login form validation method name. + * + * This method is kept for third-party code that may still call the previous + * public static method directly. + * + * @since 0.9.0 + * @deprecated 0.17.0 Use validate_login_form_2fa() instead. + * + * @param WP_User $user The WP_User instance. + * @param string $nonce The nonce provided. + * @param string $provider The provider to use, if known. + * @param string $redirect_to The redirection location. + * @param bool $is_post_request Whether the incoming request was a POST request or not. + * @return void + */ + public static function _login_form_validate_2fa( $user, $nonce = '', $provider = '', $redirect_to = '', $is_post_request = false ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore -- Backward-compatible wrapper for the legacy public method name. + self::validate_login_form_2fa( $user, $nonce, $provider, $redirect_to, $is_post_request ); + } + /** * Display the "Revalidate Two Factor" page. @@ -1721,12 +1742,12 @@ public static function _login_form_validate_2fa( $user, $nonce = '', $provider = * @since 0.9.0 */ public static function login_form_revalidate_2fa() { - $nonce = ( isset( $_REQUEST['wp-auth-nonce'] ) && is_scalar( $_REQUEST['wp-auth-nonce'] ) ) ? sanitize_text_field( wp_unslash( (string) $_REQUEST['wp-auth-nonce'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in _login_form_revalidate_2fa() for POST before processing. - $provider = ! empty( $_REQUEST['provider'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['provider'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in _login_form_revalidate_2fa() for POST before processing. - $redirect_to = ! empty( $_REQUEST['redirect_to'] ) ? esc_url_raw( wp_unslash( $_REQUEST['redirect_to'] ) ) : admin_url(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in _login_form_revalidate_2fa() for POST before processing. + $nonce = ( isset( $_REQUEST['wp-auth-nonce'] ) && is_scalar( $_REQUEST['wp-auth-nonce'] ) ) ? sanitize_text_field( wp_unslash( (string) $_REQUEST['wp-auth-nonce'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in revalidate_login_form_2fa() for POST before processing. + $provider = ! empty( $_REQUEST['provider'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['provider'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in revalidate_login_form_2fa() for POST before processing. + $redirect_to = ! empty( $_REQUEST['redirect_to'] ) ? esc_url_raw( wp_unslash( $_REQUEST['redirect_to'] ) ) : admin_url(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in revalidate_login_form_2fa() for POST before processing. $is_post_request = isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' === strtoupper( $_SERVER['REQUEST_METHOD'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- REQUEST_METHOD is not user input. - self::_login_form_revalidate_2fa( $nonce, $provider, $redirect_to, $is_post_request ); + self::revalidate_login_form_2fa( $nonce, $provider, $redirect_to, $is_post_request ); exit; } @@ -1737,6 +1758,7 @@ public static function login_form_revalidate_2fa() { * This function expects the caller exiting after calling. * * @since 0.9.0 + * @since 0.17.0 Renamed from `_login_form_revalidate_2fa()`. * * @param string $nonce The nonce passed with the request. * @param string $provider The provider to use, if known. @@ -1744,7 +1766,7 @@ public static function login_form_revalidate_2fa() { * @param bool $is_post_request Whether the incoming request was a POST request or not. * @return void */ - public static function _login_form_revalidate_2fa( $nonce = '', $provider = '', $redirect_to = '', $is_post_request = false ) { + public static function revalidate_login_form_2fa( $nonce = '', $provider = '', $redirect_to = '', $is_post_request = false ) { if ( ! is_user_logged_in() ) { wp_safe_redirect( home_url() ); exit; @@ -1821,6 +1843,25 @@ public static function _login_form_revalidate_2fa( $nonce = '', $provider = '', exit; } + /** + * Backward-compatible wrapper for the old revalidation method name. + * + * This method is kept for third-party code that may still call the previous + * public static method directly. + * + * @since 0.9.0 + * @deprecated 0.17.0 Use revalidate_login_form_2fa() instead. + * + * @param string $nonce The nonce passed with the request. + * @param string $provider The provider to use, if known. + * @param string $redirect_to The redirection location. + * @param bool $is_post_request Whether the incoming request was a POST request or not. + * @return void + */ + public static function _login_form_revalidate_2fa( $nonce = '', $provider = '', $redirect_to = '', $is_post_request = false ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore -- Backward-compatible wrapper for the legacy public method name. + self::revalidate_login_form_2fa( $nonce, $provider, $redirect_to, $is_post_request ); + } + /** * Process the 2FA provider authentication. * @@ -1996,7 +2037,7 @@ public static function notify_user_password_reset( $user ) { ); $user_message = str_replace( "\t", '', $user_message ); - return wp_mail( $user->user_email, __( 'Your password was compromised and has been reset', 'two-factor' ), $user_message ); + return wp_mail( $user->user_email, __( 'Your password was compromised and has been reset', 'two-factor' ), $user_message ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_mail_wp_mail -- Plugin sends a single transactional security email to the affected user. } /** @@ -2032,7 +2073,7 @@ public static function notify_admin_user_password_reset( $user ) { ); $message = str_replace( "\t", '', $message ); - return wp_mail( $admin_email, $subject, $message ); + return wp_mail( $admin_email, $subject, $message ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_mail_wp_mail -- Plugin sends a single transactional security email to the site admin. } /** @@ -2444,10 +2485,11 @@ public static function user_two_factor_options_update( $user_id ) { return; } - $user = self::fetch_user( $user_id ); - $providers = self::get_supported_providers_for_user( $user_id ); - $enabled_providers = $_POST[ self::ENABLED_PROVIDERS_USER_META_KEY ]; - $existing_providers = self::get_enabled_providers_for_user( $user_id ); + $user = self::fetch_user( $user_id ); + $providers = self::get_supported_providers_for_user( $user_id ); + $enabled_providers_input = wp_unslash( $_POST[ self::ENABLED_PROVIDERS_USER_META_KEY ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Array values are sanitized below. + $enabled_providers = array_map( 'sanitize_text_field', $enabled_providers_input ); + $existing_providers = self::get_enabled_providers_for_user( $user_id ); // Enable only the available providers. $enabled_providers = array_intersect_key( $providers, array_flip( $enabled_providers ) ); @@ -2476,7 +2518,7 @@ public static function user_two_factor_options_update( $user_id ) { update_user_meta( $user_id, self::ENABLED_PROVIDERS_USER_META_KEY, array_keys( $enabled_providers ) ); // Primary provider must be enabled. - $new_provider = isset( $_POST[ self::PROVIDER_USER_META_KEY ] ) ? $_POST[ self::PROVIDER_USER_META_KEY ] : ''; + $new_provider = isset( $_POST[ self::PROVIDER_USER_META_KEY ] ) ? sanitize_text_field( wp_unslash( $_POST[ self::PROVIDER_USER_META_KEY ] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Value sanitized inline. if ( ! empty( $new_provider ) && isset( $enabled_providers[ $new_provider ] ) ) { update_user_meta( $user_id, self::PROVIDER_USER_META_KEY, $new_provider ); } else { diff --git a/providers/class-two-factor-provider.php b/providers/class-two-factor-provider.php index fd0d42d2..5659309b 100644 --- a/providers/class-two-factor-provider.php +++ b/providers/class-two-factor-provider.php @@ -104,7 +104,7 @@ abstract public function authentication_page( $user ); * @param WP_User $user WP_User object of the logged-in user. * @return boolean */ - public function pre_process_authentication( $user ) { + public function pre_process_authentication( $user ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- Base implementation keeps the provider interface signature but does not use the user. return false; } diff --git a/providers/class-two-factor-totp.php b/providers/class-two-factor-totp.php index 487ae887..2d78fef8 100644 --- a/providers/class-two-factor-totp.php +++ b/providers/class-two-factor-totp.php @@ -343,8 +343,8 @@ public static function generate_qr_code_url( $user, $secret_key ) { */ public function user_two_factor_options( $user ) { if ( ! ( $user instanceof WP_User ) ) { - return; - } + return; + } $key = $this->get_user_totp_key( $user->ID ); diff --git a/settings/class-two-factor-settings.php b/settings/class-two-factor-settings.php index 9e7fa886..fb824cae 100644 --- a/settings/class-two-factor-settings.php +++ b/settings/class-two-factor-settings.php @@ -32,7 +32,7 @@ public static function render_settings_page() { if ( isset( $_POST['two_factor_settings_submit'] ) ) { check_admin_referer( 'two_factor_save_settings', 'two_factor_settings_nonce' ); - $posted = isset( $_POST['two_factor_enabled_providers'] ) && is_array( $_POST['two_factor_enabled_providers'] ) ? wp_unslash( $_POST['two_factor_enabled_providers'] ) : array(); + $posted = isset( $_POST['two_factor_enabled_providers'] ) && is_array( $_POST['two_factor_enabled_providers'] ) ? wp_unslash( $_POST['two_factor_enabled_providers'] ) : array(); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce verified above; array values sanitized immediately below. // Sanitize posted values immediately. $posted = array_map( 'sanitize_text_field', (array) $posted ); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 6d3ae4ce..badfc1a8 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -29,6 +29,7 @@ require_once $_tests_dir . '/includes/functions.php'; require_once dirname( __DIR__ ) . '/includes/function.login-header.php'; require_once dirname( __DIR__ ) . '/includes/function.login-footer.php'; +require_once __DIR__ . '/class-two-factor-redirect-exception.php'; // Activate the plugin. tests_add_filter( diff --git a/tests/class-two-factor-core.php b/tests/class-two-factor-core.php index 38052106..dc346c7f 100644 --- a/tests/class-two-factor-core.php +++ b/tests/class-two-factor-core.php @@ -5,11 +5,6 @@ * @package Two_Factor */ -/** - * Exception thrown when wp_redirect fires, to prevent exit() from terminating the test process. - */ -class Two_Factor_Redirect_Exception extends RuntimeException {} - /** * Class Test_ClassTwoFactorCore * @@ -1228,7 +1223,7 @@ public function test_is_current_user_session_two_factor_without_two_factor() { * * @covers Two_Factor_Core::is_current_user_session_two_factor() * @covers Two_Factor_Core::current_user_can_update_two_factor_options() - * @covers Two_Factor_Core::_login_form_validate_2fa() + * @covers Two_Factor_Core::validate_login_form_2fa() */ public function test_is_current_user_session_two_factor_with_two_factor() { $user = $this->get_dummy_user( array( 'Two_Factor_Dummy' => 'Two_Factor_Dummy' ) ); @@ -1245,7 +1240,7 @@ public function test_is_current_user_session_two_factor_with_two_factor() { $this->assertNotFalse( $login_nonce ); ob_start(); - Two_Factor_Core::_login_form_validate_2fa( $user, $login_nonce['key'], 'Two_Factor_Dummy', '', false ); + Two_Factor_Core::validate_login_form_2fa( $user, $login_nonce['key'], 'Two_Factor_Dummy', '', false ); ob_end_clean(); // Validate that the session is not set, as it wasn't a POST. @@ -1257,7 +1252,7 @@ public function test_is_current_user_session_two_factor_with_two_factor() { // Process it. $redirect_url = $this->do_redirect_callable( function () use ( $user, $login_nonce ) { - Two_Factor_Core::_login_form_validate_2fa( $user, $login_nonce['key'], 'Two_Factor_Dummy', '', true ); + Two_Factor_Core::validate_login_form_2fa( $user, $login_nonce['key'], 'Two_Factor_Dummy', '', true ); } ); $this->assertNotNull( $redirect_url, 'Expected a redirect after successful 2FA validation.' ); @@ -1280,7 +1275,7 @@ function () use ( $user, $login_nonce ) { /** * Validate that a simulated 2fa revalidation updates the session two-factor data. * - * @covers Two_Factor_Core::_login_form_revalidate_2fa() + * @covers Two_Factor_Core::revalidate_login_form_2fa() * @covers Two_Factor_Core::current_user_can_update_two_factor_options() */ public function test_revalidation_sets_time() { @@ -1301,7 +1296,7 @@ public function test_revalidation_sets_time() { $this->assertNotFalse( $login_nonce ); ob_start(); - Two_Factor_Core::_login_form_validate_2fa( $user, $login_nonce['key'], 'Two_Factor_Dummy', '', false ); + Two_Factor_Core::validate_login_form_2fa( $user, $login_nonce['key'], 'Two_Factor_Dummy', '', false ); ob_end_clean(); $login_nonce = Two_Factor_Core::create_login_nonce( $user->ID ); @@ -1310,7 +1305,7 @@ public function test_revalidation_sets_time() { // Process it. $redirect_url = $this->do_redirect_callable( function () use ( $user, $login_nonce ) { - Two_Factor_Core::_login_form_validate_2fa( $user, $login_nonce['key'], 'Two_Factor_Dummy', '', true ); + Two_Factor_Core::validate_login_form_2fa( $user, $login_nonce['key'], 'Two_Factor_Dummy', '', true ); } ); $this->assertNotNull( $redirect_url, 'Expected a redirect after successful 2FA validation.' ); @@ -1347,7 +1342,7 @@ function () use ( $user, $login_nonce ) { // Revalidate. // Simulate displaying it. ob_start(); - Two_Factor_Core::_login_form_revalidate_2fa( '', 'Two_Factor_Dummy', '', false ); + Two_Factor_Core::revalidate_login_form_2fa( '', 'Two_Factor_Dummy', '', false ); ob_end_clean(); // Check it's still expired. @@ -1357,7 +1352,7 @@ function () use ( $user, $login_nonce ) { $bad_nonce = '__BAD_NONCE__'; $bad_redirect_url = $this->do_redirect_callable( function () use ( $bad_nonce ) { - Two_Factor_Core::_login_form_revalidate_2fa( $bad_nonce, 'Two_Factor_Dummy', '', true ); + Two_Factor_Core::revalidate_login_form_2fa( $bad_nonce, 'Two_Factor_Dummy', '', true ); } ); $this->assertNotNull( $bad_redirect_url, 'Expected a redirect after bad-nonce revalidation attempt.' ); @@ -1371,7 +1366,7 @@ function () use ( $bad_nonce ) { $good_redirect_url = $this->do_redirect_callable( function () use ( $login_nonce ) { - Two_Factor_Core::_login_form_revalidate_2fa( $login_nonce, 'Two_Factor_Dummy', '', true ); + Two_Factor_Core::revalidate_login_form_2fa( $login_nonce, 'Two_Factor_Dummy', '', true ); } ); $this->assertNotNull( $good_redirect_url, 'Expected a redirect after successful revalidation.' ); @@ -1607,7 +1602,7 @@ public function test_filter_session_information() { // Process it. $redirect_url = $this->do_redirect_callable( function () use ( $user, $login_nonce ) { - Two_Factor_Core::_login_form_validate_2fa( $user, $login_nonce['key'], 'Two_Factor_Dummy', '', true ); + Two_Factor_Core::validate_login_form_2fa( $user, $login_nonce['key'], 'Two_Factor_Dummy', '', true ); } ); $this->assertNotNull( $redirect_url, 'Expected a redirect after successful 2FA validation.' ); diff --git a/tests/class-two-factor-redirect-exception.php b/tests/class-two-factor-redirect-exception.php new file mode 100644 index 00000000..34e3b282 --- /dev/null +++ b/tests/class-two-factor-redirect-exception.php @@ -0,0 +1,11 @@ +user->create() ); + // phpcs:disable WordPressVIPMinimum.Variables.ServerVariables.UserControlledHeaders, WordPressVIPMinimum.Variables.RestrictedVariables.cache_constraints___SERVER__REMOTE_ADDR__ -- Test fixture for IP-dependent email token generation. $prev_remote_addr = $_SERVER['REMOTE_ADDR'] ?? null; $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; try { @@ -166,6 +167,7 @@ public function test_generate_and_email_token() { $_SERVER['REMOTE_ADDR'] = $prev_remote_addr; } } + // phpcs:enable WordPressVIPMinimum.Variables.ServerVariables.UserControlledHeaders, WordPressVIPMinimum.Variables.RestrictedVariables.cache_constraints___SERVER__REMOTE_ADDR__ $pattern = '/verification code below:\R\R(\d+)/'; $content = $GLOBALS['phpmailer']->Body; diff --git a/tests/providers/class-two-factor-totp.php b/tests/providers/class-two-factor-totp.php index 50ea34f8..3f641b33 100644 --- a/tests/providers/class-two-factor-totp.php +++ b/tests/providers/class-two-factor-totp.php @@ -14,9 +14,25 @@ */ class Tests_Two_Factor_Totp extends WP_UnitTestCase { + /** + * Shared token test vector seed. + * + * @var string + */ private static $token = '12345678901234567890'; - private static $step = 30; + /** + * TOTP step size used for RFC vectors. + * + * @var int + */ + private static $step = 30; + + /** + * RFC test vectors keyed by timestamp. + * + * @var array> + */ private static $vectors = array( 59 => array( '94287082', '46119246', '90693936' ), 1111111109 => array( '07081804', '68084774', '25091201' ), @@ -329,6 +345,8 @@ public function test_get_authcode_valid_ticktime() { } /** + * Verify SHA-1 test vectors generate the expected TOTP values. + * * @covers Two_Factor_Totp::calc_totp */ public function test_sha1_generate() { @@ -348,6 +366,8 @@ public function test_sha1_generate() { } /** + * Verify SHA-1 generated codes authenticate successfully. + * * @covers Two_Factor_Totp::is_valid_authcode * @covers Two_Factor_Totp::calc_totp */ @@ -368,6 +388,8 @@ public function test_sha1_authenticate() { } /** + * Verify SHA-256 test vectors generate the expected TOTP values. + * * @covers Two_Factor_Totp::calc_totp */ public function test_sha256_generate() { @@ -387,6 +409,8 @@ public function test_sha256_generate() { } /** + * Verify SHA-256 generated codes authenticate successfully. + * * @covers Two_Factor_Totp::is_valid_authcode * @covers Two_Factor_Totp::calc_totp */ @@ -407,6 +431,8 @@ public function test_sha256_authenticate() { } /** + * Verify SHA-512 test vectors generate the expected TOTP values. + * * @covers Two_Factor_Totp::calc_totp */ public function test_sha512_generate() { @@ -426,6 +452,8 @@ public function test_sha512_generate() { } /** + * Verify SHA-512 generated codes authenticate successfully. + * * @covers Two_Factor_Totp::is_valid_authcode * @covers Two_Factor_Totp::calc_totp */ diff --git a/two-factor.php b/two-factor.php index ecb70efb..fe22a4ff 100644 --- a/two-factor.php +++ b/two-factor.php @@ -153,7 +153,8 @@ function two_factor_filter_enabled_providers( $providers ) { } // On the settings page itself, show all providers so admins can change the selection. - if ( is_admin() && isset( $_GET['page'] ) && 'two-factor-settings' === $_GET['page'] ) { + $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading the current admin page slug only; no state change occurs here. + if ( is_admin() && 'two-factor-settings' === $page ) { return $providers; } @@ -176,7 +177,7 @@ function two_factor_filter_enabled_providers( $providers ) { * @param int $user_id ID of the user being filtered. * @return array Filtered list of provider classnames allowed by the site. */ -function two_factor_filter_enabled_providers_for_user( $enabled, $user_id ) { +function two_factor_filter_enabled_providers_for_user( $enabled, $user_id ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- Filter signature includes user_id, but this site-wide restriction only needs the enabled providers list. $site_enabled = two_factor_get_enabled_providers_option(); // null means the option was never saved — allow all.