UNPKG

@iabtcf/core

Version:

Ensures consistent encoding and decoding of TC Signals for the iab. Transparency and Consent Framework (TCF).

585 lines (584 loc) 22.8 kB
import { Cloneable } from './Cloneable.js'; import { GVLError } from './errors/index.js'; import { Json } from './Json.js'; import { ConsentLanguages } from './model/index.js'; /** * class with utilities for managing the global vendor list. Will use JSON to * fetch the vendor list from specified url and will serialize it into this * object and provide accessors. Provides ways to group vendors on the list by * purpose and feature. */ export class GVL extends Cloneable { static LANGUAGE_CACHE = new Map(); static CACHE = new Map(); static LATEST_CACHE_KEY = 0; static DEFAULT_LANGUAGE = 'EN'; /** * Set of available consent languages published by the IAB */ static consentLanguages = new ConsentLanguages(); static baseUrl_; /** * baseUrl - Entities using the vendor-list.json are required by the iab to * host their own copy of it to reduce the load on the iab's infrastructure * so a 'base' url must be set to be put together with the versioning scheme * of the filenames. * * @static * @param {string} url - the base url to load the vendor-list.json from. This is * broken out from the filename because it follows a different scheme for * latest file vs versioned files. * * @throws {GVLError} - If the url is http[s]://vendorlist.consensu.org/... * this will throw an error. IAB Europe requires that that CMPs and Vendors * cache their own copies of the GVL to minimize load on their * infrastructure. For more information regarding caching of the * vendor-list.json, please see [the TCF documentation on 'Caching the Global * Vendor List' * ](https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/TCFv2/IAB%20Tech%20Lab%20-%20Consent%20string%20and%20vendor%20list%20formats%20v2.md#caching-the-global-vendor-list) */ static set baseUrl(url) { const notValid = /^https?:\/\/vendorlist\.consensu\.org\//; if (notValid.test(url)) { throw new GVLError('Invalid baseUrl! You may not pull directly from vendorlist.consensu.org and must provide your own cache'); } // if a trailing slash was forgotten if (url.length > 0 && url[url.length - 1] !== '/') { url += '/'; } this.baseUrl_ = url; } ; /** * baseUrl - Entities using the vendor-list.json are required by the iab to * host their own copy of it to reduce the load on the iab's infrastructure * so a 'base' url must be set to be put together with the versioning scheme * of the filenames. * * @static * @return {string} - returns the previously set baseUrl, the default is * `undefined` */ static get baseUrl() { return this.baseUrl_; } /** * @static * @param {string} - the latest is assumed to be vendor-list.json because * that is what the iab uses, but it could be different... if you want */ static latestFilename = 'vendor-list.json'; /** * @static * @param {string} - the versioned name is assumed to be * vendor-list-v[VERSION].json where [VERSION] will be replaced with the * specified version. But it could be different... if you want just make * sure to include the [VERSION] macro if you have a numbering scheme, it's a * simple string substitution. * * eg. * ```javascript * GVL.baseUrl = "http://www.mydomain.com/iabcmp/"; * GVL.versionedFilename = "vendorlist?getVersion=[VERSION]"; * ``` */ static versionedFilename = 'archives/vendor-list-v[VERSION].json'; /** * @param {string} - Translations of the names and descriptions for Purposes, * Special Purposes, Features, and Special Features to non-English languages * are contained in a file where attributes containing English content * (except vendor declaration information) are translated. The iab publishes * one following the scheme below where the LANG is the iso639-1 language * code. For a list of available translations * [please go here](https://register.consensu.org/Translation). * * eg. * ```javascript * GVL.baseUrl = "http://www.mydomain.com/iabcmp/"; * GVL.languageFilename = "purposes?getPurposes=[LANG]"; * ``` */ static languageFilename = 'purposes-[LANG].json'; /** * @param {Promise} resolved when this GVL object is populated with the data * or rejected if there is an error. */ readyPromise; /** * @param {number} gvlSpecificationVersion - schema version for the GVL that is used */ gvlSpecificationVersion; /** * @param {number} incremented with each published file change */ vendorListVersion; /** * @param {number} tcfPolicyVersion - The TCF MO will increment this value * whenever a GVL change (such as adding a new Purpose or Feature or a change * in Purpose wording) legally invalidates existing TC Strings and requires * CMPs to re-establish transparency and consent from users. If the policy * version number in the latest GVL is different from the value in your TC * String, then you need to re-establish transparency and consent for that * user. A version 1 format TC String is considered to have a version value * of 1. */ tcfPolicyVersion; /** * @param {string | Date} lastUpdated - the date in which the vendor list * json file was last updated. */ lastUpdated; /** * @param {IntMap<Purpose>} a collection of [[Purpose]]s */ purposes; /** * @param {IntMap<Purpose>} a collection of [[Purpose]]s */ specialPurposes; /** * @param {IntMap<Feature>} a collection of [[Feature]]s */ features; /** * @param {IntMap<Feature>} a collection of [[Feature]]s */ specialFeatures; /** * @param {boolean} internal reference of when the GVL is ready to be used */ isReady_ = false; /** * @param {IntMap<Vendor>} a collection of [[Vendor]]s */ vendors_; vendorIds; /** * @param {IntMap<Vendor>} a collection of [[Vendor]]. Used as a backup if a whitelist is sets */ fullVendorList; /** * @param {ByPurposeVendorMap} vendors by purpose */ byPurposeVendorMap; /** * @param {IDSetMap} vendors by special purpose */ bySpecialPurposeVendorMap; /** * @param {IDSetMap} vendors by feature */ byFeatureVendorMap; /** * @param {IDSetMap} vendors by special feature */ bySpecialFeatureVendorMap; /** * @param {IntMap<Stack>} a collection of [[Stack]]s */ stacks; lang_; isLatest = false; /** * @param {VersionOrVendorList} [versionOrVendorList] - can be either a * [[VendorList]] object or a version number represented as a string or * number to download. If nothing is passed the latest version of the GVL * will be loaded */ constructor(versionOrVendorList) { super(); /** * should have been configured before and instance was created and will * persist through the app */ let url = GVL.baseUrl; this.lang_ = GVL.DEFAULT_LANGUAGE; if (this.isVendorList(versionOrVendorList)) { this.populate(versionOrVendorList); this.readyPromise = Promise.resolve(); } else { if (!url) { throw new GVLError('must specify GVL.baseUrl before loading GVL json'); } if (versionOrVendorList > 0) { const version = versionOrVendorList; if (GVL.CACHE.has(version)) { this.populate(GVL.CACHE.get(version)); this.readyPromise = Promise.resolve(); } else { // load version specified url += GVL.versionedFilename.replace('[VERSION]', String(version)); this.readyPromise = this.fetchJson(url); } } else { /** * whatever it is (or isn't)... it doesn't matter we'll just get the * latest. In this case we may have cached the latest version at key 0. * If we have then we'll just use that instead of making a request. * Otherwise we'll have to load it (and then we'll cache it for next * time) */ if (GVL.CACHE.has(GVL.LATEST_CACHE_KEY)) { this.populate(GVL.CACHE.get(GVL.LATEST_CACHE_KEY)); this.readyPromise = Promise.resolve(); } else { this.isLatest = true; this.readyPromise = this.fetchJson(url + GVL.latestFilename); } } } } /** * emptyLanguageCache * * @param {string} [lang] - Optional ISO 639-1 langauge code to remove from * the cache. Should be one of the languages in GVL.consentLanguages set. * If not then the whole cache will be deleted. * @return {boolean} - true if anything was deleted from the cache */ static emptyLanguageCache(lang) { let retr = false; if (lang === undefined && GVL.LANGUAGE_CACHE.size > 0) { GVL.LANGUAGE_CACHE = new Map(); retr = true; } else if (typeof lang === 'string' && this.consentLanguages.has(lang.toUpperCase())) { GVL.LANGUAGE_CACHE.delete(lang.toUpperCase()); retr = true; } return retr; } /** * emptyCache * * @param {number} [vendorListVersion] - version of the vendor list to delete * from the cache. If none is specified then the whole cache is deleted. * @return {boolean} - true if anything was deleted from the cache */ static emptyCache(vendorListVersion) { let retr = false; if (Number.isInteger(vendorListVersion) && vendorListVersion >= 0) { GVL.CACHE.delete(vendorListVersion); retr = true; } else if (vendorListVersion === undefined) { GVL.CACHE = new Map(); retr = true; } return retr; } cacheLanguage() { if (!GVL.LANGUAGE_CACHE.has(this.lang_)) { GVL.LANGUAGE_CACHE.set(this.lang_, { purposes: this.purposes, specialPurposes: this.specialPurposes, features: this.features, specialFeatures: this.specialFeatures, stacks: this.stacks, }); } } async fetchJson(url) { try { this.populate(await Json.fetch(url)); } catch (err) { throw new GVLError(err.message); } } /** * getJson - Method for getting the JSON that was downloaded to created this * `GVL` object * * @return {VendorList} - The basic JSON structure without the extra * functionality and methods of this class. */ getJson() { return JSON.parse(JSON.stringify({ gvlSpecificationVersion: this.gvlSpecificationVersion, vendorListVersion: this.vendorListVersion, tcfPolicyVersion: this.tcfPolicyVersion, lastUpdated: this.lastUpdated, purposes: this.purposes, specialPurposes: this.specialPurposes, features: this.features, specialFeatures: this.specialFeatures, stacks: this.stacks, vendors: this.fullVendorList, })); } /** * changeLanguage - retrieves the purpose language translation and sets the * internal language variable * * @param {string} lang - ISO 639-1 langauge code to change language to * @return {Promise<void | GVLError>} - returns the `readyPromise` and * resolves when this GVL is populated with the data from the language file. */ async changeLanguage(lang) { const langUpper = lang.toUpperCase(); if (GVL.consentLanguages.has(langUpper)) { if (langUpper !== this.lang_) { this.lang_ = langUpper; if (GVL.LANGUAGE_CACHE.has(langUpper)) { const cached = GVL.LANGUAGE_CACHE.get(langUpper); for (const prop in cached) { if (cached.hasOwnProperty(prop)) { this[prop] = cached[prop]; } } } else { // load Language specified const url = GVL.baseUrl + GVL.languageFilename.replace('[LANG]', lang); try { await this.fetchJson(url); this.cacheLanguage(); } catch (err) { throw new GVLError('unable to load language: ' + err.message); } } } } else { throw new GVLError(`unsupported language ${lang}`); } } get language() { return this.lang_; } isVendorList(gvlObject) { return gvlObject !== undefined && gvlObject.vendors !== undefined; } populate(gvlObject) { /** * these are populated regardless of whether it's a Declarations file or * a VendorList */ this.purposes = gvlObject.purposes; this.specialPurposes = gvlObject.specialPurposes; this.features = gvlObject.features; this.specialFeatures = gvlObject.specialFeatures; this.stacks = gvlObject.stacks; if (this.isVendorList(gvlObject)) { this.gvlSpecificationVersion = gvlObject.gvlSpecificationVersion; this.tcfPolicyVersion = gvlObject.tcfPolicyVersion; this.vendorListVersion = gvlObject.vendorListVersion; this.lastUpdated = gvlObject.lastUpdated; if (typeof this.lastUpdated === 'string') { this.lastUpdated = new Date(this.lastUpdated); } this.vendors_ = gvlObject.vendors; this.fullVendorList = gvlObject.vendors; this.mapVendors(); this.isReady_ = true; if (this.isLatest) { /** * If the "LATEST" was requested then this flag will be set to true. * In that case we'll cache the GVL at the special key */ GVL.CACHE.set(GVL.LATEST_CACHE_KEY, this.getJson()); } /** * Whether or not it's the "LATEST" we'll cache the gvl at the version it * is declared to be (if it's not already). to avoid downloading it again * in the future. */ if (!GVL.CACHE.has(this.vendorListVersion)) { GVL.CACHE.set(this.vendorListVersion, this.getJson()); } } this.cacheLanguage(); } mapVendors(vendorIds) { // create new instances of the maps this.byPurposeVendorMap = {}; this.bySpecialPurposeVendorMap = {}; this.byFeatureVendorMap = {}; this.bySpecialFeatureVendorMap = {}; // initializes data structure for purpose map Object.keys(this.purposes).forEach((purposeId) => { this.byPurposeVendorMap[purposeId] = { legInt: new Set(), consent: new Set(), flexible: new Set(), }; }); // initializes data structure for special purpose map Object.keys(this.specialPurposes).forEach((purposeId) => { this.bySpecialPurposeVendorMap[purposeId] = new Set(); }); // initializes data structure for feature map Object.keys(this.features).forEach((featureId) => { this.byFeatureVendorMap[featureId] = new Set(); }); // initializes data structure for feature map Object.keys(this.specialFeatures).forEach((featureId) => { this.bySpecialFeatureVendorMap[featureId] = new Set(); }); if (!Array.isArray(vendorIds)) { vendorIds = Object.keys(this.fullVendorList).map((vId) => +vId); } this.vendorIds = new Set(vendorIds); // assigns vendor ids to their respective maps this.vendors_ = vendorIds.reduce((vendors, vendorId) => { const vendor = this.vendors_[String(vendorId)]; if (vendor && vendor.deletedDate === undefined) { vendor.purposes.forEach((purposeId) => { const purpGroup = this.byPurposeVendorMap[String(purposeId)]; purpGroup.consent.add(vendorId); }); vendor.specialPurposes.forEach((purposeId) => { this.bySpecialPurposeVendorMap[String(purposeId)].add(vendorId); }); vendor.legIntPurposes.forEach((purposeId) => { this.byPurposeVendorMap[String(purposeId)].legInt.add(vendorId); }); // could not be there if (vendor.flexiblePurposes) { vendor.flexiblePurposes.forEach((purposeId) => { this.byPurposeVendorMap[String(purposeId)].flexible.add(vendorId); }); } vendor.features.forEach((featureId) => { this.byFeatureVendorMap[String(featureId)].add(vendorId); }); vendor.specialFeatures.forEach((featureId) => { this.bySpecialFeatureVendorMap[String(featureId)].add(vendorId); }); vendors[vendorId] = vendor; } return vendors; }, {}); } getFilteredVendors(purposeOrFeature, id, subType, special) { const properPurposeOrFeature = purposeOrFeature.charAt(0).toUpperCase() + purposeOrFeature.slice(1); let vendorSet; const retr = {}; if (purposeOrFeature === 'purpose' && subType) { vendorSet = this['by' + properPurposeOrFeature + 'VendorMap'][String(id)][subType]; } else { vendorSet = this['by' + (special ? 'Special' : '') + properPurposeOrFeature + 'VendorMap'][String(id)]; } vendorSet.forEach((vendorId) => { retr[String(vendorId)] = this.vendors[String(vendorId)]; }); return retr; } /** * getVendorsWithConsentPurpose * * @param {number} purposeId * @return {IntMap<Vendor>} - list of vendors that have declared the consent purpose id */ getVendorsWithConsentPurpose(purposeId) { return this.getFilteredVendors('purpose', purposeId, 'consent'); } /** * getVendorsWithLegIntPurpose * * @param {number} purposeId * @return {IntMap<Vendor>} - list of vendors that have declared the legInt (Legitimate Interest) purpose id */ getVendorsWithLegIntPurpose(purposeId) { return this.getFilteredVendors('purpose', purposeId, 'legInt'); } /** * getVendorsWithFlexiblePurpose * * @param {number} purposeId * @return {IntMap<Vendor>} - list of vendors that have declared the flexible purpose id */ getVendorsWithFlexiblePurpose(purposeId) { return this.getFilteredVendors('purpose', purposeId, 'flexible'); } /** * getVendorsWithSpecialPurpose * * @param {number} specialPurposeId * @return {IntMap<Vendor>} - list of vendors that have declared the special purpose id */ getVendorsWithSpecialPurpose(specialPurposeId) { return this.getFilteredVendors('purpose', specialPurposeId, undefined, true); } /** * getVendorsWithFeature * * @param {number} featureId * @return {IntMap<Vendor>} - list of vendors that have declared the feature id */ getVendorsWithFeature(featureId) { return this.getFilteredVendors('feature', featureId); } /** * getVendorsWithSpecialFeature * * @param {number} specialFeatureId * @return {IntMap<Vendor>} - list of vendors that have declared the special feature id */ getVendorsWithSpecialFeature(specialFeatureId) { return this.getFilteredVendors('feature', specialFeatureId, undefined, true); } /** * vendors * * @return {IntMap<Vendor>} - the list of vendors as it would on the JSON file * except if `narrowVendorsTo` was called, it would be that narrowed list */ get vendors() { return this.vendors_; } /** * narrowVendorsTo - narrows vendors represented in this GVL to the list of ids passed in * * @param {number[]} vendorIds - list of ids to narrow this GVL to * @return {void} */ narrowVendorsTo(vendorIds) { this.mapVendors(vendorIds); } /** * isReady - Whether or not this instance is ready to be used. This will be * immediately and synchronously true if a vendorlist object is passed into * the constructor or once the JSON vendorllist is retrieved. * * @return {boolean} whether or not the instance is ready to be interacted * with and all the data is populated */ get isReady() { return this.isReady_; } /** * clone - overrides base `clone()` method since GVL is a special class that * represents a JSON structure with some additional functionality. * * @return {GVL} */ clone() { const result = new GVL(this.getJson()); /* * If the current language of the GVL is not the default language, we set the language of * the clone to the current language since a new GVL is always created with the default * language. */ if (this.lang_ !== GVL.DEFAULT_LANGUAGE) { /* * Since the GVL language was changed, this means that an asynchronous changeLanguage * call was made prior to cloning the GVL. The new language specified has been cached * by the GVL and this changeLanguage call made as a part of cloning the GVL will be * synchronous. The code will look for the language definitions in the cache instead * of creating a http request. */ result.changeLanguage(this.lang_); } return result; } static isInstanceOf(questionableInstance) { const isSo = typeof questionableInstance === 'object'; return (isSo && typeof questionableInstance.narrowVendorsTo === 'function'); } }