PageSourceSearch

https://nira.fi/media/plg_system_gdprconsent/js/gdprconsent.js?633675

js nira.fi collected 2026-09-24 18:52:11 UTC 23,150 bytes, 715 lines download raw bytes

1(() => {
2  'use strict';
3
4  if (!window.Joomla || typeof window.Joomla.getOptions !== 'function') {
5    return;
6  }
7
8  const options = window.Joomla.getOptions('plg_system_gdprconsent') || {};
9  const debug = !!options.debug;
10
11  const log = (...args) => {
12    if (debug) {
13      console.log('[GDPRConsent]', ...args);
14    }
15  };
16
17  const STORAGE = {
18    LOCAL: 'localstorage',
19    COOKIE: 'cookie',
20  };
21
22  const runtime = {
23    activatedScripts: [],
24    activatedIframes: [],
25    blockedScripts: 0,
26    blockedIframes: 0,
27  };
28
29  const safeString = (value, fallback = '') => (typeof value === 'string' && value.length ? value : fallback);
30  const clone = (value) => JSON.parse(JSON.stringify(value));
31
32  const storageKey = safeString(options.storageKey, 'joomla_gdprconsent_v3');
33  const cookieName = safeString(options.cookieName, 'joomla_gdprconsent_v3');
34  const cookieDays = Number.isFinite(options.cookieDays) ? options.cookieDays : 180;
35  const cookieSameSite = safeString(options.cookieSameSite, 'Lax');
36  const cookieSecure = !!options.cookieSecure;
37
38  const text = {
39    title: safeString(options.texts?.title, 'Cookies'),
40    message: safeString(options.texts?.message, ''),
41    settingsTitle: safeString(options.texts?.settingsTitle, 'Cookie settings'),
42    settingsIntro: safeString(options.texts?.settingsIntro, ''),
43    acceptAll: safeString(options.texts?.acceptAll, 'Accept all'),
44    reject: safeString(options.texts?.reject, 'Reject non-essential'),
45    settings: safeString(options.texts?.settings, 'Settings'),
46    save: safeString(options.texts?.save, 'Save preferences'),
47    close: safeString(options.texts?.close, 'Close'),
48    policy: safeString(options.texts?.policy, 'Privacy policy'),
49    manage: safeString(options.texts?.manage, 'Manage cookies'),
50    alwaysEnabled: safeString(options.texts?.alwaysEnabled, 'Always enabled'),
51    emptyState: safeString(options.texts?.emptyState, 'No optional categories are configured.'),
52    blockedEmbedTitle: safeString(options.texts?.blockedEmbedTitle, 'Embedded content is blocked'),
53    blockedEmbedText: safeString(options.texts?.blockedEmbedText, 'Allow the required cookie category to load this content.'),
54    blockedEmbedButton: safeString(options.texts?.blockedEmbedButton, 'Open settings'),
55  };
56
57  const escapeHtml = (value) => String(value ?? '').replace(/[&<>"']/g, (char) => ({
58    '&': '&amp;',
59    '<': '&lt;',
60    '>': '&gt;',
61    '"': '&quot;',
62    "'": '&#039;',
63  }[char]));
64
65  const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
66
67  const readCookie = (name) => {
68    const match = document.cookie.match(new RegExp(`(^|;\\s*)${escapeRegExp(name)}=([^;]*)`));
69    return match ? decodeURIComponent(match[2]) : null;
70  };
71
72  const writeCookie = (name, value, days) => {
73    const expires = new Date(Date.now() + days * 864e5).toUTCString();
74    const parts = [
75      `${encodeURIComponent(name)}=${encodeURIComponent(value)}`,
76      `expires=${expires}`,
77      'path=/',
78      `SameSite=${cookieSameSite}`,
79    ];
80
81    if (cookieSecure || cookieSameSite === 'None') {
82      parts.push('Secure');
83    }
84
85    document.cookie = parts.join('; ');
86  };
87
88  const removeCookie = (name) => {
89    const parts = [
90      `${encodeURIComponent(name)}=`,
91      'expires=Thu, 01 Jan 1970 00:00:00 GMT',
92      'path=/',
93      `SameSite=${cookieSameSite}`,
94    ];
95
96    if (cookieSecure || cookieSameSite === 'None') {
97      parts.push('Secure');
98    }
99
100    document.cookie = parts.join('; ');
101  };
102
103  const defaultState = () => {
104    const categories = {};
105    const defs = options.categories || {};
106
107    Object.keys(defs).forEach((key) => {
108      const def = defs[key] || {};
109      if (def.enabled === false) return;
110      categories[key] = !!def.default;
111      if (def.required) {
112        categories[key] = true;
113      }
114    });
115
116    return {
117      version: options.stateVersion || 31,
118      decided: false,
119      categories,
120      updatedAt: null,
121    };
122  };
123
124  const normalizeState = (inputState) => {
125    const defs = options.categories || {};
126    const output = inputState && typeof inputState === 'object' ? clone(inputState) : defaultState();
127
128    output.version = options.stateVersion || 31;
129    output.decided = !!output.decided;
130    output.categories = output.categories && typeof output.categories === 'object' ? output.categories : {};
131    output.updatedAt = output.updatedAt || null;
132
133    Object.keys(defs).forEach((key) => {
134      const def = defs[key] || {};
135
136      if (def.enabled === false) {
137        delete output.categories[key];
138        return;
139      }
140
141      if (!(key in output.categories)) {
142        output.categories[key] = !!def.default;
143      }
144
145      if (def.required) {
146        output.categories[key] = true;
147      }
148    });
149
150    return output;
151  };
152
153  const loadState = () => {
154    try {
155      if (options.storage === STORAGE.COOKIE) {
156        const raw = readCookie(cookieName);
157        return raw ? normalizeState(JSON.parse(raw)) : null;
158      }
159
160      const raw = window.localStorage.getItem(storageKey);
161      return raw ? normalizeState(JSON.parse(raw)) : null;
162    } catch (error) {
163      log('Failed to load consent state', error);
164      return null;
165    }
166  };
167
168  const saveState = (state) => {
169    const raw = JSON.stringify(state);
170
171    try {
172      if (options.storage === STORAGE.COOKIE) {
173        writeCookie(cookieName, raw, cookieDays);
174        return;
175      }
176
177      window.localStorage.setItem(storageKey, raw);
178    } catch (error) {
179      log('Failed to save consent state', error);
180    }
181  };
182
183  const clearState = () => {
184    try {
185      if (options.storage === STORAGE.COOKIE) {
186        removeCookie(cookieName);
187      } else {
188        window.localStorage.removeItem(storageKey);
189      }
190    } catch (error) {
191      log('Failed to clear consent state', error);
192    }
193  };
194
195  let state = normalizeState(loadState() || defaultState());
196
197  const hasOptionalCategory = () => {
198    const defs = options.categories || {};
199    return Object.keys(defs).some((key) => {
200      const def = defs[key] || {};
201      return def.enabled !== false && !def.required && key !== 'essential';
202    });
203  };
204
205  const isAllowed = (categoryKey) => !!(state.categories && state.categories[categoryKey]);
206
207  const setAll = (value) => {
208    const defs = options.categories || {};
209    Object.keys(defs).forEach((key) => {
210      const def = defs[key] || {};
211      if (def.enabled === false) return;
212      if (def.required) {
213        state.categories[key] = true;
214        return;
215      }
216      state.categories[key] = !!value;
217    });
218  };
219
220  const pushConsentEvent = () => {
221    window.dataLayer = window.dataLayer || [];
222    window.dataLayer.push({
223      event: 'joomla_gdpr_consent_update',
224      joomla_gdpr_consent: {
225        decided: state.decided,
226        categories: clone(state.categories),
227        updatedAt: state.updatedAt,
228      },
229    });
230  };
231
232  const mapGoogleConsent = () => {
233    const preferencesEnabled = options.categories?.preferences?.enabled !== false;
234    const preferencesAllowed = preferencesEnabled ? !!state.categories.preferences : true;
235
236    return {
237      ad_storage: isAllowed('marketing') ? 'granted' : 'denied',
238      ad_user_data: isAllowed('marketing') ? 'granted' : 'denied',
239      ad_personalization: isAllowed('marketing') ? 'granted' : 'denied',
240      analytics_storage: isAllowed('analytics') ? 'granted' : 'denied',
241      functionality_storage: preferencesAllowed ? 'granted' : 'denied',
242      personalization_storage: preferencesEnabled && preferencesAllowed ? 'granted' : 'denied',
243      security_storage: 'granted',
244    };
245  };
246
247  const syncGoogleConsent = () => {
248    if (!options.consentMode?.enabled) {
249      return;
250    }
251
252    window.dataLayer = window.dataLayer || [];
253    window.gtag = window.gtag || function gtag(){ window.dataLayer.push(arguments); };
254    window.gtag('consent', 'update', mapGoogleConsent());
255  };
256
257  const dispatchChange = () => {
258    document.dispatchEvent(new CustomEvent('joomla-gdprconsent:changed', {
259      detail: clone(state),
260    }));
261  };
262
263  const recordActivation = (listName, vendor, category) => {
264    const list = runtime[listName];
265    if (!Array.isArray(list)) return;
266    const key = `${vendor}|${category}`;
267    if (!list.find((item) => item.key === key)) {
268      list.push({ key, vendor, category, at: new Date().toISOString() });
269    }
270  };
271
272  const copyScriptAttributes = (source, target) => {
273    Array.from(source.attributes).forEach((attribute) => {
274      if (attribute.name === 'type') return;
275      if (attribute.name.startsWith('data-gdpr-')) return;
276      target.setAttribute(attribute.name, attribute.value);
277    });
278
279    if (source.dataset.gdprType) {
280      target.type = source.dataset.gdprType;
281    }
282  };
283
284  const activateDeferredScripts = () => {
285    if (!options.autoActivateScripts) {
286      return;
287    }
288
289    const deferredScripts = document.querySelectorAll('script[type="text/plain"][data-gdpr-category]');
290    runtime.blockedScripts = deferredScripts.length;
291
292    deferredScripts.forEach((script) => {
293      const category = script.dataset.gdprCategory;
294      const vendor = script.dataset.gdprVendor || 'custom';
295
296      if (!category || !isAllowed(category)) {
297        return;
298      }
299
300      const replacement = document.createElement('script');
301      copyScriptAttributes(script, replacement);
302
303      if (script.getAttribute('src')) {
304        replacement.src = script.getAttribute('src');
305      }
306
307      if (!script.getAttribute('src') && script.textContent) {
308        replacement.textContent = script.textContent;
309      }
310
311      script.parentNode.insertBefore(replacement, script);
312      script.remove();
313      recordActivation('activatedScripts', vendor, category);
314    });
315  };
316
317  const removeEmbedPlaceholder = (iframe) => {
318    const placeholderId = iframe.dataset.gdprPlaceholderId;
319    if (!placeholderId) return;
320    const placeholder = document.getElementById(placeholderId);
321    if (placeholder) {
322      placeholder.remove();
323    }
324    delete iframe.dataset.gdprPlaceholderId;
325  };
326
327  const createEmbedPlaceholder = (iframe, category) => {
328    const placeholder = document.createElement('div');
329    const placeholderId = `joomla-gdprconsent-placeholder-${Math.random().toString(36).slice(2)}`;
330
331    placeholder.id = placeholderId;
332    placeholder.className = 'joomla-gdprconsent-embed-placeholder';
333    placeholder.innerHTML = `
334      <div class="joomla-gdprconsent-embed-placeholder__title">${escapeHtml(text.blockedEmbedTitle)}</div>
335      <div class="joomla-gdprconsent-embed-placeholder__text">${escapeHtml(text.blockedEmbedText)}</div>
336      <div class="joomla-gdprconsent-embed-placeholder__meta">${escapeHtml(category)}</div>
337      <button type="button" class="joomla-gdprconsent-embed-placeholder__button">${escapeHtml(text.blockedEmbedButton)}</button>
338    `;
339
340    placeholder.querySelector('button')?.addEventListener('click', () => {
341      renderModal();
342    });
343
344    iframe.insertAdjacentElement('beforebegin', placeholder);
345    iframe.dataset.gdprPlaceholderId = placeholderId;
346  };
347
348  const updateDeferredEmbeds = () => {
349    const deferredIframes = document.querySelectorAll('iframe[data-gdpr-src][data-gdpr-category]');
350    runtime.blockedIframes = Array.from(deferredIframes).filter((iframe) => !isAllowed(iframe.dataset.gdprCategory || '')).length;
351
352    deferredIframes.forEach((iframe) => {
353      const category = iframe.dataset.gdprCategory;
354      const vendor = iframe.dataset.gdprVendor || 'embed';
355
356      if (!category) return;
357
358      if (isAllowed(category)) {
359        if (!iframe.getAttribute('src')) {
360          iframe.setAttribute('src', iframe.dataset.gdprSrc || '');
361          recordActivation('activatedIframes', vendor, category);
362        }
363        iframe.style.display = '';
364        removeEmbedPlaceholder(iframe);
365        return;
366      }
367
368      iframe.style.display = 'none';
369      if (!iframe.dataset.gdprPlaceholderId) {
370        createEmbedPlaceholder(iframe, category);
371      }
372    });
373  };
374
375  const shouldReloadForRevocation = (previousState, nextState) => {
376    if (!options.reloadOnRevocation || !previousState) {
377      return false;
378    }
379
380    const prevCategories = previousState.categories || {};
381    const nextCategories = nextState.categories || {};
382
383    return Object.keys(nextCategories).some((key) => !!prevCategories[key] && !nextCategories[key]);
384  };
385
386  const closeBanner = () => {
387    document.getElementById('joomla-gdprconsent-banner')?.remove();
388  };
389
390  const closeModal = () => {
391    document.getElementById('joomla-gdprconsent-modal')?.remove();
392  };
393
394  const renderManageButton = () => {
395    if (!options.showManageButton) {
396      document.getElementById('joomla-gdprconsent-manage')?.remove();
397      return;
398    }
399
400    let button = document.getElementById('joomla-gdprconsent-manage');
401
402    if (!button) {
403      button = document.createElement('button');
404      button.type = 'button';
405      button.id = 'joomla-gdprconsent-manage';
406      button.className = `joomla-gdprconsent-manage pos-${safeString(options.manageButtonPosition, 'bottom-left')}`;
407      button.addEventListener('click', () => renderModal());
408      document.body.appendChild(button);
409    }
410
411    button.className = `joomla-gdprconsent-manage pos-${safeString(options.manageButtonPosition, 'bottom-left')}`;
412    button.textContent = text.manage;
413  };
414
415  const renderDebugPanel = () => {
416    if (!options.showDebugPanel) {
417      document.getElementById('joomla-gdprconsent-debug')?.remove();
418      return;
419    }
420
421    let panel = document.getElementById('joomla-gdprconsent-debug');
422
423    if (!panel) {
424      panel = document.createElement('details');
425      panel.id = 'joomla-gdprconsent-debug';
426      panel.className = 'joomla-gdprconsent-debug';
427      panel.innerHTML = '<summary>GDPR debug</summary><pre></pre>';
428      document.body.appendChild(panel);
429    }
430
431    const pre = panel.querySelector('pre');
432    if (pre) {
433      pre.textContent = JSON.stringify({
434        geo: options.geo || {},
435        consentMode: options.consentMode || {},
436        state,
437        blockedScripts: runtime.blockedScripts,
438        blockedIframes: runtime.blockedIframes,
439        activatedScripts: runtime.activatedScripts,
440        activatedIframes: runtime.activatedIframes,
441        configuredVendors: options.configuredVendors || [],
442      }, null, 2);
443    }
444  };
445
446  const applyState = () => {
447    syncGoogleConsent();
448    activateDeferredScripts();
449    updateDeferredEmbeds();
450    pushConsentEvent();
451    dispatchChange();
452    renderManageButton();
453    renderDebugPanel();
454  };
455
456  const persistAndApply = (decided, previousState) => {
457    state.decided = !!decided;
458    state.updatedAt = new Date().toISOString();
459    saveState(state);
460    applyState();
461
462    if (shouldReloadForRevocation(previousState, state)) {
463      window.setTimeout(() => window.location.reload(), 80);
464    }
465  };
466
467  const renderModal = () => {
468    closeModal();
469
470    const defs = options.categories || {};
471    const rows = Object.keys(defs)
472      .filter((key) => defs[key]?.enabled !== false)
473      .map((key) => {
474        const def = defs[key] || {};
475        const checked = isAllowed(key) ? 'checked' : '';
476        const disabled = def.required ? 'disabled' : '';
477        const hint = def.required ? `<span class="joomla-gdprconsent-toggle__hint">${escapeHtml(text.alwaysEnabled)}</span>` : '';
478
479        return `
480          <div class="joomla-gdprconsent-toggle">
481            <label class="joomla-gdprconsent-toggle__row">
482              <input type="checkbox" data-category="${escapeHtml(key
482)}" ${checked} ${disabled}>
483              <span class="joomla-gdprconsent-toggle__label">${escapeHtml(def.label || key)}</span>
484            </label>
485            <div class="joomla-gdprconsent-toggle__desc">${escapeHtml(def.description || '')} ${hint}</div>
486          </div>
487        `;
488      })
489      .join('');
490
491    const policyLink = options.privacyPolicyUrl
492      ? `<div class="joomla-gdprconsent-modal__policy"><a href="${escapeHtml(options.privacyPolicyUrl)}">${escapeHtml(text.policy)}</a></div>`
493      : '';
494
495    const modal = document.createElement('div');
496    modal.id = 'joomla-gdprconsent-modal';
497    modal.className = 'joomla-gdprconsent-modal';
498    modal.setAttribute('role', 'dialog');
499    modal.setAttribute('aria-modal', 'true');
500
501    modal.innerHTML = `
502      <div class="joomla-gdprconsent-modal__backdrop" data-action="close"></div>
503      <div class="joomla-gdprconsent-modal__dialog">
504        <div class="joomla-gdprconsent-modal__header">
505          <div class="joomla-gdprconsent-modal__title">${escapeHtml(text.settingsTitle)}</div>
506          <button type="button" class="joomla-gdprconsent-modal__close" data-action="close" aria-label="${escapeHtml(text.close)}">x</button>
507        </div>
508        <div class="joomla-gdprconsent-modal__body">
509          ${text.settingsIntro ? `<div class="joomla-gdprconsent-modal__intro">${escapeHtml(text.settingsIntro)}</div>` : ''}
510          ${rows || `<div class="joomla-gdprconsent-modal__empty">${escapeHtml(text.emptyState)}</div>`}
511          ${policyLink}
512        </div>
513        <div class="joomla-gdprconsent-modal__footer">
514          <button type="button" class="btn btn-outline-secondary" data-action="reject">${escapeHtml(text.reject)}</button>
515          <button type="button" class="btn btn-primary" data-action="acceptAll">${escapeHtml(text.acceptAll)}</button>
516          <button type="button" class="btn btn-success" data-action="save">${escapeHtml(text.save)}</button>
517        </div>
518      </div>
519    `;
520
521    document.body.appendChild(modal);
522
523    const destroyModal = () => {
524      closeModal();
525      document.removeEventListener('keydown', onKeyDown);
526    };
527
528    const onKeyDown = (event) => {
529      if (event.key === 'Escape') {
530        destroyModal();
531      }
532    };
533
534    document.addEventListener('keydown', onKeyDown);
535
536    modal.addEventListener('click', (event) => {
537      const actionElement = event.target.closest('[data-action]');
538      if (!actionElement) return;
539
540      const action = actionElement.dataset.action;
541      const previousState = clone(state);
542
543      if (action === 'close') {
544        destroyModal();
545        return;
546      }
547
548      if (action === 'acceptAll') {
549        setAll(true);
550        persistAndApply(true, previousState);
551        destroyModal();
552        closeBanner();
553        return;
554      }
555
556      if (action === 'reject') {
557        setAll(false);
558        persistAndApply(true, previousState);
559        destroyModal();
560        closeBanner();
561        return;
562      }
563
564      if (action === 'save') {
565        modal.querySelectorAll('input[type="checkbox"][data-category]').forEach((input) => {
566          const key = input.dataset.category;
567          const def = defs[key] || {};
568          if (def.required) {
569            state.categories[key] = true;
570            return;
571          }
572          state.categories[key] = input.checked;
573        });
574        persistAndApply(true, previousState);
575        destroyModal();
576        closeBanner();
577      }
578    });
579  };
580
581  const renderBanner = () => {
582    closeBanner();
583
584    if (!options.showBanner || !hasOptionalCategory() || state.decided) {
585      return;
586    }
587
588    const policyLink = options.privacyPolicyUrl
589      ? `<div class="joomla-gdprconsent-banner__policy"><a href="${escapeHtml(options.privacyPolicyUrl)}">${escapeHtml(text.policy)}</a></div>`
590      : '';
591
592    const banner = document.createElement('div');
593    banner.id = 'joomla-gdprconsent-banner';
594    banner.className = `joomla-gdprconsent-banner pos-${safeString(options.position, 'bottom')} theme-${safeString(options.theme, 'light')}`;
595    banner.setAttribute('role', 'dialog');
596    banner.setAttribute('aria-live', 'polite');
597
598    banner.innerHTML = `
599      <div class="joomla-gdprconsent-banner__inner">
600        <div class="joomla-gdprconsent-banner__content">
601          <div class="joomla-gdprconsent-banner__title">${escapeHtml(text.title)}</div>
602          <div class="joomla-gdprconsent-banner__message">${escapeHtml(text.message)}</div>
603          ${policyLink}
604        </div>
605        <div class="joomla-gdprconsent-banner__actions">
606          <button type="button" class="btn btn-primary" data-action="acceptAll">${escapeHtml(text.acceptAll)}</button>
607          <button type="button" class="btn btn-outline-secondary" data-action="reject">${escapeHtml(text.reject)}</button>
608          <button type="button" class="btn btn-link" data-action="settings">${escapeHtml(text.settings)}</button>
609        </div>
610      </div>
611    `;
612
613    document.body.appendChild(banner);
614
615    banner.addEventListener('click', (event) => {
616      const button = event.target.closest('button[data-action]');
617      if (!button) return;
618
619      const action = button.dataset.action;
620      const previousState = clone(state);
621
622      if (action === 'acceptAll') {
623        setAll(true);
624        persistAndApply(true, previousState);
625        closeBanner();
626        return;
627      }
628
629      if (action === 'reject') {
630        setAll(false);
631        persistAndApply(true, previousState);
632        closeBanner();
633        return;
634      }
635
636      if (action === 'settings') {
637        renderModal();
638      }
639    });
640  };
641
642  const applyGeoBehavior = () => {
643    if (state.decided) {
644      return;
645    }
646
647    const geo = options.geo || {};
648
649    if (geo.mode !== 'regulated') {
650      return;
651    }
652
653    const previousState = clone(state);
654
655    if (geo.regulated === false) {
656      if (geo.nonRegulatedBehavior === 'hide_banner_accept_all') {
657        setAll(true);
658        persistAndApply(true, previousState);
659      } else if (geo.nonRegulatedBehavior === 'hide_banner_use_defaults') {
660        state.decided = true;
661        state.updatedAt = new Date().toISOString();
662        saveState(state);
663      }
664      return;
665    }
666
667    if (geo.regulated === null && geo.unknownBehavior === 'hide_banner_use_defaults') {
668      state.decided = true;
669      state.updatedAt = new Date().toISOString();
670      saveState(state);
671    }
672  };
673
674  window.JoomlaGdprConsent = Object.freeze({
675    getOptions: () => clone(options),
676    getState: () => clone(state),
677    isAllowed: (categoryKey) => isAllowed(categoryKey),
678    acceptAll: () => {
679      const previousState = clone(state);
680      setAll(true);
681      persistAndApply(true, previousState);
682      closeBanner();
683      closeModal();
684    },
685    rejectAll: () => {
686      const previousState = clone(state);
687      setAll(false);
688      persistAndApply(true, previousState);
689      closeBanner();
690      closeModal();
691    },
692    openSettings: () => renderModal(),
693    reset: () => {
694      clearState();
695      state = normalizeState(defaultState());
696      renderBanner();
697      renderManageButton();
698      renderDebugPanel();
699    },
700    onChange: (handler) => {
701      if (typeof handler !== 'function') return () => {};
702      const listener = (event) => handler(event.detail);
703      document.addEventListener('joomla-gdprconsent:changed', listener);
704      return () => document.removeEventListener('joomla-gdprconsent:changed', listener);
705    },
706  });
707
708  document.addEventListener('DOMContentLoaded', () => {
709    applyGeoBehavior();
710    applyState();
711    renderBanner();
712    renderManageButton();
713    renderDebugPanel();
714  });
715})();

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.