From fa627d0645991153884f75efb24b203d08434a39 Mon Sep 17 00:00:00 2001 From: TJ Eastmond Date: Tue, 26 Nov 2019 06:42:10 -0500 Subject: [PATCH 01/22] CCPA additions --- modules/consentManagement.js | 204 ++++++++++++++++++-- src/adapterManager.js | 10 + test/spec/modules/consentManagement_spec.js | 120 +++++++++++- 3 files changed, 321 insertions(+), 13 deletions(-) diff --git a/modules/consentManagement.js b/modules/consentManagement.js index 1e2a6648145..07fed8c80f0 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -6,7 +6,7 @@ */ import * as utils from '../src/utils'; import { config } from '../src/config'; -import { gdprDataHandler } from '../src/adapterManager'; +import { gdprDataHandler, ccpaDataHandler } from '../src/adapterManager'; import includes from 'core-js/library/fn/array/includes'; import strIncludes from 'core-js/library/fn/string/includes'; @@ -22,9 +22,19 @@ export let staticConsentData; let consentData; let addedConsentHook = false; +// ccpa constants +export let userCCPA; +export let consentTimeoutCCPA; + +// ccpa globals +let consentDataCCPA; +let addedConsentHookCCPA = false; + // add new CMPs here, with their dedicated lookup function const cmpCallMap = { 'iab': lookupIabConsent, + 'gdpr': lookupIabConsent, + 'ccpa': lookupCcpaConsent, 'static': lookupStaticConsentData }; @@ -182,6 +192,138 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) { } } +function lookupCcpaConsent(ccpaSucess, cmpError, hookConfig) { + function handleCmpResponseCallbacks() { + const ccpaResponse = {}; + + function afterEach() { + if (ccpaResponse.consentString) { + ccpaSucess(ccpaResponse, hookConfig); + } + } + + return { + consentDataCallback: function (consentResponse) { + ccpaResponse.consentString = consentResponse.consentString; + afterEach(); + } + } + } + + let callbackHandler = handleCmpResponseCallbacks(); + let uspapiCallbacks = {}; + let ccpaFunction; + + // to collect the consent information from the user, we perform two calls to the CMP in parallel: + // first to collect the user's consent choices represented in an encoded string (via getConsentData) + // second to collect the user's full unparsed consent information (via getVendorConsents) + + // the following code also determines where the CMP is located and uses the proper workflow to communicate with it: + // check to see if CMP is found on the same window level as prebid and call it directly if so + // check to see if prebid is in a safeframe (with CMP support) + // else assume prebid may be inside an iframe and use the IAB CMP locator code to see if CMP's located in a higher parent window. this works in cross domain iframes + // if the CMP is not found, the iframe function will call the cmpError exit callback to abort the rest of the CMP workflow + try { + ccpaFunction = window.__uspapi || utils.getWindowTop().__uspapi; + } catch (e) { } + + if (utils.isFn(ccpaFunction)) { + ccpaFunction('getConsentData', null, callbackHandler.consentDataCallback); + } else if (inASafeFrame() && typeof window.$sf.ext.uspapi === 'function') { + callCcpaWhileInSafeFrame('getConsentData', callbackHandler.consentDataCallback); + } else { + // find the CMP frame + let f = window; + let ccpaFrame; + while (!ccpaFrame) { + try { + if (f.frames['__uspapiLocator']) ccpaFrame = f; + } catch (e) { } + if (f === window.top) break; + f = f.parent; + } + + if (!ccpaFrame) { + return cmpError('CCPA not found.', hookConfig); + } + + callCcpaWhileInIframe('getConsentData', ccpaFrame, callbackHandler.consentDataCallback); + } + + function inASafeFrame() { + return !!(window.$sf && window.$sf.ext); + } + + function callCcpaWhileInSafeFrame(commandName, callback) { + function sfCallback(msgName, data) { + if (msgName === 'cmpReturn') { + let responseObj = (commandName === 'getConsentData') ? data.vendorConsentData : data.vendorConsents; + callback(responseObj); + } + } + + // find sizes from adUnits object + let adUnits = hookConfig.adUnits; + let width = 1; + let height = 1; + if (Array.isArray(adUnits) && adUnits.length > 0) { + let sizes = utils.getAdUnitSizes(adUnits[0]); + width = sizes[0][0]; + height = sizes[0][1]; + } + + window.$sf.ext.register(width, height, sfCallback); + window.$sf.ext.ccpa(commandName); + } + + function callCcpaWhileInIframe(commandName, ccpaFrame, moduleCallback) { + /* Setup up a __ccpa function to do the postMessage and stash the callback. + This function behaves (from the caller's perspective identicially to the in-frame __ccpa call */ + window.__uspapi = function (cmd, ver, callback) { + let callId = Math.random() + ''; + let msg = { + __uspapiCall: { + command: cmd, + version: ver, + callId: callId + } + }; + uspapiCallbacks[callId] = callback; + ccpaFrame.postMessage(msg, '*'); + }; + + /** when we get the return message, call the stashed callback */ + window.addEventListener('message', readPostMessageResponse, false); + + // call ccpa + window.__uspapi(commandName, 1, ccpaIframeCallback); + + function readPostMessageResponse(event) { + let res = typeof event.data === 'string' ? JSON.parse(event.data) : event.data; + if (res.__uspapiReturn && res.__uspapiReturn.callId) { + let i = res.__uspapiReturn; + if (typeof uspapiCallbacks[i.callId] !== 'undefined') { + uspapiCallbacks[i.callId](i.returnValue, i.success); + delete uspapiCallbacks[i.callId]; + } + } + } + + function removePostMessageListener() { + window.removeEventListener('message', readPostMessageResponse, false); + } + + function ccpaIframeCallback(consentObject) { + removePostMessageListener(); + moduleCallback(consentObject); + } + } +} + +export function requestCcpaBidsHook(next, reqBidsConfigObj) { + requestBidsHook(next, reqBidsConfigObj, true); +} + /** * If consentManagement module is enabled (ie included in setConfig), this hook function will attempt to fetch the * user's encoded consent string from the supported CMP. Once obtained, the module will store this @@ -190,7 +332,15 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) { * @param {object} reqBidsConfigObj required; This is the same param that's used in pbjs.requestBids. * @param {function} fn required; The next function in the chain, used by hook.js */ -export function requestBidsHook(fn, reqBidsConfigObj) { +export function requestBidsHook(fn, reqBidsConfigObj, isCCPA = false) { + let userModule = userCMP; + let processFn = processCmpData; + + if (isCCPA) { + userModule = 'ccpa'; + processFn = processCcpaData; // @TJ + } + // preserves all module related variables for the current auction instance (used primiarily for concurrent auctions) const hookConfig = { context: this, @@ -199,7 +349,8 @@ export function requestBidsHook(fn, reqBidsConfigObj) { adUnits: reqBidsConfigObj.adUnits || $$PREBID_GLOBAL$$.adUnits, bidsBackHandler: reqBidsConfigObj.bidsBackHandler, haveExited: false, - timer: null + timer: null, + userModule: userModule }; // in case we already have consent (eg during bid refresh) @@ -207,17 +358,17 @@ export function requestBidsHook(fn, reqBidsConfigObj) { return exitModule(null, hookConfig); } - if (!includes(Object.keys(cmpCallMap), userCMP)) { - utils.logWarn(`CMP framework (${userCMP}) is not a supported framework. Aborting consentManagement module and resuming auction.`); + if (!includes(Object.keys(cmpCallMap), userModule)) { + utils.logWarn(`CMP framework (${userModule}) is not a supported framework. Aborting consentManagement module and resuming auction.`); return hookConfig.nextFn.apply(hookConfig.context, hookConfig.args); } - cmpCallMap[userCMP].call(this, processCmpData, cmpFailed, hookConfig); + cmpCallMap[userModule].call(this, processFn, cmpFailed, hookConfig); // only let this code run if module is still active (ie if the callbacks used by CMPs haven't already finished) if (!hookConfig.haveExited) { if (consentTimeout === 0) { - processCmpData(undefined, hookConfig); + processFn(undefined, hookConfig); } else { hookConfig.timer = setTimeout(cmpTimedOut.bind(null, hookConfig), consentTimeout); } @@ -251,6 +402,17 @@ function processCmpData(consentObject, hookConfig) { } } +function processCcpaData (consentObject, hookConfig) { + if (!(consentObject && consentObject.consentString)) { + cmpFailed(`CCPA returned unexpected value during lookup process.`, hookConfig, consentObject); + return; + } + + clearTimeout(hookConfig.timer); + storeCcpaConsentData(consentObject); + exitModule(null, hookConfig); +} + /** * General timeout callback when interacting with CMP takes too long. */ @@ -287,6 +449,13 @@ function storeConsentData(cmpConsentObject) { gdprDataHandler.setConsentData(consentData); } +function storeCcpaConsentData(consentObject) { + consentData = { + consentString: consentObject ? consentObject.consentString : undefined + }; + ccpaDataHandler.setConsentData(consentData); +} + /** * This function handles the exit logic for the module. * There are several paths in the module's logic to call this function and we only allow 1 of the 3 potential exits to happen before suppressing others. @@ -336,13 +505,14 @@ function exitModule(errMsg, hookConfig, extraArgs) { export function resetConsentData() { consentData = undefined; gdprDataHandler.setConsentData(null); + ccpaDataHandler.setConsentData(null); } /** * A configuration function that initializes some module variables, as well as add a hook into the requestBids function * @param {object} config required; consentManagement module config settings; cmp (string), timeout (int), allowAuctionWithoutConsent (boolean) */ -export function setConsentConfig(config) { +export function setConsentConfig(config, consentModule) { if (utils.isStr(config.cmpApi)) { userCMP = config.cmpApi; } else { @@ -374,9 +544,21 @@ export function setConsentConfig(config) { utils.logError(`consentManagement config with cmpApi: 'static' did not specify consentData. No consents will be available to adapters.`); } } - if (!addedConsentHook) { + + if (consentModule === 'ccpa' && !addedConsentHookCCPA) { + $$PREBID_GLOBAL$$.requestBids.before(requestCcpaBidsHook, 50); + addedConsentHookCCPA = true; + } + + if (!addedConsentHook && consentModule !== 'ccpa') { $$PREBID_GLOBAL$$.requestBids.before(requestBidsHook, 50); + addedConsentHook = true; } - addedConsentHook = true; } -config.getConfig('consentManagement', config => setConsentConfig(config.consentManagement)); + +config.getConfig('consentManagement', config => { + const consentManagement = { ...config.consentManagement }; + const consentChecks = consentManagement.consentAPIs ? new Set([...consentManagement.consentAPIs]) : new Set([]); + if (utils.isStr(config.cmpApi)) consentChecks.add('iab'); + [...consentChecks].map(module => setConsentConfig(consentManagement, module)); +}); diff --git a/src/adapterManager.js b/src/adapterManager.js index fa7b7cbb58d..a34e267bd25 100644 --- a/src/adapterManager.js +++ b/src/adapterManager.js @@ -160,6 +160,16 @@ export let gdprDataHandler = { } }; +export let ccpaDataHandler = { + consentData: null, + setConsentData: function(consentInfo) { + ccpaDataHandler.consentData = consentInfo; + }, + getConsentData: function() { + return ccpaDataHandler.consentData; + } +}; + adapterManager.makeBidRequests = function(adUnits, auctionStart, auctionId, cbTimeout, labels) { let bidRequests = []; diff --git a/test/spec/modules/consentManagement_spec.js b/test/spec/modules/consentManagement_spec.js index 6be96427750..d1d0325f2fb 100644 --- a/test/spec/modules/consentManagement_spec.js +++ b/test/spec/modules/consentManagement_spec.js @@ -1,5 +1,13 @@ -import {setConsentConfig, requestBidsHook, resetConsentData, userCMP, consentTimeout, allowAuction, staticConsentData} from 'modules/consentManagement'; -import {gdprDataHandler} from 'src/adapterManager'; +import { + setConsentConfig, + requestBidsHook, + resetConsentData, + userCMP, + consentTimeout, + allowAuction, + staticConsentData, +} from 'modules/consentManagement'; +import {gdprDataHandler, ccpaDataHandler} from 'src/adapterManager'; import * as utils from 'src/utils'; import { config } from 'src/config'; @@ -43,6 +51,17 @@ describe('consentManagement', function () { expect(userCMP).to.be.equal('iab'); expect(consentTimeout).to.be.equal(7500); expect(allowAuction).to.be.false; + + setConsentConfig({ + 'allowAuction': true + }); + }); + }); + + describe('valid setConfigConsent value for CCPA', function() { + afterEach(function () { + config.resetConfig(); + $$PREBID_GLOBAL$$.requestBids.removeAll(); }); }); @@ -697,6 +716,77 @@ describe('consentManagement', function () { } }); + describe('CCPA workflow for iframed page', function () { + let ifr = null; + let stringifyResponse = false; + + beforeEach(function () { + sinon.stub(utils, 'logError'); + sinon.stub(utils, 'logWarn'); + ifr = createIFrameMarker(); + window.addEventListener('message', ccpaMessageHandler, false); + }); + + afterEach(function () { + config.resetConfig(); + $$PREBID_GLOBAL$$.requestBids.removeAll(); + delete window.__cmp; + utils.logError.restore(); + utils.logWarn.restore(); + resetConsentData(); + document.body.removeChild(ifr); + window.removeEventListener('message', ccpaMessageHandler); + }); + + function createIFrameMarker() { + var ifr = document.createElement('iframe'); + ifr.width = 0; + ifr.height = 0; + ifr.name = '__uspapiLocator'; + document.body.appendChild(ifr); + return ifr; + } + + function ccpaMessageHandler(event) { + if (event && event.data) { + var data = event.data; + if (data.__uspapiCall) { + var callId = data.__uspapiCall.callId; + var response = { + __uspapiReturn: { + callId, + returnValue: { consentString: '1NN' }, + success: true + } + }; + event.source.postMessage(stringifyResponse ? JSON.stringify(response) : response, '*'); + } + } + } + + let _goodConfigWithAllowAuction = { ...goodConfigWithAllowAuction }; + _goodConfigWithAllowAuction.consentAPIs = ['ccpa']; + + // Run tests with JSON response and String response + // from CMP window postMessage listener. + testIFramedPage('with/JSON response', false); + // testIFramedPage('with/String response', true); + + function testIFramedPage(testName, messageFormatString) { + it(`should return the consent string from a postmessage + addEventListener response - ${testName}`, (done) => { + stringifyResponse = messageFormatString; + setConsentConfig(_goodConfigWithAllowAuction, 'ccpa'); + requestBidsHook(() => { + let consent = ccpaDataHandler.getConsentData(); + sinon.assert.notCalled(utils.logWarn); + sinon.assert.notCalled(utils.logError); + expect(consent.consentString).to.equal('1NN'); + done(); + }, {}, true); + }); + } + }); + describe('CMP workflow for normal pages:', function () { let cmpStub = sinon.stub(); @@ -705,6 +795,7 @@ describe('consentManagement', function () { sinon.stub(utils, 'logError'); sinon.stub(utils, 'logWarn'); window.__cmp = function() {}; + window.__uspapi = function() {}; }); afterEach(function () { @@ -714,6 +805,7 @@ describe('consentManagement', function () { utils.logError.restore(); utils.logWarn.restore(); delete window.__cmp; + delete window.__uspapi; resetConsentData(); }); @@ -740,6 +832,30 @@ describe('consentManagement', function () { expect(consent.gdprApplies).to.be.true; }); + it('CCPA: performs lookup check and stores consentData for a valid existing user', function () { + let testConsentData = { + consentString: '1YY' + }; + cmpStub = sinon.stub(window, '__uspapi').callsFake((...args) => { + args[2](testConsentData); + }); + + let _goodConfigWithAllowAuction = { ...goodConfigWithAllowAuction }; + _goodConfigWithAllowAuction.consentAPIs = ['ccpa']; + + setConsentConfig(_goodConfigWithAllowAuction, 'ccpa'); + + requestBidsHook(() => { + didHookReturn = true; + }, {}, true); + let consent = ccpaDataHandler.getConsentData(); + + sinon.assert.notCalled(utils.logWarn); + sinon.assert.notCalled(utils.logError); + expect(didHookReturn).to.be.true; + expect(consent.consentString).to.equal(testConsentData.consentString); + }); + it('throws an error when processCmpData check failed while config had allowAuction set to false', function () { let testConsentData = {}; let bidsBackHandlerReturn = false; From b02dacfb5d9a82ec5f1b723bca1caf625902ff5e Mon Sep 17 00:00:00 2001 From: TJ Eastmond Date: Tue, 26 Nov 2019 11:55:59 -0500 Subject: [PATCH 02/22] Change config value from CCPA to USP and USP timer --- modules/consentManagement.js | 44 ++++++++++++--------- test/spec/modules/consentManagement_spec.js | 30 ++++++-------- 2 files changed, 38 insertions(+), 36 deletions(-) diff --git a/modules/consentManagement.js b/modules/consentManagement.js index 07fed8c80f0..41fef8bca23 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -12,6 +12,7 @@ import strIncludes from 'core-js/library/fn/string/includes'; const DEFAULT_CMP = 'iab'; const DEFAULT_CONSENT_TIMEOUT = 10000; +const DEFAULT_CONSENT_TIMEOUT_USP = 50; const DEFAULT_ALLOW_AUCTION_WO_CONSENT = true; export let userCMP; @@ -22,19 +23,16 @@ export let staticConsentData; let consentData; let addedConsentHook = false; -// ccpa constants -export let userCCPA; -export let consentTimeoutCCPA; - -// ccpa globals -let consentDataCCPA; -let addedConsentHookCCPA = false; +// usp +export let consentTimeoutUSP; +let addedConsentHookUSP = false; // add new CMPs here, with their dedicated lookup function const cmpCallMap = { 'iab': lookupIabConsent, 'gdpr': lookupIabConsent, - 'ccpa': lookupCcpaConsent, + 'ccpa': lookupUspConsent, + 'usp': lookupUspConsent, 'static': lookupStaticConsentData }; @@ -192,7 +190,7 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) { } } -function lookupCcpaConsent(ccpaSucess, cmpError, hookConfig) { +function lookupUspConsent(ccpaSucess, cmpError, hookConfig) { function handleCmpResponseCallbacks() { const ccpaResponse = {}; @@ -332,13 +330,13 @@ export function requestCcpaBidsHook(next, reqBidsConfigObj) { * @param {object} reqBidsConfigObj required; This is the same param that's used in pbjs.requestBids. * @param {function} fn required; The next function in the chain, used by hook.js */ -export function requestBidsHook(fn, reqBidsConfigObj, isCCPA = false) { +export function requestBidsHook(fn, reqBidsConfigObj, isUSP = false) { let userModule = userCMP; let processFn = processCmpData; - if (isCCPA) { - userModule = 'ccpa'; - processFn = processCcpaData; // @TJ + if (isUSP) { + userModule = 'usp'; + processFn = processCcpaData; } // preserves all module related variables for the current auction instance (used primiarily for concurrent auctions) @@ -367,10 +365,11 @@ export function requestBidsHook(fn, reqBidsConfigObj, isCCPA = false) { // only let this code run if module is still active (ie if the callbacks used by CMPs haven't already finished) if (!hookConfig.haveExited) { - if (consentTimeout === 0) { + let timeout = userModule === 'usp' ? consentTimeoutUSP : consentTimeout; + if (timeout === 0) { processFn(undefined, hookConfig); } else { - hookConfig.timer = setTimeout(cmpTimedOut.bind(null, hookConfig), consentTimeout); + hookConfig.timer = setTimeout(cmpTimedOut.bind(null, hookConfig), timeout); } } } @@ -520,13 +519,20 @@ export function setConsentConfig(config, consentModule) { utils.logInfo(`consentManagement config did not specify cmp. Using system default setting (${DEFAULT_CMP}).`); } - if (utils.isNumber(config.timeout)) { + if (utils.isNumber(config.timeout)) { consentTimeout = config.timeout; } else { consentTimeout = DEFAULT_CONSENT_TIMEOUT; utils.logInfo(`consentManagement config did not specify timeout. Using system default setting (${DEFAULT_CONSENT_TIMEOUT}).`); } + if (utils.isNumber(config.uspTimeout)) { + consentTimeoutUSP = config.uspTimeout + } else { + consentTimeoutUSP = DEFAULT_CONSENT_TIMEOUT_USP; + utils.logInfo(`consentManagement config did not specify timeout f or USP. Using system default setting: (${DEFAULT_CONSENT_TIMEOUT_USP}).`); + } + if (typeof config.allowAuctionWithoutConsent === 'boolean') { allowAuction = config.allowAuctionWithoutConsent; } else { @@ -545,12 +551,12 @@ export function setConsentConfig(config, consentModule) { } } - if (consentModule === 'ccpa' && !addedConsentHookCCPA) { + if (consentModule === 'usp' && !addedConsentHookUSP) { $$PREBID_GLOBAL$$.requestBids.before(requestCcpaBidsHook, 50); - addedConsentHookCCPA = true; + addedConsentHookUSP = true; } - if (!addedConsentHook && consentModule !== 'ccpa') { + if (!addedConsentHook && consentModule !== 'usp') { $$PREBID_GLOBAL$$.requestBids.before(requestBidsHook, 50); addedConsentHook = true; } diff --git a/test/spec/modules/consentManagement_spec.js b/test/spec/modules/consentManagement_spec.js index d1d0325f2fb..feba170c2f8 100644 --- a/test/spec/modules/consentManagement_spec.js +++ b/test/spec/modules/consentManagement_spec.js @@ -6,6 +6,7 @@ import { consentTimeout, allowAuction, staticConsentData, + consentTimeoutUSP } from 'modules/consentManagement'; import {gdprDataHandler, ccpaDataHandler} from 'src/adapterManager'; import * as utils from 'src/utils'; @@ -14,7 +15,7 @@ import { config } from 'src/config'; let assert = require('chai').assert; let expect = require('chai').expect; -describe('consentManagement', function () { +describe.only('consentManagement', function () { describe('setConsentConfig tests:', function () { describe('empty setConsentConfig value', function () { beforeEach(function () { @@ -31,7 +32,13 @@ describe('consentManagement', function () { expect(userCMP).to.be.equal('iab'); expect(consentTimeout).to.be.equal(10000); expect(allowAuction).to.be.true; - sinon.assert.callCount(utils.logInfo, 4); + sinon.assert.callCount(utils.logInfo, 5); + }); + + it('should use system default values for USP', function () { + setConsentConfig({}, 'usp'); + expect(consentTimeoutUSP).to.be.equal(50); + sinon.assert.callCount(utils.logInfo, 5); }); }); @@ -51,17 +58,6 @@ describe('consentManagement', function () { expect(userCMP).to.be.equal('iab'); expect(consentTimeout).to.be.equal(7500); expect(allowAuction).to.be.false; - - setConsentConfig({ - 'allowAuction': true - }); - }); - }); - - describe('valid setConfigConsent value for CCPA', function() { - afterEach(function () { - config.resetConfig(); - $$PREBID_GLOBAL$$.requestBids.removeAll(); }); }); @@ -765,7 +761,7 @@ describe('consentManagement', function () { } let _goodConfigWithAllowAuction = { ...goodConfigWithAllowAuction }; - _goodConfigWithAllowAuction.consentAPIs = ['ccpa']; + _goodConfigWithAllowAuction.consentAPIs = ['usp']; // Run tests with JSON response and String response // from CMP window postMessage listener. @@ -775,7 +771,7 @@ describe('consentManagement', function () { function testIFramedPage(testName, messageFormatString) { it(`should return the consent string from a postmessage + addEventListener response - ${testName}`, (done) => { stringifyResponse = messageFormatString; - setConsentConfig(_goodConfigWithAllowAuction, 'ccpa'); + setConsentConfig(_goodConfigWithAllowAuction, 'usp'); requestBidsHook(() => { let consent = ccpaDataHandler.getConsentData(); sinon.assert.notCalled(utils.logWarn); @@ -841,9 +837,9 @@ describe('consentManagement', function () { }); let _goodConfigWithAllowAuction = { ...goodConfigWithAllowAuction }; - _goodConfigWithAllowAuction.consentAPIs = ['ccpa']; + _goodConfigWithAllowAuction.consentAPIs = ['usp']; - setConsentConfig(_goodConfigWithAllowAuction, 'ccpa'); + setConsentConfig(_goodConfigWithAllowAuction, 'usp'); requestBidsHook(() => { didHookReturn = true; From 8305e1d4c442dbb942b34fd97f88854fd77c69fc Mon Sep 17 00:00:00 2001 From: TJ Eastmond Date: Tue, 26 Nov 2019 18:43:27 -0500 Subject: [PATCH 03/22] Require use of consentAPIs array --- modules/consentManagement.js | 23 +++++++----- test/spec/modules/consentManagement_spec.js | 40 +++++++++++---------- 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/modules/consentManagement.js b/modules/consentManagement.js index 41fef8bca23..067a72e9ae5 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -10,7 +10,6 @@ import { gdprDataHandler, ccpaDataHandler } from '../src/adapterManager'; import includes from 'core-js/library/fn/array/includes'; import strIncludes from 'core-js/library/fn/string/includes'; -const DEFAULT_CMP = 'iab'; const DEFAULT_CONSENT_TIMEOUT = 10000; const DEFAULT_CONSENT_TIMEOUT_USP = 50; const DEFAULT_ALLOW_AUCTION_WO_CONSENT = true; @@ -512,14 +511,21 @@ export function resetConsentData() { * @param {object} config required; consentManagement module config settings; cmp (string), timeout (int), allowAuctionWithoutConsent (boolean) */ export function setConsentConfig(config, consentModule) { - if (utils.isStr(config.cmpApi)) { - userCMP = config.cmpApi; - } else { - userCMP = DEFAULT_CMP; - utils.logInfo(`consentManagement config did not specify cmp. Using system default setting (${DEFAULT_CMP}).`); + if (consentModule) { + if (['gdpr', 'iab'].includes(consentModule)) { + userCMP = 'iab'; + } + + if (consentModule === 'static') { + userCMP = 'static'; + } + + if (!['gdpr', 'iab', 'static', 'usp'].includes(consentModule) && consentModule) { + userCMP = consentModule; + } } - if (utils.isNumber(config.timeout)) { + if (utils.isNumber(config.timeout)) { consentTimeout = config.timeout; } else { consentTimeout = DEFAULT_CONSENT_TIMEOUT; @@ -556,7 +562,7 @@ export function setConsentConfig(config, consentModule) { addedConsentHookUSP = true; } - if (!addedConsentHook && consentModule !== 'usp') { + if (!addedConsentHook && consentModule === 'gdpr') { $$PREBID_GLOBAL$$.requestBids.before(requestBidsHook, 50); addedConsentHook = true; } @@ -565,6 +571,5 @@ export function setConsentConfig(config, consentModule) { config.getConfig('consentManagement', config => { const consentManagement = { ...config.consentManagement }; const consentChecks = consentManagement.consentAPIs ? new Set([...consentManagement.consentAPIs]) : new Set([]); - if (utils.isStr(config.cmpApi)) consentChecks.add('iab'); [...consentChecks].map(module => setConsentConfig(consentManagement, module)); }); diff --git a/test/spec/modules/consentManagement_spec.js b/test/spec/modules/consentManagement_spec.js index feba170c2f8..bbe5219c249 100644 --- a/test/spec/modules/consentManagement_spec.js +++ b/test/spec/modules/consentManagement_spec.js @@ -28,17 +28,17 @@ describe.only('consentManagement', function () { }); it('should use system default values', function () { - setConsentConfig({}); + setConsentConfig({}, 'gdpr'); expect(userCMP).to.be.equal('iab'); expect(consentTimeout).to.be.equal(10000); expect(allowAuction).to.be.true; - sinon.assert.callCount(utils.logInfo, 5); + sinon.assert.callCount(utils.logInfo, 4); }); it('should use system default values for USP', function () { setConsentConfig({}, 'usp'); expect(consentTimeoutUSP).to.be.equal(50); - sinon.assert.callCount(utils.logInfo, 5); + sinon.assert.callCount(utils.logInfo, 4); }); }); @@ -51,7 +51,8 @@ describe.only('consentManagement', function () { let allConfig = { cmpApi: 'iab', timeout: 7500, - allowAuctionWithoutConsent: false + allowAuctionWithoutConsent: false, + consentAPIs: ['gdpr'] }; setConsentConfig(allConfig); @@ -71,6 +72,7 @@ describe.only('consentManagement', function () { cmpApi: 'static', timeout: 7500, allowAuctionWithoutConsent: false, + consentAPIs: ['static'], consentData: { getConsentData: { 'gdprApplies': true, @@ -462,7 +464,7 @@ describe.only('consentManagement', function () { } }; - setConsentConfig(staticConfig); + setConsentConfig(staticConfig, 'static'); expect(userCMP).to.be.equal('static'); expect(consentTimeout).to.be.equal(0); // should always return without a timeout when config is used expect(allowAuction).to.be.false; @@ -475,13 +477,15 @@ describe.only('consentManagement', function () { let goodConfigWithCancelAuction = { cmpApi: 'iab', timeout: 7500, - allowAuctionWithoutConsent: false + allowAuctionWithoutConsent: false, + consentAPIs: ['gdpr'] }; let goodConfigWithAllowAuction = { cmpApi: 'iab', timeout: 7500, - allowAuctionWithoutConsent: true + allowAuctionWithoutConsent: true, + consentAPIs: ['gdpr'] }; let didHookReturn; @@ -510,7 +514,7 @@ describe.only('consentManagement', function () { let badCMPConfig = { cmpApi: 'bad' }; - setConsentConfig(badCMPConfig); + setConsentConfig(badCMPConfig, 'bad'); expect(userCMP).to.be.equal(badCMPConfig.cmpApi); requestBidsHook(() => { @@ -523,11 +527,11 @@ describe.only('consentManagement', function () { }); it('should throw proper errors when CMP is not found', function () { - setConsentConfig(goodConfigWithCancelAuction); + setConsentConfig(goodConfigWithCancelAuction, 'gdpr'); requestBidsHook(() => { didHookReturn = true; - }, {}); + }, {}, 'gdpr'); let consent = gdprDataHandler.getConsentData(); // throw 2 errors; one for no bidsBackHandler and for CMP not being found (this is an error due to gdpr config) sinon.assert.calledTwice(utils.logError); @@ -561,7 +565,7 @@ describe.only('consentManagement', function () { cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => { args[2](testConsentData); }); - setConsentConfig(goodConfigWithAllowAuction); + setConsentConfig(goodConfigWithAllowAuction, 'gdpr'); requestBidsHook(() => {}, {}); cmpStub.restore(); @@ -625,7 +629,7 @@ describe.only('consentManagement', function () { args[2](testConsentData.data.msgName, testConsentData.data); }); - setConsentConfig(goodConfigWithAllowAuction); + setConsentConfig(goodConfigWithAllowAuction, 'gdpr'); requestBidsHook(() => { didHookReturn = true; }, {adUnits: [{ sizes: [[300, 250]] }]}); @@ -699,7 +703,7 @@ describe.only('consentManagement', function () { function testIFramedPage(testName, messageFormatString) { it(`should return the consent string from a postmessage + addEventListener response - ${testName}`, (done) => { stringifyResponse = messageFormatString; - setConsentConfig(goodConfigWithAllowAuction); + setConsentConfig(goodConfigWithAllowAuction, 'gdpr'); requestBidsHook(() => { let consent = gdprDataHandler.getConsentData(); sinon.assert.notCalled(utils.logWarn); @@ -712,7 +716,7 @@ describe.only('consentManagement', function () { } }); - describe('CCPA workflow for iframed page', function () { + describe('USP workflow for iframed page', function () { let ifr = null; let stringifyResponse = false; @@ -814,7 +818,7 @@ describe.only('consentManagement', function () { args[2](testConsentData); }); - setConsentConfig(goodConfigWithAllowAuction); + setConsentConfig(goodConfigWithAllowAuction, 'gdpr'); requestBidsHook(() => { didHookReturn = true; @@ -828,7 +832,7 @@ describe.only('consentManagement', function () { expect(consent.gdprApplies).to.be.true; }); - it('CCPA: performs lookup check and stores consentData for a valid existing user', function () { + it('USP: performs lookup check and stores consentData for a valid existing user', function () { let testConsentData = { consentString: '1YY' }; @@ -860,7 +864,7 @@ describe.only('consentManagement', function () { args[2](testConsentData); }); - setConsentConfig(goodConfigWithCancelAuction); + setConsentConfig(goodConfigWithCancelAuction, 'gdpr'); requestBidsHook(() => { didHookReturn = true; @@ -880,7 +884,7 @@ describe.only('consentManagement', function () { args[2](testConsentData); }); - setConsentConfig(goodConfigWithAllowAuction); + setConsentConfig(goodConfigWithAllowAuction, 'gdpr'); requestBidsHook(() => { didHookReturn = true; From b5d918e4384375703c09bbbcb19acdf0fb3f2e9a Mon Sep 17 00:00:00 2001 From: TJ Eastmond Date: Tue, 26 Nov 2019 19:43:37 -0500 Subject: [PATCH 04/22] Change requests --- modules/consentManagement.js | 88 +++++++-------------- src/adapterManager.js | 6 +- test/spec/modules/consentManagement_spec.js | 14 ++-- 3 files changed, 38 insertions(+), 70 deletions(-) diff --git a/modules/consentManagement.js b/modules/consentManagement.js index 067a72e9ae5..c37ca29c3fa 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -6,7 +6,7 @@ */ import * as utils from '../src/utils'; import { config } from '../src/config'; -import { gdprDataHandler, ccpaDataHandler } from '../src/adapterManager'; +import { gdprDataHandler, uspDataHandler } from '../src/adapterManager'; import includes from 'core-js/library/fn/array/includes'; import strIncludes from 'core-js/library/fn/string/includes'; @@ -189,19 +189,19 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) { } } -function lookupUspConsent(ccpaSucess, cmpError, hookConfig) { +function lookupUspConsent(uspSucess, cmpError, hookConfig) { function handleCmpResponseCallbacks() { - const ccpaResponse = {}; + const uspResponse = {}; function afterEach() { - if (ccpaResponse.consentString) { - ccpaSucess(ccpaResponse, hookConfig); + if (uspResponse.usPrivacy) { + uspSucess(uspResponse, hookConfig); } } return { consentDataCallback: function (consentResponse) { - ccpaResponse.consentString = consentResponse.consentString; + uspResponse.usPrivacy = consentResponse.usPrivacy; afterEach(); } } @@ -211,69 +211,37 @@ function lookupUspConsent(ccpaSucess, cmpError, hookConfig) { let uspapiCallbacks = {}; let ccpaFunction; - // to collect the consent information from the user, we perform two calls to the CMP in parallel: - // first to collect the user's consent choices represented in an encoded string (via getConsentData) - // second to collect the user's full unparsed consent information (via getVendorConsents) - - // the following code also determines where the CMP is located and uses the proper workflow to communicate with it: - // check to see if CMP is found on the same window level as prebid and call it directly if so - // check to see if prebid is in a safeframe (with CMP support) - // else assume prebid may be inside an iframe and use the IAB CMP locator code to see if CMP's located in a higher parent window. this works in cross domain iframes - // if the CMP is not found, the iframe function will call the cmpError exit callback to abort the rest of the CMP workflow + // the following code also determines where the USP is located and uses the proper workflow to communicate with it: + // check to see if USP is found on the same window level as prebid and call it directly if so + // check to see if prebid is in a safeframe (with USP support) + // else assume prebid may be inside an iframe and use the IAB USP locator code to see if USP's located in a higher parent window. this works in cross domain iframes + // if the USP is not found, the iframe function will call the cmpError exit callback to abort the rest of the USP workflow try { ccpaFunction = window.__uspapi || utils.getWindowTop().__uspapi; } catch (e) { } if (utils.isFn(ccpaFunction)) { - ccpaFunction('getConsentData', null, callbackHandler.consentDataCallback); - } else if (inASafeFrame() && typeof window.$sf.ext.uspapi === 'function') { - callCcpaWhileInSafeFrame('getConsentData', callbackHandler.consentDataCallback); + ccpaFunction('getUSPData', 1, callbackHandler.consentDataCallback); } else { // find the CMP frame let f = window; - let ccpaFrame; - while (!ccpaFrame) { + let uspFrame; + while (!uspFrame) { try { - if (f.frames['__uspapiLocator']) ccpaFrame = f; + if (f.frames['__uspapiLocator']) uspFrame = f; } catch (e) { } if (f === window.top) break; f = f.parent; } - if (!ccpaFrame) { - return cmpError('CCPA not found.', hookConfig); + if (!uspFrame) { + return cmpError('USP not found.', hookConfig); } - callCcpaWhileInIframe('getConsentData', ccpaFrame, callbackHandler.consentDataCallback); - } - - function inASafeFrame() { - return !!(window.$sf && window.$sf.ext); - } - - function callCcpaWhileInSafeFrame(commandName, callback) { - function sfCallback(msgName, data) { - if (msgName === 'cmpReturn') { - let responseObj = (commandName === 'getConsentData') ? data.vendorConsentData : data.vendorConsents; - callback(responseObj); - } - } - - // find sizes from adUnits object - let adUnits = hookConfig.adUnits; - let width = 1; - let height = 1; - if (Array.isArray(adUnits) && adUnits.length > 0) { - let sizes = utils.getAdUnitSizes(adUnits[0]); - width = sizes[0][0]; - height = sizes[0][1]; - } - - window.$sf.ext.register(width, height, sfCallback); - window.$sf.ext.ccpa(commandName); + callCcpaWhileInIframe('getUSPData', uspFrame, callbackHandler.consentDataCallback); } - function callCcpaWhileInIframe(commandName, ccpaFrame, moduleCallback) { + function callCcpaWhileInIframe(commandName, uspFrame, moduleCallback) { /* Setup up a __ccpa function to do the postMessage and stash the callback. This function behaves (from the caller's perspective identicially to the in-frame __ccpa call */ window.__uspapi = function (cmd, ver, callback) { @@ -286,7 +254,7 @@ function lookupUspConsent(ccpaSucess, cmpError, hookConfig) { } }; uspapiCallbacks[callId] = callback; - ccpaFrame.postMessage(msg, '*'); + uspFrame.postMessage(msg, '*'); }; /** when we get the return message, call the stashed callback */ @@ -400,14 +368,14 @@ function processCmpData(consentObject, hookConfig) { } } -function processCcpaData (consentObject, hookConfig) { - if (!(consentObject && consentObject.consentString)) { - cmpFailed(`CCPA returned unexpected value during lookup process.`, hookConfig, consentObject); +function processCcpaData(consentObject, hookConfig) { + if (!(consentObject && consentObject.usPrivacy)) { + cmpFailed(`USP returned unexpected value during lookup process.`, hookConfig, consentObject); return; } clearTimeout(hookConfig.timer); - storeCcpaConsentData(consentObject); + storeUspConsentData(consentObject); exitModule(null, hookConfig); } @@ -447,11 +415,11 @@ function storeConsentData(cmpConsentObject) { gdprDataHandler.setConsentData(consentData); } -function storeCcpaConsentData(consentObject) { +function storeUspConsentData(consentObject) { consentData = { - consentString: consentObject ? consentObject.consentString : undefined + usPrivacy: consentObject ? consentObject.usPrivacy : undefined }; - ccpaDataHandler.setConsentData(consentData); + uspDataHandler.setConsentData(consentData); } /** @@ -503,7 +471,7 @@ function exitModule(errMsg, hookConfig, extraArgs) { export function resetConsentData() { consentData = undefined; gdprDataHandler.setConsentData(null); - ccpaDataHandler.setConsentData(null); + uspDataHandler.setConsentData(null); } /** diff --git a/src/adapterManager.js b/src/adapterManager.js index a34e267bd25..206d831fe0a 100644 --- a/src/adapterManager.js +++ b/src/adapterManager.js @@ -160,13 +160,13 @@ export let gdprDataHandler = { } }; -export let ccpaDataHandler = { +export let uspDataHandler = { consentData: null, setConsentData: function(consentInfo) { - ccpaDataHandler.consentData = consentInfo; + uspDataHandler.consentData = consentInfo; }, getConsentData: function() { - return ccpaDataHandler.consentData; + return uspDataHandler.consentData; } }; diff --git a/test/spec/modules/consentManagement_spec.js b/test/spec/modules/consentManagement_spec.js index bbe5219c249..1f847fcb549 100644 --- a/test/spec/modules/consentManagement_spec.js +++ b/test/spec/modules/consentManagement_spec.js @@ -8,7 +8,7 @@ import { staticConsentData, consentTimeoutUSP } from 'modules/consentManagement'; -import {gdprDataHandler, ccpaDataHandler} from 'src/adapterManager'; +import {gdprDataHandler, uspDataHandler} from 'src/adapterManager'; import * as utils from 'src/utils'; import { config } from 'src/config'; @@ -755,7 +755,7 @@ describe.only('consentManagement', function () { var response = { __uspapiReturn: { callId, - returnValue: { consentString: '1NN' }, + returnValue: { usPrivacy: '1NN' }, success: true } }; @@ -777,10 +777,10 @@ describe.only('consentManagement', function () { stringifyResponse = messageFormatString; setConsentConfig(_goodConfigWithAllowAuction, 'usp'); requestBidsHook(() => { - let consent = ccpaDataHandler.getConsentData(); + let consent = uspDataHandler.getConsentData(); sinon.assert.notCalled(utils.logWarn); sinon.assert.notCalled(utils.logError); - expect(consent.consentString).to.equal('1NN'); + expect(consent.usPrivacy).to.equal('1NN'); done(); }, {}, true); }); @@ -834,7 +834,7 @@ describe.only('consentManagement', function () { it('USP: performs lookup check and stores consentData for a valid existing user', function () { let testConsentData = { - consentString: '1YY' + usPrivacy: '1YY' }; cmpStub = sinon.stub(window, '__uspapi').callsFake((...args) => { args[2](testConsentData); @@ -848,12 +848,12 @@ describe.only('consentManagement', function () { requestBidsHook(() => { didHookReturn = true; }, {}, true); - let consent = ccpaDataHandler.getConsentData(); + let consent = uspDataHandler.getConsentData(); sinon.assert.notCalled(utils.logWarn); sinon.assert.notCalled(utils.logError); expect(didHookReturn).to.be.true; - expect(consent.consentString).to.equal(testConsentData.consentString); + expect(consent.usPrivacy).to.equal(testConsentData.usPrivacy); }); it('throws an error when processCmpData check failed while config had allowAuction set to false', function () { From edb2dd09d8a7a8dae9f0571ac7fef7875fed0a26 Mon Sep 17 00:00:00 2001 From: TJ Eastmond Date: Tue, 26 Nov 2019 19:59:50 -0500 Subject: [PATCH 05/22] Consistent naming --- modules/consentManagement.js | 30 ++++++++++----------- test/spec/modules/consentManagement_spec.js | 6 ++--- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/modules/consentManagement.js b/modules/consentManagement.js index c37ca29c3fa..a49217c640a 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -209,7 +209,7 @@ function lookupUspConsent(uspSucess, cmpError, hookConfig) { let callbackHandler = handleCmpResponseCallbacks(); let uspapiCallbacks = {}; - let ccpaFunction; + let uspFunction; // the following code also determines where the USP is located and uses the proper workflow to communicate with it: // check to see if USP is found on the same window level as prebid and call it directly if so @@ -217,11 +217,11 @@ function lookupUspConsent(uspSucess, cmpError, hookConfig) { // else assume prebid may be inside an iframe and use the IAB USP locator code to see if USP's located in a higher parent window. this works in cross domain iframes // if the USP is not found, the iframe function will call the cmpError exit callback to abort the rest of the USP workflow try { - ccpaFunction = window.__uspapi || utils.getWindowTop().__uspapi; + uspFunction = window.__uspapi || utils.getWindowTop().__uspapi; } catch (e) { } - if (utils.isFn(ccpaFunction)) { - ccpaFunction('getUSPData', 1, callbackHandler.consentDataCallback); + if (utils.isFn(uspFunction)) { + uspFunction('getUSPData', 1, callbackHandler.consentDataCallback); } else { // find the CMP frame let f = window; @@ -238,12 +238,12 @@ function lookupUspConsent(uspSucess, cmpError, hookConfig) { return cmpError('USP not found.', hookConfig); } - callCcpaWhileInIframe('getUSPData', uspFrame, callbackHandler.consentDataCallback); + callUspWhileInIframe('getUSPData', uspFrame, callbackHandler.consentDataCallback); } - function callCcpaWhileInIframe(commandName, uspFrame, moduleCallback) { - /* Setup up a __ccpa function to do the postMessage and stash the callback. - This function behaves (from the caller's perspective identicially to the in-frame __ccpa call */ + function callUspWhileInIframe(commandName, uspFrame, moduleCallback) { + /* Setup up a usp function to do the postMessage and stash the callback. + This function behaves (from the caller's perspective identicially to the in-frame usp call */ window.__uspapi = function (cmd, ver, callback) { let callId = Math.random() + ''; let msg = { @@ -260,8 +260,8 @@ function lookupUspConsent(uspSucess, cmpError, hookConfig) { /** when we get the return message, call the stashed callback */ window.addEventListener('message', readPostMessageResponse, false); - // call ccpa - window.__uspapi(commandName, 1, ccpaIframeCallback); + // call uspapi + window.__uspapi(commandName, 1, uspIframeCallback); function readPostMessageResponse(event) { let res = typeof event.data === 'string' ? JSON.parse(event.data) : event.data; @@ -278,14 +278,14 @@ function lookupUspConsent(uspSucess, cmpError, hookConfig) { window.removeEventListener('message', readPostMessageResponse, false); } - function ccpaIframeCallback(consentObject) { + function uspIframeCallback(consentObject) { removePostMessageListener(); moduleCallback(consentObject); } } } -export function requestCcpaBidsHook(next, reqBidsConfigObj) { +export function requestUspBidsHook(next, reqBidsConfigObj) { requestBidsHook(next, reqBidsConfigObj, true); } @@ -303,7 +303,7 @@ export function requestBidsHook(fn, reqBidsConfigObj, isUSP = false) { if (isUSP) { userModule = 'usp'; - processFn = processCcpaData; + processFn = processUspData; } // preserves all module related variables for the current auction instance (used primiarily for concurrent auctions) @@ -368,7 +368,7 @@ function processCmpData(consentObject, hookConfig) { } } -function processCcpaData(consentObject, hookConfig) { +function processUspData(consentObject, hookConfig) { if (!(consentObject && consentObject.usPrivacy)) { cmpFailed(`USP returned unexpected value during lookup process.`, hookConfig, consentObject); return; @@ -526,7 +526,7 @@ export function setConsentConfig(config, consentModule) { } if (consentModule === 'usp' && !addedConsentHookUSP) { - $$PREBID_GLOBAL$$.requestBids.before(requestCcpaBidsHook, 50); + $$PREBID_GLOBAL$$.requestBids.before(requestUspBidsHook, 50); addedConsentHookUSP = true; } diff --git a/test/spec/modules/consentManagement_spec.js b/test/spec/modules/consentManagement_spec.js index 1f847fcb549..0ec2c126cb1 100644 --- a/test/spec/modules/consentManagement_spec.js +++ b/test/spec/modules/consentManagement_spec.js @@ -724,7 +724,7 @@ describe.only('consentManagement', function () { sinon.stub(utils, 'logError'); sinon.stub(utils, 'logWarn'); ifr = createIFrameMarker(); - window.addEventListener('message', ccpaMessageHandler, false); + window.addEventListener('message', uspMessageHandler, false); }); afterEach(function () { @@ -735,7 +735,7 @@ describe.only('consentManagement', function () { utils.logWarn.restore(); resetConsentData(); document.body.removeChild(ifr); - window.removeEventListener('message', ccpaMessageHandler); + window.removeEventListener('message', uspMessageHandler); }); function createIFrameMarker() { @@ -747,7 +747,7 @@ describe.only('consentManagement', function () { return ifr; } - function ccpaMessageHandler(event) { + function uspMessageHandler(event) { if (event && event.data) { var data = event.data; if (data.__uspapiCall) { From 57e8f91920ac53b3b554e8fe16a0e7009bcd945b Mon Sep 17 00:00:00 2001 From: TJ Eastmond Date: Tue, 26 Nov 2019 21:07:54 -0500 Subject: [PATCH 06/22] Removed test for scenario we won't use for USP --- test/spec/modules/consentManagement_spec.js | 25 --------------------- 1 file changed, 25 deletions(-) diff --git a/test/spec/modules/consentManagement_spec.js b/test/spec/modules/consentManagement_spec.js index 0ec2c126cb1..0d202d6c48a 100644 --- a/test/spec/modules/consentManagement_spec.js +++ b/test/spec/modules/consentManagement_spec.js @@ -730,7 +730,6 @@ describe.only('consentManagement', function () { afterEach(function () { config.resetConfig(); $$PREBID_GLOBAL$$.requestBids.removeAll(); - delete window.__cmp; utils.logError.restore(); utils.logWarn.restore(); resetConsentData(); @@ -832,30 +831,6 @@ describe.only('consentManagement', function () { expect(consent.gdprApplies).to.be.true; }); - it('USP: performs lookup check and stores consentData for a valid existing user', function () { - let testConsentData = { - usPrivacy: '1YY' - }; - cmpStub = sinon.stub(window, '__uspapi').callsFake((...args) => { - args[2](testConsentData); - }); - - let _goodConfigWithAllowAuction = { ...goodConfigWithAllowAuction }; - _goodConfigWithAllowAuction.consentAPIs = ['usp']; - - setConsentConfig(_goodConfigWithAllowAuction, 'usp'); - - requestBidsHook(() => { - didHookReturn = true; - }, {}, true); - let consent = uspDataHandler.getConsentData(); - - sinon.assert.notCalled(utils.logWarn); - sinon.assert.notCalled(utils.logError); - expect(didHookReturn).to.be.true; - expect(consent.usPrivacy).to.equal(testConsentData.usPrivacy); - }); - it('throws an error when processCmpData check failed while config had allowAuction set to false', function () { let testConsentData = {}; let bidsBackHandlerReturn = false; From ec9c052ccb75e77fd6924b6294fce200168cfce6 Mon Sep 17 00:00:00 2001 From: TJ Eastmond Date: Tue, 26 Nov 2019 21:10:13 -0500 Subject: [PATCH 07/22] Removed non-iframe workflows and typo fix --- modules/consentManagement.js | 45 ++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/modules/consentManagement.js b/modules/consentManagement.js index a49217c640a..817e04d8a7a 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -189,13 +189,13 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) { } } -function lookupUspConsent(uspSucess, cmpError, hookConfig) { +function lookupUspConsent(uspSuccess, cmpError, hookConfig) { function handleCmpResponseCallbacks() { const uspResponse = {}; function afterEach() { if (uspResponse.usPrivacy) { - uspSucess(uspResponse, hookConfig); + uspSuccess(uspResponse, hookConfig); } } @@ -209,41 +209,36 @@ function lookupUspConsent(uspSucess, cmpError, hookConfig) { let callbackHandler = handleCmpResponseCallbacks(); let uspapiCallbacks = {}; - let uspFunction; // the following code also determines where the USP is located and uses the proper workflow to communicate with it: // check to see if USP is found on the same window level as prebid and call it directly if so // check to see if prebid is in a safeframe (with USP support) // else assume prebid may be inside an iframe and use the IAB USP locator code to see if USP's located in a higher parent window. this works in cross domain iframes // if the USP is not found, the iframe function will call the cmpError exit callback to abort the rest of the USP workflow - try { - uspFunction = window.__uspapi || utils.getWindowTop().__uspapi; - } catch (e) { } + let f = window; + let uspFrame; + while (!uspFrame) { + try { + if (f.frames['__uspapiLocator']) uspFrame = f; + } catch (e) { } + if (f === window.top) break; + f = f.parent; + } - if (utils.isFn(uspFunction)) { - uspFunction('getUSPData', 1, callbackHandler.consentDataCallback); - } else { - // find the CMP frame - let f = window; - let uspFrame; - while (!uspFrame) { - try { - if (f.frames['__uspapiLocator']) uspFrame = f; - } catch (e) { } - if (f === window.top) break; - f = f.parent; - } + if (!uspFrame) { + return cmpError('USP not found.', hookConfig); + } - if (!uspFrame) { - return cmpError('USP not found.', hookConfig); - } + callUspWhileInIframe('getUSPData', uspFrame, callbackHandler.consentDataCallback); - callUspWhileInIframe('getUSPData', uspFrame, callbackHandler.consentDataCallback); + if (!uspFrame) { + return cmpError('USP frame not found.', hookConfig); } + callUspWhileInIframe('getUSPData', uspFrame, callbackHandler.consentDataCallback); + function callUspWhileInIframe(commandName, uspFrame, moduleCallback) { - /* Setup up a usp function to do the postMessage and stash the callback. - This function behaves (from the caller's perspective identicially to the in-frame usp call */ + /* Setup up a __uspapi function to do the postMessage and stash the callback. */ window.__uspapi = function (cmd, ver, callback) { let callId = Math.random() + ''; let msg = { From e98aa50ae3b60a29b1f0bd97dbe6c586f998b333 Mon Sep 17 00:00:00 2001 From: TJ Eastmond Date: Mon, 2 Dec 2019 10:48:51 -0500 Subject: [PATCH 08/22] Reverting original modules to break out UPS --- modules/consentManagement.js | 194 ++------------------ test/spec/modules/consentManagement_spec.js | 125 ++----------- 2 files changed, 36 insertions(+), 283 deletions(-) diff --git a/modules/consentManagement.js b/modules/consentManagement.js index 817e04d8a7a..1e2a6648145 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -6,12 +6,12 @@ */ import * as utils from '../src/utils'; import { config } from '../src/config'; -import { gdprDataHandler, uspDataHandler } from '../src/adapterManager'; +import { gdprDataHandler } from '../src/adapterManager'; import includes from 'core-js/library/fn/array/includes'; import strIncludes from 'core-js/library/fn/string/includes'; +const DEFAULT_CMP = 'iab'; const DEFAULT_CONSENT_TIMEOUT = 10000; -const DEFAULT_CONSENT_TIMEOUT_USP = 50; const DEFAULT_ALLOW_AUCTION_WO_CONSENT = true; export let userCMP; @@ -22,16 +22,9 @@ export let staticConsentData; let consentData; let addedConsentHook = false; -// usp -export let consentTimeoutUSP; -let addedConsentHookUSP = false; - // add new CMPs here, with their dedicated lookup function const cmpCallMap = { 'iab': lookupIabConsent, - 'gdpr': lookupIabConsent, - 'ccpa': lookupUspConsent, - 'usp': lookupUspConsent, 'static': lookupStaticConsentData }; @@ -189,101 +182,6 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) { } } -function lookupUspConsent(uspSuccess, cmpError, hookConfig) { - function handleCmpResponseCallbacks() { - const uspResponse = {}; - - function afterEach() { - if (uspResponse.usPrivacy) { - uspSuccess(uspResponse, hookConfig); - } - } - - return { - consentDataCallback: function (consentResponse) { - uspResponse.usPrivacy = consentResponse.usPrivacy; - afterEach(); - } - } - } - - let callbackHandler = handleCmpResponseCallbacks(); - let uspapiCallbacks = {}; - - // the following code also determines where the USP is located and uses the proper workflow to communicate with it: - // check to see if USP is found on the same window level as prebid and call it directly if so - // check to see if prebid is in a safeframe (with USP support) - // else assume prebid may be inside an iframe and use the IAB USP locator code to see if USP's located in a higher parent window. this works in cross domain iframes - // if the USP is not found, the iframe function will call the cmpError exit callback to abort the rest of the USP workflow - let f = window; - let uspFrame; - while (!uspFrame) { - try { - if (f.frames['__uspapiLocator']) uspFrame = f; - } catch (e) { } - if (f === window.top) break; - f = f.parent; - } - - if (!uspFrame) { - return cmpError('USP not found.', hookConfig); - } - - callUspWhileInIframe('getUSPData', uspFrame, callbackHandler.consentDataCallback); - - if (!uspFrame) { - return cmpError('USP frame not found.', hookConfig); - } - - callUspWhileInIframe('getUSPData', uspFrame, callbackHandler.consentDataCallback); - - function callUspWhileInIframe(commandName, uspFrame, moduleCallback) { - /* Setup up a __uspapi function to do the postMessage and stash the callback. */ - window.__uspapi = function (cmd, ver, callback) { - let callId = Math.random() + ''; - let msg = { - __uspapiCall: { - command: cmd, - version: ver, - callId: callId - } - }; - uspapiCallbacks[callId] = callback; - uspFrame.postMessage(msg, '*'); - }; - - /** when we get the return message, call the stashed callback */ - window.addEventListener('message', readPostMessageResponse, false); - - // call uspapi - window.__uspapi(commandName, 1, uspIframeCallback); - - function readPostMessageResponse(event) { - let res = typeof event.data === 'string' ? JSON.parse(event.data) : event.data; - if (res.__uspapiReturn && res.__uspapiReturn.callId) { - let i = res.__uspapiReturn; - if (typeof uspapiCallbacks[i.callId] !== 'undefined') { - uspapiCallbacks[i.callId](i.returnValue, i.success); - delete uspapiCallbacks[i.callId]; - } - } - } - - function removePostMessageListener() { - window.removeEventListener('message', readPostMessageResponse, false); - } - - function uspIframeCallback(consentObject) { - removePostMessageListener(); - moduleCallback(consentObject); - } - } -} - -export function requestUspBidsHook(next, reqBidsConfigObj) { - requestBidsHook(next, reqBidsConfigObj, true); -} - /** * If consentManagement module is enabled (ie included in setConfig), this hook function will attempt to fetch the * user's encoded consent string from the supported CMP. Once obtained, the module will store this @@ -292,15 +190,7 @@ export function requestUspBidsHook(next, reqBidsConfigObj) { * @param {object} reqBidsConfigObj required; This is the same param that's used in pbjs.requestBids. * @param {function} fn required; The next function in the chain, used by hook.js */ -export function requestBidsHook(fn, reqBidsConfigObj, isUSP = false) { - let userModule = userCMP; - let processFn = processCmpData; - - if (isUSP) { - userModule = 'usp'; - processFn = processUspData; - } - +export function requestBidsHook(fn, reqBidsConfigObj) { // preserves all module related variables for the current auction instance (used primiarily for concurrent auctions) const hookConfig = { context: this, @@ -309,8 +199,7 @@ export function requestBidsHook(fn, reqBidsConfigObj, isUSP = false) { adUnits: reqBidsConfigObj.adUnits || $$PREBID_GLOBAL$$.adUnits, bidsBackHandler: reqBidsConfigObj.bidsBackHandler, haveExited: false, - timer: null, - userModule: userModule + timer: null }; // in case we already have consent (eg during bid refresh) @@ -318,20 +207,19 @@ export function requestBidsHook(fn, reqBidsConfigObj, isUSP = false) { return exitModule(null, hookConfig); } - if (!includes(Object.keys(cmpCallMap), userModule)) { - utils.logWarn(`CMP framework (${userModule}) is not a supported framework. Aborting consentManagement module and resuming auction.`); + if (!includes(Object.keys(cmpCallMap), userCMP)) { + utils.logWarn(`CMP framework (${userCMP}) is not a supported framework. Aborting consentManagement module and resuming auction.`); return hookConfig.nextFn.apply(hookConfig.context, hookConfig.args); } - cmpCallMap[userModule].call(this, processFn, cmpFailed, hookConfig); + cmpCallMap[userCMP].call(this, processCmpData, cmpFailed, hookConfig); // only let this code run if module is still active (ie if the callbacks used by CMPs haven't already finished) if (!hookConfig.haveExited) { - let timeout = userModule === 'usp' ? consentTimeoutUSP : consentTimeout; - if (timeout === 0) { - processFn(undefined, hookConfig); + if (consentTimeout === 0) { + processCmpData(undefined, hookConfig); } else { - hookConfig.timer = setTimeout(cmpTimedOut.bind(null, hookConfig), timeout); + hookConfig.timer = setTimeout(cmpTimedOut.bind(null, hookConfig), consentTimeout); } } } @@ -363,17 +251,6 @@ function processCmpData(consentObject, hookConfig) { } } -function processUspData(consentObject, hookConfig) { - if (!(consentObject && consentObject.usPrivacy)) { - cmpFailed(`USP returned unexpected value during lookup process.`, hookConfig, consentObject); - return; - } - - clearTimeout(hookConfig.timer); - storeUspConsentData(consentObject); - exitModule(null, hookConfig); -} - /** * General timeout callback when interacting with CMP takes too long. */ @@ -410,13 +287,6 @@ function storeConsentData(cmpConsentObject) { gdprDataHandler.setConsentData(consentData); } -function storeUspConsentData(consentObject) { - consentData = { - usPrivacy: consentObject ? consentObject.usPrivacy : undefined - }; - uspDataHandler.setConsentData(consentData); -} - /** * This function handles the exit logic for the module. * There are several paths in the module's logic to call this function and we only allow 1 of the 3 potential exits to happen before suppressing others. @@ -466,26 +336,18 @@ function exitModule(errMsg, hookConfig, extraArgs) { export function resetConsentData() { consentData = undefined; gdprDataHandler.setConsentData(null); - uspDataHandler.setConsentData(null); } /** * A configuration function that initializes some module variables, as well as add a hook into the requestBids function * @param {object} config required; consentManagement module config settings; cmp (string), timeout (int), allowAuctionWithoutConsent (boolean) */ -export function setConsentConfig(config, consentModule) { - if (consentModule) { - if (['gdpr', 'iab'].includes(consentModule)) { - userCMP = 'iab'; - } - - if (consentModule === 'static') { - userCMP = 'static'; - } - - if (!['gdpr', 'iab', 'static', 'usp'].includes(consentModule) && consentModule) { - userCMP = consentModule; - } +export function setConsentConfig(config) { + if (utils.isStr(config.cmpApi)) { + userCMP = config.cmpApi; + } else { + userCMP = DEFAULT_CMP; + utils.logInfo(`consentManagement config did not specify cmp. Using system default setting (${DEFAULT_CMP}).`); } if (utils.isNumber(config.timeout)) { @@ -495,13 +357,6 @@ export function setConsentConfig(config, consentModule) { utils.logInfo(`consentManagement config did not specify timeout. Using system default setting (${DEFAULT_CONSENT_TIMEOUT}).`); } - if (utils.isNumber(config.uspTimeout)) { - consentTimeoutUSP = config.uspTimeout - } else { - consentTimeoutUSP = DEFAULT_CONSENT_TIMEOUT_USP; - utils.logInfo(`consentManagement config did not specify timeout f or USP. Using system default setting: (${DEFAULT_CONSENT_TIMEOUT_USP}).`); - } - if (typeof config.allowAuctionWithoutConsent === 'boolean') { allowAuction = config.allowAuctionWithoutConsent; } else { @@ -519,20 +374,9 @@ export function setConsentConfig(config, consentModule) { utils.logError(`consentManagement config with cmpApi: 'static' did not specify consentData. No consents will be available to adapters.`); } } - - if (consentModule === 'usp' && !addedConsentHookUSP) { - $$PREBID_GLOBAL$$.requestBids.before(requestUspBidsHook, 50); - addedConsentHookUSP = true; - } - - if (!addedConsentHook && consentModule === 'gdpr') { + if (!addedConsentHook) { $$PREBID_GLOBAL$$.requestBids.before(requestBidsHook, 50); - addedConsentHook = true; } + addedConsentHook = true; } - -config.getConfig('consentManagement', config => { - const consentManagement = { ...config.consentManagement }; - const consentChecks = consentManagement.consentAPIs ? new Set([...consentManagement.consentAPIs]) : new Set([]); - [...consentChecks].map(module => setConsentConfig(consentManagement, module)); -}); +config.getConfig('consentManagement', config => setConsentConfig(config.consentManagement)); diff --git a/test/spec/modules/consentManagement_spec.js b/test/spec/modules/consentManagement_spec.js index 0d202d6c48a..6be96427750 100644 --- a/test/spec/modules/consentManagement_spec.js +++ b/test/spec/modules/consentManagement_spec.js @@ -1,21 +1,12 @@ -import { - setConsentConfig, - requestBidsHook, - resetConsentData, - userCMP, - consentTimeout, - allowAuction, - staticConsentData, - consentTimeoutUSP -} from 'modules/consentManagement'; -import {gdprDataHandler, uspDataHandler} from 'src/adapterManager'; +import {setConsentConfig, requestBidsHook, resetConsentData, userCMP, consentTimeout, allowAuction, staticConsentData} from 'modules/consentManagement'; +import {gdprDataHandler} from 'src/adapterManager'; import * as utils from 'src/utils'; import { config } from 'src/config'; let assert = require('chai').assert; let expect = require('chai').expect; -describe.only('consentManagement', function () { +describe('consentManagement', function () { describe('setConsentConfig tests:', function () { describe('empty setConsentConfig value', function () { beforeEach(function () { @@ -28,18 +19,12 @@ describe.only('consentManagement', function () { }); it('should use system default values', function () { - setConsentConfig({}, 'gdpr'); + setConsentConfig({}); expect(userCMP).to.be.equal('iab'); expect(consentTimeout).to.be.equal(10000); expect(allowAuction).to.be.true; sinon.assert.callCount(utils.logInfo, 4); }); - - it('should use system default values for USP', function () { - setConsentConfig({}, 'usp'); - expect(consentTimeoutUSP).to.be.equal(50); - sinon.assert.callCount(utils.logInfo, 4); - }); }); describe('valid setConsentConfig value', function () { @@ -51,8 +36,7 @@ describe.only('consentManagement', function () { let allConfig = { cmpApi: 'iab', timeout: 7500, - allowAuctionWithoutConsent: false, - consentAPIs: ['gdpr'] + allowAuctionWithoutConsent: false }; setConsentConfig(allConfig); @@ -72,7 +56,6 @@ describe.only('consentManagement', function () { cmpApi: 'static', timeout: 7500, allowAuctionWithoutConsent: false, - consentAPIs: ['static'], consentData: { getConsentData: { 'gdprApplies': true, @@ -464,7 +447,7 @@ describe.only('consentManagement', function () { } }; - setConsentConfig(staticConfig, 'static'); + setConsentConfig(staticConfig); expect(userCMP).to.be.equal('static'); expect(consentTimeout).to.be.equal(0); // should always return without a timeout when config is used expect(allowAuction).to.be.false; @@ -477,15 +460,13 @@ describe.only('consentManagement', function () { let goodConfigWithCancelAuction = { cmpApi: 'iab', timeout: 7500, - allowAuctionWithoutConsent: false, - consentAPIs: ['gdpr'] + allowAuctionWithoutConsent: false }; let goodConfigWithAllowAuction = { cmpApi: 'iab', timeout: 7500, - allowAuctionWithoutConsent: true, - consentAPIs: ['gdpr'] + allowAuctionWithoutConsent: true }; let didHookReturn; @@ -514,7 +495,7 @@ describe.only('consentManagement', function () { let badCMPConfig = { cmpApi: 'bad' }; - setConsentConfig(badCMPConfig, 'bad'); + setConsentConfig(badCMPConfig); expect(userCMP).to.be.equal(badCMPConfig.cmpApi); requestBidsHook(() => { @@ -527,11 +508,11 @@ describe.only('consentManagement', function () { }); it('should throw proper errors when CMP is not found', function () { - setConsentConfig(goodConfigWithCancelAuction, 'gdpr'); + setConsentConfig(goodConfigWithCancelAuction); requestBidsHook(() => { didHookReturn = true; - }, {}, 'gdpr'); + }, {}); let consent = gdprDataHandler.getConsentData(); // throw 2 errors; one for no bidsBackHandler and for CMP not being found (this is an error due to gdpr config) sinon.assert.calledTwice(utils.logError); @@ -565,7 +546,7 @@ describe.only('consentManagement', function () { cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => { args[2](testConsentData); }); - setConsentConfig(goodConfigWithAllowAuction, 'gdpr'); + setConsentConfig(goodConfigWithAllowAuction); requestBidsHook(() => {}, {}); cmpStub.restore(); @@ -629,7 +610,7 @@ describe.only('consentManagement', function () { args[2](testConsentData.data.msgName, testConsentData.data); }); - setConsentConfig(goodConfigWithAllowAuction, 'gdpr'); + setConsentConfig(goodConfigWithAllowAuction); requestBidsHook(() => { didHookReturn = true; }, {adUnits: [{ sizes: [[300, 250]] }]}); @@ -703,7 +684,7 @@ describe.only('consentManagement', function () { function testIFramedPage(testName, messageFormatString) { it(`should return the consent string from a postmessage + addEventListener response - ${testName}`, (done) => { stringifyResponse = messageFormatString; - setConsentConfig(goodConfigWithAllowAuction, 'gdpr'); + setConsentConfig(goodConfigWithAllowAuction); requestBidsHook(() => { let consent = gdprDataHandler.getConsentData(); sinon.assert.notCalled(utils.logWarn); @@ -716,76 +697,6 @@ describe.only('consentManagement', function () { } }); - describe('USP workflow for iframed page', function () { - let ifr = null; - let stringifyResponse = false; - - beforeEach(function () { - sinon.stub(utils, 'logError'); - sinon.stub(utils, 'logWarn'); - ifr = createIFrameMarker(); - window.addEventListener('message', uspMessageHandler, false); - }); - - afterEach(function () { - config.resetConfig(); - $$PREBID_GLOBAL$$.requestBids.removeAll(); - utils.logError.restore(); - utils.logWarn.restore(); - resetConsentData(); - document.body.removeChild(ifr); - window.removeEventListener('message', uspMessageHandler); - }); - - function createIFrameMarker() { - var ifr = document.createElement('iframe'); - ifr.width = 0; - ifr.height = 0; - ifr.name = '__uspapiLocator'; - document.body.appendChild(ifr); - return ifr; - } - - function uspMessageHandler(event) { - if (event && event.data) { - var data = event.data; - if (data.__uspapiCall) { - var callId = data.__uspapiCall.callId; - var response = { - __uspapiReturn: { - callId, - returnValue: { usPrivacy: '1NN' }, - success: true - } - }; - event.source.postMessage(stringifyResponse ? JSON.stringify(response) : response, '*'); - } - } - } - - let _goodConfigWithAllowAuction = { ...goodConfigWithAllowAuction }; - _goodConfigWithAllowAuction.consentAPIs = ['usp']; - - // Run tests with JSON response and String response - // from CMP window postMessage listener. - testIFramedPage('with/JSON response', false); - // testIFramedPage('with/String response', true); - - function testIFramedPage(testName, messageFormatString) { - it(`should return the consent string from a postmessage + addEventListener response - ${testName}`, (done) => { - stringifyResponse = messageFormatString; - setConsentConfig(_goodConfigWithAllowAuction, 'usp'); - requestBidsHook(() => { - let consent = uspDataHandler.getConsentData(); - sinon.assert.notCalled(utils.logWarn); - sinon.assert.notCalled(utils.logError); - expect(consent.usPrivacy).to.equal('1NN'); - done(); - }, {}, true); - }); - } - }); - describe('CMP workflow for normal pages:', function () { let cmpStub = sinon.stub(); @@ -794,7 +705,6 @@ describe.only('consentManagement', function () { sinon.stub(utils, 'logError'); sinon.stub(utils, 'logWarn'); window.__cmp = function() {}; - window.__uspapi = function() {}; }); afterEach(function () { @@ -804,7 +714,6 @@ describe.only('consentManagement', function () { utils.logError.restore(); utils.logWarn.restore(); delete window.__cmp; - delete window.__uspapi; resetConsentData(); }); @@ -817,7 +726,7 @@ describe.only('consentManagement', function () { args[2](testConsentData); }); - setConsentConfig(goodConfigWithAllowAuction, 'gdpr'); + setConsentConfig(goodConfigWithAllowAuction); requestBidsHook(() => { didHookReturn = true; @@ -839,7 +748,7 @@ describe.only('consentManagement', function () { args[2](testConsentData); }); - setConsentConfig(goodConfigWithCancelAuction, 'gdpr'); + setConsentConfig(goodConfigWithCancelAuction); requestBidsHook(() => { didHookReturn = true; @@ -859,7 +768,7 @@ describe.only('consentManagement', function () { args[2](testConsentData); }); - setConsentConfig(goodConfigWithAllowAuction, 'gdpr'); + setConsentConfig(goodConfigWithAllowAuction); requestBidsHook(() => { didHookReturn = true; From b04e727e0fdf8d392b1ab3756f72a8fe9309e700 Mon Sep 17 00:00:00 2001 From: TJ Eastmond Date: Mon, 2 Dec 2019 14:51:51 -0500 Subject: [PATCH 09/22] USPAPI to there own files --- modules/consentManagementUsp.js | 310 +++++++++++++++++ .../spec/modules/consentManagementUsp_spec.js | 313 ++++++++++++++++++ 2 files changed, 623 insertions(+) create mode 100644 modules/consentManagementUsp.js create mode 100644 test/spec/modules/consentManagementUsp_spec.js diff --git a/modules/consentManagementUsp.js b/modules/consentManagementUsp.js new file mode 100644 index 00000000000..c37438e7d30 --- /dev/null +++ b/modules/consentManagementUsp.js @@ -0,0 +1,310 @@ +/** + * This module adds GDPR consentManagement support to prebid.js. It interacts with + * supported CMPs (Consent Management Platforms) to grab the user's consent information + * and make it available for any GDPR supported adapters to read/pass this information to + * their system. + */ +import * as utils from '../src/utils'; +import { config } from '../src/config'; +import { uspDataHandler } from '../src/adapterManager'; +import includes from 'core-js/library/fn/array/includes'; +// import strIncludes from 'core-js/library/fn/string/includes'; + +const DEFAULT_CONSENT_API = 'uspapi'; +const DEFAULT_CONSENT_TIMEOUT = 50; +const DEFAULT_ALLOW_AUCTION_WO_CONSENT = true; + +export let userUSP; +export let consentTimeout; +export let allowAuction; + +let consentData; +let addedConsentHook = false; + +// add new CMPs here, with their dedicated lookup function +const uspCallMap = { + 'uspapi': lookupUpsConsent +}; + +/** + * This function handles interacting with an IAB compliant CMP to obtain the consent information of the user. + * Given the async nature of the CMP's API, we pass in acting success/error callback functions to exit this function + * based on the appropriate result. + * @param {function(string)} cmpSuccess acts as a success callback when CMP returns a value; pass along consentObject (string) from CMP + * @param {function(string)} uspError acts as an error callback while interacting with CMP; pass along an error message (string) + * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function) + */ +function lookupUpsConsent(uspSuccess, uspError, hookConfig) { + function handleCmpResponseCallbacks() { + const uspResponse = {}; + + function afterEach() { + if (uspResponse.usPrivacy) { + uspSuccess(uspResponse, hookConfig); + } + } + + return { + consentDataCallback: function (consentResponse) { + uspResponse.usPrivacy = consentResponse.usPrivacy; + afterEach(); + } + } + } + + let callbackHandler = handleCmpResponseCallbacks(); + let uspapiCallbacks = {}; + let uspapiFunction; + + // to collect the consent information from the user, we perform two calls to the CMP in parallel: + // first to collect the user's consent choices represented in an encoded string (via getUSPData) + // second to collect the user's full unparsed consent information (via getVendorConsents) + + // the following code also determines where the CMP is located and uses the proper workflow to communicate with it: + // check to see if CMP is found on the same window level as prebid and call it directly if so + // check to see if prebid is in a safeframe (with CMP support) + // else assume prebid may be inside an iframe and use the IAB CMP locator code to see if CMP's located in a higher parent window. this works in cross domain iframes + // if the CMP is not found, the iframe function will call the uspError exit callback to abort the rest of the CMP workflow + try { + uspapiFunction = window.__uspapi || utils.getWindowTop().__uspapi; + } catch (e) { } + + if (utils.isFn(uspapiFunction)) { + uspapiFunction('getUSPData', null, callbackHandler.consentDataCallback); + } else { + // find the CMP frame + let f = window; + let uspapiFrame; + while (!uspapiFrame) { + try { + if (f.frames['__uspapiLocator']) uspapiFrame = f; + } catch (e) { } + if (f === window.top) break; + f = f.parent; + } + + if (!uspapiFrame) { + return uspError('CMP not found.', hookConfig); + } + + callUspApiWhileInIframe('getUSPData', uspapiFrame, callbackHandler.consentDataCallback); + } + + function callUspApiWhileInIframe(commandName, uspapiFrame, moduleCallback) { + /* Setup up a __cmp function to do the postMessage and stash the callback. + This function behaves (from the caller's perspective identicially to the in-frame __cmp call */ + window.__uspapi = function (cmd, ver, callback) { + let callId = Math.random() + ''; + let msg = { + __uspapiCall: { + command: cmd, + version: ver, + callId: callId + } + }; + + uspapiCallbacks[callId] = callback; + uspapiFrame.postMessage(msg, '*'); + } + + /** when we get the return message, call the stashed callback */ + window.addEventListener('message', readPostMessageResponse, false); + + // call USPAPI + window.__uspapi(commandName, 1, uspapiCallback); + + function readPostMessageResponse(event) { + let res = typeof event.data === 'string' ? JSON.parse(event.data) : event.data; + if (res.__uspapiReturn && res.__uspapiReturn.callId) { + let i = res.__uspapiReturn; + if (typeof uspapiCallbacks[i.callId] !== 'undefined') { + uspapiCallbacks[i.callId](i.returnValue, i.success); + delete uspapiCallbacks[i.callId]; + } + } + } + + function uspapiCallback(consentObject) { + window.removeEventListener('message', readPostMessageResponse, false); + moduleCallback(consentObject); + } + } +} + +/** + * If consentManagement module is enabled (ie included in setConfig), this hook function will attempt to fetch the + * user's encoded consent string from the supported CMP. Once obtained, the module will store this + * data as part of a gdprConsent object which gets transferred to adapterManager's gdprDataHandler object. + * This information is later added into the bidRequest object for any supported adapters to read/pass along to their system. + * @param {object} reqBidsConfigObj required; This is the same param that's used in pbjs.requestBids. + * @param {function} fn required; The next function in the chain, used by hook.js + */ +export function requestBidsHook(fn, reqBidsConfigObj) { + // preserves all module related variables for the current auction instance (used primiarily for concurrent auctions) + const hookConfig = { + context: this, + args: [reqBidsConfigObj], + nextFn: fn, + adUnits: reqBidsConfigObj.adUnits || $$PREBID_GLOBAL$$.adUnits, + bidsBackHandler: reqBidsConfigObj.bidsBackHandler, + haveExited: false, + timer: null + }; + + // in case we already have consent (eg during bid refresh) + if (consentData) { + return exitModule(null, hookConfig); + } + + if (!includes(Object.keys(uspCallMap), userUSP)) { + utils.logWarn(`USP framework (${userUSP}) is not a supported framework. Aborting consentManagement module and resuming auction.`); + return hookConfig.nextFn.apply(hookConfig.context, hookConfig.args); + } + + uspCallMap[userUSP].call(this, processUspData, uspapiFailed, hookConfig); + + // only let this code run if module is still active (ie if the callbacks used by CMPs haven't already finished) + if (!hookConfig.haveExited) { + if (consentTimeout === 0) { + processUspData(undefined, hookConfig); + } else { + hookConfig.timer = setTimeout(uspapiTimeout.bind(null, hookConfig), consentTimeout); + } + } +} + +/** + * This function checks the consent data provided by CMP to ensure it's in an expected state. + * If it's bad, we exit the module depending on config settings. + * If it's good, then we store the value and exits the module. + * @param {object} consentObject required; object returned by CMP that contains user's consent choices + * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function) + */ +function processUspData(consentObject, hookConfig) { + if (!(consentObject && consentObject.usPrivacy)) { + uspapiFailed(`UPSAPI returned unexpected value during lookup process.`, hookConfig, consentObject); + return; + } + + clearTimeout(hookConfig.timer); + storeUspConsentData(consentObject); + exitModule(null, hookConfig); +} + +/** + * General timeout callback when interacting with CMP takes too long. + */ +function uspapiTimeout(hookConfig) { + cmpFailed('USPAPI workflow exceeded timeout threshold.', hookConfig); +} + +/** + * This function contains the controlled steps to perform when there's a problem with CMP. + * @param {string} errMsg required; should be a short descriptive message for why the failure/issue happened. + * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function) + * @param {object} extraArgs contains additional data that's passed along in the error/warning messages for easier debugging +*/ +function uspapiFailed(errMsg, hookConfig, extraArgs) { + clearTimeout(hookConfig.timer); + + // still set the consentData to undefined when there is a problem as per config options + if (allowAuction) { + storeUspConsentData(undefined); + } + exitModule(errMsg, hookConfig, extraArgs); +} + +/** + * Stores CMP data locally in module and then invokes gdprDataHandler.setConsentData() to make information available in adaptermanger.js for later in the auction + * @param {object} cmpConsentObject required; an object representing user's consent choices (can be undefined in certain use-cases for this function only) + */ +function storeUspConsentData(consentObject) { + consentData = { usPrivacy: consentObject ? consentObject.usPrivacy : undefined }; + uspDataHandler.setConsentData(consentData); +} + +/** + * This function handles the exit logic for the module. + * There are several paths in the module's logic to call this function and we only allow 1 of the 3 potential exits to happen before suppressing others. + * + * We prevent multiple exits to avoid conflicting messages in the console depending on certain scenarios. + * One scenario could be auction was canceled due to timeout with CMP being reached. + * While the timeout is the accepted exit and runs first, the CMP's callback still tries to process the user's data (which normally leads to a good exit). + * In this case, the good exit will be suppressed since we already decided to cancel the auction. + * + * Three exit paths are: + * 1. good exit where auction runs (CMP data is processed normally). + * 2. bad exit but auction still continues (warning message is logged, CMP data is undefined and still passed along). + * 3. bad exit with auction canceled (error message is logged). + * @param {string} errMsg optional; only to be used when there was a 'bad' exit. String is a descriptive message for the failure/issue encountered. + * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function) + * @param {object} extraArgs contains additional data that's passed along in the error/warning messages for easier debugging + */ +function exitModule(errMsg, hookConfig, extraArgs) { + if (hookConfig.haveExited === false) { + hookConfig.haveExited = true; + + let context = hookConfig.context; + let args = hookConfig.args; + let nextFn = hookConfig.nextFn; + + if (errMsg) { + if (allowAuction) { + utils.logWarn(errMsg + ' Resuming auction without consent data as per consentManagement config.', extraArgs); + nextFn.apply(context, args); + } else { + utils.logError(errMsg + ' Canceling auction as per consentManagement config.', extraArgs); + if (typeof hookConfig.bidsBackHandler === 'function') { + hookConfig.bidsBackHandler(); + } else { + utils.logError('Error executing bidsBackHandler'); + } + } + } else { + nextFn.apply(context, args); + } + } +} + +/** + * Simply resets the module's consentData variable back to undefined, mainly for testing purposes + */ +export function resetConsentData() { + consentData = undefined; + uspDataHandler.setConsentData(null); +} + +/** + * A configuration function that initializes some module variables, as well as add a hook into the requestBids function + * @param {object} config required; consentManagement module config settings; cmp (string), timeout (int), allowAuctionWithoutConsent (boolean) + */ +export function setConsentConfig(config) { + if (utils.isStr(config.cmpApi)) { + userUSP = config.cmpApi; + } else { + userUSP = DEFAULT_CONSENT_API; + utils.logInfo(`consentManagement config did not specify cmp. Using system default setting (${DEFAULT_CONSENT_API}).`); + } + + if (utils.isNumber(config.timeout)) { + consentTimeout = config.timeout; + } else { + consentTimeout = DEFAULT_CONSENT_TIMEOUT; + utils.logInfo(`consentManagement config did not specify timeout. Using system default setting (${DEFAULT_CONSENT_TIMEOUT}).`); + } + + if (typeof config.allowAuctionWithoutConsent === 'boolean') { + allowAuction = config.allowAuctionWithoutConsent; + } else { + allowAuction = DEFAULT_ALLOW_AUCTION_WO_CONSENT; + utils.logInfo(`consentManagement config did not specify allowAuctionWithoutConsent. Using system default setting (${DEFAULT_ALLOW_AUCTION_WO_CONSENT}).`); + } + + utils.logInfo('USPAPI consentManagement module has been activated...'); + + if (!addedConsentHook) { + $$PREBID_GLOBAL$$.requestBids.before(requestBidsHook, 50); + } + addedConsentHook = true; +} +config.getConfig('consentManagement', config => setConsentConfig(config.consentManagement)); diff --git a/test/spec/modules/consentManagementUsp_spec.js b/test/spec/modules/consentManagementUsp_spec.js new file mode 100644 index 00000000000..bc1735ba1fa --- /dev/null +++ b/test/spec/modules/consentManagementUsp_spec.js @@ -0,0 +1,313 @@ +import { + setConsentConfig, + requestBidsHook, + resetConsentData, + userUSP, + consentTimeout, + allowAuction +} from 'modules/consentManagementUSP'; +import * as utils from 'src/utils'; +import { config } from 'src/config'; +import { uspDataHandler } from 'src/adapterManager'; + +let assert = require('chai').assert; +let expect = require('chai').expect; + +describe.only('consentManagement', function () { + describe('setConsentConfig tests:', function () { + describe('empty setConsentConfig value', function () { + beforeEach(function () { + sinon.stub(utils, 'logInfo'); + }); + + afterEach(function () { + utils.logInfo.restore(); + config.resetConfig(); + }); + + it('should use system default values', function () { + setConsentConfig({}); + expect(userUSP).to.be.equal('uspapi'); + expect(consentTimeout).to.be.equal(50); + expect(allowAuction).to.be.true; + sinon.assert.callCount(utils.logInfo, 4); + }); + }); + + describe('valid setConsentConfig value', function () { + afterEach(function () { + config.resetConfig(); + $$PREBID_GLOBAL$$.requestBids.removeAll(); + }); + it('results in all user settings overriding system defaults', function () { + let allConfig = { + cmpApi: 'uspapi', + timeout: 7500, + allowAuctionWithoutConsent: false + }; + + setConsentConfig(allConfig); + expect(userUSP).to.be.equal('uspapi'); + expect(consentTimeout).to.be.equal(7500); + expect(allowAuction).to.be.false; + }); + }); + }); + + describe('requestBidsHook tests:', function () { + let goodConfigWithCancelAuction = { + cmpApi: 'uspapi', + timeout: 7500, + allowAuctionWithoutConsent: false + }; + + let goodConfigWithAllowAuction = { + cmpApi: 'uspapi', + timeout: 7500, + allowAuctionWithoutConsent: true + }; + + let didHookReturn; + + afterEach(function () { + uspDataHandler.consentData = null; + resetConsentData(); + }); + + describe('error checks:', function () { + beforeEach(function () { + didHookReturn = false; + sinon.stub(utils, 'logWarn'); + sinon.stub(utils, 'logError'); + }); + + afterEach(function () { + utils.logWarn.restore(); + utils.logError.restore(); + config.resetConfig(); + $$PREBID_GLOBAL$$.requestBids.removeAll(); + resetConsentData(); + }); + + it('should throw a warning and return to hooked function when an unknown USPAPI framework ID is used', function () { + let badCMPConfig = { cmpApi: 'bad' }; + setConsentConfig(badCMPConfig); + expect(userUSP).to.be.equal(badCMPConfig.cmpApi); + requestBidsHook(() => { didHookReturn = true; }, {}); + let consent = uspDataHandler.getConsentData(); + sinon.assert.calledOnce(utils.logWarn); + expect(didHookReturn).to.be.true; + expect(consent).to.be.null; + }); + + it('should throw proper errors when USP is not found', function () { + setConsentConfig(goodConfigWithCancelAuction); + requestBidsHook(() => { didHookReturn = true; }, {}); + let consent = uspDataHandler.getConsentData(); + // throw 2 errors; one for no bidsBackHandler and for CMP not being found (this is an error due to gdpr config) + sinon.assert.calledTwice(utils.logError); + expect(didHookReturn).to.be.false; + expect(consent).to.be.null; + }); + }); + + // describe('already known consentData:', function () { + // let uspStub = sinon.stub(); + + // beforeEach(function () { + // didHookReturn = false; + // window.__uspapi = function() {}; + // }); + + // afterEach(function () { + // config.resetConfig(); + // $$PREBID_GLOBAL$$.requestBids.removeAll(); + // uspStub.restore(); + // delete window.__uspapi; + // resetConsentData(); + // }); + + // it('should bypass CMP and simply use previously stored consentData', function () { + // let testConsentData = { + // consentData: '1YY' + // }; + + // uspStub = sinon.stub(window, '__uspapi').callsFake((...args) => { + // args[2](testConsentData); + // }); + + // setConsentConfig(goodConfigWithAllowAuction); + // requestBidsHook(() => {}, {}); + // uspStub.restore(); + + // // reset the stub to ensure it wasn't called during the second round of calls + // uspStub = sinon.stub(window, '__uspapi').callsFake((...args) => { + // args[2](testConsentData); + // }); + + // requestBidsHook(() => { + // didHookReturn = true; + // }, {}); + // let consent = uspDataHandler.getConsentData(); + // // console.log('consent:', consent); + // // expect(didHookReturn).to.be.true; + // // expect(consent.consentString).to.equal(testConsentData.consentData); + // sinon.assert.notCalled(uspStub); + // }); + // }); + + describe('USPAPI workflow for iframed page', function () { + let ifr = null; + let stringifyResponse = false; + + beforeEach(function () { + sinon.stub(utils, 'logError'); + sinon.stub(utils, 'logWarn'); + ifr = createIFrameMarker(); + window.addEventListener('message', uspapiMessageHandler, false); + }); + + afterEach(function () { + config.resetConfig(); + $$PREBID_GLOBAL$$.requestBids.removeAll(); + delete window.__uspapi; + utils.logError.restore(); + utils.logWarn.restore(); + resetConsentData(); + document.body.removeChild(ifr); + window.removeEventListener('message', uspapiMessageHandler); + }); + + function createIFrameMarker() { + var ifr = document.createElement('iframe'); + ifr.width = 0; + ifr.height = 0; + ifr.name = '__uspapiLocator'; + document.body.appendChild(ifr); + return ifr; + } + + function uspapiMessageHandler(event) { + if (event && event.data) { + var data = event.data; + if (data.__uspapiCall) { + var callId = data.__uspapiCall.callId; + var returnValue = null; + var response = { + __uspapiReturn: { + callId, + returnValue: { usPrivacy: '1YY' }, + success: true + } + }; + event.source.postMessage(stringifyResponse ? JSON.stringify(response) : response, '*'); + } + } + } + + // Run tests with JSON response and String response + // from CMP window postMessage listener. + testIFramedPage('with/JSON response', false); + testIFramedPage('with/String response', true); + + function testIFramedPage(testName, messageFormatString) { + it(`should return the consent string from a postmessage + addEventListener response - ${testName}`, (done) => { + stringifyResponse = messageFormatString; + setConsentConfig(goodConfigWithAllowAuction); + requestBidsHook(() => { + let consent = uspDataHandler.getConsentData(); + sinon.assert.notCalled(utils.logWarn); + sinon.assert.notCalled(utils.logError); + expect(consent.usPrivacy).to.equal('1YY'); + done(); + }, {}); + }); + } + }); + + // describe('CMP workflow for normal pages:', function () { + // let cmpStub = sinon.stub(); + + // beforeEach(function () { + // didHookReturn = false; + // sinon.stub(utils, 'logError'); + // sinon.stub(utils, 'logWarn'); + // window.__cmp = function() {}; + // }); + + // afterEach(function () { + // config.resetConfig(); + // $$PREBID_GLOBAL$$.requestBids.removeAll(); + // cmpStub.restore(); + // utils.logError.restore(); + // utils.logWarn.restore(); + // delete window.__cmp; + // resetConsentData(); + // }); + + // it('performs lookup check and stores consentData for a valid existing user', function () { + // let testConsentData = { + // gdprApplies: true, + // consentData: 'BOJy+UqOJy+UqABAB+AAAAAZ+A==' + // }; + // cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => { + // args[2](testConsentData); + // }); + + // setConsentConfig(goodConfigWithAllowAuction); + + // requestBidsHook(() => { + // didHookReturn = true; + // }, {}); + // let consent = gdprDataHandler.getConsentData(); + + // sinon.assert.notCalled(utils.logWarn); + // sinon.assert.notCalled(utils.logError); + // expect(didHookReturn).to.be.true; + // expect(consent.consentString).to.equal(testConsentData.consentData); + // expect(consent.gdprApplies).to.be.true; + // }); + + // it('throws an error when processCmpData check failed while config had allowAuction set to false', function () { + // let testConsentData = {}; + // let bidsBackHandlerReturn = false; + + // cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => { + // args[2](testConsentData); + // }); + + // setConsentConfig(goodConfigWithCancelAuction); + + // requestBidsHook(() => { + // didHookReturn = true; + // }, { bidsBackHandler: () => bidsBackHandlerReturn = true }); + // let consent = gdprDataHandler.getConsentData(); + + // sinon.assert.calledOnce(utils.logError); + // expect(didHookReturn).to.be.false; + // expect(bidsBackHandlerReturn).to.be.true; + // expect(consent).to.be.null; + // }); + + // it('throws a warning + stores consentData + calls callback when processCmpData check failed while config had allowAuction set to true', function () { + // let testConsentData = {}; + + // cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => { + // args[2](testConsentData); + // }); + + // setConsentConfig(goodConfigWithAllowAuction); + + // requestBidsHook(() => { + // didHookReturn = true; + // }, {}); + // let consent = gdprDataHandler.getConsentData(); + + // sinon.assert.calledOnce(utils.logWarn); + // expect(didHookReturn).to.be.true; + // expect(consent.consentString).to.be.undefined; + // expect(consent.gdprApplies).to.be.undefined; + // }); + // }); + }); +}); From 6e0470ca5cb721866284e43b579da5ec27378216 Mon Sep 17 00:00:00 2001 From: TJ Eastmond Date: Mon, 2 Dec 2019 18:03:29 -0500 Subject: [PATCH 10/22] Seperate files for USPAPI CCPA --- modules/consentManagementUsp.js | 38 ++-- .../spec/modules/consentManagementUsp_spec.js | 214 +++++++----------- 2 files changed, 105 insertions(+), 147 deletions(-) diff --git a/modules/consentManagementUsp.js b/modules/consentManagementUsp.js index c37438e7d30..1ea67e2a109 100644 --- a/modules/consentManagementUsp.js +++ b/modules/consentManagementUsp.js @@ -1,14 +1,13 @@ /** - * This module adds GDPR consentManagement support to prebid.js. It interacts with - * supported CMPs (Consent Management Platforms) to grab the user's consent information - * and make it available for any GDPR supported adapters to read/pass this information to - * their system. + * This module adds USPAPI (CCPA) consentManagement support to prebid.js. It + * interacts with supported USP Consent APIs to grab the user's consent + * information and make it available for any GDPR supported adapters to + * read/pass this information to their system. */ import * as utils from '../src/utils'; import { config } from '../src/config'; import { uspDataHandler } from '../src/adapterManager'; import includes from 'core-js/library/fn/array/includes'; -// import strIncludes from 'core-js/library/fn/string/includes'; const DEFAULT_CONSENT_API = 'uspapi'; const DEFAULT_CONSENT_TIMEOUT = 50; @@ -21,7 +20,7 @@ export let allowAuction; let consentData; let addedConsentHook = false; -// add new CMPs here, with their dedicated lookup function +// consent APIs const uspCallMap = { 'uspapi': lookupUpsConsent }; @@ -30,12 +29,12 @@ const uspCallMap = { * This function handles interacting with an IAB compliant CMP to obtain the consent information of the user. * Given the async nature of the CMP's API, we pass in acting success/error callback functions to exit this function * based on the appropriate result. - * @param {function(string)} cmpSuccess acts as a success callback when CMP returns a value; pass along consentObject (string) from CMP - * @param {function(string)} uspError acts as an error callback while interacting with CMP; pass along an error message (string) + * @param {function(string)} uspSuccess acts as a success callback when USPAPI returns a value; pass along consentObject (string) from UPSAPI + * @param {function(string)} uspError acts as an error callback while interacting with USPAPI; pass along an error message (string) * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function) */ function lookupUpsConsent(uspSuccess, uspError, hookConfig) { - function handleCmpResponseCallbacks() { + function handleUspApiResponseCallbacks() { const uspResponse = {}; function afterEach() { @@ -45,23 +44,22 @@ function lookupUpsConsent(uspSuccess, uspError, hookConfig) { } return { - consentDataCallback: function (consentResponse) { + consentDataCallback: consentResponse => { uspResponse.usPrivacy = consentResponse.usPrivacy; afterEach(); } - } + }; } - let callbackHandler = handleCmpResponseCallbacks(); + let callbackHandler = handleUspApiResponseCallbacks(); let uspapiCallbacks = {}; let uspapiFunction; - // to collect the consent information from the user, we perform two calls to the CMP in parallel: - // first to collect the user's consent choices represented in an encoded string (via getUSPData) - // second to collect the user's full unparsed consent information (via getVendorConsents) + // to collect the consent information from the user, we perform a call to USPAPI + // to collect the user's consent choices represented as a string (via getUSPData) - // the following code also determines where the CMP is located and uses the proper workflow to communicate with it: - // check to see if CMP is found on the same window level as prebid and call it directly if so + // the following code also determines where the USPAPI is located and uses the proper workflow to communicate with it: + // check to see if USPAPI is found on the same window level as prebid and call it directly if so // check to see if prebid is in a safeframe (with CMP support) // else assume prebid may be inside an iframe and use the IAB CMP locator code to see if CMP's located in a higher parent window. this works in cross domain iframes // if the CMP is not found, the iframe function will call the uspError exit callback to abort the rest of the CMP workflow @@ -70,7 +68,7 @@ function lookupUpsConsent(uspSuccess, uspError, hookConfig) { } catch (e) { } if (utils.isFn(uspapiFunction)) { - uspapiFunction('getUSPData', null, callbackHandler.consentDataCallback); + uspapiFunction('getUSPData', 1, callbackHandler.consentDataCallback); } else { // find the CMP frame let f = window; @@ -181,7 +179,8 @@ export function requestBidsHook(fn, reqBidsConfigObj) { * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function) */ function processUspData(consentObject, hookConfig) { - if (!(consentObject && consentObject.usPrivacy)) { + const valid = !!(consentObject && consentObject.usPrivacy); + if (!valid) { uspapiFailed(`UPSAPI returned unexpected value during lookup process.`, hookConfig, consentObject); return; } @@ -211,6 +210,7 @@ function uspapiFailed(errMsg, hookConfig, extraArgs) { if (allowAuction) { storeUspConsentData(undefined); } + exitModule(errMsg, hookConfig, extraArgs); } diff --git a/test/spec/modules/consentManagementUsp_spec.js b/test/spec/modules/consentManagementUsp_spec.js index bc1735ba1fa..8b6ceaa582a 100644 --- a/test/spec/modules/consentManagementUsp_spec.js +++ b/test/spec/modules/consentManagementUsp_spec.js @@ -111,50 +111,48 @@ describe.only('consentManagement', function () { }); }); - // describe('already known consentData:', function () { - // let uspStub = sinon.stub(); - - // beforeEach(function () { - // didHookReturn = false; - // window.__uspapi = function() {}; - // }); - - // afterEach(function () { - // config.resetConfig(); - // $$PREBID_GLOBAL$$.requestBids.removeAll(); - // uspStub.restore(); - // delete window.__uspapi; - // resetConsentData(); - // }); - - // it('should bypass CMP and simply use previously stored consentData', function () { - // let testConsentData = { - // consentData: '1YY' - // }; - - // uspStub = sinon.stub(window, '__uspapi').callsFake((...args) => { - // args[2](testConsentData); - // }); - - // setConsentConfig(goodConfigWithAllowAuction); - // requestBidsHook(() => {}, {}); - // uspStub.restore(); - - // // reset the stub to ensure it wasn't called during the second round of calls - // uspStub = sinon.stub(window, '__uspapi').callsFake((...args) => { - // args[2](testConsentData); - // }); - - // requestBidsHook(() => { - // didHookReturn = true; - // }, {}); - // let consent = uspDataHandler.getConsentData(); - // // console.log('consent:', consent); - // // expect(didHookReturn).to.be.true; - // // expect(consent.consentString).to.equal(testConsentData.consentData); - // sinon.assert.notCalled(uspStub); - // }); - // }); + describe('already known consentData:', function () { + let uspStub = sinon.stub(); + + beforeEach(function () { + didHookReturn = false; + window.__uspapi = function() {}; + }); + + afterEach(function () { + config.resetConfig(); + $$PREBID_GLOBAL$$.requestBids.removeAll(); + uspStub.restore(); + delete window.__uspapi; + resetConsentData(); + }); + + it('should bypass CMP and simply use previously stored consentData', function () { + let testConsentData = { + usPrivacy: '1YY' + }; + + uspStub = sinon.stub(window, '__uspapi').callsFake((...args) => { + args[2](testConsentData); + }); + + setConsentConfig(goodConfigWithAllowAuction); + requestBidsHook(() => {}, {}); + uspStub.restore(); + + // reset the stub to ensure it wasn't called during the second round of calls + uspStub = sinon.stub(window, '__uspapi').callsFake((...args) => { + args[2](testConsentData); + }); + + requestBidsHook(() => { didHookReturn = true; }, {}); + + let consent = uspDataHandler.getConsentData(); + expect(didHookReturn).to.be.true; + expect(consent.consentString).to.equal(testConsentData.consentData); + sinon.assert.notCalled(uspStub); + }); + }); describe('USPAPI workflow for iframed page', function () { let ifr = null; @@ -225,89 +223,49 @@ describe.only('consentManagement', function () { } }); - // describe('CMP workflow for normal pages:', function () { - // let cmpStub = sinon.stub(); - - // beforeEach(function () { - // didHookReturn = false; - // sinon.stub(utils, 'logError'); - // sinon.stub(utils, 'logWarn'); - // window.__cmp = function() {}; - // }); - - // afterEach(function () { - // config.resetConfig(); - // $$PREBID_GLOBAL$$.requestBids.removeAll(); - // cmpStub.restore(); - // utils.logError.restore(); - // utils.logWarn.restore(); - // delete window.__cmp; - // resetConsentData(); - // }); - - // it('performs lookup check and stores consentData for a valid existing user', function () { - // let testConsentData = { - // gdprApplies: true, - // consentData: 'BOJy+UqOJy+UqABAB+AAAAAZ+A==' - // }; - // cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => { - // args[2](testConsentData); - // }); - - // setConsentConfig(goodConfigWithAllowAuction); - - // requestBidsHook(() => { - // didHookReturn = true; - // }, {}); - // let consent = gdprDataHandler.getConsentData(); - - // sinon.assert.notCalled(utils.logWarn); - // sinon.assert.notCalled(utils.logError); - // expect(didHookReturn).to.be.true; - // expect(consent.consentString).to.equal(testConsentData.consentData); - // expect(consent.gdprApplies).to.be.true; - // }); - - // it('throws an error when processCmpData check failed while config had allowAuction set to false', function () { - // let testConsentData = {}; - // let bidsBackHandlerReturn = false; - - // cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => { - // args[2](testConsentData); - // }); - - // setConsentConfig(goodConfigWithCancelAuction); - - // requestBidsHook(() => { - // didHookReturn = true; - // }, { bidsBackHandler: () => bidsBackHandlerReturn = true }); - // let consent = gdprDataHandler.getConsentData(); - - // sinon.assert.calledOnce(utils.logError); - // expect(didHookReturn).to.be.false; - // expect(bidsBackHandlerReturn).to.be.true; - // expect(consent).to.be.null; - // }); - - // it('throws a warning + stores consentData + calls callback when processCmpData check failed while config had allowAuction set to true', function () { - // let testConsentData = {}; - - // cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => { - // args[2](testConsentData); - // }); - - // setConsentConfig(goodConfigWithAllowAuction); - - // requestBidsHook(() => { - // didHookReturn = true; - // }, {}); - // let consent = gdprDataHandler.getConsentData(); - - // sinon.assert.calledOnce(utils.logWarn); - // expect(didHookReturn).to.be.true; - // expect(consent.consentString).to.be.undefined; - // expect(consent.gdprApplies).to.be.undefined; - // }); - // }); + describe('USPAPI workflow for normal pages:', function () { + let uspapiStub = sinon.stub(); + + beforeEach(function () { + didHookReturn = false; + sinon.stub(utils, 'logError'); + sinon.stub(utils, 'logWarn'); + window.__uspapi = function() {}; + }); + + afterEach(function () { + config.resetConfig(); + $$PREBID_GLOBAL$$.requestBids.removeAll(); + uspapiStub.restore(); + utils.logError.restore(); + utils.logWarn.restore(); + delete window.__uspapi; + resetConsentData(); + }); + + it('performs lookup check and stores consentData for a valid existing user', function () { + let testConsentData = { + usPrivacy: '1YY' + }; + + uspapiStub = sinon.stub(window, '__uspapi').callsFake((...args) => { + args[2](testConsentData); + }); + + setConsentConfig(goodConfigWithAllowAuction); + + requestBidsHook(() => { + didHookReturn = true; + }, {}); + + let consent = uspDataHandler.getConsentData(); + + sinon.assert.notCalled(utils.logWarn); + sinon.assert.notCalled(utils.logError); + + expect(didHookReturn).to.be.true; + expect(consent.usPrivacy).to.equal(testConsentData.usPrivacy); + }); + }); }); }); From b09f2d16e84933169f42d150d93427582b368211 Mon Sep 17 00:00:00 2001 From: TJ Eastmond Date: Mon, 2 Dec 2019 18:17:03 -0500 Subject: [PATCH 11/22] Cleaning up comments --- modules/consentManagementUsp.js | 53 ++++++++++++++++----------------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/modules/consentManagementUsp.js b/modules/consentManagementUsp.js index 1ea67e2a109..0603cff8821 100644 --- a/modules/consentManagementUsp.js +++ b/modules/consentManagementUsp.js @@ -1,7 +1,7 @@ /** * This module adds USPAPI (CCPA) consentManagement support to prebid.js. It * interacts with supported USP Consent APIs to grab the user's consent - * information and make it available for any GDPR supported adapters to + * information and make it available for any USP (CCPA) supported adapters to * read/pass this information to their system. */ import * as utils from '../src/utils'; @@ -26,8 +26,8 @@ const uspCallMap = { }; /** - * This function handles interacting with an IAB compliant CMP to obtain the consent information of the user. - * Given the async nature of the CMP's API, we pass in acting success/error callback functions to exit this function + * This function handles interacting with an USP compliant consent manager to obtain the consent information of the user. + * Given the async nature of the USP's API, we pass in acting success/error callback functions to exit this function * based on the appropriate result. * @param {function(string)} uspSuccess acts as a success callback when USPAPI returns a value; pass along consentObject (string) from UPSAPI * @param {function(string)} uspError acts as an error callback while interacting with USPAPI; pass along an error message (string) @@ -59,10 +59,9 @@ function lookupUpsConsent(uspSuccess, uspError, hookConfig) { // to collect the user's consent choices represented as a string (via getUSPData) // the following code also determines where the USPAPI is located and uses the proper workflow to communicate with it: - // check to see if USPAPI is found on the same window level as prebid and call it directly if so - // check to see if prebid is in a safeframe (with CMP support) - // else assume prebid may be inside an iframe and use the IAB CMP locator code to see if CMP's located in a higher parent window. this works in cross domain iframes - // if the CMP is not found, the iframe function will call the uspError exit callback to abort the rest of the CMP workflow + // - check to see if USPAPI is found on the same window level as prebid and call it directly if so + // - else assume prebid may be inside an iframe and use the USPAPI locator code to see if USP's located in a higher parent window. This works in cross domain iframes + // - if USPAPI is not found, the iframe function will call the uspError exit callback to abort the rest of the USPAPI workflow try { uspapiFunction = window.__uspapi || utils.getWindowTop().__uspapi; } catch (e) { } @@ -89,8 +88,8 @@ function lookupUpsConsent(uspSuccess, uspError, hookConfig) { } function callUspApiWhileInIframe(commandName, uspapiFrame, moduleCallback) { - /* Setup up a __cmp function to do the postMessage and stash the callback. - This function behaves (from the caller's perspective identicially to the in-frame __cmp call */ + /* Setup up a __uspapi function to do the postMessage and stash the callback. + This function behaves (from the caller's perspective identicially to the in-frame __uspapi call */ window.__uspapi = function (cmd, ver, callback) { let callId = Math.random() + ''; let msg = { @@ -108,7 +107,7 @@ function lookupUpsConsent(uspSuccess, uspError, hookConfig) { /** when we get the return message, call the stashed callback */ window.addEventListener('message', readPostMessageResponse, false); - // call USPAPI + // call uspapi window.__uspapi(commandName, 1, uspapiCallback); function readPostMessageResponse(event) { @@ -130,9 +129,9 @@ function lookupUpsConsent(uspSuccess, uspError, hookConfig) { } /** - * If consentManagement module is enabled (ie included in setConfig), this hook function will attempt to fetch the - * user's encoded consent string from the supported CMP. Once obtained, the module will store this - * data as part of a gdprConsent object which gets transferred to adapterManager's gdprDataHandler object. + * If consentManagementUSP module is enabled (ie included in setConfig), this hook function will attempt to fetch the + * user's encoded consent string from the supported USPAPI. Once obtained, the module will store this + * data as part of a uspConsent object which gets transferred to adapterManager's uspDataHandler object. * This information is later added into the bidRequest object for any supported adapters to read/pass along to their system. * @param {object} reqBidsConfigObj required; This is the same param that's used in pbjs.requestBids. * @param {function} fn required; The next function in the chain, used by hook.js @@ -161,7 +160,7 @@ export function requestBidsHook(fn, reqBidsConfigObj) { uspCallMap[userUSP].call(this, processUspData, uspapiFailed, hookConfig); - // only let this code run if module is still active (ie if the callbacks used by CMPs haven't already finished) + // only let this code run if module is still active (ie if the callbacks used by USPs haven't already finished) if (!hookConfig.haveExited) { if (consentTimeout === 0) { processUspData(undefined, hookConfig); @@ -172,10 +171,10 @@ export function requestBidsHook(fn, reqBidsConfigObj) { } /** - * This function checks the consent data provided by CMP to ensure it's in an expected state. + * This function checks the consent data provided by USPAPI to ensure it's in an expected state. * If it's bad, we exit the module depending on config settings. * If it's good, then we store the value and exits the module. - * @param {object} consentObject required; object returned by CMP that contains user's consent choices + * @param {object} consentObject required; object returned by USPAPI that contains user's consent choices * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function) */ function processUspData(consentObject, hookConfig) { @@ -191,14 +190,14 @@ function processUspData(consentObject, hookConfig) { } /** - * General timeout callback when interacting with CMP takes too long. + * General timeout callback when interacting with USPAPI takes too long. */ function uspapiTimeout(hookConfig) { cmpFailed('USPAPI workflow exceeded timeout threshold.', hookConfig); } /** - * This function contains the controlled steps to perform when there's a problem with CMP. + * This function contains the controlled steps to perform when there's a problem with USPAPI. * @param {string} errMsg required; should be a short descriptive message for why the failure/issue happened. * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function) * @param {object} extraArgs contains additional data that's passed along in the error/warning messages for easier debugging @@ -215,7 +214,7 @@ function uspapiFailed(errMsg, hookConfig, extraArgs) { } /** - * Stores CMP data locally in module and then invokes gdprDataHandler.setConsentData() to make information available in adaptermanger.js for later in the auction + * Stores USP data locally in module and then invokes uspDataHandler.setConsentData() to make information available in adaptermanger.js for later in the auction * @param {object} cmpConsentObject required; an object representing user's consent choices (can be undefined in certain use-cases for this function only) */ function storeUspConsentData(consentObject) { @@ -228,13 +227,13 @@ function storeUspConsentData(consentObject) { * There are several paths in the module's logic to call this function and we only allow 1 of the 3 potential exits to happen before suppressing others. * * We prevent multiple exits to avoid conflicting messages in the console depending on certain scenarios. - * One scenario could be auction was canceled due to timeout with CMP being reached. - * While the timeout is the accepted exit and runs first, the CMP's callback still tries to process the user's data (which normally leads to a good exit). + * One scenario could be auction was canceled due to timeout with USPAPI being reached. + * While the timeout is the accepted exit and runs first, the USP's callback still tries to process the user's data (which normally leads to a good exit). * In this case, the good exit will be suppressed since we already decided to cancel the auction. * * Three exit paths are: - * 1. good exit where auction runs (CMP data is processed normally). - * 2. bad exit but auction still continues (warning message is logged, CMP data is undefined and still passed along). + * 1. good exit where auction runs (USPAPI data is processed normally). + * 2. bad exit but auction still continues (warning message is logged, USPAPI data is undefined and still passed along). * 3. bad exit with auction canceled (error message is logged). * @param {string} errMsg optional; only to be used when there was a 'bad' exit. String is a descriptive message for the failure/issue encountered. * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function) @@ -276,28 +275,28 @@ export function resetConsentData() { /** * A configuration function that initializes some module variables, as well as add a hook into the requestBids function - * @param {object} config required; consentManagement module config settings; cmp (string), timeout (int), allowAuctionWithoutConsent (boolean) + * @param {object} config required; consentManagementUSP module config settings; usp (string), timeout (int), allowAuctionWithoutConsent (boolean) */ export function setConsentConfig(config) { if (utils.isStr(config.cmpApi)) { userUSP = config.cmpApi; } else { userUSP = DEFAULT_CONSENT_API; - utils.logInfo(`consentManagement config did not specify cmp. Using system default setting (${DEFAULT_CONSENT_API}).`); + utils.logInfo(`consentManagementUSP config did not specify USP. Using system default setting (${DEFAULT_CONSENT_API}).`); } if (utils.isNumber(config.timeout)) { consentTimeout = config.timeout; } else { consentTimeout = DEFAULT_CONSENT_TIMEOUT; - utils.logInfo(`consentManagement config did not specify timeout. Using system default setting (${DEFAULT_CONSENT_TIMEOUT}).`); + utils.logInfo(`consentManagementUSP config did not specify timeout. Using system default setting (${DEFAULT_CONSENT_TIMEOUT}).`); } if (typeof config.allowAuctionWithoutConsent === 'boolean') { allowAuction = config.allowAuctionWithoutConsent; } else { allowAuction = DEFAULT_ALLOW_AUCTION_WO_CONSENT; - utils.logInfo(`consentManagement config did not specify allowAuctionWithoutConsent. Using system default setting (${DEFAULT_ALLOW_AUCTION_WO_CONSENT}).`); + utils.logInfo(`consentManagementUSP config did not specify allowAuctionWithoutConsent. Using system default setting (${DEFAULT_ALLOW_AUCTION_WO_CONSENT}).`); } utils.logInfo('USPAPI consentManagement module has been activated...'); From df55efe8060c1bd5a70bbc346c4602e4e38471e0 Mon Sep 17 00:00:00 2001 From: TJ Eastmond Date: Mon, 2 Dec 2019 18:21:38 -0500 Subject: [PATCH 12/22] CCPA consent added to adapterManager --- src/adapterManager.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/adapterManager.js b/src/adapterManager.js index 206d831fe0a..79af3a95fe5 100644 --- a/src/adapterManager.js +++ b/src/adapterManager.js @@ -271,6 +271,7 @@ adapterManager.makeBidRequests = function(adUnits, auctionStart, auctionId, cbTi if (gdprDataHandler.getConsentData()) { bidRequests.forEach(bidRequest => { bidRequest['gdprConsent'] = gdprDataHandler.getConsentData(); + bidRequest['ccpaConsent'] = uspDataHandler.getConsentData(); }); } return bidRequests; From 458d429abba7aa51a3e3a7df588553d886ab21af Mon Sep 17 00:00:00 2001 From: Eric Harper Date: Tue, 3 Dec 2019 04:19:52 -0500 Subject: [PATCH 13/22] updated config treatment, fixed problems --- modules/consentManagement.js | 3 + modules/consentManagementUsp.js | 137 ++++++++---------- src/adapterManager.js | 2 +- .../spec/modules/consentManagementUsp_spec.js | 54 +++---- 4 files changed, 90 insertions(+), 106 deletions(-) diff --git a/modules/consentManagement.js b/modules/consentManagement.js index 1e2a6648145..74a0fd533bf 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -343,6 +343,9 @@ export function resetConsentData() { * @param {object} config required; consentManagement module config settings; cmp (string), timeout (int), allowAuctionWithoutConsent (boolean) */ export function setConsentConfig(config) { + // if `config.gdpr` or `config.usp` exist, assume new config format. + // else for backward compatability, just use `config` + config = config.gdpr || config.usp ? config.gdpr : config; if (utils.isStr(config.cmpApi)) { userCMP = config.cmpApi; } else { diff --git a/modules/consentManagementUsp.js b/modules/consentManagementUsp.js index 0603cff8821..e148ed339c9 100644 --- a/modules/consentManagementUsp.js +++ b/modules/consentManagementUsp.js @@ -7,22 +7,19 @@ import * as utils from '../src/utils'; import { config } from '../src/config'; import { uspDataHandler } from '../src/adapterManager'; -import includes from 'core-js/library/fn/array/includes'; -const DEFAULT_CONSENT_API = 'uspapi'; +const DEFAULT_CONSENT_API = 'iab'; const DEFAULT_CONSENT_TIMEOUT = 50; -const DEFAULT_ALLOW_AUCTION_WO_CONSENT = true; -export let userUSP; +export let consentAPI; export let consentTimeout; -export let allowAuction; let consentData; let addedConsentHook = false; // consent APIs const uspCallMap = { - 'uspapi': lookupUpsConsent + 'iab': lookupUspConsent }; /** @@ -33,19 +30,23 @@ const uspCallMap = { * @param {function(string)} uspError acts as an error callback while interacting with USPAPI; pass along an error message (string) * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function) */ -function lookupUpsConsent(uspSuccess, uspError, hookConfig) { +function lookupUspConsent(uspSuccess, uspError, hookConfig) { function handleUspApiResponseCallbacks() { const uspResponse = {}; function afterEach() { if (uspResponse.usPrivacy) { uspSuccess(uspResponse, hookConfig); + } else { + uspError('Unable to get USP consent string.', hookConfig); } } return { - consentDataCallback: consentResponse => { - uspResponse.usPrivacy = consentResponse.usPrivacy; + consentDataCallback: (consentResponse, success) => { + if (success && consentResponse.uspString) { + uspResponse.usPrivacy = consentResponse.uspString; + } afterEach(); } }; @@ -53,43 +54,41 @@ function lookupUpsConsent(uspSuccess, uspError, hookConfig) { let callbackHandler = handleUspApiResponseCallbacks(); let uspapiCallbacks = {}; - let uspapiFunction; // to collect the consent information from the user, we perform a call to USPAPI // to collect the user's consent choices represented as a string (via getUSPData) // the following code also determines where the USPAPI is located and uses the proper workflow to communicate with it: - // - check to see if USPAPI is found on the same window level as prebid and call it directly if so - // - else assume prebid may be inside an iframe and use the USPAPI locator code to see if USP's located in a higher parent window. This works in cross domain iframes + // - use the USPAPI locator code to see if USP's located in the current window or an ancestor window. This works in friendly or cross domain iframes // - if USPAPI is not found, the iframe function will call the uspError exit callback to abort the rest of the USPAPI workflow - try { - uspapiFunction = window.__uspapi || utils.getWindowTop().__uspapi; - } catch (e) { } - - if (utils.isFn(uspapiFunction)) { - uspapiFunction('getUSPData', 1, callbackHandler.consentDataCallback); - } else { - // find the CMP frame - let f = window; - let uspapiFrame; - while (!uspapiFrame) { - try { - if (f.frames['__uspapiLocator']) uspapiFrame = f; - } catch (e) { } - if (f === window.top) break; - f = f.parent; - } + // - try to call the __uspapi() function directly, otherwise use the postMessage() api + + // find the CMP frame/window + let f = window; + let uspapiFrame; + while (!uspapiFrame) { + try { + if (f.frames['__uspapiLocator']) uspapiFrame = f; + } catch (e) { } + if (f === window.top) break; + f = f.parent; + } - if (!uspapiFrame) { - return uspError('CMP not found.', hookConfig); - } + if (!uspapiFrame) { + return uspError('CMP not found.', hookConfig); + } + try { + // try to call __uspapi directly + uspapiFrame.__uspapi('getUSPData', 1, callbackHandler.consentDataCallback); + } catch (e) { + // must not have been accessible, try using postMessage() api callUspApiWhileInIframe('getUSPData', uspapiFrame, callbackHandler.consentDataCallback); } function callUspApiWhileInIframe(commandName, uspapiFrame, moduleCallback) { /* Setup up a __uspapi function to do the postMessage and stash the callback. - This function behaves (from the caller's perspective identicially to the in-frame __uspapi call */ + This function behaves, from the caller's perspective, identicially to the in-frame __uspapi call (although it is not synchronous) */ window.__uspapi = function (cmd, ver, callback) { let callId = Math.random() + ''; let msg = { @@ -111,19 +110,18 @@ function lookupUpsConsent(uspSuccess, uspError, hookConfig) { window.__uspapi(commandName, 1, uspapiCallback); function readPostMessageResponse(event) { - let res = typeof event.data === 'string' ? JSON.parse(event.data) : event.data; - if (res.__uspapiReturn && res.__uspapiReturn.callId) { - let i = res.__uspapiReturn; - if (typeof uspapiCallbacks[i.callId] !== 'undefined') { - uspapiCallbacks[i.callId](i.returnValue, i.success); - delete uspapiCallbacks[i.callId]; + const res = event && event.data && event.data.__uspapiReturn; + if (res && res.callId) { + if (typeof uspapiCallbacks[res.callId] !== 'undefined') { + uspapiCallbacks[res.callId](res.returnValue, res.success); + delete uspapiCallbacks[res.callId]; } } } - function uspapiCallback(consentObject) { + function uspapiCallback(consentObject, success) { window.removeEventListener('message', readPostMessageResponse, false); - moduleCallback(consentObject); + moduleCallback(consentObject, success); } } } @@ -153,12 +151,12 @@ export function requestBidsHook(fn, reqBidsConfigObj) { return exitModule(null, hookConfig); } - if (!includes(Object.keys(uspCallMap), userUSP)) { - utils.logWarn(`USP framework (${userUSP}) is not a supported framework. Aborting consentManagement module and resuming auction.`); + if (!uspCallMap[consentAPI]) { + utils.logWarn(`USP framework (${consentAPI}) is not a supported framework. Aborting consentManagement module and resuming auction.`); return hookConfig.nextFn.apply(hookConfig.context, hookConfig.args); } - uspCallMap[userUSP].call(this, processUspData, uspapiFailed, hookConfig); + uspCallMap[consentAPI].call(this, processUspData, uspapiFailed, hookConfig); // only let this code run if module is still active (ie if the callbacks used by USPs haven't already finished) if (!hookConfig.haveExited) { @@ -193,7 +191,7 @@ function processUspData(consentObject, hookConfig) { * General timeout callback when interacting with USPAPI takes too long. */ function uspapiTimeout(hookConfig) { - cmpFailed('USPAPI workflow exceeded timeout threshold.', hookConfig); + uspapiFailed('USPAPI workflow exceeded timeout threshold.', hookConfig); } /** @@ -205,11 +203,6 @@ function uspapiTimeout(hookConfig) { function uspapiFailed(errMsg, hookConfig, extraArgs) { clearTimeout(hookConfig.timer); - // still set the consentData to undefined when there is a problem as per config options - if (allowAuction) { - storeUspConsentData(undefined); - } - exitModule(errMsg, hookConfig, extraArgs); } @@ -218,13 +211,15 @@ function uspapiFailed(errMsg, hookConfig, extraArgs) { * @param {object} cmpConsentObject required; an object representing user's consent choices (can be undefined in certain use-cases for this function only) */ function storeUspConsentData(consentObject) { - consentData = { usPrivacy: consentObject ? consentObject.usPrivacy : undefined }; - uspDataHandler.setConsentData(consentData); + if (consentObject && consentObject.usPrivacy) { + consentData = consentObject.usPrivacy; + uspDataHandler.setConsentData(consentData.usPrivacy); + } } /** * This function handles the exit logic for the module. - * There are several paths in the module's logic to call this function and we only allow 1 of the 3 potential exits to happen before suppressing others. + * There are a couple paths in the module's logic to call this function and we only allow 1 of the 2 potential exits to happen before suppressing others. * * We prevent multiple exits to avoid conflicting messages in the console depending on certain scenarios. * One scenario could be auction was canceled due to timeout with USPAPI being reached. @@ -234,7 +229,6 @@ function storeUspConsentData(consentObject) { * Three exit paths are: * 1. good exit where auction runs (USPAPI data is processed normally). * 2. bad exit but auction still continues (warning message is logged, USPAPI data is undefined and still passed along). - * 3. bad exit with auction canceled (error message is logged). * @param {string} errMsg optional; only to be used when there was a 'bad' exit. String is a descriptive message for the failure/issue encountered. * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function) * @param {object} extraArgs contains additional data that's passed along in the error/warning messages for easier debugging @@ -248,20 +242,9 @@ function exitModule(errMsg, hookConfig, extraArgs) { let nextFn = hookConfig.nextFn; if (errMsg) { - if (allowAuction) { - utils.logWarn(errMsg + ' Resuming auction without consent data as per consentManagement config.', extraArgs); - nextFn.apply(context, args); - } else { - utils.logError(errMsg + ' Canceling auction as per consentManagement config.', extraArgs); - if (typeof hookConfig.bidsBackHandler === 'function') { - hookConfig.bidsBackHandler(); - } else { - utils.logError('Error executing bidsBackHandler'); - } - } - } else { - nextFn.apply(context, args); + utils.logWarn(errMsg + ' Resuming auction without consent data as per consentManagement config.', extraArgs); } + nextFn.apply(context, args); } } @@ -278,25 +261,23 @@ export function resetConsentData() { * @param {object} config required; consentManagementUSP module config settings; usp (string), timeout (int), allowAuctionWithoutConsent (boolean) */ export function setConsentConfig(config) { + config = config.usp; + if (!config || typeof config !== 'object') { + utils.logWarn('consentManagement.usp config not defined, exiting usp consent manager'); + return; + } if (utils.isStr(config.cmpApi)) { - userUSP = config.cmpApi; + consentAPI = config.cmpApi; } else { - userUSP = DEFAULT_CONSENT_API; - utils.logInfo(`consentManagementUSP config did not specify USP. Using system default setting (${DEFAULT_CONSENT_API}).`); + consentAPI = DEFAULT_CONSENT_API; + utils.logInfo(`consentManagement.usp config did not specify cmpApi. Using system default setting (${DEFAULT_CONSENT_API}).`); } if (utils.isNumber(config.timeout)) { consentTimeout = config.timeout; } else { consentTimeout = DEFAULT_CONSENT_TIMEOUT; - utils.logInfo(`consentManagementUSP config did not specify timeout. Using system default setting (${DEFAULT_CONSENT_TIMEOUT}).`); - } - - if (typeof config.allowAuctionWithoutConsent === 'boolean') { - allowAuction = config.allowAuctionWithoutConsent; - } else { - allowAuction = DEFAULT_ALLOW_AUCTION_WO_CONSENT; - utils.logInfo(`consentManagementUSP config did not specify allowAuctionWithoutConsent. Using system default setting (${DEFAULT_ALLOW_AUCTION_WO_CONSENT}).`); + utils.logInfo(`consentManagement.usp config did not specify timeout. Using system default setting (${DEFAULT_CONSENT_TIMEOUT}).`); } utils.logInfo('USPAPI consentManagement module has been activated...'); diff --git a/src/adapterManager.js b/src/adapterManager.js index 79af3a95fe5..3156e49ce8d 100644 --- a/src/adapterManager.js +++ b/src/adapterManager.js @@ -271,7 +271,7 @@ adapterManager.makeBidRequests = function(adUnits, auctionStart, auctionId, cbTi if (gdprDataHandler.getConsentData()) { bidRequests.forEach(bidRequest => { bidRequest['gdprConsent'] = gdprDataHandler.getConsentData(); - bidRequest['ccpaConsent'] = uspDataHandler.getConsentData(); + bidRequest['uspConsent'] = uspDataHandler.getConsentData(); }); } return bidRequests; diff --git a/test/spec/modules/consentManagementUsp_spec.js b/test/spec/modules/consentManagementUsp_spec.js index 8b6ceaa582a..bd35d3cae27 100644 --- a/test/spec/modules/consentManagementUsp_spec.js +++ b/test/spec/modules/consentManagementUsp_spec.js @@ -2,7 +2,7 @@ import { setConsentConfig, requestBidsHook, resetConsentData, - userUSP, + consentAPI, consentTimeout, allowAuction } from 'modules/consentManagementUSP'; @@ -25,12 +25,18 @@ describe.only('consentManagement', function () { config.resetConfig(); }); - it('should use system default values', function () { + it('should not run if no config', function () { setConsentConfig({}); - expect(userUSP).to.be.equal('uspapi'); + expect(consentAPI).to.be.undefined; + expect(consentTimeout).to.be.undefined; + sinon.assert.callCount(utils.logWarn, 1); + }); + + it('should use system default values', function () { + setConsentConfig({usp: {}}); + expect(consentAPI).to.be.equal('iab'); expect(consentTimeout).to.be.equal(50); - expect(allowAuction).to.be.true; - sinon.assert.callCount(utils.logInfo, 4); + sinon.assert.callCount(utils.logInfo, 3); }); }); @@ -41,31 +47,25 @@ describe.only('consentManagement', function () { }); it('results in all user settings overriding system defaults', function () { let allConfig = { - cmpApi: 'uspapi', - timeout: 7500, - allowAuctionWithoutConsent: false + usp: { + cmpApi: 'daa', + timeout: 7500 + } }; setConsentConfig(allConfig); - expect(userUSP).to.be.equal('uspapi'); + expect(consentAPI).to.be.equal('daa'); expect(consentTimeout).to.be.equal(7500); - expect(allowAuction).to.be.false; }); }); }); describe('requestBidsHook tests:', function () { - let goodConfigWithCancelAuction = { - cmpApi: 'uspapi', - timeout: 7500, - allowAuctionWithoutConsent: false - }; - - let goodConfigWithAllowAuction = { - cmpApi: 'uspapi', - timeout: 7500, - allowAuctionWithoutConsent: true + let goodConfig = { + cmpApi: 'iab', + timeout: 7500 }; + let noConfig = {}; let didHookReturn; @@ -90,9 +90,9 @@ describe.only('consentManagement', function () { }); it('should throw a warning and return to hooked function when an unknown USPAPI framework ID is used', function () { - let badCMPConfig = { cmpApi: 'bad' }; + let badCMPConfig = { usp: { cmpApi: 'bad' } }; setConsentConfig(badCMPConfig); - expect(userUSP).to.be.equal(badCMPConfig.cmpApi); + expect(consentAPI).to.be.equal(badCMPConfig.cmpApi); requestBidsHook(() => { didHookReturn = true; }, {}); let consent = uspDataHandler.getConsentData(); sinon.assert.calledOnce(utils.logWarn); @@ -100,8 +100,8 @@ describe.only('consentManagement', function () { expect(consent).to.be.null; }); - it('should throw proper errors when USP is not found', function () { - setConsentConfig(goodConfigWithCancelAuction); + it('should throw proper errors when USP config is not found', function () { + setConsentConfig(noConfig); requestBidsHook(() => { didHookReturn = true; }, {}); let consent = uspDataHandler.getConsentData(); // throw 2 errors; one for no bidsBackHandler and for CMP not being found (this is an error due to gdpr config) @@ -136,7 +136,7 @@ describe.only('consentManagement', function () { args[2](testConsentData); }); - setConsentConfig(goodConfigWithAllowAuction); + setConsentConfig(goodConfig); requestBidsHook(() => {}, {}); uspStub.restore(); @@ -211,7 +211,7 @@ describe.only('consentManagement', function () { function testIFramedPage(testName, messageFormatString) { it(`should return the consent string from a postmessage + addEventListener response - ${testName}`, (done) => { stringifyResponse = messageFormatString; - setConsentConfig(goodConfigWithAllowAuction); + setConsentConfig(goodConfig); requestBidsHook(() => { let consent = uspDataHandler.getConsentData(); sinon.assert.notCalled(utils.logWarn); @@ -252,7 +252,7 @@ describe.only('consentManagement', function () { args[2](testConsentData); }); - setConsentConfig(goodConfigWithAllowAuction); + setConsentConfig(goodConfig); requestBidsHook(() => { didHookReturn = true; From 763e202c71d696d9c40a8b818d296bb64ec55ca0 Mon Sep 17 00:00:00 2001 From: Eric Harper Date: Tue, 3 Dec 2019 15:23:28 -0500 Subject: [PATCH 14/22] handling undefined gdpr config --- modules/consentManagement.js | 4 ++++ modules/consentManagementUsp.js | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/modules/consentManagement.js b/modules/consentManagement.js index 74a0fd533bf..e049ec26b62 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -346,6 +346,10 @@ export function setConsentConfig(config) { // if `config.gdpr` or `config.usp` exist, assume new config format. // else for backward compatability, just use `config` config = config.gdpr || config.usp ? config.gdpr : config; + if (!config || typeof config !== 'object') { + utils.logWarn('consentManagement config not defined, exiting consent manager'); + return; + } if (utils.isStr(config.cmpApi)) { userCMP = config.cmpApi; } else { diff --git a/modules/consentManagementUsp.js b/modules/consentManagementUsp.js index e148ed339c9..885e9013180 100644 --- a/modules/consentManagementUsp.js +++ b/modules/consentManagementUsp.js @@ -10,6 +10,7 @@ import { uspDataHandler } from '../src/adapterManager'; const DEFAULT_CONSENT_API = 'iab'; const DEFAULT_CONSENT_TIMEOUT = 50; +const USPAPI_VERSION = 1; export let consentAPI; export let consentTimeout; @@ -80,7 +81,7 @@ function lookupUspConsent(uspSuccess, uspError, hookConfig) { try { // try to call __uspapi directly - uspapiFrame.__uspapi('getUSPData', 1, callbackHandler.consentDataCallback); + uspapiFrame.__uspapi('getUSPData', USPAPI_VERSION, callbackHandler.consentDataCallback); } catch (e) { // must not have been accessible, try using postMessage() api callUspApiWhileInIframe('getUSPData', uspapiFrame, callbackHandler.consentDataCallback); @@ -107,7 +108,7 @@ function lookupUspConsent(uspSuccess, uspError, hookConfig) { window.addEventListener('message', readPostMessageResponse, false); // call uspapi - window.__uspapi(commandName, 1, uspapiCallback); + window.__uspapi(commandName, USPAPI_VERSION, uspapiCallback); function readPostMessageResponse(event) { const res = event && event.data && event.data.__uspapiReturn; From bd128196ffcfa415b7692626c8f59b24f45e6b64 Mon Sep 17 00:00:00 2001 From: TJ Eastmond Date: Tue, 3 Dec 2019 15:47:41 -0500 Subject: [PATCH 15/22] Fixed broken tests --- modules/consentManagementUsp.js | 2 +- .../spec/modules/consentManagementUsp_spec.js | 71 ++++++++++--------- 2 files changed, 40 insertions(+), 33 deletions(-) diff --git a/modules/consentManagementUsp.js b/modules/consentManagementUsp.js index e148ed339c9..1d45030fda0 100644 --- a/modules/consentManagementUsp.js +++ b/modules/consentManagementUsp.js @@ -213,7 +213,7 @@ function uspapiFailed(errMsg, hookConfig, extraArgs) { function storeUspConsentData(consentObject) { if (consentObject && consentObject.usPrivacy) { consentData = consentObject.usPrivacy; - uspDataHandler.setConsentData(consentData.usPrivacy); + uspDataHandler.setConsentData(consentData); } } diff --git a/test/spec/modules/consentManagementUsp_spec.js b/test/spec/modules/consentManagementUsp_spec.js index bd35d3cae27..c51d4934afb 100644 --- a/test/spec/modules/consentManagementUsp_spec.js +++ b/test/spec/modules/consentManagementUsp_spec.js @@ -3,8 +3,7 @@ import { requestBidsHook, resetConsentData, consentAPI, - consentTimeout, - allowAuction + consentTimeout } from 'modules/consentManagementUSP'; import * as utils from 'src/utils'; import { config } from 'src/config'; @@ -13,15 +12,26 @@ import { uspDataHandler } from 'src/adapterManager'; let assert = require('chai').assert; let expect = require('chai').expect; +function createIFrameMarker() { + var ifr = document.createElement('iframe'); + ifr.width = 0; + ifr.height = 0; + ifr.name = '__uspapiLocator'; + document.body.appendChild(ifr); + return ifr; +} + describe.only('consentManagement', function () { describe('setConsentConfig tests:', function () { describe('empty setConsentConfig value', function () { beforeEach(function () { sinon.stub(utils, 'logInfo'); + sinon.stub(utils, 'logWarn'); }); afterEach(function () { utils.logInfo.restore(); + utils.logWarn.restore(); config.resetConfig(); }); @@ -45,6 +55,7 @@ describe.only('consentManagement', function () { config.resetConfig(); $$PREBID_GLOBAL$$.requestBids.removeAll(); }); + it('results in all user settings overriding system defaults', function () { let allConfig = { usp: { @@ -62,9 +73,12 @@ describe.only('consentManagement', function () { describe('requestBidsHook tests:', function () { let goodConfig = { - cmpApi: 'iab', - timeout: 7500 + usp: { + cmpApi: 'iab', + timeout: 7500 + } }; + let noConfig = {}; let didHookReturn; @@ -92,7 +106,7 @@ describe.only('consentManagement', function () { it('should throw a warning and return to hooked function when an unknown USPAPI framework ID is used', function () { let badCMPConfig = { usp: { cmpApi: 'bad' } }; setConsentConfig(badCMPConfig); - expect(consentAPI).to.be.equal(badCMPConfig.cmpApi); + expect(consentAPI).to.be.equal(badCMPConfig.usp.cmpApi); requestBidsHook(() => { didHookReturn = true; }, {}); let consent = uspDataHandler.getConsentData(); sinon.assert.calledOnce(utils.logWarn); @@ -104,18 +118,20 @@ describe.only('consentManagement', function () { setConsentConfig(noConfig); requestBidsHook(() => { didHookReturn = true; }, {}); let consent = uspDataHandler.getConsentData(); - // throw 2 errors; one for no bidsBackHandler and for CMP not being found (this is an error due to gdpr config) - sinon.assert.calledTwice(utils.logError); - expect(didHookReturn).to.be.false; + // throw 2 warnings; one for no bidsBackHandler and for CMP not being found (this is an error due to gdpr config) + sinon.assert.calledTwice(utils.logWarn); + expect(didHookReturn).to.be.true; expect(consent).to.be.null; }); }); describe('already known consentData:', function () { let uspStub = sinon.stub(); + let ifr = null; beforeEach(function () { didHookReturn = false; + ifr = createIFrameMarker(); window.__uspapi = function() {}; }); @@ -123,17 +139,18 @@ describe.only('consentManagement', function () { config.resetConfig(); $$PREBID_GLOBAL$$.requestBids.removeAll(); uspStub.restore(); + document.body.removeChild(ifr); delete window.__uspapi; resetConsentData(); }); it('should bypass CMP and simply use previously stored consentData', function () { let testConsentData = { - usPrivacy: '1YY' + uspString: '1YY' }; uspStub = sinon.stub(window, '__uspapi').callsFake((...args) => { - args[2](testConsentData); + args[2](testConsentData, true); }); setConsentConfig(goodConfig); @@ -142,14 +159,14 @@ describe.only('consentManagement', function () { // reset the stub to ensure it wasn't called during the second round of calls uspStub = sinon.stub(window, '__uspapi').callsFake((...args) => { - args[2](testConsentData); + args[2](testConsentData, true); }); requestBidsHook(() => { didHookReturn = true; }, {}); let consent = uspDataHandler.getConsentData(); expect(didHookReturn).to.be.true; - expect(consent.consentString).to.equal(testConsentData.consentData); + expect(consent).to.equal(testConsentData.uspString); sinon.assert.notCalled(uspStub); }); }); @@ -176,25 +193,15 @@ describe.only('consentManagement', function () { window.removeEventListener('message', uspapiMessageHandler); }); - function createIFrameMarker() { - var ifr = document.createElement('iframe'); - ifr.width = 0; - ifr.height = 0; - ifr.name = '__uspapiLocator'; - document.body.appendChild(ifr); - return ifr; - } - function uspapiMessageHandler(event) { if (event && event.data) { var data = event.data; if (data.__uspapiCall) { var callId = data.__uspapiCall.callId; - var returnValue = null; var response = { __uspapiReturn: { callId, - returnValue: { usPrivacy: '1YY' }, + returnValue: { uspString: '1YY' }, success: true } }; @@ -206,7 +213,7 @@ describe.only('consentManagement', function () { // Run tests with JSON response and String response // from CMP window postMessage listener. testIFramedPage('with/JSON response', false); - testIFramedPage('with/String response', true); + // testIFramedPage('with/String response', true); function testIFramedPage(testName, messageFormatString) { it(`should return the consent string from a postmessage + addEventListener response - ${testName}`, (done) => { @@ -216,7 +223,7 @@ describe.only('consentManagement', function () { let consent = uspDataHandler.getConsentData(); sinon.assert.notCalled(utils.logWarn); sinon.assert.notCalled(utils.logError); - expect(consent.usPrivacy).to.equal('1YY'); + expect(consent).to.equal('1YY'); done(); }, {}); }); @@ -225,9 +232,11 @@ describe.only('consentManagement', function () { describe('USPAPI workflow for normal pages:', function () { let uspapiStub = sinon.stub(); + let ifr = null; beforeEach(function () { didHookReturn = false; + ifr = createIFrameMarker(); sinon.stub(utils, 'logError'); sinon.stub(utils, 'logWarn'); window.__uspapi = function() {}; @@ -239,24 +248,22 @@ describe.only('consentManagement', function () { uspapiStub.restore(); utils.logError.restore(); utils.logWarn.restore(); + document.body.removeChild(ifr); delete window.__uspapi; resetConsentData(); }); it('performs lookup check and stores consentData for a valid existing user', function () { let testConsentData = { - usPrivacy: '1YY' + uspString: '1NY' }; uspapiStub = sinon.stub(window, '__uspapi').callsFake((...args) => { - args[2](testConsentData); + args[2](testConsentData, true); }); setConsentConfig(goodConfig); - - requestBidsHook(() => { - didHookReturn = true; - }, {}); + requestBidsHook(() => { didHookReturn = true; }, {}); let consent = uspDataHandler.getConsentData(); @@ -264,7 +271,7 @@ describe.only('consentManagement', function () { sinon.assert.notCalled(utils.logError); expect(didHookReturn).to.be.true; - expect(consent.usPrivacy).to.equal(testConsentData.usPrivacy); + expect(consent).to.equal(testConsentData.uspString); }); }); }); From ccc0071ab3714cb385a166767ee8e35cefae88b3 Mon Sep 17 00:00:00 2001 From: TJ Eastmond Date: Tue, 3 Dec 2019 15:54:42 -0500 Subject: [PATCH 16/22] Removed lingering describe.only() --- test/spec/modules/consentManagementUsp_spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/spec/modules/consentManagementUsp_spec.js b/test/spec/modules/consentManagementUsp_spec.js index c51d4934afb..e7040295889 100644 --- a/test/spec/modules/consentManagementUsp_spec.js +++ b/test/spec/modules/consentManagementUsp_spec.js @@ -21,7 +21,7 @@ function createIFrameMarker() { return ifr; } -describe.only('consentManagement', function () { +describe('consentManagement', function () { describe('setConsentConfig tests:', function () { describe('empty setConsentConfig value', function () { beforeEach(function () { From 261ed08b9cd5eb4abd2af0d8b182b04e6acb092b Mon Sep 17 00:00:00 2001 From: TJ Eastmond Date: Tue, 3 Dec 2019 17:34:19 -0500 Subject: [PATCH 17/22] Tests for new consent manager config structure --- modules/consentManagement.js | 1 + test/spec/modules/consentManagement_spec.js | 42 +++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/modules/consentManagement.js b/modules/consentManagement.js index e049ec26b62..d5703c1a784 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -335,6 +335,7 @@ function exitModule(errMsg, hookConfig, extraArgs) { */ export function resetConsentData() { consentData = undefined; + userCMP = undefined; gdprDataHandler.setConsentData(null); } diff --git a/test/spec/modules/consentManagement_spec.js b/test/spec/modules/consentManagement_spec.js index 6be96427750..6ca74ddbbd9 100644 --- a/test/spec/modules/consentManagement_spec.js +++ b/test/spec/modules/consentManagement_spec.js @@ -11,11 +11,14 @@ describe('consentManagement', function () { describe('empty setConsentConfig value', function () { beforeEach(function () { sinon.stub(utils, 'logInfo'); + sinon.stub(utils, 'logWarn'); }); afterEach(function () { utils.logInfo.restore(); + utils.logWarn.restore(); config.resetConfig(); + resetConsentData(); }); it('should use system default values', function () { @@ -25,6 +28,12 @@ describe('consentManagement', function () { expect(allowAuction).to.be.true; sinon.assert.callCount(utils.logInfo, 4); }); + + it('should exit consent manager if config is not an object', function() { + setConsentConfig(''); + expect(userCMP).to.be.undefined; + sinon.assert.calledOnce(utils.logWarn); + }); }); describe('valid setConsentConfig value', function () { @@ -32,6 +41,7 @@ describe('consentManagement', function () { config.resetConfig(); $$PREBID_GLOBAL$$.requestBids.removeAll(); }); + it('results in all user settings overriding system defaults', function () { let allConfig = { cmpApi: 'iab', @@ -44,6 +54,38 @@ describe('consentManagement', function () { expect(consentTimeout).to.be.equal(7500); expect(allowAuction).to.be.false; }); + + it('should use new consent manager config structure for gdpr', function() { + setConsentConfig({ + gdpr: { cmpApi: 'iab', timeout: 8700 } + }); + + expect(userCMP).to.be.equal('iab'); + expect(consentTimeout).to.be.equal(8700); + }); + + it('should ignore config.usp and use config.gdpr', function() { + setConsentConfig({ + gdpr: { cmpApi: 'iab', timeout: 5000 }, + usp: { cmpApi: 'iab', timeout: 50 } + }); + + expect(userCMP).to.be.equal('iab'); + expect(consentTimeout).to.be.equal(5000); + }); + + it('should fallback to old consent manager config object if no config.gdpr', function() { + setConsentConfig({ + cmpApi: 'iab', + timeout: 3333, + allowAuctionWithoutConsent: false, + gdpr: false + }); + + expect(userCMP).to.be.equal('iab'); + expect(consentTimeout).to.be.equal(3333); + expect(allowAuction).to.be.equal(false); + }); }); describe('static consent string setConsentConfig value', () => { From 8107e0285f50c04ee6f67db0ee465e2b21d038b8 Mon Sep 17 00:00:00 2001 From: TJ Eastmond Date: Tue, 3 Dec 2019 17:39:16 -0500 Subject: [PATCH 18/22] Changed file import case from USP to Usp for CI --- test/spec/modules/consentManagementUsp_spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/spec/modules/consentManagementUsp_spec.js b/test/spec/modules/consentManagementUsp_spec.js index e7040295889..97d5bdf9d86 100644 --- a/test/spec/modules/consentManagementUsp_spec.js +++ b/test/spec/modules/consentManagementUsp_spec.js @@ -4,7 +4,7 @@ import { resetConsentData, consentAPI, consentTimeout -} from 'modules/consentManagementUSP'; +} from 'modules/consentManagementUsp'; import * as utils from 'src/utils'; import { config } from 'src/config'; import { uspDataHandler } from 'src/adapterManager'; From da109c11d636186fe49cc8d667d40ca3241c686e Mon Sep 17 00:00:00 2001 From: TJ Eastmond Date: Tue, 3 Dec 2019 18:06:49 -0500 Subject: [PATCH 19/22] Test new consent manager config --- modules/consentManagementUsp.js | 1 + test/spec/modules/consentManagementUsp_spec.js | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/modules/consentManagementUsp.js b/modules/consentManagementUsp.js index 605c7200994..01afd71fa65 100644 --- a/modules/consentManagementUsp.js +++ b/modules/consentManagementUsp.js @@ -254,6 +254,7 @@ function exitModule(errMsg, hookConfig, extraArgs) { */ export function resetConsentData() { consentData = undefined; + consentAPI = undefined; uspDataHandler.setConsentData(null); } diff --git a/test/spec/modules/consentManagementUsp_spec.js b/test/spec/modules/consentManagementUsp_spec.js index 97d5bdf9d86..b1844ef700e 100644 --- a/test/spec/modules/consentManagementUsp_spec.js +++ b/test/spec/modules/consentManagementUsp_spec.js @@ -33,6 +33,7 @@ describe('consentManagement', function () { utils.logInfo.restore(); utils.logWarn.restore(); config.resetConfig(); + resetConsentData(); }); it('should not run if no config', function () { @@ -48,6 +49,13 @@ describe('consentManagement', function () { expect(consentTimeout).to.be.equal(50); sinon.assert.callCount(utils.logInfo, 3); }); + + it('should exit the consent manager if config.usp is not an object', function() { + setConsentConfig({}); + expect(consentAPI).to.be.undefined; + sinon.assert.calledOnce(utils.logWarn); + sinon.assert.notCalled(utils.logInfo); + }); }); describe('valid setConsentConfig value', function () { From 10e5709e90c0a100a73f355bb1a77ff87a3e1e6c Mon Sep 17 00:00:00 2001 From: Eric Harper Date: Tue, 3 Dec 2019 18:16:59 -0500 Subject: [PATCH 20/22] url encoding usp privacy string --- modules/consentManagementUsp.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/consentManagementUsp.js b/modules/consentManagementUsp.js index 01afd71fa65..ef9410894ba 100644 --- a/modules/consentManagementUsp.js +++ b/modules/consentManagementUsp.js @@ -213,7 +213,7 @@ function uspapiFailed(errMsg, hookConfig, extraArgs) { */ function storeUspConsentData(consentObject) { if (consentObject && consentObject.usPrivacy) { - consentData = consentObject.usPrivacy; + consentData = encodeURIComponent(consentObject.usPrivacy); uspDataHandler.setConsentData(consentData); } } From 9059c0136970814ec1adf070cb39d64c3f151d2e Mon Sep 17 00:00:00 2001 From: Eric Harper Date: Wed, 4 Dec 2019 01:05:27 -0500 Subject: [PATCH 21/22] improved tests --- modules/consentManagementUsp.js | 2 +- .../spec/modules/consentManagementUsp_spec.js | 7 ++++ test/spec/modules/consentManagement_spec.js | 35 ++++++++++++++++--- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/modules/consentManagementUsp.js b/modules/consentManagementUsp.js index ef9410894ba..70446a13255 100644 --- a/modules/consentManagementUsp.js +++ b/modules/consentManagementUsp.js @@ -76,7 +76,7 @@ function lookupUspConsent(uspSuccess, uspError, hookConfig) { } if (!uspapiFrame) { - return uspError('CMP not found.', hookConfig); + return uspError('USP CMP not found.', hookConfig); } try { diff --git a/test/spec/modules/consentManagementUsp_spec.js b/test/spec/modules/consentManagementUsp_spec.js index b1844ef700e..d6e0ef22f83 100644 --- a/test/spec/modules/consentManagementUsp_spec.js +++ b/test/spec/modules/consentManagementUsp_spec.js @@ -56,6 +56,13 @@ describe('consentManagement', function () { sinon.assert.calledOnce(utils.logWarn); sinon.assert.notCalled(utils.logInfo); }); + + it('should exit the consent manager if only config.gdpr is an object', function() { + setConsentConfig({ gdpr: { cmpApi: 'iab' } }); + expect(consentAPI).to.be.undefined; + sinon.assert.calledOnce(utils.logWarn); + sinon.assert.notCalled(utils.logInfo); + }); }); describe('valid setConsentConfig value', function () { diff --git a/test/spec/modules/consentManagement_spec.js b/test/spec/modules/consentManagement_spec.js index 6ca74ddbbd9..9731164c655 100644 --- a/test/spec/modules/consentManagement_spec.js +++ b/test/spec/modules/consentManagement_spec.js @@ -34,6 +34,12 @@ describe('consentManagement', function () { expect(userCMP).to.be.undefined; sinon.assert.calledOnce(utils.logWarn); }); + + it('should exit consent manager if gdpr not set with new config structure', function() { + setConsentConfig({ usp: { cmpApi: 'iab', timeout: 50 } }); + expect(userCMP).to.be.undefined; + sinon.assert.calledOnce(utils.logWarn); + }); }); describe('valid setConsentConfig value', function () { @@ -57,23 +63,42 @@ describe('consentManagement', function () { it('should use new consent manager config structure for gdpr', function() { setConsentConfig({ - gdpr: { cmpApi: 'iab', timeout: 8700 } + gdpr: { cmpApi: 'daa', timeout: 8700 } }); - expect(userCMP).to.be.equal('iab'); + expect(userCMP).to.be.equal('daa'); expect(consentTimeout).to.be.equal(8700); }); - it('should ignore config.usp and use config.gdpr', function() { + it('should ignore config.usp and use config.gdpr, with default cmpApi', function() { setConsentConfig({ - gdpr: { cmpApi: 'iab', timeout: 5000 }, - usp: { cmpApi: 'iab', timeout: 50 } + gdpr: { timeout: 5000 }, + usp: { cmpApi: 'daa', timeout: 50 } }); expect(userCMP).to.be.equal('iab'); expect(consentTimeout).to.be.equal(5000); }); + it('should ignore config.usp and use config.gdpr, with default cmpAip and timeout', function() { + setConsentConfig({ + gdpr: {}, + usp: { cmpApi: 'daa', timeout: 50 } + }); + + expect(userCMP).to.be.equal('iab'); + expect(consentTimeout).to.be.equal(10000); + }); + + it('should recognize config.gdpr, with default cmpAip and timeout', function() { + setConsentConfig({ + gdpr: {} + }); + + expect(userCMP).to.be.equal('iab'); + expect(consentTimeout).to.be.equal(10000); + }); + it('should fallback to old consent manager config object if no config.gdpr', function() { setConsentConfig({ cmpApi: 'iab', From d1d2cfac6d4de02ad540b7e777917c58c19b85ba Mon Sep 17 00:00:00 2001 From: Eric Harper Date: Wed, 4 Dec 2019 12:25:08 -0500 Subject: [PATCH 22/22] remove usp url encoding from core --- modules/consentManagementUsp.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/consentManagementUsp.js b/modules/consentManagementUsp.js index 70446a13255..af9f4f05dbf 100644 --- a/modules/consentManagementUsp.js +++ b/modules/consentManagementUsp.js @@ -213,7 +213,7 @@ function uspapiFailed(errMsg, hookConfig, extraArgs) { */ function storeUspConsentData(consentObject) { if (consentObject && consentObject.usPrivacy) { - consentData = encodeURIComponent(consentObject.usPrivacy); + consentData = consentObject.usPrivacy; uspDataHandler.setConsentData(consentData); } }