== Changelog == = 2.3.0 = * Fix: "Delete all data" (the Danger Zone button and the deactivation modal) left several things behind - the routine had drifted away from the plugin it cleans. **Uploaded files were the worst of it**: the destination was read from `accua_forms_file_data`, a name the plugin stopped writing long ago, so it always resolved to the default directory. A site with a configured upload path therefore had that default directory wiped while every file its visitors had submitted stayed on disk. The path now comes from the option the uploads really use (`accua_forms_default_file_field_data`), and the default directory is cleaned as well since files predating a custom path still live there. Because that makes the deletion actually follow an administrator-set path, it is now refused for any directory that is not plugin-owned - the uploads root, wp-content, wp-admin, the WordPress root or anything outside the install - so a destination like "wp-content/uploads" can never take the media library with it. * Fix: four options survived "Delete all data": the captcha settings (which hold the reCAPTCHA and Cap **secret keys**), the analytics settings, the file-field settings and the recovery backup of the field list. The option list now covers everything the plugin writes, and keeps the names used by older versions so a long-lived install is cleaned out too. The cached dashboard statistics (a transient holding submission counts) and the per-user submissions-list preferences (per-page choice and hidden columns) are removed as well. Matching is deliberately narrow: an extension plugin's own options, tables and user meta are never touched. * Improved: the reCAPTCHA v3 badge position setting says when its effect is visible. The badge only appears once the visitor starts filling the form, because no request is made to Google before that, and the CSS that positions or hides it is printed with the form - so a page cache keeps serving the previous position until it is emptied. Both are now stated under the setting; without that, changing the position looks like it does nothing. * Fix: the values the plugin stores for its own use were offered as submission columns. The field columns of the submissions list come from `SELECT DISTINCT afsv_field_id`, which also returns internal rows - notably `_accua_download_token`, the token that authorizes a file download - so they appeared as "(removed)" columns in the list and in Screen Options, and their values went into the Excel export. Column keys with the reserved `__` prefix (which the Fields page refuses as a slug) or the internal `_accua_` prefix are now skipped, the same two prefixes the single submission page has always skipped. * The DB 18 collation conversion is applied on every install run - every plugin activation as well as every version change - rather than only when the stored DB version is older. `dbDelta()` rewrites `afsv_field_id` back to the table's default (case-insensitive) collation whenever it runs, so a version-gated conversion would be lost again at the next activation, taking the case-sensitive slugs with it. The conversion returns early when the collation is already binary, so re-checking it costs one information_schema lookup. * Fix (DB 19): forms carried over from a pre-2.0 installation saved submissions with **no field values at all**. The 1.x -> 2.0 conversion, unchanged since 2.0.0, rebuilt a form's comma-separated field list as instances that had no instance id and sat under numeric keys. Such a form renders perfectly (the render loop reads the field reference) and the submission is recorded, but every value is dropped, because the submission handler matches each posted field against the instance keys and finds nothing. The conversion now produces instances keyed by instance id, the way the form editor writes them, and DB 19 repairs the forms an earlier version already converted - saved forms and trashed ones alike, since a trashed form can be restored. A form that was saved normally is left untouched, and a field used twice in the same form keeps both instances. * Feature: `uninstall.php`, honoring a new opt-in preference. The plugin had no uninstall handler at all - deleting it from the Plugins screen left every table and option behind, and only the Danger Zone button or the deactivation modal ever removed anything. Deleting a plugin is routinely a step in reinstalling or troubleshooting it, so the new `accua_forms_delete_data_on_uninstall` option ships **off** and the uninstaller then does nothing whatsoever. Ticking "Delete all Contact Forms data when the plugin is deleted" in the Danger Zone makes it run the same routine as the Danger Zone button: uploaded files at the configured destination (with the plugin-owned-directory guard), the three tables, every option including the captcha secret keys and the preference itself, the cron event, the transients and the per-user screen preferences - and nothing another plugin owns. Multisite is handled per site: the preference, the options and the tables all belong to a single site, so a network uninstall deletes the data of the sites that opted in and leaves the others exactly as they are. The deletion routine and its helpers live in `includes/data-deletion.php`, which defines functions and registers no hooks, because uninstall.php runs with the plugin not loaded and has to require what it uses itself. * Fix: an unreadable field list is no longer replaced by the shipped defaults on upgrade (found on a real site while testing this release). If the `accua_forms_avail_fields` option cannot be unserialized, `get_option()` returns false, `accua_forms_install()` reads that as "no fields defined yet" and writes the default field set over it - discarding every field the site had defined, with their labels and options, and no way back. It only bites on an upgrade, because the install routine runs when the stored DB version changes, which is exactly when such a site meets it. The usual cause is a migration or search-and-replace tool that rewrote the option text (classically normalizing CRLF to LF) without recomputing the `s::` headers PHP validates: the data is all there, only the lengths disagree. The upgrade now recomputes those lengths and accepts the result only if it unserializes into something shaped like a field list; either way the untouched original is copied to `accua_forms_avail_fields_corrupt_backup` before anything is written, so nothing is lost even when the repair does not work. A healthy option is never touched: each declared length is trusted first and only shortened when it fails to land on the closing quote. * Compatibility note for the DB 18 conversion: the binary collation is derived from the column's own charset (`latin1_bin`, `utf8mb3_bin`, `utf8mb4_bin` …), verified on real latin1 and legacy-utf8 installs, so a site that never moved to utf8mb4 converts correctly instead of failing on a hardcoded collation name. Only `afsv_field_id` changes; `afsv_value` and every other column keep theirs. Code that joins its own table against `afsv_field_id` keeps working without changes, whichever collation it uses on its side - a plugin that reads the live collation gains the case distinction, one that hardcodes a utf8mb4 collation simply keeps matching case-insensitively as before. A field slug still cannot be changed once the field exists: it is the key every form instance and every stored submission refers to, and anything else holding it - an extension, custom code, an external integration - could not be updated along with it. * Fix: field slugs are now case-sensitive throughout, and two fields whose slugs differ only in case no longer collide (user-reported from a 2.2.32 site). Field definitions live in a PHP array, where keys are case-sensitive, so a site could legitimately hold both a `role` text field and a `Role` checkbox — but the submission values table stored `afsv_field_id` with the site's case-insensitive collation, and to MySQL the two were the same string. The visible symptom was that the submissions list and the Excel export offered a single column for the pair, because both are built from `SELECT DISTINCT afsv_field_id`. The unreported and more serious one: the table's `PRIMARY KEY (afsv_sub_id, afsv_field_id)` also treated them as one, so on every submission the second field's value was rejected as a duplicate key and silently lost. DB version 18 converts that one column to the binary collation of its own charset (an explicit ALTER, the way WordPress core changes collations; dbDelta does not compare them). Converting in this direction cannot fail on existing data: values that were equal under the old collation could never both exist. Only that column changes, so its indexes keep working and no query needs a per-query COLLATE. * Fix: the Excel export ran the selected column keys through `sanitize_key()`, which lowercases. Column keys embed the field slug verbatim (`_field_{slug}`), so exporting a site with `role` and `Role` merged the two regardless of the database. The export now uses a case-preserving sanitizer (`accua_forms_sanitize_column_key()`) accepting the same characters a slug may contain. * Change: newly created field slugs are lowercased, the way WordPress lowercases term slugs. Existing slugs are never touched — a site already holding mixed-case slugs keeps them working, and they remain editable — but a new field can no longer accidentally introduce a `role`/`Role` pair. The Fields page input lowercases as you type, so it always shows what will be stored. * Change: reCAPTCHA v2 now defaults to "Reject with an error message" instead of the silent "Accept silently and mark as Spam" default introduced for both captcha types in 2.2.40 (user-reported). v2 shows the visitor a checkbox to solve, so a failed check is usually not a bot: it is a real person who did not tick it, whose token expired (Google keeps them valid for two minutes) or who retried with a token already consumed (they are single-use). Accepting those silently filed a genuine message under the Spam status and sent no notification email, with nothing on screen to tell the visitor. Google's guidance for a failed verification is to surface the error and let the visitor solve the challenge again, which is what this default now does — the AJAX handler already resets the widget after every response, so the retry carries a fresh token. reCAPTCHA v3 keeps the silent "mark as Spam" default: its score is computed invisibly, there is nothing for the visitor to solve, and a low score is best absorbed without bouncing anyone. * Implementation: the two types now have separate site-wide defaults in the "Captcha defaults" settings box — "When the reCAPTCHA v2 check fails" (new `captcha_spam_action_v2` option, shipped default `reject`) and "When the reCAPTCHA v3 check fails" (the existing `captcha_spam_action`, shipped default `spam`) — because the settings page writes its select on every save, so a shared key would have masked the new v2 default on any site that had ever saved the settings page. `accua_forms_captcha_default_spam_action()` and `accua_forms_captcha_spam_action()` take the field type as a new argument (defaulting to the v3 resolution, so pre-2.3.0 callers are unaffected); the per-field "override" checkbox in the form editor is unchanged, and fields that already carry an explicit `spam_action` keep it. * Fix: a form carrying both a v2 and a v3 field could apply the wrong action. The submission handler resolved one action per form by scanning for the first captcha field, which was harmless while both types shared a default but not once they differ. `AccuaForm_Validation_CaptchaSpam` now records the silent action of the validator that actually failed (`getSpamAction()`), and the handler follows that, so each captcha field's setting governs its own failures. * Fix: the "Contact Form" block rendered nothing on WordPress 5.9 and 6.0, which the plugin still declares support for. `block.json` describes its dynamic rendering with the `render` property, honored only from WordPress 6.1, and the block was registered without a `render_callback` fallback — so on older versions it registered fine, appeared in the inserter, and produced an empty front end. `register_block_type()` is now called with an explicit `render_callback`; on 6.1+ the explicit argument wins over the metadata-derived one and both resolve to the same `block-editor/render.php`, so nothing changes on current WordPress. * Public release consolidating the 2.2.33 - 2.2.47 development versions (the previously published version was 2.2.32). The readme.txt changelog entry for 2.3.0 summarizes all of them; the per-version detail stays in this file. * Plugin Check (PCP) 2.0.0 final compliance pass: fixed the one finding on the shipped file set — the Fields page live preview printed the JSON-encoded "this is a preview" note into its inline script without an escaping annotation. The value is wp_json_encode() output of an already-escaped string, safe in a script context, now annotated as such (admin/fields-page.php). No functional changes. * i18n: completed the one missing translation in the bundled catalogs (the "%d field deleted." plural, it_IT and es_ES) and regenerated POT/PO/MO. Both catalogs are now 100% translated. * Fix: the two high-contrast blocks in frontend.css were written as `@media (prefers-contrast: high)`. `high` is not a value in Media Queries Level 5 (`no-preference | more | less | custom`), so the query never matched in Chrome or Firefox and the styles — a bolder floating label in the inline-label layout, 2px borders and a focus outline on the post-select control — had never applied for anyone browsing with a contrast preference. Now `@media (prefers-contrast: more), (prefers-contrast: high)`, keeping the legacy value for older WebKit. CSS version 205. * Readme accuracy pass over the whole readme.txt, verified claim by claim against the code. Corrected: "20 available field types" (there are 24, and the list omitted Telephone, Date, Post select and Color picker); the "orange C icon" (the classic-editor button is the cyan Contact Forms logo, and the Gutenberg "Contact Form" block was not mentioned anywhere despite shipping since 2.0); a "Preview/Test Tab" that does not exist (the form editor has a live preview pane); the "Appearance" tab (labelled "Appearance and General"); an "Advanced Excel Export option" (there are two export buttons, and the frozen header row plus column filters apply to every export); Cloudflare Turnstile listed as if built in (it requires the free Simple Cloudflare Turnstile plugin); the spam-handling sentence, which read as if Spam, Trash and delete were all the default (only "mark as Spam" is); screenshot 1 calling Top Labels the default layout (the default is labels on the left); "ARIA labels" (labelling is native `label for`, the ARIA work is `aria-describedby`/`aria-invalid` and live regions); "all captcha fields" for Hide field title (Turnstile does not offer it); the reCAPTCHA v3 notice being printed "under the form" (it renders where the v3 field sits); "long values compress in every column" (URL and field columns); the dashboard "Go to:" links listing "each lead status" (only those in use); and the claim that extension field types show their own settings on the Fields page (they get the generic sections — that page fires no `accua_forms_field_settings` hook). * Added the missing `Requires PHP: 7.4` line to the plugin header, which previously declared it only in readme.txt. * Improved: the "Essential Columns" button tooltip explains where its list comes from (user-reported: the tooltip named the columns but read as a fixed, generic list). It already listed the columns actually configured on the site — the always-essential ones plus the flagged field columns — but nothing told the administrator that the field part is theirs to choose. A second line now says so, naming the option and the screen: "Choose which field columns are kept: edit the field on the Fields page and enable 'Show in essential columns'." Rendered as a real second line (title attributes honor newlines) and translated in it_IT and es_ES. The first line keeps its existing wording and translations. = 2.2.47 = * Feature: per-field "Show in essential columns" checkbox on the Fields page (add and edit form). `Accua_Forms_Submissions_List_Table::essential_columns()` is no longer a hardcoded list: Actions, ID, Form and Submitted are always essential, and a field column is included when its definition carries the new `essential_column` flag — the "Essential Columns" button and its tooltip follow automatically since both already consume `essential_columns()`. The previously hardcoded IP, Page, Referrer and Language main columns are no longer essential (diagnostic metadata, not what a submission list is scanned for). The flag is written explicitly (0/1) by `accua_forms_fields_filter_values()` — a missing key means "never saved on this version", which is what the upgrade migration keys on. The checkbox section is hidden for field types that never store a submission value (submit, html, the four captcha types); extension types get it like the other generic sections. It renders as the last option of the editor, behind an hr separator: it is an admin-list-only setting with no effect on how the field renders in forms, so it is kept visually apart from the settings above, which all shape the field itself. * Feature: sensible defaults for the flag — fresh installs mark first_name, last_name, email and message (who wrote, and what they wrote); the first-visit default hidden columns of the submissions list (`default_hidden_columns` filter) now follow the same flag instead of hardcoding the email field. * Upgrade: DB version 17. Existing installations get `essential_column = 1` on the email field only — exactly the field column of the old hardcoded list, so the button keeps showing the same field columns right after the update. Guarded by isset (the Fields page save always writes the key), so unchecking email later is never overridden by a later plugin activation run. Dev-stack note: bump the DB version constant last when editing a live-mounted install — the constant is compared on every init, so a request landing between the constant bump and the migration code burns the version with a no-op run (happened during development; recovered by resetting `accua_forms_db_version` and reloading). * Fix: the Fields page live preview frame appeared oversized and shrank to the right size a moment later (user-reported). `resizePreview()` ran right after `document.write()`, measuring the written document before its stylesheets had loaded — unstyled form content is taller, so the frame grew immediately and only settled when the CSS applied and the ResizeObserver/timed re-measures caught up. The first resize (and the ResizeObserver setup) now waits for the frame's load event — bound on the iframe element, which survives `document.open()`, and before `doc.close()` so it cannot be missed — with a 1.5s fallback in case a subresource hangs; the loading overlay stays on until then; superseded refreshes clear the pending settle/re-measure timers, and the outgoing document's observer is disconnected before the write so its detached nodes cannot trigger a measure of the unstyled new document. The frame now goes from its previous height straight to the final one. Follow-up polish in the same release (user request: nothing in the admin may jump or resize abruptly): the frame starts compact (48px — below the 80px measure floor, so the first render can only grow it) instead of at an oversized 200px placeholder; every height change animates (0.25s ease, disabled under prefers-reduced-motion); the heavy white veil + large spinner were replaced by a small WordPress-size spinner with the frame content kept fully transparent while loading — the white wrapper shows through, and since the reveal happens at the frame's load event a document still loading its stylesheets can never flash unstyled (the veil used to mask that FOUC; removing it exposed the flash until the opacity gate restored the guarantee); and the preview document is written with `html { overflow: hidden }` so no scrollbar flashes while the frame is shorter than its content or animating — the resize logic restores scrolling only when the content exceeds the 700px height cap. The form editor preview keeps its existing veil + spinner treatment. * Fix: the preview document is written with a `` — without one the iframe rendered in Quirks Mode (jQuery is explicitly unsupported there and layout metrics differ; jQuery Migrate warned about it on every preview render). * Improved: submitting the Fields page editor form cancels the pending debounced preview refresh and aborts an in-flight preview request — typing the label and clicking "Add new field" within the 700ms debounce window fired a refresh that raced the page unload for nothing. If the client-side validation blocks the submit, the next input schedules a fresh refresh as usual. * Verification: submissions-list.spec.ts grew from 14 to 15 tests — an "Essential Columns button" describe seeds one flagged and one unflagged E2E field with stored values and asserts the click keeps ID/Actions/Submitted and the flagged field column while hiding the unflagged field, IP and Page (the admin's saved hidden-columns screen option is snapshotted and restored); the tooltip test now asserts the Email label and the smaller column set. fields-page.spec.ts grew from 29 to 30 — checkbox visibility per type (shown for textfield and unknown extension types, hidden for captcha/submit/html) and a full round trip: create with the flag → stored as 1 and present in essential_columns(), edit shows it pre-checked, uncheck → stored as 0 and absent. Full suite green. A Chromium automation gotcha surfaced by the new test is documented in the workspace testing skill: after any form-POST navigation the Playwright-bundled Chromium stops servicing requestAnimationFrame for that tab (reproduced on core options-general.php — no plugin code involved), so actionability-gated clicks on pages after a POST need force: true; not a plugin bug. * i18n: 2 new strings translated in the bundled it_IT and es_ES catalogs (POT/PO/MO regenerated). * Release housekeeping: 2.2.46 was not published to wordpress.org either (the public version is still 2.2.32), so the consolidated readme.txt entry is renamed to 2.2.47 and now includes the essential-columns feature and the preview sizing fix. CSS version 204, JS version 132, DB version 17. = 2.2.46 = * Fix: the submissions list Page and Referrer columns lost their compression — long URLs stretched the columns to the full URL width instead of collapsing behind the [+] expandable widget. Root cause: the 2.2.39 switch to content-based column sizing dropped the core 'fixed' table class, and without `table-layout: fixed` the CSS truncation on the cells (overflow + text-overflow) cannot constrain a table column, so `expandable-cells.js` never measured an overflowing cell and never built the `
` widget. The still-published 2.2.32 predates that change and truncates fine, which is how the regression surfaced (production compressed, dev not). The URL columns now carry an explicit `max-width: 15em` in admin.css — honored by auto table layout in all browsers — so truncation and the [+] widget work again while every other column keeps sizing to its content. The dashboard "Last 10 submissions" table was never affected (it kept the fixed class and a colgroup). * Feature: "Expand All Rows" and "Collapse All Rows" buttons in the submissions list toolbar next to "Essential Columns" — icon-only buttons with the action name in the tooltip (`title`) and in `aria-label` for screen readers. No dashicon conveys "expand/collapse rows" (the editor-expand/contract pair reads as fullscreen), so the icons are two new plugin SVGs (`assets/img/expand-rows.svg` / `collapse-rows.svg`: a bulleted row list with vertical arrows pointing outward, respectively inward), applied via CSS `mask` with `background-color: currentColor` so they take the button text color in every state. The buttons are flex containers (`inline-flex` + `align-items: center`), so the icon is centered exactly regardless of the `.button` line-height/min-height metrics that differ between WordPress releases. They open/close every [+] expandable URL cell on the page at once (new `accuaToggleAllRows()` in submissions-list.js; the per-cell toggle listeners keep handling the row-height styles). * Improved: the "Essential Columns" button explains itself — its tooltip lists the columns it keeps, with the same localized labels the table shows ("Keep only these columns visible: ID, Actions, Form, IP, Page, Referrer, Language, Submitted, Email"), built from `get_columns()` and skipping keys absent on the site. The column-key list moved to a single source of truth (`Accua_Forms_Submissions_List_Table::essential_columns()`), mirrored to `setEssentialColumns()` in submissions-list.js via `wp_localize_script` instead of being duplicated there. * Fix: browser find-in-page (Ctrl+F) counted every match inside a compressed URL cell twice (user-reported; e.g. searching a word contained in a collapsed referrer). The [+] widget was a `
`/`` holding the text twice — a truncated preview in the summary plus the full link in the details body — and both Chrome and Firefox search collapsed `
` content (that is also what lets them auto-open one on a match), so both copies counted, collapsed or expanded. The widget is rebuilt around a single text copy: the link itself, CSS-truncated while collapsed (`.accua-expandable-text`), with a real `[+]`/`[−]` toggle `