diff --git a/modules/sharedIdSystem.js b/modules/sharedIdSystem.js index 35848f7bfbc..2e3abd6b1a2 100644 --- a/modules/sharedIdSystem.js +++ b/modules/sharedIdSystem.js @@ -7,170 +7,66 @@ import * as utils from '../src/utils.js'; import {submodule} from '../src/hook.js'; -import {ajax} from '../src/ajax.js'; -import { uspDataHandler, coppaDataHandler } from '../src/adapterManager.js'; +import { coppaDataHandler } from '../src/adapterManager.js'; import {getStorageManager} from '../src/storageManager.js'; const GVLID = 887; -export const storage = getStorageManager(GVLID, 'pubCommonId'); +const storage = getStorageManager(GVLID, 'pubCommonId'); const COOKIE = 'cookie'; const LOCAL_STORAGE = 'html5'; -const SHAREDID_OPT_OUT_VALUE = '00000000000000000000000000'; -const SHAREDID_URL = 'https://id.sharedid.org/id'; -const SHAREDID_SUFFIX = '_sharedid'; -const EXPIRED_COOKIE_DATE = 'Thu, 01 Jan 1970 00:00:01 GMT'; -const SHAREDID_DEFAULT_STATE = true; - +const OPTOUT_NAME = '_pubcid_optout'; const PUB_COMMON_ID = 'PublisherCommonId'; /** - * Store sharedid in either cookie or local storage - * @param {Object} config Need config.storage object to derive key, expiry time, and storage type. - * @param {string} value Shareid value to store + * Read a value either from cookie or local storage + * @param {string} name Name of the item + * @param {string} type storage type override + * @returns {string|null} a string if item exists */ - -function storeData(config, value) { - try { - if (value) { - const key = config.storage.name + SHAREDID_SUFFIX; - const expiresStr = (new Date(Date.now() + (config.storage.expires * (60 * 60 * 24 * 1000)))).toUTCString(); - - if (config.storage.type === COOKIE) { - if (storage.cookiesAreEnabled()) { - storage.setCookie(key, value, expiresStr, 'LAX', sharedIdSystemSubmodule.domainOverride()); - } - } else if (config.storage.type === LOCAL_STORAGE) { - if (storage.hasLocalStorage()) { - storage.setDataInLocalStorage(`${key}_exp`, expiresStr); - storage.setDataInLocalStorage(key, value); - } +function readValue(name, type) { + if (type === COOKIE) { + return storage.getCookie(name); + } else if (type === LOCAL_STORAGE) { + if (storage.hasLocalStorage()) { + const expValue = storage.getDataFromLocalStorage(`${name}_exp`); + if (!expValue) { + return storage.getDataFromLocalStorage(name); + } else if ((new Date(expValue)).getTime() - Date.now() > 0) { + return storage.getDataFromLocalStorage(name) } } - } catch (error) { - utils.logError(error); } } -/** - * Read sharedid from cookie or local storage - * @param config Need config.storage to derive key and storage type - * @return {string} - */ -function readData(config) { - try { - const key = config.storage.name + SHAREDID_SUFFIX; - if (config.storage.type === COOKIE) { - if (storage.cookiesAreEnabled()) { - return storage.getCookie(key); - } - } else if (config.storage.type === LOCAL_STORAGE) { - if (storage.hasLocalStorage()) { - const expValue = storage.getDataFromLocalStorage(`${key}_exp`); - if (!expValue) { - return storage.getDataFromLocalStorage(key); - } else if ((new Date(expValue)).getTime() - Date.now() > 0) { - return storage.getDataFromLocalStorage(key) - } - } +function getIdCallback(pubcid, pixelCallback) { + return function (callback) { + if (typeof pixelCallback === 'function') { + pixelCallback(); } - } catch (error) { - utils.logError(error); + callback(pubcid); } } -/** - * Delete sharedid from cookie or local storage - * @param config Need config.storage to derive key and storage type - */ -function delData(config) { - try { - const key = config.storage.name + SHAREDID_SUFFIX; - if (config.storage.type === COOKIE) { - if (storage.cookiesAreEnabled()) { - storage.setCookie(key, '', EXPIRED_COOKIE_DATE); - } - } else if (config.storage.type === LOCAL_STORAGE) { - storage.removeDataFromLocalStorage(`${key}_exp`); - storage.removeDataFromLocalStorage(key); - } - } catch (error) { - utils.logError(error); +function queuePixelCallback(pixelUrl, id = '', callback) { + if (!pixelUrl) { + return; } -} -/** - * setup success and error handler for sharedid callback thru ajax - * @param {string} pubcid Current pubcommon id - * @param {function} callback userId module callback. - * @param {Object} config Need config.storage to derive sharedid storage params - * @return {{success: success, error: error}} - */ + // Use pubcid as a cache buster + const urlInfo = utils.parseUrl(pixelUrl); + urlInfo.search.id = encodeURIComponent('pubcid:' + id); + const targetUrl = utils.buildUrl(urlInfo); -function handleResponse(pubcid, callback, config) { - return { - success: function (responseBody) { - if (responseBody) { - try { - let responseObj = JSON.parse(responseBody); - utils.logInfo('PubCommonId: Generated SharedId: ' + responseObj.sharedId); - if (responseObj.sharedId) { - if (responseObj.sharedId !== SHAREDID_OPT_OUT_VALUE) { - // Store sharedId locally - storeData(config, responseObj.sharedId); - } else { - // Delete local copy if the user has opted out - delData(config); - } - } - // Pass pubcid even though there is no change in order to trigger decode - callback(pubcid); - } catch (error) { - utils.logError(error); - } - } - }, - error: function (statusText, responseBody) { - utils.logInfo('PubCommonId: failed to get sharedid'); - } - } + return function () { + utils.triggerPixel(targetUrl); + }; } -/** - * Builds and returns the shared Id URL with attached consent data if applicable - * @param {Object} consentData - * @return {string} - */ -function sharedIdUrl(consentData) { - const usPrivacyString = uspDataHandler.getConsentData(); - let sharedIdUrl = SHAREDID_URL; - if (usPrivacyString && typeof usPrivacyString === 'string') { - sharedIdUrl = `${SHAREDID_URL}?us_privacy=${usPrivacyString}`; - } - if (!consentData || typeof consentData.gdprApplies !== 'boolean' || !consentData.gdprApplies) return sharedIdUrl; - if (usPrivacyString) { - sharedIdUrl = `${sharedIdUrl}&gdpr=1&gdpr_consent=${consentData.consentString}` - return sharedIdUrl; - } - sharedIdUrl = `${SHAREDID_URL}?gdpr=1&gdpr_consent=${consentData.consentString}`; - return sharedIdUrl +function hasOptedOut() { + return !!((storage.cookiesAreEnabled() && readValue(OPTOUT_NAME, COOKIE)) || + (storage.hasLocalStorage() && readValue(OPTOUT_NAME, LOCAL_STORAGE))); } -/** - * Wraps pixelCallback in order to call sharedid sync - * @param {string} pubcid Pubcommon id value - * @param {function|undefined} pixelCallback fires a pixel to first party server - * @param {Object} config Need config.storage to derive sharedid storage params. - * @return {function(...[*]=)} - */ - -function getIdCallback(pubcid, pixelCallback, config, consentData) { - return function (callback) { - if (typeof pixelCallback === 'function') { - pixelCallback(); - } - ajax(sharedIdUrl(consentData), handleResponse(pubcid, callback, config), undefined, {method: 'GET', withCredentials: true}); - } -} export const sharedIdSystemSubmodule = { /** * used to link submodule with config @@ -183,20 +79,7 @@ export const sharedIdSystemSubmodule = { * @type {Number} */ gvlid: GVLID, - makeCallback: function (pixelUrl, id = '') { - if (!pixelUrl) { - return; - } - // Use pubcid as a cache buster - const urlInfo = utils.parseUrl(pixelUrl); - urlInfo.search.id = encodeURIComponent('pubcid:' + id); - const targetUrl = utils.buildUrl(urlInfo); - - return function () { - utils.triggerPixel(targetUrl); - }; - }, /** * decode the stored id value for passing to bid requests * @function @@ -205,14 +88,12 @@ export const sharedIdSystemSubmodule = { * @returns {{pubcid:string}} */ decode(value, config) { - const idObj = {'pubcid': value}; - const {params: {enableSharedId = SHAREDID_DEFAULT_STATE} = {}} = config; - - if (enableSharedId) { - const sharedId = readData(config); - if (sharedId) idObj['sharedid'] = {id: sharedId}; + if (hasOptedOut()) { + utils.logInfo('PubCommonId decode: Has opted-out'); + return undefined; } - + utils.logInfo(' Decoded value PubCommonId ' + value); + const idObj = {'pubcid': value}; return idObj; }, /** @@ -224,12 +105,17 @@ export const sharedIdSystemSubmodule = { * @returns {IdResponse} */ getId: function (config = {}, consentData, storedId) { + if (hasOptedOut()) { + utils.logInfo('PubCommonId: Has opted-out'); + return; + } const coppa = coppaDataHandler.getCoppa(); + if (coppa) { utils.logInfo('PubCommonId: IDs not provided for coppa requests, exiting PubCommonId'); return; } - const {params: {create = true, pixelUrl, enableSharedId = SHAREDID_DEFAULT_STATE} = {}} = config; + const {params: {create = true, pixelUrl} = {}} = config; let newId = storedId; if (!newId) { try { @@ -243,10 +129,8 @@ export const sharedIdSystemSubmodule = { if (!newId) newId = (create && utils.hasDeviceAccess()) ? utils.generateUUID() : undefined; } - const pixelCallback = this.makeCallback(pixelUrl, newId); - const combinedCallback = enableSharedId ? getIdCallback(newId, pixelCallback, config, consentData) : pixelCallback; - - return {id: newId, callback: combinedCallback}; + const pixelCallback = queuePixelCallback(pixelUrl, newId); + return {id: newId, callback: getIdCallback(newId, pixelCallback)}; }, /** * performs action to extend an id. There are generally two ways to extend the expiration time @@ -259,8 +143,7 @@ export const sharedIdSystemSubmodule = { * having the script-side overwriting server-side. This applies to both pubcid and sharedid. * * On the other hand, if there is no pixelUrl, then the extendId should return storedId so that - * its expiration time is updated. Sharedid, however, will have to be updated by this submodule - * separately. + * its expiration time is updated. * * @function * @param {SubmoduleParams} [config] @@ -269,67 +152,25 @@ export const sharedIdSystemSubmodule = { * @returns {IdResponse|undefined} */ extendId: function(config = {}, consentData, storedId) { + if (hasOptedOut()) { + utils.logInfo('PubCommonId: Has opted-out'); + return {id: undefined}; + } const coppa = coppaDataHandler.getCoppa(); if (coppa) { utils.logInfo('PubCommonId: IDs not provided for coppa requests, exiting PubCommonId'); return; } - const {params: {extend = false, pixelUrl, enableSharedId = SHAREDID_DEFAULT_STATE} = {}} = config; + const {params: {extend = false, pixelUrl} = {}} = config; if (extend) { - try { - if (typeof window[PUB_COMMON_ID] === 'object') { - if (enableSharedId) { - // If the page includes its own pubcid module, then there is nothing to do - // except to update sharedid's expiration time - storeData(config, readData(config)); - } - return; - } - } catch (e) { - } - if (pixelUrl) { - const callback = this.makeCallback(pixelUrl, storedId); + const callback = queuePixelCallback(pixelUrl, storedId); return {callback: callback}; } else { - if (enableSharedId) { - // Update with the same value to extend expiration time - storeData(config, readData(config)); - } return {id: storedId}; } } - }, - - /** - * @param {string} domain - * @param {HTMLDocument} document - * @return {(string|undefined)} - */ - domainOverride: function () { - const domainElements = document.domain.split('.'); - const cookieName = `_gd${Date.now()}`; - for (let i = 0, topDomain, testCookie; i < domainElements.length; i++) { - const nextDomain = domainElements.slice(i).join('.'); - - // write test cookie - storage.setCookie(cookieName, '1', undefined, undefined, nextDomain); - - // read test cookie to verify domain was valid - testCookie = storage.getCookie(cookieName); - - // delete test cookie - storage.setCookie(cookieName, '', 'Thu, 01 Jan 1970 00:00:01 GMT', undefined, nextDomain); - - if (testCookie === '1') { - // cookie was written successfully using test domain so the topDomain is updated - topDomain = nextDomain; - } else { - // cookie failed to write using test domain so exit by returning the topDomain - return topDomain; - } - } } }; diff --git a/test/spec/modules/sharedIdSystem_spec.js b/test/spec/modules/sharedIdSystem_spec.js index 4c3de9cfec0..534d0b3f381 100644 --- a/test/spec/modules/sharedIdSystem_spec.js +++ b/test/spec/modules/sharedIdSystem_spec.js @@ -1,45 +1,34 @@ -import { - sharedIdSystemSubmodule, - storage -} from 'modules/sharedIdSystem.js'; -import { server } from 'test/mocks/xhr.js'; -import {uspDataHandler} from 'src/adapterManager'; +import {sharedIdSystemSubmodule, storage} from 'modules/sharedIdSystem.js'; +import {coppaDataHandler} from 'src/adapterManager'; + import sinon from 'sinon'; import * as utils from 'src/utils.js'; let expect = require('chai').expect; -describe('SharedId System', function() { +describe('SharedId System', function () { const UUID = '15fde1dc-1861-4894-afdf-b757272f3568'; - const START_TIME_MILLIS = 1234; - before(function() { + before(function () { sinon.stub(utils, 'generateUUID').returns(UUID); + sinon.stub(utils, 'logInfo'); }); - after(function() { + after(function () { utils.generateUUID.restore(); + utils.logInfo.restore(); }); - - describe('Xhr Requests from getId()', function() { - const SHAREDID_RESPONSE = {sharedId: 'testsharedid'}; + describe('SharedId System getId()', function () { const callbackSpy = sinon.spy(); - let uspConsentDataStub - let setCookeStub; + let coppaDataHandlerDataStub let sandbox; - beforeEach(function() { + beforeEach(function () { sandbox = sinon.sandbox.create(); - - uspConsentDataStub = sandbox.stub(uspDataHandler, 'getConsentData'); - setCookeStub = sandbox.stub(storage, 'setCookie'); - - sandbox.stub(storage, 'cookiesAreEnabled').returns(true); + coppaDataHandlerDataStub = sandbox.stub(coppaDataHandler, 'getCoppa'); sandbox.stub(utils, 'hasDeviceAccess').returns(true); - - sandbox.useFakeTimers(START_TIME_MILLIS); - + coppaDataHandlerDataStub.returns(''); callbackSpy.resetHistory(); }); @@ -47,7 +36,7 @@ describe('SharedId System', function() { sandbox.restore(); }); - it('should call shared id endpoint without consent data and handle a valid response', function () { + it('should call UUID', function () { let config = { storage: { type: 'cookie', @@ -58,63 +47,48 @@ describe('SharedId System', function() { let submoduleCallback = sharedIdSystemSubmodule.getId(config, undefined).callback; submoduleCallback(callbackSpy); - - let request = server.requests[0]; - expect(request.url).to.equal('https://id.sharedid.org/id'); - expect(request.withCredentials).to.be.true; - - request.respond(200, {}, JSON.stringify(SHAREDID_RESPONSE)); - expect(callbackSpy.calledOnce).to.be.true; expect(callbackSpy.lastCall.lastArg).to.equal(UUID); - - expect(setCookeStub.calledThrice).to.be.true; - - let testCookieName = `_gd${START_TIME_MILLIS}`; - expect(setCookeStub.firstCall.args).to.eql([testCookieName, '1', undefined, undefined, 'localhost']); - expect(setCookeStub.secondCall.args).to.eql([testCookieName, '', 'Thu, 01 Jan 1970 00:00:01 GMT', undefined, 'localhost']); - - let expires = new Date(START_TIME_MILLIS + (config.storage.expires * (60 * 60 * 24 * 1000))).toUTCString(); - expect(setCookeStub.lastCall.args).to.eql(['_pubcid_sharedid', 'testsharedid', expires, 'LAX', undefined]); }); - - it('should call shared id endpoint with consent data and handle a valid response', function () { - let consentData = { - gdprApplies: true, - consentString: 'abc12345234', - }; - - let submoduleCallback = sharedIdSystemSubmodule.getId(undefined, consentData).callback; - submoduleCallback(callbackSpy); - - let request = server.requests[0]; - expect(request.url).to.equal('https://id.sharedid.org/id?gdpr=1&gdpr_consent=abc12345234'); - expect(request.withCredentials).to.be.true; - - request.respond(200, {}, JSON.stringify(SHAREDID_RESPONSE)); - - expect(callbackSpy.calledOnce).to.be.true; - expect(callbackSpy.lastCall.lastArg).to.equal(UUID); + it('should log message if coppa is set', function () { + coppaDataHandlerDataStub.returns('true'); + sharedIdSystemSubmodule.getId({}); + expect(utils.logInfo.args[0][0]).to.exist.and.to.equal('PubCommonId: IDs not provided for coppa requests, exiting PubCommonId'); }); + }); + describe('SharedId System extendId()', function () { + const callbackSpy = sinon.spy(); + let coppaDataHandlerDataStub; + let sandbox; - it('should call shared id endpoint with usp consent data and handle a valid response', function () { - uspConsentDataStub.returns('1YYY'); - let consentData = { - gdprApplies: true, - consentString: 'abc12345234', + beforeEach(function () { + sandbox = sinon.sandbox.create(); + coppaDataHandlerDataStub = sandbox.stub(coppaDataHandler, 'getCoppa'); + sandbox.stub(utils, 'hasDeviceAccess').returns(true); + callbackSpy.resetHistory(); + coppaDataHandlerDataStub.returns(''); + }); + afterEach(function () { + sandbox.restore(); + }); + it('should call UUID', function () { + let config = { + params: { + extend: true + }, + storage: { + type: 'cookie', + name: '_pubcid', + expires: 10 + } }; - - let submoduleCallback = sharedIdSystemSubmodule.getId(undefined, consentData).callback; - submoduleCallback(callbackSpy); - - let request = server.requests[0]; - expect(request.url).to.equal('https://id.sharedid.org/id?us_privacy=1YYY&gdpr=1&gdpr_consent=abc12345234'); - expect(request.withCredentials).to.be.true; - - request.respond(200, {}, JSON.stringify(SHAREDID_RESPONSE)); - - expect(callbackSpy.calledOnce).to.be.true; - expect(callbackSpy.lastCall.lastArg).to.equal(UUID); + let pubcommId = sharedIdSystemSubmodule.extendId(config, undefined, 'TestId').id; + expect(pubcommId).to.equal('TestId'); + }); + it('should log message if coppa is set', function () { + coppaDataHandlerDataStub.returns('true'); + sharedIdSystemSubmodule.extendId({}, undefined, 'TestId'); + expect(utils.logInfo.args[0][0]).to.exist.and.to.equal('PubCommonId: IDs not provided for coppa requests, exiting PubCommonId'); }); }); });