diff --git a/modules/quantcastBidAdapter.js b/modules/quantcastBidAdapter.js index 4ffb84b8934..fbf3396a40b 100644 --- a/modules/quantcastBidAdapter.js +++ b/modules/quantcastBidAdapter.js @@ -6,6 +6,10 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'quantcast'; const DEFAULT_BID_FLOOR = 0.0000000001; +const QUANTCAST_VENDOR_ID = '11'; +// Check other required purposes on server +const PURPOSE_DATA_COLLECT = '1'; + export const QUANTCAST_DOMAIN = 'qcx.quantserve.com'; export const QUANTCAST_TEST_DOMAIN = 's2s-canary.quantserve.com'; export const QUANTCAST_NET_REVENUE = true; @@ -72,6 +76,35 @@ function getDomain(url) { return url.replace('http://', '').replace('https://', '').replace('www.', '').split(/[/?#]/)[0]; } +function checkTCFv1(vendorData) { + let vendorConsent = vendorData.vendorConsents && vendorData.vendorConsents[QUANTCAST_VENDOR_ID]; + let purposeConsent = vendorData.purposeConsents && vendorData.purposeConsents[PURPOSE_DATA_COLLECT]; + + return !!(vendorConsent && purposeConsent); +} + +function checkTCFv2(tcData) { + if (tcData.purposeOneTreatment && tcData.publisherCC === 'DE') { + // special purpose 1 treatment for Germany + return true; + } + + let restrictions = tcData.publisher ? tcData.publisher.restrictions : {}; + let qcRestriction = restrictions && restrictions[PURPOSE_DATA_COLLECT] + ? restrictions[PURPOSE_DATA_COLLECT][QUANTCAST_VENDOR_ID] + : null; + + if (qcRestriction === 0 || qcRestriction === 2) { + // Not allowed by publisher, or requires legitimate interest + return false; + } + + let vendorConsent = tcData.vendor && tcData.vendor.consents && tcData.vendor.consents[QUANTCAST_VENDOR_ID]; + let purposeConsent = tcData.purpose && tcData.purpose.consents && tcData.purpose.consents[PURPOSE_DATA_COLLECT]; + + return !!(vendorConsent && purposeConsent); +} + /** * The documentation for Prebid.js Adapter 1.0 can be found at link below, * http://prebid.org/dev-docs/bidder-adapter-1.html @@ -107,6 +140,21 @@ export const spec = { const page = utils.deepAccess(bidderRequest, 'refererInfo.canonicalUrl') || config.getConfig('pageUrl') || utils.deepAccess(window, 'location.href'); const domain = getDomain(page); + // Check for GDPR consent for purpose 1, and drop request if consent has not been given + // Remaining consent checks are performed server-side. + if (gdprConsent.gdprApplies) { + if (gdprConsent.vendorData) { + if (gdprConsent.apiVersion === 1 && !checkTCFv1(gdprConsent.vendorData)) { + utils.logInfo(`${BIDDER_CODE}: No purpose 1 consent for TCF v1`); + return; + } + if (gdprConsent.apiVersion === 2 && !checkTCFv2(gdprConsent.vendorData)) { + utils.logInfo(`${BIDDER_CODE}: No purpose 1 consent for TCF v2`); + return; + } + } + } + let bidRequestsList = []; bids.forEach(bid => { diff --git a/test/spec/modules/quantcastBidAdapter_spec.js b/test/spec/modules/quantcastBidAdapter_spec.js index 351cea0b086..96e69664859 100644 --- a/test/spec/modules/quantcastBidAdapter_spec.js +++ b/test/spec/modules/quantcastBidAdapter_spec.js @@ -348,13 +348,254 @@ describe('Quantcast adapter', function () { }); it('propagates GDPR consent string and signal', function () { - const bidderRequest = { gdprConsent: { gdprApplies: true, consentString: 'consentString' } } + const bidderRequest = { + gdprConsent: { + gdprApplies: true, + consentString: 'consentString' + } + }; + + 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('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 from Germany for purpose 1', function () { + const bidderRequest = { + gdprConsent: { + gdprApplies: true, + consentString: 'consentString', + vendorData: { + publisherCC: 'DE', + purposeOneTreatment: true + }, + apiVersion: 2 + } + }; + 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('allows TCF v2 request when Quantcast has consent for purpose 1', function() { + const bidderRequest = { + gdprConsent: { + gdprApplies: true, + consentString: 'consentString', + vendorData: { + vendor: { + consents: { + '11': true + } + }, + purpose: { + consents: { + '1': true + } + } + }, + apiVersion: 2 + } + }; + + 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 v2 request when no consent for Quantcast', function() { + const bidderRequest = { + gdprConsent: { + gdprApplies: true, + consentString: 'consentString', + vendorData: { + vendor: { + consents: { + '11': false + } + }, + purpose: { + consents: { + '1': true + } + } + }, + apiVersion: 2 + } + }; + + const requests = qcSpec.buildRequests([bidRequest], bidderRequest); + + expect(requests).to.equal(undefined); + }); + + it('blocks TCF v2 request when no consent for purpose 1', function() { + const bidderRequest = { + gdprConsent: { + gdprApplies: true, + consentString: 'consentString', + vendorData: { + vendor: { + consents: { + '11': true + } + }, + purpose: { + consents: { + '1': false + } + } + }, + apiVersion: 2 + } + }; + + const requests = qcSpec.buildRequests([bidRequest], bidderRequest); + + expect(requests).to.equal(undefined); + }); + + it('blocks TCF v2 request when Quantcast not allowed by publisher', function () { + const bidderRequest = { + gdprConsent: { + gdprApplies: true, + consentString: 'consentString', + vendorData: { + vendor: { + consents: { + '11': true + } + }, + purpose: { + consents: { + '1': true + } + }, + publisher: { + restrictions: { + '1': { + '11': 0 + } + } + } + }, + apiVersion: 2 + } + }; + + const requests = qcSpec.buildRequests([bidRequest], bidderRequest); + + expect(requests).to.equal(undefined); + }); + + it('blocks TCF v2 request when legitimate interest required', function () { + const bidderRequest = { + gdprConsent: { + gdprApplies: true, + consentString: 'consentString', + vendorData: { + vendor: { + consents: { + '11': true + } + }, + purpose: { + consents: { + '1': true + } + }, + publisher: { + restrictions: { + '1': { + '11': 2 + } + } + } + }, + apiVersion: 2 + } + }; + + const requests = qcSpec.buildRequests([bidRequest], bidderRequest); + + expect(requests).to.equal(undefined); + }); + it('propagates US Privacy/CCPA consent information', function () { const bidderRequest = { uspConsent: 'consentString' } const requests = qcSpec.buildRequests([bidRequest], bidderRequest);