/**
* @class
* @classdesc Plugin to manage stored data.
* @name Storage
* @memberOf ATInternet.Tracker.Plugins
* @type {function}
* @param tag {object} Instance of the Tag used
* @description
* This plugin allows the storage of elements such as strings, tables or objects.
* It uses serialization. For that reason the values must be <i>JSON</i> compatible.
* @public
*/
ATInternet.Tracker.Plugins.Storage = function (tag) {
'use strict';
var self = this;
var _config = {};
var _isLocalStorage = false;
// Reference of storage object
var Storage = null;
// Set plugin configuration.
tag.configPlugin('Storage', dfltPluginCfg || {}, function (newConf) {
_config = newConf;
if (_config.storageMode === 'localStorage') {
_isLocalStorage = ATInternet.Utils.isLocalStorageAvailable();
}
});
// Cache of items to avoid useless search in cookies or local storage.
var _itemCache = {};
/**
* Encode value depending on configuration mode
* @memberof ATInternet.Tracker.Plugins.Storage#
* @function
* @param value {*} element to encode
* @return {string}
* @private
*/
var _encodeValue = function (value) {
if (!!tag.getConfig('base64Storage')) {
return ATInternet.Utils.Base64.encode(value);
}
return encodeURIComponent(value);
};
/**
* Decode value depending on configuration mode
* @memberof ATInternet.Tracker.Plugins.Storage#
* @function
* @param value {*} element to decode
* @return {string}
* @private
*/
var _decodeValue = function (value) {
if (!!tag.getConfig('base64Storage')) {
return ATInternet.Utils.Base64.decode(value);
}
return decodeURIComponent(value);
};
/**
* Object used to manage local storage
* @name LocalStorage
* @memberof ATInternet.Tracker.Plugins.Storage#
* @inner
* @type {function}
* @property {function} getData Tag helper, see details here {@link ATInternet.Tracker.Tag#LocalStorage.getData}
* @property {function} setData Tag helper, see details here {@link ATInternet.Tracker.Tag#LocalStorage.setData}
* @private
*/
var LocalStorage = function () {
/**
* Process item in order to check validity
* @memberof ATInternet.Tracker.Plugins.Storage#
* @function
* @param item {object} Item to process
* @return {object}
* @private
*/
var _processTimestamp = function (item) {
var currentTimestamp = +new Date();
var toDelete = false;
var timestampOption;
if (item.options) {
if (typeof item.options.expires !== 'undefined') {
timestampOption = item.options.expires;
} else {
var end = item.options.end || {};
if (typeof end.getTime === 'function') {
timestampOption = end.getTime();
} else if (typeof end === 'number') {
timestampOption = currentTimestamp + (end * 1000);
}
}
}
if (typeof timestampOption === 'number') {
if (currentTimestamp >= timestampOption) {
toDelete = true;
}
}
return {
itemToDelete: toDelete,
timestamp: timestampOption
};
};
/**
* Delete item from local storage
* @memberof ATInternet.Tracker.Plugins.Storage#
* @function
* @param name {string} Name of the stored data to delete
* @return {boolean}
* @private
*/
var _deleteItemFromLocalStorage = function (name) {
var deleted = false;
try {
localStorage.removeItem(name);
deleted = true;
} catch (e) {
}
return deleted;
};
/**
* Get data from local storage
* @alias LocalStorage.getData
* @memberof! ATInternet.Tracker.Plugins.Storage#
* @function
* @param name {string} Name of the stored data
* @return {null|object}
* @private
*/
this.getData = function (name) {
var storedData = null;
var localStorageData = localStorage.getItem(name);
if (localStorageData) {
var decodedData = _decodeValue(localStorageData);
var item = ATInternet.Utils.jsonParse(decodedData);
if (item && typeof item === 'object') {
var processedTimestamp = _processTimestamp(item);
if (!processedTimestamp.itemToDelete || !_deleteItemFromLocalStorage(name)) {
delete item.options.expires;
storedData = ATInternet.Utils.jsonSerialize(item);
}
} else {
storedData = decodedData;
}
}
/* @if debug */
tag.debug('Storage:Get:localStorage', 'DEBUG', '', {
name: name,
value: storedData
});
/* @endif */
return storedData;
};
/**
* Set data into local storage
* @alias LocalStorage.setData
* @memberof! ATInternet.Tracker.Plugins.Storage#
* @function
* @param item {object} Item to store
* @return {boolean}
* @private
*/
this.setData = function (item) {
var setData = false;
if (item.name && typeof item.name === 'string') {
var processedTimestamp = _processTimestamp(item);
if (typeof processedTimestamp.timestamp === 'number') {
item.options.expires = processedTimestamp.timestamp;
}
var dataToStore = ATInternet.Utils.jsonSerialize(item);
if (processedTimestamp.itemToDelete) {
setData = _deleteItemFromLocalStorage(item.name);
} else {
try {
localStorage.setItem(item.name, _encodeValue(dataToStore));
// Set a boolean that indicates if the data has been stored
setData = true;
} catch (e) {
}
}
/* @if debug */
if (setData) {
tag.debug('Storage:Set:localStorage:Success', 'DEBUG', '', {
name: item.name,
value: dataToStore
});
} else {
tag.debug('Storage:Set:localStorage:Error', 'ERROR', 'Cannot set data in local storage', {
name: item.name,
value: dataToStore
});
}
/* @endif */
}
return setData;
};
// For unit tests on private elements !!!
/* @if test */
this._processTimestamp = _processTimestamp;
this._deleteItemFromLocalStorage = _deleteItemFromLocalStorage;
/* @endif */
};
/**
* Object used to manage cookies
* @name Cookie
* @memberof ATInternet.Tracker.Plugins.Storage#
* @inner
* @type {function}
* @property {function} getData Tag helper, see details here {@link ATInternet.Tracker.Tag#Cookie.getData}
* @property {function} setData Tag helper, see details here {@link ATInternet.Tracker.Tag#Cookie.setData}
* @private
*/
var Cookie = function () {
/**
* Get data from document.cookie
* @alias Cookie.getData
* @memberof! ATInternet.Tracker.Plugins.Storage#
* @function
* @param name {string} Name of the stored data
* @return {null|object}
* @private
*/
this.getData = function (name) {
var storedData = null;
var regExp = new RegExp('(?:^| )' + name + '=([^;]+)');
var cookieData = regExp.exec(document.cookie) || null;
if (cookieData) {
storedData = _decodeValue(cookieData[1]);
}
/* @if debug */
tag.debug('Storage:Get:cookie', 'DEBUG', '', {
name: name,
value: storedData
});
/* @endif */
return storedData;
};
/**
* Set data into document.cookie
* @alias Cookie.setData
* @memberof! ATInternet.Tracker.Plugins.Storage#
* @function
* @param item {object} Item to store
* @return {boolean}
* @private
*/
this.setData = function (item) {
var dataHasBeenStored = false;
if (item.name && typeof item.name === 'string') {
var options = item.options || {};
var end = options.end || {};
var domain = options.domain || tag.getConfig('cookieDomain');
var secure = options.secure || tag.getConfig('cookieSecure');
var dataToStore = ATInternet.Utils.jsonSerialize(item);
var cookie = item.name + '=' + _encodeValue(dataToStore);
cookie += options.path && typeof options.path === 'string' ? ';path=' + options.path : '';
cookie += domain && typeof domain === 'string' ? ';domain=' + domain : '';
cookie += secure && typeof secure === 'boolean' ? ';secure' : '';
if (typeof end.toUTCString === 'function') {
cookie += ';expires=' + end.toUTCString();
} else if (typeof end === 'number') {
cookie += ';max-age=' + end.toString();
}
document.cookie = cookie;
if (this.getData(item.name)) {
// Set a boolean that indicates if the data has been stored
dataHasBeenStored = true;
}
/* @if debug */
if (dataHasBeenStored) {
tag.debug('Storage:Set:cookie:Success', 'DEBUG', '', {
name: item.name,
value: dataToStore
});
} else {
tag.debug('Storage:Set:cookie:Error', 'ERROR', 'Cannot set data in cookie', {
name: item.name,
value: dataToStore
});
}
/* @endif */
}
return dataHasBeenStored;
};
};
/**
* Get storage object depending on configuration (LocalStorage, Cookie, etc.)
* @memberof ATInternet.Tracker.Plugins.Storage#
* @function
* @return {object}
* @private
*/
var _getStorageObject = function () {
if (_isLocalStorage) {
return new LocalStorage();
} else {
return new Cookie();
}
};
// Init storage object
Storage = _getStorageObject();
/**
* Get the value of a stored data
* @name _getStorageData
* @memberof ATInternet.Tracker.Plugins.Storage#
* @function
* @param name {string} Name of the stored data wanted
* @return {string|null} Return null if the stored data does not exist
* @private
*/
var _getStorageData = function (name) {
var storedData = null;
if (!tag.getConfig('disableCookie') && !tag.getConfig('disableStorage') && name && typeof name === 'string') {
storedData = Storage.getData(name);
}
return storedData;
};
/**
* Create, update or erase a stored data
* @memberof ATInternet.Tracker.Plugins.Storage#
* @name _setStorageData
* @function
* @param item {object} Data to create/erase
* @param force {boolean} Force cookie writing when "consent page mode" is activated
* @return {boolean} Returns true if the data has been set
* @private
*/
var _setStorageData = function (item, force) {
var setData = false;
// If force option is true then cookies must be set
if (item && typeof item === 'object') {
if (force || (ATInternet.Utils.consent && !tag.getConfig('disableCookie') && !tag.getConfig('disableStorage'))) {
setData = Storage.setData(item);
}
}
return setData;
};
/**
* Create an item to store
* @memberof ATInternet.Tracker.Plugins.Storage#
* @function
* @param name {string} Name of the Item
* @param val {*} element to stock, JSON compatible
* @param options {object} Object which contains all stored data and items options
* @return {object}
* @private
*/
var _createItem = function (name, val, options) {
var item = {
name: name,
val: val
};
if (options && options.session && typeof options.session === 'number') {
options.end = options.session;
}
item.options = options || {};
return item;
};
/**
* Parse the data obtained from storage
* @memberof ATInternet.Tracker.Plugins.Storage#
* @function
* @param name {string} Name of the stored data
* @return {object|null}
* @private
*/
var _getItem = function (name) {
var item = null;
var storedItem = _getStorageData(name);
if (storedItem) {
item = ATInternet.Utils.jsonParse(storedItem);
}
return item;
};
/**
* Get a specific property from an object (item)
* @memberof ATInternet.Tracker.Plugins.Storage#
* @function
* @param item {object} Stored data (json-parsed)
* @param prop {string} Name of the property
* @return {*}
* @private
*/
var _getItemProperty = function (item, prop) {
var value = null;
if (item && typeof item.val === 'object' && !(item.val instanceof Array) && item.val[prop] !== undefined) {
value = item.val[prop];
}
return value;
};
/**
* Store an object (item). Replace the stored data if exists.
* @memberof ATInternet.Tracker.Plugins.Storage#
* @function
* @param item {object} Data to store (json-parsed)
* @param force {boolean} Force cookie writing when "consent page mode" is activated
* @return {object|null}
* @private
*/
var _setItem = function (item, force) {
var clonedItem = ATInternet.Utils.cloneSimpleObject(item);
if (_setStorageData(clonedItem, force)) {
return ATInternet.Utils.jsonParse(ATInternet.Utils.jsonSerialize(item));
}
return null;
};
/**
* Set or update the property of an object (item).
* @memberof ATInternet.Tracker.Plugins.Storage#
* @function
* @param item {object} Data to process (json-parsed)
* @param prop {string} name of the property to set
* @param val {*} value of the property to set
* @return {*}
* @private
*/
var _setItemProperty = function (item, prop, val) {
if (item && typeof item.val === 'object' && !(item.val instanceof Array)) {
if (typeof val === 'undefined') {
delete item.val[prop];
} else {
item.val[prop] = val;
}
return item;
}
return null;
};
/**
* Get the value of an object (item) or a property from storage. Begin to search in the cache and then
* in the cookie or local storage if not found.
* @memberof ATInternet.Tracker.Plugins.Storage#
* @function
* @param name {string} name of the element
* @param prop {string} name of the property
* @param byPassCache {boolean} If true, the value will be directly retrieved from cookie or local storage depending on configuration
* @return {*}
* @private
*/
var _get = function (name, prop, byPassCache) {
var item;
// We try to get the item from the cache first
if (!byPassCache && _itemCache[name]) {
item = _itemCache[name];
/* @if debug */
tag.debug('Storage:Get:cache', 'DEBUG', '', {
name: name,
value: item
});
/* @endif */
} else {
item = _getItem(name);
if (item) {
item.options = item.options || {};
// If the session is valid, we must extend the expiration by rewriting the stored data
if (item.options.session && typeof item.options.session === 'number') {
item.options.end = item.options.session;
_setItem(item, false);
}
// Put it in the cache
_itemCache[name] = item;
}
}
if (item) {
if (prop) {
// If prop is defined we return it if exists
return _getItemProperty(item, prop);
} else {
// Otherwise we return the value of the item
return item.val;
}
}
return null;
};
/**
* Create, update or erase an object or one of its properties
* @memberof ATInternet.Tracker.Plugins.Storage#
* @function
* @param name {string} name of the data to store
* @param prop {string|null} name of the property (may be null)
* @param val {*} value of the element or of the property if exists
* @param force {boolean} Force cookie writing when "consent page mode" is activated
* @param options {object} Object which contains options. Useless if you create or update a property.
* @private
*/
var _set = function (name, prop, val, force, options) {
var item;
if (prop) {
item = _getItem(name);
if (item) {
item = _setItemProperty(item, prop, val);
if (item) {
item = _setItem(item, force);
}
}
} else {
options = options || {};
item = _createItem(name, val, options);
item = _setItem(item, force);
}
if (item) {
_itemCache[name] = item;
return item.val;
}
return null;
};
/**
* Delete an object or its property in the linked stored data
* @memberof ATInternet.Tracker.Plugins.Storage#
* @function
* @param name {string} name of the stored data
* @param prop {string} name of the property
* @private
*/
var _del = function (name, prop) {
if (prop) {
_set(name, prop, undefined, true, null);
} else {
_itemCache[name] = undefined;
var item = _createItem(name, '', {end: new Date('Thu, 01 Jan 1970 00:00:00 UTC'), path: '/'});
_setStorageData(item, true);
}
};
/**
* [Object added by plugin {@link ATInternet.Tracker.Plugins.Storage Storage}] Methods to manage data to store.
* @name storage
* @memberof ATInternet.Tracker.Tag
* @inner
* @type {object}
* @property {function} get Tag helper, see details here {@link ATInternet.Tracker.Tag#storage.get}
* @property {function} getPrivate Tag helper, see details here {@link ATInternet.Tracker.Tag#storage.getPrivate}
* @property {function} set Tag helper, see details here {@link ATInternet.Tracker.Tag#storage.set}
* @property {function} setPrivate Tag helper, see details here {@link ATInternet.Tracker.Tag#storage.setPrivate}
* @property {function} del Tag helper, see details here {@link ATInternet.Tracker.Tag#storage.del}
* @property {function} delPrivate Tag helper, see details here {@link ATInternet.Tracker.Tag#storage.delPrivate}
* @property {function} cacheInvalidation Tag helper, see details here {@link ATInternet.Tracker.Tag#storage.cacheInvalidation}
* @public
*/
tag.storage = {};
/**
* Retrieve all stored data.
* @name getAll
* @memberof ATInternet.Tracker.Plugins.Storage#
* @function
* @return {Object}
* @public
*/
tag.storage.getAll = function () {
return _itemCache;
};
/**
* Retrieve a stored data or a its property.
* @name get
* @memberof ATInternet.Tracker.Plugins.Storage#
* @function
* @param key {string|Array} If a string, the data's name, if an array the first element is the data's name and the second is the property's name
* @param [byPassCache] {boolean} If true, the value will be directly retrieved from storage
* @return {*} Return null if the element does not exist
* @public
*/
tag.storage.get = self.get = function (key, byPassCache) {
byPassCache = !!byPassCache;
if (key instanceof Array) {
return _get(key[0], key[1], byPassCache);
} else {
return _get(key, '', byPassCache);
}
};
/**
* Same as the 'get' method but for a private stored data linked to the current numsite.
* @name getPrivate
* @memberof ATInternet.Tracker.Plugins.Storage#
* @function
* @param key {string|Array} If a string, the data's name, if an array the first element is the data's name and the second is the property's name
* @param [byPassCache] {boolean} If true, the value will be directly retrieved from cookie or local storage
* @return {*} Return null if the element does not exist
* @public
*/
tag.storage.getPrivate = self.getPrivate = function (key, byPassCache) {
if (key instanceof Array) {
key[0] = key[0] + tag.getConfig('site');
} else {
key = key + tag.getConfig('site');
}
return self.get(key, byPassCache);
};
/**
* Create, update or erase a stored data or its property.
* @name set
* @memberof ATInternet.Tracker.Plugins.Storage#
* @function
* @param key {string|Array} If a string, name the element you want to create/update, if an array the first element is the data's name and the second is its property
* @param val {*} Value to save
* @param options {object} Object which contains options. Useless if you create or update a property.
* @param force {boolean} Force cookie writing when "consent page mode" is activated
* @return {boolean|null} Return true if the data has been set
* @example
* <pre><code class="javascript">set('cookie', 'value', {end: '3600', domain: '.site.com', path: '/', secure: true});
* set('cookie', [1, 2, 3], {end: '3600'});
* set('cookie', {'myvar1': 1, 'myvar2': 2}, {session: '3600'});
* set(['cookie', 'myvar2'], 3);
*
* var date = new Date;
* date.setMinutes(date.getMinutes() + 30);
* set('cookie', 'value', {end: date.getMinutes() + 30});
* </code></pre>
* @public
*/
tag.storage.set = self.set = function (key, val, options, force) {
var _param, _prop, _options;
if (key instanceof Array) {
_param = key[0];
_prop = key[1];
_options = null;
} else {
_param = key;
_prop = null;
_options = options;
}
var storageParam = ATInternet.Utils.privacy.testStorageParam(_param, _prop);
if (storageParam.toSetInStorage || force) {
return _set(_param, _prop, val, force, _options);
} else {
return null;
}
};
/**
* Same as the 'set' method but for a private data linked to the current numsite
* @name setPrivate
* @memberof ATInternet.Tracker.Plugins.Storage#
* @function
* @return {boolean|null} Return true if the data has been set
* @public
*/
tag.storage.setPrivate = self.setPrivate = function (key, val, options) {
if (key instanceof Array) {
key[0] = key[0] + tag.getConfig('site');
} else {
key = key + tag.getConfig('site');
}
return self.set(key, val, options);
};
/**
* Delete a data or its property
* @name del
* @memberof ATInternet.Tracker.Plugins.Storage#
* @function
* @param key {string|Array} If a string, name the element you wan to create/update, if an array the first element is the data's name and the second is its property
* @public
*/
tag.storage.del = self.del = function (key) {
if (key instanceof Array) {
_del(key[0], key[1]);
} else {
_del(key, '');
}
};
/**
* Same as the 'del' method but for a private data linked to the current numsite
* @name delPrivate
* @memberof ATInternet.Tracker.Plugins.Storage#
* @function
* @param key {string|Array}
* @public
*/
tag.storage.delPrivate = self.delPrivate = function (key) {
if (key instanceof Array) {
key[0] = key[0] + tag.getConfig('site');
} else {
key = key + tag.getConfig('site');
}
self.del(key);
};
/**
* Clear the cache
* @name cacheInvalidation
* @memberof ATInternet.Tracker.Plugins.Storage#
* @function
* @public
*/
tag.storage.cacheInvalidation = self.cacheInvalidation = function () {
_itemCache = {};
/* @if debug */
tag.debug('Storage:storage:cacheInvalidation', 'DEBUG', '');
/* @endif */
};
// For unit tests on private elements !!!
/* @if test */
self._encodeValue = _encodeValue;
self._decodeValue = _decodeValue;
self.LocalStorage = LocalStorage;
self.Cookie = Cookie;
self._getStorageObject = _getStorageObject;
self.Storage = Storage;
self._getStorageData = _getStorageData;
self._setStorageData = _setStorageData;
self._createItem = _createItem;
self._getItem = _getItem;
self._getItemProperty = _getItemProperty;
self._setItem = _setItem;
self._setItemProperty = _setItemProperty;
self._get = _get;
self._set = _set;
self._del = _del;
self._getCache = function () {
return _itemCache
};
/* @endif */
};
ATInternet.Tracker.addPlugin('Storage');