Bug 1111022 - Load self-support page in a hidden tab. r=ttaubert, a=lmandel
authorAlessio Placitelli <alessio.placitelli@gmail.com>
Tue, 27 Jan 2015 08:11:00 +0100
changeset 250113 41fd4a4df8ac
parent 250112 de7488acda8a
child 250114 e7a0f9cd3482
push id4503
push userryanvm@gmail.com
push date2015-02-27 21:34 +0000
treeherdermozilla-beta@87d76aead804 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersttaubert, lmandel
bugs1111022
milestone37.0
Bug 1111022 - Load self-support page in a hidden tab. r=ttaubert, a=lmandel
browser/app/profile/firefox.js
browser/components/nsBrowserGlue.js
browser/modules/SelfSupportBackend.jsm
browser/modules/moz.build
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1022,16 +1022,18 @@ pref("browser.EULA.version", 3);
 pref("browser.rights.version", 3);
 pref("browser.rights.3.shown", false);
 
 #ifdef DEBUG
 // Don't show the about:rights notification in debug builds.
 pref("browser.rights.override", true);
 #endif
 
+pref("browser.selfsupport.url", "http://self-repair.mozilla.org/%LOCALE%/repair");
+
 pref("browser.sessionstore.resume_from_crash", true);
 pref("browser.sessionstore.resume_session_once", false);
 
 // minimal interval between two save operations in milliseconds
 pref("browser.sessionstore.interval", 15000);
 // on which sites to save text data, POSTDATA and cookies
 // 0 = everywhere, 1 = unencrypted sites, 2 = nowhere
 pref("browser.sessionstore.privacy_level", 0);
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -80,16 +80,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/osfile.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "RemotePrompt",
                                   "resource:///modules/RemotePrompt.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ContentPrefServiceParent",
                                   "resource://gre/modules/ContentPrefServiceParent.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "SelfSupportBackend",
+                                  "resource:///modules/SelfSupportBackend.jsm");
+
 XPCOMUtils.defineLazyModuleGetter(this, "SessionStore",
                                   "resource:///modules/sessionstore/SessionStore.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry",
                                   "resource:///modules/BrowserUITelemetry.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
                                   "resource://gre/modules/AsyncShutdown.jsm");
@@ -589,16 +592,18 @@ BrowserGlue.prototype = {
     ContentSearch.init();
     FormValidationHandler.init();
 
     RemotePrompt.init();
     ContentPrefServiceParent.init();
 
     LoginManagerParent.init();
 
+    SelfSupportBackend.init();
+
 #ifdef NIGHTLY_BUILD
     Services.prefs.addObserver(POLARIS_ENABLED, this, false);
 #endif
 
 #ifdef MOZ_DEBUG_UA
     UserAgentOverrides.init();
     DebugUserAgent.init();
 #endif
@@ -815,16 +820,18 @@ BrowserGlue.prototype = {
     try {
       let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"]
                          .getService(Ci.nsIAppStartup);
       appStartup.trackStartupCrashEnd();
     } catch (e) {
       Cu.reportError("Could not end startup crash tracking in quit-application-granted: " + e);
     }
 
+    SelfSupportBackend.uninit();
+
     CustomizationTabPreloader.uninit();
     WebappManager.uninit();
 #ifdef NIGHTLY_BUILD
     if (Services.prefs.getBoolPref("dom.identity.enabled")) {
       SignInToWebsiteUX.uninit();
     }
 #endif
 #ifdef MOZ_DEBUG_UA
new file mode 100644
--- /dev/null
+++ b/browser/modules/SelfSupportBackend.jsm
@@ -0,0 +1,323 @@
+/* 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";
+
+this.EXPORTED_SYMBOLS = ["SelfSupportBackend"];
+
+const Cu = Components.utils;
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Timer.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "HiddenFrame",
+  "resource:///modules/HiddenFrame.jsm");
+
+// Enables or disables the Self Support.
+const PREF_ENABLED = "browser.selfsupport.enabled";
+// Url to open in the Self Support browser, in the urlFormatter service format.
+const PREF_URL = "browser.selfsupport.url";
+// FHR status.
+const PREF_FHR_ENABLED = "datareporting.healthreport.service.enabled";
+// UITour status.
+const PREF_UITOUR_ENABLED = "browser.uitour.enabled";
+
+// Controls the interval at which the self support page tries to reload in case of
+// errors.
+const RETRY_INTERVAL_MS = 30000;
+// Maximum number of SelfSupport page load attempts in case of failure.
+const MAX_RETRIES = 5;
+// The delay after which to load the self-support, at startup.
+const STARTUP_DELAY_MS = 5000;
+
+const LOGGER_NAME = "Browser.SelfSupportBackend";
+const PREF_BRANCH_LOG = "browser.selfsupport.log.";
+const PREF_LOG_LEVEL = PREF_BRANCH_LOG + "level";
+const PREF_LOG_DUMP = PREF_BRANCH_LOG + "dump";
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+const UITOUR_FRAME_SCRIPT = "chrome://browser/content/content-UITour.js";
+
+let gLogAppenderDump = null;
+
+this.SelfSupportBackend = Object.freeze({
+  init: function () {
+    SelfSupportBackendInternal.init();
+  },
+
+  uninit: function () {
+    SelfSupportBackendInternal.uninit();
+  },
+});
+
+let SelfSupportBackendInternal = {
+  // The browser element that will load the SelfSupport page.
+  _browser: null,
+  // The Id of the timer triggering delayed SelfSupport page load.
+  _delayedLoadTimerId: null,
+  // The HiddenFrame holding the _browser element.
+  _frame: null,
+  _log: null,
+  _progressListener: null,
+
+  /**
+   * Initializes the self support backend.
+   */
+  init: function () {
+    this._configureLogging();
+
+    this._log.trace("init");
+
+    Preferences.observe(PREF_BRANCH_LOG, this._configureLogging, this);
+
+    // Only allow to use SelfSupport if FHR is enabled.
+    let fhrEnabled = Preferences.get(PREF_FHR_ENABLED, false);
+    if (!fhrEnabled) {
+      this._log.config("init - Disabling SelfSupport because Health Report is disabled.");
+      return;
+    }
+
+    // Make sure UITour is enabled.
+    let uiTourEnabled = Preferences.get(PREF_UITOUR_ENABLED, false);
+    if (!uiTourEnabled) {
+      this._log.config("init - Disabling SelfSupport because UITour is disabled.");
+      return;
+    }
+
+    // Check the preferences to see if we want this to be active.
+    if (!Preferences.get(PREF_ENABLED, true)) {
+      this._log.config("init - SelfSupport is disabled.");
+      return;
+    }
+
+    Services.obs.addObserver(this, "sessionstore-windows-restored", false);
+  },
+
+  /**
+   * Shut down the self support backend, if active.
+   */
+  uninit: function () {
+    this._log.trace("uninit");
+
+    Preferences.ignore(PREF_BRANCH_LOG, this._configureLogging, this);
+
+    // Cancel delayed loading, if still active, when shutting down.
+    clearTimeout(this._delayedLoadTimerId);
+
+    // Dispose of the hidden browser.
+    if (this._browser !== null) {
+      if (this._browser.contentWindow) {
+        this._browser.contentWindow.removeEventListener("DOMWindowClose", this, true);
+      }
+
+      if (this._progressListener) {
+        this._browser.removeProgressListener(this._progressListener);
+        this._progressListener.destroy();
+        this._progressListener = null;
+      }
+
+      this._browser.remove();
+      this._browser = null;
+    }
+
+    if (this._frame) {
+      this._frame.destroy();
+      this._frame = null;
+    }
+  },
+
+  /**
+   * Handle notifications. Once all windows are created, we wait a little bit more
+   * since tabs might still be loading. Then, we open the self support.
+   */
+  observe: function (aSubject, aTopic, aData) {
+    this._log.trace("observe - Topic " + aTopic);
+
+    if (aTopic === "sessionstore-windows-restored") {
+      Services.obs.removeObserver(this, "sessionstore-windows-restored");
+      this._delayedLoadTimerId = setTimeout(this._loadSelfSupport.bind(this), STARTUP_DELAY_MS);
+    }
+  },
+
+  /**
+   * Configure the logger based on the preferences.
+   */
+  _configureLogging: function() {
+    if (!this._log) {
+      this._log = Log.repository.getLogger(LOGGER_NAME);
+
+      // Log messages need to go to the browser console.
+      let consoleAppender = new Log.ConsoleAppender(new Log.BasicFormatter());
+      this._log.addAppender(consoleAppender);
+    }
+
+    // Make sure the logger keeps up with the logging level preference.
+    this._log.level = Log.Level[Preferences.get(PREF_LOG_LEVEL, "Warn")];
+
+    // If enabled in the preferences, add a dump appender.
+    let logDumping = Preferences.get(PREF_LOG_DUMP, false);
+    if (logDumping != !!gLogAppenderDump) {
+      if (logDumping) {
+        gLogAppenderDump = new Log.DumpAppender(new Log.BasicFormatter());
+        this._log.addAppender(gLogAppenderDump);
+      } else {
+        this._log.removeAppender(gLogAppenderDump);
+        gLogAppenderDump = null;
+      }
+    }
+  },
+
+  /**
+   * Create an hidden frame to host our |browser|, then load the SelfSupport page in it.
+   * @param aURL The URL to load in the browser.
+   */
+  _makeHiddenBrowser: function(aURL) {
+    this._frame = new HiddenFrame();
+    return this._frame.get().then(aFrame => {
+      let doc = aFrame.document;
+
+      this._browser = doc.createElementNS(XUL_NS, "browser");
+      this._browser.setAttribute("type", "content");
+      this._browser.setAttribute("disableglobalhistory", "true");
+      this._browser.setAttribute("src", aURL);
+
+      doc.documentElement.appendChild(this._browser);
+    });
+  },
+
+  handleEvent: function(aEvent) {
+    this._log.trace("handleEvent - aEvent.type " + aEvent.type + ", Trusted " + aEvent.isTrusted);
+
+    if (aEvent.type === "DOMWindowClose") {
+      let window = this._browser.contentDocument.defaultView;
+      let target = aEvent.target;
+
+      if (target == window) {
+        // preventDefault stops the default window.close(). We need to do that to prevent
+        // Services.appShell.hiddenDOMWindow from being destroyed.
+        aEvent.preventDefault();
+
+        this.uninit();
+      }
+    }
+  },
+
+  /**
+   * Called when the self support page correctly loads.
+   */
+  _pageSuccessCallback: function() {
+    this._log.debug("_pageSuccessCallback - Page correctly loaded.");
+    this._browser.removeProgressListener(this._progressListener);
+    this._progressListener.destroy();
+    this._progressListener = null;
+
+    // Allow SelfSupportBackend to catch |window.close()| issued by the content.
+    this._browser.contentWindow.addEventListener("DOMWindowClose", this, true);
+  },
+
+  /**
+   * Called when the self support page fails to load.
+   */
+  _pageLoadErrorCallback: function() {
+    this._log.info("_pageLoadErrorCallback - Too many failed load attempts. Giving up.");
+    this.uninit();
+  },
+
+  /**
+   * Create a browser and attach it to an hidden window. The browser will contain the
+   * self support page and attempt to load the page content. If loading fails, try again
+   * after an interval.
+   */
+  _loadSelfSupport: function() {
+    // Fetch the Self Support URL from the preferences.
+    let unformattedURL = Preferences.get(PREF_URL, null);
+    let url = Services.urlFormatter.formatURL(unformattedURL);
+
+    this._log.config("_loadSelfSupport - URL " + url);
+
+    // Create the hidden browser.
+    this._makeHiddenBrowser(url).then(() => {
+      // Load UITour frame script.
+      this._browser.messageManager.loadFrameScript(UITOUR_FRAME_SCRIPT, true);
+
+      // We need to watch for load errors as well and, in case, try to reload
+      // the self support page.
+      const webFlags = Ci.nsIWebProgress.NOTIFY_STATE_WINDOW |
+                       Ci.nsIWebProgress.NOTIFY_STATE_REQUEST |
+                       Ci.nsIWebProgress.NOTIFY_LOCATION;
+
+      this._progressListener = new ProgressListener(() => this._pageLoadErrorCallback(),
+                                                    () => this._pageSuccessCallback());
+
+      this._browser.addProgressListener(this._progressListener, webFlags);
+    });
+  }
+};
+
+/**
+ * A progress listener object which notifies of page load error and load success
+ * through callbacks. When the page fails to load, the progress listener tries to
+ * reload it up to MAX_RETRIES times. The page is not loaded again immediately, but
+ * after a timeout.
+ *
+ * @param aLoadErrorCallback Called when a page failed to load MAX_RETRIES times.
+ * @param aLoadSuccessCallback Called when a page correctly loads.
+ */
+function ProgressListener(aLoadErrorCallback, aLoadSuccessCallback) {
+  this._loadErrorCallback = aLoadErrorCallback;
+  this._loadSuccessCallback = aLoadSuccessCallback;
+  // The number of page loads attempted.
+  this._loadAttempts = 0;
+  this._log = Log.repository.getLogger(LOGGER_NAME);
+  // The Id of the timer which triggers page load again in case of errors.
+  this._reloadTimerId = null;
+}
+
+ProgressListener.prototype = {
+  onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) {
+    if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
+      this._log.warn("onLocationChange - There was a problem fetching the SelfSupport URL (attempt " +
+                     this._loadAttempts + ").");
+
+      // Increase the number of attempts and bail out if we failed too many times.
+      this._loadAttempts++;
+      if (this._loadAttempts > MAX_RETRIES) {
+        this._loadErrorCallback();
+        return;
+      }
+
+      // Reload the page after the retry interval expires. The interval is multiplied
+      // by the number of attempted loads, so that it takes a bit more to try to reload
+      // when frequently failing.
+      this._reloadTimerId = setTimeout(() => {
+        this._log.debug("onLocationChange - Reloading SelfSupport URL in the hidden browser.");
+        aWebProgress.DOMWindow.location.reload();
+      }, RETRY_INTERVAL_MS * this._loadAttempts);
+    }
+  },
+
+  onStateChange: function (aWebProgress, aRequest, aFlags, aStatus) {
+    if (aFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+        aFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
+        aFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW &&
+        Components.isSuccessCode(aStatus)) {
+      this._loadSuccessCallback();
+    }
+  },
+
+  destroy: function () {
+    // Make sure we don't try to reload self support when shutting down.
+    clearTimeout(this._reloadTimerId);
+  },
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
+                                         Ci.nsISupportsWeakReference]),
+};
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -23,16 +23,17 @@ EXTRA_JS_MODULES += [
     'Feeds.jsm',
     'FormSubmitObserver.jsm',
     'FormValidationHandler.jsm',
     'HiddenFrame.jsm',
     'NetworkPrioritizer.jsm',
     'offlineAppCache.jsm',
     'PanelFrame.jsm',
     'RemotePrompt.jsm',
+    'SelfSupportBackend.jsm',
     'SitePermissions.jsm',
     'Social.jsm',
     'TabCrashReporter.jsm',
     'WebappManager.jsm',
 ]
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows':
     EXTRA_JS_MODULES += [