Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions web/src/flow/stages/captcha/CaptchaStage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,7 @@ export class CaptchaStage
const template = iframeTemplate(captchaElement, {
challengeURL: challengeURL.toString(),
theme: this.activeTheme,
scriptOnLoad: !(controller instanceof TurnstileController),
});

if (
Expand Down
44 changes: 30 additions & 14 deletions web/src/flow/stages/captcha/controllers/turnstile.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
/* eslint-disable @typescript-eslint/triple-slash-reference */
/// <reference types="turnstile-types"/>
import { ifPresent } from "#elements/utils/attributes";

import { CaptchaController } from "#flow/stages/captcha/controllers/CaptchaController";

import { TurnstileObject } from "turnstile-types";
Expand All @@ -20,7 +18,16 @@ export class TurnstileController extends CaptchaController {
public prepareURL = (): URL | null => {
const input = this.host.challenge?.jsUrl;

return input && URL.canParse(input) ? new URL(input) : null;
if (!input || !URL.canParse(input)) return null;

const url = new URL(input);

// Use explicit rendering to prevent Turnstile's 3-hour self-upgrade
// from calling implicitRenderAll() and duplicating widgets.
url.searchParams.set("render", "explicit");
url.searchParams.set("onload", "onTurnstileReady");

return url;
};

/**
Expand All @@ -33,25 +40,34 @@ export class TurnstileController extends CaptchaController {
/**
* Renders the Turnstile captcha frame.
*
* Uses explicit rendering to avoid Turnstile's self-upgrade mechanism
* (every ~3 hours) from calling `implicitRenderAll()` and duplicating widgets.
*
* @remarks
*
* Turnstile will log a warning if the `data-language` attribute
* Turnstile will log a warning if the `language` option
* is not in lower-case format.
*
* @see {@link https://developers.cloudflare.com/turnstile/reference/supported-languages/ Turnstile Supported Languages}
*/
public interactive = () => {
const languageTag = this.host.activeLanguageTag.toLowerCase();
const siteKey = this.host.challenge?.siteKey ?? "";
const theme = this.host.activeTheme;
const language = this.host.activeLanguageTag.toLowerCase();

return html`<div
id="ak-container"
class="cf-turnstile"
data-sitekey=${ifPresent(this.host.challenge?.siteKey)}
data-theme=${this.host.activeTheme}
data-callback="callback"
data-size="flexible"
data-language=${ifPresent(languageTag)}
></div>`;
return html`<div id="ak-container"></div>
<script>
function onTurnstileReady() {
turnstile.render("#ak-container", {
sitekey: "${siteKey}",
theme: "${theme}",
language: "${language}",
size: "flexible",
callback,
});
loadListener();
}
</script>`;
};

public refreshInteractive = async () => {
Expand Down
12 changes: 10 additions & 2 deletions web/src/flow/stages/captcha/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ export function themeMeta(theme: ResolvedUITheme) {
export interface IFrameTemplateInit {
challengeURL: URL | string;
theme: ResolvedUITheme;
/**
* If `true`, the script element will fire `loadListener()` on load.
* Defaults to `true`.
*/
scriptOnLoad?: boolean;
}

/**
Expand All @@ -37,7 +42,7 @@ export interface IFrameTemplateInit {
*/
export function iframeTemplate(
children: TemplateResult,
{ challengeURL, theme }: IFrameTemplateInit,
{ challengeURL, theme, scriptOnLoad = true }: IFrameTemplateInit,
) {
return createDocumentTemplate({
head: html`
Expand Down Expand Up @@ -90,7 +95,10 @@ export function iframeTemplate(
}
</style>
${children}
<script onload="loadListener()" src="${challengeURL.toString()}"></script>
<script
${scriptOnLoad ? 'onload="loadListener()"' : ""}
src="${challengeURL.toString()}"
></script>
`,
});
}
Loading