Releases: ChoiceOMG/choice-uft
Release list
Version 3.28.4
Fixed
- Click tracking table now installs on hosts whose default storage engine is MyISAM (or InnoDB with the old 767/1000-byte index limit). The unique index on click_id covered all 255 utf8mb4 characters (1,020 bytes), which MySQL rejects there with "Specified key was too long; max key length is 1000 bytes", so the table was never created and no clicks were recorded. The index now covers the first 191 characters; click IDs are far shorter, so uniqueness is unchanged, and existing tables are not altered.
Installation
Automatic Update
If you already have the plugin installed, it should automatically detect this update. Go to Plugins in your WordPress admin and click Update Now.
Manual Installation
- Download the
choice-uft-v3.28.4.zipfile from the assets below - In WordPress admin, go to Plugins → Add New → Upload Plugin
- Choose the downloaded zip file and click Install Now
- Activate the plugin after installation
Requirements
- WordPress 5.0 or higher
- PHP 7.4 or higher
Support
For issues or questions, please visit: https://github.com/ChoiceOMG/choice-uft/issues
Version 3.28.3
Fixed
- Click table repair now records MySQL's own error text. 3.28.2 read the error after running its existence check, which clears it, so failures showed "Unknown database error". When the table is still missing after dbDelta(), the CREATE is run once directly to capture the error, and a failed repair waits an hour before retrying instead of retrying on every admin page load.
Installation
Automatic Update
If you already have the plugin installed, it should automatically detect this update. Go to Plugins in your WordPress admin and click Update Now.
Manual Installation
- Download the
choice-uft-v3.28.3.zipfile from the assets below - In WordPress admin, go to Plugins → Add New → Upload Plugin
- Choose the downloaded zip file and click Install Now
- Activate the plugin after installation
Requirements
- WordPress 5.0 or higher
- PHP 7.4 or higher
Support
For issues or questions, please visit: https://github.com/ChoiceOMG/choice-uft/issues
Version 3.28.2
Fixed
- Click writes that fail leave no trace.
CUFT_Click_Tracker::track_click()only logged on success; a failed insert or update returnedfalsesilently, andCUFT_Click_Tracker::create_table()trusted dbDelta's own report (which logs "Created table" before it runs the query and does not surface a failedCREATE TABLE). A write failure is now logged throughCUFT_Loggerat error level and recorded, with no PII (no IP, no user agent, only a short prefix of the click id), in a smallcuft_last_click_write_erroroption, surfaced as a notice on the Click Tracking admin page. - Error-level log entries are now recorded even with debug logging off.
CUFT_Logger::log()previously discarded every entry, including failures, unless "Enable Debug Logging" was already on, so a site had to be reconfigured before a failure could be seen. Error-level entries now bypass that gate; every other level still requires debug logging. - Schema self-heal. A site whose click tracking table went missing, or whose table predates the
events(3.12.0) orga_client_id(3.22.0) columns, is repaired automatically:CUFT_Click_Tracker::self_heal_schema()runs onadmin_initand after a version change, verifies with a realSHOW TABLES/SHOW COLUMNScheck (not just what a migration recorded), and creates or alters what is missing. The result is cached per plugin version so a healthy site costs no extra query on repeat admin page loads. - Click Tracking admin list hid real rows by default.
filter_has_eventsdefaulted to "Has Events", which excludes a gclid-only visit that never fired a form event; a site with real click rows could look empty. Defaults to "All" now; the filter is still available. The stats tiles and CSV exports already shared the same query, so they are consistent by construction.
Fixed (internal)
CUFT_Logger::log()now tolerates the historical call shape used across this codebase,CUFT_Logger::log( 'error', 'message' )(level and message reversed from the documented signature), by detecting and normalising it, so those entries are stored under the level the caller meant.
Installation
Automatic Update
If you already have the plugin installed, it should automatically detect this update. Go to Plugins in your WordPress admin and click Update Now.
Manual Installation
- Download the
choice-uft-v3.28.2.zipfile from the assets below - In WordPress admin, go to Plugins → Add New → Upload Plugin
- Choose the downloaded zip file and click Install Now
- Activate the plugin after installation
Requirements
- WordPress 5.0 or higher
- PHP 7.4 or higher
Support
For issues or questions, please visit: https://github.com/ChoiceOMG/choice-uft/issues
Version 3.28.1
Fixed
generate_leadfired twice for any lead with email, phone and a click ID.cuft-dataLayer-utils.jspushed the broadgenerate_lead(email present) and then, inside thequalify_leadblock, dual-fired a secondgenerate_leadwith the strict payload andcuft_deprecated: true/cuft_migrate_to: "qualify_lead". That dual-fire was meant to last one version after the April 2026 event rename (3.27.0) and was never removed. It is gone;generate_leadnow pushes and records at most once per submission.qualify_leadis unchanged, and thecuft_generate_lead_enabledgating added in 3.28.0 still applies. A GTM trigger on the event namegenerate_leadalone needs no change beyond seeing one event per lead instead of two; a trigger conditioned oncuft_deprecatedequalstruewill stop firing and should be migrated toqualify_lead, which was always the intended replacement.- Gravity Forms pushed
generate_leada second, independent way.CUFT_Gravity_Forms::track_submission()(PHP, hooked togform_after_submission) also generated an inlinegenerate_leaddataLayer push, on top of the client-side push every framework already fires viacuft-dataLayer-utils.js. For Elementor Pro, CF7 and Ninja Forms the equivalent server-side inline script never reaches the browser (their forms submit by AJAX, so the hook fires on a request whose response carries no enqueued script tag), but Gravity Forms can complete the same request as a full page render, in which case the inline script did print and run. Removed the server-sidegenerate_lead_event()push; Gravity's server-sideform_submitpush is unaffected.
Installation
Automatic Update
If you already have the plugin installed, it should automatically detect this update. Go to Plugins in your WordPress admin and click Update Now.
Manual Installation
- Download the
choice-uft-v3.28.1.zipfile from the assets below - In WordPress admin, go to Plugins → Add New → Upload Plugin
- Choose the downloaded zip file and click Install Now
- Activate the plugin after installation
Requirements
- WordPress 5.0 or higher
- PHP 7.4 or higher
Support
For issues or questions, please visit: https://github.com/ChoiceOMG/choice-uft/issues
Version 3.28.0
WordPress.org directory review preparation. Plugin Check (general, plugin_repo, security, performance, accessibility) on the directory package went from 635 warnings to none.
Changed
generate_leadobeys the Generate Lead Events setting.cuft-dataLayer-utils.jspushed it on every submission with an email address whatevercuft_generate_lead_enabledsaid. The setting now reaches the browser aswindow.cuftLeadSettings(inline beforecuft-dataLayer-utils), and gates the push, the deprecated strict dual-fire, and therecordEventcall;cuft_record_eventalso refusesgenerate_leadwhile the setting is off. A page cached before the upgrade, with nocuftLeadSettings, keeps pushing as it did.- Upgrade defaults (migration 3.28.0,
cuft_db_versionnow 3.28.0). An existing install getscuft_generate_lead_enabled = 1andcuft_webhook_require_key = 0, so it behaves as before; a new install getscuft_generate_lead_enabled = 0andcuft_webhook_require_key = 1with a generated key. Existing means: a storedcuft_db_version; or, during activation,cuft_db_versionorcuft_gtm_idpresent before activation wrote its defaults; or, outside activation,cuft_gtm_idpresent. - Requires WordPress 6.2. Every query goes through
$wpdb->prepare()with identifiers bound by%i. - Main class renamed
Choice_Universal_Form_TrackertoCUFT_Plugin, andAbstract_CUFT_AdaptertoCUFT_Abstract_Adapter, for the directory's prefix rule. - Plugin URI now points at https://choice.marketing/tools/choice-uft/; License header reads
GPLv2 or later. - Admin notices show only on the plugin's screens and the Plugins screen; the informational notice is dismissible per user.
- Inline admin scripts and styles moved to
assets/cuft-admin.jsandassets/cuft-admin.css; the GTM loader is enqueued throughwp_add_inline_script. error_log()calls route throughCUFT_Logger::debug_log(), which writes only underWP_DEBUG.- readme
== External services ==rewritten per service, with terms and privacy links for the Choice OMG phone validation service and its sub-processors.
Security
- Webhook key. New setting "Require webhook key" on the Click Tracking screen. When on,
cuft_webhookrejects any request whosekeydoes not matchcuft_webhook_key(hash_equals), with HTTP 403. The screen shows the full keyed URL and a nonce-checked Regenerate button; the "security through obscurity" wording is gone. - Test mode, which let any visitor append
?test_mode=1and suppress form notification emails, is removed with the Test Form Builder. - Nonce plus
manage_optionson every admin action and AJAX handler; request, cookie and server input unslashed and sanitized. - CSV exports neutralise spreadsheet formulas in visitor-supplied columns.
Removed
- Test Form Builder. Framework adapters, adapter factory, form builder and its AJAX endpoints, the test form templates, sessions and validator, test mode (
?test_mode=1), the/cuft-test-form/rewrite rule and theform_id/test_modequery vars, and their Testing Dashboard card and scripts. The sample data generator, event simulator and test events table remain. Test forms made by earlier versions are deleted on upgrade and uninstall byCUFT_Legacy_Test_Forms, which matches only posts carrying_cuft_test_form = 1and acuft_test_<time>_<4 digits>instance ID, never titles. - The Gravity Forms and Ninja Forms scripts' test-mode blocks, which wrote fake
gclidand UTM values into session storage on any URL containingtest=1,cuft_test=1or-test-form.
Fixed
- Stray translator comment printed on the Testing Dashboard.
- Migration index check looked for
date_updatedwhile creatingidx_date_updated, so a second run failed. CUFT_Logger::log()was called with its arguments reversed in the click tracker and event recorder.- Uninstall cleared a cron hook the plugin never scheduled and left the real ones, the test events table, and transient timeouts behind.
- The directory package hides the GTM template download buttons, whose files it does not ship.
Installation
Automatic Update
If you already have the plugin installed, it should automatically detect this update. Go to Plugins in your WordPress admin and click Update Now.
Manual Installation
- Download the
choice-uft-v3.28.0.zipfile from the assets below - In WordPress admin, go to Plugins → Add New → Upload Plugin
- Choose the downloaded zip file and click Install Now
- Activate the plugin after installation
Requirements
- WordPress 5.0 or higher
- PHP 7.4 or higher
Support
For issues or questions, please visit: https://github.com/ChoiceOMG/choice-uft/issues
Version 3.27.1
Fixed
- A cached page could silently cost a visitor their attribution. The
cuft_store_utmcall that writes the UTM and click-id cookies carries a nonce printed into the page HTML, and a page cache can serve that HTML for longer than WordPress keeps a nonce valid (24 hours). Past that point the call was rejected, no cookie was written, and every form submission from that page arrived with no attribution and no error recorded anywhere. The cookies are now written by the page itself, in the format the server already reads, so attribution survives a stale cached page. Verified against a page whose store call returns 403: the cookie is written and the webhook carries the full attribution set.
Security
- The
cuft_store_utmendpoint keeps its nonce check. Removing it would let any third-party site forge attribution cookies for a visitor through the endpoint, and the client-side write closes the cache gap without weakening that.
Installation
Automatic Update
If you already have the plugin installed, it should automatically detect this update. Go to Plugins in your WordPress admin and click Update Now.
Manual Installation
- Download the
choice-uft-v3.27.1.zipfile from the assets below - In WordPress admin, go to Plugins → Add New → Upload Plugin
- Choose the downloaded zip file and click Install Now
- Activate the plugin after installation
Requirements
- WordPress 5.0 or higher
- PHP 7.4 or higher
Support
For issues or questions, please visit: https://github.com/ChoiceOMG/choice-uft/issues
Version 3.27.0
Fixed
- Attribution never reached the stored Elementor submission.
add_attribution_to_record()was hooked onelementor_pro/forms/new_record, which Elementor Pro fires after every submit action has already run, including the one that saves the entry. The hidden fields it added therefore arrived too late for the Submissions module and for the webhook, and had never appeared on a stored entry since 3.24.0. Attribution is now captured onelementor_pro/forms/record/actions_before, which runs in the visitor's request before any action. submitted_atreported the time a later action fired, not the submit time. Elementor runs actions in sequence in one request, so a slow action ahead of the webhook pushed the timestamp out by however long it took; on one site a mail service added a steady 11 seconds. The payload is now assembled once per submission and reused, sosubmitted_at, the stored entry and the webhook all carry the same submit time.
Added
tests/unit/test-elementor-attribution-capture.php: drives Elementor's real hook sequence (record/actions_before, the actions,new_record) and covers the capture, the reuse of the captured payload when the cookies are gone, timestamp stability, the live-request fallback, and the field cleanup described below.
Security
- Attribution added to the record for the Submissions module is removed again as soon as the entry is stored, so it cannot reach a notification email. Elementor's
[all-fields]shortcode prints every field on the record with no filtering. The injection is also skipped entirely unless the form stores submissions and the Submissions action is ordered ahead of both email actions, verified per submission rather than assumed.
Installation
Automatic Update
If you already have the plugin installed, it should automatically detect this update. Go to Plugins in your WordPress admin and click Update Now.
Manual Installation
- Download the
choice-uft-v3.27.0.zipfile from the assets below - In WordPress admin, go to Plugins → Add New → Upload Plugin
- Choose the downloaded zip file and click Install Now
- Activate the plugin after installation
Requirements
- WordPress 5.0 or higher
- PHP 7.4 or higher
Support
For issues or questions, please visit: https://github.com/ChoiceOMG/choice-uft/issues
Version 3.26.1
Fixed
- Avada forms pushed
form_submiton pages nobody submitted.isAvadaSuccessState()testedelement.style.display !== "none", which reads only the inline style attribute. Avada renders.fusion-form-response-successinto the static markup and hides it with a CSS class carrying no style attribute, so the value was"", the check passed about 50 ms after the page loaded, and every render that started an observation produced a submission event. Measured on one client: 29 contact-less rows against 32 real leads in a month, driven by OEM lock-screen ad renderers (com.samsung.android.dynamiclock,com.hihonor.magazine,com.heytap.pictorial) and Meta's ad crawler re-rendering the landing page, each phantom carrying the ad'sgclid. Visibility is now read from computed style plus layout, and every success signal must transition from absent to present after the submission, so a message that was already on the page cannot register as one. - Avada submissions with neither an email address nor a phone number no longer push
form_submit. The framework module already requires an email field before it tracks a form at all, so a submission reaching this state is a false positive, not a name-only form. - The Avada click watcher listened on
.fusion-button, which on a typical Avada page matches dozens of ordinary buttons (32 on the client contact page). It now binds to real submit controls only.
Added
tests/standalone/test-avada-success-detection.html: browser regression cover for the above. A hidden-by-class success node must not fire, a success node becoming visible must fire, a contact-less success must be suppressed, and a non-submit Fusion button must start no observation.
Changed
trackFormSubmission()accepts an opt-inrequire_contactoption that suppresses aform_submitcarrying neither email nor phone. Only the Avada module sets it. It is off by default because the dataLayer specification requiresform_submiton every real submission, name-only and multi-step forms included, so the other frameworks are untouched.
Removed
- Auto-BCC email system (Feature 010): Deleted the 11 files it left behind. The feature was dropped from the plugin loader in 3.22.0, which orphaned
includes/email/, the AJAX handler, the settings view, and its CSS and JS in the tree. Nothing required them and there is no autoloader, so those classes were never defined, theclass_exists()guards inchoice-universal-form-tracker.phpnever fired, and the code could not run on any install. The dead init block went with it.uninstall.phpalready sweepscuft_%options and transients, so acuft_auto_bcc_configrow left by a pre-3.22.0 install is still cleaned up on uninstall.
Installation
Automatic Update
If you already have the plugin installed, it should automatically detect this update. Go to Plugins in your WordPress admin and click Update Now.
Manual Installation
- Download the
choice-uft-v3.26.1.zipfile from the assets below - In WordPress admin, go to Plugins → Add New → Upload Plugin
- Choose the downloaded zip file and click Install Now
- Activate the plugin after installation
Requirements
- WordPress 5.0 or higher
- PHP 7.4 or higher
Support
For issues or questions, please visit: https://github.com/ChoiceOMG/choice-uft/issues
Version 3.26.0
Added
- WordPress.org distribution:
build.shnow produces two packages from one tree../build.shbuilds the GitHub release exactly as before;./build.sh --wporgbuilds the directory package, which omits the self-update subsystem because directory guideline 8 forbids a hosted plugin from serving its own updates. The extra exclusions live in.wporgignore. Choice_Universal_Form_Tracker::has_updater()reports whether the update subsystem is bundled. Loading, the Force Update tab, its assets, and the custom update notice all check it, so the directory build degrades to core-managed updates instead of failing.build.shfails the build on a version mismatch across the three version sources, on hidden files, on em-dashes in shipped files, and (for--wporg) on any self-update file or third-party CDN reference reaching the package.readme.txtgained an== External services ==section itemising every third-party endpoint the plugin can contact, what is transmitted, when, and under which terms and privacy policy.
Changed
- SHA-256 now ships with the plugin. CryptoJS was loaded from
cdnjs.cloudflare.com, which guideline 8 prohibits and which introduced a race: a slow CDN response leftlead_idoff the event entirely. The bundled implementation is synchronous, solead_idis always available when the payload is built. Digests are unchanged and still match PHPhash('sha256'). - Declared PHP 7.4 consistently. The plugin header had no
Requires PHPat all,readme.txtclaimed 7.4, and the runtime notice named 7.0. - Added
Plugin URI,Requires at least,Requires PHP, andLicense URIto the plugin header. - Text domain corrected to
choice-universal-form-trackerin 275 places that usedchoice-uft, which did not match the declaredText Domainheader and left those strings untranslatable. readme.txttags reduced from 12 to the 5 WordPress.org permits, and the six that were other companies' trademarks removed. Framework compatibility is described in the Description body instead.
Fixed
- GitHub updates were silently switched off on every settings save. The checkbox that
save_settings()read was removed in Feature 008, so the value was always false and got written back over the stored option. save_settings()now verifies the settings nonce itself rather than relying on its caller, and unslashes$_POSTvalues before sanitising them.- Escaped previously unescaped output across the admin screens, the GTM injector, and the testing dashboard.
- Replaced
date()withgmdate(),rand()withwp_rand(), andparse_url()withwp_parse_url(). /llms.txt,/ai.txt, and/llms-full.txtnow sendX-Content-Type-Options: nosniff.
Installation
Automatic Update
If you already have the plugin installed, it should automatically detect this update. Go to Plugins in your WordPress admin and click Update Now.
Manual Installation
- Download the
choice-uft-v3.26.0.zipfile from the assets below - In WordPress admin, go to Plugins → Add New → Upload Plugin
- Choose the downloaded zip file and click Install Now
- Activate the plugin after installation
Requirements
- WordPress 5.0 or higher
- PHP 7.4 or higher
Support
For issues or questions, please visit: https://github.com/ChoiceOMG/choice-uft/issues
Version 3.25.0
Added
- Shared cross-system lead ID (OPS-2210): every lead now carries one deterministic
lead_idacross the webhook payload, the stored entry, the email notification, and the analytics event, so a booked client in the CRM can be matched back to the campaign that produced them.lead_idis the lowercase sha256 hex of the normalized email (trim + lowercase). This matches Google Enhanced Conversions and Meta CAPI email normalization, so the same hash doubles as a cross-platform match key.- Phone fallback: when a submission has no email,
lead_idis the sha256 of the phone normalized to E.164 (+<digits>, NANP default, filterable viacuft_lead_id_phone_country). - New
lead_id_sourcefield records the basis (emailorphone) so downstream consumers know which value was hashed. Both fields are omitted when neither email nor phone is present. - New
CUFT_Form_Attribution::lead_id_from_email()andlead_id_from_phone()helpers; final value filterable viacuft_lead_id. - Added to the Elementor server-side
form_submitandgenerate_leaddataLayer pushes, and computed client-side via CryptoJS for the analytics event (PHP and JS produce identical digests).
Why
Closes OPS-2210 and unblocks downstream offline-conversion import (OPS-2211) and lead reporting (OPS-2212): a deterministic hash lets WordPress, n8n, Cliniko, QuickBooks, and the ad platforms all derive the same identifier from the same person without passing a generated UUID around, while keeping raw PII out of analytics.
Installation
Automatic Update
If you already have the plugin installed, it should automatically detect this update. Go to Plugins in your WordPress admin and click Update Now.
Manual Installation
- Download the
choice-uft-v3.25.0.zipfile from the assets below - In WordPress admin, go to Plugins → Add New → Upload Plugin
- Choose the downloaded zip file and click Install Now
- Activate the plugin after installation
Requirements
- WordPress 5.0 or higher
- PHP 7.4 or higher
Support
For issues or questions, please visit: https://github.com/ChoiceOMG/choice-uft/issues