diff --git a/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm b/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm
index 8a5c0853e8a..fdfe45dbf09 100644
--- a/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm
+++ b/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm
@@ -248,6 +248,13 @@ function parseRDFManifest(aId, aUpdateKey, aRequest) {
return getValue(aDs.GetTarget(aSource, EM_R(aProperty), true));
}
+ function getBooleanProperty(aDs, aSource, aProperty) {
+ let propValue = aDs.GetTarget(aSource, EM_R(aProperty), true);
+ if (!propValue)
+ return undefined;
+ return getValue(propValue) == "true";
+ }
+
function getRequiredProperty(aDs, aSource, aProperty) {
let value = getProperty(aDs, aSource, aProperty);
if (!value)
@@ -351,10 +358,11 @@ function parseRDFManifest(aId, aUpdateKey, aRequest) {
let result = {
id: aId,
version: version,
+ multiprocessCompatible: getBooleanProperty(ds, item, "multiprocessCompatible"),
updateURL: getProperty(ds, targetApp, "updateLink"),
updateHash: getProperty(ds, targetApp, "updateHash"),
updateInfoURL: getProperty(ds, targetApp, "updateInfoURL"),
- strictCompatibility: getProperty(ds, targetApp, "strictCompatibility") == "true",
+ strictCompatibility: !!getBooleanProperty(ds, targetApp, "strictCompatibility"),
targetApplications: [appEntry]
};
diff --git a/toolkit/mozapps/extensions/internal/XPIProvider.jsm b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
index d0c5fee4216..21dfd2cb763 100644
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -6440,6 +6440,8 @@ AddonInternal.prototype = {
}
});
});
+ if (aUpdate.multiprocessCompatible !== undefined)
+ this.multiprocessCompatible = aUpdate.multiprocessCompatible;
this.appDisabled = !isUsableAddon(this);
},
@@ -6599,7 +6601,7 @@ function AddonWrapper(aAddon) {
"providesUpdatesSecurely", "blocklistState", "blocklistURL", "appDisabled",
"softDisabled", "skinnable", "size", "foreignInstall", "hasBinaryComponents",
"strictCompatibility", "compatibilityOverrides", "updateURL",
- "getDataDirectory"].forEach(function(aProp) {
+ "getDataDirectory", "multiprocessCompatible"].forEach(function(aProp) {
this.__defineGetter__(aProp, function AddonWrapper_propertyGetter() aAddon[aProp]);
}, this);
diff --git a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
index fdb13b553e4..4b1f7d492bb 100644
--- a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
+++ b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
@@ -352,6 +352,11 @@ function DBAddonInternalPrototype()
}
});
});
+ if (aUpdate.multiprocessCompatible !== undefined &&
+ aUpdate.multiprocessCompatible != this.multiprocessCompatible) {
+ this.multiprocessCompatible = aUpdate.multiprocessCompatible;
+ XPIDatabase.saveChanges();
+ }
XPIProvider.updateAddonDisabledState(this);
};
diff --git a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
index 729608f77d5..a56dcbb7f0e 100644
--- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
@@ -174,18 +174,21 @@ function do_get_addon(aName) {
}
function do_get_addon_hash(aName, aAlgorithm) {
+ let file = do_get_addon(aName);
+ return do_get_file_hash(file);
+}
+
+function do_get_file_hash(aFile, aAlgorithm) {
if (!aAlgorithm)
aAlgorithm = "sha1";
- let file = do_get_addon(aName);
-
let crypto = AM_Cc["@mozilla.org/security/hash;1"].
createInstance(AM_Ci.nsICryptoHash);
crypto.initWithString(aAlgorithm);
let fis = AM_Cc["@mozilla.org/network/file-input-stream;1"].
createInstance(AM_Ci.nsIFileInputStream);
- fis.init(file, -1, -1, false);
- crypto.updateFromStream(fis, file.fileSize);
+ fis.init(aFile, -1, -1, false);
+ crypto.updateFromStream(fis, aFile.fileSize);
// return the two-digit hexadecimal code for a byte
function toHexString(charCode)
@@ -508,7 +511,8 @@ function loadAddonsList() {
gAddonsList = {
extensions: [],
- themes: []
+ themes: [],
+ mpIncompatible: new Set()
};
if (!gExtensionsINI.exists())
@@ -519,6 +523,11 @@ function loadAddonsList() {
var parser = factory.createINIParser(gExtensionsINI);
gAddonsList.extensions = readDirectories("ExtensionDirs");
gAddonsList.themes = readDirectories("ThemeDirs");
+ var keys = parser.getKeys("MultiprocessIncompatibleExtensions");
+ while (keys.hasMore()) {
+ let id = parser.getString("MultiprocessIncompatibleExtensions", keys.getNext());
+ gAddonsList.mpIncompatible.add(id);
+ }
}
function isItemInAddonsList(aType, aDir, aId) {
@@ -538,6 +547,10 @@ function isItemInAddonsList(aType, aDir, aId) {
return false;
}
+function isItemMarkedMPIncompatible(aId) {
+ return gAddonsList.mpIncompatible.has(aId);
+}
+
function isThemeInAddonsList(aDir, aId) {
return isItemInAddonsList("themes", aDir, aId);
}
@@ -588,6 +601,54 @@ function writeLocaleStrings(aData) {
return rdf;
}
+/**
+ * Creates an update.rdf structure as a string using for the update data passed.
+ *
+ * @param aData
+ * The update data as a JS object. Each property name is an add-on ID,
+ * the property value is an array of each version of the add-on. Each
+ * array value is a JS object containing the data for the version, at
+ * minimum a "version" and "targetApplications" property should be
+ * included to create a functional update manifest.
+ * @return the update.rdf structure as a string.
+ */
+function createUpdateRDF(aData) {
+ var rdf = '\n';
+ rdf += '\n';
+
+ for (let addon in aData) {
+ rdf += ' \n';
+
+ for (let versionData of aData[addon]) {
+ rdf += ' \n';
+
+ for (let prop of ["version", "multiprocessCompatible"]) {
+ if (prop in versionData)
+ rdf += " " + escapeXML(versionData[prop]) + "\n";
+ }
+
+ if ("targetApplications" in versionData) {
+ for (let app of versionData.targetApplications) {
+ rdf += " \n";
+ for (let prop of ["id", "minVersion", "maxVersion", "updateLink", "updateHash"]) {
+ if (prop in app)
+ rdf += " " + escapeXML(app[prop]) + "\n";
+ }
+ rdf += " \n";
+ }
+ }
+
+ rdf += ' \n';
+ }
+
+ rdf += ' \n'
+ }
+ rdf += "\n";
+
+ return rdf;
+}
+
function createInstallRDF(aData) {
var rdf = '\n';
rdf += '" + escapeXML(aData[aProp]) + "\n";
});
@@ -728,25 +789,65 @@ function writeInstallRDFForExtension(aData, aDir, aId, aExtraFile) {
function writeInstallRDFToXPI(aData, aDir, aId, aExtraFile) {
var id = aId ? aId : aData.id
- var dir = aDir.clone();
+ if (!aDir.exists())
+ aDir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
- if (!dir.exists())
- dir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
- dir.append(id + ".xpi");
+ var file = aDir.clone();
+ file.append(id + ".xpi");
+ writeInstallRDFToXPIFile(aData, file, aExtraFile);
+
+ return file;
+}
+
+/**
+ * Writes an install.rdf manifest into an XPI file using the properties passed
+ * in a JS object. The objects should contain a property for each property to
+ * appear in the RDF. The object may contain an array of objects with id,
+ * minVersion and maxVersion in the targetApplications property to give target
+ * application compatibility.
+ *
+ * @param aData
+ * The object holding data about the add-on
+ * @param aFile
+ * The XPI file to write to. Any existing file will be overwritten
+ * @param aExtraFile
+ * An optional dummy file to create in the extension
+ */
+function writeInstallRDFToXPIFile(aData, aFile, aExtraFile) {
var rdf = createInstallRDF(aData);
var stream = AM_Cc["@mozilla.org/io/string-input-stream;1"].
createInstance(AM_Ci.nsIStringInputStream);
stream.setData(rdf, -1);
var zipW = AM_Cc["@mozilla.org/zipwriter;1"].
createInstance(AM_Ci.nsIZipWriter);
- zipW.open(dir, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE);
+ zipW.open(aFile, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE);
zipW.addEntryStream("install.rdf", 0, AM_Ci.nsIZipWriter.COMPRESSION_NONE,
stream, false);
if (aExtraFile)
zipW.addEntryStream(aExtraFile, 0, AM_Ci.nsIZipWriter.COMPRESSION_NONE,
stream, false);
zipW.close();
- return dir;
+}
+
+let temp_xpis = [];
+/**
+ * Creates an XPI file for some manifest data in the temporary directory and
+ * returns the nsIFile for it. The file will be deleted when the test completes.
+ *
+ * @param aData
+ * The object holding data about the add-on
+ * @return A file pointing to the created XPI file
+ */
+function createTempXPIFile(aData) {
+ var file = gTmpD.clone();
+ file.append("foo.xpi");
+ do {
+ file.leafName = Math.floor(Math.random() * 1000000) + ".xpi";
+ } while (file.exists());
+
+ temp_xpis.push(file);
+ writeInstallRDFToXPIFile(aData, file);
+ return file;
}
/**
@@ -1163,6 +1264,12 @@ function completeAllInstalls(aInstalls, aCallback) {
});
}
+function promiseCompleteAllInstalls(aInstalls) {
+ return new Promise(resolve => {
+ completeAllInstalls(aInstalls, resolve);
+ });
+}
+
/**
* A helper method to install an array of files and call a callback after the
* installs are completed.
@@ -1415,6 +1522,11 @@ do_register_cleanup(function addon_cleanup() {
if (timer)
timer.cancel();
+ for (let file of temp_xpis) {
+ if (file.exists())
+ file.remove(false);
+ }
+
// Check that the temporary directory is empty
var dirEntries = gTmpD.directoryEntries
.QueryInterface(AM_Ci.nsIDirectoryEnumerator);
@@ -1613,3 +1725,27 @@ function promiseAddonsByIDs(list) {
function promiseAddonByID(aId) {
return new Promise((resolve, reject) => AddonManager.getAddonByID(aId, resolve));
}
+
+/**
+ * Returns a promise that will be resolved when an add-on update check is
+ * complete. The value resolved will be an AddonInstall if a new version was
+ * found.
+ */
+function promiseFindAddonUpdates(addon, reason = AddonManager.UPDATE_WHEN_PERIODIC_UPDATE) {
+ return new Promise((resolve, reject) => {
+ addon.findUpdates({
+ install: null,
+
+ onUpdateAvailable: function(addon, install) {
+ this.install = install;
+ },
+
+ onUpdateFinished: function(addon, error) {
+ if (error == AddonManager.UPDATE_STATUS_NO_ERROR)
+ resolve(this.install);
+ else
+ reject(error);
+ }
+ }, reason);
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_multiprocessCompatible.js b/toolkit/mozapps/extensions/test/xpcshell/test_multiprocessCompatible.js
new file mode 100644
index 00000000000..ab5a976cc55
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_multiprocessCompatible.js
@@ -0,0 +1,118 @@
+Components.utils.import("resource://testing-common/httpd.js");
+var gServer;
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+
+function build_test(multiprocessCompatible, bootstrap, updateMultiprocessCompatible) {
+ return function* () {
+ dump("Running test" +
+ " multiprocessCompatible: " + multiprocessCompatible +
+ " bootstrap: " + bootstrap +
+ " updateMultiprocessCompatible: " + updateMultiprocessCompatible +
+ "\n");
+
+ let addonData = {
+ id: "addon@tests.mozilla.org",
+ name: "Test Add-on",
+ version: "1.0",
+ multiprocessCompatible,
+ bootstrap,
+ updateURL: "http://localhost:" + gPort + "/updaterdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+ }
+
+ gServer.registerPathHandler("/updaterdf", function(request, response) {
+ let updateData = {};
+ updateData[addonData.id] = [{
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+ }];
+
+ if (updateMultiprocessCompatible !== undefined) {
+ updateData[addonData.id][0].multiprocessCompatible = updateMultiprocessCompatible;
+ }
+
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write(createUpdateRDF(updateData));
+ });
+
+ let expectedMPC = updateMultiprocessCompatible === undefined ?
+ multiprocessCompatible :
+ updateMultiprocessCompatible;
+
+ let xpifile = createTempXPIFile(addonData);
+ let install = yield new Promise(resolve => AddonManager.getInstallForFile(xpifile, resolve));
+ do_check_eq(install.addon.multiprocessCompatible, multiprocessCompatible);
+ yield promiseCompleteAllInstalls([install]);
+
+ if (!bootstrap) {
+ yield promiseRestartManager();
+ do_check_true(isExtensionInAddonsList(profileDir, addonData.id));
+ do_check_eq(isItemMarkedMPIncompatible(addonData.id), !multiprocessCompatible);
+ }
+
+ let addon = yield promiseAddonByID(addonData.id);
+ do_check_neq(addon, null);
+ do_check_eq(addon.multiprocessCompatible, multiprocessCompatible);
+
+ yield promiseFindAddonUpdates(addon);
+
+ // Should have applied the compatibility change
+ do_check_eq(addon.multiprocessCompatible, expectedMPC);
+ yield promiseRestartManager();
+
+ addon = yield promiseAddonByID(addonData.id);
+ // Should have persisted the compatibility change
+ do_check_eq(addon.multiprocessCompatible, expectedMPC);
+ if (!bootstrap) {
+ do_check_true(isExtensionInAddonsList(profileDir, addonData.id));
+ do_check_eq(isItemMarkedMPIncompatible(addonData.id), !multiprocessCompatible);
+ }
+
+ addon.uninstall();
+ yield promiseRestartManager();
+
+ gServer.registerPathHandler("/updaterdf", null);
+ }
+}
+
+/* Builds a set of tests to run the same steps for every combination of:
+ * The add-on being restartless
+ * The initial add-on supporting multiprocess
+ * The update saying the add-on should or should not support multiprocess (or not say anything at all)
+ */
+for (let bootstrap of [false, true]) {
+ for (let multiprocessCompatible of [false, true]) {
+ for (let updateMultiprocessCompatible of [undefined, false, true]) {
+ add_task(build_test(multiprocessCompatible, bootstrap, updateMultiprocessCompatible));
+ }
+ }
+}
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
+ startupManager();
+
+ // Create and configure the HTTP server.
+ gServer = new HttpServer();
+ gServer.registerDirectory("/data/", gTmpD);
+ gServer.start(-1);
+ gPort = gServer.identity.primaryPort;
+
+ run_next_test();
+}
+
+function end_test() {
+ gServer.stop(do_test_finished);
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini b/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini
index 9c57763248a..a4d6d792c26 100644
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini
@@ -220,6 +220,7 @@ requesttimeoutfactor = 2
[test_migrate5.js]
[test_migrateAddonRepository.js]
[test_migrate_max_version.js]
+[test_multiprocessCompatible.js]
[test_no_addons.js]
[test_onPropertyChanged_appDisabled.js]
[test_permissions.js]
diff --git a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
index b21daa481af..acaa7388187 100644
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
@@ -25,5 +25,4 @@ run-if = appname == "firefox"
[test_XPIcancel.js]
[test_XPIStates.js]
-
[include:xpcshell-shared.ini]