browser/components/sessionstore/src/ContentRestore.jsm

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

mercurial