@@ -19,6 +19,7 @@ export let consentTimeout;
19
19
export let allowAuction ;
20
20
export let staticConsentData ;
21
21
22
+ let cmpVersion = 0 ;
22
23
let consentData ;
23
24
let addedConsentHook = false ;
24
25
@@ -47,11 +48,70 @@ function lookupStaticConsentData(cmpSuccess, cmpError, hookConfig) {
47
48
* @param {object } hookConfig contains module related variables (see comment in requestBidsHook function)
48
49
*/
49
50
function lookupIabConsent ( cmpSuccess , cmpError , hookConfig ) {
50
- function handleCmpResponseCallbacks ( ) {
51
+ function findCMP ( ) {
52
+ let f = window ;
53
+ let cmpFrame ;
54
+ let cmpFunction ;
55
+ while ( ! cmpFrame ) {
56
+ try {
57
+ if ( typeof f . __tcfapi === 'function' || typeof f . __cmp === 'function' ) {
58
+ if ( typeof f . __tcfapi === 'function' ) {
59
+ cmpVersion = 2 ;
60
+ cmpFunction = f . __tcfapi ;
61
+ } else {
62
+ cmpVersion = 1 ;
63
+ cmpFunction = f . __cmp ;
64
+ }
65
+ cmpFrame = f ;
66
+ break ;
67
+ }
68
+ } catch ( e ) { }
69
+
70
+ // 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
71
+ try {
72
+ if ( f . frames [ '__tcfapiLocator' ] ) {
73
+ cmpVersion = 2 ;
74
+ cmpFrame = f ;
75
+ break ;
76
+ }
77
+ } catch ( e ) { }
78
+
79
+ try {
80
+ if ( f . frames [ '__cmpLocator' ] ) {
81
+ cmpVersion = 1 ;
82
+ cmpFrame = f ;
83
+ break ;
84
+ }
85
+ } catch ( e ) { }
86
+
87
+ if ( f === window . top ) break ;
88
+ f = f . parent ;
89
+ }
90
+ return {
91
+ cmpFrame,
92
+ cmpFunction
93
+ } ;
94
+ }
95
+
96
+ function v2CmpResponseCallback ( tcfData , success ) {
97
+ utils . logInfo ( 'Received a response from CMP' , tcfData ) ;
98
+ if ( success ) {
99
+ if ( tcfData . eventStatus === 'tcloaded' || tcfData . eventStatus === 'useractioncomplete' ) {
100
+ cmpSuccess ( tcfData , hookConfig ) ;
101
+ } else if ( tcfData . eventStatus === 'cmpuishown' && tcfData . tcString . length > 0 && tcfData . purposeOneTreatment === true ) {
102
+ cmpSuccess ( tcfData , hookConfig ) ;
103
+ }
104
+ } else {
105
+ cmpError ( 'CMP unable to register callback function. Please check CMP setup.' , hookConfig ) ;
106
+ }
107
+ }
108
+
109
+ function handleV1CmpResponseCallbacks ( ) {
51
110
const cmpResponse = { } ;
52
111
53
112
function afterEach ( ) {
54
113
if ( cmpResponse . getConsentData && cmpResponse . getVendorConsents ) {
114
+ utils . logInfo ( 'Received all requested responses from CMP' , cmpResponse ) ;
55
115
cmpSuccess ( cmpResponse , hookConfig ) ;
56
116
}
57
117
}
@@ -68,10 +128,13 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) {
68
128
}
69
129
}
70
130
71
- let callbackHandler = handleCmpResponseCallbacks ( ) ;
131
+ let v1CallbackHandler = handleV1CmpResponseCallbacks ( ) ;
72
132
let cmpCallbacks = { } ;
73
- let cmpFunction ;
133
+ let { cmpFrame , cmpFunction } = findCMP ( ) ;
74
134
135
+ if ( ! cmpFrame ) {
136
+ return cmpError ( 'CMP not found.' , hookConfig ) ;
137
+ }
75
138
// to collect the consent information from the user, we perform two calls to the CMP in parallel:
76
139
// first to collect the user's consent choices represented in an encoded string (via getConsentData)
77
140
// second to collect the user's full unparsed consent information (via getVendorConsents)
@@ -81,34 +144,28 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) {
81
144
// check to see if prebid is in a safeframe (with CMP support)
82
145
// else assume prebid may be inside an iframe and use the IAB CMP locator code to see if CMP's located in a higher parent window. this works in cross domain iframes
83
146
// if the CMP is not found, the iframe function will call the cmpError exit callback to abort the rest of the CMP workflow
84
- try {
85
- cmpFunction = window . __cmp || utils . getWindowTop ( ) . __cmp ;
86
- } catch ( e ) { }
87
147
88
148
if ( utils . isFn ( cmpFunction ) ) {
89
- cmpFunction ( 'getConsentData' , null , callbackHandler . consentDataCallback ) ;
90
- cmpFunction ( 'getVendorConsents' , null , callbackHandler . vendorConsentsCallback ) ;
91
- } else if ( inASafeFrame ( ) && typeof window . $sf . ext . cmp === 'function' ) {
92
- callCmpWhileInSafeFrame ( 'getConsentData' , callbackHandler . consentDataCallback ) ;
93
- callCmpWhileInSafeFrame ( 'getVendorConsents' , callbackHandler . vendorConsentsCallback ) ;
94
- } else {
95
- // find the CMP frame
96
- let f = window ;
97
- let cmpFrame ;
98
- while ( ! cmpFrame ) {
99
- try {
100
- if ( f . frames [ '__cmpLocator' ] ) cmpFrame = f ;
101
- } catch ( e ) { }
102
- if ( f === window . top ) break ;
103
- f = f . parent ;
149
+ utils . logInfo ( 'Detected CMP API is directly accessible, calling it now...' ) ;
150
+ if ( cmpVersion === 1 ) {
151
+ cmpFunction ( 'getConsentData' , null , v1CallbackHandler . consentDataCallback ) ;
152
+ cmpFunction ( 'getVendorConsents' , null , v1CallbackHandler . vendorConsentsCallback ) ;
153
+ } else if ( cmpVersion === 2 ) {
154
+ cmpFunction ( 'addEventListener' , cmpVersion , v2CmpResponseCallback ) ;
104
155
}
105
-
106
- if ( ! cmpFrame ) {
107
- return cmpError ( 'CMP not found.' , hookConfig ) ;
156
+ } else if ( cmpVersion === 1 && inASafeFrame ( ) && typeof window . $sf . ext . cmp === 'function' ) {
157
+ // this safeframe workflow is only supported with TCF v1 spec; the v2 recommends to use the iframe postMessage route instead (even if you are in a safeframe).
158
+ utils . logInfo ( 'Detected Prebid.js is encased in a SafeFrame and CMP is registered, calling it now...' ) ;
159
+ callCmpWhileInSafeFrame ( 'getConsentData' , v1CallbackHandler . consentDataCallback ) ;
160
+ callCmpWhileInSafeFrame ( 'getVendorConsents' , v1CallbackHandler . vendorConsentsCallback ) ;
161
+ } else {
162
+ utils . logInfo ( 'Detected CMP is outside the current iframe where Prebid.js is located, calling it now...' ) ;
163
+ if ( cmpVersion === 1 ) {
164
+ callCmpWhileInIframe ( 'getConsentData' , cmpFrame , v1CallbackHandler . consentDataCallback ) ;
165
+ callCmpWhileInIframe ( 'getVendorConsents' , cmpFrame , v1CallbackHandler . vendorConsentsCallback ) ;
166
+ } else if ( cmpVersion === 2 ) {
167
+ callCmpWhileInIframe ( 'addEventListener' , cmpFrame , v2CmpResponseCallback ) ;
108
168
}
109
-
110
- callCmpWhileInIframe ( 'getConsentData' , cmpFrame , callbackHandler . consentDataCallback ) ;
111
- callCmpWhileInIframe ( 'getVendorConsents' , cmpFrame , callbackHandler . vendorConsentsCallback ) ;
112
169
}
113
170
114
171
function inASafeFrame ( ) {
@@ -138,17 +195,22 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) {
138
195
}
139
196
140
197
function callCmpWhileInIframe ( commandName , cmpFrame , moduleCallback ) {
198
+ let apiName = ( cmpVersion === 2 ) ? '__tcfapi' : '__cmp' ;
199
+
141
200
/* Setup up a __cmp function to do the postMessage and stash the callback.
142
201
This function behaves (from the caller's perspective identicially to the in-frame __cmp call */
143
- window . __cmp = function ( cmd , arg , callback ) {
202
+ window [ apiName ] = function ( cmd , arg , callback ) {
144
203
let callId = Math . random ( ) + '' ;
204
+ let callName = `${ apiName } Call` ;
145
205
let msg = {
146
- __cmpCall : {
206
+ [ callName ] : {
147
207
command : cmd ,
148
208
parameter : arg ,
149
209
callId : callId
150
210
}
151
211
} ;
212
+ if ( cmpVersion !== 1 ) msg [ callName ] . version = cmpVersion ;
213
+
152
214
cmpCallbacks [ callId ] = callback ;
153
215
cmpFrame . postMessage ( msg , '*' ) ;
154
216
}
@@ -157,28 +219,19 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) {
157
219
window . addEventListener ( 'message' , readPostMessageResponse , false ) ;
158
220
159
221
// call CMP
160
- window . __cmp ( commandName , null , cmpIframeCallback ) ;
222
+ window [ apiName ] ( commandName , null , moduleCallback ) ;
161
223
162
224
function readPostMessageResponse ( event ) {
163
- let json = ( typeof event . data === 'string' && strIncludes ( event . data , 'cmpReturn' ) ) ? JSON . parse ( event . data ) : event . data ;
164
- if ( json . __cmpReturn && json . __cmpReturn . callId ) {
165
- let i = json . __cmpReturn ;
225
+ let cmpDataPkgName = `${ apiName } Return` ;
226
+ let json = ( typeof event . data === 'string' && strIncludes ( event . data , cmpDataPkgName ) ) ? JSON . parse ( event . data ) : event . data ;
227
+ if ( json [ cmpDataPkgName ] && json [ cmpDataPkgName ] . callId ) {
228
+ let payload = json [ cmpDataPkgName ] ;
166
229
// TODO - clean up this logic (move listeners?); we have duplicate messages responses because 2 eventlisteners are active from the 2 cmp requests running in parallel
167
- if ( typeof cmpCallbacks [ i . callId ] !== 'undefined' ) {
168
- cmpCallbacks [ i . callId ] ( i . returnValue , i . success ) ;
169
- delete cmpCallbacks [ i . callId ] ;
230
+ if ( typeof cmpCallbacks [ payload . callId ] !== 'undefined' ) {
231
+ cmpCallbacks [ payload . callId ] ( payload . returnValue , payload . success ) ;
170
232
}
171
233
}
172
234
}
173
-
174
- function removePostMessageListener ( ) {
175
- window . removeEventListener ( 'message' , readPostMessageResponse , false ) ;
176
- }
177
-
178
- function cmpIframeCallback ( consentObject ) {
179
- removePostMessageListener ( ) ;
180
- moduleCallback ( consentObject ) ;
181
- }
182
235
}
183
236
}
184
237
@@ -204,6 +257,7 @@ export function requestBidsHook(fn, reqBidsConfigObj) {
204
257
205
258
// in case we already have consent (eg during bid refresh)
206
259
if ( consentData ) {
260
+ utils . logInfo ( 'User consent information already known. Pulling internally stored information...' ) ;
207
261
return exitModule ( null , hookConfig ) ;
208
262
}
209
263
@@ -232,22 +286,50 @@ export function requestBidsHook(fn, reqBidsConfigObj) {
232
286
* @param {object } hookConfig contains module related variables (see comment in requestBidsHook function)
233
287
*/
234
288
function processCmpData ( consentObject , hookConfig ) {
235
- let gdprApplies = consentObject && consentObject . getConsentData && consentObject . getConsentData . gdprApplies ;
236
- if (
237
- ( typeof gdprApplies !== 'boolean' ) ||
238
- ( gdprApplies === true &&
239
- ! ( utils . isStr ( consentObject . getConsentData . consentData ) &&
240
- utils . isPlainObject ( consentObject . getVendorConsents ) &&
241
- Object . keys ( consentObject . getVendorConsents ) . length > 1
289
+ function checkV1Data ( consentObject ) {
290
+ let gdprApplies = consentObject && consentObject . getConsentData && consentObject . getConsentData . gdprApplies ;
291
+ return ! ! (
292
+ ( typeof gdprApplies !== 'boolean' ) ||
293
+ ( gdprApplies === true &&
294
+ ! ( utils . isStr ( consentObject . getConsentData . consentData ) &&
295
+ utils . isPlainObject ( consentObject . getVendorConsents ) &&
296
+ Object . keys ( consentObject . getVendorConsents ) . length > 1
297
+ )
242
298
)
243
- )
244
- ) {
245
- cmpFailed ( `CMP returned unexpected value during lookup process.` , hookConfig , consentObject ) ;
246
- } else {
247
- clearTimeout ( hookConfig . timer ) ;
248
- storeConsentData ( consentObject ) ;
299
+ ) ;
300
+ }
301
+
302
+ function checkV2Data ( ) {
303
+ let gdprApplies = consentObject && consentObject . gdprApplies ;
304
+ let tcString = consentObject && consentObject . tcString ;
305
+ return ! ! (
306
+ ( typeof gdprApplies !== 'boolean' ) ||
307
+ ( gdprApplies === true && ! utils . isStr ( tcString ) )
308
+ ) ;
309
+ }
249
310
250
- exitModule ( null , hookConfig ) ;
311
+ // do extra things for static config
312
+ if ( userCMP === 'static' ) {
313
+ cmpVersion = ( consentObject . getConsentData ) ? 1 : ( consentObject . getTCData ) ? 2 : 0 ;
314
+ // remove extra layer in static v2 data object so it matches normal v2 CMP object for processing step
315
+ if ( cmpVersion === 2 ) {
316
+ consentObject = consentObject . getTCData ;
317
+ }
318
+ }
319
+
320
+ // determine which set of checks to run based on cmpVersion
321
+ let checkFn = ( cmpVersion === 1 ) ? checkV1Data : ( cmpVersion === 2 ) ? checkV2Data : null ;
322
+
323
+ if ( utils . isFn ( checkFn ) ) {
324
+ if ( checkFn ( consentObject ) ) {
325
+ cmpFailed ( `CMP returned unexpected value during lookup process.` , hookConfig , consentObject ) ;
326
+ } else {
327
+ clearTimeout ( hookConfig . timer ) ;
328
+ storeConsentData ( consentObject ) ;
329
+ exitModule ( null , hookConfig ) ;
330
+ }
331
+ } else {
332
+ cmpFailed ( 'Unable to derive CMP version to process data. Consent object does not conform to TCF v1 or v2 specs.' , hookConfig , consentObject ) ;
251
333
}
252
334
}
253
335
@@ -279,17 +361,27 @@ function cmpFailed(errMsg, hookConfig, extraArgs) {
279
361
* @param {object } cmpConsentObject required; an object representing user's consent choices (can be undefined in certain use-cases for this function only)
280
362
*/
281
363
function storeConsentData ( cmpConsentObject ) {
282
- consentData = {
283
- consentString : ( cmpConsentObject ) ? cmpConsentObject . getConsentData . consentData : undefined ,
284
- vendorData : ( cmpConsentObject ) ? cmpConsentObject . getVendorConsents : undefined ,
285
- gdprApplies : ( cmpConsentObject ) ? cmpConsentObject . getConsentData . gdprApplies : undefined
286
- } ;
364
+ if ( cmpVersion === 1 ) {
365
+ consentData = {
366
+ consentString : ( cmpConsentObject ) ? cmpConsentObject . getConsentData . consentData : undefined ,
367
+ vendorData : ( cmpConsentObject ) ? cmpConsentObject . getVendorConsents : undefined ,
368
+ gdprApplies : ( cmpConsentObject ) ? cmpConsentObject . getConsentData . gdprApplies : undefined
369
+ } ;
370
+ } else {
371
+ consentData = {
372
+ consentString : ( cmpConsentObject ) ? cmpConsentObject . tcString : undefined ,
373
+ vendorData : ( cmpConsentObject ) || undefined ,
374
+ gdprApplies : ( cmpConsentObject ) ? cmpConsentObject . gdprApplies : undefined
375
+ } ;
376
+ }
377
+ consentData . apiVersion = cmpVersion ;
378
+
287
379
gdprDataHandler . setConsentData ( consentData ) ;
288
380
}
289
381
290
382
/**
291
383
* This function handles the exit logic for the module.
292
- * There are several paths in the module's logic to call this function and we only allow 1 of the 3 potential exits to happen before suppressing others.
384
+ * While there are several paths in the module's logic to call this function, we only allow 1 of the 3 potential exits to happen before suppressing others.
293
385
*
294
386
* We prevent multiple exits to avoid conflicting messages in the console depending on certain scenarios.
295
387
* One scenario could be auction was canceled due to timeout with CMP being reached.
@@ -336,6 +428,7 @@ function exitModule(errMsg, hookConfig, extraArgs) {
336
428
export function resetConsentData ( ) {
337
429
consentData = undefined ;
338
430
userCMP = undefined ;
431
+ cmpVersion = 0 ;
339
432
gdprDataHandler . setConsentData ( null ) ;
340
433
}
341
434
0 commit comments