diff --git a/toolkit/mozapps/extensions/internal/XPIProvider.jsm b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
index 7c501b6e3e8..e0af60f8292 100644
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -98,6 +98,7 @@ const PREF_INSTALL_DISTRO_ADDONS = "extensions.installDistroAddons";
const PREF_BRANCH_INSTALLED_ADDON = "extensions.installedDistroAddon.";
const PREF_SHOWN_SELECTION_UI = "extensions.shownSelectionUI";
const PREF_INTERPOSITION_ENABLED = "extensions.interposition.enabled";
+const PREF_SYSTEM_ADDON_SET = "extensions.systemAddonSet";
const PREF_EM_MIN_COMPAT_APP_VERSION = "extensions.minCompatibleAppVersion";
const PREF_EM_MIN_COMPAT_PLATFORM_VERSION = "extensions.minCompatiblePlatformVersion";
@@ -115,6 +116,7 @@ const URI_EXTENSION_STRINGS = "chrome://mozapps/locale/extensions/exte
const STRING_TYPE_NAME = "type.%ID%.name";
const DIR_EXTENSIONS = "extensions";
+const DIR_SYSTEM_ADDONS = "features";
const DIR_STAGE = "staged";
const DIR_TRASH = "trash";
@@ -130,6 +132,8 @@ const KEY_TEMPDIR = "TmpD";
const KEY_APP_DISTRIBUTION = "XREAppDist";
const KEY_APP_PROFILE = "app-profile";
+const KEY_APP_SYSTEM_ADDONS = "app-system-addons";
+const KEY_APP_SYSTEM_DEFAULTS = "app-system-defaults";
const KEY_APP_GLOBAL = "app-global";
const KEY_APP_SYSTEM_LOCAL = "app-system-local";
const KEY_APP_SYSTEM_SHARE = "app-system-share";
@@ -1968,7 +1972,7 @@ this.XPIStates = {
for (let location of XPIProvider.installLocations) {
// The list of add-on like file/directory names in the install location.
- let addons = location.addonLocations;
+ let addons = location.getAddonLocations();
// The results of scanning this location.
let foundAddons = new SerializableMap();
@@ -2324,6 +2328,28 @@ this.XPIProvider = {
XPIProvider.installLocationsByName[location.name] = location;
}
+ function addSystemAddonInstallLocation(aName, aKey, aPaths, aScope) {
+ try {
+ var dir = FileUtils.getDir(aKey, aPaths);
+ }
+ catch (e) {
+ // Some directories aren't defined on some platforms, ignore them
+ logger.debug("Skipping unavailable install location " + aName);
+ return;
+ }
+
+ try {
+ var location = new SystemAddonInstallLocation(aName, dir, aScope, aAppChanged !== false);
+ }
+ catch (e) {
+ logger.warn("Failed to add system add-on install location " + aName, e);
+ return;
+ }
+
+ XPIProvider.installLocations.push(location);
+ XPIProvider.installLocationsByName[location.name] = location;
+ }
+
function addRegistryInstallLocation(aName, aRootkey, aScope) {
try {
var location = new WinRegInstallLocation(aName, aRootkey, aScope);
@@ -2366,6 +2392,14 @@ this.XPIProvider = {
[DIR_EXTENSIONS],
AddonManager.SCOPE_PROFILE, false);
+ addSystemAddonInstallLocation(KEY_APP_SYSTEM_ADDONS, KEY_PROFILEDIR,
+ [DIR_SYSTEM_ADDONS],
+ AddonManager.SCOPE_PROFILE);
+
+ addDirectoryInstallLocation(KEY_APP_SYSTEM_DEFAULTS, KEY_APP_DISTRIBUTION,
+ [DIR_SYSTEM_ADDONS],
+ AddonManager.SCOPE_PROFILE, true);
+
if (enabledScopes & AddonManager.SCOPE_USER) {
addDirectoryInstallLocation(KEY_APP_SYSTEM_USER, "XREUSysExt",
[Services.appinfo.ID],
@@ -6768,7 +6802,7 @@ function DirectoryInstallLocation(aName, aDirectory, aScope) {
this._IDToFileMap = {};
this._linkedAddons = [];
- if (!aDirectory.exists())
+ if (!aDirectory || !aDirectory.exists())
return;
if (!aDirectory.isDirectory())
throw new Error("Location must be a directory.");
@@ -6893,7 +6927,7 @@ DirectoryInstallLocation.prototype = {
/**
* Gets an array of nsIFiles for add-ons installed in this location.
*/
- get addonLocations() {
+ getAddonLocations: function() {
let locations = new Map();
for (let id in this._IDToFileMap) {
locations.set(id, this._IDToFileMap[id].clone());
@@ -7212,6 +7246,128 @@ Object.assign(MutableDirectoryInstallLocation.prototype, {
},
});
+/**
+ * An object which identifies a directory install location for system add-ons.
+ * The location consists of a directory which contains the add-ons installed in
+ * the location.
+ *
+ * @param aName
+ * The string identifier for the install location
+ * @param aDirectory
+ * The nsIFile directory for the install location
+ * @param aScope
+ * The scope of add-ons installed in this location
+ * @param aResetSet
+ * True to throw away the current add-on set
+ */
+function SystemAddonInstallLocation(aName, aDirectory, aScope, aResetSet) {
+ this._baseDir = aDirectory;
+
+ if (aResetSet) {
+ this._addonSet = { schema: 1, addons: {} };
+ this._saveAddonSet(this._addonSet);
+ }
+ else {
+ this._addonSet = this._loadAddonSet();
+ }
+
+ this._directory = null;
+ if (this._addonSet.directory) {
+ this._directory = aDirectory.clone();
+ this._directory.append(this._addonSet.directory);
+ logger.info("SystemAddonInstallLocation scanning directory " + this._directory.path);
+ }
+ else {
+ logger.info("SystemAddonInstallLocation directory is missing");
+ }
+
+ DirectoryInstallLocation.call(this, aName, this._directory, aScope);
+ this.locked = true;
+}
+
+SystemAddonInstallLocation.prototype = Object.create(DirectoryInstallLocation.prototype);
+Object.assign(SystemAddonInstallLocation.prototype, {
+ /**
+ * Reads the current set of system add-ons
+ */
+ _loadAddonSet: function() {
+ try {
+ let setStr = Preferences.get(PREF_SYSTEM_ADDON_SET, null);
+ if (setStr) {
+ let addonSet = JSON.parse(setStr);
+ if ((typeof addonSet == "object") && addonSet.schema == 1)
+ return addonSet;
+ }
+ }
+ catch (e) {
+ logger.error("Malformed system add-on set, resetting.");
+ }
+
+ return { schema: 1, addons: {} };
+ },
+
+ /**
+ * Saves the current set of system add-ons
+ */
+ _saveAddonSet: function(aAddonSet) {
+ Preferences.set(PREF_SYSTEM_ADDON_SET, JSON.stringify(aAddonSet));
+ },
+
+ getAddonLocations: function() {
+ let addons = DirectoryInstallLocation.prototype.getAddonLocations.call(this);
+
+ // Strip out any unexpected add-ons from the list
+ for (let id of addons.keys()) {
+ if (!(id in this._addonSet.addons))
+ addons.delete(id);
+ }
+
+ return addons;
+ },
+
+ /**
+ * Tests whether updated system add-ons are expected.
+ */
+ isActive: function() {
+ return this._directory != null;
+ },
+
+ /**
+ * Tests whether the loaded add-on information matches what is expected.
+ */
+ isValid: function(aAddons) {
+ for (let id of Object.keys(this._addonSet.addons)) {
+ if (!aAddons.has(id)) {
+ logger.warn("Expected add-on " + id + " is missing from the system add-on location.");
+ return false;
+ }
+
+ let addon = aAddons.get(id);
+ if (addon.appDisabled) {
+ logger.warn("System add-on " + id + " isn't compatible with the application.");
+ return false;
+ }
+
+ if (addon.unpack) {
+ logger.warn("System add-on " + id + " isn't a packed add-on.");
+ return false;
+ }
+
+ if (!addon.bootstrap) {
+ logger.warn("System add-on " + id + " isn't restartless.");
+ return false;
+ }
+
+ if (addon.version != this._addonSet.addons[id].version) {
+ logger.warn("System add-on " + id + " wasn't the correct version.");
+ return false;
+ }
+ }
+
+ return true;
+ },
+});
+
#ifdef XP_WIN
/**
* An object that identifies a registry install location for add-ons. The location
@@ -7318,7 +7474,7 @@ WinRegInstallLocation.prototype = {
/**
* Gets an array of nsIFiles for add-ons installed in this location.
*/
- get addonLocations() {
+ getAddonLocations: function() {
let locations = new Map();
for (let id in this._IDToFileMap) {
locations.set(id, this._IDToFileMap[id].clone());
diff --git a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
index 48b3ecec272..eca104eeb73 100644
--- a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
+++ b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
@@ -53,6 +53,8 @@ const PREF_EM_DSS_ENABLED = "extensions.dss.enabled";
const PREF_EM_AUTO_DISABLED_SCOPES = "extensions.autoDisableScopes";
const KEY_APP_PROFILE = "app-profile";
+const KEY_APP_SYSTEM_ADDONS = "app-system-addons";
+const KEY_APP_SYSTEM_DEFAULTS = "app-system-defaults";
const KEY_APP_GLOBAL = "app-global";
// Properties that only exist in the database
@@ -1512,10 +1514,13 @@ this.XPIDatabaseReconcile = {
* Returns a map of ID -> add-on. When the same add-on ID exists in multiple
* install locations the highest priority location is chosen.
*/
- flattenByID(addonMap) {
+ flattenByID(addonMap, hideLocation) {
let map = new Map();
for (let installLocation of XPIProvider.installLocations) {
+ if (installLocation.name == hideLocation)
+ continue;
+
let locationMap = addonMap.get(installLocation.name);
if (!locationMap)
continue;
@@ -1623,7 +1628,12 @@ this.XPIDatabaseReconcile = {
aNewAddon._installLocation = aInstallLocation;
aNewAddon.installDate = aAddonState.mtime;
aNewAddon.updateDate = aAddonState.mtime;
- aNewAddon.foreignInstall = isDetectedInstall;
+
+ // Assume that add-ons in the system add-ons install location aren't
+ // foreign and should default to enabled.
+ aNewAddon.foreignInstall = isDetectedInstall &&
+ aInstallLocation.name != KEY_APP_SYSTEM_ADDONS &&
+ aInstallLocation.name != KEY_APP_SYSTEM_DEFAULTS;
// appDisabled depends on whether the add-on is a foreignInstall so update
aNewAddon.appDisabled = !isUsableAddon(aNewAddon);
@@ -1901,7 +1911,8 @@ this.XPIDatabaseReconcile = {
// has changed
let newAddon = loadedManifest(installLocation, id);
if (newAddon || oldAddon.updateDate != xpiState.mtime ||
- (aUpdateCompatibility && installLocation.name == KEY_APP_GLOBAL)) {
+ (aUpdateCompatibility && (installLocation.name == KEY_APP_GLOBAL ||
+ installLocation.name == KEY_APP_SYSTEM_DEFAULTS))) {
newAddon = this.updateMetadata(installLocation, oldAddon, xpiState, newAddon);
}
else if (oldAddon.descriptor != xpiState.descriptor) {
@@ -1958,8 +1969,24 @@ this.XPIDatabaseReconcile = {
}
}
+ // Validate the updated system add-ons
+ let systemAddonLocation = XPIProvider.installLocationsByName[KEY_APP_SYSTEM_ADDONS];
+ let addons = currentAddons.get(KEY_APP_SYSTEM_ADDONS) || new Map();
+
+ let hideLocation;
+ if (systemAddonLocation.isActive() && systemAddonLocation.isValid(addons)) {
+ // Hide the system add-on defaults
+ logger.info("Hiding the default system add-ons.");
+ hideLocation = KEY_APP_SYSTEM_DEFAULTS;
+ }
+ else {
+ // Hide the system add-on updates
+ logger.info("Hiding the updated system add-ons.");
+ hideLocation = KEY_APP_SYSTEM_ADDONS;
+ }
+
let previousVisible = this.getVisibleAddons(previousAddons);
- let currentVisible = this.flattenByID(currentAddons);
+ let currentVisible = this.flattenByID(currentAddons, hideLocation);
let sawActiveTheme = false;
XPIProvider.bootstrappedAddons = {};
@@ -2027,11 +2054,9 @@ this.XPIDatabaseReconcile = {
// and still exists then call its uninstall method.
if (previousAddon.bootstrap && previousAddon._installLocation &&
currentAddon._installLocation != previousAddon._installLocation &&
- currentAddons.get(previousAddon._installLocation.name).has(id)) {
+ previousAddon._sourceBundle.exists()) {
- let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
- file.persistentDescriptor = previousAddon._sourceBundle.persistentDescriptor;
- XPIProvider.callBootstrapMethod(previousAddon, file,
+ XPIProvider.callBootstrapMethod(previousAddon, previousAddon._sourceBundle,
"uninstall", installReason,
{ newVersion: currentAddon.version });
XPIProvider.unloadBootstrapScope(previousAddon.id);
@@ -2086,6 +2111,15 @@ this.XPIDatabaseReconcile = {
AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_UNINSTALLED, id);
}
+ // Make sure add-ons from hidden locations are marked invisible and inactive
+ let locationAddonMap = currentAddons.get(hideLocation);
+ if (locationAddonMap) {
+ for (let addon of locationAddonMap.values()) {
+ addon.visible = false;
+ addon.active = false;
+ }
+ }
+
// None of the active add-ons match the selected theme, enable the default.
if (!sawActiveTheme) {
XPIProvider.enableDefaultTheme();
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app0/empty b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app0/empty
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app1/features/system1@tests.mozilla.org.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app1/features/system1@tests.mozilla.org.xpi
new file mode 100644
index 00000000000..838b1b6584a
Binary files /dev/null and b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app1/features/system1@tests.mozilla.org.xpi differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app1/features/system2@tests.mozilla.org.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app1/features/system2@tests.mozilla.org.xpi
new file mode 100644
index 00000000000..c346cf3b7d0
Binary files /dev/null and b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app1/features/system2@tests.mozilla.org.xpi differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app1/features/system_1_1/bootstrap.js b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app1/features/system_1_1/bootstrap.js
new file mode 100644
index 00000000000..c5d862f45a7
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app1/features/system_1_1/bootstrap.js
@@ -0,0 +1,18 @@
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+const ID = "system1@tests.mozilla.org";
+const VERSION = "1.0";
+
+function install(data, reason) {
+}
+
+function startup(data, reason) {
+ Services.prefs.setCharPref("bootstraptest." + ID + ".active_version", VERSION);
+}
+
+function shutdown(data, reason) {
+ Services.prefs.clearUserPref("bootstraptest." + ID + ".active_version");
+}
+
+function uninstall(data, reason) {
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app1/features/system_1_1/install.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app1/features/system_1_1/install.rdf
new file mode 100644
index 00000000000..5ec174d0440
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app1/features/system_1_1/install.rdf
@@ -0,0 +1,23 @@
+
+
+
+
+
+ system1@tests.mozilla.org
+ 1.0
+ true
+
+
+ System Add-on 1
+
+
+
+ xpcshell@tests.mozilla.org
+ 1
+ 5
+
+
+
+
+
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app1/features/system_2_1/bootstrap.js b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app1/features/system_2_1/bootstrap.js
new file mode 100644
index 00000000000..e9449de6ec9
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app1/features/system_2_1/bootstrap.js
@@ -0,0 +1,18 @@
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+const ID = "system2@tests.mozilla.org";
+const VERSION = "1.0";
+
+function install(data, reason) {
+}
+
+function startup(data, reason) {
+ Services.prefs.setCharPref("bootstraptest." + ID + ".active_version", VERSION);
+}
+
+function shutdown(data, reason) {
+ Services.prefs.clearUserPref("bootstraptest." + ID + ".active_version");
+}
+
+function uninstall(data, reason) {
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app1/features/system_2_1/install.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app1/features/system_2_1/install.rdf
new file mode 100644
index 00000000000..b6b4efc82ed
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app1/features/system_2_1/install.rdf
@@ -0,0 +1,23 @@
+
+
+
+
+
+ system2@tests.mozilla.org
+ 1.0
+ true
+
+
+ System Add-on 2
+
+
+
+ xpcshell@tests.mozilla.org
+ 1
+ 5
+
+
+
+
+
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app2/features/system1@tests.mozilla.org.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app2/features/system1@tests.mozilla.org.xpi
new file mode 100644
index 00000000000..f82f607f9c7
Binary files /dev/null and b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app2/features/system1@tests.mozilla.org.xpi differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app2/features/system3@tests.mozilla.org.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app2/features/system3@tests.mozilla.org.xpi
new file mode 100644
index 00000000000..7fa67926282
Binary files /dev/null and b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app2/features/system3@tests.mozilla.org.xpi differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app2/features/system_1_2/bootstrap.js b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app2/features/system_1_2/bootstrap.js
new file mode 100644
index 00000000000..7a18c8ca3fa
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app2/features/system_1_2/bootstrap.js
@@ -0,0 +1,18 @@
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+const ID = "system1@tests.mozilla.org";
+const VERSION = "2.0";
+
+function install(data, reason) {
+}
+
+function startup(data, reason) {
+ Services.prefs.setCharPref("bootstraptest." + ID + ".active_version", VERSION);
+}
+
+function shutdown(data, reason) {
+ Services.prefs.clearUserPref("bootstraptest." + ID + ".active_version");
+}
+
+function uninstall(data, reason) {
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app2/features/system_1_2/install.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app2/features/system_1_2/install.rdf
new file mode 100644
index 00000000000..75a89f52d56
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app2/features/system_1_2/install.rdf
@@ -0,0 +1,23 @@
+
+
+
+
+
+ system1@tests.mozilla.org
+ 2.0
+ true
+
+
+ System Add-on 1
+
+
+
+ xpcshell@tests.mozilla.org
+ 1
+ 5
+
+
+
+
+
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app2/features/system_3_1/bootstrap.js b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app2/features/system_3_1/bootstrap.js
new file mode 100644
index 00000000000..198946e6ea9
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app2/features/system_3_1/bootstrap.js
@@ -0,0 +1,18 @@
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+const ID = "system3@tests.mozilla.org";
+const VERSION = "1.0";
+
+function install(data, reason) {
+}
+
+function startup(data, reason) {
+ Services.prefs.setCharPref("bootstraptest." + ID + ".active_version", VERSION);
+}
+
+function shutdown(data, reason) {
+ Services.prefs.clearUserPref("bootstraptest." + ID + ".active_version");
+}
+
+function uninstall(data, reason) {
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app2/features/system_3_1/install.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app2/features/system_3_1/install.rdf
new file mode 100644
index 00000000000..83be8dcc912
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/app2/features/system_3_1/install.rdf
@@ -0,0 +1,23 @@
+
+
+
+
+
+ system3@tests.mozilla.org
+ 1.0
+ true
+
+
+ System Add-on 3
+
+
+
+ xpcshell@tests.mozilla.org
+ 1
+ 5
+
+
+
+
+
diff --git a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
index 6a4ac99bcf6..e6a72cd2fbe 100644
--- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
@@ -1026,7 +1026,7 @@ function getFileForAddon(aDir, aId) {
function registerDirectory(aKey, aDir) {
var dirProvider = {
getFile: function(aProp, aPersistent) {
- aPersistent.value = true;
+ aPersistent.value = false;
if (aProp == aKey)
return aDir.clone();
return null;
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_system_reset.js b/toolkit/mozapps/extensions/test/xpcshell/test_system_reset.js
new file mode 100644
index 00000000000..159ba2f15b2
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_reset.js
@@ -0,0 +1,200 @@
+// Tests that we reset to the default system add-ons correctly when switching
+// application versions
+const PREF_SYSTEM_ADDON_SET = "extensions.systemAddonSet";
+
+const featureDir = gProfD.clone();
+featureDir.append("features");
+
+const distroDir = do_get_file("data/system_addons/app0");
+registerDirectory("XREAppDist", distroDir);
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "0");
+
+function makeUUID() {
+ let uuidGen = AM_Cc["@mozilla.org/uuid-generator;1"].
+ getService(AM_Ci.nsIUUIDGenerator);
+ return uuidGen.generateUUID().toString();
+}
+
+function* check_installed(inProfile, ...versions) {
+ let expectedDir;
+ if (inProfile) {
+ expectedDir = featureDir;
+ }
+ else {
+ expectedDir = distroDir.clone();
+ expectedDir.append("features");
+ }
+
+ for (let i = 0; i < versions.length; i++) {
+ let id = "system" + (i + 1) + "@tests.mozilla.org";
+ let addon = yield promiseAddonByID(id);
+
+ if (versions[i]) {
+ // Add-on should be installed
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, versions[i]);
+ do_check_true(addon.isActive);
+ do_check_false(addon.foreignInstall);
+
+ // Verify the add-ons file is in the right place
+ let file = expectedDir.clone();
+ file.append(id + ".xpi");
+ do_check_true(file.exists());
+ do_check_true(file.isFile());
+
+ let uri = addon.getResourceURI(null);
+ do_check_true(uri instanceof AM_Ci.nsIFileURL);
+ do_check_eq(uri.file.path, file.path);
+
+ // Verify the add-on actually started
+ let installed = Services.prefs.getCharPref("bootstraptest." + id + ".active_version");
+ do_check_eq(installed, versions[i]);
+ }
+ else {
+ // Add-on should not be installed
+ do_check_eq(addon, null);
+
+ try {
+ Services.prefs.getCharPref("bootstraptest." + id + ".active_version");
+ do_throw("Expected pref to be missing");
+ }
+ catch (e) {
+ }
+ }
+ }
+}
+
+// Test with a missing features directory
+add_task(function* test_missing_app_dir() {
+ startupManager();
+
+ yield check_installed(false, null, null, null);
+
+ do_check_false(featureDir.exists());
+
+ yield promiseShutdownManager();
+});
+
+// Add some features in a new version
+add_task(function* test_new_version() {
+ gAppInfo.version = "1";
+ distroDir.leafName = "app1";
+ startupManager();
+
+ yield check_installed(false, "1.0", "1.0", null);
+
+ do_check_false(featureDir.exists());
+
+ yield promiseShutdownManager();
+});
+
+// Another new version swaps one feature and upgrades another
+add_task(function* test_upgrade() {
+ gAppInfo.version = "2";
+ distroDir.leafName = "app2";
+ startupManager();
+
+ yield check_installed(false, "2.0", null, "1.0");
+
+ do_check_false(featureDir.exists());
+
+ yield promiseShutdownManager();
+});
+
+// Downgrade
+add_task(function* test_downgrade() {
+ gAppInfo.version = "1";
+ distroDir.leafName = "app1";
+ startupManager();
+
+ yield check_installed(false, "1.0", "1.0", null);
+
+ do_check_false(featureDir.exists());
+
+ yield promiseShutdownManager();
+});
+
+// Fake a mid-cycle install
+add_task(function* test_updated() {
+ // Create a random dir to install into
+ let dirname = makeUUID();
+ FileUtils.getDir("ProfD", ["features", dirname], true);
+ featureDir.append(dirname);
+
+ // Copy in the system add-ons
+ let file = do_get_file("data/system_addons/app1/features/system2@tests.mozilla.org.xpi");
+ file.copyTo(featureDir, file.leafName);
+ file = do_get_file("data/system_addons/app2/features/system3@tests.mozilla.org.xpi");
+ file.copyTo(featureDir, file.leafName);
+
+ // Inject it into the system set
+ let addonSet = {
+ schema: 1,
+ directory: dirname,
+ addons: {
+ "system2@tests.mozilla.org": {
+ version: "1.0"
+ },
+ "system3@tests.mozilla.org": {
+ version: "1.0"
+ },
+ }
+ };
+ Services.prefs.setCharPref(PREF_SYSTEM_ADDON_SET, JSON.stringify(addonSet));
+
+ startupManager(false);
+
+ yield check_installed(true, null, "1.0", "1.0");
+
+ yield promiseShutdownManager();
+});
+
+// An additional add-on in the directory should be ignored
+add_task(function* test_skips_additional() {
+ // Copy in the system add-ons
+ let file = do_get_file("data/system_addons/app1/features/system1@tests.mozilla.org.xpi");
+ file.copyTo(featureDir, file.leafName);
+
+ startupManager(false);
+
+ yield check_installed(true, null, "1.0", "1.0");
+
+ yield promiseShutdownManager();
+});
+
+// Missing add-on should revert to the default set
+add_task(function* test_revert() {
+ manuallyUninstall(featureDir, "system2@tests.mozilla.org");
+
+ startupManager(false);
+
+ // With system add-on 2 gone the updated set is now invalid so it reverts to
+ // the default set which is system add-ons 1 and 2.
+ yield check_installed(false, "1.0", "1.0", null);
+
+ yield promiseShutdownManager();
+});
+
+// Putting it back will make the set work again
+add_task(function* test_reuse() {
+ let file = do_get_file("data/system_addons/app1/features/system2@tests.mozilla.org.xpi");
+ file.copyTo(featureDir, file.leafName);
+
+ startupManager(false);
+
+ yield check_installed(true, null, "1.0", "1.0");
+
+ yield promiseShutdownManager();
+});
+
+// Making the pref corrupt should revert to the default set
+add_task(function* test_corrupt_pref() {
+ Services.prefs.setCharPref(PREF_SYSTEM_ADDON_SET, "foo");
+
+ startupManager(false);
+
+ yield check_installed(false, "1.0", "1.0", null);
+
+ yield promiseShutdownManager();
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
index 03041c8f9f1..a87c4ac399e 100644
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
@@ -24,6 +24,7 @@ skip-if = appname != "firefox"
[test_provider_unsafe_access_shutdown.js]
[test_provider_unsafe_access_startup.js]
[test_shutdown.js]
+[test_system_reset.js]
[test_XPIcancel.js]
[test_XPIStates.js]