From 39091d9406910beca0e2990f087d7f7be932bd96 Mon Sep 17 00:00:00 2001 From: Dave Townsend Date: Fri, 30 Jan 2015 17:20:12 -0800 Subject: [PATCH] Bug 1040158: Allow overriding an add-ons multiprocessCompatible flag in the update manifest for an add-on. r=Unfocused --- .../internal/AddonUpdateChecker.jsm | 10 +- .../extensions/internal/XPIProvider.jsm | 4 +- .../extensions/internal/XPIProviderUtils.js | 5 + .../extensions/test/xpcshell/head_addons.js | 160 ++++++++++++++++-- .../xpcshell/test_multiprocessCompatible.js | 118 +++++++++++++ .../test/xpcshell/xpcshell-shared.ini | 1 + .../extensions/test/xpcshell/xpcshell.ini | 1 - 7 files changed, 284 insertions(+), 15 deletions(-) create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_multiprocessCompatible.js 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]