diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index 958173a09655..d8b55f94f9c8 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -329,9 +329,10 @@ export function newBidder(spec) { function registerSyncs(responses, gdprConsent) { if (spec.getUserSyncs) { + let filterConfig = config.getConfig('userSync.filterSettings'); let syncs = spec.getUserSyncs({ - iframeEnabled: config.getConfig('userSync.iframeEnabled'), - pixelEnabled: config.getConfig('userSync.pixelEnabled'), + iframeEnabled: !!(config.getConfig('userSync.iframeEnabled') || (filterConfig && (filterConfig.iframe || filterConfig.all))), + pixelEnabled: !!(config.getConfig('userSync.pixelEnabled') || (filterConfig && (filterConfig.image || filterConfig.all))), }, responses, gdprConsent); if (syncs) { if (!Array.isArray(syncs)) { diff --git a/src/userSync.js b/src/userSync.js index 059d3ea62c12..20563d447a27 100644 --- a/src/userSync.js +++ b/src/userSync.js @@ -1,5 +1,6 @@ import * as utils from 'src/utils'; import { config } from 'src/config'; +import includes from 'core-js/library/fn/array/includes'; // Set userSync default values config.setDefaults({ @@ -28,6 +29,12 @@ export function newUserSync(userSyncDependencies) { // How many bids for each adapter let numAdapterBids = {}; + // for now - default both to false in case filterSettings config is absent/misconfigured + let permittedPixels = { + image: false, + iframe: false + } + // Use what is in config by default let usConfig = userSyncDependencies.config; // Update if it's (re)set @@ -77,7 +84,7 @@ export function newUserSync(userSyncDependencies) { * @private */ function fireImagePixels() { - if (!usConfig.pixelEnabled) { + if (!(usConfig.pixelEnabled || permittedPixels.image)) { return; } // Randomize the order of the pixels before firing @@ -97,7 +104,7 @@ export function newUserSync(userSyncDependencies) { * @private */ function loadIframes() { - if (!usConfig.iframeEnabled) { + if (!(usConfig.iframeEnabled || permittedPixels.iframe)) { return; } // Randomize the order of these syncs just like the pixels above @@ -148,15 +155,89 @@ export function newUserSync(userSyncDependencies) { if (Number(numAdapterBids[bidder]) >= usConfig.syncsPerBidder) { return utils.logWarn(`Number of user syncs exceeded for "${bidder}"`); } - // All bidders are enabled by default. If specified only register for enabled bidders. - let hasEnabledBidders = usConfig.enabledBidders && usConfig.enabledBidders.length; - if (hasEnabledBidders && usConfig.enabledBidders.indexOf(bidder) < 0) { - return utils.logWarn(`Bidder "${bidder}" not supported`); + + if (usConfig.filterSettings) { + if (shouldBidderBeBlocked(type, bidder)) { + return utils.logWarn(`Bidder '${bidder}' is not permitted to register their userSync ${type} pixels as per filterSettings config.`); + } + // TODO remove this else if code that supports deprecated fields (sometime in 2.x); for now - only run if filterSettings config is not present + } else if (usConfig.enabledBidders && usConfig.enabledBidders.length && usConfig.enabledBidders.indexOf(bidder) < 0) { + return utils.logWarn(`Bidder "${bidder}" not permitted to register their userSync pixels.`); } + + // the bidder's pixel has passed all checks and is allowed to register queue[type].push([bidder, url]); numAdapterBids = incrementAdapterBids(numAdapterBids, bidder); }; + /** + * @function shouldBidderBeBlocked + * @summary Check filterSettings logic to determine if the bidder should be prevented from registering their userSync tracker + * @private + * @param {string} type The type of the sync; either image or iframe + * @param {string} bidder The name of the adapter. e.g. "rubicon" + * @returns {boolean} true => bidder is not allowed to register; false => bidder can register + */ + function shouldBidderBeBlocked(type, bidder) { + let filterConfig = usConfig.filterSettings; + + // apply the filter check if the config object is there (eg filterSettings.iframe exists) and if the config object is properly setup + if (isFilterConfigValid(filterConfig, type)) { + permittedPixels[type] = true; + + let activeConfig = (filterConfig.all) ? filterConfig.all : filterConfig[type]; + let biddersToFilter = (activeConfig.bidders === '*') ? [bidder] : activeConfig.bidders; + let filterType = activeConfig.filter || 'include'; // set default if undefined + + // return true if the bidder is either: not part of the include (ie outside the whitelist) or part of the exclude (ie inside the blacklist) + const checkForFiltering = { + 'include': (bidders, bidder) => !includes(bidders, bidder), + 'exclude': (bidders, bidder) => includes(bidders, bidder) + } + return checkForFiltering[filterType](biddersToFilter, bidder); + } + return false; + } + + /** + * @function isFilterConfigValid + * @summary Check if the filterSettings object in the userSync config is setup properly + * @private + * @param {object} filterConfig sub-config object taken from filterSettings + * @param {string} type The type of the sync; either image or iframe + * @returns {boolean} true => config is setup correctly, false => setup incorrectly or filterConfig[type] is not present + */ + function isFilterConfigValid(filterConfig, type) { + if (filterConfig.all && filterConfig[type]) { + utils.logWarn(`Detected presence of the "filterSettings.all" and "filterSettings.${type}" in userSync config. You cannot mix "all" with "iframe/image" configs; they are mutually exclusive.`); + return false; + } + + let activeConfig = (filterConfig.all) ? filterConfig.all : filterConfig[type]; + let activeConfigName = (filterConfig.all) ? 'all' : type; + + // if current pixel type isn't part of the config's logic, skip rest of the config checks... + // we return false to skip subsequent filter checks in shouldBidderBeBlocked() function + if (!activeConfig) { + return false; + } + + let filterField = activeConfig.filter; + let biddersField = activeConfig.bidders; + + if (filterField && filterField !== 'include' && filterField !== 'exclude') { + utils.logWarn(`UserSync "filterSettings.${activeConfigName}.filter" setting '${filterField}' is not a valid option; use either 'include' or 'exclude'.`); + return false; + } + + if (biddersField !== '*' && !(Array.isArray(biddersField) && biddersField.length > 0 && biddersField.every(bidderInList => utils.isStr(bidderInList) && bidderInList !== '*'))) { + utils.logWarn(`Detected an invalid setup in userSync "filterSettings.${activeConfigName}.bidders"; use either '*' (to represent all bidders) or an array of bidders.`); + return false; + } + + return true; + } + /** * @function syncUsers * @summary Trigger all the user syncs based on publisher-defined timeout @@ -207,4 +288,5 @@ export const userSync = newUserSync({ * @property {boolean} iframeEnabled * @property {int} syncsPerBidder * @property {string[]} enabledBidders + * @property {Object} filterSettings */ diff --git a/test/spec/userSync_spec.js b/test/spec/userSync_spec.js index 9a72629c71a3..60e07441e0ca 100644 --- a/test/spec/userSync_spec.js +++ b/test/spec/userSync_spec.js @@ -192,4 +192,153 @@ describe('user sync', () => { expect(triggerPixelStub.getCall(0)).to.not.be.null; expect(triggerPixelStub.getCall(0).args[0]).to.exist.and.to.equal('http://example.com'); }); + + it('should register both image and iframe pixels with filterSettings.all config', () => { + const userSync = newTestUserSync({ + filterSettings: { + all: { + bidders: ['atestBidder', 'testBidder'], + filter: 'include' + }, + } + }); + userSync.registerSync('image', 'atestBidder', 'http://example.com/1'); + userSync.registerSync('iframe', 'testBidder', 'http://example.com/iframe'); + userSync.syncUsers(); + expect(triggerPixelStub.getCall(0)).to.not.be.null; + expect(triggerPixelStub.getCall(0).args[0]).to.exist.and.to.equal('http://example.com/1'); + expect(insertUserSyncIframeStub.getCall(0)).to.not.be.null; + expect(insertUserSyncIframeStub.getCall(0).args[0]).to.equal('http://example.com/iframe'); + }); + + it('should register iframe and not register image pixels based on filterSettings config', () => { + const userSync = newTestUserSync({ + filterSettings: { + image: { + bidders: '*', + filter: 'exclude' + }, + iframe: { + bidders: ['testBidder'] + } + } + }); + userSync.registerSync('image', 'atestBidder', 'http://example.com/1'); + userSync.registerSync('iframe', 'testBidder', 'http://example.com/iframe'); + userSync.syncUsers(); + expect(triggerPixelStub.getCall(0)).to.be.null; + expect(insertUserSyncIframeStub.getCall(0)).to.not.be.null; + expect(insertUserSyncIframeStub.getCall(0).args[0]).to.equal('http://example.com/iframe'); + }); + + it('should throw a warning and default to basic resgistration rules when filterSettings config is invalid', () => { + // invalid config - passed invalid filter option + const userSync1 = newTestUserSync({ + filterSettings: { + iframe: { + bidders: ['testBidder'], + filter: 'includes' + } + } + }); + userSync1.registerSync('image', 'atestBidder', 'http://example.com/1'); + userSync1.registerSync('iframe', 'testBidder', 'http://example.com/iframe'); + userSync1.syncUsers(); + expect(logWarnStub.getCall(0).args[0]).to.exist; + expect(triggerPixelStub.getCall(0)).to.not.be.null; + expect(triggerPixelStub.getCall(0).args[0]).to.exist.and.to.equal('http://example.com/1'); + expect(insertUserSyncIframeStub.getCall(0)).to.be.null; + + // invalid config - bidders is not an array of strings + const userSync2 = newTestUserSync({ + filterSettings: { + iframe: { + bidders: ['testBidder', 0], + filter: 'include' + } + } + }); + userSync2.registerSync('image', 'atestBidder', 'http://example.com/1'); + userSync2.registerSync('iframe', 'testBidder', 'http://example.com/iframe'); + userSync2.syncUsers(); + expect(logWarnStub.getCall(1).args[0]).to.exist; + expect(triggerPixelStub.getCall(1)).to.not.be.null; + expect(triggerPixelStub.getCall(1).args[0]).to.exist.and.to.equal('http://example.com/1'); + expect(insertUserSyncIframeStub.getCall(0)).to.be.null; + + // invalid config - bidders list includes wildcard + const userSync3 = newTestUserSync({ + filterSettings: { + iframe: { + bidders: ['testBidder', '*'], + filter: 'include' + } + } + }); + userSync3.registerSync('image', 'atestBidder', 'http://example.com/1'); + userSync3.registerSync('iframe', 'testBidder', 'http://example.com/iframe'); + userSync3.syncUsers(); + expect(logWarnStub.getCall(2).args[0]).to.exist; + expect(triggerPixelStub.getCall(2)).to.not.be.null; + expect(triggerPixelStub.getCall(2).args[0]).to.exist.and.to.equal('http://example.com/1'); + expect(insertUserSyncIframeStub.getCall(0)).to.be.null; + + // invalid config - incorrect wildcard + const userSync4 = newTestUserSync({ + filterSettings: { + iframe: { + bidders: '***', + filter: 'include' + } + } + }); + userSync4.registerSync('image', 'atestBidder', 'http://example.com/1'); + userSync4.registerSync('iframe', 'testBidder', 'http://example.com/iframe'); + userSync4.syncUsers(); + expect(logWarnStub.getCall(3).args[0]).to.exist; + expect(triggerPixelStub.getCall(3)).to.not.be.null; + expect(triggerPixelStub.getCall(3).args[0]).to.exist.and.to.equal('http://example.com/1'); + expect(insertUserSyncIframeStub.getCall(0)).to.be.null; + + // invalid config - missing bidders field + const userSync5 = newTestUserSync({ + filterSettings: { + iframe: { + filter: 'include' + } + } + }); + userSync5.registerSync('image', 'atestBidder', 'http://example.com/1'); + userSync5.registerSync('iframe', 'testBidder', 'http://example.com/iframe'); + userSync5.syncUsers(); + expect(logWarnStub.getCall(4).args[0]).to.exist; + expect(triggerPixelStub.getCall(4)).to.not.be.null; + expect(triggerPixelStub.getCall(4).args[0]).to.exist.and.to.equal('http://example.com/1'); + expect(insertUserSyncIframeStub.getCall(0)).to.be.null; + }); + + it('should overwrite logic of deprecated fields when filterSettings is defined', () => { + const userSync = newTestUserSync({ + pixelsEnabled: false, + iframeEnabled: true, + enabledBidders: ['ctestBidder'], + filterSettings: { + image: { + bidders: '*', + filter: 'include' + }, + iframe: { + bidders: ['testBidder'], + filter: 'exclude' + } + } + }); + userSync.registerSync('image', 'atestBidder', 'http://example.com/1'); + userSync.registerSync('iframe', 'testBidder', 'http://example.com/iframe'); + userSync.syncUsers(); + expect(logWarnStub.getCall(0).args[0]).to.exist; + expect(triggerPixelStub.getCall(0)).to.not.be.null; + expect(triggerPixelStub.getCall(0).args[0]).to.exist.and.to.equal('http://example.com/1'); + expect(insertUserSyncIframeStub.getCall(0)).to.be.null; + }); });