1/* 2 * Prior-blocking cookie-consent script blocker (PRD 10.8 / 11). 3 * 4 * Emitted FIRST in the <head> of public pages by PortalTemplate.aspx.extension.cs, 5 * and only when the consent banner is enabled (HasCookieNotification ON) AND the 6 * visitor has not yet accepted. Neutralizes cross-domain, non-allow-listed 7 * <script> elements until the visitor clicks Accept, at which point the consent 8 * banner calls window.chReactivateBlockedScripts(). 9 * 10 * A dynamically created external script is only fetched by the browser once it is 11 * inserted into the document, so the primary, reliable choke point is DOM 12 * insertion (appendChild / insertBefore / replaceChild / append / prepend). We 13 * neutralize the node there, BEFORE it enters the DOM, regardless of how its src 14 * was assigned (element.src = ..., setAttribute('src', ...), or createElement). 15 * The createElement setter, setAttribute patch, MutationObserver and document.write 16 * patch are additional layers of defense. 17 * 18 * IMPORTANT (PRD 11 / 14): the allow-list below is a starting point and MUST be 19 * verified against a live CHO public-site network trace before this blocker is 20 * relied upon. In particular, add the real payment-gateway domains so checkout 21 * and booking are never blocked. 22 * 23 * Manual validation harness (run before consent, with Network tools open): 24 * var s = document.createElement('script'); 25 * s.setAttribute('src', 'https://example-third-party.invalid/x.js'); 26 * document.head.appendChild(s); 27 * // Expect: NO network request; s has type="javascript/blocked" and 28 * // data-mf-blocked-src set. After Accept, the request fires. 29 */ 30(function () { 31 "use strict"; 32 33 var ACCEPTED = "CLUBPOLICY=ClubPrivacyPolicy"; 34 35 // If the visitor already accepted, do not install the blocker at all. 36 if (document.cookie.indexOf(ACCEPTED) !== -1) { 37 return; 38 } 39 40 // Essential hosts that must always load, regardless of consent. 41 // Matched by exact host or as a parent domain (".example.com"). 42 var ALLOW_LIST = [ 43 window.location.host.toLowerCase(), 44 "code.jquery.com", 45 "ajax.googleapis.com", 46 "cdnjs.cloudflare.com", 47 "maxcdn.bootstrapcdn.com", 48 "cdn.jsdelivr.net", 49 "use.typekit.net", 50 "www.google.com", // reCAPTCHA loader (captcha behavior gated separately, PRD 10.7) 51 // Payment gateways 52 "api.shift4test.com/api/rest/v1/", 53 "api.shift4api.net/api/rest/v1/", 54 "access.shift4test.com", 55 "access.i4go.com", 56 "i4m.shift4test.com/js/jquery.i4goTrueToken.js", 57 "i4m.i4go.com/js/jquery.i4goTrueToken.js", 58 "shift4-test.jonasprocessing.com", 59 "shift4.jonasprocessing.com", 60 "staging-pyxisapi.csipay.com", 61 "pyxisapi.csipay.com", 62 "js.stripe.com", 63 "m.stripe.com", 64 "m.stripe.network", 65 "r.stripe.com", 66 ]; 67 68 function hostOf(src) { 69 if (!src) return ""; 70 var s = String(src); 71 if (s.indexOf("//") === 0) { 72 s = window.location.protocol + s; 73 } 74 var m = s.match(/^[a-z]+:\/\/([^\/?#]+)/i); 75 // No host => relative/inline script => treated as first-party (allowed). 76 return m ? m[1].toLowerCase() : ""; 77 } 78 79 function isAllowed(src) { 80 var host = hostOf(src); 81 if (!host) return true; 82 for (var i = 0; i < ALLOW_LIST.length; i++) { 83 var allowed = ALLOW_LIST[i]; 84 if (!allowed) continue; 85 if (host === allowed) return true; 86 // parent-domain match: host ends with "." + allowed 87 if (host.length > allowed.length && 88 host.lastIndexOf("." + allowed) === host.length - allowed.length - 1) { 89 return true; 90 } 91 } 92 return false; 93 } 94 95 var blocked = []; 96 97 function rememberBlocked(node) { 98 if (node && blocked.indexOf(node) === -1) { 99 blocked.push(node); 100 } 101 } 102 103 // Move a non-allow-listed script's src into data-mf-blocked-src so the browser 104 // never fetches it. Idempotent: once neutralized there is no src to re-process. 105 function neutralizeScriptNode(node) { 106 if (!node || node.nodeType !== 1 || node.tagName !== "SCRIPT") return; 107 // Read via getAttribute so we see the value no matter how
107it was assigned. 108 var src = origGetAttribute ? origGetAttribute.call(node, "src") : node.getAttribute("src"); 109 if (!src || isAllowed(src)) return; 110 // Use the original setAttribute/removeAttribute so we don't re-enter our patch. 111 (origSetAttribute || node.setAttribute).call(node, "data-mf-blocked-src", src); 112 node.type = "javascript/blocked"; 113 (origRemoveAttribute || node.removeAttribute).call(node, "src"); 114 rememberBlocked(node); 115 } 116 117 // Neutralize any script(s) about to be inserted, including inside a fragment. 118 function neutralizeBeforeInsert(node) { 119 if (!node || node.nodeType == null) return; 120 if (node.nodeType === 1) { 121 if (node.tagName === "SCRIPT") { 122 neutralizeScriptNode(node); 123 } else if (node.getElementsByTagName) { 124 var inner = node.getElementsByTagName("script"); 125 for (var i = 0; i < inner.length; i++) neutralizeScriptNode(inner[i]); 126 } 127 } else if (node.nodeType === 11 && node.querySelectorAll) { // DocumentFragment 128 var scripts = node.querySelectorAll("script"); 129 for (var j = 0; j < scripts.length; j++) neutralizeScriptNode(scripts[j]); 130 } 131 } 132 133 // ---- Capture originals up front so patched methods can call through safely. ---- 134 var elProto = (typeof Element !== "undefined" && Element.prototype) ? Element.prototype : null; 135 var nodeProto = (typeof Node !== "undefined" && Node.prototype) ? Node.prototype : null; 136 137 var origCreateElement = document.createElement; 138 var origSetAttribute = elProto ? elProto.setAttribute : null; 139 var origGetAttribute = elProto ? elProto.getAttribute : null; 140 var origRemoveAttribute = elProto ? elProto.removeAttribute : null; 141 var origAppendChild = nodeProto ? nodeProto.appendChild : null; 142 var origInsertBefore = nodeProto ? nodeProto.insertBefore : null; 143 var origReplaceChild = nodeProto ? nodeProto.replaceChild : null; 144 var origAppend = elProto ? elProto.append : null; 145 var origPrepend = elProto ? elProto.prepend : null; 146 var origWrite = document.write; 147 148 // 1) Intercept dynamically created <script> elements' src property assignment. 149 document.createElement = function (tagName) { 150 var el = origCreateElement.apply(document, arguments); 151 if (typeof tagName === "string" && tagName.toLowerCase() === "script") { 152 try { 153 Object.defineProperty(el, "src", { 154 configurable: true, 155 enumerable: true, 156 get: function () { return origGetAttribute.call(el, "src") || ""; }, 157 set: function (value) { 158 if (isAllowed(value)) { 159 origSetAttribute.call(el, "src", value); 160 } else { 161 origSetAttribute.call(el, "data-mf-blocked-src", value); 162 el.type = "javascript/blocked"; 163 rememberBlocked(el); 164 } 165 } 166 }); 167 } catch (e) { /* fall back to setAttribute / insertion patches */ } 168 } 169 return el; 170 }; 171 document.createElement.__chOriginal = origCreateElement; 172 173 // 2) Intercept setAttribute('src', ...) on <script> (bypasses the .src setter). 174 if (elProto && origSetAttribute) { 175 elProto.setAttribute = function (name, value) { 176 if (this && this.tagName === "SCRIPT" && 177 typeof name === "string" && name.toLowerCase() === "src" && 178 !isAllowed(value)) { 179 origSetAttribute.call(this, "data-mf-blocked-src", value); 180 origSetAttribute.call(this, "type", "javascript/blocked"); 181 rememberBlocked(this); 182 return; 183 } 184 return origSetAttribute.apply(this, arguments); 185 }; 186 } 187 188 // 3) Primary choke point: neutralize scripts at DOM-insertion time. This catches 189 // script.setAttribute('src', url); head.appendChild(script); and every other 190 // dynamic path, because the browser only fetches once the node is inserted. 191 if (nodeProto && origAppendChild) { 192 nodeProto.appendChild = function (node) { 193 neutralizeBeforeInsert(node); 194 return origAppendChild.call(this, node); 195 }; 196 } 197 if (nodeProto && origInsertBefore) { 198 nodeProto.insertBefore = function (node, ref) { 199 neutralizeBeforeInsert(node); 200 return origInsertBefore.call(this, node, ref); 201 }; 202 } 203 if (nodeProto && origReplaceChild) { 204 nodeProto.replaceChild = function (newNode, oldNode) { 205 neutralizeBeforeInsert(newNode); 206 return origReplaceChild.call(this, newNode, oldNode); 207 }; 208 } 209 if (elProto && origAppend) { 210 elProto.append = function () { 211 for (var i = 0; i < arguments.length; i++) neutralizeBeforeInsert(arguments[i]); 212 return origAppend.apply(this, arguments); 213 }; 214 } 215 if (elProto && origPrepend) { 216 elProto.prepend = function () { 217 for (var i = 0; i < arguments.length; i++) neutralizeBeforeInsert(arguments[i]); 218 return origPrepend.apply(this, arguments); 219 }; 220 } 221 222 // 4) Backstop for parser-inserted / otherwise-missed scripts. 223 var observer = null; 224 if (typeof MutationObserver !== "undefined") { 225 observer = new MutationObserver(function (mutations) { 226 for (var i = 0; i < mutations.length; i++) { 227 var added = mutations[i].addedNodes; 228 for (var j = 0; j < added.length; j++) neutralizeBeforeInsert(added[j]); 229 } 230 }); 231 try { 232 observer.observe(document.documentElement, { childList: true, subtree: true });
233 } catch (e) { /* no-op */ } 234 } 235 236 // 5) Strip non-allow-listed external scripts written via document.write. 237 document.write = function (markup) { 238 if (typeof markup === "string" && /<script/i.test(markup)) { 239 markup = markup.replace( 240 /<script\b[^>]*\bsrc\s*=\s*(['"])(.*?)\1[^>]*>(\s*<\/script>)?/gi, 241 function (full, quote, src) { 242 return isAllowed(src) ? full : "<!-- mf-blocked: " + src + " -->"; 243 } 244 ); 245 } 246 return origWrite.call(document, markup); 247 }; 248 249 // 6) Re-activation hook, called by the consent banner on Accept. 250 window.chReactivateBlockedScripts = function () { 251 // Restore all patched APIs FIRST so re-inserting scripts is not re-blocked. 252 try { document.createElement = origCreateElement; } catch (e) {} 253 try { document.write = origWrite; } catch (e) {} 254 if (elProto) { 255 try { if (origSetAttribute) elProto.setAttribute = origSetAttribute; } catch (e) {} 256 try { if (origAppend) elProto.append = origAppend; } catch (e) {} 257 try { if (origPrepend) elProto.prepend = origPrepend; } catch (e) {} 258 } 259 if (nodeProto) { 260 try { if (origAppendChild) nodeProto.appendChild = origAppendChild; } catch (e) {} 261 try { if (origInsertBefore) nodeProto.insertBefore = origInsertBefore; } catch (e) {} 262 try { if (origReplaceChild) nodeProto.replaceChild = origReplaceChild; } catch (e) {} 263 } 264 if (observer) { try { observer.disconnect(); } catch (e) {} } 265 266 var pending = blocked; 267 blocked = []; 268 for (var i = 0; i < pending.length; i++) { 269 var old = pending[i]; 270 var src = origGetAttribute 271 ? origGetAttribute.call(old, "data-mf-blocked-src") 272 : old.getAttribute("data-mf-blocked-src"); 273 if (!src) continue; 274 var s = origCreateElement.call(document, "script"); 275 s.async = true; 276 s.src = src; 277 var parent = old.parentNode || document.head || document.documentElement; 278 origAppendChild.call(parent, s); 279 } 280 }; 281})();
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.