browser/components/sessionstore/src/ContentRestore.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 "use strict";
michael@0 6
michael@0 7 this.EXPORTED_SYMBOLS = ["ContentRestore"];
michael@0 8
michael@0 9 const Cu = Components.utils;
michael@0 10 const Ci = Components.interfaces;
michael@0 11
michael@0 12 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
michael@0 13
michael@0 14 XPCOMUtils.defineLazyModuleGetter(this, "DocShellCapabilities",
michael@0 15 "resource:///modules/sessionstore/DocShellCapabilities.jsm");
michael@0 16 XPCOMUtils.defineLazyModuleGetter(this, "FormData",
michael@0 17 "resource://gre/modules/FormData.jsm");
michael@0 18 XPCOMUtils.defineLazyModuleGetter(this, "PageStyle",
michael@0 19 "resource:///modules/sessionstore/PageStyle.jsm");
michael@0 20 XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition",
michael@0 21 "resource://gre/modules/ScrollPosition.jsm");
michael@0 22 XPCOMUtils.defineLazyModuleGetter(this, "SessionHistory",
michael@0 23 "resource:///modules/sessionstore/SessionHistory.jsm");
michael@0 24 XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage",
michael@0 25 "resource:///modules/sessionstore/SessionStorage.jsm");
michael@0 26 XPCOMUtils.defineLazyModuleGetter(this, "Utils",
michael@0 27 "resource:///modules/sessionstore/Utils.jsm");
michael@0 28
michael@0 29 /**
michael@0 30 * This module implements the content side of session restoration. The chrome
michael@0 31 * side is handled by SessionStore.jsm. The functions in this module are called
michael@0 32 * by content-sessionStore.js based on messages received from SessionStore.jsm
michael@0 33 * (or, in one case, based on a "load" event). Each tab has its own
michael@0 34 * ContentRestore instance, constructed by content-sessionStore.js.
michael@0 35 *
michael@0 36 * In a typical restore, content-sessionStore.js will call the following based
michael@0 37 * on messages and events it receives:
michael@0 38 *
michael@0 39 * restoreHistory(epoch, tabData, reloadCallback)
michael@0 40 * Restores the tab's history and session cookies.
michael@0 41 * restoreTabContent(finishCallback)
michael@0 42 * Starts loading the data for the current page to restore.
michael@0 43 * restoreDocument()
michael@0 44 * Restore form and scroll data.
michael@0 45 *
michael@0 46 * When the page has been loaded from the network, we call finishCallback. It
michael@0 47 * should send a message to SessionStore.jsm, which may cause other tabs to be
michael@0 48 * restored.
michael@0 49 *
michael@0 50 * When the page has finished loading, a "load" event will trigger in
michael@0 51 * content-sessionStore.js, which will call restoreDocument. At that point,
michael@0 52 * form data is restored and the restore is complete.
michael@0 53 *
michael@0 54 * At any time, SessionStore.jsm can cancel the ongoing restore by sending a
michael@0 55 * reset message, which causes resetRestore to be called. At that point it's
michael@0 56 * legal to begin another restore.
michael@0 57 *
michael@0 58 * The epoch that is passed into restoreHistory is merely a token. All messages
michael@0 59 * sent back to SessionStore.jsm include the epoch. This way, SessionStore.jsm
michael@0 60 * can discard messages that relate to restores that it has canceled (by
michael@0 61 * starting a new restore, say).
michael@0 62 */
michael@0 63 function ContentRestore(chromeGlobal) {
michael@0 64 let internal = new ContentRestoreInternal(chromeGlobal);
michael@0 65 let external = {};
michael@0 66
michael@0 67 let EXPORTED_METHODS = ["restoreHistory",
michael@0 68 "restoreTabContent",
michael@0 69 "restoreDocument",
michael@0 70 "resetRestore",
michael@0 71 "getRestoreEpoch",
michael@0 72 ];
michael@0 73
michael@0 74 for (let method of EXPORTED_METHODS) {
michael@0 75 external[method] = internal[method].bind(internal);
michael@0 76 }
michael@0 77
michael@0 78 return Object.freeze(external);
michael@0 79 }
michael@0 80
michael@0 81 function ContentRestoreInternal(chromeGlobal) {
michael@0 82 this.chromeGlobal = chromeGlobal;
michael@0 83
michael@0 84 // The following fields are only valid during certain phases of the restore
michael@0 85 // process.
michael@0 86
michael@0 87 // The epoch that was passed into restoreHistory. Removed in restoreDocument.
michael@0 88 this._epoch = 0;
michael@0 89
michael@0 90 // The tabData for the restore. Set in restoreHistory and removed in
michael@0 91 // restoreTabContent.
michael@0 92 this._tabData = null;
michael@0 93
michael@0 94 // Contains {entry, pageStyle, scrollPositions, formdata}, where entry is a
michael@0 95 // single entry from the tabData.entries array. Set in
michael@0 96 // restoreTabContent and removed in restoreDocument.
michael@0 97 this._restoringDocument = null;
michael@0 98
michael@0 99 // This listener is used to detect reloads on restoring tabs. Set in
michael@0 100 // restoreHistory and removed in restoreTabContent.
michael@0 101 this._historyListener = null;
michael@0 102
michael@0 103 // This listener detects when a restoring tab has finished loading data from
michael@0 104 // the network. Set in restoreTabContent and removed in resetRestore.
michael@0 105 this._progressListener = null;
michael@0 106 }
michael@0 107
michael@0 108 /**
michael@0 109 * The API for the ContentRestore module. Methods listed in EXPORTED_METHODS are
michael@0 110 * public.
michael@0 111 */
michael@0 112 ContentRestoreInternal.prototype = {
michael@0 113
michael@0 114 get docShell() {
michael@0 115 return this.chromeGlobal.docShell;
michael@0 116 },
michael@0 117
michael@0 118 /**
michael@0 119 * Starts the process of restoring a tab. The tabData to be restored is passed
michael@0 120 * in here and used throughout the restoration. The epoch (which must be
michael@0 121 * non-zero) is passed through to all the callbacks. If the tab is ever
michael@0 122 * reloaded during the restore process, reloadCallback is called.
michael@0 123 */
michael@0 124 restoreHistory: function (epoch, tabData, reloadCallback) {
michael@0 125 this._tabData = tabData;
michael@0 126 this._epoch = epoch;
michael@0 127
michael@0 128 // In case about:blank isn't done yet.
michael@0 129 let webNavigation = this.docShell.QueryInterface(Ci.nsIWebNavigation);
michael@0 130 webNavigation.stop(Ci.nsIWebNavigation.STOP_ALL);
michael@0 131
michael@0 132 // Make sure currentURI is set so that switch-to-tab works before the tab is
michael@0 133 // restored. We'll reset this to about:blank when we try to restore the tab
michael@0 134 // to ensure that docshell doeesn't get confused.
michael@0 135 let activeIndex = tabData.index - 1;
michael@0 136 let activePageData = tabData.entries[activeIndex] || {};
michael@0 137 let uri = activePageData.url || null;
michael@0 138 if (uri) {
michael@0 139 webNavigation.setCurrentURI(Utils.makeURI(uri));
michael@0 140 }
michael@0 141
michael@0 142 SessionHistory.restore(this.docShell, tabData);
michael@0 143
michael@0 144 // Add a listener to watch for reloads.
michael@0 145 let listener = new HistoryListener(this.docShell, reloadCallback);
michael@0 146 webNavigation.sessionHistory.addSHistoryListener(listener);
michael@0 147 this._historyListener = listener;
michael@0 148
michael@0 149 // Make sure to reset the capabilities and attributes in case this tab gets
michael@0 150 // reused.
michael@0 151 let disallow = new Set(tabData.disallow && tabData.disallow.split(","));
michael@0 152 DocShellCapabilities.restore(this.docShell, disallow);
michael@0 153
michael@0 154 if (tabData.storage && this.docShell instanceof Ci.nsIDocShell)
michael@0 155 SessionStorage.restore(this.docShell, tabData.storage);
michael@0 156 },
michael@0 157
michael@0 158 /**
michael@0 159 * Start loading the current page. When the data has finished loading from the
michael@0 160 * network, finishCallback is called. Returns true if the load was successful.
michael@0 161 */
michael@0 162 restoreTabContent: function (finishCallback) {
michael@0 163 let tabData = this._tabData;
michael@0 164 this._tabData = null;
michael@0 165
michael@0 166 let webNavigation = this.docShell.QueryInterface(Ci.nsIWebNavigation);
michael@0 167 let history = webNavigation.sessionHistory;
michael@0 168
michael@0 169 // The reload listener is no longer needed.
michael@0 170 this._historyListener.uninstall();
michael@0 171 this._historyListener = null;
michael@0 172
michael@0 173 // We're about to start a load. This listener will be called when the load
michael@0 174 // has finished getting everything from the network.
michael@0 175 let progressListener = new ProgressListener(this.docShell, () => {
michael@0 176 // Call resetRestore to reset the state back to normal. The data needed
michael@0 177 // for restoreDocument (which hasn't happened yet) will remain in
michael@0 178 // _restoringDocument.
michael@0 179 this.resetRestore(this.docShell);
michael@0 180
michael@0 181 finishCallback();
michael@0 182 });
michael@0 183 this._progressListener = progressListener;
michael@0 184
michael@0 185 // Reset the current URI to about:blank. We changed it above for
michael@0 186 // switch-to-tab, but now it must go back to the correct value before the
michael@0 187 // load happens.
michael@0 188 webNavigation.setCurrentURI(Utils.makeURI("about:blank"));
michael@0 189
michael@0 190 try {
michael@0 191 if (tabData.userTypedValue && tabData.userTypedClear) {
michael@0 192 // If the user typed a URL into the URL bar and hit enter right before
michael@0 193 // we crashed, we want to start loading that page again. A non-zero
michael@0 194 // userTypedClear value means that the load had started.
michael@0 195 let activeIndex = tabData.index - 1;
michael@0 196 if (activeIndex > 0) {
michael@0 197 // Go to the right history entry, but don't load anything yet.
michael@0 198 history.getEntryAtIndex(activeIndex, true);
michael@0 199 }
michael@0 200
michael@0 201 // Load userTypedValue and fix up the URL if it's partial/broken.
michael@0 202 webNavigation.loadURI(tabData.userTypedValue,
michael@0 203 Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP,
michael@0 204 null, null, null);
michael@0 205 } else if (tabData.entries.length) {
michael@0 206 // Stash away the data we need for restoreDocument.
michael@0 207 let activeIndex = tabData.index - 1;
michael@0 208 this._restoringDocument = {entry: tabData.entries[activeIndex] || {},
michael@0 209 formdata: tabData.formdata || {},
michael@0 210 pageStyle: tabData.pageStyle || {},
michael@0 211 scrollPositions: tabData.scroll || {}};
michael@0 212
michael@0 213 // In order to work around certain issues in session history, we need to
michael@0 214 // force session history to update its internal index and call reload
michael@0 215 // instead of gotoIndex. See bug 597315.
michael@0 216 history.getEntryAtIndex(activeIndex, true);
michael@0 217 history.reloadCurrentEntry();
michael@0 218 } else {
michael@0 219 // If there's nothing to restore, we should still blank the page.
michael@0 220 webNavigation.loadURI("about:blank",
michael@0 221 Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY,
michael@0 222 null, null, null);
michael@0 223 }
michael@0 224
michael@0 225 return true;
michael@0 226 } catch (ex if ex instanceof Ci.nsIException) {
michael@0 227 // Ignore page load errors, but return false to signal that the load never
michael@0 228 // happened.
michael@0 229 return false;
michael@0 230 }
michael@0 231 },
michael@0 232
michael@0 233 /**
michael@0 234 * Accumulates a list of frames that need to be restored for the given browser
michael@0 235 * element. A frame is only restored if its current URL matches the one saved
michael@0 236 * in the session data. Each frame to be restored is returned along with its
michael@0 237 * associated session data.
michael@0 238 *
michael@0 239 * @param browser the browser being restored
michael@0 240 * @return an array of [frame, data] pairs
michael@0 241 */
michael@0 242 getFramesToRestore: function (content, data) {
michael@0 243 function hasExpectedURL(aDocument, aURL) {
michael@0 244 return !aURL || aURL.replace(/#.*/, "") == aDocument.location.href.replace(/#.*/, "");
michael@0 245 }
michael@0 246
michael@0 247 let frameList = [];
michael@0 248
michael@0 249 function enumerateFrame(content, data) {
michael@0 250 // Skip the frame if the user has navigated away before loading finished.
michael@0 251 if (!hasExpectedURL(content.document, data.url)) {
michael@0 252 return;
michael@0 253 }
michael@0 254
michael@0 255 frameList.push([content, data]);
michael@0 256
michael@0 257 for (let i = 0; i < content.frames.length; i++) {
michael@0 258 if (data.children && data.children[i]) {
michael@0 259 enumerateFrame(content.frames[i], data.children[i]);
michael@0 260 }
michael@0 261 }
michael@0 262 }
michael@0 263
michael@0 264 enumerateFrame(content, data);
michael@0 265
michael@0 266 return frameList;
michael@0 267 },
michael@0 268
michael@0 269 /**
michael@0 270 * Finish restoring the tab by filling in form data and setting the scroll
michael@0 271 * position. The restore is complete when this function exits. It should be
michael@0 272 * called when the "load" event fires for the restoring tab.
michael@0 273 */
michael@0 274 restoreDocument: function () {
michael@0 275 this._epoch = 0;
michael@0 276
michael@0 277 if (!this._restoringDocument) {
michael@0 278 return;
michael@0 279 }
michael@0 280 let {entry, pageStyle, formdata, scrollPositions} = this._restoringDocument;
michael@0 281 this._restoringDocument = null;
michael@0 282
michael@0 283 let window = this.docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
michael@0 284 let frameList = this.getFramesToRestore(window, entry);
michael@0 285
michael@0 286 // Support the old pageStyle format.
michael@0 287 if (typeof(pageStyle) === "string") {
michael@0 288 PageStyle.restore(this.docShell, frameList, pageStyle);
michael@0 289 } else {
michael@0 290 PageStyle.restoreTree(this.docShell, pageStyle);
michael@0 291 }
michael@0 292
michael@0 293 FormData.restoreTree(window, formdata);
michael@0 294 ScrollPosition.restoreTree(window, scrollPositions);
michael@0 295
michael@0 296 // We need to support the old form and scroll data for a while at least.
michael@0 297 for (let [frame, data] of frameList) {
michael@0 298 if (data.hasOwnProperty("formdata") || data.hasOwnProperty("innerHTML")) {
michael@0 299 let formdata = data.formdata || {};
michael@0 300 formdata.url = data.url;
michael@0 301
michael@0 302 if (data.hasOwnProperty("innerHTML")) {
michael@0 303 formdata.innerHTML = data.innerHTML;
michael@0 304 }
michael@0 305
michael@0 306 FormData.restore(frame, formdata);
michael@0 307 }
michael@0 308
michael@0 309 ScrollPosition.restore(frame, data.scroll || "");
michael@0 310 }
michael@0 311 },
michael@0 312
michael@0 313 /**
michael@0 314 * Cancel an ongoing restore. This function can be called any time between
michael@0 315 * restoreHistory and restoreDocument.
michael@0 316 *
michael@0 317 * This function is called externally (if a restore is canceled) and
michael@0 318 * internally (when the loads for a restore have finished). In the latter
michael@0 319 * case, it's called before restoreDocument, so it cannot clear
michael@0 320 * _restoringDocument.
michael@0 321 */
michael@0 322 resetRestore: function () {
michael@0 323 this._tabData = null;
michael@0 324
michael@0 325 if (this._historyListener) {
michael@0 326 this._historyListener.uninstall();
michael@0 327 }
michael@0 328 this._historyListener = null;
michael@0 329
michael@0 330 if (this._progressListener) {
michael@0 331 this._progressListener.uninstall();
michael@0 332 }
michael@0 333 this._progressListener = null;
michael@0 334 },
michael@0 335
michael@0 336 /**
michael@0 337 * If a restore is ongoing, this function returns the value of |epoch| that
michael@0 338 * was passed to restoreHistory. If no restore is ongoing, it returns 0.
michael@0 339 */
michael@0 340 getRestoreEpoch: function () {
michael@0 341 return this._epoch;
michael@0 342 },
michael@0 343 };
michael@0 344
michael@0 345 /*
michael@0 346 * This listener detects when a page being restored is reloaded. It triggers a
michael@0 347 * callback and cancels the reload. The callback will send a message to
michael@0 348 * SessionStore.jsm so that it can restore the content immediately.
michael@0 349 */
michael@0 350 function HistoryListener(docShell, callback) {
michael@0 351 let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
michael@0 352 webNavigation.sessionHistory.addSHistoryListener(this);
michael@0 353
michael@0 354 this.webNavigation = webNavigation;
michael@0 355 this.callback = callback;
michael@0 356 }
michael@0 357 HistoryListener.prototype = {
michael@0 358 QueryInterface: XPCOMUtils.generateQI([
michael@0 359 Ci.nsISHistoryListener,
michael@0 360 Ci.nsISupportsWeakReference
michael@0 361 ]),
michael@0 362
michael@0 363 uninstall: function () {
michael@0 364 this.webNavigation.sessionHistory.removeSHistoryListener(this);
michael@0 365 },
michael@0 366
michael@0 367 OnHistoryNewEntry: function(newURI) {},
michael@0 368 OnHistoryGoBack: function(backURI) { return true; },
michael@0 369 OnHistoryGoForward: function(forwardURI) { return true; },
michael@0 370 OnHistoryGotoIndex: function(index, gotoURI) { return true; },
michael@0 371 OnHistoryPurge: function(numEntries) { return true; },
michael@0 372 OnHistoryReplaceEntry: function(index) {},
michael@0 373
michael@0 374 OnHistoryReload: function(reloadURI, reloadFlags) {
michael@0 375 this.callback();
michael@0 376
michael@0 377 // Cancel the load.
michael@0 378 return false;
michael@0 379 },
michael@0 380 }
michael@0 381
michael@0 382 /**
michael@0 383 * This class informs SessionStore.jsm whenever the network requests for a
michael@0 384 * restoring page have completely finished. We only restore three tabs
michael@0 385 * simultaneously, so this is the signal for SessionStore.jsm to kick off
michael@0 386 * another restore (if there are more to do).
michael@0 387 */
michael@0 388 function ProgressListener(docShell, callback)
michael@0 389 {
michael@0 390 let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 391 .getInterface(Ci.nsIWebProgress);
michael@0 392 webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_WINDOW);
michael@0 393
michael@0 394 this.webProgress = webProgress;
michael@0 395 this.callback = callback;
michael@0 396 }
michael@0 397 ProgressListener.prototype = {
michael@0 398 QueryInterface: XPCOMUtils.generateQI([
michael@0 399 Ci.nsIWebProgressListener,
michael@0 400 Ci.nsISupportsWeakReference
michael@0 401 ]),
michael@0 402
michael@0 403 uninstall: function() {
michael@0 404 this.webProgress.removeProgressListener(this);
michael@0 405 },
michael@0 406
michael@0 407 onStateChange: function(webProgress, request, stateFlags, status) {
michael@0 408 if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
michael@0 409 stateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
michael@0 410 stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) {
michael@0 411 this.callback();
michael@0 412 }
michael@0 413 },
michael@0 414
michael@0 415 onLocationChange: function() {},
michael@0 416 onProgressChange: function() {},
michael@0 417 onStatusChange: function() {},
michael@0 418 onSecurityChange: function() {},
michael@0 419 };

mercurial