summaryrefslogtreecommitdiff
path: root/services/sync/modules
diff options
context:
space:
mode:
Diffstat (limited to 'services/sync/modules')
-rw-r--r--services/sync/modules/FxaMigrator.jsm546
-rw-r--r--services/sync/modules/addonsreconciler.js8
-rw-r--r--services/sync/modules/addonutils.js8
-rw-r--r--services/sync/modules/browserid_identity.js786
-rw-r--r--services/sync/modules/constants.js4
-rw-r--r--services/sync/modules/engines.js373
-rw-r--r--services/sync/modules/engines/addons.js31
-rw-r--r--services/sync/modules/engines/apps.js136
-rw-r--r--services/sync/modules/engines/bookmarks.js134
-rw-r--r--services/sync/modules/engines/clients.js89
-rw-r--r--services/sync/modules/engines/forms.js125
-rw-r--r--services/sync/modules/engines/history.js33
-rw-r--r--services/sync/modules/engines/passwords.js253
-rw-r--r--services/sync/modules/engines/prefs.js44
-rw-r--r--services/sync/modules/engines/tabs.js292
-rw-r--r--services/sync/modules/healthreport.jsm262
-rw-r--r--services/sync/modules/identity.js125
-rw-r--r--services/sync/modules/jpakeclient.js10
-rw-r--r--services/sync/modules/keys.js6
-rw-r--r--services/sync/modules/main.js2
-rw-r--r--services/sync/modules/notifications.js19
-rw-r--r--services/sync/modules/policies.js324
-rw-r--r--services/sync/modules/record.js127
-rw-r--r--services/sync/modules/resource.js34
-rw-r--r--services/sync/modules/rest.js18
-rw-r--r--services/sync/modules/service.js365
-rw-r--r--services/sync/modules/stages/cluster.js26
-rw-r--r--services/sync/modules/stages/declined.js76
-rw-r--r--services/sync/modules/stages/enginesync.js106
-rw-r--r--services/sync/modules/status.js37
-rw-r--r--services/sync/modules/userapi.js10
-rw-r--r--services/sync/modules/util.js284
32 files changed, 3486 insertions, 1207 deletions
diff --git a/services/sync/modules/FxaMigrator.jsm b/services/sync/modules/FxaMigrator.jsm
new file mode 100644
index 000000000..605ee5d7f
--- /dev/null
+++ b/services/sync/modules/FxaMigrator.jsm
@@ -0,0 +1,546 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict;"
+
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
+ "resource://gre/modules/FxAccounts.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "WeaveService", function() {
+ return Cc["@mozilla.org/weave/service;1"]
+ .getService(Components.interfaces.nsISupports)
+ .wrappedJSObject;
+});
+
+XPCOMUtils.defineLazyModuleGetter(this, "Weave",
+ "resource://services-sync/main.js");
+
+// FxAccountsCommon.js doesn't use a "namespace", so create one here.
+let fxAccountsCommon = {};
+Cu.import("resource://gre/modules/FxAccountsCommon.js", fxAccountsCommon);
+
+// We send this notification whenever the "user" migration state changes.
+const OBSERVER_STATE_CHANGE_TOPIC = "fxa-migration:state-changed";
+// We also send the state notification when we *receive* this. This allows
+// consumers to avoid loading this module until it receives a notification
+// from us (which may never happen if there's no migration to do)
+const OBSERVER_STATE_REQUEST_TOPIC = "fxa-migration:state-request";
+
+// We send this notification whenever the migration is paused waiting for
+// something internal to complete.
+const OBSERVER_INTERNAL_STATE_CHANGE_TOPIC = "fxa-migration:internal-state-changed";
+
+// We use this notification so Sync's healthreport module can record telemetry
+// (actually via "health report") for us.
+const OBSERVER_INTERNAL_TELEMETRY_TOPIC = "fxa-migration:internal-telemetry";
+
+const OBSERVER_TOPICS = [
+ "xpcom-shutdown",
+ "weave:service:sync:start",
+ "weave:service:sync:finish",
+ "weave:service:sync:error",
+ "weave:eol",
+ OBSERVER_STATE_REQUEST_TOPIC,
+ fxAccountsCommon.ONLOGIN_NOTIFICATION,
+ fxAccountsCommon.ONLOGOUT_NOTIFICATION,
+ fxAccountsCommon.ONVERIFIED_NOTIFICATION,
+];
+
+// A list of preference names we write to the migration sentinel. We only
+// write ones that have a user-set value.
+const FXA_SENTINEL_PREFS = [
+ "identity.fxaccounts.auth.uri",
+ "identity.fxaccounts.remote.force_auth.uri",
+ "identity.fxaccounts.remote.signup.uri",
+ "identity.fxaccounts.remote.signin.uri",
+ "identity.fxaccounts.settings.uri",
+ "services.sync.tokenServerURI",
+];
+
+function Migrator() {
+ // Leave the log-level as Debug - Sync will setup log appenders such that
+ // these messages generally will not be seen unless other log related
+ // prefs are set.
+ this.log.level = Log.Level.Debug;
+
+ this._nextUserStatePromise = Promise.resolve();
+
+ for (let topic of OBSERVER_TOPICS) {
+ Services.obs.addObserver(this, topic, false);
+ }
+ // ._state is an optimization so we avoid sending redundant observer
+ // notifications when the state hasn't actually changed.
+ this._state = null;
+}
+
+Migrator.prototype = {
+ log: Log.repository.getLogger("Sync.SyncMigration"),
+
+ // What user action is necessary to push the migration forward?
+ // A |null| state means there is nothing to do. Note that a null state implies
+ // either. (a) no migration is necessary or (b) that the migrator module is
+ // waiting for something outside of the user's control - eg, sync to complete,
+ // the migration sentinel to be uploaded, etc. In most cases the wait will be
+ // short, but edge cases (eg, no network, sync bugs that prevent it stopping
+ // until shutdown) may require a significantly longer wait.
+ STATE_USER_FXA: "waiting for user to be signed in to FxA",
+ STATE_USER_FXA_VERIFIED: "waiting for a verified FxA user",
+
+ // What internal state are we at? This is primarily used for FHR reporting so
+ // we can determine why exactly we might be stalled.
+ STATE_INTERNAL_WAITING_SYNC_COMPLETE: "waiting for sync to complete",
+ STATE_INTERNAL_WAITING_WRITE_SENTINEL: "waiting for sentinel to be written",
+ STATE_INTERNAL_WAITING_START_OVER: "waiting for sync to reset itself",
+ STATE_INTERNAL_COMPLETE: "migration complete",
+
+ // Flags for the telemetry we record. The UI will call a helper to record
+ // the fact some UI was interacted with.
+ TELEMETRY_ACCEPTED: "accepted",
+ TELEMETRY_DECLINED: "declined",
+ TELEMETRY_UNLINKED: "unlinked",
+
+ finalize() {
+ for (let topic of OBSERVER_TOPICS) {
+ Services.obs.removeObserver(this, topic);
+ }
+ },
+
+ observe(subject, topic, data) {
+ this.log.debug("observed " + topic);
+ switch (topic) {
+ case "xpcom-shutdown":
+ this.finalize();
+ break;
+
+ case OBSERVER_STATE_REQUEST_TOPIC:
+ // someone has requested the state - send it.
+ this._queueCurrentUserState(true);
+ break;
+
+ default:
+ // some other observer that may affect our state has fired, so update.
+ this._queueCurrentUserState().then(
+ () => this.log.debug("update state from observer " + topic + " complete")
+ ).catch(err => {
+ let msg = "Failed to handle topic " + topic + ": " + err;
+ Cu.reportError(msg);
+ this.log.error(msg);
+ });
+ }
+ },
+
+ // Try and move to a state where we are blocked on a user action.
+ // This needs to be restartable, and the states may, in edge-cases, end
+ // up going backwards (eg, user logs out while we are waiting to be told
+ // about verification)
+ // This is called by our observer notifications - so if there is already
+ // a promise in-flight, it's possible we will miss something important - so
+ // we wait for the in-flight one to complete then fire another (ie, this
+ // is effectively a queue of promises)
+ _queueCurrentUserState(forceObserver = false) {
+ return this._nextUserStatePromise = this._nextUserStatePromise.then(
+ () => this._promiseCurrentUserState(forceObserver),
+ err => {
+ let msg = "Failed to determine the current user state: " + err;
+ Cu.reportError(msg);
+ this.log.error(msg);
+ return this._promiseCurrentUserState(forceObserver)
+ }
+ );
+ },
+
+ _promiseCurrentUserState: Task.async(function* (forceObserver) {
+ this.log.trace("starting _promiseCurrentUserState");
+ let update = (newState, email=null) => {
+ this.log.info("Migration state: '${state}' => '${newState}'",
+ {state: this._state, newState: newState});
+ if (forceObserver || newState !== this._state) {
+ this._state = newState;
+ let subject = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString);
+ subject.data = email || "";
+ Services.obs.notifyObservers(subject, OBSERVER_STATE_CHANGE_TOPIC, newState);
+ }
+ return newState;
+ }
+
+ // If we have no sync user, or are already using an FxA account we must
+ // be done.
+ if (WeaveService.fxAccountsEnabled) {
+ // should not be necessary, but if we somehow ended up with FxA enabled
+ // and sync blocked it would be bad - so better safe than sorry.
+ this.log.debug("FxA enabled - there's nothing to do!")
+ this._unblockSync();
+ return update(null);
+ }
+
+ // so we need to migrate - let's see how far along we are.
+ // If sync isn't in EOL mode, then we are still waiting for the server
+ // to offer the migration process - so no user action necessary.
+ let isEOL = false;
+ try {
+ isEOL = !!Services.prefs.getCharPref("services.sync.errorhandler.alert.mode");
+ } catch (e) {}
+
+ if (!isEOL) {
+ return update(null);
+ }
+
+ // So we are in EOL mode - have we a user?
+ let fxauser = yield fxAccounts.getSignedInUser();
+ if (!fxauser) {
+ // See if there is a migration sentinel so we can send the email
+ // address that was used on a different device for this account (ie, if
+ // this is a "join the party" migration rather than the first)
+ let sentinel = yield this._getSyncMigrationSentinel();
+ return update(this.STATE_USER_FXA, sentinel && sentinel.email);
+ }
+ if (!fxauser.verified) {
+ return update(this.STATE_USER_FXA_VERIFIED, fxauser.email);
+ }
+
+ // So we just have housekeeping to do - we aren't blocked on a user, so
+ // reflect that.
+ this.log.info("No next user state - doing some housekeeping");
+ update(null);
+
+ // We need to disable sync from automatically starting,
+ // and if we are currently syncing wait for it to complete.
+ this._blockSync();
+
+ // Are we currently syncing?
+ if (Weave.Service._locked) {
+ // our observers will kick us further along when complete.
+ this.log.info("waiting for sync to complete")
+ Services.obs.notifyObservers(null, OBSERVER_INTERNAL_STATE_CHANGE_TOPIC,
+ this.STATE_INTERNAL_WAITING_SYNC_COMPLETE);
+ return null;
+ }
+
+ // Write the migration sentinel if necessary.
+ Services.obs.notifyObservers(null, OBSERVER_INTERNAL_STATE_CHANGE_TOPIC,
+ this.STATE_INTERNAL_WAITING_WRITE_SENTINEL);
+ yield this._setMigrationSentinelIfNecessary();
+
+ // Get the list of enabled engines to we can restore that state.
+ let enginePrefs = this._getEngineEnabledPrefs();
+
+ // Must be ready to perform the actual migration.
+ this.log.info("Performing final sync migration steps");
+ // Do the actual migration. We setup one observer for when the new identity
+ // is about to be initialized so we can reset some key preferences - but
+ // there's no promise associated with this.
+ let observeStartOverIdentity;
+ Services.obs.addObserver(observeStartOverIdentity = () => {
+ this.log.info("observed that startOver is about to re-initialize the identity");
+ Services.obs.removeObserver(observeStartOverIdentity, "weave:service:start-over:init-identity");
+ // We've now reset all sync prefs - set the engine related prefs back to
+ // what they were.
+ for (let [prefName, prefType, prefVal] of enginePrefs) {
+ this.log.debug("Restoring pref ${prefName} (type=${prefType}) to ${prefVal}",
+ {prefName, prefType, prefVal});
+ switch (prefType) {
+ case Services.prefs.PREF_BOOL:
+ Services.prefs.setBoolPref(prefName, prefVal);
+ break;
+ case Services.prefs.PREF_STRING:
+ Services.prefs.setCharPref(prefName, prefVal);
+ break;
+ default:
+ // _getEngineEnabledPrefs doesn't return any other type...
+ Cu.reportError("unknown engine pref type for " + prefName + ": " + prefType);
+ }
+ }
+ }, "weave:service:start-over:init-identity", false);
+
+ // And another observer for the startOver being fully complete - the only
+ // reason for this is so we can wait until everything is fully reset.
+ let startOverComplete = new Promise((resolve, reject) => {
+ let observe;
+ Services.obs.addObserver(observe = () => {
+ this.log.info("observed that startOver is complete");
+ Services.obs.removeObserver(observe, "weave:service:start-over:finish");
+ resolve();
+ }, "weave:service:start-over:finish", false);
+ });
+
+ Weave.Service.startOver();
+ // need to wait for an observer.
+ Services.obs.notifyObservers(null, OBSERVER_INTERNAL_STATE_CHANGE_TOPIC,
+ this.STATE_INTERNAL_WAITING_START_OVER);
+ yield startOverComplete;
+ // observer fired, now kick things off with the FxA user.
+ this.log.info("scheduling initial FxA sync.");
+ // Note we technically don't need to unblockSync as by now all sync prefs
+ // have been reset - but it doesn't hurt.
+ this._unblockSync();
+ Weave.Service.scheduler.scheduleNextSync(0);
+
+ // Tell the front end that migration is now complete -- Sync is now
+ // configured with an FxA user.
+ forceObserver = true;
+ this.log.info("Migration complete");
+ update(null);
+
+ Services.obs.notifyObservers(null, OBSERVER_INTERNAL_STATE_CHANGE_TOPIC,
+ this.STATE_INTERNAL_COMPLETE);
+ return null;
+ }),
+
+ /* Return an object with the preferences we care about */
+ _getSentinelPrefs() {
+ let result = {};
+ for (let pref of FXA_SENTINEL_PREFS) {
+ if (Services.prefs.prefHasUserValue(pref)) {
+ result[pref] = Services.prefs.getCharPref(pref);
+ }
+ }
+ return result;
+ },
+
+ /* Apply any preferences we've obtained from the sentinel */
+ _applySentinelPrefs(savedPrefs) {
+ for (let pref of FXA_SENTINEL_PREFS) {
+ if (savedPrefs[pref]) {
+ Services.prefs.setCharPref(pref, savedPrefs[pref]);
+ }
+ }
+ },
+
+ /* Ask sync to upload the migration sentinel */
+ _setSyncMigrationSentinel: Task.async(function* () {
+ yield WeaveService.whenLoaded();
+ let signedInUser = yield fxAccounts.getSignedInUser();
+ let sentinel = {
+ email: signedInUser.email,
+ uid: signedInUser.uid,
+ verified: signedInUser.verified,
+ prefs: this._getSentinelPrefs(),
+ };
+ yield Weave.Service.setFxAMigrationSentinel(sentinel);
+ }),
+
+ /* Ask sync to upload the migration sentinal if we (or any other linked device)
+ haven't previously written one.
+ */
+ _setMigrationSentinelIfNecessary: Task.async(function* () {
+ if (!(yield this._getSyncMigrationSentinel())) {
+ this.log.info("writing the migration sentinel");
+ yield this._setSyncMigrationSentinel();
+ }
+ }),
+
+ /* Ask sync to return a migration sentinel if one exists, otherwise return null */
+ _getSyncMigrationSentinel: Task.async(function* () {
+ yield WeaveService.whenLoaded();
+ let sentinel = yield Weave.Service.getFxAMigrationSentinel();
+ this.log.debug("got migration sentinel ${}", sentinel);
+ return sentinel;
+ }),
+
+ _getDefaultAccountName: Task.async(function* (sentinel) {
+ // Requires looking to see if other devices have written a migration
+ // sentinel (eg, see _haveSynchedMigrationSentinel), and if not, see if
+ // the legacy account name appears to be a valid email address (via the
+ // services.sync.account pref), otherwise return null.
+ // NOTE: Sync does all this synchronously via nested event loops, but we
+ // expose a promise to make future migration to an async-sync easier.
+ if (sentinel && sentinel.email) {
+ this.log.info("defaultAccountName found via sentinel: ${}", sentinel.email);
+ return sentinel.email;
+ }
+ // No previous migrations, so check the existing account name.
+ let account = Weave.Service.identity.account;
+ if (account && account.contains("@")) {
+ this.log.info("defaultAccountName found via legacy account name: {}", account);
+ return account;
+ }
+ this.log.info("defaultAccountName could not find an account");
+ return null;
+ }),
+
+ // Prevent sync from automatically starting
+ _blockSync() {
+ Weave.Service.scheduler.blockSync();
+ },
+
+ _unblockSync() {
+ Weave.Service.scheduler.unblockSync();
+ },
+
+ /* Return a list of [prefName, prefType, prefVal] for all engine related
+ preferences.
+ */
+ _getEngineEnabledPrefs() {
+ let result = [];
+ for (let engine of Weave.Service.engineManager.getAll()) {
+ let prefName = "services.sync.engine." + engine.prefName;
+ let prefVal;
+ try {
+ prefVal = Services.prefs.getBoolPref(prefName);
+ result.push([prefName, Services.prefs.PREF_BOOL, prefVal]);
+ } catch (ex) {} /* just skip this pref */
+ }
+ // and the declined list.
+ try {
+ let prefName = "services.sync.declinedEngines";
+ let prefVal = Services.prefs.getCharPref(prefName);
+ result.push([prefName, Services.prefs.PREF_STRING, prefVal]);
+ } catch (ex) {}
+ return result;
+ },
+
+ /* return true if all engines are enabled, false otherwise. */
+ _allEnginesEnabled() {
+ return Weave.Service.engineManager.getAll().every(e => e.enabled);
+ },
+
+ /*
+ * Some helpers for the UI to try and move to the next state.
+ */
+
+ // Open a UI for the user to create a Firefox Account. This should only be
+ // called while we are in the STATE_USER_FXA state. When the user completes
+ // the creation we'll see an ONLOGIN_NOTIFICATION notification from FxA and
+ // we'll move to either the STATE_USER_FXA_VERIFIED state or we'll just
+ // complete the migration if they login as an already verified user.
+ createFxAccount: Task.async(function* (win) {
+ let {url, options} = yield this.getFxAccountCreationOptions();
+ win.switchToTabHavingURI(url, true, options);
+ // An FxA observer will fire when the user completes this, which will
+ // cause us to move to the next "user blocked" state and notify via our
+ // observer notification.
+ }),
+
+ // Returns an object with properties "url" and "options", suitable for
+ // opening FxAccounts to create/signin to FxA suitable for the migration
+ // state. The caller of this is responsible for the actual opening of the
+ // page.
+ // This should only be called while we are in the STATE_USER_FXA state. When
+ // the user completes the creation we'll see an ONLOGIN_NOTIFICATION
+ // notification from FxA and we'll move to either the STATE_USER_FXA_VERIFIED
+ // state or we'll just complete the migration if they login as an already
+ // verified user.
+ getFxAccountCreationOptions: Task.async(function* (win) {
+ // warn if we aren't in the expected state - but go ahead anyway!
+ if (this._state != this.STATE_USER_FXA) {
+ this.log.warn("getFxAccountCreationOptions called in an unexpected state: ${}", this._state);
+ }
+ // We need to obtain the sentinel and apply any prefs that might be
+ // specified *before* attempting to setup FxA as the prefs might
+ // specify custom servers etc.
+ let sentinel = yield this._getSyncMigrationSentinel();
+ if (sentinel && sentinel.prefs) {
+ this._applySentinelPrefs(sentinel.prefs);
+ }
+ // If we already have a sentinel then we assume the user has previously
+ // created the specified account, so just ask to sign-in.
+ let action = sentinel ? "signin" : "signup";
+ // See if we can find a default account name to use.
+ let email = yield this._getDefaultAccountName(sentinel);
+ let tail = email ? "&email=" + encodeURIComponent(email) : "";
+ // A special flag so server-side metrics can tell this is part of migration.
+ tail += "&migration=sync11";
+ // We want to ask FxA to offer a "Customize Sync" checkbox iff any engines
+ // are disabled.
+ let customize = !this._allEnginesEnabled();
+ tail += "&customizeSync=" + customize;
+
+ // We assume the caller of this is going to actually use it, so record
+ // telemetry now.
+ this.recordTelemetry(this.TELEMETRY_ACCEPTED);
+ return {
+ url: "about:accounts?action=" + action + tail,
+ options: {ignoreFragment: true, replaceQueryString: true}
+ };
+ }),
+
+ // Ask the FxA servers to re-send a verification mail for the currently
+ // logged in user. This should only be called while we are in the
+ // STATE_USER_FXA_VERIFIED state. When the user clicks on the link in
+ // the mail we should see an ONVERIFIED_NOTIFICATION which will cause us
+ // to complete the migration.
+ resendVerificationMail: Task.async(function * (win) {
+ // warn if we aren't in the expected state - but go ahead anyway!
+ if (this._state != this.STATE_USER_FXA_VERIFIED) {
+ this.log.warn("resendVerificationMail called in an unexpected state: ${}", this._state);
+ }
+ let ok = true;
+ try {
+ yield fxAccounts.resendVerificationEmail();
+ } catch (ex) {
+ this.log.error("Failed to resend verification mail: ${}", ex);
+ ok = false;
+ }
+ this.recordTelemetry(this.TELEMETRY_ACCEPTED);
+ let fxauser = yield fxAccounts.getSignedInUser();
+ let sb = Services.strings.createBundle("chrome://browser/locale/accounts.properties");
+
+ let heading = ok ?
+ sb.formatStringFromName("verificationSentHeading", [fxauser.email], 1) :
+ sb.GetStringFromName("verificationNotSentHeading");
+ let title = sb.GetStringFromName(ok ? "verificationSentTitle" : "verificationNotSentTitle");
+ let description = sb.GetStringFromName(ok ? "verificationSentDescription"
+ : "verificationNotSentDescription");
+
+ let factory = Cc["@mozilla.org/prompter;1"]
+ .getService(Ci.nsIPromptFactory);
+ let prompt = factory.getPrompt(win, Ci.nsIPrompt);
+ let bag = prompt.QueryInterface(Ci.nsIWritablePropertyBag2);
+ bag.setPropertyAsBool("allowTabModal", true);
+
+ prompt.alert(title, heading + "\n\n" + description);
+ }),
+
+ // "forget" about the current Firefox account. This should only be called
+ // while we are in the STATE_USER_FXA_VERIFIED state. After this we will
+ // see an ONLOGOUT_NOTIFICATION, which will cause the migrator to return back
+ // to the STATE_USER_FXA state, from where they can choose a different account.
+ forgetFxAccount: Task.async(function * () {
+ // warn if we aren't in the expected state - but go ahead anyway!
+ if (this._state != this.STATE_USER_FXA_VERIFIED) {
+ this.log.warn("forgetFxAccount called in an unexpected state: ${}", this._state);
+ }
+ return fxAccounts.signOut();
+ }),
+
+ recordTelemetry(flag) {
+ // Note the value is the telemetry field name - but this is an
+ // implementation detail which could be changed later.
+ switch (flag) {
+ case this.TELEMETRY_ACCEPTED:
+ case this.TELEMETRY_UNLINKED:
+ case this.TELEMETRY_DECLINED:
+ Services.obs.notifyObservers(null, OBSERVER_INTERNAL_TELEMETRY_TOPIC, flag);
+ break;
+ default:
+ throw new Error("Unexpected telemetry flag: " + flag);
+ }
+ },
+
+ get learnMoreLink() {
+ try {
+ var url = Services.prefs.getCharPref("app.support.baseURL");
+ } catch (err) {
+ return null;
+ }
+ url += "sync-upgrade";
+ let sb = Services.strings.createBundle("chrome://weave/locale/services/sync.properties");
+ return {
+ text: sb.GetStringFromName("sync.eol.learnMore.label"),
+ href: Services.urlFormatter.formatURL(url),
+ };
+ },
+};
+
+// We expose a singleton
+this.EXPORTED_SYMBOLS = ["fxaMigrator"];
+let fxaMigrator = new Migrator();
diff --git a/services/sync/modules/addonsreconciler.js b/services/sync/modules/addonsreconciler.js
index 7ae681e6b..2e838e885 100644
--- a/services/sync/modules/addonsreconciler.js
+++ b/services/sync/modules/addonsreconciler.js
@@ -19,7 +19,7 @@
const Cu = Components.utils;
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://gre/modules/AddonManager.jsm");
@@ -113,9 +113,9 @@ this.EXPORTED_SYMBOLS = ["AddonsReconciler", "CHANGE_INSTALLED",
* heed them like they were normal. In the end, the state is proper.
*/
this.AddonsReconciler = function AddonsReconciler() {
- this._log = Log4Moz.repository.getLogger("Sync.AddonsReconciler");
+ this._log = Log.repository.getLogger("Sync.AddonsReconciler");
let level = Svc.Prefs.get("log.logger.addonsreconciler", "Debug");
- this._log.level = Log4Moz.Level[level];
+ this._log.level = Log.Level[level];
Svc.Obs.add("xpcom-shutdown", this.stopListening, this);
};
@@ -140,7 +140,7 @@ AddonsReconciler.prototype = {
*/
_shouldPersist: true,
- /** log4moz logger instance */
+ /** Log logger instance */
_log: null,
/**
diff --git a/services/sync/modules/addonutils.js b/services/sync/modules/addonutils.js
index fee7649f6..54b441b9e 100644
--- a/services/sync/modules/addonutils.js
+++ b/services/sync/modules/addonutils.js
@@ -9,17 +9,17 @@ this.EXPORTED_SYMBOLS = ["AddonUtils"];
const {interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/util.js");
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository",
- "resource://gre/modules/AddonRepository.jsm");
+ "resource://gre/modules/addons/AddonRepository.jsm");
function AddonUtilsInternal() {
- this._log = Log4Moz.repository.getLogger("Sync.AddonUtils");
- this._log.Level = Log4Moz.Level[Svc.Prefs.get("log.logger.addonutils")];
+ this._log = Log.repository.getLogger("Sync.AddonUtils");
+ this._log.Level = Log.Level[Svc.Prefs.get("log.logger.addonutils")];
}
AddonUtilsInternal.prototype = {
/**
diff --git a/services/sync/modules/browserid_identity.js b/services/sync/modules/browserid_identity.js
new file mode 100644
index 000000000..bc8ea6b30
--- /dev/null
+++ b/services/sync/modules/browserid_identity.js
@@ -0,0 +1,786 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["BrowserIDManager"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://services-common/async.js");
+Cu.import("resource://services-common/utils.js");
+Cu.import("resource://services-common/tokenserverclient.js");
+Cu.import("resource://services-crypto/utils.js");
+Cu.import("resource://services-sync/identity.js");
+Cu.import("resource://services-sync/util.js");
+Cu.import("resource://services-common/tokenserverclient.js");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://services-sync/constants.js");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://services-sync/stages/cluster.js");
+Cu.import("resource://gre/modules/FxAccounts.jsm");
+
+// Lazy imports to prevent unnecessary load on startup.
+XPCOMUtils.defineLazyModuleGetter(this, "Weave",
+ "resource://services-sync/main.js");
+
+XPCOMUtils.defineLazyModuleGetter(this, "BulkKeyBundle",
+ "resource://services-sync/keys.js");
+
+XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
+ "resource://gre/modules/FxAccounts.jsm");
+
+XPCOMUtils.defineLazyGetter(this, 'log', function() {
+ let log = Log.repository.getLogger("Sync.BrowserIDManager");
+ log.level = Log.Level[Svc.Prefs.get("log.logger.identity")] || Log.Level.Error;
+ return log;
+});
+
+// FxAccountsCommon.js doesn't use a "namespace", so create one here.
+let fxAccountsCommon = {};
+Cu.import("resource://gre/modules/FxAccountsCommon.js", fxAccountsCommon);
+
+const OBSERVER_TOPICS = [
+ fxAccountsCommon.ONLOGIN_NOTIFICATION,
+ fxAccountsCommon.ONLOGOUT_NOTIFICATION,
+];
+
+const PREF_SYNC_SHOW_CUSTOMIZATION = "services.sync-setup.ui.showCustomizationDialog";
+
+function deriveKeyBundle(kB) {
+ let out = CryptoUtils.hkdf(kB, undefined,
+ "identity.mozilla.com/picl/v1/oldsync", 2*32);
+ let bundle = new BulkKeyBundle();
+ // [encryptionKey, hmacKey]
+ bundle.keyPair = [out.slice(0, 32), out.slice(32, 64)];
+ return bundle;
+}
+
+/*
+ General authentication error for abstracting authentication
+ errors from multiple sources (e.g., from FxAccounts, TokenServer).
+ details is additional details about the error - it might be a string, or
+ some other error object (which should do the right thing when toString() is
+ called on it)
+*/
+function AuthenticationError(details) {
+ this.details = details;
+}
+
+AuthenticationError.prototype = {
+ toString: function() {
+ return "AuthenticationError(" + this.details + ")";
+ }
+}
+
+this.BrowserIDManager = function BrowserIDManager() {
+ // NOTE: _fxaService and _tokenServerClient are replaced with mocks by
+ // the test suite.
+ this._fxaService = fxAccounts;
+ this._tokenServerClient = new TokenServerClient();
+ this._tokenServerClient.observerPrefix = "weave:service";
+ // will be a promise that resolves when we are ready to authenticate
+ this.whenReadyToAuthenticate = null;
+ this._log = log;
+};
+
+this.BrowserIDManager.prototype = {
+ __proto__: IdentityManager.prototype,
+
+ _fxaService: null,
+ _tokenServerClient: null,
+ // https://docs.services.mozilla.com/token/apis.html
+ _token: null,
+ _signedInUser: null, // the signedinuser we got from FxAccounts.
+
+ // null if no error, otherwise a LOGIN_FAILED_* value that indicates why
+ // we failed to authenticate (but note it might not be an actual
+ // authentication problem, just a transient network error or similar)
+ _authFailureReason: null,
+
+ // it takes some time to fetch a sync key bundle, so until this flag is set,
+ // we don't consider the lack of a keybundle as a failure state.
+ _shouldHaveSyncKeyBundle: false,
+
+ get readyToAuthenticate() {
+ // We are finished initializing when we *should* have a sync key bundle,
+ // although we might not actually have one due to auth failures etc.
+ return this._shouldHaveSyncKeyBundle;
+ },
+
+ get needsCustomization() {
+ try {
+ return Services.prefs.getBoolPref(PREF_SYNC_SHOW_CUSTOMIZATION);
+ } catch (e) {
+ return false;
+ }
+ },
+
+ initialize: function() {
+ for (let topic of OBSERVER_TOPICS) {
+ Services.obs.addObserver(this, topic, false);
+ }
+ return this.initializeWithCurrentIdentity();
+ },
+
+ /**
+ * Ensure the user is logged in. Returns a promise that resolves when
+ * the user is logged in, or is rejected if the login attempt has failed.
+ */
+ ensureLoggedIn: function() {
+ if (!this._shouldHaveSyncKeyBundle) {
+ // We are already in the process of logging in.
+ return this.whenReadyToAuthenticate.promise;
+ }
+
+ // If we are already happy then there is nothing more to do.
+ if (this._syncKeyBundle) {
+ return Promise.resolve();
+ }
+
+ // Similarly, if we have a previous failure that implies an explicit
+ // re-entering of credentials by the user is necessary we don't take any
+ // further action - an observer will fire when the user does that.
+ if (Weave.Status.login == LOGIN_FAILED_LOGIN_REJECTED) {
+ return Promise.reject();
+ }
+
+ // So - we've a previous auth problem and aren't currently attempting to
+ // log in - so fire that off.
+ this.initializeWithCurrentIdentity();
+ return this.whenReadyToAuthenticate.promise;
+ },
+
+ finalize: function() {
+ // After this is called, we can expect Service.identity != this.
+ for (let topic of OBSERVER_TOPICS) {
+ Services.obs.removeObserver(this, topic);
+ }
+ this.resetCredentials();
+ this._signedInUser = null;
+ return Promise.resolve();
+ },
+
+ offerSyncOptions: function () {
+ // If the user chose to "Customize sync options" when signing
+ // up with Firefox Accounts, ask them to choose what to sync.
+ const url = "chrome://browser/content/sync/customize.xul";
+ const features = "centerscreen,chrome,modal,dialog,resizable=no";
+ let win = Services.wm.getMostRecentWindow("navigator:browser");
+
+ let data = {accepted: false};
+ win.openDialog(url, "_blank", features, data);
+
+ return data;
+ },
+
+ initializeWithCurrentIdentity: function(isInitialSync=false) {
+ // While this function returns a promise that resolves once we've started
+ // the auth process, that process is complete when
+ // this.whenReadyToAuthenticate.promise resolves.
+ this._log.trace("initializeWithCurrentIdentity");
+
+ // Reset the world before we do anything async.
+ this.whenReadyToAuthenticate = Promise.defer();
+ this.whenReadyToAuthenticate.promise.then(null, (err) => {
+ this._log.error("Could not authenticate", err);
+ });
+
+ // initializeWithCurrentIdentity() can be called after the
+ // identity module was first initialized, e.g., after the
+ // user completes a force authentication, so we should make
+ // sure all credentials are reset before proceeding.
+ this.resetCredentials();
+ this._authFailureReason = null;
+
+ return this._fxaService.getSignedInUser().then(accountData => {
+ if (!accountData) {
+ this._log.info("initializeWithCurrentIdentity has no user logged in");
+ this.account = null;
+ // and we are as ready as we can ever be for auth.
+ this._shouldHaveSyncKeyBundle = true;
+ this.whenReadyToAuthenticate.reject("no user is logged in");
+ return;
+ }
+
+ this.account = accountData.email;
+ this._updateSignedInUser(accountData);
+ // The user must be verified before we can do anything at all; we kick
+ // this and the rest of initialization off in the background (ie, we
+ // don't return the promise)
+ this._log.info("Waiting for user to be verified.");
+ this._fxaService.whenVerified(accountData).then(accountData => {
+ this._updateSignedInUser(accountData);
+ this._log.info("Starting fetch for key bundle.");
+ if (this.needsCustomization) {
+ let data = this.offerSyncOptions();
+ if (data.accepted) {
+ Services.prefs.clearUserPref(PREF_SYNC_SHOW_CUSTOMIZATION);
+
+ // Mark any non-selected engines as declined.
+ Weave.Service.engineManager.declineDisabled();
+ } else {
+ // Log out if the user canceled the dialog.
+ return this._fxaService.signOut();
+ }
+ }
+ }).then(() => {
+ return this._fetchTokenForUser();
+ }).then(token => {
+ this._token = token;
+ this._shouldHaveSyncKeyBundle = true; // and we should actually have one...
+ this.whenReadyToAuthenticate.resolve();
+ this._log.info("Background fetch for key bundle done");
+ Weave.Status.login = LOGIN_SUCCEEDED;
+ if (isInitialSync) {
+ this._log.info("Doing initial sync actions");
+ Svc.Prefs.set("firstSync", "resetClient");
+ Services.obs.notifyObservers(null, "weave:service:setup-complete", null);
+ Weave.Utils.nextTick(Weave.Service.sync, Weave.Service);
+ }
+ }).then(null, err => {
+ this._shouldHaveSyncKeyBundle = true; // but we probably don't have one...
+ this.whenReadyToAuthenticate.reject(err);
+ // report what failed...
+ this._log.error("Background fetch for key bundle failed", err);
+ });
+ // and we are done - the fetch continues on in the background...
+ }).then(null, err => {
+ this._log.error("Processing logged in account", err);
+ });
+ },
+
+ _updateSignedInUser: function(userData) {
+ // This object should only ever be used for a single user. It is an
+ // error to update the data if the user changes (but updates are still
+ // necessary, as each call may add more attributes to the user).
+ // We start with no user, so an initial update is always ok.
+ if (this._signedInUser && this._signedInUser.email != userData.email) {
+ throw new Error("Attempting to update to a different user.")
+ }
+ this._signedInUser = userData;
+ },
+
+ logout: function() {
+ // This will be called when sync fails (or when the account is being
+ // unlinked etc). It may have failed because we got a 401 from a sync
+ // server, so we nuke the token. Next time sync runs and wants an
+ // authentication header, we will notice the lack of the token and fetch a
+ // new one.
+ this._token = null;
+ },
+
+ observe: function (subject, topic, data) {
+ this._log.debug("observed " + topic);
+ switch (topic) {
+ case fxAccountsCommon.ONLOGIN_NOTIFICATION:
+ // This should only happen if we've been initialized without a current
+ // user - otherwise we'd have seen the LOGOUT notification and been
+ // thrown away.
+ // The exception is when we've initialized with a user that needs to
+ // reauth with the server - in that case we will also get here, but
+ // should have the same identity.
+ // initializeWithCurrentIdentity will throw and log if these constraints
+ // aren't met, so just go ahead and do the init.
+ this.initializeWithCurrentIdentity(true);
+ break;
+
+ case fxAccountsCommon.ONLOGOUT_NOTIFICATION:
+ Weave.Service.startOver();
+ // startOver will cause this instance to be thrown away, so there's
+ // nothing else to do.
+ break;
+ }
+ },
+
+ /**
+ * Compute the sha256 of the message bytes. Return bytes.
+ */
+ _sha256: function(message) {
+ let hasher = Cc["@mozilla.org/security/hash;1"]
+ .createInstance(Ci.nsICryptoHash);
+ hasher.init(hasher.SHA256);
+ return CryptoUtils.digestBytes(message, hasher);
+ },
+
+ /**
+ * Compute the X-Client-State header given the byte string kB.
+ *
+ * Return string: hex(first16Bytes(sha256(kBbytes)))
+ */
+ _computeXClientState: function(kBbytes) {
+ return CommonUtils.bytesAsHex(this._sha256(kBbytes).slice(0, 16), false);
+ },
+
+ /**
+ * Provide override point for testing token expiration.
+ */
+ _now: function() {
+ return this._fxaService.now()
+ },
+
+ get _localtimeOffsetMsec() {
+ return this._fxaService.localtimeOffsetMsec;
+ },
+
+ usernameFromAccount: function(val) {
+ // we don't differentiate between "username" and "account"
+ return val;
+ },
+
+ /**
+ * Obtains the HTTP Basic auth password.
+ *
+ * Returns a string if set or null if it is not set.
+ */
+ get basicPassword() {
+ this._log.error("basicPassword getter should be not used in BrowserIDManager");
+ return null;
+ },
+
+ /**
+ * Set the HTTP basic password to use.
+ *
+ * Changes will not persist unless persistSyncCredentials() is called.
+ */
+ set basicPassword(value) {
+ throw "basicPassword setter should be not used in BrowserIDManager";
+ },
+
+ /**
+ * Obtain the Sync Key.
+ *
+ * This returns a 26 character "friendly" Base32 encoded string on success or
+ * null if no Sync Key could be found.
+ *
+ * If the Sync Key hasn't been set in this session, this will look in the
+ * password manager for the sync key.
+ */
+ get syncKey() {
+ if (this.syncKeyBundle) {
+ // TODO: This is probably fine because the code shouldn't be
+ // using the sync key directly (it should use the sync key
+ // bundle), but I don't like it. We should probably refactor
+ // code that is inspecting this to not do validation on this
+ // field directly and instead call a isSyncKeyValid() function
+ // that we can override.
+ return "99999999999999999999999999";
+ }
+ else {
+ return null;
+ }
+ },
+
+ set syncKey(value) {
+ throw "syncKey setter should be not used in BrowserIDManager";
+ },
+
+ get syncKeyBundle() {
+ return this._syncKeyBundle;
+ },
+
+ /**
+ * Resets/Drops all credentials we hold for the current user.
+ */
+ resetCredentials: function() {
+ this.resetSyncKey();
+ this._token = null;
+ },
+
+ /**
+ * Resets/Drops the sync key we hold for the current user.
+ */
+ resetSyncKey: function() {
+ this._syncKey = null;
+ this._syncKeyBundle = null;
+ this._syncKeyUpdated = true;
+ this._shouldHaveSyncKeyBundle = false;
+ },
+
+ /**
+ * Pre-fetches any information that might help with migration away from this
+ * identity. Called after every sync and is really just an optimization that
+ * allows us to avoid a network request for when we actually need the
+ * migration info.
+ */
+ prefetchMigrationSentinel: function(service) {
+ // nothing to do here until we decide to migrate away from FxA.
+ },
+
+ /**
+ * Return credentials hosts for this identity only.
+ */
+ _getSyncCredentialsHosts: function() {
+ return Utils.getSyncCredentialsHostsFxA();
+ },
+
+ /**
+ * The current state of the auth credentials.
+ *
+ * This essentially validates that enough credentials are available to use
+ * Sync. It doesn't check we have all the keys we need as the master-password
+ * may have been locked when we tried to get them - we rely on
+ * unlockAndVerifyAuthState to check that for us.
+ */
+ get currentAuthState() {
+ if (this._authFailureReason) {
+ this._log.info("currentAuthState returning " + this._authFailureReason +
+ " due to previous failure");
+ return this._authFailureReason;
+ }
+ // TODO: need to revisit this. Currently this isn't ready to go until
+ // both the username and syncKeyBundle are both configured and having no
+ // username seems to make things fail fast so that's good.
+ if (!this.username) {
+ return LOGIN_FAILED_NO_USERNAME;
+ }
+
+ return STATUS_OK;
+ },
+
+ // Do we currently have keys, or do we have enough that we should be able
+ // to successfully fetch them?
+ _canFetchKeys: function() {
+ let userData = this._signedInUser;
+ // a keyFetchToken means we can almost certainly grab them.
+ // kA and kB means we already have them.
+ return userData && (userData.keyFetchToken || (userData.kA && userData.kB));
+ },
+
+ /**
+ * Verify the current auth state, unlocking the master-password if necessary.
+ *
+ * Returns a promise that resolves with the current auth state after
+ * attempting to unlock.
+ */
+ unlockAndVerifyAuthState: function() {
+ if (this._canFetchKeys()) {
+ log.debug("unlockAndVerifyAuthState already has (or can fetch) sync keys");
+ return Promise.resolve(STATUS_OK);
+ }
+ // so no keys - ensure MP unlocked.
+ if (!Utils.ensureMPUnlocked()) {
+ // user declined to unlock, so we don't know if they are stored there.
+ log.debug("unlockAndVerifyAuthState: user declined to unlock master-password");
+ return Promise.resolve(MASTER_PASSWORD_LOCKED);
+ }
+ // now we are unlocked we must re-fetch the user data as we may now have
+ // the details that were previously locked away.
+ return this._fxaService.getSignedInUser().then(
+ accountData => {
+ this._updateSignedInUser(accountData);
+ // If we still can't get keys it probably means the user authenticated
+ // without unlocking the MP or cleared the saved logins, so we've now
+ // lost them - the user will need to reauth before continuing.
+ let result = this._canFetchKeys() ? STATUS_OK : LOGIN_FAILED_LOGIN_REJECTED;
+ log.debug("unlockAndVerifyAuthState re-fetched credentials and is returning", result);
+ return result;
+ }
+ );
+ },
+
+ /**
+ * Do we have a non-null, not yet expired token for the user currently
+ * signed in?
+ */
+ hasValidToken: function() {
+ // If pref is set to ignore cached authentication credentials for debugging,
+ // then return false to force the fetching of a new token.
+ let ignoreCachedAuthCredentials = false;
+ try {
+ ignoreCachedAuthCredentials = Svc.Prefs.get("debug.ignoreCachedAuthCredentials");
+ } catch(e) {
+ // Pref doesn't exist
+ }
+ if (ignoreCachedAuthCredentials) {
+ return false;
+ }
+ if (!this._token) {
+ return false;
+ }
+ if (this._token.expiration < this._now()) {
+ return false;
+ }
+ return true;
+ },
+
+ // Refresh the sync token for our user. Returns a promise that resolves
+ // with a token (which may be null in one sad edge-case), or rejects with an
+ // error.
+ _fetchTokenForUser: function() {
+ let tokenServerURI = Svc.Prefs.get("tokenServerURI");
+ if (tokenServerURI.endsWith("/")) { // trailing slashes cause problems...
+ tokenServerURI = tokenServerURI.slice(0, -1);
+ }
+ let log = this._log;
+ let client = this._tokenServerClient;
+ let fxa = this._fxaService;
+ let userData = this._signedInUser;
+
+ // We need kA and kB for things to work. If we don't have them, just
+ // return null for the token - sync calling unlockAndVerifyAuthState()
+ // before actually syncing will setup the error states if necessary.
+ if (!this._canFetchKeys()) {
+ log.info("Unable to fetch keys (master-password locked?), so aborting token fetch");
+ return Promise.resolve(null);
+ }
+
+ let maybeFetchKeys = () => {
+ // This is called at login time and every time we need a new token - in
+ // the latter case we already have kA and kB, so optimise that case.
+ if (userData.kA && userData.kB) {
+ return;
+ }
+ log.info("Fetching new keys");
+ return this._fxaService.getKeys().then(
+ newUserData => {
+ userData = newUserData;
+ this._updateSignedInUser(userData); // throws if the user changed.
+ }
+ );
+ }
+
+ let getToken = (tokenServerURI, assertion) => {
+ log.debug("Getting a token");
+ let deferred = Promise.defer();
+ let cb = function (err, token) {
+ if (err) {
+ return deferred.reject(err);
+ }
+ log.debug("Successfully got a sync token");
+ return deferred.resolve(token);
+ };
+
+ let kBbytes = CommonUtils.hexToBytes(userData.kB);
+ let headers = {"X-Client-State": this._computeXClientState(kBbytes)};
+ client.getTokenFromBrowserIDAssertion(tokenServerURI, assertion, cb, headers);
+ return deferred.promise;
+ }
+
+ let getAssertion = () => {
+ log.info("Getting an assertion from", tokenServerURI);
+ let audience = Services.io.newURI(tokenServerURI, null, null).prePath;
+ return fxa.getAssertion(audience);
+ };
+
+ // wait until the account email is verified and we know that
+ // getAssertion() will return a real assertion (not null).
+ return fxa.whenVerified(this._signedInUser)
+ .then(() => maybeFetchKeys())
+ .then(() => getAssertion())
+ .then(assertion => getToken(tokenServerURI, assertion))
+ .then(token => {
+ // TODO: Make it be only 80% of the duration, so refresh the token
+ // before it actually expires. This is to avoid sync storage errors
+ // otherwise, we get a nasty notification bar briefly. Bug 966568.
+ token.expiration = this._now() + (token.duration * 1000) * 0.80;
+ if (!this._syncKeyBundle) {
+ // We are given kA/kB as hex.
+ this._syncKeyBundle = deriveKeyBundle(Utils.hexToBytes(userData.kB));
+ }
+ return token;
+ })
+ .then(null, err => {
+ // TODO: unify these errors - we need to handle errors thrown by
+ // both tokenserverclient and hawkclient.
+ // A tokenserver error thrown based on a bad response.
+ if (err.response && err.response.status === 401) {
+ err = new AuthenticationError(err);
+ // A hawkclient error.
+ } else if (err.code && err.code === 401) {
+ err = new AuthenticationError(err);
+ }
+
+ // TODO: write tests to make sure that different auth error cases are handled here
+ // properly: auth error getting assertion, auth error getting token (invalid generation
+ // and client-state error)
+ if (err instanceof AuthenticationError) {
+ this._log.error("Authentication error in _fetchTokenForUser", err);
+ // set it to the "fatal" LOGIN_FAILED_LOGIN_REJECTED reason.
+ this._authFailureReason = LOGIN_FAILED_LOGIN_REJECTED;
+ } else {
+ this._log.error("Non-authentication error in _fetchTokenForUser", err);
+ // for now assume it is just a transient network related problem
+ // (although sadly, it might also be a regular unhandled exception)
+ this._authFailureReason = LOGIN_FAILED_NETWORK_ERROR;
+ }
+ // this._authFailureReason being set to be non-null in the above if clause
+ // ensures we are in the correct currentAuthState, and
+ // this._shouldHaveSyncKeyBundle being true ensures everything that cares knows
+ // that there is no authentication dance still under way.
+ this._shouldHaveSyncKeyBundle = true;
+ Weave.Status.login = this._authFailureReason;
+ Services.obs.notifyObservers(null, "weave:ui:login:error", null);
+ throw err;
+ });
+ },
+
+ // Returns a promise that is resolved when we have a valid token for the
+ // current user stored in this._token. When resolved, this._token is valid.
+ _ensureValidToken: function() {
+ if (this.hasValidToken()) {
+ this._log.debug("_ensureValidToken already has one");
+ return Promise.resolve();
+ }
+ // reset this._token as a safety net to reduce the possibility of us
+ // repeatedly attempting to use an invalid token if _fetchTokenForUser throws.
+ this._token = null;
+ return this._fetchTokenForUser().then(
+ token => {
+ this._token = token;
+ }
+ );
+ },
+
+ getResourceAuthenticator: function () {
+ return this._getAuthenticationHeader.bind(this);
+ },
+
+ /**
+ * Obtain a function to be used for adding auth to RESTRequest instances.
+ */
+ getRESTRequestAuthenticator: function() {
+ return this._addAuthenticationHeader.bind(this);
+ },
+
+ /**
+ * @return a Hawk HTTP Authorization Header, lightly wrapped, for the .uri
+ * of a RESTRequest or AsyncResponse object.
+ */
+ _getAuthenticationHeader: function(httpObject, method) {
+ let cb = Async.makeSpinningCallback();
+ this._ensureValidToken().then(cb, cb);
+ try {
+ cb.wait();
+ } catch (ex) {
+ this._log.error("Failed to fetch a token for authentication", ex);
+ return null;
+ }
+ if (!this._token) {
+ return null;
+ }
+ let credentials = {algorithm: "sha256",
+ id: this._token.id,
+ key: this._token.key,
+ };
+ method = method || httpObject.method;
+
+ // Get the local clock offset from the Firefox Accounts server. This should
+ // be close to the offset from the storage server.
+ let options = {
+ now: this._now(),
+ localtimeOffsetMsec: this._localtimeOffsetMsec,
+ credentials: credentials,
+ };
+
+ let headerValue = CryptoUtils.computeHAWK(httpObject.uri, method, options);
+ return {headers: {authorization: headerValue.field}};
+ },
+
+ _addAuthenticationHeader: function(request, method) {
+ let header = this._getAuthenticationHeader(request, method);
+ if (!header) {
+ return null;
+ }
+ request.setHeader("authorization", header.headers.authorization);
+ return request;
+ },
+
+ createClusterManager: function(service) {
+ return new BrowserIDClusterManager(service);
+ }
+
+};
+
+/* An implementation of the ClusterManager for this identity
+ */
+
+function BrowserIDClusterManager(service) {
+ ClusterManager.call(this, service);
+}
+
+BrowserIDClusterManager.prototype = {
+ __proto__: ClusterManager.prototype,
+
+ _findCluster: function() {
+ let endPointFromIdentityToken = function() {
+ // The only reason (in theory ;) that we can end up with a null token
+ // is when this.identity._canFetchKeys() returned false. In turn, this
+ // should only happen if the master-password is locked or the credentials
+ // storage is screwed, and in those cases we shouldn't have started
+ // syncing so shouldn't get here anyway.
+ // But better safe than sorry! To keep things clearer, throw an explicit
+ // exception - the message will appear in the logs and the error will be
+ // treated as transient.
+ if (!this.identity._token) {
+ throw new Error("Can't get a cluster URL as we can't fetch keys.");
+ }
+ let endpoint = this.identity._token.endpoint;
+ // For Sync 1.5 storage endpoints, we use the base endpoint verbatim.
+ // However, it should end in "/" because we will extend it with
+ // well known path components. So we add a "/" if it's missing.
+ if (!endpoint.endsWith("/")) {
+ endpoint += "/";
+ }
+ log.debug("_findCluster returning " + endpoint);
+ return endpoint;
+ }.bind(this);
+
+ // Spinningly ensure we are ready to authenticate and have a valid token.
+ let promiseClusterURL = function() {
+ return this.identity.whenReadyToAuthenticate.promise.then(
+ () => {
+ // We need to handle node reassignment here. If we are being asked
+ // for a clusterURL while the service already has a clusterURL, then
+ // it's likely a 401 was received using the existing token - in which
+ // case we just discard the existing token and fetch a new one.
+ if (this.service.clusterURL) {
+ log.debug("_findCluster found existing clusterURL, so discarding the current token");
+ this.identity._token = null;
+ }
+ return this.identity._ensureValidToken();
+ }
+ ).then(endPointFromIdentityToken
+ );
+ }.bind(this);
+
+ let cb = Async.makeSpinningCallback();
+ promiseClusterURL().then(function (clusterURL) {
+ cb(null, clusterURL);
+ }).then(
+ null, err => {
+ log.info("Failed to fetch the cluster URL", err);
+ // service.js's verifyLogin() method will attempt to fetch a cluster
+ // URL when it sees a 401. If it gets null, it treats it as a "real"
+ // auth error and sets Status.login to LOGIN_FAILED_LOGIN_REJECTED, which
+ // in turn causes a notification bar to appear informing the user they
+ // need to re-authenticate.
+ // On the other hand, if fetching the cluster URL fails with an exception,
+ // verifyLogin() assumes it is a transient error, and thus doesn't show
+ // the notification bar under the assumption the issue will resolve
+ // itself.
+ // Thus:
+ // * On a real 401, we must return null.
+ // * On any other problem we must let an exception bubble up.
+ if (err instanceof AuthenticationError) {
+ // callback with no error and a null result - cb.wait() returns null.
+ cb(null, null);
+ } else {
+ // callback with an error - cb.wait() completes by raising an exception.
+ cb(err);
+ }
+ });
+ return cb.wait();
+ },
+
+ getUserBaseURL: function() {
+ // Legacy Sync and FxA Sync construct the userBaseURL differently. Legacy
+ // Sync appends path components onto an empty path, and in FxA Sync the
+ // token server constructs this for us in an opaque manner. Since the
+ // cluster manager already sets the clusterURL on Service and also has
+ // access to the current identity, we added this functionality here.
+ return this.service.clusterURL;
+ }
+}
diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js
index 9b6535d34..c8d66d921 100644
--- a/services/sync/modules/constants.js
+++ b/services/sync/modules/constants.js
@@ -54,6 +54,9 @@ HMAC_EVENT_INTERVAL: 600000,
// How long to wait between sync attempts if the Master Password is locked.
MASTER_PASSWORD_LOCKED_RETRY_INTERVAL: 15 * 60 * 1000, // 15 minutes
+// The default for how long we "block" sync from running when doing a migration.
+DEFAULT_BLOCK_PERIOD: 2 * 24 * 60 * 60 * 1000, // 2 days
+
// Separate from the ID fetch batch size to allow tuning for mobile.
MOBILE_BATCH_SIZE: 50,
@@ -119,6 +122,7 @@ LOGIN_FAILED_NETWORK_ERROR: "error.login.reason.network",
LOGIN_FAILED_SERVER_ERROR: "error.login.reason.server",
LOGIN_FAILED_INVALID_PASSPHRASE: "error.login.reason.recoverykey",
LOGIN_FAILED_LOGIN_REJECTED: "error.login.reason.account",
+LOGIN_FAILED_NOT_READY: "error.login.reason.initializing",
// sync failure status codes
METARECORD_DOWNLOAD_FAIL: "error.sync.reason.metarecord_download_fail",
diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js
index 74be244e8..49569f11d 100644
--- a/services/sync/modules/engines.js
+++ b/services/sync/modules/engines.js
@@ -13,7 +13,7 @@ this.EXPORTED_SYMBOLS = [
const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
Cu.import("resource://services-common/async.js");
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-common/observers.js");
Cu.import("resource://services-common/utils.js");
Cu.import("resource://services-sync/constants.js");
@@ -42,16 +42,22 @@ this.Tracker = function Tracker(name, engine) {
this.name = this.file = name.toLowerCase();
this.engine = engine;
- this._log = Log4Moz.repository.getLogger("Sync.Tracker." + name);
+ this._log = Log.repository.getLogger("Sync.Tracker." + name);
let level = Svc.Prefs.get("log.logger.engine." + this.name, "Debug");
- this._log.level = Log4Moz.Level[level];
+ this._log.level = Log.Level[level];
this._score = 0;
this._ignored = [];
this.ignoreAll = false;
this.changedIDs = {};
this.loadChangedIDs();
-}
+
+ Svc.Obs.add("weave:engine:start-tracking", this);
+ Svc.Obs.add("weave:engine:stop-tracking", this);
+
+ Svc.Prefs.observe("engine." + this.engine.prefName, this);
+};
+
Tracker.prototype = {
/*
* Score can be called as often as desired to decide which engines to sync
@@ -73,7 +79,7 @@ Tracker.prototype = {
},
// Should be called by service everytime a sync has been done for an engine
- resetScore: function T_resetScore() {
+ resetScore: function () {
this._score = 0;
},
@@ -88,7 +94,7 @@ Tracker.prototype = {
this._log.debug("Not saving changedIDs.");
return;
}
- Utils.namedTimer(function() {
+ Utils.namedTimer(function () {
this._log.debug("Saving changed IDs to " + this.file);
Utils.jsonSave("changes/" + this.file, this, this.changedIDs, cb);
}, 1000, this, "_lazySave");
@@ -112,39 +118,43 @@ Tracker.prototype = {
// being processed, or that shouldn't be synced.
// But note: not persisted to disk
- ignoreID: function T_ignoreID(id) {
+ ignoreID: function (id) {
this.unignoreID(id);
this._ignored.push(id);
},
- unignoreID: function T_unignoreID(id) {
+ unignoreID: function (id) {
let index = this._ignored.indexOf(id);
if (index != -1)
this._ignored.splice(index, 1);
},
- addChangedID: function addChangedID(id, when) {
+ addChangedID: function (id, when) {
if (!id) {
this._log.warn("Attempted to add undefined ID to tracker");
return false;
}
- if (this.ignoreAll || (id in this._ignored))
+
+ if (this.ignoreAll || (id in this._ignored)) {
return false;
+ }
- // Default to the current time in seconds if no time is provided
- if (when == null)
+ // Default to the current time in seconds if no time is provided.
+ if (when == null) {
when = Math.floor(Date.now() / 1000);
+ }
- // Add/update the entry if we have a newer time
+ // Add/update the entry if we have a newer time.
if ((this.changedIDs[id] || -Infinity) < when) {
this._log.trace("Adding changed ID: " + id + ", " + when);
this.changedIDs[id] = when;
this.saveChangedIDs(this.onSavedChangedIDs);
}
+
return true;
},
- removeChangedID: function T_removeChangedID(id) {
+ removeChangedID: function (id) {
if (!id) {
this._log.warn("Attempted to remove undefined ID to tracker");
return false;
@@ -159,10 +169,69 @@ Tracker.prototype = {
return true;
},
- clearChangedIDs: function T_clearChangedIDs() {
+ clearChangedIDs: function () {
this._log.trace("Clearing changed ID list");
this.changedIDs = {};
this.saveChangedIDs();
+ },
+
+ _isTracking: false,
+
+ // Override these in your subclasses.
+ startTracking: function () {
+ },
+
+ stopTracking: function () {
+ },
+
+ engineIsEnabled: function () {
+ if (!this.engine) {
+ // Can't tell -- we must be running in a test!
+ return true;
+ }
+ return this.engine.enabled;
+ },
+
+ onEngineEnabledChanged: function (engineEnabled) {
+ if (engineEnabled == this._isTracking) {
+ return;
+ }
+
+ if (engineEnabled) {
+ this.startTracking();
+ this._isTracking = true;
+ } else {
+ this.stopTracking();
+ this._isTracking = false;
+ this.clearChangedIDs();
+ }
+ },
+
+ observe: function (subject, topic, data) {
+ switch (topic) {
+ case "weave:engine:start-tracking":
+ if (!this.engineIsEnabled()) {
+ return;
+ }
+ this._log.trace("Got start-tracking.");
+ if (!this._isTracking) {
+ this.startTracking();
+ this._isTracking = true;
+ }
+ return;
+ case "weave:engine:stop-tracking":
+ this._log.trace("Got stop-tracking.");
+ if (this._isTracking) {
+ this.stopTracking();
+ this._isTracking = false;
+ }
+ return;
+ case "nsPref:changed":
+ if (data == PREFS_BRANCH + "engine." + this.engine.prefName) {
+ this.onEngineEnabledChanged(this.engine.enabled);
+ }
+ return;
+ }
}
};
@@ -197,9 +266,9 @@ this.Store = function Store(name, engine) {
this.name = name.toLowerCase();
this.engine = engine;
- this._log = Log4Moz.repository.getLogger("Sync.Store." + name);
+ this._log = Log.repository.getLogger("Sync.Store." + name);
let level = Svc.Prefs.get("log.logger.engine." + this.name, "Debug");
- this._log.level = Log4Moz.Level[level];
+ this._log.level = Log.Level[level];
XPCOMUtils.defineLazyGetter(this, "_timer", function() {
return Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
@@ -228,7 +297,7 @@ Store.prototype = {
* @param records Array of records to apply
* @return Array of record IDs which did not apply cleanly
*/
- applyIncomingBatch: function applyIncomingBatch(records) {
+ applyIncomingBatch: function (records) {
let failed = [];
for each (let record in records) {
try {
@@ -260,7 +329,7 @@ Store.prototype = {
* @param record
* Record to apply
*/
- applyIncoming: function Store_applyIncoming(record) {
+ applyIncoming: function (record) {
if (record.deleted)
this.remove(record);
else if (!this.itemExists(record.id))
@@ -280,7 +349,7 @@ Store.prototype = {
* @param record
* The store record to create an item from
*/
- create: function Store_create(record) {
+ create: function (record) {
throw "override create in a subclass";
},
@@ -293,7 +362,7 @@ Store.prototype = {
* @param record
* The store record to delete an item from
*/
- remove: function Store_remove(record) {
+ remove: function (record) {
throw "override remove in a subclass";
},
@@ -306,7 +375,7 @@ Store.prototype = {
* @param record
* The record to use to update an item from
*/
- update: function Store_update(record) {
+ update: function (record) {
throw "override update in a subclass";
},
@@ -320,7 +389,7 @@ Store.prototype = {
* string record ID
* @return boolean indicating whether record exists locally
*/
- itemExists: function Store_itemExists(id) {
+ itemExists: function (id) {
throw "override itemExists in a subclass";
},
@@ -338,7 +407,7 @@ Store.prototype = {
* constructor for the newly-created record.
* @return record type for this engine
*/
- createRecord: function Store_createRecord(id, collection) {
+ createRecord: function (id, collection) {
throw "override createRecord in a subclass";
},
@@ -350,7 +419,7 @@ Store.prototype = {
* @param newID
* string new record ID
*/
- changeItemID: function Store_changeItemID(oldID, newID) {
+ changeItemID: function (oldID, newID) {
throw "override changeItemID in a subclass";
},
@@ -360,7 +429,7 @@ Store.prototype = {
* @return Object with ID strings as keys and values of true. The values
* are ignored.
*/
- getAllIDs: function Store_getAllIDs() {
+ getAllIDs: function () {
throw "override getAllIDs in a subclass";
},
@@ -374,7 +443,7 @@ Store.prototype = {
* can be thought of as clearing out all state and restoring the "new
* browser" state.
*/
- wipe: function Store_wipe() {
+ wipe: function () {
throw "override wipe in a subclass";
}
};
@@ -383,19 +452,22 @@ this.EngineManager = function EngineManager(service) {
this.service = service;
this._engines = {};
- this._log = Log4Moz.repository.getLogger("Sync.EngineManager");
- this._log.level = Log4Moz.Level[Svc.Prefs.get(
- "log.logger.service.engines", "Debug")];
+
+ // This will be populated by Service on startup.
+ this._declined = new Set();
+ this._log = Log.repository.getLogger("Sync.EngineManager");
+ this._log.level = Log.Level[Svc.Prefs.get("log.logger.service.engines", "Debug")];
}
EngineManager.prototype = {
- get: function get(name) {
+ get: function (name) {
// Return an array of engines if we have an array of names
if (Array.isArray(name)) {
let engines = [];
name.forEach(function(name) {
let engine = this.get(name);
- if (engine)
+ if (engine) {
engines.push(engine);
+ }
}, this);
return engines;
}
@@ -403,18 +475,80 @@ EngineManager.prototype = {
let engine = this._engines[name];
if (!engine) {
this._log.debug("Could not get engine: " + name);
- if (Object.keys)
+ if (Object.keys) {
this._log.debug("Engines are: " + JSON.stringify(Object.keys(this._engines)));
+ }
}
return engine;
},
- getAll: function getAll() {
+ getAll: function () {
return [engine for ([name, engine] in Iterator(this._engines))];
},
- getEnabled: function getEnabled() {
- return this.getAll().filter(function(engine) engine.enabled);
+ /**
+ * N.B., does not pay attention to the declined list.
+ */
+ getEnabled: function () {
+ return this.getAll()
+ .filter((engine) => engine.enabled)
+ .sort((a, b) => a.syncPriority - b.syncPriority);
+ },
+
+ get enabledEngineNames() {
+ return [e.name for each (e in this.getEnabled())];
+ },
+
+ persistDeclined: function () {
+ Svc.Prefs.set("declinedEngines", [...this._declined].join(","));
+ },
+
+ /**
+ * Returns an array.
+ */
+ getDeclined: function () {
+ return [...this._declined];
+ },
+
+ setDeclined: function (engines) {
+ this._declined = new Set(engines);
+ this.persistDeclined();
+ },
+
+ isDeclined: function (engineName) {
+ return this._declined.has(engineName);
+ },
+
+ /**
+ * Accepts a Set or an array.
+ */
+ decline: function (engines) {
+ for (let e of engines) {
+ this._declined.add(e);
+ }
+ this.persistDeclined();
+ },
+
+ undecline: function (engines) {
+ for (let e of engines) {
+ this._declined.delete(e);
+ }
+ this.persistDeclined();
+ },
+
+ /**
+ * Mark any non-enabled engines as declined.
+ *
+ * This is useful after initial customization during setup.
+ */
+ declineDisabled: function () {
+ for (let e of this.getAll()) {
+ if (!e.enabled) {
+ this._log.debug("Declining disabled engine " + e.name);
+ this._declined.add(e.name);
+ }
+ }
+ this.persistDeclined();
},
/**
@@ -425,19 +559,20 @@ EngineManager.prototype = {
* Engine object used to get an instance of the engine
* @return The engine object if anything failed
*/
- register: function register(engineObject) {
- if (Array.isArray(engineObject))
+ register: function (engineObject) {
+ if (Array.isArray(engineObject)) {
return engineObject.map(this.register, this);
+ }
try {
let engine = new engineObject(this.service);
let name = engine.name;
- if (name in this._engines)
+ if (name in this._engines) {
this._log.error("Engine '" + name + "' is already registered!");
- else
+ } else {
this._engines[name] = engine;
- }
- catch(ex) {
+ }
+ } catch (ex) {
this._log.error(CommonUtils.exceptionStr(ex));
let mesg = ex.message ? ex.message : ex;
@@ -452,14 +587,15 @@ EngineManager.prototype = {
}
},
- unregister: function unregister(val) {
+ unregister: function (val) {
let name = val;
- if (val instanceof Engine)
+ if (val instanceof Engine) {
name = val.name;
+ }
delete this._engines[name];
},
- clear: function clear() {
+ clear: function () {
for (let name in this._engines) {
delete this._engines[name];
}
@@ -476,9 +612,9 @@ this.Engine = function Engine(name, service) {
this.service = service;
this._notify = Utils.notify("weave:engine:");
- this._log = Log4Moz.repository.getLogger("Sync.Engine." + this.Name);
+ this._log = Log.repository.getLogger("Sync.Engine." + this.Name);
let level = Svc.Prefs.get("log.logger.engine." + this.name, "Debug");
- this._log.level = Log4Moz.Level[level];
+ this._log.level = Log.Level[level];
this._tracker; // initialize tracker to load previously changed IDs
this._log.debug("Engine initialized");
@@ -492,45 +628,58 @@ Engine.prototype = {
// Signal to the engine that processing further records is pointless.
eEngineAbortApplyIncoming: "error.engine.abort.applyincoming",
- get prefName() this.name,
- get enabled() Svc.Prefs.get("engine." + this.prefName, false),
- set enabled(val) Svc.Prefs.set("engine." + this.prefName, !!val),
+ get prefName() {
+ return this.name;
+ },
+
+ get enabled() {
+ return Svc.Prefs.get("engine." + this.prefName, false);
+ },
- get score() this._tracker.score,
+ set enabled(val) {
+ Svc.Prefs.set("engine." + this.prefName, !!val);
+ },
+
+ get score() {
+ return this._tracker.score;
+ },
get _store() {
let store = new this._storeObj(this.Name, this);
- this.__defineGetter__("_store", function() store);
+ this.__defineGetter__("_store", () => store);
return store;
},
get _tracker() {
let tracker = new this._trackerObj(this.Name, this);
- this.__defineGetter__("_tracker", function() tracker);
+ this.__defineGetter__("_tracker", () => tracker);
return tracker;
},
- sync: function Engine_sync() {
- if (!this.enabled)
+ sync: function () {
+ if (!this.enabled) {
return;
+ }
- if (!this._sync)
+ if (!this._sync) {
throw "engine does not implement _sync method";
+ }
this._notify("sync", this.name, this._sync)();
},
/**
- * Get rid of any local meta-data
+ * Get rid of any local meta-data.
*/
- resetClient: function Engine_resetClient() {
- if (!this._resetClient)
+ resetClient: function () {
+ if (!this._resetClient) {
throw "engine does not implement _resetClient method";
+ }
this._notify("reset-client", this.name, this._resetClient)();
},
- _wipeClient: function Engine__wipeClient() {
+ _wipeClient: function () {
this.resetClient();
this._log.debug("Deleting all local data");
this._tracker.ignoreAll = true;
@@ -539,7 +688,7 @@ Engine.prototype = {
this._tracker.clearChangedIDs();
},
- wipeClient: function Engine_wipeClient() {
+ wipeClient: function () {
this._notify("wipe-client", this.name, this._wipeClient)();
}
};
@@ -563,27 +712,44 @@ SyncEngine.prototype = {
__proto__: Engine.prototype,
_recordObj: CryptoWrapper,
version: 1,
-
+
+ // Which sortindex to use when retrieving records for this engine.
+ _defaultSort: undefined,
+
+ // A relative priority to use when computing an order
+ // for engines to be synced. Higher-priority engines
+ // (lower numbers) are synced first.
+ // It is recommended that a unique value be used for each engine,
+ // in order to guarantee a stable sequence.
+ syncPriority: 0,
+
// How many records to pull in a single sync. This is primarily to avoid very
// long first syncs against profiles with many history records.
downloadLimit: null,
-
+
// How many records to pull at one time when specifying IDs. This is to avoid
// URI length limitations.
guidFetchBatchSize: DEFAULT_GUID_FETCH_BATCH_SIZE,
mobileGUIDFetchBatchSize: DEFAULT_MOBILE_GUID_FETCH_BATCH_SIZE,
-
+
// How many records to process in a single batch.
applyIncomingBatchSize: DEFAULT_STORE_BATCH_SIZE,
- get storageURL() Svc.Prefs.get("clusterURL") + SYNC_API_VERSION +
- "/" + this.service.identity.username + "/storage/",
+ get storageURL() {
+ return this.service.storageURL;
+ },
- get engineURL() this.storageURL + this.name,
+ get engineURL() {
+ return this.storageURL + this.name;
+ },
- get cryptoKeysURL() this.storageURL + "crypto/keys",
+ get cryptoKeysURL() {
+ return this.storageURL + "crypto/keys";
+ },
- get metaURL() this.storageURL + "meta/global",
+ get metaURL() {
+ return this.storageURL + "meta/global";
+ },
get syncID() {
// Generate a random syncID if we don't have one
@@ -606,27 +772,30 @@ SyncEngine.prototype = {
// Store the value as a string to keep floating point precision
Svc.Prefs.set(this.name + ".lastSync", value.toString());
},
- resetLastSync: function SyncEngine_resetLastSync() {
+ resetLastSync: function () {
this._log.debug("Resetting " + this.name + " last sync time");
Svc.Prefs.reset(this.name + ".lastSync");
Svc.Prefs.set(this.name + ".lastSync", "0");
this.lastSyncLocal = 0;
},
- get toFetch() this._toFetch,
+ get toFetch() {
+ return this._toFetch;
+ },
set toFetch(val) {
+ let cb = (error) => this._log.error(Utils.exceptionStr(error));
// Coerce the array to a string for more efficient comparison.
if (val + "" == this._toFetch) {
return;
}
this._toFetch = val;
Utils.namedTimer(function () {
- Utils.jsonSave("toFetch/" + this.name, this, val);
+ Utils.jsonSave("toFetch/" + this.name, this, val, cb);
}, 0, this, "_toFetchDelay");
},
- loadToFetch: function loadToFetch() {
- // Initialize to empty if there's no file
+ loadToFetch: function () {
+ // Initialize to empty if there's no file.
this._toFetch = [];
Utils.jsonLoad("toFetch/" + this.name, this, function(toFetch) {
if (toFetch) {
@@ -635,19 +804,22 @@ SyncEngine.prototype = {
});
},
- get previousFailed() this._previousFailed,
+ get previousFailed() {
+ return this._previousFailed;
+ },
set previousFailed(val) {
+ let cb = (error) => this._log.error(Utils.exceptionStr(error));
// Coerce the array to a string for more efficient comparison.
if (val + "" == this._previousFailed) {
return;
}
this._previousFailed = val;
Utils.namedTimer(function () {
- Utils.jsonSave("failed/" + this.name, this, val);
+ Utils.jsonSave("failed/" + this.name, this, val, cb);
}, 0, this, "_previousFailedDelay");
},
- loadPreviousFailed: function loadPreviousFailed() {
+ loadPreviousFailed: function () {
// Initialize to empty if there's no file
this._previousFailed = [];
Utils.jsonLoad("failed/" + this.name, this, function(previousFailed) {
@@ -673,12 +845,12 @@ SyncEngine.prototype = {
* can override this method to bypass the tracker for certain or all
* changed items.
*/
- getChangedIDs: function getChangedIDs() {
+ getChangedIDs: function () {
return this._tracker.changedIDs;
},
- // Create a new record using the store and add in crypto fields
- _createRecord: function SyncEngine__createRecord(id) {
+ // Create a new record using the store and add in crypto fields.
+ _createRecord: function (id) {
let record = this._store.createRecord(id, this.name);
record.id = id;
record.collection = this.name;
@@ -686,7 +858,7 @@ SyncEngine.prototype = {
},
// Any setup that needs to happen at the beginning of each sync.
- _syncStartup: function SyncEngine__syncStartup() {
+ _syncStartup: function () {
// Determine if we need to wipe on outdated versions
let metaGlobal = this.service.recordManager.get(this.metaURL);
@@ -783,13 +955,17 @@ SyncEngine.prototype = {
newitems = this._itemSource();
}
+ if (this._defaultSort) {
+ newitems.sort = this._defaultSort;
+ }
+
if (isMobile) {
batchSize = MOBILE_BATCH_SIZE;
}
newitems.newer = this.lastSync;
newitems.full = true;
newitems.limit = batchSize;
-
+
// applied => number of items that should be applied.
// failed => number of items that failed in this sync.
// newFailed => number of items that failed for the first time in this sync.
@@ -1031,11 +1207,11 @@ SyncEngine.prototype = {
*
* @return GUID of the similar item; falsy otherwise
*/
- _findDupe: function _findDupe(item) {
+ _findDupe: function (item) {
// By default, assume there's no dupe items for the engine
},
- _deleteId: function _deleteId(id) {
+ _deleteId: function (id) {
this._tracker.removeChangedID(id);
// Remember this id to delete at the end of sync
@@ -1055,8 +1231,8 @@ SyncEngine.prototype = {
* @return boolean
* Truthy if incoming record should be applied. False if not.
*/
- _reconcile: function _reconcile(item) {
- if (this._log.level <= Log4Moz.Level.Trace) {
+ _reconcile: function (item) {
+ if (this._log.level <= Log.Level.Trace) {
this._log.trace("Incoming: " + item);
}
@@ -1227,8 +1403,8 @@ SyncEngine.prototype = {
return remoteIsNewer;
},
- // Upload outgoing records
- _uploadOutgoing: function SyncEngine__uploadOutgoing() {
+ // Upload outgoing records.
+ _uploadOutgoing: function () {
this._log.trace("Uploading local changes to server.");
let modifiedIDs = Object.keys(this._modified);
@@ -1273,7 +1449,7 @@ SyncEngine.prototype = {
for each (let id in modifiedIDs) {
try {
let out = this._createRecord(id);
- if (this._log.level <= Log4Moz.Level.Trace)
+ if (this._log.level <= Log.Level.Trace)
this._log.trace("Outgoing: " + out);
out.encrypt(this.service.collectionKeys.keyForCollection(this.name));
@@ -1298,7 +1474,7 @@ SyncEngine.prototype = {
// Any cleanup necessary.
// Save the current snapshot so as to calculate changes at next sync
- _syncFinish: function SyncEngine__syncFinish() {
+ _syncFinish: function () {
this._log.trace("Finishing up sync");
this._tracker.resetScore();
@@ -1325,9 +1501,10 @@ SyncEngine.prototype = {
}
},
- _syncCleanup: function _syncCleanup() {
- if (!this._modified)
+ _syncCleanup: function () {
+ if (!this._modified) {
return;
+ }
// Mark failed WBOs as changed again so they are reuploaded next time.
for (let [id, when] in Iterator(this._modified)) {
@@ -1336,7 +1513,7 @@ SyncEngine.prototype = {
this._modified = {};
},
- _sync: function SyncEngine__sync() {
+ _sync: function () {
try {
this._syncStartup();
Observers.notify("weave:engine:sync:status", "process-incoming");
@@ -1349,7 +1526,7 @@ SyncEngine.prototype = {
}
},
- canDecrypt: function canDecrypt() {
+ canDecrypt: function () {
// Report failure even if there's nothing to decrypt
let canDecrypt = false;
@@ -1377,13 +1554,13 @@ SyncEngine.prototype = {
return canDecrypt;
},
- _resetClient: function SyncEngine__resetClient() {
+ _resetClient: function () {
this.resetLastSync();
this.previousFailed = [];
this.toFetch = [];
},
- wipeServer: function wipeServer() {
+ wipeServer: function () {
let response = this.service.resource(this.engineURL).delete();
if (response.status != 200 && response.status != 404) {
throw response;
@@ -1391,7 +1568,7 @@ SyncEngine.prototype = {
this._resetClient();
},
- removeClientData: function removeClientData() {
+ removeClientData: function () {
// Implement this method in engines that store client specific data
// on the server.
},
@@ -1412,7 +1589,7 @@ SyncEngine.prototype = {
*
* All return values will be part of the kRecoveryStrategy enumeration.
*/
- handleHMACMismatch: function handleHMACMismatch(item, mayRetry) {
+ handleHMACMismatch: function (item, mayRetry) {
// By default we either try again, or bail out noisily.
return (this.service.handleHMACEvent() && mayRetry) ?
SyncEngine.kRecoveryStrategy.retry :
diff --git a/services/sync/modules/engines/addons.js b/services/sync/modules/engines/addons.js
index d8cf014be..ab3131c30 100644
--- a/services/sync/modules/engines/addons.js
+++ b/services/sync/modules/engines/addons.js
@@ -48,7 +48,7 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository",
- "resource://gre/modules/AddonRepository.jsm");
+ "resource://gre/modules/addons/AddonRepository.jsm");
this.EXPORTED_SYMBOLS = ["AddonsEngine"];
@@ -119,6 +119,8 @@ AddonsEngine.prototype = {
_recordObj: AddonRecord,
version: 1,
+ syncPriority: 5,
+
_reconciler: null,
/**
@@ -655,9 +657,6 @@ AddonsStore.prototype = {
*/
function AddonsTracker(name, engine) {
Tracker.call(this, name, engine);
-
- Svc.Obs.add("weave:engine:start-tracking", this);
- Svc.Obs.add("weave:engine:stop-tracking", this);
}
AddonsTracker.prototype = {
__proto__: Tracker.prototype,
@@ -691,20 +690,16 @@ AddonsTracker.prototype = {
this.score += SCORE_INCREMENT_XLARGE;
},
- observe: function(subject, topic, data) {
- switch (topic) {
- case "weave:engine:start-tracking":
- if (this.engine.enabled) {
- this.reconciler.startListening();
- }
+ startTracking: function() {
+ if (this.engine.enabled) {
+ this.reconciler.startListening();
+ }
- this.reconciler.addChangeListener(this);
- break;
+ this.reconciler.addChangeListener(this);
+ },
- case "weave:engine:stop-tracking":
- this.reconciler.removeChangeListener(this);
- this.reconciler.stopListening();
- break;
- }
- }
+ stopTracking: function() {
+ this.reconciler.removeChangeListener(this);
+ this.reconciler.stopListening();
+ },
};
diff --git a/services/sync/modules/engines/apps.js b/services/sync/modules/engines/apps.js
deleted file mode 100644
index 58967acad..000000000
--- a/services/sync/modules/engines/apps.js
+++ /dev/null
@@ -1,136 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-this.EXPORTED_SYMBOLS = ['AppsEngine', 'AppRec'];
-
-const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
-
-Cu.import("resource://services-sync/util.js");
-Cu.import("resource://services-sync/record.js");
-Cu.import("resource://services-sync/engines.js");
-Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Webapps.jsm");
-
-this.AppRec = function AppRec(collection, id) {
- CryptoWrapper.call(this, collection, id);
-}
-
-AppRec.prototype = {
- __proto__: CryptoWrapper.prototype,
- _logName: "Sync.Record.App"
-}
-
-Utils.deferGetSet(AppRec, "cleartext", ["value"]);
-
-function AppStore(name, engine) {
- Store.call(this, name, engine);
-}
-
-AppStore.prototype = {
- __proto__: Store.prototype,
-
- getAllIDs: function getAllIDs() {
- let apps = DOMApplicationRegistry.getAllIDs();
- return apps;
- },
-
- changeItemID: function changeItemID(oldID, newID) {
- this._log.trace("AppsStore does not support changeItemID");
- },
-
- itemExists: function itemExists(guid) {
- return DOMApplicationRegistry.itemExists(guid);
- },
-
- createRecord: function createRecord(guid, collection) {
- let record = new AppRec(collection, guid);
- let app = DOMApplicationRegistry.getAppById(guid);
-
- if (app) {
- app.syncId = guid;
- let callback = Async.makeSyncCallback();
- DOMApplicationRegistry.getManifestFor(app.origin, function(aManifest) {
- app.manifest = aManifest;
- callback();
- });
- Async.waitForSyncCallback(callback);
- record.value = app;
- } else {
- record.deleted = true;
- }
-
- return record;
- },
-
- applyIncomingBatch: function applyIncomingBatch(aRecords) {
- let callback = Async.makeSyncCallback();
- DOMApplicationRegistry.updateApps(aRecords, callback);
- Async.waitForSyncCallback(callback);
- return [];
- },
-
- wipe: function wipe(record) {
- let callback = Async.makeSyncCallback();
- DOMApplicationRegistry.wipe(callback);
- Async.waitForSyncCallback(callback);
- }
-}
-
-
-function AppTracker(name, engine) {
- Tracker.call(this, name, engine);
- Svc.Obs.add("weave:engine:start-tracking", this);
- Svc.Obs.add("weave:engine:stop-tracking", this);
-}
-
-AppTracker.prototype = {
- __proto__: Tracker.prototype,
- QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
-
- _enabled: false,
-
- observe: function(aSubject, aTopic, aData) {
- switch (aTopic) {
- case "webapps-sync-install":
- case "webapps-sync-uninstall":
- // ask for immediate sync. not sure if we really need this or
- // if a lower score increment would be enough
- let app;
- this.score += SCORE_INCREMENT_XLARGE;
- try {
- app = JSON.parse(aData);
- } catch (e) {
- this._log.error("JSON.parse failed in observer " + e);
- return;
- }
- this.addChangedID(app.id);
- break;
- case "weave:engine:start-tracking":
- this._enabled = true;
- Svc.Obs.add("webapps-sync-install", this);
- Svc.Obs.add("webapps-sync-uninstall", this);
- break;
- case "weave:engine:stop-tracking":
- this._enabled = false;
- Svc.Obs.remove("webapps-sync-install", this);
- Svc.Obs.remove("webapps-sync-uninstall", this);
- break;
- }
- }
-}
-
-this.AppsEngine = function AppsEngine(service) {
- SyncEngine.call(this, "Apps", service);
-}
-
-AppsEngine.prototype = {
- __proto__: SyncEngine.prototype,
- _storeObj: AppStore,
- _trackerObj: AppTracker,
- _recordObj: AppRec,
- applyIncomingBatchSize: APPS_STORE_BATCH_SIZE
-}
diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js
index 7ef86cc73..1936afc3f 100644
--- a/services/sync/modules/engines/bookmarks.js
+++ b/services/sync/modules/engines/bookmarks.js
@@ -185,12 +185,24 @@ let kSpecialIds = {
return null;
},
- get menu() PlacesUtils.bookmarksMenuFolderId,
- get places() PlacesUtils.placesRootId,
- get tags() PlacesUtils.tagsFolderId,
- get toolbar() PlacesUtils.toolbarFolderId,
- get unfiled() PlacesUtils.unfiledBookmarksFolderId,
- get mobile() this.findMobileRoot(true),
+ get menu() {
+ return PlacesUtils.bookmarksMenuFolderId;
+ },
+ get places() {
+ return PlacesUtils.placesRootId;
+ },
+ get tags() {
+ return PlacesUtils.tagsFolderId;
+ },
+ get toolbar() {
+ return PlacesUtils.toolbarFolderId;
+ },
+ get unfiled() {
+ return PlacesUtils.unfiledBookmarksFolderId;
+ },
+ get mobile() {
+ return this.findMobileRoot(true);
+ },
};
this.BookmarksEngine = function BookmarksEngine(service) {
@@ -202,6 +214,9 @@ BookmarksEngine.prototype = {
_storeObj: BookmarksStore,
_trackerObj: BookmarksTracker,
version: 2,
+ _defaultSort: "index",
+
+ syncPriority: 4,
_sync: function _sync() {
let engine = this;
@@ -349,31 +364,42 @@ BookmarksEngine.prototype = {
Task.spawn(function() {
// For first-syncs, make a backup for the user to restore
if (this.lastSync == 0) {
+ this._log.debug("Bookmarks backup starting.");
yield PlacesBackups.create(null, true);
+ this._log.debug("Bookmarks backup done.");
}
+ }.bind(this)).then(
+ cb, ex => {
+ // Failure to create a backup is somewhat bad, but probably not bad
+ // enough to prevent syncing of bookmarks - so just log the error and
+ // continue.
+ this._log.warn("Got exception \"" + Utils.exceptionStr(ex) +
+ "\" backing up bookmarks, but continuing with sync.");
+ cb();
+ }
+ );
- this.__defineGetter__("_guidMap", function() {
- // Create a mapping of folder titles and separator positions to GUID.
- // We do this lazily so that we don't do any work unless we reconcile
- // incoming items.
- let guidMap;
- try {
- guidMap = this._buildGUIDMap();
- } catch (ex) {
- this._log.warn("Got exception \"" + Utils.exceptionStr(ex) +
- "\" building GUID map." +
- " Skipping all other incoming items.");
- throw {code: Engine.prototype.eEngineAbortApplyIncoming,
- cause: ex};
- }
- delete this._guidMap;
- return this._guidMap = guidMap;
- });
-
- this._store._childrenToOrder = {};
- cb();
- }.bind(this));
cb.wait();
+
+ this.__defineGetter__("_guidMap", function() {
+ // Create a mapping of folder titles and separator positions to GUID.
+ // We do this lazily so that we don't do any work unless we reconcile
+ // incoming items.
+ let guidMap;
+ try {
+ guidMap = this._buildGUIDMap();
+ } catch (ex) {
+ this._log.warn("Got exception \"" + Utils.exceptionStr(ex) +
+ "\" building GUID map." +
+ " Skipping all other incoming items.");
+ throw {code: Engine.prototype.eEngineAbortApplyIncoming,
+ cause: ex};
+ }
+ delete this._guidMap;
+ return this._guidMap = guidMap;
+ });
+
+ this._store._childrenToOrder = {};
},
_processIncoming: function (newitems) {
@@ -428,7 +454,7 @@ function BookmarksStore(name, engine) {
// Explicitly nullify our references to our cached services so we don't leak
Svc.Obs.add("places-shutdown", function() {
- for each ([query, stmt] in Iterator(this._stmts)) {
+ for each (let [query, stmt] in Iterator(this._stmts)) {
stmt.finalize();
}
this._stmts = {};
@@ -731,10 +757,10 @@ BookmarksStore.prototype = {
feedURI: Utils.makeURI(record.feedUri),
siteURI: siteURI,
guid: record.id};
- PlacesUtils.livemarks.addLivemark(livemarkObj,
- function (aStatus, aLivemark) {
- spinningCb(null, [aStatus, aLivemark]);
- });
+ PlacesUtils.livemarks.addLivemark(livemarkObj).then(
+ aLivemark => { spinningCb(null, [Components.results.NS_OK, aLivemark]) },
+ () => { spinningCb(null, [Components.results.NS_ERROR_UNEXPECTED, aLivemark]) }
+ );
let [status, livemark] = spinningCb.wait();
if (!Components.isSuccessCode(status)) {
@@ -1257,7 +1283,7 @@ BookmarksStore.prototype = {
}
// Filter out any null/undefined/empty tags.
- tags = tags.filter(function(t) t);
+ tags = tags.filter(t => t);
// Temporarily tag a dummy URI to preserve tag ids when untagging.
let dummyURI = Utils.makeURI("about:weave#BStore_tagURI");
@@ -1298,34 +1324,28 @@ function BookmarksTracker(name, engine) {
Tracker.call(this, name, engine);
Svc.Obs.add("places-shutdown", this);
- Svc.Obs.add("weave:engine:start-tracking", this);
- Svc.Obs.add("weave:engine:stop-tracking", this);
}
BookmarksTracker.prototype = {
__proto__: Tracker.prototype,
- _enabled: false,
+ startTracking: function() {
+ PlacesUtils.bookmarks.addObserver(this, true);
+ Svc.Obs.add("bookmarks-restore-begin", this);
+ Svc.Obs.add("bookmarks-restore-success", this);
+ Svc.Obs.add("bookmarks-restore-failed", this);
+ },
+
+ stopTracking: function() {
+ PlacesUtils.bookmarks.removeObserver(this);
+ Svc.Obs.remove("bookmarks-restore-begin", this);
+ Svc.Obs.remove("bookmarks-restore-success", this);
+ Svc.Obs.remove("bookmarks-restore-failed", this);
+ },
+
observe: function observe(subject, topic, data) {
- switch (topic) {
- case "weave:engine:start-tracking":
- if (!this._enabled) {
- PlacesUtils.bookmarks.addObserver(this, true);
- Svc.Obs.add("bookmarks-restore-begin", this);
- Svc.Obs.add("bookmarks-restore-success", this);
- Svc.Obs.add("bookmarks-restore-failed", this);
- this._enabled = true;
- }
- break;
- case "weave:engine:stop-tracking":
- if (this._enabled) {
- PlacesUtils.bookmarks.removeObserver(this);
- Svc.Obs.remove("bookmarks-restore-begin", this);
- Svc.Obs.remove("bookmarks-restore-success", this);
- Svc.Obs.remove("bookmarks-restore-failed", this);
- this._enabled = false;
- }
- break;
+ Tracker.prototype.observe.call(this, subject, topic, data);
+ switch (topic) {
case "bookmarks-restore-begin":
this._log.debug("Ignoring changes from importing bookmarks.");
this.ignoreAll = true;
@@ -1437,9 +1457,9 @@ BookmarksTracker.prototype = {
},
_ensureMobileQuery: function _ensureMobileQuery() {
- let find = function (val)
+ let find = val =>
PlacesUtils.annotations.getItemsWithAnnotation(ORGANIZERQUERY_ANNO, {}).filter(
- function (id) PlacesUtils.annotations.getItemAnnotation(id, ORGANIZERQUERY_ANNO) == val
+ id => PlacesUtils.annotations.getItemAnnotation(id, ORGANIZERQUERY_ANNO) == val
);
// Don't continue if the Library isn't ready
diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js
index e891ce119..f423242c9 100644
--- a/services/sync/modules/engines/clients.js
+++ b/services/sync/modules/engines/clients.js
@@ -18,6 +18,8 @@ Cu.import("resource://services-sync/util.js");
const CLIENTS_TTL = 1814400; // 21 days
const CLIENTS_TTL_REFRESH = 604800; // 7 days
+const SUPPORTED_PROTOCOL_VERSIONS = ["1.1", "1.5"];
+
this.ClientsRec = function ClientsRec(collection, id) {
CryptoWrapper.call(this, collection, id);
}
@@ -27,7 +29,11 @@ ClientsRec.prototype = {
ttl: CLIENTS_TTL
};
-Utils.deferGetSet(ClientsRec, "cleartext", ["name", "type", "commands"]);
+Utils.deferGetSet(ClientsRec,
+ "cleartext",
+ ["name", "type", "commands",
+ "version", "protocols",
+ "formfactor", "os", "appPackage", "application", "device"]);
this.ClientEngine = function ClientEngine(service) {
@@ -69,6 +75,28 @@ ClientEngine.prototype = {
return stats;
},
+ /**
+ * Obtain information about device types.
+ *
+ * Returns a Map of device types to integer counts.
+ */
+ get deviceTypes() {
+ let counts = new Map();
+
+ counts.set(this.localType, 1);
+
+ for each (let record in this._store._remoteClients) {
+ let type = record.type;
+ if (!counts.has(type)) {
+ counts.set(type, 0);
+ }
+
+ counts.set(type, counts.get(type) + 1);
+ }
+
+ return counts;
+ },
+
get localID() {
// Generate a random GUID id we don't have one
let localID = Svc.Prefs.get("client.GUID", "");
@@ -76,35 +104,17 @@ ClientEngine.prototype = {
},
set localID(value) Svc.Prefs.set("client.GUID", value),
+ get brandName() {
+ let brand = new StringBundle("chrome://branding/locale/brand.properties");
+ return brand.get("brandShortName");
+ },
+
get localName() {
let localName = Svc.Prefs.get("client.name", "");
if (localName != "")
return localName;
- // Generate a client name if we don't have a useful one yet
- let env = Cc["@mozilla.org/process/environment;1"]
- .getService(Ci.nsIEnvironment);
- let user = env.get("USER") || env.get("USERNAME") ||
- Svc.Prefs.get("account") || Svc.Prefs.get("username");
-
- let appName;
- let brand = new StringBundle("chrome://branding/locale/brand.properties");
- let brandName = brand.get("brandShortName");
- try {
- let syncStrings = new StringBundle("chrome://browser/locale/sync.properties");
- appName = syncStrings.getFormattedString("sync.defaultAccountApplication", [brandName]);
- } catch (ex) {}
- appName = appName || brandName;
-
- let system =
- // 'device' is defined on unix systems
- Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2).get("device") ||
- // hostname of the system, usually assigned by the user or admin
- Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2).get("host") ||
- // fall back on ua info string
- Cc["@mozilla.org/network/protocol;1?name=http"].getService(Ci.nsIHttpProtocolHandler).oscpu;
-
- return this.localName = Str.sync.get("client.name2", [user, appName, system]);
+ return this.localName = Utils.getDefaultDeviceName();
},
set localName(value) Svc.Prefs.set("client.name", value),
@@ -132,7 +142,9 @@ ClientEngine.prototype = {
},
// Treat reset the same as wiping for locally cached clients
- _resetClient: function _resetClient() this._wipeClient(),
+ _resetClient() {
+ this._wipeClient();
+ },
_wipeClient: function _wipeClient() {
SyncEngine.prototype._resetClient.call(this);
@@ -236,7 +248,7 @@ ClientEngine.prototype = {
this.clearCommands();
// Process each command in order.
- for each ({command: command, args: args} in commands) {
+ for each (let {command, args} in commands) {
this._log.debug("Processing command: " + command + "(" + args + ")");
let engines = [args[0]];
@@ -368,7 +380,9 @@ function ClientStore(name, engine) {
ClientStore.prototype = {
__proto__: Store.prototype,
- create: function create(record) this.update(record),
+ create(record) {
+ this.update(record)
+ },
update: function update(record) {
// Only grab commands from the server; local name/type always wins
@@ -386,14 +400,27 @@ ClientStore.prototype = {
record.name = this.engine.localName;
record.type = this.engine.localType;
record.commands = this.engine.localCommands;
- }
- else
+ record.version = Services.appinfo.version;
+ record.protocols = SUPPORTED_PROTOCOL_VERSIONS;
+
+ // Optional fields.
+ record.os = Services.appinfo.OS; // "Darwin"
+ record.appPackage = Services.appinfo.ID;
+ record.application = this.engine.brandName // "Nightly"
+
+ // We can't compute these yet.
+ // record.device = ""; // Bug 1100723
+ // record.formfactor = ""; // Bug 1100722
+ } else {
record.cleartext = this._remoteClients[id];
+ }
return record;
},
- itemExists: function itemExists(id) id in this.getAllIDs(),
+ itemExists(id) {
+ return id in this.getAllIDs();
+ },
getAllIDs: function getAllIDs() {
let ids = {};
diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js
index bd74e55bb..d26d57176 100644
--- a/services/sync/modules/engines/forms.js
+++ b/services/sync/modules/engines/forms.js
@@ -14,7 +14,7 @@ Cu.import("resource://services-sync/record.js");
Cu.import("resource://services-common/async.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
const FORMS_TTL = 5184000; // 60 days
@@ -31,7 +31,7 @@ Utils.deferGetSet(FormRec, "cleartext", ["name", "value"]);
let FormWrapper = {
- _log: Log4Moz.repository.getLogger("Sync.Engine.Forms"),
+ _log: Log.repository.getLogger("Sync.Engine.Forms"),
_getEntryCols: ["fieldname", "value"],
_guidCols: ["guid"],
@@ -53,6 +53,9 @@ let FormWrapper = {
},
_updateSpinningly: function(changes) {
+ if (!Svc.FormHistory.enabled) {
+ return; // update isn't going to do anything.
+ }
let cb = Async.makeSpinningCallback();
let callbacks = {
handleCompletion: function(reason) {
@@ -104,6 +107,8 @@ FormEngine.prototype = {
_recordObj: FormRec,
applyIncomingBatchSize: FORMS_STORE_BATCH_SIZE,
+ syncPriority: 6,
+
get prefName() "history",
_findDupe: function _findDupe(item) {
@@ -202,53 +207,32 @@ FormStore.prototype = {
function FormTracker(name, engine) {
Tracker.call(this, name, engine);
- Svc.Obs.add("weave:engine:start-tracking", this);
- Svc.Obs.add("weave:engine:stop-tracking", this);
- Svc.Obs.add("profile-change-teardown", this);
}
FormTracker.prototype = {
__proto__: Tracker.prototype,
QueryInterface: XPCOMUtils.generateQI([
- Ci.nsIFormSubmitObserver,
Ci.nsIObserver,
Ci.nsISupportsWeakReference]),
- _enabled: false,
+ startTracking: function() {
+ Svc.Obs.add("satchel-storage-changed", this);
+ },
+
+ stopTracking: function() {
+ Svc.Obs.remove("satchel-storage-changed", this);
+ },
+
observe: function (subject, topic, data) {
+ Tracker.prototype.observe.call(this, subject, topic, data);
+
switch (topic) {
- case "weave:engine:start-tracking":
- if (!this._enabled) {
- Svc.Obs.add("form-notifier", this);
- Svc.Obs.add("satchel-storage-changed", this);
- // HTMLFormElement doesn't use the normal observer/observe
- // pattern and looks up nsIFormSubmitObservers to .notify()
- // them so add manually to observers
- Cc["@mozilla.org/observer-service;1"]
- .getService(Ci.nsIObserverService)
- .addObserver(this, "earlyformsubmit", true);
- this._enabled = true;
- }
- break;
- case "weave:engine:stop-tracking":
- if (this._enabled) {
- Svc.Obs.remove("form-notifier", this);
- Svc.Obs.remove("satchel-storage-changed", this);
- Cc["@mozilla.org/observer-service;1"]
- .getService(Ci.nsIObserverService)
- .removeObserver(this, "earlyformsubmit");
- this._enabled = false;
- }
- break;
case "satchel-storage-changed":
if (data == "formhistory-add" || data == "formhistory-remove") {
let guid = subject.QueryInterface(Ci.nsISupportsString).toString();
this.trackEntry(guid);
}
break;
- case "profile-change-teardown":
- FormWrapper._finalize();
- break;
}
},
@@ -256,79 +240,4 @@ FormTracker.prototype = {
this.addChangedID(guid);
this.score += SCORE_INCREMENT_MEDIUM;
},
-
- notify: function (formElement, aWindow, actionURI) {
- if (this.ignoreAll) {
- return;
- }
-
- this._log.trace("Form submission notification for " + actionURI.spec);
-
- // XXX Bug 487541 Copy the logic from nsFormHistory::Notify to avoid
- // divergent logic, which can lead to security issues, until there's a
- // better way to get satchel's results like with a notification.
-
- // Determine if a dom node has the autocomplete attribute set to "off"
- let completeOff = function(domNode) {
- let autocomplete = domNode.getAttribute("autocomplete");
- return autocomplete && autocomplete.search(/^off$/i) == 0;
- }
-
- if (completeOff(formElement)) {
- this._log.trace("Form autocomplete set to off");
- return;
- }
-
- /* Get number of elements in form, add points and changedIDs */
- let len = formElement.length;
- let elements = formElement.elements;
- for (let i = 0; i < len; i++) {
- let el = elements.item(i);
-
- // Grab the name for debugging, but check if empty when satchel would
- let name = el.name;
- if (name === "") {
- name = el.id;
- }
-
- if (!(el instanceof Ci.nsIDOMHTMLInputElement)) {
- this._log.trace(name + " is not a DOMHTMLInputElement: " + el);
- continue;
- }
-
- if (el.type.search(/^text$/i) != 0) {
- this._log.trace(name + "'s type is not 'text': " + el.type);
- continue;
- }
-
- if (completeOff(el)) {
- this._log.trace(name + "'s autocomplete set to off");
- continue;
- }
-
- if (el.value === "") {
- this._log.trace(name + "'s value is empty");
- continue;
- }
-
- if (el.value == el.defaultValue) {
- this._log.trace(name + "'s value is the default");
- continue;
- }
-
- if (name === "") {
- this._log.trace("Text input element has no name or id");
- continue;
- }
-
- // Get the GUID on a delay so that it can be added to the DB first...
- Utils.nextTick(function() {
- this._log.trace("Logging form element: " + [name, el.value]);
- let guid = FormWrapper.getGUID(name, el.value);
- if (guid) {
- this.trackEntry(guid);
- }
- }, this);
- }
- }
};
diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js
index 03bbe1348..99ecb4506 100644
--- a/services/sync/modules/engines/history.js
+++ b/services/sync/modules/engines/history.js
@@ -11,9 +11,10 @@ const Cr = Components.results;
const HISTORY_TTL = 5184000; // 60 days
+Cu.import("resource://gre/modules/PlacesUtils.jsm", this);
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://services-common/async.js");
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/record.js");
@@ -40,7 +41,9 @@ HistoryEngine.prototype = {
_storeObj: HistoryStore,
_trackerObj: HistoryTracker,
downloadLimit: MAX_HISTORY_DOWNLOAD,
- applyIncomingBatchSize: HISTORY_STORE_BATCH_SIZE
+ applyIncomingBatchSize: HISTORY_STORE_BATCH_SIZE,
+
+ syncPriority: 7,
};
function HistoryStore(name, engine) {
@@ -348,28 +351,18 @@ HistoryStore.prototype = {
function HistoryTracker(name, engine) {
Tracker.call(this, name, engine);
- Svc.Obs.add("weave:engine:start-tracking", this);
- Svc.Obs.add("weave:engine:stop-tracking", this);
}
HistoryTracker.prototype = {
__proto__: Tracker.prototype,
- _enabled: false,
- observe: function observe(subject, topic, data) {
- switch (topic) {
- case "weave:engine:start-tracking":
- if (!this._enabled) {
- PlacesUtils.history.addObserver(this, true);
- this._enabled = true;
- }
- break;
- case "weave:engine:stop-tracking":
- if (this._enabled) {
- PlacesUtils.history.removeObserver(this);
- this._enabled = false;
- }
- break;
- }
+ startTracking: function() {
+ this._log.info("Adding Places observer.");
+ PlacesUtils.history.addObserver(this, true);
+ },
+
+ stopTracking: function() {
+ this._log.info("Removing Places observer.");
+ PlacesUtils.history.removeObserver(this);
},
QueryInterface: XPCOMUtils.generateQI([
diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js
index 5952b01f3..994b59767 100644
--- a/services/sync/modules/engines/passwords.js
+++ b/services/sync/modules/engines/passwords.js
@@ -4,10 +4,7 @@
this.EXPORTED_SYMBOLS = ['PasswordEngine', 'LoginRec'];
-const Cu = Components.utils;
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cr = Components.results;
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://services-sync/record.js");
Cu.import("resource://services-sync/constants.js");
@@ -22,8 +19,10 @@ LoginRec.prototype = {
_logName: "Sync.Record.Login",
};
-Utils.deferGetSet(LoginRec, "cleartext", ["hostname", "formSubmitURL",
- "httpRealm", "username", "password", "usernameField", "passwordField"]);
+Utils.deferGetSet(LoginRec, "cleartext", [
+ "hostname", "formSubmitURL",
+ "httpRealm", "username", "password", "usernameField", "passwordField",
+ ]);
this.PasswordEngine = function PasswordEngine(service) {
@@ -34,72 +33,84 @@ PasswordEngine.prototype = {
_storeObj: PasswordStore,
_trackerObj: PasswordTracker,
_recordObj: LoginRec,
+
applyIncomingBatchSize: PASSWORDS_STORE_BATCH_SIZE,
- _syncFinish: function _syncFinish() {
+ syncPriority: 2,
+
+ _syncFinish: function () {
SyncEngine.prototype._syncFinish.call(this);
- // Delete the weave credentials from the server once
- if (!Svc.Prefs.get("deletePwd", false)) {
+ // Delete the Weave credentials from the server once.
+ if (!Svc.Prefs.get("deletePwdFxA", false)) {
try {
- let ids = Services.logins.findLogins({}, PWDMGR_HOST, "", "")
- .map(function(info) {
- return info.QueryInterface(Components.interfaces.nsILoginMetaInfo).guid;
- });
- let coll = new Collection(this.engineURL, null, this.service);
- coll.ids = ids;
- let ret = coll.delete();
- this._log.debug("Delete result: " + ret);
-
- Svc.Prefs.set("deletePwd", true);
- }
- catch(ex) {
+ let ids = [];
+ for (let host of Utils.getSyncCredentialsHosts()) {
+ for (let info of Services.logins.findLogins({}, host, "", "")) {
+ ids.push(info.QueryInterface(Components.interfaces.nsILoginMetaInfo).guid);
+ }
+ }
+ if (ids.length) {
+ let coll = new Collection(this.engineURL, null, this.service);
+ coll.ids = ids;
+ let ret = coll.delete();
+ this._log.debug("Delete result: " + ret);
+ if (!ret.success && ret.status != 400) {
+ // A non-400 failure means try again next time.
+ return;
+ }
+ } else {
+ this._log.debug("Didn't find any passwords to delete");
+ }
+ // If there were no ids to delete, or we succeeded, or got a 400,
+ // record success.
+ Svc.Prefs.set("deletePwdFxA", true);
+ Svc.Prefs.reset("deletePwd"); // The old prefname we previously used.
+ } catch (ex) {
this._log.debug("Password deletes failed: " + Utils.exceptionStr(ex));
}
}
},
- _findDupe: function _findDupe(item) {
+ _findDupe: function (item) {
let login = this._store._nsLoginInfoFromRecord(item);
- if (!login)
+ if (!login) {
return;
+ }
+
+ let logins = Services.logins.findLogins({}, login.hostname, login.formSubmitURL, login.httpRealm);
- let logins = Services.logins.findLogins(
- {}, login.hostname, login.formSubmitURL, login.httpRealm);
this._store._sleep(0); // Yield back to main thread after synchronous operation.
- // Look for existing logins that match the hostname but ignore the password
- for each (let local in logins)
- if (login.matches(local, true) && local instanceof Ci.nsILoginMetaInfo)
+ // Look for existing logins that match the hostname, but ignore the password.
+ for each (let local in logins) {
+ if (login.matches(local, true) && local instanceof Ci.nsILoginMetaInfo) {
return local.guid;
- }
+ }
+ }
+ },
};
function PasswordStore(name, engine) {
Store.call(this, name, engine);
- this._nsLoginInfo = new Components.Constructor(
- "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init");
-
- XPCOMUtils.defineLazyGetter(this, "DBConnection", function() {
- return Services.logins.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.mozIStorageConnection);
- });
+ this._nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init");
}
PasswordStore.prototype = {
__proto__: Store.prototype,
- _nsLoginInfoFromRecord: function PasswordStore__nsLoginInfoRec(record) {
- if (record.formSubmitURL &&
- record.httpRealm) {
- this._log.warn("Record " + record.id +
- " has both formSubmitURL and httpRealm. Skipping.");
+ _nsLoginInfoFromRecord: function (record) {
+ function nullUndefined(x) {
+ return (x == undefined) ? null : x;
+ }
+
+ if (record.formSubmitURL && record.httpRealm) {
+ this._log.warn("Record " + record.id + " has both formSubmitURL and httpRealm. Skipping.");
return null;
}
-
+
// Passing in "undefined" results in an empty string, which later
// counts as a value. Explicitly `|| null` these fields according to JS
// truthiness. Records with empty strings or null will be unmolested.
- function nullUndefined(x) (x == undefined) ? null : x;
let info = new this._nsLoginInfo(record.hostname,
nullUndefined(record.formSubmitURL),
nullUndefined(record.httpRealm),
@@ -112,46 +123,32 @@ PasswordStore.prototype = {
return info;
},
- _getLoginFromGUID: function PasswordStore__getLoginFromGUID(id) {
- let prop = Cc["@mozilla.org/hash-property-bag;1"].
- createInstance(Ci.nsIWritablePropertyBag2);
+ _getLoginFromGUID: function (id) {
+ let prop = Cc["@mozilla.org/hash-property-bag;1"].createInstance(Ci.nsIWritablePropertyBag2);
prop.setPropertyAsAUTF8String("guid", id);
let logins = Services.logins.searchLogins({}, prop);
this._sleep(0); // Yield back to main thread after synchronous operation.
+
if (logins.length > 0) {
this._log.trace(logins.length + " items matching " + id + " found.");
return logins[0];
- } else {
- this._log.trace("No items matching " + id + " found. Ignoring");
}
- return null;
- },
- applyIncomingBatch: function applyIncomingBatch(records) {
- if (!this.DBConnection) {
- return Store.prototype.applyIncomingBatch.call(this, records);
- }
-
- return Utils.runInTransaction(this.DBConnection, function() {
- return Store.prototype.applyIncomingBatch.call(this, records);
- }, this);
- },
-
- applyIncoming: function applyIncoming(record) {
- Store.prototype.applyIncoming.call(this, record);
- this._sleep(0); // Yield back to main thread after synchronous operation.
+ this._log.trace("No items matching " + id + " found. Ignoring");
+ return null;
},
- getAllIDs: function PasswordStore__getAllIDs() {
+ getAllIDs: function () {
let items = {};
let logins = Services.logins.getAllLogins({});
for (let i = 0; i < logins.length; i++) {
- // Skip over Weave password/passphrase entries
+ // Skip over Weave password/passphrase entries.
let metaInfo = logins[i].QueryInterface(Ci.nsILoginMetaInfo);
- if (metaInfo.hostname == PWDMGR_HOST)
+ if (Utils.getSyncCredentialsHosts().has(metaInfo.hostname)) {
continue;
+ }
items[metaInfo.guid] = metaInfo;
}
@@ -159,7 +156,7 @@ PasswordStore.prototype = {
return items;
},
- changeItemID: function PasswordStore__changeItemID(oldID, newID) {
+ changeItemID: function (oldID, newID) {
this._log.trace("Changing item ID: " + oldID + " to " + newID);
let oldLogin = this._getLoginFromGUID(oldID);
@@ -172,41 +169,43 @@ PasswordStore.prototype = {
return;
}
- let prop = Cc["@mozilla.org/hash-property-bag;1"].
- createInstance(Ci.nsIWritablePropertyBag2);
+ let prop = Cc["@mozilla.org/hash-property-bag;1"]
+ .createInstance(Ci.nsIWritablePropertyBag2);
prop.setPropertyAsAUTF8String("guid", newID);
Services.logins.modifyLogin(oldLogin, prop);
},
- itemExists: function PasswordStore__itemExists(id) {
- if (this._getLoginFromGUID(id))
- return true;
- return false;
+ itemExists: function (id) {
+ return !!this._getLoginFromGUID(id);
},
- createRecord: function createRecord(id, collection) {
+ createRecord: function (id, collection) {
let record = new LoginRec(collection, id);
let login = this._getLoginFromGUID(id);
- if (login) {
- record.hostname = login.hostname;
- record.formSubmitURL = login.formSubmitURL;
- record.httpRealm = login.httpRealm;
- record.username = login.username;
- record.password = login.password;
- record.usernameField = login.usernameField;
- record.passwordField = login.passwordField;
- }
- else
+ if (!login) {
record.deleted = true;
+ return record;
+ }
+
+ record.hostname = login.hostname;
+ record.formSubmitURL = login.formSubmitURL;
+ record.httpRealm = login.httpRealm;
+ record.username = login.username;
+ record.password = login.password;
+ record.usernameField = login.usernameField;
+ record.passwordField = login.passwordField;
+
return record;
},
- create: function PasswordStore__create(record) {
+ create: function (record) {
let login = this._nsLoginInfoFromRecord(record);
- if (!login)
+ if (!login) {
return;
+ }
+
this._log.debug("Adding login for " + record.hostname);
this._log.trace("httpRealm: " + JSON.stringify(login.httpRealm) + "; " +
"formSubmitURL: " + JSON.stringify(login.formSubmitURL));
@@ -218,7 +217,7 @@ PasswordStore.prototype = {
}
},
- remove: function PasswordStore__remove(record) {
+ remove: function (record) {
this._log.trace("Removing login " + record.id);
let loginItem = this._getLoginFromGUID(record.id);
@@ -230,7 +229,7 @@ PasswordStore.prototype = {
Services.logins.removeLogin(loginItem);
},
- update: function PasswordStore__update(record) {
+ update: function (record) {
let loginItem = this._getLoginFromGUID(record.id);
if (!loginItem) {
this._log.debug("Skipping update for unknown item: " + record.hostname);
@@ -239,8 +238,10 @@ PasswordStore.prototype = {
this._log.debug("Updating " + record.hostname);
let newinfo = this._nsLoginInfoFromRecord(record);
- if (!newinfo)
+ if (!newinfo) {
return;
+ }
+
try {
Services.logins.modifyLogin(loginItem, newinfo);
} catch(ex) {
@@ -250,9 +251,9 @@ PasswordStore.prototype = {
}
},
- wipe: function PasswordStore_wipe() {
+ wipe: function () {
Services.logins.removeAllLogins();
- }
+ },
};
function PasswordTracker(name, engine) {
@@ -263,49 +264,43 @@ function PasswordTracker(name, engine) {
PasswordTracker.prototype = {
__proto__: Tracker.prototype,
- _enabled: false,
- observe: function PasswordTracker_observe(aSubject, aTopic, aData) {
- switch (aTopic) {
- case "weave:engine:start-tracking":
- if (!this._enabled) {
- Svc.Obs.add("passwordmgr-storage-changed", this);
- this._enabled = true;
- }
- return;
- case "weave:engine:stop-tracking":
- if (this._enabled) {
- Svc.Obs.remove("passwordmgr-storage-changed", this);
- this._enabled = false;
- }
- return;
- }
+ startTracking: function () {
+ Svc.Obs.add("passwordmgr-storage-changed", this);
+ },
+
+ stopTracking: function () {
+ Svc.Obs.remove("passwordmgr-storage-changed", this);
+ },
+
+ observe: function (subject, topic, data) {
+ Tracker.prototype.observe.call(this, subject, topic, data);
- if (this.ignoreAll)
+ if (this.ignoreAll) {
return;
+ }
// A single add, remove or change or removing all items
// will trigger a sync for MULTI_DEVICE.
- switch (aData) {
- case 'modifyLogin':
- aSubject = aSubject.QueryInterface(Ci.nsIArray).
- queryElementAt(1, Ci.nsILoginMetaInfo);
- // fallthrough
- case 'addLogin':
- case 'removeLogin':
- // Skip over Weave password/passphrase changes
- aSubject.QueryInterface(Ci.nsILoginMetaInfo).
- QueryInterface(Ci.nsILoginInfo);
- if (aSubject.hostname == PWDMGR_HOST)
- break;
+ switch (data) {
+ case "modifyLogin":
+ subject = subject.QueryInterface(Ci.nsIArray).queryElementAt(1, Ci.nsILoginMetaInfo);
+ // Fall through.
+ case "addLogin":
+ case "removeLogin":
+ // Skip over Weave password/passphrase changes.
+ subject.QueryInterface(Ci.nsILoginMetaInfo).QueryInterface(Ci.nsILoginInfo);
+ if (Utils.getSyncCredentialsHosts().has(subject.hostname)) {
+ break;
+ }
- this.score += SCORE_INCREMENT_XLARGE;
- this._log.trace(aData + ": " + aSubject.guid);
- this.addChangedID(aSubject.guid);
- break;
- case 'removeAllLogins':
- this._log.trace(aData);
- this.score += SCORE_INCREMENT_XLARGE;
- break;
+ this.score += SCORE_INCREMENT_XLARGE;
+ this._log.trace(data + ": " + subject.guid);
+ this.addChangedID(subject.guid);
+ break;
+ case "removeAllLogins":
+ this._log.trace(data);
+ this.score += SCORE_INCREMENT_XLARGE;
+ break;
}
- }
+ },
};
diff --git a/services/sync/modules/engines/prefs.js b/services/sync/modules/engines/prefs.js
index 2418cd0dc..49d73dbef 100644
--- a/services/sync/modules/engines/prefs.js
+++ b/services/sync/modules/engines/prefs.js
@@ -41,6 +41,8 @@ PrefsEngine.prototype = {
_recordObj: PrefRec,
version: 2,
+ syncPriority: 1,
+
getChangedIDs: function getChangedIDs() {
// No need for a proper timestamp (no conflict resolution needed).
let changedIDs = {};
@@ -67,7 +69,7 @@ PrefsEngine.prototype = {
function PrefStore(name, engine) {
Store.call(this, name, engine);
- Svc.Obs.add("profile-before-change", function() {
+ Svc.Obs.add("profile-before-change", function () {
this.__prefs = null;
}, this);
}
@@ -214,38 +216,36 @@ PrefTracker.prototype = {
__prefs: null,
get _prefs() {
- if (!this.__prefs)
+ if (!this.__prefs) {
this.__prefs = new Preferences();
+ }
return this.__prefs;
},
- _enabled: false,
- observe: function(aSubject, aTopic, aData) {
- switch (aTopic) {
- case "weave:engine:start-tracking":
- if (!this._enabled) {
- Cc["@mozilla.org/preferences-service;1"]
- .getService(Ci.nsIPrefBranch).addObserver("", this, false);
- this._enabled = true;
- }
- break;
- case "weave:engine:stop-tracking":
- if (this._enabled)
- this._enabled = false;
- // Fall through to clean up.
+ startTracking: function () {
+ Services.prefs.addObserver("", this, false);
+ },
+
+ stopTracking: function () {
+ this.__prefs = null;
+ Services.prefs.removeObserver("", this);
+ },
+
+ observe: function (subject, topic, data) {
+ Tracker.prototype.observe.call(this, subject, topic, data);
+
+ switch (topic) {
case "profile-before-change":
- this.__prefs = null;
- Cc["@mozilla.org/preferences-service;1"]
- .getService(Ci.nsIPrefBranch).removeObserver("", this);
+ this.stopTracking();
break;
case "nsPref:changed":
// Trigger a sync for MULTI-DEVICE for a change that determines
// which prefs are synced or a regular pref change.
- if (aData.indexOf(WEAVE_SYNC_PREFS) == 0 ||
- this._prefs.get(WEAVE_SYNC_PREFS + aData, false)) {
+ if (data.indexOf(WEAVE_SYNC_PREFS) == 0 ||
+ this._prefs.get(WEAVE_SYNC_PREFS + data, false)) {
this.score += SCORE_INCREMENT_XLARGE;
this.modified = true;
- this._log.trace("Preference " + aData + " changed");
+ this._log.trace("Preference " + data + " changed");
}
break;
}
diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js
index 24dbf0b2f..1fce737d2 100644
--- a/services/sync/modules/engines/tabs.js
+++ b/services/sync/modules/engines/tabs.js
@@ -2,13 +2,12 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-this.EXPORTED_SYMBOLS = ['TabEngine', 'TabSetRecord'];
+this.EXPORTED_SYMBOLS = ["TabEngine", "TabSetRecord"];
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cu = Components.utils;
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
-const TABS_TTL = 604800; // 7 days
+const TABS_TTL = 604800; // 7 days.
+const TAB_ENTRIES_LIMIT = 25; // How many URLs to include in tab history.
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
@@ -27,7 +26,7 @@ this.TabSetRecord = function TabSetRecord(collection, id) {
TabSetRecord.prototype = {
__proto__: CryptoWrapper.prototype,
_logName: "Sync.Record.Tabs",
- ttl: TABS_TTL
+ ttl: TABS_TTL,
};
Utils.deferGetSet(TabSetRecord, "cleartext", ["clientName", "tabs"]);
@@ -36,7 +35,7 @@ Utils.deferGetSet(TabSetRecord, "cleartext", ["clientName", "tabs"]);
this.TabEngine = function TabEngine(service) {
SyncEngine.call(this, "Tabs", service);
- // Reset the client on every startup so that we fetch recent tabs
+ // Reset the client on every startup so that we fetch recent tabs.
this._resetClient();
}
TabEngine.prototype = {
@@ -45,7 +44,9 @@ TabEngine.prototype = {
_trackerObj: TabTracker,
_recordObj: TabSetRecord,
- getChangedIDs: function getChangedIDs() {
+ syncPriority: 3,
+
+ getChangedIDs: function () {
// No need for a proper timestamp (no conflict resolution needed).
let changedIDs = {};
if (this._tracker.modified)
@@ -53,39 +54,46 @@ TabEngine.prototype = {
return changedIDs;
},
- // API for use by Weave UI code to give user choices of tabs to open:
- getAllClients: function TabEngine_getAllClients() {
+ // API for use by Sync UI code to give user choices of tabs to open.
+ getAllClients: function () {
return this._store._remoteClients;
},
- getClientById: function TabEngine_getClientById(id) {
+ getClientById: function (id) {
return this._store._remoteClients[id];
},
- _resetClient: function TabEngine__resetClient() {
+ _resetClient: function () {
SyncEngine.prototype._resetClient.call(this);
this._store.wipe();
this._tracker.modified = true;
},
- removeClientData: function removeClientData() {
+ removeClientData: function () {
let url = this.engineURL + "/" + this.service.clientsEngine.localID;
this.service.resource(url).delete();
},
- /* The intent is not to show tabs in the menu if they're already
- * open locally. There are a couple ways to interpret this: for
- * instance, we could do it by removing a tab from the list when
- * you open it -- but then if you close it, you can't get back to
- * it. So the way I'm doing it here is to not show a tab in the menu
- * if you have a tab open to the same URL, even though this means
- * that as soon as you navigate anywhere, the original tab will
- * reappear in the menu.
+ /**
+ * Return a Set of open URLs.
*/
- locallyOpenTabMatchesURL: function TabEngine_localTabMatches(url) {
- return this._store.getAllTabs().some(function(tab) {
- return tab.urlHistory[0] == url;
- });
+ getOpenURLs: function () {
+ let urls = new Set();
+ for (let entry of this._store.getAllTabs()) {
+ urls.add(entry.urlHistory[0]);
+ }
+ return urls;
+ },
+
+ _reconcile: function (item) {
+ // Skip our own record.
+ // TabStore.itemExists tests only against our local client ID.
+ if (this._store.itemExists(item.id)) {
+ this._log.trace("Ignoring incoming tab item because of its id: " + item.id);
+ return false;
+ }
+
+ return SyncEngine.prototype._reconcile.call(this, item);
}
};
@@ -96,69 +104,90 @@ function TabStore(name, engine) {
TabStore.prototype = {
__proto__: Store.prototype,
- itemExists: function TabStore_itemExists(id) {
+ itemExists: function (id) {
return id == this.engine.service.clientsEngine.localID;
},
- /**
- * Return the recorded last used time of the provided tab, or
- * 0 if none is present.
- * The result will always be an integer value.
- */
- tabLastUsed: function tabLastUsed(tab) {
- // weaveLastUsed will only be set if the tab was ever selected (or
- // opened after Sync was running).
- let weaveLastUsed = tab.extData && tab.extData.weaveLastUsed;
- if (!weaveLastUsed) {
- return 0;
- }
- return parseInt(weaveLastUsed, 10) || 0;
+ getWindowEnumerator: function () {
+ return Services.wm.getEnumerator("navigator:browser");
+ },
+
+ shouldSkipWindow: function (win) {
+ return win.closed ||
+ PrivateBrowsingUtils.isWindowPrivate(win);
+ },
+
+ getTabState: function (tab) {
+ return JSON.parse(Svc.Session.getTabState(tab));
},
- getAllTabs: function getAllTabs(filter) {
+ getAllTabs: function (filter) {
let filteredUrls = new RegExp(Svc.Prefs.get("engine.tabs.filteredUrls"), "i");
let allTabs = [];
- let currentState = JSON.parse(Svc.Session.getBrowserState());
- let tabLastUsed = this.tabLastUsed;
- currentState.windows.forEach(function(window) {
- if (window.isPrivate) {
- return;
+ let winEnum = this.getWindowEnumerator();
+ while (winEnum.hasMoreElements()) {
+ let win = winEnum.getNext();
+ if (this.shouldSkipWindow(win)) {
+ continue;
}
- window.tabs.forEach(function(tab) {
+
+ for (let tab of win.gBrowser.tabs) {
+ tabState = this.getTabState(tab);
+
// Make sure there are history entries to look at.
- if (!tab.entries.length)
- return;
- // Until we store full or partial history, just grab the current entry.
- // index is 1 based, so make sure we adjust.
- let entry = tab.entries[tab.index - 1];
-
- // Filter out some urls if necessary. SessionStore can return empty
- // tabs in some cases - easiest thing is to just ignore them for now.
- if (!entry.url || filter && filteredUrls.test(entry.url))
- return;
-
- // I think it's also possible that attributes[.image] might not be set
- // so handle that as well.
+ if (!tabState || !tabState.entries.length) {
+ continue;
+ }
+
+ let acceptable = !filter ? (url) => url :
+ (url) => url && !filteredUrls.test(url);
+
+ let entries = tabState.entries;
+ let index = tabState.index;
+ let current = entries[index - 1];
+
+ // We ignore the tab completely if the current entry url is
+ // not acceptable (we need something accurate to open).
+ if (!acceptable(current.url)) {
+ continue;
+ }
+
+ // The element at `index` is the current page. Previous URLs were
+ // previously visited URLs; subsequent URLs are in the 'forward' stack,
+ // which we can't represent in Sync, so we truncate here.
+ let candidates = (entries.length == index) ?
+ entries :
+ entries.slice(0, index);
+
+ let urls = candidates.map((entry) => entry.url)
+ .filter(acceptable)
+ .reverse(); // Because Sync puts current at index 0, and history after.
+
+ // Truncate if necessary.
+ if (urls.length > TAB_ENTRIES_LIMIT) {
+ urls.length = TAB_ENTRIES_LIMIT;
+ }
+
allTabs.push({
- title: entry.title || "",
- urlHistory: [entry.url],
- icon: tab.attributes && tab.attributes.image || "",
- lastUsed: tabLastUsed(tab)
+ title: current.title || "",
+ urlHistory: urls,
+ icon: tabState.attributes && tabState.attributes.image || "",
+ lastUsed: Math.floor((tabState.lastAccessed || 0) / 1000),
});
- });
- });
+ }
+ }
return allTabs;
},
- createRecord: function createRecord(id, collection) {
+ createRecord: function (id, collection) {
let record = new TabSetRecord(collection, id);
record.clientName = this.engine.service.clientsEngine.localName;
// Sort tabs in descending-used order to grab the most recently used
- let tabs = this.getAllTabs(true).sort(function(a, b) {
+ let tabs = this.getAllTabs(true).sort(function (a, b) {
return b.lastUsed - a.lastUsed;
});
@@ -178,7 +207,7 @@ TabStore.prototype = {
}
this._log.trace("Created tabs " + tabs.length + " of " + origLength);
- tabs.forEach(function(tab) {
+ tabs.forEach(function (tab) {
this._log.trace("Wrapping tab: " + JSON.stringify(tab));
}, this);
@@ -186,7 +215,7 @@ TabStore.prototype = {
return record;
},
- getAllIDs: function TabStore_getAllIds() {
+ getAllIDs: function () {
// Don't report any tabs if all windows are in private browsing for
// first syncs.
let ids = {};
@@ -212,31 +241,38 @@ TabStore.prototype = {
return ids;
},
- wipe: function TabStore_wipe() {
+ wipe: function () {
this._remoteClients = {};
},
- create: function TabStore_create(record) {
+ create: function (record) {
this._log.debug("Adding remote tabs from " + record.clientName);
this._remoteClients[record.id] = record.cleartext;
- // Lose some precision, but that's good enough (seconds)
+ // Lose some precision, but that's good enough (seconds).
let roundModify = Math.floor(record.modified / 1000);
let notifyState = Svc.Prefs.get("notifyTabState");
- // If there's no existing pref, save this first modified time
- if (notifyState == null)
+
+ // If there's no existing pref, save this first modified time.
+ if (notifyState == null) {
Svc.Prefs.set("notifyTabState", roundModify);
- // Don't change notifyState if it's already 0 (don't notify)
- else if (notifyState == 0)
return;
- // We must have gotten a new tab that isn't the same as last time
- else if (notifyState != roundModify)
+ }
+
+ // Don't change notifyState if it's already 0 (don't notify).
+ if (notifyState == 0) {
+ return;
+ }
+
+ // We must have gotten a new tab that isn't the same as last time.
+ if (notifyState != roundModify) {
Svc.Prefs.set("notifyTabState", 0);
+ }
},
- update: function update(record) {
+ update: function (record) {
this._log.trace("Ignoring tab updates as local ones win");
- }
+ },
};
@@ -245,7 +281,7 @@ function TabTracker(name, engine) {
Svc.Obs.add("weave:engine:start-tracking", this);
Svc.Obs.add("weave:engine:stop-tracking", this);
- // Make sure "this" pointer is always set correctly for event listeners
+ // Make sure "this" pointer is always set correctly for event listeners.
this.onTab = Utils.bind2(this, this.onTab);
this._unregisterListeners = Utils.bind2(this, this._unregisterListeners);
}
@@ -254,16 +290,17 @@ TabTracker.prototype = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
- loadChangedIDs: function loadChangedIDs() {
+ loadChangedIDs: function () {
// Don't read changed IDs from disk at start up.
},
- clearChangedIDs: function clearChangedIDs() {
+ clearChangedIDs: function () {
this.modified = false;
},
_topics: ["pageshow", "TabOpen", "TabClose", "TabSelect"],
- _registerListenersForWindow: function registerListenersFW(window) {
+
+ _registerListenersForWindow: function (window) {
this._log.trace("Registering tab listeners in window");
for each (let topic in this._topics) {
window.addEventListener(topic, this.onTab, false);
@@ -271,11 +308,11 @@ TabTracker.prototype = {
window.addEventListener("unload", this._unregisterListeners, false);
},
- _unregisterListeners: function unregisterListeners(event) {
+ _unregisterListeners: function (event) {
this._unregisterListenersForWindow(event.target);
},
- _unregisterListenersForWindow: function unregisterListenersFW(window) {
+ _unregisterListenersForWindow: function (window) {
this._log.trace("Removing tab listeners in window");
window.removeEventListener("unload", this._unregisterListeners, false);
for each (let topic in this._topics) {
@@ -283,43 +320,43 @@ TabTracker.prototype = {
}
},
- _enabled: false,
- observe: function TabTracker_observe(aSubject, aTopic, aData) {
- switch (aTopic) {
- case "weave:engine:start-tracking":
- if (!this._enabled) {
- Svc.Obs.add("domwindowopened", this);
- let wins = Services.wm.getEnumerator("navigator:browser");
- while (wins.hasMoreElements())
- this._registerListenersForWindow(wins.getNext());
- this._enabled = true;
- }
- break;
- case "weave:engine:stop-tracking":
- if (this._enabled) {
- Svc.Obs.remove("domwindowopened", this);
- let wins = Services.wm.getEnumerator("navigator:browser");
- while (wins.hasMoreElements())
- this._unregisterListenersForWindow(wins.getNext());
- this._enabled = false;
- }
- return;
+ startTracking: function () {
+ Svc.Obs.add("domwindowopened", this);
+ let wins = Services.wm.getEnumerator("navigator:browser");
+ while (wins.hasMoreElements()) {
+ this._registerListenersForWindow(wins.getNext());
+ }
+ },
+
+ stopTracking: function () {
+ Svc.Obs.remove("domwindowopened", this);
+ let wins = Services.wm.getEnumerator("navigator:browser");
+ while (wins.hasMoreElements()) {
+ this._unregisterListenersForWindow(wins.getNext());
+ }
+ },
+
+ observe: function (subject, topic, data) {
+ Tracker.prototype.observe.call(this, subject, topic, data);
+
+ switch (topic) {
case "domwindowopened":
- // Add tab listeners now that a window has opened
- let self = this;
- aSubject.addEventListener("load", function onLoad(event) {
- aSubject.removeEventListener("load", onLoad, false);
- // Only register after the window is done loading to avoid unloads
- self._registerListenersForWindow(aSubject);
- }, false);
+ let onLoad = () => {
+ subject.removeEventListener("load", onLoad, false);
+ // Only register after the window is done loading to avoid unloads.
+ this._registerListenersForWindow(subject);
+ };
+
+ // Add tab listeners now that a window has opened.
+ subject.addEventListener("load", onLoad, false);
break;
}
},
- onTab: function onTab(event) {
+ onTab: function (event) {
if (event.originalTarget.linkedBrowser) {
- let win = event.originalTarget.linkedBrowser.contentWindow;
- if (PrivateBrowsingUtils.isWindowPrivate(win) &&
+ let browser = event.originalTarget.linkedBrowser;
+ if (PrivateBrowsingUtils.isBrowserPrivate(browser) &&
!PrivateBrowsingUtils.permanentPrivateBrowsing) {
this._log.trace("Ignoring tab event from private browsing.");
return;
@@ -329,20 +366,11 @@ TabTracker.prototype = {
this._log.trace("onTab event: " + event.type);
this.modified = true;
- // For pageshow events, only give a partial score bump (~.1)
- let chance = .1;
-
- // For regular Tab events, do a full score bump and remember when it changed
- if (event.type != "pageshow") {
- chance = 1;
-
- // Store a timestamp in the tab to track when it was last used
- Svc.Session.setTabValue(event.originalTarget, "weaveLastUsed",
- Math.floor(Date.now() / 1000));
- }
-
- // Only increase the score by whole numbers, so use random for partial score
- if (Math.random() < chance)
+ // For page shows, bump the score 10% of the time, emulating a partial
+ // score. We don't want to sync too frequently. For all other page
+ // events, always bump the score.
+ if (event.type != "pageshow" || Math.random() < .1) {
this.score += SCORE_INCREMENT_SMALL;
+ }
},
-}
+};
diff --git a/services/sync/modules/healthreport.jsm b/services/sync/modules/healthreport.jsm
new file mode 100644
index 000000000..47161c095
--- /dev/null
+++ b/services/sync/modules/healthreport.jsm
@@ -0,0 +1,262 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "SyncProvider",
+];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Metrics.jsm", this);
+Cu.import("resource://gre/modules/Promise.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+
+const DAILY_LAST_NUMERIC_FIELD = {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC};
+const DAILY_LAST_TEXT_FIELD = {type: Metrics.Storage.FIELD_DAILY_LAST_TEXT};
+const DAILY_COUNTER_FIELD = {type: Metrics.Storage.FIELD_DAILY_COUNTER};
+
+XPCOMUtils.defineLazyModuleGetter(this, "Weave",
+ "resource://services-sync/main.js");
+
+function SyncMeasurement1() {
+ Metrics.Measurement.call(this);
+}
+
+SyncMeasurement1.prototype = Object.freeze({
+ __proto__: Metrics.Measurement.prototype,
+
+ name: "sync",
+ version: 1,
+
+ fields: {
+ enabled: DAILY_LAST_NUMERIC_FIELD,
+ preferredProtocol: DAILY_LAST_TEXT_FIELD,
+ activeProtocol: DAILY_LAST_TEXT_FIELD,
+ syncStart: DAILY_COUNTER_FIELD,
+ syncSuccess: DAILY_COUNTER_FIELD,
+ syncError: DAILY_COUNTER_FIELD,
+ },
+});
+
+function SyncDevicesMeasurement1() {
+ Metrics.Measurement.call(this);
+}
+
+SyncDevicesMeasurement1.prototype = Object.freeze({
+ __proto__: Metrics.Measurement.prototype,
+
+ name: "devices",
+ version: 1,
+
+ fields: {},
+
+ shouldIncludeField: function (name) {
+ return true;
+ },
+
+ fieldType: function (name) {
+ return Metrics.Storage.FIELD_DAILY_COUNTER;
+ },
+});
+
+function SyncMigrationMeasurement1() {
+ Metrics.Measurement.call(this);
+}
+
+SyncMigrationMeasurement1.prototype = Object.freeze({
+ __proto__: Metrics.Measurement.prototype,
+
+ name: "migration",
+ version: 1,
+
+ fields: {
+ state: DAILY_LAST_TEXT_FIELD, // last "user" or "internal" state we saw for the day
+ accepted: DAILY_COUNTER_FIELD, // number of times user tried to start migration
+ declined: DAILY_COUNTER_FIELD, // number of times user closed nagging infobar
+ unlinked: DAILY_LAST_NUMERIC_FIELD, // did the user decline and unlink
+ },
+});
+
+this.SyncProvider = function () {
+ Metrics.Provider.call(this);
+};
+SyncProvider.prototype = Object.freeze({
+ __proto__: Metrics.Provider.prototype,
+
+ name: "org.mozilla.sync",
+
+ measurementTypes: [
+ SyncDevicesMeasurement1,
+ SyncMeasurement1,
+ SyncMigrationMeasurement1,
+ ],
+
+ _OBSERVERS: [
+ "weave:service:sync:start",
+ "weave:service:sync:finish",
+ "weave:service:sync:error",
+ "fxa-migration:state-changed",
+ "fxa-migration:internal-state-changed",
+ "fxa-migration:internal-telemetry",
+ ],
+
+ postInit: function () {
+ for (let o of this._OBSERVERS) {
+ Services.obs.addObserver(this, o, false);
+ }
+
+ return Promise.resolve();
+ },
+
+ onShutdown: function () {
+ for (let o of this._OBSERVERS) {
+ Services.obs.removeObserver(this, o);
+ }
+
+ return Promise.resolve();
+ },
+
+ observe: function (subject, topic, data) {
+ switch (topic) {
+ case "weave:service:sync:start":
+ case "weave:service:sync:finish":
+ case "weave:service:sync:error":
+ return this._observeSync(subject, topic, data);
+
+ case "fxa-migration:state-changed":
+ case "fxa-migration:internal-state-changed":
+ case "fxa-migration:internal-telemetry":
+ return this._observeMigration(subject, topic, data);
+ }
+ Cu.reportError("unexpected topic in sync healthreport provider: " + topic);
+ },
+
+ _observeSync: function (subject, topic, data) {
+ let field;
+ switch (topic) {
+ case "weave:service:sync:start":
+ field = "syncStart";
+ break;
+
+ case "weave:service:sync:finish":
+ field = "syncSuccess";
+ break;
+
+ case "weave:service:sync:error":
+ field = "syncError";
+ break;
+
+ default:
+ Cu.reportError("unexpected sync topic in sync healthreport provider: " + topic);
+ return;
+ }
+
+ let m = this.getMeasurement(SyncMeasurement1.prototype.name,
+ SyncMeasurement1.prototype.version);
+ return this.enqueueStorageOperation(function recordSyncEvent() {
+ return m.incrementDailyCounter(field);
+ });
+ },
+
+ _observeMigration: function(subject, topic, data) {
+ switch (topic) {
+ case "fxa-migration:state-changed":
+ case "fxa-migration:internal-state-changed": {
+ // We record both "user" and "internal" states in the same field. This
+ // works for us as user state is always null when there is an internal
+ // state.
+ if (!data) {
+ return; // we don't count the |null| state
+ }
+ let m = this.getMeasurement(SyncMigrationMeasurement1.prototype.name,
+ SyncMigrationMeasurement1.prototype.version);
+ return this.enqueueStorageOperation(function() {
+ return m.setDailyLastText("state", data);
+ });
+ }
+
+ case "fxa-migration:internal-telemetry": {
+ // |data| is our field name.
+ let m = this.getMeasurement(SyncMigrationMeasurement1.prototype.name,
+ SyncMigrationMeasurement1.prototype.version);
+ return this.enqueueStorageOperation(function() {
+ switch (data) {
+ case "accepted":
+ case "declined":
+ return m.incrementDailyCounter(data);
+ case "unlinked":
+ return m.setDailyLastNumeric(data, 1);
+ default:
+ Cu.reportError("Unexpected migration field in sync healthreport provider: " + data);
+ return Promise.resolve();
+ }
+ });
+ }
+
+ default:
+ Cu.reportError("unexpected migration topic in sync healthreport provider: " + topic);
+ return;
+ }
+ },
+
+ collectDailyData: function () {
+ return this.storage.enqueueTransaction(this._populateDailyData.bind(this));
+ },
+
+ _populateDailyData: function* () {
+ let m = this.getMeasurement(SyncMeasurement1.prototype.name,
+ SyncMeasurement1.prototype.version);
+
+ let svc = Cc["@mozilla.org/weave/service;1"]
+ .getService(Ci.nsISupports)
+ .wrappedJSObject;
+
+ let enabled = svc.enabled;
+ yield m.setDailyLastNumeric("enabled", enabled ? 1 : 0);
+
+ // preferredProtocol is constant and only changes as the client
+ // evolves.
+ yield m.setDailyLastText("preferredProtocol", "1.5");
+
+ let protocol = svc.fxAccountsEnabled ? "1.5" : "1.1";
+ yield m.setDailyLastText("activeProtocol", protocol);
+
+ if (!enabled) {
+ return;
+ }
+
+ // Before grabbing more information, be sure the Sync service
+ // is fully initialized. This has the potential to initialize
+ // Sync on the spot. This may be undesired if Sync appears to
+ // be enabled but it really isn't. That responsibility should
+ // be up to svc.enabled to not return false positives, however.
+ yield svc.whenLoaded();
+
+ if (Weave.Status.service != Weave.STATUS_OK) {
+ return;
+ }
+
+ // Device types are dynamic. So we need to dynamically create fields if
+ // they don't exist.
+ let dm = this.getMeasurement(SyncDevicesMeasurement1.prototype.name,
+ SyncDevicesMeasurement1.prototype.version);
+ let devices = Weave.Service.clientsEngine.deviceTypes;
+ for (let [field, count] of devices) {
+ let hasField = this.storage.hasFieldFromMeasurement(dm.id, field,
+ this.storage.FIELD_DAILY_LAST_NUMERIC);
+ let fieldID;
+ if (hasField) {
+ fieldID = this.storage.fieldIDFromMeasurement(dm.id, field);
+ } else {
+ fieldID = yield this.storage.registerField(dm.id, field,
+ this.storage.FIELD_DAILY_LAST_NUMERIC);
+ }
+
+ yield this.storage.setDailyLastNumericFromFieldID(fieldID, count);
+ }
+ },
+});
diff --git a/services/sync/modules/identity.js b/services/sync/modules/identity.js
index e3ecd7635..2bee13b5b 100644
--- a/services/sync/modules/identity.js
+++ b/services/sync/modules/identity.js
@@ -9,8 +9,9 @@ this.EXPORTED_SYMBOLS = ["IdentityManager"];
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/util.js");
// Lazy import to prevent unnecessary load on startup.
@@ -21,7 +22,8 @@ for (let symbol of ["BulkKeyBundle", "SyncKeyBundle"]) {
}
/**
- * Manages identity and authentication for Sync.
+ * Manages "legacy" identity and authentication for Sync.
+ * See browserid_identity for the Firefox Accounts based identity manager.
*
* The following entities are managed:
*
@@ -57,8 +59,8 @@ for (let symbol of ["BulkKeyBundle", "SyncKeyBundle"]) {
* and any other function that involves the built-in functionality.
*/
this.IdentityManager = function IdentityManager() {
- this._log = Log4Moz.repository.getLogger("Sync.Identity");
- this._log.Level = Log4Moz.Level[Svc.Prefs.get("log.logger.identity")];
+ this._log = Log.repository.getLogger("Sync.Identity");
+ this._log.Level = Log.Level[Svc.Prefs.get("log.logger.identity")];
this._basicPassword = null;
this._basicPasswordAllowLookup = true;
@@ -81,6 +83,45 @@ IdentityManager.prototype = {
_syncKeyBundle: null,
+ /**
+ * Initialize the identity provider. Returns a promise that is resolved
+ * when initialization is complete and the provider can be queried for
+ * its state
+ */
+ initialize: function() {
+ // Nothing to do for this identity provider.
+ return Promise.resolve();
+ },
+
+ finalize: function() {
+ // Nothing to do for this identity provider.
+ return Promise.resolve();
+ },
+
+ /**
+ * Called whenever Service.logout() is called.
+ */
+ logout: function() {
+ // nothing to do for this identity provider.
+ },
+
+ /**
+ * Ensure the user is logged in. Returns a promise that resolves when
+ * the user is logged in, or is rejected if the login attempt has failed.
+ */
+ ensureLoggedIn: function() {
+ // nothing to do for this identity provider
+ return Promise.resolve();
+ },
+
+ /**
+ * Indicates if the identity manager is still initializing
+ */
+ get readyToAuthenticate() {
+ // We initialize in a fully sync manner, so we are always finished.
+ return true;
+ },
+
get account() {
return Svc.Prefs.get("account", this.username);
},
@@ -133,7 +174,21 @@ IdentityManager.prototype = {
// If we change the username, we interpret this as a major change event
// and wipe out the credentials.
this._log.info("Username changed. Removing stored credentials.");
+ this.resetCredentials();
+ },
+
+ /**
+ * Resets/Drops all credentials we hold for the current user.
+ */
+ resetCredentials: function() {
this.basicPassword = null;
+ this.resetSyncKey();
+ },
+
+ /**
+ * Resets/Drops the sync key we hold for the current user.
+ */
+ resetSyncKey: function() {
this.syncKey = null;
// syncKeyBundle cleared as a result of setting syncKey.
},
@@ -323,6 +378,25 @@ IdentityManager.prototype = {
},
/**
+ * Verify the current auth state, unlocking the master-password if necessary.
+ *
+ * Returns a promise that resolves with the current auth state after
+ * attempting to unlock.
+ */
+ unlockAndVerifyAuthState: function() {
+ // Try to fetch the passphrase - this will prompt for MP unlock as a
+ // side-effect...
+ try {
+ this.syncKey;
+ } catch (ex) {
+ this._log.debug("Fetching passphrase threw " + ex +
+ "; assuming master password locked.");
+ return Promise.resolve(MASTER_PASSWORD_LOCKED);
+ }
+ return Promise.resolve(STATUS_OK);
+ },
+
+ /**
* Persist credentials to password store.
*
* When credentials are updated, they are changed in memory only. This will
@@ -373,6 +447,22 @@ IdentityManager.prototype = {
},
/**
+ * Pre-fetches any information that might help with migration away from this
+ * identity. Called after every sync and is really just an optimization that
+ * allows us to avoid a network request for when we actually need the
+ * migration info.
+ */
+ prefetchMigrationSentinel: function(service) {
+ // Try and fetch the migration sentinel - it will end up in the recordManager
+ // cache.
+ try {
+ service.recordManager.get(service.storageURL + "meta/fxa_credentials");
+ } catch (ex) {
+ this._log.warn("Failed to pre-fetch the migration sentinel", ex);
+ }
+ },
+
+ /**
* Obtains the array of basic logins from nsiPasswordManager.
*/
_getLogins: function _getLogins(realm) {
@@ -411,12 +501,21 @@ IdentityManager.prototype = {
},
/**
+ * Return credentials hosts for this identity only.
+ */
+ _getSyncCredentialsHosts: function() {
+ return Utils.getSyncCredentialsHostsLegacy();
+ },
+
+ /**
* Deletes Sync credentials from the password manager.
*/
deleteSyncCredentials: function deleteSyncCredentials() {
- let logins = Services.logins.findLogins({}, PWDMGR_HOST, "", "");
- for each (let login in logins) {
- Services.logins.removeLogin(login);
+ for (let host of this._getSyncCredentialsHosts()) {
+ let logins = Services.logins.findLogins({}, host, "", "");
+ for each (let login in logins) {
+ Services.logins.removeLogin(login);
+ }
}
// Wait until after store is updated in case it fails.
@@ -491,5 +590,15 @@ IdentityManager.prototype = {
onRESTRequestBasic: function onRESTRequestBasic(request) {
let up = this.username + ":" + this.basicPassword;
request.setHeader("authorization", "Basic " + btoa(up));
- }
+ },
+
+ createClusterManager: function(service) {
+ Cu.import("resource://services-sync/stages/cluster.js");
+ return new ClusterManager(service);
+ },
+
+ offerSyncOptions: function () {
+ // Do nothing for Sync 1.1.
+ return {accepted: true};
+ },
};
diff --git a/services/sync/modules/jpakeclient.js b/services/sync/modules/jpakeclient.js
index a8e343543..10f405371 100644
--- a/services/sync/modules/jpakeclient.js
+++ b/services/sync/modules/jpakeclient.js
@@ -6,7 +6,7 @@ this.EXPORTED_SYMBOLS = ["JPAKEClient", "SendCredentialsController"];
const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-common/rest.js");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/util.js");
@@ -114,8 +114,8 @@ const JPAKE_VERIFY_VALUE = "0123456789ABCDEF";
this.JPAKEClient = function JPAKEClient(controller) {
this.controller = controller;
- this._log = Log4Moz.repository.getLogger("Sync.JPAKEClient");
- this._log.level = Log4Moz.Level[Svc.Prefs.get(
+ this._log = Log.repository.getLogger("Sync.JPAKEClient");
+ this._log.level = Log.Level[Svc.Prefs.get(
"log.logger.service.jpakeclient", "Debug")];
this._serverURL = Svc.Prefs.get("jpake.serverURL");
@@ -700,8 +700,8 @@ JPAKEClient.prototype = {
*/
this.SendCredentialsController =
function SendCredentialsController(jpakeclient, service) {
- this._log = Log4Moz.repository.getLogger("Sync.SendCredentialsController");
- this._log.level = Log4Moz.Level[Svc.Prefs.get("log.logger.service.main")];
+ this._log = Log.repository.getLogger("Sync.SendCredentialsController");
+ this._log.level = Log.Level[Svc.Prefs.get("log.logger.service.main")];
this._log.trace("Loading.");
this.jpakeclient = jpakeclient;
diff --git a/services/sync/modules/keys.js b/services/sync/modules/keys.js
index e228db31f..bf909bdc2 100644
--- a/services/sync/modules/keys.js
+++ b/services/sync/modules/keys.js
@@ -12,7 +12,7 @@ this.EXPORTED_SYMBOLS = [
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/util.js");
/**
@@ -120,7 +120,7 @@ KeyBundle.prototype = {
* This is just a KeyBundle with a collection attached.
*/
this.BulkKeyBundle = function BulkKeyBundle(collection) {
- let log = Log4Moz.repository.getLogger("Sync.BulkKeyBundle");
+ let log = Log.repository.getLogger("Sync.BulkKeyBundle");
log.info("BulkKeyBundle being created for " + collection);
KeyBundle.call(this);
@@ -177,7 +177,7 @@ BulkKeyBundle.prototype = {
* If the username or Sync Key is invalid, an Error will be thrown.
*/
this.SyncKeyBundle = function SyncKeyBundle(username, syncKey) {
- let log = Log4Moz.repository.getLogger("Sync.SyncKeyBundle");
+ let log = Log.repository.getLogger("Sync.SyncKeyBundle");
log.info("SyncKeyBundle being created.");
KeyBundle.call(this);
diff --git a/services/sync/modules/main.js b/services/sync/modules/main.js
index df3868e20..488a2594b 100644
--- a/services/sync/modules/main.js
+++ b/services/sync/modules/main.js
@@ -21,7 +21,7 @@ function lazyImport(module, dest, props) {
delete dest[prop];
return dest[prop] = ns[prop];
};
- props.forEach(function(prop) dest.__defineGetter__(prop, getter(prop)));
+ props.forEach(function (prop) { dest.__defineGetter__(prop, getter(prop)); });
}
for (let mod in lazies) {
diff --git a/services/sync/modules/notifications.js b/services/sync/modules/notifications.js
index 18955aa45..1ee24f2cd 100644
--- a/services/sync/modules/notifications.js
+++ b/services/sync/modules/notifications.js
@@ -10,14 +10,14 @@ const Cr = Components.results;
const Cu = Components.utils;
Cu.import("resource://services-common/observers.js");
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/util.js");
this.Notifications = {
// Match the referenced values in toolkit/content/widgets/notification.xml.
- get PRIORITY_INFO() 1, // PRIORITY_INFO_LOW
- get PRIORITY_WARNING() 4, // PRIORITY_WARNING_LOW
- get PRIORITY_ERROR() 7, // PRIORITY_CRITICAL_LOW
+ get PRIORITY_INFO() { return 1; }, // PRIORITY_INFO_LOW
+ get PRIORITY_WARNING() { return 4; }, // PRIORITY_WARNING_LOW
+ get PRIORITY_ERROR() { return 7; }, // PRIORITY_CRITICAL_LOW
// FIXME: instead of making this public, dress the Notifications object
// to behave like an iterator (using generators?) and have callers access
@@ -68,8 +68,8 @@ this.Notifications = {
* Title of notifications to remove; falsy value means remove all
*/
removeAll: function Notifications_removeAll(title) {
- this.notifications.filter(function(old) old.title == title || !title).
- forEach(function(old) this.remove(old), this);
+ this.notifications.filter(old => (old.title == title || !title)).
+ forEach(old => { this.remove(old); }, this);
},
// replaces all existing notifications with the same title as the new one
@@ -84,7 +84,7 @@ this.Notifications = {
* A basic notification. Subclass this to create more complex notifications.
*/
this.Notification =
- function Notification(title, description, iconURL, priority, buttons) {
+function Notification(title, description, iconURL, priority, buttons, link) {
this.title = title;
this.description = description;
@@ -96,6 +96,9 @@ this.Notification =
if (buttons)
this.buttons = buttons;
+
+ if (link)
+ this.link = link;
}
// We set each prototype property individually instead of redefining
@@ -115,7 +118,7 @@ this.NotificationButton =
try {
callback.apply(this, arguments);
} catch (e) {
- let logger = Log4Moz.repository.getLogger("Sync.Notifications");
+ let logger = Log.repository.getLogger("Sync.Notifications");
logger.error("An exception occurred: " + Utils.exceptionStr(e));
logger.info(Utils.stackTrace(e));
throw e;
diff --git a/services/sync/modules/policies.js b/services/sync/modules/policies.js
index 8cd8ab46b..d799cb235 100644
--- a/services/sync/modules/policies.js
+++ b/services/sync/modules/policies.js
@@ -9,18 +9,21 @@ this.EXPORTED_SYMBOLS = [
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/engines.js");
-Cu.import("resource://services-sync/status.js");
Cu.import("resource://services-sync/util.js");
+Cu.import("resource://services-common/logmanager.js");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Status",
+ "resource://services-sync/status.js");
this.SyncScheduler = function SyncScheduler(service) {
this.service = service;
this.init();
}
SyncScheduler.prototype = {
- _log: Log4Moz.repository.getLogger("Sync.SyncScheduler"),
+ _log: Log.repository.getLogger("Sync.SyncScheduler"),
_fatalLoginStatus: [LOGIN_FAILED_NO_USERNAME,
LOGIN_FAILED_NO_PASSWORD,
@@ -36,10 +39,18 @@ SyncScheduler.prototype = {
setDefaults: function setDefaults() {
this._log.trace("Setting SyncScheduler policy values to defaults.");
- this.singleDeviceInterval = Svc.Prefs.get("scheduler.singleDeviceInterval") * 1000;
+ let service = Cc["@mozilla.org/weave/service;1"]
+ .getService(Ci.nsISupports)
+ .wrappedJSObject;
+
+ let part = service.fxAccountsEnabled ? "fxa" : "sync11";
+ let prefSDInterval = "scheduler." + part + ".singleDeviceInterval";
+ this.singleDeviceInterval = Svc.Prefs.get(prefSDInterval) * 1000;
+
this.idleInterval = Svc.Prefs.get("scheduler.idleInterval") * 1000;
this.activeInterval = Svc.Prefs.get("scheduler.activeInterval") * 1000;
this.immediateInterval = Svc.Prefs.get("scheduler.immediateInterval") * 1000;
+ this.eolInterval = Svc.Prefs.get("scheduler.eolInterval") * 1000;
// A user is non-idle on startup by default.
this.idle = false;
@@ -66,7 +77,7 @@ SyncScheduler.prototype = {
set numClients(value) Svc.Prefs.set("numClients", value),
init: function init() {
- this._log.level = Log4Moz.Level[Svc.Prefs.get("log.logger.service.main")];
+ this._log.level = Log.Level[Svc.Prefs.get("log.logger.service.main")];
this.setDefaults();
Svc.Obs.add("weave:engine:score:updated", this);
Svc.Obs.add("network:offline-status-changed", this);
@@ -82,8 +93,10 @@ SyncScheduler.prototype = {
Svc.Obs.add("weave:engine:sync:applied", this);
Svc.Obs.add("weave:service:setup-complete", this);
Svc.Obs.add("weave:service:start-over", this);
+ Svc.Obs.add("FxA:hawk:backoff:interval", this);
if (Status.checkSetup() == STATUS_OK) {
+ Svc.Obs.add("wake_notification", this);
Svc.Idle.addIdleObserver(this, Svc.Prefs.get("scheduler.idleTime"));
}
},
@@ -171,6 +184,7 @@ SyncScheduler.prototype = {
this.nextSync = 0;
this.handleSyncError();
break;
+ case "FxA:hawk:backoff:interval":
case "weave:service:backoff:interval":
let requested_interval = subject * 1000;
this._log.debug("Got backoff notification: " + requested_interval + "ms");
@@ -199,6 +213,7 @@ SyncScheduler.prototype = {
case "weave:service:setup-complete":
Services.prefs.savePrefFile(null);
Svc.Idle.addIdleObserver(this, Svc.Prefs.get("scheduler.idleTime"));
+ Svc.Obs.add("wake_notification", this);
break;
case "weave:service:start-over":
this.setDefaults();
@@ -217,7 +232,7 @@ SyncScheduler.prototype = {
// were just active.)
this.adjustSyncInterval();
break;
- case "back":
+ case "active":
this._log.trace("Received notification that we're back from idle.");
this.idle = false;
Utils.namedTimer(function onBack() {
@@ -234,15 +249,32 @@ SyncScheduler.prototype = {
}
}, IDLE_OBSERVER_BACK_DELAY, this, "idleDebouncerTimer");
break;
+ case "wake_notification":
+ this._log.debug("Woke from sleep.");
+ Utils.nextTick(() => {
+ // Trigger a sync if we have multiple clients.
+ if (this.numClients > 1) {
+ this._log.debug("More than 1 client. Syncing.");
+ this.scheduleNextSync(0);
+ }
+ });
+ break;
}
},
adjustSyncInterval: function adjustSyncInterval() {
+ if (Status.eol) {
+ this._log.debug("Server status is EOL; using eolInterval.");
+ this.syncInterval = this.eolInterval;
+ return;
+ }
+
if (this.numClients <= 1) {
this._log.trace("Adjusting syncInterval to singleDeviceInterval.");
this.syncInterval = this.singleDeviceInterval;
return;
}
+
// Only MULTI_DEVICE clients will enter this if statement
// since SINGLE_USER clients will be handled above.
if (this.idle) {
@@ -464,22 +496,67 @@ SyncScheduler.prototype = {
if (this.syncTimer)
this.syncTimer.clear();
},
-};
-const LOG_PREFIX_SUCCESS = "success-";
-const LOG_PREFIX_ERROR = "error-";
+ /**
+ * Prevent new syncs from starting. This is used by the FxA migration code
+ * where we can't afford to have a sync start partway through the migration.
+ * To handle the edge-case of a sync starting and not stopping, we store
+ * this state in a pref, so on the next startup we remain blocked (and thus
+ * sync will never start) so the migration can complete.
+ *
+ * As a safety measure, we only block for some period of time, and after
+ * that it will automatically unblock. This ensures that if things go
+ * really pear-shaped and we never end up calling unblockSync() we haven't
+ * completely broken the world.
+ */
+ blockSync: function(until = null) {
+ if (!until) {
+ until = Date.now() + DEFAULT_BLOCK_PERIOD;
+ }
+ // until is specified in ms, but Prefs can't hold that much
+ Svc.Prefs.set("scheduler.blocked-until", Math.floor(until / 1000));
+ },
+
+ unblockSync: function() {
+ Svc.Prefs.reset("scheduler.blocked-until");
+ // the migration code should be ready to roll, so resume normal operations.
+ this.checkSyncStatus();
+ },
+
+ get isBlocked() {
+ let until = Svc.Prefs.get("scheduler.blocked-until");
+ if (until === undefined) {
+ return false;
+ }
+ if (until <= Math.floor(Date.now() / 1000)) {
+ // we were previously blocked but the time has expired.
+ Svc.Prefs.reset("scheduler.blocked-until");
+ return false;
+ }
+ // we remain blocked.
+ return true;
+ },
+};
this.ErrorHandler = function ErrorHandler(service) {
this.service = service;
this.init();
}
ErrorHandler.prototype = {
+ MINIMUM_ALERT_INTERVAL_MSEC: 604800000, // One week.
/**
* Flag that turns on error reporting for all errors, incl. network errors.
*/
dontIgnoreErrors: false,
+ /**
+ * Flag that indicates if we have already reported a prolonged failure.
+ * Once set, we don't report it again, meaning this error is only reported
+ * one per run.
+ */
+ didReportProlongedError: false,
+
init: function init() {
Svc.Obs.add("weave:engine:sync:applied", this);
Svc.Obs.add("weave:engine:sync:error", this);
@@ -491,25 +568,16 @@ ErrorHandler.prototype = {
},
initLogs: function initLogs() {
- this._log = Log4Moz.repository.getLogger("Sync.ErrorHandler");
- this._log.level = Log4Moz.Level[Svc.Prefs.get("log.logger.service.main")];
- this._cleaningUpFileLogs = false;
+ this._log = Log.repository.getLogger("Sync.ErrorHandler");
+ this._log.level = Log.Level[Svc.Prefs.get("log.logger.service.main")];
- let root = Log4Moz.repository.getLogger("Sync");
- root.level = Log4Moz.Level[Svc.Prefs.get("log.rootLogger")];
+ let root = Log.repository.getLogger("Sync");
+ root.level = Log.Level[Svc.Prefs.get("log.rootLogger")];
- let formatter = new Log4Moz.BasicFormatter();
- let capp = new Log4Moz.ConsoleAppender(formatter);
- capp.level = Log4Moz.Level[Svc.Prefs.get("log.appender.console")];
- root.addAppender(capp);
+ let logs = ["Sync", "FirefoxAccounts", "Hawk", "Common.TokenServerClient",
+ "Sync.SyncMigration"];
- let dapp = new Log4Moz.DumpAppender(formatter);
- dapp.level = Log4Moz.Level[Svc.Prefs.get("log.appender.dump")];
- root.addAppender(dapp);
-
- let fapp = this._logAppender = new Log4Moz.StorageStreamAppender(formatter);
- fapp.level = Log4Moz.Level[Svc.Prefs.get("log.appender.file.level")];
- root.addAppender(fapp);
+ this._logManager = new LogManager(Svc.Prefs, logs, "sync");
},
observe: function observe(subject, topic, data) {
@@ -534,8 +602,7 @@ ErrorHandler.prototype = {
this._log.debug(engine_name + " failed: " + Utils.exceptionStr(exception));
break;
case "weave:service:login:error":
- this.resetFileLog(Svc.Prefs.get("log.appender.file.logOnError"),
- LOG_PREFIX_ERROR);
+ this.resetFileLog(this._logManager.REASON_ERROR);
if (this.shouldReportError()) {
this.notifyOnNextTick("weave:ui:login:error");
@@ -550,8 +617,7 @@ ErrorHandler.prototype = {
this.service.logout();
}
- this.resetFileLog(Svc.Prefs.get("log.appender.file.logOnError"),
- LOG_PREFIX_ERROR);
+ this.resetFileLog(this._logManager.REASON_ERROR);
if (this.shouldReportError()) {
this.notifyOnNextTick("weave:ui:sync:error");
@@ -577,8 +643,7 @@ ErrorHandler.prototype = {
if (Status.service == SYNC_FAILED_PARTIAL) {
this._log.debug("Some engines did not sync correctly.");
- this.resetFileLog(Svc.Prefs.get("log.appender.file.logOnError"),
- LOG_PREFIX_ERROR);
+ this.resetFileLog(this._logManager.REASON_ERROR);
if (this.shouldReportError()) {
this.dontIgnoreErrors = false;
@@ -586,8 +651,7 @@ ErrorHandler.prototype = {
break;
}
} else {
- this.resetFileLog(Svc.Prefs.get("log.appender.file.logOnSuccess"),
- LOG_PREFIX_SUCCESS);
+ this.resetFileLog(this._logManager.REASON_SUCCESS);
}
this.dontIgnoreErrors = false;
this.notifyOnNextTick("weave:ui:sync:finish");
@@ -615,94 +679,21 @@ ErrorHandler.prototype = {
},
/**
- * Finds all logs older than maxErrorAge and deletes them without tying up I/O.
- */
- cleanupLogs: function cleanupLogs() {
- let direntries = FileUtils.getDir("ProfD", ["weave", "logs"]).directoryEntries;
- let oldLogs = [];
- let index = 0;
- let threshold = Date.now() - 1000 * Svc.Prefs.get("log.appender.file.maxErrorAge");
-
- this._log.debug("Log cleanup threshold time: " + threshold);
- while (direntries.hasMoreElements()) {
- let logFile = direntries.getNext().QueryInterface(Ci.nsIFile);
- if (logFile.lastModifiedTime < threshold) {
- this._log.trace(" > Noting " + logFile.leafName +
- " for cleanup (" + logFile.lastModifiedTime + ")");
- oldLogs.push(logFile);
- }
- }
-
- // Deletes a file from oldLogs each tick until there are none left.
- let errorHandler = this;
- function deleteFile() {
- if (index >= oldLogs.length) {
- errorHandler._log.debug("Done deleting files.");
- errorHandler._cleaningUpFileLogs = false;
- Svc.Obs.notify("weave:service:cleanup-logs");
- return;
- }
- try {
- let file = oldLogs[index];
- file.remove(false);
- errorHandler._log.trace("Deleted " + file.leafName + ".");
- } catch (ex) {
- errorHandler._log._debug("Encountered error trying to clean up old log file '"
- + oldLogs[index].leafName + "':"
- + Utils.exceptionStr(ex));
- }
- index++;
- Utils.nextTick(deleteFile);
- }
-
- if (oldLogs.length > 0) {
- this._cleaningUpFileLogs = true;
- Utils.nextTick(deleteFile);
- } else {
- this._log.debug("No logs to clean up.");
- }
- },
-
- /**
* Generate a log file for the sync that just completed
* and refresh the input & output streams.
*
- * @param flushToFile
- * the log file to be flushed/reset
- *
- * @param filenamePrefix
- * a value of either LOG_PREFIX_SUCCESS or LOG_PREFIX_ERROR
- * to be used as the log filename prefix
+ * @param reason
+ * A constant from the LogManager that indicates the reason for the
+ * reset.
*/
- resetFileLog: function resetFileLog(flushToFile, filenamePrefix) {
- let inStream = this._logAppender.getInputStream();
- this._logAppender.reset();
- if (flushToFile && inStream) {
- this._log.debug("Flushing file log.");
- try {
- let filename = filenamePrefix + Date.now() + ".txt";
- let file = FileUtils.getFile("ProfD", ["weave", "logs", filename]);
- let outStream = FileUtils.openFileOutputStream(file);
-
- this._log.trace("Beginning stream copy to " + file.leafName + ": " +
- Date.now());
- NetUtil.asyncCopy(inStream, outStream, function onCopyComplete() {
- this._log.trace("onCopyComplete: " + Date.now());
- this._log.trace("Output file timestamp: " + file.lastModifiedTime);
- Svc.Obs.notify("weave:service:reset-file-log");
- this._log.trace("Notified: " + Date.now());
- if (filenamePrefix == LOG_PREFIX_ERROR &&
- !this._cleaningUpFileLogs) {
- this._log.trace("Scheduling cleanup.");
- Utils.nextTick(this.cleanupLogs, this);
- }
- }.bind(this));
- } catch (ex) {
- Svc.Obs.notify("weave:service:reset-file-log");
- }
- } else {
+ resetFileLog: function resetFileLog(reason) {
+ let onComplete = () => {
Svc.Obs.notify("weave:service:reset-file-log");
- }
+ this._log.trace("Notified: " + Date.now());
+ };
+ // Note we do not return the promise here - the caller doesn't need to wait
+ // for this to complete.
+ this._logManager.resetFileLog(reason).then(onComplete, onComplete);
},
/**
@@ -746,11 +737,23 @@ ErrorHandler.prototype = {
return true;
}
+ if (Status.login == LOGIN_FAILED_LOGIN_REJECTED) {
+ // An explicit LOGIN_REJECTED state is always reported (bug 1081158)
+ this._log.trace("shouldReportError: true (login was rejected)");
+ return true;
+ }
+
let lastSync = Svc.Prefs.get("lastSync");
if (lastSync && ((Date.now() - Date.parse(lastSync)) >
Svc.Prefs.get("errorhandler.networkFailureReportTimeout") * 1000)) {
Status.sync = PROLONGED_SYNC_FAILURE;
- this._log.trace("shouldReportError: true (prolonged sync failure).");
+ if (this.didReportProlongedError) {
+ this._log.trace("shouldReportError: false (prolonged sync failure, but" +
+ " we've already reported it).");
+ return false;
+ }
+ this._log.trace("shouldReportError: true (first prolonged sync failure).");
+ this.didReportProlongedError = true;
return true;
}
@@ -767,12 +770,97 @@ ErrorHandler.prototype = {
[Status.login, Status.sync].indexOf(LOGIN_FAILED_NETWORK_ERROR) == -1);
},
+ get currentAlertMode() {
+ return Svc.Prefs.get("errorhandler.alert.mode");
+ },
+
+ set currentAlertMode(str) {
+ return Svc.Prefs.set("errorhandler.alert.mode", str);
+ },
+
+ get earliestNextAlert() {
+ return Svc.Prefs.get("errorhandler.alert.earliestNext", 0) * 1000;
+ },
+
+ set earliestNextAlert(msec) {
+ return Svc.Prefs.set("errorhandler.alert.earliestNext", msec / 1000);
+ },
+
+ clearServerAlerts: function () {
+ // If we have any outstanding alerts, apparently they're no longer relevant.
+ Svc.Prefs.resetBranch("errorhandler.alert");
+ },
+
+ /**
+ * X-Weave-Alert headers can include a JSON object:
+ *
+ * {
+ * "code": // One of "hard-eol", "soft-eol".
+ * "url": // For "Learn more" link.
+ * "message": // Logged in Sync logs.
+ * }
+ */
+ handleServerAlert: function (xwa) {
+ if (!xwa.code) {
+ this._log.warn("Got structured X-Weave-Alert, but no alert code.");
+ return;
+ }
+
+ switch (xwa.code) {
+ // Gently and occasionally notify the user that this service will be
+ // shutting down.
+ case "soft-eol":
+ // Fall through.
+
+ // Tell the user that this service has shut down, and drop our syncing
+ // frequency dramatically.
+ case "hard-eol":
+ // Note that both of these alerts should be subservient to future "sign
+ // in with your Firefox Account" storage alerts.
+ if ((this.currentAlertMode != xwa.code) ||
+ (this.earliestNextAlert < Date.now())) {
+ Utils.nextTick(function() {
+ Svc.Obs.notify("weave:eol", xwa);
+ }, this);
+ this._log.error("X-Weave-Alert: " + xwa.code + ": " + xwa.message);
+ this.earliestNextAlert = Date.now() + this.MINIMUM_ALERT_INTERVAL_MSEC;
+ this.currentAlertMode = xwa.code;
+ }
+ break;
+ default:
+ this._log.debug("Got unexpected X-Weave-Alert code: " + xwa.code);
+ }
+ },
+
/**
* Handle HTTP response results or exceptions and set the appropriate
* Status.* bits.
+ *
+ * This method also looks for "side-channel" warnings.
*/
- checkServerError: function checkServerError(resp) {
+ checkServerError: function (resp) {
switch (resp.status) {
+ case 200:
+ case 404:
+ case 513:
+ let xwa = resp.headers['x-weave-alert'];
+
+ // Only process machine-readable alerts.
+ if (!xwa || !xwa.startsWith("{")) {
+ this.clearServerAlerts();
+ return;
+ }
+
+ try {
+ xwa = JSON.parse(xwa);
+ } catch (ex) {
+ this._log.warn("Malformed X-Weave-Alert from server: " + xwa);
+ return;
+ }
+
+ this.handleServerAlert(xwa);
+ break;
+
case 400:
if (resp == RESPONSE_OVER_QUOTA) {
Status.sync = OVER_QUOTA;
diff --git a/services/sync/modules/record.js b/services/sync/modules/record.js
index b1194bea6..4b3324d30 100644
--- a/services/sync/modules/record.js
+++ b/services/sync/modules/record.js
@@ -18,7 +18,7 @@ const Cu = Components.utils;
const CRYPTO_COLLECTION = "crypto";
const KEYS_WBO = "keys";
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/keys.js");
Cu.import("resource://services-sync/resource.js");
@@ -105,68 +105,6 @@ WBORecord.prototype = {
Utils.deferGetSet(WBORecord, "data", ["id", "modified", "sortindex", "payload"]);
-/**
- * An interface and caching layer for records.
- */
-this.RecordManager = function RecordManager(service) {
- this.service = service;
-
- this._log = Log4Moz.repository.getLogger(this._logName);
- this._records = {};
-}
-RecordManager.prototype = {
- _recordType: WBORecord,
- _logName: "Sync.RecordManager",
-
- import: function RecordMgr_import(url) {
- this._log.trace("Importing record: " + (url.spec ? url.spec : url));
- try {
- // Clear out the last response with empty object if GET fails
- this.response = {};
- this.response = this.service.resource(url).get();
-
- // Don't parse and save the record on failure
- if (!this.response.success)
- return null;
-
- let record = new this._recordType(url);
- record.deserialize(this.response);
-
- return this.set(url, record);
- } catch(ex) {
- this._log.debug("Failed to import record: " + Utils.exceptionStr(ex));
- return null;
- }
- },
-
- get: function RecordMgr_get(url) {
- // Use a url string as the key to the hash
- let spec = url.spec ? url.spec : url;
- if (spec in this._records)
- return this._records[spec];
- return this.import(url);
- },
-
- set: function RecordMgr_set(url, record) {
- let spec = url.spec ? url.spec : url;
- return this._records[spec] = record;
- },
-
- contains: function RecordMgr_contains(url) {
- if ((url.spec || url) in this._records)
- return true;
- return false;
- },
-
- clearCache: function recordMgr_clearCache() {
- this._records = {};
- },
-
- del: function RecordMgr_del(url) {
- delete this._records[url];
- }
-};
-
this.CryptoWrapper = function CryptoWrapper(collection, id) {
this.cleartext = {};
WBORecord.call(this, collection, id);
@@ -269,6 +207,67 @@ CryptoWrapper.prototype = {
Utils.deferGetSet(CryptoWrapper, "payload", ["ciphertext", "IV", "hmac"]);
Utils.deferGetSet(CryptoWrapper, "cleartext", "deleted");
+/**
+ * An interface and caching layer for records.
+ */
+this.RecordManager = function RecordManager(service) {
+ this.service = service;
+
+ this._log = Log.repository.getLogger(this._logName);
+ this._records = {};
+}
+RecordManager.prototype = {
+ _recordType: CryptoWrapper,
+ _logName: "Sync.RecordManager",
+
+ import: function RecordMgr_import(url) {
+ this._log.trace("Importing record: " + (url.spec ? url.spec : url));
+ try {
+ // Clear out the last response with empty object if GET fails
+ this.response = {};
+ this.response = this.service.resource(url).get();
+
+ // Don't parse and save the record on failure
+ if (!this.response.success)
+ return null;
+
+ let record = new this._recordType(url);
+ record.deserialize(this.response);
+
+ return this.set(url, record);
+ } catch(ex) {
+ this._log.debug("Failed to import record: " + Utils.exceptionStr(ex));
+ return null;
+ }
+ },
+
+ get: function RecordMgr_get(url) {
+ // Use a url string as the key to the hash
+ let spec = url.spec ? url.spec : url;
+ if (spec in this._records)
+ return this._records[spec];
+ return this.import(url);
+ },
+
+ set: function RecordMgr_set(url, record) {
+ let spec = url.spec ? url.spec : url;
+ return this._records[spec] = record;
+ },
+
+ contains: function RecordMgr_contains(url) {
+ if ((url.spec || url) in this._records)
+ return true;
+ return false;
+ },
+
+ clearCache: function recordMgr_clearCache() {
+ this._records = {};
+ },
+
+ del: function RecordMgr_del(url) {
+ delete this._records[url];
+ }
+};
/**
* Keeps track of mappings between collection names ('tabs') and KeyBundles.
@@ -281,7 +280,7 @@ this.CollectionKeyManager = function CollectionKeyManager() {
this._collections = {};
this._default = null;
- this._log = Log4Moz.repository.getLogger("Sync.CollectionKeyManager");
+ this._log = Log.repository.getLogger("Sync.CollectionKeyManager");
}
// TODO: persist this locally as an Identity. Bug 610913.
diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js
index e6587cd43..1c2a67b90 100644
--- a/services/sync/modules/resource.js
+++ b/services/sync/modules/resource.js
@@ -14,7 +14,7 @@ const Cu = Components.utils;
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://services-common/async.js");
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-common/observers.js");
Cu.import("resource://services-common/utils.js");
Cu.import("resource://services-sync/constants.js");
@@ -50,9 +50,9 @@ const DEFAULT_LOAD_FLAGS =
* the status of the HTTP response.
*/
this.AsyncResource = function AsyncResource(uri) {
- this._log = Log4Moz.repository.getLogger(this._logName);
+ this._log = Log.repository.getLogger(this._logName);
this._log.level =
- Log4Moz.Level[Svc.Prefs.get("log.logger.network.resources")];
+ Log.Level[Svc.Prefs.get("log.logger.network.resources")];
this.uri = uri;
this._headers = {};
this._onComplete = Utils.bind2(this, this._onComplete);
@@ -146,7 +146,14 @@ AsyncResource.prototype = {
// to obtain a request channel.
//
_createRequest: function Res__createRequest(method) {
- let channel = Services.io.newChannel(this.spec, null, null)
+ let channel = Services.io.newChannel2(this.spec,
+ null,
+ null,
+ null, // aLoadingNode
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null, // aTriggeringPrincipal
+ Ci.nsILoadInfo.SEC_NORMAL,
+ Ci.nsIContentPolicy.TYPE_OTHER)
.QueryInterface(Ci.nsIRequest)
.QueryInterface(Ci.nsIHttpChannel);
@@ -265,7 +272,7 @@ AsyncResource.prototype = {
this._log.debug(mesg);
// Additionally give the full response body when Trace logging.
- if (this._log.level <= Log4Moz.Level.Trace)
+ if (this._log.level <= Log.Level.Trace)
this._log.trace(action + " body: " + data);
} catch(ex) {
@@ -302,6 +309,14 @@ AsyncResource.prototype = {
Observers.notify("weave:service:quota:remaining",
parseInt(headers["x-weave-quota-remaining"], 10));
}
+
+ let contentLength = headers["content-length"];
+ if (success && contentLength && data &&
+ contentLength != data.length) {
+ this._log.warn("The response body's length of: " + data.length +
+ " doesn't match the header's content-length of: " +
+ contentLength + ".");
+ }
} catch (ex) {
this._log.debug("Caught exception " + CommonUtils.exceptionStr(ex) +
" visiting headers in _onComplete.");
@@ -380,7 +395,8 @@ Resource.prototype = {
function callback(error, ret) {
if (error)
cb.throw(error);
- cb(ret);
+ else
+ cb(ret);
}
// The channel listener might get a failure code
@@ -588,8 +604,8 @@ ChannelListener.prototype = {
function ChannelNotificationListener(headersToCopy) {
this._headersToCopy = headersToCopy;
- this._log = Log4Moz.repository.getLogger(this._logName);
- this._log.level = Log4Moz.Level[Svc.Prefs.get("log.logger.network.resources")];
+ this._log = Log.repository.getLogger(this._logName);
+ this._log.level = Log.Level[Svc.Prefs.get("log.logger.network.resources")];
}
ChannelNotificationListener.prototype = {
_logName: "Sync.Resource",
@@ -609,7 +625,7 @@ ChannelNotificationListener.prototype = {
},
notifyCertProblem: function certProblem(socketInfo, sslStatus, targetHost) {
- let log = Log4Moz.repository.getLogger("Sync.CertListener");
+ let log = Log.repository.getLogger("Sync.CertListener");
log.warn("Invalid HTTPS certificate encountered!");
// This suppresses the UI warning only. The request is still cancelled.
diff --git a/services/sync/modules/rest.js b/services/sync/modules/rest.js
index 15e83a24f..34382eed5 100644
--- a/services/sync/modules/rest.js
+++ b/services/sync/modules/rest.js
@@ -4,7 +4,7 @@
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-common/rest.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://services-sync/constants.js");
@@ -86,5 +86,21 @@ SyncStorageRequest.prototype = {
Svc.Obs.notify("weave:service:quota:remaining",
parseInt(headers["x-weave-quota-remaining"], 10));
}
+ },
+
+ onStopRequest: function onStopRequest(channel, context, statusCode) {
+ if (this.status != this.ABORTED) {
+ let resp = this.response;
+ let contentLength = resp.headers ? resp.headers["content-length"] : "";
+
+ if (resp.success && contentLength &&
+ contentLength != resp.body.length) {
+ this._log.warn("The response body's length of: " + resp.body.length +
+ " doesn't match the header's content-length of: " +
+ contentLength + ".");
+ }
+ }
+
+ RESTRequest.prototype.onStopRequest.apply(this, arguments);
}
};
diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js
index 85829a00f..4b792adf8 100644
--- a/services/sync/modules/service.js
+++ b/services/sync/modules/service.js
@@ -20,7 +20,7 @@ const KEYS_WBO = "keys";
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-common/utils.js");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/engines.js");
@@ -30,15 +30,14 @@ Cu.import("resource://services-sync/policies.js");
Cu.import("resource://services-sync/record.js");
Cu.import("resource://services-sync/resource.js");
Cu.import("resource://services-sync/rest.js");
-Cu.import("resource://services-sync/stages/cluster.js");
Cu.import("resource://services-sync/stages/enginesync.js");
+Cu.import("resource://services-sync/stages/declined.js");
Cu.import("resource://services-sync/status.js");
Cu.import("resource://services-sync/userapi.js");
Cu.import("resource://services-sync/util.js");
const ENGINE_MODULES = {
Addons: "addons.js",
- Apps: "apps.js",
Bookmarks: "bookmarks.js",
Form: "forms.js",
History: "history.js",
@@ -52,6 +51,14 @@ const STORAGE_INFO_TYPES = [INFO_COLLECTIONS,
INFO_COLLECTION_COUNTS,
INFO_QUOTA];
+// A structure mapping a (boolean) telemetry probe name to a preference name.
+// The probe will record true if the pref is modified, false otherwise.
+const TELEMETRY_CUSTOM_SERVER_PREFS = {
+ WEAVE_CUSTOM_LEGACY_SERVER_CONFIGURATION: "services.sync.serverURL",
+ WEAVE_CUSTOM_FXA_SERVER_CONFIGURATION: "identity.fxaccounts.auth.uri",
+ WEAVE_CUSTOM_TOKEN_SERVER_CONFIGURATION: "services.sync.tokenServerURI",
+};
+
function Sync11Service() {
this._notify = Utils.notify("weave:service:");
@@ -62,16 +69,11 @@ Sync11Service.prototype = {
_locked: false,
_loggedIn: false,
- userBaseURL: null,
infoURL: null,
storageURL: null,
metaURL: null,
cryptoKeyURL: null,
- get enabledEngineNames() {
- return [e.name for each (e in this.engineManager.getEnabled())];
- },
-
get serverURL() Svc.Prefs.get("serverURL"),
set serverURL(value) {
if (!value.endsWith("/")) {
@@ -111,7 +113,7 @@ Sync11Service.prototype = {
get userAPIURI() {
// Append to the serverURL if it's a relative fragment.
let url = Svc.Prefs.get("userURL");
- if (!url.contains(":")) {
+ if (!url.includes(":")) {
url = this.serverURL + url;
}
@@ -158,13 +160,18 @@ Sync11Service.prototype = {
return Utils.catch.call(this, func, lockExceptions);
},
+ get userBaseURL() {
+ if (!this._clusterManager) {
+ return null;
+ }
+ return this._clusterManager.getUserBaseURL();
+ },
+
_updateCachedURLs: function _updateCachedURLs() {
// Nothing to cache yet if we don't have the building blocks
- if (this.clusterURL == "" || this.identity.username == "")
+ if (!this.clusterURL || !this.identity.username)
return;
- let storageAPI = this.clusterURL + SYNC_API_VERSION + "/";
- this.userBaseURL = storageAPI + this.identity.username + "/";
this._log.debug("Caching URLs under storage user base: " + this.userBaseURL);
// Generate and cache various URLs under the storage API for this user
@@ -298,6 +305,21 @@ Sync11Service.prototype = {
return false;
},
+ // The global "enabled" state comes from prefs, and will be set to false
+ // whenever the UI that exposes what to sync finds all Sync engines disabled.
+ get enabled() {
+ return Svc.Prefs.get("enabled");
+ },
+ set enabled(val) {
+ // There's no real reason to impose this other than to catch someone doing
+ // something we don't expect with bad consequences - all setting of this
+ // pref are in the UI code and external to this module.
+ if (val) {
+ throw new Error("Only disabling via this setter is supported");
+ }
+ Svc.Prefs.set("enabled", val);
+ },
+
/**
* Prepare to initialize the rest of Weave after waiting a little bit
*/
@@ -318,17 +340,15 @@ Sync11Service.prototype = {
this.errorHandler = new ErrorHandler(this);
- this._log = Log4Moz.repository.getLogger("Sync.Service");
+ this._log = Log.repository.getLogger("Sync.Service");
this._log.level =
- Log4Moz.Level[Svc.Prefs.get("log.logger.service.main")];
+ Log.Level[Svc.Prefs.get("log.logger.service.main")];
this._log.info("Loading Weave " + WEAVE_VERSION);
- this._clusterManager = new ClusterManager(this);
+ this._clusterManager = this.identity.createClusterManager(this);
this.recordManager = new RecordManager(this);
- this.enabled = true;
-
this._registerEngines();
let ua = Cc["@mozilla.org/network/protocol;1?name=http"].
@@ -357,6 +377,12 @@ Sync11Service.prototype = {
Svc.Obs.notify("weave:engine:start-tracking");
}
+ // Telemetry probes to indicate if the user is using custom servers.
+ for (let [probeName, prefName] of Iterator(TELEMETRY_CUSTOM_SERVER_PREFS)) {
+ let isCustomized = Services.prefs.prefHasUserValue(prefName);
+ Services.telemetry.getHistogramById(probeName).add(isCustomized);
+ }
+
// Send an event now that Weave service is ready. We don't do this
// synchronously so that observers can import this module before
// registering an observer.
@@ -428,6 +454,12 @@ Sync11Service.prototype = {
engines = pref.split(",");
}
+ let declined = [];
+ pref = Svc.Prefs.get("declinedEngines");
+ if (pref) {
+ declined = pref.split(",");
+ }
+
this.clientsEngine = new ClientEngine(this);
for (let name of engines) {
@@ -446,12 +478,14 @@ Sync11Service.prototype = {
continue;
}
- this.engineManager.register(ns[engineName], this);
+ this.engineManager.register(ns[engineName]);
} catch (ex) {
this._log.warn("Could not register engine " + name + ": " +
CommonUtils.exceptionStr(ex));
}
}
+
+ this.engineManager.setDeclined(declined);
},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
@@ -507,9 +541,10 @@ Sync11Service.prototype = {
},
/**
- * Perform the info fetch as part of a login or key fetch.
+ * Perform the info fetch as part of a login or key fetch, or
+ * inside engine sync.
*/
- _fetchInfo: function _fetchInfo(url) {
+ _fetchInfo: function (url) {
let infoURL = url || this.infoURL;
this._log.trace("In _fetchInfo: " + infoURL);
@@ -520,9 +555,11 @@ Sync11Service.prototype = {
this.errorHandler.checkServerError(ex);
throw ex;
}
+
+ // Always check for errors; this is also where we look for X-Weave-Alert.
+ this.errorHandler.checkServerError(info);
if (!info.success) {
- this.errorHandler.checkServerError(info);
- throw "aborting sync, failed to get collections";
+ throw "Aborting sync: failed to get collections.";
}
return info;
},
@@ -646,24 +683,35 @@ Sync11Service.prototype = {
}
},
- verifyLogin: function verifyLogin() {
+ verifyLogin: function verifyLogin(allow40XRecovery = true) {
+ // If the identity isn't ready it might not know the username...
+ if (!this.identity.readyToAuthenticate) {
+ this._log.info("Not ready to authenticate in verifyLogin.");
+ this.status.login = LOGIN_FAILED_NOT_READY;
+ return false;
+ }
+
if (!this.identity.username) {
this._log.warn("No username in verifyLogin.");
this.status.login = LOGIN_FAILED_NO_USERNAME;
return false;
}
- // Unlock master password, or return.
// Attaching auth credentials to a request requires access to
// passwords, which means that Resource.get can throw MP-related
// exceptions!
- // Try to fetch the passphrase first, while we still have control.
- try {
- this.identity.syncKey;
- } catch (ex) {
- this._log.debug("Fetching passphrase threw " + ex +
- "; assuming master password locked.");
- this.status.login = MASTER_PASSWORD_LOCKED;
+ // So we ask the identity to verify the login state after unlocking the
+ // master password (ie, this call is expected to prompt for MP unlock
+ // if necessary) while we still have control.
+ let cb = Async.makeSpinningCallback();
+ this.identity.unlockAndVerifyAuthState().then(
+ result => cb(null, result),
+ cb
+ );
+ let unlockedState = cb.wait();
+ this._log.debug("Fetching unlocked auth state returned " + unlockedState);
+ if (unlockedState != STATUS_OK) {
+ this.status.login = unlockedState;
return false;
}
@@ -673,7 +721,6 @@ Sync11Service.prototype = {
// to succeed, since that probably means we just don't have storage.
if (this.clusterURL == "" && !this._clusterManager.setCluster()) {
this.status.sync = NO_SYNC_NODE_FOUND;
- Svc.Obs.notify("weave:service:sync:delayed");
return true;
}
@@ -695,7 +742,7 @@ Sync11Service.prototype = {
// Go ahead and do remote setup, so that we can determine
// conclusively that our passphrase is correct.
- if (this._remoteSetup()) {
+ if (this._remoteSetup(test)) {
// Username/password verified.
this.status.login = LOGIN_SUCCEEDED;
return true;
@@ -711,8 +758,8 @@ Sync11Service.prototype = {
case 404:
// Check that we're verifying with the correct cluster
- if (this._clusterManager.setCluster()) {
- return this.verifyLogin();
+ if (allow40XRecovery && this._clusterManager.setCluster()) {
+ return this.verifyLogin(false);
}
// We must have the right cluster, but the server doesn't expect us
@@ -763,20 +810,20 @@ Sync11Service.prototype = {
info = info.obj;
if (!(CRYPTO_COLLECTION in info)) {
- this._log.error("Consistency failure: info/collections excludes " +
+ this._log.error("Consistency failure: info/collections excludes " +
"crypto after successful upload.");
throw new Error("Symmetric key upload failed.");
}
// Can't check against local modified: clock drift.
if (info[CRYPTO_COLLECTION] < serverModified) {
- this._log.error("Consistency failure: info/collections crypto entry " +
+ this._log.error("Consistency failure: info/collections crypto entry " +
"is stale after successful upload.");
throw new Error("Symmetric key upload failed.");
}
-
+
// Doesn't matter if the timestamp is ahead.
-
+
// Download and install them.
let cryptoKeys = new CryptoWrapper(CRYPTO_COLLECTION, KEYS_WBO);
let cryptoResp = cryptoKeys.fetch(this.resource(this.cryptoKeysURL)).response;
@@ -840,14 +887,6 @@ Sync11Service.prototype = {
Svc.Obs.notify("weave:engine:stop-tracking");
this.status.resetSync();
- // We want let UI consumers of the following notification know as soon as
- // possible, so let's fake for the CLIENT_NOT_CONFIGURED status for now
- // by emptying the passphrase (we still need the password).
- this.identity.syncKey = null;
- this.status.login = LOGIN_FAILED_NO_PASSPHRASE;
- this.logout();
- Svc.Obs.notify("weave:service:start-over");
-
// Deletion doesn't make sense if we aren't set up yet!
if (this.clusterURL != "") {
// Clear client-specific data from the server, including disabled engines.
@@ -859,10 +898,20 @@ Sync11Service.prototype = {
+ Utils.exceptionStr(ex));
}
}
+ this._log.debug("Finished deleting client data.");
} else {
this._log.debug("Skipping client data removal: no cluster URL.");
}
+ // We want let UI consumers of the following notification know as soon as
+ // possible, so let's fake for the CLIENT_NOT_CONFIGURED status for now
+ // by emptying the passphrase (we still need the password).
+ this._log.info("Service.startOver dropping sync key and logging out.");
+ this.identity.resetSyncKey();
+ this.status.login = LOGIN_FAILED_NO_PASSPHRASE;
+ this.logout();
+ Svc.Obs.notify("weave:service:start-over");
+
// Reset all engines and clear keys.
this.resetClient();
this.collectionKeys.clear();
@@ -877,7 +926,36 @@ Sync11Service.prototype = {
this.identity.deleteSyncCredentials();
- Svc.Obs.notify("weave:service:start-over:finish");
+ // If necessary, reset the identity manager, then re-initialize it so the
+ // FxA manager is used. This is configurable via a pref - mainly for tests.
+ let keepIdentity = false;
+ try {
+ keepIdentity = Services.prefs.getBoolPref("services.sync-testing.startOverKeepIdentity");
+ } catch (_) { /* no such pref */ }
+ if (keepIdentity) {
+ Svc.Obs.notify("weave:service:start-over:finish");
+ return;
+ }
+
+ this.identity.finalize().then(
+ () => {
+ // an observer so the FxA migration code can take some action before
+ // the new identity is created.
+ Svc.Obs.notify("weave:service:start-over:init-identity");
+ this.identity.username = "";
+ this.status.__authManager = null;
+ this.identity = Status._authManager;
+ this._clusterManager = this.identity.createClusterManager(this);
+ Svc.Obs.notify("weave:service:start-over:finish");
+ }
+ ).then(null,
+ err => {
+ this._log.error("startOver failed to re-initialize the identity manager: " + err);
+ // Still send the observer notification so the current state is
+ // reflected in the UI.
+ Svc.Obs.notify("weave:service:start-over:finish");
+ }
+ );
},
persistLogin: function persistLogin() {
@@ -911,14 +989,20 @@ Sync11Service.prototype = {
throw "Aborting login, client not configured.";
}
+ // Ask the identity manager to explicitly login now.
+ let cb = Async.makeSpinningCallback();
+ this.identity.ensureLoggedIn().then(cb, cb);
+
+ // Just let any errors bubble up - they've more context than we do!
+ cb.wait();
+
// Calling login() with parameters when the client was
// previously not configured means setup was completed.
if (initialStatus == CLIENT_NOT_CONFIGURED
&& (username || password || passphrase)) {
Svc.Obs.notify("weave:service:setup-complete");
}
-
- this._log.info("Logging in user " + this.identity.username);
+ this._log.info("Logging in the user.");
this._updateCachedURLs();
if (!this.verifyLogin()) {
@@ -936,11 +1020,11 @@ Sync11Service.prototype = {
},
logout: function logout() {
- // No need to do anything if we're already logged out.
- if (!this._loggedIn)
- return;
-
+ // If we failed during login, we aren't going to have this._loggedIn set,
+ // but we still want to ask the identity to logout, so it doesn't try and
+ // reuse any old credentials next time we sync.
this._log.info("Logging out");
+ this.identity.logout();
this._loggedIn = false;
Svc.Obs.notify("weave:service:logout:finish");
@@ -1008,11 +1092,20 @@ Sync11Service.prototype = {
// ... fetch the current record from the server, and COPY THE FLAGS.
let newMeta = this.recordManager.get(this.metaURL);
+ // If we got a 401, we do not want to create a new meta/global - we
+ // should be able to get the existing meta after we get a new node.
+ if (this.recordManager.response.status == 401) {
+ this._log.debug("Fetching meta/global record on the server returned 401.");
+ this.errorHandler.checkServerError(this.recordManager.response);
+ return false;
+ }
+
if (!this.recordManager.response.success || !newMeta) {
this._log.debug("No meta/global record on the server. Creating one.");
newMeta = new WBORecord("meta", "global");
newMeta.payload.syncID = this.syncID;
newMeta.payload.storageVersion = STORAGE_VERSION;
+ newMeta.payload.declined = this.engineManager.getDeclined();
newMeta.isNew = true;
@@ -1162,6 +1255,10 @@ Sync11Service.prototype = {
},
sync: function sync() {
+ if (!this.enabled) {
+ this._log.debug("Not syncing as Sync is disabled.");
+ return;
+ }
let dateStr = new Date().toLocaleFormat(LOG_DATE_FORMAT);
this._log.debug("User-Agent: " + SyncStorageRequest.prototype.userAgent);
this._log.info("Starting sync at " + dateStr);
@@ -1188,6 +1285,9 @@ Sync11Service.prototype = {
return this._lock("service.js: sync",
this._notify("sync", "", function onNotify() {
+ let histogram = Services.telemetry.getHistogramById("WEAVE_START_COUNT");
+ histogram.add(1);
+
let synchronizer = new EngineSynchronizer(this);
let cb = Async.makeSpinningCallback();
synchronizer.onComplete = cb;
@@ -1196,10 +1296,142 @@ Sync11Service.prototype = {
// wait() throws if the first argument is truthy, which is exactly what
// we want.
let result = cb.wait();
+
+ histogram = Services.telemetry.getHistogramById("WEAVE_COMPLETE_SUCCESS_COUNT");
+ histogram.add(1);
+
+ // We successfully synchronized.
+ // Check if the identity wants to pre-fetch a migration sentinel from
+ // the server.
+ // If we have no clusterURL, we are probably doing a node reassignment
+ // so don't attempt to get it in that case.
+ if (this.clusterURL) {
+ this.identity.prefetchMigrationSentinel(this);
+ }
+
+ // Now let's update our declined engines.
+ let meta = this.recordManager.get(this.metaURL);
+ if (!meta) {
+ this._log.warn("No meta/global; can't update declined state.");
+ return;
+ }
+
+ let declinedEngines = new DeclinedEngines(this);
+ let didChange = declinedEngines.updateDeclined(meta, this.engineManager);
+ if (!didChange) {
+ this._log.info("No change to declined engines. Not reuploading meta/global.");
+ return;
+ }
+
+ this.uploadMetaGlobal(meta);
}))();
},
/**
+ * Upload meta/global, throwing the response on failure.
+ */
+ uploadMetaGlobal: function (meta) {
+ this._log.debug("Uploading meta/global: " + JSON.stringify(meta));
+
+ // It would be good to set the X-If-Unmodified-Since header to `timestamp`
+ // for this PUT to ensure at least some level of transactionality.
+ // Unfortunately, the servers don't support it after a wipe right now
+ // (bug 693893), so we're going to defer this until bug 692700.
+ let res = this.resource(this.metaURL);
+ let response = res.put(meta);
+ if (!response.success) {
+ throw response;
+ }
+ this.recordManager.set(this.metaURL, meta);
+ },
+
+ /**
+ * Get a migration sentinel for the Firefox Accounts migration.
+ * Returns a JSON blob - it is up to callers of this to make sense of the
+ * data.
+ *
+ * Returns a promise that resolves with the sentinel, or null.
+ */
+ getFxAMigrationSentinel: function() {
+ if (this._shouldLogin()) {
+ this._log.debug("In getFxAMigrationSentinel: should login.");
+ if (!this.login()) {
+ this._log.debug("Can't get migration sentinel: login returned false.");
+ return Promise.resolve(null);
+ }
+ }
+ if (!this.identity.syncKeyBundle) {
+ this._log.error("Can't get migration sentinel: no syncKeyBundle.");
+ return Promise.resolve(null);
+ }
+ try {
+ let collectionURL = this.storageURL + "meta/fxa_credentials";
+ let cryptoWrapper = this.recordManager.get(collectionURL);
+ if (!cryptoWrapper || !cryptoWrapper.payload) {
+ // nothing to decrypt - .decrypt is noisy in that case, so just bail
+ // now.
+ return Promise.resolve(null);
+ }
+ // If the payload has a sentinel it means we must have put back the
+ // decrypted version last time we were called.
+ if (cryptoWrapper.payload.sentinel) {
+ return Promise.resolve(cryptoWrapper.payload.sentinel);
+ }
+ // If decryption fails it almost certainly means the key is wrong - but
+ // it's not clear if we need to take special action for that case?
+ let payload = cryptoWrapper.decrypt(this.identity.syncKeyBundle);
+ // After decrypting the ciphertext is lost, so we just stash the
+ // decrypted payload back into the wrapper.
+ cryptoWrapper.payload = payload;
+ return Promise.resolve(payload.sentinel);
+ } catch (ex) {
+ this._log.error("Failed to fetch the migration sentinel: ${}", ex);
+ return Promise.resolve(null);
+ }
+ },
+
+ /**
+ * Set a migration sentinel for the Firefox Accounts migration.
+ * Accepts a JSON blob - it is up to callers of this to make sense of the
+ * data.
+ *
+ * Returns a promise that resolves with a boolean which indicates if the
+ * sentinel was successfully written.
+ */
+ setFxAMigrationSentinel: function(sentinel) {
+ if (this._shouldLogin()) {
+ this._log.debug("In setFxAMigrationSentinel: should login.");
+ if (!this.login()) {
+ this._log.debug("Can't set migration sentinel: login returned false.");
+ return Promise.resolve(false);
+ }
+ }
+ if (!this.identity.syncKeyBundle) {
+ this._log.error("Can't set migration sentinel: no syncKeyBundle.");
+ return Promise.resolve(false);
+ }
+ try {
+ let collectionURL = this.storageURL + "meta/fxa_credentials";
+ let cryptoWrapper = new CryptoWrapper("meta", "fxa_credentials");
+ cryptoWrapper.cleartext.sentinel = sentinel;
+
+ cryptoWrapper.encrypt(this.identity.syncKeyBundle);
+
+ let res = this.resource(collectionURL);
+ let response = res.put(cryptoWrapper.toJSON());
+
+ if (!response.success) {
+ throw response;
+ }
+ this.recordManager.set(collectionURL, cryptoWrapper);
+ } catch (ex) {
+ this._log.error("Failed to set the migration sentinel: ${}", ex);
+ return Promise.resolve(false);
+ }
+ return Promise.resolve(true);
+ },
+
+ /**
* If we have a passphrase, rather than a 25-alphadigit sync key,
* use the provided sync ID to bootstrap it using PBKDF2.
*
@@ -1254,26 +1486,19 @@ Sync11Service.prototype = {
let meta = new WBORecord("meta", "global");
meta.payload.syncID = this.syncID;
meta.payload.storageVersion = STORAGE_VERSION;
+ meta.payload.declined = this.engineManager.getDeclined();
meta.isNew = true;
- this._log.debug("New metadata record: " + JSON.stringify(meta.payload));
- let res = this.resource(this.metaURL);
- // It would be good to set the X-If-Unmodified-Since header to `timestamp`
- // for this PUT to ensure at least some level of transactionality.
- // Unfortunately, the servers don't support it after a wipe right now
- // (bug 693893), so we're going to defer this until bug 692700.
- let resp = res.put(meta);
- if (!resp.success) {
- // If we got into a race condition, we'll abort the sync this way, too.
- // That's fine. We'll just wait till the next sync. The client that we're
- // racing is probably busy uploading stuff right now anyway.
- throw resp;
- }
- this.recordManager.set(this.metaURL, meta);
+ // uploadMetaGlobal throws on failure -- including race conditions.
+ // If we got into a race condition, we'll abort the sync this way, too.
+ // That's fine. We'll just wait till the next sync. The client that we're
+ // racing is probably busy uploading stuff right now anyway.
+ this.uploadMetaGlobal(meta);
// Wipe everything we know about except meta because we just uploaded it
let engines = [this.clientsEngine].concat(this.engineManager.getAll());
let collections = [engine.name for each (engine in engines)];
+ // TODO: there's a bug here. We should be calling resetClient, no?
// Generate, upload, and download new keys. Do this last so we don't wipe
// them...
@@ -1381,7 +1606,9 @@ Sync11Service.prototype = {
// Only wipe the engines provided.
if (engines) {
- engines.forEach(function(e) this.clientsEngine.sendCommand("wipeEngine", [e]), this);
+ engines.forEach(function(e) {
+ this.clientsEngine.sendCommand("wipeEngine", [e]);
+ }, this);
}
// Tell the remote machines to wipe themselves.
else {
diff --git a/services/sync/modules/stages/cluster.js b/services/sync/modules/stages/cluster.js
index dd7717201..dd358bf98 100644
--- a/services/sync/modules/stages/cluster.js
+++ b/services/sync/modules/stages/cluster.js
@@ -1,12 +1,12 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
this.EXPORTED_SYMBOLS = ["ClusterManager"];
const {utils: Cu} = Components;
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/policies.js");
Cu.import("resource://services-sync/util.js");
@@ -15,8 +15,8 @@ Cu.import("resource://services-sync/util.js");
* Contains code for managing the Sync cluster we are in.
*/
this.ClusterManager = function ClusterManager(service) {
- this._log = Log4Moz.repository.getLogger("Sync.Service");
- this._log.level = Log4Moz.Level[Svc.Prefs.get("log.logger.service.main")];
+ this._log = Log.repository.getLogger("Sync.Service");
+ this._log.level = Log.Level[Svc.Prefs.get("log.logger.service.main")];
this.service = service;
}
@@ -91,5 +91,21 @@ ClusterManager.prototype = {
return true;
},
+
+ getUserBaseURL: function getUserBaseURL() {
+ // Legacy Sync and FxA Sync construct the userBaseURL differently. Legacy
+ // Sync appends path components onto an empty path, and in FxA Sync, the
+ // token server constructs this for us in an opaque manner. Since the
+ // cluster manager already sets the clusterURL on Service and also has
+ // access to the current identity, we added this functionality here.
+
+ // If the clusterURL hasn't been set, the userBaseURL shouldn't be set
+ // either. Some tests expect "undefined" to be returned here.
+ if (!this.service.clusterURL) {
+ return undefined;
+ }
+ let storageAPI = this.service.clusterURL + SYNC_API_VERSION + "/";
+ return storageAPI + this.identity.username + "/";
+ }
};
Object.freeze(ClusterManager.prototype);
diff --git a/services/sync/modules/stages/declined.js b/services/sync/modules/stages/declined.js
new file mode 100644
index 000000000..b0877e929
--- /dev/null
+++ b/services/sync/modules/stages/declined.js
@@ -0,0 +1,76 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This file contains code for maintaining the set of declined engines,
+ * in conjunction with EngineManager.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["DeclinedEngines"];
+
+const {utils: Cu} = Components;
+
+Cu.import("resource://services-sync/constants.js");
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://services-common/utils.js");
+Cu.import("resource://services-common/observers.js");
+Cu.import("resource://gre/modules/Preferences.jsm");
+
+
+
+this.DeclinedEngines = function (service) {
+ this._log = Log.repository.getLogger("Sync.Declined");
+ this._log.level = Log.Level[new Preferences(PREFS_BRANCH).get("log.logger.declined")];
+
+ this.service = service;
+}
+this.DeclinedEngines.prototype = {
+ updateDeclined: function (meta, engineManager=this.service.engineManager) {
+ let enabled = new Set([e.name for each (e in engineManager.getEnabled())]);
+ let known = new Set([e.name for each (e in engineManager.getAll())]);
+ let remoteDeclined = new Set(meta.payload.declined || []);
+ let localDeclined = new Set(engineManager.getDeclined());
+
+ this._log.debug("Handling remote declined: " + JSON.stringify([...remoteDeclined]));
+ this._log.debug("Handling local declined: " + JSON.stringify([...localDeclined]));
+
+ // Any engines that are locally enabled should be removed from the remote
+ // declined list.
+ //
+ // Any engines that are locally declined should be added to the remote
+ // declined list.
+ let newDeclined = CommonUtils.union(localDeclined, CommonUtils.difference(remoteDeclined, enabled));
+
+ // If our declined set has changed, put it into the meta object and mark
+ // it as changed.
+ let declinedChanged = !CommonUtils.setEqual(newDeclined, remoteDeclined);
+ this._log.debug("Declined changed? " + declinedChanged);
+ if (declinedChanged) {
+ meta.changed = true;
+ meta.payload.declined = [...newDeclined];
+ }
+
+ // Update the engine manager regardless.
+ engineManager.setDeclined(newDeclined);
+
+ // Any engines that are locally known, locally disabled, and not remotely
+ // or locally declined, are candidates for enablement.
+ let undecided = CommonUtils.difference(CommonUtils.difference(known, enabled), newDeclined);
+ if (undecided.size) {
+ let subject = {
+ declined: newDeclined,
+ enabled: enabled,
+ known: known,
+ undecided: undecided,
+ };
+ CommonUtils.nextTick(() => {
+ Observers.notify("weave:engines:notdeclined", subject);
+ });
+ }
+
+ return declinedChanged;
+ },
+};
diff --git a/services/sync/modules/stages/enginesync.js b/services/sync/modules/stages/enginesync.js
index 823c71ffc..ed91adddb 100644
--- a/services/sync/modules/stages/enginesync.js
+++ b/services/sync/modules/stages/enginesync.js
@@ -1,6 +1,6 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* This file contains code for synchronizing engines.
@@ -10,7 +10,7 @@ this.EXPORTED_SYMBOLS = ["EngineSynchronizer"];
const {utils: Cu} = Components;
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/policies.js");
@@ -22,8 +22,8 @@ Cu.import("resource://services-sync/util.js");
* This was originally split out of service.js. The API needs lots of love.
*/
this.EngineSynchronizer = function EngineSynchronizer(service) {
- this._log = Log4Moz.repository.getLogger("Sync.Synchronizer");
- this._log.level = Log4Moz.Level[Svc.Prefs.get("log.logger.synchronizer")];
+ this._log = Log.repository.getLogger("Sync.Synchronizer");
+ this._log.level = Log.Level[Svc.Prefs.get("log.logger.synchronizer")];
this.service = service;
@@ -71,11 +71,13 @@ EngineSynchronizer.prototype = {
Svc.Prefs.set("lastPing", now);
}
+ let engineManager = this.service.engineManager;
+
// Figure out what the last modified time is for each collection
let info = this.service._fetchInfo(infoURL);
// Convert the response to an object and read out the modified times
- for (let engine of [this.service.clientsEngine].concat(this.service.engineManager.getAll())) {
+ for (let engine of [this.service.clientsEngine].concat(engineManager.getAll())) {
engine.lastModified = info.obj[engine.name] || 0;
}
@@ -97,13 +99,13 @@ EngineSynchronizer.prototype = {
// Wipe data in the desired direction if necessary
switch (Svc.Prefs.get("firstSync")) {
case "resetClient":
- this.service.resetClient(this.service.enabledEngineNames);
+ this.service.resetClient(engineManager.enabledEngineNames);
break;
case "wipeClient":
- this.service.wipeClient(this.service.enabledEngineNames);
+ this.service.wipeClient(engineManager.enabledEngineNames);
break;
case "wipeRemote":
- this.service.wipeRemote(this.service.enabledEngineNames);
+ this.service.wipeRemote(engineManager.enabledEngineNames);
break;
}
@@ -142,7 +144,7 @@ EngineSynchronizer.prototype = {
}
try {
- for each (let engine in this.service.engineManager.getEnabled()) {
+ for (let engine of engineManager.getEnabled()) {
// If there's any problems with syncing the engine, report the failure
if (!(this._syncEngine(engine)) || this.service.status.enforceBackoff) {
this._log.info("Aborting sync for failure in " + engine.name);
@@ -160,12 +162,17 @@ EngineSynchronizer.prototype = {
return;
}
- // Upload meta/global if any engines changed anything
+ // Upload meta/global if any engines changed anything.
let meta = this.service.recordManager.get(this.service.metaURL);
if (meta.isNew || meta.changed) {
- this.service.resource(this.service.metaURL).put(meta);
- delete meta.isNew;
- delete meta.changed;
+ this._log.info("meta/global changed locally: reuploading.");
+ try {
+ this.service.uploadMetaGlobal(meta);
+ delete meta.isNew;
+ delete meta.changed;
+ } catch (error) {
+ this._log.error("Unable to upload meta/global. Leaving marked as new.");
+ }
}
// If there were no sync engine failures
@@ -205,17 +212,19 @@ EngineSynchronizer.prototype = {
return true;
},
- _updateEnabledEngines: function _updateEnabledEngines() {
+ _updateEnabledFromMeta: function (meta, numClients, engineManager=this.service.engineManager) {
this._log.info("Updating enabled engines: " +
- this.service.scheduler.numClients + " clients.");
- let meta = this.service.recordManager.get(this.service.metaURL);
- if (meta.isNew || !meta.payload.engines)
+ numClients + " clients.");
+
+ if (meta.isNew || !meta.payload.engines) {
+ this._log.debug("meta/global isn't new, or is missing engines. Not updating enabled state.");
return;
+ }
// If we're the only client, and no engines are marked as enabled,
// thumb our noses at the server data: it can't be right.
// Belt-and-suspenders approach to Bug 615926.
- if ((this.service.scheduler.numClients <= 1) &&
+ if ((numClients <= 1) &&
([e for (e in meta.payload.engines) if (e != "clients")].length == 0)) {
this._log.info("One client and no enabled engines: not touching local engine status.");
return;
@@ -223,7 +232,11 @@ EngineSynchronizer.prototype = {
this.service._ignorePrefObserver = true;
- let enabled = this.service.enabledEngineNames;
+ let enabled = engineManager.enabledEngineNames;
+
+ let toDecline = new Set();
+ let toUndecline = new Set();
+
for (let engineName in meta.payload.engines) {
if (engineName == "clients") {
// Clients is special.
@@ -235,40 +248,73 @@ EngineSynchronizer.prototype = {
enabled.splice(index, 1);
continue;
}
- let engine = this.service.engineManager.get(engineName);
+ let engine = engineManager.get(engineName);
if (!engine) {
// The engine doesn't exist locally. Nothing to do.
continue;
}
- if (Svc.Prefs.get("engineStatusChanged." + engine.prefName, false)) {
- // The engine was disabled locally. Wipe server data and
- // disable it everywhere.
+ let attemptedEnable = false;
+ // If the engine was enabled remotely, enable it locally.
+ if (!Svc.Prefs.get("engineStatusChanged." + engine.prefName, false)) {
+ this._log.trace("Engine " + engineName + " was enabled. Marking as non-declined.");
+ toUndecline.add(engineName);
+ this._log.trace(engineName + " engine was enabled remotely.");
+ engine.enabled = true;
+ // Note that setting engine.enabled to true might not have worked for
+ // the password engine if a master-password is enabled. However, it's
+ // still OK that we added it to undeclined - the user *tried* to enable
+ // it remotely - so it still winds up as not being flagged as declined
+ // even though it's disabled remotely.
+ attemptedEnable = true;
+ }
+
+ // If either the engine was disabled locally or enabling the engine
+ // failed (see above re master-password) then wipe server data and
+ // disable it everywhere.
+ if (!engine.enabled) {
this._log.trace("Wiping data for " + engineName + " engine.");
engine.wipeServer();
delete meta.payload.engines[engineName];
- meta.changed = true;
- } else {
- // The engine was enabled remotely. Enable it locally.
- this._log.trace(engineName + " engine was enabled remotely.");
- engine.enabled = true;
+ meta.changed = true; // the new enabled state must propagate
+ // We also here mark the engine as declined, because the pref
+ // was explicitly changed to false - unless we tried, and failed,
+ // to enable it - in which case we leave the declined state alone.
+ if (!attemptedEnable) {
+ // This will be reflected in meta/global in the next stage.
+ this._log.trace("Engine " + engineName + " was disabled locally. Marking as declined.");
+ toDecline.add(engineName);
+ }
}
}
// Any remaining engines were either enabled locally or disabled remotely.
for each (let engineName in enabled) {
- let engine = this.service.engineManager.get(engineName);
+ let engine = engineManager.get(engineName);
if (Svc.Prefs.get("engineStatusChanged." + engine.prefName, false)) {
this._log.trace("The " + engineName + " engine was enabled locally.");
+ toUndecline.add(engineName);
} else {
this._log.trace("The " + engineName + " engine was disabled remotely.");
+
+ // Don't automatically mark it as declined!
engine.enabled = false;
}
}
+ engineManager.decline(toDecline);
+ engineManager.undecline(toUndecline);
+
Svc.Prefs.resetBranch("engineStatusChanged.");
this.service._ignorePrefObserver = false;
},
+ _updateEnabledEngines: function () {
+ let meta = this.service.recordManager.get(this.service.metaURL);
+ let numClients = this.service.scheduler.numClients;
+ let engineManager = this.service.engineManager;
+
+ this._updateEnabledFromMeta(meta, numClients, engineManager);
+ },
};
Object.freeze(EngineSynchronizer.prototype);
diff --git a/services/sync/modules/status.js b/services/sync/modules/status.js
index f17736a94..19dff9712 100644
--- a/services/sync/modules/status.js
+++ b/services/sync/modules/status.js
@@ -10,21 +10,39 @@ const Cr = Components.results;
const Cu = Components.utils;
Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/identity.js");
+Cu.import("resource://services-sync/browserid_identity.js");
Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://services-common/async.js");
this.Status = {
- _log: Log4Moz.repository.getLogger("Sync.Status"),
- _authManager: new IdentityManager(),
+ _log: Log.repository.getLogger("Sync.Status"),
+ __authManager: null,
ready: false,
+ get _authManager() {
+ if (this.__authManager) {
+ return this.__authManager;
+ }
+ let service = Components.classes["@mozilla.org/weave/service;1"]
+ .getService(Components.interfaces.nsISupports)
+ .wrappedJSObject;
+ let idClass = service.fxAccountsEnabled ? BrowserIDManager : IdentityManager;
+ this.__authManager = new idClass();
+ // .initialize returns a promise, so we need to spin until it resolves.
+ let cb = Async.makeSpinningCallback();
+ this.__authManager.initialize().then(cb, cb);
+ cb.wait();
+ return this.__authManager;
+ },
+
get service() {
return this._service;
},
set service(code) {
- this._log.debug("Status.service: " + this._service + " => " + code);
+ this._log.debug("Status.service: " + (this._service || undefined) + " => " + code);
this._service = code;
},
@@ -57,6 +75,15 @@ this.Status = {
this.service = code == SYNC_SUCCEEDED ? STATUS_OK : SYNC_FAILED;
},
+ get eol() {
+ let modePref = PREFS_BRANCH + "errorhandler.alert.mode";
+ try {
+ return Services.prefs.getCharPref(modePref) == "hard-eol";
+ } catch (ex) {
+ return false;
+ }
+ },
+
get engines() {
return this._engines;
},
@@ -105,7 +132,7 @@ this.Status = {
} catch (ex) {
// Use default.
}
- this._log.level = Log4Moz.Level[logLevel];
+ this._log.level = Log.Level[logLevel];
this._log.info("Resetting Status.");
this.service = STATUS_OK;
diff --git a/services/sync/modules/userapi.js b/services/sync/modules/userapi.js
index d09a98d5d..ec77d63e2 100644
--- a/services/sync/modules/userapi.js
+++ b/services/sync/modules/userapi.js
@@ -10,7 +10,7 @@ this.EXPORTED_SYMBOLS = [
const {utils: Cu} = Components;
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-common/rest.js");
Cu.import("resource://services-common/utils.js");
Cu.import("resource://services-sync/identity.js");
@@ -24,8 +24,8 @@ Cu.import("resource://services-sync/util.js");
* Instances are constructed with the base URI of the service.
*/
this.UserAPI10Client = function UserAPI10Client(baseURI) {
- this._log = Log4Moz.repository.getLogger("Sync.UserAPI");
- this._log.level = Log4Moz.Level[Svc.Prefs.get("log.logger.userapi")];
+ this._log = Log.repository.getLogger("Sync.UserAPI");
+ this._log.level = Log.Level[Svc.Prefs.get("log.logger.userapi")];
this.baseURI = baseURI;
}
@@ -165,7 +165,7 @@ UserAPI10Client.prototype = {
return;
}
- let error = new Error("Sync node retrieval failed.");
+ error = new Error("Sync node retrieval failed.");
switch (response.status) {
case 400:
error.denied = true;
@@ -214,7 +214,7 @@ UserAPI10Client.prototype = {
return;
}
- let error = new Error("Could not create user.");
+ error = new Error("Could not create user.");
error.body = response.body;
cb(error, null);
diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js
index e0aae8486..67cc3f063 100644
--- a/services/sync/modules/util.js
+++ b/services/sync/modules/util.js
@@ -2,24 +2,29 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-this.EXPORTED_SYMBOLS = ["XPCOMUtils", "Services", "NetUtil", "PlacesUtils",
- "FileUtils", "Utils", "Async", "Svc", "Str"];
+this.EXPORTED_SYMBOLS = ["XPCOMUtils", "Services", "Utils", "Async", "Svc", "Str"];
const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-common/observers.js");
Cu.import("resource://services-common/stringbundle.js");
Cu.import("resource://services-common/utils.js");
Cu.import("resource://services-common/async.js", this);
Cu.import("resource://services-crypto/utils.js");
Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://gre/modules/FileUtils.jsm", this);
-Cu.import("resource://gre/modules/NetUtil.jsm", this);
-Cu.import("resource://gre/modules/PlacesUtils.jsm", this);
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/Services.jsm", this);
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/osfile.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+
+// FxAccountsCommon.js doesn't use a "namespace", so create one here.
+XPCOMUtils.defineLazyGetter(this, "FxAccountsCommon", function() {
+ let FxAccountsCommon = {};
+ Cu.import("resource://gre/modules/FxAccountsCommon.js", FxAccountsCommon);
+ return FxAccountsCommon;
+});
/*
* Utility functions
@@ -38,6 +43,7 @@ this.Utils = {
safeAtoB: CommonUtils.safeAtoB,
byteArrayToString: CommonUtils.byteArrayToString,
bytesAsHex: CommonUtils.bytesAsHex,
+ hexToBytes: CommonUtils.hexToBytes,
encodeBase32: CommonUtils.encodeBase32,
decodeBase32: CommonUtils.decodeBase32,
@@ -60,7 +66,7 @@ this.Utils = {
*
* @usage MyObj._catch = Utils.catch;
* MyObj.foo = function() { this._catch(func)(); }
- *
+ *
* Optionally pass a function which will be called if an
* exception occurs.
*/
@@ -101,7 +107,7 @@ this.Utils = {
}
};
},
-
+
isLockException: function isLockException(ex) {
return ex && ex.indexOf && ex.indexOf("Could not acquire lock.") == 0;
},
@@ -109,14 +115,14 @@ this.Utils = {
/**
* Wrap functions to notify when it starts and finishes executing or if it
* threw an error.
- *
+ *
* The message is a combination of a provided prefix, the local name, and
* the event. Possible events are: "start", "finish", "error". The subject
* is the function's return value on "finish" or the caught exception on
* "error". The data argument is the predefined data value.
- *
+ *
* Example:
- *
+ *
* @usage function MyObj(name) {
* this.name = name;
* this._notify = Utils.notify("obj:");
@@ -151,22 +157,6 @@ this.Utils = {
};
},
- runInTransaction: function(db, callback, thisObj) {
- let hasTransaction = false;
- try {
- db.beginTransaction();
- hasTransaction = true;
- } catch(e) { /* om nom nom exceptions */ }
-
- try {
- return callback.call(thisObj);
- } finally {
- if (hasTransaction) {
- db.commitTransaction();
- }
- }
- },
-
/**
* GUIDs are 9 random bytes encoded with base64url (RFC 4648).
* That makes them 12 characters long with 72 bits of entropy.
@@ -187,57 +177,34 @@ this.Utils = {
* @param obj
* Object to add properties to defer in its prototype
* @param defer
- * Hash property of obj to defer to (dot split each level)
+ * Property of obj to defer to
* @param prop
* Property name to defer (or an array of property names)
*/
deferGetSet: function Utils_deferGetSet(obj, defer, prop) {
if (Array.isArray(prop))
- return prop.map(function(prop) Utils.deferGetSet(obj, defer, prop));
-
- // Split the defer into each dot part for each level to dereference
- let parts = defer.split(".");
- let deref = function(base) Utils.deref(base, parts);
+ return prop.map(prop => Utils.deferGetSet(obj, defer, prop));
let prot = obj.prototype;
// Create a getter if it doesn't exist yet
if (!prot.__lookupGetter__(prop)) {
- // Yes, this should be a one-liner, but there are errors if it's not
- // broken out. *sigh*
- // Errors are these:
- // JavaScript strict warning: resource://services-sync/util.js, line 304: reference to undefined property deref(this)[prop]
- // JavaScript strict warning: resource://services-sync/util.js, line 304: reference to undefined property deref(this)[prop]
- let f = function() {
- let d = deref(this);
- if (!d)
- return undefined;
- let out = d[prop];
- return out;
- }
- prot.__defineGetter__(prop, f);
+ prot.__defineGetter__(prop, function () {
+ return this[defer][prop];
+ });
}
// Create a setter if it doesn't exist yet
- if (!prot.__lookupSetter__(prop))
- prot.__defineSetter__(prop, function(val) deref(this)[prop] = val);
+ if (!prot.__lookupSetter__(prop)) {
+ prot.__defineSetter__(prop, function (val) {
+ this[defer][prop] = val;
+ });
+ }
},
-
- /**
- * Dereference an array of properties starting from a base object
- *
- * @param base
- * Base object to start dereferencing
- * @param props
- * Array of properties to dereference (one for each level)
- */
- deref: function Utils_deref(base, props) props.reduce(function(curr, prop)
- curr[prop], base),
-
lazyStrings: function Weave_lazyStrings(name) {
let bundle = "chrome://weave/locale/services/" + name + ".properties";
- return function() new StringBundle(bundle);
+ return () => new StringBundle(bundle);
},
deepEquals: function eq(a, b) {
@@ -360,41 +327,28 @@ this.Utils = {
* Function to process json object as its first argument. If the file
* could not be loaded, the first argument will be undefined.
*/
- jsonLoad: function jsonLoad(filePath, that, callback) {
- let path = "weave/" + filePath + ".json";
+ jsonLoad: Task.async(function*(filePath, that, callback) {
+ let path = OS.Path.join(OS.Constants.Path.profileDir, "weave", filePath + ".json");
if (that._log) {
that._log.trace("Loading json from disk: " + filePath);
}
- let file = FileUtils.getFile("ProfD", path.split("/"), true);
- if (!file.exists()) {
- callback.call(that);
- return;
- }
+ let json;
- let channel = NetUtil.newChannel(file);
- channel.contentType = "application/json";
-
- NetUtil.asyncFetch(channel, function (is, result) {
- if (!Components.isSuccessCode(result)) {
- callback.call(that);
- return;
- }
- let string = NetUtil.readInputStreamToString(is, is.available());
- is.close();
- let json;
- try {
- json = JSON.parse(string);
- } catch (ex) {
- if (that._log) {
- that._log.debug("Failed to load json: " +
- CommonUtils.exceptionStr(ex));
- }
+ try {
+ json = yield CommonUtils.readJSON(path);
+ } catch (e if e instanceof OS.File.Error && e.becauseNoSuchFile) {
+ // Ignore non-existent files.
+ } catch (e) {
+ if (that._log) {
+ that._log.debug("Failed to load json: " +
+ CommonUtils.exceptionStr(e));
}
- callback.call(that, json);
- });
- },
+ }
+
+ callback.call(that, json);
+ }),
/**
* Save a json-able object to disk in the profile directory.
@@ -412,36 +366,30 @@ this.Utils = {
* constant on error or null if no error was encountered (and
* the file saved successfully).
*/
- jsonSave: function jsonSave(filePath, that, obj, callback) {
- let path = "weave/" + filePath + ".json";
- if (that._log) {
- that._log.trace("Saving json to disk: " + path);
- }
+ jsonSave: Task.async(function*(filePath, that, obj, callback) {
+ let path = OS.Path.join(OS.Constants.Path.profileDir, "weave",
+ ...(filePath + ".json").split("/"));
+ let dir = OS.Path.dirname(path);
+ let error = null;
- let file = FileUtils.getFile("ProfD", path.split("/"), true);
- let json = typeof obj == "function" ? obj.call(that) : obj;
- let out = JSON.stringify(json);
+ try {
+ yield OS.File.makeDir(dir, { from: OS.Constants.Path.profileDir });
- let fos = FileUtils.openSafeFileOutputStream(file);
- let is = this._utf8Converter.convertToInputStream(out);
- NetUtil.asyncCopy(is, fos, function (result) {
- if (typeof callback == "function") {
- let error = (result == Cr.NS_OK) ? null : result;
- callback.call(that, error);
+ if (that._log) {
+ that._log.trace("Saving json to disk: " + path);
}
- });
- },
- getIcon: function(iconUri, defaultIcon) {
- try {
- let iconURI = Utils.makeURI(iconUri);
- return PlacesUtils.favicons.getFaviconLinkForIcon(iconURI).spec;
+ let json = typeof obj == "function" ? obj.call(that) : obj;
+
+ yield CommonUtils.writeJSON(json, path);
+ } catch (e) {
+ error = e
}
- catch(ex) {}
- // Just give the provided default icon or the system's default
- return defaultIcon || PlacesUtils.favicons.defaultFavicon.spec;
- },
+ if (typeof callback == "function") {
+ callback.call(that, error);
+ }
+ }),
getErrorString: function Utils_getErrorString(error, args) {
try {
@@ -547,7 +495,7 @@ this.Utils = {
// Something else -- just return.
return pp;
},
-
+
normalizeAccount: function normalizeAccount(acc) {
return acc.trim();
},
@@ -559,7 +507,7 @@ this.Utils = {
arraySub: function arraySub(minuend, subtrahend) {
if (!minuend.length || !subtrahend.length)
return minuend;
- return minuend.filter(function(i) subtrahend.indexOf(i) == -1);
+ return minuend.filter(i => subtrahend.indexOf(i) == -1);
},
/**
@@ -577,6 +525,22 @@ this.Utils = {
return function innerBind() { return method.apply(object, arguments); };
},
+ /**
+ * Is there a master password configured, regardless of current lock state?
+ */
+ mpEnabled: function mpEnabled() {
+ let modules = Cc["@mozilla.org/security/pkcs11moduledb;1"]
+ .getService(Ci.nsIPKCS11ModuleDB);
+ let sdrSlot = modules.findSlotByName("");
+ let status = sdrSlot.status;
+ let slots = Ci.nsIPKCS11Slot;
+
+ return status != slots.SLOT_UNINITIALIZED && status != slots.SLOT_READY;
+ },
+
+ /**
+ * Is there a master password configured and currently locked?
+ */
mpLocked: function mpLocked() {
let modules = Cc["@mozilla.org/security/pkcs11moduledb;1"]
.getService(Ci.nsIPKCS11ModuleDB);
@@ -590,7 +554,7 @@ this.Utils = {
if (status == slots.SLOT_NOT_LOGGED_IN)
return true;
-
+
// something wacky happened, pretend MP is locked
return true;
},
@@ -609,7 +573,7 @@ this.Utils = {
} catch(e) {}
return false;
},
-
+
/**
* Return a value for a backoff interval. Maximum is eight hours, unless
* Status.backoffInterval is higher.
@@ -623,6 +587,92 @@ this.Utils = {
return Math.max(Math.min(backoffInterval, MAXIMUM_BACKOFF_INTERVAL),
statusInterval);
},
+
+ /**
+ * Return a set of hostnames (including the protocol) which may have
+ * credentials for sync itself stored in the login manager.
+ *
+ * In general, these hosts will not have their passwords synced, will be
+ * reset when we drop sync credentials, etc.
+ */
+ getSyncCredentialsHosts: function() {
+ let result = new Set(this.getSyncCredentialsHostsLegacy());
+ for (let host of this.getSyncCredentialsHostsFxA()) {
+ result.add(host);
+ }
+ return result;
+ },
+
+ /*
+ * Get the "legacy" identity hosts.
+ */
+ getSyncCredentialsHostsLegacy: function() {
+ // the legacy sync host
+ return new Set([PWDMGR_HOST]);
+ },
+
+ /*
+ * Get the FxA identity hosts.
+ */
+ getSyncCredentialsHostsFxA: function() {
+ // This is somewhat expensive and the result static, so we cache the result.
+ if (this._syncCredentialsHostsFxA) {
+ return this._syncCredentialsHostsFxA;
+ }
+ let result = new Set();
+ // the FxA host
+ result.add(FxAccountsCommon.FXA_PWDMGR_HOST);
+ //
+ // The FxA hosts - these almost certainly all have the same hostname, but
+ // better safe than sorry...
+ for (let prefName of ["identity.fxaccounts.remote.force_auth.uri",
+ "identity.fxaccounts.remote.signup.uri",
+ "identity.fxaccounts.remote.signin.uri",
+ "identity.fxaccounts.settings.uri"]) {
+ let prefVal;
+ try {
+ prefVal = Services.prefs.getCharPref(prefName);
+ } catch (_) {
+ continue;
+ }
+ let uri = Services.io.newURI(prefVal, null, null);
+ result.add(uri.prePath);
+ }
+ return this._syncCredentialsHostsFxA = result;
+ },
+
+ getDefaultDeviceName() {
+ // Generate a client name if we don't have a useful one yet
+ let env = Cc["@mozilla.org/process/environment;1"]
+ .getService(Ci.nsIEnvironment);
+ let user = env.get("USER") || env.get("USERNAME") ||
+ Svc.Prefs.get("account") || Svc.Prefs.get("username");
+ // A little hack for people using the the moz-build environment on Windows
+ // which sets USER to the literal "%USERNAME%" (yes, really)
+ if (user == "%USERNAME%" && env.get("USERNAME")) {
+ user = env.get("USERNAME");
+ }
+
+ let brand = new StringBundle("chrome://branding/locale/brand.properties");
+ let brandName = brand.get("brandShortName");
+
+ let appName;
+ try {
+ let syncStrings = new StringBundle("chrome://browser/locale/sync.properties");
+ appName = syncStrings.getFormattedString("sync.defaultAccountApplication", [brandName]);
+ } catch (ex) {}
+ appName = appName || brandName;
+
+ let system =
+ // 'device' is defined on unix systems
+ Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2).get("device") ||
+ // hostname of the system, usually assigned by the user or admin
+ Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2).get("host") ||
+ // fall back on ua info string
+ Cc["@mozilla.org/network/protocol;1?name=http"].getService(Ci.nsIHttpProtocolHandler).oscpu;
+
+ return Str.sync.get("client.name2", [user, appName, system]);
+ }
};
XPCOMUtils.defineLazyGetter(Utils, "_utf8Converter", function() {