gdpr-cookie-consent
Version:
Lightweight, headless cookie consent library providing technical tools for implementing consent mechanisms. Data-attribute driven, no dependencies, framework-agnostic. Legal compliance is user's responsibility - consult legal professionals.
1,322 lines (1,174 loc) • 41 kB
JavaScript
/**
* GDPR Cookie Consent Library
* A headless, data-attribute-driven cookie consent system
*
* @version 1.0.0
* @author Your Name
* @license MIT
*/
(function (window) {
"use strict";
class GDPRCookies {
constructor() {
this.config = {
cookiePrefix: "gdpr_",
cookieDuration: 365,
categories: [],
categoryConfig: {},
onAccept: null,
onDecline: null,
onSave: null,
autoShow: true,
showDelay: 1000,
};
this.elements = {};
this.currentPreferences = {};
this.isInitialized = false;
this.errorLog = [];
}
/**
* Safe error logging that doesn't expose sensitive details
*/
logError(message, context = {}) {
const errorEntry = {
timestamp: new Date().toISOString(),
message: message,
context: this.sanitizeContext(context)
};
this.errorLog.push(errorEntry);
// Keep only last 10 errors to prevent memory issues
if (this.errorLog.length > 10) {
this.errorLog.shift();
}
// Log to console in development (when not in production)
if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
console.warn(`GDPR: ${message}`, context);
}
}
/**
* Sanitize context to prevent logging sensitive information
*/
sanitizeContext(context) {
const sanitized = {};
for (const [key, value] of Object.entries(context)) {
if (typeof value === 'string' && value.length > 100) {
sanitized[key] = '[TRUNCATED]';
} else if (typeof value === 'object' && value !== null) {
sanitized[key] = '[OBJECT]';
} else {
sanitized[key] = value;
}
}
return sanitized;
}
/**
* Initialize the GDPR system
* @param {Object} options - Configuration options
*/
init(options = {}) {
try {
if (this.isInitialized) {
this.logError('GDPR system already initialized');
return false;
}
// Validate input
if (options !== null && typeof options !== 'object') {
this.logError('Invalid options provided to init method', { optionsType: typeof options });
return false;
}
// Merge configuration safely
try {
this.config = { ...this.config, ...options };
} catch (mergeError) {
this.logError('Failed to merge configuration options');
return false;
}
// Set default category configurations
this.setupDefaultCategories();
// Find and cache DOM elements
this.cacheElements();
// Setup event listeners
this.bindEvents();
// Initialize categories UI
this.renderCategories();
// Check existing consent
this.loadExistingConsent();
this.isInitialized = true;
this.updateStatusDisplay();
return true;
} catch (error) {
this.logError('Critical error during initialization');
this.isInitialized = false;
return false;
}
}
/**
* Setup default category configurations
*/
setupDefaultCategories() {
const defaults = {
analytics: {
title: "📊 Analytics Cookies",
description:
"Help us understand how visitors interact with our website",
scripts: [],
onAccept: null,
onDecline: null,
},
marketing: {
title: "🎯 Marketing Cookies",
description:
"Used to deliver personalized advertisements and measure their effectiveness",
scripts: [],
onAccept: null,
onDecline: null,
},
functional: {
title: "⚙️ Functional Cookies",
description:
"Enable enhanced functionality and personalization features",
scripts: [],
onAccept: null,
onDecline: null,
},
};
// Merge user config with defaults
this.config.categories.forEach((category) => {
if (!this.config.categoryConfig[category]) {
this.config.categoryConfig[category] = defaults[category] || {
title: `${
category.charAt(0).toUpperCase() + category.slice(1)
} Cookies`,
description: `Cookies for ${category} purposes`,
scripts: [],
};
}
});
}
/**
* Cache DOM elements using data attributes
*/
cacheElements() {
try {
this.elements = {
banner: this.safeQuerySelector("[data-gdpr-banner]"),
modal: this.safeQuerySelector("[data-gdpr-modal]"),
categoriesContainer: this.safeQuerySelector("[data-gdpr-categories]"),
categoryTemplate: this.safeQuerySelector("[data-gdpr-category-template]"),
statusDisplay: this.safeQuerySelector("[data-gdpr-status]"),
};
// Validate required elements are available
this.validateRequiredElements();
} catch (error) {
this.logError('Failed to cache DOM elements');
}
}
/**
* Safe DOM query selector with error handling
*/
safeQuerySelector(selector) {
try {
if (!selector || typeof selector !== 'string') {
this.logError('Invalid selector provided', { selector });
return null;
}
return document.querySelector(selector);
} catch (error) {
this.logError('DOM query failed', { selector });
return null;
}
}
/**
* Validate that required DOM elements are present
*/
validateRequiredElements() {
if (!this.elements.banner) {
this.logError('Banner element with [data-gdpr-banner] not found');
}
if (!this.elements.modal) {
this.logError('Modal element with [data-gdpr-modal] not found');
}
if (!this.elements.categoriesContainer) {
this.logError('Categories container with [data-gdpr-categories] not found');
}
if (!this.elements.categoryTemplate) {
this.logError('Category template with [data-gdpr-category-template] not found');
}
}
/**
* Bind event listeners using event delegation
*/
bindEvents() {
try {
// Use event delegation for all GDPR actions
document.addEventListener("click", (e) => {
try {
if (!e.target) return;
const action = e.target.getAttribute("data-gdpr-action");
if (action) {
this.handleAction(action, e);
}
} catch (error) {
this.logError('Error handling click event');
}
});
// Handle category toggle changes
document.addEventListener("change", (e) => {
try {
if (!e.target) return;
const toggle = e.target.getAttribute("data-category-toggle");
if (toggle !== null) {
const categoryElement = e.target.closest("[data-category]");
if (categoryElement) {
const category = categoryElement.getAttribute("data-category");
if (category) {
this.currentPreferences[category] = e.target.checked;
}
}
}
} catch (error) {
this.logError('Error handling change event');
}
});
// Close modal on overlay click
if (this.elements.modal) {
this.elements.modal.addEventListener("click", (e) => {
try {
if (e.target === this.elements.modal) {
this.hideModal();
}
} catch (error) {
this.logError('Error handling modal click');
}
});
}
// Keyboard accessibility
document.addEventListener("keydown", (e) => {
try {
if (e.key === "Escape") {
this.hideModal();
}
} catch (error) {
this.logError('Error handling keydown event');
}
});
} catch (error) {
this.logError('Critical error binding events');
}
}
/**
* Handle action button clicks
*/
handleAction(action, event) {
event.preventDefault();
switch (action) {
case "show-banner":
this.showBanner();
break;
case "show-preferences":
this.showModal();
break;
case "close-modal":
this.hideModal();
break;
case "accept-all":
this.acceptAll();
break;
case "accept-essential":
this.acceptEssential();
break;
case "save-preferences":
this.savePreferences();
break;
case "clear-all":
this.clearAll();
break;
default:
break;
}
}
/**
* Render category toggles dynamically
*/
renderCategories() {
try {
if (
!this.elements.categoriesContainer ||
!this.elements.categoryTemplate
) {
this.logError('Required elements missing for category rendering');
return;
}
// Clear existing categories safely
try {
this.elements.categoriesContainer.replaceChildren();
} catch (clearError) {
// Fallback for older browsers
this.elements.categoriesContainer.innerHTML = '';
}
// Validate categories array
if (!Array.isArray(this.config.categories)) {
this.logError('Categories configuration is not an array');
return;
}
// Create category elements from template
this.config.categories.forEach((categoryKey) => {
try {
if (!categoryKey || typeof categoryKey !== 'string') {
this.logError('Invalid category key', { categoryKey });
return;
}
const config = this.config.categoryConfig[categoryKey];
if (!config) {
this.logError('No configuration found for category', { categoryKey });
return;
}
const template = this.elements.categoryTemplate.content.cloneNode(true);
if (!template) {
this.logError('Failed to clone category template');
return;
}
// Set category data safely
const container = template.querySelector(".cookie-category");
if (container) {
container.setAttribute("data-category", categoryKey);
} else {
this.logError('Cookie category container not found in template');
}
const title = template.querySelector("[data-category-title]");
const description = template.querySelector("[data-category-description]");
const toggle = template.querySelector("[data-category-toggle]");
if (title && config.title) {
title.textContent = config.title;
}
if (description && config.description) {
description.textContent = config.description;
}
if (toggle) {
toggle.checked = this.currentPreferences[categoryKey] || false;
}
this.elements.categoriesContainer.appendChild(template);
} catch (categoryError) {
this.logError('Failed to render category', { categoryKey });
}
});
} catch (error) {
this.logError('Critical error in category rendering');
}
}
/**
* Cookie management methods
*/
setCookie(name, value, days = this.config.cookieDuration) {
try {
if (!name || typeof name !== 'string') {
this.logError('Invalid cookie name provided', { name });
return false;
}
if (value === undefined) {
this.logError('Cookie value is undefined', { cookieName: name });
return false;
}
const expires = new Date(Date.now() + days * 864e5).toUTCString();
const secure = location.protocol === "https:" ? "; Secure" : "";
// Use Strict SameSite for better security
const sameSite = "; SameSite=Strict";
// Add domain for subdomain support
const domain = this.getDomainForCookie();
let serializedValue;
try {
serializedValue = encodeURIComponent(JSON.stringify(value));
} catch (serializeError) {
this.logError('Failed to serialize cookie value', { cookieName: name });
return false;
}
const cookieString = `${
this.config.cookiePrefix
}${name}=${serializedValue}; expires=${expires}; path=/${domain}${sameSite}${secure}`;
// Validate cookie string length (browsers have limits)
if (cookieString.length > 4096) {
this.logError('Cookie string too long', { cookieName: name, length: cookieString.length });
return false;
}
document.cookie = cookieString;
// Verify cookie was set by attempting to read it back
const verification = this.getCookie(name);
if (verification === null && value !== null) {
this.logError('Cookie verification failed', { cookieName: name });
return false;
}
return true;
} catch (error) {
this.logError('Cookie setting failed', { cookieName: name });
return false;
}
}
getCookie(name) {
try {
if (!name || typeof name !== 'string') {
this.logError('Invalid cookie name provided', { name });
return null;
}
const cookieValue = document.cookie
.split("; ")
.find((row) => row.startsWith(`${this.config.cookiePrefix}${name}=`))
?.split("=")[1];
if (cookieValue) {
try {
return JSON.parse(decodeURIComponent(cookieValue));
} catch (parseError) {
this.logError('Failed to parse cookie value', { cookieName: name });
// Try to return the raw value as fallback
try {
return decodeURIComponent(cookieValue);
} catch (decodeError) {
this.logError('Failed to decode cookie value', { cookieName: name });
return null;
}
}
}
return null;
} catch (error) {
this.logError('Cookie retrieval failed', { cookieName: name });
return null;
}
}
deleteCookie(name) {
try {
if (!name || typeof name !== 'string') {
this.logError('Invalid cookie name provided for deletion', { name });
return false;
}
const domain = this.getDomainForCookie();
document.cookie = `${this.config.cookiePrefix}${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/${domain}`;
// Also try deleting without domain for broader compatibility
document.cookie = `${this.config.cookiePrefix}${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`;
return true;
} catch (error) {
this.logError('Cookie deletion failed', { cookieName: name });
return false;
}
}
/**
* Get domain string for cookie setting
*/
getDomainForCookie() {
// Only set domain for non-localhost environments to support subdomains
const hostname = location.hostname;
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname.includes(':')) {
return '';
}
// Use the current domain, allowing cookies to be accessible to subdomains
const parts = hostname.split('.');
if (parts.length > 2) {
// For subdomains like 'app.example.com', use '.example.com'
return `; domain=.${parts.slice(-2).join('.')}`;
}
return `; domain=${hostname}`;
}
/**
* Load existing consent from cookies
*/
loadExistingConsent() {
const consent = this.getCookie("consent");
const preferences = this.getCookie("preferences");
if (consent && preferences) {
this.currentPreferences = preferences;
this.applyConsent(preferences);
this.updateCategoryToggles();
} else if (this.config.autoShow) {
// Show banner after delay for new users
setTimeout(() => this.showBanner(), this.config.showDelay);
}
}
/**
* Update category toggle states
*/
updateCategoryToggles() {
try {
if (!Array.isArray(this.config.categories)) {
this.logError('Categories configuration is not an array');
return;
}
this.config.categories.forEach((category) => {
try {
if (!category || typeof category !== 'string') {
this.logError('Invalid category in updateCategoryToggles', { category });
return;
}
const toggle = this.safeQuerySelector(
`[data-category="${category}"] [data-category-toggle]`
);
if (toggle) {
toggle.checked = this.currentPreferences[category] || false;
}
} catch (toggleError) {
this.logError('Failed to update category toggle', { category });
}
});
} catch (updateError) {
this.logError('Critical error updating category toggles');
}
}
/**
* Apply consent decisions (load/unload scripts)
*/
applyConsent(preferences) {
this.config.categories.forEach((category) => {
const config = this.config.categoryConfig[category];
const accepted = preferences[category];
if (accepted) {
// Load scripts for this category
this.loadCategoryScripts(category);
// Call onAccept callback
if (config.onAccept && typeof config.onAccept === "function") {
config.onAccept(category);
}
} else {
// Unload scripts for this category
this.unloadCategoryScripts(category);
// Call onDecline callback
if (config.onDecline && typeof config.onDecline === "function") {
config.onDecline(category);
}
}
});
}
/**
* Load scripts for a specific category
*/
async loadCategoryScripts(category) {
try {
if (!category || typeof category !== 'string') {
this.logError('Invalid category provided for script loading', { category });
return;
}
const config = this.config.categoryConfig[category];
if (!config) {
this.logError('No configuration found for category', { category });
return;
}
if (config.scripts && Array.isArray(config.scripts) && config.scripts.length > 0) {
// Load scripts sequentially to avoid conflicts
for (const scriptConfig of config.scripts) {
try {
await this.loadScript(scriptConfig, category);
} catch (scriptError) {
this.logError('Failed to load script in category', { category, script: scriptConfig });
// Continue loading other scripts even if one fails
}
}
}
} catch (categoryError) {
this.logError('Critical error loading category scripts', { category });
}
}
/**
* Unload scripts for a specific category
*/
unloadCategoryScripts(category) {
// Remove scripts with data-gdpr-category attribute
const scripts = document.querySelectorAll(
`script[data-gdpr-category="${category}"]`
);
scripts.forEach((script) => script.remove());
// Clean up category-specific cookies
this.cleanupCategoryCookies(category);
}
/**
* Validate URL to prevent script injection attacks
*/
isValidScriptUrl(url) {
try {
const parsedUrl = new URL(url, location.origin);
// Only allow HTTP and HTTPS protocols
if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
return false;
}
// Prevent javascript: and data: URLs
if (parsedUrl.protocol === 'javascript:' || parsedUrl.protocol === 'data:') {
return false;
}
return true;
} catch (e) {
return false;
}
}
/**
* Load a script dynamically with error handling and timeout
*/
loadScript(scriptConfig, category) {
return new Promise((resolve, reject) => {
try {
if (!scriptConfig) {
this.logError('No script configuration provided');
reject(new Error('No script configuration'));
return;
}
if (!category || typeof category !== 'string') {
this.logError('Invalid category provided for script loading', { category });
reject(new Error('Invalid category'));
return;
}
const script = document.createElement("script");
script.setAttribute("data-gdpr-category", category);
let scriptSrc;
let timeout = 10000; // Default 10 second timeout
if (typeof scriptConfig === "string") {
scriptSrc = scriptConfig;
} else if (typeof scriptConfig === 'object' && scriptConfig !== null) {
scriptSrc = scriptConfig.src;
if (scriptConfig.async !== undefined) script.async = scriptConfig.async;
if (scriptConfig.defer !== undefined) script.defer = scriptConfig.defer;
if (scriptConfig.type) script.type = scriptConfig.type;
if (scriptConfig.timeout && typeof scriptConfig.timeout === 'number') {
timeout = scriptConfig.timeout;
}
} else {
this.logError('Invalid script configuration format', { scriptConfig });
reject(new Error('Invalid script configuration'));
return;
}
if (!scriptSrc || typeof scriptSrc !== 'string') {
this.logError('No script source URL provided');
reject(new Error('No script source'));
return;
}
// Validate the script URL before loading
if (!this.isValidScriptUrl(scriptSrc)) {
this.logError('Invalid script URL blocked for security', { url: scriptSrc });
reject(new Error('Invalid script URL'));
return;
}
// Set up timeout handler
const timeoutId = setTimeout(() => {
this.logError('Script loading timeout', { url: scriptSrc, category, timeout });
script.remove();
reject(new Error('Script loading timeout'));
}, timeout);
// Handle successful loading
script.onload = () => {
clearTimeout(timeoutId);
resolve();
};
// Handle loading errors
script.onerror = (error) => {
clearTimeout(timeoutId);
this.logError('Script loading failed', { url: scriptSrc, category });
script.remove();
reject(new Error('Script loading failed'));
};
script.src = scriptSrc;
document.head.appendChild(script);
} catch (error) {
this.logError('Critical error in script loading', { category });
reject(error);
}
});
}
/**
* Clean up cookies for a specific category
*/
cleanupCategoryCookies(category) {
const cookiesToClean = {
analytics: [
"_ga",
"_ga_*",
"_gid",
"_gat",
"__utma",
"__utmb",
"__utmc",
"__utmt",
"__utmz",
],
marketing: [
"_fbp",
"_fbc",
"fr",
"__Secure-FRCMP",
"DSID",
"IDE",
"test_cookie",
],
functional: [],
};
const cookies = cookiesToClean[category] || [];
cookies.forEach((cookieName) => {
if (cookieName.includes("*")) {
// Handle wildcard cookies
const prefix = cookieName.replace("*", "");
document.cookie.split(";").forEach((cookie) => {
const name = cookie.split("=")[0].trim();
if (name.startsWith(prefix)) {
const domain = this.getDomainForCookie();
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/${domain}`;
}
});
} else {
const domain = this.getDomainForCookie();
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/${domain}`;
}
});
}
/**
* UI control methods
*/
showBanner() {
try {
if (this.elements.banner) {
this.elements.banner.style.display = "block";
// Trigger reflow for animation
this.elements.banner.offsetHeight;
this.elements.banner.classList.add("show");
} else {
this.logError('Banner element not available for display');
}
} catch (error) {
this.logError('Failed to show banner');
}
}
hideBanner() {
try {
if (this.elements.banner) {
this.elements.banner.classList.remove("show");
setTimeout(() => {
try {
if (this.elements.banner) {
this.elements.banner.style.display = "none";
}
} catch (hideError) {
this.logError('Failed to hide banner after timeout');
}
}, 300);
}
} catch (error) {
this.logError('Failed to initiate banner hiding');
}
}
showModal() {
try {
if (this.elements.modal) {
this.updateCategoryToggles();
this.elements.modal.style.display = "flex";
// Trigger reflow for animation
this.elements.modal.offsetHeight;
this.elements.modal.classList.add("show");
try {
document.body.style.overflow = "hidden";
} catch (bodyError) {
this.logError('Failed to set body overflow');
}
} else {
this.logError('Modal element not available for display');
}
} catch (error) {
this.logError('Failed to show modal');
}
}
hideModal() {
try {
if (this.elements.modal) {
this.elements.modal.classList.remove("show");
try {
document.body.style.overflow = "";
} catch (bodyError) {
this.logError('Failed to reset body overflow');
}
setTimeout(() => {
try {
if (this.elements.modal) {
this.elements.modal.style.display = "none";
}
} catch (hideError) {
this.logError('Failed to hide modal after timeout');
}
}, 300);
}
} catch (error) {
this.logError('Failed to initiate modal hiding');
}
}
/**
* Consent action methods
*/
acceptAll() {
const preferences = { essential: true };
this.config.categories.forEach((category) => {
preferences[category] = true;
});
this.saveConsent("all", preferences);
this.hideBanner();
this.hideModal();
if (this.config.onAccept) {
this.config.onAccept(preferences);
}
}
acceptEssential() {
const preferences = { essential: true };
this.config.categories.forEach((category) => {
preferences[category] = false;
});
this.saveConsent("essential", preferences);
this.hideBanner();
this.hideModal();
if (this.config.onDecline) {
this.config.onDecline(preferences);
}
}
savePreferences() {
const preferences = { essential: true, ...this.currentPreferences };
this.saveConsent("custom", preferences);
this.hideModal();
this.hideBanner();
if (this.config.onSave) {
this.config.onSave(preferences);
}
}
saveConsent(type, preferences) {
this.setCookie("consent", {
type: type,
timestamp: new Date().toISOString(),
preferences: preferences,
});
this.setCookie("preferences", preferences);
this.currentPreferences = preferences;
this.applyConsent(preferences);
this.updateStatusDisplay();
}
clearAll() {
// Clear consent cookies
this.deleteCookie("consent");
this.deleteCookie("preferences");
// Unload all category scripts
this.config.categories.forEach((category) => {
this.unloadCategoryScripts(category);
});
// Reset preferences
this.currentPreferences = {};
this.updateCategoryToggles();
this.updateStatusDisplay();
// Show banner again
if (this.config.autoShow) {
this.showBanner();
}
}
/**
* Update status display for demo purposes
*/
updateStatusDisplay() {
if (!this.elements.statusDisplay) return;
const consent = this.getCookie("consent");
const preferences = this.getCookie("preferences") || {};
// Clear existing content safely
this.elements.statusDisplay.replaceChildren();
// Create status elements using safe DOM manipulation
const statusTitle = document.createElement('strong');
statusTitle.textContent = 'Consent Status:';
this.elements.statusDisplay.appendChild(statusTitle);
this.elements.statusDisplay.appendChild(document.createElement('br'));
const typeText = document.createTextNode(`Type: ${consent ? consent.type : 'Not set'}`);
this.elements.statusDisplay.appendChild(typeText);
this.elements.statusDisplay.appendChild(document.createElement('br'));
const timestampText = document.createTextNode(`Timestamp: ${
consent ? new Date(consent.timestamp).toLocaleString() : 'N/A'
}`);
this.elements.statusDisplay.appendChild(timestampText);
this.elements.statusDisplay.appendChild(document.createElement('br'));
this.elements.statusDisplay.appendChild(document.createElement('br'));
const categoriesTitle = document.createElement('strong');
categoriesTitle.textContent = 'Categories:';
this.elements.statusDisplay.appendChild(categoriesTitle);
this.elements.statusDisplay.appendChild(document.createElement('br'));
const essentialText = document.createTextNode(`Essential: ${preferences.essential ? '✅' : '❌'}`);
this.elements.statusDisplay.appendChild(essentialText);
this.elements.statusDisplay.appendChild(document.createElement('br'));
this.config.categories.forEach((category) => {
const categoryText = document.createTextNode(`${category}: ${preferences[category] ? '✅' : '❌'}`);
this.elements.statusDisplay.appendChild(categoryText);
this.elements.statusDisplay.appendChild(document.createElement('br'));
});
}
/**
* Public API methods
*/
getConsent() {
try {
if (!this.isInitialized) {
this.logError('GDPR system not initialized - cannot get consent');
return null;
}
return this.getCookie("consent");
} catch (error) {
this.logError('Failed to get consent');
return null;
}
}
getPreferences() {
try {
if (!this.isInitialized) {
this.logError('GDPR system not initialized - cannot get preferences');
return {};
}
return this.getCookie("preferences") || {};
} catch (error) {
this.logError('Failed to get preferences');
return {};
}
}
hasConsent(category = null) {
try {
if (!this.isInitialized) {
this.logError('GDPR system not initialized - cannot check consent');
return false;
}
if (category !== null && (typeof category !== 'string' || category.trim() === '')) {
this.logError('Invalid category provided to hasConsent', { category });
return false;
}
const preferences = this.getPreferences();
if (category) {
return preferences[category] === true;
}
return Object.keys(preferences).length > 0;
} catch (error) {
this.logError('Failed to check consent status', { category });
return false;
}
}
updateCategory(category, enabled) {
try {
if (!this.isInitialized) {
this.logError('GDPR system not initialized - cannot update category');
return false;
}
if (!category || typeof category !== 'string' || category.trim() === '') {
this.logError('Invalid category provided to updateCategory', { category });
return false;
}
if (typeof enabled !== 'boolean') {
this.logError('Invalid enabled value provided to updateCategory', { category, enabled });
return false;
}
if (!Array.isArray(this.config.categories) || !this.config.categories.includes(category)) {
this.logError('Category not found in configuration', { category });
return false;
}
const preferences = this.getPreferences();
preferences[category] = enabled;
this.saveConsent("custom", preferences);
return true;
} catch (error) {
this.logError('Failed to update category', { category, enabled });
return false;
}
}
addScript(category, scriptConfig) {
try {
if (!this.isInitialized) {
this.logError('GDPR system not initialized - cannot add script');
return false;
}
if (!category || typeof category !== 'string' || category.trim() === '') {
this.logError('Invalid category provided to addScript', { category });
return false;
}
if (!scriptConfig) {
this.logError('No script configuration provided to addScript', { category });
return false;
}
// Validate script configuration format
if (typeof scriptConfig === 'string') {
if (scriptConfig.trim() === '') {
this.logError('Empty script URL provided', { category });
return false;
}
} else if (typeof scriptConfig === 'object' && scriptConfig !== null) {
if (!scriptConfig.src || typeof scriptConfig.src !== 'string' || scriptConfig.src.trim() === '') {
this.logError('Invalid script source in configuration', { category });
return false;
}
} else {
this.logError('Invalid script configuration format', { category, configType: typeof scriptConfig });
return false;
}
if (!this.config.categoryConfig[category]) {
this.logError('Category configuration not found for addScript', { category });
return false;
}
this.config.categoryConfig[category].scripts.push(scriptConfig);
// If category is already accepted, load the script immediately
if (this.hasConsent(category)) {
this.loadScript(scriptConfig, category).catch(() => {
this.logError('Failed to load newly added script', { category });
});
}
return true;
} catch (error) {
this.logError('Failed to add script', { category });
return false;
}
}
onCategoryChange(category, callback) {
try {
if (!this.isInitialized) {
this.logError('GDPR system not initialized - cannot set category change handler');
return false;
}
if (!category || typeof category !== 'string' || category.trim() === '') {
this.logError('Invalid category provided to onCategoryChange', { category });
return false;
}
if (!callback || typeof callback !== 'function') {
this.logError('Invalid callback provided to onCategoryChange', { category });
return false;
}
if (!this.config.categoryConfig[category]) {
this.config.categoryConfig[category] = {
title: `${category} Cookies`,
description: `Cookies for ${category} functionality`,
scripts: [],
};
}
const config = this.config.categoryConfig[category];
const originalOnAccept = config.onAccept;
const originalOnDecline = config.onDecline;
config.onAccept = (cat) => {
try {
if (originalOnAccept && typeof originalOnAccept === 'function') {
originalOnAccept(cat);
}
callback(cat, true);
} catch (callbackError) {
this.logError('Error in category accept callback', { category: cat });
}
};
config.onDecline = (cat) => {
try {
if (originalOnDecline && typeof originalOnDecline === 'function') {
originalOnDecline(cat);
}
callback(cat, false);
} catch (callbackError) {
this.logError('Error in category decline callback', { category: cat });
}
};
return true;
} catch (error) {
this.logError('Failed to set category change handler', { category });
return false;
}
}
}
// Create global instance
const gdprInstance = new GDPRCookies();
// Expose to global scope with error handling wrappers
window.GDPRCookies = {
init: (options) => {
try {
return gdprInstance.init(options);
} catch (error) {
if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
console.error('GDPR: Failed to initialize');
}
return false;
}
},
getConsent: () => {
try {
return gdprInstance.getConsent();
} catch (error) {
return null;
}
},
getPreferences: () => {
try {
return gdprInstance.getPreferences();
} catch (error) {
return {};
}
},
hasConsent: (category) => {
try {
return gdprInstance.hasConsent(category);
} catch (error) {
return false;
}
},
updateCategory: (category, enabled) => {
try {
return gdprInstance.updateCategory(category, enabled);
} catch (error) {
return false;
}
},
addScript: (category, script) => {
try {
return gdprInstance.addScript(category, script);
} catch (error) {
return false;
}
},
onCategoryChange: (category, callback) => {
try {
return gdprInstance.onCategoryChange(category, callback);
} catch (error) {
return false;
}
},
showBanner: () => {
try {
return gdprInstance.showBanner();
} catch (error) {
return false;
}
},
showPreferences: () => {
try {
return gdprInstance.showModal();
} catch (error) {
return false;
}
},
clearAll: () => {
try {
return gdprInstance.clearAll();
} catch (error) {
return false;
}
},
};
// Auto-initialize if config is provided via data attribute
document.addEventListener("DOMContentLoaded", () => {
try {
const configScript = document.querySelector("script[data-gdpr-config]");
if (configScript) {
const configAttr = configScript.getAttribute("data-gdpr-config");
if (!configAttr || configAttr.trim() === '') {
if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
console.warn('GDPR: Empty configuration attribute found');
}
return;
}
try {
const config = JSON.parse(configAttr);
if (typeof config !== 'object' || config === null) {
if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
console.error('GDPR: Invalid configuration format - must be an object');
}
return;
}
window.GDPRCookies.init(config);
} catch (parseError) {
if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
console.error('GDPR: Failed to parse configuration - invalid JSON format');
}
}
}
} catch (error) {
if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
console.error('GDPR: Auto-initialization failed');
}
}
});
})(window);