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

consentManagement - add support for safeframe workflow #2523

Merged
merged 1 commit into from
May 15, 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
34 changes: 32 additions & 2 deletions modules/consentManagement.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ const cmpCallMap = {
* 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) {
function lookupIabConsent(cmpSuccess, cmpError, adUnits) {
let cmpCallbacks;

// check if the CMP is located on the same window level as the prebid code.
Expand All @@ -47,10 +48,37 @@ function lookupIabConsent(cmpSuccess, cmpError) {
// 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.
if (utils.isFn(window.__cmp)) {
window.__cmp('getVendorConsents', null, cmpSuccess);
} else if (inASafeFrame() && typeof window.$sf.ext.cmp === 'function') {
callCmpWhileInSafeFrame();
} else {
callCmpWhileInIframe();
}

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

function callCmpWhileInSafeFrame() {
function sfCallback(msgName, data) {
if (msgName === 'cmpReturn') {
cmpSuccess(data.vendorConsents);
Copy link
Collaborator

Choose a reason for hiding this comment

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

The spec uses data.VendorConsents (with a capital V). Not sure if it's a typo, or may be referring to the full VendorConsents object. should confirm this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I checked internally with our resident CMP developer and he indicated it should be lower-case (ie normal syntax).

Copy link
Collaborator

Choose a reason for hiding this comment

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

cool. Looks good then.

}
}

// 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');
}

function callCmpWhileInIframe() {
/**
* START OF STOCK CODE FROM IAB 1.1 CMP SPEC
Expand Down Expand Up @@ -134,6 +162,7 @@ export function requestBidsHook(config, fn) {
args = arguments;
nextFn = fn;
haveExited = false;
let adUnits = config.adUnits || $$PREBID_GLOBAL$$.adUnits;

// in case we already have consent (eg during bid refresh)
if (consentData) {
Expand All @@ -145,7 +174,7 @@ export function requestBidsHook(config, fn) {
return nextFn.apply(context, args);
}

cmpCallMap[userCMP].call(this, processCmpData, cmpFailed);
cmpCallMap[userCMP].call(this, processCmpData, cmpFailed, adUnits);

// only let this code run if module is still active (ie if the callbacks used by CMPs haven't already finished)
if (!haveExited) {
Expand Down Expand Up @@ -245,6 +274,7 @@ function exitModule(errMsg) {
*/
export function resetConsentData() {
consentData = undefined;
gdprDataHandler.setConsentData(null);
}

/**
Expand Down
29 changes: 29 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,35 @@ exports.transformAdServerTargetingObj = function (targeting) {
}
};

/**
* Read an adUnit object and return the sizes used in an [[728, 90]] format (even if they had [728, 90] defined)
* Preference is given to the `adUnit.mediaTypes.banner.sizes` object over the `adUnit.sizes`
* @param {object} adUnit one adUnit object from the normal list of adUnits
* @returns {array[array[number]]} array of arrays containing numeric sizes
*/
export function getAdUnitSizes(adUnit) {
Copy link
Member

Choose a reason for hiding this comment

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

any reason this is a new function vs reusing parseSizesInput ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

that function converts the sizes into an array of strings like ["300x250"]. I just wanted the actual sizes to reassign into the register call and not have to parse it again.

if (!adUnit) {
return;
}

let sizes = [];
if (adUnit.mediaTypes && adUnit.mediaTypes.banner && Array.isArray(adUnit.mediaTypes.banner.sizes)) {
let bannerSizes = adUnit.mediaTypes.banner.sizes;
if (Array.isArray(bannerSizes[0])) {
sizes = bannerSizes;
} else {
sizes.push(bannerSizes);
}
} else if (Array.isArray(adUnit.sizes)) {
if (Array.isArray(adUnit.sizes[0])) {
sizes = adUnit.sizes;
} else {
sizes.push(adUnit.sizes);
}
}
return sizes;
}

/**
* Parse a GPT-Style general size Array like `[[300, 250]]` or `"300x250,970x90"` into an array of sizes `["300x250"]` or '['300x250', '970x90']'
* @param {array[array|number]} sizeObj Input array or double array [300,250] or [[300,250], [728,90]]
Expand Down
64 changes: 58 additions & 6 deletions test/spec/modules/consentManagement_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ describe('consentManagement', function () {
utils.logWarn.restore();
config.resetConfig();
$$PREBID_GLOBAL$$.requestBids.removeHook(requestBidsHook);
gdprDataHandler.consentData = null;
resetConsentData();
});

it('should return Warning message and return to hooked function', () => {
Expand Down Expand Up @@ -113,7 +113,7 @@ describe('consentManagement', function () {
$$PREBID_GLOBAL$$.requestBids.removeHook(requestBidsHook);
cmpStub.restore();
delete window.__cmp;
gdprDataHandler.consentData = null;
resetConsentData();
});

it('should bypass CMP and simply use previously stored consentData', () => {
Expand Down Expand Up @@ -146,13 +146,66 @@ describe('consentManagement', function () {
});
});

describe('CMP workflow for safeframe page', () => {
let registerStub = sinon.stub();

beforeEach(() => {
didHookReturn = false;
window.$sf = {
ext: {
register: function() {},
cmp: function() {}
}
};
sinon.stub(utils, 'logError');
sinon.stub(utils, 'logWarn');
});

afterEach(() => {
delete window.$sf;
config.resetConfig();
$$PREBID_GLOBAL$$.requestBids.removeHook(requestBidsHook);
registerStub.restore();
utils.logError.restore();
utils.logWarn.restore();
resetConsentData();
});

it('should return the consent data from a safeframe callback', () => {
var testConsentData = {
data: {
msgName: 'cmpReturn',
vendorConsents: {
metadata: 'abc123def',
gdprApplies: true
}
}
};
registerStub = sinon.stub(window.$sf.ext, 'register').callsFake((...args) => {
args[2](testConsentData.data.msgName, testConsentData.data);
});

setConfig(goodConfigWithAllowAuction);
debugger; //eslint-disable-line
requestBidsHook({adUnits: [{ sizes: [[300, 250]] }]}, () => {
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('abc123def');
expect(consent.gdprApplies).to.be.true;
});
});

describe('CMP workflow for iframed page', () => {
let eventStub = sinon.stub();
let cmpStub = sinon.stub();

beforeEach(() => {
didHookReturn = false;
resetConsentData();
window.__cmp = function() {};
sinon.stub(utils, 'logError');
sinon.stub(utils, 'logWarn');
Expand All @@ -166,7 +219,7 @@ describe('consentManagement', function () {
delete window.__cmp;
utils.logError.restore();
utils.logWarn.restore();
gdprDataHandler.consentData = null;
resetConsentData();
});

it('should return the consent string from a postmessage + addEventListener response', () => {
Expand Down Expand Up @@ -210,7 +263,6 @@ describe('consentManagement', function () {

beforeEach(() => {
didHookReturn = false;
resetConsentData();
sinon.stub(utils, 'logError');
sinon.stub(utils, 'logWarn');
window.__cmp = function() {};
Expand All @@ -223,7 +275,7 @@ describe('consentManagement', function () {
utils.logError.restore();
utils.logWarn.restore();
delete window.__cmp;
gdprDataHandler.consentData = null;
resetConsentData();
});

it('performs lookup check and stores consentData for a valid existing user', () => {
Expand Down
28 changes: 28 additions & 0 deletions test/spec/utils_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -789,4 +789,32 @@ describe('Utils', function () {
expect(test2).to.equal(var2);
});
});

describe('getAdUnitSizes', () => {
it('returns an empty response when adUnits is undefined', () => {
let sizes = utils.getAdUnitSizes();
expect(sizes).to.be.undefined;
});

it('returns an empty array when invalid data is present in adUnit object', () => {
let sizes = utils.getAdUnitSizes({ sizes: 300 });
expect(sizes).to.deep.equal([]);
});

it('retuns an array of arrays when reading from adUnit.sizes', () => {
let sizes = utils.getAdUnitSizes({ sizes: [300, 250] });
expect(sizes).to.deep.equal([[300, 250]]);

sizes = utils.getAdUnitSizes({ sizes: [[300, 250], [300, 600]] });
expect(sizes).to.deep.equal([[300, 250], [300, 600]]);
});

it('returns an array of arrays when reading from adUnit.mediaTypes.banner.sizes', () => {
let sizes = utils.getAdUnitSizes({ mediaTypes: { banner: { sizes: [300, 250] } } });
expect(sizes).to.deep.equal([[300, 250]]);

sizes = utils.getAdUnitSizes({ mediaTypes: { banner: { sizes: [[300, 250], [300, 600]] } } });
expect(sizes).to.deep.equal([[300, 250], [300, 600]]);
});
});
});