browser/components/sessionstore/src/SessionStore.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

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();
  1004       this._windows[aWindow.__SSi] = this._statesToRestore[aWindow.__SS_restoreID];
  1005       delete this._statesToRestore[aWindow.__SS_restoreID];
  1006       delete aWindow.__SS_restoreID;
  1009     // ignore windows not tracked by SessionStore
  1010     if (!aWindow.__SSi || !this._windows[aWindow.__SSi]) {
  1011       return;
  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;
  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]);
  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();
  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();
  1094     for (let i = 0; i < tabbrowser.tabs.length; i++) {
  1095       this.onTabRemove(aWindow, tabbrowser.tabs[i], true);
  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");
  1162     if (aData != "restart") {
  1163       // Throw away the previous session on shutdown
  1164       LastSession.clear();
  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];
  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();
  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;
  1223       return aEntry.children && aEntry.children.some(containsDomain, this);
  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);
  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--;
  1249       if (openTabs.length == 0) {
  1250         this._closedWindows.splice(ix, 1);
  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;
  1263     if (this._loadState == STATE_RUNNING) {
  1264       SessionSaver.run();
  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);
  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;
  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);
  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();
  1330     if (!aNoNotification) {
  1331       this.saveStateDelayed(aWindow);
  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;
  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;
  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);
  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);
  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();
  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);
  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);
  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);
  1464     if (!state.windows) {
  1465       throw Components.Exception("No windows", Cr.NS_ERROR_INVALID_ARG);
  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;
  1480     // close all other browser windows
  1481     this._forEachBrowserWindow(function(aWindow) {
  1482       if (aWindow != window) {
  1483         aWindow.close();
  1484         this.onClose(aWindow);
  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));
  1507     if (DyingWindowCache.has(aWindow)) {
  1508       let data = DyingWindowCache.get(aWindow);
  1509       return this._toJSONString({ windows: [data] });
  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);
  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);
  1527     if (!aTab.ownerDocument.defaultView.__SSi) {
  1528       throw Components.Exception("Default view is not tracked", Cr.NS_ERROR_INVALID_ARG);
  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);
  1545     if (typeof tabState != "object") {
  1546       throw Components.Exception("Not an object", Cr.NS_ERROR_INVALID_ARG);
  1548     if (!("entries" in tabState)) {
  1549       throw Components.Exception("Invalid state object: no entries", Cr.NS_ERROR_INVALID_ARG);
  1551     if (!aTab.ownerDocument) {
  1552       throw Components.Exception("Invalid tab object: no ownerDocument", Cr.NS_ERROR_INVALID_ARG);
  1555     let window = aTab.ownerDocument.defaultView;
  1556     if (!("__SSi" in window)) {
  1557       throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
  1560     if (aTab.linkedBrowser.__SS_restoreState) {
  1561       this._resetTabRestoringState(aTab);
  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);
  1572     if (!aTab.ownerDocument.defaultView.__SSi) {
  1573       throw Components.Exception("Default view is not tracked", Cr.NS_ERROR_INVALID_ARG);
  1575     if (!aWindow.getBrowser) {
  1576       throw Components.Exception("Invalid window object: no getBrowser", Cr.NS_ERROR_INVALID_ARG);
  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;
  1606     if (!DyingWindowCache.has(aWindow)) {
  1607       throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
  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);
  1618     if (!DyingWindowCache.has(aWindow)) {
  1619       throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
  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);
  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);
  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);
  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);
  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);
  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);
  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] || "";
  1714     if (DyingWindowCache.has(aWindow)) {
  1715       let data = DyingWindowCache.get(aWindow).extData || {};
  1716       return data[aKey] || "";
  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");
  1727     if (!("__SSi" in aWindow)) {
  1728       throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
  1730     if (!this._windows[aWindow.__SSi].extData) {
  1731       this._windows[aWindow.__SSi].extData = {};
  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;
  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;
  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");
  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;
  1767     else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) {
  1768       saveTo = aTab.linkedBrowser.__SS_data.extData;
  1770     else {
  1771       aTab.__SS_extdata = {};
  1772       saveTo = aTab.__SS_extdata;
  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;
  1787     else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) {
  1788       deleteFrom = aTab.linkedBrowser.__SS_data.extData;
  1791     if (deleteFrom && aKey in deleteFrom) {
  1792       delete deleteFrom[aKey];
  1793       this.saveStateDelayed(aTab.ownerDocument.defaultView);
  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");
  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();
  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");
  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);
  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;
  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);
  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);
  1903       else {
  1904         this._openWindowWithState({ windows: [winState] });
  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();
  1914     if (lastSessionState.scratchpads) {
  1915       ScratchpadManager.restoreSession(lastSessionState.scratchpads);
  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.
  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];
  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);
  1981     if (tabbrowser.tabs.length == removableTabs.length) {
  1982       canOverwriteTabs = true;
  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 });
  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);
  2043         else { // always update the window features (whose change alone never triggers a save operation)
  2044           this._updateWindowFeatures(aWindow);
  2046       });
  2047       DirtyWindows.clear();
  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++;
  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++;
  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)
  2099 #endif
  2101     if (activeWindow) {
  2102       this.activeWindowSSiCache = activeWindow.__SSi || "";
  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();
  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;
  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);
  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));
  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;
  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
  2225     catch (ex) { // invalid state object - don't restore anything
  2226       debug(ex);
  2227       this._sendRestoreCompletedNotifications();
  2228       return;
  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;
  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;
  2256     winData = root.windows[0];
  2257     if (!winData.tabs) {
  2258       winData.tabs = [];
  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 = [];
  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]);
  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);
  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]);
  2308       else {
  2309         tabbrowser.showTab(tabs[t]);
  2310         numVisibleTabs++;
  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);
  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]);
  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);
  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);
  2353     if (overwriteTabs) {
  2354       this.restoreWindowFeatures(aWindow, winData);
  2355       delete this._windows[aWindow.__SSi].extData;
  2357     if (winData.cookies) {
  2358       this.restoreCookies(winData.cookies);
  2360     if (winData.extData) {
  2361       if (!this._windows[aWindow.__SSi].extData) {
  2362         this._windows[aWindow.__SSi].extData = {};
  2364       for (var key in winData.extData) {
  2365         this._windows[aWindow.__SSi].extData[key] = winData.extData[key];
  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);
  2388     this.restoreTabs(aWindow, tabs, winData.tabs,
  2389       (overwriteTabs ? (parseInt(winData.selected || "1")) : 0));
  2391     if (aState.scratchpads) {
  2392       ScratchpadManager.restoreSession(aState.scratchpads);
  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];
  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]);
  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;
  2459           aTabs = aTabs.splice(firstVisibleTab, maxVisibleTabs).concat(aTabs);
  2460           aTabData =
  2461             aTabData.splice(firstVisibleTab, maxVisibleTabs).concat(aTabData);
  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);
  2477       aTabBrowser.selectedTab = selectedTab;
  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)
  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;
  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;
  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;
  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));
  2560       if (!tabData.entries) {
  2561         tabData.entries = [];
  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;
  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);
  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();
  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.
  2646    * @param aTab
  2647    *        the tab to restore
  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 = {};
  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);
  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");
  2726     else {
  2727       delete this._windows[aWindow.__SSi].isPopup;
  2728       if (aWindow.gURLBar) {
  2729         aWindow.gURLBar.readOnly = false;
  2730         aWindow.gURLBar.setAttribute("enablehistory", "true");
  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;
  2774       if (aHeight > screenHeight.value) {
  2775         aHeight = screenHeight.value;
  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;
  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;
  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);
  2798     if (!isNaN(aLeft) && !isNaN(aTop) && (aLeft != win_("screenX") || aTop != win_("screenY"))) {
  2799       aWindow.moveTo(aLeft, aTop);
  2801     if (aSizeMode && win_("sizemode") != aSizeMode)
  2803       switch (aSizeMode)
  2805       case "maximized":
  2806         aWindow.maximize();
  2807         break;
  2808       case "minimized":
  2809         aWindow.minimize();
  2810         break;
  2811       case "normal":
  2812         aWindow.restore();
  2813         break;
  2816     var sidebar = aWindow.document.getElementById("sidebar-box");
  2817     if (sidebar.getAttribute("sidebarcommand") != aSidebar) {
  2818       aWindow.toggleSidebar(aSidebar);
  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();
  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);
  2842       catch (ex) { console.error(ex); } // don't let a single cookie stop recovering
  2844   },
  2846   /* ........ Disk Access .............. */
  2848   /**
  2849    * Save the current session state to disk, after a delay.
  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);
  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.
  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);
  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);
  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);
  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";
  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.
  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;
  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;
  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";
  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;
  3052     if (aWindow.windowState == aWindow.STATE_NORMAL) {
  3053       return dimension;
  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;
  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;
  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
  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.
  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).
  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;
  3184         tIndex++;
  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];
  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;
  3228       wIndex++;
  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;
  3260       cIndex++;
  3262   },
  3264   /**
  3265    * Converts a JavaScript object into a JSON string
  3266    * (see http://www.json.org/ for more information).
  3268    * The inverse operation consists of JSON.parse(JSON_string).
  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;
  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;
  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
  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];
  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.
  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;
  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).
  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);
  3454   },
  3456   _resetTabRestoringState: function (tab) {
  3457     let browser = tab.linkedBrowser;
  3458     if (browser.__SS_restoreState) {
  3459       browser.messageManager.sendAsyncMessage("SessionStore:resetRestore", {});
  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;
  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;
  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;
  3527       const PREF = "browser.sessionstore.restore_hidden_tabs";
  3528       Services.prefs.addObserver(PREF, updateValue, false);
  3529       return updateValue();
  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);
  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);
  3565     if (index > -1) {
  3566       set.splice(index, 1);
  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;
  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");
  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");
  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);
  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();
  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);
  3687 };

mercurial