=== Threshold Login Guard ===
Contributors: spencertaylor
Tags: login, security, captcha, brute-force, woocommerce, rate-limit, cloudflare
Requires at least: 6.0
Tested up to: 6.8
Requires PHP: 7.4
Stable tag: 1.15.2
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Lean, JS-free login protection for WordPress and WooCommerce. Stateless math CAPTCHA + IP rate limiting + Cloudflare trust-but-verify + emergency bypass via wp-config.php.

== Description ==

A purpose-built, minimal login-protection plugin with no third-party JS, no external services, and no database bloat. Built to replace heavyweight security plugins that accumulate hundreds of stale option rows over time.

= Features =

* **Stateless math CAPTCHA** — HMAC-SHA256 signed tokens. Zero DB writes per form render.
* **IP-based rate limiting** — auto-expiring transients lock out brute-force attempts after a configurable threshold.
* **Cloudflare trust-but-verify** — bundled CF IP ranges; CF-Connecting-IP headers are only trusted when the request actually came from a Cloudflare edge IP, preventing header-spoofing attacks against the origin.
* **Detection status panel** at the top of every settings tab — see at a glance whether CF is detected, verified, or being spoofed.
* **IP whitelist** — bypass both CAPTCHA and rate limiter for trusted IPs.
* **Email alerts** on lockout.
* **Active lockouts dashboard** with one-click unlock.
* **WP-config.php emergency bypass** — never lock yourself out of a client site.
* **WooCommerce-aware** — protects My Account login, registration, lost password, and Checkout.
* **Self-hosted auto-updates** — fetches updates from a private JSON manifest on your own infrastructure.

= Protected forms =

* `wp-login.php` (login, registration, lost password)
* WooCommerce My Account (login, registration, lost password)
* WooCommerce Checkout (login flow)

= Emergency recovery =

If you ever lock yourself or a client out:

`define( 'TLG_DISABLED', true );`
disables the plugin entirely.

`define( 'TLG_BYPASS_IP', '1.2.3.4' );`
whitelists a single IP at PHP level.

Or rename the plugin's folder via SFTP — standard WordPress recovery.

== Installation ==

1. Upload the plugin .zip via Plugins → Add New → Upload Plugin.
2. Activate.
3. Configure at Settings → Login Guard.
4. (Optional) Add your office/home IP to the whitelist on first run.

== Changelog ==

= 1.15.2 =
* Change: Admin UI is charcoal-only - links, checkbox fills and focus rings use #222222 (links are underlined and highlight gold on hover); the two code-sample boxes use a gold rule; the magic-link email's direct link is charcoal and underlined. No blue remains on the settings screen.

= 1.15.1 =
* Fix: "Worst offending IPs" (Audit tab) and the summary email's offender list were empty on MariaDB/MySQL - the queries used aggregate aliases inside HAVING/ORDER BY expressions, which MariaDB rejects. Both now use a derived table. (1.15.0 was built and canaried but never published; its notes follow.)

= 1.15.0 =
* Change: Branding refreshed to match the new spencer-taylor.com - the Threshold Solutions logo now ships inside the plugin (no external image request), company name shown as Threshold Solutions, LLC, link colour #245a9a (7:1 on white), and the User guide link points at https://spencer-taylor.com/threshold-login-guard/#guide and the Plugin URI at the plugin's page.
* Change: New defaults - Magic link login ON with "Keep the user signed in" ON; Hardening "Block probe files (.htaccess)" ON. Existing sites are flipped on once when they upgrade to 1.15.0 (untick afterwards if unwanted; later upgrades leave the choice alone), and the .htaccess block is written at upgrade/activation when missing; Notification Email default is blank (uses the site admin email). CAPTCHA stays ON with the Math provider; General tab defaults unchanged (weekly Thursday summary; instant alerts for new-country admin login, two-factor reset, salt rotation and red Site Check items; email master and lockout alert off).
* New: Recommended panel can one-click enable Magic link login and Block probe files.
* Fix: Summary-email frequency and weekday selects now carry accessible names.
* New: General tab -> "When the plugin is deleted": Delete all Login Guard data from the database (on by default). Unticked, settings, the audit log and user 2FA/Google data survive an uninstall for a reinstall; scheduled jobs, the .htaccess block and temporary lockouts are always cleared.
* Fix (security): Blocked usernames could still log in with the correct password - WordPress discards the plugin's earlier error when both fields are filled. The block now runs inside core (wp_authenticate_user), the same fix the rate limiter got in 1.11. Same change for the XML-RPC locked-out check.
* Fix (security): Rotate salts no longer leaves a date-named copy of wp-config.php in the web root; the backup gets a random name and is deleted once the rewrite succeeds.
* Fix: "Admin login from a new country" never fired on normal Cloudflare sites (the country check looked for a detection label that does not exist); countries now record on verified Cloudflare, server-restored and trusted-proxy requests.
* Fix: WooCommerce login failures were counted twice against the lockout threshold and logged twice in the audit log.
* Fix: The wp-login CAPTCHA was also enforced on WooCommerce and third-party login forms that never showed one (when "WooCommerce login" was unticked every Woo login failed). It now applies to wp-login.php only; Woo keeps its own check.
* Fix: Settings saved on the General tab were re-read stale when re-arming the summary email (daily/weekly/off changes took one extra cycle to apply).
* Fix: Site Check reported HTTPS as red in the summary email even when the site URL is https (is_ssl() is false under cron).
* Fix: Recovery -> Reset two-factor sent one alert email per user on top of its own summary; now only the summary.
* Fix: Magic link per-IP hint showed 0 minutes (wrong settings key).
* Fix: The Recommended panel's "red Site Check items" count refreshes immediately after a one-click fix instead of an hour later.
* Fix: Recovery tools now work with the master switch off - that is when they are needed.
* Fix: 2FA and magic-link events on sites that never turned the master switch on wrote to an audit table that did not exist yet; the table is created on first write.
* Fix: Magic links open on a one-click "Sign in" confirmation page instead of signing in on the first GET, so Outlook Safe Links and other email scanners cannot spend the link before you click it. (The settings text claimed links were IP-bound; they never were - text corrected.)
* Fix: Google "confirm your password" step now burns after 5 wrong passwords and counts failures in the rate limiter.
* Fix: Uninstall also drops the audit table, clears its purge job and removes Google links, breached-password flags and remaining transients.
* Fix: Deactivating the plugin removes its .htaccess rules.
* Accessibility: Unique ids for the CAPTCHA and honeypot fields when WooCommerce shows login and register forms on one page; every number input, select and free-text field on the settings and recovery screens now has an accessible name; the 2FA profile block, magic-link email and Google sign-in UI use 16px #222 text (no 12-15px gray).
* Removed: duplicate load of qrcode.js on the profile screen.

= 1.14.2 =
* Change: Typography is fully explicit on the settings screen - base 16px, H2 22px, H3 19px, reduced text only ever .8em (12.8px, used for the connection-status panel); every text colour is #222 (table headers white on #222, footer light on #222). No sizes are inherited.
* Change: Tables scroll sideways on narrow screens instead of squeezing.
* Change: Defaults for new installs - weekly summary on Thursday; instant alerts on for new-country admin login, two-factor reset, salt rotation and red Site Check items (plugin/theme/core change stays off). Email notifications master and lockout alert remain off.

= 1.14.1 =
* Change: Admin Menu → "Hide from" scope: Everyone but me (records the saving user as owner), Everyone except exempt users, or Only these roles.
* Change: Settings screen typography is explicit (no inheritance): 16px #222 text, inputs and secondary buttons with 2px #222 borders, 22px headings, 16px primary buttons and tabs, color-picker labels #222; status panel at .8em.
* Change: Magic Link description now carries the mail-transport note; User guide link in the header and footer (TLG_GUIDE_URL).

= 1.14.0 =
* New: Magic link button. A full-width "Email me a login link" button now sits above the wp-login.php and WooCommerce login forms (the small text link remains below). Style it under Login Screen → "Login Link Button" (background, text, border color/width, corner radius; defaults #222 / #ffc966). The link email is now HTML with a "Login Now" button plus the direct link.
* New: Recovery tab - force sign-out everywhere, rotate security keys/salts (backs up wp-config.php first), reinstall WordPress core, reinstall selected wordpress.org plugins (`worker` and self-hosted plugins are never touched), bulk two-factor reset (all / role / user; users re-enrol at next sign-in, nobody is locked out), require password reset by role, and a reference of the wp-config.php kill switches and `wp tlg` commands. Every tool is admin-only, nonce'd, type-to-confirm and audit-logged.
* New: Admin Menu tab - hide any admin menu/submenu item and the Login Guard menu itself from everyone except exempt usernames; hide the front-end toolbar per role. Cosmetic only (capabilities unchanged).
* New: Email summary (daily or weekly) with event totals, worst offending IPs with country, plugin/theme/core changes and red Site Check items; instant alerts for admin login from a new country, plugin/theme/core changes, two-factor reset and salt rotation. General tab → Notifications; "Send test summary now" on Diagnostics.
* New: Recommended panel under the status bar lists protections that are off with a one-click Enable (no score).
* New: Audit Log → "Worst offending IPs (last 30 days)" with per-row Block IP and a shared-IP warning.
* New: Hardening → "Block probe files (.htaccess)" denies wp-admin/install.php, readme.html, license.txt and debug.log on Apache/LiteSpeed.
* Change: Menu icon is now a bolder shield with a single T. Settings screen text is #222 at 16px throughout (tabs, descriptions, buttons, footer).

= 1.13.0 =
* New: Magic link login. One-time, IP-bound, single-use email links (Login Guard → Magic Link). Optional links under wp-login.php and the WooCommerce My Account form, a "Send login link" row action on Users, role restriction, per-user and per-IP throttling, and a `wp tlg magic <user> [--send]` command. Magic links do not satisfy two-factor unless you switch that on.
* New: Top-level "Login Guard" admin menu with its own shield icon. Settings → Login Guard remains as an alias.
* New: Site Check tab. File-permission and exposure report (wp-config.php, .htaccess, core directories, DISALLOW_FILE_EDIT, debug display, public debug.log / readme.html / install.php, uploads directory listing, HTTPS, core update, PHP version) with a chmod-only Fix button where PHP is allowed to change the mode.
* New: Audit log records the visitor country from Cloudflare (CF-IPCountry, only when the edge is verified) and now logs plugin activate / deactivate / update / delete, theme switch / update and core updates, with the acting user.
* New: Two-factor front-end route switches on automatically on WP Engine (Two-Factor → "Auto front-end route on WP Engine", default on).
* Change: Audit log CSV export includes the country column.

= 1.12.0 =
* New: Two-Factor tab - native TOTP two-factor authentication (RFC 6238, authenticator apps), no external services. Enrol from Users -> Profile with a QR code (bundled MIT QR generator, no external calls) or manual key; one valid code required before it turns on; 10 single-use backup codes shown once and stored hashed. Login flow covers wp-login.php and WooCommerce My Account; REST / application passwords are unaffected. Options: require per role with a grace period and dashboard reminder (enrolment is requested at sign-in once grace runs out - nobody gets locked out), "Trust this browser" for N days, email-code fallback (opt-in), Sign in with Google counts as the second factor (default on), optional front-end route for hosts that block wp-login.php. Bad codes count toward the IP rate limiter; five bad codes end the attempt. Recovery: backup codes, admin "Reset 2FA" (profile + Users row action), `wp tlg 2fa reset <user>`, and define( 'TLG_2FA_DISABLED', true ) in wp-config.php. Secrets are encrypted with the site salt and never included in settings export. Audit events: 2fa_enrolled, 2fa_verified, 2fa_failed, 2fa_backup_used, 2fa_reset, 2fa_disabled, 2fa_email_sent, 2fa_trusted.
* New: Diagnostics -> Server Clock shows drift against a public time source (TOTP needs the server within ±30 s).
* Change: Split-screen login layout removed (it never rendered reliably). Sites that had it selected are switched to centered on first load of 1.12.0 (stored option cleaned up); the "Google button inside the panel" option went with it.
* Fix: Admin notices no longer land inside the plugin header. They now appear under it beneath an "Admin Messages" heading that only shows when there is something to read.

= 1.11.0 =
* New: Hardening tab - XML-RPC guard (disable auth / block; XML-RPC failures now count toward the rate limiter and locked IPs are refused), disable application passwords, disable file editing, strip generator meta + X-Pingback, user-enumeration prevention (?author=, /wp/v2/users, oEmbed), generic login error, disable comments site-wide, standalone honeypot fields, Have I Been Pwned password check (login warn/block; reset and profile always blocked).
* New: Cloudflare Turnstile as an optional CAPTCHA provider (math stays as fallback and is used automatically if keys are missing or Cloudflare is unreachable).
* New: Audit Log tab - logins, failures, lockouts, reset emails/throttles, CAPTCHA/honeypot rejections, blocked IPs, registrations; 90-day retention; CSV export.
* New: IP / CIDR block-list (login-surface or whole-site scope) on the Whitelist tab, plus "Block permanently" on Active Lockouts. Lockouts now show the plain IP.
* New: Instant lockout when the attempted username does not exist (Rate Limit tab).
* New: Settings export / import (General tab) and a TLG_DEFAULTS wp-config constant for fleet-wide defaults.
* Fix: a locked-out IP could still log in with the correct password - the rate limiter now also guards wp_authenticate_user, which WordPress core honours.
* Fix: expired lockout transients no longer appear as active on the Active Lockouts tab.

= 1.10.0 =
* New: Lost-password throttle (Rate Limit tab). One reset email per user per N minutes (default 15) and a per-IP cap (default 3 requests within the lockout window). Applies to wp-login.php and WooCommerce My Account. Stops reset-email floods from bots hitting wp-login.php?action=lostpassword even when the CAPTCHA on that form is disabled.

= 1.9.0 =
* Fix: Cloudflare detection behind WP Engine Advanced Network / GES - CF-Connecting-IP arriving as a comma-joined list ("ip, ip, ip") is now parsed (first valid IP), so IP verification reports verified/server_restored instead of falsely claiming a direct connection. Same parsing applied to True-Client-IP and X-Real-IP.
* New: WP Engine detection overhaul - the IS_WPE constant no longer exists on current WPE, so detection now fingerprints X-WPE-Request-ID / X-WPE-Install-Name / WPE-Readonly / X-WPEngine-PHP-Version headers plus PWP_NAME / WPE_APIKEY / WpeCommon server-side. Diagnostics shows install name, edge mode (AN/GES = Cloudflare edge), PHP version and the WPE bot score for the current request.
* New: Optional front-end OAuth endpoint (Google Login tab) - moves the Google callback AND the admin password-confirm step off wp-login.php, for hosts/WAFs that block or break wp-login.php requests (WP Engine edge, ModSecurity CRS). Default OFF. Enabling it changes the Authorized redirect URI - register the new URI in Google Cloud Console first.

= 1.8.0 =
* New: Option to place the "Sign in with Google" button inside the split-screen login panel (Login Screen tab -> Split-screen panel -> "Show the Google sign-in button inside this panel"). On desktop the button sits in the branded panel; on mobile, where the panel is hidden, it automatically falls back to beside the login form, so there is always a button. Requires the split-screen layout and Google sign-in to be enabled.

= 1.7.1 =
* Improve: Login Screen color fields (form background, split-screen panel background) now use the WordPress color picker with a hex entry field, instead of the fiddly native OS color box.
* Fix: American spelling ("color") in the admin labels.
* Fix: The TLG_DISABLED / TLG_BYPASS_ALL wp-config kill-switch now fully disables the login-styling and Google sign-in hooks too. Previously those initialized before the kill-switch check, so the emergency constant was a weaker off-switch than documented. It is now a true full off-switch.

= 1.7.0 =
* New: Administrator Google sign-in now works inline. The first time an admin/editor signs in with Google, they confirm their WordPress password once on a secure screen; that links the account and signs them in. Every time after is one click. No more pre-linking from the profile and no silent bounce to the login screen.
* Change: Removed the old "confirm password to connect" form on the user profile (replaced by the inline confirmation at login). The profile now just shows a clear Connected / Not connected status with a Disconnect option.
* Change: Split-screen is now the default login layout for new installs.
* Improve: The login form card is now flat (drop shadow removed).
* Fix: Lockout notification emails are throttled to at most one per hour site-wide, so a distributed brute force can no longer flood your inbox with dozens of alerts.

= 1.6.2 =
* Fix: The "Settings saved." notice appeared twice on the settings screen. WordPress core already shows this message for the plugin's ?updated=1 redirect (back-compat in options-head.php); the plugin was adding a second identical notice. Removed the duplicate.

= 1.6.1 =
* Fix: Google sign-in callback returned a 403 on servers running the OWASP ModSecurity ruleset. Google echoes the granted scope back on the redirect, and the "userinfo.profile" value contains ".profile", which trips CRS rule 930120 (OS-file/LFI). The plugin now requests only the "openid email" scopes - enough to identify the user - which avoids the false positive. (Trade-off: auto-created users get their email as their display name rather than their Google name.)

= 1.6.0 =
* New: Sign in with Google (OpenID Connect / OAuth 2.0) built into the plugin - new "Google Login" tab. Buttons on wp-login.php and WooCommerce login/registration. Matches by verified Google email, optional auto-provisioning of new users (WooCommerce customer / subscriber), optional email-domain allowlist.
* Security: administrator and editor accounts must connect Google from Users -> Profile (re-entering their WordPress password) before Google sign-in works for them - blocks account takeover via a matching Google email and unattended-session linking. Customers/subscribers sign in frictionlessly.
* Self-contained: no third-party OAuth library, no external JS. Server-side code exchange; the id_token is validated by aud/iss/exp and requires email_verified.

= 1.5.0 =
* New: Login Screen customizer (new "Login Screen" tab) - brand wp-login.php with your logo (media picker or URL, logo links to your site), page background colour/image, button & link accent colour, form-card background and rounding, and a raw Custom CSS box.
* New: Split-screen login layout - login form on one side, a branded colour/image panel with heading + text on the other; collapses to the centered layout on tablet and mobile.
* New: "Inherit from theme" - any logo/colour field left blank falls back to the site Logo, theme background colour, and theme.json accent colour (the theme stylesheet itself is not loaded on the login page).
* Fix: Admin footer site link now renders in brand gold (was inheriting blue).

= 1.4.2 =
* New: Branded admin UI styled after spencer-taylor.com - Threshold Solutions logo, Ubuntu Sans, gold/charcoal palette, signature pill buttons, and a branded header + footer with company name, author, and site link.
* Change: Settings menu label renamed from "Login Guard" to "Threshold Login Guard".

= 1.4.1 =
* Change: Email alerts are now OFF by default on fresh installs — both "Send email alerts" and "Notify on lockout" start unchecked.
* Change: Default notification recipient is pre-filled as spencertaylor@gmail.com.
* Improve: Lightweight admin UI styling — header card, version badge, and card-style settings panels. No JavaScript, no external assets.

= 1.4.0 =
* New: Username blocking - reject all login attempts to specified usernames (default: "admin") regardless of password. Catches WP login, WC login, XML-RPC, and REST API in one hook.
* Stealth: blocked usernames return the same generic error WordPress shows for non-existent users, so attackers cannot fingerprint that a blocklist exists.
* Rate limit integration: blocked attempts automatically count toward IP rate limits.
* Email-form login: also catches `admin@example.com`-style attempts where the local part matches the blocklist.
* Safety: admin UI warns if any name on the blocklist matches an actual existing WP user on the site, before the admin can lock themselves out.

= 1.3.0 =
* New: Disposable email blocker — blocks WP registration, WC registration, and WC checkout against a list of ~5,400 known disposable email domains (MIT-licensed disposable-email-domains list, bundled with the plugin).
* New: Weekly auto-refresh of the disposable email list from the update server. Falls back to bundled list if fetch fails.
* New: Custom blocklist and allowlist textareas — extend the master list with your own additions, or whitelist internal/testing domains.
* New: "Refresh List Now" button + master-list status panel (source, domain count, last fetched).
* New: Email normalization strips subaddresses (`user+tag@domain.tld` → checked against `domain.tld`).

= 1.2.0 =
* New: Auto-detection of managed-WP hosts (WP Engine, Kinsta, Pressable, Pantheon, Flywheel) by header/constant fingerprints. The plugin now self-configures its trusted-proxy chain for these environments.
* New: Generic reverse-proxy detection — if REMOTE_ADDR is an RFC1918 private IP and X-Forwarded-For is present, the plugin trusts XFF automatically.
* New: True-Client-IP header support (Akamai, CF Enterprise).
* New: Diagnostics tab — shows detected host, all relevant headers and constants, detection state, and a copy-paste override snippet for unusual environments.
* New: "Check for Updates Now" now reports a clear result — "Update available", "Up to date (vX.Y.Z)", or "Could not reach update server".
* New filter: `tlg_host_environment` — override the detected host entirely.
* Status panel now shows detected environment label on every tab.

= 1.1.1 =
* Fix: CF detection now correctly handles server-side IP restoration (Apache mod_remoteip + cloudflare.conf, LiteSpeed useIpInProxyHeader=2, nginx real_ip). Previously these "gold-standard" CF setups were misclassified as "suspicious" because REMOTE_ADDR was the visitor's real IP instead of a CF edge IP. The plugin now treats the case where REMOTE_ADDR === CF-Connecting-IP as verified (via server-side restoration).
* Status panel now shows the CF-Connecting-IP header value in the "suspicious" state for easier debugging.
* Added a `via` field to the detection result distinguishing edge / server_restored / x_real_ip verification paths.

= 1.1.0 =
* Cloudflare trust-but-verify: bundled CF IPv4/v6 ranges, CF-Connecting-IP only trusted when REMOTE_ADDR is a real CF edge IP.
* New status panel at top of every settings tab shows detection state (verified / direct / suspicious) with visitor + edge IPs.
* Self-hosted auto-update system: fetches a JSON manifest, integrates with WP's native update flow.
* "Check for Updates Now" button on the General tab.
* New filters: `tlg_trusted_proxy_ranges`, `tlg_trust_x_real_ip`.

= 1.0.0 =
* Initial release.
* HMAC-signed math CAPTCHA for WP and WC login/register/lostpass forms.
* IP-based rate limiting with shared counter across WP and WC.
* IP whitelist with admin UI + "add my current IP" helper.
* Active lockouts dashboard with unlock controls.
* Email alerts on lockout.
* Emergency bypass via wp-config.php constants.
* Clean uninstall.
