diff options
Diffstat (limited to 'components/update/content/updates.js')
-rw-r--r-- | components/update/content/updates.js | 1365 |
1 files changed, 1365 insertions, 0 deletions
diff --git a/components/update/content/updates.js b/components/update/content/updates.js new file mode 100644 index 000000000..3e05566f1 --- /dev/null +++ b/components/update/content/updates.js @@ -0,0 +1,1365 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +'use strict'; + +/* import-globals-from ../../../content/contentAreaUtils.js */ + +// Firefox's macBrowserOverlay.xul includes scripts that define Cc, Ci, and Cr +// so we have to use different names. +const {classes: CoC, interfaces: CoI, results: CoR, utils: CoU} = Components; + +/* globals DownloadUtils, Services */ +CoU.import("resource://gre/modules/DownloadUtils.jsm", this); +CoU.import("resource://gre/modules/Services.jsm", this); + +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_CERT_ERRORS = "app.update.cert.errors"; +const PREF_APP_UPDATE_ELEVATE_NEVER = "app.update.elevate.never"; +const PREF_APP_UPDATE_ENABLED = "app.update.enabled"; +const PREF_APP_UPDATE_LOG = "app.update.log"; +const PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED = "app.update.notifiedUnsupported"; +const PREF_APP_UPDATE_TEST_LOOP = "app.update.test.loop"; +const PREF_APP_UPDATE_URL_MANUAL = "app.update.url.manual"; + +const PREFBRANCH_APP_UPDATE_NEVER = "app.update.never."; + +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_PENDING_SERVICE = "pending-service"; +const STATE_PENDING_ELEVATE = "pending-elevate"; +const STATE_APPLYING = "applying"; +const STATE_APPLIED = "applied"; +const STATE_APPLIED_SERVICE = "applied-service"; +const STATE_SUCCEEDED = "succeeded"; +const STATE_DOWNLOAD_FAILED = "download-failed"; +const STATE_FAILED = "failed"; + +const SRCEVT_FOREGROUND = 1; +const SRCEVT_BACKGROUND = 2; + +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")); +} + +/** + * 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, + + /** + * 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. 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. There are currently two + // "never" prefs: the older PREFBRANCH_APP_UPDATE_NEVER as well as the + // OSX-only PREF_APP_UPDATE_ELEVATE_NEVER. We set both of these prefs (if + // applicable) to ensure that we don't prompt the user regardless of which + // pref is checked. + let neverPrefName = PREFBRANCH_APP_UPDATE_NEVER + this.update.appVersion; + Services.prefs.setBoolPref(neverPrefName, true); + let aus = CoC["@mozilla.org/updates/update-service;1"]. + getService(CoI.nsIApplicationUpdateService); + if (aus.elevationRequired) { + Services.prefs.setCharPref(PREF_APP_UPDATE_ELEVATE_NEVER, + this.update.appVersion); + } + }, + + /** + * 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 + * the user has the app.update.auto preference set to false. + */ + 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 = Services.prefs.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 -- updatesfoundbasic + * showUpdateDownloaded nsIUpdate obj pending background -- finishedBackground + * showUpdateError nsIUpdate obj failed either partial errorpatching + * showUpdateError nsIUpdate obj failed either complete errors + * checkForUpdates null -- foreground -- checking + * checkForUpdates null downloading foreground -- downloading + * + * @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 == BACKGROUNDCHECK_MULTIPLE_FAILURES) { + aCallback("errorextra"); + return; + } + + if (this.update.unsupported) { + aCallback("unsupported"); + return; + } + + var p = this.update.selectedPatch; + if (p) { + let state = p.state; + let patchFailed = this.update.getProperty("patchingFailed"); + if (patchFailed) { + if (patchFailed != "partial" || this.update.patchCount != 2) { + // 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. This also + // handles the case when an elevation was cancelled on Mac OS X. + state = STATE_DOWNLOAD_FAILED; + } else { + // 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; + } + } + + // Now select the best page to start with, given the current state of + // the Update. + switch (state) { + case STATE_PENDING: + case STATE_PENDING_SERVICE: + case STATE_PENDING_ELEVATE: + case STATE_APPLIED: + case STATE_APPLIED_SERVICE: + 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; + } + } + + let aus = CoC["@mozilla.org/updates/update-service;1"]. + getService(CoI.nsIApplicationUpdateService); + if (!aus.canApplyUpdates) { + aCallback("manualUpdate"); + return; + } + + aCallback(this.updatesFoundPageId); + return; + } + } + else { + var um = CoC["@mozilla.org/updates/update-manager;1"]. + getService(CoI.nsIUpdateManager); + if (um.activeUpdate) { + this.setUpdate(um.activeUpdate); + aCallback("downloading"); + return; + } + } + aCallback("checking"); + }, + + /** + * 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 = "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(PREFBRANCH_APP_UPDATE_NEVER); + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ELEVATE_NEVER)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_ELEVATE_NEVER); + } + + // 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); + } + + // The preference will be set back to true if the system is still + // unsupported. + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED); + } + + 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 + */ + 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 (gUpdates.update.unsupported) { + gUpdates.wiz.goTo("unsupported"); + return; + } + + if (!aus.canApplyUpdates || gUpdates.update.elevationFailure) { + // Prevent multiple notifications for the same update when the user is + // unable to apply updates. + gUpdates.never(); + gUpdates.wiz.goTo("manualUpdate"); + return; + } + + 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); + 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 "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 (Services.prefs.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 "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_URL_MANUAL); + var manualUpdateLinkLabel = document.getElementById("manualUpdateLinkLabel"); + manualUpdateLinkLabel.value = manualURL; + manualUpdateLinkLabel.setAttribute("url", manualURL); + + gUpdates.setButtons(null, null, "okButton", true); + gUpdates.wiz.getButton("finish").focus(); + } +}; + +/** + * The "System Unsupported" page. Provides the user with information about their + * system no longer being supported and an url for more information. + */ +var gUnsupportedPage = { + onPageShow: function() { + Services.prefs.setBoolPref(PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED, true); + if (gUpdates.update.detailsURL) { + let unsupportedLinkLabel = document.getElementById("unsupportedLinkLabel"); + unsupportedLinkLabel.setAttribute("url", gUpdates.update.detailsURL); + } + + 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 "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, + + /** + * Have we registered an observer for a background update being staged + */ + _updateApplyingObserver: 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); + + // It's possible the update has already been downloaded and is being + // applied by the time this page is shown, depending on how fast the + // download goes and how quickly the 'next' button is clicked to get here. + if (activeUpdate.state == STATE_PENDING || + activeUpdate.state == STATE_PENDING_ELEVATE || + activeUpdate.state == STATE_PENDING_SERVICE) { + if (!activeUpdate.getProperty("stagingFailed")) { + gUpdates.setButtons("hideButton", null, null, false); + gUpdates.wiz.getButton("extra1").focus(); + + this._setUpdateApplying(); + return; + } + + gUpdates.wiz.goTo("finished"); + return; + } + } + + 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.cleanUp(); + gUpdates.wiz.goTo("errors"); + return; + } + // 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); + } + }, + + /** + * Wait for an update being staged in the background. + */ + _setUpdateApplying: function() { + this._downloadProgress.mode = "undetermined"; + this._pauseButton.hidden = true; + let applyingStatus = gUpdates.getAUSString("applyingUpdate"); + this._setStatus(applyingStatus); + + Services.obs.addObserver(this, "update-staged", false); + this._updateApplyingObserver = true; + }, + + /** + * Clean up the listener and observer registered for the wizard. + */ + cleanUp: function() { + var aus = CoC["@mozilla.org/updates/update-service;1"]. + getService(CoI.nsIApplicationUpdateService); + aus.removeDownloadListener(this); + + if (this._updateApplyingObserver) { + Services.obs.removeObserver(this, "update-staged"); + this._updateApplyingObserver = false; + } + }, + + /** + * 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.cleanUp(); + }, + + /** + * 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.cleanUp(); + + 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_CORRUPTED_CONTENT: + 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.cleanUp(); + 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"); + // If the background update pref is set, we should wait until the update + // is actually staged in the background. + let aus = CoC["@mozilla.org/updates/update-service;1"]. + getService(CoI.nsIApplicationUpdateService); + if (aus.canStageUpdates) { + this._setUpdateApplying(); + } else { + this.cleanUp(); + gUpdates.wiz.goTo("finished"); + } + break; + default: + LOG("gDownloadingPage", "onStopRequest - transfer failed"); + // Some kind of transfer error, die. + this.cleanUp(); + gUpdates.wiz.goTo("errors"); + break; + } + }, + + /** + * See nsIObserver.idl + */ + observe: function(aSubject, aTopic, aData) { + if (aTopic == "update-staged") { + if (aData == STATE_DOWNLOADING) { + // We've fallen back to downloding the full update because the + // partial update failed to get staged in the background. + this._setStatus("downloading"); + return; + } + this.cleanUp(); + if (aData == STATE_APPLIED || + aData == STATE_APPLIED_SERVICE || + aData == STATE_PENDING || + aData == STATE_PENDING_SERVICE || + aData == STATE_PENDING_ELEVATE) { + // If the update is successfully applied, or if the updater has + // fallen back to non-staged updates, go to the finish page. + gUpdates.wiz.goTo("finished"); + } else { + gUpdates.wiz.goTo("errors"); + } + } + }, + + /** + * See nsISupports.idl + */ + QueryInterface: function(iid) { + if (!iid.equals(CoI.nsIRequestObserver) && + !iid.equals(CoI.nsIProgressEventSink) && + !iid.equals(CoI.nsIObserver) && + !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_URL_MANUAL); + 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_BACKGROUNDERRORS)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_BACKGROUNDERRORS); + } + + document.getElementById("genericBackgroundErrorLabel").hidden = false; + let manualURL = Services.urlFormatter.formatURLPref(PREF_APP_UPDATE_URL_MANUAL); + let errorLinkLabel = document.getElementById("errorExtraLinkLabel"); + errorLinkLabel.value = manualURL; + errorLinkLabel.setAttribute("url", manualURL); + } +}; + +/** + * 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_APPLIED: + case STATE_APPLIED_SERVICE: + gUpdates.wiz.goTo("finished"); + break; + case STATE_PENDING: + case STATE_PENDING_SERVICE: + let aus = CoC["@mozilla.org/updates/update-service;1"]. + getService(CoI.nsIApplicationUpdateService); + if (!aus.canStageUpdates) { + gUpdates.wiz.goTo("finished"); + break; + } + // intentional fallthrough + 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() { + let aus = CoC["@mozilla.org/updates/update-service;1"]. + getService(CoI.nsIApplicationUpdateService); + if (aus.elevationRequired) { + LOG("gFinishedPage", "elevationRequired"); + gUpdates.setButtons("restartLaterButton", "noThanksButton", + "restartNowButton", true); + } else { + LOG("gFinishedPage", "not elevationRequired"); + gUpdates.setButtons("restartLaterButton", null, "restartNowButton", + true); + } + gUpdates.wiz.getButton("finish").focus(); + }, + + /** + * Initialize the Wizard Page for a Background Source Event + */ + onPageShowBackground: function() { + this.onPageShow(); + let updateFinishedName = document.getElementById("updateFinishedName"); + updateFinishedName.value = gUpdates.update.name; + + let 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; + } + let aus = CoC["@mozilla.org/updates/update-service;1"]. + getService(CoI.nsIApplicationUpdateService); + if (aus.elevationRequired) { + let more = document.getElementById("finishedBackgroundMore"); + more.setAttribute("hidden", "true"); + let moreElevated = + document.getElementById("finishedBackgroundMoreElevated"); + moreElevated.setAttribute("hidden", "false"); + let moreElevatedLink = + document.getElementById("finishedBackgroundMoreElevatedLink"); + moreElevatedLink.setAttribute("hidden", "false"); + let moreElevatedLinkLabel = + document.getElementById("finishedBackgroundMoreElevatedLinkLabel"); + let manualURL = Services.urlFormatter.formatURLPref(PREF_APP_UPDATE_URL_MANUAL); + moreElevatedLinkLabel.value = manualURL; + moreElevatedLinkLabel.setAttribute("url", manualURL); + moreElevatedLinkLabel.setAttribute("hidden", "false"); + } + + if (Services.prefs.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"); + + let aus = CoC["@mozilla.org/updates/update-service;1"]. + getService(CoI.nsIApplicationUpdateService); + if (aus.elevationRequired) { + let um = CoC["@mozilla.org/updates/update-manager;1"]. + getService(CoI.nsIUpdateManager); + if (um) { + um.elevationOptedIn(); + } + } + + // 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() { + gUpdates.wiz.cancel(); + }, + + /** + * When elevation is required and the user clicks "No Thanks" in the wizard. + */ + onExtra2: Task.async(function*() { + Services.obs.notifyObservers(null, "update-canceled", null); + let um = CoC["@mozilla.org/updates/update-manager;1"]. + getService(CoI.nsIUpdateManager); + um.cleanupActiveUpdate(); + gUpdates.never(); + gUpdates.wiz.cancel(); + }), +}; + +/** + * 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); +} |