Bug #1313298. Allow to reinstall extensions that were disabled by user, if user wants to do it. draft
authorapranovich <artyom.pranovich@gmail.com>
Tue, 12 Jun 2018 13:09:37 +0300
changeset 809618 4e504c80354fee670a90967f49c39ff32e4b8842
parent 806789 62ab31ea0ec73e72a1ea9d44c9c4b003813b6724
push id113729
push userbmo:artyom.pranovich@gmail.com
push dateFri, 22 Jun 2018 15:22:04 +0000
bugs1313298
milestone62.0a1
Bug #1313298. Allow to reinstall extensions that were disabled by user, if user wants to do it. MozReview-Commit-ID: Jhq0MVVp26g
toolkit/mozapps/extensions/internal/XPIDatabase.jsm
toolkit/mozapps/extensions/internal/XPIInstall.jsm
toolkit/mozapps/extensions/internal/XPIProvider.jsm
toolkit/mozapps/extensions/test/xpcshell/test_blocklistchange.js
toolkit/mozapps/extensions/test/xpcshell/test_install.js
toolkit/mozapps/extensions/test/xpcshell/test_reinstall_disabled_addon.js
toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
--- a/toolkit/mozapps/extensions/internal/XPIDatabase.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIDatabase.jsm
@@ -518,20 +518,26 @@ class AddonInternal {
     return app;
   }
 
   async findBlocklistEntry() {
     return Blocklist.getAddonBlocklistEntry(this.wrapper);
   }
 
   async updateBlocklistState(options = {}) {
-    let {applySoftBlock = true, oldAddon = null, updateDatabase = true} = options;
+    let {applySoftBlock = true, oldAddon = null, updateDatabase = true, userIntentionalInstall = false} = options;
 
     if (oldAddon) {
-      this.userDisabled = oldAddon.userDisabled;
+      // If user manually reinstall existing extension of different version
+      // (it means that it's manually upgrading) we should enable it first.
+      // Case when user reinstalls existing extension of the SAME version treats
+      // differently and handled at XPIInstall.startInstall().
+      this.userDisabled = userIntentionalInstall && this.version !== oldAddon.version
+       ? false
+       : oldAddon.userDisabled;
       this.softDisabled = oldAddon.softDisabled;
       this.blocklistState = oldAddon.blocklistState;
     }
     let oldState = this.blocklistState;
 
     let entry = await this.findBlocklistEntry();
     let newState = entry ? entry.state : Services.blocklist.STATE_NOT_BLOCKED;
 
--- a/toolkit/mozapps/extensions/internal/XPIInstall.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIInstall.jsm
@@ -1353,16 +1353,19 @@ class AddonInstall {
     this.version = options.version || null;
 
     this.file = null;
     this.ownsTempFile = null;
 
     this.addon = null;
     this.state = null;
 
+    // indicate that addon update is running by browser automatically
+    this.browserAutoAddonUpdate = options.browserAutoAddonUpdate || false;
+
     XPIInstall.installs.add(this);
   }
 
   /**
    * Starts installation of this add-on from whatever state it is currently at
    * if possible.
    *
    * Note this method is overridden to handle additional state in
@@ -1666,16 +1669,25 @@ class AddonInstall {
       this.postpone(resumeFn);
       return;
     }
 
     this.state = AddonManager.STATE_READY;
     this.install();
   }
 
+  ifReinstallOfExistingSameVersionDisabledAddon() {
+    return this.existingAddon &&
+           this.existingAddon.location === this.location &&
+           this.existingAddon.version === this.addon.version &&
+           this.existingAddon.userDisabled &&
+           !this.existingAddon.active &&
+           !this.browserAutoAddonUpdate;
+  }
+
   // TODO This relies on the assumption that we are always installing into the
   // highest priority install location so the resulting add-on will be visible
   // overriding any existing copy in another install location (bug 557710).
   /**
    * Installs the add-on into the install location.
    */
   startInstall() {
     this.state = AddonManager.STATE_INSTALLING;
@@ -1693,16 +1705,29 @@ class AddonInstall {
       if (install.state == AddonManager.STATE_INSTALLED &&
           install.location == this.location &&
           install.addon.id == this.addon.id) {
         logger.debug(`Cancelling previous pending install of ${install.addon.id}`);
         install.cancel();
       }
     }
 
+    // reinstall existing user-disabled addon (of the same installed version)
+    // if addon is marked to be uninstalled - don't reinstall it (bug 1313298)
+    const reinstall = this.ifReinstallOfExistingSameVersionDisabledAddon();
+    if (reinstall && !this.existingAddon.pendingUninstall) {
+      (async () => {
+        // enable existing addon back and handle chrome settings API
+        await XPIInternal.BootstrapScope.get(this.existingAddon).enable(this.existingAddon);
+        this.state = AddonManager.STATE_INSTALLED;
+        this._callInstallListeners("onInstallEnded", this.existingAddon.wrapper);
+      })();
+      return;
+    }
+
     let isUpgrade = this.existingAddon &&
                     this.existingAddon.location == this.location;
 
     logger.debug("Starting install of " + this.addon.id + " from " + this.sourceURI.spec);
     AddonManagerPrivate.callAddonListeners("onInstalling",
                                            this.addon.wrapper,
                                            false);
 
@@ -1734,18 +1759,17 @@ class AddonInstall {
           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);
+          this.addon = XPIDatabase.updateAddonMetadata(this.existingAddon, this.addon, file.path);
           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);
@@ -1768,17 +1792,17 @@ class AddonInstall {
         // Notify providers that a new theme has been enabled.
         if (isTheme(this.addon.type) && this.addon.active)
           AddonManagerPrivate.notifyAddonChanged(this.addon.id, this.addon.type);
       };
 
       this._startupPromise = (async () => {
         if (this.existingAddon) {
           await XPIInternal.BootstrapScope.get(this.existingAddon).update(
-            this.addon, !this.addon.disabled, install);
+          this.addon, !this.addon.disabled, install);
         } else {
           await install();
           await XPIInternal.BootstrapScope.get(this.addon).install(undefined, true);
         }
       })();
 
       await this._startupPromise;
     })().catch((e) => {
@@ -1793,16 +1817,17 @@ class AddonInstall {
                                              this.addon.wrapper);
       this._callInstallListeners("onInstallFailed");
     }).then(() => {
       this.removeTemporaryFile();
       return this.location.installer.releaseStagingDir();
     });
   }
 
+
   /**
    * Stages an add-on for install.
    *
    * @param {boolean} restartRequired
    *        If true, the final installation will be deferred until the
    *        next app startup.
    * @param {AddonInternal} stagedAddon
    *        The AddonInternal object for the staged install.
@@ -2358,17 +2383,17 @@ var DownloadAddonInstall = class extends
     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;
     }
-    await this.addon.updateBlocklistState({oldAddon: this.existingAddon});
+    await this.addon.updateBlocklistState({oldAddon: this.existingAddon, userIntentionalInstall: !this.browserAutoAddonUpdate});
 
     if (this._callInstallListeners("onDownloadEnded")) {
       // If a listener changed our state then do not proceed with the install
       if (this.state != AddonManager.STATE_DOWNLOADED)
         return;
 
       // proceed with the install state machine.
       this.install();
@@ -2423,17 +2448,19 @@ function createUpdate(aCallback, aAddon,
     if (url instanceof Ci.nsIFileURL) {
       install = new LocalAddonInstall(aAddon.location, url, opts);
       await install.init();
     } else {
       install = new DownloadAddonInstall(aAddon.location, url, opts);
     }
     try {
       if (aUpdate.updateInfoURL)
-        install.releaseNotesURI = Services.io.newURI(escapeAddonURI(aAddon, aUpdate.updateInfoURL));
+      // if extension is updating by background update and extension is disabled, we aren't going to reinstall disabled extensions
+      // so that we use browserAutoAddonUpdate param here
+      install = new DownloadAddonInstall(aAddon.location, url, {...opts, browserAutoAddonUpdate: true});
     } catch (e) {
       // If the releaseNotesURI cannot be parsed then just ignore it.
     }
 
     aCallback(install);
   })();
 }
 
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -1838,16 +1838,38 @@ class BootstrapScope {
 
     if (updateCallback) {
       await updateCallback();
     }
 
     this.addon = newAddon;
     return this._install(reason, callUpdate, startup, extraArgs);
   }
+
+  /**
+   * Update disabled state of extension by updating appropriate addon metadata in DB.
+   * If extension was disabled before, it then calls bootstrap scope's startup method
+   * with BOOTSTRAP_REASONS.ADDON_ENABLE reason.
+   * Along the way it notifies appropriate registered addon listeners (e.g. onEnabled, etc..).
+   *
+   * @param {Object} aAddon
+   *        Addon that is going to be enabled.
+   * @param {function} [enableCallback]
+   *        An optional callback function to call before enabling
+   *        the old add-on.
+   * @returns {Promise<bool>}
+   *        Resolves when all required bootstrap callbacks have
+   *        completed.
+   */
+  async enable(aAddon, enableCallback) {
+    if (enableCallback) {
+      await enableCallback();
+    }
+    return XPIDatabase.updateAddonDisabledState(aAddon, false);
+  }
 }
 
 var XPIProvider = {
   get name() {
     return "XPIProvider";
   },
 
   BOOTSTRAP_REASONS: Object.freeze(BOOTSTRAP_REASONS),
--- a/toolkit/mozapps/extensions/test/xpcshell/test_blocklistchange.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_blocklistchange.js
@@ -1244,32 +1244,32 @@ add_task(async function run_manual_updat
   await promiseRestartManager();
 
   await Pmanual_update("2");
   await promiseRestartManager();
 
   [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS);
 
   check_addon(s1, "2.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
-  check_addon(s2, "2.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+  check_addon(s2, "2.0", false, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
   check_addon(s3, "2.0", false, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
-  check_addon(s4, "2.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+  check_addon(s4, "2.0", false, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
   // Can't manually update to a hardblocked add-on
   check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
   check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
 
   await Pmanual_update("3");
   await promiseRestartManager();
 
   [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS);
 
   check_addon(s1, "3.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
-  check_addon(s2, "3.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+  check_addon(s2, "3.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
   check_addon(s3, "3.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
-  check_addon(s4, "3.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+  check_addon(s4, "3.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
   check_addon(h, "3.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
   check_addon(r, "3.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
 });
 
 // Starts with add-ons blocked and then new versions are installed outside of
 // the app to change them to unblocked.
 add_task(async function run_manual_update_2_test() {
   await promiseShutdownManager();
@@ -1309,31 +1309,31 @@ add_task(async function run_manual_updat
   await promiseRestartManager();
 
   await Pmanual_update("2");
   await promiseRestartManager();
 
   [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS);
 
   check_addon(s1, "2.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
-  check_addon(s2, "2.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+  check_addon(s2, "2.0", false, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
   check_addon(s3, "2.0", false, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
   // Can't manually update to a hardblocked add-on
   check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
   check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
 
   await promiseRestartManager();
 
   await Pmanual_update("3");
   await promiseRestartManager();
 
   [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS);
 
   check_addon(s1, "3.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
-  check_addon(s2, "3.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+  check_addon(s2, "3.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
   check_addon(s3, "3.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
   check_addon(h, "3.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
   check_addon(r, "3.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
 
   await s1.enable();
   await s2.enable();
   await s4.disable();
 });
--- a/toolkit/mozapps/extensions/test/xpcshell/test_install.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_install.js
@@ -699,17 +699,17 @@ add_task(async function test_15() {
 
   install.addListener({
     onInstallStarted() {
       do_throw("Install should not have continued");
     }
   });
 });
 
-// Verify that the userDisabled value carries over to the upgrade by default
+// on manual user intentional reinstall, addon should be enabled back
 add_task(async function test_16() {
   await promiseRestartManager();
 
   let url = "http://example.com/addons/test_install2_1.xpi";
   let aInstall = await AddonManager.getInstallForURL(url, "application/x-xpinstall");
   await new Promise(resolve => {
     aInstall.addListener({
       onInstallStarted() {
@@ -739,26 +739,26 @@ add_task(async function test_16() {
       onInstallEnded() {
         resolve();
       }
     });
     aInstall_2.install();
   });
 
   checkAddon("addon2@tests.mozilla.org", aInstall_2.addon, {
-    userDisabled: true,
-    isActive: false,
+    userDisabled: false,
+    isActive: true,
   });
 
   await promiseRestartManager();
 
   let a2_2 = await AddonManager.getAddonByID("addon2@tests.mozilla.org");
   checkAddon("addon2@tests.mozilla.org", a2_2, {
-    userDisabled: true,
-    isActive: false,
+    userDisabled: false,
+    isActive: true,
   });
 
   await a2_2.uninstall();
 });
 
 // Verify that changing the userDisabled value before onInstallEnded works
 add_task(async function test_17() {
   await promiseRestartManager();
@@ -839,17 +839,18 @@ add_task(async function test_18() {
     isActive: false,
   });
 
   let url_2 = "http://example.com/addons/test_install2_2.xpi";
   let aInstall_2 = await AddonManager.getInstallForURL(url_2, "application/x-xpinstall");
   await new Promise(resolve => {
     aInstall_2.addListener({
       onInstallStarted() {
-        ok(aInstall_2.addon.userDisabled);
+        // userDisabled is set to false already while running XPIDatabase.updateBlocklistState
+        ok(!aInstall_2.addon.userDisabled);
         aInstall_2.addon.enable();
       },
 
       onInstallEnded() {
         resolve();
       }
     });
     aInstall_2.install();
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_reinstall_disabled_addon.js
@@ -0,0 +1,344 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+ChromeUtils.import("resource://testing-common/httpd.js");
+
+var testserver = AddonTestUtils.createHttpServer({hosts: ["example.com"]});
+
+const ID = "test_addon@tests.mozilla.org";
+const ID_UPDATE = "addon1@tests.mozilla.org";
+const url1 = "http://example.com/addons/test_install1_1.xpi";
+const url2 = "http://example.com/addons/test_install1_2.xpi";
+
+const ADDONS = {
+  test_install1_1: {
+    "install.rdf": {
+      id: ID,
+      version: "1.0",
+      name: "Test 1 Addon",
+      description: "Test Description",
+      bootstrap: true,
+
+      targetApplications: [{
+          id: "xpcshell@tests.mozilla.org",
+          minVersion: "1",
+          maxVersion: "1"}],
+
+    },
+    "icon.png": "Fake icon image",
+  },
+  test_install1_2: {
+    "install.rdf": {
+      id: ID,
+      version: "2.0",
+      name: "Test 2 Addon",
+      description: "Test Description",
+      bootstrap: true,
+
+      targetApplications: [{
+          id: "xpcshell@tests.mozilla.org",
+          minVersion: "1",
+          maxVersion: "1"}],
+    },
+  },
+  test_update: {
+    "install.rdf": {
+      id: ID_UPDATE,
+      version: "2.0",
+      bootstrap: true,
+      targetApplications: [{
+        id: "xpcshell@tests.mozilla.org",
+        minVersion: "1",
+        maxVersion: "1"
+      }],
+      name: "Test Update 1",
+    },
+  },
+};
+
+const XPIS = {};
+
+for (let [name, files] of Object.entries(ADDONS)) {
+  XPIS[name] = AddonTestUtils.createTempXPIFile(files);
+  testserver.registerFile(`/addons/${name}.xpi`, XPIS[name]);
+}
+
+// The test extension uses an insecure update url.
+Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, false);
+
+add_task(async function setup() {
+  createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+  await promiseStartupManager();
+
+  // Make sure we only register once despite multiple calls
+  AddonManager.addInstallListener(InstallListener);
+  AddonManager.addAddonListener(AddonListener);
+  AddonManager.addInstallListener(InstallListener);
+  AddonManager.addAddonListener(AddonListener);
+
+  // Create and configure the HTTP server.
+  testserver.registerDirectory("/data/", do_get_file("data"));
+  testserver.registerPathHandler("/redirect", function(aRequest, aResponse) {
+    aResponse.setStatusLine(null, 301, "Moved Permanently");
+    let url = aRequest.host + ":" + aRequest.port + aRequest.queryString;
+    aResponse.setHeader("Location", "http://" + url);
+  });
+  gPort = testserver.identity.primaryPort;
+});
+
+
+// user intentionally reinstalls existing disabled addon of the same version, so we should enable it
+// no onInstalling nor onInstalled are fired
+add_task(async function reinstallExistingDisabledAddonSameVersion() {
+  prepare_test({
+    [ID]: [
+      ["onInstalling", false],
+      "onInstalled",
+    ]
+  }, [
+    "onNewInstall",
+    "onDownloadStarted",
+    "onDownloadEnded",
+    "onInstallStarted",
+    "onInstallEnded",
+  ]);
+  let aInstall = await AddonManager.getInstallForURL(url1, "application/x-xpinstall");
+  await aInstall.install();
+  ensure_test_completed();
+
+  await promiseRestartManager();
+
+  let a1 = await promiseAddonByID(ID);
+  Assert.notEqual(a1, null);
+  Assert.equal(a1.pendingOperations, AddonManager.PENDING_NONE);
+  Assert.ok(a1.isActive);
+  Assert.ok(!a1.userDisabled);
+
+  prepare_test({
+    [ID]: [
+      ["onDisabling", false],
+      "onDisabled"
+    ]
+  });
+  await a1.disable();
+  ensure_test_completed();
+
+  await promiseRestartManager();
+
+  a1 = await promiseAddonByID(ID);
+  Assert.notEqual(a1, null);
+  Assert.equal(a1.pendingOperations, AddonManager.PENDING_NONE);
+  Assert.ok(!a1.isActive);
+  Assert.ok(a1.userDisabled);
+
+  prepare_test({
+    [ID]: [
+      ["onEnabling", false],
+      "onEnabled",
+    ]
+  }, [
+    "onNewInstall",
+    "onDownloadStarted",
+    "onDownloadEnded",
+    "onInstallStarted",
+    "onInstallEnded",
+  ]);
+  aInstall = await AddonManager.getInstallForURL(url1, "application/x-xpinstall");
+  await aInstall.install();
+  ensure_test_completed();
+
+  a1 = await promiseAddonByID(ID);
+  Assert.notEqual(a1, null);
+  Assert.equal(a1.pendingOperations, AddonManager.PENDING_NONE);
+  Assert.ok(a1.isActive);
+  Assert.ok(!a1.userDisabled);
+
+  await promiseRestartManager();
+  a1 = await promiseAddonByID(ID);
+  Assert.notEqual(a1, null);
+  Assert.ok(a1.isActive);
+  Assert.ok(!a1.userDisabled);
+
+  await a1.uninstall();
+
+  await promiseRestartManager();
+
+  a1 = await promiseAddonByID(ID);
+  Assert.equal(a1, null);
+});
+
+// user intentionally reinstalls existing disabled addon of different version, so we should enable it too
+// look at the expected addon listeners fired to see the difference from the test above
+add_task(async function reinstallExistingDisabledAddonDifferentVersion() {
+  prepare_test({
+    [ID]: [
+      ["onInstalling", false],
+      "onInstalled",
+    ]
+  }, [
+    "onNewInstall",
+    "onDownloadStarted",
+    "onDownloadEnded",
+    "onInstallStarted",
+    "onInstallEnded",
+  ]);
+  let aInstall = await AddonManager.getInstallForURL(url1, "application/x-xpinstall");
+  await aInstall.install();
+  ensure_test_completed();
+
+  await promiseRestartManager();
+
+  let a1 = await promiseAddonByID(ID);
+  Assert.notEqual(a1, null);
+  Assert.equal(a1.pendingOperations, AddonManager.PENDING_NONE);
+  Assert.ok(a1.isActive);
+  Assert.ok(!a1.userDisabled);
+
+  prepare_test({
+    [ID]: [
+      ["onDisabling", false],
+      "onDisabled"
+    ]
+  });
+  await a1.disable();
+  ensure_test_completed();
+
+  await promiseRestartManager();
+
+  a1 = await promiseAddonByID(ID);
+  Assert.notEqual(a1, null);
+  Assert.equal(a1.pendingOperations, AddonManager.PENDING_NONE);
+  Assert.ok(!a1.isActive);
+  Assert.ok(a1.userDisabled);
+
+  prepare_test({
+    [ID]: [
+      ["onInstalling", false],
+      "onInstalled",
+    ]
+  }, [
+    "onNewInstall",
+    "onDownloadStarted",
+    "onDownloadEnded",
+    "onInstallStarted",
+    "onInstallEnded",
+  ]);
+  aInstall = await AddonManager.getInstallForURL(url2, "application/x-xpinstall");
+  await aInstall.install();
+  ensure_test_completed();
+
+  a1 = await promiseAddonByID(ID);
+  Assert.notEqual(a1, null);
+  Assert.equal(a1.pendingOperations, AddonManager.PENDING_NONE);
+  Assert.ok(a1.isActive);
+  Assert.ok(!a1.userDisabled);
+  Assert.equal(a1.version, "2.0");
+
+  await promiseRestartManager();
+  a1 = await promiseAddonByID(ID);
+  Assert.notEqual(a1, null);
+  Assert.ok(a1.isActive);
+  Assert.ok(!a1.userDisabled);
+
+  await a1.uninstall();
+
+  await promiseRestartManager();
+
+  a1 = await promiseAddonByID(ID);
+  Assert.equal(a1, null);
+});
+
+// disabled addon is updating by auto background check, but addon still should be disabled
+add_task(async function reinstallDisabledExistingAddonByBackgroundCheck() {
+  prepare_test({
+    [ID_UPDATE]: [
+      ["onInstalling", false],
+      "onInstalled",
+    ]
+  }, [
+    "onNewInstall",
+    "onInstallStarted",
+    "onInstallEnded",
+  ]);
+
+  await promiseInstallXPI({
+    id: [ID_UPDATE],
+    version: "1.0",
+    bootstrap: true,
+    updateURL: "http://example.com/data/test_update.json",
+    targetApplications: [{
+      id: "xpcshell@tests.mozilla.org",
+      minVersion: "1",
+      maxVersion: "1"
+    }],
+    name: "Test Addon 1",
+  });
+
+  let a1 = await AddonManager.getAddonByID(ID_UPDATE);
+  Assert.notEqual(a1, null);
+  Assert.equal(a1.pendingOperations, AddonManager.PENDING_NONE);
+  Assert.ok(a1.isActive);
+
+  prepare_test({
+    [ID_UPDATE]: [
+      ["onDisabling", false],
+      "onDisabled"
+    ]
+  });
+  await a1.disable();
+
+  a1 = await promiseAddonByID(ID_UPDATE);
+  Assert.notEqual(a1, null);
+  Assert.equal(a1.pendingOperations, AddonManager.PENDING_NONE);
+  Assert.ok(!a1.isActive);
+  Assert.ok(a1.userDisabled);
+
+  let install = await new Promise(resolve => {
+    prepare_test({}, [
+      "onNewInstall",
+      "onDownloadStarted",
+      "onDownloadEnded"
+    ], resolve);
+
+    AddonManagerInternal.backgroundUpdateCheck();
+  });
+
+  Assert.notEqual(install.existingAddon, null);
+  Assert.equal(install.existingAddon.id, ID_UPDATE);
+
+  await new Promise(resolve => {
+    prepare_test({
+      [ID_UPDATE]: [
+        ["onInstalling", false],
+        "onInstalled",
+      ]
+    }, [
+      "onInstallStarted",
+      "onInstallEnded",
+    ], resolve);
+  });
+
+  a1 = await AddonManager.getAddonByID(ID_UPDATE);
+  Assert.notEqual(a1, null);
+  Assert.equal(a1.version, "2.0");
+  Assert.ok(!a1.isActive);
+  Assert.ok(a1.userDisabled);
+
+  prepare_test({
+    [ID_UPDATE]: [
+      ["onUninstalling", false],
+      "onUninstalled",
+    ]
+  });
+  await a1.uninstall();
+  ensure_test_completed();
+
+  await promiseRestartManager();
+  a1 = await promiseAddonByID(ID_UPDATE);
+  Assert.equal(a1, null);
+});
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
@@ -285,8 +285,9 @@ tags = webextensions
 [test_webextension_install_syntax_error.js]
 skip-if = appname == "thunderbird"
 tags = webextensions
 [test_webextension_langpack.js]
 skip-if = appname == "thunderbird"
 tags = webextensions
 [test_webextension_theme.js]
 tags = webextensions
+[test_reinstall_disabled_addon.js]