UNPKG

@iabtechlabtcf/core

Version:

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

281 lines (280 loc) 10.3 kB
import { PurposeRestriction } from './PurposeRestriction.js'; import { RestrictionType } from './RestrictionType.js'; import { Cloneable } from '../Cloneable.js'; export class PurposeRestrictionVector extends Cloneable { /** * if this originatd from an encoded string we'll need a place to store the * bit length; it can be set and got from here */ bitLength = 0; /** * a map indexed by a string which will be a 'hash' of the purpose and * restriction type. */ map = new Map(); gvl_; has(hash) { return this.map.has(hash); } isOkToHave(restrictionType, purposeId, vendorId) { let result = true; /** * without a gvl set, there's no way to know... in that case we'll return * true but once the GVL is set later we'll go through these and clean up * the mess. */ if (this.gvl?.vendors) { const vendor = this.gvl.vendors[vendorId]; if (vendor) { if (restrictionType === RestrictionType.NOT_ALLOWED) { /** * if it's "not allowed" then flexible declaration is ignored but if * if it isn't even listed as one of the purposes the vendor uses, * then there is no reason to encode the value so check both arrays * to see if it exists. If it does then we can restrict it. */ result = (vendor.legIntPurposes.includes(purposeId) || vendor.purposes.includes(purposeId)); } else if (vendor.flexiblePurposes.length) { switch (restrictionType) { /** * If the vendor has the purposeId in flexiblePurposes and it is * listed as a legitimate interest purpose we can set the * override to require consent. */ case RestrictionType.REQUIRE_CONSENT: result = (vendor.flexiblePurposes.includes(purposeId) && vendor.legIntPurposes.includes(purposeId)); break; /** * If the vendor has the purposeId in flexiblePurposes and it is * listed as a consent purpose we can set the * override to require legitimate interest. */ case RestrictionType.REQUIRE_LI: result = (vendor.flexiblePurposes.includes(purposeId) && vendor.purposes.includes(purposeId)); break; } } else { result = false; } } else { // this vendor doesn't exist result = false; } } // if the gvl isn't defined, we can't do anything until later return result; } /** * add - adds a given Vendor ID under a given Purpose Restriction * * @param {number} vendorId * @param {PurposeRestriction} purposeRestriction * @return {void} */ add(vendorId, purposeRestriction) { if (this.isOkToHave(purposeRestriction.restrictionType, purposeRestriction.purposeId, vendorId)) { const hash = purposeRestriction.hash; if (!this.has(hash)) { this.map.set(hash, new Set()); this.bitLength = 0; } /** * Previously I had a check here to remove a duplicate value, but because * we're using a tree the value is guaranteed to be unique so there is no * need to add an additional de-duplication here. */ this.map.get(hash).add(vendorId); } } /** * restrictPurposeToLegalBasis - adds all Vendors under a given Purpose Restriction * * @param {PurposeRestriction} purposeRestriction * @return {void} */ restrictPurposeToLegalBasis(purposeRestriction) { const vendors = Array.from(this.gvl.vendorIds); const hash = purposeRestriction.hash; const lastEntry = vendors[vendors.length - 1]; /** * Create an ordered array of vendor IDs from `1` (the minimum value for Vendor ID) to `lastEntry` */ const values = [...Array(lastEntry).keys()].map((i) => i + 1); if (!this.has(hash)) { this.map.set(hash, new Set(values)); // use static method `build` to create a `BST` from the ordered array of IDs this.bitLength = 0; } else { for (let i = 1; i <= lastEntry; i++) { /** * Previously I had a check here to remove a duplicate value, but because * we're using a tree the value is guaranteed to be unique so there is no * need to add an additional de-duplication here. */ this.map.get(hash).add(i); } } } /** * getVendors - returns array of vendor ids optionally narrowed by a given * Purpose Restriction. If no purpose restriction is passed then all vendor * ids will be returned. One can expect this result to be a unique set of * ids no duplicates. * * @param {PurposeRestriction} [purposeRestriction] - optionally passed to * get only Vendor IDs restricted under the given Purpose Restriction * @return {number[]} - Unique ID set of vendors */ getVendors(purposeRestriction) { let vendorIds = []; if (purposeRestriction) { const hash = purposeRestriction.hash; if (this.has(hash)) { vendorIds = Array.from(this.map.get(hash)); } } else { const vendorSet = new Set(); this.map.forEach((set) => { set.forEach((vendorId) => { vendorSet.add(vendorId); }); }); vendorIds = Array.from(vendorSet); } return vendorIds.sort((a, b) => a - b); } getRestrictionType(vendorId, purposeId) { let rType; this.getRestrictions(vendorId).forEach((purposeRestriction) => { if (purposeRestriction.purposeId === purposeId) { if (rType === undefined || rType > purposeRestriction.restrictionType) { rType = purposeRestriction.restrictionType; } } }); return rType; } /** * vendorHasRestriction - determines whether a given Vendor ID is under a * given Purpose Restriction * * @param {number} vendorId * @param {PurposeRestriction} purposeRestriction * @return {boolean} - true if the give Vendor ID is under the given Purpose * Restriction */ vendorHasRestriction(vendorId, purposeRestriction) { let has = false; const restrictions = this.getRestrictions(vendorId); for (let i = 0; i < restrictions.length && !has; i++) { has = purposeRestriction.isSameAs(restrictions[i]); } return has; } /** * getMaxVendorId - gets the Maximum Vendor ID regardless of Purpose * Restriction * * @return {number} - maximum Vendor ID */ getMaxVendorId() { let retr = 0; this.map.forEach((set) => { retr = Math.max(Array.from(set)[set.size - 1], retr); }); return retr; } getRestrictions(vendorId) { const retr = []; this.map.forEach((set, hash) => { if (vendorId) { if (set.has(vendorId)) { retr.push(PurposeRestriction.unHash(hash)); } } else { retr.push(PurposeRestriction.unHash(hash)); } }); return retr; } getPurposes() { const purposeIds = new Set(); this.map.forEach((set, hash) => { purposeIds.add(PurposeRestriction.unHash(hash).purposeId); }); return Array.from(purposeIds); } /** * remove - removes Vendor ID from a Purpose Restriction * * @param {number} vendorId * @param {PurposeRestriction} purposeRestriction * @return {void} */ remove(vendorId, purposeRestriction) { const hash = purposeRestriction.hash; const set = this.map.get(hash); if (set) { set.delete(vendorId); // if it's empty let's delete the key so it doesn't show up empty if (set.size == 0) { this.map.delete(hash); this.bitLength = 0; } } } /** * Essential for being able to determine whether we can actually set a * purpose restriction since they have to have a flexible legal basis * * @param {GVL} value - the GVL instance */ set gvl(value) { if (!this.gvl_) { this.gvl_ = value; /** * if we have restrictions set before the gvl is set then we'll have to * go through and remove some if they're not valid */ this.map.forEach((set, hash) => { const purposeRestriction = PurposeRestriction.unHash(hash); const vendors = Array.from(set); vendors.forEach((vendorId) => { if (!this.isOkToHave(purposeRestriction.restrictionType, purposeRestriction.purposeId, vendorId)) { set.delete(vendorId); } }); }); } } /** * gvl returns local copy of the GVL these restrictions apply to * * @return {GVL} */ get gvl() { return this.gvl_; } /** * isEmpty - whether or not this vector has any restrictions in it * * @return {boolean} */ isEmpty() { return this.map.size === 0; } ; /** * numRestrictions - returns the number of Purpose Restrictions. * * @return {number} */ get numRestrictions() { return this.map.size; } }