diff --git a/modules/lotamePanoramaIdSystem.js b/modules/lotamePanoramaIdSystem.js index 64d631c2469..3be7b261723 100644 --- a/modules/lotamePanoramaIdSystem.js +++ b/modules/lotamePanoramaIdSystem.js @@ -11,13 +11,11 @@ import { isBoolean, buildUrl, isEmpty, - isArray, - isEmptyStr + isArray } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; import {getStorageManager} from '../src/storageManager.js'; -import { uspDataHandler } from '../src/adapterManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; /** @@ -38,16 +36,24 @@ const MISSING_CORE_CONSENT = 111; const GVLID = 95; const ID_HOST = 'id.crwdcntrl.net'; const ID_HOST_COOKIELESS = 'c.ltmsphrcl.net'; +const DO_NOT_HONOR_CONFIG = false; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); let cookieDomain; +let appliedConfig = { + name: 'lotamePanoramaId', + storage: { + type: 'cookie&html5', + name: 'panoramaId' + } +}; /** * Set the Lotame First Party Profile ID in the first party namespace * @param {String} profileId */ function setProfileId(profileId) { - if (storage.cookiesAreEnabled()) { + if (cookiesAreEnabled()) { let expirationDate = new Date(timestamp() + NINE_MONTHS_MS).toUTCString(); storage.setCookie( KEY_PROFILE, @@ -58,7 +64,7 @@ function setProfileId(profileId) { undefined ); } - if (storage.hasLocalStorage()) { + if (localStorageIsEnabled()) { storage.setDataInLocalStorage(KEY_PROFILE, profileId, undefined); } } @@ -68,10 +74,10 @@ function setProfileId(profileId) { */ function getProfileId() { let profileId; - if (storage.cookiesAreEnabled()) { + if (cookiesAreEnabled(DO_NOT_HONOR_CONFIG)) { profileId = storage.getCookie(KEY_PROFILE, undefined); } - if (!profileId && storage.hasLocalStorage()) { + if (!profileId && localStorageIsEnabled(DO_NOT_HONOR_CONFIG)) { profileId = storage.getDataFromLocalStorage(KEY_PROFILE, undefined); } return profileId; @@ -83,21 +89,11 @@ function getProfileId() { */ function getFromStorage(key) { let value = null; - if (storage.cookiesAreEnabled()) { + if (cookiesAreEnabled(DO_NOT_HONOR_CONFIG)) { value = storage.getCookie(key, undefined); } - if (storage.hasLocalStorage() && value === null) { - const storedValueExp = storage.getDataFromLocalStorage( - `${key}_exp`, undefined - ); - - if (storedValueExp === '' || storedValueExp === null) { - value = storage.getDataFromLocalStorage(key, undefined); - } else if (storedValueExp) { - if ((new Date(parseInt(storedValueExp, 10))).getTime() - Date.now() > 0) { - value = storage.getDataFromLocalStorage(key, undefined); - } - } + if (value === null && localStorageIsEnabled(DO_NOT_HONOR_CONFIG)) { + value = storage.getDataFromLocalStorage(key, undefined); } return value; } @@ -115,7 +111,7 @@ function saveLotameCache( ) { if (key && value) { let expirationDate = new Date(expirationTimestamp).toUTCString(); - if (storage.cookiesAreEnabled()) { + if (cookiesAreEnabled()) { storage.setCookie( key, value, @@ -125,12 +121,7 @@ function saveLotameCache( undefined ); } - if (storage.hasLocalStorage()) { - storage.setDataInLocalStorage( - `${key}_exp`, - String(expirationTimestamp), - undefined - ); + if (localStorageIsEnabled()) { storage.setDataInLocalStorage(key, value, undefined); } } @@ -172,7 +163,7 @@ function getLotameLocalCache(clientId = undefined) { */ function clearLotameCache(key) { if (key) { - if (storage.cookiesAreEnabled()) { + if (cookiesAreEnabled(DO_NOT_HONOR_CONFIG)) { let expirationDate = new Date(0).toUTCString(); storage.setCookie( key, @@ -183,11 +174,50 @@ function clearLotameCache(key) { undefined ); } - if (storage.hasLocalStorage()) { + if (localStorageIsEnabled(DO_NOT_HONOR_CONFIG)) { storage.removeDataFromLocalStorage(key, undefined); } } } +/** + * @param {boolean} honorConfig - false to override for reading or deleting old cookies + * @returns {boolean} for whether we can write the cookie + */ +function cookiesAreEnabled(honorConfig = true) { + if (honorConfig) { + return storage.cookiesAreEnabled() && appliedConfig.storage.type.includes('cookie'); + } + return storage.cookiesAreEnabled(); +} +/** + * @param {boolean} honorConfig - false to override for reading or deleting old stored items + * @returns {boolean} for whether we can write the cookie + */ +function localStorageIsEnabled(honorConfig = true) { + if (honorConfig) { + return storage.hasLocalStorage() && appliedConfig.storage.type.includes('html5'); + } + return storage.hasLocalStorage(); +} +/** + * @param {SubmoduleConfig} config + * @returns {null|string} - string error if it finds one, null otherwise. + */ +function checkConfigHasErrorsAndReport(config) { + let error = null; + if (typeof config.storage !== 'undefined') { + Object.assign(appliedConfig.storage, appliedConfig.storage, config.storage); + const READABLE_MODULE_NAME = 'Lotame ID module'; + const PERMITTED_STORAGE_TYPES = ['cookie', 'html5', 'cookie&html5']; + if (typeof config.storage.name !== 'undefined' && config.storage.name !== KEY_ID) { + logError(`Misconfigured ${READABLE_MODULE_NAME}, "storage.name" is expected to be "${KEY_ID}", actual is "${config.storage.name}"`); + error = true; + } else if (config.storage.type !== 'undefined' && !PERMITTED_STORAGE_TYPES.includes(config.storage.type)) { + logError(`Misconfigured ${READABLE_MODULE_NAME}, "storage.type" is expected to be one of "${PERMITTED_STORAGE_TYPES.join(', ')}", actual is "${config.storage.type}"`); + } + } + return error; +} /** @type {Submodule} */ export const lotamePanoramaIdSubmodule = { /** @@ -222,6 +252,9 @@ export const lotamePanoramaIdSubmodule = { * @returns {IdResponse|undefined} */ getId(config, consentData, cacheIdObj) { + if (checkConfigHasErrorsAndReport(config)) { + return; + } cookieDomain = lotamePanoramaIdSubmodule.findRootDomain(); const configParams = (config && config.params) || {}; const clientId = configParams.clientId; @@ -249,18 +282,6 @@ export const lotamePanoramaIdSubmodule = { const storedUserId = getProfileId(); - // Add CCPA Consent data handling - const usp = uspDataHandler.getConsentData(); - - let usPrivacy; - if (typeof usp !== 'undefined' && !isEmpty(usp) && !isEmptyStr(usp)) { - usPrivacy = usp; - } - if (!usPrivacy) { - // fallback to 1st party cookie - usPrivacy = getFromStorage('us_privacy'); - } - const getRequestHost = function() { if (navigator.userAgent && navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1) { return ID_HOST_COOKIELESS; @@ -281,22 +302,10 @@ export const lotamePanoramaIdSubmodule = { } consentString = consentData.consentString; } - // If no consent string, try to read it from 1st party cookies - if (!consentString) { - consentString = getFromStorage('eupubconsent-v2'); - } - if (!consentString) { - consentString = getFromStorage('euconsent-v2'); - } if (consentString) { queryParams.gdpr_consent = consentString; } - // Add usPrivacy to the url - if (usPrivacy) { - queryParams.us_privacy = usPrivacy; - } - // Add clientId to the url if (hasCustomClientId) { queryParams.c = clientId; diff --git a/modules/lotamePanoramaIdSystem.md b/modules/lotamePanoramaIdSystem.md index e960f4b5695..1fbc3f561c7 100644 --- a/modules/lotamePanoramaIdSystem.md +++ b/modules/lotamePanoramaIdSystem.md @@ -17,9 +17,32 @@ Retrieve the Lotame Panorama Id pbjs.setConfig({ usersync: { userIds: [ - { - name: 'lotamePanoramaId' // The only parameter that is needed - }], + { + name: 'lotamePanoramaId', + storage: { + name: 'panoramaId', + type: 'cookie&html5', + expires: 7 + } + } + ], } }); -``` \ No newline at end of file +``` + +| Parameters under `userSync.userIds[]` | Scope | Type | Description | Example | +| ---| --- | --- | --- | --- | +| name | Required | String | Name for the Lotame ID submodule | `"lotamePanoramaId"` | +| storage | Optional | Object | Configures how to cache User IDs locally in the browser | See [storage settings](#storage-settings) | + + +### Storage Settings + +The following settings are available for the `storage` property in the `userSync.userIds[]` object. Please note that inclusion of the `storage` property is optional, but if provided, all three attributes listed below *must* be specified: + +| Param name | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| name | Required | String| Name of the cookie or localStorage where the user ID will be stored; *must* be `"panoramaId"` | `"panoramaId"` | +| type | Required | String | `"cookie&html5"` (preferred) or `"cookie"` or `"html5"` | `"cookie&html5"` | +| expires | Required | Number | How long (in days) the user ID information will be stored. Lotame recommends `7`. | `7` | + diff --git a/test/spec/modules/lotamePanoramaIdSystem_spec.js b/test/spec/modules/lotamePanoramaIdSystem_spec.js index fbd4ba7c000..27efef9df50 100644 --- a/test/spec/modules/lotamePanoramaIdSystem_spec.js +++ b/test/spec/modules/lotamePanoramaIdSystem_spec.js @@ -2,7 +2,6 @@ import { lotamePanoramaIdSubmodule, storage, } from 'modules/lotamePanoramaIdSystem.js'; -import { uspDataHandler } from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; import { server } from 'test/mocks/xhr.js'; import sinon from 'sinon'; @@ -19,7 +18,6 @@ describe('LotameId', function() { let setLocalStorageStub; let removeFromLocalStorageStub; let timeStampStub; - let uspConsentDataStub; let requestHost; const nowTimestamp = new Date().getTime(); @@ -34,7 +32,6 @@ describe('LotameId', function() { 'removeDataFromLocalStorage' ); timeStampStub = sinon.stub(utils, 'timestamp').returns(nowTimestamp); - uspConsentDataStub = sinon.stub(uspDataHandler, 'getConsentData'); if (navigator.userAgent && navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1) { requestHost = 'https://c.ltmsphrcl.net/id'; } else { @@ -50,7 +47,6 @@ describe('LotameId', function() { setLocalStorageStub.restore(); removeFromLocalStorageStub.restore(); timeStampStub.restore(); - uspConsentDataStub.restore(); }); describe('caching initial data received from the remote server', function () { @@ -451,70 +447,6 @@ describe('LotameId', function() { }); }); - describe('when gdpr applies and falls back to eupubconsent cookie', function () { - let request; - let callBackSpy = sinon.spy(); - let consentData = { - gdprApplies: true, - consentString: undefined - }; - - beforeEach(function () { - getCookieStub - .withArgs('eupubconsent-v2') - .returns('consentGiven'); - - let submoduleCallback = lotamePanoramaIdSubmodule.getId({}, consentData).callback; - submoduleCallback(callBackSpy); - - // the contents of the response don't matter for this - request = server.requests[0]; - request.respond(200, responseHeader, ''); - }); - - it('should call the remote server when getId is called', function () { - expect(callBackSpy.calledOnce).to.be.true; - }); - - it('should pass the gdpr consent string back', function() { - expect(request.url).to.be.eq( - `${requestHost}?gdpr_applies=true&gdpr_consent=consentGiven` - ); - }); - }); - - describe('when gdpr applies and falls back to euconsent cookie', function () { - let request; - let callBackSpy = sinon.spy(); - let consentData = { - gdprApplies: true, - consentString: undefined - }; - - beforeEach(function () { - getCookieStub - .withArgs('euconsent-v2') - .returns('consentGiven'); - - let submoduleCallback = lotamePanoramaIdSubmodule.getId({}, consentData).callback; - submoduleCallback(callBackSpy); - - // the contents of the response don't matter for this - request = server.requests[0]; - request.respond(200, responseHeader, ''); - }); - - it('should call the remote server when getId is called', function () { - expect(callBackSpy.calledOnce).to.be.true; - }); - - it('should pass the gdpr consent string back', function() { - expect(request.url).to.be.eq( - `${requestHost}?gdpr_applies=true&gdpr_consent=consentGiven` - ); - }); - }); - describe('when gdpr applies but no consent string is available', function () { let request; let callBackSpy = sinon.spy(); @@ -543,64 +475,6 @@ describe('LotameId', function() { }); }); - describe('when no consentData and falls back to eupubconsent cookie', function () { - let request; - let callBackSpy = sinon.spy(); - let consentData; - - beforeEach(function () { - getCookieStub - .withArgs('eupubconsent-v2') - .returns('consentGiven'); - - let submoduleCallback = lotamePanoramaIdSubmodule.getId({}, consentData).callback; - submoduleCallback(callBackSpy); - - // the contents of the response don't matter for this - request = server.requests[0]; - request.respond(200, responseHeader, ''); - }); - - it('should call the remote server when getId is called', function () { - expect(callBackSpy.calledOnce).to.be.true; - }); - - it('should pass the gdpr consent string back', function() { - expect(request.url).to.be.eq( - `${requestHost}?gdpr_consent=consentGiven` - ); - }); - }); - - describe('when no consentData and falls back to euconsent cookie', function () { - let request; - let callBackSpy = sinon.spy(); - let consentData; - - beforeEach(function () { - getCookieStub - .withArgs('euconsent-v2') - .returns('consentGiven'); - - let submoduleCallback = lotamePanoramaIdSubmodule.getId({}, consentData).callback; - submoduleCallback(callBackSpy); - - // the contents of the response don't matter for this - request = server.requests[0]; - request.respond(200, responseHeader, ''); - }); - - it('should call the remote server when getId is called', function () { - expect(callBackSpy.calledOnce).to.be.true; - }); - - it('should pass the gdpr consent string back', function() { - expect(request.url).to.be.eq( - `${requestHost}?gdpr_consent=consentGiven` - ); - }); - }); - describe('when no consentData and no cookies', function () { let request; let callBackSpy = sinon.spy(); @@ -809,7 +683,6 @@ describe('LotameId', function() { let callBackSpy = sinon.spy(); beforeEach(function () { - uspConsentDataStub.returns('1NNN'); let submoduleCallback = lotamePanoramaIdSubmodule.getId( { params: { @@ -840,12 +713,6 @@ describe('LotameId', function() { expect(callBackSpy.calledOnce).to.be.true; }); - it('should pass the usp consent string and client id back', function () { - expect(request.url).to.be.eq( - `${requestHost}?gdpr_applies=false&us_privacy=1NNN&c=1234` - ); - }); - it('should NOT set an expiry for the client', function () { sinon.assert.neverCalledWith( setCookieStub,