Bug 694068: Automatically download and install an available hotfix add-on. r=Unfocused
authorDave Townsend <dtownsend@oxymoronical.com>
Fri, 16 Dec 2011 12:04:50 -0800
changeset 82804 51926af07f68bdf0e543c35028bddfe9458ae675
parent 82803 0ab6831c77bce759bba7640f4f5114052d56c0b2
child 82805 e7cd9a56b5dadd17cea77d831e29ad1998ad449d
push id4122
push userdtownsend@mozilla.com
push dateFri, 16 Dec 2011 20:07:03 +0000
treeherdermozilla-inbound@e5d8d2fb987d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersUnfocused
bugs694068
milestone11.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 694068: Automatically download and install an available hotfix add-on. r=Unfocused
browser/app/profile/firefox.js
toolkit/mozapps/extensions/AddonManager.jsm
toolkit/mozapps/extensions/XPIProvider.jsm
toolkit/mozapps/extensions/test/addons/test_hotfix_1/install.rdf
toolkit/mozapps/extensions/test/addons/test_hotfix_2/install.rdf
toolkit/mozapps/extensions/test/xpcshell/data/test_hotfix_1.rdf
toolkit/mozapps/extensions/test/xpcshell/data/test_hotfix_2.rdf
toolkit/mozapps/extensions/test/xpcshell/data/test_hotfix_3.rdf
toolkit/mozapps/extensions/test/xpcshell/head_addons.js
toolkit/mozapps/extensions/test/xpcshell/test_backgroundupdate.js
toolkit/mozapps/extensions/test/xpcshell/test_hotfix.js
toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -76,16 +76,18 @@ pref("extensions.blocklist.interval", 86
 // blocking them.
 pref("extensions.blocklist.level", 2);
 pref("extensions.blocklist.url", "https://addons.mozilla.org/blocklist/3/%APP_ID%/%APP_VERSION%/%PRODUCT%/%BUILD_ID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/%PING_COUNT%/%TOTAL_PING_COUNT%/%DAYS_SINCE_LAST_PING%/");
 pref("extensions.blocklist.detailsURL", "https://www.mozilla.com/%LOCALE%/blocklist/");
 pref("extensions.blocklist.itemURL", "https://addons.mozilla.org/%LOCALE%/%APP%/blocked/%blockID%");
 
 pref("extensions.update.autoUpdateDefault", true);
 
+pref("extensions.hotfix.id", "firefox-hotfix@mozilla.org");
+
 // Disable add-ons installed into the shared user and shared system areas by
 // default. This does not include the application directory. See the SCOPE
 // constants in AddonManager.jsm for values to use here
 pref("extensions.autoDisableScopes", 15);
 
 // Dictionary download preference
 pref("browser.dictionaries.download.url", "https://addons.mozilla.org/%LOCALE%/firefox/dictionaries/");
 
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -45,16 +45,27 @@ const Ci = Components.interfaces;
 const Cr = Components.results;
 
 const PREF_BLOCKLIST_PINGCOUNTVERSION = "extensions.blocklist.pingCountVersion";
 const PREF_EM_UPDATE_ENABLED          = "extensions.update.enabled";
 const PREF_EM_LAST_APP_VERSION        = "extensions.lastAppVersion";
 const PREF_EM_LAST_PLATFORM_VERSION   = "extensions.lastPlatformVersion";
 const PREF_EM_AUTOUPDATE_DEFAULT      = "extensions.update.autoUpdateDefault";
 const PREF_EM_STRICT_COMPATIBILITY    = "extensions.strictCompatibility";
+const PREF_EM_UPDATE_URL              = "extensions.update.url";
+const PREF_APP_UPDATE_ENABLED         = "app.update.enabled";
+const PREF_APP_UPDATE_AUTO            = "app.update.auto";
+const PREF_EM_HOTFIX_ID               = "extensions.hotfix.id";
+const PREF_EM_HOTFIX_LASTVERSION      = "extensions.hotfix.lastVersion";
+const PREF_EM_HOTFIX_URL              = "extensions.hotfix.url";
+const PREF_MATCH_OS_LOCALE            = "intl.locale.matchOS";
+const PREF_SELECTED_LOCALE            = "general.useragent.locale";
+
+const UPDATE_REQUEST_VERSION          = 2;
+const CATEGORY_UPDATE_PARAMS          = "extension-update-params";
 
 // Note: This has to be kept in sync with the same constant in AddonRepository.jsm
 const STRICT_COMPATIBILITY_DEFAULT    = true;
 
 const TOOLKIT_ID                      = "toolkit@mozilla.org";
 
 const VALID_TYPES_REGEXP = /^[\w\-]+$/;
 
@@ -122,16 +133,43 @@ function callProvider(aProvider, aMethod
   }
   catch (e) {
     ERROR("Exception calling provider " + aMethod, e);
     return aDefault;
   }
 }
 
 /**
+ * Gets the currently selected locale for display.
+ * @return  the selected locale or "en-US" if none is selected
+ */
+function getLocale() {
+  try {
+    if (Services.prefs.getBoolPref(PREF_MATCH_OS_LOCALE))
+      return Services.locale.getLocaleComponentForUserAgent();
+  }
+  catch (e) { }
+
+  try {
+    let locale = Services.prefs.getComplexValue(PREF_SELECTED_LOCALE,
+                                                Ci.nsIPrefLocalizedString);
+    if (locale)
+      return locale;
+  }
+  catch (e) { }
+
+  try {
+    return Services.prefs.getCharPref(PREF_SELECTED_LOCALE);
+  }
+  catch (e) { }
+
+  return "en-US";
+}
+
+/**
  * A helper class to repeatedly call a listener with each object in an array
  * optionally checking whether the object has a method in it.
  *
  * @param  aObjects
  *         The array of objects to iterate through
  * @param  aMethod
  *         An optional method name, if not null any objects without this method
  *         will not be passed to the listener
@@ -604,70 +642,226 @@ var AddonManagerInternal = {
       if (gStrictCompatibility != oldValue)
         this.updateAddonAppDisabledStates();
 
       break;
     }
   },
 
   /**
+   * Replaces %...% strings in an addon url (update and updateInfo) with
+   * appropriate values.
+   *
+   * @param  aAddon
+   *         The AddonInternal representing the add-on
+   * @param  aUri
+   *         The uri to escape
+   * @param  aAppVersion
+   *         The optional application version to use for %APP_VERSION%
+   * @return the appropriately escaped uri.
+   */
+  escapeAddonURI: function AMI_escapeAddonURI(aAddon, aUri, aAppVersion)
+  {
+    var addonStatus = aAddon.userDisabled || aAddon.softDisabled ? "userDisabled"
+                                                                 : "userEnabled";
+
+    if (!aAddon.isCompatible)
+      addonStatus += ",incompatible";
+    if (aAddon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED)
+      addonStatus += ",blocklisted";
+    if (aAddon.blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED)
+      addonStatus += ",softblocked";
+
+    try {
+      var xpcomABI = Services.appinfo.XPCOMABI;
+    } catch (ex) {
+      xpcomABI = UNKNOWN_XPCOM_ABI;
+    }
+
+    let uri = aUri.replace(/%ITEM_ID%/g, aAddon.id);
+    uri = uri.replace(/%ITEM_VERSION%/g, aAddon.version);
+    uri = uri.replace(/%ITEM_STATUS%/g, addonStatus);
+    uri = uri.replace(/%APP_ID%/g, Services.appinfo.ID);
+    uri = uri.replace(/%APP_VERSION%/g, aAppVersion ? aAppVersion :
+                                                      Services.appinfo.version);
+    uri = uri.replace(/%REQ_VERSION%/g, UPDATE_REQUEST_VERSION);
+    uri = uri.replace(/%APP_OS%/g, Services.appinfo.OS);
+    uri = uri.replace(/%APP_ABI%/g, xpcomABI);
+    uri = uri.replace(/%APP_LOCALE%/g, getLocale());
+    uri = uri.replace(/%CURRENT_APP_VERSION%/g, Services.appinfo.version);
+
+    // Replace custom parameters (names of custom parameters must have at
+    // least 3 characters to prevent lookups for something like %D0%C8)
+    var catMan = null;
+    uri = uri.replace(/%(\w{3,})%/g, function(aMatch, aParam) {
+      if (!catMan) {
+        catMan = Cc["@mozilla.org/categorymanager;1"].
+                 getService(Ci.nsICategoryManager);
+      }
+
+      try {
+        var contractID = catMan.getCategoryEntry(CATEGORY_UPDATE_PARAMS, aParam);
+        var paramHandler = Cc[contractID].getService(Ci.nsIPropertyBag2);
+        return paramHandler.getPropertyAsAString(aParam);
+      }
+      catch(e) {
+        return aMatch;
+      }
+    });
+
+    // escape() does not properly encode + symbols in any embedded FVF strings.
+    return uri.replace(/\+/g, "%2B");
+  },
+
+  /**
    * Performs a background update check by starting an update for all add-ons
    * that can be updated.
    */
   backgroundUpdateCheck: function AMI_backgroundUpdateCheck() {
-    if (!Services.prefs.getBoolPref(PREF_EM_UPDATE_ENABLED))
+    let hotfixID = null;
+    if (Services.prefs.getPrefType(PREF_EM_HOTFIX_ID) == Ci.nsIPrefBranch.PREF_STRING)
+      hotfixID = Services.prefs.getCharPref(PREF_EM_HOTFIX_ID);
+
+    let checkHotfix = hotfixID &&
+                      Services.prefs.getBoolPref(PREF_APP_UPDATE_ENABLED) &&
+                      Services.prefs.getBoolPref(PREF_APP_UPDATE_AUTO);
+
+    let checkAddons = Services.prefs.getBoolPref(PREF_EM_UPDATE_ENABLED);
+
+    if (!checkAddons && !checkHotfix)
       return;
 
     Services.obs.notifyObservers(null, "addons-background-update-start", null);
-    let pendingUpdates = 0;
+
+    // Start this from one to ensure the whole of this function completes before
+    // we can send the complete notification. Some parts can in some cases
+    // complete synchronously before later parts have a chance to increment
+    // pendingUpdates.
+    let pendingUpdates = 1;
 
     function notifyComplete() {
       if (--pendingUpdates == 0) {
         Services.obs.notifyObservers(null,
                                      "addons-background-update-complete",
                                      null);
       }
     }
 
-    let scope = {};
-    Components.utils.import("resource://gre/modules/AddonRepository.jsm", scope);
-    Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm", scope);
-    scope.LightweightThemeManager.updateCurrentTheme();
+    if (checkAddons) {
+      let scope = {};
+      Components.utils.import("resource://gre/modules/AddonRepository.jsm", scope);
+      Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm", scope);
+      scope.LightweightThemeManager.updateCurrentTheme();
 
-    pendingUpdates++;
-    this.getAllAddons(function getAddonsCallback(aAddons) {
-      // Repopulate repository cache first, to ensure compatibility overrides
-      // are up to date before checking for addon updates.
-      var ids = [a.id for each (a in aAddons)];
-      scope.AddonRepository.repopulateCache(ids, function BUC_repopulateCacheCallback() {
-        AddonManagerInternal.updateAddonRepositoryData(function BUC_updateAddonCallback() {
+      pendingUpdates++;
+      this.getAllAddons(function getAddonsCallback(aAddons) {
+        // If there is a known hotfix then exclude it from the list of add-ons to update.
+        var ids = [a.id for each (a in aAddons) if (a.id != hotfixID)];
 
-          pendingUpdates += aAddons.length;
+        // Repopulate repository cache first, to ensure compatibility overrides
+        // are up to date before checking for addon updates.
+        scope.AddonRepository.repopulateCache(ids, function BUC_repopulateCacheCallback() {
+          AddonManagerInternal.updateAddonRepositoryData(function BUC_updateAddonCallback() {
 
-          aAddons.forEach(function BUC_forEachCallback(aAddon) {
-            // Check all add-ons for updates so that any compatibility updates will
-            // be applied
-            aAddon.findUpdates({
-              onUpdateAvailable: function BUC_onUpdateAvailable(aAddon, aInstall) {
-                // Start installing updates when the add-on can be updated and
-                // background updates should be applied.
-                if (aAddon.permissions & AddonManager.PERM_CAN_UPGRADE &&
-                    AddonManager.shouldAutoUpdate(aAddon)) {
-                  aInstall.install();
-                }
-              },
-    
-              onUpdateFinished: notifyComplete
-            }, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
+            pendingUpdates += aAddons.length;
+            aAddons.forEach(function BUC_forEachCallback(aAddon) {
+              if (aAddon.id == hotfixID) {
+                notifyComplete();
+                return;
+              }
+
+              // Check all add-ons for updates so that any compatibility updates will
+              // be applied
+              aAddon.findUpdates({
+                onUpdateAvailable: function BUC_onUpdateAvailable(aAddon, aInstall) {
+                  // Start installing updates when the add-on can be updated and
+                  // background updates should be applied.
+                  if (aAddon.permissions & AddonManager.PERM_CAN_UPGRADE &&
+                      AddonManager.shouldAutoUpdate(aAddon)) {
+                    aInstall.install();
+                  }
+                },
+
+                onUpdateFinished: notifyComplete
+              }, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
+            });
+
+            notifyComplete();
           });
-    
-          notifyComplete();
         });
       });
-    });
+    }
+
+    if (checkHotfix) {
+      var hotfixVersion = "";
+      try {
+        hotfixVersion = Services.prefs.getCharPref(PREF_EM_HOTFIX_LASTVERSION);
+      }
+      catch (e) { }
+
+      let url = null;
+      if (Services.prefs.getPrefType(PREF_EM_HOTFIX_URL) == Ci.nsIPrefBranch.PREF_STRING)
+        url = Services.prefs.getCharPref(PREF_EM_HOTFIX_URL);
+      else
+        url = Services.prefs.getCharPref(PREF_EM_UPDATE_URL);
+
+      // Build the URI from a fake add-on data.
+      url = AddonManager.escapeAddonURI({
+        id: hotfixID,
+        version: hotfixVersion,
+        userDisabled: false,
+        appDisabled: false
+      }, url);
+
+      pendingUpdates++;
+      Components.utils.import("resource://gre/modules/AddonUpdateChecker.jsm");
+      AddonUpdateChecker.checkForUpdates(hotfixID, "extension", null, url, {
+        onUpdateCheckComplete: function(aUpdates) {
+          let update = AddonUpdateChecker.getNewestCompatibleUpdate(aUpdates);
+          if (!update) {
+            notifyComplete();
+            return;
+          }
+
+          // If the available version isn't newer than the last installed
+          // version then ignore it.
+          if (Services.vc.compare(hotfixVersion, update.version) >= 0) {
+            notifyComplete();
+            return;
+          }
+
+          LOG("Downloading hotfix version " + update.version);
+          AddonManager.getInstallForURL(update.updateURL, function(aInstall) {
+            aInstall.addListener({
+              onInstallEnded: function(aInstall) {
+                // Remember the last successfully installed version.
+                Services.prefs.setCharPref(PREF_EM_HOTFIX_LASTVERSION,
+                                           aInstall.version);
+              },
+
+              onInstallCancelled: function(aInstall) {
+                // Revert to the previous version if the installation was
+                // cancelled.
+                Services.prefs.setCharPref(PREF_EM_HOTFIX_LASTVERSION,
+                                           hotfixVersion);
+              }
+            });
+
+            aInstall.install();
+
+            notifyComplete();
+          }, "application/x-xpinstall", update.updateHash, null,
+             null, update.version);
+        },
+
+        onUpdateCheckError: notifyComplete
+      });
+    }
+
+    notifyComplete();
   },
 
   /**
    * Adds a add-on to the list of detected changes for this startup. If
    * addStartupChange is called multiple times for the same add-on in the same
    * startup then only the most recent change will be remembered.
    *
    * @param  aType
@@ -1592,14 +1786,18 @@ var AddonManager = {
       return true;
     if (aAddon.applyBackgroundUpdates == AddonManager.AUTOUPDATE_DISABLE)
       return false;
     return this.autoUpdateDefault;
   },
 
   get strictCompatibility() {
     return AddonManagerInternal.strictCompatibility;
+  },
+
+  escapeAddonURI: function AM_escapeAddonURI(aAddon, aUri, aAppVersion) {
+    return AddonManagerInternal.escapeAddonURI(aAddon, aUri, aAppVersion);
   }
 };
 
 Object.freeze(AddonManagerInternal);
 Object.freeze(AddonManagerPrivate);
 Object.freeze(AddonManager);
--- a/toolkit/mozapps/extensions/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/XPIProvider.jsm
@@ -108,32 +108,29 @@ const KEY_TEMPDIR                     = 
 const KEY_APP_DISTRIBUTION            = "XREAppDist";
 
 const KEY_APP_PROFILE                 = "app-profile";
 const KEY_APP_GLOBAL                  = "app-global";
 const KEY_APP_SYSTEM_LOCAL            = "app-system-local";
 const KEY_APP_SYSTEM_SHARE            = "app-system-share";
 const KEY_APP_SYSTEM_USER             = "app-system-user";
 
-const CATEGORY_UPDATE_PARAMS          = "extension-update-params";
-
 const UNKNOWN_XPCOM_ABI               = "unknownABI";
 const XPI_PERMISSION                  = "install";
 
 const PREFIX_ITEM_URI                 = "urn:mozilla:item:";
 const RDFURI_ITEM_ROOT                = "urn:mozilla:item:root"
 const RDFURI_INSTALL_MANIFEST_ROOT    = "urn:mozilla:install-manifest";
 const PREFIX_NS_EM                    = "http://www.mozilla.org/2004/em-rdf#";
 
 const TOOLKIT_ID                      = "toolkit@mozilla.org";
 
 const BRANCH_REGEXP                   = /^([^\.]+\.[0-9]+[a-z]*).*/gi;
 
 const DB_SCHEMA                       = 12;
-const REQ_VERSION                     = 2;
 
 #ifdef MOZ_COMPATIBILITY_NIGHTLY
 const PREF_EM_CHECK_COMPATIBILITY = PREF_EM_CHECK_COMPATIBILITY_BASE +
                                     ".nightly";
 #else
 const PREF_EM_CHECK_COMPATIBILITY = PREF_EM_CHECK_COMPATIBILITY_BASE + "." +
                                     Services.appinfo.version.replace(BRANCH_REGEXP, "$1");
 #endif
@@ -1127,43 +1124,17 @@ function verifyZipSigning(aZip, aPrincip
  *         An optional number representing the type of update, only applicable
  *         when creating a url for retrieving an update manifest
  * @param  aAppVersion
  *         The optional application version to use for %APP_VERSION%
  * @return the appropriately escaped uri.
  */
 function escapeAddonURI(aAddon, aUri, aUpdateType, aAppVersion)
 {
-  var addonStatus = aAddon.userDisabled || aAddon.softDisabled ? "userDisabled"
-                                                               : "userEnabled";
-
-  if (!aAddon.isCompatible)
-    addonStatus += ",incompatible";
-  if (aAddon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED)
-    addonStatus += ",blocklisted";
-  if (aAddon.blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED)
-    addonStatus += ",softblocked";
-
-  try {
-    var xpcomABI = Services.appinfo.XPCOMABI;
-  } catch (ex) {
-    xpcomABI = UNKNOWN_XPCOM_ABI;
-  }
-
-  let uri = aUri.replace(/%ITEM_ID%/g, aAddon.id);
-  uri = uri.replace(/%ITEM_VERSION%/g, aAddon.version);
-  uri = uri.replace(/%ITEM_STATUS%/g, addonStatus);
-  uri = uri.replace(/%APP_ID%/g, Services.appinfo.ID);
-  uri = uri.replace(/%APP_VERSION%/g, aAppVersion ? aAppVersion :
-                                                    Services.appinfo.version);
-  uri = uri.replace(/%REQ_VERSION%/g, REQ_VERSION);
-  uri = uri.replace(/%APP_OS%/g, Services.appinfo.OS);
-  uri = uri.replace(/%APP_ABI%/g, xpcomABI);
-  uri = uri.replace(/%APP_LOCALE%/g, getLocale());
-  uri = uri.replace(/%CURRENT_APP_VERSION%/g, Services.appinfo.version);
+  let uri = AddonManager.escapeAddonURI(aAddon, aUri, aAppVersion);
 
   // If there is an updateType then replace the UPDATE_TYPE string
   if (aUpdateType)
     uri = uri.replace(/%UPDATE_TYPE%/g, aUpdateType);
 
   // If this add-on has compatibility information for either the current
   // application or toolkit then replace the ITEM_MAXAPPVERSION with the
   // maxVersion
@@ -1176,37 +1147,17 @@ function escapeAddonURI(aAddon, aUri, aU
 
   let compatMode = "normal";
   if (!XPIProvider.checkCompatibility)
     compatMode = "ignore";
   else if (AddonManager.strictCompatibility)
     compatMode = "strict";
   uri = uri.replace(/%COMPATIBILITY_MODE%/g, compatMode);
 
-  // Replace custom parameters (names of custom parameters must have at
-  // least 3 characters to prevent lookups for something like %D0%C8)
-  var catMan = null;
-  uri = uri.replace(/%(\w{3,})%/g, function(aMatch, aParam) {
-    if (!catMan) {
-      catMan = Cc["@mozilla.org/categorymanager;1"].
-               getService(Ci.nsICategoryManager);
-    }
-
-    try {
-      var contractID = catMan.getCategoryEntry(CATEGORY_UPDATE_PARAMS, aParam);
-      var paramHandler = Cc[contractID].getService(Ci.nsIPropertyBag2);
-      return paramHandler.getPropertyAsAString(aParam);
-    }
-    catch(e) {
-      return aMatch;
-    }
-  });
-
-  // escape() does not properly encode + symbols in any embedded FVF strings.
-  return uri.replace(/\+/g, "%2B");
+  return uri;
 }
 
 /**
  * Copies properties from one object to another. If no target object is passed
  * a new object will be created and returned.
  *
  * @param  aObject
  *         An object to copy from
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_hotfix_1/install.rdf
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+  <Description about="urn:mozilla:install-manifest">
+    <em:id>hotfix@tests.mozilla.org</em:id>
+    <em:version>1.0</em:version>
+
+    <!-- Front End MetaData -->
+    <em:name>Test 1</em:name>
+    <em:description>Test Description</em:description>
+
+    <em:targetApplication>
+      <Description>
+        <em:id>xpcshell@tests.mozilla.org</em:id>
+        <em:minVersion>1</em:minVersion>
+        <em:maxVersion>1</em:maxVersion>
+      </Description>
+    </em:targetApplication>
+
+  </Description>
+</RDF>
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_hotfix_2/install.rdf
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+  <Description about="urn:mozilla:install-manifest">
+    <em:id>hotfix@tests.mozilla.org</em:id>
+    <em:version>2.0</em:version>
+
+    <!-- Front End MetaData -->
+    <em:name>Test 1</em:name>
+    <em:description>Test Description</em:description>
+
+    <em:targetApplication>
+      <Description>
+        <em:id>xpcshell@tests.mozilla.org</em:id>
+        <em:minVersion>1</em:minVersion>
+        <em:maxVersion>1</em:maxVersion>
+      </Description>
+    </em:targetApplication>
+
+  </Description>
+</RDF>
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_hotfix_1.rdf
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8" ?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+  <Description about="urn:mozilla:extension:hotfix@tests.mozilla.org">
+    <em:updates>
+      <Seq>
+        <li>
+          <Description>
+            <em:version>1.0</em:version>
+            <em:targetApplication>
+              <Description>
+                <em:id>xpcshell@tests.mozilla.org</em:id>
+                <em:minVersion>1</em:minVersion>
+                <em:maxVersion>1</em:maxVersion>
+                <em:updateLink>http://localhost:4444/addons/test_hotfix_1.xpi</em:updateLink>
+              </Description>
+            </em:targetApplication>
+          </Description>
+        </li>
+      </Seq>
+    </em:updates>
+  </Description>
+
+</RDF>
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_hotfix_2.rdf
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8" ?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+  <Description about="urn:mozilla:extension:hotfix@tests.mozilla.org">
+    <em:updates>
+      <Seq>
+        <li>
+          <Description>
+            <em:version>2.0</em:version>
+            <em:targetApplication>
+              <Description>
+                <em:id>xpcshell@tests.mozilla.org</em:id>
+                <em:minVersion>1</em:minVersion>
+                <em:maxVersion>1</em:maxVersion>
+                <em:updateLink>http://localhost:4444/addons/test_hotfix_2.xpi</em:updateLink>
+              </Description>
+            </em:targetApplication>
+          </Description>
+        </li>
+      </Seq>
+    </em:updates>
+  </Description>
+
+</RDF>
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_hotfix_3.rdf
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8" ?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+  <Description about="urn:mozilla:extension:hotfix@tests.mozilla.org">
+    <em:updates>
+      <Seq>
+        <li>
+          <Description>
+            <em:version>3.0</em:version>
+            <em:targetApplication>
+              <Description>
+                <em:id>xpcshell@tests.mozilla.org</em:id>
+                <em:minVersion>2</em:minVersion>
+                <em:maxVersion>2</em:maxVersion>
+                <em:updateLink>http://localhost:4444/addons/test_hotfix_3.xpi</em:updateLink>
+              </Description>
+            </em:targetApplication>
+          </Description>
+        </li>
+      </Seq>
+    </em:updates>
+  </Description>
+
+</RDF>
--- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
@@ -1130,16 +1130,19 @@ Services.prefs.setCharPref("extensions.u
 Services.prefs.setCharPref("extensions.blocklist.url", "http://127.0.0.1/blocklistURL");
 
 // By default ignore bundled add-ons
 Services.prefs.setBoolPref("extensions.installDistroAddons", false);
 
 // By default use strict compatibility
 Services.prefs.setBoolPref("extensions.strictCompatibility", true);
 
+// By default don't check for hotfixes
+Services.prefs.setCharPref("extensions.hotfix.id", "");
+
 // By default, set min compatible versions to 0
 Services.prefs.setCharPref(PREF_EM_MIN_COMPAT_APP_VERSION, "0");
 Services.prefs.setCharPref(PREF_EM_MIN_COMPAT_PLATFORM_VERSION, "0");
 
 // Register a temporary directory for the tests.
 const gTmpD = gProfD.clone();
 gTmpD.append("temp");
 gTmpD.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
--- a/toolkit/mozapps/extensions/test/xpcshell/test_backgroundupdate.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_backgroundupdate.js
@@ -71,16 +71,20 @@ function run_test_2() {
       minVersion: "1",
       maxVersion: "1"
     }],
     name: "Test Addon 2",
   }, profileDir);
 
   restartManager();
 
+  // Do hotfix checks
+  Services.prefs.setCharPref("extensions.hotfix.id", "hotfix@tests.mozilla.org");
+  Services.prefs.setCharPref("extensions.hotfix.url", "http://localhost:4444/missing.rdf");
+
   let installCount = 0;
   let completeCount = 0;
   let sawCompleteNotification = false;
 
   Services.obs.addObserver(function() {
     Services.obs.removeObserver(arguments.callee, "addons-background-update-complete");
 
     do_check_eq(installCount, 2);
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_hotfix.js
@@ -0,0 +1,336 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that hotfix installation works
+
+// The test extension uses an insecure update url.
+Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false);
+
+do_load_httpd_js();
+var testserver;
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function run_test() {
+  createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+  // Create and configure the HTTP server.
+  testserver = new nsHttpServer();
+  testserver.registerDirectory("/data/", do_get_file("data"));
+  testserver.registerDirectory("/addons/", do_get_file("addons"));
+  testserver.start(4444);
+
+  startupManager();
+
+  do_test_pending();
+  run_test_1();
+}
+
+function end_test() {
+  testserver.stop(do_test_finished);
+}
+
+// Test that background updates find and install any available hotfix
+function run_test_1() {
+  Services.prefs.setCharPref("extensions.hotfix.id", "hotfix@tests.mozilla.org");
+  Services.prefs.setCharPref("extensions.update.url", "http://localhost:4444/data/test_hotfix_1.rdf");
+
+  prepare_test({
+    "hotfix@tests.mozilla.org": [
+      "onInstalling"
+    ]
+  }, [
+    "onNewInstall",
+    "onDownloadStarted",
+    "onDownloadEnded",
+    "onInstallStarted",
+    "onInstallEnded",
+  ], check_test_1);
+
+  // Fake a timer event
+  gInternalManager.notify(null);
+}
+
+function check_test_1() {
+  restartManager();
+
+  AddonManager.getAddonByID("hotfix@tests.mozilla.org", function(aAddon) {
+    do_check_neq(aAddon, null);
+    do_check_eq(aAddon.version, "1.0");
+
+    aAddon.uninstall();
+    restartManager();
+
+    run_test_2();
+  });
+}
+
+// Don't install an already used hotfix
+function run_test_2() {
+  AddonManager.addInstallListener({
+    onNewInstall: function() {
+      do_throw("Should not have seen a new install created");
+    }
+  });
+
+  function observer() {
+    Services.obs.removeObserver(arguments.callee, "addons-background-update-complete");
+
+    restartManager();
+    run_test_3();
+  }
+
+  Services.obs.addObserver(observer, "addons-background-update-complete", false);
+
+  // Fake a timer event
+  gInternalManager.notify(null);
+}
+
+// Install a newer hotfix
+function run_test_3() {
+  Services.prefs.setCharPref("extensions.hotfix.url", "http://localhost:4444/data/test_hotfix_2.rdf");
+
+  prepare_test({
+    "hotfix@tests.mozilla.org": [
+      "onInstalling"
+    ]
+  }, [
+    "onNewInstall",
+    "onDownloadStarted",
+    "onDownloadEnded",
+    "onInstallStarted",
+    "onInstallEnded",
+  ], check_test_3);
+
+  // Fake a timer event
+  gInternalManager.notify(null);
+}
+
+function check_test_3() {
+  restartManager();
+
+  AddonManager.getAddonByID("hotfix@tests.mozilla.org", function(aAddon) {
+    do_check_neq(aAddon, null);
+    do_check_eq(aAddon.version, "2.0");
+
+    aAddon.uninstall();
+    restartManager();
+
+    run_test_4();
+  });
+}
+
+// Don't install an incompatible hotfix
+function run_test_4() {
+  Services.prefs.setCharPref("extensions.hotfix.url", "http://localhost:4444/data/test_hotfix_3.rdf");
+
+  AddonManager.addInstallListener({
+    onNewInstall: function() {
+      do_throw("Should not have seen a new install created");
+    }
+  });
+
+  function observer() {
+    Services.obs.removeObserver(arguments.callee, "addons-background-update-complete");
+
+    restartManager();
+    run_test_5();
+  }
+
+  Services.obs.addObserver(observer, "addons-background-update-complete", false);
+
+  // Fake a timer event
+  gInternalManager.notify(null);
+}
+
+// Don't install an older hotfix
+function run_test_5() {
+  Services.prefs.setCharPref("extensions.hotfix.url", "http://localhost:4444/data/test_hotfix_1.rdf");
+
+  AddonManager.addInstallListener({
+    onNewInstall: function() {
+      do_throw("Should not have seen a new install created");
+    }
+  });
+
+  function observer() {
+    Services.obs.removeObserver(arguments.callee, "addons-background-update-complete");
+
+    restartManager();
+    run_test_6();
+  }
+
+  Services.obs.addObserver(observer, "addons-background-update-complete", false);
+
+  // Fake a timer event
+  gInternalManager.notify(null);
+}
+
+// Don't re-download an already pending install
+function run_test_6() {
+  Services.prefs.setCharPref("extensions.hotfix.lastVersion", "0");
+  Services.prefs.setCharPref("extensions.hotfix.url", "http://localhost:4444/data/test_hotfix_1.rdf");
+
+  prepare_test({
+    "hotfix@tests.mozilla.org": [
+      "onInstalling"
+    ]
+  }, [
+    "onNewInstall",
+    "onDownloadStarted",
+    "onDownloadEnded",
+    "onInstallStarted",
+    "onInstallEnded",
+  ], check_test_6);
+
+  // Fake a timer event
+  gInternalManager.notify(null);
+}
+
+function check_test_6() {
+  AddonManager.addInstallListener({
+    onNewInstall: function() {
+      do_throw("Should not have seen a new install created");
+    }
+  });
+
+  function observer() {
+    Services.obs.removeObserver(arguments.callee, "addons-background-update-complete");
+    restartManager();
+
+    AddonManager.getAddonByID("hotfix@tests.mozilla.org", function(aAddon) {
+      aAddon.uninstall();
+
+      restartManager();
+      run_test_7();
+    });
+  }
+
+  Services.obs.addObserver(observer, "addons-background-update-complete", false);
+
+  // Fake a timer event
+  gInternalManager.notify(null);
+}
+
+// Start downloading again if something cancels the install
+function run_test_7() {
+  Services.prefs.setCharPref("extensions.hotfix.lastVersion", "0");
+
+  prepare_test({
+    "hotfix@tests.mozilla.org": [
+      "onInstalling"
+    ]
+  }, [
+    "onNewInstall",
+    "onDownloadStarted",
+    "onDownloadEnded",
+    "onInstallStarted",
+    "onInstallEnded",
+  ], check_test_7);
+
+  // Fake a timer event
+  gInternalManager.notify(null);
+}
+
+function check_test_7(aInstall) {
+  prepare_test({
+    "hotfix@tests.mozilla.org": [
+      "onOperationCancelled"
+    ]
+  }, [
+    "onInstallCancelled",
+  ]);
+
+  aInstall.cancel();
+
+  prepare_test({
+    "hotfix@tests.mozilla.org": [
+      "onInstalling"
+    ]
+  }, [
+    "onNewInstall",
+    "onDownloadStarted",
+    "onDownloadEnded",
+    "onInstallStarted",
+    "onInstallEnded",
+  ], finish_test_7);
+
+  // Fake a timer event
+  gInternalManager.notify(null);
+}
+
+function finish_test_7() {
+  restartManager();
+
+  AddonManager.getAddonByID("hotfix@tests.mozilla.org", function(aAddon) {
+    do_check_neq(aAddon, null);
+    do_check_eq(aAddon.version, "1.0");
+
+    aAddon.uninstall();
+    restartManager();
+
+    run_test_8();
+  });
+}
+
+// Cancel a pending install when a newer version is already available
+function run_test_8() {
+  Services.prefs.setCharPref("extensions.hotfix.lastVersion", "0");
+  Services.prefs.setCharPref("extensions.hotfix.url", "http://localhost:4444/data/test_hotfix_1.rdf");
+
+  prepare_test({
+    "hotfix@tests.mozilla.org": [
+      "onInstalling"
+    ]
+  }, [
+    "onNewInstall",
+    "onDownloadStarted",
+    "onDownloadEnded",
+    "onInstallStarted",
+    "onInstallEnded",
+  ], check_test_8);
+
+  // Fake a timer event
+  gInternalManager.notify(null);
+}
+
+function check_test_8() {
+  Services.prefs.setCharPref("extensions.hotfix.url", "http://localhost:4444/data/test_hotfix_2.rdf");
+
+  prepare_test({
+    "hotfix@tests.mozilla.org": [
+      "onOperationCancelled",
+      "onInstalling"
+    ]
+  }, [
+    "onNewInstall",
+    "onDownloadStarted",
+    "onDownloadEnded",
+    "onInstallStarted",
+    "onInstallCancelled",
+    "onInstallEnded",
+  ], finish_test_8);
+
+  // Fake a timer event
+  gInternalManager.notify(null);
+}
+
+function finish_test_8() {
+  AddonManager.getAllInstalls(function(aInstalls) {
+    do_check_eq(aInstalls.length, 1);
+    do_check_eq(aInstalls[0].version, "2.0");
+
+    restartManager();
+
+    AddonManager.getAddonByID("hotfix@tests.mozilla.org", function(aAddon) {
+      do_check_neq(aAddon, null);
+      do_check_eq(aAddon.version, "2.0");
+
+      aAddon.uninstall();
+      restartManager();
+
+      end_test();
+    });
+  });
+}
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
@@ -157,16 +157,17 @@ skip-if = os == "android"
 [test_gfxBlacklist_Equal_OK.js]
 [test_gfxBlacklist_GTE_DriverOld.js]
 [test_gfxBlacklist_GTE_OK.js]
 [test_gfxBlacklist_OK.js]
 [test_gfxBlacklist_OS.js]
 [test_gfxBlacklist_Vendor.js]
 [test_gfxBlacklist_prefs.js]
 [test_hasbinarycomponents.js]
+[test_hotfix.js]
 [test_install.js]
 # Bug 676992: test consistently hangs on Android
 skip-if = os == "android"
 [test_install_strictcompat.js]
 # Bug 676992: test consistently hangs on Android
 skip-if = os == "android"
 [test_locale.js]
 [test_locked.js]