v20140527.01/resource/update.jsm
author Gregory Szorc <gps@mozilla.com>
Mon, 14 Jul 2014 17:58:04 -0700
changeset 29 d29a3d24405cb8064383bdd4b0b8bfacf15fba5f
parent 26 0e0218c49ddcf48d8377d10f117c4728ed1ac9e5
child 30 b54f363cda591a9b24086370a5a865dd3a99e78a
permissions -rw-r--r--
Bug 928173 - Bump version of update hotfix

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/**
 * This file provides logic to upgrade an old Firefox client to a modern
 * Firefox.
 *
 * Please note this code is expected to work in Firefox 10 through 30. If you
 * see usage of legacy patterns, that is why.
 */

"use strict";

this.EXPORTED_SYMBOLS = [
  "log",
  "manager",
];

const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;

Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
                                  "resource://gre/modules/AddonManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ctypes",
                                  "resource://gre/modules/ctypes.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                  "resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                  "resource://gre/modules/NetUtil.jsm");

const MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;

let Logging;

// The logging module has been moved around over the years.
try {
  // It is currently at Log.jsm.
  let ns = {};
  Cu.import("resource://gre/modules/Log.jsm", ns);
  Logging = ns.Log;
} catch (ex) {
  try {
    // It previously existed in services-common.
    let ns = {};
    Cu.import("resource://services-common/log4moz.js", ns);
    Logging = ns.Log4Moz;
  } catch (ex) {
    // Before services-common, there was services-sync.
    let ns = {};
    Cu.import("resource://services-sync/log4moz.js", ns);
    Logging = ns.Log4Moz;
  }
}

/**
 * A logging appender that buffers messages in memory.
 *
 * We use this so messages aren't lost before the hotfix is started and
 * the log file is configured.
 */
function InMemoryAppender(formatter) {
  Logging.Appender.call(this, formatter);
  this.messages = [];
}
InMemoryAppender.prototype = {
  __proto__: Logging.Appender.prototype,

  append: function (message) {
    if (message) {
      this.messages.push(message);
    }
  }
};

/**
 * Old file appender from legacy log4moz.js.
 *
 * The new one doesn't suit our needs because it doesn't do appends
 * properly. And, the mismatch between synchronous and asynchronous behavior
 * results in lots of extra complexity.
 *
 * This appender, while doing main thread I/O, makes our life easier.
 * Most clients shouldn't be dropping lots of log events, so the main
 * thread I/O should be in check.
 */
function MainThreadFileAppender(file, formatter) {
  Logging.Appender.call(this, formatter);

  this._file = file;
  this._stream = null;

  this._populateStream();
}
MainThreadFileAppender.prototype = {
  __proto__: Logging.Appender.prototype,

  _populateStream: function () {
    // First create a raw stream. We can bail out early if that fails.
    let os;
    try {
      os = FileUtils.openFileOutputStream(this._file,
                                          FileUtils.MODE_WRONLY |
                                          FileUtils.MODE_CREATE |
                                          FileUtils.MODE_APPEND);
    } catch (e) {
      return null;
    }

    // Wrap the raw stream in an nsIConverterOutputStream. We can reuse
    // the instance if we already have one.
    this._stream = Cc["@mozilla.org/intl/converter-output-stream;1"]
                     .createInstance(Ci.nsIConverterOutputStream);
    this._stream.init(os, "UTF-8", 4096,
                      this._stream.DEFAULT_REPLACEMENT_CHARACTER);
  },

  doAppend: function doAppend(message) {
    if (!message || !this._stream) {
      return;
    }

    // Different versions of Log.jsm insert the "\n" at different places.
    if (message.charAt(message.length - 1) != "\n") {
      message += "\n";
    }

    // If this fails, there's nothing much we can do. Sadness.
    try {
      this._stream.writeString(message);
    } catch (e) { }
  },

  close: function () {
    if (this._stream) {
      this._stream.close();
      this._stream = null;
    }
  },

  flush: function () {
    if (this._stream) {
      this._stream.flush();
    }
  },
};

/**
 * Log formatter that writes newline-delimited JSON arrays.
 */
function JSONFormatter() {
  Logging.Formatter.call(this);
}
JSONFormatter.prototype = {
  format: function (message) {
    return JSON.stringify([
      message.time,
      message.level,
      message.message,
    ]);
  },
};

this.log = Logging.repository.getLogger("hotfix.autoupdate");

// Enable to make local testing easier.
//this.log.addAppender(new Logging.ConsoleAppender());

let memoryAppender = new InMemoryAppender();
this.log.addAppender(memoryAppender);

let gFileAppender;

/**
 * Coordinates the background updating of old Firefox clients.
 *
 * From a high level, this type is responsible for upgrading old Firefox
 * clients to a new, modern version. It does this by testing for upgrade
 * applicability, downloading an installer, and executing that installer.
 * There are UI elements that may be displayed to remind users to upgrade.
 * There is also built-in reporting of results to Mozilla (i.e. a hotfix
 * health report).
 *
 * This type has the following responsibilities:
 *
 * 1) Global and add-on state management. This includes configuration of
 *    the instance, filesystem and state management, and logging management.
 *
 * 2) Event coordination and orchestration.
 *
 * 3) Downloading an installer to local disk. This includes support for
 *    resuming interrupted downloads and verifying the downloaded file
 *    matches expectations.
 *
 * 4) Launching an installer.
 *
 * 5) Notifying/reminding the user about a pending upgrade.
 *
 * 6) Uploading analytics to Mozilla for diagnosis.
 *
 * 7) Generic utilities and support code.
 *
 * The code layout has attempted to isolate each responsibility so that
 * code is grouped together.
 *
 * Workflow
 * ========
 *
 * Upon installation, start() is called and we check to see if this add-on
 * is applicable. If we are not applicable, the add-on is uninstalled.
 * Not applicable cases include where automatic updates are disabled.
 *
 * If the add-on is applicable (meaning we are running on an old Firefox
 * version and we qualify for an upgrade), we gather state and start the
 * work to run the installer. See tryToDownloadAndUpdate().
 *
 * If the configured installer is not downloaded, we download it in the
 * background. See _ensureInstallerDownloaded(). We support resuming
 * partial downloads.
 *
 * When the installer is fully downloaded and ready to execute, we call
 * _onInstallerReady(). On the first call, we attempt to execute the
 * installer. If that goes well, the add-on is uninstalled and our work
 * is done.
 *
 * We differentiate failed installations by their likelihood of recurring.
 * We assume some failures such as permission declined or permission not
 * allowed are transient (the next time the user tries they may click a
 * different button or may have an administrator available to type in a
 * password, etc). Non-transient errors result in immediate add-on uninstall.
 *
 * If transient install errors are detected, the add-on will display a
 * pop-up notification of a pending Firefox upgrade on Firefox start-up.
 * The pop-up will always be displayed on about:home (opening it if
 * necessary). The notification will be displayed at most once per calendar
 * day. These pop-ups will appear indefinitely until Firefox is upgraded.
 * Note that these pop-ups will not be displayed if app updates are disabled
 * (due to failing the applicability check).
 */
function UpgradeManager() {
  this._s = null;
  this._log = log;
  this._prefs = Services.prefs.getBranch(this.PREFS_BRANCH);
  this._locale = null;
  this._stateDir = null;
  this._logFile = null;

  // nsIFile of JSON file holding our persisted state.
  this._stateFile = null;

  // URL of installer to download.
  this._installerURL = null;

  // Byte size of installer.
  this._installerExpectedSize = null;

  // Hex SHA-512 of installer.
  this._installerExpectedHash = null;

  // nsIFile of installer to execute.
  this._installerFile = null;

  // nsIFile of temporary file to download installer to.
  this._installerTempFile = null;

  // nsIFile of where to install Firefox.
  this._targetDir = null;

  // nsIFile of the .exe launcher that launches the installer.
  this._launcherFile = null;

  // nsIFile of the log file created by the launcher.
  this._launcherLogFile = null;

  // nsIFile of the log file created by the installer.
  this._installerLogFile = null;

  // nsIFile of the .ini file used by the installer.
  this._installerIniFile = null;

  // Whether this instance has been started.
  this._started = false;

  // Whether this instance has been destoryed.
  this._destroyed = false;

  // nsITimer managing retry attempts.
  this._retryTimer = null;

  // Callbacks for in-progress state save requests.
  this._pendingStateQueue = [];
}
UpgradeManager.prototype = {
  // Where to store our preferences.
  PREFS_BRANCH: "hotfix.v20140527.01.",

  // URL of tab that we should open notification on.
  NEW_TAB_URL: "about:home",

  // Where to find JSON describing installers.
  INSTALLERS_URI: "chrome://firefox-hotfix/content/installers.json",

  // Where to find the installer launcher executable.
  LAUNCHER_URI: "chrome://firefox-hotfix/content/InstallerLauncher.exe",

  // Upgrade versions of Firefox older than this. Versions equal to this
  // will not be upgraded.
  MAX_UPGRADE_VERSION: "29",

  // Version to mark as upgraded.
  UPGRADED_VERSION: "30",

  // How often to retry failed downloads.
  // Value was chosen arbitrarily.
  DOWNLOAD_RETRY_MILLISECONDS: 60 * 60 * 1000, // 1 hour.

  // How often to re-attempt failed installation.
  // The value specified may not result in a notification: it's just how
  // often to run our main routine that *may* take action.
  INSTALL_RETRY_MILLISECONDS: 24 * 60 * 60 * 1000, // 1 day.

  // Maximum number of times a reported successful download should fail
  // before we give up and never try again.
  MAX_DOWNLOAD_FAILURES: 10,

  // Where to upload anonymous execution results to.
  UPLOAD_URL: "https://hotfix.telemetry.mozilla.org/submit/hotfix/",

  // Value of startup.homepage_override_url to set on successful upgrade.
  UPGRADE_HOMEPAGE_OVERRIDE: "https://www.mozilla.org/%LOCALE%/firefox/%VERSION%/whatsnew/?oldversion=%OLD_VERSION%",

  // URL to open upon a failed install.
  FAILED_INSTALL_URL: "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/failed-update",

  /**
   * Start execution.
   *
   * This performs start-up tasks for the extension.
   *
   * @param isInstall
   *        (bool) Whether the add-on is being installed as opposed to
   *        merely starting up like normal.
   */
  start: function (isInstall) {
    if (this._destroyed) {
      this._log.warn("Cannot start an instance after it's been destroyed.");
      return;
    }

    // We should always have a profile if this is called since bootstrap()
    // is called after profile init.
    this._stateDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
    this._stateDir.append("hotfix-update");

    try {
      this._stateDir.create(Ci.nsIFile.DIRECTORY_TYPE, 493 /* 0755 */);
      this._log.warn("Created state directory.");
    } catch (ex) {
      if (ex.result != Cr.NS_ERROR_FILE_ALREADY_EXISTS) {
        this._log.error("Error creating download dir: " + ex);
        this._selfUninstall("PROFILE_PERMISSIONS_ERROR");
        return;
      }
    }

    if (!gFileAppender) {
      this._logFile = this._stateDir.clone();
      this._logFile.append("update.log");

      let formatter = new JSONFormatter();
      let appender = new MainThreadFileAppender(this._logFile, formatter);
      log.addAppender(appender);
      gFileAppender = appender;

      // Now append buffered messages produced before we loaded into the
      // file appender.
      for each (let message in memoryAppender.messages) {
        appender.append(message);
      }

      // And remove the memory appender since it no longer has a purpose.
      log.removeAppender(memoryAppender);
      memoryAppender = null;
    }

    // Wait for browser to load before continuing.
    if (!isInstall) {
      this._log.warn("Waiting for session restore to perform startup.");
      let us = this;
      Services.obs.addObserver(function obs() {
        Services.obs.removeObserver(obs, "sessionstore-windows-restored");

        us._startup(isInstall);
      }, "sessionstore-windows-restored", false);
    } else {
      this._startup(isInstall);
    }
  },

  /**
   * Perform the low-level work of initializing.
   *
   * The instance is not usable until this is called. This should only
   * be called once the browser has been fully initialized.
   */
  _startup: function (isInstall) {
    this._log.warn("Performing start-up tasks.");

    this._stateFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
    this._stateFile.append("hotfix.v20140527.01.json");

    this._loadState(isInstall, function onStateLoaded(state) {
      this._s = state;
      this._onStateLoaded();
    }.bind(this));
  },

  _onStateLoaded: function () {
    // If Firefox closes while an install is running, our state is undefined.
    // Try to ascertain what happened.
    if (this._s._installInProgress) {
      this._log.warn("State says install in progress. Reconciling.");

      if (this._isUpgraded()) {
        this._log.warn("Upgrade appears to have completed successfully.");
        this._s._installInProgress = false;
        this._s.installSuccesses++;
        this._s.uninstallReason = "SUCCESSFUL_UPGRADE";
      } else {
        this._log.warn("Upgrade appears to not have worked. Will attempt again.");
        this._s._installInProgress = false;
      }

      this._saveState();
    }

    // We need to initialize these early for forensic upload to work right.
    // XCurProcD isn't reliable. See bug 1014194 comments 50 and on.
    this._targetDir = Services.dirsvc.get("XREExeF", Ci.nsIFile).parent;
    this._installerLogFile = this._targetDir.clone();
    this._installerLogFile.append("install.log");
    this._installerIniFile = this._stateDir.clone();
    this._installerIniFile.append("install.ini");

    this._ensureLocaleLoaded(this._onLocaleLoaded.bind(this));
  },

  _onLocaleLoaded: function () {
    // We have a few major branches we could take here. In chronological order:
    //
    // 1) The hotfix was just installed. We have no state. We need to determine
    //    whether the hotfix is applicable and continue running or uninstall
    //    as appropriate.
    //
    // 2) The hotfix is starting after it was installed on a prior browser
    //    session. We should resume where we left off.
    //
    // 3) The hotfix is starting after a completed upgrade. In this state, we
    //    want to report on the results of the upgrade using FHR.

    if (!this._isHotfixApplicable()) {
      // It looks like this is a first-time use on an incompatible client.
      // Uninstall immediately.
      if (!this._s._everCompatible) {
        this._selfUninstall("NOT_APPLICABLE");
        return;
      }

      // If we get here, the hotfix was marked as applicable before but is no
      // longer so. The hotfix could have upgraded us to a modern client. Or,
      // the client changed a setting or installed a different version
      // manually that makes it no longer compatible.

      if (!this._s.installSuccesses) {
        this._selfUninstall("NO_LONGER_APPLICABLE");
        return;
      }

      this._log.warn("Starting after successful upgrade.");
      this._onPostUpgrade();
      return;
    }

    // This gets set once and is never reset.
    this._s._everCompatible = true;

    // Date preferences can suffer from clock skew. Look for times in the
    // future and reset.
    if (this._s.firstNotifyDay && this._s.firstNotifyDay > this._daysSinceEpoch()) {
      this._log.warn("Resetting firstNotifyDay because of apparent clock skew.");
      this._s.firstNotifyDay = 0;
    }

    if (this._s.lastNotifyDay && this._s.lastNotifyDay > this._daysSinceEpoch()) {
      this._log.warn("Resetting lastNotifyDay because of apparent clock skew.");
      this._s.lastNotifyDay = 0;
    }

    // Back up the upgrade URL in case it is set.
    if (Services.prefs.prefHasUserValue("startup.homepage_override_url")) {
      // But only if we haven't backed it up yet (so we don't overrite the backup.)
      if (!this._prefs.prefHasUserValue("override_url_backup_performed")) {
        this._prefs.setCharPref("override_url_backup",
                                Services.prefs.getCharPref("startup.homepage_override_url"));
        this._prefs.setBoolPref("override_url_backup_performed", true);
      }
    }

    // If we can't obtain a locale, give up immediately.
    // This should never happen. But you never know.
    if (!this._locale) {
      this._log.error("Could not obtain locale!");
      this._selfUninstall("NO_LOCALE");
      return;
    }

    let installerInfo = this._getInstallersJSON();
    let builds = installerInfo.win32;

    if (!(this._locale in builds)) {
      this._log.error("No installer for locale: " + this._locale);
      this._selfUninstall("UNSUPPORTED_LOCALE");
      return;
    }

    this._installerURL = installerInfo.WIN32_INSTALLER_URL_PATTERN;
    this._installerURL = this._installerURL.replace("%LOCALE%", this._locale);
    this._installerExpectedSize = builds[this._locale][0];
    this._installerExpectedHash = builds[this._locale][1];
    this._installerFile = this._stateDir.clone();
    this._installerFile.append(installerInfo.WIN32_INSTALLER_FILENAME);
    this._installerTempFile = this._stateDir.clone();
    this._installerTempFile.append(installerInfo.WIN32_INSTALLER_FILENAME + ".part");
    this._log.warn("Installer URL: " + this._installerURL);
    this._log.warn("Installer size (expected): " + this._installerExpectedSize);
    this._log.warn("Installer hash (expected): " + this._installerExpectedHash);

    this._launcherLogFile = this._stateDir.clone();
    this._launcherLogFile.append("installer.log");
    this._launcherFile = this._stateDir.clone();
    // Give a friendly name so people don't get scared.
    this._launcherFile.append("FirefoxInstallLauncher.exe");

    this._saveState(function () {
      this._log.warn("Finished start-up.");

      if (this._started) {
        return;
      }

      // One-time tasks go here.
      browserWindowCall(injectCSS);
      Services.wm.addListener(windowListener);

      this._started = true;
      this.tryToDownloadAndUpdate();
    }.bind(this));
  },

  /**
   * Try to ascertain the installer locale to use.
   *
   * When the callback is called, this._locale should have a value. It may
   * be null if a locale could not be determined.
   */
  _ensureLocaleLoaded: function (cb) {
    if (this._locale) {
      cb();
      return;
    }

    // Modern versions of Firefox have the locale in omni.jar.
    // We don't care about main thread I/O because omni.jar reads are
    // negligible since omni.jar is loaded into memory.
    for each (let res in ["app", "gre"]) {
      try {
        let url = "resource://" + res + "/update.locale";
        let channel = Services.io.newChannel(url, null, null);
        let is = channel.open();
        this._locale = this._readInputStream(is);
        if (this._locale) {
          cb();
          return;
        }
      } catch (e) { }
    }

    // Older versions have an update.locale file on disk.
    let file = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
    file.append("update.locale");
    this._readFileToString(file, function onRead1(error, content) {
      if (!error) {
        if (content[content.length - 1] == "\n") {
          content = content.slice(0, -1);
        }

        this._locale = content;
        cb();
        return;
      }

      this._log.warn("update.locale not found in XCurProcD");
      file = Services.dirsvc.get("GreD", Ci.nsIFile);
      file.append("update.locale");
      this._readFileToString(file, function onRead2(error, content) {
        if (error) {
          this._log.warn("update.locale not found in GreD");
          this._log.error("Unable to obtain locale!");
          this._locale = null;
          cb();
          return;
        }

        if (content[content.length - 1] == "\n") {
          content = content.slice(0, -1);
        }

        this._locale = content;
        cb();
      }.bind(this));
    }.bind(this));
  },

  _getInstallersJSON: function () {
    let uri = Services.io.newURI(this.INSTALLERS_URI, null, null);

    let channel = Services.io.newChannelFromURI(uri);
    let is = channel.open();

    let sis = Cc["@mozilla.org/scriptableinputstream;1"]
                .createInstance(Ci.nsIScriptableInputStream);
    sis.init(is);

    let data = sis.readBytes(is.available());
    return JSON.parse(data);
  },

  /**
   * Perform tasks during shutdown or during add-on disabling.
   */
  shutdown: function () {
    this._log.warn("Performing shutdown actions.");

    if (this._retryTimer) {
      this._retryTimer.cancel();
      this._retryTimer = null;
    }

    Services.wm.removeListener(windowListener);
    browserWindowCall(removeCSS);
  },

  /**
   * Perform cleanup actions on uninstall.
   */
  _cleanup: function (reason, cb) {
    if (this._destroyed) {
      return;
    }

    this._log.warn("Performing cleanup actions.");

    this.shutdown();

    if (!this._s.uninstallReason) {
      this._s.uninstallReason = reason;
    }

    // Restoring the homepage override url is a little funky.
    // If there was a custom pref before the hotfix executed, it will be saved
    // in the backup pref. If not, there will be no backup pref.
    //
    // We ensure there is no custom value in the pref then we restore the
    // original custom value, if any.
    let defaultPrefs = Services.prefs.getDefaultBranch("startup.");
    Services.prefs.setCharPref("startup.homepage_override_url",
                               defaultPrefs.getCharPref("homepage_override_url"));

    if (this._prefs.prefHasUserValue("override_url_backup")) {
      Services.prefs.setCharPref("startup.homepage_override_url",
                                 this._prefs.getCharPref("override_url_backup"));
    }

    // Operates on user and default despite using a user branch object.
    this._prefs.deleteBranch("");

    this._saveState();

    // The file log handle may prevent directory removal. Stop logging to
    // a file.
    if (gFileAppender) {
      this._log.warn("Closing file appender.");
      try {
        gFileAppender.close();
      } catch (e) {
        this._log.warn("Error closing file appender: " + e);
      }
      this._log.removeAppender(gFileAppender);
      gFileAppender = null;
    }

    if (this._stateDir && this._stateDir.exists()) {
      try {
        this._stateDir.remove(true);
      } catch (ex) {
        this._log.warn("Error removing state directory. Oh well.");
      }
    }

    this._destroyed = true;

    if (cb) {
      cb();
    }
  },

  /**
   * Perform a self-initiated uninstall.
   *
   * This will cleanup and uninstall in the cleanest manner possible.
   *
   * We upload forensic state, cleanup, and finally uninstall the add-on.
   */
  _selfUninstall: function (reason) {
    this._log.warn("Performing a self-uninstall.");

    this._uploadForensics(function () {
      this._cleanup(reason || "UNKNOWN", uninstallHotfix);
    }.bind(this));
  },

  /**
   * Called when an uninstall is requested via the add-on manager.
   */
  addonManagerUninstall: function () {
    this._cleanup("ADDON_MANAGER_UNINSTALL");
  },

  /**
   * Loads persisted state from the filesystem.
   *
   * The callback receives an object with various properties.
   *
   * If the state file does not exist, a default object is returned.
   */
  _loadState: function (isInstall, cb) {
    let state = {
      // Old application version that we upgraded from.
      // null if an upgrade hasn't been performed.
      upgradedFrom: null,

      // Reason the hotfix was uninstalled.
      uninstallReason: null,

      // Entity ID used to resume interrupted downloads.
      _entityID: null,

      // ID to attach to uploaded documents.
      forensicsID: this._generateUUID(),

      // Whether an installation is in progress.
      // This may be true on start if Firefox was closed while an install
      // was ongoing.
      _installInProgress: false,

      // Whether this client was at one time a candidate for the hotfix.
      _everCompatible: false,

      // The Windows version reported to GetVersionEx()/
      reportedWindowsVersion: null,

      // The actual Windows version, as detected by API sniffing. This will
      // be different from reportedWindowsVersion if running in compatibility
      // mode. The value may be under-reported. See _getActualWindowsVersion().
      actualWindowsVersion: null,

      // The first day an upgraded notification was presented.
      firstNotifyDay: 0,
      // The last day we presented an upgrade notification.
      lastNotifyDay: 0,
      // The number of times we've attempted a download.
      downloadAttempts: 0,
      // The number of completed downloads that didn't validate.
      downloadFailures: 0,
      // The number of times we've attempted to run the installer.
      installAttempts: 0,
      // The number of times the installer has completed successfully.
      installSuccesses: 0,
      // The number of times we've failed to start the launcher process.
      installLauncherFailures: 0,
      // The number of times we've launched the installer with failure.
      installFailures: 0,
      // The number of times we've shown a pop-up notification.
      notificationsShown: 0,
      // The number of times the user clicked the notification and started
      // an install.
      notificationsClicked: 0,
      // The number of times the notification was dismissed.
      notificationsDismissed: 0,
      // The number of times the notfications was removed.
      notificationsRemoved: 0,

      // Maps launcher numeric exit codes to counts.
      launcherExitCodes: {},
    };

    if (isInstall) {
      this._log.warn("Fresh install. Loading fresh state.");
      this._saveState();
      cb(state);
      return;
    }

    if (!this._stateFile.exists()) {
      this._log.warn("No state file. First run?");
      this._saveState();
      cb(state);
      return;
    }

    this._readFileToString(this._stateFile, function onRead(error, s) {
      if (error) {
        this._log.warn("Error reading state. Resetting.");
        cb(state);
        return;
      }

      try {
        state = JSON.parse(s);
      } catch (e) {
        this._log.error("Error parsing JSON from state file: " + e);
      }

      cb(state);
    }.bind(this));
  },

  _saveState: function (cb) {
    // Many calls to this function don't wait on the callback. This can
    // result in race conditions during saving. To prevent this, we chain
    // calls if necessary.
    this._pendingStateQueue.push(cb);

    if (this._pendingStateQueue.length > 1) {
      this._log.warn("Deferring save because another in progress.");
      return;
    }

    // When we're done saving, call the callback for this instance. If we
    // have chained saves, invoke them.
    let onSave = function (error) {
      this._log.warn("State save finished.");
      let cb = this._pendingStateQueue.shift();
      if (cb) {
        try {
          cb(error);
        } catch (e) {}
      }

      if (this._pendingStateQueue.length) {
        this._log.warn("Saving state file from chain.");
        this._writeStringToFile(JSON.stringify(this._s), this._stateFile, onSave);
      }
    }.bind(this);

    this._log.warn("Saving state file.");
    this._writeStringToFile(JSON.stringify(this._s), this._stateFile, onSave);
  },

  /**
   * Whether this hotfix is applicable to this Firefox install.
   */
  _isHotfixApplicable: function () {
    // Don't attempt upgrade past a specific modern version.
    if (Services.vc.compare(Services.appinfo.version, this.MAX_UPGRADE_VERSION) >= 0) {
      this._log.warn("Not applicable - modern version: " + Services.appinfo.version);
      return false;
    }

    // We only target Windows.
    if (Services.appinfo.OS != "WINNT") {
      this._log.warn("Not applicable - not Windows: " + Services.appinfo.OS);
      return false;
    }

    // Mozilla has only ever distributed 32-bit builds on Windows.
    if (Services.appinfo.XPCOMABI != "x86-msvc") {
      this._log.warn("Not applicable - not 32-bit: " + Services.appinfo.XPCOMABI);
      return false;
    }

    // We don't upgrade Windows XP SP1 and older because they are only
    // compatible up to Firefox 12.
    let version = this._getWindowsVersion();
    if (!version) {
      // This is being conservative. If we see high incidence of this in the
      // wild, we should first try to fix the underlying error and then
      // consider ignoring the failure.
      this._log.warn("Not applicable - unable to obtain Windows version.");
      return false;
    }

    this._s.reportedWindowsVersion = version;

    let actualVersion = this._getActualWindowsVersion();
    // actualVersion could be [null, null, null] for very old Windows.
    if (actualVersion && actualVersion[0]) {
      this._s.actualWindowsVersion = actualVersion;
      if (actualVersion[0] > version[0] || actualVersion[1] > version[1]) {
        this._log.warn("Windows compatibility mode detected!");
      }
      this._log.warn("Reported Windows version: " + version[0] +
                     "." + version[1] + " SP" + version[2]);
      this._log.warn("Actual Windows version: " + actualVersion[0] +
                     "." + actualVersion[1] + " SP" + actualVersion[2]);
      version = actualVersion;
    }

    // 5.0 = 2000
    // 5.1 = XP
    // 5.2 = Server 2003, XP 64
    if (version[0] < 5) {
      this._log.warn("Not applicable - Running ancient Windows.");
      return false;
    }

    if (version[0] == 5) {
      if (version[1] == 0) {
        this._log.warn("Not applicable - Running Windows 2000.");
        return false;
      } else if (version[1] == 2) {
        // Server 2003 and XP 64 should not be used much in the wild. Just
        // ignore them.
        this._log.warn("Not applicable - Running Windows XP 64 or Server 2003.");
        return false;
      }

      // We must be running XP (5.1).
      // We require XP SP2 or higher on Firefox 13 and newer (bug 563318).
      if (version[2] < 2) {
        this._log.warn("Not applicable - Running Windows XP SP1 or older.");
        return false;
      }
    }

    // Partner builds may have undefined behavior when the full installer
    // is executed.
    try {
      let partners = Services.prefs.getChildList("app.partner.");
      if (partners.length) {
        this._log.warn("Not applicable - app.partner.* pref set");
        return false;
      }
    } catch (e) {}

    // We are upgrading to release builds, so only target the release channel.
    try {
      let channel = Services.prefs.getCharPref("app.update.channel");
      if (channel != "release") {
        this._log.warn("Not applicable - not release channel: " + channel);
        return false;
      }
    } catch (e) {}

    // Only target installs that have updating enabled.
    try {
      if (!Services.prefs.getBoolPref("app.update.enabled")) {
        this._log.warn("Not applicable - app.update disabled");
        return false;
      }

      if (!Services.prefs.getBoolPref("app.update.auto")) {
        this._log.warn("Not applicable - app.update.auto disabled");
        return false;
      }
    } catch (e) {}

    this._log.warn("Hotfix applicable");
    return true;
  },

  /**
   * Whether it appears we have upgraded to the version we were supposed to.
   */
  _isUpgraded: function () {
    return Services.vc.compare(Services.appinfo.version, this.UPGRADED_VERSION) >= 0;
  },

  // ---------------------------
  // BEGIN EVENT MANAGEMENT CODE
  // ---------------------------

  /**
   * Attempt to download and update Firefox.
   *
   * This is the main function that should be called to trigger download,
   * launching the installer, and notifying the user. It is called
   * automatically on startup and periodically after that if an error occurs.
   */
  tryToDownloadAndUpdate: function () {
    this._ensureInstallerDownloaded(function onResult(error) {
      if (!error) {
        this._onInstallerReady();
        return;
      }

      this._log.warn("Download failed. Attempt " + this._s.downloadAttempts);

      if (this._s.downloadFailures >= this.MAX_DOWNLOAD_FAILURES) {
        this._log.warn("Reached maximum download failures. Giving up.");
        this._displayFailureSupport();
        this._selfUninstall("MAX_DOWNLOAD_FAILURES");
        return;
      }

      this._retryTimer = Cc["@mozilla.org/timer;1"]
                           .createInstance(Ci.nsITimer);
      this._retryTimer.initWithCallback(function onTimer() {
        this._log.warn("Retry timer fired. Attempting another download and/or update.");
        this._retryTimer = null;
        this.tryToDownloadAndUpdate();
      }.bind(this), this.DOWNLOAD_RETRY_MILLISECONDS, this._retryTimer.TYPE_ONE_SHOT);
    }.bind(this));
  },

  /**
   * Called when an installer is downloaded and ready for execution.
   *
   * This may get called on startup.
   */
  _onInstallerReady: function () {
    this._log.warn("onInstallerReady()");

    // If the download just finished and we haven't tried to run it yet, do
    // so now without any warning.
    if (!this._s.installAttempts) {
      this._log.warn("Installer ready and no install attempts. Attempting " +
                     "now.");
      this._attemptInstall(function onAttempt() {
        this._log.warn("Install attempt failed in a possibly transient way.");
        this._retryTimer = Cc["@mozilla.org/timer;1"]
                             .createInstance(Ci.nsITimer);
        this._retryTimer.initWithCallback(function onTimer() {
          this._log.warn("Retry timer fired. Attempting another install attempt.");
          this._retryTimer = null;
          // We could call _onInstallerReady(). But it's safer to start from
          // the beginning.
          this.tryToDownloadAndUpdate();
        }.bind(this), this.INSTALL_RETRY_MILLISECONDS, Ci.nsITimer.TYPE_ONE_SHOT);

      }.bind(this));
      return;
    }

    // After we've attempted an install once, show a notification and prompt
    // to begin the install for all subsequent attempts.

    // If we haven't notified yet, do it now.
    if (!this._s.firstNotifyDay || !this._s.lastNotifyDay) {
      this._log.warn("Showing notification because we haven't notified yet.");
      this._showNotification();
      return;
    }

    // We only show the notification at most once per day.
    if (this._daysSinceEpoch() == this._s.lastNotifyDay) {
      this._log.warn("Not showing notification because we've already shown today.");
      return;
    }

    this._log.warn("Showing notification because we haven't notified today.");
    this._showNotification();
  },

  /**
   * Attempt to run the installer.
   *
   * The passed callback will only be called if the installer failed in a
   * (hopefully) transient way. All other results are success or fatal error
   * and result in the add-on being uninstalled.
   */
  _attemptInstall: function (cb) {
    this._s._installInProgress = true;

    // Record our old version in a persisted location so we can later
    // obtain info on hotfix-impacted users.
    this._s.upgradedFrom = Services.appinfo.version;

    this._saveState();

    this._runInstaller(function onInstaller(error, status) {
      this._s._installInProgress = false;
      this._saveState();

      // We have two types of errors: launcher process failed and installer
      // failed.

      // If the launcher process failed to even start, we assume it will fail
      // again. So, we give up. If this assumption is wrong, we'll have a
      // forensic ping with details of the failure so we can hopefully push
      // a new hotfix with the fix.
      if (error) {
        this._log.error("Got error running launcher. Assuming fatal: " + error);
        this._displayFailureSupport();
        this._selfUninstall("LAUNCHER_START_ERROR");
        return;
      }

      // In the case of successful install, Firefox will continue running.
      // The update will be complete when Firefox is restarted. At that
      // time, this add-on will detect it is no longer applicable and
      // it will uninstall itself.
      if (status == this.INSTALLER_STATUS_SUCCESS) {
        this._log.warn("Installer finished successfully!");
        Services.prefs.setCharPref("startup.homepage_override_url",
                                   this.UPGRADE_HOMEPAGE_OVERRIDE);

        // Set this here so it won't get overwritten during uninstall after
        // restart.
        this._s.uninstallReason = "SUCCESSFUL_UPGRADE";

        this._saveState();
        return;
      }

      if (status == this.INSTALLER_STATUS_FATAL) {
        this._log.error("Error with install. Assuming non-recoverable.");
        this._displayFailureSupport();
        this._selfUninstall("LAUNCHER_RUN_ERROR");
        return;
      }

      if (status == this.INSTALLER_STATUS_TRANSIENT) {
        this._uploadForensics(function onForensics() {
          if (cb) {
            cb();
          }
        }.bind(this));
        return;
      }

      // This should only happen due to a bug in the hotfix. We don't
      // display the failure page here because the burden to fix this should
      // be on the hotfix, not the user.
      this._log.error("Unexpected installer status: " + status);
      this._selfUninstall("UNEXPECTED_INSTALLER_STATUS");
    }.bind(this));
  },

  /**
   * Called when a the pop-up notification is shown.
   */
  _onNotifyShown: function () {
    this._log.warn("Notification shown.");
    this._s.notificationsShown++;

    let thisDay = this._daysSinceEpoch();

    if (!this._s.firstNotifyDay) {
      this._log.warn("This was the first notification.");
      this._s.firstNotifyDay = thisDay;
    }

    this._s.lastNotifyDay = thisDay;
    this._saveState();
  },

  /**
   * Called when the "use now" button is clicked and we should start the upgrade.
   */
  _onStartInstallClicked: function () {
    this._log.warn("User clicked notification to begin install.");
    this._s.notificationsClicked++;
    this._saveState();

    this._attemptInstall();
  },

  _onNotifyDismissed: function () {
    this._log.warn("Notification dismissed.");
    this._s.notificationsDismissed++;
    this._saveState();
  },

  _onNotifyRemoved: function () {
    this._log.warn("Notification removed.");
    this._s.notificationsRemoved++;
    this._saveState();
  },

  /**
   * Called after a non-transient failure to run the installer.
   */
  _displayFailureSupport: function () {
    let window = Services.wm.getMostRecentWindow("navigator:browser");
    let url = Services.urlFormatter.formatURL(this.FAILED_INSTALL_URL);
    window.gBrowser.addTab(url);
  },

  /**
   * This is called after an upgrade is performed using the hotfix.
   *
   * The role of this function is to try to send data to Mozilla and then
   * to uninstall the hotfix.
   *
   * This isn't as easy as it sounds.
   *
   * For data upload to work, the user needs to agree to it. This requires
   * Telemetry or FHR to be enabled. Telemetry has been around for ages, but
   * it likely isn't enabled. FHR was introduced in Firefox 21. FHR, since
   * it is enabled more easily, will likely be the vehicle that allows upload
   * to occur.
   *
   * If we were to look for FHR state immediately after upgrade, chances are
   * it won't be present because FHR waits 24 hours after first run before it
   * does anything. This would mean that upload wouldn't be allowed and we'd
   * likely not report data for pre-21 clients impacted by this hotfix. That's
   * not acceptable.
   */
  _onPostUpgrade: function () {
    // If we know an upload is allowed, go ahead and do it without involving
    // all the FHR logic.
    if (this._isForensicsUploadAllowed()) {
      this._selfUninstall("SUCCESSFUL_UPGRADE");
      return;
    }

    // Since we've upgraded, we should be running on a build that has FHR.
    // That means we can use its API.
    try {
      let service = Cc["@mozilla.org/datareporting/service;1"]
                      .getService(Ci.nsISupports)
                      .wrappedJSObject;

      let reporter = service.healthReporter;
      let policy = service.policy;

      if (!reporter) {
        this._log.warn("FHR not present.");
        this._selfUninstall("SUCCESSFUL_UPGRADE");
        return;
      }

      if (!policy) {
        this._log.warn("FHR policy not present.");
        this._selfUninstall("SUCCESSFUL_UPGRADE");
        return;
      }

      // Ensure FHR is initialized.
      reporter.onInit().then(
        function onFHRInit() {
          try{
            // If the user has responded to the policy one way or another,
            // do uninstall, maybe doing upload along the way.
            if (policy.ensureNotifyResponse(policy.now())) {
              this._log.warn("User has responded to FHR policy.");
              this._selfUninstall("SUCCESSFUL_UPGRADE");
              return;
            }

            // If the user hasn't responded, we wait. We could involve
            // polling logic, etc here. By why add complexity? We just
            // shut down and try again on the next browser restart.
            this._log.warn("User hasn't responded to FHR policy. Shutting down.");
            this.shutdown();
          } catch (e) {
            this._log.warn("Error interacting with FHR policy: " + e);
            this._selfUninstall("SUCCESSFUL_UPGRADE");
          }
        }.bind(this),
        function onFHRFailure() {
          this._log.warn("Could not initialize FHR. Weird.");
          this._selfUninstall("SUCCESSFUL_UPGRADE");
        }.bind(this)
      );
    } catch (e) {
      this._log.error("Exception interacting with FHR: " + e);
      this._selfUninstall("SUCCESSFUL_UPGRADE");
    }
  },

  // ----------------------
  // BEGIN DOWNLOADING CODE
  // ----------------------

  /**
   * Ensure the installer is downloaded and validated.
   *
   * If the file exists locally, it will be validated and used if it passes
   * checks. If it doesn't pass checks, it will be removed and re-downloaded.
   *
   * If the file does not exist locally, it will be downloaded.
   */
  _ensureInstallerDownloaded: function (cb) {
    if (this._installerFile.exists()) {
      this._log.warn("Existing installer present. Verifying.");

      // NOTE: This could happen on startup, triggering up to 30MB in read I/O.
      this._verifyDownload(this._installerFile, function onVerify(success) {
        if (success) {
          cb(null, this._installerFile);
          return;
        }

        // If the existing file could not be validated, blow it away and
        // try again.
        // NOTE: we should never get here because the file will be validated
        // before it is moved into its final location. In theory filesystem
        // corruption could cause this failure, so we catch it.
        this._log.warn("Removing downloaded file and entityID.");
        this._installerFile.remove(false);
        this._s._entityID = null;
        this._saveState();
        this._ensureInstallerDownloaded(cb);
      }.bind(this));

      return;
    }

    this._s.downloadAttempts++;
    this._log.warn("Starting download. Attempt " + this._s.downloadAttempts);
    this._saveState();

    // No existing installer file. Download it.
    this._doDownload(function onDownload(error) {
      if (error) {
        this._log.warn("Download did not complete successfully.");
        cb(error);
        return;
      }

      this._log.warn("Verifying download.");
      this._verifyDownload(this._installerTempFile, function onResult(success) {
        if (success) {
          this._installerTempFile.moveTo(this._stateDir, this._installerFile.leafName);
          this._log.warn("Moved installer to final location.");
          this._s._entityID = null;
          this._saveState();
          cb(null, this._installerFile);
          return;
        }

        this._s.downloadFailures++;
        this._saveState();
        this._log.warn("Download failure #" + this._s.downloadFailures);

        this._log.warn("Removing temp file due to failed verification.");
        this._installerTempFile.remove(false);
        this._s._entityID = null;
        this._saveState();
        cb(new Error("Resetting download state because file verification failed."));
      }.bind(this));
    }.bind(this));
  },

  /**
   * Download the installer.
   *
   * This will attempt to download this._installerURL to
   * this._installerTempFile. If the download is already in progress
   * (denoted by the existence of the file), we attempt to resume it.
   *
   * NOTE: a success to the cb does not necessarily mean the file can be
   * trusted. Callers are responsible for validating the downloaded file.
   *
   * @param cb
   *        (function) Invoked on download completion. Receives an error
   *        indicator as its argument.
   */
  _doDownload: function (cb) {
    // We start with an nsIChannel that performs the HTTP GET. We take
    // data from this channel and write it to a pipe - which is acting
    // as an in-memory buffer. We create an nsIFileOutputStream to write
    // to a temporary file. A nsISimpleStreamListener moves the data from
    // the channel to the pipe on a background thread. A nsIAsyncStreamCopier
    // moves the data from the pipe to the file on a background thread.

    let channel = NetUtil.newChannel(NetUtil.newURI(this._installerURL))
                         .QueryInterface(Ci.nsIRequest)
                         .QueryInterface(Ci.nsIHttpChannel);

    // Don't cache download because we do file resuming automatically.
    channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;

    // Don't add cookies, etc.
    channel.loadFlags |= Ci.nsIRequest.LOAD_ANONYMOUS;

    let resumeFromBytes = 0;

    // Work around nsIFile caching, just in case.
    this._installerTempFile = this._installerTempFile.clone();

    if (this._installerTempFile.exists()) {
      let size = this._installerTempFile.fileSize;

      // If size >= expected, chances are the server will fail the range
      // request. Verification will catch the size mismatch. It doesn't
      // make sense to introduce complexity here.
      if (size >= this._installerExpectedSize) {
        this._log.warn("Temp file already matches expected size. " +
                       "Skipping download.");
        cb(null);
        return;
      }

      // If we have a partial download and we can resume, try to resume.
      if (channel instanceof Ci.nsIResumableChannel && this._s._entityID) {
        this._log.warn("Resuming download at byte offset " + size);
        channel.resumeAt(size, this._s._entityID);
        resumeFromBytes = size;
      }
    }

    let lastReported = 0;
    channel.notificationCallbacks = {
      QueryInterface: XPCOMUtils.generateQI([Ci.nsIInterfaceRequestor,
                                             Ci.nsIChannelEventSink,
                                             Ci.nsIProgressEventSink]),

      // nsIInterfaceRequestor

      getInterface: function (iid) {
        return this.QueryInterface(iid);
      },

      // nsIProgressEventSink

      // Log download progress.
      // This arguably isn't necessary. But it helps with forensics.
      onProgress: function (request, context, progress, progressMax) {
        if (progressMax == -1) {
          return;
        }

        let currentBytes = resumeFromBytes + progress;
        let totalBytes = resumeFromBytes + progressMax;

        if (currentBytes - lastReported < 1000000) {
          return;
        }

        this._log.warn("Download progress: " + currentBytes + "/" + totalBytes);
        lastReported = currentBytes;
      }.bind(this),

      onStatus: function () {},

      // nsIChannelEventSink.

      // We allow redirects to occur, but only if they are internal or to
      // the same URI. The download URLs should never issue HTTP 3xx.
      asyncOnChannelRedirect: function (oldChannel, newChannel, flags, callback) {
        this._log.warn("Channel redirected!");
        try {
          newChannel.QueryInterface(Ci.nsIHttpChannel);
        } catch (e) {
          this._log.warn("New channel is not an nsIHttpChannel.");
          throw Cr.NS_ERROR_NO_INTERFACE;
        }

        let isInternal = !!(flags & Ci.nsIChannelEventSink.REDIRECT_INTERNAL);
        let isSameURI = newChannel.URI.equals(oldChannel.URI);
        this._log.warn("Redirect is internal? " + isInternal + "; is same URI? " +
                       isSameURI);

        if (!isInternal || !isSameURI) {
          this._log.warn("Cancelling channel redirect.");
          oldChannel.cancel(Cr.NS_ERROR_ABORT);
          throw Cr.NS_BINDING_REDIRECTED;
        }

        channel = newChannel;
        callback.onRedirectVerifyCallback(Cr.NS_OK);
      }.bind(this),
    };

    let pipe = Cc["@mozilla.org/pipe;1"]
                 .createInstance(Ci.nsIPipe);
    // Bug 943511 removed the 5th argument. But it's required by older
    // versions.
    pipe.init(true, true, 0, 0xffffffff, null);

    let copier = Cc["@mozilla.org/network/async-stream-copier;1"]
                   .createInstance(Ci.nsIAsyncStreamCopier);
    let channelListener = Cc["@mozilla.org/network/simple-stream-listener;1"]
                            .createInstance(Ci.nsISimpleStreamListener);

    let copierStarted = false;
    let channelFailed = false;

    channelListener.init(pipe.outputStream, {
      onStartRequest: function (request, context) {
        this._log.warn("channel:onStartRequest()");

        try {
          request.QueryInterface(Ci.nsIHttpChannel);
        } catch (e) {
          this._log.error("Unexpected error: channel isn't a nsIHttpChannel.");
          request.cancel(Cr.NS_BINDING_ABORTED);
          return;
        }

        try {
          // Blocked by Windows parental controls.
          if (request.responseStatus == 450) {
            this._log.warn("Blocked by parental controls.");
            request.cancel(Cr.NS_BINDING_ABORTED);
            return;
          }
        } catch (e) {
          if (e.result == Cr.NS_ERROR_NOT_AVAILABLE) {
            this._log.warn("HTTP request seems to have failed.");
            // We should see onStopRequest immediately. No need to cancel
            // the request.
            return;
          }
        }

        // If we are attempting a resume and the entity ID changed, it
        // isn't safe to continue the resume.
        //
        // The entity ID should never change for the installers. Encountering
        // a change is very suspect of something funky happening, such as
        // a captive portal or other proxy.
        //
        // We have a few choices here.
        //
        // We could blow away our partial file and try again. The risk here is
        // that the condition is temporary (such as a captive portal) and we'll
        // be making the client do more download work.
        //
        // We could pause for a little bit and try again later. This would
        // introduce more state complexity.
        //
        // For now, we go with blowing away the partial download.
        //
        // NOTE: Firefox <27 don't verify the 206 response size is what we
        // actually expect. This could lead to more failures than expected on
        // those clients.
        if (request.status == Cr.NS_ERROR_ENTITY_CHANGED) {
          this._log.warn("Entity ID changed!");
          this._s._entityID = null;
          this._saveState();
          resumeFromBytes = 0;
        }

        // Ensure a requested resume will actually work.
        if (request instanceof Ci.nsIResumableChannel) {
          let entityID = null;
          try {
            // This may throw.
            entityID = request.entityID;
            this._log.warn("Download is resumable.");
          } catch (ex if ex instanceof Components.Exception &&
                         ex.result == Cr.NS_ERROR_NOT_RESUMABLE) {
            resumeFromBytes = 0;
            this._log.warn("Download is not resumable.");
          } finally {
            this._s._entityID = entityID;
            this._saveState();
          }
        }

        let openFlags = 0x02; // write
        if (resumeFromBytes) {
          openFlags |= 0x10; // append
        } else {
          openFlags |= 0x08 | 0x20; // create | truncate
        }

        // NOTE: safe-file-output-stream is buggy in append mode (bug 834042).
        let fos = Cc["@mozilla.org/network/file-output-stream;1"]
                    .createInstance(Ci.nsIFileOutputStream);
        fos.init(this._installerTempFile, openFlags, 493 /* 0755 */, fos.DEFER_OPEN);

        // We delay initializing the copier until this point because we don't
        // want the copier mucking about with the file output stream before
        // that stream has been seeked.
        copier.init(pipe.inputStream, fos, null, true, false, 1048576, true, true);

        copier.asyncCopy({
          onStartRequest: function (request, context) {
            this._log.warn("copier:onStartRequest()");
          }.bind(this),

          onStopRequest: function (request, context, status) {
            this._log.warn("copier:onStopRequest(" + this._err2str(status) + ")");

            if (channelFailed) {
              this._log.warn("Channel didn't complete successfully.");
              cb(new Error("Download failed."));
              return;
            }

            if (!Components.isSuccessCode(status)) {
              this._log.warn("File saving didn't complete successfully.");
              cb(new Error("Download failed."));
              return;
            }

            cb(null);
          }.bind(this),
        }, null); // copier.asyncCopy

        copierStarted = true;

      }.bind(this), // channelListener.onStartRequest

      onStopRequest: function (request, context, status) {
        this._log.warn("channel:onStopRequest(" + this._err2str(status) + ")");

        let failChannel = function () {
          if (channelFailed) {
            return;
          }

          // We could be called before copier:onStartRequest. This would make
          // the .cancel() below no-op. We set a flag to supplement
          // the copier's status.
          channelFailed = true;

          // This should trigger the copier's onStopRequest(), which will
          // invoke cb.
          pipe.outputStream.closeWithStatus(Cr.NS_BINDING_ABORTED);

          if (copierStarted) {
            this._log.warn("Closing pipe due to failed channel.");
          } else {
            // But if we didn't start the copier, we need to call cb here.
            this._log.warn("Channel failed before data was received.");
            cb(new Error("Channel failed before data was received."));
          }
        }.bind(this);

        if (!Components.isSuccessCode(status)) {
          failChannel();
          return;
        }

        let responseCode = channel.responseStatus;
        this._log.warn("Got HTTP " + responseCode);

        // 200=OK; 206=Partial Content
        if (responseCode != 200 && responseCode != 206) {
          failChannel();
          return;
        }

        // This should trigger the copier's onStopRequest().
        pipe.outputStream.close();
      }.bind(this),
    });

    // NOTE: channelListener:onStopRequest will get called on shutdown, so
    // we don't need to listen for shutdown events.
    channel.asyncOpen(channelListener, null);
  },

  /**
   * Verify a download matches expectations.
   *
   * @param file
   *        (nsIFile) Downloaded file to verify.
   * @param cb
   *        (function) Receives a single boolean argument defining whether
   *        verification was successful.
   */
  _verifyDownload: function (file, cb) {
    // nsIFile.fileSize may lie on Windows. We copy the nsIFile as a workaround.
    // See bug 1022704.
    file = file.clone();

    if (file.fileSize != this._installerExpectedSize) {
      this._log.warn("File size does not match: " + file.fileSize + " != " +
                     this._installerExpectedSize);
      cb(false);
      return;
    }

    let hash = Cc["@mozilla.org/security/hash;1"]
                 .createInstance(Ci.nsICryptoHash);
    hash.initWithString("SHA512");

    let fis = Cc["@mozilla.org/network/file-input-stream;1"]
               .createInstance(Ci.nsIFileInputStream);
    fis.init(file, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, fis.DEFER_OPEN);

    let pump = Cc["@mozilla.org/network/input-stream-pump;1"]
                 .createInstance(Ci.nsIInputStreamPump);
    pump.init(fis, -1, -1, 0, 0, true);
    pump.asyncRead({
      onStartRequest: function () {},
      onDataAvailable: function (request, context, is, offset, count) {
        hash.updateFromStream(is, count);
      }.bind(this),

      onStopRequest: function (request, context, status) {
        let digest = hash.finish(false);
        digest = this._bin2hex(digest);

        if (digest == this._installerExpectedHash) {
          this._log.warn("File hash matches.");
          cb(true);
          return;
        }

        this._log.warn("File hash mismatch!");
        cb(false);
        return;
      }.bind(this),
    }, null);
  },

  // --------------------
  // BEGIN INSTALLER CODE
  // --------------------

  /**
   * Run the installer.
   *
   * This configures the launcher and attempts to run it.
   */
  _runInstaller: function (cb) {
    this._copyLauncherToFilesystem(function onCopy(error) {
      if (error) {
        cb(error, null);
        return;
      }

      this._writeInstallerIniFile(function onIniWrite(error) {
        if (error) {
          cb(error, null);
          return;
        }

        this._runLauncher(function onRun(error, status) {
          cb(error, status);
        }.bind(this));
      }.bind(this));
    }.bind(this));
  },

  /**
   * Copy the launcher to the filesystem.
   *
   * The launcher is bundled with the extension inside the .xpi. We can't
   * run exe files inside zip files, so we copy the file to the filesystem.
   */
  _copyLauncherToFilesystem: function (cb) {
    this._log.warn("Copying launcher exe to filesystem");

    let sourceURI = Services.io.newURI(this.LAUNCHER_URI, null, null);
    NetUtil.asyncFetch(sourceURI, function onFetch(is, status, request) {
      if (!Components.isSuccessCode(status)) {
        this._log.warn("Could not fetch launcher: " + this._err2str(status));
        cb(new Error("Could not fetch launcher"));
        return;
      }

      let fos = Cc["@mozilla.org/network/file-output-stream;1"]
                  .createInstance(Ci.nsIFileOutputStream);
      fos.init(this._launcherFile,
               0x02 | 0x08 | 0x20, // write | create | truncate
               493, // 0755
               fos.DEFER_OPEN);

      NetUtil.asyncCopy(is, fos, function onCopy(status) {
        this._log.warn("Launcher copy finished - " + this._err2str(status));

        if (!Components.isSuccessCode(status)) {
          cb(new Error("Failed to copy launcher."));
          return;
        }

        cb(null);
      }.bind(this));
    }.bind(this));
  },

  /**
   * Determine whether the install path is writable.
   */
  _isInstallPathWritable: function () {
    // Ideally we'd test for writability of firefox.exe. However, Windows
    // has almost certainly opened firefox.exe without write sharing access
    // and attempts to open firefox.exe will likely result in
    // ERROR_SHARING_VIOLATION. So, we choose an arbitrary should-not-exist
    // path.
    let file = this._targetDir.clone();
    file.append("update-hotfix-test");

    let fos = Cc["@mozilla.org/network/file-output-stream;1"]
                .createInstance(Ci.nsIFileOutputStream);
    try {
      fos.init(file,
               FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_APPEND,
               FileUtils.PERMS_FILE, 0);
      fos.close();
      file.remove(false);
      return true;
    } catch (e) {
      this._log.warn("Could not open install directory for writing: " + e);
      return false;
    }
  },

  _writeInstallerIniFile: function (cb) {
    const lines = [
      "[Install]",
      "PreventRebootRequired=true",
      "QuickLaunchShortcut=false",
      "DesktopShortcut=false",
      "StartMenuShortcuts=false",
      "MaintenanceService=true",
    ];

    // Adding InstallDirectoryPath to the ini makes the installer run hidden.
    // We want the installer to run hidden if it can (fewer things that could
    // go wrong, etc).
    //
    // The only time we can't run hidden is on Windows XP when the install
    // directory is not writable. On modern Windows versions, we'll request
    // UAC elevation, which should result in a writable installation directory.
    let v = this._s.actualWindowsVersion;
    let installWritable = this._isInstallPathWritable();
    if (installWritable) {
      this._log.warn("Install path is writable.");
    } else {
      this._log.warn("Install path is not writable.");
    }
    if (v[0] != 5 || v[1] != 1 || installWritable) {
      this._log.warn("Launching installer in hidden mode.");
      lines.push("InstallDirectoryPath=%InstallPath%");
    }

    let template = lines.join("\n");
    let content = template.replace("%InstallPath%", this._targetDir.path);

    this._writeStringToFile(content, this._installerIniFile, cb);
  },

  _getFailureDetailsFromLog: function (cb) {
    this._readFileToString(this._launcherLogFile, function onData(error, s) {
      if (error) {
        cb(error);
        return;
      }

      let lines = s.split("\n");

      if (!lines.length) {
        cb(null, null);
        return;
      }

      let o = {status: lines[0], details: null};

      try {
        o.status = parseInt(o.status, 10);
      } catch (e) {
        this._log.warn("Unable to parse status code to int: " + o.status);
      }

      if (lines.length > 1) {
        o.details = lines[1];
      }

      cb(null, o);
    }.bind(this));
  },

  LAUNCHER_EXIT_CODES: {
    0: "Success",
    1: "InitializationFailed",
    2: "InvalidArguments",
    3: "CantWriteToInstallDir",
    4: "OpenLauncherLogFailed",
    5: "ElevationFailed",
    6: "ElevationCancelled",
    7: "UnarchivingFailed",
    8: "AccessingLogFailed",
    9: "InstallationFailed",
    100: "Unknown",
  },

  LAUNCHER_TRANSIENT_EXIT_CODES: [5, 6],

  // Installer completed successfully.
  INSTALLER_STATUS_SUCCESS: "success",

  // Installer failed but in a transient manner.
  INSTALLER_STATUS_TRANSIENT: "transient",

  // Installer failed in a manner that will likely reproduce.
  INSTALLER_STATUS_FATAL: "fatal",

  /**
   * Run the launcher and attempt to install Firefox.
   *
   * The callback receives (error, installStatus). error will be defined
   * if we got something unexpected in JavaScript land. This should not
   * happen! installStatus will be one of the INSTALLER_STATUS_*
   * prototype entries.
   */
  _runLauncher: function (cb) {
    this._s.installAttempts++;
    this._saveState();

    let process = Cc["@mozilla.org/process/util;1"]
                    .createInstance(Ci.nsIProcess);
    process.init(this._launcherFile);

    let args = [
      this._installerFile.path,
      this._installerIniFile.path,
      this._targetDir.path,
      this._launcherLogFile.path,
    ];

    let onProcessFinished = function (process, topic) {
      if (topic == "process-failed") {
        this._log.warn("Launcher failed.");
        this._s.installLauncherFailures++;
        this._saveState();
        cb(new Error("Launcher failed."));
        return;
      }

      if (topic != "process-finished") {
        this._log.warn("Unexpected topic for process observer: " + topic);
        // Fall through.
      } else {
        this._log.warn("Got process-finished.");
      }

      // nsIProcess has a... fun API. If the process failed to even start,
      // it sends "process-finished" and sets a non-zero process.exitValue.
      // If the process exits with a non-0 code, process.exitValue is
      // defined. However, if the process exits successfully, .exitValue
      // is undefined. Furthermore, .exitValue appears to sometimes be
      // undefined on non-0 exit code. Oy.

      this._getFailureDetailsFromLog(function (error, details) {
        if (error) {
          this._log.warn("Could not read launcher log file.");
          this._s.installFailures++;
          this._saveState();
          cb(new Error("Launcher likely failed."));
          return;
        }

        let exitCode = process.exitValue;
        if (exitCode === undefined) {
          this._log.warn("Got undefined exitValue.");

          exitCode = details.status;

          // We should never get this. Just in case, we set a dummy value.
          if (exitCode === null) {
            exitCode = 100;
          }
        }

        let exitStatus = this.LAUNCHER_EXIT_CODES[exitCode] || "unknown";

        this._log.warn("Launcher exit code: " + exitCode + "; status: " + exitStatus);

        this._s.launcherExitCodes[exitCode] = (this._s.launcherExitCodes[exitCode] || 0) + 1;

        if (exitCode) {
          this._log.warn("Launcher did not complete successfully.");
          this._s.installFailures++;
          this._saveState();

          if (details.details) {
            this._log.warn("Failure details: " + details.details);
          }

          if (this.LAUNCHER_TRANSIENT_EXIT_CODES.indexOf(exitCode) !== -1) {
            cb(null, this.INSTALLER_STATUS_TRANSIENT);
          } else {
            cb(null, this.INSTALLER_STATUS_FATAL);
          }

          return;
        }

        this._s.installSuccesses++;
        this._saveState();
        cb(null, this.INSTALLER_STATUS_SUCCESS);
      }.bind(this));
    }.bind(this);

    this._log.warn("Running launcher process.");
    process.runAsync(args, args.length, {
      QueryInterface: XPCOMUtils.generateQI([
        Ci.nsISupportsWeakReference,
        Ci.nsISupports,
        Ci.nsIObserver,
      ]),

      observe: function (process, topic, data) {
        onProcessFinished(process, topic);
      },
    });
  },

  // ------------------------------
  // BEGIN NOTIFICATION AND UI CODE
  // ------------------------------

  _showNotification: function () {
    let bundle = Services.strings.createBundle("chrome://firefox-hotfix/locale/hotfix.properties");

    let message = bundle.GetStringFromName("ready_full");
    let primaryMessage = bundle.GetStringFromName("install_full");

    // We initially display a somewhat generic message. After a week passes,
    // we move on to more descriptive messages.
    let delta = this._daysSinceEpoch() - this._s.firstNotifyDay;
    if (this._s.firstNotifyDay && delta >= 7) {
      let index = Math.floor(Math.random() * 2) + 1;
      message = bundle.GetStringFromName("ready_alt" + index);
    }

    let mainAction = {
      label: primaryMessage,
      accessKey: bundle.GetStringFromName("install.accesskey"),
      callback: this._onStartInstallClicked.bind(this),
    };

    // We don't need to set because "Not Now" is the default message and it
    // should already be translated.
    let secondaryActions = null;

    // Dismissing the notification to the tray doesn't work in Firefox <15 due
    // to incompatibilities between our CSS/XBL and legacy versions of the
    // pop-up notifications feature. See bug 1031021.
    let anchorID = "upgrade-notification-tray-icon";
    let removeOnDismissal = false;
    if (Services.vc.compare(Services.appinfo.version, "14.*") <= 0) {
      anchorID = null;
      removeOnDismissal = true;
    }

    let us = this;
    let options = {
      // Persist forever. We avoid really large values because it may overflow
      // JavaScript's Date type. 2^36 in milliseconds corresponds to ~795 days.
      timeout: Math.pow(2, 36),

      // Keep showing during location changes. Upgrades are important!
      persistWhileVisible: true,

      removeOnDismissal: removeOnDismissal,

      eventCallback: function (action) {
        switch (action) {
          case "dismissed":
            us._onNotifyDismissed();
            return;

          case "removed":
            us._onNotifyRemoved();
            return;

          case "shown":
            us._onNotifyShown();
            return;
        }
      },
    };

    let window = Services.wm.getMostRecentWindow("navigator:browser");
    if (window.switchToTabHavingURI(this.NEW_TAB_URL, true)) {
      this._log.warn("Showing notification on existing tab.");
    } else {
      this._log.warn("Opening a new tab to show notification.");
    }

    try {
      window.PopupNotifications.show(window.gBrowser.selectedBrowser, "upgrade",
                                     message, anchorID, mainAction,
                                     secondaryActions, options);
    } catch (e) {
      this._log.error("Error showing notification: " + e);
    }
  },

  // -----------------------------
  // BEGIN FORENSIC UPLOADING CODE
  // -----------------------------

  /**
   * Obtain the forensic payload to be uploaded to the server for analyis.
   *
   * NOTE: It is important that we don't leak private or user identifiable
   * data in the payload!
   */
  _getForensicPayload: function (cb) {
    let o = {
      os: Services.appinfo.OS,
      xpcomabi: Services.appinfo.XPCOMABI,
      version: Services.appinfo.version,
      state: {},
    };

    // _ prefixed keys in state are local.
    if (this._s) {
      for (let key in this._s) {
        if (key[0] == "_") {
          continue;
        }

        o.state[key] = this._s[key];
      }
    }

    o.locale = this._locale;

    // this._installerURL won't be defined if hotfix not applicable.
    try {
      o.installerURL = this._installerURL.spec;
    } catch (e) {
      o.installerURL = null;
    }

    // Getting windows version will fail on not windows.
    try {
      let version = this._getWindowsVersion();
      o.windowsMajorVersion = version[0];
      o.windowsServicePackVersion = version[1];
    } catch (e) {
      o.windowsMajorVersion = null;
      o.windowsServicePackVersion = null;
    }

    try {
      o.partner = Services.prefs.getCharPref("app.partner");
    } catch (e) {
      o.partner = null;
    }

    try {
      o.channel = Services.prefs.getCharPref("app.update.channel");
    } catch (e) {
      o.channel = null;
    }

    try {
      o.updateEnabled = Services.prefs.getBoolPref("app.update.enabled");
    } catch (e) {
      o.updateEnabled = null;
    }

    try {
      o.updateAuto = Services.prefs.getBoolPref("app.update.auto");
    } catch (e) {
      o.updateAuto = null;
    }

    let populateLog = function (file, key, isJSON, isUTF16, cb) {
      // We sometimes don't even have the file objects if we upload
      // very early.
      if (!file) {
        cb();
        return;
      }

      this._readFileToString(file, function onRead(error, data) {
        if (error) {
          this._log.error("Error fetching log: " + key);
          o[key] = null;
          cb();
          return;
        }

        if (isUTF16) {
            let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
                              .createInstance(Ci.nsIScriptableUnicodeConverter);
            converter.charset = "UTF-16LE";
            data = converter.ConvertToUnicode(data);
        }

        try {
          data = this._removeSensitiveData(data);

          if (isJSON) {
            try {
              o[key] = [];
              let lines = data.split("\n");
              for each (let line in lines) {
                if (line) {
                  o[key].push(JSON.parse(line));
                }
              }
            } catch (e) {
              o[key].push([Date.now(), 40, "ERROR READING LOG"]);
              throw e;
            }
          } else {
            // Normalize line endings.
            data = data.replace("\r\n", "\n", "g");
            o[key] = data;
          }
        } catch (e) {
          this._log.error("Error populating log: " + e);
        }

        cb();
      }.bind(this));
    }.bind(this);

    populateLog(this._logFile, "logHotfix", true, false, function () {
      populateLog(this._installerLogFile, "logInstaller", false, true, function () {
        cb(o);
      }.bind(this));
    }.bind(this));
  },

  /**
   * Whether forensics upload is allowed.
   *
   * Forensics upload is only allowed if the client was applicable for the
   * hotfix *and* the user has agreed to data collection.
   *
   * We look at both the Telemetry and FHR controls for data upload to
   * determine the answer to whether the user has agreed to data collection.
   */
  _isForensicsUploadAllowed: function () {
    if (!this._s._everCompatible) {
      this._log.warn("Forensics upload not allowed because hotfix not compatible.");
      return false;
    }

    try {
      if (Services.prefs.getBoolPref("toolkit.telemetry.enabled")) {
        this._log.warn("Forensics upload allowed via Telemetry.");
        return true;
      }
    } catch (e) {}

    try {
      const FHR_PREFS = [
        "datareporting.policy.dataSubmissionPolicyAccepted",
        "datareporting.healthreport.uploadEnabled",
      ];
      for each (let pref in FHR_PREFS) {
        if (!Services.prefs.getBoolPref(pref)) {
          return false;
        }
      }

      this._log.warn("Forensics upload allowed via FHR.");
      return true;
    } catch (e) {}

    // We couldn't find a user intent to allow uploading. Disallow.
    this._log.warn("Forensics upload not allowed due to missing user agreement.");
    return false;
  },

  /**
   * Upload forensic data to Mozilla. Maybe.
   *
   * This will attempt to upload forensic data collected by this hotfix to
   * Mozilla. If upload is not applicable or not allowed (via the user not
   * agreeing to data collection), then this no-ops.
   *
   * All uploaded data should be anonymous and contain no personally
   * identifiable information.
   */
  _uploadForensics: function (cb) {
    if (!this._isForensicsUploadAllowed()) {
      cb(null);
      return;
    }

    this._log.warn("Starting forensic upload procedure.");

    if (gFileAppender) {
      try {
        gFileAppender.flush();
      } catch (ex) {
        this._log.warn("Error flushing file appender: " + ex);
      }
    }

    this._getForensicPayload(function onPayload(o) {
      this._log.warn("Obtained payload.");
      let url = this.UPLOAD_URL + this._s.forensicsID;

      this._uploadJSON(url, o, function onUpload(error) {
        if (error) {
          this._log.warn("Upload error: " + error);
          cb(error);
          return;
        }

        this._log.warn("Upload success.");
        cb(null);
      }.bind(this));
    }.bind(this));
  },

  /**
   * Attempt to remove sensitive data from a string that we don't want sent
   * to the server.
   */
  _removeSensitiveData: function (s) {
    const DIR_SERVICE_KEYS = {
      ProfD: "Profile",
      CurWorkDir: "CurWorkDir",
      Home: "Home",
      TmpD: "Temp",
      ProgF: "ProgramFiles",
    };

    let doReplacement = function (name, file) {
      let uri = Services.io.newFileURI(file);

      // Order of operation is important here. We do the URI before the path
      // version because the path may be a subset of the URI.

      // try here because .spec may throw.
      try {
        // We use .spec and not .asciiSpec because some paths may have
        // Unicode.
        s = s.replace(uri.spec, '<' + name + 'URI>', 'g');
      } catch (e) { }

      s = s.replace(file.path, '<' + name + 'Path>', 'g');
    };

    // This might be a superset of CurWorkDir so do first.
    try {
      doReplacement("InstallDir", this._targetDir);
    } catch (e) { }

    for (let key in DIR_SERVICE_KEYS) {
      try {
        let name = DIR_SERVICE_KEYS[key];
        let file = Services.dirsvc.get(key, Ci.nsIFile);
        doReplacement(name, file);
      } catch (e) { }
    }

    return s;
  },

  // --------------------------
  // BEGIN GENERIC UTILITY CODE
  // --------------------------

  /**
   * Number of days elapsed since UNIX epoch.
   *
   * This is used to drive prompting. We never prompt twice on the same
   * calendar day (in local time). Furthermore, we select an arbitrary time
   * in the early morning when most people should be asleep. This avoids
   * a potentially dual prompt between 23:59 and 00:00.
   */
  _daysSinceEpoch: function () {
    let now = new Date();
    // New days begin at 4 AM local time.
    let adjusted = now.getTime() - now.getTimezoneOffset() + 4 * 60 * 60 * 1000;
    return Math.floor(adjusted / MILLISECONDS_IN_DAY);
  },

  /**
   * Convert a binary string to its hex representation.
   */
  _bin2hex: function (b) {
    let result = "";
    for (let i = 0; i < b.length; i++) {
      let hex = b.charCodeAt(i).toString(16);
      if (hex.length == 1) {
        hex = "0" + hex;
      }

      result += hex;
    }

    return result;
  },

  /**
   * Attempt to resolve an integer error code to a string name.
   */
  _err2str: function (e) {
    for (let k in Cr) {
      if (Cr[k] == e) {
        return k;
      }
    }

    return "" + e;
  },

  /**
   * Read a file to a string buffer.
   *
   * @param file
   *        (nsIFile) File to read.
   * @param cb
   *        (function) Receives (error, string). The string will be in
   *        binary.
   */
  _readFileToString: function (file, cb) {
    let fis = Cc["@mozilla.org/network/file-input-stream;1"]
                .createInstance(Ci.nsIFileInputStream);
    try {
      fis.init(file, 0x01 /* read */, 292 /* 0444 */, fis.DEFER_OPEN);
    } catch (ex) {
      this._log.warn("Error reading file: " + ex);
      cb(ex);
      return;
    }

    let pump = Cc["@mozilla.org/network/input-stream-pump;1"]
                 .createInstance(Ci.nsIInputStreamPump);
    pump.init(fis, -1, -1, 0, 0, true);

    let bis = Cc["@mozilla.org/binaryinputstream;1"]
                .createInstance(Ci.nsIBinaryInputStream);
    let buffer = [];

    pump.asyncRead({
      onStartRequest: function () {},
      onDataAvailable: function (request, context, is, offset, count) {
        bis.setInputStream(is);
        buffer.push(bis.readBytes(is.available()));
      },

      onStopRequest: function (request, context, status) {
        if (!Components.isSuccessCode(status)) {
          this._log.warn("Error reading file " + file.leafName + ": " +
                         this._err2str(status));
          cb(new Error("Error reading file."));
          return;
        }

        cb(null, buffer.join(""));
      }.bind(this),
    }, null);
  },

  /**
   * Write a string to a file off the main thread.
   *
   * @param s
   *        (string) Data to write. We assume it is unicode.
   * @param file
   *        (nsIFile) File to write to.
   * @param cb
   *        (function) Called on completion. Receives (error).
   */
  _writeStringToFile: function (s, file, cb) {
    let os = FileUtils.openSafeFileOutputStream(file);
    let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
                      .createInstance(Ci.nsIScriptableUnicodeConverter);
    converter.charset = "UTF-8";

    let is = converter.convertToInputStream(s);

    NetUtil.asyncCopy(is, os, function (status) {
      if (!Components.isSuccessCode(status)) {
        this._log.warn("Error writing file " + file.leafName + ": " +
                       this._err2str(status));
        cb(new Error("Error writing to file."));
        return;
      }

      cb(null);
    }.bind(this));
  },

  /**
   * Read an input stream synchronously and strip trailing newline.
   */
  _readInputStream: function (is) {
    let sis = Cc["@mozilla.org/scriptableinputstream;1"]
                .createInstance(Ci.nsIScriptableInputStream);
    sis.init(is);
    let text = sis.read(sis.available());
    sis.close();

    if (text[text.length - 1] == "\n") {
      text = text.slice(0, -1);
    }

    return text;
  },

  _getKernel32: function () {
    try {
      return ctypes.open("Kernel32");
    } catch (e) {
      this._log.error("Error opening Kernel32 with ctypes: " + e);
    }

    return null;
  },

  /**
   * Obtain the version of Windows being used.
   *
   * Returns an array of [Major Version, Minor Version, Service Pack Version].
   * If we couldn't obtain data, returns null.
   */
  _getWindowsVersion: function () {
    const BYTE = ctypes.uint8_t;
    const WORD = ctypes.uint16_t;
    const DWORD = ctypes.uint32_t;
    const WCHAR = ctypes.jschar;
    const BOOL = ctypes.int;

    // This structure is described at:
    // http://msdn.microsoft.com/en-us/library/ms724833%28v=vs.85%29.aspx
    const SZCSDVERSIONLENGTH = 128;
    const OSVERSIONINFOEXW = new ctypes.StructType('OSVERSIONINFOEXW', [
      {dwOSVersionInfoSize: DWORD},
      {dwMajorVersion: DWORD},
      {dwMinorVersion: DWORD},
      {dwBuildNumber: DWORD},
      {dwPlatformId: DWORD},
      {szCSDVersion: ctypes.ArrayType(WCHAR, SZCSDVERSIONLENGTH)},
      {wServicePackMajor: WORD},
      {wServicePackMinor: WORD},
      {wSuiteMask: WORD},
      {wProductType: BYTE},
      {wReserved: BYTE},
    ]);

    let kernel32 = this._getKernel32();
    if (!kernel32) {
      this._log.error("Could not obtain Kernel32 ctypes wrapper.");
      return null;
    }

    try {
      let GetVersionEx = kernel32.declare("GetVersionExW",
                                          ctypes.winapi_abi,
                                          BOOL,
                                          OSVERSIONINFOEXW.ptr);
      let struct = OSVERSIONINFOEXW();
      struct.dwOSVersionInfoSize = OSVERSIONINFOEXW.size;

      if (!GetVersionEx(struct.address())) {
        this._log.error("GetVersionExW call failed.");
        return null;
      }

      return [struct.dwMajorVersion, struct.dwMinorVersion, struct.wServicePackMajor];
    } catch (e) {
      this._log.error("Exception calling GetVersionExW: " + e);
    } finally {
      kernel32.close();
    }
  },

  /**
   * Attempts to determine the actual running Windows version.
   *
   * GetVersionExW() lies when the application is running in Windows
   * compatibility mode. This function falls back to an alternate version
   * detection method to identify clients that are running in compatibility
   * mode.
   *
   * It does this by looking for exported symbols from Windows libraries.
   * If a symbol is present, we are pretty much guaranteed that it is a modern
   * version.
   *
   * This function returns an array of [majorVersion, minorVersion, spVersion].
   * The actual versions could be greater than what is returned. However,
   * we will never give back versions higher than what's possible. Therefore
   * this function is useful for enforcing a lower bound, not an upper bound.
   *
   * This function doesn't work for ancient versions of Windows. Also,
   * reporting of Service Pack version is not highly accurate. It may be
   * null if it can't be determined.
   */
  _getActualWindowsVersion: function () {
    const HMODULE = ctypes.size_t;
    const LPCSTR = ctypes.char.ptr;
    const LPCTSTR = ctypes.jschar.ptr;
    const FARPROC = ctypes.size_t;

    let kernel32 = this._getKernel32();
    if (!kernel32) {
      return null;
    }

    try {
      let GetModuleHandleW = kernel32.declare("GetModuleHandleW",
                                              ctypes.winapi_abi,
                                              HMODULE,
                                              LPCTSTR);

      let module = GetModuleHandleW("kernel32");
      if (!module) {
        this._log.warn("Unable to obtain handle on kernel32.dll.");
        return null;
      }

      let GetProcAddress = kernel32.declare("GetProcAddress",
                                            ctypes.winapi_abi,
                                            FARPROC,
                                            HMODULE,
                                            LPCSTR);

      let isFunctionExported = function (m, f) {
        let NOFUNCTION = ctypes.UInt64(0);
        let result = GetProcAddress(m, f);
        return ctypes.UInt64.compare(result, NOFUNCTION) != 0;
      }.bind(this);

      // 4.10  Windows 98 and 98 SE
      // 4.90  Windows ME
      // 5.0   Windows 2000 original, SP1 to SP4
      // 5.1   Windows XP original, SP1 to SP3
      // 5.2   Server 2003 original, SP1 and SP2; XP 64
      // 6.0   Windows Vista original, SP1 and SP2; Server 2008
      // 6.1   Windows 7 original, SP1; Server 2008 R2 original and SP1
      // 6.2   Windows 8; Server 2012
      // 6.3   Windows 8.1; Server 2012 R2

      let majorVersion = null;
      let minorVersion = null;
      let spMajorVersion = null;

      // CheckTokenCompatibility() was added in Windows 8 and Server 2012.
      // http://msdn.microsoft.com/en-us/library/windows/desktop/hh448477%28v=vs.85%29.aspx
      if (isFunctionExported(module, "CheckTokenCompatibility")) {
        majorVersion = 6;
        minorVersion = 2;
      }
      // GetNumaNodeNumberFromHandle() was added in Windows 7 and Server 2008 R2.
      // http://msdn.microsoft.com/en-us/library/dd405492%28v=vs.85%29.aspx
      else if (isFunctionExported(module, "GetNumaNodeNumberFromHandle")) {
        majorVersion = 6;
        minorVersion = 1;

        // CopyContext() was added in 7 SP1 and Server 2008 R2 SP1.
        // http://msdn.microsoft.com/en-us/library/windows/desktop/hh134234%28v=vs.85%29.aspx
        if (isFunctionExported(module, "CopyContext")) {
          spMajorVersion = 1;
        } else {
          spMajorVersion = 0;
        }
      }
      // GetLocaleInfoEx() was added in Vista and Server 2008.
      // http://msdn.microsoft.com/en-us/library/windows/desktop/dd318103%28v=vs.85%29.aspx
      else if (isFunctionExported(module, "GetLocaleInfoEx")) {
        majorVersion = 6;
        minorVersion = 0;

        // NOTE: We could not find any kernel32 APIs added in Vista SP2.

        // GetPhysicallyInstalledSystemMemory() was added in Vista SP1 and
        // Server 2008.
        // http://msdn.microsoft.com/en-us/library/windows/desktop/cc300158%28v=vs.85%29.aspx
        if (isFunctionExported(module, "GetPhysicallyInstalledSystemMemory")) {
          spMajorVersion = 1;
        } else {
          spMajorVersion = 0;
        }
      }
      // FindFirstStreamW() was added in Vista and Server 2003.
      // http://msdn.microsoft.com/en-us/library/windows/desktop/aa364424%28v=vs.85%29.aspx
      else if (isFunctionExported(module, "FindFirstStreamW")) {
        majorVersion = 5;
        minorVersion = 2;
      }
      // GetNativeSystemInfo() was added in XP and Server 2003.
      // http://msdn.microsoft.com/en-us/library/windows/desktop/ms724340%28v=vs.85%29.aspx
      else if (isFunctionExported(module, "GetNativeSystemInfo")) {
        majorVersion = 5;
        minorVersion = 1;

        // GetLogicalProcessorInformation() was added in XP SP3, XP 64, and Vista.
        // http://msdn.microsoft.com/en-us/library/windows/desktop/ms683194%28v=vs.85%29.aspx
        if (isFunctionExported(module, "GetLogicalProcessorInformation")) {
          spMajorVersion = 3;
        }
        // DecodeSystemPointer() was added in Vista, XP SP2, Server 2008, and
        // Server 2003 SP1.
        // http://msdn.microsoft.com/en-us/library/bb432243%28v=vs.85%29.aspx
        else if (isFunctionExported(module, "DecodeSystemPointer")) {
          spMajorVersion = 2;
        }

        // GetDllDirectory() was added in XP SP1, Vista, and Server 2003.
        // http://msdn.microsoft.com/en-us/library/windows/desktop/ms683186%28v=vs.85%29.aspx
        else if (isFunctionExported(module, "GetDllDirectory")) {
          spMajorVersion = 1;
        }
        // We must be XP original.
        else {
          spMajorVersion = 0;
        }
      }

      return [majorVersion, minorVersion, spMajorVersion];
    } catch (e) {
      this._log.warn("Exception obtaining true Windows version: " + e);
      return null;
    } finally {
      kernel32.close();
    }
  },

  _convertString: function (input, outputEncoding) {
    let scs = Cc["@mozilla.org/streamConverters;1"]
                .getService(Ci.nsIStreamConverterService);
    let loader = Cc["@mozilla.org/network/stream-loader;1"]
                   .createInstance(Ci.nsIStreamLoader);

    let buffer = "";
    loader.init({
      onStreamComplete: function (loader, context, status, length, result) {
        buffer = String.fromCharCode.apply(this, result);
      },
    });

    let converter = scs.asyncConvertData("uncompressed", outputEncoding,
                                         loader, null);

    let ss = Cc["@mozilla.org/io/string-input-stream;1"]
               .createInstance(Ci.nsIStringInputStream);
    ss.data = input;
    converter.onStartRequest(null, null);
    converter.onDataAvailable(null, null, ss, 0, input.length);
    converter.onStopRequest(null, null, null);

    return buffer;
  },

  /**
   * Upload an object to a server.
   */
  _uploadJSON: function (url, obj, cb) {
    let json = JSON.stringify(obj);

    let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
                      .createInstance(Ci.nsIScriptableUnicodeConverter);
    converter.charset = "UTF-8";
    let utf8Payload = converter.ConvertFromUnicode(json);
    utf8Payload += converter.Finish();

    let payloadStream = Cc["@mozilla.org/io/string-input-stream;1"]
                          .createInstance(Ci.nsIStringInputStream);
    payloadStream.data = this._convertString(utf8Payload, "gzip");

    let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
                    .createInstance(Ci.nsIXMLHttpRequest);
    request.mozBackgroundRequest = true;
    request.open("POST", url, true);
    request.overrideMimeType("text/plain");
    request.setRequestHeader("Content-Type", "application/json; charset=UTF-8");
    request.setRequestHeader("Content-Encoding", "gzip");

    request.addEventListener("error", function onError(event) {
      cb(event);
    }.bind(this));

    request.addEventListener("load", function onLoad() {
      cb(null);
    }.bind(this));

    this._log.warn("Uploading payload to " + url);
    request.send(payloadStream);
  },

  _generateUUID: function () {
    let uuid = Cc["@mozilla.org/uuid-generator;1"]
                 .getService(Ci.nsIUUIDGenerator)
                 .generateUUID()
                 .toString();
    // Trim {}.
    return uuid.substring(1, uuid.length - 1);
  },
};

// Our singleton instance.
this.manager = new UpgradeManager();

this.uninstallHotfix = function () {
  log.warn("Uninstalling add-on.");
  AddonManager.getAddonByID(manager.addonID, function(addon) {
    if (addon) {
      addon.uninstall();
    }
  });
};

/**
 * Call a function for each browser window.
 */
function browserWindowCall(cb) {
  let e = Services.wm.getEnumerator("navigator:browser");
  while (e.hasMoreElements()) {
    let win = e.getNext().QueryInterface(Ci.nsIDOMWindow);
    if (win) {
      cb(win);
    }
  }
}

let windowListener = {
  onOpenWindow: function (window) {
    let dw = window.QueryInterface(Ci.nsIInterfaceRequestor)
                   .getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow);
    dw.addEventListener("load", function loadListener() {
      dw.removeEventListener("load", loadListener, false);
      injectCSS(dw);
    });
  },
  onCloseWindow: function () {},
  onWindowTitleChange: function () {},
};

/**
 * Inject CSS to register our custom notification type.
 *
 * Old Firefox versions don't have icons in notifications. We add
 * that. We also give our notification a dedicated binding to make
 * manipulation easier.
 */
function injectCSS(window) {
  let doc = window.document;

  let pi = doc.createProcessingInstruction(
    "xml-stylesheet",
    'id="upgrade-notification-css" href="chrome://firefox-hotfix/content/notification.css" type="text/css"');

  doc.insertBefore(pi, doc.getElementById('main-window'));

  let image = doc.createElement("image");
  image.setAttribute("id", "upgrade-notification-tray-icon");
  image.setAttribute("class", "notification-anchor-icon");
  image.setAttribute("role", "button");
  let el = doc.getElementById("notification-popup-box");
  if (el) {
    el.appendChild(image);
  }
}

function removeCSS(window) {
  let nodes = window.document.childNodes;
  let toDelete = [];

  for (let i = 0; i < nodes.length; i++) {
    let node = nodes[i];
    if (node.nodeName != "xml-stylesheet") {
      continue;
    }

    if (node.data.indexOf('id="upgrade-notification-css"') != -1) {
      toDelete.push(node);
    }
  }

  for each (let node in toDelete) {
    node.parentNode.removeChild(node);
  }

  let el = window.document.getElementById("upgrade-notification-tray-icon");
  if (el) {
    el.parentNode.removeChild(el);
  }
}