From 3682d62b76ece8a6fa13efc0bd1d362985006973 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Tue, 7 Jun 2022 05:35:40 -0700 Subject: [PATCH] Prebid 7: remove support for TCFv1 (#8356) * Prebid 7: remove support for TCFv1 * Remove superfluous argument * convert let to const * Small improvements * Fix behavior on timeout: 0 * Remove some of the duplication for bidders checking p1 consent * Extract `hasPurpose1Consent` util function * Update quantcastBidAdapter.js Co-authored-by: Patrick McCann Co-authored-by: Patrick McCann --- modules/apacdexBidAdapter.js | 11 +- modules/appnexusBidAdapter.js | 15 +- modules/consentManagement.js | 317 ++++-------------- modules/craftBidAdapter.js | 14 +- 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/pixfutureBidAdapter.js | 13 +- modules/prebidServerBidAdapter/index.js | 11 +- modules/proxistoreBidAdapter.js | 10 - modules/quantcastBidAdapter.js | 15 +- modules/quantcastIdSystem.js | 8 +- modules/sspBCBidAdapter.js | 1 + modules/teadsBidAdapter.js | 14 +- modules/ucfunnelBidAdapter.js | 15 +- modules/userId/index.js | 23 +- modules/winrBidAdapter.js | 21 +- modules/yahoosspBidAdapter.js | 11 +- src/utils/gpdr.js | 14 + test/spec/modules/consentManagement_spec.js | 275 +++------------ .../modules/prebidServerBidAdapter_spec.js | 54 ++- test/spec/modules/quantcastBidAdapter_spec.js | 68 ---- test/spec/modules/teadsBidAdapter_spec.js | 26 +- test/spec/modules/userId_spec.js | 2 + test/spec/modules/yahoosspBidAdapter_spec.js | 1 + 30 files changed, 315 insertions(+), 929 deletions(-) create mode 100644 src/utils/gpdr.js diff --git a/modules/apacdexBidAdapter.js b/modules/apacdexBidAdapter.js index 9f351f9a55b..a6ab1ea03da 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'; import {parseDomain} from '../src/refererDetection.js'; const BIDDER_CODE = 'apacdex'; const ENDPOINT = 'https://useast.quantumdex.io/auction/pbjs' @@ -344,14 +345,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 55b9595ea4f..9a2d3fa0a50 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'; @@ -384,7 +385,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' @@ -547,16 +548,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 = { @@ -565,7 +556,7 @@ function formatRequest(payload, bidderRequest) { let endpointUrl = URL; - if (!hasPurpose1Consent(bidderRequest)) { + if (!hasPurpose1Consent(bidderRequest?.gdprConsent)) { endpointUrl = URL_SIMPLE; } diff --git a/modules/consentManagement.js b/modules/consentManagement.js index 80b56eb8c97..71c47b62407 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 {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; } @@ -95,7 +73,7 @@ function lookupIabConsent({onSuccess, onError, width, height}) { }; } - 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') { @@ -106,31 +84,8 @@ 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(); + const cmpCallbacks = {}; + const { cmpFrame, cmpFunction } = findCMP(); if (!cmpFrame) { return onError('CMP not found.'); @@ -145,100 +100,47 @@ function lookupIabConsent({onSuccess, onError, width, height}) { // 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...'); - 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, cmpResponseCallback); } 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, cmpResponseCallback); } 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) { + const callId = Math.random() + ''; + const 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`; - 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); @@ -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; @@ -267,7 +166,7 @@ function loadConsentData(cb, width = 1, height = 1) { } isDone = true; gdprDataHandler.setConsentData(consentData); - if (cb != null) { + if (typeof cb === 'function') { cb(shouldCancelAuction, errMsg, ...extraArgs); } } @@ -280,38 +179,37 @@ function loadConsentData(cb, width = 1, height = 1) { const callbacks = { onSuccess: (data) => done(data, false), 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); + done(null, true, msg, ...extraArgs); } } - cmpCallMap[userCMP]({ - width, - height, - ...callbacks - }); + cmpCallMap[userCMP](callbacks); if (!isDone) { if (consentTimeout === 0) { - processCmpData(undefined, callbacks); + done(storeConsentData(undefined), false) } 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 +219,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,23 +247,10 @@ 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; + const gdprApplies = consentObject && typeof consentObject.gdprApplies === 'boolean' ? consentObject.gdprApplies : gdprScope; + const tcString = consentObject && consentObject.tcString; return !!( (typeof gdprApplies !== 'boolean') || (gdprApplies === true && !isStr(tcString)) @@ -400,24 +259,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()) { + 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 +274,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; } @@ -453,7 +293,6 @@ export function resetConsentData() { consentData = undefined; userCMP = undefined; consentTimeout = undefined; - cmpVersion = 0; gdprDataHandler.reset(); } @@ -483,11 +322,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; @@ -507,12 +341,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/modules/craftBidAdapter.js b/modules/craftBidAdapter.js index 777b71e31e3..147e45d3d65 100644 --- a/modules/craftBidAdapter.js +++ b/modules/craftBidAdapter.js @@ -1,7 +1,6 @@ import { convertCamelToUnderscore, convertTypes, - deepAccess, getBidRequest, isArray, isEmpty, @@ -14,6 +13,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'; @@ -137,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 4a54367a2a2..c29d216509f 100644 --- a/modules/gdprEnforcement.js +++ b/modules/gdprEnforcement.js @@ -45,7 +45,7 @@ const storageBlocked = []; const biddersBlocked = []; const analyticsBlocked = []; -let addedDeviceAccessHook = false; +let hooksAdded = false; // Helps in stubbing these functions in unit tests. export const internal = { @@ -180,29 +180,23 @@ export function deviceAccessHook(fn, gvlid, moduleName, moduleType, 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, moduleType); - if (isAllowed) { - result.valid = true; - fn.call(this, gvlid, moduleName, moduleType, result); - } else { - curModule && logWarn(`TCF2 denied device access for ${curModule}`); - result.valid = false; - storageBlocked.push(curModule); - fn.call(this, gvlid, moduleName, moduleType, 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, moduleType); + if (isAllowed) { result.valid = true; fn.call(this, gvlid, moduleName, moduleType, result); + } else { + curModule && logWarn(`TCF2 denied device access for ${curModule}`); + result.valid = false; + storageBlocked.push(curModule); + fn.call(this, gvlid, moduleName, moduleType, result); } } else { result.valid = true; @@ -219,19 +213,14 @@ export function deviceAccessHook(fn, gvlid, moduleName, moduleType, 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); @@ -246,24 +235,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); } @@ -278,25 +262,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); } @@ -311,25 +290,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); } @@ -386,20 +360,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 49229b31b38..9d5f6d71c6a 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'; @@ -533,16 +534,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 = { @@ -551,7 +542,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 687eabca2d7..a7a97f4c839 100644 --- a/modules/improvedigitalBidAdapter.js +++ b/modules/improvedigitalBidAdapter.js @@ -17,6 +17,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; @@ -227,7 +228,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 []; } @@ -666,12 +667,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 212696383d9..b76a759a047 100644 --- a/modules/inskinBidAdapter.js +++ b/modules/inskinBidAdapter.js @@ -83,31 +83,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 a710db7a712..57cbe6acd07 100644 --- a/modules/kubientBidAdapter.js +++ b/modules/kubientBidAdapter.js @@ -152,15 +152,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 5f3339ee5b4..4cd6b60e8a2 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'; @@ -479,16 +480,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 = { @@ -497,7 +488,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 b69f3b09d97..cfe18301b32 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) { @@ -387,7 +379,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/pixfutureBidAdapter.js b/modules/pixfutureBidAdapter.js index 70b741d1028..bffd5badded 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'}); @@ -164,7 +165,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 @@ -197,16 +198,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/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index 7aa886e8977..ecf13ae5659 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; @@ -1151,16 +1152,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 9db765a9e2a..19a559edfda 100644 --- a/modules/quantcastBidAdapter.js +++ b/modules/quantcastBidAdapter.js @@ -75,14 +75,7 @@ function makeBannerImp(bid) { }; } -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] @@ -146,11 +139,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/sspBCBidAdapter.js b/modules/sspBCBidAdapter.js index c8e3d6cb5d3..89bf4bb331d 100644 --- a/modules/sspBCBidAdapter.js +++ b/modules/sspBCBidAdapter.js @@ -712,6 +712,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 841b134d692..c40f76e743e 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 1718382946c..33bcda29881 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/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/modules/winrBidAdapter.js b/modules/winrBidAdapter.js index 9113ea695c2..3b86e5b59dc 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'; @@ -356,24 +357,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 = { @@ -382,7 +365,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 18356725f99..926c3317f1d 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/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/consentManagement_spec.js b/test/spec/modules/consentManagement_spec.js index b0cd0197f8b..a286a4ed1b9 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; @@ -378,41 +302,59 @@ 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; + } }) }); 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 +362,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 +370,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 +426,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 +450,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 +475,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 +502,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 +522,7 @@ describe('consentManagement', function () { args[2](testConsentData, true); }); - setConsentConfig(goodConfigWithAllowAuction); + setConsentConfig(goodConfig); requestBidsHook(() => { didHookReturn = true; @@ -730,7 +549,7 @@ describe('consentManagement', function () { args[2](testConsentData, true); }); - setConsentConfig(goodConfigWithAllowAuction); + setConsentConfig(goodConfig); requestBidsHook(() => { didHookReturn = true; @@ -755,7 +574,7 @@ describe('consentManagement', function () { args[2](testConsentData, true); }); - setConsentConfig(goodConfigWithAllowAuction); + setConsentConfig(goodConfig); requestBidsHook(() => { didHookReturn = true; @@ -769,7 +588,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 +596,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()); diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index c79362a6577..7aa6a5c7f42 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -644,6 +644,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(); @@ -654,16 +662,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 }); @@ -680,17 +685,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(); @@ -712,10 +715,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; @@ -724,7 +724,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'); }); @@ -740,10 +740,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); @@ -760,10 +757,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; @@ -831,17 +825,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 }); @@ -860,10 +851,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 @@ -873,7 +861,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 6ba9d9b4e01..d10fea829bc 100644 --- a/test/spec/modules/quantcastBidAdapter_spec.js +++ b/test/spec/modules/quantcastBidAdapter_spec.js @@ -447,74 +447,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/teadsBidAdapter_spec.js b/test/spec/modules/teadsBidAdapter_spec.js index a8315050970..c0845d93bde 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/userId_spec.js b/test/spec/modules/userId_spec.js index 7ed68ee45d5..95071e6731d 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -50,6 +50,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; @@ -142,6 +143,7 @@ describe('User ID', function () { before(function () { hook.ready(); + uninstallGdprEnforcement(); localStorage.removeItem(PBJS_USER_ID_OPTOUT_NAME); }); diff --git a/test/spec/modules/yahoosspBidAdapter_spec.js b/test/spec/modules/yahoosspBidAdapter_spec.js index 3c958f2cdc8..4dce1358fae 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( {