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.