Skip to content
Snippets Groups Projects
Forked from clients / Cesium-grp / Cesium
598 commits behind the upstream repository.
settings-services.js 12.83 KiB
angular.module('cesium.es.settings.services', ['cesium.services', 'cesium.es.http.services'])
.config(function(PluginServiceProvider, csConfig) {
    'ngInject';

    var enable = csConfig.plugins && csConfig.plugins.es;
    if (enable) {
      // Will force to load this service
      PluginServiceProvider.registerEagerLoadingService('esSettings');
    }

  })

.factory('esSettings', function($rootScope, $q, $timeout, Api, esHttp,
                            csConfig, csSettings, CryptoUtils, Device, UIUtils, csWallet) {
  'ngInject';

  var
    SETTINGS_SAVE_SPEC = {
      includes: ['locale', 'showUDHistory', 'useRelative', 'useLocalStorage', 'useLocalStorageEncryption', 'expertMode', 'logoutIdle', 'blockValidityWindow'],
      excludes: ['timeout', 'cacheTimeMs', 'version', 'build', 'minVersion', 'fallbackLanguage'],
      plugins: {
        es: {
          excludes: ['enable', 'host', 'port', 'useSsl', 'fallbackNodes', 'enableGoogleApi', 'googleApiKey', 'document', 'maxUploadBodySize', 'defaultCountry'],
          notifications: {
          }
        }
      },
      wallet: {
        includes: ['alertIfUnusedWallet'],
        excludes: ['notificationReadTime'] // deprecated - should be removed later
      },
      helptip: {
        excludes: ['installDocUrl']
      },
      notifications: {
        excludes: ['time', 'warnCount', 'unreadCount']
      }
    },
    defaultSettings = angular.merge({
        plugins: {
          es: {
            askEnable: false,
            useRemoteStorage: true,
            notifications: {
              txSent: true,
              txReceived: true,
              certSent: true,
              certReceived: true,
              emitHtml5: false
            },
            invitations: {
              readTime: true
            },
            defaultCountry: undefined,
            enableGoogleApi: false,
            googleApiKey: undefined,
            wot: {
              enableMixedSearch: true
            },
            document: {
              index: 'user,page,group',
              type: 'profile,record,comment'
            },
            registry: {
              defaultSearch: {
                location: null,
                geoPoint: null
              }
            },
            geoDistance: '20km'
          }
        }
    }, {plugins: {es: csConfig.plugins && csConfig.plugins.es || {}}}),
    that = this,
    api = new Api('esSettings'),
    previousRemoteData,
    listeners,
    ignoreSettingsChanged = false,
    failEnable = false
  ;

  that.api = api;
  that.get = esHttp.get('/user/settings/:id');
  that.add = esHttp.record.post('/user/settings');
  that.update = esHttp.record.post('/user/settings/:id/_update');

  that.isEnable = function() {
    return csSettings.data.plugins &&
      csSettings.data.plugins.es &&
      csSettings.data.plugins.es.enable &&
      !!csSettings.data.plugins.es.host;
  };

  that.notifications = {};
  that.notifications.isEmitHtml5Enable = function() {
    return that.isEnable() &&
      csSettings.data.plugins.es.notifications &&
      angular.isDefined(csSettings.data.plugins.es.notifications.emitHtml5) ? csSettings.data.plugins.es.notifications.emitHtml5 : false;
  };

  that.wot = {};
  that.wot.isMixedSearchEnable = function() {
    return that.isEnable() &&
      (angular.isDefined(csSettings.data.plugins.es.wot && csSettings.data.plugins.es.wot.enableMixedSearch) ?
        csSettings.data.plugins.es.wot.enableMixedSearch : true);
  };

  function copyUsingSpec(data, copySpec) {
    var result = {};

    // Add implicit includes
    if (copySpec.includes) {
      _.forEach(_.keys(copySpec), function(key) {
        if (key != "includes" && key != "excludes") {
          copySpec.includes.push(key);
        }
      });
    }

    _.forEach(_.keys(data), function(key) {
      if ((!copySpec.includes || _.contains(copySpec.includes, key)) &&
        (!copySpec.excludes || !_.contains(copySpec.excludes, key))) {
        if (data[key] && (typeof data[key] == 'object') &&
          copySpec[key] && (typeof copySpec[key] == 'object')) {
          result[key] = copyUsingSpec(data[key], copySpec[key]);
        }
        else {
          result[key] = data[key];
        }
      }
    });
    return result;
  }

  // Load settings
  function loadSettings(pubkey, boxKeypair) {
    var now = Date.now();
    return that.get({id: pubkey})
        .catch(function(err){
          if (err && err.ucode && err.ucode == 404) {
            return null; // not found
          }
          else {
            throw err;
          }
        })
      .then(function(res) {
        if (!res || !res._source) {
          return;
        }
        var record = res._source;
        // Do not apply if same version
        if (record.time === csSettings.data.time) {
          console.debug('[ES] [settings] Loaded in '+ (Date.now()-now) +'ms, but already up to date');
          return;
        }
        var nonce = CryptoUtils.util.decode_base58(record.nonce);
        // Decrypt settings content
        return CryptoUtils.box.open(record.content, nonce, boxKeypair.boxPk, boxKeypair.boxSk)
          .then(function(json) {
            var settings = JSON.parse(json || '{}');
            settings.time = record.time;
            console.debug('[ES] [settings] Loaded and decrypted in '+ (Date.now()-now) +'ms');
            return settings;
          })
          // if error: skip stored content
          .catch(function(err){
            console.error('[ES] [settings] Could not load remote settings: ' + (err && err.message || 'decryption error'));
            // make sure to remove time, to be able to save it again
            delete csSettings.data.time;
            return null;
          });
      });
  }

  function onSettingsReset(data, deferred) {
    deferred = deferred || $q.defer();
    angular.merge(data, defaultSettings);

    failEnable = false;
    deferred.resolve(data);
    return deferred.promise;
  }

  function onWalletAuth(data, deferred) {
    deferred = deferred || $q.defer();
    if (!data || !data.pubkey || !data.keypair || !data.keypair.signSk || !data.keypair.boxSk) {
      deferred.resolve();
      return deferred.promise;
    }

    console.debug('[ES] [settings] Loading user settings...');

    // Load settings
    loadSettings(data.pubkey, data.keypair)
      .then(function(settings) {
        if (!settings) return; // not found or up to date
        angular.merge(csSettings.data, settings);

        // Remember for comparison
        previousRemoteData = settings;

        console.debug('[ES] [settings] Applied');
        return storeSettingsLocally();
      })
    .then(function() {
      deferred.resolve(data);
    })
    .catch(function(err){
      deferred.reject(err);
    });

    return deferred.promise;
  }

  // Listen for settings changed
  function onSettingsChanged(data) {
    // avoid recursive call, because storeSettingsLocally() could emit event again
    if (ignoreSettingsChanged) return;

    var wasEnable = listeners && listeners.length > 0;

    // Force to stop & restart, if ES node has changed
    if (esHttp.isStarted() && !esHttp.node.isFallback() && !esHttp.node.sameAsSettings(data)) {
      stop();
    }

    refreshState();

    var isEnable = that.isEnable();
    if (isEnable && csWallet.isAuth()) {
      if (!wasEnable) {
        onWalletAuth(csWallet.data);
      }
      else {
        storeSettingsRemotely(data);
      }
    }
  }

  function storeSettingsLocally() {
    if (ignoreSettingsChanged) return $q.when();
    ignoreSettingsChanged = true;
    return csSettings.store()
      .then(function(){
        ignoreSettingsChanged = false;
      })
      .catch(function(err) {
        ignoreSettingsChanged = false;
        throw err;
      });
  }

  function storeSettingsRemotely(data) {
    var filteredData = copyUsingSpec(data, SETTINGS_SAVE_SPEC);
    if (previousRemoteData && angular.equals(filteredData, previousRemoteData)) {
      return $q.when();
    }

    // Skip remote saving, if remote storage disable
    if (!csSettings.data.plugins.es.useRemoteStorage) {
      return storeSettingsLocally();
    }

    var time = moment().utc().unix(); // always update time
    console.debug('[ES] [settings] Saving user settings remotely...');

    return $q.all([
        csWallet.getKeypair(), // same result as esWallet.box.getKeypair(), because box keypair computed on auth
        CryptoUtils.util.random_nonce()
      ])
      .then(function(res) {
        var boxKeypair = res[0];
        var nonce = res[1];

        // Make sure user has not disconnect
        // This can occur, when auth + disabling ES plugin in settings
        if (!boxKeypair.boxPk || !boxKeypair.boxSk) return;

        var record = {
          issuer: csWallet.data.pubkey,
          nonce: CryptoUtils.util.encode_base58(nonce),
          time: time
        };

        //console.debug("Will store settings remotely: ", filteredData);
        var json = JSON.stringify(filteredData);

        return CryptoUtils.box.pack(json, nonce, boxKeypair.boxPk, boxKeypair.boxSk)
          .then(function(cypherText) {
            record.content = cypherText;
            // create or update
            return angular.isUndefined(data.time) ?
              that.add(record) :
              that.update(record, {id: record.issuer})
                .catch(function(err) {
                  // Workaround if update failed: try to add() instead
                  // Can occur when changing the cesium+ pod
                  if (err && err.ucode == 404) return that.add(record);
                  throw err;
                });
          })
          .then(function() {
            return true;
          });
      })
      .then(function(saved) {
        if (!saved) return;
        // Update settings version, then store (on local store only)
        data.time = time;
        previousRemoteData = filteredData;
        console.debug('[ES] [settings] Saved user settings remotely in ' + (moment().utc().unix() - time) + 'ms');
        return storeSettingsLocally();
      })
      .catch(function(err) {
        console.error(err);
        throw err;
      })
    ;
  }

  function removeListeners() {
    _.forEach(listeners, function(remove){
      remove();
    });
    listeners = [];
  }

  function addListeners() {
    // Extend csWallet.login()
    listeners = [
      csSettings.api.data.on.reset($rootScope, onSettingsReset, this),
      csWallet.api.data.on.auth($rootScope, onWalletAuth, this)
    ];
  }

  function stop() {
    removeListeners();
    esHttp.stop();
  }

  function refreshState() {
    var enable = that.isEnable();

    // Disable
    if (!enable && listeners && listeners.length > 0) {
      console.debug("[ES] [settings] Disable");
      removeListeners();

      // Force ES node to stop
      return esHttp.stop()
        .then(function() {
          // Emit event
          api.state.raise.changed(enable);
        });
    }

    // Enable
    else if (enable && (!listeners || listeners.length === 0 || !esHttp.isStarted()) ) {
      return esHttp.start()
        .then(function(alive) {
          if (!alive) {
            csSettings.data.plugins.es.enable = false;
            // Will ask user to enable ES plugins (WARN: if config.js allow it)
            csSettings.data.plugins.es.askEnable = true;
            failEnable = true;

            api.state.raise.changed(false);
            console.error('[ES] [settings] Disable, has ES node could not be started');
            return;
          }
          console.debug("[ES] [settings] Enable");
          addListeners();

          if (csWallet.isAuth()) {
            return onWalletAuth(csWallet.data)
              .then(function() {
                // Emit event
                api.state.raise.changed(enable);
              });
          }
          else {
            // Emit event
            api.state.raise.changed(enable);
          }
        });
    }
  }

  api.registerEvent('state', 'changed');

  csSettings.ready().then(function() {

    csSettings.api.data.on.changed($rootScope, onSettingsChanged, this);
    esHttp.api.node.on.stop($rootScope, function() {
      previousRemoteData = null;
    }, this);
    return refreshState();
  })

  .then(function() {
    // Ask (once) user to enable ES plugin
    if (!failEnable && // If NOT trying to start just before
      csConfig.plugins && csConfig.plugins.es && csConfig.plugins.es.askEnable && // AND if config ask enable
      !that.isEnable() && // AND user settings has disable plugin
      csSettings.data.plugins.es.askEnable // AND user has not yet answer 'NO'
    ) {

      return UIUtils.alert.confirm('ES_SETTINGS.CONFIRM.ASK_ENABLE', 'ES_SETTINGS.CONFIRM.ASK_ENABLE_TITLE',
        {
          cancelText: 'COMMON.BTN_NO',
          okText: 'COMMON.BTN_YES'
        })
        .then(function (confirm) {
          if (confirm) {
            csSettings.data.plugins.es.enable = true;
          }
          csSettings.data.plugins.es.askEnable = false;
          return csSettings.store();
        });
    }
  });

  return that;
});