/* 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 = ["ContentRestore"]; const Cu = Components.utils; const Ci = Components.interfaces; Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); XPCOMUtils.defineLazyModuleGetter(this, "DocShellCapabilities", "resource:///modules/sessionstore/DocShellCapabilities.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "FormData", "resource://gre/modules/FormData.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PageStyle", "resource:///modules/sessionstore/PageStyle.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition", "resource://gre/modules/ScrollPosition.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "SessionHistory", "resource:///modules/sessionstore/SessionHistory.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage", "resource:///modules/sessionstore/SessionStorage.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Utils", "resource:///modules/sessionstore/Utils.jsm"); /** * This module implements the content side of session restoration. The chrome * side is handled by SessionStore.jsm. The functions in this module are called * by content-sessionStore.js based on messages received from SessionStore.jsm * (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.jsm, 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.jsm 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. */ 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, pageStyle, 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.setCurrentURI(Utils.makeURI(uri)); } 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, callbacks.onLoadFinished); }); webNavigation.sessionHistory.addSHistoryListener(listener); this._historyListener = listener; // Make sure to reset the capabilities and attributes in case this tab gets // reused. let disallow = new Set(tabData.disallow && tabData.disallow.split(",")); DocShellCapabilities.restore(this.docShell, disallow); if (tabData.storage && this.docShell instanceof Ci.nsIDocShell) { SessionStorage.restore(this.docShell, tabData.storage); delete tabData.storage; } // 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: function (loadArguments, finishCallback) { let tabData = this._tabData; this._tabData = null; let webNavigation = this.docShell.QueryInterface(Ci.nsIWebNavigation); let history = webNavigation.sessionHistory; // 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 (!loadArguments) { webNavigation.setCurrentURI(Utils.makeURI("about:blank")); } try { if (loadArguments) { // A load has been redirected to a new process so get history into the // same state it was before the load started then trigger the load. let referrer = loadArguments.referrer ? Utils.makeURI(loadArguments.referrer) : null; let referrerPolicy = ('referrerPolicy' in loadArguments ? loadArguments.referrerPolicy : Ci.nsIHttpChannel.REFERRER_POLICY_DEFAULT); let postData = loadArguments.postData ? Utils.makeInputStream(loadArguments.postData) : null; webNavigation.loadURIWithOptions(loadArguments.uri, loadArguments.flags, referrer, referrerPolicy, postData, null, null); } 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. webNavigation.loadURI(tabData.userTypedValue, Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP, null, null, null); } else if (tabData.entries.length) { // Stash away the data we need for restoreDocument. let activeIndex = tabData.index - 1; this._restoringDocument = {entry: tabData.entries[activeIndex] || {}, formdata: tabData.formdata || {}, pageStyle: tabData.pageStyle || {}, 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. history.reloadCurrentEntry(); } else { // If there's nothing to restore, we should still blank the page. webNavigation.loadURI("about:blank", Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY, null, null, null); } 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; } }, /** * 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. */ restoreDocument: function () { if (!this._restoringDocument) { return; } let {entry, pageStyle, formdata, scrollPositions} = this._restoringDocument; this._restoringDocument = null; let window = this.docShell.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindow); PageStyle.restoreTree(this.docShell, pageStyle); FormData.restoreTree(window, formdata); ScrollPosition.restoreTree(window, scrollPositions); }, /** * 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: function () { 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.jsm so that it can restore the content immediately. */ function HistoryListener(docShell, callback) { let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation); webNavigation.sessionHistory.addSHistoryListener(this); this.webNavigation = webNavigation; this.callback = callback; } HistoryListener.prototype = { QueryInterface: XPCOMUtils.generateQI([ Ci.nsISHistoryListener, Ci.nsISupportsWeakReference ]), uninstall: function () { let shistory = this.webNavigation.sessionHistory; if (shistory) { shistory.removeSHistoryListener(this); } }, OnHistoryGoBack: function(backURI) { return true; }, OnHistoryGoForward: function(forwardURI) { return true; }, OnHistoryGotoIndex: function(index, gotoURI) { return true; }, OnHistoryPurge: function(numEntries) { return true; }, OnHistoryReplaceEntry: function(index) {}, // 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.setCurrentURI(Utils.makeURI("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 flags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP; this.webNavigation.loadURI(newURI.spec, flags, null, null, null); }, OnHistoryReload(reloadURI, reloadFlags) { this.callback(); // Cancel the load. return false; }, } /** * This class informs SessionStore.jsm whenever the network requests for a * restoring page have completely finished. We only restore three tabs * simultaneously, so this is the signal for SessionStore.jsm 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: XPCOMUtils.generateQI([ Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference ]), uninstall: function() { this.webProgress.removeProgressListener(this); }, onStateChange: function(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(); } }, onLocationChange: function() {}, onProgressChange: function() {}, onStatusChange: function() {}, onSecurityChange: function() {}, };