=== LW Simple Forms ===
Contributors: furao77
Tags: form, contact form, custom form, contact, email
Requires at least: 6.0
Tested up to: 7.1
Stable tag: 1.4.0
Requires PHP: 7.4
License: GPL2
License URI: https://www.gnu.org/licenses/gpl-2.0.html

A flexible form plugin that implements the flow of input → confirmation → completion screen, while also supporting simple one-step submissions.

== Description ==

LW Simple Forms is a versatile WordPress form plugin that allows you to create customizable forms. It supports the Japanese-style workflow of a complete form submission process:

1. Input screen - Users enter their information
2. Confirmation screen - Users review their input before submitting
3. Completion screen - Thank you message after successful submission

The plugin also supports a simpler one-step submission process when confirmation is not needed.

A sample form with all supported field types is automatically created on first activation, so you can get started right away.

**Important: Page Cache and CDN Compatibility**
Form pages must not be served from a page cache. A cached input page keeps a security token that expires after 12 to 24 hours, so submissions start failing. A cached completion page can show one visitor's submitted details to other visitors.
Since version 1.3.0, every page whose content contains a form shortcode is automatically marked as non-cacheable: the plugin sends no-cache HTTP headers and defines the `DONOTCACHEPAGE` constant, which caching plugins such as WP Super Cache, W3 Total Cache, WP Rocket, and LiteSpeed Cache respect.
Some caches ignore these signals. Examples are a CDN rule that overrides the origin's cache headers (such as a Cloudflare Edge TTL setting that ignores the Cache-Control header), nginx FastCGI Cache or Varnish when configured to ignore them, and a hosting company's own page cache. If you use one of these, please exclude the form page URLs (input, confirmation, completion, error) from caching.
If you place a form outside the page content (in a widget, a synced pattern, or a theme template), caching plugins still skip the page, but the HTTP headers may already have been sent. In that case, exclude those URLs from CDN and server-level caches manually.
After updating to 1.3.0, please purge your page cache and CDN cache once so that previously cached copies of your form pages are removed.

**CSS Styling**
This plugin does not include any frontend CSS for form display. You are expected to style the forms using your own theme's stylesheet. HTML/CSS samples are available for reference on the plugin's website.

== Development Concept ==

* **HTML-First Approach**: This plugin is designed to faithfully reproduce your designed HTML forms, confirmation screens, and error screens without forcing you to adapt to plugin limitations. The forms conform to your design, not the other way around.

* **Built for Web Professionals**: LW Simple Forms does not provide CSS or HTML templates for the frontend. It's specifically created for web designers and developers who already have designed their form screens and need a way to implement the functionality. (HTML/CSS samples are available for reference.)

* **Minimalist Philosophy**: This plugin intentionally maintains a minimalist approach, focusing on core functionality rather than excessive features. We prioritize site speed and minimal data usage to keep your websites running efficiently.

== Key Features ==

* **Complete Form Workflow**: Create forms with input screen, error screen, confirmation screen, and completion screen
* **Flexible Design**: Customize each screen with your own HTML
* **Form Validation**: Server-side and client-side validation for each input field (required fields, email format, phone number validation)
* **Email Notifications**: Send confirmation emails to both administrators and users
* **Security Features**: CSRF protection, data sanitization, input validation, and secure data handling
* **Multiple Field Types**: Support for text fields, text areas, checkboxes, radio buttons, select menus, and multi-select menus
* **Shortcode Support**: Easy integration into WordPress pages via shortcodes
* **Custom HTML**: Design your forms with complete HTML freedom
* **Multi-Form Support**: Create and manage multiple forms on a single site
* **Database Storage**: Form data is temporarily stored in the database rather than in sessions or cookies, allowing for unlimited submission data
* **JavaScript/No-JavaScript Support**: Forms work properly even in environments where JavaScript is disabled (e.g., screen readers)
* **Field Validation**: Extensive validation system with customizable error messages
* **Spam Protection Services**: Optional Google reCAPTCHA v3 or Cloudflare Turnstile (since 1.4.0), besides the honeypot and the rate limit
* **Duplicate Submission Prevention**: PRG (Post-Redirect-Get) pattern prevents duplicate form submissions on page reload
* **Security Measures**: Protection against common vulnerabilities including CSRF attacks and header injection

== Installation ==

1. Upload the `lw-simple-forms` folder to the `/wp-content/plugins/` directory
2. Activate the plugin through the 'Plugins' menu in WordPress
3. Create a new form from the 'Forms' menu item
4. Configure the form settings including HTML templates and email notifications
5. Create pages for your form screens (input, confirmation, completion)
6. Add the appropriate shortcodes to your pages:
   * `[lwsf_input id="123"]` - For the input screen
   * `[lwsf_confirm id="123"]` - For the confirmation screen (optional - leave empty to skip confirmation)
   * `[lwsf_complete id="123"]` - For the completion screen (optional - displayed on the input screen URL if omitted)
   * `[lwsf_error id="123"]` - For the error screen (optional - errors are displayed on the input screen if omitted)

== Usage Guide ==

1. Design and create the HTML for input screen, confirmation screen, and completion screen
2. Register each screen as a WordPress page
3. Create a new form from "Forms" in the WordPress admin panel
4. Configure each section. You can configure the following:
   * HTML for each screen (input, confirmation, completion)
   * URL for each screen
   * Administrator email settings
   * User email settings
   * Validation settings (required fields, email format, phone number validation)
5. Use the "Parse HTML" button to automatically extract form fields from your input screen HTML
6. Add the appropriate shortcodes to your pages
7. Test the form operation before going live

Note: This plugin is designed to give you maximum freedom in writing form HTML, so you need to write the HTML for your form pages yourself. The plugin does not provide CSS or images for form display. HTML samples are available for reference.

== Supported Input Fields ==

* `<input type="text">`
* `<input type="tel">`
* `<input type="email">`
* `<input type="radio">`
* `<input type="checkbox">`
* `<textarea>`
* `<select>`
* `<select multiple>`

== Form Screen Placeholders ==

**Input Screen:**
* Use `[lwsf_value_fieldname]` to display previously entered values
* Use `[lwsf_error_fieldname]` to display validation error messages
* Use `[lwsf_send]` for the submit button
* Optional: `[lwsf_turnstile]` puts Cloudflare Turnstile's checkbox (shown only when Cloudflare asks for one) where you write it; without it, it goes just before the submit button (since 1.4.0)

**Confirmation Screen:**
* Use `[lwsf_confirm_fieldname]` to display submitted values
* Use `[lwsf_back]` for the back button
* Use `[lwsf_send]` for the submit button

The button tags `[lwsf_send]` and `[lwsf_back]` take an optional `class` attribute so that your theme's button styles apply, for example `[lwsf_send class="button primary"]` (since 1.3.4).

Radio buttons, checkboxes and select menus keep their selected state by themselves: do not write `[lwsf_value_fieldname]` inside their tags (since 1.3.5 such a placeholder is removed on saving, with a note).

**Completion Screen:**
* Use `[lwsf_field_fieldname]` to display submitted values

== Email Settings ==

**Available placeholders for email templates:**
* `[lwsf_field_fieldname]` - Display submitted form data
* `[lwsf_site_admin_email]` - Display site admin email
* `[lwsf_site_name]` - Display site name
* `[lwsf_site_home_url]` - Display site URL

== Additional Information ==

* For items that allow multiple selections (`<input type="checkbox">`, `<select multiple>`), you need to add [] to the name attribute.
  Example:
  - For checkboxes: `<input type="checkbox" name="services[]" value="ServiceA"> Service A <input type="checkbox" name="services[]" value="ServiceB"> Service B`
  - For select multiple:
  ```
  <select id="products" name="products[]" multiple size="4">
    <option value="ProductA">Product A</option>
    <option value="ProductB">Product B</option>
  </select>
  ```

* Works in environments where JavaScript is disabled, such as screen readers
* The outputted source code uses entity references for security measures and stable operation
* Includes Japanese language files (UTF-8 only)
* Avoids using reserved WordPress query variable names for form fields to prevent conflicts
* **PHP Compatibility**: Requires PHP 7.4 or higher. Tested on PHP 7.4 through PHP 8.5.

== Data Storage and Security ==

* Form submissions are temporarily stored in the WordPress database (prefix_lwsf_form_data table)
* Data is automatically cleaned up after 1 hour
* All user inputs are sanitized before processing
* CSRF protection is implemented on all form submissions
* Email headers are validated to prevent header injection

== Why This Plugin Was Created ==

This plugin was created because MW WP Form, which had been used for client work for many years, ended development. There was a need for a form plugin with confirmation screens that operated cleanly. The plugin was created primarily for web development work, and we thought many web development companies might have similar needs.

== Frequently Asked Questions ==

= How do I create a form? =
Navigate to LW Simple Forms → Add New Form in your WordPress admin, configure the form settings, and save. You'll then need to add the appropriate shortcodes to your pages.

= How do I set required fields? =
After creating your form HTML, use the "Parse HTML" button to detect form fields automatically. Then you can mark fields as required in the "Validation Settings" section.

= Can I skip the confirmation screen? =
Yes. Simply leave the "Confirmation Screen URL" field empty, and the form will skip straight to completion after submission.

= How long is submission data kept in the system? =
Data is temporarily stored in the database for one hour. After this period, the data is automatically deleted.

= Will the form work if a user has JavaScript disabled? =
Yes. The plugin is designed to work both with and without JavaScript. All validations and form processing have server-side fallbacks. With reCAPTCHA or Turnstile on, such a submission cannot be checked by the service (both need JavaScript); since 1.4.0 it is accepted if the form was opened at least 3 seconds and at most 24 hours before, and at most 2 times per hour from the same address to the same form (these are the defaults; developers can change them with the filter `lwsf_unverified_limits`), and the admin email starts with "[Unverified]".

= Which spam protection service should I use? =
Cloudflare Turnstile is free without a monthly limit, and your site does not need to use Cloudflare. Google reCAPTCHA v3 is free up to 10,000 checks a month per Google Cloud organization; over that, Google either bills you or answers "success" for every token, which the plugin then treats as "not checked" and reports in the admin screens. Choose the service in Settings > Spam Protection Service; developers can change the conditions for unverified submissions with the filters `lwsf_unverified_limits` and `lwsf_accept_unverified`.

= Does the form work if my host or a security plugin blocks the REST API? =
Yes. With JavaScript, the form checks the input through the REST API (`/wp-json/lw-simple-forms/v1/`). Since 1.3.4, if that request is blocked or cannot connect, the form goes on with the submission that works without JavaScript, without showing an error. When this happens, the plugin's admin screens show a notice to administrators. To use the faster JavaScript checks, allow this route in your host or security plugin settings.

= Can the Return-Path of the emails match the sender address? =
Yes. Since 1.3.4, turn on "Return-Path" in the form settings. The envelope sender of the admin email and the auto-reply is then their sender email address, if that address is on the site's own domain. This can help SPF pass for that domain when its SPF record allows your server. If an SMTP plugin or a mail service sets the Return-Path itself, its setting is used.

= Do I need to exclude my form pages from caching? =
Since version 1.3.0, pages whose content contains a form shortcode are automatically excluded from caching by most caching plugins and by CDNs that follow the page's cache headers. You only need to exclude the form page URLs manually if your cache ignores these headers (for example a CDN rule that overrides the origin's cache headers, a server-level cache configured to ignore them, or a hosting company's own page cache), or if the form is placed outside the page content. Note that a form placed in a sitewide widget or footer turns off page caching on every page where it appears. See "Page Cache and CDN Compatibility" in the Description.

= How do I show different error messages for different validation types? =
Currently, the plugin uses standard error messages. Future versions may support customizable validation messages.

= Can I use special characters in my forms? =
Yes. The plugin properly handles special characters and different encodings, but it's recommended to use UTF-8 encoding.

= How do I send confirmation emails to users? =
Enter a field name (usually an email field) in the "Recipient Email Address Field" setting under "User Auto-send Email Settings".
e.g. If the name attribute of the email field is set to "your-email", you should write it as [lwsf_field_your-email].

== Screenshots ==

1. Example of an Edit Form screen.
2. Example of an Edit Form screen in Japanese.
3. Example of embedding the form input screen shortcode in a static page.
4. Example of the form input screen on the front end (CSS not applied).
5. Example of embedding the input error screen shortcode in a static page.
6. Example of the input error screen on the front end (CSS not applied).
7. Example of embedding the confirmation screen shortcode in a static page.
8. Example of the form input screen on the front end (with basic CSS applied).
9. Example of embedding the submission completion screen shortcode in a static page.
10. Example of the submission completion screen on the front end (CSS not applied).

== Future Implementation Plans ==

* The ability to change validation error messages
* PHP-based validation hooks that operate before and after form submission
* Additional validation types (URL, numeric values, custom regex patterns)
* Hooks to trigger at important timings such as just before and just after email sending
* File upload handling

== Changelog ==

= 1.4.0 =

**New Features:**

* Cloudflare Turnstile as a second spam protection service besides Google reCAPTCHA v3 (Settings > Spam Protection Service). It is free and the site does not need to use Cloudflare. The form shows Turnstile's checkbox only when Cloudflare asks for one, just before the send button or where `[lwsf_turnstile]` is written.

**Changes:**

* Submissions without a token (JavaScript off, the service's script blocked, a direct POST) were accepted without any check while spam protection was on, so bots that do not run JavaScript were only stopped by the honeypot and the rate limit. They are now accepted only if the form was opened at least 3 seconds and at most 24 hours before (a signed time in the form) and at most 2 per hour from the same address to the same form (defaults), and the admin email starts with "[Unverified]" with the reason at the end. The same holds when the service could not check a token (not reachable, an unreadable answer, a wrong secret key). Developers can change this with the filters `lwsf_unverified_limits` and `lwsf_accept_unverified`.
* reCAPTCHA over its free quota: Google answers "success" for every token, so all submissions passed. Such answers are now treated as not checked, and a notice in the admin screens tells the site owner.
* When reCAPTCHA or Turnstile does not answer within 10 seconds, the token is asked for once more; after that the form is sent without a token and handled as above (no more "reCAPTCHA verification failed" for a slow first load). No token is fetched any more when going on to the confirmation screen.
* Settings: the secret key is checked with the service after saving, and the settings screen shows how many submissions were accepted without verification or refused in the last 7 days.
* Admin notices when the chosen service lacks a key (no protection at all), when Cloudflare's test keys are set (depending on the key, every check passes, fails or asks to click), and when the reCAPTCHA switch of an older version disagrees with the chosen service.
* Without JavaScript, a submission refused by these checks shows the form again with the typed values and the message (instead of the message alone).
* Forms placed outside the page content (a widget, a template, a synced pattern) now get the form script and the service's script too.

**Bug Fixes:**

* Admin notice "the REST API gave an unexpected answer when checked from this server": on a site behind a Cloudflare rule that blocks requests from programs (while browsers pass), the check got Cloudflare's own block page and showed the notice although visitors could send the form. A Cloudflare block page (403 or 429) is now treated like a Cloudflare challenge: it cannot be judged and is not reported. Cloudflare's error pages for a failing server (520 and above) are still reported. A visitor who is really blocked is still reported by the other notice (the visitor's browser could not use the REST API).
* The sample form created on first activation had `[lwsf_value_…]` inside its radio button, checkbox and option tags, so saving it showed "Removed unsafe elements from content". The sample no longer has them. Such a placeholder written directly inside a tag (not inside an attribute value) is now removed on saving with a note instead of a warning: it is not needed, because the selected state is restored automatically (in a text field, write `value="[lwsf_value_…]"`). A real unsafe element is still reported as before.

= 1.3.4 =

**Security:**

* Completion screen: submitted values were output without HTML escaping. Someone could store an entry containing HTML or a script and send another person a link to its completion screen (within about an hour), where it would run in that person's browser. On a form whose fields were never registered with "Parse HTML", any value was affected; on other forms, values placed in an attribute (for example `href="mailto:[lwsf_field_email]"`). The values are now HTML-escaped, and values in a link attribute (`href`, `src`, `action` and similar) are escaped as URLs.

**Bug Fixes:**

* Sites where the REST API is blocked (by the host, a firewall or a security plugin): depending on how it was blocked, the form showed "A system error occurred" for a moment and then submitted without JavaScript (nothing was submitted on a form without an error page), or showed the REST API's error message and submitted nothing. When the REST API does not answer with this plugin's own response, the form now goes on with the submission that works without JavaScript, without showing an error. A server error or gateway timeout (500, 502, 504, 520, 524) is shown as an error instead, because the server may already have handled the request.
* An email that could not be sent (`wp_mail()` returned false) was still treated as sent: the visitor saw the completion screen, and (while "Send Only Once" is on, the default) the same entry could not be sent again for about an hour. The visitor now sees "The email could not be sent. Please try again later." and can send it again. Only the admin email is checked; the auto-reply is sent only after the admin email went out. Note: a site that stops mail on purpose with a `pre_wp_mail` filter returning false (for example on a staging site) now shows this message. To keep the earlier behaviour, return false from the new `lwsf_check_mail_result` filter.
* Settings check in the form editor: a URL without a page on this site, an unpublished page, and a shortcode with another form's id (for example `id="56"` for form 5) are now reported, each with its own message. An escaped `[[lwsf_complete id="5"]]` does not count. The form list also shows a warning next to a URL whose page is missing, unpublished or without the shortcode. Pages built with a page builder, a synced pattern or a template may show the shortcode warning although the form works.

**Improvements:**

* Rate limit: only submissions that pass validation are counted (on forms with a confirmation screen, each time the confirmation screen is reached), so a visitor correcting input errors is no longer locked out. No more than the set number get through, also when many are sent at the same time. A submission whose email could not be sent does not count.
* Admin notices when the REST API is not available to visitors (shown to administrators on the plugin's screens): when a visitor's browser could not use the REST API (shown for 7 days after the last time), and when a request of the same kind, made from the server without logging in, gets an unexpected answer such as a 403 (checked every 12 hours, and every hour while the notice is shown; no answer, an HTTP authentication prompt or a Cloudflare challenge is not reported).
* New form setting "Return-Path" (off by default): the envelope sender of the admin email and the auto-reply is set to their sender email address, but only when that address is on the site's own domain (a sender such as the visitor's address is left alone). This can help SPF pass for that domain when its SPF record allows your server. If an SMTP plugin or a mail service sets the Return-Path itself, its setting is used.
* `[lwsf_send]` and `[lwsf_back]` take an optional `class` attribute, for example `[lwsf_send class="button primary"]`, so that your theme's button styles apply. Without it the output is the same as before.

= 1.3.3 =

**Bug Fixes:**

* Forms with a confirmation screen and a completion page, JavaScript disabled: an entry with the same values as an earlier one (from anyone; on a form whose fields were never registered with "Parse HTML", any entry) sent within about an hour went back to the input screen, and no email was sent. The completion screen is now shown, and the entry is emailed unless the same visitor already sent it (see Improvements).
* Forms with a confirmation screen, JavaScript enabled: two visitors who sent the same values in the registered fields at the same time (or any values, on a form whose fields were never registered) could get each other's entry. One of them saw the other's data on the completion screen, and one email was never sent. Each submission now has its own random key, which is passed from the input screen through the confirmation screen to the completion screen.
* Security: a POST made directly to the completion page (or, on a form without a completion page, to the input page) sent email without validation, rate limit, honeypot check or the confirmation screen, including the auto-reply to any address in the POST. Many such POSTs sent at the same time with one key could also send the same entry several times. The completion screen now sends only a stored entry that passed the input screen's checks (and, on forms with a confirmation screen, was shown on it), and sends each entry once. The error page, which receives the input screen's submissions when JavaScript is disabled, now also runs the rate limit, honeypot and reCAPTCHA checks.
* Rate limit: the limit refused one submission too early (with the default of 5, the 5th submission within the period was refused). The set number of submissions is now allowed. Attempts are counted, not only sent emails (for example, with JavaScript enabled, a submission that fails validation also counts).

**Improvements:**

* Duplicate prevention now also covers forms with a confirmation screen: the same entry (every value the same, from the same IP address) sent again within about an hour is emailed once, and the completion screen is shown each time. Different visitors who share an IP address (an office network or a mobile carrier) and send identical values count as one entry.
* New setting: LW Simple Forms > Settings > Duplicate Submissions > "Send Only Once" turns duplicate prevention on or off for all forms (on by default, as before). The `lwsf_prevent_duplicate_send` filter still changes it for a single form; its default value now comes from this setting.

**Note:** A visitor who leaves the confirmation screen open for longer than the storage period (about 1 hour) and then presses Send is now asked to start again (before, the entry was sent). Without JavaScript, a visitor whose confirmation screen was opened before you updated to 1.3.3 is also asked to start again.

= 1.3.2 =

**Bug Fixes:**

* Form fields wider than the screen: a field written with a fixed size in the form HTML (for example `<input size="60">` or `<textarea cols="80">`) stuck out of a phone screen and made the whole page scroll sideways. Text fields, select boxes and text areas in the form are now limited to the screen width (`max-width: 100%` with `box-sizing: border-box`). The rule has zero specificity, so your theme's own rules override it. Checkboxes and radio buttons are not changed. If your theme sets a field's width or height in CSS without its own `box-sizing` rule, that field may become smaller by its padding and border. To turn the rule off, dequeue the `lwsf-form` style at priority 11 or later: `add_action( 'wp_enqueue_scripts', function () { wp_dequeue_style( 'lwsf-form' ); }, 20 );`

= 1.3.1 =

**Bug Fixes:**

* Duplicate emails on forms without a confirmation screen: submitting the same entry again (for example from another tab or after going back) sent the notification emails again. The same entry is now sent only once while its data is stored (about 1 hour after the last submission), and the completion screen is shown again instead. An entry counts as the same only when every submitted value matches and it comes from the same IP address. Visitors who share an IP address (an office network or a mobile carrier) and send identical values within that time are treated as one entry; for forms where that is common, such as a yes/no form, turn this off with the new `lwsf_prevent_duplicate_send` filter.
* Duplicate emails with JavaScript disabled: one submission sent the emails twice on forms without a confirmation screen that have a completion page or an error page. Each submission now sends them once.
* reCAPTCHA: if the reCAPTCHA script never answered (for example when Google is slow or blocked by the network or an ad blocker), the submit button stayed disabled and the form appeared frozen. The plugin now waits at most 10 seconds, then shows an error message and re-enables the button.
* The confirmation, completion and error pages broke REST API responses (for example `/wp-json/wp/v2/pages/<id>`, the block editor, or headless use), because the redirect script for direct access was printed outside the page content. It is now part of the shortcode output, and is left out in REST API and admin requests. The redirect also works when the input page URL contains more than one query parameter.
* Bundled Japanese translation: 7 admin screen strings containing double quotes (for example the note about the `[lwsf_send]` button) were not translated because of an escaping error in the bundled .mo file. Translations from translate.wordpress.org were not affected.

**Improvements:**

* The `lwsf_prevent_page_cache` filter now also receives the form ID and the page ID: `apply_filters( 'lwsf_prevent_page_cache', true, $form_id, $post_id )`. Use them to turn off the automatic cache exclusion for a single form.
* New `lwsf_prevent_duplicate_send` filter: `apply_filters( 'lwsf_prevent_duplicate_send', true, $form_id )`. Return false to send the emails for every submission (once per submission), as before 1.3.1.
* Tested with WordPress 7.1.3 and PHP 7.4 to 8.5.

= 1.3.0 =

**Improvements:**

* Pages that contain a form shortcode are now automatically excluded from page caching. The plugin sends no-cache HTTP headers and defines the `DONOTCACHEPAGE` constant (respected by WP Super Cache, W3 Total Cache, WP Rocket, LiteSpeed Cache, and others), and also notifies LiteSpeed Cache through its API. This prevents submissions from failing because a cached input page kept an expired security token, and stops cached completion pages from showing one visitor's data to others.
* The `no-store` directive is left out of the no-cache headers so that browsers can still restore typed values when a visitor presses Back on the confirmation screen. `private` already keeps CDNs and other shared caches from storing the page. For logged-in users, `no-store` is kept as WordPress sends it.
* New `lwsf_prevent_page_cache` filter. Return false to turn off the automatic cache exclusion.
* After updating, purge your page cache and CDN cache once to remove copies of form pages that were cached before.

**Bug Fixes:**

* When a visitor pressed the browser's Back button on the confirmation screen and the input screen was restored from the browser's back/forward cache, the submit button stayed disabled and the form could not be submitted again. The buttons are now re-enabled when the page is restored. If the form had already been sent (forms without a confirmation screen), the page is reloaded instead, so the same entry is not sent twice by accident.

**Documentation:**

* Rewrote the cache compatibility note: explains why form pages must not be cached and which caches still need manual exclusion
* Fixed a non-existent plugin name ("WP Total Cache") in the cache compatibility note
* Added an FAQ entry about excluding form pages from caching
* Tested up to WordPress 7.1

= 1.2.0 =

**New Features:**

* IP-based rate limiting - Prevents the same IP address from submitting forms excessively within a configurable time window. Default: 5 submissions per hour. Configurable in Settings.
* Honeypot field - An invisible form field that catches automated bots. Bots that fill all fields are silently rejected. Enabled by default, can be toggled in Settings.
* Confirmation screen enforcement - When a confirmation screen is configured, the server now verifies that the confirmation screen was actually displayed before allowing completion. Prevents bots from bypassing the confirmation step.

**Improvements:**

* reCAPTCHA v3 score threshold increased from 0.3 to 0.5 (Google's recommended default) for better spam detection
* New "Spam Protection" settings section in the admin panel for configuring rate limiting and honeypot options
* Rate limit records are automatically cleaned up after 24 hours

**Security:**

* Added protection against automated SQL injection scanning attacks
* Bots that trigger the honeypot receive a fake success response to prevent detection of the security measure

= 1.1.1 =

**New Features:**

* Sample form with all supported field types (text, tel, email, radio, checkbox, textarea, select, select multiple) is automatically created on first activation for easy reference

**Documentation:**

* Added cache plugin/CDN compatibility warning
* Added CSS styling note (no frontend CSS included)
* Marked optional shortcodes in Installation section
* Clarified PHP version compatibility (tested on PHP 8.3)
* Updated WordPress compatibility to 6.9

= 1.1.0 =

**New Features:**

* reCAPTCHA v3 integration - Configure Site Key and Secret Key in LW Simple Forms > Settings. When enabled, reCAPTCHA tokens are automatically generated and verified on form submission. Fails open on API communication errors to avoid blocking legitimate users. Score threshold: 0.5.
* PRG (Post-Redirect-Get) pattern for duplicate submission prevention - After form completion, a cookie-based session key is stored and the browser is redirected to a clean URL via 302 redirect. The completion screen is displayed once, and page reload redirects back to the input page.

**Improvements:**

* WordPress reserved word validation now blocks saving (previously only warned) when form field names use reserved query variables (e.g., `name`, `p`, `s`, `page`). Case-sensitive comparison: `Name` is allowed, `name` is blocked.
* Extended wp_kses allowed HTML tags to include `<form>`, `<button>`, and `<textarea>` with their common attributes. This ensures confirmation screen buttons and form elements retain their HTML attributes (e.g., class, id).
* Frontend CSS externalized - The plugin no longer outputs inline CSS. Error messages and button styles should be defined in your site's stylesheet.
* POST data now properly handled with `wp_unslash()` to prevent double-escaping issues caused by WordPress `wp_magic_quotes()` (e.g., `I'm` no longer becomes `I\'m`).
* Improved HTML sanitization warnings - Normalized comparison to avoid false positives from `wp_kses` removing trailing semicolons in style attributes. Warning messages now show the actual changed lines instead of a generic message.
* Updated admin notes to clarify that style attributes are allowed (only script tags are blocked for security).
* Confirmation button default label changed from "Confirm Input" to "Confirm".

**Bug Fixes:**

* Fixed direct submission mode (without confirmation screen) - Replaced query parameter approach (`?lwsf_complete=1&key=...`) which caused 404 errors due to WordPress interpreting query parameters. Email is now sent within the REST API call, and a flag is returned to JavaScript.
* Fixed reCAPTCHA token regeneration - Tokens are now regenerated before the final form submission since reCAPTCHA tokens can only be used once.
* Documented incompatibility with async-javascript plugin - The plugin adding `async` attribute to `lwsf.js` breaks form functionality. Workaround: exclude `lwsf-form-handler` in the async-javascript plugin settings.

= 1.0.0 =
* Initial release

== Upgrade Notice ==

= 1.4.0 =
Adds Cloudflare Turnstile. With spam protection on, submissions without a verified token are now accepted only under conditions and marked "[Unverified]", and reCAPTCHA over its free quota no longer lets everything through. Check Settings > Spam Protection Service after updating.

= 1.3.4 =
Security fix: values on the completion screen are now escaped. Forms keep working when the REST API is blocked, and the admin screens tell you. If the admin email cannot be sent, visitors now see an error and can try again. Send a test entry after updating.

= 1.3.3 =
Fixes forms with a confirmation screen showing one visitor's entry to another, and closes a way to send email without the form's checks. Strongly recommended for all users.

= 1.3.2 =
Form fields no longer stick out of the screen on phones. Recommended for all users.

= 1.3.1 =
Prevents duplicate emails (when the same entry is submitted again, and with JavaScript disabled), and stops the form from freezing when reCAPTCHA does not respond. Recommended for all users.

= 1.3.0 =
Form pages are now automatically excluded from page caching. Recommended for all sites that use a caching plugin or CDN. After updating, purge your page cache and CDN cache once.

= 1.2.0 =
Added IP rate limiting, honeypot bot detection, and confirmation screen enforcement. Strongly recommended update for spam protection.

= 1.1.1 =
Sample form on first install, cache compatibility warning, and documentation improvements. Recommended for new installations.

= 1.1.0 =
Added reCAPTCHA v3 spam protection, PRG pattern for duplicate submission prevention, and several bug fixes. Recommended update for all users.

= 1.0.0 =
Initial release

== External services ==

This plugin can connect to one spam protection service, chosen in Settings > Spam Protection Service. Nothing is sent while "None" is chosen.

**Google reCAPTCHA v3** - used to tell people from bots. On pages with a form, the visitor's browser loads https://www.google.com/recaptcha/api.js, which sends Google information about the browser and the visit. When the form is sent, the plugin sends the token from the browser and the secret key to https://www.google.com/recaptcha/api/siteverify. Terms: https://policies.google.com/terms - Privacy: https://policies.google.com/privacy

**Cloudflare Turnstile** - used to tell people from bots. On pages with a form, the visitor's browser loads https://challenges.cloudflare.com/turnstile/v0/api.js, which sends Cloudflare information about the browser and the visit (Cloudflare says Turnstile does not read the form's contents). When the form is sent, the plugin sends the token and the secret key to https://challenges.cloudflare.com/turnstile/v0/siteverify. Terms: https://www.cloudflare.com/website-terms/ - Privacy: https://www.cloudflare.com/privacypolicy/ - Turnstile privacy addendum: https://www.cloudflare.com/turnstile-privacy-policy/

When you save the settings, the plugin also sends the secret key of the chosen service (reCAPTCHA or Turnstile) once, with a dummy token, to that service's siteverify URL above, to tell you whether the key is accepted.

Mention the service you use in your site's privacy policy (Cloudflare asks for this when the Invisible widget type is used). A Content Security Policy must allow the service's domain for scripts and frames.

== Privacy Policy ==

This plugin stores form submission data in the WordPress database (prefix_lwsf_form_data table) for the purpose of displaying confirmation and completion screens. Data is automatically deleted after 1 hour.

To recognise the same entry being submitted again, a hash of the submitted values and the visitor's IP address is used as the key of a stored entry (forms without a confirmation screen) or of a separate record of sent entries (forms with a confirmation screen, while "Send Only Once" is on). The IP address itself is not stored. Rate limiting stores IP addresses separately and deletes them after 24 hours.

When enabled, user email addresses may be used to send confirmation emails to form submitters.

When reCAPTCHA v3 or Turnstile is enabled, see "External services" above. The plugin itself sends the service only the token from the visitor's browser and the secret key; no form contents and no IP address. No other data is shared with external services.

With spam protection on, the form contains the time it was shown, signed with a key of the site (no personal data). Submissions that the service did not confirm are counted per IP address for the stricter limit; these records are deleted after 24 hours like the other rate-limit records.