forked from prebid/Prebid.js
-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* share CMP client code * consentManagementGpp: fetch section data * mock out getSection for legacy tests * Make gppConsent available as an activity param; also fix various test suites to set up during set up * gppControl_usnat * load all section data (instead of trying to figure out what is applicable) * Do not expect top window to be accessible * update usnat consent interpretation * Update activityControls.js * Update activityControls.js * Update activityControls_spec.js * Update activityControls.js * Update activityControls.js * Update activityControls_spec.js * Update activityControls.js * Update activityControls.js * Update activityControls.js * Update activityControls_spec.js * Update activityControls_spec.js * Update activityControls_spec.js * Update activityControls.js * Update activityControls_spec.js * Update activityControls_spec.js * Update activityControls_spec.js * Update activityControls_spec.js * Update activityControls_spec.js * Update activityControls_spec.js * Update activityControls_spec.js * Update activityControls.js * Update activityControls_spec.js * Update activityControls_spec.js * Update activityControls_spec.js * Update activityControls.js * Update activityControls_spec.js * Update activityControls_spec.js * Update activityControls_spec.js * Update activityControls.js * Update activityControls_spec.js * Update activityControls.js * Update activityControls.js * Update activityControls.js --------- Co-authored-by: Patrick McCann <[email protected]>
- Loading branch information
1 parent
8f075c4
commit 5e22f2f
Showing
24 changed files
with
1,176 additions
and
466 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
import {GreedyPromise} from '../../src/utils/promise.js'; | ||
|
||
/** | ||
* @typedef {function} CMPClient | ||
* | ||
* @param {{}} params CMP parameters. Currently this is a subset of {command, callback, parameter, version}. | ||
* @returns {Promise<*>} a promise that: | ||
* - if a `callback` param was provided, resolves (with no result) just before the first time it's run; | ||
* - if `callback` was *not* provided, resolves to the return value of the CMP command | ||
* @property {boolean} isDirect true if the CMP is directly accessible (no postMessage required) | ||
*/ | ||
|
||
/** | ||
* Returns a function that can interface with a CMP regardless of where it's located. | ||
* | ||
* @param apiName name of the CMP api, e.g. "__gpp" | ||
* @param apiVersion? CMP API version | ||
* @param apiArgs? names of the arguments taken by the api function, in order. | ||
* @param callbackArgs? names of the cross-frame response payload properties that should be passed as callback arguments, in order | ||
* @param win | ||
* @returns {CMPClient} CMP invocation function (or null if no CMP was found). | ||
*/ | ||
export function cmpClient( | ||
{ | ||
apiName, | ||
apiVersion, | ||
apiArgs = ['command', 'callback', 'parameter', 'version'], | ||
callbackArgs = ['returnValue', 'success'], | ||
}, | ||
win = window | ||
) { | ||
const cmpCallbacks = {}; | ||
const callName = `${apiName}Call`; | ||
const cmpDataPkgName = `${apiName}Return`; | ||
|
||
function handleMessage(event) { | ||
const json = (typeof event.data === 'string' && event.data.includes(cmpDataPkgName)) ? JSON.parse(event.data) : event.data; | ||
if (json?.[cmpDataPkgName]?.callId) { | ||
const payload = json[cmpDataPkgName]; | ||
|
||
if (cmpCallbacks.hasOwnProperty(payload.callId)) { | ||
cmpCallbacks[payload.callId](...callbackArgs.map(name => payload[name])); | ||
} | ||
} | ||
} | ||
|
||
function findCMP() { | ||
let f = win; | ||
let cmpFrame; | ||
let isDirect = false; | ||
while (f != null) { | ||
try { | ||
if (typeof f[apiName] === 'function') { | ||
cmpFrame = f; | ||
isDirect = true; | ||
break; | ||
} | ||
} catch (e) { | ||
} | ||
|
||
// need separate try/catch blocks due to the exception errors thrown when trying to check for a frame that doesn't exist in 3rd party env | ||
try { | ||
if (f.frames[`${apiName}Locator`]) { | ||
cmpFrame = f; | ||
break; | ||
} | ||
} catch (e) { | ||
} | ||
|
||
if (f === win.top) break; | ||
f = f.parent; | ||
} | ||
|
||
return [ | ||
cmpFrame, | ||
isDirect | ||
]; | ||
} | ||
|
||
const [cmpFrame, isDirect] = findCMP(); | ||
|
||
if (!cmpFrame) { | ||
return; | ||
} | ||
|
||
function resolveParams(params) { | ||
params = Object.assign({version: apiVersion}, params); | ||
return apiArgs.map(arg => [arg, params[arg]]) | ||
} | ||
|
||
function wrapCallback(callback, resolve, reject, preamble) { | ||
return function (result, success) { | ||
preamble && preamble(); | ||
const resolver = success == null || success ? resolve : reject; | ||
if (typeof callback === 'function') { | ||
resolver(); | ||
return callback.apply(this, arguments); | ||
} else { | ||
resolver(result); | ||
} | ||
} | ||
} | ||
|
||
let client; | ||
|
||
if (isDirect) { | ||
client = function invokeCMPDirect(params = {}) { | ||
return new GreedyPromise((resolve, reject) => { | ||
const ret = cmpFrame[apiName](...resolveParams({ | ||
...params, | ||
callback: params.callback && wrapCallback(params.callback, resolve, reject) | ||
}).map(([_, val]) => val)); | ||
if (params.callback == null) { | ||
resolve(ret); | ||
} | ||
}); | ||
}; | ||
} else { | ||
win.addEventListener('message', handleMessage, false); | ||
|
||
client = function invokeCMPFrame(params) { | ||
return new GreedyPromise((resolve, reject) => { | ||
// call CMP via postMessage | ||
const callId = Math.random().toString(); | ||
const msg = { | ||
[callName]: { | ||
...Object.fromEntries(resolveParams(params).filter(([param]) => param !== 'callback')), | ||
callId: callId | ||
} | ||
}; | ||
|
||
cmpCallbacks[callId] = wrapCallback(params?.callback, resolve, reject, params?.callback == null && (() => { delete cmpCallbacks[callId] })); | ||
cmpFrame.postMessage(msg, '*'); | ||
}); | ||
}; | ||
} | ||
client.isDirect = isDirect; | ||
return client; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import {registerActivityControl} from '../../src/activities/rules.js'; | ||
import { | ||
ACTIVITY_ENRICH_EIDS, | ||
ACTIVITY_ENRICH_UFPD, | ||
ACTIVITY_SYNC_USER, | ||
ACTIVITY_TRANSMIT_PRECISE_GEO | ||
} from '../../src/activities/activities.js'; | ||
import {gppDataHandler} from '../../src/adapterManager.js'; | ||
|
||
// default interpretation for MSPA consent(s): | ||
// https://docs.google.com/document/d/1yPEVx3bBdSkIw-QNkQR5qi1ztmn9zoXk-LaGQB6iw7Q | ||
|
||
export function isBasicConsentDenied(cd) { | ||
// service provider mode is always consent denied | ||
return ['MspaServiceProviderMode', 'Gpc'].some(prop => cd[prop] === 1) || | ||
// you cannot consent to what you were not notified of | ||
cd.PersonalDataConsents === 2 || | ||
// minors 13+ who have not given consent | ||
cd.KnownChildSensitiveDataConsents[0] === 1 || | ||
// minors under 13 cannot consent | ||
cd.KnownChildSensitiveDataConsents[1] !== 0 || | ||
// sensitive category consent impossible without notice | ||
(cd.SensitiveDataProcessing.some(element => element === 2) && (cd.SensitiveDataLimitUseNotice !== 1 || cd.SensitiveDataProcessingOptOutNotice !== 1)); | ||
} | ||
|
||
export function isSensitiveNoticeMissing(cd) { | ||
return ['SensitiveDataProcessingOptOutNotice', 'SensitiveDataLimitUseNotice'].some(prop => cd[prop] === 2) | ||
} | ||
|
||
export function isConsentDenied(cd) { | ||
return isBasicConsentDenied(cd) || | ||
// user opts out | ||
(['SaleOptOut', 'SharingOptOut', 'TargetedAdvertisingOptOut'].some(prop => cd[prop] === 1)) || | ||
// notice not given | ||
(['SaleOptOutNotice', 'SharingNotice', 'SharingOptOutNotice', 'TargetedAdvertisingOptOutNotice'].some(prop => cd[prop] === 2)) || | ||
// sale opted in but notice does not apply | ||
((cd.SaleOptOut === 2 && cd.SaleOptOutNotice === 0)) || | ||
// targeted opted in but notice does not apply | ||
((cd.TargetedAdvertisingOptOut === 2 && cd.TargetedAdvertisingOptOutNotice === 0)) || | ||
// sharing opted in but notices do not apply | ||
((cd.SharingOptOut === 2 && (cd.SharingNotice === 0 || cd.SharingOptOutNotice === 0))); | ||
} | ||
|
||
export function isTransmitUfpdConsentDenied(cd) { | ||
// SensitiveDataProcessing[1-5,11]=1 OR SensitiveDataProcessing[6,7,9,10,12]<>0 OR | ||
const mustBeZero = [6, 7, 9, 10, 12]; | ||
const mustBeZeroSubtractedVector = mustBeZero.map((number) => number - 1); | ||
const SensitiveDataProcessingMustBeZero = cd.SensitiveDataProcessing.filter(index => mustBeZeroSubtractedVector.includes(index)); | ||
const cannotBeOne = [1, 2, 3, 4, 5, 11]; | ||
const cannotBeOneSubtractedVector = cannotBeOne.map((number) => number - 1); | ||
const SensitiveDataProcessingCannotBeOne = cd.SensitiveDataProcessing.filter(index => cannotBeOneSubtractedVector.includes(index)); | ||
return isConsentDenied(cd) || | ||
isSensitiveNoticeMissing(cd) || | ||
SensitiveDataProcessingCannotBeOne.some(val => val === 1) || | ||
SensitiveDataProcessingMustBeZero.some(val => val !== 0); | ||
} | ||
|
||
export function isTransmitGeoConsentDenied(cd) { | ||
return isBasicConsentDenied(cd) || | ||
isSensitiveNoticeMissing(cd) || | ||
cd.SensitiveDataProcessing[7] === 1 | ||
} | ||
|
||
const CONSENT_RULES = { | ||
[ACTIVITY_SYNC_USER]: isConsentDenied, | ||
[ACTIVITY_ENRICH_EIDS]: isConsentDenied, | ||
[ACTIVITY_ENRICH_UFPD]: isTransmitUfpdConsentDenied, | ||
[ACTIVITY_TRANSMIT_PRECISE_GEO]: isTransmitGeoConsentDenied | ||
} | ||
|
||
export function mspaRule(sids, getConsent, denies, applicableSids = () => gppDataHandler.getConsentData()?.applicableSections) { | ||
return function() { | ||
if (applicableSids().some(sid => sids.includes(sid))) { | ||
const consent = getConsent(); | ||
if (consent == null) { | ||
return {allow: false, reason: 'consent data not available'}; | ||
} | ||
if (denies(consent)) { | ||
return {allow: false} | ||
} | ||
} | ||
} | ||
} | ||
|
||
export function setupRules(api, sids, normalizeConsent = (c) => c, rules = CONSENT_RULES, registerRule = registerActivityControl, getConsentData = () => gppDataHandler.getConsentData()) { | ||
const unreg = []; | ||
Object.entries(rules).forEach(([activity, denies]) => { | ||
unreg.push(registerRule(activity, `MSPA (${api})`, mspaRule(sids, () => normalizeConsent(getConsentData()?.sectionData?.[api]), denies, () => getConsentData()?.applicableSections || []))) | ||
}) | ||
return () => unreg.forEach(ur => ur()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.