|
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 |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 const Cc = Components.classes; |
|
6 const Ci = Components.interfaces; |
|
7 const Cu = Components.utils; |
|
8 const Cr = Components.results; |
|
9 |
|
10 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
11 Cu.import("resource://gre/modules/Services.jsm"); |
|
12 |
|
13 #ifdef MOZ_CRASHREPORTER |
|
14 XPCOMUtils.defineLazyServiceGetter(this, "CrashReporter", |
|
15 "@mozilla.org/xre/app-info;1", "nsICrashReporter"); |
|
16 #endif |
|
17 |
|
18 XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm"); |
|
19 XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm"); |
|
20 XPCOMUtils.defineLazyModuleGetter(this, "sendMessageToJava", "resource://gre/modules/Messaging.jsm"); |
|
21 |
|
22 function dump(a) { |
|
23 Services.console.logStringMessage(a); |
|
24 } |
|
25 |
|
26 // ----------------------------------------------------------------------- |
|
27 // Session Store |
|
28 // ----------------------------------------------------------------------- |
|
29 |
|
30 const STATE_STOPPED = 0; |
|
31 const STATE_RUNNING = 1; |
|
32 |
|
33 function SessionStore() { } |
|
34 |
|
35 SessionStore.prototype = { |
|
36 classID: Components.ID("{8c1f07d6-cba3-4226-a315-8bd43d67d032}"), |
|
37 |
|
38 QueryInterface: XPCOMUtils.generateQI([Ci.nsISessionStore, |
|
39 Ci.nsIDOMEventListener, |
|
40 Ci.nsIObserver, |
|
41 Ci.nsISupportsWeakReference]), |
|
42 |
|
43 _windows: {}, |
|
44 _lastSaveTime: 0, |
|
45 _interval: 10000, |
|
46 _maxTabsUndo: 1, |
|
47 _pendingWrite: 0, |
|
48 |
|
49 init: function ss_init() { |
|
50 // Get file references |
|
51 this._sessionFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile); |
|
52 this._sessionFileBackup = this._sessionFile.clone(); |
|
53 this._sessionFile.append("sessionstore.js"); |
|
54 this._sessionFileBackup.append("sessionstore.bak"); |
|
55 |
|
56 this._loadState = STATE_STOPPED; |
|
57 |
|
58 this._interval = Services.prefs.getIntPref("browser.sessionstore.interval"); |
|
59 this._maxTabsUndo = Services.prefs.getIntPref("browser.sessionstore.max_tabs_undo"); |
|
60 }, |
|
61 |
|
62 _clearDisk: function ss_clearDisk() { |
|
63 OS.File.remove(this._sessionFile.path); |
|
64 OS.File.remove(this._sessionFileBackup.path); |
|
65 }, |
|
66 |
|
67 observe: function ss_observe(aSubject, aTopic, aData) { |
|
68 let self = this; |
|
69 let observerService = Services.obs; |
|
70 switch (aTopic) { |
|
71 case "app-startup": |
|
72 observerService.addObserver(this, "final-ui-startup", true); |
|
73 observerService.addObserver(this, "domwindowopened", true); |
|
74 observerService.addObserver(this, "domwindowclosed", true); |
|
75 observerService.addObserver(this, "browser:purge-session-history", true); |
|
76 observerService.addObserver(this, "Session:Restore", true); |
|
77 observerService.addObserver(this, "application-background", true); |
|
78 break; |
|
79 case "final-ui-startup": |
|
80 observerService.removeObserver(this, "final-ui-startup"); |
|
81 this.init(); |
|
82 break; |
|
83 case "domwindowopened": { |
|
84 let window = aSubject; |
|
85 window.addEventListener("load", function() { |
|
86 self.onWindowOpen(window); |
|
87 window.removeEventListener("load", arguments.callee, false); |
|
88 }, false); |
|
89 break; |
|
90 } |
|
91 case "domwindowclosed": // catch closed windows |
|
92 this.onWindowClose(aSubject); |
|
93 break; |
|
94 case "browser:purge-session-history": // catch sanitization |
|
95 this._clearDisk(); |
|
96 |
|
97 // Clear all data about closed tabs |
|
98 for (let [ssid, win] in Iterator(this._windows)) |
|
99 win.closedTabs = []; |
|
100 |
|
101 if (this._loadState == STATE_RUNNING) { |
|
102 // Save the purged state immediately |
|
103 this.saveState(); |
|
104 } |
|
105 |
|
106 Services.obs.notifyObservers(null, "sessionstore-state-purge-complete", ""); |
|
107 break; |
|
108 case "timer-callback": |
|
109 // Timer call back for delayed saving |
|
110 this._saveTimer = null; |
|
111 if (this._pendingWrite) { |
|
112 this.saveState(); |
|
113 } |
|
114 break; |
|
115 case "Session:Restore": { |
|
116 Services.obs.removeObserver(this, "Session:Restore"); |
|
117 if (aData) { |
|
118 // Be ready to handle any restore failures by making sure we have a valid tab opened |
|
119 let window = Services.wm.getMostRecentWindow("navigator:browser"); |
|
120 let restoreCleanup = { |
|
121 observe: function (aSubject, aTopic, aData) { |
|
122 Services.obs.removeObserver(restoreCleanup, "sessionstore-windows-restored"); |
|
123 |
|
124 if (window.BrowserApp.tabs.length == 0) { |
|
125 window.BrowserApp.addTab("about:home", { |
|
126 selected: true |
|
127 }); |
|
128 } |
|
129 |
|
130 // Let Java know we're done restoring tabs so tabs added after this can be animated |
|
131 sendMessageToJava({ |
|
132 type: "Session:RestoreEnd" |
|
133 }); |
|
134 }.bind(this) |
|
135 }; |
|
136 Services.obs.addObserver(restoreCleanup, "sessionstore-windows-restored", false); |
|
137 |
|
138 // Do a restore, triggered by Java |
|
139 let data = JSON.parse(aData); |
|
140 this.restoreLastSession(data.sessionString); |
|
141 } else { |
|
142 // Not doing a restore; just send restore message |
|
143 Services.obs.notifyObservers(null, "sessionstore-windows-restored", ""); |
|
144 } |
|
145 break; |
|
146 } |
|
147 case "application-background": |
|
148 // We receive this notification when Android's onPause callback is |
|
149 // executed. After onPause, the application may be terminated at any |
|
150 // point without notice; therefore, we must synchronously write out any |
|
151 // pending save state to ensure that this data does not get lost. |
|
152 this.flushPendingState(); |
|
153 break; |
|
154 } |
|
155 }, |
|
156 |
|
157 handleEvent: function ss_handleEvent(aEvent) { |
|
158 let window = aEvent.currentTarget.ownerDocument.defaultView; |
|
159 switch (aEvent.type) { |
|
160 case "TabOpen": { |
|
161 let browser = aEvent.target; |
|
162 this.onTabAdd(window, browser); |
|
163 break; |
|
164 } |
|
165 case "TabClose": { |
|
166 let browser = aEvent.target; |
|
167 this.onTabClose(window, browser); |
|
168 this.onTabRemove(window, browser); |
|
169 break; |
|
170 } |
|
171 case "TabSelect": { |
|
172 let browser = aEvent.target; |
|
173 this.onTabSelect(window, browser); |
|
174 break; |
|
175 } |
|
176 case "DOMTitleChanged": { |
|
177 let browser = aEvent.currentTarget; |
|
178 |
|
179 // Handle only top-level DOMTitleChanged event |
|
180 if (browser.contentDocument !== aEvent.originalTarget) |
|
181 return; |
|
182 |
|
183 // Use DOMTitleChanged to detect page loads over alternatives. |
|
184 // onLocationChange happens too early, so we don't have the page title |
|
185 // yet; pageshow happens too late, so we could lose session data if the |
|
186 // browser were killed. |
|
187 this.onTabLoad(window, browser); |
|
188 break; |
|
189 } |
|
190 } |
|
191 }, |
|
192 |
|
193 onWindowOpen: function ss_onWindowOpen(aWindow) { |
|
194 // Return if window has already been initialized |
|
195 if (aWindow && aWindow.__SSID && this._windows[aWindow.__SSID]) |
|
196 return; |
|
197 |
|
198 // Ignore non-browser windows and windows opened while shutting down |
|
199 if (aWindow.document.documentElement.getAttribute("windowtype") != "navigator:browser") |
|
200 return; |
|
201 |
|
202 // Assign it a unique identifier (timestamp) and create its data object |
|
203 aWindow.__SSID = "window" + Date.now(); |
|
204 this._windows[aWindow.__SSID] = { tabs: [], selected: 0, closedTabs: [] }; |
|
205 |
|
206 // Perform additional initialization when the first window is loading |
|
207 if (this._loadState == STATE_STOPPED) { |
|
208 this._loadState = STATE_RUNNING; |
|
209 this._lastSaveTime = Date.now(); |
|
210 } |
|
211 |
|
212 // Add tab change listeners to all already existing tabs |
|
213 let tabs = aWindow.BrowserApp.tabs; |
|
214 for (let i = 0; i < tabs.length; i++) |
|
215 this.onTabAdd(aWindow, tabs[i].browser, true); |
|
216 |
|
217 // Notification of tab add/remove/selection |
|
218 let browsers = aWindow.document.getElementById("browsers"); |
|
219 browsers.addEventListener("TabOpen", this, true); |
|
220 browsers.addEventListener("TabClose", this, true); |
|
221 browsers.addEventListener("TabSelect", this, true); |
|
222 }, |
|
223 |
|
224 onWindowClose: function ss_onWindowClose(aWindow) { |
|
225 // Ignore windows not tracked by SessionStore |
|
226 if (!aWindow.__SSID || !this._windows[aWindow.__SSID]) |
|
227 return; |
|
228 |
|
229 let browsers = aWindow.document.getElementById("browsers"); |
|
230 browsers.removeEventListener("TabOpen", this, true); |
|
231 browsers.removeEventListener("TabClose", this, true); |
|
232 browsers.removeEventListener("TabSelect", this, true); |
|
233 |
|
234 if (this._loadState == STATE_RUNNING) { |
|
235 // Update all window data for a last time |
|
236 this._collectWindowData(aWindow); |
|
237 |
|
238 // Clear this window from the list |
|
239 delete this._windows[aWindow.__SSID]; |
|
240 |
|
241 // Save the state without this window to disk |
|
242 this.saveStateDelayed(); |
|
243 } |
|
244 |
|
245 let tabs = aWindow.BrowserApp.tabs; |
|
246 for (let i = 0; i < tabs.length; i++) |
|
247 this.onTabRemove(aWindow, tabs[i].browser, true); |
|
248 |
|
249 delete aWindow.__SSID; |
|
250 }, |
|
251 |
|
252 onTabAdd: function ss_onTabAdd(aWindow, aBrowser, aNoNotification) { |
|
253 aBrowser.addEventListener("DOMTitleChanged", this, true); |
|
254 if (!aNoNotification) |
|
255 this.saveStateDelayed(); |
|
256 this._updateCrashReportURL(aWindow); |
|
257 }, |
|
258 |
|
259 onTabRemove: function ss_onTabRemove(aWindow, aBrowser, aNoNotification) { |
|
260 aBrowser.removeEventListener("DOMTitleChanged", this, true); |
|
261 |
|
262 // If this browser is being restored, skip any session save activity |
|
263 if (aBrowser.__SS_restore) |
|
264 return; |
|
265 |
|
266 delete aBrowser.__SS_data; |
|
267 |
|
268 if (!aNoNotification) |
|
269 this.saveStateDelayed(); |
|
270 }, |
|
271 |
|
272 onTabClose: function ss_onTabClose(aWindow, aBrowser) { |
|
273 if (this._maxTabsUndo == 0) |
|
274 return; |
|
275 |
|
276 if (aWindow.BrowserApp.tabs.length > 0) { |
|
277 // Bundle this browser's data and extra data and save in the closedTabs |
|
278 // window property |
|
279 let data = aBrowser.__SS_data; |
|
280 data.extData = aBrowser.__SS_extdata; |
|
281 |
|
282 this._windows[aWindow.__SSID].closedTabs.unshift(data); |
|
283 let length = this._windows[aWindow.__SSID].closedTabs.length; |
|
284 if (length > this._maxTabsUndo) |
|
285 this._windows[aWindow.__SSID].closedTabs.splice(this._maxTabsUndo, length - this._maxTabsUndo); |
|
286 } |
|
287 }, |
|
288 |
|
289 onTabLoad: function ss_onTabLoad(aWindow, aBrowser) { |
|
290 // If this browser is being restored, skip any session save activity |
|
291 if (aBrowser.__SS_restore) |
|
292 return; |
|
293 |
|
294 // Ignore a transient "about:blank" |
|
295 if (!aBrowser.canGoBack && aBrowser.currentURI.spec == "about:blank") |
|
296 return; |
|
297 |
|
298 let history = aBrowser.sessionHistory; |
|
299 |
|
300 // Serialize the tab data |
|
301 let entries = []; |
|
302 let index = history.index + 1; |
|
303 for (let i = 0; i < history.count; i++) { |
|
304 let historyEntry = history.getEntryAtIndex(i, false); |
|
305 // Don't try to restore wyciwyg URLs |
|
306 if (historyEntry.URI.schemeIs("wyciwyg")) { |
|
307 // Adjust the index to account for skipped history entries |
|
308 if (i <= history.index) |
|
309 index--; |
|
310 continue; |
|
311 } |
|
312 let entry = this._serializeHistoryEntry(historyEntry); |
|
313 entries.push(entry); |
|
314 } |
|
315 let data = { entries: entries, index: index }; |
|
316 |
|
317 delete aBrowser.__SS_data; |
|
318 this._collectTabData(aWindow, aBrowser, data); |
|
319 this.saveStateDelayed(); |
|
320 |
|
321 this._updateCrashReportURL(aWindow); |
|
322 }, |
|
323 |
|
324 onTabSelect: function ss_onTabSelect(aWindow, aBrowser) { |
|
325 if (this._loadState != STATE_RUNNING) |
|
326 return; |
|
327 |
|
328 let browsers = aWindow.document.getElementById("browsers"); |
|
329 let index = browsers.selectedIndex; |
|
330 this._windows[aWindow.__SSID].selected = parseInt(index) + 1; // 1-based |
|
331 |
|
332 // Restore the resurrected browser |
|
333 if (aBrowser.__SS_restore) { |
|
334 let data = aBrowser.__SS_data; |
|
335 if (data.entries.length > 0) |
|
336 this._restoreHistory(data, aBrowser.sessionHistory); |
|
337 |
|
338 delete aBrowser.__SS_restore; |
|
339 aBrowser.removeAttribute("pending"); |
|
340 } |
|
341 |
|
342 this.saveStateDelayed(); |
|
343 this._updateCrashReportURL(aWindow); |
|
344 }, |
|
345 |
|
346 saveStateDelayed: function ss_saveStateDelayed() { |
|
347 if (!this._saveTimer) { |
|
348 // Interval until the next disk operation is allowed |
|
349 let minimalDelay = this._lastSaveTime + this._interval - Date.now(); |
|
350 |
|
351 // If we have to wait, set a timer, otherwise saveState directly |
|
352 let delay = Math.max(minimalDelay, 2000); |
|
353 if (delay > 0) { |
|
354 this._pendingWrite++; |
|
355 this._saveTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); |
|
356 this._saveTimer.init(this, delay, Ci.nsITimer.TYPE_ONE_SHOT); |
|
357 } else { |
|
358 this.saveState(); |
|
359 } |
|
360 } |
|
361 }, |
|
362 |
|
363 saveState: function ss_saveState() { |
|
364 this._pendingWrite++; |
|
365 this._saveState(true); |
|
366 }, |
|
367 |
|
368 // Immediately and synchronously writes any pending state to disk. |
|
369 flushPendingState: function ss_flushPendingState() { |
|
370 if (this._pendingWrite) { |
|
371 this._saveState(false); |
|
372 } |
|
373 }, |
|
374 |
|
375 _saveState: function ss_saveState(aAsync) { |
|
376 // Kill any queued timer and save immediately |
|
377 if (this._saveTimer) { |
|
378 this._saveTimer.cancel(); |
|
379 this._saveTimer = null; |
|
380 } |
|
381 |
|
382 let data = this._getCurrentState(); |
|
383 let normalData = { windows: [] }; |
|
384 let privateData = { windows: [] }; |
|
385 |
|
386 for (let winIndex = 0; winIndex < data.windows.length; ++winIndex) { |
|
387 let win = data.windows[winIndex]; |
|
388 let normalWin = {}; |
|
389 for (let prop in win) { |
|
390 normalWin[prop] = data[prop]; |
|
391 } |
|
392 normalWin.tabs = []; |
|
393 normalData.windows.push(normalWin); |
|
394 privateData.windows.push({ tabs: [] }); |
|
395 |
|
396 // Split the session data into private and non-private data objects. |
|
397 // Non-private session data will be saved to disk, and private session |
|
398 // data will be sent to Java for Android to hold it in memory. |
|
399 for (let i = 0; i < win.tabs.length; ++i) { |
|
400 let tab = win.tabs[i]; |
|
401 let savedWin = tab.isPrivate ? privateData.windows[winIndex] : normalData.windows[winIndex]; |
|
402 savedWin.tabs.push(tab); |
|
403 if (win.selected == i + 1) { |
|
404 savedWin.selected = savedWin.tabs.length; |
|
405 } |
|
406 } |
|
407 } |
|
408 |
|
409 // Write only non-private data to disk |
|
410 this._writeFile(this._sessionFile, JSON.stringify(normalData), aAsync); |
|
411 |
|
412 // If we have private data, send it to Java; otherwise, send null to |
|
413 // indicate that there is no private data |
|
414 sendMessageToJava({ |
|
415 type: "PrivateBrowsing:Data", |
|
416 session: (privateData.windows[0].tabs.length > 0) ? JSON.stringify(privateData) : null |
|
417 }); |
|
418 |
|
419 this._lastSaveTime = Date.now(); |
|
420 }, |
|
421 |
|
422 _getCurrentState: function ss_getCurrentState() { |
|
423 let self = this; |
|
424 this._forEachBrowserWindow(function(aWindow) { |
|
425 self._collectWindowData(aWindow); |
|
426 }); |
|
427 |
|
428 let data = { windows: [] }; |
|
429 for (let index in this._windows) { |
|
430 data.windows.push(this._windows[index]); |
|
431 } |
|
432 |
|
433 return data; |
|
434 }, |
|
435 |
|
436 _collectTabData: function ss__collectTabData(aWindow, aBrowser, aHistory) { |
|
437 // If this browser is being restored, skip any session save activity |
|
438 if (aBrowser.__SS_restore) |
|
439 return; |
|
440 |
|
441 aHistory = aHistory || { entries: [{ url: aBrowser.currentURI.spec, title: aBrowser.contentTitle }], index: 1 }; |
|
442 |
|
443 let tabData = {}; |
|
444 tabData.entries = aHistory.entries; |
|
445 tabData.index = aHistory.index; |
|
446 tabData.attributes = { image: aBrowser.mIconURL }; |
|
447 tabData.desktopMode = aWindow.BrowserApp.getTabForBrowser(aBrowser).desktopMode; |
|
448 tabData.isPrivate = aBrowser.docShell.QueryInterface(Ci.nsILoadContext).usePrivateBrowsing; |
|
449 |
|
450 aBrowser.__SS_data = tabData; |
|
451 }, |
|
452 |
|
453 _collectWindowData: function ss__collectWindowData(aWindow) { |
|
454 // Ignore windows not tracked by SessionStore |
|
455 if (!aWindow.__SSID || !this._windows[aWindow.__SSID]) |
|
456 return; |
|
457 |
|
458 let winData = this._windows[aWindow.__SSID]; |
|
459 winData.tabs = []; |
|
460 |
|
461 let browsers = aWindow.document.getElementById("browsers"); |
|
462 let index = browsers.selectedIndex; |
|
463 winData.selected = parseInt(index) + 1; // 1-based |
|
464 |
|
465 let tabs = aWindow.BrowserApp.tabs; |
|
466 for (let i = 0; i < tabs.length; i++) { |
|
467 let browser = tabs[i].browser; |
|
468 if (browser.__SS_data) { |
|
469 let tabData = browser.__SS_data; |
|
470 if (browser.__SS_extdata) |
|
471 tabData.extData = browser.__SS_extdata; |
|
472 winData.tabs.push(tabData); |
|
473 } |
|
474 } |
|
475 }, |
|
476 |
|
477 _forEachBrowserWindow: function ss_forEachBrowserWindow(aFunc) { |
|
478 let windowsEnum = Services.wm.getEnumerator("navigator:browser"); |
|
479 while (windowsEnum.hasMoreElements()) { |
|
480 let window = windowsEnum.getNext(); |
|
481 if (window.__SSID && !window.closed) |
|
482 aFunc.call(this, window); |
|
483 } |
|
484 }, |
|
485 |
|
486 _writeFile: function ss_writeFile(aFile, aData, aAsync) { |
|
487 let stateString = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); |
|
488 stateString.data = aData; |
|
489 Services.obs.notifyObservers(stateString, "sessionstore-state-write", ""); |
|
490 |
|
491 // Don't touch the file if an observer has deleted all state data |
|
492 if (!stateString.data) |
|
493 return; |
|
494 |
|
495 if (aAsync) { |
|
496 let array = new TextEncoder().encode(aData); |
|
497 let pendingWrite = this._pendingWrite; |
|
498 OS.File.writeAtomic(aFile.path, array, { tmpPath: aFile.path + ".tmp" }).then(function onSuccess() { |
|
499 // Make sure this._pendingWrite is the same value it was before we |
|
500 // fired off the async write. If the count is different, another write |
|
501 // is pending, so we shouldn't reset this._pendingWrite yet. |
|
502 if (pendingWrite === this._pendingWrite) |
|
503 this._pendingWrite = 0; |
|
504 Services.obs.notifyObservers(null, "sessionstore-state-write-complete", ""); |
|
505 }.bind(this)); |
|
506 } else { |
|
507 this._pendingWrite = 0; |
|
508 let foStream = Cc["@mozilla.org/network/file-output-stream;1"]. |
|
509 createInstance(Ci.nsIFileOutputStream); |
|
510 foStream.init(aFile, 0x02 | 0x08 | 0x20, 0666, 0); |
|
511 let converter = Cc["@mozilla.org/intl/converter-output-stream;1"]. |
|
512 createInstance(Ci.nsIConverterOutputStream); |
|
513 converter.init(foStream, "UTF-8", 0, 0); |
|
514 converter.writeString(aData); |
|
515 converter.close(); |
|
516 } |
|
517 }, |
|
518 |
|
519 _updateCrashReportURL: function ss_updateCrashReportURL(aWindow) { |
|
520 #ifdef MOZ_CRASHREPORTER |
|
521 if (!aWindow.BrowserApp.selectedBrowser) |
|
522 return; |
|
523 |
|
524 try { |
|
525 let currentURI = aWindow.BrowserApp.selectedBrowser.currentURI.clone(); |
|
526 // if the current URI contains a username/password, remove it |
|
527 try { |
|
528 currentURI.userPass = ""; |
|
529 } |
|
530 catch (ex) { } // ignore failures on about: URIs |
|
531 |
|
532 CrashReporter.annotateCrashReport("URL", currentURI.spec); |
|
533 } |
|
534 catch (ex) { |
|
535 // don't make noise when crashreporter is built but not enabled |
|
536 if (ex.result != Components.results.NS_ERROR_NOT_INITIALIZED) |
|
537 Components.utils.reportError("SessionStore:" + ex); |
|
538 } |
|
539 #endif |
|
540 }, |
|
541 |
|
542 _serializeHistoryEntry: function _serializeHistoryEntry(aEntry) { |
|
543 let entry = { url: aEntry.URI.spec }; |
|
544 |
|
545 if (aEntry.title && aEntry.title != entry.url) |
|
546 entry.title = aEntry.title; |
|
547 |
|
548 if (!(aEntry instanceof Ci.nsISHEntry)) |
|
549 return entry; |
|
550 |
|
551 let cacheKey = aEntry.cacheKey; |
|
552 if (cacheKey && cacheKey instanceof Ci.nsISupportsPRUint32 && cacheKey.data != 0) |
|
553 entry.cacheKey = cacheKey.data; |
|
554 |
|
555 entry.ID = aEntry.ID; |
|
556 entry.docshellID = aEntry.docshellID; |
|
557 |
|
558 if (aEntry.referrerURI) |
|
559 entry.referrer = aEntry.referrerURI.spec; |
|
560 |
|
561 if (aEntry.contentType) |
|
562 entry.contentType = aEntry.contentType; |
|
563 |
|
564 let x = {}, y = {}; |
|
565 aEntry.getScrollPosition(x, y); |
|
566 if (x.value != 0 || y.value != 0) |
|
567 entry.scroll = x.value + "," + y.value; |
|
568 |
|
569 if (aEntry.owner) { |
|
570 try { |
|
571 let binaryStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(Ci.nsIObjectOutputStream); |
|
572 let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe); |
|
573 pipe.init(false, false, 0, 0xffffffff, null); |
|
574 binaryStream.setOutputStream(pipe.outputStream); |
|
575 binaryStream.writeCompoundObject(aEntry.owner, Ci.nsISupports, true); |
|
576 binaryStream.close(); |
|
577 |
|
578 // Now we want to read the data from the pipe's input end and encode it. |
|
579 let scriptableStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIBinaryInputStream); |
|
580 scriptableStream.setInputStream(pipe.inputStream); |
|
581 let ownerBytes = scriptableStream.readByteArray(scriptableStream.available()); |
|
582 // We can stop doing base64 encoding once our serialization into JSON |
|
583 // is guaranteed to handle all chars in strings, including embedded |
|
584 // nulls. |
|
585 entry.owner_b64 = btoa(String.fromCharCode.apply(null, ownerBytes)); |
|
586 } catch (e) { dump(e); } |
|
587 } |
|
588 |
|
589 entry.docIdentifier = aEntry.BFCacheEntry.ID; |
|
590 |
|
591 if (aEntry.stateData != null) { |
|
592 entry.structuredCloneState = aEntry.stateData.getDataAsBase64(); |
|
593 entry.structuredCloneVersion = aEntry.stateData.formatVersion; |
|
594 } |
|
595 |
|
596 if (!(aEntry instanceof Ci.nsISHContainer)) |
|
597 return entry; |
|
598 |
|
599 if (aEntry.childCount > 0) { |
|
600 let children = []; |
|
601 for (let i = 0; i < aEntry.childCount; i++) { |
|
602 let child = aEntry.GetChildAt(i); |
|
603 |
|
604 if (child) { |
|
605 // don't try to restore framesets containing wyciwyg URLs (cf. bug 424689 and bug 450595) |
|
606 if (child.URI.schemeIs("wyciwyg")) { |
|
607 children = []; |
|
608 break; |
|
609 } |
|
610 children.push(this._serializeHistoryEntry(child)); |
|
611 } |
|
612 |
|
613 if (children.length) |
|
614 entry.children = children; |
|
615 } |
|
616 } |
|
617 |
|
618 return entry; |
|
619 }, |
|
620 |
|
621 _deserializeHistoryEntry: function _deserializeHistoryEntry(aEntry, aIdMap, aDocIdentMap) { |
|
622 let shEntry = Cc["@mozilla.org/browser/session-history-entry;1"].createInstance(Ci.nsISHEntry); |
|
623 |
|
624 shEntry.setURI(Services.io.newURI(aEntry.url, null, null)); |
|
625 shEntry.setTitle(aEntry.title || aEntry.url); |
|
626 if (aEntry.subframe) |
|
627 shEntry.setIsSubFrame(aEntry.subframe || false); |
|
628 shEntry.loadType = Ci.nsIDocShellLoadInfo.loadHistory; |
|
629 if (aEntry.contentType) |
|
630 shEntry.contentType = aEntry.contentType; |
|
631 if (aEntry.referrer) |
|
632 shEntry.referrerURI = Services.io.newURI(aEntry.referrer, null, null); |
|
633 |
|
634 if (aEntry.cacheKey) { |
|
635 let cacheKey = Cc["@mozilla.org/supports-PRUint32;1"].createInstance(Ci.nsISupportsPRUint32); |
|
636 cacheKey.data = aEntry.cacheKey; |
|
637 shEntry.cacheKey = cacheKey; |
|
638 } |
|
639 |
|
640 if (aEntry.ID) { |
|
641 // get a new unique ID for this frame (since the one from the last |
|
642 // start might already be in use) |
|
643 let id = aIdMap[aEntry.ID] || 0; |
|
644 if (!id) { |
|
645 for (id = Date.now(); id in aIdMap.used; id++); |
|
646 aIdMap[aEntry.ID] = id; |
|
647 aIdMap.used[id] = true; |
|
648 } |
|
649 shEntry.ID = id; |
|
650 } |
|
651 |
|
652 if (aEntry.docshellID) |
|
653 shEntry.docshellID = aEntry.docshellID; |
|
654 |
|
655 if (aEntry.structuredCloneState && aEntry.structuredCloneVersion) { |
|
656 shEntry.stateData = |
|
657 Cc["@mozilla.org/docshell/structured-clone-container;1"]. |
|
658 createInstance(Ci.nsIStructuredCloneContainer); |
|
659 |
|
660 shEntry.stateData.initFromBase64(aEntry.structuredCloneState, aEntry.structuredCloneVersion); |
|
661 } |
|
662 |
|
663 if (aEntry.scroll) { |
|
664 let scrollPos = aEntry.scroll.split(","); |
|
665 scrollPos = [parseInt(scrollPos[0]) || 0, parseInt(scrollPos[1]) || 0]; |
|
666 shEntry.setScrollPosition(scrollPos[0], scrollPos[1]); |
|
667 } |
|
668 |
|
669 let childDocIdents = {}; |
|
670 if (aEntry.docIdentifier) { |
|
671 // If we have a serialized document identifier, try to find an SHEntry |
|
672 // which matches that doc identifier and adopt that SHEntry's |
|
673 // BFCacheEntry. If we don't find a match, insert shEntry as the match |
|
674 // for the document identifier. |
|
675 let matchingEntry = aDocIdentMap[aEntry.docIdentifier]; |
|
676 if (!matchingEntry) { |
|
677 matchingEntry = {shEntry: shEntry, childDocIdents: childDocIdents}; |
|
678 aDocIdentMap[aEntry.docIdentifier] = matchingEntry; |
|
679 } else { |
|
680 shEntry.adoptBFCacheEntry(matchingEntry.shEntry); |
|
681 childDocIdents = matchingEntry.childDocIdents; |
|
682 } |
|
683 } |
|
684 |
|
685 if (aEntry.owner_b64) { |
|
686 let ownerInput = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream); |
|
687 let binaryData = atob(aEntry.owner_b64); |
|
688 ownerInput.setData(binaryData, binaryData.length); |
|
689 let binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIObjectInputStream); |
|
690 binaryStream.setInputStream(ownerInput); |
|
691 try { // Catch possible deserialization exceptions |
|
692 shEntry.owner = binaryStream.readObject(true); |
|
693 } catch (ex) { dump(ex); } |
|
694 } |
|
695 |
|
696 if (aEntry.children && shEntry instanceof Ci.nsISHContainer) { |
|
697 for (let i = 0; i < aEntry.children.length; i++) { |
|
698 if (!aEntry.children[i].url) |
|
699 continue; |
|
700 |
|
701 // We're getting sessionrestore.js files with a cycle in the |
|
702 // doc-identifier graph, likely due to bug 698656. (That is, we have |
|
703 // an entry where doc identifier A is an ancestor of doc identifier B, |
|
704 // and another entry where doc identifier B is an ancestor of A.) |
|
705 // |
|
706 // If we were to respect these doc identifiers, we'd create a cycle in |
|
707 // the SHEntries themselves, which causes the docshell to loop forever |
|
708 // when it looks for the root SHEntry. |
|
709 // |
|
710 // So as a hack to fix this, we restrict the scope of a doc identifier |
|
711 // to be a node's siblings and cousins, and pass childDocIdents, not |
|
712 // aDocIdents, to _deserializeHistoryEntry. That is, we say that two |
|
713 // SHEntries with the same doc identifier have the same document iff |
|
714 // they have the same parent or their parents have the same document. |
|
715 |
|
716 shEntry.AddChild(this._deserializeHistoryEntry(aEntry.children[i], aIdMap, childDocIdents), i); |
|
717 } |
|
718 } |
|
719 |
|
720 return shEntry; |
|
721 }, |
|
722 |
|
723 _restoreHistory: function _restoreHistory(aTabData, aHistory) { |
|
724 if (aHistory.count > 0) |
|
725 aHistory.PurgeHistory(aHistory.count); |
|
726 aHistory.QueryInterface(Ci.nsISHistoryInternal); |
|
727 |
|
728 // helper hashes for ensuring unique frame IDs and unique document |
|
729 // identifiers. |
|
730 let idMap = { used: {} }; |
|
731 let docIdentMap = {}; |
|
732 |
|
733 for (let i = 0; i < aTabData.entries.length; i++) { |
|
734 if (!aTabData.entries[i].url) |
|
735 continue; |
|
736 aHistory.addEntry(this._deserializeHistoryEntry(aTabData.entries[i], idMap, docIdentMap), true); |
|
737 } |
|
738 |
|
739 // We need to force set the active history item and cause it to reload since |
|
740 // we stop the load above |
|
741 let activeIndex = (aTabData.index || aTabData.entries.length) - 1; |
|
742 aHistory.getEntryAtIndex(activeIndex, true); |
|
743 aHistory.QueryInterface(Ci.nsISHistory).reloadCurrentEntry(); |
|
744 }, |
|
745 |
|
746 getBrowserState: function ss_getBrowserState() { |
|
747 return this._getCurrentState(); |
|
748 }, |
|
749 |
|
750 _restoreWindow: function ss_restoreWindow(aData) { |
|
751 let state; |
|
752 try { |
|
753 state = JSON.parse(aData); |
|
754 } catch (e) { |
|
755 Cu.reportError("SessionStore: invalid session JSON"); |
|
756 return false; |
|
757 } |
|
758 |
|
759 // To do a restore, we must have at least one window with one tab |
|
760 if (!state || state.windows.length == 0 || !state.windows[0].tabs || state.windows[0].tabs.length == 0) { |
|
761 Cu.reportError("SessionStore: no tabs to restore"); |
|
762 return false; |
|
763 } |
|
764 |
|
765 let window = Services.wm.getMostRecentWindow("navigator:browser"); |
|
766 |
|
767 let tabs = state.windows[0].tabs; |
|
768 let selected = state.windows[0].selected; |
|
769 if (selected == null || selected > tabs.length) // Clamp the selected index if it's bogus |
|
770 selected = 1; |
|
771 |
|
772 for (let i = 0; i < tabs.length; i++) { |
|
773 let tabData = tabs[i]; |
|
774 let entry = tabData.entries[tabData.index - 1]; |
|
775 |
|
776 // Use stubbed tab if we've already created it; otherwise, make a new tab |
|
777 let tab; |
|
778 if (tabData.tabId == null) { |
|
779 let params = { |
|
780 selected: (selected == i+1), |
|
781 delayLoad: true, |
|
782 title: entry.title, |
|
783 desktopMode: (tabData.desktopMode == true), |
|
784 isPrivate: (tabData.isPrivate == true) |
|
785 }; |
|
786 tab = window.BrowserApp.addTab(entry.url, params); |
|
787 } else { |
|
788 tab = window.BrowserApp.getTabForId(tabData.tabId); |
|
789 delete tabData.tabId; |
|
790 |
|
791 // Don't restore tab if user has closed it |
|
792 if (tab == null) { |
|
793 continue; |
|
794 } |
|
795 } |
|
796 |
|
797 if (window.BrowserApp.selectedTab == tab) { |
|
798 this._restoreHistory(tabData, tab.browser.sessionHistory); |
|
799 delete tab.browser.__SS_restore; |
|
800 tab.browser.removeAttribute("pending"); |
|
801 } else { |
|
802 // Make sure the browser has its session data for the delay reload |
|
803 tab.browser.__SS_data = tabData; |
|
804 tab.browser.__SS_restore = true; |
|
805 tab.browser.setAttribute("pending", "true"); |
|
806 } |
|
807 |
|
808 tab.browser.__SS_extdata = tabData.extData; |
|
809 } |
|
810 |
|
811 return true; |
|
812 }, |
|
813 |
|
814 getClosedTabCount: function ss_getClosedTabCount(aWindow) { |
|
815 if (!aWindow || !aWindow.__SSID || !this._windows[aWindow.__SSID]) |
|
816 return 0; // not a browser window, or not otherwise tracked by SS. |
|
817 |
|
818 return this._windows[aWindow.__SSID].closedTabs.length; |
|
819 }, |
|
820 |
|
821 getClosedTabData: function ss_getClosedTabData(aWindow) { |
|
822 if (!aWindow.__SSID) |
|
823 throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); |
|
824 |
|
825 return JSON.stringify(this._windows[aWindow.__SSID].closedTabs); |
|
826 }, |
|
827 |
|
828 undoCloseTab: function ss_undoCloseTab(aWindow, aIndex) { |
|
829 if (!aWindow.__SSID) |
|
830 throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); |
|
831 |
|
832 let closedTabs = this._windows[aWindow.__SSID].closedTabs; |
|
833 if (!closedTabs) |
|
834 return null; |
|
835 |
|
836 // default to the most-recently closed tab |
|
837 aIndex = aIndex || 0; |
|
838 if (!(aIndex in closedTabs)) |
|
839 throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); |
|
840 |
|
841 // fetch the data of closed tab, while removing it from the array |
|
842 let closedTab = closedTabs.splice(aIndex, 1).shift(); |
|
843 |
|
844 // create a new tab and bring to front |
|
845 let params = { selected: true }; |
|
846 let tab = aWindow.BrowserApp.addTab(closedTab.entries[closedTab.index - 1].url, params); |
|
847 this._restoreHistory(closedTab, tab.browser.sessionHistory); |
|
848 |
|
849 // Put back the extra data |
|
850 tab.browser.__SS_extdata = closedTab.extData; |
|
851 |
|
852 return tab.browser; |
|
853 }, |
|
854 |
|
855 forgetClosedTab: function ss_forgetClosedTab(aWindow, aIndex) { |
|
856 if (!aWindow.__SSID) |
|
857 throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); |
|
858 |
|
859 let closedTabs = this._windows[aWindow.__SSID].closedTabs; |
|
860 |
|
861 // default to the most-recently closed tab |
|
862 aIndex = aIndex || 0; |
|
863 if (!(aIndex in closedTabs)) |
|
864 throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); |
|
865 |
|
866 // remove closed tab from the array |
|
867 closedTabs.splice(aIndex, 1); |
|
868 }, |
|
869 |
|
870 getTabValue: function ss_getTabValue(aTab, aKey) { |
|
871 let browser = aTab.browser; |
|
872 let data = browser.__SS_extdata || {}; |
|
873 return data[aKey] || ""; |
|
874 }, |
|
875 |
|
876 setTabValue: function ss_setTabValue(aTab, aKey, aStringValue) { |
|
877 let browser = aTab.browser; |
|
878 |
|
879 if (!browser.__SS_extdata) |
|
880 browser.__SS_extdata = {}; |
|
881 browser.__SS_extdata[aKey] = aStringValue; |
|
882 this.saveStateDelayed(); |
|
883 }, |
|
884 |
|
885 deleteTabValue: function ss_deleteTabValue(aTab, aKey) { |
|
886 let browser = aTab.browser; |
|
887 if (browser.__SS_extdata && browser.__SS_extdata[aKey]) |
|
888 delete browser.__SS_extdata[aKey]; |
|
889 else |
|
890 throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); |
|
891 }, |
|
892 |
|
893 restoreLastSession: function ss_restoreLastSession(aSessionString) { |
|
894 let self = this; |
|
895 |
|
896 function restoreWindow(data) { |
|
897 if (!self._restoreWindow(data)) { |
|
898 throw "Could not restore window"; |
|
899 } |
|
900 |
|
901 notifyObservers(); |
|
902 } |
|
903 |
|
904 function notifyObservers(aMessage) { |
|
905 Services.obs.notifyObservers(null, "sessionstore-windows-restored", aMessage || ""); |
|
906 } |
|
907 |
|
908 try { |
|
909 // Normally, we'll receive the session string from Java, but there are |
|
910 // cases where we may want to restore that Java cannot detect (e.g., if |
|
911 // browser.sessionstore.resume_session_once is true). In these cases, the |
|
912 // session will be read from sessionstore.bak (which is also used for |
|
913 // "tabs from last time"). |
|
914 if (aSessionString == null) { |
|
915 Task.spawn(function() { |
|
916 let bytes = yield OS.File.read(this._sessionFileBackup.path); |
|
917 let data = JSON.parse(new TextDecoder().decode(bytes) || ""); |
|
918 restoreWindow(data); |
|
919 }.bind(this)).then(null, function onError(reason) { |
|
920 if (reason instanceof OS.File.Error && reason.becauseNoSuchFile) { |
|
921 Cu.reportError("Session file doesn't exist"); |
|
922 } else { |
|
923 Cu.reportError("SessionStore: " + reason.message); |
|
924 } |
|
925 notifyObservers("fail"); |
|
926 }); |
|
927 } else { |
|
928 restoreWindow(aSessionString); |
|
929 } |
|
930 } catch (e) { |
|
931 Cu.reportError("SessionStore: " + e); |
|
932 notifyObservers("fail"); |
|
933 } |
|
934 } |
|
935 }; |
|
936 |
|
937 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SessionStore]); |