Bug 1192921: Add an install location for system add-ons. r=rhelmer

This adds two new directory install locations. One contains the default system
add-ons that ship with the application, the other contains system add-on that
will eventually be updatable at runtime.

The updatable location tracks the expected list of add-ons in a pref. and only
returns add-ons from that list when asked for its list of add-ons.

After processFileChanges has scanned all add-ons and updated the database it
checks if the updated system add-ons match the expected set. If not we ignore
those add-ons when working out which add-ons should be visible. If they do match
then we ignore the app-shipped system add-ons when working out which are
visible.
This commit is contained in:
Dave Townsend 2015-09-04 12:00:47 -07:00
parent a43d2f8c6c
commit 5f994dfb96
18 changed files with 568 additions and 13 deletions

View File

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

View File

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

View File

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

View File

@ -0,0 +1,23 @@
<?xml version="1.0"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:install-manifest">
<em:id>system1@tests.mozilla.org</em:id>
<em:version>1.0</em:version>
<em:bootstrap>true</em:bootstrap>
<!-- Front End MetaData -->
<em:name>System Add-on 1</em:name>
<em:targetApplication>
<Description>
<em:id>xpcshell@tests.mozilla.org</em:id>
<em:minVersion>1</em:minVersion>
<em:maxVersion>5</em:maxVersion>
</Description>
</em:targetApplication>
</Description>
</RDF>

View File

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

View File

@ -0,0 +1,23 @@
<?xml version="1.0"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:install-manifest">
<em:id>system2@tests.mozilla.org</em:id>
<em:version>1.0</em:version>
<em:bootstrap>true</em:bootstrap>
<!-- Front End MetaData -->
<em:name>System Add-on 2</em:name>
<em:targetApplication>
<Description>
<em:id>xpcshell@tests.mozilla.org</em:id>
<em:minVersion>1</em:minVersion>
<em:maxVersion>5</em:maxVersion>
</Description>
</em:targetApplication>
</Description>
</RDF>

View File

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

View File

@ -0,0 +1,23 @@
<?xml version="1.0"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:install-manifest">
<em:id>system1@tests.mozilla.org</em:id>
<em:version>2.0</em:version>
<em:bootstrap>true</em:bootstrap>
<!-- Front End MetaData -->
<em:name>System Add-on 1</em:name>
<em:targetApplication>
<Description>
<em:id>xpcshell@tests.mozilla.org</em:id>
<em:minVersion>1</em:minVersion>
<em:maxVersion>5</em:maxVersion>
</Description>
</em:targetApplication>
</Description>
</RDF>

View File

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

View File

@ -0,0 +1,23 @@
<?xml version="1.0"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:install-manifest">
<em:id>system3@tests.mozilla.org</em:id>
<em:version>1.0</em:version>
<em:bootstrap>true</em:bootstrap>
<!-- Front End MetaData -->
<em:name>System Add-on 3</em:name>
<em:targetApplication>
<Description>
<em:id>xpcshell@tests.mozilla.org</em:id>
<em:minVersion>1</em:minVersion>
<em:maxVersion>5</em:maxVersion>
</Description>
</em:targetApplication>
</Description>
</RDF>

View File

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

View File

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

View File

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