Hi,
I needed Turnstile on Elementor Pro's Form widget, so I built it as a module in the style of your existing Turnstile ones, against 1.10.0. Sharing it in case you want it.
Thanks for the plugin, it's doing a lot of work for us.
I needed Turnstile on Elementor Pro's Form widget, so I built it as a module in the style of your existing Turnstile ones, against 1.10.0. Sharing it in case you want it.
The gap
Elementor Pro only ships reCAPTCHA v2/v3 natively, but it already hard codes aturnstile field slug in its own validation strip lists (form-record.php, the skip_all_strip_types default). It clearly expects a third party to provide one. Your MetForm module hooks the same elementor/widget/render_content filter, but it checks for MetForm's button widget, so Elementor Pro's own Form widget isn't covered.What's attached
One new file plus three small diffs:ElementorPro.php, goes insrc/DigitalPoint/Cloudflare/Turnstile/setup-defaults.diff:'onElementorPro' => 0default in Setup.phppub-wiring.diff:isPluginActivegate in Base/Pub.phpsettings-toggle.diff: checkbox in the Contact forms group in Settings.php
addTurnstileScript() on the same turnstile handle, addTurnstileHtml(), getTurnstileResponse(), generateError() so the strings and translations match the other integrations, and getCloudflareRepo()->verifyTurnstileResponse(). No HTTP or options code of its own, and it's PHP 5.4 clean.Why it's built the way it is
A few things are specific to how Elementor works and worth knowing about:- The token is read from
$_POSTviagetTurnstileResponse(), not from theForm_Record. Elementor rebuilds the form definition from the stored post meta at submit time, so an injected widget never appears in the record. Nice side effect: the token can't leak into the email or the Submissions table. - There's an explicit editor/preview bail. The Elementor editor canvas and the preview iframe are ordinary frontend requests, so the render filter runs in both.
- The widget is rendered explicitly against a unique per widget id instead of relying on the implicit
.cf-turnstilescan. Elementor submits over AJAX and never reloads the page, and tokens are single use with a 300 second life, so every attempt needs aturnstile.reset(), which needs the widgetId thatturnstile.render()returns. The implicit scan doesn't hand one back. The reset is bound to Elementor's ownreset/errorform events (the same ones their native reCAPTCHA uses), pluselementor/popup/showfor popup forms. - Errors go through
$ajaxHandler->add_error_message()rather thanadd_error(), because an injected widget has no#form-field-{id}anchor for a per field error to attach to. It renders as a form level message instead. - The script handle is registered (and the inline script attached) on every request via
wp_enqueue_scripts, not just at render time. Elementor's element cache serves widget HTML from cache and re-enqueues that page's scripts by handle only, without runningrender_contentagain. If the handle isn't registered on a cache hit, the widget on a cached page silently never renders while validation stays live. Registration is weightless until something actually enqueues it.
Things I noticed along the way
Turnstile/MetForm.phphardcodesid="cf-turnstile"and callsturnstile.render("#cf-turnstile")unconditionally, so on a page with more than one MetForm only the first widget renders. The samestr_replaceI'm using, with the widget id folded into the id, fixes it.- The
app_for_cf_turnstile_formsfilter is a really nice extension point and I couldn't find it documented anywhere. I'm using it in production right now to bridge this until it hopefully lands natively: add the toggle to the array andsanitizeSettingspersists the checkbox with no extra save code. Might deserve a readme mention next to the otherapp_for_cf_*filters. - Optional and entirely take it or leave it: the Turnstile docs recommend also validating
hostname(andactionwhere one is set) in the siteverify response. All six current integrations check onlysuccess, so I kept parity rather than making this module behave differently, but it'd be a reasonable plugin wide hardening. - Unrelated trap worth knowing about: enabling
onLoginon a site that uses Elementor Pro's Login widget locks users out. That widget posts to wp-login.php without a Turnstile widget ever being rendered on the page. Not something I've tried to fix.
Testing
Verified so far: the widget renders once per form, placed just before the submit button; api.js loads exactly once; both failure paths via direct POSTs to admin-ajax.php come back with the right errors ("No Turnstile response" with no token, "Invalid Turnstile challenge" with a garbage one, which also proves the siteverify round trip); and cached pages serve the widget correctly.Thanks for the plugin, it's doing a lot of work for us.