1/* 2 * MRMTRIAGE-587 â Cookie-consent domain auto-blocker (member club/ pages) 3 * 4 * Prior-blocking / opt-in. Until the visitor has accepted cookies, this neutralizes 5 * cross-domain <script src> that is NOT on the essential allow-list (ticket §11), so 6 * analytics / marketing / unknown third-party scripts cannot run before consent. 7 * 8 * Strategy: intercept the dynamic-injection paths (createElement src setter, 9 * appendChild / insertBefore, document.write / writeln) and watch the DOM with a 10 * MutationObserver. Blocked scripts are tagged with data-mf-blocked-src and have their 11 * src removed + type set to text/plain. On Accept, window.mfCookieConsentReactivate() 12 * restores them and stops blocking. 13 * 14 * Also neutralizes non-allow-listed cross-domain <iframe> embeds (e.g. SnapWidget) by 15 * parking them on about:blank and stashing the real src for restore on Accept â the 16 * iframe makes its own network request that the <script> guards do not cover. 17 * 18 * Known limitation (ticket §13): parser-inserted static <script src> may begin fetching 19 * before the observer fires; <img> pixels and fetch/sendBeacon are NOT covered. 20 * 21 * MUST be the first script in <head>. 22 */ 23(function () { 24 'use strict'; 25 26 // Essential allow-list â exact hosts always permitted (ticket §11). 27 var ALLOW_EXACT = { 28 'code.jquery.com': 1, 29 'ajax.googleapis.com': 1, 30 'cdnjs.cloudflare.com': 1, 31 'maxcdn.bootstrapcdn.com': 1, 32 'cdn.jsdelivr.net': 1, 33 'raw.githack.com': 1, 34 'use.typekit.net': 1, 35 'maps.googleapis.com': 1, 36 'api.paysimple.com': 1, 37 'sandbox-api.paysimple.com': 1, 38 'www.google.com': 1, 39 'cdn.memfirstweb.net': 1 40 }; 41 // Suffix allow (wildcard): *.memfirstweb.net (and the bare apex). 42 var ALLOW_SUFFIX = ['memfirstweb.net']; 43 44 function readConsent() { 45 // MRMTRIAGE-587 (PR #3403 FIX 2): fail CLOSED on a malformed cookie value. 46 // Only "accepted" / "declined" are valid; anything else (incl. decode errors) 47 // is treated as no consent so non-essential scripts stay blocked. 48 var m = document.cookie.match(/(?:^|;\s*)mf_cookie_consent=([^;]*)/); 49 if (!m) { return ''; } 50 var v; 51 try { v = decodeURIComponent(m[1]).toLowerCase(); } catch (e) { return ''; } 52 return (v === 'accepted' || v === 'declined') ? v : ''; 53 } 54 55 // If consent is already accepted, do nothing at all (zero overhead path). 56 if (readConsent() === 'accepted') { 57 window.mfCookieConsentReactivate = function () {}; 58 return; 59 } 60 61 var blocking = true; 62 // MRMTRIAGE-587 (PR #3403 FIX 4): use hostname (no port) so first-party matching 63 // agrees with hostOf(), which returns hostname without port. 64 var selfHost = (location.hostname || '').toLowerCase(); 65 66 function hostOf(url) { 67 if (!url) { return ''; } 68 var s = String(url).trim(); 69 // Relative / fragment / data / about / blob => first-party or non-network. 70 if (s.charAt(0) === '/' && s.charAt(1) !== '/') { return selfHost; } 71 if (/^(#|\?|data:|about:|blob:|javascript:)/i.test(s)) { return ''; } 72 try { 73 return new URL(s, location.href).hostname.toLowerCase(); 74 } catch (e) { 75 var mm = s.match(/^(?:[a-z][a-z0-9+.-]*:)?\/\/([^\/?#]+)/i); 76 return mm ? mm[1].toLowerCase().replace(/:.*/, '') : selfHost; 77 } 78 } 79 80 function hostAllowed(url) { 81 var host = hostOf(url); 82 if (!host) { return true; } // relative / non-network 83 if (host === selfHost) { return true; } // first-party 84 if (ALLOW_EXACT[host]) { return true; } 85 for (var i = 0; i < ALLOW_SUFFIX.length; i++) { 86 var suf = ALLOW_SUFFIX[i]; 87 if (host === suf || host.length > suf.length && host.slice(-(suf.length + 1)) === ('.' + suf)) { 88 return true; 89 } 90 } 91 return false; 92 } 93 94 // Neutralize a single <script> node (called before it is connected/executed). 95 function neutralizeScript(node) { 96 if (!blocking || !node || node.nodeType !== 1) { return; } 97 if (String(node.nodeName).toLowerCase() !== 'script') { return; } 98 if (node.getAttribute('data-mf-blocked-src')) { return; } 99 var src = node.getAttribute('src'); 100 if (src && !hostAllowed(src)) { 101 // MRMTRIAGE-587 (PR #3403 FIX 3): preserve the original type (e.g. module, 102 // application/ld+json) so it can be restored on reactivation. 103 var originalType = node.getAttribute('type'); 104 if (originalType && originalType.toLowerCase() !== 'text/plain') { 105 node.setAttribute('data-mf-blocked-type', originalType); 106 } 107 node.setAttribute('data-mf-blocked-src', src); 108 node.setAttribute('type', 'text/plain'); 109 node.removeAttribute('src'); 110 } 111 } 112 113 // Neutralize a single cross-domain <iframe> (e.g. SnapWidget) by parking it on 114 // about:blank and stashing the real src so it can be restored on Accept. The 115 // <script> guards do not help here because the iframe issues its own request. 116 function neutralizeIframe(node) { 117 if (!blocking || !node || node.nodeType !== 1) { return; } 118 if (String(node.nodeName).toLowerCase() !== 'iframe') { return; } 119 if (node.getAttribute('data-mf-blocked-src')) { return; } 120 var src = node.getAttribute('src'); 121 if (src && !hostAllowed(src)) { 122 node.setAttribute('data-mf-blocked-src', src); 123 node.setAttribute('src', 'about:blank'); 124 } 125 } 126 127 function neutralizeNode(node) { 128 var tag = String(node.nodeName).toLowerCase(); 129 if (tag === 'script') { 130 neutralizeScript(node); 131 } else if (tag === 'iframe') { 132 neutralizeIframe(node); 133 } 134 } 135 136 function neutralizeTree(node) { 137 if (!node) { return; } 138 if (node.nodeType === 1) { 139 var tag = String(node.nodeName).toLowerCase(); 140 if (tag === 'script' || tag === 'iframe') { 141 neutralizeNode(node); 142 } else if (node.querySelectorAll) { 143 var inner = node.querySelectorAll('script[src], iframe[src]'); 144 for (var i = 0; i < inner.length; i++) { neutralizeNode(inner[i]); } 145 } 146 } else if (node.nodeType === 11 && node.childNodes) { // DocumentFragment 147 for (var j = 0; j < node.childNodes.length; j++) { neutralizeTree(node.childNodes[j]); } 148 } 149 } 150 151 // --- Intercept createElement so dynamically created scripts get a guarded
151src --- 152 var nativeCreateElement = document.createElement; 153 var scriptSrcDesc = null; 154 try { 155 scriptSrcDesc = Object.getOwnPropertyDescriptor(HTMLScriptElement.prototype, 'src'); 156 } catch (e) { scriptSrcDesc = null; } 157 158 document.createElement = function (tagName) { 159 var el = nativeCreateElement.apply(document, arguments); 160 try { 161 if (blocking && scriptSrcDesc && scriptSrcDesc.set && 162 String(tagName).toLowerCase() === 'script') { 163 Object.defineProperty(el, 'src', { 164 configurable: true, 165 enumerable: true, 166 get: function () { return scriptSrcDesc.get.call(this); }, 167 set: function (value) { 168 if (blocking && !hostAllowed(value)) { 169 // FIX 3: stash original type before overwriting. 170 var originalType = this.getAttribute('type'); 171 if (originalType && originalType.toLowerCase() !== 'text/plain') { 172 this.setAttribute('data-mf-blocked-type', originalType); 173 } 174 this.setAttribute('data-mf-blocked-src', value); 175 this.setAttribute('type', 'text/plain'); 176 } else { 177 scriptSrcDesc.set.call(this, value); 178 } 179 } 180 }); 181 } 182 } catch (e2) { /* fall through to insertion-time guard */ } 183 return el; 184 }; 185 186 // --- Intercept insertion so anything appended is checked before it runs --- 187 var nativeAppendChild = Node.prototype.appendChild; 188 var nativeInsertBefore = Node.prototype.insertBefore; 189 190 Node.prototype.appendChild = function (node) { 191 neutralizeTree(node); 192 return nativeAppendChild.call(this, node); 193 }; 194 Node.prototype.insertBefore = function (node, ref) { 195 neutralizeTree(node); 196 return nativeInsertBefore.call(this, node, ref); 197 }; 198 199 // --- Filter document.write / writeln markup --- 200 var nativeWrite = document.write; 201 var nativeWriteln = document.writeln; 202 203 function filterMarkup(str) { 204 if (!blocking || str == null) { return str; } 205 return String(str).replace(/<script\b[^>]*>/gi, function (tag) { 206 var m = tag.match(/\ssrc\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i); 207 if (!m) { return tag; } 208 var url = m[1] || m[2] || m[3] || ''; 209 if (hostAllowed(url)) { return tag; } 210 var rebuilt = tag 211 .replace(/\ssrc\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/i, ' data-mf-blocked-src="' + url.replace(/"/g, '"') + '"'); 212 // FIX 3: stash any original type so it can be restored on reactivation. 213 var tm = rebuilt.match(/\stype\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i); 214 if (tm) { 215 var ot = tm[1] || tm[2] || tm[3] || ''; 216 rebuilt = rebuilt.replace(/\stype\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/i, ' data-mf-blocked-type="' + ot.replace(/"/g, '"') + '" type="text/plain"'); 217 } else { 218 rebuilt = rebuilt.replace(/^<script\b/i, '<script type="text/plain"'); 219 } 220 return rebuilt; 221 }); 222 } 223 224 function filterMarkupArgs(args) { 225 var filtered = []; 226 for (var i = 0; i < args.length; i++) { filtered.push(filterMarkup(args[i])); } 227 return filtered; 228 } 229 230 document.write = function () { return nativeWrite.apply(document, filterMarkupArgs(arguments)); }; 231 document.writeln = function () { return nativeWriteln.apply(document, filterMarkupArgs(arguments)); }; 232 233 // --- Watch the DOM for parser-/script-inserted scripts --- 234 var observer = null; 235 if (window.MutationObserver) { 236 observer = new MutationObserver(function (mutations) { 237 if (!blocking) { return; } 238 for (var i = 0; i < mutations.length; i++) { 239 var added = mutations[i].addedNodes; 240 for (var j = 0; j < added.length; j++) { neutralizeTree(added[j]); } 241 } 242 }); 243 try { 244 observer.observe(document.documentElement || document, { childList: true, subtree: true }); 245 } catch (e) { /* observer unavailable */ } 246 } 247 248 // Recreate a <script> node so it executes when connected, copying both HTML 249 // attributes and node-bound runtime properties that are NOT attributes 250 // (MRMTRIAGE-587 risk MEDIUM-2: onload / onerror / nonce / referrerPolicy would 251 // otherwise be lost when only attributes are copied). 252 function cloneExecutableScript(old) { 253 var fresh = nativeCreateElement.call(document, 'script'); 254 for (var a = 0; a < old.attributes.length; a++) { 255 var at = old.attributes[a]; 256 var name = at.name.toLowerCase(); 257 // Skip our bookkeeping attrs and the synthetic text/plain type; a real 258 // (non-synthetic) type is restored below. (FIX 3) 259 if (name === 'data-mf-blocked-src' || name === 'data-mf-blocked-type') { continue; } 260 if (name === 'type' && String(at.value || '').toLowerCase() === 'text/plain') { continue; } 261 try { fresh.setAttribute(at.name, at.value); } catch (e) {} 262 } 263 // FIX 3: restore the original type stashed when the script was neutralized. 264 var blockedType = old.getAttribute('data-mf-blocked-type'); 265 if (blockedType) { 266 try { fresh.setAttribute('type', blockedType); } catch (e0) {} 267 } 268 // Preserve node-bound properties lost by attribute-only copies. 269 try { if (old.onload) { fresh.onload = old.onload; } } catch (e1) {} 270 try { if (old.onerror) { fresh.onerror = old.onerror; } } catch (e2) {} 271 try { if (old.nonce) { fresh.nonce = old.nonce; } } catch (e3) {} 272 try { if (old.referrerPolicy) { fresh.referrerPolicy = old.referrerPolicy; } } catch (e4) {} 273 if (old.text) { fresh.text = old.text; } 274 return fresh; 275 } 276 277 // Revive any <script> elements within a freshly-inserted subtree so they execute 278 // (innerHTML-parsed scripts are inert until recreated). 279 function reviveScriptsIn(node) { 280 if (!node || !node.querySelectorAll) { return; } 281 var scripts = node.querySelectorAll('script'); 282 for (var i = 0; i < scripts.length; i++) { 283 var old = scripts[i]; 284 if (!old.parentNode) { continue; } 285 old.parentNode.replaceChild(cloneExecutableScript(old), old); 286 } 287 } 288 289 // --- Reactivation on Accept --- 290 window.mfCookieConsentReactivate = function () { 291 blocking = false; // all guards above become pass-through 292 if (observer) { try { observer.disconnect(); } catch (e) {} } 293 294 // 1) Auto-blocked external scripts tagged with data-mf-blocked-src. 295 var blocked = document.querySelectorAll('script[data-mf-blocked-src]'); 296 for (var i = 0; i < blocked.length; i++) { 297 var old = blocked[i]; 298 var src = old.getAttribute('data-mf-blocked-src'); 299 if (!src) { continue; } 300 var fresh = cloneExecutableScript(old); 301 fresh.src = src; // createElement is pass-through now, so this loads normally 302 if (old.parentNode) { 303 old.parentNode.insertBefore(fresh, old); 304 old.parentNode.removeChild(old); 305 } else { 306 (document.head || document.documentElement).appendChild(fresh); 307 } 308 } 309 310 // 1b) Auto-blocked cross-domain iframes (e.g. SnapWidget): restore the real src 311 // so the embed loads now that consent is granted. 312 var blockedFrames = document.querySelectorAll('iframe[data-mf-blocked-src]'); 313 for (var fi = 0; fi < blockedFrames.length; fi++) { 314 var frame = blockedFrames[fi]; 315 var fsrc = frame.getAttribute('data-mf-blocked-src'); 316 if (!fsrc) { continue; } 317 frame.removeAttribute('data-mf-blocked-src'); 318 frame.setAttribute('src', fsrc); 319 } 320 321 // 2) Inert server-emitted sitewide analytics slots (risk HIGH-1): the original
322 // markup was HTML-encoded into data-mf-blocked-html on an inert placeholder. 323 // Parse it, insert its nodes in place, and recreate scripts so they execute. 324 var slots = document.querySelectorAll('.mf-blocked-html[data-mf-blocked-html]'); 325 for (var s = 0; s < slots.length; s++) { 326 var ph = slots[s]; 327 var markup = ph.getAttribute('data-mf-blocked-html'); 328 if (!ph.parentNode) { continue; } 329 if (markup) { 330 var holder = nativeCreateElement.call(document, 'div'); 331 holder.innerHTML = markup; 332 var kids = Array.prototype.slice.call(holder.childNodes); 333 for (var k = 0; k < kids.length; k++) { 334 var n = kids[k]; 335 if (n.nodeType === 1 && String(n.nodeName).toLowerCase() === 'script') { 336 ph.parentNode.insertBefore(cloneExecutableScript(n), ph); 337 } else { 338 ph.parentNode.insertBefore(n, ph); 339 reviveScriptsIn(n); 340 } 341 } 342 } 343 ph.parentNode.removeChild(ph); 344 } 345 }; 346})();
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.