Skip to content
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

GDPR use getConsentData CMP call to get consentString #2603

Merged
merged 5 commits into from
May 24, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
138 changes: 80 additions & 58 deletions modules/consentManagement.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,79 +34,101 @@ const cmpCallMap = {
};

/**
* This function handles interacting with an IAB compliant CMP to obtain the consentObject value of the user.
* This function handles interacting with an IAB compliant CMP to obtain the consent information of the user.
* Given the async nature of the CMP's API, we pass in acting success/error callback functions to exit this function
* based on the appropriate result.
* @param {function(string)} cmpSuccess acts as a success callback when CMP returns a value; pass along consentObject (string) from CMP
* @param {function(string)} cmpError acts as an error callback while interacting with CMP; pass along an error message (string)
* @param {[objects]} adUnits used in the safeframe workflow to know what sizes to include in the $sf.ext.register call
*/
function lookupIabConsent(cmpSuccess, cmpError, adUnits) {
let cmpCallbacks;
function handleCmpResponseCallbacks() {
const cmpResponse = {};

// check if the CMP is located on the same window level as the prebid code.
// if it's found, directly call the CMP via it's API and call the cmpSuccess callback.
// if it's not found, assume the prebid code may be inside an iframe and the CMP code is located in a higher parent window.
// in this case, use the IAB's iframe locator sample code (which is slightly cutomized) to try to find the CMP and use postMessage() to communicate with the CMP.
function afterEach() {
if (cmpResponse.getConsentData && cmpResponse.getVendorConsents) {
cmpSuccess(cmpResponse);
}
}

return {
consentDataCallback: function(consentResponse) {
cmpResponse.getConsentData = consentResponse;
afterEach();
},
vendorConsentsCallback: function(consentResponse) {
cmpResponse.getVendorConsents = consentResponse;
afterEach();
}
}
}
let callbackHandler = handleCmpResponseCallbacks();
let cmpCallbacks = {};

// to collect the consent information from the user, we perform two calls to the CMP in parallel:
// first to collect the user's consent choices represented in an encoded string (via getConsentData)
// second to collect the user's full unparsed consent information (via getVendorConsents)

// the following code also determines where the CMP is located and uses the proper workflow to communicate with it:
// check to see if CMP is found on the same window level as prebid and call it directly if so
// check to see if prebid is in a safeframe (with CMP support)
// else assume prebid may be inside an iframe and use the IAB CMP locator code to see if CMP's located in a higher parent window. this works in cross domain iframes
// if the CMP is not found, the iframe function will call the cmpError exit callback to abort the rest of the CMP workflow
if (utils.isFn(window.__cmp)) {
window.__cmp('getVendorConsents', null, cmpSuccess);
window.__cmp('getConsentData', null, callbackHandler.consentDataCallback);
window.__cmp('getVendorConsents', null, callbackHandler.vendorConsentsCallback);
} else if (inASafeFrame() && typeof window.$sf.ext.cmp === 'function') {
callCmpWhileInSafeFrame();
callCmpWhileInSafeFrame('getConsentData', callbackHandler.consentDataCallback);
callCmpWhileInSafeFrame('getVendorConsents', callbackHandler.vendorConsentsCallback);
} else {
callCmpWhileInIframe();
// find the CMP frame
let f = window;
let cmpFrame;
while (!cmpFrame) {
try {
if (f.frames['__cmpLocator']) cmpFrame = f;
} catch (e) {}
if (f === window.top) break;
f = f.parent;
}

callCmpWhileInIframe('getConsentData', cmpFrame, callbackHandler.consentDataCallback);
callCmpWhileInIframe('getVendorConsents', cmpFrame, callbackHandler.vendorConsentsCallback);
}

function inASafeFrame() {
return !!(window.$sf && window.$sf.ext);
}

function callCmpWhileInSafeFrame() {
function callCmpWhileInSafeFrame(commandName, callback) {
function sfCallback(msgName, data) {
if (msgName === 'cmpReturn') {
cmpSuccess(data.vendorConsents);
let responseObj = (commandName === 'getConsentData') ? data.vendorConsentData : data.vendorConsents;
callback(responseObj);
}
}

// find sizes from adUnits object
let width = 1;
let height = 1;

if (Array.isArray(adUnits) && adUnits.length > 0) {
let sizes = utils.getAdUnitSizes(adUnits[0]);
width = sizes[0][0];
height = sizes[0][1];
}

window.$sf.ext.register(width, height, sfCallback);
window.$sf.ext.cmp('getVendorConsents');
window.$sf.ext.cmp(commandName);
}

function callCmpWhileInIframe() {
/**
* START OF STOCK CODE FROM IAB 1.1 CMP SPEC
*/

// find the CMP frame
let f = window;
let cmpFrame;
while (!cmpFrame) {
try {
if (f.frames['__cmpLocator']) cmpFrame = f;
} catch (e) {}
if (f === window.top) break;
f = f.parent;
}

cmpCallbacks = {};

function callCmpWhileInIframe(commandName, cmpFrame, moduleCallback) {
/* Setup up a __cmp function to do the postMessage and stash the callback.
This function behaves (from the caller's perspective identicially to the in-frame __cmp call */
window.__cmp = function(cmd, arg, callback) {
if (!cmpFrame) {
removePostMessageListener();

let errmsg = 'CMP not found';
// small customization to properly return error
return cmpError(errmsg);
}
let callId = Math.random() + '';
Expand All @@ -120,34 +142,31 @@ function lookupIabConsent(cmpSuccess, cmpError, adUnits) {
}

/** when we get the return message, call the stashed callback */
// small customization to remove this eventListener later in module
window.addEventListener('message', readPostMessageResponse, false);

/**
* END OF STOCK CODE FROM IAB 1.1 CMP SPEC
*/

// call CMP
window.__cmp('getVendorConsents', null, cmpIframeCallback);
}

function readPostMessageResponse(event) {
// small customization to prevent reading strings from other sources that aren't JSON.stringified
let json = (typeof event.data === 'string' && strIncludes(event.data, 'cmpReturn')) ? JSON.parse(event.data) : event.data;
if (json.__cmpReturn) {
let i = json.__cmpReturn;
cmpCallbacks[i.callId](i.returnValue, i.success);
delete cmpCallbacks[i.callId];
window.__cmp(commandName, null, cmpIframeCallback);

function readPostMessageResponse(event) {
let json = (typeof event.data === 'string' && strIncludes(event.data, 'cmpReturn')) ? JSON.parse(event.data) : event.data;
if (json.__cmpReturn && json.__cmpReturn.callId) {
let i = json.__cmpReturn;
// 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[i.callId] !== 'undefined') {
cmpCallbacks[i.callId](i.returnValue, i.success);
delete cmpCallbacks[i.callId];
}
}
}
}

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

function cmpIframeCallback(consentObject) {
removePostMessageListener();
cmpSuccess(consentObject);
function cmpIframeCallback(consentObject) {
removePostMessageListener();
moduleCallback(consentObject);
}
}
}

Expand Down Expand Up @@ -196,7 +215,10 @@ export function requestBidsHook(reqBidsConfigObj, fn) {
* @param {object} consentObject required; object returned by CMP that contains user's consent choices
*/
function processCmpData(consentObject) {
if (!utils.isPlainObject(consentObject) || !utils.isStr(consentObject.metadata) || consentObject.metadata === '') {
if (
!utils.isPlainObject(consentObject) ||
(!utils.isPlainObject(consentObject.getVendorConsents) || Object.keys(consentObject.getVendorConsents).length === 0) ||
(!utils.isPlainObject(consentObject.getConsentData) || Object.keys(consentObject.getConsentData).length === 0)) {
cmpFailed(`CMP returned unexpected value during lookup process; returned value was (${consentObject}).`);
} else {
clearTimeout(timer);
Expand Down Expand Up @@ -233,9 +255,9 @@ function cmpFailed(errMsg) {
*/
function storeConsentData(cmpConsentObject) {
consentData = {
consentString: (cmpConsentObject) ? cmpConsentObject.metadata : undefined,
vendorData: cmpConsentObject,
gdprApplies: (cmpConsentObject) ? cmpConsentObject.gdprApplies : undefined
consentString: (cmpConsentObject) ? cmpConsentObject.getConsentData.consentData : undefined,
vendorData: (cmpConsentObject) ? cmpConsentObject.getVendorConsents : undefined,
gdprApplies: (cmpConsentObject) ? cmpConsentObject.getConsentData.gdprApplies : undefined
};
gdprDataHandler.setConsentData(consentData);
}
Expand Down
20 changes: 12 additions & 8 deletions test/spec/modules/consentManagement_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ describe('consentManagement', function () {
it('should bypass CMP and simply use previously stored consentData', () => {
let testConsentData = {
gdprApplies: true,
metadata: 'xyz'
consentData: 'xyz'
};

cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => {
Expand All @@ -140,7 +140,7 @@ describe('consentManagement', function () {
let consent = gdprDataHandler.getConsentData();

expect(didHookReturn).to.be.true;
expect(consent.consentString).to.equal(testConsentData.metadata);
expect(consent.consentString).to.equal(testConsentData.consentData);
expect(consent.gdprApplies).to.be.true;
sinon.assert.notCalled(cmpStub);
});
Expand Down Expand Up @@ -178,6 +178,10 @@ describe('consentManagement', function () {
vendorConsents: {
metadata: 'abc123def',
gdprApplies: true
},
vendorConsentData: {
consentData: 'abc123def',
gdprApplies: true
}
}
};
Expand Down Expand Up @@ -227,7 +231,7 @@ describe('consentManagement', function () {
__cmpReturn: {
returnValue: {
gdprApplies: true,
metadata: 'BOJy+UqOJy+UqABAB+AAAAAZ+A=='
consentData: 'BOJy+UqOJy+UqABAB+AAAAAZ+A=='
}
}
}
Expand All @@ -238,7 +242,7 @@ describe('consentManagement', function () {
cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => {
args[2]({
gdprApplies: true,
metadata: 'BOJy+UqOJy+UqABAB+AAAAAZ+A=='
consentData: 'BOJy+UqOJy+UqABAB+AAAAAZ+A=='
});
});

Expand Down Expand Up @@ -280,7 +284,7 @@ describe('consentManagement', function () {
it('performs lookup check and stores consentData for a valid existing user', () => {
let testConsentData = {
gdprApplies: true,
metadata: 'BOJy+UqOJy+UqABAB+AAAAAZ+A=='
consentData: 'BOJy+UqOJy+UqABAB+AAAAAZ+A=='
};
cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => {
args[2](testConsentData);
Expand All @@ -296,12 +300,12 @@ describe('consentManagement', function () {
sinon.assert.notCalled(utils.logWarn);
sinon.assert.notCalled(utils.logError);
expect(didHookReturn).to.be.true;
expect(consent.consentString).to.equal(testConsentData.metadata);
expect(consent.consentString).to.equal(testConsentData.consentData);
expect(consent.gdprApplies).to.be.true;
});

it('throws an error when processCmpData check failed while config had allowAuction set to false', () => {
let testConsentData = null;
let testConsentData = {};
let bidsBackHandlerReturn = false;

cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => {
Expand All @@ -322,7 +326,7 @@ describe('consentManagement', function () {
});

it('throws a warning + stores consentData + calls callback when processCmpData check failed while config had allowAuction set to true', () => {
let testConsentData = null;
let testConsentData = {};

cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => {
args[2](testConsentData);
Expand Down