mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
75bacf714f
Import engines to the Weave global object and use them to register engines, which checks if the arg is an array. To support handling of errors (unused), the engine is returned on register failure.
1229 lines
41 KiB
JavaScript
1229 lines
41 KiB
JavaScript
/* ***** BEGIN LICENSE BLOCK *****
|
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
|
*
|
|
* The contents of this file are subject to the Mozilla Public License Version
|
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
|
* the License. You may obtain a copy of the License at
|
|
* http://www.mozilla.org/MPL/
|
|
*
|
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
|
* for the specific language governing rights and limitations under the
|
|
* License.
|
|
*
|
|
* The Original Code is Bookmarks Sync.
|
|
*
|
|
* The Initial Developer of the Original Code is Mozilla.
|
|
* Portions created by the Initial Developer are Copyright (C) 2007
|
|
* the Initial Developer. All Rights Reserved.
|
|
*
|
|
* Contributor(s):
|
|
* Dan Mills <thunder@mozilla.com>
|
|
* Myk Melez <myk@mozilla.org>
|
|
*
|
|
* Alternatively, the contents of this file may be used under the terms of
|
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
|
* of those above. If you wish to allow use of your version of this file only
|
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
|
* use your version of this file under the terms of the MPL, indicate your
|
|
* decision by deleting the provisions above and replace them with the notice
|
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
|
* the provisions above, a recipient may use your version of this file under
|
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
|
*
|
|
* ***** END LICENSE BLOCK ***** */
|
|
|
|
const EXPORTED_SYMBOLS = ['Weave'];
|
|
|
|
const Cc = Components.classes;
|
|
const Ci = Components.interfaces;
|
|
const Cr = Components.results;
|
|
const Cu = Components.utils;
|
|
|
|
// The following constants determine when Weave will automatically sync data.
|
|
|
|
// An interval of one minute, initial threshold of 100, and step of 5 means
|
|
// that we'll try to sync each engine 21 times, once per minute, at
|
|
// consecutively lower thresholds (from 100 down to 5 in steps of 5 and then
|
|
// one more time with the threshold set to the minimum 1) before resetting
|
|
// the engine's threshold to the initial value and repeating the cycle
|
|
// until at some point the engine's score exceeds the threshold, at which point
|
|
// we'll sync it, reset its threshold to the initial value, rinse, and repeat.
|
|
|
|
// How long we wait between sync checks.
|
|
const SCHEDULED_SYNC_INTERVAL = 60 * 1000 * 5; // five minutes
|
|
|
|
// INITIAL_THRESHOLD represents the value an engine's score has to exceed
|
|
// in order for us to sync it the first time we start up (and the first time
|
|
// we do a sync check after having synced the engine or reset the threshold).
|
|
const INITIAL_THRESHOLD = 75;
|
|
|
|
// THRESHOLD_DECREMENT_STEP is the amount by which we decrement an engine's
|
|
// threshold each time we do a sync check and don't sync that engine.
|
|
const THRESHOLD_DECREMENT_STEP = 25;
|
|
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
Cu.import("resource://weave/log4moz.js");
|
|
Cu.import("resource://weave/constants.js");
|
|
Cu.import("resource://weave/util.js");
|
|
Cu.import("resource://weave/wrap.js");
|
|
Cu.import("resource://weave/faultTolerance.js");
|
|
Cu.import("resource://weave/auth.js");
|
|
Cu.import("resource://weave/resource.js");
|
|
Cu.import("resource://weave/base_records/wbo.js");
|
|
Cu.import("resource://weave/base_records/crypto.js");
|
|
Cu.import("resource://weave/base_records/keys.js");
|
|
Cu.import("resource://weave/engines.js");
|
|
Cu.import("resource://weave/oauth.js");
|
|
Cu.import("resource://weave/identity.js");
|
|
Cu.import("resource://weave/async.js");
|
|
Cu.import("resource://weave/engines/clientData.js");
|
|
|
|
Function.prototype.async = Async.sugar;
|
|
|
|
// for export
|
|
let Weave = {};
|
|
Cu.import("resource://weave/constants.js", Weave);
|
|
Cu.import("resource://weave/util.js", Weave);
|
|
Cu.import("resource://weave/async.js", Weave);
|
|
Cu.import("resource://weave/faultTolerance.js", Weave);
|
|
Cu.import("resource://weave/auth.js", Weave);
|
|
Cu.import("resource://weave/resource.js", Weave);
|
|
Cu.import("resource://weave/base_records/keys.js", Weave);
|
|
Cu.import("resource://weave/notifications.js", Weave);
|
|
Cu.import("resource://weave/identity.js", Weave);
|
|
Cu.import("resource://weave/stores.js", Weave);
|
|
Cu.import("resource://weave/engines.js", Weave);
|
|
Cu.import("resource://weave/oauth.js", Weave);
|
|
|
|
Cu.import("resource://weave/engines/bookmarks.js", Weave);
|
|
Cu.import("resource://weave/engines/clientData.js", Weave);
|
|
Cu.import("resource://weave/engines/forms.js", Weave);
|
|
Cu.import("resource://weave/engines/history.js", Weave);
|
|
Cu.import("resource://weave/engines/passwords.js", Weave);
|
|
Cu.import("resource://weave/engines/tabs.js", Weave);
|
|
|
|
Utils.lazy(Weave, 'Service', WeaveSvc);
|
|
|
|
/*
|
|
* Service status query system. See constants defined in constants.js.
|
|
*/
|
|
|
|
function StatusRecord() {
|
|
this._init();
|
|
}
|
|
StatusRecord.prototype = {
|
|
_init: function() {
|
|
this.server = [];
|
|
this.sync = null;
|
|
this.engines = {};
|
|
},
|
|
|
|
addServerStatus: function(statusCode) {
|
|
this.server.push(statusCode);
|
|
},
|
|
|
|
setSyncStatus: function(statusCode) {
|
|
this.sync = statusCode;
|
|
},
|
|
|
|
setEngineStatus: function(engineName, statusCode) {
|
|
this.engines[engineName] = statusCode;
|
|
}
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
* Service singleton
|
|
* Main entry point into Weave's sync framework
|
|
*/
|
|
|
|
function WeaveSvc() {
|
|
this._notify = Wrap.notify("weave:service:");
|
|
}
|
|
WeaveSvc.prototype = {
|
|
|
|
_localLock: Wrap.localLock,
|
|
_catchAll: Wrap.catchAll,
|
|
_isQuitting: false,
|
|
_loggedIn: false,
|
|
_syncInProgress: false,
|
|
_keyGenEnabled: true,
|
|
|
|
// WEAVE_STATUS_OK, WEAVE_STATUS_FAILED, or WEAVE_STATUS_PARTIAL
|
|
_weaveStatusCode: null,
|
|
// More detailed status info:
|
|
_detailedStatus: null,
|
|
|
|
// object for caching public and private keys
|
|
_keyPair: {},
|
|
|
|
// Timer object for automagically syncing
|
|
_syncTimer: null,
|
|
|
|
get username() {
|
|
return Svc.Prefs.get("username", "");
|
|
},
|
|
set username(value) {
|
|
if (value)
|
|
Svc.Prefs.set("username", value);
|
|
else
|
|
Svc.Prefs.reset("username");
|
|
|
|
// fixme - need to loop over all Identity objects - needs some rethinking...
|
|
ID.get('WeaveID').username = value;
|
|
ID.get('WeaveCryptoID').username = value;
|
|
|
|
// FIXME: need to also call this whenever the username pref changes
|
|
this._genKeyURLs();
|
|
},
|
|
|
|
get password() { return ID.get('WeaveID').password; },
|
|
set password(value) { ID.get('WeaveID').password = value; },
|
|
|
|
get passphrase() { return ID.get('WeaveCryptoID').password; },
|
|
set passphrase(value) { ID.get('WeaveCryptoID').password = value; },
|
|
|
|
// chrome-provided callbacks for when the service needs a password/passphrase
|
|
set onGetPassword(value) {
|
|
ID.get('WeaveID').onGetPassword = value;
|
|
},
|
|
set onGetPassphrase(value) {
|
|
ID.get('WeaveCryptoID').onGetPassword = value;
|
|
},
|
|
|
|
get baseURL() {
|
|
let url = Svc.Prefs.get("serverURL");
|
|
if (!url)
|
|
throw "No server URL set";
|
|
if (url[url.length-1] != '/')
|
|
url += '/';
|
|
url += "0.3/";
|
|
return url;
|
|
},
|
|
set baseURL(value) {
|
|
Svc.Prefs.set("serverURL", value);
|
|
},
|
|
|
|
get clusterURL() {
|
|
let url = Svc.Prefs.get("clusterURL");
|
|
if (!url)
|
|
return null;
|
|
if (url[url.length-1] != '/')
|
|
url += '/';
|
|
url += "0.3/user/";
|
|
return url;
|
|
},
|
|
set clusterURL(value) {
|
|
Svc.Prefs.set("clusterURL", value);
|
|
this._genKeyURLs();
|
|
},
|
|
|
|
get userPath() { return ID.get('WeaveID').username; },
|
|
|
|
get isLoggedIn() { return this._loggedIn; },
|
|
|
|
get isQuitting() { return this._isQuitting; },
|
|
set isQuitting(value) { this._isQuitting = value; },
|
|
|
|
get cancelRequested() { return Engines.cancelRequested; },
|
|
set cancelRequested(value) { Engines.cancelRequested = value; },
|
|
|
|
get keyGenEnabled() { return this._keyGenEnabled; },
|
|
set keyGenEnabled(value) { this._keyGenEnabled = value; },
|
|
|
|
get enabled() { return Svc.Prefs.get("enabled"); },
|
|
set enabled(value) { Svc.Prefs.set("enabled", value); },
|
|
|
|
get statusCode() { return this._weaveStatusCode; },
|
|
get detailedStatus() { return this._detailedStatus; },
|
|
|
|
get locked() { return this._locked; },
|
|
lock: function Svc_lock() {
|
|
if (this._locked)
|
|
return false;
|
|
this._locked = true;
|
|
return true;
|
|
},
|
|
unlock: function Svc_unlock() {
|
|
this._locked = false;
|
|
},
|
|
|
|
_setSyncFailure: function WeavSvc__setSyncFailure(code) {
|
|
this._weaveStatusCode = WEAVE_STATUS_FAILED;
|
|
this._detailedStatus.setSyncStatus(code);
|
|
},
|
|
|
|
_genKeyURLs: function WeaveSvc__genKeyURLs() {
|
|
let url = this.clusterURL + this.username;
|
|
PubKeys.defaultKeyUri = url + "/keys/pubkey";
|
|
PrivKeys.defaultKeyUri = url + "/keys/privkey";
|
|
},
|
|
|
|
_checkCrypto: function WeaveSvc__checkCrypto() {
|
|
let ok = false;
|
|
|
|
try {
|
|
let iv = Svc.Crypto.generateRandomIV();
|
|
if (iv.length == 24)
|
|
ok = true;
|
|
|
|
} catch (e) {
|
|
this._log.debug("Crypto check failed: " + e);
|
|
}
|
|
|
|
return ok;
|
|
},
|
|
|
|
onWindowOpened: function WeaveSvc__onWindowOpened() {
|
|
},
|
|
|
|
// one-time initialization like setting up observers and the like
|
|
// xxx we might need to split some of this out into something we can call
|
|
// again when username/server/etc changes
|
|
_onStartup: function WeaveSvc__onStartup() {
|
|
let self = yield;
|
|
this._initLogs();
|
|
this._log.info("Weave " + WEAVE_VERSION + " initializing");
|
|
|
|
this._detailedStatus = new StatusRecord();
|
|
|
|
// Reset our sync id if we're upgrading, so sync knows to reset local data
|
|
if (WEAVE_VERSION != Svc.Prefs.get("lastversion")) {
|
|
this._log.info("Resetting client syncID from _onStartup.");
|
|
Clients.resetSyncID();
|
|
}
|
|
|
|
let ua = Cc["@mozilla.org/network/protocol;1?name=http"].
|
|
getService(Ci.nsIHttpProtocolHandler).userAgent;
|
|
this._log.info(ua);
|
|
|
|
if (!this._checkCrypto()) {
|
|
this.enabled = false;
|
|
this._log.error("Could not load the Weave crypto component. Disabling " +
|
|
"Weave, since it will not work correctly.");
|
|
}
|
|
|
|
Utils.prefs.addObserver("", this, false);
|
|
Svc.Observer.addObserver(this, "network:offline-status-changed", true);
|
|
Svc.Observer.addObserver(this, "private-browsing", true);
|
|
Svc.Observer.addObserver(this, "quit-application", true);
|
|
FaultTolerance.Service; // initialize FT service
|
|
|
|
if (!this.enabled)
|
|
this._log.info("Weave Sync disabled");
|
|
|
|
// Create Weave identities (for logging in, and for encryption)
|
|
ID.set('WeaveID', new Identity('Mozilla Services Password', this.username));
|
|
Auth.defaultAuthenticator = new BasicAuthenticator(ID.get('WeaveID'));
|
|
|
|
ID.set('WeaveCryptoID',
|
|
new Identity('Mozilla Services Encryption Passphrase', this.username));
|
|
|
|
this._genKeyURLs();
|
|
|
|
if (Svc.Prefs.get("autoconnect") && this.username) {
|
|
try {
|
|
if (yield this.login(self.cb))
|
|
yield this.sync(self.cb, true);
|
|
} catch (e) {}
|
|
}
|
|
self.done();
|
|
},
|
|
onStartup: function WeaveSvc_onStartup(callback) {
|
|
this._onStartup.async(this, callback);
|
|
},
|
|
|
|
_initLogs: function WeaveSvc__initLogs() {
|
|
this._log = Log4Moz.repository.getLogger("Service.Main");
|
|
this._log.level =
|
|
Log4Moz.Level[Svc.Prefs.get("log.logger.service.main")];
|
|
|
|
let formatter = new Log4Moz.BasicFormatter();
|
|
let root = Log4Moz.repository.rootLogger;
|
|
root.level = Log4Moz.Level[Svc.Prefs.get("log.rootLogger")];
|
|
|
|
let capp = new Log4Moz.ConsoleAppender(formatter);
|
|
capp.level = Log4Moz.Level[Svc.Prefs.get("log.appender.console")];
|
|
root.addAppender(capp);
|
|
|
|
let dapp = new Log4Moz.DumpAppender(formatter);
|
|
dapp.level = Log4Moz.Level[Svc.Prefs.get("log.appender.dump")];
|
|
root.addAppender(dapp);
|
|
|
|
let brief = Svc.Directory.get("ProfD", Ci.nsIFile);
|
|
brief.QueryInterface(Ci.nsILocalFile);
|
|
brief.append("weave");
|
|
brief.append("logs");
|
|
brief.append("brief-log.txt");
|
|
if (!brief.exists())
|
|
brief.create(brief.NORMAL_FILE_TYPE, PERMS_FILE);
|
|
|
|
let verbose = brief.parent.clone();
|
|
verbose.append("verbose-log.txt");
|
|
if (!verbose.exists())
|
|
verbose.create(verbose.NORMAL_FILE_TYPE, PERMS_FILE);
|
|
|
|
this._briefApp = new Log4Moz.RotatingFileAppender(brief, formatter);
|
|
this._briefApp.level = Log4Moz.Level[Svc.Prefs.get("log.appender.briefLog")];
|
|
root.addAppender(this._briefApp);
|
|
this._debugApp = new Log4Moz.RotatingFileAppender(verbose, formatter);
|
|
this._debugApp.level = Log4Moz.Level[Svc.Prefs.get("log.appender.debugLog")];
|
|
root.addAppender(this._debugApp);
|
|
},
|
|
|
|
clearLogs: function WeaveSvc_clearLogs() {
|
|
this._briefApp.clear();
|
|
this._debugApp.clear();
|
|
},
|
|
|
|
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
|
|
Ci.nsISupportsWeakReference]),
|
|
|
|
// nsIObserver
|
|
|
|
observe: function WeaveSvc__observe(subject, topic, data) {
|
|
switch (topic) {
|
|
case "nsPref:changed":
|
|
switch (data) {
|
|
case "enabled":
|
|
case "schedule":
|
|
// Potentially we'll want to reschedule syncs
|
|
this._checkSync();
|
|
break;
|
|
}
|
|
break;
|
|
case "network:offline-status-changed":
|
|
// Whether online or offline, we'll reschedule syncs
|
|
this._log.debug("Network offline status change: " + data);
|
|
this._checkSync();
|
|
break;
|
|
case "private-browsing":
|
|
// Entering or exiting private browsing? Reschedule syncs
|
|
this._log.debug("Private browsing change: " + data);
|
|
this._checkSync();
|
|
break;
|
|
case "quit-application":
|
|
this._onQuitApplication();
|
|
break;
|
|
}
|
|
},
|
|
|
|
_onQuitApplication: function WeaveSvc__onQuitApplication() {
|
|
},
|
|
|
|
// These are global (for all engines)
|
|
|
|
// gets cluster from central LDAP server and returns it, or null on error
|
|
findCluster: function WeaveSvc_findCluster(onComplete, username) {
|
|
let fn = function WeaveSvc__findCluster() {
|
|
let self = yield;
|
|
let ret = null;
|
|
|
|
this._log.debug("Finding cluster for user " + username);
|
|
|
|
let res = new Resource(this.baseURL + "api/register/chknode/" + username);
|
|
try {
|
|
yield res.get(self.cb);
|
|
} catch (e) { /* we check status below */ }
|
|
|
|
if (res.lastChannel.responseStatus == 404) {
|
|
this._log.debug("Using serverURL as data cluster (multi-cluster support disabled)");
|
|
ret = Svc.Prefs.get("serverURL");
|
|
} else if (res.lastChannel.responseStatus == 200) {
|
|
// XXX Bug 480480 Work around the server sending a trailing newline
|
|
ret = 'https://' + res.data.trim() + '/';
|
|
}
|
|
|
|
self.done(ret);
|
|
};
|
|
fn.async(this, onComplete);
|
|
},
|
|
|
|
// gets cluster from central LDAP server and sets this.clusterURL
|
|
setCluster: function WeaveSvc_setCluster(onComplete, username) {
|
|
let fn = function WeaveSvc__setCluster() {
|
|
let self = yield;
|
|
let ret = false;
|
|
|
|
let cluster = yield this.findCluster(self.cb, username);
|
|
if (cluster) {
|
|
this._log.debug("Saving cluster setting");
|
|
this.clusterURL = cluster;
|
|
ret = true;
|
|
} else
|
|
this._log.debug("Error setting cluster for user " + username);
|
|
|
|
self.done(ret);
|
|
};
|
|
fn.async(this, onComplete);
|
|
},
|
|
|
|
verifyLogin: function WeaveSvc_verifyLogin(onComplete, username, password, isLogin) {
|
|
let fn = function WeaveSvc__verifyLogin() {
|
|
let self = yield;
|
|
this._log.debug("Verifying login for user " + username);
|
|
|
|
let url = yield this.findCluster(self.cb, username);
|
|
if (isLogin)
|
|
this.clusterURL = url;
|
|
|
|
if (url[url.length-1] != '/')
|
|
url += '/';
|
|
url += "0.3/user/";
|
|
|
|
let res = new Resource(url + username);
|
|
res.authenticator = {
|
|
onRequest: function(headers) {
|
|
headers['Authorization'] = 'Basic ' + btoa(username + ':' + password);
|
|
return headers;
|
|
}
|
|
};
|
|
yield res.get(self.cb);
|
|
|
|
//JSON.parse(res.data); // throws if not json
|
|
self.done(true);
|
|
};
|
|
this._catchAll(this._notify("verify-login", "", fn)).async(this, onComplete);
|
|
},
|
|
|
|
verifyPassphrase: function WeaveSvc_verifyPassphrase(onComplete, username,
|
|
password, passphrase) {
|
|
let fn = function WeaveSvc__verifyPassphrase() {
|
|
let self = yield;
|
|
|
|
this._log.debug("Verifying passphrase");
|
|
|
|
this.username = username;
|
|
ID.get('WeaveID').setTempPassword(password);
|
|
|
|
let id = new Identity('Passphrase Verification', username);
|
|
id.setTempPassword(passphrase);
|
|
|
|
let pubkey = yield PubKeys.getDefaultKey(self.cb);
|
|
let privkey = yield PrivKeys.get(self.cb, pubkey.PrivKeyUri);
|
|
|
|
// fixme: decrypt something here
|
|
};
|
|
this._catchAll(
|
|
this._localLock(
|
|
this._notify("verify-passphrase", "", fn))).async(this, onComplete);
|
|
},
|
|
|
|
login: function WeaveSvc_login(onComplete, username, password, passphrase) {
|
|
let user = username, pass = password, passp = passphrase;
|
|
|
|
let fn = function WeaveSvc__login() {
|
|
let self = yield;
|
|
|
|
this._loggedIn = false;
|
|
this._detailedStatus = new StatusRecord();
|
|
|
|
if (typeof(user) != 'undefined')
|
|
this.username = user;
|
|
if (typeof(pass) != 'undefined')
|
|
ID.get('WeaveID').setTempPassword(pass);
|
|
if (typeof(passp) != 'undefined')
|
|
ID.get('WeaveCryptoID').setTempPassword(passp);
|
|
|
|
if (!this.username) {
|
|
this._setSyncFailure(LOGIN_FAILED_NO_USERNAME);
|
|
throw "No username set, login failed";
|
|
}
|
|
|
|
if (!this.password) {
|
|
this._setSyncFailure(LOGIN_FAILED_NO_PASSWORD);
|
|
throw "No password given or found in password manager";
|
|
}
|
|
|
|
this._log.debug("Logging in user " + this.username);
|
|
|
|
if (!(yield this.verifyLogin(self.cb, this.username, this.password, true))) {
|
|
this._setSyncFailure(LOGIN_FAILED_REJECTED);
|
|
throw "Login failed";
|
|
}
|
|
|
|
// Try starting the sync timer now that we're logged in
|
|
this._loggedIn = true;
|
|
this._checkSync();
|
|
|
|
self.done(true);
|
|
};
|
|
this._catchAll(this._localLock(this._notify("login", "", fn))).
|
|
async(this, onComplete);
|
|
},
|
|
|
|
logout: function WeaveSvc_logout() {
|
|
this._log.info("Logging out");
|
|
this._loggedIn = false;
|
|
this._keyPair = {};
|
|
ID.get('WeaveID').setTempPassword(null); // clear cached password
|
|
ID.get('WeaveCryptoID').setTempPassword(null); // and passphrase
|
|
|
|
// Cancel the sync timer now that we're logged out
|
|
this._checkSync();
|
|
|
|
Svc.Observer.notifyObservers(null, "weave:service:logout:finish", "");
|
|
},
|
|
|
|
createAccount: function WeaveSvc_createAccount(onComplete, username, password, email,
|
|
captchaChallenge, captchaResponse) {
|
|
let fn = function WeaveSvc__createAccount() {
|
|
let self = yield;
|
|
|
|
function enc(x) encodeURIComponent(x);
|
|
let message = "uid=" + enc(username) + "&password=" + enc(password) +
|
|
"&mail=" + enc(email) +
|
|
"&recaptcha_challenge_field=" + enc(captchaChallenge) +
|
|
"&recaptcha_response_field=" + enc(captchaResponse);
|
|
|
|
let url = Svc.Prefs.get('tmpServerURL') + '0.3/api/register/new';
|
|
let res = new Weave.Resource(url);
|
|
res.authenticator = new Weave.NoOpAuthenticator();
|
|
res.setHeader("Content-Type", "application/x-www-form-urlencoded",
|
|
"Content-Length", message.length);
|
|
|
|
// fixme: Resource throws on error - it really shouldn't :-/
|
|
let resp;
|
|
try {
|
|
resp = yield res.post(self.cb, message);
|
|
} catch (e) {
|
|
this._log.trace("Create account error: " + e);
|
|
}
|
|
|
|
if (res.lastChannel.responseStatus != 200 &&
|
|
res.lastChannel.responseStatus != 201)
|
|
this._log.info("Failed to create account. " +
|
|
"status: " + res.lastChannel.responseStatus + ", " +
|
|
"response: " + resp);
|
|
else
|
|
this._log.info("Account created: " + resp);
|
|
|
|
self.done(res.lastChannel.responseStatus);
|
|
};
|
|
fn.async(this, onComplete);
|
|
},
|
|
|
|
// stuff we need to to after login, before we can really do
|
|
// anything (e.g. key setup)
|
|
_remoteSetup: function WeaveSvc__remoteSetup() {
|
|
let self = yield;
|
|
let ret = false; // false to abort sync
|
|
let reset = false;
|
|
|
|
this._log.debug("Fetching global metadata record");
|
|
let meta = yield Records.import(self.cb, this.clusterURL + this.username +
|
|
"/meta/global");
|
|
|
|
let remoteVersion = (meta && meta.payload.storageVersion)?
|
|
meta.payload.storageVersion : "";
|
|
|
|
this._log.debug("Min supported storage version is " + MIN_SERVER_STORAGE_VERSION);
|
|
this._log.debug("Remote storage version is " + remoteVersion);
|
|
|
|
if (!meta || !meta.payload.storageVersion || !meta.payload.syncID ||
|
|
Svc.Version.compare(MIN_SERVER_STORAGE_VERSION, remoteVersion) > 0) {
|
|
|
|
// abort the server wipe if the GET status was anything other than 404 or 200
|
|
let status = Records.lastResource.lastChannel.responseStatus;
|
|
if (status != 200 && status != 404) {
|
|
this._setSyncFailure(METARECORD_DOWNLOAD_FAIL);
|
|
this._log.warn("Unknown error while downloading metadata record. " +
|
|
"Aborting sync.");
|
|
self.done(false);
|
|
return;
|
|
}
|
|
|
|
if (!meta)
|
|
this._log.info("No metadata record, server wipe needed");
|
|
if (meta && !meta.payload.syncID)
|
|
this._log.warn("No sync id, server wipe needed");
|
|
if (Svc.Version.compare(MIN_SERVER_STORAGE_VERSION, remoteVersion) > 0)
|
|
this._log.info("Server storage version no longer supported, server wipe needed");
|
|
|
|
if (!this._keyGenEnabled) {
|
|
this._log.info("...and key generation is disabled. Not wiping. " +
|
|
"Aborting sync.");
|
|
this._setSyncFailure(DESKTOP_VERSION_OUT_OF_DATE);
|
|
self.done(false);
|
|
return;
|
|
}
|
|
reset = true;
|
|
this._log.info("Wiping server data");
|
|
yield this._freshStart.async(this, self.cb);
|
|
|
|
if (status == 404)
|
|
this._log.info("Metadata record not found, server wiped to ensure " +
|
|
"consistency.");
|
|
else // 200
|
|
this._log.info("Server data wiped to ensure consistency after client " +
|
|
"upgrade (" + remoteVersion + " -> " + WEAVE_VERSION + ")");
|
|
|
|
} else if (Svc.Version.compare(remoteVersion, WEAVE_VERSION) > 0) {
|
|
this._setSyncFailure(VERSION_OUT_OF_DATE);
|
|
this._log.warn("Server data is of a newer Weave version, this client " +
|
|
"needs to be upgraded. Aborting sync.");
|
|
self.done(false);
|
|
return;
|
|
|
|
} else if (meta.payload.syncID != Clients.syncID) {
|
|
this._log.warn("Meta.payload.syncID is " + meta.payload.syncID +
|
|
", Clients.syncID is " + Clients.syncID);
|
|
yield this.resetClient(self.cb);
|
|
this._log.info("Reset client because of syncID mismatch.");
|
|
Clients.syncID = meta.payload.syncID;
|
|
this._log.info("Reset the client after a server/client sync ID mismatch");
|
|
}
|
|
|
|
let needKeys = true;
|
|
let pubkey = yield PubKeys.getDefaultKey(self.cb);
|
|
if (pubkey) {
|
|
// make sure we have a matching privkey
|
|
let privkey = yield PrivKeys.get(self.cb, pubkey.privateKeyUri);
|
|
if (privkey) {
|
|
needKeys = false;
|
|
ret = true;
|
|
}
|
|
}
|
|
if (needKeys) {
|
|
if (PubKeys.lastResource.lastChannel.responseStatus != 404 &&
|
|
PrivKeys.lastResource.lastChannel.responseStatus != 404) {
|
|
this._log.warn("Couldn't download keys from server, aborting sync");
|
|
this._log.debug("PubKey HTTP response status: " +
|
|
PubKeys.lastResource.lastChannel.responseStatus);
|
|
this._log.debug("PrivKey HTTP response status: " +
|
|
PrivKeys.lastResource.lastChannel.responseStatus);
|
|
this._setSyncFailure(KEYS_DOWNLOAD_FAIL);
|
|
self.done(false);
|
|
return;
|
|
}
|
|
|
|
if (!this._keyGenEnabled) {
|
|
this._log.warn("Couldn't download keys from server, and key generation" +
|
|
"is disabled. Aborting sync");
|
|
this._setSyncFailure(NO_KEYS_NO_KEYGEN);
|
|
self.done(false);
|
|
return;
|
|
}
|
|
|
|
if (!reset) {
|
|
this._log.warn("Calling freshStart from !reset case.");
|
|
yield this._freshStart.async(this, self.cb);
|
|
this._log.info("Server data wiped to ensure consistency due to missing keys");
|
|
}
|
|
|
|
let pass = yield ID.get('WeaveCryptoID').getPassword(self.cb);
|
|
if (pass) {
|
|
let keys = PubKeys.createKeypair(pass, PubKeys.defaultKeyUri,
|
|
PrivKeys.defaultKeyUri);
|
|
try {
|
|
yield PubKeys.uploadKeypair(self.cb, keys);
|
|
ret = true;
|
|
} catch (e) {
|
|
this._setSyncFailure(KEYS_UPLOAD_FAIL);
|
|
this._log.error("Could not upload keys: " + Utils.exceptionStr(e));
|
|
// FIXME no lastRequest anymore
|
|
//this._log.error(keys.pubkey.lastRequest.responseText);
|
|
}
|
|
} else {
|
|
this._setSyncFailure(SETUP_FAILED_NO_PASSPHRASE);
|
|
this._log.warn("Could not get encryption passphrase");
|
|
}
|
|
}
|
|
|
|
self.done(ret);
|
|
},
|
|
|
|
/**
|
|
* Engine scores must meet or exceed this value before we sync them when
|
|
* using thresholds. These are engine-specific, as different kinds of data
|
|
* change at different rates, so we store them in a hash by engine name.
|
|
*/
|
|
_syncThresh: {},
|
|
|
|
/**
|
|
* Determine if a sync should run. If so, schedule a repeating sync;
|
|
* otherwise, cancel future syncs and return a reason.
|
|
*
|
|
* @return Reason for not syncing; not-truthy if sync should run
|
|
*/
|
|
_checkSync: function WeaveSvc__checkSync() {
|
|
let reason = "";
|
|
if (!this.enabled)
|
|
reason = kSyncWeaveDisabled;
|
|
else if (!this._loggedIn)
|
|
reason = kSyncNotLoggedIn;
|
|
else if (Svc.IO.offline)
|
|
reason = kSyncNetworkOffline;
|
|
else if (Svc.Private && Svc.Private.privateBrowsingEnabled)
|
|
// Svc.Private doesn't exist on Fennec -- don't assume it's there.
|
|
reason = kSyncInPrivateBrowsing;
|
|
else if (Svc.Prefs.get("schedule", 0) != 1)
|
|
reason = kSyncNotScheduled;
|
|
|
|
// A truthy reason means we shouldn't continue to sync
|
|
if (reason) {
|
|
// Cancel any future syncs
|
|
if (this._syncTimer) {
|
|
this._syncTimer.cancel();
|
|
this._syncTimer = null;
|
|
}
|
|
this._log.config("Weave scheduler disabled: " + reason);
|
|
}
|
|
// We're good to sync, so schedule a repeating sync if we haven't yet
|
|
else if (!this._syncTimer) {
|
|
this._syncTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
|
let listener = new Utils.EventListener(Utils.bind2(this,
|
|
function WeaveSvc__checkSyncCallback(timer) this.sync(null, false)));
|
|
this._syncTimer.initWithCallback(listener, SCHEDULED_SYNC_INTERVAL,
|
|
Ci.nsITimer.TYPE_REPEATING_SLACK);
|
|
this._log.config("Weave scheduler enabled");
|
|
}
|
|
|
|
return reason;
|
|
},
|
|
|
|
/**
|
|
* Sync up engines with the server.
|
|
*
|
|
* @param fullSync
|
|
* True to unconditionally sync all engines
|
|
* @throw Reason for not syncing
|
|
*/
|
|
_sync: function WeaveSvc__sync(fullSync) {
|
|
let self = yield;
|
|
|
|
// Use thresholds to determine what to sync only if it's not a full sync
|
|
let useThresh = !fullSync;
|
|
|
|
// Make sure we should sync or record why we shouldn't. We always obey the
|
|
// reason if we're using thresholds (not a full sync); otherwise, allow
|
|
// "not scheduled" as future syncs have already been canceled by checkSync.
|
|
let reason = this._checkSync();
|
|
if (reason && (useThresh || reason != kSyncNotScheduled)) {
|
|
// this is a purposeful abort rather than a failure, so don't set
|
|
// WEAVE_STATUS_FAILED; instead, leave it as it was.
|
|
this._detailedStatus.setSyncStatus(reason);
|
|
reason = "Can't sync: " + reason;
|
|
throw reason;
|
|
}
|
|
|
|
if (!(yield this._remoteSetup.async(this, self.cb))) {
|
|
throw "aborting sync, remote setup failed";
|
|
}
|
|
|
|
this._log.debug("Refreshing client list");
|
|
yield Clients.sync(self.cb);
|
|
|
|
// Process the incoming commands if we have any
|
|
if (Clients.getClients()[Clients.clientID].commands) {
|
|
try {
|
|
if (!(yield this.processCommands(self.cb))) {
|
|
this._detailedStatus.setSyncStatus(ABORT_SYNC_COMMAND);
|
|
throw "aborting sync, process commands said so";
|
|
}
|
|
|
|
// Repeat remoteSetup in-case the commands forced us to reset
|
|
if (!(yield this._remoteSetup.async(this, self.cb)))
|
|
throw "aborting sync, remote setup failed after processing commands";
|
|
}
|
|
finally {
|
|
// Always immediately push back the local client (now without commands)
|
|
yield Clients.sync(self.cb);
|
|
}
|
|
}
|
|
|
|
try {
|
|
for each (let engine in Engines.getAll()) {
|
|
let name = engine.name;
|
|
|
|
// Nothing to do for disabled engines
|
|
if (!engine.enabled)
|
|
continue;
|
|
|
|
// Conditionally reset the threshold for the current engine
|
|
let resetThresh = Utils.bind2(this, function WeaveSvc__resetThresh(cond)
|
|
cond ? this._syncThresh[name] = INITIAL_THRESHOLD : undefined);
|
|
|
|
// Initialize the threshold if it doesn't exist yet
|
|
resetThresh(!(name in this._syncThresh));
|
|
|
|
// Determine if we should sync if using thresholds
|
|
if (useThresh) {
|
|
let score = engine.score;
|
|
let thresh = this._syncThresh[name];
|
|
if (score >= thresh)
|
|
this._log.debug("Syncing " + name + "; " +
|
|
"score " + score + " >= thresh " + thresh);
|
|
else {
|
|
this._log.debug("Not syncing " + name + "; " +
|
|
"score " + score + " < thresh " + thresh);
|
|
|
|
// Decrement the threshold by a standard amount with a lower bound of 1
|
|
this._syncThresh[name] = Math.max(thresh - THRESHOLD_DECREMENT_STEP, 1);
|
|
|
|
// No need to sync this engine for now
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// If there's any problems with syncing the engine, report the failure
|
|
if (!(yield this._syncEngine.async(this, self.cb, engine))) {
|
|
this._log.info("Aborting sync");
|
|
break;
|
|
}
|
|
|
|
// We've successfully synced, so reset the threshold. We do this after
|
|
// a successful sync so failures can try again on next sync, but this
|
|
// could trigger too many syncs if the server is having problems.
|
|
resetThresh(useThresh);
|
|
}
|
|
|
|
if (this._syncError)
|
|
this._log.warn("Some engines did not sync correctly");
|
|
else {
|
|
Svc.Prefs.set("lastSync", new Date().toString());
|
|
this._weaveStatusCode = WEAVE_STATUS_OK;
|
|
this._log.info("Sync completed successfully");
|
|
}
|
|
} finally {
|
|
this.cancelRequested = false;
|
|
this._syncError = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Do a synchronized sync (only one sync at a time).
|
|
*
|
|
* @param onComplete
|
|
* Callback when this method completes
|
|
* @param fullSync
|
|
* True to unconditionally sync all engines
|
|
*/
|
|
sync: function WeaveSvc_sync(onComplete, fullSync) {
|
|
fullSync = true; // not doing thresholds yet
|
|
this._catchAll(this._notify("sync", "", this._localLock(this._sync))).
|
|
async(this, onComplete, fullSync);
|
|
},
|
|
|
|
// returns true if sync should proceed
|
|
// false / no return value means sync should be aborted
|
|
_syncEngine: function WeaveSvc__syncEngine(engine) {
|
|
let self = yield;
|
|
try {
|
|
yield engine.sync(self.cb);
|
|
if (!this.cancelRequested)
|
|
self.done(true);
|
|
}
|
|
catch(e) {
|
|
this._syncError = true;
|
|
this._weaveStatusCode = WEAVE_STATUS_PARTIAL;
|
|
this._detailedStatus.setEngineStatus(engine.name, e);
|
|
if (FaultTolerance.Service.onException(e))
|
|
self.done(true);
|
|
}
|
|
},
|
|
|
|
_freshStart: function WeaveSvc__freshStart() {
|
|
let self = yield;
|
|
yield this.resetClient(self.cb);
|
|
this._log.info("Reset client data from freshStart.");
|
|
this._log.info("Client metadata wiped, deleting server data");
|
|
yield this.wipeServer(self.cb);
|
|
|
|
this._log.debug("Uploading new metadata record");
|
|
meta = new WBORecord(this.clusterURL + this.username + "/meta/global");
|
|
this._log.debug("Setting meta payload storage version to " + WEAVE_VERSION);
|
|
meta.payload.storageVersion = WEAVE_VERSION;
|
|
meta.payload.syncID = Clients.syncID;
|
|
let res = new Resource(meta.uri);
|
|
yield res.put(self.cb, meta.serialize());
|
|
},
|
|
|
|
/**
|
|
* Wipe all user data from the server.
|
|
*
|
|
* @param onComplete
|
|
* Callback when this method completes
|
|
* @param engines [optional]
|
|
* Array of engine names to wipe. If not given, all engines are used.
|
|
*/
|
|
wipeServer: function WeaveSvc_wipeServer(onComplete, engines) {
|
|
let fn = function WeaveSvc__wipeServer() {
|
|
let self = yield;
|
|
|
|
// Grab all the collections for the user
|
|
let userURL = this.clusterURL + this.username + "/";
|
|
let res = new Resource(userURL);
|
|
yield res.get(self.cb);
|
|
|
|
// Get the array of collections and delete each one
|
|
let allCollections = JSON.parse(res.data);
|
|
for each (let name in allCollections) {
|
|
try {
|
|
// If we have a list of engines, make sure it's one we want
|
|
if (engines && engines.indexOf(name) == -1)
|
|
continue;
|
|
|
|
yield new Resource(userURL + name).delete(self.cb);
|
|
}
|
|
catch(ex) {
|
|
this._log.debug("Exception on wipe of '" + name + "': " + Utils.exceptionStr(ex));
|
|
}
|
|
}
|
|
};
|
|
this._catchAll(this._notify("wipe-server", "", fn)).async(this, onComplete);
|
|
},
|
|
|
|
/**
|
|
* Wipe all local user data.
|
|
*
|
|
* @param onComplete
|
|
* Callback when this method completes
|
|
* @param engines [optional]
|
|
* Array of engine names to wipe. If not given, all engines are used.
|
|
*/
|
|
wipeClient: function WeaveSvc_wipeClient(onComplete, engines) {
|
|
let fn = function WeaveSvc__wipeClient() {
|
|
let self = yield;
|
|
|
|
// If we don't have any engines, reset the service and wipe all engines
|
|
if (!engines) {
|
|
// Clear out any service data
|
|
yield this.resetService(self.cb);
|
|
|
|
engines = [Clients].concat(Engines.getAll());
|
|
}
|
|
// Convert the array of names into engines
|
|
else
|
|
engines = Engines.get(engines);
|
|
|
|
// Fully wipe each engine
|
|
for each (let engine in engines)
|
|
yield engine.wipeClient(self.cb);
|
|
};
|
|
this._catchAll(this._notify("wipe-client", "", fn)).async(this, onComplete);
|
|
},
|
|
|
|
/**
|
|
* Wipe all remote user data by wiping the server then telling each remote
|
|
* client to wipe itself.
|
|
*
|
|
* @param onComplete
|
|
* Callback when this method completes
|
|
* @param engines [optional]
|
|
* Array of engine names to wipe. If not given, all engines are used.
|
|
*/
|
|
wipeRemote: function WeaveSvc_wipeRemote(onComplete, engines) {
|
|
let fn = function WeaveSvc__wipeRemote() {
|
|
let self = yield;
|
|
|
|
// Clear out any server data
|
|
//yield this.wipeServer(self.cb, engines);
|
|
|
|
// Only wipe the engines provided
|
|
if (engines) {
|
|
engines.forEach(function(e) this.prepCommand("wipeEngine", [e]), this);
|
|
return;
|
|
}
|
|
|
|
// Tell the remote machines to wipe themselves
|
|
this.prepCommand("wipeAll", []);
|
|
};
|
|
this._catchAll(this._notify("wipe-remote", "", fn)).async(this, onComplete);
|
|
},
|
|
|
|
/**
|
|
* Reset local service information like logs, sync times, caches.
|
|
*
|
|
* @param onComplete
|
|
* Callback when this method completes
|
|
*/
|
|
resetService: function WeaveSvc__resetService(onComplete) {
|
|
let fn = function WeaveSvc__resetService() {
|
|
let self = yield;
|
|
|
|
// First drop old logs to track client resetting behavior
|
|
this.clearLogs();
|
|
this._log.info("Logs reinitialized for service reset");
|
|
|
|
// Pretend we've never synced to the server and drop cached data
|
|
Clients.resetSyncID();
|
|
Svc.Prefs.reset("lastSync");
|
|
for each (let cache in [PubKeys, PrivKeys, CryptoMetas, Records])
|
|
cache.clearCache();
|
|
};
|
|
this._catchAll(this._notify("reset-service", "", fn)).async(this, onComplete);
|
|
},
|
|
|
|
/**
|
|
* Reset the client by getting rid of any local server data and client data.
|
|
*
|
|
* @param onComplete
|
|
* Callback when this method completes
|
|
* @param engines [optional]
|
|
* Array of engine names to reset. If not given, all engines are used.
|
|
*/
|
|
resetClient: function WeaveSvc_resetClient(onComplete, engines) {
|
|
let fn = function WeaveSvc__resetClient() {
|
|
let self = yield;
|
|
|
|
// If we don't have any engines, reset everything including the service
|
|
if (!engines) {
|
|
// Clear out any service data
|
|
yield this.resetService(self.cb);
|
|
|
|
engines = [Clients].concat(Engines.getAll());
|
|
}
|
|
// Convert the array of names into engines
|
|
else
|
|
engines = Engines.get(engines);
|
|
|
|
// Have each engine drop any temporary meta data
|
|
for each (let engine in engines)
|
|
yield engine.resetClient(self.cb);
|
|
|
|
// XXX Bug 480448: Delete any snapshots from old code
|
|
try {
|
|
let cruft = Svc.Directory.get("ProfD", Ci.nsIFile);
|
|
cruft.QueryInterface(Ci.nsILocalFile);
|
|
cruft.append("weave");
|
|
cruft.append("snapshots");
|
|
if (cruft.exists())
|
|
cruft.remove(true);
|
|
} catch (e) {
|
|
this._log.debug("Could not remove old snapshots: " + Utils.exceptionStr(e));
|
|
}
|
|
};
|
|
this._catchAll(this._notify("reset-client", "", fn)).async(this, onComplete);
|
|
},
|
|
|
|
/**
|
|
* A hash of valid commands that the client knows about. The key is a command
|
|
* and the value is a hash containing information about the command such as
|
|
* number of arguments and description.
|
|
*/
|
|
_commands: [
|
|
["resetAll", 0, "Clear temporary local data for all engines"],
|
|
["resetEngine", 1, "Clear temporary local data for engine"],
|
|
["wipeAll", 0, "Delete all client data for all engines"],
|
|
["wipeEngine", 1, "Delete all client data for engine"],
|
|
].reduce(function WeaveSvc__commands(commands, entry) {
|
|
commands[entry[0]] = {};
|
|
for (let [i, attr] in Iterator(["args", "desc"]))
|
|
commands[entry[0]][attr] = entry[i + 1];
|
|
return commands;
|
|
}, {}),
|
|
|
|
/**
|
|
* Check if the local client has any remote commands and perform them.
|
|
*
|
|
* @param onComplete
|
|
* Callback when this method completes
|
|
* @return False to abort sync
|
|
*/
|
|
processCommands: function WeaveSvc_processCommands(onComplete) {
|
|
let fn = function WeaveSvc__processCommands() {
|
|
let self = yield;
|
|
let info = Clients.getInfo(Clients.clientID);
|
|
let commands = info.commands;
|
|
|
|
// Immediately clear out the commands as we've got them locally
|
|
delete info.commands;
|
|
Clients.setInfo(Clients.clientID, info);
|
|
|
|
// Process each command in order
|
|
for each ({command: command, args: args} in commands) {
|
|
this._log.debug("Processing command: " + command + "(" + args + ")");
|
|
|
|
let engines = [args[0]];
|
|
switch (command) {
|
|
case "resetAll":
|
|
engines = null;
|
|
// Fallthrough
|
|
case "resetEngine":
|
|
yield this.resetClient(self.cb, engines);
|
|
break;
|
|
|
|
case "wipeAll":
|
|
engines = null;
|
|
// Fallthrough
|
|
case "wipeEngine":
|
|
yield this.wipeClient(self.cb, engines);
|
|
break;
|
|
|
|
default:
|
|
this._log.debug("Received an unknown command: " + command);
|
|
break;
|
|
}
|
|
}
|
|
|
|
self.done(true);
|
|
};
|
|
this._notify("process-commands", "", fn).async(this, onComplete);
|
|
},
|
|
|
|
/**
|
|
* Prepare to send a command to each remote client. Calling this doesn't
|
|
* actually sync the command data to the server. If the client already has
|
|
* the command/args pair, it won't get a duplicate action.
|
|
*
|
|
* @param command
|
|
* Command to invoke on remote clients
|
|
* @param args
|
|
* Array of arguments to give to the command
|
|
*/
|
|
prepCommand: function WeaveSvc_prepCommand(command, args) {
|
|
let commandData = this._commands[command];
|
|
// Don't send commands that we don't know about
|
|
if (commandData == null) {
|
|
this._log.error("Unknown command to send: " + command);
|
|
return;
|
|
}
|
|
// Don't send a command with the wrong number of arguments
|
|
else if (args == null || args.length != commandData.args) {
|
|
this._log.error("Expected " + commandData.args + " args for '" +
|
|
command + "', but got " + args);
|
|
return;
|
|
}
|
|
|
|
// Package the command/args pair into an object
|
|
let action = {
|
|
command: command,
|
|
args: args,
|
|
};
|
|
let actionStr = command + "(" + args + ")";
|
|
|
|
// Convert args into a string to simplify array comparisons
|
|
let jsonArgs = JSON.stringify(args);
|
|
let notDupe = function(action) action.command != command ||
|
|
JSON.stringify(action.args) != jsonArgs;
|
|
|
|
this._log.info("Sending clients: " + actionStr + "; " + commandData.desc);
|
|
|
|
// Add the action to each remote client
|
|
for (let guid in Clients.getClients()) {
|
|
// Don't send commands to the local client
|
|
if (guid == Clients.clientID)
|
|
continue;
|
|
|
|
let info = Clients.getInfo(guid);
|
|
// Set the action to be a new commands array if none exists
|
|
if (info.commands == null)
|
|
info.commands = [action];
|
|
// Add the new action if there are no duplicates
|
|
else if (info.commands.every(notDupe))
|
|
info.commands.push(action);
|
|
// Must have been a dupe.. skip!
|
|
else
|
|
continue;
|
|
|
|
Clients.setInfo(guid, info);
|
|
this._log.trace("Client " + guid + " got a new action: " + actionStr);
|
|
}
|
|
},
|
|
};
|