1/** 2 * OMmatic Captcha â client side. 3 * Cache-safe: this static file only fetches a fresh, server-signed challenge from 4 * a dynamic REST endpoint, solves an invisible proof-of-work, and injects the 5 * token (+ honeypot) into Elementor Pro, WPForms and Contact Form 7 forms. No 6 * third parties, no consent needed. 7 * 8 * The hidden inputs are appended to the <form> itself, so every submit path picks 9 * them up: Elementor serialises the form, WPForms and CF7 build a FormData from 10 * the form element (classic submits post them anyway). 11 */ 12(function () { 13 'use strict'; 14 15 var cfg = window.__ommaticCaptcha; 16 if (!cfg || !cfg.challengeUrl) { 17 return; 18 } 19 20 var FORM_SELECTOR = 'form.elementor-form, form.wpforms-form, form.wpcf7-form'; 21 var solving = false; 22 var token = null; // { cap: string, nonce: string } 23 24 function forms() { 25 return document.querySelectorAll(FORM_SELECTOR); 26 } 27 28 function hasForm() { 29 return forms().length > 0; 30 } 31 32 // --- proof-of-work in a Web Worker (SubtleCrypto; https-only) ------------- 33 function solvePow(id, zeros) { 34 return new Promise(function (resolve, reject) { 35 var prefix = ''; 36 for (var i = 0; i < zeros; i++) { 37 prefix += '0'; 38 } 39 var code = 40 'self.onmessage=async function(e){' + 41 'var id=e.data.id,prefix=e.data.prefix,enc=new TextEncoder();' + 42 'for(var n=0;;n++){' + 43 'var b=await crypto.subtle.digest("SHA-256",enc.encode(id+":"+n));' + 44 'var a=new Uint8Array(b),h="";' + 45 'for(var i=0;i<prefix.length;i++){var v=a[i>>1];var nib=(i&1)?(v&15):(v>>4);' + 46 'if(nib!==0){h="x";break;}}' + 47 'if(h===""){self.postMessage(String(n));return;}}};'; 48 try { 49 var url = URL.createObjectURL(new Blob([code], { type: 'text/javascript' })); 50 var w = new Worker(url); 51 w.onmessage = function (e) { 52 w.terminate(); 53 URL.revokeObjectURL(url); 54 resolve(e.data); 55 }; 56 w.onerror = function (err) { 57 w.terminate(); 58 URL.revokeObjectURL(url); 59 reject(err); 60 }; 61 w.postMessage({ id: id, prefix: prefix }); 62 } catch (e) { 63 reject(e); 64 } 65 }); 66 } 67 68 function setHidden(form, name, value) { 69 var i = form.querySelector('input[name="' + name + '"]'); 70 if (!i) { 71 i = document.createElement('input'); 72 i.type = 'hidden'; 73 i.name = name; 74 form.appendChild(i); 75 } 76 i.value = value; 77 // Also as an attribute: CF7 calls form.reset() after a send, which would 78 // otherwise blank a value that was only set on the property. 79 i.setAttribute('value', value); 80 } 81 82 function addHoneypot(form) { 83 if (form.querySelector('input[name="ommatic_website"]')) { 84 return; 85 } 86 var hp = document.createElement('input'); 87 hp.type = 'text'; 88 hp.name = 'ommatic_website'; 89 hp.tabIndex = -1; 90 hp.autocomplete = 'off'; 91 hp.setAttribute('aria-hidden', 'true'); 92 hp.style.cssText = 93 'position:absolute!important;left:-9999px!important;top:auto;width:1px;height:1px;opacity:0;overflow:hidden;pointer-events:none'; 94 form.appendChild(hp); 95 } 96 97 // The honeypot goes in right away, not with the token: bots fill the form on 98 // page load, and a honeypot that only appears once the token is ready was 99 // still empty when they did â their token-valid retry then passed. 100 function addHoneypots() { 101 var list = forms(); 102 for (var i = 0; i < list.length; i++) { 103 addHoneypot(list[i]); 104 } 105 } 106 107 function injectAll() { 108 addHoneypots(); 109 if (!token) { 110 return; 111 } 112 var list = forms(); 113 for (var i = 0; i < list.length; i++) { 114 setHidden(list[i], 'ommatic_cap', token.cap); 115 setHidden(list[i], 'ommatic_pow', token.nonce); 116 } 117 } 118 119 // --- submit hold ------------------------------------------------------------ 120 // A submit before the token is ready is held and replayed once it is, so a 121 // real visitor never posts without a token. The server can then treat a 122 // tokenless post as a bot signal and ban its IP + email. 123 var held = null; // { form, submitter } waiting for the token 124 var bypass = false; // set while replaying, so the hold lets it through 125 126 function resubmit(form, submitter) { 127 if (submitter && submitter.form !== form) { 128 submitter = null; 129 } 130 bypass = true; 131 try { 132 if (typeof form.requestSubmit === 'function') { 133 if (submitter) { 134 form.requestSubmit(submitter); 135 } else { 136 form.requestSubmit(); 137 } 138 } else { 139 var btn = submitter || form.querySelector('[type="submit"]'); 140 if (btn) { 141 btn.click(); 142 } 143 } 144 } finally { 145 bypass = false; 146 } 147 } 148 149 function releaseHeld() { 150 if (!held) { 151 return; 152 }
153 var h = held; 154 held = null; 155 injectAll(); 156 resubmit(h.form, h.submitter); 157 } 158 159 // Capture phase on the document runs before the form systems' own submit 160 // handlers (all bound on the form), so stopping here keeps them from posting. 161 function holdSubmit(e) { 162 var form = e.target; 163 if (token || bypass || !form || !form.matches || !form.matches(FORM_SELECTOR)) { 164 return; 165 } 166 e.preventDefault(); 167 e.stopImmediatePropagation(); 168 held = { form: form, submitter: e.submitter || null }; 169 ensureToken(); 170 } 171 172 function ensureToken() { 173 if (token || solving || !hasForm()) { 174 if (token) { 175 injectAll(); 176 } 177 return; 178 } 179 solving = true; 180 fetch(cfg.challengeUrl, { headers: { Accept: 'application/json' }, credentials: 'same-origin' }) 181 .then(function (r) { 182 return r.json(); 183 }) 184 .then(function (ch) { 185 return solvePow(ch.id, ch.zeros).then(function (nonce) { 186 token = { cap: ch.id + '.' + ch.iat + '.' + ch.zeros + '.' + ch.sig, nonce: nonce }; 187 injectAll(); 188 }); 189 }) 190 .catch(function () { 191 // leave forms without a token â server decides (fail-closed by default) 192 }) 193 .finally(function () { 194 solving = false; 195 // Replay a held submit â with the token, or without it if solving 196 // failed, so the visitor at least gets the server's message instead 197 // of a dead button. 198 releaseHeld(); 199 }); 200 } 201 202 function boot() { 203 addHoneypots(); 204 ensureToken(); 205 document.addEventListener('submit', holdSubmit, true); 206 207 // CF7 keeps the visitor on the page after a send; the challenge is burned 208 // by then, so a second enquiry from the same page needs a fresh one. 209 document.addEventListener('wpcf7mailsent', function () { 210 token = null; 211 ensureToken(); 212 }); 213 214 // Elementor popups / AJAX render forms later â watch for them. 215 if ('MutationObserver' in window) { 216 var mo = new MutationObserver(function () { 217 if (hasForm()) { 218 addHoneypots(); 219 if (token) { 220 injectAll(); 221 } else { 222 ensureToken(); 223 } 224 } 225 }); 226 mo.observe(document.documentElement, { childList: true, subtree: true }); 227 } 228 } 229 230 if (document.readyState === 'loading') { 231 document.addEventListener('DOMContentLoaded', boot, { once: true }); 232 } else { 233 boot(); 234 } 235})();
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.