1/* global CookieConsent */ 2 3'use strict'; 4 5(function () { 6 var COOKIE_NAME = 'cc_cookie'; 7 8 // Delivered by the theme's filter:config.get hook. window.__piConsentDomain 9 // is the pre-54bd4e6 fallback, when the (now removed) header.tpl inlined it. 10 function settings() { 11 var cfg = (window.config && window.config.piConsent) || {}; 12 return { 13 cookieDomain: cfg.cookieDomain || window.__piConsentDomain || '', 14 gtmId: cfg.gtmId || '', 15 }; 16 } 17 18 // Signed-in summary for the analytics user properties. lib/theme.js 19 // (injectConsentScripts) writes it as a JSON data block next to these script 20 // tags â a data block never executes, so CSP is not a concern. 21 function readUser() { 22 var el = document.getElementById('pi-user'); 23 if (!el) return { uid: 0, tier: 'guest' }; 24 try { 25 var parsed = JSON.parse(el.textContent || '{}'); 26 return { uid: Number(parsed.uid) || 0, tier: parsed.tier || 'guest' }; 27 } catch (_err) { 28 return { uid: 0, tier: 'guest' }; 29 } 30 } 31 32 // Category / page type for the forum pageview push, from ajaxify's page data. 33 function forumCategoryName(page) { 34 if (!page) return ''; 35 if (page.template && page.template.name === 'category' && page.name) return page.name; 36 if (page.category && page.category.name) return page.category.name; 37 return ''; 38 } 39 40 function hasConsent(category) { 41 var needle = COOKIE_NAME + '='; 42 var parts = document.cookie.split(';'); 43 44 for (var i = 0; i < parts.length; i += 1) { 45 var value = parts[i].trim(); 46 if (!value.startsWith(needle)) continue; 47 48 var raw = value.slice(needle.length); 49 var parsed; 50 51 try { 52 parsed = JSON.parse(decodeURIComponent(raw)); 53 } catch (_decodeErr) { 54 try { 55 parsed = JSON.parse(raw); 56 } catch (_jsonErr) { 57 parsed = null; 58 } 59 } 60 61 return !!(parsed && Array.isArray(parsed.categories) && parsed.categories.includes(category)); 62 } 63 64 return false; 65 } 66 67 // Run `callback` as soon as `category` is granted â now, or whenever the 68 // visitor accepts later. Never runs if they decline. 69 function whenConsented(category, callback) { 70 if (hasConsent(category)) { 71 callback(); 72 return; 73 } 74 75 var onConsent = function () { 76 if (!hasConsent(category)) return; 77 window.removeEventListener('cc:onConsent', onConsent); 78 window.removeEventListener('cc:onChange', onConsent); 79 window.removeEventListener('pi:consent:changed', onConsent); 80 callback(); 81 }; 82 83 window.addEventListener('cc:onConsent', onConsent); 84 window.addEventListener('cc:onChange', onConsent); 85 window.addEventListener('pi:consent:changed', onConsent); 86 } 87 88 function ensureGptScript(onReady) { 89 if (window.__piGptScriptLoaded) { 90 onReady(); 91 return; 92 } 93 94 if (window.__piGptScriptLoading) { 95 window.__piGptQueue = window.__piGptQueue || []; 96 window.__piGptQueue.push(onReady); 97 return; 98 } 99 100 window.__piGptScriptLoading = true; 101 window.__piGptQueue = [onReady]; 102 103 var script = document.createElement('script'); 104 script.async = true; 105 script.src = 'https://securepubads.g.doubleclick.net/tag/js/gpt.js'; 106 107 script.onload = function () { 108 window.__piGptScriptLoaded = true; 109 window.__piGptScriptLoading = false; 110 var queue = window.__piGptQueue || []; 111 window.__piGptQueue = []; 112 queue.forEach(function (fn) { 113 try { 114 fn(); 115 } catch (_err) { 116 // swallow callback errors 117 } 118 }); 119 }; 120 121 script.onerror = function () { 122 window.__piGptScriptLoading = false; 123 window.__piGptQueue = []; 124 }; 125 126 document.head.appendChild(script); 127 } 128 129 function createConsentConfig() { 130 var domain = settings().cookieDomain; 131 132 return { 133 disablePageInteraction: true, 134 cookie: { 135 name: COOKIE_NAME, 136 domain: domain, 137 path: '/', 138 sameSite: 'Lax', 139 expiresAfterDays: 182, 140 }, 141 guiOptions: { 142 consentModal: { 143 layout: 'box', 144 position: 'middle center', 145 equalWeightButtons: true, 146 }, 147 preferencesModal: { 148 layout: 'box', 149 equalWeightButtons: true, 150 }, 151 }, 152 categories: { 153 necessary: { 154 enabled: true, 155 readOnly: true, 156 }, 157 ads: { 158 enabled: false, 159 }, 160 analytics: { 161 enabled: false, 162 }, 163 functional: { 164 enabled: false, 165 }, 166 }, 167 language: { 168 default: 'da', 169 autoDetect: 'document', 170 translations: { 171 da: { 172 consentModal: { 173 title: 'Vi bruger cookies', 174 description: 'Vi bruger nødvendige cookies til drift og valgfri co
174okies til funktioner og annoncer. Du kan altid ændre dit valg via cookieknappen nederst til venstre.', 175 acceptAllBtn: 'Accepter alle', 176 acceptNecessaryBtn: 'Kun nødvendige', 177 showPreferencesBtn: 'Tilpas valg', 178 }, 179 preferencesModal: { 180 title: 'Cookieindstillinger', 181 acceptAllBtn: 'Accepter alle', 182 acceptNecessaryBtn: 'Kun nødvendige', 183 savePreferencesBtn: 'Gem valg', 184 closeIconLabel: 'Luk', 185 sections: [ 186 { 187 title: 'SÃ¥dan bruger vi cookies', 188 description: 'Du kan vælge, hvilke valgfri cookies vi mÃ¥ bruge. Nødvendige cookies kan ikke fravælges.', 189 }, 190 { 191 title: 'Nødvendige cookies', 192 description: 'PÃ¥krævet for login, sikkerhed og grundlæggende drift.', 193 linkedCategory: 'necessary', 194 }, 195 { 196 title: 'Funktionelle cookies', 197 description: 'Forbedrer funktioner og brugeroplevelse.', 198 linkedCategory: 'functional', 199 }, 200 { 201 title: 'Statistikcookies', 202 description: 203 'Bruges til at mÃ¥le trafik og forstÃ¥, hvordan siden bruges, sÃ¥ vi kan forbedre den.', 204 linkedCategory: 'analytics', 205 }, 206 { 207 title: 'Annoncecookies', 208 description: 'Bruges til annoncering og mÃ¥ling af annoncelevering.', 209 linkedCategory: 'ads', 210 }, 211 { 212 title: 'Mere information', 213 description: 'Læs mere i vores <a href="/privatlivspolitik">privatlivspolitik</a>.', 214 }, 215 ], 216 }, 217 }, 218 en: { 219 consentModal: { 220 title: 'We use cookies', 221 description: 'We use necessary cookies for core functionality and optional cookies for features and ads. You can always update your choice with the cookie button in the bottom-left corner.', 222 acceptAllBtn: 'Accept all', 223 acceptNecessaryBtn: 'Necessary only', 224 showPreferencesBtn: 'Manage choices', 225 }, 226 preferencesModal: { 227 title: 'Cookie preferences', 228 acceptAllBtn: 'Accept all', 229 acceptNecessaryBtn: 'Necessary only', 230 savePreferencesBtn: 'Save choices', 231 closeIconLabel: 'Close', 232 sections: [ 233 { 234 title: 'How we use cookies', 235 description: 'Choose which optional cookie categories you allow. Necessary cookies cannot be disabled.', 236 }, 237 { 238 title: 'Necessary cookies', 239 description: 'Required for login, security and core functionality.', 240 linkedCategory: 'necessary', 241 }, 242 { 243 title: 'Functional cookies', 244 description: 'Improve features and user experience.', 245 linkedCategory: 'functional', 246 }, 247 { 248 title: 'Analytics cookies', 249 description: 250 'Used to measure traffic and understand how the site is used so we can improve it.', 251 linkedCategory: 'analytics', 252 }, 253 { 254 title: 'Advertising cookies', 255 description: 'Used for ad delivery and ad measurement.', 256 linkedCategory: 'ads', 257 }, 258 { 259 title: 'More information', 260 description: 'See our <a href="/privatlivspolitik">privacy policy</a>.', 261 }, 262 ], 263 }, 264 }, 265 }, 266 }, 267 onFirstConsent: function () { 268 window.dispatchEvent(new Event('pi:consent:changed')); 269 }, 270 onConsent: function () { 271 window.dispatchEvent(new Event('pi:consent:changed')); 272 }, 273 onChange: function () { 274 window.dispatchEvent(new Event('pi:consent:changed')); 275 }, 276 }; 277 } 278 279 function initConsent() { 280 if (!window.CookieConsent || window.__piConsentInitialized) { 281 window.dispatchEvent(new Event('pi:consent:ready')); 282 return; 283 } 284 285 window.CookieConsent.run(createConsentConfig()); 286 window.__piConsentInitialized = true; 287 window.dispatchEvent(new Event('pi:consent:ready')); 288 } 289 290 window.piShowConsentPreferences = function () { 291 if (window.CookieConsent && typeof window.CookieConsent.showPreferences === 'function') { 292 window.CookieConsent.showPreferences(); 293 } 294 }; 295 296 window.piLoadGptIfConsented = function (callback) { 297 whenConsented('ads', function () { 298 ensureGptScript(callback); 299 }); 300 }; 301 302 // ââ Google Tag Manager ââââââââââââââââââââââââââââââââââââââââââââââââââââââ 303 // Same container as the Next.js side (frontend/src/components/
303analytics/ 304 // GtmLoader.tsx), so forum and site sessions land in one GA property. Loads 305 // only once the 'analytics' category is granted â the CMP above writes the 306 // same first-party cc_cookie the frontend reads, so a choice made on either 307 // end of the site is honoured on the other. 308 309 // ââ Event tracking ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 310 // Forum-side twin of frontend/src/lib/analytics/track.ts: same event names, 311 // same `pi` parameter namespace, same consent rule. Catalogue and GTM setup 312 // in docs/analytics.md. Nothing is pushed without the analytics grant â 313 // pre-consent pushes would be flushed to GA the moment GTM boots. The `pi` 314 // key is reset before every push because GTM's data layer model merges 315 // pushes; without it a `channel` from one event leaks onto the next. 316 window.piTrack = function (eventName, params) { 317 if (!eventName || !hasConsent('analytics')) return; 318 var clean = {}; 319 Object.keys(params || {}).forEach(function (key) { 320 var value = params[key]; 321 if (value === undefined || value === null || value === '') return; 322 clean[key] = value; 323 }); 324 window.dataLayer = window.dataLayer || []; 325 window.dataLayer.push({ pi: null }); 326 window.dataLayer.push({ event: eventName, pi: clean }); 327 }; 328 329 function bootGtm(gtmId) { 330 if (window.__piGtmLoaded) return; 331 window.__piGtmLoaded = true; 332 333 window.dataLayer = window.dataLayer || []; 334 335 // User state goes in BEFORE gtm.start so the Google tag reads user_id / 336 // member_tier on the first page_view. Same on the Next.js side (GtmLoader). 337 var user = readUser(); 338 var userState = { member_tier: user.tier }; 339 if (user.uid > 0) userState.user_id = String(user.uid); 340 window.dataLayer.push(userState); 341 342 window.dataLayer.push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' }); 343 344 var script = document.createElement('script'); 345 script.async = true; 346 script.src = 'https://www.googletagmanager.com/gtm.js?id=' + encodeURIComponent(gtmId); 347 document.head.appendChild(script); 348 349 // NodeBB is a SPA: after the first load every navigation is ajaxify, which 350 // fires no browser pageview. Push one per route change so the forum does 351 // not report a single pageview per session. 352 if (window.$ && typeof window.$(window).on === 'function') { 353 window.$(window).on('action:ajaxify.end', function (ev, data) { 354 var page = (window.ajaxify && window.ajaxify.data) || {}; 355 window.dataLayer.push({ pi: null }); 356 window.dataLayer.push({ 357 event: 'pi.pageview', 358 page_path: (window.config && window.config.relative_path ? window.config.relative_path : '') + 359 '/' + ((data && data.url) || ''), 360 page_title: document.title, 361 // Page type (topic / category / recent / â¦) and category name, so 362 // "which categories are read" is a report. Mapped by the pi.pageview 363 // tag in GTM â see docs/analytics.md. 364 pi: { 365 forum_template: (page.template && page.template.name) || '', 366 forum_category: forumCategoryName(page), 367 }, 368 }); 369 }); 370 } 371 } 372 373 function initGtm() { 374 var gtmId = settings().gtmId; 375 if (!gtmId) return; 376 whenConsented('analytics', function () { 377 bootGtm(gtmId); 378 }); 379 } 380 381 initConsent(); 382 initGtm(); 383}());
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.