Skip to content

Commit 278acc5

Browse files
authored
Merge pull request #143 from digi-trust/master
Third party vendor/publisher consent
2 parents 7b6c3a7 + 1e28afc commit 278acc5

File tree

11 files changed

+179
-57
lines changed

11 files changed

+179
-57
lines changed

.eslintrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
"indent": [2, "tab", {"SwitchCase": 1}],
5656
"no-mixed-spaces-and-tabs": [2, "smart-tabs"],
5757
"no-trailing-spaces": [2, { "skipBlankLines": true }],
58-
"max-nested-callbacks": [1, 3],
58+
"max-nested-callbacks": 0,
5959
"no-eval": 2,
6060
"no-implied-eval": 2,
6161
"no-new-func": 2,

src/components/popup/details/purposes/purposes.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,10 @@ export default class Purposes extends Component {
108108
const allPurposes = [...purposes, ...customPurposes];
109109
const selectedPurpose = allPurposes[selectedPurposeIndex];
110110
const selectedPurposeId = selectedPurpose && selectedPurpose.id;
111-
const purposeIsActive = selectedPurposeIndex < purposes.length ?
111+
const currentPurposeLocalizePrefix = `${selectedPurposeIndex >= purposes.length ? 'customPurpose' : 'purpose'}${selectedPurposeId}`;
112+
let purposeIsActive = selectedPurposeIndex < purposes.length ?
112113
selectedPurposeIds.has(selectedPurposeId) :
113114
selectedCustomPurposeIds.has(selectedPurposeId);
114-
const currentPurposeLocalizePrefix = `${selectedPurposeIndex >= purposes.length ? 'customPurpose' : 'purpose'}${selectedPurposeId}`;
115115

116116
return (
117117
<div class={style.container} >

src/docs/assets/portal.js

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ const parts = host.split('.');
88
const GLOBAL_VENDOR_LIST_DOMAIN = 'https://vendorlist.consensu.org/vendorlist.json';
99
const COOKIE_DOMAIN = parts.length > 1 ? `;domain=.${parts.slice(-2).join('.')}` : '';
1010
const COOKIE_MAX_AGE = 33696000;
11-
const COOKIE_NAME = 'euconsent';
11+
const VENDOR_COOKIE_NAME = 'euconsent';
12+
const PUBLISHER_COOKIE_NAME = 'pubconsent';
1213

1314
const readVendorListPromise = fetch(config.globalVendorListLocation)
1415
.then(res => res.json())
@@ -43,11 +44,19 @@ const commands = {
4344
readVendorList: () => readVendorListPromise,
4445

4546
readVendorConsent: () => {
46-
return readCookie(COOKIE_NAME);
47+
return readCookie(VENDOR_COOKIE_NAME);
4748
},
4849

49-
writeVendorConsent: ({encodedValue }) => {
50-
return writeCookie({name: COOKIE_NAME, value: encodedValue});
50+
writeVendorConsent: ({encodedValue}) => {
51+
return writeCookie({name: VENDOR_COOKIE_NAME, value: encodedValue});
52+
},
53+
54+
readPublisherConsent: () => {
55+
return readCookie(PUBLISHER_COOKIE_NAME);
56+
},
57+
58+
writePublisherConsent: ({encodedValue}) => {
59+
return writeCookie({name: PUBLISHER_COOKIE_NAME, value: encodedValue});
5160
}
5261
};
5362

src/docs/assets/purposes.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,22 @@
22
"version": 1,
33
"purposes": [
44
{
5-
"id": 1,
5+
"id": 25,
66
"name": "Custom Purpose 1",
77
"description": "Here's a description of the first purpose"
88
},
99
{
10-
"id": 2,
10+
"id": 26,
1111
"name": "Custom Purpose 2",
1212
"description": "Here's a description of the second purpose"
1313
},
1414
{
15-
"id": 3,
15+
"id": 27,
1616
"name": "Custom Purpose 3",
1717
"description": "Here's a description of the third purpose"
1818
},
1919
{
20-
"id": 4,
20+
"id": 28,
2121
"name": "Custom Purpose 4",
2222
"description": "Here's a description of the fourth purpose"
2323
}

src/lib/cmp.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ describe('cmp', () => {
8989

9090
it('getPublisherConsents executes', (done) => {
9191
cmp.processCommand('getPublisherConsents', null, data => {
92-
expect(Object.keys(data.standardPurposes).length).to.equal(vendorList.purposes.length);
92+
expect(Object.keys(data.standardPurposes).length).to.equal(24); // Per the spec, future purposes may be added, up to 24 total
9393
expect(Object.keys(data.customPurposes).length).to.equal(customPurposeList.purposes.length);
9494
done();
9595
});

src/lib/config.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import log from './log';
22
const metadata = require('../../metadata.json');
33
const defaultConfig = {
4+
storePublisherData: true,
45
customPurposeListLocation: null,
6+
storeConsentGlobally: true,
7+
storePublisherConsentGlobally: false,
58
globalVendorListLocation: metadata.globalVendorListLocation,
69
globalConsentLocation: metadata.globalConsentLocation,
7-
storeConsentGlobally: true,
8-
storePublisherData: true,
10+
globalPublisherConsentLocation: null,
911
logging: false,
1012
localization: {},
1113
forceLocale: null,

src/lib/cookie/cookie.js

Lines changed: 85 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ const PUBLISHER_CONSENT_COOKIE_MAX_AGE = 33696000;
1919
const VENDOR_CONSENT_COOKIE_NAME = 'euconsent';
2020
const VENDOR_CONSENT_COOKIE_MAX_AGE = 33696000;
2121

22+
const MAX_STANDARD_PURPOSE_ID = 24;
23+
const START_CUSTOM_PURPOSE_ID = 25;
2224

2325
function encodeVendorIdsToBits(maxVendorId, selectedVendorIds = new Set()) {
2426
let vendorString = '';
@@ -29,20 +31,28 @@ function encodeVendorIdsToBits(maxVendorId, selectedVendorIds = new Set()) {
2931
}
3032

3133
function encodePurposeIdsToBits(purposes, selectedPurposeIds = new Set()) {
34+
let purposeString = '';
35+
for (let id = 1; id <= MAX_STANDARD_PURPOSE_ID; id++) {
36+
purposeString += (selectedPurposeIds.has(id) ? '1' : '0');
37+
}
38+
return purposeString;
39+
}
40+
41+
function encodeCustomPurposeIdsToBits(purposes, selectedPurposeIds = new Set()) {
3242
const maxPurposeId = Math.max(0,
3343
...purposes.map(({id}) => id),
3444
...Array.from(selectedPurposeIds));
3545
let purposeString = '';
36-
for (let id = 1; id <= maxPurposeId; id++) {
46+
for (let id = START_CUSTOM_PURPOSE_ID; id <= maxPurposeId; id++) {
3747
purposeString += (selectedPurposeIds.has(id) ? '1' : '0');
3848
}
3949
return purposeString;
4050
}
4151

42-
function decodeBitsToIds(bitString) {
52+
function decodeBitsToIds(bitString, addToIndex = 0) {
4353
return bitString.split('').reduce((acc, bit, index) => {
4454
if (bit === '1') {
45-
acc.add(index + 1);
55+
acc.add(index + addToIndex + 1);
4656
}
4757
return acc;
4858
}, new Set());
@@ -176,11 +186,11 @@ function encodePublisherConsentData(publisherData) {
176186
...publisherData,
177187
numCustomPurposes: customPurposes.length,
178188
standardPurposeIdBitString: encodePurposeIdsToBits(purposes, selectedPurposeIds),
179-
customPurposeIdBitString: encodePurposeIdsToBits(customPurposes, selectedCustomPurposeIds)
189+
customPurposeIdBitString: encodeCustomPurposeIdsToBits(customPurposes, selectedCustomPurposeIds)
180190
});
181191
}
182192

183-
function decodePublisherConsentData(cookieValue) {
193+
function decodePublisherConsentData(cookieValue, source) {
184194
const {
185195
cookieVersion,
186196
cmpId,
@@ -196,6 +206,7 @@ function decodePublisherConsentData(cookieValue) {
196206
} = decodePublisherCookieValue(cookieValue);
197207

198208
return {
209+
consentString: cookieValue,
199210
cookieVersion,
200211
cmpId,
201212
cmpVersion,
@@ -205,8 +216,9 @@ function decodePublisherConsentData(cookieValue) {
205216
publisherPurposesVersion,
206217
created,
207218
lastUpdated,
219+
source,
208220
selectedPurposeIds: decodeBitsToIds(standardPurposeIdBitString),
209-
selectedCustomPurposeIds: decodeBitsToIds(customPurposeIdBitString)
221+
selectedCustomPurposeIds: decodeBitsToIds(customPurposeIdBitString, MAX_STANDARD_PURPOSE_ID)
210222
};
211223

212224
}
@@ -225,25 +237,81 @@ function writeCookie(name, value, maxAgeSeconds, path = '/') {
225237
document.cookie = `${name}=${value};path=${path}${maxAge}`;
226238
}
227239

228-
function readPublisherConsentCookie() {
229-
// If configured try to read publisher cookie
230-
if (config.storePublisherData) {
231-
const cookie = readCookie(PUBLISHER_CONSENT_COOKIE_NAME);
232-
log.debug('Read publisher consent data from local cookie', cookie);
233-
if (cookie) {
234-
return decodePublisherConsentData(cookie);
240+
/**
241+
* Read publisher consent data from third-party cookie on the
242+
* configured third party domain. Fallback to first-party cookie
243+
* if the operation fails.
244+
*
245+
* @returns Promise resolved with decoded cookie object
246+
*/
247+
function readGlobalPublisherConsentCookie() {
248+
log.debug('Request publisher consent data from global cookie');
249+
return sendPortalCommand({
250+
command: 'readPublisherConsent',
251+
}).then(result => {
252+
log.debug('Read publisher consent data from global cookie', result);
253+
if (result) {
254+
return decodePublisherConsentData(result, "global");
235255
}
236-
}
256+
return readLocalPublisherConsentCookie();
257+
}).catch(err => {
258+
log.error('Failed reading third party publisher consent cookie', err);
259+
});
237260
}
238261

239-
function writePublisherConsentCookie(publisherConsentData) {
262+
/**
263+
* Write publisher consent data to third-party cookie on the
264+
* configured third party domain. Fallback to first-party cookie
265+
* if the operation fails.
266+
*
267+
* @returns Promise resolved after cookie is written
268+
*/
269+
function writeGlobalPublisherConsentCookie(publisherConsentData) {
270+
log.debug('Write publisher consent data to third party cookie', publisherConsentData);
271+
return sendPortalCommand({
272+
command: 'writePublisherConsent',
273+
encodedValue: encodePublisherConsentData(publisherConsentData),
274+
publisherConsentData
275+
})
276+
.then((succeeded) => {
277+
if ( !succeeded ) {
278+
return writeLocalPublisherConsentCookie(publisherConsentData);
279+
}
280+
return Promise.resolve();
281+
})
282+
.catch(err => {
283+
log.error('Failed writing third party publisher consent cookie', err);
284+
});
285+
}
286+
287+
function readLocalPublisherConsentCookie() {
288+
// If configured try to read publisher cookie
289+
const cookie = readCookie(PUBLISHER_CONSENT_COOKIE_NAME);
290+
log.debug('Read publisher consent data from local cookie', cookie);
291+
return Promise.resolve(cookie && decodePublisherConsentData(cookie, 'local'));
292+
}
293+
294+
function writeLocalPublisherConsentCookie(publisherConsentData) {
240295
log.debug('Write publisher consent data to local cookie', publisherConsentData);
241-
writeCookie(PUBLISHER_CONSENT_COOKIE_NAME,
296+
return Promise.resolve(writeCookie(PUBLISHER_CONSENT_COOKIE_NAME,
242297
encodePublisherConsentData(publisherConsentData),
243298
PUBLISHER_CONSENT_COOKIE_MAX_AGE,
244-
'/');
299+
'/'));
245300
}
246301

302+
function readPublisherConsentCookie() {
303+
if (config.storePublisherData) {
304+
return config.storePublisherConsentGlobally ?
305+
readGlobalPublisherConsentCookie() : readLocalPublisherConsentCookie();
306+
}
307+
}
308+
309+
function writePublisherConsentCookie(publisherConsentData) {
310+
if (config.storePublisherData) {
311+
return config.storePublisherConsentGlobally ?
312+
writeGlobalPublisherConsentCookie(publisherConsentData) : writeLocalPublisherConsentCookie(publisherConsentData);
313+
}
314+
}
247315

248316
/**
249317
* Read vendor consent data from third-party cookie on the

src/lib/cookie/cookie.test.js

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -128,17 +128,18 @@ describe('cookie', () => {
128128
publisherPurposesVersion: 1,
129129
created: aDate,
130130
lastUpdated: aDate,
131-
selectedCustomPurposeIds: new Set([2, 3])
131+
source: 'local',
132+
selectedCustomPurposeIds: new Set([25, 26])
132133
};
133134

134135
const encodedString = encodePublisherConsentData({
135136
...vendorConsentData, ...publisherConsentData,
136137
vendorList,
137138
customPurposeList
138139
});
139-
const decoded = decodePublisherConsentData(encodedString);
140+
const decoded = decodePublisherConsentData(encodedString, 'local');
140141

141-
expect(decoded).to.deep.equal({...vendorConsentData, ...publisherConsentData});
142+
expect(decoded).to.deep.include({...vendorConsentData, ...publisherConsentData});
142143
});
143144

144145
it('writes and reads the local cookie when globalConsent = false', () => {
@@ -199,26 +200,70 @@ describe('cookie', () => {
199200
});
200201
});
201202

202-
it('writes and reads the publisher consent cookie', () => {
203+
it('writes and reads the publisher consent cookie locally', () => {
203204
config.update({
204-
storeConsentGlobally: false,
205+
storePublisherConsentGlobally: false,
205206
storePublisherData: true
206207
});
207208

208209
const publisherConsentData = {
209210
cookieVersion: 1,
210-
cmpId: 1,
211+
cmpId: 15,
212+
cmpVersion: 2,
213+
consentScreen: 1,
214+
consentLanguage: "EN",
211215
vendorListVersion: 1,
212216
publisherPurposesVersion: 1,
213217
created: aDate,
214218
lastUpdated: aDate,
219+
source: 'local',
220+
vendorList,
221+
customPurposeList,
222+
selectedPurposeIds: new Set([1, 2]),
223+
selectedCustomPurposeIds: new Set([25, 26])
215224
};
216225

217-
writePublisherConsentCookie(publisherConsentData);
218-
const fromCookie = readPublisherConsentCookie();
226+
return writePublisherConsentCookie(publisherConsentData).then(() => {
227+
return readPublisherConsentCookie().then((cookie) => {
228+
expect(document.cookie).to.contain(PUBLISHER_CONSENT_COOKIE_NAME);
229+
expect(cookie).to.deep.include({
230+
"cookieVersion":1,
231+
"cmpId":15,
232+
"cmpVersion":2,
233+
"consentScreen":1,
234+
"consentLanguage":"EN",
235+
"vendorListVersion":1,
236+
"publisherPurposesVersion":1,
237+
"created": aDate,
238+
"lastUpdated": aDate,
239+
"source":"local",
240+
"selectedPurposeIds": new Set([1, 2]),
241+
"selectedCustomPurposeIds": new Set([25, 26])
242+
});
243+
});
244+
});
245+
});
219246

220-
expect(document.cookie).to.contain(PUBLISHER_CONSENT_COOKIE_NAME);
221-
expect(fromCookie).to.deep.include(publisherConsentData);
247+
it('writes and reads the publisher consent cookie on a third party domain', () => {
248+
config.update({
249+
storePublisherConsentGlobally: true,
250+
globalPublisherConsentLocation: "https://www.example.com",
251+
storePublisherData: true
252+
});
253+
254+
const publisherConsentData = {
255+
cmpId: 1,
256+
vendorListVersion: 1,
257+
publisherPurposesVersion: 1,
258+
created: aDate,
259+
lastUpdated: aDate,
260+
source: 'global'
261+
};
262+
263+
return writePublisherConsentCookie(publisherConsentData).then(() => {
264+
expect(mockPortal.sendPortalCommand.mock.calls[0][0].command).to.deep.equal('writePublisherConsent');
265+
expect(document.cookie).to.contain(PUBLISHER_CONSENT_COOKIE_NAME);
266+
});
222267
});
223268

224269
it('converts selected vendor list to a range', () => {

0 commit comments

Comments
 (0)