PageSourceSearch

https://www.teknion.com/ResourcePackages/Shared/assets/js/consent-gate.js

js teknion.com collected 2026-10-02 17:16:45 UTC 11,314 bytes, 283 lines download raw bytes

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.