• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

prebid / Prebid.js / #298

29 May 2025 11:21AM UTC coverage: 82.464% (-7.7%) from 90.144%
#298

push

travis-ci

prebidjs-release
Prebid 9.45.0 release

12606 of 17518 branches covered (71.96%)

18622 of 22582 relevant lines covered (82.46%)

157.38 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.35
/modules/tcfControl.js
1
/**
1✔
2
 * This module gives publishers extra set of features to enforce individual purposes of TCF v2
3
 */
4

5
import {deepAccess, logError, logWarn} from '../src/utils.js';
6
import {config} from '../src/config.js';
7
import adapterManager, {gdprDataHandler} from '../src/adapterManager.js';
8
import * as events from '../src/events.js';
9
import {EVENTS} from '../src/constants.js';
10
import {GDPR_GVLIDS, VENDORLESS_GVLID} from '../src/consentHandler.js';
11
import {
12
  MODULE_TYPE_ANALYTICS,
13
  MODULE_TYPE_BIDDER,
14
  MODULE_TYPE_PREBID,
15
  MODULE_TYPE_RTD,
16
  MODULE_TYPE_UID
17
} from '../src/activities/modules.js';
18
import {
19
  ACTIVITY_PARAM_ANL_CONFIG,
20
  ACTIVITY_PARAM_COMPONENT_NAME,
21
  ACTIVITY_PARAM_COMPONENT_TYPE
22
} from '../src/activities/params.js';
23
import {registerActivityControl} from '../src/activities/rules.js';
24
import {
25
  ACTIVITY_ACCESS_DEVICE,
26
  ACTIVITY_ENRICH_EIDS,
27
  ACTIVITY_ENRICH_UFPD,
28
  ACTIVITY_FETCH_BIDS,
29
  ACTIVITY_REPORT_ANALYTICS,
30
  ACTIVITY_SYNC_USER,
31
  ACTIVITY_TRANSMIT_EIDS,
32
  ACTIVITY_TRANSMIT_PRECISE_GEO,
33
  ACTIVITY_TRANSMIT_UFPD
34
} from '../src/activities/activities.js';
35

36
export const STRICT_STORAGE_ENFORCEMENT = 'strictStorageEnforcement';
1✔
37

38
export const ACTIVE_RULES = {
1✔
39
  purpose: {},
40
  feature: {}
41
};
42

43
const CONSENT_PATHS = {
1✔
44
  purpose: false,
45
  feature: 'specialFeatureOptins'
46
};
47

48
const CONFIGURABLE_RULES = {
1✔
49
  storage: {
50
    type: 'purpose',
51
    default: {
52
      purpose: 'storage',
53
      enforcePurpose: true,
54
      enforceVendor: true,
55
      vendorExceptions: []
56
    },
57
    id: 1,
58
  },
59
  basicAds: {
60
    type: 'purpose',
61
    id: 2,
62
    default: {
63
      purpose: 'basicAds',
64
      enforcePurpose: true,
65
      enforceVendor: true,
66
      vendorExceptions: []
67
    }
68
  },
69
  personalizedAds: {
70
    type: 'purpose',
71
    id: 4,
72
  },
73
  measurement: {
74
    type: 'purpose',
75
    id: 7,
76
  },
77
  transmitPreciseGeo: {
78
    type: 'feature',
79
    id: 1,
80
  },
81
};
82

83
const storageBlocked = new Set();
1✔
84
const biddersBlocked = new Set();
1✔
85
const analyticsBlocked = new Set();
1✔
86
const ufpdBlocked = new Set();
1✔
87
const eidsBlocked = new Set();
1✔
88
const geoBlocked = new Set();
1✔
89

90
let hooksAdded = false;
1✔
91
let strictStorageEnforcement = false;
1✔
92

93
const GVLID_LOOKUP_PRIORITY = [
1✔
94
  MODULE_TYPE_BIDDER,
95
  MODULE_TYPE_UID,
96
  MODULE_TYPE_ANALYTICS,
97
  MODULE_TYPE_RTD
98
];
99

100
const RULE_NAME = 'TCF2';
1✔
101
const RULE_HANDLES = [];
1✔
102

103
// in JS we do not have access to the GVL; assume that everyone declares legitimate interest for basic ads
104
const LI_PURPOSES = [2];
1✔
105
const PUBLISHER_LI_PURPOSES = [2, 7, 9, 10];
1✔
106

107
/**
108
 * Retrieve a module's GVL ID.
109
 */
110
export function getGvlid(moduleType, moduleName, fallbackFn) {
111
  if (moduleName) {
74✔
112
    // Check user defined GVL Mapping in pbjs.setConfig()
113
    const gvlMapping = config.getConfig('gvlMapping');
73✔
114

115
    // Return GVL ID from user defined gvlMapping
116
    if (gvlMapping && gvlMapping[moduleName]) {
73✔
117
      return gvlMapping[moduleName];
3✔
118
    } else if (moduleType === MODULE_TYPE_PREBID) {
70✔
119
      return VENDORLESS_GVLID;
2✔
120
    } else {
121
      let {gvlid, modules} = GDPR_GVLIDS.get(moduleName);
68✔
122
      if (gvlid == null && Object.keys(modules).length > 0) {
68✔
123
        // this behavior is for backwards compatibility; if multiple modules with the same
124
        // name declare different GVL IDs, pick the bidder's first, then userId, then analytics
125
        for (const type of GVLID_LOOKUP_PRIORITY) {
4✔
126
          if (modules.hasOwnProperty(type)) {
6✔
127
            gvlid = modules[type];
4✔
128
            if (type !== moduleType) {
4!
129
              logWarn(`Multiple GVL IDs found for module '${moduleName}'; using the ${type} module's ID (${gvlid}) instead of the ${moduleType}'s ID (${modules[moduleType]})`);
4✔
130
            }
131
            break;
4✔
132
          }
133
        }
134
      }
135
      if (gvlid == null && fallbackFn) {
68✔
136
        gvlid = fallbackFn();
1✔
137
      }
138
      return gvlid || null;
68✔
139
    }
140
  }
141
  return null;
1✔
142
}
143

144
/**
145
 * Retrieve GVL IDs that are dynamically set on analytics adapters.
146
 */
147
export function getGvlidFromAnalyticsAdapter(code, config) {
148
  const adapter = adapterManager.getAnalyticsAdapter(code);
6✔
149
  return ((gvlid) => {
6✔
150
    if (typeof gvlid !== 'function') return gvlid;
6✔
151
    try {
2✔
152
      return gvlid.call(adapter.adapter, config);
2✔
153
    } catch (e) {
154
      logError(`Error invoking ${code} adapter.gvlid()`, e);
1✔
155
    }
156
  })(adapter?.adapter?.gvlid);
157
}
158

159
export function shouldEnforce(consentData, purpose, name) {
160
  if (consentData == null && gdprDataHandler.enabled) {
1,073!
161
    // there is no consent data, but the GDPR module has been installed and configured
162
    // NOTE: this check is not foolproof, as when Prebid first loads, enforcement hooks have not been attached yet
163
    // This piece of code would not run at all, and `gdprDataHandler.enabled` would be false, until the first
164
    // `setConfig({consentManagement})`
165
    logWarn(`Attempting operation that requires purpose ${purpose} consent while consent data is not available${name ? ` (module: ${name})` : ''}. Assuming no consent was given.`);
×
166
    return true;
×
167
  }
168
  return consentData && consentData.gdprApplies;
1,073✔
169
}
170

171
function getConsentOrLI(consentData, path, id, acceptLI) {
172
  const data = deepAccess(consentData, `vendorData.${path}`);
508✔
173
  return !!data?.consents?.[id] || (acceptLI && !!data?.legitimateInterests?.[id]);
508✔
174
}
175

176
function getConsent(consentData, type, purposeNo, gvlId) {
177
  let purpose;
178
  if (CONSENT_PATHS[type] !== false) {
255✔
179
    purpose = !!deepAccess(consentData, `vendorData.${CONSENT_PATHS[type]}.${purposeNo}`);
2✔
180
  } else {
181
    const [path, liPurposes] = gvlId === VENDORLESS_GVLID
253✔
182
      ? ['publisher', PUBLISHER_LI_PURPOSES]
183
      : ['purpose', LI_PURPOSES];
184
    purpose = getConsentOrLI(consentData, path, purposeNo, liPurposes.includes(purposeNo));
253✔
185
  }
186
  return {
255✔
187
    purpose,
188
    vendor: getConsentOrLI(consentData, 'vendor', gvlId, LI_PURPOSES.includes(purposeNo))
189
  }
190
}
191

192
/**
193
 * This function takes in a rule and consentData and validates against the consentData provided. Depending on what it returns,
194
 * the caller may decide to suppress a TCF-sensitive activity.
195
 * @param {Object} rule - enforcement rules set in config
196
 * @param {Object} consentData - gdpr consent data
197
 * @param {string=} currentModule - Bidder code of the current module
198
 * @param {number=} gvlId - GVL ID for the module
199
 * @returns {boolean}
200
 */
201
export function validateRules(rule, consentData, currentModule, gvlId) {
202
  const ruleOptions = CONFIGURABLE_RULES[rule.purpose];
108✔
203

204
  // return 'true' if vendor present in 'vendorExceptions'
205
  if ((rule.vendorExceptions || []).includes(currentModule)) {
108✔
206
    return true;
6✔
207
  }
208
  const vendorConsentRequred = rule.enforceVendor && !((gvlId === VENDORLESS_GVLID || (rule.softVendorExceptions || []).includes(currentModule)));
102✔
209
  const {purpose, vendor} = getConsent(consentData, ruleOptions.type, ruleOptions.id, gvlId);
102✔
210
  return (!rule.enforcePurpose || purpose) && (!vendorConsentRequred || vendor);
102✔
211
}
212

213
function gdprRule(purposeNo, checkConsent, blocked = null, gvlidFallback = () => null) {
28!
214
  return function (params) {
9✔
215
    const consentData = gdprDataHandler.getConsentData();
1,073✔
216
    const modName = params[ACTIVITY_PARAM_COMPONENT_NAME];
1,073✔
217

218
    if (shouldEnforce(consentData, purposeNo, modName)) {
1,073✔
219
      const gvlid = getGvlid(params[ACTIVITY_PARAM_COMPONENT_TYPE], modName, gvlidFallback(params));
60✔
220
      let allow = !!checkConsent(consentData, modName, gvlid);
60✔
221
      if (!allow) {
60✔
222
        blocked && blocked.add(modName);
19✔
223
        return {allow};
19✔
224
      }
225
    }
226
  };
227
}
228

229
function singlePurposeGdprRule(purposeNo, blocked = null, gvlidFallback = () => null) {
29!
230
  return gdprRule(purposeNo, (cd, modName, gvlid) => !!validateRules(ACTIVE_RULES.purpose[purposeNo], cd, modName, gvlid), blocked, gvlidFallback);
32✔
231
}
232

233
function exceptPrebidModules(ruleFn) {
234
  return function (params) {
2✔
235
    if (params[ACTIVITY_PARAM_COMPONENT_TYPE] === MODULE_TYPE_PREBID) {
38!
236
      // TODO: this special case is for the PBS adapter (componentType is 'prebid')
237
      // we should check for generic purpose 2 consent & vendor consent based on the PBS vendor's GVL ID;
238
      // that is, however, a breaking change and skipped for now
239
      return;
×
240
    }
241
    return ruleFn(params);
38✔
242
  };
243
}
244

245
export const accessDeviceRule = ((rule) => {
1✔
246
  return function (params) {
1✔
247
    // for vendorless (core) storage, do not enforce rules unless strictStorageEnforcement is set
248
    if (params[ACTIVITY_PARAM_COMPONENT_TYPE] === MODULE_TYPE_PREBID && !strictStorageEnforcement) return;
1,078✔
249
    return rule(params);
1,018✔
250
  };
251
})(singlePurposeGdprRule(1, storageBlocked));
252

253
export const syncUserRule = singlePurposeGdprRule(1, storageBlocked);
1✔
254
export const enrichEidsRule = singlePurposeGdprRule(1, storageBlocked);
1✔
255
export const fetchBidsRule = exceptPrebidModules(singlePurposeGdprRule(2, biddersBlocked));
1✔
256
export const reportAnalyticsRule = singlePurposeGdprRule(7, analyticsBlocked, (params) => getGvlidFromAnalyticsAdapter(params[ACTIVITY_PARAM_COMPONENT_NAME], params[ACTIVITY_PARAM_ANL_CONFIG]));
3✔
257
export const ufpdRule = singlePurposeGdprRule(4, ufpdBlocked);
1✔
258

259
export const transmitEidsRule = exceptPrebidModules((() => {
1✔
260
  // Transmit EID special case:
261
  // by default, legal basis or vendor exceptions for any purpose between 2 and 10
262
  // (but disregarding enforcePurpose and enforceVendor config) is enough to allow EIDs through
263
  function check2to10Consent(consentData, modName, gvlId) {
264
    for (let pno = 2; pno <= 10; pno++) {
26✔
265
      if (ACTIVE_RULES.purpose[pno]?.vendorExceptions?.includes(modName)) {
156✔
266
        return true;
3✔
267
      }
268
      const {purpose, vendor} = getConsent(consentData, 'purpose', pno, gvlId);
153✔
269
      if (purpose && (vendor || ACTIVE_RULES.purpose[pno]?.softVendorExceptions?.includes(modName))) {
153✔
270
        return true;
13✔
271
      }
272
    }
273
    return false;
10✔
274
  }
275

276
  const defaultBehavior = gdprRule('2-10', check2to10Consent, eidsBlocked);
1✔
277
  const p4Behavior = singlePurposeGdprRule(4, eidsBlocked);
1✔
278
  return function () {
1✔
279
    const fn = ACTIVE_RULES.purpose[4]?.eidsRequireP4Consent ? p4Behavior : defaultBehavior;
33✔
280
    return fn.apply(this, arguments);
33✔
281
  };
282
})());
283

284
export const transmitPreciseGeoRule = gdprRule('Special Feature 1', (cd, modName, gvlId) => validateRules(ACTIVE_RULES.feature[1], cd, modName, gvlId), geoBlocked);
2✔
285

286
/**
287
 * Compiles the TCF2.0 enforcement results into an object, which is emitted as an event payload to "tcf2Enforcement" event.
288
 */
289
function emitTCF2FinalResults() {
290
  // remove null and duplicate values
291
  const formatSet = function (st) {
69✔
292
    return Array.from(st.keys()).filter(el => el != null);
414✔
293
  };
294
  const tcf2FinalResults = {
69✔
295
    storageBlocked: formatSet(storageBlocked),
296
    biddersBlocked: formatSet(biddersBlocked),
297
    analyticsBlocked: formatSet(analyticsBlocked),
298
    ufpdBlocked: formatSet(ufpdBlocked),
299
    eidsBlocked: formatSet(eidsBlocked),
300
    geoBlocked: formatSet(geoBlocked)
301
  };
302

303
  events.emit(EVENTS.TCF2_ENFORCEMENT, tcf2FinalResults);
69✔
304
  [storageBlocked, biddersBlocked, analyticsBlocked, ufpdBlocked, eidsBlocked, geoBlocked].forEach(el => el.clear());
414✔
305
}
306

307
events.on(EVENTS.AUCTION_END, emitTCF2FinalResults);
1✔
308

309
/**
310
 * A configuration function that initializes some module variables, as well as adds hooks
311
 * @param {Object} config - GDPR enforcement config object
312
 */
313
export function setEnforcementConfig(config) {
314
  let rules = deepAccess(config, 'gdpr.rules');
65✔
315
  if (!rules) {
65✔
316
    logWarn('TCF2: enforcing P1 and P2 by default');
29✔
317
  }
318
  rules = Object.fromEntries((rules || []).map(r => [r.purpose, r]));
65✔
319
  strictStorageEnforcement = !!deepAccess(config, STRICT_STORAGE_ENFORCEMENT);
65✔
320

321
  Object.entries(CONFIGURABLE_RULES).forEach(([name, opts]) => {
65✔
322
    ACTIVE_RULES[opts.type][opts.id] = rules[name] ?? opts.default;
325✔
323
  });
324

325
  if (!hooksAdded) {
65✔
326
    if (ACTIVE_RULES.purpose[1] != null) {
1!
327
      hooksAdded = true;
1✔
328
      RULE_HANDLES.push(registerActivityControl(ACTIVITY_ACCESS_DEVICE, RULE_NAME, accessDeviceRule));
1✔
329
      RULE_HANDLES.push(registerActivityControl(ACTIVITY_SYNC_USER, RULE_NAME, syncUserRule));
1✔
330
      RULE_HANDLES.push(registerActivityControl(ACTIVITY_ENRICH_EIDS, RULE_NAME, enrichEidsRule));
1✔
331
    }
332
    if (ACTIVE_RULES.purpose[2] != null) {
1!
333
      RULE_HANDLES.push(registerActivityControl(ACTIVITY_FETCH_BIDS, RULE_NAME, fetchBidsRule));
1✔
334
    }
335
    if (ACTIVE_RULES.purpose[4] != null) {
1!
336
      RULE_HANDLES.push(
×
337
        registerActivityControl(ACTIVITY_TRANSMIT_UFPD, RULE_NAME, ufpdRule),
338
        registerActivityControl(ACTIVITY_ENRICH_UFPD, RULE_NAME, ufpdRule)
339
      );
340
    }
341
    if (ACTIVE_RULES.purpose[7] != null) {
1!
342
      RULE_HANDLES.push(registerActivityControl(ACTIVITY_REPORT_ANALYTICS, RULE_NAME, reportAnalyticsRule));
×
343
    }
344
    if (ACTIVE_RULES.feature[1] != null) {
1!
345
      RULE_HANDLES.push(registerActivityControl(ACTIVITY_TRANSMIT_PRECISE_GEO, RULE_NAME, transmitPreciseGeoRule));
×
346
    }
347
    RULE_HANDLES.push(registerActivityControl(ACTIVITY_TRANSMIT_EIDS, RULE_NAME, transmitEidsRule));
1✔
348
  }
349
}
350

351
export function uninstall() {
352
  while (RULE_HANDLES.length) RULE_HANDLES.pop()();
5✔
353
  hooksAdded = false;
2✔
354
}
355

356
config.getConfig('consentManagement', config => setEnforcementConfig(config.consentManagement));
10✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc