Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adagio Bid Adapter: external script compliance #11351

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 33 additions & 51 deletions modules/adagioBidAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import {
import {config} from '../src/config.js';
import {registerBidder} from '../src/adapters/bidderFactory.js';
import {loadExternalScript} from '../src/adloader.js';
import {verify} from 'criteo-direct-rsa-validate/build/verify.js';
import {getStorageManager} from '../src/storageManager.js';
import {getRefererInfo, parseDomain} from '../src/refererDetection.js';
import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js';
Expand All @@ -33,14 +32,20 @@ import { getGlobal } from '../src/prebidGlobal.js';
import { convertOrtbRequestToProprietaryNative } from '../src/native.js';
import { userSync } from '../src/userSync.js';
import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js';
import {bidderSettings} from '../src/bidderSettings.js';

const BIDDER_CODE = 'adagio';
const LOG_PREFIX = 'Adagio:';
const FEATURES_VERSION = '1';
export const ENDPOINT = 'https://mp.4dex.io/prebid';
const SUPPORTED_MEDIA_TYPES = [BANNER, NATIVE, VIDEO];
const ADAGIO_TAG_URL = 'https://script.4dex.io/localstore.js';
const ADAGIO_LOCALSTORAGE_KEY = 'adagioScript';

const ADAGIOJS_VERSION_LOCK = '2.0.2';
const ADAGIOJS_VERSION_LATEST = 'latest';
const ADAGIOJS_VERSION_NONE = 'none';
const ADAGIOJS_VERSION_PLACEHOLDER = '%ADAGIOJS_VERSION%';
const PUBLISHER_TAG_URL_TEMPLATE = 'https://script.4dex.io/a' + ADAGIOJS_VERSION_PLACEHOLDER + '/adagio.js';

const GVLID = 617;
export const storage = getStorageManager({bidderCode: BIDDER_CODE});

Expand All @@ -49,8 +54,6 @@ const BB_RENDERER_DEFAULT = 'renderer';
export const BB_RENDERER_URL = `https://${BB_PUBLICATION}.bbvms.com/r/$RENDERER.js`;

const MAX_SESS_DURATION = 30 * 60 * 1000;
const ADAGIO_PUBKEY = 'AL16XT44Sfp+8SHVF1UdC7hydPSMVLMhsYknKDdwqq+0ToDSJrP0+Qh0ki9JJI2uYm/6VEYo8TJED9WfMkiJ4vf02CW3RvSWwc35bif2SK1L8Nn/GfFYr/2/GG/Rm0vUsv+vBHky6nuuYls20Og0HDhMgaOlXoQ/cxMuiy5QSktp';
const ADAGIO_PUBKEY_E = 65537;
const CURRENCY = 'USD';

// This provide a whitelist and a basic validation of OpenRTB 2.5 options used by the Adagio SSP.
Expand Down Expand Up @@ -145,58 +148,38 @@ export const GlobalExchange = (function() {
};
})();

export function adagioScriptFromLocalStorageCb(ls) {
try {
if (!ls) {
logWarn(`${LOG_PREFIX} script not found.`);
return;
}

const hashRgx = /^(\/\/ hash: (.+)\n)(.+\n)$/;

if (!hashRgx.test(ls)) {
logWarn(`${LOG_PREFIX} no hash found.`);
storage.removeDataFromLocalStorage(ADAGIO_LOCALSTORAGE_KEY);
} else {
const r = ls.match(hashRgx);
const hash = r[2];
const content = r[3];
/**
* @param {string|number} adagiojsVersion The external adagio.js script version to use
* @returns {string} https://script.4dex.io/a[/VERSION]/adagio.js
*/
export function computeAdagioScriptUrl(adagiojsVersion) {
let versionDir = `/${ADAGIOJS_VERSION_LOCK}`;
const semverRgx = /^\d+\.\d+\.\d+$/;

if (verify(content, hash, ADAGIO_PUBKEY, ADAGIO_PUBKEY_E)) {
logInfo(`${LOG_PREFIX} start script.`);
Function(ls)(); // eslint-disable-line no-new-func
} else {
logWarn(`${LOG_PREFIX} invalid script found.`);
storage.removeDataFromLocalStorage(ADAGIO_LOCALSTORAGE_KEY);
}
}
} catch (err) {
logError(LOG_PREFIX, err);
if (adagiojsVersion === ADAGIOJS_VERSION_LATEST || semverRgx.test(adagiojsVersion)) {
versionDir = `/${adagiojsVersion}`;
} else {
logInfo(`${LOG_PREFIX} Invalid adagio.js version. Using the default version.`);
}

return PUBLISHER_TAG_URL_TEMPLATE.replace(ADAGIOJS_VERSION_PLACEHOLDER, versionDir);
}

export function getAdagioScript() {
storage.getDataFromLocalStorage(ADAGIO_LOCALSTORAGE_KEY, (ls) => {
internal.adagioScriptFromLocalStorageCb(ls);
});

storage.localStorageIsEnabled(isValid => {
if (isValid) {
loadExternalScript(ADAGIO_TAG_URL, BIDDER_CODE);
} else {
// Try-catch to avoid error when 3rd party cookies is disabled (e.g. in privacy mode)
try {
// ensure adagio removing for next time.
// It's an antipattern regarding the TCF2 enforcement logic
// but it's the only way to respect the user choice update.
window.localStorage.removeItem(ADAGIO_LOCALSTORAGE_KEY);
// Extra data from external script.
// This key is removed only if localStorage is not accessible.
window.localStorage.removeItem('adagio');
} catch (e) {
logInfo(`${LOG_PREFIX} unable to clear Adagio scripts from localstorage.`);
}
if (!isValid) {
return
}

const adagiojsVersion = bidderSettings.get('adagio', 'scriptVersion') || config.getConfig('adagio.scriptVersion');

// If the publisher explicitly set the version to 'none', we don't load anything.
if (adagiojsVersion === ADAGIOJS_VERSION_NONE) {
return;
}

const url = computeAdagioScriptUrl(adagiojsVersion);
loadExternalScript(url, BIDDER_CODE, undefined, undefined, { id: `adagiojs-${getUniqueIdentifierStr()}`, version: adagiojsVersion });
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we'll need to discuss if we're going to let a bidder do this, likely we will not, as that was the initial plan. Why not distribute your sidecar script independently of prebid or make it another module?

});
}

Expand Down Expand Up @@ -362,7 +345,6 @@ export const internal = {
getSite,
getElementFromTopWindow,
getRefererInfo,
adagioScriptFromLocalStorageCb,
getCurrentWindow,
canAccessTopWindow,
isRendererPreferredFromPublisher,
Expand Down
8 changes: 6 additions & 2 deletions modules/adagioBidAdapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,17 @@ pbjs.setConfig({

### Bidder Settings

The Adagio bid adapter uses browser local storage. Since Prebid.js 7.x, the access to it must be explicitly set.
- `storageAllowed`: Adagio uses browser local storage, explicit access to it must be configured _(since Prebid.js 7.x)_
- `scriptVersion`: Adagio loads an additional script used for measurement. By default the adapter loads a specific version of this script, defined in the adapter itself. Two values can override this behavior:
- `latest`: allow to always get the more recent version of the script
- `none`: deactivates the feature, no script is loaded
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please make this the default


```js
// https://docs.prebid.org/dev-docs/publisher-api-reference/bidderSettings.html
pbjs.bidderSettings = {
adagio: {
storageAllowed: true
storageAllowed: true,
scriptVersion: 'latest'
}
}
```
Expand Down
124 changes: 40 additions & 84 deletions test/spec/modules/adagioBidAdapter_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1760,29 +1760,8 @@ describe('Adagio bid adapter', () => {
});
});

describe('adagioScriptFromLocalStorageCb()', function() {
const VALID_HASH = 'Lddcw3AADdQDrPtbRJkKxvA+o1CtScGDIMNRpHB3NnlC/FYmy/9RKXelKrYj/sjuWusl5YcOpo+lbGSkk655i8EKuDiOvK6ae/imxSrmdziIp+S/TA6hTFJXcB8k1Q9OIp4CMCT52jjXgHwX6G0rp+uYoCR25B1jHaHnpH26A6I=';
const INVALID_HASH = 'invalid';
const VALID_SCRIPT_CONTENT = 'var _ADAGIO=function(){};(_ADAGIO)();\n';
const INVALID_SCRIPT_CONTENT = 'var _ADAGIO=function(){//corrupted};(_ADAGIO)();\n';
const ADAGIO_LOCALSTORAGE_KEY = 'adagioScript';

beforeEach(function() {
localStorage.removeItem(ADAGIO_LOCALSTORAGE_KEY);
});

describe('getAdagioScript', function() {
it('should run storage.getDataFromLocalStorage callback and call adagioScriptFromLocalStorageCb() ', function() {
sandbox.spy(adagio, 'adagioScriptFromLocalStorageCb');
const getDataFromLocalStorageStub = sandbox.stub(storage, 'getDataFromLocalStorage').callsArg(1);
localStorage.setItem(ADAGIO_LOCALSTORAGE_KEY, '// hash: ' + VALID_HASH + '\n' + VALID_SCRIPT_CONTENT);

getAdagioScript();

sinon.assert.callCount(getDataFromLocalStorageStub, 1);
sinon.assert.callCount(adagio.adagioScriptFromLocalStorageCb, 1);
});

describe('adagio external script loading', function() {
describe('external script loading regarding the user consent storageAllowed flag', function() {
it('should load external script if the user consent', function() {
sandbox.stub(storage, 'localStorageIsEnabled').callsArgWith(0, true);
getAdagioScript();
Expand All @@ -1796,75 +1775,52 @@ describe('Adagio bid adapter', () => {

expect(loadExternalScript.called).to.be.false;
});
})

it('should remove the localStorage key if exists and the user does not consent', function() {
sandbox.stub(storage, 'localStorageIsEnabled').callsArgWith(0, false);
localStorage.setItem(ADAGIO_LOCALSTORAGE_KEY, 'the script');

getAdagioScript();

expect(loadExternalScript.called).to.be.false;
expect(localStorage.getItem(ADAGIO_LOCALSTORAGE_KEY)).to.be.null;
describe('getAdagioScript with expected version accordingly to bidderSettings config', function() {
beforeEach(function() {
sandbox.stub(storage, 'localStorageIsEnabled').callsArgWith(0, true);
});
});

it('should verify valid hash with valid script', function () {
localStorage.setItem(ADAGIO_LOCALSTORAGE_KEY, '// hash: ' + VALID_HASH + '\n' + VALID_SCRIPT_CONTENT);

utilsMock.expects('logInfo').withExactArgs('Adagio: start script.').once();
utilsMock.expects('logWarn').withExactArgs('Adagio: no hash found.').never();
utilsMock.expects('logWarn').withExactArgs('Adagio: invalid script found.').never();

adagioScriptFromLocalStorageCb(localStorage.getItem(ADAGIO_LOCALSTORAGE_KEY));

expect(localStorage.getItem(ADAGIO_LOCALSTORAGE_KEY)).to.equals('// hash: ' + VALID_HASH + '\n' + VALID_SCRIPT_CONTENT);
utilsMock.verify();
});

it('should verify valid hash with invalid script', function () {
localStorage.setItem(ADAGIO_LOCALSTORAGE_KEY, '// hash: ' + VALID_HASH + '\n' + INVALID_SCRIPT_CONTENT);

utilsMock.expects('logInfo').withExactArgs('Adagio: start script').never();
utilsMock.expects('logWarn').withExactArgs('Adagio: no hash found.').never();
utilsMock.expects('logWarn').withExactArgs('Adagio: invalid script found.').once();

adagioScriptFromLocalStorageCb(localStorage.getItem(ADAGIO_LOCALSTORAGE_KEY));

expect(localStorage.getItem(ADAGIO_LOCALSTORAGE_KEY)).to.be.null;
utilsMock.verify();
});
const lockedVersion = '2.0.2';

it('should verify invalid hash with valid script', function () {
localStorage.setItem(ADAGIO_LOCALSTORAGE_KEY, '// hash: ' + INVALID_HASH + '\n' + VALID_SCRIPT_CONTENT);

utilsMock.expects('logInfo').withExactArgs('Adagio: start script').never();
utilsMock.expects('logWarn').withExactArgs('Adagio: no hash found.').never();
utilsMock.expects('logWarn').withExactArgs('Adagio: invalid script found.').once();

adagioScriptFromLocalStorageCb(localStorage.getItem(ADAGIO_LOCALSTORAGE_KEY));

expect(localStorage.getItem(ADAGIO_LOCALSTORAGE_KEY)).to.be.null;
utilsMock.verify();
});

it('should verify missing hash', function () {
localStorage.setItem(ADAGIO_LOCALSTORAGE_KEY, VALID_SCRIPT_CONTENT);
it('loads a determined version', function() {
const version = '2.0.5';
$$PREBID_GLOBAL$$.bidderSettings.adagio.scriptVersion = version;
getAdagioScript();
expect(loadExternalScript.called).to.be.true;
expect(loadExternalScript.args[0][0]).to.deep.equal(`https://script.4dex.io/a/${version}/adagio.js`);
});

utilsMock.expects('logInfo').withExactArgs('Adagio: start script').never();
utilsMock.expects('logWarn').withExactArgs('Adagio: no hash found.').once();
utilsMock.expects('logWarn').withExactArgs('Adagio: invalid script found.').never();
it('loads the latest external script, bypass the lock version', function() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure i'm comfortable with this, we'll need to talk in committee

const version = 'latest';
$$PREBID_GLOBAL$$.bidderSettings.adagio.scriptVersion = version;
getAdagioScript();
expect(loadExternalScript.called).to.be.true;
expect(loadExternalScript.args[0][0]).to.deep.equal(`https://script.4dex.io/a/${version}/adagio.js`);
});

adagioScriptFromLocalStorageCb(localStorage.getItem(ADAGIO_LOCALSTORAGE_KEY));
it('loads the locked version, bad version provided', function() {
const version = 'false';
$$PREBID_GLOBAL$$.bidderSettings.adagio.scriptVersion = version;
getAdagioScript();
expect(loadExternalScript.called).to.be.true;
expect(loadExternalScript.args[0][0]).to.deep.equal(`https://script.4dex.io/a/${lockedVersion}/adagio.js`);
});

expect(localStorage.getItem(ADAGIO_LOCALSTORAGE_KEY)).to.be.null;
utilsMock.verify();
});
it('loads nothing', function() {
const version = 'none';
$$PREBID_GLOBAL$$.bidderSettings.adagio.scriptVersion = version;
getAdagioScript();
expect(loadExternalScript.called).to.be.false;
});

it('should return false if content script does not exist in localStorage', function() {
sandbox.spy(utils, 'logWarn');
expect(adagioScriptFromLocalStorageCb(null)).to.be.undefined;
sinon.assert.callCount(utils.logWarn, 1);
sinon.assert.calledWith(utils.logWarn, 'Adagio: script not found.');
it('loads the locked version - default behavior', function() {
delete $$PREBID_GLOBAL$$.bidderSettings.adagio.scriptVersion;
getAdagioScript();
expect(loadExternalScript.called).to.be.true;
expect(loadExternalScript.args[0][0]).to.deep.equal(`https://script.4dex.io/a/${lockedVersion}/adagio.js`);
});
});
});
});