|
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 = ["SessionStore"]; |
|
8 |
|
9 const Cu = Components.utils; |
|
10 const Cc = Components.classes; |
|
11 const Ci = Components.interfaces; |
|
12 const Cr = Components.results; |
|
13 |
|
14 const STATE_STOPPED = 0; |
|
15 const STATE_RUNNING = 1; |
|
16 const STATE_QUITTING = -1; |
|
17 |
|
18 const TAB_STATE_NEEDS_RESTORE = 1; |
|
19 const TAB_STATE_RESTORING = 2; |
|
20 |
|
21 const NOTIFY_WINDOWS_RESTORED = "sessionstore-windows-restored"; |
|
22 const NOTIFY_BROWSER_STATE_RESTORED = "sessionstore-browser-state-restored"; |
|
23 const NOTIFY_LAST_SESSION_CLEARED = "sessionstore-last-session-cleared"; |
|
24 |
|
25 const NOTIFY_TAB_RESTORED = "sessionstore-debug-tab-restored"; // WARNING: debug-only |
|
26 |
|
27 // Maximum number of tabs to restore simultaneously. Previously controlled by |
|
28 // the browser.sessionstore.max_concurrent_tabs pref. |
|
29 const MAX_CONCURRENT_TAB_RESTORES = 3; |
|
30 |
|
31 // global notifications observed |
|
32 const OBSERVING = [ |
|
33 "domwindowopened", "domwindowclosed", |
|
34 "quit-application-requested", "quit-application-granted", |
|
35 "browser-lastwindow-close-granted", |
|
36 "quit-application", "browser:purge-session-history", |
|
37 "browser:purge-domain-data", |
|
38 "gather-telemetry", |
|
39 ]; |
|
40 |
|
41 // XUL Window properties to (re)store |
|
42 // Restored in restoreDimensions() |
|
43 const WINDOW_ATTRIBUTES = ["width", "height", "screenX", "screenY", "sizemode"]; |
|
44 |
|
45 // Hideable window features to (re)store |
|
46 // Restored in restoreWindowFeatures() |
|
47 const WINDOW_HIDEABLE_FEATURES = [ |
|
48 "menubar", "toolbar", "locationbar", "personalbar", "statusbar", "scrollbars" |
|
49 ]; |
|
50 |
|
51 const MESSAGES = [ |
|
52 // The content script gives us a reference to an object that performs |
|
53 // synchronous collection of session data. |
|
54 "SessionStore:setupSyncHandler", |
|
55 |
|
56 // The content script sends us data that has been invalidated and needs to |
|
57 // be saved to disk. |
|
58 "SessionStore:update", |
|
59 |
|
60 // The restoreHistory code has run. This is a good time to run SSTabRestoring. |
|
61 "SessionStore:restoreHistoryComplete", |
|
62 |
|
63 // The load for the restoring tab has begun. We update the URL bar at this |
|
64 // time; if we did it before, the load would overwrite it. |
|
65 "SessionStore:restoreTabContentStarted", |
|
66 |
|
67 // All network loads for a restoring tab are done, so we should consider |
|
68 // restoring another tab in the queue. |
|
69 "SessionStore:restoreTabContentComplete", |
|
70 |
|
71 // The document has been restored, so the restore is done. We trigger |
|
72 // SSTabRestored at this time. |
|
73 "SessionStore:restoreDocumentComplete", |
|
74 |
|
75 // A tab that is being restored was reloaded. We call restoreTabContent to |
|
76 // finish restoring it right away. |
|
77 "SessionStore:reloadPendingTab", |
|
78 ]; |
|
79 |
|
80 // These are tab events that we listen to. |
|
81 const TAB_EVENTS = [ |
|
82 "TabOpen", "TabClose", "TabSelect", "TabShow", "TabHide", "TabPinned", |
|
83 "TabUnpinned" |
|
84 ]; |
|
85 |
|
86 // The number of milliseconds in a day |
|
87 const MS_PER_DAY = 1000.0 * 60.0 * 60.0 * 24.0; |
|
88 |
|
89 Cu.import("resource://gre/modules/Services.jsm", this); |
|
90 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); |
|
91 Cu.import("resource://gre/modules/TelemetryTimestamps.jsm", this); |
|
92 Cu.import("resource://gre/modules/TelemetryStopwatch.jsm", this); |
|
93 Cu.import("resource://gre/modules/osfile.jsm", this); |
|
94 Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm", this); |
|
95 Cu.import("resource://gre/modules/Promise.jsm", this); |
|
96 Cu.import("resource://gre/modules/Task.jsm", this); |
|
97 |
|
98 XPCOMUtils.defineLazyServiceGetter(this, "gSessionStartup", |
|
99 "@mozilla.org/browser/sessionstartup;1", "nsISessionStartup"); |
|
100 XPCOMUtils.defineLazyServiceGetter(this, "gScreenManager", |
|
101 "@mozilla.org/gfx/screenmanager;1", "nsIScreenManager"); |
|
102 XPCOMUtils.defineLazyServiceGetter(this, "Telemetry", |
|
103 "@mozilla.org/base/telemetry;1", "nsITelemetry"); |
|
104 |
|
105 XPCOMUtils.defineLazyModuleGetter(this, "console", |
|
106 "resource://gre/modules/devtools/Console.jsm"); |
|
107 XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow", |
|
108 "resource:///modules/RecentWindow.jsm"); |
|
109 |
|
110 XPCOMUtils.defineLazyModuleGetter(this, "GlobalState", |
|
111 "resource:///modules/sessionstore/GlobalState.jsm"); |
|
112 XPCOMUtils.defineLazyModuleGetter(this, "PrivacyFilter", |
|
113 "resource:///modules/sessionstore/PrivacyFilter.jsm"); |
|
114 XPCOMUtils.defineLazyModuleGetter(this, "ScratchpadManager", |
|
115 "resource:///modules/devtools/scratchpad-manager.jsm"); |
|
116 XPCOMUtils.defineLazyModuleGetter(this, "SessionSaver", |
|
117 "resource:///modules/sessionstore/SessionSaver.jsm"); |
|
118 XPCOMUtils.defineLazyModuleGetter(this, "SessionCookies", |
|
119 "resource:///modules/sessionstore/SessionCookies.jsm"); |
|
120 XPCOMUtils.defineLazyModuleGetter(this, "SessionFile", |
|
121 "resource:///modules/sessionstore/SessionFile.jsm"); |
|
122 XPCOMUtils.defineLazyModuleGetter(this, "TabAttributes", |
|
123 "resource:///modules/sessionstore/TabAttributes.jsm"); |
|
124 XPCOMUtils.defineLazyModuleGetter(this, "TabState", |
|
125 "resource:///modules/sessionstore/TabState.jsm"); |
|
126 XPCOMUtils.defineLazyModuleGetter(this, "TabStateCache", |
|
127 "resource:///modules/sessionstore/TabStateCache.jsm"); |
|
128 XPCOMUtils.defineLazyModuleGetter(this, "Utils", |
|
129 "resource:///modules/sessionstore/Utils.jsm"); |
|
130 |
|
131 /** |
|
132 * |true| if we are in debug mode, |false| otherwise. |
|
133 * Debug mode is controlled by preference browser.sessionstore.debug |
|
134 */ |
|
135 let gDebuggingEnabled = false; |
|
136 function debug(aMsg) { |
|
137 if (gDebuggingEnabled) { |
|
138 aMsg = ("SessionStore: " + aMsg).replace(/\S{80}/g, "$&\n"); |
|
139 Services.console.logStringMessage(aMsg); |
|
140 } |
|
141 } |
|
142 |
|
143 this.SessionStore = { |
|
144 get promiseInitialized() { |
|
145 return SessionStoreInternal.promiseInitialized; |
|
146 }, |
|
147 |
|
148 get canRestoreLastSession() { |
|
149 return SessionStoreInternal.canRestoreLastSession; |
|
150 }, |
|
151 |
|
152 set canRestoreLastSession(val) { |
|
153 SessionStoreInternal.canRestoreLastSession = val; |
|
154 }, |
|
155 |
|
156 init: function ss_init() { |
|
157 SessionStoreInternal.init(); |
|
158 }, |
|
159 |
|
160 getBrowserState: function ss_getBrowserState() { |
|
161 return SessionStoreInternal.getBrowserState(); |
|
162 }, |
|
163 |
|
164 setBrowserState: function ss_setBrowserState(aState) { |
|
165 SessionStoreInternal.setBrowserState(aState); |
|
166 }, |
|
167 |
|
168 getWindowState: function ss_getWindowState(aWindow) { |
|
169 return SessionStoreInternal.getWindowState(aWindow); |
|
170 }, |
|
171 |
|
172 setWindowState: function ss_setWindowState(aWindow, aState, aOverwrite) { |
|
173 SessionStoreInternal.setWindowState(aWindow, aState, aOverwrite); |
|
174 }, |
|
175 |
|
176 getTabState: function ss_getTabState(aTab) { |
|
177 return SessionStoreInternal.getTabState(aTab); |
|
178 }, |
|
179 |
|
180 setTabState: function ss_setTabState(aTab, aState) { |
|
181 SessionStoreInternal.setTabState(aTab, aState); |
|
182 }, |
|
183 |
|
184 duplicateTab: function ss_duplicateTab(aWindow, aTab, aDelta = 0) { |
|
185 return SessionStoreInternal.duplicateTab(aWindow, aTab, aDelta); |
|
186 }, |
|
187 |
|
188 getClosedTabCount: function ss_getClosedTabCount(aWindow) { |
|
189 return SessionStoreInternal.getClosedTabCount(aWindow); |
|
190 }, |
|
191 |
|
192 getClosedTabData: function ss_getClosedTabDataAt(aWindow) { |
|
193 return SessionStoreInternal.getClosedTabData(aWindow); |
|
194 }, |
|
195 |
|
196 undoCloseTab: function ss_undoCloseTab(aWindow, aIndex) { |
|
197 return SessionStoreInternal.undoCloseTab(aWindow, aIndex); |
|
198 }, |
|
199 |
|
200 forgetClosedTab: function ss_forgetClosedTab(aWindow, aIndex) { |
|
201 return SessionStoreInternal.forgetClosedTab(aWindow, aIndex); |
|
202 }, |
|
203 |
|
204 getClosedWindowCount: function ss_getClosedWindowCount() { |
|
205 return SessionStoreInternal.getClosedWindowCount(); |
|
206 }, |
|
207 |
|
208 getClosedWindowData: function ss_getClosedWindowData() { |
|
209 return SessionStoreInternal.getClosedWindowData(); |
|
210 }, |
|
211 |
|
212 undoCloseWindow: function ss_undoCloseWindow(aIndex) { |
|
213 return SessionStoreInternal.undoCloseWindow(aIndex); |
|
214 }, |
|
215 |
|
216 forgetClosedWindow: function ss_forgetClosedWindow(aIndex) { |
|
217 return SessionStoreInternal.forgetClosedWindow(aIndex); |
|
218 }, |
|
219 |
|
220 getWindowValue: function ss_getWindowValue(aWindow, aKey) { |
|
221 return SessionStoreInternal.getWindowValue(aWindow, aKey); |
|
222 }, |
|
223 |
|
224 setWindowValue: function ss_setWindowValue(aWindow, aKey, aStringValue) { |
|
225 SessionStoreInternal.setWindowValue(aWindow, aKey, aStringValue); |
|
226 }, |
|
227 |
|
228 deleteWindowValue: function ss_deleteWindowValue(aWindow, aKey) { |
|
229 SessionStoreInternal.deleteWindowValue(aWindow, aKey); |
|
230 }, |
|
231 |
|
232 getTabValue: function ss_getTabValue(aTab, aKey) { |
|
233 return SessionStoreInternal.getTabValue(aTab, aKey); |
|
234 }, |
|
235 |
|
236 setTabValue: function ss_setTabValue(aTab, aKey, aStringValue) { |
|
237 SessionStoreInternal.setTabValue(aTab, aKey, aStringValue); |
|
238 }, |
|
239 |
|
240 deleteTabValue: function ss_deleteTabValue(aTab, aKey) { |
|
241 SessionStoreInternal.deleteTabValue(aTab, aKey); |
|
242 }, |
|
243 |
|
244 getGlobalValue: function ss_getGlobalValue(aKey) { |
|
245 return SessionStoreInternal.getGlobalValue(aKey); |
|
246 }, |
|
247 |
|
248 setGlobalValue: function ss_setGlobalValue(aKey, aStringValue) { |
|
249 SessionStoreInternal.setGlobalValue(aKey, aStringValue); |
|
250 }, |
|
251 |
|
252 deleteGlobalValue: function ss_deleteGlobalValue(aKey) { |
|
253 SessionStoreInternal.deleteGlobalValue(aKey); |
|
254 }, |
|
255 |
|
256 persistTabAttribute: function ss_persistTabAttribute(aName) { |
|
257 SessionStoreInternal.persistTabAttribute(aName); |
|
258 }, |
|
259 |
|
260 restoreLastSession: function ss_restoreLastSession() { |
|
261 SessionStoreInternal.restoreLastSession(); |
|
262 }, |
|
263 |
|
264 getCurrentState: function (aUpdateAll) { |
|
265 return SessionStoreInternal.getCurrentState(aUpdateAll); |
|
266 }, |
|
267 |
|
268 /** |
|
269 * Backstage pass to implementation details, used for testing purpose. |
|
270 * Controlled by preference "browser.sessionstore.testmode". |
|
271 */ |
|
272 get _internal() { |
|
273 if (Services.prefs.getBoolPref("browser.sessionstore.debug")) { |
|
274 return SessionStoreInternal; |
|
275 } |
|
276 return undefined; |
|
277 }, |
|
278 }; |
|
279 |
|
280 // Freeze the SessionStore object. We don't want anyone to modify it. |
|
281 Object.freeze(SessionStore); |
|
282 |
|
283 let SessionStoreInternal = { |
|
284 QueryInterface: XPCOMUtils.generateQI([ |
|
285 Ci.nsIDOMEventListener, |
|
286 Ci.nsIObserver, |
|
287 Ci.nsISupportsWeakReference |
|
288 ]), |
|
289 |
|
290 // set default load state |
|
291 _loadState: STATE_STOPPED, |
|
292 |
|
293 _globalState: new GlobalState(), |
|
294 |
|
295 // During the initial restore and setBrowserState calls tracks the number of |
|
296 // windows yet to be restored |
|
297 _restoreCount: -1, |
|
298 |
|
299 // This number gets incremented each time we start to restore a tab. |
|
300 _nextRestoreEpoch: 1, |
|
301 |
|
302 // For each <browser> element being restored, records the current epoch. |
|
303 _browserEpochs: new WeakMap(), |
|
304 |
|
305 // whether a setBrowserState call is in progress |
|
306 _browserSetState: false, |
|
307 |
|
308 // time in milliseconds when the session was started (saved across sessions), |
|
309 // defaults to now if no session was restored or timestamp doesn't exist |
|
310 _sessionStartTime: Date.now(), |
|
311 |
|
312 // states for all currently opened windows |
|
313 _windows: {}, |
|
314 |
|
315 // counter for creating unique window IDs |
|
316 _nextWindowID: 0, |
|
317 |
|
318 // states for all recently closed windows |
|
319 _closedWindows: [], |
|
320 |
|
321 // collection of session states yet to be restored |
|
322 _statesToRestore: {}, |
|
323 |
|
324 // counts the number of crashes since the last clean start |
|
325 _recentCrashes: 0, |
|
326 |
|
327 // whether the last window was closed and should be restored |
|
328 _restoreLastWindow: false, |
|
329 |
|
330 // number of tabs currently restoring |
|
331 _tabsRestoringCount: 0, |
|
332 |
|
333 // When starting Firefox with a single private window, this is the place |
|
334 // where we keep the session we actually wanted to restore in case the user |
|
335 // decides to later open a non-private window as well. |
|
336 _deferredInitialState: null, |
|
337 |
|
338 // A promise resolved once initialization is complete |
|
339 _deferredInitialized: Promise.defer(), |
|
340 |
|
341 // Whether session has been initialized |
|
342 _sessionInitialized: false, |
|
343 |
|
344 // Promise that is resolved when we're ready to initialize |
|
345 // and restore the session. |
|
346 _promiseReadyForInitialization: null, |
|
347 |
|
348 /** |
|
349 * A promise fulfilled once initialization is complete. |
|
350 */ |
|
351 get promiseInitialized() { |
|
352 return this._deferredInitialized.promise; |
|
353 }, |
|
354 |
|
355 get canRestoreLastSession() { |
|
356 return LastSession.canRestore; |
|
357 }, |
|
358 |
|
359 set canRestoreLastSession(val) { |
|
360 // Cheat a bit; only allow false. |
|
361 if (!val) { |
|
362 LastSession.clear(); |
|
363 } |
|
364 }, |
|
365 |
|
366 /** |
|
367 * Initialize the sessionstore service. |
|
368 */ |
|
369 init: function () { |
|
370 if (this._initialized) { |
|
371 throw new Error("SessionStore.init() must only be called once!"); |
|
372 } |
|
373 |
|
374 TelemetryTimestamps.add("sessionRestoreInitialized"); |
|
375 OBSERVING.forEach(function(aTopic) { |
|
376 Services.obs.addObserver(this, aTopic, true); |
|
377 }, this); |
|
378 |
|
379 this._initPrefs(); |
|
380 this._initialized = true; |
|
381 }, |
|
382 |
|
383 /** |
|
384 * Initialize the session using the state provided by SessionStartup |
|
385 */ |
|
386 initSession: function () { |
|
387 let state; |
|
388 let ss = gSessionStartup; |
|
389 |
|
390 try { |
|
391 if (ss.doRestore() || |
|
392 ss.sessionType == Ci.nsISessionStartup.DEFER_SESSION) |
|
393 state = ss.state; |
|
394 } |
|
395 catch(ex) { dump(ex + "\n"); } // no state to restore, which is ok |
|
396 |
|
397 if (state) { |
|
398 try { |
|
399 // If we're doing a DEFERRED session, then we want to pull pinned tabs |
|
400 // out so they can be restored. |
|
401 if (ss.sessionType == Ci.nsISessionStartup.DEFER_SESSION) { |
|
402 let [iniState, remainingState] = this._prepDataForDeferredRestore(state); |
|
403 // If we have a iniState with windows, that means that we have windows |
|
404 // with app tabs to restore. |
|
405 if (iniState.windows.length) |
|
406 state = iniState; |
|
407 else |
|
408 state = null; |
|
409 |
|
410 if (remainingState.windows.length) { |
|
411 LastSession.setState(remainingState); |
|
412 } |
|
413 } |
|
414 else { |
|
415 // Get the last deferred session in case the user still wants to |
|
416 // restore it |
|
417 LastSession.setState(state.lastSessionState); |
|
418 |
|
419 if (ss.previousSessionCrashed) { |
|
420 this._recentCrashes = (state.session && |
|
421 state.session.recentCrashes || 0) + 1; |
|
422 |
|
423 if (this._needsRestorePage(state, this._recentCrashes)) { |
|
424 // replace the crashed session with a restore-page-only session |
|
425 let pageData = { |
|
426 url: "about:sessionrestore", |
|
427 formdata: { |
|
428 id: { "sessionData": state }, |
|
429 xpath: {} |
|
430 } |
|
431 }; |
|
432 state = { windows: [{ tabs: [{ entries: [pageData] }] }] }; |
|
433 } else if (this._hasSingleTabWithURL(state.windows, |
|
434 "about:welcomeback")) { |
|
435 // On a single about:welcomeback URL that crashed, replace about:welcomeback |
|
436 // with about:sessionrestore, to make clear to the user that we crashed. |
|
437 state.windows[0].tabs[0].entries[0].url = "about:sessionrestore"; |
|
438 } |
|
439 } |
|
440 |
|
441 // Update the session start time using the restored session state. |
|
442 this._updateSessionStartTime(state); |
|
443 |
|
444 // make sure that at least the first window doesn't have anything hidden |
|
445 delete state.windows[0].hidden; |
|
446 // Since nothing is hidden in the first window, it cannot be a popup |
|
447 delete state.windows[0].isPopup; |
|
448 // We don't want to minimize and then open a window at startup. |
|
449 if (state.windows[0].sizemode == "minimized") |
|
450 state.windows[0].sizemode = "normal"; |
|
451 // clear any lastSessionWindowID attributes since those don't matter |
|
452 // during normal restore |
|
453 state.windows.forEach(function(aWindow) { |
|
454 delete aWindow.__lastSessionWindowID; |
|
455 }); |
|
456 } |
|
457 } |
|
458 catch (ex) { debug("The session file is invalid: " + ex); } |
|
459 } |
|
460 |
|
461 // at this point, we've as good as resumed the session, so we can |
|
462 // clear the resume_session_once flag, if it's set |
|
463 if (this._loadState != STATE_QUITTING && |
|
464 this._prefBranch.getBoolPref("sessionstore.resume_session_once")) |
|
465 this._prefBranch.setBoolPref("sessionstore.resume_session_once", false); |
|
466 |
|
467 this._performUpgradeBackup(); |
|
468 |
|
469 return state; |
|
470 }, |
|
471 |
|
472 /** |
|
473 * If this is the first time we launc this build of Firefox, |
|
474 * backup sessionstore.js. |
|
475 */ |
|
476 _performUpgradeBackup: function ssi_performUpgradeBackup() { |
|
477 // Perform upgrade backup, if necessary |
|
478 const PREF_UPGRADE = "sessionstore.upgradeBackup.latestBuildID"; |
|
479 |
|
480 let buildID = Services.appinfo.platformBuildID; |
|
481 let latestBackup = this._prefBranch.getCharPref(PREF_UPGRADE); |
|
482 if (latestBackup == buildID) { |
|
483 return Promise.resolve(); |
|
484 } |
|
485 return Task.spawn(function task() { |
|
486 try { |
|
487 // Perform background backup |
|
488 yield SessionFile.createBackupCopy("-" + buildID); |
|
489 |
|
490 this._prefBranch.setCharPref(PREF_UPGRADE, buildID); |
|
491 |
|
492 // In case of success, remove previous backup. |
|
493 yield SessionFile.removeBackupCopy("-" + latestBackup); |
|
494 } catch (ex) { |
|
495 debug("Could not perform upgrade backup " + ex); |
|
496 debug(ex.stack); |
|
497 } |
|
498 }.bind(this)); |
|
499 }, |
|
500 |
|
501 _initPrefs : function() { |
|
502 this._prefBranch = Services.prefs.getBranch("browser."); |
|
503 |
|
504 gDebuggingEnabled = this._prefBranch.getBoolPref("sessionstore.debug"); |
|
505 |
|
506 Services.prefs.addObserver("browser.sessionstore.debug", () => { |
|
507 gDebuggingEnabled = this._prefBranch.getBoolPref("sessionstore.debug"); |
|
508 }, false); |
|
509 |
|
510 this._max_tabs_undo = this._prefBranch.getIntPref("sessionstore.max_tabs_undo"); |
|
511 this._prefBranch.addObserver("sessionstore.max_tabs_undo", this, true); |
|
512 |
|
513 this._max_windows_undo = this._prefBranch.getIntPref("sessionstore.max_windows_undo"); |
|
514 this._prefBranch.addObserver("sessionstore.max_windows_undo", this, true); |
|
515 }, |
|
516 |
|
517 /** |
|
518 * Called on application shutdown, after notifications: |
|
519 * quit-application-granted, quit-application |
|
520 */ |
|
521 _uninit: function ssi_uninit() { |
|
522 if (!this._initialized) { |
|
523 throw new Error("SessionStore is not initialized."); |
|
524 } |
|
525 |
|
526 // save all data for session resuming |
|
527 if (this._sessionInitialized) { |
|
528 SessionSaver.run(); |
|
529 } |
|
530 |
|
531 // clear out priority queue in case it's still holding refs |
|
532 TabRestoreQueue.reset(); |
|
533 |
|
534 // Make sure to cancel pending saves. |
|
535 SessionSaver.cancel(); |
|
536 }, |
|
537 |
|
538 /** |
|
539 * Handle notifications |
|
540 */ |
|
541 observe: function ssi_observe(aSubject, aTopic, aData) { |
|
542 switch (aTopic) { |
|
543 case "domwindowopened": // catch new windows |
|
544 this.onOpen(aSubject); |
|
545 break; |
|
546 case "domwindowclosed": // catch closed windows |
|
547 this.onClose(aSubject); |
|
548 break; |
|
549 case "quit-application-requested": |
|
550 this.onQuitApplicationRequested(); |
|
551 break; |
|
552 case "quit-application-granted": |
|
553 this.onQuitApplicationGranted(); |
|
554 break; |
|
555 case "browser-lastwindow-close-granted": |
|
556 this.onLastWindowCloseGranted(); |
|
557 break; |
|
558 case "quit-application": |
|
559 this.onQuitApplication(aData); |
|
560 break; |
|
561 case "browser:purge-session-history": // catch sanitization |
|
562 this.onPurgeSessionHistory(); |
|
563 break; |
|
564 case "browser:purge-domain-data": |
|
565 this.onPurgeDomainData(aData); |
|
566 break; |
|
567 case "nsPref:changed": // catch pref changes |
|
568 this.onPrefChange(aData); |
|
569 break; |
|
570 case "gather-telemetry": |
|
571 this.onGatherTelemetry(); |
|
572 break; |
|
573 } |
|
574 }, |
|
575 |
|
576 /** |
|
577 * This method handles incoming messages sent by the session store content |
|
578 * script and thus enables communication with OOP tabs. |
|
579 */ |
|
580 receiveMessage: function ssi_receiveMessage(aMessage) { |
|
581 var browser = aMessage.target; |
|
582 var win = browser.ownerDocument.defaultView; |
|
583 let tab = this._getTabForBrowser(browser); |
|
584 if (!tab) { |
|
585 // Ignore messages from <browser> elements that are not tabs. |
|
586 return; |
|
587 } |
|
588 |
|
589 switch (aMessage.name) { |
|
590 case "SessionStore:setupSyncHandler": |
|
591 TabState.setSyncHandler(browser, aMessage.objects.handler); |
|
592 break; |
|
593 case "SessionStore:update": |
|
594 this.recordTelemetry(aMessage.data.telemetry); |
|
595 TabState.update(browser, aMessage.data); |
|
596 this.saveStateDelayed(win); |
|
597 break; |
|
598 case "SessionStore:restoreHistoryComplete": |
|
599 if (this.isCurrentEpoch(browser, aMessage.data.epoch)) { |
|
600 // Notify the tabbrowser that the tab chrome has been restored. |
|
601 let tabData = browser.__SS_data; |
|
602 |
|
603 // wall-paper fix for bug 439675: make sure that the URL to be loaded |
|
604 // is always visible in the address bar |
|
605 let activePageData = tabData.entries[tabData.index - 1] || null; |
|
606 let uri = activePageData ? activePageData.url || null : null; |
|
607 browser.userTypedValue = uri; |
|
608 |
|
609 // If the page has a title, set it. |
|
610 if (activePageData) { |
|
611 if (activePageData.title) { |
|
612 tab.label = activePageData.title; |
|
613 tab.crop = "end"; |
|
614 } else if (activePageData.url != "about:blank") { |
|
615 tab.label = activePageData.url; |
|
616 tab.crop = "center"; |
|
617 } |
|
618 } |
|
619 |
|
620 // Restore the tab icon. |
|
621 if ("image" in tabData) { |
|
622 win.gBrowser.setIcon(tab, tabData.image); |
|
623 } |
|
624 |
|
625 let event = win.document.createEvent("Events"); |
|
626 event.initEvent("SSTabRestoring", true, false); |
|
627 tab.dispatchEvent(event); |
|
628 } |
|
629 break; |
|
630 case "SessionStore:restoreTabContentStarted": |
|
631 if (this.isCurrentEpoch(browser, aMessage.data.epoch)) { |
|
632 // If the user was typing into the URL bar when we crashed, but hadn't hit |
|
633 // enter yet, then we just need to write that value to the URL bar without |
|
634 // loading anything. This must happen after the load, since it will clear |
|
635 // userTypedValue. |
|
636 let tabData = browser.__SS_data; |
|
637 if (tabData.userTypedValue && !tabData.userTypedClear) { |
|
638 browser.userTypedValue = tabData.userTypedValue; |
|
639 win.URLBarSetURI(); |
|
640 } |
|
641 } |
|
642 break; |
|
643 case "SessionStore:restoreTabContentComplete": |
|
644 if (this.isCurrentEpoch(browser, aMessage.data.epoch)) { |
|
645 // This callback is used exclusively by tests that want to |
|
646 // monitor the progress of network loads. |
|
647 if (gDebuggingEnabled) { |
|
648 Services.obs.notifyObservers(browser, NOTIFY_TAB_RESTORED, null); |
|
649 } |
|
650 |
|
651 if (tab) { |
|
652 SessionStoreInternal._resetLocalTabRestoringState(tab); |
|
653 SessionStoreInternal.restoreNextTab(); |
|
654 } |
|
655 } |
|
656 break; |
|
657 case "SessionStore:restoreDocumentComplete": |
|
658 if (this.isCurrentEpoch(browser, aMessage.data.epoch)) { |
|
659 // Document has been restored. Delete all the state associated |
|
660 // with it and trigger SSTabRestored. |
|
661 let tab = browser.__SS_restore_tab; |
|
662 |
|
663 delete browser.__SS_restore_data; |
|
664 delete browser.__SS_restore_tab; |
|
665 delete browser.__SS_data; |
|
666 |
|
667 this._sendTabRestoredNotification(tab); |
|
668 } |
|
669 break; |
|
670 case "SessionStore:reloadPendingTab": |
|
671 if (this.isCurrentEpoch(browser, aMessage.data.epoch)) { |
|
672 if (tab && browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) { |
|
673 this.restoreTabContent(tab); |
|
674 } |
|
675 } |
|
676 break; |
|
677 default: |
|
678 debug("received unknown message '" + aMessage.name + "'"); |
|
679 break; |
|
680 } |
|
681 }, |
|
682 |
|
683 /** |
|
684 * Record telemetry measurements stored in an object. |
|
685 * @param telemetry |
|
686 * {histogramID: value, ...} An object mapping histogramIDs to the |
|
687 * value to be recorded for that ID, |
|
688 */ |
|
689 recordTelemetry: function (telemetry) { |
|
690 for (let histogramId in telemetry){ |
|
691 Telemetry.getHistogramById(histogramId).add(telemetry[histogramId]); |
|
692 } |
|
693 }, |
|
694 |
|
695 /* ........ Window Event Handlers .............. */ |
|
696 |
|
697 /** |
|
698 * Implement nsIDOMEventListener for handling various window and tab events |
|
699 */ |
|
700 handleEvent: function ssi_handleEvent(aEvent) { |
|
701 var win = aEvent.currentTarget.ownerDocument.defaultView; |
|
702 let browser; |
|
703 switch (aEvent.type) { |
|
704 case "TabOpen": |
|
705 this.onTabAdd(win, aEvent.originalTarget); |
|
706 break; |
|
707 case "TabClose": |
|
708 // aEvent.detail determines if the tab was closed by moving to a different window |
|
709 if (!aEvent.detail) |
|
710 this.onTabClose(win, aEvent.originalTarget); |
|
711 this.onTabRemove(win, aEvent.originalTarget); |
|
712 break; |
|
713 case "TabSelect": |
|
714 this.onTabSelect(win); |
|
715 break; |
|
716 case "TabShow": |
|
717 this.onTabShow(win, aEvent.originalTarget); |
|
718 break; |
|
719 case "TabHide": |
|
720 this.onTabHide(win, aEvent.originalTarget); |
|
721 break; |
|
722 case "TabPinned": |
|
723 case "TabUnpinned": |
|
724 this.saveStateDelayed(win); |
|
725 break; |
|
726 } |
|
727 this._clearRestoringWindows(); |
|
728 }, |
|
729 |
|
730 /** |
|
731 * Generate a unique window identifier |
|
732 * @return string |
|
733 * A unique string to identify a window |
|
734 */ |
|
735 _generateWindowID: function ssi_generateWindowID() { |
|
736 return "window" + (this._nextWindowID++); |
|
737 }, |
|
738 |
|
739 /** |
|
740 * If it's the first window load since app start... |
|
741 * - determine if we're reloading after a crash or a forced-restart |
|
742 * - restore window state |
|
743 * - restart downloads |
|
744 * Set up event listeners for this window's tabs |
|
745 * @param aWindow |
|
746 * Window reference |
|
747 * @param aInitialState |
|
748 * The initial state to be loaded after startup (optional) |
|
749 */ |
|
750 onLoad: function ssi_onLoad(aWindow, aInitialState = null) { |
|
751 // return if window has already been initialized |
|
752 if (aWindow && aWindow.__SSi && this._windows[aWindow.__SSi]) |
|
753 return; |
|
754 |
|
755 // ignore windows opened while shutting down |
|
756 if (this._loadState == STATE_QUITTING) |
|
757 return; |
|
758 |
|
759 // Assign the window a unique identifier we can use to reference |
|
760 // internal data about the window. |
|
761 aWindow.__SSi = this._generateWindowID(); |
|
762 |
|
763 let mm = aWindow.messageManager; |
|
764 MESSAGES.forEach(msg => mm.addMessageListener(msg, this)); |
|
765 |
|
766 // Load the frame script after registering listeners. |
|
767 mm.loadFrameScript("chrome://browser/content/content-sessionStore.js", true); |
|
768 |
|
769 // and create its data object |
|
770 this._windows[aWindow.__SSi] = { tabs: [], selected: 0, _closedTabs: [], busy: false }; |
|
771 |
|
772 let isPrivateWindow = false; |
|
773 if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) |
|
774 this._windows[aWindow.__SSi].isPrivate = isPrivateWindow = true; |
|
775 if (!this._isWindowLoaded(aWindow)) |
|
776 this._windows[aWindow.__SSi]._restoring = true; |
|
777 if (!aWindow.toolbar.visible) |
|
778 this._windows[aWindow.__SSi].isPopup = true; |
|
779 |
|
780 // perform additional initialization when the first window is loading |
|
781 if (this._loadState == STATE_STOPPED) { |
|
782 this._loadState = STATE_RUNNING; |
|
783 SessionSaver.updateLastSaveTime(); |
|
784 |
|
785 // restore a crashed session resp. resume the last session if requested |
|
786 if (aInitialState) { |
|
787 if (isPrivateWindow) { |
|
788 // We're starting with a single private window. Save the state we |
|
789 // actually wanted to restore so that we can do it later in case |
|
790 // the user opens another, non-private window. |
|
791 this._deferredInitialState = gSessionStartup.state; |
|
792 |
|
793 // Nothing to restore now, notify observers things are complete. |
|
794 Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED, ""); |
|
795 } else { |
|
796 TelemetryTimestamps.add("sessionRestoreRestoring"); |
|
797 this._restoreCount = aInitialState.windows ? aInitialState.windows.length : 0; |
|
798 |
|
799 // global data must be restored before restoreWindow is called so that |
|
800 // it happens before observers are notified |
|
801 this._globalState.setFromState(aInitialState); |
|
802 |
|
803 let overwrite = this._isCmdLineEmpty(aWindow, aInitialState); |
|
804 let options = {firstWindow: true, overwriteTabs: overwrite}; |
|
805 this.restoreWindow(aWindow, aInitialState, options); |
|
806 } |
|
807 } |
|
808 else { |
|
809 // Nothing to restore, notify observers things are complete. |
|
810 Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED, ""); |
|
811 |
|
812 // The next delayed save request should execute immediately. |
|
813 SessionSaver.clearLastSaveTime(); |
|
814 } |
|
815 } |
|
816 // this window was opened by _openWindowWithState |
|
817 else if (!this._isWindowLoaded(aWindow)) { |
|
818 let state = this._statesToRestore[aWindow.__SS_restoreID]; |
|
819 let options = {overwriteTabs: true, isFollowUp: state.windows.length == 1}; |
|
820 this.restoreWindow(aWindow, state, options); |
|
821 } |
|
822 // The user opened another, non-private window after starting up with |
|
823 // a single private one. Let's restore the session we actually wanted to |
|
824 // restore at startup. |
|
825 else if (this._deferredInitialState && !isPrivateWindow && |
|
826 aWindow.toolbar.visible) { |
|
827 |
|
828 // global data must be restored before restoreWindow is called so that |
|
829 // it happens before observers are notified |
|
830 this._globalState.setFromState(this._deferredInitialState); |
|
831 |
|
832 this._restoreCount = this._deferredInitialState.windows ? |
|
833 this._deferredInitialState.windows.length : 0; |
|
834 this.restoreWindow(aWindow, this._deferredInitialState, {firstWindow: true}); |
|
835 this._deferredInitialState = null; |
|
836 } |
|
837 else if (this._restoreLastWindow && aWindow.toolbar.visible && |
|
838 this._closedWindows.length && !isPrivateWindow) { |
|
839 |
|
840 // default to the most-recently closed window |
|
841 // don't use popup windows |
|
842 let closedWindowState = null; |
|
843 let closedWindowIndex; |
|
844 for (let i = 0; i < this._closedWindows.length; i++) { |
|
845 // Take the first non-popup, point our object at it, and break out. |
|
846 if (!this._closedWindows[i].isPopup) { |
|
847 closedWindowState = this._closedWindows[i]; |
|
848 closedWindowIndex = i; |
|
849 break; |
|
850 } |
|
851 } |
|
852 |
|
853 if (closedWindowState) { |
|
854 let newWindowState; |
|
855 #ifndef XP_MACOSX |
|
856 if (!this._doResumeSession()) { |
|
857 #endif |
|
858 // We want to split the window up into pinned tabs and unpinned tabs. |
|
859 // Pinned tabs should be restored. If there are any remaining tabs, |
|
860 // they should be added back to _closedWindows. |
|
861 // We'll cheat a little bit and reuse _prepDataForDeferredRestore |
|
862 // even though it wasn't built exactly for this. |
|
863 let [appTabsState, normalTabsState] = |
|
864 this._prepDataForDeferredRestore({ windows: [closedWindowState] }); |
|
865 |
|
866 // These are our pinned tabs, which we should restore |
|
867 if (appTabsState.windows.length) { |
|
868 newWindowState = appTabsState.windows[0]; |
|
869 delete newWindowState.__lastSessionWindowID; |
|
870 } |
|
871 |
|
872 // In case there were no unpinned tabs, remove the window from _closedWindows |
|
873 if (!normalTabsState.windows.length) { |
|
874 this._closedWindows.splice(closedWindowIndex, 1); |
|
875 } |
|
876 // Or update _closedWindows with the modified state |
|
877 else { |
|
878 delete normalTabsState.windows[0].__lastSessionWindowID; |
|
879 this._closedWindows[closedWindowIndex] = normalTabsState.windows[0]; |
|
880 } |
|
881 #ifndef XP_MACOSX |
|
882 } |
|
883 else { |
|
884 // If we're just restoring the window, make sure it gets removed from |
|
885 // _closedWindows. |
|
886 this._closedWindows.splice(closedWindowIndex, 1); |
|
887 newWindowState = closedWindowState; |
|
888 delete newWindowState.hidden; |
|
889 } |
|
890 #endif |
|
891 if (newWindowState) { |
|
892 // Ensure that the window state isn't hidden |
|
893 this._restoreCount = 1; |
|
894 let state = { windows: [newWindowState] }; |
|
895 let options = {overwriteTabs: this._isCmdLineEmpty(aWindow, state)}; |
|
896 this.restoreWindow(aWindow, state, options); |
|
897 } |
|
898 } |
|
899 // we actually restored the session just now. |
|
900 this._prefBranch.setBoolPref("sessionstore.resume_session_once", false); |
|
901 } |
|
902 if (this._restoreLastWindow && aWindow.toolbar.visible) { |
|
903 // always reset (if not a popup window) |
|
904 // we don't want to restore a window directly after, for example, |
|
905 // undoCloseWindow was executed. |
|
906 this._restoreLastWindow = false; |
|
907 } |
|
908 |
|
909 var tabbrowser = aWindow.gBrowser; |
|
910 |
|
911 // add tab change listeners to all already existing tabs |
|
912 for (let i = 0; i < tabbrowser.tabs.length; i++) { |
|
913 this.onTabAdd(aWindow, tabbrowser.tabs[i], true); |
|
914 } |
|
915 // notification of tab add/remove/selection/show/hide |
|
916 TAB_EVENTS.forEach(function(aEvent) { |
|
917 tabbrowser.tabContainer.addEventListener(aEvent, this, true); |
|
918 }, this); |
|
919 }, |
|
920 |
|
921 /** |
|
922 * On window open |
|
923 * @param aWindow |
|
924 * Window reference |
|
925 */ |
|
926 onOpen: function ssi_onOpen(aWindow) { |
|
927 let onload = () => { |
|
928 aWindow.removeEventListener("load", onload); |
|
929 |
|
930 let windowType = aWindow.document.documentElement.getAttribute("windowtype"); |
|
931 |
|
932 // Ignore non-browser windows. |
|
933 if (windowType != "navigator:browser") { |
|
934 return; |
|
935 } |
|
936 |
|
937 if (this._sessionInitialized) { |
|
938 this.onLoad(aWindow); |
|
939 return; |
|
940 } |
|
941 |
|
942 // The very first window that is opened creates a promise that is then |
|
943 // re-used by all subsequent windows. The promise will be used to tell |
|
944 // when we're ready for initialization. |
|
945 if (!this._promiseReadyForInitialization) { |
|
946 let deferred = Promise.defer(); |
|
947 |
|
948 // Wait for the given window's delayed startup to be finished. |
|
949 Services.obs.addObserver(function obs(subject, topic) { |
|
950 if (aWindow == subject) { |
|
951 Services.obs.removeObserver(obs, topic); |
|
952 deferred.resolve(); |
|
953 } |
|
954 }, "browser-delayed-startup-finished", false); |
|
955 |
|
956 // We are ready for initialization as soon as the session file has been |
|
957 // read from disk and the initial window's delayed startup has finished. |
|
958 this._promiseReadyForInitialization = |
|
959 Promise.all([deferred.promise, gSessionStartup.onceInitialized]); |
|
960 } |
|
961 |
|
962 // We can't call this.onLoad since initialization |
|
963 // hasn't completed, so we'll wait until it is done. |
|
964 // Even if additional windows are opened and wait |
|
965 // for initialization as well, the first opened |
|
966 // window should execute first, and this.onLoad |
|
967 // will be called with the initialState. |
|
968 this._promiseReadyForInitialization.then(() => { |
|
969 if (aWindow.closed) { |
|
970 return; |
|
971 } |
|
972 |
|
973 if (this._sessionInitialized) { |
|
974 this.onLoad(aWindow); |
|
975 } else { |
|
976 let initialState = this.initSession(); |
|
977 this._sessionInitialized = true; |
|
978 this.onLoad(aWindow, initialState); |
|
979 |
|
980 // Let everyone know we're done. |
|
981 this._deferredInitialized.resolve(); |
|
982 } |
|
983 }, console.error); |
|
984 }; |
|
985 |
|
986 aWindow.addEventListener("load", onload); |
|
987 }, |
|
988 |
|
989 /** |
|
990 * On window close... |
|
991 * - remove event listeners from tabs |
|
992 * - save all window data |
|
993 * @param aWindow |
|
994 * Window reference |
|
995 */ |
|
996 onClose: function ssi_onClose(aWindow) { |
|
997 // this window was about to be restored - conserve its original data, if any |
|
998 let isFullyLoaded = this._isWindowLoaded(aWindow); |
|
999 if (!isFullyLoaded) { |
|
1000 if (!aWindow.__SSi) { |
|
1001 aWindow.__SSi = this._generateWindowID(); |
|
1002 } |
|
1003 |
|
1004 this._windows[aWindow.__SSi] = this._statesToRestore[aWindow.__SS_restoreID]; |
|
1005 delete this._statesToRestore[aWindow.__SS_restoreID]; |
|
1006 delete aWindow.__SS_restoreID; |
|
1007 } |
|
1008 |
|
1009 // ignore windows not tracked by SessionStore |
|
1010 if (!aWindow.__SSi || !this._windows[aWindow.__SSi]) { |
|
1011 return; |
|
1012 } |
|
1013 |
|
1014 // notify that the session store will stop tracking this window so that |
|
1015 // extensions can store any data about this window in session store before |
|
1016 // that's not possible anymore |
|
1017 let event = aWindow.document.createEvent("Events"); |
|
1018 event.initEvent("SSWindowClosing", true, false); |
|
1019 aWindow.dispatchEvent(event); |
|
1020 |
|
1021 if (this.windowToFocus && this.windowToFocus == aWindow) { |
|
1022 delete this.windowToFocus; |
|
1023 } |
|
1024 |
|
1025 var tabbrowser = aWindow.gBrowser; |
|
1026 |
|
1027 TAB_EVENTS.forEach(function(aEvent) { |
|
1028 tabbrowser.tabContainer.removeEventListener(aEvent, this, true); |
|
1029 }, this); |
|
1030 |
|
1031 let winData = this._windows[aWindow.__SSi]; |
|
1032 |
|
1033 // Collect window data only when *not* closed during shutdown. |
|
1034 if (this._loadState == STATE_RUNNING) { |
|
1035 // Flush all data queued in the content script before the window is gone. |
|
1036 TabState.flushWindow(aWindow); |
|
1037 |
|
1038 // update all window data for a last time |
|
1039 this._collectWindowData(aWindow); |
|
1040 |
|
1041 if (isFullyLoaded) { |
|
1042 winData.title = aWindow.content.document.title || tabbrowser.selectedTab.label; |
|
1043 winData.title = this._replaceLoadingTitle(winData.title, tabbrowser, |
|
1044 tabbrowser.selectedTab); |
|
1045 SessionCookies.update([winData]); |
|
1046 } |
|
1047 |
|
1048 #ifndef XP_MACOSX |
|
1049 // Until we decide otherwise elsewhere, this window is part of a series |
|
1050 // of closing windows to quit. |
|
1051 winData._shouldRestore = true; |
|
1052 #endif |
|
1053 |
|
1054 // Store the window's close date to figure out when each individual tab |
|
1055 // was closed. This timestamp should allow re-arranging data based on how |
|
1056 // recently something was closed. |
|
1057 winData.closedAt = Date.now(); |
|
1058 |
|
1059 // Save non-private windows if they have at |
|
1060 // least one saveable tab or are the last window. |
|
1061 if (!winData.isPrivate) { |
|
1062 // Remove any open private tabs the window may contain. |
|
1063 PrivacyFilter.filterPrivateTabs(winData); |
|
1064 |
|
1065 // Determine whether the window has any tabs worth saving. |
|
1066 let hasSaveableTabs = winData.tabs.some(this._shouldSaveTabState); |
|
1067 |
|
1068 // When closing windows one after the other until Firefox quits, we |
|
1069 // will move those closed in series back to the "open windows" bucket |
|
1070 // before writing to disk. If however there is only a single window |
|
1071 // with tabs we deem not worth saving then we might end up with a |
|
1072 // random closed or even a pop-up window re-opened. To prevent that |
|
1073 // we explicitly allow saving an "empty" window state. |
|
1074 let isLastWindow = |
|
1075 Object.keys(this._windows).length == 1 && |
|
1076 !this._closedWindows.some(win => win._shouldRestore || false); |
|
1077 |
|
1078 if (hasSaveableTabs || isLastWindow) { |
|
1079 // we don't want to save the busy state |
|
1080 delete winData.busy; |
|
1081 |
|
1082 this._closedWindows.unshift(winData); |
|
1083 this._capClosedWindows(); |
|
1084 } |
|
1085 } |
|
1086 |
|
1087 // clear this window from the list |
|
1088 delete this._windows[aWindow.__SSi]; |
|
1089 |
|
1090 // save the state without this window to disk |
|
1091 this.saveStateDelayed(); |
|
1092 } |
|
1093 |
|
1094 for (let i = 0; i < tabbrowser.tabs.length; i++) { |
|
1095 this.onTabRemove(aWindow, tabbrowser.tabs[i], true); |
|
1096 } |
|
1097 |
|
1098 // Cache the window state until it is completely gone. |
|
1099 DyingWindowCache.set(aWindow, winData); |
|
1100 |
|
1101 let mm = aWindow.messageManager; |
|
1102 MESSAGES.forEach(msg => mm.removeMessageListener(msg, this)); |
|
1103 |
|
1104 delete aWindow.__SSi; |
|
1105 }, |
|
1106 |
|
1107 /** |
|
1108 * On quit application requested |
|
1109 */ |
|
1110 onQuitApplicationRequested: function ssi_onQuitApplicationRequested() { |
|
1111 // get a current snapshot of all windows |
|
1112 this._forEachBrowserWindow(function(aWindow) { |
|
1113 // Flush all data queued in the content script to not lose it when |
|
1114 // shutting down. |
|
1115 TabState.flushWindow(aWindow); |
|
1116 this._collectWindowData(aWindow); |
|
1117 }); |
|
1118 // we must cache this because _getMostRecentBrowserWindow will always |
|
1119 // return null by the time quit-application occurs |
|
1120 var activeWindow = this._getMostRecentBrowserWindow(); |
|
1121 if (activeWindow) |
|
1122 this.activeWindowSSiCache = activeWindow.__SSi || ""; |
|
1123 DirtyWindows.clear(); |
|
1124 }, |
|
1125 |
|
1126 /** |
|
1127 * On quit application granted |
|
1128 */ |
|
1129 onQuitApplicationGranted: function ssi_onQuitApplicationGranted() { |
|
1130 // freeze the data at what we've got (ignoring closing windows) |
|
1131 this._loadState = STATE_QUITTING; |
|
1132 }, |
|
1133 |
|
1134 /** |
|
1135 * On last browser window close |
|
1136 */ |
|
1137 onLastWindowCloseGranted: function ssi_onLastWindowCloseGranted() { |
|
1138 // last browser window is quitting. |
|
1139 // remember to restore the last window when another browser window is opened |
|
1140 // do not account for pref(resume_session_once) at this point, as it might be |
|
1141 // set by another observer getting this notice after us |
|
1142 this._restoreLastWindow = true; |
|
1143 }, |
|
1144 |
|
1145 /** |
|
1146 * On quitting application |
|
1147 * @param aData |
|
1148 * String type of quitting |
|
1149 */ |
|
1150 onQuitApplication: function ssi_onQuitApplication(aData) { |
|
1151 if (aData == "restart") { |
|
1152 this._prefBranch.setBoolPref("sessionstore.resume_session_once", true); |
|
1153 // The browser:purge-session-history notification fires after the |
|
1154 // quit-application notification so unregister the |
|
1155 // browser:purge-session-history notification to prevent clearing |
|
1156 // session data on disk on a restart. It is also unnecessary to |
|
1157 // perform any other sanitization processing on a restart as the |
|
1158 // browser is about to exit anyway. |
|
1159 Services.obs.removeObserver(this, "browser:purge-session-history"); |
|
1160 } |
|
1161 |
|
1162 if (aData != "restart") { |
|
1163 // Throw away the previous session on shutdown |
|
1164 LastSession.clear(); |
|
1165 } |
|
1166 |
|
1167 this._loadState = STATE_QUITTING; // just to be sure |
|
1168 this._uninit(); |
|
1169 }, |
|
1170 |
|
1171 /** |
|
1172 * On purge of session history |
|
1173 */ |
|
1174 onPurgeSessionHistory: function ssi_onPurgeSessionHistory() { |
|
1175 SessionFile.wipe(); |
|
1176 // If the browser is shutting down, simply return after clearing the |
|
1177 // session data on disk as this notification fires after the |
|
1178 // quit-application notification so the browser is about to exit. |
|
1179 if (this._loadState == STATE_QUITTING) |
|
1180 return; |
|
1181 LastSession.clear(); |
|
1182 let openWindows = {}; |
|
1183 this._forEachBrowserWindow(function(aWindow) { |
|
1184 Array.forEach(aWindow.gBrowser.tabs, function(aTab) { |
|
1185 delete aTab.linkedBrowser.__SS_data; |
|
1186 if (aTab.linkedBrowser.__SS_restoreState) |
|
1187 this._resetTabRestoringState(aTab); |
|
1188 }, this); |
|
1189 openWindows[aWindow.__SSi] = true; |
|
1190 }); |
|
1191 // also clear all data about closed tabs and windows |
|
1192 for (let ix in this._windows) { |
|
1193 if (ix in openWindows) { |
|
1194 this._windows[ix]._closedTabs = []; |
|
1195 } else { |
|
1196 delete this._windows[ix]; |
|
1197 } |
|
1198 } |
|
1199 // also clear all data about closed windows |
|
1200 this._closedWindows = []; |
|
1201 // give the tabbrowsers a chance to clear their histories first |
|
1202 var win = this._getMostRecentBrowserWindow(); |
|
1203 if (win) { |
|
1204 win.setTimeout(() => SessionSaver.run(), 0); |
|
1205 } else if (this._loadState == STATE_RUNNING) { |
|
1206 SessionSaver.run(); |
|
1207 } |
|
1208 |
|
1209 this._clearRestoringWindows(); |
|
1210 }, |
|
1211 |
|
1212 /** |
|
1213 * On purge of domain data |
|
1214 * @param aData |
|
1215 * String domain data |
|
1216 */ |
|
1217 onPurgeDomainData: function ssi_onPurgeDomainData(aData) { |
|
1218 // does a session history entry contain a url for the given domain? |
|
1219 function containsDomain(aEntry) { |
|
1220 if (Utils.hasRootDomain(aEntry.url, aData)) { |
|
1221 return true; |
|
1222 } |
|
1223 return aEntry.children && aEntry.children.some(containsDomain, this); |
|
1224 } |
|
1225 // remove all closed tabs containing a reference to the given domain |
|
1226 for (let ix in this._windows) { |
|
1227 let closedTabs = this._windows[ix]._closedTabs; |
|
1228 for (let i = closedTabs.length - 1; i >= 0; i--) { |
|
1229 if (closedTabs[i].state.entries.some(containsDomain, this)) |
|
1230 closedTabs.splice(i, 1); |
|
1231 } |
|
1232 } |
|
1233 // remove all open & closed tabs containing a reference to the given |
|
1234 // domain in closed windows |
|
1235 for (let ix = this._closedWindows.length - 1; ix >= 0; ix--) { |
|
1236 let closedTabs = this._closedWindows[ix]._closedTabs; |
|
1237 let openTabs = this._closedWindows[ix].tabs; |
|
1238 let openTabCount = openTabs.length; |
|
1239 for (let i = closedTabs.length - 1; i >= 0; i--) |
|
1240 if (closedTabs[i].state.entries.some(containsDomain, this)) |
|
1241 closedTabs.splice(i, 1); |
|
1242 for (let j = openTabs.length - 1; j >= 0; j--) { |
|
1243 if (openTabs[j].entries.some(containsDomain, this)) { |
|
1244 openTabs.splice(j, 1); |
|
1245 if (this._closedWindows[ix].selected > j) |
|
1246 this._closedWindows[ix].selected--; |
|
1247 } |
|
1248 } |
|
1249 if (openTabs.length == 0) { |
|
1250 this._closedWindows.splice(ix, 1); |
|
1251 } |
|
1252 else if (openTabs.length != openTabCount) { |
|
1253 // Adjust the window's title if we removed an open tab |
|
1254 let selectedTab = openTabs[this._closedWindows[ix].selected - 1]; |
|
1255 // some duplication from restoreHistory - make sure we get the correct title |
|
1256 let activeIndex = (selectedTab.index || selectedTab.entries.length) - 1; |
|
1257 if (activeIndex >= selectedTab.entries.length) |
|
1258 activeIndex = selectedTab.entries.length - 1; |
|
1259 this._closedWindows[ix].title = selectedTab.entries[activeIndex].title; |
|
1260 } |
|
1261 } |
|
1262 |
|
1263 if (this._loadState == STATE_RUNNING) { |
|
1264 SessionSaver.run(); |
|
1265 } |
|
1266 |
|
1267 this._clearRestoringWindows(); |
|
1268 }, |
|
1269 |
|
1270 /** |
|
1271 * On preference change |
|
1272 * @param aData |
|
1273 * String preference changed |
|
1274 */ |
|
1275 onPrefChange: function ssi_onPrefChange(aData) { |
|
1276 switch (aData) { |
|
1277 // if the user decreases the max number of closed tabs they want |
|
1278 // preserved update our internal states to match that max |
|
1279 case "sessionstore.max_tabs_undo": |
|
1280 this._max_tabs_undo = this._prefBranch.getIntPref("sessionstore.max_tabs_undo"); |
|
1281 for (let ix in this._windows) { |
|
1282 this._windows[ix]._closedTabs.splice(this._max_tabs_undo, this._windows[ix]._closedTabs.length); |
|
1283 } |
|
1284 break; |
|
1285 case "sessionstore.max_windows_undo": |
|
1286 this._max_windows_undo = this._prefBranch.getIntPref("sessionstore.max_windows_undo"); |
|
1287 this._capClosedWindows(); |
|
1288 break; |
|
1289 } |
|
1290 }, |
|
1291 |
|
1292 /** |
|
1293 * set up listeners for a new tab |
|
1294 * @param aWindow |
|
1295 * Window reference |
|
1296 * @param aTab |
|
1297 * Tab reference |
|
1298 * @param aNoNotification |
|
1299 * bool Do not save state if we're updating an existing tab |
|
1300 */ |
|
1301 onTabAdd: function ssi_onTabAdd(aWindow, aTab, aNoNotification) { |
|
1302 if (!aNoNotification) { |
|
1303 this.saveStateDelayed(aWindow); |
|
1304 } |
|
1305 }, |
|
1306 |
|
1307 /** |
|
1308 * remove listeners for a tab |
|
1309 * @param aWindow |
|
1310 * Window reference |
|
1311 * @param aTab |
|
1312 * Tab reference |
|
1313 * @param aNoNotification |
|
1314 * bool Do not save state if we're updating an existing tab |
|
1315 */ |
|
1316 onTabRemove: function ssi_onTabRemove(aWindow, aTab, aNoNotification) { |
|
1317 let browser = aTab.linkedBrowser; |
|
1318 delete browser.__SS_data; |
|
1319 |
|
1320 // If this tab was in the middle of restoring or still needs to be restored, |
|
1321 // we need to reset that state. If the tab was restoring, we will attempt to |
|
1322 // restore the next tab. |
|
1323 let previousState = browser.__SS_restoreState; |
|
1324 if (previousState) { |
|
1325 this._resetTabRestoringState(aTab); |
|
1326 if (previousState == TAB_STATE_RESTORING) |
|
1327 this.restoreNextTab(); |
|
1328 } |
|
1329 |
|
1330 if (!aNoNotification) { |
|
1331 this.saveStateDelayed(aWindow); |
|
1332 } |
|
1333 }, |
|
1334 |
|
1335 /** |
|
1336 * When a tab closes, collect its properties |
|
1337 * @param aWindow |
|
1338 * Window reference |
|
1339 * @param aTab |
|
1340 * Tab reference |
|
1341 */ |
|
1342 onTabClose: function ssi_onTabClose(aWindow, aTab) { |
|
1343 // notify the tabbrowser that the tab state will be retrieved for the last time |
|
1344 // (so that extension authors can easily set data on soon-to-be-closed tabs) |
|
1345 var event = aWindow.document.createEvent("Events"); |
|
1346 event.initEvent("SSTabClosing", true, false); |
|
1347 aTab.dispatchEvent(event); |
|
1348 |
|
1349 // don't update our internal state if we don't have to |
|
1350 if (this._max_tabs_undo == 0) { |
|
1351 return; |
|
1352 } |
|
1353 |
|
1354 // Flush all data queued in the content script before the tab is gone. |
|
1355 TabState.flush(aTab.linkedBrowser); |
|
1356 |
|
1357 // Get the latest data for this tab (generally, from the cache) |
|
1358 let tabState = TabState.collect(aTab); |
|
1359 |
|
1360 // Don't save private tabs |
|
1361 let isPrivateWindow = PrivateBrowsingUtils.isWindowPrivate(aWindow); |
|
1362 if (!isPrivateWindow && tabState.isPrivate) { |
|
1363 return; |
|
1364 } |
|
1365 |
|
1366 // store closed-tab data for undo |
|
1367 if (this._shouldSaveTabState(tabState)) { |
|
1368 let tabTitle = aTab.label; |
|
1369 let tabbrowser = aWindow.gBrowser; |
|
1370 tabTitle = this._replaceLoadingTitle(tabTitle, tabbrowser, aTab); |
|
1371 |
|
1372 this._windows[aWindow.__SSi]._closedTabs.unshift({ |
|
1373 state: tabState, |
|
1374 title: tabTitle, |
|
1375 image: tabbrowser.getIcon(aTab), |
|
1376 pos: aTab._tPos, |
|
1377 closedAt: Date.now() |
|
1378 }); |
|
1379 var length = this._windows[aWindow.__SSi]._closedTabs.length; |
|
1380 if (length > this._max_tabs_undo) |
|
1381 this._windows[aWindow.__SSi]._closedTabs.splice(this._max_tabs_undo, length - this._max_tabs_undo); |
|
1382 } |
|
1383 }, |
|
1384 |
|
1385 /** |
|
1386 * When a tab is selected, save session data |
|
1387 * @param aWindow |
|
1388 * Window reference |
|
1389 */ |
|
1390 onTabSelect: function ssi_onTabSelect(aWindow) { |
|
1391 if (this._loadState == STATE_RUNNING) { |
|
1392 this._windows[aWindow.__SSi].selected = aWindow.gBrowser.tabContainer.selectedIndex; |
|
1393 |
|
1394 let tab = aWindow.gBrowser.selectedTab; |
|
1395 // If __SS_restoreState is still on the browser and it is |
|
1396 // TAB_STATE_NEEDS_RESTORE, then then we haven't restored |
|
1397 // this tab yet. Explicitly call restoreTabContent to kick off the restore. |
|
1398 if (tab.linkedBrowser.__SS_restoreState && |
|
1399 tab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) |
|
1400 this.restoreTabContent(tab); |
|
1401 } |
|
1402 }, |
|
1403 |
|
1404 onTabShow: function ssi_onTabShow(aWindow, aTab) { |
|
1405 // If the tab hasn't been restored yet, move it into the right bucket |
|
1406 if (aTab.linkedBrowser.__SS_restoreState && |
|
1407 aTab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) { |
|
1408 TabRestoreQueue.hiddenToVisible(aTab); |
|
1409 |
|
1410 // let's kick off tab restoration again to ensure this tab gets restored |
|
1411 // with "restore_hidden_tabs" == false (now that it has become visible) |
|
1412 this.restoreNextTab(); |
|
1413 } |
|
1414 |
|
1415 // Default delay of 2 seconds gives enough time to catch multiple TabShow |
|
1416 // events due to changing groups in Panorama. |
|
1417 this.saveStateDelayed(aWindow); |
|
1418 }, |
|
1419 |
|
1420 onTabHide: function ssi_onTabHide(aWindow, aTab) { |
|
1421 // If the tab hasn't been restored yet, move it into the right bucket |
|
1422 if (aTab.linkedBrowser.__SS_restoreState && |
|
1423 aTab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) { |
|
1424 TabRestoreQueue.visibleToHidden(aTab); |
|
1425 } |
|
1426 |
|
1427 // Default delay of 2 seconds gives enough time to catch multiple TabHide |
|
1428 // events due to changing groups in Panorama. |
|
1429 this.saveStateDelayed(aWindow); |
|
1430 }, |
|
1431 |
|
1432 onGatherTelemetry: function() { |
|
1433 // On the first gather-telemetry notification of the session, |
|
1434 // gather telemetry data. |
|
1435 Services.obs.removeObserver(this, "gather-telemetry"); |
|
1436 let stateString = SessionStore.getBrowserState(); |
|
1437 return SessionFile.gatherTelemetry(stateString); |
|
1438 }, |
|
1439 |
|
1440 /* ........ nsISessionStore API .............. */ |
|
1441 |
|
1442 getBrowserState: function ssi_getBrowserState() { |
|
1443 let state = this.getCurrentState(); |
|
1444 |
|
1445 // Don't include the last session state in getBrowserState(). |
|
1446 delete state.lastSessionState; |
|
1447 |
|
1448 // Don't include any deferred initial state. |
|
1449 delete state.deferredInitialState; |
|
1450 |
|
1451 return this._toJSONString(state); |
|
1452 }, |
|
1453 |
|
1454 setBrowserState: function ssi_setBrowserState(aState) { |
|
1455 this._handleClosedWindows(); |
|
1456 |
|
1457 try { |
|
1458 var state = JSON.parse(aState); |
|
1459 } |
|
1460 catch (ex) { /* invalid state object - don't restore anything */ } |
|
1461 if (!state) { |
|
1462 throw Components.Exception("Invalid state string: not JSON", Cr.NS_ERROR_INVALID_ARG); |
|
1463 } |
|
1464 if (!state.windows) { |
|
1465 throw Components.Exception("No windows", Cr.NS_ERROR_INVALID_ARG); |
|
1466 } |
|
1467 |
|
1468 this._browserSetState = true; |
|
1469 |
|
1470 // Make sure the priority queue is emptied out |
|
1471 this._resetRestoringState(); |
|
1472 |
|
1473 var window = this._getMostRecentBrowserWindow(); |
|
1474 if (!window) { |
|
1475 this._restoreCount = 1; |
|
1476 this._openWindowWithState(state); |
|
1477 return; |
|
1478 } |
|
1479 |
|
1480 // close all other browser windows |
|
1481 this._forEachBrowserWindow(function(aWindow) { |
|
1482 if (aWindow != window) { |
|
1483 aWindow.close(); |
|
1484 this.onClose(aWindow); |
|
1485 } |
|
1486 }); |
|
1487 |
|
1488 // make sure closed window data isn't kept |
|
1489 this._closedWindows = []; |
|
1490 |
|
1491 // determine how many windows are meant to be restored |
|
1492 this._restoreCount = state.windows ? state.windows.length : 0; |
|
1493 |
|
1494 // global data must be restored before restoreWindow is called so that |
|
1495 // it happens before observers are notified |
|
1496 this._globalState.setFromState(state); |
|
1497 |
|
1498 // restore to the given state |
|
1499 this.restoreWindow(window, state, {overwriteTabs: true}); |
|
1500 }, |
|
1501 |
|
1502 getWindowState: function ssi_getWindowState(aWindow) { |
|
1503 if ("__SSi" in aWindow) { |
|
1504 return this._toJSONString(this._getWindowState(aWindow)); |
|
1505 } |
|
1506 |
|
1507 if (DyingWindowCache.has(aWindow)) { |
|
1508 let data = DyingWindowCache.get(aWindow); |
|
1509 return this._toJSONString({ windows: [data] }); |
|
1510 } |
|
1511 |
|
1512 throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG); |
|
1513 }, |
|
1514 |
|
1515 setWindowState: function ssi_setWindowState(aWindow, aState, aOverwrite) { |
|
1516 if (!aWindow.__SSi) { |
|
1517 throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG); |
|
1518 } |
|
1519 |
|
1520 this.restoreWindow(aWindow, aState, {overwriteTabs: aOverwrite}); |
|
1521 }, |
|
1522 |
|
1523 getTabState: function ssi_getTabState(aTab) { |
|
1524 if (!aTab.ownerDocument) { |
|
1525 throw Components.Exception("Invalid tab object: no ownerDocument", Cr.NS_ERROR_INVALID_ARG); |
|
1526 } |
|
1527 if (!aTab.ownerDocument.defaultView.__SSi) { |
|
1528 throw Components.Exception("Default view is not tracked", Cr.NS_ERROR_INVALID_ARG); |
|
1529 } |
|
1530 |
|
1531 let tabState = TabState.collect(aTab); |
|
1532 |
|
1533 return this._toJSONString(tabState); |
|
1534 }, |
|
1535 |
|
1536 setTabState: function ssi_setTabState(aTab, aState) { |
|
1537 // Remove the tab state from the cache. |
|
1538 // Note that we cannot simply replace the contents of the cache |
|
1539 // as |aState| can be an incomplete state that will be completed |
|
1540 // by |restoreTabs|. |
|
1541 let tabState = JSON.parse(aState); |
|
1542 if (!tabState) { |
|
1543 throw Components.Exception("Invalid state string: not JSON", Cr.NS_ERROR_INVALID_ARG); |
|
1544 } |
|
1545 if (typeof tabState != "object") { |
|
1546 throw Components.Exception("Not an object", Cr.NS_ERROR_INVALID_ARG); |
|
1547 } |
|
1548 if (!("entries" in tabState)) { |
|
1549 throw Components.Exception("Invalid state object: no entries", Cr.NS_ERROR_INVALID_ARG); |
|
1550 } |
|
1551 if (!aTab.ownerDocument) { |
|
1552 throw Components.Exception("Invalid tab object: no ownerDocument", Cr.NS_ERROR_INVALID_ARG); |
|
1553 } |
|
1554 |
|
1555 let window = aTab.ownerDocument.defaultView; |
|
1556 if (!("__SSi" in window)) { |
|
1557 throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG); |
|
1558 } |
|
1559 |
|
1560 if (aTab.linkedBrowser.__SS_restoreState) { |
|
1561 this._resetTabRestoringState(aTab); |
|
1562 } |
|
1563 |
|
1564 this._setWindowStateBusy(window); |
|
1565 this.restoreTabs(window, [aTab], [tabState], 0); |
|
1566 }, |
|
1567 |
|
1568 duplicateTab: function ssi_duplicateTab(aWindow, aTab, aDelta = 0) { |
|
1569 if (!aTab.ownerDocument) { |
|
1570 throw Components.Exception("Invalid tab object: no ownerDocument", Cr.NS_ERROR_INVALID_ARG); |
|
1571 } |
|
1572 if (!aTab.ownerDocument.defaultView.__SSi) { |
|
1573 throw Components.Exception("Default view is not tracked", Cr.NS_ERROR_INVALID_ARG); |
|
1574 } |
|
1575 if (!aWindow.getBrowser) { |
|
1576 throw Components.Exception("Invalid window object: no getBrowser", Cr.NS_ERROR_INVALID_ARG); |
|
1577 } |
|
1578 |
|
1579 // Flush all data queued in the content script because we will need that |
|
1580 // state to properly duplicate the given tab. |
|
1581 TabState.flush(aTab.linkedBrowser); |
|
1582 |
|
1583 // Duplicate the tab state |
|
1584 let tabState = TabState.clone(aTab); |
|
1585 |
|
1586 tabState.index += aDelta; |
|
1587 tabState.index = Math.max(1, Math.min(tabState.index, tabState.entries.length)); |
|
1588 tabState.pinned = false; |
|
1589 |
|
1590 this._setWindowStateBusy(aWindow); |
|
1591 let newTab = aTab == aWindow.gBrowser.selectedTab ? |
|
1592 aWindow.gBrowser.addTab(null, {relatedToCurrent: true, ownerTab: aTab}) : |
|
1593 aWindow.gBrowser.addTab(); |
|
1594 |
|
1595 this.restoreTabs(aWindow, [newTab], [tabState], 0, |
|
1596 true /* Load this tab right away. */); |
|
1597 |
|
1598 return newTab; |
|
1599 }, |
|
1600 |
|
1601 getClosedTabCount: function ssi_getClosedTabCount(aWindow) { |
|
1602 if ("__SSi" in aWindow) { |
|
1603 return this._windows[aWindow.__SSi]._closedTabs.length; |
|
1604 } |
|
1605 |
|
1606 if (!DyingWindowCache.has(aWindow)) { |
|
1607 throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG); |
|
1608 } |
|
1609 |
|
1610 return DyingWindowCache.get(aWindow)._closedTabs.length; |
|
1611 }, |
|
1612 |
|
1613 getClosedTabData: function ssi_getClosedTabDataAt(aWindow) { |
|
1614 if ("__SSi" in aWindow) { |
|
1615 return this._toJSONString(this._windows[aWindow.__SSi]._closedTabs); |
|
1616 } |
|
1617 |
|
1618 if (!DyingWindowCache.has(aWindow)) { |
|
1619 throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG); |
|
1620 } |
|
1621 |
|
1622 let data = DyingWindowCache.get(aWindow); |
|
1623 return this._toJSONString(data._closedTabs); |
|
1624 }, |
|
1625 |
|
1626 undoCloseTab: function ssi_undoCloseTab(aWindow, aIndex) { |
|
1627 if (!aWindow.__SSi) { |
|
1628 throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG); |
|
1629 } |
|
1630 |
|
1631 var closedTabs = this._windows[aWindow.__SSi]._closedTabs; |
|
1632 |
|
1633 // default to the most-recently closed tab |
|
1634 aIndex = aIndex || 0; |
|
1635 if (!(aIndex in closedTabs)) { |
|
1636 throw Components.Exception("Invalid index: not in the closed tabs", Cr.NS_ERROR_INVALID_ARG); |
|
1637 } |
|
1638 |
|
1639 // fetch the data of closed tab, while removing it from the array |
|
1640 let closedTab = closedTabs.splice(aIndex, 1).shift(); |
|
1641 let closedTabState = closedTab.state; |
|
1642 |
|
1643 this._setWindowStateBusy(aWindow); |
|
1644 // create a new tab |
|
1645 let tabbrowser = aWindow.gBrowser; |
|
1646 let tab = tabbrowser.addTab(); |
|
1647 |
|
1648 // restore tab content |
|
1649 this.restoreTabs(aWindow, [tab], [closedTabState], 1); |
|
1650 |
|
1651 // restore the tab's position |
|
1652 tabbrowser.moveTabTo(tab, closedTab.pos); |
|
1653 |
|
1654 // focus the tab's content area (bug 342432) |
|
1655 tab.linkedBrowser.focus(); |
|
1656 |
|
1657 return tab; |
|
1658 }, |
|
1659 |
|
1660 forgetClosedTab: function ssi_forgetClosedTab(aWindow, aIndex) { |
|
1661 if (!aWindow.__SSi) { |
|
1662 throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG); |
|
1663 } |
|
1664 |
|
1665 var closedTabs = this._windows[aWindow.__SSi]._closedTabs; |
|
1666 |
|
1667 // default to the most-recently closed tab |
|
1668 aIndex = aIndex || 0; |
|
1669 if (!(aIndex in closedTabs)) { |
|
1670 throw Components.Exception("Invalid index: not in the closed tabs", Cr.NS_ERROR_INVALID_ARG); |
|
1671 } |
|
1672 |
|
1673 // remove closed tab from the array |
|
1674 closedTabs.splice(aIndex, 1); |
|
1675 }, |
|
1676 |
|
1677 getClosedWindowCount: function ssi_getClosedWindowCount() { |
|
1678 return this._closedWindows.length; |
|
1679 }, |
|
1680 |
|
1681 getClosedWindowData: function ssi_getClosedWindowData() { |
|
1682 return this._toJSONString(this._closedWindows); |
|
1683 }, |
|
1684 |
|
1685 undoCloseWindow: function ssi_undoCloseWindow(aIndex) { |
|
1686 if (!(aIndex in this._closedWindows)) { |
|
1687 throw Components.Exception("Invalid index: not in the closed windows", Cr.NS_ERROR_INVALID_ARG); |
|
1688 } |
|
1689 |
|
1690 // reopen the window |
|
1691 let state = { windows: this._closedWindows.splice(aIndex, 1) }; |
|
1692 let window = this._openWindowWithState(state); |
|
1693 this.windowToFocus = window; |
|
1694 return window; |
|
1695 }, |
|
1696 |
|
1697 forgetClosedWindow: function ssi_forgetClosedWindow(aIndex) { |
|
1698 // default to the most-recently closed window |
|
1699 aIndex = aIndex || 0; |
|
1700 if (!(aIndex in this._closedWindows)) { |
|
1701 throw Components.Exception("Invalid index: not in the closed windows", Cr.NS_ERROR_INVALID_ARG); |
|
1702 } |
|
1703 |
|
1704 // remove closed window from the array |
|
1705 this._closedWindows.splice(aIndex, 1); |
|
1706 }, |
|
1707 |
|
1708 getWindowValue: function ssi_getWindowValue(aWindow, aKey) { |
|
1709 if ("__SSi" in aWindow) { |
|
1710 var data = this._windows[aWindow.__SSi].extData || {}; |
|
1711 return data[aKey] || ""; |
|
1712 } |
|
1713 |
|
1714 if (DyingWindowCache.has(aWindow)) { |
|
1715 let data = DyingWindowCache.get(aWindow).extData || {}; |
|
1716 return data[aKey] || ""; |
|
1717 } |
|
1718 |
|
1719 throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG); |
|
1720 }, |
|
1721 |
|
1722 setWindowValue: function ssi_setWindowValue(aWindow, aKey, aStringValue) { |
|
1723 if (typeof aStringValue != "string") { |
|
1724 throw new TypeError("setWindowValue only accepts string values"); |
|
1725 } |
|
1726 |
|
1727 if (!("__SSi" in aWindow)) { |
|
1728 throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG); |
|
1729 } |
|
1730 if (!this._windows[aWindow.__SSi].extData) { |
|
1731 this._windows[aWindow.__SSi].extData = {}; |
|
1732 } |
|
1733 this._windows[aWindow.__SSi].extData[aKey] = aStringValue; |
|
1734 this.saveStateDelayed(aWindow); |
|
1735 }, |
|
1736 |
|
1737 deleteWindowValue: function ssi_deleteWindowValue(aWindow, aKey) { |
|
1738 if (aWindow.__SSi && this._windows[aWindow.__SSi].extData && |
|
1739 this._windows[aWindow.__SSi].extData[aKey]) |
|
1740 delete this._windows[aWindow.__SSi].extData[aKey]; |
|
1741 this.saveStateDelayed(aWindow); |
|
1742 }, |
|
1743 |
|
1744 getTabValue: function ssi_getTabValue(aTab, aKey) { |
|
1745 let data = {}; |
|
1746 if (aTab.__SS_extdata) { |
|
1747 data = aTab.__SS_extdata; |
|
1748 } |
|
1749 else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) { |
|
1750 // If the tab hasn't been fully restored, get the data from the to-be-restored data |
|
1751 data = aTab.linkedBrowser.__SS_data.extData; |
|
1752 } |
|
1753 return data[aKey] || ""; |
|
1754 }, |
|
1755 |
|
1756 setTabValue: function ssi_setTabValue(aTab, aKey, aStringValue) { |
|
1757 if (typeof aStringValue != "string") { |
|
1758 throw new TypeError("setTabValue only accepts string values"); |
|
1759 } |
|
1760 |
|
1761 // If the tab hasn't been restored, then set the data there, otherwise we |
|
1762 // could lose newly added data. |
|
1763 let saveTo; |
|
1764 if (aTab.__SS_extdata) { |
|
1765 saveTo = aTab.__SS_extdata; |
|
1766 } |
|
1767 else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) { |
|
1768 saveTo = aTab.linkedBrowser.__SS_data.extData; |
|
1769 } |
|
1770 else { |
|
1771 aTab.__SS_extdata = {}; |
|
1772 saveTo = aTab.__SS_extdata; |
|
1773 } |
|
1774 |
|
1775 saveTo[aKey] = aStringValue; |
|
1776 this.saveStateDelayed(aTab.ownerDocument.defaultView); |
|
1777 }, |
|
1778 |
|
1779 deleteTabValue: function ssi_deleteTabValue(aTab, aKey) { |
|
1780 // We want to make sure that if data is accessed early, we attempt to delete |
|
1781 // that data from __SS_data as well. Otherwise we'll throw in cases where |
|
1782 // data can be set or read. |
|
1783 let deleteFrom; |
|
1784 if (aTab.__SS_extdata) { |
|
1785 deleteFrom = aTab.__SS_extdata; |
|
1786 } |
|
1787 else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) { |
|
1788 deleteFrom = aTab.linkedBrowser.__SS_data.extData; |
|
1789 } |
|
1790 |
|
1791 if (deleteFrom && aKey in deleteFrom) { |
|
1792 delete deleteFrom[aKey]; |
|
1793 this.saveStateDelayed(aTab.ownerDocument.defaultView); |
|
1794 } |
|
1795 }, |
|
1796 |
|
1797 getGlobalValue: function ssi_getGlobalValue(aKey) { |
|
1798 return this._globalState.get(aKey); |
|
1799 }, |
|
1800 |
|
1801 setGlobalValue: function ssi_setGlobalValue(aKey, aStringValue) { |
|
1802 if (typeof aStringValue != "string") { |
|
1803 throw new TypeError("setGlobalValue only accepts string values"); |
|
1804 } |
|
1805 |
|
1806 this._globalState.set(aKey, aStringValue); |
|
1807 this.saveStateDelayed(); |
|
1808 }, |
|
1809 |
|
1810 deleteGlobalValue: function ssi_deleteGlobalValue(aKey) { |
|
1811 this._globalState.delete(aKey); |
|
1812 this.saveStateDelayed(); |
|
1813 }, |
|
1814 |
|
1815 persistTabAttribute: function ssi_persistTabAttribute(aName) { |
|
1816 if (TabAttributes.persist(aName)) { |
|
1817 this.saveStateDelayed(); |
|
1818 } |
|
1819 }, |
|
1820 |
|
1821 /** |
|
1822 * Restores the session state stored in LastSession. This will attempt |
|
1823 * to merge data into the current session. If a window was opened at startup |
|
1824 * with pinned tab(s), then the remaining data from the previous session for |
|
1825 * that window will be opened into that winddow. Otherwise new windows will |
|
1826 * be opened. |
|
1827 */ |
|
1828 restoreLastSession: function ssi_restoreLastSession() { |
|
1829 // Use the public getter since it also checks PB mode |
|
1830 if (!this.canRestoreLastSession) { |
|
1831 throw Components.Exception("Last session can not be restored"); |
|
1832 } |
|
1833 |
|
1834 // First collect each window with its id... |
|
1835 let windows = {}; |
|
1836 this._forEachBrowserWindow(function(aWindow) { |
|
1837 if (aWindow.__SS_lastSessionWindowID) |
|
1838 windows[aWindow.__SS_lastSessionWindowID] = aWindow; |
|
1839 }); |
|
1840 |
|
1841 let lastSessionState = LastSession.getState(); |
|
1842 |
|
1843 // This shouldn't ever be the case... |
|
1844 if (!lastSessionState.windows.length) { |
|
1845 throw Components.Exception("lastSessionState has no windows", Cr.NS_ERROR_UNEXPECTED); |
|
1846 } |
|
1847 |
|
1848 // We're technically doing a restore, so set things up so we send the |
|
1849 // notification when we're done. We want to send "sessionstore-browser-state-restored". |
|
1850 this._restoreCount = lastSessionState.windows.length; |
|
1851 this._browserSetState = true; |
|
1852 |
|
1853 // We want to re-use the last opened window instead of opening a new one in |
|
1854 // the case where it's "empty" and not associated with a window in the session. |
|
1855 // We will do more processing via _prepWindowToRestoreInto if we need to use |
|
1856 // the lastWindow. |
|
1857 let lastWindow = this._getMostRecentBrowserWindow(); |
|
1858 let canUseLastWindow = lastWindow && |
|
1859 !lastWindow.__SS_lastSessionWindowID; |
|
1860 |
|
1861 // global data must be restored before restoreWindow is called so that |
|
1862 // it happens before observers are notified |
|
1863 this._globalState.setFromState(lastSessionState); |
|
1864 |
|
1865 // Restore into windows or open new ones as needed. |
|
1866 for (let i = 0; i < lastSessionState.windows.length; i++) { |
|
1867 let winState = lastSessionState.windows[i]; |
|
1868 let lastSessionWindowID = winState.__lastSessionWindowID; |
|
1869 // delete lastSessionWindowID so we don't add that to the window again |
|
1870 delete winState.__lastSessionWindowID; |
|
1871 |
|
1872 // See if we can use an open window. First try one that is associated with |
|
1873 // the state we're trying to restore and then fallback to the last selected |
|
1874 // window. |
|
1875 let windowToUse = windows[lastSessionWindowID]; |
|
1876 if (!windowToUse && canUseLastWindow) { |
|
1877 windowToUse = lastWindow; |
|
1878 canUseLastWindow = false; |
|
1879 } |
|
1880 |
|
1881 let [canUseWindow, canOverwriteTabs] = this._prepWindowToRestoreInto(windowToUse); |
|
1882 |
|
1883 // If there's a window already open that we can restore into, use that |
|
1884 if (canUseWindow) { |
|
1885 // Since we're not overwriting existing tabs, we want to merge _closedTabs, |
|
1886 // putting existing ones first. Then make sure we're respecting the max pref. |
|
1887 if (winState._closedTabs && winState._closedTabs.length) { |
|
1888 let curWinState = this._windows[windowToUse.__SSi]; |
|
1889 curWinState._closedTabs = curWinState._closedTabs.concat(winState._closedTabs); |
|
1890 curWinState._closedTabs.splice(this._max_tabs_undo, curWinState._closedTabs.length); |
|
1891 } |
|
1892 |
|
1893 // Restore into that window - pretend it's a followup since we'll already |
|
1894 // have a focused window. |
|
1895 //XXXzpao This is going to merge extData together (taking what was in |
|
1896 // winState over what is in the window already. The hack we have |
|
1897 // in _preWindowToRestoreInto will prevent most (all?) Panorama |
|
1898 // weirdness but we will still merge other extData. |
|
1899 // Bug 588217 should make this go away by merging the group data. |
|
1900 let options = {overwriteTabs: canOverwriteTabs, isFollowUp: true}; |
|
1901 this.restoreWindow(windowToUse, { windows: [winState] }, options); |
|
1902 } |
|
1903 else { |
|
1904 this._openWindowWithState({ windows: [winState] }); |
|
1905 } |
|
1906 } |
|
1907 |
|
1908 // Merge closed windows from this session with ones from last session |
|
1909 if (lastSessionState._closedWindows) { |
|
1910 this._closedWindows = this._closedWindows.concat(lastSessionState._closedWindows); |
|
1911 this._capClosedWindows(); |
|
1912 } |
|
1913 |
|
1914 if (lastSessionState.scratchpads) { |
|
1915 ScratchpadManager.restoreSession(lastSessionState.scratchpads); |
|
1916 } |
|
1917 |
|
1918 // Set data that persists between sessions |
|
1919 this._recentCrashes = lastSessionState.session && |
|
1920 lastSessionState.session.recentCrashes || 0; |
|
1921 |
|
1922 // Update the session start time using the restored session state. |
|
1923 this._updateSessionStartTime(lastSessionState); |
|
1924 |
|
1925 LastSession.clear(); |
|
1926 }, |
|
1927 |
|
1928 /** |
|
1929 * See if aWindow is usable for use when restoring a previous session via |
|
1930 * restoreLastSession. If usable, prepare it for use. |
|
1931 * |
|
1932 * @param aWindow |
|
1933 * the window to inspect & prepare |
|
1934 * @returns [canUseWindow, canOverwriteTabs] |
|
1935 * canUseWindow: can the window be used to restore into |
|
1936 * canOverwriteTabs: all of the current tabs are home pages and we |
|
1937 * can overwrite them |
|
1938 */ |
|
1939 _prepWindowToRestoreInto: function ssi_prepWindowToRestoreInto(aWindow) { |
|
1940 if (!aWindow) |
|
1941 return [false, false]; |
|
1942 |
|
1943 // We might be able to overwrite the existing tabs instead of just adding |
|
1944 // the previous session's tabs to the end. This will be set if possible. |
|
1945 let canOverwriteTabs = false; |
|
1946 |
|
1947 // Step 1 of processing: |
|
1948 // Inspect extData for Panorama identifiers. If found, then we want to |
|
1949 // inspect further. If there is a single group, then we can use this |
|
1950 // window. If there are multiple groups then we won't use this window. |
|
1951 let groupsData = this.getWindowValue(aWindow, "tabview-groups"); |
|
1952 if (groupsData) { |
|
1953 groupsData = JSON.parse(groupsData); |
|
1954 |
|
1955 // If there are multiple groups, we don't want to use this window. |
|
1956 if (groupsData.totalNumber > 1) |
|
1957 return [false, false]; |
|
1958 } |
|
1959 |
|
1960 // Step 2 of processing: |
|
1961 // If we're still here, then the window is usable. Look at the open tabs in |
|
1962 // comparison to home pages. If all the tabs are home pages then we'll end |
|
1963 // up overwriting all of them. Otherwise we'll just close the tabs that |
|
1964 // match home pages. Tabs with the about:blank URI will always be |
|
1965 // overwritten. |
|
1966 let homePages = ["about:blank"]; |
|
1967 let removableTabs = []; |
|
1968 let tabbrowser = aWindow.gBrowser; |
|
1969 let normalTabsLen = tabbrowser.tabs.length - tabbrowser._numPinnedTabs; |
|
1970 let startupPref = this._prefBranch.getIntPref("startup.page"); |
|
1971 if (startupPref == 1) |
|
1972 homePages = homePages.concat(aWindow.gHomeButton.getHomePage().split("|")); |
|
1973 |
|
1974 for (let i = tabbrowser._numPinnedTabs; i < tabbrowser.tabs.length; i++) { |
|
1975 let tab = tabbrowser.tabs[i]; |
|
1976 if (homePages.indexOf(tab.linkedBrowser.currentURI.spec) != -1) { |
|
1977 removableTabs.push(tab); |
|
1978 } |
|
1979 } |
|
1980 |
|
1981 if (tabbrowser.tabs.length == removableTabs.length) { |
|
1982 canOverwriteTabs = true; |
|
1983 } |
|
1984 else { |
|
1985 // If we're not overwriting all of the tabs, then close the home tabs. |
|
1986 for (let i = removableTabs.length - 1; i >= 0; i--) { |
|
1987 tabbrowser.removeTab(removableTabs.pop(), { animate: false }); |
|
1988 } |
|
1989 } |
|
1990 |
|
1991 return [true, canOverwriteTabs]; |
|
1992 }, |
|
1993 |
|
1994 /* ........ Saving Functionality .............. */ |
|
1995 |
|
1996 /** |
|
1997 * Store window dimensions, visibility, sidebar |
|
1998 * @param aWindow |
|
1999 * Window reference |
|
2000 */ |
|
2001 _updateWindowFeatures: function ssi_updateWindowFeatures(aWindow) { |
|
2002 var winData = this._windows[aWindow.__SSi]; |
|
2003 |
|
2004 WINDOW_ATTRIBUTES.forEach(function(aAttr) { |
|
2005 winData[aAttr] = this._getWindowDimension(aWindow, aAttr); |
|
2006 }, this); |
|
2007 |
|
2008 var hidden = WINDOW_HIDEABLE_FEATURES.filter(function(aItem) { |
|
2009 return aWindow[aItem] && !aWindow[aItem].visible; |
|
2010 }); |
|
2011 if (hidden.length != 0) |
|
2012 winData.hidden = hidden.join(","); |
|
2013 else if (winData.hidden) |
|
2014 delete winData.hidden; |
|
2015 |
|
2016 var sidebar = aWindow.document.getElementById("sidebar-box").getAttribute("sidebarcommand"); |
|
2017 if (sidebar) |
|
2018 winData.sidebar = sidebar; |
|
2019 else if (winData.sidebar) |
|
2020 delete winData.sidebar; |
|
2021 }, |
|
2022 |
|
2023 /** |
|
2024 * gather session data as object |
|
2025 * @param aUpdateAll |
|
2026 * Bool update all windows |
|
2027 * @returns object |
|
2028 */ |
|
2029 getCurrentState: function (aUpdateAll) { |
|
2030 this._handleClosedWindows(); |
|
2031 |
|
2032 var activeWindow = this._getMostRecentBrowserWindow(); |
|
2033 |
|
2034 TelemetryStopwatch.start("FX_SESSION_RESTORE_COLLECT_ALL_WINDOWS_DATA_MS"); |
|
2035 if (this._loadState == STATE_RUNNING) { |
|
2036 // update the data for all windows with activities since the last save operation |
|
2037 this._forEachBrowserWindow(function(aWindow) { |
|
2038 if (!this._isWindowLoaded(aWindow)) // window data is still in _statesToRestore |
|
2039 return; |
|
2040 if (aUpdateAll || DirtyWindows.has(aWindow) || aWindow == activeWindow) { |
|
2041 this._collectWindowData(aWindow); |
|
2042 } |
|
2043 else { // always update the window features (whose change alone never triggers a save operation) |
|
2044 this._updateWindowFeatures(aWindow); |
|
2045 } |
|
2046 }); |
|
2047 DirtyWindows.clear(); |
|
2048 } |
|
2049 TelemetryStopwatch.finish("FX_SESSION_RESTORE_COLLECT_ALL_WINDOWS_DATA_MS"); |
|
2050 |
|
2051 // An array that at the end will hold all current window data. |
|
2052 var total = []; |
|
2053 // The ids of all windows contained in 'total' in the same order. |
|
2054 var ids = []; |
|
2055 // The number of window that are _not_ popups. |
|
2056 var nonPopupCount = 0; |
|
2057 var ix; |
|
2058 |
|
2059 // collect the data for all windows |
|
2060 for (ix in this._windows) { |
|
2061 if (this._windows[ix]._restoring) // window data is still in _statesToRestore |
|
2062 continue; |
|
2063 total.push(this._windows[ix]); |
|
2064 ids.push(ix); |
|
2065 if (!this._windows[ix].isPopup) |
|
2066 nonPopupCount++; |
|
2067 } |
|
2068 |
|
2069 TelemetryStopwatch.start("FX_SESSION_RESTORE_COLLECT_COOKIES_MS"); |
|
2070 SessionCookies.update(total); |
|
2071 TelemetryStopwatch.finish("FX_SESSION_RESTORE_COLLECT_COOKIES_MS"); |
|
2072 |
|
2073 // collect the data for all windows yet to be restored |
|
2074 for (ix in this._statesToRestore) { |
|
2075 for each (let winData in this._statesToRestore[ix].windows) { |
|
2076 total.push(winData); |
|
2077 if (!winData.isPopup) |
|
2078 nonPopupCount++; |
|
2079 } |
|
2080 } |
|
2081 |
|
2082 // shallow copy this._closedWindows to preserve current state |
|
2083 let lastClosedWindowsCopy = this._closedWindows.slice(); |
|
2084 |
|
2085 #ifndef XP_MACOSX |
|
2086 // If no non-popup browser window remains open, return the state of the last |
|
2087 // closed window(s). We only want to do this when we're actually "ending" |
|
2088 // the session. |
|
2089 //XXXzpao We should do this for _restoreLastWindow == true, but that has |
|
2090 // its own check for popups. c.f. bug 597619 |
|
2091 if (nonPopupCount == 0 && lastClosedWindowsCopy.length > 0 && |
|
2092 this._loadState == STATE_QUITTING) { |
|
2093 // prepend the last non-popup browser window, so that if the user loads more tabs |
|
2094 // at startup we don't accidentally add them to a popup window |
|
2095 do { |
|
2096 total.unshift(lastClosedWindowsCopy.shift()) |
|
2097 } while (total[0].isPopup && lastClosedWindowsCopy.length > 0) |
|
2098 } |
|
2099 #endif |
|
2100 |
|
2101 if (activeWindow) { |
|
2102 this.activeWindowSSiCache = activeWindow.__SSi || ""; |
|
2103 } |
|
2104 ix = ids.indexOf(this.activeWindowSSiCache); |
|
2105 // We don't want to restore focus to a minimized window or a window which had all its |
|
2106 // tabs stripped out (doesn't exist). |
|
2107 if (ix != -1 && total[ix] && total[ix].sizemode == "minimized") |
|
2108 ix = -1; |
|
2109 |
|
2110 let session = { |
|
2111 lastUpdate: Date.now(), |
|
2112 startTime: this._sessionStartTime, |
|
2113 recentCrashes: this._recentCrashes |
|
2114 }; |
|
2115 |
|
2116 // get open Scratchpad window states too |
|
2117 let scratchpads = ScratchpadManager.getSessionState(); |
|
2118 |
|
2119 let state = { |
|
2120 windows: total, |
|
2121 selectedWindow: ix + 1, |
|
2122 _closedWindows: lastClosedWindowsCopy, |
|
2123 session: session, |
|
2124 scratchpads: scratchpads, |
|
2125 global: this._globalState.getState() |
|
2126 }; |
|
2127 |
|
2128 // Persist the last session if we deferred restoring it |
|
2129 if (LastSession.canRestore) { |
|
2130 state.lastSessionState = LastSession.getState(); |
|
2131 } |
|
2132 |
|
2133 // If we were called by the SessionSaver and started with only a private |
|
2134 // window we want to pass the deferred initial state to not lose the |
|
2135 // previous session. |
|
2136 if (this._deferredInitialState) { |
|
2137 state.deferredInitialState = this._deferredInitialState; |
|
2138 } |
|
2139 |
|
2140 return state; |
|
2141 }, |
|
2142 |
|
2143 /** |
|
2144 * serialize session data for a window |
|
2145 * @param aWindow |
|
2146 * Window reference |
|
2147 * @returns string |
|
2148 */ |
|
2149 _getWindowState: function ssi_getWindowState(aWindow) { |
|
2150 if (!this._isWindowLoaded(aWindow)) |
|
2151 return this._statesToRestore[aWindow.__SS_restoreID]; |
|
2152 |
|
2153 if (this._loadState == STATE_RUNNING) { |
|
2154 this._collectWindowData(aWindow); |
|
2155 } |
|
2156 |
|
2157 let windows = [this._windows[aWindow.__SSi]]; |
|
2158 SessionCookies.update(windows); |
|
2159 |
|
2160 return { windows: windows }; |
|
2161 }, |
|
2162 |
|
2163 _collectWindowData: function ssi_collectWindowData(aWindow) { |
|
2164 if (!this._isWindowLoaded(aWindow)) |
|
2165 return; |
|
2166 TelemetryStopwatch.start("FX_SESSION_RESTORE_COLLECT_SINGLE_WINDOW_DATA_MS"); |
|
2167 |
|
2168 let tabbrowser = aWindow.gBrowser; |
|
2169 let tabs = tabbrowser.tabs; |
|
2170 let winData = this._windows[aWindow.__SSi]; |
|
2171 let tabsData = winData.tabs = []; |
|
2172 |
|
2173 // update the internal state data for this window |
|
2174 for (let tab of tabs) { |
|
2175 tabsData.push(TabState.collect(tab)); |
|
2176 } |
|
2177 winData.selected = tabbrowser.mTabBox.selectedIndex + 1; |
|
2178 |
|
2179 this._updateWindowFeatures(aWindow); |
|
2180 |
|
2181 // Make sure we keep __SS_lastSessionWindowID around for cases like entering |
|
2182 // or leaving PB mode. |
|
2183 if (aWindow.__SS_lastSessionWindowID) |
|
2184 this._windows[aWindow.__SSi].__lastSessionWindowID = |
|
2185 aWindow.__SS_lastSessionWindowID; |
|
2186 |
|
2187 DirtyWindows.remove(aWindow); |
|
2188 TelemetryStopwatch.finish("FX_SESSION_RESTORE_COLLECT_SINGLE_WINDOW_DATA_MS"); |
|
2189 }, |
|
2190 |
|
2191 /* ........ Restoring Functionality .............. */ |
|
2192 |
|
2193 /** |
|
2194 * restore features to a single window |
|
2195 * @param aWindow |
|
2196 * Window reference |
|
2197 * @param aState |
|
2198 * JS object or its eval'able source |
|
2199 * @param aOptions |
|
2200 * {overwriteTabs: true} to overwrite existing tabs w/ new ones |
|
2201 * {isFollowUp: true} if this is not the restoration of the 1st window |
|
2202 * {firstWindow: true} if this is the first non-private window we're |
|
2203 * restoring in this session, that might open an |
|
2204 * external link as well |
|
2205 */ |
|
2206 restoreWindow: function ssi_restoreWindow(aWindow, aState, aOptions = {}) { |
|
2207 let overwriteTabs = aOptions && aOptions.overwriteTabs; |
|
2208 let isFollowUp = aOptions && aOptions.isFollowUp; |
|
2209 let firstWindow = aOptions && aOptions.firstWindow; |
|
2210 |
|
2211 if (isFollowUp) { |
|
2212 this.windowToFocus = aWindow; |
|
2213 } |
|
2214 // initialize window if necessary |
|
2215 if (aWindow && (!aWindow.__SSi || !this._windows[aWindow.__SSi])) |
|
2216 this.onLoad(aWindow); |
|
2217 |
|
2218 try { |
|
2219 var root = typeof aState == "string" ? JSON.parse(aState) : aState; |
|
2220 if (!root.windows[0]) { |
|
2221 this._sendRestoreCompletedNotifications(); |
|
2222 return; // nothing to restore |
|
2223 } |
|
2224 } |
|
2225 catch (ex) { // invalid state object - don't restore anything |
|
2226 debug(ex); |
|
2227 this._sendRestoreCompletedNotifications(); |
|
2228 return; |
|
2229 } |
|
2230 |
|
2231 TelemetryStopwatch.start("FX_SESSION_RESTORE_RESTORE_WINDOW_MS"); |
|
2232 |
|
2233 // We're not returning from this before we end up calling restoreTabs |
|
2234 // for this window, so make sure we send the SSWindowStateBusy event. |
|
2235 this._setWindowStateBusy(aWindow); |
|
2236 |
|
2237 if (root._closedWindows) |
|
2238 this._closedWindows = root._closedWindows; |
|
2239 |
|
2240 var winData; |
|
2241 if (!root.selectedWindow || root.selectedWindow > root.windows.length) { |
|
2242 root.selectedWindow = 0; |
|
2243 } |
|
2244 |
|
2245 // open new windows for all further window entries of a multi-window session |
|
2246 // (unless they don't contain any tab data) |
|
2247 for (var w = 1; w < root.windows.length; w++) { |
|
2248 winData = root.windows[w]; |
|
2249 if (winData && winData.tabs && winData.tabs[0]) { |
|
2250 var window = this._openWindowWithState({ windows: [winData] }); |
|
2251 if (w == root.selectedWindow - 1) { |
|
2252 this.windowToFocus = window; |
|
2253 } |
|
2254 } |
|
2255 } |
|
2256 winData = root.windows[0]; |
|
2257 if (!winData.tabs) { |
|
2258 winData.tabs = []; |
|
2259 } |
|
2260 // don't restore a single blank tab when we've had an external |
|
2261 // URL passed in for loading at startup (cf. bug 357419) |
|
2262 else if (firstWindow && !overwriteTabs && winData.tabs.length == 1 && |
|
2263 (!winData.tabs[0].entries || winData.tabs[0].entries.length == 0)) { |
|
2264 winData.tabs = []; |
|
2265 } |
|
2266 |
|
2267 var tabbrowser = aWindow.gBrowser; |
|
2268 var openTabCount = overwriteTabs ? tabbrowser.browsers.length : -1; |
|
2269 var newTabCount = winData.tabs.length; |
|
2270 var tabs = []; |
|
2271 |
|
2272 // disable smooth scrolling while adding, moving, removing and selecting tabs |
|
2273 var tabstrip = tabbrowser.tabContainer.mTabstrip; |
|
2274 var smoothScroll = tabstrip.smoothScroll; |
|
2275 tabstrip.smoothScroll = false; |
|
2276 |
|
2277 // unpin all tabs to ensure they are not reordered in the next loop |
|
2278 if (overwriteTabs) { |
|
2279 for (let t = tabbrowser._numPinnedTabs - 1; t > -1; t--) |
|
2280 tabbrowser.unpinTab(tabbrowser.tabs[t]); |
|
2281 } |
|
2282 |
|
2283 // We need to keep track of the initially open tabs so that they |
|
2284 // can be moved to the end of the restored tabs. |
|
2285 let initialTabs = []; |
|
2286 if (!overwriteTabs && firstWindow) { |
|
2287 initialTabs = Array.slice(tabbrowser.tabs); |
|
2288 } |
|
2289 |
|
2290 // make sure that the selected tab won't be closed in order to |
|
2291 // prevent unnecessary flickering |
|
2292 if (overwriteTabs && tabbrowser.selectedTab._tPos >= newTabCount) |
|
2293 tabbrowser.moveTabTo(tabbrowser.selectedTab, newTabCount - 1); |
|
2294 |
|
2295 let numVisibleTabs = 0; |
|
2296 |
|
2297 for (var t = 0; t < newTabCount; t++) { |
|
2298 tabs.push(t < openTabCount ? |
|
2299 tabbrowser.tabs[t] : |
|
2300 tabbrowser.addTab("about:blank", {skipAnimation: true})); |
|
2301 |
|
2302 if (winData.tabs[t].pinned) |
|
2303 tabbrowser.pinTab(tabs[t]); |
|
2304 |
|
2305 if (winData.tabs[t].hidden) { |
|
2306 tabbrowser.hideTab(tabs[t]); |
|
2307 } |
|
2308 else { |
|
2309 tabbrowser.showTab(tabs[t]); |
|
2310 numVisibleTabs++; |
|
2311 } |
|
2312 } |
|
2313 |
|
2314 if (!overwriteTabs && firstWindow) { |
|
2315 // Move the originally open tabs to the end |
|
2316 let endPosition = tabbrowser.tabs.length - 1; |
|
2317 for (let i = 0; i < initialTabs.length; i++) { |
|
2318 tabbrowser.moveTabTo(initialTabs[i], endPosition); |
|
2319 } |
|
2320 } |
|
2321 |
|
2322 // if all tabs to be restored are hidden, make the first one visible |
|
2323 if (!numVisibleTabs && winData.tabs.length) { |
|
2324 winData.tabs[0].hidden = false; |
|
2325 tabbrowser.showTab(tabs[0]); |
|
2326 } |
|
2327 |
|
2328 // If overwriting tabs, we want to reset each tab's "restoring" state. Since |
|
2329 // we're overwriting those tabs, they should no longer be restoring. The |
|
2330 // tabs will be rebuilt and marked if they need to be restored after loading |
|
2331 // state (in restoreTabs). |
|
2332 if (overwriteTabs) { |
|
2333 for (let i = 0; i < tabbrowser.tabs.length; i++) { |
|
2334 let tab = tabbrowser.tabs[i]; |
|
2335 if (tabbrowser.browsers[i].__SS_restoreState) |
|
2336 this._resetTabRestoringState(tab); |
|
2337 } |
|
2338 } |
|
2339 |
|
2340 // We want to correlate the window with data from the last session, so |
|
2341 // assign another id if we have one. Otherwise clear so we don't do |
|
2342 // anything with it. |
|
2343 delete aWindow.__SS_lastSessionWindowID; |
|
2344 if (winData.__lastSessionWindowID) |
|
2345 aWindow.__SS_lastSessionWindowID = winData.__lastSessionWindowID; |
|
2346 |
|
2347 // when overwriting tabs, remove all superflous ones |
|
2348 if (overwriteTabs && newTabCount < openTabCount) { |
|
2349 Array.slice(tabbrowser.tabs, newTabCount, openTabCount) |
|
2350 .forEach(tabbrowser.removeTab, tabbrowser); |
|
2351 } |
|
2352 |
|
2353 if (overwriteTabs) { |
|
2354 this.restoreWindowFeatures(aWindow, winData); |
|
2355 delete this._windows[aWindow.__SSi].extData; |
|
2356 } |
|
2357 if (winData.cookies) { |
|
2358 this.restoreCookies(winData.cookies); |
|
2359 } |
|
2360 if (winData.extData) { |
|
2361 if (!this._windows[aWindow.__SSi].extData) { |
|
2362 this._windows[aWindow.__SSi].extData = {}; |
|
2363 } |
|
2364 for (var key in winData.extData) { |
|
2365 this._windows[aWindow.__SSi].extData[key] = winData.extData[key]; |
|
2366 } |
|
2367 } |
|
2368 |
|
2369 let newClosedTabsData = winData._closedTabs || []; |
|
2370 |
|
2371 if (overwriteTabs || firstWindow) { |
|
2372 // Overwrite existing closed tabs data when overwriteTabs=true |
|
2373 // or we're the first window to be restored. |
|
2374 this._windows[aWindow.__SSi]._closedTabs = newClosedTabsData; |
|
2375 } else if (this._max_tabs_undo > 0) { |
|
2376 // If we merge tabs, we also want to merge closed tabs data. We'll assume |
|
2377 // the restored tabs were closed more recently and append the current list |
|
2378 // of closed tabs to the new one... |
|
2379 newClosedTabsData = |
|
2380 newClosedTabsData.concat(this._windows[aWindow.__SSi]._closedTabs); |
|
2381 |
|
2382 // ... and make sure that we don't exceed the max number of closed tabs |
|
2383 // we can restore. |
|
2384 this._windows[aWindow.__SSi]._closedTabs = |
|
2385 newClosedTabsData.slice(0, this._max_tabs_undo); |
|
2386 } |
|
2387 |
|
2388 this.restoreTabs(aWindow, tabs, winData.tabs, |
|
2389 (overwriteTabs ? (parseInt(winData.selected || "1")) : 0)); |
|
2390 |
|
2391 if (aState.scratchpads) { |
|
2392 ScratchpadManager.restoreSession(aState.scratchpads); |
|
2393 } |
|
2394 |
|
2395 // set smoothScroll back to the original value |
|
2396 tabstrip.smoothScroll = smoothScroll; |
|
2397 |
|
2398 TelemetryStopwatch.finish("FX_SESSION_RESTORE_RESTORE_WINDOW_MS"); |
|
2399 |
|
2400 this._sendRestoreCompletedNotifications(); |
|
2401 }, |
|
2402 |
|
2403 /** |
|
2404 * Sets the tabs restoring order with the following priority: |
|
2405 * Selected tab, pinned tabs, optimized visible tabs, other visible tabs and |
|
2406 * hidden tabs. |
|
2407 * @param aTabBrowser |
|
2408 * Tab browser object |
|
2409 * @param aTabs |
|
2410 * Array of tab references |
|
2411 * @param aTabData |
|
2412 * Array of tab data |
|
2413 * @param aSelectedTab |
|
2414 * Index of selected tab (1 is first tab, 0 no selected tab) |
|
2415 */ |
|
2416 _setTabsRestoringOrder : function ssi__setTabsRestoringOrder( |
|
2417 aTabBrowser, aTabs, aTabData, aSelectedTab) { |
|
2418 |
|
2419 // Store the selected tab. Need to substract one to get the index in aTabs. |
|
2420 let selectedTab; |
|
2421 if (aSelectedTab > 0 && aTabs[aSelectedTab - 1]) { |
|
2422 selectedTab = aTabs[aSelectedTab - 1]; |
|
2423 } |
|
2424 |
|
2425 // Store the pinned tabs and hidden tabs. |
|
2426 let pinnedTabs = []; |
|
2427 let pinnedTabsData = []; |
|
2428 let hiddenTabs = []; |
|
2429 let hiddenTabsData = []; |
|
2430 if (aTabs.length > 1) { |
|
2431 for (let t = aTabs.length - 1; t >= 0; t--) { |
|
2432 if (aTabData[t].pinned) { |
|
2433 pinnedTabs.unshift(aTabs.splice(t, 1)[0]); |
|
2434 pinnedTabsData.unshift(aTabData.splice(t, 1)[0]); |
|
2435 } else if (aTabData[t].hidden) { |
|
2436 hiddenTabs.unshift(aTabs.splice(t, 1)[0]); |
|
2437 hiddenTabsData.unshift(aTabData.splice(t, 1)[0]); |
|
2438 } |
|
2439 } |
|
2440 } |
|
2441 |
|
2442 // Optimize the visible tabs only if there is a selected tab. |
|
2443 if (selectedTab) { |
|
2444 let selectedTabIndex = aTabs.indexOf(selectedTab); |
|
2445 if (selectedTabIndex > 0) { |
|
2446 let scrollSize = aTabBrowser.tabContainer.mTabstrip.scrollClientSize; |
|
2447 let tabWidth = aTabs[0].getBoundingClientRect().width; |
|
2448 let maxVisibleTabs = Math.ceil(scrollSize / tabWidth); |
|
2449 if (maxVisibleTabs < aTabs.length) { |
|
2450 let firstVisibleTab = 0; |
|
2451 let nonVisibleTabsCount = aTabs.length - maxVisibleTabs; |
|
2452 if (nonVisibleTabsCount >= selectedTabIndex) { |
|
2453 // Selected tab is leftmost since we scroll to it when possible. |
|
2454 firstVisibleTab = selectedTabIndex; |
|
2455 } else { |
|
2456 // Selected tab is rightmost or no more room to scroll right. |
|
2457 firstVisibleTab = nonVisibleTabsCount; |
|
2458 } |
|
2459 aTabs = aTabs.splice(firstVisibleTab, maxVisibleTabs).concat(aTabs); |
|
2460 aTabData = |
|
2461 aTabData.splice(firstVisibleTab, maxVisibleTabs).concat(aTabData); |
|
2462 } |
|
2463 } |
|
2464 } |
|
2465 |
|
2466 // Merge the stored tabs in order. |
|
2467 aTabs = pinnedTabs.concat(aTabs, hiddenTabs); |
|
2468 aTabData = pinnedTabsData.concat(aTabData, hiddenTabsData); |
|
2469 |
|
2470 // Load the selected tab to the first position and select it. |
|
2471 if (selectedTab) { |
|
2472 let selectedTabIndex = aTabs.indexOf(selectedTab); |
|
2473 if (selectedTabIndex > 0) { |
|
2474 aTabs = aTabs.splice(selectedTabIndex, 1).concat(aTabs); |
|
2475 aTabData = aTabData.splice(selectedTabIndex, 1).concat(aTabData); |
|
2476 } |
|
2477 aTabBrowser.selectedTab = selectedTab; |
|
2478 } |
|
2479 |
|
2480 return [aTabs, aTabData]; |
|
2481 }, |
|
2482 |
|
2483 /** |
|
2484 * Manage history restoration for a window |
|
2485 * @param aWindow |
|
2486 * Window to restore the tabs into |
|
2487 * @param aTabs |
|
2488 * Array of tab references |
|
2489 * @param aTabData |
|
2490 * Array of tab data |
|
2491 * @param aSelectTab |
|
2492 * Index of selected tab |
|
2493 * @param aRestoreImmediately |
|
2494 * Flag to indicate whether the given set of tabs aTabs should be |
|
2495 * restored/loaded immediately even if restore_on_demand = true |
|
2496 */ |
|
2497 restoreTabs: function (aWindow, aTabs, aTabData, aSelectTab, |
|
2498 aRestoreImmediately = false) |
|
2499 { |
|
2500 |
|
2501 var tabbrowser = aWindow.gBrowser; |
|
2502 |
|
2503 if (!this._isWindowLoaded(aWindow)) { |
|
2504 // from now on, the data will come from the actual window |
|
2505 delete this._statesToRestore[aWindow.__SS_restoreID]; |
|
2506 delete aWindow.__SS_restoreID; |
|
2507 delete this._windows[aWindow.__SSi]._restoring; |
|
2508 } |
|
2509 |
|
2510 // It's important to set the window state to dirty so that |
|
2511 // we collect their data for the first time when saving state. |
|
2512 DirtyWindows.add(aWindow); |
|
2513 |
|
2514 // Set the state to restore as the window's current state. Normally, this |
|
2515 // will just be overridden the next time we collect state but we need this |
|
2516 // as a fallback should Firefox be shutdown early without notifying us |
|
2517 // beforehand. |
|
2518 this._windows[aWindow.__SSi].tabs = aTabData.slice(); |
|
2519 this._windows[aWindow.__SSi].selected = aSelectTab; |
|
2520 |
|
2521 if (aTabs.length == 0) { |
|
2522 // This is normally done later, but as we're returning early |
|
2523 // here we need to take care of it. |
|
2524 this._setWindowStateReady(aWindow); |
|
2525 return; |
|
2526 } |
|
2527 |
|
2528 // Sets the tabs restoring order. |
|
2529 [aTabs, aTabData] = |
|
2530 this._setTabsRestoringOrder(tabbrowser, aTabs, aTabData, aSelectTab); |
|
2531 |
|
2532 // Prepare the tabs so that they can be properly restored. We'll pin/unpin |
|
2533 // and show/hide tabs as necessary. We'll also set the labels, user typed |
|
2534 // value, and attach a copy of the tab's data in case we close it before |
|
2535 // it's been restored. |
|
2536 for (let t = 0; t < aTabs.length; t++) { |
|
2537 let tab = aTabs[t]; |
|
2538 let browser = tabbrowser.getBrowserForTab(tab); |
|
2539 let tabData = aTabData[t]; |
|
2540 |
|
2541 if (tabData.pinned) |
|
2542 tabbrowser.pinTab(tab); |
|
2543 else |
|
2544 tabbrowser.unpinTab(tab); |
|
2545 |
|
2546 if (tabData.hidden) |
|
2547 tabbrowser.hideTab(tab); |
|
2548 else |
|
2549 tabbrowser.showTab(tab); |
|
2550 |
|
2551 if (tabData.lastAccessed) { |
|
2552 tab.lastAccessed = tabData.lastAccessed; |
|
2553 } |
|
2554 |
|
2555 if ("attributes" in tabData) { |
|
2556 // Ensure that we persist tab attributes restored from previous sessions. |
|
2557 Object.keys(tabData.attributes).forEach(a => TabAttributes.persist(a)); |
|
2558 } |
|
2559 |
|
2560 if (!tabData.entries) { |
|
2561 tabData.entries = []; |
|
2562 } |
|
2563 if (tabData.extData) { |
|
2564 tab.__SS_extdata = {}; |
|
2565 for (let key in tabData.extData) |
|
2566 tab.__SS_extdata[key] = tabData.extData[key]; |
|
2567 } else { |
|
2568 delete tab.__SS_extdata; |
|
2569 } |
|
2570 |
|
2571 // Flush all data from the content script synchronously. This is done so |
|
2572 // that all async messages that are still on their way to chrome will |
|
2573 // be ignored and don't override any tab data set when restoring. |
|
2574 TabState.flush(tab.linkedBrowser); |
|
2575 |
|
2576 // Ensure the index is in bounds. |
|
2577 let activeIndex = (tabData.index || tabData.entries.length) - 1; |
|
2578 activeIndex = Math.min(activeIndex, tabData.entries.length - 1); |
|
2579 activeIndex = Math.max(activeIndex, 0); |
|
2580 |
|
2581 // Save the index in case we updated it above. |
|
2582 tabData.index = activeIndex + 1; |
|
2583 |
|
2584 // In electrolysis, we may need to change the browser's remote |
|
2585 // attribute so that it runs in a content process. |
|
2586 let activePageData = tabData.entries[activeIndex] || null; |
|
2587 let uri = activePageData ? activePageData.url || null : null; |
|
2588 tabbrowser.updateBrowserRemoteness(browser, uri); |
|
2589 |
|
2590 // Start a new epoch and include the epoch in the restoreHistory |
|
2591 // message. If a message is received that relates to a previous epoch, we |
|
2592 // discard it. |
|
2593 let epoch = this._nextRestoreEpoch++; |
|
2594 this._browserEpochs.set(browser.permanentKey, epoch); |
|
2595 |
|
2596 // keep the data around to prevent dataloss in case |
|
2597 // a tab gets closed before it's been properly restored |
|
2598 browser.__SS_data = tabData; |
|
2599 browser.__SS_restoreState = TAB_STATE_NEEDS_RESTORE; |
|
2600 browser.setAttribute("pending", "true"); |
|
2601 tab.setAttribute("pending", "true"); |
|
2602 |
|
2603 // Update the persistent tab state cache with |tabData| information. |
|
2604 TabStateCache.update(browser, { |
|
2605 history: {entries: tabData.entries, index: tabData.index}, |
|
2606 scroll: tabData.scroll || null, |
|
2607 storage: tabData.storage || null, |
|
2608 formdata: tabData.formdata || null, |
|
2609 disallow: tabData.disallow || null, |
|
2610 pageStyle: tabData.pageStyle || null |
|
2611 }); |
|
2612 |
|
2613 browser.messageManager.sendAsyncMessage("SessionStore:restoreHistory", |
|
2614 {tabData: tabData, epoch: epoch}); |
|
2615 |
|
2616 // Restore tab attributes. |
|
2617 if ("attributes" in tabData) { |
|
2618 TabAttributes.set(tab, tabData.attributes); |
|
2619 } |
|
2620 |
|
2621 // This could cause us to ignore MAX_CONCURRENT_TAB_RESTORES a bit, but |
|
2622 // it ensures each window will have its selected tab loaded. |
|
2623 if (aRestoreImmediately || tabbrowser.selectedBrowser == browser) { |
|
2624 this.restoreTabContent(tab); |
|
2625 } else { |
|
2626 TabRestoreQueue.add(tab); |
|
2627 this.restoreNextTab(); |
|
2628 } |
|
2629 } |
|
2630 |
|
2631 this._setWindowStateReady(aWindow); |
|
2632 }, |
|
2633 |
|
2634 /** |
|
2635 * Restores the specified tab. If the tab can't be restored (eg, no history or |
|
2636 * calling gotoIndex fails), then state changes will be rolled back. |
|
2637 * This method will check if gTabsProgressListener is attached to the tab's |
|
2638 * window, ensuring that we don't get caught without one. |
|
2639 * This method removes the session history listener right before starting to |
|
2640 * attempt a load. This will prevent cases of "stuck" listeners. |
|
2641 * If this method returns false, then it is up to the caller to decide what to |
|
2642 * do. In the common case (restoreNextTab), we will want to then attempt to |
|
2643 * restore the next tab. In the other case (selecting the tab, reloading the |
|
2644 * tab), the caller doesn't actually want to do anything if no page is loaded. |
|
2645 * |
|
2646 * @param aTab |
|
2647 * the tab to restore |
|
2648 * |
|
2649 * @returns true/false indicating whether or not a load actually happened |
|
2650 */ |
|
2651 restoreTabContent: function (aTab) { |
|
2652 let window = aTab.ownerDocument.defaultView; |
|
2653 let browser = aTab.linkedBrowser; |
|
2654 let tabData = browser.__SS_data; |
|
2655 |
|
2656 // Make sure that this tab is removed from the priority queue. |
|
2657 TabRestoreQueue.remove(aTab); |
|
2658 |
|
2659 // Increase our internal count. |
|
2660 this._tabsRestoringCount++; |
|
2661 |
|
2662 // Set this tab's state to restoring |
|
2663 browser.__SS_restoreState = TAB_STATE_RESTORING; |
|
2664 browser.removeAttribute("pending"); |
|
2665 aTab.removeAttribute("pending"); |
|
2666 |
|
2667 let activeIndex = tabData.index - 1; |
|
2668 |
|
2669 // Attach data that will be restored on "load" event, after tab is restored. |
|
2670 if (tabData.entries.length) { |
|
2671 // restore those aspects of the currently active documents which are not |
|
2672 // preserved in the plain history entries (mainly scroll state and text data) |
|
2673 browser.__SS_restore_data = tabData.entries[activeIndex] || {}; |
|
2674 } else { |
|
2675 browser.__SS_restore_data = {}; |
|
2676 } |
|
2677 |
|
2678 browser.__SS_restore_tab = aTab; |
|
2679 |
|
2680 browser.messageManager.sendAsyncMessage("SessionStore:restoreTabContent"); |
|
2681 }, |
|
2682 |
|
2683 /** |
|
2684 * This _attempts_ to restore the next available tab. If the restore fails, |
|
2685 * then we will attempt the next one. |
|
2686 * There are conditions where this won't do anything: |
|
2687 * if we're in the process of quitting |
|
2688 * if there are no tabs to restore |
|
2689 * if we have already reached the limit for number of tabs to restore |
|
2690 */ |
|
2691 restoreNextTab: function ssi_restoreNextTab() { |
|
2692 // If we call in here while quitting, we don't actually want to do anything |
|
2693 if (this._loadState == STATE_QUITTING) |
|
2694 return; |
|
2695 |
|
2696 // Don't exceed the maximum number of concurrent tab restores. |
|
2697 if (this._tabsRestoringCount >= MAX_CONCURRENT_TAB_RESTORES) |
|
2698 return; |
|
2699 |
|
2700 let tab = TabRestoreQueue.shift(); |
|
2701 if (tab) { |
|
2702 this.restoreTabContent(tab); |
|
2703 } |
|
2704 }, |
|
2705 |
|
2706 /** |
|
2707 * Restore visibility and dimension features to a window |
|
2708 * @param aWindow |
|
2709 * Window reference |
|
2710 * @param aWinData |
|
2711 * Object containing session data for the window |
|
2712 */ |
|
2713 restoreWindowFeatures: function ssi_restoreWindowFeatures(aWindow, aWinData) { |
|
2714 var hidden = (aWinData.hidden)?aWinData.hidden.split(","):[]; |
|
2715 WINDOW_HIDEABLE_FEATURES.forEach(function(aItem) { |
|
2716 aWindow[aItem].visible = hidden.indexOf(aItem) == -1; |
|
2717 }); |
|
2718 |
|
2719 if (aWinData.isPopup) { |
|
2720 this._windows[aWindow.__SSi].isPopup = true; |
|
2721 if (aWindow.gURLBar) { |
|
2722 aWindow.gURLBar.readOnly = true; |
|
2723 aWindow.gURLBar.setAttribute("enablehistory", "false"); |
|
2724 } |
|
2725 } |
|
2726 else { |
|
2727 delete this._windows[aWindow.__SSi].isPopup; |
|
2728 if (aWindow.gURLBar) { |
|
2729 aWindow.gURLBar.readOnly = false; |
|
2730 aWindow.gURLBar.setAttribute("enablehistory", "true"); |
|
2731 } |
|
2732 } |
|
2733 |
|
2734 var _this = this; |
|
2735 aWindow.setTimeout(function() { |
|
2736 _this.restoreDimensions.apply(_this, [aWindow, |
|
2737 +aWinData.width || 0, |
|
2738 +aWinData.height || 0, |
|
2739 "screenX" in aWinData ? +aWinData.screenX : NaN, |
|
2740 "screenY" in aWinData ? +aWinData.screenY : NaN, |
|
2741 aWinData.sizemode || "", aWinData.sidebar || ""]); |
|
2742 }, 0); |
|
2743 }, |
|
2744 |
|
2745 /** |
|
2746 * Restore a window's dimensions |
|
2747 * @param aWidth |
|
2748 * Window width |
|
2749 * @param aHeight |
|
2750 * Window height |
|
2751 * @param aLeft |
|
2752 * Window left |
|
2753 * @param aTop |
|
2754 * Window top |
|
2755 * @param aSizeMode |
|
2756 * Window size mode (eg: maximized) |
|
2757 * @param aSidebar |
|
2758 * Sidebar command |
|
2759 */ |
|
2760 restoreDimensions: function ssi_restoreDimensions(aWindow, aWidth, aHeight, aLeft, aTop, aSizeMode, aSidebar) { |
|
2761 var win = aWindow; |
|
2762 var _this = this; |
|
2763 function win_(aName) { return _this._getWindowDimension(win, aName); } |
|
2764 |
|
2765 // find available space on the screen where this window is being placed |
|
2766 let screen = gScreenManager.screenForRect(aLeft, aTop, aWidth, aHeight); |
|
2767 if (screen) { |
|
2768 let screenLeft = {}, screenTop = {}, screenWidth = {}, screenHeight = {}; |
|
2769 screen.GetAvailRectDisplayPix(screenLeft, screenTop, screenWidth, screenHeight); |
|
2770 // constrain the dimensions to the actual space available |
|
2771 if (aWidth > screenWidth.value) { |
|
2772 aWidth = screenWidth.value; |
|
2773 } |
|
2774 if (aHeight > screenHeight.value) { |
|
2775 aHeight = screenHeight.value; |
|
2776 } |
|
2777 // and then pull the window within the screen's bounds |
|
2778 if (aLeft < screenLeft.value) { |
|
2779 aLeft = screenLeft.value; |
|
2780 } else if (aLeft + aWidth > screenLeft.value + screenWidth.value) { |
|
2781 aLeft = screenLeft.value + screenWidth.value - aWidth; |
|
2782 } |
|
2783 if (aTop < screenTop.value) { |
|
2784 aTop = screenTop.value; |
|
2785 } else if (aTop + aHeight > screenTop.value + screenHeight.value) { |
|
2786 aTop = screenTop.value + screenHeight.value - aHeight; |
|
2787 } |
|
2788 } |
|
2789 |
|
2790 // only modify those aspects which aren't correct yet |
|
2791 if (aWidth && aHeight && (aWidth != win_("width") || aHeight != win_("height"))) { |
|
2792 // Don't resize the window if it's currently maximized and we would |
|
2793 // maximize it again shortly after. |
|
2794 if (aSizeMode != "maximized" || win_("sizemode") != "maximized") { |
|
2795 aWindow.resizeTo(aWidth, aHeight); |
|
2796 } |
|
2797 } |
|
2798 if (!isNaN(aLeft) && !isNaN(aTop) && (aLeft != win_("screenX") || aTop != win_("screenY"))) { |
|
2799 aWindow.moveTo(aLeft, aTop); |
|
2800 } |
|
2801 if (aSizeMode && win_("sizemode") != aSizeMode) |
|
2802 { |
|
2803 switch (aSizeMode) |
|
2804 { |
|
2805 case "maximized": |
|
2806 aWindow.maximize(); |
|
2807 break; |
|
2808 case "minimized": |
|
2809 aWindow.minimize(); |
|
2810 break; |
|
2811 case "normal": |
|
2812 aWindow.restore(); |
|
2813 break; |
|
2814 } |
|
2815 } |
|
2816 var sidebar = aWindow.document.getElementById("sidebar-box"); |
|
2817 if (sidebar.getAttribute("sidebarcommand") != aSidebar) { |
|
2818 aWindow.toggleSidebar(aSidebar); |
|
2819 } |
|
2820 // since resizing/moving a window brings it to the foreground, |
|
2821 // we might want to re-focus the last focused window |
|
2822 if (this.windowToFocus) { |
|
2823 this.windowToFocus.focus(); |
|
2824 } |
|
2825 }, |
|
2826 |
|
2827 /** |
|
2828 * Restores cookies |
|
2829 * @param aCookies |
|
2830 * Array of cookie objects |
|
2831 */ |
|
2832 restoreCookies: function ssi_restoreCookies(aCookies) { |
|
2833 // MAX_EXPIRY should be 2^63-1, but JavaScript can't handle that precision |
|
2834 var MAX_EXPIRY = Math.pow(2, 62); |
|
2835 for (let i = 0; i < aCookies.length; i++) { |
|
2836 var cookie = aCookies[i]; |
|
2837 try { |
|
2838 Services.cookies.add(cookie.host, cookie.path || "", cookie.name || "", |
|
2839 cookie.value, !!cookie.secure, !!cookie.httponly, true, |
|
2840 "expiry" in cookie ? cookie.expiry : MAX_EXPIRY); |
|
2841 } |
|
2842 catch (ex) { console.error(ex); } // don't let a single cookie stop recovering |
|
2843 } |
|
2844 }, |
|
2845 |
|
2846 /* ........ Disk Access .............. */ |
|
2847 |
|
2848 /** |
|
2849 * Save the current session state to disk, after a delay. |
|
2850 * |
|
2851 * @param aWindow (optional) |
|
2852 * Will mark the given window as dirty so that we will recollect its |
|
2853 * data before we start writing. |
|
2854 */ |
|
2855 saveStateDelayed: function (aWindow = null) { |
|
2856 if (aWindow) { |
|
2857 DirtyWindows.add(aWindow); |
|
2858 } |
|
2859 |
|
2860 SessionSaver.runDelayed(); |
|
2861 }, |
|
2862 |
|
2863 /* ........ Auxiliary Functions .............. */ |
|
2864 |
|
2865 /** |
|
2866 * Update the session start time and send a telemetry measurement |
|
2867 * for the number of days elapsed since the session was started. |
|
2868 * |
|
2869 * @param state |
|
2870 * The session state. |
|
2871 */ |
|
2872 _updateSessionStartTime: function ssi_updateSessionStartTime(state) { |
|
2873 // Attempt to load the session start time from the session state |
|
2874 if (state.session && state.session.startTime) { |
|
2875 this._sessionStartTime = state.session.startTime; |
|
2876 |
|
2877 // ms to days |
|
2878 let sessionLength = (Date.now() - this._sessionStartTime) / MS_PER_DAY; |
|
2879 |
|
2880 if (sessionLength > 0) { |
|
2881 // Submit the session length telemetry measurement |
|
2882 Services.telemetry.getHistogramById("FX_SESSION_RESTORE_SESSION_LENGTH").add(sessionLength); |
|
2883 } |
|
2884 } |
|
2885 }, |
|
2886 |
|
2887 /** |
|
2888 * call a callback for all currently opened browser windows |
|
2889 * (might miss the most recent one) |
|
2890 * @param aFunc |
|
2891 * Callback each window is passed to |
|
2892 */ |
|
2893 _forEachBrowserWindow: function ssi_forEachBrowserWindow(aFunc) { |
|
2894 var windowsEnum = Services.wm.getEnumerator("navigator:browser"); |
|
2895 |
|
2896 while (windowsEnum.hasMoreElements()) { |
|
2897 var window = windowsEnum.getNext(); |
|
2898 if (window.__SSi && !window.closed) { |
|
2899 aFunc.call(this, window); |
|
2900 } |
|
2901 } |
|
2902 }, |
|
2903 |
|
2904 /** |
|
2905 * Returns most recent window |
|
2906 * @returns Window reference |
|
2907 */ |
|
2908 _getMostRecentBrowserWindow: function ssi_getMostRecentBrowserWindow() { |
|
2909 return RecentWindow.getMostRecentBrowserWindow({ allowPopups: true }); |
|
2910 }, |
|
2911 |
|
2912 /** |
|
2913 * Calls onClose for windows that are determined to be closed but aren't |
|
2914 * destroyed yet, which would otherwise cause getBrowserState and |
|
2915 * setBrowserState to treat them as open windows. |
|
2916 */ |
|
2917 _handleClosedWindows: function ssi_handleClosedWindows() { |
|
2918 var windowsEnum = Services.wm.getEnumerator("navigator:browser"); |
|
2919 |
|
2920 while (windowsEnum.hasMoreElements()) { |
|
2921 var window = windowsEnum.getNext(); |
|
2922 if (window.closed) { |
|
2923 this.onClose(window); |
|
2924 } |
|
2925 } |
|
2926 }, |
|
2927 |
|
2928 /** |
|
2929 * open a new browser window for a given session state |
|
2930 * called when restoring a multi-window session |
|
2931 * @param aState |
|
2932 * Object containing session data |
|
2933 */ |
|
2934 _openWindowWithState: function ssi_openWindowWithState(aState) { |
|
2935 var argString = Cc["@mozilla.org/supports-string;1"]. |
|
2936 createInstance(Ci.nsISupportsString); |
|
2937 argString.data = ""; |
|
2938 |
|
2939 // Build feature string |
|
2940 let features = "chrome,dialog=no,macsuppressanimation,all"; |
|
2941 let winState = aState.windows[0]; |
|
2942 WINDOW_ATTRIBUTES.forEach(function(aFeature) { |
|
2943 // Use !isNaN as an easy way to ignore sizemode and check for numbers |
|
2944 if (aFeature in winState && !isNaN(winState[aFeature])) |
|
2945 features += "," + aFeature + "=" + winState[aFeature]; |
|
2946 }); |
|
2947 |
|
2948 if (winState.isPrivate) { |
|
2949 features += ",private"; |
|
2950 } |
|
2951 |
|
2952 var window = |
|
2953 Services.ww.openWindow(null, this._prefBranch.getCharPref("chromeURL"), |
|
2954 "_blank", features, argString); |
|
2955 |
|
2956 do { |
|
2957 var ID = "window" + Math.random(); |
|
2958 } while (ID in this._statesToRestore); |
|
2959 this._statesToRestore[(window.__SS_restoreID = ID)] = aState; |
|
2960 |
|
2961 return window; |
|
2962 }, |
|
2963 |
|
2964 /** |
|
2965 * Gets the tab for the given browser. This should be marginally better |
|
2966 * than using tabbrowser's getTabForContentWindow. This assumes the browser |
|
2967 * is the linkedBrowser of a tab, not a dangling browser. |
|
2968 * |
|
2969 * @param aBrowser |
|
2970 * The browser from which to get the tab. |
|
2971 */ |
|
2972 _getTabForBrowser: function ssi_getTabForBrowser(aBrowser) { |
|
2973 let window = aBrowser.ownerDocument.defaultView; |
|
2974 for (let i = 0; i < window.gBrowser.tabs.length; i++) { |
|
2975 let tab = window.gBrowser.tabs[i]; |
|
2976 if (tab.linkedBrowser == aBrowser) |
|
2977 return tab; |
|
2978 } |
|
2979 return undefined; |
|
2980 }, |
|
2981 |
|
2982 /** |
|
2983 * Whether or not to resume session, if not recovering from a crash. |
|
2984 * @returns bool |
|
2985 */ |
|
2986 _doResumeSession: function ssi_doResumeSession() { |
|
2987 return this._prefBranch.getIntPref("startup.page") == 3 || |
|
2988 this._prefBranch.getBoolPref("sessionstore.resume_session_once"); |
|
2989 }, |
|
2990 |
|
2991 /** |
|
2992 * whether the user wants to load any other page at startup |
|
2993 * (except the homepage) - needed for determining whether to overwrite the current tabs |
|
2994 * C.f.: nsBrowserContentHandler's defaultArgs implementation. |
|
2995 * @returns bool |
|
2996 */ |
|
2997 _isCmdLineEmpty: function ssi_isCmdLineEmpty(aWindow, aState) { |
|
2998 var pinnedOnly = aState.windows && |
|
2999 aState.windows.every(function (win) |
|
3000 win.tabs.every(function (tab) tab.pinned)); |
|
3001 |
|
3002 let hasFirstArgument = aWindow.arguments && aWindow.arguments[0]; |
|
3003 if (!pinnedOnly) { |
|
3004 let defaultArgs = Cc["@mozilla.org/browser/clh;1"]. |
|
3005 getService(Ci.nsIBrowserHandler).defaultArgs; |
|
3006 if (aWindow.arguments && |
|
3007 aWindow.arguments[0] && |
|
3008 aWindow.arguments[0] == defaultArgs) |
|
3009 hasFirstArgument = false; |
|
3010 } |
|
3011 |
|
3012 return !hasFirstArgument; |
|
3013 }, |
|
3014 |
|
3015 /** |
|
3016 * on popup windows, the XULWindow's attributes seem not to be set correctly |
|
3017 * we use thus JSDOMWindow attributes for sizemode and normal window attributes |
|
3018 * (and hope for reasonable values when maximized/minimized - since then |
|
3019 * outerWidth/outerHeight aren't the dimensions of the restored window) |
|
3020 * @param aWindow |
|
3021 * Window reference |
|
3022 * @param aAttribute |
|
3023 * String sizemode | width | height | other window attribute |
|
3024 * @returns string |
|
3025 */ |
|
3026 _getWindowDimension: function ssi_getWindowDimension(aWindow, aAttribute) { |
|
3027 if (aAttribute == "sizemode") { |
|
3028 switch (aWindow.windowState) { |
|
3029 case aWindow.STATE_FULLSCREEN: |
|
3030 case aWindow.STATE_MAXIMIZED: |
|
3031 return "maximized"; |
|
3032 case aWindow.STATE_MINIMIZED: |
|
3033 return "minimized"; |
|
3034 default: |
|
3035 return "normal"; |
|
3036 } |
|
3037 } |
|
3038 |
|
3039 var dimension; |
|
3040 switch (aAttribute) { |
|
3041 case "width": |
|
3042 dimension = aWindow.outerWidth; |
|
3043 break; |
|
3044 case "height": |
|
3045 dimension = aWindow.outerHeight; |
|
3046 break; |
|
3047 default: |
|
3048 dimension = aAttribute in aWindow ? aWindow[aAttribute] : ""; |
|
3049 break; |
|
3050 } |
|
3051 |
|
3052 if (aWindow.windowState == aWindow.STATE_NORMAL) { |
|
3053 return dimension; |
|
3054 } |
|
3055 return aWindow.document.documentElement.getAttribute(aAttribute) || dimension; |
|
3056 }, |
|
3057 |
|
3058 /** |
|
3059 * Get nsIURI from string |
|
3060 * @param string |
|
3061 * @returns nsIURI |
|
3062 */ |
|
3063 _getURIFromString: function ssi_getURIFromString(aString) { |
|
3064 return Services.io.newURI(aString, null, null); |
|
3065 }, |
|
3066 |
|
3067 /** |
|
3068 * @param aState is a session state |
|
3069 * @param aRecentCrashes is the number of consecutive crashes |
|
3070 * @returns whether a restore page will be needed for the session state |
|
3071 */ |
|
3072 _needsRestorePage: function ssi_needsRestorePage(aState, aRecentCrashes) { |
|
3073 const SIX_HOURS_IN_MS = 6 * 60 * 60 * 1000; |
|
3074 |
|
3075 // don't display the page when there's nothing to restore |
|
3076 let winData = aState.windows || null; |
|
3077 if (!winData || winData.length == 0) |
|
3078 return false; |
|
3079 |
|
3080 // don't wrap a single about:sessionrestore page |
|
3081 if (this._hasSingleTabWithURL(winData, "about:sessionrestore") || |
|
3082 this._hasSingleTabWithURL(winData, "about:welcomeback")) { |
|
3083 return false; |
|
3084 } |
|
3085 |
|
3086 // don't automatically restore in Safe Mode |
|
3087 if (Services.appinfo.inSafeMode) |
|
3088 return true; |
|
3089 |
|
3090 let max_resumed_crashes = |
|
3091 this._prefBranch.getIntPref("sessionstore.max_resumed_crashes"); |
|
3092 let sessionAge = aState.session && aState.session.lastUpdate && |
|
3093 (Date.now() - aState.session.lastUpdate); |
|
3094 |
|
3095 return max_resumed_crashes != -1 && |
|
3096 (aRecentCrashes > max_resumed_crashes || |
|
3097 sessionAge && sessionAge >= SIX_HOURS_IN_MS); |
|
3098 }, |
|
3099 |
|
3100 /** |
|
3101 * @param aWinData is the set of windows in session state |
|
3102 * @param aURL is the single URL we're looking for |
|
3103 * @returns whether the window data contains only the single URL passed |
|
3104 */ |
|
3105 _hasSingleTabWithURL: function(aWinData, aURL) { |
|
3106 if (aWinData && |
|
3107 aWinData.length == 1 && |
|
3108 aWinData[0].tabs && |
|
3109 aWinData[0].tabs.length == 1 && |
|
3110 aWinData[0].tabs[0].entries && |
|
3111 aWinData[0].tabs[0].entries.length == 1) { |
|
3112 return aURL == aWinData[0].tabs[0].entries[0].url; |
|
3113 } |
|
3114 return false; |
|
3115 }, |
|
3116 |
|
3117 /** |
|
3118 * Determine if the tab state we're passed is something we should save. This |
|
3119 * is used when closing a tab or closing a window with a single tab |
|
3120 * |
|
3121 * @param aTabState |
|
3122 * The current tab state |
|
3123 * @returns boolean |
|
3124 */ |
|
3125 _shouldSaveTabState: function ssi_shouldSaveTabState(aTabState) { |
|
3126 // If the tab has only a transient about: history entry, no other |
|
3127 // session history, and no userTypedValue, then we don't actually want to |
|
3128 // store this tab's data. |
|
3129 return aTabState.entries.length && |
|
3130 !(aTabState.entries.length == 1 && |
|
3131 (aTabState.entries[0].url == "about:blank" || |
|
3132 aTabState.entries[0].url == "about:newtab") && |
|
3133 !aTabState.userTypedValue); |
|
3134 }, |
|
3135 |
|
3136 /** |
|
3137 * This is going to take a state as provided at startup (via |
|
3138 * nsISessionStartup.state) and split it into 2 parts. The first part |
|
3139 * (defaultState) will be a state that should still be restored at startup, |
|
3140 * while the second part (state) is a state that should be saved for later. |
|
3141 * defaultState will be comprised of windows with only pinned tabs, extracted |
|
3142 * from state. It will contain the cookies that go along with the history |
|
3143 * entries in those tabs. It will also contain window position information. |
|
3144 * |
|
3145 * defaultState will be restored at startup. state will be passed into |
|
3146 * LastSession and will be kept in case the user explicitly wants |
|
3147 * to restore the previous session (publicly exposed as restoreLastSession). |
|
3148 * |
|
3149 * @param state |
|
3150 * The state, presumably from nsISessionStartup.state |
|
3151 * @returns [defaultState, state] |
|
3152 */ |
|
3153 _prepDataForDeferredRestore: function ssi_prepDataForDeferredRestore(state) { |
|
3154 // Make sure that we don't modify the global state as provided by |
|
3155 // nsSessionStartup.state. |
|
3156 state = Cu.cloneInto(state, {}); |
|
3157 |
|
3158 let defaultState = { windows: [], selectedWindow: 1 }; |
|
3159 |
|
3160 state.selectedWindow = state.selectedWindow || 1; |
|
3161 |
|
3162 // Look at each window, remove pinned tabs, adjust selectedindex, |
|
3163 // remove window if necessary. |
|
3164 for (let wIndex = 0; wIndex < state.windows.length;) { |
|
3165 let window = state.windows[wIndex]; |
|
3166 window.selected = window.selected || 1; |
|
3167 // We're going to put the state of the window into this object |
|
3168 let pinnedWindowState = { tabs: [], cookies: []}; |
|
3169 for (let tIndex = 0; tIndex < window.tabs.length;) { |
|
3170 if (window.tabs[tIndex].pinned) { |
|
3171 // Adjust window.selected |
|
3172 if (tIndex + 1 < window.selected) |
|
3173 window.selected -= 1; |
|
3174 else if (tIndex + 1 == window.selected) |
|
3175 pinnedWindowState.selected = pinnedWindowState.tabs.length + 2; |
|
3176 // + 2 because the tab isn't actually in the array yet |
|
3177 |
|
3178 // Now add the pinned tab to our window |
|
3179 pinnedWindowState.tabs = |
|
3180 pinnedWindowState.tabs.concat(window.tabs.splice(tIndex, 1)); |
|
3181 // We don't want to increment tIndex here. |
|
3182 continue; |
|
3183 } |
|
3184 tIndex++; |
|
3185 } |
|
3186 |
|
3187 // At this point the window in the state object has been modified (or not) |
|
3188 // We want to build the rest of this new window object if we have pinnedTabs. |
|
3189 if (pinnedWindowState.tabs.length) { |
|
3190 // First get the other attributes off the window |
|
3191 WINDOW_ATTRIBUTES.forEach(function(attr) { |
|
3192 if (attr in window) { |
|
3193 pinnedWindowState[attr] = window[attr]; |
|
3194 delete window[attr]; |
|
3195 } |
|
3196 }); |
|
3197 // We're just copying position data into the pinned window. |
|
3198 // Not copying over: |
|
3199 // - _closedTabs |
|
3200 // - extData |
|
3201 // - isPopup |
|
3202 // - hidden |
|
3203 |
|
3204 // Assign a unique ID to correlate the window to be opened with the |
|
3205 // remaining data |
|
3206 window.__lastSessionWindowID = pinnedWindowState.__lastSessionWindowID |
|
3207 = "" + Date.now() + Math.random(); |
|
3208 |
|
3209 // Extract the cookies that belong with each pinned tab |
|
3210 this._splitCookiesFromWindow(window, pinnedWindowState); |
|
3211 |
|
3212 // Actually add this window to our defaultState |
|
3213 defaultState.windows.push(pinnedWindowState); |
|
3214 // Remove the window from the state if it doesn't have any tabs |
|
3215 if (!window.tabs.length) { |
|
3216 if (wIndex + 1 <= state.selectedWindow) |
|
3217 state.selectedWindow -= 1; |
|
3218 else if (wIndex + 1 == state.selectedWindow) |
|
3219 defaultState.selectedIndex = defaultState.windows.length + 1; |
|
3220 |
|
3221 state.windows.splice(wIndex, 1); |
|
3222 // We don't want to increment wIndex here. |
|
3223 continue; |
|
3224 } |
|
3225 |
|
3226 |
|
3227 } |
|
3228 wIndex++; |
|
3229 } |
|
3230 |
|
3231 return [defaultState, state]; |
|
3232 }, |
|
3233 |
|
3234 /** |
|
3235 * Splits out the cookies from aWinState into aTargetWinState based on the |
|
3236 * tabs that are in aTargetWinState. |
|
3237 * This alters the state of aWinState and aTargetWinState. |
|
3238 */ |
|
3239 _splitCookiesFromWindow: |
|
3240 function ssi_splitCookiesFromWindow(aWinState, aTargetWinState) { |
|
3241 if (!aWinState.cookies || !aWinState.cookies.length) |
|
3242 return; |
|
3243 |
|
3244 // Get the hosts for history entries in aTargetWinState |
|
3245 let cookieHosts = SessionCookies.getHostsForWindow(aTargetWinState); |
|
3246 |
|
3247 // By creating a regex we reduce overhead and there is only one loop pass |
|
3248 // through either array (cookieHosts and aWinState.cookies). |
|
3249 let hosts = Object.keys(cookieHosts).join("|").replace("\\.", "\\.", "g"); |
|
3250 // If we don't actually have any hosts, then we don't want to do anything. |
|
3251 if (!hosts.length) |
|
3252 return; |
|
3253 let cookieRegex = new RegExp(".*(" + hosts + ")"); |
|
3254 for (let cIndex = 0; cIndex < aWinState.cookies.length;) { |
|
3255 if (cookieRegex.test(aWinState.cookies[cIndex].host)) { |
|
3256 aTargetWinState.cookies = |
|
3257 aTargetWinState.cookies.concat(aWinState.cookies.splice(cIndex, 1)); |
|
3258 continue; |
|
3259 } |
|
3260 cIndex++; |
|
3261 } |
|
3262 }, |
|
3263 |
|
3264 /** |
|
3265 * Converts a JavaScript object into a JSON string |
|
3266 * (see http://www.json.org/ for more information). |
|
3267 * |
|
3268 * The inverse operation consists of JSON.parse(JSON_string). |
|
3269 * |
|
3270 * @param aJSObject is the object to be converted |
|
3271 * @returns the object's JSON representation |
|
3272 */ |
|
3273 _toJSONString: function ssi_toJSONString(aJSObject) { |
|
3274 return JSON.stringify(aJSObject); |
|
3275 }, |
|
3276 |
|
3277 _sendRestoreCompletedNotifications: function ssi_sendRestoreCompletedNotifications() { |
|
3278 // not all windows restored, yet |
|
3279 if (this._restoreCount > 1) { |
|
3280 this._restoreCount--; |
|
3281 return; |
|
3282 } |
|
3283 |
|
3284 // observers were already notified |
|
3285 if (this._restoreCount == -1) |
|
3286 return; |
|
3287 |
|
3288 // This was the last window restored at startup, notify observers. |
|
3289 Services.obs.notifyObservers(null, |
|
3290 this._browserSetState ? NOTIFY_BROWSER_STATE_RESTORED : NOTIFY_WINDOWS_RESTORED, |
|
3291 ""); |
|
3292 |
|
3293 this._browserSetState = false; |
|
3294 this._restoreCount = -1; |
|
3295 }, |
|
3296 |
|
3297 /** |
|
3298 * Set the given window's busy state |
|
3299 * @param aWindow the window |
|
3300 * @param aValue the window's busy state |
|
3301 */ |
|
3302 _setWindowStateBusyValue: |
|
3303 function ssi_changeWindowStateBusyValue(aWindow, aValue) { |
|
3304 |
|
3305 this._windows[aWindow.__SSi].busy = aValue; |
|
3306 |
|
3307 // Keep the to-be-restored state in sync because that is returned by |
|
3308 // getWindowState() as long as the window isn't loaded, yet. |
|
3309 if (!this._isWindowLoaded(aWindow)) { |
|
3310 let stateToRestore = this._statesToRestore[aWindow.__SS_restoreID].windows[0]; |
|
3311 stateToRestore.busy = aValue; |
|
3312 } |
|
3313 }, |
|
3314 |
|
3315 /** |
|
3316 * Set the given window's state to 'not busy'. |
|
3317 * @param aWindow the window |
|
3318 */ |
|
3319 _setWindowStateReady: function ssi_setWindowStateReady(aWindow) { |
|
3320 this._setWindowStateBusyValue(aWindow, false); |
|
3321 this._sendWindowStateEvent(aWindow, "Ready"); |
|
3322 }, |
|
3323 |
|
3324 /** |
|
3325 * Set the given window's state to 'busy'. |
|
3326 * @param aWindow the window |
|
3327 */ |
|
3328 _setWindowStateBusy: function ssi_setWindowStateBusy(aWindow) { |
|
3329 this._setWindowStateBusyValue(aWindow, true); |
|
3330 this._sendWindowStateEvent(aWindow, "Busy"); |
|
3331 }, |
|
3332 |
|
3333 /** |
|
3334 * Dispatch an SSWindowState_____ event for the given window. |
|
3335 * @param aWindow the window |
|
3336 * @param aType the type of event, SSWindowState will be prepended to this string |
|
3337 */ |
|
3338 _sendWindowStateEvent: function ssi_sendWindowStateEvent(aWindow, aType) { |
|
3339 let event = aWindow.document.createEvent("Events"); |
|
3340 event.initEvent("SSWindowState" + aType, true, false); |
|
3341 aWindow.dispatchEvent(event); |
|
3342 }, |
|
3343 |
|
3344 /** |
|
3345 * Dispatch the SSTabRestored event for the given tab. |
|
3346 * @param aTab the which has been restored |
|
3347 */ |
|
3348 _sendTabRestoredNotification: function ssi_sendTabRestoredNotification(aTab) { |
|
3349 let event = aTab.ownerDocument.createEvent("Events"); |
|
3350 event.initEvent("SSTabRestored", true, false); |
|
3351 aTab.dispatchEvent(event); |
|
3352 }, |
|
3353 |
|
3354 /** |
|
3355 * @param aWindow |
|
3356 * Window reference |
|
3357 * @returns whether this window's data is still cached in _statesToRestore |
|
3358 * because it's not fully loaded yet |
|
3359 */ |
|
3360 _isWindowLoaded: function ssi_isWindowLoaded(aWindow) { |
|
3361 return !aWindow.__SS_restoreID; |
|
3362 }, |
|
3363 |
|
3364 /** |
|
3365 * Replace "Loading..." with the tab label (with minimal side-effects) |
|
3366 * @param aString is the string the title is stored in |
|
3367 * @param aTabbrowser is a tabbrowser object, containing aTab |
|
3368 * @param aTab is the tab whose title we're updating & using |
|
3369 * |
|
3370 * @returns aString that has been updated with the new title |
|
3371 */ |
|
3372 _replaceLoadingTitle : function ssi_replaceLoadingTitle(aString, aTabbrowser, aTab) { |
|
3373 if (aString == aTabbrowser.mStringBundle.getString("tabs.connecting")) { |
|
3374 aTabbrowser.setTabTitle(aTab); |
|
3375 [aString, aTab.label] = [aTab.label, aString]; |
|
3376 } |
|
3377 return aString; |
|
3378 }, |
|
3379 |
|
3380 /** |
|
3381 * Resize this._closedWindows to the value of the pref, except in the case |
|
3382 * where we don't have any non-popup windows on Windows and Linux. Then we must |
|
3383 * resize such that we have at least one non-popup window. |
|
3384 */ |
|
3385 _capClosedWindows : function ssi_capClosedWindows() { |
|
3386 if (this._closedWindows.length <= this._max_windows_undo) |
|
3387 return; |
|
3388 let spliceTo = this._max_windows_undo; |
|
3389 #ifndef XP_MACOSX |
|
3390 let normalWindowIndex = 0; |
|
3391 // try to find a non-popup window in this._closedWindows |
|
3392 while (normalWindowIndex < this._closedWindows.length && |
|
3393 !!this._closedWindows[normalWindowIndex].isPopup) |
|
3394 normalWindowIndex++; |
|
3395 if (normalWindowIndex >= this._max_windows_undo) |
|
3396 spliceTo = normalWindowIndex + 1; |
|
3397 #endif |
|
3398 this._closedWindows.splice(spliceTo, this._closedWindows.length); |
|
3399 }, |
|
3400 |
|
3401 /** |
|
3402 * Clears the set of windows that are "resurrected" before writing to disk to |
|
3403 * make closing windows one after the other until shutdown work as expected. |
|
3404 * |
|
3405 * This function should only be called when we are sure that there has been |
|
3406 * a user action that indicates the browser is actively being used and all |
|
3407 * windows that have been closed before are not part of a series of closing |
|
3408 * windows. |
|
3409 */ |
|
3410 _clearRestoringWindows: function ssi_clearRestoringWindows() { |
|
3411 for (let i = 0; i < this._closedWindows.length; i++) { |
|
3412 delete this._closedWindows[i]._shouldRestore; |
|
3413 } |
|
3414 }, |
|
3415 |
|
3416 /** |
|
3417 * Reset state to prepare for a new session state to be restored. |
|
3418 */ |
|
3419 _resetRestoringState: function ssi_initRestoringState() { |
|
3420 TabRestoreQueue.reset(); |
|
3421 this._tabsRestoringCount = 0; |
|
3422 }, |
|
3423 |
|
3424 /** |
|
3425 * Reset the restoring state for a particular tab. This will be called when |
|
3426 * removing a tab or when a tab needs to be reset (it's being overwritten). |
|
3427 * |
|
3428 * @param aTab |
|
3429 * The tab that will be "reset" |
|
3430 */ |
|
3431 _resetLocalTabRestoringState: function (aTab) { |
|
3432 let window = aTab.ownerDocument.defaultView; |
|
3433 let browser = aTab.linkedBrowser; |
|
3434 |
|
3435 // Keep the tab's previous state for later in this method |
|
3436 let previousState = browser.__SS_restoreState; |
|
3437 |
|
3438 // The browser is no longer in any sort of restoring state. |
|
3439 delete browser.__SS_restoreState; |
|
3440 this._browserEpochs.delete(browser.permanentKey); |
|
3441 |
|
3442 aTab.removeAttribute("pending"); |
|
3443 browser.removeAttribute("pending"); |
|
3444 |
|
3445 if (previousState == TAB_STATE_RESTORING) { |
|
3446 if (this._tabsRestoringCount) |
|
3447 this._tabsRestoringCount--; |
|
3448 } else if (previousState == TAB_STATE_NEEDS_RESTORE) { |
|
3449 // Make sure that the tab is removed from the list of tabs to restore. |
|
3450 // Again, this is normally done in restoreTabContent, but that isn't being called |
|
3451 // for this tab. |
|
3452 TabRestoreQueue.remove(aTab); |
|
3453 } |
|
3454 }, |
|
3455 |
|
3456 _resetTabRestoringState: function (tab) { |
|
3457 let browser = tab.linkedBrowser; |
|
3458 if (browser.__SS_restoreState) { |
|
3459 browser.messageManager.sendAsyncMessage("SessionStore:resetRestore", {}); |
|
3460 } |
|
3461 this._resetLocalTabRestoringState(tab); |
|
3462 }, |
|
3463 |
|
3464 /** |
|
3465 * Each time a <browser> element is restored, we increment its "epoch". To |
|
3466 * check if a message from content-sessionStore.js is out of date, we can |
|
3467 * compare the epoch received with the message to the <browser> element's |
|
3468 * epoch. This function does that, and returns true if |epoch| is up-to-date |
|
3469 * with respect to |browser|. |
|
3470 */ |
|
3471 isCurrentEpoch: function (browser, epoch) { |
|
3472 return this._browserEpochs.get(browser.permanentKey, 0) == epoch; |
|
3473 }, |
|
3474 |
|
3475 }; |
|
3476 |
|
3477 /** |
|
3478 * Priority queue that keeps track of a list of tabs to restore and returns |
|
3479 * the tab we should restore next, based on priority rules. We decide between |
|
3480 * pinned, visible and hidden tabs in that and FIFO order. Hidden tabs are only |
|
3481 * restored with restore_hidden_tabs=true. |
|
3482 */ |
|
3483 let TabRestoreQueue = { |
|
3484 // The separate buckets used to store tabs. |
|
3485 tabs: {priority: [], visible: [], hidden: []}, |
|
3486 |
|
3487 // Preferences used by the TabRestoreQueue to determine which tabs |
|
3488 // are restored automatically and which tabs will be on-demand. |
|
3489 prefs: { |
|
3490 // Lazy getter that returns whether tabs are restored on demand. |
|
3491 get restoreOnDemand() { |
|
3492 let updateValue = () => { |
|
3493 let value = Services.prefs.getBoolPref(PREF); |
|
3494 let definition = {value: value, configurable: true}; |
|
3495 Object.defineProperty(this, "restoreOnDemand", definition); |
|
3496 return value; |
|
3497 } |
|
3498 |
|
3499 const PREF = "browser.sessionstore.restore_on_demand"; |
|
3500 Services.prefs.addObserver(PREF, updateValue, false); |
|
3501 return updateValue(); |
|
3502 }, |
|
3503 |
|
3504 // Lazy getter that returns whether pinned tabs are restored on demand. |
|
3505 get restorePinnedTabsOnDemand() { |
|
3506 let updateValue = () => { |
|
3507 let value = Services.prefs.getBoolPref(PREF); |
|
3508 let definition = {value: value, configurable: true}; |
|
3509 Object.defineProperty(this, "restorePinnedTabsOnDemand", definition); |
|
3510 return value; |
|
3511 } |
|
3512 |
|
3513 const PREF = "browser.sessionstore.restore_pinned_tabs_on_demand"; |
|
3514 Services.prefs.addObserver(PREF, updateValue, false); |
|
3515 return updateValue(); |
|
3516 }, |
|
3517 |
|
3518 // Lazy getter that returns whether we should restore hidden tabs. |
|
3519 get restoreHiddenTabs() { |
|
3520 let updateValue = () => { |
|
3521 let value = Services.prefs.getBoolPref(PREF); |
|
3522 let definition = {value: value, configurable: true}; |
|
3523 Object.defineProperty(this, "restoreHiddenTabs", definition); |
|
3524 return value; |
|
3525 } |
|
3526 |
|
3527 const PREF = "browser.sessionstore.restore_hidden_tabs"; |
|
3528 Services.prefs.addObserver(PREF, updateValue, false); |
|
3529 return updateValue(); |
|
3530 } |
|
3531 }, |
|
3532 |
|
3533 // Resets the queue and removes all tabs. |
|
3534 reset: function () { |
|
3535 this.tabs = {priority: [], visible: [], hidden: []}; |
|
3536 }, |
|
3537 |
|
3538 // Adds a tab to the queue and determines its priority bucket. |
|
3539 add: function (tab) { |
|
3540 let {priority, hidden, visible} = this.tabs; |
|
3541 |
|
3542 if (tab.pinned) { |
|
3543 priority.push(tab); |
|
3544 } else if (tab.hidden) { |
|
3545 hidden.push(tab); |
|
3546 } else { |
|
3547 visible.push(tab); |
|
3548 } |
|
3549 }, |
|
3550 |
|
3551 // Removes a given tab from the queue, if it's in there. |
|
3552 remove: function (tab) { |
|
3553 let {priority, hidden, visible} = this.tabs; |
|
3554 |
|
3555 // We'll always check priority first since we don't |
|
3556 // have an indicator if a tab will be there or not. |
|
3557 let set = priority; |
|
3558 let index = set.indexOf(tab); |
|
3559 |
|
3560 if (index == -1) { |
|
3561 set = tab.hidden ? hidden : visible; |
|
3562 index = set.indexOf(tab); |
|
3563 } |
|
3564 |
|
3565 if (index > -1) { |
|
3566 set.splice(index, 1); |
|
3567 } |
|
3568 }, |
|
3569 |
|
3570 // Returns and removes the tab with the highest priority. |
|
3571 shift: function () { |
|
3572 let set; |
|
3573 let {priority, hidden, visible} = this.tabs; |
|
3574 |
|
3575 let {restoreOnDemand, restorePinnedTabsOnDemand} = this.prefs; |
|
3576 let restorePinned = !(restoreOnDemand && restorePinnedTabsOnDemand); |
|
3577 if (restorePinned && priority.length) { |
|
3578 set = priority; |
|
3579 } else if (!restoreOnDemand) { |
|
3580 if (visible.length) { |
|
3581 set = visible; |
|
3582 } else if (this.prefs.restoreHiddenTabs && hidden.length) { |
|
3583 set = hidden; |
|
3584 } |
|
3585 } |
|
3586 |
|
3587 return set && set.shift(); |
|
3588 }, |
|
3589 |
|
3590 // Moves a given tab from the 'hidden' to the 'visible' bucket. |
|
3591 hiddenToVisible: function (tab) { |
|
3592 let {hidden, visible} = this.tabs; |
|
3593 let index = hidden.indexOf(tab); |
|
3594 |
|
3595 if (index > -1) { |
|
3596 hidden.splice(index, 1); |
|
3597 visible.push(tab); |
|
3598 } else { |
|
3599 throw new Error("restore queue: hidden tab not found"); |
|
3600 } |
|
3601 }, |
|
3602 |
|
3603 // Moves a given tab from the 'visible' to the 'hidden' bucket. |
|
3604 visibleToHidden: function (tab) { |
|
3605 let {visible, hidden} = this.tabs; |
|
3606 let index = visible.indexOf(tab); |
|
3607 |
|
3608 if (index > -1) { |
|
3609 visible.splice(index, 1); |
|
3610 hidden.push(tab); |
|
3611 } else { |
|
3612 throw new Error("restore queue: visible tab not found"); |
|
3613 } |
|
3614 } |
|
3615 }; |
|
3616 |
|
3617 // A map storing a closed window's state data until it goes aways (is GC'ed). |
|
3618 // This ensures that API clients can still read (but not write) states of |
|
3619 // windows they still hold a reference to but we don't. |
|
3620 let DyingWindowCache = { |
|
3621 _data: new WeakMap(), |
|
3622 |
|
3623 has: function (window) { |
|
3624 return this._data.has(window); |
|
3625 }, |
|
3626 |
|
3627 get: function (window) { |
|
3628 return this._data.get(window); |
|
3629 }, |
|
3630 |
|
3631 set: function (window, data) { |
|
3632 this._data.set(window, data); |
|
3633 }, |
|
3634 |
|
3635 remove: function (window) { |
|
3636 this._data.delete(window); |
|
3637 } |
|
3638 }; |
|
3639 |
|
3640 // A weak set of dirty windows. We use it to determine which windows we need to |
|
3641 // recollect data for when getCurrentState() is called. |
|
3642 let DirtyWindows = { |
|
3643 _data: new WeakMap(), |
|
3644 |
|
3645 has: function (window) { |
|
3646 return this._data.has(window); |
|
3647 }, |
|
3648 |
|
3649 add: function (window) { |
|
3650 return this._data.set(window, true); |
|
3651 }, |
|
3652 |
|
3653 remove: function (window) { |
|
3654 this._data.delete(window); |
|
3655 }, |
|
3656 |
|
3657 clear: function (window) { |
|
3658 this._data.clear(); |
|
3659 } |
|
3660 }; |
|
3661 |
|
3662 // The state from the previous session (after restoring pinned tabs). This |
|
3663 // state is persisted and passed through to the next session during an app |
|
3664 // restart to make the third party add-on warning not trash the deferred |
|
3665 // session |
|
3666 let LastSession = { |
|
3667 _state: null, |
|
3668 |
|
3669 get canRestore() { |
|
3670 return !!this._state; |
|
3671 }, |
|
3672 |
|
3673 getState: function () { |
|
3674 return this._state; |
|
3675 }, |
|
3676 |
|
3677 setState: function (state) { |
|
3678 this._state = state; |
|
3679 }, |
|
3680 |
|
3681 clear: function () { |
|
3682 if (this._state) { |
|
3683 this._state = null; |
|
3684 Services.obs.notifyObservers(null, NOTIFY_LAST_SESSION_CLEARED, null); |
|
3685 } |
|
3686 } |
|
3687 }; |