summaryrefslogtreecommitdiffstats
path: root/browser/components/sessionstore/ContentRestore.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--browser/components/sessionstore/ContentRestore.sys.mjs430
1 files changed, 430 insertions, 0 deletions
diff --git a/browser/components/sessionstore/ContentRestore.sys.mjs b/browser/components/sessionstore/ContentRestore.sys.mjs
new file mode 100644
index 0000000000..7cfd5c02ff
--- /dev/null
+++ b/browser/components/sessionstore/ContentRestore.sys.mjs
@@ -0,0 +1,430 @@
+/* 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/. */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ SessionHistory: "resource://gre/modules/sessionstore/SessionHistory.sys.mjs",
+ Utils: "resource://gre/modules/sessionstore/Utils.sys.mjs",
+});
+
+/**
+ * This module implements the content side of session restoration. The chrome
+ * side is handled by SessionStore.sys.mjs. The functions in this module are called
+ * by content-sessionStore.js based on messages received from SessionStore.sys.mjs
+ * (or, in one case, based on a "load" event). Each tab has its own
+ * ContentRestore instance, constructed by content-sessionStore.js.
+ *
+ * In a typical restore, content-sessionStore.js will call the following based
+ * on messages and events it receives:
+ *
+ * restoreHistory(tabData, loadArguments, callbacks)
+ * Restores the tab's history and session cookies.
+ * restoreTabContent(loadArguments, finishCallback)
+ * Starts loading the data for the current page to restore.
+ * restoreDocument()
+ * Restore form and scroll data.
+ *
+ * When the page has been loaded from the network, we call finishCallback. It
+ * should send a message to SessionStore.sys.mjs, which may cause other tabs to be
+ * restored.
+ *
+ * When the page has finished loading, a "load" event will trigger in
+ * content-sessionStore.js, which will call restoreDocument. At that point,
+ * form data is restored and the restore is complete.
+ *
+ * At any time, SessionStore.sys.mjs can cancel the ongoing restore by sending a
+ * reset message, which causes resetRestore to be called. At that point it's
+ * legal to begin another restore.
+ */
+export function ContentRestore(chromeGlobal) {
+ let internal = new ContentRestoreInternal(chromeGlobal);
+ let external = {};
+
+ let EXPORTED_METHODS = [
+ "restoreHistory",
+ "restoreTabContent",
+ "restoreDocument",
+ "resetRestore",
+ ];
+
+ for (let method of EXPORTED_METHODS) {
+ external[method] = internal[method].bind(internal);
+ }
+
+ return Object.freeze(external);
+}
+
+function ContentRestoreInternal(chromeGlobal) {
+ this.chromeGlobal = chromeGlobal;
+
+ // The following fields are only valid during certain phases of the restore
+ // process.
+
+ // The tabData for the restore. Set in restoreHistory and removed in
+ // restoreTabContent.
+ this._tabData = null;
+
+ // Contains {entry, scrollPositions, formdata}, where entry is a
+ // single entry from the tabData.entries array. Set in
+ // restoreTabContent and removed in restoreDocument.
+ this._restoringDocument = null;
+
+ // This listener is used to detect reloads on restoring tabs. Set in
+ // restoreHistory and removed in restoreTabContent.
+ this._historyListener = null;
+
+ // This listener detects when a pending tab starts loading (when not
+ // initiated by sessionstore) and when a restoring tab has finished loading
+ // data from the network. Set in restoreHistory() and restoreTabContent(),
+ // removed in resetRestore().
+ this._progressListener = null;
+}
+
+/**
+ * The API for the ContentRestore module. Methods listed in EXPORTED_METHODS are
+ * public.
+ */
+ContentRestoreInternal.prototype = {
+ get docShell() {
+ return this.chromeGlobal.docShell;
+ },
+
+ /**
+ * Starts the process of restoring a tab. The tabData to be restored is passed
+ * in here and used throughout the restoration. The epoch (which must be
+ * non-zero) is passed through to all the callbacks. If a load in the tab
+ * is started while it is pending, the appropriate callbacks are called.
+ */
+ restoreHistory(tabData, loadArguments, callbacks) {
+ this._tabData = tabData;
+
+ // In case about:blank isn't done yet.
+ let webNavigation = this.docShell.QueryInterface(Ci.nsIWebNavigation);
+ webNavigation.stop(Ci.nsIWebNavigation.STOP_ALL);
+
+ // Make sure currentURI is set so that switch-to-tab works before the tab is
+ // restored. We'll reset this to about:blank when we try to restore the tab
+ // to ensure that docshell doeesn't get confused. Don't bother doing this if
+ // we're restoring immediately due to a process switch. It just causes the
+ // URL bar to be temporarily blank.
+ let activeIndex = tabData.index - 1;
+ let activePageData = tabData.entries[activeIndex] || {};
+ let uri = activePageData.url || null;
+ if (uri && !loadArguments) {
+ webNavigation.setCurrentURIForSessionStore(Services.io.newURI(uri));
+ }
+
+ lazy.SessionHistory.restore(this.docShell, tabData);
+
+ // Add a listener to watch for reloads.
+ let listener = new HistoryListener(this.docShell, () => {
+ // On reload, restore tab contents.
+ this.restoreTabContent(null, false, callbacks.onLoadFinished);
+ });
+
+ webNavigation.sessionHistory.legacySHistory.addSHistoryListener(listener);
+ this._historyListener = listener;
+
+ // Make sure to reset the capabilities and attributes in case this tab gets
+ // reused.
+ SessionStoreUtils.restoreDocShellCapabilities(
+ this.docShell,
+ tabData.disallow
+ );
+
+ // Add a progress listener to correctly handle browser.loadURI()
+ // calls from foreign code.
+ this._progressListener = new ProgressListener(this.docShell, {
+ onStartRequest: () => {
+ // Some code called browser.loadURI() on a pending tab. It's safe to
+ // assume we don't care about restoring scroll or form data.
+ this._tabData = null;
+
+ // Listen for the tab to finish loading.
+ this.restoreTabContentStarted(callbacks.onLoadFinished);
+
+ // Notify the parent.
+ callbacks.onLoadStarted();
+ },
+ });
+ },
+
+ /**
+ * Start loading the current page. When the data has finished loading from the
+ * network, finishCallback is called. Returns true if the load was successful.
+ */
+ restoreTabContent(loadArguments, isRemotenessUpdate, finishCallback) {
+ let tabData = this._tabData;
+ this._tabData = null;
+
+ let webNavigation = this.docShell.QueryInterface(Ci.nsIWebNavigation);
+
+ // Listen for the tab to finish loading.
+ this.restoreTabContentStarted(finishCallback);
+
+ // Reset the current URI to about:blank. We changed it above for
+ // switch-to-tab, but now it must go back to the correct value before the
+ // load happens. Don't bother doing this if we're restoring immediately
+ // due to a process switch.
+ if (!isRemotenessUpdate) {
+ webNavigation.setCurrentURIForSessionStore(
+ Services.io.newURI("about:blank")
+ );
+ }
+
+ try {
+ if (loadArguments) {
+ // If the load was started in another process, and the in-flight channel
+ // was redirected into this process, resume that load within our process.
+ //
+ // NOTE: In this case `isRemotenessUpdate` must be true.
+ webNavigation.resumeRedirectedLoad(
+ loadArguments.redirectLoadSwitchId,
+ loadArguments.redirectHistoryIndex
+ );
+ } else if (tabData.userTypedValue && tabData.userTypedClear) {
+ // If the user typed a URL into the URL bar and hit enter right before
+ // we crashed, we want to start loading that page again. A non-zero
+ // userTypedClear value means that the load had started.
+ // Load userTypedValue and fix up the URL if it's partial/broken.
+ let loadURIOptions = {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP,
+ };
+ webNavigation.loadURI(tabData.userTypedValue, loadURIOptions);
+ } else if (tabData.entries.length) {
+ // Stash away the data we need for restoreDocument.
+ this._restoringDocument = {
+ formdata: tabData.formdata || {},
+ scrollPositions: tabData.scroll || {},
+ };
+
+ // In order to work around certain issues in session history, we need to
+ // force session history to update its internal index and call reload
+ // instead of gotoIndex. See bug 597315.
+ let history = webNavigation.sessionHistory.legacySHistory;
+ history.reloadCurrentEntry();
+ } else {
+ // If there's nothing to restore, we should still blank the page.
+ let loadURIOptions = {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY,
+ // Specify an override to force the load to finish in the current
+ // process, as tests rely on this behaviour for non-fission session
+ // restore.
+ remoteTypeOverride: Services.appinfo.remoteType,
+ };
+ webNavigation.loadURI("about:blank", loadURIOptions);
+ }
+
+ return true;
+ } catch (ex) {
+ if (ex instanceof Ci.nsIException) {
+ // Ignore page load errors, but return false to signal that the load never
+ // happened.
+ return false;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * To be called after restoreHistory(). Removes all listeners needed for
+ * pending tabs and makes sure to notify when the tab finished loading.
+ */
+ restoreTabContentStarted(finishCallback) {
+ // The reload listener is no longer needed.
+ this._historyListener.uninstall();
+ this._historyListener = null;
+
+ // Remove the old progress listener.
+ this._progressListener.uninstall();
+
+ // We're about to start a load. This listener will be called when the load
+ // has finished getting everything from the network.
+ this._progressListener = new ProgressListener(this.docShell, {
+ onStopRequest: () => {
+ // Call resetRestore() to reset the state back to normal. The data
+ // needed for restoreDocument() (which hasn't happened yet) will
+ // remain in _restoringDocument.
+ this.resetRestore();
+
+ finishCallback();
+ },
+ });
+ },
+
+ /**
+ * Finish restoring the tab by filling in form data and setting the scroll
+ * position. The restore is complete when this function exits. It should be
+ * called when the "load" event fires for the restoring tab. Returns true
+ * if we're restoring a document.
+ */
+ restoreDocument() {
+ if (!this._restoringDocument) {
+ return;
+ }
+
+ let { formdata, scrollPositions } = this._restoringDocument;
+ this._restoringDocument = null;
+
+ let window = this.docShell.domWindow;
+
+ // Restore form data.
+ lazy.Utils.restoreFrameTreeData(window, formdata, (frame, data) => {
+ // restore() will return false, and thus abort restoration for the
+ // current |frame| and its descendants, if |data.url| is given but
+ // doesn't match the loaded document's URL.
+ return SessionStoreUtils.restoreFormData(frame.document, data);
+ });
+
+ // Restore scroll data.
+ lazy.Utils.restoreFrameTreeData(window, scrollPositions, (frame, data) => {
+ if (data.scroll) {
+ SessionStoreUtils.restoreScrollPosition(frame, data);
+ }
+ });
+ },
+
+ /**
+ * Cancel an ongoing restore. This function can be called any time between
+ * restoreHistory and restoreDocument.
+ *
+ * This function is called externally (if a restore is canceled) and
+ * internally (when the loads for a restore have finished). In the latter
+ * case, it's called before restoreDocument, so it cannot clear
+ * _restoringDocument.
+ */
+ resetRestore() {
+ this._tabData = null;
+
+ if (this._historyListener) {
+ this._historyListener.uninstall();
+ }
+ this._historyListener = null;
+
+ if (this._progressListener) {
+ this._progressListener.uninstall();
+ }
+ this._progressListener = null;
+ },
+};
+
+/*
+ * This listener detects when a page being restored is reloaded. It triggers a
+ * callback and cancels the reload. The callback will send a message to
+ * SessionStore.sys.mjs so that it can restore the content immediately.
+ */
+function HistoryListener(docShell, callback) {
+ let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
+ webNavigation.sessionHistory.legacySHistory.addSHistoryListener(this);
+
+ this.webNavigation = webNavigation;
+ this.callback = callback;
+}
+HistoryListener.prototype = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsISHistoryListener",
+ "nsISupportsWeakReference",
+ ]),
+
+ uninstall() {
+ let shistory = this.webNavigation.sessionHistory.legacySHistory;
+ if (shistory) {
+ shistory.removeSHistoryListener(this);
+ }
+ },
+
+ OnHistoryGotoIndex() {},
+ OnHistoryPurge() {},
+ OnHistoryReplaceEntry() {},
+
+ // This will be called for a pending tab when loadURI(uri) is called where
+ // the given |uri| only differs in the fragment.
+ OnHistoryNewEntry(newURI) {
+ let currentURI = this.webNavigation.currentURI;
+
+ // Ignore new SHistory entries with the same URI as those do not indicate
+ // a navigation inside a document by changing the #hash part of the URL.
+ // We usually hit this when purging session history for browsers.
+ if (currentURI && currentURI.spec == newURI.spec) {
+ return;
+ }
+
+ // Reset the tab's URL to what it's actually showing. Without this loadURI()
+ // would use the current document and change the displayed URL only.
+ this.webNavigation.setCurrentURIForSessionStore(
+ Services.io.newURI("about:blank")
+ );
+
+ // Kick off a new load so that we navigate away from about:blank to the
+ // new URL that was passed to loadURI(). The new load will cause a
+ // STATE_START notification to be sent and the ProgressListener will then
+ // notify the parent and do the rest.
+ let loadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP;
+ let loadURIOptions = {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ loadFlags,
+ };
+ this.webNavigation.loadURI(newURI.spec, loadURIOptions);
+ },
+
+ OnHistoryReload() {
+ this.callback();
+
+ // Cancel the load.
+ return false;
+ },
+};
+
+/**
+ * This class informs SessionStore.sys.mjs whenever the network requests for a
+ * restoring page have completely finished. We only restore three tabs
+ * simultaneously, so this is the signal for SessionStore.sys.mjs to kick off
+ * another restore (if there are more to do).
+ *
+ * The progress listener is also used to be notified when a load not initiated
+ * by sessionstore starts. Pending tabs will then need to be marked as no
+ * longer pending.
+ */
+function ProgressListener(docShell, callbacks) {
+ let webProgress = docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_WINDOW);
+
+ this.webProgress = webProgress;
+ this.callbacks = callbacks;
+}
+
+ProgressListener.prototype = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+
+ uninstall() {
+ this.webProgress.removeProgressListener(this);
+ },
+
+ onStateChange(webProgress, request, stateFlags, status) {
+ let {
+ STATE_IS_WINDOW,
+ STATE_STOP,
+ STATE_START,
+ } = Ci.nsIWebProgressListener;
+ if (!webProgress.isTopLevel || !(stateFlags & STATE_IS_WINDOW)) {
+ return;
+ }
+
+ if (stateFlags & STATE_START && this.callbacks.onStartRequest) {
+ this.callbacks.onStartRequest();
+ }
+
+ if (stateFlags & STATE_STOP && this.callbacks.onStopRequest) {
+ this.callbacks.onStopRequest();
+ }
+ },
+};