diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js
index 61af5ef..071b7df 100644
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -922,22 +922,23 @@ pref("browser.taskbar.lists.frequent.enabled", true);
pref("browser.taskbar.lists.recent.enabled", false);
pref("browser.taskbar.lists.maxListItemCount", 7);
pref("browser.taskbar.lists.tasks.enabled", true);
pref("browser.taskbar.lists.refreshInSeconds", 120);
#endif
#ifdef MOZ_SERVICES_SYNC
// The sync engines to use.
-pref("services.sync.registerEngines", "Bookmarks,Form,History,Password,Prefs,Tab");
+pref("services.sync.registerEngines", "Bookmarks,Form,History,Password,Prefs,Tab,Addons");
// Preferences to be synced by default
pref("services.sync.prefs.sync.accessibility.blockautorefresh", true);
pref("services.sync.prefs.sync.accessibility.browsewithcaret", true);
pref("services.sync.prefs.sync.accessibility.typeaheadfind", true);
pref("services.sync.prefs.sync.accessibility.typeaheadfind.linksonly", true);
+pref("services.sync.prefs.sync.addons.ignoreRepositoryChecking", false);
pref("services.sync.prefs.sync.app.update.mode", true);
pref("services.sync.prefs.sync.browser.download.manager.closeWhenDone", true);
pref("services.sync.prefs.sync.browser.download.manager.retention", true);
pref("services.sync.prefs.sync.browser.download.manager.scanWhenDone", true);
pref("services.sync.prefs.sync.browser.download.manager.showWhenStarting", true);
pref("services.sync.prefs.sync.browser.formfill.enable", true);
pref("services.sync.prefs.sync.browser.link.open_newwindow", true);
pref("services.sync.prefs.sync.browser.offline-apps.notify", true);
diff --git a/browser/base/content/syncSetup.js b/browser/base/content/syncSetup.js
index 0be9c24..05ca444 100644
--- a/browser/base/content/syncSetup.js
+++ b/browser/base/content/syncSetup.js
@@ -577,17 +577,18 @@ var gSyncSetup = {
this.completePairing();
}
if (!this._resettingSync) {
function isChecked(element) {
return document.getElementById(element).hasAttribute("checked");
}
- let prefs = ["engine.bookmarks", "engine.passwords", "engine.history", "engine.tabs", "engine.prefs"];
+ let prefs = ["engine.bookmarks", "engine.passwords", "engine.history",
+ "engine.tabs", "engine.prefs", "engine.addons"];
for (let i = 0;i < prefs.length;i++) {
Weave.Svc.Prefs.set(prefs[i], isChecked(prefs[i]));
}
this._handleNoScript(false);
if (Weave.Svc.Prefs.get("firstSync", "") == "notReady")
Weave.Svc.Prefs.reset("firstSync");
Weave.Service.persistLogin();
diff --git a/browser/base/content/syncSetup.xul b/browser/base/content/syncSetup.xul
index 551f412..e978a48 100644
--- a/browser/base/content/syncSetup.xul
+++ b/browser/base/content/syncSetup.xul
@@ -423,16 +423,20 @@
+
diff --git a/browser/components/preferences/sync.xul b/browser/components/preferences/sync.xul
index 7fbbf8b..3e4ee0b 100644
--- a/browser/components/preferences/sync.xul
+++ b/browser/components/preferences/sync.xul
@@ -57,16 +57,17 @@
onpaneload="gSyncPane.init()">
+
@@ -147,16 +148,21 @@
accesskey="&engine.history.accesskey;"
preference="engine.history"/>
+
+
+
diff --git a/browser/locales/en-US/chrome/browser/preferences/sync.dtd b/browser/locales/en-US/chrome/browser/preferences/sync.dtd
index ad348c8..e56afeb 100644
--- a/browser/locales/en-US/chrome/browser/preferences/sync.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/sync.dtd
@@ -25,16 +25,18 @@
+
+
diff --git a/browser/locales/en-US/chrome/browser/syncSetup.dtd b/browser/locales/en-US/chrome/browser/syncSetup.dtd
index f346bd0..cca142a 100644
--- a/browser/locales/en-US/chrome/browser/syncSetup.dtd
+++ b/browser/locales/en-US/chrome/browser/syncSetup.dtd
@@ -83,16 +83,18 @@
+
+
diff --git a/services/sync/modules/addonsreconciler.js b/services/sync/modules/addonsreconciler.js
new file mode 100644
index 0000000..ea001e9
--- /dev/null
+++ b/services/sync/modules/addonsreconciler.js
@@ -0,0 +1,564 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Firefox Sync.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Gregory Szorc
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * This file contains middleware to reconcile state of AddonManager for
+ * purposes of tracking events for Sync. The content in this file exists
+ * because AddonManager does not have a getChangesSinceX() API and adding
+ * that functionality properly was deemed too time-consuming at the time
+ * add-on sync was originally written. If/when AddonManager adds this API,
+ * this file can go away and the add-ons engine can be rewritten to use it.
+ *
+ * It was decided to have this tracking functionality exist in a separate
+ * standalone file so it could be more easily understood, tested, and
+ * hopefully ported.
+ */
+
+"use strict";
+
+const Cu = Components.utils;
+
+Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-sync/util.js");
+Cu.import("resource://gre/modules/AddonManager.jsm");
+
+const EXPORTED_SYMBOLS = ["AddonsReconciler"];
+
+const DEFAULT_STATE_FILE = "addonsreconciler";
+
+const CHANGE_INSTALLED = 1;
+const CHANGE_UNINSTALLED = 2;
+const CHANGE_ENABLED = 3;
+const CHANGE_DISABLED = 4;
+
+/**
+ * Maintains state of add-ons.
+ *
+ * The AddonsReconciler is installed as an AddonManager listener. When it
+ * receives change notifications, it updates its internal state database.
+ *
+ * The internal state is persisted to a JSON file in the profile directory.
+ *
+ * An instance of this is bound to an AddonsEngine instance. In reality, it
+ * likely exists as a singleton. To AddonsEngine, it functions as a store and
+ * an entity which emits events for tracking.
+ *
+ * The usage pattern for instances of this class is:
+ *
+ * let reconciler = new AddonsReconciler();
+ * reconciler.loadState(null, function(error) { ... });
+ *
+ * // At this point, your instance should be ready to use.
+ *
+ * When you are finished with the instance, please call:
+ *
+ * reconciler.stopListening();
+ * reconciler.saveStateFile(...);
+ *
+ *
+ * --------------
+ *
+ * There are 2 classes of listeners in the AddonManager: AddonListener and
+ * InstallListener. The class is a listener for both (member functions just
+ * get called directly).
+ *
+ * When an add-on is installed, listeners are called in the following order:
+ *
+ * IL.onInstallStarted, AL.onInstalling, IL.onInstallEnded, AL.onInstalled
+ *
+ * For non-restartless add-ons, an application restart may occur between
+ * IL.onInstallEnded and AL.onInstalled. Unfortunately, Sync likely will
+ * not be lodaded when AL.onInstalled is fired shortly after application
+ * start, so it won't see this event. Therefore, for add-ons requiring a
+ * restart, Sync treats the IL.onInstallEnded event as good enough to
+ * denote an install. For restartless add-ons, Sync assumes AL.onInstalled
+ * will follow shortly after IL.onInstallEnded and thus is ignores
+ * IL.onInstallEnded.
+ *
+ * For uninstalls, we see AL.onUninstalling then AL.onUninstalled. Like
+ * installs, the events could be separated by an application restart and Sync
+ * may not see the onUninstalled event. Again, if we require a restart, we
+ * react to onUninstalling. If not, we assume we'll get onUninstalled.
+ *
+ * Enabling and disabling work by sending:
+ *
+ * AL.onEnabling, AL.onEnabled
+ * AL.onDisabling, AL.onDisabled
+ *
+ * Again, they may be separated by a restart, so we heed the requiresRestart
+ * flag.
+ *
+ * Actions can be undone. All undoable actions notify the same
+ * AL.onOperationCancelled event. We treat this event like any other.
+ *
+ * Restartless add-ons have interesting behavior during uninstall. These
+ * add-ons are first disabled then they are actually uninstalled. So, the
+ * tracker will see onDisabling and onDisabled. The onUninstalling and
+ * onUninstalled events only come after the Addon Manager is closed or another
+ * view is switched to.
+ *
+ * To further complicate things, the tracker will see some notifications before
+ * changes hit the database. In the case of non-restartless add-ons,
+ * IL.onInstallEnded is the observed event and this is called *before* the
+ * add-on has hit the database. In the case of restartless add-ons,
+ * AL.onInstalled is fired after the database is populated. What this means
+ * is the tracker knows a particular ID has changed, but when a sync comes
+ * along and the store calls AddonManager.getAddonBySyncGUID(), that may fail
+ * to retrieve anything because the add-on hasn't been inserted into the
+ * database yet! If we were to do nothing, the sync engine would treat this
+ * as a deleted record and the install event would never make it to a record.
+ *
+ * Fortunately, the tracker knows what the state should be based on the events
+ * it has seen. The tracker maintains state on syncGUIDs that should eventually
+ * be in a certain state. During a sync, the tracker is consulted. The tracker
+ * can effectively override the AddonManager, saying "Don't trust data for this
+ * add-on. But, if the AddonManager and I agree, you can trust the
+ * AddonManager."
+ *
+ */
+function AddonsReconciler() {
+ this._log = Log4Moz.repository.getLogger("Sync.AddonsReconciler");
+ let level = Svc.Prefs.get("log.logger.addonsreconciler", "Debug");
+ this._log.level = Log4Moz.Level[level];
+
+ AddonManager.addAddonListener(this);
+ AddonManager.addInstallListener(this);
+ this._listening = true;
+
+ let us = this;
+ Svc.Obs.add("xpcom-shutdown", function() {
+ us.stopListening();
+ });
+};
+AddonsReconciler.prototype = {
+ /** Flag indicating whether we are listening to AddonManager events. */
+ _listening: false,
+
+ /** log4moz logger instance */
+ _log: null,
+
+ /**
+ * This is the main data structure for an instance.
+ *
+ * Keys are add-on IDs. Values are objects which describe the state of the
+ * add-on.
+ */
+ _addons: {},
+
+ /**
+ * List of add-on changes over time.
+ *
+ * Each element is an array of [time, change, id].
+ */
+ _changes: [],
+
+ _listeners: [],
+
+ get addons() {
+ return this._addons;
+ },
+
+ /**
+ * Loads reconciler state from a file.
+ *
+ * The path is relative to the weave directory in the profile. If no
+ * path is given, the default one is used.
+ *
+ * @param path
+ * Path to load. ".json" is appended automatically.
+ * @param callback
+ * Callback to be executed upon file load. The callback receives a
+ * truthy error argument signifying whether an error occurred and a
+ * boolean indicating whether data was loaded.
+ */
+ loadState: function loadState(path, callback) {
+ let file = path || DEFAULT_STATE_FILE;
+ Utils.jsonLoad(file, this, function(json) {
+ this._addons = {};
+ this._changes = [];
+
+ if (json != undefined) {
+ this._addons = json.addons;
+ for each (let record in this._addons) {
+ record.modified = new Date(record.modified);
+ }
+
+ for each (let change in json.changes) {
+ this._changes.push([new Date(change[0]), change[1], change[2]]);
+ }
+
+ if (callback) {
+ callback(false, true);
+ }
+ } else {
+ if (callback) {
+ callback(false, false);
+ }
+ }
+ });
+ },
+
+ /**
+ * Saves the state to a file in the local profile.
+ *
+ * @param path
+ * String path in profile to save to. If not defined, the default
+ * will be used.
+ * @param callback
+ * Function to be invoked on save completion. No parameters will be
+ * passed to callback.
+ */
+ saveState: function saveState(path, callback) {
+ let state = {addons: {}, changes: []};
+
+ for (let [id, record] in Iterator(this._addons)) {
+ state[id] = {};
+ for (let [k, v] in Iterator(record)) {
+ if (k == "modified") {
+ state[id][k] = v.getTime();
+ }
+ else {
+ state[id][k] = v;
+ }
+ }
+ }
+
+ for each (let change in this._changes) {
+ state.changes.push([change[0].getTime(), change[1], change[2]]);
+ }
+
+ Utils.jsonSave(path || DEFAULT_STATE_FILE, this, state, callback);
+ },
+
+ /**
+ * Registers a change listener with this instance.
+ *
+ * Change listeners are called every time a change is recorded. The listener
+ * should be a function that takes 3 arguments, the Date at which the change
+ * happened, the type of change (a CHANGE_* constant), and the add-on state
+ * object reflecting the current state of the add-on at the time of the
+ * change.
+ */
+ addChangeListener: function addChangeListener(listener) {
+ if (!this._listeners.some(function(i) { return i == listener; })) {
+ this._log.debug("Adding change listener.");
+ this._listeners.push(listener);
+ }
+ },
+
+ /**
+ * Removes a previously-installed change listener from the instance.
+ */
+ removeChangeListener: function removeChangeListener(listener) {
+ this._listeners = this._listeners.filter(function(element) {
+ if (element == listener) {
+ this._log.debug("Removing change listener.");
+ return false;
+ } else {
+ this._log.debug("Retaining change listener.");
+ return true;
+ }
+ }.bind(this));
+ },
+
+ /**
+ * Tells the instance to stop listening for AddonManager changes.
+ *
+ * The reconciler should always be listening. This should only be called when
+ * the instance is being destroyed.
+ */
+ stopListening: function stopListening() {
+ if (this._listening) {
+ AddonManager.removeInstallListener(this);
+ AddonManager.removeAddonListener(this);
+ this._listening = false;
+ }
+ },
+
+ /**
+ * Refreshes the global state of add-ons by querying the AddonsManager.
+ */
+ refreshGlobalState: function refreshGlobalState(callback) {
+ this._log.info("Refreshing global state from AddonManager.");
+ AddonManager.getAllAddons(function (addons) {
+ let ids = {};
+
+ for each (let addon in addons) {
+ ids[addon.id] = true;
+ this.rectifyStateFromAddon(addon);
+ }
+
+ // Look for locally-defined add-ons that don't exist any more and update
+ // their record
+ for (let [id, addon] in Iterator(this._addons)) {
+ if (id in ids) {
+ continue;
+ }
+
+ // If the id isn't in ids, it means that the add-on has been deleted.
+ if (addon.installed) {
+ addon.installed = false;
+ this.addChange(new Date(), CHANGE_UNINSTALLED, addon);
+ }
+ }
+
+ this.saveState(null, callback);
+ }.bind(this));
+ },
+
+ /**
+ * Rectifies the state of an add-on from an add-on instance.
+ *
+ * @param addon
+ * Addon instance being updated.
+ */
+ rectifyStateFromAddon: function rectifyStateFromAddon(addon) {
+ this._log.debug("Rectifying state for addon: " + addon.id);
+
+ let id = addon.id;
+ let enabled = !addon.userDisabled;
+ let guid = addon.syncGUID;
+ let now = new Date();
+
+ if (!(id in this._addons)) {
+ let record = {
+ id: id,
+ guid: guid,
+ enabled: enabled,
+ installed: true,
+ modified: now,
+ type: addon.type,
+ scope: addon.scope,
+ foreignInstall: addon.foreignInstall
+ };
+ this._addons[id] = record;
+ this.addChange(now, CHANGE_INSTALLED, record);
+ return;
+ }
+
+ let record = this._addons[id];
+
+ if (!record.installed) {
+ record.installed = true;
+ record.modified = now;
+ }
+
+ if (record.enabled != enabled) {
+ record.enabled = enabled;
+ record.modified = now;
+ let change = enabled ? CHANGE_ENABLED : CHANGE_DISABLED;
+ this.addChange(new Date(), change, record);
+ }
+
+ if (record.guid != guid) {
+ record.guid = guid;
+ // We don't record a change because the Sync engine rectifies this on its
+ // own.
+ }
+ },
+
+ addChange: function addChange(date, change, addon) {
+ this._log.info("Change recorded for " + addon.id);
+ this._changes.push([date, change, addon.id]);
+
+ for each (let listener in this._listeners) {
+ try {
+ listener.changeListener.call(listener, date, change, addon);
+ } catch (ex) {
+ this._log.warn("Exception calling change listener: " +
+ Utils.exceptionStr(ex));
+ }
+ }
+ },
+
+ /**
+ * Obtain the set of changes to add-ons since the date passed.
+ *
+ * This will return an array of arrays. Each entry in the array has the
+ * elements [time, change_type, id], where
+ *
+ * date - Date instance representing when the change occurred
+ * change_type - One of CHANGE_* constants.
+ */
+ getChangesSinceDate: function getChangesSinceDate(date) {
+ let length = this._changes.length;
+ for (let i = 0; i < length; i++) {
+ let entry = this._changes[i];
+
+ if (entry[0] < date) {
+ continue;
+ }
+
+ return this._changes.slice(i);
+ }
+
+ return [];
+ },
+
+ /**
+ * Prunes all recorded changes from before the specified Date.
+ *
+ * @param date
+ * Entries older than this Date will be removed.
+ */
+ pruneChangesBeforeDate: function pruneChangesBeforeDate(date) {
+ while (this._changes.length > 0) {
+ let entry = this._changes[0];
+
+ if (entry[0] < date) {
+ delete this._changes[0];
+ } else {
+ return;
+ }
+ }
+ },
+
+ /**
+ * Obtains the set of all known Sync GUIDs for add-ons.
+ *
+ * @return Object with guids as keys and values of true.
+ */
+ getAllSyncGUIDs: function getAllSyncGUIDs() {
+ let result = {};
+ for (let id in this._addons) {
+ result[id] = true;
+ }
+
+ return result;
+ },
+
+ /**
+ * Obtain the add-on state record for an add-on by Sync GUID.
+ *
+ * If the add-on could not be found, returns null.
+ *
+ * @param guid
+ * Sync GUID of add-on to retrieve.
+ * @return Object on success on null on failure.
+ */
+ getAddonStateFromSyncGUID: function getAddonStateFromSyncGUID(guid) {
+ for each (let addon in this._addons) {
+ if (addon.guid == guid) {
+ return addon;
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Handler that is invoked as part of the AddonManager listeners.
+ */
+ _handleListener: function _handlerListener(action, addon, requiresRestart) {
+ // Since this is called as an observer, we explicitly trap errors and
+ // log them to ourselves so we don't see errors reported elsewhere.
+ try {
+ let id = addon.id;
+ this._log.debug("Add-on change: " + action + " to " + id);
+
+ // We assume that every event for non-restartless add-ons is
+ // followed by another event and that this follow-up event is the most
+ // appropriate to react to. Currently we ignore onEnabling, onDisabling,
+ // and onUninstalling for non-restartless add-ons.
+ if (requiresRestart === false) {
+ this._log.debug("Ignoring notification because restartless");
+ return;
+ }
+
+ switch (action) {
+ case "onEnabling":
+ case "onEnabled":
+ case "onDisabling":
+ case "onDisabled":
+ case "onInstalled":
+ case "onOperationCancelled":
+ this.rectifyStateFromAddon(addon);
+ break;
+
+ case "onUninstalling":
+ case "onUninstalled":
+ let id = addon.id;
+ if (id in this._addons) {
+ let now = new Date();
+ let record = this._addons[id];
+ record.installed = false;
+ record.modified = now;
+ this.addChange(now, CHANGE_UNINSTALLED, record);
+ }
+ }
+
+ this.saveState(null, null);
+ }
+ catch (ex) {
+ this._log.warn("Exception: " + Utils.exceptionStr(ex));
+ }
+ },
+
+ // AddonListeners
+ onEnabling: function onEnabling(addon, requiresRestart) {
+ this._handleListener("onEnabling", addon, requiresRestart);
+ },
+ onEnabled: function onEnabled(addon) {
+ this._handleListener("onEnabled", addon);
+ },
+ onDisabling: function onDisabling(addon, requiresRestart) {
+ this._handleListener("onDisabling", addon, requiresRestart);
+ },
+ onDisabled: function onDisabled(addon) {
+ this._handleListener("onDisabled", addon);
+ },
+ onInstalling: function onInstalling(addon, requiresRestart) {
+ this._handleListener("onInstalling", addon, requiresRestart);
+ },
+ onInstalled: function onInstalled(addon) {
+ this._handleListener("onInstalled", addon);
+ },
+ onUninstalling: function onUninstalling(addon, requiresRestart) {
+ this._handleListener("onUninstalling", addon, requiresRestart);
+ },
+ onUninstalled: function onUninstalled(addon) {
+ this._handleListener("onUninstalled", addon);
+ },
+ onOperationCancelled: function onOperationCancelled(addon) {
+ this._handleListener("onOperationCancelled", addon);
+ },
+
+ // InstallListeners
+ onInstallEnded: function onInstallEnded(install, addon) {
+ this._handleListener("onInstallEnded", addon);
+ }
+};
diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js
index 273abcb..0df6a98 100644
--- a/services/sync/modules/constants.js
+++ b/services/sync/modules/constants.js
@@ -103,16 +103,17 @@ MOBILE_BATCH_SIZE: 50,
DEFAULT_GUID_FETCH_BATCH_SIZE: 50,
DEFAULT_MOBILE_GUID_FETCH_BATCH_SIZE: 50,
// Default batch size for applying incoming records.
DEFAULT_STORE_BATCH_SIZE: 1,
HISTORY_STORE_BATCH_SIZE: 50, // same as MOBILE_BATCH_SIZE
FORMS_STORE_BATCH_SIZE: 50, // same as MOBILE_BATCH_SIZE
PASSWORDS_STORE_BATCH_SIZE: 50, // same as MOBILE_BATCH_SIZE
+ADDONS_STORE_BATCH_SIZE: 1000000, // process all addons at once
// score thresholds for early syncs
SINGLE_USER_THRESHOLD: 1000,
MULTI_DEVICE_THRESHOLD: 300,
// Other score increment constants
SCORE_INCREMENT_SMALL: 1,
SCORE_INCREMENT_MEDIUM: 10,
diff --git a/services/sync/modules/engines/addons.js b/services/sync/modules/engines/addons.js
new file mode 100644
index 0000000..3c170fd
--- /dev/null
+++ b/services/sync/modules/engines/addons.js
@@ -0,0 +1,755 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Firefox Sync.
+ *
+ * The Initial Developer of the Original Code is the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Gregory Szorc
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/*
+ * This file defines the add-on sync functionality.
+ *
+ * There are currently a number of known limitations:
+ * - We only sync XPI extensions and themes available from addons.mozilla.org.
+ * We hope to expand support for other add-ons eventually.
+ * - We only attempt syncing of add-ons between applications of the same type.
+ * This means add-ons will not synchronize between Firefox desktop and
+ * Firefox mobile, for example. This is because of significant add-on
+ * incompatibility between application types.
+ *
+ * Add-on records exist for each known {add-on, app-id} pair in the Sync client
+ * set. Each record has a randomly chosen GUID. The records then contain
+ * basic metadata about the add-on.
+ *
+ * We currently synchronize:
+ *
+ * - Installations
+ * - Uninstallations
+ * - User enabling and disabling
+ */
+
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://services-sync/addonsreconciler.js");
+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/async.js");
+
+Cu.import("resource://gre/modules/AddonManager.jsm");
+Cu.import("resource://gre/modules/AddonRepository.jsm");
+
+const EXPORTED_SYMBOLS = ["AddonsEngine"];
+
+const ADDON_REPOSITORY_WHITELIST_HOSTNAME = "addons.mozilla.org";
+
+// 7 days in milliseconds.
+const PRUNE_ADDON_CHANGES_THRESHOLD = 60 * 60 * 24 * 7 * 1000;
+
+/**
+ * AddonRecord represents the state of an add-on in an application.
+ *
+ * Each add-on has its own record for each application ID it is installed
+ * on.
+ *
+ * The ID of add-on records is a randomly-generated GUID. It is random instead
+ * of deterministic so the URIs of the records cannot be guessed and so
+ * compromised server credentials won't result in disclosure of the specific
+ * add-ons present in a Sync account.
+ *
+ * The record contains the following fields:
+ *
+ * addonID
+ * ID of the add-on. This correlates to the "id" property on an Addon type.
+ *
+ * applicationID
+ * The application ID this record is associated with. Clients currently
+ * ignore records from other application IDs.
+ *
+ * enabled
+ * Boolean stating whether add-on is enabled or disabled by the user.
+ *
+ * source
+ * String indicating where an add-on is from. Currently, we only support
+ * the value "amo" which indicates that the add-on came from the official
+ * add-ons repository, addons.mozilla.org. In the future, we may support
+ * installing add-ons from other sources. This provides a future-compatible
+ * mechanism for clients to only apply records they know how to handle.
+ */
+function AddonRecord(collection, id) {
+ CryptoWrapper.call(this, collection, id);
+}
+AddonRecord.prototype = {
+ __proto__: CryptoWrapper.prototype,
+ _logName: "Record.Addon"
+};
+
+Utils.deferGetSet(AddonRecord, "cleartext", ["addonID",
+ "applicationID",
+ "enabled",
+ "source"]);
+
+/**
+ * The AddonsEngine handles synchronization of add-ons between clients.
+ *
+ * The engine handles incoming add-ons in one large batch, as it needs
+ * to assess the overall state at one time.
+ *
+ * The engine fires the following notifications (all prefixed with
+ * "weave:engine:addons:"):
+ *
+ * restart-required Fired at the tail end of performing a sync when an
+ * an application restart is required to finish add-on
+ * processing. The observer receives an array of add-on IDs
+ * that require restart. Observers should likely wait until
+ * after the sync is done (signified by reception of the
+ * "weave:service:sync:finish" event) to actually restart
+ * or give the user an opportunity to restart.
+ */
+function AddonsEngine() {
+ SyncEngine.call(this, "Addons");
+
+ this._reconciler = new AddonsReconciler();
+}
+AddonsEngine.prototype = {
+ __proto__: SyncEngine.prototype,
+ _storeObj: AddonsStore,
+ _trackerObj: AddonsTracker,
+ _recordObj: AddonRecord,
+ version: 1,
+
+ _reconciler: null,
+ _reconcilerStateLoaded: false,
+
+ /**
+ * Override parent method to find add-ons by ID, not Sync GUID.
+ */
+ _findDupe: function _findDupe(item) {
+ let id = item.addonID;
+
+ let addons = this._reconciler.addons;
+ if (!(id in addons)) {
+ return null;
+ }
+
+ let addon = addons[id];
+ if (addon.guid != item.id) {
+ return addon.guid;
+ }
+
+ return null;
+ },
+
+ /**
+ * We override getChangedIDs to pull in tracker changes plus changes from the
+ * reconciler log.
+ */
+ getChangedIDs: function getChangedIDs() {
+ let changes = {};
+ for (let [id, modified] in Iterator(this._tracker.changedIDs)) {
+ changes[id] = modified;
+ }
+
+ let lastSyncDate = new Date(this.lastSync * 1000);
+ let reconcilerChanges = this._reconciler.getChangesSinceDate(lastSyncDate);
+ let addons = this._reconciler.addons;
+ for each (let change in reconcilerChanges) {
+ let changeTime = change[0];
+ let id = change[2];
+
+ if (!(id in addons)) {
+ continue;
+ }
+
+ // Keep newest modified time.
+ if (id in changes && changeTime < changes[id]) {
+ continue;
+ }
+
+ this._log.debug("Adding changed add-on from changes log: " + id);
+ let addon = addons[id];
+ changes[addon.guid] = changeTime.getTime() / 1000;
+ }
+
+ return changes;
+ },
+
+ /**
+ * Override start of sync function to refresh add-on global state and pull
+ * in any missing changes.
+ */
+ _syncStartup: function _syncStartup() {
+ // We refresh state before calling parent because syncStartup in the parent
+ // looks for changed IDs, which is dependent on add-on state being up to
+ // date.
+ this._refreshReconcilerState();
+
+ SyncEngine.prototype._syncStartup.call(this);
+ },
+
+ /**
+ * Override end of sync to perform a little housekeeping on the reconciler.
+ */
+ _syncCleanup: function _syncCleanup() {
+ let ms = 1000 * this.lastSync - PRUNE_ADDON_CHANGES_THRESHOLD;
+ this._reconciler.pruneChangesBeforeDate(new Date(ms));
+
+ SyncEngine.prototype._syncCleanup.call(this);
+ },
+
+ /**
+ * Helper function to ensure reconciler is up to date.
+ */
+ _refreshReconcilerState: function _refreshReconcilerState() {
+ this._log.debug("Refreshing reconciler state");
+ if (!this._reconcilerStateLoaded) {
+ let cb = Async.makeSpinningCallback();
+ this._reconciler.loadState(null, cb);
+ cb.wait();
+ this._reconcilerStateLoaded = true;
+ }
+
+ let cb = Async.makeSpinningCallback();
+ this._reconciler.refreshGlobalState(cb);
+ cb.wait();
+ }
+};
+
+/**
+ * This is the primary interface between Sync and the Addons Manager.
+ */
+function AddonsStore(name) {
+ Store.call(this, name);
+}
+AddonsStore.prototype = {
+ __proto__: Store.prototype,
+
+ // Define the add-on types (.type) that we support.
+ _syncableTypes: ["extension", "theme"],
+
+ get reconciler() {
+ return this.engine._reconciler;
+ },
+
+ get engine() {
+ return Engines.get("addons");
+ },
+
+ /**
+ * Override applyIncoming to filter out records we can't handle.
+ */
+ applyIncoming: function applyIncoming(record) {
+ // Ignore records not belonging to our application ID because that is the
+ // current policy.
+ if (record.applicationID != Services.appinfo.ID) {
+ this._log.info("Ignoring incoming record from other App ID: " +
+ record.id);
+ return;
+ }
+
+ // Ignore records that aren't from the official add-on repository, as that
+ // is our current policy.
+ if (record.source != "amo") {
+ this._log.info("Ignoring unknown add-on source (" + record.source + ")" +
+ " for " + record.id);
+ return;
+ }
+
+ Store.prototype.applyIncoming.call(this, record);
+ },
+
+
+ /**
+ * Provides core Store API to create/install an add-on from a record.
+ */
+ create: function create(record) {
+ let cb = Async.makeSpinningCallback();
+ this.installAddonsFromIDs([record.addonID], 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.
+ cb.wait();
+
+ this._log.info("Add-on installed: " + record.addonID);
+ let addon = this.getAddonByID(record.addonID);
+ if (addon) {
+ this._log.info("Setting add-on Sync GUID to remote: " + record.id);
+ addon.syncGUID = record.id;
+ }
+ },
+
+ /**
+ * Provides core Store API to remove/uninstall an add-on from a record.
+ */
+ remove: function remove(record) {
+ let addon = this.getAddonByID(record.addonID);
+ if (!addon) {
+ return;
+ }
+
+ this._log.debug("Uninstalling add-on: " + addon.id);
+ let cb = Async.makeSpinningCallback();
+ this.uninstallAddon(addon, cb);
+ cb.wait();
+ },
+
+ update: function update(record) {
+ let addon = this.getAddonByID(record.addonID);
+ if (!addon) {
+ // TODO log error?
+ return;
+ }
+
+ if (record.enabled == addon.userDisabled) {
+ this._log.info("Updating userEnabled flag: " + addon.id);
+
+ addon.userDisabled = !record.enabled;
+ }
+ },
+
+ itemExists: function itemExists(guid) {
+ let addon = this.reconciler.getAddonStateFromSyncGUID(guid);
+
+ if (!addon) {
+ return false;
+ }
+
+ return !!addon.installed;
+ },
+
+ /**
+ * Create an add-on record from its GUID.
+ *
+ * @param guid
+ * Add-on GUID (from extensions DB)
+ * @param collection
+ * Collection to add record to.
+ *
+ * @return AddonRecord instance
+ */
+ createRecord: function createRecord(guid, collection) {
+ let record = new AddonRecord(collection, guid);
+ record.applicationID = Services.appinfo.ID;
+
+ let addon = this.reconciler.getAddonStateFromSyncGUID(guid);
+
+ // If we don't know about this GUID or if it has been uninstalled, we mark
+ // the record as deleted.
+ if (!addon || !addon.installed) {
+ record.deleted = true;
+ return record;
+ }
+
+ record.modified = addon.modified.getTime() / 1000;
+
+ record.addonID = addon.id;
+ record.enabled = addon.enabled;
+
+ // This needs to be dynamic when add-ons don't come from AddonRepository.
+ record.source = "amo";
+
+ return record;
+ },
+
+ /**
+ * Changes the id of an add-on.
+ *
+ * This implements a core API of the store.
+ */
+ changeItemID: function changeItemID(oldID, newID) {
+ let addon = this.getAddonByGUID(oldID);
+ if (!addon) {
+ this._log.debug("Cannot change item ID because old add-on not present: " +
+ oldID);
+ return;
+ }
+
+ addon.syncGUID = newID;
+
+ let state = this.reconciler.addons[addon.id];
+ if (state) {
+ state.guid = newID;
+ }
+ },
+
+ /**
+ * Obtain the set of all syncable add-on Sync GUIDs.
+ *
+ * This implements a core Store API.
+ */
+ getAllIDs: function getAllIDs() {
+ let ids = {};
+
+ let addons = this.reconciler.addons;
+ for each (let addon in addons) {
+ if (this.isAddonSyncable(addon)) {
+ ids[addon.guid] = true;
+ }
+ }
+
+ return ids;
+ },
+
+ /**
+ * Wipe engine data.
+ *
+ * This uninstalls all syncable addons from the application. In case of
+ * error, it logs the error and keeps trying with other add-ons.
+ */
+ wipe: function wipe() {
+ this._log.info("Processing wipe.");
+
+ // TODO should this wipe *all* add-ons or just the syncable ones?
+ this.engine._refreshReconcilerState();
+
+ for (let id in this.getAllIDs()) {
+ let addon = this.getAddonByID(id);
+ if (!addon) {
+ this._log.debug("Ignoring add-on because it couldn't be obtained: " +
+ id);
+ continue;
+ }
+
+ this._log.info("Uninstalling add-on as part of wipe: " + addon.id);
+ Utils.catch(addon.uninstall)();
+ }
+ },
+
+ /***************************************************************************
+ * Functions below are unique to this store and not part of the Store API *
+ ***************************************************************************/
+
+ /**
+ * Obtain an add-on from its database ID
+ *
+ * @param id
+ * Add-on ID
+ * @return Addon or undefined if not found
+ */
+ getAddonByID: function getAddonByID(id) {
+ let cb = Async.makeSyncCallback();
+ AddonManager.getAddonByID(id, cb);
+ return Async.waitForSyncCallback(cb);
+ },
+
+ /**
+ * Obtain an add-on from its database/Sync GUID
+ *
+ * @param guid
+ * Add-on Sync GUID
+ * @return DBAddonInternal or null
+ */
+ getAddonByGUID: function getAddonByGUID(guid) {
+ let cb = Async.makeSyncCallback();
+ AddonManager.getAddonBySyncGUID(guid, cb);
+ return Async.waitForSyncCallback(cb);
+ },
+
+ /**
+ * Determines whether an add-on is suitable for Sync.
+ *
+ * @param addon
+ * Addon instance
+ * @return Boolean indicating whether it is appropriate for Sync
+ */
+ isAddonSyncable: function isAddonSyncable(addon) {
+ // Currently, we limit syncable add-ons to those that:
+ // 1) In a well-defined set of types
+ // 2) Installed in current profile
+ // 3) Not installed by a foreign entity (i.e. installed by the app)
+ // since they act like global extensions.
+ // 4) Are installed from AMO
+ let syncable = addon &&
+ this._syncableTypes.indexOf(addon.type) != -1 &&
+ addon.scope | AddonManager.SCOPE_PROFILE &&
+ !addon.foreignInstall;
+
+ // We provide a back door to skip the repository checking of an add-on.
+ // This is utilized by the tests to make testing easier.
+ if (Svc.Prefs.get("addons.ignoreRepositoryChecking", false)) {
+ return syncable;
+ }
+
+ let cb = Async.makeSyncCallback();
+ AddonRepository.getCachedAddonByID(addon.id, cb);
+ let result = Async.waitForSyncCallback(cb);
+
+ return result && result.sourceURI &&
+ result.sourceURI.host == ADDON_REPOSITORY_WHITELIST_HOSTNAME;
+ },
+
+ /**
+ * Obtain an AddonInstall object from an AddonSearchResult instance.
+ *
+ * The callback will be invoked with the result of the operation. The
+ * callback receives 2 arguments, error and result. Error will be falsey
+ * on success or some kind of error value otherwise. The result argument
+ * will be an AddonInstall on success or null on failure. It is possible
+ * for the error to be falsey but result to be null. This could happen if
+ * an install was not found.
+ *
+ * @param addon
+ * AddonSearchResult to obtain install from.
+ * @param cb
+ * Function to be called with result of operation.
+ */
+ getInstallFromSearchResult: function getInstallFromSearchResult(addon, cb) {
+ if (addon.install) {
+ cb(null, addon.install);
+ return;
+ }
+
+ this._log.debug("Manually obtaining install for " + addon.id);
+
+ // TODO do we need extra verification on sourceURI source?
+ AddonManager.getInstallForURL(
+ addon.sourceURI.spec,
+ function handleInstall(install) {
+ cb(null, install);
+ },
+ "application/x-xpinstall",
+ undefined,
+ addon.name,
+ addon.iconURL,
+ addon.version
+ );
+ },
+
+ /**
+ * Installs an add-on from an AddonSearchResult instance.
+ *
+ * When complete it calls a callback with 2 arguments, error and result.
+ *
+ * If error is falesy, result is an object. If error is truthy, result is
+ * null.
+ *
+ * The result object has the following keys:
+ * id ID of add-on that was installed.
+ * requiresRestart Boolean indicating whether install requires restart.
+ *
+ * @param addon
+ * AddonSearchResult to install add-on from.
+ * @param cb
+ * Function to be invoked with result of operation.
+ */
+ installAddonFromSearchResult:
+ function installAddonFromSearchResult(addon, cb) {
+ this._log.info("Trying to install add-on from search result: " + addon.id);
+
+ this.getInstallFromSearchResult(addon, function(error, install) {
+ if (error) {
+ cb(error, null);
+ return;
+ }
+
+ if (!install) {
+ cb("AddonInstall not available: " + addon.id, null);
+ return;
+ }
+
+ try {
+ this._log.info("Installing " + addon.id);
+
+ let restart = addon.operationRequiringRestart &
+ AddonManager.OP_NEEDS_RESTART_INSTALL;
+
+ let listener = {
+ onInstallEnded: function(install, addon) {
+ install.removeListener(listener);
+
+ cb(null, {id: addon.id, requiresRestart: restart});
+ },
+ onInstallFailed: function(install) {
+ install.removeListener(listener);
+
+ cb("Install failed: " + install.error, null);
+ },
+ onDownloadFailed: function(install) {
+ install.removeListener(listener);
+
+ cb("Download failed: " + install.error, null);
+ }
+ };
+ install.addListener(listener);
+ install.install();
+ }
+ catch (ex) {
+ this._log.error("Error installing add-on: " + Utils.exceptionstr(ex));
+ cb(ex, null);
+ }
+ }.bind(this));
+ },
+
+ /**
+ * Uninstalls the Addon instance and invoke a callback when it is done.
+ *
+ * @param addon
+ * Addon instance ot uninstall.
+ * @param callback
+ * Function to be invoked when uninstall has finished. It receives a
+ * truthy value signifying error and the add-on which was uninstalled.
+ */
+ uninstallAddon: function uninstallAddon(addon, callback) {
+ let listener = {
+ onUninstalled: function(uninstalled) {
+ if (addon.id == uninstalled.id) {
+ AddonManager.removeAddonListener(listener);
+ callback(false, addon);
+ }
+ }
+ };
+ AddonManager.addAddonListener(listener);
+ addon.uninstall();
+ },
+
+ /**
+ * Installs multiple add-ons specified by their IDs.
+ *
+ * The callback will be called when activity on all add-ons is complete. The
+ * callback receives 2 arguments, error and result.
+ *
+ * If error is truthy, it contains a string describing the overall error.
+ *
+ * result is always an object with details on the overall execution state. It
+ * contains the following keys.
+ *
+ * installed Array of add-on IDs that were installed
+ * errors Array of errors encountered. Only has elements if error is
+ * truthy.
+ *
+ * @param ids
+ * Array of add-on string IDs to install.
+ * @param cb
+ * Function to be called when all actions are complete.
+ */
+ installAddonsFromIDs: function installAddonsFromIDs(ids, cb) {
+ AddonRepository.getAddonsByIDs(ids, {
+ searchSucceeded: function searchSucceeded(addons, addonsLength, total) {
+ this._log.info("Found " + addonsLength + "/" + ids.length +
+ " add-ons during repository search.");
+
+ let ourResult = {
+ installed: [],
+ errors: [],
+ };
+
+ if (!addonsLength) {
+ cb(null, ourResult);
+ return;
+ }
+
+ let finishedCount = 0;
+ let installCallback = function installCallback(error, result) {
+ finishedCount++;
+
+ if (error) {
+ ourResult.errors.push(error);
+ } else {
+ ourResult.installed.push(result.id);
+ }
+
+ if (finishedCount >= addonsLength) {
+ if (ourResult.errors.length > 0) {
+ cb("1 or more add-ons failed to install", ourResult);
+ } else {
+ cb(null, ourResult);
+ }
+ }
+ }.bind(this);
+
+ for (let i = 0; i < addonsLength; i++) {
+ this.installAddonFromSearchResult(addons[i], installCallback);
+ }
+
+ }.bind(this),
+
+ searchFailed: function searchFailed() {
+ cb("AddonRepository search failed", null);
+ }.bind(this)
+ });
+ }
+};
+
+function AddonsTracker(name) {
+ Tracker.call(this, name);
+
+ Svc.Obs.add("weave:engine:start-tracking", this);
+ Svc.Obs.add("weave:engine:stop-tracking", this);
+}
+AddonsTracker.prototype = {
+ __proto__: Tracker.prototype,
+
+ get reconciler() {
+ return Engines.get("addons")._reconciler;
+ },
+
+ get store() {
+ return Engines.get("addons")._store;
+ },
+
+ /**
+ * This callback is executed whenever the AddonsReconciler sends out a change
+ * notification. See AddonsReconciler.addChangeListener().
+ */
+ changeListener: function changeHandler(date, change, addon) {
+ this._log.debug("changeListener invoked: " + change + " " + addon.id);
+ // Ignore changes that occur during sync.
+ if (this.ignoreAll) {
+ return;
+ }
+
+ if (!this.store.isAddonSyncable(addon)) {
+ this._log.debug("Ignoring change because add-on isn't syncable: " +
+ addon.id);
+ return;
+ }
+
+ this.addChangedID(addon.guid, date.getTime() / 1000);
+ this.score += SCORE_INCREMENT_XLARGE;
+ },
+
+ observe: function(subject, topic, data) {
+ switch (topic) {
+ case "weave:engine:start-tracking":
+ this.reconciler.addChangeListener(this);
+ break;
+
+ case "weave:engine:stop-tracking":
+ this.reconciler.removeChangeListener(this);
+ break;
+ }
+ }
+};
diff --git a/services/sync/modules/main.js b/services/sync/modules/main.js
index 23bbe33..f35addd 100644
--- a/services/sync/modules/main.js
+++ b/services/sync/modules/main.js
@@ -37,16 +37,17 @@
const EXPORTED_SYMBOLS = ['Weave'];
let Weave = {};
Components.utils.import("resource://services-sync/constants.js", Weave);
let lazies = {
"record.js": ["CollectionKeys", "BulkKeyBundle", "SyncKeyBundle"],
"engines.js": ['Engines', 'Engine', 'SyncEngine', 'Store'],
+ "engines/addons.js": ["AddonsEngine"],
"engines/bookmarks.js": ['BookmarksEngine', 'BookmarksSharingManager'],
"engines/clients.js": ["Clients"],
"engines/forms.js": ["FormEngine"],
"engines/history.js": ["HistoryEngine"],
"engines/prefs.js": ["PrefsEngine"],
"engines/passwords.js": ["PasswordEngine"],
"engines/tabs.js": ["TabEngine"],
"identity.js": ["Identity", "ID"],
diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js
index 6b92bee..bd81249 100644
--- a/services/sync/modules/util.js
+++ b/services/sync/modules/util.js
@@ -189,16 +189,20 @@ let Utils = {
/**
* Encode byte string as base64url (RFC 4648).
*/
encodeBase64url: function encodeBase64url(bytes) {
return btoa(bytes).replace('+', '-', 'g').replace('/', '_', 'g');
},
+ decodeBase64url: function decodeBase64url(data) {
+ return atob(data.replace('-', '+', 'g').replace('_', '/', 'g'));
+ },
+
/**
* GUIDs are 9 random bytes encoded with base64url (RFC 4648).
* That makes them 12 characters long with 72 bits of entropy.
*/
makeGUID: function makeGUID() {
return Utils.encodeBase64url(Utils.generateRandomBytes(9));
},
diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js
index 56e715f..400ce62 100644
--- a/services/sync/services-sync.js
+++ b/services/sync/services-sync.js
@@ -12,16 +12,17 @@ pref("services.sync.sendVersionInfo", true);
pref("services.sync.scheduler.singleDeviceInterval", 86400); // 1 day
pref("services.sync.scheduler.idleInterval", 3600); // 1 hour
pref("services.sync.scheduler.activeInterval", 600); // 10 minutes
pref("services.sync.scheduler.immediateInterval", 90); // 1.5 minutes
pref("services.sync.scheduler.idleTime", 300); // 5 minutes
pref("services.sync.errorhandler.networkFailureReportTimeout", 604800); // 1 week
+pref("services.sync.engine.addons", true);
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:.*)$");
pref("services.sync.jpake.serverURL", "https://setup.services.mozilla.com/");
@@ -44,9 +45,10 @@ pref("services.sync.log.logger.network.resources", "Debug");
pref("services.sync.log.logger.service.jpakeclient", "Debug");
pref("services.sync.log.logger.engine.bookmarks", "Debug");
pref("services.sync.log.logger.engine.clients", "Debug");
pref("services.sync.log.logger.engine.forms", "Debug");
pref("services.sync.log.logger.engine.history", "Debug");
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.cryptoDebug", false);
diff --git a/services/sync/tests/tps/all_tests.json b/services/sync/tests/tps/all_tests.json
index 43f4bed..35bcd34 100644
--- a/services/sync/tests/tps/all_tests.json
+++ b/services/sync/tests/tps/all_tests.json
@@ -16,13 +16,16 @@
"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",
"test_client_wipe.js",
- "test_special_tabs.js"
+ "test_special_tabs.js",
+ "test_addon_sanity.js",
+ "test_addon_restartless_xpi.js",
+ "test_addon_nonrestartless_xpi.js"
]
}
diff --git a/services/sync/tests/tps/restartless-xpi.xml b/services/sync/tests/tps/restartless-xpi.xml
new file mode 100644
index 0000000..a6be2ec
--- /dev/null
+++ b/services/sync/tests/tps/restartless-xpi.xml
@@ -0,0 +1,27 @@
+
+
+
+ Restartless Test XPI
+ Extension
+ restartless-xpi@tests.mozilla.org
+ restartless-xpi
+ 1.0
+
+
+ Firefox
+ 1
+ 3.6
+ *
+ {ec8030f7-c20a-464f-9b0e-13a3a9e97384}
+
+ ALL
+
+ http://127.0.0.1:4567/restartless.xpi
+
+ 2009-09-14T04:47:42Z
+
+
+ 2011-09-05T20:42:09Z
+
+
+
diff --git a/services/sync/tests/tps/restartless.xml b/services/sync/tests/tps/restartless.xml
deleted file mode 100644
index a6be2ec..0000000
--- a/services/sync/tests/tps/restartless.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
- Restartless Test XPI
- Extension
- restartless-xpi@tests.mozilla.org
- restartless-xpi
- 1.0
-
-
- Firefox
- 1
- 3.6
- *
- {ec8030f7-c20a-464f-9b0e-13a3a9e97384}
-
- ALL
-
- http://127.0.0.1:4567/restartless.xpi
-
- 2009-09-14T04:47:42Z
-
-
- 2011-09-05T20:42:09Z
-
-
-
diff --git a/services/sync/tests/tps/test_addon_nonrestartless_xpi.js b/services/sync/tests/tps/test_addon_nonrestartless_xpi.js
new file mode 100644
index 0000000..d5d8dff
--- /dev/null
+++ b/services/sync/tests/tps/test_addon_nonrestartless_xpi.js
@@ -0,0 +1,91 @@
+
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test verifies that install of extensions that require restart
+// syncs between profiles.
+
+let phases = {
+ "phase01": "profile1",
+ "phase02": "profile1",
+ "phase03": "profile2",
+ "phase04": "profile2",
+ "phase05": "profile1",
+ "phase06": "profile1",
+ "phase07": "profile2",
+ "phase08": "profile2",
+ "phase09": "profile1",
+ "phase10": "profile1",
+ "phase11": "profile2",
+ "phase12": "profile2",
+ "phase13": "profile1",
+ "phase14": "profile1",
+ "phase15": "profile2",
+ "phase16": "profile2"
+};
+
+const id = "unsigned-xpi@tests.mozilla.org";
+
+Phase("phase01", [
+ [Sync, SYNC_WIPE_SERVER],
+ [Addons.verifyNot, [id]],
+ [Addons.install, ["unsigned-xpi.xml"]],
+ [Addons.verify, [id], STATE_DISABLED],
+]);
+Phase("phase02", [
+ [Addons.verify, [id], STATE_ENABLED],
+ [Sync],
+]);
+Phase("phase03", [
+ [Addons.verifyNot, [id]],
+ [Sync],
+]);
+Phase("phase04", [
+ [Addons.verify, [id], STATE_ENABLED],
+]);
+
+// Now we disable the add-on
+Phase("phase05", [
+ [Addons.setState, [id], STATE_DISABLED]
+]);
+Phase("phase06", [
+ [Addons.verify, [id], STATE_DISABLED],
+ [Sync]
+]);
+Phase("phase07", [
+ [Sync]
+]);
+Phase("phase08", [
+ [Addons.verify, [id], STATE_DISABLED]
+]);
+
+// Now we re-enable it again.
+Phase("phase09", [
+ [Addons.setState, [id], STATE_ENABLED]
+]);
+Phase("phase10", [
+ [Addons.verify, [id], STATE_ENABLED],
+ [Sync]
+]);
+Phase("phase11", [
+ [Sync]
+]);
+Phase("phase12", [
+ [Addons.verify, [id], STATE_ENABLED]
+]);
+
+// And we uninstall it
+Phase("phase13", [
+ [Addons.verify, [id], STATE_ENABLED],
+ [Addons.uninstall, [id]]
+]);
+Phase("phase14", [
+ [Addons.verifyNot, [id]],
+ [Sync]
+]);
+Phase("phase15", [
+ [Sync]
+]);
+Phase("phase16", [
+ [Addons.verifyNot, [id]]
+]);
diff --git a/services/sync/tests/tps/test_addon_restartless_xpi.js b/services/sync/tests/tps/test_addon_restartless_xpi.js
new file mode 100644
index 0000000..3e9b6a8
--- /dev/null
+++ b/services/sync/tests/tps/test_addon_restartless_xpi.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test verifies that install of restartless extensions syncs to
+// other profiles.
+let phases = {
+ "phase01": "profile1",
+ "phase02": "profile2",
+ "phase03": "profile1",
+ "phase04": "profile2",
+ "phase05": "profile1",
+ "phase06": "profile2",
+ "phase07": "profile1",
+ "phase08": "profile2"
+};
+
+const id = "restartless-xpi@tests.mozilla.org";
+
+// Verify install is synced
+Phase("phase01", [
+ [Addons.verifyNot, [id]],
+ [Addons.install, ["restartless-xpi.xml"]],
+ [Addons.verify, [id], STATE_ENABLED],
+ [Sync, SYNC_WIPE_SERVER]
+]);
+Phase("phase02", [
+ [Addons.verifyNot, [id]],
+ [Sync],
+ [Addons.verify, [id], STATE_ENABLED]
+]);
+
+// Now disable and see that is is synced.
+Phase("phase03", [
+ [Addons.setState, [id], STATE_DISABLED],
+ [Addons.verify, [id], STATE_DISABLED],
+ [Sync]
+]);
+Phase("phase04", [
+ [Addons.verify, [id], STATE_ENABLED],
+ [Sync],
+ [Addons.verify, [id], STATE_DISABLED]
+]);
+
+// Enable and see it is synced.
+Phase("phase05", [
+ [Addons.setState, [id], STATE_ENABLED],
+ [Addons.verify, [id], STATE_ENABLED],
+ [Sync]
+]);
+Phase("phase06", [
+ [Sync],
+ [Addons.verify, [id], STATE_ENABLED]
+]);
+
+// Uninstall and see it is synced.
+Phase("phase07", [
+ [Addons.verify, [id], STATE_ENABLED],
+ [Addons.uninstall, [id]],
+ [Addons.verifyNot, [id]],
+ [Sync]
+]);
+Phase("phase08", [
+ [Addons.verify, [id], STATE_ENABLED],
+ [Sync],
+ [Addons.verifyNot, [id]]
+]);
diff --git a/services/sync/tests/tps/unsigned-1.0.xml b/services/sync/tests/tps/unsigned-1.0.xml
deleted file mode 100644
index e311f47..0000000
--- a/services/sync/tests/tps/unsigned-1.0.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
- Unsigned Test XPI
- Extension
- unsigned-xpi@tests.mozilla.org
- unsigned-xpi
- 1.0
-
-
- Firefox
- 1
- 3.6
- *
- {ec8030f7-c20a-464f-9b0e-13a3a9e97384}
-
- ALL
-
- http://127.0.0.1:4567/unsigned-1.0.xpi
-
- 2009-09-14T04:47:42Z
-
-
- 2011-09-05T20:42:09Z
-
-
-
\ No newline at end of file
diff --git a/services/sync/tests/tps/unsigned-1.0.xpi b/services/sync/tests/tps/unsigned-1.0.xpi
deleted file mode 100644
index 51b0047..0000000
Binary files a/services/sync/tests/tps/unsigned-1.0.xpi and /dev/null differ
diff --git a/services/sync/tests/tps/unsigned-xpi.xml b/services/sync/tests/tps/unsigned-xpi.xml
new file mode 100644
index 0000000..614927e
--- /dev/null
+++ b/services/sync/tests/tps/unsigned-xpi.xml
@@ -0,0 +1,27 @@
+
+
+
+ Unsigned Test XPI
+ Extension
+ unsigned-xpi@tests.mozilla.org
+ unsigned-xpi
+ 1.0
+
+
+ Firefox
+ 1
+ 3.6
+ *
+ {ec8030f7-c20a-464f-9b0e-13a3a9e97384}
+
+ ALL
+
+ http://127.0.0.1:4567/unsigned.xpi
+
+ 2009-09-14T04:47:42Z
+
+
+ 2011-09-05T20:42:09Z
+
+
+
diff --git a/services/sync/tests/tps/unsigned.xpi b/services/sync/tests/tps/unsigned.xpi
new file mode 100644
index 0000000..51b0047
Binary files /dev/null and b/services/sync/tests/tps/unsigned.xpi differ
diff --git a/services/sync/tests/unit/head_appinfo.js b/services/sync/tests/unit/head_appinfo.js
index bfdee48..ed06d34 100644
--- a/services/sync/tests/unit/head_appinfo.js
+++ b/services/sync/tests/unit/head_appinfo.js
@@ -1,12 +1,12 @@
-var gProfD;
+let gSyncProfile;
do_load_httpd_js();
-gProfD = do_get_profile();
+gSyncProfile = do_get_profile();
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
// Make sure to provide the right OS so crypto loads the right binaries
let OS = "XPCShell";
if ("@mozilla.org/windows-registry-key;1" in Cc)
OS = "WINNT";
else if ("nsILocalFileMac" in Ci)
@@ -21,17 +21,18 @@ let XULAppInfo = {
version: "1",
appBuildID: "20100621",
platformVersion: "",
platformBuildID: "20100621",
inSafeMode: false,
logConsoleErrors: true,
OS: OS,
XPCOMABI: "noarch-spidermonkey",
- QueryInterface: XPCOMUtils.generateQI([Ci.nsIXULAppInfo, Ci.nsIXULRuntime])
+ 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);
}
diff --git a/services/sync/tests/unit/head_helpers.js b/services/sync/tests/unit/head_helpers.js
index cad7448..66580cc 100644
--- a/services/sync/tests/unit/head_helpers.js
+++ b/services/sync/tests/unit/head_helpers.js
@@ -1,8 +1,14 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Cu.import("resource://services-sync/async.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://services-sync/record.js");
Cu.import("resource://services-sync/engines.js");
var btoa;
let provider = {
getFile: function(prop, persistent) {
persistent.value = true;
@@ -65,16 +71,110 @@ function initTestLogging(level) {
appender.level = Log4Moz.Level.Trace;
// Overwrite any other appenders (e.g. from previous incarnations)
log.ownAppenders = [appender];
log.updateAppenders();
return logStats;
}
+// This is needed for loadAddonTestFunctions().
+let gGlobalScope = this;
+
+/**
+ * Loads the AddonManager test functions by importing its test file.
+ *
+ * This should be called in the global scope of any test file needing to
+ * interface with the AddonManager. It should only be called once, or the
+ * universe will end.
+ */
+function loadAddonTestFunctions() {
+ const path = "../../../../toolkit/mozapps/extensions/test/xpcshell/head_addons.js";
+ let file = do_get_file(path);
+ let uri = Services.io.newFileURI(file);
+ Services.scriptloader.loadSubScript(uri.spec, gGlobalScope);
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
+}
+
+function getAddonInstall(name) {
+ let f = do_get_file("../../../../toolkit/mozapps/extensions/test/xpcshell/addons/"
+ + name + ".xpi");
+ let cb = Async.makeSyncCallback();
+ AddonManager.getInstallForFile(f, cb);
+
+ return Async.waitForSyncCallback(cb);
+}
+
+/**
+ * Obtains an addon from the add-on manager by id.
+ *
+ * This is merely a synchronous wrapper.
+ *
+ * @param id
+ * ID of add-on to fetch
+ * @return addon object on success or undefined or null on failure
+ */
+function getAddonFromAddonManagerByID(id) {
+ let cb = Async.makeSyncCallback();
+ AddonManager.getAddonByID(id, cb);
+ return Async.waitForSyncCallback(cb);
+}
+
+/**
+ * Installs an add-on synchronously from an addonInstall
+ *
+ * @param install addonInstall instance to install
+ */
+function installAddonFromInstall(install) {
+ let cb = Async.makeSyncCallback();
+ let listener = {onInstalled: cb};
+ AddonManager.addAddonListener(listener);
+ install.install();
+ Async.waitForSyncCallback(cb);
+ AddonManager.removeAddonListener(listener);
+
+ do_check_neq(null, install.addon);
+ do_check_neq(null, install.addon.syncGUID);
+
+ return install.addon;
+}
+
+/**
+ * Convenience function to install an add-on from the extensions unit tests.
+ *
+ * @param name
+ * String name of add-on to install. e.g. test_install1
+ * @return addon object that was installed
+ */
+function installAddon(name) {
+ let install = getAddonInstall(name);
+ do_check_neq(null, install);
+ return installAddonFromInstall(install);
+}
+
+/**
+ * Convenience function to uninstall an add-on synchronously.
+ *
+ * @param addon
+ * Addon instance to uninstall
+ */
+function uninstallAddon(addon) {
+ let cb = Async.makeSyncCallback();
+ let listener = {onUninstalled: function(uninstalled) {
+ if (uninstalled.id == addon.id) {
+ AddonManager.removeAddonListener(listener);
+ cb(uninstalled);
+ }
+ }};
+
+ AddonManager.addAddonListener(listener);
+ addon.uninstall();
+ Async.waitForSyncCallback(cb);
+}
+
function FakeFilesystemService(contents) {
this.fakeContents = contents;
let self = this;
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);
callback.call(that);
diff --git a/services/sync/tests/unit/head_http_server.js b/services/sync/tests/unit/head_http_server.js
index da3a190..02676bd 100644
--- a/services/sync/tests/unit/head_http_server.js
+++ b/services/sync/tests/unit/head_http_server.js
@@ -1,15 +1,17 @@
const Cm = Components.manager;
// Shared logging for all HTTP server functions.
Cu.import("resource://services-sync/log4moz.js");
const SYNC_HTTP_LOGGER = "Sync.Test.Server";
const SYNC_API_VERSION = "1.1";
+Cu.import("resource://services-sync/engines.js");
+
// Use the same method that record.js does, which mirrors the server.
// The server returns timestamps with 1/100 sec granularity. Note that this is
// subject to change: see Bug 650435.
function new_timestamp() {
return Math.round(Date.now() / 10) / 100;
}
function return_timestamp(request, response, timestamp) {
diff --git a/services/sync/tests/unit/install1-search.xml b/services/sync/tests/unit/install1-search.xml
new file mode 100644
index 0000000..99e6080
--- /dev/null
+++ b/services/sync/tests/unit/install1-search.xml
@@ -0,0 +1,27 @@
+
+
+
+ Restartless Test Extension
+ Extension
+ addon1@tests.mozilla.org
+ addon1
+ 1.0
+
+
+ Firefox
+ 1
+ 3.6
+ *
+ {3e3ba16c-1675-4e88-b9c8-afef81b3d2ef}
+
+ ALL
+
+ http://127.0.0.1:8888/install1.xpi
+
+ 2009-09-14T04:47:42Z
+
+
+ 2011-09-05T20:42:09Z
+
+
+
diff --git a/services/sync/tests/unit/missing-xpi-search.xml b/services/sync/tests/unit/missing-xpi-search.xml
new file mode 100644
index 0000000..9b547cd
--- /dev/null
+++ b/services/sync/tests/unit/missing-xpi-search.xml
@@ -0,0 +1,27 @@
+
+
+
+ Restartless Test Extension
+ Extension
+ missing-xpi@tests.mozilla.org
+ missing-xpi
+ 1.0
+
+
+ Firefox
+ 1
+ 3.6
+ *
+ {3e3ba16c-1675-4e88-b9c8-afef81b3d2ef}
+
+ ALL
+
+ http://127.0.0.1:8888/THIS_DOES_NOT_EXIST.xpi
+
+ 2009-09-14T04:47:42Z
+
+
+ 2011-09-05T20:42:09Z
+
+
+
diff --git a/services/sync/tests/unit/test_addons_engine.js b/services/sync/tests/unit/test_addons_engine.js
new file mode 100644
index 0000000..a6cc363
--- /dev/null
+++ b/services/sync/tests/unit/test_addons_engine.js
@@ -0,0 +1,117 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Cu.import("resource://gre/modules/AddonManager.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-sync/engines/addons.js");
+
+loadAddonTestFunctions();
+startupManager();
+
+Engines.register(AddonsEngine);
+let engine = Engines.get("addons");
+let reconciler = engine._reconciler;
+let tracker = engine._tracker;
+
+function advance_test() {
+ reconciler._addons = {};
+ reconciler._changes = [];
+
+ let cb = Async.makeSpinningCallback();
+ reconciler.saveState(null, cb);
+ cb.wait();
+
+ run_next_test();
+}
+
+// This is a basic sanity test for the unit test itself. If this breaks, the
+// add-ons API likely changed upstream.
+add_test(function test_addon_install() {
+ _("Ensure basic add-on APIs work as expected.");
+
+ let install = getAddonInstall("test_install1");
+ do_check_neq(install, null);
+ do_check_eq(install.type, "extension");
+ do_check_eq(install.name, "Test 1");
+
+ advance_test();
+});
+
+add_test(function test_find_dupe() {
+ _("Ensure the _findDupe() implementation is sane.");
+
+ // This gets invoked at the top of sync, which is bypassed by this
+ // test, so we do it manually.
+ engine._refreshReconcilerState();
+
+ let addon = installAddon("test_install1");
+
+ let record = {
+ id: Utils.makeGUID(),
+ addonID: addon.id,
+ enabled: true,
+ applicationID: Services.appinfo.ID,
+ source: "amo"
+ };
+
+ let dupe = engine._findDupe(record);
+ do_check_eq(addon.syncGUID, dupe);
+
+ record.id = addon.syncGUID;
+ dupe = engine._findDupe(record);
+ do_check_eq(null, dupe);
+
+ uninstallAddon(addon);
+ advance_test();
+});
+
+add_test(function test_get_changed_ids() {
+ _("Ensure getChangedIDs() has the appropriate behavior.");
+
+ _("Ensure getChangedIDs() returns an empty object by default.");
+ let changes = engine.getChangedIDs();
+ do_check_eq("object", typeof(changes));
+ do_check_eq(0, Object.keys(changes).length);
+
+ _("Ensure tracker changes are populated.");
+ let now = new Date();
+ let changeTime = now.getTime() / 1000;
+ let guid1 = Utils.makeGUID();
+ tracker.addChangedID(guid1, changeTime);
+
+ changes = engine.getChangedIDs();
+ do_check_eq("object", typeof(changes));
+ do_check_eq(1, Object.keys(changes).length);
+ do_check_true(guid1 in changes);
+ do_check_eq(changeTime, changes[guid1]);
+
+ tracker.clearChangedIDs();
+
+ _("Ensure reconciler changes are populated.");
+ let addon = installAddon("test_install1");
+ tracker.clearChangedIDs(); // Just in case.
+ changes = engine.getChangedIDs();
+ do_check_eq("object", typeof(changes));
+ do_check_eq(1, Object.keys(changes).length);
+ do_check_true(addon.syncGUID in changes);
+ do_check_true(changes[addon.syncGUID] > changeTime);
+
+ let oldTime = changes[addon.syncGUID];
+ let guid2 = addon.syncGUID;
+ uninstallAddon(addon);
+ changes = engine.getChangedIDs();
+ do_check_eq(1, Object.keys(changes).length);
+ do_check_true(guid2 in changes);
+ do_check_true(changes[guid2] > oldTime);
+
+ advance_test();
+});
+
+function run_test() {
+ initTestLogging("Trace");
+ Log4Moz.repository.getLogger("Sync.Engine.Addons").level = Log4Moz.Level.Trace;
+ advance_test();
+}
diff --git a/services/sync/tests/unit/test_addons_store.js b/services/sync/tests/unit/test_addons_store.js
new file mode 100644
index 0000000..78229f1
--- /dev/null
+++ b/services/sync/tests/unit/test_addons_store.js
@@ -0,0 +1,293 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Cu.import("resource://services-sync/engines/addons.js");
+Cu.import("resource://services-sync/ext/Preferences.js");
+
+const HTTP_PORT = 8888;
+
+let prefs = new Preferences();
+
+Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
+prefs.set("extensions.getAddons.get.url", "http://localhost:8888/search/guid:%IDS%");
+loadAddonTestFunctions();
+startupManager();
+
+Engines.register(AddonsEngine);
+let engine = Engines.get("addons");
+let tracker = engine._tracker;
+let store = engine._store;
+let reconciler = engine._reconciler;
+
+/**
+ * Create a AddonsRec for this application with the fields specified.
+ *
+ * @param id Sync GUID of record
+ * @param addonId ID of add-on
+ * @param enabled Boolean whether record is enabled
+ * @param deleted Boolean whether record was deleted
+ */
+function createRecordForThisApp(id, addonId, enabled, deleted) {
+ return {
+ id: id,
+ addonID: addonId,
+ enabled: enabled,
+ deleted: !!deleted,
+ applicationID: Services.appinfo.ID,
+ source: "amo"
+ };
+}
+
+function createAndStartHTTPServer(port) {
+ try {
+ let server = new nsHttpServer();
+
+ let install1_xpi = "../../../../toolkit/mozapps/extensions/test/xpcshell/" +
+ "addons/test_install1.xpi";
+
+ server.registerFile("/search/guid:addon1%40tests.mozilla.org",
+ do_get_file("install1-search.xml"));
+ server.registerFile("/install1.xpi", do_get_file(install1_xpi));
+
+ server.registerFile("/search/guid:missing-xpi%40tests.mozilla.org",
+ do_get_file("missing-xpi-search.xml"));
+
+ server.start(port);
+
+ return server;
+ } catch (ex) {
+ _("Got exception starting HTTP server on port " + port);
+ _("Error: " + Utils.exceptionStr(ex));
+ do_throw(ex);
+ }
+}
+
+function run_test() {
+ initTestLogging("Trace");
+ Log4Moz.repository.getLogger("Sync.Engine.Addons").level = Log4Moz.Level.Trace;
+
+ run_next_test();
+}
+
+add_test(function test_get_all_ids() {
+ _("Ensures that getAllIDs() returns an appropriate set.");
+
+ engine._refreshReconcilerState();
+
+ let addon1 = installAddon("test_install1");
+ let addon2 = installAddon("test_install2_1");
+
+ let ids = store.getAllIDs();
+ do_check_eq("object", typeof(ids));
+ do_check_eq(2, Object.keys(ids).length);
+ do_check_true(addon1.syncGUID in ids);
+ do_check_true(addon2.syncGUID in ids);
+
+ uninstallAddon(addon1);
+ uninstallAddon(addon2);
+
+ run_next_test();
+});
+
+add_test(function test_change_item_id() {
+ _("Ensures that changeItemID() works properly.");
+
+ let addon = installAddon("test_install1");
+
+ let oldID = addon.syncGUID;
+ let newID = Utils.makeGUID();
+
+ store.changeItemID(oldID, newID);
+
+ let newAddon = getAddonFromAddonManagerByID(addon.id);
+ do_check_neq(null, newAddon);
+ do_check_eq(newID, newAddon.syncGUID);
+
+ uninstallAddon(newAddon);
+
+ run_next_test();
+});
+
+add_test(function test_create() {
+ _("Ensure creating/installing an add-on from a record works.");
+
+ let server = createAndStartHTTPServer(HTTP_PORT);
+
+ let addon = installAddon("test_install1");
+ let id = addon.id;
+ uninstallAddon(addon);
+
+ let guid = Utils.makeGUID();
+ let record = createRecordForThisApp(guid, id, true, false);
+
+ let failed = store.applyIncomingBatch([record]);
+ do_check_eq(0, failed.length);
+
+ let newAddon = getAddonFromAddonManagerByID(id);
+ do_check_neq(null, newAddon);
+ do_check_eq(guid, newAddon.syncGUID);
+ do_check_false(newAddon.userDisabled);
+
+ uninstallAddon(newAddon);
+
+ server.stop(run_next_test);
+});
+
+add_test(function test_create_missing_search() {
+ _("Ensures that failed add-on searches are handled gracefully.");
+
+ let server = createAndStartHTTPServer(HTTP_PORT);
+
+ // The handler for this ID is not installed, so a search should 404.
+ const id = "missing@tests.mozilla.org";
+ let guid = Utils.makeGUID();
+ let record = createRecordForThisApp(guid, id, true, false);
+
+ let failed = store.applyIncomingBatch([record]);
+ do_check_eq(1, failed.length);
+ do_check_eq(guid, failed[0]);
+
+ let addon = getAddonFromAddonManagerByID(id);
+ do_check_eq(null, addon);
+
+ server.stop(run_next_test);
+});
+
+add_test(function test_create_bad_install() {
+ _("Ensures that add-ons without a valid install are handled gracefully.");
+
+ let server = createAndStartHTTPServer(HTTP_PORT);
+
+ // The handler returns a search result but the XPI will 404.
+ const id = "missing-xpi@tests.mozilla.org";
+ let guid = Utils.makeGUID();
+ let record = createRecordForThisApp(guid, id, true, false);
+
+ let failed = store.applyIncomingBatch([record]);
+ do_check_eq(1, failed.length);
+ do_check_eq(guid, failed[0]);
+
+ let addon = getAddonFromAddonManagerByID(id);
+ do_check_eq(null, addon);
+
+ server.stop(run_next_test);
+});
+
+add_test(function test_remove() {
+ _("Ensure removing add-ons from deleted records works.");
+
+ let addon = installAddon("test_install1");
+ let record = createRecordForThisApp(addon.syncGUID, addon.id, true, true);
+
+ let failed = store.applyIncomingBatch([record]);
+ do_check_eq(0, failed.length);
+
+ let newAddon = getAddonFromAddonManagerByID(addon.id);
+ do_check_eq(null, newAddon);
+
+ run_next_test();
+});
+
+add_test(function test_apply_enabled() {
+ _("Ensures that changes to the userEnabled flag apply.");
+
+ let addon = installAddon("test_install1");
+ do_check_false(addon.userDisabled);
+
+ let records = [];
+ records.push(createRecordForThisApp(addon.syncGUID, addon.id, false, false));
+ _("Ensure application of a disable record works as expected.");
+ let failed = store.applyIncomingBatch(records);
+ do_check_eq(0, failed.length);
+ addon = getAddonFromAddonManagerByID(addon.id);
+ do_check_true(addon.userDisabled);
+
+ records = [];
+
+ _("Ensure enable record works as expected.");
+ records.push(createRecordForThisApp(addon.syncGUID, addon.id, true, false));
+ failed = store.applyIncomingBatch(records);
+ do_check_eq(0, failed.length);
+ addon = getAddonFromAddonManagerByID(addon.id);
+ do_check_false(addon.userDisabled);
+
+ uninstallAddon(addon);
+
+ run_next_test();
+});
+
+add_test(function test_ignore_different_appid() {
+ _("Ensure that incoming records with a different application ID are ignored.");
+
+ // We test by creating a record that should result in an update.
+ let addon = installAddon("test_install1");
+ do_check_false(addon.userDisabled);
+
+ let record = createRecordForThisApp(addon.syncGUID, addon.id, false, false);
+ record.applicationID = "FAKE_ID";
+
+ let failed = store.applyIncomingBatch([record]);
+ do_check_eq(0, failed.length);
+
+ let newAddon = getAddonFromAddonManagerByID(addon.id);
+ do_check_false(addon.userDisabled);
+
+ uninstallAddon(addon);
+
+ run_next_test();
+});
+
+add_test(function test_ignore_unknown_source() {
+ _("Ensure incoming records with unknown source are ignored.");
+
+ let addon = installAddon("test_install1");
+
+ let record = createRecordForThisApp(addon.syncGUID, addon.id, false, false);
+ record.source = "DUMMY_SOURCE";
+
+ let failed = store.applyIncomingBatch([record]);
+ do_check_eq(0, failed.length);
+
+ let newAddon = getAddonFromAddonManagerByID(addon.id);
+ do_check_false(addon.userDisabled);
+
+ uninstallAddon(addon);
+
+ run_next_test();
+});
+
+add_test(function test_apply_uninstall() {
+ _("Ensures that uninstalling an add-on from a record works.");
+
+ let addon = installAddon("test_install1");
+
+ let records = [];
+ records.push(createRecordForThisApp(addon.syncGUID, addon.id, true, true));
+ let failed = store.applyIncomingBatch(records);
+ do_check_eq(0, failed.length);
+
+ addon = getAddonFromAddonManagerByID(addon.id);
+ do_check_eq(null, addon);
+
+ run_next_test();
+});
+
+/*
+add_test(function test_wipe() {
+ _("Ensures that wiping causes add-ons to be uninstalled.");
+
+ let addon1 = installAddon("test_install1");
+ let addon2 = installAddon("test_install2_1");
+
+ store.wipe();
+
+ let addon = getAddonFromAddonManagerByID(addon1.id);
+ do_check_eq(null, addon);
+ addon = getAddonFromAddonManagerByID(addon2.id);
+ do_check_eq(null, addon);
+
+ run_next_test();
+});
+*/
diff --git a/services/sync/tests/unit/test_addons_tracker.js b/services/sync/tests/unit/test_addons_tracker.js
new file mode 100644
index 0000000..76634e5
--- /dev/null
+++ b/services/sync/tests/unit/test_addons_tracker.js
@@ -0,0 +1,167 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Cu.import("resource://services-sync/engines/addons.js");
+Cu.import("resource://services-sync/constants.js");
+Cu.import("resource://services-sync/util.js");
+Cu.import("resource://gre/modules/AddonManager.jsm");
+
+loadAddonTestFunctions();
+startupManager();
+Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
+
+Engines.register(AddonsEngine);
+let engine = Engines.get("addons");
+let reconciler = engine._reconciler;
+let store = engine._store;
+let tracker = engine._tracker;
+
+const addon1ID = "addon1@tests.mozilla.org";
+
+function cleanup_and_advance() {
+ Svc.Obs.notify("weave:engine:stop-tracking");
+ tracker.observe(null, "weave:engine:stop-tracking");
+
+ tracker.resetScore();
+ tracker.clearChangedIDs();
+
+ reconciler._addons = {};
+ reconciler._changes = [];
+ let cb = Async.makeSpinningCallback();
+ reconciler.saveState(null, cb);
+ cb.wait();
+
+ run_next_test();
+}
+
+function run_test() {
+ initTestLogging("Trace");
+ Log4Moz.repository.getLogger("Sync.Engine.Addons").level = Log4Moz.Level.Trace;
+
+ cleanup_and_advance();
+}
+
+add_test(function test_empty() {
+ _("Verify the tracker is empty to start with.");
+
+ do_check_eq(0, Object.keys(tracker.changedIDs).length);
+ do_check_eq(0, tracker.score);
+
+ cleanup_and_advance();
+});
+
+add_test(function test_not_tracking() {
+ _("Ensures the tracker doesn't do anything when it isn't tracking.");
+
+ let addon = installAddon("test_install1");
+ uninstallAddon(addon);
+
+ do_check_eq(0, Object.keys(tracker.changedIDs).length);
+ do_check_eq(0, tracker.score);
+
+ cleanup_and_advance();
+});
+
+add_test(function test_track_install() {
+ _("Ensure that installing an add-on notifies tracker.");
+
+ Svc.Obs.notify("weave:engine:start-tracking");
+
+ do_check_eq(0, tracker.score);
+ let addon = installAddon("test_install1");
+ let changed = tracker.changedIDs;
+
+ do_check_eq(1, Object.keys(changed).length);
+ do_check_true(addon.syncGUID in changed);
+ do_check_eq(SCORE_INCREMENT_XLARGE, tracker.score);
+
+ uninstallAddon(addon);
+ cleanup_and_advance();
+});
+
+add_test(function test_track_uninstall() {
+ _("Ensure that uninstalling an add-on notifies tracker.");
+
+ let addon = installAddon("test_install1");
+ let guid = addon.syncGUID;
+ do_check_eq(0, tracker.score);
+
+ Svc.Obs.notify("weave:engine:start-tracking");
+
+ uninstallAddon(addon);
+ let changed = tracker.changedIDs;
+ do_check_eq(1, Object.keys(changed).length);
+ do_check_true(guid in changed);
+ do_check_eq(SCORE_INCREMENT_XLARGE, tracker.score);
+
+ cleanup_and_advance();
+});
+
+// The following don't work for an unknown reason. The listeners for disabling
+// never get invoked. Weird
+// TODO fix this
+/*
+add_test(function test_track_user_disable() {
+ _("Ensure that tracker sees disabling of add-on");
+
+ let addon = installAddon("test_install1");
+ do_check_false(addon.userDisabled);
+
+ Svc.Obs.notify("weave:engine:start-tracking");
+ do_check_eq(0, tracker.score);
+
+ let cb = Async.makeSyncCallback();
+
+ let listener = {
+ onDisabled: function(disabled) {
+ _("onDisabled");
+ if (disabled.id == addon.id) {
+ AddonManager.removeAddonListener(listener);
+ cb();
+ }
+ },
+ onDisabling: function(disabling) {
+ _("onDisabling add-on");
+ }
+ };
+ AddonManager.addAddonListener(listener);
+
+ _("Disabling add-on");
+ addon.userDisabled = true;
+ _("Disabling started...");
+ Async.waitForSyncCallback(cb);
+
+ let changed = tracker.changedIDs;
+ do_check_eq(1, Object.keys(changed).length);
+ do_check_true(addon.syncGUID in changed);
+ do_check_eq(SCORE_INCREMENT_XLARGE, tracker.score);
+
+ uninstallAddon(addon);
+ cleanup_and_advance();
+});
+
+add_test(function test_track_enable() {
+ _("Ensure that enabling a disabled add-on notifies tracker.");
+
+ let addon = installAddon("test_install1");
+ addon.userDisabled = true;
+ store._sleep(0);
+
+ do_check_eq(0, tracker.score);
+
+ Svc.Obs.notify("weave:engine:start-tracking");
+ addon.userDisabled = false;
+ store._sleep(0);
+
+ let changed = tracker.changedIDs;
+ do_check_eq(1, Object.keys(changed).length);
+ do_check_true(addon.syncGUID in changed);
+ do_check_eq(SCORE_INCREMENT_XLARGE, tracker.score);
+
+ uninstallAddon(addon);
+ cleanup_and_advance();
+});
+*/
+
diff --git a/services/sync/tests/unit/test_places_guid_downgrade.js b/services/sync/tests/unit/test_places_guid_downgrade.js
index 38e4c7e..c080219 100644
--- a/services/sync/tests/unit/test_places_guid_downgrade.js
+++ b/services/sync/tests/unit/test_places_guid_downgrade.js
@@ -10,22 +10,22 @@ const storageSvc = Cc["@mozilla.org/storage/service;1"]
const fxuri = Utils.makeURI("http://getfirefox.com/");
const tburi = Utils.makeURI("http://getthunderbird.com/");
function setPlacesDatabase(aFileName) {
removePlacesDatabase();
_("Copying over places.sqlite.");
let file = do_get_file(aFileName);
- file.copyTo(gProfD, kDBName);
+ file.copyTo(gSyncProfile, kDBName);
}
function removePlacesDatabase() {
_("Removing places.sqlite.");
- let file = gProfD.clone();
+ let file = gSyncProfile.clone();
file.append(kDBName);
try {
file.remove(false);
} catch (ex) {
// Windows is awesome. NOT.
}
}
@@ -33,17 +33,17 @@ Svc.Obs.add("places-shutdown", function () {
do_timeout(0, removePlacesDatabase);
});
// Verify initial database state. Function borrowed from places tests.
function test_initial_state() {
// Mostly sanity checks our starting DB to make sure it's setup as we expect
// it to be.
- let dbFile = gProfD.clone();
+ let dbFile = gSyncProfile.clone();
dbFile.append(kDBName);
let db = storageSvc.openUnsharedDatabase(dbFile);
let stmt = db.createStatement("PRAGMA journal_mode");
do_check_true(stmt.executeStep());
// WAL journal mode should have been unset this database when it was migrated
// down to v10.
do_check_neq(stmt.getString(0).toLowerCase(), "wal");
diff --git a/services/sync/tests/unit/xpcshell.ini b/services/sync/tests/unit/xpcshell.ini
index f8cc583..d8fce75 100644
--- a/services/sync/tests/unit/xpcshell.ini
+++ b/services/sync/tests/unit/xpcshell.ini
@@ -1,14 +1,17 @@
[DEFAULT]
head = head_appinfo.js head_helpers.js head_http_server.js
tail =
[test_Observers.js]
[test_Preferences.js]
+[test_addons_engine.js]
+[test_addons_store.js]
+[test_addons_tracker.js]
[test_async_chain.js]
[test_async_querySpinningly.js]
[test_auth_manager.js]
[test_bookmark_batch_fail.js]
[test_bookmark_engine.js]
[test_bookmark_legacy_microsummaries_support.js]
[test_bookmark_livemarks.js]
[test_bookmark_order.js]
diff --git a/services/sync/tps/extensions/tps/modules/addons.jsm b/services/sync/tps/extensions/tps/modules/addons.jsm
index 9324b7a..4139127 100644
--- a/services/sync/tps/extensions/tps/modules/addons.jsm
+++ b/services/sync/tps/extensions/tps/modules/addons.jsm
@@ -41,18 +41,16 @@ const CI = Components.interfaces;
const CU = Components.utils;
CU.import("resource://gre/modules/AddonManager.jsm");
CU.import("resource://gre/modules/AddonRepository.jsm");
CU.import("resource://gre/modules/Services.jsm");
CU.import("resource://services-sync/async.js");
CU.import("resource://services-sync/util.js");
CU.import("resource://tps/logger.jsm");
-var XPIProvider = CU.import("resource://gre/modules/XPIProvider.jsm")
- .XPIProvider;
const ADDONSGETURL = 'http://127.0.0.1:4567/';
const STATE_ENABLED = 1;
const STATE_DISABLED = 2;
function GetFileAsText(file)
{
let channel = Services.io.newChannel(file, null, null);
@@ -83,60 +81,48 @@ function Addon(TPS, id) {
Addon.prototype = {
_addons_requiring_restart: [],
_addons_pending_install: [],
Delete: function() {
// find our addon locally
let cb = Async.makeSyncCallback();
- XPIProvider.getAddonsByTypes(null, cb);
- let results = Async.waitForSyncCallback(cb);
- var addon;
- var id = this.id;
- results.forEach(function(result) {
- if (result.id == id) {
- addon = result;
- }
- });
+ AddonManager.getAddonByID(this.id, cb);
+ let addon = Async.waitForSyncCallback(cb);
Logger.AssertTrue(!!addon, 'could not find addon ' + this.id + ' to uninstall');
addon.uninstall();
},
Find: function(state) {
- let cb = Async.makeSyncCallback();
let addon_found = false;
- var that = this;
+ let that = this;
- var log_addon = function(addon) {
+ let log_addon = function(addon) {
that.addon = addon;
Logger.logInfo('addon ' + addon.id + ' found, isActive: ' + addon.isActive);
if (state == STATE_ENABLED || state == STATE_DISABLED) {
Logger.AssertEqual(addon.isActive,
state == STATE_ENABLED ? true : false,
"addon " + that.id + " has an incorrect enabled state");
}
};
- // first look in the list of all addons
- XPIProvider.getAddonsByTypes(null, cb);
- let addonlist = Async.waitForSyncCallback(cb);
- addonlist.forEach(function(addon) {
- if (addon.id == that.id) {
+ let cb = Async.makeSyncCallback();
+ AddonManager.getAddonByID(this.id, cb);
+ let addon = Async.waitForSyncCallback(cb);
+ if (addon) {
addon_found = true;
log_addon.call(that, addon);
- }
- });
-
- if (!addon_found) {
+ } else {
// then look in the list of recent installs
- cb = Async.makeSyncCallback();
- XPIProvider.getInstallsByTypes(null, cb);
+ let cb = Async.makeSyncCallback();
+ AddonManager.getInstallsByTypes(null, cb);
addonlist = Async.waitForSyncCallback(cb);
- for (var i in addonlist) {
+ for (let i in addonlist) {
if (addonlist[i].addon && addonlist[i].addon.id == that.id &&
addonlist[i].state == AddonManager.STATE_INSTALLED) {
addon_found = true;
log_addon.call(that, addonlist[i].addon);
}
}
}
@@ -173,17 +159,18 @@ Addon.prototype = {
Logger.AssertTrue(install_addons,
"no addons found for id " + this.id);
Logger.AssertEqual(install_addons.length,
1,
"multiple addons found for id " + this.id);
let addon = install_addons[0];
Logger.logInfo(JSON.stringify(addon), null, ' ');
- if (XPIProvider.installRequiresRestart(addon)) {
+
+ if (addon.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_INSTALL) {
this._addons_requiring_restart.push(addon.id);
}
// Start installing the addon asynchronously; finish up in
// onInstallEnded(), onInstallFailed(), or onDownloadFailed().
this._addons_pending_install.push(addon.id);
this.TPS.StartAsyncOperation();
diff --git a/testing/tps/tps/mozhttpd.py b/testing/tps/tps/mozhttpd.py
index daccf8f..6171d28 100644
--- a/testing/tps/tps/mozhttpd.py
+++ b/testing/tps/tps/mozhttpd.py
@@ -51,17 +51,43 @@ DOCROOT = '.'
class EasyServer(ThreadingMixIn, BaseHTTPServer.HTTPServer):
allow_reuse_address = True
class MozRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
def translate_path(self, path):
# It appears that the default path is '/' and os.path.join makes the '/'
o = urlparse(path)
- return "%s%s" % ('' if sys.platform == 'win32' else '/', '/'.join([i.strip('/') for i in (DOCROOT, o.path)]))
+
+ sep = '/'
+ if sys.platform == 'win32':
+ sep = ''
+
+ ret = '%s%s' % ( sep, DOCROOT.strip('/') )
+
+ # Stub out addons.mozilla.org search API, which is used when installing
+ # add-ons. The version is hard-coded because we want tests to fail when
+ # the API updates so we can update our stubbed files with the changes.
+ if o.path.find('/en-US/firefox/api/1.5/search/guid:') == 0:
+ ids = urllib.unquote(o.path[len('/en-US/firefox/api/1.5/search/guid:'):])
+
+ if ids.count(',') > 0:
+ raise Exception('Only searching for single ids is currently supported.')
+
+ base = ids
+ at_loc = ids.find('@')
+ if at_loc > 0:
+ base = ids[0:at_loc]
+
+ ret += '/%s.xml' % base
+
+ else:
+ ret += '/%s' % o.path.strip('/')
+
+ return ret
# I found on my local network that calls to this were timing out
# I believe all of these calls are from log_message
def address_string(self):
return "a.b.c.d"
# This produces a LOT of noise
def log_message(self, format, *args):
diff --git a/testing/tps/tps/testrunner.py b/testing/tps/tps/testrunner.py
index 4ecf528..0cecc4b 100644
--- a/testing/tps/tps/testrunner.py
+++ b/testing/tps/tps/testrunner.py
@@ -84,25 +84,30 @@ class TPSTestRunner(object):
default_env = { 'MOZ_CRASHREPORTER_DISABLE': '1',
'GNOME_DISABLE_CRASH_DIALOG': '1',
'XRE_NO_WINDOWS_CRASH_DIALOG': '1',
'MOZ_NO_REMOTE': '1',
'XPCOM_DEBUG_BREAK': 'warn',
}
default_preferences = { 'app.update.enabled' : False,
+ 'extensions.getAddons.get.url':
+ 'http://127.0.0.1:4567/en-US/firefox/api/%API_VERSION%/search/guid:%IDS%',
+ 'extensions.logging.enabled': True,
'extensions.update.enabled' : False,
'extensions.update.notifyUser' : False,
'browser.shell.checkDefaultBrowser' : False,
'browser.tabs.warnOnClose' : False,
'browser.warnOnQuit': False,
'browser.sessionstore.resume_from_crash': False,
+ 'services.sync.addon.ignoreRepositoryChecking': True,
'services.sync.firstSync': 'notReady',
'services.sync.lastversion': '1.0',
'services.sync.log.rootLogger': 'Trace',
+ 'services.sync.log.logger.engine.addons': 'Trace',
'services.sync.log.logger.service.main': 'Trace',
'services.sync.log.logger.engine.bookmarks': 'Trace',
'services.sync.log.appender.console': 'Trace',
'services.sync.log.appender.debugLog.enabled': True,
'browser.dom.window.dump.enabled': True,
# Allow installing extensions dropped into the profile folder
'extensions.autoDisableScopes': 10,
# Don't open a dialog to show available add-on updates
diff --git a/toolkit/mozapps/extensions/AddonManager.jsm b/toolkit/mozapps/extensions/AddonManager.jsm
index 1c53330..8915728 100644
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -1399,16 +1399,38 @@ var AddonManager = {
* @return An array of add-on IDs
*/
getStartupChanges: function AM_getStartupChanges(aType) {
if (!(aType in AddonManagerInternal.startupChanges))
return [];
return AddonManagerInternal.startupChanges[aType].slice(0);
},
+ /**
+ * Returns an object describing all startup changes.
+ *
+ * Object keys are the AddonManager.STARTUP_CHANGE_* constants. Values
+ * are arrays of add-on IDs.
+ *
+ * The object is copied from the source of truth, so it is safe to
+ * mutate.
+ */
+ getAllStartupChanges: function AM_getAllStartupChanges() {
+ let result = {};
+ for (let [k, v] in Iterator(AddonManagerInternal.startupChanges)) {
+ if (!v || !v.length) {
+ continue;
+ }
+
+ result[k] = v.slice(0);
+ }
+
+ return result;
+ },
+
getAddonByID: function AM_getAddonByID(aId, aCallback) {
AddonManagerInternal.getAddonByID(aId, aCallback);
},
getAddonBySyncGUID: function AM_getAddonBySyncGUID(aId, aCallback) {
AddonManagerInternal.getAddonBySyncGUID(aId, aCallback);
},
diff --git a/toolkit/mozapps/extensions/XPIProvider.jsm b/toolkit/mozapps/extensions/XPIProvider.jsm
index e3c266c..ffcaa9f 100644
--- a/toolkit/mozapps/extensions/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/XPIProvider.jsm
@@ -818,16 +818,26 @@ function loadManifestFromRDF(aUri, aStream) {
}
else {
addon.userDisabled = false;
addon.softDisabled = addon.blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED;
}
addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT;
+ // Generate random GUID used for Sync.
+ let rng = Cc["@mozilla.org/security/random-generator;1"].
+ createInstance(Ci.nsIRandomGenerator);
+ let bytes = rng.generateRandomBytes(9);
+ let byte_string = [String.fromCharCode(byte) for each (byte in bytes)]
+ .join("");
+ // Base64 encode
+ addon.syncGUID = btoa(byte_string).replace('+', '-', 'g')
+ .replace('/', '_', 'g');
+
return addon;
}
/**
* Loads an AddonInternal object from an add-on extracted in a directory.
*
* @param aDir
* The nsIFile directory holding the add-on
@@ -5282,28 +5292,16 @@ var XPIDatabase = {
* Synchronously adds an AddonInternal's metadata to the database.
*
* @param aAddon
* AddonInternal to add
* @param aDescriptor
* The file descriptor of the add-on
*/
addAddonMetadata: function XPIDB_addAddonMetadata(aAddon, aDescriptor) {
- // Create a GUID if one does not exist
- if (!aAddon.syncGUID) {
- let rng = Cc["@mozilla.org/security/random-generator;1"].
- createInstance(Ci.nsIRandomGenerator);
- let bytes = rng.generateRandomBytes(9);
- let byte_string = [String.fromCharCode(byte) for each (byte in bytes)]
- .join("");
- // Base64 encode
- aAddon.syncGUID = btoa(byte_string).replace('+', '-', 'g')
- .replace('/', '_', 'g');
- }
-
// If there is no DB yet then forcibly create one
if (!this.connection)
this.openConnection(false, true);
this.beginTransaction();
var self = this;
function insertLocale(aLocale) {
@@ -7546,17 +7544,20 @@ function AddonWrapper(aAddon) {
return val;
});
this.__defineSetter__("syncGUID", function(val) {
if (aAddon.syncGUID == val)
return val;
+ if (aAddon instanceof DBAddonInternal) {
XPIDatabase.setAddonSyncGUID(aAddon, val);
+ }
+
aAddon.syncGUID = val;
return val;
});
this.__defineGetter__("install", function() {
if (!("_install" in aAddon) || !aAddon._install)
return null;
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_install.js b/toolkit/mozapps/extensions/test/xpcshell/test_install.js
index 5daddb4..bd12e8f 100644
--- a/toolkit/mozapps/extensions/test/xpcshell/test_install.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_install.js
@@ -67,17 +67,17 @@ function run_test_1() {
do_check_neq(install, null);
do_check_eq(install.linkedInstalls, null);
do_check_eq(install.type, "extension");
do_check_eq(install.version, "1.0");
do_check_eq(install.name, "Test 1");
do_check_eq(install.state, AddonManager.STATE_DOWNLOADED);
do_check_true(install.addon.hasResource("install.rdf"));
- do_check_eq(install.addon.syncGUID, null);
+ do_check_neq(install.addon.syncGUID, null);
do_check_eq(install.addon.install, install);
do_check_eq(install.addon.size, ADDON1_SIZE);
do_check_true(hasFlag(install.addon.operationsRequiringRestart,
AddonManager.OP_NEEDS_RESTART_INSTALL));
let file = do_get_addon("test_install1");
let uri = Services.io.newFileURI(file).spec;
do_check_eq(install.addon.getResourceURI("install.rdf").spec, "jar:" + uri + "!/install.rdf");
do_check_eq(install.addon.iconURL, "jar:" + uri + "!/icon.png");