From 24e17806cab5915973cddbe9114719e1297db575 Mon Sep 17 00:00:00 2001 From: annavane <101708287+annavane@users.noreply.github.com> Date: Thu, 26 Dec 2024 17:48:14 +0100 Subject: [PATCH] TNC Id Module : user input url validation and optimizations (#12527) * Bug Fixes: modules/tncIdSystem.js - Optimized User ID Recovery: Replaced the existing user ID recovery function with a faster and more efficient method, improving performance. modules/userId/userId.md - Documentation Correction: Resolved inconsistencies in the documentation, ensuring accurate information for module configuration and usage. * - Tests fixed * - TNCID module fix: "getTNCID is not a function" error * modules/tncIdSystem.js - user input URL validations added - TNCID recovered from cookie storage if available - code optimizations for faster TNCID load ________________________________________ test/spec/modules/tncIdSystem_spec.js - added tests for new functions ________________________________________ modules/tncIdSystem.md - updated documentation * - fixed lint errors * - Sales description removed * Looking forward for code approval. --- modules/tncIdSystem.js | 130 +++++++++++++++++++++----- modules/tncIdSystem.md | 32 +++++-- test/spec/modules/tncIdSystem_spec.js | 70 +++++++------- 3 files changed, 167 insertions(+), 65 deletions(-) diff --git a/modules/tncIdSystem.js b/modules/tncIdSystem.js index 9d3187462be..3e1d1e9b926 100644 --- a/modules/tncIdSystem.js +++ b/modules/tncIdSystem.js @@ -1,37 +1,114 @@ +/** + * This module adds TncId to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/tncIdSystem + * @requires module:modules/userId + */ + import { submodule } from '../src/hook.js'; -import { logInfo } from '../src/utils.js'; +import { parseUrl, buildUrl, logInfo, logMessage, logError } from '../src/utils.js'; +import { getStorageManager } from '../src/storageManager.js'; import { loadExternalScript } from '../src/adloader.js'; import { MODULE_TYPE_UID } from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + const MODULE_NAME = 'tncId'; -let url = null; +const TNC_API_URL = 'https://js.tncid.app/remote.js'; +const TNC_DEFAULT_NS = '__tnc'; +const TNC_PREBID_NS = '__tncPbjs'; +const TNC_PREBIDJS_PROVIDER_ID = 'c8549079-f149-4529-a34b-3fa91ef257d1'; +const TNC_LOCAL_VALUE_KEY = 'tncid'; +let moduleConfig = null; + +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); + +function fixURL(config, ns) { + config.params = (config && config.params) ? config.params : {}; + config.params.url = config.params.url || TNC_API_URL; + let url = parseUrl(config.params.url); + url.search = url.search || {}; + let providerId = config.params.publisherId || config.params.providerId || url.search.publisherId || url.search.providerId || TNC_PREBIDJS_PROVIDER_ID; + delete url.search.publisherId; + url.search.providerId = providerId; + url.search.ns = ns; + return url; +} -const waitTNCScript = (tncNS) => { +const loadRemoteScript = function(url) { return new Promise((resolve, reject) => { - var tnc = window[tncNS]; - if (!tnc) reject(new Error('No TNC Object')); - if (tnc.tncid) resolve(tnc.tncid); - tnc.ready(async () => { - let tncid = await tnc.getTNCID('prebid'); - resolve(tncid); - }); + let endpoint = buildUrl(url); + logMessage('TNC Endpoint', endpoint); + loadExternalScript(endpoint, MODULE_TYPE_UID, MODULE_NAME, resolve); }); } -const loadRemoteScript = () => { - return new Promise((resolve) => { - loadExternalScript(url, MODULE_TYPE_UID, MODULE_NAME, resolve); - }) +function TNCObject(ns) { + let tnc = window[ns]; + tnc = typeof tnc !== 'undefined' && tnc !== null && typeof tnc.ready == 'function' ? tnc : { + ready: function(f) { this.ready.q = this.ready.q || []; return typeof f == 'function' ? (this.ready.q.push(f), this) : new Promise(resolve => this.ready.q.push(resolve)); }, + }; + window[ns] = tnc; + return tnc; } -const tncCallback = function (cb) { - let tncNS = '__tnc'; - let promiseArray = []; - if (!window[tncNS]) { - tncNS = '__tncPbjs'; - promiseArray.push(loadRemoteScript()); +function getlocalValue(key) { + let value; + if (storage.hasLocalStorage()) { + value = storage.getDataFromLocalStorage(key); + } + if (!value) { + value = storage.getCookie(key); + } + + if (typeof value === 'string') { + // if it's a json object parse it and return the tncid value, otherwise assume the value is the id + if (value.charAt(0) === '{') { + try { + const obj = JSON.parse(value); + if (obj) { + return obj.tncid; + } + } catch (e) { + logError(e); + } + } else { + return value; + } + } + return null; +} + +const tncCallback = async function(cb) { + try { + let tncNS = TNC_DEFAULT_NS; + let tncid = getlocalValue(TNC_LOCAL_VALUE_KEY); + + if (!window[tncNS] || typeof window[tncNS].ready !== 'function') { + tncNS = TNC_PREBID_NS; // Register a new namespace for TNC global object + let url = fixURL(moduleConfig, tncNS); + if (!url) return cb(); + TNCObject(tncNS); // create minimal TNC object + await loadRemoteScript(url); // load remote script + } + if (!tncid) { + await new Promise(resolve => window[tncNS].ready(resolve)); + tncid = await window[tncNS].getTNCID('prebid'); // working directly with (possibly) overridden TNC Object + logMessage('tncId Module - tncid retrieved from remote script', tncid); + } else { + logMessage('tncId Module - tncid already exists', tncid); + window[tncNS].ready(() => window[tncNS].getTNCID('prebid')); + } + return cb(tncid); + } catch (err) { + logMessage('tncId Module', err); + return cb(); } - return Promise.all(promiseArray).then(() => waitTNCScript(tncNS)).then(cb).catch(() => cb()); } export const tncidSubModule = { @@ -42,6 +119,14 @@ export const tncidSubModule = { }; }, gvlid: 750, + /** + * performs action to obtain id + * Use a tncid cookie first if it is present, otherwise callout to get a new id + * @function + * @param {SubmoduleConfig} [config] Config object with params and storage properties + * @param {ConsentData} [consentData] GDPR consent + * @returns {IdResponse} + */ getId(config, consentData) { const gdpr = (consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies) ? 1 : 0; const consentString = gdpr ? consentData.consentString : ''; @@ -51,10 +136,11 @@ export const tncidSubModule = { return; } - if (config.params && config.params.url) { url = config.params.url; } + moduleConfig = config; return { callback: function (cb) { return tncCallback(cb); } + // callback: tncCallback } }, eids: { diff --git a/modules/tncIdSystem.md b/modules/tncIdSystem.md index b94d03c8e85..b806d545cc4 100644 --- a/modules/tncIdSystem.md +++ b/modules/tncIdSystem.md @@ -1,14 +1,16 @@ -# TNCID UserID Module +# Overview -### Prebid Configuration +Module Name: tncIdSystem + +## Prebid Configuration First, make sure to add the TNCID submodule to your Prebid.js package with: -``` +```bash gulp build --modules=tncIdSystem,userId ``` -### TNCIDIdSystem module Configuration +## TNCIdSystem module Configuration Disclosure: This module loads external script unreviewed by the prebid.js community @@ -20,16 +22,26 @@ pbjs.setConfig({ userIds: [{ name: 'tncId', params: { - url: 'https://js.tncid.app/remote.min.js' //Optional + url: 'TNC-fallback-script-url' // Fallback url, not required if onpage tag is present (ask TNC for it) + }, + storage: { + type: "cookie", + name: "tncid", + expires: 365 // in days } }], syncDelay: 5000 } }); ``` -#### Configuration Params -| Param Name | Required | Type | Description | -| --- | --- | --- | --- | -| name | Required | String | ID value for the TNCID module: `"tncId"` | -| params.url | Optional | String | Provide TNC fallback script URL, this script is loaded if there is no TNC script on page | +## Configuration Params + +The following configuration parameters are available: + +| Param under userSync.userIds[] | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| name | Required | String | The name of this sub-module | `"tncId"` | +| params ||| Details for the sub-module initialization || +| params.url | Optional | String | TNC script fallback URL - This script is loaded if there is no TNC script on page | `"https://js.tncid.app/remote.min.js"` | +| params.publisherId | Optional | String | Publisher ID used in TNC fallback script - As default Prebid specific Publisher ID is used | `"c8549079-f149-4529-a34b-3fa91ef257d1"` | diff --git a/test/spec/modules/tncIdSystem_spec.js b/test/spec/modules/tncIdSystem_spec.js index 4626c940a59..9fbffdddd68 100644 --- a/test/spec/modules/tncIdSystem_spec.js +++ b/test/spec/modules/tncIdSystem_spec.js @@ -1,7 +1,6 @@ import { tncidSubModule } from 'modules/tncIdSystem'; -import {attachIdSystem} from '../../../modules/userId/index.js'; -import {createEidsArray} from '../../../modules/userId/eids.js'; -import {expect} from 'chai/index.mjs'; +import { attachIdSystem } from '../../../modules/userId/index.js'; +import { createEidsArray } from '../../../modules/userId/eids.js'; const consentData = { gdprApplies: true, @@ -40,39 +39,38 @@ describe('TNCID tests', function () { expect(res).to.be.undefined; }); - it('GDPR is OK and page has no TNC script on page, script goes in error, no TNCID is returned', function () { + it('Should NOT give TNCID if there is no TNC script on page and no fallback url in configuration', async function () { const completeCallback = sinon.spy(); const {callback} = tncidSubModule.getId({}, consentData); - return callback(completeCallback).then(() => { - expect(completeCallback.calledOnce).to.be.true; - }) + await callback(completeCallback); + expect(callback).to.be.an('function'); + expect(completeCallback.calledOnceWithExactly()).to.be.true; }); - it('GDPR is OK and page has TNC script with ns: __tnc, present TNCID is returned', function () { - Object.defineProperty(window, '__tnc', { - value: { - ready: (readyFunc) => { readyFunc() }, - tncid: 'TNCID_TEST_ID_1', - providerId: 'TEST_PROVIDER_ID_1', - }, - configurable: true - }); + it('Should NOT give TNCID if fallback script is not loaded correctly', async function () { + const completeCallback = sinon.spy(); + const {callback} = tncidSubModule.getId({ + params: { url: 'www.thenewco.tech' } + }, consentData); + await callback(completeCallback); + expect(completeCallback.calledOnceWithExactly()).to.be.true; + }); + + it(`Should call external script if TNC is not loaded on page`, async function() { const completeCallback = sinon.spy(); - const {callback} = tncidSubModule.getId({}, { gdprApplies: false }); + const {callback} = tncidSubModule.getId({params: {url: 'https://www.thenewco.tech?providerId=test'}}, { gdprApplies: false }); - return callback(completeCallback).then(() => { - expect(completeCallback.calledOnceWithExactly('TNCID_TEST_ID_1')).to.be.true; - }) + await callback(completeCallback); + expect(window).to.contain.property('__tncPbjs'); }); - it('GDPR is OK and page has TNC script with ns: __tnc but not loaded, TNCID is assigned and returned', function () { + it('TNCID is returned if page has TNC script with ns: __tnc', async function () { Object.defineProperty(window, '__tnc', { value: { - ready: async (readyFunc) => { await readyFunc() }, + ready: (readyFunc) => { readyFunc() }, getTNCID: async (name) => { return 'TNCID_TEST_ID_1' }, - providerId: 'TEST_PROVIDER_ID_1', }, configurable: true }); @@ -80,17 +78,23 @@ describe('TNCID tests', function () { const completeCallback = sinon.spy(); const {callback} = tncidSubModule.getId({}, { gdprApplies: false }); - return callback(completeCallback).then(() => { - expect(completeCallback.calledOnceWithExactly('TNCID_TEST_ID_1')).to.be.true; - }) + await callback(completeCallback); + expect(completeCallback.calledOnceWithExactly('TNCID_TEST_ID_1')).to.be.true; }); - it('GDPR is OK and page has TNC script with ns: __tncPbjs, TNCID is returned', function () { + it('TNC script with ns __tncPbjs is created', async function () { + const completeCallback = sinon.spy(); + const {callback} = tncidSubModule.getId({params: {url: 'TEST_URL'}}, consentData); + + await callback(completeCallback); + expect(window).to.contain.property('__tncPbjs'); + }); + + it('TNCID is returned if page has TNC script with ns: __tncPbjs', async function () { Object.defineProperty(window, '__tncPbjs', { value: { - ready: async (readyFunc) => { await readyFunc() }, + ready: (readyFunc) => { readyFunc() }, getTNCID: async (name) => { return 'TNCID_TEST_ID_2' }, - providerId: 'TEST_PROVIDER_ID_1', options: {}, }, configurable: true, @@ -98,13 +102,13 @@ describe('TNCID tests', function () { }); const completeCallback = sinon.spy(); - const {callback} = tncidSubModule.getId({params: {url: 'TEST_URL'}}, consentData); + const {callback} = tncidSubModule.getId({params: {url: 'www.thenewco.tech'}}, consentData); - return callback(completeCallback).then(() => { - expect(completeCallback.calledOnceWithExactly('TNCID_TEST_ID_2')).to.be.true; - }) + await callback(completeCallback); + expect(completeCallback.calledOnceWithExactly('TNCID_TEST_ID_2')).to.be.true; }); }); + describe('eid', () => { before(() => { attachIdSystem(tncidSubModule);