Skip to content

CCPA additions #4502

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Dec 4, 2019
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 175 additions & 19 deletions modules/consentManagement.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
*/
import * as utils from '../src/utils';
import { config } from '../src/config';
import { gdprDataHandler } from '../src/adapterManager';
import { gdprDataHandler, uspDataHandler } from '../src/adapterManager';
import includes from 'core-js/library/fn/array/includes';
import strIncludes from 'core-js/library/fn/string/includes';

const DEFAULT_CMP = 'iab';
const DEFAULT_CONSENT_TIMEOUT = 10000;
const DEFAULT_CONSENT_TIMEOUT_USP = 50;
const DEFAULT_ALLOW_AUCTION_WO_CONSENT = true;

export let userCMP;
Expand All @@ -22,9 +22,16 @@ export let staticConsentData;
let consentData;
let addedConsentHook = false;

// usp
export let consentTimeoutUSP;
let addedConsentHookUSP = false;

// add new CMPs here, with their dedicated lookup function
const cmpCallMap = {
'iab': lookupIabConsent,
'gdpr': lookupIabConsent,
'ccpa': lookupUspConsent,
'usp': lookupUspConsent,
'static': lookupStaticConsentData
};

Expand Down Expand Up @@ -182,6 +189,101 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) {
}
}

function lookupUspConsent(uspSuccess, cmpError, hookConfig) {
function handleCmpResponseCallbacks() {
const uspResponse = {};

function afterEach() {
if (uspResponse.usPrivacy) {
uspSuccess(uspResponse, hookConfig);
}
}

return {
consentDataCallback: function (consentResponse) {
uspResponse.usPrivacy = consentResponse.usPrivacy;
afterEach();
}
}
}

let callbackHandler = handleCmpResponseCallbacks();
let uspapiCallbacks = {};

// the following code also determines where the USP is located and uses the proper workflow to communicate with it:
// check to see if USP is found on the same window level as prebid and call it directly if so
// check to see if prebid is in a safeframe (with USP support)
// else assume prebid may be inside an iframe and use the IAB USP locator code to see if USP's located in a higher parent window. this works in cross domain iframes
// if the USP is not found, the iframe function will call the cmpError exit callback to abort the rest of the USP workflow
let f = window;
let uspFrame;
while (!uspFrame) {
try {
if (f.frames['__uspapiLocator']) uspFrame = f;
} catch (e) { }
if (f === window.top) break;
f = f.parent;
}

if (!uspFrame) {
return cmpError('USP not found.', hookConfig);
}

callUspWhileInIframe('getUSPData', uspFrame, callbackHandler.consentDataCallback);

if (!uspFrame) {
return cmpError('USP frame not found.', hookConfig);
}

callUspWhileInIframe('getUSPData', uspFrame, callbackHandler.consentDataCallback);

function callUspWhileInIframe(commandName, uspFrame, moduleCallback) {
/* Setup up a __uspapi function to do the postMessage and stash the callback. */
window.__uspapi = function (cmd, ver, callback) {
let callId = Math.random() + '';
let msg = {
__uspapiCall: {
command: cmd,
version: ver,
callId: callId
}
};
uspapiCallbacks[callId] = callback;
uspFrame.postMessage(msg, '*');
};

/** when we get the return message, call the stashed callback */
window.addEventListener('message', readPostMessageResponse, false);

// call uspapi
window.__uspapi(commandName, 1, uspIframeCallback);

function readPostMessageResponse(event) {
let res = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
if (res.__uspapiReturn && res.__uspapiReturn.callId) {
let i = res.__uspapiReturn;
if (typeof uspapiCallbacks[i.callId] !== 'undefined') {
uspapiCallbacks[i.callId](i.returnValue, i.success);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

correctly passing both the returnValue and success params, but the callback only handles the first param.

delete uspapiCallbacks[i.callId];
}
}
}

function removePostMessageListener() {
window.removeEventListener('message', readPostMessageResponse, false);
}

function uspIframeCallback(consentObject) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as mentioned in the comment for line 266, there are two arguments passed, but this is only handling one of them.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

depending on how you handle it, you may also want to pass the second argument to moduleCallback()

removePostMessageListener();
moduleCallback(consentObject);
}
}
}

export function requestUspBidsHook(next, reqBidsConfigObj) {
requestBidsHook(next, reqBidsConfigObj, true);
}

/**
* If consentManagement module is enabled (ie included in setConfig), this hook function will attempt to fetch the
* user's encoded consent string from the supported CMP. Once obtained, the module will store this
Expand All @@ -190,7 +292,15 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) {
* @param {object} reqBidsConfigObj required; This is the same param that's used in pbjs.requestBids.
* @param {function} fn required; The next function in the chain, used by hook.js
*/
export function requestBidsHook(fn, reqBidsConfigObj) {
export function requestBidsHook(fn, reqBidsConfigObj, isUSP = false) {
let userModule = userCMP;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as per a comment below, I'm not sure this is being used properly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my comment below your comment below.

let processFn = processCmpData;

if (isUSP) {
userModule = 'usp';
processFn = processUspData;
}

// preserves all module related variables for the current auction instance (used primiarily for concurrent auctions)
const hookConfig = {
context: this,
Expand All @@ -199,27 +309,29 @@ export function requestBidsHook(fn, reqBidsConfigObj) {
adUnits: reqBidsConfigObj.adUnits || $$PREBID_GLOBAL$$.adUnits,
bidsBackHandler: reqBidsConfigObj.bidsBackHandler,
haveExited: false,
timer: null
timer: null,
userModule: userModule
};

// in case we already have consent (eg during bid refresh)
if (consentData) {
return exitModule(null, hookConfig);
}

if (!includes(Object.keys(cmpCallMap), userCMP)) {
utils.logWarn(`CMP framework (${userCMP}) is not a supported framework. Aborting consentManagement module and resuming auction.`);
if (!includes(Object.keys(cmpCallMap), userModule)) {
utils.logWarn(`CMP framework (${userModule}) is not a supported framework. Aborting consentManagement module and resuming auction.`);
return hookConfig.nextFn.apply(hookConfig.context, hookConfig.args);
}

cmpCallMap[userCMP].call(this, processCmpData, cmpFailed, hookConfig);
cmpCallMap[userModule].call(this, processFn, cmpFailed, hookConfig);

// only let this code run if module is still active (ie if the callbacks used by CMPs haven't already finished)
if (!hookConfig.haveExited) {
if (consentTimeout === 0) {
processCmpData(undefined, hookConfig);
let timeout = userModule === 'usp' ? consentTimeoutUSP : consentTimeout;
if (timeout === 0) {
processFn(undefined, hookConfig);
} else {
hookConfig.timer = setTimeout(cmpTimedOut.bind(null, hookConfig), consentTimeout);
hookConfig.timer = setTimeout(cmpTimedOut.bind(null, hookConfig), timeout);
}
}
}
Expand Down Expand Up @@ -251,6 +363,17 @@ function processCmpData(consentObject, hookConfig) {
}
}

function processUspData(consentObject, hookConfig) {
if (!(consentObject && consentObject.usPrivacy)) {
cmpFailed(`USP returned unexpected value during lookup process.`, hookConfig, consentObject);
return;
}

clearTimeout(hookConfig.timer);
storeUspConsentData(consentObject);
exitModule(null, hookConfig);
}

/**
* General timeout callback when interacting with CMP takes too long.
*/
Expand Down Expand Up @@ -287,6 +410,13 @@ function storeConsentData(cmpConsentObject) {
gdprDataHandler.setConsentData(consentData);
}

function storeUspConsentData(consentObject) {
consentData = {
usPrivacy: consentObject ? consentObject.usPrivacy : undefined
};
uspDataHandler.setConsentData(consentData);
}

/**
* This function handles the exit logic for the module.
* There are several paths in the module's logic to call this function and we only allow 1 of the 3 potential exits to happen before suppressing others.
Expand Down Expand Up @@ -336,18 +466,26 @@ function exitModule(errMsg, hookConfig, extraArgs) {
export function resetConsentData() {
consentData = undefined;
gdprDataHandler.setConsentData(null);
uspDataHandler.setConsentData(null);
}

/**
* A configuration function that initializes some module variables, as well as add a hook into the requestBids function
* @param {object} config required; consentManagement module config settings; cmp (string), timeout (int), allowAuctionWithoutConsent (boolean)
*/
export function setConsentConfig(config) {
if (utils.isStr(config.cmpApi)) {
userCMP = config.cmpApi;
} else {
userCMP = DEFAULT_CMP;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where are we handling the default?

utils.logInfo(`consentManagement config did not specify cmp. Using system default setting (${DEFAULT_CMP}).`);
export function setConsentConfig(config, consentModule) {
if (consentModule) {
if (['gdpr', 'iab'].includes(consentModule)) {
userCMP = 'iab';
}

if (consentModule === 'static') {
userCMP = 'static';
}

if (!['gdpr', 'iab', 'static', 'usp'].includes(consentModule) && consentModule) {
userCMP = consentModule;
}
}

if (utils.isNumber(config.timeout)) {
Expand All @@ -357,6 +495,13 @@ export function setConsentConfig(config) {
utils.logInfo(`consentManagement config did not specify timeout. Using system default setting (${DEFAULT_CONSENT_TIMEOUT}).`);
}

if (utils.isNumber(config.uspTimeout)) {
consentTimeoutUSP = config.uspTimeout
} else {
consentTimeoutUSP = DEFAULT_CONSENT_TIMEOUT_USP;
utils.logInfo(`consentManagement config did not specify timeout f or USP. Using system default setting: (${DEFAULT_CONSENT_TIMEOUT_USP}).`);
}

if (typeof config.allowAuctionWithoutConsent === 'boolean') {
allowAuction = config.allowAuctionWithoutConsent;
} else {
Expand All @@ -374,9 +519,20 @@ export function setConsentConfig(config) {
utils.logError(`consentManagement config with cmpApi: 'static' did not specify consentData. No consents will be available to adapters.`);
}
}
if (!addedConsentHook) {

if (consentModule === 'usp' && !addedConsentHookUSP) {
$$PREBID_GLOBAL$$.requestBids.before(requestUspBidsHook, 50);
addedConsentHookUSP = true;
}

if (!addedConsentHook && consentModule === 'gdpr') {
$$PREBID_GLOBAL$$.requestBids.before(requestBidsHook, 50);
addedConsentHook = true;
}
addedConsentHook = true;
}
config.getConfig('consentManagement', config => setConsentConfig(config.consentManagement));

config.getConfig('consentManagement', config => {
const consentManagement = { ...config.consentManagement };
const consentChecks = consentManagement.consentAPIs ? new Set([...consentManagement.consentAPIs]) : new Set([]);
[...consentChecks].map(module => setConsentConfig(consentManagement, module));
});
10 changes: 10 additions & 0 deletions src/adapterManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,16 @@ export let gdprDataHandler = {
}
};

export let uspDataHandler = {
consentData: null,
setConsentData: function(consentInfo) {
uspDataHandler.consentData = consentInfo;
},
getConsentData: function() {
return uspDataHandler.consentData;
}
};

adapterManager.makeBidRequests = function(adUnits, auctionStart, auctionId, cbTimeout, labels) {
let bidRequests = [];

Expand Down
Loading