From 7df1c4bb7fd44bc4cede9935a5149519c4778655 Mon Sep 17 00:00:00 2001 From: Sachin Shastri Date: Mon, 14 Jun 2021 02:11:52 -0700 Subject: [PATCH 01/10] add code for firing pixel without measure --- modules/quantcastIdSystem.js | 166 +++++++++++++- test/spec/modules/quantcastIdSystem_spec.js | 238 +++++++++++++++++++- 2 files changed, 402 insertions(+), 2 deletions(-) diff --git a/modules/quantcastIdSystem.js b/modules/quantcastIdSystem.js index e86c130dc5b..1572fefe23d 100644 --- a/modules/quantcastIdSystem.js +++ b/modules/quantcastIdSystem.js @@ -7,11 +7,156 @@ import {submodule} from '../src/hook.js' import { getStorageManager } from '../src/storageManager.js'; +import { triggerPixel, logInfo } from '../src/utils.js'; +import { uspDataHandler, coppaDataHandler, gdprDataHandler } from '../src/adapterManager.js'; const QUANTCAST_FPA = '__qca'; +const DEFAULT_COOKIE_EXP_TIME = 392; // (13 months - 2 days) +const PREBID_PCODE = 'p-KceJUEvXN48CE'; // Not associated with a real account +const DOMAIN_QSERVE = 'https://pixel.quantserve.com/pixel'; +const QUANTCAST_VENDOR_ID = '11'; +const PURPOSE_DATA_COLLECT = '1'; +const PURPOSE_PRODUCT_IMPROVEMENT = '10'; + +var clientId; +var cookieExpTime; export const storage = getStorageManager(); +export function firePixel() { + // check for presence of Quantcast Measure tag _qevent obj + if (!window._qevents) { + const gdprPrivacyString = gdprDataHandler.getConsentData(); + const usPrivacyString = uspDataHandler.getConsentData(); + + var fpa = storage.getCookie(QUANTCAST_FPA); + var fpan = '0'; + var now = new Date(); + var domain = quantcastIdSubmodule.findRootDomain(); + var et = now.getTime(); + var tzo = now.getTimezoneOffset(); + var usPrivacyParamString = ''; + var firstPartyParamStrings; + var gdprParamStrings; + + if (!(hasGDPRConsent(gdprPrivacyString) && hasCCPAConsent(usPrivacyString))) { + var expired = new Date(0).toUTCString(); + fpan = 'u'; + fpa = ''; + storage.setCookie(QUANTCAST_FPA, fpa, expired, '/', domain, null); + } else if (!fpa) { + var expires = new Date(now.getTime() + (cookieExpTime * 86400000)).toGMTString(); + fpa = 'B0-' + Math.round(Math.random() * 2147483647) + '-' + et; + fpan = '1'; + storage.setCookie(QUANTCAST_FPA, fpa, expires, '/', domain, null); + } + + firstPartyParamStrings = `&fpan=${fpan}&fpa=${fpa}`; + gdprParamStrings = '&gdpr=0'; + if (gdprPrivacyString && typeof gdprPrivacyString.gdprApplies === 'boolean' && gdprPrivacyString.gdprApplies) { + gdprParamStrings = `gdpr=1&gdpr_consent=${gdprPrivacyString.consentString}`; + } + if (usPrivacyString && typeof usPrivacyString === 'string') { + usPrivacyParamString = `&us_privacy=${usPrivacyString}`; + } + + let url = DOMAIN_QSERVE + + '?d=' + domain + + '&et=' + et + + '&tzo=' + tzo + + '&client_id=' + clientId + + '&a=' + PREBID_PCODE + + usPrivacyParamString + + gdprParamStrings + + firstPartyParamStrings; + + triggerPixel(url); + } +}; + +export function hasGDPRConsent(gdprConsent) { + // Check for GDPR consent for purpose 1 and 10, and drop request if consent has not been given + // Remaining consent checks are performed server-side. + if (gdprConsent && typeof gdprConsent.gdprApplies === 'boolean' && gdprConsent.gdprApplies) { + if (!gdprConsent.vendorData) { + return false; + } + if (gdprConsent.apiVersion === 1) { + return checkTCFv1(gdprConsent.vendorData); + } + if (gdprConsent.apiVersion === 2) { + return checkTCFv2(gdprConsent.vendorData); + } + } + return true; +} + +export function checkTCFv1(vendorData) { + var vendorConsent = vendorData.vendorConsents && vendorData.vendorConsents[QUANTCAST_VENDOR_ID]; + var purposeConsent = vendorData.purposeConsents && vendorData.purposeConsents[PURPOSE_DATA_COLLECT]; + + return !!(vendorConsent && purposeConsent); +} + +export function checkTCFv2(vendorData) { + var vendorConsent = vendorData.vendor && vendorData.vendor.consents && vendorData.vendor.consents[QUANTCAST_VENDOR_ID]; + var vendorInterest = vendorData.vendor.legitimateInterests && vendorData.vendor.legitimateInterests[QUANTCAST_VENDOR_ID]; + var restrictions = vendorData.publisher ? vendorData.publisher.restrictions : {}; + + // Restrictions for purpose 1 + var qcRestriction = restrictions && restrictions[PURPOSE_DATA_COLLECT] + ? restrictions[PURPOSE_DATA_COLLECT][QUANTCAST_VENDOR_ID] + : null; + + var purposeConsent = vendorData.purpose && vendorData.purpose.consents && vendorData.purpose.consents[PURPOSE_DATA_COLLECT]; + + // No consent, not allowed by publisher or requires legitimate interest + if (!vendorConsent || !purposeConsent || qcRestriction === 0 || qcRestriction === 2) { + return false; + } + + // Restrictions for purpose 10 + qcRestriction = restrictions && restrictions[PURPOSE_PRODUCT_IMPROVEMENT] + ? restrictions[PURPOSE_PRODUCT_IMPROVEMENT][QUANTCAST_VENDOR_ID] + : null; + + // Not allowed by publisher + if (qcRestriction === 0) { + return false; + } + + // publisher has explicitly restricted to consent + if (qcRestriction === 1) { + purposeConsent = vendorData.purpose && vendorData.purpose.consents && vendorData.purpose.consents[PURPOSE_PRODUCT_IMPROVEMENT]; + + // No consent, or requires legitimate interest + if (!vendorConsent || !purposeConsent) { + return false; + } + } else if (qcRestriction === 2) { + let purposeInterest = vendorData.purpose.LegitimateInterests && vendorData.purpose.LegitimateInterests[PURPOSE_PRODUCT_IMPROVEMENT]; + + // No legitimate interest, not allowed by publisher or requires legitimate interest + if (!vendorInterest || !purposeInterest) { + return false; + } + } + + return true; +} + +/** + * tests if us_privacy consent string is present, us_privacy applies, and do-not-sell is not set + * @returns {boolean} + */ +function hasCCPAConsent(usPrivacyConsent) { + // TODO : Needs to be revisited + if (usPrivacyConsent && usPrivacyConsent !== '1---') { + return false + } + return true; +} + /** @type {Submodule} */ export const quantcastIdSubmodule = { /** @@ -34,9 +179,28 @@ export const quantcastIdSubmodule = { * @function * @returns {{id: {quantcastId: string} | undefined}}} */ - getId() { + getId(config) { // Consent signals are currently checked on the server side. let fpa = storage.getCookie(QUANTCAST_FPA); + + const coppa = coppaDataHandler.getCoppa(); + if (coppa) { + logInfo('QuantcastId: IDs not provided for coppa requests, exiting QuantcastId'); + return; + } + + const configParams = (config && config.params) || {}; + const storageParams = (config && config.storage) || {}; + + clientId = configParams.clientId || ''; + cookieExpTime = storageParams.expires || DEFAULT_COOKIE_EXP_TIME; + + // Callbacks on Event Listeners won't trigger if the event is already complete so this check is required + if (document.readyState === 'complete') { + firePixel(); + } + window.addEventListener('load', firePixel); + return { id: fpa ? { quantcastId: fpa } : undefined } } }; diff --git a/test/spec/modules/quantcastIdSystem_spec.js b/test/spec/modules/quantcastIdSystem_spec.js index 12c8689fd3f..2dd2dea5a48 100644 --- a/test/spec/modules/quantcastIdSystem_spec.js +++ b/test/spec/modules/quantcastIdSystem_spec.js @@ -1,8 +1,19 @@ -import { quantcastIdSubmodule, storage } from 'modules/quantcastIdSystem.js'; +import { quantcastIdSubmodule, storage, firePixel, hasGDPRConsent, checkTCFv2 } from 'modules/quantcastIdSystem.js'; +import * as utils from 'src/utils.js'; +import {coppaDataHandler, gdprDataHandler, uspDataHandler} from 'src/adapterManager'; describe('QuantcastId module', function () { beforeEach(function() { storage.setCookie('__qca', '', 'Thu, 01 Jan 1970 00:00:00 GMT'); + sinon.stub(window, 'addEventListener'); + sinon.stub(coppaDataHandler, 'getCoppa'); + sinon.stub(utils, 'triggerPixel'); + }); + + afterEach(function () { + utils.triggerPixel.restore(); + window.addEventListener.restore(); + coppaDataHandler.getCoppa.restore(); }); it('getId() should return a quantcast id when the Quantcast first party cookie exists', function () { @@ -17,3 +28,228 @@ describe('QuantcastId module', function () { expect(id).to.be.deep.equal({id: undefined}); }); }); + +describe('QuantcastId fire pixel', function () { + beforeEach(function () { + storage.setCookie('__qca', '', 'Thu, 01 Jan 1970 00:00:00 GMT'); + sinon.stub(utils, 'triggerPixel'); + sinon.stub(uspDataHandler, 'getConsentData'); + sinon.stub(gdprDataHandler, 'getConsentData'); + }); + + afterEach(function () { + utils.triggerPixel.restore(); + uspDataHandler.getConsentData.restore(); + gdprDataHandler.getConsentData.restore(); + }); + + it('fpa should be set when not present on this call', function () { + firePixel(); + let urlString = utils.triggerPixel.getCall(0).args[0]; + let url = new URL(urlString); + let urlSearchParams = new URLSearchParams(url.search); + assert.equal(urlSearchParams.get('fpan'), '0'); + assert.notEqual(urlSearchParams.get('fpa'), null); + }); + + it('fpa should be extracted from the Quantcast first party cookie when present on this call', function () { + storage.setCookie('__qca', 'P0-TestFPA'); + firePixel(); + let urlString = utils.triggerPixel.getCall(0).args[0]; + let url = new URL(urlString); + let urlSearchParams = new URLSearchParams(url.search); + assert.equal(urlSearchParams.get('fpan'), '0'); + assert.equal(urlSearchParams.get('fpa'), 'P0-TestFPA'); + }); + + it('called once', function () { + storage.setCookie('__qca', 'P0-TestFPA'); + firePixel(); + expect(utils.triggerPixel.calledOnce).to.equal(true); + }); +}); + +describe('Quantcast GDPR consent check', function() { + it("returns true when GDPR doesn't apply", function() { + expect(hasGDPRConsent({gdprApplies: false})).to.equal(true); + }); + + it('returns false if denied consent, even if special purpose 1 treatment is true in DE', function() { + expect(checkTCFv2({ + gdprApplies: true, + publisherCC: 'DE', + purposeOneTreatment: true, + vendor: { + consents: { '11': false } + }, + purpose: { + consents: { '1': false } + }, + publisher: { + restrictions: { + '1': { + '11': 0 // flatly disallow Quantcast + } + } + } + })).toBe(false); + }); + + it('returns false if publisher flatly denies required purpose', function() { + expect(checkTCFv2({ + gdprApplies: true, + vendor: { + consents: { '11': true } + }, + purpose: { + consents: { '1': true } + }, + publisher: { + restrictions: { + '1': { + '11': 0 // flatly disallow Quantcast + } + } + } + })).toBe(false); + }); + + it('returns true if positive consent for required purpose', function() { + expect(checkTCFv2({ + gdprApplies: true, + vendor: { + consents: { '11': true } + }, + purpose: { + consents: { '1': true } + } + })).toBe(true); + }); + + it('returns false if positive consent but publisher requires legitimate interest for required purpose', function() { + expect(checkTCFv2({ + gdprApplies: true, + vendor: { + consents: { '11': true } + }, + purpose: { + consents: { '1': true } + }, + publisher: { + restrictions: { + '1': { + '11': 2 // require legitimate interest for Quantcast + } + } + } + })).toBe(false); + }); + + it('returns false if no vendor consent and no legitimate interest', function() { + expect(checkTCFv2({ + gdprApplies: true, + vendor: { + consents: { '11': false } + }, + purpose: { + consents: { '1': true } + } + })).toBe(false); + }); + + it('returns false if no purpose consent and no legitimate interest', function() { + expect(checkTCFv2({ + gdprApplies: true, + vendor: { + consents: { '11': true } + }, + purpose: { + consents: { '1': false } + } + })).toBe(false); + }); + + it('returns false if no consent, but legitimate interest for consent-first purpose, and no restrictions specified', function() { + expect(checkTCFv2({ + gdprApplies: true, + vendor: { + consents: { '11': true }, + legitimateInterests: { '11': true } + }, + purpose: { + consents: { '1': false }, + legitimateInterests: { '1': true } + } + })).toBe(false); + }); + + it('returns false if consent, but no legitimate interest for legitimate-interest-first purpose, and no restrictions specified', function() { + expect(checkTCFv2({ + gdprApplies: true, + vendor: { + consents: { '11': true }, + legitimateInterests: { '11': true } + }, + purpose: { + consents: { '10': true }, + legitimateInterests: { '10': false } + } + })).toBe(false); + }); + + it('returns true if consent, but no legitimate interest for legitimate-interest-first purpose, and corresponding consent restriction specified', function() { + expect(checkTCFv2({ + gdprApplies: true, + vendor: { + consents: { '11': true }, + legitimateInterests: { '11': true } + }, + purpose: { + consents: { '10': true }, + legitimateInterests: { '10': false } + }, + publisher: { + restrictions: { + '10': { + '11': 1 // require consent for Quantcast + } + } + } + })).toBe(true); + }); + + it('returns false if no consent but legitimate interest for required purpose other than 1, but publisher requires consent', function() { + expect(checkTCFv2({ + gdprApplies: true, + vendor: { + consents: { '11': false }, + legitimateInterests: { '11': true } + }, + purpose: { + consents: { '10': false }, + legitimateInterests: { '10': true } + }, + publisher: { + restrictions: { + '10': { + '11': 1 // require consent for Quantcast + } + } + } + })).toBe(false); + }); + + it('returns false if no consent and no legitimate interest for vendor for required purpose other than 1', function() { + expect(checkTCFv2({ + gdprApplies: true, + vendor: { + consents: { '11': false }, + legitimateInterests: { '11': false } + }, + purpose: { + consents: { '10': false }, + legitimateInterests: { '10': true } + } + })).toBe(false); + }); +}); From 05ec12c76fd15ba6089dfd5c781e253cafd5bb38 Mon Sep 17 00:00:00 2001 From: Sachin Shastri Date: Mon, 14 Jun 2021 12:34:20 -0700 Subject: [PATCH 02/10] add more unit tests --- test/spec/modules/quantcastIdSystem_spec.js | 79 +++++++++++++++++---- 1 file changed, 67 insertions(+), 12 deletions(-) diff --git a/test/spec/modules/quantcastIdSystem_spec.js b/test/spec/modules/quantcastIdSystem_spec.js index 2dd2dea5a48..79efa68b8df 100644 --- a/test/spec/modules/quantcastIdSystem_spec.js +++ b/test/spec/modules/quantcastIdSystem_spec.js @@ -48,7 +48,7 @@ describe('QuantcastId fire pixel', function () { let urlString = utils.triggerPixel.getCall(0).args[0]; let url = new URL(urlString); let urlSearchParams = new URLSearchParams(url.search); - assert.equal(urlSearchParams.get('fpan'), '0'); + assert.equal(urlSearchParams.get('fpan'), '1'); assert.notEqual(urlSearchParams.get('fpa'), null); }); @@ -92,7 +92,7 @@ describe('Quantcast GDPR consent check', function() { } } } - })).toBe(false); + })).to.equal(false); }); it('returns false if publisher flatly denies required purpose', function() { @@ -111,7 +111,7 @@ describe('Quantcast GDPR consent check', function() { } } } - })).toBe(false); + })).to.equal(false); }); it('returns true if positive consent for required purpose', function() { @@ -123,7 +123,7 @@ describe('Quantcast GDPR consent check', function() { purpose: { consents: { '1': true } } - })).toBe(true); + })).to.equal(true); }); it('returns false if positive consent but publisher requires legitimate interest for required purpose', function() { @@ -142,7 +142,7 @@ describe('Quantcast GDPR consent check', function() { } } } - })).toBe(false); + })).to.equal(false); }); it('returns false if no vendor consent and no legitimate interest', function() { @@ -154,7 +154,7 @@ describe('Quantcast GDPR consent check', function() { purpose: { consents: { '1': true } } - })).toBe(false); + })).to.equal(false); }); it('returns false if no purpose consent and no legitimate interest', function() { @@ -166,7 +166,7 @@ describe('Quantcast GDPR consent check', function() { purpose: { consents: { '1': false } } - })).toBe(false); + })).to.equal(false); }); it('returns false if no consent, but legitimate interest for consent-first purpose, and no restrictions specified', function() { @@ -180,7 +180,7 @@ describe('Quantcast GDPR consent check', function() { consents: { '1': false }, legitimateInterests: { '1': true } } - })).toBe(false); + })).to.equal(false); }); it('returns false if consent, but no legitimate interest for legitimate-interest-first purpose, and no restrictions specified', function() { @@ -194,7 +194,7 @@ describe('Quantcast GDPR consent check', function() { consents: { '10': true }, legitimateInterests: { '10': false } } - })).toBe(false); + })).to.equal(false); }); it('returns true if consent, but no legitimate interest for legitimate-interest-first purpose, and corresponding consent restriction specified', function() { @@ -215,7 +215,7 @@ describe('Quantcast GDPR consent check', function() { } } } - })).toBe(true); + })).to.equal(true); }); it('returns false if no consent but legitimate interest for required purpose other than 1, but publisher requires consent', function() { @@ -236,7 +236,7 @@ describe('Quantcast GDPR consent check', function() { } } } - })).toBe(false); + })).to.equal(false); }); it('returns false if no consent and no legitimate interest for vendor for required purpose other than 1', function() { @@ -250,6 +250,61 @@ describe('Quantcast GDPR consent check', function() { consents: { '10': false }, legitimateInterests: { '10': true } } - })).toBe(false); + })).to.equal(false); + }); + + it('returns false if no consent and no legitimate interest for required purpose other than 1', function() { + expect(checkTCFv2({ + gdprApplies: true, + vendor: { + consents: { '11': false }, + legitimateInterests: { '11': true } + }, + purpose: { + consents: { '10': false }, + legitimateInterests: { '10': false } + } + })).to.equal(false); + }); + + it('returns false if no consent but legitimate interest for required purpose, but required purpose is purpose 1', function() { + expect(checkTCFv2({ + gdprApplies: true, + vendor: { + consents: { '11': false }, + legitimateInterests: { '11': true } + }, + purpose: { + consents: { '1': false }, + legitimateInterests: { '1': true } + } + })).to.equal(false); + }); + + it('returns true if different legal bases for multiple required purposes', function() { + expect(checkTCFv2({ + gdprApplies: true, + vendor: { + consents: { '11': true }, + legitimateInterests: { '11': true } + }, + purpose: { + consents: { + '1': true, + '10': false + }, + legitimateInterests: { + '1': false, + '10': true + } + }, + publisher: { + restrictions: { + '10': { + '11': 2 // require legitimate interest for Quantcast + } + } + } + })).to.equal(true); }); }); From 8439525fb6426dafc6cbcdb779c5d38d9eee7f60 Mon Sep 17 00:00:00 2001 From: Sachin Shastri Date: Wed, 16 Jun 2021 09:29:51 -0700 Subject: [PATCH 03/10] correct gdpr logic; don't fire when no consent --- modules/quantcastIdSystem.js | 87 ++++++++++++--------- test/spec/modules/quantcastIdSystem_spec.js | 81 ++++++++++++++++--- 2 files changed, 118 insertions(+), 50 deletions(-) diff --git a/modules/quantcastIdSystem.js b/modules/quantcastIdSystem.js index 1572fefe23d..93a0c3f2c69 100644 --- a/modules/quantcastIdSystem.js +++ b/modules/quantcastIdSystem.js @@ -17,6 +17,9 @@ const DOMAIN_QSERVE = 'https://pixel.quantserve.com/pixel'; const QUANTCAST_VENDOR_ID = '11'; const PURPOSE_DATA_COLLECT = '1'; const PURPOSE_PRODUCT_IMPROVEMENT = '10'; +const QC_TCF_REQUIRED_PURPOSES = [PURPOSE_DATA_COLLECT, PURPOSE_PRODUCT_IMPROVEMENT]; +const QC_TCF_CONSENT_FIRST_PURPOSES = [PURPOSE_DATA_COLLECT]; +const QC_TCF_CONSENT_ONLY_PUPROSES = [PURPOSE_DATA_COLLECT]; var clientId; var cookieExpTime; @@ -41,9 +44,8 @@ export function firePixel() { if (!(hasGDPRConsent(gdprPrivacyString) && hasCCPAConsent(usPrivacyString))) { var expired = new Date(0).toUTCString(); - fpan = 'u'; - fpa = ''; - storage.setCookie(QUANTCAST_FPA, fpa, expired, '/', domain, null); + storage.setCookie(QUANTCAST_FPA, '', expired, '/', domain, null); + return; } else if (!fpa) { var expires = new Date(now.getTime() + (cookieExpTime * 86400000)).toGMTString(); fpa = 'B0-' + Math.round(Math.random() * 2147483647) + '-' + et; @@ -98,51 +100,62 @@ export function checkTCFv1(vendorData) { return !!(vendorConsent && purposeConsent); } -export function checkTCFv2(vendorData) { - var vendorConsent = vendorData.vendor && vendorData.vendor.consents && vendorData.vendor.consents[QUANTCAST_VENDOR_ID]; - var vendorInterest = vendorData.vendor.legitimateInterests && vendorData.vendor.legitimateInterests[QUANTCAST_VENDOR_ID]; +export function checkTCFv2(vendorData, requiredPurposes = QC_TCF_REQUIRED_PURPOSES) { + var gdprApplies = vendorData.gdprApplies; + var purposes = vendorData.purpose; + var vendors = vendorData.vendor; + var qcConsent = vendors && vendors.consents && vendors.consents[QUANTCAST_VENDOR_ID]; + var qcInterest = vendors && vendors.legitimateInterests && vendors.legitimateInterests[QUANTCAST_VENDOR_ID]; var restrictions = vendorData.publisher ? vendorData.publisher.restrictions : {}; - // Restrictions for purpose 1 - var qcRestriction = restrictions && restrictions[PURPOSE_DATA_COLLECT] - ? restrictions[PURPOSE_DATA_COLLECT][QUANTCAST_VENDOR_ID] - : null; - - var purposeConsent = vendorData.purpose && vendorData.purpose.consents && vendorData.purpose.consents[PURPOSE_DATA_COLLECT]; - - // No consent, not allowed by publisher or requires legitimate interest - if (!vendorConsent || !purposeConsent || qcRestriction === 0 || qcRestriction === 2) { - return false; + if (!gdprApplies) { + return true; } - // Restrictions for purpose 10 - qcRestriction = restrictions && restrictions[PURPOSE_PRODUCT_IMPROVEMENT] - ? restrictions[PURPOSE_PRODUCT_IMPROVEMENT][QUANTCAST_VENDOR_ID] - : null; + return requiredPurposes.map(function(purpose) { + var purposeConsent = purposes.consents ? purposes.consents[purpose] : false; + var purposeInterest = purposes.legitimateInterests ? purposes.legitimateInterests[purpose] : false; - // Not allowed by publisher - if (qcRestriction === 0) { - return false; - } + var qcRestriction = restrictions && restrictions[purpose] + ? restrictions[purpose][QUANTCAST_VENDOR_ID] + : null; - // publisher has explicitly restricted to consent - if (qcRestriction === 1) { - purposeConsent = vendorData.purpose && vendorData.purpose.consents && vendorData.purpose.consents[PURPOSE_PRODUCT_IMPROVEMENT]; - - // No consent, or requires legitimate interest - if (!vendorConsent || !purposeConsent) { + if (qcRestriction === 0) { return false; } - } else if (qcRestriction === 2) { - let purposeInterest = vendorData.purpose.LegitimateInterests && vendorData.purpose.LegitimateInterests[PURPOSE_PRODUCT_IMPROVEMENT]; - // No legitimate interest, not allowed by publisher or requires legitimate interest - if (!vendorInterest || !purposeInterest) { - return false; + // Seek consent or legitimate interest based on our default legal + // basis for the purpose, falling back to the other if possible. + if ( + // we have positive vendor consent + qcConsent && + // there is positive purpose consent + purposeConsent && + // publisher does not require legitimate interest + qcRestriction !== 2 && + // purpose is a consent-first purpose or publisher has explicitly restricted to consent + (QC_TCF_CONSENT_FIRST_PURPOSES.indexOf(purpose) != -1 || qcRestriction === 1) + ) { + return true; + } else if ( + // publisher does not require consent + qcRestriction !== 1 && + // we have legitimate interest for vendor + qcInterest && + // there is legitimate interest for purpose + purposeInterest && + // purpose's legal basis does not require consent + QC_TCF_CONSENT_ONLY_PUPROSES.indexOf(purpose) == -1 && + // purpose is a legitimate-interest-first purpose or publisher has explicitly restricted to legitimate interest + (QC_TCF_CONSENT_FIRST_PURPOSES.indexOf(purpose) == -1 || qcRestriction === 2) + ) { + return true; } - } - return true; + return false; + }).reduce(function(a, b) { + return a && b; + }, true); } /** diff --git a/test/spec/modules/quantcastIdSystem_spec.js b/test/spec/modules/quantcastIdSystem_spec.js index 79efa68b8df..2a820ae0ed2 100644 --- a/test/spec/modules/quantcastIdSystem_spec.js +++ b/test/spec/modules/quantcastIdSystem_spec.js @@ -92,7 +92,7 @@ describe('Quantcast GDPR consent check', function() { } } } - })).to.equal(false); + }, ['1'])).to.equal(false); }); it('returns false if publisher flatly denies required purpose', function() { @@ -111,7 +111,7 @@ describe('Quantcast GDPR consent check', function() { } } } - })).to.equal(false); + }, ['1'])).to.equal(false); }); it('returns true if positive consent for required purpose', function() { @@ -123,7 +123,7 @@ describe('Quantcast GDPR consent check', function() { purpose: { consents: { '1': true } } - })).to.equal(true); + }, ['1'])).to.equal(true); }); it('returns false if positive consent but publisher requires legitimate interest for required purpose', function() { @@ -142,7 +142,7 @@ describe('Quantcast GDPR consent check', function() { } } } - })).to.equal(false); + }, ['1'])).to.equal(false); }); it('returns false if no vendor consent and no legitimate interest', function() { @@ -154,7 +154,7 @@ describe('Quantcast GDPR consent check', function() { purpose: { consents: { '1': true } } - })).to.equal(false); + }, ['1'])).to.equal(false); }); it('returns false if no purpose consent and no legitimate interest', function() { @@ -166,7 +166,7 @@ describe('Quantcast GDPR consent check', function() { purpose: { consents: { '1': false } } - })).to.equal(false); + }, ['1'])).to.equal(false); }); it('returns false if no consent, but legitimate interest for consent-first purpose, and no restrictions specified', function() { @@ -180,7 +180,7 @@ describe('Quantcast GDPR consent check', function() { consents: { '1': false }, legitimateInterests: { '1': true } } - })).to.equal(false); + }, ['1'])).to.equal(false); }); it('returns false if consent, but no legitimate interest for legitimate-interest-first purpose, and no restrictions specified', function() { @@ -194,7 +194,7 @@ describe('Quantcast GDPR consent check', function() { consents: { '10': true }, legitimateInterests: { '10': false } } - })).to.equal(false); + }, ['10'])).to.equal(false); }); it('returns true if consent, but no legitimate interest for legitimate-interest-first purpose, and corresponding consent restriction specified', function() { @@ -215,7 +215,7 @@ describe('Quantcast GDPR consent check', function() { } } } - })).to.equal(true); + }, ['10'])).to.equal(true); }); it('returns false if no consent but legitimate interest for required purpose other than 1, but publisher requires consent', function() { @@ -236,7 +236,7 @@ describe('Quantcast GDPR consent check', function() { } } } - })).to.equal(false); + }, ['10'])).to.equal(false); }); it('returns false if no consent and no legitimate interest for vendor for required purpose other than 1', function() { @@ -250,7 +250,7 @@ describe('Quantcast GDPR consent check', function() { consents: { '10': false }, legitimateInterests: { '10': true } } - })).to.equal(false); + }, ['10'])).to.equal(false); }); it('returns false if no consent and no legitimate interest for required purpose other than 1', function() { @@ -264,7 +264,7 @@ describe('Quantcast GDPR consent check', function() { consents: { '10': false }, legitimateInterests: { '10': false } } - })).to.equal(false); + }, ['10'])).to.equal(false); }); it('returns false if no consent but legitimate interest for required purpose, but required purpose is purpose 1', function() { @@ -278,7 +278,7 @@ describe('Quantcast GDPR consent check', function() { consents: { '1': false }, legitimateInterests: { '1': true } } - })).to.equal(false); + }, ['1'])).to.equal(false); }); it('returns true if different legal bases for multiple required purposes', function() { @@ -307,4 +307,59 @@ describe('Quantcast GDPR consent check', function() { } })).to.equal(true); }); + + it('returns true if full consent and legitimate interest for all required purposes with no restrictions specified', function() { + expect(checkTCFv2({ + gdprApplies: true, + vendor: { + consents: { '11': true }, + legitimateInterests: { '11': true } + }, + purpose: { + consents: { + '1': true, + '3': true, + '7': true, + '8': true, + '9': true, + '10': true + }, + legitimateInterests: { + '1': true, + '3': true, + '7': true, + '8': true, + '9': true, + '10': true + } + } + })).to.equal(true); + }); + + it('returns false if one of multiple required purposes has no legal basis', function() { + expect(checkTCFv2({ + gdprApplies: true, + vendor: { + consents: { '11': true }, + legitimateInterests: { '11': true } + }, + purpose: { + consents: { + '1': true, + '10': false + }, + legitimateInterests: { + '11': false, + '10': true + } + }, + publisher: { + restrictions: { + '10': { + '11': 1 // require consent for Quantcast + } + } + } + })).to.equal(false); + }); }); From 7ba57782bad7a662fc7b94f389b4a569d19d8865 Mon Sep 17 00:00:00 2001 From: Sachin Shastri Date: Thu, 17 Jun 2021 15:04:22 -0700 Subject: [PATCH 04/10] add clientId check --- modules/quantcastIdSystem.js | 19 +++++++++---------- test/spec/modules/quantcastIdSystem_spec.js | 16 ++++++++++------ 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/modules/quantcastIdSystem.js b/modules/quantcastIdSystem.js index 93a0c3f2c69..289e07676c5 100644 --- a/modules/quantcastIdSystem.js +++ b/modules/quantcastIdSystem.js @@ -21,14 +21,11 @@ const QC_TCF_REQUIRED_PURPOSES = [PURPOSE_DATA_COLLECT, PURPOSE_PRODUCT_IMPROVEM const QC_TCF_CONSENT_FIRST_PURPOSES = [PURPOSE_DATA_COLLECT]; const QC_TCF_CONSENT_ONLY_PUPROSES = [PURPOSE_DATA_COLLECT]; -var clientId; -var cookieExpTime; - export const storage = getStorageManager(); -export function firePixel() { - // check for presence of Quantcast Measure tag _qevent obj - if (!window._qevents) { +export function firePixel(clientId, cookieExpTime = DEFAULT_COOKIE_EXP_TIME) { + // check for presence of Quantcast Measure tag _qevent obj and publisher provided clientID + if (!window._qevents && clientId && clientId != '') { const gdprPrivacyString = gdprDataHandler.getConsentData(); const usPrivacyString = uspDataHandler.getConsentData(); @@ -205,14 +202,16 @@ export const quantcastIdSubmodule = { const configParams = (config && config.params) || {}; const storageParams = (config && config.storage) || {}; - clientId = configParams.clientId || ''; - cookieExpTime = storageParams.expires || DEFAULT_COOKIE_EXP_TIME; + var clientId = configParams.clientId || ''; + var cookieExpTime = storageParams.expires || DEFAULT_COOKIE_EXP_TIME; // Callbacks on Event Listeners won't trigger if the event is already complete so this check is required if (document.readyState === 'complete') { - firePixel(); + firePixel(clientId, cookieExpTime); } - window.addEventListener('load', firePixel); + window.addEventListener('load', function () { + firePixel(clientId, cookieExpTime); + }); return { id: fpa ? { quantcastId: fpa } : undefined } } diff --git a/test/spec/modules/quantcastIdSystem_spec.js b/test/spec/modules/quantcastIdSystem_spec.js index 2a820ae0ed2..99426b383f8 100644 --- a/test/spec/modules/quantcastIdSystem_spec.js +++ b/test/spec/modules/quantcastIdSystem_spec.js @@ -5,14 +5,12 @@ import {coppaDataHandler, gdprDataHandler, uspDataHandler} from 'src/adapterMana describe('QuantcastId module', function () { beforeEach(function() { storage.setCookie('__qca', '', 'Thu, 01 Jan 1970 00:00:00 GMT'); - sinon.stub(window, 'addEventListener'); sinon.stub(coppaDataHandler, 'getCoppa'); sinon.stub(utils, 'triggerPixel'); }); afterEach(function () { utils.triggerPixel.restore(); - window.addEventListener.restore(); coppaDataHandler.getCoppa.restore(); }); @@ -44,7 +42,7 @@ describe('QuantcastId fire pixel', function () { }); it('fpa should be set when not present on this call', function () { - firePixel(); + firePixel('clientId'); let urlString = utils.triggerPixel.getCall(0).args[0]; let url = new URL(urlString); let urlSearchParams = new URLSearchParams(url.search); @@ -54,7 +52,7 @@ describe('QuantcastId fire pixel', function () { it('fpa should be extracted from the Quantcast first party cookie when present on this call', function () { storage.setCookie('__qca', 'P0-TestFPA'); - firePixel(); + firePixel('clientId'); let urlString = utils.triggerPixel.getCall(0).args[0]; let url = new URL(urlString); let urlSearchParams = new URLSearchParams(url.search); @@ -62,11 +60,17 @@ describe('QuantcastId fire pixel', function () { assert.equal(urlSearchParams.get('fpa'), 'P0-TestFPA'); }); - it('called once', function () { + it('function to trigger pixel is called once', function () { storage.setCookie('__qca', 'P0-TestFPA'); - firePixel(); + firePixel('clientId'); expect(utils.triggerPixel.calledOnce).to.equal(true); }); + + it('function to trigger pixel is not called when client id is absent', function () { + storage.setCookie('__qca', 'P0-TestFPA'); + firePixel(); + expect(utils.triggerPixel.calledOnce).to.equal(false); + }); }); describe('Quantcast GDPR consent check', function() { From 17a40588513c7fe9bca8162926358f2f1a01121d Mon Sep 17 00:00:00 2001 From: Sachin Shastri Date: Mon, 21 Jun 2021 10:28:18 -0700 Subject: [PATCH 05/10] address more pr comments --- modules/quantcastIdSystem.js | 47 +++++++++------------ test/spec/modules/quantcastIdSystem_spec.js | 2 + 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/modules/quantcastIdSystem.js b/modules/quantcastIdSystem.js index 289e07676c5..ef065555e06 100644 --- a/modules/quantcastIdSystem.js +++ b/modules/quantcastIdSystem.js @@ -20,30 +20,27 @@ const PURPOSE_PRODUCT_IMPROVEMENT = '10'; const QC_TCF_REQUIRED_PURPOSES = [PURPOSE_DATA_COLLECT, PURPOSE_PRODUCT_IMPROVEMENT]; const QC_TCF_CONSENT_FIRST_PURPOSES = [PURPOSE_DATA_COLLECT]; const QC_TCF_CONSENT_ONLY_PUPROSES = [PURPOSE_DATA_COLLECT]; +const GDPR_PRIVACY_STRING = gdprDataHandler.getConsentData(); +const US_PRIVACY_STRING = uspDataHandler.getConsentData(); export const storage = getStorageManager(); export function firePixel(clientId, cookieExpTime = DEFAULT_COOKIE_EXP_TIME) { - // check for presence of Quantcast Measure tag _qevent obj and publisher provided clientID - if (!window._qevents && clientId && clientId != '') { - const gdprPrivacyString = gdprDataHandler.getConsentData(); - const usPrivacyString = uspDataHandler.getConsentData(); - + // check for presence of Quantcast Measure tag _qevent obj, + // required gdpr consent / legitimate interest for purpose 1 and 10 + // for Quantcast as a vendor, and publisher provided clientID + if (!window._qevents && hasGDPRConsent(GDPR_PRIVACY_STRING) && clientId && clientId != '') { var fpa = storage.getCookie(QUANTCAST_FPA); var fpan = '0'; - var now = new Date(); var domain = quantcastIdSubmodule.findRootDomain(); + var now = new Date(); var et = now.getTime(); var tzo = now.getTimezoneOffset(); var usPrivacyParamString = ''; var firstPartyParamStrings; var gdprParamStrings; - if (!(hasGDPRConsent(gdprPrivacyString) && hasCCPAConsent(usPrivacyString))) { - var expired = new Date(0).toUTCString(); - storage.setCookie(QUANTCAST_FPA, '', expired, '/', domain, null); - return; - } else if (!fpa) { + if (!fpa) { var expires = new Date(now.getTime() + (cookieExpTime * 86400000)).toGMTString(); fpa = 'B0-' + Math.round(Math.random() * 2147483647) + '-' + et; fpan = '1'; @@ -52,11 +49,11 @@ export function firePixel(clientId, cookieExpTime = DEFAULT_COOKIE_EXP_TIME) { firstPartyParamStrings = `&fpan=${fpan}&fpa=${fpa}`; gdprParamStrings = '&gdpr=0'; - if (gdprPrivacyString && typeof gdprPrivacyString.gdprApplies === 'boolean' && gdprPrivacyString.gdprApplies) { - gdprParamStrings = `gdpr=1&gdpr_consent=${gdprPrivacyString.consentString}`; + if (GDPR_PRIVACY_STRING && typeof GDPR_PRIVACY_STRING.gdprApplies === 'boolean' && GDPR_PRIVACY_STRING.gdprApplies) { + gdprParamStrings = `gdpr=1&gdpr_consent=${GDPR_PRIVACY_STRING.consentString}`; } - if (usPrivacyString && typeof usPrivacyString === 'string') { - usPrivacyParamString = `&us_privacy=${usPrivacyString}`; + if (US_PRIVACY_STRING && typeof US_PRIVACY_STRING === 'string') { + usPrivacyParamString = `&us_privacy=${US_PRIVACY_STRING}`; } let url = DOMAIN_QSERVE + @@ -81,7 +78,8 @@ export function hasGDPRConsent(gdprConsent) { return false; } if (gdprConsent.apiVersion === 1) { - return checkTCFv1(gdprConsent.vendorData); + // We are not supporting TCF v1 + return false; } if (gdprConsent.apiVersion === 2) { return checkTCFv2(gdprConsent.vendorData); @@ -90,13 +88,6 @@ export function hasGDPRConsent(gdprConsent) { return true; } -export function checkTCFv1(vendorData) { - var vendorConsent = vendorData.vendorConsents && vendorData.vendorConsents[QUANTCAST_VENDOR_ID]; - var purposeConsent = vendorData.purposeConsents && vendorData.purposeConsents[PURPOSE_DATA_COLLECT]; - - return !!(vendorConsent && purposeConsent); -} - export function checkTCFv2(vendorData, requiredPurposes = QC_TCF_REQUIRED_PURPOSES) { var gdprApplies = vendorData.gdprApplies; var purposes = vendorData.purpose; @@ -194,9 +185,13 @@ export const quantcastIdSubmodule = { let fpa = storage.getCookie(QUANTCAST_FPA); const coppa = coppaDataHandler.getCoppa(); - if (coppa) { - logInfo('QuantcastId: IDs not provided for coppa requests, exiting QuantcastId'); - return; + + if (coppa || !hasCCPAConsent(US_PRIVACY_STRING)) { + var expired = new Date(0).toUTCString(); + var domain = quantcastIdSubmodule.findRootDomain(); + logInfo('QuantcastId: Necessary consent not present for Id, exiting QuantcastId'); + storage.setCookie(QUANTCAST_FPA, '', expired, '/', domain, null); + return undefined; } const configParams = (config && config.params) || {}; diff --git a/test/spec/modules/quantcastIdSystem_spec.js b/test/spec/modules/quantcastIdSystem_spec.js index 99426b383f8..7ac9c92a52b 100644 --- a/test/spec/modules/quantcastIdSystem_spec.js +++ b/test/spec/modules/quantcastIdSystem_spec.js @@ -7,11 +7,13 @@ describe('QuantcastId module', function () { storage.setCookie('__qca', '', 'Thu, 01 Jan 1970 00:00:00 GMT'); sinon.stub(coppaDataHandler, 'getCoppa'); sinon.stub(utils, 'triggerPixel'); + sinon.stub(window, 'addEventListener'); }); afterEach(function () { utils.triggerPixel.restore(); coppaDataHandler.getCoppa.restore(); + window.addEventListener.restore(); }); it('getId() should return a quantcast id when the Quantcast first party cookie exists', function () { From 35a9e387c59de89d67cec9d751e3866472efddfb Mon Sep 17 00:00:00 2001 From: Sachin Shastri Date: Mon, 21 Jun 2021 13:07:58 -0700 Subject: [PATCH 06/10] add glvid --- modules/quantcastIdSystem.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/modules/quantcastIdSystem.js b/modules/quantcastIdSystem.js index ef065555e06..c7c62daef47 100644 --- a/modules/quantcastIdSystem.js +++ b/modules/quantcastIdSystem.js @@ -166,6 +166,12 @@ export const quantcastIdSubmodule = { */ name: 'quantcastId', + /** + * Vendor id of Quantcast + * @type {Number} + */ + gvlid: QUANTCAST_VENDOR_ID, + /** * decode the stored id value for passing to bid requests * @function From b31c0f9e75f432dd16f938ebe7a994be4f540d52 Mon Sep 17 00:00:00 2001 From: Sachin Shastri Date: Mon, 21 Jun 2021 13:30:26 -0700 Subject: [PATCH 07/10] remove et / tzo --- modules/quantcastIdSystem.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/modules/quantcastIdSystem.js b/modules/quantcastIdSystem.js index c7c62daef47..e79e93834d5 100644 --- a/modules/quantcastIdSystem.js +++ b/modules/quantcastIdSystem.js @@ -34,8 +34,6 @@ export function firePixel(clientId, cookieExpTime = DEFAULT_COOKIE_EXP_TIME) { var fpan = '0'; var domain = quantcastIdSubmodule.findRootDomain(); var now = new Date(); - var et = now.getTime(); - var tzo = now.getTimezoneOffset(); var usPrivacyParamString = ''; var firstPartyParamStrings; var gdprParamStrings; @@ -58,8 +56,6 @@ export function firePixel(clientId, cookieExpTime = DEFAULT_COOKIE_EXP_TIME) { let url = DOMAIN_QSERVE + '?d=' + domain + - '&et=' + et + - '&tzo=' + tzo + '&client_id=' + clientId + '&a=' + PREBID_PCODE + usPrivacyParamString + From 7a94f1366f2cad7a9b6487ae84d3b876357729ed Mon Sep 17 00:00:00 2001 From: Sachin Shastri Date: Tue, 22 Jun 2021 08:48:36 -0700 Subject: [PATCH 08/10] rename vars; update ccpa check --- modules/quantcastIdSystem.js | 42 +++++++++++---------- test/spec/modules/quantcastIdSystem_spec.js | 16 ++++++++ 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/modules/quantcastIdSystem.js b/modules/quantcastIdSystem.js index e79e93834d5..24d89f0e74a 100644 --- a/modules/quantcastIdSystem.js +++ b/modules/quantcastIdSystem.js @@ -11,9 +11,9 @@ import { triggerPixel, logInfo } from '../src/utils.js'; import { uspDataHandler, coppaDataHandler, gdprDataHandler } from '../src/adapterManager.js'; const QUANTCAST_FPA = '__qca'; -const DEFAULT_COOKIE_EXP_TIME = 392; // (13 months - 2 days) -const PREBID_PCODE = 'p-KceJUEvXN48CE'; // Not associated with a real account -const DOMAIN_QSERVE = 'https://pixel.quantserve.com/pixel'; +const DEFAULT_COOKIE_EXP_DAYS = 392; // (13 months - 2 days) +const PREBID_PCODE = 'p-KceJUEvXN48CE'; +const QSERVE_URL = 'https://pixel.quantserve.com/pixel'; const QUANTCAST_VENDOR_ID = '11'; const PURPOSE_DATA_COLLECT = '1'; const PURPOSE_PRODUCT_IMPROVEMENT = '10'; @@ -25,21 +25,20 @@ const US_PRIVACY_STRING = uspDataHandler.getConsentData(); export const storage = getStorageManager(); -export function firePixel(clientId, cookieExpTime = DEFAULT_COOKIE_EXP_TIME) { - // check for presence of Quantcast Measure tag _qevent obj, - // required gdpr consent / legitimate interest for purpose 1 and 10 - // for Quantcast as a vendor, and publisher provided clientID - if (!window._qevents && hasGDPRConsent(GDPR_PRIVACY_STRING) && clientId && clientId != '') { +export function firePixel(clientId, cookieExpDays = DEFAULT_COOKIE_EXP_DAYS) { + // check for presence of Quantcast Measure tag _qevent obj and publisher provided clientID + if (!window._qevents && clientId && clientId != '') { var fpa = storage.getCookie(QUANTCAST_FPA); var fpan = '0'; var domain = quantcastIdSubmodule.findRootDomain(); var now = new Date(); + var et = now.getTime(); var usPrivacyParamString = ''; var firstPartyParamStrings; var gdprParamStrings; if (!fpa) { - var expires = new Date(now.getTime() + (cookieExpTime * 86400000)).toGMTString(); + var expires = new Date(now.getTime() + (cookieExpDays * 86400000)).toGMTString(); fpa = 'B0-' + Math.round(Math.random() * 2147483647) + '-' + et; fpan = '1'; storage.setCookie(QUANTCAST_FPA, fpa, expires, '/', domain, null); @@ -54,7 +53,7 @@ export function firePixel(clientId, cookieExpTime = DEFAULT_COOKIE_EXP_TIME) { usPrivacyParamString = `&us_privacy=${US_PRIVACY_STRING}`; } - let url = DOMAIN_QSERVE + + let url = QSERVE_URL + '?d=' + domain + '&client_id=' + clientId + '&a=' + PREBID_PCODE + @@ -143,12 +142,17 @@ export function checkTCFv2(vendorData, requiredPurposes = QC_TCF_REQUIRED_PURPOS } /** - * tests if us_privacy consent string is present, us_privacy applies, and do-not-sell is not set + * tests if us_privacy consent string is present, us_privacy applies, and notice_given / do-not-sell is set to yes * @returns {boolean} */ -function hasCCPAConsent(usPrivacyConsent) { - // TODO : Needs to be revisited - if (usPrivacyConsent && usPrivacyConsent !== '1---') { +export function hasCCPAConsent(usPrivacyConsent) { + if ( + usPrivacyConsent && + typeof usPrivacyConsent === 'string' && + usPrivacyConsent.length == 4 && + usPrivacyConsent.charAt(1) == 'Y' && + usPrivacyConsent.charAt(2) == 'Y' + ) { return false } return true; @@ -162,7 +166,7 @@ export const quantcastIdSubmodule = { */ name: 'quantcastId', - /** + /** * Vendor id of Quantcast * @type {Number} */ @@ -188,7 +192,7 @@ export const quantcastIdSubmodule = { const coppa = coppaDataHandler.getCoppa(); - if (coppa || !hasCCPAConsent(US_PRIVACY_STRING)) { + if (coppa || !hasCCPAConsent(US_PRIVACY_STRING) || !hasGDPRConsent(GDPR_PRIVACY_STRING)) { var expired = new Date(0).toUTCString(); var domain = quantcastIdSubmodule.findRootDomain(); logInfo('QuantcastId: Necessary consent not present for Id, exiting QuantcastId'); @@ -200,14 +204,14 @@ export const quantcastIdSubmodule = { const storageParams = (config && config.storage) || {}; var clientId = configParams.clientId || ''; - var cookieExpTime = storageParams.expires || DEFAULT_COOKIE_EXP_TIME; + var cookieExpDays = storageParams.expires || DEFAULT_COOKIE_EXP_DAYS; // Callbacks on Event Listeners won't trigger if the event is already complete so this check is required if (document.readyState === 'complete') { - firePixel(clientId, cookieExpTime); + firePixel(clientId, cookieExpDays); } window.addEventListener('load', function () { - firePixel(clientId, cookieExpTime); + firePixel(clientId, cookieExpDays); }); return { id: fpa ? { quantcastId: fpa } : undefined } diff --git a/test/spec/modules/quantcastIdSystem_spec.js b/test/spec/modules/quantcastIdSystem_spec.js index 7ac9c92a52b..199f7e52be8 100644 --- a/test/spec/modules/quantcastIdSystem_spec.js +++ b/test/spec/modules/quantcastIdSystem_spec.js @@ -75,6 +75,22 @@ describe('QuantcastId fire pixel', function () { }); }); +describe('Quantcast CCPA consent check', function() { + it('returns true when CCPA constent string is not present', function() { + expect(hasCCPAConsent()).to.equal(true); + }); + + it("returns true when notice_given or do-not-sell in CCPA constent string is not 'Y' ", function() { + expect(hasCCPAConsent('1NNN')).to.equal(true); + expect(hasCCPAConsent('1YNN')).to.equal(true); + expect(hasCCPAConsent('1NYN')).to.equal(true); + }); + + it("returns false when CCPA consent string is present, and notice_given or do-not-sell in the string is 'Y' ", function() { + expect(hasCCPAConsent('1YYN')).to.equal(false); + }); +}); + describe('Quantcast GDPR consent check', function() { it("returns true when GDPR doesn't apply", function() { expect(hasGDPRConsent({gdprApplies: false})).to.equal(true); From 22f253253b04c011e56f45398179c930ed69f9b9 Mon Sep 17 00:00:00 2001 From: Sachin Shastri Date: Wed, 23 Jun 2021 07:10:44 -0700 Subject: [PATCH 09/10] move to protocol relative url --- modules/quantcastIdSystem.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/quantcastIdSystem.js b/modules/quantcastIdSystem.js index 24d89f0e74a..9ad1a158c98 100644 --- a/modules/quantcastIdSystem.js +++ b/modules/quantcastIdSystem.js @@ -13,7 +13,7 @@ import { uspDataHandler, coppaDataHandler, gdprDataHandler } from '../src/adapte const QUANTCAST_FPA = '__qca'; const DEFAULT_COOKIE_EXP_DAYS = 392; // (13 months - 2 days) const PREBID_PCODE = 'p-KceJUEvXN48CE'; -const QSERVE_URL = 'https://pixel.quantserve.com/pixel'; +const QSERVE_URL = '//pixel.quantserve.com/pixel'; const QUANTCAST_VENDOR_ID = '11'; const PURPOSE_DATA_COLLECT = '1'; const PURPOSE_PRODUCT_IMPROVEMENT = '10'; @@ -32,13 +32,13 @@ export function firePixel(clientId, cookieExpDays = DEFAULT_COOKIE_EXP_DAYS) { var fpan = '0'; var domain = quantcastIdSubmodule.findRootDomain(); var now = new Date(); - var et = now.getTime(); var usPrivacyParamString = ''; var firstPartyParamStrings; var gdprParamStrings; if (!fpa) { var expires = new Date(now.getTime() + (cookieExpDays * 86400000)).toGMTString(); + var et = now.getTime(); fpa = 'B0-' + Math.round(Math.random() * 2147483647) + '-' + et; fpan = '1'; storage.setCookie(QUANTCAST_FPA, fpa, expires, '/', domain, null); From b03709b06904b04140e03819389ce1b7843ebc90 Mon Sep 17 00:00:00 2001 From: Sachin Shastri Date: Thu, 24 Jun 2021 11:29:21 -0700 Subject: [PATCH 10/10] refactor code --- modules/quantcastIdSystem.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/quantcastIdSystem.js b/modules/quantcastIdSystem.js index 9ad1a158c98..9e2c53a28ff 100644 --- a/modules/quantcastIdSystem.js +++ b/modules/quantcastIdSystem.js @@ -12,8 +12,9 @@ import { uspDataHandler, coppaDataHandler, gdprDataHandler } from '../src/adapte const QUANTCAST_FPA = '__qca'; const DEFAULT_COOKIE_EXP_DAYS = 392; // (13 months - 2 days) +const DAY_MS = 86400000; const PREBID_PCODE = 'p-KceJUEvXN48CE'; -const QSERVE_URL = '//pixel.quantserve.com/pixel'; +const QSERVE_URL = 'https://pixel.quantserve.com/pixel'; const QUANTCAST_VENDOR_ID = '11'; const PURPOSE_DATA_COLLECT = '1'; const PURPOSE_PRODUCT_IMPROVEMENT = '10'; @@ -37,9 +38,10 @@ export function firePixel(clientId, cookieExpDays = DEFAULT_COOKIE_EXP_DAYS) { var gdprParamStrings; if (!fpa) { - var expires = new Date(now.getTime() + (cookieExpDays * 86400000)).toGMTString(); var et = now.getTime(); - fpa = 'B0-' + Math.round(Math.random() * 2147483647) + '-' + et; + var expires = new Date(et+ (cookieExpDays * DAY_MS)).toGMTString(); + var rand = Math.round(Math.random() * 2147483647); + fpa = `B0-${rand}-${et}`; fpan = '1'; storage.setCookie(QUANTCAST_FPA, fpa, expires, '/', domain, null); }