v1.2.1
This commit is contained in:
parent
a115f08319
commit
17078a91bd
8 changed files with 123 additions and 49 deletions
|
|
@ -1,5 +1,30 @@
|
|||
== Changelog ==
|
||||
|
||||
= 1.2.1 =
|
||||
|
||||
_Release date: 2026-06-05_
|
||||
|
||||
**Fixed**
|
||||
|
||||
* Recovery code confirmation without the method checkbox now correctly activates the method.
|
||||
* Fatal error on admin profile pages (add_settings_error not available at init in multisite).
|
||||
* OTP and recovery text inputs no longer disabled by JS when Enable toggle is off.
|
||||
* Regenerate codes now requires re-confirmation before the method reactivates.
|
||||
* Entering a valid OTP or recovery code activates the method even without checking the checkbox.
|
||||
* Recovery regeneration field validated with strict value check.
|
||||
|
||||
**Compatibility**
|
||||
|
||||
* WordPress: 6.4 – 7.1
|
||||
* PHP: 8.0 – 8.5
|
||||
|
||||
**Tests**
|
||||
|
||||
* PHP Coding Standards: PHP_CodeSniffer 3.13.5 / WPCS 3.3.0
|
||||
* PHPStan: level 9 — 0 errors
|
||||
* PHPCompatibility: 8.0–8.5 — 0 issues
|
||||
* PHPUnit: 9.6.34 — 42 tests, 109 assertions
|
||||
|
||||
= 1.1.0 =
|
||||
|
||||
_Release date: 2026-06-05_
|
||||
|
|
|
|||
|
|
@ -62,6 +62,16 @@ class Frontend_Profile {
|
|||
* @return void
|
||||
*/
|
||||
public function maybe_handle_frontend_save(): void {
|
||||
// On wp-admin pages the profile form is already handled by the
|
||||
// personal_options_update / edit_user_profile_update hooks that fire
|
||||
// later in the admin bootstrap (when all admin includes are loaded).
|
||||
// Running here too would call save_profile_settings() twice and
|
||||
// trigger a fatal because add_settings_error() is not yet available
|
||||
// at init time in admin context.
|
||||
if ( is_admin() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( 'POST' !== $_SERVER['REQUEST_METHOD'] ) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -383,7 +383,7 @@ class Profile_Settings {
|
|||
</p>
|
||||
<?php endif; ?>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Enter the current six-digit code from your authenticator app to activate this method.', 'robotstxt-2fa' ); ?>
|
||||
<?php esc_html_e( 'Enter the current six-digit code from your authenticator app to activate this method. Submitting a valid code activates the authenticator app even if the Enable checkbox above is not checked.', 'robotstxt-2fa' ); ?>
|
||||
</p>
|
||||
<label class="screen-reader-text" for="robotstxt-2fa-otp-code">
|
||||
<?php esc_html_e( 'Authenticator code', 'robotstxt-2fa' ); ?>
|
||||
|
|
@ -532,7 +532,10 @@ class Profile_Settings {
|
|||
|
||||
function applyState() {
|
||||
var on = toggle.checked;
|
||||
methods.querySelectorAll( 'input, select, button' ).forEach( function ( el ) {
|
||||
// Only disable checkboxes and select elements — text inputs (OTP code,
|
||||
// recovery confirmation) must remain submittable so entering a code
|
||||
// activates the method even when the Enable toggle is off.
|
||||
methods.querySelectorAll( 'input[type="checkbox"], select' ).forEach( function ( el ) {
|
||||
var isRequired = el.closest( '[data-2fa-required]' );
|
||||
if ( ! isRequired ) {
|
||||
el.disabled = ! on;
|
||||
|
|
@ -588,8 +591,18 @@ class Profile_Settings {
|
|||
$raw_fmt = get_option( 'date_format' );
|
||||
$fmt = is_string( $raw_fmt ) ? $raw_fmt : 'Y-m-d';
|
||||
?>
|
||||
<td><?php echo esc_html( wp_date( $fmt, $device['created_at'] ) ?: '' ); ?></td>
|
||||
<td><?php echo esc_html( wp_date( $fmt, $device['expires_at'] ) ?: '' ); ?></td>
|
||||
<td>
|
||||
<?php
|
||||
$created = wp_date( $fmt, $device['created_at'] );
|
||||
echo esc_html( false !== $created ? $created : '' );
|
||||
?>
|
||||
</td>
|
||||
<td>
|
||||
<?php
|
||||
$expires = wp_date( $fmt, $device['expires_at'] );
|
||||
echo esc_html( false !== $expires ? $expires : '' );
|
||||
?>
|
||||
</td>
|
||||
<td>
|
||||
<button type="submit" class="button button-small" name="<?php echo esc_attr( self::REVOKE_DEVICE_FIELD ); ?>" value="<?php echo esc_attr( $token_hash ); ?>">
|
||||
<?php esc_html_e( 'Revoke', 'robotstxt-2fa' ); ?>
|
||||
|
|
@ -717,6 +730,14 @@ class Profile_Settings {
|
|||
* @return void
|
||||
*/
|
||||
public function save_profile_settings( int $user_id ): void {
|
||||
// add_settings_error() is defined in wp-admin/includes/template.php which
|
||||
// may not be loaded when this method is called outside of a standard admin
|
||||
// request (e.g. via the [robotstxt_2fa_profile] shortcode on the frontend
|
||||
// or during the init hook on multisite). Load it on demand.
|
||||
if ( ! function_exists( 'add_settings_error' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/template.php';
|
||||
}
|
||||
|
||||
if ( ! current_user_can( 'edit_user', $user_id ) ) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -795,7 +816,7 @@ class Profile_Settings {
|
|||
$recovery_was_enabled = in_array( 'recovery_codes', $previous_methods, true );
|
||||
|
||||
// Handle recovery code regeneration request — replaces the full batch.
|
||||
if ( isset( $_POST[ self::RECOVERY_REGENERATE_FIELD ] ) && $recovery_was_enabled ) {
|
||||
if ( '1' === ( isset( $_POST[ self::RECOVERY_REGENERATE_FIELD ] ) && is_string( $_POST[ self::RECOVERY_REGENERATE_FIELD ] ) ? $_POST[ self::RECOVERY_REGENERATE_FIELD ] : '' ) && $recovery_was_enabled ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified above.
|
||||
try {
|
||||
$result = $this->recovery_codes->regenerate_codes_for_user( $user );
|
||||
} catch ( \Throwable $exception ) {
|
||||
|
|
@ -812,6 +833,19 @@ class Profile_Settings {
|
|||
}
|
||||
|
||||
$this->remember_recovery_preview( $user->ID, $result['codes'], $result['generated_at'] );
|
||||
|
||||
// Temporarily remove recovery_codes from active methods so the confirmation
|
||||
// field is shown, exactly as in the initial-activation flow.
|
||||
$methods_without_recovery = array_values( array_diff( $previous_methods, array( 'recovery_codes' ) ) );
|
||||
$this->user_settings_repository->save_user_settings(
|
||||
$user_id,
|
||||
array(
|
||||
'enabled' => $is_forced || ! empty( $methods_without_recovery ),
|
||||
'methods' => $methods_without_recovery,
|
||||
'frequency' => $user_settings['frequency'],
|
||||
)
|
||||
);
|
||||
|
||||
$this->focus_section = true;
|
||||
add_settings_error( 'robotstxt-2fa', 'robotstxt-2fa-recovery-regenerated', __( 'New recovery codes generated. Copy them and confirm one to activate.', 'robotstxt-2fa' ), 'updated' );
|
||||
return;
|
||||
|
|
@ -837,7 +871,13 @@ class Profile_Settings {
|
|||
$selected_methods[] = 'email';
|
||||
}
|
||||
|
||||
$otp_requested = in_array( 'otp', $requested_methods, true );
|
||||
$otp_requested = in_array( 'otp', $requested_methods, true );
|
||||
|
||||
// If the user entered a code without checking the checkbox, treat it as an activation attempt.
|
||||
if ( ! $otp_was_enabled && ! $otp_requested && '' !== $otp_code ) {
|
||||
$otp_requested = true;
|
||||
}
|
||||
|
||||
$otp_should_remain_enabled = false;
|
||||
|
||||
if ( $otp_was_enabled ) {
|
||||
|
|
@ -868,6 +908,7 @@ class Profile_Settings {
|
|||
);
|
||||
} elseif ( $this->otp_manager->verify_code( $user, $otp_code ) ) {
|
||||
$selected_methods[] = 'otp';
|
||||
$enabled = true;
|
||||
$this->focus_section = true;
|
||||
add_settings_error(
|
||||
'robotstxt-2fa',
|
||||
|
|
@ -933,7 +974,8 @@ class Profile_Settings {
|
|||
);
|
||||
}
|
||||
} elseif ( $recovery_confirmation_success ) {
|
||||
$selected_methods[] = 'recovery_codes';
|
||||
$selected_methods[] = 'recovery_codes';
|
||||
$enabled = true;
|
||||
} elseif ( $recovery_requested ) {
|
||||
$this->focus_section = true;
|
||||
add_settings_error(
|
||||
|
|
|
|||
|
|
@ -92,10 +92,7 @@ class Two_Factor_Config {
|
|||
* @param array<int, string> $required Method slugs required by the user's roles.
|
||||
* @param \WP_User $user User being authenticated.
|
||||
*/
|
||||
/**
|
||||
* @var array<int, string> $filtered
|
||||
* Note: at runtime a callback may return any type; the map below sanitizes it.
|
||||
*/
|
||||
// @phpstan-var array<int, string> $filtered — apply_filters returns mixed; sanitized below.
|
||||
$filtered = apply_filters( 'robotstxt_2fa_required_methods_for_user', $required, $user );
|
||||
|
||||
return array_values( array_filter( $filtered ) );
|
||||
|
|
|
|||
64
readme.txt
64
readme.txt
|
|
@ -4,7 +4,7 @@ Tags: security, two-factor authentication, login, otp
|
|||
Requires at least: 6.4
|
||||
Tested up to: 7.0
|
||||
Requires PHP: 8.0
|
||||
Stable tag: 1.2.0
|
||||
Stable tag: 1.2.1
|
||||
License: GPLv3 or later
|
||||
License URI: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
|
|
@ -59,51 +59,51 @@ Yes. Activate the plugin at the network level. Network administrators can set an
|
|||
|
||||
== Changelog ==
|
||||
|
||||
= 1.2.1 =
|
||||
|
||||
_Release date: 2026-06-05_
|
||||
|
||||
**Fixed**
|
||||
|
||||
* Recovery code confirmation without checking the method checkbox now correctly activates the method and enables 2FA.
|
||||
* Fatal error on admin profile pages in multisite — `maybe_handle_frontend_save()` now returns early when `is_admin()` is true.
|
||||
* OTP and recovery confirmation text inputs were disabled by JS when the Enable toggle was off, preventing values from reaching the server.
|
||||
* "Regenerate codes" now removes recovery codes from active methods and requires re-confirmation before reactivating.
|
||||
* Entering a valid OTP code or recovery confirmation code activates the method even without checking the Enable checkbox.
|
||||
|
||||
**Security**
|
||||
|
||||
* `RECOVERY_REGENERATE_FIELD` POST value now validated with strict `=== '1'` check.
|
||||
|
||||
= 1.1.0 =
|
||||
|
||||
_Release date: 2026-06-05_
|
||||
|
||||
* Added `[robotstxt_2fa_profile]` shortcode for frontend 2FA management without wp-admin.
|
||||
* Added developer action/filter hooks: `robotstxt_2fa_skip_challenge`, `robotstxt_2fa_verification_success`, `robotstxt_2fa_verification_failed`, `robotstxt_2fa_code_length`, `robotstxt_2fa_code_ttl`, and more.
|
||||
* Added "Regenerate codes" button on the profile when recovery codes are active.
|
||||
* Added top-up mode for recovery codes: generates only the missing codes to refill the batch.
|
||||
**Added**
|
||||
|
||||
* `[robotstxt_2fa_profile]` shortcode for frontend 2FA management without wp-admin.
|
||||
* Developer action/filter hooks: `robotstxt_2fa_skip_challenge`, `robotstxt_2fa_verification_success`, `robotstxt_2fa_verification_failed`, `robotstxt_2fa_code_length`, `robotstxt_2fa_code_ttl`, and more.
|
||||
* "Regenerate codes" button on the profile when recovery codes are active.
|
||||
* Top-up mode for recovery codes: generates only the missing codes to refill the batch.
|
||||
|
||||
= 1.0.0 =
|
||||
|
||||
_Release date: 2026-06-05_
|
||||
|
||||
* Fixed: Enable 2FA checkbox now defaults to email method on first activation.
|
||||
* Fixed: QR code now renders correctly (data URI was stripped by esc_url).
|
||||
* Fixed: Resend link shows a 60-second live countdown to prevent email flooding.
|
||||
* Changed: Recovery codes section redesigned — plain list, no green notice box.
|
||||
* Changed: 2FA login links now a vertical list instead of inline bullets.
|
||||
* Removed: Redundant "Generate new secret" button from OTP section.
|
||||
**Fixed**
|
||||
|
||||
= 0.3.0 =
|
||||
* Enable 2FA checkbox now defaults to email method on first activation.
|
||||
* QR code now renders correctly (data URI was stripped by esc_url).
|
||||
* Resend link shows a 60-second live countdown to prevent email flooding.
|
||||
|
||||
_Release date: 2026-06-05_
|
||||
**Changed**
|
||||
|
||||
* Added delete-on-uninstall option (data preserved by default per AGENTS.md).
|
||||
* Security: email verification codes now use `random_int()` (CSPRNG) instead of `wp_rand()`.
|
||||
* Security: recovery code preview transient now expires after 5 minutes.
|
||||
* Fixed network admin settings page form submission.
|
||||
* Fixed settings option set to `autoload=false` (not needed on every page load).
|
||||
* Recovery codes section redesigned — plain list, no green notice box.
|
||||
* 2FA login links now a vertical list instead of inline bullets.
|
||||
|
||||
= 0.2.0 =
|
||||
**Removed**
|
||||
|
||||
_Release date: 2026-06-05_
|
||||
|
||||
* Added per-role 2FA method matrix in admin settings with select-all row and column controls.
|
||||
* Added authenticator app (TOTP) support with QR provisioning and manual setup key.
|
||||
* Added recovery codes: 10 single-use 8-digit codes with confirmation workflow.
|
||||
* Added configurable verification frequency remembered per device.
|
||||
* Added network-wide multisite support.
|
||||
* Changed profile method checkboxes to require explicit confirmation before activation.
|
||||
* Fixed recovery-code preview persistence and QR generation error handling.
|
||||
|
||||
= 0.1.0 =
|
||||
|
||||
Initial release.
|
||||
* Redundant "Generate new secret" button from OTP section.
|
||||
|
||||
= Previous versions =
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* Plugin Name: 2FA (by ROBOTSTXT)
|
||||
* Plugin URI: https://www.robotstxt.es/plugins/robotstxt-2fa/
|
||||
* Description: Adds two-factor authentication to the WordPress login flow.
|
||||
* Version: 1.2.0
|
||||
* Version: 1.2.1
|
||||
* Author: ROBOTSTXT
|
||||
* Author URI: https://www.robotstxt.es/
|
||||
* Text Domain: robotstxt-2fa
|
||||
|
|
@ -23,7 +23,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
}
|
||||
|
||||
if ( ! defined( 'ROBOTSTXT_2FA_VERSION' ) ) {
|
||||
define( 'ROBOTSTXT_2FA_VERSION', '1.2.0' );
|
||||
define( 'ROBOTSTXT_2FA_VERSION', '1.2.1' );
|
||||
}
|
||||
|
||||
if ( ! defined( 'ROBOTSTXT_2FA_FILE' ) ) {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"name": "2FA (by ROBOTSTXT)",
|
||||
"slug": "robotstxt-2fa",
|
||||
"version": "1.2.0",
|
||||
"download_url": "https://git.robotstxt.es/ROBOTSTXT/robotstxt-2fa/releases/download/1.0.0/robotstxt-2fa-1.0.0.zip",
|
||||
"version": "1.2.1",
|
||||
"download_url": "https://git.robotstxt.es/ROBOTSTXT/robotstxt-2fa/releases/download/1.2.1/robotstxt-2fa-1.2.1.zip",
|
||||
"requires": "6.4",
|
||||
"requires_php": "8.0",
|
||||
"tested": "7.1",
|
||||
|
|
@ -11,10 +11,10 @@
|
|||
"author_profile": "https://www.robotstxt.es/",
|
||||
"homepage": "https://www.robotstxt.es/plugins/robotstxt-2fa/",
|
||||
"description": "Adds per-role two-factor authentication to the WordPress login flow. Supports email codes, authenticator apps (TOTP), and recovery codes.",
|
||||
"changelog": "<h3>1.0.0 - 2026-06-05</h3><ul><li><strong>Fixed:</strong> Enable 2FA toggle defaults to email on first activation</li><li><strong>Fixed:</strong> QR code now displays correctly in user profiles</li><li><strong>Fixed:</strong> Resend link shows 60-second countdown</li><li><strong>Changed:</strong> Recovery codes redesigned as plain list</li><li><strong>Changed:</strong> Login links now vertical list</li></ul>",
|
||||
"changelog": "<h3>1.2.1 - 2026-06-05</h3><ul><li><strong>Fixed:</strong> Recovery code confirmation without checkbox now correctly activates the method</li><li><strong>Fixed:</strong> Fatal error in multisite when saving profile (add_settings_error not available at init)</li><li><strong>Fixed:</strong> Text inputs (OTP code, recovery confirm) no longer disabled by JS when Enable toggle is off</li><li><strong>Fixed:</strong> Regenerate codes now requires re-confirmation before reactivating</li><li><strong>Fixed:</strong> Entering a valid OTP or recovery code activates the method even without checking the checkbox</li></ul>",
|
||||
"sections": {
|
||||
"description": "Adds per-role two-factor authentication to the WordPress login flow. Supports email codes, authenticator apps (TOTP), and recovery codes.",
|
||||
"changelog": "<h3>1.0.0 - 2026-06-05</h3><ul><li><strong>Fixed:</strong> Enable 2FA toggle defaults to email on first activation</li><li><strong>Fixed:</strong> QR code now displays correctly in user profiles</li><li><strong>Fixed:</strong> Resend link shows 60-second countdown</li><li><strong>Changed:</strong> Recovery codes redesigned as plain list</li><li><strong>Changed:</strong> Login links now vertical list</li></ul>"
|
||||
"changelog": "<h3>1.2.1 - 2026-06-05</h3><ul><li><strong>Fixed:</strong> Recovery code confirmation without checkbox now correctly activates the method</li><li><strong>Fixed:</strong> Fatal error in multisite when saving profile (add_settings_error not available at init)</li><li><strong>Fixed:</strong> Text inputs (OTP code, recovery confirm) no longer disabled by JS when Enable toggle is off</li><li><strong>Fixed:</strong> Regenerate codes now requires re-confirmation before reactivating</li><li><strong>Fixed:</strong> Entering a valid OTP or recovery code activates the method even without checking the checkbox</li></ul>"
|
||||
},
|
||||
"banners": {
|
||||
"low": "",
|
||||
|
|
|
|||
4
vendor/composer/installed.php
vendored
4
vendor/composer/installed.php
vendored
|
|
@ -3,7 +3,7 @@
|
|||
'name' => 'robotstxt/robotstxt-2fa',
|
||||
'pretty_version' => 'dev-main',
|
||||
'version' => 'dev-main',
|
||||
'reference' => '15bccb135d29767e53678ad70372dbd78c679c91',
|
||||
'reference' => 'c3e85cb9ff673d9102bf843668c66141b25f5db6',
|
||||
'type' => 'wordpress-plugin',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
'robotstxt/robotstxt-2fa' => array(
|
||||
'pretty_version' => 'dev-main',
|
||||
'version' => 'dev-main',
|
||||
'reference' => '15bccb135d29767e53678ad70372dbd78c679c91',
|
||||
'reference' => 'c3e85cb9ff673d9102bf843668c66141b25f5db6',
|
||||
'type' => 'wordpress-plugin',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue