Bug 694068: Automatically download and install an available hotfix add-on. r=Unfocused, a=LegNeato
authorDave Townsend <dtownsend@oxymoronical.com>
Tue, 10 Jan 2012 15:48:06 -0800
changeset 81580 44c06c70065c79c4df07bdbeb14243a8f4a603e5
parent 81579 53d904a6d14d961889a01f89b5c0a90e884088e9
child 81581 749ae51cf3822043b64ea526a4da4e9201cd33f4
push id483
push userdtownsend@mozilla.com
push dateTue, 10 Jan 2012 23:57:45 +0000
treeherdermozilla-beta@e634ee5d7aca [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersUnfocused, LegNeato
bugs694068
milestone10.0
Bug 694068: Automatically download and install an available hotfix add-on. r=Unfocused, a=LegNeato
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
@@ -1557,14 +1751,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                       = 11;
-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
@@ -1115,43 +1112,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
@@ -1164,37 +1135,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
@@ -159,16 +159,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]