=== FormGhost ===
Contributors: byabdalla
Tags: spam, antispam, honeypot, captcha alternative, gdpr
Requires at least: 6.0
Tested up to: 7.1
Requires PHP: 7.4
Stable tag: 1.4.0
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Invisible 9-layer spam protection — the spam never happened. Zero CAPTCHAs. Zero cloud. 100% free. GDPR-compliant by design.

== Description ==

**The spam never happened.** FormGhost is a privacy-first WordPress antispam plugin that makes spam quietly disappear: bots are shown a fake success page and walk away believing they got through, while you never receive a thing. No CAPTCHA is shown unless you choose to add one, nothing is sent to anyone but you (FormGhost emails you through your site's own mail setup, and talks to a third party only if you switch on the optional CAPTCHA), and what FormGhost keeps stays in your own database: held submissions encrypted for a limited time, IP addresses only as keyed hashes. Spam goes into the Vault. Real users never see anything.

**Every feature is free. There is no Pro version, no license key, no upsell.**

= Why FormGhost =

* No CAPTCHAs. No "select all the buses." No "I'm not a robot" checkbox.
* No cloud dependency — the spam detection itself makes no external HTTP calls. The only optional exceptions (CAPTCHA verification, MX lookup) are off by default and documented under External services.
* GDPR / DSGVO friendly. IP addresses are hashed with a per-site secret before storage. Form payloads are encrypted (AES-256-GCM) in the vault.
* Ghost Response: bots see a fake success message and walk away thinking the submission worked. You never get the email.
* Nobody real gets lost silently: when a held message passed every bot check, FormGhost flags it under "Needs review", counts it in the admin menu and emails you.
* Blocked something legitimate? Find it in the Vault by name, email or a word from the message and forward it to the inbox it was headed to in a couple of clicks.

= How it works =

Every submission is checked by up to nine independent layers, plus an identity check that notices one mailbox sending under many names. Each layer adds to a spam score. A score of 50 or more is held, and a filled-in honeypot is held straight away — unless the timing, proof-of-work and behaviour checks all show a person, because a browser's auto-fill sometimes writes into hidden fields. FormGhost then usually answers with a silent Ghost Response, so the bot never learns it was blocked.

1. **Honeypot** — Daily-rotated, site-suffixed hidden fields. Different on every FormGhost site; resistant to "fill every field" bots.
2. **Timing** — Encrypted submission timestamp. Submissions that arrive too fast (or with a replayed token) fail this layer.
3. **Proof-of-Work** — Lightweight browser challenge solved by a WebWorker. Stops curl / wget / scripted submissions cold.
4. **Behavioral fingerprint** — Mouse curvature, keystroke variance, scroll, focus and touch signals computed entirely in the browser. Only the resulting score is transmitted.
5. **Rate limiting** — Per-IP limits for each kind of form (contact forms, comments, registration, WooCommerce, password reset) within a time window, a separate failed-login limit per account and per IP address, and whitelisting of single IPs or CIDR ranges.
6. **Disposable email detection** — Curated disposable-domain blocklist (extensible with your own block/allow lists), optional MX heuristic.
7. **Content patterns** — Casino / SEO / pharma phrase detection with a self-learning twist.
8. **WordPress hardening** — Disable XML-RPC, harden the REST users endpoint, generic login errors, optional Application Passwords lockdown.
9. **Optional CAPTCHA** — Turnstile / hCaptcha / ALTCHA on the core WordPress forms you choose (comments, login, registration, password reset) if you want a visible challenge as a last line of defence. Off by default.

= Form plugin compatibility =

Built-in adapters for:

* WordPress core comments, login, registration, password-reset
* Contact Form 7
* WPForms
* Gravity Forms
* Elementor Pro Forms
* WooCommerce checkout & registration
* Fluent Forms
* Ninja Forms
* Formidable Forms
* Forminator
* HTML Forms
* Jetpack Forms
* Bricks Builder forms
* Kadence Blocks forms
* Divi contact forms
* BuddyPress signup & activity
* bbPress topics & replies

Custom forms POSTing to `wp-admin/admin-post.php` or `wp-admin/admin-ajax.php` are picked up automatically by the generic adapter.

= The Vault =

Blocked submissions are stored encrypted for 30 days (configurable). Review them in the admin: see who sent what, why it was held (in plain language), which page it was submitted from, and which inbox the notification was headed to. Search by name, email address, words from the message, domain or entry number; filter by form, reason and date; switch between form submissions, held, delivered and reviewed entries, and login attempts. Mark false positives as legitimate and forward them by email in a couple of clicks — the sender's address becomes the Reply-To, so answering in your mail client reaches the person who filled the form. The forwarded email treats the submission as untrusted: links and addresses in it are broken up so they cannot be clicked by accident. Passed (legitimate) submissions are logged too, so you can audit both sides of every decision.

**Needs review.** A Ghost Response shows the sender a success message, so a real person caught by mistake would never find out. When a held submission passed every bot check — a real browser, a person typing — only the content or volume checks held it. FormGhost flags such entries under "Needs review", shows the count in the admin menu, and emails you (at most ten a day; Settings → Notifications).

= Self-learning =

When you mark a Vault entry as "spam" the engine extracts the sender's address, the email domain, the domains of links in the message and distinctive phrases (and the hashed IP address), and stores them in a learned-rules table with a score modifier. Marking an entry as legitimate does the opposite: that sender and the matching signals get negative weights. Future submissions matching those signatures get extra points, automatically. Auto-confirmed rules from repeat offenders are added too. Rules FormGhost learns by itself add at most 40 points together, so they never hold a message on their own, and nothing is learned from a submission that passed every bot check. Decay over time keeps the rule set fresh. Review, re-weight, deactivate or delete every learned rule from the Learned Rules screen, or add your own (trust a partner's domain, block an address); a rule you switch off stays off.

= Privacy =

* The nine detection layers, the Vault and the self-learning system run entirely on your server. Two optional, off-by-default features talk to the outside: CAPTCHA verification (Cloudflare Turnstile / hCaptcha) and the DNS MX check — see "External services" below.
* IPs are stored only as `sha256(site_secret + "|" + ip)`, never as the address itself. The secret is kept in the same database, so the hash stops anyone from simply reading an address; someone with a full copy of the database could still work an IPv4 address out by trying every possible one.
* Vault payloads are encrypted at rest with AES-256-GCM (authenticated encryption).
* Uninstalling the plugin removes every option, every table and every cron event. Nothing is left behind.

== Installation ==

1. Upload the `formghost` folder to `wp-content/plugins/` (or install through the Plugins screen).
2. Activate FormGhost on the Plugins screen.
3. Run the one-time wizard that appears on activation — toggle the surfaces to protect and pick a sensitivity profile; FormGhost sets timing, proof-of-work and detection sensitivity to match.
4. Done. Real users keep submitting; bots stop arriving.

== Frequently Asked Questions ==

= Is FormGhost really completely free? =

Yes. All nine layers, the Vault, the self-learning system, Ghost Response — everything ships in the free plugin. There is no Pro tier and no license key.

= Will it break my real forms? =

No. FormGhost is invisible — no CAPTCHA, no extra fields visible to humans, no JavaScript prompts. The honeypot fields are hidden via multiple CSS techniques and `aria-hidden`. The timing token and proof-of-work fields are injected by JavaScript with no UI footprint.

= Does it work without JavaScript? =

Partly. The WordPress comment, login, registration and password-reset forms work without JavaScript as long as "Allow visitors without JavaScript" is on (the default). Forms from form plugins need JavaScript: FormGhost adds its checks to them with JavaScript, so a submission without it looks like a bot and is held. Turning the setting off makes the core forms strict too.

= Does it send data to your servers? =

No — FormGhost has no servers of its own and its spam detection never leaves your site. The only outbound calls the plugin can make are the optional, off-by-default Turnstile / hCaptcha token verification and the DNS MX lookup, both described under "External services" below.

= How does the wizard configure my site? =

Choose which surfaces to protect (comments, login, registration, contact forms, WooCommerce, password reset) and a sensitivity profile — Low, Medium or High. FormGhost applies a curated recipe of timing thresholds, proof-of-work difficulty and pattern aggressiveness. You can fine-tune everything afterwards in Settings, and re-run the wizard any time.

= I marked a legitimate submission as blocked. What now? =

Open the Vault (search for the sender's name or address), open the entry and click **Send email** with "Also mark as legitimate" ticked: it is forwarded to the inbox it was originally headed to and marked legitimate in one step. FormGhost learns from your correction: that sender's address and the matching domains and phrases get negative weights, so similar messages are less likely to be held.

= Can I see why a submission was held? =

Yes. The Vault list says in plain language why each entry was held (for example "Filled in a hidden trap field that people never see"), and the detail view adds the score, the check and its reason code, the page the form was submitted from, and the inbox the notification was headed to.

= Will FormGhost tell me when it may have held a real person? =

Yes. A held submission that passed every bot check is flagged under "Needs review" in the Vault, counted next to FormGhost in the admin menu, and announced by email to the site's administration address, or to one or more addresses you set under Settings → Notifications (comma-separated). You get at most ten such emails a day. The email contains the sender's name and address and the start of their message.

= Does it work with caching plugins? =

Yes. The honeypot CSS is inline, and the timing token, proof-of-work challenge and Behavioral layer nonce are fetched fresh from a no-cache endpoint when the page loads, so cached pages still work. Heavy page caching does not weaken FormGhost.

= Is it compatible with WP Rocket / W3 Total Cache / Cloudflare? =

Yes. Everything that must be fresh is fetched after the page loads, and the plugin sends `nocache_headers()` on its own AJAX endpoint. On a Cloudflare-fronted site, also see the rate-limit question below.

= How do I switch FormGhost off if I cannot log in? =

Add this line to `wp-config.php`, above the line that says "That's all, stop editing!":

`define( 'FORMGHOST_DISABLE', true );`

FormGhost then does nothing at all — no checks, no scripts, no hooks — while staying activated, so your settings are kept. Log in, fix the setting that caused the problem, and remove the line to switch FormGhost back on.

= Why does the login CAPTCHA let some logins through unchecked? =

On the login form the CAPTCHA is a bot deterrent, not an authentication control. If it cannot be checked — the widget did not load, the provider could not be reached, or it rejected your secret key — FormGhost lets the login continue instead of locking you out of your own site. Every other FormGhost check and the login rate limit still apply to that attempt, so a bot that simply leaves the CAPTCHA out gains nothing it could use against an account it does not already have the password for. A token the provider actually rejects still blocks the login. When a successful login gets through unchecked, or the provider cannot be reached, or your secret key is rejected, FormGhost shows an admin notice so a broken setup does not go unnoticed. Comments, registration and password reset do not fail open.

= How does the login rate limit work? =

Only failed logins count. By default, one IP address can fail 10 times on the same account, and 30 times across all accounts, within 15 minutes. After that FormGhost blocks further login attempts from that address until the 15 minutes are up — the correct password included, so a password-guessing bot gets nowhere. A successful login clears that account's failures, so someone who mistypes a few times still gets in. Developers can change the numbers with the `ratelimit_limits` setting.

= My site is behind Cloudflare. Does the rate limit lock out everyone at once? =

Not on Cloudflare. With "Trust reverse-proxy headers" off, WordPress sees Cloudflare's addresses instead of your visitors', and many visitors share each one. FormGhost recognises Cloudflare's published addresses and uses the visitor address Cloudflare reports for the login rate limit, so one person's typos do not lock others out. Behind other proxies — nginx, a load balancer, Docker — FormGhost cannot safely trust the forwarded address on its own, so everyone behind the proxy shares one limit until you act. In both cases wp-admin shows a notice: turn on "Trust reverse-proxy headers" under FormGhost → Settings → General, which also lets every other check tell your visitors apart.

= Where are blocked submissions stored? =

In a `{prefix}formghost_vault` table you control, encrypted with AES-256-GCM against a per-site secret stored in `formghost_site_secret`. Default retention is 30 days; configurable; uninstall removes the table entirely.

= I run a forum / membership / e-commerce site — what do you recommend? =

Run the wizard once and enable the registration / WooCommerce scopes that match your attack surface, with the Medium sensitivity profile. Layers 4 (Behavioral), 5 (Rate limit) and 6 (Email) add the most uplift against modern bots and are on by default.

== Screenshots ==

1. The Dashboard — blocked and passed counts, layer breakdown, recent activity.
2. The Vault — blocked, ghosted and passed submissions with the page they came from, the inbox they were headed to and whether they were delivered; forward a false positive to that inbox.
3. Settings — choose exactly where FormGhost runs; every layer is tunable.
4. Learned Rules — what FormGhost learned from your reviews and from spam it caught on its own; adjust or switch off any rule.
5. The wp-admin dashboard widget — key numbers right after login.

== Changelog ==

= 1.4.0 =
* New: login attempts are kept apart from form submissions. They have their own retention (7 days by default; 0 stores none and only counts them) and their own cap (2,000), and no longer count towards the Vault's maximum, so a login flood cannot push real messages out. Entries older than the new retention are removed after the update. Settings → Vault.
* New: attack tools are recognised. A user agent of just "Mozilla/5.0" (sent by login-flooding tools, never by a browser) holds the request; a missing user agent adds points. Applies to every form and the login, never to API clients. Settings → Layers → WordPress hardening.
* New: FormGhost notices when bots guess the site's own names. If held login attempts use an author's display name, author address name or real login name, the dashboard says so, with advice.
* New: message templates as learned rules. A spam campaign that sends the same message again and again — also "spun" with swapped words around the same link — is learned as one rule for the whole message, instead of many separate phrases. You can also paste a spam message under Learned Rules → Add a rule.
* Database: no table change; schema 8 trims stored login attempts to the new retention.

= 1.3.2 =
* Maintenance: the changelog and code comments use neutral example names. No change in behaviour.

= 1.3.1 =
* Fixed: the Vault list showed every entry marked legitimate as "Delivered". Marking a held message legitimate does not send it — only Forward does. Such entries now say "Not forwarded yet" (the detail view already said so), and entries FormGhost let through keep "Delivered".
* Fixed: in the Vault list the row actions (View, Forward, Legit, Spam, Delete) ran over the sender column on WordPress 7; they now wrap inside their column. "Legit" is no longer offered on entries already marked legitimate, "Spam" not on entries already marked as spam.
* Fixed: on Settings → Layers, jumping to a layer from the index put the layer flush against the top of the screen; it now stops with space below the admin bar.

= 1.3.0 =
* Fixed: browser auto-fill could get real people blocked. Edge and Chrome fill hidden fields named like "organization" or "alt_phone" from the visitor's saved address, and a filled honeypot held the message straight away, so real people could be ghosted while seeing a success message. The honeypot names no longer resemble anything a browser fills in, and a filled honeypot adds 30 points instead of holding the message when the timing token, the proof-of-work and the behaviour check all show a person.
* New: Needs review. A held submission that passed every bot check is flagged in the Vault, counted next to FormGhost in the admin menu, and announced by email (on by default, at most ten a day, to one or more addresses under Settings → Notifications). The email contains the sender's name, address and the start of the message. Such entries are always stored, even with Ghost Response logging off.
* Changed: rules FormGhost learns by itself add at most 40 points together, below the block threshold of 50. Two learned two-word phrases (+30 each) could hold a real enquiry on their own. Rules from your reviews and rules you add count in full.
* Fixed: a learned rule you switched off was switched back on when FormGhost learned it again. Switched-off rules now stay off, and changing a rule's weight makes it yours.
* Fixed: nothing is learned any more from a held submission that passed every bot check, and phrases containing a word of the site's own name or domain ("I came across example-shop.com", "Hi Example Shop team") are no longer learned.
* Fixed: on the Learned Rules screen every bulk action (activate, deactivate, delete) failed with "The link you followed has expired": two security fields with the same name collided.
* Fixed: in the Vault, the Filter button also submitted the bulk-action form: with entries ticked and an action chosen, pressing Filter applied the action, deleting entries included. Filters and bulk actions are now separate forms, bulk actions are sent as POST and deleting asks for confirmation.
* New Vault: search by name, email address, words from the message, domain or entry number (#123); views for form submissions, needs review, held, delivered, marked legit, marked spam and login attempts; filters for form, reason and date range; the sender and message shown in the list with a plain-language reason; "select all matching entries" to delete many at once; forward and mark legitimate in one step; a phone layout.
* New Learned Rules: filters for status and weight, clearer columns, and a form to add a rule by hand (trust a partner's domain, block an address or phrase).
* New Settings: a Notifications tab; "Add my address" next to the IP whitelist, showing the address FormGhost sees; a layer index and a setting finder on the Layers tab; a save bar that stays in view and warns about unsaved changes.
* Database: the Vault gains a needs_review column (schema 7), added automatically on update.

= 1.2.8 =
* Fixed: the identity check (one email address used under several names, or one IP sending as several people within seven days) could hold a message on its own. It now adds at most 45 points, below the block threshold of 50, so it only holds a message when another check also finds something. One person registering several colleagues with their own address, or a team sending from one office network, is no longer held for that alone.
* Fixed: marking a sender as legitimate did not protect them from the identity check. The -50 on their address only offset content-pattern points, so the next message from the same person could be held again. Senders you mark as legitimate, or allow with a manual rule, are now skipped by the identity check, and entries marked legitimate no longer count towards it.

= 1.2.7 =
* Fixed: on sites whose forms come from the theme or custom code (shown as "Custom form"), self-learning could learn the site's own page titles as spam. Such forms send the page title in a hidden field; when a bot left the message empty, FormGhost took the title for the message and learned phrases from it, which then counted against real visitors who wrote a short message from that page. Hidden fields are now recognised on these forms too: FormGhost's script reports which fields are hidden, and fields named like a page title are recognised by name.
* Self-learning now takes phrases only from the field the visitor writes the message in (Message, Nachricht, Kommentar and the like), and never from the site's own name, tagline, domain or the hidden fields of the same submission.
* Self-learning never learns the site's own email domain or addresses at it. Bots put the site's own addresses in the sender field, and the rule then counted against the site's staff.
* Web addresses learned from messages no longer keep trailing punctuation ("example.com," and "example.com" were two rules).
* After the update, a background task switches off automatic rules for the site's own email domain and for phrases made of the site's name, and merges the duplicate web-address rules. Nothing is deleted; rules you created by reviewing entries are left alone.

= 1.2.6 =
* **Privacy fix: visitor IP addresses were stored in plain text. Update now if FormGhost protects your WordPress comments.**
* **What was stored:** the visitor's IP address and browser identification (user agent), as plain text inside the stored copy of the submission in the Vault. The Vault encrypts that copy by default (not if you switched encryption off), but the key is kept in the same database. This readme said IP addresses were only ever kept as hashes; for these entries that was wrong.
* **Which sites:** every site where FormGhost protects WordPress comments ("Protect comments", on by default). WordPress hands the commenter's IP address and browser to plugins together with the comment, and FormGhost stored all of it. This affects every comment FormGhost held back and kept in the Vault, in every version from 1.0.0 to 1.2.5 (on WordPress.org: 1.2.4 and 1.2.5), and, when "Store passed submissions and learn from reviews" is on (the default), every comment it let through, since 1.1.0.
* **Also affected:** Jetpack contact forms that FormGhost checked through Jetpack's own spam check, which happens for forms that do not submit in the background (for example forms with a custom redirect), since 1.2.0. There the stored copy also held the IP address, the browser and the request headers, which can include forwarded IP addresses.
* **How long:** as long as the entry stayed in the Vault — 30 days by default, or indefinitely if you set "Retention days" to 0.
* **What 1.2.6 does:** it no longer stores IP addresses, browser identification or request headers anywhere in the Vault, only the keyed hashes this readme describes. After the update, a background task removes them from every entry already stored. It starts with the first visits after the update and needs no action from you. An entry FormGhost can no longer decrypt (for example after the site's FormGhost key was deleted) is left as it is; FormGhost cannot read it either, and the Vault's retention deletes it.
* **What 1.2.6 cannot change:** copies outside the Vault, such as database backups made before the update.
* The Vault also no longer stores FormGhost's own technical fields (the behavior score and its one-time nonce, CAPTCHA answers) or password fields. Entries saved by earlier versions are cleaned by the same background task.
* Self-learning now learns only from what a visitor actually wrote. Hidden fields that a form fills in by itself — the page address, where the visitor came from, tracking tags — no longer teach FormGhost anything and no longer add to the spam score. Before, such a field could turn a link shortener, a search engine or your own website into a penalty for real visitors.
* Self-learning never learns your own website or common search, social and link-shortener addresses (Google, Bing, X, Facebook, LinkedIn and others). Rules it had already learned for these are switched off, not deleted. Rules you created yourself by reviewing entries are left alone, and wp-admin points you to them.
* Fixed: someone who took more than 30 minutes to fill in a form could be treated as a bot. The page now renews its check in the background before it runs out.
* A learned rule now counts a hit only when it helped hold a submission, so rules that only ever match real visitors fade out as intended.
* Updates now finish their database work in the background instead of during a visitor's page load, and two visitors arriving at the same moment right after an update no longer both try to create the same table.
* For developers: new filters `formghost/fields/internal_keys`, `formghost/fields/context_keys` and `formghost/learning/never_learn_domains`; new `FormGhost_Settings::sanitize_all()`; the `formghost/vault/created` action now receives the payload as stored.

= 1.2.5 =
* **Fixed a lockout.** If you had switched on the optional CAPTCHA, FormGhost rejected every login and comment — including your own login with the correct password. The CAPTCHA answer is now read correctly on every form that uses it (login, comments, registration, password reset).
* The login page can no longer lock you out because of the CAPTCHA. If the CAPTCHA widget does not load, the CAPTCHA service cannot be reached, or your secret key is wrong, the login goes ahead (every other check still applies) and wp-admin warns you instead. A CAPTCHA answer that is actually wrong still blocks the login. Comments, registration and password reset stay strict.
* New wp-admin warning when the login CAPTCHA could not do its job in the last 7 days: how many logins, since when and why. You can dismiss it; it comes back if it happens again. Bots that skip the CAPTCHA and fail to log in are logged but do not trigger it.
* When FormGhost blocks a login, the message now says so instead of claiming your password was wrong. It reads the same whether or not the password was right, so it gives an attacker nothing.
* Reworked login rate limit: only failed logins count — by default 10 per account and 30 per IP address in 15 minutes — and a successful login clears your own mistakes, so a few typos no longer lock you out while password guessing is still stopped. A burst of simultaneous guesses can no longer slip past the limit.
* Fixed: "Skip logged-in users" no longer applies to the login, registration and password-reset forms. Before, anyone with an account — for example a subscriber on a site with open registration — could get past FormGhost there.
* Behind Cloudflare with "Trust reverse-proxy headers" off, the login rate limit now tells your visitors apart instead of treating them as one. When FormGhost sees any reverse proxy with that setting off, wp-admin suggests turning it on.
* New emergency switch: `define( 'FORMGHOST_DISABLE', true );` in wp-config.php turns FormGhost off completely without deactivating it. See the FAQ.
* Fixed: search boxes no longer get FormGhost's hidden fields, so these no longer show up in search URLs.
* Redesigned the email you get when you forward a submission from the Vault: the message first, the details below, a plain-text version, dark-mode support, and no images, links or tracking. It only says "marked legitimate" when you really marked it.
* Forwarded submissions are treated as possibly dangerous: links, web addresses and email addresses in them are broken up (for example `hxxps[:]// example[.] com`) so nothing can be clicked by accident, and FormGhost's own hidden fields are no longer shown.
* Fixed: forwarding a Vault entry from WP-CLI or a scheduled task crashed.

= 1.2.4 =
* WordPress.org review: the release package no longer contains the directory artwork folder or other non-runtime files.
* Elementor Pro preflight now respects the master switch, the contact-form scope toggle, the IP whitelist and the `formghost/skip_form` filter, so a disabled FormGhost never intercepts an Elementor submission.
* Layer 9 CAPTCHA can now actually be switched on: new per-form toggles (comments, login, registration, password reset) and the widget container is printed on those core forms automatically.
* "Store passed submissions" now controls whether passed submissions are stored in the Vault (they are still counted when off).
* Programmatic settings saves (setup wizard) no longer reset developer-managed array settings.
* Uninstall also removes the per-user "items per page" screen options.
* Honeypot CSS is delivered once: the stylesheet WordPress prints in the page head now carries a marker, and the front-end script only injects its own copy when that stylesheet is missing.
* Documentation: FAQ and Privacy sections corrected (optional external calls, what learned rules can contain); stale development notes removed from code comments.

= 1.2.3 =
* WordPress.org review: the generic interceptor now classifies admin-ajax.php / admin-post.php requests via core's own signals (`wp_doing_ajax()`, `$pagenow`) instead of matching the request URI, so non-standard wp-admin locations work.
* WordPress.org review: settings tab selection now travels inside the Settings API's nonce-verified option input; the Contact Form 7 adapter no longer falls back to reading the raw request; the Elementor recipient resolver receives the validated page ID from the adapter instead of reading the request itself.
* WordPress.org review: POST field names in the Kadence and Divi adapters are sanitized before use; nested array values in the generic interceptor are recursively sanitized before scoring.
* Hardened Vault and Rules admin write paths: explicit capability check before any action, nonce checks untangled into standalone conditions, and row actions validated against a closed list. Rules row-action nonces are now bound to the individual rule ID.
* Token issuance (timing token, proof-of-work challenge, behavior nonce, ALTCHA challenge) is now fully stateless — HMAC-signed with the expiry embedded — so anonymous bootstrap requests write nothing to the database; single-use markers are written only when a token is actually consumed by a submission.
* When "behind reverse proxy" mode is enabled, the client IP is now taken from the right-most public X-Forwarded-For hop (the one vouched for by the trusted proxy) instead of the left-most, which a client can spoof.
* Ghost Response success messages are sanitized at the source and HTML-escaped in every per-plugin JSON envelope.
* The version constant is a literal instead of re-parsing the plugin header on every request.

= 1.2.2 =
* WordPress.org review: removed leftover cleanup code from the pre-1.1 licensing system and the retired Ghost Network source; the plugin contains no gating code of any kind — every feature is free.
* Inline `<style>` blocks (dashboard widget, honeypot) moved to enqueued stylesheets.
* Translations now come from translate.wordpress.org language packs; the bundled compiled files and the `load_plugin_textdomain()` call were removed.
* readme: full "External services" disclosure for the optional Turnstile / hCaptcha verification calls and the optional DNS MX lookup.
* Improved: marking a Vault entry as spam is now decisive — the sender's exact address (+50) and hashed IP (+30) are learned alongside the content signals, so the next message from that sender is ghosted; marking as legitimate (-50 on the address) guarantees a wrongly held sender passes. Learned rules now also apply to submissions without free text.

= 1.2.1 =
* Fixed: Contact Form 7 submissions arrive through the REST API, where the timing, proof-of-work and behavior layers and the bot preflight were skipped — only the honeypot and content patterns judged them. They now run for CF7 exactly as for a page POST. New filter `formghost/engine/rest_browser_form_types` for other JS-driven REST forms.
* New: cross-layer check — a submission that passed the timing or proof-of-work check but carries no behavior data (which the tracker injects into every form) is scored as a bot (`missing_after_js`).
* New: identity-churn detection — one email address submitting under changing names, or one IP sending as many different people, within seven days. The Vault stores keyed hashes only (never the address). Toggle under Layer 5.
* New: the Learning layer now tracks the full sender address as a rule type ("Email address"), so a mailbox reused by a bot escalates as a repeat offender even on gmail.com / mail.ru.
* Improved: content patterns learn lead-generation, "website traffic", directory-listing, newsletter-bombing and advance-fee scam vocabulary; new structural signals — unsubscribe boilerplate, a third-party link carrying this site's own domain, "Hello <Domain> Owner" greetings, repeated links, emoji bait, throwaway hosting (vercel.app, netlify.app, workers.dev, …) and 19 more URL shorteners, matched by host.

= 1.2.0 =
* New: adapters for six more form plugins — Forminator, HTML Forms, Jetpack Forms, Bricks Builder, Kadence Blocks, and Divi contact forms.
* New: complete Arabic translation, with right-to-left admin support.

= 1.1.0 =
* Everything is free: all nine layers, self-learning, Learned Rules screen and Ghost Response now ship free of charge. The former activation screen was removed.
* New: "Passed" card on the dashboard — see the submissions that were NOT blocked and click through to review them.
* New: FormGhost widget on the main wp-admin dashboard with blocked/passed counts and quick links.
* New: dashboard stat cards link into the filtered Vault views.
* Improved: "Blocked today" now follows the site's timezone instead of UTC.
* Improved: ghost-response counting is exact — it no longer under-counts when ghost logging is off or entries are reviewed/purged.
* Improved: email domains in the Vault are shown in full, and the search box matches partial domains.
* Improved: the IP whitelist now honours CIDR ranges in every layer, including the engine's early skip.
* Improved: Vault filters cover every supported form plugin (Fluent, Ninja, Formidable, BuddyPress, bbPress, custom forms, password reset).
* Fixed: default settings are now merged on front-end requests too, so a partially saved settings option can never silently disable a layer.
* Fixed: the "Protect contact-form plugins" toggle now also covers custom forms caught by the generic interceptor.

= 1.0.0 =
* Initial public release.

== Upgrade Notice ==

= 1.4.0 =
Login attempts get their own short retention so they no longer push real messages out of the Vault, attack tools are recognised, and spam campaigns are learned as whole-message rules.

= 1.3.2 =
Maintenance release: text changes only, no change in behaviour.

= 1.3.1 =
The Vault no longer shows entries you only marked legitimate as "Delivered": marking does not send a message, Forward does. Also fixes overlapping row actions.

= 1.3.0 =
Stops browser auto-fill from getting real people blocked, flags held messages that look human and emails you about them (on by default), and adds Vault search by name, email or message, filters and working bulk actions.

= 1.2.8 =
Stops the identity check from holding real messages on its own, for example one person registering several colleagues or a team on one office network, and makes "Mark as legitimate" protect that sender from it.

= 1.2.7 =
Fixes self-learning on theme and custom forms: it could learn your own page titles and email domain as spam and then hold back real visitors' short messages. The update switches off the affected rules.

= 1.2.6 =
Privacy fix. Update now if FormGhost protects your WordPress comments: every earlier version stored commenters' IP addresses and browser details in plain text in the Vault, although IPs were documented as hashed only. 1.2.6 stops this and removes them from stored entries.

= 1.2.5 =
Update now if you switched on the CAPTCHA for login or comments: 1.2.4 rejects every login and comment while it is on, including your own login. Also adds the FORMGHOST_DISABLE emergency switch.

= 1.1.0 =
All features are now free. New passed-submissions view and wp-admin dashboard widget.

== External services ==

FormGhost's spam detection runs entirely on your own server. Two optional features, both **off by default**, connect to third parties. Nothing is sent unless you enable them in the settings.

**Cloudflare Turnstile** (Layer 9, optional CAPTCHA provider)
When you enable the CAPTCHA layer with Turnstile for a form, the plugin renders the widget container on that form (you add Cloudflare's widget script `https://challenges.cloudflare.com/turnstile/v0/api.js` to your site yourself — the plugin does not load any vendor script), and on every submission of that form the plugin sends the widget's response token, your Turnstile secret key and the visitor's IP address to `https://challenges.cloudflare.com/turnstile/v0/siteverify` to verify the challenge.
Terms: https://www.cloudflare.com/terms/ — Privacy: https://www.cloudflare.com/privacypolicy/

**hCaptcha** (Layer 9, optional CAPTCHA provider)
When you enable the CAPTCHA layer with hCaptcha for a form, the plugin renders the widget container on that form (you add hCaptcha's widget script `https://js.hcaptcha.com/1/api.js` to your site yourself — the plugin does not load any vendor script), and on every submission of that form the plugin sends the widget's response token, your hCaptcha secret key and the visitor's IP address to `https://api.hcaptcha.com/siteverify` to verify the challenge.
Terms: https://www.hcaptcha.com/terms — Privacy: https://www.hcaptcha.com/privacy

**ALTCHA** (Layer 9, optional CAPTCHA provider) is self-hosted: the challenge endpoint and the verification run on your server (`admin-ajax.php?action=formghost_altcha_challenge`); no data is sent anywhere. You add the ALTCHA widget script to your site yourself.

**DNS MX lookup** (Layer 6, optional "Check MX records" setting)
When enabled, the plugin asks your server's DNS resolver for the MX records of the submitted email address's domain (PHP `getmxrr`). This is a standard DNS query from your server, not a request to a third-party API; the email address itself is not transmitted, only its domain.

== Privacy ==

FormGhost is designed for privacy-conscious operators.

**Data stored on your server.**

* `{prefix}formghost_vault` — form submissions FormGhost held (and, if "Store passed submissions" is on, the ones it let through), encrypted with AES-256-GCM using the per-site secret; encryption is on by default and can be switched off in Settings. Each entry also records, unencrypted: the sender's email domain, the page the form was submitted from, the inbox it was headed to, and keyed hashes of the IP address, browser, sender address and sender name — never the address or IP itself. Configurable retention (default 30 days, at most 10,000 form submissions; login and password-reset attempts are kept 7 days, at most 2,000). Removed on uninstall.
* **Versions before 1.2.6 stored IP addresses in plain text.** Contrary to what this readme said, the stored copy of every WordPress comment FormGhost checked included the commenter's IP address and browser identification (user agent) as plain text — see the 1.2.6 changelog for which sites and since when. 1.2.6 stops storing them, and updating removes them from entries already in the Vault.
* FormGhost's own technical fields that arrive with a submission — the behavior score and its one-time nonce, and CAPTCHA response tokens — are not stored in the Vault. Entries saved by version 1.2.5 or earlier are cleaned automatically in the background after the update.
* `{prefix}formghost_learned_rules` — score modifiers learned from your reviews and from spam FormGhost caught on its own (honeypot, proof-of-work, bot checks). A rule may hold the sender email address of such a submission, or an IP address stored only as `sha256(site_secret + "|" + ip)`. Rules decay when stale and can be deleted from the Learned Rules screen. Removed on uninstall.
* `{prefix}formghost_log` — one row per decision (blocked / passed, layer, form type) for the dashboard, with a hashed IP address. Purged on the Vault retention schedule. Removed on uninstall.
* `formghost_template_samples` (option) — one sample text per learned message template (at most 500), taken only from submissions held as bots or confirmed as spam, to show on the Learned Rules screen. Removed on uninstall.
* `formghost_login_watch` (option) — daily counts of held login attempts that used an author's display name, author address name or login name, per user account, for 14 days. No IP address, no password. Removed on uninstall.
* `{prefix}formghost_counters` — failed-login counters for the login rate limit, keyed by keyed hashes of the IP address and of the account; no address, username or password is stored. Each counter lapses after its window (15 minutes by default) and old rows are purged daily. Removed on uninstall. Rate-limit counts for other forms are kept in short-lived WordPress transients under hashed keys.
* `formghost_site_secret` (option) — 64-char random secret used for IP / UA hashing, payload encryption and CSS class derivation. Removed on uninstall.

**Emails FormGhost sends.** Only to you, through your site's own mail setup (`wp_mail`): review alerts (on by default; Settings → Notifications) contain the held sender's name, email address and the first part of their message, so you can recognise a real person; and submissions you forward from the Vault yourself. Links and addresses from the submission are broken up so they cannot be clicked.

**Data sent to third parties.** None by default. Only the optional services listed under "External services" send anything, and only after you enable them.

**Cookies.** FormGhost does not set cookies.

For full source, audit, and integration docs, see https://byabdalla.com/lab/formghost.
