1/*! 2 * Pontifex Attribution â Submit Forwarder v2.5.6 3 * 4 * Forwards form submissions to the microservice /lead endpoint via three 5 * complementary hooks (submit event, HTMLFormElement.submit/requestSubmit 6 * patch, and submit-button click fallback). Calendly clicks â /calendly-context. 7 * 8 * The triple-hook approach is needed because modern form builders submit via 9 * different mechanisms: native form submit (event-driven), jQuery .submit() 10 * (event-driven), form.requestSubmit() (event-driven), form.submit() (no event), 11 * and Gravity Forms 2.10+ XHR (no event, no DOM submit call). Each hook is 12 * idempotent via a WeakSet â /lead is sent at most once per form per session. 13 * 14 * Sends a canonical {email, phone} sniffed from HTML5 input types alongside 15 * the raw fields, so the Worker can identify the contact even when the form 16 * builder uses opaque field names (Gravity Forms `input_3`, WPForms, etc.). 17 * 18 * Refuses to send anything if no site_token is configured. 19 */ 20(function () { 21 var CFG = window.PFX_CONFIG || {}; 22 var DBG = !!CFG.debug; 23 function log() { if (DBG && window.console) { try { console.log.apply(console, ['[pfx-submit]'].concat([].slice.call(arguments))); } catch(e){} } } 24 25 if (!CFG.siteToken) { 26 log('disabled â no site_token configured (plugin not authorized)'); 27 return; 28 } 29 if (!CFG.microserviceUrl) { 30 log('disabled â no microservice URL configured'); 31 return; 32 } 33 34 function tracking() { 35 try { return JSON.parse(localStorage.getItem('tracking_params') || '{}'); } catch(e) { return {}; } 36 } 37 38 function post(path, body) { 39 var url = CFG.microserviceUrl + path; 40 var payload = JSON.stringify(body); 41 try { 42 // Use fetch with keepalive instead of sendBeacon to avoid CORS 43 // preflight issues with JSON content-type on cross-origin requests. 44 fetch(url, { 45 method: 'POST', 46 headers: { 'Content-Type': 'application/json' }, 47 body: payload, 48 keepalive: true, 49 mode: 'cors' 50 }).then(function() { log('sent', path); }) 51 .catch(function(e) { log('send failed', path, e); }); 52 } catch(e) { log('post failed', path, e); } 53 } 54 55 function isExcluded(form) { 56 var sels = (CFG.excludedSelectors || []); 57 for (var i = 0; i < sels.length; i++) { 58 try { if (sels[i] && form.matches(sels[i])) return true; } catch(e) {} 59 } 60 return false; 61 } 62 63 // Marketing/analytics endpoints that some WP plugins (PixelYourSite, Meta 64 // CAPI, GA-conversion proxies) hit by submitting a hidden <form action="â¦">. 65 // Those aren't real user submissions, just conversion pings â but our 66 // submit hook can't tell the difference at the DOM level, so we filter on 67 // form.action here. Worker has a matching defensive denylist for clients 68 // still running an older tracker. 69 var TRACKING_HOSTS = [ 70 'facebook.com/tr', 71 'google-analytics.com/collect', 72 'google-analytics.com/g/collect', 73 'analytics.google.com/g/collect', 74 'analytics.tiktok.com', 75 'bat.bing.com', 76 'linkedin.com/li.lms-analytics', 77 'analytics.pinterest.com', 78 'ct.pinterest.com', 79 'googleadservices.com/pagead/conversion', 80 'doubleclick.net', 81 'analytics.snapchat.com' 82 ]; 83 84 function isTrackingPixelForm(form) { 85 var a = (form && form.action) || ''; 86 if (!a) return false; 87 a = String(a).toLowerCase(); 88 for (var i = 0; i < TRACKING_HOSTS.length; i++) { 89 if (a.indexOf(TRACKING_HOSTS[i]) !== -1) return true; 90 } 91 return false; 92 } 93 94 // Divi's Visual Builder POSTs a synthetic <form id="preview-data-submission"> 95 // (carrying is_fb_preview / et_pb_preview_nonce, action ?et_pb_preview=true) 96 // every time it re-renders shortcode previews while a page is being edited. 97 // Those aren't real submissions, but our submit hook can't tell at the DOM 98 // level, so one editing session floods /lead with no-email pings. Filter 99 // them here; the Worker has a matching defensive check for older trackers. 100 function isBuilderPreviewForm(form) { 101 try { 102 if (form.id === 'preview-data-submission') return true; 103 if (form.querySelector('[name="is_fb_preview"], [name="et_pb_preview_nonce"]')) return true; 104 var a = String((form && form.action) || '').toLowerCase(); 105 if (a.indexOf('et_pb_preview=true') !== -1) return true; 106 } catch(e) {} 107 return false; 108 } 109 110 function detectFormSource(form) { 111 if (form.id && /gform_/.test(form.id)) return 'gravity_forms'; 112 if (form.className && /wpcf7-form/.test(form.className)) return 'contact_form_7'; 113 if (form.id && /wpforms-form/.test(form.id)) return 'wpforms'; 114 if (form.className && /frm-fluent-form|fluentform/.test(form.className)) return 'fluent_forms'; 115 if (form.action && /aweber\.com/.test(form.action)) return 'aweber'; 116 if (form.action && /list-manage\.com/.test(form.action)) return 'mailchimp'; 117 return 'generic'; 118 } 119 120 function serializeForm(form) { 121 var out = {}; 122 try { 123 var fd = new FormData(form); 124 fd.forEach(function (val, key) { 125 if (typeof File !== 'undefined' && val instanceof File) return; 126 if (out[key] !== undefined) { 127 if (!Array.isArray(out[key])) out[key] = [out[key]]; 128 out[key].push(val); 129 } else { 130 out[key] = val; 131 } 132 }); 133 } catch(e) {} 134 return out; 135 } 136 137 // Identify canonical contact fields by HTML5 input type, not by name. 138 // Form builders use opaque names (Gravity Forms 'input_3', WPForms 139 // 'wpforms[fields][N]') that the Worker cannot decode. Sniffing types here 140 // gives the Worker reliable email/phone regardless of which builder is used. 141 function extractCanonical(form) { 142 var out = {}; 143 try { 144 var emails = form.querySelectorAll('input[type="email"]'); 145 for (var i = 0; i < emails.length; i++) { 146 var ev = (emails[i].value || '').trim(); 147 if (ev) { out.email = ev; break; } 148 } 149 var tels = form.querySelectorAll('input[type="tel"]'); 150 for (var j = 0; j < tels.length; j++) { 151 var pv = (tels[j].value || '').trim(); 152 if (pv) { out.phone = pv; break; } 153 } 154 } catch(e) {} 155 return out; 156 } 157 158 // ââ FORM SUBMIT FORWARDER ââââââââââââââââââââââââââââââââââââââââââââââââ 159 // Modern form builders submit through several different mechanisms. 160 // Hook 1 â submit event capture: catches jQuery .submit(), form.requestSubmit(), 161 // and standard browser-initiated form submission. 162 // Hook 2 â HTMLFormElement.prototype.submit/requestSubmit patch: catches direct 163 // DOM API calls. form.submit() does NOT fire a submit event. 164 // Hook 3 â submit-button click + checkValidity: catches Gravity Forms 2.10's 165 // XHR submission path which bypasses BOTH the submit event and form.submit(). 166 // All hooks dedupe via __pfxSentForms so /lead is sent at most once per form. 167 var __pfxSentForms = (typeof WeakSet !== 'undefined') 168 ? new WeakSet() 169 : { has: function(){ return false; }, add: function(){} }; 170 171 function sendLead(form) { 172 if (!form || form.tagName !== 'FORM') return; 173 if (isExcluded(form)) return; 174 if (isTrackingPixelForm(form)) { 175 // Email-bypass exception: if the form has a real email value, send 176 // anyway. Meta CAPI and similar conversion wrappers can ride on 177 // real form submissions; a misconfigured plugin might also point a 178 // genuine form's action at a tracking endpoint by accident. Don't 179 // drop a real lead just because the action looks pixel-shaped. 180 var probe = extractCanonical(form); 181 if (!probe.email || !/.+@.+\..+/.test(probe.email)) { 182 log('skipped tracking-pixel form:', form.action); 183 return; 184 } 185 log('tracking-pixel action with valid email - processing:', form.action); 186 } 187 if (isBuilderPreviewForm(form)) { 188 log('skipped builder preview submission:', form.id || form.action); 189 return; 190 } 191 if (__pfxSentForms.has(form)) return; 192 __pfxSentForms.add(form); 193 194 var fields = serializeForm(form); 195 var t = tracking(); 196 var cleaned = {};
197 Object.keys(fields).forEach(function (k) { if (!(k in t)) cleaned[k] = fields[k]; }); 198 199 post('/lead', { 200 site_token: CFG.siteToken, 201 site_url: CFG.siteUrl, 202 form_source: detectFormSource(form), 203 form_id: form.id || '', 204 form_action: form.action || '', 205 page_url: window.location.href, 206 fields: cleaned, 207 canonical: extractCanonical(form), 208 tracking_params: t, 209 ts: Date.now() 210 }); 211 } 212 213 if (CFG.enableForms) { 214 // Hook 1: submit event in capture phase. 215 document.addEventListener('submit', function (e) { 216 if (e.target && e.target.tagName === 'FORM') sendLead(e.target); 217 }, true); 218 219 // Hook 2: monkey-patch DOM submission methods. 220 try { 221 var __realSubmit = HTMLFormElement.prototype.submit; 222 HTMLFormElement.prototype.submit = function () { 223 sendLead(this); 224 return __realSubmit.apply(this, arguments); 225 }; 226 if (typeof HTMLFormElement.prototype.requestSubmit === 'function') { 227 var __realRequestSubmit = HTMLFormElement.prototype.requestSubmit; 228 HTMLFormElement.prototype.requestSubmit = function () { 229 sendLead(this); 230 return __realRequestSubmit.apply(this, arguments); 231 }; 232 } 233 } catch (e) { log('hook2 patch failed', e); } 234 235 // Hook 3: click on submit button â wait briefly for hooks 1/2, then fire 236 // /lead if neither did. checkValidity gates out invalid submissions. 237 document.addEventListener('click', function (e) { 238 var t = e.target; 239 var btn = (t && (t.tagName === 'BUTTON' || t.tagName === 'INPUT')) 240 ? t 241 : (t && t.closest && t.closest('button[type=submit], input[type=submit]')); 242 if (!btn || btn.type !== 'submit') return; 243 var form = btn.closest && btn.closest('form'); 244 if (!form) return; 245 setTimeout(function () { 246 if (__pfxSentForms.has(form)) return; 247 if (typeof form.checkValidity === 'function' && !form.checkValidity()) return; 248 sendLead(form); 249 }, 250); 250 }, true); 251 } 252 253 // ââ CALENDLY CONTEXT FORWARDER âââââââââââââââââââââââââââââââââââââââââââ 254 if (CFG.enableCalendly) { 255 var lastSent = 0; 256 function sendContext(reason) { 257 var t = tracking(); 258 if (!t.visitor_id) return; 259 var now = Date.now(); 260 if (now - lastSent < 5000) return; 261 lastSent = now; 262 post('/calendly-context', { 263 site_token: CFG.siteToken, 264 site_url: CFG.siteUrl, 265 visitor_id: t.visitor_id, 266 tracking_params: t, 267 page_url: window.location.href, 268 reason: reason || 'click', 269 ts: now 270 }); 271 } 272 273 document.addEventListener('click', function (e) { 274 var el = e.target; 275 while (el && el !== document.body) { 276 if (el.tagName === 'A' && el.href && /calendly\.com/.test(el.href)) { sendContext('link'); return; } 277 if (el.getAttribute && el.getAttribute('onclick') && /Calendly\.initPopupWidget/.test(el.getAttribute('onclick'))) { sendContext('popup'); return; } 278 if (el.classList && (el.classList.contains('calendly-inline-widget') || el.classList.contains('calendly-badge-widget'))) { sendContext('inline'); return; } 279 el = el.parentNode; 280 } 281 }, true); 282 283 function scanForEmbeds() { 284 if (document.querySelector('.calendly-inline-widget, .calendly-badge-widget, a[href*="c
284alendly.com"], [onclick*="Calendly.initPopupWidget"]')) { 285 sendContext('embed_present'); 286 } 287 } 288 if (document.readyState === 'loading') { 289 document.addEventListener('DOMContentLoaded', scanForEmbeds); 290 } else { 291 scanForEmbeds(); 292 } 293 } 294})();
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.