From 43ac01e51d25cf92d6036009cf41208285c2a747 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Mon, 2 May 2022 09:33:26 -0700 Subject: [PATCH 1/8] Prebid 7: remove support for TCFv1 --- modules/consentManagement.js | 291 ++++---------------- test/spec/modules/consentManagement_spec.js | 256 ++--------------- 2 files changed, 88 insertions(+), 459 deletions(-) diff --git a/modules/consentManagement.js b/modules/consentManagement.js index 5fbcc0f8ac1..e02e8ddb8e6 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -4,25 +4,20 @@ * and make it available for any GDPR supported adapters to read/pass this information to * their system. */ -import {getAdUnitSizes, isFn, isNumber, isPlainObject, isStr, logError, logInfo, logWarn} from '../src/utils.js'; +import {isFn, isNumber, isPlainObject, isStr, logError, logInfo, logWarn} from '../src/utils.js'; import {config} from '../src/config.js'; import {gdprDataHandler} from '../src/adapterManager.js'; import {includes} from '../src/polyfill.js'; const DEFAULT_CMP = 'iab'; const DEFAULT_CONSENT_TIMEOUT = 10000; -const DEFAULT_ALLOW_AUCTION_WO_CONSENT = true; +const CMP_VERSION = 2; -export const allowAuction = { - value: DEFAULT_ALLOW_AUCTION_WO_CONSENT, - definedInConfig: false -} export let userCMP; export let consentTimeout; export let gdprScope; export let staticConsentData; -let cmpVersion = 0; let consentData; let addedConsentHook = false; @@ -46,24 +41,16 @@ function lookupStaticConsentData({onSuccess, onError}) { * based on the appropriate result. * @param {function({})} onSuccess acts as a success callback when CMP returns a value; pass along consentObjectfrom CMP * @param {function(string, ...{}?)} cmpError acts as an error callback while interacting with CMP; pass along an error message (string) and any extra error arguments (purely for logging) - * @param width - * @param height size info passed to the SafeFrame API (used only for TCFv1 when Prebid is running within a safeframe) */ -function lookupIabConsent({onSuccess, onError, width, height}) { +function lookupIabConsent({onSuccess, onError}) { function findCMP() { let f = window; let cmpFrame; let cmpFunction; - while (!cmpFrame) { + while (true) { try { - if (typeof f.__tcfapi === 'function' || typeof f.__cmp === 'function') { - if (typeof f.__tcfapi === 'function') { - cmpVersion = 2; - cmpFunction = f.__tcfapi; - } else { - cmpVersion = 1; - cmpFunction = f.__cmp; - } + if (typeof f.__tcfapi === 'function') { + cmpFunction = f.__tcfapi; cmpFrame = f; break; } @@ -72,15 +59,6 @@ function lookupIabConsent({onSuccess, onError, width, height}) { // need separate try/catch blocks due to the exception errors thrown when trying to check for a frame that doesn't exist in 3rd party env try { if (f.frames['__tcfapiLocator']) { - cmpVersion = 2; - cmpFrame = f; - break; - } - } catch (e) { } - - try { - if (f.frames['__cmpLocator']) { - cmpVersion = 1; cmpFrame = f; break; } @@ -106,29 +84,6 @@ function lookupIabConsent({onSuccess, onError, width, height}) { } } - function handleV1CmpResponseCallbacks() { - const cmpResponse = {}; - - function afterEach() { - if (cmpResponse.getConsentData && cmpResponse.getVendorConsents) { - logInfo('Received all requested responses from CMP', cmpResponse); - processCmpData(cmpResponse, {onSuccess, onError}); - } - } - - return { - consentDataCallback: function (consentResponse) { - cmpResponse.getConsentData = consentResponse; - afterEach(); - }, - vendorConsentsCallback: function (consentResponse) { - cmpResponse.getVendorConsents = consentResponse; - afterEach(); - } - } - } - - let v1CallbackHandler = handleV1CmpResponseCallbacks(); let cmpCallbacks = {}; let { cmpFrame, cmpFunction } = findCMP(); @@ -147,92 +102,39 @@ function lookupIabConsent({onSuccess, onError, width, height}) { if (isFn(cmpFunction)) { logInfo('Detected CMP API is directly accessible, calling it now...'); - if (cmpVersion === 1) { - cmpFunction('getConsentData', null, v1CallbackHandler.consentDataCallback); - cmpFunction('getVendorConsents', null, v1CallbackHandler.vendorConsentsCallback); - } else if (cmpVersion === 2) { - cmpFunction('addEventListener', cmpVersion, v2CmpResponseCallback); - } - } else if (cmpVersion === 1 && inASafeFrame() && typeof window.$sf.ext.cmp === 'function') { - // this safeframe workflow is only supported with TCF v1 spec; the v2 recommends to use the iframe postMessage route instead (even if you are in a safeframe). - logInfo('Detected Prebid.js is encased in a SafeFrame and CMP is registered, calling it now...'); - callCmpWhileInSafeFrame('getConsentData', v1CallbackHandler.consentDataCallback); - callCmpWhileInSafeFrame('getVendorConsents', v1CallbackHandler.vendorConsentsCallback); + cmpFunction('addEventListener', CMP_VERSION, v2CmpResponseCallback); } else { logInfo('Detected CMP is outside the current iframe where Prebid.js is located, calling it now...'); - if (cmpVersion === 1) { - callCmpWhileInIframe('getConsentData', cmpFrame, v1CallbackHandler.consentDataCallback); - callCmpWhileInIframe('getVendorConsents', cmpFrame, v1CallbackHandler.vendorConsentsCallback); - } else if (cmpVersion === 2) { - callCmpWhileInIframe('addEventListener', cmpFrame, v2CmpResponseCallback); - } - } - - function inASafeFrame() { - return !!(window.$sf && window.$sf.ext); - } - - function callCmpWhileInSafeFrame(commandName, callback) { - function sfCallback(msgName, data) { - if (msgName === 'cmpReturn') { - let responseObj = (commandName === 'getConsentData') ? data.vendorConsentData : data.vendorConsents; - callback(responseObj); - } - } - - window.$sf.ext.register(width, height, sfCallback); - window.$sf.ext.cmp(commandName); + callCmpWhileInIframe('addEventListener', cmpFrame, v2CmpResponseCallback); } function callCmpWhileInIframe(commandName, cmpFrame, moduleCallback) { - let apiName = (cmpVersion === 2) ? '__tcfapi' : '__cmp'; + const apiName = '__tcfapi'; - let callName = `${apiName}Call`; + const callName = `${apiName}Call`; /* 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 */ - if (cmpVersion === 2) { - window[apiName] = function (cmd, cmpVersion, callback, arg) { - let callId = Math.random() + ''; - let msg = { - [callName]: { - command: cmd, - version: cmpVersion, - parameter: arg, - callId: callId - } - }; - - cmpCallbacks[callId] = callback; - cmpFrame.postMessage(msg, '*'); - } - - /** when we get the return message, call the stashed callback */ - window.addEventListener('message', readPostMessageResponse, false); + window[apiName] = function (cmd, cmpVersion, callback, arg) { + let callId = Math.random() + ''; + let msg = { + [callName]: { + command: cmd, + version: cmpVersion, + parameter: arg, + callId: callId + } + }; - // call CMP - window[apiName](commandName, cmpVersion, moduleCallback); - } else { - window[apiName] = function (cmd, arg, callback) { - let callId = Math.random() + ''; - let msg = { - [callName]: { - command: cmd, - parameter: arg, - callId: callId - } - }; - - cmpCallbacks[callId] = callback; - cmpFrame.postMessage(msg, '*'); - } + cmpCallbacks[callId] = callback; + cmpFrame.postMessage(msg, '*'); + } - /** when we get the return message, call the stashed callback */ - window.addEventListener('message', readPostMessageResponse, false); + /** when we get the return message, call the stashed callback */ + window.addEventListener('message', readPostMessageResponse, false); - // call CMP - window[apiName](commandName, undefined, moduleCallback); - } + // call CMP + window[apiName](commandName, CMP_VERSION, moduleCallback); function readPostMessageResponse(event) { let cmpDataPkgName = `${apiName}Return`; @@ -253,11 +155,8 @@ function lookupIabConsent({onSuccess, onError, width, height}) { * * @param cb A callback that takes: a boolean that is true if the auction should be canceled; an error message and extra * error arguments that will be undefined if there's no error. - * @param width if we are running in an iframe, the TCFv1 spec requires us to use the SafeFrame API to find the CMP - which - * in turn requires width and height. - * @param height see width above */ -function loadConsentData(cb, width = 1, height = 1) { +function loadConsentData(cb) { let isDone = false; let timer = null; @@ -282,36 +181,37 @@ function loadConsentData(cb, width = 1, height = 1) { onError: function (msg, ...extraArgs) { let consentData = null; let shouldCancelAuction = true; - if (allowAuction.value && cmpVersion === 1) { - // still set the consentData to undefined when there is a problem as per config options - consentData = storeConsentData(undefined); - shouldCancelAuction = false; - } done(consentData, shouldCancelAuction, msg, ...extraArgs); } } - cmpCallMap[userCMP]({ - width, - height, - ...callbacks - }); + cmpCallMap[userCMP](callbacks); if (!isDone) { if (consentTimeout === 0) { processCmpData(undefined, callbacks); } else { timer = setTimeout(function () { - if (cmpVersion === 2) { - // for TCFv2, we allow the auction to continue on timeout - done(storeConsentData(undefined), false, `No response from CMP, continuing auction...`) - } else { - callbacks.onError('CMP workflow exceeded timeout threshold.'); - } + // on timeout, allow the auction to continue + done(storeConsentData(undefined), false, `No response from CMP, continuing auction...`) }, consentTimeout); } } } +/** + * Like `loadConsentData`, but cache and re-use previously loaded data. + * @param cb + */ +function loadIfMissing(cb) { + if (consentData) { + logInfo('User consent information already known. Pulling internally stored information...'); + // eslint-disable-next-line standard/no-callback-literal + cb(false); + } else { + loadConsentData(cb); + } +} + /** * 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 @@ -321,36 +221,10 @@ function loadConsentData(cb, width = 1, height = 1) { * @param {function} fn required; The next function in the chain, used by hook.js */ export function requestBidsHook(fn, reqBidsConfigObj) { - const load = (() => { - if (consentData) { - logInfo('User consent information already known. Pulling internally stored information...'); - return function (cb) { - // eslint-disable-next-line standard/no-callback-literal - cb(false); - } - } else { - // find sizes from adUnits object - let adUnits = reqBidsConfigObj.adUnits || $$PREBID_GLOBAL$$.adUnits; - let width = 1; - let height = 1; - if (Array.isArray(adUnits) && adUnits.length > 0) { - let sizes = getAdUnitSizes(adUnits[0]); - width = sizes?.[0]?.[0] || 1; - height = sizes?.[0]?.[1] || 1; - } - - return function (cb) { - loadConsentData(cb, width, height); - } - } - })(); - - load(function (shouldCancelAuction, errMsg, ...extraArgs) { + loadIfMissing(function (shouldCancelAuction, errMsg, ...extraArgs) { if (errMsg) { let log = logWarn; - if (cmpVersion === 1 && !shouldCancelAuction) { - errMsg = `${errMsg} 'allowAuctionWithoutConsent' activated.`; - } else if (shouldCancelAuction) { + if (shouldCancelAuction) { log = logError; errMsg = `${errMsg} Canceling auction as per consentManagement config.`; } @@ -375,20 +249,7 @@ export function requestBidsHook(fn, reqBidsConfigObj) { * If it's good, then we store the value and call `onSuccess` */ function processCmpData(consentObject, {onSuccess, onError}) { - function checkV1Data(consentObject) { - let gdprApplies = consentObject && consentObject.getConsentData && consentObject.getConsentData.gdprApplies; - return !!( - (typeof gdprApplies !== 'boolean') || - (gdprApplies === true && - !(isStr(consentObject.getConsentData.consentData) && - isPlainObject(consentObject.getVendorConsents) && - Object.keys(consentObject.getVendorConsents).length > 1 - ) - ) - ); - } - - function checkV2Data() { + function checkData() { // if CMP does not respond with a gdprApplies boolean, use defaultGdprScope (gdprScope) let gdprApplies = consentObject && typeof consentObject.gdprApplies === 'boolean' ? consentObject.gdprApplies : gdprScope; let tcString = consentObject && consentObject.tcString; @@ -400,24 +261,13 @@ function processCmpData(consentObject, {onSuccess, onError}) { // do extra things for static config if (userCMP === 'static') { - cmpVersion = (consentObject.getConsentData) ? 1 : (consentObject.getTCData) ? 2 : 0; - // remove extra layer in static v2 data object so it matches normal v2 CMP object for processing step - if (cmpVersion === 2) { - consentObject = consentObject.getTCData; - } + consentObject = consentObject.getTCData; } - // determine which set of checks to run based on cmpVersion - let checkFn = (cmpVersion === 1) ? checkV1Data : (cmpVersion === 2) ? checkV2Data : null; - - if (isFn(checkFn)) { - if (checkFn(consentObject)) { - onError(`CMP returned unexpected value during lookup process.`, consentObject); - } else { - onSuccess(storeConsentData(consentObject)); - } + if (checkData(consentObject)) { + onError(`CMP returned unexpected value during lookup process.`, consentObject); } else { - onError('Unable to derive CMP version to process data. Consent object does not conform to TCF v1 or v2 specs.', consentObject); + onSuccess(storeConsentData(consentObject)); } } @@ -426,23 +276,15 @@ function processCmpData(consentObject, {onSuccess, onError}) { * @param {object} cmpConsentObject required; an object representing user's consent choices (can be undefined in certain use-cases for this function only) */ function storeConsentData(cmpConsentObject) { - if (cmpVersion === 1) { - consentData = { - consentString: (cmpConsentObject) ? cmpConsentObject.getConsentData.consentData : undefined, - vendorData: (cmpConsentObject) ? cmpConsentObject.getVendorConsents : undefined, - gdprApplies: (cmpConsentObject) ? cmpConsentObject.getConsentData.gdprApplies : gdprScope - }; - } else { - consentData = { - consentString: (cmpConsentObject) ? cmpConsentObject.tcString : undefined, - vendorData: (cmpConsentObject) || undefined, - gdprApplies: cmpConsentObject && typeof cmpConsentObject.gdprApplies === 'boolean' ? cmpConsentObject.gdprApplies : gdprScope - }; - if (cmpConsentObject && cmpConsentObject.addtlConsent && isStr(cmpConsentObject.addtlConsent)) { - consentData.addtlConsent = cmpConsentObject.addtlConsent; - }; + consentData = { + consentString: (cmpConsentObject) ? cmpConsentObject.tcString : undefined, + vendorData: (cmpConsentObject) || undefined, + gdprApplies: cmpConsentObject && typeof cmpConsentObject.gdprApplies === 'boolean' ? cmpConsentObject.gdprApplies : gdprScope + }; + if (cmpConsentObject && cmpConsentObject.addtlConsent && isStr(cmpConsentObject.addtlConsent)) { + consentData.addtlConsent = cmpConsentObject.addtlConsent; } - consentData.apiVersion = cmpVersion; + consentData.apiVersion = CMP_VERSION; return consentData; } @@ -452,7 +294,6 @@ function storeConsentData(cmpConsentObject) { export function resetConsentData() { consentData = undefined; userCMP = undefined; - cmpVersion = 0; gdprDataHandler.reset(); } @@ -482,11 +323,6 @@ export function setConsentConfig(config) { logInfo(`consentManagement config did not specify timeout. Using system default setting (${DEFAULT_CONSENT_TIMEOUT}).`); } - if (typeof config.allowAuctionWithoutConsent === 'boolean') { - allowAuction.value = config.allowAuctionWithoutConsent; - allowAuction.definedInConfig = true; - } - // if true, then gdprApplies should be set to true gdprScope = config.defaultGdprScope === true; @@ -506,12 +342,5 @@ export function setConsentConfig(config) { addedConsentHook = true; gdprDataHandler.enable(); loadConsentData(); // immediately look up consent data to make it available without requiring an auction - - // Raise deprecation warning if 'allowAuctionWithoutConsent' is used with TCF 2. - if (allowAuction.definedInConfig && cmpVersion === 2) { - logWarn(`'allowAuctionWithoutConsent' ignored for TCF 2`); - } else if (!allowAuction.definedInConfig && cmpVersion === 1) { - logInfo(`'allowAuctionWithoutConsent' using system default: (${DEFAULT_ALLOW_AUCTION_WO_CONSENT}).`); - } } config.getConfig('consentManagement', config => setConsentConfig(config.consentManagement)); diff --git a/test/spec/modules/consentManagement_spec.js b/test/spec/modules/consentManagement_spec.js index b0cd0197f8b..75b2204104e 100644 --- a/test/spec/modules/consentManagement_spec.js +++ b/test/spec/modules/consentManagement_spec.js @@ -1,4 +1,4 @@ -import { setConsentConfig, requestBidsHook, resetConsentData, userCMP, consentTimeout, allowAuction, staticConsentData, gdprScope } from 'modules/consentManagement.js'; +import { setConsentConfig, requestBidsHook, resetConsentData, userCMP, consentTimeout, staticConsentData, gdprScope } from 'modules/consentManagement.js'; import { gdprDataHandler } from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; import { config } from 'src/config.js'; @@ -69,17 +69,12 @@ describe('consentManagement', function () { let allConfig = { cmpApi: 'iab', timeout: 7500, - allowAuctionWithoutConsent: false, defaultGdprScope: true }; setConsentConfig(allConfig); expect(userCMP).to.be.equal('iab'); expect(consentTimeout).to.be.equal(7500); - expect(allowAuction).to.deep.equal({ - value: false, - definedInConfig: true - }); expect(gdprScope).to.be.true; }); @@ -125,16 +120,11 @@ describe('consentManagement', 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.deep.equal({ - value: false, - definedInConfig: true - }); expect(gdprScope).to.be.equal(false); }); @@ -148,63 +138,11 @@ describe('consentManagement', function () { afterEach(() => { config.resetConfig(); }); - it('results in user settings overriding system defaults for v1 spec', () => { - let staticConfig = { - cmpApi: 'static', - timeout: 7500, - allowAuctionWithoutConsent: false, - consentData: { - getConsentData: { - 'gdprApplies': true, - 'hasGlobalScope': false, - 'consentData': 'BOOgjO9OOgjO9APABAENAi-AAAAWd7_______9____7_9uz_Gv_r_ff_3nW0739P1A_r_Oz_rm_-zzV44_lpQQRCEA' - }, - getVendorConsents: { - 'metadata': 'BOOgjO9OOgjO9APABAENAi-AAAAWd7_______9____7_9uz_Gv_r_ff_3nW0739P1A_r_Oz_rm_-zzV44_lpQQRCEA', - 'gdprApplies': true, - 'hasGlobalScope': false, - 'isEU': true, - 'cookieVersion': 1, - 'created': '2018-05-29T07:45:48.522Z', - 'lastUpdated': '2018-05-29T07:45:48.522Z', - 'cmpId': 15, - 'cmpVersion': 1, - 'consentLanguage': 'EN', - 'vendorListVersion': 34, - 'maxVendorId': 359, - 'purposeConsents': { - '1': true, - '2': true, - '3': true, - '4': true, - '5': true - }, - 'vendorConsents': { - '1': true, - '2': true, - '3': true, - '4': true, - '5': false - } - } - } - }; - - 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.deep.equal({ - value: false, - definedInConfig: true - }); - expect(staticConsentData).to.be.equal(staticConfig.consentData); - }); it('results in user settings overriding system defaults for v2 spec', () => { let staticConfig = { cmpApi: 'static', timeout: 7500, - allowAuctionWithoutConsent: false, consentData: { getTCData: { 'tcString': 'COuqj-POu90rDBcBkBENAZCgAPzAAAPAACiQFwwBAABAA1ADEAbQC4YAYAAgAxAG0A', @@ -276,10 +214,6 @@ describe('consentManagement', function () { 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.deep.equal({ - value: false, - definedInConfig: true - }); expect(gdprScope).to.be.equal(false); const consent = gdprDataHandler.getConsentData(); expect(consent.consentString).to.eql(staticConfig.consentData.getTCData.tcString); @@ -290,16 +224,9 @@ describe('consentManagement', function () { }); describe('requestBidsHook tests:', function () { - let goodConfigWithCancelAuction = { + let goodConfig = { cmpApi: 'iab', timeout: 7500, - allowAuctionWithoutConsent: false - }; - - let goodConfigWithAllowAuction = { - cmpApi: 'iab', - timeout: 7500, - allowAuctionWithoutConsent: true }; const staticConfig = { @@ -312,10 +239,8 @@ describe('consentManagement', function () { let didHookReturn; - afterEach(function () { - gdprDataHandler.consentData = null; - resetConsentData(); - }); + beforeEach(resetConsentData); + after(resetConsentData) describe('error checks:', function () { beforeEach(function () { @@ -328,7 +253,6 @@ describe('consentManagement', function () { utils.logWarn.restore(); utils.logError.restore(); config.resetConfig(); - resetConsentData(); }); it('should throw a warning and return to hooked function when an unknown CMP framework ID is used', function () { @@ -356,7 +280,7 @@ describe('consentManagement', function () { }) it('should throw proper errors when CMP is not found', function () { - setConsentConfig(goodConfigWithCancelAuction); + setConsentConfig(goodConfig); requestBidsHook(() => { didHookReturn = true; @@ -384,35 +308,34 @@ describe('consentManagement', function () { describe('already known consentData:', function () { let cmpStub = sinon.stub(); + function mockCMP(cmpResponse) { + return function(...args) { + args[2](Object.assign({eventStatus: 'tcloaded'}, cmpResponse), true); + } + } + beforeEach(function () { didHookReturn = false; - window.__cmp = function () { }; + window.__tcfapi = function () { }; }); afterEach(function () { config.resetConfig(); cmpStub.restore(); - delete window.__cmp; + delete window.__tcfapi; resetConsentData(); }); it('should bypass CMP and simply use previously stored consentData', function () { let testConsentData = { gdprApplies: true, - consentData: 'xyz' + tcString: 'xyz', }; - cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => { - args[2](testConsentData); - }); - setConsentConfig(goodConfigWithAllowAuction); + cmpStub = sinon.stub(window, '__tcfapi').callsFake(mockCMP(testConsentData)); + setConsentConfig(goodConfig); requestBidsHook(() => { }, {}); - cmpStub.restore(); - - // reset the stub to ensure it wasn't called during the second round of calls - cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => { - args[2](testConsentData); - }); + cmpStub.reset(); requestBidsHook(() => { didHookReturn = true; @@ -420,7 +343,7 @@ describe('consentManagement', function () { let consent = gdprDataHandler.getConsentData(); expect(didHookReturn).to.be.true; - expect(consent.consentString).to.equal(testConsentData.consentData); + expect(consent.consentString).to.equal(testConsentData.tcString); expect(consent.gdprApplies).to.be.true; sinon.assert.notCalled(cmpStub); }); @@ -428,12 +351,10 @@ describe('consentManagement', function () { it('should not set consent.gdprApplies to true if defaultGdprScope is true', function () { let testConsentData = { gdprApplies: false, - consentData: 'xyz' + tcString: 'xyz', }; - cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => { - args[2](testConsentData); - }); + cmpStub = sinon.stub(window, '__tcfapi').callsFake(mockCMP(testConsentData)); setConsentConfig({ cmpApi: 'iab', @@ -486,7 +407,7 @@ describe('consentManagement', function () { function testIFramedPage(testName, messageFormatString, tarConsentString, ver) { it(`should return the consent string from a postmessage + addEventListener response - ${testName}`, (done) => { stringifyResponse = messageFormatString; - setConsentConfig(goodConfigWithAllowAuction); + setConsentConfig(goodConfig); requestBidsHook(() => { let consent = gdprDataHandler.getConsentData(); sinon.assert.notCalled(utils.logError); @@ -510,93 +431,6 @@ describe('consentManagement', function () { resetConsentData(); }); - describe('v1 CMP workflow for safeframe page', function () { - let registerStub = sinon.stub(); - let ifrSf = null; - beforeEach(function () { - didHookReturn = false; - window.$sf = { - ext: { - register: function () { }, - cmp: function () { } - } - }; - ifrSf = createIFrameMarker('__cmpLocator'); - }); - - afterEach(function () { - delete window.$sf; - registerStub.restore(); - document.body.removeChild(ifrSf); - }); - - it('should return the consent data from a safeframe callback', function () { - let testConsentData = { - data: { - msgName: 'cmpReturn', - vendorConsents: { - metadata: 'abc123def', - gdprApplies: true - }, - vendorConsentData: { - consentData: 'abc123def', - gdprApplies: true - } - } - }; - registerStub = sinon.stub(window.$sf.ext, 'register').callsFake((...args) => { - args[2](testConsentData.data.msgName, testConsentData.data); - }); - - setConsentConfig(goodConfigWithAllowAuction); - requestBidsHook(() => { - didHookReturn = true; - }, { adUnits: [{ sizes: [[300, 250]] }] }); - let consent = gdprDataHandler.getConsentData(); - - sinon.assert.notCalled(utils.logWarn); - sinon.assert.notCalled(utils.logError); - expect(didHookReturn).to.be.true; - expect(consent.consentString).to.equal('abc123def'); - expect(consent.gdprApplies).to.be.true; - expect(consent.apiVersion).to.equal(1); - }); - }); - - describe('v1 CMP workflow for iframe pages', function () { - stringifyResponse = false; - let ifr1 = null; - - beforeEach(function () { - ifr1 = createIFrameMarker('__cmpLocator'); - cmpPostMessageCb = creatCmpMessageHandler('__cmp', { - consentData: 'encoded_consent_data_via_post_message', - gdprApplies: true, - }); - window.addEventListener('message', cmpPostMessageCb, false); - }); - - afterEach(function () { - delete window.__cmp; // deletes the local copy made by the postMessage CMP call function - document.body.removeChild(ifr1); - window.removeEventListener('message', cmpPostMessageCb); - }); - - // Run tests with JSON response and String response - // from CMP window postMessage listener. - testIFramedPage('with/JSON response', false, 'encoded_consent_data_via_post_message', 1); - testIFramedPage('with/String response', true, 'encoded_consent_data_via_post_message', 1); - - it('should contain correct V1 CMP definition', (done) => { - setConsentConfig(goodConfigWithAllowAuction); - requestBidsHook(() => { - const nbArguments = window.__cmp.toString().split('\n')[0].split(', ').length; - expect(nbArguments).to.equal(3); - done(); - }, {}); - }); - }); - describe('v2 CMP workflow for iframe pages:', function () { stringifyResponse = false; let ifr2 = null; @@ -622,7 +456,7 @@ describe('consentManagement', function () { testIFramedPage('with/String response', true, 'abc12345234', 2); it('should contain correct v2 CMP definition', (done) => { - setConsentConfig(goodConfigWithAllowAuction); + setConsentConfig(goodConfig); requestBidsHook(() => { const nbArguments = window.__tcfapi.toString().split('\n')[0].split(', ').length; expect(nbArguments).to.equal(4); @@ -649,40 +483,6 @@ describe('consentManagement', function () { resetConsentData(); }); - describe('v1 CMP workflow for normal pages:', function () { - beforeEach(function () { - window.__cmp = function () { }; - }); - - afterEach(function () { - delete window.__cmp; - }); - - 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; - expect(consent.apiVersion).to.equal(1); - }); - }); - describe('v2 CMP workflow for normal pages:', function () { beforeEach(function() { window.__tcfapi = function () { }; @@ -703,7 +503,7 @@ describe('consentManagement', function () { args[2](testConsentData, true); }); - setConsentConfig(goodConfigWithAllowAuction); + setConsentConfig(goodConfig); requestBidsHook(() => { didHookReturn = true; @@ -730,7 +530,7 @@ describe('consentManagement', function () { args[2](testConsentData, true); }); - setConsentConfig(goodConfigWithAllowAuction); + setConsentConfig(goodConfig); requestBidsHook(() => { didHookReturn = true; @@ -755,7 +555,7 @@ describe('consentManagement', function () { args[2](testConsentData, true); }); - setConsentConfig(goodConfigWithAllowAuction); + setConsentConfig(goodConfig); requestBidsHook(() => { didHookReturn = true; @@ -769,7 +569,7 @@ describe('consentManagement', function () { expect(consent.apiVersion).to.equal(2); }); - it('throws an error when processCmpData check fails + does not call requestBids callbcack even when allowAuction is true', function () { + it('throws an error when processCmpData check fails + does not call requestBids callback', function () { let testConsentData = {}; let bidsBackHandlerReturn = false; @@ -777,9 +577,9 @@ describe('consentManagement', function () { args[2](testConsentData); }); - setConsentConfig(goodConfigWithAllowAuction); + setConsentConfig(goodConfig); - sinon.assert.calledOnce(utils.logWarn); + sinon.assert.notCalled(utils.logWarn); sinon.assert.notCalled(utils.logError); [utils.logWarn, utils.logError].forEach((stub) => stub.reset()); From 86d40890572fecf5adf1a3957aeb0b4b33288cf9 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Mon, 2 May 2022 09:48:17 -0700 Subject: [PATCH 2/8] Remove superfluous argument --- modules/consentManagement.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/consentManagement.js b/modules/consentManagement.js index e02e8ddb8e6..964baa3628a 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -264,7 +264,7 @@ function processCmpData(consentObject, {onSuccess, onError}) { consentObject = consentObject.getTCData; } - if (checkData(consentObject)) { + if (checkData()) { onError(`CMP returned unexpected value during lookup process.`, consentObject); } else { onSuccess(storeConsentData(consentObject)); From 49985eb75b0dbea877825812281245384af9eca4 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Fri, 6 May 2022 08:55:14 -0700 Subject: [PATCH 3/8] convert let to const --- modules/consentManagement.js | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/modules/consentManagement.js b/modules/consentManagement.js index 964baa3628a..32f39fc6a2a 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -84,8 +84,8 @@ function lookupIabConsent({onSuccess, onError}) { } } - let cmpCallbacks = {}; - let { cmpFrame, cmpFunction } = findCMP(); + const cmpCallbacks = {}; + const { cmpFrame, cmpFunction } = findCMP(); if (!cmpFrame) { return onError('CMP not found.'); @@ -116,8 +116,8 @@ function lookupIabConsent({onSuccess, onError}) { /* 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[apiName] = function (cmd, cmpVersion, callback, arg) { - let callId = Math.random() + ''; - let msg = { + const callId = Math.random() + ''; + const msg = { [callName]: { command: cmd, version: cmpVersion, @@ -137,10 +137,10 @@ function lookupIabConsent({onSuccess, onError}) { window[apiName](commandName, CMP_VERSION, moduleCallback); function readPostMessageResponse(event) { - let cmpDataPkgName = `${apiName}Return`; - let json = (typeof event.data === 'string' && includes(event.data, cmpDataPkgName)) ? JSON.parse(event.data) : event.data; + const cmpDataPkgName = `${apiName}Return`; + const json = (typeof event.data === 'string' && includes(event.data, cmpDataPkgName)) ? JSON.parse(event.data) : event.data; if (json[cmpDataPkgName] && json[cmpDataPkgName].callId) { - let payload = json[cmpDataPkgName]; + const payload = json[cmpDataPkgName]; // TODO - clean up this logic (move listeners?); we have duplicate messages responses because 2 eventlisteners are active from the 2 cmp requests running in parallel if (typeof cmpCallbacks[payload.callId] !== 'undefined') { cmpCallbacks[payload.callId](payload.returnValue, payload.success); @@ -179,9 +179,7 @@ function loadConsentData(cb) { const callbacks = { onSuccess: (data) => done(data, false), onError: function (msg, ...extraArgs) { - let consentData = null; - let shouldCancelAuction = true; - done(consentData, shouldCancelAuction, msg, ...extraArgs); + done(null, true, msg, ...extraArgs); } } cmpCallMap[userCMP](callbacks); @@ -251,8 +249,8 @@ export function requestBidsHook(fn, reqBidsConfigObj) { function processCmpData(consentObject, {onSuccess, onError}) { function checkData() { // if CMP does not respond with a gdprApplies boolean, use defaultGdprScope (gdprScope) - let gdprApplies = consentObject && typeof consentObject.gdprApplies === 'boolean' ? consentObject.gdprApplies : gdprScope; - let tcString = consentObject && consentObject.tcString; + const gdprApplies = consentObject && typeof consentObject.gdprApplies === 'boolean' ? consentObject.gdprApplies : gdprScope; + const tcString = consentObject && consentObject.tcString; return !!( (typeof gdprApplies !== 'boolean') || (gdprApplies === true && !isStr(tcString)) From f2c8d8ec2d1e7fae05be8b1c34be54f646b974e9 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Fri, 6 May 2022 09:07:20 -0700 Subject: [PATCH 4/8] Small improvements --- modules/consentManagement.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/consentManagement.js b/modules/consentManagement.js index 32f39fc6a2a..e96cbf6d860 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -4,7 +4,7 @@ * and make it available for any GDPR supported adapters to read/pass this information to * their system. */ -import {isFn, isNumber, isPlainObject, isStr, logError, logInfo, logWarn} from '../src/utils.js'; +import {isNumber, isPlainObject, isStr, logError, logInfo, logWarn} from '../src/utils.js'; import {config} from '../src/config.js'; import {gdprDataHandler} from '../src/adapterManager.js'; import {includes} from '../src/polyfill.js'; @@ -73,7 +73,7 @@ function lookupIabConsent({onSuccess, onError}) { }; } - function v2CmpResponseCallback(tcfData, success) { + function cmpResponseCallback(tcfData, success) { logInfo('Received a response from CMP', tcfData); if (success) { if (tcfData.gdprApplies === false || tcfData.eventStatus === 'tcloaded' || tcfData.eventStatus === 'useractioncomplete') { @@ -92,7 +92,7 @@ function lookupIabConsent({onSuccess, onError}) { } // 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) + // second to collect the usefr'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 @@ -100,12 +100,12 @@ function lookupIabConsent({onSuccess, onError}) { // 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 - if (isFn(cmpFunction)) { + if (typeof cmpFunction === 'function') { logInfo('Detected CMP API is directly accessible, calling it now...'); - cmpFunction('addEventListener', CMP_VERSION, v2CmpResponseCallback); + cmpFunction('addEventListener', CMP_VERSION, cmpResponseCallback); } else { logInfo('Detected CMP is outside the current iframe where Prebid.js is located, calling it now...'); - callCmpWhileInIframe('addEventListener', cmpFrame, v2CmpResponseCallback); + callCmpWhileInIframe('addEventListener', cmpFrame, cmpResponseCallback); } function callCmpWhileInIframe(commandName, cmpFrame, moduleCallback) { @@ -166,7 +166,7 @@ function loadConsentData(cb) { } isDone = true; gdprDataHandler.setConsentData(consentData); - if (cb != null) { + if (typeof cb === 'function') { cb(shouldCancelAuction, errMsg, ...extraArgs); } } From 3b7e2935d655a86e0562f78b1dacb586f273357b Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Tue, 10 May 2022 12:21:31 -0700 Subject: [PATCH 5/8] Fix behavior on timeout: 0 --- modules/consentManagement.js | 4 ++-- test/spec/modules/consentManagement_spec.js | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/modules/consentManagement.js b/modules/consentManagement.js index e96cbf6d860..59c77e3427f 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -92,7 +92,7 @@ function lookupIabConsent({onSuccess, onError}) { } // 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 usefr's full unparsed consent information (via getVendorConsents) + // 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 @@ -186,7 +186,7 @@ function loadConsentData(cb) { if (!isDone) { if (consentTimeout === 0) { - processCmpData(undefined, callbacks); + done(storeConsentData(undefined), false) } else { timer = setTimeout(function () { // on timeout, allow the auction to continue diff --git a/test/spec/modules/consentManagement_spec.js b/test/spec/modules/consentManagement_spec.js index 75b2204104e..a286a4ed1b9 100644 --- a/test/spec/modules/consentManagement_spec.js +++ b/test/spec/modules/consentManagement_spec.js @@ -302,6 +302,25 @@ describe('consentManagement', function () { return gdprDataHandler.promise.then(() => { expect(ran).to.be.true; }); + }); + + it('should continue the auction immediately, without consent data, if timeout is 0', (done) => { + setConsentConfig({ + cmpApi: 'iab', + timeout: 0, + defaultGdprScope: true + }); + window.__tcfapi = function () {}; + try { + requestBidsHook(() => { + const consent = gdprDataHandler.getConsentData(); + expect(consent.gdprApplies).to.be.true; + expect(consent.consentString).to.be.undefined; + done(); + }, {}) + } finally { + delete window.__tcfapi; + } }) }); From 3f044117148a200226a789f8eeaf5fa530f461fd Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Tue, 31 May 2022 12:30:29 -0700 Subject: [PATCH 6/8] Remove some of the duplication for bidders checking p1 consent --- modules/apacdexBidAdapter.js | 11 +- modules/appnexusBidAdapter.js | 15 +- modules/craftBidAdapter.js | 13 +- modules/gdprEnforcement.js | 184 ++++++++---------- modules/goldbachBidAdapter.js | 13 +- modules/improvedigitalBidAdapter.js | 12 +- modules/inskinBidAdapter.js | 46 +++-- modules/kubientBidAdapter.js | 10 +- modules/mediafuseBidAdapter.js | 13 +- modules/nobidBidAdapter.js | 12 +- modules/optoutBidAdapter.js | 15 +- modules/prebidServerBidAdapter/index.js | 11 +- modules/proxistoreBidAdapter.js | 10 - modules/quantcastBidAdapter.js | 15 +- modules/quantcastIdSystem.js | 8 +- modules/userId/index.js | 23 +-- src/utils/gpdr.js | 14 ++ .../modules/prebidServerBidAdapter_spec.js | 54 ++--- test/spec/modules/quantcastBidAdapter_spec.js | 68 ------- test/spec/modules/userId_spec.js | 2 + 20 files changed, 168 insertions(+), 381 deletions(-) create mode 100644 src/utils/gpdr.js diff --git a/modules/apacdexBidAdapter.js b/modules/apacdexBidAdapter.js index d7b6b7c4020..aa8656fe38c 100644 --- a/modules/apacdexBidAdapter.js +++ b/modules/apacdexBidAdapter.js @@ -1,6 +1,7 @@ import { deepAccess, isPlainObject, isArray, replaceAuctionPrice, isFn } from '../src/utils.js'; import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +import {hasPurpose1Consent} from '../src/utils/gpdr.js'; const BIDDER_CODE = 'apacdex'; const ENDPOINT = 'https://useast.quantumdex.io/auction/pbjs' const USERSYNC = 'https://sync.quantumdex.io/usersync/pbjs' @@ -378,14 +379,4 @@ function getBidFloor(bid) { return null; } -function hasPurpose1Consent(gdprConsent) { - let result = true; - if (gdprConsent) { - if (gdprConsent.gdprApplies && gdprConsent.apiVersion === 2) { - result = !!(deepAccess(gdprConsent, 'vendorData.purpose.consents.1') === true); - } - } - return result; -} - registerBidder(spec); diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index 2e2d6b093db..f7c9a4c184d 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -33,6 +33,7 @@ import {find, includes} from '../src/polyfill.js'; import {INSTREAM, OUTSTREAM} from '../src/video.js'; import {getStorageManager} from '../src/storageManager.js'; import {bidderSettings} from '../src/bidderSettings.js'; +import {hasPurpose1Consent} from '../src/utils/gpdr.js'; const BIDDER_CODE = 'appnexus'; const URL = 'https://ib.adnxs.com/ut/v3/prebid'; @@ -383,7 +384,7 @@ export const spec = { }, getUserSyncs: function (syncOptions, responses, gdprConsent) { - if (syncOptions.iframeEnabled && hasPurpose1Consent({gdprConsent})) { + if (syncOptions.iframeEnabled && hasPurpose1Consent(gdprConsent)) { return [{ type: 'iframe', url: 'https://acdn.adnxs.com/dmp/async_usersync.html' @@ -546,16 +547,6 @@ function getViewabilityScriptUrlFromPayload(viewJsPayload) { return jsTrackerSrc; } -function hasPurpose1Consent(bidderRequest) { - let result = true; - if (bidderRequest && bidderRequest.gdprConsent) { - if (bidderRequest.gdprConsent.gdprApplies && bidderRequest.gdprConsent.apiVersion === 2) { - result = !!(deepAccess(bidderRequest.gdprConsent, 'vendorData.purpose.consents.1') === true); - } - } - return result; -} - function formatRequest(payload, bidderRequest) { let request = []; let options = { @@ -564,7 +555,7 @@ function formatRequest(payload, bidderRequest) { let endpointUrl = URL; - if (!hasPurpose1Consent(bidderRequest)) { + if (!hasPurpose1Consent(bidderRequest?.gdprConsent)) { endpointUrl = URL_SIMPLE; } diff --git a/modules/craftBidAdapter.js b/modules/craftBidAdapter.js index 61ca4f929e7..4ece93dbc32 100644 --- a/modules/craftBidAdapter.js +++ b/modules/craftBidAdapter.js @@ -14,6 +14,7 @@ import {auctionManager} from '../src/auctionManager.js'; import {find, includes} from '../src/polyfill.js'; import {getStorageManager} from '../src/storageManager.js'; import {ajax} from '../src/ajax.js'; +import {hasPurpose1Consent} from '../src/utils/gpdr.js'; const BIDDER_CODE = 'craft'; const URL_BASE = 'https://gacraft.jp/prebid-v3'; @@ -136,19 +137,9 @@ function deleteValues(keyPairObj) { } } -function hasPurpose1Consent(bidderRequest) { - let result = true; - if (bidderRequest && bidderRequest.gdprConsent) { - if (bidderRequest.gdprConsent.gdprApplies && bidderRequest.gdprConsent.apiVersion === 2) { - result = !!(deepAccess(bidderRequest.gdprConsent, 'vendorData.purpose.consents.1') === true); - } - } - return result; -} - function formatRequest(payload, bidderRequest) { let options = {}; - if (!hasPurpose1Consent(bidderRequest)) { + if (!hasPurpose1Consent(bidderRequest?.gdprConsent)) { options = { withCredentials: false }; diff --git a/modules/gdprEnforcement.js b/modules/gdprEnforcement.js index 161f530f202..9e300e1de97 100644 --- a/modules/gdprEnforcement.js +++ b/modules/gdprEnforcement.js @@ -43,7 +43,7 @@ const storageBlocked = []; const biddersBlocked = []; const analyticsBlocked = []; -let addedDeviceAccessHook = false; +let hooksAdded = false; // Helps in stubbing these functions in unit tests. export const internal = { @@ -174,29 +174,23 @@ export function deviceAccessHook(fn, gvlid, moduleName, result) { } else { const consentData = gdprDataHandler.getConsentData(); if (consentData && consentData.gdprApplies) { - if (consentData.apiVersion === 2) { - const curBidder = config.getCurrentBidder(); - // Bidders have a copy of storage object with bidder code binded. Aliases will also pass the same bidder code when invoking storage functions and hence if alias tries to access device we will try to grab the gvl id for alias instead of original bidder - if (curBidder && (curBidder != moduleName) && adapterManager.aliasRegistry[curBidder] === moduleName) { - gvlid = getGvlid(curBidder); - } else { - gvlid = getGvlid(moduleName) || gvlid; - } - const curModule = moduleName || curBidder; - let isAllowed = validateRules(purpose1Rule, consentData, curModule, gvlid); - if (isAllowed) { - result.valid = true; - fn.call(this, gvlid, moduleName, result); - } else { - curModule && logWarn(`TCF2 denied device access for ${curModule}`); - result.valid = false; - storageBlocked.push(curModule); - fn.call(this, gvlid, moduleName, result); - } + const curBidder = config.getCurrentBidder(); + // Bidders have a copy of storage object with bidder code binded. Aliases will also pass the same bidder code when invoking storage functions and hence if alias tries to access device we will try to grab the gvl id for alias instead of original bidder + if (curBidder && (curBidder != moduleName) && adapterManager.aliasRegistry[curBidder] === moduleName) { + gvlid = getGvlid(curBidder); } else { - // The module doesn't enforce TCF1.1 strings + gvlid = getGvlid(moduleName) || gvlid; + } + const curModule = moduleName || curBidder; + let isAllowed = validateRules(purpose1Rule, consentData, curModule, gvlid); + if (isAllowed) { result.valid = true; fn.call(this, gvlid, moduleName, result); + } else { + curModule && logWarn(`TCF2 denied device access for ${curModule}`); + result.valid = false; + storageBlocked.push(curModule); + fn.call(this, gvlid, moduleName, result); } } else { result.valid = true; @@ -213,19 +207,14 @@ export function deviceAccessHook(fn, gvlid, moduleName, result) { export function userSyncHook(fn, ...args) { const consentData = gdprDataHandler.getConsentData(); if (consentData && consentData.gdprApplies) { - if (consentData.apiVersion === 2) { - const curBidder = config.getCurrentBidder(); - const gvlid = getGvlid(curBidder); - let isAllowed = validateRules(purpose1Rule, consentData, curBidder, gvlid); - if (isAllowed) { - fn.call(this, ...args); - } else { - logWarn(`User sync not allowed for ${curBidder}`); - storageBlocked.push(curBidder); - } - } else { - // The module doesn't enforce TCF1.1 strings + const curBidder = config.getCurrentBidder(); + const gvlid = getGvlid(curBidder); + let isAllowed = validateRules(purpose1Rule, consentData, curBidder, gvlid); + if (isAllowed) { fn.call(this, ...args); + } else { + logWarn(`User sync not allowed for ${curBidder}`); + storageBlocked.push(curBidder); } } else { fn.call(this, ...args); @@ -240,24 +229,19 @@ export function userSyncHook(fn, ...args) { */ export function userIdHook(fn, submodules, consentData) { if (consentData && consentData.gdprApplies) { - if (consentData.apiVersion === 2) { - let userIdModules = submodules.map((submodule) => { - const gvlid = getGvlid(submodule.submodule); - const moduleName = submodule.submodule.name; - let isAllowed = validateRules(purpose1Rule, consentData, moduleName, gvlid); - if (isAllowed) { - return submodule; - } else { - logWarn(`User denied permission to fetch user id for ${moduleName} User id module`); - storageBlocked.push(moduleName); - } - return undefined; - }).filter(module => module) - fn.call(this, userIdModules, { ...consentData, hasValidated: true }); - } else { - // The module doesn't enforce TCF1.1 strings - fn.call(this, submodules, consentData); - } + let userIdModules = submodules.map((submodule) => { + const gvlid = getGvlid(submodule.submodule); + const moduleName = submodule.submodule.name; + let isAllowed = validateRules(purpose1Rule, consentData, moduleName, gvlid); + if (isAllowed) { + return submodule; + } else { + logWarn(`User denied permission to fetch user id for ${moduleName} User id module`); + storageBlocked.push(moduleName); + } + return undefined; + }).filter(module => module) + fn.call(this, userIdModules, { ...consentData, hasValidated: true }); } else { fn.call(this, submodules, consentData); } @@ -272,25 +256,20 @@ export function userIdHook(fn, submodules, consentData) { export function makeBidRequestsHook(fn, adUnits, ...args) { const consentData = gdprDataHandler.getConsentData(); if (consentData && consentData.gdprApplies) { - if (consentData.apiVersion === 2) { - adUnits.forEach(adUnit => { - adUnit.bids = adUnit.bids.filter(bid => { - const currBidder = bid.bidder; - const gvlId = getGvlid(currBidder); - if (includes(biddersBlocked, currBidder)) return false; - const isAllowed = !!validateRules(purpose2Rule, consentData, currBidder, gvlId); - if (!isAllowed) { - logWarn(`TCF2 blocked auction for ${currBidder}`); - biddersBlocked.push(currBidder); - } - return isAllowed; - }); + adUnits.forEach(adUnit => { + adUnit.bids = adUnit.bids.filter(bid => { + const currBidder = bid.bidder; + const gvlId = getGvlid(currBidder); + if (includes(biddersBlocked, currBidder)) return false; + const isAllowed = !!validateRules(purpose2Rule, consentData, currBidder, gvlId); + if (!isAllowed) { + logWarn(`TCF2 blocked auction for ${currBidder}`); + biddersBlocked.push(currBidder); + } + return isAllowed; }); - fn.call(this, adUnits, ...args); - } else { - // The module doesn't enforce TCF1.1 strings - fn.call(this, adUnits, ...args); - } + }); + fn.call(this, adUnits, ...args); } else { fn.call(this, adUnits, ...args); } @@ -305,25 +284,20 @@ export function makeBidRequestsHook(fn, adUnits, ...args) { export function enableAnalyticsHook(fn, config) { const consentData = gdprDataHandler.getConsentData(); if (consentData && consentData.gdprApplies) { - if (consentData.apiVersion === 2) { - if (!isArray(config)) { - config = [config] - } - config = config.filter(conf => { - const analyticsAdapterCode = conf.provider; - const gvlid = getGvlid(analyticsAdapterCode); - const isAllowed = !!validateRules(purpose7Rule, consentData, analyticsAdapterCode, gvlid); - if (!isAllowed) { - analyticsBlocked.push(analyticsAdapterCode); - logWarn(`TCF2 blocked analytics adapter ${conf.provider}`); - } - return isAllowed; - }); - fn.call(this, config); - } else { - // This module doesn't enforce TCF1.1 strings - fn.call(this, config); + if (!isArray(config)) { + config = [config] } + config = config.filter(conf => { + const analyticsAdapterCode = conf.provider; + const gvlid = getGvlid(analyticsAdapterCode); + const isAllowed = !!validateRules(purpose7Rule, consentData, analyticsAdapterCode, gvlid); + if (!isAllowed) { + analyticsBlocked.push(analyticsAdapterCode); + logWarn(`TCF2 blocked analytics adapter ${conf.provider}`); + } + return isAllowed; + }); + fn.call(this, config); } else { fn.call(this, config); } @@ -380,20 +354,32 @@ export function setEnforcementConfig(config) { purpose2Rule = DEFAULT_RULES[1]; } - if (purpose1Rule && !addedDeviceAccessHook) { - addedDeviceAccessHook = true; - validateStorageEnforcement.before(deviceAccessHook, 49); - registerSyncInner.before(userSyncHook, 48); - // Using getHook as user id and gdprEnforcement are both optional modules. Using import will auto include the file in build - getHook('validateGdprEnforcement').before(userIdHook, 47); - } - if (purpose2Rule) { - getHook('makeBidRequests').before(makeBidRequestsHook); + if (!hooksAdded) { + if (purpose1Rule) { + hooksAdded = true; + validateStorageEnforcement.before(deviceAccessHook, 49); + registerSyncInner.before(userSyncHook, 48); + // Using getHook as user id and gdprEnforcement are both optional modules. Using import will auto include the file in build + getHook('validateGdprEnforcement').before(userIdHook, 47); + } + if (purpose2Rule) { + getHook('makeBidRequests').before(makeBidRequestsHook); + } + if (purpose7Rule) { + getHook('enableAnalyticsCb').before(enableAnalyticsHook); + } } +} - if (purpose7Rule) { - getHook('enableAnalyticsCb').before(enableAnalyticsHook); - } +export function uninstall() { + [ + validateStorageEnforcement.getHooks({hook: deviceAccessHook}), + registerSyncInner.getHooks({hook: userSyncHook}), + getHook('validateGdprEnforcement').getHooks({hook: userIdHook}), + getHook('makeBidRequests').getHooks({hook: makeBidRequestsHook}), + getHook('enableAnalyticsCb').getHooks({hook: enableAnalyticsHook}), + ].forEach(hook => hook.remove()); + hooksAdded = false; } config.getConfig('consentManagement', config => setEnforcementConfig(config.consentManagement)); diff --git a/modules/goldbachBidAdapter.js b/modules/goldbachBidAdapter.js index 7b6ae810b67..64474730c3a 100644 --- a/modules/goldbachBidAdapter.js +++ b/modules/goldbachBidAdapter.js @@ -29,6 +29,7 @@ import {ADPOD, BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {auctionManager} from '../src/auctionManager.js'; import {find, includes} from '../src/polyfill.js'; import {INSTREAM, OUTSTREAM} from '../src/video.js'; +import {hasPurpose1Consent} from '../src/utils/gpdr.js'; const BIDDER_CODE = 'goldbach'; const URL = 'https://ib.adnxs.com/ut/v3/prebid'; @@ -532,16 +533,6 @@ function getViewabilityScriptUrlFromPayload(viewJsPayload) { return jsTrackerSrc; } -function hasPurpose1Consent(bidderRequest) { - let result = true; - if (bidderRequest && bidderRequest.gdprConsent) { - if (bidderRequest.gdprConsent.gdprApplies && bidderRequest.gdprConsent.apiVersion === 2) { - result = !!(deepAccess(bidderRequest.gdprConsent, 'vendorData.purpose.consents.1') === true); - } - } - return result; -} - function formatRequest(payload, bidderRequest) { let request = []; let options = { @@ -550,7 +541,7 @@ function formatRequest(payload, bidderRequest) { let endpointUrl = URL; - if (!hasPurpose1Consent(bidderRequest)) { + if (!hasPurpose1Consent(bidderRequest?.gdprConsent)) { endpointUrl = URL_SIMPLE; } diff --git a/modules/improvedigitalBidAdapter.js b/modules/improvedigitalBidAdapter.js index 3a94356ff71..3dbea2567cc 100644 --- a/modules/improvedigitalBidAdapter.js +++ b/modules/improvedigitalBidAdapter.js @@ -7,6 +7,7 @@ import {config} from '../src/config.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {Renderer} from '../src/Renderer.js'; import {createEidsArray} from './userId/eids.js'; +import {hasPurpose1Consent} from '../src/utils/gpdr.js'; const BIDDER_CODE = 'improvedigital'; const CREATIVE_TTL = 300; @@ -217,7 +218,7 @@ export const spec = { * @return {UserSync[]} The user syncs which should be dropped. */ getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { - if (config.getConfig('coppa') === true || !ID_UTIL.hasPurpose1Consent(gdprConsent)) { + if (config.getConfig('coppa') === true || !hasPurpose1Consent(gdprConsent)) { return []; } @@ -656,12 +657,3 @@ const ID_RAZR = { razr.queue.push(payload); } }; - -const ID_UTIL = { - hasPurpose1Consent(gdprConsent) { - if (gdprConsent && gdprConsent.gdprApplies && gdprConsent.apiVersion === 2) { - return (deepAccess(gdprConsent, 'vendorData.purpose.consents.1') === true); - } - return true; - } -}; diff --git a/modules/inskinBidAdapter.js b/modules/inskinBidAdapter.js index 781bca90660..e3ad468441b 100644 --- a/modules/inskinBidAdapter.js +++ b/modules/inskinBidAdapter.js @@ -82,31 +82,29 @@ export const spec = { gdprConsentRequired: (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') ? bidderRequest.gdprConsent.gdprApplies : true }; - if (bidderRequest.gdprConsent.apiVersion === 2) { - const purposes = [ - {id: 1, kw: 'nocookies'}, - {id: 2, kw: 'nocontext'}, - {id: 3, kw: 'nodmp'}, - {id: 4, kw: 'nodata'}, - {id: 7, kw: 'noclicks'}, - {id: 9, kw: 'noresearch'} - ]; - - const d = bidderRequest.gdprConsent.vendorData; - - if (d) { - if (d.purposeOneTreatment) { - data.keywords.push('cst-nodisclosure'); - restrictions.push('nodisclosure'); - } - - purposes.map(p => { - if (!checkConsent(p.id, d)) { - data.keywords.push('cst-' + p.kw); - restrictions.push(p.kw); - } - }); + const purposes = [ + {id: 1, kw: 'nocookies'}, + {id: 2, kw: 'nocontext'}, + {id: 3, kw: 'nodmp'}, + {id: 4, kw: 'nodata'}, + {id: 7, kw: 'noclicks'}, + {id: 9, kw: 'noresearch'} + ]; + + const d = bidderRequest.gdprConsent.vendorData; + + if (d) { + if (d.purposeOneTreatment) { + data.keywords.push('cst-nodisclosure'); + restrictions.push('nodisclosure'); } + + purposes.map(p => { + if (!checkConsent(p.id, d)) { + data.keywords.push('cst-' + p.kw); + restrictions.push(p.kw); + } + }); } } diff --git a/modules/kubientBidAdapter.js b/modules/kubientBidAdapter.js index 46360572576..9ed5b077eae 100644 --- a/modules/kubientBidAdapter.js +++ b/modules/kubientBidAdapter.js @@ -151,15 +151,7 @@ function encodeQueryData(data) { function kubientGetConsentGiven(gdprConsent) { let consentGiven = 0; if (typeof gdprConsent !== 'undefined') { - let apiVersion = deepAccess(gdprConsent, `apiVersion`); - switch (apiVersion) { - case 1: - consentGiven = deepAccess(gdprConsent, `vendorData.vendorConsents.${VENDOR_ID}`) ? 1 : 0; - break; - case 2: - consentGiven = deepAccess(gdprConsent, `vendorData.vendor.consents.${VENDOR_ID}`) ? 1 : 0; - break; - } + consentGiven = deepAccess(gdprConsent, `vendorData.vendor.consents.${VENDOR_ID}`) ? 1 : 0; } return consentGiven; } diff --git a/modules/mediafuseBidAdapter.js b/modules/mediafuseBidAdapter.js index f813fb2e131..2a57168b9e5 100644 --- a/modules/mediafuseBidAdapter.js +++ b/modules/mediafuseBidAdapter.js @@ -8,6 +8,7 @@ import {find, includes} from '../src/polyfill.js'; import { OUTSTREAM, INSTREAM } from '../src/video.js'; import { getStorageManager } from '../src/storageManager.js'; import { bidderSettings } from '../src/bidderSettings.js'; +import {hasPurpose1Consent} from '../src/utils/gpdr.js'; const BIDDER_CODE = 'mediafuse'; const URL = 'https://ib.adnxs.com/ut/v3/prebid'; @@ -478,16 +479,6 @@ function getViewabilityScriptUrlFromPayload(viewJsPayload) { return jsTrackerSrc; } -function hasPurpose1Consent(bidderRequest) { - let result = true; - if (bidderRequest && bidderRequest.gdprConsent) { - if (bidderRequest.gdprConsent.gdprApplies && bidderRequest.gdprConsent.apiVersion === 2) { - result = !!(deepAccess(bidderRequest.gdprConsent, 'vendorData.purpose.consents.1') === true); - } - } - return result; -} - function formatRequest(payload, bidderRequest) { let request = []; let options = { @@ -496,7 +487,7 @@ function formatRequest(payload, bidderRequest) { let endpointUrl = URL; - if (!hasPurpose1Consent(bidderRequest)) { + if (!hasPurpose1Consent(bidderRequest?.gdprConsent)) { endpointUrl = URL_SIMPLE; } diff --git a/modules/nobidBidAdapter.js b/modules/nobidBidAdapter.js index 08119beafb4..a0efbe1bfc2 100644 --- a/modules/nobidBidAdapter.js +++ b/modules/nobidBidAdapter.js @@ -3,6 +3,7 @@ import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { getStorageManager } from '../src/storageManager.js'; +import {hasPurpose1Consent} from '../src/utils/gpdr.js'; const GVLID = 816; const BIDDER_CODE = 'nobid'; @@ -25,15 +26,6 @@ function nobidSetCookie(cname, cvalue, hours) { function nobidGetCookie(cname) { return storage.getCookie(cname); } -function nobidHasPurpose1Consent(bidderRequest) { - let result = true; - if (bidderRequest && bidderRequest.gdprConsent) { - if (bidderRequest.gdprConsent.gdprApplies && bidderRequest.gdprConsent.apiVersion === 2) { - result = !!(deepAccess(bidderRequest.gdprConsent, 'vendorData.purpose.consents.1') === true); - } - } - return result; -} function nobidBuildRequests(bids, bidderRequest) { var serializeState = function(divIds, siteId, adunits) { var filterAdUnitsByIds = function(divIds, adUnits) { @@ -386,7 +378,7 @@ export const spec = { const endpoint = buildEndpoint(); let options = {}; - if (!nobidHasPurpose1Consent(bidderRequest)) { + if (!hasPurpose1Consent(bidderRequest?.gdprConsent)) { options = { withCredentials: false }; } diff --git a/modules/optoutBidAdapter.js b/modules/optoutBidAdapter.js index 1ee785182ed..f7b5934665c 100644 --- a/modules/optoutBidAdapter.js +++ b/modules/optoutBidAdapter.js @@ -1,6 +1,7 @@ import { deepAccess } from '../src/utils.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {hasPurpose1Consent} from '../src/utils/gpdr.js'; const BIDDER_CODE = 'optout'; @@ -19,16 +20,6 @@ function getCurrency() { return cur; } -function hasPurpose1Consent(bidderRequest) { - let result = false; - if (bidderRequest && bidderRequest.gdprConsent) { - if (bidderRequest.gdprConsent.apiVersion === 2) { - result = !!(deepAccess(bidderRequest.gdprConsent, 'vendorData.purpose.consents.1') === true); - } - } - return result; -} - export const spec = { code: BIDDER_CODE, @@ -44,7 +35,7 @@ export const spec = { if (bidRequest.gdprConsent) { gdpr = (typeof bidRequest.gdprConsent.gdprApplies === 'boolean') ? Number(bidRequest.gdprConsent.gdprApplies) : 0; consentString = bidRequest.gdprConsent.consentString; - if (!gdpr || hasPurpose1Consent(bidRequest)) { + if (!gdpr || hasPurpose1Consent(bidRequest.gdprConsent)) { endPoint = 'https://prebid.adscience.nl/prebid/display'; } } @@ -73,7 +64,7 @@ export const spec = { getUserSyncs: function (syncOptions, responses, gdprConsent) { if (gdprConsent) { let gdpr = (typeof gdprConsent.gdprApplies === 'boolean') ? Number(gdprConsent.gdprApplies) : 0; - if (syncOptions.iframeEnabled && (!gdprConsent.gdprApplies || hasPurpose1Consent({gdprConsent}))) { + if (syncOptions.iframeEnabled && (!gdprConsent.gdprApplies || hasPurpose1Consent(gdprConsent))) { return [{ type: 'iframe', url: 'https://umframe.adscience.nl/matching/iframe?gdpr=' + gdpr + '&gdpr_consent=' + gdprConsent.consentString diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index b97a720a6cf..6f113becef1 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -39,6 +39,7 @@ import { S2S_VENDORS } from './config.js'; import { ajax } from '../../src/ajax.js'; import {hook} from '../../src/hook.js'; import {getGlobal} from '../../src/prebidGlobal.js'; +import {hasPurpose1Consent} from '../../src/utils/gpdr.js'; const getConfig = config.getConfig; @@ -1154,16 +1155,6 @@ function bidWonHandler(bid) { } } -function hasPurpose1Consent(gdprConsent) { - let result = true; - if (gdprConsent) { - if (gdprConsent.gdprApplies && gdprConsent.apiVersion === 2) { - result = !!(deepAccess(gdprConsent, 'vendorData.purpose.consents.1') === true); - } - } - return result; -} - function getMatchingConsentUrl(urlProp, gdprConsent) { return hasPurpose1Consent(gdprConsent) ? urlProp.p1Consent : urlProp.noP1Consent; } diff --git a/modules/proxistoreBidAdapter.js b/modules/proxistoreBidAdapter.js index 42a98bcdb09..04d363be7fb 100644 --- a/modules/proxistoreBidAdapter.js +++ b/modules/proxistoreBidAdapter.js @@ -54,10 +54,8 @@ function _createServerRequest(bidRequests, bidderRequest) { if (gdprConsent.vendorData) { var vendorData = gdprConsent.vendorData; - var apiVersion = gdprConsent.apiVersion; if ( - apiVersion === 2 && vendorData.vendor && vendorData.vendor.consents && typeof vendorData.vendor.consents[PROXISTORE_VENDOR_ID.toString(10)] !== @@ -65,14 +63,6 @@ function _createServerRequest(bidRequests, bidderRequest) { ) { payload.gdpr.consentGiven = !!vendorData.vendor.consents[PROXISTORE_VENDOR_ID.toString(10)]; - } else if ( - apiVersion === 1 && - vendorData.vendorConsents && - typeof vendorData.vendorConsents[PROXISTORE_VENDOR_ID.toString(10)] !== - 'undefined' - ) { - payload.gdpr.consentGiven = - !!vendorData.vendorConsents[PROXISTORE_VENDOR_ID.toString(10)]; } } } diff --git a/modules/quantcastBidAdapter.js b/modules/quantcastBidAdapter.js index 449c7d12d6f..124cd3b1bd7 100644 --- a/modules/quantcastBidAdapter.js +++ b/modules/quantcastBidAdapter.js @@ -81,14 +81,7 @@ function getDomain(url) { return url.replace('http://', '').replace('https://', '').replace('www.', '').split(/[/?#]/)[0]; } -function checkTCFv1(vendorData) { - let vendorConsent = vendorData.vendorConsents && vendorData.vendorConsents[QUANTCAST_VENDOR_ID]; - let purposeConsent = vendorData.purposeConsents && vendorData.purposeConsents[PURPOSE_DATA_COLLECT]; - - return !!(vendorConsent && purposeConsent); -} - -function checkTCFv2(tcData) { +function checkTCF(tcData) { let restrictions = tcData.publisher ? tcData.publisher.restrictions : {}; let qcRestriction = restrictions && restrictions[PURPOSE_DATA_COLLECT] ? restrictions[PURPOSE_DATA_COLLECT][QUANTCAST_VENDOR_ID] @@ -152,11 +145,7 @@ export const spec = { // Remaining consent checks are performed server-side. if (gdprConsent.gdprApplies) { if (gdprConsent.vendorData) { - if (gdprConsent.apiVersion === 1 && !checkTCFv1(gdprConsent.vendorData)) { - logInfo(`${BIDDER_CODE}: No purpose 1 consent for TCF v1`); - return; - } - if (gdprConsent.apiVersion === 2 && !checkTCFv2(gdprConsent.vendorData)) { + if (!checkTCF(gdprConsent.vendorData)) { logInfo(`${BIDDER_CODE}: No purpose 1 consent for TCF v2`); return; } diff --git a/modules/quantcastIdSystem.js b/modules/quantcastIdSystem.js index 7d82be884da..b803096cd31 100644 --- a/modules/quantcastIdSystem.js +++ b/modules/quantcastIdSystem.js @@ -74,13 +74,7 @@ export function hasGDPRConsent(gdprConsent) { if (!gdprConsent.vendorData) { return false; } - if (gdprConsent.apiVersion === 1) { - // We are not supporting TCF v1 - return false; - } - if (gdprConsent.apiVersion === 2) { - return checkTCFv2(gdprConsent.vendorData); - } + return checkTCFv2(gdprConsent.vendorData); } return true; } diff --git a/modules/userId/index.js b/modules/userId/index.js index 809ca624748..7edc2862b57 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -153,6 +153,7 @@ import { } from '../../src/utils.js'; import {getPPID as coreGetPPID} from '../../src/adserver.js'; import {promiseControls} from '../../src/utils/promise.js'; +import {hasPurpose1Consent} from '../../src/utils/gpdr.js'; const MODULE_NAME = 'User ID'; const COOKIE = 'cookie'; @@ -338,26 +339,6 @@ function storedConsentDataMatchesConsentData(storedConsentData, consentData) { ); } -/** - * test if consent module is present, applies, and is valid for local storage or cookies (purpose 1) - * @param {ConsentData} consentData - * @returns {boolean} - */ -function hasGDPRConsent(consentData) { - if (consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies) { - if (!consentData.consentString) { - return false; - } - if (consentData.apiVersion === 1 && deepAccess(consentData, 'vendorData.purposeConsents.1') === false) { - return false; - } - if (consentData.apiVersion === 2 && deepAccess(consentData, 'vendorData.purpose.consents.1') === false) { - return false; - } - } - return true; -} - /** * Find the root domain * @param {string|undefined} fullDomain @@ -850,7 +831,7 @@ function populateSubmoduleId(submodule, consentData, storedConsentData, forceRef function initSubmodules(dest, submodules, consentData, forceRefresh = false) { // gdpr consent with purpose one is required, otherwise exit immediately let { userIdModules, hasValidated } = validateGdprEnforcement(submodules, consentData); - if (!hasValidated && !hasGDPRConsent(consentData)) { + if (!hasValidated && !hasPurpose1Consent(consentData)) { logWarn(`${MODULE_NAME} - gdpr permission not valid for local storage or cookies, exit module`); return []; } diff --git a/src/utils/gpdr.js b/src/utils/gpdr.js new file mode 100644 index 00000000000..19c7126b7d7 --- /dev/null +++ b/src/utils/gpdr.js @@ -0,0 +1,14 @@ +import {deepAccess} from '../utils.js'; + +/** + * Check if GDPR purpose 1 consent was given. + * + * @param gdprConsent GDPR consent data + * @returns {boolean} true if the gdprConsent is null-y; or GDPR does not apply; or if purpose 1 consent was given. + */ +export function hasPurpose1Consent(gdprConsent) { + if (gdprConsent?.gdprApplies) { + return deepAccess(gdprConsent, 'vendorData.purpose.consents.1') === true; + } + return true; +} diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index 13c136ac373..29de3989ec9 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -645,6 +645,14 @@ describe('S2S Adapter', function () { expect(adapter.callBids).to.exist.and.to.be.a('function'); }); + function mockConsent({applies = true, hasP1Consent = true} = {}) { + return { + consentString: 'mockConsent', + gdprApplies: applies, + vendorData: {purpose: {consents: {1: hasP1Consent}}}, + } + } + describe('gdpr tests', function () { afterEach(function () { $$PREBID_GLOBAL$$.requestBids.removeAll(); @@ -655,16 +663,13 @@ describe('S2S Adapter', function () { config.setConfig(consentConfig); let gdprBidRequest = utils.deepClone(BID_REQUESTS); - gdprBidRequest[0].gdprConsent = { - consentString: 'abc123', - gdprApplies: true - }; + gdprBidRequest[0].gdprConsent = mockConsent(); adapter.callBids(REQUEST, gdprBidRequest, addBidResponse, done, ajax); let requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.regs.ext.gdpr).is.equal(1); - expect(requestBid.user.ext.consent).is.equal('abc123'); + expect(requestBid.user.ext.consent).is.equal('mockConsent'); config.resetConfig(); config.setConfig({ s2sConfig: CONFIG }); @@ -681,17 +686,15 @@ describe('S2S Adapter', function () { config.setConfig(consentConfig); let gdprBidRequest = utils.deepClone(BID_REQUESTS); - gdprBidRequest[0].gdprConsent = { - consentString: 'abc123', + gdprBidRequest[0].gdprConsent = Object.assign(mockConsent(), { addtlConsent: 'superduperconsent', - gdprApplies: true - }; + }); adapter.callBids(REQUEST, gdprBidRequest, addBidResponse, done, ajax); let requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.regs.ext.gdpr).is.equal(1); - expect(requestBid.user.ext.consent).is.equal('abc123'); + expect(requestBid.user.ext.consent).is.equal('mockConsent'); expect(requestBid.user.ext.ConsentedProvidersSettings.consented_providers).is.equal('superduperconsent'); config.resetConfig(); @@ -713,10 +716,7 @@ describe('S2S Adapter', function () { let gdprBidRequest = utils.deepClone(BID_REQUESTS); - gdprBidRequest[0].gdprConsent = { - consentString: 'abc123def', - gdprApplies: true - }; + gdprBidRequest[0].gdprConsent = mockConsent(); const s2sBidRequest = utils.deepClone(REQUEST); s2sBidRequest.s2sConfig = cookieSyncConfig; @@ -725,7 +725,7 @@ describe('S2S Adapter', function () { let requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.gdpr).is.equal(1); - expect(requestBid.gdpr_consent).is.equal('abc123def'); + expect(requestBid.gdpr_consent).is.equal('mockConsent'); expect(requestBid.bidders).to.contain('appnexus').and.to.have.lengthOf(1); expect(requestBid.account).is.equal('1'); }); @@ -741,10 +741,7 @@ describe('S2S Adapter', function () { s2sBidRequest.s2sConfig = cookieSyncConfig; let gdprBidRequest = utils.deepClone(BID_REQUESTS); - gdprBidRequest[0].gdprConsent = { - consentString: 'xyz789abcc', - gdprApplies: false - }; + gdprBidRequest[0].gdprConsent = mockConsent({applies: false}); adapter.callBids(s2sBidRequest, gdprBidRequest, addBidResponse, done, ajax); let requestBid = JSON.parse(server.requests[0].requestBody); @@ -761,10 +758,7 @@ describe('S2S Adapter', function () { config.setConfig(consentConfig); let gdprBidRequest = utils.deepClone(BID_REQUESTS); - gdprBidRequest[0].gdprConsent = { - consentString: undefined, - gdprApplies: false - }; + gdprBidRequest[0].gdprConsent = mockConsent({applies: false}); const s2sBidRequest = utils.deepClone(REQUEST); s2sBidRequest.s2sConfig = cookieSyncConfig; @@ -832,17 +826,14 @@ describe('S2S Adapter', function () { let consentBidRequest = utils.deepClone(BID_REQUESTS); consentBidRequest[0].uspConsent = '1NYN'; - consentBidRequest[0].gdprConsent = { - consentString: 'abc123', - gdprApplies: true - }; + consentBidRequest[0].gdprConsent = mockConsent(); adapter.callBids(REQUEST, consentBidRequest, addBidResponse, done, ajax); let requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.regs.ext.us_privacy).is.equal('1NYN'); expect(requestBid.regs.ext.gdpr).is.equal(1); - expect(requestBid.user.ext.consent).is.equal('abc123'); + expect(requestBid.user.ext.consent).is.equal('mockConsent'); config.resetConfig(); config.setConfig({ s2sConfig: CONFIG }); @@ -861,10 +852,7 @@ describe('S2S Adapter', function () { let consentBidRequest = utils.deepClone(BID_REQUESTS); consentBidRequest[0].uspConsent = '1YNN'; - consentBidRequest[0].gdprConsent = { - consentString: 'abc123def', - gdprApplies: true - }; + consentBidRequest[0].gdprConsent = mockConsent(); const s2sBidRequest = utils.deepClone(REQUEST); s2sBidRequest.s2sConfig = cookieSyncConfig @@ -874,7 +862,7 @@ describe('S2S Adapter', function () { expect(requestBid.us_privacy).is.equal('1YNN'); expect(requestBid.gdpr).is.equal(1); - expect(requestBid.gdpr_consent).is.equal('abc123def'); + expect(requestBid.gdpr_consent).is.equal('mockConsent'); expect(requestBid.bidders).to.contain('appnexus').and.to.have.lengthOf(1); expect(requestBid.account).is.equal('1'); }); diff --git a/test/spec/modules/quantcastBidAdapter_spec.js b/test/spec/modules/quantcastBidAdapter_spec.js index 9e3b24c79d2..b63b401fb0d 100644 --- a/test/spec/modules/quantcastBidAdapter_spec.js +++ b/test/spec/modules/quantcastBidAdapter_spec.js @@ -446,74 +446,6 @@ describe('Quantcast adapter', function () { expect(parsed.gdprConsent).to.equal('consentString'); }); - it('allows TCF v1 request with consent for purpose 1', function () { - const bidderRequest = { - gdprConsent: { - gdprApplies: true, - consentString: 'consentString', - vendorData: { - vendorConsents: { - '11': true - }, - purposeConsents: { - '1': true - } - }, - apiVersion: 1 - } - }; - - const requests = qcSpec.buildRequests([bidRequest], bidderRequest); - const parsed = JSON.parse(requests[0].data); - - expect(parsed.gdprSignal).to.equal(1); - expect(parsed.gdprConsent).to.equal('consentString'); - }); - - it('blocks TCF v1 request without vendor consent', function () { - const bidderRequest = { - gdprConsent: { - gdprApplies: true, - consentString: 'consentString', - vendorData: { - vendorConsents: { - '11': false - }, - purposeConsents: { - '1': true - } - }, - apiVersion: 1 - } - }; - - const requests = qcSpec.buildRequests([bidRequest], bidderRequest); - - expect(requests).to.equal(undefined); - }); - - it('blocks TCF v1 request without consent for purpose 1', function () { - const bidderRequest = { - gdprConsent: { - gdprApplies: true, - consentString: 'consentString', - vendorData: { - vendorConsents: { - '11': true - }, - purposeConsents: { - '1': false - } - }, - apiVersion: 1 - } - }; - - const requests = qcSpec.buildRequests([bidRequest], bidderRequest); - - expect(requests).to.equal(undefined); - }); - it('allows TCF v2 request when Quantcast has consent for purpose 1', function() { const bidderRequest = { gdprConsent: { diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index a52a72977a1..9a517f3baa2 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -51,6 +51,7 @@ import 'src/prebid.js'; import {hook} from '../../../src/hook.js'; import {mockGdprConsent} from '../../helpers/consentData.js'; import {getPPID} from '../../../src/adserver.js'; +import {uninstall as uninstallGdprEnforcement} from 'modules/gdprEnforcement.js'; let assert = require('chai').assert; let expect = require('chai').expect; @@ -143,6 +144,7 @@ describe('User ID', function () { before(function () { hook.ready(); + uninstallGdprEnforcement(); localStorage.removeItem(PBJS_USER_ID_OPTOUT_NAME); }); From 7024ce9c5d8dd619e23b44e1f1affec8b97f492c Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Tue, 31 May 2022 12:53:52 -0700 Subject: [PATCH 7/8] Extract `hasPurpose1Consent` util function --- modules/craftBidAdapter.js | 1 - modules/pixfutureBidAdapter.js | 13 ++-------- modules/sspBCBidAdapter.js | 1 + modules/teadsBidAdapter.js | 14 +++-------- modules/ucfunnelBidAdapter.js | 15 +++-------- modules/winrBidAdapter.js | 21 ++-------------- modules/yahoosspBidAdapter.js | 11 ++------- test/spec/modules/teadsBidAdapter_spec.js | 26 ++++++++++---------- test/spec/modules/yahoosspBidAdapter_spec.js | 1 + 9 files changed, 28 insertions(+), 75 deletions(-) diff --git a/modules/craftBidAdapter.js b/modules/craftBidAdapter.js index 4ece93dbc32..9ba5a161620 100644 --- a/modules/craftBidAdapter.js +++ b/modules/craftBidAdapter.js @@ -1,7 +1,6 @@ import { convertCamelToUnderscore, convertTypes, - deepAccess, getBidRequest, isArray, isEmpty, diff --git a/modules/pixfutureBidAdapter.js b/modules/pixfutureBidAdapter.js index aa2a12cc513..10afa4f3a20 100644 --- a/modules/pixfutureBidAdapter.js +++ b/modules/pixfutureBidAdapter.js @@ -14,6 +14,7 @@ import { transformBidderParamKeywords } from '../src/utils.js'; import {auctionManager} from '../src/auctionManager.js'; +import {hasPurpose1Consent} from '../src/utils/gpdr.js'; const SOURCE = 'pbjs'; const storageManager = getStorageManager({bidderCode: 'pixfuture'}); @@ -163,7 +164,7 @@ export const spec = { getUserSyncs: function (syncOptions, bid, gdprConsent) { var pixid = ''; if (typeof bid[0] === 'undefined' || bid[0] === null) { pixid = '0'; } else { pixid = bid[0].body.pix_id; } - if (syncOptions.iframeEnabled && hasPurpose1Consent({gdprConsent})) { + if (syncOptions.iframeEnabled && hasPurpose1Consent(gdprConsent)) { return [{ type: 'iframe', url: 'https://gosrv.pixfuture.com/cookiesync?adsync=' + gdprConsent.consentString + '&pixid=' + pixid + '&gdprconcent=' + gdprConsent.gdprApplies @@ -196,16 +197,6 @@ function newBid(serverBid, rtbBid, placementId, uuid) { return bid; } -function hasPurpose1Consent(bidderRequest) { - let result = true; - if (bidderRequest && bidderRequest.gdprConsent) { - if (bidderRequest.gdprConsent.gdprApplies && bidderRequest.gdprConsent.apiVersion === 2) { - result = !!(deepAccess(bidderRequest.gdprConsent, 'vendorData.purpose.consents.1') === true); - } - } - return result; -} - // Functions related optional parameters function bidToTag(bid) { const tag = {}; diff --git a/modules/sspBCBidAdapter.js b/modules/sspBCBidAdapter.js index 67f806ff792..dd621fe5f9a 100644 --- a/modules/sspBCBidAdapter.js +++ b/modules/sspBCBidAdapter.js @@ -718,6 +718,7 @@ const spec = { }, getUserSyncs(syncOptions, serverResponses, gdprConsent) { let mySyncs = []; + // TODO: the check on CMP api version does not seem to make sense here. It means "always run the usersync unless an old (v1) CMP was detected". No attention is paid to the consent choices. if (syncOptions.iframeEnabled && consentApiVersion != 1) { mySyncs.push({ type: 'iframe', diff --git a/modules/teadsBidAdapter.js b/modules/teadsBidAdapter.js index ea94411b80f..c60379c2110 100644 --- a/modules/teadsBidAdapter.js +++ b/modules/teadsBidAdapter.js @@ -67,7 +67,7 @@ export const spec = { let isCmp = typeof gdpr.gdprApplies === 'boolean'; let isConsentString = typeof gdpr.consentString === 'string'; let status = isCmp - ? findGdprStatus(gdpr.gdprApplies, gdpr.vendorData, gdpr.apiVersion) + ? findGdprStatus(gdpr.gdprApplies, gdpr.vendorData) : gdprStatus.CMP_NOT_FOUND_OR_ERROR; payload.gdpr_iab = { consent: isConsentString ? gdpr.consentString : '', @@ -165,10 +165,10 @@ function getTimeToFirstByte(win) { return ttfbWithTimingV1 ? ttfbWithTimingV1.toString() : ''; } -function findGdprStatus(gdprApplies, gdprData, apiVersion) { +function findGdprStatus(gdprApplies, gdprData) { let status = gdprStatus.GDPR_APPLIES_PUBLISHER; if (gdprApplies) { - if (isGlobalConsent(gdprData, apiVersion)) { + if (gdprData && !gdprData.isServiceSpecific) { status = gdprStatus.GDPR_APPLIES_GLOBAL; } } else { @@ -177,14 +177,6 @@ function findGdprStatus(gdprApplies, gdprData, apiVersion) { return status; } -function isGlobalConsent(gdprData, apiVersion) { - return gdprData && apiVersion === 1 - ? (gdprData.hasGlobalScope || gdprData.hasGlobalConsent) - : gdprData && apiVersion === 2 - ? !gdprData.isServiceSpecific - : false; -} - function buildRequestObject(bid) { const reqObj = {}; let placementId = getValue(bid.params, 'placementId'); diff --git a/modules/ucfunnelBidAdapter.js b/modules/ucfunnelBidAdapter.js index 5c1b4680611..cdf16eb6c94 100644 --- a/modules/ucfunnelBidAdapter.js +++ b/modules/ucfunnelBidAdapter.js @@ -314,17 +314,10 @@ function getRequestData(bid, bidderRequest) { } if (bidderRequest && bidderRequest.gdprConsent) { - if (bidderRequest.gdprConsent.apiVersion == 1) { - Object.assign(bidData, { - gdpr: bidderRequest.gdprConsent.gdprApplies ? 1 : 0, - euconsent: bidderRequest.gdprConsent.consentString - }); - } else if (bidderRequest.gdprConsent.apiVersion == 2) { - Object.assign(bidData, { - gdpr: bidderRequest.gdprConsent.gdprApplies ? 1 : 0, - 'euconsent-v2': bidderRequest.gdprConsent.consentString - }); - } + Object.assign(bidData, { + gdpr: bidderRequest.gdprConsent.gdprApplies ? 1 : 0, + 'euconsent-v2': bidderRequest.gdprConsent.consentString + }); } if (config.getConfig('coppa')) { diff --git a/modules/winrBidAdapter.js b/modules/winrBidAdapter.js index 072f8320cfc..db8da6ee371 100644 --- a/modules/winrBidAdapter.js +++ b/modules/winrBidAdapter.js @@ -17,6 +17,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; import {find, includes} from '../src/polyfill.js'; import {getStorageManager} from '../src/storageManager.js'; +import {hasPurpose1Consent} from '../src/utils/gpdr.js'; const BIDDER_CODE = 'winr'; const URL = 'https://ib.adnxs.com/ut/v3/prebid'; @@ -355,24 +356,6 @@ function deleteValues(keyPairObj) { } } -function hasPurpose1Consent(bidderRequest) { - let result = true; - if (bidderRequest && bidderRequest.gdprConsent) { - if ( - bidderRequest.gdprConsent.gdprApplies && - bidderRequest.gdprConsent.apiVersion === 2 - ) { - result = !!( - deepAccess( - bidderRequest.gdprConsent, - 'vendorData.purpose.consents.1' - ) === true - ); - } - } - return result; -} - function formatRequest(payload, bidderRequest) { let request = []; let options = { @@ -381,7 +364,7 @@ function formatRequest(payload, bidderRequest) { let endpointUrl = URL; - if (!hasPurpose1Consent(bidderRequest)) { + if (!hasPurpose1Consent(bidderRequest?.gdprConsent)) { endpointUrl = URL_SIMPLE; } diff --git a/modules/yahoosspBidAdapter.js b/modules/yahoosspBidAdapter.js index a1fdb84774f..94877757075 100644 --- a/modules/yahoosspBidAdapter.js +++ b/modules/yahoosspBidAdapter.js @@ -3,6 +3,7 @@ import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { deepAccess, isFn, isStr, isNumber, isArray, isEmpty, isPlainObject, generateUUID, logInfo, logWarn } from '../src/utils.js'; import { config } from '../src/config.js'; import { Renderer } from '../src/Renderer.js'; +import {hasPurpose1Consent} from '../src/utils/gpdr.js'; const INTEGRATION_METHOD = 'prebid.js'; const BIDDER_CODE = 'yahoossp'; @@ -54,14 +55,6 @@ const SUPPORTED_USER_ID_SOURCES = [ ]; /* Utility functions */ -function hasPurpose1Consent(bidderRequest) { - if (bidderRequest && bidderRequest.gdprConsent) { - if (bidderRequest.gdprConsent.gdprApplies && bidderRequest.gdprConsent.apiVersion === 2) { - return deepAccess(bidderRequest.gdprConsent, 'vendorData.purpose.consents.1') === true; - } - } - return true; -} function getSize(size) { return { @@ -547,7 +540,7 @@ export const spec = { } }; - requestOptions.withCredentials = hasPurpose1Consent(bidderRequest); + requestOptions.withCredentials = hasPurpose1Consent(bidderRequest.gdprConsent); const filteredBidRequests = filterBidRequestByMode(validBidRequests); diff --git a/test/spec/modules/teadsBidAdapter_spec.js b/test/spec/modules/teadsBidAdapter_spec.js index 8ebe7b907f5..e1843b57f94 100644 --- a/test/spec/modules/teadsBidAdapter_spec.js +++ b/test/spec/modules/teadsBidAdapter_spec.js @@ -147,9 +147,9 @@ describe('teadsBidAdapter', () => { 'consentString': consentString, 'gdprApplies': true, 'vendorData': { - 'hasGlobalConsent': false + 'isServiceSpecific': true }, - 'apiVersion': 1 + 'apiVersion': 2 } }; @@ -159,7 +159,7 @@ describe('teadsBidAdapter', () => { expect(payload.gdpr_iab).to.exist; expect(payload.gdpr_iab.consent).to.equal(consentString); expect(payload.gdpr_iab.status).to.equal(12); - expect(payload.gdpr_iab.apiVersion).to.equal(1); + expect(payload.gdpr_iab.apiVersion).to.equal(2); }); it('should add referer info to payload', function () { @@ -245,9 +245,9 @@ describe('teadsBidAdapter', () => { 'consentString': consentString, 'gdprApplies': true, 'vendorData': { - 'hasGlobalScope': true + 'isServiceSpecific': false, }, - 'apiVersion': 1 + 'apiVersion': 2 } }; @@ -257,7 +257,7 @@ describe('teadsBidAdapter', () => { expect(payload.gdpr_iab).to.exist; expect(payload.gdpr_iab.consent).to.equal(consentString); expect(payload.gdpr_iab.status).to.equal(11); - expect(payload.gdpr_iab.apiVersion).to.equal(1); + expect(payload.gdpr_iab.apiVersion).to.equal(2); }); it('should send GDPR TCF2 to endpoint with 12 status', function() { @@ -294,7 +294,7 @@ describe('teadsBidAdapter', () => { 'consentString': undefined, 'gdprApplies': undefined, 'vendorData': undefined, - 'apiVersion': 1 + 'apiVersion': 2 } }; @@ -304,7 +304,7 @@ describe('teadsBidAdapter', () => { expect(payload.gdpr_iab).to.exist; expect(payload.gdpr_iab.consent).to.equal(''); expect(payload.gdpr_iab.status).to.equal(22); - expect(payload.gdpr_iab.apiVersion).to.equal(1); + expect(payload.gdpr_iab.apiVersion).to.equal(2); }); it('should send GDPR to endpoint with 0 status', function() { @@ -319,7 +319,7 @@ describe('teadsBidAdapter', () => { 'vendorData': { 'hasGlobalScope': false }, - 'apiVersion': 1 + 'apiVersion': 2 } }; @@ -329,7 +329,7 @@ describe('teadsBidAdapter', () => { expect(payload.gdpr_iab).to.exist; expect(payload.gdpr_iab.consent).to.equal(consentString); expect(payload.gdpr_iab.status).to.equal(0); - expect(payload.gdpr_iab.apiVersion).to.equal(1); + expect(payload.gdpr_iab.apiVersion).to.equal(2); }); it('should send GDPR to endpoint with 0 status when gdprApplies = false (vendorData = undefined)', function() { @@ -341,7 +341,7 @@ describe('teadsBidAdapter', () => { 'consentString': undefined, 'gdprApplies': false, 'vendorData': undefined, - 'apiVersion': 1 + 'apiVersion': 2 } }; @@ -351,7 +351,7 @@ describe('teadsBidAdapter', () => { expect(payload.gdpr_iab).to.exist; expect(payload.gdpr_iab.consent).to.equal(''); expect(payload.gdpr_iab.status).to.equal(0); - expect(payload.gdpr_iab.apiVersion).to.equal(1); + expect(payload.gdpr_iab.apiVersion).to.equal(2); }); it('should send GDPR to endpoint with 12 status when apiVersion = 0', function() { @@ -364,7 +364,7 @@ describe('teadsBidAdapter', () => { 'consentString': consentString, 'gdprApplies': true, 'vendorData': { - 'hasGlobalScope': false + 'isServiceSpecific': true }, 'apiVersion': 0 } diff --git a/test/spec/modules/yahoosspBidAdapter_spec.js b/test/spec/modules/yahoosspBidAdapter_spec.js index 4b157865c06..fca2669f74a 100644 --- a/test/spec/modules/yahoosspBidAdapter_spec.js +++ b/test/spec/modules/yahoosspBidAdapter_spec.js @@ -809,6 +809,7 @@ describe('YahooSSP Bid Adapter:', () => { describe('Request Headers validation:', () => { it('should return request objects with the relevant custom headers and content type declaration', () => { const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); + bidderRequest.gdprConsent.gdprApplies = false; const options = spec.buildRequests(validBidRequests, bidderRequest).options; expect(options).to.deep.equal( { From 8f479614e94a885747c9441c3b6dede821d2f0bd Mon Sep 17 00:00:00 2001 From: Patrick McCann Date: Tue, 7 Jun 2022 08:29:59 -0400 Subject: [PATCH 8/8] Update quantcastBidAdapter.js --- modules/quantcastBidAdapter.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/modules/quantcastBidAdapter.js b/modules/quantcastBidAdapter.js index b6aed28bd70..19a559edfda 100644 --- a/modules/quantcastBidAdapter.js +++ b/modules/quantcastBidAdapter.js @@ -75,13 +75,6 @@ function makeBannerImp(bid) { }; } -function getDomain(url) { - if (!url) { - return url; - } - return url.replace('http://', '').replace('https://', '').replace('www.', '').split(/[/?#]/)[0]; -} - function checkTCF(tcData) { let restrictions = tcData.publisher ? tcData.publisher.restrictions : {}; let qcRestriction = restrictions && restrictions[PURPOSE_DATA_COLLECT]