[Bug 🐞 + Fix] Form Builder: Post-submit Redirect Fails When Cloudflare Turnstile Captcha is Enabled - Question | JoomShaper

[Bug 🐞 + Fix] Form Builder: Post-submit Redirect Fails When Cloudflare Turnstile Captcha is Enabled

Brad Thompson

Brad Thompson

SP Page Builder 1 day ago

Summary

When a Form Builder (or Optin Form) add-on is configured with Cloudflare Turnstile as the captcha and a success redirect URL, the redirect never fires after a successful AJAX submission. The form also appears to "hang" — the submit button spinner stays visible and the button remains disabled. Other captcha types (reCAPTCHA, hCaptcha, POW Captcha) redirect correctly.

Steps to reproduce

  1. Add a Form Builder add-on to a page.
  2. In the add-on settings, enable Captcha and select Turnstile (requires the Joomla plg_captcha_turnstile plugin to be installed, configured with site/secret keys, and enabled).
  3. Enable Redirect and set a Redirect URL (e.g. /thank-you).
  4. Save and load the page on the front end.
  5. Fill in the form, complete the Turnstile widget, and submit.

Expected result

The form submits successfully, the success message is shown, and after ~2.5 seconds the browser redirects to the configured Redirect URL.

Actual result

  • The form submits and the server returns a success response, but:

    • The submit button spinner never clears and the button stays disabled.
    • The success message is not displayed.
    • No redirect occurs — the user remains on the same page.
  • Opening the browser DevTools console reveals an uncaught exception thrown during the AJAX success callback:

    Uncaught TypeError: Cannot read properties of undefined (reading 'split')
        at ... (sppagebuilder.js, in the success handler)

Disabling Turnstile (or switching to any other captcha type) immediately restores correct redirect behavior.

System information

  • Joomla: 6.1.3 Stable
  • SP Page Builder: 6.8.0
  • Captcha plugin: Cloudflare Turnstile (plg_captcha_turnstile, e.g. SharkyKZ's plugin or any plugin that renders <div class="cf-turnstile" ...> and lets Cloudflare's api.js implicitly inject the hidden cf-turnstile-response input)
  • Browser: any (the error is in SP Page Builder's JS, not browser-specific)

Root cause

The Turnstile captcha plugin renders a widget container div with an id, for example:

<div class="cf-turnstile sppb-dynamic-recaptcha" id="custom_captcha_123" data-sitekey="..."></div>

Cloudflare's api.js then implicitly injects a hidden input inside that div to carry the token:

<input type="hidden" name="cf-turnstile-response" value="TOKEN">

Per Cloudflare's documentation, this implicitly-injected input has a name but no id attribute. (See https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/widget-configurations/response-field-name controls the name; no id is set.)

The SP Page Builder front-end JS (components/com_sppagebuilder/assets/js/sppagebuilder.js) attempts to reset the Turnstile widget after a successful AJAX submission. The reset code (minified; present in 3 handlers — ajax_contact, form_builder, optin_form) does this:

var c = form.find("#custom_captcha_" + response.gcaptchaId.split("_")[2] + " input").attr("id").split("-")[3].split("_")[0];
c ? turnstile.reset(c) : turnstile.reset();

Step-by-step failure:

  1. form.find("#custom_captcha_123 input") → finds the hidden cf-turnstile-response input.
  2. .attr("id") → returns undefined (the input has no id).
  3. .split("-") → throws TypeError: Cannot read properties of undefined (reading 'split').

Because this line is inside the AJAX success callback, the uncaught exception aborts the entire callback. The subsequent statements — which clear the spinner, re-enable the submit button, show the success message, and schedule the redirect — never execute:

form.find(".fa-spin").removeClass("fa-spinner fa-spin"),
form.find('button[type="submit"]').prop("disabled", false),
form.next(".sppb-ajax-contact-status").html(content).fadeIn().delay(4000).fadeOut(500),
response?.status && redirect === "yes" && setTimeout(function(){ window.location.href = redirectUrl }, 2500)  // never reached

This is why the symptom is specific to Turnstile: the reCAPTCHA, hCaptcha, and POW Captcha branches use different reset logic that does not throw.

Proposed fix

The fix has two parts:

  1. Read the widget id from the correct element. turnstile.reset(id) expects the widget id — i.e. the id of the .cf-turnstile container div, which the captcha plugin always sets. The original code tried to derive it from the id-less hidden input. The fix reads it from the .cf-turnstile container directly.

  2. Wrap the turnstile.reset() call in a try/catch. Even with the correct widget id, turnstile.reset() can throw in certain widget states (e.g. after the token has already been consumed by the server-side verification, or if the widget has been implicitly rendered and the id is not registered as an explicit widget id). Because this code runs inside the AJAX success callback, any throw — even from reset() — aborts the entire callback and prevents the redirect. The try/catch ensures the rest of the success callback (spinner removal, button re-enable, success message, redirect) always executes, even if the reset fails.

Handler 1 — ajax_contact (latent: throws a console error on every successful Turnstile submission; no redirect feature, but the error is noise and the success UI is still broken)

Before:

else if("turnstile"==f){var p=a.find("#custom_captcha_"+n.gcaptchaId.split("_")[2]+" input").attr("id").split("-")[3].split("_")[0];p?turnstile.reset(p):turnstile.reset()}

After:

else if("turnstile"==f){var p=a.find(".cf-turnstile").attr("id");try{p?turnstile.reset(p):turnstile.reset()}catch(e){}}

Handler 2 — form_builder (the reported redirect bug)

Before:

else if("turnstile"==g){var c=i.find("#custom_captcha_"+s.gcaptchaId.split("_")[2]+" input").attr("id").split("-")[3].split("_")[0];c?turnstile.reset(c):turnstile.reset()}

After:

else if("turnstile"==g){var c=i.find(".cf-turnstile").attr("id");try{c?turnstile.reset(c):turnstile.reset()}catch(e){}}

Handler 3 — optin_form (same redirect bug for optin forms)

Before:

else if("turnstile"==v){var h=i.find("#custom_captcha_"+o.gcaptchaId.split("_")[2]+" input").attr("id").split("-")[3].split("_")[0];h?turnstile.reset(h):turnstile.reset()}

After:

else if("turnstile"==v){var h=i.find(".cf-turnstile").attr("id");try{h?turnstile.reset(h):turnstile.reset()}catch(e){}}

Why this is correct and safe

  • The Turnstile plugin always renders <div class="cf-turnstile" id="..."> — that id is the widget id Cloudflare's turnstile.reset(id) expects.
  • It removes the brittle .split("-")[3].split("_")[0] chain that assumed a specific id format on the hidden input (which has no id at all).
  • The try/catch ensures that even if turnstile.reset() throws for any reason (invalid widget id, widget already destroyed, token already consumed), the rest of the success callback still runs — the spinner is cleared, the submit button is re-enabled, the success message is shown, and the redirect fires. The captcha reset is a cosmetic convenience (so the user can submit again without reloading the page); it is not security-critical and must not be allowed to break the success flow.
  • The else fallback (turnstile.reset() with no argument → resets all widgets on the page) is preserved for the theoretical case where no .cf-turnstile element is found.
  • No other captcha branches (reCAPTCHA / hCaptcha / POW Captcha / default) are touched.

Files modified

  • components/com_sppagebuilder/assets/js/sppagebuilder.js — 3 single-line edits (one per AJAX submit handler: ajax_contact, form_builder, optin_form).

Verification

After applying the fix:

  1. Clear Joomla cache (System → Clear Cache) — the JS is loaded with a versioned query string.
  2. Load a Form Builder page with Turnstile enabled + redirect configured; submit the form.
  3. Confirm: spinner stops, submit button re-enables, success message shows, and after ~2.5s the browser redirects to the configured URL.
  4. Open DevTools console during submission — confirm no TypeError is thrown.
  5. Repeat with an Optin Form using Turnstile + redirect.
  6. Sanity-check a reCAPTCHA (non-Turnstile) form still resets and redirects normally.
0
0 Answers