/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is the Update Service.
 *
 * The Initial Developer of the Original Code is Google Inc.
 * Portions created by the Initial Developer are Copyright (C) 2005
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *  Ben Goodger <ben@mozilla.org> (Original Author)
 *  Asaf Romano <mozilla.mano@sent.com>
 *  Jeff Walden <jwalden+code@mit.edu>
 *  Robert Strong <robert.bugzilla@gmail.com>
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

Components.utils.import("resource://gre/modules/DownloadUtils.jsm");
Components.utils.import("resource://gre/modules/AddonManager.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");

// Firefox's macBrowserOverlay.xul includes scripts that define Cc, Ci, and Cr
// so we have to use different names.
const CoC = Components.classes;
const CoI = Components.interfaces;
const CoR = Components.results;

const XMLNS_XUL               = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";

const PREF_APP_UPDATE_BACKGROUNDERRORS   = "app.update.backgroundErrors";
const PREF_APP_UPDATE_BILLBOARD_TEST_URL = "app.update.billboard.test_url";
const PREF_APP_UPDATE_CERT_ERRORS        = "app.update.cert.errors";
const PREF_APP_UPDATE_ENABLED            = "app.update.enabled";
const PREF_APP_UPDATE_LOG                = "app.update.log";
const PREF_APP_UPDATE_MANUAL_URL         = "app.update.url.manual";
const PREF_APP_UPDATE_NEVER_BRANCH       = "app.update.never.";
const PREF_APP_UPDATE_TEST_LOOP          = "app.update.test.loop";
const PREF_PLUGINS_UPDATEURL             = "plugins.update.url";

const PREF_EM_HOTFIX_ID                  = "extensions.hotfix.id";

const UPDATE_TEST_LOOP_INTERVAL     = 2000;

const URI_UPDATES_PROPERTIES  = "chrome://mozapps/locale/update/updates.properties";

const STATE_DOWNLOADING       = "downloading";
const STATE_PENDING           = "pending";
const STATE_APPLYING          = "applying";
const STATE_SUCCEEDED         = "succeeded";
const STATE_DOWNLOAD_FAILED   = "download-failed";
const STATE_FAILED            = "failed";

const SRCEVT_FOREGROUND       = 1;
const SRCEVT_BACKGROUND       = 2;

const CERT_ATTR_CHECK_FAILED_NO_UPDATE  = 100;
const CERT_ATTR_CHECK_FAILED_HAS_UPDATE = 101;
const BACKGROUNDCHECK_MULTIPLE_FAILURES = 110;

var gLogEnabled = false;
var gUpdatesFoundPageId;

// Notes:
// 1. use the wizard's goTo method whenever possible to change the wizard
//    page since it is simpler than most other methods and behaves nicely with
//    mochitests.
// 2. using a page's onPageShow method to then change to a different page will
//    of course call that page's onPageShow method which can make mochitests
//    overly complicated and fragile so avoid doing this if at all possible.
//    This is why a page's next attribute is set prior to the page being shown
//    whenever possible.

/**
 * Logs a string to the error console.
 * @param   string
 *          The string to write to the error console..
 */
function LOG(module, string) {
  if (gLogEnabled) {
    dump("*** AUS:UI " + module + ":" + string + "\n");
    Services.console.logStringMessage("AUS:UI " + module + ":" + string);
  }
}

/**
 * Opens a URL using the event target's url attribute for the URL. This is a
 * workaround for Bug 263433 which prevents respecting tab browser preferences
 * for where to open a URL.
 */
function openUpdateURL(event) {
  if (event.button == 0)
    openURL(event.target.getAttribute("url"));
}

/**
 * Gets a preference value, handling the case where there is no default.
 * @param   func
 *          The name of the preference function to call, on nsIPrefBranch
 * @param   preference
 *          The name of the preference
 * @param   defaultValue
 *          The default value to return in the event the preference has
 *          no setting
 * @returns The value of the preference, or undefined if there was no
 *          user or default value.
 */
function getPref(func, preference, defaultValue) {
  try {
    return Services.prefs[func](preference);
  }
  catch (e) {
    LOG("General", "getPref - failed to get preference: " + preference);
  }
  return defaultValue;
}

/**
 * A set of shared data and control functions for the wizard as a whole.
 */
var gUpdates = {
  /**
   * The nsIUpdate object being used by this window (either for downloading,
   * notification or both).
   */
  update: null,

  /**
   * List of incompatible add-ons
   */
  addons: [],

  /**
   * The updates.properties <stringbundle> element.
   */
  strings: null,

  /**
   * The Application brandShortName (e.g. "Firefox")
   */
  brandName: null,

  /**
   * The <wizard> element
   */
  wiz: null,

  /**
   * Whether to run the unload handler. This will be set to false when the user
   * exits the wizard via onWizardCancel or onWizardFinish.
   */
  _runUnload: true,

  /**
   * Helper function for setButtons
   * Resets button to original label & accesskey if string is null.
   */
  _setButton: function(button, string) {
    if (string) {
      var label = this.getAUSString(string);
      if (label.indexOf("%S") != -1)
        label = label.replace(/%S/, this.brandName);
      button.label = label;
      button.setAttribute("accesskey",
                          this.getAUSString(string + ".accesskey"));
    } else {
      button.label = button.defaultLabel;
      button.setAttribute("accesskey", button.defaultAccesskey);
    }
  },

  /**
   * Sets the attributes needed for this Wizard's control buttons (labels,
   * disabled, hidden, etc.)
   * @param   extra1ButtonString
   *          The property in the stringbundle containing the label to put on
   *          the first extra button, or null to hide the first extra button.
   * @param   extra2ButtonString
   *          The property in the stringbundle containing the label to put on
   *          the second extra button, or null to hide the second extra button.
   * @param   nextFinishButtonString
   *          The property in the stringbundle containing the label to put on
   *          the Next / Finish button, or null to hide the button. The Next and
   *          Finish buttons are never displayed at the same time in a wizard
   *          with the the Finish button only being displayed when there are no
   *          additional pages to display in the wizard.
   * @param   canAdvance
   *          true if the wizard can be advanced (e.g. the next / finish button
   *          should be enabled), false otherwise.
   * @param   showCancel
   *          true if the wizard's cancel button should be shown, false
   *          otherwise. If not specified this will default to false.
   *
   * Note:
   * Per Bug 324121 the wizard should not look like a wizard and to accomplish
   * this the back button is never displayed and the cancel button is only
   * displayed for the checking and the incompatibleCheck pages. This causes the
   * wizard buttons to be arranged as follows on Windows with the next and
   * finish buttons never being displayed at the same time.
   * +--------------------------------------------------------------+
   * | [ extra1 ] [ extra2 ]                     [ next or finish ] |
   * +--------------------------------------------------------------+
   */
  setButtons: function(extra1ButtonString, extra2ButtonString,
                       nextFinishButtonString, canAdvance, showCancel) {
    this.wiz.canAdvance = canAdvance;

    var bnf = this.wiz.getButton(this.wiz.onLastPage ? "finish" : "next");
    var be1 = this.wiz.getButton("extra1");
    var be2 = this.wiz.getButton("extra2");
    var bc = this.wiz.getButton("cancel");

    // Set the labels for the next / finish, extra1, and extra2 buttons
    this._setButton(bnf, nextFinishButtonString);
    this._setButton(be1, extra1ButtonString);
    this._setButton(be2, extra2ButtonString);

    bnf.hidden = bnf.disabled = !nextFinishButtonString;
    be1.hidden = be1.disabled = !extra1ButtonString;
    be2.hidden = be2.disabled = !extra2ButtonString;
    bc.hidden = bc.disabled = !showCancel;

    // Hide and disable the back button each time setButtons is called
    // (see bug 464765).
    var btn = this.wiz.getButton("back");
    btn.hidden = btn.disabled = true;

    // Hide and disable the finish button if not on the last page or the next
    // button if on the last page each time setButtons is called.
    btn = this.wiz.getButton(this.wiz.onLastPage ? "next" : "finish");
    btn.hidden = btn.disabled = true;
  },

  getAUSString: function(key, strings) {
    if (strings)
      return this.strings.getFormattedString(key, strings);
    return this.strings.getString(key);
  },

  never: function () {
    // If the user clicks "No Thanks", we should not prompt them to update to
    // this version again unless they manually select "Check for Updates..."
    // which will clear all of the "never" prefs.
    var neverPrefName = PREF_APP_UPDATE_NEVER_BRANCH + this.update.appVersion;
    Services.prefs.setBoolPref(neverPrefName, true);
  },

  /**
   * A hash of |pageid| attribute to page object. Can be used to dispatch
   * function calls to the appropriate page.
   */
  _pages: { },

  /**
   * Called when the user presses the "Finish" button on the wizard, dispatches
   * the function call to the selected page.
   */
  onWizardFinish: function() {
    this._runUnload = false;
    var pageid = document.documentElement.currentPage.pageid;
    if ("onWizardFinish" in this._pages[pageid])
      this._pages[pageid].onWizardFinish();
  },

  /**
   * Called when the user presses the "Cancel" button on the wizard, dispatches
   * the function call to the selected page.
   */
  onWizardCancel: function() {
    this._runUnload = false;
    var pageid = document.documentElement.currentPage.pageid;
    if ("onWizardCancel" in this._pages[pageid])
      this._pages[pageid].onWizardCancel();
  },

  /**
   * Called when the user presses the "Next" button on the wizard, dispatches
   * the function call to the selected page.
   */
  onWizardNext: function() {
    var cp = document.documentElement.currentPage;
    if (!cp)
      return;
    var pageid = cp.pageid;
    if ("onWizardNext" in this._pages[pageid])
      this._pages[pageid].onWizardNext();
  },

  /**
   * The checking process that spawned this update UI. There are two types:
   * SRCEVT_FOREGROUND:
   *   Some user-generated event caused this UI to appear, e.g. the Help
   *   menu item or the button in preferences. When in this mode, the UI
   *   should remain active for the duration of the download.
   * SRCEVT_BACKGROUND:
   *   A background update check caused this UI to appear, probably because
   *   incompatibilities in Extensions or other addons were discovered and
   *   the user's consent to continue was required. When in this mode, the
   *   UI will disappear after the user's consent is obtained.
   */
  sourceEvent: SRCEVT_FOREGROUND,

  /**
   * Helper function for onLoad
   * Saves default button label & accesskey for use by _setButton
   */
  _cacheButtonStrings: function (buttonName) {
    var button = this.wiz.getButton(buttonName);
    button.defaultLabel = button.label;
    button.defaultAccesskey = button.getAttribute("accesskey");
  },

  /**
   * Called when the wizard UI is loaded.
   */
  onLoad: function() {
    this.wiz = document.documentElement;

    gLogEnabled = getPref("getBoolPref", PREF_APP_UPDATE_LOG, false)

    this.strings = document.getElementById("updateStrings");
    var brandStrings = document.getElementById("brandStrings");
    this.brandName = brandStrings.getString("brandShortName");

    var pages = this.wiz.childNodes;
    for (var i = 0; i < pages.length; ++i) {
      var page = pages[i];
      if (page.localName == "wizardpage")
        this._pages[page.pageid] = eval(page.getAttribute("object"));
    }

    // Cache the standard button labels in case we need to restore them
    this._cacheButtonStrings("next");
    this._cacheButtonStrings("finish");
    this._cacheButtonStrings("extra1");
    this._cacheButtonStrings("extra2");

    // Advance to the Start page.
    this.getStartPageID(function(startPageID) {
      LOG("gUpdates", "onLoad - setting current page to startpage " + startPageID);
      gUpdates.wiz.currentPage = document.getElementById(startPageID);
    });
  },

  /**
   * Called when the wizard UI is unloaded.
   */
  onUnload: function() {
    if (this._runUnload) {
      var cp = this.wiz.currentPage;
      if (cp.pageid != "finished" && cp.pageid != "finishedBackground")
        this.onWizardCancel();
    }
  },

  /**
   * Gets the ID of the <wizardpage> object that should be displayed first. This
   * is an asynchronous method that passes the resulting object to a callback
   * function.
   *
   * This is determined by how we were called by the update prompt:
   *
   * Prompt Method:       Arg0:         Update State: Src Event:  Failed:   Result:
   * showUpdateAvailable  nsIUpdate obj --            background  --        see Note below
   * showUpdateDownloaded nsIUpdate obj pending       background  --        finishedBackground
   * showUpdateInstalled  "installed"   --            --          --        installed
   * showUpdateError      nsIUpdate obj failed        either      partial   errorpatching
   * showUpdateError      nsIUpdate obj failed        either      complete  errors
   * checkForUpdates      null          --            foreground  --        checking
   * checkForUpdates      null          downloading   foreground  --        downloading
   *
   * Note: the page returned (e.g. Result) for showUpdateAvaulable is as follows:
   * New enabled incompatible add-ons   : incompatibleCheck page
   * No new enabled incompatible add-ons: either updatesfoundbasic or
   *                                      updatesfoundbillboard as determined by
   *                                      updatesFoundPageId
   * @param   aCallback
   *          A callback to pass the <wizardpage> object to be displayed first to.
   */
  getStartPageID: function(aCallback) {
    if ("arguments" in window && window.arguments[0]) {
      var arg0 = window.arguments[0];
      if (arg0 instanceof CoI.nsIUpdate) {
        // If the first argument is a nsIUpdate object, we are notifying the
        // user that the background checking found an update that requires
        // their permission to install, and it's ready for download.
        this.setUpdate(arg0);
        if (this.update.errorCode == CERT_ATTR_CHECK_FAILED_NO_UPDATE ||
            this.update.errorCode == CERT_ATTR_CHECK_FAILED_HAS_UPDATE ||
            this.update.errorCode == BACKGROUNDCHECK_MULTIPLE_FAILURES) {
          aCallback("errorextra");
          return;
        }

        var p = this.update.selectedPatch;
        if (p) {
          var state = p.state;
          var patchFailed;
          try {
            patchFailed = this.update.getProperty("patchingFailed");
          }
          catch (e) {
          }
          if (patchFailed) {
            if (patchFailed == "partial" && this.update.patchCount == 2) {
              // If the system failed to apply the partial patch, show the
              // screen which best describes this condition, which is triggered
              // by the |STATE_FAILED| state.
              state = STATE_FAILED;
            }
            else {
              // Otherwise, if the complete patch failed, which is far less
              // likely, show the error text held by the update object in the
              // generic errors page, triggered by the |STATE_DOWNLOAD_FAILED|
              // state.
              state = STATE_DOWNLOAD_FAILED;
            }
          }

          // Now select the best page to start with, given the current state of
          // the Update.
          switch (state) {
          case STATE_PENDING:
            this.sourceEvent = SRCEVT_BACKGROUND;
            aCallback("finishedBackground");
            return;
          case STATE_DOWNLOADING:
            aCallback("downloading");
            return;
          case STATE_FAILED:
            window.getAttention();
            aCallback("errorpatching");
            return;
          case STATE_DOWNLOAD_FAILED:
          case STATE_APPLYING:
            aCallback("errors");
            return;
          }
        }
        if (this.update.licenseURL)
          this.wiz.getPageById(this.updatesFoundPageId).setAttribute("next", "license");

        var self = this;
        this.getShouldCheckAddonCompatibility(function(shouldCheck) {
          if (shouldCheck) {
            var incompatCheckPage = document.getElementById("incompatibleCheck");
            incompatCheckPage.setAttribute("next", self.updatesFoundPageId);
            aCallback(incompatCheckPage.id);
          }
          else {
            aCallback(self.updatesFoundPageId);
          }
        });
        return;
      }
      else if (arg0 == "installed") {
        aCallback("installed");
        return;
      }
    }
    else {
      var um = CoC["@mozilla.org/updates/update-manager;1"].
               getService(CoI.nsIUpdateManager);
      if (um.activeUpdate) {
        this.setUpdate(um.activeUpdate);
        aCallback("downloading");
        return;
      }
    }

    // Provide the ability to test the billboard html
    var billboardTestURL = getPref("getCharPref", PREF_APP_UPDATE_BILLBOARD_TEST_URL, null);
    if (billboardTestURL) {
      var updatesFoundBillboardPage = document.getElementById("updatesfoundbillboard");
      updatesFoundBillboardPage.setAttribute("next", "dummy");
      gUpdatesFoundBillboardPage.onExtra1 = function(){ gUpdates.wiz.cancel(); };
      gUpdatesFoundBillboardPage.onExtra2 = function(){ gUpdates.wiz.cancel(); };
      this.onWizardNext = function() { gUpdates.wiz.cancel(); };
      this.update = { billboardURL        : billboardTestURL,
                      brandName           : this.brandName,
                      displayVersion      : "Billboard Test 1.0",
                      showNeverForVersion : true,
                      type                : "major" };
      aCallback(updatesFoundBillboardPage.id);
    }
    else {
      aCallback("checking");
    }
  },

  getShouldCheckAddonCompatibility: function(aCallback) {
    // this early return should never happen
    if (!this.update) {
      aCallback(false);
      return;
    }

    if (!this.update.appVersion ||
        Services.vc.compare(this.update.appVersion, Services.appinfo.version) == 0) {
      aCallback(false);
      return;
    }

    try {
      var hotfixID = Services.prefs.getCharPref(PREF_EM_HOTFIX_ID);
    }
    catch (e) { }

    var self = this;
    AddonManager.getAllAddons(function(addons) {
      self.addons = [];
      addons.forEach(function(addon) {
        // Protect against code that overrides the add-ons manager and doesn't
        // implement the isCompatibleWith or the findUpdates method.
        if (!("isCompatibleWith" in addon) || !("findUpdates" in addon)) {
          let errMsg = "Add-on doesn't implement either the isCompatibleWith " +
                       "or the findUpdates method!";
          if (addon.id)
            errMsg += " Add-on ID: " + addon.id;
          Components.utils.reportError(errMsg);
          return;
        }

        // If an add-on isn't appDisabled and isn't userDisabled then it is
        // either active now or the user expects it to be active after the
        // restart. If that is the case and the add-on is not installed by the
        // application and is not compatible with the new application version
        // then the user should be warned that the add-on will become
        // incompatible. If an addon's type equals plugin it is skipped since
        // checking plugins compatibility information isn't supported and
        // getting the scope property of a plugin breaks in some environments
        // (see bug 566787). The hotfix add-on is also ignored as it shouldn't
        // block the user from upgrading.
        try {
          if (addon.type != "plugin" && addon.id != hotfixID &&
              !addon.appDisabled && !addon.userDisabled &&
              addon.scope != AddonManager.SCOPE_APPLICATION &&
              addon.isCompatible &&
              !addon.isCompatibleWith(self.update.appVersion,
                                      self.update.platformVersion))
            self.addons.push(addon);
        }
        catch (e) {
          Components.utils.reportError(e);
        }
      });

      aCallback(self.addons.length != 0);
    });
  },

  /**
   * Returns the string page ID for the appropriate updates found page based
   * on the update's metadata.
   */
  get updatesFoundPageId() {
    if (gUpdatesFoundPageId)
      return gUpdatesFoundPageId;
    return gUpdatesFoundPageId = this.update.billboardURL ? "updatesfoundbillboard"
                                                          : "updatesfoundbasic";
  },

  /**
   * Sets the Update object for this wizard
   * @param   update
   *          The update object
   */
  setUpdate: function(update) {
    this.update = update;
    if (this.update)
      this.update.QueryInterface(CoI.nsIWritablePropertyBag);
  }
}

/**
 * The "Checking for Updates" page. Provides feedback on the update checking
 * process.
 */
var gCheckingPage = {
  /**
   * The nsIUpdateChecker that is currently checking for updates. We hold onto
   * this so we can cancel the update check if the user closes the window.
   */
  _checker: null,

  /**
   * Initialize
   */
  onPageShow: function() {
    gUpdates.setButtons(null, null, null, false, true);
    gUpdates.wiz.getButton("cancel").focus();

    // Clear all of the "never" prefs to handle the scenario where the user
    // clicked "never" for an update, selected "Check for Updates...", and
    // then canceled.  If we don't clear the "never" prefs future
    // notifications will never happen.
    Services.prefs.deleteBranch(PREF_APP_UPDATE_NEVER_BRANCH);

    // The user will be notified if there is an error so clear the background
    // check error count.
    if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_BACKGROUNDERRORS))
      Services.prefs.clearUserPref(PREF_APP_UPDATE_BACKGROUNDERRORS);

    this._checker = CoC["@mozilla.org/updates/update-checker;1"].
                    createInstance(CoI.nsIUpdateChecker);
    this._checker.checkForUpdates(this.updateListener, true);
  },

  /**
   * The user has closed the window, either by pressing cancel or using a Window
   * Manager control, so stop checking for updates.
   */
  onWizardCancel: function() {
    this._checker.stopChecking(CoI.nsIUpdateChecker.CURRENT_CHECK);
  },

  /**
   * An object implementing nsIUpdateCheckListener that is notified as the
   * update check commences.
   */
  updateListener: {
    /**
     * See nsIUpdateCheckListener
     */
    onProgress: function(request, position, totalSize) {
      var pm = document.getElementById("checkingProgress");
      pm.mode = "normal";
      pm.value = Math.floor(100 * (position / totalSize));
    },

    /**
     * See nsIUpdateCheckListener
     */
    onCheckComplete: function(request, updates, updateCount) {
      var aus = CoC["@mozilla.org/updates/update-service;1"].
                getService(CoI.nsIApplicationUpdateService);
      gUpdates.setUpdate(aus.selectUpdate(updates, updates.length));
      if (gUpdates.update) {
        LOG("gCheckingPage", "onCheckComplete - update found");
        if (!aus.canApplyUpdates) {
          // Prevent multiple notifications for the same update when the user is
          // unable to apply updates.
          gUpdates.never();
          gUpdates.wiz.goTo("manualUpdate");
          return;
        }

        if (gUpdates.update.licenseURL) {
          // gUpdates.updatesFoundPageId returns the pageid and not the
          // element's id so use the wizard's getPageById method.
          gUpdates.wiz.getPageById(gUpdates.updatesFoundPageId).setAttribute("next", "license");
        }

        gUpdates.getShouldCheckAddonCompatibility(function(shouldCheck) {
          if (shouldCheck) {
            var incompatCheckPage = document.getElementById("incompatibleCheck");
            incompatCheckPage.setAttribute("next", gUpdates.updatesFoundPageId);
            gUpdates.wiz.goTo("incompatibleCheck");
          }
          else {
            gUpdates.wiz.goTo(gUpdates.updatesFoundPageId);
          }
        });
        return;
      }

      LOG("gCheckingPage", "onCheckComplete - no update found");
      gUpdates.wiz.goTo("noupdatesfound");
    },

    /**
     * See nsIUpdateCheckListener
     */
    onError: function(request, update) {
      LOG("gCheckingPage", "onError - proceeding to error page");
      gUpdates.setUpdate(update);
      if (update.errorCode &&
          (update.errorCode == CERT_ATTR_CHECK_FAILED_NO_UPDATE ||
           update.errorCode == CERT_ATTR_CHECK_FAILED_HAS_UPDATE)) {
        gUpdates.wiz.goTo("errorextra");
      }
      else {
        gUpdates.wiz.goTo("errors");
      }
    },

    /**
     * See nsISupports.idl
     */
    QueryInterface: function(aIID) {
      if (!aIID.equals(CoI.nsIUpdateCheckListener) &&
          !aIID.equals(CoI.nsISupports))
        throw CoR.NS_ERROR_NO_INTERFACE;
      return this;
    }
  }
};

/**
 * The "You have outdated plugins" page
 */
var gPluginsPage = {
  /**
   * URL of the plugin updates page
   */
  _url: null,
  
  /**
   * Initialize
   */
  onPageShow: function() {
    var prefs = Services.prefs;
    if (prefs.getPrefType(PREF_PLUGINS_UPDATEURL) == prefs.PREF_INVALID) {
      gUpdates.wiz.goTo("noupdatesfound");
      return;
    }
    
    this._url = Services.urlFormatter.formatURLPref(PREF_PLUGINS_UPDATEURL);
    var link = document.getElementById("pluginupdateslink");
    link.setAttribute("href", this._url);


    var phs = CoC["@mozilla.org/plugin/host;1"].
                 getService(CoI.nsIPluginHost);
    var plugins = phs.getPluginTags();
    var blocklist = CoC["@mozilla.org/extensions/blocklist;1"].
                      getService(CoI.nsIBlocklistService);

    var hasOutdated = false;
    for (let i = 0; i < plugins.length; i++) {
      let pluginState = blocklist.getPluginBlocklistState(plugins[i]);
      if (pluginState == CoI.nsIBlocklistService.STATE_OUTDATED) {
        hasOutdated = true;
        break;
      }
    }
    if (!hasOutdated) {
      gUpdates.wiz.goTo("noupdatesfound");
      return;
    }

    gUpdates.setButtons(null, null, "okButton", true);
    gUpdates.wiz.getButton("finish").focus();
  },
  
  /**
   * Finish button clicked.
   */
  onWizardFinish: function() {
    openURL(this._url);
  }
};

/**
 * The "No Updates Are Available" page
 */
var gNoUpdatesPage = {
  /**
   * Initialize
   */
  onPageShow: function() {
    LOG("gNoUpdatesPage", "onPageShow - could not select an appropriate " +
        "update. Either there were no updates or |selectUpdate| failed");

    if (getPref("getBoolPref", PREF_APP_UPDATE_ENABLED, true))
      document.getElementById("noUpdatesAutoEnabled").hidden = false;
    else
      document.getElementById("noUpdatesAutoDisabled").hidden = false;

    gUpdates.setButtons(null, null, "okButton", true);
    gUpdates.wiz.getButton("finish").focus();
  }
};


/**
 * The page that checks if there are any incompatible add-ons.
 */
var gIncompatibleCheckPage = {
  /**
   * Count of incompatible add-ons to check for updates
   */
  _totalCount: 0,

  /**
   * Count of incompatible add-ons that have beend checked for updates
   */
  _completedCount: 0,

  /**
   * The progress bar for this page
   */
  _pBar: null,

  /**
   * Initialize
   */
  onPageShow: function() {
    LOG("gIncompatibleCheckPage", "onPageShow - checking for updates to " +
        "incompatible add-ons");

    gUpdates.setButtons(null, null, null, false, true);
    gUpdates.wiz.getButton("cancel").focus();
    this._pBar = document.getElementById("incompatibleCheckProgress");
    this._totalCount = gUpdates.addons.length;

    this._pBar.mode = "normal";
    gUpdates.addons.forEach(function(addon) {
      addon.findUpdates(this, AddonManager.UPDATE_WHEN_NEW_APP_DETECTED,
                        gUpdates.update.appVersion,
                        gUpdates.update.platformVersion);
    }, this);
  },

  // Addon UpdateListener
  onCompatibilityUpdateAvailable: function(addon) {
    // Remove the add-on from the list of add-ons that will become incompatible
    // with the new version of the application.
    for (var i = 0; i < gUpdates.addons.length; ++i) {
      if (gUpdates.addons[i].id == addon.id) {
        LOG("gIncompatibleCheckPage", "onCompatibilityUpdateAvailable - " +
            "found update for add-on ID: " + addon.id);
        gUpdates.addons.splice(i, 1);
        break;
      }
    }
  },

  onUpdateAvailable: function(addon, install) {
    // If the new version of this add-on is blocklisted for the new application
    // then it isn't a valid update and the user should still be warned that
    // the add-on will become incompatible.
    let bs = CoC["@mozilla.org/extensions/blocklist;1"].
             getService(CoI.nsIBlocklistService);
    if (bs.isAddonBlocklisted(addon.id, install.version,
                              gUpdates.update.appVersion,
                              gUpdates.update.platformVersion))
      return;

    // Compatibility or new version updates mean the same thing here.
    this.onCompatibilityUpdateAvailable(addon);
  },

  onUpdateFinished: function(addon) {
    ++this._completedCount;
    this._pBar.value = Math.ceil((this._completedCount / this._totalCount) * 100);

    if (this._completedCount < this._totalCount)
      return;

    if (gUpdates.addons.length == 0) {
      LOG("gIncompatibleCheckPage", "onUpdateFinished - updates were found " +
          "for all incompatible add-ons");
    }
    else {
      LOG("gIncompatibleCheckPage", "onUpdateFinished - there are still " +
          "incompatible add-ons");
      if (gUpdates.update.licenseURL) {
        document.getElementById("license").setAttribute("next", "incompatibleList");
      }
      else {
        // gUpdates.updatesFoundPageId returns the pageid and not the element's
        // id so use the wizard's getPageById method.
        gUpdates.wiz.getPageById(gUpdates.updatesFoundPageId).setAttribute("next", "incompatibleList");
      }
    }
    gUpdates.wiz.goTo(gUpdates.updatesFoundPageId);
  }
};

/**
 * The "Unable to Update" page. Provides the user information about why they
 * were unable to update and a manual download url.
 */
var gManualUpdatePage = {
  onPageShow: function() {
    var manualURL = Services.urlFormatter.formatURLPref(PREF_APP_UPDATE_MANUAL_URL);
    var manualUpdateLinkLabel = document.getElementById("manualUpdateLinkLabel");
    manualUpdateLinkLabel.value = manualURL;
    manualUpdateLinkLabel.setAttribute("url", manualURL);

    gUpdates.setButtons(null, null, "okButton", true);
    gUpdates.wiz.getButton("finish").focus();
  }
};

/**
 * The "Updates Are Available" page. Provides the user information about the
 * available update.
 */
var gUpdatesFoundBasicPage = {
  /**
   * Initialize
   */
  onPageShow: function() {
    gUpdates.wiz.canRewind = false;
    var update = gUpdates.update;
    gUpdates.setButtons("askLaterButton",
                        update.showNeverForVersion ? "noThanksButton" : null,
                        "updateButton_" + update.type, true);
    var btn = gUpdates.wiz.getButton("next");
    btn.focus();

    var updateName = update.name;
    if (update.channel == "nightly") {
      updateName = gUpdates.getAUSString("updateNightlyName",
                                         [gUpdates.brandName,
                                          update.displayVersion,
                                          update.buildID]);
    }
    var updateNameElement = document.getElementById("updateName");
    updateNameElement.value = updateName;

    var introText = gUpdates.getAUSString("intro_" + update.type,
                                          [gUpdates.brandName, update.displayVersion]);
    var introElem = document.getElementById("updatesFoundInto");
    introElem.setAttribute("severity", update.type);
    introElem.textContent = introText;

    var updateMoreInfoURL = document.getElementById("updateMoreInfoURL");
    if (update.detailsURL)
      updateMoreInfoURL.setAttribute("url", update.detailsURL);
    else
      updateMoreInfoURL.hidden = true;

    var updateTitle = gUpdates.getAUSString("updatesfound_" + update.type +
                                            ".title");
    document.getElementById("updatesFoundBasicHeader").setAttribute("label", updateTitle);
  },

  onExtra1: function() {
    gUpdates.wiz.cancel();
  },

  onExtra2: function() {
    gUpdates.never();
    gUpdates.wiz.cancel();
  }
};

/**
 * The "Updates Are Available" page with a billboard. Provides the user
 * information about the available update.
 */
var gUpdatesFoundBillboardPage = {
  /**
   * If this page has been previously loaded
   */
  _billboardLoaded: false,

  /**
   * Initialize
   */
  onPageShow: function() {
    var update = gUpdates.update;
    gUpdates.setButtons("askLaterButton",
                        update.showNeverForVersion ? "noThanksButton" : null,
                        "updateButton_" + update.type, true);
    gUpdates.wiz.getButton("next").focus();

    if (this._billboardLoaded)
      return;

    var remoteContent = document.getElementById("updateMoreInfoContent");
    remoteContent.addEventListener("load",
                                   gUpdatesFoundBillboardPage.onBillboardLoad,
                                   false);
    // update_name and update_version need to be set before url
    // so that when attempting to download the url, we can show
    // the formatted "Download..." string
    remoteContent.update_name = gUpdates.brandName;
    remoteContent.update_version = update.displayVersion;

    var billboardTestURL = getPref("getCharPref", PREF_APP_UPDATE_BILLBOARD_TEST_URL, null);
    if (billboardTestURL) {
      // Allow file urls when testing the billboard and fallback to the
      // normal method if the URL isn't a file.
      var scheme = Services.io.newURI(billboardTestURL, null, null).scheme;
      if (scheme == "file")
        remoteContent.testFileUrl = update.billboardURL;
      else
        remoteContent.url = update.billboardURL;
    }
    else
      remoteContent.url = update.billboardURL;

    this._billboardLoaded = true;
  },

  /**
   * When the billboard document has loaded
   */
  onBillboardLoad: function(aEvent) {
    var remoteContent = document.getElementById("updateMoreInfoContent");
    // Note: may be called multiple times due to multiple onLoad events.
    var state = remoteContent.getAttribute("state");
    if (state == "loading" || aEvent.originalTarget != remoteContent)
      return;

    remoteContent.removeEventListener("load", gUpdatesFoundBillboardPage.onBillboardLoad, false);
    if (state == "error") {
      gUpdatesFoundPageId = "updatesfoundbasic";
      var next = gUpdates.wiz.getPageById("updatesfoundbillboard").getAttribute("next");
      gUpdates.wiz.getPageById(gUpdates.updatesFoundPageId).setAttribute("next", next);
      gUpdates.wiz.goTo(gUpdates.updatesFoundPageId);
    }
  },

  onExtra1: function() {
    this.onWizardCancel();
    gUpdates.wiz.cancel();
  },

  onExtra2: function() {
    this.onWizardCancel();
    gUpdates.never();
    gUpdates.wiz.cancel();
  },

  /**
   * When the user cancels the wizard
   */
  onWizardCancel: function() {
    try {
      var remoteContent = document.getElementById("updateMoreInfoContent");
      if (remoteContent)
        remoteContent.stopDownloading();
    }
    catch (e) {
      LOG("gUpdatesFoundBillboardPage", "onWizardCancel - " +
          "moreInfoContent.stopDownloading() failed: " + e);
    }
  }
};

/**
 * The page which shows the user a license associated with an update. The
 * user must agree to the terms of the license before continuing to install
 * the update.
 */
var gLicensePage = {
  /**
   * If the license url has been previously loaded
   */
  _licenseLoaded: false,

  /**
   * Initialize
   */
  onPageShow: function() {
    gUpdates.setButtons("backButton", null, "acceptTermsButton", false);

    var licenseContent = document.getElementById("licenseContent");
    if (this._licenseLoaded || licenseContent.getAttribute("state") == "error") {
      this.onAcceptDeclineRadio();
      var licenseGroup = document.getElementById("acceptDeclineLicense");
      licenseGroup.focus();
      return;
    }

    gUpdates.wiz.canAdvance = false;

    // Disable the license radiogroup until the EULA has been downloaded
    document.getElementById("acceptDeclineLicense").disabled = true;
    gUpdates.update.setProperty("licenseAccepted", "false");

    licenseContent.addEventListener("load", gLicensePage.onLicenseLoad, false);
    // The update_name and update_version need to be set before url so the ui
    // can display the formatted "Download..." string when attempting to
    // download the url.
    licenseContent.update_name = gUpdates.brandName;
    licenseContent.update_version = gUpdates.update.displayVersion;
    licenseContent.url = gUpdates.update.licenseURL;
  },

  /**
   * When the license document has loaded
   */
  onLicenseLoad: function(aEvent) {
    var licenseContent = document.getElementById("licenseContent");
    // Disable or enable the radiogroup based on the state attribute of
    // licenseContent.
    // Note: may be called multiple times due to multiple onLoad events.
    var state = licenseContent.getAttribute("state");
    if (state == "loading" || aEvent.originalTarget != licenseContent)
      return;

    licenseContent.removeEventListener("load", gLicensePage.onLicenseLoad, false);

    if (state == "error") {
      gUpdates.wiz.goTo("manualUpdate");
      return;
    }

    gLicensePage._licenseLoaded = true;
    document.getElementById("acceptDeclineLicense").disabled = false;
    gUpdates.wiz.getButton("extra1").disabled = false;
  },

  /**
   * When the user changes the state of the accept / decline radio group
   */
  onAcceptDeclineRadio: function() {
    // Return early if this page hasn't been loaded (bug 405257). This event is
    // fired during the construction of the wizard before gUpdates has received
    // its onload event (bug 452389).
    if (!this._licenseLoaded)
      return;

    var selectedIndex = document.getElementById("acceptDeclineLicense")
                                .selectedIndex;
    // 0 == Accept, 1 == Decline
    var licenseAccepted = (selectedIndex == 0);
    gUpdates.wiz.canAdvance = licenseAccepted;
  },

  /**
   * The non-standard "Back" button.
   */
  onExtra1: function() {
    gUpdates.wiz.goTo(gUpdates.updatesFoundPageId);
  },

  /**
   * When the user clicks next after accepting the license
   */
  onWizardNext: function() {
    try {
      gUpdates.update.setProperty("licenseAccepted", "true");
      var um = CoC["@mozilla.org/updates/update-manager;1"].
               getService(CoI.nsIUpdateManager);
      um.saveUpdates();
    }
    catch (e) {
      LOG("gLicensePage", "onWizardNext - nsIUpdateManager:saveUpdates() " +
          "failed: " + e);
    }
  },

  /**
   * When the user cancels the wizard
   */
  onWizardCancel: function() {
    try {
      var licenseContent = document.getElementById("licenseContent");
      // If the license was downloading, stop it.
      if (licenseContent)
        licenseContent.stopDownloading();
    }
    catch (e) {
      LOG("gLicensePage", "onWizardCancel - " +
          "licenseContent.stopDownloading() failed: " + e);
    }
  }
};

/**
 * The page which shows add-ons that are incompatible and do not have updated
 * compatibility information or a version update available to make them
 * compatible.
 */
var gIncompatibleListPage = {
  /**
   * Initialize
   */
  onPageShow: function() {
    gUpdates.setButtons("backButton", null, "okButton", true);
    var listbox = document.getElementById("incompatibleListbox");
    if (listbox.children.length > 0)
      return;

    var intro = gUpdates.getAUSString("incompatAddons_" + gUpdates.update.type,
                                      [gUpdates.brandName,
                                       gUpdates.update.displayVersion]);
    document.getElementById("incompatibleListDesc").textContent = intro;

    var addons = gUpdates.addons;
    for (var i = 0; i < addons.length; ++i) {
      var listitem = document.createElement("listitem");
      var addonLabel = gUpdates.getAUSString("addonLabel", [addons[i].name,
                                                            addons[i].version]);
      listitem.setAttribute("label", addonLabel);
      listbox.appendChild(listitem);
    }
  },

  /**
   * The non-standard "Back" button.
   */
  onExtra1: function() {
    gUpdates.wiz.goTo(gUpdates.update.licenseURL ? "license"
                                                 : gUpdates.updatesFoundPageId);
  }
};

/**
 * The "Update is Downloading" page - provides feedback for the download
 * process plus a pause/resume UI
 */
var gDownloadingPage = {
  /**
   * DOM Elements
   */
  _downloadStatus: null,
  _downloadProgress: null,
  _pauseButton: null,

  /**
   * Whether or not we are currently paused
   */
  _paused: false,

  /**
   * Label cache to hold the 'Connecting' string
   */
  _label_downloadStatus: null,

  /**
   * Member variables for updating download status
   */
  _lastSec: Infinity,
  _startTime: null,
  _pausedStatus: "",

  _hiding: false,

  /**
   * Initialize
   */
  onPageShow: function() {
    this._downloadStatus = document.getElementById("downloadStatus");
    this._downloadProgress = document.getElementById("downloadProgress");
    this._pauseButton = document.getElementById("pauseButton");
    this._label_downloadStatus = this._downloadStatus.textContent;

    this._pauseButton.setAttribute("tooltiptext",
                                   gUpdates.getAUSString("pauseButtonPause"));

    // move focus to the pause/resume button and then disable it (bug #353177)
    this._pauseButton.focus();
    this._pauseButton.disabled = true;

    var aus = CoC["@mozilla.org/updates/update-service;1"].
              getService(CoI.nsIApplicationUpdateService);

    var um = CoC["@mozilla.org/updates/update-manager;1"].
             getService(CoI.nsIUpdateManager);
    var activeUpdate = um.activeUpdate;
    if (activeUpdate)
      gUpdates.setUpdate(activeUpdate);

    if (!gUpdates.update) {
      LOG("gDownloadingPage", "onPageShow - no valid update to download?!");
      return;
    }

    this._startTime = Date.now();

    try {
      // Say that this was a foreground download, not a background download,
      // since the user cared enough to look in on this process.
      gUpdates.update.QueryInterface(CoI.nsIWritablePropertyBag);
      gUpdates.update.setProperty("foregroundDownload", "true");

      // Pause any active background download and restart it as a foreground
      // download.
      aus.pauseDownload();
      var state = aus.downloadUpdate(gUpdates.update, false);
      if (state == "failed") {
        // We've tried as hard as we could to download a valid update -
        // we fell back from a partial patch to a complete patch and even
        // then we couldn't validate. Show a validation error with instructions
        // on how to manually update.
        this.removeDownloadListener();
        gUpdates.wiz.goTo("errors");
        return;
      }
      else {
        // Add this UI as a listener for active downloads
        aus.addDownloadListener(this);
      }

      if (activeUpdate)
        this._setUIState(!aus.isDownloading);
    }
    catch(e) {
      LOG("gDownloadingPage", "onPageShow - error: " + e);
    }

    gUpdates.setButtons("hideButton", null, null, false);
    gUpdates.wiz.getButton("extra1").focus();
  },

  /**
   * Updates the text status message
   */
  _setStatus: function(status) {
    // Don't bother setting the same text more than once. This can happen
    // due to the asynchronous behavior of the downloader.
    if (this._downloadStatus.textContent == status)
      return;
    while (this._downloadStatus.hasChildNodes())
      this._downloadStatus.removeChild(this._downloadStatus.firstChild);
    this._downloadStatus.appendChild(document.createTextNode(status));
  },

  /**
   * Update download progress status to show time left, speed, and progress.
   * Also updates the status needed for pausing the download.
   *
   * @param aCurr
   *        Current number of bytes transferred
   * @param aMax
   *        Total file size of the download
   * @return Current active download status
   */
  _updateDownloadStatus: function(aCurr, aMax) {
    let status;

    // Get the download time left and progress
    let rate = aCurr / (Date.now() - this._startTime) * 1000;
    [status, this._lastSec] =
      DownloadUtils.getDownloadStatus(aCurr, aMax, rate, this._lastSec);

    // Get the download progress for pausing
    this._pausedStatus = DownloadUtils.getTransferTotal(aCurr, aMax);

    return status;
  },

  /**
   * Adjust UI to suit a certain state of paused-ness
   * @param   paused
   *          Whether or not the download is paused
   */
  _setUIState: function(paused) {
    var u = gUpdates.update;
    if (paused) {
      if (this._downloadProgress.mode != "normal")
        this._downloadProgress.mode = "normal";
      this._pauseButton.setAttribute("tooltiptext",
                                     gUpdates.getAUSString("pauseButtonResume"));
      this._pauseButton.setAttribute("paused", "true");
      var p = u.selectedPatch.QueryInterface(CoI.nsIPropertyBag);
      var status = p.getProperty("status");
      if (status) {
        let pausedStatus = gUpdates.getAUSString("downloadPausedStatus", [status]);
        this._setStatus(pausedStatus);
      }
    }
    else {
      if (this._downloadProgress.mode != "undetermined")
        this._downloadProgress.mode = "undetermined";
      this._pauseButton.setAttribute("paused", "false");
      this._pauseButton.setAttribute("tooltiptext",
                                     gUpdates.getAUSString("pauseButtonPause"));
      this._setStatus(this._label_downloadStatus);
    }
  },

  /**
   * Removes the download listener.
   */
  removeDownloadListener: function() {
    var aus = CoC["@mozilla.org/updates/update-service;1"].
              getService(CoI.nsIApplicationUpdateService);
    aus.removeDownloadListener(this);
  },

  /**
   * When the user clicks the Pause/Resume button
   */
  onPause: function() {
    var aus = CoC["@mozilla.org/updates/update-service;1"].
              getService(CoI.nsIApplicationUpdateService);
    if (this._paused)
      aus.downloadUpdate(gUpdates.update, false);
    else {
      var patch = gUpdates.update.selectedPatch;
      patch.QueryInterface(CoI.nsIWritablePropertyBag);
      patch.setProperty("status", this._pausedStatus);
      aus.pauseDownload();
    }
    this._paused = !this._paused;

    // Update the UI
    this._setUIState(this._paused);
  },

  /**
   * When the user has closed the window using a Window Manager control (this
   * page doesn't have a cancel button) cancel the update in progress.
   */
  onWizardCancel: function() {
    if (this._hiding)
      return;

    this.removeDownloadListener();
  },

  /**
   * When the user closes the Wizard UI by clicking the Hide button
   */
  onHide: function() {
    // Set _hiding to true to prevent onWizardCancel from cancelling the update
    // that is in progress.
    this._hiding = true;

    // Remove ourself as a download listener so that we don't continue to be
    // fed progress and state notifications after the UI we're updating has
    // gone away.
    this.removeDownloadListener();

    var aus = CoC["@mozilla.org/updates/update-service;1"].
              getService(CoI.nsIApplicationUpdateService);
    var um = CoC["@mozilla.org/updates/update-manager;1"].
             getService(CoI.nsIUpdateManager);
    um.activeUpdate = gUpdates.update;

    // If the download was paused by the user, ask the user if they want to
    // have the update resume in the background.
    var downloadInBackground = true;
    if (this._paused) {
      var title = gUpdates.getAUSString("resumePausedAfterCloseTitle");
      var message = gUpdates.getAUSString("resumePausedAfterCloseMsg",
                                          [gUpdates.brandName]);
      var ps = Services.prompt;
      var flags = ps.STD_YES_NO_BUTTONS;
      // Focus the software update wizard before prompting. This will raise
      // the software update wizard if it is minimized making it more obvious
      // what the prompt is for and will solve the problem of windows
      // obscuring the prompt. See bug #350299 for more details.
      window.focus();
      var rv = ps.confirmEx(window, title, message, flags, null, null, null,
                            null, { });
      if (rv == CoI.nsIPromptService.BUTTON_POS_0)
        downloadInBackground = false;
    }
    if (downloadInBackground) {
      // Continue download in the background at full speed.
      LOG("gDownloadingPage", "onHide - continuing download in background " +
          "at full speed");
      aus.downloadUpdate(gUpdates.update, false);
    }
    gUpdates.wiz.cancel();
  },

  /**
   * When the data transfer begins
   * @param   request
   *          The nsIRequest object for the transfer
   * @param   context
   *          Additional data
   */
  onStartRequest: function(request, context) {
    // This !paused test is necessary because onStartRequest may fire after
    // the download was paused (for those speedy clickers...)
    if (this._paused)
      return;

    if (this._downloadProgress.mode != "undetermined")
      this._downloadProgress.mode = "undetermined";
    this._setStatus(this._label_downloadStatus);
  },

  /**
   * When new data has been downloaded
   * @param   request
   *          The nsIRequest object for the transfer
   * @param   context
   *          Additional data
   * @param   progress
   *          The current number of bytes transferred
   * @param   maxProgress
   *          The total number of bytes that must be transferred
   */
  onProgress: function(request, context, progress, maxProgress) {
    let status = this._updateDownloadStatus(progress, maxProgress);
    var currentProgress = Math.round(100 * (progress / maxProgress));

    var p = gUpdates.update.selectedPatch;
    p.QueryInterface(CoI.nsIWritablePropertyBag);
    p.setProperty("progress", currentProgress);
    p.setProperty("status", status);

    // This !paused test is necessary because onProgress may fire after
    // the download was paused (for those speedy clickers...)
    if (this._paused)
      return;

    if (this._downloadProgress.mode != "normal")
      this._downloadProgress.mode = "normal";
    if (this._downloadProgress.value != currentProgress)
      this._downloadProgress.value = currentProgress;
    if (this._pauseButton.disabled)
      this._pauseButton.disabled = false;

    // If the update has completed downloading and the download status contains
    // the original text return early to avoid an assertion in debug builds.
    // Since the page will advance immmediately due to the update completing the
    // download updating the status is not important.
    // nsTextFrame::GetTrimmedOffsets 'Can only call this on frames that have
    // been reflowed'.
    if (progress == maxProgress &&
        this._downloadStatus.textContent == this._label_downloadStatus)
      return;

    this._setStatus(status);
  },

  /**
   * When we have new status text
   * @param   request
   *          The nsIRequest object for the transfer
   * @param   context
   *          Additional data
   * @param   status
   *          A status code
   * @param   statusText
   *          Human readable version of |status|
   */
  onStatus: function(request, context, status, statusText) {
    this._setStatus(statusText);
  },

  /**
   * When data transfer ceases
   * @param   request
   *          The nsIRequest object for the transfer
   * @param   context
   *          Additional data
   * @param   status
   *          Status code containing the reason for the cessation.
   */
  onStopRequest: function(request, context, status) {
    if (this._downloadProgress.mode != "normal")
      this._downloadProgress.mode = "normal";

    var u = gUpdates.update;
    switch (status) {
    case CoR.NS_ERROR_UNEXPECTED:
      if (u.selectedPatch.state == STATE_DOWNLOAD_FAILED &&
          (u.isCompleteUpdate || u.patchCount != 2)) {
        // Verification error of complete patch, informational text is held in
        // the update object.
        this.removeDownloadListener();
        gUpdates.wiz.goTo("errors");
        break;
      }
      // Verification failed for a partial patch, complete patch is now
      // downloading so return early and do NOT remove the download listener!

      // Reset the progress meter to "undertermined" mode so that we don't
      // show old progress for the new download of the "complete" patch.
      this._downloadProgress.mode = "undetermined";
      this._pauseButton.disabled = true;
      document.getElementById("verificationFailed").hidden = false;
      break;
    case CoR.NS_BINDING_ABORTED:
      LOG("gDownloadingPage", "onStopRequest - pausing download");
      // Do not remove UI listener since the user may resume downloading again.
      break;
    case CoR.NS_OK:
      LOG("gDownloadingPage", "onStopRequest - patch verification succeeded");
      this.removeDownloadListener();
      gUpdates.wiz.goTo("finished");
      break;
    default:
      LOG("gDownloadingPage", "onStopRequest - transfer failed");
      // Some kind of transfer error, die.
      this.removeDownloadListener();
      gUpdates.wiz.goTo("errors");
      break;
    }
  },

  /**
   * See nsISupports.idl
   */
  QueryInterface: function(iid) {
    if (!iid.equals(CoI.nsIRequestObserver) &&
        !iid.equals(CoI.nsIProgressEventSink) &&
        !iid.equals(CoI.nsISupports))
      throw CoR.NS_ERROR_NO_INTERFACE;
    return this;
  }
};

/**
 * The "There was an error during the update" page.
 */
var gErrorsPage = {
  /**
   * Initialize
   */
  onPageShow: function() {
    gUpdates.setButtons(null, null, "okButton", true);
    gUpdates.wiz.getButton("finish").focus();

    var statusText = gUpdates.update.statusText;
    LOG("gErrorsPage" , "onPageShow - update.statusText: " + statusText);

    var errorReason = document.getElementById("errorReason");
    errorReason.value = statusText;
    var manualURL = Services.urlFormatter.formatURLPref(PREF_APP_UPDATE_MANUAL_URL);
    var errorLinkLabel = document.getElementById("errorLinkLabel");
    errorLinkLabel.value = manualURL;
    errorLinkLabel.setAttribute("url", manualURL);
  }
};

/**
 * The page shown when there is a background check or a certificate attribute
 * error.
 */
var gErrorExtraPage = {
  /**
   * Initialize
   */
  onPageShow: function() {
    gUpdates.setButtons(null, null, "okButton", true);
    gUpdates.wiz.getButton("finish").focus();

    if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CERT_ERRORS))
      Services.prefs.clearUserPref(PREF_APP_UPDATE_CERT_ERRORS);

    if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_BACKGROUNDERRORS))
      Services.prefs.clearUserPref(PREF_APP_UPDATE_BACKGROUNDERRORS);

    if (gUpdates.update.errorCode == CERT_ATTR_CHECK_FAILED_HAS_UPDATE) {
      document.getElementById("errorCertAttrHasUpdateLabel").hidden = false;
    }
    else {
      if (gUpdates.update.errorCode == CERT_ATTR_CHECK_FAILED_NO_UPDATE)
        document.getElementById("errorCertCheckNoUpdateLabel").hidden = false;
      else
        document.getElementById("genericBackgroundErrorLabel").hidden = false;
      var manualURL = Services.urlFormatter.formatURLPref(PREF_APP_UPDATE_MANUAL_URL);
      var errorLinkLabel = document.getElementById("errorExtraLinkLabel");
      errorLinkLabel.value = manualURL;
      errorLinkLabel.setAttribute("url", manualURL);
      errorLinkLabel.hidden = false;
    }
  }
};

/**
 * The "There was an error applying a partial patch" page.
 */
var gErrorPatchingPage = {
  /**
   * Initialize
   */
  onPageShow: function() {
    gUpdates.setButtons(null, null, "okButton", true);
  },

  onWizardNext: function() {
    switch (gUpdates.update.selectedPatch.state) {
    case STATE_PENDING:
      gUpdates.wiz.goTo("finished");
      break;
    case STATE_DOWNLOADING:
      gUpdates.wiz.goTo("downloading");
      break;
    case STATE_DOWNLOAD_FAILED:
      gUpdates.wiz.goTo("errors");
      break;
    }
  }
};

/**
 * The "Update has been downloaded" page. Shows information about what
 * was downloaded.
 */
var gFinishedPage = {
  /**
   * Initialize
   */
  onPageShow: function() {
    gUpdates.setButtons("restartLaterButton", null, "restartNowButton",
                        true);
    gUpdates.wiz.getButton("finish").focus();
  },

  /**
   * Initialize the Wizard Page for a Background Source Event
   */
  onPageShowBackground: function() {
    this.onPageShow();
    var updateFinishedName = document.getElementById("updateFinishedName");
    updateFinishedName.value = gUpdates.update.name;

    var link = document.getElementById("finishedBackgroundLink");
    if (gUpdates.update.detailsURL) {
      link.setAttribute("url", gUpdates.update.detailsURL);
      // The details link is stealing focus so it is disabled by default and
      // should only be enabled after onPageShow has been called.
      link.disabled = false;
    }
    else
      link.hidden = true;

    if (getPref("getBoolPref", PREF_APP_UPDATE_TEST_LOOP, false)) {
      setTimeout(function () {
                   gUpdates.wiz.getButton("finish").click();
                 }, UPDATE_TEST_LOOP_INTERVAL);
    }
  },

  /**
   * Called when the wizard finishes, i.e. the "Restart Now" button is
   * clicked.
   */
  onWizardFinish: function() {
    // Do the restart
    LOG("gFinishedPage" , "onWizardFinish - restarting the application");

    // disable the "finish" (Restart) and "extra1" (Later) buttons
    // because the Software Update wizard is still up at the point,
    // and will remain up until we return and we close the
    // window with a |window.close()| in wizard.xml
    // (it was the firing the "wizardfinish" event that got us here.)
    // This prevents the user from switching back
    // to the Software Update dialog and clicking "Restart" or "Later"
    // when dealing with the "confirm close" prompts.
    // See bug #350299 for more details.
    gUpdates.wiz.getButton("finish").disabled = true;
    gUpdates.wiz.getButton("extra1").disabled = true;

    // Notify all windows that an application quit has been requested.
    var cancelQuit = CoC["@mozilla.org/supports-PRBool;1"].
                     createInstance(CoI.nsISupportsPRBool);
    Services.obs.notifyObservers(cancelQuit, "quit-application-requested",
                                 "restart");

    // Something aborted the quit process.
    if (cancelQuit.data)
      return;

    // If already in safe mode restart in safe mode (bug 327119)
    if (Services.appinfo.inSafeMode) {
      let env = CoC["@mozilla.org/process/environment;1"].
                getService(CoI.nsIEnvironment);
      env.set("MOZ_SAFE_MODE_RESTART", "1");
    }

    // Restart the application
    CoC["@mozilla.org/toolkit/app-startup;1"].getService(CoI.nsIAppStartup).
    quit(CoI.nsIAppStartup.eAttemptQuit | CoI.nsIAppStartup.eRestart);
  },

  /**
   * When the user clicks the "Restart Later" instead of the Restart Now" button
   * in the wizard after an update has been downloaded.
   */
  onExtra1: function() {
    // XXXrstrong - reminding the user to restart is broken (see bug 464835)
    gUpdates.wiz.cancel();
  }
};

/**
 * The "Update was Installed Successfully" page.
 */
var gInstalledPage = {
  /**
   * Initialize
   */
  onPageShow: function() {
    var branding = document.getElementById("brandStrings");
    try {
      // whatsNewURL should just be a pref (bug 546609).
      var url = branding.getFormattedString("whatsNewURL", [Services.appinfo.version]);
      var whatsnewLink = document.getElementById("whatsnewLink");
      whatsnewLink.setAttribute("url", url);
      whatsnewLink.hidden = false;
    }
    catch (e) {
    }

    gUpdates.setButtons(null, null, "okButton", true);
    gUpdates.wiz.getButton("finish").focus();
  }
};

/**
 * Callback for the Update Prompt to set the current page if an Update Wizard
 * window is already found to be open.
 * @param   pageid
 *          The ID of the page to switch to
 */
function setCurrentPage(pageid) {
  gUpdates.wiz.currentPage = document.getElementById(pageid);
}
