Bug 1272446 - rebuild addons DB from manifests when schema has changed r?kmag draft
authorRobert Helmer <rhelmer@mozilla.com>
Tue, 22 Nov 2016 12:56:18 -0800
changeset 443060 bfa59285cb5b5a1d538205257e3c2dc97e43484d
parent 442499 1a3194836cb4c3da6ba3a9742a2d25cf26669b55
child 537967 f6e9dc84fea8666e4e04699318eaaa5ff7159973
push id36902
push userrhelmer@mozilla.com
push dateWed, 23 Nov 2016 20:45:26 +0000
reviewerskmag
bugs1272446
milestone53.0a1
Bug 1272446 - rebuild addons DB from manifests when schema has changed r?kmag MozReview-Commit-ID: BHzwRJaFU7V
toolkit/mozapps/extensions/internal/XPIProvider.jsm
toolkit/mozapps/extensions/internal/XPIProviderUtils.js
toolkit/mozapps/extensions/test/xpcshell/test_schema_change.js
toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
--- 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]