1/** 2 * LW Cookie â Consent Manager (v2.0 â cache-proof) 3 * 4 * Reads consent state from the browser cookie (not from PHP config). 5 * Saves consent via REST API (not admin-ajax). 6 * Integrates with guard.js via window.__lwGuard.refresh(). 7 * 8 * @package LightweightPlugins\Cookie 9 */ 10 11(function () { 12 'use strict'; 13 14 var config = window.lwCookieConfig || {}; 15 var COOKIE_NAME = config.cookieName || 'lw_cookie_consent'; 16 17 /** 18 * Read consent data from the browser cookie. 19 * 20 * @return {Object|null} Parsed consent or null. 21 */ 22 function readConsentCookie() { 23 var match = document.cookie.match( '(?:^|; )' + COOKIE_NAME + '=([^;]*)' ); 24 if ( ! match ) { 25 return null; 26 } 27 try { 28 return JSON.parse( atob( match[1] ) ); 29 } catch ( e ) { 30 return null; 31 } 32 } 33 34 /** 35 * Check if stored consent matches current policy version. 36 */ 37 function isConsentValid() { 38 var data = readConsentCookie(); 39 return data && data.version === config.policyVersion && data.categories; 40 } 41 42 /** 43 * Get current consent categories from cookie. 44 * 45 * @return {Object} categories or empty fallback. 46 */ 47 function getConsentCategories() { 48 var data = readConsentCookie(); 49 if ( data && data.version === config.policyVersion && data.categories ) { 50 return data.categories; 51 } 52 return { necessary: true }; 53 } 54 55 /** 56 * Initialize the consent manager. 57 */ 58 function init() { 59 bindEvents(); 60 } 61 62 /** 63 * Bind event listeners. 64 */ 65 function bindEvents() { 66 document.querySelectorAll( '[data-lw-cookie-accept]' ).forEach( 67 function ( btn ) { 68 btn.addEventListener( 'click', acceptAll ); 69 } 70 ); 71 72 document.querySelectorAll( '[data-lw-cookie-reject]' ).forEach( 73 function ( btn ) { 74 btn.addEventListener( 'click', rejectAll ); 75 } 76 ); 77 78 document.querySelectorAll( '[data-lw-cookie-customize]' ).forEach( 79 function ( btn ) { 80 btn.addEventListener( 'click', openPreferences ); 81 } 82 ); 83 84 document.querySelectorAll( '[data-lw-cookie-save]' ).forEach( 85 function ( btn ) { 86 btn.addEventListener( 'click', savePreferences ); 87 } 88 ); 89 90 document.querySelectorAll( '[data-lw-cookie-close-modal]' ).forEach( 91 function ( el ) { 92 el.addEventListener( 'click', closePreferences ); 93 } 94 ); 95 96 document.querySelectorAll( '[data-lw-cookie-open-preferences]' ).forEach( 97 function ( btn ) { 98 btn.addEventListener( 'click', openPreferences ); 99 } 100 ); 101 102 document.addEventListener( 103 'keydown', 104 function ( e ) { 105 if ( e.key === 'Escape' ) { 106 closePreferences(); 107 } 108 } 109 ); 110 } 111 112 /** 113 * Show the cookie banner. 114 */ 115 function showBanner() { 116 var banner = document.getElementById( 'lw-cookie-notice' ); 117 if ( banner ) { 118 banner.classList.remove( 'lw-cookie-hidden' ); 119 banner.style.display = ''; 120 } 121 } 122 123 /** 124 * Hide the cookie banner. 125 */ 126 function hideBanner() { 127 var banner = document.getElementById( 'lw-cookie-notice' ); 128 if ( banner ) { 129 banner.classList.add( 'lw-cookie-hidden' ); 130 } 131 } 132 133 /** 134 * Open preferences modal. 135 */ 136 function openPreferences() { 137 hideBanner(); 138 var modal = document.getElementById( 'lw-cookie-preferences' ); 139 if ( modal ) { 140 modal.style.display = 'flex'; 141 modal.classList.remove( 'lw-cookie-hidden' ); 142 143 setCheckboxStates(); 144 145 var firstCheckbox = modal.querySelector( 'input[type="checkbox"]:not(:disabled)' ); 146 if ( firstCheckbox ) { 147 firstCheckbox.focus(); 148 } 149 } 150 } 151 152 /** 153 * Close preferences modal. 154 */ 155 function closePreferences() { 156 var modal = document.getElementById( 'lw-cookie-preferences' ); 157 if ( modal ) { 158 modal.style.display = 'none'; 159 modal.classList.add( 'lw-cookie-hidden' ); 160 } 161 162 // Show banner again if no valid consent. 163 if ( ! isConsentValid() ) { 164 showBanner(); 165 } 166 } 167 168 /** 169 * Set checkbox states from cookie (not from PHP config). 170 */ 171 function setCheckboxStates() { 172 var categories = getConsentCategories(); 173 174 Object.keys( categories ).forEach( 175 function ( key ) { 176 var checkbox = document.querySelector( '[data-category="' + key + '"]' ); 177 if ( checkbox && ! checkbox.disabled ) { 178 checkbox.checked = categories[key]; 179 } 180 } 181 ); 182 } 183 184 /** 185 * Accept all cookies. 186 */ 187 function acceptAll() { 188 var categories = { 189 necessary: true, 190 functional: true, 191 analytics: true, 192 marketing: true 193 }; 194 195 saveConsent( categories, 'accept_all' ); 196 } 197 198 /** 199 * Reject all optional cookies. 200 */ 201 function rejectAll() { 202 var categories = { 203 necessary: true, 204 functional: false,
205 analytics: false, 206 marketing: false 207 }; 208 209 saveConsent( categories, 'reject_all' ); 210 } 211 212 /** 213 * Save custom preferences. 214 */ 215 function savePreferences() { 216 var categories = { 217 necessary: true, 218 functional: isChecked( 'functional' ), 219 analytics: isChecked( 'analytics' ), 220 marketing: isChecked( 'marketing' ) 221 }; 222 223 saveConsent( categories, 'customize' ); 224 closePreferences(); 225 } 226 227 /** 228 * Check if a category checkbox is checked. 229 * 230 * @param {string} category Category key. 231 * @return {boolean} 232 */ 233 function isChecked( category ) { 234 var checkbox = document.querySelector( '[data-category="' + category + '"]' ); 235 return checkbox ? checkbox.checked : false; 236 } 237 238 /** 239 * Save consent to cookie and server. 240 * 241 * @param {Object} categories Category consent states. 242 * @param {string} actionType Action type. 243 * @param {boolean} skipReload Skip page reload. 244 */ 245 function saveConsent( categories, actionType, skipReload ) { 246 // Save to cookie immediately. 247 saveCookie( categories ); 248 249 // Hide banner. 250 hideBanner(); 251 252 // Notify guard.js to update SW, GCM, observer, etc. 253 var synced = null; 254 if ( window.__lwGuard && window.__lwGuard.refresh ) { 255 synced = window.__lwGuard.refresh( categories ); 256 } 257 258 // Send to server via REST API (fire-and-forget; keepalive lets it outlive a reload). 259 if ( config.restUrl ) { 260 fetch( 261 config.restUrl, 262 { 263 method: 'POST', 264 headers: { 'Content-Type': 'application/json' }, 265 body: JSON.stringify( 266 { 267 categories: categories, 268 action_type: actionType 269 } 270 ), 271 credentials: 'same-origin', 272 // Survive the reload that may follow right after accepting. 273 keepalive: true 274 } 275 ).catch( 276 function ( error ) { 277 console.error( 'LW Cookie: Failed to log consent', error ); 278 } 279 ); 280 } 281 282 // Dispatch events for other scripts. 283 dispatchConsentEvent( categories, actionType ); 284 285 // Reload only if scripts were previously blocked and now need loading. 286 // The SW can block but cannot retroactively load scripts. 287 if ( ! skipReload && needsReload( categories ) ) { 288 reloadWhenSynced( synced ); 289 } 290 } 291 292 /** 293 * Reload once the Service Worker holds the new consent state. 294 * 295 * The reloaded page requests its scripts before guard.js can message the 296 * worker, so the worker must know about the change beforehand. Waits for 297 * guard.js's sync (which caps the wait itself), and at least 100 ms so the 298 * consent log request is on its way. 299 * 300 * @param {Promise|null} synced Settles when the SW has the new state. 301 */ 302 function reloadWhenSynced( synced ) { 303 var reload = function () { 304 window.location.reload(); 305 }; 306 307 if ( typeof Promise === 'undefined' ) { 308 setTimeout( reload, 100 ); 309 return; 310 } 311 312 var minDelay = new Promise( 313 function ( resolve ) { 314 setTimeout( resolve, 100 ); 315 } 316 ); 317 318 Promise.all( [ synced, minDelay ] ).then( reload, reload ); 319 } 320 321 /** 322 * Check if a page reload is needed. 323 * 324 * Reload if any previously blocked category is now allowed, 325 * since blocked scripts cannot be un-blocked without reload. 326 * 327 * @param {Object} categories New categories. 328 * @return {boolean} 329 */ 330 function needsReload( categories ) { 331 return categories.analytics || categories.marketing || categories.functional; 332 } 333 334 /** 335 * Save consent to cookie. 336 * 337 * @param {Object} categories Category consent states. 338 */ 339 function saveCookie( categories ) { 340 var consent = { 341 id: generateUUID(), 342 version: config.policyVersion, 343 timestamp: Math.floor( Date.now() / 1000 ), 344 categories: categories 345 }; 346 347 var cookieValue = btoa( JSON.stringify( consent ) ); 348 var duration = config.consentDuration || 365; 349 var expires = new Date(); 350 expires.setDate( expires.getDate() + duration ); 351 352 document.cookie = COOKIE_NAME + '=' + cookieValue + 353 ';expires=' + expires.toUTCString() + 354 ';path=/;SameSite=Lax'; 355 } 356 357 /** 358 * Delete all cookies for the current domain. 359 */ 360 function deleteAllCookies() { 361 var cookies = document.cookie.split( ';' ); 362 var domain = window.location.hostname; 363 var paths = ['/', window.location.pathname]; 364 var cookiesLength = cookies.length; 365 var pathsLength = paths.length; 366 367 for ( var i = 0; i < cookiesLength; i++ ) { 368 var cookie = cookies[i]; 369 var eqPos = cookie.indexOf( '=' ); 370 var cookieName = eqPos > -1 ? cookie.substr( 0, eqPos ).trim() : cookie.trim(); 371 372 if ( ! cookieName ) { 373 continue; 374 } 375 376 for ( var j = 0; j < pathsLength; j++ ) { 377 document.cookie = cookieName + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=' + paths[j]; 378 document.cookie = cookieName + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=' + paths[j] + ';domain=' + domain; 379 380 if ( domain.indexOf( 'www.' ) !== 0 ) { 381 document.cookie = cookieName + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=' + paths[j] + ';domain=.' + domain; 382 } 383 } 384 } 385 386 window.location.reload(); 387 } 388 389 /** 390 * Generate a UUID v4. 391 * 392 * @return {string} 393 */ 394 function generateUUID() { 395 return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace( 396 /[xy]/g, 397 function ( c ) { 398 var r = Math.random() * 16 | 0; 399 var v = c === 'x' ? r : (r & 0x3 | 0x8); 400 return v.toString( 16 ); 401 } 402 ); 403 } 404 405 /** 406 * Dispatch consent event for other scripts. 407 * 408 * @param {Object} categories Category consent states. 409 * @param {string} actionType Action type. 410 */ 411 function dispatchConsentEvent( categories, actionType ) { 412 var event = new CustomEvent( 413 'lwCookieConsent', 414 {
415 detail: { 416 categories: categories, 417 action: actionType 418 } 419 } 420 ); 421 window.dispatchEvent( event ); 422 423 window.dataLayer = window.dataLayer || []; 424 window.dataLayer.push( 425 { 426 'event': 'lw_cookie_consent_update', 427 'lw_cookie_consent': { 428 'necessary': true, 429 'functional': categories.functional || false, 430 'analytics': categories.analytics || false, 431 'marketing': categories.marketing || false 432 }, 433 'lw_cookie_action': actionType 434 } 435 ); 436 437 // GCM v2 update is handled by guard.js.refresh() above. 438 439 // Meta Pixel consent API. 440 if ( typeof fbq === 'function' ) { 441 if ( categories.marketing ) { 442 fbq( 'consent', 'grant' ); 443 } else { 444 fbq( 'consent', 'revoke' ); 445 } 446 } 447 } 448 449 // Initialize on DOM ready. 450 if ( document.readyState === 'loading' ) { 451 document.addEventListener( 'DOMContentLoaded', init ); 452 } else { 453 init(); 454 } 455 456 /** 457 * Grant a single category and persist it. 458 * 459 * Used by the blocked-embed placeholder (guard.js): the visitor consents 460 * to just the category the embed needs, and guard.js restores it in place 461 * via __lwGuard.refresh() (called from saveConsent). When a script of that 462 * category is blocked on the page, the page reloads instead: a blocked 463 * script cannot be restored in place, since code relying on it (an SDK's 464 * init call, say) has already run without it. 465 * 466 * @param {string} category Category key to grant. 467 */ 468 function acceptCategory( category ) { 469 var categories = getConsentCategories(); 470 categories.necessary = true; 471 if ( category ) { 472 categories[category] = true; 473 } 474 475 var guard = window.__lwGuard; 476 var reload = ! ! ( guard && guard.hasBlockedScripts && guard.hasBlockedScripts( category ) ); 477 478 saveConsent( categories, 'load_embed', ! reload ); 479 } 480 481 // Expose API globally. 482 window.LWCookie = { 483 acceptAll: acceptAll, 484 rejectAll: rejectAll, 485 openPreferences: openPreferences, 486 deleteAllCookies: deleteAllCookies, 487 saveConsent: saveConsent, 488 acceptCategory: acceptCategory, 489 getConsent: function () { 490 return getConsentCategories(); 491 }, 492 isAllowed: function ( category ) { 493 var cats = getConsentCategories(); 494 return cats[category] === true; 495 } 496 }; 497 498})();
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.