xcoobee-cookie-kit-core
Version:
GDPR / CCPA Easy Cookie, Script, and Fingerprint Consent Management for Websites
296 lines (261 loc) • 9.93 kB
JavaScript
/* global XcooBee */
/**
* Sets cookies
*
* @param {String} name the name of cookie
* @param {String} value the value of cookie
* @param {Number} days optional: the number of days to persist cookie, if not provided will become session cookie
*/
function xckSetCookie(name, value, days) {
let expires = "";
if (days) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = `; expires=${date.toUTCString()}`;
}
document.cookie = `${name}=${value || ""}${expires}; path=/`;
}
/**
* Deletes cookie with given name
*
* @param {String} name the name of cookie
*/
function xckEraseCookie(name) {
document.cookie = `${name}=; Max-Age=-99999999; path=/`;
}
/**
* Retrieves array of cookie names
*
* @returns {Array} array of cookie names
*/
function xckGetCookieNames() {
const ca = document.cookie.split(";");
const ra = [];
for (let i = 0; i < ca.length; i++) {
const c = ca[i].split("=")[0].trim();
ra.push(c);
}
return ra;
}
/**
* Compares two arrays, return array of elements contained in newArray not part of oldArray
*
* @param {Array} oldArray the initial array to compare against
* @param {Array} newArray the second array. Only elements from this array will be returned
* @returns {Array} array of elements only contained in newArray
*/
function xckReturnNewElements(oldArray, newArray) {
const ra = [];
for (let i = 0; i < newArray.length; i++) {
const item = newArray[i];
if (oldArray.indexOf(item) === -1) {
ra.push(item);
}
}
return ra;
}
/**
* Updates our local store with cookies that have been set for a given category so far
*
* @param {Array} aInitialCookies - The array of cookies that were set before script was run
* @param {String} category - cookie category related to this load
*/
function xckSaveCookieState(aInitialCookies, category) {
const aNewCookies = xckGetCookieNames();
const aAddedCookies = xckReturnNewElements(aInitialCookies, aNewCookies);
// Check what cookies are set now and save this to our local store
if (typeof category !== "undefined") {
// Load and save the cookies set for this category
const storeName = `xck_cat_${category}`;
let currentCategoryCookies = localStorage.getItem(storeName);
if (aAddedCookies.length > 0) {
if (currentCategoryCookies === null) {
currentCategoryCookies = aAddedCookies.join();
} else {
currentCategoryCookies = `${currentCategoryCookies},${aAddedCookies.join()}`;
}
localStorage.setItem(storeName, currentCategoryCookies);
}
}
}
/**
* Loads Javascript. Can load from remote file or code.
*
* @param {Object} loadScripts - The HTMLCollection of script tags to be loaded
* @param {String} category - cookie category related to this load
*/
function xckLoadJs(loadScripts, category) {
const aInitialCookies = xckGetCookieNames();
if (loadScripts.length > 0) {
for (let i = 0; i < loadScripts.length; i++) {
const item = loadScripts[i];
const script = document.createElement("script");
if (item.src === "") {
script.text = item.text;
} else {
// Load from file
script.async = true; // we always load async
script.src = item.src;
// After script is loaded we need to process the cookie state
script.onload = function onload() {
if (XcooBee.kit.getParam("testMode") && (item.src !== "")) {
// eslint-disable-next-line no-console
console.log(`Cookie script from ${item.src} is ready!`);
}
xckSaveCookieState(aInitialCookies, category);
};
// Other elements
if (item.integrity !== "") {
script.integrity = item.integrity;
}
if (item.crossOrigin !== "") {
script.crossOrigin = item.crossOrigin;
}
}
// Now append to document for execution and save cookie state
document.body.appendChild(script);
if (item.src === "") {
xckSaveCookieState(aInitialCookies, category);
}
}
}
}
/**
* Removes cookies for categories without consent.
* Clears all cookies crated by scripts that have no longer consent from user.
*
* @param {Object} cookieTypes - The object collection of cookie categories consented by user
*/
function xckClearCookies(cookieTypes) {
const entries = Object.entries(cookieTypes);
for (let i = 0; i < entries.length; i++) {
const category = entries[i];
const storeName = `xck_cat_${category[0]}`;
// Check whether we need to delete the category
if (!category[1]) {
const currentSetCookies = localStorage.getItem(storeName);
if (currentSetCookies !== null) {
const cookies = currentSetCookies.split(",");
// Iterate and delete
for (let j = 0; j < cookies.length; j++) {
xckEraseCookie(cookies[j]);
}
// Clear local storage
localStorage.removeItem(storeName);
}
}
}
}
/**
* Parse html encoded script directives
*
* @param {Array} htmlData - string with script tags to be loaded
* @return {Object} the fully parsed html elements in HtmlCollection
*/
function xckParseHtml(htmlData) {
// Parse encoded via text area
const txt = document.createElement("textarea");
txt.innerHTML = htmlData;
// Now add this as html to our shadow doc
const el = document.createElement("html");
el.innerHTML = txt.innerText;
return el;
}
/**
* Scans document for HTML defined xbee-script tags and converts them into
* scripts that will be loaded once user grants consent. Will also look for
* unset scripts when user removes consent.
*
* @param {Object} cookieTypes object with true/false for each user cookie type.
* Example Object { application: false, usage: true, statistics: false, advertising: false }
*/
function xckHandleXbeeScriptTags(cookieTypes) {
const xbeCookieScripts = document.getElementsByTagName("xbee-script");
let i = 0;
let resolvedScripts = "";
if (xbeCookieScripts.length > 0 && (XcooBee.kit.getConsentStatus() === "complete")) {
for (i = 0; i < xbeCookieScripts.length; i++) {
const item = xbeCookieScripts[i];
let script = item.outerHTML;
resolvedScripts = "";
try {
// We use this notation to cause error when required attributes are not present
const scriptAction = item.attributes.action.value;
const scriptCategory = item.attributes.category.value;
// For setting continue if user has responded, we are allowed the category, and action matches
if ((scriptAction === "set") && (cookieTypes[scriptCategory])) {
// Replace xbee-script with script
script = script.replace(/xbee-script/ig, "script");
resolvedScripts += script;
}
// For un-setting getCookieTypes() has to be false we use separate condition for easier readability
if ((scriptAction === "unset") && (!cookieTypes[scriptCategory])) {
// Replace xbee-script with script
script = script.replace(/xbee-script/ig, "script");
resolvedScripts += script;
}
// Parse and load text scripts if we can find them
if (resolvedScripts !== "") {
const runnableScripts = xckParseHtml(resolvedScripts);
// Load scripts presumably to set cookies
xckLoadJs(runnableScripts.getElementsByTagName("script"), scriptCategory);
}
} catch (error) {
// Error happened
const msg = "Tag caused error, please make sure you have supplied required attributes:";
console.error(msg, script.substr(0, 40));
if (XcooBee.kit.getParam("testMode")) {
console.error(error);
}
}
}
}
}
/**
* Scans document for HTML defined xbee-cookie tags and sets or removes cookies
* once user grants consent.
*
* @param {Object} cookieTypes object with true/false for each user cookie category.
* Example Object { application: false, usage: true, statistics: false, advertising: false }
*/
function xckHandleXbeeCookieTags(cookieTypes) {
const xbeCookieTags = document.getElementsByTagName("xbee-cookie");
let i = 0;
if (xbeCookieTags.length > 0 && (XcooBee.kit.getConsentStatus() === "complete")) {
for (i = 0; i < xbeCookieTags.length; i++) {
const item = xbeCookieTags[i];
let cookieValue = item.innerHTML;
const itemOuter = item.outerHTML;
try {
// We use the array notation since it will throw error when we have missing required attributes
cookieValue = cookieValue.replace(/\r?\n|\r/g, ""); // Remove CRLF
const cookieName = item.attributes.name.value;
const cookieCategory = item.attributes.category.value;
let cookieDays = 0;
// Days is optional so we use this notation to retrieve it
if (item.attributes.getNamedItem("days") !== null) {
cookieDays = parseInt(item.attributes("days"), 10);
}
// Set or erase this cookie based on whether category is allowed
if (cookieTypes[cookieCategory]) {
// Set cookie
xckSetCookie(cookieName, cookieValue, cookieDays);
} else {
// Erase cookie
xckEraseCookie(cookieName);
}
} catch (error) {
// Error happened
console.error("error in cookie definition in tag:", itemOuter.substr(0, 30));
if (XcooBee.kit.getParam("testMode")) {
console.error(error);
}
}
}
}
}
export default {
xckClearCookies,
xckHandleXbeeScriptTags,
xckHandleXbeeCookieTags,
};