=== 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.14.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.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.
