Bug 1040158: Allow overriding an add-ons multiprocessCompatible flag in the update manifest for an add-on. r=Unfocused

--HG--
extra : rebase_source : 346d93a2ae26e8961c6fa800d63f9b926d0c1efe
This commit is contained in:
Dave Townsend 2015-01-30 17:20:12 -08:00
parent a064a64c0d
commit ff0db14e05
7 changed files with 284 additions and 15 deletions

View File

@ -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]
};

View File

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

View File

@ -352,6 +352,11 @@ function DBAddonInternalPrototype()
}
});
});
if (aUpdate.multiprocessCompatible !== undefined &&
aUpdate.multiprocessCompatible != this.multiprocessCompatible) {
this.multiprocessCompatible = aUpdate.multiprocessCompatible;
XPIDatabase.saveChanges();
}
XPIProvider.updateAddonDisabledState(this);
};

View File

@ -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 = '<?xml version="1.0"?>\n';
rdf += '<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"\n' +
' xmlns:em="http://www.mozilla.org/2004/em-rdf#">\n';
for (let addon in aData) {
rdf += ' <Description about="urn:mozilla:extension:' + escapeXML(addon) + '"><em:updates><Seq>\n';
for (let versionData of aData[addon]) {
rdf += ' <li><Description>\n';
for (let prop of ["version", "multiprocessCompatible"]) {
if (prop in versionData)
rdf += " <em:" + prop + ">" + escapeXML(versionData[prop]) + "</em:" + prop + ">\n";
}
if ("targetApplications" in versionData) {
for (let app of versionData.targetApplications) {
rdf += " <em:targetApplication><Description>\n";
for (let prop of ["id", "minVersion", "maxVersion", "updateLink", "updateHash"]) {
if (prop in app)
rdf += " <em:" + prop + ">" + escapeXML(app[prop]) + "</em:" + prop + ">\n";
}
rdf += " </Description></em:targetApplication>\n";
}
}
rdf += ' </Description></li>\n';
}
rdf += ' </Seq></em:updates></Description>\n'
}
rdf += "</RDF>\n";
return rdf;
}
function createInstallRDF(aData) {
var rdf = '<?xml version="1.0"?>\n';
rdf += '<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"\n' +
@ -596,7 +657,7 @@ function createInstallRDF(aData) {
["id", "version", "type", "internalName", "updateURL", "updateKey",
"optionsURL", "optionsType", "aboutURL", "iconURL", "icon64URL",
"skinnable", "bootstrap", "strictCompatibility"].forEach(function(aProp) {
"skinnable", "bootstrap", "strictCompatibility", "multiprocessCompatible"].forEach(function(aProp) {
if (aProp in aData)
rdf += "<em:" + aProp + ">" + escapeXML(aData[aProp]) + "</em:" + 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);
});
}

View File

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

View File

@ -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]

View File

@ -25,5 +25,4 @@ run-if = appname == "firefox"
[test_XPIcancel.js]
[test_XPIStates.js]
[include:xpcshell-shared.ini]