2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 * The nsIAboutNewTabService is accessed by the AboutRedirector anytime
9 * about:home, about:newtab or about:welcome are requested. The primary
10 * job of an nsIAboutNewTabService is to tell the AboutRedirector what
11 * resources to actually load for those requests.
13 * The nsIAboutNewTabService is not involved when the user has overridden
14 * the default about:home or about:newtab pages.
16 * There are two implementations of this service - one for the parent
17 * process, and one for content processes. Each one has some secondary
18 * responsibilties that are process-specific.
20 * The need for two implementations is an unfortunate consequence of how
21 * document loading and process redirection for about: pages currently
22 * works in Gecko. The commonalities between the two implementations has
23 * been put into an abstract base class.
26 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
27 import { E10SUtils } from "resource://gre/modules/E10SUtils.sys.mjs";
28 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
32 ChromeUtils.defineESModuleGetters(lazy, {
33 BasePromiseWorker: "resource://gre/modules/PromiseWorker.sys.mjs",
34 NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
38 * BEWARE: Do not add variables for holding state in the global scope.
39 * Any state variables should be properties of the appropriate class
40 * below. This is to avoid confusion where the state is set in one process,
43 * Constants are fine in the global scope.
46 const PREF_ABOUT_HOME_CACHE_TESTING =
47 "browser.startup.homepage.abouthome_cache.testing";
48 const ABOUT_WELCOME_URL =
49 "chrome://browser/content/aboutwelcome/aboutwelcome.html";
51 const CACHE_WORKER_URL = "resource://activity-stream/lib/cache.worker.js";
53 const IS_PRIVILEGED_PROCESS =
54 Services.appinfo.remoteType === E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE;
56 const PREF_SEPARATE_PRIVILEGEDABOUT_CONTENT_PROCESS =
57 "browser.tabs.remote.separatePrivilegedContentProcess";
58 const PREF_ACTIVITY_STREAM_DEBUG = "browser.newtabpage.activity-stream.debug";
61 * The AboutHomeStartupCacheChild is responsible for connecting the
62 * nsIAboutNewTabService with a cached document and script for about:home
63 * if one happens to exist. The AboutHomeStartupCacheChild is only ever
64 * handed the streams for those caches when the "privileged about content
65 * process" first launches, so subsequent loads of about:home do not read
68 * See https://firefox-source-docs.mozilla.org/browser/components/newtab/docs/v2-system-addon/about_home_startup_cache.html
69 * for further details.
71 export const AboutHomeStartupCacheChild = {
73 CACHE_REQUEST_MESSAGE: "AboutHomeStartupCache:CacheRequest",
74 CACHE_RESPONSE_MESSAGE: "AboutHomeStartupCache:CacheResponse",
75 CACHE_USAGE_RESULT_MESSAGE: "AboutHomeStartupCache:UsageResult",
80 PAGE_AND_SCRIPT_CONSUMED: 3,
92 * Called via a process script very early on in the process lifetime. This
93 * prepares the AboutHomeStartupCacheChild to pass an nsIChannel back to
94 * the nsIAboutNewTabService when the initial about:home document is
95 * eventually requested.
97 * @param pageInputStream (nsIInputStream)
98 * The stream for the cached page markup.
99 * @param scriptInputStream (nsIInputStream)
100 * The stream for the cached script to run on the page.
102 init(pageInputStream, scriptInputStream) {
104 !IS_PRIVILEGED_PROCESS &&
105 !Services.prefs.getBoolPref(PREF_ABOUT_HOME_CACHE_TESTING, false)
108 "Can only instantiate in the privileged about content processes."
112 if (!lazy.NimbusFeatures.abouthomecache.getVariable("enabled")) {
117 throw new Error("AboutHomeStartupCacheChild already initted.");
120 Services.obs.addObserver(this, "memory-pressure");
121 Services.cpmm.addMessageListener(this.CACHE_REQUEST_MESSAGE, this);
123 this._pageInputStream = pageInputStream;
124 this._scriptInputStream = scriptInputStream;
125 this._initted = true;
126 this.setState(this.STATES.UNCONSUMED);
130 * A function that lets us put the AboutHomeStartupCacheChild back into
131 * its initial state. This is used by tests to let us simulate the startup
132 * behaviour of the module without having to manually launch a new privileged
133 * about content process every time.
136 if (!Services.prefs.getBoolPref(PREF_ABOUT_HOME_CACHE_TESTING, false)) {
138 "Cannot uninit AboutHomeStartupCacheChild unless testing."
142 if (!this._initted) {
146 Services.obs.removeObserver(this, "memory-pressure");
147 Services.cpmm.removeMessageListener(this.CACHE_REQUEST_MESSAGE, this);
149 if (this._cacheWorker) {
150 this._cacheWorker.terminate();
151 this._cacheWorker = null;
154 this._pageInputStream = null;
155 this._scriptInputStream = null;
156 this._initted = false;
157 this._state = this.STATES.UNAVAILABLE;
158 this._consumerBCID = null;
162 * A public method called from nsIAboutNewTabService that attempts
163 * return an nsIChannel for a cached about:home document that we
164 * were initialized with. If we failed to be initted with the
165 * cache, or the input streams that we were sent have no data
166 * yet available, this function returns null. The caller should
167 * fall back to generating the page dynamically.
169 * This function will be called when loading about:home, or
170 * about:home?jscache - the latter returns the cached script.
172 * It is expected that the same BrowsingContext that loads the cached
173 * page will also load the cached script.
175 * @param uri (nsIURI)
176 * The URI for the requested page, as passed by nsIAboutNewTabService.
177 * @param loadInfo (nsILoadInfo)
178 * The nsILoadInfo for the requested load, as passed by
179 * nsIAboutNewWTabService.
180 * @return nsIChannel or null.
182 maybeGetCachedPageChannel(uri, loadInfo) {
183 if (!this._initted) {
187 if (this._state >= this.STATES.PAGE_AND_SCRIPT_CONSUMED) {
192 uri.query === "jscache"
193 ? this.REQUEST_TYPE.SCRIPT
194 : this.REQUEST_TYPE.PAGE;
196 // If this is a page request, then we need to be in the UNCONSUMED state,
197 // since we expect the page request to come first. If this is a script
198 // request, we expect to be in PAGE_CONSUMED state, since the page cache
199 // stream should he been consumed already.
201 (requestType === this.REQUEST_TYPE.PAGE &&
202 this._state !== this.STATES.UNCONSUMED) ||
203 (requestType === this.REQUEST_TYPE_SCRIPT &&
204 this._state !== this.STATES.PAGE_CONSUMED)
209 // If by this point, we don't have anything in the streams,
210 // then either the cache was too slow to give us data, or the cache
211 // doesn't exist. The caller should fall back to generating the
214 // We only do this on the page request, because by the time
215 // we get to the script request, we should have already drained
216 // the page input stream.
217 if (requestType === this.REQUEST_TYPE.PAGE) {
220 !this._scriptInputStream.available() ||
221 !this._pageInputStream.available()
223 this.setState(this.STATES.FAILED);
224 this.reportUsageResult(false /* success */);
228 this.setState(this.STATES.FAILED);
229 if (e.result === Cr.NS_BASE_STREAM_CLOSED) {
230 this.reportUsageResult(false /* success */);
238 requestType === this.REQUEST_TYPE.SCRIPT &&
239 this._consumerBCID !== loadInfo.browsingContextID
241 // Some other document is somehow requesting the script - one
242 // that didn't originally request the page. This is not allowed.
243 this.setState(this.STATES.FAILED);
248 "@mozilla.org/network/input-stream-channel;1"
249 ].createInstance(Ci.nsIInputStreamChannel);
250 channel.QueryInterface(Ci.nsIChannel);
252 channel.loadInfo = loadInfo;
253 channel.contentStream =
254 requestType === this.REQUEST_TYPE.PAGE
255 ? this._pageInputStream
256 : this._scriptInputStream;
258 if (requestType === this.REQUEST_TYPE.SCRIPT) {
259 this.setState(this.STATES.PAGE_AND_SCRIPT_CONSUMED);
260 this.reportUsageResult(true /* success */);
262 this.setState(this.STATES.PAGE_CONSUMED);
263 // Stash the BrowsingContext ID so that when the script stream
264 // attempts to be consumed, we ensure that it's from the same
265 // BrowsingContext that loaded the page.
266 this._consumerBCID = loadInfo.browsingContextID;
273 * This function takes the state information required to generate
274 * the about:home cache markup and script, and then generates that
275 * markup in script asynchronously. Once that's done, a message
276 * is sent to the parent process with the nsIInputStream's for the
277 * markup and script contents.
279 * @param state (Object)
280 * The Redux state of the about:home document to render.
282 * @resolves undefined
283 * After the message with the nsIInputStream's have been sent to
286 async constructAndSendCache(state) {
287 if (!IS_PRIVILEGED_PROCESS) {
288 throw new Error("Wrong process type.");
291 let worker = this.getOrCreateWorker();
293 TelemetryStopwatch.start("FX_ABOUTHOME_CACHE_CONSTRUCTION");
295 let { page, script } = await worker
296 .post("construct", [state])
298 TelemetryStopwatch.finish("FX_ABOUTHOME_CACHE_CONSTRUCTION");
301 let pageInputStream = Cc[
302 "@mozilla.org/io/string-input-stream;1"
303 ].createInstance(Ci.nsIStringInputStream);
305 pageInputStream.setUTF8Data(page);
307 let scriptInputStream = Cc[
308 "@mozilla.org/io/string-input-stream;1"
309 ].createInstance(Ci.nsIStringInputStream);
311 scriptInputStream.setUTF8Data(script);
313 Services.cpmm.sendAsyncMessage(this.CACHE_RESPONSE_MESSAGE, {
320 getOrCreateWorker() {
321 if (this._cacheWorker) {
322 return this._cacheWorker;
325 this._cacheWorker = new lazy.BasePromiseWorker(CACHE_WORKER_URL);
326 return this._cacheWorker;
329 receiveMessage(message) {
330 if (message.name === this.CACHE_REQUEST_MESSAGE) {
331 let { state } = message.data;
332 this.constructAndSendCache(state);
336 reportUsageResult(success) {
337 Services.cpmm.sendAsyncMessage(this.CACHE_USAGE_RESULT_MESSAGE, {
342 observe(subject, topic, data) {
343 if (topic === "memory-pressure" && this._cacheWorker) {
344 this._cacheWorker.terminate();
345 this._cacheWorker = null;
350 * Transitions the AboutHomeStartupCacheChild from one state
351 * to the next, where each state is defined in this.STATES.
353 * States can only be transitioned in increasing order, otherwise
354 * an error is logged.
357 if (state > this._state) {
361 "AboutHomeStartupCacheChild could not transition from state " +
362 `${this._state} to ${state}`,
369 * If the cache hasn't been used, transitions it into the DISQUALIFIED
370 * state so that it cannot be used. This should be called if it's been
371 * determined that about:newtab is going to be loaded, which doesn't
375 if (this._state === this.STATES.UNCONSUMED) {
376 this.setState(this.STATES.DISQUALIFIED);
377 this.reportUsageResult(false /* success */);
383 * This is an abstract base class for the nsIAboutNewTabService
384 * implementations that has some common methods and properties.
386 class BaseAboutNewTabService {
388 if (!AppConstants.RELEASE_OR_BETA) {
389 XPCOMUtils.defineLazyPreferenceGetter(
391 "activityStreamDebug",
392 PREF_ACTIVITY_STREAM_DEBUG,
396 this.activityStreamDebug = false;
399 XPCOMUtils.defineLazyPreferenceGetter(
401 "privilegedAboutProcessEnabled",
402 PREF_SEPARATE_PRIVILEGEDABOUT_CONTENT_PROCESS,
406 this.classID = Components.ID("{cb36c925-3adc-49b3-b720-a5cc49d8a40e}");
407 this.QueryInterface = ChromeUtils.generateQI([
408 "nsIAboutNewTabService",
414 * Returns the default URL.
416 * This URL depends on various activity stream prefs. Overriding
417 * the newtab page has no effect on the result of this function.
420 // Generate the desired activity stream resource depending on state, e.g.,
421 // "resource://activity-stream/prerendered/activity-stream.html"
422 // "resource://activity-stream/prerendered/activity-stream-debug.html"
423 // "resource://activity-stream/prerendered/activity-stream-noscripts.html"
425 "resource://activity-stream/prerendered/",
427 // Debug version loads dev scripts but noscripts separately loads scripts
428 this.activityStreamDebug && !this.privilegedAboutProcessEnabled
431 this.privilegedAboutProcessEnabled ? "-noscripts" : "",
438 * Returns the about:welcome URL
440 * This is calculated in the same way the default URL is.
443 lazy.NimbusFeatures.aboutwelcome.recordExposureEvent({ once: true });
444 if (lazy.NimbusFeatures.aboutwelcome.getVariable("enabled") ?? true) {
445 return ABOUT_WELCOME_URL;
447 return this.defaultURL;
450 aboutHomeChannel(uri, loadInfo) {
451 throw Components.Exception(
452 "AboutHomeChannel not implemented for this process.",
453 Cr.NS_ERROR_NOT_IMPLEMENTED
459 * The child-process implementation of nsIAboutNewTabService,
460 * which also does the work of redirecting about:home loads to
461 * the about:home startup cache if its available.
463 class AboutNewTabChildService extends BaseAboutNewTabService {
464 aboutHomeChannel(uri, loadInfo) {
465 if (IS_PRIVILEGED_PROCESS) {
466 let cacheChannel = AboutHomeStartupCacheChild.maybeGetCachedPageChannel(
475 let pageURI = Services.io.newURI(this.defaultURL);
476 let fileChannel = Services.io.newChannelFromURIWithLoadInfo(
480 fileChannel.originalURI = uri;
485 if (IS_PRIVILEGED_PROCESS) {
486 // This is a bit of a hack, but attempting to load about:newtab will
487 // enter this code path in order to get at the expected URL, and we
488 // can use that to disqualify the about:home cache, since we don't
489 // use it for about:newtab loads, and we don't want the about:home
490 // cache to be wildly out of date when about:home is eventually
491 // loaded (for example, in the first new window).
492 AboutHomeStartupCacheChild.disqualifyCache();
495 return super.defaultURL;
500 * The AboutNewTabStubService is a function called in both the main and
501 * content processes when trying to get at the nsIAboutNewTabService. This
502 * function does the job of choosing the appropriate implementation of
503 * nsIAboutNewTabService for the process type.
505 export function AboutNewTabStubService() {
506 if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT) {
507 return new BaseAboutNewTabService();
509 return new AboutNewTabChildService();