== Changelog ==

= 1.6.7 =

_Release date: 2026-09-17_

**Fixed**

* The forced-setup wizard felt like a broken redirect loop: the wizard redirect bounced users to the profile with no explanation (it now carries a flag that renders a clear "activate at least one verification method, then save" warning), and saving with an expired form nonce silently did nothing (it now raises a visible "form session expired" error so users know to try again).

**Compatibility**

* WordPress: 5.6 – 7.1
* PHP: 8.0 – 8.5

**Tests**

* PHP Coding Standards: PHP_CodeSniffer 3.13.6 / WPCS 3.4.1 — 0 errors
* PHPStan: level 9 — 0 errors
* PHPCompatibility: 8.0–8.5 — 0 issues
* PHPUnit: 9.6.36 — 98 tests, 197 assertions

= 1.6.6 =

_Release date: 2026-09-17_

**Added**

* Forced-enrollment warning on the profile 2FA section: when a user's role enforces 2FA but they have no verification method configured yet, the 2FA section of their profile (wp-admin and the frontend shortcode) opens with a warning notice asking them to activate at least one method. While a grace period is active, the notice appends the remaining days.

**Compatibility**

* WordPress: 5.6 – 7.1
* PHP: 8.0 – 8.5

**Tests**

* PHP Coding Standards: PHP_CodeSniffer 3.13.6 / WPCS 3.4.1 — 0 errors
* PHPStan: level 9 — 0 errors
* PHPCompatibility: 8.0–8.5 — 0 issues
* PHPUnit: 9.6.36 — 93 tests, 189 assertions

= 1.6.5 =

_Release date: 2026-09-17_

**Fixed**

* Fatal error on the login screen with newer WordPress versions: the `login_message` filter can deliver null when no message is set (observed on WordPress 7.x with PHP 8.4, TypeError on a plain visit to wp-login.php). The message callback now accepts `string|null` and coalesces null to an empty string.
* Hardened all externally-fed filter callbacks against null payloads: the four `authenticate` callbacks now accept `string|null` credentials (custom REST/SSO endpoints are known to apply the filter with null), and the `wp_redirect` filter callback coalesces a null location. Verified safe as-is: shortcode callback, Settings API sanitizer, activation hooks, and admin-hook parameters.

**Compatibility**

* WordPress: 5.6 – 7.1
* PHP: 8.0 – 8.5

**Tests**

* PHP Coding Standards: PHP_CodeSniffer 3.13.6 / WPCS 3.4.1 — 0 errors
* PHPStan: level 9 — 0 errors
* PHPCompatibility: 8.0–8.5 — 0 issues
* PHPUnit: 9.6.36 — 87 tests, 180 assertions

= 1.6.4 =

_Release date: 2026-09-11_

**Fixed**

* Compatibility with Restrict Content Pro's "Hijack Login URL" option: RCP's `login_url` filter made every `wp_login_url()` call return a membership page, so the 2FA verification redirect landed on a restricted page where the verification form cannot render, and the visitor was bounced to the registration page. Verification-stage URLs are now built from the canonical `wp-login.php`, mirroring WordPress core's URL construction before the filterable output. The "Back to login" link keeps the site-configured login URL.

**Compatibility**

* WordPress: 5.6 – 7.1
* PHP: 8.0 – 8.5

**Tests**

* PHP Coding Standards: PHP_CodeSniffer 3.13.6 / WPCS 3.4.1 — 0 errors
* PHPStan: level 9 — 0 errors
* PHPCompatibility: 8.0–8.5 — 0 issues
* PHPUnit: 9.6.36 — 78 tests, 171 assertions

= 1.6.3 =

_Release date: 2026-08-24_

**Changed**

* Manager detection now uses the ecosystem presence constant (`ROBOTSTXT_MANAGER_NOTICED`, defined by Manager 1.6.2+) with a fallback to the plugin-list scan for older Manager versions, so a stale plugin list can no longer produce false "Manager missing" notices.

**Fixed**

* Compatibility with the ALTCHA Spam Protection plugin: when "Protect login" was enabled, submitting the 2FA verification code failed with "[ALTCHA] Sorry, your request could not be processed.". The ALTCHA interceptor is now disabled while the verification screen is shown; the first login step keeps its ALTCHA check.

**Compatibility**

* WordPress: 5.6 – 7.1
* PHP: 8.0 – 8.5

**Tests**

* PHP Coding Standards: PHP_CodeSniffer 3.13.6 / WPCS 3.4.1 — 0 errors
* PHPStan: level 9 — 0 errors
* PHPCompatibility: 8.0–8.5 — 0 issues
* PHPUnit: 9.6.36 — 73 tests, 163 assertions

= 1.6.2 =

_Release date: 2026-08-17_

**Added**

* Recommendation notice for the Manager (by ROBOTSTXT) plugin: when it is not installed and active, a dismissible notice appears on the Plugins screen and a permanent notice is shown on the plugin settings page, since updates are delivered through the Manager plugin.

**Changed**

* Updates are now handled by the Manager (by ROBOTSTXT) plugin. The bundled self-updater (robotstxt-updater.php and update.json) has been removed.
* Plugin and update URLs moved to robotstxt.software.
* Minimum WordPress version lowered from 6.4 to 5.6 after a full compatibility review (the code only requires WordPress 5.3+ functions, and WordPress 5.6 is the first release that runs on the required PHP 8.0).

**Compatibility**

* WordPress: 5.6 – 7.1
* PHP: 8.0 – 8.5

**Tests**

* PHP Coding Standards: PHP_CodeSniffer 3.13.6 / WPCS 3.4.1 — 0 errors
* PHPStan: level 9 — 0 errors
* PHPCompatibility: 8.0–8.5 — 0 issues
* PHPUnit: 9.6.36 — 59 tests, 148 assertions

= 1.6.1 =

_Release date: 2026-08-14_

**Fixed**

* Fatal error during editor autosaves: the `[robotstxt_2fa_profile]` shortcode expanded when WordPress applied content filters to a post revision via REST (for example, an article that merely mentions the shortcode in its text). In that context `settings_errors()` and `submit_button()` are not defined, causing a fatal error and a failed autosave. The shortcode now bails out early on REST requests (avoiding side effects such as OTP secret generation) and loads the required wp-admin includes on demand elsewhere.

**Compatibility**

* WordPress: 6.4 – 7.1
* PHP: 8.0 – 8.5

**Tests**

* PHP Coding Standards: PHP_CodeSniffer / WPCS — 0 errors
* PHPStan: level 9 — 0 errors
* PHPCompatibility: 8.0–8.5 — 0 issues
* PHPUnit: 9.6.34 — 59 tests, 148 assertions

= 1.6.0 =

_Release date: 2026-08-07_

**Added**

* REST API (namespace `robotstxt-2fa/v1`, admin-only): `GET/PUT /settings` to read and update the full configuration; `GET /users?role=&status=` to list users with their 2FA status (enabled, configured methods, required methods, forced, frequency, preferred method, OTP configured, unused recovery codes). No secrets exposed.

**Changed**

* `wp 2fa list` — separate Enabled, Methods, and Required columns; new `--required` filter.

**Compatibility**

* WordPress: 6.4 – 7.1
* PHP: 8.0 – 8.5

**Tests**

* PHP Coding Standards: PHP_CodeSniffer / WPCS — 0 errors
* PHPStan: level 9 — 0 errors
* PHPCompatibility: 8.0–8.5 — 0 issues
* PHPUnit: 9.6.34 — 55 tests, 140 assertions

= 1.5.3 =

_Release date: 2026-08-07_

**Added**

* Per-user preferred sign-in method — "Preferred method" dropdown on the user profile (shown when two or more methods are enabled) lets each user pick which verification method is requested first at login.
* Failed attempts are now cleared for the user on any successful 2FA login (email, OTP, or recovery), so no errors remain queued once they authenticate correctly.
* GeoIP auto-download — optional "Use the free ROBOTSTXT GeoIP database" checkbox in 2FA > Settings downloads the country database from ip.robotstxt.es into the uploads directory and refreshes it daily via WordPress cron. Includes a manual "Update now" button.

**Fixed**

* Security: the "Require 2FA to create Application Passwords" guard could be bypassed by requesting `/wp/v2/users/me/application-passwords` (the regex only matched numeric user IDs). Now accepts the `me` alias.

**Changed**

* Role enforcement is now a floor, not a ceiling. Required methods are still forced on the profile (enforced setup), but at login users may authenticate with any method they have configured — not only the role-required ones. Fixes users who only saw email despite configuring OTP/recovery.
* "Remember this browser" duration now follows each user's verification frequency (daily/weekly/monthly) instead of a fixed global value, so the checkbox always complies with the profile setting.

**Compatibility**

* WordPress: 6.4 – 7.1
* PHP: 8.0 – 8.5

**Tests**

* PHP Coding Standards: PHP_CodeSniffer / WPCS — 0 errors
* PHPStan: level 9 — 0 errors
* PHPCompatibility: 8.0–8.5 — 0 issues
* PHPUnit: 9.6.34 — 50 tests, 121 assertions

= 1.5.2 =

_Release date: 2026-06-06_

**Added**

* OTP import tool (2FA > Import) — import authenticator secrets from third-party 2FA plugins without requiring users to reconfigure their app:
  * Two Factor (community) — plain Base32 from `_two_factor_totp_key` user meta.
  * WP 2FA (Melapress) — plain or encrypted Base32 from `wp_2fa_totp_key` user meta; decrypted if WP 2FA is active.
  * Wordfence Login Security — raw binary from `wfls_2fa_secrets` table, Base32-encoded on import.
* Admin notice banner when importable secrets are detected; dismissible per-admin for 7 days or permanently via Import page preference.
* Import correctly skips users who already have OTP actively configured in our plugin; only imports for users who have not yet activated OTP here.

**Fixed**

* Deactivating the Authenticator App method from the profile no longer pre-generates a new OTP secret immediately. The secret is deleted cleanly and a fresh QR code is generated lazily the next time the user views their profile. This ensures the import tool can correctly detect these users as candidates.

**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.5.1 =

_Release date: 2026-06-06_

**Security**

* CSV exports use RFC 4180 encoding — fields with commas, double-quotes, or line breaks are correctly quoted. Replaces `addslashes()`.
* GeoIP database path validated with `is_file()` and `!is_link()` at read time to block symlink traversal.
* `robotstxt_2fa_app_password_verification_window` filter return validated as a positive integer; falls back to 900 for invalid values.

**Fixed**

* Export CSV button now only rendered to users with `manage_options` capability.

**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.5.0 =

_Release date: 2026-06-05_

**Added**

* Full audit user table (WP_List_Table) with sortable columns, role/status filters, and CSV export.
* GeoIP country restrictions via optional MaxMind GeoLite2 database: per-country allow list, deny list, always-challenge list.
* Require recent 2FA verification before creating Application Passwords (REST endpoint, 15-min window, filterable).
* WP-CLI `wp 2fa export` — CSV report via stdout.
* `robotstxt_2fa_force_challenge` filter — override frequency skip; used by GeoIP always-challenge.
* `maxmind-db/reader` added as optional production Composer dependency.

**Fixed**

* Email digest cron now reschedules correctly when frequency changes (weekly ↔ monthly).

**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.4.0 =

_Release date: 2026-06-05_

**Added**

* Audit Dashboard — top-level admin page with summary cards (total users, 2FA-enabled, recent failed attempts), failed attempts table, and 2FA status column in Users list.
* Failed attempts log — `robotstxt_2fa_failed_log` ring buffer (max 100 entries) populated on every failed verification; IPs anonymized.
* Email notifications (each independently configurable):
  * Admin-enabled 2FA: user is notified when an administrator activates 2FA for them.
  * New location login: user is notified on first successful login from an unrecognised context.
  * Recovery code used: user (and optionally site admin) notified when a recovery code is consumed.
  * Activity digest: WP-Cron weekly or monthly summary sent to administrators.
* Application Passwords exemption: REST API clients skip the 2FA browser challenge by default.
* IP allow list: IPs/CIDR ranges that bypass 2FA (via `robotstxt_2fa_skip_challenge`).
* IP deny list: IPs/CIDR ranges blocked from login entirely (via `authenticate` at priority 1).
* IPv4 + IPv6 CIDR matching (pure PHP, no external dependency).

**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.3.0 =

_Release date: 2026-06-05_

**Security**

* TOTP replay prevention: accepted counter step stored in a 90-second transient; same code rejected on second submission within the ±1 window.
* Login username removed from 2FA redirect URLs: `robotstxt-2fa-login` query parameter replaced with an opaque 32-character token resolved server-side. Username never appears in browser history, logs, or referrer headers.

**Added**

* WP-CLI command family `wp 2fa` (loaded only when `WP_CLI` is defined):
  * `wp 2fa status <user_id>` — show 2FA configuration.
  * `wp 2fa enable <user_id> [--method=<method>]` — enable 2FA.
  * `wp 2fa disable <user_id>` — disable 2FA, preserving secrets and codes.
  * `wp 2fa reset-recovery <user_id>` — regenerate and display recovery codes.
  * `wp 2fa list [--role=<role>] [--without-2fa] [--format=table|csv|json]` — list users with 2FA status.
  * `wp 2fa force-setup [<user_id>] [--role=<role>] [--method=<method>]` — enforce 2FA.
  * `wp 2fa bypass <user_id> [--days=<n>]` — grant temporary bypass (max 30 days).

**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.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_

**Added**

* Developer filter/action hooks: `robotstxt_2fa_skip_challenge`, `robotstxt_2fa_verification_success`, `robotstxt_2fa_verification_failed`, `robotstxt_2fa_method_enabled`, `robotstxt_2fa_method_disabled`, `robotstxt_2fa_code_length`, `robotstxt_2fa_code_ttl`, `robotstxt_2fa_resend_interval`, `robotstxt_2fa_email_subject`, `robotstxt_2fa_email_message`, `robotstxt_2fa_before_send_email`.
* `robotstxt_2fa_required_methods_for_user` filter to override per-user method requirements.
* `robotstxt_2fa_profile_wrapper_class` filter for the frontend shortcode container.
* `[robotstxt_2fa_profile]` shortcode — renders the full 2FA settings section on any WordPress page without requiring wp-admin access. Supports `user_id` and `redirect` attributes.
* `top_up_codes_for_user()` method on `Recovery_Codes` — generates only the missing codes to fill the batch back to 10, preserving existing unused codes.
* "Regenerate codes" button on the profile when recovery codes are active, without needing to disable and re-enable the method.

**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.0.0 =

_Release date: 2026-06-05_

**Fixed**

* Enable 2FA checkbox now defaults to email and pre-checks it in the UI on first activation.
* QR code for authenticator apps now displays correctly in the user profile.
* "Send the code again" link is disabled for 60 seconds after delivery with a live countdown.

**Changed**

* Recovery codes section redesigned: plain list with code chips, no coloured notification box.
* 2FA login screen links now in a vertical list for better readability.

**Removed**

* "Generate new secret" button from the OTP section.

**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

= 0.3.0 =

_Release date: 2026-06-05_

**Added**

* Delete-on-uninstall option (disabled by default — all plugin data is preserved on removal).

**Security**

* Email verification codes now use `random_int()` (CSPRNG).
* Recovery code preview transient TTL reduced from indefinite to 5 minutes.

**Fixed**

* Network admin settings now save correctly via a dedicated handler writing to `wp_sitemeta`.
* Settings option registered with `autoload=false`.
* `update.json` corrected to reference the 2FA plugin instead of the SMTP plugin.

**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

= 0.2.0 =

_Release date: 2026-06-05_

**Added**

* Per-role 2FA method matrix with select-all row/column controls.
* Authenticator app (TOTP) support with QR provisioning and manual setup key.
* Recovery codes: 10 single-use 8-digit codes with confirmation workflow.
* Email-based verification codes with 60-second resend throttle and 10-minute expiry.
* Configurable verification frequency (every login, daily, weekly, monthly) per device.
* Dedicated stage token (WP nonce) securing the 2FA screen.
* Network-wide multisite support.
* Admin settings page with top-level menu.

**Changed**

* Profile method checkboxes require explicit confirmation before activation.

**Fixed**

* QR generation errors caught; recovery-code preview persists until acknowledged.

**Compatibility**

* WordPress: 6.4 – 7.0
* PHP: 8.0 – 8.5

**Tests**

* PHP Coding Standards: PHP_CodeSniffer 3.13.5 / WPCS 3.3.0
* PHPStan: level 9 — 0 errors
* PHPUnit: 9.6.34

= 0.1.0 =

_Release date: 2024-04-08_

**Added**

* Initial plugin skeleton.
* Documentation files.
* Placeholder classes for login handling, profile integration, and admin settings.
