michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["ContentRestore"]; michael@0: michael@0: const Cu = Components.utils; michael@0: const Ci = Components.interfaces; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "DocShellCapabilities", michael@0: "resource:///modules/sessionstore/DocShellCapabilities.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "FormData", michael@0: "resource://gre/modules/FormData.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PageStyle", michael@0: "resource:///modules/sessionstore/PageStyle.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition", michael@0: "resource://gre/modules/ScrollPosition.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "SessionHistory", michael@0: "resource:///modules/sessionstore/SessionHistory.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage", michael@0: "resource:///modules/sessionstore/SessionStorage.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Utils", michael@0: "resource:///modules/sessionstore/Utils.jsm"); michael@0: michael@0: /** michael@0: * This module implements the content side of session restoration. The chrome michael@0: * side is handled by SessionStore.jsm. The functions in this module are called michael@0: * by content-sessionStore.js based on messages received from SessionStore.jsm michael@0: * (or, in one case, based on a "load" event). Each tab has its own michael@0: * ContentRestore instance, constructed by content-sessionStore.js. michael@0: * michael@0: * In a typical restore, content-sessionStore.js will call the following based michael@0: * on messages and events it receives: michael@0: * michael@0: * restoreHistory(epoch, tabData, reloadCallback) michael@0: * Restores the tab's history and session cookies. michael@0: * restoreTabContent(finishCallback) michael@0: * Starts loading the data for the current page to restore. michael@0: * restoreDocument() michael@0: * Restore form and scroll data. michael@0: * michael@0: * When the page has been loaded from the network, we call finishCallback. It michael@0: * should send a message to SessionStore.jsm, which may cause other tabs to be michael@0: * restored. michael@0: * michael@0: * When the page has finished loading, a "load" event will trigger in michael@0: * content-sessionStore.js, which will call restoreDocument. At that point, michael@0: * form data is restored and the restore is complete. michael@0: * michael@0: * At any time, SessionStore.jsm can cancel the ongoing restore by sending a michael@0: * reset message, which causes resetRestore to be called. At that point it's michael@0: * legal to begin another restore. michael@0: * michael@0: * The epoch that is passed into restoreHistory is merely a token. All messages michael@0: * sent back to SessionStore.jsm include the epoch. This way, SessionStore.jsm michael@0: * can discard messages that relate to restores that it has canceled (by michael@0: * starting a new restore, say). michael@0: */ michael@0: function ContentRestore(chromeGlobal) { michael@0: let internal = new ContentRestoreInternal(chromeGlobal); michael@0: let external = {}; michael@0: michael@0: let EXPORTED_METHODS = ["restoreHistory", michael@0: "restoreTabContent", michael@0: "restoreDocument", michael@0: "resetRestore", michael@0: "getRestoreEpoch", michael@0: ]; michael@0: michael@0: for (let method of EXPORTED_METHODS) { michael@0: external[method] = internal[method].bind(internal); michael@0: } michael@0: michael@0: return Object.freeze(external); michael@0: } michael@0: michael@0: function ContentRestoreInternal(chromeGlobal) { michael@0: this.chromeGlobal = chromeGlobal; michael@0: michael@0: // The following fields are only valid during certain phases of the restore michael@0: // process. michael@0: michael@0: // The epoch that was passed into restoreHistory. Removed in restoreDocument. michael@0: this._epoch = 0; michael@0: michael@0: // The tabData for the restore. Set in restoreHistory and removed in michael@0: // restoreTabContent. michael@0: this._tabData = null; michael@0: michael@0: // Contains {entry, pageStyle, scrollPositions, formdata}, where entry is a michael@0: // single entry from the tabData.entries array. Set in michael@0: // restoreTabContent and removed in restoreDocument. michael@0: this._restoringDocument = null; michael@0: michael@0: // This listener is used to detect reloads on restoring tabs. Set in michael@0: // restoreHistory and removed in restoreTabContent. michael@0: this._historyListener = null; michael@0: michael@0: // This listener detects when a restoring tab has finished loading data from michael@0: // the network. Set in restoreTabContent and removed in resetRestore. michael@0: this._progressListener = null; michael@0: } michael@0: michael@0: /** michael@0: * The API for the ContentRestore module. Methods listed in EXPORTED_METHODS are michael@0: * public. michael@0: */ michael@0: ContentRestoreInternal.prototype = { michael@0: michael@0: get docShell() { michael@0: return this.chromeGlobal.docShell; michael@0: }, michael@0: michael@0: /** michael@0: * Starts the process of restoring a tab. The tabData to be restored is passed michael@0: * in here and used throughout the restoration. The epoch (which must be michael@0: * non-zero) is passed through to all the callbacks. If the tab is ever michael@0: * reloaded during the restore process, reloadCallback is called. michael@0: */ michael@0: restoreHistory: function (epoch, tabData, reloadCallback) { michael@0: this._tabData = tabData; michael@0: this._epoch = epoch; michael@0: michael@0: // In case about:blank isn't done yet. michael@0: let webNavigation = this.docShell.QueryInterface(Ci.nsIWebNavigation); michael@0: webNavigation.stop(Ci.nsIWebNavigation.STOP_ALL); michael@0: michael@0: // Make sure currentURI is set so that switch-to-tab works before the tab is michael@0: // restored. We'll reset this to about:blank when we try to restore the tab michael@0: // to ensure that docshell doeesn't get confused. michael@0: let activeIndex = tabData.index - 1; michael@0: let activePageData = tabData.entries[activeIndex] || {}; michael@0: let uri = activePageData.url || null; michael@0: if (uri) { michael@0: webNavigation.setCurrentURI(Utils.makeURI(uri)); michael@0: } michael@0: michael@0: SessionHistory.restore(this.docShell, tabData); michael@0: michael@0: // Add a listener to watch for reloads. michael@0: let listener = new HistoryListener(this.docShell, reloadCallback); michael@0: webNavigation.sessionHistory.addSHistoryListener(listener); michael@0: this._historyListener = listener; michael@0: michael@0: // Make sure to reset the capabilities and attributes in case this tab gets michael@0: // reused. michael@0: let disallow = new Set(tabData.disallow && tabData.disallow.split(",")); michael@0: DocShellCapabilities.restore(this.docShell, disallow); michael@0: michael@0: if (tabData.storage && this.docShell instanceof Ci.nsIDocShell) michael@0: SessionStorage.restore(this.docShell, tabData.storage); michael@0: }, michael@0: michael@0: /** michael@0: * Start loading the current page. When the data has finished loading from the michael@0: * network, finishCallback is called. Returns true if the load was successful. michael@0: */ michael@0: restoreTabContent: function (finishCallback) { michael@0: let tabData = this._tabData; michael@0: this._tabData = null; michael@0: michael@0: let webNavigation = this.docShell.QueryInterface(Ci.nsIWebNavigation); michael@0: let history = webNavigation.sessionHistory; michael@0: michael@0: // The reload listener is no longer needed. michael@0: this._historyListener.uninstall(); michael@0: this._historyListener = null; michael@0: michael@0: // We're about to start a load. This listener will be called when the load michael@0: // has finished getting everything from the network. michael@0: let progressListener = new ProgressListener(this.docShell, () => { michael@0: // Call resetRestore to reset the state back to normal. The data needed michael@0: // for restoreDocument (which hasn't happened yet) will remain in michael@0: // _restoringDocument. michael@0: this.resetRestore(this.docShell); michael@0: michael@0: finishCallback(); michael@0: }); michael@0: this._progressListener = progressListener; michael@0: michael@0: // Reset the current URI to about:blank. We changed it above for michael@0: // switch-to-tab, but now it must go back to the correct value before the michael@0: // load happens. michael@0: webNavigation.setCurrentURI(Utils.makeURI("about:blank")); michael@0: michael@0: try { michael@0: if (tabData.userTypedValue && tabData.userTypedClear) { michael@0: // If the user typed a URL into the URL bar and hit enter right before michael@0: // we crashed, we want to start loading that page again. A non-zero michael@0: // userTypedClear value means that the load had started. michael@0: let activeIndex = tabData.index - 1; michael@0: if (activeIndex > 0) { michael@0: // Go to the right history entry, but don't load anything yet. michael@0: history.getEntryAtIndex(activeIndex, true); michael@0: } michael@0: michael@0: // Load userTypedValue and fix up the URL if it's partial/broken. michael@0: webNavigation.loadURI(tabData.userTypedValue, michael@0: Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP, michael@0: null, null, null); michael@0: } else if (tabData.entries.length) { michael@0: // Stash away the data we need for restoreDocument. michael@0: let activeIndex = tabData.index - 1; michael@0: this._restoringDocument = {entry: tabData.entries[activeIndex] || {}, michael@0: formdata: tabData.formdata || {}, michael@0: pageStyle: tabData.pageStyle || {}, michael@0: scrollPositions: tabData.scroll || {}}; michael@0: michael@0: // In order to work around certain issues in session history, we need to michael@0: // force session history to update its internal index and call reload michael@0: // instead of gotoIndex. See bug 597315. michael@0: history.getEntryAtIndex(activeIndex, true); michael@0: history.reloadCurrentEntry(); michael@0: } else { michael@0: // If there's nothing to restore, we should still blank the page. michael@0: webNavigation.loadURI("about:blank", michael@0: Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY, michael@0: null, null, null); michael@0: } michael@0: michael@0: return true; michael@0: } catch (ex if ex instanceof Ci.nsIException) { michael@0: // Ignore page load errors, but return false to signal that the load never michael@0: // happened. michael@0: return false; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Accumulates a list of frames that need to be restored for the given browser michael@0: * element. A frame is only restored if its current URL matches the one saved michael@0: * in the session data. Each frame to be restored is returned along with its michael@0: * associated session data. michael@0: * michael@0: * @param browser the browser being restored michael@0: * @return an array of [frame, data] pairs michael@0: */ michael@0: getFramesToRestore: function (content, data) { michael@0: function hasExpectedURL(aDocument, aURL) { michael@0: return !aURL || aURL.replace(/#.*/, "") == aDocument.location.href.replace(/#.*/, ""); michael@0: } michael@0: michael@0: let frameList = []; michael@0: michael@0: function enumerateFrame(content, data) { michael@0: // Skip the frame if the user has navigated away before loading finished. michael@0: if (!hasExpectedURL(content.document, data.url)) { michael@0: return; michael@0: } michael@0: michael@0: frameList.push([content, data]); michael@0: michael@0: for (let i = 0; i < content.frames.length; i++) { michael@0: if (data.children && data.children[i]) { michael@0: enumerateFrame(content.frames[i], data.children[i]); michael@0: } michael@0: } michael@0: } michael@0: michael@0: enumerateFrame(content, data); michael@0: michael@0: return frameList; michael@0: }, michael@0: michael@0: /** michael@0: * Finish restoring the tab by filling in form data and setting the scroll michael@0: * position. The restore is complete when this function exits. It should be michael@0: * called when the "load" event fires for the restoring tab. michael@0: */ michael@0: restoreDocument: function () { michael@0: this._epoch = 0; michael@0: michael@0: if (!this._restoringDocument) { michael@0: return; michael@0: } michael@0: let {entry, pageStyle, formdata, scrollPositions} = this._restoringDocument; michael@0: this._restoringDocument = null; michael@0: michael@0: let window = this.docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow); michael@0: let frameList = this.getFramesToRestore(window, entry); michael@0: michael@0: // Support the old pageStyle format. michael@0: if (typeof(pageStyle) === "string") { michael@0: PageStyle.restore(this.docShell, frameList, pageStyle); michael@0: } else { michael@0: PageStyle.restoreTree(this.docShell, pageStyle); michael@0: } michael@0: michael@0: FormData.restoreTree(window, formdata); michael@0: ScrollPosition.restoreTree(window, scrollPositions); michael@0: michael@0: // We need to support the old form and scroll data for a while at least. michael@0: for (let [frame, data] of frameList) { michael@0: if (data.hasOwnProperty("formdata") || data.hasOwnProperty("innerHTML")) { michael@0: let formdata = data.formdata || {}; michael@0: formdata.url = data.url; michael@0: michael@0: if (data.hasOwnProperty("innerHTML")) { michael@0: formdata.innerHTML = data.innerHTML; michael@0: } michael@0: michael@0: FormData.restore(frame, formdata); michael@0: } michael@0: michael@0: ScrollPosition.restore(frame, data.scroll || ""); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Cancel an ongoing restore. This function can be called any time between michael@0: * restoreHistory and restoreDocument. michael@0: * michael@0: * This function is called externally (if a restore is canceled) and michael@0: * internally (when the loads for a restore have finished). In the latter michael@0: * case, it's called before restoreDocument, so it cannot clear michael@0: * _restoringDocument. michael@0: */ michael@0: resetRestore: function () { michael@0: this._tabData = null; michael@0: michael@0: if (this._historyListener) { michael@0: this._historyListener.uninstall(); michael@0: } michael@0: this._historyListener = null; michael@0: michael@0: if (this._progressListener) { michael@0: this._progressListener.uninstall(); michael@0: } michael@0: this._progressListener = null; michael@0: }, michael@0: michael@0: /** michael@0: * If a restore is ongoing, this function returns the value of |epoch| that michael@0: * was passed to restoreHistory. If no restore is ongoing, it returns 0. michael@0: */ michael@0: getRestoreEpoch: function () { michael@0: return this._epoch; michael@0: }, michael@0: }; michael@0: michael@0: /* michael@0: * This listener detects when a page being restored is reloaded. It triggers a michael@0: * callback and cancels the reload. The callback will send a message to michael@0: * SessionStore.jsm so that it can restore the content immediately. michael@0: */ michael@0: function HistoryListener(docShell, callback) { michael@0: let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation); michael@0: webNavigation.sessionHistory.addSHistoryListener(this); michael@0: michael@0: this.webNavigation = webNavigation; michael@0: this.callback = callback; michael@0: } michael@0: HistoryListener.prototype = { michael@0: QueryInterface: XPCOMUtils.generateQI([ michael@0: Ci.nsISHistoryListener, michael@0: Ci.nsISupportsWeakReference michael@0: ]), michael@0: michael@0: uninstall: function () { michael@0: this.webNavigation.sessionHistory.removeSHistoryListener(this); michael@0: }, michael@0: michael@0: OnHistoryNewEntry: function(newURI) {}, michael@0: OnHistoryGoBack: function(backURI) { return true; }, michael@0: OnHistoryGoForward: function(forwardURI) { return true; }, michael@0: OnHistoryGotoIndex: function(index, gotoURI) { return true; }, michael@0: OnHistoryPurge: function(numEntries) { return true; }, michael@0: OnHistoryReplaceEntry: function(index) {}, michael@0: michael@0: OnHistoryReload: function(reloadURI, reloadFlags) { michael@0: this.callback(); michael@0: michael@0: // Cancel the load. michael@0: return false; michael@0: }, michael@0: } michael@0: michael@0: /** michael@0: * This class informs SessionStore.jsm whenever the network requests for a michael@0: * restoring page have completely finished. We only restore three tabs michael@0: * simultaneously, so this is the signal for SessionStore.jsm to kick off michael@0: * another restore (if there are more to do). michael@0: */ michael@0: function ProgressListener(docShell, callback) michael@0: { michael@0: let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIWebProgress); michael@0: webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_WINDOW); michael@0: michael@0: this.webProgress = webProgress; michael@0: this.callback = callback; michael@0: } michael@0: ProgressListener.prototype = { michael@0: QueryInterface: XPCOMUtils.generateQI([ michael@0: Ci.nsIWebProgressListener, michael@0: Ci.nsISupportsWeakReference michael@0: ]), michael@0: michael@0: uninstall: function() { michael@0: this.webProgress.removeProgressListener(this); michael@0: }, michael@0: michael@0: onStateChange: function(webProgress, request, stateFlags, status) { michael@0: if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP && michael@0: stateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK && michael@0: stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) { michael@0: this.callback(); michael@0: } michael@0: }, michael@0: michael@0: onLocationChange: function() {}, michael@0: onProgressChange: function() {}, michael@0: onStatusChange: function() {}, michael@0: onSecurityChange: function() {}, michael@0: };