forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			431 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			431 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* 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, false, 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, isRemotenessUpdate, 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 (!isRemotenessUpdate) {
 | |
|       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;
 | |
| 
 | |
|         if (loadArguments.userContextId) {
 | |
|           webNavigation.setOriginAttributesBeforeLoading({ userContextId: loadArguments.userContextId });
 | |
|         }
 | |
| 
 | |
|         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() {},
 | |
| };
 | 
