We have been using this module successfully up until 1.1.13. Since the update the capture for turnstile no longer loads. This has been thoroughly testes on Drupal 10.3 + and Drupal 11 website we run and maintain.

So something invalidate here:
https://git.drupalcode.org/project/turnstile/-/compare/1.1.13...1.1.14?f...

I assume the attach has something to do with it?

Issue fork turnstile-3513543

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

  • 1.0.x Comparecompare

Comments

glynster created an issue. See original summary.

greatmatter’s picture

What error (if any) are you receiving?

glynster’s picture

I am debugging at the moment to try and be useful. There are no console errors. However here are some observations:

Locally it does load fine.
On production it does not.
Markup wise I can see

<div class="cf-turnstile" data-sitekey="################" data-theme="light" data-size="normal" data-language="auto" data-retry="auto" interval="8000" data-appearance="always"></div>

but on version 1.1.13 the markup is:

<div class="cf-turnstile" data-sitekey="################" data-theme="light" data-size="normal" data-language="auto" data-retry="auto" interval="8000" data-appearance="always"><div><input type="hidden" name="cf-turnstile-response" id="cf-chl-widget-b5dkd_response" value="TOKEN"></div></div>

greatmatter’s picture

I think I got it--I've just pushed a hotfix to 1.1.16

glynster’s picture

So it seems this actually fixes the issue:

1. Run This in Browser Console to Force a Reset

document.querySelectorAll('.cf-turnstile').forEach(el => {
turnstile.remove(el);
});
turnstile.render('.cf-turnstile', { sitekey: "######" });

2. Modify the Form Rendering Code to Prevent Duplicate Initialization

To avoid this problem, modify your Drupal form rendering:

$form['turnstile_widget'] = [
  '#markup' => '<div id="turnstile-container" class="cf-turnstile" data-sitekey="' . turnstile_site_key() . '"></div>',
];

$form['#attached']['drupalSettings']['turnstile'] = [
  'callback' => 'turnstileSuccess',
  'errorCallback' => 'turnstileError',
];

$form['#attached']['library'][] = 'turnstile/turnstile';

Then add this script in turnstile.ajax.js to remove any existing Turnstile instances before rendering a new one:

document.addEventListener("DOMContentLoaded", function () {
  const turnstileContainer = document.querySelector('.cf-turnstile');
  if (turnstileContainer) {
    turnstile.remove(turnstileContainer);
    turnstile.render(turnstileContainer, {
      sitekey: drupalSettings.turnstile.sitekey
    });
  }
});
greatmatter’s picture

Would you mind letting me know if 1.1.16 fixes it for you?

glynster’s picture

@greatmatter thanks so much for being so responsive. I just deployed with your new hotfix and sadly I get the same results, fails to load/initialize.

As soon as I run this in the console on production it loads:
turnstile.render('.cf-turnstile', { sitekey: "#################" });

greatmatter’s picture

How odd--I've tested it across our sites--would you mind clearing your caches (including edge caches, if any)?

glynster’s picture

No luck on my end We have cleared caches Drupal, Varnish, Cloudflare, etc. The only fix is this:

turnstile.render('.cf-turnstile', { sitekey: "#################" });

With the current version or revert to 1.1.13.

greatmatter’s picture

Please try resaving your keys--I'm wondering if something weird happened with the upgrading/downgrading.

glynster’s picture

I wish I could have good news and it be that simple but it is not.

This seems to work:

/**
 * @file
 * JavaScript behaviors for Turnstile.
 */
(function (Drupal, once) {
  'use strict';

  /**
   * Ensures Turnstile renders correctly on page load & AJAX events.
   */
  Drupal.behaviors.turnstileAutoRender = {
    attach: function (context) {
      once('turnstile-init', '.cf-turnstile', context).forEach((el) => {
        turnstile.render(el);
      });
    }
  };

})(Drupal, once);
glynster’s picture

Version: 1.1.15 » 1.1.16
greatmatter’s picture

I cannot seem to replicate this issue. The code you sent will likely break the Form/AJAX functionality (using EventSubscriber) introduced in 1.1.10.

I'm wondering if, for some reason, the

isn't getting automatically called by the script. Do you have multiple CAPTCHAs on the same page?
glynster’s picture

Only 1, nothing fancy going on here. These are just basic webforms and GIn Login pages.

glynster’s picture

Okay well all our sites are behind Cloudflare and as soon as we disable RocketLoader it works. But we have always used this service. Since the change this is now conflicting it seems.

keiserjb’s picture

I was getting cookie errors in the Chrome console.

Reading cookie in cross-site context may be impacted on Chrome
Cookies with the SameSite=None; Secure and not Partitioned attributes that operate in cross-site contexts are third-party cookies. Chrome is moving towards a new experience that allows users to choose to browse without third-party cookies.

Learn more from the linked article about preparing your site to avoid potential breakage.

It listed multiple cookies from the the cloudflare domain.

Thanks for the tip about Rocket Loader in Cloudflare. I turned that off and it started working.

keiserjb’s picture

Rolling back to 1.1.13 works as well.

greatmatter’s picture

Hm. I wonder why it's working in our sites. We're behind Cloudflare, but don't use RocketLoader. We also have SameSite=Lax - so I'm wondering if that plays a role. I'm open to ideas of how to make this work.

@keiserjb - what errors are you getting in the console?

The big change between versions was how the main Cloudflare Turnstile script was being included. We could add to the turnstile_library_info_build() the defer and async attributes--would you mind testing that to let me know if that changes anything? I'm not going to make a patch before knowing if it works for you.

EDIT: here's the change to 1.1.16:

/**
 * Implements hook_library_info_build().
 */
function turnstile_library_info_build() {
  $config = \Drupal::config('turnstile.settings');
  $turnstile_src = $config->get('turnstile_src');

  $libraries = [];

  $libraries['turnstile.remote'] = [
    'js' => [
      $turnstile_src => [
        'type' => 'external',
        'attributes' => [
          'defer' => TRUE,
          'async' => TRUE,
        ],
      ],
    ],
  ];

  return $libraries;
}
glynster’s picture

@greatmatter you are referring to the services file right?

parameters:
  session.storage.options:
    cookie_samesite: Lax

We already have this set on our sites, so we can rule that one out.

I can confirm that updating the widget does resolve the issue

/**
 * Build the Turnstile captcha form.
 *
 * @return mixed
 *   The return value.
 */
public function getWidget($validation_function) {
  // Add Turnstile script with async and defer attributes.
  $widget['form']['#attached']['html_head'][] = [
    [
      '#tag' => 'script',
      '#attributes' => [
        'src' => 'https://challenges.cloudflare.com/turnstile/v0/api.js',
        'async' => 'async',
        'defer' => 'defer',
      ],
    ],
    'turnstile_api',
  ];

  // Captcha requires TRUE to be returned in solution.
  $widget['solution'] = TRUE;
  $widget['captcha_validate'] = $validation_function;
  $widget['form']['captcha_response'] = [
    '#type' => 'hidden',
    '#value' => self::EMPTY_CAPTCHA_RESPONSE,
  ];

  // Allows the CAPTCHA to be displayed on cached pages.
  $widget['cacheable'] = TRUE;

  // Ensure the widget is properly rendered.
  $widget['form']['turnstile_widget'] = [
    '#markup' => '<div' . $this->getAttributesString() . '></div>',
  ];
  
  return $widget;
}
glynster’s picture

Yes this suggestion resolves the issue and it much better that what I suggested:

/**
 * Implements hook_library_info_build().
 */
function turnstile_library_info_build() {
  $config = \Drupal::config('turnstile.settings');
  $turnstile_src = $config->get('turnstile_src');

  $libraries = [];

  $libraries['turnstile.remote'] = [
    'js' => [
      $turnstile_src => [
        'type' => 'external',
        'attributes' => [
          'defer' => TRUE,
          'async' => TRUE,
        ],
      ],
    ],
  ];

  return $libraries;
}

Adding the defer and async allows everything to get along and Rocketloader can be reinstated. Win, win.

keiserjb’s picture

Changing hook_library_info_build works for me as well with 1.1.16.

I had trouble replicating this problem after discovery last week as it still worked locally and on my dev site. Today, I was able to find out how to break Turnstile on my dev site by adding a domain. Then I got the cross-site cookie errors just like production. The js alterations fix it though.

greatmatter’s picture

Status: Active » Fixed

Ok, it's all fixed in 1.1.17. Thanks, everyone!

glynster’s picture

Rockstar thank you so much for pushing this fix.

greatmatter’s picture

Status: Fixed » Closed (fixed)