Bug 1272446 - rebuild addons DB from manifests when schema has changed r?kmag
MozReview-Commit-ID: BHzwRJaFU7V
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -3816,17 +3816,18 @@ this.XPIProvider = {
// from the filesystem
if (updateReasons.length > 0) {
AddonManagerPrivate.recordSimpleMeasure("XPIDB_startup_load_reasons", updateReasons);
XPIDatabase.syncLoadDB(false);
try {
extensionListChanged = XPIDatabaseReconcile.processFileChanges(manifests,
aAppChanged,
aOldAppVersion,
- aOldPlatformVersion);
+ aOldPlatformVersion,
+ updateReasons.includes("schemaChanged"));
}
catch (e) {
logger.error("Failed to process extension changes at startup", e);
}
}
if (aAppChanged) {
// When upgrading the app and using a custom skin make sure it is still
--- a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
+++ b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
@@ -33,16 +33,18 @@ XPCOMUtils.defineLazyModuleGetter(this,
"resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "Blocklist",
"@mozilla.org/extensions/blocklist;1",
Ci.nsIBlocklistService);
Cu.import("resource://gre/modules/Log.jsm");
const LOGGER_ID = "addons.xpi-utils";
+const nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile");
+
// Create a new logger for use by the Addons XPI Provider Utils
// (Requires AddonManager.jsm)
var logger = Log.repository.getLogger(LOGGER_ID);
const KEY_PROFILEDIR = "ProfD";
const FILE_DATABASE = "extensions.sqlite";
const FILE_JSON_DB = "extensions.json";
const FILE_OLD_DATABASE = "extensions.rdf";
@@ -1815,31 +1817,53 @@ this.XPIDatabaseReconcile = {
* @param aAddonState
* The new state of the add-on
* @param aOldAppVersion
* The version of the application last run with this profile or null
* if it is a new profile or the version is unknown
* @param aOldPlatformVersion
* The version of the platform last run with this profile or null
* if it is a new profile or the version is unknown
- * @return a boolean indicating if flushing caches is required to complete
- * changing this add-on
+ * @param aReloadMetadata
+ * A boolean which indicates whether metadata should be reloaded from
+ * the addon manifests. Default to false.
+ * @return the new addon.
*/
- updateCompatibility(aInstallLocation, aOldAddon, aAddonState, aOldAppVersion, aOldPlatformVersion) {
+ updateCompatibility(aInstallLocation, aOldAddon, aAddonState, aOldAppVersion,
+ aOldPlatformVersion, aReloadMetadata) {
logger.debug("Updating compatibility for add-on " + aOldAddon.id + " in " + aInstallLocation.name);
// If updating from a version of the app that didn't support signedState
// then fetch that property now
if (aOldAddon.signedState === undefined && ADDON_SIGNING &&
SIGNED_TYPES.has(aOldAddon.type)) {
let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
file.persistentDescriptor = aAddonState.descriptor;
let manifest = syncLoadManifestFromFile(file, aInstallLocation);
aOldAddon.signedState = manifest.signedState;
}
+
+ // May be updating from a version of the app that didn't support all the
+ // properties of the currently-installed add-ons.
+ if (aReloadMetadata) {
+ let file = new nsIFile()
+ file.persistentDescriptor = aAddonState.descriptor;
+ let manifest = syncLoadManifestFromFile(file, aInstallLocation);
+
+ // Avoid re-reading these properties from manifest,
+ // use existing addon instead.
+ // TODO - consider re-scanning for targetApplications.
+ let remove = ["syncGUID", "foreignInstall", "visible", "active",
+ "userDisabled", "applyBackgroundUpdates", "sourceURI",
+ "releaseNotesURI", "targetApplications"];
+
+ let props = PROP_JSON_FIELDS.filter(a => !remove.includes(a));
+ copyProperties(manifest, props, aOldAddon);
+ }
+
// This updates the addon's JSON cached data in place
applyBlocklistChanges(aOldAddon, aOldAddon, aOldAppVersion,
aOldPlatformVersion);
aOldAddon.appDisabled = !isUsableAddon(aOldAddon);
return aOldAddon;
},
@@ -1857,20 +1881,23 @@ this.XPIDatabaseReconcile = {
* true to update add-ons appDisabled property when the application
* version has changed
* @param aOldAppVersion
* The version of the application last run with this profile or null
* if it is a new profile or the version is unknown
* @param aOldPlatformVersion
* The version of the platform last run with this profile or null
* if it is a new profile or the version is unknown
+ * @param aSchemaChange
+ * The schema has changed and all add-on manifests should be re-read.
* @return a boolean indicating if a change requiring flushing the caches was
* detected
*/
- processFileChanges(aManifests, aUpdateCompatibility, aOldAppVersion, aOldPlatformVersion) {
+ processFileChanges(aManifests, aUpdateCompatibility, aOldAppVersion, aOldPlatformVersion,
+ aSchemaChange) {
let loadedManifest = (aInstallLocation, aId) => {
if (!(aInstallLocation.name in aManifests))
return null;
if (!(aId in aManifests[aInstallLocation.name]))
return null;
return aManifests[aInstallLocation.name][aId];
};
@@ -1931,32 +1958,38 @@ this.XPIDatabaseReconcile = {
oldtime: oldAddon.updateDate
});
} else {
XPIProvider.setTelemetry(oldAddon.id, "modifiedFile",
XPIProvider._mostRecentlyModifiedFile[id]);
}
}
- // The add-on has changed if the modification time has changed, or
- // we have an updated manifest for it. Also reload the metadata for
- // add-ons in the application directory when the application version
- // has changed
+ // The add-on has changed if the modification time has changed, if
+ // we have an updated manifest for it, or if the schema version has
+ // changed.
+ //
+ // Also reload the metadata for add-ons in the application directory
+ // when the application version has changed.
let newAddon = loadedManifest(installLocation, id);
if (newAddon || oldAddon.updateDate != xpiState.mtime ||
(aUpdateCompatibility && (installLocation.name == KEY_APP_GLOBAL ||
installLocation.name == KEY_APP_SYSTEM_DEFAULTS))) {
newAddon = this.updateMetadata(installLocation, oldAddon, xpiState, newAddon);
}
else if (oldAddon.descriptor != xpiState.descriptor) {
newAddon = this.updateDescriptor(installLocation, oldAddon, xpiState);
}
- else if (aUpdateCompatibility) {
+ // Check compatility when the application version and/or schema
+ // version has changed. A schema change also reloads metadata from
+ // the manifests.
+ else if (aUpdateCompatibility || aSchemaChange) {
newAddon = this.updateCompatibility(installLocation, oldAddon, xpiState,
- aOldAppVersion, aOldPlatformVersion);
+ aOldAppVersion, aOldPlatformVersion,
+ aSchemaChange);
}
else {
// No change
newAddon = oldAddon;
}
if (newAddon)
locationAddonMap.set(newAddon.id, newAddon);
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_schema_change.js
@@ -0,0 +1,317 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+BootstrapMonitor.init();
+
+const PREF_DB_SCHEMA = "extensions.databaseSchema";
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "49");
+startupManager();
+
+/**
+ * Schema change with no application update reloads metadata.
+ */
+add_task(function* schema_change() {
+ const ID = "schema-change@tests.mozilla.org";
+
+ let xpiFile = createTempXPIFile({
+ id: ID,
+ name: "Test Add-on",
+ version: "1.0",
+ bootstrap: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1.9.2"
+ }]
+ });
+
+ yield promiseInstallFile(xpiFile);
+
+ let addon = yield promiseAddonByID(ID);
+
+ notEqual(addon, null, "Got an addon object as expected");
+ equal(addon.version, "1.0", "Got the expected version");
+
+ yield promiseShutdownManager();
+
+ xpiFile = createTempXPIFile({
+ id: ID,
+ name: "Test Add-on 2",
+ version: "2.0",
+ bootstrap: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1.9.2"
+ }]
+ });
+
+ Services.prefs.setIntPref(PREF_DB_SCHEMA, 0);
+
+ let file = profileDir.clone();
+ file.append(`${ID}.xpi`);
+
+ // Make sure the timestamp is unchanged, so it is not re-scanned for that reason.
+ let timestamp = file.lastModifiedTime;
+ xpiFile.moveTo(profileDir, `${ID}.xpi`);
+
+ file.lastModifiedTime = timestamp;
+
+ yield promiseStartupManager();
+
+ addon = yield promiseAddonByID(ID);
+ notEqual(addon, null, "Got an addon object as expected");
+ equal(addon.version, "2.0", "Got the expected version");
+
+ let waitUninstall = promiseAddonEvent("onUninstalled");
+ addon.uninstall();
+ yield waitUninstall;
+});
+
+/**
+ * Application update with no schema change does not reload metadata.
+ */
+add_task(function* schema_change() {
+ const ID = "schema-change@tests.mozilla.org";
+
+ let xpiFile = createTempXPIFile({
+ id: ID,
+ name: "Test Add-on",
+ version: "1.0",
+ bootstrap: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "2"
+ }]
+ });
+
+ yield promiseInstallFile(xpiFile);
+
+ let addon = yield promiseAddonByID(ID);
+
+ notEqual(addon, null, "Got an addon object as expected");
+ equal(addon.version, "1.0", "Got the expected version");
+
+ yield promiseShutdownManager();
+
+ xpiFile = createTempXPIFile({
+ id: ID,
+ name: "Test Add-on 2",
+ version: "2.0",
+ bootstrap: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "2"
+ }]
+ });
+
+ gAppInfo.version = "2";
+ let file = profileDir.clone();
+ file.append(`${ID}.xpi`);
+
+ // Make sure the timestamp is unchanged, so it is not re-scanned for that reason.
+ let timestamp = file.lastModifiedTime;
+ xpiFile.moveTo(profileDir, `${ID}.xpi`);
+
+ file.lastModifiedTime = timestamp;
+
+ yield promiseStartupManager();
+
+ addon = yield promiseAddonByID(ID);
+ notEqual(addon, null, "Got an addon object as expected");
+ equal(addon.version, "1.0", "Got the expected version");
+
+ let waitUninstall = promiseAddonEvent("onUninstalled");
+ addon.uninstall();
+ yield waitUninstall;
+});
+
+/**
+ * App update and a schema change causes a reload of the manifest.
+ */
+add_task(function* schema_change_app_update() {
+ const ID = "schema-change@tests.mozilla.org";
+
+ let xpiFile = createTempXPIFile({
+ id: ID,
+ name: "Test Add-on",
+ version: "1.0",
+ bootstrap: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+ });
+
+ yield promiseInstallFile(xpiFile);
+
+ let addon = yield promiseAddonByID(ID);
+
+ notEqual(addon, null, "Got an addon object as expected");
+ equal(addon.version, "1.0", "Got the expected version");
+
+ yield promiseShutdownManager();
+
+ xpiFile = createTempXPIFile({
+ id: ID,
+ name: "Test Add-on 2",
+ version: "2.0",
+ bootstrap: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+ });
+
+ gAppInfo.version = "3";
+ Services.prefs.setIntPref(PREF_DB_SCHEMA, 0);
+
+ let file = profileDir.clone();
+ file.append(`${ID}.xpi`);
+
+ // Make sure the timestamp is unchanged, so it is not re-scanned for that reason.
+ let timestamp = file.lastModifiedTime;
+ xpiFile.moveTo(profileDir, `${ID}.xpi`);
+
+ file.lastModifiedTime = timestamp;
+
+ yield promiseStartupManager();
+
+ addon = yield promiseAddonByID(ID);
+ notEqual(addon, null, "Got an addon object as expected");
+ equal(addon.appDisabled, false);
+ equal(addon.version, "2.0", "Got the expected version");
+
+ let waitUninstall = promiseAddonEvent("onUninstalled");
+ addon.uninstall();
+ yield waitUninstall;
+});
+
+/**
+ * No schema change, no manifest reload.
+ */
+add_task(function* schema_change() {
+ const ID = "schema-change@tests.mozilla.org";
+
+ let xpiFile = createTempXPIFile({
+ id: ID,
+ name: "Test Add-on",
+ version: "1.0",
+ bootstrap: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1.9.2"
+ }]
+ });
+
+ yield promiseInstallFile(xpiFile);
+
+ let addon = yield promiseAddonByID(ID);
+
+ notEqual(addon, null, "Got an addon object as expected");
+ equal(addon.version, "1.0", "Got the expected version");
+
+ yield promiseShutdownManager();
+
+ xpiFile = createTempXPIFile({
+ id: ID,
+ name: "Test Add-on 2",
+ version: "2.0",
+ bootstrap: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1.9.2"
+ }]
+ });
+
+ let file = profileDir.clone();
+ file.append(`${ID}.xpi`);
+
+ // Make sure the timestamp is unchanged, so it is not re-scanned for that reason.
+ let timestamp = file.lastModifiedTime;
+ xpiFile.moveTo(profileDir, `${ID}.xpi`);
+
+ file.lastModifiedTime = timestamp;
+
+ yield promiseStartupManager();
+
+ addon = yield promiseAddonByID(ID);
+ notEqual(addon, null, "Got an addon object as expected");
+ equal(addon.version, "1.0", "Got the expected version");
+
+ let waitUninstall = promiseAddonEvent("onUninstalled");
+ addon.uninstall();
+ yield waitUninstall;
+});
+
+/**
+ * Modified timestamp on the XPI causes a reload of the manifest.
+ */
+add_task(function* schema_change() {
+ const ID = "schema-change@tests.mozilla.org";
+
+ let xpiFile = createTempXPIFile({
+ id: ID,
+ name: "Test Add-on",
+ version: "1.0",
+ bootstrap: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1.9.2"
+ }]
+ });
+
+ yield promiseInstallFile(xpiFile);
+
+ let addon = yield promiseAddonByID(ID);
+
+ notEqual(addon, null, "Got an addon object as expected");
+ equal(addon.version, "1.0", "Got the expected version");
+
+ yield promiseShutdownManager();
+
+ xpiFile = createTempXPIFile({
+ id: ID,
+ name: "Test Add-on 2",
+ version: "2.0",
+ bootstrap: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1.9.2"
+ }]
+ });
+
+ xpiFile.moveTo(profileDir, `${ID}.xpi`);
+
+ let file = profileDir.clone();
+ file.append(`${ID}.xpi`);
+
+ // Set timestamp in the future so manifest is re-scanned.
+ let timestamp = new Date(Date.now() + 60000);
+ xpiFile.moveTo(profileDir, `${ID}.xpi`);
+
+ file.lastModifiedTime = timestamp;
+
+ yield promiseStartupManager();
+
+ addon = yield promiseAddonByID(ID);
+ notEqual(addon, null, "Got an addon object as expected");
+ equal(addon.version, "2.0", "Got the expected version");
+
+ let waitUninstall = promiseAddonEvent("onUninstalled");
+ addon.uninstall();
+ yield waitUninstall;
+});
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
@@ -40,10 +40,11 @@ tags = webextensions
[test_pass_symbol.js]
[test_delay_update.js]
[test_nodisable_hidden.js]
[test_delay_update_webextension.js]
skip-if = appname == "thunderbird"
tags = webextensions
[test_dependencies.js]
[test_system_delay_update.js]
+[test_schema_change.js]
[include:xpcshell-shared.ini]