Skip to content

Commit

Permalink
User id module: Ability to store user IDs in cookie and localStorage …
Browse files Browse the repository at this point in the history
…simultaneously (prebid#11482)

* Refactoring - break functions that are handling multiple storage types.

* user id: introduce the concept of enabled storage types

* refactor the way enabled storage types are populated
  • Loading branch information
carlosfelix authored and DecayConstant committed Jul 18, 2024
1 parent 151a456 commit 4efab1c
Show file tree
Hide file tree
Showing 2 changed files with 273 additions and 81 deletions.
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 => {
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*/) || [];
}

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

0 comments on commit 4efab1c

Please sign in to comment.