Import Tycho weave client

This commit is contained in:
wolfbeast 2018-10-06 06:57:51 +02:00 committed by Roy Tam
commit c53787dfdb
183 changed files with 7272 additions and 16759 deletions

16
services/sync/Makefile.in Normal file
View file

@ -0,0 +1,16 @@
# 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/.
# Definitions used by constants.js.
weave_version := 1.40.0
weave_id := {340c2bbc-ce74-4362-90b5-7c26312808ef}
# Preprocess files.
SYNC_PP := modules/constants.js
SYNC_PP_FLAGS := \
-Dweave_version=$(weave_version) \
-Dweave_id='$(weave_id)'
SYNC_PP_PATH = $(FINAL_TARGET)/modules/services-sync
PP_TARGETS += SYNC_PP

View file

@ -1,22 +1,28 @@
# WeaveService has to restrict its registration for the app-startup category
# to the specific list of apps that use it so it doesn't get loaded in xpcshell.
# Thus we restrict it to these apps:
# WebappRT doesn't need these instructions, and they don't necessarily work
# with it, but it does use a GRE directory that the GRE shares with Firefox,
# so in order to prevent the instructions from being processed for WebappRT,
# we need to restrict them to the applications that depend on them, i.e.:
#
# b2g: {3c2e2abc-06d4-11e1-ac3b-374f68613e61}
# basilisk: {ec8030f7-c20a-464f-9b0e-13a3a9e97384}
# pale moon: {8de7fcbb-c55c-4fbe-bfc5-fc555c87dbc4}
# browser: {8de7fcbb-c55c-4fbe-bfc5-fc555c87dbc4}
# mobile/android: {aa3c5121-dab2-40e2-81ca-7ea25febc110}
# mobile/xul: {a23983c0-fd0e-11dc-95ff-0800200c9a66}
# suite (comm): {92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}
# graphene: {d1bfe7d9-c01e-4237-998b-7b5f960a4314}
#
# In theory we should do this for all these instructions, but in practice it is
# sufficient to do it for the app-startup one, and the file is simpler that way.
# Weave.js
component {74b89fb0-f200-4ae8-a3ec-dd164117f6de} Weave.js
contract @mozilla.org/weave/service;1 {74b89fb0-f200-4ae8-a3ec-dd164117f6de}
category app-startup WeaveService service,@mozilla.org/weave/service;1 application={3c2e2abc-06d4-11e1-ac3b-374f68613e61} application={ec8030f7-c20a-464f-9b0e-13a3a9e97384} application={8de7fcbb-c55c-4fbe-bfc5-fc555c87dbc4} application={aa3c5121-dab2-40e2-81ca-7ea25febc110} application={a23983c0-fd0e-11dc-95ff-0800200c9a66} application={92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a} application={99bceaaa-e3c6-48c1-b981-ef9b46b67d60} application={d1bfe7d9-c01e-4237-998b-7b5f960a4314}
category app-startup WeaveService service,@mozilla.org/weave/service;1 application={3c2e2abc-06d4-11e1-ac3b-374f68613e61} application={8de7fcbb-c55c-4fbe-bfc5-fc555c87dbc4} application={aa3c5121-dab2-40e2-81ca-7ea25febc110} application={a23983c0-fd0e-11dc-95ff-0800200c9a66} application={92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}
component {d28f8a0b-95da-48f4-b712-caf37097be41} Weave.js
contract @mozilla.org/network/protocol/about;1?what=sync-log {d28f8a0b-95da-48f4-b712-caf37097be41}
# Register resource aliases
# (Note, for tests these are also set up in addResourceAlias)
resource services-sync resource://gre/modules/services-sync/
#ifdef MOZ_SERVICES_HEALTHREPORT
category healthreport-js-provider-default SyncProvider resource://services-sync/healthreport.jsm
#endif

View file

@ -72,6 +72,13 @@ WeaveService.prototype = {
Ci.nsISupportsWeakReference]),
ensureLoaded: function () {
// XXX: We don't support FxA, so prevent migrator calls
// to the Sync server from this module! Don't load it.
// If we are loaded and not using FxA, load the migration module.
//if (!this.fxAccountsEnabled) {
// Cu.import("resource://services-sync/FxaMigrator.jsm");
//}
Components.utils.import("resource://services-sync/main.js");
// Side-effect of accessing the service is that it is instantiated.
@ -96,11 +103,14 @@ WeaveService.prototype = {
* Whether Firefox Accounts is enabled.
*
* @return bool
*
* This function is currently always returning false because we don't support
* the use of FxA/Sync-1.5 but do want to keep the code "just in case".
*/
get fxAccountsEnabled() {
#ifdef MC_PALEMOON
// Early exit: FxA not supported.
return false;
#else
try {
// Old sync guarantees '@' will never appear in the username while FxA
// uses the FxA email address - so '@' is the flag we use.
@ -109,7 +119,6 @@ WeaveService.prototype = {
} catch (_) {
return true; // No username == only allow FxA to be configured.
}
#endif
},
/**
@ -123,7 +132,8 @@ WeaveService.prototype = {
*/
get enabled() {
let prefs = Services.prefs.getBranch(SYNC_PREFS_BRANCH);
return prefs.prefHasUserValue("username");
return prefs.prefHasUserValue("username") &&
prefs.prefHasUserValue("clusterURL");
},
observe: function (subject, topic, data) {
@ -183,13 +193,10 @@ AboutWeaveLog.prototype = {
channel.originalURI = aURI;
// Ensure that the about page has the same privileges as a regular directory
// view. That way links to files can be opened. make sure we use the correct
// origin attributes when creating the principal for accessing the
// about:sync-log data.
// view. That way links to files can be opened.
let ssm = Cc["@mozilla.org/scriptsecuritymanager;1"]
.getService(Ci.nsIScriptSecurityManager);
let principal = ssm.createCodebasePrincipal(uri, aLoadInfo.originAttributes);
let principal = ssm.getNoAppCodebasePrincipal(uri);
channel.owner = principal;
return channel;
}

View file

@ -12,7 +12,7 @@ error.login.reason.server = Server incorrectly configured
error.sync.failed_partial = One or more data types could not be synced
# LOCALIZATION NOTE (error.sync.reason.serverMaintenance): We removed the extraneous period from this string
error.sync.reason.serverMaintenance = Sync server maintenance is underway, syncing will resume automatically
error.sync.reason.serverMaintenance = Sync server maintenance is underway; syncing will resume automatically
invalid-captcha = Incorrect words, try again
weak-password = Use a stronger password
@ -20,8 +20,8 @@ weak-password = Use a stronger password
# this is the fallback, if we hit an error we didn't bother to localize
error.reason.unknown = Unknown error
change.password.pwSameAsPassword = Password cant match current password
change.password.pwSameAsUsername = Password cant match your user name
change.password.pwSameAsEmail = Password cant match your email address
change.password.pwSameAsPassword = Password can't match current password
change.password.pwSameAsUsername = Password can't match your user name
change.password.pwSameAsEmail = Password can't match your email address
change.password.mismatch = The passwords entered do not match
change.password.tooShort = The password entered is too short

View file

@ -3,14 +3,17 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# %1: the user name (Ed), %2: the app name (Firefox), %3: the operating system (Android)
client.name2 = %1$Ss %2$S on %3$S
client.name2 = %1$S's %2$S on %3$S
# %S is the date and time at which the last sync successfully completed
lastSync2.label = Last sync: %S
# signInToSync.description is the tooltip for the Sync buttons when Sync is
# not configured.
signInToSync.description = Sign In To Sync
mobile.label = Mobile Bookmarks
remote.pending.label = Remote tabs are being synced…
remote.missing2.label = Sync your other devices again to access their tabs
remote.opened.label = All remote tabs are already open
remote.notification.label = Recent desktop tabs will be available once they sync
error.login.title = Error While Signing In
error.login.description = Sync encountered an error while connecting: %1$S. Please try again.
@ -31,16 +34,15 @@ error.sync.tryAgainButton.label = Sync Now
error.sync.tryAgainButton.accesskey = S
warning.sync.quota.label = Approaching Server Quota
warning.sync.quota.description = You are approaching the server quota. Please review which data to sync.
error.sync.quota.label = Server Quota Exceeded
error.sync.quota.description = Sync failed because it exceeded the server quota. Please review which data to sync.
error.sync.viewQuotaButton.label = View Quota
error.sync.viewQuotaButton.accesskey = V
warning.sync.eol.label = Service Shutting Down
# %1: the app name (Basilisk)
# %1: the app name (Firefox)
warning.sync.eol.description = Your Sync service is shutting down soon. Upgrade %1$S to keep syncing.
error.sync.eol.label = Service Unavailable
# %1: the app name (Basilisk)
# %1: the app name (Firefox)
error.sync.eol.description = Your Sync service is no longer available. You need to upgrade %1$S to keep syncing.
sync.eol.learnMore.label = Learn more
sync.eol.learnMore.accesskey = L
syncnow.label = Sync Now
syncing2.label = Syncing…

View file

@ -1,4 +1,4 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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

View file

@ -11,31 +11,17 @@ this.EXPORTED_SYMBOLS = [
"fakeSHA256HMAC",
];
var {utils: Cu} = Components;
const {utils: Cu} = Components;
Cu.import("resource://services-sync/record.js");
Cu.import("resource://services-sync/util.js");
var btoa = Cu.import("resource://gre/modules/Log.jsm").btoa;
let btoa = Cu.import("resource://gre/modules/Log.jsm").btoa;
this.FakeFilesystemService = function FakeFilesystemService(contents) {
this.fakeContents = contents;
let self = this;
// Save away the unmocked versions of the functions we replace here for tests
// that really want the originals. As this may be called many times per test,
// we must be careful to not replace them with ones we previously replaced.
// (And WTF are we bothering with these mocks in the first place? Is the
// performance of the filesystem *really* such that it outweighs the downside
// of not running our real JSON functions in the tests? Eg, these mocks don't
// always throw exceptions when the real ones do. Anyway...)
for (let name of ["jsonSave", "jsonLoad", "jsonMove", "jsonRemove"]) {
let origName = "_real_" + name;
if (!Utils[origName]) {
Utils[origName] = Utils[name];
}
}
Utils.jsonSave = function jsonSave(filePath, that, obj, callback) {
let json = typeof obj == "function" ? obj.call(that) : obj;
self.fakeContents["weave/" + filePath + ".json"] = JSON.stringify(json);
@ -50,18 +36,6 @@ this.FakeFilesystemService = function FakeFilesystemService(contents) {
}
cb.call(that, obj);
};
Utils.jsonMove = function jsonMove(aFrom, aTo, that) {
const fromPath = "weave/" + aFrom + ".json";
self.fakeContents["weave/" + aTo + ".json"] = self.fakeContents[fromPath];
delete self.fakeContents[fromPath];
return Promise.resolve();
};
Utils.jsonRemove = function jsonRemove(filePath, that) {
delete self.fakeContents["weave/" + filePath + ".json"];
return Promise.resolve();
};
};
this.fakeSHA256HMAC = function fakeSHA256HMAC(message) {
@ -76,9 +50,7 @@ this.FakeGUIDService = function FakeGUIDService() {
let latestGUID = 0;
Utils.makeGUID = function makeGUID() {
// ensure that this always returns a unique 12 character string
let nextGUID = "fake-guid-" + String(latestGUID++).padStart(2, "0");
return nextGUID.slice(nextGUID.length-12, nextGUID.length);
return "fake-guid-" + latestGUID++;
};
}

View file

@ -4,7 +4,7 @@ this.EXPORTED_SYMBOLS = [
"initializeIdentityWithTokenServerResponse",
];
var {utils: Cu} = Components;
const {utils: Cu} = Components;
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/main.js");

View file

@ -11,7 +11,7 @@ this.EXPORTED_SYMBOLS = [
"RotaryTracker",
];
var {utils: Cu} = Components;
const {utils: Cu} = Components;
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/record.js");
@ -32,8 +32,8 @@ RotaryRecord.prototype = {
};
Utils.deferGetSet(RotaryRecord, "cleartext", ["denomination"]);
this.RotaryStore = function RotaryStore(name, engine) {
Store.call(this, name, engine);
this.RotaryStore = function RotaryStore(engine) {
Store.call(this, "Rotary", engine);
this.items = {};
}
RotaryStore.prototype = {
@ -88,8 +88,8 @@ RotaryStore.prototype = {
}
};
this.RotaryTracker = function RotaryTracker(name, engine) {
Tracker.call(this, name, engine);
this.RotaryTracker = function RotaryTracker(engine) {
Tracker.call(this, "Rotary", engine);
}
RotaryTracker.prototype = {
__proto__: Tracker.prototype
@ -115,7 +115,7 @@ RotaryEngine.prototype = {
return "DUPE_LOCAL";
}
for (let [id, value] of Object.entries(this._store.items)) {
for (let [id, value] in Iterator(this._store.items)) {
if (item.denomination == value) {
return id;
}

View file

@ -7,23 +7,18 @@
this.EXPORTED_SYMBOLS = [
"btoa", // It comes from a module import.
"encryptPayload",
"isConfiguredWithLegacyIdentity",
"ensureLegacyIdentityManager",
"setBasicCredentials",
"makeIdentityConfig",
"makeFxAccountsInternalMock",
"configureFxAccountIdentity",
"configureIdentity",
"SyncTestingInfrastructure",
"waitForZeroTimer",
"Promise", // from a module import
"add_identity_test",
"MockFxaStorageManager",
"AccountState", // from a module import
"sumHistogram",
];
var {utils: Cu} = Components;
const {utils: Cu} = Components;
Cu.import("resource://services-sync/status.js");
Cu.import("resource://services-sync/identity.js");
@ -34,49 +29,8 @@ Cu.import("resource://services-sync/browserid_identity.js");
Cu.import("resource://testing-common/services/common/logging.js");
Cu.import("resource://testing-common/services/sync/fakeservices.js");
Cu.import("resource://gre/modules/FxAccounts.jsm");
Cu.import("resource://gre/modules/FxAccountsClient.jsm");
Cu.import("resource://gre/modules/FxAccountsCommon.js");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Services.jsm");
// and grab non-exported stuff via a backstage pass.
const {AccountState} = Cu.import("resource://gre/modules/FxAccounts.jsm", {});
// A mock "storage manager" for FxAccounts that doesn't actually write anywhere.
function MockFxaStorageManager() {
}
MockFxaStorageManager.prototype = {
promiseInitialized: Promise.resolve(),
initialize(accountData) {
this.accountData = accountData;
},
finalize() {
return Promise.resolve();
},
getAccountData() {
return Promise.resolve(this.accountData);
},
updateAccountData(updatedFields) {
for (let [name, value] of Object.entries(updatedFields)) {
if (value == null) {
delete this.accountData[name];
} else {
this.accountData[name] = value;
}
}
return Promise.resolve();
},
deleteAccountData() {
this.accountData = null;
return Promise.resolve();
}
}
/**
* First wait >100ms (nsITimers can take up to that much time to fire, so
@ -96,18 +50,6 @@ this.waitForZeroTimer = function waitForZeroTimer(callback) {
CommonUtils.namedTimer(wait, 150, {}, "timer");
}
/**
* Return true if Sync is configured with the "legacy" identity provider.
*/
this.isConfiguredWithLegacyIdentity = function() {
let ns = {};
Cu.import("resource://services-sync/service.js", ns);
// We can't use instanceof as BrowserIDManager (the "other" identity) inherits
// from IdentityManager so that would return true - so check the prototype.
return Object.getPrototypeOf(ns.Service.identity) === IdentityManager.prototype;
}
/**
* Ensure Sync is configured with the "legacy" identity provider.
*/
@ -145,15 +87,14 @@ this.makeIdentityConfig = function(overrides) {
kA: 'kA',
kB: 'kB',
sessionToken: 'sessionToken',
uid: "a".repeat(32),
uid: 'user_uid',
verified: true,
},
token: {
endpoint: null,
endpoint: Svc.Prefs.get("tokenServerURI"),
duration: 300,
id: "id",
key: "key",
hashed_fxa_uid: "f".repeat(32), // used during telemetry validation
// uid will be set to the username.
}
},
@ -181,47 +122,27 @@ this.makeIdentityConfig = function(overrides) {
return result;
}
this.makeFxAccountsInternalMock = function(config) {
return {
newAccountState(credentials) {
// We only expect this to be called with null indicating the (mock)
// storage should be read.
if (credentials) {
throw new Error("Not expecting to have credentials passed");
}
let storageManager = new MockFxaStorageManager();
storageManager.initialize(config.fxaccount.user);
let accountState = new AccountState(storageManager);
return accountState;
},
_getAssertion(audience) {
return Promise.resolve("assertion");
},
};
};
// Configure an instance of an FxAccount identity provider with the specified
// config (or the default config if not specified).
this.configureFxAccountIdentity = function(authService,
config = makeIdentityConfig(),
fxaInternal = makeFxAccountsInternalMock(config)) {
config = makeIdentityConfig()) {
let MockInternal = {};
let fxa = new FxAccounts(MockInternal);
// until we get better test infrastructure for bid_identity, we set the
// signedin user's "email" to the username, simply as many tests rely on this.
config.fxaccount.user.email = config.username;
let fxa = new FxAccounts(fxaInternal);
let MockFxAccountsClient = function() {
FxAccountsClient.apply(this);
fxa.internal.currentAccountState.signedInUser = {
version: DATA_FORMAT_VERSION,
accountData: config.fxaccount.user
};
MockFxAccountsClient.prototype = {
__proto__: FxAccountsClient.prototype,
accountStatus() {
return Promise.resolve(true);
}
fxa.internal.currentAccountState.getCertificate = function(data, keyPair, mustBeValidUntil) {
this.cert = {
validUntil: fxa.internal.now() + CERT_LIFETIME,
cert: "certificate",
};
return Promise.resolve(this.cert.cert);
};
let mockFxAClient = new MockFxAccountsClient();
fxa.internal._fxAccountsClient = mockFxAClient;
let mockTSC = { // TokenServerClient
getTokenFromBrowserIDAssertion: function(uri, assertion, cb) {
@ -233,7 +154,7 @@ this.configureFxAccountIdentity = function(authService,
authService._tokenServerClient = mockTSC;
// Set the "account" of the browserId manager to be the "email" of the
// logged in user of the mockFXA service.
authService._signedInUser = config.fxaccount.user;
authService._signedInUser = fxa.internal.currentAccountState.signedInUser.accountData;
authService._account = config.fxaccount.user.email;
}
@ -320,7 +241,7 @@ this.add_identity_test = function(test, testFunction) {
let ns = {};
Cu.import("resource://services-sync/service.js", ns);
// one task for the "old" identity manager.
test.add_task(function* () {
test.add_task(function() {
note("sync");
let oldIdentity = Status._authManager;
ensureLegacyIdentityManager();
@ -328,7 +249,7 @@ this.add_identity_test = function(test, testFunction) {
Status.__authManager = ns.Service.identity = oldIdentity;
});
// another task for the FxAccounts identity manager.
test.add_task(function* () {
test.add_task(function() {
note("FxAccounts");
let oldIdentity = Status._authManager;
Status.__authManager = ns.Service.identity = new BrowserIDManager();
@ -336,15 +257,3 @@ this.add_identity_test = function(test, testFunction) {
Status.__authManager = ns.Service.identity = oldIdentity;
});
}
this.sumHistogram = function(name, options = {}) {
let histogram = options.key ? Services.telemetry.getKeyedHistogramById(name) :
Services.telemetry.getHistogramById(name);
let snapshot = histogram.snapshot(options.key);
let sum = -Infinity;
if (snapshot) {
sum = snapshot.sum;
}
histogram.clear();
return sum;
}

View file

@ -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();

View file

@ -1,301 +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 = ["SyncedTabs"];
const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/PlacesUtils.jsm", this);
Cu.import("resource://services-sync/main.js");
Cu.import("resource://gre/modules/Preferences.jsm");
// The Sync XPCOM service
XPCOMUtils.defineLazyGetter(this, "weaveXPCService", function() {
return Cc["@mozilla.org/weave/service;1"]
.getService(Ci.nsISupports)
.wrappedJSObject;
});
// from MDN...
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
// A topic we fire whenever we have new tabs available. This might be due
// to a request made by this module to refresh the tab list, or as the result
// of a regularly scheduled sync. The intent is that consumers just listen
// for this notification and update their UI in response.
const TOPIC_TABS_CHANGED = "services.sync.tabs.changed";
// The interval, in seconds, before which we consider the existing list
// of tabs "fresh enough" and don't force a new sync.
const TABS_FRESH_ENOUGH_INTERVAL = 30;
let log = Log.repository.getLogger("Sync.RemoteTabs");
// A new scope to do the logging thang...
(function() {
let level = Preferences.get("services.sync.log.logger.tabs");
if (level) {
let appender = new Log.DumpAppender();
log.level = appender.level = Log.Level[level] || Log.Level.Debug;
log.addAppender(appender);
}
})();
// A private singleton that does the work.
let SyncedTabsInternal = {
/* Make a "tab" record. Returns a promise */
_makeTab: Task.async(function* (client, tab, url, showRemoteIcons) {
let icon;
if (showRemoteIcons) {
icon = tab.icon;
}
if (!icon) {
try {
icon = (yield PlacesUtils.promiseFaviconLinkUrl(url)).spec;
} catch (ex) { /* no favicon avaiable */ }
}
if (!icon) {
icon = "";
}
return {
type: "tab",
title: tab.title || url,
url,
icon,
client: client.id,
lastUsed: tab.lastUsed,
};
}),
/* Make a "client" record. Returns a promise for consistency with _makeTab */
_makeClient: Task.async(function* (client) {
return {
id: client.id,
type: "client",
name: Weave.Service.clientsEngine.getClientName(client.id),
isMobile: Weave.Service.clientsEngine.isMobile(client.id),
lastModified: client.lastModified * 1000, // sec to ms
tabs: []
};
}),
_tabMatchesFilter(tab, filter) {
let reFilter = new RegExp(escapeRegExp(filter), "i");
return tab.url.match(reFilter) || tab.title.match(reFilter);
},
getTabClients: Task.async(function* (filter) {
log.info("Generating tab list with filter", filter);
let result = [];
// If Sync isn't ready, don't try and get anything.
if (!weaveXPCService.ready) {
log.debug("Sync isn't yet ready, so returning an empty tab list");
return result;
}
// A boolean that controls whether we should show the icon from the remote tab.
const showRemoteIcons = Preferences.get("services.sync.syncedTabs.showRemoteIcons", true);
let engine = Weave.Service.engineManager.get("tabs");
let seenURLs = new Set();
let parentIndex = 0;
let ntabs = 0;
for (let [guid, client] of Object.entries(engine.getAllClients())) {
if (!Weave.Service.clientsEngine.remoteClientExists(client.id)) {
continue;
}
let clientRepr = yield this._makeClient(client);
log.debug("Processing client", clientRepr);
for (let tab of client.tabs) {
let url = tab.urlHistory[0];
log.debug("remote tab", url);
// Note there are some issues with tracking "seen" tabs, including:
// * We really can't return the entire urlHistory record as we are
// only checking the first entry - others might be different.
// * We don't update the |lastUsed| timestamp to reflect the
// most-recently-seen time.
// In a followup we should consider simply dropping this |seenUrls|
// check and return duplicate records - it seems the user will be more
// confused by tabs not showing up on a device (because it was detected
// as a dupe so it only appears on a different device) than being
// confused by seeing the same tab on different clients.
if (!url || seenURLs.has(url)) {
continue;
}
let tabRepr = yield this._makeTab(client, tab, url, showRemoteIcons);
if (filter && !this._tabMatchesFilter(tabRepr, filter)) {
continue;
}
seenURLs.add(url);
clientRepr.tabs.push(tabRepr);
}
// We return all clients, even those without tabs - the consumer should
// filter it if they care.
ntabs += clientRepr.tabs.length;
result.push(clientRepr);
}
log.info(`Final tab list has ${result.length} clients with ${ntabs} tabs.`);
return result;
}),
syncTabs(force) {
if (!force) {
// Don't bother refetching tabs if we already did so recently
let lastFetch = Preferences.get("services.sync.lastTabFetch", 0);
let now = Math.floor(Date.now() / 1000);
if (now - lastFetch < TABS_FRESH_ENOUGH_INTERVAL) {
log.info("_refetchTabs was done recently, do not doing it again");
return Promise.resolve(false);
}
}
// If Sync isn't configured don't try and sync, else we will get reports
// of a login failure.
if (Weave.Status.checkSetup() == Weave.CLIENT_NOT_CONFIGURED) {
log.info("Sync client is not configured, so not attempting a tab sync");
return Promise.resolve(false);
}
// Ask Sync to just do the tabs engine if it can.
// Sync is currently synchronous, so do it after an event-loop spin to help
// keep the UI responsive.
return new Promise((resolve, reject) => {
Services.tm.currentThread.dispatch(() => {
try {
log.info("Doing a tab sync.");
Weave.Service.sync(["tabs"]);
resolve(true);
} catch (ex) {
log.error("Sync failed", ex);
reject(ex);
};
}, Ci.nsIThread.DISPATCH_NORMAL);
});
},
observe(subject, topic, data) {
log.trace(`observed topic=${topic}, data=${data}, subject=${subject}`);
switch (topic) {
case "weave:engine:sync:finish":
if (data != "tabs") {
return;
}
// The tabs engine just finished syncing
// Set our lastTabFetch pref here so it tracks both explicit sync calls
// and normally scheduled ones.
Preferences.set("services.sync.lastTabFetch", Math.floor(Date.now() / 1000));
Services.obs.notifyObservers(null, TOPIC_TABS_CHANGED, null);
break;
case "weave:service:start-over":
// start-over needs to notify so consumers find no tabs.
Preferences.reset("services.sync.lastTabFetch");
Services.obs.notifyObservers(null, TOPIC_TABS_CHANGED, null);
break;
case "nsPref:changed":
Services.obs.notifyObservers(null, TOPIC_TABS_CHANGED, null);
break;
default:
break;
}
},
// Returns true if Sync is configured to Sync tabs, false otherwise
get isConfiguredToSyncTabs() {
if (!weaveXPCService.ready) {
log.debug("Sync isn't yet ready; assuming tab engine is enabled");
return true;
}
let engine = Weave.Service.engineManager.get("tabs");
return engine && engine.enabled;
},
get hasSyncedThisSession() {
let engine = Weave.Service.engineManager.get("tabs");
return engine && engine.hasSyncedThisSession;
},
};
Services.obs.addObserver(SyncedTabsInternal, "weave:engine:sync:finish", false);
Services.obs.addObserver(SyncedTabsInternal, "weave:service:start-over", false);
// Observe the pref the indicates the state of the tabs engine has changed.
// This will force consumers to re-evaluate the state of sync and update
// accordingly.
Services.prefs.addObserver("services.sync.engine.tabs", SyncedTabsInternal, false);
// The public interface.
this.SyncedTabs = {
// A mock-point for tests.
_internal: SyncedTabsInternal,
// We make the topic for the observer notification public.
TOPIC_TABS_CHANGED,
// Returns true if Sync is configured to Sync tabs, false otherwise
get isConfiguredToSyncTabs() {
return this._internal.isConfiguredToSyncTabs;
},
// Returns true if a tab sync has completed once this session. If this
// returns false, then getting back no clients/tabs possibly just means we
// are waiting for that first sync to complete.
get hasSyncedThisSession() {
return this._internal.hasSyncedThisSession;
},
// Return a promise that resolves with an array of client records, each with
// a .tabs array. Note that part of the contract for this module is that the
// returned objects are not shared between invocations, so callers are free
// to mutate the returned objects (eg, sort, truncate) however they see fit.
getTabClients(query) {
return this._internal.getTabClients(query);
},
// Starts a background request to start syncing tabs. Returns a promise that
// resolves when the sync is complete, but there's no resolved value -
// callers should be listening for TOPIC_TABS_CHANGED.
// If |force| is true we always sync. If false, we only sync if the most
// recent sync wasn't "recently".
syncTabs(force) {
return this._internal.syncTabs(force);
},
sortTabClientsByLastUsed(clients, maxTabs = Infinity) {
// First sort and filter the list of tabs for each client. Note that
// this module promises that the objects it returns are never
// shared, so we are free to mutate those objects directly.
for (let client of clients) {
let tabs = client.tabs;
tabs.sort((a, b) => b.lastUsed - a.lastUsed);
if (Number.isFinite(maxTabs)) {
client.tabs = tabs.slice(0, maxTabs);
}
}
// Now sort the clients - the clients are sorted in the order of the
// most recent tab for that client (ie, it is important the tabs for
// each client are already sorted.)
clients.sort((a, b) => {
if (a.tabs.length == 0) {
return 1; // b comes first.
}
if (b.tabs.length == 0) {
return -1; // a comes first.
}
return b.tabs[0].lastUsed - a.tabs[0].lastUsed;
});
},
};

View file

@ -17,7 +17,7 @@
"use strict";
var Cu = Components.utils;
const Cu = Components.utils;
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/util.js");
@ -218,12 +218,11 @@ AddonsReconciler.prototype = {
}
this._addons = json.addons;
for (let id in this._addons) {
let record = this._addons[id];
for each (let record in this._addons) {
record.modified = new Date(record.modified);
}
for (let [time, change, id] of json.changes) {
for each (let [time, change, id] in json.changes) {
this._changes.push([new Date(time), change, id]);
}
@ -247,9 +246,9 @@ AddonsReconciler.prototype = {
let file = path || DEFAULT_STATE_FILE;
let state = {version: 1, addons: {}, changes: []};
for (let [id, record] of Object.entries(this._addons)) {
for (let [id, record] in Iterator(this._addons)) {
state.addons[id] = {};
for (let [k, v] of Object.entries(record)) {
for (let [k, v] in Iterator(record)) {
if (k == "modified") {
state.addons[id][k] = v.getTime();
}
@ -259,7 +258,7 @@ AddonsReconciler.prototype = {
}
}
for (let [time, change, id] of this._changes) {
for each (let [time, change, id] in this._changes) {
state.changes.push([time.getTime(), change, id]);
}
@ -351,14 +350,14 @@ AddonsReconciler.prototype = {
AddonManager.getAllAddons(function (addons) {
let ids = {};
for (let addon of addons) {
for each (let addon in addons) {
ids[addon.id] = true;
this.rectifyStateFromAddon(addon);
}
// Look for locally-defined add-ons that no longer exist and update their
// record.
for (let [id, addon] of Object.entries(this._addons)) {
for (let [id, addon] in Iterator(this._addons)) {
if (id in ids) {
continue;
}
@ -374,7 +373,7 @@ AddonsReconciler.prototype = {
}
let installFound = false;
for (let install of installs) {
for each (let install in installs) {
if (install.addon && install.addon.id == id &&
install.state == AddonManager.STATE_INSTALLED) {
@ -417,7 +416,7 @@ AddonsReconciler.prototype = {
* Addon instance being updated.
*/
rectifyStateFromAddon: function rectifyStateFromAddon(addon) {
this._log.debug(`Rectifying state for addon ${addon.name} (version=${addon.version}, id=${addon.id})`);
this._log.debug("Rectifying state for addon: " + addon.id);
this._ensureStateLoaded();
let id = addon.id;
@ -434,8 +433,7 @@ AddonsReconciler.prototype = {
modified: now,
type: addon.type,
scope: addon.scope,
foreignInstall: addon.foreignInstall,
isSyncable: addon.isSyncable,
foreignInstall: addon.foreignInstall
};
this._addons[id] = record;
this._log.debug("Adding change because add-on not present locally: " +
@ -445,7 +443,6 @@ AddonsReconciler.prototype = {
}
let record = this._addons[id];
record.isSyncable = addon.isSyncable;
if (!record.installed) {
// It is possible the record is marked as uninstalled because an
@ -486,11 +483,12 @@ AddonsReconciler.prototype = {
this._log.info("Change recorded for " + state.id);
this._changes.push([date, change, state.id]);
for (let listener of this._listeners) {
for each (let listener in this._listeners) {
try {
listener.changeListener.call(listener, date, change, state);
} catch (ex) {
this._log.warn("Exception calling change listener", ex);
this._log.warn("Exception calling change listener: " +
Utils.exceptionStr(ex));
}
}
},
@ -556,8 +554,7 @@ AddonsReconciler.prototype = {
* @return Object on success on null on failure.
*/
getAddonStateFromSyncGUID: function getAddonStateFromSyncGUID(guid) {
for (let id in this.addons) {
let addon = this.addons[id];
for each (let addon in this.addons) {
if (addon.guid == guid) {
return addon;
}
@ -636,7 +633,7 @@ AddonsReconciler.prototype = {
}
}
catch (ex) {
this._log.warn("Exception", ex);
this._log.warn("Exception: " + Utils.exceptionStr(ex));
}
},

View file

@ -6,7 +6,7 @@
this.EXPORTED_SYMBOLS = ["AddonUtils"];
var {interfaces: Ci, utils: Cu} = Components;
const {interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Log.jsm");
@ -38,10 +38,21 @@ AddonUtilsInternal.prototype = {
* Function to be called with result of operation.
*/
getInstallFromSearchResult:
function getInstallFromSearchResult(addon, cb) {
function getInstallFromSearchResult(addon, cb, requireSecureURI=true) {
this._log.debug("Obtaining install for " + addon.id);
// Verify that the source URI uses TLS. We don't allow installs from
// insecure sources for security reasons. The Addon Manager ensures that
// cert validation, etc is performed.
if (requireSecureURI) {
let scheme = addon.sourceURI.scheme;
if (scheme != "https") {
cb(new Error("Insecure source URI scheme: " + scheme), addon.install);
return;
}
}
// We should theoretically be able to obtain (and use) addon.install if
// it is available. However, the addon.sourceURI rewriting won't be
// reflected in the AddonInstall, so we can't use it. If we ever get rid
@ -69,6 +80,8 @@ AddonUtilsInternal.prototype = {
* syncGUID - Sync GUID to use for the new add-on.
* enabled - Boolean indicating whether the add-on should be enabled upon
* install.
* requireSecureURI - Boolean indicating whether to require a secure
* URI to install from. This defaults to true.
*
* When complete it calls a callback with 2 arguments, error and result.
*
@ -92,6 +105,10 @@ AddonUtilsInternal.prototype = {
function installAddonFromSearchResult(addon, options, cb) {
this._log.info("Trying to install add-on from search result: " + addon.id);
if (options.requireSecureURI === undefined) {
options.requireSecureURI = true;
}
this.getInstallFromSearchResult(addon, function onResult(error, install) {
if (error) {
cb(error, null);
@ -147,10 +164,10 @@ AddonUtilsInternal.prototype = {
install.install();
}
catch (ex) {
this._log.error("Error installing add-on", ex);
this._log.error("Error installing add-on: " + Utils.exceptionstr(ex));
cb(ex, null);
}
}.bind(this));
}.bind(this), options.requireSecureURI);
},
/**
@ -231,7 +248,7 @@ AddonUtilsInternal.prototype = {
}
let ids = [];
for (let addon of installs) {
for each (let addon in installs) {
ids.push(addon.id);
}
@ -244,7 +261,6 @@ AddonUtilsInternal.prototype = {
installedIDs: [],
installs: [],
addons: [],
skipped: [],
errors: []
};
@ -282,21 +298,15 @@ AddonUtilsInternal.prototype = {
// server-side metrics aren't skewed (bug 708134). The server should
// ideally send proper URLs, but this solution was deemed too
// complicated at the time the functionality was implemented.
for (let addon of addons) {
// Find the specified options for this addon.
let options;
for (let install of installs) {
if (install.id == addon.id) {
options = install;
break;
}
}
if (!this.canInstallAddon(addon, options)) {
ourResult.skipped.push(addon.id);
for each (let addon in addons) {
// sourceURI presence isn't enforced by AddonRepository. So, we skip
// add-ons without a sourceURI.
if (!addon.sourceURI) {
this._log.info("Skipping install of add-on because missing " +
"sourceURI: " + addon.id);
continue;
}
// We can go ahead and attempt to install it.
toInstall.push(addon);
// We should always be able to QI the nsIURI to nsIURL. If not, we
@ -332,9 +342,9 @@ AddonUtilsInternal.prototype = {
// Start all the installs asynchronously. They will report back to us
// as they finish, eventually triggering the global callback.
for (let addon of toInstall) {
for each (let addon in toInstall) {
let options = {};
for (let install of installs) {
for each (let install in installs) {
if (install.id == addon.id) {
options = install;
break;
@ -352,52 +362,10 @@ AddonUtilsInternal.prototype = {
});
},
/**
* Returns true if we are able to install the specified addon, false
* otherwise. It is expected that this will log the reason if it returns
* false.
*
* @param addon
* (Addon) Add-on instance to check.
* @param options
* (object) The options specified for this addon. See installAddons()
* for the valid elements.
*/
canInstallAddon(addon, options) {
// sourceURI presence isn't enforced by AddonRepository. So, we skip
// add-ons without a sourceURI.
if (!addon.sourceURI) {
this._log.info("Skipping install of add-on because missing " +
"sourceURI: " + addon.id);
return false;
}
// Verify that the source URI uses TLS. We don't allow installs from
// insecure sources for security reasons. The Addon Manager ensures
// that cert validation etc is performed.
// (We should also consider just dropping this entirely and calling
// XPIProvider.isInstallAllowed, but that has additional semantics we might
// need to think through...)
let requireSecureURI = true;
if (options && options.requireSecureURI !== undefined) {
requireSecureURI = options.requireSecureURI;
}
if (requireSecureURI) {
let scheme = addon.sourceURI.scheme;
if (scheme != "https") {
this._log.info(`Skipping install of add-on "${addon.id}" because sourceURI's scheme of "${scheme}" is not trusted`);
return false;
}
}
this._log.info(`Add-on "${addon.id}" is able to be installed`);
return true;
},
/**
* Update the user disabled flag for an add-on.
*
* The supplied callback will be called when the operation is
* The supplied callback will ba called when the operation is
* complete. If the new flag matches the existing or if the add-on
* isn't currently active, the function will fire the callback
* immediately. Else, the callback is invoked when the AddonManager

View file

@ -1,784 +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";
const Cu = Components.utils;
Cu.import("resource://gre/modules/PlacesUtils.jsm");
Cu.import("resource://gre/modules/PlacesSyncUtils.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
this.EXPORTED_SYMBOLS = ["BookmarkValidator", "BookmarkProblemData"];
const LEFT_PANE_ROOT_ANNO = "PlacesOrganizer/OrganizerFolder";
const LEFT_PANE_QUERY_ANNO = "PlacesOrganizer/OrganizerQuery";
// Indicates if a local bookmark tree node should be excluded from syncing.
function isNodeIgnored(treeNode) {
return treeNode.annos && treeNode.annos.some(anno => anno.name == LEFT_PANE_ROOT_ANNO ||
anno.name == LEFT_PANE_QUERY_ANNO);
}
const BOOKMARK_VALIDATOR_VERSION = 1;
/**
* Result of bookmark validation. Contains the following fields which describe
* server-side problems unless otherwise specified.
*
* - missingIDs (number): # of objects with missing ids
* - duplicates (array of ids): ids seen more than once
* - parentChildMismatches (array of {parent: parentid, child: childid}):
* instances where the child's parentid and the parent's children array
* do not match
* - cycles (array of array of ids). List of cycles found in the server-side tree.
* - clientCycles (array of array of ids). List of cycles found in the client-side tree.
* - orphans (array of {id: string, parent: string}): List of nodes with
* either no parentid, or where the parent could not be found.
* - missingChildren (array of {parent: id, child: id}):
* List of parent/children where the child id couldn't be found
* - deletedChildren (array of { parent: id, child: id }):
* List of parent/children where child id was a deleted item (but still showed up
* in the children array)
* - multipleParents (array of {child: id, parents: array of ids}):
* List of children that were part of multiple parent arrays
* - deletedParents (array of ids) : List of records that aren't deleted but
* had deleted parents
* - childrenOnNonFolder (array of ids): list of non-folders that still have
* children arrays
* - duplicateChildren (array of ids): list of records who have the same
* child listed multiple times in their children array
* - parentNotFolder (array of ids): list of records that have parents that
* aren't folders
* - rootOnServer (boolean): true if the root came from the server
* - badClientRoots (array of ids): Contains any client-side root ids where
* the root is missing or isn't a (direct) child of the places root.
*
* - clientMissing: Array of ids on the server missing from the client
* - serverMissing: Array of ids on the client missing from the server
* - serverDeleted: Array of ids on the client that the server had marked as deleted.
* - serverUnexpected: Array of ids that appear on the server but shouldn't
* because the client attempts to never upload them.
* - differences: Array of {id: string, differences: string array} recording
* the non-structural properties that are differente between the client and server
* - structuralDifferences: As above, but contains the items where the differences were
* structural, that is, they contained childGUIDs or parentid
*/
class BookmarkProblemData {
constructor() {
this.rootOnServer = false;
this.missingIDs = 0;
this.duplicates = [];
this.parentChildMismatches = [];
this.cycles = [];
this.clientCycles = [];
this.orphans = [];
this.missingChildren = [];
this.deletedChildren = [];
this.multipleParents = [];
this.deletedParents = [];
this.childrenOnNonFolder = [];
this.duplicateChildren = [];
this.parentNotFolder = [];
this.badClientRoots = [];
this.clientMissing = [];
this.serverMissing = [];
this.serverDeleted = [];
this.serverUnexpected = [];
this.differences = [];
this.structuralDifferences = [];
}
/**
* Convert ("difference", [{ differences: ["tags", "name"] }, { differences: ["name"] }]) into
* [{ name: "difference:tags", count: 1}, { name: "difference:name", count: 2 }], etc.
*/
_summarizeDifferences(prefix, diffs) {
let diffCounts = new Map();
for (let { differences } of diffs) {
for (let type of differences) {
let name = prefix + ":" + type;
let count = diffCounts.get(name) || 0;
diffCounts.set(name, count + 1);
}
}
return [...diffCounts].map(([name, count]) => ({ name, count }));
}
/**
* Produce a list summarizing problems found. Each entry contains {name, count},
* where name is the field name for the problem, and count is the number of times
* the problem was encountered.
*
* Validation has failed if all counts are not 0.
*
* If the `full` argument is truthy, we also include information about which
* properties we saw structural differences in. Currently, this means either
* "sdiff:parentid" and "sdiff:childGUIDS" may be present.
*/
getSummary(full) {
let result = [
{ name: "clientMissing", count: this.clientMissing.length },
{ name: "serverMissing", count: this.serverMissing.length },
{ name: "serverDeleted", count: this.serverDeleted.length },
{ name: "serverUnexpected", count: this.serverUnexpected.length },
{ name: "structuralDifferences", count: this.structuralDifferences.length },
{ name: "differences", count: this.differences.length },
{ name: "missingIDs", count: this.missingIDs },
{ name: "rootOnServer", count: this.rootOnServer ? 1 : 0 },
{ name: "duplicates", count: this.duplicates.length },
{ name: "parentChildMismatches", count: this.parentChildMismatches.length },
{ name: "cycles", count: this.cycles.length },
{ name: "clientCycles", count: this.clientCycles.length },
{ name: "badClientRoots", count: this.badClientRoots.length },
{ name: "orphans", count: this.orphans.length },
{ name: "missingChildren", count: this.missingChildren.length },
{ name: "deletedChildren", count: this.deletedChildren.length },
{ name: "multipleParents", count: this.multipleParents.length },
{ name: "deletedParents", count: this.deletedParents.length },
{ name: "childrenOnNonFolder", count: this.childrenOnNonFolder.length },
{ name: "duplicateChildren", count: this.duplicateChildren.length },
{ name: "parentNotFolder", count: this.parentNotFolder.length },
];
if (full) {
let structural = this._summarizeDifferences("sdiff", this.structuralDifferences);
result.push.apply(result, structural);
}
return result;
}
}
// Defined lazily to avoid initializing PlacesUtils.bookmarks too soon.
XPCOMUtils.defineLazyGetter(this, "SYNCED_ROOTS", () => [
PlacesUtils.bookmarks.menuGuid,
PlacesUtils.bookmarks.toolbarGuid,
PlacesUtils.bookmarks.unfiledGuid,
PlacesUtils.bookmarks.mobileGuid,
]);
class BookmarkValidator {
_followQueries(recordMap) {
for (let [guid, entry] of recordMap) {
if (entry.type !== "query" && (!entry.bmkUri || !entry.bmkUri.startsWith("place:"))) {
continue;
}
// Might be worth trying to parse the place: query instead so that this
// works "automatically" with things like aboutsync.
let queryNodeParent = PlacesUtils.getFolderContents(entry, false, true);
if (!queryNodeParent || !queryNodeParent.root.hasChildren) {
continue;
}
queryNodeParent = queryNodeParent.root;
let queryNode = null;
let numSiblings = 0;
let containerWasOpen = queryNodeParent.containerOpen;
queryNodeParent.containerOpen = true;
try {
try {
numSiblings = queryNodeParent.childCount;
} catch (e) {
// This throws when we can't actually get the children. This is the
// case for history containers, tag queries, ...
continue;
}
for (let i = 0; i < numSiblings && !queryNode; ++i) {
let child = queryNodeParent.getChild(i);
if (child && child.bookmarkGuid && child.bookmarkGuid === guid) {
queryNode = child;
}
}
} finally {
queryNodeParent.containerOpen = containerWasOpen;
}
if (!queryNode) {
continue;
}
let concreteId = PlacesUtils.getConcreteItemGuid(queryNode);
if (!concreteId) {
continue;
}
let concreteItem = recordMap.get(concreteId);
if (!concreteItem) {
continue;
}
entry.concrete = concreteItem;
}
}
createClientRecordsFromTree(clientTree) {
// Iterate over the treeNode, converting it to something more similar to what
// the server stores.
let records = [];
let recordsByGuid = new Map();
let syncedRoots = SYNCED_ROOTS;
function traverse(treeNode, synced) {
if (!synced) {
synced = syncedRoots.includes(treeNode.guid);
} else if (isNodeIgnored(treeNode)) {
synced = false;
}
let guid = PlacesSyncUtils.bookmarks.guidToSyncId(treeNode.guid);
let itemType = 'item';
treeNode.ignored = !synced;
treeNode.id = guid;
switch (treeNode.type) {
case PlacesUtils.TYPE_X_MOZ_PLACE:
let query = null;
if (treeNode.annos && treeNode.uri.startsWith("place:")) {
query = treeNode.annos.find(({name}) =>
name === PlacesSyncUtils.bookmarks.SMART_BOOKMARKS_ANNO);
}
if (query && query.value) {
itemType = 'query';
} else {
itemType = 'bookmark';
}
break;
case PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER:
let isLivemark = false;
if (treeNode.annos) {
for (let anno of treeNode.annos) {
if (anno.name === PlacesUtils.LMANNO_FEEDURI) {
isLivemark = true;
treeNode.feedUri = anno.value;
} else if (anno.name === PlacesUtils.LMANNO_SITEURI) {
isLivemark = true;
treeNode.siteUri = anno.value;
}
}
}
itemType = isLivemark ? "livemark" : "folder";
break;
case PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR:
itemType = 'separator';
break;
}
if (treeNode.tags) {
treeNode.tags = treeNode.tags.split(",");
} else {
treeNode.tags = [];
}
treeNode.type = itemType;
treeNode.pos = treeNode.index;
treeNode.bmkUri = treeNode.uri;
records.push(treeNode);
// We want to use the "real" guid here.
recordsByGuid.set(treeNode.guid, treeNode);
if (treeNode.type === 'folder') {
treeNode.childGUIDs = [];
if (!treeNode.children) {
treeNode.children = [];
}
for (let child of treeNode.children) {
traverse(child, synced);
child.parent = treeNode;
child.parentid = guid;
treeNode.childGUIDs.push(child.guid);
}
}
}
traverse(clientTree, false);
clientTree.id = 'places';
this._followQueries(recordsByGuid);
return records;
}
/**
* Process the server-side list. Mainly this builds the records into a tree,
* but it also records information about problems, and produces arrays of the
* deleted and non-deleted nodes.
*
* Returns an object containing:
* - records:Array of non-deleted records. Each record contains the following
* properties
* - childGUIDs (array of strings, only present if type is 'folder'): the
* list of child GUIDs stored on the server.
* - children (array of records, only present if type is 'folder'):
* each record has these same properties. This may differ in content
* from what you may expect from the childGUIDs list, as it won't
* contain any records that could not be found.
* - parent (record): The parent to this record.
* - Unchanged properties send down from the server: id, title, type,
* parentName, parentid, bmkURI, keyword, tags, pos, queryId, loadInSidebar
* - root: Root of the server-side bookmark tree. Has the same properties as
* above.
* - deletedRecords: As above, but only contains items that the server sent
* where it also sent indication that the item should be deleted.
* - problemData: a BookmarkProblemData object, with the caveat that
* the fields describing client/server relationship will not have been filled
* out yet.
*/
inspectServerRecords(serverRecords) {
let deletedItemIds = new Set();
let idToRecord = new Map();
let deletedRecords = [];
let folders = [];
let problems = [];
let problemData = new BookmarkProblemData();
let resultRecords = [];
for (let record of serverRecords) {
if (!record.id) {
++problemData.missingIDs;
continue;
}
if (record.deleted) {
deletedItemIds.add(record.id);
} else {
if (idToRecord.has(record.id)) {
problemData.duplicates.push(record.id);
continue;
}
}
idToRecord.set(record.id, record);
if (record.children) {
if (record.type !== "folder") {
// Due to implementation details in engines/bookmarks.js, (Livemark
// subclassing BookmarkFolder) Livemarks will have a children array,
// but it should still be empty.
if (!record.children.length) {
continue;
}
// Otherwise we mark it as an error and still try to resolve the children
problemData.childrenOnNonFolder.push(record.id);
}
folders.push(record);
if (new Set(record.children).size !== record.children.length) {
problemData.duplicateChildren.push(record.id)
}
// The children array stores special guids as their local guid values,
// e.g. 'menu________' instead of 'menu', but all other parts of the
// serverside bookmark info stores it as the special value ('menu').
record.childGUIDs = record.children;
record.children = record.children.map(childID => {
return PlacesSyncUtils.bookmarks.guidToSyncId(childID);
});
}
}
for (let deletedId of deletedItemIds) {
let record = idToRecord.get(deletedId);
if (record && !record.isDeleted) {
deletedRecords.push(record);
record.isDeleted = true;
}
}
let root = idToRecord.get('places');
if (!root) {
// Fabricate a root. We want to remember that it's fake so that we can
// avoid complaining about stuff like it missing it's childGUIDs later.
root = { id: 'places', children: [], type: 'folder', title: '', fake: true };
resultRecords.push(root);
idToRecord.set('places', root);
} else {
problemData.rootOnServer = true;
}
// Build the tree, find orphans, and record most problems having to do with
// the tree structure.
for (let [id, record] of idToRecord) {
if (record === root) {
continue;
}
if (record.isDeleted) {
continue;
}
let parentID = record.parentid;
if (!parentID) {
problemData.orphans.push({id: record.id, parent: parentID});
continue;
}
let parent = idToRecord.get(parentID);
if (!parent) {
problemData.orphans.push({id: record.id, parent: parentID});
continue;
}
if (parent.type !== 'folder') {
problemData.parentNotFolder.push(record.id);
if (!parent.children) {
parent.children = [];
}
if (!parent.childGUIDs) {
parent.childGUIDs = [];
}
}
if (!record.isDeleted) {
resultRecords.push(record);
}
record.parent = parent;
if (parent !== root || problemData.rootOnServer) {
let childIndex = parent.children.indexOf(id);
if (childIndex < 0) {
problemData.parentChildMismatches.push({parent: parent.id, child: record.id});
} else {
parent.children[childIndex] = record;
}
} else {
parent.children.push(record);
}
if (parent.isDeleted && !record.isDeleted) {
problemData.deletedParents.push(record.id);
}
// We used to check if the parentName on the server matches the actual
// local parent name, but given this is used only for de-duping a record
// the first time it is seen and expensive to keep up-to-date, we decided
// to just stop recording it. See bug 1276969 for more.
}
// Check that we aren't missing any children.
for (let folder of folders) {
folder.unfilteredChildren = folder.children;
folder.children = [];
for (let ci = 0; ci < folder.unfilteredChildren.length; ++ci) {
let child = folder.unfilteredChildren[ci];
let childObject;
if (typeof child == "string") {
// This can happen the parent refers to a child that has a different
// parentid, or if it refers to a missing or deleted child. It shouldn't
// be possible with totally valid bookmarks.
childObject = idToRecord.get(child);
if (!childObject) {
problemData.missingChildren.push({parent: folder.id, child});
} else {
folder.unfilteredChildren[ci] = childObject;
if (childObject.isDeleted) {
problemData.deletedChildren.push({ parent: folder.id, child });
}
}
} else {
childObject = child;
}
if (!childObject) {
continue;
}
if (childObject.parentid === folder.id) {
folder.children.push(childObject);
continue;
}
// The child is very probably in multiple `children` arrays --
// see if we already have a problem record about it.
let currentProblemRecord = problemData.multipleParents.find(pr =>
pr.child === child);
if (currentProblemRecord) {
currentProblemRecord.parents.push(folder.id);
continue;
}
let otherParent = idToRecord.get(childObject.parentid);
// it's really an ... orphan ... sort of.
if (!otherParent) {
// if we never end up adding to this parent's list, we filter it out after this loop.
problemData.multipleParents.push({
child,
parents: [folder.id]
});
if (!problemData.orphans.some(r => r.id === child)) {
problemData.orphans.push({
id: child,
parent: childObject.parentid
});
}
continue;
}
if (otherParent.isDeleted) {
if (!problemData.deletedParents.includes(child)) {
problemData.deletedParents.push(child);
}
continue;
}
if (otherParent.childGUIDs && !otherParent.childGUIDs.includes(child)) {
if (!problemData.parentChildMismatches.some(r => r.child === child)) {
// Might not be possible to get here.
problemData.parentChildMismatches.push({ child, parent: folder.id });
}
}
problemData.multipleParents.push({
child,
parents: [childObject.parentid, folder.id]
});
}
}
problemData.multipleParents = problemData.multipleParents.filter(record =>
record.parents.length >= 2);
problemData.cycles = this._detectCycles(resultRecords);
return {
deletedRecords,
records: resultRecords,
problemData,
root,
};
}
// helper for inspectServerRecords
_detectCycles(records) {
// currentPath and pathLookup contain the same data. pathLookup is faster to
// query, but currentPath gives is the order of traversal that we need in
// order to report the members of the cycles.
let pathLookup = new Set();
let currentPath = [];
let cycles = [];
let seenEver = new Set();
const traverse = node => {
if (pathLookup.has(node)) {
let cycleStart = currentPath.lastIndexOf(node);
let cyclePath = currentPath.slice(cycleStart).map(n => n.id);
cycles.push(cyclePath);
return;
} else if (seenEver.has(node)) {
// If we're checking the server, this is a problem, but it should already be reported.
// On the client, this could happen due to including `node.concrete` in the child list.
return;
}
seenEver.add(node);
let children = node.children || [];
if (node.concrete) {
children.push(node.concrete);
}
if (children) {
pathLookup.add(node);
currentPath.push(node);
for (let child of children) {
traverse(child);
}
currentPath.pop();
pathLookup.delete(node);
}
};
for (let record of records) {
if (!seenEver.has(record)) {
traverse(record);
}
}
return cycles;
}
// Perform client-side sanity checking that doesn't involve server data
_validateClient(problemData, clientRecords) {
problemData.clientCycles = this._detectCycles(clientRecords);
for (let rootGUID of SYNCED_ROOTS) {
let record = clientRecords.find(record =>
record.guid === rootGUID);
if (!record || record.parentid !== "places") {
problemData.badClientRoots.push(rootGUID);
}
}
}
/**
* Compare the list of server records with the client tree.
*
* Returns the same data as described in the inspectServerRecords comment,
* with the following additional fields.
* - clientRecords: an array of client records in a similar format to
* the .records (ie, server records) entry.
* - problemData is the same as for inspectServerRecords, except all properties
* will be filled out.
*/
compareServerWithClient(serverRecords, clientTree) {
let clientRecords = this.createClientRecordsFromTree(clientTree);
let inspectionInfo = this.inspectServerRecords(serverRecords);
inspectionInfo.clientRecords = clientRecords;
// Mainly do this to remove deleted items and normalize child guids.
serverRecords = inspectionInfo.records;
let problemData = inspectionInfo.problemData;
this._validateClient(problemData, clientRecords);
let matches = [];
let allRecords = new Map();
let serverDeletedLookup = new Set(inspectionInfo.deletedRecords.map(r => r.id));
for (let sr of serverRecords) {
if (sr.fake) {
continue;
}
allRecords.set(sr.id, {client: null, server: sr});
}
for (let cr of clientRecords) {
let unified = allRecords.get(cr.id);
if (!unified) {
allRecords.set(cr.id, {client: cr, server: null});
} else {
unified.client = cr;
}
}
for (let [id, {client, server}] of allRecords) {
if (!client && server) {
problemData.clientMissing.push(id);
continue;
}
if (!server && client) {
if (serverDeletedLookup.has(id)) {
problemData.serverDeleted.push(id);
} else if (!client.ignored && client.id != "places") {
problemData.serverMissing.push(id);
}
continue;
}
if (server && client && client.ignored) {
problemData.serverUnexpected.push(id);
}
let differences = [];
let structuralDifferences = [];
// Don't bother comparing titles of roots. It's okay if locally it's
// "Mobile Bookmarks", but the server thinks it's "mobile".
// TODO: We probably should be handing other localized bookmarks (e.g.
// default bookmarks) here as well, see bug 1316041.
if (!SYNCED_ROOTS.includes(client.guid)) {
// We want to treat undefined, null and an empty string as identical
if ((client.title || "") !== (server.title || "")) {
differences.push("title");
}
}
if (client.parentid || server.parentid) {
if (client.parentid !== server.parentid) {
structuralDifferences.push('parentid');
}
}
if (client.tags || server.tags) {
let cl = client.tags || [];
let sl = server.tags || [];
if (cl.length !== sl.length || !cl.every((tag, i) => sl.indexOf(tag) >= 0)) {
differences.push('tags');
}
}
let sameType = client.type === server.type;
if (!sameType) {
if (server.type === "query" && client.type === "bookmark" && client.bmkUri.startsWith("place:")) {
sameType = true;
}
}
if (!sameType) {
differences.push('type');
} else {
switch (server.type) {
case 'bookmark':
case 'query':
if (server.bmkUri !== client.bmkUri) {
differences.push('bmkUri');
}
break;
case "livemark":
if (server.feedUri != client.feedUri) {
differences.push("feedUri");
}
if (server.siteUri != client.siteUri) {
differences.push("siteUri");
}
break;
case 'folder':
if (server.id === 'places' && !problemData.rootOnServer) {
// It's the fabricated places root. It won't have the GUIDs, but
// it doesn't matter.
break;
}
if (client.childGUIDs || server.childGUIDs) {
let cl = client.childGUIDs || [];
let sl = server.childGUIDs || [];
if (cl.length !== sl.length || !cl.every((id, i) => sl[i] === id)) {
structuralDifferences.push('childGUIDs');
}
}
break;
}
}
if (differences.length) {
problemData.differences.push({id, differences});
}
if (structuralDifferences.length) {
problemData.structuralDifferences.push({ id, differences: structuralDifferences });
}
}
return inspectionInfo;
}
_getServerState(engine) {
let collection = engine.itemSource();
let collectionKey = engine.service.collectionKeys.keyForCollection(engine.name);
collection.full = true;
let items = [];
collection.recordHandler = function(item) {
item.decrypt(collectionKey);
items.push(item.cleartext);
};
let resp = collection.getBatched();
if (!resp.success) {
throw resp;
}
return items;
}
validate(engine) {
let self = this;
return Task.spawn(function*() {
let start = Date.now();
let clientTree = yield PlacesUtils.promiseBookmarksTree("", {
includeItemIds: true
});
let serverState = self._getServerState(engine);
let serverRecordCount = serverState.length;
let result = self.compareServerWithClient(serverState, clientTree);
let end = Date.now();
let duration = end-start;
return {
duration,
version: self.version,
problems: result.problemData,
recordCount: serverRecordCount
};
});
}
};
BookmarkValidator.prototype.version = BOOKMARK_VALIDATOR_VERSION;

View file

@ -4,9 +4,9 @@
"use strict";
this.EXPORTED_SYMBOLS = ["BrowserIDManager", "AuthenticationError"];
this.EXPORTED_SYMBOLS = ["BrowserIDManager"];
var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-common/async.js");
@ -39,13 +39,12 @@ XPCOMUtils.defineLazyGetter(this, 'log', function() {
});
// FxAccountsCommon.js doesn't use a "namespace", so create one here.
var fxAccountsCommon = {};
let fxAccountsCommon = {};
Cu.import("resource://gre/modules/FxAccountsCommon.js", fxAccountsCommon);
const OBSERVER_TOPICS = [
fxAccountsCommon.ONLOGIN_NOTIFICATION,
fxAccountsCommon.ONLOGOUT_NOTIFICATION,
fxAccountsCommon.ON_ACCOUNT_STATE_CHANGE_NOTIFICATION,
];
const PREF_SYNC_SHOW_CUSTOMIZATION = "services.sync-setup.ui.showCustomizationDialog";
@ -66,9 +65,8 @@ function deriveKeyBundle(kB) {
some other error object (which should do the right thing when toString() is
called on it)
*/
function AuthenticationError(details, source) {
function AuthenticationError(details) {
this.details = details;
this.source = source;
}
AuthenticationError.prototype = {
@ -106,6 +104,12 @@ this.BrowserIDManager.prototype = {
// 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);
@ -114,34 +118,11 @@ this.BrowserIDManager.prototype = {
}
},
hashedUID() {
if (!this._token) {
throw new Error("hashedUID: Don't have token");
}
return this._token.hashed_fxa_uid
},
deviceID() {
return this._signedInUser && this._signedInUser.deviceId;
},
initialize: function() {
for (let topic of OBSERVER_TOPICS) {
Services.obs.addObserver(this, topic, false);
}
// and a background fetch of account data just so we can set this.account,
// so we have a username available before we've actually done a login.
// XXX - this is actually a hack just for tests and really shouldn't be
// necessary. Also, you'd think it would be safe to allow this.account to
// be set to null when there's no user logged in, but argue with the test
// suite, not with me :)
this._fxaService.getSignedInUser().then(accountData => {
if (accountData) {
this.account = accountData.email;
}
}).catch(err => {
// As above, this is only for tests so it is safe to ignore.
});
return this.initializeWithCurrentIdentity();
},
/**
@ -149,7 +130,7 @@ this.BrowserIDManager.prototype = {
* the user is logged in, or is rejected if the login attempt has failed.
*/
ensureLoggedIn: function() {
if (!this._shouldHaveSyncKeyBundle && this.whenReadyToAuthenticate) {
if (!this._shouldHaveSyncKeyBundle) {
// We are already in the process of logging in.
return this.whenReadyToAuthenticate.promise;
}
@ -163,7 +144,7 @@ this.BrowserIDManager.prototype = {
// 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(new Error("User needs to re-authenticate"));
return Promise.reject();
}
// So - we've a previous auth problem and aren't currently attempting to
@ -179,6 +160,7 @@ this.BrowserIDManager.prototype = {
}
this.resetCredentials();
this._signedInUser = null;
return Promise.resolve();
},
offerSyncOptions: function () {
@ -202,7 +184,7 @@ this.BrowserIDManager.prototype = {
// Reset the world before we do anything async.
this.whenReadyToAuthenticate = Promise.defer();
this.whenReadyToAuthenticate.promise.catch(err => {
this.whenReadyToAuthenticate.promise.then(null, (err) => {
this._log.error("Could not authenticate", err);
});
@ -258,14 +240,14 @@ this.BrowserIDManager.prototype = {
Services.obs.notifyObservers(null, "weave:service:setup-complete", null);
Weave.Utils.nextTick(Weave.Service.sync, Weave.Service);
}
}).catch(authErr => {
// report what failed...
this._log.error("Background fetch for key bundle failed", authErr);
}).then(null, err => {
this._shouldHaveSyncKeyBundle = true; // but we probably don't have one...
this.whenReadyToAuthenticate.reject(authErr);
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...
}).catch(err => {
}).then(null, err => {
this._log.error("Processing logged in account", err);
});
},
@ -301,8 +283,7 @@ this.BrowserIDManager.prototype = {
// 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 (indirectly, via _updateSignedInUser()), so just go ahead
// and do the init.
// aren't met, so just go ahead and do the init.
this.initializeWithCurrentIdentity(true);
break;
@ -311,13 +292,6 @@ this.BrowserIDManager.prototype = {
// startOver will cause this instance to be thrown away, so there's
// nothing else to do.
break;
case fxAccountsCommon.ON_ACCOUNT_STATE_CHANGE_NOTIFICATION:
// throw away token and fetch a new one
this.resetCredentials();
this._ensureValidToken().catch(err =>
this._log.error("Error while fetching a new token", err));
break;
}
},
@ -413,9 +387,6 @@ this.BrowserIDManager.prototype = {
resetCredentials: function() {
this.resetSyncKey();
this._token = null;
// The cluster URL comes from the token, so resetting it to empty will
// force Sync to not accidentally use a value from an earlier token.
Weave.Service.clusterURL = null;
},
/**
@ -503,12 +474,7 @@ this.BrowserIDManager.prototype = {
// 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;
if (this._canFetchKeys()) {
result = STATUS_OK;
} else {
result = LOGIN_FAILED_LOGIN_REJECTED;
}
let result = this._canFetchKeys() ? STATUS_OK : LOGIN_FAILED_LOGIN_REJECTED;
log.debug("unlockAndVerifyAuthState re-fetched credentials and is returning", result);
return result;
}
@ -540,27 +506,14 @@ this.BrowserIDManager.prototype = {
return true;
},
// Get our tokenServerURL - a private helper. Returns a string.
get _tokenServerUrl() {
// We used to support services.sync.tokenServerURI but this was a
// pain-point for people using non-default servers as Sync may auto-reset
// all services.sync prefs. So if that still exists, it wins.
let url = Svc.Prefs.get("tokenServerURI"); // Svc.Prefs "root" is services.sync
if (!url) {
url = Services.prefs.getCharPref("identity.sync.tokenserver.uri");
}
while (url.endsWith("/")) { // trailing slashes cause problems...
url = url.slice(0, -1);
}
return url;
},
// 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() {
// tokenServerURI is mis-named - convention is uri means nsISomething...
let tokenServerURI = this._tokenServerUrl;
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;
@ -589,7 +542,7 @@ this.BrowserIDManager.prototype = {
);
}
let getToken = assertion => {
let getToken = (tokenServerURI, assertion) => {
log.debug("Getting a token");
let deferred = Promise.defer();
let cb = function (err, token) {
@ -617,18 +570,7 @@ this.BrowserIDManager.prototype = {
return fxa.whenVerified(this._signedInUser)
.then(() => maybeFetchKeys())
.then(() => getAssertion())
.then(assertion => getToken(assertion))
.catch(err => {
// If we get a 401 fetching the token it may be that our certificate
// needs to be regenerated.
if (!err.response || err.response.status !== 401) {
return Promise.reject(err);
}
log.warn("Token server returned 401, refreshing certificate and retrying token fetch");
return fxa.invalidateCertificate()
.then(() => getAssertion())
.then(assertion => getToken(assertion))
})
.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
@ -640,18 +582,15 @@ this.BrowserIDManager.prototype = {
}
return token;
})
.catch(err => {
.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, "tokenserver");
err = new AuthenticationError(err);
// A hawkclient error.
} else if (err.code && err.code === 401) {
err = new AuthenticationError(err, "hawkclient");
// An FxAccounts.jsm error.
} else if (err.message == fxAccountsCommon.ERROR_AUTH_ERROR) {
err = new AuthenticationError(err, "fxaccounts");
err = new AuthenticationError(err);
}
// TODO: write tests to make sure that different auth error cases are handled here
@ -673,6 +612,7 @@ this.BrowserIDManager.prototype = {
// 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;
});
},
@ -684,19 +624,12 @@ this.BrowserIDManager.prototype = {
this._log.debug("_ensureValidToken already has one");
return Promise.resolve();
}
const notifyStateChanged =
() => Services.obs.notifyObservers(null, "weave:service:login:change", null);
// 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;
notifyStateChanged();
},
error => {
notifyStateChanged();
throw error
}
);
},
@ -719,16 +652,9 @@ this.BrowserIDManager.prototype = {
_getAuthenticationHeader: function(httpObject, method) {
let cb = Async.makeSpinningCallback();
this._ensureValidToken().then(cb, cb);
// Note that in failure states we return null, causing the request to be
// made without authorization headers, thereby presumably causing a 401,
// which causes Sync to log out. If we throw, this may not happen as
// expected.
try {
cb.wait();
} catch (ex) {
if (Async.isShutdownException(ex)) {
throw ex;
}
this._log.error("Failed to fetch a token for authentication", ex);
return null;
}
@ -764,17 +690,8 @@ this.BrowserIDManager.prototype = {
createClusterManager: function(service) {
return new BrowserIDClusterManager(service);
},
}
// Tell Sync what the login status should be if it saw a 401 fetching
// info/collections as part of login verification (typically immediately
// after login.)
// In our case, it almost certainly means a transient error fetching a token
// (and hitting this will cause us to logout, which will correctly handle an
// authoritative login issue.)
loginStatusFromVerification404() {
return LOGIN_FAILED_NETWORK_ERROR;
},
};
/* An implementation of the ClusterManager for this identity
@ -820,7 +737,7 @@ BrowserIDClusterManager.prototype = {
// 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 has a pre-existing clusterURL, so discarding the current token");
log.debug("_findCluster found existing clusterURL, so discarding the current token");
this.identity._token = null;
}
return this.identity._ensureValidToken();

View file

@ -1,204 +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";
const Cu = Components.utils;
Cu.import("resource://services-sync/record.js");
Cu.import("resource://services-sync/main.js");
this.EXPORTED_SYMBOLS = ["CollectionValidator", "CollectionProblemData"];
class CollectionProblemData {
constructor() {
this.missingIDs = 0;
this.duplicates = [];
this.clientMissing = [];
this.serverMissing = [];
this.serverDeleted = [];
this.serverUnexpected = [];
this.differences = [];
}
/**
* Produce a list summarizing problems found. Each entry contains {name, count},
* where name is the field name for the problem, and count is the number of times
* the problem was encountered.
*
* Validation has failed if all counts are not 0.
*/
getSummary() {
return [
{ name: "clientMissing", count: this.clientMissing.length },
{ name: "serverMissing", count: this.serverMissing.length },
{ name: "serverDeleted", count: this.serverDeleted.length },
{ name: "serverUnexpected", count: this.serverUnexpected.length },
{ name: "differences", count: this.differences.length },
{ name: "missingIDs", count: this.missingIDs },
{ name: "duplicates", count: this.duplicates.length }
];
}
}
class CollectionValidator {
// Construct a generic collection validator. This is intended to be called by
// subclasses.
// - name: Name of the engine
// - idProp: Property that identifies a record. That is, if a client and server
// record have the same value for the idProp property, they should be
// compared against eachother.
// - props: Array of properties that should be compared
constructor(name, idProp, props) {
this.name = name;
this.props = props;
this.idProp = idProp;
}
// Should a custom ProblemData type be needed, return it here.
emptyProblemData() {
return new CollectionProblemData();
}
getServerItems(engine) {
let collection = engine.itemSource();
let collectionKey = engine.service.collectionKeys.keyForCollection(engine.name);
collection.full = true;
let items = [];
collection.recordHandler = function(item) {
item.decrypt(collectionKey);
items.push(item.cleartext);
};
let resp = collection.getBatched();
if (!resp.success) {
throw resp;
}
return items;
}
// Should return a promise that resolves to an array of client items.
getClientItems() {
return Promise.reject("Must implement");
}
// Turn the client item into something that can be compared with the server item,
// and is also safe to mutate.
normalizeClientItem(item) {
return Cu.cloneInto(item, {});
}
// Turn the server item into something that can be easily compared with the client
// items.
normalizeServerItem(item) {
return item;
}
// Return whether or not a server item should be present on the client. Expected
// to be overridden.
clientUnderstands(item) {
return true;
}
// Return whether or not a client item should be present on the server. Expected
// to be overridden
syncedByClient(item) {
return true;
}
// Compare the server item and the client item, and return a list of property
// names that are different. Can be overridden if needed.
getDifferences(client, server) {
let differences = [];
for (let prop of this.props) {
let clientProp = client[prop];
let serverProp = server[prop];
if ((clientProp || "") !== (serverProp || "")) {
differences.push(prop);
}
}
return differences;
}
// Returns an object containing
// problemData: an instance of the class returned by emptyProblemData(),
// clientRecords: Normalized client records
// records: Normalized server records,
// deletedRecords: Array of ids that were marked as deleted by the server.
compareClientWithServer(clientItems, serverItems) {
clientItems = clientItems.map(item => this.normalizeClientItem(item));
serverItems = serverItems.map(item => this.normalizeServerItem(item));
let problems = this.emptyProblemData();
let seenServer = new Map();
let serverDeleted = new Set();
let allRecords = new Map();
for (let record of serverItems) {
let id = record[this.idProp];
if (!id) {
++problems.missingIDs;
continue;
}
if (record.deleted) {
serverDeleted.add(record);
} else {
let possibleDupe = seenServer.get(id);
if (possibleDupe) {
problems.duplicates.push(id);
} else {
seenServer.set(id, record);
allRecords.set(id, { server: record, client: null, });
}
record.understood = this.clientUnderstands(record);
}
}
let recordPairs = [];
let seenClient = new Map();
for (let record of clientItems) {
let id = record[this.idProp];
record.shouldSync = this.syncedByClient(record);
seenClient.set(id, record);
let combined = allRecords.get(id);
if (combined) {
combined.client = record;
} else {
allRecords.set(id, { client: record, server: null });
}
}
for (let [id, { server, client }] of allRecords) {
if (!client && !server) {
throw new Error("Impossible: no client or server record for " + id);
} else if (server && !client) {
if (server.understood) {
problems.clientMissing.push(id);
}
} else if (client && !server) {
if (client.shouldSync) {
problems.serverMissing.push(id);
}
} else {
if (!client.shouldSync) {
if (!problems.serverUnexpected.includes(id)) {
problems.serverUnexpected.push(id);
}
continue;
}
let differences = this.getDifferences(client, server);
if (differences && differences.length) {
problems.differences.push({ id, differences });
}
}
}
return {
problemData: problems,
clientRecords: clientItems,
records: serverItems,
deletedRecords: [...serverDeleted]
};
}
}
// Default to 0, some engines may override.
CollectionValidator.prototype.version = 0;

View file

@ -4,8 +4,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// Process each item in the "constants hash" to add to "global" and give a name
this.EXPORTED_SYMBOLS = [];
for (let [key, val] of Object.entries({
this.EXPORTED_SYMBOLS = [((this[key] = val), key) for ([key, val] in Iterator({
WEAVE_VERSION: "@weave_version@",
@ -45,7 +44,7 @@ MAX_IGNORE_ERROR_COUNT: 5,
// Backoff intervals
MINIMUM_BACKOFF_INTERVAL: 15 * 60 * 1000, // 15 minutes
MAXIMUM_BACKOFF_INTERVAL: 8 * 60 * 60 * 1000, // 8 hours
MAXIMUM_BACKOFF_INTERVAL: 8 * 60 * 60 * 1000, // 8 hours
// HMAC event handling timeout.
// 10 minutes: a compromise between the multi-desktop sync interval
@ -76,10 +75,6 @@ PASSWORDS_STORE_BATCH_SIZE: 50, // same as MOBILE_BATCH_SIZE
ADDONS_STORE_BATCH_SIZE: 1000000, // process all addons at once
APPS_STORE_BATCH_SIZE: 50, // same as MOBILE_BATCH_SIZE
// Default batch size for download batching
// (how many records are fetched at a time from the server when batching is used).
DEFAULT_DOWNLOAD_BATCH_SIZE: 1000,
// score thresholds for early syncs
SINGLE_USER_THRESHOLD: 1000,
MULTI_DEVICE_THRESHOLD: 300,
@ -98,16 +93,13 @@ SCORE_UPDATE_DELAY: 100,
// observed spurious idle/back events and short enough to pre-empt user activity.
IDLE_OBSERVER_BACK_DELAY: 100,
// Max number of records or bytes to upload in a single POST - we'll do multiple POSTS if either
// MAX_UPLOAD_RECORDS or MAX_UPLOAD_BYTES is hit)
// Number of records to upload in a single POST (multiple POSTS if exceeded)
// FIXME: Record size limit is 256k (new cluster), so this can be quite large!
// (Bug 569295)
MAX_UPLOAD_RECORDS: 100,
MAX_UPLOAD_BYTES: 1024 * 1023, // just under 1MB
MAX_HISTORY_UPLOAD: 5000,
MAX_HISTORY_DOWNLOAD: 5000,
// TTL of the message sent to another device when sending a tab
NOTIFY_TAB_SENT_TTL_SECS: 1 * 3600, // 1 hour
// Top-level statuses:
STATUS_OK: "success.status_ok",
SYNC_FAILED: "error.sync.failed",
@ -130,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",
@ -152,8 +145,6 @@ ENGINE_UNKNOWN_FAIL: "error.engine.reason.unknown_fail",
ENGINE_APPLY_FAIL: "error.engine.reason.apply_fail",
ENGINE_METARECORD_DOWNLOAD_FAIL: "error.engine.reason.metarecord_download_fail",
ENGINE_METARECORD_UPLOAD_FAIL: "error.engine.reason.metarecord_upload_fail",
// an upload failure where the batch was interrupted with a 412
ENGINE_BATCH_INTERRUPTED: "error.engine.reason.batch_interrupted",
JPAKE_ERROR_CHANNEL: "jpake.error.channel",
JPAKE_ERROR_NETWORK: "jpake.error.network",
@ -181,7 +172,7 @@ kSyncBackoffNotMet: "Trying to sync before the server said it
kFirstSyncChoiceNotMade: "User has not selected an action for first sync",
// Application IDs
FIREFOX_ID: "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}",
FIREFOX_ID: "{8de7fcbb-c55c-4fbe-bfc5-fc555c87dbc4}",
FENNEC_ID: "{a23983c0-fd0e-11dc-95ff-0800200c9a66}",
SEAMONKEY_ID: "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}",
TEST_HARNESS_ID: "xuth@mozilla.org",
@ -189,10 +180,6 @@ TEST_HARNESS_ID: "xuth@mozilla.org",
MIN_PP_LENGTH: 12,
MIN_PASS_LENGTH: 8,
DEVICE_TYPE_DESKTOP: "desktop",
DEVICE_TYPE_MOBILE: "mobile",
LOG_DATE_FORMAT: "%Y-%m-%d %H:%M:%S",
})) {
this[key] = val;
this.EXPORTED_SYMBOLS.push(key);
}
}))];

View file

@ -7,24 +7,21 @@ this.EXPORTED_SYMBOLS = [
"Engine",
"SyncEngine",
"Tracker",
"Store",
"Changeset"
"Store"
];
var {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
Cu.import("resource://services-common/async.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");
Cu.import("resource://services-sync/identity.js");
Cu.import("resource://services-sync/record.js");
Cu.import("resource://services-sync/resource.js");
Cu.import("resource://services-sync/util.js");
XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
"resource://gre/modules/FxAccounts.jsm");
/*
* Trackers are associated with a single engine and deal with
* listening for changes to their particular data type.
@ -107,7 +104,7 @@ Tracker.prototype = {
Utils.jsonLoad("changes/" + this.file, this, function(json) {
if (json && (typeof(json) == "object")) {
this.changedIDs = json;
} else if (json !== null) {
} else {
this._log.warn("Changed IDs file " + this.file + " contains non-object value.");
json = null;
}
@ -132,30 +129,26 @@ Tracker.prototype = {
this._ignored.splice(index, 1);
},
_saveChangedID(id, when) {
this._log.trace(`Adding changed ID: ${id}, ${JSON.stringify(when)}`);
this.changedIDs[id] = when;
this.saveChangedIDs(this.onSavedChangedIDs);
},
addChangedID: function (id, when) {
if (!id) {
this._log.warn("Attempted to add undefined ID to tracker");
return false;
}
if (this.ignoreAll || this._ignored.includes(id)) {
if (this.ignoreAll || (id in this._ignored)) {
return false;
}
// Default to the current time in seconds if no time is provided.
if (when == null) {
when = this._now();
when = Math.floor(Date.now() / 1000);
}
// Add/update the entry if we have a newer time.
if ((this.changedIDs[id] || -Infinity) < when) {
this._saveChangedID(id, when);
this._log.trace("Adding changed ID: " + id + ", " + when);
this.changedIDs[id] = when;
this.saveChangedIDs(this.onSavedChangedIDs);
}
return true;
@ -166,9 +159,8 @@ Tracker.prototype = {
this._log.warn("Attempted to remove undefined ID to tracker");
return false;
}
if (this.ignoreAll || this._ignored.includes(id)) {
if (this.ignoreAll || (id in this._ignored))
return false;
}
if (this.changedIDs[id] != null) {
this._log.trace("Removing changed ID " + id);
delete this.changedIDs[id];
@ -183,10 +175,6 @@ Tracker.prototype = {
this.saveChangedIDs();
},
_now() {
return Date.now() / 1000;
},
_isTracking: false,
// Override these in your subclasses.
@ -311,21 +299,17 @@ Store.prototype = {
*/
applyIncomingBatch: function (records) {
let failed = [];
for (let record of records) {
for each (let record in records) {
try {
this.applyIncoming(record);
} catch (ex if (ex.code == Engine.prototype.eEngineAbortApplyIncoming)) {
// This kind of exception should have a 'cause' attribute, which is an
// originating exception.
// ex.cause will carry its stack with it when rethrown.
throw ex.cause;
} catch (ex) {
if (ex.code == Engine.prototype.eEngineAbortApplyIncoming) {
// This kind of exception should have a 'cause' attribute, which is an
// originating exception.
// ex.cause will carry its stack with it when rethrown.
throw ex.cause;
}
if (Async.isShutdownException(ex)) {
throw ex;
}
this._log.warn("Failed to apply incoming record " + record.id, ex);
this.engine._noteApplyFailure();
this._log.warn("Failed to apply incoming record " + record.id);
this._log.warn("Encountered exception: " + Utils.exceptionStr(ex));
failed.push(record.id);
}
};
@ -499,11 +483,7 @@ EngineManager.prototype = {
},
getAll: function () {
let engines = [];
for (let [, engine] of Object.entries(this._engines)) {
engines.push(engine);
}
return engines;
return [engine for ([name, engine] in Iterator(this._engines))];
},
/**
@ -516,7 +496,7 @@ EngineManager.prototype = {
},
get enabledEngineNames() {
return this.getEnabled().map(e => e.name);
return [e.name for each (e in this.getEnabled())];
},
persistDeclined: function () {
@ -593,11 +573,16 @@ EngineManager.prototype = {
this._engines[name] = engine;
}
} catch (ex) {
this._log.error(CommonUtils.exceptionStr(ex));
let mesg = ex.message ? ex.message : ex;
let name = engineObject || "";
name = name.prototype || "";
name = name.name || "";
this._log.error(`Could not initialize engine ${name}`, ex);
let out = "Could not initialize engine '" + name + "': " + mesg;
this._log.error(out);
return engineObject;
}
},
@ -643,17 +628,16 @@ Engine.prototype = {
// Signal to the engine that processing further records is pointless.
eEngineAbortApplyIncoming: "error.engine.abort.applyincoming",
// Should we keep syncing if we find a record that cannot be uploaded (ever)?
// If this is false, we'll throw, otherwise, we'll ignore the record and
// continue. This currently can only happen due to the record being larger
// than the record upload limit.
allowSkippedRecord: true,
get prefName() {
return this.name;
},
get enabled() {
// XXX: Disable non-functional add-ons syncing for the time being
// This check can go away when add-on syncing is addressed
if (this.prefName == "addons")
return false;
return Svc.Prefs.get("engine." + this.prefName, false);
},
@ -711,15 +695,6 @@ Engine.prototype = {
wipeClient: function () {
this._notify("wipe-client", this.name, this._wipeClient)();
},
/**
* If one exists, initialize and return a validator for this engine (which
* must have a `validate(engine)` method that returns a promise to an object
* with a getSummary method). Otherwise return null.
*/
getValidator: function () {
return null;
}
};
@ -813,11 +788,7 @@ SyncEngine.prototype = {
return this._toFetch;
},
set toFetch(val) {
let cb = (error) => {
if (error) {
this._log.error("Failed to read JSON records to fetch", error);
}
}
let cb = (error) => this._log.error(Utils.exceptionStr(error));
// Coerce the array to a string for more efficient comparison.
if (val + "" == this._toFetch) {
return;
@ -842,13 +813,7 @@ SyncEngine.prototype = {
return this._previousFailed;
},
set previousFailed(val) {
let cb = (error) => {
if (error) {
this._log.error("Failed to set previousFailed", error);
} else {
this._log.debug("Successfully wrote previousFailed.");
}
}
let cb = (error) => this._log.error(Utils.exceptionStr(error));
// Coerce the array to a string for more efficient comparison.
if (val + "" == this._previousFailed) {
return;
@ -881,8 +846,9 @@ SyncEngine.prototype = {
},
/*
* Returns a changeset for this sync. Engine implementations can override this
* method to bypass the tracker for certain or all changed items.
* Returns a mapping of IDs -> changed timestamp. Engine implementations
* can override this method to bypass the tracker for certain or all
* changed items.
*/
getChangedIDs: function () {
return this._tracker.changedIDs;
@ -950,16 +916,20 @@ SyncEngine.prototype = {
// this._modified to the tracker.
this.lastSyncLocal = Date.now();
if (this.lastSync) {
this._modified = this.pullNewChanges();
this._modified = this.getChangedIDs();
} else {
// Mark all items to be uploaded, but treat them as changed from long ago
this._log.debug("First sync, uploading all items");
this._modified = this.pullAllChanges();
this._modified = {};
for (let id in this._store.getAllIDs()) {
this._modified[id] = 0;
}
}
// Clear the tracker now. If the sync fails we'll add the ones we failed
// to upload back.
this._tracker.clearChangedIDs();
this._log.info(this._modified.count() +
this._log.info(Object.keys(this._modified).length +
" outgoing items pre-reconciliation");
// Keep track of what to delete at the end of sync
@ -970,7 +940,7 @@ SyncEngine.prototype = {
* A tiny abstraction to make it easier to test incoming record
* application.
*/
itemSource: function () {
_itemSource: function () {
return new Collection(this.engineURL, this._recordObj, this.service);
},
@ -987,7 +957,7 @@ SyncEngine.prototype = {
let isMobile = (Svc.Prefs.get("client.type") == "mobile");
if (!newitems) {
newitems = this.itemSource();
newitems = this._itemSource();
}
if (this._defaultSort) {
@ -1024,12 +994,10 @@ SyncEngine.prototype = {
try {
failed = failed.concat(this._store.applyIncomingBatch(applyBatch));
} catch (ex) {
if (Async.isShutdownException(ex)) {
throw ex;
}
// Catch any error that escapes from applyIncomingBatch. At present
// those will all be abort events.
this._log.warn("Got exception, aborting processIncoming", ex);
this._log.warn("Got exception " + Utils.exceptionStr(ex) +
", aborting processIncoming.");
aborting = ex;
}
this._tracker.ignoreAll = false;
@ -1074,10 +1042,7 @@ SyncEngine.prototype = {
try {
try {
item.decrypt(key);
} catch (ex) {
if (!Utils.isHMACMismatch(ex)) {
throw ex;
}
} catch (ex if Utils.isHMACMismatch(ex)) {
let strategy = self.handleHMACMismatch(item, true);
if (strategy == SyncEngine.kRecoveryStrategy.retry) {
// You only get one retry.
@ -1087,10 +1052,7 @@ SyncEngine.prototype = {
key = self.service.collectionKeys.keyForCollection(self.name);
item.decrypt(key);
strategy = null;
} catch (ex) {
if (!Utils.isHMACMismatch(ex)) {
throw ex;
}
} catch (ex if Utils.isHMACMismatch(ex)) {
strategy = self.handleHMACMismatch(item, false);
}
}
@ -1103,8 +1065,7 @@ SyncEngine.prototype = {
self._log.debug("Ignoring second retry suggestion.");
// Fall through to error case.
case SyncEngine.kRecoveryStrategy.error:
self._log.warn("Error decrypting record", ex);
self._noteApplyFailure();
self._log.warn("Error decrypting record: " + Utils.exceptionStr(ex));
failed.push(item.id);
return;
case SyncEngine.kRecoveryStrategy.ignore:
@ -1114,11 +1075,7 @@ SyncEngine.prototype = {
}
}
} catch (ex) {
if (Async.isShutdownException(ex)) {
throw ex;
}
self._log.warn("Error decrypting record", ex);
self._noteApplyFailure();
self._log.warn("Error decrypting record: " + Utils.exceptionStr(ex));
failed.push(item.id);
return;
}
@ -1126,20 +1083,15 @@ SyncEngine.prototype = {
let shouldApply;
try {
shouldApply = self._reconcile(item);
} catch (ex if (ex.code == Engine.prototype.eEngineAbortApplyIncoming)) {
self._log.warn("Reconciliation failed: aborting incoming processing.");
failed.push(item.id);
aborting = ex.cause;
} catch (ex) {
if (ex.code == Engine.prototype.eEngineAbortApplyIncoming) {
self._log.warn("Reconciliation failed: aborting incoming processing.");
self._noteApplyFailure();
failed.push(item.id);
aborting = ex.cause;
} else if (!Async.isShutdownException(ex)) {
self._log.warn("Failed to reconcile incoming record " + item.id, ex);
self._noteApplyFailure();
failed.push(item.id);
return;
} else {
throw ex;
}
self._log.warn("Failed to reconcile incoming record " + item.id);
self._log.warn("Encountered exception: " + Utils.exceptionStr(ex));
failed.push(item.id);
return;
}
if (shouldApply) {
@ -1158,7 +1110,7 @@ SyncEngine.prototype = {
// Only bother getting data from the server if there's new things
if (this.lastModified == null || this.lastModified > this.lastSync) {
let resp = newitems.getBatched();
let resp = newitems.get();
doApplyBatchAndPersistFailed.call(this);
if (!resp.success) {
resp.failureCode = ENGINE_DOWNLOAD_FAIL;
@ -1243,13 +1195,7 @@ SyncEngine.prototype = {
// Apply remaining items.
doApplyBatchAndPersistFailed.call(this);
count.newFailed = this.previousFailed.reduce((count, engine) => {
if (failedInPreviousSync.indexOf(engine) == -1) {
count++;
this._noteApplyNewFailure();
}
return count;
}, 0);
count.newFailed = Utils.arraySub(this.previousFailed, failedInPreviousSync).length;
count.succeeded = Math.max(0, count.applied - count.failed);
this._log.info(["Records:",
count.applied, "applied,",
@ -1260,14 +1206,6 @@ SyncEngine.prototype = {
Observers.notify("weave:engine:sync:applied", count, this.name);
},
_noteApplyFailure: function () {
// here would be a good place to record telemetry...
},
_noteApplyNewFailure: function () {
// here would be a good place to record telemetry...
},
/**
* Find a GUID of an item that is a duplicate of the incoming item but happens
* to have a different GUID
@ -1278,16 +1216,6 @@ SyncEngine.prototype = {
// By default, assume there's no dupe items for the engine
},
// Called when the server has a record marked as deleted, but locally we've
// changed it more recently than the deletion. If we return false, the
// record will be deleted locally. If we return true, we'll reupload the
// record to the server -- any extra work that's needed as part of this
// process should be done at this point (such as mark the record's parent
// for reuploading in the case of bookmarks).
_shouldReviveRemotelyDeletedRecord(remoteItem) {
return true;
},
_deleteId: function (id) {
this._tracker.removeChangedID(id);
@ -1298,18 +1226,6 @@ SyncEngine.prototype = {
this._delete.ids.push(id);
},
_switchItemToDupe(localDupeGUID, incomingItem) {
// The local, duplicate ID is always deleted on the server.
this._deleteId(localDupeGUID);
// We unconditionally change the item's ID in case the engine knows of
// an item but doesn't expose it through itemExists. If the API
// contract were stronger, this could be changed.
this._log.debug("Switching local ID to incoming: " + localDupeGUID + " -> " +
incomingItem.id);
this._store.changeItemID(localDupeGUID, incomingItem.id);
},
/**
* Reconcile incoming record with local state.
*
@ -1329,12 +1245,12 @@ SyncEngine.prototype = {
// because some state may change during the course of this function and we
// need to operate on the original values.
let existsLocally = this._store.itemExists(item.id);
let locallyModified = this._modified.has(item.id);
let locallyModified = item.id in this._modified;
// TODO Handle clock drift better. Tracked in bug 721181.
let remoteAge = AsyncResource.serverTime - item.modified;
let localAge = locallyModified ?
(Date.now() / 1000 - this._modified.getModifiedTimestamp(item.id)) : null;
(Date.now() / 1000 - this._modified[item.id]) : null;
let remoteIsNewer = remoteAge < localAge;
this._log.trace("Reconciling " + item.id + ". exists=" +
@ -1363,18 +1279,15 @@ SyncEngine.prototype = {
"exists and isn't modified.");
return true;
}
this._log.trace("Incoming record is deleted but we had local changes.");
if (remoteIsNewer) {
this._log.trace("Remote record is newer -- deleting local record.");
return true;
}
// If the local record is newer, we defer to individual engines for
// how to handle this. By default, we revive the record.
let willRevive = this._shouldReviveRemotelyDeletedRecord(item);
this._log.trace("Local record is newer -- reviving? " + willRevive);
return !willRevive;
// TODO As part of bug 720592, determine whether we should do more here.
// In the case where the local changes are newer, it is quite possible
// that the local client will restore data a remote client had tried to
// delete. There might be a good reason for that delete and it might be
// enexpected for this client to restore that data.
this._log.trace("Incoming record is deleted but we had local changes. " +
"Applying the youngest record.");
return remoteIsNewer;
}
// At this point the incoming record is not for a deletion and must have
@ -1386,32 +1299,40 @@ SyncEngine.prototype = {
// refresh the metadata collected above. See bug 710448 for the history
// of this logic.
if (!existsLocally) {
let localDupeGUID = this._findDupe(item);
if (localDupeGUID) {
this._log.trace("Local item " + localDupeGUID + " is a duplicate for " +
let dupeID = this._findDupe(item);
if (dupeID) {
this._log.trace("Local item " + dupeID + " is a duplicate for " +
"incoming item " + item.id);
// The local, duplicate ID is always deleted on the server.
this._deleteId(dupeID);
// The current API contract does not mandate that the ID returned by
// _findDupe() actually exists. Therefore, we have to perform this
// check.
existsLocally = this._store.itemExists(localDupeGUID);
existsLocally = this._store.itemExists(dupeID);
// We unconditionally change the item's ID in case the engine knows of
// an item but doesn't expose it through itemExists. If the API
// contract were stronger, this could be changed.
this._log.debug("Switching local ID to incoming: " + dupeID + " -> " +
item.id);
this._store.changeItemID(dupeID, item.id);
// If the local item was modified, we carry its metadata forward so
// appropriate reconciling can be performed.
if (this._modified.has(localDupeGUID)) {
if (dupeID in this._modified) {
locallyModified = true;
localAge = this._tracker._now() - this._modified.getModifiedTimestamp(localDupeGUID);
localAge = Date.now() / 1000 - this._modified[dupeID];
remoteIsNewer = remoteAge < localAge;
this._modified.swap(localDupeGUID, item.id);
this._modified[item.id] = this._modified[dupeID];
delete this._modified[dupeID];
} else {
locallyModified = false;
localAge = null;
}
// Tell the engine to do whatever it needs to switch the items.
this._switchItemToDupe(localDupeGUID, item);
this._log.debug("Local item after duplication: age=" + localAge +
"; modified=" + locallyModified + "; exists=" +
existsLocally);
@ -1440,7 +1361,7 @@ SyncEngine.prototype = {
if (remoteIsNewer) {
this._log.trace("Applying incoming because local item was deleted " +
"before the incoming item was changed.");
this._modified.delete(item.id);
delete this._modified[item.id];
return true;
}
@ -1466,7 +1387,7 @@ SyncEngine.prototype = {
this._log.trace("Ignoring incoming item because the local item is " +
"identical.");
this._modified.delete(item.id);
delete this._modified[item.id];
return false;
}
@ -1491,97 +1412,69 @@ SyncEngine.prototype = {
_uploadOutgoing: function () {
this._log.trace("Uploading local changes to server.");
let modifiedIDs = this._modified.ids();
let modifiedIDs = Object.keys(this._modified);
if (modifiedIDs.length) {
this._log.trace("Preparing " + modifiedIDs.length +
" outgoing records");
let counts = { sent: modifiedIDs.length, failed: 0 };
// collection we'll upload
let up = new Collection(this.engineURL, null, this.service);
let count = 0;
let failed = [];
let successful = [];
let handleResponse = (resp, batchOngoing = false) => {
// Note: We don't want to update this.lastSync, or this._modified until
// the batch is complete, however we want to remember success/failure
// indicators for when that happens.
// Upload what we've got so far in the collection
let doUpload = Utils.bind2(this, function(desc) {
this._log.info("Uploading " + desc + " of " + modifiedIDs.length +
" records");
let resp = up.post();
if (!resp.success) {
this._log.debug("Uploading records failed: " + resp);
resp.failureCode = resp.status == 412 ? ENGINE_BATCH_INTERRUPTED : ENGINE_UPLOAD_FAIL;
resp.failureCode = ENGINE_UPLOAD_FAIL;
throw resp;
}
// Update server timestamp from the upload.
failed = failed.concat(Object.keys(resp.obj.failed));
successful = successful.concat(resp.obj.success);
if (batchOngoing) {
// Nothing to do yet
return;
}
// Advance lastSync since we've finished the batch.
let modified = resp.headers["x-weave-timestamp"];
if (modified > this.lastSync) {
if (modified > this.lastSync)
this.lastSync = modified;
}
if (failed.length && this._log.level <= Log.Level.Debug) {
let failed_ids = Object.keys(resp.obj.failed);
if (failed_ids.length)
this._log.debug("Records that will be uploaded again because "
+ "the server couldn't store them: "
+ failed.join(", "));
+ failed_ids.join(", "));
// Clear successfully uploaded objects.
for each (let id in resp.obj.success) {
delete this._modified[id];
}
counts.failed += failed.length;
up.clearRecords();
});
for (let id of successful) {
this._modified.delete(id);
}
this._onRecordsWritten(successful, failed);
// clear for next batch
failed.length = 0;
successful.length = 0;
};
let postQueue = up.newPostQueue(this._log, this.lastSync, handleResponse);
for (let id of modifiedIDs) {
let out;
let ok = false;
for each (let id in modifiedIDs) {
try {
out = this._createRecord(id);
let out = this._createRecord(id);
if (this._log.level <= Log.Level.Trace)
this._log.trace("Outgoing: " + out);
out.encrypt(this.service.collectionKeys.keyForCollection(this.name));
ok = true;
} catch (ex) {
if (Async.isShutdownException(ex)) {
throw ex;
}
this._log.warn("Error creating record", ex);
up.pushData(out);
}
if (ok) {
let { enqueued, error } = postQueue.enqueue(out);
if (!enqueued) {
++counts.failed;
if (!this.allowSkippedRecord) {
throw error;
}
}
catch(ex) {
this._log.warn("Error creating record: " + Utils.exceptionStr(ex));
}
// Partial upload
if ((++count % MAX_UPLOAD_RECORDS) == 0)
doUpload((count - MAX_UPLOAD_RECORDS) + " - " + count + " out");
this._store._sleep(0);
}
postQueue.flush(true);
Observers.notify("weave:engine:sync:uploaded", counts, this.name);
}
},
_onRecordsWritten(succeeded, failed) {
// Implement this method to take specific actions against successfully
// uploaded records and failed records.
// Final upload
if (count % MAX_UPLOAD_RECORDS > 0)
doUpload(count >= MAX_UPLOAD_RECORDS ? "last batch" : "all");
}
},
// Any cleanup necessary.
@ -1596,7 +1489,7 @@ SyncEngine.prototype = {
coll.delete();
});
for (let [key, val] of Object.entries(this._delete)) {
for (let [key, val] in Iterator(this._delete)) {
// Remove the key for future uses
delete this._delete[key];
@ -1619,8 +1512,10 @@ SyncEngine.prototype = {
}
// Mark failed WBOs as changed again so they are reuploaded next time.
this.trackRemainingChanges();
this._modified.clear();
for (let [id, when] in Iterator(this._modified)) {
this._tracker.addChangedID(id, when);
}
this._modified = {};
},
_sync: function () {
@ -1656,11 +1551,9 @@ SyncEngine.prototype = {
try {
this._log.trace("Trying to decrypt a record from the server..");
test.get();
} catch (ex) {
if (Async.isShutdownException(ex)) {
throw ex;
}
this._log.debug("Failed test decrypt", ex);
}
catch(ex) {
this._log.debug("Failed test decrypt: " + Utils.exceptionStr(ex));
}
return canDecrypt;
@ -1706,108 +1599,5 @@ SyncEngine.prototype = {
return (this.service.handleHMACEvent() && mayRetry) ?
SyncEngine.kRecoveryStrategy.retry :
SyncEngine.kRecoveryStrategy.error;
},
/**
* Returns a changeset containing all items in the store. The default
* implementation returns a changeset with timestamps from long ago, to
* ensure we always use the remote version if one exists.
*
* This function is only called for the first sync. Subsequent syncs call
* `pullNewChanges`.
*
* @return A `Changeset` object.
*/
pullAllChanges() {
let changeset = new Changeset();
for (let id in this._store.getAllIDs()) {
changeset.set(id, 0);
}
return changeset;
},
/*
* Returns a changeset containing entries for all currently tracked items.
* The default implementation returns a changeset with timestamps indicating
* when the item was added to the tracker.
*
* @return A `Changeset` object.
*/
pullNewChanges() {
return new Changeset(this.getChangedIDs());
},
/**
* Adds all remaining changeset entries back to the tracker, typically for
* items that failed to upload. This method is called at the end of each sync.
*
*/
trackRemainingChanges() {
for (let [id, change] of this._modified.entries()) {
this._tracker.addChangedID(id, change);
}
},
}
};
/**
* A changeset is created for each sync in `Engine::get{Changed, All}IDs`,
* and stores opaque change data for tracked IDs. The default implementation
* only records timestamps, though engines can extend this to store additional
* data for each entry.
*/
class Changeset {
// Creates a changeset with an initial set of tracked entries.
constructor(changes = {}) {
this.changes = changes;
}
// Returns the last modified time, in seconds, for an entry in the changeset.
// `id` is guaranteed to be in the set.
getModifiedTimestamp(id) {
return this.changes[id];
}
// Adds a change for a tracked ID to the changeset.
set(id, change) {
this.changes[id] = change;
}
// Indicates whether an entry is in the changeset.
has(id) {
return id in this.changes;
}
// Deletes an entry from the changeset. Used to clean up entries for
// reconciled and successfully uploaded records.
delete(id) {
delete this.changes[id];
}
// Swaps two entries in the changeset. Used when reconciling duplicates that
// have local changes.
swap(oldID, newID) {
this.changes[newID] = this.changes[oldID];
delete this.changes[oldID];
}
// Returns an array of all tracked IDs in this changeset.
ids() {
return Object.keys(this.changes);
}
// Returns an array of `[id, change]` tuples. Used to repopulate the tracker
// with entries for failed uploads at the end of a sync.
entries() {
return Object.entries(this.changes);
}
// Returns the number of entries in this changeset.
count() {
return this.ids().length;
}
// Clears the changeset.
clear() {
this.changes = {};
}
}

View file

@ -25,18 +25,15 @@
*
* Synchronization is influenced by the following preferences:
*
* - services.sync.addons.ignoreRepositoryChecking
* - services.sync.addons.ignoreUserEnabledChanges
* - services.sync.addons.trustedSourceHostnames
*
* and also influenced by whether addons have repository caching enabled and
* whether they allow installation of addons from insecure options (both of
* which are themselves influenced by the "extensions." pref branch)
*
* See the documentation in services-sync.js for the behavior of these prefs.
*/
"use strict";
var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://services-sync/addonutils.js");
Cu.import("resource://services-sync/addonsreconciler.js");
@ -44,7 +41,6 @@ Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/record.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/collection_validator.js");
Cu.import("resource://services-common/async.js");
Cu.import("resource://gre/modules/Preferences.jsm");
@ -54,7 +50,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository",
"resource://gre/modules/addons/AddonRepository.jsm");
this.EXPORTED_SYMBOLS = ["AddonsEngine", "AddonValidator"];
this.EXPORTED_SYMBOLS = ["AddonsEngine"];
// 7 days in milliseconds.
const PRUNE_ADDON_CHANGES_THRESHOLD = 60 * 60 * 24 * 7 * 1000;
@ -154,7 +150,7 @@ AddonsEngine.prototype = {
*/
getChangedIDs: function getChangedIDs() {
let changes = {};
for (let [id, modified] of Object.entries(this._tracker.changedIDs)) {
for (let [id, modified] in Iterator(this._tracker.changedIDs)) {
changes[id] = modified;
}
@ -164,7 +160,7 @@ AddonsEngine.prototype = {
// we assume this function is only called from within a sync.
let reconcilerChanges = this._reconciler.getChangesSinceDate(lastSyncDate);
let addons = this._reconciler.addons;
for (let change of reconcilerChanges) {
for each (let change in reconcilerChanges) {
let changeTime = change[0];
let id = change[2];
@ -177,7 +173,7 @@ AddonsEngine.prototype = {
continue;
}
if (!this.isAddonSyncable(addons[id])) {
if (!this._store.isAddonSyncable(addons[id])) {
continue;
}
@ -235,10 +231,6 @@ AddonsEngine.prototype = {
let cb = Async.makeSpinningCallback();
this._reconciler.refreshGlobalState(cb);
cb.wait();
},
isAddonSyncable(addon, ignoreRepoCheck) {
return this._store.isAddonSyncable(addon, ignoreRepoCheck);
}
};
@ -286,14 +278,6 @@ AddonsStore.prototype = {
}
}
// Ignore incoming records for which an existing non-syncable addon
// exists.
let existingMeta = this.reconciler.addons[record.addonID];
if (existingMeta && !this.isAddonSyncable(existingMeta)) {
this._log.info("Ignoring incoming record for an existing but non-syncable addon", record.addonID);
return;
}
Store.prototype.applyIncoming.call(this, record);
},
@ -307,23 +291,15 @@ AddonsStore.prototype = {
id: record.addonID,
syncGUID: record.id,
enabled: record.enabled,
requireSecureURI: this._extensionsPrefs.get("install.requireSecureOrigin", true),
requireSecureURI: !Svc.Prefs.get("addons.ignoreRepositoryChecking", false),
}], cb);
// This will throw if there was an error. This will get caught by the sync
// engine and the record will try to be applied later.
let results = cb.wait();
if (results.skipped.includes(record.addonID)) {
this._log.info("Add-on skipped: " + record.addonID);
// Just early-return for skipped addons - we don't want to arrange to
// try again next time because the condition that caused up to skip
// will remain true for this addon forever.
return;
}
let addon;
for (let a of results.addons) {
for each (let a in results.addons) {
if (a.id == record.addonID) {
addon = a;
break;
@ -467,8 +443,7 @@ AddonsStore.prototype = {
let ids = {};
let addons = this.reconciler.addons;
for (let id in addons) {
let addon = addons[id];
for each (let addon in addons) {
if (this.isAddonSyncable(addon)) {
ids[addon.guid] = true;
}
@ -499,7 +474,7 @@ AddonsStore.prototype = {
}
this._log.info("Uninstalling add-on as part of wipe: " + addon.id);
Utils.catch.call(this, () => addon.uninstall())();
Utils.catch(addon.uninstall)();
}
},
@ -538,22 +513,16 @@ AddonsStore.prototype = {
*
* @param addon
* Addon instance
* @param ignoreRepoCheck
* Should we skip checking the Addons repository (primarially useful
* for testing and validation).
* @return Boolean indicating whether it is appropriate for Sync
*/
isAddonSyncable: function isAddonSyncable(addon, ignoreRepoCheck = false) {
isAddonSyncable: function isAddonSyncable(addon) {
// Currently, we limit syncable add-ons to those that are:
// 1) In a well-defined set of types
// 2) Installed in the current profile
// 3) Not installed by a foreign entity (i.e. installed by the app)
// since they act like global extensions.
// 4) Is not a hotfix.
// 5) The addons XPIProvider doesn't veto it (i.e not being installed in
// the profile directory, or any other reasons it says the addon can't
// be synced)
// 6) Are installed from AMO
// 5) Are installed from AMO
// We could represent the test as a complex boolean expression. We go the
// verbose route so the failure reason is logged.
@ -573,12 +542,6 @@ AddonsStore.prototype = {
return false;
}
// If the addon manager says it's not syncable, we skip it.
if (!addon.isSyncable) {
this._log.debug(addon.id + " not syncable: vetoed by the addon manager.");
return false;
}
// This may be too aggressive. If an add-on is downloaded from AMO and
// manually placed in the profile directory, foreignInstall will be set.
// Arguably, that add-on should be syncable.
@ -589,20 +552,15 @@ AddonsStore.prototype = {
}
// Ignore hotfix extensions (bug 741670). The pref may not be defined.
// XXX - note that addon.isSyncable will be false for hotfix addons, so
// this check isn't strictly necessary - except for Sync tests which aren't
// setup to create a "real" hotfix addon. This can be removed once those
// tests are fixed (but keeping it doesn't hurt either)
if (this._extensionsPrefs.get("hotfix.id", null) == addon.id) {
this._log.debug(addon.id + " not syncable: is a hotfix.");
return false;
}
// If the AddonRepository's cache isn't enabled (which it typically isn't
// in tests), getCachedAddonByID always returns null - so skip the check
// in that case. We also provide a way to specifically opt-out of the check
// even if the cache is enabled, which is used by the validators.
if (ignoreRepoCheck || !AddonRepository.cacheEnabled) {
// We provide a back door to skip the repository checking of an add-on.
// This is utilized by the tests to make testing easier. Users could enable
// this, but it would sacrifice security.
if (Svc.Prefs.get("addons.ignoreRepositoryChecking", false)) {
return true;
}
@ -745,69 +703,3 @@ AddonsTracker.prototype = {
this.reconciler.stopListening();
},
};
class AddonValidator extends CollectionValidator {
constructor(engine = null) {
super("addons", "id", [
"addonID",
"enabled",
"applicationID",
"source"
]);
this.engine = engine;
}
getClientItems() {
return Promise.all([
new Promise(resolve =>
AddonManager.getAllAddons(resolve)),
new Promise(resolve =>
AddonManager.getAddonsWithOperationsByTypes(["extension", "theme"], resolve)),
]).then(([installed, addonsWithPendingOperation]) => {
// Addons pending install won't be in the first list, but addons pending
// uninstall/enable/disable will be in both lists.
let all = new Map(installed.map(addon => [addon.id, addon]));
for (let addon of addonsWithPendingOperation) {
all.set(addon.id, addon);
}
// Convert to an array since Map.prototype.values returns an iterable
return [...all.values()];
});
}
normalizeClientItem(item) {
let enabled = !item.userDisabled;
if (item.pendingOperations & AddonManager.PENDING_ENABLE) {
enabled = true;
} else if (item.pendingOperations & AddonManager.PENDING_DISABLE) {
enabled = false;
}
return {
enabled,
id: item.syncGUID,
addonID: item.id,
applicationID: Services.appinfo.ID,
source: "amo", // check item.foreignInstall?
original: item
};
}
normalizeServerItem(item) {
let guid = this.engine._findDupe(item);
if (guid) {
item.id = guid;
}
return item;
}
clientUnderstands(item) {
return item.applicationID === Services.appinfo.ID;
}
syncedByClient(item) {
return !item.original.hidden &&
!item.original.isSystem &&
!(item.original.pendingOperations & AddonManager.PENDING_UNINSTALL) &&
this.engine.isAddonSyncable(item.original, true);
}
}

File diff suppressed because it is too large Load diff

View file

@ -2,57 +2,24 @@
* 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/. */
/**
* How does the clients engine work?
*
* - We use 2 files - commands.json and commands-syncing.json.
*
* - At sync upload time, we attempt a rename of commands.json to
* commands-syncing.json, and ignore errors (helps for crash during sync!).
* - We load commands-syncing.json and stash the contents in
* _currentlySyncingCommands which lives for the duration of the upload process.
* - We use _currentlySyncingCommands to build the outgoing records
* - Immediately after successful upload, we delete commands-syncing.json from
* disk (and clear _currentlySyncingCommands). We reconcile our local records
* with what we just wrote in the server, and add failed IDs commands
* back in commands.json
* - Any time we need to "save" a command for future syncs, we load
* commands.json, update it, and write it back out.
*/
this.EXPORTED_SYMBOLS = [
"ClientEngine",
"ClientsRec"
];
var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://services-common/async.js");
Cu.import("resource://services-common/stringbundle.js");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/record.js");
Cu.import("resource://services-sync/resource.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
"resource://gre/modules/FxAccounts.jsm");
const CLIENTS_TTL = 1814400; // 21 days
const CLIENTS_TTL_REFRESH = 604800; // 7 days
const STALE_CLIENT_REMOTE_AGE = 604800; // 7 days
const SUPPORTED_PROTOCOL_VERSIONS = ["1.1", "1.5"];
function hasDupeCommand(commands, action) {
if (!commands) {
return false;
}
return commands.some(other => other.command == action.command &&
Utils.deepEquals(other.args, action.args));
}
this.ClientsRec = function ClientsRec(collection, id) {
CryptoWrapper.call(this, collection, id);
}
@ -66,27 +33,23 @@ Utils.deferGetSet(ClientsRec,
"cleartext",
["name", "type", "commands",
"version", "protocols",
"formfactor", "os", "appPackage", "application", "device",
"fxaDeviceId"]);
"formfactor", "os", "appPackage", "application", "device"]);
this.ClientEngine = function ClientEngine(service) {
SyncEngine.call(this, "Clients", service);
// Reset the last sync timestamp on every startup so that we fetch all clients
this.resetLastSync();
// Reset the client on every startup so that we fetch recent clients
this._resetClient();
}
ClientEngine.prototype = {
__proto__: SyncEngine.prototype,
_storeObj: ClientStore,
_recordObj: ClientsRec,
_trackerObj: ClientsTracker,
allowSkippedRecord: false,
// Always sync client data as it controls other sync behavior
get enabled() {
return true;
},
get enabled() true,
get lastRecordUpload() {
return Svc.Prefs.get(this.name + ".lastRecordUpload", 0);
@ -95,31 +58,18 @@ ClientEngine.prototype = {
Svc.Prefs.set(this.name + ".lastRecordUpload", Math.floor(value));
},
get remoteClients() {
// return all non-stale clients for external consumption.
return Object.values(this._store._remoteClients).filter(v => !v.stale);
},
remoteClientExists(id) {
let client = this._store._remoteClients[id];
return !!(client && !client.stale);
},
// Aggregate some stats on the composition of clients on this account
get stats() {
let stats = {
hasMobile: this.localType == DEVICE_TYPE_MOBILE,
hasMobile: this.localType == "mobile",
names: [this.localName],
numClients: 1,
};
for (let id in this._store._remoteClients) {
let {name, type, stale} = this._store._remoteClients[id];
if (!stale) {
stats.hasMobile = stats.hasMobile || type == DEVICE_TYPE_MOBILE;
stats.names.push(name);
stats.numClients++;
}
for each (let {name, type} in this._store._remoteClients) {
stats.hasMobile = stats.hasMobile || type == "mobile";
stats.names.push(name);
stats.numClients++;
}
return stats;
@ -135,11 +85,7 @@ ClientEngine.prototype = {
counts.set(this.localType, 1);
for (let id in this._store._remoteClients) {
let record = this._store._remoteClients[id];
if (record.stale) {
continue; // pretend "stale" records don't exist.
}
for each (let record in this._store._remoteClients) {
let type = record.type;
if (!counts.has(type)) {
counts.set(type, 0);
@ -156,9 +102,7 @@ ClientEngine.prototype = {
let localID = Svc.Prefs.get("client.GUID", "");
return localID == "" ? this.localID = Utils.makeGUID() : localID;
},
set localID(value) {
Svc.Prefs.set("client.GUID", value);
},
set localID(value) Svc.Prefs.set("client.GUID", value),
get brandName() {
let brand = new StringBundle("chrome://branding/locale/brand.properties");
@ -166,97 +110,23 @@ ClientEngine.prototype = {
},
get localName() {
let name = Utils.getDeviceName();
// If `getDeviceName` returns the default name, set the pref. FxA registers
// the device before syncing, so we don't need to update the registration
// in this case.
Svc.Prefs.set("client.name", name);
return name;
},
set localName(value) {
Svc.Prefs.set("client.name", value);
// Update the registration in the background.
fxAccounts.updateDeviceRegistration().catch(error => {
this._log.warn("failed to update fxa device registration", error);
});
},
let localName = Svc.Prefs.get("client.name", "");
if (localName != "")
return localName;
get localType() {
return Utils.getDeviceType();
},
set localType(value) {
Svc.Prefs.set("client.type", value);
return this.localName = Utils.getDefaultDeviceName();
},
set localName(value) Svc.Prefs.set("client.name", value),
getClientName(id) {
if (id == this.localID) {
return this.localName;
}
let client = this._store._remoteClients[id];
return client ? client.name : "";
},
getClientFxaDeviceId(id) {
if (this._store._remoteClients[id]) {
return this._store._remoteClients[id].fxaDeviceId;
}
return null;
},
get localType() Svc.Prefs.get("client.type", "desktop"),
set localType(value) Svc.Prefs.set("client.type", value),
isMobile: function isMobile(id) {
if (this._store._remoteClients[id])
return this._store._remoteClients[id].type == DEVICE_TYPE_MOBILE;
return this._store._remoteClients[id].type == "mobile";
return false;
},
_readCommands() {
let cb = Async.makeSpinningCallback();
Utils.jsonLoad("commands", this, commands => cb(null, commands));
return cb.wait() || {};
},
/**
* Low level function, do not use directly (use _addClientCommand instead).
*/
_saveCommands(commands) {
let cb = Async.makeSpinningCallback();
Utils.jsonSave("commands", this, commands, error => {
if (error) {
this._log.error("Failed to save JSON outgoing commands", error);
}
cb();
});
cb.wait();
},
_prepareCommandsForUpload() {
let cb = Async.makeSpinningCallback();
Utils.jsonMove("commands", "commands-syncing", this).catch(() => {}) // Ignore errors
.then(() => {
Utils.jsonLoad("commands-syncing", this, commands => cb(null, commands));
});
return cb.wait() || {};
},
_deleteUploadedCommands() {
delete this._currentlySyncingCommands;
Async.promiseSpinningly(
Utils.jsonRemove("commands-syncing", this).catch(err => {
this._log.error("Failed to delete syncing-commands file", err);
})
);
},
_addClientCommand(clientId, command) {
const allCommands = this._readCommands();
const clientCommands = allCommands[clientId] || [];
if (hasDupeCommand(clientCommands, command)) {
return;
}
allCommands[clientId] = clientCommands.concat(command);
this._saveCommands(allCommands);
},
_syncStartup: function _syncStartup() {
// Reupload new client record periodically.
if (Date.now() / 1000 - this.lastRecordUpload > CLIENTS_TTL_REFRESH) {
@ -266,157 +136,9 @@ ClientEngine.prototype = {
SyncEngine.prototype._syncStartup.call(this);
},
_processIncoming() {
// Fetch all records from the server.
this.lastSync = 0;
this._incomingClients = {};
try {
SyncEngine.prototype._processIncoming.call(this);
// Since clients are synced unconditionally, any records in the local store
// that don't exist on the server must be for disconnected clients. Remove
// them, so that we don't upload records with commands for clients that will
// never see them. We also do this to filter out stale clients from the
// tabs collection, since showing their list of tabs is confusing.
for (let id in this._store._remoteClients) {
if (!this._incomingClients[id]) {
this._log.info(`Removing local state for deleted client ${id}`);
this._removeRemoteClient(id);
}
}
// Bug 1264498: Mobile clients don't remove themselves from the clients
// collection when the user disconnects Sync, so we mark as stale clients
// with the same name that haven't synced in over a week.
// (Note we can't simply delete them, or we re-apply them next sync - see
// bug 1287687)
delete this._incomingClients[this.localID];
let names = new Set([this.localName]);
for (let id in this._incomingClients) {
let record = this._store._remoteClients[id];
if (!names.has(record.name)) {
names.add(record.name);
continue;
}
let remoteAge = AsyncResource.serverTime - this._incomingClients[id];
if (remoteAge > STALE_CLIENT_REMOTE_AGE) {
this._log.info(`Hiding stale client ${id} with age ${remoteAge}`);
record.stale = true;
}
}
} finally {
this._incomingClients = null;
}
},
_uploadOutgoing() {
this._currentlySyncingCommands = this._prepareCommandsForUpload();
const clientWithPendingCommands = Object.keys(this._currentlySyncingCommands);
for (let clientId of clientWithPendingCommands) {
if (this._store._remoteClients[clientId] || this.localID == clientId) {
this._modified.set(clientId, 0);
}
}
SyncEngine.prototype._uploadOutgoing.call(this);
},
_onRecordsWritten(succeeded, failed) {
// Reconcile the status of the local records with what we just wrote on the
// server
for (let id of succeeded) {
const commandChanges = this._currentlySyncingCommands[id];
if (id == this.localID) {
if (this.localCommands) {
this.localCommands = this.localCommands.filter(command => !hasDupeCommand(commandChanges, command));
}
} else {
const clientRecord = this._store._remoteClients[id];
if (!commandChanges || !clientRecord) {
// should be impossible, else we wouldn't have been writing it.
this._log.warn("No command/No record changes for a client we uploaded");
continue;
}
// fixup the client record, so our copy of _remoteClients matches what we uploaded.
clientRecord.commands = this._store.createRecord(id);
// we could do better and pass the reference to the record we just uploaded,
// but this will do for now
}
}
// Re-add failed commands
for (let id of failed) {
const commandChanges = this._currentlySyncingCommands[id];
if (!commandChanges) {
continue;
}
this._addClientCommand(id, commandChanges);
}
this._deleteUploadedCommands();
// Notify other devices that their own client collection changed
const idsToNotify = succeeded.reduce((acc, id) => {
if (id == this.localID) {
return acc;
}
const fxaDeviceId = this.getClientFxaDeviceId(id);
return fxaDeviceId ? acc.concat(fxaDeviceId) : acc;
}, []);
if (idsToNotify.length > 0) {
this._notifyCollectionChanged(idsToNotify);
}
},
_notifyCollectionChanged(ids) {
const message = {
version: 1,
command: "sync:collection_changed",
data: {
collections: ["clients"]
}
};
fxAccounts.notifyDevices(ids, message, NOTIFY_TAB_SENT_TTL_SECS);
},
_syncFinish() {
// Record histograms for our device types, and also write them to a pref
// so non-histogram telemetry (eg, UITelemetry) has easy access to them.
for (let [deviceType, count] of this.deviceTypes) {
let hid;
let prefName = this.name + ".devices.";
switch (deviceType) {
case "desktop":
hid = "WEAVE_DEVICE_COUNT_DESKTOP";
prefName += "desktop";
break;
case "mobile":
hid = "WEAVE_DEVICE_COUNT_MOBILE";
prefName += "mobile";
break;
default:
this._log.warn(`Unexpected deviceType "${deviceType}" recording device telemetry.`);
continue;
}
Services.telemetry.getHistogramById(hid).add(count);
Svc.Prefs.set(prefName, count);
}
SyncEngine.prototype._syncFinish.call(this);
},
_reconcile: function _reconcile(item) {
// Every incoming record is reconciled, so we use this to track the
// contents of the collection on the server.
this._incomingClients[item.id] = item.modified;
if (!this._store.itemExists(item.id)) {
return true;
}
// Clients are synced unconditionally, so we'll always have new records.
// Unfortunately, this will cause the scheduler to use the immediate sync
// interval for the multi-device case, instead of the active interval. We
// work around this by updating the record during reconciliation, and
// returning false to indicate that the record doesn't need to be applied
// later.
this._store.update(item);
return false;
// Always process incoming items because they might have commands
_reconcile: function _reconcile() {
return true;
},
// Treat reset the same as wiping for locally cached clients
@ -426,13 +148,7 @@ ClientEngine.prototype = {
_wipeClient: function _wipeClient() {
SyncEngine.prototype._resetClient.call(this);
delete this.localCommands;
this._store.wipe();
const logRemoveError = err => this._log.warn("Could not delete json file", err);
Async.promiseSpinningly(
Utils.jsonRemove("commands", this).catch(logRemoveError)
.then(Utils.jsonRemove("commands-syncing", this).catch(logRemoveError))
);
},
removeClientData: function removeClientData() {
@ -470,6 +186,14 @@ ClientEngine.prototype = {
displayURI: { args: 3, desc: "Instruct a client to display a URI" },
},
/**
* Remove any commands for the local client and mark it for upload.
*/
clearCommands: function clearCommands() {
delete this.localCommands;
this._tracker.addChangedID(this.localID);
},
/**
* Sends a command+args pair to a specific client.
*
@ -484,17 +208,30 @@ ClientEngine.prototype = {
if (!client) {
throw new Error("Unknown remote client ID: '" + clientId + "'.");
}
if (client.stale) {
throw new Error("Stale remote client ID: '" + clientId + "'.");
}
// notDupe compares two commands and returns if they are not equal.
let notDupe = function(other) {
return other.command != command || !Utils.deepEquals(other.args, args);
};
let action = {
command: command,
args: args,
};
if (!client.commands) {
client.commands = [action];
}
// Add the new action if there are no duplicates.
else if (client.commands.every(notDupe)) {
client.commands.push(action);
}
// It must be a dupe. Skip.
else {
return;
}
this._log.trace("Client " + clientId + " got a new action: " + [command, args]);
this._addClientCommand(clientId, action);
this._tracker.addChangedID(clientId);
},
@ -505,17 +242,13 @@ ClientEngine.prototype = {
*/
processIncomingCommands: function processIncomingCommands() {
return this._notify("clients:process-commands", "", function() {
if (!this.localCommands) {
return true;
}
let commands = this.localCommands;
const clearedCommands = this._readCommands()[this.localID];
const commands = this.localCommands.filter(command => !hasDupeCommand(clearedCommands, command));
// Immediately clear out the commands as we've got them locally.
this.clearCommands();
let URIsToDisplay = [];
// Process each command in order.
for (let rawCommand of commands) {
let {command, args} = rawCommand;
for each (let {command, args} in commands) {
this._log.debug("Processing command: " + command + "(" + args + ")");
let engines = [args[0]];
@ -536,20 +269,12 @@ ClientEngine.prototype = {
this.service.logout();
return false;
case "displayURI":
let [uri, clientId, title] = args;
URIsToDisplay.push({ uri, clientId, title });
this._handleDisplayURI.apply(this, args);
break;
default:
this._log.debug("Received an unknown command: " + command);
break;
}
// Add the command to the "cleared" commands list
this._addClientCommand(this.localID, rawCommand)
}
this._tracker.addChangedID(this.localID);
if (URIsToDisplay.length) {
this._handleDisplayURIs(URIsToDisplay);
}
return true;
@ -588,10 +313,8 @@ ClientEngine.prototype = {
if (clientId) {
this._sendCommandToClient(command, args, clientId);
} else {
for (let [id, record] of Object.entries(this._store._remoteClients)) {
if (!record.stale) {
this._sendCommandToClient(command, args, id);
}
for (let id in this._store._remoteClients) {
this._sendCommandToClient(command, args, id);
}
}
},
@ -622,11 +345,11 @@ ClientEngine.prototype = {
},
/**
* Handle a bunch of received 'displayURI' commands.
* Handle a single received 'displayURI' command.
*
* Interested parties should observe the "weave:engine:clients:display-uris"
* topic. The callback will receive an array as the subject parameter
* containing objects with the following keys:
* Interested parties should observe the "weave:engine:clients:display-uri"
* topic. The callback will receive an object as the subject parameter with
* the following keys:
*
* uri URI (string) that is requested for display.
* clientId ID of client that sent the command.
@ -634,24 +357,21 @@ ClientEngine.prototype = {
*
* The 'data' parameter to the callback will not be defined.
*
* @param uris
* An array containing URI objects to display
* @param uris[].uri
* @param uri
* String URI that was received
* @param uris[].clientId
* @param clientId
* ID of client that sent URI
* @param uris[].title
* @param title
* String title of page that URI corresponds to. Older clients may not
* send this.
*/
_handleDisplayURIs: function _handleDisplayURIs(uris) {
Svc.Obs.notify("weave:engine:clients:display-uris", uris);
},
_handleDisplayURI: function _handleDisplayURI(uri, clientId, title) {
this._log.info("Received a URI for display: " + uri + " (" + title +
") from " + clientId);
_removeRemoteClient(id) {
delete this._store._remoteClients[id];
this._tracker.removeChangedID(id);
},
let subject = {uri: uri, client: clientId, title: title};
Svc.Obs.notify("weave:engine:clients:display-uri", subject);
}
};
function ClientStore(name, engine) {
@ -660,48 +380,29 @@ function ClientStore(name, engine) {
ClientStore.prototype = {
__proto__: Store.prototype,
_remoteClients: {},
create(record) {
this.update(record);
this.update(record)
},
update: function update(record) {
if (record.id == this.engine.localID) {
// Only grab commands from the server; local name/type always wins
// Only grab commands from the server; local name/type always wins
if (record.id == this.engine.localID)
this.engine.localCommands = record.commands;
} else {
else
this._remoteClients[record.id] = record.cleartext;
}
},
createRecord: function createRecord(id, collection) {
let record = new ClientsRec(collection, id);
const commandsChanges = this.engine._currentlySyncingCommands ?
this.engine._currentlySyncingCommands[id] :
[];
// Package the individual components into a record for the local client
if (id == this.engine.localID) {
let cb = Async.makeSpinningCallback();
fxAccounts.getDeviceId().then(id => cb(null, id), cb);
try {
record.fxaDeviceId = cb.wait();
} catch(error) {
this._log.warn("failed to get fxa device id", error);
}
record.name = this.engine.localName;
record.type = this.engine.localType;
record.commands = this.engine.localCommands;
record.version = Services.appinfo.version;
record.protocols = SUPPORTED_PROTOCOL_VERSIONS;
// Substract the commands we recorded that we've already executed
if (commandsChanges && commandsChanges.length &&
this.engine.localCommands && this.engine.localCommands.length) {
record.commands = this.engine.localCommands.filter(command => !hasDupeCommand(commandsChanges, command));
}
// Optional fields.
record.os = Services.appinfo.OS; // "Darwin"
record.appPackage = Services.appinfo.ID;
@ -712,20 +413,6 @@ ClientStore.prototype = {
// record.formfactor = ""; // Bug 1100722
} else {
record.cleartext = this._remoteClients[id];
// Add the commands we have to send
if (commandsChanges && commandsChanges.length) {
const recordCommands = record.cleartext.commands || [];
const newCommands = commandsChanges.filter(command => !hasDupeCommand(recordCommands, command));
record.cleartext.commands = recordCommands.concat(newCommands);
}
if (record.cleartext.stale) {
// It's almost certainly a logic error for us to upload a record we
// consider stale, so make log noise, but still remove the flag.
this._log.error(`Preparing to upload record ${id} that we consider stale`);
delete record.cleartext.stale;
}
}
return record;
@ -768,7 +455,7 @@ ClientsTracker.prototype = {
break;
case "weave:engine:stop-tracking":
if (this._enabled) {
Svc.Prefs.ignore("client.name", this);
Svc.Prefs.ignore("clients.name", this);
this._enabled = false;
}
break;

View file

@ -2,11 +2,11 @@
* 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 = ['FormEngine', 'FormRec', 'FormValidator'];
this.EXPORTED_SYMBOLS = ['FormEngine', 'FormRec'];
var Cc = Components.classes;
var Ci = Components.interfaces;
var Cu = Components.utils;
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://services-sync/engines.js");
@ -14,10 +14,9 @@ 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-sync/collection_validator.js");
Cu.import("resource://gre/modules/Log.jsm");
const FORMS_TTL = 3 * 365 * 24 * 60 * 60; // Three years in seconds.
const FORMS_TTL = 5184000; // 60 days
this.FormRec = function FormRec(collection, id) {
CryptoWrapper.call(this, collection, id);
@ -31,30 +30,26 @@ FormRec.prototype = {
Utils.deferGetSet(FormRec, "cleartext", ["name", "value"]);
var FormWrapper = {
let FormWrapper = {
_log: Log.repository.getLogger("Sync.Engine.Forms"),
_getEntryCols: ["fieldname", "value"],
_guidCols: ["guid"],
_promiseSearch: function(terms, searchData) {
return new Promise(resolve => {
let results = [];
let callbacks = {
handleResult(result) {
results.push(result);
},
handleCompletion(reason) {
resolve(results);
}
};
Svc.FormHistory.search(terms, searchData, callbacks);
})
},
// Do a "sync" search by spinning the event loop until it completes.
_searchSpinningly: function(terms, searchData) {
return Async.promiseSpinningly(this._promiseSearch(terms, searchData));
let results = [];
let cb = Async.makeSpinningCallback();
let callbacks = {
handleResult: function(result) {
results.push(result);
},
handleCompletion: function(reason) {
cb(null, results);
}
};
Svc.FormHistory.search(terms, searchData, callbacks);
return cb.wait();
},
_updateSpinningly: function(changes) {
@ -114,9 +109,7 @@ FormEngine.prototype = {
syncPriority: 6,
get prefName() {
return "history";
},
get prefName() "history",
_findDupe: function _findDupe(item) {
return FormWrapper.getGUID(item.name, item.value);
@ -232,9 +225,7 @@ FormTracker.prototype = {
observe: function (subject, topic, data) {
Tracker.prototype.observe.call(this, subject, topic, data);
if (this.ignoreAll) {
return;
}
switch (topic) {
case "satchel-storage-changed":
if (data == "formhistory-add" || data == "formhistory-remove") {
@ -250,56 +241,3 @@ FormTracker.prototype = {
this.score += SCORE_INCREMENT_MEDIUM;
},
};
class FormsProblemData extends CollectionProblemData {
getSummary() {
// We don't support syncing deleted form data, so "clientMissing" isn't a problem
return super.getSummary().filter(entry =>
entry.name !== "clientMissing");
}
}
class FormValidator extends CollectionValidator {
constructor() {
super("forms", "id", ["name", "value"]);
}
emptyProblemData() {
return new FormsProblemData();
}
getClientItems() {
return FormWrapper._promiseSearch(["guid", "fieldname", "value"], {});
}
normalizeClientItem(item) {
return {
id: item.guid,
guid: item.guid,
name: item.fieldname,
fieldname: item.fieldname,
value: item.value,
original: item,
};
}
normalizeServerItem(item) {
let res = Object.assign({
guid: item.id,
fieldname: item.name,
original: item,
}, item);
// Missing `name` or `value` causes the getGUID call to throw
if (item.name !== undefined && item.value !== undefined) {
let guid = FormWrapper.getGUID(item.name, item.value);
if (guid) {
res.guid = guid;
res.id = guid;
res.duped = true;
}
}
return res;
}
}

View file

@ -4,10 +4,10 @@
this.EXPORTED_SYMBOLS = ['HistoryEngine', 'HistoryRec'];
var Cc = Components.classes;
var Ci = Components.interfaces;
var Cu = Components.utils;
var Cr = Components.results;
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const Cr = Components.results;
const HISTORY_TTL = 5184000; // 60 days
@ -44,25 +44,6 @@ HistoryEngine.prototype = {
applyIncomingBatchSize: HISTORY_STORE_BATCH_SIZE,
syncPriority: 7,
_processIncoming: function (newitems) {
// We want to notify history observers that a batch operation is underway
// so they don't do lots of work for each incoming record.
let observers = PlacesUtils.history.getObservers();
function notifyHistoryObservers(notification) {
for (let observer of observers) {
try {
observer[notification]();
} catch (ex) { }
}
}
notifyHistoryObservers("onBeginUpdateBatch");
try {
return SyncEngine.prototype._processIncoming.call(this, newitems);
} finally {
notifyHistoryObservers("onEndUpdateBatch");
}
},
};
function HistoryStore(name, engine) {
@ -70,8 +51,7 @@ function HistoryStore(name, engine) {
// Explicitly nullify our references to our cached services so we don't leak
Svc.Obs.add("places-shutdown", function() {
for (let query in this._stmts) {
let stmt = this._stmts;
for each ([query, stmt] in Iterator(this._stmts)) {
stmt.finalize();
}
this._stmts = {};
@ -105,7 +85,7 @@ HistoryStore.prototype = {
return this._getStmt(
"UPDATE moz_places " +
"SET guid = :guid " +
"WHERE url_hash = hash(:page_url) AND url = :page_url");
"WHERE url = :page_url");
},
// Some helper functions to handle GUIDs
@ -127,7 +107,7 @@ HistoryStore.prototype = {
return this._getStmt(
"SELECT guid " +
"FROM moz_places " +
"WHERE url_hash = hash(:page_url) AND url = :page_url");
"WHERE url = :page_url");
},
_guidCols: ["guid"],
@ -146,12 +126,12 @@ HistoryStore.prototype = {
},
get _visitStm() {
return this._getStmt(`/* do not warn (bug 599936) */
SELECT visit_type type, visit_date date
FROM moz_historyvisits
JOIN moz_places h ON h.id = place_id
WHERE url_hash = hash(:url) AND url = :url
ORDER BY date DESC LIMIT 20`);
return this._getStmt(
"/* do not warn (bug 599936) */ " +
"SELECT visit_type type, visit_date date " +
"FROM moz_historyvisits " +
"WHERE place_id = (SELECT id FROM moz_places WHERE url = :url) " +
"ORDER BY date DESC LIMIT 10");
},
_visitCols: ["date", "type"],
@ -223,10 +203,7 @@ HistoryStore.prototype = {
} else {
shouldApply = this._recordToPlaceInfo(record);
}
} catch (ex) {
if (Async.isShutdownException(ex)) {
throw ex;
}
} catch(ex) {
failed.push(record.id);
shouldApply = false;
}
@ -299,14 +276,14 @@ HistoryStore.prototype = {
if (!visit.date || typeof visit.date != "number") {
this._log.warn("Encountered record with invalid visit date: "
+ visit.date);
continue;
throw "Visit has no date!";
}
if (!visit.type ||
!Object.values(PlacesUtils.history.TRANSITIONS).includes(visit.type)) {
this._log.warn("Encountered record with invalid visit type: " +
visit.type + "; ignoring.");
continue;
if (!visit.type || !(visit.type >= PlacesUtils.history.TRANSITION_LINK &&
visit.type <= PlacesUtils.history.TRANSITION_FRAMED_LINK)) {
this._log.warn("Encountered record with invalid visit type: "
+ visit.type);
throw "Invalid visit type!";
}
// Dates need to be integers.
@ -317,7 +294,6 @@ HistoryStore.prototype = {
// overwritten.
continue;
}
visit.visitDate = visit.date;
visit.transitionType = visit.type;
k += 1;
@ -369,9 +345,7 @@ HistoryStore.prototype = {
},
wipe: function HistStore_wipe() {
let cb = Async.makeSyncCallback();
PlacesUtils.history.clear().then(result => {cb(null, result)}, err => {cb(err)});
return Async.waitForSyncCallback(cb);
PlacesUtils.history.removeAllPages();
}
};

View file

@ -2,16 +2,14 @@
* 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 = ['PasswordEngine', 'LoginRec', 'PasswordValidator'];
this.EXPORTED_SYMBOLS = ['PasswordEngine', 'LoginRec'];
var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://services-sync/record.js");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/collection_validator.js");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://services-common/async.js");
this.LoginRec = function LoginRec(collection, id) {
CryptoWrapper.call(this, collection, id);
@ -24,7 +22,6 @@ LoginRec.prototype = {
Utils.deferGetSet(LoginRec, "cleartext", [
"hostname", "formSubmitURL",
"httpRealm", "username", "password", "usernameField", "passwordField",
"timeCreated", "timePasswordChanged",
]);
@ -70,10 +67,7 @@ PasswordEngine.prototype = {
Svc.Prefs.set("deletePwdFxA", true);
Svc.Prefs.reset("deletePwd"); // The old prefname we previously used.
} catch (ex) {
if (Async.isShutdownException(ex)) {
throw ex;
}
this._log.debug("Password deletes failed", ex);
this._log.debug("Password deletes failed: " + Utils.exceptionStr(ex));
}
}
},
@ -89,7 +83,7 @@ PasswordEngine.prototype = {
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 (let local of logins) {
for each (let local in logins) {
if (login.matches(local, true) && local instanceof Ci.nsILoginMetaInfo) {
return local.guid;
}
@ -104,13 +98,6 @@ function PasswordStore(name, engine) {
PasswordStore.prototype = {
__proto__: Store.prototype,
_newPropertyBag: function () {
return Cc["@mozilla.org/hash-property-bag;1"].createInstance(Ci.nsIWritablePropertyBag2);
},
/**
* Return an instance of nsILoginInfo (and, implicitly, nsILoginMetaInfo).
*/
_nsLoginInfoFromRecord: function (record) {
function nullUndefined(x) {
return (x == undefined) ? null : x;
@ -131,21 +118,13 @@ PasswordStore.prototype = {
record.password,
record.usernameField,
record.passwordField);
info.QueryInterface(Ci.nsILoginMetaInfo);
info.guid = record.id;
if (record.timeCreated) {
info.timeCreated = record.timeCreated;
}
if (record.timePasswordChanged) {
info.timePasswordChanged = record.timePasswordChanged;
}
return info;
},
_getLoginFromGUID: function (id) {
let prop = this._newPropertyBag();
let prop = Cc["@mozilla.org/hash-property-bag;1"].createInstance(Ci.nsIWritablePropertyBag2);
prop.setPropertyAsAUTF8String("guid", id);
let logins = Services.logins.searchLogins({}, prop);
@ -190,7 +169,8 @@ PasswordStore.prototype = {
return;
}
let prop = this._newPropertyBag();
let prop = Cc["@mozilla.org/hash-property-bag;1"]
.createInstance(Ci.nsIWritablePropertyBag2);
prop.setPropertyAsAUTF8String("guid", newID);
Services.logins.modifyLogin(oldLogin, prop);
@ -217,11 +197,6 @@ PasswordStore.prototype = {
record.usernameField = login.usernameField;
record.passwordField = login.passwordField;
// Optional fields.
login.QueryInterface(Ci.nsILoginMetaInfo);
record.timeCreated = login.timeCreated;
record.timePasswordChanged = login.timePasswordChanged;
return record;
},
@ -237,7 +212,8 @@ PasswordStore.prototype = {
try {
Services.logins.addLogin(login);
} catch(ex) {
this._log.debug(`Adding record ${record.id} resulted in exception`, ex);
this._log.debug("Adding record " + record.id +
" resulted in exception " + Utils.exceptionStr(ex));
}
},
@ -269,7 +245,9 @@ PasswordStore.prototype = {
try {
Services.logins.modifyLogin(loginItem, newinfo);
} catch(ex) {
this._log.debug(`Modifying record ${record.id} resulted in exception; not modifying`, ex);
this._log.debug("Modifying record " + record.id +
" resulted in exception " + Utils.exceptionStr(ex) +
". Not modifying.");
}
},
@ -326,46 +304,3 @@ PasswordTracker.prototype = {
}
},
};
class PasswordValidator extends CollectionValidator {
constructor() {
super("passwords", "id", [
"hostname",
"formSubmitURL",
"httpRealm",
"password",
"passwordField",
"username",
"usernameField",
]);
}
getClientItems() {
let logins = Services.logins.getAllLogins({});
let syncHosts = Utils.getSyncCredentialsHosts()
let result = logins.map(l => l.QueryInterface(Ci.nsILoginMetaInfo))
.filter(l => !syncHosts.has(l.hostname));
return Promise.resolve(result);
}
normalizeClientItem(item) {
return {
id: item.guid,
guid: item.guid,
hostname: item.hostname,
formSubmitURL: item.formSubmitURL,
httpRealm: item.httpRealm,
password: item.password,
passwordField: item.passwordField,
username: item.username,
usernameField: item.usernameField,
original: item,
}
}
normalizeServerItem(item) {
return Object.assign({ guid: item.id }, item);
}
}

View file

@ -4,11 +4,11 @@
this.EXPORTED_SYMBOLS = ['PrefsEngine', 'PrefRec'];
var Cc = Components.classes;
var Ci = Components.interfaces;
var Cu = Components.utils;
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const PREF_SYNC_PREFS_PREFIX = "services.sync.prefs.sync.";
const SYNC_PREFS_PREFIX = "services.sync.prefs.sync.";
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/record.js");
@ -42,7 +42,6 @@ PrefsEngine.prototype = {
version: 2,
syncPriority: 1,
allowSkippedRecord: false,
getChangedIDs: function () {
// No need for a proper timestamp (no conflict resolution needed).
@ -88,45 +87,37 @@ PrefStore.prototype = {
_getSyncPrefs: function () {
let syncPrefs = Cc["@mozilla.org/preferences-service;1"]
.getService(Ci.nsIPrefService)
.getBranch(PREF_SYNC_PREFS_PREFIX)
.getBranch(SYNC_PREFS_PREFIX)
.getChildList("", {});
// Also sync preferences that determine which prefs get synced.
let controlPrefs = syncPrefs.map(pref => PREF_SYNC_PREFS_PREFIX + pref);
let controlPrefs = syncPrefs.map(pref => SYNC_PREFS_PREFIX + pref);
return controlPrefs.concat(syncPrefs);
},
_isSynced: function (pref) {
return pref.startsWith(PREF_SYNC_PREFS_PREFIX) ||
this._prefs.get(PREF_SYNC_PREFS_PREFIX + pref, false);
return pref.startsWith(SYNC_PREFS_PREFIX) ||
this._prefs.get(SYNC_PREFS_PREFIX + pref, false);
},
_getAllPrefs: function () {
let values = {};
for (let pref of this._getSyncPrefs()) {
for each (let pref in this._getSyncPrefs()) {
if (this._isSynced(pref)) {
// Missing and default prefs get the null value.
values[pref] = this._prefs.isSet(pref) ? this._prefs.get(pref, null) : null;
// Missing prefs get the null value.
values[pref] = this._prefs.get(pref, null);
}
}
return values;
},
_updateLightWeightTheme (themeID) {
let themeObject = null;
if (themeID) {
themeObject = LightweightThemeManager.getUsedTheme(themeID);
}
LightweightThemeManager.currentTheme = themeObject;
},
_setAllPrefs: function (values) {
let selectedThemeIDPref = "lightweightThemes.selectedThemeID";
let selectedThemeIDBefore = this._prefs.get(selectedThemeIDPref, null);
let selectedThemeIDAfter = selectedThemeIDBefore;
let enabledPref = "lightweightThemes.isThemeSelected";
let enabledBefore = this._prefs.get(enabledPref, false);
let prevTheme = LightweightThemeManager.currentTheme;
// Update 'services.sync.prefs.sync.foo.pref' before 'foo.pref', otherwise
// _isSynced returns false when 'foo.pref' doesn't exist (e.g., on a new device).
let prefs = Object.keys(values).sort(a => -a.indexOf(PREF_SYNC_PREFS_PREFIX));
let prefs = Object.keys(values).sort(a => -a.indexOf(SYNC_PREFS_PREFIX));
for (let pref of prefs) {
if (!this._isSynced(pref)) {
continue;
@ -134,30 +125,26 @@ PrefStore.prototype = {
let value = values[pref];
switch (pref) {
// Some special prefs we don't want to set directly.
case selectedThemeIDPref:
selectedThemeIDAfter = value;
break;
// default is to just set the pref
default:
if (value == null) {
// Pref has gone missing. The best we can do is reset it.
this._prefs.reset(pref);
} else {
try {
this._prefs.set(pref, value);
} catch(ex) {
this._log.trace("Failed to set pref: " + pref + ": " + ex);
}
}
// Pref has gone missing. The best we can do is reset it.
if (value == null) {
this._prefs.reset(pref);
continue;
}
try {
this._prefs.set(pref, value);
} catch(ex) {
this._log.trace("Failed to set pref: " + pref + ": " + ex);
}
}
// Notify the lightweight theme manager if the selected theme has changed.
if (selectedThemeIDBefore != selectedThemeIDAfter) {
this._updateLightWeightTheme(selectedThemeIDAfter);
// Notify the lightweight theme manager of all the new values
let enabledNow = this._prefs.get(enabledPref, false);
if (enabledBefore && !enabledNow) {
LightweightThemeManager.currentTheme = null;
} else if (enabledNow && LightweightThemeManager.usedThemes[0] != prevTheme) {
LightweightThemeManager.currentTheme = null;
LightweightThemeManager.currentTheme = LightweightThemeManager.usedThemes[0];
}
},
@ -261,8 +248,8 @@ PrefTracker.prototype = {
case "nsPref:changed":
// Trigger a sync for MULTI-DEVICE for a change that determines
// which prefs are synced or a regular pref change.
if (data.indexOf(PREF_SYNC_PREFS_PREFIX) == 0 ||
this._prefs.get(PREF_SYNC_PREFS_PREFIX + data, false)) {
if (data.indexOf(SYNC_PREFS_PREFIX) == 0 ||
this._prefs.get(SYNC_PREFS_PREFIX + data, false)) {
this.score += SCORE_INCREMENT_XLARGE;
this.modified = true;
this._log.trace("Preference " + data + " changed");

View file

@ -4,7 +4,7 @@
this.EXPORTED_SYMBOLS = ["TabEngine", "TabSetRecord"];
var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
const TABS_TTL = 604800; // 7 days.
const TAB_ENTRIES_LIMIT = 25; // How many URLs to include in tab history.
@ -43,11 +43,6 @@ TabEngine.prototype = {
_storeObj: TabStore,
_trackerObj: TabTracker,
_recordObj: TabSetRecord,
// A flag to indicate if we have synced in this session. This is to help
// consumers of remote tabs that may want to differentiate between "I've an
// empty tab list as I haven't yet synced" vs "I've an empty tab list
// as there really are no tabs"
hasSyncedThisSession: false,
syncPriority: 3,
@ -72,7 +67,6 @@ TabEngine.prototype = {
SyncEngine.prototype._resetClient.call(this);
this._store.wipe();
this._tracker.modified = true;
this.hasSyncedThisSession = false;
},
removeClientData: function () {
@ -100,12 +94,7 @@ TabEngine.prototype = {
}
return SyncEngine.prototype._reconcile.call(this, item);
},
_syncFinish() {
this.hasSyncedThisSession = true;
return SyncEngine.prototype._syncFinish.call(this);
},
}
};
@ -145,7 +134,7 @@ TabStore.prototype = {
}
for (let tab of win.gBrowser.tabs) {
let tabState = this.getTabState(tab);
tabState = this.getTabState(tab);
// Make sure there are history entries to look at.
if (!tabState || !tabState.entries.length) {
@ -165,11 +154,6 @@ TabStore.prototype = {
continue;
}
if (current.url.length >= (MAX_UPLOAD_BYTES - 1000)) {
this._log.trace("Skipping over-long 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.
@ -189,9 +173,7 @@ TabStore.prototype = {
allTabs.push({
title: current.title || "",
urlHistory: urls,
icon: tabState.image ||
(tabState.attributes && tabState.attributes.image) ||
"",
icon: tabState.attributes && tabState.attributes.image || "",
lastUsed: Math.floor((tabState.lastAccessed || 0) / 1000),
});
}
@ -265,9 +247,27 @@ TabStore.prototype = {
create: function (record) {
this._log.debug("Adding remote tabs from " + record.clientName);
this._remoteClients[record.id] = Object.assign({}, record.cleartext, {
lastModified: record.modified
});
this._remoteClients[record.id] = record.cleartext;
// 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) {
Svc.Prefs.set("notifyTabState", roundModify);
return;
}
// 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 (record) {
@ -302,14 +302,10 @@ TabTracker.prototype = {
_registerListenersForWindow: function (window) {
this._log.trace("Registering tab listeners in window");
for (let topic of this._topics) {
for each (let topic in this._topics) {
window.addEventListener(topic, this.onTab, false);
}
window.addEventListener("unload", this._unregisterListeners, false);
// If it's got a tab browser we can listen for things like navigation.
if (window.gBrowser) {
window.gBrowser.addProgressListener(this);
}
},
_unregisterListeners: function (event) {
@ -319,12 +315,9 @@ TabTracker.prototype = {
_unregisterListenersForWindow: function (window) {
this._log.trace("Removing tab listeners in window");
window.removeEventListener("unload", this._unregisterListeners, false);
for (let topic of this._topics) {
for each (let topic in this._topics) {
window.removeEventListener(topic, this.onTab, false);
}
if (window.gBrowser) {
window.gBrowser.removeProgressListener(this);
}
},
startTracking: function () {
@ -380,14 +373,4 @@ TabTracker.prototype = {
this.score += SCORE_INCREMENT_SMALL;
}
},
// web progress listeners.
onLocationChange: function (webProgress, request, location, flags) {
// We only care about top-level location changes which are not in the same
// document.
if (webProgress.isTopLevel &&
((flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) == 0)) {
this.modified = true;
}
},
};

View file

@ -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);
}
},
});

View file

@ -6,14 +6,13 @@
this.EXPORTED_SYMBOLS = ["IdentityManager"];
var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
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://gre/modules/Log.jsm");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://services-common/async.js");
// Lazy import to prevent unnecessary load on startup.
for (let symbol of ["BulkKeyBundle", "SyncKeyBundle"]) {
@ -85,14 +84,18 @@ IdentityManager.prototype = {
_syncKeyBundle: null,
/**
* Initialize the identity provider.
* 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();
},
/**
@ -111,6 +114,14 @@ IdentityManager.prototype = {
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);
},
@ -195,7 +206,7 @@ IdentityManager.prototype = {
return null;
}
for (let login of this._getLogins(PWDMGR_PASSWORD_REALM)) {
for each (let login in this._getLogins(PWDMGR_PASSWORD_REALM)) {
if (login.username.toLowerCase() == username) {
// It should already be UTF-8 encoded, but we don't take any chances.
this._basicPassword = Utils.encodeUTF8(login.password);
@ -249,7 +260,7 @@ IdentityManager.prototype = {
return null;
}
for (let login of this._getLogins(PWDMGR_PASSPHRASE_REALM)) {
for each (let login in this._getLogins(PWDMGR_PASSPHRASE_REALM)) {
if (login.username.toLowerCase() == username) {
this._syncKey = login.password;
}
@ -326,7 +337,7 @@ IdentityManager.prototype = {
try {
this._syncKeyBundle = new SyncKeyBundle(this.username, this.syncKey);
} catch (ex) {
this._log.warn("Failed to create sync bundle", ex);
this._log.warn(Utils.exceptionStr(ex));
return null;
}
}
@ -400,7 +411,7 @@ IdentityManager.prototype = {
this._setLogin(PWDMGR_PASSWORD_REALM, this.username,
this._basicPassword);
} else {
for (let login of this._getLogins(PWDMGR_PASSWORD_REALM)) {
for each (let login in this._getLogins(PWDMGR_PASSWORD_REALM)) {
Services.logins.removeLogin(login);
}
}
@ -412,7 +423,7 @@ IdentityManager.prototype = {
if (this._syncKey) {
this._setLogin(PWDMGR_PASSPHRASE_REALM, this.username, this._syncKey);
} else {
for (let login of this._getLogins(PWDMGR_PASSPHRASE_REALM)) {
for each (let login in this._getLogins(PWDMGR_PASSPHRASE_REALM)) {
Services.logins.removeLogin(login);
}
}
@ -447,9 +458,6 @@ IdentityManager.prototype = {
try {
service.recordManager.get(service.storageURL + "meta/fxa_credentials");
} catch (ex) {
if (Async.isShutdownException(ex)) {
throw ex;
}
this._log.warn("Failed to pre-fetch the migration sentinel", ex);
}
},
@ -469,7 +477,7 @@ IdentityManager.prototype = {
*/
_setLogin: function _setLogin(realm, username, password) {
let exists = false;
for (let login of this._getLogins(realm)) {
for each (let login in this._getLogins(realm)) {
if (login.username == username && login.password == password) {
exists = true;
} else {
@ -505,7 +513,7 @@ IdentityManager.prototype = {
deleteSyncCredentials: function deleteSyncCredentials() {
for (let host of this._getSyncCredentialsHosts()) {
let logins = Services.logins.findLogins({}, host, "", "");
for (let login of logins) {
for each (let login in logins) {
Services.logins.removeLogin(login);
}
}
@ -593,13 +601,4 @@ IdentityManager.prototype = {
// Do nothing for Sync 1.1.
return {accepted: true};
},
// Tell Sync what the login status should be if it saw a 401 fetching
// info/collections as part of login verification (typically immediately
// after login.)
// In our case it means an authoritative "password is incorrect".
loginStatusFromVerification404() {
return LOGIN_FAILED_LOGIN_REJECTED;
}
};

View file

@ -4,7 +4,7 @@
this.EXPORTED_SYMBOLS = ["JPAKEClient", "SendCredentialsController"];
var {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-common/rest.js");
@ -281,7 +281,8 @@ JPAKEClient.prototype = {
let rng = Cc["@mozilla.org/security/random-generator;1"]
.createInstance(Ci.nsIRandomGenerator);
let bytes = rng.generateRandomBytes(JPAKE_LENGTH_CLIENTID / 2);
this._clientID = bytes.map(byte => ("0" + byte.toString(16)).slice(-2)).join("");
this._clientID = [("0" + byte.toString(16)).slice(-2)
for each (byte in bytes)].join("");
},
_createSecret: function _createSecret() {
@ -290,7 +291,8 @@ JPAKEClient.prototype = {
let rng = Cc["@mozilla.org/security/random-generator;1"]
.createInstance(Ci.nsIRandomGenerator);
let bytes = rng.generateRandomBytes(JPAKE_LENGTH_SECRET);
return bytes.map(byte => key[Math.floor(byte * key.length / 256)]).join("");
return [key[Math.floor(byte * key.length / 256)]
for each (byte in bytes)].join("");
},
_newRequest: function _newRequest(uri) {

View file

@ -9,7 +9,7 @@ this.EXPORTED_SYMBOLS = [
"SyncKeyBundle"
];
var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://gre/modules/Log.jsm");

View file

@ -6,7 +6,7 @@ this.EXPORTED_SYMBOLS = ['Weave'];
this.Weave = {};
Components.utils.import("resource://services-sync/constants.js", Weave);
var lazies = {
let lazies = {
"jpakeclient.js": ["JPAKEClient", "SendCredentialsController"],
"notifications.js": ["Notifications", "Notification", "NotificationButton"],
"service.js": ["Service"],
@ -15,14 +15,12 @@ var lazies = {
};
function lazyImport(module, dest, props) {
function getter(prop) {
return function() {
let ns = {};
Components.utils.import(module, ns);
delete dest[prop];
return dest[prop] = ns[prop];
};
}
function getter(prop) function() {
let ns = {};
Components.utils.import(module, ns);
delete dest[prop];
return dest[prop] = ns[prop];
};
props.forEach(function (prop) { dest.__defineGetter__(prop, getter(prop)); });
}

View file

@ -4,10 +4,10 @@
this.EXPORTED_SYMBOLS = ["Notifications", "Notification", "NotificationButton"];
var Cc = Components.classes;
var Ci = Components.interfaces;
var Cr = Components.results;
var Cu = Components.utils;
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
Cu.import("resource://services-common/observers.js");
Cu.import("resource://gre/modules/Log.jsm");

View file

@ -7,26 +7,16 @@ this.EXPORTED_SYMBOLS = [
"SyncScheduler",
];
var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
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/util.js");
Cu.import("resource://services-common/logmanager.js");
Cu.import("resource://services-common/async.js");
XPCOMUtils.defineLazyModuleGetter(this, "Status",
"resource://services-sync/status.js");
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
// Get the value for an interval that's stored in preferences. To save users
// from themselves (and us from them!) the minimum time they can specify
// is 60s.
function getThrottledIntervalPreference(prefName) {
return Math.max(Svc.Prefs.get(prefName), 60) * 1000;
}
this.SyncScheduler = function SyncScheduler(service) {
this.service = service;
@ -55,12 +45,12 @@ SyncScheduler.prototype = {
let part = service.fxAccountsEnabled ? "fxa" : "sync11";
let prefSDInterval = "scheduler." + part + ".singleDeviceInterval";
this.singleDeviceInterval = getThrottledIntervalPreference(prefSDInterval);
this.singleDeviceInterval = Svc.Prefs.get(prefSDInterval) * 1000;
this.idleInterval = getThrottledIntervalPreference("scheduler.idleInterval");
this.activeInterval = getThrottledIntervalPreference("scheduler.activeInterval");
this.immediateInterval = getThrottledIntervalPreference("scheduler.immediateInterval");
this.eolInterval = getThrottledIntervalPreference("scheduler.eolInterval");
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;
@ -71,40 +61,20 @@ SyncScheduler.prototype = {
},
// nextSync is in milliseconds, but prefs can't hold that much
get nextSync() {
return Svc.Prefs.get("nextSync", 0) * 1000;
},
set nextSync(value) {
Svc.Prefs.set("nextSync", Math.floor(value / 1000));
},
get nextSync() Svc.Prefs.get("nextSync", 0) * 1000,
set nextSync(value) Svc.Prefs.set("nextSync", Math.floor(value / 1000)),
get syncInterval() {
return Svc.Prefs.get("syncInterval", this.singleDeviceInterval);
},
set syncInterval(value) {
Svc.Prefs.set("syncInterval", value);
},
get syncInterval() Svc.Prefs.get("syncInterval", this.singleDeviceInterval),
set syncInterval(value) Svc.Prefs.set("syncInterval", value),
get syncThreshold() {
return Svc.Prefs.get("syncThreshold", SINGLE_USER_THRESHOLD);
},
set syncThreshold(value) {
Svc.Prefs.set("syncThreshold", value);
},
get syncThreshold() Svc.Prefs.get("syncThreshold", SINGLE_USER_THRESHOLD),
set syncThreshold(value) Svc.Prefs.set("syncThreshold", value),
get globalScore() {
return Svc.Prefs.get("globalScore", 0);
},
set globalScore(value) {
Svc.Prefs.set("globalScore", value);
},
get globalScore() Svc.Prefs.get("globalScore", 0),
set globalScore(value) Svc.Prefs.set("globalScore", value),
get numClients() {
return Svc.Prefs.get("numClients", 0);
},
set numClients(value) {
Svc.Prefs.set("numClients", value);
},
get numClients() Svc.Prefs.get("numClients", 0),
set numClients(value) Svc.Prefs.set("numClients", value),
init: function init() {
this._log.level = Log.Level[Svc.Prefs.get("log.logger.service.main")];
@ -249,10 +219,7 @@ SyncScheduler.prototype = {
this.setDefaults();
try {
Svc.Idle.removeIdleObserver(this, Svc.Prefs.get("scheduler.idleTime"));
} catch (ex) {
if (ex.result != Cr.NS_ERROR_FAILURE) {
throw ex;
}
} catch (ex if (ex.result == Cr.NS_ERROR_FAILURE)) {
// In all likelihood we didn't have an idle observer registered yet.
// It's all good.
}
@ -285,11 +252,10 @@ SyncScheduler.prototype = {
case "wake_notification":
this._log.debug("Woke from sleep.");
Utils.nextTick(() => {
// Trigger a sync if we have multiple clients. We give it 5 seconds
// incase the network is still in the process of coming back up.
// Trigger a sync if we have multiple clients.
if (this.numClients > 1) {
this._log.debug("More than 1 client. Will sync in 5s.");
this.scheduleNextSync(5000);
this._log.debug("More than 1 client. Syncing.");
this.scheduleNextSync(0);
}
});
break;
@ -531,6 +497,45 @@ SyncScheduler.prototype = {
this.syncTimer.clear();
},
/**
* 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) {
@ -570,10 +575,7 @@ ErrorHandler.prototype = {
root.level = Log.Level[Svc.Prefs.get("log.rootLogger")];
let logs = ["Sync", "FirefoxAccounts", "Hawk", "Common.TokenServerClient",
"Sync.SyncMigration", "browserwindow.syncui",
"Services.Common.RESTRequest", "Services.Common.RESTRequest",
"BookmarkSyncUtils"
];
"Sync.SyncMigration"];
this._logManager = new LogManager(Svc.Prefs, logs, "sync");
},
@ -590,25 +592,17 @@ ErrorHandler.prototype = {
this._log.debug(data + " failed to apply some records.");
}
break;
case "weave:engine:sync:error": {
case "weave:engine:sync:error":
let exception = subject; // exception thrown by engine's sync() method
let engine_name = data; // engine name that threw the exception
this.checkServerError(exception);
Status.engines = [engine_name, exception.failureCode || ENGINE_UNKNOWN_FAIL];
if (Async.isShutdownException(exception)) {
this._log.debug(engine_name + " was interrupted due to the application shutting down");
} else {
this._log.debug(engine_name + " failed", exception);
Services.telemetry.getKeyedHistogramById("WEAVE_ENGINE_SYNC_ERRORS")
.add(engine_name);
}
this._log.debug(engine_name + " failed: " + Utils.exceptionStr(exception));
break;
}
case "weave:service:login:error":
this._log.error("Sync encountered a login error");
this.resetFileLog();
this.resetFileLog(this._logManager.REASON_ERROR);
if (this.shouldReportError()) {
this.notifyOnNextTick("weave:ui:login:error");
@ -618,23 +612,12 @@ ErrorHandler.prototype = {
this.dontIgnoreErrors = false;
break;
case "weave:service:sync:error": {
case "weave:service:sync:error":
if (Status.sync == CREDENTIALS_CHANGED) {
this.service.logout();
}
let exception = subject;
if (Async.isShutdownException(exception)) {
// If we are shutting down we just log the fact, attempt to flush
// the log file and get out of here!
this._log.error("Sync was interrupted due to the application shutting down");
this.resetFileLog();
break;
}
// Not a shutdown related exception...
this._log.error("Sync encountered an error", exception);
this.resetFileLog();
this.resetFileLog(this._logManager.REASON_ERROR);
if (this.shouldReportError()) {
this.notifyOnNextTick("weave:ui:sync:error");
@ -644,7 +627,6 @@ ErrorHandler.prototype = {
this.dontIgnoreErrors = false;
break;
}
case "weave:service:sync:finish":
this._log.trace("Status.service is " + Status.service);
@ -660,8 +642,8 @@ ErrorHandler.prototype = {
}
if (Status.service == SYNC_FAILED_PARTIAL) {
this._log.error("Some engines did not sync correctly.");
this.resetFileLog();
this._log.debug("Some engines did not sync correctly.");
this.resetFileLog(this._logManager.REASON_ERROR);
if (this.shouldReportError()) {
this.dontIgnoreErrors = false;
@ -669,7 +651,7 @@ ErrorHandler.prototype = {
break;
}
} else {
this.resetFileLog();
this.resetFileLog(this._logManager.REASON_SUCCESS);
}
this.dontIgnoreErrors = false;
this.notifyOnNextTick("weave:ui:sync:finish");
@ -696,52 +678,22 @@ ErrorHandler.prototype = {
Utils.nextTick(this.service.sync, this.service);
},
_dumpAddons: function _dumpAddons() {
// Just dump the items that sync may be concerned with. Specifically,
// active extensions that are not hidden.
let addonPromise = new Promise(resolve => {
try {
AddonManager.getAddonsByTypes(["extension"], resolve);
} catch (e) {
this._log.warn("Failed to dump addons", e)
resolve([])
}
});
return addonPromise.then(addons => {
let relevantAddons = addons.filter(x => x.isActive && !x.hidden);
this._log.debug("Addons installed", relevantAddons.length);
for (let addon of relevantAddons) {
this._log.debug(" - ${name}, version ${version}, id ${id}", addon);
}
});
},
/**
* Generate a log file for the sync that just completed
* and refresh the input & output streams.
*
* @param reason
* A constant from the LogManager that indicates the reason for the
* reset.
*/
resetFileLog: function resetFileLog() {
let onComplete = logType => {
resetFileLog: function resetFileLog(reason) {
let onComplete = () => {
Svc.Obs.notify("weave:service:reset-file-log");
this._log.trace("Notified: " + Date.now());
if (logType == this._logManager.ERROR_LOG_WRITTEN) {
Cu.reportError("Sync encountered an error - see about:sync-log for the log file.");
}
};
// If we're writing an error log, dump extensions that may be causing problems.
let beforeResetLog;
if (this._logManager.sawError) {
beforeResetLog = this._dumpAddons();
} else {
beforeResetLog = Promise.resolve();
}
// Note we do not return the promise here - the caller doesn't need to wait
// for this to complete.
beforeResetLog
.then(() => this._logManager.resetFileLog())
.then(onComplete, onComplete);
this._logManager.resetFileLog(reason).then(onComplete, onComplete);
},
/**
@ -775,9 +727,6 @@ ErrorHandler.prototype = {
}
},
// A function to indicate if Sync errors should be "reported" - which in this
// context really means "should be notify observers of an error" - but note
// that since bug 1180587, no one is going to surface an error to the user.
shouldReportError: function shouldReportError() {
if (Status.login == MASTER_PASSWORD_LOCKED) {
this._log.trace("shouldReportError: false (master password locked).");
@ -817,12 +766,8 @@ ErrorHandler.prototype = {
return false;
}
let result = ([Status.login, Status.sync].indexOf(SERVER_MAINTENANCE) == -1 &&
[Status.login, Status.sync].indexOf(LOGIN_FAILED_NETWORK_ERROR) == -1);
this._log.trace("shouldReportError: ${result} due to login=${login}, sync=${sync}",
{result, login: Status.login, sync: Status.sync});
return result;
return ([Status.login, Status.sync].indexOf(SERVER_MAINTENANCE) == -1 &&
[Status.login, Status.sync].indexOf(LOGIN_FAILED_NETWORK_ERROR) == -1);
},
get currentAlertMode() {
@ -925,7 +870,7 @@ ErrorHandler.prototype = {
case 401:
this.service.logout();
this._log.info("Got 401 response; resetting clusterURL.");
this.service.clusterURL = null;
Svc.Prefs.reset("clusterURL");
let delay = 0;
if (Svc.Prefs.get("lastSyncReassigned")) {

View file

@ -10,10 +10,10 @@ this.EXPORTED_SYMBOLS = [
"Collection",
];
var Cc = Components.classes;
var Ci = Components.interfaces;
var Cr = Components.results;
var Cu = Components.utils;
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
const CRYPTO_COLLECTION = "crypto";
const KEYS_WBO = "keys";
@ -23,7 +23,6 @@ Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/keys.js");
Cu.import("resource://services-sync/resource.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://services-common/async.js");
this.WBORecord = function WBORecord(collection, id) {
this.data = {};
@ -86,7 +85,7 @@ WBORecord.prototype = {
toJSON: function toJSON() {
// Copy fields from data to be stringified, making sure payload is a string
let obj = {};
for (let [key, val] of Object.entries(this.data))
for (let [key, val] in Iterator(this.data))
obj[key] = key == "payload" ? JSON.stringify(val) : val;
if (this.ttl)
obj.ttl = this.ttl;
@ -196,9 +195,7 @@ CryptoWrapper.prototype = {
},
// The custom setter below masks the parent's getter, so explicitly call it :(
get id() {
return WBORecord.prototype.__lookupGetter__("id").call(this);
},
get id() WBORecord.prototype.__lookupGetter__("id").call(this),
// Keep both plaintext and encrypted versions of the id to verify integrity
set id(val) {
@ -238,11 +235,8 @@ RecordManager.prototype = {
record.deserialize(this.response);
return this.set(url, record);
} catch (ex) {
if (Async.isShutdownException(ex)) {
throw ex;
}
this._log.debug("Failed to import record", ex);
} catch(ex) {
this._log.debug("Failed to import record: " + Utils.exceptionStr(ex));
return null;
}
},
@ -281,10 +275,10 @@ RecordManager.prototype = {
* You can update this thing simply by giving it /info/collections. It'll
* use the last modified time to bring itself up to date.
*/
this.CollectionKeyManager = function CollectionKeyManager(lastModified, default_, collections) {
this.lastModified = lastModified || 0;
this._default = default_ || null;
this._collections = collections || {};
this.CollectionKeyManager = function CollectionKeyManager() {
this.lastModified = 0;
this._collections = {};
this._default = null;
this._log = Log.repository.getLogger("Sync.CollectionKeyManager");
}
@ -293,19 +287,6 @@ this.CollectionKeyManager = function CollectionKeyManager(lastModified, default_
// Note that the last modified time needs to be preserved.
CollectionKeyManager.prototype = {
/**
* Generate a new CollectionKeyManager that has the same attributes
* as this one.
*/
clone() {
const newCollections = {};
for (let c in this._collections) {
newCollections[c] = this._collections[c];
}
return new CollectionKeyManager(this.lastModified, this._default, newCollections);
},
// Return information about old vs new keys:
// * same: true if two collections are equal
// * changed: an array of collection names that changed.
@ -328,7 +309,7 @@ CollectionKeyManager.prototype = {
// Return a sorted, unique array.
changed.sort();
let last;
changed = changed.filter(x => (x != last) && (last = x));
changed = [x for each (x in changed) if ((x != last) && (last = x))];
return {same: changed.length == 0,
changed: changed};
},
@ -374,15 +355,15 @@ CollectionKeyManager.prototype = {
/**
* Create a WBO for the current keys.
*/
asWBO: function(collection, id) {
return this._makeWBO(this._collections, this._default);
},
asWBO: function(collection, id)
this._makeWBO(this._collections, this._default),
/**
* Compute a new default key, and new keys for any specified collections.
*/
newKeys: function(collections) {
let newDefaultKeyBundle = this.newDefaultKeyBundle();
let newDefaultKey = new BulkKeyBundle(DEFAULT_KEYBUNDLE_NAME);
newDefaultKey.generateRandom();
let newColls = {};
if (collections) {
@ -392,7 +373,7 @@ CollectionKeyManager.prototype = {
newColls[c] = b;
});
}
return [newDefaultKeyBundle, newColls];
return [newDefaultKey, newColls];
},
/**
@ -406,57 +387,6 @@ CollectionKeyManager.prototype = {
return this._makeWBO(newColls, newDefaultKey);
},
/**
* Create a new default key.
*
* @returns {BulkKeyBundle}
*/
newDefaultKeyBundle() {
const key = new BulkKeyBundle(DEFAULT_KEYBUNDLE_NAME);
key.generateRandom();
return key;
},
/**
* Create a new default key and store it as this._default, since without one you cannot use setContents.
*/
generateDefaultKey() {
this._default = this.newDefaultKeyBundle();
},
/**
* Return true if keys are already present for each of the given
* collections.
*/
hasKeysFor(collections) {
// We can't use filter() here because sometimes collections is an iterator.
for (let collection of collections) {
if (!this._collections[collection]) {
return false;
}
}
return true;
},
/**
* Return a new CollectionKeyManager that has keys for each of the
* given collections (creating new ones for collections where we
* don't already have keys).
*/
ensureKeysFor(collections) {
const newKeys = Object.assign({}, this._collections);
for (let c of collections) {
if (newKeys[c]) {
continue; // don't replace existing keys
}
const b = new BulkKeyBundle(c);
b.generateRandom();
newKeys[c] = b;
}
return new CollectionKeyManager(this.lastModified, this._default, newKeys);
},
// Take the fetched info/collections WBO, checking the change
// time of the crypto collection.
updateNeeded: function(info_collections) {
@ -487,6 +417,9 @@ CollectionKeyManager.prototype = {
//
setContents: function setContents(payload, modified) {
if (!modified)
throw "No modified time provided to setContents.";
let self = this;
this._log.info("Setting collection keys contents. Our last modified: " +
@ -516,7 +449,9 @@ CollectionKeyManager.prototype = {
if (v) {
let keyObj = new BulkKeyBundle(k);
keyObj.keyPairB64 = v;
newCollections[k] = keyObj;
if (keyObj) {
newCollections[k] = keyObj;
}
}
}
}
@ -527,11 +462,8 @@ CollectionKeyManager.prototype = {
let sameColls = collComparison.same;
if (sameDefault && sameColls) {
self._log.info("New keys are the same as our old keys!");
if (modified) {
self._log.info("Bumped local modified time.");
self.lastModified = modified;
}
self._log.info("New keys are the same as our old keys! Bumped local modified time.");
self.lastModified = modified;
return false;
}
@ -543,10 +475,8 @@ CollectionKeyManager.prototype = {
this._collections = newCollections;
// Always trust the server.
if (modified) {
self._log.info("Bumping last modified to " + modified);
self.lastModified = modified;
}
self._log.info("Bumping last modified to " + modified);
self.lastModified = modified;
return sameDefault ? collComparison.changed : true;
},
@ -594,12 +524,6 @@ this.Collection = function Collection(uri, recordObj, service) {
this._older = 0;
this._newer = 0;
this._data = [];
// optional members used by batch upload operations.
this._batch = null;
this._commit = false;
// Used for batch download operations -- note that this is explicitly an
// opaque value and not (necessarily) a number.
this._offset = null;
}
Collection.prototype = {
__proto__: Resource.prototype,
@ -623,12 +547,6 @@ Collection.prototype = {
args.push("ids=" + this.ids);
if (this.limit > 0 && this.limit != Infinity)
args.push("limit=" + this.limit);
if (this._batch)
args.push("batch=" + encodeURIComponent(this._batch));
if (this._commit)
args.push("commit=true");
if (this._offset)
args.push("offset=" + encodeURIComponent(this._offset));
this.uri.query = (args.length > 0)? '?' + args.join('&') : '';
},
@ -641,14 +559,14 @@ Collection.prototype = {
},
// Apply the action to a certain set of ids
get ids() { return this._ids; },
get ids() this._ids,
set ids(value) {
this._ids = value;
this._rebuildURL();
},
// Limit how many records to get
get limit() { return this._limit; },
get limit() this._limit,
set limit(value) {
this._limit = value;
this._rebuildURL();
@ -678,100 +596,12 @@ Collection.prototype = {
this._rebuildURL();
},
get offset() { return this._offset; },
set offset(value) {
this._offset = value;
this._rebuildURL();
pushData: function Coll_pushData(data) {
this._data.push(data);
},
// Set information about the batch for this request.
get batch() { return this._batch; },
set batch(value) {
this._batch = value;
this._rebuildURL();
},
get commit() { return this._commit; },
set commit(value) {
this._commit = value && true;
this._rebuildURL();
},
// Similar to get(), but will page through the items `batchSize` at a time,
// deferring calling the record handler until we've gotten them all.
//
// Returns the last response processed, and doesn't run the record handler
// on any items if a non-success status is received while downloading the
// records (or if a network error occurs).
getBatched(batchSize = DEFAULT_DOWNLOAD_BATCH_SIZE) {
let totalLimit = Number(this.limit) || Infinity;
if (batchSize <= 0 || batchSize >= totalLimit) {
// Invalid batch sizes should arguably be an error, but they're easy to handle
return this.get();
}
if (!this.full) {
throw new Error("getBatched is unimplemented for guid-only GETs");
}
// _onComplete and _onProgress are reset after each `get` by AsyncResource.
// We overwrite _onRecord to something that stores the data in an array
// until the end.
let { _onComplete, _onProgress, _onRecord } = this;
let recordBuffer = [];
let resp;
try {
this._onRecord = r => recordBuffer.push(r);
let lastModifiedTime;
this.limit = batchSize;
do {
this._onProgress = _onProgress;
this._onComplete = _onComplete;
if (batchSize + recordBuffer.length > totalLimit) {
this.limit = totalLimit - recordBuffer.length;
}
this._log.trace("Performing batched GET", { limit: this.limit, offset: this.offset });
// Actually perform the request
resp = this.get();
if (!resp.success) {
break;
}
// Initialize last modified, or check that something broken isn't happening.
let lastModified = resp.headers["x-last-modified"];
if (!lastModifiedTime) {
lastModifiedTime = lastModified;
this.setHeader("X-If-Unmodified-Since", lastModified);
} else if (lastModified != lastModifiedTime) {
// Should be impossible -- We'd get a 412 in this case.
throw new Error("X-Last-Modified changed in the middle of a download batch! " +
`${lastModified} => ${lastModifiedTime}`)
}
// If this is missing, we're finished.
this.offset = resp.headers["x-weave-next-offset"];
} while (this.offset && totalLimit > recordBuffer.length);
} finally {
// Ensure we undo any temporary state so that subsequent calls to get()
// or getBatched() work properly. We do this before calling the record
// handler so that we can more convincingly pretend to be a normal get()
// call. Note: we're resetting these to the values they had before this
// function was called.
this._onRecord = _onRecord;
this._limit = totalLimit;
this._offset = null;
delete this._headers["x-if-unmodified-since"];
this._rebuildURL();
}
if (resp.success && Async.checkAppReady()) {
// call the original _onRecord (e.g. the user supplied record handler)
// for each record we've stored
for (let record of recordBuffer) {
this._onRecord(record);
}
}
return resp;
clearRecords: function Coll_clearRecords() {
this._data = [];
},
set recordHandler(onRecord) {
@ -781,8 +611,6 @@ Collection.prototype = {
// Switch to newline separated records for incremental parsing
coll.setHeader("Accept", "application/newlines");
this._onRecord = onRecord;
this._onProgress = function() {
let newline;
while ((newline = this._data.indexOf("\n")) > 0) {
@ -793,247 +621,8 @@ Collection.prototype = {
// Deserialize a record from json and give it to the callback
let record = new coll._recordObj();
record.deserialize(json);
coll._onRecord(record);
onRecord(record);
}
};
},
// This object only supports posting via the postQueue object.
post() {
throw new Error("Don't directly post to a collection - use newPostQueue instead");
},
newPostQueue(log, timestamp, postCallback) {
let poster = (data, headers, batch, commit) => {
this.batch = batch;
this.commit = commit;
for (let [header, value] of headers) {
this.setHeader(header, value);
}
return Resource.prototype.post.call(this, data);
}
let getConfig = (name, defaultVal) => {
if (this._service.serverConfiguration && this._service.serverConfiguration.hasOwnProperty(name)) {
return this._service.serverConfiguration[name];
}
return defaultVal;
}
let config = {
max_post_bytes: getConfig("max_post_bytes", MAX_UPLOAD_BYTES),
max_post_records: getConfig("max_post_records", MAX_UPLOAD_RECORDS),
max_batch_bytes: getConfig("max_total_bytes", Infinity),
max_batch_records: getConfig("max_total_records", Infinity),
}
// Handle config edge cases
if (config.max_post_records <= 0) { config.max_post_records = MAX_UPLOAD_RECORDS; }
if (config.max_batch_records <= 0) { config.max_batch_records = Infinity; }
if (config.max_post_bytes <= 0) { config.max_post_bytes = MAX_UPLOAD_BYTES; }
if (config.max_batch_bytes <= 0) { config.max_batch_bytes = Infinity; }
// Max size of BSO payload is 256k. This assumes at most 4k of overhead,
// which sounds like plenty. If the server says it can't handle this, we
// might have valid records we can't sync, so we give up on syncing.
let requiredMax = 260 * 1024;
if (config.max_post_bytes < requiredMax) {
this._log.error("Server configuration max_post_bytes is too low", config);
throw new Error("Server configuration max_post_bytes is too low");
}
return new PostQueue(poster, timestamp, config, log, postCallback);
},
};
/* A helper to manage the posting of records while respecting the various
size limits.
This supports the concept of a server-side "batch". The general idea is:
* We queue as many records as allowed in memory, then make a single POST.
* This first POST (optionally) gives us a batch ID, which we use for
all subsequent posts, until...
* At some point we hit a batch-maximum, and jump through a few hoops to
commit the current batch (ie, all previous POSTs) and start a new one.
* Eventually commit the final batch.
In most cases we expect there to be exactly 1 batch consisting of possibly
multiple POSTs.
*/
function PostQueue(poster, timestamp, config, log, postCallback) {
// The "post" function we should use when it comes time to do the post.
this.poster = poster;
this.log = log;
// The config we use. We expect it to have fields "max_post_records",
// "max_batch_records", "max_post_bytes", and "max_batch_bytes"
this.config = config;
// The callback we make with the response when we do get around to making the
// post (which could be during any of the enqueue() calls or the final flush())
// This callback may be called multiple times and must not add new items to
// the queue.
// The second argument passed to this callback is a boolean value that is true
// if we're in the middle of a batch, and false if either the batch is
// complete, or it's a post to a server that does not understand batching.
this.postCallback = postCallback;
// The string where we are capturing the stringified version of the records
// queued so far. It will always be invalid JSON as it is always missing the
// closing bracket.
this.queued = "";
// The number of records we've queued so far but are yet to POST.
this.numQueued = 0;
// The number of records/bytes we've processed in previous POSTs for our
// current batch. Does *not* include records currently queued for the next POST.
this.numAlreadyBatched = 0;
this.bytesAlreadyBatched = 0;
// The ID of our current batch. Can be undefined (meaning we are yet to make
// the first post of a patch, so don't know if we have a batch), null (meaning
// we've made the first post but the server response indicated no batching
// semantics), otherwise we have made the first post and it holds the batch ID
// returned from the server.
this.batchID = undefined;
// Time used for X-If-Unmodified-Since -- should be the timestamp from the last GET.
this.lastModified = timestamp;
}
PostQueue.prototype = {
enqueue(record) {
// We want to ensure the record has a .toJSON() method defined - even
// though JSON.stringify() would implicitly call it, the stringify might
// still work even if it isn't defined, which isn't what we want.
let jsonRepr = record.toJSON();
if (!jsonRepr) {
throw new Error("You must only call this with objects that explicitly support JSON");
}
let bytes = JSON.stringify(jsonRepr);
// Do a flush if we can't add this record without exceeding our single-request
// limits, or without exceeding the total limit for a single batch.
let newLength = this.queued.length + bytes.length + 2; // extras for leading "[" / "," and trailing "]"
let maxAllowedBytes = Math.min(256 * 1024, this.config.max_post_bytes);
let postSizeExceeded = this.numQueued >= this.config.max_post_records ||
newLength >= maxAllowedBytes;
let batchSizeExceeded = (this.numQueued + this.numAlreadyBatched) >= this.config.max_batch_records ||
(newLength + this.bytesAlreadyBatched) >= this.config.max_batch_bytes;
let singleRecordTooBig = bytes.length + 2 > maxAllowedBytes;
if (postSizeExceeded || batchSizeExceeded) {
this.log.trace(`PostQueue flushing due to postSizeExceeded=${postSizeExceeded}, batchSizeExceeded=${batchSizeExceeded}` +
`, max_batch_bytes: ${this.config.max_batch_bytes}, max_post_bytes: ${this.config.max_post_bytes}`);
if (singleRecordTooBig) {
return { enqueued: false, error: new Error("Single record too large to submit to server") };
}
// We need to write the queue out before handling this one, but we only
// commit the batch (and thus start a new one) if the batch is full.
// Note that if a single record is too big for the batch or post, then
// the batch may be empty, and so we don't flush in that case.
if (this.numQueued) {
this.flush(batchSizeExceeded || singleRecordTooBig);
}
}
// Either a ',' or a '[' depending on whether this is the first record.
this.queued += this.numQueued ? "," : "[";
this.queued += bytes;
this.numQueued++;
return { enqueued: true };
},
flush(finalBatchPost) {
if (!this.queued) {
// nothing queued - we can't be in a batch, and something has gone very
// bad if we think we are.
if (this.batchID) {
throw new Error(`Flush called when no queued records but we are in a batch ${this.batchID}`);
}
return;
}
// the batch query-param and headers we'll send.
let batch;
let headers = [];
if (this.batchID === undefined) {
// First commit in a (possible) batch.
batch = "true";
} else if (this.batchID) {
// We have an existing batch.
batch = this.batchID;
} else {
// Not the first post and we know we have no batch semantics.
batch = null;
}
headers.push(["x-if-unmodified-since", this.lastModified]);
this.log.info(`Posting ${this.numQueued} records of ${this.queued.length+1} bytes with batch=${batch}`);
let queued = this.queued + "]";
if (finalBatchPost) {
this.bytesAlreadyBatched = 0;
this.numAlreadyBatched = 0;
} else {
this.bytesAlreadyBatched += queued.length;
this.numAlreadyBatched += this.numQueued;
}
this.queued = "";
this.numQueued = 0;
let response = this.poster(queued, headers, batch, !!(finalBatchPost && this.batchID !== null));
if (!response.success) {
this.log.trace("Server error response during a batch", response);
// not clear what we should do here - we expect the consumer of this to
// abort by throwing in the postCallback below.
return this.postCallback(response, !finalBatchPost);
}
if (finalBatchPost) {
this.log.trace("Committed batch", this.batchID);
this.batchID = undefined; // we are now in "first post for the batch" state.
this.lastModified = response.headers["x-last-modified"];
return this.postCallback(response, false);
}
if (response.status != 202) {
if (this.batchID) {
throw new Error("Server responded non-202 success code while a batch was in progress");
}
this.batchID = null; // no batch semantics are in place.
this.lastModified = response.headers["x-last-modified"];
return this.postCallback(response, false);
}
// this response is saying the server has batch semantics - we should
// always have a batch ID in the response.
let responseBatchID = response.obj.batch;
this.log.trace("Server responsed 202 with batch", responseBatchID);
if (!responseBatchID) {
this.log.error("Invalid server response: 202 without a batch ID", response);
throw new Error("Invalid server response: 202 without a batch ID");
}
if (this.batchID === undefined) {
this.batchID = responseBatchID;
if (!this.lastModified) {
this.lastModified = response.headers["x-last-modified"];
if (!this.lastModified) {
throw new Error("Batch response without x-last-modified");
}
}
}
if (this.batchID != responseBatchID) {
throw new Error(`Invalid client/server batch state - client has ${this.batchID}, server has ${responseBatchID}`);
}
this.postCallback(response, true);
},
}

View file

@ -7,13 +7,12 @@ this.EXPORTED_SYMBOLS = [
"Resource"
];
var Cc = Components.classes;
var Ci = Components.interfaces;
var Cr = Components.results;
var Cu = Components.utils;
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
Cu.import("resource://services-common/async.js");
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-common/observers.js");
@ -74,6 +73,20 @@ AsyncResource.prototype = {
*/
authenticator: null,
// The string to use as the base User-Agent in Sync requests.
// These strings will look something like
//
// Firefox/4.0 FxSync/1.8.0.20100101.mobile
//
// or
//
// Firefox Aurora/5.0a1 FxSync/1.9.0.20110409.desktop
//
_userAgent:
Services.appinfo.name + "/" + Services.appinfo.version + // Product.
" FxSync/" + WEAVE_VERSION + "." + // Sync.
Services.appinfo.appBuildID + ".", // Build.
// Wait 5 minutes before killing a request.
ABORT_TIMEOUT: 300000,
@ -121,9 +134,7 @@ AsyncResource.prototype = {
//
// Get and set the data encapulated in the resource.
_data: null,
get data() {
return this._data;
},
get data() this._data,
set data(value) {
this._data = value;
},
@ -135,9 +146,16 @@ AsyncResource.prototype = {
// to obtain a request channel.
//
_createRequest: function Res__createRequest(method) {
let channel = NetUtil.newChannel({uri: this.spec, loadUsingSystemPrincipal: true})
.QueryInterface(Ci.nsIRequest)
.QueryInterface(Ci.nsIHttpChannel);
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);
channel.loadFlags |= DEFAULT_LOAD_FLAGS;
@ -147,7 +165,8 @@ AsyncResource.prototype = {
// Compose a UA string fragment from the various available identifiers.
if (Svc.Prefs.get("sendVersionInfo", true)) {
channel.setRequestHeader("user-agent", Utils.userAgent, false);
let ua = this._userAgent + Svc.Prefs.get("client.type", "desktop");
channel.setRequestHeader("user-agent", ua, false);
}
let headers = this.headers;
@ -155,7 +174,7 @@ AsyncResource.prototype = {
if (this.authenticator) {
let result = this.authenticator(this, method);
if (result && result.headers) {
for (let [k, v] of Object.entries(result.headers)) {
for (let [k, v] in Iterator(result.headers)) {
headers[k.toLowerCase()] = v;
}
}
@ -163,7 +182,7 @@ AsyncResource.prototype = {
this._log.debug("No authenticator found.");
}
for (let [key, value] of Object.entries(headers)) {
for (let [key, value] in Iterator(headers)) {
if (key == 'authorization')
this._log.trace("HTTP Header " + key + ": ***** (suppressed)");
else
@ -209,10 +228,10 @@ AsyncResource.prototype = {
this._log, this.ABORT_TIMEOUT);
channel.requestMethod = action;
try {
channel.asyncOpen2(listener);
channel.asyncOpen(listener, null);
} catch (ex) {
// asyncOpen2 can throw in a bunch of cases -- e.g., a forbidden port.
this._log.warn("Caught an error in asyncOpen2", ex);
// asyncOpen can throw in a bunch of cases -- e.g., a forbidden port.
this._log.warn("Caught an error in asyncOpen: " + CommonUtils.exceptionStr(ex));
CommonUtils.nextTick(callback.bind(this, ex));
}
},
@ -259,7 +278,9 @@ AsyncResource.prototype = {
} catch(ex) {
// Got a response, but an exception occurred during processing.
// This shouldn't occur.
this._log.warn("Caught unexpected exception in _oncomplete", ex);
this._log.warn("Caught unexpected exception " + CommonUtils.exceptionStr(ex) +
" in _onComplete.");
this._log.debug(CommonUtils.stackTrace(ex));
}
// Process headers. They can be empty, or the call can otherwise fail, so
@ -297,18 +318,16 @@ AsyncResource.prototype = {
contentLength + ".");
}
} catch (ex) {
this._log.debug("Caught exception visiting headers in _onComplete", ex);
this._log.debug("Caught exception " + CommonUtils.exceptionStr(ex) +
" visiting headers in _onComplete.");
this._log.debug(CommonUtils.stackTrace(ex));
}
let ret = new String(data);
ret.url = channel.URI.spec;
ret.status = status;
ret.success = success;
ret.headers = headers;
if (!success) {
this._log.warn(`${action} request to ${ret.url} failed with status ${status}`);
}
// Make a lazy getter to convert the json response into an object.
// Note that this can cause a parse error to be thrown far away from the
// actual fetch, so be warned!
@ -316,7 +335,7 @@ AsyncResource.prototype = {
try {
return JSON.parse(ret);
} catch (ex) {
this._log.warn("Got exception parsing response body", ex);
this._log.warn("Got exception parsing response body: \"" + CommonUtils.exceptionStr(ex));
// Stringify to avoid possibly printing non-printable characters.
this._log.debug("Parse fail: Response body starts: \"" +
JSON.stringify((ret + "").slice(0, 100)) +
@ -384,12 +403,7 @@ Resource.prototype = {
try {
this._doRequest(action, data, callback);
return Async.waitForSyncCallback(cb);
} catch (ex) {
if (Async.isShutdownException(ex)) {
throw ex;
}
this._log.warn("${action} request to ${url} failed: ${ex}",
{ action, url: this.uri.spec, ex });
} catch(ex) {
// Combine the channel stack with this request stack. Need to create
// a new error object for that.
let error = Error(ex.message);
@ -527,7 +541,7 @@ ChannelListener.prototype = {
siStream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(Ci.nsIScriptableInputStream);
siStream.init(stream);
} catch (ex) {
this._log.warn("Exception creating nsIScriptableInputStream", ex);
this._log.warn("Exception creating nsIScriptableInputStream." + CommonUtils.exceptionStr(ex));
this._log.debug("Parameters: " + req.URI.spec + ", " + stream + ", " + off + ", " + count);
// Cannot proceed, so rethrow and allow the channel to cancel itself.
throw ex;
@ -543,11 +557,9 @@ ChannelListener.prototype = {
try {
this._onProgress();
} catch (ex) {
if (Async.isShutdownException(ex)) {
throw ex;
}
this._log.warn("Got exception calling onProgress handler during fetch of "
+ req.URI.spec, ex);
+ req.URI.spec);
this._log.debug(CommonUtils.exceptionStr(ex));
this._log.trace("Rethrowing; expect a failure code from the HTTP channel.");
throw ex;
}
@ -562,7 +574,7 @@ ChannelListener.prototype = {
try {
CommonUtils.namedTimer(this.abortRequest, this._timeout, this, "abortTimer");
} catch (ex) {
this._log.warn("Got exception extending abort timer", ex);
this._log.warn("Got exception extending abort timer: " + CommonUtils.exceptionStr(ex));
}
},
@ -656,14 +668,14 @@ ChannelNotificationListener.prototype = {
}
}
} catch (ex) {
this._log.error("Error copying headers", ex);
this._log.error("Error copying headers: " + CommonUtils.exceptionStr(ex));
}
// We let all redirects proceed.
try {
callback.onRedirectVerifyCallback(Cr.NS_OK);
} catch (ex) {
this._log.error("onRedirectVerifyCallback threw!", ex);
this._log.error("onRedirectVerifyCallback threw!" + CommonUtils.exceptionStr(ex));
}
}
};

View file

@ -2,7 +2,7 @@
* 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/. */
var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-common/rest.js");
@ -27,6 +27,21 @@ SyncStorageRequest.prototype = {
_logName: "Sync.StorageRequest",
/**
* The string to use as the base User-Agent in Sync requests.
* These strings will look something like
*
* Firefox/4.0 FxSync/1.8.0.20100101.mobile
*
* or
*
* Firefox Aurora/5.0a1 FxSync/1.9.0.20110409.desktop
*/
userAgent:
Services.appinfo.name + "/" + Services.appinfo.version + // Product.
" FxSync/" + WEAVE_VERSION + "." + // Sync.
Services.appinfo.appBuildID + ".", // Build.
/**
* Wait 5 minutes before killing a request.
*/
@ -35,7 +50,8 @@ SyncStorageRequest.prototype = {
dispatch: function dispatch(method, data, onComplete, onProgress) {
// Compose a UA string fragment from the various available identifiers.
if (Svc.Prefs.get("sendVersionInfo", true)) {
this.setHeader("user-agent", Utils.userAgent);
let ua = this.userAgent + Svc.Prefs.get("client.type", "desktop");
this.setHeader("user-agent", ua);
}
if (this.authenticator) {

View file

@ -4,10 +4,10 @@
this.EXPORTED_SYMBOLS = ["Service"];
var Cc = Components.classes;
var Ci = Components.interfaces;
var Cr = Components.results;
var Cu = Components.utils;
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
// How long before refreshing the cluster
const CLUSTER_BACKOFF = 5 * 60 * 1000; // 5 minutes
@ -21,6 +21,7 @@ const KEYS_WBO = "keys";
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
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");
Cu.import("resource://services-sync/engines/clients.js");
@ -32,7 +33,6 @@ Cu.import("resource://services-sync/rest.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/telemetry.js");
Cu.import("resource://services-sync/userapi.js");
Cu.import("resource://services-sync/util.js");
@ -51,6 +51,15 @@ 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:");
}
@ -64,13 +73,8 @@ Sync11Service.prototype = {
storageURL: null,
metaURL: null,
cryptoKeyURL: null,
// The cluster URL comes via the ClusterManager object, which in the FxA
// world is ebbedded in the token returned from the token server.
_clusterURL: null,
get serverURL() {
return Svc.Prefs.get("serverURL");
},
get serverURL() Svc.Prefs.get("serverURL"),
set serverURL(value) {
if (!value.endsWith("/")) {
value += "/";
@ -80,20 +84,14 @@ Sync11Service.prototype = {
if (value == this.serverURL)
return;
// A new server most likely uses a different cluster, so clear that
Svc.Prefs.set("serverURL", value);
// A new server most likely uses a different cluster, so clear that.
this._clusterURL = null;
Svc.Prefs.reset("clusterURL");
},
get clusterURL() {
return this._clusterURL || "";
},
get clusterURL() Svc.Prefs.get("clusterURL", ""),
set clusterURL(value) {
if (value != null && typeof value != "string") {
throw new Error("cluster must be a string, got " + (typeof value));
}
this._clusterURL = value;
Svc.Prefs.set("clusterURL", value);
this._updateCachedURLs();
},
@ -171,16 +169,8 @@ Sync11Service.prototype = {
_updateCachedURLs: function _updateCachedURLs() {
// Nothing to cache yet if we don't have the building blocks
if (!this.clusterURL || !this.identity.username) {
// Also reset all other URLs used by Sync to ensure we aren't accidentally
// using one cached earlier - if there's no cluster URL any cached ones
// are invalid.
this.infoURL = undefined;
this.storageURL = undefined;
this.metaURL = undefined;
this.cryptoKeysURL = undefined;
if (!this.clusterURL || !this.identity.username)
return;
}
this._log.debug("Caching URLs under storage user base: " + this.userBaseURL);
@ -315,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
*/
@ -344,8 +349,6 @@ Sync11Service.prototype = {
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"].
@ -359,7 +362,6 @@ Sync11Service.prototype = {
}
Svc.Obs.add("weave:service:setup-complete", this);
Svc.Obs.add("sync:collection_changed", this); // Pulled from FxAccountsCommon
Svc.Prefs.observe("engine.", this);
this.scheduler = new SyncScheduler(this);
@ -375,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.
@ -424,7 +432,7 @@ Sync11Service.prototype = {
// Map each old pref to the current pref branch
let oldPref = new Preferences(oldPrefBranch);
for (let pref of oldPrefNames)
for each (let pref in oldPrefNames)
Svc.Prefs.set(pref, oldPref.get(pref));
// Remove all the old prefs and remember that we've migrated
@ -472,7 +480,8 @@ Sync11Service.prototype = {
this.engineManager.register(ns[engineName]);
} catch (ex) {
this._log.warn("Could not register engine " + name, ex);
this._log.warn("Could not register engine " + name + ": " +
CommonUtils.exceptionStr(ex));
}
}
@ -486,13 +495,6 @@ Sync11Service.prototype = {
observe: function observe(subject, topic, data) {
switch (topic) {
// Ideally this observer should be in the SyncScheduler, but it would require
// some work to know about the sync specific engines. We should move this there once it does.
case "sync:collection_changed":
if (data.includes("clients")) {
this.sync([]); // [] = clients collection only
}
break;
case "weave:service:setup-complete":
let status = this._checkSetup();
if (status != STATUS_DISABLED && status != CLIENT_NOT_CONFIGURED)
@ -557,8 +559,7 @@ Sync11Service.prototype = {
// Always check for errors; this is also where we look for X-Weave-Alert.
this.errorHandler.checkServerError(info);
if (!info.success) {
this._log.error("Aborting sync: failed to get collections.")
throw info;
throw "Aborting sync: failed to get collections.";
}
return info;
},
@ -675,13 +676,21 @@ Sync11Service.prototype = {
} catch (ex) {
// This means no keys are present, or there's a network error.
this._log.debug("Failed to fetch and verify keys", ex);
this._log.debug("Failed to fetch and verify keys: "
+ Utils.exceptionStr(ex));
this.errorHandler.checkServerError(ex);
return false;
}
},
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;
@ -753,12 +762,8 @@ Sync11Service.prototype = {
return this.verifyLogin(false);
}
// We must have the right cluster, but the server doesn't expect us.
// The implications of this depend on the identity being used - for
// the legacy identity, it's an authoritatively "incorrect password",
// (ie, LOGIN_FAILED_LOGIN_REJECTED) but for FxA it probably means
// "transient error fetching auth token".
this.status.login = this.identity.loginStatusFromVerification404();
// We must have the right cluster, but the server doesn't expect us
this.status.login = LOGIN_FAILED_LOGIN_REJECTED;
return false;
default:
@ -769,7 +774,7 @@ Sync11Service.prototype = {
}
} catch (ex) {
// Must have failed on some network issue
this._log.debug("verifyLogin failed", ex);
this._log.debug("verifyLogin failed: " + Utils.exceptionStr(ex));
this.status.login = LOGIN_FAILED_NETWORK_ERROR;
this.errorHandler.checkServerError(ex);
return false;
@ -842,7 +847,8 @@ Sync11Service.prototype = {
try {
cb.wait();
} catch (ex) {
this._log.debug("Password change failed", ex);
this._log.debug("Password change failed: " +
CommonUtils.exceptionStr(ex));
return false;
}
@ -884,11 +890,12 @@ Sync11Service.prototype = {
// 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.
for (let engine of [this.clientsEngine].concat(this.engineManager.getAll())) {
for each (let engine in [this.clientsEngine].concat(this.engineManager.getAll())) {
try {
engine.removeClientData();
} catch(ex) {
this._log.warn(`Deleting client data for ${engine.name} failed`, ex);
this._log.warn("Deleting client data for " + engine.name + " failed:"
+ Utils.exceptionStr(ex));
}
}
this._log.debug("Finished deleting client data.");
@ -914,7 +921,6 @@ Sync11Service.prototype = {
this._ignorePrefObserver = true;
Svc.Prefs.resetBranch("");
this._ignorePrefObserver = false;
this.clusterURL = null;
Svc.Prefs.set("lastversion", WEAVE_VERSION);
@ -931,22 +937,25 @@ Sync11Service.prototype = {
return;
}
try {
this.identity.finalize();
// 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");
} catch (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");
}
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() {
@ -981,12 +990,8 @@ Sync11Service.prototype = {
}
// Ask the identity manager to explicitly login now.
this._log.info("Logging in the user.");
let cb = Async.makeSpinningCallback();
this.identity.ensureLoggedIn().then(
() => cb(null),
err => cb(err || "ensureLoggedIn failed")
);
this.identity.ensureLoggedIn().then(cb, cb);
// Just let any errors bubble up - they've more context than we do!
cb.wait();
@ -997,9 +1002,9 @@ Sync11Service.prototype = {
&& (username || password || passphrase)) {
Svc.Obs.notify("weave:service:setup-complete");
}
this._log.info("Logging in the user.");
this._updateCachedURLs();
this._log.info("User logged in successfully - verifying login.");
if (!this.verifyLogin()) {
// verifyLogin sets the failure states here.
throw "Login failed: " + this.status.login;
@ -1064,49 +1069,11 @@ Sync11Service.prototype = {
}
},
// Note: returns false if we failed for a reason other than the server not yet
// supporting the api.
_fetchServerConfiguration() {
if (Svc.Prefs.get("APILevel") >= 2) {
// This is similar to _fetchInfo, but with different error handling.
// Only supported by later sync implementations.
let infoURL = this.userBaseURL + "info/configuration";
this._log.debug("Fetching server configuration", infoURL);
let configResponse;
try {
configResponse = this.resource(infoURL).get();
} catch (ex) {
// This is probably a network or similar error.
this._log.warn("Failed to fetch info/configuration", ex);
this.errorHandler.checkServerError(ex);
return false;
}
if (configResponse.status == 404) {
// This server doesn't support the URL yet - that's OK.
this._log.debug("info/configuration returned 404 - using default upload semantics");
} else if (configResponse.status != 200) {
this._log.warn(`info/configuration returned ${configResponse.status} - using default configuration`);
this.errorHandler.checkServerError(configResponse);
return false;
} else {
this.serverConfiguration = configResponse.obj;
}
this._log.trace("info/configuration for this server", this.serverConfiguration);
}
return true;
},
// Stuff we need to do after login, before we can really do
// anything (e.g. key setup).
_remoteSetup: function _remoteSetup(infoResponse) {
let reset = false;
if (!this._fetchServerConfiguration()) {
return false;
}
this._log.debug("Fetching global metadata record");
let meta = this.recordManager.get(this.metaURL);
@ -1133,7 +1100,7 @@ Sync11Service.prototype = {
return false;
}
if (this.recordManager.response.status == 404) {
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;
@ -1143,16 +1110,10 @@ Sync11Service.prototype = {
newMeta.isNew = true;
this.recordManager.set(this.metaURL, newMeta);
let uploadRes = newMeta.upload(this.resource(this.metaURL));
if (!uploadRes.success) {
if (!newMeta.upload(this.resource(this.metaURL)).success) {
this._log.warn("Unable to upload new meta/global. Failing remote setup.");
this.errorHandler.checkServerError(uploadRes);
return false;
}
} else if (!newMeta) {
this._log.warn("Unable to get meta/global. Failing remote setup.");
this.errorHandler.checkServerError(this.recordManager.response);
return false;
} else {
// If newMeta, then it stands to reason that meta != null.
newMeta.isNew = meta.isNew;
@ -1293,9 +1254,13 @@ Sync11Service.prototype = {
return reason;
},
sync: function sync(engineNamesToSync) {
let dateStr = Utils.formatTimestamp(new Date());
this._log.debug("User-Agent: " + Utils.userAgent);
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);
this._catch(function () {
// Make sure we're logged in.
@ -1309,14 +1274,14 @@ Sync11Service.prototype = {
else {
this._log.trace("In sync: no need to login.");
}
return this._lockedSync(engineNamesToSync);
return this._lockedSync.apply(this, arguments);
})();
},
/**
* Sync up engines with the server.
*/
_lockedSync: function _lockedSync(engineNamesToSync) {
_lockedSync: function _lockedSync() {
return this._lock("service.js: sync",
this._notify("sync", "", function onNotify() {
@ -1327,7 +1292,7 @@ Sync11Service.prototype = {
let cb = Async.makeSpinningCallback();
synchronizer.onComplete = cb;
synchronizer.sync(engineNamesToSync);
synchronizer.sync();
// wait() throws if the first argument is truthy, which is exactly what
// we want.
let result = cb.wait();
@ -1338,31 +1303,27 @@ Sync11Service.prototype = {
// We successfully synchronized.
// Check if the identity wants to pre-fetch a migration sentinel from
// the server.
// Only supported by Sync server API level 2+
// If we have no clusterURL, we are probably doing a node reassignment
// so don't attempt to get it in that case.
if (Svc.Prefs.get("APILevel") >= 2 && this.clusterURL) {
this.identity.prefetchMigrationSentinel(this);
//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;
}
// Now let's update our declined engines (but only if we have a metaURL;
// if Sync failed due to no node we will not have one)
if (this.metaURL) {
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);
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);
}))();
},
@ -1536,7 +1497,7 @@ Sync11Service.prototype = {
// Wipe everything we know about except meta because we just uploaded it
let engines = [this.clientsEngine].concat(this.engineManager.getAll());
let collections = engines.map(engine => engine.name);
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
@ -1555,7 +1516,6 @@ Sync11Service.prototype = {
*/
wipeServer: function wipeServer(collections) {
let response;
let histogram = Services.telemetry.getHistogramById("WEAVE_WIPE_SERVER_SUCCEEDED");
if (!collections) {
// Strip the trailing slash.
let res = this.resource(this.storageURL.slice(0, -1));
@ -1563,17 +1523,14 @@ Sync11Service.prototype = {
try {
response = res.delete();
} catch (ex) {
this._log.debug("Failed to wipe server", ex);
histogram.add(false);
this._log.debug("Failed to wipe server: " + CommonUtils.exceptionStr(ex));
throw ex;
}
if (response.status != 200 && response.status != 404) {
this._log.debug("Aborting wipeServer. Server responded with " +
response.status + " response for " + this.storageURL);
histogram.add(false);
throw response;
}
histogram.add(true);
return response.headers["x-weave-timestamp"];
}
@ -1583,15 +1540,14 @@ Sync11Service.prototype = {
try {
response = this.resource(url).delete();
} catch (ex) {
this._log.debug("Failed to wipe '" + name + "' collection", ex);
histogram.add(false);
this._log.debug("Failed to wipe '" + name + "' collection: " +
Utils.exceptionStr(ex));
throw ex;
}
if (response.status != 200 && response.status != 404) {
this._log.debug("Aborting wipeServer. Server responded with " +
response.status + " response for " + url);
histogram.add(false);
throw response;
}
@ -1599,7 +1555,7 @@ Sync11Service.prototype = {
timestamp = response.headers["x-weave-timestamp"];
}
}
histogram.add(true);
return timestamp;
},
@ -1623,7 +1579,7 @@ Sync11Service.prototype = {
}
// Fully wipe each engine if it's able to decrypt data
for (let engine of engines) {
for each (let engine in engines) {
if (engine.canDecrypt()) {
engine.wipeClient();
}
@ -1701,7 +1657,7 @@ Sync11Service.prototype = {
}
// Have each engine drop any temporary meta data
for (let engine of engines) {
for each (let engine in engines) {
engine.resetClient();
}
})();
@ -1731,7 +1687,8 @@ Sync11Service.prototype = {
return this.getStorageRequest(url).get(function onComplete(error) {
// Note: 'this' is the request.
if (error) {
this._log.debug("Failed to retrieve '" + info_type + "'", error);
this._log.debug("Failed to retrieve '" + info_type + "': " +
Utils.exceptionStr(error));
return callback(error);
}
if (this.response.status != 200) {

View file

@ -4,7 +4,7 @@
this.EXPORTED_SYMBOLS = ["ClusterManager"];
var {utils: Cu} = Components;
const {utils: Cu} = Components;
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/constants.js");
@ -80,9 +80,6 @@ ClusterManager.prototype = {
return false;
}
// Convert from the funky "String object with additional properties" that
// resource.js returns to a plain-old string.
cluster = cluster.toString();
// Don't update stuff if we already have the right cluster
if (cluster == this.service.clusterURL) {
return false;
@ -90,6 +87,7 @@ ClusterManager.prototype = {
this._log.debug("Setting cluster to " + cluster);
this.service.clusterURL = cluster;
Svc.Prefs.set("lastClusterUpdate", Date.now().toString());
return true;
},

View file

@ -11,7 +11,7 @@
this.EXPORTED_SYMBOLS = ["DeclinedEngines"];
var {utils: Cu} = Components;
const {utils: Cu} = Components;
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://gre/modules/Log.jsm");
@ -29,8 +29,8 @@ this.DeclinedEngines = function (service) {
}
this.DeclinedEngines.prototype = {
updateDeclined: function (meta, engineManager=this.service.engineManager) {
let enabled = new Set(engineManager.getEnabled().map(e => e.name));
let known = new Set(engineManager.getAll().map(e => e.name));
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());

View file

@ -8,16 +8,13 @@
this.EXPORTED_SYMBOLS = ["EngineSynchronizer"];
var {utils: Cu} = Components;
const {utils: Cu} = Components;
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");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://services-common/observers.js");
Cu.import("resource://services-common/async.js");
Cu.import("resource://gre/modules/Task.jsm");
/**
* Perform synchronization of engines.
@ -34,7 +31,7 @@ this.EngineSynchronizer = function EngineSynchronizer(service) {
}
EngineSynchronizer.prototype = {
sync: function sync(engineNamesToSync) {
sync: function sync() {
if (!this.onComplete) {
throw new Error("onComplete handler not installed.");
}
@ -99,9 +96,6 @@ EngineSynchronizer.prototype = {
return;
}
// We only honor the "hint" of what engines to Sync if this isn't
// a first sync.
let allowEnginesHint = false;
// Wipe data in the desired direction if necessary
switch (Svc.Prefs.get("firstSync")) {
case "resetClient":
@ -113,9 +107,6 @@ EngineSynchronizer.prototype = {
case "wipeRemote":
this.service.wipeRemote(engineManager.enabledEngineNames);
break;
default:
allowEnginesHint = true;
break;
}
if (this.service.clientsEngine.localCommands) {
@ -145,31 +136,20 @@ EngineSynchronizer.prototype = {
try {
this._updateEnabledEngines();
} catch (ex) {
this._log.debug("Updating enabled engines failed", ex);
this._log.debug("Updating enabled engines failed: " +
Utils.exceptionStr(ex));
this.service.errorHandler.checkServerError(ex);
this.onComplete(ex);
return;
}
// If the engines to sync has been specified, we sync in the order specified.
let enginesToSync;
if (allowEnginesHint && engineNamesToSync) {
this._log.info("Syncing specified engines", engineNamesToSync);
enginesToSync = engineManager.get(engineNamesToSync).filter(e => e.enabled);
} else {
this._log.info("Syncing all enabled engines.");
enginesToSync = engineManager.getEnabled();
}
try {
// We don't bother validating engines that failed to sync.
let enginesToValidate = [];
for (let engine of enginesToSync) {
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);
break;
}
enginesToValidate.push(engine);
}
// If _syncEngine fails for a 401, we might not have a cluster URL here.
@ -195,8 +175,6 @@ EngineSynchronizer.prototype = {
}
}
Async.promiseSpinningly(this._tryValidateEngines(enginesToValidate));
// If there were no sync engine failures
if (this.service.status.service != SYNC_FAILED_PARTIAL) {
Svc.Prefs.set("lastSync", new Date().toString());
@ -206,7 +184,7 @@ EngineSynchronizer.prototype = {
Svc.Prefs.reset("firstSync");
let syncTime = ((Date.now() - startTime) / 1000).toFixed(2);
let dateStr = Utils.formatTimestamp(new Date());
let dateStr = new Date().toLocaleFormat(LOG_DATE_FORMAT);
this._log.info("Sync completed at " + dateStr
+ " after " + syncTime + " secs.");
}
@ -214,106 +192,6 @@ EngineSynchronizer.prototype = {
this.onComplete(null);
},
_tryValidateEngines: Task.async(function* (recentlySyncedEngines) {
if (!Services.telemetry.canRecordBase || !Svc.Prefs.get("validation.enabled", false)) {
this._log.info("Skipping validation: validation or telemetry reporting is disabled");
return;
}
let lastValidation = Svc.Prefs.get("validation.lastTime", 0);
let validationInterval = Svc.Prefs.get("validation.interval");
let nowSeconds = Math.floor(Date.now() / 1000);
if (nowSeconds - lastValidation < validationInterval) {
this._log.info("Skipping validation: too recent since last validation attempt");
return;
}
// Update the time now, even if we may return false still. We don't want to
// check the rest of these more frequently than once a day.
Svc.Prefs.set("validation.lastTime", nowSeconds);
// Validation only occurs a certain percentage of the time.
let validationProbability = Svc.Prefs.get("validation.percentageChance", 0) / 100.0;
if (validationProbability < Math.random()) {
this._log.info("Skipping validation: Probability threshold not met");
return;
}
let maxRecords = Svc.Prefs.get("validation.maxRecords");
if (!maxRecords) {
// Don't bother asking the server for the counts if we know validation
// won't happen anyway.
return;
}
// maxRecords of -1 means "any number", so we can skip asking the server.
// Used for tests.
let info;
if (maxRecords < 0) {
info = {};
for (let e of recentlySyncedEngines) {
info[e.name] = 1; // needs to be < maxRecords
}
maxRecords = 2;
} else {
let collectionCountsURL = this.service.userBaseURL + "info/collection_counts";
try {
let infoResp = this.service._fetchInfo(collectionCountsURL);
if (!infoResp.success) {
this._log.error("Can't run validation: request to info/collection_counts responded with "
+ resp.status);
return;
}
info = infoResp.obj; // might throw because obj is a getter which parses json.
} catch (e) {
// Not running validation is totally fine, so we just write an error log and return.
this._log.error("Can't run validation: Caught error when fetching counts", e);
return;
}
}
if (!info) {
return;
}
let engineLookup = new Map(recentlySyncedEngines.map(e => [e.name, e]));
let toRun = [];
for (let [engineName, recordCount] of Object.entries(info)) {
let engine = engineLookup.get(engineName);
if (recordCount > maxRecords || !engine) {
this._log.debug(`Skipping validation for ${engineName} because it's not an engine or ` +
`the number of records (${recordCount}) is greater than the maximum allowed (${maxRecords}).`);
continue;
}
let validator = engine.getValidator();
if (!validator) {
continue;
}
// Put this in an array so that we know how many we're going to do, so we
// don't tell users we're going to run some validators when we aren't.
toRun.push({ engine, validator });
}
if (!toRun.length) {
return;
}
Services.console.logStringMessage(
"Sync is about to run a consistency check. This may be slow, and " +
"can be controlled using the pref \"services.sync.validation.enabled\".\n" +
"If you encounter any problems because of this, please file a bug.");
for (let { validator, engine } of toRun) {
try {
let result = yield validator.validate(engine);
Observers.notify("weave:engine:validate:finish", result, engine.name);
} catch (e) {
this._log.error(`Failed to run validation on ${engine.name}!`, e);
Observers.notify("weave:engine:validate:error", e, engine.name)
// Keep validating -- there's no reason to think that a failure for one
// validator would mean the others will fail.
}
}
}),
// Returns true if sync should proceed.
// false / no return value means sync should be aborted.
_syncEngine: function _syncEngine(engine) {
@ -346,15 +224,8 @@ EngineSynchronizer.prototype = {
// 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.
let hasEnabledEngines = false;
for (let e in meta.payload.engines) {
if (e != "clients") {
hasEnabledEngines = true;
break;
}
}
if ((numClients <= 1) && !hasEnabledEngines) {
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;
}
@ -418,7 +289,7 @@ EngineSynchronizer.prototype = {
}
// Any remaining engines were either enabled locally or disabled remotely.
for (let engineName of enabled) {
for each (let engineName in enabled) {
let engine = engineManager.get(engineName);
if (Svc.Prefs.get("engineStatusChanged." + engine.prefName, false)) {
this._log.trace("The " + engineName + " engine was enabled locally.");

View file

@ -4,10 +4,10 @@
this.EXPORTED_SYMBOLS = ["Status"];
var Cc = Components.classes;
var Ci = Components.interfaces;
var Cr = Components.results;
var Cu = Components.utils;
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://gre/modules/Log.jsm");
@ -30,7 +30,10 @@ this.Status = {
.wrappedJSObject;
let idClass = service.fxAccountsEnabled ? BrowserIDManager : IdentityManager;
this.__authManager = new idClass();
this.__authManager.initialize();
// .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;
},

View file

@ -1,578 +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";
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
this.EXPORTED_SYMBOLS = ["SyncTelemetry"];
Cu.import("resource://services-sync/browserid_identity.js");
Cu.import("resource://services-sync/main.js");
Cu.import("resource://services-sync/status.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://services-common/observers.js");
Cu.import("resource://services-common/async.js");
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/TelemetryController.jsm");
Cu.import("resource://gre/modules/FxAccounts.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/osfile.jsm", this);
let constants = {};
Cu.import("resource://services-sync/constants.js", constants);
var fxAccountsCommon = {};
Cu.import("resource://gre/modules/FxAccountsCommon.js", fxAccountsCommon);
XPCOMUtils.defineLazyServiceGetter(this, "Telemetry",
"@mozilla.org/base/telemetry;1",
"nsITelemetry");
const log = Log.repository.getLogger("Sync.Telemetry");
const TOPICS = [
"profile-before-change",
"weave:service:sync:start",
"weave:service:sync:finish",
"weave:service:sync:error",
"weave:engine:sync:start",
"weave:engine:sync:finish",
"weave:engine:sync:error",
"weave:engine:sync:applied",
"weave:engine:sync:uploaded",
"weave:engine:validate:finish",
"weave:engine:validate:error",
];
const PING_FORMAT_VERSION = 1;
// The set of engines we record telemetry for - any other engines are ignored.
const ENGINES = new Set(["addons", "bookmarks", "clients", "forms", "history",
"passwords", "prefs", "tabs", "extension-storage"]);
// A regex we can use to replace the profile dir in error messages. We use a
// regexp so we can simply replace all case-insensitive occurences.
// This escaping function is from:
// https://developer.mozilla.org/en/docs/Web/JavaScript/Guide/Regular_Expressions
const reProfileDir = new RegExp(
OS.Constants.Path.profileDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
"gi");
function transformError(error, engineName) {
if (Async.isShutdownException(error)) {
return { name: "shutdownerror" };
}
if (typeof error === "string") {
if (error.startsWith("error.")) {
// This is hacky, but I can't imagine that it's not also accurate.
return { name: "othererror", error };
}
// There's a chance the profiledir is in the error string which is PII we
// want to avoid including in the ping.
error = error.replace(reProfileDir, "[profileDir]");
return { name: "unexpectederror", error };
}
if (error.failureCode) {
return { name: "othererror", error: error.failureCode };
}
if (error instanceof AuthenticationError) {
return { name: "autherror", from: error.source };
}
if (error instanceof Ci.mozIStorageError) {
return { name: "sqlerror", code: error.result };
}
let httpCode = error.status ||
(error.response && error.response.status) ||
error.code;
if (httpCode) {
return { name: "httperror", code: httpCode };
}
if (error.result) {
return { name: "nserror", code: error.result };
}
return {
name: "unexpectederror",
// as above, remove the profile dir value.
error: String(error).replace(reProfileDir, "[profileDir]")
}
}
function tryGetMonotonicTimestamp() {
try {
return Telemetry.msSinceProcessStart();
} catch (e) {
log.warn("Unable to get a monotonic timestamp!");
return -1;
}
}
function timeDeltaFrom(monotonicStartTime) {
let now = tryGetMonotonicTimestamp();
if (monotonicStartTime !== -1 && now !== -1) {
return Math.round(now - monotonicStartTime);
}
return -1;
}
class EngineRecord {
constructor(name) {
// startTime is in ms from process start, but is monotonic (unlike Date.now())
// so we need to keep both it and when.
this.startTime = tryGetMonotonicTimestamp();
this.name = name;
}
toJSON() {
let result = Object.assign({}, this);
delete result.startTime;
return result;
}
finished(error) {
let took = timeDeltaFrom(this.startTime);
if (took > 0) {
this.took = took;
}
if (error) {
this.failureReason = transformError(error, this.name);
}
}
recordApplied(counts) {
if (this.incoming) {
log.error(`Incoming records applied multiple times for engine ${this.name}!`);
return;
}
if (this.name === "clients" && !counts.failed) {
// ignore successful application of client records
// since otherwise they show up every time and are meaningless.
return;
}
let incomingData = {};
let properties = ["applied", "failed", "newFailed", "reconciled"];
// Only record non-zero properties and only record incoming at all if
// there's at least one property we care about.
for (let property of properties) {
if (counts[property]) {
incomingData[property] = counts[property];
this.incoming = incomingData;
}
}
}
recordValidation(validationResult) {
if (this.validation) {
log.error(`Multiple validations occurred for engine ${this.name}!`);
return;
}
let { problems, version, duration, recordCount } = validationResult;
let validation = {
version: version || 0,
checked: recordCount || 0,
};
if (duration > 0) {
validation.took = Math.round(duration);
}
let summarized = problems.getSummary(true).filter(({count}) => count > 0);
if (summarized.length) {
validation.problems = summarized;
}
this.validation = validation;
}
recordValidationError(e) {
if (this.validation) {
log.error(`Multiple validations occurred for engine ${this.name}!`);
return;
}
this.validation = {
failureReason: transformError(e)
};
}
recordUploaded(counts) {
if (counts.sent || counts.failed) {
if (!this.outgoing) {
this.outgoing = [];
}
this.outgoing.push({
sent: counts.sent || undefined,
failed: counts.failed || undefined,
});
}
}
}
class TelemetryRecord {
constructor(allowedEngines) {
this.allowedEngines = allowedEngines;
// Our failure reason. This property only exists in the generated ping if an
// error actually occurred.
this.failureReason = undefined;
this.uid = "";
this.when = Date.now();
this.startTime = tryGetMonotonicTimestamp();
this.took = 0; // will be set later.
// All engines that have finished (ie, does not include the "current" one)
// We omit this from the ping if it's empty.
this.engines = [];
// The engine that has started but not yet stopped.
this.currentEngine = null;
}
toJSON() {
let result = {
when: this.when,
uid: this.uid,
took: this.took,
failureReason: this.failureReason,
status: this.status,
deviceID: this.deviceID,
devices: this.devices,
};
let engines = [];
for (let engine of this.engines) {
engines.push(engine.toJSON());
}
if (engines.length > 0) {
result.engines = engines;
}
return result;
}
finished(error) {
this.took = timeDeltaFrom(this.startTime);
if (this.currentEngine != null) {
log.error("Finished called for the sync before the current engine finished");
this.currentEngine.finished(null);
this.onEngineStop(this.currentEngine.name);
}
if (error) {
this.failureReason = transformError(error);
}
// We don't bother including the "devices" field if we can't come up with a
// UID or device ID for *this* device -- If that's the case, any data we'd
// put there would be likely to be full of garbage anyway.
let includeDeviceInfo = false;
try {
this.uid = Weave.Service.identity.hashedUID();
let deviceID = Weave.Service.identity.deviceID();
if (deviceID) {
// Combine the raw device id with the metrics uid to create a stable
// unique identifier that can't be mapped back to the user's FxA
// identity without knowing the metrics HMAC key.
this.deviceID = Utils.sha256(deviceID + this.uid);
includeDeviceInfo = true;
}
} catch (e) {
this.uid = "0".repeat(32);
this.deviceID = undefined;
}
if (includeDeviceInfo) {
let remoteDevices = Weave.Service.clientsEngine.remoteClients;
this.devices = remoteDevices.map(device => {
return {
os: device.os,
version: device.version,
id: Utils.sha256(device.id + this.uid)
};
});
}
// Check for engine statuses. -- We do this now, and not in engine.finished
// to make sure any statuses that get set "late" are recorded
for (let engine of this.engines) {
let status = Status.engines[engine.name];
if (status && status !== constants.ENGINE_SUCCEEDED) {
engine.status = status;
}
}
let statusObject = {};
let serviceStatus = Status.service;
if (serviceStatus && serviceStatus !== constants.STATUS_OK) {
statusObject.service = serviceStatus;
this.status = statusObject;
}
let syncStatus = Status.sync;
if (syncStatus && syncStatus !== constants.SYNC_SUCCEEDED) {
statusObject.sync = syncStatus;
this.status = statusObject;
}
}
onEngineStart(engineName) {
if (this._shouldIgnoreEngine(engineName, false)) {
return;
}
if (this.currentEngine) {
log.error(`Being told that engine ${engineName} has started, but current engine ${
this.currentEngine.name} hasn't stopped`);
// Just discard the current engine rather than making up data for it.
}
this.currentEngine = new EngineRecord(engineName);
}
onEngineStop(engineName, error) {
// We only care if it's the current engine if we have a current engine.
if (this._shouldIgnoreEngine(engineName, !!this.currentEngine)) {
return;
}
if (!this.currentEngine) {
// It's possible for us to get an error before the start message of an engine
// (somehow), in which case we still want to record that error.
if (!error) {
return;
}
log.error(`Error triggered on ${engineName} when no current engine exists: ${error}`);
this.currentEngine = new EngineRecord(engineName);
}
this.currentEngine.finished(error);
this.engines.push(this.currentEngine);
this.currentEngine = null;
}
onEngineApplied(engineName, counts) {
if (this._shouldIgnoreEngine(engineName)) {
return;
}
this.currentEngine.recordApplied(counts);
}
onEngineValidated(engineName, validationData) {
if (this._shouldIgnoreEngine(engineName, false)) {
return;
}
let engine = this.engines.find(e => e.name === engineName);
if (!engine && this.currentEngine && engineName === this.currentEngine.name) {
engine = this.currentEngine;
}
if (engine) {
engine.recordValidation(validationData);
} else {
log.warn(`Validation event triggered for engine ${engineName}, which hasn't been synced!`);
}
}
onEngineValidateError(engineName, error) {
if (this._shouldIgnoreEngine(engineName, false)) {
return;
}
let engine = this.engines.find(e => e.name === engineName);
if (!engine && this.currentEngine && engineName === this.currentEngine.name) {
engine = this.currentEngine;
}
if (engine) {
engine.recordValidationError(error);
} else {
log.warn(`Validation failure event triggered for engine ${engineName}, which hasn't been synced!`);
}
}
onEngineUploaded(engineName, counts) {
if (this._shouldIgnoreEngine(engineName)) {
return;
}
this.currentEngine.recordUploaded(counts);
}
_shouldIgnoreEngine(engineName, shouldBeCurrent = true) {
if (!this.allowedEngines.has(engineName)) {
log.info(`Notification for engine ${engineName}, but we aren't recording telemetry for it`);
return true;
}
if (shouldBeCurrent) {
if (!this.currentEngine || engineName != this.currentEngine.name) {
log.error(`Notification for engine ${engineName} but it isn't current`);
return true;
}
}
return false;
}
}
class SyncTelemetryImpl {
constructor(allowedEngines) {
log.level = Log.Level[Svc.Prefs.get("log.logger.telemetry", "Trace")];
// This is accessible so we can enable custom engines during tests.
this.allowedEngines = allowedEngines;
this.current = null;
this.setupObservers();
this.payloads = [];
this.discarded = 0;
this.maxPayloadCount = Svc.Prefs.get("telemetry.maxPayloadCount");
this.submissionInterval = Svc.Prefs.get("telemetry.submissionInterval") * 1000;
this.lastSubmissionTime = Telemetry.msSinceProcessStart();
}
getPingJSON(reason) {
return {
why: reason,
discarded: this.discarded || undefined,
version: PING_FORMAT_VERSION,
syncs: this.payloads.slice(),
};
}
finish(reason) {
// Note that we might be in the middle of a sync right now, and so we don't
// want to touch this.current.
let result = this.getPingJSON(reason);
this.payloads = [];
this.discarded = 0;
this.submit(result);
}
setupObservers() {
for (let topic of TOPICS) {
Observers.add(topic, this, this);
}
}
shutdown() {
this.finish("shutdown");
for (let topic of TOPICS) {
Observers.remove(topic, this, this);
}
}
submit(record) {
// We still call submit() with possibly illegal payloads so that tests can
// know that the ping was built. We don't end up submitting them, however.
if (record.syncs.length) {
log.trace(`submitting ${record.syncs.length} sync record(s) to telemetry`);
TelemetryController.submitExternalPing("sync", record);
}
}
onSyncStarted() {
if (this.current) {
log.warn("Observed weave:service:sync:start, but we're already recording a sync!");
// Just discard the old record, consistent with our handling of engines, above.
this.current = null;
}
this.current = new TelemetryRecord(this.allowedEngines);
}
_checkCurrent(topic) {
if (!this.current) {
log.warn(`Observed notification ${topic} but no current sync is being recorded.`);
return false;
}
return true;
}
onSyncFinished(error) {
if (!this.current) {
log.warn("onSyncFinished but we aren't recording");
return;
}
this.current.finished(error);
if (this.payloads.length < this.maxPayloadCount) {
this.payloads.push(this.current.toJSON());
} else {
++this.discarded;
}
this.current = null;
if ((Telemetry.msSinceProcessStart() - this.lastSubmissionTime) > this.submissionInterval) {
this.finish("schedule");
this.lastSubmissionTime = Telemetry.msSinceProcessStart();
}
}
observe(subject, topic, data) {
log.trace(`observed ${topic} ${data}`);
switch (topic) {
case "profile-before-change":
this.shutdown();
break;
/* sync itself state changes */
case "weave:service:sync:start":
this.onSyncStarted();
break;
case "weave:service:sync:finish":
if (this._checkCurrent(topic)) {
this.onSyncFinished(null);
}
break;
case "weave:service:sync:error":
// argument needs to be truthy (this should always be the case)
this.onSyncFinished(subject || "Unknown");
break;
/* engine sync state changes */
case "weave:engine:sync:start":
if (this._checkCurrent(topic)) {
this.current.onEngineStart(data);
}
break;
case "weave:engine:sync:finish":
if (this._checkCurrent(topic)) {
this.current.onEngineStop(data, null);
}
break;
case "weave:engine:sync:error":
if (this._checkCurrent(topic)) {
// argument needs to be truthy (this should always be the case)
this.current.onEngineStop(data, subject || "Unknown");
}
break;
/* engine counts */
case "weave:engine:sync:applied":
if (this._checkCurrent(topic)) {
this.current.onEngineApplied(data, subject);
}
break;
case "weave:engine:sync:uploaded":
if (this._checkCurrent(topic)) {
this.current.onEngineUploaded(data, subject);
}
break;
case "weave:engine:validate:finish":
if (this._checkCurrent(topic)) {
this.current.onEngineValidated(data, subject);
}
break;
case "weave:engine:validate:error":
if (this._checkCurrent(topic)) {
this.current.onEngineValidateError(data, subject || "Unknown");
}
break;
default:
log.warn(`unexpected observer topic ${topic}`);
break;
}
}
}
this.SyncTelemetry = new SyncTelemetryImpl(ENGINES);

View file

@ -8,7 +8,7 @@ this.EXPORTED_SYMBOLS = [
"UserAPI10Client",
];
var {utils: Cu} = Components;
const {utils: Cu} = Components;
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-common/rest.js");

View file

@ -4,7 +4,7 @@
this.EXPORTED_SYMBOLS = ["XPCOMUtils", "Services", "Utils", "Async", "Svc", "Str"];
var {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-common/observers.js");
@ -35,6 +35,8 @@ this.Utils = {
// In the ideal world, references to these would be removed.
nextTick: CommonUtils.nextTick,
namedTimer: CommonUtils.namedTimer,
exceptionStr: CommonUtils.exceptionStr,
stackTrace: CommonUtils.stackTrace,
makeURI: CommonUtils.makeURI,
encodeUTF8: CommonUtils.encodeUTF8,
decodeUTF8: CommonUtils.decodeUTF8,
@ -52,7 +54,6 @@ this.Utils = {
digestBytes: CryptoUtils.digestBytes,
sha1: CryptoUtils.sha1,
sha1Base32: CryptoUtils.sha1Base32,
sha256: CryptoUtils.sha256,
makeHMACKey: CryptoUtils.makeHMACKey,
makeHMACHasher: CryptoUtils.makeHMACHasher,
hkdfExpand: CryptoUtils.hkdfExpand,
@ -60,25 +61,6 @@ this.Utils = {
deriveKeyFromPassphrase: CryptoUtils.deriveKeyFromPassphrase,
getHTTPMACSHA1Header: CryptoUtils.getHTTPMACSHA1Header,
/**
* The string to use as the base User-Agent in Sync requests.
* This string will look something like
*
* Firefox/49.0a1 (Windows NT 6.1; WOW64; rv:46.0) FxSync/1.51.0.20160516142357.desktop
*/
_userAgent: null,
get userAgent() {
if (!this._userAgent) {
let hph = Cc["@mozilla.org/network/protocol;1?name=http"].getService(Ci.nsIHttpProtocolHandler);
this._userAgent =
Services.appinfo.name + "/" + Services.appinfo.version + // Product.
" (" + hph.oscpu + ")" + // (oscpu)
" FxSync/" + WEAVE_VERSION + "." + // Sync.
Services.appinfo.appBuildID + "."; // Build.
}
return this._userAgent + Svc.Prefs.get("client.type", "desktop");
},
/**
* Wrap a function to catch all exceptions and log them
*
@ -95,7 +77,7 @@ this.Utils = {
return func.call(thisArg);
}
catch(ex) {
thisArg._log.debug("Exception calling " + (func.name || "anonymous function"), ex);
thisArg._log.debug("Exception: " + Utils.exceptionStr(ex));
if (exceptionCallback) {
return exceptionCallback.call(thisArg, ex);
}
@ -271,14 +253,14 @@ this.Utils = {
*/
base32ToFriendly: function base32ToFriendly(input) {
return input.toLowerCase()
.replace(/l/g, '8')
.replace(/o/g, '9');
.replace("l", '8', "g")
.replace("o", '9', "g");
},
base32FromFriendly: function base32FromFriendly(input) {
return input.toUpperCase()
.replace(/8/g, 'L')
.replace(/9/g, 'O');
.replace("8", 'L', "g")
.replace("9", 'O', "g");
},
/**
@ -356,14 +338,12 @@ this.Utils = {
try {
json = yield CommonUtils.readJSON(path);
} catch (e if e instanceof OS.File.Error && e.becauseNoSuchFile) {
// Ignore non-existent files.
} catch (e) {
if (e instanceof OS.File.Error && e.becauseNoSuchFile) {
// Ignore non-existent files, but explicitly return null.
json = null;
} else {
if (that._log) {
that._log.debug("Failed to load json", e);
}
if (that._log) {
that._log.debug("Failed to load json: " +
CommonUtils.exceptionStr(e));
}
}
@ -411,52 +391,6 @@ this.Utils = {
}
}),
/**
* Move a json file in the profile directory. Will fail if a file exists at the
* destination.
*
* @returns a promise that resolves to undefined on success, or rejects on failure
*
* @param aFrom
* Current path to the JSON file saved on disk, relative to profileDir/weave
* .json will be appended to the file name.
* @param aTo
* New path to the JSON file saved on disk, relative to profileDir/weave
* .json will be appended to the file name.
* @param that
* Object to use for logging
*/
jsonMove(aFrom, aTo, that) {
let pathFrom = OS.Path.join(OS.Constants.Path.profileDir, "weave",
...(aFrom + ".json").split("/"));
let pathTo = OS.Path.join(OS.Constants.Path.profileDir, "weave",
...(aTo + ".json").split("/"));
if (that._log) {
that._log.trace("Moving " + pathFrom + " to " + pathTo);
}
return OS.File.move(pathFrom, pathTo, { noOverwrite: true });
},
/**
* Removes a json file in the profile directory.
*
* @returns a promise that resolves to undefined on success, or rejects on failure
*
* @param filePath
* Current path to the JSON file saved on disk, relative to profileDir/weave
* .json will be appended to the file name.
* @param that
* Object to use for logging
*/
jsonRemove(filePath, that) {
let path = OS.Path.join(OS.Constants.Path.profileDir, "weave",
...(filePath + ".json").split("/"));
if (that._log) {
that._log.trace("Deleting " + path);
}
return OS.File.remove(path, { ignoreAbsent: true });
},
getErrorString: function Utils_getErrorString(error, args) {
try {
return Str.errors.get(error, args || null);
@ -543,7 +477,7 @@ this.Utils = {
// 20-char sync key.
if (pp.length == 23 &&
[5, 11, 17].every(i => pp[i] == '-')) {
[5, 11, 17].every(function(i) pp[i] == '-')) {
return pp.slice(0, 5) + pp.slice(6, 11)
+ pp.slice(12, 17) + pp.slice(18, 23);
@ -551,7 +485,7 @@ this.Utils = {
// "Modern" 26-char key.
if (pp.length == 31 &&
[1, 7, 13, 19, 25].every(i => pp[i] == '-')) {
[1, 7, 13, 19, 25].every(function(i) pp[i] == '-')) {
return pp.slice(0, 1) + pp.slice(2, 7)
+ pp.slice(8, 13) + pp.slice(14, 19)
@ -681,12 +615,30 @@ this.Utils = {
* 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);
// We used to include the FxA hosts (hence the Set() result) but we now
// don't give them special treatment (hence the Set() with exactly 1 item)
return result;
//
// 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() {
@ -720,32 +672,6 @@ this.Utils = {
Cc["@mozilla.org/network/protocol;1?name=http"].getService(Ci.nsIHttpProtocolHandler).oscpu;
return Str.sync.get("client.name2", [user, appName, system]);
},
getDeviceName() {
const deviceName = Svc.Prefs.get("client.name", "");
if (deviceName === "") {
return this.getDefaultDeviceName();
}
return deviceName;
},
getDeviceType() {
return Svc.Prefs.get("client.type", DEVICE_TYPE_DESKTOP);
},
formatTimestamp(date) {
// Format timestamp as: "%Y-%m-%d %H:%M:%S"
let year = String(date.getFullYear());
let month = String(date.getMonth() + 1).padStart(2, "0");
let day = String(date.getDate()).padStart(2, "0");
let hours = String(date.getHours()).padStart(2, "0");
let minutes = String(date.getMinutes()).padStart(2, "0");
let seconds = String(date.getSeconds()).padStart(2, "0");
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
};
@ -764,7 +690,7 @@ Svc.Prefs = new Preferences(PREFS_BRANCH);
Svc.DefaultPrefs = new Preferences({branch: PREFS_BRANCH, defaultBranch: true});
Svc.Obs = Observers;
var _sessionCID = Services.appinfo.ID == SEAMONKEY_ID ?
let _sessionCID = Services.appinfo.ID == SEAMONKEY_ID ?
"@mozilla.org/suite/sessionstore;1" :
"@mozilla.org/browser/sessionstore;1";

View file

@ -1,31 +1,28 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
with Files('**'):
BUG_COMPONENT = ('Mozilla Services', 'Firefox Sync: Backend')
DIRS += ['locales']
XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini']
EXTRA_COMPONENTS += [
'SyncComponents.manifest',
'Weave.js',
]
EXTRA_PP_COMPONENTS += [
'Weave.js',
'SyncComponents.manifest',
]
EXTRA_JS_MODULES['services-sync'] += [
'modules/addonsreconciler.js',
'modules/addonutils.js',
'modules/bookmark_validator.js',
'modules/browserid_identity.js',
'modules/collection_validator.js',
'modules/engines.js',
'modules/FxaMigrator.jsm',
'modules/healthreport.jsm',
'modules/identity.js',
'modules/jpakeclient.js',
'modules/keys.js',
@ -35,22 +32,12 @@ EXTRA_JS_MODULES['services-sync'] += [
'modules/record.js',
'modules/resource.js',
'modules/rest.js',
'modules/service.js',
'modules/status.js',
'modules/SyncedTabs.jsm',
'modules/telemetry.js',
'modules/userapi.js',
'modules/util.js',
]
EXTRA_PP_JS_MODULES['services-sync'] += [
'modules/constants.js',
'modules/service.js',
]
# Definitions used by constants.js
DEFINES['weave_version'] = '1.54.1'
DEFINES['weave_id'] = '{340c2bbc-ce74-4362-90b5-7c26312808ef}'
EXTRA_JS_MODULES['services-sync'].engines += [
'modules/engines/addons.js',
'modules/engines/bookmarks.js',
@ -78,3 +65,4 @@ TESTING_JS_MODULES.services.sync += [
JS_PREFERENCE_FILES += [
'services-sync.js',
]

View file

@ -2,17 +2,16 @@
* 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/. */
pref("services.sync.serverURL", "https://auth.services.mozilla.com/");
pref("services.sync.serverURL", "https://pmsync.palemoon.org/sync/index.php/");
pref("services.sync.userURL", "user/");
pref("services.sync.miscURL", "misc/");
pref("services.sync.termsURL", "https://services.mozilla.com/tos/");
pref("services.sync.privacyURL", "https://services.mozilla.com/privacy-policy/");
pref("services.sync.statusURL", "https://services.mozilla.com/status/");
pref("services.sync.syncKeyHelpURL", "https://services.mozilla.com/help/synckey");
pref("services.sync.termsURL", "http://www.palemoon.org/sync/terms.shtml");
pref("services.sync.privacyURL", "http://www.palemoon.org/sync/privacy.shtml");
pref("services.sync.statusURL", "https://pmsync.palemoon.org/status/");
pref("services.sync.syncKeyHelpURL", "http://www.palemoon.org/sync/keyhelp.shtml");
pref("services.sync.lastversion", "firstrun");
pref("services.sync.sendVersionInfo", true);
pref("services.sync.APILevel", 2);
pref("services.sync.scheduler.eolInterval", 604800); // 1 week
pref("services.sync.scheduler.idleInterval", 3600); // 1 hour
@ -25,29 +24,37 @@ pref("services.sync.scheduler.sync11.singleDeviceInterval", 86400); // 1 day
pref("services.sync.errorhandler.networkFailureReportTimeout", 1209600); // 2 weeks
pref("services.sync.engine.addons", true);
// A "master" pref for Sync being enabled. Will be set to false if the sync
// customization UI finds all our builtin engines disabled (and addons are
// free to force this to true if they have their own engine)
pref("services.sync.enabled", true);
// Our engines.
pref("services.sync.engine.addons", false);
pref("services.sync.engine.bookmarks", true);
pref("services.sync.engine.history", true);
pref("services.sync.engine.passwords", true);
pref("services.sync.engine.prefs", true);
pref("services.sync.engine.tabs", true);
pref("services.sync.engine.tabs.filteredUrls", "^(about:.*|chrome://weave/.*|wyciwyg:.*|file:.*|blob:.*)$");
pref("services.sync.engine.tabs.filteredUrls", "^(about:.*|chrome://weave/.*|wyciwyg:.*|file:.*)$");
pref("services.sync.jpake.serverURL", "https://setup.services.mozilla.com/");
pref("services.sync.jpake.serverURL", "https://keyserver.palemoon.org/");
pref("services.sync.jpake.pollInterval", 1000);
pref("services.sync.jpake.firstMsgMaxTries", 300); // 5 minutes
pref("services.sync.jpake.lastMsgMaxTries", 300); // 5 minutes
pref("services.sync.jpake.maxTries", 10);
// Allow add-ons to be synced from non-trusted sources.
pref("services.sync.addons.ignoreRepositoryChecking", true);
// If true, add-on sync ignores changes to the user-enabled flag. This
// allows people to have the same set of add-ons installed across all
// profiles while maintaining different enabled states.
pref("services.sync.addons.ignoreUserEnabledChanges", false);
// Comma-delimited list of hostnames to trust for add-on install.
pref("services.sync.addons.trustedSourceHostnames", "addons.mozilla.org");
pref("services.sync.addons.trustedSourceHostnames", "addons.palemoon.org,addons.mozilla.org");
pref("services.sync.log.appender.console", "Fatal");
pref("services.sync.log.appender.console", "Warn");
pref("services.sync.log.appender.dump", "Error");
pref("services.sync.log.appender.file.level", "Trace");
pref("services.sync.log.appender.file.logOnError", true);
@ -69,28 +76,12 @@ pref("services.sync.log.logger.engine.passwords", "Debug");
pref("services.sync.log.logger.engine.prefs", "Debug");
pref("services.sync.log.logger.engine.tabs", "Debug");
pref("services.sync.log.logger.engine.addons", "Debug");
pref("services.sync.log.logger.engine.extension-storage", "Debug");
pref("services.sync.log.logger.engine.apps", "Debug");
pref("services.sync.log.logger.identity", "Debug");
pref("services.sync.log.logger.userapi", "Debug");
pref("services.sync.log.cryptoDebug", false);
pref("services.sync.tokenServerURI", "https://token.services.mozilla.com/1.0/sync/1.5");
pref("services.sync.fxa.termsURL", "https://accounts.firefox.com/legal/terms");
pref("services.sync.fxa.privacyURL", "https://accounts.firefox.com/legal/privacy");
pref("services.sync.telemetry.submissionInterval", 43200); // 12 hours in seconds
pref("services.sync.telemetry.maxPayloadCount", 500);
// Note that services.sync.validation.enabled is located in application/[application name]/app/profile/[application name].js
// We consider validation this frequently. After considering validation, even
// if we don't end up validating, we won't try again unless this much time has passed.
pref("services.sync.validation.interval", 86400); // 24 hours in seconds
// We only run validation `services.sync.validation.percentageChance` percent of
// the time, even if it's been the right amount of time since the last validation,
// and you meet the maxRecord checks.
pref("services.sync.validation.percentageChance", 10);
// We won't validate an engine if it has more than this many records on the server.
pref("services.sync.validation.maxRecords", 100);

View file

@ -12,7 +12,7 @@
<application_id>1</application_id>
<min_version>3.6</min_version>
<max_version>*</max_version>
<appID>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</appID>
<appID>{8de7fcbb-c55c-4fbe-bfc5-fc555c87dbc4}</appID>
</application></compatible_applications>
<all_compatible_os><os>ALL</os></all_compatible_os>

View file

@ -12,7 +12,7 @@
<application_id>1</application_id>
<min_version>3.6</min_version>
<max_version>*</max_version>
<appID>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</appID>
<appID>{8de7fcbb-c55c-4fbe-bfc5-fc555c87dbc4}</appID>
</application></compatible_applications>
<all_compatible_os><os>ALL</os></all_compatible_os>

View file

@ -1,5 +1,4 @@
{ "tests": [
"test_bookmark_conflict.js",
"test_sync.js",
"test_prefs.js",
"test_tabs.js",
@ -17,6 +16,7 @@
"test_bug575423.js",
"test_bug546807.js",
"test_history_collision.js",
"test_privbrw_formdata.js",
"test_privbrw_passwords.js",
"test_privbrw_tabs.js",
"test_bookmarks_in_same_named_folder.js",

View file

@ -6,7 +6,7 @@
// syncs between profiles.
EnableEngines(["addons"]);
var phases = {
let phases = {
"phase01": "profile1",
"phase02": "profile1",
"phase03": "profile2",
@ -33,8 +33,7 @@ Phase("phase01", [
[Sync]
]);
Phase("phase02", [
[Addons.verify, [id], STATE_ENABLED],
[Sync]
[Addons.verify, [id], STATE_ENABLED]
]);
Phase("phase03", [
[Addons.verifyNot, [id]],
@ -42,7 +41,6 @@ Phase("phase03", [
]);
Phase("phase04", [
[Addons.verify, [id], STATE_ENABLED],
[Sync]
]);
// Now we disable the add-on
@ -53,15 +51,13 @@ Phase("phase05", [
]);
Phase("phase06", [
[Addons.verify, [id], STATE_DISABLED],
[Sync]
]);
Phase("phase07", [
[Addons.verify, [id], STATE_ENABLED],
[Sync]
]);
Phase("phase08", [
[Addons.verify, [id], STATE_DISABLED],
[Sync]
[Addons.verify, [id], STATE_DISABLED]
]);
// Now we re-enable it again.
@ -72,15 +68,13 @@ Phase("phase09", [
]);
Phase("phase10", [
[Addons.verify, [id], STATE_ENABLED],
[Sync]
]);
Phase("phase11", [
[Addons.verify, [id], STATE_DISABLED],
[Sync]
]);
Phase("phase12", [
[Addons.verify, [id], STATE_ENABLED],
[Sync]
[Addons.verify, [id], STATE_ENABLED]
]);
// And we uninstall it
@ -92,14 +86,12 @@ Phase("phase13", [
[Sync]
]);
Phase("phase14", [
[Addons.verifyNot, [id]],
[Sync]
[Addons.verifyNot, [id]]
]);
Phase("phase15", [
[Addons.verify, [id], STATE_ENABLED],
[Sync]
]);
Phase("phase16", [
[Addons.verifyNot, [id]],
[Sync]
[Addons.verifyNot, [id]]
]);

View file

@ -6,7 +6,7 @@
// the proper action is taken.
EnableEngines(["addons"]);
var phases = {
let phases = {
"phase01": "profile1",
"phase02": "profile2",
"phase03": "profile1",
@ -34,9 +34,6 @@ Phase("phase02", [
Phase("phase03", [
[Sync], // Get GUID updates, potentially.
[Addons.setEnabled, [id], STATE_DISABLED],
// We've changed the state, but don't want this profile to sync until phase5,
// so if we ran a validation now we'd be expecting to find errors.
[Addons.skipValidation]
]);
Phase("phase04", [
[EnsureTracking],

View file

@ -5,7 +5,7 @@
// other profiles.
EnableEngines(["addons"]);
var phases = {
let phases = {
"phase01": "profile1",
"phase02": "profile2",
"phase03": "profile1",

View file

@ -9,7 +9,7 @@
EnableEngines(["addons"]);
var phases = { "phase1": "profile1",
let phases = { "phase1": "profile1",
"phase2": "profile1" };
const id = "unsigned-xpi@tests.mozilla.org";
@ -25,6 +25,5 @@ Phase("phase1", [
Phase("phase2", [
// Add-on should be present after restart
[Addons.verify, [id], STATE_ENABLED],
[Sync] // Sync to ensure everything is initialized enough for the addon validator to run
[Addons.verify, [id], STATE_ENABLED]
]);

View file

@ -8,7 +8,7 @@
EnableEngines(["addons"]);
var phases = {
let phases = {
"phase01": "profile1",
"phase02": "profile1",
"phase03": "profile1"
@ -30,6 +30,5 @@ Phase("phase02", [
]);
Phase("phase03", [
[Addons.verify, [id1], STATE_ENABLED],
[Addons.verify, [id2], STATE_ENABLED],
[Sync] // Sync to ensure that the addon validator can run without error
[Addons.verify, [id2], STATE_ENABLED]
]);

View file

@ -1,143 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* The list of phases mapped to their corresponding profiles. The object
* here must be in strict JSON format, as it will get parsed by the Python
* testrunner (no single quotes, extra comma's, etc).
*/
EnableEngines(["bookmarks"]);
var phases = { "phase1": "profile1",
"phase2": "profile2",
"phase3": "profile1",
"phase4": "profile2" };
// the initial list of bookmarks to add to the browser
var bookmarksInitial = {
"menu": [
{ folder: "foldera" },
{ folder: "folderb" },
{ folder: "folderc" },
{ folder: "folderd" },
],
"menu/foldera": [{ uri: "http://www.cnn.com", title: "CNN" }],
"menu/folderb": [{ uri: "http://www.apple.com", title: "Apple", tags: [] }],
"menu/folderc": [{ uri: "http://www.yahoo.com", title: "Yahoo" }],
"menu/folderd": []
};
// a list of bookmarks to delete during a 'delete' action on P2
var bookmarksToDelete = {
"menu": [
{ folder: "foldera" },
{ folder: "folderb" },
],
"menu/folderc": [{ uri: "http://www.yahoo.com", title: "Yahoo" }],
};
// the modifications to make on P1, after P2 has synced, but before P1 has gotten
// P2's changes
var bookmarkMods = {
"menu": [
{ folder: "foldera" },
{ folder: "folderb" },
{ folder: "folderc" },
{ folder: "folderd" },
],
// we move this child out of its folder (p1), after deleting the folder (p2)
// and expect the child to come back to p2 after sync.
"menu/foldera": [{
uri: "http://www.cnn.com",
title: "CNN",
changes: { location: "menu/folderd" }
}],
// we rename this child (p1) after deleting the folder (p2), and expect the child
// to be moved into great grandparent (menu)
"menu/folderb": [{
uri: "http://www.apple.com",
title: "Apple",
tags: [],
changes: { title: "Mac" }
}],
// we move this child (p1) after deleting the child (p2) and expect it to survive
"menu/folderc": [{
uri: "http://www.yahoo.com",
title: "Yahoo",
changes: { location: "menu/folderd" }
}],
"menu/folderd": []
};
// a list of bookmarks to delete during a 'delete' action
var bookmarksToDelete = {
"menu": [
{ folder: "foldera" },
{ folder: "folderb" },
],
"menu/folderc": [
{ uri: "http://www.yahoo.com", title: "Yahoo" },
],
};
// expected bookmark state after conflict resolution
var bookmarksExpected = {
"menu": [
{ folder: "folderc" },
{ folder: "folderd" },
{ uri: "http://www.apple.com", title: "Mac", },
],
"menu/folderc": [],
"menu/folderd": [
{ uri: "http://www.cnn.com", title: "CNN" },
{ uri: "http://www.yahoo.com", title: "Yahoo" }
]
};
// Add bookmarks to profile1 and sync.
Phase("phase1", [
[Bookmarks.add, bookmarksInitial],
[Bookmarks.verify, bookmarksInitial],
[Sync],
[Bookmarks.verify, bookmarksInitial],
]);
// Sync to profile2 and verify that the bookmarks are present. Delete
// bookmarks/folders, verify that it's not present, and sync
Phase("phase2", [
[Sync],
[Bookmarks.verify, bookmarksInitial],
[Bookmarks.delete, bookmarksToDelete],
[Bookmarks.verifyNot, bookmarksToDelete],
[Sync]
]);
// Using profile1, modify the bookmarks, and sync *after* the modification,
// and then sync again to propagate the reconciliation changes.
Phase("phase3", [
[Bookmarks.verify, bookmarksInitial],
[Bookmarks.modify, bookmarkMods],
[Sync],
[Bookmarks.verify, bookmarksExpected],
[Bookmarks.verifyNot, bookmarksToDelete],
]);
// Back in profile2, do a sync and verify that we're in the expected state
Phase("phase4", [
[Sync],
[Bookmarks.verify, bookmarksExpected],
[Bookmarks.verifyNot, bookmarksToDelete],
]);

View file

@ -23,7 +23,7 @@ var prefs1 = [
{ name: "browser.urlbar.maxRichResults",
value: 20
},
{ name: "privacy.clearOnShutdown.siteSettings",
{ name: "security.OCSP.require",
value: true
}
];
@ -35,7 +35,7 @@ var prefs2 = [
{ name: "browser.urlbar.maxRichResults",
value: 18
},
{ name: "privacy.clearOnShutdown.siteSettings",
{ name: "security.OCSP.require",
value: false
}
];

View file

@ -88,8 +88,7 @@ Phase('phase2', [
[Sync],
[Bookmarks.verify, bookmarks_initial],
[Bookmarks.delete, bookmarks_to_delete],
[Bookmarks.verifyNot, bookmarks_to_delete],
[Bookmarks.skipValidation]
[Bookmarks.verifyNot, bookmarks_to_delete]
]);
// Using profile1, sync again with wipe-server set to true. Verify our

View file

@ -108,7 +108,7 @@ var prefs1 = [
{ name: "browser.urlbar.maxRichResults",
value: 20
},
{ name: "privacy.clearOnShutdown.siteSettings",
{ name: "security.OCSP.require",
value: true
}
];
@ -120,7 +120,7 @@ var prefs2 = [
{ name: "browser.urlbar.maxRichResults",
value: 18
},
{ name: "privacy.clearOnShutdown.siteSettings",
{ name: "security.OCSP.require",
value: false
}
];

View file

@ -31,11 +31,6 @@ var formdata1 = [
}
];
// This is currently pointless - it *looks* like it is trying to check that
// one of the entries in formdata1 has been removed, but (a) the delete code
// isn't active (see comments below), and (b) the way the verification works
// means it would never do the right thing - it only checks all the entries
// here exist, but not that they are the only entries in the DB.
var formdata2 = [
{ fieldname: "testing",
value: "success",
@ -52,11 +47,6 @@ var formdata_delete = [
}
];
var formdata_new = [
{ fieldname: "new-field",
value: "new-value"
}
]
/*
* Test phases
*/
@ -82,15 +72,12 @@ Phase('phase3', [
[Formdata.delete, formdata_delete],
//[Formdata.verifyNot, formdata_delete],
[Formdata.verify, formdata2],
// add new data after the first Sync, ensuring the tracker works.
[Formdata.add, formdata_new],
[Sync],
]);
Phase('phase4', [
[Sync],
[Formdata.verify, formdata2],
[Formdata.verify, formdata_new],
//[Formdata.verifyNot, formdata_delete]
]);

View file

@ -19,7 +19,7 @@ var prefs1 = [
{ name: "browser.urlbar.maxRichResults",
value: 20
},
{ name: "privacy.clearOnShutdown.siteSettings",
{ name: "security.OCSP.require",
value: true
}
];
@ -31,7 +31,7 @@ var prefs2 = [
{ name: "browser.urlbar.maxRichResults",
value: 18
},
{ name: "privacy.clearOnShutdown.siteSettings",
{ name: "security.OCSP.require",
value: false
}
];

View file

@ -0,0 +1,73 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* The list of phases mapped to their corresponding profiles. The object
* here must be in strict JSON format, as it will get parsed by the Python
* testrunner (no single quotes, extra comma's, etc).
*/
EnableEngines(["forms"]);
var phases = { "phase1": "profile1",
"phase2": "profile2",
"phase3": "profile1",
"phase4": "profile2" };
/*
* Form data
*/
// the form data to add to the browser
var formdata1 = [
{ fieldname: "name",
value: "xyz",
date: -1
},
{ fieldname: "email",
value: "abc@gmail.com",
date: -2
},
{ fieldname: "username",
value: "joe"
}
];
// the form data to add in private browsing mode
var formdata2 = [
{ fieldname: "password",
value: "secret",
date: -1
},
{ fieldname: "city",
value: "mtview"
}
];
/*
* Test phases
*/
Phase('phase1', [
[Formdata.add, formdata1],
[Formdata.verify, formdata1],
[Sync]
]);
Phase('phase2', [
[Sync],
[Formdata.verify, formdata1]
]);
Phase('phase3', [
[Sync],
[Windows.add, { private: true }],
[Formdata.add, formdata2],
[Formdata.verify, formdata2],
[Sync],
]);
Phase('phase4', [
[Sync],
[Formdata.verify, formdata1],
[Formdata.verifyNot, formdata2]
]);

View file

@ -4,7 +4,7 @@ Cu.import("resource://services-sync/util.js");
// Fake Sample Data
// ----------------------------------------
var fakeSampleLogins = [
let fakeSampleLogins = [
// Fake nsILoginInfo object.
{hostname: "www.boogle.com",
formSubmitURL: "http://www.boogle.com/search",

View file

@ -1,54 +1,65 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
var {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
var gSyncProfile;
let gSyncProfile;
gSyncProfile = do_get_profile();
// Init FormHistoryStartup and pretend we opened a profile.
var fhs = Cc["@mozilla.org/satchel/form-history-startup;1"]
let fhs = Cc["@mozilla.org/satchel/form-history-startup;1"]
.getService(Ci.nsIObserver);
fhs.observe(null, "profile-after-change", null);
// An app is going to have some prefs set which xpcshell tests don't.
Services.prefs.setCharPref("identity.sync.tokenserver.uri", "http://token-server");
// Set the validation prefs to attempt validation every time to avoid non-determinism.
Services.prefs.setIntPref("services.sync.validation.interval", 0);
Services.prefs.setIntPref("services.sync.validation.percentageChance", 100);
Services.prefs.setIntPref("services.sync.validation.maxRecords", -1);
Services.prefs.setBoolPref("services.sync.validation.enabled", true);
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
// Make sure to provide the right OS so crypto loads the right binaries
function getOS() {
switch (mozinfo.os) {
case "win":
return "WINNT";
case "mac":
return "Darwin";
default:
return "Linux";
}
}
let OS = "XPCShell";
if ("@mozilla.org/windows-registry-key;1" in Cc)
OS = "WINNT";
else if ("nsILocalFileMac" in Ci)
OS = "Darwin";
else
OS = "Linux";
Cu.import("resource://testing-common/AppInfo.jsm", this);
updateAppInfo({
let XULAppInfo = {
vendor: "Mozilla",
name: "XPCShell",
ID: "xpcshell@tests.mozilla.org",
version: "1",
appBuildID: "20100621",
platformVersion: "",
OS: getOS(),
});
platformBuildID: "20100621",
inSafeMode: false,
logConsoleErrors: true,
OS: OS,
XPCOMABI: "noarch-spidermonkey",
QueryInterface: XPCOMUtils.generateQI([Ci.nsIXULAppInfo, Ci.nsIXULRuntime]),
invalidateCachesOnRestart: function invalidateCachesOnRestart() { }
};
let XULAppInfoFactory = {
createInstance: function (outer, iid) {
if (outer != null)
throw Cr.NS_ERROR_NO_AGGREGATION;
return XULAppInfo.QueryInterface(iid);
}
};
let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
registrar.registerFactory(Components.ID("{fbfae60b-64a4-44ef-a911-08ceb70b9f31}"),
"XULAppInfo", "@mozilla.org/xre/app-info;1",
XULAppInfoFactory);
// Register resource aliases. Normally done in SyncComponents.manifest.
function addResourceAlias() {
Cu.import("resource://gre/modules/Services.jsm");
const resProt = Services.io.getProtocolHandler("resource")
.QueryInterface(Ci.nsIResProtocolHandler);
for (let s of ["common", "sync", "crypto"]) {
for each (let s in ["common", "sync", "crypto"]) {
let uri = Services.io.newURI("resource://gre/modules/services-" + s + "/", null,
null);
resProt.setSubstitution("services-" + s, uri);

View file

@ -1,112 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
Cu.import("resource://services-sync/engines.js");
// Common code for test_errorhandler_{1,2}.js -- pulled out to make it less
// monolithic and take less time to execute.
const EHTestsCommon = {
service_unavailable(request, response) {
let body = "Service Unavailable";
response.setStatusLine(request.httpVersion, 503, "Service Unavailable");
response.setHeader("Retry-After", "42");
response.bodyOutputStream.write(body, body.length);
},
sync_httpd_setup() {
let global = new ServerWBO("global", {
syncID: Service.syncID,
storageVersion: STORAGE_VERSION,
engines: {clients: {version: Service.clientsEngine.version,
syncID: Service.clientsEngine.syncID},
catapult: {version: Service.engineManager.get("catapult").version,
syncID: Service.engineManager.get("catapult").syncID}}
});
let clientsColl = new ServerCollection({}, true);
// Tracking info/collections.
let collectionsHelper = track_collections_helper();
let upd = collectionsHelper.with_updated_collection;
let handler_401 = httpd_handler(401, "Unauthorized");
return httpd_setup({
// Normal server behaviour.
"/1.1/johndoe/storage/meta/global": upd("meta", global.handler()),
"/1.1/johndoe/info/collections": collectionsHelper.handler,
"/1.1/johndoe/storage/crypto/keys":
upd("crypto", (new ServerWBO("keys")).handler()),
"/1.1/johndoe/storage/clients": upd("clients", clientsColl.handler()),
// Credentials are wrong or node reallocated.
"/1.1/janedoe/storage/meta/global": handler_401,
"/1.1/janedoe/info/collections": handler_401,
// Maintenance or overloaded (503 + Retry-After) at info/collections.
"/maintenance/1.1/broken.info/info/collections": EHTestsCommon.service_unavailable,
// Maintenance or overloaded (503 + Retry-After) at meta/global.
"/maintenance/1.1/broken.meta/storage/meta/global": EHTestsCommon.service_unavailable,
"/maintenance/1.1/broken.meta/info/collections": collectionsHelper.handler,
// Maintenance or overloaded (503 + Retry-After) at crypto/keys.
"/maintenance/1.1/broken.keys/storage/meta/global": upd("meta", global.handler()),
"/maintenance/1.1/broken.keys/info/collections": collectionsHelper.handler,
"/maintenance/1.1/broken.keys/storage/crypto/keys": EHTestsCommon.service_unavailable,
// Maintenance or overloaded (503 + Retry-After) at wiping collection.
"/maintenance/1.1/broken.wipe/info/collections": collectionsHelper.handler,
"/maintenance/1.1/broken.wipe/storage/meta/global": upd("meta", global.handler()),
"/maintenance/1.1/broken.wipe/storage/crypto/keys":
upd("crypto", (new ServerWBO("keys")).handler()),
"/maintenance/1.1/broken.wipe/storage": EHTestsCommon.service_unavailable,
"/maintenance/1.1/broken.wipe/storage/clients": upd("clients", clientsColl.handler()),
"/maintenance/1.1/broken.wipe/storage/catapult": EHTestsCommon.service_unavailable
});
},
CatapultEngine: (function() {
function CatapultEngine() {
SyncEngine.call(this, "Catapult", Service);
}
CatapultEngine.prototype = {
__proto__: SyncEngine.prototype,
exception: null, // tests fill this in
_sync: function _sync() {
if (this.exception) {
throw this.exception;
}
}
};
return CatapultEngine;
}()),
generateCredentialsChangedFailure() {
// Make sync fail due to changed credentials. We simply re-encrypt
// the keys with a different Sync Key, without changing the local one.
let newSyncKeyBundle = new SyncKeyBundle("johndoe", "23456234562345623456234562");
let keys = Service.collectionKeys.asWBO();
keys.encrypt(newSyncKeyBundle);
keys.upload(Service.resource(Service.cryptoKeysURL));
},
setUp(server) {
return configureIdentity({ username: "johndoe" }).then(
() => {
Service.serverURL = server.baseURI + "/";
Service.clusterURL = server.baseURI + "/";
}
).then(
() => EHTestsCommon.generateAndUploadKeys()
);
},
generateAndUploadKeys() {
generateNewKeys(Service.collectionKeys);
let serverKeys = Service.collectionKeys.asWBO("crypto", "keys");
serverKeys.encrypt(Service.identity.syncKeyBundle);
return serverKeys.upload(Service.resource(Service.cryptoKeysURL)).success;
}
};

View file

@ -4,39 +4,8 @@
Cu.import("resource://services-common/async.js");
Cu.import("resource://testing-common/services/common/utils.js");
Cu.import("resource://testing-common/PlacesTestUtils.jsm");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyGetter(this, 'SyncPingSchema', function() {
let ns = {};
Cu.import("resource://gre/modules/FileUtils.jsm", ns);
let stream = Cc["@mozilla.org/network/file-input-stream;1"]
.createInstance(Ci.nsIFileInputStream);
let jsonReader = Cc["@mozilla.org/dom/json;1"]
.createInstance(Components.interfaces.nsIJSON);
let schema;
try {
let schemaFile = do_get_file("sync_ping_schema.json");
stream.init(schemaFile, ns.FileUtils.MODE_RDONLY, ns.FileUtils.PERMS_FILE, 0);
schema = jsonReader.decodeFromStream(stream, stream.available());
} finally {
stream.close();
}
// Allow tests to make whatever engines they want, this shouldn't cause
// validation failure.
schema.definitions.engine.properties.name = { type: "string" };
return schema;
});
XPCOMUtils.defineLazyGetter(this, 'SyncPingValidator', function() {
let ns = {};
Cu.import("resource://testing-common/ajv-4.1.1.js", ns);
let ajv = new ns.Ajv({ async: "co*" });
return ajv.compile(SyncPingSchema);
});
var provider = {
let provider = {
getFile: function(prop, persistent) {
persistent.value = true;
switch (prop) {
@ -51,7 +20,7 @@ var provider = {
Services.dirsvc.QueryInterface(Ci.nsIDirectoryService).registerProvider(provider);
// This is needed for loadAddonTestFunctions().
var gGlobalScope = this;
let gGlobalScope = this;
function ExtensionsTestPath(path) {
if (path[0] != "/") {
@ -76,24 +45,6 @@ function loadAddonTestFunctions() {
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
}
function webExtensionsTestPath(path) {
if (path[0] != "/") {
throw Error("Path must begin with '/': " + path);
}
return "../../../../toolkit/components/extensions/test/xpcshell" + path;
}
/**
* Loads the WebExtension test functions by importing its test file.
*/
function loadWebExtensionTestFunctions() {
const path = webExtensionsTestPath("/head_sync.js");
let file = do_get_file(path);
let uri = Services.io.newFileURI(file);
Services.scriptloader.loadSubScript(uri.spec, gGlobalScope);
}
function getAddonInstall(name) {
let f = do_get_file(ExtensionsTestPath("/addons/" + name + ".xpi"));
let cb = Async.makeSyncCallback();
@ -255,192 +206,3 @@ function do_check_array_eq(a1, a2) {
do_check_eq(a1[i], a2[i]);
}
}
// Helper function to get the sync telemetry and add the typically used test
// engine names to its list of allowed engines.
function get_sync_test_telemetry() {
let ns = {};
Cu.import("resource://services-sync/telemetry.js", ns);
let testEngines = ["rotary", "steam", "sterling", "catapult"];
for (let engineName of testEngines) {
ns.SyncTelemetry.allowedEngines.add(engineName);
}
ns.SyncTelemetry.submissionInterval = -1;
return ns.SyncTelemetry;
}
function assert_valid_ping(record) {
// This is called as the test harness tears down due to shutdown. This
// will typically have no recorded syncs, and the validator complains about
// it. So ignore such records (but only ignore when *both* shutdown and
// no Syncs - either of them not being true might be an actual problem)
if (record && (record.why != "shutdown" || record.syncs.length != 0)) {
if (!SyncPingValidator(record)) {
deepEqual([], SyncPingValidator.errors, "Sync telemetry ping validation failed");
}
equal(record.version, 1);
record.syncs.forEach(p => {
lessOrEqual(p.when, Date.now());
if (p.devices) {
ok(!p.devices.some(device => device.id == p.deviceID));
equal(new Set(p.devices.map(device => device.id)).size,
p.devices.length, "Duplicate device ids in ping devices list");
}
});
}
}
// Asserts that `ping` is a ping that doesn't contain any failure information
function assert_success_ping(ping) {
ok(!!ping);
assert_valid_ping(ping);
ping.syncs.forEach(record => {
ok(!record.failureReason);
equal(undefined, record.status);
greater(record.engines.length, 0);
for (let e of record.engines) {
ok(!e.failureReason);
equal(undefined, e.status);
if (e.validation) {
equal(undefined, e.validation.problems);
equal(undefined, e.validation.failureReason);
}
if (e.outgoing) {
for (let o of e.outgoing) {
equal(undefined, o.failed);
notEqual(undefined, o.sent);
}
}
if (e.incoming) {
equal(undefined, e.incoming.failed);
equal(undefined, e.incoming.newFailed);
notEqual(undefined, e.incoming.applied || e.incoming.reconciled);
}
}
});
}
// Hooks into telemetry to validate all pings after calling.
function validate_all_future_pings() {
let telem = get_sync_test_telemetry();
telem.submit = assert_valid_ping;
}
function wait_for_ping(callback, allowErrorPings, getFullPing = false) {
return new Promise(resolve => {
let telem = get_sync_test_telemetry();
let oldSubmit = telem.submit;
telem.submit = function(record) {
telem.submit = oldSubmit;
if (allowErrorPings) {
assert_valid_ping(record);
} else {
assert_success_ping(record);
}
if (getFullPing) {
resolve(record);
} else {
equal(record.syncs.length, 1);
resolve(record.syncs[0]);
}
};
callback();
});
}
// Short helper for wait_for_ping
function sync_and_validate_telem(allowErrorPings, getFullPing = false) {
return wait_for_ping(() => Service.sync(), allowErrorPings, getFullPing);
}
// Used for the (many) cases where we do a 'partial' sync, where only a single
// engine is actually synced, but we still want to ensure we're generating a
// valid ping. Returns a promise that resolves to the ping, or rejects with the
// thrown error after calling an optional callback.
function sync_engine_and_validate_telem(engine, allowErrorPings, onError) {
return new Promise((resolve, reject) => {
let telem = get_sync_test_telemetry();
let caughtError = null;
// Clear out status, so failures from previous syncs won't show up in the
// telemetry ping.
let ns = {};
Cu.import("resource://services-sync/status.js", ns);
ns.Status._engines = {};
ns.Status.partial = false;
// Ideally we'd clear these out like we do with engines, (probably via
// Status.resetSync()), but this causes *numerous* tests to fail, so we just
// assume that if no failureReason or engine failures are set, and the
// status properties are the same as they were initially, that it's just
// a leftover.
// This is only an issue since we're triggering the sync of just one engine,
// without doing any other parts of the sync.
let initialServiceStatus = ns.Status._service;
let initialSyncStatus = ns.Status._sync;
let oldSubmit = telem.submit;
telem.submit = function(ping) {
telem.submit = oldSubmit;
ping.syncs.forEach(record => {
if (record && record.status) {
// did we see anything to lead us to believe that something bad actually happened
let realProblem = record.failureReason || record.engines.some(e => {
if (e.failureReason || e.status) {
return true;
}
if (e.outgoing && e.outgoing.some(o => o.failed > 0)) {
return true;
}
return e.incoming && e.incoming.failed;
});
if (!realProblem) {
// no, so if the status is the same as it was initially, just assume
// that its leftover and that we can ignore it.
if (record.status.sync && record.status.sync == initialSyncStatus) {
delete record.status.sync;
}
if (record.status.service && record.status.service == initialServiceStatus) {
delete record.status.service;
}
if (!record.status.sync && !record.status.service) {
delete record.status;
}
}
}
});
if (allowErrorPings) {
assert_valid_ping(ping);
} else {
assert_success_ping(ping);
}
equal(ping.syncs.length, 1);
if (caughtError) {
if (onError) {
onError(ping.syncs[0]);
}
reject(caughtError);
} else {
resolve(ping.syncs[0]);
}
}
Svc.Obs.notify("weave:service:sync:start");
try {
engine.sync();
} catch (e) {
caughtError = e;
}
if (caughtError) {
Svc.Obs.notify("weave:service:sync:error", caughtError);
} else {
Svc.Obs.notify("weave:service:sync:finish");
}
});
}
// Avoid an issue where `client.name2` containing unicode characters causes
// a number of tests to fail, due to them assuming that we do not need to utf-8
// encode or decode data sent through the mocked server (see bug 1268912).
Utils.getDefaultDeviceName = function() {
return "Test device name";
};

View file

@ -1,4 +1,4 @@
var Cm = Components.manager;
const Cm = Components.manager;
// Shared logging for all HTTP server functions.
Cu.import("resource://gre/modules/Log.jsm");
@ -178,13 +178,9 @@ ServerCollection.prototype = {
* @return an array of IDs.
*/
keys: function keys(filter) {
let ids = [];
for (let [id, wbo] of Object.entries(this._wbos)) {
if (wbo.payload && (!filter || filter(id, wbo))) {
ids.push(id);
}
}
return ids;
return [id for ([id, wbo] in Iterator(this._wbos))
if (wbo.payload &&
(!filter || filter(id, wbo)))];
},
/**
@ -198,13 +194,8 @@ ServerCollection.prototype = {
* @return an array of ServerWBOs.
*/
wbos: function wbos(filter) {
let os = [];
for (let [id, wbo] of Object.entries(this._wbos)) {
if (wbo.payload) {
os.push(wbo);
}
}
let os = [wbo for ([id, wbo] in Iterator(this._wbos))
if (wbo.payload)];
if (filter) {
return os.filter(filter);
}
@ -276,7 +267,7 @@ ServerCollection.prototype = {
count: function(options) {
options = options || {};
let c = 0;
for (let [id, wbo] of Object.entries(this._wbos)) {
for (let [id, wbo] in Iterator(this._wbos)) {
if (wbo.modified && this._inResultSet(wbo, options)) {
c++;
}
@ -287,23 +278,12 @@ ServerCollection.prototype = {
get: function(options) {
let result;
if (options.full) {
let data = [];
for (let [id, wbo] of Object.entries(this._wbos)) {
// Drop deleted.
if (wbo.modified && this._inResultSet(wbo, options)) {
data.push(wbo.get());
}
}
let start = options.offset || 0;
let data = [wbo.get() for ([id, wbo] in Iterator(this._wbos))
// Drop deleted.
if (wbo.modified &&
this._inResultSet(wbo, options))];
if (options.limit) {
let numItemsPastOffset = data.length - start;
data = data.slice(start, start + options.limit);
// use options as a backchannel to set x-weave-next-offset
if (numItemsPastOffset > options.limit) {
options.nextOffset = start + options.limit;
}
} else if (start) {
data = data.slice(start);
data = data.slice(0, options.limit);
}
// Our implementation of application/newlines.
result = data.join("\n") + "\n";
@ -311,18 +291,10 @@ ServerCollection.prototype = {
// Use options as a backchannel to report count.
options.recordCount = data.length;
} else {
let data = [];
for (let [id, wbo] of Object.entries(this._wbos)) {
if (this._inResultSet(wbo, options)) {
data.push(id);
}
}
let start = options.offset || 0;
let data = [id for ([id, wbo] in Iterator(this._wbos))
if (this._inResultSet(wbo, options))];
if (options.limit) {
data = data.slice(start, start + options.limit);
options.nextOffset = start + options.limit;
} else if (start) {
data = data.slice(start);
data = data.slice(0, options.limit);
}
result = JSON.stringify(data);
options.recordCount = data.length;
@ -337,8 +309,7 @@ ServerCollection.prototype = {
// This will count records where we have an existing ServerWBO
// registered with us as successful and all other records as failed.
for (let key in input) {
let record = input[key];
for each (let record in input) {
let wbo = this.wbo(record.id);
if (!wbo && this.acceptNew) {
this._log.debug("Creating WBO " + JSON.stringify(record.id) +
@ -361,7 +332,7 @@ ServerCollection.prototype = {
delete: function(options) {
let deleted = [];
for (let [id, wbo] of Object.entries(this._wbos)) {
for (let [id, wbo] in Iterator(this._wbos)) {
if (this._inResultSet(wbo, options)) {
this._log.debug("Deleting " + JSON.stringify(wbo));
deleted.push(wbo.id);
@ -383,7 +354,7 @@ ServerCollection.prototype = {
// Parse queryString
let options = {};
for (let chunk of request.queryString.split("&")) {
for each (let chunk in request.queryString.split("&")) {
if (!chunk) {
continue;
}
@ -403,36 +374,29 @@ ServerCollection.prototype = {
if (options.limit) {
options.limit = parseInt(options.limit, 10);
}
if (options.offset) {
options.offset = parseInt(options.offset, 10);
}
switch(request.method) {
case "GET":
body = self.get(options, request);
// see http://moz-services-docs.readthedocs.io/en/latest/storage/apis-1.5.html
// for description of these headers.
let { recordCount: records, nextOffset } = options;
self._log.info("Records: " + records + ", nextOffset: " + nextOffset);
body = self.get(options);
// "If supported by the db, this header will return the number of
// records total in the request body of any multiple-record GET
// request."
let records = options.recordCount;
self._log.info("Records: " + records);
if (records != null) {
response.setHeader("X-Weave-Records", "" + records);
}
if (nextOffset) {
response.setHeader("X-Weave-Next-Offset", "" + nextOffset);
}
response.setHeader("X-Last-Modified", "" + this.timestamp);
break;
case "POST":
let res = self.post(readBytesFromInputStream(request.bodyInputStream), request);
let res = self.post(readBytesFromInputStream(request.bodyInputStream));
body = JSON.stringify(res);
response.newModified = res.modified;
break;
case "DELETE":
self._log.debug("Invoking ServerCollection.DELETE.");
let deleted = self.delete(options, request);
let deleted = self.delete(options);
let ts = new_timestamp();
body = JSON.stringify(ts);
response.newModified = ts;
@ -541,7 +505,7 @@ function track_collections_helper() {
* find out what it needs without monkeypatching. Use this object as your
* prototype, and override as appropriate.
*/
var SyncServerCallback = {
let SyncServerCallback = {
onCollectionDeleted: function onCollectionDeleted(user, collection) {},
onItemDeleted: function onItemDeleted(user, collection, wboID) {},
@ -581,13 +545,13 @@ SyncServer.prototype = {
* Start the SyncServer's underlying HTTP server.
*
* @param port
* The numeric port on which to start. -1 implies the default, a
* randomly chosen port.
* The numeric port on which to start. A falsy value implies the
* default, a randomly chosen port.
* @param cb
* A callback function (of no arguments) which is invoked after
* startup.
*/
start: function start(port = -1, cb) {
start: function start(port, cb) {
if (this.started) {
this._log.warn("Warning: server already started on " + this.port);
return;
@ -605,7 +569,7 @@ SyncServer.prototype = {
} catch (ex) {
_("==========================================");
_("Got exception starting Sync HTTP server.");
_("Error: " + Log.exceptionStr(ex));
_("Error: " + Utils.exceptionStr(ex));
_("Is there a process already listening on port " + port + "?");
_("==========================================");
do_throw(ex);
@ -703,10 +667,10 @@ SyncServer.prototype = {
throw new Error("Unknown user.");
}
let userCollections = this.users[username].collections;
for (let [id, contents] of Object.entries(collections)) {
for (let [id, contents] in Iterator(collections)) {
let coll = userCollections[id] ||
this._insertCollection(userCollections, id);
for (let [wboID, payload] of Object.entries(contents)) {
for (let [wboID, payload] in Iterator(contents)) {
coll.insert(wboID, payload);
}
}
@ -740,8 +704,7 @@ SyncServer.prototype = {
throw new Error("Unknown user.");
}
let userCollections = this.users[username].collections;
for (let name in userCollections) {
let coll = userCollections[name];
for each (let [name, coll] in Iterator(userCollections)) {
this._log.trace("Bulk deleting " + name + " for " + username + "...");
coll.delete({});
}
@ -805,10 +768,7 @@ SyncServer.prototype = {
*/
respond: function respond(req, resp, code, status, body, headers) {
resp.setStatusLine(req.httpVersion, code, status);
if (!headers)
headers = this.defaultHeaders;
for (let header in headers) {
let value = headers[header];
for each (let [header, value] in Iterator(headers || this.defaultHeaders)) {
resp.setHeader(header, value);
}
resp.setHeader("X-Weave-Timestamp", "" + this.timestamp(), false);
@ -1035,7 +995,7 @@ SyncServer.prototype = {
*/
function serverForUsers(users, contents, callback) {
let server = new SyncServer(callback);
for (let [user, pass] of Object.entries(users)) {
for (let [user, pass] in Iterator(users)) {
server.registerUser(user, pass);
server.createContents(user, contents);
}

View file

@ -1,25 +0,0 @@
// This is a "preferences" file used by test_prefs_store.js
// The prefs that control what should be synced.
// Most of these are "default" prefs, so the value itself will not sync.
pref("services.sync.prefs.sync.testing.int", true);
pref("services.sync.prefs.sync.testing.string", true);
pref("services.sync.prefs.sync.testing.bool", true);
pref("services.sync.prefs.sync.testing.dont.change", true);
// this one is a user pref, so it *will* sync.
user_pref("services.sync.prefs.sync.testing.turned.off", false);
pref("services.sync.prefs.sync.testing.nonexistent", true);
pref("services.sync.prefs.sync.testing.default", true);
// The preference values - these are all user_prefs, otherwise their value
// will not be synced.
user_pref("testing.int", 123);
user_pref("testing.string", "ohai");
user_pref("testing.bool", true);
user_pref("testing.dont.change", "Please don't change me.");
user_pref("testing.turned.off", "I won't get synced.");
user_pref("testing.not.turned.on", "I won't get synced either!");
// A pref that exists but still has the default value - will be synced with
// null as the value.
pref("testing.default", "I'm the default value");

View file

@ -1,198 +0,0 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "schema for Sync pings, documentation avaliable in toolkit/components/telemetry/docs/sync-ping.rst",
"type": "object",
"additionalProperties": false,
"required": ["version", "syncs", "why"],
"properties": {
"version": { "type": "integer", "minimum": 0 },
"discarded": { "type": "integer", "minimum": 1 },
"why": { "enum": ["shutdown", "schedule"] },
"syncs": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#/definitions/payload" }
}
},
"definitions": {
"payload": {
"type": "object",
"additionalProperties": false,
"required": ["when", "uid", "took"],
"properties": {
"didLogin": { "type": "boolean" },
"when": { "type": "integer" },
"uid": {
"type": "string",
"pattern": "^[0-9a-f]{32}$"
},
"devices": {
"type": "array",
"items": { "$ref": "#/definitions/device" }
},
"deviceID": {
"type": "string",
"pattern": "^[0-9a-f]{64}$"
},
"status": {
"type": "object",
"anyOf": [
{ "required": ["sync"] },
{ "required": ["service"] }
],
"additionalProperties": false,
"properties": {
"sync": { "type": "string" },
"service": { "type": "string" }
}
},
"why": { "enum": ["startup", "schedule", "score", "user", "tabs"] },
"took": { "type": "integer", "minimum": -1 },
"failureReason": { "$ref": "#/definitions/error" },
"engines": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#/definitions/engine" }
}
}
},
"device": {
"required": ["os", "id", "version"],
"additionalProperties": false,
"type": "object",
"properties": {
"id": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
"os": { "type": "string" },
"version": { "type": "string" }
}
},
"engine": {
"required": ["name"],
"additionalProperties": false,
"properties": {
"failureReason": { "$ref": "#/definitions/error" },
"name": { "enum": ["addons", "bookmarks", "clients", "forms", "history", "passwords", "prefs", "tabs"] },
"took": { "type": "integer", "minimum": 1 },
"status": { "type": "string" },
"incoming": {
"type": "object",
"additionalProperties": false,
"anyOf": [
{"required": ["applied"]},
{"required": ["failed"]},
{"required": ["newFailed"]},
{"required": ["reconciled"]}
],
"properties": {
"applied": { "type": "integer", "minimum": 1 },
"failed": { "type": "integer", "minimum": 1 },
"newFailed": { "type": "integer", "minimum": 1 },
"reconciled": { "type": "integer", "minimum": 1 }
}
},
"outgoing": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#/definitions/outgoingBatch" }
},
"validation": {
"type": "object",
"additionalProperties": false,
"anyOf": [
{ "required": ["checked"] },
{ "required": ["failureReason"] }
],
"properties": {
"checked": { "type": "integer", "minimum": 0 },
"failureReason": { "$ref": "#/definitions/error" },
"took": { "type": "integer" },
"version": { "type": "integer" },
"problems": {
"type": "array",
"minItems": 1,
"$ref": "#/definitions/validationProblem"
}
}
}
}
},
"outgoingBatch": {
"type": "object",
"additionalProperties": false,
"anyOf": [
{"required": ["sent"]},
{"required": ["failed"]}
],
"properties": {
"sent": { "type": "integer", "minimum": 1 },
"failed": { "type": "integer", "minimum": 1 }
}
},
"error": {
"oneOf": [
{ "$ref": "#/definitions/httpError" },
{ "$ref": "#/definitions/nsError" },
{ "$ref": "#/definitions/shutdownError" },
{ "$ref": "#/definitions/authError" },
{ "$ref": "#/definitions/otherError" },
{ "$ref": "#/definitions/unexpectedError" },
{ "$ref": "#/definitions/sqlError" }
]
},
"httpError": {
"required": ["name", "code"],
"properties": {
"name": { "enum": ["httperror"] },
"code": { "type": "integer" }
}
},
"nsError": {
"required": ["name", "code"],
"properties": {
"name": { "enum": ["nserror"] },
"code": { "type": "integer" }
}
},
"shutdownError": {
"required": ["name"],
"properties": {
"name": { "enum": ["shutdownerror"] }
}
},
"authError": {
"required": ["name"],
"properties": {
"name": { "enum": ["autherror"] },
"from": { "enum": ["tokenserver", "fxaccounts", "hawkclient"] }
}
},
"otherError": {
"required": ["name"],
"properties": {
"name": { "enum": ["othererror"] },
"error": { "type": "string" }
}
},
"unexpectedError": {
"required": ["name"],
"properties": {
"name": { "enum": ["unexpectederror"] },
"error": { "type": "string" }
}
},
"sqlError": {
"required": ["name"],
"properties": {
"name": { "enum": ["sqlerror"] },
"code": { "type": "integer" }
}
},
"validationProblem": {
"required": ["name", "count"],
"properties": {
"name": { "type": "string" },
"count": { "type": "integer" }
}
}
}
}

View file

@ -1,27 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<searchresults total_results="1">
<addon id="5618">
<name>System Add-on Test</name>
<type id="1">Extension</type>
<guid>system1@tests.mozilla.org</guid>
<slug>addon11</slug>
<version>1.0</version>
<compatible_applications><application>
<name>Firefox</name>
<application_id>1</application_id>
<min_version>3.6</min_version>
<max_version>*</max_version>
<appID>xpcshell@tests.mozilla.org</appID>
</application></compatible_applications>
<all_compatible_os><os>ALL</os></all_compatible_os>
<install os="ALL" size="999">http://127.0.0.1:8888/system.xpi</install>
<created epoch="1252903662">
2009-09-14T04:47:42Z
</created>
<last_updated epoch="1315255329">
2011-09-05T20:42:09Z
</last_updated>
</addon>
</searchresults>

View file

@ -3,7 +3,6 @@
"use strict";
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://services-sync/addonutils.js");
Cu.import("resource://services-sync/util.js");
@ -11,7 +10,7 @@ Cu.import("resource://services-sync/util.js");
const HTTP_PORT = 8888;
const SERVER_ADDRESS = "http://127.0.0.1:8888";
var prefs = new Preferences();
let prefs = new Preferences();
prefs.set("extensions.getAddons.get.url",
SERVER_ADDRESS + "/search/guid:%IDS%");
@ -36,7 +35,7 @@ function createAndStartHTTPServer(port=HTTP_PORT) {
return server;
} catch (ex) {
_("Got exception starting HTTP server on port " + port);
_("Error: " + Log.exceptionStr(ex));
_("Error: " + Utils.exceptionStr(ex));
do_throw(ex);
}
}
@ -61,9 +60,6 @@ add_test(function test_handle_empty_source_uri() {
do_check_true("installedIDs" in result);
do_check_eq(0, result.installedIDs.length);
do_check_true("skipped" in result);
do_check_true(result.skipped.includes(ID));
server.stop(run_next_test);
});
@ -83,18 +79,44 @@ add_test(function test_ignore_untrusted_source_uris() {
let sourceURI = ioService.newURI(s, null, null);
let addon = {sourceURI: sourceURI, name: "bad", id: "bad"};
let canInstall = AddonUtils.canInstallAddon(addon);
do_check_false(canInstall, "Correctly rejected a bad URL");
try {
let cb = Async.makeSpinningCallback();
AddonUtils.getInstallFromSearchResult(addon, cb, true);
cb.wait();
} catch (ex) {
do_check_neq(null, ex);
do_check_eq(0, ex.message.indexOf("Insecure source URI"));
continue;
}
// We should never get here if an exception is thrown.
do_check_true(false);
}
let count = 0;
for (let s of good) {
let sourceURI = ioService.newURI(s, null, null);
let addon = {sourceURI: sourceURI, name: "good", id: "good"};
let canInstall = AddonUtils.canInstallAddon(addon);
do_check_true(canInstall, "Correctly accepted a good URL");
// Despite what you might think, we don't get an error in the callback.
// The install won't work because the underlying Addon instance wasn't
// proper. But, that just results in an AddonInstall that is missing
// certain values. We really just care that the callback is being invoked
// anyway.
let callback = function onInstall(error, install) {
do_check_null(error);
do_check_neq(null, install);
do_check_eq(sourceURI.spec, install.sourceURI.spec);
count += 1;
if (count >= good.length) {
run_next_test();
}
};
AddonUtils.getInstallFromSearchResult(addon, callback, true);
}
run_next_test();
});
add_test(function test_source_uri_rewrite() {
@ -103,6 +125,8 @@ add_test(function test_source_uri_rewrite() {
// This tests for conformance with bug 708134 so server-side metrics aren't
// skewed.
Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
// We resort to monkeypatching because of the API design.
let oldFunction = AddonUtils.__proto__.installAddonFromSearchResult;
@ -127,15 +151,12 @@ add_test(function test_source_uri_rewrite() {
let server = createAndStartHTTPServer();
let installCallback = Async.makeSpinningCallback();
let installOptions = {
id: "rewrite@tests.mozilla.org",
requireSecureURI: false,
}
AddonUtils.installAddons([installOptions], installCallback);
AddonUtils.installAddons([{id: "rewrite@tests.mozilla.org"}], installCallback);
installCallback.wait();
do_check_true(installCalled);
AddonUtils.__proto__.installAddonFromSearchResult = oldFunction;
Svc.Prefs.reset("addons.ignoreRepositoryChecking");
server.stop(run_next_test);
});

View file

@ -13,20 +13,19 @@ Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://testing-common/services/sync/utils.js");
var prefs = new Preferences();
let prefs = new Preferences();
prefs.set("extensions.getAddons.get.url",
"http://localhost:8888/search/guid:%IDS%");
prefs.set("extensions.install.requireSecureOrigin", false);
loadAddonTestFunctions();
startupManager();
var engineManager = Service.engineManager;
let engineManager = Service.engineManager;
engineManager.register(AddonsEngine);
var engine = engineManager.get("addons");
var reconciler = engine._reconciler;
var tracker = engine._tracker;
let engine = engineManager.get("addons");
let reconciler = engine._reconciler;
let tracker = engine._tracker;
function advance_test() {
reconciler._addons = {};
@ -36,6 +35,8 @@ function advance_test() {
reconciler.saveState(null, cb);
cb.wait();
Svc.Prefs.reset("addons.ignoreRepositoryChecking");
run_next_test();
}
@ -103,6 +104,7 @@ add_test(function test_get_changed_ids() {
tracker.clearChangedIDs();
_("Ensure reconciler changes are populated.");
Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
let addon = installAddon("test_bootstrap1_1");
tracker.clearChangedIDs(); // Just in case.
changes = engine.getChangedIDs();
@ -149,6 +151,9 @@ add_test(function test_disabled_install_semantics() {
// This is essentially a test for bug 712542, which snuck into the original
// add-on sync drop. It ensures that when an add-on is installed that the
// disabled state and incoming syncGUID is preserved, even on the next sync.
Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
const USER = "foo";
const PASSWORD = "password";
const PASSPHRASE = "abcdeabcdeabcdeabcdeabcdea";

View file

@ -71,7 +71,7 @@ add_test(function test_install_detection() {
const KEYS = ["id", "guid", "enabled", "installed", "modified", "type",
"scope", "foreignInstall"];
for (let key of KEYS) {
for each (let key in KEYS) {
do_check_true(key in record);
do_check_neq(null, record[key]);
}

View file

@ -3,47 +3,25 @@
"use strict";
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://services-sync/addonutils.js");
Cu.import("resource://services-sync/engines/addons.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://testing-common/services/sync/utils.js");
Cu.import("resource://gre/modules/FileUtils.jsm");
const HTTP_PORT = 8888;
var prefs = new Preferences();
let prefs = new Preferences();
prefs.set("extensions.getAddons.get.url", "http://localhost:8888/search/guid:%IDS%");
prefs.set("extensions.install.requireSecureOrigin", false);
const SYSTEM_ADDON_ID = "system1@tests.mozilla.org";
let systemAddonFile;
// The system add-on must be installed before AddonManager is started.
function loadSystemAddon() {
let addonFilename = SYSTEM_ADDON_ID + ".xpi";
const distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "app0"], true);
do_get_file(ExtensionsTestPath("/data/system_addons/system1_1.xpi")).copyTo(distroDir, addonFilename);
systemAddonFile = FileUtils.File(distroDir.path);
systemAddonFile.append(addonFilename);
systemAddonFile.lastModifiedTime = Date.now();
// As we're not running in application, we need to setup the features directory
// used by system add-ons.
registerDirectory("XREAppFeat", distroDir);
}
loadAddonTestFunctions();
loadSystemAddon();
startupManager();
Service.engineManager.register(AddonsEngine);
var engine = Service.engineManager.get("addons");
var tracker = engine._tracker;
var store = engine._store;
var reconciler = engine._reconciler;
let engine = Service.engineManager.get("addons");
let tracker = engine._tracker;
let store = engine._store;
let reconciler = engine._reconciler;
/**
* Create a AddonsRec for this application with the fields specified.
@ -77,16 +55,12 @@ function createAndStartHTTPServer(port) {
server.registerFile("/search/guid:missing-xpi%40tests.mozilla.org",
do_get_file("missing-xpi-search.xml"));
server.registerFile("/search/guid:system1%40tests.mozilla.org",
do_get_file("systemaddon-search.xml"));
server.registerFile("/system.xpi", systemAddonFile);
server.start(port);
return server;
} catch (ex) {
_("Got exception starting HTTP server on port " + port);
_("Error: " + Log.exceptionStr(ex));
_("Error: " + Utils.exceptionStr(ex));
do_throw(ex);
}
}
@ -94,7 +68,6 @@ function createAndStartHTTPServer(port) {
function run_test() {
initTestLogging("Trace");
Log.repository.getLogger("Sync.Engine.Addons").level = Log.Level.Trace;
Log.repository.getLogger("Sync.Tracker.Addons").level = Log.Level.Trace;
Log.repository.getLogger("Sync.AddonsRepository").level =
Log.Level.Trace;
@ -219,6 +192,7 @@ add_test(function test_apply_uninstall() {
add_test(function test_addon_syncability() {
_("Ensure isAddonSyncable functions properly.");
Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
Svc.Prefs.set("addons.trustedSourceHostnames",
"addons.mozilla.org,other.example.com");
@ -228,8 +202,8 @@ add_test(function test_addon_syncability() {
do_check_true(store.isAddonSyncable(addon));
let dummy = {};
const KEYS = ["id", "syncGUID", "type", "scope", "foreignInstall", "isSyncable"];
for (let k of KEYS) {
const KEYS = ["id", "syncGUID", "type", "scope", "foreignInstall"];
for each (let k in KEYS) {
dummy[k] = addon[k];
}
@ -243,10 +217,6 @@ add_test(function test_addon_syncability() {
do_check_false(store.isAddonSyncable(dummy));
dummy.scope = addon.scope;
dummy.isSyncable = false;
do_check_false(store.isAddonSyncable(dummy));
dummy.isSyncable = addon.isSyncable;
dummy.foreignInstall = true;
do_check_false(store.isAddonSyncable(dummy));
dummy.foreignInstall = false;
@ -272,16 +242,16 @@ add_test(function test_addon_syncability() {
"https://untrusted.example.com/foo", // non-trusted hostname`
];
for (let uri of trusted) {
for each (let uri in trusted) {
do_check_true(store.isSourceURITrusted(createURI(uri)));
}
for (let uri of untrusted) {
for each (let uri in untrusted) {
do_check_false(store.isSourceURITrusted(createURI(uri)));
}
Svc.Prefs.set("addons.trustedSourceHostnames", "");
for (let uri of trusted) {
for each (let uri in trusted) {
do_check_false(store.isSourceURITrusted(createURI(uri)));
}
@ -296,6 +266,8 @@ add_test(function test_addon_syncability() {
add_test(function test_ignore_hotfixes() {
_("Ensure that hotfix extensions are ignored.");
Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
// A hotfix extension is one that has the id the same as the
// extensions.hotfix.id pref.
let prefs = new Preferences("extensions.");
@ -304,8 +276,8 @@ add_test(function test_ignore_hotfixes() {
do_check_true(store.isAddonSyncable(addon));
let dummy = {};
const KEYS = ["id", "syncGUID", "type", "scope", "foreignInstall", "isSyncable"];
for (let k of KEYS) {
const KEYS = ["id", "syncGUID", "type", "scope", "foreignInstall"];
for each (let k in KEYS) {
dummy[k] = addon[k];
}
@ -327,6 +299,7 @@ add_test(function test_ignore_hotfixes() {
uninstallAddon(addon);
Svc.Prefs.reset("addons.ignoreRepositoryChecking");
prefs.reset("hotfix.id");
run_next_test();
@ -336,6 +309,8 @@ add_test(function test_ignore_hotfixes() {
add_test(function test_get_all_ids() {
_("Ensures that getAllIDs() returns an appropriate set.");
Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
_("Installing two addons.");
let addon1 = installAddon("test_install1");
let addon2 = installAddon("test_bootstrap1_1");
@ -354,6 +329,7 @@ add_test(function test_get_all_ids() {
addon1.install.cancel();
uninstallAddon(addon2);
Svc.Prefs.reset("addons.ignoreRepositoryChecking");
run_next_test();
});
@ -379,6 +355,9 @@ add_test(function test_change_item_id() {
add_test(function test_create() {
_("Ensure creating/installing an add-on from a record works.");
// Set this so that getInstallFromSearchResult doesn't end up
// failing the install due to an insecure source URI scheme.
Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
let server = createAndStartHTTPServer(HTTP_PORT);
let addon = installAddon("test_bootstrap1_1");
@ -398,6 +377,7 @@ add_test(function test_create() {
uninstallAddon(newAddon);
Svc.Prefs.reset("addons.ignoreRepositoryChecking");
server.stop(run_next_test);
});
@ -432,18 +412,8 @@ add_test(function test_create_bad_install() {
let record = createRecordForThisApp(guid, id, true, false);
let failed = store.applyIncomingBatch([record]);
// This addon had no source URI so was skipped - but it's not treated as
// failure.
// XXX - this test isn't testing what we thought it was. Previously the addon
// was not being installed due to requireSecureURL checking *before* we'd
// attempted to get the XPI.
// With requireSecureURL disabled we do see a download failure, but the addon
// *does* get added to |failed|.
// FTR: onDownloadFailed() is called with ERROR_NETWORK_FAILURE, so it's going
// to be tricky to distinguish a 404 from other transient network errors
// where we do want the addon to end up in |failed|.
// This is being tracked in bug 1284778.
//do_check_eq(0, failed.length);
do_check_eq(1, failed.length);
do_check_eq(guid, failed[0]);
let addon = getAddonFromAddonManagerByID(id);
do_check_eq(null, addon);
@ -451,56 +421,19 @@ add_test(function test_create_bad_install() {
server.stop(run_next_test);
});
add_test(function test_ignore_system() {
_("Ensure we ignore system addons");
// Our system addon should not appear in getAllIDs
engine._refreshReconcilerState();
let num = 0;
for (let guid in store.getAllIDs()) {
num += 1;
let addon = reconciler.getAddonStateFromSyncGUID(guid);
do_check_neq(addon.id, SYSTEM_ADDON_ID);
}
do_check_true(num > 1, "should have seen at least one.")
run_next_test();
});
add_test(function test_incoming_system() {
_("Ensure we handle incoming records that refer to a system addon");
// eg, loop initially had a normal addon but it was then "promoted" to be a
// system addon but wanted to keep the same ID. The server record exists due
// to this.
// before we start, ensure the system addon isn't disabled.
do_check_false(getAddonFromAddonManagerByID(SYSTEM_ADDON_ID).userDisabled);
// Now simulate an incoming record with the same ID as the system addon,
// but flagged as disabled - it should not be applied.
let server = createAndStartHTTPServer(HTTP_PORT);
// We make the incoming record flag the system addon as disabled - it should
// be ignored.
let guid = Utils.makeGUID();
let record = createRecordForThisApp(guid, SYSTEM_ADDON_ID, false, false);
let failed = store.applyIncomingBatch([record]);
do_check_eq(0, failed.length);
// The system addon should still not be userDisabled.
do_check_false(getAddonFromAddonManagerByID(SYSTEM_ADDON_ID).userDisabled);
server.stop(run_next_test);
});
add_test(function test_wipe() {
_("Ensures that wiping causes add-ons to be uninstalled.");
let addon1 = installAddon("test_bootstrap1_1");
Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
store.wipe();
let addon = getAddonFromAddonManagerByID(addon1.id);
do_check_eq(null, addon);
Svc.Prefs.reset("addons.ignoreRepositoryChecking");
run_next_test();
});
@ -515,6 +448,7 @@ add_test(function test_wipe_and_install() {
let record = createRecordForThisApp(installed.syncGUID, installed.id, true,
false);
Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
store.wipe();
let deleted = getAddonFromAddonManagerByID(installed.id);
@ -528,6 +462,7 @@ add_test(function test_wipe_and_install() {
let fetched = getAddonFromAddonManagerByID(record.addonID);
do_check_true(!!fetched);
Svc.Prefs.reset("addons.ignoreRepositoryChecking");
server.stop(run_next_test);
});

View file

@ -11,13 +11,14 @@ Cu.import("resource://services-sync/util.js");
loadAddonTestFunctions();
startupManager();
Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
Svc.Prefs.set("engine.addons", true);
Service.engineManager.register(AddonsEngine);
var engine = Service.engineManager.get("addons");
var reconciler = engine._reconciler;
var store = engine._store;
var tracker = engine._tracker;
let engine = Service.engineManager.get("addons");
let reconciler = engine._reconciler;
let store = engine._store;
let tracker = engine._tracker;
// Don't write out by default.
tracker.persistChangedIDs = false;

View file

@ -0,0 +1,37 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
Cu.import("resource://services-sync/main.js");
Cu.import("resource://services-sync/util.js");
// Simple test for block/unblock.
add_task(function *() {
Assert.ok(!Weave.Service.scheduler.isBlocked, "sync is not blocked.")
Assert.ok(!Svc.Prefs.has("scheduler.blocked-until"), "have no blocked pref");
Weave.Service.scheduler.blockSync();
Assert.ok(Weave.Service.scheduler.isBlocked, "sync is blocked.")
Assert.ok(Svc.Prefs.has("scheduler.blocked-until"), "have the blocked pref");
Weave.Service.scheduler.unblockSync();
Assert.ok(!Weave.Service.scheduler.isBlocked, "sync is not blocked.")
Assert.ok(!Svc.Prefs.has("scheduler.blocked-until"), "have no blocked pref");
// now check the "until" functionality.
let until = Date.now() + 1000;
Weave.Service.scheduler.blockSync(until);
Assert.ok(Weave.Service.scheduler.isBlocked, "sync is blocked.")
Assert.ok(Svc.Prefs.has("scheduler.blocked-until"), "have the blocked pref");
// wait for 'until' to pass.
yield new Promise((resolve, reject) => {
CommonUtils.namedTimer(resolve, 1000, {}, "timer");
});
// should have automagically unblocked and removed the pref.
Assert.ok(!Weave.Service.scheduler.isBlocked, "sync is not blocked.")
Assert.ok(!Svc.Prefs.has("scheduler.blocked-until"), "have no blocked pref");
});
function run_test() {
run_next_test();
}

View file

@ -1,644 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
Cu.import("resource://gre/modules/PlacesUtils.jsm");
Cu.import("resource://services-common/async.js");
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/engines/bookmarks.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://testing-common/services/sync/utils.js");
Cu.import("resource://services-sync/bookmark_validator.js");
initTestLogging("Trace");
const bms = PlacesUtils.bookmarks;
Service.engineManager.register(BookmarksEngine);
const engine = new BookmarksEngine(Service);
const store = engine._store;
store._log.level = Log.Level.Trace;
engine._log.level = Log.Level.Trace;
function promiseOneObserver(topic) {
return new Promise((resolve, reject) => {
let observer = function(subject, topic, data) {
Services.obs.removeObserver(observer, topic);
resolve({ subject: subject, data: data });
}
Services.obs.addObserver(observer, topic, false);
});
}
function setup() {
let server = serverForUsers({"foo": "password"}, {
meta: {global: {engines: {bookmarks: {version: engine.version,
syncID: engine.syncID}}}},
bookmarks: {},
});
generateNewKeys(Service.collectionKeys);
new SyncTestingInfrastructure(server.server);
let collection = server.user("foo").collection("bookmarks");
Svc.Obs.notify("weave:engine:start-tracking"); // We skip usual startup...
return { server, collection };
}
function* cleanup(server) {
Svc.Obs.notify("weave:engine:stop-tracking");
Services.prefs.setBoolPref("services.sync-testing.startOverKeepIdentity", true);
let promiseStartOver = promiseOneObserver("weave:service:start-over:finish");
Service.startOver();
yield promiseStartOver;
yield new Promise(resolve => server.stop(resolve));
yield bms.eraseEverything();
}
function getFolderChildrenIDs(folderId) {
let index = 0;
let result = [];
while (true) {
let childId = bms.getIdForItemAt(folderId, index);
if (childId == -1) {
break;
}
result.push(childId);
index++;
}
return result;
}
function createFolder(parentId, title) {
let id = bms.createFolder(parentId, title, 0);
let guid = store.GUIDForId(id);
return { id, guid };
}
function createBookmark(parentId, url, title, index = bms.DEFAULT_INDEX) {
let uri = Utils.makeURI(url);
let id = bms.insertBookmark(parentId, uri, index, title)
let guid = store.GUIDForId(id);
return { id, guid };
}
function getServerRecord(collection, id) {
let wbo = collection.get({ full: true, ids: [id] });
// Whew - lots of json strings inside strings.
return JSON.parse(JSON.parse(JSON.parse(wbo).payload).ciphertext);
}
function* promiseNoLocalItem(guid) {
// Check there's no item with the specified guid.
let got = yield bms.fetch({ guid });
ok(!got, `No record remains with GUID ${guid}`);
// and while we are here ensure the places cache doesn't still have it.
yield Assert.rejects(PlacesUtils.promiseItemId(guid));
}
function* validate(collection, expectedFailures = []) {
let validator = new BookmarkValidator();
let records = collection.payloads();
let problems = validator.inspectServerRecords(records).problemData;
// all non-zero problems.
let summary = problems.getSummary().filter(prob => prob.count != 0);
// split into 2 arrays - expected and unexpected.
let isInExpectedFailures = elt => {
for (let i = 0; i < expectedFailures.length; i++) {
if (elt.name == expectedFailures[i].name && elt.count == expectedFailures[i].count) {
return true;
}
}
return false;
}
let expected = [];
let unexpected = [];
for (let elt of summary) {
(isInExpectedFailures(elt) ? expected : unexpected).push(elt);
}
if (unexpected.length || expected.length != expectedFailures.length) {
do_print("Validation failed:");
do_print(JSON.stringify(summary));
// print the entire validator output as it has IDs etc.
do_print(JSON.stringify(problems, undefined, 2));
// All server records and the entire bookmark tree.
do_print("Server records:\n" + JSON.stringify(collection.payloads(), undefined, 2));
let tree = yield PlacesUtils.promiseBookmarksTree("", { includeItemIds: true });
do_print("Local bookmark tree:\n" + JSON.stringify(tree, undefined, 2));
ok(false);
}
}
add_task(function* test_dupe_bookmark() {
_("Ensure that a bookmark we consider a dupe is handled correctly.");
let { server, collection } = this.setup();
try {
// The parent folder and one bookmark in it.
let {id: folder1_id, guid: folder1_guid } = createFolder(bms.toolbarFolder, "Folder 1");
let {id: bmk1_id, guid: bmk1_guid} = createBookmark(folder1_id, "http://getfirefox.com/", "Get Firefox!");
engine.sync();
// We've added the bookmark, its parent (folder1) plus "menu", "toolbar", "unfiled", and "mobile".
equal(collection.count(), 6);
equal(getFolderChildrenIDs(folder1_id).length, 1);
// Now create a new incoming record that looks alot like a dupe.
let newGUID = Utils.makeGUID();
let to_apply = {
id: newGUID,
bmkUri: "http://getfirefox.com/",
type: "bookmark",
title: "Get Firefox!",
parentName: "Folder 1",
parentid: folder1_guid,
};
collection.insert(newGUID, encryptPayload(to_apply), Date.now() / 1000 + 10);
_("Syncing so new dupe record is processed");
engine.lastSync = engine.lastSync - 0.01;
engine.sync();
// We should have logically deleted the dupe record.
equal(collection.count(), 7);
ok(getServerRecord(collection, bmk1_guid).deleted);
// and physically removed from the local store.
yield promiseNoLocalItem(bmk1_guid);
// Parent should still only have 1 item.
equal(getFolderChildrenIDs(folder1_id).length, 1);
// The parent record on the server should now reference the new GUID and not the old.
let serverRecord = getServerRecord(collection, folder1_guid);
ok(!serverRecord.children.includes(bmk1_guid));
ok(serverRecord.children.includes(newGUID));
// and a final sanity check - use the validator
yield validate(collection);
} finally {
yield cleanup(server);
}
});
add_task(function* test_dupe_reparented_bookmark() {
_("Ensure that a bookmark we consider a dupe from a different parent is handled correctly");
let { server, collection } = this.setup();
try {
// The parent folder and one bookmark in it.
let {id: folder1_id, guid: folder1_guid } = createFolder(bms.toolbarFolder, "Folder 1");
let {id: bmk1_id, guid: bmk1_guid} = createBookmark(folder1_id, "http://getfirefox.com/", "Get Firefox!");
// Another parent folder *with the same name*
let {id: folder2_id, guid: folder2_guid } = createFolder(bms.toolbarFolder, "Folder 1");
do_print(`folder1_guid=${folder1_guid}, folder2_guid=${folder2_guid}, bmk1_guid=${bmk1_guid}`);
engine.sync();
// We've added the bookmark, 2 folders plus "menu", "toolbar", "unfiled", and "mobile".
equal(collection.count(), 7);
equal(getFolderChildrenIDs(folder1_id).length, 1);
equal(getFolderChildrenIDs(folder2_id).length, 0);
// Now create a new incoming record that looks alot like a dupe of the
// item in folder1_guid, but with a record that points to folder2_guid.
let newGUID = Utils.makeGUID();
let to_apply = {
id: newGUID,
bmkUri: "http://getfirefox.com/",
type: "bookmark",
title: "Get Firefox!",
parentName: "Folder 1",
parentid: folder2_guid,
};
collection.insert(newGUID, encryptPayload(to_apply), Date.now() / 1000 + 10);
_("Syncing so new dupe record is processed");
engine.lastSync = engine.lastSync - 0.01;
engine.sync();
// We should have logically deleted the dupe record.
equal(collection.count(), 8);
ok(getServerRecord(collection, bmk1_guid).deleted);
// and physically removed from the local store.
yield promiseNoLocalItem(bmk1_guid);
// The original folder no longer has the item
equal(getFolderChildrenIDs(folder1_id).length, 0);
// But the second dupe folder does.
equal(getFolderChildrenIDs(folder2_id).length, 1);
// The record for folder1 on the server should reference neither old or new GUIDs.
let serverRecord1 = getServerRecord(collection, folder1_guid);
ok(!serverRecord1.children.includes(bmk1_guid));
ok(!serverRecord1.children.includes(newGUID));
// The record for folder2 on the server should only reference the new new GUID.
let serverRecord2 = getServerRecord(collection, folder2_guid);
ok(!serverRecord2.children.includes(bmk1_guid));
ok(serverRecord2.children.includes(newGUID));
// and a final sanity check - use the validator
yield validate(collection);
} finally {
yield cleanup(server);
}
});
add_task(function* test_dupe_reparented_locally_changed_bookmark() {
_("Ensure that a bookmark with local changes we consider a dupe from a different parent is handled correctly");
let { server, collection } = this.setup();
try {
// The parent folder and one bookmark in it.
let {id: folder1_id, guid: folder1_guid } = createFolder(bms.toolbarFolder, "Folder 1");
let {id: bmk1_id, guid: bmk1_guid} = createBookmark(folder1_id, "http://getfirefox.com/", "Get Firefox!");
// Another parent folder *with the same name*
let {id: folder2_id, guid: folder2_guid } = createFolder(bms.toolbarFolder, "Folder 1");
do_print(`folder1_guid=${folder1_guid}, folder2_guid=${folder2_guid}, bmk1_guid=${bmk1_guid}`);
engine.sync();
// We've added the bookmark, 2 folders plus "menu", "toolbar", "unfiled", and "mobile".
equal(collection.count(), 7);
equal(getFolderChildrenIDs(folder1_id).length, 1);
equal(getFolderChildrenIDs(folder2_id).length, 0);
// Now create a new incoming record that looks alot like a dupe of the
// item in folder1_guid, but with a record that points to folder2_guid.
let newGUID = Utils.makeGUID();
let to_apply = {
id: newGUID,
bmkUri: "http://getfirefox.com/",
type: "bookmark",
title: "Get Firefox!",
parentName: "Folder 1",
parentid: folder2_guid,
};
collection.insert(newGUID, encryptPayload(to_apply), Date.now() / 1000 + 10);
// Make a change to the bookmark that's a dupe, and set the modification
// time further in the future than the incoming record. This will cause
// us to issue the infamous "DATA LOSS" warning in the logs but cause us
// to *not* apply the incoming record.
engine._tracker.addChangedID(bmk1_guid, Date.now() / 1000 + 60);
_("Syncing so new dupe record is processed");
engine.lastSync = engine.lastSync - 0.01;
engine.sync();
// We should have logically deleted the dupe record.
equal(collection.count(), 8);
ok(getServerRecord(collection, bmk1_guid).deleted);
// and physically removed from the local store.
yield promiseNoLocalItem(bmk1_guid);
// The original folder still longer has the item
equal(getFolderChildrenIDs(folder1_id).length, 1);
// The second folder does not.
equal(getFolderChildrenIDs(folder2_id).length, 0);
// The record for folder1 on the server should reference only the GUID.
let serverRecord1 = getServerRecord(collection, folder1_guid);
ok(!serverRecord1.children.includes(bmk1_guid));
ok(serverRecord1.children.includes(newGUID));
// The record for folder2 on the server should reference nothing.
let serverRecord2 = getServerRecord(collection, folder2_guid);
ok(!serverRecord2.children.includes(bmk1_guid));
ok(!serverRecord2.children.includes(newGUID));
// and a final sanity check - use the validator
yield validate(collection);
} finally {
yield cleanup(server);
}
});
add_task(function* test_dupe_reparented_to_earlier_appearing_parent_bookmark() {
_("Ensure that a bookmark we consider a dupe from a different parent that " +
"appears in the same sync before the dupe item");
let { server, collection } = this.setup();
try {
// The parent folder and one bookmark in it.
let {id: folder1_id, guid: folder1_guid } = createFolder(bms.toolbarFolder, "Folder 1");
let {id: bmk1_id, guid: bmk1_guid} = createBookmark(folder1_id, "http://getfirefox.com/", "Get Firefox!");
// One more folder we'll use later.
let {id: folder2_id, guid: folder2_guid} = createFolder(bms.toolbarFolder, "A second folder");
do_print(`folder1=${folder1_guid}, bmk1=${bmk1_guid} folder2=${folder2_guid}`);
engine.sync();
// We've added the bookmark, 2 folders plus "menu", "toolbar", "unfiled", and "mobile".
equal(collection.count(), 7);
equal(getFolderChildrenIDs(folder1_id).length, 1);
let newGUID = Utils.makeGUID();
let newParentGUID = Utils.makeGUID();
// Have the new parent appear before the dupe item.
collection.insert(newParentGUID, encryptPayload({
id: newParentGUID,
type: "folder",
title: "Folder 1",
parentName: "A second folder",
parentid: folder2_guid,
children: [newGUID],
tags: [],
}), Date.now() / 1000 + 10);
// And also the update to "folder 2" that references the new parent.
collection.insert(folder2_guid, encryptPayload({
id: folder2_guid,
type: "folder",
title: "A second folder",
parentName: "Bookmarks Toolbar",
parentid: "toolbar",
children: [newParentGUID],
tags: [],
}), Date.now() / 1000 + 10);
// Now create a new incoming record that looks alot like a dupe of the
// item in folder1_guid, with a record that points to a parent with the
// same name which appeared earlier in this sync.
collection.insert(newGUID, encryptPayload({
id: newGUID,
bmkUri: "http://getfirefox.com/",
type: "bookmark",
title: "Get Firefox!",
parentName: "Folder 1",
parentid: newParentGUID,
tags: [],
}), Date.now() / 1000 + 10);
_("Syncing so new records are processed.");
engine.lastSync = engine.lastSync - 0.01;
engine.sync();
// Everything should be parented correctly.
equal(getFolderChildrenIDs(folder1_id).length, 0);
let newParentID = store.idForGUID(newParentGUID);
let newID = store.idForGUID(newGUID);
deepEqual(getFolderChildrenIDs(newParentID), [newID]);
// Make sure the validator thinks everything is hunky-dory.
yield validate(collection);
} finally {
yield cleanup(server);
}
});
add_task(function* test_dupe_reparented_to_later_appearing_parent_bookmark() {
_("Ensure that a bookmark we consider a dupe from a different parent that " +
"doesn't exist locally as we process the child, but does appear in the same sync");
let { server, collection } = this.setup();
try {
// The parent folder and one bookmark in it.
let {id: folder1_id, guid: folder1_guid } = createFolder(bms.toolbarFolder, "Folder 1");
let {id: bmk1_id, guid: bmk1_guid} = createBookmark(folder1_id, "http://getfirefox.com/", "Get Firefox!");
// One more folder we'll use later.
let {id: folder2_id, guid: folder2_guid} = createFolder(bms.toolbarFolder, "A second folder");
do_print(`folder1=${folder1_guid}, bmk1=${bmk1_guid} folder2=${folder2_guid}`);
engine.sync();
// We've added the bookmark, 2 folders plus "menu", "toolbar", "unfiled", and "mobile".
equal(collection.count(), 7);
equal(getFolderChildrenIDs(folder1_id).length, 1);
// Now create a new incoming record that looks alot like a dupe of the
// item in folder1_guid, but with a record that points to a parent with the
// same name, but a non-existing local ID.
let newGUID = Utils.makeGUID();
let newParentGUID = Utils.makeGUID();
collection.insert(newGUID, encryptPayload({
id: newGUID,
bmkUri: "http://getfirefox.com/",
type: "bookmark",
title: "Get Firefox!",
parentName: "Folder 1",
parentid: newParentGUID,
tags: [],
}), Date.now() / 1000 + 10);
// Now have the parent appear after (so when the record above is processed
// this is still unknown.)
collection.insert(newParentGUID, encryptPayload({
id: newParentGUID,
type: "folder",
title: "Folder 1",
parentName: "A second folder",
parentid: folder2_guid,
children: [newGUID],
tags: [],
}), Date.now() / 1000 + 10);
// And also the update to "folder 2" that references the new parent.
collection.insert(folder2_guid, encryptPayload({
id: folder2_guid,
type: "folder",
title: "A second folder",
parentName: "Bookmarks Toolbar",
parentid: "toolbar",
children: [newParentGUID],
tags: [],
}), Date.now() / 1000 + 10);
_("Syncing so out-of-order records are processed.");
engine.lastSync = engine.lastSync - 0.01;
engine.sync();
// The intended parent did end up existing, so it should be parented
// correctly after de-duplication.
equal(getFolderChildrenIDs(folder1_id).length, 0);
let newParentID = store.idForGUID(newParentGUID);
let newID = store.idForGUID(newGUID);
deepEqual(getFolderChildrenIDs(newParentID), [newID]);
// Make sure the validator thinks everything is hunky-dory.
yield validate(collection);
} finally {
yield cleanup(server);
}
});
add_task(function* test_dupe_reparented_to_future_arriving_parent_bookmark() {
_("Ensure that a bookmark we consider a dupe from a different parent that " +
"doesn't exist locally and doesn't appear in this Sync is handled correctly");
let { server, collection } = this.setup();
try {
// The parent folder and one bookmark in it.
let {id: folder1_id, guid: folder1_guid } = createFolder(bms.toolbarFolder, "Folder 1");
let {id: bmk1_id, guid: bmk1_guid} = createBookmark(folder1_id, "http://getfirefox.com/", "Get Firefox!");
// One more folder we'll use later.
let {id: folder2_id, guid: folder2_guid} = createFolder(bms.toolbarFolder, "A second folder");
do_print(`folder1=${folder1_guid}, bmk1=${bmk1_guid} folder2=${folder2_guid}`);
engine.sync();
// We've added the bookmark, 2 folders plus "menu", "toolbar", "unfiled", and "mobile".
equal(collection.count(), 7);
equal(getFolderChildrenIDs(folder1_id).length, 1);
// Now create a new incoming record that looks alot like a dupe of the
// item in folder1_guid, but with a record that points to a parent with the
// same name, but a non-existing local ID.
let newGUID = Utils.makeGUID();
let newParentGUID = Utils.makeGUID();
collection.insert(newGUID, encryptPayload({
id: newGUID,
bmkUri: "http://getfirefox.com/",
type: "bookmark",
title: "Get Firefox!",
parentName: "Folder 1",
parentid: newParentGUID,
tags: [],
}), Date.now() / 1000 + 10);
_("Syncing so new dupe record is processed");
engine.lastSync = engine.lastSync - 0.01;
engine.sync();
// We should have logically deleted the dupe record.
equal(collection.count(), 8);
ok(getServerRecord(collection, bmk1_guid).deleted);
// and physically removed from the local store.
yield promiseNoLocalItem(bmk1_guid);
// The intended parent doesn't exist, so it remains in the original folder
equal(getFolderChildrenIDs(folder1_id).length, 1);
// The record for folder1 on the server should reference the new GUID.
let serverRecord1 = getServerRecord(collection, folder1_guid);
ok(!serverRecord1.children.includes(bmk1_guid));
ok(serverRecord1.children.includes(newGUID));
// As the incoming parent is missing the item should have been annotated
// with that missing parent.
equal(PlacesUtils.annotations.getItemAnnotation(store.idForGUID(newGUID), "sync/parent"),
newParentGUID);
// Check the validator. Sadly, this is known to cause a mismatch between
// the server and client views of the tree.
let expected = [
// We haven't fixed the incoming record that referenced the missing parent.
{ name: "orphans", count: 1 },
];
yield validate(collection, expected);
// Now have the parent magically appear in a later sync - but
// it appears as being in a different parent from our existing "Folder 1",
// so the folder itself isn't duped.
collection.insert(newParentGUID, encryptPayload({
id: newParentGUID,
type: "folder",
title: "Folder 1",
parentName: "A second folder",
parentid: folder2_guid,
children: [newGUID],
tags: [],
}), Date.now() / 1000 + 10);
// We also queue an update to "folder 2" that references the new parent.
collection.insert(folder2_guid, encryptPayload({
id: folder2_guid,
type: "folder",
title: "A second folder",
parentName: "Bookmarks Toolbar",
parentid: "toolbar",
children: [newParentGUID],
tags: [],
}), Date.now() / 1000 + 10);
_("Syncing so missing parent appears");
engine.lastSync = engine.lastSync - 0.01;
engine.sync();
// The intended parent now does exist, so it should have been reparented.
equal(getFolderChildrenIDs(folder1_id).length, 0);
let newParentID = store.idForGUID(newParentGUID);
let newID = store.idForGUID(newGUID);
deepEqual(getFolderChildrenIDs(newParentID), [newID]);
// validation now has different errors :(
expected = [
// The validator reports multipleParents because:
// * The incoming record newParentGUID still (and correctly) references
// newGUID as a child.
// * Our original Folder1 was updated to include newGUID when it
// originally de-deuped and couldn't find the parent.
// * When the parent *did* eventually arrive we used the parent annotation
// to correctly reparent - but that reparenting process does not change
// the server record.
// Hence, newGUID is a child of both those server records :(
{ name: "multipleParents", count: 1 },
];
yield validate(collection, expected);
} finally {
yield cleanup(server);
}
});
add_task(function* test_dupe_empty_folder() {
_("Ensure that an empty folder we consider a dupe is handled correctly.");
// Empty folders aren't particularly interesting in practice (as that seems
// an edge-case) but duping folders with items is broken - bug 1293163.
let { server, collection } = this.setup();
try {
// The folder we will end up duping away.
let {id: folder1_id, guid: folder1_guid } = createFolder(bms.toolbarFolder, "Folder 1");
engine.sync();
// We've added 1 folder, "menu", "toolbar", "unfiled", and "mobile".
equal(collection.count(), 5);
// Now create new incoming records that looks alot like a dupe of "Folder 1".
let newFolderGUID = Utils.makeGUID();
collection.insert(newFolderGUID, encryptPayload({
id: newFolderGUID,
type: "folder",
title: "Folder 1",
parentName: "Bookmarks Toolbar",
parentid: "toolbar",
children: [],
}), Date.now() / 1000 + 10);
_("Syncing so new dupe records are processed");
engine.lastSync = engine.lastSync - 0.01;
engine.sync();
yield validate(collection);
// Collection now has one additional record - the logically deleted dupe.
equal(collection.count(), 6);
// original folder should be logically deleted.
ok(getServerRecord(collection, folder1_guid).deleted);
yield promiseNoLocalItem(folder1_guid);
} finally {
yield cleanup(server);
}
});
// XXX - TODO - folders with children. Bug 1293163

View file

@ -2,10 +2,9 @@
http://creativecommons.org/publicdomain/zero/1.0/ */
Cu.import("resource://gre/modules/PlacesUtils.jsm");
Cu.import("resource://gre/modules/PlacesSyncUtils.jsm");
Cu.import("resource://gre/modules/BookmarkJSONUtils.jsm");
Cu.import("resource://services-common/async.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/engines/bookmarks.js");
Cu.import("resource://services-sync/service.js");
@ -13,168 +12,9 @@ Cu.import("resource://services-sync/util.js");
Cu.import("resource://testing-common/services/sync/utils.js");
Cu.import("resource://gre/modules/Promise.jsm");
initTestLogging("Trace");
Service.engineManager.register(BookmarksEngine);
function* assertChildGuids(folderGuid, expectedChildGuids, message) {
let tree = yield PlacesUtils.promiseBookmarksTree(folderGuid);
let childGuids = tree.children.map(child => child.guid);
deepEqual(childGuids, expectedChildGuids, message);
}
add_task(function* test_change_during_sync() {
_("Ensure that we track changes made during a sync.");
let engine = new BookmarksEngine(Service);
let store = engine._store;
let tracker = engine._tracker;
let server = serverForFoo(engine);
new SyncTestingInfrastructure(server.server);
let collection = server.user("foo").collection("bookmarks");
let bz_id = PlacesUtils.bookmarks.insertBookmark(
PlacesUtils.bookmarksMenuFolderId, Utils.makeURI("https://bugzilla.mozilla.org/"),
PlacesUtils.bookmarks.DEFAULT_INDEX, "Bugzilla");
let bz_guid = yield PlacesUtils.promiseItemGuid(bz_id);
_(`Bugzilla GUID: ${bz_guid}`);
Svc.Obs.notify("weave:engine:start-tracking");
try {
let folder1_id = PlacesUtils.bookmarks.createFolder(
PlacesUtils.bookmarks.toolbarFolder, "Folder 1", 0);
let folder1_guid = store.GUIDForId(folder1_id);
_(`Folder GUID: ${folder1_guid}`);
let bmk1_id = PlacesUtils.bookmarks.insertBookmark(
folder1_id, Utils.makeURI("http://getthunderbird.com/"),
PlacesUtils.bookmarks.DEFAULT_INDEX, "Get Thunderbird!");
let bmk1_guid = store.GUIDForId(bmk1_id);
_(`Thunderbird GUID: ${bmk1_guid}`);
// Sync is synchronous, so, to simulate a bookmark change made during a
// sync, we create a server record that adds a bookmark as a side effect.
let bmk2_guid = "get-firefox1"; // New child of Folder 1, created remotely.
let bmk3_id = -1; // New child of Folder 1, created locally during sync.
let folder2_guid = "folder2-1111"; // New folder, created remotely.
let tagQuery_guid = "tag-query111"; // New tag query child of Folder 2, created remotely.
let bmk4_guid = "example-org1"; // New tagged child of Folder 2, created remotely.
{
// An existing record changed on the server that should not trigger
// another sync when applied.
let bzBmk = new Bookmark("bookmarks", bz_guid);
bzBmk.bmkUri = "https://bugzilla.mozilla.org/";
bzBmk.description = "New description";
bzBmk.title = "Bugzilla";
bzBmk.tags = ["new", "tags"];
bzBmk.parentName = "Bookmarks Toolbar";
bzBmk.parentid = "toolbar";
collection.insert(bz_guid, encryptPayload(bzBmk.cleartext));
let remoteFolder = new BookmarkFolder("bookmarks", folder2_guid);
remoteFolder.title = "Folder 2";
remoteFolder.children = [bmk4_guid, tagQuery_guid];
remoteFolder.parentName = "Bookmarks Menu";
remoteFolder.parentid = "menu";
collection.insert(folder2_guid, encryptPayload(remoteFolder.cleartext));
let localFxBmk = new Bookmark("bookmarks", bmk2_guid);
localFxBmk.bmkUri = "http://getfirefox.com/";
localFxBmk.description = "Firefox is awesome.";
localFxBmk.title = "Get Firefox!";
localFxBmk.tags = ["firefox", "awesome", "browser"];
localFxBmk.keyword = "awesome";
localFxBmk.loadInSidebar = false;
localFxBmk.parentName = "Folder 1";
localFxBmk.parentid = folder1_guid;
let remoteFxBmk = collection.insert(bmk2_guid, encryptPayload(localFxBmk.cleartext));
remoteFxBmk.get = function get() {
_("Inserting bookmark into local store");
bmk3_id = PlacesUtils.bookmarks.insertBookmark(
folder1_id, Utils.makeURI("https://mozilla.org/"),
PlacesUtils.bookmarks.DEFAULT_INDEX, "Mozilla");
return ServerWBO.prototype.get.apply(this, arguments);
};
// A tag query referencing a nonexistent tag folder, which we should
// create locally when applying the record.
let localTagQuery = new BookmarkQuery("bookmarks", tagQuery_guid);
localTagQuery.bmkUri = "place:type=7&folder=999";
localTagQuery.title = "Taggy tags";
localTagQuery.folderName = "taggy";
localTagQuery.parentName = "Folder 2";
localTagQuery.parentid = folder2_guid;
collection.insert(tagQuery_guid, encryptPayload(localTagQuery.cleartext));
// A bookmark that should appear in the results for the tag query.
let localTaggedBmk = new Bookmark("bookmarks", bmk4_guid);
localTaggedBmk.bmkUri = "https://example.org";
localTaggedBmk.title = "Tagged bookmark";
localTaggedBmk.tags = ["taggy"];
localTaggedBmk.parentName = "Folder 2";
localTaggedBmk.parentid = folder2_guid;
collection.insert(bmk4_guid, encryptPayload(localTaggedBmk.cleartext));
}
yield* assertChildGuids(folder1_guid, [bmk1_guid], "Folder should have 1 child before first sync");
_("Perform first sync");
{
let changes = engine.pullNewChanges();
deepEqual(changes.ids().sort(), [folder1_guid, bmk1_guid, "toolbar"].sort(),
"Should track bookmark and folder created before first sync");
yield sync_engine_and_validate_telem(engine, false);
}
let bmk2_id = store.idForGUID(bmk2_guid);
let bmk3_guid = store.GUIDForId(bmk3_id);
_(`Mozilla GUID: ${bmk3_guid}`);
{
equal(store.GUIDForId(bmk2_id), bmk2_guid,
"Remote bookmark should be applied during first sync");
ok(bmk3_id > -1,
"Bookmark created during first sync should exist locally");
ok(!collection.wbo(bmk3_guid),
"Bookmark created during first sync shouldn't be uploaded yet");
yield* assertChildGuids(folder1_guid, [bmk1_guid, bmk3_guid, bmk2_guid],
"Folder 1 should have 3 children after first sync");
yield* assertChildGuids(folder2_guid, [bmk4_guid, tagQuery_guid],
"Folder 2 should have 2 children after first sync");
let taggedURIs = PlacesUtils.tagging.getURIsForTag("taggy");
equal(taggedURIs.length, 1, "Should have 1 tagged URI");
equal(taggedURIs[0].spec, "https://example.org/",
"Synced tagged bookmark should appear in tagged URI list");
}
_("Perform second sync");
{
let changes = engine.pullNewChanges();
deepEqual(changes.ids().sort(), [bmk3_guid, folder1_guid].sort(),
"Should track bookmark added during last sync and its parent");
yield sync_engine_and_validate_telem(engine, false);
ok(collection.wbo(bmk3_guid),
"Bookmark created during first sync should be uploaded during second sync");
yield* assertChildGuids(folder1_guid, [bmk1_guid, bmk3_guid, bmk2_guid],
"Folder 1 should have same children after second sync");
yield* assertChildGuids(folder2_guid, [bmk4_guid, tagQuery_guid],
"Folder 2 should have same children after second sync");
}
} finally {
store.wipe();
Svc.Prefs.resetBranch("");
Service.recordManager.clearCache();
yield new Promise(resolve => server.stop(resolve));
Svc.Obs.notify("weave:engine:stop-tracking");
}
});
add_task(function* bad_record_allIDs() {
add_test(function bad_record_allIDs() {
let server = new SyncServer();
server.start();
let syncTesting = new SyncTestingInfrastructure(server.server);
@ -192,6 +32,9 @@ add_task(function* bad_record_allIDs() {
_("Record is " + badRecordID);
_("Type: " + PlacesUtils.bookmarks.getItemType(badRecordID));
_("Fetching children.");
store._getChildren("toolbar", {});
_("Fetching all IDs.");
let all = store.getAllIDs();
@ -201,7 +44,49 @@ add_task(function* bad_record_allIDs() {
_("Clean up.");
PlacesUtils.bookmarks.removeItem(badRecordID);
yield new Promise(r => server.stop(r));
server.stop(run_next_test);
});
add_test(function test_ID_caching() {
let server = new SyncServer();
server.start();
let syncTesting = new SyncTestingInfrastructure(server.server);
_("Ensure that Places IDs are not cached.");
let engine = new BookmarksEngine(Service);
let store = engine._store;
_("All IDs: " + JSON.stringify(store.getAllIDs()));
let mobileID = store.idForGUID("mobile");
_("Change the GUID for that item, and drop the mobile anno.");
store._setGUID(mobileID, "abcdefghijkl");
PlacesUtils.annotations.removeItemAnnotation(mobileID, "mobile/bookmarksRoot");
let err;
let newMobileID;
// With noCreate, we don't find an entry.
try {
newMobileID = store.idForGUID("mobile", true);
_("New mobile ID: " + newMobileID);
} catch (ex) {
err = ex;
_("Error: " + Utils.exceptionStr(err));
}
do_check_true(!err);
// With !noCreate, lookup works, and it's different.
newMobileID = store.idForGUID("mobile", false);
_("New mobile ID: " + newMobileID);
do_check_true(!!newMobileID);
do_check_neq(newMobileID, mobileID);
// And it's repeatable, even with creation enabled.
do_check_eq(newMobileID, store.idForGUID("mobile", false));
do_check_eq(store.GUIDForId(mobileID), "abcdefghijkl");
server.stop(run_next_test);
});
function serverForFoo(engine) {
@ -212,7 +97,7 @@ function serverForFoo(engine) {
});
}
add_task(function* test_processIncoming_error_orderChildren() {
add_test(function test_processIncoming_error_orderChildren() {
_("Ensure that _orderChildren() is called even when _processIncoming() throws an error.");
let engine = new BookmarksEngine(Service);
@ -259,11 +144,11 @@ add_task(function* test_processIncoming_error_orderChildren() {
let error;
try {
yield sync_engine_and_validate_telem(engine, true)
engine.sync();
} catch(ex) {
error = ex;
}
ok(!!error);
do_check_true(!!error);
// Verify that the bookmark order has been applied.
let new_children = store.createRecord(folder1_guid).children;
@ -278,11 +163,11 @@ add_task(function* test_processIncoming_error_orderChildren() {
store.wipe();
Svc.Prefs.resetBranch("");
Service.recordManager.clearCache();
yield new Promise(resolve => server.stop(resolve));
server.stop(run_next_test);
}
});
add_task(function* test_restorePromptsReupload() {
add_task(function test_restorePromptsReupload() {
_("Ensure that restoring from a backup will reupload all records.");
let engine = new BookmarksEngine(Service);
let store = engine._store;
@ -319,7 +204,8 @@ add_task(function* test_restorePromptsReupload() {
backupFile.append("t_b_e_" + Date.now() + ".json");
_("Backing up to file " + backupFile.path);
yield BookmarkJSONUtils.exportToFile(backupFile.path);
backupFile.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, 0600);
yield BookmarkJSONUtils.exportToFile(backupFile);
_("Create a different record and sync.");
let bmk2_id = PlacesUtils.bookmarks.insertBookmark(
@ -331,17 +217,17 @@ add_task(function* test_restorePromptsReupload() {
let error;
try {
yield sync_engine_and_validate_telem(engine, false);
engine.sync();
} catch(ex) {
error = ex;
_("Got error: " + Log.exceptionStr(ex));
_("Got error: " + Utils.exceptionStr(ex));
}
do_check_true(!error);
_("Verify that there's only one bookmark on the server, and it's Thunderbird.");
// Of course, there's also the Bookmarks Toolbar and Bookmarks Menu...
let wbos = collection.keys(function (id) {
return ["menu", "toolbar", "mobile", "unfiled", folder1_guid].indexOf(id) == -1;
return ["menu", "toolbar", "mobile", folder1_guid].indexOf(id) == -1;
});
do_check_eq(wbos.length, 1);
do_check_eq(wbos[0], bmk2_guid);
@ -371,14 +257,14 @@ add_task(function* test_restorePromptsReupload() {
do_check_true(found);
_("Have the correct number of IDs locally, too.");
do_check_eq(count, ["menu", "toolbar", "mobile", "unfiled", folder1_id, bmk1_id].length);
do_check_eq(count, ["menu", "toolbar", folder1_id, bmk1_id].length);
_("Sync again. This'll wipe bookmarks from the server.");
try {
yield sync_engine_and_validate_telem(engine, false);
engine.sync();
} catch(ex) {
error = ex;
_("Got error: " + Log.exceptionStr(ex));
_("Got error: " + Utils.exceptionStr(ex));
}
do_check_true(!error);
@ -391,9 +277,7 @@ add_task(function* test_restorePromptsReupload() {
let folderWBOs = payloads.filter(function (wbo) {
return ((wbo.type == "folder") &&
(wbo.id != "menu") &&
(wbo.id != "toolbar") &&
(wbo.id != "unfiled") &&
(wbo.id != "mobile"));
(wbo.id != "toolbar"));
});
do_check_eq(bookmarkWBOs.length, 1);
@ -420,12 +304,10 @@ function FakeRecord(constructor, r) {
for (let x in r) {
this[x] = r[x];
}
// Borrow the constructor's conversion functions.
this.toSyncBookmark = constructor.prototype.toSyncBookmark;
}
// Bug 632287.
add_task(function* test_mismatched_types() {
add_test(function test_mismatched_types() {
_("Ensure that handling a record that changes type causes deletion " +
"then re-adding.");
@ -437,7 +319,6 @@ add_task(function* test_mismatched_types() {
"description":null,
"parentid": "toolbar"
};
oldRecord.cleartext = oldRecord;
let newRecord = {
"id": "l1nZZXfB8nC7",
@ -453,7 +334,6 @@ add_task(function* test_mismatched_types() {
"oT74WwV8_j4P", "IztsItWVSo3-"],
"parentid": "toolbar"
};
newRecord.cleartext = newRecord;
let engine = new BookmarksEngine(Service);
let store = engine._store;
@ -466,8 +346,8 @@ add_task(function* test_mismatched_types() {
let bms = PlacesUtils.bookmarks;
let oldR = new FakeRecord(BookmarkFolder, oldRecord);
let newR = new FakeRecord(Livemark, newRecord);
oldR.parentid = PlacesUtils.bookmarks.toolbarGuid;
newR.parentid = PlacesUtils.bookmarks.toolbarGuid;
oldR._parent = PlacesUtils.bookmarks.toolbarFolder;
newR._parent = PlacesUtils.bookmarks.toolbarFolder;
store.applyIncoming(oldR);
_("Applied old. It's a folder.");
@ -490,11 +370,11 @@ add_task(function* test_mismatched_types() {
store.wipe();
Svc.Prefs.resetBranch("");
Service.recordManager.clearCache();
yield new Promise(r => server.stop(r));
server.stop(run_next_test);
}
});
add_task(function* test_bookmark_guidMap_fail() {
add_test(function test_bookmark_guidMap_fail() {
_("Ensure that failures building the GUID map cause early death.");
let engine = new BookmarksEngine(Service);
@ -514,9 +394,7 @@ add_task(function* test_bookmark_guidMap_fail() {
engine.lastSync = 1; // So we don't back up.
// Make building the GUID map fail.
let pbt = PlacesUtils.promiseBookmarksTree;
PlacesUtils.promiseBookmarksTree = function() { return Promise.reject("Nooo"); };
store.getAllIDs = function () { throw "Nooo"; };
// Ensure that we throw when accessing _guidMap.
engine._syncStartup();
@ -542,11 +420,26 @@ add_task(function* test_bookmark_guidMap_fail() {
}
do_check_eq(err, "Nooo");
PlacesUtils.promiseBookmarksTree = pbt;
yield new Promise(r => server.stop(r));
server.stop(run_next_test);
});
add_task(function* test_bookmark_tag_but_no_uri() {
add_test(function test_bookmark_is_taggable() {
let engine = new BookmarksEngine(Service);
let store = engine._store;
do_check_true(store.isTaggable("bookmark"));
do_check_true(store.isTaggable("microsummary"));
do_check_true(store.isTaggable("query"));
do_check_false(store.isTaggable("folder"));
do_check_false(store.isTaggable("livemark"));
do_check_false(store.isTaggable(null));
do_check_false(store.isTaggable(undefined));
do_check_false(store.isTaggable(""));
run_next_test();
});
add_test(function test_bookmark_tag_but_no_uri() {
_("Ensure that a bookmark record with tags, but no URI, doesn't throw an exception.");
let engine = new BookmarksEngine(Service);
@ -555,43 +448,30 @@ add_task(function* test_bookmark_tag_but_no_uri() {
// We're simply checking that no exception is thrown, so
// no actual checks in this test.
yield PlacesSyncUtils.bookmarks.insert({
kind: PlacesSyncUtils.bookmarks.KINDS.BOOKMARK,
syncId: Utils.makeGUID(),
parentSyncId: "toolbar",
url: "http://example.com",
tags: ["foo"],
});
yield PlacesSyncUtils.bookmarks.insert({
kind: PlacesSyncUtils.bookmarks.KINDS.BOOKMARK,
syncId: Utils.makeGUID(),
parentSyncId: "toolbar",
url: "http://example.org",
tags: null,
});
yield PlacesSyncUtils.bookmarks.insert({
kind: PlacesSyncUtils.bookmarks.KINDS.BOOKMARK,
syncId: Utils.makeGUID(),
url: "about:fake",
parentSyncId: "toolbar",
tags: null,
});
store._tagURI(null, ["foo"]);
store._tagURI(null, null);
store._tagURI(Utils.makeURI("about:fake"), null);
let record = new FakeRecord(BookmarkFolder, {
parentid: "toolbar",
let record = {
_parent: PlacesUtils.bookmarks.toolbarFolder,
id: Utils.makeGUID(),
description: "",
tags: ["foo"],
title: "Taggy tag",
type: "folder"
});
};
// Because update() walks the cleartext.
record.cleartext = record;
store.create(record);
record.tags = ["bar"];
store.update(record);
run_next_test();
});
add_task(function* test_misreconciled_root() {
add_test(function test_misreconciled_root() {
_("Ensure that we don't reconcile an arbitrary record with a root.");
let engine = new BookmarksEngine(Service);
@ -636,9 +516,6 @@ add_task(function* test_misreconciled_root() {
_("Applying record.");
engine._processIncoming({
getBatched() {
return this.get();
},
get: function () {
this.recordHandler(encrypted);
return {success: true}
@ -655,7 +532,7 @@ add_task(function* test_misreconciled_root() {
do_check_eq(parentGUIDBefore, parentGUIDAfter);
do_check_eq(parentIDBefore, parentIDAfter);
yield new Promise(r => server.stop(r));
server.stop(run_next_test);
});
function run_test() {

View file

@ -1,63 +0,0 @@
Cu.import("resource://gre/modules/PlacesUtils.jsm");
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/engines/bookmarks.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/util.js");
Service.engineManager.register(BookmarksEngine);
var engine = Service.engineManager.get("bookmarks");
var store = engine._store;
var tracker = engine._tracker;
add_task(function* test_ignore_invalid_uri() {
_("Ensure that we don't die with invalid bookmarks.");
// First create a valid bookmark.
let bmid = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
Services.io.newURI("http://example.com/", null, null),
PlacesUtils.bookmarks.DEFAULT_INDEX,
"the title");
// Now update moz_places with an invalid url.
yield PlacesUtils.withConnectionWrapper("test_ignore_invalid_uri", Task.async(function* (db) {
yield db.execute(
`UPDATE moz_places SET url = :url, url_hash = hash(:url)
WHERE id = (SELECT b.fk FROM moz_bookmarks b
WHERE b.id = :id LIMIT 1)`,
{ id: bmid, url: "<invalid url>" });
}));
// Ensure that this doesn't throw even though the DB is now in a bad state (a
// bookmark has an illegal url).
engine._buildGUIDMap();
});
add_task(function* test_ignore_missing_uri() {
_("Ensure that we don't die with a bookmark referencing an invalid bookmark id.");
// First create a valid bookmark.
let bmid = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
Services.io.newURI("http://example.com/", null, null),
PlacesUtils.bookmarks.DEFAULT_INDEX,
"the title");
// Now update moz_bookmarks to reference a non-existing places ID
yield PlacesUtils.withConnectionWrapper("test_ignore_missing_uri", Task.async(function* (db) {
yield db.execute(
`UPDATE moz_bookmarks SET fk = 999999
WHERE id = :id`
, { id: bmid });
}));
// Ensure that this doesn't throw even though the DB is now in a bad state (a
// bookmark has an illegal url).
engine._buildGUIDMap();
});
function run_test() {
initTestLogging('Trace');
run_next_test();
}

View file

@ -85,12 +85,12 @@ function run_test() {
do_check_eq(PlacesUtils.bookmarks.getKeywordForBookmark(id), null);
do_check_throws(
() => PlacesUtils.annotations.getItemAnnotation(id, GENERATORURI_ANNO),
function () PlacesUtils.annotations.getItemAnnotation(id, GENERATORURI_ANNO),
Cr.NS_ERROR_NOT_AVAILABLE
);
do_check_throws(
() => PlacesUtils.annotations.getItemAnnotation(id, STATICTITLE_ANNO),
function () PlacesUtils.annotations.getItemAnnotation(id, STATICTITLE_ANNO),
Cr.NS_ERROR_NOT_AVAILABLE
);

View file

@ -12,11 +12,11 @@ Cu.import("resource://testing-common/services/common/utils.js");
const DESCRIPTION_ANNO = "bookmarkProperties/description";
var engine = Service.engineManager.get("bookmarks");
var store = engine._store;
let engine = Service.engineManager.get("bookmarks");
let store = engine._store;
// Record borrowed from Bug 631361.
var record631361 = {
let record631361 = {
id: "M5bwUKK8hPyF",
index: 150,
modified: 1296768176.49,
@ -103,11 +103,20 @@ add_test(function test_livemark_descriptions() {
add_test(function test_livemark_invalid() {
_("Livemarks considered invalid by nsLivemarkService are skipped.");
_("Parent is 0, which is invalid. Will be set to unfiled.");
let noParentRec = makeLivemark(record631361.payload, true);
noParentRec._parent = 0;
store.create(noParentRec);
let recID = store.idForGUID(noParentRec.id, true);
do_check_true(recID > 0);
do_check_eq(PlacesUtils.bookmarks.getFolderIdForItem(recID), PlacesUtils.bookmarks.unfiledBookmarksFolder);
_("Parent is unknown. Will be set to unfiled.");
let lateParentRec = makeLivemark(record631361.payload, true);
let parentGUID = Utils.makeGUID();
lateParentRec.parentid = parentGUID;
do_check_eq(-1, store.idForGUID(parentGUID));
lateParentRec._parent = store.idForGUID(parentGUID); // Usually done by applyIncoming.
do_check_eq(-1, lateParentRec._parent);
store.create(lateParentRec);
recID = store.idForGUID(lateParentRec.id, true);
@ -124,7 +133,7 @@ add_test(function test_livemark_invalid() {
_("Parent is a Livemark. Will be skipped.");
let lmParentRec = makeLivemark(record631361.payload, true);
lmParentRec.parentid = store.GUIDForId(recID);
lmParentRec._parent = recID;
store.create(lmParentRec);
// No exception, but no creation occurs.
do_check_eq(-1, store.idForGUID(lmParentRec.id, true));

View file

@ -2,61 +2,53 @@
http://creativecommons.org/publicdomain/zero/1.0/ */
_("Making sure after processing incoming bookmarks, they show up in the right order");
Cu.import("resource://gre/modules/PlacesUtils.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/PlacesUtils.jsm", this);
Cu.import("resource://services-sync/engines/bookmarks.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/util.js");
var check = Task.async(function* (expected, message) {
let root = yield PlacesUtils.promiseBookmarksTree();
function getBookmarks(folderId) {
let bookmarks = [];
let bookmarks = (function mapTree(children) {
return children.map(child => {
let result = {
guid: child.guid,
index: child.index,
};
if (child.children) {
result.children = mapTree(child.children);
}
if (child.annos) {
let orphanAnno = child.annos.find(
anno => anno.name == "sync/parent");
if (orphanAnno) {
result.requestedParent = orphanAnno.value;
}
}
return result;
});
}(root.children));
let pos = 0;
while (true) {
let itemId = PlacesUtils.bookmarks.getIdForItemAt(folderId, pos);
_("Got itemId", itemId, "under", folderId, "at", pos);
if (itemId == -1)
break;
switch (PlacesUtils.bookmarks.getItemType(itemId)) {
case PlacesUtils.bookmarks.TYPE_BOOKMARK:
bookmarks.push(PlacesUtils.bookmarks.getItemTitle(itemId));
break;
case PlacesUtils.bookmarks.TYPE_FOLDER:
bookmarks.push(getBookmarks(itemId));
break;
default:
_("Unsupported item type..");
}
pos++;
}
return bookmarks;
}
function check(expected) {
let bookmarks = getBookmarks(PlacesUtils.bookmarks.unfiledBookmarksFolder);
_("Checking if the bookmark structure is", JSON.stringify(expected));
_("Got bookmarks:", JSON.stringify(bookmarks));
deepEqual(bookmarks, expected);
});
do_check_true(Utils.deepEquals(bookmarks, expected));
}
add_task(function* test_bookmark_order() {
function run_test() {
let store = new BookmarksEngine(Service)._store;
initTestLogging("Trace");
_("Starting with a clean slate of no bookmarks");
store.wipe();
yield check([{
guid: PlacesUtils.bookmarks.menuGuid,
index: 0,
}, {
guid: PlacesUtils.bookmarks.toolbarGuid,
index: 1,
}, {
// Index 2 is the tags root. (Root indices depend on the order of the
// `CreateRoot` calls in `Database::CreateBookmarkRoots`).
guid: PlacesUtils.bookmarks.unfiledGuid,
index: 3,
}, {
guid: PlacesUtils.bookmarks.mobileGuid,
index: 4,
}], "clean slate");
check([]);
function bookmark(name, parent) {
let bookmark = new Bookmark("http://weave.server/my-bookmark");
@ -83,447 +75,64 @@ add_task(function* test_bookmark_order() {
store._orderChildren();
delete store._childrenToOrder;
}
let id10 = "10_aaaaaaaaa";
_("basic add first bookmark");
apply(bookmark(id10, ""));
yield check([{
guid: PlacesUtils.bookmarks.menuGuid,
index: 0,
}, {
guid: PlacesUtils.bookmarks.toolbarGuid,
index: 1,
}, {
guid: PlacesUtils.bookmarks.unfiledGuid,
index: 3,
children: [{
guid: id10,
index: 0,
}],
}, {
guid: PlacesUtils.bookmarks.mobileGuid,
index: 4,
}], "basic add first bookmark");
let id20 = "20_aaaaaaaaa";
apply(bookmark("10", ""));
check(["10"]);
_("basic append behind 10");
apply(bookmark(id20, ""));
yield check([{
guid: PlacesUtils.bookmarks.menuGuid,
index: 0,
}, {
guid: PlacesUtils.bookmarks.toolbarGuid,
index: 1,
}, {
guid: PlacesUtils.bookmarks.unfiledGuid,
index: 3,
children: [{
guid: id10,
index: 0,
}, {
guid: id20,
index: 1,
}],
}, {
guid: PlacesUtils.bookmarks.mobileGuid,
index: 4,
}], "basic append behind 10");
apply(bookmark("20", ""));
check(["10", "20"]);
let id31 = "31_aaaaaaaaa";
let id30 = "f30_aaaaaaaa";
_("basic create in folder");
apply(bookmark(id31, id30));
let f30 = folder(id30, "", [id31]);
apply(bookmark("31", "f30"));
let f30 = folder("f30", "", ["31"]);
apply(f30);
yield check([{
guid: PlacesUtils.bookmarks.menuGuid,
index: 0,
}, {
guid: PlacesUtils.bookmarks.toolbarGuid,
index: 1,
}, {
guid: PlacesUtils.bookmarks.unfiledGuid,
index: 3,
children: [{
guid: id10,
index: 0,
}, {
guid: id20,
index: 1,
}, {
guid: id30,
index: 2,
children: [{
guid: id31,
index: 0,
}],
}],
}, {
guid: PlacesUtils.bookmarks.mobileGuid,
index: 4,
}], "basic create in folder");
check(["10", "20", ["31"]]);
let id41 = "41_aaaaaaaaa";
let id40 = "f40_aaaaaaaa";
_("insert missing parent -> append to unfiled");
apply(bookmark(id41, id40));
yield check([{
guid: PlacesUtils.bookmarks.menuGuid,
index: 0,
}, {
guid: PlacesUtils.bookmarks.toolbarGuid,
index: 1,
}, {
guid: PlacesUtils.bookmarks.unfiledGuid,
index: 3,
children: [{
guid: id10,
index: 0,
}, {
guid: id20,
index: 1,
}, {
guid: id30,
index: 2,
children: [{
guid: id31,
index: 0,
}],
}, {
guid: id41,
index: 3,
requestedParent: id40,
}],
}, {
guid: PlacesUtils.bookmarks.mobileGuid,
index: 4,
}], "insert missing parent -> append to unfiled");
let id42 = "42_aaaaaaaaa";
apply(bookmark("41", "f40"));
check(["10", "20", ["31"], "41"]);
_("insert another missing parent -> append");
apply(bookmark(id42, id40));
yield check([{
guid: PlacesUtils.bookmarks.menuGuid,
index: 0,
}, {
guid: PlacesUtils.bookmarks.toolbarGuid,
index: 1,
}, {
guid: PlacesUtils.bookmarks.unfiledGuid,
index: 3,
children: [{
guid: id10,
index: 0,
}, {
guid: id20,
index: 1,
}, {
guid: id30,
index: 2,
children: [{
guid: id31,
index: 0,
}],
}, {
guid: id41,
index: 3,
requestedParent: id40,
}, {
guid: id42,
index: 4,
requestedParent: id40,
}],
}, {
guid: PlacesUtils.bookmarks.mobileGuid,
index: 4,
}], "insert another missing parent -> append");
apply(bookmark("42", "f40"));
check(["10", "20", ["31"], "41", "42"]);
_("insert folder -> move children and followers");
let f40 = folder(id40, "", [id41, id42]);
let f40 = folder("f40", "", ["41", "42"]);
apply(f40);
yield check([{
guid: PlacesUtils.bookmarks.menuGuid,
index: 0,
}, {
guid: PlacesUtils.bookmarks.toolbarGuid,
index: 1,
}, {
guid: PlacesUtils.bookmarks.unfiledGuid,
index: 3,
children: [{
guid: id10,
index: 0,
}, {
guid: id20,
index: 1,
}, {
guid: id30,
index: 2,
children: [{
guid: id31,
index: 0,
}],
}, {
guid: id40,
index: 3,
children: [{
guid: id41,
index: 0,
}, {
guid: id42,
index: 1,
}]
}],
}, {
guid: PlacesUtils.bookmarks.mobileGuid,
index: 4,
}], "insert folder -> move children and followers");
check(["10", "20", ["31"], ["41", "42"]]);
_("Moving 41 behind 42 -> update f40");
f40.children = [id42, id41];
f40.children = ["42", "41"];
apply(f40);
yield check([{
guid: PlacesUtils.bookmarks.menuGuid,
index: 0,
}, {
guid: PlacesUtils.bookmarks.toolbarGuid,
index: 1,
}, {
guid: PlacesUtils.bookmarks.unfiledGuid,
index: 3,
children: [{
guid: id10,
index: 0,
}, {
guid: id20,
index: 1,
}, {
guid: id30,
index: 2,
children: [{
guid: id31,
index: 0,
}],
}, {
guid: id40,
index: 3,
children: [{
guid: id42,
index: 0,
}, {
guid: id41,
index: 1,
}]
}],
}, {
guid: PlacesUtils.bookmarks.mobileGuid,
index: 4,
}], "Moving 41 behind 42 -> update f40");
check(["10", "20", ["31"], ["42", "41"]]);
_("Moving 10 back to front -> update 10, 20");
f40.children = [id41, id42];
f40.children = ["41", "42"];
apply(f40);
yield check([{
guid: PlacesUtils.bookmarks.menuGuid,
index: 0,
}, {
guid: PlacesUtils.bookmarks.toolbarGuid,
index: 1,
}, {
guid: PlacesUtils.bookmarks.unfiledGuid,
index: 3,
children: [{
guid: id10,
index: 0,
}, {
guid: id20,
index: 1,
}, {
guid: id30,
index: 2,
children: [{
guid: id31,
index: 0,
}],
}, {
guid: id40,
index: 3,
children: [{
guid: id41,
index: 0,
}, {
guid: id42,
index: 1,
}]
}],
}, {
guid: PlacesUtils.bookmarks.mobileGuid,
index: 4,
}], "Moving 10 back to front -> update 10, 20");
check(["10", "20", ["31"], ["41", "42"]]);
_("Moving 20 behind 42 in f40 -> update 50");
apply(bookmark(id20, id40));
yield check([{
guid: PlacesUtils.bookmarks.menuGuid,
index: 0,
}, {
guid: PlacesUtils.bookmarks.toolbarGuid,
index: 1,
}, {
guid: PlacesUtils.bookmarks.unfiledGuid,
index: 3,
children: [{
guid: id10,
index: 0,
}, {
guid: id30,
index: 1,
children: [{
guid: id31,
index: 0,
}],
}, {
guid: id40,
index: 2,
children: [{
guid: id41,
index: 0,
}, {
guid: id42,
index: 1,
}, {
guid: id20,
index: 2,
}]
}],
}, {
guid: PlacesUtils.bookmarks.mobileGuid,
index: 4,
}], "Moving 20 behind 42 in f40 -> update 50");
apply(bookmark("20", "f40"));
check(["10", ["31"], ["41", "42", "20"]]);
_("Moving 10 in front of 31 in f30 -> update 10, f30");
apply(bookmark(id10, id30));
f30.children = [id10, id31];
apply(bookmark("10", "f30"));
f30.children = ["10", "31"];
apply(f30);
yield check([{
guid: PlacesUtils.bookmarks.menuGuid,
index: 0,
}, {
guid: PlacesUtils.bookmarks.toolbarGuid,
index: 1,
}, {
guid: PlacesUtils.bookmarks.unfiledGuid,
index: 3,
children: [{
guid: id30,
index: 0,
children: [{
guid: id10,
index: 0,
}, {
guid: id31,
index: 1,
}],
}, {
guid: id40,
index: 1,
children: [{
guid: id41,
index: 0,
}, {
guid: id42,
index: 1,
}, {
guid: id20,
index: 2,
}]
}],
}, {
guid: PlacesUtils.bookmarks.mobileGuid,
index: 4,
}], "Moving 10 in front of 31 in f30 -> update 10, f30");
check([["10", "31"], ["41", "42", "20"]]);
_("Moving 20 from f40 to f30 -> update 20, f30");
apply(bookmark(id20, id30));
f30.children = [id10, id20, id31];
apply(bookmark("20", "f30"));
f30.children = ["10", "20", "31"];
apply(f30);
yield check([{
guid: PlacesUtils.bookmarks.menuGuid,
index: 0,
}, {
guid: PlacesUtils.bookmarks.toolbarGuid,
index: 1,
}, {
guid: PlacesUtils.bookmarks.unfiledGuid,
index: 3,
children: [{
guid: id30,
index: 0,
children: [{
guid: id10,
index: 0,
}, {
guid: id20,
index: 1,
}, {
guid: id31,
index: 2,
}],
}, {
guid: id40,
index: 1,
children: [{
guid: id41,
index: 0,
}, {
guid: id42,
index: 1,
}]
}],
}, {
guid: PlacesUtils.bookmarks.mobileGuid,
index: 4,
}], "Moving 20 from f40 to f30 -> update 20, f30");
check([["10", "20", "31"], ["41", "42"]]);
_("Move 20 back to front -> update 20, f30");
apply(bookmark(id20, ""));
f30.children = [id10, id31];
apply(bookmark("20", ""));
f30.children = ["10", "31"];
apply(f30);
yield check([{
guid: PlacesUtils.bookmarks.menuGuid,
index: 0,
}, {
guid: PlacesUtils.bookmarks.toolbarGuid,
index: 1,
}, {
guid: PlacesUtils.bookmarks.unfiledGuid,
index: 3,
children: [{
guid: id30,
index: 0,
children: [{
guid: id10,
index: 0,
}, {
guid: id31,
index: 1,
}],
}, {
guid: id40,
index: 1,
children: [{
guid: id41,
index: 0,
}, {
guid: id42,
index: 1,
}],
}, {
guid: id20,
index: 2,
}],
}, {
guid: PlacesUtils.bookmarks.mobileGuid,
index: 4,
}], "Move 20 back to front -> update 20, f30");
check([["10", "31"], ["41", "42"], "20"]);
});
}

View file

@ -7,54 +7,45 @@ Cu.import("resource://services-sync/engines/bookmarks.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/util.js");
var engine = new BookmarksEngine(Service);
var store = engine._store;
function makeTagRecord(id, uri) {
let tagRecord = new BookmarkQuery("bookmarks", id);
tagRecord.queryId = "MagicTags";
tagRecord.parentName = "Bookmarks Toolbar";
tagRecord.bmkUri = uri;
tagRecord.title = "tagtag";
tagRecord.folderName = "bar";
tagRecord.parentid = PlacesUtils.bookmarks.toolbarGuid;
return tagRecord;
}
let engine = new BookmarksEngine(Service);
let store = engine._store;
function run_test() {
initTestLogging("Trace");
Log.repository.getLogger("Sync.Engine.Bookmarks").level = Log.Level.Trace;
Log.repository.getLogger("Sync.Store.Bookmarks").level = Log.Level.Trace;
let tagRecord = new BookmarkQuery("bookmarks", "abcdefabcdef");
let uri = "place:folder=499&type=7&queryType=1";
let tagRecord = makeTagRecord("abcdefabcdef", uri);
tagRecord.queryId = "MagicTags";
tagRecord.parentName = "Bookmarks Toolbar";
tagRecord.bmkUri = uri;
tagRecord.title = "tagtag";
tagRecord.folderName = "bar";
_("Type: " + tagRecord.type);
_("Folder name: " + tagRecord.folderName);
store.applyIncoming(tagRecord);
store.preprocessTagQuery(tagRecord);
let tags = PlacesUtils.getFolderContents(PlacesUtils.tagsFolderId).root;
_("Verify that the URI has been rewritten.");
do_check_neq(tagRecord.bmkUri, uri);
let tags = store._getNode(PlacesUtils.tagsFolderId);
tags.containerOpen = true;
let tagID;
try {
for (let i = 0; i < tags.childCount; ++i) {
let child = tags.getChild(i);
if (child.title == "bar") {
tagID = child.itemId;
}
}
} finally {
tags.containerOpen = false;
for (let i = 0; i < tags.childCount; ++i) {
let child = tags.getChild(i);
if (child.title == "bar")
tagID = child.itemId;
}
tags.containerOpen = false;
_("Tag ID: " + tagID);
let insertedRecord = store.createRecord("abcdefabcdef", "bookmarks");
do_check_eq(insertedRecord.bmkUri, uri.replace("499", tagID));
do_check_eq(tagRecord.bmkUri, uri.replace("499", tagID));
_("... but not if the type is wrong.");
let wrongTypeURI = "place:folder=499&type=2&queryType=1";
let wrongTypeRecord = makeTagRecord("fedcbafedcba", wrongTypeURI);
store.applyIncoming(wrongTypeRecord);
insertedRecord = store.createRecord("fedcbafedcba", "bookmarks");
do_check_eq(insertedRecord.bmkUri, wrongTypeURI);
tagRecord.bmkUri = wrongTypeURI;
store.preprocessTagQuery(tagRecord);
do_check_eq(tagRecord.bmkUri, wrongTypeURI);
}

View file

@ -16,8 +16,8 @@ var IOService = Cc["@mozilla.org/network/io-service;1"]
Service.engineManager.register(BookmarksEngine);
var engine = Service.engineManager.get("bookmarks");
var store = engine._store;
let engine = Service.engineManager.get("bookmarks");
let store = engine._store;
// Clean up after other tests. Only necessary in XULRunner.
store.wipe();
@ -57,7 +57,7 @@ function serverForFoo(engine) {
// Verify that Places smart bookmarks have their annotation uploaded and
// handled locally.
add_task(function *test_annotation_uploaded() {
add_test(function test_annotation_uploaded() {
let server = serverForFoo(engine);
new SyncTestingInfrastructure(server.server);
@ -110,9 +110,9 @@ add_task(function *test_annotation_uploaded() {
let collection = server.user("foo").collection("bookmarks");
try {
yield sync_engine_and_validate_telem(engine, false);
engine.sync();
let wbos = collection.keys(function (id) {
return ["menu", "toolbar", "mobile", "unfiled"].indexOf(id) == -1;
return ["menu", "toolbar", "mobile"].indexOf(id) == -1;
});
do_check_eq(wbos.length, 1);
@ -141,7 +141,7 @@ add_task(function *test_annotation_uploaded() {
do_check_eq(smartBookmarkCount(), startCount);
_("Sync. Verify that the downloaded record carries the annotation.");
yield sync_engine_and_validate_telem(engine, false);
engine.sync();
_("Verify that the Places DB now has an annotated bookmark.");
_("Our count has increased again.");

View file

@ -11,17 +11,17 @@ const PARENT_ANNO = "sync/parent";
Service.engineManager.register(BookmarksEngine);
var engine = Service.engineManager.get("bookmarks");
var store = engine._store;
var tracker = engine._tracker;
let engine = Service.engineManager.get("bookmarks");
let store = engine._store;
let tracker = engine._tracker;
// Don't write some persistence files asynchronously.
tracker.persistChangedIDs = false;
var fxuri = Utils.makeURI("http://getfirefox.com/");
var tburi = Utils.makeURI("http://getthunderbird.com/");
let fxuri = Utils.makeURI("http://getfirefox.com/");
let tburi = Utils.makeURI("http://getthunderbird.com/");
add_task(function* test_ignore_specials() {
add_test(function test_ignore_specials() {
_("Ensure that we can't delete bookmark roots.");
// Belt...
@ -30,7 +30,6 @@ add_task(function* test_ignore_specials() {
do_check_neq(null, store.idForGUID("toolbar"));
store.applyIncoming(record);
yield store.deletePending();
// Ensure that the toolbar exists.
do_check_neq(null, store.idForGUID("toolbar"));
@ -40,11 +39,11 @@ add_task(function* test_ignore_specials() {
// Braces...
store.remove(record);
yield store.deletePending();
do_check_neq(null, store.idForGUID("toolbar"));
engine._buildGUIDMap();
store.wipe();
run_next_test();
});
add_test(function test_bookmark_create() {
@ -81,8 +80,8 @@ add_test(function test_bookmark_create() {
_("Have the store create a new record object. Verify that it has the same data.");
let newrecord = store.createRecord(fxrecord.id);
do_check_true(newrecord instanceof Bookmark);
for (let property of ["type", "bmkUri", "description", "title",
"keyword", "parentName", "parentid"]) {
for each (let property in ["type", "bmkUri", "description", "title",
"keyword", "parentName", "parentid"]) {
do_check_eq(newrecord[property], fxrecord[property]);
}
do_check_true(Utils.deepEquals(newrecord.tags.sort(),
@ -167,7 +166,7 @@ add_test(function test_bookmark_createRecord() {
_("Verify that the record is created accordingly.");
let record = store.createRecord(bmk1_guid);
do_check_eq(record.title, "");
do_check_eq(record.title, null);
do_check_eq(record.description, null);
do_check_eq(record.keyword, null);
@ -198,7 +197,7 @@ add_test(function test_folder_create() {
_("Have the store create a new record object. Verify that it has the same data.");
let newrecord = store.createRecord(folder.id);
do_check_true(newrecord instanceof BookmarkFolder);
for (let property of ["title", "parentName", "parentid"])
for each (let property in ["title", "parentName", "parentid"])
do_check_eq(newrecord[property], folder[property]);
_("Folders have high sort index to ensure they're synced first.");
@ -244,7 +243,7 @@ add_test(function test_folder_createRecord() {
}
});
add_task(function* test_deleted() {
add_test(function test_deleted() {
try {
_("Create a bookmark that will be deleted.");
let bmk1_id = PlacesUtils.bookmarks.insertBookmark(
@ -256,7 +255,7 @@ add_task(function* test_deleted() {
let record = new PlacesItem("bookmarks", bmk1_guid);
record.deleted = true;
store.applyIncoming(record);
yield store.deletePending();
_("Ensure it has been deleted.");
let error;
try {
@ -272,6 +271,7 @@ add_task(function* test_deleted() {
} finally {
_("Clean up.");
store.wipe();
run_next_test();
}
});
@ -428,106 +428,6 @@ add_test(function test_empty_query_doesnt_die() {
run_next_test();
});
function assertDeleted(id) {
let error;
try {
PlacesUtils.bookmarks.getItemType(id);
} catch (e) {
error = e;
}
equal(error.result, Cr.NS_ERROR_ILLEGAL_VALUE)
}
add_task(function* test_delete_buffering() {
store.wipe();
try {
_("Create a folder with two bookmarks.");
let folder = new BookmarkFolder("bookmarks", "testfolder-1");
folder.parentName = "Bookmarks Toolbar";
folder.parentid = "toolbar";
folder.title = "Test Folder";
store.applyIncoming(folder);
let fxRecord = new Bookmark("bookmarks", "get-firefox1");
fxRecord.bmkUri = fxuri.spec;
fxRecord.title = "Get Firefox!";
fxRecord.parentName = "Test Folder";
fxRecord.parentid = "testfolder-1";
let tbRecord = new Bookmark("bookmarks", "get-tndrbrd1");
tbRecord.bmkUri = tburi.spec;
tbRecord.title = "Get Thunderbird!";
tbRecord.parentName = "Test Folder";
tbRecord.parentid = "testfolder-1";
store.applyIncoming(fxRecord);
store.applyIncoming(tbRecord);
let folderId = store.idForGUID(folder.id);
let fxRecordId = store.idForGUID(fxRecord.id);
let tbRecordId = store.idForGUID(tbRecord.id);
_("Check everything was created correctly.");
equal(PlacesUtils.bookmarks.getItemType(fxRecordId),
PlacesUtils.bookmarks.TYPE_BOOKMARK);
equal(PlacesUtils.bookmarks.getItemType(tbRecordId),
PlacesUtils.bookmarks.TYPE_BOOKMARK);
equal(PlacesUtils.bookmarks.getItemType(folderId),
PlacesUtils.bookmarks.TYPE_FOLDER);
equal(PlacesUtils.bookmarks.getFolderIdForItem(fxRecordId), folderId);
equal(PlacesUtils.bookmarks.getFolderIdForItem(tbRecordId), folderId);
equal(PlacesUtils.bookmarks.getFolderIdForItem(folderId),
PlacesUtils.bookmarks.toolbarFolder);
_("Delete the folder and one bookmark.");
let deleteFolder = new PlacesItem("bookmarks", "testfolder-1");
deleteFolder.deleted = true;
let deleteFxRecord = new PlacesItem("bookmarks", "get-firefox1");
deleteFxRecord.deleted = true;
store.applyIncoming(deleteFolder);
store.applyIncoming(deleteFxRecord);
_("Check that we haven't deleted them yet, but that the deletions are queued");
// these will throw if we've deleted them
equal(PlacesUtils.bookmarks.getItemType(fxRecordId),
PlacesUtils.bookmarks.TYPE_BOOKMARK);
equal(PlacesUtils.bookmarks.getItemType(folderId),
PlacesUtils.bookmarks.TYPE_FOLDER);
equal(PlacesUtils.bookmarks.getFolderIdForItem(fxRecordId), folderId);
ok(store._foldersToDelete.has(folder.id));
ok(store._atomsToDelete.has(fxRecord.id));
ok(!store._atomsToDelete.has(tbRecord.id));
_("Process pending deletions and ensure that the right things are deleted.");
let updatedGuids = yield store.deletePending();
deepEqual(updatedGuids.sort(), ["get-tndrbrd1", "toolbar"]);
assertDeleted(fxRecordId);
assertDeleted(folderId);
ok(!store._foldersToDelete.has(folder.id));
ok(!store._atomsToDelete.has(fxRecord.id));
equal(PlacesUtils.bookmarks.getFolderIdForItem(tbRecordId),
PlacesUtils.bookmarks.toolbarFolder);
} finally {
_("Clean up.");
store.wipe();
}
});
function run_test() {
initTestLogging('Trace');
run_next_test();

File diff suppressed because it is too large Load diff

View file

@ -1,347 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
Components.utils.import("resource://services-sync/bookmark_validator.js");
Components.utils.import("resource://services-sync/util.js");
function inspectServerRecords(data) {
return new BookmarkValidator().inspectServerRecords(data);
}
add_test(function test_isr_rootOnServer() {
let c = inspectServerRecords([{
id: 'places',
type: 'folder',
children: [],
}]);
ok(c.problemData.rootOnServer);
run_next_test();
});
add_test(function test_isr_empty() {
let c = inspectServerRecords([]);
ok(!c.problemData.rootOnServer);
notEqual(c.root, null);
run_next_test();
});
add_test(function test_isr_cycles() {
let c = inspectServerRecords([
{id: 'C', type: 'folder', children: ['A', 'B'], parentid: 'places'},
{id: 'A', type: 'folder', children: ['B'], parentid: 'B'},
{id: 'B', type: 'folder', children: ['A'], parentid: 'A'},
]).problemData;
equal(c.cycles.length, 1);
ok(c.cycles[0].indexOf('A') >= 0);
ok(c.cycles[0].indexOf('B') >= 0);
run_next_test();
});
add_test(function test_isr_orphansMultiParents() {
let c = inspectServerRecords([
{ id: 'A', type: 'bookmark', parentid: 'D' },
{ id: 'B', type: 'folder', parentid: 'places', children: ['A']},
{ id: 'C', type: 'folder', parentid: 'places', children: ['A']},
]).problemData;
deepEqual(c.orphans, [{ id: "A", parent: "D" }]);
equal(c.multipleParents.length, 1)
ok(c.multipleParents[0].parents.indexOf('B') >= 0);
ok(c.multipleParents[0].parents.indexOf('C') >= 0);
run_next_test();
});
add_test(function test_isr_orphansMultiParents2() {
let c = inspectServerRecords([
{ id: 'A', type: 'bookmark', parentid: 'D' },
{ id: 'B', type: 'folder', parentid: 'places', children: ['A']},
]).problemData;
equal(c.orphans.length, 1);
equal(c.orphans[0].id, 'A');
equal(c.multipleParents.length, 0);
run_next_test();
});
add_test(function test_isr_deletedParents() {
let c = inspectServerRecords([
{ id: 'A', type: 'bookmark', parentid: 'B' },
{ id: 'B', type: 'folder', parentid: 'places', children: ['A']},
{ id: 'B', type: 'item', deleted: true},
]).problemData;
deepEqual(c.deletedParents, ['A'])
run_next_test();
});
add_test(function test_isr_badChildren() {
let c = inspectServerRecords([
{ id: 'A', type: 'bookmark', parentid: 'places', children: ['B', 'C'] },
{ id: 'C', type: 'bookmark', parentid: 'A' }
]).problemData;
deepEqual(c.childrenOnNonFolder, ['A'])
deepEqual(c.missingChildren, [{parent: 'A', child: 'B'}]);
deepEqual(c.parentNotFolder, ['C']);
run_next_test();
});
add_test(function test_isr_parentChildMismatches() {
let c = inspectServerRecords([
{ id: 'A', type: 'folder', parentid: 'places', children: [] },
{ id: 'B', type: 'bookmark', parentid: 'A' }
]).problemData;
deepEqual(c.parentChildMismatches, [{parent: 'A', child: 'B'}]);
run_next_test();
});
add_test(function test_isr_duplicatesAndMissingIDs() {
let c = inspectServerRecords([
{id: 'A', type: 'folder', parentid: 'places', children: []},
{id: 'A', type: 'folder', parentid: 'places', children: []},
{type: 'folder', parentid: 'places', children: []}
]).problemData;
equal(c.missingIDs, 1);
deepEqual(c.duplicates, ['A']);
run_next_test();
});
add_test(function test_isr_duplicateChildren() {
let c = inspectServerRecords([
{id: 'A', type: 'folder', parentid: 'places', children: ['B', 'B']},
{id: 'B', type: 'bookmark', parentid: 'A'},
]).problemData;
deepEqual(c.duplicateChildren, ['A']);
run_next_test();
});
// Each compareServerWithClient test mutates these, so we can't just keep them
// global
function getDummyServerAndClient() {
let server = [
{
id: 'menu',
parentid: 'places',
type: 'folder',
parentName: '',
title: 'foo',
children: ['bbbbbbbbbbbb', 'cccccccccccc']
},
{
id: 'bbbbbbbbbbbb',
type: 'bookmark',
parentid: 'menu',
parentName: 'foo',
title: 'bar',
bmkUri: 'http://baz.com'
},
{
id: 'cccccccccccc',
parentid: 'menu',
parentName: 'foo',
title: '',
type: 'query',
bmkUri: 'place:type=6&sort=14&maxResults=10'
}
];
let client = {
"guid": "root________",
"title": "",
"id": 1,
"type": "text/x-moz-place-container",
"children": [
{
"guid": "menu________",
"title": "foo",
"id": 1000,
"type": "text/x-moz-place-container",
"children": [
{
"guid": "bbbbbbbbbbbb",
"title": "bar",
"id": 1001,
"type": "text/x-moz-place",
"uri": "http://baz.com"
},
{
"guid": "cccccccccccc",
"title": "",
"id": 1002,
"annos": [{
"name": "Places/SmartBookmark",
"flags": 0,
"expires": 4,
"value": "RecentTags"
}],
"type": "text/x-moz-place",
"uri": "place:type=6&sort=14&maxResults=10"
}
]
}
]
};
return {server, client};
}
add_test(function test_cswc_valid() {
let {server, client} = getDummyServerAndClient();
let c = new BookmarkValidator().compareServerWithClient(server, client).problemData;
equal(c.clientMissing.length, 0);
equal(c.serverMissing.length, 0);
equal(c.differences.length, 0);
run_next_test();
});
add_test(function test_cswc_serverMissing() {
let {server, client} = getDummyServerAndClient();
// remove c
server.pop();
server[0].children.pop();
let c = new BookmarkValidator().compareServerWithClient(server, client).problemData;
deepEqual(c.serverMissing, ['cccccccccccc']);
equal(c.clientMissing.length, 0);
deepEqual(c.structuralDifferences, [{id: 'menu', differences: ['childGUIDs']}]);
run_next_test();
});
add_test(function test_cswc_clientMissing() {
let {server, client} = getDummyServerAndClient();
client.children[0].children.pop();
let c = new BookmarkValidator().compareServerWithClient(server, client).problemData;
deepEqual(c.clientMissing, ['cccccccccccc']);
equal(c.serverMissing.length, 0);
deepEqual(c.structuralDifferences, [{id: 'menu', differences: ['childGUIDs']}]);
run_next_test();
});
add_test(function test_cswc_differences() {
{
let {server, client} = getDummyServerAndClient();
client.children[0].children[0].title = 'asdf';
let c = new BookmarkValidator().compareServerWithClient(server, client).problemData;
equal(c.clientMissing.length, 0);
equal(c.serverMissing.length, 0);
deepEqual(c.differences, [{id: 'bbbbbbbbbbbb', differences: ['title']}]);
}
{
let {server, client} = getDummyServerAndClient();
server[2].type = 'bookmark';
let c = new BookmarkValidator().compareServerWithClient(server, client).problemData;
equal(c.clientMissing.length, 0);
equal(c.serverMissing.length, 0);
deepEqual(c.differences, [{id: 'cccccccccccc', differences: ['type']}]);
}
run_next_test();
});
add_test(function test_cswc_serverUnexpected() {
let {server, client} = getDummyServerAndClient();
client.children.push({
"guid": "dddddddddddd",
"title": "",
"id": 2000,
"annos": [{
"name": "places/excludeFromBackup",
"flags": 0,
"expires": 4,
"value": 1
}, {
"name": "PlacesOrganizer/OrganizerFolder",
"flags": 0,
"expires": 4,
"value": 7
}],
"type": "text/x-moz-place-container",
"children": [{
"guid": "eeeeeeeeeeee",
"title": "History",
"annos": [{
"name": "places/excludeFromBackup",
"flags": 0,
"expires": 4,
"value": 1
}, {
"name": "PlacesOrganizer/OrganizerQuery",
"flags": 0,
"expires": 4,
"value": "History"
}],
"type": "text/x-moz-place",
"uri": "place:type=3&sort=4"
}]
});
server.push({
id: 'dddddddddddd',
parentid: 'places',
parentName: '',
title: '',
type: 'folder',
children: ['eeeeeeeeeeee']
}, {
id: 'eeeeeeeeeeee',
parentid: 'dddddddddddd',
parentName: '',
title: 'History',
type: 'query',
bmkUri: 'place:type=3&sort=4'
});
let c = new BookmarkValidator().compareServerWithClient(server, client).problemData;
equal(c.clientMissing.length, 0);
equal(c.serverMissing.length, 0);
equal(c.serverUnexpected.length, 2);
deepEqual(c.serverUnexpected, ["dddddddddddd", "eeeeeeeeeeee"]);
run_next_test();
});
function validationPing(server, client, duration) {
return wait_for_ping(function() {
// fake this entirely
Svc.Obs.notify("weave:service:sync:start");
Svc.Obs.notify("weave:engine:sync:start", null, "bookmarks");
Svc.Obs.notify("weave:engine:sync:finish", null, "bookmarks");
let validator = new BookmarkValidator();
let data = {
// We fake duration and version just so that we can verify they're passed through.
duration,
version: validator.version,
recordCount: server.length,
problems: validator.compareServerWithClient(server, client).problemData,
};
Svc.Obs.notify("weave:engine:validate:finish", data, "bookmarks");
Svc.Obs.notify("weave:service:sync:finish");
}, true); // Allow "failing" pings, since having validation info indicates failure.
}
add_task(function *test_telemetry_integration() {
let {server, client} = getDummyServerAndClient();
// remove "c"
server.pop();
server[0].children.pop();
const duration = 50;
let ping = yield validationPing(server, client, duration);
ok(ping.engines);
let bme = ping.engines.find(e => e.name === "bookmarks");
ok(bme);
ok(bme.validation);
ok(bme.validation.problems)
equal(bme.validation.checked, server.length);
equal(bme.validation.took, duration);
bme.validation.problems.sort((a, b) => String.localeCompare(a.name, b.name));
equal(bme.validation.version, new BookmarkValidator().version);
deepEqual(bme.validation.problems, [
{ name: "badClientRoots", count: 3 },
{ name: "sdiff:childGUIDs", count: 1 },
{ name: "serverMissing", count: 1 },
{ name: "structuralDifferences", count: 1 },
]);
});
function run_test() {
run_next_test();
}

View file

@ -16,14 +16,13 @@ Cu.import("resource://gre/modules/FxAccountsCommon.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/status.js");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-common/tokenserverclient.js");
const SECOND_MS = 1000;
const MINUTE_MS = SECOND_MS * 60;
const HOUR_MS = MINUTE_MS * 60;
var identityConfig = makeIdentityConfig();
var browseridManager = new BrowserIDManager();
let identityConfig = makeIdentityConfig();
let browseridManager = new BrowserIDManager();
configureFxAccountIdentity(browseridManager, identityConfig);
/**
@ -32,14 +31,11 @@ configureFxAccountIdentity(browseridManager, identityConfig);
* headers. We will use this to test clock skew compensation in these headers
* below.
*/
var MockFxAccountsClient = function() {
let MockFxAccountsClient = function() {
FxAccountsClient.apply(this);
};
MockFxAccountsClient.prototype = {
__proto__: FxAccountsClient.prototype,
accountStatus() {
return Promise.resolve(true);
}
__proto__: FxAccountsClient.prototype
};
function MockFxAccounts() {
@ -77,7 +73,7 @@ add_test(function test_initial_state() {
}
);
add_task(function* test_initialializeWithCurrentIdentity() {
add_task(function test_initialializeWithCurrentIdentity() {
_("Verify start after initializeWithCurrentIdentity");
browseridManager.initializeWithCurrentIdentity();
yield browseridManager.whenReadyToAuthenticate.promise;
@ -87,57 +83,7 @@ add_task(function* test_initialializeWithCurrentIdentity() {
}
);
add_task(function* test_initialializeWithAuthErrorAndDeletedAccount() {
_("Verify sync unpair after initializeWithCurrentIdentity with auth error + account deleted");
var identityConfig = makeIdentityConfig();
var browseridManager = new BrowserIDManager();
// Use the real `_getAssertion` method that calls
// `mockFxAClient.signCertificate`.
let fxaInternal = makeFxAccountsInternalMock(identityConfig);
delete fxaInternal._getAssertion;
configureFxAccountIdentity(browseridManager, identityConfig, fxaInternal);
browseridManager._fxaService.internal.initialize();
let signCertificateCalled = false;
let accountStatusCalled = false;
let MockFxAccountsClient = function() {
FxAccountsClient.apply(this);
};
MockFxAccountsClient.prototype = {
__proto__: FxAccountsClient.prototype,
signCertificate() {
signCertificateCalled = true;
return Promise.reject({
code: 401,
errno: ERRNO_INVALID_AUTH_TOKEN,
});
},
accountStatus() {
accountStatusCalled = true;
return Promise.resolve(false);
}
};
let mockFxAClient = new MockFxAccountsClient();
browseridManager._fxaService.internal._fxAccountsClient = mockFxAClient;
yield browseridManager.initializeWithCurrentIdentity();
yield Assert.rejects(browseridManager.whenReadyToAuthenticate.promise,
"should reject due to an auth error");
do_check_true(signCertificateCalled);
do_check_true(accountStatusCalled);
do_check_false(browseridManager.account);
do_check_false(browseridManager._token);
do_check_false(browseridManager.hasValidToken());
do_check_false(browseridManager.account);
});
add_task(function* test_initialializeWithNoKeys() {
add_task(function test_initialializeWithNoKeys() {
_("Verify start after initializeWithCurrentIdentity without kA, kB or keyFetchToken");
let identityConfig = makeIdentityConfig();
delete identityConfig.fxaccount.user.kA;
@ -306,7 +252,7 @@ add_test(function test_RESTResourceAuthenticatorSkew() {
run_next_test();
});
add_task(function* test_ensureLoggedIn() {
add_task(function test_ensureLoggedIn() {
configureFxAccountIdentity(browseridManager);
yield browseridManager.initializeWithCurrentIdentity();
yield browseridManager.whenReadyToAuthenticate.promise;
@ -318,8 +264,8 @@ add_task(function* test_ensureLoggedIn() {
// arrange for no logged in user.
let fxa = browseridManager._fxaService
let signedInUser = fxa.internal.currentAccountState.storageManager.accountData;
fxa.internal.currentAccountState.storageManager.accountData = null;
let signedInUser = fxa.internal.currentAccountState.signedInUser;
fxa.internal.currentAccountState.signedInUser = null;
browseridManager.initializeWithCurrentIdentity();
Assert.ok(!browseridManager._shouldHaveSyncKeyBundle,
"_shouldHaveSyncKeyBundle should be false so we know we are testing what we think we are.");
@ -327,8 +273,7 @@ add_task(function* test_ensureLoggedIn() {
yield Assert.rejects(browseridManager.ensureLoggedIn(), "expecting rejection due to no user");
Assert.ok(browseridManager._shouldHaveSyncKeyBundle,
"_shouldHaveSyncKeyBundle should always be true after ensureLogin completes.");
// Restore the logged in user to what it was.
fxa.internal.currentAccountState.storageManager.accountData = signedInUser;
fxa.internal.currentAccountState.signedInUser = signedInUser;
Status.login = LOGIN_FAILED_LOGIN_REJECTED;
yield Assert.rejects(browseridManager.ensureLoggedIn(),
"LOGIN_FAILED_LOGIN_REJECTED should have caused immediate rejection");
@ -404,7 +349,7 @@ add_test(function test_computeXClientStateHeader() {
run_next_test();
});
add_task(function* test_getTokenErrors() {
add_task(function test_getTokenErrors() {
_("BrowserIDManager correctly handles various failures to get a token.");
_("Arrange for a 401 - Sync should reflect an auth error.");
@ -437,75 +382,7 @@ add_task(function* test_getTokenErrors() {
Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login state is LOGIN_FAILED_NETWORK_ERROR");
});
add_task(function* test_refreshCertificateOn401() {
_("BrowserIDManager refreshes the FXA certificate after a 401.");
var identityConfig = makeIdentityConfig();
var browseridManager = new BrowserIDManager();
// Use the real `_getAssertion` method that calls
// `mockFxAClient.signCertificate`.
let fxaInternal = makeFxAccountsInternalMock(identityConfig);
delete fxaInternal._getAssertion;
configureFxAccountIdentity(browseridManager, identityConfig, fxaInternal);
browseridManager._fxaService.internal.initialize();
let getCertCount = 0;
let MockFxAccountsClient = function() {
FxAccountsClient.apply(this);
};
MockFxAccountsClient.prototype = {
__proto__: FxAccountsClient.prototype,
signCertificate() {
++getCertCount;
}
};
let mockFxAClient = new MockFxAccountsClient();
browseridManager._fxaService.internal._fxAccountsClient = mockFxAClient;
let didReturn401 = false;
let didReturn200 = false;
let mockTSC = mockTokenServer(() => {
if (getCertCount <= 1) {
didReturn401 = true;
return {
status: 401,
headers: {"content-type": "application/json"},
body: JSON.stringify({}),
};
} else {
didReturn200 = true;
return {
status: 200,
headers: {"content-type": "application/json"},
body: JSON.stringify({
id: "id",
key: "key",
api_endpoint: "http://example.com/",
uid: "uid",
duration: 300,
})
};
}
});
browseridManager._tokenServerClient = mockTSC;
yield browseridManager.initializeWithCurrentIdentity();
yield browseridManager.whenReadyToAuthenticate.promise;
do_check_eq(getCertCount, 2);
do_check_true(didReturn401);
do_check_true(didReturn200);
do_check_true(browseridManager.account);
do_check_true(browseridManager._token);
do_check_true(browseridManager.hasValidToken());
do_check_true(browseridManager.account);
});
add_task(function* test_getTokenErrorWithRetry() {
add_task(function test_getTokenErrorWithRetry() {
_("tokenserver sends an observer notification on various backoff headers.");
// Set Sync's backoffInterval to zero - after we simulated the backoff header
@ -547,7 +424,7 @@ add_task(function* test_getTokenErrorWithRetry() {
Assert.ok(Status.backoffInterval >= 200000);
});
add_task(function* test_getKeysErrorWithBackoff() {
add_task(function test_getKeysErrorWithBackoff() {
_("Auth server (via hawk) sends an observer notification on backoff headers.");
// Set Sync's backoffInterval to zero - after we simulated the backoff header
@ -581,7 +458,7 @@ add_task(function* test_getKeysErrorWithBackoff() {
Assert.ok(Status.backoffInterval >= 100000);
});
add_task(function* test_getKeysErrorWithRetry() {
add_task(function test_getKeysErrorWithRetry() {
_("Auth server (via hawk) sends an observer notification on retry headers.");
// Set Sync's backoffInterval to zero - after we simulated the backoff header
@ -615,7 +492,7 @@ add_task(function* test_getKeysErrorWithRetry() {
Assert.ok(Status.backoffInterval >= 100000);
});
add_task(function* test_getHAWKErrors() {
add_task(function test_getHAWKErrors() {
_("BrowserIDManager correctly handles various HAWK failures.");
_("Arrange for a 401 - Sync should reflect an auth error.");
@ -648,7 +525,7 @@ add_task(function* test_getHAWKErrors() {
Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login state is LOGIN_FAILED_NETWORK_ERROR");
});
add_task(function* test_getGetKeysFailing401() {
add_task(function test_getGetKeysFailing401() {
_("BrowserIDManager correctly handles 401 responses fetching keys.");
_("Arrange for a 401 - Sync should reflect an auth error.");
@ -669,7 +546,7 @@ add_task(function* test_getGetKeysFailing401() {
Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected");
});
add_task(function* test_getGetKeysFailing503() {
add_task(function test_getGetKeysFailing503() {
_("BrowserIDManager correctly handles 5XX responses fetching keys.");
_("Arrange for a 503 - Sync should reflect a network error.");
@ -690,7 +567,7 @@ add_task(function* test_getGetKeysFailing503() {
Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "state reflects network error");
});
add_task(function* test_getKeysMissing() {
add_task(function test_getKeysMissing() {
_("BrowserIDManager correctly handles getKeys succeeding but not returning keys.");
let browseridManager = new BrowserIDManager();
@ -708,17 +585,7 @@ add_task(function* test_getKeysMissing() {
fetchAndUnwrapKeys: function () {
return Promise.resolve({});
},
fxAccountsClient: new MockFxAccountsClient(),
newAccountState(credentials) {
// We only expect this to be called with null indicating the (mock)
// storage should be read.
if (credentials) {
throw new Error("Not expecting to have credentials passed");
}
let storageManager = new MockFxaStorageManager();
storageManager.initialize(identityConfig.fxaccount.user);
return new AccountState(storageManager);
},
fxAccountsClient: new MockFxAccountsClient()
});
// Add a mock to the currentAccountState object.
@ -730,6 +597,9 @@ add_task(function* test_getKeysMissing() {
return Promise.resolve(this.cert.cert);
};
// Ensure the new FxAccounts mock has a signed-in user.
fxa.internal.currentAccountState.signedInUser = browseridManager._fxaService.internal.currentAccountState.signedInUser;
browseridManager._fxaService = fxa;
yield browseridManager.initializeWithCurrentIdentity();
@ -744,41 +614,6 @@ add_task(function* test_getKeysMissing() {
Assert.ok(ex.message.indexOf("missing kA or kB") >= 0);
});
add_task(function* test_signedInUserMissing() {
_("BrowserIDManager detects getSignedInUser returning incomplete account data");
let browseridManager = new BrowserIDManager();
let config = makeIdentityConfig();
// Delete stored keys and the key fetch token.
delete identityConfig.fxaccount.user.kA;
delete identityConfig.fxaccount.user.kB;
delete identityConfig.fxaccount.user.keyFetchToken;
configureFxAccountIdentity(browseridManager, identityConfig);
let fxa = new FxAccounts({
fetchAndUnwrapKeys: function () {
return Promise.resolve({});
},
fxAccountsClient: new MockFxAccountsClient(),
newAccountState(credentials) {
// We only expect this to be called with null indicating the (mock)
// storage should be read.
if (credentials) {
throw new Error("Not expecting to have credentials passed");
}
let storageManager = new MockFxaStorageManager();
storageManager.initialize(identityConfig.fxaccount.user);
return new AccountState(storageManager);
},
});
browseridManager._fxaService = fxa;
let status = yield browseridManager.unlockAndVerifyAuthState();
Assert.equal(status, LOGIN_FAILED_LOGIN_REJECTED);
});
// End of tests
// Utility functions follow
@ -803,17 +638,7 @@ function* initializeIdentityWithHAWKResponseFactory(config, cbGetResponse) {
callback.call(this);
},
get: function(callback) {
// Skip /status requests (browserid_identity checks if the account still
// exists after an auth error)
if (this._uri.startsWith("http://mockedserver:9999/account/status")) {
this.response = {
status: 200,
headers: {"content-type": "application/json"},
body: JSON.stringify({exists: true}),
};
} else {
this.response = cbGetResponse("get", null, this._uri, this._credentials, this._extra);
}
this.response = cbGetResponse("get", null, this._uri, this._credentials, this._extra);
callback.call(this);
}
}
@ -833,18 +658,11 @@ function* initializeIdentityWithHAWKResponseFactory(config, cbGetResponse) {
fxaClient.hawk = new MockedHawkClient();
let internal = {
fxAccountsClient: fxaClient,
newAccountState(credentials) {
// We only expect this to be called with null indicating the (mock)
// storage should be read.
if (credentials) {
throw new Error("Not expecting to have credentials passed");
}
let storageManager = new MockFxaStorageManager();
storageManager.initialize(config.fxaccount.user);
return new AccountState(storageManager);
},
}
let fxa = new FxAccounts(internal);
fxa.internal.currentAccountState.signedInUser = {
accountData: config.fxaccount.user,
};
browseridManager._fxaService = fxa;
browseridManager._signedInUser = null;
@ -862,29 +680,3 @@ function getTimestampDelta(hawkAuthHeader, now=Date.now()) {
return Math.abs(getTimestamp(hawkAuthHeader) - now);
}
function mockTokenServer(func) {
let requestLog = Log.repository.getLogger("testing.mock-rest");
if (!requestLog.appenders.length) { // might as well see what it says :)
requestLog.addAppender(new Log.DumpAppender());
requestLog.level = Log.Level.Trace;
}
function MockRESTRequest(url) {};
MockRESTRequest.prototype = {
_log: requestLog,
setHeader: function() {},
get: function(callback) {
this.response = func();
callback.call(this);
}
}
// The mocked TokenServer client which will get the response.
function MockTSC() { }
MockTSC.prototype = new TokenServerClient();
MockTSC.prototype.constructor = MockTSC;
MockTSC.prototype.newRESTRequest = function(url) {
return new MockRESTRequest(url);
}
// Arrange for the same observerPrefix as browserid_identity uses.
MockTSC.prototype.observerPrefix = "weave:service";
return new MockTSC();
}

File diff suppressed because it is too large Load diff

View file

@ -1,195 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
Cu.import("resource://services-sync/record.js");
Cu.import("resource://services-sync/service.js");
function run_test() {
initTestLogging("Trace");
Log.repository.getLogger("Sync.Collection").level = Log.Level.Trace;
run_next_test();
}
function recordRange(lim, offset, total) {
let res = [];
for (let i = offset; i < Math.min(lim + offset, total); ++i) {
res.push(JSON.stringify({ id: String(i), payload: "test:" + i }));
}
return res.join("\n") + "\n";
}
function get_test_collection_info({ totalRecords, batchSize, lastModified,
throwAfter = Infinity,
interruptedAfter = Infinity }) {
let coll = new Collection("http://example.com/test/", WBORecord, Service);
coll.full = true;
let requests = [];
let responses = [];
let sawRecord = false;
coll.get = function() {
ok(!sawRecord); // make sure we call record handler after all requests.
let limit = +this.limit;
let offset = 0;
if (this.offset) {
equal(this.offset.slice(0, 6), "foobar");
offset = +this.offset.slice(6);
}
requests.push({
limit,
offset,
spec: this.spec,
headers: Object.assign({}, this.headers)
});
if (--throwAfter === 0) {
throw "Some Network Error";
}
let body = recordRange(limit, offset, totalRecords);
this._onProgress.call({ _data: body });
let response = {
body,
success: true,
status: 200,
headers: {}
};
if (--interruptedAfter === 0) {
response.success = false;
response.status = 412;
response.body = "";
} else if (offset + limit < totalRecords) {
// Ensure we're treating this as an opaque string, since the docs say
// it might not be numeric.
response.headers["x-weave-next-offset"] = "foobar" + (offset + batchSize);
}
response.headers["x-last-modified"] = lastModified;
responses.push(response);
return response;
};
let records = [];
coll.recordHandler = function(record) {
sawRecord = true;
// ensure records are coming in in the right order
equal(record.id, String(records.length));
equal(record.payload, "test:" + records.length);
records.push(record);
};
return { records, responses, requests, coll };
}
add_test(function test_success() {
const totalRecords = 11;
const batchSize = 2;
const lastModified = "111111";
let { records, responses, requests, coll } = get_test_collection_info({
totalRecords,
batchSize,
lastModified,
});
let response = coll.getBatched(batchSize);
equal(requests.length, Math.ceil(totalRecords / batchSize));
// records are mostly checked in recordHandler, we just care about the length
equal(records.length, totalRecords);
// ensure we're returning the last response
equal(responses[responses.length - 1], response);
// check first separately since its a bit of a special case
ok(!requests[0].headers["x-if-unmodified-since"]);
ok(!requests[0].offset);
equal(requests[0].limit, batchSize);
let expectedOffset = 2;
for (let i = 1; i < requests.length; ++i) {
let req = requests[i];
equal(req.headers["x-if-unmodified-since"], lastModified);
equal(req.limit, batchSize);
if (i !== requests.length - 1) {
equal(req.offset, expectedOffset);
}
expectedOffset += batchSize;
}
// ensure we cleaned up anything that would break further
// use of this collection.
ok(!coll._headers["x-if-unmodified-since"]);
ok(!coll.offset);
ok(!coll.limit || (coll.limit == Infinity));
run_next_test();
});
add_test(function test_total_limit() {
_("getBatched respects the (initial) value of the limit property");
const totalRecords = 100;
const recordLimit = 11;
const batchSize = 2;
const lastModified = "111111";
let { records, responses, requests, coll } = get_test_collection_info({
totalRecords,
batchSize,
lastModified,
});
coll.limit = recordLimit;
let response = coll.getBatched(batchSize);
equal(requests.length, Math.ceil(recordLimit / batchSize));
equal(records.length, recordLimit);
for (let i = 0; i < requests.length; ++i) {
let req = requests[i];
if (i !== requests.length - 1) {
equal(req.limit, batchSize);
} else {
equal(req.limit, recordLimit % batchSize);
}
}
equal(coll._limit, recordLimit);
run_next_test();
});
add_test(function test_412() {
_("We shouldn't record records if we get a 412 in the middle of a batch");
const totalRecords = 11;
const batchSize = 2;
const lastModified = "111111";
let { records, responses, requests, coll } = get_test_collection_info({
totalRecords,
batchSize,
lastModified,
interruptedAfter: 3
});
let response = coll.getBatched(batchSize);
equal(requests.length, 3);
equal(records.length, 0); // record handler shouldn't be called for anything
// ensure we're returning the last response
equal(responses[responses.length - 1], response);
ok(!response.success);
equal(response.status, 412);
run_next_test();
});
add_test(function test_get_throws() {
_("We shouldn't record records if get() throws for some reason");
const totalRecords = 11;
const batchSize = 2;
const lastModified = "111111";
let { records, responses, requests, coll } = get_test_collection_info({
totalRecords,
batchSize,
lastModified,
throwAfter: 3
});
throws(() => coll.getBatched(batchSize), "Some Network Error");
equal(requests.length, 3);
equal(records.length, 0);
run_next_test();
});

View file

@ -6,7 +6,7 @@ Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://testing-common/services/sync/utils.js");
add_identity_test(this, function* test_missing_crypto_collection() {
add_identity_test(this, function test_missing_crypto_collection() {
let johnHelper = track_collections_helper();
let johnU = johnHelper.with_updated_collection;
let johnColls = johnHelper.collections;
@ -33,10 +33,7 @@ add_identity_test(this, function* test_missing_crypto_collection() {
};
let collections = ["clients", "bookmarks", "forms", "history",
"passwords", "prefs", "tabs"];
// Disable addon sync because AddonManager won't be initialized here.
Service.engineManager.unregister("addons");
for (let coll of collections) {
for each (let coll in collections) {
handlers["/1.1/johndoe/storage/" + coll] =
johnU(coll, new ServerCollection({}, true).handler());
}
@ -53,7 +50,7 @@ add_identity_test(this, function* test_missing_crypto_collection() {
};
_("Startup, no meta/global: freshStart called once.");
yield sync_and_validate_telem();
Service.sync();
do_check_eq(fresh, 1);
fresh = 0;
@ -63,12 +60,12 @@ add_identity_test(this, function* test_missing_crypto_collection() {
_("Simulate a bad info/collections.");
delete johnColls.crypto;
yield sync_and_validate_telem();
Service.sync();
do_check_eq(fresh, 1);
fresh = 0;
_("Regular sync: no need to freshStart.");
yield sync_and_validate_telem();
Service.sync();
do_check_eq(fresh, 0);
} finally {

View file

@ -14,7 +14,7 @@ Cu.import("resource://services-sync/util.js");
Cu.import("resource://testing-common/services/sync/utils.js");
Cu.import("resource://gre/modules/Promise.jsm");
add_task(function* test_locally_changed_keys() {
add_task(function test_locally_changed_keys() {
let passphrase = "abcdeabcdeabcdeabcdeabcdea";
let hmacErrorCount = 0;
@ -51,7 +51,7 @@ add_task(function* test_locally_changed_keys() {
}]}]};
delete Svc.Session;
Svc.Session = {
getBrowserState: () => JSON.stringify(myTabs)
getBrowserState: function () JSON.stringify(myTabs)
};
setBasicCredentials("johndoe", "password", passphrase);
@ -59,7 +59,6 @@ add_task(function* test_locally_changed_keys() {
Service.clusterURL = server.baseURI;
Service.engineManager.register(HistoryEngine);
Service.engineManager.unregister("addons");
function corrupt_local_keys() {
Service.collectionKeys._default.keyPair = [Svc.Crypto.generateRandomKey(),
@ -87,7 +86,7 @@ add_task(function* test_locally_changed_keys() {
do_check_true(Service.isLoggedIn);
// Sync should upload records.
yield sync_and_validate_telem();
Service.sync();
// Tabs exist.
_("Tabs modified: " + johndoe.modified("tabs"));
@ -140,9 +139,7 @@ add_task(function* test_locally_changed_keys() {
_("HMAC error count: " + hmacErrorCount);
// Now syncing should succeed, after one HMAC error.
let ping = yield wait_for_ping(() => Service.sync(), true);
equal(ping.engines.find(e => e.name == "history").incoming.applied, 5);
Service.sync();
do_check_eq(hmacErrorCount, 1);
_("Keys now: " + Service.collectionKeys.keyForCollection("history").keyPair);
@ -186,9 +183,7 @@ add_task(function* test_locally_changed_keys() {
Service.lastHMACEvent = 0;
_("Syncing...");
ping = yield sync_and_validate_telem(true);
do_check_eq(ping.engines.find(e => e.name == "history").incoming.failed, 5);
Service.sync();
_("Keys now: " + Service.collectionKeys.keyForCollection("history").keyPair);
_("Server keys have been updated, and we skipped over 5 more HMAC errors without adjusting history.");
do_check_true(johndoe.modified("crypto") > old_key_time);
@ -209,7 +204,6 @@ add_task(function* test_locally_changed_keys() {
function run_test() {
let logger = Log.repository.rootLogger;
Log.repository.rootLogger.addAppender(new Log.DumpAppender());
validate_all_future_pings();
ensureLegacyIdentityManager();

View file

@ -25,8 +25,8 @@ SteamTracker.prototype = {
__proto__: Tracker.prototype
};
function SteamEngine(name, service) {
Engine.call(this, name, service);
function SteamEngine(service) {
Engine.call(this, "Steam", service);
this.wasReset = false;
this.wasSynced = false;
}
@ -44,7 +44,7 @@ SteamEngine.prototype = {
}
};
var engineObserver = {
let engineObserver = {
topics: [],
observe: function(subject, topic, data) {
@ -69,7 +69,7 @@ function run_test() {
add_test(function test_members() {
_("Engine object members");
let engine = new SteamEngine("Steam", Service);
let engine = new SteamEngine(Service);
do_check_eq(engine.Name, "Steam");
do_check_eq(engine.prefName, "steam");
do_check_true(engine._store instanceof SteamStore);
@ -79,7 +79,7 @@ add_test(function test_members() {
add_test(function test_score() {
_("Engine.score corresponds to tracker.score and is readonly");
let engine = new SteamEngine("Steam", Service);
let engine = new SteamEngine(Service);
do_check_eq(engine.score, 0);
engine._tracker.score += 5;
do_check_eq(engine.score, 5);
@ -97,7 +97,7 @@ add_test(function test_score() {
add_test(function test_resetClient() {
_("Engine.resetClient calls _resetClient");
let engine = new SteamEngine("Steam", Service);
let engine = new SteamEngine(Service);
do_check_false(engine.wasReset);
engine.resetClient();
@ -112,7 +112,7 @@ add_test(function test_resetClient() {
add_test(function test_invalidChangedIDs() {
_("Test that invalid changed IDs on disk don't end up live.");
let engine = new SteamEngine("Steam", Service);
let engine = new SteamEngine(Service);
let tracker = engine._tracker;
tracker.changedIDs = 5;
tracker.saveChangedIDs(function onSaved() {
@ -127,7 +127,7 @@ add_test(function test_invalidChangedIDs() {
add_test(function test_wipeClient() {
_("Engine.wipeClient calls resetClient, wipes store, clears changed IDs");
let engine = new SteamEngine("Steam", Service);
let engine = new SteamEngine(Service);
do_check_false(engine.wasReset);
do_check_false(engine._store.wasWiped);
do_check_true(engine._tracker.addChangedID("a-changed-id"));
@ -150,7 +150,7 @@ add_test(function test_wipeClient() {
add_test(function test_enabled() {
_("Engine.enabled corresponds to preference");
let engine = new SteamEngine("Steam", Service);
let engine = new SteamEngine(Service);
try {
do_check_false(engine.enabled);
Svc.Prefs.set("engine.steam", true);
@ -165,18 +165,16 @@ add_test(function test_enabled() {
});
add_test(function test_sync() {
let engine = new SteamEngine("Steam", Service);
let engine = new SteamEngine(Service);
try {
_("Engine.sync doesn't call _sync if it's not enabled");
do_check_false(engine.enabled);
do_check_false(engine.wasSynced);
engine.sync();
do_check_false(engine.wasSynced);
_("Engine.sync calls _sync if it's enabled");
engine.enabled = true;
engine.sync();
do_check_true(engine.wasSynced);
do_check_eq(engineObserver.topics[0], "weave:engine:sync:start");
@ -191,7 +189,7 @@ add_test(function test_sync() {
add_test(function test_disabled_no_track() {
_("When an engine is disabled, its tracker is not tracking.");
let engine = new SteamEngine("Steam", Service);
let engine = new SteamEngine(Service);
let tracker = engine._tracker;
do_check_eq(engine, tracker.engine);

File diff suppressed because it is too large Load diff

View file

@ -1,913 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
Cu.import("resource://services-sync/engines/clients.js");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/keys.js");
Cu.import("resource://services-sync/policies.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/status.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://testing-common/services/sync/utils.js");
Cu.import("resource://gre/modules/FileUtils.jsm");
var fakeServer = new SyncServer();
fakeServer.start();
do_register_cleanup(function() {
return new Promise(resolve => {
fakeServer.stop(resolve);
});
});
var fakeServerUrl = "http://localhost:" + fakeServer.port;
const logsdir = FileUtils.getDir("ProfD", ["weave", "logs"], true);
const PROLONGED_ERROR_DURATION =
(Svc.Prefs.get('errorhandler.networkFailureReportTimeout') * 2) * 1000;
const NON_PROLONGED_ERROR_DURATION =
(Svc.Prefs.get('errorhandler.networkFailureReportTimeout') / 2) * 1000;
Service.engineManager.clear();
function setLastSync(lastSyncValue) {
Svc.Prefs.set("lastSync", (new Date(Date.now() - lastSyncValue)).toString());
}
var engineManager = Service.engineManager;
engineManager.register(EHTestsCommon.CatapultEngine);
// This relies on Service/ErrorHandler being a singleton. Fixing this will take
// a lot of work.
var errorHandler = Service.errorHandler;
function run_test() {
initTestLogging("Trace");
Log.repository.getLogger("Sync.Service").level = Log.Level.Trace;
Log.repository.getLogger("Sync.SyncScheduler").level = Log.Level.Trace;
Log.repository.getLogger("Sync.ErrorHandler").level = Log.Level.Trace;
ensureLegacyIdentityManager();
run_next_test();
}
function clean() {
Service.startOver();
Status.resetSync();
Status.resetBackoff();
errorHandler.didReportProlongedError = false;
}
add_identity_test(this, function* test_401_logout() {
let server = EHTestsCommon.sync_httpd_setup();
yield EHTestsCommon.setUp(server);
// By calling sync, we ensure we're logged in.
yield sync_and_validate_telem();
do_check_eq(Status.sync, SYNC_SUCCEEDED);
do_check_true(Service.isLoggedIn);
let deferred = Promise.defer();
Svc.Obs.add("weave:service:sync:error", onSyncError);
function onSyncError() {
_("Got weave:service:sync:error in first sync.");
Svc.Obs.remove("weave:service:sync:error", onSyncError);
// Wait for the automatic next sync.
function onLoginError() {
_("Got weave:service:login:error in second sync.");
Svc.Obs.remove("weave:service:login:error", onLoginError);
let expected = isConfiguredWithLegacyIdentity() ?
LOGIN_FAILED_LOGIN_REJECTED : LOGIN_FAILED_NETWORK_ERROR;
do_check_eq(Status.login, expected);
do_check_false(Service.isLoggedIn);
// Clean up.
Utils.nextTick(function () {
Service.startOver();
server.stop(deferred.resolve);
});
}
Svc.Obs.add("weave:service:login:error", onLoginError);
}
// Make sync fail due to login rejected.
yield configureIdentity({username: "janedoe"});
Service._updateCachedURLs();
_("Starting first sync.");
let ping = yield sync_and_validate_telem(true);
deepEqual(ping.failureReason, { name: "httperror", code: 401 });
_("First sync done.");
yield deferred.promise;
});
add_identity_test(this, function* test_credentials_changed_logout() {
let server = EHTestsCommon.sync_httpd_setup();
yield EHTestsCommon.setUp(server);
// By calling sync, we ensure we're logged in.
yield sync_and_validate_telem();
do_check_eq(Status.sync, SYNC_SUCCEEDED);
do_check_true(Service.isLoggedIn);
EHTestsCommon.generateCredentialsChangedFailure();
let ping = yield sync_and_validate_telem(true);
equal(ping.status.sync, CREDENTIALS_CHANGED);
deepEqual(ping.failureReason, {
name: "unexpectederror",
error: "Error: Aborting sync, remote setup failed"
});
do_check_eq(Status.sync, CREDENTIALS_CHANGED);
do_check_false(Service.isLoggedIn);
// Clean up.
Service.startOver();
let deferred = Promise.defer();
server.stop(deferred.resolve);
yield deferred.promise;
});
add_identity_test(this, function test_no_lastSync_pref() {
// Test reported error.
Status.resetSync();
errorHandler.dontIgnoreErrors = true;
Status.sync = CREDENTIALS_CHANGED;
do_check_true(errorHandler.shouldReportError());
// Test unreported error.
Status.resetSync();
errorHandler.dontIgnoreErrors = true;
Status.login = LOGIN_FAILED_NETWORK_ERROR;
do_check_true(errorHandler.shouldReportError());
});
add_identity_test(this, function test_shouldReportError() {
Status.login = MASTER_PASSWORD_LOCKED;
do_check_false(errorHandler.shouldReportError());
// Give ourselves a clusterURL so that the temporary 401 no-error situation
// doesn't come into play.
Service.serverURL = fakeServerUrl;
Service.clusterURL = fakeServerUrl;
// Test dontIgnoreErrors, non-network, non-prolonged, login error reported
Status.resetSync();
setLastSync(NON_PROLONGED_ERROR_DURATION);
errorHandler.dontIgnoreErrors = true;
Status.login = LOGIN_FAILED_NO_PASSWORD;
do_check_true(errorHandler.shouldReportError());
// Test dontIgnoreErrors, non-network, non-prolonged, sync error reported
Status.resetSync();
setLastSync(NON_PROLONGED_ERROR_DURATION);
errorHandler.dontIgnoreErrors = true;
Status.sync = CREDENTIALS_CHANGED;
do_check_true(errorHandler.shouldReportError());
// Test dontIgnoreErrors, non-network, prolonged, login error reported
Status.resetSync();
setLastSync(PROLONGED_ERROR_DURATION);
errorHandler.dontIgnoreErrors = true;
Status.login = LOGIN_FAILED_NO_PASSWORD;
do_check_true(errorHandler.shouldReportError());
// Test dontIgnoreErrors, non-network, prolonged, sync error reported
Status.resetSync();
setLastSync(PROLONGED_ERROR_DURATION);
errorHandler.dontIgnoreErrors = true;
Status.sync = CREDENTIALS_CHANGED;
do_check_true(errorHandler.shouldReportError());
// Test dontIgnoreErrors, network, non-prolonged, login error reported
Status.resetSync();
setLastSync(NON_PROLONGED_ERROR_DURATION);
errorHandler.dontIgnoreErrors = true;
Status.login = LOGIN_FAILED_NETWORK_ERROR;
do_check_true(errorHandler.shouldReportError());
// Test dontIgnoreErrors, network, non-prolonged, sync error reported
Status.resetSync();
setLastSync(NON_PROLONGED_ERROR_DURATION);
errorHandler.dontIgnoreErrors = true;
Status.sync = LOGIN_FAILED_NETWORK_ERROR;
do_check_true(errorHandler.shouldReportError());
// Test dontIgnoreErrors, network, prolonged, login error reported
Status.resetSync();
setLastSync(PROLONGED_ERROR_DURATION);
errorHandler.dontIgnoreErrors = true;
Status.login = LOGIN_FAILED_NETWORK_ERROR;
do_check_true(errorHandler.shouldReportError());
// Test dontIgnoreErrors, network, prolonged, sync error reported
Status.resetSync();
setLastSync(PROLONGED_ERROR_DURATION);
errorHandler.dontIgnoreErrors = true;
Status.sync = LOGIN_FAILED_NETWORK_ERROR;
do_check_true(errorHandler.shouldReportError());
// Test non-network, prolonged, login error reported
do_check_false(errorHandler.didReportProlongedError);
Status.resetSync();
setLastSync(PROLONGED_ERROR_DURATION);
errorHandler.dontIgnoreErrors = false;
Status.login = LOGIN_FAILED_NO_PASSWORD;
do_check_true(errorHandler.shouldReportError());
do_check_true(errorHandler.didReportProlongedError);
// Second time with prolonged error and without resetting
// didReportProlongedError, sync error should not be reported.
Status.resetSync();
setLastSync(PROLONGED_ERROR_DURATION);
errorHandler.dontIgnoreErrors = false;
Status.login = LOGIN_FAILED_NO_PASSWORD;
do_check_false(errorHandler.shouldReportError());
do_check_true(errorHandler.didReportProlongedError);
// Test non-network, prolonged, sync error reported
Status.resetSync();
setLastSync(PROLONGED_ERROR_DURATION);
errorHandler.dontIgnoreErrors = false;
errorHandler.didReportProlongedError = false;
Status.sync = CREDENTIALS_CHANGED;
do_check_true(errorHandler.shouldReportError());
do_check_true(errorHandler.didReportProlongedError);
errorHandler.didReportProlongedError = false;
// Test network, prolonged, login error reported
Status.resetSync();
setLastSync(PROLONGED_ERROR_DURATION);
errorHandler.dontIgnoreErrors = false;
Status.login = LOGIN_FAILED_NETWORK_ERROR;
do_check_true(errorHandler.shouldReportError());
do_check_true(errorHandler.didReportProlongedError);
errorHandler.didReportProlongedError = false;
// Test network, prolonged, sync error reported
Status.resetSync();
setLastSync(PROLONGED_ERROR_DURATION);
errorHandler.dontIgnoreErrors = false;
Status.sync = LOGIN_FAILED_NETWORK_ERROR;
do_check_true(errorHandler.shouldReportError());
do_check_true(errorHandler.didReportProlongedError);
errorHandler.didReportProlongedError = false;
// Test non-network, non-prolonged, login error reported
Status.resetSync();
setLastSync(NON_PROLONGED_ERROR_DURATION);
errorHandler.dontIgnoreErrors = false;
Status.login = LOGIN_FAILED_NO_PASSWORD;
do_check_true(errorHandler.shouldReportError());
do_check_false(errorHandler.didReportProlongedError);
// Test non-network, non-prolonged, sync error reported
Status.resetSync();
setLastSync(NON_PROLONGED_ERROR_DURATION);
errorHandler.dontIgnoreErrors = false;
Status.sync = CREDENTIALS_CHANGED;
do_check_true(errorHandler.shouldReportError());
do_check_false(errorHandler.didReportProlongedError);
// Test network, non-prolonged, login error reported
Status.resetSync();
setLastSync(NON_PROLONGED_ERROR_DURATION);
errorHandler.dontIgnoreErrors = false;
Status.login = LOGIN_FAILED_NETWORK_ERROR;
do_check_false(errorHandler.shouldReportError());
do_check_false(errorHandler.didReportProlongedError);
// Test network, non-prolonged, sync error reported
Status.resetSync();
setLastSync(NON_PROLONGED_ERROR_DURATION);
errorHandler.dontIgnoreErrors = false;
Status.sync = LOGIN_FAILED_NETWORK_ERROR;
do_check_false(errorHandler.shouldReportError());
do_check_false(errorHandler.didReportProlongedError);
// Test server maintenance, sync errors are not reported
Status.resetSync();
setLastSync(NON_PROLONGED_ERROR_DURATION);
errorHandler.dontIgnoreErrors = false;
Status.sync = SERVER_MAINTENANCE;
do_check_false(errorHandler.shouldReportError());
do_check_false(errorHandler.didReportProlongedError);
// Test server maintenance, login errors are not reported
Status.resetSync();
setLastSync(NON_PROLONGED_ERROR_DURATION);
errorHandler.dontIgnoreErrors = false;
Status.login = SERVER_MAINTENANCE;
do_check_false(errorHandler.shouldReportError());
do_check_false(errorHandler.didReportProlongedError);
// Test prolonged, server maintenance, sync errors are reported
Status.resetSync();
setLastSync(PROLONGED_ERROR_DURATION);
errorHandler.dontIgnoreErrors = false;
Status.sync = SERVER_MAINTENANCE;
do_check_true(errorHandler.shouldReportError());
do_check_true(errorHandler.didReportProlongedError);
errorHandler.didReportProlongedError = false;
// Test prolonged, server maintenance, login errors are reported
Status.resetSync();
setLastSync(PROLONGED_ERROR_DURATION);
errorHandler.dontIgnoreErrors = false;
Status.login = SERVER_MAINTENANCE;
do_check_true(errorHandler.shouldReportError());
do_check_true(errorHandler.didReportProlongedError);
errorHandler.didReportProlongedError = false;
// Test dontIgnoreErrors, server maintenance, sync errors are reported
Status.resetSync();
setLastSync(NON_PROLONGED_ERROR_DURATION);
errorHandler.dontIgnoreErrors = true;
Status.sync = SERVER_MAINTENANCE;
do_check_true(errorHandler.shouldReportError());
// dontIgnoreErrors means we don't set didReportProlongedError
do_check_false(errorHandler.didReportProlongedError);
// Test dontIgnoreErrors, server maintenance, login errors are reported
Status.resetSync();
setLastSync(NON_PROLONGED_ERROR_DURATION);
errorHandler.dontIgnoreErrors = true;
Status.login = SERVER_MAINTENANCE;
do_check_true(errorHandler.shouldReportError());
do_check_false(errorHandler.didReportProlongedError);
// Test dontIgnoreErrors, prolonged, server maintenance,
// sync errors are reported
Status.resetSync();
setLastSync(PROLONGED_ERROR_DURATION);
errorHandler.dontIgnoreErrors = true;
Status.sync = SERVER_MAINTENANCE;
do_check_true(errorHandler.shouldReportError());
do_check_false(errorHandler.didReportProlongedError);
// Test dontIgnoreErrors, prolonged, server maintenance,
// login errors are reported
Status.resetSync();
setLastSync(PROLONGED_ERROR_DURATION);
errorHandler.dontIgnoreErrors = true;
Status.login = SERVER_MAINTENANCE;
do_check_true(errorHandler.shouldReportError());
do_check_false(errorHandler.didReportProlongedError);
});
add_identity_test(this, function* test_shouldReportError_master_password() {
_("Test error ignored due to locked master password");
let server = EHTestsCommon.sync_httpd_setup();
yield EHTestsCommon.setUp(server);
// Monkey patch Service.verifyLogin to imitate
// master password being locked.
Service._verifyLogin = Service.verifyLogin;
Service.verifyLogin = function () {
Status.login = MASTER_PASSWORD_LOCKED;
return false;
};
setLastSync(NON_PROLONGED_ERROR_DURATION);
Service.sync();
do_check_false(errorHandler.shouldReportError());
// Clean up.
Service.verifyLogin = Service._verifyLogin;
clean();
let deferred = Promise.defer();
server.stop(deferred.resolve);
yield deferred.promise;
});
// Test that even if we don't have a cluster URL, a login failure due to
// authentication errors is always reported.
add_identity_test(this, function test_shouldReportLoginFailureWithNoCluster() {
// Ensure no clusterURL - any error not specific to login should not be reported.
Service.serverURL = "";
Service.clusterURL = "";
// Test explicit "login rejected" state.
Status.resetSync();
// If we have a LOGIN_REJECTED state, we always report the error.
Status.login = LOGIN_FAILED_LOGIN_REJECTED;
do_check_true(errorHandler.shouldReportError());
// But any other status with a missing clusterURL is treated as a mid-sync
// 401 (ie, should be treated as a node reassignment)
Status.login = LOGIN_SUCCEEDED;
do_check_false(errorHandler.shouldReportError());
});
// XXX - how to arrange for 'Service.identity.basicPassword = null;' in
// an fxaccounts environment?
add_task(function* test_login_syncAndReportErrors_non_network_error() {
// Test non-network errors are reported
// when calling syncAndReportErrors
let server = EHTestsCommon.sync_httpd_setup();
yield EHTestsCommon.setUp(server);
Service.identity.basicPassword = null;
let deferred = Promise.defer();
Svc.Obs.add("weave:ui:login:error", function onSyncError() {
Svc.Obs.remove("weave:ui:login:error", onSyncError);
do_check_eq(Status.login, LOGIN_FAILED_NO_PASSWORD);
clean();
server.stop(deferred.resolve);
});
setLastSync(NON_PROLONGED_ERROR_DURATION);
errorHandler.syncAndReportErrors();
yield deferred.promise;
});
add_identity_test(this, function* test_sync_syncAndReportErrors_non_network_error() {
// Test non-network errors are reported
// when calling syncAndReportErrors
let server = EHTestsCommon.sync_httpd_setup();
yield EHTestsCommon.setUp(server);
// By calling sync, we ensure we're logged in.
Service.sync();
do_check_eq(Status.sync, SYNC_SUCCEEDED);
do_check_true(Service.isLoggedIn);
EHTestsCommon.generateCredentialsChangedFailure();
let deferred = Promise.defer();
Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
Svc.Obs.remove("weave:ui:sync:error", onSyncError);
do_check_eq(Status.sync, CREDENTIALS_CHANGED);
// If we clean this tick, telemetry won't get the right error
server.stop(() => {
clean();
deferred.resolve();
});
});
setLastSync(NON_PROLONGED_ERROR_DURATION);
let ping = yield wait_for_ping(() => errorHandler.syncAndReportErrors(), true);
equal(ping.status.sync, CREDENTIALS_CHANGED);
deepEqual(ping.failureReason, {
name: "unexpectederror",
error: "Error: Aborting sync, remote setup failed"
});
yield deferred.promise;
});
// XXX - how to arrange for 'Service.identity.basicPassword = null;' in
// an fxaccounts environment?
add_task(function* test_login_syncAndReportErrors_prolonged_non_network_error() {
// Test prolonged, non-network errors are
// reported when calling syncAndReportErrors.
let server = EHTestsCommon.sync_httpd_setup();
yield EHTestsCommon.setUp(server);
Service.identity.basicPassword = null;
let deferred = Promise.defer();
Svc.Obs.add("weave:ui:login:error", function onSyncError() {
Svc.Obs.remove("weave:ui:login:error", onSyncError);
do_check_eq(Status.login, LOGIN_FAILED_NO_PASSWORD);
clean();
server.stop(deferred.resolve);
});
setLastSync(PROLONGED_ERROR_DURATION);
errorHandler.syncAndReportErrors();
yield deferred.promise;
});
add_identity_test(this, function* test_sync_syncAndReportErrors_prolonged_non_network_error() {
// Test prolonged, non-network errors are
// reported when calling syncAndReportErrors.
let server = EHTestsCommon.sync_httpd_setup();
yield EHTestsCommon.setUp(server);
// By calling sync, we ensure we're logged in.
Service.sync();
do_check_eq(Status.sync, SYNC_SUCCEEDED);
do_check_true(Service.isLoggedIn);
EHTestsCommon.generateCredentialsChangedFailure();
let deferred = Promise.defer();
Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
Svc.Obs.remove("weave:ui:sync:error", onSyncError);
do_check_eq(Status.sync, CREDENTIALS_CHANGED);
// If we clean this tick, telemetry won't get the right error
server.stop(() => {
clean();
deferred.resolve();
});
});
setLastSync(PROLONGED_ERROR_DURATION);
let ping = yield wait_for_ping(() => errorHandler.syncAndReportErrors(), true);
equal(ping.status.sync, CREDENTIALS_CHANGED);
deepEqual(ping.failureReason, {
name: "unexpectederror",
error: "Error: Aborting sync, remote setup failed"
});
yield deferred.promise;
});
add_identity_test(this, function* test_login_syncAndReportErrors_network_error() {
// Test network errors are reported when calling syncAndReportErrors.
yield configureIdentity({username: "broken.wipe"});
Service.serverURL = fakeServerUrl;
Service.clusterURL = fakeServerUrl;
let deferred = Promise.defer();
Svc.Obs.add("weave:ui:login:error", function onSyncError() {
Svc.Obs.remove("weave:ui:login:error", onSyncError);
do_check_eq(Status.login, LOGIN_FAILED_NETWORK_ERROR);
clean();
deferred.resolve();
});
setLastSync(NON_PROLONGED_ERROR_DURATION);
errorHandler.syncAndReportErrors();
yield deferred.promise;
});
add_test(function test_sync_syncAndReportErrors_network_error() {
// Test network errors are reported when calling syncAndReportErrors.
Services.io.offline = true;
Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
Svc.Obs.remove("weave:ui:sync:error", onSyncError);
do_check_eq(Status.sync, LOGIN_FAILED_NETWORK_ERROR);
Services.io.offline = false;
clean();
run_next_test();
});
setLastSync(NON_PROLONGED_ERROR_DURATION);
errorHandler.syncAndReportErrors();
});
add_identity_test(this, function* test_login_syncAndReportErrors_prolonged_network_error() {
// Test prolonged, network errors are reported
// when calling syncAndReportErrors.
yield configureIdentity({username: "johndoe"});
Service.serverURL = fakeServerUrl;
Service.clusterURL = fakeServerUrl;
let deferred = Promise.defer();
Svc.Obs.add("weave:ui:login:error", function onSyncError() {
Svc.Obs.remove("weave:ui:login:error", onSyncError);
do_check_eq(Status.login, LOGIN_FAILED_NETWORK_ERROR);
clean();
deferred.resolve();
});
setLastSync(PROLONGED_ERROR_DURATION);
errorHandler.syncAndReportErrors();
yield deferred.promise;
});
add_test(function test_sync_syncAndReportErrors_prolonged_network_error() {
// Test prolonged, network errors are reported
// when calling syncAndReportErrors.
Services.io.offline = true;
Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
Svc.Obs.remove("weave:ui:sync:error", onSyncError);
do_check_eq(Status.sync, LOGIN_FAILED_NETWORK_ERROR);
Services.io.offline = false;
clean();
run_next_test();
});
setLastSync(PROLONGED_ERROR_DURATION);
errorHandler.syncAndReportErrors();
});
add_task(function* test_login_prolonged_non_network_error() {
// Test prolonged, non-network errors are reported
let server = EHTestsCommon.sync_httpd_setup();
yield EHTestsCommon.setUp(server);
Service.identity.basicPassword = null;
let deferred = Promise.defer();
Svc.Obs.add("weave:ui:login:error", function onSyncError() {
Svc.Obs.remove("weave:ui:login:error", onSyncError);
do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
do_check_true(errorHandler.didReportProlongedError);
clean();
server.stop(deferred.resolve);
});
setLastSync(PROLONGED_ERROR_DURATION);
Service.sync();
yield deferred.promise;
});
add_task(function* test_sync_prolonged_non_network_error() {
// Test prolonged, non-network errors are reported
let server = EHTestsCommon.sync_httpd_setup();
yield EHTestsCommon.setUp(server);
// By calling sync, we ensure we're logged in.
Service.sync();
do_check_eq(Status.sync, SYNC_SUCCEEDED);
do_check_true(Service.isLoggedIn);
EHTestsCommon.generateCredentialsChangedFailure();
let deferred = Promise.defer();
Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
Svc.Obs.remove("weave:ui:sync:error", onSyncError);
do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
do_check_true(errorHandler.didReportProlongedError);
server.stop(() => {
clean();
deferred.resolve();
});
});
setLastSync(PROLONGED_ERROR_DURATION);
let ping = yield sync_and_validate_telem(true);
equal(ping.status.sync, PROLONGED_SYNC_FAILURE);
deepEqual(ping.failureReason, {
name: "unexpectederror",
error: "Error: Aborting sync, remote setup failed"
});
yield deferred.promise;
});
add_identity_test(this, function* test_login_prolonged_network_error() {
// Test prolonged, network errors are reported
yield configureIdentity({username: "johndoe"});
Service.serverURL = fakeServerUrl;
Service.clusterURL = fakeServerUrl;
let deferred = Promise.defer();
Svc.Obs.add("weave:ui:login:error", function onSyncError() {
Svc.Obs.remove("weave:ui:login:error", onSyncError);
do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
do_check_true(errorHandler.didReportProlongedError);
clean();
deferred.resolve();
});
setLastSync(PROLONGED_ERROR_DURATION);
Service.sync();
yield deferred.promise;
});
add_test(function test_sync_prolonged_network_error() {
// Test prolonged, network errors are reported
Services.io.offline = true;
Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
Svc.Obs.remove("weave:ui:sync:error", onSyncError);
do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
do_check_true(errorHandler.didReportProlongedError);
Services.io.offline = false;
clean();
run_next_test();
});
setLastSync(PROLONGED_ERROR_DURATION);
Service.sync();
});
add_task(function* test_login_non_network_error() {
// Test non-network errors are reported
let server = EHTestsCommon.sync_httpd_setup();
yield EHTestsCommon.setUp(server);
Service.identity.basicPassword = null;
let deferred = Promise.defer();
Svc.Obs.add("weave:ui:login:error", function onSyncError() {
Svc.Obs.remove("weave:ui:login:error", onSyncError);
do_check_eq(Status.login, LOGIN_FAILED_NO_PASSWORD);
do_check_false(errorHandler.didReportProlongedError);
clean();
server.stop(deferred.resolve);
});
setLastSync(NON_PROLONGED_ERROR_DURATION);
Service.sync();
yield deferred.promise;
});
add_task(function* test_sync_non_network_error() {
// Test non-network errors are reported
let server = EHTestsCommon.sync_httpd_setup();
yield EHTestsCommon.setUp(server);
// By calling sync, we ensure we're logged in.
Service.sync();
do_check_eq(Status.sync, SYNC_SUCCEEDED);
do_check_true(Service.isLoggedIn);
EHTestsCommon.generateCredentialsChangedFailure();
let deferred = Promise.defer();
Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
Svc.Obs.remove("weave:ui:sync:error", onSyncError);
do_check_eq(Status.sync, CREDENTIALS_CHANGED);
do_check_false(errorHandler.didReportProlongedError);
clean();
server.stop(deferred.resolve);
});
setLastSync(NON_PROLONGED_ERROR_DURATION);
Service.sync();
yield deferred.promise;
});
add_identity_test(this, function* test_login_network_error() {
yield configureIdentity({username: "johndoe"});
Service.serverURL = fakeServerUrl;
Service.clusterURL = fakeServerUrl;
let deferred = Promise.defer();
// Test network errors are not reported.
Svc.Obs.add("weave:ui:clear-error", function onClearError() {
Svc.Obs.remove("weave:ui:clear-error", onClearError);
do_check_eq(Status.login, LOGIN_FAILED_NETWORK_ERROR);
do_check_false(errorHandler.didReportProlongedError);
Services.io.offline = false;
clean();
deferred.resolve()
});
setLastSync(NON_PROLONGED_ERROR_DURATION);
Service.sync();
yield deferred.promise;
});
add_test(function test_sync_network_error() {
// Test network errors are not reported.
Services.io.offline = true;
Svc.Obs.add("weave:ui:sync:finish", function onUIUpdate() {
Svc.Obs.remove("weave:ui:sync:finish", onUIUpdate);
do_check_eq(Status.sync, LOGIN_FAILED_NETWORK_ERROR);
do_check_false(errorHandler.didReportProlongedError);
Services.io.offline = false;
clean();
run_next_test();
});
setLastSync(NON_PROLONGED_ERROR_DURATION);
Service.sync();
});
add_identity_test(this, function* test_sync_server_maintenance_error() {
// Test server maintenance errors are not reported.
let server = EHTestsCommon.sync_httpd_setup();
yield EHTestsCommon.setUp(server);
const BACKOFF = 42;
let engine = engineManager.get("catapult");
engine.enabled = true;
engine.exception = {status: 503,
headers: {"retry-after": BACKOFF}};
function onSyncError() {
do_throw("Shouldn't get here!");
}
Svc.Obs.add("weave:ui:sync:error", onSyncError);
do_check_eq(Status.service, STATUS_OK);
let deferred = Promise.defer();
Svc.Obs.add("weave:ui:sync:finish", function onSyncFinish() {
Svc.Obs.remove("weave:ui:sync:finish", onSyncFinish);
do_check_eq(Status.service, SYNC_FAILED_PARTIAL);
do_check_eq(Status.sync, SERVER_MAINTENANCE);
do_check_false(errorHandler.didReportProlongedError);
Svc.Obs.remove("weave:ui:sync:error", onSyncError);
server.stop(() => {
clean();
deferred.resolve();
})
});
setLastSync(NON_PROLONGED_ERROR_DURATION);
let ping = yield sync_and_validate_telem(true);
equal(ping.status.sync, SERVER_MAINTENANCE);
deepEqual(ping.engines.find(e => e.failureReason).failureReason, { name: "httperror", code: 503 })
yield deferred.promise;
});
add_identity_test(this, function* test_info_collections_login_server_maintenance_error() {
// Test info/collections server maintenance errors are not reported.
let server = EHTestsCommon.sync_httpd_setup();
yield EHTestsCommon.setUp(server);
Service.username = "broken.info";
yield configureIdentity({username: "broken.info"});
Service.serverURL = server.baseURI + "/maintenance/";
Service.clusterURL = server.baseURI + "/maintenance/";
let backoffInterval;
Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
Svc.Obs.remove("weave:service:backoff:interval", observe);
backoffInterval = subject;
});
function onUIUpdate() {
do_throw("Shouldn't experience UI update!");
}
Svc.Obs.add("weave:ui:login:error", onUIUpdate);
do_check_false(Status.enforceBackoff);
do_check_eq(Status.service, STATUS_OK);
let deferred = Promise.defer();
Svc.Obs.add("weave:ui:clear-error", function onLoginFinish() {
Svc.Obs.remove("weave:ui:clear-error", onLoginFinish);
do_check_true(Status.enforceBackoff);
do_check_eq(backoffInterval, 42);
do_check_eq(Status.service, LOGIN_FAILED);
do_check_eq(Status.login, SERVER_MAINTENANCE);
do_check_false(errorHandler.didReportProlongedError);
Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
clean();
server.stop(deferred.resolve);
});
setLastSync(NON_PROLONGED_ERROR_DURATION);
Service.sync();
yield deferred.promise;
});
add_identity_test(this, function* test_meta_global_login_server_maintenance_error() {
// Test meta/global server maintenance errors are not reported.
let server = EHTestsCommon.sync_httpd_setup();
yield EHTestsCommon.setUp(server);
yield configureIdentity({username: "broken.meta"});
Service.serverURL = server.baseURI + "/maintenance/";
Service.clusterURL = server.baseURI + "/maintenance/";
let backoffInterval;
Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
Svc.Obs.remove("weave:service:backoff:interval", observe);
backoffInterval = subject;
});
function onUIUpdate() {
do_throw("Shouldn't get here!");
}
Svc.Obs.add("weave:ui:login:error", onUIUpdate);
do_check_false(Status.enforceBackoff);
do_check_eq(Status.service, STATUS_OK);
let deferred = Promise.defer();
Svc.Obs.add("weave:ui:clear-error", function onLoginFinish() {
Svc.Obs.remove("weave:ui:clear-error", onLoginFinish);
do_check_true(Status.enforceBackoff);
do_check_eq(backoffInterval, 42);
do_check_eq(Status.service, LOGIN_FAILED);
do_check_eq(Status.login, SERVER_MAINTENANCE);
do_check_false(errorHandler.didReportProlongedError);
Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
clean();
server.stop(deferred.resolve);
});
setLastSync(NON_PROLONGED_ERROR_DURATION);
Service.sync();
yield deferred.promise;
});

File diff suppressed because it is too large Load diff

View file

@ -43,7 +43,7 @@ function sync_httpd_setup(infoHandler) {
return httpd_setup(handlers);
}
function* setUp(server) {
function setUp(server) {
yield configureIdentity({username: "johndoe"});
Service.serverURL = server.baseURI + "/";
Service.clusterURL = server.baseURI + "/";
@ -66,7 +66,7 @@ function do_check_hard_eol(eh, start) {
do_check_true(Status.eol);
}
add_identity_test(this, function* test_200_hard() {
add_identity_test(this, function test_200_hard() {
let eh = Service.errorHandler;
let start = Date.now();
let server = sync_httpd_setup(handler200("hard-eol"));
@ -88,7 +88,7 @@ add_identity_test(this, function* test_200_hard() {
yield deferred.promise;
});
add_identity_test(this, function* test_513_hard() {
add_identity_test(this, function test_513_hard() {
let eh = Service.errorHandler;
let start = Date.now();
let server = sync_httpd_setup(handler513);
@ -114,7 +114,7 @@ add_identity_test(this, function* test_513_hard() {
yield deferred.promise;
});
add_identity_test(this, function* test_200_soft() {
add_identity_test(this, function test_200_soft() {
let eh = Service.errorHandler;
let start = Date.now();
let server = sync_httpd_setup(handler200("soft-eol"));

View file

@ -21,7 +21,7 @@ const DELAY_BUFFER = 500; // Buffer for timers on different OS platforms.
const PROLONGED_ERROR_DURATION =
(Svc.Prefs.get('errorhandler.networkFailureReportTimeout') * 2) * 1000;
var errorHandler = Service.errorHandler;
let errorHandler = Service.errorHandler;
function setLastSync(lastSyncValue) {
Svc.Prefs.set("lastSync", (new Date(Date.now() - lastSyncValue)).toString());
@ -35,8 +35,6 @@ function run_test() {
Log.repository.getLogger("Sync.SyncScheduler").level = Log.Level.Trace;
Log.repository.getLogger("Sync.ErrorHandler").level = Log.Level.Trace;
validate_all_future_pings();
run_next_test();
}
@ -47,22 +45,20 @@ add_test(function test_noOutput() {
// Clear log output from startup.
Svc.Prefs.set("log.appender.file.logOnSuccess", false);
Svc.Obs.notify("weave:service:sync:finish");
Svc.Obs.add("weave:service:reset-file-log", function onResetFileLogOuter() {
Svc.Obs.remove("weave:service:reset-file-log", onResetFileLogOuter);
// Clear again without having issued any output.
Svc.Prefs.set("log.appender.file.logOnSuccess", true);
Svc.Obs.add("weave:service:reset-file-log", function onResetFileLogInner() {
Svc.Obs.remove("weave:service:reset-file-log", onResetFileLogInner);
// Clear again without having issued any output.
Svc.Prefs.set("log.appender.file.logOnSuccess", true);
errorHandler._logManager._fileAppender.level = Log.Level.Trace;
Svc.Prefs.resetBranch("");
run_next_test();
});
Svc.Obs.add("weave:service:reset-file-log", function onResetFileLog() {
Svc.Obs.remove("weave:service:reset-file-log", onResetFileLog);
// Fake a successful sync.
Svc.Obs.notify("weave:service:sync:finish");
errorHandler._logManager._fileAppender.level = Log.Level.Trace;
Svc.Prefs.resetBranch("");
run_next_test();
});
// Fake a successful sync.
Svc.Obs.notify("weave:service:sync:finish");
});
add_test(function test_logOnSuccess_false() {
@ -85,14 +81,16 @@ add_test(function test_logOnSuccess_false() {
});
function readFile(file, callback) {
NetUtil.asyncFetch({
uri: NetUtil.newURI(file),
loadUsingSystemPrincipal: true
}, function (inputStream, statusCode, request) {
NetUtil.asyncFetch2(file, function (inputStream, statusCode, request) {
let data = NetUtil.readInputStreamToString(inputStream,
inputStream.available());
callback(statusCode, data);
});
},
null, // aLoadingNode
Services.scriptSecurityManager.getSystemPrincipal(),
null, // aTriggeringPrincipal
Ci.nsILoadInfo.SEC_NORMAL,
Ci.nsIContentPolicy.TYPE_OTHER);
}
add_test(function test_logOnSuccess_true() {
@ -269,51 +267,6 @@ add_test(function test_login_error_logOnError_true() {
Svc.Obs.notify("weave:service:login:error");
});
add_test(function test_errorLog_dumpAddons() {
Svc.Prefs.set("log.appender.file.logOnError", true);
let log = Log.repository.getLogger("Sync.Test.FileLog");
// We need to wait until the log cleanup started by this test is complete
// or the next test will fail as it is ongoing.
Svc.Obs.add("services-tests:common:log-manager:cleanup-logs", function onCleanupLogs() {
Svc.Obs.remove("services-tests:common:log-manager:cleanup-logs", onCleanupLogs);
run_next_test();
});
Svc.Obs.add("weave:service:reset-file-log", function onResetFileLog() {
Svc.Obs.remove("weave:service:reset-file-log", onResetFileLog);
let entries = logsdir.directoryEntries;
do_check_true(entries.hasMoreElements());
let logfile = entries.getNext().QueryInterface(Ci.nsILocalFile);
do_check_eq(logfile.leafName.slice(-4), ".txt");
do_check_true(logfile.leafName.startsWith("error-sync-"), logfile.leafName);
do_check_false(entries.hasMoreElements());
// Ensure we logged some addon list (which is probably empty)
readFile(logfile, function (error, data) {
do_check_true(Components.isSuccessCode(error));
do_check_neq(data.indexOf("Addons installed"), -1);
// Clean up.
try {
logfile.remove(false);
} catch(ex) {
dump("Couldn't delete file: " + ex + "\n");
// Stupid Windows box.
}
Svc.Prefs.resetBranch("");
});
});
// Fake an unsuccessful sync due to prolonged failure.
setLastSync(PROLONGED_ERROR_DURATION);
Svc.Obs.notify("weave:service:sync:error");
});
// Check that error log files are deleted above an age threshold.
add_test(function test_logErrorCleanup_age() {
_("Beginning test_logErrorCleanup_age.");

View file

@ -13,7 +13,7 @@ Cu.import("resource://testing-common/services/sync/utils.js");
initTestLogging("Trace");
var engineManager = Service.engineManager;
let engineManager = Service.engineManager;
engineManager.clear();
function promiseStopServer(server) {
@ -59,7 +59,7 @@ function sync_httpd_setup() {
return httpd_setup(handlers);
}
function* setUp(server) {
function setUp(server) {
yield configureIdentity({username: "johndoe"});
Service.serverURL = server.baseURI + "/";
Service.clusterURL = server.baseURI + "/";
@ -75,7 +75,7 @@ function generateAndUploadKeys(server) {
}
add_identity_test(this, function* test_backoff500() {
add_identity_test(this, function test_backoff500() {
_("Test: HTTP 500 sets backoff status.");
let server = sync_httpd_setup();
yield setUp(server);
@ -102,7 +102,7 @@ add_identity_test(this, function* test_backoff500() {
yield promiseStopServer(server);
});
add_identity_test(this, function* test_backoff503() {
add_identity_test(this, function test_backoff503() {
_("Test: HTTP 503 with Retry-After header leads to backoff notification and sets backoff status.");
let server = sync_httpd_setup();
yield setUp(server);
@ -138,7 +138,7 @@ add_identity_test(this, function* test_backoff503() {
yield promiseStopServer(server);
});
add_identity_test(this, function* test_overQuota() {
add_identity_test(this, function test_overQuota() {
_("Test: HTTP 400 with body error code 14 means over quota.");
let server = sync_httpd_setup();
yield setUp(server);
@ -167,7 +167,7 @@ add_identity_test(this, function* test_overQuota() {
yield promiseStopServer(server);
});
add_identity_test(this, function* test_service_networkError() {
add_identity_test(this, function test_service_networkError() {
_("Test: Connection refused error from Service.sync() leads to the right status code.");
let server = sync_httpd_setup();
yield setUp(server);
@ -193,14 +193,13 @@ add_identity_test(this, function* test_service_networkError() {
yield deferred.promise;
});
add_identity_test(this, function* test_service_offline() {
add_identity_test(this, function test_service_offline() {
_("Test: Wanting to sync in offline mode leads to the right status code but does not increment the ignorable error count.");
let server = sync_httpd_setup();
yield setUp(server);
let deferred = Promise.defer();
server.stop(() => {
Services.io.offline = true;
Services.prefs.setBoolPref("network.dns.offline-localhost", false);
try {
do_check_eq(Status.sync, SYNC_SUCCEEDED);
@ -215,13 +214,12 @@ add_identity_test(this, function* test_service_offline() {
Service.startOver();
}
Services.io.offline = false;
Services.prefs.clearUserPref("network.dns.offline-localhost");
deferred.resolve();
});
yield deferred.promise;
});
add_identity_test(this, function* test_engine_networkError() {
add_identity_test(this, function test_engine_networkError() {
_("Test: Network related exceptions from engine.sync() lead to the right status code.");
let server = sync_httpd_setup();
yield setUp(server);
@ -248,7 +246,7 @@ add_identity_test(this, function* test_engine_networkError() {
yield promiseStopServer(server);
});
add_identity_test(this, function* test_resource_timeout() {
add_identity_test(this, function test_resource_timeout() {
let server = sync_httpd_setup();
yield setUp(server);
@ -276,7 +274,6 @@ add_identity_test(this, function* test_resource_timeout() {
});
function run_test() {
validate_all_future_pings();
engineManager.register(CatapultEngine);
run_next_test();
}

View file

@ -1,93 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
Cu.import("resource://services-crypto/utils.js");
Cu.import("resource://services-sync/engines/extension-storage.js");
Cu.import("resource://services-sync/util.js");
/**
* Like Assert.throws, but for generators.
*
* @param {string | Object | function} constraint
* What to use to check the exception.
* @param {function} f
* The function to call.
*/
function* throwsGen(constraint, f) {
let threw = false;
let exception;
try {
yield* f();
}
catch (e) {
threw = true;
exception = e;
}
ok(threw, "did not throw an exception");
const debuggingMessage = `got ${exception}, expected ${constraint}`;
let message = exception;
if (typeof exception === "object") {
message = exception.message;
}
if (typeof constraint === "function") {
ok(constraint(message), debuggingMessage);
} else {
ok(constraint === message, debuggingMessage);
}
}
/**
* An EncryptionRemoteTransformer that uses a fixed key bundle,
* suitable for testing.
*/
class StaticKeyEncryptionRemoteTransformer extends EncryptionRemoteTransformer {
constructor(keyBundle) {
super();
this.keyBundle = keyBundle;
}
getKeys() {
return Promise.resolve(this.keyBundle);
}
}
const BORING_KB = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
const STRETCHED_KEY = CryptoUtils.hkdf(BORING_KB, undefined, `testing storage.sync encryption`, 2*32);
const KEY_BUNDLE = {
sha256HMACHasher: Utils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256, Utils.makeHMACKey(STRETCHED_KEY.slice(0, 32))),
encryptionKeyB64: btoa(STRETCHED_KEY.slice(32, 64)),
};
const transformer = new StaticKeyEncryptionRemoteTransformer(KEY_BUNDLE);
add_task(function* test_encryption_transformer_roundtrip() {
const POSSIBLE_DATAS = [
"string",
2, // number
[1, 2, 3], // array
{key: "value"}, // object
];
for (let data of POSSIBLE_DATAS) {
const record = {data: data, id: "key-some_2D_key", key: "some-key"};
deepEqual(record, yield transformer.decode(yield transformer.encode(record)));
}
});
add_task(function* test_refuses_to_decrypt_tampered() {
const encryptedRecord = yield transformer.encode({data: [1, 2, 3], id: "key-some_2D_key", key: "some-key"});
const tamperedHMAC = Object.assign({}, encryptedRecord, {hmac: "0000000000000000000000000000000000000000000000000000000000000001"});
yield* throwsGen(Utils.isHMACMismatch, function*() {
yield transformer.decode(tamperedHMAC);
});
const tamperedIV = Object.assign({}, encryptedRecord, {IV: "aaaaaaaaaaaaaaaaaaaaaa=="});
yield* throwsGen(Utils.isHMACMismatch, function*() {
yield transformer.decode(tamperedIV);
});
});

Some files were not shown because too many files have changed in this diff Show more