1/** 2 * Public JavaScript for Ric Ultimate Cookie Consent. 3 * Handles granular consent, consent expiry, floating widget, preference modal, 4 * script blocking, Google Consent Mode v2, and the rucc_consent API. 5 */ 6 7// =============== IMMEDIATE CONSENT CHECK & GCM UPDATE =============== 8// These run immediately when the script loads (in footer) to provide 9// the earliest possible consent state to Google Consent Mode. 10 11(function() { 12 var expiryKey = 'ric_cookie_expiry'; 13 var storageKey = 'ric_cookie_consent'; 14 var catsKey = 'ric_cookie_categories'; 15 16 function ruccIsExpired() { 17 try { 18 var expiry = localStorage.getItem(expiryKey); 19 if (!expiry) return true; 20 return Date.now() > parseInt(expiry, 10); 21 } catch(e) { 22 console.error('Ric Cookie Consent: Error checking expiry', e); 23 return true; 24 } 25 } 26 27 function ruccGetSavedCategories() { 28 try { 29 var data = localStorage.getItem(catsKey); 30 return data ? JSON.parse(data) : []; 31 } catch(e) { 32 console.error('Ric Cookie Consent: Error parsing saved categories', e); 33 return []; 34 } 35 } 36 37 function ruccIsCCPAOptedOut() { 38 try { 39 var val = localStorage.getItem('ric_ccpa_optout'); 40 var expiry = localStorage.getItem('ric_ccpa_expiry'); 41 if (val !== 'opted_out' || !expiry) return false; 42 return Date.now() < parseInt(expiry, 10); 43 } catch(e) { 44 console.error('Ric Cookie Consent: Error checking CCPA opt-out', e); 45 return false; 46 } 47 } 48 49 function ruccUpdateGoogleConsent(consentType, categories) { 50 if (typeof window.dataLayer === 'undefined') return; 51 if (typeof gtag !== 'function') return; 52 53 var isAccepted = (consentType === 'accepted' || consentType === 'custom'); 54 var hasAnalytics = isAccepted && (consentType === 'accepted' || categories.indexOf('analytics') !== -1); 55 var hasMarketing = isAccepted && (consentType === 'accepted' || categories.indexOf('marketing') !== -1); 56 var hasFunctional = isAccepted && (consentType === 'accepted' || categories.indexOf('functional') !== -1); 57 58 // CCPA opt-out overrides ad-related signals regardless of cookie consent 59 var ccpaOptedOut = ruccIsCCPAOptedOut(); 60 var adGranted = hasMarketing && !ccpaOptedOut; 61 62 gtag('consent', 'update', { 63 'ad_storage': adGranted ? 'granted' : 'denied', 64 'ad_user_data': adGranted ? 'granted' : 'denied', 65 'ad_personalization': adGranted ? 'granted' : 'denied', 66 'analytics_storage': hasAnalytics ? 'granted' : 'denied', 67 'functionality_storage': hasFunctional ? 'granted' : 'denied', 68 'personalization_storage': hasFunctional ? 'granted' : 'denied', 69 'security_storage': 'granted' 70 }); 71 } 72 73 // Check for saved consent and immediately update GCM 74 var savedConsent = null; 75 try { savedConsent = localStorage.getItem(storageKey); } catch(e) {} 76 if (savedConsent && !ruccIsExpired()) { 77 var savedCats = ruccGetSavedCategories(); 78 ruccUpdateGoogleConsent(savedConsent, savedCats); 79 } 80 81})(); 82 83// =============== window.rucc_consent PUBLIC API =============== 84// Provides a JavaScript API for other plugins/themes to check consent status. 85(function() { 86 var expiryKey = 'ric_cookie_expiry'; 87 var storageKey = 'ric_cookie_consent'; 88 var catsKey = 'ric_cookie_categories'; 89 90 window.rucc_consent = { 91 /** 92 * Get the current consent status. 93 * @returns {string|null} 'accepted', 'declined', 'custom', or null if no consent given. 94 */ 95 getConsent: function() { 96 try { 97 return localStorage.getItem(storageKey); 98 } catch(e) { 99 console.error('Ric Cookie Consent: Error getting consent', e); 100 return null; 101 } 102 }, 103 104 /** 105 * Get the list of consented category keys. 106 * @returns {string[]} Array of category keys (e.g., ['necessary', 'analytics']). 107 */ 108 getCategories: function() { 109 try { 110 var data = localStorage.getItem(catsKey); 111 return data ? JSON.parse(data) : []; 112 } catch(e) { 113 console.error('Ric Cookie Consent: Error parsing categories', e); 114 return []; 115 } 116 }, 117 118 /** 119 * Check if a specific cookie category has been consented to. 120 * @param {string} category - The category key to check (e.g., 'analytics', 'marketing'). 121 * @returns {boolean} True if the category is consented. 122 */ 123 hasCategory: function(category) { 124 var consent = this.getConsent(); 125 if (!consent) return false;
126 if (consent === 'accepted') return true; 127 if (consent === 'declined') return false; 128 // 'custom' mode â check categories list 129 var cats = this.getCategories(); 130 return cats.indexOf(category) !== -1; 131 }, 132 133 /** 134 * Register a callback that fires when consent status changes. 135 * The callback receives (consentType, categories). 136 * @param {function} callback - Function to call on consent change. 137 */ 138 onChange: function(callback) { 139 if (typeof callback !== 'function') return; 140 if (!this._callbacks) this._callbacks = []; 141 this._callbacks.push(callback); 142 }, 143 144 /** 145 * Check if the user has opted out under CCPA. 146 * The opt-out persists for 12 months and denies ad-related data sharing/selling. 147 * @returns {boolean} True if the user has an active CCPA opt-out. 148 */ 149 getCCPAOptOut: function() { 150 try { 151 var val = localStorage.getItem('ric_ccpa_optout'); 152 var expiry = localStorage.getItem('ric_ccpa_expiry'); 153 if (val !== 'opted_out' || !expiry) return false; 154 return Date.now() < parseInt(expiry, 10); 155 } catch(e) { 156 console.error('Ric Cookie Consent: Error checking CCPA opt-out', e); 157 return false; 158 } 159 }, 160 161 /** 162 * Internal: notify all registered callbacks. 163 * @private 164 */ 165 _notify: function(consentType, categories) { 166 if (!this._callbacks) return; 167 for (var i = 0; i < this._callbacks.length; i++) { 168 try { 169 this._callbacks[i](consentType, categories); 170 } catch(e) { 171 console.error('Ric Cookie Consent: Error in consent change callback', e); 172 } 173 } 174 // Also dispatch a DOM event for broad compatibility 175 var event = new CustomEvent('rucc_consent_change', { 176 detail: { consent: consentType, categories: categories } 177 }); 178 document.dispatchEvent(event); 179 } 180 }; 181})(); 182 183// =============== SCRIPT BLOCKING ENGINE =============== 184// Scans the DOM for blocked scripts (type="text/plain" with data-category) 185// and selectively loads them based on user consent. 186 187window.ruccScriptBlocker = { 188 /** 189 * Get the data-category attribute value(s) from a script element. 190 * Supports comma-separated values. 191 */ 192 getScriptCategories: function(scriptEl) { 193 var catAttr = scriptEl.getAttribute('data-category'); 194 if (!catAttr) return []; 195 return catAttr.split(',').map(function(c) { return c.trim(); }); 196 }, 197 198 /** 199 * Check if a script should be loaded given the user's consented categories. 200 */ 201 shouldLoad: function(scriptEl, userCategories) { 202 var scriptCats = this.getScriptCategories(scriptEl); 203 if (!scriptCats.length) return false; 204 if (userCategories.indexOf('all') !== -1) return true; 205 for (var i = 0; i < scriptCats.length; i++) { 206 if (userCategories.indexOf(scriptCats[i]) !== -1) return true; 207 } 208 return false; 209 }, 210 211 /** 212 * Resolve consent type ('accepted', 'declined', 'custom') into a flat category array. 213 */ 214 resolveCategories: function(consentType, categories) { 215 if (consentType === 'accepted') { 216 return ['necessary', 'analytics', 'marketing', 'functional', 'all']; 217 } 218 if (consentType === 'declined') { 219 return ['necessary']; 220 } 221 // 'custom' or any other mode â use the provided or saved categories 222 return categories || window.rucc_consent.getCategories(); 223 }, 224 225 /** 226 * Load a blocked script by creating a new active script element. 227 */ 228 loadScript: function(scriptEl) { 229 var newScript = document.createElement('script'); 230 for (var i = 0; i < scriptEl.attributes.length; i++) { 231 var attr = scriptEl.attributes[i]; 232 var name = attr.name; 233 if (name === 'type') continue; 234 if (name === 'data-category') continue; 235 if (name === 'data-src') { 236 newScript.setAttribute('src', attr.value); 237 continue; 238 } 239 newScript.setAttribute(name, attr.value); 240 } 241 newScript.type = 'text/javascript'; 242 newScript.textContent = scriptEl.textContent; 243 newScript.setAttribute('data-rucc-loaded', 'true'); 244 scriptEl.parentNode.replaceChild(newScript, scriptEl); 245 }, 246 247 /** 248 * Apply consent to all blocked scripts on the page. 249 */ 250 applyConsent: function(consentType, categories) { 251 var userCats = this.resolveCategories(consentType, categories); 252 var blockedScripts = document.querySelectorAll('script[type="text/plain"][data-category]'); 253 254 for (var i = 0; i < blockedScripts.length; i++) { 255 var script = blockedScripts[i]; 256 if (script.getAttribute('data-rucc-loaded')) continue; 257 if (this.shouldLoad(script, userCats)) { 258 this.loadScript(script); 259 } 260 } 261 } 262}; 263 264// =============== DOMContentLoaded â BANNER & CONSENT LOGIC =============== 265document.addEventListener("DOMContentLoaded", function () { 266 const banner = document.getElementById('cookie-banner-glass');
267 const overlay = document.getElementById('cookie-banner-overlay'); 268 const floatingWidget = document.getElementById('ric-floating-widget'); 269 const preferenceModal = document.getElementById('ric-preference-modal'); 270 const preferenceOverlay = document.getElementById('ric-preference-overlay'); 271 272 if (!banner) { 273 return; // Exit if banner element not found 274 } 275 276 const storageKey = 'ric_cookie_consent'; 277 const expiryKey = 'ric_cookie_expiry'; 278 const catsKey = 'ric_cookie_categories'; 279 const consentMode = rucc_settings.consent_mode || 'simple'; 280 const expiryDays = parseInt(rucc_settings.expiry_days) || 365; 281 const showWidget = rucc_settings.show_widget || 'yes'; 282 const enableCCPA = rucc_settings.enable_ccpa || 'no'; 283 const ccpaButtonText = rucc_settings.ccpa_button_text || 'Do Not Sell My Personal Information'; 284 285 /** 286 * Check if consent is expired. 287 */ 288 function isConsentExpired() { 289 var expiry = localStorage.getItem(expiryKey); 290 if (!expiry) return true; 291 return Date.now() > parseInt(expiry); 292 } 293 294 /** 295 * Show the banner and overlay. 296 */ 297 function showBanner() { 298 banner.classList.add('show'); 299 if (overlay) { 300 overlay.classList.add('show'); 301 } 302 } 303 304 /** 305 * Hide banner, overlay, and preference modal. 306 */ 307 function hideAll() { 308 banner.classList.remove('show'); 309 if (overlay) { 310 overlay.classList.remove('show'); 311 } 312 if (preferenceModal) { 313 preferenceModal.classList.remove('show'); 314 } 315 if (preferenceOverlay) { 316 preferenceOverlay.classList.remove('show'); 317 } 318 } 319 320 /** 321 * Send consent to the server via AJAX. 322 */ 323 function sendConsent(consentType, categories) { 324 var params = 'action=rucc_handle_consent&nonce=' + rucc_settings.nonce + '&consent=' + encodeURIComponent(consentType); 325 if (categories && categories.length > 0) { 326 for (var i = 0; i < categories.length; i++) { 327 params += '&categories[]=' + encodeURIComponent(categories[i]); 328 } 329 } 330 var xhr = new XMLHttpRequest(); 331 xhr.open('POST', rucc_settings.ajax_url, true); 332 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 333 xhr.send(params); 334 } 335 336 /** 337 * Get selected categories from checkboxes. 338 */ 339 function getSelectedCategories() { 340 var checkboxes = document.querySelectorAll('.cookie-cat-checkbox:checked'); 341 var cats = []; 342 for (var i = 0; i < checkboxes.length; i++) { 343 cats.push(checkboxes[i].value); 344 } 345 return cats; 346 } 347 348 /** 349 * Get all available category keys. 350 */ 351 function getAllCategoryKeys() { 352 var checkboxes = document.querySelectorAll('.cookie-cat-checkbox'); 353 var cats = []; 354 for (var i = 0; i < checkboxes.length; i++) { 355 cats.push(checkboxes[i].value); 356 } 357 return cats; 358 } 359 360 /** 361 * Set expiry timestamp in localStorage. 362 */ 363 function setExpiry() { 364 if (expiryDays > 0) { 365 var expiry = Date.now() + (expiryDays * 24 * 60 * 60 * 1000); 366 localStorage.setItem(expiryKey, expiry.toString()); 367 } 368 } 369 370 /** 371 * Update Google Consent Mode v2 with the given consent choices. 372 */ 373 function isCCPAOptedOut() { 374 try { 375 var val = localStorage.getItem('ric_ccpa_optout'); 376 var expiry = localStorage.getItem('ric_ccpa_expiry'); 377 if (val !== 'opted_out' || !expiry) return false; 378 return Date.now() < parseInt(expiry, 10); 379 } catch(e) { 380 return false; 381 } 382 } 383 384 function updateGoogleConsent(consentType, categories) { 385 if (typeof window.dataLayer === 'undefined') return; 386 if (typeof gtag !== 'function') return; 387 388 var isAccepted = (consentType === 'accepted' || consentType === 'custom'); 389 var hasAnalytics = isAccepted && (consentType === 'accepted' || categories.indexOf('analytics') !== -1); 390 var hasMarketing = isAccepted && (consentType === 'accepted' || categories.indexOf('marketing') !== -1); 391 var hasFunctional = isAccepted && (consentType === 'accepted' || categories.indexOf('functional') !== -1); 392 393 // CCPA opt-out overrides ad-related signals regardless of cookie consent 394 var ccpaOptedOut = isCCPAOptedOut(); 395 var adGranted = hasMarketing && !ccpaOptedOut; 396 397 gtag('consent', 'update', { 398 'ad_storage': adGranted ? 'granted' : 'denied', 399 'ad_user_data': adGranted ? 'granted' : 'denied', 400 'ad_personalization': adGranted ? 'granted' : 'denied',
401 'analytics_storage': hasAnalytics ? 'granted' : 'denied', 402 'functionality_storage': hasFunctional ? 'granted' : 'denied', 403 'personalization_storage': hasFunctional ? 'granted' : 'denied', 404 'security_storage': 'granted' 405 }); 406 } 407 408 /** 409 * Load or block scripts based on the user's consent categories. 410 * Delegates to the global ruccScriptBlocker for consistency. 411 */ 412 function applyConsentToScripts(consentType, categories) { 413 if (window.ruccScriptBlocker) { 414 window.ruccScriptBlocker.applyConsent(consentType, categories); 415 } 416 } 417 418 /** 419 * Send CCPA opt-out to the server via AJAX. 420 */ 421 function sendCCPAOptOut() { 422 var params = 'action=rucc_handle_ccpa_optout&nonce=' + rucc_settings.nonce; 423 var xhr = new XMLHttpRequest(); 424 xhr.open('POST', rucc_settings.ajax_url, true); 425 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 426 xhr.send(params); 427 } 428 429 /** 430 * Handle CCPA opt-out action. 431 */ 432 function handleCCPAOptOut() { 433 var ccpaKey = 'ric_ccpa_optout'; 434 var ccpaExpiryKey = 'ric_ccpa_expiry'; 435 436 localStorage.setItem(ccpaKey, 'opted_out'); 437 var expiry = Date.now() + (365 * 24 * 60 * 60 * 1000); // 12 months 438 localStorage.setItem(ccpaExpiryKey, expiry.toString()); 439 440 // Update GCM to deny ad-related signals 441 updateGoogleConsent(localStorage.getItem(storageKey) || 'declined', []); 442 443 sendCCPAOptOut(); 444 445 // Hide the banner if it's still visible 446 hideAll(); 447 showFloatingWidget(); 448 } 449 450 /** 451 * Handle consent action: save to localStorage, update GCM, notify API, block/load scripts. 452 */ 453 function handleConsent(type, categories) { 454 localStorage.setItem(storageKey, type); 455 localStorage.setItem(catsKey, JSON.stringify(categories)); 456 setExpiry(); 457 updateGoogleConsent(type, categories); 458 applyConsentToScripts(type, categories); 459 hideAll(); 460 sendConsent(type, categories); 461 showFloatingWidget(); 462 463 // Notify the rucc_consent API listeners 464 if (window.rucc_consent && window.rucc_consent._notify) { 465 window.rucc_consent._notify(type, categories); 466 } 467 } 468 469 /** 470 * Show floating widget after consent is given. 471 */ 472 function showFloatingWidget() { 473 if (floatingWidget && showWidget === 'yes') { 474 floatingWidget.classList.add('show'); 475 } 476 } 477 478 /** 479 * Hide floating widget. 480 */ 481 function hideFloatingWidget() { 482 if (floatingWidget) { 483 floatingWidget.classList.remove('show'); 484 } 485 } 486 487 /** 488 * Open preference modal and pre-fill from saved preferences. 489 */ 490 function openPreferences() { 491 // Pre-fill checkboxes from saved preferences 492 var savedCats = localStorage.getItem(catsKey); 493 if (savedCats) { 494 try { 495 var cats = JSON.parse(savedCats); 496 var checkboxes = document.querySelectorAll('.cookie-cat-checkbox:not([disabled])'); 497 for (var i = 0; i < checkboxes.length; i++) { 498 checkboxes[i].checked = cats.indexOf(checkboxes[i].value) !== -1; 499 } 500 } catch(e) {} 501 } 502 if (preferenceModal) { 503 preferenceModal.classList.add('show'); 504 } 505 if (preferenceOverlay) { 506 preferenceOverlay.classList.add('show'); 507 } 508 } 509 510 /** 511 * Close preference modal. 512 */ 513 function closePreferences() { 514 if (preferenceModal) { 515 preferenceModal.classList.remove('show'); 516 } 517 if (preferenceOverlay) { 518 preferenceOverlay.classList.remove('show'); 519 } 520 } 521 522 // =============== INIT =============== 523 524 var savedConsent = localStorage.getItem(storageKey); 525 var expired = isConsentExpired(); 526 527 if (!savedConsent || expired) { 528 // Show banner (no floating widget yet) 529 hideFloatingWidget(); 530 showBanner(); 531 } else { 532 // Consent exists and is valid: apply scripts, show floating widget, hide banner 533 hideAll(); 534 applyConsentToScripts(savedConsent, window.rucc_consent ? window.rucc_consent.getCategories() : []); 535 showFloatingWidget(); 536 } 537 538 // =============== CCPA OPT-OUT CHECK ON LOAD =============== 539 // If a CCPA opt-out was previously saved, respect it by updating GCM 540 if (enableCCPA === 'yes' && isCCPAOptedOut()) { 541 updateGoogleConsent(localStorage.getItem(storageKey) || 'declined', []); 542 } 543 544 // =============== BANNER BUTTONS =============== 545 546 var acceptButton = document.getElementById('cookie-accept'); 547 var declineButton = document.getElementById('cookie-decline'); 548 var customizeButton = document.getElementById('cookie-customize'); 549 var savePrefsButton = document.getElementById('cookie-save-prefs'); 550 var categoriesSection = document.getElementById('cookie-categories'); 551 552 if (acceptButton) { 553 acceptButton.addEventListener('click', function () { 554 var cats = (consentMode === 'granular') ? getAllCategoryKeys() : ['all']; 555 handleConsent('accepted', cats); 556 }); 557 } 558 559 if (declineButton) { 560 declineButton.addEventListener('click', function () { 561 handleConsent('declined', []); 562 }); 563 } 564 565 // CCPA "Do Not Sell" button in the main banner 566 var ccpaButton = document.getElementById('cookie-ccpa-optout'); 567 if (ccpaButton) { 568 ccpaButton.addEventListener('click', function () { 569 handleCCPAOptOut(); 570 }); 571 } 572 573 // Customize link: expand categories, hide link, show save prefs 574 if (customizeButton && categoriesSection) { 575 customizeButton.addEventListener('click', function () { 576 categoriesSection.classList.add('expanded'); 577 customizeButton.style.display = 'none'; 578 if (savePrefsButton) savePrefsButton.style.display = ''; 579 }); 580 } 581 582 if (savePrefsButton) { 583 savePrefsButton.addEventListener('click', function () { 584 var cats = getSelectedCategories(); 585 handleConsent('custom', cats); 586 }); 587 } 588 589 // Note: Overlay click no longer dismisses the banner. 590 // The banner must stay visible until the user explicitly clicks Accept or Decline. 591 592 // =============== FLOATING WIDGET =============== 593 594 if (floatingWidget) { 595 floatingWidget.addEventListener('click', function () { 596 if (preferenceModal) { 597 openPreferences(); 598 } else if (banner) { 599 // If no preference modal, show the banner again 600 showBanner(); 601 if (overlay) overlay.classList.add('show'); 602 } 603 }); 604 } 605 606 // =============== PREFERENCE MODAL =============== 607 608 if (preferenceOverlay) { 609 preferenceOverlay.addEventListener('click', function () { 610 closePreferences(); 611 }); 612 } 613 614 // Close preference modal with Escape key 615 document.addEventListener('keydown', function (e) { 616 if (e.key === 'Escape') { 617 closePreferences(); 618 } 619 }); 620 621 // If preference modal exists, add buttons 622 if (preferenceModal) { 623 var prefAccept = document.getElementById('pref-accept'); 624 var prefSave = document.getElementById('pref-save'); 625 var prefDecline = document.getElementById('pref-decline'); 626 627 if (prefAccept) { 628 prefAccept.addEventListener('click', function () { 629 var cats = (consentMode === 'granular') ? getAllCategoryKeys() : ['all']; 630 handleConsent('accepted', cats); 631 }); 632 } 633 634 if (prefSave) { 635 prefSave.addEventListener('click', function () { 636 var cats = getSelectedCategories(); 637 handleConsent('custom', cats); 638 }); 639 } 640 641 if (prefDecline) { 642 prefDecline.addEventListener('click', function () { 643 handleConsent('declined', []); 644 }); 645 } 646 647 // CCPA "Do Not Sell" button in the preference modal 648 var prefCCPAButton = document.getElementById('pref-ccpa-optout'); 649 if (prefCCPAButton) { 650 prefCCPAButton.addEventListener('click', function () { 651 handleCCPAOptOut(); 652 }); 653 } 654 } 655 656 // =============== APPLY SCRIPT BLOCKING ON NEW SCRIPTS (MUTATION OBSERVER) =============== 657 // Watch for dynamically added blocked scripts and apply consent if already given. 658 var savedConsentType = localStorage.getItem(storageKey); 659 if (savedConsentType && !isConsentExpired() && window.ruccScriptBlocker) { 660 var observer = new MutationObserver(function(mutations) { 661 for (var m = 0; m < mutations.length; m++) { 662 var mutation = mutations[m]; 663 if (mutation.type === 'childList' && mutation.addedNodes.length) { 664 for (var n = 0; n < mutation.addedNodes.length; n++) { 665 var node = mutation.addedNodes[n]; 666 if (node.nodeType === 1 && node.matches && node.matches('script[type="text/plain"][data-category]')) { 667 var userCats = window.ruccScriptBlocker.resolve
667Categories( 668 window.rucc_consent.getConsent(), 669 window.rucc_consent.getCategories() 670 ); 671 if (window.ruccScriptBlocker.shouldLoad(node, userCats)) { 672 window.ruccScriptBlocker.loadScript(node); 673 } 674 } 675 } 676 } 677 } 678 }); 679 680 observer.observe(document.documentElement, { 681 childList: true, 682 subtree: true 683 }); 684 } 685});
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.