Skip to content

Commit

Permalink
Core: Add Session Storage Manager & Contxtful RTD Provider: use sessi…
Browse files Browse the repository at this point in the history
…on storage (prebid#11928)

* feat: sessionstorage in storagemanager

* fix: use storage manager

* fix: lint

* fix: storage from rtd

* doc: no changes needed

* refactor storageManager/sessionStorage

---------

Co-authored-by: Demetrio Girardi <dgirardi@prebid.org>
  • Loading branch information
2 people authored and DecayConstant committed Jul 18, 2024
1 parent a2b0a48 commit 2daff19
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 128 deletions.
23 changes: 19 additions & 4 deletions modules/contxtfulRtdProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,19 @@ import {
isArray,
} from '../src/utils.js';
import { loadExternalScript } from '../src/adloader.js';
import { getStorageManager } from '../src/storageManager.js';
import { MODULE_TYPE_RTD } from '../src/activities/modules.js';

const MODULE_NAME = 'contxtful';
const MODULE = `${MODULE_NAME}RtdProvider`;

const CONTXTFUL_RECEPTIVITY_DOMAIN = 'api.receptivity.io';

const storageManager = getStorageManager({
moduleType: MODULE_TYPE_RTD,
moduleName: MODULE_NAME
});

let rxApi = null;
let isFirstBidRequestCall = true;

Expand All @@ -35,10 +42,19 @@ function getRxEngineReceptivity(requester) {
return rxApi?.receptivity(requester);
}

function getItemFromSessionStorage(key) {
let value = null;
try {
// Use the Storage Manager
value = storageManager.getDataFromSessionStorage(key, null);
} catch (error) {
}

return value;
}

function loadSessionReceptivity(requester) {
// TODO: commented out because of rule violations
/*
let sessionStorageValue = sessionStorage.getItem(requester);
let sessionStorageValue = getItemFromSessionStorage(requester);
if (!sessionStorageValue) {
return null;
}
Expand All @@ -56,7 +72,6 @@ function loadSessionReceptivity(requester) {
} catch {
return null;
}
*/
}

/**
Expand Down
1 change: 1 addition & 0 deletions modules/contxtfulRtdProvider.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ pbjs.setConfig({
}
});
```

## Parameters

| Name | Type | Scope | Description |
Expand Down
131 changes: 58 additions & 73 deletions src/storageManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,27 +88,6 @@ export function newStorageManager({moduleName, moduleType} = {}, {isAllowed = is
return schedule(cb, STORAGE_TYPE_COOKIES, done);
};

/**
* @returns {boolean}
*/
const localStorageIsEnabled = function (done) {
let cb = function (result) {
if (result && result.valid) {
try {
localStorage.setItem('prebid.cookieTest', '1');
return localStorage.getItem('prebid.cookieTest') === '1';
} catch (error) {
} finally {
try {
localStorage.removeItem('prebid.cookieTest');
} catch (error) {}
}
}
return false;
}
return schedule(cb, STORAGE_TYPE_LOCALSTORAGE, done);
}

/**
* @returns {boolean}
*/
Expand All @@ -122,60 +101,69 @@ export function newStorageManager({moduleName, moduleType} = {}, {isAllowed = is
return schedule(cb, STORAGE_TYPE_COOKIES, done);
}

/**
* @param {string} key
* @param {string} value
*/
const setDataInLocalStorage = function (key, value, done) {
let cb = function (result) {
if (result && result.valid && hasLocalStorage()) {
window.localStorage.setItem(key, value);
}
}
return schedule(cb, STORAGE_TYPE_LOCALSTORAGE, done);
}

/**
* @param {string} key
* @returns {(string|null)}
*/
const getDataFromLocalStorage = function (key, done) {
let cb = function (result) {
if (result && result.valid && hasLocalStorage()) {
return window.localStorage.getItem(key);
}
return null;
}
return schedule(cb, STORAGE_TYPE_LOCALSTORAGE, done);
}
function storageMethods(name) {
const capName = name.charAt(0).toUpperCase() + name.substring(1);
const backend = () => window[name];

/**
* @param {string} key
*/
const removeDataFromLocalStorage = function (key, done) {
let cb = function (result) {
if (result && result.valid && hasLocalStorage()) {
window.localStorage.removeItem(key);
const hasStorage = function (done) {
let cb = function (result) {
if (result && result.valid) {
try {
return !!backend();
} catch (e) {
logError(`${name} api disabled`);
}
}
return false;
}
return schedule(cb, STORAGE_TYPE_LOCALSTORAGE, done);
}
return schedule(cb, STORAGE_TYPE_LOCALSTORAGE, done);
}

/**
* @returns {boolean}
*/
const hasLocalStorage = function (done) {
let cb = function (result) {
if (result && result.valid) {
try {
return !!window.localStorage;
} catch (e) {
logError('Local storage api disabled');
return {
[`has${capName}`]: hasStorage,
[`${name}IsEnabled`](done) {
let cb = function (result) {
if (result && result.valid) {
try {
backend().setItem('prebid.cookieTest', '1');
return backend().getItem('prebid.cookieTest') === '1';
} catch (error) {
} finally {
try {
backend().removeItem('prebid.cookieTest');
} catch (error) {}
}
}
return false;
}
return schedule(cb, STORAGE_TYPE_LOCALSTORAGE, done);
},
[`setDataIn${capName}`](key, value, done) {
let cb = function (result) {
if (result && result.valid && hasStorage()) {
backend().setItem(key, value);
}
}
return schedule(cb, STORAGE_TYPE_LOCALSTORAGE, done);
},
[`getDataFrom${capName}`](key, done) {
let cb = function (result) {
if (result && result.valid && hasStorage()) {
return backend().getItem(key);
}
return null;
}
return schedule(cb, STORAGE_TYPE_LOCALSTORAGE, done);
},
[`removeDataFrom${capName}`](key, done) {
let cb = function (result) {
if (result && result.valid && hasStorage()) {
backend().removeItem(key);
}
}
return schedule(cb, STORAGE_TYPE_LOCALSTORAGE, done);
}
return false;
}
return schedule(cb, STORAGE_TYPE_LOCALSTORAGE, done);
}

/**
Expand Down Expand Up @@ -211,12 +199,9 @@ export function newStorageManager({moduleName, moduleType} = {}, {isAllowed = is
return {
setCookie,
getCookie,
localStorageIsEnabled,
cookiesAreEnabled,
setDataInLocalStorage,
getDataFromLocalStorage,
removeDataFromLocalStorage,
hasLocalStorage,
...storageMethods('localStorage'),
...storageMethods('sessionStorage'),
findSimilarCookies
}
}
Expand Down
119 changes: 68 additions & 51 deletions test/spec/unit/core/storageManager_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@ describe('storage manager', function() {
hook.ready();
});

beforeEach(function() {
beforeEach(function () {
resetData();
});

afterEach(function() {
afterEach(function () {
config.resetConfig();
})

it('should allow to set cookie for core modules without checking gdpr enforcements', function() {
it('should allow to set cookie for core modules without checking gdpr enforcements', function () {
const coreStorage = getCoreStorageManager();
let date = new Date();
date.setTime(date.getTime() + (24 * 60 * 60 * 1000));
Expand All @@ -43,7 +43,7 @@ describe('storage manager', function() {
expect(coreStorage.getCookie('hello')).to.equal('world');
});

it('should add done callbacks to storageCallbacks array', function() {
it('should add done callbacks to storageCallbacks array', function () {
let noop = sinon.spy();
const coreStorage = newStorageManager();

Expand All @@ -55,11 +55,15 @@ describe('storage manager', function() {
coreStorage.getDataFromLocalStorage('foo', noop);
coreStorage.removeDataFromLocalStorage('foo', noop);
coreStorage.hasLocalStorage(noop);
coreStorage.setDataInSessionStorage('foo', 'bar', noop);
coreStorage.getDataFromSessionStorage('foo', noop);
coreStorage.removeDataFromSessionStorage('foo', noop);
coreStorage.hasSessionStorage(noop);

expect(storageCallbacks.length).to.equal(8);
expect(storageCallbacks.length).to.equal(12);
});

it('should allow bidder to access device if gdpr enforcement module is not included', function() {
it('should allow bidder to access device if gdpr enforcement module is not included', function () {
let deviceAccessSpy = sinon.spy(utils, 'hasDeviceAccess');
const storage = newStorageManager();
storage.setCookie('foo1', 'baz1');
Expand Down Expand Up @@ -87,12 +91,16 @@ describe('storage manager', function() {
}));
});

it('should deny access if activity is denied', () => {
isAllowed.returns(false);
const mgr = mkManager(MODULE_TYPE_PREBID, 'mockMod');
mgr.setDataInLocalStorage('testKey', 'val');
expect(mgr.getDataFromLocalStorage('testKey')).to.not.exist;
});
['Local', 'Session'].forEach(type => {
describe(`${type} storage`, () => {
it('should deny access if activity is denied', () => {
isAllowed.returns(false);
const mgr = mkManager(MODULE_TYPE_PREBID, 'mockMod');
mgr[`setDataIn${type}Storage`]('testKey', 'val');
expect(mgr[`getDataFrom${type}Storage`]('testKey')).to.not.exist;
});
})
})

it('should use bidder aliases when possible', () => {
adapterManager.registerBidAdapter({callBids: sinon.stub(), getSpec: () => ({})}, 'mockBidder');
Expand All @@ -103,57 +111,66 @@ describe('storage manager', function() {
[ACTIVITY_PARAM_COMPONENT_NAME]: 'mockAlias'
}))
})
})

describe('localstorage forbidden access in 3rd-party context', function() {
let errorLogSpy;
let originalLocalStorage;
const localStorageMock = { get: () => { throw Error } };
});

beforeEach(function() {
originalLocalStorage = window.localStorage;
Object.defineProperty(window, 'localStorage', localStorageMock);
errorLogSpy = sinon.spy(utils, 'logError');
});
['localStorage', 'sessionStorage'].forEach(storage => {
const Storage = storage.charAt(0).toUpperCase() + storage.substring(1);

afterEach(function() {
Object.defineProperty(window, 'localStorage', { get: () => originalLocalStorage });
errorLogSpy.restore();
})
describe(`${storage} forbidden access in 3rd-party context`, function () {
let errorLogSpy;
let originalStorage;
const storageMock = {
get: () => {
throw Error
}
};

it('should not throw if the localstorage is not accessible when setting/getting/removing from localstorage', function() {
const coreStorage = newStorageManager();
beforeEach(function () {
originalStorage = window[storage];
Object.defineProperty(window, storage, storageMock);
errorLogSpy = sinon.spy(utils, 'logError');
});

coreStorage.setDataInLocalStorage('key', 'value');
const val = coreStorage.getDataFromLocalStorage('key');
coreStorage.removeDataFromLocalStorage('key');
afterEach(function () {
Object.defineProperty(window, storage, {get: () => originalStorage});
errorLogSpy.restore();
})

expect(val).to.be.null;
sinon.assert.calledThrice(errorLogSpy);
})
})
it('should not throw if storage is not accessible when setting/getting/removing', function () {
const coreStorage = newStorageManager();

describe('localstorage is enabled', function() {
let localStorage;
coreStorage[`setDataIn${Storage}`]('key', 'value');
const val = coreStorage[`getDataFrom${Storage}`]('key');
coreStorage[`removeDataFrom${Storage}`]('key');

beforeEach(function() {
localStorage = window.localStorage;
localStorage.clear();
expect(val).to.be.null;
sinon.assert.calledThrice(errorLogSpy);
});
});
});

afterEach(function() {
localStorage.clear();
})
['localStorage', 'sessionStorage'].forEach(storage => {
describe(`${storage} is enabled`, function () {
let store;
beforeEach(function () {
store = window[storage];
store.clear();
});

it('should remove side-effect after checking', function () {
const storage = newStorageManager();
afterEach(function () {
store.clear();
})

localStorage.setItem('unrelated', 'dummy');
const val = storage.localStorageIsEnabled();
it('should remove side-effect after checking', function () {
const storageMgr = newStorageManager();

expect(val).to.be.true;
expect(localStorage.length).to.be.eq(1);
expect(localStorage.getItem('unrelated')).to.be.eq('dummy');
store.setItem('unrelated', 'dummy');
const val = storageMgr[`${storage}IsEnabled`]();

expect(val).to.be.true;
expect(store.length).to.be.eq(1);
expect(store.getItem('unrelated')).to.be.eq('dummy');
});
});
});

Expand Down

0 comments on commit 2daff19

Please sign in to comment.