1/* 2 * Cookie consent gate. 3 * 4 * MUST be loaded synchronously in <head> BEFORE any tracking tag (Google 5 * gtag/GTM, ZoomInfo WebSights, etc.). Two jobs: 6 * 7 * 1. Sets the Google Consent Mode v2 default to "denied" for all four 8 * parameters. Google requires this to be pushed before gtag.js loads, 9 * otherwise the tag writes _ga cookies before the visitor has chosen. 10 * 11 * 2. Provides TeknionConsent.whenAccepted(fn) for tags that must be withheld 12 * entirely rather than run in a restricted mode, i.e. anything with no 13 * consent API of its own. No tag uses this yet: ZoomInfo WebSights is 14 * deliberately left ungated for now. 15 * 16 * The authoritative decision comes from the CookieDisclaimer widget, which 17 * knows the visitor's geo and whether a banner applies. It signals via the 18 * cookieConsentAccepted / cookieConsentRejected window events. This gate 19 * deliberately does NOT read the consent cookie itself: the cookie name is 20 * per-geo (cookie_consent_<geo>) and resolved server side, so duplicating 21 * that logic here would risk honouring a choice made in a different geo. 22 * 23 * TEMPLATES WITHOUT THE WIDGET 24 * Page templates are assembled in the Sitefinity backend, so a layout file 25 * cannot know whether the widget was placed on the template using it. That is 26 * resolved on DOMContentLoaded: the widget renders its script inline in the 27 * body, so once the document has finished parsing, "nothing has signalled" is 28 * proof the widget is not on the page -- an ordering guarantee, not a timing 29 * guess. Those templates are then granted full consent, which is how they 30 * behaved before this gate existed. 31 * 32 * So the gate fails CLOSED while a decision is pending, and fails OPEN only 33 * once it has established there is no widget to decide. Note the widget 34 * signals in every case it is present, including an undecided visitor in a 35 * banner geo -- that dispatches cookieConsentRejected on load. 36 */ 37(function (w, d) { 38 'use strict'; 39 40 if (w.TeknionConsent) { 41 return; 42 } 43 44 // --- Diagnostics -------------------------------------------------------- 45 // Off by default so production consoles stay clean. Turn on with 46 // ?consentdebug=1 (sticks for the rest of the browser tab), off with 47 // ?consentdebug=0. TeknionConsent.report() always works, flag or not. 48 var debug = false; 49 try { 50 var search = (w.location && w.location.search) || ''; 51 if (search.indexOf('consentdebug=1') > -1) { 52 w.sessionStorage.setItem('teknionConsentDebug', '1'); 53 } else if (search.indexOf('consentdebug=0') > -1) { 54 w.sessionStorage.removeItem('teknionConsentDebug'); 55 } 56 debug = w.sessionStorage.getItem('teknionConsentDebug') === '1'; 57 } catch (e) { 58 // Storage blocked (private mode / cookie settings). Diagnostics stay off. 59 } 60 61 function trace(msg) { 62 if (debug && w.console && w.console.log) { 63 w.console.log('%c[consent]%c ' + msg, 'color:#0a7;font-weight:bold', ''); 64 } 65 } 66 67 // Records why tracking ended up on or off, for report(). 68 var decidedBy = 'nothing yet (default deny still in force)'; 69 70 // --- Google Consent Mode v2 default ------------------------------------- 71 // All four parameters are required since v2; ad_user_data and 72 // ad_personalization were added on top of the original two. 73 w.dataLayer = w.dataLayer || []; 74 function gtagShim() { 75 w.dataLayer.push(arguments); 76 } 77 if (typeof w.gtag !== 'function') { 78 w.gtag = gtagShim; 79 } 80 gtagShim('consent', 'default', { 81 'ad_storage': 'denied', 82 'ad_user_data': 'denied', 83 'ad_personalization': 'denied', 84 'analytics_storage': 'denied', 85 // Hold tags briefly so the banner can call consent/update first. 86 'wait_for_update': 500 87 }); 88 trace('gate loaded. Default: all four signals DENIED, awaiting a decision.'); 89 90 function setGoogleConsent(value) { 91 gtagShim('consent', 'update', { 92 'ad_storage': value, 93 'ad_user_data': value, 94 'ad_personalization': value,
95 'analytics_storage': value 96 }); 97 } 98 99 // --- Deferred tag loading ---------------------------------------------- 100 var registered = []; 101 var granted = false; 102 var signalled = false; // has the CookieDisclaimer widget said anything? 103 104 function run(entry) { 105 if (entry.done) { 106 return; 107 } 108 entry.done = true; 109 try { 110 entry.fn(); 111 } catch (e) { 112 if (w.console && w.console.error) { 113 w.console.error('consent-gate: gated tag failed', e); 114 } 115 } 116 } 117 118 function grant() { 119 granted = true; 120 for (var i = 0; i < registered.length; i++) { 121 run(registered[i]); 122 } 123 } 124 125 function deny() { 126 // Entries are kept, not dropped, so accepting later in the same page 127 // view (via the footer "Cookie Settings" trigger) still loads them. 128 granted = false; 129 } 130 131 w.TeknionConsent = { 132 whenAccepted: function (fn) { 133 if (typeof fn !== 'function') { 134 return; 135 } 136 var entry = { fn: fn, done: false }; 137 registered.push(entry); 138 if (granted) { 139 run(entry); 140 } 141 }, 142 isGranted: function () { 143 return granted; 144 }, 145 grant: grant, 146 deny: deny 147 }; 148 149 w.addEventListener('cookieConsentAccepted', function () { 150 if (!signalled) { 151 decidedBy = 'Cookie Disclaimer widget signalled ACCEPTED ' + 152 '(visitor accepted, or this geo has no banner configured)'; 153 } 154 signalled = true; 155 trace('widget signalled ACCEPTED -> tracking ON'); 156 grant(); 157 }); 158 w.addEventListener('cookieConsentRejected', function () { 159 if (!signalled) { 160 decidedBy = 'Cookie Disclaimer widget signalled REJECTED ' + 161 '(visitor rejected, or has not chosen yet)'; 162 } 163 signalled = true; 164 trace('widget signalled REJECTED -> tracking OFF'); 165 deny(); 166 }); 167 168 // --- No widget on this template -> full tracking ------------------------- 169 function grantIfNoWidget() { 170 if (signalled) { 171 return; 172 } 173 // Safety net for the case this script is ever made async/deferred: a 174 // rendered banner proves the widget is present and its signal was simply 175 // missed, so leave the decision to it rather than granting over the top. 176 if (d.getElementById('cookieDisclaimerBanner')) { 177 trace('banner is on the page but never signalled - leaving it denied. ' + 178 'Is this script async/deferred? It must be synchronous.'); 179 return; 180 } 181 decidedBy = 'no Cookie Disclaimer widget on this template -> granted ' + 182 'at DOMContentLoaded (pre-gate behaviour)'; 183 // Set before dispatching below: the event re-enters our own accept 184 // listener, which would otherwise overwrite decidedBy and make report() 185 // claim the widget decided this. 186 signalled = true; 187 trace('no widget on this template -> tracking ON (full, as before the gate)'); 188 setGoogleConsent('granted'); 189 grant(); 190 // Keeps page-level listeners consistent with the widget's own signalling. 191 w.dispatchEvent(new CustomEvent('cookieConsentAccepted')); 192 } 193 194 if (d.readyState === 'loading') { 195 d.addEventListener('DOMContentLoaded', grantIfNoWidget); 196 } else { 197 grantIfNoWidget(); 198 } 199 200 // --- report() ----------------------------------------------------------- 201 // Human-readable snapshot. Call TeknionConsent.report() in the console at 202 // any time; also runs automatically when ?consentdebug=1 is on. 203 function cookieNames(prefixes) { 204 var found = []; 205 var jar = d.cookie ? d.cookie.split(';') : []; 206 for (var i = 0; i < jar.length; i++) { 207 var name = jar[i].split('=')[0].replace(/^\s+|\s+$/g, ''); 208 for (var j = 0; j < prefixes.length; j++) { 209 if (name && name.indexOf(prefixes[j]) === 0) { 210 found.push(name); 211 break; 212 } 213 } 214 } 215 return found; 216 } 217 218 function scriptsFrom(host) { 219 var hits = []; 220 var tags = d.getElementsByTagName('script'); 221 for (var i = 0; i < tags.length; i++) { 222 if (tags[i].src && tags[i].src.indexOf(host) > -1) { 223 hits.push(tags[i].src); 224 } 225 } 226 return hits; 227 } 228 229 function report() { 230 if (!w.console || !w.console.log) { 231 return; 232 } 233 var consentPushes = []; 234 for (var i = 0; i < w.dataLayer.length; i++) { 235 var a = w.dataLayer[i]; 236 if (a && a[0] === 'consent') { 237 consentPushes.push(a[1] + ': ' + JSON.stringify(a[2])); 238 } 239 } 240 241 var lines = [
242 ['TRACKING', granted ? 'ON (cookies allowed)' : 'OFF (denied - cookieless pings only)'], 243 ['decided by', decidedBy], 244 ['banner rendered on page', d.getElementById('cookieDisclaimerBanner') ? 'yes' : 'no'], 245 ['consent cookie', cookieNames(['cookie_consent_']).join(', ') || 'none stored yet'], 246 ['tracker cookies now', cookieNames(['_ga', '_gid', '_gat', '_gcl', '_fbp', '_fbc', '_hj', '_zitok', 'zi_']).join(', ') || 'none'], 247 ['google tag on page', scriptsFrom('googletagmanager.com').length ? 'yes' : 'no'], 248 ['zoominfo on page', scriptsFrom('ws.zoominfo.com').length ? 'yes (ungated by design)' : 'no'], 249 ['gated tags waiting', String(registered.length ? registered.length : 0)], 250 ['consent pushed to dataLayer', consentPushes.join(' | ') || 'none'] 251 ]; 252 253 var title = 'Cookie consent: TRACKING ' + (granted ? 'ON' : 'OFF'); 254 if (w.console.group) { 255 w.console.group(title); 256 } else { 257 w.console.log(title); 258 } 259 for (var k = 0; k < lines.length; k++) { 260 w.console.log(' ' + lines[k][0] + ': ' + lines[k][1]); 261 } 262 w.console.log(' (tip: add ?consentdebug=1 to log every transition live)'); 263 if (w.console.groupEnd) { 264 w.console.groupEnd(); 265 } 266 } 267 268 w.TeknionConsent.report = report; 269 270 // Auto-report once things have settled: the decision is made at 271 // DOMContentLoaded, but Google writes its cookies a moment later, so wait 272 // before snapshotting or the cookie list reads as empty when it is not. 273 if (debug && w.setTimeout) { 274 var autoReport = function () { 275 w.setTimeout(report, 1500); 276 }; 277 if (d.readyState === 'loading') { 278 d.addEventListener('DOMContentLoaded', autoReport); 279 } else { 280 autoReport(); 281 } 282 } 283})(window, document);
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.