Bumping manifests a=b2g-bump
[gecko.git] / browser / modules / SelfSupportBackend.jsm
blobe9a6e09a15e6874852de4e62a71840204715b1a7
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 this.EXPORTED_SYMBOLS = ["SelfSupportBackend"];
9 const Cu = Components.utils;
10 const Cc = Components.classes;
11 const Ci = Components.interfaces;
13 Cu.import("resource://gre/modules/Log.jsm");
14 Cu.import("resource://gre/modules/Preferences.jsm");
15 Cu.import("resource://gre/modules/Services.jsm");
16 Cu.import("resource://gre/modules/Timer.jsm");
17 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
19 XPCOMUtils.defineLazyModuleGetter(this, "HiddenFrame",
20   "resource:///modules/HiddenFrame.jsm");
22 // Enables or disables the Self Support.
23 const PREF_ENABLED = "browser.selfsupport.enabled";
24 // Url to open in the Self Support browser, in the urlFormatter service format.
25 const PREF_URL = "browser.selfsupport.url";
26 // FHR status.
27 const PREF_FHR_ENABLED = "datareporting.healthreport.service.enabled";
28 // UITour status.
29 const PREF_UITOUR_ENABLED = "browser.uitour.enabled";
31 // Controls the interval at which the self support page tries to reload in case of
32 // errors.
33 const RETRY_INTERVAL_MS = 30000;
34 // Maximum number of SelfSupport page load attempts in case of failure.
35 const MAX_RETRIES = 5;
36 // The delay after which to load the self-support, at startup.
37 const STARTUP_DELAY_MS = 5000;
39 const LOGGER_NAME = "Browser.SelfSupportBackend";
40 const PREF_BRANCH_LOG = "browser.selfsupport.log.";
41 const PREF_LOG_LEVEL = PREF_BRANCH_LOG + "level";
42 const PREF_LOG_DUMP = PREF_BRANCH_LOG + "dump";
44 const HTML_NS = "http://www.w3.org/1999/xhtml";
45 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
47 const UITOUR_FRAME_SCRIPT = "chrome://browser/content/content-UITour.js";
49 let gLogAppenderDump = null;
51 this.SelfSupportBackend = Object.freeze({
52   init: function () {
53     SelfSupportBackendInternal.init();
54   },
56   uninit: function () {
57     SelfSupportBackendInternal.uninit();
58   },
59 });
61 let SelfSupportBackendInternal = {
62   // The browser element that will load the SelfSupport page.
63   _browser: null,
64   // The Id of the timer triggering delayed SelfSupport page load.
65   _delayedLoadTimerId: null,
66   // The HiddenFrame holding the _browser element.
67   _frame: null,
68   _log: null,
69   _progressListener: null,
71   /**
72    * Initializes the self support backend.
73    */
74   init: function () {
75     this._configureLogging();
77     this._log.trace("init");
79     Preferences.observe(PREF_BRANCH_LOG, this._configureLogging, this);
81     // Only allow to use SelfSupport if FHR is enabled.
82     let fhrEnabled = Preferences.get(PREF_FHR_ENABLED, false);
83     if (!fhrEnabled) {
84       this._log.config("init - Disabling SelfSupport because Health Report is disabled.");
85       return;
86     }
88     // Make sure UITour is enabled.
89     let uiTourEnabled = Preferences.get(PREF_UITOUR_ENABLED, false);
90     if (!uiTourEnabled) {
91       this._log.config("init - Disabling SelfSupport because UITour is disabled.");
92       return;
93     }
95     // Check the preferences to see if we want this to be active.
96     if (!Preferences.get(PREF_ENABLED, true)) {
97       this._log.config("init - SelfSupport is disabled.");
98       return;
99     }
101     Services.obs.addObserver(this, "sessionstore-windows-restored", false);
102   },
104   /**
105    * Shut down the self support backend, if active.
106    */
107   uninit: function () {
108     this._log.trace("uninit");
110     Preferences.ignore(PREF_BRANCH_LOG, this._configureLogging, this);
112     // Cancel delayed loading, if still active, when shutting down.
113     clearTimeout(this._delayedLoadTimerId);
115     // Dispose of the hidden browser.
116     if (this._browser !== null) {
117       if (this._browser.contentWindow) {
118         this._browser.contentWindow.removeEventListener("DOMWindowClose", this, true);
119       }
121       if (this._progressListener) {
122         this._browser.removeProgressListener(this._progressListener);
123         this._progressListener.destroy();
124         this._progressListener = null;
125       }
127       this._browser.remove();
128       this._browser = null;
129     }
131     if (this._frame) {
132       this._frame.destroy();
133       this._frame = null;
134     }
135   },
137   /**
138    * Handle notifications. Once all windows are created, we wait a little bit more
139    * since tabs might still be loading. Then, we open the self support.
140    */
141   observe: function (aSubject, aTopic, aData) {
142     this._log.trace("observe - Topic " + aTopic);
144     if (aTopic === "sessionstore-windows-restored") {
145       Services.obs.removeObserver(this, "sessionstore-windows-restored");
146       this._delayedLoadTimerId = setTimeout(this._loadSelfSupport.bind(this), STARTUP_DELAY_MS);
147     }
148   },
150   /**
151    * Configure the logger based on the preferences.
152    */
153   _configureLogging: function() {
154     if (!this._log) {
155       this._log = Log.repository.getLogger(LOGGER_NAME);
157       // Log messages need to go to the browser console.
158       let consoleAppender = new Log.ConsoleAppender(new Log.BasicFormatter());
159       this._log.addAppender(consoleAppender);
160     }
162     // Make sure the logger keeps up with the logging level preference.
163     this._log.level = Log.Level[Preferences.get(PREF_LOG_LEVEL, "Warn")];
165     // If enabled in the preferences, add a dump appender.
166     let logDumping = Preferences.get(PREF_LOG_DUMP, false);
167     if (logDumping != !!gLogAppenderDump) {
168       if (logDumping) {
169         gLogAppenderDump = new Log.DumpAppender(new Log.BasicFormatter());
170         this._log.addAppender(gLogAppenderDump);
171       } else {
172         this._log.removeAppender(gLogAppenderDump);
173         gLogAppenderDump = null;
174       }
175     }
176   },
178   /**
179    * Create an hidden frame to host our |browser|, then load the SelfSupport page in it.
180    * @param aURL The URL to load in the browser.
181    */
182   _makeHiddenBrowser: function(aURL) {
183     this._frame = new HiddenFrame();
184     return this._frame.get().then(aFrame => {
185       let doc = aFrame.document;
187       this._browser = doc.createElementNS(XUL_NS, "browser");
188       this._browser.setAttribute("type", "content");
189       this._browser.setAttribute("disableglobalhistory", "true");
190       this._browser.setAttribute("src", aURL);
192       doc.documentElement.appendChild(this._browser);
193     });
194   },
196   handleEvent: function(aEvent) {
197     this._log.trace("handleEvent - aEvent.type " + aEvent.type + ", Trusted " + aEvent.isTrusted);
199     if (aEvent.type === "DOMWindowClose") {
200       let window = this._browser.contentDocument.defaultView;
201       let target = aEvent.target;
203       if (target == window) {
204         // preventDefault stops the default window.close(). We need to do that to prevent
205         // Services.appShell.hiddenDOMWindow from being destroyed.
206         aEvent.preventDefault();
208         this.uninit();
209       }
210     }
211   },
213   /**
214    * Called when the self support page correctly loads.
215    */
216   _pageSuccessCallback: function() {
217     this._log.debug("_pageSuccessCallback - Page correctly loaded.");
218     this._browser.removeProgressListener(this._progressListener);
219     this._progressListener.destroy();
220     this._progressListener = null;
222     // Allow SelfSupportBackend to catch |window.close()| issued by the content.
223     this._browser.contentWindow.addEventListener("DOMWindowClose", this, true);
224   },
226   /**
227    * Called when the self support page fails to load.
228    */
229   _pageLoadErrorCallback: function() {
230     this._log.info("_pageLoadErrorCallback - Too many failed load attempts. Giving up.");
231     this.uninit();
232   },
234   /**
235    * Create a browser and attach it to an hidden window. The browser will contain the
236    * self support page and attempt to load the page content. If loading fails, try again
237    * after an interval.
238    */
239   _loadSelfSupport: function() {
240     // Fetch the Self Support URL from the preferences.
241     let unformattedURL = Preferences.get(PREF_URL, null);
242     let url = Services.urlFormatter.formatURL(unformattedURL);
244     this._log.config("_loadSelfSupport - URL " + url);
246     // Create the hidden browser.
247     this._makeHiddenBrowser(url).then(() => {
248       // Load UITour frame script.
249       this._browser.messageManager.loadFrameScript(UITOUR_FRAME_SCRIPT, true);
251       // We need to watch for load errors as well and, in case, try to reload
252       // the self support page.
253       const webFlags = Ci.nsIWebProgress.NOTIFY_STATE_WINDOW |
254                        Ci.nsIWebProgress.NOTIFY_STATE_REQUEST |
255                        Ci.nsIWebProgress.NOTIFY_LOCATION;
257       this._progressListener = new ProgressListener(() => this._pageLoadErrorCallback(),
258                                                     () => this._pageSuccessCallback());
260       this._browser.addProgressListener(this._progressListener, webFlags);
261     });
262   }
266  * A progress listener object which notifies of page load error and load success
267  * through callbacks. When the page fails to load, the progress listener tries to
268  * reload it up to MAX_RETRIES times. The page is not loaded again immediately, but
269  * after a timeout.
271  * @param aLoadErrorCallback Called when a page failed to load MAX_RETRIES times.
272  * @param aLoadSuccessCallback Called when a page correctly loads.
273  */
274 function ProgressListener(aLoadErrorCallback, aLoadSuccessCallback) {
275   this._loadErrorCallback = aLoadErrorCallback;
276   this._loadSuccessCallback = aLoadSuccessCallback;
277   // The number of page loads attempted.
278   this._loadAttempts = 0;
279   this._log = Log.repository.getLogger(LOGGER_NAME);
280   // The Id of the timer which triggers page load again in case of errors.
281   this._reloadTimerId = null;
284 ProgressListener.prototype = {
285   onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) {
286     if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
287       this._log.warn("onLocationChange - There was a problem fetching the SelfSupport URL (attempt " +
288                      this._loadAttempts + ").");
290       // Increase the number of attempts and bail out if we failed too many times.
291       this._loadAttempts++;
292       if (this._loadAttempts > MAX_RETRIES) {
293         this._loadErrorCallback();
294         return;
295       }
297       // Reload the page after the retry interval expires. The interval is multiplied
298       // by the number of attempted loads, so that it takes a bit more to try to reload
299       // when frequently failing.
300       this._reloadTimerId = setTimeout(() => {
301         this._log.debug("onLocationChange - Reloading SelfSupport URL in the hidden browser.");
302         aWebProgress.DOMWindow.location.reload();
303       }, RETRY_INTERVAL_MS * this._loadAttempts);
304     }
305   },
307   onStateChange: function (aWebProgress, aRequest, aFlags, aStatus) {
308     if (aFlags & Ci.nsIWebProgressListener.STATE_STOP &&
309         aFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
310         aFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW &&
311         Components.isSuccessCode(aStatus)) {
312       this._loadSuccessCallback();
313     }
314   },
316   destroy: function () {
317     // Make sure we don't try to reload self support when shutting down.
318     clearTimeout(this._reloadTimerId);
319   },
321   QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
322                                          Ci.nsISupportsWeakReference]),