Source: jsxc.lib.storage.js

/**
 * Handle long-live data
 *
 * @namespace jsxc.storage
 */
jsxc.storage = {
   /**
    * Prefix for localstorage
    *
    * @privat
    */
   PREFIX: 'jsxc',

   SEP: ':',

   hasSupport: function() {
      if (typeof localStorage === 'undefined' || localStorage === null) {
         return false;
      }

      try {
         localStorage.setItem('jsxc:storage:test', 'jsxc');
         localStorage.removeItem('jsxc:storage:test');
      } catch (err) {
         jsxc.warn('Can not save any data. Probably your quota exceeded or you use Safari in private Mode:', (err) ? err.message : undefined);
         return false;
      }

      return true;
   },

   /**
    * @param {type} uk Should we generate a user prefix?
    * @returns {String} prefix
    * @memberOf jsxc.storage
    */
   getPrefix: function(uk) {
      var self = jsxc.storage;

      if (uk && !jsxc.bid) {
         jsxc.warn('Unable to create user prefix');
      }

      return self.PREFIX + self.SEP + ((uk && jsxc.bid) ? jsxc.bid + self.SEP : '');
   },

   /**
    * Save item to storage
    *
    * @function
    * @param {String} key variablename
    * @param {Object} value value
    * @param {String} uk Userkey? Should we add the bid as prefix?
    */
   setItem: function(key, value, uk) {

      // Workaround for non-conform browser
      if (jsxc.storageNotConform > 0 && key !== 'rid') {
         if (jsxc.storageNotConform > 1 && jsxc.toSNC === null) {
            jsxc.toSNC = window.setTimeout(function() {
               jsxc.storageNotConform = 0;
               jsxc.storage.setItem('storageNotConform', 0);
            }, 1000);
         }

         jsxc.ls.push(JSON.stringify({
            key: key,
            value: value
         }));
      }

      if (typeof(value) === 'object') {
         // exclude jquery objects, because otherwise safari will fail
         value = JSON.stringify(value, function(key, val) {
            if (!(val instanceof jQuery)) {
               return val;
            }
         });
      }

      try {
         localStorage.setItem(jsxc.storage.getPrefix(uk) + key, value);
      } catch (err) {
         jsxc.error('An error occured while saving data.', (err) ? err.message : undefined);
      }
   },

   setUserItem: function(type, key, value) {
      var self = jsxc.storage;

      if (arguments.length === 2) {
         value = key;
         key = type;
         type = '';
      } else if (arguments.length === 3) {
         key = type + self.SEP + key;
      }

      return jsxc.storage.setItem(key, value, true);
   },

   /**
    * Load item from storage
    *
    * @function
    * @param {String} key variablename
    * @param {String} uk Userkey? Should we add the bid as prefix?
    */
   getItem: function(key, uk) {
      key = jsxc.storage.getPrefix(uk) + key;

      var value = localStorage.getItem(key);
      try {
         return JSON.parse(value);
      } catch (e) {
         return value;
      }
   },

   /**
    * Get a user item from storage.
    *
    * @param key
    * @returns user item
    */
   getUserItem: function(type, key) {
      var self = jsxc.storage;

      if (arguments.length === 1) {
         key = type;
      } else if (arguments.length === 2) {
         key = type + self.SEP + key;
      }

      return jsxc.storage.getItem(key, true);
   },

   /**
    * Remove item from storage
    *
    * @function
    * @param {String} key variablename
    * @param {String} uk Userkey? Should we add the bid as prefix?
    */
   removeItem: function(key, uk) {

      // Workaround for non-conforming browser
      if (jsxc.storageNotConform && key !== 'rid') {
         jsxc.ls.push(JSON.stringify({
            key: jsxc.storage.prefix + key,
            value: ''
         }));
      }

      localStorage.removeItem(jsxc.storage.getPrefix(uk) + key);
   },

   /**
    * Remove user item from storage.
    *
    * @param key
    */
   removeUserItem: function(type, key) {
      var self = jsxc.storage;

      if (arguments.length === 1) {
         key = type;
      } else if (arguments.length === 2) {
         key = type + self.SEP + key;
      }

      jsxc.storage.removeItem(key, true);
   },

   /**
    * Updates value of a variable in a saved object.
    *
    * @function
    * @param {String} key variablename
    * @param {String|object} variable variablename in object or object with
    *        variable/key pairs
    * @param {Object} [value] value
    * @param {String} uk Userkey? Should we add the bid as prefix?
    */
   updateItem: function(key, variable, value, uk) {

      var data = jsxc.storage.getItem(key, uk) || {};

      if (typeof(variable) === 'object') {

         $.each(variable, function(key, val) {
            if (typeof(data[key]) === 'undefined') {
               jsxc.debug('Variable ' + key + ' doesn\'t exist in ' + variable + '. It was created.');
            }

            data[key] = val;
         });
      } else {
         if (typeof(data[variable]) === 'undefined') {
            jsxc.debug('Variable ' + variable + ' doesn\'t exist. It was created.');
         }

         data[variable] = value;
      }

      jsxc.storage.setItem(key, data, uk);
   },

   /**
    * Updates value of a variable in a saved user object.
    *
    * @param {String} type variable type (a prefix)
    * @param {String} key variable name
    * @param {String|object} variable variable name in object or object with
    *        variable/key pairs
    * @param {Object} [value] value (not used if the variable was an object)
    */
   updateUserItem: function(type, key, variable, value) {
      var self = jsxc.storage;

      if (arguments.length === 4 || (arguments.length === 3 && typeof variable === 'object')) {
         key = type + self.SEP + key;
      } else {
         value = variable;
         variable = key;
         key = type;
      }

      return jsxc.storage.updateItem(key, variable, value, true);
   },

   /**
    * Increments value
    *
    * @function
    * @param {String} key variablename
    * @param {String} uk Userkey? Should we add the bid as prefix?
    */
   ink: function(key, uk) {

      jsxc.storage.setItem(key, Number(jsxc.storage.getItem(key, uk)) + 1, uk);
   },

   /**
    * Remove element from array or object
    *
    * @param {string} key name of array or object
    * @param {string} name name of element in array or object
    * @param {String} uk Userkey? Should we add the bid as prefix?
    * @returns {undefined}
    */
   removeElement: function(key, name, uk) {
      var item = jsxc.storage.getItem(key, uk);

      if ($.isArray(item)) {
         item = $.grep(item, function(e) {
            return e !== name;
         });
      } else if (typeof(item) === 'object' && item !== null) {
         delete item[name];
      }

      jsxc.storage.setItem(key, item, uk);
   },

   removeUserElement: function(type, key, name) {
      var self = jsxc.storage;

      if (arguments.length === 2) {
         name = key;
         key = type;
      } else if (arguments.length === 3) {
         key = type + self.SEP + key;
      }

      return jsxc.storage.removeElement(key, name, true);
   },

   /**
    * Triggered if changes are recognized
    *
    * @function
    * @param {event} e Storage event
    * @param {String} e.key Key name which triggered event
    * @param {Object} e.oldValue Old Value for key
    * @param {Object} e.newValue New Value for key
    * @param {String} e.url
    */
   onStorage: function(e) {

      // skip
      if (e.key === jsxc.storage.PREFIX + jsxc.storage.SEP + 'rid' || !e.key) {
         return;
      }

      var re = new RegExp('^' + jsxc.storage.PREFIX + jsxc.storage.SEP + '(?:[^' + jsxc.storage.SEP + ']+@[^' + jsxc.storage.SEP + ']+' + jsxc.storage.SEP + ')?(.*)', 'i');
      var key = e.key.replace(re, '$1');

      // Workaround for non-conforming browser, which trigger
      // events on every page (notably IE): Ignore own writes
      // (own)
      if (jsxc.storageNotConform > 0 && jsxc.ls.length > 0) {

         var val = e.newValue;
         try {
            val = JSON.parse(val);
         } catch (err) {}

         var index = $.inArray(JSON.stringify({
            key: key,
            value: val
         }), jsxc.ls);

         if (index >= 0) {

            // confirm that the storage event is not fired regularly
            if (jsxc.storageNotConform > 1) {
               window.clearTimeout(jsxc.toSNC);
               jsxc.storageNotConform = 1;
               jsxc.storage.setItem('storageNotConform', 1);
            }

            jsxc.ls.splice(index, 1);
            return;
         }
      }

      // Workaround for non-conforming browser
      if (e.oldValue === e.newValue) {
         return;
      }

      var n, o;
      var bid = key.replace(new RegExp('[^' + jsxc.storage.SEP + ']+' + jsxc.storage.SEP + '(.*)', 'i'), '$1');

      // react if someone asks whether there is a master
      if (jsxc.master && key === 'alive') {
         jsxc.debug('Master request.');

         if (e.newValue && e.newValue.match(/:master$/)) {
            jsxc.warn('Master request from master. Something went wrong... :-(');
            return;
         }

         jsxc.keepAlive();
         return;
      }

      // master alive
      if (!jsxc.master && (key === 'alive' || key === 'alive_busy')) {

         // reset timeouts
         jsxc.to = $.grep(jsxc.to, function(timeout) {
            window.clearTimeout(timeout);

            return false;
         });

         if (typeof e.newValue === 'undefined' || e.newValue === null) {
            jsxc.xmpp.disconnected();
            return;
         }

         jsxc.to.push(window.setTimeout(jsxc.checkMaster, ((key === 'alive') ? jsxc.options.timeout : jsxc.options.busyTimeout) + jsxc.random(60)));

         // only call the first time
         if (!jsxc.role_allocation) {
            jsxc.onSlave();
         }

         return;
      }

      if (jsxc.master && key === 'sid' && !e.newValue) {
         jsxc.xmpp.logout(false);
      }

      if (key.match(/^notices/)) {
         jsxc.notice.load();
      }

      if (key.match(/^presence/)) {
         jsxc.gui.changePresence(e.newValue, true);
      }

      if (key.match(/^options/) && e.newValue) {
         n = JSON.parse(e.newValue);

         if (typeof n.muteNotification !== 'undefined' && n.muteNotification) {
            jsxc.notification.muteSound(true);
         } else {
            jsxc.notification.unmuteSound(true);
         }
      }

      if (key.match(/^hidden/)) {
         if (jsxc.master) {
            clearTimeout(jsxc.toNotification);
         } else {
            jsxc.isHidden();
         }
      }

      if (key.match(/^focus/)) {
         if (jsxc.master) {
            clearTimeout(jsxc.toNotification);
         } else {
            jsxc.hasFocus();
         }
      }

      if (key.match(new RegExp('^history' + jsxc.storage.SEP))) {

         var history = JSON.parse(e.newValue);
         var uid, el, message;

         if (!jsxc.master) {
            var win = jsxc.gui.window.get(bid);
            win.find('.jsxc_textarea').empty();
         }

         while (history.length > 0) {
            uid = history.pop();

            message = new jsxc.Message(uid);
            el = message.getDOM();

            if (el.length === 0) {
               if (jsxc.master && message.direction === jsxc.Message.OUT) {
                  jsxc.xmpp.sendMessage(message);
               }

               jsxc.gui.window._postMessage(message, true);
            } else if (message.isReceived()) {
               el.addClass('jsxc_received');
            }
         }
         return;
      }

      if (key.match(new RegExp('^window' + jsxc.storage.SEP))) {

         if (!e.newValue) {
            jsxc.gui.window._close(bid);
            return;
         }

         if (!e.oldValue) {
            jsxc.gui.window.open(bid);
            return;
         }

         n = JSON.parse(e.newValue);
         o = JSON.parse(e.oldValue);

         if (n.minimize !== o.minimize) {
            if (n.minimize) {
               jsxc.gui.window._hide(bid);
            } else {
               jsxc.gui.window._show(bid);
            }
         }

         jsxc.gui.window.setText(bid, n.text);

         if (n.unread !== o.unread) {
            if (n.unread === 0) {
               jsxc.gui.readMsg(bid);
            } else {
               jsxc.gui._unreadMsg(bid, n.unread);
            }
         }

         return;
      }

      if (key.match(/^unreadMsg/) && jsxc.gui.favicon) {
         jsxc.gui.favicon.badge(parseInt(e.newValue) || 0);
      }

      if (key.match(new RegExp('^smp' + jsxc.storage.SEP))) {

         if (!e.newValue) {

            jsxc.gui.dialog.close('smp');
            jsxc.gui.window.hideOverlay(bid);

            if (jsxc.master) {
               jsxc.otr.objects[bid].sm.abort();
            }

            return;
         }

         n = JSON.parse(e.newValue);

         if (typeof(n.data) !== 'undefined') {

            jsxc.gui.window.smpRequest(bid, n.data);

         } else if (jsxc.master && n.sec) {
            jsxc.gui.dialog.close('smp');
            jsxc.gui.window.hideOverlay(bid);

            jsxc.otr.sendSmpReq(bid, n.sec, n.quest);
         }
      }

      if (!jsxc.master && key.match(new RegExp('^buddy' + jsxc.storage.SEP))) {

         if (!e.newValue) {
            jsxc.gui.roster.purge(bid);
            return;
         }
         if (jsxc.gui.roster.getItem(bid).length === 0) {
            jsxc.gui.roster.add(bid);
            return;
         }

         n = JSON.parse(e.newValue);
         o = JSON.parse(e.oldValue);

         jsxc.gui.update(bid);

         if (o.status !== n.status || o.sub !== n.sub) {
            jsxc.gui.roster.reorder(bid);
         }
      }

      if (jsxc.master && key.match(new RegExp('^deletebuddy' + jsxc.storage.SEP)) && e.newValue) {
         n = JSON.parse(e.newValue);

         jsxc.xmpp.removeBuddy(n.jid);
         jsxc.storage.removeUserItem(key);
      }

      if (jsxc.master && key.match(new RegExp('^buddy' + jsxc.storage.SEP))) {

         n = JSON.parse(e.newValue);
         o = JSON.parse(e.oldValue);

         if (o.transferReq !== n.transferReq) {
            jsxc.storage.updateUserItem('buddy', bid, 'transferReq', -1);

            if (n.transferReq === 0) {
               jsxc.otr.goPlain(bid);
            }
            if (n.transferReq === 1) {
               jsxc.otr.goEncrypt(bid);
            }
         }

         if (o.name !== n.name) {
            jsxc.gui.roster._rename(bid, n.name);
         }
      }

      if (key === 'friendReq') {
         n = JSON.parse(e.newValue);

         if (jsxc.master && n.approve >= 0) {
            jsxc.xmpp.resFriendReq(n.jid, n.approve);
         }
      }

      if (jsxc.master && key.match(new RegExp('^add' + jsxc.storage.SEP))) {
         n = JSON.parse(e.newValue);

         jsxc.xmpp.addBuddy(n.username, n.alias);
      }

      if (key === 'roster') {
         jsxc.gui.roster.toggle(e.newValue);
      }

      if (jsxc.master && key.match(new RegExp('^vcard' + jsxc.storage.SEP)) && e.newValue !== null && e.newValue.match(/^request:/)) {

         jsxc.xmpp.loadVcard(bid, function(stanza) {
            jsxc.storage.setUserItem('vcard', bid, {
               state: 'success',
               data: $('<div>').append(stanza).html()
            });
         }, function() {
            jsxc.storage.setUserItem('vcard', bid, {
               state: 'error'
            });
         });
      }

      if (!jsxc.master && key.match(new RegExp('^vcard' + jsxc.storage.SEP)) && e.newValue !== null && !e.newValue.match(/^request:/)) {
         n = JSON.parse(e.newValue);

         if (typeof n.state !== 'undefined') {
            $(document).trigger('loaded.vcard.jsxc', n);
         }

         jsxc.storage.removeUserItem('vcard', bid);
      }

      if (key === '_cmd' && e.newValue) {
         n = JSON.parse(e.newValue) || {};
         jsxc.storage.removeUserItem('_cmd');

         if (n.cmd && n.target === jsxc.tab.CONST[jsxc.master ? 'MASTER' : 'SLAVE']) {
            jsxc.debug('Execute tab cmd: ' + n.cmd);

            jsxc.exec(n.cmd, n.params);
         }
      }
   },

   /**
    * Save or update buddy data.
    *
    * @memberOf jsxc.storage
    * @param bid
    * @param data
    * @returns {String} Updated or created
    */
   saveBuddy: function(bid, data) {

      if (jsxc.storage.getUserItem('buddy', bid)) {
         jsxc.storage.updateUserItem('buddy', bid, data);

         return 'updated';
      }

      jsxc.storage.setUserItem('buddy', bid, $.extend({
         jid: '',
         name: '',
         status: 0,
         sub: 'none',
         msgstate: 0,
         transferReq: -1,
         trust: false,
         fingerprint: null,
         res: [],
         type: 'chat'
      }, data));

      return 'created';
   }
};