DXR is a code search and navigation tool aimed at making sense of large projects. It supports full-text and regex searches as well as structural queries.

Mercurial (dcc6d7a0dc00)

VCS Links

Line Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333
/* 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";
// Unified Telemetry status.
const PREF_TELEMETRY_UNIFIED = "toolkit.telemetry.unified";
// 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";

// Whether the FHR/Telemetry unification features are enabled.
// Changing this pref requires a restart.
const IS_UNIFIED_TELEMETRY = Preferences.get(PREF_TELEMETRY_UNIFIED, false);

var gLogAppenderDump = null;

this.SelfSupportBackend = Object.freeze({
  init: function () {
    SelfSupportBackendInternal.init();
  },

  uninit: function () {
    SelfSupportBackendInternal.uninit();
  },
});

var 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 either FHR or Unified Telemetry is enabled.
    let reportingEnabled = Preferences.get(PREF_FHR_ENABLED, false) || IS_UNIFIED_TELEMETRY;
    if (!reportingEnabled) {
      this._log.config("init - Disabling SelfSupport because FHR and Unified Telemetry are 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);
    if (!url.startsWith("https:")) {
      this._log.error("_loadSelfSupport - Non HTTPS URL provided: " + url);
      return;
    }

    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]),
};