diff options
Diffstat (limited to 'services/sync/modules')
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() { |