gdpr-cookie-consent 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/DISCLAIMER.md +104 -0
- package/LICENSE +21 -0
- package/README.md +1119 -0
- package/main.js +1321 -0
- package/package.json +61 -0
- package/types.d.ts +60 -0
package/main.js
ADDED
|
@@ -0,0 +1,1321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GDPR Cookie Consent Library
|
|
3
|
+
* A headless, data-attribute-driven cookie consent system
|
|
4
|
+
*
|
|
5
|
+
* @version 1.0.0
|
|
6
|
+
* @author Your Name
|
|
7
|
+
* @license MIT
|
|
8
|
+
*/
|
|
9
|
+
(function (window) {
|
|
10
|
+
"use strict";
|
|
11
|
+
|
|
12
|
+
class GDPRCookies {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.config = {
|
|
15
|
+
cookiePrefix: "gdpr_",
|
|
16
|
+
cookieDuration: 365,
|
|
17
|
+
categories: [],
|
|
18
|
+
categoryConfig: {},
|
|
19
|
+
onAccept: null,
|
|
20
|
+
onDecline: null,
|
|
21
|
+
onSave: null,
|
|
22
|
+
autoShow: true,
|
|
23
|
+
showDelay: 1000,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
this.elements = {};
|
|
27
|
+
this.currentPreferences = {};
|
|
28
|
+
this.isInitialized = false;
|
|
29
|
+
this.errorLog = [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Safe error logging that doesn't expose sensitive details
|
|
34
|
+
*/
|
|
35
|
+
logError(message, context = {}) {
|
|
36
|
+
const errorEntry = {
|
|
37
|
+
timestamp: new Date().toISOString(),
|
|
38
|
+
message: message,
|
|
39
|
+
context: this.sanitizeContext(context)
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
this.errorLog.push(errorEntry);
|
|
43
|
+
|
|
44
|
+
// Keep only last 10 errors to prevent memory issues
|
|
45
|
+
if (this.errorLog.length > 10) {
|
|
46
|
+
this.errorLog.shift();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Log to console in development (when not in production)
|
|
50
|
+
if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
|
|
51
|
+
console.warn(`GDPR: ${message}`, context);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Sanitize context to prevent logging sensitive information
|
|
57
|
+
*/
|
|
58
|
+
sanitizeContext(context) {
|
|
59
|
+
const sanitized = {};
|
|
60
|
+
for (const [key, value] of Object.entries(context)) {
|
|
61
|
+
if (typeof value === 'string' && value.length > 100) {
|
|
62
|
+
sanitized[key] = '[TRUNCATED]';
|
|
63
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
64
|
+
sanitized[key] = '[OBJECT]';
|
|
65
|
+
} else {
|
|
66
|
+
sanitized[key] = value;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return sanitized;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Initialize the GDPR system
|
|
74
|
+
* @param {Object} options - Configuration options
|
|
75
|
+
*/
|
|
76
|
+
init(options = {}) {
|
|
77
|
+
try {
|
|
78
|
+
if (this.isInitialized) {
|
|
79
|
+
this.logError('GDPR system already initialized');
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Validate input
|
|
84
|
+
if (options !== null && typeof options !== 'object') {
|
|
85
|
+
this.logError('Invalid options provided to init method', { optionsType: typeof options });
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Merge configuration safely
|
|
90
|
+
try {
|
|
91
|
+
this.config = { ...this.config, ...options };
|
|
92
|
+
} catch (mergeError) {
|
|
93
|
+
this.logError('Failed to merge configuration options');
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Set default category configurations
|
|
98
|
+
this.setupDefaultCategories();
|
|
99
|
+
|
|
100
|
+
// Find and cache DOM elements
|
|
101
|
+
this.cacheElements();
|
|
102
|
+
|
|
103
|
+
// Setup event listeners
|
|
104
|
+
this.bindEvents();
|
|
105
|
+
|
|
106
|
+
// Initialize categories UI
|
|
107
|
+
this.renderCategories();
|
|
108
|
+
|
|
109
|
+
// Check existing consent
|
|
110
|
+
this.loadExistingConsent();
|
|
111
|
+
|
|
112
|
+
this.isInitialized = true;
|
|
113
|
+
this.updateStatusDisplay();
|
|
114
|
+
return true;
|
|
115
|
+
} catch (error) {
|
|
116
|
+
this.logError('Critical error during initialization');
|
|
117
|
+
this.isInitialized = false;
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Setup default category configurations
|
|
124
|
+
*/
|
|
125
|
+
setupDefaultCategories() {
|
|
126
|
+
const defaults = {
|
|
127
|
+
analytics: {
|
|
128
|
+
title: "📊 Analytics Cookies",
|
|
129
|
+
description:
|
|
130
|
+
"Help us understand how visitors interact with our website",
|
|
131
|
+
scripts: [],
|
|
132
|
+
onAccept: null,
|
|
133
|
+
onDecline: null,
|
|
134
|
+
},
|
|
135
|
+
marketing: {
|
|
136
|
+
title: "🎯 Marketing Cookies",
|
|
137
|
+
description:
|
|
138
|
+
"Used to deliver personalized advertisements and measure their effectiveness",
|
|
139
|
+
scripts: [],
|
|
140
|
+
onAccept: null,
|
|
141
|
+
onDecline: null,
|
|
142
|
+
},
|
|
143
|
+
functional: {
|
|
144
|
+
title: "⚙️ Functional Cookies",
|
|
145
|
+
description:
|
|
146
|
+
"Enable enhanced functionality and personalization features",
|
|
147
|
+
scripts: [],
|
|
148
|
+
onAccept: null,
|
|
149
|
+
onDecline: null,
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Merge user config with defaults
|
|
154
|
+
this.config.categories.forEach((category) => {
|
|
155
|
+
if (!this.config.categoryConfig[category]) {
|
|
156
|
+
this.config.categoryConfig[category] = defaults[category] || {
|
|
157
|
+
title: `${
|
|
158
|
+
category.charAt(0).toUpperCase() + category.slice(1)
|
|
159
|
+
} Cookies`,
|
|
160
|
+
description: `Cookies for ${category} purposes`,
|
|
161
|
+
scripts: [],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Cache DOM elements using data attributes
|
|
169
|
+
*/
|
|
170
|
+
cacheElements() {
|
|
171
|
+
try {
|
|
172
|
+
this.elements = {
|
|
173
|
+
banner: this.safeQuerySelector("[data-gdpr-banner]"),
|
|
174
|
+
modal: this.safeQuerySelector("[data-gdpr-modal]"),
|
|
175
|
+
categoriesContainer: this.safeQuerySelector("[data-gdpr-categories]"),
|
|
176
|
+
categoryTemplate: this.safeQuerySelector("[data-gdpr-category-template]"),
|
|
177
|
+
statusDisplay: this.safeQuerySelector("[data-gdpr-status]"),
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Validate required elements are available
|
|
181
|
+
this.validateRequiredElements();
|
|
182
|
+
} catch (error) {
|
|
183
|
+
this.logError('Failed to cache DOM elements');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Safe DOM query selector with error handling
|
|
189
|
+
*/
|
|
190
|
+
safeQuerySelector(selector) {
|
|
191
|
+
try {
|
|
192
|
+
if (!selector || typeof selector !== 'string') {
|
|
193
|
+
this.logError('Invalid selector provided', { selector });
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
return document.querySelector(selector);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
this.logError('DOM query failed', { selector });
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Validate that required DOM elements are present
|
|
205
|
+
*/
|
|
206
|
+
validateRequiredElements() {
|
|
207
|
+
if (!this.elements.banner) {
|
|
208
|
+
this.logError('Banner element with [data-gdpr-banner] not found');
|
|
209
|
+
}
|
|
210
|
+
if (!this.elements.modal) {
|
|
211
|
+
this.logError('Modal element with [data-gdpr-modal] not found');
|
|
212
|
+
}
|
|
213
|
+
if (!this.elements.categoriesContainer) {
|
|
214
|
+
this.logError('Categories container with [data-gdpr-categories] not found');
|
|
215
|
+
}
|
|
216
|
+
if (!this.elements.categoryTemplate) {
|
|
217
|
+
this.logError('Category template with [data-gdpr-category-template] not found');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Bind event listeners using event delegation
|
|
223
|
+
*/
|
|
224
|
+
bindEvents() {
|
|
225
|
+
try {
|
|
226
|
+
// Use event delegation for all GDPR actions
|
|
227
|
+
document.addEventListener("click", (e) => {
|
|
228
|
+
try {
|
|
229
|
+
if (!e.target) return;
|
|
230
|
+
|
|
231
|
+
const action = e.target.getAttribute("data-gdpr-action");
|
|
232
|
+
if (action) {
|
|
233
|
+
this.handleAction(action, e);
|
|
234
|
+
}
|
|
235
|
+
} catch (error) {
|
|
236
|
+
this.logError('Error handling click event');
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// Handle category toggle changes
|
|
241
|
+
document.addEventListener("change", (e) => {
|
|
242
|
+
try {
|
|
243
|
+
if (!e.target) return;
|
|
244
|
+
|
|
245
|
+
const toggle = e.target.getAttribute("data-category-toggle");
|
|
246
|
+
if (toggle !== null) {
|
|
247
|
+
const categoryElement = e.target.closest("[data-category]");
|
|
248
|
+
if (categoryElement) {
|
|
249
|
+
const category = categoryElement.getAttribute("data-category");
|
|
250
|
+
if (category) {
|
|
251
|
+
this.currentPreferences[category] = e.target.checked;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
} catch (error) {
|
|
256
|
+
this.logError('Error handling change event');
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Close modal on overlay click
|
|
261
|
+
if (this.elements.modal) {
|
|
262
|
+
this.elements.modal.addEventListener("click", (e) => {
|
|
263
|
+
try {
|
|
264
|
+
if (e.target === this.elements.modal) {
|
|
265
|
+
this.hideModal();
|
|
266
|
+
}
|
|
267
|
+
} catch (error) {
|
|
268
|
+
this.logError('Error handling modal click');
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Keyboard accessibility
|
|
274
|
+
document.addEventListener("keydown", (e) => {
|
|
275
|
+
try {
|
|
276
|
+
if (e.key === "Escape") {
|
|
277
|
+
this.hideModal();
|
|
278
|
+
}
|
|
279
|
+
} catch (error) {
|
|
280
|
+
this.logError('Error handling keydown event');
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
} catch (error) {
|
|
284
|
+
this.logError('Critical error binding events');
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Handle action button clicks
|
|
290
|
+
*/
|
|
291
|
+
handleAction(action, event) {
|
|
292
|
+
event.preventDefault();
|
|
293
|
+
|
|
294
|
+
switch (action) {
|
|
295
|
+
case "show-banner":
|
|
296
|
+
this.showBanner();
|
|
297
|
+
break;
|
|
298
|
+
case "show-preferences":
|
|
299
|
+
this.showModal();
|
|
300
|
+
break;
|
|
301
|
+
case "close-modal":
|
|
302
|
+
this.hideModal();
|
|
303
|
+
break;
|
|
304
|
+
case "accept-all":
|
|
305
|
+
this.acceptAll();
|
|
306
|
+
break;
|
|
307
|
+
case "accept-essential":
|
|
308
|
+
this.acceptEssential();
|
|
309
|
+
break;
|
|
310
|
+
case "save-preferences":
|
|
311
|
+
this.savePreferences();
|
|
312
|
+
break;
|
|
313
|
+
case "clear-all":
|
|
314
|
+
this.clearAll();
|
|
315
|
+
break;
|
|
316
|
+
default:
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Render category toggles dynamically
|
|
323
|
+
*/
|
|
324
|
+
renderCategories() {
|
|
325
|
+
try {
|
|
326
|
+
if (
|
|
327
|
+
!this.elements.categoriesContainer ||
|
|
328
|
+
!this.elements.categoryTemplate
|
|
329
|
+
) {
|
|
330
|
+
this.logError('Required elements missing for category rendering');
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Clear existing categories safely
|
|
335
|
+
try {
|
|
336
|
+
this.elements.categoriesContainer.replaceChildren();
|
|
337
|
+
} catch (clearError) {
|
|
338
|
+
// Fallback for older browsers
|
|
339
|
+
this.elements.categoriesContainer.innerHTML = '';
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Validate categories array
|
|
343
|
+
if (!Array.isArray(this.config.categories)) {
|
|
344
|
+
this.logError('Categories configuration is not an array');
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Create category elements from template
|
|
349
|
+
this.config.categories.forEach((categoryKey) => {
|
|
350
|
+
try {
|
|
351
|
+
if (!categoryKey || typeof categoryKey !== 'string') {
|
|
352
|
+
this.logError('Invalid category key', { categoryKey });
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const config = this.config.categoryConfig[categoryKey];
|
|
357
|
+
if (!config) {
|
|
358
|
+
this.logError('No configuration found for category', { categoryKey });
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const template = this.elements.categoryTemplate.content.cloneNode(true);
|
|
363
|
+
if (!template) {
|
|
364
|
+
this.logError('Failed to clone category template');
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Set category data safely
|
|
369
|
+
const container = template.querySelector(".cookie-category");
|
|
370
|
+
if (container) {
|
|
371
|
+
container.setAttribute("data-category", categoryKey);
|
|
372
|
+
} else {
|
|
373
|
+
this.logError('Cookie category container not found in template');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const title = template.querySelector("[data-category-title]");
|
|
377
|
+
const description = template.querySelector("[data-category-description]");
|
|
378
|
+
const toggle = template.querySelector("[data-category-toggle]");
|
|
379
|
+
|
|
380
|
+
if (title && config.title) {
|
|
381
|
+
title.textContent = config.title;
|
|
382
|
+
}
|
|
383
|
+
if (description && config.description) {
|
|
384
|
+
description.textContent = config.description;
|
|
385
|
+
}
|
|
386
|
+
if (toggle) {
|
|
387
|
+
toggle.checked = this.currentPreferences[categoryKey] || false;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
this.elements.categoriesContainer.appendChild(template);
|
|
391
|
+
} catch (categoryError) {
|
|
392
|
+
this.logError('Failed to render category', { categoryKey });
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
} catch (error) {
|
|
396
|
+
this.logError('Critical error in category rendering');
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Cookie management methods
|
|
402
|
+
*/
|
|
403
|
+
setCookie(name, value, days = this.config.cookieDuration) {
|
|
404
|
+
try {
|
|
405
|
+
if (!name || typeof name !== 'string') {
|
|
406
|
+
this.logError('Invalid cookie name provided', { name });
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (value === undefined) {
|
|
411
|
+
this.logError('Cookie value is undefined', { cookieName: name });
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const expires = new Date(Date.now() + days * 864e5).toUTCString();
|
|
416
|
+
const secure = location.protocol === "https:" ? "; Secure" : "";
|
|
417
|
+
|
|
418
|
+
// Use Strict SameSite for better security
|
|
419
|
+
const sameSite = "; SameSite=Strict";
|
|
420
|
+
|
|
421
|
+
// Add domain for subdomain support
|
|
422
|
+
const domain = this.getDomainForCookie();
|
|
423
|
+
|
|
424
|
+
let serializedValue;
|
|
425
|
+
try {
|
|
426
|
+
serializedValue = encodeURIComponent(JSON.stringify(value));
|
|
427
|
+
} catch (serializeError) {
|
|
428
|
+
this.logError('Failed to serialize cookie value', { cookieName: name });
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const cookieString = `${
|
|
433
|
+
this.config.cookiePrefix
|
|
434
|
+
}${name}=${serializedValue}; expires=${expires}; path=/${domain}${sameSite}${secure}`;
|
|
435
|
+
|
|
436
|
+
// Validate cookie string length (browsers have limits)
|
|
437
|
+
if (cookieString.length > 4096) {
|
|
438
|
+
this.logError('Cookie string too long', { cookieName: name, length: cookieString.length });
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
document.cookie = cookieString;
|
|
443
|
+
|
|
444
|
+
// Verify cookie was set by attempting to read it back
|
|
445
|
+
const verification = this.getCookie(name);
|
|
446
|
+
if (verification === null && value !== null) {
|
|
447
|
+
this.logError('Cookie verification failed', { cookieName: name });
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return true;
|
|
452
|
+
} catch (error) {
|
|
453
|
+
this.logError('Cookie setting failed', { cookieName: name });
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
getCookie(name) {
|
|
459
|
+
try {
|
|
460
|
+
if (!name || typeof name !== 'string') {
|
|
461
|
+
this.logError('Invalid cookie name provided', { name });
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const cookieValue = document.cookie
|
|
466
|
+
.split("; ")
|
|
467
|
+
.find((row) => row.startsWith(`${this.config.cookiePrefix}${name}=`))
|
|
468
|
+
?.split("=")[1];
|
|
469
|
+
|
|
470
|
+
if (cookieValue) {
|
|
471
|
+
try {
|
|
472
|
+
return JSON.parse(decodeURIComponent(cookieValue));
|
|
473
|
+
} catch (parseError) {
|
|
474
|
+
this.logError('Failed to parse cookie value', { cookieName: name });
|
|
475
|
+
// Try to return the raw value as fallback
|
|
476
|
+
try {
|
|
477
|
+
return decodeURIComponent(cookieValue);
|
|
478
|
+
} catch (decodeError) {
|
|
479
|
+
this.logError('Failed to decode cookie value', { cookieName: name });
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return null;
|
|
485
|
+
} catch (error) {
|
|
486
|
+
this.logError('Cookie retrieval failed', { cookieName: name });
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
deleteCookie(name) {
|
|
492
|
+
try {
|
|
493
|
+
if (!name || typeof name !== 'string') {
|
|
494
|
+
this.logError('Invalid cookie name provided for deletion', { name });
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const domain = this.getDomainForCookie();
|
|
499
|
+
document.cookie = `${this.config.cookiePrefix}${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/${domain}`;
|
|
500
|
+
|
|
501
|
+
// Also try deleting without domain for broader compatibility
|
|
502
|
+
document.cookie = `${this.config.cookiePrefix}${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`;
|
|
503
|
+
|
|
504
|
+
return true;
|
|
505
|
+
} catch (error) {
|
|
506
|
+
this.logError('Cookie deletion failed', { cookieName: name });
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Get domain string for cookie setting
|
|
513
|
+
*/
|
|
514
|
+
getDomainForCookie() {
|
|
515
|
+
// Only set domain for non-localhost environments to support subdomains
|
|
516
|
+
const hostname = location.hostname;
|
|
517
|
+
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname.includes(':')) {
|
|
518
|
+
return '';
|
|
519
|
+
}
|
|
520
|
+
// Use the current domain, allowing cookies to be accessible to subdomains
|
|
521
|
+
const parts = hostname.split('.');
|
|
522
|
+
if (parts.length > 2) {
|
|
523
|
+
// For subdomains like 'app.example.com', use '.example.com'
|
|
524
|
+
return `; domain=.${parts.slice(-2).join('.')}`;
|
|
525
|
+
}
|
|
526
|
+
return `; domain=${hostname}`;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Load existing consent from cookies
|
|
531
|
+
*/
|
|
532
|
+
loadExistingConsent() {
|
|
533
|
+
const consent = this.getCookie("consent");
|
|
534
|
+
const preferences = this.getCookie("preferences");
|
|
535
|
+
|
|
536
|
+
if (consent && preferences) {
|
|
537
|
+
this.currentPreferences = preferences;
|
|
538
|
+
this.applyConsent(preferences);
|
|
539
|
+
this.updateCategoryToggles();
|
|
540
|
+
} else if (this.config.autoShow) {
|
|
541
|
+
// Show banner after delay for new users
|
|
542
|
+
setTimeout(() => this.showBanner(), this.config.showDelay);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Update category toggle states
|
|
548
|
+
*/
|
|
549
|
+
updateCategoryToggles() {
|
|
550
|
+
try {
|
|
551
|
+
if (!Array.isArray(this.config.categories)) {
|
|
552
|
+
this.logError('Categories configuration is not an array');
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
this.config.categories.forEach((category) => {
|
|
557
|
+
try {
|
|
558
|
+
if (!category || typeof category !== 'string') {
|
|
559
|
+
this.logError('Invalid category in updateCategoryToggles', { category });
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const toggle = this.safeQuerySelector(
|
|
564
|
+
`[data-category="${category}"] [data-category-toggle]`
|
|
565
|
+
);
|
|
566
|
+
if (toggle) {
|
|
567
|
+
toggle.checked = this.currentPreferences[category] || false;
|
|
568
|
+
}
|
|
569
|
+
} catch (toggleError) {
|
|
570
|
+
this.logError('Failed to update category toggle', { category });
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
} catch (updateError) {
|
|
574
|
+
this.logError('Critical error updating category toggles');
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Apply consent decisions (load/unload scripts)
|
|
580
|
+
*/
|
|
581
|
+
applyConsent(preferences) {
|
|
582
|
+
this.config.categories.forEach((category) => {
|
|
583
|
+
const config = this.config.categoryConfig[category];
|
|
584
|
+
const accepted = preferences[category];
|
|
585
|
+
|
|
586
|
+
if (accepted) {
|
|
587
|
+
// Load scripts for this category
|
|
588
|
+
this.loadCategoryScripts(category);
|
|
589
|
+
|
|
590
|
+
// Call onAccept callback
|
|
591
|
+
if (config.onAccept && typeof config.onAccept === "function") {
|
|
592
|
+
config.onAccept(category);
|
|
593
|
+
}
|
|
594
|
+
} else {
|
|
595
|
+
// Unload scripts for this category
|
|
596
|
+
this.unloadCategoryScripts(category);
|
|
597
|
+
|
|
598
|
+
// Call onDecline callback
|
|
599
|
+
if (config.onDecline && typeof config.onDecline === "function") {
|
|
600
|
+
config.onDecline(category);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Load scripts for a specific category
|
|
608
|
+
*/
|
|
609
|
+
async loadCategoryScripts(category) {
|
|
610
|
+
try {
|
|
611
|
+
if (!category || typeof category !== 'string') {
|
|
612
|
+
this.logError('Invalid category provided for script loading', { category });
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const config = this.config.categoryConfig[category];
|
|
617
|
+
if (!config) {
|
|
618
|
+
this.logError('No configuration found for category', { category });
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (config.scripts && Array.isArray(config.scripts) && config.scripts.length > 0) {
|
|
623
|
+
// Load scripts sequentially to avoid conflicts
|
|
624
|
+
for (const scriptConfig of config.scripts) {
|
|
625
|
+
try {
|
|
626
|
+
await this.loadScript(scriptConfig, category);
|
|
627
|
+
} catch (scriptError) {
|
|
628
|
+
this.logError('Failed to load script in category', { category, script: scriptConfig });
|
|
629
|
+
// Continue loading other scripts even if one fails
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
} catch (categoryError) {
|
|
634
|
+
this.logError('Critical error loading category scripts', { category });
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Unload scripts for a specific category
|
|
640
|
+
*/
|
|
641
|
+
unloadCategoryScripts(category) {
|
|
642
|
+
// Remove scripts with data-gdpr-category attribute
|
|
643
|
+
const scripts = document.querySelectorAll(
|
|
644
|
+
`script[data-gdpr-category="${category}"]`
|
|
645
|
+
);
|
|
646
|
+
scripts.forEach((script) => script.remove());
|
|
647
|
+
|
|
648
|
+
// Clean up category-specific cookies
|
|
649
|
+
this.cleanupCategoryCookies(category);
|
|
650
|
+
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Validate URL to prevent script injection attacks
|
|
655
|
+
*/
|
|
656
|
+
isValidScriptUrl(url) {
|
|
657
|
+
try {
|
|
658
|
+
const parsedUrl = new URL(url, location.origin);
|
|
659
|
+
// Only allow HTTP and HTTPS protocols
|
|
660
|
+
if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
|
|
661
|
+
return false;
|
|
662
|
+
}
|
|
663
|
+
// Prevent javascript: and data: URLs
|
|
664
|
+
if (parsedUrl.protocol === 'javascript:' || parsedUrl.protocol === 'data:') {
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
667
|
+
return true;
|
|
668
|
+
} catch (e) {
|
|
669
|
+
return false;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Load a script dynamically with error handling and timeout
|
|
675
|
+
*/
|
|
676
|
+
loadScript(scriptConfig, category) {
|
|
677
|
+
return new Promise((resolve, reject) => {
|
|
678
|
+
try {
|
|
679
|
+
if (!scriptConfig) {
|
|
680
|
+
this.logError('No script configuration provided');
|
|
681
|
+
reject(new Error('No script configuration'));
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (!category || typeof category !== 'string') {
|
|
686
|
+
this.logError('Invalid category provided for script loading', { category });
|
|
687
|
+
reject(new Error('Invalid category'));
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const script = document.createElement("script");
|
|
692
|
+
script.setAttribute("data-gdpr-category", category);
|
|
693
|
+
|
|
694
|
+
let scriptSrc;
|
|
695
|
+
let timeout = 10000; // Default 10 second timeout
|
|
696
|
+
|
|
697
|
+
if (typeof scriptConfig === "string") {
|
|
698
|
+
scriptSrc = scriptConfig;
|
|
699
|
+
} else if (typeof scriptConfig === 'object' && scriptConfig !== null) {
|
|
700
|
+
scriptSrc = scriptConfig.src;
|
|
701
|
+
if (scriptConfig.async !== undefined) script.async = scriptConfig.async;
|
|
702
|
+
if (scriptConfig.defer !== undefined) script.defer = scriptConfig.defer;
|
|
703
|
+
if (scriptConfig.type) script.type = scriptConfig.type;
|
|
704
|
+
if (scriptConfig.timeout && typeof scriptConfig.timeout === 'number') {
|
|
705
|
+
timeout = scriptConfig.timeout;
|
|
706
|
+
}
|
|
707
|
+
} else {
|
|
708
|
+
this.logError('Invalid script configuration format', { scriptConfig });
|
|
709
|
+
reject(new Error('Invalid script configuration'));
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (!scriptSrc || typeof scriptSrc !== 'string') {
|
|
714
|
+
this.logError('No script source URL provided');
|
|
715
|
+
reject(new Error('No script source'));
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Validate the script URL before loading
|
|
720
|
+
if (!this.isValidScriptUrl(scriptSrc)) {
|
|
721
|
+
this.logError('Invalid script URL blocked for security', { url: scriptSrc });
|
|
722
|
+
reject(new Error('Invalid script URL'));
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Set up timeout handler
|
|
727
|
+
const timeoutId = setTimeout(() => {
|
|
728
|
+
this.logError('Script loading timeout', { url: scriptSrc, category, timeout });
|
|
729
|
+
script.remove();
|
|
730
|
+
reject(new Error('Script loading timeout'));
|
|
731
|
+
}, timeout);
|
|
732
|
+
|
|
733
|
+
// Handle successful loading
|
|
734
|
+
script.onload = () => {
|
|
735
|
+
clearTimeout(timeoutId);
|
|
736
|
+
resolve();
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
// Handle loading errors
|
|
740
|
+
script.onerror = (error) => {
|
|
741
|
+
clearTimeout(timeoutId);
|
|
742
|
+
this.logError('Script loading failed', { url: scriptSrc, category });
|
|
743
|
+
script.remove();
|
|
744
|
+
reject(new Error('Script loading failed'));
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
script.src = scriptSrc;
|
|
748
|
+
document.head.appendChild(script);
|
|
749
|
+
|
|
750
|
+
} catch (error) {
|
|
751
|
+
this.logError('Critical error in script loading', { category });
|
|
752
|
+
reject(error);
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Clean up cookies for a specific category
|
|
759
|
+
*/
|
|
760
|
+
cleanupCategoryCookies(category) {
|
|
761
|
+
const cookiesToClean = {
|
|
762
|
+
analytics: [
|
|
763
|
+
"_ga",
|
|
764
|
+
"_ga_*",
|
|
765
|
+
"_gid",
|
|
766
|
+
"_gat",
|
|
767
|
+
"__utma",
|
|
768
|
+
"__utmb",
|
|
769
|
+
"__utmc",
|
|
770
|
+
"__utmt",
|
|
771
|
+
"__utmz",
|
|
772
|
+
],
|
|
773
|
+
marketing: [
|
|
774
|
+
"_fbp",
|
|
775
|
+
"_fbc",
|
|
776
|
+
"fr",
|
|
777
|
+
"__Secure-FRCMP",
|
|
778
|
+
"DSID",
|
|
779
|
+
"IDE",
|
|
780
|
+
"test_cookie",
|
|
781
|
+
],
|
|
782
|
+
functional: [],
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
const cookies = cookiesToClean[category] || [];
|
|
786
|
+
cookies.forEach((cookieName) => {
|
|
787
|
+
if (cookieName.includes("*")) {
|
|
788
|
+
// Handle wildcard cookies
|
|
789
|
+
const prefix = cookieName.replace("*", "");
|
|
790
|
+
document.cookie.split(";").forEach((cookie) => {
|
|
791
|
+
const name = cookie.split("=")[0].trim();
|
|
792
|
+
if (name.startsWith(prefix)) {
|
|
793
|
+
const domain = this.getDomainForCookie();
|
|
794
|
+
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/${domain}`;
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
} else {
|
|
798
|
+
const domain = this.getDomainForCookie();
|
|
799
|
+
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/${domain}`;
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* UI control methods
|
|
806
|
+
*/
|
|
807
|
+
showBanner() {
|
|
808
|
+
try {
|
|
809
|
+
if (this.elements.banner) {
|
|
810
|
+
this.elements.banner.style.display = "block";
|
|
811
|
+
// Trigger reflow for animation
|
|
812
|
+
this.elements.banner.offsetHeight;
|
|
813
|
+
this.elements.banner.classList.add("show");
|
|
814
|
+
} else {
|
|
815
|
+
this.logError('Banner element not available for display');
|
|
816
|
+
}
|
|
817
|
+
} catch (error) {
|
|
818
|
+
this.logError('Failed to show banner');
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
hideBanner() {
|
|
823
|
+
try {
|
|
824
|
+
if (this.elements.banner) {
|
|
825
|
+
this.elements.banner.classList.remove("show");
|
|
826
|
+
setTimeout(() => {
|
|
827
|
+
try {
|
|
828
|
+
if (this.elements.banner) {
|
|
829
|
+
this.elements.banner.style.display = "none";
|
|
830
|
+
}
|
|
831
|
+
} catch (hideError) {
|
|
832
|
+
this.logError('Failed to hide banner after timeout');
|
|
833
|
+
}
|
|
834
|
+
}, 300);
|
|
835
|
+
}
|
|
836
|
+
} catch (error) {
|
|
837
|
+
this.logError('Failed to initiate banner hiding');
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
showModal() {
|
|
842
|
+
try {
|
|
843
|
+
if (this.elements.modal) {
|
|
844
|
+
this.updateCategoryToggles();
|
|
845
|
+
this.elements.modal.style.display = "flex";
|
|
846
|
+
// Trigger reflow for animation
|
|
847
|
+
this.elements.modal.offsetHeight;
|
|
848
|
+
this.elements.modal.classList.add("show");
|
|
849
|
+
try {
|
|
850
|
+
document.body.style.overflow = "hidden";
|
|
851
|
+
} catch (bodyError) {
|
|
852
|
+
this.logError('Failed to set body overflow');
|
|
853
|
+
}
|
|
854
|
+
} else {
|
|
855
|
+
this.logError('Modal element not available for display');
|
|
856
|
+
}
|
|
857
|
+
} catch (error) {
|
|
858
|
+
this.logError('Failed to show modal');
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
hideModal() {
|
|
863
|
+
try {
|
|
864
|
+
if (this.elements.modal) {
|
|
865
|
+
this.elements.modal.classList.remove("show");
|
|
866
|
+
try {
|
|
867
|
+
document.body.style.overflow = "";
|
|
868
|
+
} catch (bodyError) {
|
|
869
|
+
this.logError('Failed to reset body overflow');
|
|
870
|
+
}
|
|
871
|
+
setTimeout(() => {
|
|
872
|
+
try {
|
|
873
|
+
if (this.elements.modal) {
|
|
874
|
+
this.elements.modal.style.display = "none";
|
|
875
|
+
}
|
|
876
|
+
} catch (hideError) {
|
|
877
|
+
this.logError('Failed to hide modal after timeout');
|
|
878
|
+
}
|
|
879
|
+
}, 300);
|
|
880
|
+
}
|
|
881
|
+
} catch (error) {
|
|
882
|
+
this.logError('Failed to initiate modal hiding');
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Consent action methods
|
|
888
|
+
*/
|
|
889
|
+
acceptAll() {
|
|
890
|
+
const preferences = { essential: true };
|
|
891
|
+
this.config.categories.forEach((category) => {
|
|
892
|
+
preferences[category] = true;
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
this.saveConsent("all", preferences);
|
|
896
|
+
this.hideBanner();
|
|
897
|
+
this.hideModal();
|
|
898
|
+
|
|
899
|
+
if (this.config.onAccept) {
|
|
900
|
+
this.config.onAccept(preferences);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
acceptEssential() {
|
|
905
|
+
const preferences = { essential: true };
|
|
906
|
+
this.config.categories.forEach((category) => {
|
|
907
|
+
preferences[category] = false;
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
this.saveConsent("essential", preferences);
|
|
911
|
+
this.hideBanner();
|
|
912
|
+
this.hideModal();
|
|
913
|
+
|
|
914
|
+
if (this.config.onDecline) {
|
|
915
|
+
this.config.onDecline(preferences);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
savePreferences() {
|
|
920
|
+
const preferences = { essential: true, ...this.currentPreferences };
|
|
921
|
+
|
|
922
|
+
this.saveConsent("custom", preferences);
|
|
923
|
+
this.hideModal();
|
|
924
|
+
this.hideBanner();
|
|
925
|
+
|
|
926
|
+
if (this.config.onSave) {
|
|
927
|
+
this.config.onSave(preferences);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
saveConsent(type, preferences) {
|
|
932
|
+
this.setCookie("consent", {
|
|
933
|
+
type: type,
|
|
934
|
+
timestamp: new Date().toISOString(),
|
|
935
|
+
preferences: preferences,
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
this.setCookie("preferences", preferences);
|
|
939
|
+
this.currentPreferences = preferences;
|
|
940
|
+
this.applyConsent(preferences);
|
|
941
|
+
this.updateStatusDisplay();
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
clearAll() {
|
|
945
|
+
// Clear consent cookies
|
|
946
|
+
this.deleteCookie("consent");
|
|
947
|
+
this.deleteCookie("preferences");
|
|
948
|
+
|
|
949
|
+
// Unload all category scripts
|
|
950
|
+
this.config.categories.forEach((category) => {
|
|
951
|
+
this.unloadCategoryScripts(category);
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
// Reset preferences
|
|
955
|
+
this.currentPreferences = {};
|
|
956
|
+
this.updateCategoryToggles();
|
|
957
|
+
this.updateStatusDisplay();
|
|
958
|
+
|
|
959
|
+
// Show banner again
|
|
960
|
+
if (this.config.autoShow) {
|
|
961
|
+
this.showBanner();
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* Update status display for demo purposes
|
|
967
|
+
*/
|
|
968
|
+
updateStatusDisplay() {
|
|
969
|
+
if (!this.elements.statusDisplay) return;
|
|
970
|
+
|
|
971
|
+
const consent = this.getCookie("consent");
|
|
972
|
+
const preferences = this.getCookie("preferences") || {};
|
|
973
|
+
|
|
974
|
+
// Clear existing content safely
|
|
975
|
+
this.elements.statusDisplay.replaceChildren();
|
|
976
|
+
|
|
977
|
+
// Create status elements using safe DOM manipulation
|
|
978
|
+
const statusTitle = document.createElement('strong');
|
|
979
|
+
statusTitle.textContent = 'Consent Status:';
|
|
980
|
+
this.elements.statusDisplay.appendChild(statusTitle);
|
|
981
|
+
this.elements.statusDisplay.appendChild(document.createElement('br'));
|
|
982
|
+
|
|
983
|
+
const typeText = document.createTextNode(`Type: ${consent ? consent.type : 'Not set'}`);
|
|
984
|
+
this.elements.statusDisplay.appendChild(typeText);
|
|
985
|
+
this.elements.statusDisplay.appendChild(document.createElement('br'));
|
|
986
|
+
|
|
987
|
+
const timestampText = document.createTextNode(`Timestamp: ${
|
|
988
|
+
consent ? new Date(consent.timestamp).toLocaleString() : 'N/A'
|
|
989
|
+
}`);
|
|
990
|
+
this.elements.statusDisplay.appendChild(timestampText);
|
|
991
|
+
this.elements.statusDisplay.appendChild(document.createElement('br'));
|
|
992
|
+
this.elements.statusDisplay.appendChild(document.createElement('br'));
|
|
993
|
+
|
|
994
|
+
const categoriesTitle = document.createElement('strong');
|
|
995
|
+
categoriesTitle.textContent = 'Categories:';
|
|
996
|
+
this.elements.statusDisplay.appendChild(categoriesTitle);
|
|
997
|
+
this.elements.statusDisplay.appendChild(document.createElement('br'));
|
|
998
|
+
|
|
999
|
+
const essentialText = document.createTextNode(`Essential: ${preferences.essential ? '✅' : '❌'}`);
|
|
1000
|
+
this.elements.statusDisplay.appendChild(essentialText);
|
|
1001
|
+
this.elements.statusDisplay.appendChild(document.createElement('br'));
|
|
1002
|
+
|
|
1003
|
+
this.config.categories.forEach((category) => {
|
|
1004
|
+
const categoryText = document.createTextNode(`${category}: ${preferences[category] ? '✅' : '❌'}`);
|
|
1005
|
+
this.elements.statusDisplay.appendChild(categoryText);
|
|
1006
|
+
this.elements.statusDisplay.appendChild(document.createElement('br'));
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* Public API methods
|
|
1012
|
+
*/
|
|
1013
|
+
getConsent() {
|
|
1014
|
+
try {
|
|
1015
|
+
if (!this.isInitialized) {
|
|
1016
|
+
this.logError('GDPR system not initialized - cannot get consent');
|
|
1017
|
+
return null;
|
|
1018
|
+
}
|
|
1019
|
+
return this.getCookie("consent");
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
this.logError('Failed to get consent');
|
|
1022
|
+
return null;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
getPreferences() {
|
|
1027
|
+
try {
|
|
1028
|
+
if (!this.isInitialized) {
|
|
1029
|
+
this.logError('GDPR system not initialized - cannot get preferences');
|
|
1030
|
+
return {};
|
|
1031
|
+
}
|
|
1032
|
+
return this.getCookie("preferences") || {};
|
|
1033
|
+
} catch (error) {
|
|
1034
|
+
this.logError('Failed to get preferences');
|
|
1035
|
+
return {};
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
hasConsent(category = null) {
|
|
1040
|
+
try {
|
|
1041
|
+
if (!this.isInitialized) {
|
|
1042
|
+
this.logError('GDPR system not initialized - cannot check consent');
|
|
1043
|
+
return false;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
if (category !== null && (typeof category !== 'string' || category.trim() === '')) {
|
|
1047
|
+
this.logError('Invalid category provided to hasConsent', { category });
|
|
1048
|
+
return false;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
const preferences = this.getPreferences();
|
|
1052
|
+
if (category) {
|
|
1053
|
+
return preferences[category] === true;
|
|
1054
|
+
}
|
|
1055
|
+
return Object.keys(preferences).length > 0;
|
|
1056
|
+
} catch (error) {
|
|
1057
|
+
this.logError('Failed to check consent status', { category });
|
|
1058
|
+
return false;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
updateCategory(category, enabled) {
|
|
1063
|
+
try {
|
|
1064
|
+
if (!this.isInitialized) {
|
|
1065
|
+
this.logError('GDPR system not initialized - cannot update category');
|
|
1066
|
+
return false;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
if (!category || typeof category !== 'string' || category.trim() === '') {
|
|
1070
|
+
this.logError('Invalid category provided to updateCategory', { category });
|
|
1071
|
+
return false;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
if (typeof enabled !== 'boolean') {
|
|
1075
|
+
this.logError('Invalid enabled value provided to updateCategory', { category, enabled });
|
|
1076
|
+
return false;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
if (!Array.isArray(this.config.categories) || !this.config.categories.includes(category)) {
|
|
1080
|
+
this.logError('Category not found in configuration', { category });
|
|
1081
|
+
return false;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
const preferences = this.getPreferences();
|
|
1085
|
+
preferences[category] = enabled;
|
|
1086
|
+
this.saveConsent("custom", preferences);
|
|
1087
|
+
return true;
|
|
1088
|
+
} catch (error) {
|
|
1089
|
+
this.logError('Failed to update category', { category, enabled });
|
|
1090
|
+
return false;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
addScript(category, scriptConfig) {
|
|
1095
|
+
try {
|
|
1096
|
+
if (!this.isInitialized) {
|
|
1097
|
+
this.logError('GDPR system not initialized - cannot add script');
|
|
1098
|
+
return false;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
if (!category || typeof category !== 'string' || category.trim() === '') {
|
|
1102
|
+
this.logError('Invalid category provided to addScript', { category });
|
|
1103
|
+
return false;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
if (!scriptConfig) {
|
|
1107
|
+
this.logError('No script configuration provided to addScript', { category });
|
|
1108
|
+
return false;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// Validate script configuration format
|
|
1112
|
+
if (typeof scriptConfig === 'string') {
|
|
1113
|
+
if (scriptConfig.trim() === '') {
|
|
1114
|
+
this.logError('Empty script URL provided', { category });
|
|
1115
|
+
return false;
|
|
1116
|
+
}
|
|
1117
|
+
} else if (typeof scriptConfig === 'object' && scriptConfig !== null) {
|
|
1118
|
+
if (!scriptConfig.src || typeof scriptConfig.src !== 'string' || scriptConfig.src.trim() === '') {
|
|
1119
|
+
this.logError('Invalid script source in configuration', { category });
|
|
1120
|
+
return false;
|
|
1121
|
+
}
|
|
1122
|
+
} else {
|
|
1123
|
+
this.logError('Invalid script configuration format', { category, configType: typeof scriptConfig });
|
|
1124
|
+
return false;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
if (!this.config.categoryConfig[category]) {
|
|
1128
|
+
this.logError('Category configuration not found for addScript', { category });
|
|
1129
|
+
return false;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
this.config.categoryConfig[category].scripts.push(scriptConfig);
|
|
1133
|
+
|
|
1134
|
+
// If category is already accepted, load the script immediately
|
|
1135
|
+
if (this.hasConsent(category)) {
|
|
1136
|
+
this.loadScript(scriptConfig, category).catch(() => {
|
|
1137
|
+
this.logError('Failed to load newly added script', { category });
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
return true;
|
|
1142
|
+
} catch (error) {
|
|
1143
|
+
this.logError('Failed to add script', { category });
|
|
1144
|
+
return false;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
onCategoryChange(category, callback) {
|
|
1149
|
+
try {
|
|
1150
|
+
if (!this.isInitialized) {
|
|
1151
|
+
this.logError('GDPR system not initialized - cannot set category change handler');
|
|
1152
|
+
return false;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
if (!category || typeof category !== 'string' || category.trim() === '') {
|
|
1156
|
+
this.logError('Invalid category provided to onCategoryChange', { category });
|
|
1157
|
+
return false;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
if (!callback || typeof callback !== 'function') {
|
|
1161
|
+
this.logError('Invalid callback provided to onCategoryChange', { category });
|
|
1162
|
+
return false;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
if (!this.config.categoryConfig[category]) {
|
|
1166
|
+
this.config.categoryConfig[category] = {
|
|
1167
|
+
title: `${category} Cookies`,
|
|
1168
|
+
description: `Cookies for ${category} functionality`,
|
|
1169
|
+
scripts: [],
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
const config = this.config.categoryConfig[category];
|
|
1174
|
+
const originalOnAccept = config.onAccept;
|
|
1175
|
+
const originalOnDecline = config.onDecline;
|
|
1176
|
+
|
|
1177
|
+
config.onAccept = (cat) => {
|
|
1178
|
+
try {
|
|
1179
|
+
if (originalOnAccept && typeof originalOnAccept === 'function') {
|
|
1180
|
+
originalOnAccept(cat);
|
|
1181
|
+
}
|
|
1182
|
+
callback(cat, true);
|
|
1183
|
+
} catch (callbackError) {
|
|
1184
|
+
this.logError('Error in category accept callback', { category: cat });
|
|
1185
|
+
}
|
|
1186
|
+
};
|
|
1187
|
+
|
|
1188
|
+
config.onDecline = (cat) => {
|
|
1189
|
+
try {
|
|
1190
|
+
if (originalOnDecline && typeof originalOnDecline === 'function') {
|
|
1191
|
+
originalOnDecline(cat);
|
|
1192
|
+
}
|
|
1193
|
+
callback(cat, false);
|
|
1194
|
+
} catch (callbackError) {
|
|
1195
|
+
this.logError('Error in category decline callback', { category: cat });
|
|
1196
|
+
}
|
|
1197
|
+
};
|
|
1198
|
+
|
|
1199
|
+
return true;
|
|
1200
|
+
} catch (error) {
|
|
1201
|
+
this.logError('Failed to set category change handler', { category });
|
|
1202
|
+
return false;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// Create global instance
|
|
1208
|
+
const gdprInstance = new GDPRCookies();
|
|
1209
|
+
|
|
1210
|
+
// Expose to global scope with error handling wrappers
|
|
1211
|
+
window.GDPRCookies = {
|
|
1212
|
+
init: (options) => {
|
|
1213
|
+
try {
|
|
1214
|
+
return gdprInstance.init(options);
|
|
1215
|
+
} catch (error) {
|
|
1216
|
+
if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
|
|
1217
|
+
console.error('GDPR: Failed to initialize');
|
|
1218
|
+
}
|
|
1219
|
+
return false;
|
|
1220
|
+
}
|
|
1221
|
+
},
|
|
1222
|
+
getConsent: () => {
|
|
1223
|
+
try {
|
|
1224
|
+
return gdprInstance.getConsent();
|
|
1225
|
+
} catch (error) {
|
|
1226
|
+
return null;
|
|
1227
|
+
}
|
|
1228
|
+
},
|
|
1229
|
+
getPreferences: () => {
|
|
1230
|
+
try {
|
|
1231
|
+
return gdprInstance.getPreferences();
|
|
1232
|
+
} catch (error) {
|
|
1233
|
+
return {};
|
|
1234
|
+
}
|
|
1235
|
+
},
|
|
1236
|
+
hasConsent: (category) => {
|
|
1237
|
+
try {
|
|
1238
|
+
return gdprInstance.hasConsent(category);
|
|
1239
|
+
} catch (error) {
|
|
1240
|
+
return false;
|
|
1241
|
+
}
|
|
1242
|
+
},
|
|
1243
|
+
updateCategory: (category, enabled) => {
|
|
1244
|
+
try {
|
|
1245
|
+
return gdprInstance.updateCategory(category, enabled);
|
|
1246
|
+
} catch (error) {
|
|
1247
|
+
return false;
|
|
1248
|
+
}
|
|
1249
|
+
},
|
|
1250
|
+
addScript: (category, script) => {
|
|
1251
|
+
try {
|
|
1252
|
+
return gdprInstance.addScript(category, script);
|
|
1253
|
+
} catch (error) {
|
|
1254
|
+
return false;
|
|
1255
|
+
}
|
|
1256
|
+
},
|
|
1257
|
+
onCategoryChange: (category, callback) => {
|
|
1258
|
+
try {
|
|
1259
|
+
return gdprInstance.onCategoryChange(category, callback);
|
|
1260
|
+
} catch (error) {
|
|
1261
|
+
return false;
|
|
1262
|
+
}
|
|
1263
|
+
},
|
|
1264
|
+
showBanner: () => {
|
|
1265
|
+
try {
|
|
1266
|
+
return gdprInstance.showBanner();
|
|
1267
|
+
} catch (error) {
|
|
1268
|
+
return false;
|
|
1269
|
+
}
|
|
1270
|
+
},
|
|
1271
|
+
showPreferences: () => {
|
|
1272
|
+
try {
|
|
1273
|
+
return gdprInstance.showModal();
|
|
1274
|
+
} catch (error) {
|
|
1275
|
+
return false;
|
|
1276
|
+
}
|
|
1277
|
+
},
|
|
1278
|
+
clearAll: () => {
|
|
1279
|
+
try {
|
|
1280
|
+
return gdprInstance.clearAll();
|
|
1281
|
+
} catch (error) {
|
|
1282
|
+
return false;
|
|
1283
|
+
}
|
|
1284
|
+
},
|
|
1285
|
+
};
|
|
1286
|
+
|
|
1287
|
+
// Auto-initialize if config is provided via data attribute
|
|
1288
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
1289
|
+
try {
|
|
1290
|
+
const configScript = document.querySelector("script[data-gdpr-config]");
|
|
1291
|
+
if (configScript) {
|
|
1292
|
+
const configAttr = configScript.getAttribute("data-gdpr-config");
|
|
1293
|
+
if (!configAttr || configAttr.trim() === '') {
|
|
1294
|
+
if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
|
|
1295
|
+
console.warn('GDPR: Empty configuration attribute found');
|
|
1296
|
+
}
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
try {
|
|
1301
|
+
const config = JSON.parse(configAttr);
|
|
1302
|
+
if (typeof config !== 'object' || config === null) {
|
|
1303
|
+
if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
|
|
1304
|
+
console.error('GDPR: Invalid configuration format - must be an object');
|
|
1305
|
+
}
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
window.GDPRCookies.init(config);
|
|
1309
|
+
} catch (parseError) {
|
|
1310
|
+
if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
|
|
1311
|
+
console.error('GDPR: Failed to parse configuration - invalid JSON format');
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
} catch (error) {
|
|
1316
|
+
if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
|
|
1317
|
+
console.error('GDPR: Auto-initialization failed');
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
});
|
|
1321
|
+
})(window);
|