=== EffortLess Multisite Form Creator ===
Contributors: domclic
Tags: form, contact form, gutenberg, ajax, multisite
Requires at least: 5.8
Tested up to: 7.1
Requires PHP: 7.4
Stable tag: 7.5.60
License: GPL-2.0-or-later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Donate link: https://id7.dev/donate/

AJAX-powered Gutenberg contact forms. Display inline or as popups. Supports multisite, SMTP, captcha, file uploads, and CPT submission storage.

== Description ==

**EffortLess Multisite Form Creator** lets you build forms in the block editor, as a "Form" post whose content is your form's HTML. Each form is placed on a page with a shortcode or the ELMFC Form block, inline or as a popup. Submissions are sent by AJAX, emailed to the addresses you choose, and can optionally be stored as posts of a custom post type.

= Key Features =

* **Gutenberg-Ready** - Build forms directly in the block editor with HTML
* **AJAX Submission** - No page refresh needed
* **Popup or Inline** - Display forms as popups or embedded in your content
* **Fieldset Support** - Full support for fieldset and legend elements with default styling
* **Flexible Width Options** - Container width (default), narrow, default, or wide layouts
* **Email Notifications** - Send to the site admin and/or a chosen user; optionally a copy to the submitter
* **SMTP Support** - Optional SMTP configuration for reliable email delivery
* **Per-Form Customization** - Each form can have unique messages and settings
* **Theme Button Support** - Uses your theme's button styles by default (optional custom styling)
* **Fully Translatable** - All messages configurable per form (no hardcoded text)
* **Multisite Compatible** - Forms can be rendered and submitted across the sites of a multisite network
* **Responsive Design** - Mobile-friendly popups and forms with optimized spacing
* **Simple Math Captcha** - Optional spam protection; the question is loaded when the form is shown, so it keeps working on cached pages, and forms sent less than 3 seconds after loading are rejected
* **Enhanced Radio & Checkbox** - Card-style radio buttons and highlighted terms checkbox
* **Save Submissions** - Optional storage as custom post type with custom slug
* **Configurable Retention** - Optionally auto-delete stored submissions after a set number of days
* **Privacy Tools Integration** - Submissions are included in WordPress's built-in personal data export/erase tools
* **Page Builder Compatible** - Works with Elementor, Beaver Builder, Divi, and Gutenberg blocks
* **No Dependencies** - Uses native WordPress functions and jQuery

= Use Cases =

* Contact forms
* Support request forms
* Quote request forms
* Newsletter signup forms (with consent & storage)
* Subscription forms with plan selection
* Feedback forms
* Registration forms

= Form Width Options =

Choose how your inline forms should display:

* **Container Width (Default)** - Uses 100% of the parent container width
* **Narrow** - Maximum 450px width, centered
* **Default** - Maximum 720px width, centered  
* **Wide** - Maximum 1080px width, centered

= Email Configuration =

* Send to site admin email
* Send to specific WordPress user
* Send a copy to the form submitter (off by default; requires an email field)
* Optional SMTP configuration for better deliverability
* Customizable email subjects and messages per form

= Styling Features =

* Card-style radio button options with hover effects
* Highlighted terms and conditions checkbox container
* Proper fieldset and legend styling with nested support
* Consistent spacing throughout forms
* Custom or theme button styling options
* Visual width preview in admin settings

== Installation ==

1. Upload the plugin files to `/wp-content/plugins/effortless-multisite-form-creator/`
2. Activate the plugin through the 'Plugins' menu in WordPress
3. Go to Forms > Add New Form
4. Build your form using HTML in the Gutenberg editor
5. Configure settings in the sidebar meta boxes
6. Use shortcode `[elmfc id="123"]` or `[elmfc name="your-form-slug"]`
7. Add `popup="true"` to display as popup: `[elmfc id="123" popup="true"]`

== Frequently Asked Questions ==

= How do I create a form? =

1. Go to Forms > Add New Form
2. Add your form HTML in the editor (use fieldset, input, textarea, select, etc.)
3. Configure email settings, captcha, and appearance in the sidebar
4. Publish and copy the shortcode

= How do I add fieldsets? =

Use standard HTML fieldset and legend tags:
```html
<fieldset>
    <legend>Section Title</legend>
    <label>Field Name</label>
    <input type="text" name="field_name" required>
</fieldset>
```

= How do I write my own label/text around the captcha? =

The "Enable simple captcha" checkbox auto-inserts a boxed question with fixed
label text. If you'd rather write that text yourself — and have it translate
like the rest of your form content instead of through a separate settings
field — place the `[elmfc_captcha]` shortcode directly in the form's HTML
instead. It renders only the question (e.g. "5 + 6 = ?"), the answer input,
and a hidden verification token; everything around it is plain HTML you
control:

```html
<label>Security check: [elmfc_captcha]</label>
```

The shortcode already includes its own answer input and hidden token, so
nothing else is needed — just wrap it in whatever label/wording you want.
When `[elmfc_captcha]` is present in the content, the auto-inserted box is
skipped automatically so you never get two captchas on the same form.

= How do I build a form with a Custom HTML block? =

1. Go to Forms > Add New Form and give the form a title (it is shown above the form).
2. Add a Custom HTML block and write the fields. Every field needs a `name` attribute; that name is the label used in the email and in stored submissions.
3. Add a submit button: `<button type="submit">Send</button>`.
4. Optionally put `[elmfc_captcha]` where the captcha should appear; a form containing the tag always shows and checks the captcha, even with "Enable simple captcha" unticked. Without the tag, and with "Enable simple captcha" ticked, the captcha is placed just before the submit button.
5. Publish, then add the form to a page with `[elmfc id="123"]` or the ELMFC Form block.

Example:

```html
<p><label>Name <input type="text" name="name" required></label></p>
<p><label>Email <input type="email" name="email" required></label></p>
<p><label>Message <textarea name="message" rows="5" required></textarea></label></p>
<p><label>Security check: [elmfc_captcha]</label></p>
<button type="submit">Send</button>
```

Line breaks typed in a textarea are kept in the email and the stored submission.

= Can I put my own CSS and JavaScript in a form? =

Yes. In the same Custom HTML block, add a `<style>` element and a `<script>` element. This works when the form's author has the `unfiltered_html` capability: administrators on a single-site install, and super admins on multisite. For authors without that capability, `<script>` and `<style>` are removed from the form when it is displayed.

= How do I style the form myself, without fighting the plugin's default CSS? =

Go to Forms > Settings and enable "Disable the plugin's built-in CSS". This
stops `assets/css/elmfc.css` from loading site-wide, so every form (fields,
buttons, the popup, the CAPTCHA) falls back to your theme's default
appearance and you write 100% of the styling yourself. The plugin's
JavaScript — AJAX submission, popup open/close, validation, multi-step
navigation — keeps working normally either way; only the visual styling is
affected.

= How do I display the form as a popup? =

Add `popup="true"` to your shortcode:
`[elmfc id="123" popup="true"]`

= How do I make the form full width? =

By default, forms use their container's full width. To explicitly control width:

1. Edit your form
2. Go to Appearance Settings sidebar
3. Select "Container" for full width, or choose Narrow/Default/Wide for specific max-widths

= Can I change the button texts and messages of one form? =

Yes. Add hidden fields to the form's HTML; each one overrides the default text for that form only:

* `elmfc_submit_text` - submit button text (also used to restore the button after an error)
* `elmfc_sent_text` - button text after a successful submission
* `elmfc_sending_message` - button text while the form is being sent
* `elmfc_success_message` - message shown after a successful submission
* `elmfc_error_message` - message shown when the email could not be sent
* `elmfc_validation_message` - message shown when a required field is empty
* `elmfc_captcha_error` - message shown after a wrong captcha answer
* `elmfc_captcha_question` - label of the automatically inserted captcha

```html
<input type="hidden" name="elmfc_submit_text" value="Send Message">
<input type="hidden" name="elmfc_success_message" value="Thanks for contacting us!">
```

The notification email's subject and introduction (`_elmfc_admin_subject`, `_elmfc_admin_message`, `_elmfc_user_subject`, `_elmfc_user_message`) cannot be set by hidden fields, so a visitor cannot change them. Set them through Export / Import Settings on the form. The redirect after submission is set in the Appearance Settings box.

= Which CSS classes give fields the built-in styling? =

* `radio-label` on a `<label>` wrapping a radio button: card-style option.
* `checkbox-label` on a `<label>` wrapping a checkbox, inside a `<div class="terms-container">`: highlighted terms checkbox.
* `elmfc-buttons` on the `<div>` around the buttons; `elmfc-btn` on a button uses the plugin's button style when "custom buttons" is enabled in Appearance Settings, otherwise the theme's button style.
* `elmfc-cancel` on a `type="button"` button: resets the form (only when "custom buttons" is enabled in Appearance Settings).
* `<span class="required">*</span>` in a label: required-field marker.

```html
<label class="radio-label"><input type="radio" name="source" value="search" required> <span>Search engine</span></label>
<div class="terms-container">
    <label class="checkbox-label"><input type="checkbox" name="terms" required> <span>I agree to the terms <span class="required">*</span></span></label>
</div>
<div class="elmfc-buttons">
    <button type="submit" class="elmfc-btn">Send Message</button>
    <button type="button" class="elmfc-btn elmfc-cancel">Reset</button>
</div>
```

= How do I enable SMTP? =

1. Edit your form
2. In Email Settings sidebar, check "Use SMTP"
3. Enter your SMTP host, port, encryption, username, and password
4. Save the form

= Does it work with page builders? =

Yes! The plugin includes CSS overrides for:
* Elementor
* Beaver Builder
* Divi
* Gutenberg blocks
* Generic row/column layouts

= How do I save form submissions? =

1. Edit your form
2. In Storage Settings sidebar, check "Save submissions to CPT"
3. Enter a CPT slug (e.g., "newsletter", "contact-requests")
4. Submissions will appear as a submenu under Forms

= How is stored submission data handled for privacy (GDPR)? =

If you enable "Save submissions to CPT" for a form, submitted data (name, email, message, and any other field) is stored in your site's database indefinitely by default. You can set "Auto-delete submissions after (days)" in Storage Settings to have the plugin permanently delete submissions older than that automatically (checked once daily); leave it at 0 to keep them forever.

Stored submissions are also included in WordPress's built-in personal data tools (Tools → Export Personal Data / Erase Personal Data), so you can fulfil a visitor's data request without manually searching the database.

= How are forms translated on a multisite network? =

With the EffortLess Multisite Auto Translate plugin active, a form saved on the network's source site is copied and translated to the other sites like a post:

* The text in the form's Custom HTML blocks is translated, and nothing else changes. With EffortLess Multisite Auto Translate 3.3.8 or later, only the visible text and the `placeholder`, `title`, `alt`, `aria-label` and button-label attributes are sent for translation; field names, IDs, classes, types, inline styles and hidden values stay exactly as on the source. The form fields are kept when the translated copy is saved.
* CSS and JavaScript written in the form's Custom HTML block (`<style>`, `<script>`) stay on the source site: WordPress removes them when a translated copy is saved. A translated form runs the source form's `<style>` and `<script>` instead, so edit the code on the source site only; every translated site uses it.
* The form keeps the same slug on every site, so `[elmfc name="..."]` works everywhere. On a page that uses the ELMFC Form block, the block is pointed at the translated form of that site.
* Per-form texts that you set (button labels, messages, email subjects) are translated; the plugin's default texts follow each site's language.
* Form statistics are not copied.

If a site has no translated copy of a form yet, `[elmfc name="..."]` shows the source site's form until the translation exists.

= Can I translate form messages? =

Yes. Each form can override its messages with hidden fields (see "Can I change the button texts and messages of one form?" above).

The plugin's own texts (submit and sending labels, success and error messages, the captcha label and placeholder) follow the site language. The plugin ships translations for German, Spanish, French, Italian, Japanese, Korean, Brazilian Portuguese and Traditional Chinese; they are used until a language pack for that language is available from translate.wordpress.org, which then takes priority. Text you write yourself in the form content, such as field labels or a submit button in a Custom HTML block, is not translated automatically: write it in the site's language.

== External services ==

This plugin can optionally connect to a third-party service to reduce spam. It is off by default and only activates when Akismet is installed and configured.

**Akismet**

If the Akismet plugin is installed, active, and configured with an API key on your site, this plugin sends each form submission's author name, email, message text, IP address, user agent, and referrer to Akismet's comment-check endpoint (`https://REST.akismet.com/1.1/comment-check`) to determine whether the submission is spam. This only happens if Akismet is already active on your site; if it is not, no data is sent to Akismet.

This service is provided by Automattic: [Terms of Service](https://akismet.com/tos/), [Privacy Policy](https://automattic.com/privacy/).

== Screenshots ==

1. Form builder in Gutenberg editor with fieldset support
2. Popup form
3. Admin settings - Email configuration with SMTP
4. Admin settings - Width options with visual preview
5. Admin settings - Security and appearance options
6. Mobile-responsive form with card-style radio buttons
7. Form with fieldsets and legend styling
8. Submissions stored as custom post type

== Changelog ==

= 7.5.60 =
* Change: the Forms list shows only each form's title. In "Extended view" it also printed an excerpt of the form's content (its CSS code and field texts); forms now have no excerpt in the admin, and the Compact/Extended view switch is no longer offered for forms.

= 7.5.59 =
* Change: the Shortcode box and the Slug column of the forms list show a form's non-Latin slug as written (for example `[elmfc name="預約品嚐體驗"]`) instead of WordPress's percent-encoded storage form (`%e9%a0%90...`). Both forms work in the shortcode.

= 7.5.58 =
* Docs: translated forms keep the source form's markup exactly when EffortLess Multisite Auto Translate 3.3.8 or later is used, which translates a form's Custom HTML block text-only. No code change in this plugin; the FAQ explains what is translated.

= 7.5.56 =
* Fix: forms translated by EffortLess Multisite Auto Translate lost their fields. Translations are saved in the background, where WordPress's content filter removes `<input>`, `<select>`, `<textarea>` and `<button>`. While a form translation is being saved, these form tags are now added to the filter's allowed tags (`<script>`, `<style>` and event attributes are still removed).
* Fix: CSS and JavaScript written in a form's Custom HTML block were missing on translated sites, for the same reason. A translated form now runs the `<style>` and `<script>` of its source form, provided the source form's author may publish unfiltered HTML. Keep the code in the native Custom HTML block on the source site.
* Change: a form's Custom HTML block is translated even when it contains `<style>` or `<script>` (reverting the 7.5.55 behaviour): the translated copy's code is not used, so its text can be translated safely.

= 7.5.55 =
* Fix: forms were no longer translated by EffortLess Multisite Auto Translate. That plugin renamed its hooks and settings from `ms_gpt_*` to `elmat_*`, so none of this plugin's translation hooks ran any more: forms were not selected for translation, their HTML was not translated, slugs were not kept and the form block was not pointed at the translated form. The hooks are now registered under both names, and the source site is read from the new setting (with the old one as fallback).
* Change: a form's Custom HTML block that contains `<script>` or `<style>` is copied to other sites without automatic translation, so its code is not altered. Blocks without code are translated as before.

= 7.5.54 =
* Fix: the browser's own validation messages ("Please fill out this field.") follow the browser's language, so they stayed in English on a site in another language. Forms now show the plugin's translated messages for empty required fields, unticked required checkboxes, unselected options and invalid email, web address and number values. A field's own `data-error` message is used when set.

= 7.5.53 =
* Removed: the `bin/smoke-test.php` developer test script is no longer included in the plugin. It was never used by the plugin itself.

= 7.5.52 =
* Fix: searching the submissions list did nothing; the search term was read from the wrong place. Search now also looks inside every submitted field, not only the identifier (email).
* Fix: answers from a group of checkboxes showed as encoded text (`a:2:{…}`) on the submission detail page and in a resent email.
* Fix: "Resend email" added two internal rows ("Elmfc Form", "Elmfc Url") to the email, and sent the copy to the visitor again. It now sends the notifications only, without those rows.
* Fix: line breaks in a submitted message are shown on the submission detail page.
* Fix: the submissions conversion column always showed 0 submissions; the submission counter was never increased.
* Fix: forms in the trash, and draft, pending or scheduled forms, were shown to visitors when a page used their shortcode or block. Visitors now see published forms (and private forms when logged in); drafts are visible only to users who can edit them, for previewing.
* Change: viewing, searching, exporting, resending and deleting stored submissions now requires the `manage_options` capability (administrators) instead of `edit_posts`, because submissions contain visitors' personal data. Developers can change it with the `elmfc_submissions_capability` filter.
* Added: new submissions are marked "New" until opened, the All Submissions menu badge counts new submissions, and the list has "Mark as read" / "Mark as new" bulk actions. Submissions stored before this version are not counted as new.
* Change: CSV export reads submissions 100 at a time, so large exports use less memory.
* Change: the ELMFC Form block is registered from a `block.json` file (block API version 3). Requires WordPress 5.8 or later.
* Change: after a form's emails are sent through its own SMTP server, the shared mailer's keep-alive and timeout settings are restored, so later emails in the same request are not affected.
* Change: the delete confirmations no longer use inline `onclick` attributes, and the admin pages use even side margins.
* Removed: the `[elmfc_submissions]` shortcode, which showed stored submissions on the website. Pages that still contain it display nothing in its place; submissions are under Forms > All Submissions.
* Readme: the "Additional Information" section was moved into the FAQ and corrected (email subjects cannot be set with hidden fields; there is no `popup_width` setting).

= 7.5.51 =
* Fix: the bundled translations were never used, so the plugin's texts (submit and sending labels, success message, captcha label and placeholder) stayed in English on a site set to another language. The bundled translation for the site language is now loaded when no translate.wordpress.org language pack is installed for it; an installed language pack still takes priority.

= 7.5.50 =
* Fix: the ELMFC Form block's labels in the editor ("ELMFC Form", "Select a form", "Open as popup", and so on) used the wrong text domain (`elmfc`), so they could never be translated. They now use the plugin's text domain and are included in the translation template and the bundled translations.
* Added: bundled editor translations for the block in German, Spanish, French, Italian, Japanese, Korean, Brazilian Portuguese and Traditional Chinese, loaded with `wp_set_script_translations()`.

= 7.5.49 =
* Translations: added the "Please take a moment to check your answers, then send the form again." message (shown when a form is sent too quickly) to the translation template and to the bundled German, Spanish, French, Italian, Japanese, Korean, Brazilian Portuguese and Traditional Chinese translations.

= 7.5.48 =
* Fix: after a successful submission the success message could stay hidden. On forms whose submit button sits inside a paragraph or `<div>` (common in a Custom HTML block), hiding the form also hid the message placed next to the button. The message is now moved out of the form fields before they are hidden.

= 7.5.47 =
* Fix: the captcha stayed as "… = ?" when the form contained the `[elmfc_captcha]` tag but "Enable simple captcha" was unticked. A form containing the tag now always shows and checks the captcha. Before 7.5.46 such a form showed a question but did not check the answer.
* Fix: the captcha stayed as "… = ?" on forms that were still drafts. The question is now loaded for every form the shortcode can display.
* Change: when the captcha cannot be loaded, the reason is written to the browser console.

= 7.5.46 =
* Change: the captcha question and the form's security token are now loaded when the form is shown, instead of being written into the page. Forms keep working on pages served from a full-page cache, where they previously failed with "Incorrect captcha" or "Security check failed" after an hour or a day. Rendering a page no longer stores a captcha in the database.
* Change: form views ("impressions") are counted when a browser shows the form, not on every page render, so crawlers and cache warm-ups are no longer counted.
* Added: a form sent less than 3 seconds after its captcha was loaded is rejected with a message asking the visitor to check the answers and send again.
* Change: "Send copy to user" is now off by default for new forms. Anyone can type another person's address into a form, so the copy is only sent when you enable it. Forms that already have the setting saved are unchanged.
* Change: when a form uses its own SMTP server, the admin email and the copies of one submission share one connection, and an unresponsive server is abandoned after 10 seconds instead of 5 minutes.
* Change: SMTP passwords and webhook secrets are encrypted with a key generated for the plugin, instead of one derived from WordPress's security keys, so changing those keys no longer makes the stored passwords unreadable. Existing values are re-encrypted the first time an administrator opens the dashboard of each site.
* Change: the REST route (`/wp-json/elmfc/v1/submit`) now uses the same code as the AJAX submission, so it also runs the Akismet check and accepts file uploads.
* Change: the `[elmfc_submissions]` shortcode is now shown to administrators only (`manage_options`); it previously showed stored submissions to any user who could edit posts, including Contributors.
* Change: stored submissions are no longer registered in the REST API.

= 7.5.45 =
* Fix: when a form has no `[elmfc_captcha]` tag, the captcha is now placed just before the submit button also for a plain `<button type="submit">` or `<input type="submit">` written in a Custom HTML block. Previously it was only placed correctly when the button sat in a Buttons block or an `elmfc-buttons` wrapper, and otherwise ended up after the button.
* Fix: line breaks in multi-line fields (for example a message textarea) are kept in the notification email, the stored submission and the webhook payload. They were previously merged into a single line.
* Fix: the form's CC and BCC addresses are only added to the admin notification, no longer to the copy sent to the person who filled in the form.
* Change: submission storage types are registered from a cached list, removing one database query per storing form on every page load.

= 7.5.44 =
* Fix: the popup overlay's background blur is now applied only while a popup is open. It was always present on the page, which could make scrolling sluggish on some browsers.
* Fix: pages that use the form block now load the plugin's stylesheet in the page head, so the form no longer appears unstyled and then shifts when the page loads.

= 7.5.43 =
* Change: after a successful submission the form fields are hidden and only the success message is shown, in a larger green box with a check mark, and scrolled into view.

= 7.5.42 =
* Added: a Custom HTML block inside a form can now contain its own `<style>` and `<script>` code, when the form's author has the `unfiltered_html` capability (administrators on single-site installs, super admins on multisite). For other authors the previous filtering still applies.

= 7.5.41 =
* Dev: bin/smoke-test.php now escapes its console output, reads the .htaccess file through WP_Filesystem, uses strict comparison, and no longer reuses the names of WordPress globals ($id, $path) for its cleanup loop variables.

= 7.5.40 =
* Added bundled translation files: a template (.pot) and translations for Spanish, Traditional Chinese, Korean, French, Japanese, German, Italian and Brazilian Portuguese.

= 7.5.39 - 2026-10-10 =
* Change: the form submission REST route no longer uses the generic `__return_true` permission callback. A named permission function documents why the route is public and where spam protection (honeypot, math captcha, validation) is applied. No change in behaviour.

= 7.5.38 - 2026-09-12 =
* Fix: the form's outer container (popup and inline display modes alike) had no explicit text color of its own, so fieldset labels, radio options, and the terms-and-conditions checkbox text silently inherited the active theme's body text color — on a theme with light/white body text, they could render nearly invisible against the form's own light backgrounds. The container now sets its own explicit text color instead of relying on the theme.

= 7.5.37 - 2026-09-05 =
* Change: the bundled donate-notice class is renamed from the shared `Effortless_Donate_Notice` to `ELMFC_Donate_Notice`, so the class name carries this plugin's own prefix and cannot collide with another EffortLess plugin loading its own copy.
* Fix: the PHPCS configuration file is no longer included in the released plugin. It is a development-only file and did not belong in a distributed build.

= 7.5.36 - 2026-09-05 =
* Change: tested against WordPress 7.1 and the "Tested up to" header updated to 7.1. No functional changes.
* Change: the donate notice's outbound link no longer includes the site address as a query parameter. It still identifies the plugin and version so support requests can be matched to a release, but not which installation was viewing it.

= 7.5.35 - 2026-08-18 =
* Added: Forms > Settings page with a "Disable the plugin's built-in CSS" option. When enabled, forms render with no plugin styling at all — fields, buttons, the popup, and the CAPTCHA fall back to the active theme's default appearance, for site owners who want to write their own CSS from scratch. Applies site-wide; the plugin's JavaScript (AJAX submission, popup, validation, multi-step navigation) keeps working either way.

= 7.5.34 - 2026-08-18 =
* Fix: form field labels could still inherit an underline from the active theme's default label styling. The previous fix only covered the legacy auto-inserted CAPTCHA box's own label; the general `.elmfc-form label` rule (which also covers any label an author writes by hand around the new `[elmfc_captcha]` shortcode) never reset text-decoration. Now set to none, with !important to reliably win against theme styles.

= 7.5.33 - 2026-08-18 =
* Added: `[elmfc_captcha]` shortcode — a bare arithmetic CAPTCHA (question, answer input, and verification token only, no label or box) that can be placed directly in a form's HTML. Lets you write the surrounding label/instructions as ordinary form text — so it translates through the same pipeline as the rest of the form instead of a separate settings field. When present, the auto-inserted CAPTCHA box from "Enable simple captcha" is skipped automatically.

= 7.5.32 - 2026-08-18 =
* Fix: the CAPTCHA question label and answer placeholder were rendered from the plugin's hardcoded default text instead of the per-form (and, on translated sites, GPT-translated) value stored in postmeta, so a translated form's CAPTCHA always displayed in the source site's default language.
* Fix: "send copy to user" and the submitter Reply-To header were matched by the admin-configured field name only. On a translated copy of a form, if translation ever alters the underlying HTML field name, the match would silently fail with no submitter email sent. Now falls back to the first submitted value that is itself a valid email address.
* Fix: the CAPTCHA label could pick up an underline from the active theme's default label styling, since the plugin's own CSS never set text-decoration on it. Explicitly set to none.

= 7.5.31 - 2026-08-18 =
* Fix: forms are registered as a non-public post type, which excluded them from the EffortLess Multisite Auto Translate plugin's default translatable post types and silently prevented any form text from being translated to destination sites. Forms now register themselves for translation via the `ms_gpt_translatable_post_types` filter.

= 7.5.30 - 2026-07-11 =
* Fix: corrected plugin name casing to EffortLess (was Effortless) in the plugin header, readme, and privacy exporter/eraser labels.

= 7.5.29 - 2026-07-10 =
* Added: Dismissible "Support the development" donate notice on the form and submissions admin screens, plus the standard Donate link plugin header.

= 7.5.28 - 2026-07-04 =
* Dev: Added bin/smoke-test.php, a WP-CLI script (`wp eval-file bin/smoke-test.php`) that exercises file-upload handling, the privacy exporter/eraser, and the daily cleanup cron directly against a live install, cleaning up all test data it creates. Not part of the plugin's runtime; excluded from PHPCS scanning in phpcs.xml since it's CLI-only output, not web-facing.

= 7.5.27 - 2026-07-04 =
* Added: GDPR/Privacy Tools integration — form submissions are now included in WordPress's built-in Export Personal Data / Erase Personal Data tools (Tools → Export/Erase Personal Data), matched by the requester's email address across every submission CPT. Suggested privacy policy language is also registered via wp_add_privacy_policy_content().
* Added: Configurable submission retention — a new "Auto-delete submissions after (days)" field in Storage Settings, enforced by a new daily cron job. Leave at 0 (default) to keep submissions forever, matching prior behavior.
* Security: Form file attachments are now uploaded into a dedicated `wp-content/uploads/elmfc-attachments/` directory with a deny-all `.htaccess`/`index.php`, instead of the standard public year/month uploads folder, since they may contain sensitive user-submitted documents. Cleanup of these files (and the multisite blog-switch restore) is now guaranteed via try/finally even if a hook throws mid-request.
* Security: Rate-limit rows written directly to wp_options (for atomicity) are now purged once expired by the new daily cron, instead of accumulating indefinitely.
* Security: Email subject lines are now run through sanitize_text_field() before being handed to wp_mail(), as defense-in-depth against control-character/header injection regardless of how the underlying setting was populated.
* Fix: `wp_die( 'Unauthorized' )` in the CSV export handler now uses `esc_html__()` like every other message in the plugin.
* Fix: `uninstall.php` now also removes the `elmfc_recaptcha_captcha_fixed` migration-tracking option and the daily cleanup cron event.
* Cleanup: The shortcode's KSES allowlist is now memoized (built once per request) instead of being rebuilt on every form render.
* Docs: Clarified the security trade-offs of the `ELMFC_TRUST_PROXY` constant in its doc comment.

= 7.5.26 - 2026-07-03 =
* Security: The submissions admin page now validates the `cpt` query parameter against the plugin's own list of managed submission post types before rendering or listing anything, closing a gap that let any user with the broad `edit_posts` capability browse/export posts of an arbitrary post type through this screen.
* Security: Deleting a submission (single or bulk) now also checks `current_user_can( 'delete_post', $id )` for that specific post, not just the broad `edit_posts` capability.
* Fix: The honeypot field label now uses `esc_html__()` instead of an unescaped `__()` call.
* Fix: The one-time "my_form" → "elmfc_form" CPT migration now clears the WordPress post cache for every renamed post, so sites using a persistent object cache (Redis/Memcached) don't keep serving stale cached posts under the old post type after the rename.

= 7.5.25 - 2026-07-03 =
* Fix: The one-time "my_form" → "elmfc_form" CPT migration now also runs immediately after switch_to_blog() in cross-site rendering/submission code paths, not just on init. Previously, a source site reached only via switch_to_blog() (never visited directly since upgrading) would keep rejecting valid cross-site submissions as "Invalid form".
* Fix: Forms that had Google reCAPTCHA enabled with the math captcha explicitly turned off now automatically get the math captcha re-enabled on upgrade, instead of silently ending up with no CAPTCHA at all after reCAPTCHA was removed. Stale reCAPTCHA postmeta is cleaned up at the same time.
* Fix: Submission detail view no longer shows the literal text "Array" for multi-value fields (e.g. checkbox groups); values are now joined into a readable comma-separated list.
* Fix: Submission detail view field ordering now matches the previous case-insensitive sort instead of PHP's case-sensitive default.
* Fix: Admin asset loading now matches the submissions page by its exact registered hook suffix instead of a substring match on the screen ID.
* Cleanup: Removed a dead, unreachable branch left over from the reCAPTCHA removal in the settings import handler, and a redundant duplicate merge pass in the shortcode's KSES allowlist builder.

= 7.5.24 - 2026-07-03 =
* Code quality: Removed ~45 unnecessary phpcs:ignore/disable suppressions across the codebase (verified empirically that the underlying code already satisfied WordPress Coding Standards without them). Replaced a raw SQL query with get_post_meta() in the submission detail view, used $wpdb->esc_like() instead of a hand-escaped LIKE wildcard, and sanitized a couple of previously-unwrapped $_SERVER/$_FILES reads directly instead of suppressing the warning. No functional changes.

= 7.5.23 - 2026-07-03 =
* Removed: Google reCAPTCHA v3 integration entirely (settings UI, verification, footer script). Forms now rely solely on the built-in, self-hosted math captcha plus honeypot and rate limiting — no external CAPTCHA service or data leaves your site.

= 7.5.22 - 2026-07-03 =
* Fix: Renamed the "my_form" custom post type to the prefixed "elmfc_form" to avoid naming collisions with other plugins/themes. Existing forms are migrated automatically (one-time rename on the first admin request per site); no action needed.
* Cleanup: Removed AI-assistant planning/spec files from the shipped plugin (docs/superpowers/); added them to .gitignore.

= 7.5.21 - 2026-07-02 =
* Fix: Enqueue Google reCAPTCHA and admin/submissions meta-box JS via wp_enqueue_script()/wp_add_inline_script() instead of printing inline `<script>` tags.
* Fix: Use wp_handle_upload() instead of move_uploaded_file() for form file attachments.
* Fix: Apply a form-aware KSES allowlist to shortcode-rendered form content as defense-in-depth escaping.
* Docs: Document Google reCAPTCHA and Akismet as external services in the readme.

= 7.5.20 - 2026-06-30 =
* Update: Confirmed compatibility with WordPress 7.0.

= 7.5.19 - 2026-06-30 =
* Update: Tested up to WordPress 7.0.

= 7.5.18 - 2026-06-30 =
* FEATURE: Shortcode pre-fill — pass any field's name= attribute directly in the shortcode to pre-populate text inputs, textarea, radio buttons, checkboxes, and selects. Example: [elmfc id="123" plan="premium" firstname="John"]. Comma-separated values pre-check multiple checkboxes in a group (name="field[]"). Reserved attributes (id, name, popup) are not treated as field names.

= 7.5.17 - 2026-06-30 =
* FIXED: User copy email never sent when the form's email input had any name other than "email" — the handler hardcoded $data['email'] with no way to configure it. Added "Email field name" setting (defaults to "email") in Email Settings so any field name works. Also fixes Reply-To on admin emails for the same reason.

= 7.5.16 - 2026-06-30 =
* UX: Fixed popup close animation — overlay now fades out correctly instead of snapping off instantly; replaced display:none toggle with pointer-events/visibility so CSS transitions run on both open and close.
* UX: Scroll to first invalid field on validation failure — page now smoothly scrolls to and focuses the first field that failed validation, not just the error banner.
* UX: Submit button loading spinner — button shows a CSS spinner while the AJAX request is in flight; text goes transparent so button size stays stable.
* UX: SMTP password field show/hide toggle — added a reveal button (dashicons-visibility) next to the password input in the email settings meta box.
* UX: Stats column now shows impressions → submits (conversion %) instead of two separate raw-number columns; tooltip gives full label text.

= 7.5.15 - 2026-06-30 =
* REFACTOR: elmfc_get_config_keys() and elmfc_get_skip_fields() now use static caching — arrays are built once per request instead of on every call.
* FIXED: Removed duplicate elmfc_captcha_answer and elmfc_redirect_url entries from elmfc_get_skip_fields() — both keys are already included via elmfc_get_config_keys().
* REFACTOR: Removed trivial get_skip_fields() proxy method from ELMFC_Email_Handler — extract_form_fields() calls elmfc_get_skip_fields() directly.
* REFACTOR: Removed dead $plain parameter from ELMFC_Email_Handler::format_html_email() — the plain-text body was passed in but never used inside the method.
* REFACTOR: Extracted duplicated main-site ID resolution in the shortcode into a single pre-computed variable — the ms_gpt_source_site / get_main_site_id() lookup was previously copy-pasted in both the id= and name= fallback paths.
* DOCS: Added return type hint :array to elmfc_default_texts() and elmfc_get_config_keys(); improved docblock for elmfc_get_config_keys().

= 7.5.14 - 2026-06-29 =
* SECURITY: Rate limiter no longer bypassed on sites running a persistent object cache (Redis/Memcached) — counter rows are now written directly to the options table via $wpdb->replace() instead of set_transient(), so the atomic SQL UPDATE always finds the rows it needs.
* SECURITY: Math captcha tokens now include the form_id in the transient key ('elmfc_cap_{form_id}_{token}') — a token solved on one form can no longer be replayed against a different form in the network.
* SECURITY: AJAX nonce is now scoped to the specific form_id ('elmfc_form_submit_{id}') — a nonce obtained from form A can no longer be used to target form B on any site in the network.
* SECURITY: Email subject and intro text are now read from post meta, not from $_POST — an attacker can no longer spoof the admin notification email's subject line or opening sentence.
* FIXED: form_title and form_url are now saved to CPT submissions as 'elmfc_form' and 'elmfc_url' meta keys — the rename block was previously dead code because it ran after array_diff_key() had already stripped those keys.
* FIXED: Akismet API key now read with get_site_option() before switch_to_blog() — cross-site form submissions on multisite no longer bypass the spam check when Akismet is network-activated (key stored in sitemeta, not per-site options).
* FIXED: elmfc_rl_limit and elmfc_rl_window added to the canonical elmfc_get_skip_fields() — these internal rate-limit config keys are no longer forwarded to external webhook payloads; local duplicates removed from email handler and CPT storage.

= 7.5.13 - 2026-06-29 =
* REFACTOR: reCAPTCHA v3 verification extracted to shared elmfc_verify_recaptcha() helper — AJAX and REST handlers no longer duplicate the secret-fetch, HTTP call, and score-check logic.
* REFACTOR: Rate-limit config reading extracted to shared elmfc_get_rate_limit_config() helper — eliminates duplicated get_post_meta() calls in both submission handlers.
* REFACTOR: DISTINCT postmeta scan for submission meta keys extracted to shared elmfc_get_submission_meta_keys() helper — list table and CSV export now share the same cached query; fetch_meta_keys() delegates to it.
* FIXED: Webhook payload builder now uses elmfc_get_skip_fields() instead of a partial hand-rolled list, ensuring elmfc_cc, elmfc_bcc, and any future skip-list additions are excluded from outgoing webhook payloads.
* IMPROVED: elmfc_default_texts() now memoizes its return value with a static variable, avoiding array reconstruction on every call.
* IMPROVED: elmfc_is_valid_blog() now returns true for blog ID 1 on single-site installations instead of always returning false.
* DOCS: readme.txt Additional Information section corrected — elmfc_redirect_url is configured in the Appearance Settings sidebar (post meta), not via a hidden form field.
* DOCS: REST endpoint now has a clear note that file uploads are not supported; the AJAX handler should be used for forms with file fields.
* DOCS: docs/ directory added to .gitignore and excluded from distributable zip.

= 7.5.12 - 2026-06-29 =
* SECURITY: reCAPTCHA v3 — omitting the token or leaving the secret unconfigured now rejects the submission instead of silently skipping verification (previously bypassed both reCAPTCHA and math captcha simultaneously).
* SECURITY: Rate-limit guard now distinguishes a DB error (false) from zero rows affected (0) — a transient DB failure no longer resets the window and bypasses the limit.
* FIXED: Cross-site multisite — redirect URL after submit now read from source blog postmeta before restore_current_blog(); previously always returned empty on cross-site forms (AJAX and REST).
* FIXED: Email handler get_skip_fields() now delegates to the global elmfc_get_skip_fields() so future additions to the skip list are automatically inherited.
* FIXED: send_emails() now returns true when no email routes are configured, preventing a false "Error sending email" response after a successful CPT save.
* FIXED: Impression and submit stat counters — on the very first submission a concurrent-insert race could leave the counter at 1 instead of N; retry logic ensures no increment is lost.
* IMPROVED: fetch_meta_keys() in the submissions list table is now cached in a 60-second transient (invalidated on new submission), eliminating a full postmeta DISTINCT scan on every page load.
* IMPROVED: "Send to specific user" dropdown raised from 200 to 500 users; sites with more than 500 users now see an inline notice with the total count.

= 7.5.11 - 2026-04-28 =
* SECURITY: elmfc_redirect_url is no longer emitted as a hidden form field — it is now read directly from post meta by the AJAX and REST handlers, eliminating any possibility of client-side tampering with the post-submission redirect destination.

= 7.5.10 - 2026-04-27 =
* SECURITY: REST endpoint now enforces the same captcha stack as the AJAX handler — math captcha (token/transient) and reCAPTCHA v3 are both checked before processing REST submissions.
* SECURITY: reCAPTCHA v3 secret key and webhook signing secret are now encrypted at rest using AES-256-CBC (same scheme as SMTP password). Existing plain-text values are decrypted transparently via the enc: prefix detection.
* SECURITY: Duplicate-form action now uses object-level capability check (edit_post + post ID) instead of the broad edit_posts capability.
* SECURITY: Single-submission delete action now validates the post type with elmfc_is_plugin_cpt() before calling wp_delete_post().
* FIXED: JSON form import no longer allows overwriting the CPT slug (_elmfc_cpt_slug) — changing the slug via import could silently rename the submission table.
* FIXED: wp_die() strings in admin actions are now translatable.

= 7.5.9 - 2026-04-27 =
* FIXED: Form field labels, placeholder text, and other text inside core/html blocks is now translated. Registers the ms_gpt_translate_html_block filter (added in auto-translate 3.2.31) for my_form posts so GPT receives and translates the raw HTML content.

= 7.5.8 - 2026-04-27 =
* FIXED: Forms now translate correctly — removed the filter that was telling the auto-translate plugin to skip all block content for my_form posts.
* FIXED: The elmfc/form Gutenberg block now registers ms_gpt_remap_block_attrs so its formId attribute is remapped to the destination site's translated form when a page is translated.
* FIXED: my_form slugs are now preserved on translated sites (ms_gpt_preserve_post_slug) so [elmfc name="..."] shortcodes continue to work.
* FIXED: [elmfc id=...] shortcode now falls back to the source site when the translated form doesn't exist yet on the destination site.

= 7.5.7 - 2026-04-26 =
* FIXED: Resolved final two PHPCS warnings — reworded inline comment in email handler that was flagged as commented-out code; corrected operator alignment in shortcode file.

= 7.5.6 - 2026-04-27 =
* SECURITY: REST endpoint redirect URL now validated with wp_validate_redirect() (parity with AJAX handler).
* SECURITY: [elmfc_submissions] shortcode now validates the cpt attribute against plugin-owned slugs via elmfc_is_plugin_cpt() — prevents listing arbitrary CPT content.
* FIXED: CPT slug save in meta boxes now rejects slugs that belong to built-in or third-party post types (post, page, my_form, etc.) to prevent form submissions being inserted as regular WordPress posts.

= 7.5.5 - 2026-04-26 =
* SECURITY: Rate limit counter is now atomic (SQL UPDATE + expiry JOIN) — eliminates TOCTOU race that allowed bypass under concurrent requests.
* SECURITY: REST endpoint now uses elmfc_get_client_ip() (proxy-aware) instead of bare $_SERVER['REMOTE_ADDR'] for rate limiting.
* SECURITY: Copy-to-site handler validates target blog ID with elmfc_is_valid_blog() before switch_to_blog().
* SECURITY: Resend-email AJAX handler verifies the submission belongs to a plugin-owned CPT via new elmfc_is_plugin_cpt() helper.
* SECURITY: Bulk delete and CSV export now validate the ?cpt= slug is a plugin-owned CPT before acting.
* SECURITY: Bulk delete verifies each post ID belongs to the expected CPT before deleting.
* SECURITY: Redirect URL after submission is validated with wp_validate_redirect() to enforce same-origin policy.
* SECURITY: File upload rename() return value is now checked — failed rename discards the temp file instead of attaching a phantom path.
* SECURITY: CPT submission meta keys are sanitized with sanitize_key() and private-meta prefix (_) is rejected in elmfc_save_to_cpt().
* SECURITY: Settings import rejects files larger than 512 KB before calling file_get_contents().

= 7.5.4 - 2026-04-26 =
* SECURITY: Fixed DOM-based XSS in showMessage() — replaced .html() with safe DOM construction; captcha question update now uses .text() via new .elmfc-captcha-question span.
* SECURITY: REST endpoint (/wp-json/elmfc/v1/submit) now enforces the same per-form, per-IP rate limiting as the AJAX handler.
* SECURITY: SMTP password no longer passed through sanitize_text_field() which was silently corrupting passwords containing <, >, or &.
* SECURITY: Server-side blocklist of executable extensions (php, phar, asp, sh, etc.) applied to file uploads regardless of admin-configured allowlist.
* SECURITY: elmfc_source_site POST/REST parameter is now validated against real network blog IDs before switch_to_blog() is called.
* SECURITY: reCAPTCHA v3 verification is now fail-closed — network errors reject the submission instead of silently passing it.
* FIXED: elmfc_submissions_shortcode now escapes get_the_title() output and requires edit_posts capability.
* FIXED: Cross-site block in main shortcode wrapped in try/finally so restore_current_blog() always runs on exception.
* FIXED: Resend email AJAX handler now checks current_user_can('edit_post', $form_id) to prevent cross-form access.
* FIXED: Copy-to-site handler now verifies nonce before capability check (correct security gate order).
* FIXED: Duplicate admin notices now only display on my_form screens.
* FIXED: Settings import now applies type-appropriate sanitizers (URL, int, float, email) instead of blanket sanitize_text_field for all values.
* FIXED: CPT registration (elmfc_register_all_dynamic_cpts) now caches results in a transient, eliminating the per-request DB query.
* FIXED: Submissions badge no longer runs N+1 DB queries on every admin page load — uses elmfc_sub_counts transient.
* FIXED: get_users() in email settings now limited to 200 results to prevent memory issues on large networks.
* IMPROVED: Centralized skip-field list via elmfc_get_skip_fields() used across CPT storage.
* IMPROVED: Uninstall transient cleanup now uses $wpdb->prepare() consistently.

= 7.5.2 - 2026-03-04 =
* FIXED: Remaining PHP Deprecated warnings — `str_replace(null)` caused by `get_edit_post_link()` returning null for non-public CPT in duplicate handler; replaced with `admin_url('post.php?...')`.
* FIXED: `preg_replace()` null return (on PCRE error) propagating as null `$form_content` in shortcode; cast result to string.
* FIXED: Forms not rendering for visitors — deprecation warnings output before form HTML disrupted page structure; null guards now applied before `$post` is accessed in asset enqueue.

= 7.5.1 - 2026-03-04 =
* FIXED: PHP Deprecated warnings — `strpos(null)` and `str_replace(null)` — caused by `$post->post_content` being null on some post types; cast to string before passing to `has_shortcode()` and `apply_filters('the_content', ...)`

= Earlier versions =
Entries for 7.5.0 and earlier (back to the 7.0.0 initial release) have been
removed from this file to stay under WordPress.org's Changelog length limit.
They remain viewable in earlier revisions of this same readme.txt in the
plugin's SVN repository history.

== Upgrade Notice ==

= 7.5.14 =
Security release: rate limiter fixed for Redis/Memcached setups, captcha cross-form replay patched, nonce scoped to form_id, email subject injection closed, form_title/URL CPT storage fixed. Strongly recommended.

= 7.5.13 =
Code quality release: shared helpers for reCAPTCHA and rate-limit config, webhook skip-list fix, elmfc_default_texts() memoization, and documentation corrections.

= 7.5.12 =
Security: reCAPTCHA bypass patched — omitting the token no longer skips verification. Rate-limit DB-error bypass fixed. Cross-site redirect URL fixed. Recommended update.

= 7.5.11 =
Security: redirect URL no longer sent as a hidden form field — read server-side from post meta instead.

= 7.5.10 =
Security hardening: REST endpoint now enforces captcha, secrets are encrypted at rest, capability checks tightened. Recommended update.

= 7.5.9 =
Form field labels and HTML content now fully translated. Requires effortless-multisite-auto-translate 3.2.31+.

= 7.5.8 =
Fixes multisite auto-translate integration: forms now translate correctly and the Gutenberg block remaps form IDs across sites.

= 7.5.7 =
Code quality: resolves all remaining PHPCS warnings. No functional changes.

= 7.4.9 =
Adds file upload support, a native Gutenberg block, and fixes required checkbox validation. Cancel button now fully resets the form after submission.

= 7.4.8 =
Security: SMTP passwords are now encrypted at rest. HTML emails, submission detail view, REST API, and settings export/import added.

= 7.4.7 =
Security: proxy IP headers now opt-in. Adds test email button and email delivery log in Submissions table.

= 7.4.6 =
Rate limiter fixes: real IP detection, per-form counters, runs after captcha. Per-form limit now configurable in Security Settings.

= 7.4.5 =
Bug fixes: shortcode copy buttons now work, submission counts batched correctly, rate limiting added (5/IP/10min).

= 7.4.4 =
Bug fixes: redirect URL / CC / BCC no longer appear in emails, duplicate form resets CPT storage, elmfc_before_send cancellation works correctly.

= 7.4.3 =
Adds redirect-after-submit, duplicate form, CC/BCC, developer filters, and form status columns on the list.

= 7.4.2 =
Performance: CSS/JS now loads only on pages with a form shortcode. Adds shortcode sidebar, submission badge, honeypot spam protection, and clean uninstall.

= 7.4.0 =
New dedicated Submissions admin page with full field listing, sortable table, bulk delete, and CSV export.

= 7.3.8 =
Admin refactor: consolidated asset loading, extracted JS/CSS to files, form styles now visible in Gutenberg editor.

= 7.3.7 =
Forms list now shows clickable copy buttons for ID and Slug columns — one click copies the ready-to-paste shortcode.

= 7.3.6 =
Multisite: [elmfc name="slug"] now resolves forms from the main site automatically when copied to destination sites via another plugin.

= 7.3.5 =
Security fix: captcha solution is no longer exposed in page source. Update recommended for all sites using captcha.

= 7.3.3 =
Major update with full container width support, fieldset styling, improved spacing, and enhanced admin interface. Forms now adapt to their container by default with optional width controls.

= 7.3.1 =
CPT submissions now properly organized as submenu under Forms with descriptive titles.

= 7.2.4 =
Full email system with SMTP support. Update recommended for better email deliverability.
