/** * 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"; const EXPORTED_SYMBOLS = [ "AboutNewTabStubService", "AboutHomeStartupCacheChild", ]; /** * The nsIAboutNewTabService is accessed by the AboutRedirector anytime * about:home, about:newtab or about:welcome are requested. The primary * job of an nsIAboutNewTabService is to tell the AboutRedirector what * resources to actually load for those requests. * * The nsIAboutNewTabService is not involved when the user has overridden * the default about:home or about:newtab pages. * * There are two implementations of this service - one for the parent * process, and one for content processes. Each one has some secondary * responsibilties that are process-specific. * * The need for two implementations is an unfortunate consequence of how * document loading and process redirection for about: pages currently * works in Gecko. The commonalities between the two implementations has * been put into an abstract base class. */ const { XPCOMUtils } = ChromeUtils.importESModule( "resource://gre/modules/XPCOMUtils.sys.mjs" ); const { AppConstants } = ChromeUtils.importESModule( "resource://gre/modules/AppConstants.sys.mjs" ); const { E10SUtils } = ChromeUtils.importESModule( "resource://gre/modules/E10SUtils.sys.mjs" ); const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { BasePromiseWorker: "resource://gre/modules/PromiseWorker.sys.mjs", NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", }); /** * BEWARE: Do not add variables for holding state in the global scope. * Any state variables should be properties of the appropriate class * below. This is to avoid confusion where the state is set in one process, * but not in another. * * Constants are fine in the global scope. */ const PREF_ABOUT_HOME_CACHE_TESTING = "browser.startup.homepage.abouthome_cache.testing"; const ABOUT_WELCOME_URL = "resource://activity-stream/aboutwelcome/aboutwelcome.html"; const CACHE_WORKER_URL = "resource://activity-stream/lib/cache-worker.js"; const IS_PRIVILEGED_PROCESS = Services.appinfo.remoteType === E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE; const PREF_SEPARATE_PRIVILEGEDABOUT_CONTENT_PROCESS = "browser.tabs.remote.separatePrivilegedContentProcess"; const PREF_ACTIVITY_STREAM_DEBUG = "browser.newtabpage.activity-stream.debug"; /** * The AboutHomeStartupCacheChild is responsible for connecting the * nsIAboutNewTabService with a cached document and script for about:home * if one happens to exist. The AboutHomeStartupCacheChild is only ever * handed the streams for those caches when the "privileged about content * process" first launches, so subsequent loads of about:home do not read * from this cache. * * See https://firefox-source-docs.mozilla.org/browser/components/newtab/docs/v2-system-addon/about_home_startup_cache.html * for further details. */ const AboutHomeStartupCacheChild = { _initted: false, CACHE_REQUEST_MESSAGE: "AboutHomeStartupCache:CacheRequest", CACHE_RESPONSE_MESSAGE: "AboutHomeStartupCache:CacheResponse", CACHE_USAGE_RESULT_MESSAGE: "AboutHomeStartupCache:UsageResult", STATES: { UNAVAILABLE: 0, UNCONSUMED: 1, PAGE_CONSUMED: 2, PAGE_AND_SCRIPT_CONSUMED: 3, FAILED: 4, DISQUALIFIED: 5, }, REQUEST_TYPE: { PAGE: 0, SCRIPT: 1, }, _state: 0, _consumerBCID: null, /** * Called via a process script very early on in the process lifetime. This * prepares the AboutHomeStartupCacheChild to pass an nsIChannel back to * the nsIAboutNewTabService when the initial about:home document is * eventually requested. * * @param pageInputStream (nsIInputStream) * The stream for the cached page markup. * @param scriptInputStream (nsIInputStream) * The stream for the cached script to run on the page. */ init(pageInputStream, scriptInputStream) { if ( !IS_PRIVILEGED_PROCESS && !Services.prefs.getBoolPref(PREF_ABOUT_HOME_CACHE_TESTING, false) ) { throw new Error( "Can only instantiate in the privileged about content processes." ); } if (!lazy.NimbusFeatures.abouthomecache.getVariable("enabled")) { return; } if (this._initted) { throw new Error("AboutHomeStartupCacheChild already initted."); } Services.obs.addObserver(this, "memory-pressure"); Services.cpmm.addMessageListener(this.CACHE_REQUEST_MESSAGE, this); this._pageInputStream = pageInputStream; this._scriptInputStream = scriptInputStream; this._initted = true; this.setState(this.STATES.UNCONSUMED); }, /** * A function that lets us put the AboutHomeStartupCacheChild back into * its initial state. This is used by tests to let us simulate the startup * behaviour of the module without having to manually launch a new privileged * about content process every time. */ uninit() { if (!Services.prefs.getBoolPref(PREF_ABOUT_HOME_CACHE_TESTING, false)) { throw new Error( "Cannot uninit AboutHomeStartupCacheChild unless testing." ); } if (!this._initted) { return; } Services.obs.removeObserver(this, "memory-pressure"); Services.cpmm.removeMessageListener(this.CACHE_REQUEST_MESSAGE, this); if (this._cacheWorker) { this._cacheWorker.terminate(); this._cacheWorker = null; } this._pageInputStream = null; this._scriptInputStream = null; this._initted = false; this._state = this.STATES.UNAVAILABLE; this._consumerBCID = null; }, /** * A public method called from nsIAboutNewTabService that attempts * return an nsIChannel for a cached about:home document that we * were initialized with. If we failed to be initted with the * cache, or the input streams that we were sent have no data * yet available, this function returns null. The caller should * fall back to generating the page dynamically. * * This function will be called when loading about:home, or * about:home?jscache - the latter returns the cached script. * * It is expected that the same BrowsingContext that loads the cached * page will also load the cached script. * * @param uri (nsIURI) * The URI for the requested page, as passed by nsIAboutNewTabService. * @param loadInfo (nsILoadInfo) * The nsILoadInfo for the requested load, as passed by * nsIAboutNewWTabService. * @return nsIChannel or null. */ maybeGetCachedPageChannel(uri, loadInfo) { if (!this._initted) { return null; } if (this._state >= this.STATES.PAGE_AND_SCRIPT_CONSUMED) { return null; } let requestType = uri.query === "jscache" ? this.REQUEST_TYPE.SCRIPT : this.REQUEST_TYPE.PAGE; // If this is a page request, then we need to be in the UNCONSUMED state, // since we expect the page request to come first. If this is a script // request, we expect to be in PAGE_CONSUMED state, since the page cache // stream should he been consumed already. if ( (requestType === this.REQUEST_TYPE.PAGE && this._state !== this.STATES.UNCONSUMED) || (requestType === this.REQUEST_TYPE_SCRIPT && this._state !== this.STATES.PAGE_CONSUMED) ) { return null; } // If by this point, we don't have anything in the streams, // then either the cache was too slow to give us data, or the cache // doesn't exist. The caller should fall back to generating the // page dynamically. // // We only do this on the page request, because by the time // we get to the script request, we should have already drained // the page input stream. if (requestType === this.REQUEST_TYPE.PAGE) { try { if ( !this._scriptInputStream.available() || !this._pageInputStream.available() ) { this.setState(this.STATES.FAILED); this.reportUsageResult(false /* success */); return null; } } catch (e) { this.setState(this.STATES.FAILED); if (e.result === Cr.NS_BASE_STREAM_CLOSED) { this.reportUsageResult(false /* success */); return null; } throw e; } } if ( requestType === this.REQUEST_TYPE.SCRIPT && this._consumerBCID !== loadInfo.browsingContextID ) { // Some other document is somehow requesting the script - one // that didn't originally request the page. This is not allowed. this.setState(this.STATES.FAILED); return null; } let channel = Cc[ "@mozilla.org/network/input-stream-channel;1" ].createInstance(Ci.nsIInputStreamChannel); channel.QueryInterface(Ci.nsIChannel); channel.setURI(uri); channel.loadInfo = loadInfo; channel.contentStream = requestType === this.REQUEST_TYPE.PAGE ? this._pageInputStream : this._scriptInputStream; if (requestType === this.REQUEST_TYPE.SCRIPT) { this.setState(this.STATES.PAGE_AND_SCRIPT_CONSUMED); this.reportUsageResult(true /* success */); } else { this.setState(this.STATES.PAGE_CONSUMED); // Stash the BrowsingContext ID so that when the script stream // attempts to be consumed, we ensure that it's from the same // BrowsingContext that loaded the page. this._consumerBCID = loadInfo.browsingContextID; } return channel; }, /** * This function takes the state information required to generate * the about:home cache markup and script, and then generates that * markup in script asynchronously. Once that's done, a message * is sent to the parent process with the nsIInputStream's for the * markup and script contents. * * @param state (Object) * The Redux state of the about:home document to render. * @return Promise * @resolves undefined * After the message with the nsIInputStream's have been sent to * the parent. */ async constructAndSendCache(state) { if (!IS_PRIVILEGED_PROCESS) { throw new Error("Wrong process type."); } let worker = this.getOrCreateWorker(); TelemetryStopwatch.start("FX_ABOUTHOME_CACHE_CONSTRUCTION"); let { page, script } = await worker .post("construct", [state]) .finally(() => { TelemetryStopwatch.finish("FX_ABOUTHOME_CACHE_CONSTRUCTION"); }); let pageInputStream = Cc[ "@mozilla.org/io/string-input-stream;1" ].createInstance(Ci.nsIStringInputStream); pageInputStream.setUTF8Data(page); let scriptInputStream = Cc[ "@mozilla.org/io/string-input-stream;1" ].createInstance(Ci.nsIStringInputStream); scriptInputStream.setUTF8Data(script); Services.cpmm.sendAsyncMessage(this.CACHE_RESPONSE_MESSAGE, { pageInputStream, scriptInputStream, }); }, _cacheWorker: null, getOrCreateWorker() { if (this._cacheWorker) { return this._cacheWorker; } this._cacheWorker = new lazy.BasePromiseWorker(CACHE_WORKER_URL); return this._cacheWorker; }, receiveMessage(message) { if (message.name === this.CACHE_REQUEST_MESSAGE) { let { state } = message.data; this.constructAndSendCache(state); } }, reportUsageResult(success) { Services.cpmm.sendAsyncMessage(this.CACHE_USAGE_RESULT_MESSAGE, { success, }); }, observe(subject, topic, data) { if (topic === "memory-pressure" && this._cacheWorker) { this._cacheWorker.terminate(); this._cacheWorker = null; } }, /** * Transitions the AboutHomeStartupCacheChild from one state * to the next, where each state is defined in this.STATES. * * States can only be transitioned in increasing order, otherwise * an error is logged. */ setState(state) { if (state > this._state) { this._state = state; } else { console.error( "AboutHomeStartupCacheChild could not transition from state " + `${this._state} to ${state}`, new Error().stack ); } }, /** * If the cache hasn't been used, transitions it into the DISQUALIFIED * state so that it cannot be used. This should be called if it's been * determined that about:newtab is going to be loaded, which doesn't * use the cache. */ disqualifyCache() { if (this._state === this.STATES.UNCONSUMED) { this.setState(this.STATES.DISQUALIFIED); this.reportUsageResult(false /* success */); } }, }; /** * This is an abstract base class for the nsIAboutNewTabService * implementations that has some common methods and properties. */ class BaseAboutNewTabService { constructor() { if (!AppConstants.RELEASE_OR_BETA) { XPCOMUtils.defineLazyPreferenceGetter( this, "activityStreamDebug", PREF_ACTIVITY_STREAM_DEBUG, false ); } else { this.activityStreamDebug = false; } XPCOMUtils.defineLazyPreferenceGetter( this, "privilegedAboutProcessEnabled", PREF_SEPARATE_PRIVILEGEDABOUT_CONTENT_PROCESS, false ); this.classID = Components.ID("{cb36c925-3adc-49b3-b720-a5cc49d8a40e}"); this.QueryInterface = ChromeUtils.generateQI([ "nsIAboutNewTabService", "nsIObserver", ]); } /** * Returns the default URL. * * This URL depends on various activity stream prefs. Overriding * the newtab page has no effect on the result of this function. */ get defaultURL() { // Generate the desired activity stream resource depending on state, e.g., // "resource://activity-stream/prerendered/activity-stream.html" // "resource://activity-stream/prerendered/activity-stream-debug.html" // "resource://activity-stream/prerendered/activity-stream-noscripts.html" return [ "resource://activity-stream/prerendered/", "activity-stream", // Debug version loads dev scripts but noscripts separately loads scripts this.activityStreamDebug && !this.privilegedAboutProcessEnabled ? "-debug" : "", this.privilegedAboutProcessEnabled ? "-noscripts" : "", ".html", ].join(""); } get welcomeURL() { /* * Returns the about:welcome URL * * This is calculated in the same way the default URL is. */ lazy.NimbusFeatures.aboutwelcome.recordExposureEvent({ once: true }); if (lazy.NimbusFeatures.aboutwelcome.getVariable("enabled") ?? true) { return ABOUT_WELCOME_URL; } return this.defaultURL; } aboutHomeChannel(uri, loadInfo) { throw Components.Exception( "AboutHomeChannel not implemented for this process.", Cr.NS_ERROR_NOT_IMPLEMENTED ); } } /** * The child-process implementation of nsIAboutNewTabService, * which also does the work of redirecting about:home loads to * the about:home startup cache if its available. */ class AboutNewTabChildService extends BaseAboutNewTabService { aboutHomeChannel(uri, loadInfo) { if (IS_PRIVILEGED_PROCESS) { let cacheChannel = AboutHomeStartupCacheChild.maybeGetCachedPageChannel( uri, loadInfo ); if (cacheChannel) { return cacheChannel; } } let pageURI = Services.io.newURI(this.defaultURL); let fileChannel = Services.io.newChannelFromURIWithLoadInfo( pageURI, loadInfo ); fileChannel.originalURI = uri; return fileChannel; } get defaultURL() { if (IS_PRIVILEGED_PROCESS) { // This is a bit of a hack, but attempting to load about:newtab will // enter this code path in order to get at the expected URL, and we // can use that to disqualify the about:home cache, since we don't // use it for about:newtab loads, and we don't want the about:home // cache to be wildly out of date when about:home is eventually // loaded (for example, in the first new window). AboutHomeStartupCacheChild.disqualifyCache(); } return super.defaultURL; } } /** * The AboutNewTabStubService is a function called in both the main and * content processes when trying to get at the nsIAboutNewTabService. This * function does the job of choosing the appropriate implementation of * nsIAboutNewTabService for the process type. */ function AboutNewTabStubService() { if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT) { return new BaseAboutNewTabService(); } return new AboutNewTabChildService(); }