Source: Storage/storage.js

/**
 * @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');