From 6f2d990f20bec98e358869c6a0cd40e67b29d399 Mon Sep 17 00:00:00 2001 From: "yevhen.tykhonov" Date: Thu, 3 Jul 2025 19:01:35 +0300 Subject: [PATCH 1/2] Add humansecurityMalvDefenseRTDProvider as near 1:1 copy of cleanioRTDProvider --- modules/cleanioRtdProvider.md | 6 + .../humansecurityMalvDefenseRtdProvider.js | 224 ++++++++++++++++++ .../humansecurityMalvDefenseRtdProvider.md | 63 +++++ src/adloader.js | 1 + ...umansecurityMalvDefenseRtdProvider_spec.js | 211 +++++++++++++++++ 5 files changed, 505 insertions(+) create mode 100644 modules/humansecurityMalvDefenseRtdProvider.js create mode 100644 modules/humansecurityMalvDefenseRtdProvider.md create mode 100644 test/spec/modules/humansecurityMalvDefenseRtdProvider_spec.js diff --git a/modules/cleanioRtdProvider.md b/modules/cleanioRtdProvider.md index 7870a2719b6..f69b6d01e23 100644 --- a/modules/cleanioRtdProvider.md +++ b/modules/cleanioRtdProvider.md @@ -5,6 +5,12 @@ Module Name: clean.io Rtd provider Module Type: Rtd Provider Maintainer: nick@clean.io ``` +> **Warning!** +> +> The **cleanioRtdProvider** module has been renamed to [humansecurityMalvDefenseRtdProvider](humansecurityMalvDefenseRtdProvider.md) following HUMAN Security's acquisition of the Clean.io project in 2022. +> **cleanioRtdProvider** module is maintained for backward compatibility until the next major Prebid release. +> +> Please use humansecurityMalvDefenseRtdProvider instead of cleanioRtdProvider in your Prebid integration. The clean.io Realtime module provides effective anti-malvertising solution for publishers, including, but not limited to, blocking unwanted 0- and 1-click redirects, deceptive ads or those with malicious landing pages, and various types of affiliate fraud. diff --git a/modules/humansecurityMalvDefenseRtdProvider.js b/modules/humansecurityMalvDefenseRtdProvider.js new file mode 100644 index 00000000000..676ea2879a3 --- /dev/null +++ b/modules/humansecurityMalvDefenseRtdProvider.js @@ -0,0 +1,224 @@ +/** + * This module adds humansecurityMalvDefense provider to the real time data module + * The {@link module:modules/realTimeData} module is required + * The module will wrap bid responses markup in humansecurityMalvDefense agent script for protection + * @module modules/humansecurityMalvDefenseRtdProvider + * @requires module:modules/realTimeData + */ + +import { submodule } from '../src/hook.js'; +import { loadExternalScript } from '../src/adloader.js'; +import { logError, generateUUID, insertElement } from '../src/utils.js'; +import * as events from '../src/events.js'; +import { EVENTS } from '../src/constants.js'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; + +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + +// ============================ MODULE STATE =============================== + +/** + * @type {function(): void} + * Page-wide initialization step / strategy + */ +let onModuleInit = () => {}; + +/** + * @type {function(Object): void} + * Bid response mutation step / strategy. + */ +let onBidResponse = () => {}; + +/** + * @type {number} + * 0 for unknown, 1 for preloaded, -1 for error. + */ +let preloadStatus = 0; + +// ============================ MODULE LOGIC =============================== + +/** + * Page initialization step which just preloads the script, to be available whenever we start processing the bids. + * @param {string} scriptURL The script URL to preload + */ +function pageInitStepPreloadScript(scriptURL) { + // TODO: this bypasses adLoader + const linkElement = document.createElement('link'); + linkElement.rel = 'preload'; + linkElement.as = 'script'; + linkElement.href = scriptURL; + linkElement.onload = () => { preloadStatus = 1; }; + linkElement.onerror = () => { preloadStatus = -1; }; + insertElement(linkElement); +} + +/** + * Page initialization step which adds the protector script to the whole page. With that, there is no need wrapping bids, and the coverage is better. + * @param {string} scriptURL The script URL to add to the page for protection + */ +function pageInitStepProtectPage(scriptURL) { + loadExternalScript(scriptURL, MODULE_TYPE_RTD, 'humansecurityMalvDefense'); +} + +/** + * Bid processing step which alters the ad HTML to contain bid-specific information, which can be used to identify the creative later. + * @param {Object} bidResponse Bid response data + */ +function bidWrapStepAugmentHtml(bidResponse) { + bidResponse.ad = `\n${bidResponse.ad}`; +} + +/** + * Bid processing step which applies creative protection by wrapping the ad HTML. + * @param {string} scriptURL + * @param {number} requiredPreload + * @param {Object} bidResponse + */ +function bidWrapStepProtectByWrapping(scriptURL, requiredPreload, bidResponse) { + // Still prepend bid info, it's always helpful to have creative data in its payload + bidWrapStepAugmentHtml(bidResponse); + + // If preloading failed, or if configuration requires us to finish preloading - + // we should not process this bid any further + if (preloadStatus < requiredPreload) { + return; + } + + const sid = generateUUID(); + bidResponse.ad = ` + + + `; +} + +/** + * Custom error class to differentiate validation errors + */ +class ConfigError extends Error { } + +/** + * The function to be called upon module init. Depending on the passed config, initializes properly init/bid steps or throws ConfigError. + * @param {Object} config + */ +function readConfig(config) { + if (!config.params) { + throw new ConfigError('Missing config parameters for humansecurityMalvDefense RTD module provider.'); + } + + if (typeof config.params.cdnUrl !== 'string' || !/^https?:\/\//.test(config.params.cdnUrl)) { + throw new ConfigError('Parameter "cdnUrl" is a required string parameter, which should start with "http(s)://".'); + } + + if (typeof config.params.protectionMode !== 'string') { + throw new ConfigError('Parameter "protectionMode" is a required string parameter.'); + } + + const scriptURL = config.params.cdnUrl; + + switch (config.params.protectionMode) { + case 'full': + onModuleInit = () => pageInitStepProtectPage(scriptURL); + onBidResponse = (bidResponse) => bidWrapStepAugmentHtml(bidResponse); + break; + + case 'bids': + onModuleInit = () => pageInitStepPreloadScript(scriptURL); + onBidResponse = (bidResponse) => bidWrapStepProtectByWrapping(scriptURL, 0, bidResponse); + break; + + case 'bids-nowait': + onModuleInit = () => pageInitStepPreloadScript(scriptURL); + onBidResponse = (bidResponse) => bidWrapStepProtectByWrapping(scriptURL, 1, bidResponse); + break; + + default: + throw new ConfigError('Parameter "protectionMode" must be one of "full" | "bids" | "bids-nowait".'); + } +} + +/** + * The function to be called upon module init + * Defined as a variable to be able to reset it naturally + */ +let startBillableEvents = function() { + // Upon humansecurityMalvDefense submodule initialization, every winner bid is considered to be protected + // and therefore, subjected to billing + events.on(EVENTS.BID_WON, winnerBidResponse => { + events.emit(EVENTS.BILLABLE_EVENT, { + vendor: 'humansecurityMalvDefense', + billingId: generateUUID(), + type: 'impression', + auctionId: winnerBidResponse.auctionId, + transactionId: winnerBidResponse.transactionId, + bidId: winnerBidResponse.requestId, + }); + }); +} + +// ============================ MODULE REGISTRATION =============================== + +/** + * The function which performs submodule registration. + */ +function beforeInit() { + submodule('realTimeData', /** @type {RtdSubmodule} */ ({ + name: 'humansecurityMalvDefense', + + init: (config, userConsent) => { + try { + readConfig(config); + onModuleInit(); + + // Subscribing once to ensure no duplicate events + // in case module initialization code runs multiple times + // This should have been a part of submodule definition, but well... + // The assumption here is that in production init() will be called exactly once + startBillableEvents(); + startBillableEvents = () => {}; + return true; + } catch (err) { + if (err instanceof ConfigError) { + logError(err.message); + } + return false; + } + }, + + onBidResponseEvent: (bidResponse, config, userConsent) => { + onBidResponse(bidResponse); + } + })); +} + +/** + * Exporting local (and otherwise encapsulated to this module) functions + * for testing purposes + */ +export const __TEST__ = { + pageInitStepPreloadScript, + pageInitStepProtectPage, + bidWrapStepAugmentHtml, + bidWrapStepProtectByWrapping, + ConfigError, + readConfig, + beforeInit, +} + +beforeInit(); diff --git a/modules/humansecurityMalvDefenseRtdProvider.md b/modules/humansecurityMalvDefenseRtdProvider.md new file mode 100644 index 00000000000..3a1bd68caa4 --- /dev/null +++ b/modules/humansecurityMalvDefenseRtdProvider.md @@ -0,0 +1,63 @@ +# Overview + +``` +Module Name: humansecurityMalvDefense RTD Provider +Module Type: RTD Provider +Maintainer: eugene.tikhonov@humansecurity.com +``` + +The HUMAN Security Malvertising Defense RTD submodule offers a robust, easy-to-implement anti-malvertising solution for publishers. +Its automatic updates continuously detect and block on-page malicious ad behaviors — such as unwanted redirects and deceptive ads with harmful landing pages. +This safeguards revenue and visitor experience without extra maintenance, and with minimal impact on page load speed and overall site performance. +Publishers can also opt in to add HUMAN Ad Quality monitoring for broader protection. + +Using this module requires prior agreement with [HUMAN Security](https://www.humansecurity.com/) to obtain the necessary distribution key. + +## Integration + +To integrate, add the HUMAN Security Malvertising Defense submodule to your Prebid.js package with: + +```bash +gulp build --modules="rtdModule,humansecurityMalvDefenseRtdProvider,..." +``` + +> `rtdModule` is a required module to use HUMAN Security RTD module. + +## Configuration + +This module is configured as part of the `realTimeData.dataProviders` object. + +When built into Prebid.js, this module can be configured through the following `pbjs.setConfig` call: + +```javascript +pbjs.setConfig({ + realTimeData: { + dataProviders: [{ + name: 'humansecurityMalvDefense', + params: { + cdnUrl: 'https://cadmus.script.ac//script.js', // Contact HUMAN Security to get your own CDN URL + protectionMode: 'full', // Supported modes are 'full', 'bids' and 'bids-nowait', see below. + } + }] + } +}); +``` + +### Configuration parameters + +{: .table .table-bordered .table-striped } + +| Name | Type | Scope | Description | +| :------------ | :------------ | :------------ |:------------ | +| ``cdnUrl`` | ``string`` | Required | CDN URL of the script, which is to be used for protection. | +| ``protectionMode`` | ``'full'`` or ``'bids'`` or ``'bids-nowait'`` | Required | Integration mode. Please refer to the "Integration modes" section for details. | + +### Integration modes + +{: .table .table-bordered .table-striped } + +| Integration Mode | Parameter Value | Description | +| :------------ | :------------ | :------------ | +| Full page protection | ``'full'`` | Preferred mode. The module will add the protector agent script directly to the page, and it will protect all placements. This mode will make the most out of various behavioral detection mechanisms, and will also prevent typical malicious behaviors. | +| Bids-only protection | ``'bids'`` | The module will protect specific bid responses - specifically, the HTML that represents the ad payload - by wrapping them with the agent script. Ads served outside of Prebid will not be protected in this mode, as the module can only access ads delivered through Prebid. | +| Bids-only protection with no delay on bid rendering | ``'bids-nowait'`` | Same as above, but in this mode, the script will also *not* wrap those bid responses, which arrived prior to successful preloading of agent script. | diff --git a/src/adloader.js b/src/adloader.js index a170af11f68..111c5773f8a 100644 --- a/src/adloader.js +++ b/src/adloader.js @@ -20,6 +20,7 @@ const _approvedLoadExternalJSList = [ 'browsi', 'brandmetrics', 'clean.io', + 'humansecurityMalvDefense', 'humansecurity', 'confiant', 'contxtful', diff --git a/test/spec/modules/humansecurityMalvDefenseRtdProvider_spec.js b/test/spec/modules/humansecurityMalvDefenseRtdProvider_spec.js new file mode 100644 index 00000000000..d52fbbd6799 --- /dev/null +++ b/test/spec/modules/humansecurityMalvDefenseRtdProvider_spec.js @@ -0,0 +1,211 @@ +import { loadExternalScriptStub } from 'test/mocks/adloaderStub.js'; +import * as utils from '../../../src/utils.js'; +import * as hook from '../../../src/hook.js' +import * as events from '../../../src/events.js'; +import { EVENTS } from '../../../src/constants.js'; + +import { __TEST__ } from '../../../modules/humansecurityMalvDefenseRtdProvider.js'; +import { MODULE_TYPE_RTD } from '../../../src/activities/modules.js'; + +const { + readConfig, + ConfigError, + pageInitStepPreloadScript, + pageInitStepProtectPage, + bidWrapStepAugmentHtml, + bidWrapStepProtectByWrapping, + beforeInit, +} = __TEST__; + +sinon.assert.expose(chai.assert, { prefix: 'sinon' }); + +const fakeScriptURL = 'https://example.com/script.js'; + +function makeFakeBidResponse() { + return { + ad: 'hello ad', + bidderCode: 'BIDDER', + creativeId: 'CREATIVE', + cpm: 1.23, + }; +} + +describe('humansecurityMalvDefense RTD module', function () { + describe('readConfig()', function() { + it('should throw ConfigError on invalid configurations', function() { + expect(() => readConfig({})).to.throw(ConfigError); + expect(() => readConfig({ params: {} })).to.throw(ConfigError); + expect(() => readConfig({ params: { protectionMode: 'bids' } })).to.throw(ConfigError); + expect(() => readConfig({ params: { cdnUrl: 'abc' } })).to.throw(ConfigError); + expect(() => readConfig({ params: { cdnUrl: 'abc', protectionMode: 'bids' } })).to.throw(ConfigError); + expect(() => readConfig({ params: { cdnUrl: 'https://cadmus.script.ac/abc1234567890/script.js', protectionMode: '123' } })).to.throw(ConfigError); + }); + + it('should accept valid configurations', function() { + expect(() => readConfig({ params: { cdnUrl: 'https://cadmus.script.ac/abc1234567890/script.js', protectionMode: 'full' } })).to.not.throw(); + expect(() => readConfig({ params: { cdnUrl: 'https://cadmus.script.ac/abc1234567890/script.js', protectionMode: 'bids' } })).to.not.throw(); + expect(() => readConfig({ params: { cdnUrl: 'https://cadmus.script.ac/abc1234567890/script.js', protectionMode: 'bids-nowait' } })).to.not.throw(); + }); + }); + + describe('Module initialization step', function() { + let insertElementStub; + beforeEach(function() { + insertElementStub = sinon.stub(utils, 'insertElement'); + }); + afterEach(function() { + utils.insertElement.restore(); + }); + + it('pageInitStepPreloadScript() should insert link/preload element', function() { + pageInitStepPreloadScript(fakeScriptURL); + + sinon.assert.calledOnce(insertElementStub); + sinon.assert.calledWith(insertElementStub, sinon.match(elem => elem.tagName === 'LINK')); + sinon.assert.calledWith(insertElementStub, sinon.match(elem => elem.rel === 'preload')); + sinon.assert.calledWith(insertElementStub, sinon.match(elem => elem.as === 'script')); + sinon.assert.calledWith(insertElementStub, sinon.match(elem => elem.href === fakeScriptURL)); + }); + + it('pageInitStepProtectPage() should insert script element', function() { + pageInitStepProtectPage(fakeScriptURL); + + sinon.assert.calledOnce(loadExternalScriptStub); + sinon.assert.calledWith(loadExternalScriptStub, fakeScriptURL, MODULE_TYPE_RTD, 'humansecurityMalvDefense'); + }); + }); + + function ensurePrependToBidResponse(fakeBidResponse) { + expect(fakeBidResponse).to.have.own.property('ad').which.is.a('string'); + expect(fakeBidResponse.ad).to.contain(''); + } + + function ensureWrapBidResponse(fakeBidResponse, scriptUrl) { + expect(fakeBidResponse).to.have.own.property('ad').which.is.a('string'); + expect(fakeBidResponse.ad).to.contain(`src="${scriptUrl}"`); + expect(fakeBidResponse.ad).to.contain('agent.put(ad)'); + } + + describe('Bid processing step', function() { + it('bidWrapStepAugmentHtml() should prepend bid-specific information in a comment', function() { + const fakeBidResponse = makeFakeBidResponse(); + bidWrapStepAugmentHtml(fakeBidResponse); + ensurePrependToBidResponse(fakeBidResponse); + }); + + it('bidWrapStepProtectByWrapping() should wrap payload into a script tag', function() { + const fakeBidResponse = makeFakeBidResponse(); + bidWrapStepProtectByWrapping(fakeScriptURL, 0, fakeBidResponse); + ensureWrapBidResponse(fakeBidResponse, fakeScriptURL); + }); + }); + + describe('Sumbodule execution', function() { + let submoduleStub; + let insertElementStub; + beforeEach(function () { + submoduleStub = sinon.stub(hook, 'submodule'); + insertElementStub = sinon.stub(utils, 'insertElement'); + }); + afterEach(function () { + utils.insertElement.restore(); + submoduleStub.restore(); + }); + + function getModule() { + beforeInit('humansecurityMalvDefense'); + + expect(submoduleStub.calledOnceWith('realTimeData')).to.equal(true); + + const registeredSubmoduleDefinition = submoduleStub.getCall(0).args[1]; + expect(registeredSubmoduleDefinition).to.be.an('object'); + expect(registeredSubmoduleDefinition).to.have.own.property('name', 'humansecurityMalvDefense'); + expect(registeredSubmoduleDefinition).to.have.own.property('init').that.is.a('function'); + expect(registeredSubmoduleDefinition).to.have.own.property('onBidResponseEvent').that.is.a('function'); + + return registeredSubmoduleDefinition; + } + + it('should register humansecurityMalvDefense RTD submodule provider', function () { + getModule(); + }); + + it('should refuse initialization with incorrect parameters', function () { + const { init } = getModule(); + expect(init({ params: { cdnUrl: 'abc', protectionMode: 'full' } }, {})).to.equal(false); // too short distribution name + sinon.assert.notCalled(loadExternalScriptStub); + }); + + it('should initialize in full (page) protection mode', function () { + const { init, onBidResponseEvent } = getModule(); + expect(init({ params: { cdnUrl: 'https://cadmus.script.ac/abc1234567890/script.js', protectionMode: 'full' } }, {})).to.equal(true); + sinon.assert.calledOnce(loadExternalScriptStub); + sinon.assert.calledWith(loadExternalScriptStub, 'https://cadmus.script.ac/abc1234567890/script.js', MODULE_TYPE_RTD, 'humansecurityMalvDefense'); + + const fakeBidResponse = makeFakeBidResponse(); + onBidResponseEvent(fakeBidResponse, {}, {}); + ensurePrependToBidResponse(fakeBidResponse); + }); + + it('should iniitalize in bids (frame) protection mode', function () { + const { init, onBidResponseEvent } = getModule(); + expect(init({ params: { cdnUrl: 'https://cadmus.script.ac/abc1234567890/script.js', protectionMode: 'bids' } }, {})).to.equal(true); + sinon.assert.calledOnce(insertElementStub); + sinon.assert.calledWith(insertElementStub, sinon.match(elem => elem.tagName === 'LINK')); + + const fakeBidResponse = makeFakeBidResponse(); + onBidResponseEvent(fakeBidResponse, {}, {}); + ensureWrapBidResponse(fakeBidResponse, 'https://cadmus.script.ac/abc1234567890/script.js'); + }); + + it('should respect preload status in bids-nowait protection mode', function () { + const { init, onBidResponseEvent } = getModule(); + expect(init({ params: { cdnUrl: 'https://cadmus.script.ac/abc1234567890/script.js', protectionMode: 'bids-nowait' } }, {})).to.equal(true); + sinon.assert.calledOnce(insertElementStub); + sinon.assert.calledWith(insertElementStub, sinon.match(elem => elem.tagName === 'LINK')); + const preloadLink = insertElementStub.getCall(0).args[0]; + expect(preloadLink).to.have.property('onload').which.is.a('function'); + expect(preloadLink).to.have.property('onerror').which.is.a('function'); + + const fakeBidResponse1 = makeFakeBidResponse(); + onBidResponseEvent(fakeBidResponse1, {}, {}); + ensurePrependToBidResponse(fakeBidResponse1); + + // Simulate successful preloading + preloadLink.onload(); + + const fakeBidResponse2 = makeFakeBidResponse(); + onBidResponseEvent(fakeBidResponse2, {}, {}); + ensureWrapBidResponse(fakeBidResponse2, 'https://cadmus.script.ac/abc1234567890/script.js'); + + // Simulate error + preloadLink.onerror(); + + // Now we should fallback to just prepending + const fakeBidResponse3 = makeFakeBidResponse(); + onBidResponseEvent(fakeBidResponse3, {}, {}); + ensurePrependToBidResponse(fakeBidResponse3); + }); + + it('should send billable event per bid won event', function () { + const { init } = getModule(); + expect(init({ params: { cdnUrl: 'https://cadmus.script.ac/abc1234567890/script.js', protectionMode: 'full' } }, {})).to.equal(true); + + const eventCounter = { registerHumansecurityMalvDefenseBillingEvent: function() {} }; + sinon.spy(eventCounter, 'registerHumansecurityMalvDefenseBillingEvent'); + + events.on(EVENTS.BILLABLE_EVENT, (evt) => { + if (evt.vendor === 'humansecurityMalvDefense') { + eventCounter.registerHumansecurityMalvDefenseBillingEvent() + } + }); + + events.emit(EVENTS.BID_WON, {}); + events.emit(EVENTS.BID_WON, {}); + events.emit(EVENTS.BID_WON, {}); + events.emit(EVENTS.BID_WON, {}); + + sinon.assert.callCount(eventCounter.registerHumansecurityMalvDefenseBillingEvent, 4); + }); + }); +}); From 209883868b98d5fa47b6176dc6aa04944adfd312 Mon Sep 17 00:00:00 2001 From: "yevhen.tykhonov" Date: Thu, 3 Jul 2025 22:48:12 +0300 Subject: [PATCH 2/2] cleanioRtdProvider imports all logic from humansecurityMalvDefenseRtdProvider --- modules/cleanioRtdProvider.js | 215 +----------- .../humansecurityMalvDefenseRtdProvider.js | 313 +++++++++--------- test/spec/modules/cleanioRtdProvider_spec.js | 10 +- ...umansecurityMalvDefenseRtdProvider_spec.js | 6 +- 4 files changed, 176 insertions(+), 368 deletions(-) diff --git a/modules/cleanioRtdProvider.js b/modules/cleanioRtdProvider.js index 35751210878..8f628505ed4 100644 --- a/modules/cleanioRtdProvider.js +++ b/modules/cleanioRtdProvider.js @@ -6,219 +6,14 @@ * @requires module:modules/realTimeData */ -import { submodule } from '../src/hook.js'; -import { loadExternalScript } from '../src/adloader.js'; -import { logError, generateUUID, insertElement } from '../src/utils.js'; -import * as events from '../src/events.js'; -import { EVENTS } from '../src/constants.js'; -import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; +import { createRtdSubmodule } from './humansecurityMalvDefenseRtdProvider.js'; /* eslint prebid/validate-imports: "off" */ -/** - * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule - */ - -// ============================ MODULE STATE =============================== - -/** - * @type {function(): void} - * Page-wide initialization step / strategy - */ -let onModuleInit = () => {}; - -/** - * @type {function(Object): void} - * Bid response mutation step / strategy. - */ -let onBidResponse = () => {}; - -/** - * @type {number} - * 0 for unknown, 1 for preloaded, -1 for error. - */ -let preloadStatus = 0; - -// ============================ MODULE LOGIC =============================== - -/** - * Page initialization step which just preloads the script, to be available whenever we start processing the bids. - * @param {string} scriptURL The script URL to preload - */ -function pageInitStepPreloadScript(scriptURL) { - // TODO: this bypasses adLoader - const linkElement = document.createElement('link'); - linkElement.rel = 'preload'; - linkElement.as = 'script'; - linkElement.href = scriptURL; - linkElement.onload = () => { preloadStatus = 1; }; - linkElement.onerror = () => { preloadStatus = -1; }; - insertElement(linkElement); -} - -/** - * Page initialization step which adds the protector script to the whole page. With that, there is no need wrapping bids, and the coverage is better. - * @param {string} scriptURL The script URL to add to the page for protection - */ -function pageInitStepProtectPage(scriptURL) { - loadExternalScript(scriptURL, MODULE_TYPE_RTD, 'clean.io'); -} - -/** - * Bid processing step which alters the ad HTML to contain bid-specific information, which can be used to identify the creative later. - * @param {Object} bidResponse Bid response data - */ -function bidWrapStepAugmentHtml(bidResponse) { - bidResponse.ad = `\n${bidResponse.ad}`; -} - -/** - * Bid processing step which applies creative protection by wrapping the ad HTML. - * @param {string} scriptURL - * @param {number} requiredPreload - * @param {Object} bidResponse - */ -function bidWrapStepProtectByWrapping(scriptURL, requiredPreload, bidResponse) { - // Still prepend bid info, it's always helpful to have creative data in its payload - bidWrapStepAugmentHtml(bidResponse); - - // If preloading failed, or if configuration requires us to finish preloading - - // we should not process this bid any further - if (preloadStatus < requiredPreload) { - return; - } - - const sid = generateUUID(); - bidResponse.ad = ` - - - `; -} - -/** - * Custom error class to differentiate validation errors - */ -class ConfigError extends Error { } - -/** - * The function to be called upon module init. Depending on the passed config, initializes properly init/bid steps or throws ConfigError. - * @param {Object} config - */ -function readConfig(config) { - if (!config.params) { - throw new ConfigError('Missing config parameters for clean.io RTD module provider.'); - } - - if (typeof config.params.cdnUrl !== 'string' || !/^https?:\/\//.test(config.params.cdnUrl)) { - throw new ConfigError('Parameter "cdnUrl" is a required string parameter, which should start with "http(s)://".'); - } - - if (typeof config.params.protectionMode !== 'string') { - throw new ConfigError('Parameter "protectionMode" is a required string parameter.'); - } - - const scriptURL = config.params.cdnUrl; - - switch (config.params.protectionMode) { - case 'full': - onModuleInit = () => pageInitStepProtectPage(scriptURL); - onBidResponse = (bidResponse) => bidWrapStepAugmentHtml(bidResponse); - break; - - case 'bids': - onModuleInit = () => pageInitStepPreloadScript(scriptURL); - onBidResponse = (bidResponse) => bidWrapStepProtectByWrapping(scriptURL, 0, bidResponse); - break; - - case 'bids-nowait': - onModuleInit = () => pageInitStepPreloadScript(scriptURL); - onBidResponse = (bidResponse) => bidWrapStepProtectByWrapping(scriptURL, 1, bidResponse); - break; - - default: - throw new ConfigError('Parameter "protectionMode" must be one of "full" | "bids" | "bids-nowait".'); - } -} - -/** - * The function to be called upon module init - * Defined as a variable to be able to reset it naturally - */ -let startBillableEvents = function() { - // Upon clean.io submodule initialization, every winner bid is considered to be protected - // and therefore, subjected to billing - events.on(EVENTS.BID_WON, winnerBidResponse => { - events.emit(EVENTS.BILLABLE_EVENT, { - vendor: 'clean.io', - billingId: generateUUID(), - type: 'impression', - auctionId: winnerBidResponse.auctionId, - transactionId: winnerBidResponse.transactionId, - bidId: winnerBidResponse.requestId, - }); - }); -} - -// ============================ MODULE REGISTRATION =============================== - -/** - * The function which performs submodule registration. - */ -function beforeInit() { - submodule('realTimeData', /** @type {RtdSubmodule} */ ({ - name: 'clean.io', - - init: (config, userConsent) => { - try { - readConfig(config); - onModuleInit(); - - // Subscribing once to ensure no duplicate events - // in case module initialization code runs multiple times - // This should have been a part of submodule definition, but well... - // The assumption here is that in production init() will be called exactly once - startBillableEvents(); - startBillableEvents = () => {}; - return true; - } catch (err) { - if (err instanceof ConfigError) { - logError(err.message); - } - return false; - } - }, - - onBidResponseEvent: (bidResponse, config, userConsent) => { - onBidResponse(bidResponse); - } - })); -} +const internals = createRtdSubmodule('clean.io'); /** - * Exporting local (and otherwise encapsulated to this module) functions + * Exporting encapsulated to this module functions * for testing purposes */ -export const __TEST__ = { - pageInitStepPreloadScript, - pageInitStepProtectPage, - bidWrapStepAugmentHtml, - bidWrapStepProtectByWrapping, - ConfigError, - readConfig, - beforeInit, -} +export const __CLEANIO_TEST__ = internals; -beforeInit(); +internals.beforeInit(); diff --git a/modules/humansecurityMalvDefenseRtdProvider.js b/modules/humansecurityMalvDefenseRtdProvider.js index 676ea2879a3..1f22ae1d1d3 100644 --- a/modules/humansecurityMalvDefenseRtdProvider.js +++ b/modules/humansecurityMalvDefenseRtdProvider.js @@ -17,77 +17,109 @@ import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule */ -// ============================ MODULE STATE =============================== - -/** - * @type {function(): void} - * Page-wide initialization step / strategy - */ -let onModuleInit = () => {}; - -/** - * @type {function(Object): void} - * Bid response mutation step / strategy. - */ -let onBidResponse = () => {}; - /** - * @type {number} - * 0 for unknown, 1 for preloaded, -1 for error. + * Custom error class to differentiate validation errors */ -let preloadStatus = 0; - -// ============================ MODULE LOGIC =============================== +class ConfigError extends Error { } /** - * Page initialization step which just preloads the script, to be available whenever we start processing the bids. - * @param {string} scriptURL The script URL to preload + * Bid processing step which alters the ad HTML to contain bid-specific information, which can be used to identify the creative later. + * @param {Object} bidResponse Bid response data */ -function pageInitStepPreloadScript(scriptURL) { - // TODO: this bypasses adLoader - const linkElement = document.createElement('link'); - linkElement.rel = 'preload'; - linkElement.as = 'script'; - linkElement.href = scriptURL; - linkElement.onload = () => { preloadStatus = 1; }; - linkElement.onerror = () => { preloadStatus = -1; }; - insertElement(linkElement); +function bidWrapStepAugmentHtml(bidResponse) { + bidResponse.ad = `\n${bidResponse.ad}`; } /** * Page initialization step which adds the protector script to the whole page. With that, there is no need wrapping bids, and the coverage is better. * @param {string} scriptURL The script URL to add to the page for protection + * @param {string} moduleName */ -function pageInitStepProtectPage(scriptURL) { - loadExternalScript(scriptURL, MODULE_TYPE_RTD, 'humansecurityMalvDefense'); +function pageInitStepProtectPage(scriptURL, moduleName) { + loadExternalScript(scriptURL, MODULE_TYPE_RTD, moduleName); } /** - * Bid processing step which alters the ad HTML to contain bid-specific information, which can be used to identify the creative later. - * @param {Object} bidResponse Bid response data + * Factory function that creates, registers, and returns a new RTD submodule instance. + * This is the single entry point for this module's logic. + * @param {string} moduleName - The name of the module + * @returns {Object} An object containing the module's internal functions for testing */ -function bidWrapStepAugmentHtml(bidResponse) { - bidResponse.ad = `\n${bidResponse.ad}`; -} +export function createRtdSubmodule(moduleName) { + // ============================ MODULE STATE =============================== + + /** + * @type {function(): void} + * Page-wide initialization step / strategy + */ + let onModuleInit = () => {}; + + /** + * @type {function(Object): void} + * Bid response mutation step / strategy. + */ + let onBidResponse = () => {}; + + /** + * @type {number} + * 0 for unknown, 1 for preloaded, -1 for error. + */ + let preloadStatus = 0; + + /** + * The function to be called upon module init + * Defined as a variable to be able to reset it naturally + */ + let startBillableEvents = function() { + // Upon this submodule initialization, every winner bid is considered to be protected + // and therefore, subjected to billing + events.on(EVENTS.BID_WON, winnerBidResponse => { + events.emit(EVENTS.BILLABLE_EVENT, { + vendor: moduleName, + billingId: generateUUID(), + type: 'impression', + auctionId: winnerBidResponse.auctionId, + transactionId: winnerBidResponse.transactionId, + bidId: winnerBidResponse.requestId, + }); + }); + } -/** - * Bid processing step which applies creative protection by wrapping the ad HTML. - * @param {string} scriptURL - * @param {number} requiredPreload - * @param {Object} bidResponse - */ -function bidWrapStepProtectByWrapping(scriptURL, requiredPreload, bidResponse) { - // Still prepend bid info, it's always helpful to have creative data in its payload - bidWrapStepAugmentHtml(bidResponse); - - // If preloading failed, or if configuration requires us to finish preloading - - // we should not process this bid any further - if (preloadStatus < requiredPreload) { - return; + // ============================ MODULE LOGIC =============================== + + /** + * Page initialization step which just preloads the script, to be available whenever we start processing the bids. + * @param {string} scriptURL The script URL to preload + */ + function pageInitStepPreloadScript(scriptURL) { + // TODO: this bypasses adLoader + const linkElement = document.createElement('link'); + linkElement.rel = 'preload'; + linkElement.as = 'script'; + linkElement.href = scriptURL; + linkElement.onload = () => { preloadStatus = 1; }; + linkElement.onerror = () => { preloadStatus = -1; }; + insertElement(linkElement); } - const sid = generateUUID(); - bidResponse.ad = ` + /** + * Bid processing step which applies creative protection by wrapping the ad HTML. + * @param {string} scriptURL + * @param {number} requiredPreload + * @param {Object} bidResponse + */ + function bidWrapStepProtectByWrapping(scriptURL, requiredPreload, bidResponse) { + // Still prepend bid info, it's always helpful to have creative data in its payload + bidWrapStepAugmentHtml(bidResponse); + + // If preloading failed, or if configuration requires us to finish preloading - + // we should not process this bid any further + if (preloadStatus < requiredPreload) { + return; + } + + const sid = generateUUID(); + bidResponse.ad = `