1/** 2 * Cloudflare Turnstile, loaded only when somebody actually touches a form. 3 * 4 * The honeypot already catches the bot that fills every field it finds, which 5 * is most automated traffic. What it cannot catch is somebody who looked at 6 * the form once, saw the trap and left it empty â and the newsletter form 7 * writes to GoHighLevel, where the cost of that is not noise but a 8 * contaminated list with welcome sequences firing at invented addresses. 9 * 10 * Three decisions worth stating, because each has a cost somebody will 11 * otherwise pay without knowing. 12 * 13 * Turnstile rather than reCAPTCHA: reCAPTCHA sends the visitor's data to 14 * Google, from a site that carries a consent banner, which turns a spam 15 * decision into a privacy one. Turnstile sends nothing to Google and is free 16 * without a quota. 17 * 18 * Loaded on first focus rather than on page load. The script is ~35 KB 19 * against a home page that is 190 KB â an eighteen percent tax on everybody, 20 * to protect a form that most visitors never touch. Deferred to the first 21 * interaction, the people who submit pay it and nobody else does. 22 * 23 * Inert without a site key. Absent one the forms behave exactly as they did 24 * before, so this can ship before the Cloudflare account exists rather than 25 * breaking three forms until somebody sets an environment variable. 26 */ 27(function () { 28 var KEY = document.documentElement.dataset.turnstileKey || ''; 29 if (!KEY) return; // not configured: do nothing at all 30 31 var SRC = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'; 32 var loading = null; 33 var widgets = new WeakMap(); // form -> { id, token, waiters } 34 35 function load() { 36 if (loading) return loading; 37 loading = new Promise(function (resolve, reject) { 38 var s = document.createElement('script'); 39 s.src = SRC; 40 s.async = true; 41 s.defer = true; 42 s.onload = function () { resolve(window.turnstile); }; 43 s.onerror = function () { reject(new Error('turnstile unavailable')); }; 44 document.head.appendChild(s); 45 }); 46 return loading; 47 } 48 49 /** 50 * One widget per form, rendered the moment a field is touched so the 51 * challenge â on the rare visitor who gets one â is solved by the time they 52 * finish typing rather than after they press the button. 53 */ 54 function arm(form) { 55 if (widgets.has(form)) return widgets.get(form); 56 57 var state = { id: null, token: '', waiters: [] }; 58 widgets.set(form, state); 59 60 var host = document.createElement('div'); 61 host.className = 'cf-turnstile-host'; 62 form.appendChild(host); 63 64 load().then(function (turnstile) { 65 state.id = turnstile.render(host, { 66 sitekey: KEY, 67 // Shows itself only when Cloudflare wants a human to do something. 68 // Everybody else sees nothing at all. 69 appearance: 'interaction-only', 70 theme: 'dark', 71 callback: function (token) { 72 state.token = token; 73 state.waiters.splice(0).forEach(function (r) { r(token); }); 74 }, 75 'expired-callback': function () { state.token = ''; }, 76 'error-callback': function () { 77 // Cloudflare could not decide. Release anything waiting with an 78 // empty token and let the server make the call â the alternative is 79 // a form that never submits because a third party had a bad minute. 80 state.token = ''; 81 state.waiters.splice(0).forEach(function (r) { r(''); }); 82 }, 83 }); 84 }).catch(function () { 85 state.waiters.splice(0).forEach(function (r) { r(''); }); 86 }); 87 88 return state; 89 } 90 91 /** 92 * The token for this form, or '' if there will not be one. 93 * 94 * Bounded, because a promise that never settles is a submit button that 95 * never comes back. Four seconds is long enough for a challenge to solve 96 * itself and short enough that nobody wonders whether the page is broken. 97 */ 98 window.iLindyTurnstile = function (form) { 99 var state = arm(form); 100 if (state.token) return Promise.resolve(state.token); 101 return new Promise(function (resolve) { 102 state.waiters.push(resolve); 103 setTimeout(function () { 104 var i = state.waiters.indexOf(resolve); 105 if (i !== -1) { state.waiters.splice(i, 1); resolve(state.token || ''); } 106 }, 4000); 107 }); 108 }; 109 110 // Arm on the first touch of any field inside a form we protect. 111 document.addEventListener('focusin', function (e) { 112 var form = e.target && e.target.closest && e.target.closest('form'); 113 if (form && form.dataset.protected !== undefined) arm(form); 114 }, { passive: true }); 115})();
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.