--- a/toolkit/mozapps/extensions/internal/XPIDatabase.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIDatabase.jsm
@@ -116,17 +116,17 @@ const COMPATIBLE_BY_DEFAULT_TYPES = {
"webextension-dictionary": true,
};
// Properties that exist in the extension manifest
const PROP_LOCALE_SINGLE = ["name", "description", "creator", "homepageURL"];
const PROP_LOCALE_MULTI = ["developers", "translators", "contributors"];
// Properties to save in JSON file
-const PROP_JSON_FIELDS = ["id", "syncGUID", "location", "version", "type",
+const PROP_JSON_FIELDS = ["id", "syncGUID", "version", "type",
"updateURL", "optionsURL",
"optionsType", "optionsBrowserStyle", "aboutURL",
"defaultLocale", "visible", "active", "userDisabled",
"appDisabled", "pendingUninstall", "installDate",
"updateDate", "applyBackgroundUpdates", "path",
"skinnable", "size", "sourceURI", "releaseNotesURI",
"softDisabled", "foreignInstall",
"strictCompatibility", "locales", "targetApplications",
@@ -264,22 +264,25 @@ class AddonInternal {
this.hasEmbeddedWebExtension = false;
if (addonData) {
if (addonData.descriptor && !addonData.path) {
addonData.path = descriptorToPath(addonData.descriptor);
}
copyProperties(addonData, PROP_JSON_FIELDS, this);
+ this.location = addonData.location;
if (!this.dependencies)
this.dependencies = [];
Object.freeze(this.dependencies);
- this.addedToDatabase();
+ if (this.location) {
+ this.addedToDatabase();
+ }
if (!addonData._sourceBundle) {
throw new Error("Expected passed argument to contain a path");
}
this._sourceBundle = addonData._sourceBundle;
}
}
@@ -287,24 +290,17 @@ class AddonInternal {
get wrapper() {
if (!this._wrapper) {
this._wrapper = new AddonWrapper(this);
}
return this._wrapper;
}
addedToDatabase() {
- if (this._installLocation) {
- this.location = this._installLocation.name;
- } else if (this.location) {
- this._installLocation = XPIProvider.installLocationsByName[this.location];
- }
-
- this._key = `${this.location}:${this.id}`;
-
+ this._key = `${this.location.name}:${this.id}`;
this.inDatabase = true;
}
get selectedLocale() {
if (this._selectedLocale)
return this._selectedLocale;
/**
@@ -352,17 +348,17 @@ class AddonInternal {
return this._selectedLocale;
}
get providesUpdatesSecurely() {
return !this.updateURL || this.updateURL.startsWith("https:");
}
get isCorrectlySigned() {
- switch (this._installLocation.name) {
+ switch (this.location.name) {
case KEY_APP_SYSTEM_ADDONS:
// System add-ons must be signed by the system key.
return this.signedState == AddonManager.SIGNEDSTATE_SYSTEM;
case KEY_APP_SYSTEM_DEFAULTS:
case KEY_APP_TEMPORARY:
// Temporary and built-in system add-ons do not require signing.
return true;
@@ -382,17 +378,17 @@ class AddonInternal {
return this.signedState > AddonManager.SIGNEDSTATE_MISSING;
}
get isCompatible() {
return this.isCompatibleWith();
}
get hidden() {
- return this._installLocation.isSystem;
+ return this.location.isSystem;
}
get disabled() {
return (this.userDisabled || this.appDisabled || this.softDisabled);
}
get isPlatformCompatible() {
if (this.targetPlatforms.length == 0)
@@ -579,17 +575,19 @@ class AddonInternal {
if (this.inDatabase)
XPIDatabase.updateAddonDisabledState(this);
else
this.appDisabled = !XPIDatabase.isUsableAddon(this);
}
}
toJSON() {
- return copyProperties(this, PROP_JSON_FIELDS);
+ let obj = copyProperties(this, PROP_JSON_FIELDS);
+ obj.location = this.location.name;
+ return obj;
}
/**
* When an add-on install is pending its metadata will be cached in a file.
* This method reads particular properties of that metadata that may be newer
* than that in the extension manifest, like compatibility information.
*
* @param {Object} aObj
@@ -619,21 +617,21 @@ class AddonInternal {
permissions |= AddonManager.PERM_CAN_ENABLE;
} else if (this.type != "theme") {
permissions |= AddonManager.PERM_CAN_DISABLE;
}
}
// Add-ons that are in locked install locations, or are pending uninstall
// cannot be upgraded or uninstalled
- if (!this._installLocation.locked && !this.pendingUninstall) {
+ if (!this.location.locked && !this.pendingUninstall) {
// System add-on upgrades are triggered through a different mechanism (see updateSystemAddons())
- let isSystem = this._installLocation.isSystem;
+ let isSystem = this.location.isSystem;
// Add-ons that are installed by a file link cannot be upgraded.
- if (!this._installLocation.isLinkedAddon(this.id) && !isSystem) {
+ if (!this.location.isLinkedAddon(this.id) && !isSystem) {
permissions |= AddonManager.PERM_CAN_UPGRADE;
}
permissions |= AddonManager.PERM_CAN_UNINSTALL;
}
if (Services.policies &&
!Services.policies.isAllowed(`modify-extension:${this.id}`)) {
@@ -678,17 +676,17 @@ AddonWrapper = class {
return XPIInternal.getExternalType(addonFor(this).type);
}
get isWebExtension() {
return isWebExtension(addonFor(this).type);
}
get temporarilyInstalled() {
- return addonFor(this)._installLocation == XPIInternal.TemporaryInstallLocation;
+ return addonFor(this).location.isTemporary;
}
get aboutURL() {
return this.isActive ? addonFor(this).aboutURL : null;
}
get optionsURL() {
if (!this.isActive) {
@@ -851,18 +849,18 @@ AddonWrapper = class {
get pendingUpgrade() {
let addon = addonFor(this);
return addon.pendingUpgrade ? addon.pendingUpgrade.wrapper : null;
}
get scope() {
let addon = addonFor(this);
- if (addon._installLocation)
- return addon._installLocation.scope;
+ if (addon.location)
+ return addon.location.scope;
return AddonManager.SCOPE_PROFILE;
}
get pendingOperations() {
let addon = addonFor(this);
let pending = 0;
if (!(addon.inDatabase)) {
@@ -961,24 +959,24 @@ AddonWrapper = class {
}
get hidden() {
return addonFor(this).hidden;
}
get isSystem() {
let addon = addonFor(this);
- return addon._installLocation.isSystem;
+ return addon.location.isSystem;
}
// Returns true if Firefox Sync should sync this addon. Only addons
// in the profile install location are considered syncable.
get isSyncable() {
let addon = addonFor(this);
- return (addon._installLocation.name == KEY_APP_PROFILE);
+ return (addon.location.name == KEY_APP_PROFILE);
}
get userPermissions() {
return addonFor(this).userPermissions;
}
isCompatibleWith(aAppVersion, aPlatformVersion) {
return addonFor(this).isCompatibleWith(aAppVersion, aPlatformVersion);
@@ -986,17 +984,17 @@ AddonWrapper = class {
async uninstall(alwaysAllowUndo) {
let addon = addonFor(this);
return XPIInstall.uninstallAddon(addon, alwaysAllowUndo);
}
cancelUninstall() {
let addon = addonFor(this);
- XPIProvider.cancelUninstallAddon(addon);
+ XPIInstall.cancelUninstallAddon(addon);
}
findUpdates(aListener, aReason, aAppVersion, aPlatformVersion) {
new UpdateChecker(addonFor(this), aListener, aReason, aAppVersion, aPlatformVersion);
}
// Returns true if there was an update in progress, false if there was no update to cancel
cancelUpdate() {
@@ -1270,16 +1268,19 @@ function _filterDB(addonDB, aFilter) {
this.XPIDatabase = {
// true if the database connection has been opened
initialized: false,
// The database file
jsonFile: FileUtils.getFile(KEY_PROFILEDIR, [FILE_JSON_DB], true),
rebuildingDatabase: false,
syncLoadingDB: false,
+ // Add-ons from the database in locations which are no longer
+ // supported.
+ orphanedAddons: [],
_saveTask: null,
// Saved error object if we fail to read an existing database
_loadError: null,
// Saved error object if we fail to save the database
_saveError: null,
@@ -1358,17 +1359,17 @@ this.XPIDatabase = {
if (!this.addonDB) {
// We never loaded the database?
throw new Error("Attempt to save database without loading it first");
}
let toSave = {
schemaVersion: DB_SCHEMA,
addons: Array.from(this.addonDB.values())
- .filter(addon => addon.location != KEY_APP_TEMPORARY),
+ .filter(addon => !addon.location.isTemporary),
};
return toSave;
},
/**
* Synchronously loads the database, by running the normal async load
* operation with idle dispatch disabled, and spinning the event loop
* until it finishes.
@@ -1432,19 +1433,24 @@ this.XPIDatabase = {
loadedAddon.path = descriptorToPath(loadedAddon.descriptor);
}
loadedAddon._sourceBundle = new nsIFile(loadedAddon.path);
} catch (e) {
// We can fail here when the path is invalid, usually from the
// wrong OS
logger.warn("Could not find source bundle for add-on " + loadedAddon.id, e);
}
+ loadedAddon.location = XPIStates.getLocation(loadedAddon.location);
let newAddon = new AddonInternal(loadedAddon);
- addonDB.set(newAddon._key, newAddon);
+ if (loadedAddon.location) {
+ addonDB.set(newAddon._key, newAddon);
+ } else {
+ this.orphanedAddons.push(newAddon);
+ }
});
parseTimer.done();
this.addonDB = addonDB;
logger.debug("Successfully read XPI database");
this.initialized = true;
} catch (e) {
// If we catch and log a SyntaxError from the JSON
@@ -1772,17 +1778,17 @@ this.XPIDatabase = {
/**
* Asynchronously get all the add-ons in a particular install location.
*
* @param {string} aLocation
* The name of the install location
* @returns {Promise<Array<AddonInternal>>}
*/
getAddonsInLocation(aLocation) {
- return this.getAddonList(aAddon => aAddon._installLocation.name == aLocation);
+ return this.getAddonList(aAddon => aAddon.location.name == aLocation);
},
/**
* Asynchronously gets the add-on with the specified ID that is visible.
*
* @param {string} aId
* The ID of the add-on to retrieve
* @returns {Promise<AddonInternal?>}
@@ -1896,22 +1902,22 @@ this.XPIDatabase = {
*
* @returns {boolean} Whether the addon should be disabled for being legacy
*/
isDisabledLegacy(addon) {
return (!AddonSettings.ALLOW_LEGACY_EXTENSIONS &&
LEGACY_TYPES.has(addon.type) &&
// Legacy add-ons are allowed in the system location.
- !addon._installLocation.isSystem &&
+ !addon.location.isSystem &&
// Legacy extensions may be installed temporarily in
// non-release builds.
!(AppConstants.MOZ_ALLOW_LEGACY_EXTENSIONS &&
- addon._installLocation.name == KEY_APP_TEMPORARY) &&
+ addon.location.isTemporary) &&
// Properly signed legacy extensions are allowed.
addon.signedState !== AddonManager.SIGNEDSTATE_PRIVILEGED);
},
/**
* Calculates whether an add-on should be appDisabled or not.
*
@@ -2041,19 +2047,19 @@ this.XPIDatabase = {
* The AddonInternal being removed
*/
removeAddonMetadata(aAddon) {
this.addonDB.delete(aAddon._key);
this.saveChanges();
},
updateXPIStates(addon) {
- let xpiState = XPIStates.getAddon(addon.location, addon.id);
- if (xpiState) {
- xpiState.syncWithDB(addon);
+ let state = addon.location && addon.location.get(addon.id);
+ if (state) {
+ state.syncWithDB(addon);
XPIStates.save();
}
},
/**
* Synchronously marks a AddonInternal as visible marking all other
* instances with the same ID as not visible.
*
@@ -2077,17 +2083,17 @@ this.XPIDatabase = {
},
/**
* Synchronously marks a given add-on ID visible in a given location,
* instances with the same ID as not visible.
*
* @param {string} aId
* The ID of the add-on to make visible
- * @param {InstallLocation} aLocation
+ * @param {XPIStateLocation} aLocation
* The location in which to make the add-on visible.
* @returns {AddonInternal?}
* The add-on instance which was marked visible, if any.
*/
makeAddonLocationVisible(aId, aLocation) {
logger.debug(`Make addon ${aId} visible in location ${aLocation}`);
let result;
for (let [, addon] of this.addonDB) {
@@ -2261,25 +2267,17 @@ this.XPIDatabase = {
// If the add-on is not visible or the add-on is not changing state then
// there is no need to do anything else
if (!aAddon.visible || (wasDisabled == isDisabled))
return undefined;
// Flag that active states in the database need to be updated on shutdown
Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
- // Sync with XPIStates.
- let xpiState = XPIStates.getAddon(aAddon.location, aAddon.id);
- if (xpiState) {
- xpiState.syncWithDB(aAddon);
- XPIStates.save();
- } else {
- // There should always be an xpiState
- logger.warn("No XPIState for ${id} in ${location}", aAddon);
- }
+ this.updateXPIStates(aAddon);
// Have we just gone back to the current state?
if (isDisabled != aAddon.active) {
AddonManagerPrivate.callAddonListeners("onOperationCancelled", wrapper);
} else {
if (isDisabled) {
AddonManagerPrivate.callAddonListeners("onDisabling", wrapper, false);
} else {
@@ -2297,21 +2295,17 @@ this.XPIDatabase = {
AddonManagerPrivate.callAddonListeners("onEnabled", wrapper);
}
}
// Notify any other providers that a new theme has been enabled
if (isTheme(aAddon.type)) {
if (!isDisabled) {
AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type);
-
- if (xpiState) {
- xpiState.syncWithDB(aAddon);
- XPIStates.save();
- }
+ this.updateXPIStates(aAddon);
} else if (isDisabled && !aBecauseSelecting) {
AddonManagerPrivate.notifyAddonChanged(null, "theme");
}
}
return isDisabled;
},
@@ -2344,21 +2338,21 @@ this.XPIDatabaseReconcile = {
* The add-on map to flatten.
* @param {string?} [hideLocation]
* An optional location from which to hide any add-ons.
* @returns {Map<string, AddonInternal>}
*/
flattenByID(addonMap, hideLocation) {
let map = new Map();
- for (let installLocation of XPIProvider.installLocations) {
- if (installLocation.name == hideLocation)
+ for (let loc of XPIStates.locations()) {
+ if (loc.name == hideLocation)
continue;
- let locationMap = addonMap.get(installLocation.name);
+ let locationMap = addonMap.get(loc.name);
if (!locationMap)
continue;
for (let [id, addon] of locationMap) {
if (!map.has(id))
map.set(id, addon);
}
}
@@ -2396,17 +2390,17 @@ this.XPIDatabaseReconcile = {
/**
* Called to add the metadata for an add-on in one of the install locations
* to the database. This can be called in three different cases. Either an
* add-on has been dropped into the location from outside of Firefox, or
* an add-on has been installed through the application, or the database
* has been upgraded or become corrupt and add-on data has to be reloaded
* into it.
*
- * @param {InstallLocation} aInstallLocation
+ * @param {XPIStateLocation} aLocation
* The install location containing the add-on
* @param {string} aId
* The ID of the add-on
* @param {XPIState} aAddonState
* The new state of the add-on
* @param {AddonInternal?} [aNewAddon]
* The manifest for the new add-on if it has already been loaded
* @param {string?} [aOldAppVersion]
@@ -2414,19 +2408,19 @@ this.XPIDatabaseReconcile = {
* if it is a new profile or the version is unknown
* @param {string?} [aOldPlatformVersion]
* The version of the platform last run with this profile or null
* if it is a new profile or the version is unknown
* @returns {boolean}
* A boolean indicating if flushing caches is required to complete
* changing this add-on
*/
- addMetadata(aInstallLocation, aId, aAddonState, aNewAddon, aOldAppVersion,
+ addMetadata(aLocation, aId, aAddonState, aNewAddon, aOldAppVersion,
aOldPlatformVersion) {
- logger.debug("New add-on " + aId + " installed in " + aInstallLocation.name);
+ logger.debug(`New add-on ${aId} installed in ${aLocation.name}`);
// We treat this is a new install if,
//
// a) It was explicitly registered as a staged install in the last
// session, or,
// b) We're not currently migrating or rebuilding a corrupt database. In
// that case, we can assume this add-on was found during a routine
// directory scan.
@@ -2437,64 +2431,60 @@ this.XPIDatabaseReconcile = {
let isDetectedInstall = isNewInstall && !aNewAddon;
// Load the manifest if necessary and sanity check the add-on ID
let unsigned;
try {
if (!aNewAddon) {
// Load the manifest from the add-on.
let file = new nsIFile(aAddonState.path);
- aNewAddon = XPIInstall.syncLoadManifestFromFile(file, aInstallLocation);
+ aNewAddon = XPIInstall.syncLoadManifestFromFile(file, aLocation);
}
// The add-on in the manifest should match the add-on ID.
if (aNewAddon.id != aId) {
- throw new Error("Invalid addon ID: expected addon ID " + aId +
- ", found " + aNewAddon.id + " in manifest");
+ throw new Error(`Invalid addon ID: expected addon ID ${aId}, found ${aNewAddon.id} in manifest`);
}
unsigned = XPIDatabase.mustSign(aNewAddon.type) && !aNewAddon.isCorrectlySigned;
if (unsigned) {
throw Error(`Extension ${aNewAddon.id} is not correctly signed`);
}
} catch (e) {
- logger.warn("addMetadata: Add-on " + aId + " is invalid", e);
+ logger.warn(`addMetadata: Add-on ${aId} is invalid`, e);
// Remove the invalid add-on from the install location if the install
// location isn't locked
- if (aInstallLocation.isLinkedAddon(aId))
+ if (aLocation.isLinkedAddon(aId))
logger.warn("Not uninstalling invalid item because it is a proxy file");
- else if (aInstallLocation.locked)
+ else if (aLocation.locked)
logger.warn("Could not uninstall invalid item from locked install location");
else if (unsigned && !isNewInstall)
logger.warn("Not uninstalling existing unsigned add-on");
else
- aInstallLocation.uninstallAddon(aId);
+ aLocation.installer.uninstallAddon(aId);
return null;
}
// Update the AddonInternal properties.
aNewAddon.installDate = aAddonState.mtime;
aNewAddon.updateDate = aAddonState.mtime;
// 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;
+ aNewAddon.foreignInstall = isDetectedInstall && !aLocation.isSystem;
// appDisabled depends on whether the add-on is a foreignInstall so update
aNewAddon.appDisabled = !XPIDatabase.isUsableAddon(aNewAddon);
if (isDetectedInstall && aNewAddon.foreignInstall) {
// If the add-on is a foreign install and is in a scope where add-ons
// that were dropped in should default to disabled then disable it
let disablingScopes = Services.prefs.getIntPref(PREF_EM_AUTO_DISABLED_SCOPES, 0);
- if (aInstallLocation.scope & disablingScopes) {
- logger.warn("Disabling foreign installed add-on " + aNewAddon.id + " in "
- + aInstallLocation.name);
+ if (aLocation.scope & disablingScopes) {
+ logger.warn(`Disabling foreign installed add-on ${aNewAddon.id} in ${aLocation.name}`);
aNewAddon.userDisabled = true;
aNewAddon.seen = false;
}
}
return XPIDatabase.addToDatabase(aNewAddon, aAddonState.path);
},
@@ -2502,57 +2492,59 @@ this.XPIDatabaseReconcile = {
* Called when an add-on has been removed.
*
* @param {AddonInternal} aOldAddon
* The AddonInternal as it appeared the last time the application
* ran
*/
removeMetadata(aOldAddon) {
// This add-on has disappeared
- logger.debug("Add-on " + aOldAddon.id + " removed from " + aOldAddon.location);
+ logger.debug("Add-on " + aOldAddon.id + " removed from " + aOldAddon.location.name);
XPIDatabase.removeAddonMetadata(aOldAddon);
},
/**
* Updates an add-on's metadata and determines. This is called when either the
* add-on's install directory path or last modified time has changed.
*
- * @param {InstallLocation} aInstallLocation
+ * @param {XPIStateLocation} aLocation
* The install location containing the add-on
* @param {AddonInternal} aOldAddon
* The AddonInternal as it appeared the last time the application
* ran
* @param {XPIState} aAddonState
* The new state of the add-on
* @param {AddonInternal?} [aNewAddon]
* The manifest for the new add-on if it has already been loaded
* @returns {boolean?}
* A boolean indicating if flushing caches is required to complete
* changing this add-on
*/
- updateMetadata(aInstallLocation, aOldAddon, aAddonState, aNewAddon) {
- logger.debug("Add-on " + aOldAddon.id + " modified in " + aInstallLocation.name);
+ updateMetadata(aLocation, aOldAddon, aAddonState, aNewAddon) {
+ logger.debug(`Add-on ${aOldAddon.id} modified in ${aLocation.name}`);
try {
// If there isn't an updated install manifest for this add-on then load it.
if (!aNewAddon) {
let file = new nsIFile(aAddonState.path);
- aNewAddon = XPIInstall.syncLoadManifestFromFile(file, aInstallLocation, aOldAddon);
+ aNewAddon = XPIInstall.syncLoadManifestFromFile(file, aLocation, aOldAddon);
}
// The ID in the manifest that was loaded must match the ID of the old
// add-on.
if (aNewAddon.id != aOldAddon.id)
- throw new Error("Incorrect id in install manifest for existing add-on " + aOldAddon.id);
+ throw new Error(`Incorrect id in install manifest for existing add-on ${aOldAddon.id}`);
} catch (e) {
- logger.warn("updateMetadata: Add-on " + aOldAddon.id + " is invalid", e);
+ logger.warn(`updateMetadata: Add-on ${aOldAddon.id} is invalid`, e);
+
XPIDatabase.removeAddonMetadata(aOldAddon);
- XPIStates.removeAddon(aOldAddon.location, aOldAddon.id);
- if (!aInstallLocation.locked)
- aInstallLocation.uninstallAddon(aOldAddon.id);
+ aOldAddon.location.removeAddon(aOldAddon.id);
+
+ if (!aLocation.locked)
+ aLocation.installer.uninstallAddon(aOldAddon.id);
else
logger.warn("Could not uninstall invalid item from locked install location");
return null;
}
// Set the additional properties on the new AddonInternal
aNewAddon.updateDate = aAddonState.mtime;
@@ -2560,62 +2552,62 @@ this.XPIDatabaseReconcile = {
// Update the database
return XPIDatabase.updateAddonMetadata(aOldAddon, aNewAddon, aAddonState.path);
},
/**
* Updates an add-on's path for when the add-on has moved in the
* filesystem but hasn't changed in any other way.
*
- * @param {InstallLocation} aInstallLocation
+ * @param {XPIStateLocation} aLocation
* The install location containing the add-on
* @param {AddonInternal} aOldAddon
* The AddonInternal as it appeared the last time the application
* ran
* @param {XPIState} aAddonState
* The new state of the add-on
* @returns {AddonInternal}
*/
- updatePath(aInstallLocation, aOldAddon, aAddonState) {
- logger.debug("Add-on " + aOldAddon.id + " moved to " + aAddonState.path);
+ updatePath(aLocation, aOldAddon, aAddonState) {
+ logger.debug(`Add-on ${aOldAddon.id} moved to ${aAddonState.path}`);
aOldAddon.path = aAddonState.path;
aOldAddon._sourceBundle = new nsIFile(aAddonState.path);
return aOldAddon;
},
/**
* Called when no change has been detected for an add-on's metadata but the
* application has changed so compatibility may have changed.
*
- * @param {InstallLocation} aInstallLocation
+ * @param {XPIStateLocation} aLocation
* The install location containing the add-on
* @param {AddonInternal} aOldAddon
* The AddonInternal as it appeared the last time the application
* ran
* @param {XPIState} aAddonState
* The new state of the add-on
* @param {boolean} [aReloadMetadata = false]
* A boolean which indicates whether metadata should be reloaded from
* the addon manifests. Default to false.
* @returns {AddonInternal}
* The new addon.
*/
- updateCompatibility(aInstallLocation, aOldAddon, aAddonState, aReloadMetadata) {
- logger.debug("Updating compatibility for add-on " + aOldAddon.id + " in " + aInstallLocation.name);
+ updateCompatibility(aLocation, aOldAddon, aAddonState, aReloadMetadata) {
+ logger.debug(`Updating compatibility for add-on ${aOldAddon.id} in ${aLocation.name}`);
let checkSigning = (aOldAddon.signedState === undefined &&
AddonSettings.ADDON_SIGNING &&
SIGNED_TYPES.has(aOldAddon.type));
let manifest = null;
if (checkSigning || aReloadMetadata) {
try {
let file = new nsIFile(aAddonState.path);
- manifest = XPIInstall.syncLoadManifestFromFile(file, aInstallLocation);
+ manifest = XPIInstall.syncLoadManifestFromFile(file, aLocation);
} catch (err) {
// If we can no longer read the manifest, it is no longer compatible.
aOldAddon.brokenManifest = true;
aOldAddon.appDisabled = true;
return aOldAddon;
}
}
@@ -2644,17 +2636,17 @@ this.XPIDatabaseReconcile = {
return aOldAddon;
},
/**
* Returns true if this install location is part of the application
* bundle. Add-ons in these locations are expected to change whenever
* the application updates.
*
- * @param {InstallLocation} location
+ * @param {XPIStateLocation} location
* The install location to check.
* @returns {boolean}
* True if this location is part of the application bundle.
*/
isAppBundledLocation(location) {
return (location.name == KEY_APP_GLOBAL ||
location.name == KEY_APP_SYSTEM_DEFAULTS);
},
@@ -2677,17 +2669,17 @@ this.XPIDatabaseReconcile = {
* The schema has changed and all add-on manifests should be re-read.
* @returns {AddonInternal?}
* The updated AddonInternal object for the add-on, if one
* could be created.
*/
updateExistingAddon(oldAddon, xpiState, newAddon, aUpdateCompatibility, aSchemaChange) {
XPIDatabase.recordAddonTelemetry(oldAddon);
- let installLocation = oldAddon._installLocation;
+ let installLocation = oldAddon.location;
if (xpiState.mtime < oldAddon.updateDate) {
XPIProvider.setTelemetry(oldAddon.id, "olderFile", {
mtime: xpiState.mtime,
oldtime: oldAddon.updateDate
});
}
@@ -2735,130 +2727,128 @@ this.XPIDatabaseReconcile = {
* @param {boolean} aSchemaChange
* The schema has changed and all add-on manifests should be re-read.
* @returns {boolean}
* A boolean indicating if a change requiring flushing the caches was
* detected
*/
processFileChanges(aManifests, aUpdateCompatibility, aOldAppVersion, aOldPlatformVersion,
aSchemaChange) {
- let findManifest = (aInstallLocation, aId) => {
- return (aManifests[aInstallLocation.name] &&
- aManifests[aInstallLocation.name][aId]) || null;
+ let findManifest = (loc, id) => {
+ return (aManifests[loc.name] &&
+ aManifests[loc.name][id]) || null;
};
let addonExists = addon => addon._sourceBundle.exists();
let previousAddons = new ExtensionUtils.DefaultMap(() => new Map());
let currentAddons = new ExtensionUtils.DefaultMap(() => new Map());
// Get the previous add-ons from the database and put them into maps by location
for (let addon of XPIDatabase.getAddons()) {
- previousAddons.get(addon.location).set(addon.id, addon);
+ previousAddons.get(addon.location.name).set(addon.id, addon);
}
// Keep track of add-ons whose blocklist status may have changed. We'll check this
// after everything else.
let addonsToCheckAgainstBlocklist = [];
// Build the list of current add-ons into similar maps. When add-ons are still
// present we re-use the add-on objects from the database and update their
// details directly
let addonStates = new Map();
- for (let installLocation of XPIProvider.installLocations) {
- let locationAddons = currentAddons.get(installLocation.name);
+ for (let location of XPIStates.locations()) {
+ let locationAddons = currentAddons.get(location.name);
// Get all the on-disk XPI states for this location, and keep track of which
// ones we see in the database.
- let states = XPIStates.getLocation(installLocation.name) || new Map();
- let dbAddons = previousAddons.get(installLocation.name) || new Map();
+ let dbAddons = previousAddons.get(location.name) || new Map();
for (let [id, oldAddon] of dbAddons) {
// Check if the add-on is still installed
- let xpiState = states.get(id);
+ let xpiState = location.get(id);
if (xpiState) {
let newAddon = this.updateExistingAddon(oldAddon, xpiState,
- findManifest(installLocation, id),
+ findManifest(location, id),
aUpdateCompatibility, aSchemaChange);
if (newAddon) {
locationAddons.set(newAddon.id, newAddon);
// We need to do a blocklist check later, but the add-on may have changed by then.
// Avoid storing the current copy and just get one when we need one instead.
addonsToCheckAgainstBlocklist.push(newAddon.id);
}
} else {
// The add-on is in the DB, but not in xpiState (and thus not on disk).
this.removeMetadata(oldAddon);
}
}
- for (let [id, xpiState] of states) {
+ for (let [id, xpiState] of location) {
if (locationAddons.has(id))
continue;
- let newAddon = findManifest(installLocation, id);
- let addon = this.addMetadata(installLocation, id, xpiState, newAddon,
+ let newAddon = findManifest(location, id);
+ let addon = this.addMetadata(location, id, xpiState, newAddon,
aOldAppVersion, aOldPlatformVersion);
if (addon) {
locationAddons.set(addon.id, addon);
addonStates.set(addon, xpiState);
}
}
}
- // Remove metadata for any add-ons in install locations that are no
- // longer supported.
- for (let [locationName, addons] of previousAddons) {
- if (!currentAddons.has(locationName)) {
- for (let oldAddon of addons.values())
- this.removeMetadata(oldAddon);
- }
- }
-
// Validate the updated system add-ons
let hideLocation;
{
- let systemAddonLocation = XPIProvider.installLocationsByName[KEY_APP_SYSTEM_ADDONS];
+ let systemAddonLocation = XPIStates.getLocation(KEY_APP_SYSTEM_ADDONS);
let addons = currentAddons.get(systemAddonLocation.name);
- if (!systemAddonLocation.isValid(addons)) {
+ if (!systemAddonLocation.installer.isValid(addons)) {
// Hide the system add-on updates if any are invalid.
logger.info("One or more updated system add-ons invalid, falling back to defaults.");
hideLocation = systemAddonLocation.name;
}
}
// Apply startup changes to any currently-visible add-ons, and
// uninstall any which were previously visible, but aren't anymore.
let previousVisible = this.getVisibleAddons(previousAddons);
let currentVisible = this.flattenByID(currentAddons, hideLocation);
+ for (let addon of XPIDatabase.orphanedAddons.splice(0)) {
+ if (addon.visible) {
+ previousVisible.set(addon.id, addon);
+ }
+ }
+
let promises = [];
for (let [id, addon] of currentVisible) {
// If we have a stored manifest for the add-on, it came from the
// startup data cache, and supersedes any previous XPIStates entry.
- let xpiState = (!findManifest(addon._installLocation, id) &&
+ let xpiState = (!findManifest(addon.location, id) &&
addonStates.get(addon));
promises.push(this.applyStartupChange(addon, previousVisible.get(id), xpiState));
previousVisible.delete(id);
}
if (promises.some(p => p)) {
XPIInternal.awaitPromise(Promise.all(promises));
}
for (let [id, addon] of previousVisible) {
- if (addonExists(addon)) {
- XPIInternal.BootstrapScope.get(addon).uninstall();
+ if (addon.location) {
+ if (addonExists(addon)) {
+ XPIInternal.BootstrapScope.get(addon).uninstall();
+ }
+ addon.location.removeAddon(id);
+ addon.visible = false;
+ addon.active = false;
}
+
AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_UNINSTALLED, id);
- XPIStates.removeAddon(addon.location, id);
-
- addon.visible = false;
- addon.active = false;
}
if (previousVisible.size) {
XPIInstall.flushChromeCaches();
}
// Finally update XPIStates to match everything
for (let [locationName, locationAddons] of currentAddons) {
for (let [id, addon] of locationAddons) {
@@ -2912,17 +2902,17 @@ this.XPIDatabaseReconcile = {
let isActive = !currentAddon.disabled;
let wasActive = previousAddon ? previousAddon.active : currentAddon.active;
if (previousAddon) {
if (previousAddon !== currentAddon) {
AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED, id);
- if (previousAddon._installLocation &&
+ if (previousAddon.location &&
previousAddon._sourceBundle.exists() &&
!previousAddon._sourceBundle.equals(currentAddon._sourceBundle)) {
promise = XPIInternal.BootstrapScope.get(previousAddon).update(
currentAddon);
} else {
let reason = XPIInstall.newVersionReason(previousAddon.version, currentAddon.version);
XPIInternal.BootstrapScope.get(currentAddon).install(
reason, false, {oldVersion: previousAddon.version});
--- a/toolkit/mozapps/extensions/internal/XPIInstall.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIInstall.jsm
@@ -83,22 +83,21 @@ const PREF_DISTRO_ADDONS_PERMS =
const PREF_INSTALL_REQUIRESECUREORIGIN = "extensions.install.requireSecureOrigin";
const PREF_PENDING_OPERATIONS = "extensions.pendingOperations";
const PREF_SYSTEM_ADDON_UPDATE_URL = "extensions.systemAddon.update.url";
const PREF_XPI_ENABLED = "xpinstall.enabled";
const PREF_XPI_DIRECT_WHITELISTED = "xpinstall.whitelist.directRequest";
const PREF_XPI_FILE_WHITELISTED = "xpinstall.whitelist.fileRequest";
const PREF_XPI_WHITELIST_REQUIRED = "xpinstall.whitelist.required";
-/* globals BOOTSTRAP_REASONS, KEY_APP_SYSTEM_ADDONS, KEY_APP_SYSTEM_DEFAULTS, KEY_APP_TEMPORARY, PREF_BRANCH_INSTALLED_ADDON, PREF_SYSTEM_ADDON_SET, TEMPORARY_ADDON_SUFFIX, SIGNED_TYPES, TOOLKIT_ID, XPI_PERMISSION, XPIStates, getExternalType, isTheme, isWebExtension */
+/* globals BOOTSTRAP_REASONS, KEY_APP_SYSTEM_ADDONS, KEY_APP_SYSTEM_DEFAULTS, PREF_BRANCH_INSTALLED_ADDON, PREF_SYSTEM_ADDON_SET, TEMPORARY_ADDON_SUFFIX, SIGNED_TYPES, TOOLKIT_ID, XPI_PERMISSION, XPIStates, getExternalType, isTheme, isWebExtension */
const XPI_INTERNAL_SYMBOLS = [
"BOOTSTRAP_REASONS",
"KEY_APP_SYSTEM_ADDONS",
"KEY_APP_SYSTEM_DEFAULTS",
- "KEY_APP_TEMPORARY",
"PREF_BRANCH_INSTALLED_ADDON",
"PREF_SYSTEM_ADDON_SET",
"SIGNED_TYPES",
"TEMPORARY_ADDON_SUFFIX",
"TOOLKIT_ID",
"XPI_PERMISSION",
"XPIStates",
"getExternalType",
@@ -778,17 +777,17 @@ function generateTemporaryInstallID(aFil
const sess = TEMP_INSTALL_ID_GEN_SESSION;
hasher.update(sess, sess.length);
hasher.update(data, data.length);
let id = `${getHashStringForCrypto(hasher)}${TEMPORARY_ADDON_SUFFIX}`;
logger.info(`Generated temp id ${id} (${sess.join("")}) for ${aFile.path}`);
return id;
}
-var loadManifest = async function(aPackage, aInstallLocation, aOldAddon) {
+var loadManifest = async function(aPackage, aLocation, aOldAddon) {
async function loadFromRDF(aUri) {
let manifest = await aPackage.readString("install.rdf");
let addon = await loadManifestFromRDF(aUri, manifest, aPackage);
if (await aPackage.hasResource("icon.png")) {
addon.icons[32] = "icon.png";
addon.icons[48] = "icon.png";
}
@@ -807,17 +806,17 @@ var loadManifest = async function(aPacka
}
let isWebExtension = entry == FILE_WEB_MANIFEST;
let addon = isWebExtension ?
await loadManifestFromWebManifest(aPackage.rootURI) :
await loadFromRDF(aPackage.getURI("install.rdf"));
addon._sourceBundle = aPackage.file;
- addon._installLocation = aInstallLocation;
+ addon.location = aLocation;
addon.size = 0;
await aPackage.iterFiles(entry => {
if (!entry.isDir) {
addon.size += entry.size;
}
});
@@ -826,17 +825,17 @@ var loadManifest = async function(aPacka
if (isWebExtension && !addon.id) {
if (cert) {
addon.id = cert.commonName;
if (!gIDTest.test(addon.id)) {
throw new Error(`Webextension is signed with an invalid id (${addon.id})`);
}
}
- if (!addon.id && aInstallLocation.name == KEY_APP_TEMPORARY) {
+ if (!addon.id && aLocation.isTemporary) {
addon.id = generateTemporaryInstallID(aPackage.file);
}
}
await addon.updateBlocklistState({oldAddon: aOldAddon});
addon.appDisabled = !XPIDatabase.isUsableAddon(addon);
defineSyncGUID(addon);
@@ -844,42 +843,42 @@ var loadManifest = async function(aPacka
return addon;
};
/**
* Loads an add-on's manifest from the given file or directory.
*
* @param {nsIFile} aFile
* The file to load the manifest from.
- * @param {InstallLocation} aInstallLocation
+ * @param {XPIStateLocation} aLocation
* The install location the add-on is installed in, or will be
* installed to.
* @param {AddonInternal?} aOldAddon
* The currently-installed add-on with the same ID, if one exist.
* This is used to migrate user settings like the add-on's
* disabled state.
* @returns {AddonInternal}
* The parsed Addon object for the file's manifest.
*/
-var loadManifestFromFile = async function(aFile, aInstallLocation, aOldAddon) {
+var loadManifestFromFile = async function(aFile, aLocation, aOldAddon) {
let pkg = Package.get(aFile);
try {
- let addon = await loadManifest(pkg, aInstallLocation, aOldAddon);
+ let addon = await loadManifest(pkg, aLocation, aOldAddon);
return addon;
} finally {
pkg.close();
}
};
/*
* A synchronous method for loading an add-on's manifest. Do not use
* this.
*/
-function syncLoadManifestFromFile(aFile, aInstallLocation, aOldAddon) {
- return XPIInternal.awaitPromise(loadManifestFromFile(aFile, aInstallLocation, aOldAddon));
+function syncLoadManifestFromFile(aFile, aLocation, aOldAddon) {
+ return XPIInternal.awaitPromise(loadManifestFromFile(aFile, aLocation, aOldAddon));
}
function flushChromeCaches() {
// Init this, so it will get the notification.
Services.obs.notifyObservers(null, "startupcache-invalidate");
// Flush message manager cached scripts
Services.obs.notifyObservers(null, "message-manager-flush-caches");
// Also dispatch this event to child processes
@@ -958,21 +957,21 @@ function getSignedStatus(aRv, aCert, aAd
// Any other error indicates that either the add-on isn't signed or it
// is signed by a signature that doesn't chain to the trusted root.
return AddonManager.SIGNEDSTATE_UNKNOWN;
}
}
function shouldVerifySignedState(aAddon) {
// Updated system add-ons should always have their signature checked
- if (aAddon._installLocation.name == KEY_APP_SYSTEM_ADDONS)
+ if (aAddon.location.name == KEY_APP_SYSTEM_ADDONS)
return true;
// We don't care about signatures for default system add-ons
- if (aAddon._installLocation.name == KEY_APP_SYSTEM_DEFAULTS)
+ if (aAddon.location.name == KEY_APP_SYSTEM_DEFAULTS)
return false;
// Otherwise only check signatures if signing is enabled and the add-on is one
// of the signed types.
return AddonSettings.ADDON_SIGNING && SIGNED_TYPES.has(aAddon.type);
}
/**
@@ -1333,17 +1332,17 @@ function getHashStringForCrypto(aCrypto)
/**
* Base class for objects that manage the installation of an addon.
* This class isn't instantiated directly, see the derived classes below.
*/
class AddonInstall {
/**
* Instantiates an AddonInstall.
*
- * @param {InstallLocation} installLocation
+ * @param {XPIStateLocation} installLocation
* The install location the add-on will be installed into
* @param {nsIURL} url
* The nsIURL to get the add-on from. If this is an nsIFileURL then
* the add-on will not need to be downloaded
* @param {Object} [options = {}]
* Additional options for the install
* @param {string} [options.hash]
* An optional hash for the add-on
@@ -1357,17 +1356,17 @@ class AddonInstall {
* Optional icons for the add-on
* @param {string} [options.version]
* An optional version for the add-on
* @param {function(string) : Promise<void>} [options.promptHandler]
* A callback to prompt the user before installing.
*/
constructor(installLocation, url, options = {}) {
this.wrapper = new AddonInstallWrapper(this);
- this.installLocation = installLocation;
+ this.location = installLocation;
this.sourceURI = url;
if (options.hash) {
let hashSplit = options.hash.toLowerCase().split(":");
this.originalHash = {
algorithm: hashSplit[0],
data: hashSplit[1]
};
@@ -1469,19 +1468,19 @@ class AddonInstall {
logger.debug("Cancelling download of " + this.sourceURI.spec);
this.state = AddonManager.STATE_CANCELLED;
XPIInstall.installs.delete(this);
this._callInstallListeners("onDownloadCancelled");
this.removeTemporaryFile();
break;
case AddonManager.STATE_INSTALLED:
logger.debug("Cancelling install of " + this.addon.id);
- let xpi = getFile(`${this.addon.id}.xpi`, this.installLocation.getStagingDir());
+ let xpi = getFile(`${this.addon.id}.xpi`, this.location.installer.getStagingDir());
flushJarCache(xpi);
- this.installLocation.cleanStagingDir([this.addon.id, this.addon.id + ".xpi"]);
+ this.location.installer.cleanStagingDir([this.addon.id, this.addon.id + ".xpi"]);
this.state = AddonManager.STATE_CANCELLED;
XPIInstall.installs.delete(this);
if (this.existingAddon) {
delete this.existingAddon.pendingUpgrade;
this.existingAddon.pendingUpgrade = null;
}
@@ -1491,17 +1490,17 @@ class AddonInstall {
break;
case AddonManager.STATE_POSTPONED:
logger.debug(`Cancelling postponed install of ${this.addon.id}`);
this.state = AddonManager.STATE_CANCELLED;
XPIInstall.installs.delete(this);
this._callInstallListeners("onInstallCancelled");
this.removeTemporaryFile();
- let stagingDir = this.installLocation.getStagingDir();
+ let stagingDir = this.location.installer.getStagingDir();
let stagedAddon = stagingDir.clone();
this.unstageInstall(stagedAddon);
default:
throw new Error("Cannot cancel install of " + this.sourceURI.spec +
" from this state (" + this.state + ")");
}
}
@@ -1531,29 +1530,29 @@ class AddonInstall {
}
/**
* Removes the temporary file owned by this AddonInstall if there is one.
*/
removeTemporaryFile() {
// Only proceed if this AddonInstall owns its XPI file
if (!this.ownsTempFile) {
- this.logger.debug("removeTemporaryFile: " + this.sourceURI.spec + " does not own temp file");
+ this.logger.debug(`removeTemporaryFile: ${this.sourceURI.spec} does not own temp file`);
return;
}
try {
- this.logger.debug("removeTemporaryFile: " + this.sourceURI.spec + " removing temp file " +
- this.file.path);
+ this.logger.debug(`removeTemporaryFile: ${this.sourceURI.spec} removing temp file ` +
+ this.file.path);
this.file.remove(true);
this.ownsTempFile = false;
} catch (e) {
- this.logger.warn("Failed to remove temporary file " + this.file.path + " for addon " +
- this.sourceURI.spec,
- e);
+ this.logger.warn(`Failed to remove temporary file ${this.file.path} for addon ` +
+ this.sourceURI.spec,
+ e);
}
}
/**
* Updates the sourceURI and releaseNotesURI values on the Addon being
* installed by this AddonInstall instance.
*/
updateAddonURIs() {
@@ -1575,17 +1574,17 @@ class AddonInstall {
try {
pkg = Package.get(file);
} catch (e) {
return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, e]);
}
try {
try {
- this.addon = await loadManifest(pkg, this.installLocation, this.existingAddon);
+ this.addon = await loadManifest(pkg, this.location, this.existingAddon);
} catch (e) {
return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, e]);
}
if (!this.addon.id) {
let err = new Error(`Cannot find id for addon ${file.path}`);
return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, err]);
}
@@ -1732,35 +1731,35 @@ class AddonInstall {
this._callInstallListeners("onInstallCancelled");
return;
}
// Find and cancel any pending installs for the same add-on in the same
// install location
for (let install of XPIInstall.installs) {
if (install.state == AddonManager.STATE_INSTALLED &&
- install.installLocation == this.installLocation &&
+ install.location == this.location &&
install.addon.id == this.addon.id) {
logger.debug(`Cancelling previous pending install of ${install.addon.id}`);
install.cancel();
}
}
let isUpgrade = this.existingAddon &&
- this.existingAddon._installLocation == this.installLocation;
+ this.existingAddon.location == this.location;
logger.debug("Starting install of " + this.addon.id + " from " + this.sourceURI.spec);
AddonManagerPrivate.callAddonListeners("onInstalling",
this.addon.wrapper,
false);
- let stagedAddon = this.installLocation.getStagingDir();
+ let stagedAddon = this.location.installer.getStagingDir();
(async () => {
- await this.installLocation.requestStagingDir();
+ await this.location.installer.requestStagingDir();
// remove any previously staged files
await this.unstageInstall(stagedAddon);
stagedAddon.append(`${this.addon.id}.xpi`);
await this.stageInstall(false, stagedAddon, isUpgrade);
@@ -1769,30 +1768,30 @@ class AddonInstall {
let install = () => {
if (this.existingAddon && this.existingAddon.active && !isUpgrade) {
XPIDatabase.updateAddonActive(this.existingAddon, false);
}
// Install the new add-on into its final location
let existingAddonID = this.existingAddon ? this.existingAddon.id : null;
- let file = this.installLocation.installAddon({
+ let file = this.location.installer.installAddon({
id: this.addon.id,
source: stagedAddon,
existingAddonID
});
// Update the metadata in the database
this.addon._sourceBundle = file;
this.addon.visible = true;
if (isUpgrade) {
this.addon = XPIDatabase.updateAddonMetadata(this.existingAddon, this.addon,
file.path);
- let state = XPIStates.getAddon(this.installLocation.name, this.addon.id);
+ let state = this.location.get(this.addon.id);
if (state) {
state.syncWithDB(this.addon, true);
} else {
logger.warn("Unexpected missing XPI state for add-on ${id}", this.addon);
}
} else {
this.addon.active = (this.addon.visible && !this.addon.disabled);
this.addon = XPIDatabase.addToDatabase(this.addon, file.path);
@@ -1831,17 +1830,17 @@ class AddonInstall {
this.state = AddonManager.STATE_INSTALL_FAILED;
this.error = AddonManager.ERROR_FILE_ACCESS;
XPIInstall.installs.delete(this);
AddonManagerPrivate.callAddonListeners("onOperationCancelled",
this.addon.wrapper);
this._callInstallListeners("onInstallFailed");
}).then(() => {
this.removeTemporaryFile();
- return this.installLocation.releaseStagingDir();
+ return this.location.installer.releaseStagingDir();
});
}
/**
* Stages an add-on for install.
*
* @param {boolean} restartRequired
* If true, the final installation will be deferred until the
@@ -1858,53 +1857,52 @@ class AddonInstall {
await OS.File.copy(this.file.path, stagedAddon.path);
if (restartRequired) {
// Point the add-on to its extracted files as the xpi may get deleted
this.addon._sourceBundle = stagedAddon;
// Cache the AddonInternal as it may have updated compatibility info
- XPIStates.getLocation(this.installLocation.name).stageAddon(this.addon.id,
- this.addon.toJSON());
+ this.location.stageAddon(this.addon.id, this.addon.toJSON());
logger.debug(`Staged install of ${this.addon.id} from ${this.sourceURI.spec} ready; waiting for restart.`);
if (isUpgrade) {
delete this.existingAddon.pendingUpgrade;
this.existingAddon.pendingUpgrade = this.addon;
}
}
}
/**
* Removes any previously staged upgrade.
*
* @param {nsIFile} stagingDir
* The staging directory from which to unstage the install.
*/
async unstageInstall(stagingDir) {
- XPIStates.getLocation(this.installLocation.name).unstageAddon(this.addon.id);
+ this.location.unstageAddon(this.addon.id);
await removeAsync(getFile(this.addon.id, stagingDir));
await removeAsync(getFile(`${this.addon.id}.xpi`, stagingDir));
}
/**
* Postone a pending update, until restart or until the add-on resumes.
*
* @param {function} resumeFn
* A function for the add-on to run when resuming.
*/
async postpone(resumeFn) {
this.state = AddonManager.STATE_POSTPONED;
- let stagingDir = this.installLocation.getStagingDir();
-
- await this.installLocation.requestStagingDir();
+ let stagingDir = this.location.installer.getStagingDir();
+
+ await this.location.installer.requestStagingDir();
await this.unstageInstall(stagingDir);
let stagedAddon = getFile(`${this.addon.id}.xpi`, stagingDir);
await this.stageInstall(true, stagedAddon, true);
this._callInstallListeners("onInstallPostponed");
@@ -1926,17 +1924,17 @@ class AddonInstall {
break;
}
},
});
}
// Release the staging directory lock, but since the staging dir is populated
// it will not be removed until resumed or installed by restart.
// See also cleanStagingDir()
- this.installLocation.releaseStagingDir();
+ this.location.installer.releaseStagingDir();
}
_callInstallListeners(event, ...args) {
switch (event) {
case "onDownloadCancelled":
case "onDownloadFailed":
case "onInstallCancelled":
case "onInstallFailed":
@@ -2047,18 +2045,18 @@ var LocalAddonInstall = class extends Ad
return super.install();
}
};
var DownloadAddonInstall = class extends AddonInstall {
/**
* Instantiates a DownloadAddonInstall
*
- * @param {InstallLocation} installLocation
- * The InstallLocation the add-on will be installed into
+ * @param {XPIStateLocation} installLocation
+ * The XPIStateLocation the add-on will be installed into
* @param {nsIURL} url
* The nsIURL to get the add-on from
* @param {Object} [options = {}]
* Additional options for the install
* @param {string} [options.hash]
* An optional hash for the add-on
* @param {AddonInternal} [options.existingAddon]
* The add-on this install will update if known
@@ -2457,20 +2455,20 @@ function createUpdate(aCallback, aAddon,
existingAddon: aAddon,
name: aAddon.selectedLocale.name,
type: aAddon.type,
icons: aAddon.icons,
version: aUpdate.version,
};
let install;
if (url instanceof Ci.nsIFileURL) {
- install = new LocalAddonInstall(aAddon._installLocation, url, opts);
+ install = new LocalAddonInstall(aAddon.location, url, opts);
await install.init();
} else {
- install = new DownloadAddonInstall(aAddon._installLocation, url, opts);
+ install = new DownloadAddonInstall(aAddon.location, url, opts);
}
try {
if (aUpdate.updateInfoURL)
install.releaseNotesURI = Services.io.newURI(escapeAddonURI(aAddon, aUpdate.updateInfoURL));
} catch (e) {
// If the releaseNotesURI cannot be parsed then just ignore it.
}
@@ -2693,17 +2691,17 @@ UpdateChecker.prototype = {
null :
await AddonRepository.getCompatibilityOverrides(this.addon.id);
let update = await AUC.getNewestCompatibleUpdate(
aUpdates, this.appVersion, this.platformVersion,
ignoreMaxVersion, ignoreStrictCompat, compatOverrides);
if (update && Services.vc.compare(this.addon.version, update.version) < 0
- && !this.addon._installLocation.locked) {
+ && !this.addon.location.locked) {
for (let currentInstall of XPIInstall.installs) {
// Skip installs that don't match the available update
if (currentInstall.existingAddon != this.addon ||
currentInstall.version != update.version)
continue;
// If the existing install has not yet started downloading then send an
// available update notification. If it is already downloading then
@@ -2751,62 +2749,75 @@ UpdateChecker.prototype = {
}
};
/**
* Creates a new AddonInstall to install an add-on from a local file.
*
* @param {nsIFile} file
* The file to install
- * @param {InstallLocation} location
+ * @param {XPIStateLocation} location
* The location to install to
* @returns {Promise<AddonInstall>}
* A Promise that resolves with the new install object.
*/
function createLocalInstall(file, location) {
if (!location) {
- location = XPIProvider.installLocationsByName[KEY_APP_PROFILE];
+ location = XPIStates.getLocation(KEY_APP_PROFILE);
}
let url = Services.io.newFileURI(file);
try {
let install = new LocalAddonInstall(location, url);
return install.init().then(() => install);
} catch (e) {
logger.error("Error creating install", e);
XPIInstall.installs.delete(this);
return Promise.resolve(null);
}
}
-// These are partial classes which contain the install logic for the
-// homonymous classes in XPIProvider.jsm. Those classes forward calls to
-// their install methods to these classes, with the `this` value set to
-// an instance the class as defined in XPIProvider.
-class DirectoryInstallLocation {}
-
-class MutableDirectoryInstallLocation extends DirectoryInstallLocation {
+class DirectoryInstaller {
+ constructor(location) {
+ this.location = location;
+
+ this._stagingDirLock = 0;
+ this._stagingDirPromise = null;
+ }
+
+ get name() {
+ return this.location.name;
+ }
+
+ get dir() {
+ return this.location.dir;
+ }
+ set dir(val) {
+ this.location.dir = val;
+ this.location.path = val.path;
+ }
+
/**
* Gets the staging directory to put add-ons that are pending install and
* uninstall into.
*
* @returns {nsIFile}
*/
getStagingDir() {
- return getFile(DIR_STAGE, this._directory);
+ return getFile(DIR_STAGE, this.dir);
}
requestStagingDir() {
this._stagingDirLock++;
if (this._stagingDirPromise)
return this._stagingDirPromise;
- OS.File.makeDir(this._directory.path);
- let stagepath = OS.Path.join(this._directory.path, DIR_STAGE);
+ OS.File.makeDir(this.dir.path);
+ let stagepath = OS.Path.join(this.dir.path, DIR_STAGE);
return this._stagingDirPromise = OS.File.makeDir(stagepath).catch((e) => {
if (e instanceof OS.File.Error && e.becauseExists)
return;
logger.error("Failed to create staging directory", e);
throw e;
});
}
@@ -2861,17 +2872,17 @@ class MutableDirectoryInstallLocation ex
* Returns a directory that is normally on the same filesystem as the rest of
* the install location and can be used for temporarily storing files during
* safe move operations. Calling this method will delete the existing trash
* directory and its contents.
*
* @returns {nsIFile}
*/
getTrashDir() {
- let trashDir = getFile(DIR_TRASH, this._directory);
+ let trashDir = getFile(DIR_TRASH, this.dir);
let trashDirExists = trashDir.exists();
try {
if (trashDirExists)
recursiveRemove(trashDir);
trashDirExists = false;
} catch (e) {
logger.warn("Failed to remove trash directory", e);
}
@@ -2905,21 +2916,21 @@ class MutableDirectoryInstallLocation ex
* An nsIFile indicating where the add-on was installed to
*/
installAddon({ id, source, existingAddonID, action = "move" }) {
let trashDir = this.getTrashDir();
let transaction = new SafeInstallOperation();
let moveOldAddon = aId => {
- let file = getFile(aId, this._directory);
+ let file = getFile(aId, this.dir);
if (file.exists())
transaction.moveUnder(file, trashDir);
- file = getFile(`${aId}.xpi`, this._directory);
+ file = getFile(`${aId}.xpi`, this.dir);
if (file.exists()) {
flushJarCache(file);
transaction.moveUnder(file, trashDir);
}
};
// If any of these operations fails the finally block will clean up the
// temporary directory
@@ -2948,152 +2959,149 @@ class MutableDirectoryInstallLocation ex
}
transaction.moveTo(oldDataDir, newDataDir);
}
}
}
if (action == "copy") {
- transaction.copy(source, this._directory);
+ transaction.copy(source, this.dir);
} else if (action == "move") {
flushJarCache(source);
- transaction.moveUnder(source, this._directory);
+ transaction.moveUnder(source, this.dir);
}
// Do nothing for the proxy file as we sideload an addon permanently
} finally {
// It isn't ideal if this cleanup fails but it isn't worth rolling back
// the install because of it.
try {
recursiveRemove(trashDir);
} catch (e) {
- logger.warn("Failed to remove trash directory when installing " + id, e);
+ logger.warn(`Failed to remove trash directory when installing ${id}`, e);
}
}
- let newFile = this._directory.clone();
+ let newFile = this.dir.clone();
if (action == "proxy") {
// When permanently installing sideloaded addon, we just put a proxy file
// referring to the addon sources
newFile.append(id);
writeStringToFile(newFile, source.path);
} else {
newFile.append(source.leafName);
}
try {
newFile.lastModifiedTime = Date.now();
} catch (e) {
- logger.warn("failed to set lastModifiedTime on " + newFile.path, e);
- }
- this._IDToFileMap[id] = newFile;
-
- if (existingAddonID && existingAddonID != id &&
- existingAddonID in this._IDToFileMap) {
- delete this._IDToFileMap[existingAddonID];
+ logger.warn(`failed to set lastModifiedTime on ${newFile.path}`, e);
}
return newFile;
}
/**
* Uninstalls an add-on from this location.
*
* @param {string} aId
* The ID of the add-on to uninstall
* @throws if the ID does not match any of the add-ons installed
*/
uninstallAddon(aId) {
- let file = this._IDToFileMap[aId];
- if (!file) {
- logger.warn("Attempted to remove " + aId + " from " +
- this._name + " but it was already gone");
- return;
- }
-
- file = getFile(aId, this._directory);
+ let file = getFile(aId, this.dir);
if (!file.exists())
file.leafName += ".xpi";
if (!file.exists()) {
- logger.warn("Attempted to remove " + aId + " from " +
- this._name + " but it was already gone");
-
- delete this._IDToFileMap[aId];
+ logger.warn(`Attempted to remove ${aId} from ${this.name} but it was already gone`);
+ this.location.delete(aId);
return;
}
let trashDir = this.getTrashDir();
if (file.leafName != aId) {
- logger.debug("uninstallAddon: flushing jar cache " + file.path + " for addon " + aId);
+ logger.debug(`uninstallAddon: flushing jar cache ${file.path} for addon ${aId}`);
flushJarCache(file);
}
let transaction = new SafeInstallOperation();
try {
transaction.moveUnder(file, trashDir);
} finally {
// It isn't ideal if this cleanup fails, but it is probably better than
// rolling back the uninstall at this point
try {
recursiveRemove(trashDir);
} catch (e) {
- logger.warn("Failed to remove trash directory when uninstalling " + aId, e);
+ logger.warn(`Failed to remove trash directory when uninstalling ${aId}`, e);
}
}
- XPIStates.removeAddon(this.name, aId);
-
- delete this._IDToFileMap[aId];
+ this.location.removeAddon(aId);
}
}
-class SystemAddonInstallLocation extends MutableDirectoryInstallLocation {
+class SystemAddonInstaller extends DirectoryInstaller {
+ constructor(location) {
+ super(location);
+
+ this._baseDir = location._baseDir;
+ this._nextDir = null;
+ }
+
+ get _addonSet() {
+ return this.location._addonSet;
+ }
+ set _addonSet(val) {
+ this.location._addonSet = val;
+ }
+
/**
* Saves the current set of system add-ons
*
* @param {Object} aAddonSet - object containing schema, directory and set
* of system add-on IDs and versions.
*/
static _saveAddonSet(aAddonSet) {
Services.prefs.setStringPref(PREF_SYSTEM_ADDON_SET, JSON.stringify(aAddonSet));
}
static _loadAddonSet() {
- return XPIInternal.SystemAddonInstallLocation._loadAddonSet();
+ return XPIInternal.SystemAddonLocation._loadAddonSet();
}
/**
* Gets the staging directory to put add-ons that are pending install and
* uninstall into.
*
* @returns {nsIFile}
* Staging directory for system add-on upgrades.
*/
getStagingDir() {
- this._addonSet = SystemAddonInstallLocation._loadAddonSet();
+ this._addonSet = SystemAddonInstaller._loadAddonSet();
let dir = null;
if (this._addonSet.directory) {
- this._directory = getFile(this._addonSet.directory, this._baseDir);
- dir = getFile(DIR_STAGE, this._directory);
+ this.dir = getFile(this._addonSet.directory, this._baseDir);
+ dir = getFile(DIR_STAGE, this.dir);
} else {
- logger.info("SystemAddonInstallLocation directory is missing");
+ logger.info("SystemAddonInstaller directory is missing");
}
return dir;
}
requestStagingDir() {
- this._addonSet = SystemAddonInstallLocation._loadAddonSet();
+ this._addonSet = SystemAddonInstaller._loadAddonSet();
if (this._addonSet.directory) {
- this._directory = getFile(this._addonSet.directory, this._baseDir);
+ this.dir = getFile(this._addonSet.directory, this._baseDir);
}
return super.requestStagingDir();
}
isValidAddon(aAddon) {
if (aAddon.appDisabled) {
logger.warn(`System add-on ${aAddon.id} isn't compatible with the application.`);
return false;
@@ -3135,17 +3143,17 @@ class SystemAddonInstallLocation extends
*/
async resetAddonSet() {
logger.info("Removing all system add-on upgrades.");
// remove everything from the pref first, if uninstall
// fails then at least they will not be re-activated on
// next restart.
this._addonSet = { schema: 1, addons: {} };
- SystemAddonInstallLocation._saveAddonSet(this._addonSet);
+ SystemAddonInstaller._saveAddonSet(this._addonSet);
// If this is running at app startup, the pref being cleared
// will cause later stages of startup to notice that the
// old updates are now gone.
//
// Updates will only be explicitly uninstalled if they are
// removed restartlessly, for instance if they are no longer
// part of the latest update set.
@@ -3181,17 +3189,17 @@ class SystemAddonInstallLocation extends
try {
for (;;) {
let {value: entry, done} = await iterator.next();
if (done) {
break;
}
// Skip the directory currently in use
- if (this._directory && this._directory.path == entry.path) {
+ if (this.dir && this.dir.path == entry.path) {
continue;
}
// Skip the next directory
if (this._nextDir && this._nextDir.path == entry.path) {
continue;
}
@@ -3219,17 +3227,17 @@ class SystemAddonInstallLocation extends
* add-on set in prefs.
*
* @param {Array} aAddons - An array of addons to install.
*/
async installAddonSet(aAddons) {
// Make sure the base dir exists
await OS.File.makeDir(this._baseDir.path, { ignoreExisting: true });
- let addonSet = SystemAddonInstallLocation._loadAddonSet();
+ let addonSet = SystemAddonInstaller._loadAddonSet();
// Remove any add-ons that are no longer part of the set.
for (let addonID of Object.keys(addonSet.addons)) {
if (!aAddons.includes(addonID)) {
AddonManager.getAddonByID(addonID).then(a => a.uninstall());
}
}
@@ -3243,72 +3251,71 @@ class SystemAddonInstallLocation extends
break;
} catch (e) {
logger.debug("Could not create new system add-on updates dir, retrying", e);
}
}
// Record the new upgrade directory.
let state = { schema: 1, directory: newDir.leafName, addons: {} };
- SystemAddonInstallLocation._saveAddonSet(state);
+ SystemAddonInstaller._saveAddonSet(state);
this._nextDir = newDir;
- let location = this;
let installs = [];
for (let addon of aAddons) {
- let install = await createLocalInstall(addon._sourceBundle, location);
+ let install = await createLocalInstall(addon._sourceBundle, this.location);
installs.push(install);
}
async function installAddon(install) {
// Make the new install own its temporary file.
install.ownsTempFile = true;
install.install();
}
async function postponeAddon(install) {
let resumeFn;
if (AddonManagerPrivate.hasUpgradeListener(install.addon.id)) {
logger.info(`system add-on ${install.addon.id} has an upgrade listener, postponing upgrade set until restart`);
resumeFn = () => {
logger.info(`${install.addon.id} has resumed a previously postponed addon set`);
- install.installLocation.resumeAddonSet(installs);
+ install.location.installer.resumeAddonSet(installs);
};
}
await install.postpone(resumeFn);
}
let previousState;
try {
// All add-ons in position, create the new state and store it in prefs
state = { schema: 1, directory: newDir.leafName, addons: {} };
for (let addon of aAddons) {
state.addons[addon.id] = {
version: addon.version
};
}
- previousState = SystemAddonInstallLocation._loadAddonSet();
- SystemAddonInstallLocation._saveAddonSet(state);
+ previousState = SystemAddonInstaller._loadAddonSet();
+ SystemAddonInstaller._saveAddonSet(state);
let blockers = aAddons.filter(
addon => AddonManagerPrivate.hasUpgradeListener(addon.id)
);
if (blockers.length > 0) {
await waitForAllPromises(installs.map(postponeAddon));
} else {
await waitForAllPromises(installs.map(installAddon));
}
} catch (e) {
// Roll back to previous upgrade set (if present) on restart.
if (previousState) {
- SystemAddonInstallLocation._saveAddonSet(previousState);
+ SystemAddonInstaller._saveAddonSet(previousState);
}
// Otherwise, roll back to built-in set on restart.
// TODO try to do these restartlessly
this.resetAddonSet();
try {
await OS.File.removeDir(newDir.path, { ignorePermissions: true });
} catch (e) {
@@ -3322,17 +3329,17 @@ class SystemAddonInstallLocation extends
* Resumes upgrade of a previously-delayed add-on set.
*
* @param {AddonInstall[]} installs
* The set of installs to resume.
*/
async resumeAddonSet(installs) {
async function resumeAddon(install) {
install.state = AddonManager.STATE_DOWNLOADED;
- install.installLocation.releaseStagingDir();
+ install.location.installer.releaseStagingDir();
install.install();
}
let blockers = installs.filter(
install => AddonManagerPrivate.hasUpgradeListener(install.addon.id)
);
if (blockers.length > 1) {
@@ -3346,17 +3353,17 @@ class SystemAddonInstallLocation extends
* Returns a directory that is normally on the same filesystem as the rest of
* the install location and can be used for temporarily storing files during
* safe move operations. Calling this method will delete the existing trash
* directory and its contents.
*
* @returns {nsIFile}
*/
getTrashDir() {
- let trashDir = getFile(DIR_TRASH, this._directory);
+ let trashDir = getFile(DIR_TRASH, this.dir);
let trashDirExists = trashDir.exists();
try {
if (trashDirExists)
recursiveRemove(trashDir);
trashDirExists = false;
} catch (e) {
logger.warn("Failed to remove trash directory", e);
}
@@ -3380,35 +3387,34 @@ class SystemAddonInstallLocation extends
let trashDir = this.getTrashDir();
let transaction = new SafeInstallOperation();
// If any of these operations fails the finally block will clean up the
// temporary directory
try {
flushJarCache(source);
- transaction.moveUnder(source, this._directory);
+ transaction.moveUnder(source, this.dir);
} finally {
// It isn't ideal if this cleanup fails but it isn't worth rolling back
// the install because of it.
try {
recursiveRemove(trashDir);
} catch (e) {
- logger.warn("Failed to remove trash directory when installing " + id, e);
+ logger.warn(`Failed to remove trash directory when installing ${id}`, e);
}
}
- let newFile = getFile(source.leafName, this._directory);
+ let newFile = getFile(source.leafName, this.dir);
try {
newFile.lastModifiedTime = Date.now();
} catch (e) {
logger.warn("failed to set lastModifiedTime on " + newFile.path, e);
}
- this._IDToFileMap[id] = newFile;
return newFile;
}
// old system add-on upgrade dirs get automatically removed
uninstallAddon(aAddon) {}
}
@@ -3423,82 +3429,78 @@ var XPIInstall = {
recursiveRemove,
syncLoadManifestFromFile,
/**
* @param {string} id
* The expected ID of the add-on.
* @param {nsIFile} file
* The XPI file to install the add-on from.
- * @param {InstallLocation} location
+ * @param {XPIStateLocation} location
* The install location to install the add-on to.
* @returns {AddonInternal}
* The installed Addon object, upon success.
*/
async installDistributionAddon(id, file, location) {
let addon = await loadManifestFromFile(file, location);
if (addon.id != id) {
throw new Error(`File file ${file.path} contains an add-on with an incorrect ID`);
}
- let existingEntry = null;
- try {
- existingEntry = location.getLocationForID(id);
- } catch (e) {
- }
-
- if (existingEntry) {
+ let state = location.get(id);
+
+ if (state) {
try {
- let existingAddon = await loadManifestFromFile(existingEntry, location);
+ let existingAddon = await loadManifestFromFile(state.file, location);
if (Services.vc.compare(addon.version, existingAddon.version) <= 0)
return null;
} catch (e) {
// Bad add-on in the profile so just proceed and install over the top
logger.warn("Profile contains an add-on with a bad or missing install " +
- `manifest at ${existingEntry.path}, overwriting`, e);
+ `manifest at ${state.path}, overwriting`, e);
}
} else if (Services.prefs.getBoolPref(PREF_BRANCH_INSTALLED_ADDON + id, false)) {
return null;
}
// Install the add-on
- addon._sourceBundle = location.installAddon({ id, source: file, action: "copy" });
+ addon._sourceBundle = location.installer.installAddon({ id, source: file, action: "copy" });
if (Services.prefs.getBoolPref(PREF_DISTRO_ADDONS_PERMS, false)) {
addon.userDisabled = true;
if (!XPIProvider.newDistroAddons) {
XPIProvider.newDistroAddons = new Set();
}
XPIProvider.newDistroAddons.add(id);
}
XPIStates.addAddon(addon);
- logger.debug("Installed distribution add-on " + id);
+ logger.debug(`Installed distribution add-on ${id}`);
Services.prefs.setBoolPref(PREF_BRANCH_INSTALLED_ADDON + id, true);
return addon;
},
/**
* Completes the install of an add-on which was staged during the last
* session.
*
* @param {string} id
* The expected ID of the add-on.
* @param {object} metadata
* The parsed metadata for the staged install.
- * @param {InstallLocation} location
+ * @param {XPIStateLocation} location
* The install location to install the add-on to.
* @returns {AddonInternal}
* The installed Addon object, upon success.
*/
async installStagedAddon(id, metadata, location) {
- let source = getFile(`${id}.xpi`, location.getStagingDir());
+ let source = getFile(`${id}.xpi`, location.installer.getStagingDir());
// Check that the directory's name is a valid ID.
if (!gIDTest.test(id) || !source.exists() || !source.isFile()) {
throw new Error(`Ignoring invalid staging directory entry: ${id}`);
}
let addon = await loadManifestFromFile(source, location);
@@ -3521,56 +3523,58 @@ var XPIInstall = {
XPIInternal.get(existingAddon).uninstall(reason, {newVersion});
}
} catch (e) {
Cu.reportError(e);
}
}
try {
- addon._sourceBundle = location.installAddon({
+ addon._sourceBundle = location.installer.installAddon({
id, source, existingAddonID: id,
});
XPIStates.addAddon(addon);
} catch (e) {
if (existingAddon) {
// Re-install the old add-on
XPIInternal.get(existingAddon).install();
}
throw e;
}
return addon;
},
async updateSystemAddons() {
- let systemAddonLocation = XPIProvider.installLocationsByName[KEY_APP_SYSTEM_ADDONS];
+ let systemAddonLocation = XPIStates.getLocation(KEY_APP_SYSTEM_ADDONS);
if (!systemAddonLocation)
return;
+ let installer = systemAddonLocation.installer;
+
// Don't do anything in safe mode
if (Services.appinfo.inSafeMode)
return;
// Download the list of system add-ons
let url = Services.prefs.getStringPref(PREF_SYSTEM_ADDON_UPDATE_URL, null);
if (!url) {
- await systemAddonLocation.cleanDirectories();
+ await installer.cleanDirectories();
return;
}
url = await UpdateUtils.formatUpdateURL(url);
logger.info(`Starting system add-on update check from ${url}.`);
let res = await ProductAddonChecker.getProductAddonList(url);
// If there was no list then do nothing.
if (!res || !res.gmpAddons) {
logger.info("No system add-ons list was returned.");
- await systemAddonLocation.cleanDirectories();
+ await installer.cleanDirectories();
return;
}
let addonList = new Map(
res.gmpAddons.map(spec => [spec.id, { spec, path: null, addon: null }]));
let setMatches = (wanted, existing) => {
if (wanted.size != existing.size)
@@ -3587,27 +3591,27 @@ var XPIInstall = {
return true;
};
// If this matches the current set in the profile location then do nothing.
let updatedAddons = addonMap(await XPIDatabase.getAddonsInLocation(KEY_APP_SYSTEM_ADDONS));
if (setMatches(addonList, updatedAddons)) {
logger.info("Retaining existing updated system add-ons.");
- await systemAddonLocation.cleanDirectories();
+ await installer.cleanDirectories();
return;
}
// If this matches the current set in the default location then reset the
// updated set.
let defaultAddons = addonMap(await XPIDatabase.getAddonsInLocation(KEY_APP_SYSTEM_DEFAULTS));
if (setMatches(addonList, defaultAddons)) {
logger.info("Resetting system add-ons.");
- systemAddonLocation.resetAddonSet();
- await systemAddonLocation.cleanDirectories();
+ installer.resetAddonSet();
+ await installer.cleanDirectories();
return;
}
// Download all the add-ons
async function downloadAddon(item) {
try {
let sourceAddon = updatedAddons.get(item.spec.id);
if (sourceAddon && sourceAddon.version == item.spec.version) {
@@ -3646,30 +3650,30 @@ var XPIInstall = {
return false;
}
if (item.spec.version != item.addon.version) {
logger.warn(`Expected system add-on ${item.spec.id} to be version ${item.spec.version} but was ${item.addon.version}.`);
return false;
}
- if (!systemAddonLocation.isValidAddon(item.addon))
+ if (!installer.isValidAddon(item.addon))
return false;
return true;
};
if (!Array.from(addonList.values()).every(item => item.path && item.addon && validateAddon(item))) {
throw new Error("Rejecting updated system add-on set that either could not " +
"be downloaded or contained unusable add-ons.");
}
// Install into the install location
logger.info("Installing new system add-on set");
- await systemAddonLocation.installAddonSet(Array.from(addonList.values())
+ await installer.installAddonSet(Array.from(addonList.values())
.map(a => a.addon));
},
/**
* Called to test whether installing XPI add-ons is enabled.
*
* @returns {boolean}
* True if installing is enabled.
@@ -3757,17 +3761,17 @@ var XPIInstall = {
* Icon URLs for the install
* @param {string} [aVersion]
* A version for the install
* @param {XULElement?} [aBrowser]
* The browser performing the install
* @returns {AddonInstall}
*/
async getInstallForURL(aUrl, aHash, aName, aIcons, aVersion, aBrowser) {
- let location = XPIProvider.installLocationsByName[KEY_APP_PROFILE];
+ let location = XPIStates.getLocation(KEY_APP_PROFILE);
let url = Services.io.newURI(aUrl);
let options = {
hash: aHash,
browser: aBrowser,
name: aName,
icons: aIcons,
version: aVersion,
@@ -3830,17 +3834,17 @@ var XPIInstall = {
async installTemporaryAddon(aFile) {
let installLocation = XPIInternal.TemporaryInstallLocation;
if (aFile.exists() && aFile.isFile()) {
flushJarCache(aFile);
}
let addon = await loadManifestFromFile(aFile, installLocation);
- installLocation.installAddon({ id: addon.id, source: aFile });
+ installLocation.installer.installAddon({ id: addon.id, source: aFile });
if (addon.appDisabled) {
let message = `Add-on ${addon.id} is not compatible with application version.`;
let app = addon.matchingTargetApplication;
if (app) {
if (app.minVersion) {
message += ` add-on minVersion: ${app.minVersion}.`;
@@ -3918,50 +3922,50 @@ var XPIInstall = {
* Force this addon into the pending uninstall state (used
* e.g. while the add-on manager is open and offering an
* "undo" button)
* @throws if the addon cannot be uninstalled because it is in an install
* location that does not allow it
*/
async uninstallAddon(aAddon, aForcePending) {
if (!(aAddon.inDatabase))
- throw new Error("Cannot uninstall addon " + aAddon.id + " because it is not installed");
-
- if (aAddon._installLocation.locked)
- throw new Error("Cannot uninstall addon " + aAddon.id
- + " from locked install location " + aAddon._installLocation.name);
+ throw new Error(`Cannot uninstall addon ${aAddon.id} because it is not installed`);
+
+ if (aAddon.location.locked)
+ throw new Error(`Cannot uninstall addon ${aAddon.id} ` +
+ `from locked install location ${aAddon.location.name}`);
if (aForcePending && aAddon.pendingUninstall)
throw new Error("Add-on is already marked to be uninstalled");
aAddon._hasResourceCache.clear();
if (aAddon._updateCheck) {
- logger.debug("Cancel in-progress update check for " + aAddon.id);
+ logger.debug(`Cancel in-progress update check for ${aAddon.id}`);
aAddon._updateCheck.cancel();
}
let wasPending = aAddon.pendingUninstall;
if (aForcePending) {
// We create an empty directory in the staging directory to indicate
// that an uninstall is necessary on next startup. Temporary add-ons are
// automatically uninstalled on shutdown anyway so there is no need to
// do this for them.
- if (aAddon._installLocation.name != KEY_APP_TEMPORARY) {
- let stage = getFile(aAddon.id, aAddon._installLocation.getStagingDir());
+ if (!aAddon.location.isTemporary) {
+ let stage = getFile(aAddon.id, aAddon.location.installer.getStagingDir());
if (!stage.exists())
stage.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
}
XPIDatabase.setAddonProperties(aAddon, {
pendingUninstall: true
});
Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
- let xpiState = XPIStates.getAddon(aAddon.location, aAddon.id);
+ let xpiState = aAddon.location.get(aAddon.id);
if (xpiState) {
xpiState.enabled = false;
XPIStates.save();
} else {
logger.warn("Can't find XPI state while uninstalling ${id} from ${location}", aAddon);
}
}
@@ -3972,32 +3976,32 @@ var XPIInstall = {
let wrapper = aAddon.wrapper;
// If the add-on wasn't already pending uninstall then notify listeners.
if (!wasPending) {
AddonManagerPrivate.callAddonListeners("onUninstalling", wrapper,
!!aForcePending);
}
- let existingAddon = XPIStates.findAddon(aAddon.id, loc =>
- loc.name != aAddon._installLocation.name);
+ let existingAddon = XPIStates.findAddon(aAddon.id,
+ loc => loc != aAddon.location);
let bootstrap = XPIInternal.BootstrapScope.get(aAddon);
if (!aForcePending) {
let existing;
if (existingAddon) {
existing = await XPIDatabase.getAddonInLocation(aAddon.id, existingAddon.location.name);
}
let uninstall = () => {
XPIStates.disableAddon(aAddon.id);
- aAddon._installLocation.uninstallAddon(aAddon.id);
+ aAddon.location.installer.uninstallAddon(aAddon.id);
XPIDatabase.removeAddonMetadata(aAddon);
- XPIStates.removeAddon(aAddon.location, aAddon.id);
+ aAddon.location.removeAddon(aAddon.id);
AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper);
if (existing) {
XPIDatabase.makeAddonVisible(existing);
AddonManagerPrivate.callAddonListeners("onInstalling", existing.wrapper, false);
if (!existing.disabled) {
XPIDatabase.updateAddonActive(existing, true);
@@ -4005,17 +4009,17 @@ var XPIInstall = {
}
};
if (existing) {
await bootstrap.update(existing, !existing.disabled, uninstall);
AddonManagerPrivate.callAddonListeners("onInstalled", existing.wrapper);
} else {
- XPIStates.removeAddon(aAddon.location, aAddon.id);
+ aAddon.location.removeAddon(aAddon.id);
await bootstrap.uninstall();
uninstall();
}
} else if (aAddon.active) {
XPIStates.disableAddon(aAddon.id);
bootstrap.shutdown(BOOTSTRAP_REASONS.ADDON_UNINSTALL);
XPIDatabase.updateAddonActive(aAddon, false);
}
@@ -4032,27 +4036,27 @@ var XPIInstall = {
* The DBAddonInternal to cancel uninstall for
*/
cancelUninstallAddon(aAddon) {
if (!(aAddon.inDatabase))
throw new Error("Can only cancel uninstall for installed addons.");
if (!aAddon.pendingUninstall)
throw new Error("Add-on is not marked to be uninstalled");
- if (aAddon._installLocation.name != KEY_APP_TEMPORARY)
- aAddon._installLocation.cleanStagingDir([aAddon.id]);
+ if (!aAddon.location.isTemporary)
+ aAddon.location.installer.cleanStagingDir([aAddon.id]);
XPIDatabase.setAddonProperties(aAddon, {
pendingUninstall: false
});
if (!aAddon.visible)
return;
- XPIStates.getAddon(aAddon.location, aAddon.id).syncWithDB(aAddon);
+ aAddon.location.get(aAddon.id).syncWithDB(aAddon);
XPIStates.save();
Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
// TODO hide hidden add-ons (bug 557710)
let wrapper = aAddon.wrapper;
AddonManagerPrivate.callAddonListeners("onOperationCancelled", wrapper);
@@ -4061,11 +4065,11 @@ var XPIInstall = {
XPIDatabase.updateAddonActive(aAddon, true);
}
// Notify any other providers that this theme is now enabled again.
if (isTheme(aAddon.type) && aAddon.active)
AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type, false);
},
- MutableDirectoryInstallLocation,
- SystemAddonInstallLocation,
+ DirectoryInstaller,
+ SystemAddonInstaller,
};
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -360,25 +360,22 @@ function isTheme(type) {
*/
function canRunInSafeMode(aAddon) {
// Even though the updated system add-ons aren't generally run in safe mode we
// include them here so their uninstall functions get called when switching
// back to the default set.
// TODO product should make the call about temporary add-ons running
// in safe mode. assuming for now that they are.
- let location = aAddon._installLocation || null;
+ let location = aAddon.location || null;
if (!location) {
return false;
}
- if (location.name == KEY_APP_TEMPORARY)
- return true;
-
- return location.isSystem;
+ return location.isTemporary || location.isSystem;
}
/**
* Converts an internal add-on type to the type presented through the API.
*
* @param {string} aType
* The internal add-on type
* @returns {string}
@@ -739,39 +736,73 @@ class XPIState {
}
}
/**
* Manages the state data for add-ons in a given install location.
*
* @param {string} name
* The name of the install location (e.g., "app-profile").
- * @param {string?} path
+ * @param {string | nsIFile | null} path
* The on-disk path of the install location. May be null for some
* locations which do not map to a specific on-disk path.
- * @param {object} [saved = {}]
+ * @param {integer} scope
+ * The scope of add-ons installed in this location.
+ * @param {object} [saved]
* The persisted JSON state data to restore.
*/
class XPIStateLocation extends Map {
- constructor(name, path, saved = {}) {
+ constructor(name, path, scope, saved) {
super();
this.name = name;
- this.path = path || saved.path || null;
+ this.scope = scope;
+ if (path instanceof Ci.nsIFile) {
+ this.dir = path;
+ this.path = path.path;
+ } else {
+ this.path = path;
+ this.dir = this.path && new nsIFile(this.path);
+ }
+ this.staged = {};
+ this.changed = false;
+
+ if (saved) {
+ this.restore(saved);
+ }
+
+ this._installler = undefined;
+ }
+
+ get installer() {
+ if (this._installer === undefined) {
+ this._installer = this.makeInstaller();
+ }
+ return this._installer;
+ }
+
+ makeInstaller() {
+ return null;
+ }
+
+ restore(saved) {
+ if (!this.path && saved.path) {
+ this.path = saved.path;
+ this.dir = new nsIFile(this.path);
+ }
this.staged = saved.staged || {};
this.changed = saved.changed || false;
- this.dir = this.path && new nsIFile(this.path);
for (let [id, data] of Object.entries(saved.addons || {})) {
let xpiState = this._addState(id, data);
// Make a note that this state was restored from saved data. But
// only if this location hasn't moved since the last startup,
// since that causes problems for new system add-on bundles.
- if (!path || path == saved.path) {
+ if (!this.path || this.path == saved.path) {
xpiState.wasRestored = true;
}
}
}
/**
* Returns a JSON-compatible representation of this location's state
* data, to be saved to addonStartup.json.
@@ -822,16 +853,29 @@ class XPIStateLocation extends Map {
let xpiState = this._addState(addon.id, {file: addon._sourceBundle});
xpiState.syncWithDB(addon, true);
XPIProvider.setTelemetry(addon.id, "location", this.name);
}
/**
+ * Remove the XPIState for an add-on and save the new state.
+ *
+ * @param {string} aId
+ * The ID of the add-on.
+ */
+ removeAddon(aId) {
+ if (this.has(aId)) {
+ this.delete(aId);
+ XPIStates.save();
+ }
+ }
+
+ /**
* Adds stub state data for the local file to the DB.
*
* @param {string} addonId
* The ID of the add-on represented by the given file.
* @param {nsIFile} file
* The local file or directory containing the add-on.
* @returns {XPIState}
*/
@@ -888,41 +932,476 @@ class XPIStateLocation extends Map {
* The add-on's data from the xpiState preference.
* @param {object} [bootstrapped]
* The add-on's data from the bootstrappedAddons preference, if
* applicable.
*/
migrateAddon(id, state, bootstrapped) {
this.set(id, XPIState.migrate(this, id, state, bootstrapped));
}
+
+ /**
+ * Returns true if the given addon was installed in this location by a text
+ * file pointing to its real path.
+ *
+ * @param {string} aId
+ * The ID of the addon
+ * @returns {boolean}
+ */
+ isLinkedAddon(aId) {
+ if (!this.dir) {
+ return true;
+ }
+ return this.has(aId) && !this.dir.contains(this.get(aId).file);
+ }
+
+ get isTemporary() {
+ return false;
+ }
+
+ get isSystem() {
+ return false;
+ }
+}
+
+class TemporaryLocation extends XPIStateLocation {
+ /**
+ * @param {string} name
+ * The string identifier for the install location.
+ */
+ constructor(name) {
+ super(name, null, null);
+ this.locked = false;
+ }
+
+ makeInstaller() {
+ // Installs are a no-op. We only register that add-ons exist, and
+ // run them from their current location.
+ return {
+ installAddon() {},
+ uninstallAddon() {},
+ };
+ }
+
+ toJSON() {
+ return {};
+ }
+
+ readAddons() {
+ return new Map();
+ }
+
+ get isTemporary() {
+ return true;
+ }
+}
+
+var TemporaryInstallLocation = new TemporaryLocation(KEY_APP_TEMPORARY);
+
+/**
+ * An object which identifies a directory install location for add-ons. The
+ * location consists of a directory which contains the add-ons installed in the
+ * location.
+ *
+ */
+class DirectoryLocation extends XPIStateLocation {
+ /**
+ * Each add-on installed in the location is either a directory containing the
+ * add-on's files or a text file containing an absolute path to the directory
+ * containing the add-ons files. The directory or text file must have the same
+ * name as the add-on's ID.
+ *
+ * @param {string} name
+ * The string identifier for the install location.
+ * @param {nsIFile} dir
+ * The directory for the install location.
+ * @param {integer} scope
+ * The scope of add-ons installed in this location.
+ * @param {boolean} [locked = true]
+ * If false, the location accepts new add-on installs.
+ */
+ constructor(name, dir, scope, locked = true) {
+ super(name, dir, scope);
+ this.locked = locked;
+ this.initialized = false;
+ }
+
+ makeInstaller() {
+ if (this.locked) {
+ return null;
+ }
+ return new XPIInstall.DirectoryInstaller(this);
+ }
+
+ /**
+ * Reads a single-line file containing the path to a directory, and
+ * returns an nsIFile pointing to that directory, if successful.
+ *
+ * @param {nsIFile} aFile
+ * The file containing the directory path
+ * @returns {nsIFile?}
+ * An nsIFile object representing the linked directory, or null
+ * on error.
+ */
+ _readLinkFile(aFile) {
+ let linkedDirectory;
+ if (aFile.isSymlink()) {
+ linkedDirectory = aFile.clone();
+ try {
+ linkedDirectory.normalize();
+ } catch (e) {
+ logger.warn(`Symbolic link ${aFile.path} points to a path ` +
+ `which does not exist`);
+ return null;
+ }
+ } else {
+ let fis = new FileInputStream(aFile, -1, -1, false);
+ let line = {};
+ fis.QueryInterface(Ci.nsILineInputStream).readLine(line);
+ fis.close();
+
+ if (line.value) {
+ linkedDirectory = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ try {
+ linkedDirectory.initWithPath(line.value);
+ } catch (e) {
+ linkedDirectory.setRelativeDescriptor(aFile.parent, line.value);
+ }
+ }
+ }
+
+ if (linkedDirectory) {
+ if (!linkedDirectory.exists()) {
+ logger.warn(`File pointer ${aFile.path} points to ${linkedDirectory.path} ` +
+ "which does not exist");
+ return null;
+ }
+
+ if (!linkedDirectory.isDirectory()) {
+ logger.warn(`File pointer ${aFile.path} points to ${linkedDirectory.path} ` +
+ "which is not a directory");
+ return null;
+ }
+
+ return linkedDirectory;
+ }
+
+ logger.warn(`File pointer ${aFile.path} does not contain a path`);
+ return null;
+ }
+
+ /**
+ * Finds all the add-ons installed in this location.
+ *
+ * @returns {Map<AddonID, nsIFile>}
+ * A map of add-ons present in this location.
+ */
+ readAddons() {
+ let addons = new Map();
+
+ if (!this.dir) {
+ return addons;
+ }
+ this.initialized = true;
+
+ // Use a snapshot of the directory contents to avoid possible issues with
+ // iterating over a directory while removing files from it (the YAFFS2
+ // embedded filesystem has this issue, see bug 772238).
+ let entries = getDirectoryEntries(this.dir);
+ for (let entry of entries) {
+ let id = entry.leafName;
+ if (id == DIR_STAGE || id == DIR_TRASH)
+ continue;
+
+ let isFile = id.toLowerCase().endsWith(".xpi");
+ if (isFile) {
+ id = id.substring(0, id.length - 4);
+ }
+
+ if (!gIDTest.test(id)) {
+ logger.debug("Ignoring file entry whose name is not a valid add-on ID: " +
+ entry.path);
+ continue;
+ }
+
+ if (!isFile && (entry.isFile() || entry.isSymlink())) {
+ let newEntry = this._readLinkFile(entry);
+ if (!newEntry) {
+ logger.debug(`Deleting stale pointer file ${entry.path}`);
+ try {
+ entry.remove(true);
+ } catch (e) {
+ logger.warn(`Failed to remove stale pointer file ${entry.path}`, e);
+ // Failing to remove the stale pointer file is ignorable
+ }
+ continue;
+ }
+
+ entry = newEntry;
+ }
+
+
+ addons.set(id, entry);
+ }
+ return addons;
+ }
+}
+
+/**
+ * An object which identifies a built-in install location for add-ons, such
+ * as default system add-ons.
+ *
+ * This location should point either to a XPI, or a directory in a local build.
+ */
+class BuiltInLocation extends DirectoryLocation {
+ /**
+ * Read the manifest of allowed add-ons and build a mapping between ID and URI
+ * for each.
+ *
+ * @returns {Map<AddonID, nsIFile>}
+ * A map of add-ons present in this location.
+ */
+ readAddons() {
+ let addons = new Map();
+
+ let manifest;
+ try {
+ let url = Services.io.newURI(BUILT_IN_ADDONS_URI);
+ let data = Cu.readUTF8URI(url);
+ manifest = JSON.parse(data);
+ } catch (e) {
+ logger.warn("List of valid built-in add-ons could not be parsed.", e);
+ return addons;
+ }
+
+ if (!("system" in manifest)) {
+ logger.warn("No list of valid system add-ons found.");
+ return addons;
+ }
+
+ for (let id of manifest.system) {
+ let file = this.dir.clone();
+ file.append(`${id}.xpi`);
+
+ // Only attempt to load unpacked directory if unofficial build.
+ if (!AppConstants.MOZILLA_OFFICIAL && !file.exists()) {
+ file = this.dir.clone();
+ file.append(`${id}`);
+ }
+
+ addons.set(id, file);
+ }
+
+ return addons;
+ }
+
+ get isSystem() {
+ return true;
+ }
+}
+
+/**
+ * An object which identifies a directory install location for system add-ons
+ * updates.
+ */
+class SystemAddonLocation extends DirectoryLocation {
+ /**
+ * The location consists of a directory which contains the add-ons installed.
+ *
+ * @param {string} name
+ * The string identifier for the install location.
+ * @param {nsIFile} dir
+ * The directory for the install location.
+ * @param {integer} scope
+ * The scope of add-ons installed in this location.
+ * @param {boolean} resetSet
+ * True to throw away the current add-on set
+ */
+ constructor(name, dir, scope, resetSet) {
+ let addonSet = SystemAddonLocation._loadAddonSet();
+ let directory = null;
+
+ // The system add-on update directory is stored in a pref.
+ // Therefore, this is looked up before calling the
+ // constructor on the superclass.
+ if (addonSet.directory) {
+ directory = getFile(addonSet.directory, dir);
+ logger.info(`SystemAddonLocation scanning directory ${directory.path}`);
+ } else {
+ logger.info("SystemAddonLocation directory is missing");
+ }
+
+ super(name, directory, scope, false);
+
+ this._addonSet = addonSet;
+ this._baseDir = dir;
+
+ if (resetSet) {
+ this.installer.resetAddonSet();
+ }
+ }
+
+ makeInstaller() {
+ if (this.locked) {
+ return null;
+ }
+ return new XPIInstall.SystemAddonInstaller(this);
+ }
+
+ /**
+ * Reads the current set of system add-ons
+ *
+ * @returns {Object}
+ */
+ static _loadAddonSet() {
+ try {
+ let setStr = Services.prefs.getStringPref(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: {} };
+ }
+
+ readAddons() {
+ // Updated system add-ons are ignored in safe mode
+ if (Services.appinfo.inSafeMode) {
+ return new Map();
+ }
+
+ let addons = super.readAddons();
+
+ // 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.
+ *
+ * @returns {boolean}
+ */
+ isActive() {
+ return this.dir != null;
+ }
+
+ get isSystem() {
+ return true;
+ }
+}
+
+/**
+ * An object that identifies a registry install location for add-ons. The location
+ * consists of a registry key which contains string values mapping ID to the
+ * path where an add-on is installed
+ *
+ */
+class WinRegLocation extends XPIStateLocation {
+ /**
+ * @param {string} name
+ * The string identifier for the install location.
+ * @param {integer} rootKey
+ * The root key (one of the ROOT_KEY_ values from nsIWindowsRegKey).
+ * @param {integer} scope
+ * The scope of add-ons installed in this location.
+ */
+ constructor(name, rootKey, scope) {
+ super(name, undefined, scope);
+
+ this.locked = true;
+ this._rootKey = rootKey;
+ }
+
+ /**
+ * Retrieves the path of this Application's data key in the registry.
+ */
+ get _appKeyPath() {
+ let appVendor = Services.appinfo.vendor;
+ let appName = Services.appinfo.name;
+
+ // XXX Thunderbird doesn't specify a vendor string
+ if (appVendor == "" && AppConstants.MOZ_APP_NAME == "thunderbird")
+ appVendor = "Mozilla";
+
+ return `SOFTWARE\\${appVendor}\\${appName}`;
+ }
+
+ /**
+ * Read the registry and build a mapping between ID and path for each
+ * installed add-on.
+ *
+ * @returns {Map<AddonID, nsIFile>}
+ * A map of add-ons in this location.
+ */
+ readAddons() {
+ let addons = new Map();
+
+ let path = `${this._appKeyPath}\\Extensions`;
+ let key = Cc["@mozilla.org/windows-registry-key;1"].createInstance(Ci.nsIWindowsRegKey);
+
+ // Reading the registry may throw an exception, and that's ok. In error
+ // cases, we just leave ourselves in the empty state.
+ try {
+ key.open(this._rootKey, path, Ci.nsIWindowsRegKey.ACCESS_READ);
+ } catch (e) {
+ return addons;
+ }
+
+ try {
+ let count = key.valueCount;
+ for (let i = 0; i < count; ++i) {
+ let id = key.getValueName(i);
+ let file = new nsIFile(key.readStringValue(id));
+ if (!file.exists()) {
+ logger.warn(`Ignoring missing add-on in ${file.path}`);
+ continue;
+ }
+
+ addons.set(id, file);
+ }
+ } finally {
+ key.close();
+ }
+
+ return addons;
+ }
}
/**
* Keeps track of the state of XPI add-ons on the file system.
*/
var XPIStates = {
- // Map(location name -> Map(add-on ID -> XPIState))
- db: null,
+ // Map(location-name -> XPIStateLocation)
+ db: new Map(),
_jsonFile: null,
/**
* @property {Map<string, XPIState>} sideLoadedAddons
* A map of new add-ons detected during install location
* directory scans. Keys are add-on IDs, values are XPIState
* objects corresponding to those add-ons.
*/
sideLoadedAddons: new Map(),
get size() {
let count = 0;
- if (this.db) {
- for (let location of this.db.values()) {
- count += location.size;
- }
+ for (let location of this.locations()) {
+ count += location.size;
}
return count;
},
/**
* Migrates state data from the xpiState and bootstrappedAddons
* preferences and adds it to the DB. Returns a JSON-compatible
* representation of the current state of the DB.
@@ -1004,118 +1483,112 @@ var XPIStates = {
*
* @param {boolean} [ignoreSideloads = true]
* If true, ignore changes in scopes where we don't accept
* side-loads.
*
* @returns {boolean}
* True if anything has changed.
*/
- getInstallState(ignoreSideloads = true) {
- if (!this.db) {
- this.db = new Map();
- }
-
+ scanForChanges(ignoreSideloads = true) {
let oldState = this.initialStateData || this.loadExtensionState();
this.initialStateData = oldState;
let changed = false;
let oldLocations = new Set(Object.keys(oldState));
- for (let location of XPIProvider.installLocations) {
- oldLocations.delete(location.name);
-
- // The results of scanning this location.
- let loc = this.getLocation(location.name, location.path || null,
- oldState[location.name] || undefined);
+ for (let loc of XPIStates.locations()) {
+ oldLocations.delete(loc.name);
+
+ if (oldState[loc.name]) {
+ loc.restore(oldState[loc.name]);
+ }
changed = changed || loc.changed;
// Don't bother checking scopes where we don't accept side-loads.
- if (ignoreSideloads && !(location.scope & gStartupScanScopes)) {
+ if (ignoreSideloads && !(loc.scope & gStartupScanScopes)) {
continue;
}
- if (location.name == KEY_APP_TEMPORARY) {
+ if (loc.isTemporary) {
continue;
}
let knownIds = new Set(loc.keys());
- for (let [id, file] of location.getAddonLocations(true)) {
+ for (let [id, file] of loc.readAddons()) {
knownIds.delete(id);
let xpiState = loc.get(id);
if (!xpiState) {
- logger.debug("New add-on ${id} in ${location}", {id, location: location.name});
+ logger.debug("New add-on ${id} in ${loc}", {id, location: loc.name});
changed = true;
xpiState = loc.addFile(id, file);
- if (!location.isSystem) {
+ if (!loc.isSystem) {
this.sideLoadedAddons.set(id, xpiState);
}
} else {
let addonChanged = (xpiState.getModTime(file, id) ||
file.path != xpiState.path);
xpiState.file = file.clone();
if (addonChanged) {
changed = true;
- logger.debug("Changed add-on ${id} in ${location}", {id, location: location.name});
+ logger.debug("Changed add-on ${id} in ${loc}", {id, location: loc.name});
} else {
- logger.debug("Existing add-on ${id} in ${location}", {id, location: location.name});
+ logger.debug("Existing add-on ${id} in ${loc}", {id, location: loc.name});
}
}
- XPIProvider.setTelemetry(id, "location", location.name);
+ XPIProvider.setTelemetry(id, "location", loc.name);
}
// Anything left behind in oldState was removed from the file system.
for (let id of knownIds) {
loc.delete(id);
changed = true;
}
}
// If there's anything left in oldState, an install location that held add-ons
// was removed from the browser configuration.
changed = changed || oldLocations.size > 0;
- logger.debug("getInstallState changed: ${rv}, state: ${state}",
+ logger.debug("scanForChanges changed: ${rv}, state: ${state}",
{rv: changed, state: this.db});
return changed;
},
+ locations() {
+ return this.db.values();
+ },
+
+ /**
+ * @param {string} name
+ * The location name.
+ * @param {XPIStateLocation} location
+ * The location object.
+ */
+ addLocation(name, location) {
+ if (this.db.has(name)) {
+ throw new Error(`Trying to add duplicate location: ${name}`);
+ }
+ this.db.set(name, location);
+ },
+
/**
* Get the Map of XPI states for a particular location.
*
* @param {string} name
* The name of the install location.
- * @param {string?} [path]
- * The expected path of the location, if known.
- * @param {Object?} [saved]
- * The saved data for the location, as read from the
- * addonStartup.json file.
*
* @returns {XPIStateLocation?}
* (id -> XPIState) or null if there are no add-ons in the location.
*/
- getLocation(name, path, saved) {
- let location = this.db.get(name);
-
- if (path && location && location.path != path) {
- location = null;
- saved = null;
- }
-
- if (!location || (path && location.path != path)) {
- let loc = XPIProvider.installLocationsByName[name];
- if (loc) {
- location = new XPIStateLocation(name, path || loc.path || null, saved);
- this.db.set(name, location);
- }
- }
- return location;
+ getLocation(name) {
+ return this.db.get(name);
},
/**
* Get the XPI state for a specific add-on in a location.
* If the state is not in our cache, return null.
*
* @param {string} aLocation
* The name of the location where the add-on is installed.
@@ -1139,49 +1612,48 @@ var XPIStates = {
* An optional filter to apply to install locations. If provided,
* addons in locations that do not match the filter are not considered.
*
* @returns {XPIState?}
*/
findAddon(aId, aFilter = location => true) {
// Fortunately the Map iterator returns in order of insertion, which is
// also our highest -> lowest priority order.
- for (let location of this.db.values()) {
+ for (let location of this.locations()) {
if (!aFilter(location)) {
continue;
}
if (location.has(aId)) {
return location.get(aId);
}
}
return undefined;
},
/**
* Iterates over the list of all enabled add-ons in any location.
*/
* enabledAddons() {
- for (let location of this.db.values()) {
+ for (let location of this.locations()) {
for (let entry of location.values()) {
if (entry.enabled) {
yield entry;
}
}
}
},
/**
* Add a new XPIState for an add-on and synchronize it with the DBAddonInternal.
*
* @param {DBAddonInternal} aAddon
* The add-on to add.
*/
addAddon(aAddon) {
- let location = this.getLocation(aAddon._installLocation.name);
- location.addAddon(aAddon);
+ aAddon.location.addAddon(aAddon);
},
/**
* Save the current state of installed add-ons.
*/
save() {
if (!this._jsonFile) {
this._jsonFile = new JSONFile({
@@ -1193,40 +1665,37 @@ var XPIStates = {
}
this._jsonFile.saveSoon();
},
toJSON() {
let data = {};
for (let [key, loc] of this.db.entries()) {
- if (key != TemporaryInstallLocation.name && (loc.size || loc.hasStaged)) {
+ if (!loc.isTemporary && (loc.size || loc.hasStaged)) {
data[key] = loc;
}
}
return data;
},
/**
* Remove the XPIState for an add-on and save the new state.
*
* @param {string} aLocation
* The name of the add-on location.
* @param {string} aId
* The ID of the add-on.
*
*/
removeAddon(aLocation, aId) {
- logger.debug("Removing XPIState for " + aLocation + ":" + aId);
+ logger.debug(`Removing XPIState for ${aLocation}: ${aId}`);
let location = this.db.get(aLocation);
if (location) {
- location.delete(aId);
- if (location.size == 0) {
- this.db.delete(aLocation);
- }
+ location.removeAddon(aId);
this.save();
}
},
/**
* Disable the XPIState for an add-on.
*
* @param {string} aId
@@ -1338,25 +1807,24 @@ class BootstrapScope {
this._pendingDisable = true;
for (let addon of XPIProvider.getDependentAddons(this.addon)) {
if (addon.active)
await XPIDatabase.updateAddonDisabledState(addon);
}
}
}
- let installLocation = addon._installLocation || null;
let params = {
id: addon.id,
version: addon.version,
installPath: this.file.clone(),
resourceURI: getURIForResourceInFile(this.file, ""),
signedState: addon.signedState,
- temporarilyInstalled: installLocation == TemporaryInstallLocation,
- builtIn: installLocation instanceof BuiltInInstallLocation,
+ temporarilyInstalled: addon.location.isTemporary,
+ builtIn: addon.location instanceof BuiltInLocation,
};
if (aMethod == "startup" && addon.startupData) {
params.startupData = addon.startupData;
}
Object.assign(params, aExtraParams);
@@ -1664,20 +2132,16 @@ class BootstrapScope {
var XPIProvider = {
get name() {
return "XPIProvider";
},
BOOTSTRAP_REASONS: Object.freeze(BOOTSTRAP_REASONS),
- // An array of known install locations
- installLocations: null,
- // A dictionary of known install locations by name
- installLocationsByName: null,
// A Map of active addons to their bootstrapScope by ID
activeAddons: new Map(),
// True if the platform could have activated extensions
extensionsActive: false,
// New distribution addons awaiting permissions approval
newDistroAddons: null,
// Keep track of startup phases for telemetry
runPhase: XPI_STARTING,
@@ -1789,99 +2253,93 @@ var XPIProvider = {
c.cancel();
} catch (e) {
logger.warn("Cancel failed", e);
}
}
},
setupInstallLocations(aAppChanged) {
- function DirectoryLocation(aName, aScope, aKey, aPaths, aLocked) {
+ function DirectoryLoc(aName, aScope, aKey, aPaths, aLocked) {
try {
var dir = FileUtils.getDir(aKey, aPaths);
} catch (e) {
return null;
}
- if (aLocked) {
- return new DirectoryInstallLocation(aName, dir, aScope);
- }
- return new MutableDirectoryInstallLocation(aName, dir, aScope);
+ return new DirectoryLocation(aName, dir, aScope, aLocked);
}
- function BuiltInLocation(name, scope, key, paths) {
+ function BuiltInLoc(name, scope, key, paths) {
try {
var dir = FileUtils.getDir(key, paths);
} catch (e) {
return null;
}
- return new BuiltInInstallLocation(name, dir, scope);
+ return new BuiltInLocation(name, dir, scope);
}
- function SystemLocation(aName, aScope, aKey, aPaths) {
+ function SystemLoc(aName, aScope, aKey, aPaths) {
try {
var dir = FileUtils.getDir(aKey, aPaths);
} catch (e) {
return null;
}
- return new SystemAddonInstallLocation(aName, dir, aScope, aAppChanged !== false);
+ return new SystemAddonLocation(aName, dir, aScope, aAppChanged !== false);
}
- function RegistryLocation(aName, aScope, aKey) {
+ function RegistryLoc(aName, aScope, aKey) {
if ("nsIWindowsRegKey" in Ci) {
- return new WinRegInstallLocation(aName, Ci.nsIWindowsRegKey[aKey], aScope);
+ return new WinRegLocation(aName, Ci.nsIWindowsRegKey[aKey], aScope);
}
}
let enabledScopes = Services.prefs.getIntPref(PREF_EM_ENABLED_SCOPES,
AddonManager.SCOPE_ALL);
// The profile location is always enabled
enabledScopes |= AddonManager.SCOPE_PROFILE;
// These must be in order of priority, highest to lowest,
// for processFileChanges etc. to work
let locations = [
[() => TemporaryInstallLocation, TemporaryInstallLocation.name, null],
- [DirectoryLocation, KEY_APP_PROFILE, AddonManager.SCOPE_PROFILE,
+ [DirectoryLoc, KEY_APP_PROFILE, AddonManager.SCOPE_PROFILE,
KEY_PROFILEDIR, [DIR_EXTENSIONS], false],
- [SystemLocation, KEY_APP_SYSTEM_ADDONS, AddonManager.SCOPE_PROFILE,
+ [SystemLoc, KEY_APP_SYSTEM_ADDONS, AddonManager.SCOPE_PROFILE,
KEY_PROFILEDIR, [DIR_SYSTEM_ADDONS]],
- [BuiltInLocation, KEY_APP_SYSTEM_DEFAULTS, AddonManager.SCOPE_PROFILE,
+ [BuiltInLoc, KEY_APP_SYSTEM_DEFAULTS, AddonManager.SCOPE_PROFILE,
KEY_APP_FEATURES, []],
- [DirectoryLocation, KEY_APP_SYSTEM_USER, AddonManager.SCOPE_USER,
+ [DirectoryLoc, KEY_APP_SYSTEM_USER, AddonManager.SCOPE_USER,
"XREUSysExt", [Services.appinfo.ID], true],
- [RegistryLocation, "winreg-app-user", AddonManager.SCOPE_USER,
+ [RegistryLoc, "winreg-app-user", AddonManager.SCOPE_USER,
"ROOT_KEY_CURRENT_USER"],
- [DirectoryLocation, KEY_APP_GLOBAL, AddonManager.SCOPE_APPLICATION,
+ [DirectoryLoc, KEY_APP_GLOBAL, AddonManager.SCOPE_APPLICATION,
KEY_ADDON_APP_DIR, [DIR_EXTENSIONS], true],
- [DirectoryLocation, KEY_APP_SYSTEM_SHARE, AddonManager.SCOPE_SYSTEM,
+ [DirectoryLoc, KEY_APP_SYSTEM_SHARE, AddonManager.SCOPE_SYSTEM,
"XRESysSExtPD", [Services.appinfo.ID], true],
- [DirectoryLocation, KEY_APP_SYSTEM_LOCAL, AddonManager.SCOPE_SYSTEM,
+ [DirectoryLoc, KEY_APP_SYSTEM_LOCAL, AddonManager.SCOPE_SYSTEM,
"XRESysLExtPD", [Services.appinfo.ID], true],
- [RegistryLocation, "winreg-app-global", AddonManager.SCOPE_SYSTEM,
+ [RegistryLoc, "winreg-app-global", AddonManager.SCOPE_SYSTEM,
"ROOT_KEY_LOCAL_MACHINE"],
];
- this.installLocations = [];
- this.installLocationsByName = {};
for (let [constructor, name, scope, ...args] of locations) {
if (!scope || enabledScopes & scope) {
try {
let loc = constructor(name, scope, ...args);
if (loc) {
- this.installLocations.push(loc);
- this.installLocationsByName[name] = loc;
+ XPIStates.addLocation(name, loc);
}
} catch (e) {
logger.warn(`Failed to add ${constructor.name} install location ${name}`, e);
}
}
}
},
@@ -1985,20 +2443,19 @@ var XPIProvider = {
continue;
}
// If the add-on was pending disable then shut it down and remove it
// from the persisted data.
let reason = BOOTSTRAP_REASONS.APP_SHUTDOWN;
if (addon._disable) {
reason = BOOTSTRAP_REASONS.ADDON_DISABLE;
- } else if (addon.location.name == KEY_APP_TEMPORARY) {
+ } else if (addon.location.isTemporary) {
reason = BOOTSTRAP_REASONS.ADDON_UNINSTALL;
- let existing = XPIStates.findAddon(addon.id, loc =>
- loc.name != TemporaryInstallLocation.name);
+ let existing = XPIStates.findAddon(addon.id, loc => !loc.isTemporary);
if (existing) {
reason = XPIInstall.newVersionReason(addon.version, existing.version);
}
}
let promise = BootstrapScope.get(addon).shutdown(reason);
AsyncShutdown.profileChangeTeardown.addBlocker(
`Extension shutdown: ${addon.id}`, promise);
@@ -2089,53 +2546,48 @@ var XPIProvider = {
// Ugh, if we reach this point without loading the xpi database,
// we need to load it know, otherwise the telemetry shutdown blocker
// will never resolve.
if (!XPIDatabase.initialized) {
await XPIDatabase.asyncLoadDB();
}
- this.installLocations = null;
- this.installLocationsByName = null;
-
// This is needed to allow xpcshell tests to simulate a restart
this.extensionsActive = false;
await XPIDatabase.shutdown();
},
cleanupTemporaryAddons() {
let promises = [];
- let tempLocation = XPIStates.getLocation(TemporaryInstallLocation.name);
- if (tempLocation) {
- for (let [id, addon] of tempLocation.entries()) {
- tempLocation.delete(id);
-
- let bootstrap = BootstrapScope.get(addon);
- let existing = XPIStates.findAddon(id, loc => loc != tempLocation);
-
- let cleanup = () => {
- TemporaryInstallLocation.uninstallAddon(id);
- XPIStates.removeAddon(TemporaryInstallLocation.name, id);
- };
-
- let promise;
- if (existing) {
- promise = bootstrap.update(existing, false, () => {
- cleanup();
- XPIDatabase.makeAddonLocationVisible(id, existing.location.name);
- });
- } else {
- promise = bootstrap.uninstall().then(cleanup);
- }
- AsyncShutdown.profileChangeTeardown.addBlocker(
- `Temporary extension shutdown: ${addon.id}`, promise);
- promises.push(promise);
+ let tempLocation = TemporaryInstallLocation;
+ for (let [id, addon] of tempLocation.entries()) {
+ tempLocation.delete(id);
+
+ let bootstrap = BootstrapScope.get(addon);
+ let existing = XPIStates.findAddon(id, loc => loc != tempLocation);
+
+ let cleanup = () => {
+ tempLocation.installer.uninstallAddon(id);
+ tempLocation.removeAddon(id);
+ };
+
+ let promise;
+ if (existing) {
+ promise = bootstrap.update(existing, false, () => {
+ cleanup();
+ XPIDatabase.makeAddonLocationVisible(id, existing.location);
+ });
+ } else {
+ promise = bootstrap.uninstall().then(cleanup);
}
+ AsyncShutdown.profileChangeTeardown.addBlocker(
+ `Temporary extension shutdown: ${addon.id}`, promise);
+ promises.push(promise);
}
return Promise.all(promises);
},
/**
* Adds a list of currently active add-ons to the next crash report.
*/
addAddonsToCrashReporter() {
@@ -2166,53 +2618,51 @@ var XPIProvider = {
* @param {Object} aManifests
* A dictionary to add detected install manifests to for the purpose
* of passing through updated compatibility information
* @returns {boolean}
* True if an add-on was installed or uninstalled
*/
processPendingFileChanges(aManifests) {
let changed = false;
- for (let location of this.installLocations) {
- aManifests[location.name] = {};
+ for (let loc of XPIStates.locations()) {
+ aManifests[loc.name] = {};
// We can't install or uninstall anything in locked locations
- if (location.locked) {
+ if (loc.locked) {
continue;
}
- let state = XPIStates.getLocation(location.name);
-
let cleanNames = [];
let promises = [];
- for (let [id, metadata] of state.getStagedAddons()) {
- state.unstageAddon(id);
-
- aManifests[location.name][id] = null;
+ for (let [id, metadata] of loc.getStagedAddons()) {
+ loc.unstageAddon(id);
+
+ aManifests[loc.name][id] = null;
promises.push(
- XPIInstall.installStagedAddon(id, metadata, location).then(
+ XPIInstall.installStagedAddon(id, metadata, loc).then(
addon => {
- aManifests[location.name][id] = addon;
+ aManifests[loc.name][id] = addon;
},
error => {
- delete aManifests[location.name][id];
+ delete aManifests[loc.name][id];
cleanNames.push(`${id}.xpi`);
- logger.error(`Failed to install staged add-on ${id} in ${location.name}`,
+ logger.error(`Failed to install staged add-on ${id} in ${loc.name}`,
error);
}));
}
if (promises.length) {
changed = true;
awaitPromise(Promise.all(promises));
}
try {
if (cleanNames.length) {
- location.cleanStagingDir(cleanNames);
+ loc.installer.cleanStagingDir(cleanNames);
}
} catch (e) {
// Non-critical, just saves some perf on startup if we clean this up.
logger.debug("Error cleaning staging dir", e);
}
}
return changed;
},
@@ -2237,23 +2687,22 @@ var XPIProvider = {
distroDir = FileUtils.getDir(KEY_APP_DISTRIBUTION, [DIR_EXTENSIONS]);
if (!distroDir.isDirectory())
return false;
} catch (e) {
return false;
}
let changed = false;
- let profileLocation = this.installLocationsByName[KEY_APP_PROFILE];
+ let profileLocation = XPIStates.getLocation(KEY_APP_PROFILE);
let entries = distroDir.directoryEntries
.QueryInterface(Ci.nsIDirectoryEnumerator);
let entry;
while ((entry = entries.nextFile)) {
-
let id = entry.leafName;
if (id.endsWith(".xpi")) {
id = id.slice(0, -4);
} else {
logger.debug("Ignoring distribution add-on that isn't an XPI: " + entry.path);
continue;
}
@@ -2277,17 +2726,17 @@ var XPIProvider = {
// and we'll have overwritten that so instead cache our install manifest
// which will later be put into the database in processFileChanges
if (!(KEY_APP_PROFILE in aManifests))
aManifests[KEY_APP_PROFILE] = {};
aManifests[KEY_APP_PROFILE][id] = addon;
changed = true;
}
} catch (e) {
- logger.error("Failed to install distribution add-on " + entry.path, e);
+ logger.error(`Failed to install distribution add-on ${entry.path}`, e);
}
}
entries.close();
return changed;
},
@@ -2335,17 +2784,17 @@ var XPIProvider = {
// Keep track of whether and why we need to open and update the database at
// startup time.
let updateReasons = [];
if (aAppChanged) {
updateReasons.push("appChanged");
}
- let installChanged = XPIStates.getInstallState(aAppChanged === false);
+ let installChanged = XPIStates.scanForChanges(aAppChanged === false);
if (installChanged) {
updateReasons.push("directoryState");
}
// First install any new add-ons into the locations, if there are any
// changes then we must update the database with the information in the
// install locations
let manifests = {};
@@ -2435,17 +2884,17 @@ var XPIProvider = {
/**
* Gets an array of add-ons which were placed in a known install location
* prior to startup of the current session, were detected by a directory scan
* of those locations, and are currently disabled.
*
* @returns {Promise<Array<Addon>>}
*/
async getNewSideloads() {
- if (XPIStates.getInstallState(false)) {
+ if (XPIStates.scanForChanges(false)) {
// We detected changes. Update the database to account for them.
await XPIDatabase.asyncLoadDB(false);
XPIDatabaseReconcile.processFileChanges({}, false);
this._updateActiveAddons();
}
let addons = await Promise.all(
Array.from(XPIStates.sideLoadedAddons.keys(),
@@ -2595,17 +3044,17 @@ var XPIProvider = {
throw new Error("XPIStates not yet initialized");
}
let result = [];
for (let addon of XPIStates.enabledAddons()) {
if (aTypes && !aTypes.includes(addon.type)) {
continue;
}
- let location = this.installLocationsByName[addon.location.name];
+ let {location} = addon;
let scope, isSystem;
if (location) {
({scope, isSystem} = location);
}
result.push({
id: addon.id,
version: addon.version,
type: addon.type,
@@ -2716,538 +3165,34 @@ var XPIProvider = {
case PREF_ALLOW_LEGACY:
this.updateAddonAppDisabledStates();
break;
}
}
},
};
-for (let meth of ["cancelUninstallAddon", "getInstallForFile",
- "getInstallForURL", "getInstallsByTypes",
+for (let meth of ["getInstallForFile", "getInstallForURL", "getInstallsByTypes",
"installTemporaryAddon", "isInstallAllowed",
- "isInstallEnabled", "uninstallAddon",
- "updateSystemAddons"]) {
+ "isInstallEnabled", "updateSystemAddons"]) {
XPIProvider[meth] = function() {
return XPIInstall[meth](...arguments);
};
}
-function forwardInstallMethods(cls, methods) {
- let {prototype} = cls;
- for (let meth of methods) {
- prototype[meth] = function() {
- return XPIInstall[cls.name].prototype[meth].apply(this, arguments);
- };
- }
-}
-
-/**
- * An object which identifies a directory install location for add-ons. The
- * location consists of a directory which contains the add-ons installed in the
- * location.
- *
- */
-class DirectoryInstallLocation {
- /**
- * Each add-on installed in the location is either a directory containing the
- * add-on's files or a text file containing an absolute path to the directory
- * containing the add-ons files. The directory or text file must have the same
- * name as the add-on's ID.
- *
- * @param {string} aName
- * The string identifier for the install location
- * @param {nsIFile} aDirectory
- * The nsIFile directory for the install location
- * @param {integer} aScope
- * The scope of add-ons installed in this location
- */
- constructor(aName, aDirectory, aScope) {
- this._name = aName;
- this.locked = true;
- this._directory = aDirectory;
- this._scope = aScope;
- this._IDToFileMap = {};
- this._linkedAddons = [];
-
- this.isSystem = (aName == KEY_APP_SYSTEM_ADDONS ||
- aName == KEY_APP_SYSTEM_DEFAULTS);
-
- if (!aDirectory || !aDirectory.exists())
- return;
- if (!aDirectory.isDirectory())
- throw new Error("Location must be a directory.");
-
- this.initialized = false;
- }
-
- get path() {
- return this._directory && this._directory.path;
- }
-
- /**
- * Reads a directory linked to in a file.
- *
- * @param {nsIFile} aFile
- * The file containing the directory path
- * @returns {nsIFile?}
- * An nsIFile object representing the linked directory, or null
- * on error.
- */
- _readDirectoryFromFile(aFile) {
- let linkedDirectory;
- if (aFile.isSymlink()) {
- linkedDirectory = aFile.clone();
- try {
- linkedDirectory.normalize();
- } catch (e) {
- logger.warn("Symbolic link " + aFile.path + " points to a path" +
- " which does not exist");
- return null;
- }
- } else {
- let fis = new FileInputStream(aFile, -1, -1, false);
- let line = { value: "" };
- fis.QueryInterface(Ci.nsILineInputStream).readLine(line);
- fis.close();
- if (line.value) {
- linkedDirectory = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
- try {
- linkedDirectory.initWithPath(line.value);
- } catch (e) {
- linkedDirectory.setRelativeDescriptor(aFile.parent, line.value);
- }
- }
- }
-
- if (linkedDirectory) {
- if (!linkedDirectory.exists()) {
- logger.warn("File pointer " + aFile.path + " points to " + linkedDirectory.path +
- " which does not exist");
- return null;
- }
-
- if (!linkedDirectory.isDirectory()) {
- logger.warn("File pointer " + aFile.path + " points to " + linkedDirectory.path +
- " which is not a directory");
- return null;
- }
-
- return linkedDirectory;
- }
-
- logger.warn("File pointer " + aFile.path + " does not contain a path");
- return null;
- }
-
- /**
- * Finds all the add-ons installed in this location.
- *
- * @param {boolean} [rescan = false]
- * True if the directory should be re-scanned, even if it has
- * already been initialized.
- */
- _readAddons(rescan = false) {
- if ((this.initialized && !rescan) || !this._directory) {
- return;
- }
- this.initialized = true;
-
- // Use a snapshot of the directory contents to avoid possible issues with
- // iterating over a directory while removing files from it (the YAFFS2
- // embedded filesystem has this issue, see bug 772238).
- let entries = getDirectoryEntries(this._directory);
- for (let entry of entries) {
- let id = entry.leafName;
-
- if (id == DIR_STAGE || id == DIR_TRASH)
- continue;
-
- let isFile = id.toLowerCase().endsWith(".xpi");
- if (isFile) {
- id = id.substring(0, id.length - 4);
- }
-
- if (!gIDTest.test(id)) {
- logger.debug("Ignoring file entry whose name is not a valid add-on ID: " +
- entry.path);
- continue;
- }
-
- if (!isFile && (entry.isFile() || entry.isSymlink())) {
- let newEntry = this._readDirectoryFromFile(entry);
- if (!newEntry) {
- logger.debug("Deleting stale pointer file " + entry.path);
- try {
- entry.remove(true);
- } catch (e) {
- logger.warn("Failed to remove stale pointer file " + entry.path, e);
- // Failing to remove the stale pointer file is ignorable
- }
- continue;
- }
-
- entry = newEntry;
- this._linkedAddons.push(id);
- }
-
- this._IDToFileMap[id] = entry;
- }
- }
-
- /**
- * Gets the name of this install location.
- */
- get name() {
- return this._name;
- }
-
- /**
- * Gets the scope of this install location.
- */
- get scope() {
- return this._scope;
- }
-
- /**
- * Gets an map of files for add-ons installed in this location.
- *
- * @param {boolean} [rescan = false]
- * True if the directory should be re-scanned, even if it has
- * already been initialized.
- *
- * @returns {Map<string, nsIFile>}
- * A map of all add-ons in the location, with each add-on's ID
- * as the key and an nsIFile for its location as the value.
- */
- getAddonLocations(rescan = false) {
- this._readAddons(rescan);
-
- let locations = new Map();
- for (let id in this._IDToFileMap) {
- locations.set(id, this._IDToFileMap[id].clone());
- }
- return locations;
- }
-
- /**
- * Gets the directory that the add-on with the given ID is installed in.
- *
- * @param {string} aId
- * The ID of the add-on
- * @returns {nsIFile}
- * @throws if the ID does not match any of the add-ons installed
- */
- getLocationForID(aId) {
- if (!(aId in this._IDToFileMap))
- this._readAddons();
-
- if (aId in this._IDToFileMap)
- return this._IDToFileMap[aId].clone();
- throw new Error("Unknown add-on ID " + aId);
- }
-
- /**
- * Returns true if the given addon was installed in this location by a text
- * file pointing to its real path.
- *
- * @param {string} aId
- * The ID of the addon
- * @returns {boolean}
- */
- isLinkedAddon(aId) {
- return this._linkedAddons.includes(aId);
- }
-}
-
-/**
- * An extension of DirectoryInstallLocation which adds methods to installing
- * and removing add-ons from the directory at runtime.
- */
-class MutableDirectoryInstallLocation extends DirectoryInstallLocation {
- /**
- * @param {string} aName
- * The string identifier for the install location
- * @param {nsIFile} aDirectory
- * The nsIFile directory for the install location
- * @param {integer} aScope
- * The scope of add-ons installed in this location
- */
- constructor(aName, aDirectory, aScope) {
- super(aName, aDirectory, aScope);
-
- this.locked = false;
- this._stagingDirLock = 0;
- }
-}
-forwardInstallMethods(MutableDirectoryInstallLocation,
- ["cleanStagingDir", "getStagingDir", "getTrashDir",
- "installAddon", "releaseStagingDir", "requestStagingDir",
- "uninstallAddon"]);
-
-/**
- * An object which identifies a built-in install location for add-ons, such
- * as default system add-ons.
- *
- * This location should point either to a XPI, or a directory in a local build.
- */
-class BuiltInInstallLocation extends DirectoryInstallLocation {
- /**
- * Read the manifest of allowed add-ons and build a mapping between ID and URI
- * for each.
- */
- _readAddons() {
- let manifest;
- try {
- let url = Services.io.newURI(BUILT_IN_ADDONS_URI);
- let data = Cu.readUTF8URI(url);
- manifest = JSON.parse(data);
- } catch (e) {
- logger.warn("List of valid built-in add-ons could not be parsed.", e);
- return;
- }
-
- if (!("system" in manifest)) {
- logger.warn("No list of valid system add-ons found.");
- return;
- }
-
- for (let id of manifest.system) {
- let file = new FileUtils.File(this._directory.path);
- file.append(`${id}.xpi`);
-
- // Only attempt to load unpacked directory if unofficial build.
- if (!AppConstants.MOZILLA_OFFICIAL && !file.exists()) {
- file = new FileUtils.File(this._directory.path);
- file.append(`${id}`);
- }
-
- this._IDToFileMap[id] = file;
- }
- }
-}
-
-/**
- * An object which identifies a directory install location for system add-ons
- * updates.
- */
-class SystemAddonInstallLocation extends MutableDirectoryInstallLocation {
- /**
- * The location consists of a directory which contains the add-ons installed.
- *
- * @param {string} aName
- * The string identifier for the install location
- * @param {nsIFile} aDirectory
- * The nsIFile directory for the install location
- * @param {integer} aScope
- * The scope of add-ons installed in this location
- * @param {boolean} aResetSet
- * True to throw away the current add-on set
- */
- constructor(aName, aDirectory, aScope, aResetSet) {
- let addonSet = SystemAddonInstallLocation._loadAddonSet();
- let directory = null;
-
- // The system add-on update directory is stored in a pref.
- // Therefore, this is looked up before calling the
- // constructor on the superclass.
- if (addonSet.directory) {
- directory = getFile(addonSet.directory, aDirectory);
- logger.info("SystemAddonInstallLocation scanning directory " + directory.path);
- } else {
- logger.info("SystemAddonInstallLocation directory is missing");
- }
-
- super(aName, directory, aScope);
-
- this._addonSet = addonSet;
- this._baseDir = aDirectory;
- this._nextDir = null;
- this._directory = directory;
-
- this._stagingDirLock = 0;
-
- if (aResetSet) {
- this.resetAddonSet();
- }
-
- this.locked = false;
- }
-
- /**
- * Reads the current set of system add-ons
- *
- * @returns {Object}
- */
- static _loadAddonSet() {
- try {
- let setStr = Services.prefs.getStringPref(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: {} };
- }
-
- getAddonLocations() {
- // Updated system add-ons are ignored in safe mode
- if (Services.appinfo.inSafeMode) {
- return new Map();
- }
-
- let addons = super.getAddonLocations();
-
- // 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.
- *
- * @returns {boolean}
- */
- isActive() {
- return this._directory != null;
- }
-}
-
-forwardInstallMethods(SystemAddonInstallLocation,
- ["cleanDirectories", "cleanStagingDir", "getStagingDir",
- "getTrashDir", "installAddon", "installAddon",
- "installAddonSet", "isValid", "isValidAddon",
- "releaseStagingDir", "requestStagingDir",
- "resetAddonSet", "resumeAddonSet", "uninstallAddon",
- "uninstallAddon"]);
-
-/** An object which identifies an install location for temporary add-ons.
- */
-const TemporaryInstallLocation = { locked: false, name: KEY_APP_TEMPORARY,
- scope: AddonManager.SCOPE_TEMPORARY,
- getAddonLocations: () => [], isLinkedAddon: () => false, installAddon:
- () => {}, uninstallAddon: (aAddon) => {}, getStagingDir: () => {},
-};
-
-/**
- * An object that identifies a registry install location for add-ons. The location
- * consists of a registry key which contains string values mapping ID to the
- * path where an add-on is installed
- *
- */
-class WinRegInstallLocation extends DirectoryInstallLocation {
- /**
- * @param {string} aName
- * The string identifier of this Install Location.
- * @param {integer} aRootKey
- * The root key (one of the ROOT_KEY_ values from nsIWindowsRegKey).
- * @param {integer} aScope
- * The scope of add-ons installed in this location
- */
- constructor(aName, aRootKey, aScope) {
- super(aName, undefined, aScope);
-
- this.locked = true;
- this._name = aName;
- this._rootKey = aRootKey;
- this._scope = aScope;
- this._IDToFileMap = {};
-
- let path = this._appKeyPath + "\\Extensions";
- let key = Cc["@mozilla.org/windows-registry-key;1"].createInstance(Ci.nsIWindowsRegKey);
-
- // Reading the registry may throw an exception, and that's ok. In error
- // cases, we just leave ourselves in the empty state.
- try {
- key.open(this._rootKey, path, Ci.nsIWindowsRegKey.ACCESS_READ);
- } catch (e) {
- return;
- }
-
- this._readAddons(key);
- key.close();
- }
-
- /**
- * Retrieves the path of this Application's data key in the registry.
- */
- get _appKeyPath() {
- let appVendor = Services.appinfo.vendor;
- let appName = Services.appinfo.name;
-
- // XXX Thunderbird doesn't specify a vendor string
- if (AppConstants.MOZ_APP_NAME == "thunderbird" && appVendor == "")
- appVendor = "Mozilla";
-
- // XULRunner-based apps may intentionally not specify a vendor
- if (appVendor != "")
- appVendor += "\\";
-
- return "SOFTWARE\\" + appVendor + appName;
- }
-
- /**
- * Read the registry and build a mapping between ID and path for each
- * installed add-on.
- *
- * @param {nsIWindowsRegKey} aKey
- * The key that contains the ID to path mapping
- */
- _readAddons(aKey) {
- let count = aKey.valueCount;
- for (let i = 0; i < count; ++i) {
- let id = aKey.getValueName(i);
-
- let file = new nsIFile(aKey.readStringValue(id));
-
- if (!file.exists()) {
- logger.warn("Ignoring missing add-on in " + file.path);
- continue;
- }
-
- this._IDToFileMap[id] = file;
- }
- }
-
- /**
- * Gets the name of this install location.
- */
- get name() {
- return this._name;
- }
-
- /*
- * @see DirectoryInstallLocation
- */
- isLinkedAddon(aId) {
- return true;
- }
-}
-
var XPIInternal = {
BOOTSTRAP_REASONS,
BootstrapScope,
DB_SCHEMA,
KEY_APP_SYSTEM_ADDONS,
KEY_APP_SYSTEM_DEFAULTS,
- KEY_APP_TEMPORARY,
PREF_BRANCH_INSTALLED_ADDON,
PREF_SYSTEM_ADDON_SET,
SIGNED_TYPES,
- SystemAddonInstallLocation,
+ SystemAddonLocation,
TEMPORARY_ADDON_SUFFIX,
TOOLKIT_ID,
TemporaryInstallLocation,
XPIProvider,
XPIStates,
XPI_PERMISSION,
awaitPromise,
canRunInSafeMode,
--- a/toolkit/mozapps/extensions/test/xpcshell/test_XPIStates.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_XPIStates.js
@@ -60,17 +60,17 @@ var lastTimestamp = Date.now();
* @param aPath File path to touch.
* @param aChange True if we should notice the change, False if we shouldn't.
*/
function checkChange(XS, aPath, aChange) {
Assert.ok(aPath.exists());
lastTimestamp += 10000;
info("Touching file " + aPath.path + " with " + lastTimestamp);
aPath.lastModifiedTime = lastTimestamp;
- Assert.equal(XS.getInstallState(), aChange);
+ Assert.equal(XS.scanForChanges(), aChange);
// Save the pref so we don't detect this change again
XS.save();
}
// Get a reference to the XPIState (loaded by startupManager) so we can unit test it.
function getXS() {
let XPI = ChromeUtils.import("resource://gre/modules/addons/XPIProvider.jsm", {});
return XPI.XPIStates;
@@ -90,17 +90,17 @@ add_task(async function detect_touches()
]);
info("Disable test add-ons");
await pd.disable();
let XS = getXS();
// Should be no changes detected here, because everything should start out up-to-date.
- Assert.ok(!XS.getInstallState());
+ Assert.ok(!XS.scanForChanges());
let states = XS.getLocation("app-profile");
// State should correctly reflect enabled/disabled
Assert.ok(states.get("packed-enabled@tests.mozilla.org").enabled);
Assert.ok(!states.get("packed-disabled@tests.mozilla.org").enabled);
// Touch various files and make sure the change is detected.
--- a/toolkit/mozapps/extensions/test/xpcshell/test_delay_update_webextension.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_delay_update_webextension.js
@@ -11,17 +11,17 @@ ChromeUtils.import("resource://gre/modul
if (AppConstants.platform == "win" && AppConstants.DEBUG) {
// Shutdown timing is flaky in this test, and remote extensions
// sometimes wind up leaving the XPI locked at the point when we try
// to remove it.
Services.prefs.setBoolPref("extensions.webextensions.remote", false);
}
-PromiseTestUtils.expectUncaughtRejection(/Message manager disconnected/);
+PromiseTestUtils.whitelistRejectionsGlobally(/Message manager disconnected/);
/* globals browser*/
const profileDir = gProfD.clone();
profileDir.append("extensions");
const stageDir = profileDir.clone();
stageDir.append("staged");