Bug 1452618 - make getAddonBlocklistEntry asynchronous, r?kmag draft
authorGijs Kruitbosch <gijskruitbosch@gmail.com>
Mon, 09 Apr 2018 16:00:38 +0100
changeset 783168 ed4419c6e1db27955a08441ec00862d8ab3793a5
parent 783167 5a6b5a5deb62eaa7e442a251ce861d19f3313eee
child 783195 34c4b95770e99277ead520dfd79b933345281aab
push id106628
push usergijskruitbosch@gmail.com
push dateMon, 16 Apr 2018 18:02:56 +0000
reviewerskmag
bugs1452618
milestone61.0a1
Bug 1452618 - make getAddonBlocklistEntry asynchronous, r?kmag MozReview-Commit-ID: 4Kpx7M57404
toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
toolkit/mozapps/extensions/internal/XPIInstall.jsm
toolkit/mozapps/extensions/internal/XPIProvider.jsm
toolkit/mozapps/extensions/internal/XPIProviderUtils.js
toolkit/mozapps/extensions/nsBlocklistService.js
toolkit/mozapps/extensions/test/xpcshell/test_blocklist_severities.js
toolkit/mozapps/extensions/test/xpcshell/test_blocklistchange.js
xpcom/system/nsIBlocklistService.idl
--- a/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
+++ b/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
@@ -121,17 +121,18 @@ class MockBlocklist {
   unregister() {
     MockRegistrar.unregister(this.originalCID);
   }
 
   getAddonBlocklistState(addon, appVersion, toolkitVersion) {
     return this.addons.get(addon.id, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
   }
 
-  getAddonBlocklistEntry(addon, appVersion, toolkitVersion) {
+  async getAddonBlocklistEntry(addon, appVersion, toolkitVersion) {
+    await Promise.resolve();
     let state = this.getAddonBlocklistState(addon, appVersion, toolkitVersion);
     if (state != Ci.nsIBlocklistService.STATE_NOT_BLOCKED) {
       return {
         state,
         url: "http://example.com/",
       };
     }
     return null;
--- a/toolkit/mozapps/extensions/internal/XPIInstall.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIInstall.jsm
@@ -851,17 +851,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) {
+var loadManifest = async function(aPackage, aInstallLocation, aOldAddon) {
   async function loadFromRDF(aUri) {
     let manifest = await aPackage.readString("install.rdf");
     let addon = await loadManifestFromRDF(aUri, manifest);
 
     if (await aPackage.hasResource("icon.png")) {
       addon.icons[32] = "icon.png";
       addon.icons[48] = "icon.png";
     }
@@ -904,28 +904,28 @@ var loadManifest = async function(aPacka
         throw new Error(`Webextension is signed with an invalid id (${addon.id})`);
       }
     }
     if (!addon.id && aInstallLocation.name == KEY_APP_TEMPORARY) {
       addon.id = generateTemporaryInstallID(aPackage.file);
     }
   }
 
-  addon.updateBlocklistState();
+  await addon.updateBlocklistState({oldAddon: aOldAddon});
   addon.appDisabled = !isUsableAddon(addon);
 
   defineSyncGUID(addon);
 
   return addon;
 };
 
-var loadManifestFromFile = async function(aFile, aInstallLocation) {
+var loadManifestFromFile = async function(aFile, aInstallLocation, aOldAddon) {
   let pkg = Package.get(aFile);
   try {
-    let addon = await loadManifest(pkg, aInstallLocation);
+    let addon = await loadManifest(pkg, aInstallLocation, aOldAddon);
     return addon;
   } finally {
     pkg.close();
   }
 };
 
 function flushChromeCaches() {
   // Init this, so it will get the notification.
@@ -1425,17 +1425,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.addon = await loadManifest(pkg, this.installLocation, 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]);
       }
@@ -1893,17 +1893,17 @@ var LocalAddonInstall = class extends Ad
       return;
     }
 
     let addon = await new Promise(resolve => {
       XPIDatabase.getVisibleAddonForID(this.addon.id, resolve);
     });
 
     this.existingAddon = addon;
-    this.addon.updateBlocklistState({oldAddon: this.existingAddon});
+    await this.addon.updateBlocklistState({oldAddon: this.existingAddon});
     this.addon.updateDate = Date.now();
     this.addon.installDate = addon ? addon.installDate : this.addon.updateDate;
 
     if (!this.addon.isCompatible) {
       this.state = AddonManager.STATE_CHECKING;
 
       await new Promise(resolve => {
         new UpdateChecker(this.addon, {
@@ -2286,30 +2286,30 @@ var DownloadAddonInstall = class extends
       logger.debug("downloadFailed: listener changed AddonInstall state for " +
           this.sourceURI.spec + " to " + this.state);
   }
 
   /**
    * Notify listeners that the download completed.
    */
   downloadCompleted() {
-    XPIDatabase.getVisibleAddonForID(this.addon.id, aAddon => {
+    XPIDatabase.getVisibleAddonForID(this.addon.id, async aAddon => {
       if (aAddon)
         this.existingAddon = aAddon;
 
       this.state = AddonManager.STATE_DOWNLOADED;
       this.addon.updateDate = Date.now();
 
       if (this.existingAddon) {
         this.addon.existingAddonID = this.existingAddon.id;
         this.addon.installDate = this.existingAddon.installDate;
       } else {
         this.addon.installDate = this.addon.updateDate;
       }
-      this.addon.updateBlocklistState({oldAddon: this.existingAddon});
+      await this.addon.updateBlocklistState({oldAddon: this.existingAddon});
 
       if (AddonManagerPrivate.callInstallListeners("onDownloadEnded",
                                                    this.listeners,
                                                    this.wrapper)) {
         // If a listener changed our state then do not proceed with the install
         if (this.state != AddonManager.STATE_DOWNLOADED)
           return;
 
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -904,21 +904,21 @@ function getAllAliasesForTypes(aTypes) {
 
   return [...typeset];
 }
 
 /**
  * A synchronous method for loading an add-on's manifest. This should only ever
  * be used during startup or a sync load of the add-ons DB
  */
-function syncLoadManifestFromFile(aFile, aInstallLocation) {
+function syncLoadManifestFromFile(aFile, aInstallLocation, aOldAddon) {
   let success = undefined;
   let result = null;
 
-  loadManifestFromFile(aFile, aInstallLocation).then(val => {
+  loadManifestFromFile(aFile, aInstallLocation, aOldAddon).then(val => {
     success = true;
     result = val;
   }, val => {
     success = false;
     result = val;
   });
 
   Services.tm.spinEventLoopUntil(() => success !== undefined);
@@ -4488,40 +4488,40 @@ AddonInternal.prototype = {
       if (targetApp.id == Services.appinfo.ID)
         return targetApp;
       if (targetApp.id == TOOLKIT_ID)
         app = targetApp;
     }
     return app;
   },
 
-  findBlocklistEntry() {
+  async findBlocklistEntry() {
     let staticItem = findMatchingStaticBlocklistItem(this);
     if (staticItem) {
       let url = Services.urlFormatter.formatURLPref(PREF_BLOCKLIST_ITEM_URL);
       return {
         state: staticItem.level,
         url: url.replace(/%blockID%/g, staticItem.blockID)
       };
     }
 
     return Blocklist.getAddonBlocklistEntry(this.wrapper);
   },
 
-  updateBlocklistState(options = {}) {
+  async updateBlocklistState(options = {}) {
     let {applySoftBlock = true, oldAddon = null, updateDatabase = true} = options;
 
     if (oldAddon) {
       this.userDisabled = oldAddon.userDisabled;
       this.softDisabled = oldAddon.softDisabled;
       this.blocklistState = oldAddon.blocklistState;
     }
     let oldState = this.blocklistState;
 
-    let entry = this.findBlocklistEntry();
+    let entry = await this.findBlocklistEntry();
     let newState = entry ? entry.state : Blocklist.STATE_NOT_BLOCKED;
 
     this.blocklistState = newState;
     this.blocklistURL = entry && entry.url;
 
     let userDisabled, softDisabled;
     // After a blocklist update, the blocklist service manually applies
     // new soft blocks after displaying a UI, in which cases we need to
@@ -4932,17 +4932,17 @@ AddonWrapper.prototype = {
 
     let activeAddon = XPIProvider.activeAddons.get(addon.id);
     if (activeAddon)
       return activeAddon.startupPromise || null;
     return null;
   },
 
   updateBlocklistState(applySoftBlock = true) {
-    addonFor(this).updateBlocklistState({applySoftBlock});
+    return addonFor(this).updateBlocklistState({applySoftBlock});
   },
 
   get userDisabled() {
     let addon = addonFor(this);
     return addon.softDisabled || addon.userDisabled;
   },
   set userDisabled(val) {
     let addon = addonFor(this);
--- a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
+++ b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
@@ -1220,19 +1220,17 @@ this.XPIDatabaseReconcile = {
    */
   updateMetadata(aInstallLocation, aOldAddon, aAddonState, aNewAddon) {
     logger.debug("Add-on " + aOldAddon.id + " modified in " + aInstallLocation.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 = syncLoadManifestFromFile(file, aInstallLocation);
-
-        aNewAddon.updateBlocklistState({oldAddon: aOldAddon});
+        aNewAddon = syncLoadManifestFromFile(file, aInstallLocation, 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);
     } catch (e) {
       logger.warn("updateMetadata: Add-on " + aOldAddon.id + " is invalid", e);
@@ -1281,29 +1279,22 @@ this.XPIDatabaseReconcile = {
    *
    * @param  aInstallLocation
    *         The install location containing the add-on
    * @param  aOldAddon
    *         The AddonInternal as it appeared the last time the application
    *         ran
    * @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
    * @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, aReloadMetadata) {
+  updateCompatibility(aInstallLocation, aOldAddon, aAddonState, aReloadMetadata) {
     logger.debug("Updating compatibility for add-on " + aOldAddon.id + " in " + aInstallLocation.name);
 
     let checkSigning = aOldAddon.signedState === undefined && ADDON_SIGNING &&
                        SIGNED_TYPES.has(aOldAddon.type);
 
     let manifest = null;
     if (checkSigning || aReloadMetadata) {
       try {
@@ -1332,17 +1323,16 @@ this.XPIDatabaseReconcile = {
       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);
     }
 
-    aOldAddon.updateBlocklistState({updateDatabase: false});
     aOldAddon.appDisabled = !isUsableAddon(aOldAddon);
 
     return aOldAddon;
   },
 
   /**
    * Compares the add-ons that are currently installed to those that were
    * known to be installed when the application last ran and applies any
@@ -1395,16 +1385,20 @@ this.XPIDatabaseReconcile = {
       let locationAddonMap = previousAddons.get(a.location);
       if (!locationAddonMap) {
         locationAddonMap = new Map();
         previousAddons.set(a.location, locationAddonMap);
       }
       locationAddonMap.set(a.id, a);
     }
 
+    // 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 currentAddons = new Map();
     for (let installLocation of XPIProvider.installLocations) {
       let locationAddonMap = new Map();
       currentAddons.set(installLocation.name, locationAddonMap);
 
@@ -1449,18 +1443,20 @@ this.XPIDatabaseReconcile = {
               newAddon = this.updateMetadata(installLocation, oldAddon, xpiState, newAddon);
             } else if (oldPath != xpiState.path) {
               newAddon = this.updatePath(installLocation, oldAddon, xpiState);
             } else if (aUpdateCompatibility || aSchemaChange) {
               // Check compatility when the application version and/or schema
               // version has changed. A schema change also reloads metadata from
               // the manifests.
               newAddon = this.updateCompatibility(installLocation, oldAddon, xpiState,
-                                                  aOldAppVersion, aOldPlatformVersion,
                                                   aSchemaChange);
+              // 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 {
               // No change
               newAddon = oldAddon;
             }
 
             if (newAddon)
               locationAddonMap.set(newAddon.id, newAddon);
           } else {
@@ -1655,11 +1651,31 @@ this.XPIDatabaseReconcile = {
       }
     }
     XPIStates.save();
 
     // Clear out any cached migration data.
     XPIDatabase.migrateData = null;
     XPIDatabase.saveChanges();
 
+    // Do some blocklist checks. These will happen after we've just saved everything,
+    // because they're async and depend on the blocklist loading. When we're done, save
+    // the data if any of the add-ons' blocklist state has changed.
+    AddonManager.shutdown.addBlocker(
+      "Update add-on blocklist state into add-on DB",
+      (async () => {
+        // Avoid querying the AddonManager immediately to give startup a chance
+        // to complete.
+        await Promise.resolve();
+        let addons = await AddonManager.getAddonsByIDs(addonsToCheckAgainstBlocklist);
+        await Promise.all(addons.map(addon => {
+          if (addon) {
+            return addon.updateBlocklistState({updateDatabase: false});
+          }
+          return null;
+        }));
+        XPIDatabase.saveChanges();
+      })().catch(Cu.reportError)
+    );
+
     return true;
   },
 };
--- a/toolkit/mozapps/extensions/nsBlocklistService.js
+++ b/toolkit/mozapps/extensions/nsBlocklistService.js
@@ -339,19 +339,18 @@ Blocklist.prototype = {
                   Ci.nsIBlocklistService.STATE_BLOCKED : Ci.nsIBlocklistService.STATE_SOFTBLOCKED),
           url: blItem.blockID && this._createBlocklistURL(blItem.blockID),
         };
       }
     }
     return null;
   },
 
-  getAddonBlocklistEntry(addon, appVersion, toolkitVersion) {
-    if (!this.isLoaded)
-      this._loadBlocklist();
+  async getAddonBlocklistEntry(addon, appVersion, toolkitVersion) {
+    await this.loadBlocklistAsync();
     return this._getAddonBlocklistEntry(addon, this._addonEntries,
                                         appVersion, toolkitVersion);
   },
 
   /**
    * Private version of getAddonBlocklistState that allows the caller to pass in
    * the add-on blocklist entries to compare against.
    *
@@ -761,19 +760,30 @@ Blocklist.prototype = {
     return this._addonEntries != null && this._gfxEntries != null && this._pluginEntries != null;
   },
 
   /* Used for testing */
   _clear() {
     this._addonEntries = null;
     this._gfxEntries = null;
     this._pluginEntries = null;
+    delete this._preloadPromise;
   },
 
   async loadBlocklistAsync() {
+    if (this.isLoaded) {
+      return;
+    }
+    if (!this._preloadPromise) {
+      this._preloadPromise = this._loadBlocklistAsyncInternal();
+    }
+    await this._preloadPromise;
+  },
+
+  async _loadBlocklistAsyncInternal() {
     let profPath = OS.Path.join(OS.Constants.Path.profileDir, FILE_BLOCKLIST);
     try {
       await this._preloadBlocklistFile(profPath);
       return;
     } catch (e) {
       LOG("Blocklist::loadBlocklistAsync: Failed to load XML file " + e);
     }
 
@@ -781,21 +791,25 @@ Blocklist.prototype = {
     try {
       await this._preloadBlocklistFile(appFile.path);
       return;
     } catch (e) {
       LOG("Blocklist::loadBlocklistAsync: Failed to load XML file " + e);
     }
 
     LOG("Blocklist::loadBlocklistAsync: no XML File found");
+    // Neither file is present, so we just add empty lists, to avoid JS errors fetching
+    // blocklist information otherwise.
+    this._addonEntries = [];
+    this._gfxEntries = [];
+    this._pluginEntries = [];
   },
 
   async _preloadBlocklistFile(path) {
-    if (this._addonEntries) {
-      // The file has been already loaded.
+    if (this.isLoaded) {
       return;
     }
 
     if (!gBlocklistEnabled) {
       LOG("Blocklist::_preloadBlocklistFile: blocklist is disabled");
       return;
     }
 
@@ -1212,197 +1226,196 @@ Blocklist.prototype = {
     Services.obs.notifyObservers(null, "blocklist-data-gfxItems", payload);
   },
 
   _notifyObserversBlocklistUpdated() {
     Services.obs.notifyObservers(this, "blocklist-updated");
     Services.ppmm.broadcastAsyncMessage("Blocklist:blocklistInvalidated", {});
   },
 
-  _blocklistUpdated(oldAddonEntries, oldPluginEntries) {
+  async _blocklistUpdated(oldAddonEntries, oldPluginEntries) {
     var addonList = [];
 
     // A helper function that reverts the prefs passed to default values.
     function resetPrefs(prefs) {
       for (let pref of prefs)
         Services.prefs.clearUserPref(pref);
     }
     const types = ["extension", "theme", "locale", "dictionary", "service"];
-    AddonManager.getAddonsByTypes(types, addons => {
-      for (let addon of addons) {
-        let oldState = addon.blocklistState;
-        if (addon.updateBlocklistState) {
-          addon.updateBlocklistState(false);
-        } else if (oldAddonEntries) {
-          oldState = this._getAddonBlocklistState(addon, oldAddonEntries);
-        } else {
-          oldState = Ci.nsIBlocklistService.STATE_NOTBLOCKED;
-        }
-        let state = addon.blocklistState;
+    let addons = await AddonManager.getAddonsByTypes(types);
+    for (let addon of addons) {
+      let oldState = addon.blocklistState;
+      if (addon.updateBlocklistState) {
+        await addon.updateBlocklistState(false);
+      } else if (oldAddonEntries) {
+        oldState = this._getAddonBlocklistState(addon, oldAddonEntries);
+      } else {
+        oldState = Ci.nsIBlocklistService.STATE_NOTBLOCKED;
+      }
+      let state = addon.blocklistState;
+
+      LOG("Blocklist state for " + addon.id + " changed from " +
+          oldState + " to " + state);
+
+      // We don't want to re-warn about add-ons
+      if (state == oldState)
+        continue;
+
+      if (state === Ci.nsIBlocklistService.STATE_BLOCKED) {
+        // It's a hard block. We must reset certain preferences.
+        let prefs = this._getAddonPrefs(addon);
+        resetPrefs(prefs);
+      }
+
+      // Ensure that softDisabled is false if the add-on is not soft blocked
+      if (state != Ci.nsIBlocklistService.STATE_SOFTBLOCKED)
+        addon.softDisabled = false;
+
+      // Don't warn about add-ons becoming unblocked.
+      if (state == Ci.nsIBlocklistService.STATE_NOT_BLOCKED)
+        continue;
 
-        LOG("Blocklist state for " + addon.id + " changed from " +
-            oldState + " to " + state);
+      // If an add-on has dropped from hard to soft blocked just mark it as
+      // soft disabled and don't warn about it.
+      if (state == Ci.nsIBlocklistService.STATE_SOFTBLOCKED &&
+          oldState == Ci.nsIBlocklistService.STATE_BLOCKED) {
+        addon.softDisabled = true;
+        continue;
+      }
+
+      // If the add-on is already disabled for some reason then don't warn
+      // about it
+      if (!addon.isActive) {
+        // But mark it as softblocked if necessary. Note that we avoid setting
+        // softDisabled at the same time as userDisabled to make it clear
+        // which was the original cause of the add-on becoming disabled in a
+        // way that the user can change.
+        if (state == Ci.nsIBlocklistService.STATE_SOFTBLOCKED && !addon.userDisabled)
+          addon.softDisabled = true;
+        continue;
+      }
+
+      let entry = this._getAddonBlocklistEntry(addon, this._addonEntries);
+      addonList.push({
+        name: addon.name,
+        version: addon.version,
+        icon: addon.iconURL,
+        disable: false,
+        blocked: state == Ci.nsIBlocklistService.STATE_BLOCKED,
+        item: addon,
+        url: entry && entry.url,
+      });
+    }
+
+    AddonManagerPrivate.updateAddonAppDisabledStates();
 
-        // We don't want to re-warn about add-ons
-        if (state == oldState)
-          continue;
+    var phs = Cc["@mozilla.org/plugin/host;1"].
+              getService(Ci.nsIPluginHost);
+    var plugins = phs.getPluginTags();
+
+    for (let plugin of plugins) {
+      let oldState = -1;
+      if (oldPluginEntries)
+        oldState = this._getPluginBlocklistState(plugin, oldPluginEntries);
+      let state = this.getPluginBlocklistState(plugin);
+      LOG("Blocklist state for " + plugin.name + " changed from " +
+          oldState + " to " + state);
+      // We don't want to re-warn about items
+      if (state == oldState)
+        continue;
 
-        if (state === Ci.nsIBlocklistService.STATE_BLOCKED) {
-          // It's a hard block. We must reset certain preferences.
-          let prefs = this._getAddonPrefs(addon);
-          resetPrefs(prefs);
+      if (oldState == Ci.nsIBlocklistService.STATE_BLOCKED) {
+        if (state == Ci.nsIBlocklistService.STATE_SOFTBLOCKED)
+          plugin.enabledState = Ci.nsIPluginTag.STATE_DISABLED;
+      } else if (!plugin.disabled && state != Ci.nsIBlocklistService.STATE_NOT_BLOCKED) {
+        if (state != Ci.nsIBlocklistService.STATE_OUTDATED &&
+            state != Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE &&
+            state != Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE) {
+          addonList.push({
+            name: plugin.name,
+            version: plugin.version,
+            icon: "chrome://mozapps/skin/plugins/pluginGeneric.svg",
+            disable: false,
+            blocked: state == Ci.nsIBlocklistService.STATE_BLOCKED,
+            item: plugin,
+            url: this.getPluginBlocklistURL(plugin),
+          });
         }
+      }
+    }
+
+    if (addonList.length == 0) {
+      this._notifyObserversBlocklistUpdated();
+      return;
+    }
 
-        // Ensure that softDisabled is false if the add-on is not soft blocked
-        if (state != Ci.nsIBlocklistService.STATE_SOFTBLOCKED)
-          addon.softDisabled = false;
+    if ("@mozilla.org/addons/blocklist-prompt;1" in Cc) {
+      try {
+        let blockedPrompter = Cc["@mozilla.org/addons/blocklist-prompt;1"]
+                               .getService(Ci.nsIBlocklistPrompt);
+        blockedPrompter.prompt(addonList);
+      } catch (e) {
+        LOG(e);
+      }
+      this._notifyObserversBlocklistUpdated();
+      return;
+    }
 
-        // Don't warn about add-ons becoming unblocked.
-        if (state == Ci.nsIBlocklistService.STATE_NOT_BLOCKED)
+    var args = {
+      restart: false,
+      list: addonList
+    };
+    // This lets the dialog get the raw js object
+    args.wrappedJSObject = args;
+
+    /*
+      Some tests run without UI, so the async code listens to a message
+      that can be sent programatically
+    */
+    let applyBlocklistChanges = () => {
+      for (let addon of addonList) {
+        if (!addon.disable)
           continue;
 
-        // If an add-on has dropped from hard to soft blocked just mark it as
-        // soft disabled and don't warn about it.
-        if (state == Ci.nsIBlocklistService.STATE_SOFTBLOCKED &&
-            oldState == Ci.nsIBlocklistService.STATE_BLOCKED) {
-          addon.softDisabled = true;
-          continue;
-        }
-
-        // If the add-on is already disabled for some reason then don't warn
-        // about it
-        if (!addon.isActive) {
-          // But mark it as softblocked if necessary. Note that we avoid setting
-          // softDisabled at the same time as userDisabled to make it clear
-          // which was the original cause of the add-on becoming disabled in a
-          // way that the user can change.
-          if (state == Ci.nsIBlocklistService.STATE_SOFTBLOCKED && !addon.userDisabled)
-            addon.softDisabled = true;
-          continue;
-        }
-
-        let entry = this._getAddonBlocklistEntry(addon, this._addonEntries);
-        addonList.push({
-          name: addon.name,
-          version: addon.version,
-          icon: addon.iconURL,
-          disable: false,
-          blocked: state == Ci.nsIBlocklistService.STATE_BLOCKED,
-          item: addon,
-          url: entry && entry.url,
-        });
-      }
-
-      AddonManagerPrivate.updateAddonAppDisabledStates();
-
-      var phs = Cc["@mozilla.org/plugin/host;1"].
-                getService(Ci.nsIPluginHost);
-      var plugins = phs.getPluginTags();
-
-      for (let plugin of plugins) {
-        let oldState = -1;
-        if (oldPluginEntries)
-          oldState = this._getPluginBlocklistState(plugin, oldPluginEntries);
-        let state = this.getPluginBlocklistState(plugin);
-        LOG("Blocklist state for " + plugin.name + " changed from " +
-            oldState + " to " + state);
-        // We don't want to re-warn about items
-        if (state == oldState)
-          continue;
-
-        if (oldState == Ci.nsIBlocklistService.STATE_BLOCKED) {
-          if (state == Ci.nsIBlocklistService.STATE_SOFTBLOCKED)
-            plugin.enabledState = Ci.nsIPluginTag.STATE_DISABLED;
-        } else if (!plugin.disabled && state != Ci.nsIBlocklistService.STATE_NOT_BLOCKED) {
-          if (state != Ci.nsIBlocklistService.STATE_OUTDATED &&
-              state != Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE &&
-              state != Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE) {
-            addonList.push({
-              name: plugin.name,
-              version: plugin.version,
-              icon: "chrome://mozapps/skin/plugins/pluginGeneric.svg",
-              disable: false,
-              blocked: state == Ci.nsIBlocklistService.STATE_BLOCKED,
-              item: plugin,
-              url: this.getPluginBlocklistURL(plugin),
-            });
-          }
+        if (addon.item instanceof Ci.nsIPluginTag)
+          addon.item.enabledState = Ci.nsIPluginTag.STATE_DISABLED;
+        else {
+          // This add-on is softblocked.
+          addon.item.softDisabled = true;
+          // We must revert certain prefs.
+          let prefs = this._getAddonPrefs(addon.item);
+          resetPrefs(prefs);
         }
       }
 
-      if (addonList.length == 0) {
-        this._notifyObserversBlocklistUpdated();
-        return;
-      }
+      if (args.restart)
+        restartApp();
 
-      if ("@mozilla.org/addons/blocklist-prompt;1" in Cc) {
-        try {
-          let blockedPrompter = Cc["@mozilla.org/addons/blocklist-prompt;1"]
-                                 .getService(Ci.nsIBlocklistPrompt);
-          blockedPrompter.prompt(addonList);
-        } catch (e) {
-          LOG(e);
-        }
-        this._notifyObserversBlocklistUpdated();
-        return;
-      }
+      this._notifyObserversBlocklistUpdated();
+      Services.obs.removeObserver(applyBlocklistChanges, "addon-blocklist-closed");
+    };
 
-      var args = {
-        restart: false,
-        list: addonList
-      };
-      // This lets the dialog get the raw js object
-      args.wrappedJSObject = args;
-
-      /*
-        Some tests run without UI, so the async code listens to a message
-        that can be sent programatically
-      */
-      let applyBlocklistChanges = () => {
-        for (let addon of addonList) {
-          if (!addon.disable)
-            continue;
+    Services.obs.addObserver(applyBlocklistChanges, "addon-blocklist-closed");
 
-          if (addon.item instanceof Ci.nsIPluginTag)
-            addon.item.enabledState = Ci.nsIPluginTag.STATE_DISABLED;
-          else {
-            // This add-on is softblocked.
-            addon.item.softDisabled = true;
-            // We must revert certain prefs.
-            let prefs = this._getAddonPrefs(addon.item);
-            resetPrefs(prefs);
-          }
-        }
-
-        if (args.restart)
-          restartApp();
-
-        this._notifyObserversBlocklistUpdated();
-        Services.obs.removeObserver(applyBlocklistChanges, "addon-blocklist-closed");
-      };
+    if (Services.prefs.getBoolPref(PREF_BLOCKLIST_SUPPRESSUI, false)) {
+      applyBlocklistChanges();
+      return;
+    }
 
-      Services.obs.addObserver(applyBlocklistChanges, "addon-blocklist-closed");
-
-      if (Services.prefs.getBoolPref(PREF_BLOCKLIST_SUPPRESSUI, false)) {
+    function blocklistUnloadHandler(event) {
+      if (event.target.location == URI_BLOCKLIST_DIALOG) {
         applyBlocklistChanges();
-        return;
+        blocklistWindow.removeEventListener("unload", blocklistUnloadHandler);
       }
+    }
 
-      function blocklistUnloadHandler(event) {
-        if (event.target.location == URI_BLOCKLIST_DIALOG) {
-          applyBlocklistChanges();
-          blocklistWindow.removeEventListener("unload", blocklistUnloadHandler);
-        }
-      }
-
-      let blocklistWindow = Services.ww.openWindow(null, URI_BLOCKLIST_DIALOG, "",
-                              "chrome,centerscreen,dialog,titlebar", args);
-      if (blocklistWindow)
-        blocklistWindow.addEventListener("unload", blocklistUnloadHandler);
-    });
+    let blocklistWindow = Services.ww.openWindow(null, URI_BLOCKLIST_DIALOG, "",
+                            "chrome,centerscreen,dialog,titlebar", args);
+    if (blocklistWindow)
+      blocklistWindow.addEventListener("unload", blocklistUnloadHandler);
   },
 
   classID: Components.ID("{66354bc9-7ed1-4692-ae1d-8da97d6b205e}"),
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
                                          Ci.nsIBlocklistService,
                                          Ci.nsITimerCallback]),
 };
 
--- a/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_severities.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_severities.js
@@ -9,18 +9,18 @@ ChromeUtils.import("resource://testing-c
 
 var gTestserver = AddonTestUtils.createHttpServer({hosts: ["example.com"]});
 gTestserver.registerDirectory("/data/", do_get_file("data"));
 
 // Workaround for Bug 658720 - URL formatter can leak during xpcshell tests
 const PREF_BLOCKLIST_ITEM_URL = "extensions.blocklist.itemURL";
 Services.prefs.setCharPref(PREF_BLOCKLIST_ITEM_URL, "http://example.com/blocklist/%blockID%");
 
-function getAddonBlocklistURL(addon) {
-  let entry = Services.blocklist.getAddonBlocklistEntry(addon);
+async function getAddonBlocklistURL(addon) {
+  let entry = await Services.blocklist.getAddonBlocklistEntry(addon);
   return entry && entry.url;
 }
 
 var ADDONS = [{
   // Tests how the blocklist affects a disabled add-on
   id: "test_bug455906_1@tests.mozilla.org",
   name: "Bug 455906 Addon Test 1",
   version: "5",
@@ -361,21 +361,21 @@ add_task(async function test_pt3() {
   equal(check_plugin_state(PLUGINS[2]), "false,true");
   equal(check_plugin_state(PLUGINS[3]), "true,true");
   equal(check_plugin_state(PLUGINS[4]), "false,true");
 
   // Should have gained the blocklist state but no longer be soft disabled
   checkAddonState(addons[3], {userDisabled: false, softDisabled: false, appDisabled: true});
 
   // Check blockIDs are correct
-  equal(getAddonBlocklistURL(addons[0]), create_blocklistURL(addons[0].id));
-  equal(getAddonBlocklistURL(addons[1]), create_blocklistURL(addons[1].id));
-  equal(getAddonBlocklistURL(addons[2]), create_blocklistURL(addons[2].id));
-  equal(getAddonBlocklistURL(addons[3]), create_blocklistURL(addons[3].id));
-  equal(getAddonBlocklistURL(addons[4]), create_blocklistURL(addons[4].id));
+  equal(await getAddonBlocklistURL(addons[0]), create_blocklistURL(addons[0].id));
+  equal(await getAddonBlocklistURL(addons[1]), create_blocklistURL(addons[1].id));
+  equal(await getAddonBlocklistURL(addons[2]), create_blocklistURL(addons[2].id));
+  equal(await getAddonBlocklistURL(addons[3]), create_blocklistURL(addons[3].id));
+  equal(await getAddonBlocklistURL(addons[4]), create_blocklistURL(addons[4].id));
 
   // All plugins have the same blockID on the test
   equal(Services.blocklist.getPluginBlocklistURL(PLUGINS[0]), create_blocklistURL("test_bug455906_plugin"));
   equal(Services.blocklist.getPluginBlocklistURL(PLUGINS[1]), create_blocklistURL("test_bug455906_plugin"));
   equal(Services.blocklist.getPluginBlocklistURL(PLUGINS[2]), create_blocklistURL("test_bug455906_plugin"));
   equal(Services.blocklist.getPluginBlocklistURL(PLUGINS[3]), create_blocklistURL("test_bug455906_plugin"));
   equal(Services.blocklist.getPluginBlocklistURL(PLUGINS[4]), create_blocklistURL("test_bug455906_plugin"));
 
--- a/toolkit/mozapps/extensions/test/xpcshell/test_blocklistchange.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_blocklistchange.js
@@ -799,17 +799,17 @@ add_task(async function run_app_update_s
   check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
 });
 
 add_task(async function update_schema_2() {
   await promiseShutdownManager();
 
   await changeXPIDBVersion(100);
   gAppInfo.version = "2";
-  startupManager(true);
+  await promiseStartupManager(true);
 
   let [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS);
 
   check_addon(s1, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
   check_addon(s2, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
   check_addon(s3, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
   check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
   check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
@@ -823,17 +823,17 @@ add_task(async function update_schema_2(
 });
 
 add_task(async function update_schema_3() {
   await promiseRestartManager();
 
   await promiseShutdownManager();
   await changeXPIDBVersion(100);
   gAppInfo.version = "2.5";
-  startupManager(true);
+  await promiseStartupManager(true);
 
   let [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS);
 
   check_addon(s1, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
   check_addon(s2, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
   check_addon(s3, "1.0", false, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
   check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
   check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
@@ -856,17 +856,17 @@ add_task(async function update_schema_4(
   check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
 });
 
 add_task(async function update_schema_5() {
   await promiseShutdownManager();
 
   await changeXPIDBVersion(100);
   gAppInfo.version = "1";
-  startupManager(true);
+  await promiseStartupManager(true);
 
   let [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS);
 
   check_addon(s1, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
   check_addon(s2, "1.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
   check_addon(s3, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
   check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
   check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
@@ -1057,17 +1057,17 @@ add_task(async function run_addon_change
 
   getFileForAddon(profileDir, softblock1_1.id).remove(true);
   getFileForAddon(profileDir, softblock2_1.id).remove(true);
   getFileForAddon(profileDir, softblock3_1.id).remove(true);
   getFileForAddon(profileDir, softblock4_1.id).remove(true);
   getFileForAddon(profileDir, hardblock_1.id).remove(true);
   getFileForAddon(profileDir, regexpblock_1.id).remove(true);
 
-  startupManager(false);
+  await promiseStartupManager(false);
   await promiseShutdownManager();
 
   writeInstallRDFForExtension(softblock1_2, profileDir);
   writeInstallRDFForExtension(softblock2_2, profileDir);
   writeInstallRDFForExtension(softblock3_2, profileDir);
   writeInstallRDFForExtension(softblock4_2, profileDir);
   writeInstallRDFForExtension(hardblock_2, profileDir);
   writeInstallRDFForExtension(regexpblock_2, profileDir);
@@ -1183,17 +1183,17 @@ add_task(async function run_background_u
 
   getFileForAddon(profileDir, softblock1_1.id).remove(true);
   getFileForAddon(profileDir, softblock2_1.id).remove(true);
   getFileForAddon(profileDir, softblock3_1.id).remove(true);
   getFileForAddon(profileDir, softblock4_1.id).remove(true);
   getFileForAddon(profileDir, hardblock_1.id).remove(true);
   getFileForAddon(profileDir, regexpblock_1.id).remove(true);
 
-  startupManager(false);
+  await promiseStartupManager(false);
   await promiseShutdownManager();
 
   writeInstallRDFForExtension(softblock1_3, profileDir);
   writeInstallRDFForExtension(softblock2_3, profileDir);
   writeInstallRDFForExtension(softblock3_3, profileDir);
   writeInstallRDFForExtension(softblock4_3, profileDir);
   writeInstallRDFForExtension(hardblock_3, profileDir);
   writeInstallRDFForExtension(regexpblock_3, profileDir);
@@ -1289,17 +1289,17 @@ add_task(async function run_manual_updat
 
   getFileForAddon(profileDir, softblock1_1.id).remove(true);
   getFileForAddon(profileDir, softblock2_1.id).remove(true);
   getFileForAddon(profileDir, softblock3_1.id).remove(true);
   getFileForAddon(profileDir, softblock4_1.id).remove(true);
   getFileForAddon(profileDir, hardblock_1.id).remove(true);
   getFileForAddon(profileDir, regexpblock_1.id).remove(true);
 
-  startupManager(false);
+  await promiseStartupManager(false);
   await promiseShutdownManager();
 
   writeInstallRDFForExtension(softblock1_1, profileDir);
   writeInstallRDFForExtension(softblock2_1, profileDir);
   writeInstallRDFForExtension(softblock3_1, profileDir);
   writeInstallRDFForExtension(softblock4_1, profileDir);
   writeInstallRDFForExtension(hardblock_1, profileDir);
   writeInstallRDFForExtension(regexpblock_1, profileDir);
--- a/xpcom/system/nsIBlocklistService.idl
+++ b/xpcom/system/nsIBlocklistService.idl
@@ -59,19 +59,20 @@ interface nsIBlocklistService : nsISuppo
    *          is used.
    * @returns The STATE constant.
    */
   unsigned long getPluginBlocklistState(in nsIPluginTag plugin,
                                         [optional] in AString appVersion,
                                         [optional] in AString toolkitVersion);
 
   /**
-   * Returns the blocklist entry, as an object with `state` and `url`
+   * Returns a promise that resolves to the blocklist entry.
+   * The blocklist entry is an object with `state` and `url`
    * properties, if a blocklist entry for the add-on exists, or null
-   * othereise.
+   * otherwise.
 
    * @param   addon
    *          The addon object to match.
    * @param   appVersion
    *          The version of the application we are checking in the blocklist.
    *          If this parameter is null, the version of the running application
    *          is used.
    * @param   toolkitVersion