Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User id module: Ability to store user IDs in cookie and localStorage simultaneously #11482

Merged
merged 3 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 159 additions & 73 deletions modules/userId/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,29 @@ function cookieSetter(submodule, storageMgr) {
}
}

function setValueInCookie(submodule, valueStr, expiresStr) {
const storage = submodule.config.storage;
const setCookie = cookieSetter(submodule);

setCookie(null, valueStr, expiresStr);
setCookie('_cst', getConsentHash(), expiresStr);
if (typeof storage.refreshInSeconds === 'number') {
setCookie('_last', new Date().toUTCString(), expiresStr);
}
}

function setValueInLocalStorage(submodule, valueStr, expiresStr) {
const storage = submodule.config.storage;
const mgr = submodule.storageMgr;

mgr.setDataInLocalStorage(`${storage.name}_exp`, expiresStr);
mgr.setDataInLocalStorage(`${storage.name}_cst`, getConsentHash());
mgr.setDataInLocalStorage(storage.name, encodeURIComponent(valueStr));
if (typeof storage.refreshInSeconds === 'number') {
mgr.setDataInLocalStorage(`${storage.name}_last`, new Date().toUTCString());
}
}

/**
* @param {SubmoduleContainer} submodule
* @param {(Object|string)} value
Expand All @@ -248,54 +271,62 @@ export function setStoredValue(submodule, value) {
* @type {SubmoduleStorage}
*/
const storage = submodule.config.storage;
const mgr = submodule.storageMgr;

try {
const expiresStr = (new Date(Date.now() + (storage.expires * (60 * 60 * 24 * 1000)))).toUTCString();
const valueStr = isPlainObject(value) ? JSON.stringify(value) : value;
if (storage.type === COOKIE) {
const setCookie = cookieSetter(submodule);
setCookie(null, valueStr, expiresStr);
setCookie('_cst', getConsentHash(), expiresStr);
if (typeof storage.refreshInSeconds === 'number') {
setCookie('_last', new Date().toUTCString(), expiresStr);
}
} else if (storage.type === LOCAL_STORAGE) {
mgr.setDataInLocalStorage(`${storage.name}_exp`, expiresStr);
mgr.setDataInLocalStorage(`${storage.name}_cst`, getConsentHash());
mgr.setDataInLocalStorage(storage.name, encodeURIComponent(valueStr));
if (typeof storage.refreshInSeconds === 'number') {
mgr.setDataInLocalStorage(`${storage.name}_last`, new Date().toUTCString());

submodule.enabledStorageTypes.forEach(storageType => {
switch (storageType) {
case COOKIE:
setValueInCookie(submodule, valueStr, expiresStr);
break;
case LOCAL_STORAGE:
setValueInLocalStorage(submodule, valueStr, expiresStr);
break;
}
}
});
} catch (error) {
logError(error);
}
}

function deleteValueFromCookie(submodule) {
const setCookie = cookieSetter(submodule, coreStorage);
const expiry = (new Date(Date.now() - 1000 * 60 * 60 * 24)).toUTCString();

['', '_last', '_cst'].forEach(suffix => {
try {
setCookie(suffix, '', expiry);
} catch (e) {
logError(e);
}
})
}

function deleteValueFromLocalStorage(submodule) {
['', '_last', '_exp', '_cst'].forEach(suffix => {
try {
coreStorage.removeDataFromLocalStorage(submodule.config.storage.name + suffix);
} catch (e) {
logError(e);
}
});
}

export function deleteStoredValue(submodule) {
let deleter, suffixes;
switch (submodule.config?.storage?.type) {
case COOKIE:
const setCookie = cookieSetter(submodule, coreStorage);
const expiry = (new Date(Date.now() - 1000 * 60 * 60 * 24)).toUTCString();
deleter = (suffix) => setCookie(suffix, '', expiry)
suffixes = ['', '_last', '_cst'];
break;
case LOCAL_STORAGE:
deleter = (suffix) => coreStorage.removeDataFromLocalStorage(submodule.config.storage.name + suffix)
suffixes = ['', '_last', '_exp', '_cst'];
break;
}
if (deleter) {
suffixes.forEach(suffix => {
try {
deleter(suffix)
} catch (e) {
logError(e);
}
});
}
populateEnabledStorageTypes(submodule);

submodule.enabledStorageTypes.forEach(storageType => {
switch (storageType) {
case COOKIE:
deleteValueFromCookie(submodule);
break;
case LOCAL_STORAGE:
deleteValueFromLocalStorage(submodule);
break;
}
});
}

function setPrebidServerEidPermissions(initializedSubmodules) {
Expand All @@ -305,30 +336,46 @@ function setPrebidServerEidPermissions(initializedSubmodules) {
}
}

function getValueFromCookie(submodule, storedKey) {
return submodule.storageMgr.getCookie(storedKey)
}

function getValueFromLocalStorage(submodule, storedKey) {
const mgr = submodule.storageMgr;
const storage = submodule.config.storage;
const storedValueExp = mgr.getDataFromLocalStorage(`${storage.name}_exp`);

// empty string means no expiration set
if (storedValueExp === '') {
return mgr.getDataFromLocalStorage(storedKey);
} else if (storedValueExp && ((new Date(storedValueExp)).getTime() - Date.now() > 0)) {
return decodeURIComponent(mgr.getDataFromLocalStorage(storedKey));
}
}

/**
* @param {SubmoduleContainer} submodule
* @param {String|undefined} key optional key of the value
* @returns {string}
*/
function getStoredValue(submodule, key = undefined) {
const mgr = submodule.storageMgr;
const storage = submodule.config.storage;
const storedKey = key ? `${storage.name}_${key}` : storage.name;
let storedValue;
try {
if (storage.type === COOKIE) {
storedValue = mgr.getCookie(storedKey);
} else if (storage.type === LOCAL_STORAGE) {
const storedValueExp = mgr.getDataFromLocalStorage(`${storage.name}_exp`);
// empty string means no expiration set
if (storedValueExp === '') {
storedValue = mgr.getDataFromLocalStorage(storedKey);
} else if (storedValueExp) {
if ((new Date(storedValueExp)).getTime() - Date.now() > 0) {
storedValue = decodeURIComponent(mgr.getDataFromLocalStorage(storedKey));
}
submodule.enabledStorageTypes.find(storageType => {
ncolletti marked this conversation as resolved.
Show resolved Hide resolved
switch (storageType) {
case COOKIE:
storedValue = getValueFromCookie(submodule, storedKey);
break;
case LOCAL_STORAGE:
storedValue = getValueFromLocalStorage(submodule, storedKey);
break;
}
}

return !!storedValue;
});

// support storing a string or a stringified object
if (typeof storedValue === 'string' && storedValue.trim().charAt(0) === '{') {
storedValue = JSON.parse(storedValue);
Expand Down Expand Up @@ -776,8 +823,10 @@ function populateSubmoduleId(submodule, forceRefresh, allSubmodules) {
}

if (!storedId || refreshNeeded || forceRefresh || consentChanged(submodule)) {
const extendedConfig = Object.assign({ enabledStorageTypes: submodule.enabledStorageTypes }, submodule.config);

// No id previously saved, or a refresh is needed, or consent has changed. Request a new id from the submodule.
response = submodule.submodule.getId(submodule.config, gdprConsent, storedId);
response = submodule.submodule.getId(extendedConfig, gdprConsent, storedId);
} else if (typeof submodule.submodule.extendId === 'function') {
// If the id exists already, give submodule a chance to decide additional actions that need to be taken
response = submodule.submodule.extendId(submodule.config, gdprConsent, storedId);
Expand Down Expand Up @@ -834,6 +883,8 @@ function initSubmodules(dest, submodules, forceRefresh = false) {
return uidMetrics().fork().measureTime('userId.init.modules', function () {
if (!submodules.length) return []; // to simplify log messages from here on

submodules.forEach(submod => populateEnabledStorageTypes(submod));

/**
* filter out submodules that:
*
Expand Down Expand Up @@ -884,6 +935,16 @@ function updateInitializedSubmodules(dest, submodule) {
}
}

function getConfiguredStorageTypes(config) {
return config?.storage?.type?.trim().split(/\s*&\s*/) || [];
ncolletti marked this conversation as resolved.
Show resolved Hide resolved
}

function hasValidStorageTypes(config) {
const storageTypes = getConfiguredStorageTypes(config);

return storageTypes.every(storageType => ALL_STORAGE_TYPES.has(storageType));
}

/**
* list of submodule configurations with valid 'storage' or 'value' obj definitions
* storage config: contains values for storing/retrieving User ID data in browser storage
Expand All @@ -905,7 +966,7 @@ function getValidSubmoduleConfigs(configRegistry) {
if (config.storage &&
!isEmptyStr(config.storage.type) &&
!isEmptyStr(config.storage.name) &&
ALL_STORAGE_TYPES.has(config.storage.type)) {
hasValidStorageTypes(config)) {
carry.push(config);
} else if (isPlainObject(config.value)) {
carry.push(config);
Expand All @@ -918,28 +979,53 @@ function getValidSubmoduleConfigs(configRegistry) {

const ALL_STORAGE_TYPES = new Set([LOCAL_STORAGE, COOKIE]);

function canUseStorage(submodule) {
switch (submodule.config?.storage?.type) {
case LOCAL_STORAGE:
if (submodule.storageMgr.localStorageIsEnabled()) {
if (coreStorage.getDataFromLocalStorage(PBJS_USER_ID_OPTOUT_NAME)) {
logInfo(`${MODULE_NAME} - opt-out localStorage found, storage disabled`);
return false
}
return true;
}
break;
case COOKIE:
if (submodule.storageMgr.cookiesAreEnabled()) {
if (coreStorage.getCookie(PBJS_USER_ID_OPTOUT_NAME)) {
logInfo(`${MODULE_NAME} - opt-out cookie found, storage disabled`);
return false;
}
return true
}
break;
function canUseLocalStorage(submodule) {
if (!submodule.storageMgr.localStorageIsEnabled()) {
return false;
}

if (coreStorage.getDataFromLocalStorage(PBJS_USER_ID_OPTOUT_NAME)) {
logInfo(`${MODULE_NAME} - opt-out localStorage found, storage disabled`);
return false
}

return true;
}

function canUseCookies(submodule) {
if (!submodule.storageMgr.cookiesAreEnabled()) {
return false;
}

if (coreStorage.getCookie(PBJS_USER_ID_OPTOUT_NAME)) {
logInfo(`${MODULE_NAME} - opt-out cookie found, storage disabled`);
return false;
}

return true
}

function populateEnabledStorageTypes(submodule) {
if (submodule.enabledStorageTypes) {
return;
}
return false;

const storageTypes = getConfiguredStorageTypes(submodule.config);

submodule.enabledStorageTypes = storageTypes.filter(type => {
switch (type) {
case LOCAL_STORAGE:
return canUseLocalStorage(submodule);
case COOKIE:
return canUseCookies(submodule);
}

return false;
});
}

function canUseStorage(submodule) {
return !!submodule.enabledStorageTypes.length;
}

function updateEIDConfig(submodules) {
Expand Down
Loading