Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
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/. */
5 "use strict";
7 this.EXPORTED_SYMBOLS = ["SessionStore"];
9 const Cu = Components.utils;
10 const Cc = Components.classes;
11 const Ci = Components.interfaces;
12 const Cr = Components.results;
14 const STATE_STOPPED = 0;
15 const STATE_RUNNING = 1;
16 const STATE_QUITTING = -1;
18 const TAB_STATE_NEEDS_RESTORE = 1;
19 const TAB_STATE_RESTORING = 2;
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";
25 const NOTIFY_TAB_RESTORED = "sessionstore-debug-tab-restored"; // WARNING: debug-only
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;
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 ];
41 // XUL Window properties to (re)store
42 // Restored in restoreDimensions()
43 const WINDOW_ATTRIBUTES = ["width", "height", "screenX", "screenY", "sizemode"];
45 // Hideable window features to (re)store
46 // Restored in restoreWindowFeatures()
47 const WINDOW_HIDEABLE_FEATURES = [
48 "menubar", "toolbar", "locationbar", "personalbar", "statusbar", "scrollbars"
49 ];
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",
56 // The content script sends us data that has been invalidated and needs to
57 // be saved to disk.
58 "SessionStore:update",
60 // The restoreHistory code has run. This is a good time to run SSTabRestoring.
61 "SessionStore:restoreHistoryComplete",
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",
67 // All network loads for a restoring tab are done, so we should consider
68 // restoring another tab in the queue.
69 "SessionStore:restoreTabContentComplete",
71 // The document has been restored, so the restore is done. We trigger
72 // SSTabRestored at this time.
73 "SessionStore:restoreDocumentComplete",
75 // A tab that is being restored was reloaded. We call restoreTabContent to
76 // finish restoring it right away.
77 "SessionStore:reloadPendingTab",
78 ];
80 // These are tab events that we listen to.
81 const TAB_EVENTS = [
82 "TabOpen", "TabClose", "TabSelect", "TabShow", "TabHide", "TabPinned",
83 "TabUnpinned"
84 ];
86 // The number of milliseconds in a day
87 const MS_PER_DAY = 1000.0 * 60.0 * 60.0 * 24.0;
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);
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");
105 XPCOMUtils.defineLazyModuleGetter(this, "console",
106 "resource://gre/modules/devtools/Console.jsm");
107 XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
108 "resource:///modules/RecentWindow.jsm");
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");
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 }
143 this.SessionStore = {
144 get promiseInitialized() {
145 return SessionStoreInternal.promiseInitialized;
146 },
148 get canRestoreLastSession() {
149 return SessionStoreInternal.canRestoreLastSession;
150 },
152 set canRestoreLastSession(val) {
153 SessionStoreInternal.canRestoreLastSession = val;
154 },
156 init: function ss_init() {
157 SessionStoreInternal.init();
158 },
160 getBrowserState: function ss_getBrowserState() {
161 return SessionStoreInternal.getBrowserState();
162 },
164 setBrowserState: function ss_setBrowserState(aState) {
165 SessionStoreInternal.setBrowserState(aState);
166 },
168 getWindowState: function ss_getWindowState(aWindow) {
169 return SessionStoreInternal.getWindowState(aWindow);
170 },
172 setWindowState: function ss_setWindowState(aWindow, aState, aOverwrite) {
173 SessionStoreInternal.setWindowState(aWindow, aState, aOverwrite);
174 },
176 getTabState: function ss_getTabState(aTab) {
177 return SessionStoreInternal.getTabState(aTab);
178 },
180 setTabState: function ss_setTabState(aTab, aState) {
181 SessionStoreInternal.setTabState(aTab, aState);
182 },
184 duplicateTab: function ss_duplicateTab(aWindow, aTab, aDelta = 0) {
185 return SessionStoreInternal.duplicateTab(aWindow, aTab, aDelta);
186 },
188 getClosedTabCount: function ss_getClosedTabCount(aWindow) {
189 return SessionStoreInternal.getClosedTabCount(aWindow);
190 },
192 getClosedTabData: function ss_getClosedTabDataAt(aWindow) {
193 return SessionStoreInternal.getClosedTabData(aWindow);
194 },
196 undoCloseTab: function ss_undoCloseTab(aWindow, aIndex) {
197 return SessionStoreInternal.undoCloseTab(aWindow, aIndex);
198 },
200 forgetClosedTab: function ss_forgetClosedTab(aWindow, aIndex) {
201 return SessionStoreInternal.forgetClosedTab(aWindow, aIndex);
202 },
204 getClosedWindowCount: function ss_getClosedWindowCount() {
205 return SessionStoreInternal.getClosedWindowCount();
206 },
208 getClosedWindowData: function ss_getClosedWindowData() {
209 return SessionStoreInternal.getClosedWindowData();
210 },
212 undoCloseWindow: function ss_undoCloseWindow(aIndex) {
213 return SessionStoreInternal.undoCloseWindow(aIndex);
214 },
216 forgetClosedWindow: function ss_forgetClosedWindow(aIndex) {
217 return SessionStoreInternal.forgetClosedWindow(aIndex);
218 },
220 getWindowValue: function ss_getWindowValue(aWindow, aKey) {
221 return SessionStoreInternal.getWindowValue(aWindow, aKey);
222 },
224 setWindowValue: function ss_setWindowValue(aWindow, aKey, aStringValue) {
225 SessionStoreInternal.setWindowValue(aWindow, aKey, aStringValue);
226 },
228 deleteWindowValue: function ss_deleteWindowValue(aWindow, aKey) {
229 SessionStoreInternal.deleteWindowValue(aWindow, aKey);
230 },
232 getTabValue: function ss_getTabValue(aTab, aKey) {
233 return SessionStoreInternal.getTabValue(aTab, aKey);
234 },
236 setTabValue: function ss_setTabValue(aTab, aKey, aStringValue) {
237 SessionStoreInternal.setTabValue(aTab, aKey, aStringValue);
238 },
240 deleteTabValue: function ss_deleteTabValue(aTab, aKey) {
241 SessionStoreInternal.deleteTabValue(aTab, aKey);
242 },
244 getGlobalValue: function ss_getGlobalValue(aKey) {
245 return SessionStoreInternal.getGlobalValue(aKey);
246 },
248 setGlobalValue: function ss_setGlobalValue(aKey, aStringValue) {
249 SessionStoreInternal.setGlobalValue(aKey, aStringValue);
250 },
252 deleteGlobalValue: function ss_deleteGlobalValue(aKey) {
253 SessionStoreInternal.deleteGlobalValue(aKey);
254 },
256 persistTabAttribute: function ss_persistTabAttribute(aName) {
257 SessionStoreInternal.persistTabAttribute(aName);
258 },
260 restoreLastSession: function ss_restoreLastSession() {
261 SessionStoreInternal.restoreLastSession();
262 },
264 getCurrentState: function (aUpdateAll) {
265 return SessionStoreInternal.getCurrentState(aUpdateAll);
266 },
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 };
280 // Freeze the SessionStore object. We don't want anyone to modify it.
281 Object.freeze(SessionStore);
283 let SessionStoreInternal = {
284 QueryInterface: XPCOMUtils.generateQI([
285 Ci.nsIDOMEventListener,
286 Ci.nsIObserver,
287 Ci.nsISupportsWeakReference
288 ]),
290 // set default load state
291 _loadState: STATE_STOPPED,
293 _globalState: new GlobalState(),
295 // During the initial restore and setBrowserState calls tracks the number of
296 // windows yet to be restored
297 _restoreCount: -1,
299 // This number gets incremented each time we start to restore a tab.
300 _nextRestoreEpoch: 1,
302 // For each <browser> element being restored, records the current epoch.
303 _browserEpochs: new WeakMap(),
305 // whether a setBrowserState call is in progress
306 _browserSetState: false,
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(),
312 // states for all currently opened windows
313 _windows: {},
315 // counter for creating unique window IDs
316 _nextWindowID: 0,
318 // states for all recently closed windows
319 _closedWindows: [],
321 // collection of session states yet to be restored
322 _statesToRestore: {},
324 // counts the number of crashes since the last clean start
325 _recentCrashes: 0,
327 // whether the last window was closed and should be restored
328 _restoreLastWindow: false,
330 // number of tabs currently restoring
331 _tabsRestoringCount: 0,
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,
338 // A promise resolved once initialization is complete
339 _deferredInitialized: Promise.defer(),
341 // Whether session has been initialized
342 _sessionInitialized: false,
344 // Promise that is resolved when we're ready to initialize
345 // and restore the session.
346 _promiseReadyForInitialization: null,
348 /**
349 * A promise fulfilled once initialization is complete.
350 */
351 get promiseInitialized() {
352 return this._deferredInitialized.promise;
353 },
355 get canRestoreLastSession() {
356 return LastSession.canRestore;
357 },
359 set canRestoreLastSession(val) {
360 // Cheat a bit; only allow false.
361 if (!val) {
362 LastSession.clear();
363 }
364 },
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 }
374 TelemetryTimestamps.add("sessionRestoreInitialized");
375 OBSERVING.forEach(function(aTopic) {
376 Services.obs.addObserver(this, aTopic, true);
377 }, this);
379 this._initPrefs();
380 this._initialized = true;
381 },
383 /**
384 * Initialize the session using the state provided by SessionStartup
385 */
386 initSession: function () {
387 let state;
388 let ss = gSessionStartup;
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
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;
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);
419 if (ss.previousSessionCrashed) {
420 this._recentCrashes = (state.session &&
421 state.session.recentCrashes || 0) + 1;
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 }
441 // Update the session start time using the restored session state.
442 this._updateSessionStartTime(state);
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 }
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);
467 this._performUpgradeBackup();
469 return state;
470 },
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";
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);
490 this._prefBranch.setCharPref(PREF_UPGRADE, buildID);
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 },
501 _initPrefs : function() {
502 this._prefBranch = Services.prefs.getBranch("browser.");
504 gDebuggingEnabled = this._prefBranch.getBoolPref("sessionstore.debug");
506 Services.prefs.addObserver("browser.sessionstore.debug", () => {
507 gDebuggingEnabled = this._prefBranch.getBoolPref("sessionstore.debug");
508 }, false);
510 this._max_tabs_undo = this._prefBranch.getIntPref("sessionstore.max_tabs_undo");
511 this._prefBranch.addObserver("sessionstore.max_tabs_undo", this, true);
513 this._max_windows_undo = this._prefBranch.getIntPref("sessionstore.max_windows_undo");
514 this._prefBranch.addObserver("sessionstore.max_windows_undo", this, true);
515 },
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 }
526 // save all data for session resuming
527 if (this._sessionInitialized) {
528 SessionSaver.run();
529 }
531 // clear out priority queue in case it's still holding refs
532 TabRestoreQueue.reset();
534 // Make sure to cancel pending saves.
535 SessionSaver.cancel();
536 },
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 },
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 }
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;
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;
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 }
620 // Restore the tab icon.
621 if ("image" in tabData) {
622 win.gBrowser.setIcon(tab, tabData.image);
623 }
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 }
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;
663 delete browser.__SS_restore_data;
664 delete browser.__SS_restore_tab;
665 delete browser.__SS_data;
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 },
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 },
695 /* ........ Window Event Handlers .............. */
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 },
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 },
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;
755 // ignore windows opened while shutting down
756 if (this._loadState == STATE_QUITTING)
757 return;
759 // Assign the window a unique identifier we can use to reference
760 // internal data about the window.
761 aWindow.__SSi = this._generateWindowID();
763 let mm = aWindow.messageManager;
764 MESSAGES.forEach(msg => mm.addMessageListener(msg, this));
766 // Load the frame script after registering listeners.
767 mm.loadFrameScript("chrome://browser/content/content-sessionStore.js", true);
769 // and create its data object
770 this._windows[aWindow.__SSi] = { tabs: [], selected: 0, _closedTabs: [], busy: false };
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;
780 // perform additional initialization when the first window is loading
781 if (this._loadState == STATE_STOPPED) {
782 this._loadState = STATE_RUNNING;
783 SessionSaver.updateLastSaveTime();
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;
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;
799 // global data must be restored before restoreWindow is called so that
800 // it happens before observers are notified
801 this._globalState.setFromState(aInitialState);
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, "");
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) {
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);
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) {
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 }
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] });
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 }
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 }
909 var tabbrowser = aWindow.gBrowser;
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 },
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);
930 let windowType = aWindow.document.documentElement.getAttribute("windowtype");
932 // Ignore non-browser windows.
933 if (windowType != "navigator:browser") {
934 return;
935 }
937 if (this._sessionInitialized) {
938 this.onLoad(aWindow);
939 return;
940 }
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();
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);
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 }
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 }
973 if (this._sessionInitialized) {
974 this.onLoad(aWindow);
975 } else {
976 let initialState = this.initSession();
977 this._sessionInitialized = true;
978 this.onLoad(aWindow, initialState);
980 // Let everyone know we're done.
981 this._deferredInitialized.resolve();
982 }
983 }, console.error);
984 };
986 aWindow.addEventListener("load", onload);
987 },
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 }
1004 this._windows[aWindow.__SSi] = this._statesToRestore[aWindow.__SS_restoreID];
1005 delete this._statesToRestore[aWindow.__SS_restoreID];
1006 delete aWindow.__SS_restoreID;
1007 }
1009 // ignore windows not tracked by SessionStore
1010 if (!aWindow.__SSi || !this._windows[aWindow.__SSi]) {
1011 return;
1012 }
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);
1021 if (this.windowToFocus && this.windowToFocus == aWindow) {
1022 delete this.windowToFocus;
1023 }
1025 var tabbrowser = aWindow.gBrowser;
1027 TAB_EVENTS.forEach(function(aEvent) {
1028 tabbrowser.tabContainer.removeEventListener(aEvent, this, true);
1029 }, this);
1031 let winData = this._windows[aWindow.__SSi];
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);
1038 // update all window data for a last time
1039 this._collectWindowData(aWindow);
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 }
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
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();
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);
1065 // Determine whether the window has any tabs worth saving.
1066 let hasSaveableTabs = winData.tabs.some(this._shouldSaveTabState);
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);
1078 if (hasSaveableTabs || isLastWindow) {
1079 // we don't want to save the busy state
1080 delete winData.busy;
1082 this._closedWindows.unshift(winData);
1083 this._capClosedWindows();
1084 }
1085 }
1087 // clear this window from the list
1088 delete this._windows[aWindow.__SSi];
1090 // save the state without this window to disk
1091 this.saveStateDelayed();
1092 }
1094 for (let i = 0; i < tabbrowser.tabs.length; i++) {
1095 this.onTabRemove(aWindow, tabbrowser.tabs[i], true);
1096 }
1098 // Cache the window state until it is completely gone.
1099 DyingWindowCache.set(aWindow, winData);
1101 let mm = aWindow.messageManager;
1102 MESSAGES.forEach(msg => mm.removeMessageListener(msg, this));
1104 delete aWindow.__SSi;
1105 },
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 },
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 },
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 },
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 }
1162 if (aData != "restart") {
1163 // Throw away the previous session on shutdown
1164 LastSession.clear();
1165 }
1167 this._loadState = STATE_QUITTING; // just to be sure
1168 this._uninit();
1169 },
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 }
1209 this._clearRestoringWindows();
1210 },
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 }
1263 if (this._loadState == STATE_RUNNING) {
1264 SessionSaver.run();
1265 }
1267 this._clearRestoringWindows();
1268 },
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 },
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 },
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;
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 }
1330 if (!aNoNotification) {
1331 this.saveStateDelayed(aWindow);
1332 }
1333 },
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);
1349 // don't update our internal state if we don't have to
1350 if (this._max_tabs_undo == 0) {
1351 return;
1352 }
1354 // Flush all data queued in the content script before the tab is gone.
1355 TabState.flush(aTab.linkedBrowser);
1357 // Get the latest data for this tab (generally, from the cache)
1358 let tabState = TabState.collect(aTab);
1360 // Don't save private tabs
1361 let isPrivateWindow = PrivateBrowsingUtils.isWindowPrivate(aWindow);
1362 if (!isPrivateWindow && tabState.isPrivate) {
1363 return;
1364 }
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);
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 },
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;
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 },
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);
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 }
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 },
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 }
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 },
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 },
1440 /* ........ nsISessionStore API .............. */
1442 getBrowserState: function ssi_getBrowserState() {
1443 let state = this.getCurrentState();
1445 // Don't include the last session state in getBrowserState().
1446 delete state.lastSessionState;
1448 // Don't include any deferred initial state.
1449 delete state.deferredInitialState;
1451 return this._toJSONString(state);
1452 },
1454 setBrowserState: function ssi_setBrowserState(aState) {
1455 this._handleClosedWindows();
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 }
1468 this._browserSetState = true;
1470 // Make sure the priority queue is emptied out
1471 this._resetRestoringState();
1473 var window = this._getMostRecentBrowserWindow();
1474 if (!window) {
1475 this._restoreCount = 1;
1476 this._openWindowWithState(state);
1477 return;
1478 }
1480 // close all other browser windows
1481 this._forEachBrowserWindow(function(aWindow) {
1482 if (aWindow != window) {
1483 aWindow.close();
1484 this.onClose(aWindow);
1485 }
1486 });
1488 // make sure closed window data isn't kept
1489 this._closedWindows = [];
1491 // determine how many windows are meant to be restored
1492 this._restoreCount = state.windows ? state.windows.length : 0;
1494 // global data must be restored before restoreWindow is called so that
1495 // it happens before observers are notified
1496 this._globalState.setFromState(state);
1498 // restore to the given state
1499 this.restoreWindow(window, state, {overwriteTabs: true});
1500 },
1502 getWindowState: function ssi_getWindowState(aWindow) {
1503 if ("__SSi" in aWindow) {
1504 return this._toJSONString(this._getWindowState(aWindow));
1505 }
1507 if (DyingWindowCache.has(aWindow)) {
1508 let data = DyingWindowCache.get(aWindow);
1509 return this._toJSONString({ windows: [data] });
1510 }
1512 throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
1513 },
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 }
1520 this.restoreWindow(aWindow, aState, {overwriteTabs: aOverwrite});
1521 },
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 }
1531 let tabState = TabState.collect(aTab);
1533 return this._toJSONString(tabState);
1534 },
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 }
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 }
1560 if (aTab.linkedBrowser.__SS_restoreState) {
1561 this._resetTabRestoringState(aTab);
1562 }
1564 this._setWindowStateBusy(window);
1565 this.restoreTabs(window, [aTab], [tabState], 0);
1566 },
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 }
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);
1583 // Duplicate the tab state
1584 let tabState = TabState.clone(aTab);
1586 tabState.index += aDelta;
1587 tabState.index = Math.max(1, Math.min(tabState.index, tabState.entries.length));
1588 tabState.pinned = false;
1590 this._setWindowStateBusy(aWindow);
1591 let newTab = aTab == aWindow.gBrowser.selectedTab ?
1592 aWindow.gBrowser.addTab(null, {relatedToCurrent: true, ownerTab: aTab}) :
1593 aWindow.gBrowser.addTab();
1595 this.restoreTabs(aWindow, [newTab], [tabState], 0,
1596 true /* Load this tab right away. */);
1598 return newTab;
1599 },
1601 getClosedTabCount: function ssi_getClosedTabCount(aWindow) {
1602 if ("__SSi" in aWindow) {
1603 return this._windows[aWindow.__SSi]._closedTabs.length;
1604 }
1606 if (!DyingWindowCache.has(aWindow)) {
1607 throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
1608 }
1610 return DyingWindowCache.get(aWindow)._closedTabs.length;
1611 },
1613 getClosedTabData: function ssi_getClosedTabDataAt(aWindow) {
1614 if ("__SSi" in aWindow) {
1615 return this._toJSONString(this._windows[aWindow.__SSi]._closedTabs);
1616 }
1618 if (!DyingWindowCache.has(aWindow)) {
1619 throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
1620 }
1622 let data = DyingWindowCache.get(aWindow);
1623 return this._toJSONString(data._closedTabs);
1624 },
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 }
1631 var closedTabs = this._windows[aWindow.__SSi]._closedTabs;
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 }
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;
1643 this._setWindowStateBusy(aWindow);
1644 // create a new tab
1645 let tabbrowser = aWindow.gBrowser;
1646 let tab = tabbrowser.addTab();
1648 // restore tab content
1649 this.restoreTabs(aWindow, [tab], [closedTabState], 1);
1651 // restore the tab's position
1652 tabbrowser.moveTabTo(tab, closedTab.pos);
1654 // focus the tab's content area (bug 342432)
1655 tab.linkedBrowser.focus();
1657 return tab;
1658 },
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 }
1665 var closedTabs = this._windows[aWindow.__SSi]._closedTabs;
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 }
1673 // remove closed tab from the array
1674 closedTabs.splice(aIndex, 1);
1675 },
1677 getClosedWindowCount: function ssi_getClosedWindowCount() {
1678 return this._closedWindows.length;
1679 },
1681 getClosedWindowData: function ssi_getClosedWindowData() {
1682 return this._toJSONString(this._closedWindows);
1683 },
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 }
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 },
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 }
1704 // remove closed window from the array
1705 this._closedWindows.splice(aIndex, 1);
1706 },
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 }
1714 if (DyingWindowCache.has(aWindow)) {
1715 let data = DyingWindowCache.get(aWindow).extData || {};
1716 return data[aKey] || "";
1717 }
1719 throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
1720 },
1722 setWindowValue: function ssi_setWindowValue(aWindow, aKey, aStringValue) {
1723 if (typeof aStringValue != "string") {
1724 throw new TypeError("setWindowValue only accepts string values");
1725 }
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 },
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 },
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 },
1756 setTabValue: function ssi_setTabValue(aTab, aKey, aStringValue) {
1757 if (typeof aStringValue != "string") {
1758 throw new TypeError("setTabValue only accepts string values");
1759 }
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 }
1775 saveTo[aKey] = aStringValue;
1776 this.saveStateDelayed(aTab.ownerDocument.defaultView);
1777 },
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 }
1791 if (deleteFrom && aKey in deleteFrom) {
1792 delete deleteFrom[aKey];
1793 this.saveStateDelayed(aTab.ownerDocument.defaultView);
1794 }
1795 },
1797 getGlobalValue: function ssi_getGlobalValue(aKey) {
1798 return this._globalState.get(aKey);
1799 },
1801 setGlobalValue: function ssi_setGlobalValue(aKey, aStringValue) {
1802 if (typeof aStringValue != "string") {
1803 throw new TypeError("setGlobalValue only accepts string values");
1804 }
1806 this._globalState.set(aKey, aStringValue);
1807 this.saveStateDelayed();
1808 },
1810 deleteGlobalValue: function ssi_deleteGlobalValue(aKey) {
1811 this._globalState.delete(aKey);
1812 this.saveStateDelayed();
1813 },
1815 persistTabAttribute: function ssi_persistTabAttribute(aName) {
1816 if (TabAttributes.persist(aName)) {
1817 this.saveStateDelayed();
1818 }
1819 },
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 }
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 });
1841 let lastSessionState = LastSession.getState();
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 }
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;
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;
1861 // global data must be restored before restoreWindow is called so that
1862 // it happens before observers are notified
1863 this._globalState.setFromState(lastSessionState);
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;
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 }
1881 let [canUseWindow, canOverwriteTabs] = this._prepWindowToRestoreInto(windowToUse);
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 }
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 }
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 }
1914 if (lastSessionState.scratchpads) {
1915 ScratchpadManager.restoreSession(lastSessionState.scratchpads);
1916 }
1918 // Set data that persists between sessions
1919 this._recentCrashes = lastSessionState.session &&
1920 lastSessionState.session.recentCrashes || 0;
1922 // Update the session start time using the restored session state.
1923 this._updateSessionStartTime(lastSessionState);
1925 LastSession.clear();
1926 },
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];
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;
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);
1955 // If there are multiple groups, we don't want to use this window.
1956 if (groupsData.totalNumber > 1)
1957 return [false, false];
1958 }
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("|"));
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 }
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 }
1991 return [true, canOverwriteTabs];
1992 },
1994 /* ........ Saving Functionality .............. */
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];
2004 WINDOW_ATTRIBUTES.forEach(function(aAttr) {
2005 winData[aAttr] = this._getWindowDimension(aWindow, aAttr);
2006 }, this);
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;
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 },
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();
2032 var activeWindow = this._getMostRecentBrowserWindow();
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");
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;
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 }
2069 TelemetryStopwatch.start("FX_SESSION_RESTORE_COLLECT_COOKIES_MS");
2070 SessionCookies.update(total);
2071 TelemetryStopwatch.finish("FX_SESSION_RESTORE_COLLECT_COOKIES_MS");
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 }
2082 // shallow copy this._closedWindows to preserve current state
2083 let lastClosedWindowsCopy = this._closedWindows.slice();
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
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;
2110 let session = {
2111 lastUpdate: Date.now(),
2112 startTime: this._sessionStartTime,
2113 recentCrashes: this._recentCrashes
2114 };
2116 // get open Scratchpad window states too
2117 let scratchpads = ScratchpadManager.getSessionState();
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 };
2128 // Persist the last session if we deferred restoring it
2129 if (LastSession.canRestore) {
2130 state.lastSessionState = LastSession.getState();
2131 }
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 }
2140 return state;
2141 },
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];
2153 if (this._loadState == STATE_RUNNING) {
2154 this._collectWindowData(aWindow);
2155 }
2157 let windows = [this._windows[aWindow.__SSi]];
2158 SessionCookies.update(windows);
2160 return { windows: windows };
2161 },
2163 _collectWindowData: function ssi_collectWindowData(aWindow) {
2164 if (!this._isWindowLoaded(aWindow))
2165 return;
2166 TelemetryStopwatch.start("FX_SESSION_RESTORE_COLLECT_SINGLE_WINDOW_DATA_MS");
2168 let tabbrowser = aWindow.gBrowser;
2169 let tabs = tabbrowser.tabs;
2170 let winData = this._windows[aWindow.__SSi];
2171 let tabsData = winData.tabs = [];
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;
2179 this._updateWindowFeatures(aWindow);
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;
2187 DirtyWindows.remove(aWindow);
2188 TelemetryStopwatch.finish("FX_SESSION_RESTORE_COLLECT_SINGLE_WINDOW_DATA_MS");
2189 },
2191 /* ........ Restoring Functionality .............. */
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;
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);
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 }
2231 TelemetryStopwatch.start("FX_SESSION_RESTORE_RESTORE_WINDOW_MS");
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);
2237 if (root._closedWindows)
2238 this._closedWindows = root._closedWindows;
2240 var winData;
2241 if (!root.selectedWindow || root.selectedWindow > root.windows.length) {
2242 root.selectedWindow = 0;
2243 }
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 }
2267 var tabbrowser = aWindow.gBrowser;
2268 var openTabCount = overwriteTabs ? tabbrowser.browsers.length : -1;
2269 var newTabCount = winData.tabs.length;
2270 var tabs = [];
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;
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 }
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 }
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);
2295 let numVisibleTabs = 0;
2297 for (var t = 0; t < newTabCount; t++) {
2298 tabs.push(t < openTabCount ?
2299 tabbrowser.tabs[t] :
2300 tabbrowser.addTab("about:blank", {skipAnimation: true}));
2302 if (winData.tabs[t].pinned)
2303 tabbrowser.pinTab(tabs[t]);
2305 if (winData.tabs[t].hidden) {
2306 tabbrowser.hideTab(tabs[t]);
2307 }
2308 else {
2309 tabbrowser.showTab(tabs[t]);
2310 numVisibleTabs++;
2311 }
2312 }
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 }
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 }
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 }
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;
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 }
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 }
2369 let newClosedTabsData = winData._closedTabs || [];
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);
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 }
2388 this.restoreTabs(aWindow, tabs, winData.tabs,
2389 (overwriteTabs ? (parseInt(winData.selected || "1")) : 0));
2391 if (aState.scratchpads) {
2392 ScratchpadManager.restoreSession(aState.scratchpads);
2393 }
2395 // set smoothScroll back to the original value
2396 tabstrip.smoothScroll = smoothScroll;
2398 TelemetryStopwatch.finish("FX_SESSION_RESTORE_RESTORE_WINDOW_MS");
2400 this._sendRestoreCompletedNotifications();
2401 },
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) {
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 }
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 }
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 }
2466 // Merge the stored tabs in order.
2467 aTabs = pinnedTabs.concat(aTabs, hiddenTabs);
2468 aTabData = pinnedTabsData.concat(aTabData, hiddenTabsData);
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 }
2480 return [aTabs, aTabData];
2481 },
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 {
2501 var tabbrowser = aWindow.gBrowser;
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 }
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);
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;
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 }
2528 // Sets the tabs restoring order.
2529 [aTabs, aTabData] =
2530 this._setTabsRestoringOrder(tabbrowser, aTabs, aTabData, aSelectTab);
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];
2541 if (tabData.pinned)
2542 tabbrowser.pinTab(tab);
2543 else
2544 tabbrowser.unpinTab(tab);
2546 if (tabData.hidden)
2547 tabbrowser.hideTab(tab);
2548 else
2549 tabbrowser.showTab(tab);
2551 if (tabData.lastAccessed) {
2552 tab.lastAccessed = tabData.lastAccessed;
2553 }
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 }
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 }
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);
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);
2581 // Save the index in case we updated it above.
2582 tabData.index = activeIndex + 1;
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);
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);
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");
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 });
2613 browser.messageManager.sendAsyncMessage("SessionStore:restoreHistory",
2614 {tabData: tabData, epoch: epoch});
2616 // Restore tab attributes.
2617 if ("attributes" in tabData) {
2618 TabAttributes.set(tab, tabData.attributes);
2619 }
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 }
2631 this._setWindowStateReady(aWindow);
2632 },
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;
2656 // Make sure that this tab is removed from the priority queue.
2657 TabRestoreQueue.remove(aTab);
2659 // Increase our internal count.
2660 this._tabsRestoringCount++;
2662 // Set this tab's state to restoring
2663 browser.__SS_restoreState = TAB_STATE_RESTORING;
2664 browser.removeAttribute("pending");
2665 aTab.removeAttribute("pending");
2667 let activeIndex = tabData.index - 1;
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 }
2678 browser.__SS_restore_tab = aTab;
2680 browser.messageManager.sendAsyncMessage("SessionStore:restoreTabContent");
2681 },
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;
2696 // Don't exceed the maximum number of concurrent tab restores.
2697 if (this._tabsRestoringCount >= MAX_CONCURRENT_TAB_RESTORES)
2698 return;
2700 let tab = TabRestoreQueue.shift();
2701 if (tab) {
2702 this.restoreTabContent(tab);
2703 }
2704 },
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 });
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 }
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 },
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); }
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 }
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 },
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 },
2846 /* ........ Disk Access .............. */
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 }
2860 SessionSaver.runDelayed();
2861 },
2863 /* ........ Auxiliary Functions .............. */
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;
2877 // ms to days
2878 let sessionLength = (Date.now() - this._sessionStartTime) / MS_PER_DAY;
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 },
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");
2896 while (windowsEnum.hasMoreElements()) {
2897 var window = windowsEnum.getNext();
2898 if (window.__SSi && !window.closed) {
2899 aFunc.call(this, window);
2900 }
2901 }
2902 },
2904 /**
2905 * Returns most recent window
2906 * @returns Window reference
2907 */
2908 _getMostRecentBrowserWindow: function ssi_getMostRecentBrowserWindow() {
2909 return RecentWindow.getMostRecentBrowserWindow({ allowPopups: true });
2910 },
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");
2920 while (windowsEnum.hasMoreElements()) {
2921 var window = windowsEnum.getNext();
2922 if (window.closed) {
2923 this.onClose(window);
2924 }
2925 }
2926 },
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 = "";
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 });
2948 if (winState.isPrivate) {
2949 features += ",private";
2950 }
2952 var window =
2953 Services.ww.openWindow(null, this._prefBranch.getCharPref("chromeURL"),
2954 "_blank", features, argString);
2956 do {
2957 var ID = "window" + Math.random();
2958 } while (ID in this._statesToRestore);
2959 this._statesToRestore[(window.__SS_restoreID = ID)] = aState;
2961 return window;
2962 },
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 },
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 },
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));
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 }
3012 return !hasFirstArgument;
3013 },
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 }
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 }
3052 if (aWindow.windowState == aWindow.STATE_NORMAL) {
3053 return dimension;
3054 }
3055 return aWindow.document.documentElement.getAttribute(aAttribute) || dimension;
3056 },
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 },
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;
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;
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 }
3086 // don't automatically restore in Safe Mode
3087 if (Services.appinfo.inSafeMode)
3088 return true;
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);
3095 return max_resumed_crashes != -1 &&
3096 (aRecentCrashes > max_resumed_crashes ||
3097 sessionAge && sessionAge >= SIX_HOURS_IN_MS);
3098 },
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 },
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 },
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, {});
3158 let defaultState = { windows: [], selectedWindow: 1 };
3160 state.selectedWindow = state.selectedWindow || 1;
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
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 }
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
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();
3209 // Extract the cookies that belong with each pinned tab
3210 this._splitCookiesFromWindow(window, pinnedWindowState);
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;
3221 state.windows.splice(wIndex, 1);
3222 // We don't want to increment wIndex here.
3223 continue;
3224 }
3227 }
3228 wIndex++;
3229 }
3231 return [defaultState, state];
3232 },
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;
3244 // Get the hosts for history entries in aTargetWinState
3245 let cookieHosts = SessionCookies.getHostsForWindow(aTargetWinState);
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 },
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 },
3277 _sendRestoreCompletedNotifications: function ssi_sendRestoreCompletedNotifications() {
3278 // not all windows restored, yet
3279 if (this._restoreCount > 1) {
3280 this._restoreCount--;
3281 return;
3282 }
3284 // observers were already notified
3285 if (this._restoreCount == -1)
3286 return;
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 "");
3293 this._browserSetState = false;
3294 this._restoreCount = -1;
3295 },
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) {
3305 this._windows[aWindow.__SSi].busy = aValue;
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 },
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 },
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 },
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 },
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 },
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 },
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 },
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 },
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 },
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 },
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;
3435 // Keep the tab's previous state for later in this method
3436 let previousState = browser.__SS_restoreState;
3438 // The browser is no longer in any sort of restoring state.
3439 delete browser.__SS_restoreState;
3440 this._browserEpochs.delete(browser.permanentKey);
3442 aTab.removeAttribute("pending");
3443 browser.removeAttribute("pending");
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 },
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 },
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 },
3475 };
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: []},
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 }
3499 const PREF = "browser.sessionstore.restore_on_demand";
3500 Services.prefs.addObserver(PREF, updateValue, false);
3501 return updateValue();
3502 },
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 }
3513 const PREF = "browser.sessionstore.restore_pinned_tabs_on_demand";
3514 Services.prefs.addObserver(PREF, updateValue, false);
3515 return updateValue();
3516 },
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 }
3527 const PREF = "browser.sessionstore.restore_hidden_tabs";
3528 Services.prefs.addObserver(PREF, updateValue, false);
3529 return updateValue();
3530 }
3531 },
3533 // Resets the queue and removes all tabs.
3534 reset: function () {
3535 this.tabs = {priority: [], visible: [], hidden: []};
3536 },
3538 // Adds a tab to the queue and determines its priority bucket.
3539 add: function (tab) {
3540 let {priority, hidden, visible} = this.tabs;
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 },
3551 // Removes a given tab from the queue, if it's in there.
3552 remove: function (tab) {
3553 let {priority, hidden, visible} = this.tabs;
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);
3560 if (index == -1) {
3561 set = tab.hidden ? hidden : visible;
3562 index = set.indexOf(tab);
3563 }
3565 if (index > -1) {
3566 set.splice(index, 1);
3567 }
3568 },
3570 // Returns and removes the tab with the highest priority.
3571 shift: function () {
3572 let set;
3573 let {priority, hidden, visible} = this.tabs;
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 }
3587 return set && set.shift();
3588 },
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);
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 },
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);
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 };
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(),
3623 has: function (window) {
3624 return this._data.has(window);
3625 },
3627 get: function (window) {
3628 return this._data.get(window);
3629 },
3631 set: function (window, data) {
3632 this._data.set(window, data);
3633 },
3635 remove: function (window) {
3636 this._data.delete(window);
3637 }
3638 };
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(),
3645 has: function (window) {
3646 return this._data.has(window);
3647 },
3649 add: function (window) {
3650 return this._data.set(window, true);
3651 },
3653 remove: function (window) {
3654 this._data.delete(window);
3655 },
3657 clear: function (window) {
3658 this._data.clear();
3659 }
3660 };
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,
3669 get canRestore() {
3670 return !!this._state;
3671 },
3673 getState: function () {
3674 return this._state;
3675 },
3677 setState: function (state) {
3678 this._state = state;
3679 },
3681 clear: function () {
3682 if (this._state) {
3683 this._state = null;
3684 Services.obs.notifyObservers(null, NOTIFY_LAST_SESSION_CLEARED, null);
3685 }
3686 }
3687 };