PageSourceSearch

https://www.westchesterhills.org/common/Scripts/cookie_consent_blocker.js

js westchesterhills.org collected 2026-10-05 19:07:15 UTC 14,254 bytes, 346 lines download raw bytes

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, '&quot;') + '"');
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, '&quot;') + '" 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.