Skip to content

Commit

Permalink
consentManagementGpp: support GPP 1.1 (#10282)
Browse files Browse the repository at this point in the history
* add option to use callbacks in lieu of return values when talking to CMPs

* Add `once` option to cmpClient

* introduce MODE_RETURN; add .close() method to CMP clients

* GPP 1.0/1.1 clients

* update gppControl for 1.1

* linting
  • Loading branch information
dgirardi authored Aug 16, 2023
1 parent 62c9d29 commit 7fc315f
Show file tree
Hide file tree
Showing 6 changed files with 1,074 additions and 539 deletions.
62 changes: 46 additions & 16 deletions libraries/cmp/cmpClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,53 @@ 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
* @param {bool} once if true, discard cross-frame event listeners once a reply message is received.
* @returns {Promise<*>} a promise to the API's "result" - see the `mode` argument to `cmpClient` on how that's determined.
* @property {boolean} isDirect true if the CMP is directly accessible (no postMessage required)
* @property {() => void} close close the client; currently, this just stops listening for cross-frame messages.
*/

/**
* Returns a function that can interface with a CMP regardless of where it's located.
* Returns a client 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 mode? controls the callbacks passed to the underlying API, and how the promises returned by the client are resolved.
*
* The client behaves differently when it's provided a `callback` argument vs when it's not - for short, let's name these
* cases "subscriptions" and "one-shot calls" respectively:
*
* With `mode: MODE_MIXED` (the default), promises returned on subscriptions are resolved to undefined when the callback
* is first run (that is, the promise resolves when the CMP replies, but what it replies with is discarded and
* left for the callback to deal with). For one-shot calls, the returned promise is resolved to the API's
* return value when it's directly accessible, or with the result from the first (and, presumably, the only)
* cross-frame reply when it's not;
*
* With `mode: MODE_RETURN`, the returned promise always resolves to the API's return value - which is taken to be undefined
* when cross-frame;
*
* With `mode: MODE_CALLBACK`, the underlying API is expected to never directly return anything significant; instead,
* it should always accept a callback and - for one-shot calls - invoke it only once with the result. The client will
* automatically generate an appropriate callback for one-shot calls and use the result it's given to resolve
* the returned promise. Subscriptions are treated in the same way as MODE_MIXED.
*
* @param win
* @returns {CMPClient} CMP invocation function (or null if no CMP was found).
*/

export const MODE_MIXED = 0;
export const MODE_RETURN = 1;
export const MODE_CALLBACK = 2;

export function cmpClient(
{
apiName,
apiVersion,
apiArgs = ['command', 'callback', 'parameter', 'version'],
callbackArgs = ['returnValue', 'success'],
mode = MODE_MIXED,
},
win = window
) {
Expand Down Expand Up @@ -89,15 +114,15 @@ export function cmpClient(
}

function wrapCallback(callback, resolve, reject, preamble) {
const haveCb = typeof callback === 'function';

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);
if (mode !== MODE_RETURN) {
const resolver = success == null || success ? resolve : reject;
resolver(haveCb ? undefined : result);
}
haveCb && callback.apply(this, arguments);
}
}

Expand All @@ -108,17 +133,17 @@ export function cmpClient(
return new GreedyPromise((resolve, reject) => {
const ret = cmpFrame[apiName](...resolveParams({
...params,
callback: params.callback && wrapCallback(params.callback, resolve, reject)
callback: (params.callback || mode === MODE_CALLBACK) ? wrapCallback(params.callback, resolve, reject) : undefined,
}).map(([_, val]) => val));
if (params.callback == null) {
if (mode === MODE_RETURN || (params.callback == null && mode === MODE_MIXED)) {
resolve(ret);
}
});
};
} else {
win.addEventListener('message', handleMessage, false);

client = function invokeCMPFrame(params) {
client = function invokeCMPFrame(params, once = false) {
return new GreedyPromise((resolve, reject) => {
// call CMP via postMessage
const callId = Math.random().toString();
Expand All @@ -129,11 +154,16 @@ export function cmpClient(
}
};

cmpCallbacks[callId] = wrapCallback(params?.callback, resolve, reject, params?.callback == null && (() => { delete cmpCallbacks[callId] }));
cmpCallbacks[callId] = wrapCallback(params?.callback, resolve, reject, (once || params?.callback == null) && (() => { delete cmpCallbacks[callId] }));
cmpFrame.postMessage(msg, '*');
if (mode === MODE_RETURN) resolve();
});
};
}
client.isDirect = isDirect;
return client;
return Object.assign(client, {
isDirect,
close() {
!isDirect && win.removeEventListener('message', handleMessage);
}
})
}
26 changes: 19 additions & 7 deletions libraries/mspa/activityControls.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,26 +87,38 @@ const CONSENT_RULES = {
[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() {
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}
return {allow: false};
}
}
}
};
}

function flatSection(subsections) {
if (subsections == null) return subsections;
return subsections.reduceRight((subsection, consent) => {
return Object.assign(consent, subsection);
}, {});
}

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())
unreg.push(registerRule(activity, `MSPA (${api})`, mspaRule(
sids,
() => normalizeConsent(flatSection(getConsentData()?.parsedSections?.[api])),
denies,
() => getConsentData()?.applicableSections || []
)));
});
return () => unreg.forEach(ur => ur());
}
Loading

0 comments on commit 7fc315f

Please sign in to comment.