Main patch - Bug 596813 - Check for updates inside the About window without opening a new window. r=dtownsend, ui-r=beltzner, a=blocking2.0-beta7
authorRobert Strong <robert.bugzilla@gmail.com>
Thu, 23 Sep 2010 21:02:08 -0700
changeset 54611 78c90846f8c76e57d7ef25216bc33e12b0e822d0
parent 54610 7cd8c313dfc5d9651bf34494a521fb1f8f6e1ea5
child 54612 c7ed283dda27f26e020bed851a77ab34ecdce41b
push id15953
push userrstrong@mozilla.com
push dateFri, 24 Sep 2010 04:02:44 +0000
treeherdermozilla-central@78c90846f8c7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdtownsend, beltzner, blocking2.0-beta7
bugs596813
milestone2.0b7pre
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Main patch - Bug 596813 - Check for updates inside the About window without opening a new window. r=dtownsend, ui-r=beltzner, a=blocking2.0-beta7
browser/app/profile/firefox.js
browser/base/content/aboutDialog.css
browser/base/content/aboutDialog.js
browser/base/content/aboutDialog.xul
toolkit/mozapps/update/content/updates.js
toolkit/mozapps/update/nsUpdateService.js
toolkit/xre/nsUpdateDriver.cpp
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -83,16 +83,21 @@ pref("browser.dictionaries.download.url"
 //           default=10 minutes
 pref("app.update.timer", 600000);
 
 // App-specific update preferences
 
 // The interval to check for updates (app.update.interval) is defined in
 // firefox-branding.js
 
+// Alternative windowtype for an application update user interface window. When
+// a window with this windowtype is open the application update service won't
+// open the normal application update user interface window.
+pref("app.update.altwindowtype", "Browser:About");
+
 // Enables some extra Application Update Logging (can reduce performance)
 pref("app.update.log", false);
 
 // The number of general background check failures to allow before notifying the
 // user of the failure. User initiated update checks always notify the user of
 // the failure.
 pref("app.update.backgroundMaxErrors", 10);
 
--- a/browser/base/content/aboutDialog.css
+++ b/browser/base/content/aboutDialog.css
@@ -47,23 +47,44 @@
 #distribution,
 #distributionId {
   font-weight: bold;
   display: none;
   margin-top: 0;
   margin-bottom: 0;
 }
 
-#checkForUpdatesButton,
 .text-blurb {
   margin-bottom: 10px;
   -moz-margin-start: 0;
   -moz-padding-start: 0;
 }
 
+#updateBox {
+  margin-bottom: 10px;
+}
+
+#updateButton,
+#updateDeck > hbox > label {
+  -moz-margin-start: 0;
+  -moz-padding-start: 0;
+}
+
+#updateDeck > hbox > label:not([class="text-link"]) {
+  color: #909090;
+  font-style:italic;
+}
+
+.update-throbber {
+  width: 16px;
+  min-height: 16px;
+  -moz-margin-end: 3px;
+  list-style-image: url("chrome://global/skin/icons/loading_16.png");
+}
+
 .trademark-label,
 .text-link,
 .text-link:focus {
   margin: 0px;
   padding: 0px;
 }
 
 .bottom-link,
--- a/browser/base/content/aboutDialog.js
+++ b/browser/base/content/aboutDialog.js
@@ -16,16 +16,17 @@
 # The Initial Developer of the Original Code is
 # Blake Ross (blaker@netscape.com).
 # Portions created by the Initial Developer are Copyright (C) 2002
 # the Initial Developer. All Rights Reserved.
 #
 # Contributor(s):
 #   Ehsan Akhgari <ehsan.akhgari@gmail.com>
 #   Margaret Leibovic <margaret.leibovic@gmail.com>
+#   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
@@ -60,30 +61,478 @@ function init(aEvent)
       distroIdField.style.display = "block";
     }
   }
   catch (e) {
     // Pref is unset
   }
 
 #ifdef MOZ_UPDATER
-  initUpdates();
+  gAppUpdater = new appUpdater();
 #endif
 
 #ifdef XP_MACOSX
   // it may not be sized at this point, and we need its width to calculate its position
   window.sizeToContent();
   window.moveTo((screen.availWidth / 2) - (window.outerWidth / 2), screen.availHeight / 5);
 #endif
 }
 
 #ifdef MOZ_UPDATER
-/**
- * Sets up "Check for Updates..." button.
- */
-function initUpdates()
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/DownloadUtils.jsm");
+Components.utils.import("resource://gre/modules/AddonManager.jsm");
+
+var gAppUpdater;
+
+function onUnload(aEvent) {
+  if (gAppUpdater.isChecking)
+    gAppUpdater.checker.stopChecking(Components.interfaces.nsIUpdateChecker.CURRENT_CHECK);
+  // Safe to call even when there isn't a download in progress.
+  gAppUpdater.removeDownloadListener();
+  gAppUpdater = null;
+}
+
+
+function appUpdater()
+{
+  this.updateDeck = document.getElementById("updateDeck");
+
+  // Hide the update deck when there is already an update window open to avoid
+  // syncing issues between them.
+  if (Services.wm.getMostRecentWindow("Update:Wizard")) {
+    this.updateDeck.hidden = true;
+    return;
+  }
+
+  XPCOMUtils.defineLazyServiceGetter(this, "aus",
+                                     "@mozilla.org/updates/update-service;1",
+                                     "nsIApplicationUpdateService");
+  XPCOMUtils.defineLazyServiceGetter(this, "checker",
+                                     "@mozilla.org/updates/update-checker;1",
+                                     "nsIUpdateChecker");
+  XPCOMUtils.defineLazyServiceGetter(this, "um",
+                                     "@mozilla.org/updates/update-manager;1",
+                                     "nsIUpdateManager");
+  XPCOMUtils.defineLazyServiceGetter(this, "bs",
+                                     "@mozilla.org/extensions/blocklist;1",
+                                     "nsIBlocklistService");
+
+  this.bundle = Services.strings.
+                createBundle("chrome://browser/locale/browser.properties");
+
+  this.updateBtn = document.getElementById("updateButton");
+
+  // The button label value must be set so its height is correct.
+  this.setupUpdateButton("update.checkInsideButton");
+
+  let manualURL = Services.urlFormatter.formatURLPref("app.update.url.manual");
+  let manualLink = document.getElementById("manualLink");
+  manualLink.value = manualURL;
+  manualLink.href = manualURL;
+  document.getElementById("failedLink").href = manualURL;
+
+  if (this.updateDisabledAndLocked) {
+    this.selectPanel("adminDisabled");
+    return;
+  }
+
+  if (this.isPending) {
+    this.setupUpdateButton("update.restart." +
+                           (this.isMajor ? "upgradeButton" : "applyButton"));
+    return;
+  }
+
+  if (this.isDownloading) {
+    this.startDownload();
+    return;
+  }
+
+  if (this.updateEnabled && this.updateAuto) {
+    this.selectPanel("checkingForUpdates");
+    this.isChecking = true;
+    this.checker.checkForUpdates(this.updateCheckListener, true);
+    return;
+  }
+}
+
+appUpdater.prototype =
 {
-  var browserBundle = Services.strings.
-                      createBundle("chrome://browser/locale/browser.properties");
-  var checkForUpdates = document.getElementById("checkForUpdatesButton");
-  setupCheckForUpdates(checkForUpdates, browserBundle);
-}
+  // true when there is an update check in progress.
+  isChecking: false,
+
+  // true when there is an update already staged / ready to be applied.
+  get isPending() {
+    if (this.update)
+      return this.update.state == "pending";
+    return this.um.activeUpdate && this.um.activeUpdate.state == "pending";
+  },
+
+  // true when there is an update download in progress.
+  get isDownloading() {
+    if (this.update)
+      return this.update.state == "downloading";
+    return this.um.activeUpdate &&
+           this.um.activeUpdate.state == "downloading";
+  },
+
+  // true when the update type is major.
+  get isMajor() {
+    if (this.update)
+      return this.update.type == "major";
+    return this.um.activeUpdate.type == "major";
+  },
+
+  // true when updating is disabled by an administrator.
+  get updateDisabledAndLocked() {
+    return !this.updateEnabled &&
+           Services.prefs.prefIsLocked("app.update.enabled");
+  },
+
+  // true when updating is enabled.
+  get updateEnabled() {
+    try {
+      return Services.prefs.getBoolPref("app.update.enabled");
+    }
+    catch (e) { }
+    return true; // Firefox default is true
+  },
+
+  // true when updating is automatic.
+  get updateAuto() {
+    try {
+      return Services.prefs.getBoolPref("app.update.auto");
+    }
+    catch (e) { }
+    return true; // Firefox default is true
+  },
+
+  /**
+   * Sets the deck's selected panel.
+   *
+   * @param  aChildID
+   *         The id of the deck's child to select.
+   */
+  selectPanel: function(aChildID) {
+    this.updateDeck.selectedPanel = document.getElementById(aChildID);
+  },
+
+  /**
+   * Sets the update button's label and accesskey.
+   *
+   * @param  aKeyPrefix
+   *         The prefix for the properties file entry to use for setting the
+   *         label and accesskey.
+   */
+  setupUpdateButton: function(aKeyPrefix) {
+    this.updateBtn.label = this.bundle.GetStringFromName(aKeyPrefix + ".label");
+    this.updateBtn.accessKey = this.bundle.GetStringFromName(aKeyPrefix + ".accesskey");
+    if (!document.commandDispatcher.focusedElement ||
+        document.commandDispatcher.focusedElement.isSameNode(this.updateBtn))
+      this.updateBtn.focus();
+  },
+
+  /**
+   * Handles oncommand for the update button.
+   */
+  buttonOnCommand: function() {
+    if (this.isPending) {
+      // Notify all windows that an application quit has been requested.
+      let cancelQuit = Components.classes["@mozilla.org/supports-PRBool;1"].
+                       createInstance(Components.interfaces.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 = Components.classes["@mozilla.org/process/environment;1"].
+                  getService(Components.interfaces.nsIEnvironment);
+        env.set("MOZ_SAFE_MODE_RESTART", "1");
+      }
+
+      Components.classes["@mozilla.org/toolkit/app-startup;1"].
+      getService(Components.interfaces.nsIAppStartup).
+      quit(Components.interfaces.nsIAppStartup.eAttemptQuit |
+           Components.interfaces.nsIAppStartup.eRestart);
+      return;
+    }
+
+    const URI_UPDATE_PROMPT_DIALOG = "chrome://mozapps/content/update/updates.xul";
+    // Firefox no longer displays a license for updates and the licenseURL check
+    // is just in case a distibution does.
+    if (this.update && (this.update.billboardURL || this.update.licenseURL ||
+        this.addons.length != 0)) {
+      var ary = null;
+      ary = Components.classes["@mozilla.org/supports-array;1"].
+            createInstance(Components.interfaces.nsISupportsArray);
+      ary.AppendElement(this.update);
+      var openFeatures = "chrome,centerscreen,dialog=no,resizable=no,titlebar,toolbar=no";
+      Services.ww.openWindow(null, URI_UPDATE_PROMPT_DIALOG, "", openFeatures, ary);
+      window.close();
+      return;
+    }
+
+    this.selectPanel("checkingForUpdates");
+    this.isChecking = true;
+    this.checker.checkForUpdates(this.updateCheckListener, true);
+  },
+
+  /**
+   * Implements nsIUpdateCheckListener. The methods implemented by
+   * nsIUpdateCheckListener have to be in a different scope from
+   * nsIIncrementalDownload because both nsIUpdateCheckListener and
+   * nsIIncrementalDownload implement onProgress.
+   */
+  updateCheckListener: {
+    /**
+     * See nsIUpdateService.idl
+     */
+    onProgress: function(aRequest, aPosition, aTotalSize) {
+    },
+
+    /**
+     * See nsIUpdateService.idl
+     */
+    onCheckComplete: function(aRequest, aUpdates, aUpdateCount) {
+      gAppUpdater.isChecking = false;
+      gAppUpdater.update = gAppUpdater.aus.
+                           selectUpdate(aUpdates, aUpdates.length);
+      if (!gAppUpdater.update) {
+        gAppUpdater.selectPanel("noUpdatesFound");
+        return;
+      }
+
+      if (!gAppUpdater.aus.canApplyUpdates) {
+        gAppUpdater.selectPanel("manualUpdate");
+        return;
+      }
+
+      // Firefox no longer displays a license for updates and the licenseURL
+      // check is just in case a distibution does.
+      if (gAppUpdater.update.billboardURL || gAppUpdater.update.licenseURL) {
+        gAppUpdater.selectPanel("updateButtonBox");
+        gAppUpdater.setupUpdateButton("update.openUpdateUI." +
+                                      (this.isMajor ? "upgradeButton"
+                                                    : "applyButton"));
+        return;
+      }
+
+      if (!gAppUpdater.update.appVersion ||
+          Services.vc.compare(gAppUpdater.update.appVersion,
+                              Services.appinfo.version) == 0) {
+        gAppUpdater.startDownload();
+        return;
+      }
+
+      gAppUpdater.checkAddonCompatibility();
+    },
+
+    /**
+     * See nsIUpdateService.idl
+     */
+    onError: function(aRequest, aUpdate) {
+      // Errors in the update check are treated as no updates found. If the
+      // update check fails repeatedly without a success the user will be
+      // notified with the normal app update user interface so this is safe.
+      gAppUpdater.isChecking = false;
+      gAppUpdater.selectPanel("noUpdatesFound");
+      return;
+    },
+
+    /**
+     * See nsISupports.idl
+     */
+    QueryInterface: function(aIID) {
+      if (!aIID.equals(Components.interfaces.nsIUpdateCheckListener) &&
+          !aIID.equals(Components.interfaces.nsISupports))
+        throw Components.results.NS_ERROR_NO_INTERFACE;
+      return this;
+    }
+  },
+
+  /**
+   * Checks the compatibility of add-ons for the application update.
+   */
+  checkAddonCompatibility: function() {
+    var self = this;
+    AddonManager.getAllAddons(function(aAddons) {
+      self.addons = [];
+      self.addonsCheckedCount = 0;
+      aAddons.forEach(function(aAddon) {
+        // 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).
+        if (aAddon.type != "plugin" &&
+            !aAddon.appDisabled && !aAddon.userDisabled &&
+            aAddon.scope != AddonManager.SCOPE_APPLICATION &&
+            aAddon.isCompatible &&
+            !aAddon.isCompatibleWith(self.update.appVersion,
+                                     self.update.platformVersion))
+          self.addons.push(aAddon);
+      });
+      self.addonsTotalCount = self.addons.length;
+      if (self.addonsTotalCount == 0) {
+        self.startDownload();
+        return;
+      }
+
+      self.checkAddonsForUpdates();
+    });
+  },
+
+  /**
+   * Checks if there are updates for add-ons that are incompatible with the
+   * application update.
+   */
+  checkAddonsForUpdates: function() {
+    this.addons.forEach(function(aAddon) {
+      aAddon.findUpdates(this, AddonManager.UPDATE_WHEN_NEW_APP_DETECTED,
+                         this.update.appVersion,
+                         this.update.platformVersion);
+    }, this);
+  },
+
+  /**
+   * See XPIProvider.jsm
+   */
+  onCompatibilityUpdateAvailable: function(aAddon) {
+    for (var i = 0; i < this.addons.length; ++i) {
+      if (this.addons[i].id == aAddon.id) {
+        this.addons.splice(i, 1);
+        break;
+      }
+    }
+  },
+
+  /**
+   * See XPIProvider.jsm
+   */
+  onUpdateAvailable: function(aAddon, aInstall) {
+    if (!this.bs.isAddonBlocklisted(aAddon.id, aInstall.version,
+                                    this.update.appVersion,
+                                    this.update.platformVersion)) {
+      // Compatibility or new version updates mean the same thing here.
+      this.onCompatibilityUpdateAvailable(aAddon);
+    }
+  },
+
+  /**
+   * See XPIProvider.jsm
+   */
+  onUpdateFinished: function(aAddon) {
+    ++this.addonsCheckedCount;
+
+    if (this.addonsCheckedCount < this.addonsTotalCount)
+      return;
+
+    if (this.addons.length == 0) {
+      // Compatibility updates or new version updates were found for all add-ons
+      this.startDownload();
+      return;
+    }
+
+    this.selectPanel("updateButtonBox");
+    this.setupUpdateButton("update.openUpdateUI." +
+                           (this.isMajor ? "upgradeButton" : "applyButton"));
+  },
+
+  /**
+   * Starts the download of an update mar.
+   */
+  startDownload: function() {
+    if (!this.update)
+      this.update = this.um.activeUpdate;
+    this.update.QueryInterface(Components.interfaces.nsIWritablePropertyBag);
+    this.update.setProperty("foregroundDownload", "true");
+
+    this.aus.pauseDownload();
+    let state = this.aus.downloadUpdate(this.update, false);
+    if (state == "failed") {
+      this.selectPanel("downloadFailed");      
+      return;
+    }
+
+    this.downloadStatus = document.getElementById("downloadStatus");
+    this.downloadStatus.value =
+      DownloadUtils.getTransferTotal(0, this.update.selectedPatch.size);
+    this.selectPanel("downloading");
+    this.aus.addDownloadListener(this);
+  },
+
+  removeDownloadListener: function() {
+    this.aus.removeDownloadListener(this);
+  },
+
+  /**
+   * See nsIRequestObserver.idl
+   */
+  onStartRequest: function(aRequest, aContext) {
+  },
+
+  /**
+   * See nsIRequestObserver.idl
+   */
+  onStopRequest: function(aRequest, aContext, aStatusCode) {
+    switch (aStatusCode) {
+    case Components.results.NS_ERROR_UNEXPECTED:
+      if (this.update.selectedPatch.state == "download-failed" &&
+          (this.update.isCompleteUpdate || this.update.patchCount != 2)) {
+        // Verification error of complete patch, informational text is held in
+        // the update object.
+        this.removeDownloadListener();
+        this.selectPanel("downloadFailed");
+        break;
+      }
+      // Verification failed for a partial patch, complete patch is now
+      // downloading so return early and do NOT remove the download listener!
+      break;
+    case Components.results.NS_BINDING_ABORTED:
+      // Do not remove UI listener since the user may resume downloading again.
+      break;
+    case Components.results.NS_OK:
+      this.removeDownloadListener();
+      this.selectPanel("updateButtonBox");
+      this.setupUpdateButton("update.restart." +
+                             (this.isMajor ? "upgradeButton" : "applyButton"));
+      break;
+    default:
+      this.removeDownloadListener();
+      this.selectPanel("downloadFailed");
+      break;
+    }
+
+  },
+
+  /**
+   * See nsIProgressEventSink.idl
+   */
+  onStatus: function(aRequest, aContext, aStatus, aStatusArg) {
+  },
+
+  /**
+   * See nsIProgressEventSink.idl
+   */
+  onProgress: function(aRequest, aContext, aProgress, aProgressMax) {
+    this.downloadStatus.value =
+      DownloadUtils.getTransferTotal(aProgress, aProgressMax);
+  },
+
+  /**
+   * See nsISupports.idl
+   */
+  QueryInterface: function(aIID) {
+    if (!aIID.equals(Components.interfaces.nsIProgressEventSink) &&
+        !aIID.equals(Components.interfaces.nsIRequestObserver) &&
+        !aIID.equals(Components.interfaces.nsISupports))
+      throw Components.results.NS_ERROR_NO_INTERFACE;
+    return this;
+  }
+};
 #endif
--- a/browser/base/content/aboutDialog.xul
+++ b/browser/base/content/aboutDialog.xul
@@ -18,16 +18,17 @@
 # The Initial Developer of the Original Code is
 # Blake Ross (blaker@netscape.com).
 # Portions created by the Initial Developer are Copyright (C) 2002
 # the Initial Developer. All Rights Reserved.
 #
 # Contributor(s):
 #   Ehsan Akhgari <ehsan.akhgari@gmail.com>
 #   Margaret Leibovic <margaret.leibovic@gmail.com>
+#   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
@@ -52,39 +53,67 @@
 <?xul-overlay href="chrome://browser/content/macBrowserOverlay.xul"?>
 #endif
 
 <window xmlns:html="http://www.w3.org/1999/xhtml"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         id="aboutDialog"
         windowtype="Browser:About"
         onload="init(event);"
+#ifdef MOZ_UPDATER
+        onunload="onUnload(event);"
+#endif
 #ifdef XP_MACOSX
         inwindowmenu="false"
 #else
         title="&aboutDialog.title;"
 #endif
         >
 
-  <script type="application/javascript" src="chrome://browser/content/utilityOverlay.js"/>
   <script type="application/javascript" src="chrome://browser/content/aboutDialog.js"/>
 
   <vbox>
     <hbox id="clientBox">
       <vbox id="leftBox" flex="1"/>
       <vbox id="rightBox" flex="1">
 #expand <label id="version" value="__MOZ_APP_VERSION__"/>
         <label id="distribution" class="text-blurb"/>
         <label id="distributionId" class="text-blurb"/>
+        <vbox id="updateBox">
 #ifdef MOZ_UPDATER
-        <hbox>
-          <button id="checkForUpdatesButton" oncommand="checkForUpdates();" align="start"/>
-          <spacer flex="1"/>
-        </hbox>
+          <deck id="updateDeck" orient="vertical">
+            <hbox id="updateButtonBox" align="center">
+              <button id="updateButton" align="start"
+                      oncommand="gAppUpdater.buttonOnCommand();"/>
+              <spacer flex="1"/>
+            </hbox>
+            <hbox id="checkingForUpdates" align="center">
+              <image class="update-throbber"/><label>&update.checkingForUpdates;</label>
+            </hbox>
+            <hbox id="checkingAddonCompat" align="center">
+              <image class="update-throbber"/><label>&update.checkingAddonCompat;</label>
+            </hbox>
+            <hbox id="downloading" align="center">
+              <image class="update-throbber"/><label>&update.downloading.start;</label><label id="downloadStatus"/><label>&update.downloading.end;</label>
+            </hbox>
+            <hbox id="downloadFailed" align="center">
+              <label>&update.failed.start;</label><label id="failedLink" class="text-link">&update.failed.linkText;</label><label>&update.failed.end;</label>
+            </hbox>
+            <hbox id="adminDisabled" align="center">
+              <label>&update.adminDisabled;</label>
+            </hbox>
+            <hbox id="noUpdatesFound" align="center">
+              <label>&update.noUpdatesFound;</label>
+            </hbox>
+            <hbox id="manualUpdate" align="center">
+              <label>&update.manual.start;</label><label id="manualLink" class="text-link"/><label>&update.manual.end;</label>
+            </hbox>
+          </deck>
 #endif
+        </vbox>
         <description class="text-blurb">
           &community.start2;<label class="text-link" href="http://www.mozilla.org/">&community.mozillaLink;</label>&community.middle2;<label class="text-link" href="about:credits">&community.creditsLink;</label>&community.end2;
         </description>
         <description class="text-blurb">
           &contribute.start;<label class="text-link" href="http://www.mozilla.org/contribute/">&contribute.getInvolvedLink;</label>&contribute.end;
         </description>
       </vbox>
     </hbox>
--- a/toolkit/mozapps/update/content/updates.js
+++ b/toolkit/mozapps/update/content/updates.js
@@ -1723,26 +1723,32 @@ var gFinishedPage = {
     // 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 os = CoC["@mozilla.org/observer-service;1"].
-             getService(CoI.nsIObserverService);
     var cancelQuit = CoC["@mozilla.org/supports-PRBool;1"].
                      createInstance(CoI.nsISupportsPRBool);
-    os.notifyObservers(cancelQuit, "quit-application-requested", "restart");
+    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.
--- a/toolkit/mozapps/update/nsUpdateService.js
+++ b/toolkit/mozapps/update/nsUpdateService.js
@@ -46,16 +46,17 @@ Components.utils.import("resource://gre/
 Components.utils.import("resource://gre/modules/FileUtils.jsm");
 Components.utils.import("resource://gre/modules/AddonManager.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm");
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 
+const PREF_APP_UPDATE_ALTWINDOWTYPE       = "app.update.altwindowtype";
 const PREF_APP_UPDATE_AUTO                = "app.update.auto";
 const PREF_APP_UPDATE_BACKGROUND_INTERVAL = "app.update.download.backgroundInterval";
 const PREF_APP_UPDATE_BACKGROUNDERRORS    = "app.update.backgroundErrors";
 const PREF_APP_UPDATE_BACKGROUNDMAXERRORS = "app.update.backgroundMaxErrors";
 const PREF_APP_UPDATE_CERTS_BRANCH        = "app.update.certs.";
 const PREF_APP_UPDATE_CERT_CHECKATTRS     = "app.update.cert.checkAttributes";
 const PREF_APP_UPDATE_CERT_ERRORS         = "app.update.cert.errors";
 const PREF_APP_UPDATE_CERT_MAXERRORS      = "app.update.cert.maxErrors";
@@ -2774,42 +2775,48 @@ Downloader.prototype = {
  */
 function UpdatePrompt() {
 }
 UpdatePrompt.prototype = {
   /**
    * See nsIUpdateService.idl
    */
   checkForUpdates: function UP_checkForUpdates() {
+    if (this._getAltUpdateWindow())
+      return;
+
     this._showUI(null, URI_UPDATE_PROMPT_DIALOG, null, UPDATE_WINDOW_NAME,
                  null, null);
   },
 
   /**
    * See nsIUpdateService.idl
    */
   showUpdateAvailable: function UP_showUpdateAvailable(update) {
     if (getPref("getBoolPref", PREF_APP_UPDATE_SILENT, false) ||
-        this._getUpdateWindow())
+        this._getUpdateWindow() || this._getAltUpdateWindow())
       return;
 
     var stringsPrefix = "updateAvailable_" + update.type + ".";
     var title = gUpdateBundle.formatStringFromName(stringsPrefix + "title",
                                                    [update.name], 1);
     var text = gUpdateBundle.GetStringFromName(stringsPrefix + "text");
     var imageUrl = "";
     this._showUnobtrusiveUI(null, URI_UPDATE_PROMPT_DIALOG, null,
                            UPDATE_WINDOW_NAME, "updatesavailable", update,
                            title, text, imageUrl);
   },
 
   /**
    * See nsIUpdateService.idl
    */
   showUpdateDownloaded: function UP_showUpdateDownloaded(update, background) {
+    if (this._getAltUpdateWindow())
+      return;
+
     if (background) {
       if (getPref("getBoolPref", PREF_APP_UPDATE_SILENT, false))
         return;
 
       var stringsPrefix = "updateDownloaded_" + update.type + ".";
       var title = gUpdateBundle.formatStringFromName(stringsPrefix + "title",
                                                      [update.name], 1);
       var text = gUpdateBundle.GetStringFromName(stringsPrefix + "text");
@@ -2847,17 +2854,18 @@ UpdatePrompt.prototype = {
       Services.ww.openWindow(null, URI_UPDATE_PROMPT_DIALOG, null, openFeatures, arg);
     }
   },
 
   /**
    * See nsIUpdateService.idl
    */
   showUpdateError: function UP_showUpdateError(update) {
-    if (getPref("getBoolPref", PREF_APP_UPDATE_SILENT, false))
+    if (getPref("getBoolPref", PREF_APP_UPDATE_SILENT, false) ||
+        this._getAltUpdateWindow())
       return;
 
     // In some cases, we want to just show a simple alert dialog:
     if (update.state == STATE_FAILED && update.errorCode == WRITE_ERROR) {
       var title = gUpdateBundle.GetStringFromName("updaterIOErrorTitle");
       var text = gUpdateBundle.formatStringFromName("updaterIOErrorMsg",
                                                     [Services.appinfo.name,
                                                      Services.appinfo.name], 2);
@@ -2888,16 +2896,28 @@ UpdatePrompt.prototype = {
   /**
    * Returns the update window if present.
    */
   _getUpdateWindow: function UP__getUpdateWindow() {
     return Services.wm.getMostRecentWindow(UPDATE_WINDOW_NAME);
   },
 
   /**
+   * Returns an alternative update window if present. When a window with this
+   * windowtype is open the application update service won't open the normal
+   * application update user interface window.
+   */
+  _getAltUpdateWindow: function UP__getAltUpdateWindow() {
+    let windowType = getPref("getCharPref", PREF_APP_UPDATE_ALTWINDOWTYPE, null);
+    if (!windowType)
+      return null;
+    return Services.wm.getMostRecentWindow(windowType);
+  },
+
+  /**
    * Initiate a less obtrusive UI, starting with a non-modal notification alert
    * @param   parent
    *          A parent window, can be null
    * @param   uri
    *          The URI string of the dialog to show
    * @param   name
    *          The Window Name of the dialog to show, in case it is already open
    *          and can merely be focused
--- a/toolkit/xre/nsUpdateDriver.cpp
+++ b/toolkit/xre/nsUpdateDriver.cpp
@@ -44,16 +44,17 @@
 #include "nsXULAppAPI.h"
 #include "nsAppRunner.h"
 #include "nsILocalFile.h"
 #include "nsCOMPtr.h"
 #include "nsString.h"
 #include "nsPrintfCString.h"
 #include "prproces.h"
 #include "prlog.h"
+#include "prenv.h"
 #include "nsVersionComparator.h"
 
 #ifdef XP_MACOSX
 #include "nsILocalFileMac.h"
 #include "nsCommandLineServiceMac.h"
 #endif
 
 #if defined(XP_WIN)
@@ -468,16 +469,20 @@ ApplyUpdate(nsIFile *greDir, nsIFile *up
     for (int i = 1; i < appArgc; ++i)
       argv[4 + i] = appArgv[i];
     argv[4 + appArgc] = nsnull;
   } else {
     argv[3] = nsnull;
     argc = 3;
   }
 
+  if (gSafeMode) {
+    PR_SetEnv("MOZ_SAFE_MODE_RESTART=1");
+  }
+
   LOG(("spawning updater process [%s]\n", updaterPath.get()));
 
 #if defined(USE_EXECV)
   chdir(applyToDir.get());
   execv(updaterPath.get(), argv);
 #elif defined(XP_WIN)
   _wchdir(applyToDir.get());