|
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 }; |