PageSourceSearch

https://gapfy.io/js/turnstile-bridge.nrh82xfyye.js

js gapfy.io collected 2026-09-24 15:04:14 UTC 7,596 bytes, 188 lines download raw bytes

1// Cloudflare Turnstile bridge for Blazor Server interop (explicit render).
2//
3// api.js is injected on demand by render(), NOT from App.razor: pages without
4// a Turnstile widget must never contact Cloudflare (GDPR data minimisation --
5// loading the script transmits the visitor's IP address to Cloudflare).
6//
7// api.js is loaded with ?render=explicit so Cloudflare does NOT auto-scan the
8// DOM for ".cf-turnstile" elements. Auto-discovery does not survive Blazor
9// InteractiveServer: the pre-render puts the container in the DOM, api.js draws
10// the widget (injecting an <iframe> inside it), then the Blazor circuit connects
11// and reconciles the pre-rendered DOM -- since the component's render output has
12// the container empty, Blazor removes the iframe it does not own and the widget
13// vanishes.
14//
15// Instead, the Razor component calls render() from OnAfterRenderAsync(firstRender),
16// i.e. on the first *interactive* render after hydration, when the container is
17// stable. Blazor never re-renders that static container afterwards (its markup is
18// identical on every render), so the explicitly-rendered widget stays put.
19//
20// All helpers are no-op safe: if api.js has not loaded yet they retry / return
21// null / do nothing so the Razor caller can detect and react.
22(function () {
23    'use strict';
24
25    // Maps a container id to the Cloudflare widget id returned by turnstile.render.
26    const widgetIds = {};
27    const pendingRenders = {};
28
29    let apiRequested = false;
30
31    function isApiReady() {
32        return typeof window.turnstile !== 'undefined' && window.turnstile !== null;
33    }
34
35    // Injects Cloudflare's api.js the first time a widget is actually needed.
36    // Survives enhanced navigation: apiRequested persists in this module, and
37    // if the script tag was lost with a full reload the flag resets with it.
38    function ensureApiRequested() {
39        if (apiRequested || isApiReady()) {
40            return;
41        }
42        apiRequested = true;
43        const script = document.createElement('script');
44        script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit';
45        script.async = true;
46        script.defer = true;
47        document.head.appendChild(script);
48    }
49
50    function hasWidget(containerId) {
51        return Object.prototype.hasOwnProperty.call(widgetIds, containerId);
52    }
53
54    function removeWidget(containerId) {
55        const pending = pendingRenders[containerId];
56        if (pending) {
57            clearTimeout(pending.timeout);
58            delete pendingRenders[containerId];
59        }
60        if (hasWidget(containerId)) {
61            if (isApiReady()) {
62                try {
63                    window.turnstile.remove(widgetIds[containerId]);
64                } catch (e) {
65                }
66            }
67            delete widgetIds[containerId];
68        }
69    }
70
71    window.gapfyTurnstile = {
72        // Renders the widget into the container explicitly. Idempotent: a second
73        // call for a container that already holds a live widget is a no-op. After
74        // a Blazor enhanced navigation the container is recreated empty while this
75        // module's widgetIds map persists; that stale id is cleaned up and the
76        // widget re-rendered so the captcha shows again on the second visit.
77        render: function (containerId, sitekey, theme, size) {
78            const container = document.getElementById(containerId);
79            if (!container || !sitekey) {
80                return;
81            }
82
83            if (pendingRenders[containerId]?.container === container) {
84                return;
85            }
86            if (pendingRenders[containerId]) {
87                removeWidget(containerId);
88            }
89
90            ensureApiRequested();
91
92            if (hasWidget(containerId)) {
93                // Still present in the current DOM -> nothing to do.
94                if (container.childElementCount > 0) {
95                    return;
96                }
97                // Stale: the DOM was recreated (navigation). Drop the old widget.
98                if (isApiReady()) {
99                    try {
100                        window.turnstile.remove(widgetIds[containerId]);
101                    } catch (e) {
102                        // Swallow: the old widget is gone with the old DOM anyway.
103                    }
104                }
105                delete widgetIds[containerId];
106            }
107
108            // api.js is injected on demand above and loads async, so it is not
109            // ready when OnAfterRenderAsync fires. Poll (~12s) until
110            // window.turnstile exists.
111            const pending = { container: container, timeout: null };
112            pendingRenders[containerId] = pending;
113            function tryRender(attemptsLeft) {
114                if (pendingRenders[containerId] !== pending) {
115                    return;
116                }
117                if (document.getElementById(containerId) !== container) {
118                    removeWidget(containerId);
119                    return;
120                }
121                if (!isApiReady()) {
122                    if (attemptsLeft <= 0) {
123                        delete pendingRenders[containerId];
124                        return;
125                    }
126                    pending.timeout = setTimeout(function () { tryRender(attemptsLeft - 1); }, 150);
127                    return;
128                }
129                // Guard against a double render if Blazor re-invoked between polls.
130                if (hasWidget(containerId)) {
131                    delete pendingRenders[containerId];
132                    return;
133                }
134                try {
135                    widgetIds[containerId] = window.turnstile.render('#' + containerId, {
136                        sitekey: sitekey,
137                        theme: theme || 'auto',
138                        size: size || 'flexible'
139                    });
140                } catch (e) {
141                    // Swallow: invalid sitekey / unauthorised hostname surfaces in
142                    // the console; the submit path treats a missing token as a
143                    // failed captcha.
144                } finally {
145                    delete pendingRenders[containerId];
146                }
147            }
148
149            tryRender(80);
150        },
151
152        remove: removeWidget,
153
154        // Returns the current token, or null when the widget has not solved yet
155        // / is missing / api.js failed to load.
156        getToken: function (containerId) {
157            if (!isApiReady() || !hasWidget(containerId)) {
158                return null;
159            }
160            try {
161                const token = window.turnstile.getResponse(widgetIds[containerId]);
162                return token || null;
163            } catch (e) {
164                return null;
165            }
166        },
167
168        // Resets the widget so the user can solve it again. Called after a failed
169        // verify (token rejected / expired). Tokens are one-shot; submitting the
170        // same form twice without reset always fails.
171        reset: function (containerId) {
172            if (!isApiReady() || !hasWidget(containerId)) {
173                return;
174            }
175            try {
176                window.turnstile.reset(widgetIds[containerId]);
177            } catch (e) {
178                // Swallow: nothing useful to do if reset fails.
179            }
180        },
181
182        // True when api.js has loaded. Used by the Razor component to decide
183        // whether to even try server-side verification.
184        isReady: function () {
185            return isApiReady();
186        }
187    };
188})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.