1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5"use strict";
6
7this.EXPORTED_SYMBOLS = ["SessionStore"];
8
9const Cu = Components.utils;
10const Cc = Components.classes;
11const Ci = Components.interfaces;
12const Cr = Components.results;
13
14// Current version of the format used by Session Restore.
15const FORMAT_VERSION = 1;
16
17const TAB_STATE_NEEDS_RESTORE = 1;
18const TAB_STATE_RESTORING = 2;
19const TAB_STATE_WILL_RESTORE = 3;
20
21// A new window has just been restored. At this stage, tabs are generally
22// not restored.
23const NOTIFY_SINGLE_WINDOW_RESTORED = "sessionstore-single-window-restored";
24const NOTIFY_WINDOWS_RESTORED = "sessionstore-windows-restored";
25const NOTIFY_BROWSER_STATE_RESTORED = "sessionstore-browser-state-restored";
26const NOTIFY_LAST_SESSION_CLEARED = "sessionstore-last-session-cleared";
27const NOTIFY_RESTORING_ON_STARTUP = "sessionstore-restoring-on-startup";
28const NOTIFY_INITIATING_MANUAL_RESTORE = "sessionstore-initiating-manual-restore";
29
30const NOTIFY_TAB_RESTORED = "sessionstore-debug-tab-restored"; // WARNING: debug-only
31
32// Maximum number of tabs to restore simultaneously. Previously controlled by
33// the browser.sessionstore.max_concurrent_tabs pref.
34const MAX_CONCURRENT_TAB_RESTORES = 3;
35
36// Amount (in CSS px) by which we allow window edges to be off-screen
37// when restoring a window, before we override the saved position to
38// pull the window back within the available screen area.
39const SCREEN_EDGE_SLOP = 8;
40
41// global notifications observed
42const OBSERVING = [
43  "browser-window-before-show", "domwindowclosed",
44  "quit-application-granted", "browser-lastwindow-close-granted",
45  "quit-application", "browser:purge-session-history",
46  "browser:purge-domain-data",
47  "idle-daily",
48];
49
50// XUL Window properties to (re)store
51// Restored in restoreDimensions()
52const WINDOW_ATTRIBUTES = ["width", "height", "screenX", "screenY", "sizemode"];
53
54// Hideable window features to (re)store
55// Restored in restoreWindowFeatures()
56const WINDOW_HIDEABLE_FEATURES = [
57  "menubar", "toolbar", "locationbar", "personalbar", "statusbar", "scrollbars"
58];
59
60// Messages that will be received via the Frame Message Manager.
61const MESSAGES = [
62  // The content script sends us data that has been invalidated and needs to
63  // be saved to disk.
64  "SessionStore:update",
65
66  // The restoreHistory code has run. This is a good time to run SSTabRestoring.
67  "SessionStore:restoreHistoryComplete",
68
69  // The load for the restoring tab has begun. We update the URL bar at this
70  // time; if we did it before, the load would overwrite it.
71  "SessionStore:restoreTabContentStarted",
72
73  // All network loads for a restoring tab are done, so we should
74  // consider restoring another tab in the queue. The document has
75  // been restored, and forms have been filled. We trigger
76  // SSTabRestored at this time.
77  "SessionStore:restoreTabContentComplete",
78
79  // A crashed tab was revived by navigating to a different page. Remove its
80  // browser from the list of crashed browsers to stop ignoring its messages.
81  "SessionStore:crashedTabRevived",
82
83  // The content script encountered an error.
84  "SessionStore:error",
85];
86
87// The list of messages we accept from <xul:browser>s that have no tab
88// assigned, or whose windows have gone away. Those are for example the
89// ones that preload about:newtab pages, or from browsers where the window
90// has just been closed.
91const NOTAB_MESSAGES = new Set([
92  // For a description see above.
93  "SessionStore:crashedTabRevived",
94
95  // For a description see above.
96  "SessionStore:update",
97
98  // For a description see above.
99  "SessionStore:error",
100]);
101
102// The list of messages we accept without an "epoch" parameter.
103// See getCurrentEpoch() and friends to find out what an "epoch" is.
104const NOEPOCH_MESSAGES = new Set([
105  // For a description see above.
106  "SessionStore:crashedTabRevived",
107
108  // For a description see above.
109  "SessionStore:error",
110]);
111
112// The list of messages we want to receive even during the short period after a
113// frame has been removed from the DOM and before its frame script has finished
114// unloading.
115const CLOSED_MESSAGES = new Set([
116  // For a description see above.
117  "SessionStore:crashedTabRevived",
118
119  // For a description see above.
120  "SessionStore:update",
121
122  // For a description see above.
123  "SessionStore:error",
124]);
125
126// These are tab events that we listen to.
127const TAB_EVENTS = [
128  "TabOpen", "TabBrowserInserted", "TabClose", "TabSelect", "TabShow", "TabHide", "TabPinned",
129  "TabUnpinned"
130];
131
132const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
133
134Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm", this);
135Cu.import("resource://gre/modules/Promise.jsm", this);
136Cu.import("resource://gre/modules/Services.jsm", this);
137Cu.import("resource://gre/modules/Task.jsm", this);
138Cu.import("resource://gre/modules/TelemetryStopwatch.jsm", this);
139Cu.import("resource://gre/modules/TelemetryTimestamps.jsm", this);
140Cu.import("resource://gre/modules/Timer.jsm", this);
141Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
142Cu.import("resource://gre/modules/debug.js", this);
143Cu.import("resource://gre/modules/osfile.jsm", this);
144
145XPCOMUtils.defineLazyServiceGetter(this, "gSessionStartup",
146  "@mozilla.org/browser/sessionstartup;1", "nsISessionStartup");
147XPCOMUtils.defineLazyServiceGetter(this, "gScreenManager",
148  "@mozilla.org/gfx/screenmanager;1", "nsIScreenManager");
149XPCOMUtils.defineLazyServiceGetter(this, "Telemetry",
150  "@mozilla.org/base/telemetry;1", "nsITelemetry");
151XPCOMUtils.defineLazyModuleGetter(this, "console",
152  "resource://gre/modules/Console.jsm");
153XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
154  "resource:///modules/RecentWindow.jsm");
155
156XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
157  "resource://gre/modules/AppConstants.jsm");
158XPCOMUtils.defineLazyModuleGetter(this, "GlobalState",
159  "resource:///modules/sessionstore/GlobalState.jsm");
160XPCOMUtils.defineLazyModuleGetter(this, "PrivacyFilter",
161  "resource:///modules/sessionstore/PrivacyFilter.jsm");
162XPCOMUtils.defineLazyModuleGetter(this, "RunState",
163  "resource:///modules/sessionstore/RunState.jsm");
164XPCOMUtils.defineLazyModuleGetter(this, "ScratchpadManager",
165  "resource://devtools/client/scratchpad/scratchpad-manager.jsm");
166XPCOMUtils.defineLazyModuleGetter(this, "SessionSaver",
167  "resource:///modules/sessionstore/SessionSaver.jsm");
168XPCOMUtils.defineLazyModuleGetter(this, "SessionCookies",
169  "resource:///modules/sessionstore/SessionCookies.jsm");
170XPCOMUtils.defineLazyModuleGetter(this, "SessionFile",
171  "resource:///modules/sessionstore/SessionFile.jsm");
172XPCOMUtils.defineLazyModuleGetter(this, "TabAttributes",
173  "resource:///modules/sessionstore/TabAttributes.jsm");
174XPCOMUtils.defineLazyModuleGetter(this, "TabCrashHandler",
175  "resource:///modules/ContentCrashHandlers.jsm");
176XPCOMUtils.defineLazyModuleGetter(this, "TabState",
177  "resource:///modules/sessionstore/TabState.jsm");
178XPCOMUtils.defineLazyModuleGetter(this, "TabStateCache",
179  "resource:///modules/sessionstore/TabStateCache.jsm");
180XPCOMUtils.defineLazyModuleGetter(this, "TabStateFlusher",
181  "resource:///modules/sessionstore/TabStateFlusher.jsm");
182XPCOMUtils.defineLazyModuleGetter(this, "Utils",
183  "resource://gre/modules/sessionstore/Utils.jsm");
184XPCOMUtils.defineLazyModuleGetter(this, "ViewSourceBrowser",
185  "resource://gre/modules/ViewSourceBrowser.jsm");
186XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
187  "resource://gre/modules/AsyncShutdown.jsm");
188
189/**
190 * |true| if we are in debug mode, |false| otherwise.
191 * Debug mode is controlled by preference browser.sessionstore.debug
192 */
193var gDebuggingEnabled = false;
194function debug(aMsg) {
195  if (gDebuggingEnabled) {
196    aMsg = ("SessionStore: " + aMsg).replace(/\S{80}/g, "$&\n");
197    Services.console.logStringMessage(aMsg);
198  }
199}
200
201this.SessionStore = {
202  get promiseInitialized() {
203    return SessionStoreInternal.promiseInitialized;
204  },
205
206  get canRestoreLastSession() {
207    return SessionStoreInternal.canRestoreLastSession;
208  },
209
210  set canRestoreLastSession(val) {
211    SessionStoreInternal.canRestoreLastSession = val;
212  },
213
214  get lastClosedObjectType() {
215    return SessionStoreInternal.lastClosedObjectType;
216  },
217
218  init: function ss_init() {
219    SessionStoreInternal.init();
220  },
221
222  getBrowserState: function ss_getBrowserState() {
223    return SessionStoreInternal.getBrowserState();
224  },
225
226  setBrowserState: function ss_setBrowserState(aState) {
227    SessionStoreInternal.setBrowserState(aState);
228  },
229
230  getWindowState: function ss_getWindowState(aWindow) {
231    return SessionStoreInternal.getWindowState(aWindow);
232  },
233
234  setWindowState: function ss_setWindowState(aWindow, aState, aOverwrite) {
235    SessionStoreInternal.setWindowState(aWindow, aState, aOverwrite);
236  },
237
238  getTabState: function ss_getTabState(aTab) {
239    return SessionStoreInternal.getTabState(aTab);
240  },
241
242  setTabState: function ss_setTabState(aTab, aState) {
243    SessionStoreInternal.setTabState(aTab, aState);
244  },
245
246  duplicateTab: function ss_duplicateTab(aWindow, aTab, aDelta = 0) {
247    return SessionStoreInternal.duplicateTab(aWindow, aTab, aDelta);
248  },
249
250  getClosedTabCount: function ss_getClosedTabCount(aWindow) {
251    return SessionStoreInternal.getClosedTabCount(aWindow);
252  },
253
254  getClosedTabData: function ss_getClosedTabData(aWindow, aAsString = true) {
255    return SessionStoreInternal.getClosedTabData(aWindow, aAsString);
256  },
257
258  undoCloseTab: function ss_undoCloseTab(aWindow, aIndex) {
259    return SessionStoreInternal.undoCloseTab(aWindow, aIndex);
260  },
261
262  forgetClosedTab: function ss_forgetClosedTab(aWindow, aIndex) {
263    return SessionStoreInternal.forgetClosedTab(aWindow, aIndex);
264  },
265
266  getClosedWindowCount: function ss_getClosedWindowCount() {
267    return SessionStoreInternal.getClosedWindowCount();
268  },
269
270  getClosedWindowData: function ss_getClosedWindowData(aAsString = true) {
271    return SessionStoreInternal.getClosedWindowData(aAsString);
272  },
273
274  undoCloseWindow: function ss_undoCloseWindow(aIndex) {
275    return SessionStoreInternal.undoCloseWindow(aIndex);
276  },
277
278  forgetClosedWindow: function ss_forgetClosedWindow(aIndex) {
279    return SessionStoreInternal.forgetClosedWindow(aIndex);
280  },
281
282  getWindowValue: function ss_getWindowValue(aWindow, aKey) {
283    return SessionStoreInternal.getWindowValue(aWindow, aKey);
284  },
285
286  setWindowValue: function ss_setWindowValue(aWindow, aKey, aStringValue) {
287    SessionStoreInternal.setWindowValue(aWindow, aKey, aStringValue);
288  },
289
290  deleteWindowValue: function ss_deleteWindowValue(aWindow, aKey) {
291    SessionStoreInternal.deleteWindowValue(aWindow, aKey);
292  },
293
294  getTabValue: function ss_getTabValue(aTab, aKey) {
295    return SessionStoreInternal.getTabValue(aTab, aKey);
296  },
297
298  setTabValue: function ss_setTabValue(aTab, aKey, aStringValue) {
299    SessionStoreInternal.setTabValue(aTab, aKey, aStringValue);
300  },
301
302  deleteTabValue: function ss_deleteTabValue(aTab, aKey) {
303    SessionStoreInternal.deleteTabValue(aTab, aKey);
304  },
305
306  getGlobalValue: function ss_getGlobalValue(aKey) {
307    return SessionStoreInternal.getGlobalValue(aKey);
308  },
309
310  setGlobalValue: function ss_setGlobalValue(aKey, aStringValue) {
311    SessionStoreInternal.setGlobalValue(aKey, aStringValue);
312  },
313
314  deleteGlobalValue: function ss_deleteGlobalValue(aKey) {
315    SessionStoreInternal.deleteGlobalValue(aKey);
316  },
317
318  persistTabAttribute: function ss_persistTabAttribute(aName) {
319    SessionStoreInternal.persistTabAttribute(aName);
320  },
321
322  restoreLastSession: function ss_restoreLastSession() {
323    SessionStoreInternal.restoreLastSession();
324  },
325
326  getCurrentState: function (aUpdateAll) {
327    return SessionStoreInternal.getCurrentState(aUpdateAll);
328  },
329
330  reviveCrashedTab(aTab) {
331    return SessionStoreInternal.reviveCrashedTab(aTab);
332  },
333
334  reviveAllCrashedTabs() {
335    return SessionStoreInternal.reviveAllCrashedTabs();
336  },
337
338  navigateAndRestore(tab, loadArguments, historyIndex) {
339    return SessionStoreInternal.navigateAndRestore(tab, loadArguments, historyIndex);
340  },
341
342  getSessionHistory(tab, updatedCallback) {
343    return SessionStoreInternal.getSessionHistory(tab, updatedCallback);
344  },
345
346  undoCloseById(aClosedId) {
347    return SessionStoreInternal.undoCloseById(aClosedId);
348  },
349
350  /**
351   * Determines whether the passed version number is compatible with
352   * the current version number of the SessionStore.
353   *
354   * @param version The format and version of the file, as an array, e.g.
355   * ["sessionrestore", 1]
356   */
357  isFormatVersionCompatible(version) {
358    if (!version) {
359      return false;
360    }
361    if (!Array.isArray(version)) {
362      // Improper format.
363      return false;
364    }
365    if (version[0] != "sessionrestore") {
366      // Not a Session Restore file.
367      return false;
368    }
369    let number = Number.parseFloat(version[1]);
370    if (Number.isNaN(number)) {
371      return false;
372    }
373    return number <= FORMAT_VERSION;
374  },
375};
376
377// Freeze the SessionStore object. We don't want anyone to modify it.
378Object.freeze(SessionStore);
379
380var SessionStoreInternal = {
381  QueryInterface: XPCOMUtils.generateQI([
382    Ci.nsIDOMEventListener,
383    Ci.nsIObserver,
384    Ci.nsISupportsWeakReference
385  ]),
386
387  _globalState: new GlobalState(),
388
389  // A counter to be used to generate a unique ID for each closed tab or window.
390  _nextClosedId: 0,
391
392  // During the initial restore and setBrowserState calls tracks the number of
393  // windows yet to be restored
394  _restoreCount: -1,
395
396  // For each <browser> element, records the current epoch.
397  _browserEpochs: new WeakMap(),
398
399  // Any browsers that fires the oop-browser-crashed event gets stored in
400  // here - that way we know which browsers to ignore messages from (until
401  // they get restored).
402  _crashedBrowsers: new WeakSet(),
403
404  // A map (xul:browser -> nsIFrameLoader) that maps a browser to the last
405  // associated frameLoader we heard about.
406  _lastKnownFrameLoader: new WeakMap(),
407
408  // A map (xul:browser -> object) that maps a browser associated with a
409  // recently closed tab to all its necessary state information we need to
410  // properly handle final update message.
411  _closedTabs: new WeakMap(),
412
413  // A map (xul:browser -> object) that maps a browser associated with a
414  // recently closed tab due to a window closure to the tab state information
415  // that is being stored in _closedWindows for that tab.
416  _closedWindowTabs: new WeakMap(),
417
418  // A set of window data that has the potential to be saved in the _closedWindows
419  // array for the session. We will remove window data from this set whenever
420  // forgetClosedWindow is called for the window, or when session history is
421  // purged, so that we don't accidentally save that data after the flush has
422  // completed. Closed tabs use a more complicated mechanism for this particular
423  // problem. When forgetClosedTab is called, the browser is removed from the
424  // _closedTabs map, so its data is not recorded. In the purge history case,
425  // the closedTabs array per window is overwritten so that once the flush is
426  // complete, the tab would only ever add itself to an array that SessionStore
427  // no longer cares about. Bug 1230636 has been filed to make the tab case
428  // work more like the window case, which is more explicit, and easier to
429  // reason about.
430  _saveableClosedWindowData: new WeakSet(),
431
432  // A map (xul:browser -> object) that maps a browser that is switching
433  // remoteness via navigateAndRestore, to the loadArguments that were
434  // most recently passed when calling navigateAndRestore.
435  _remotenessChangingBrowsers: new WeakMap(),
436
437  // whether a setBrowserState call is in progress
438  _browserSetState: false,
439
440  // time in milliseconds when the session was started (saved across sessions),
441  // defaults to now if no session was restored or timestamp doesn't exist
442  _sessionStartTime: Date.now(),
443
444  // states for all currently opened windows
445  _windows: {},
446
447  // counter for creating unique window IDs
448  _nextWindowID: 0,
449
450  // states for all recently closed windows
451  _closedWindows: [],
452
453  // collection of session states yet to be restored
454  _statesToRestore: {},
455
456  // counts the number of crashes since the last clean start
457  _recentCrashes: 0,
458
459  // whether the last window was closed and should be restored
460  _restoreLastWindow: false,
461
462  // number of tabs currently restoring
463  _tabsRestoringCount: 0,
464
465  // When starting Firefox with a single private window, this is the place
466  // where we keep the session we actually wanted to restore in case the user
467  // decides to later open a non-private window as well.
468  _deferredInitialState: null,
469
470  // A promise resolved once initialization is complete
471  _deferredInitialized: (function () {
472    let deferred = {};
473
474    deferred.promise = new Promise((resolve, reject) => {
475      deferred.resolve = resolve;
476      deferred.reject = reject;
477    });
478
479    return deferred;
480  })(),
481
482  // Whether session has been initialized
483  _sessionInitialized: false,
484
485  // Promise that is resolved when we're ready to initialize
486  // and restore the session.
487  _promiseReadyForInitialization: null,
488
489  // Keep busy state counters per window.
490  _windowBusyStates: new WeakMap(),
491
492  /**
493   * A promise fulfilled once initialization is complete.
494   */
495  get promiseInitialized() {
496    return this._deferredInitialized.promise;
497  },
498
499  get canRestoreLastSession() {
500    return LastSession.canRestore;
501  },
502
503  set canRestoreLastSession(val) {
504    // Cheat a bit; only allow false.
505    if (!val) {
506      LastSession.clear();
507    }
508  },
509
510  /**
511   * Returns a string describing the last closed object, either "tab" or "window".
512   *
513   * This was added to support the sessions.restore WebExtensions API.
514   */
515  get lastClosedObjectType() {
516    if (this._closedWindows.length) {
517      // Since there are closed windows, we need to check if there's a closed tab
518      // in one of the currently open windows that was closed after the
519      // last-closed window.
520      let tabTimestamps = [];
521      let windowsEnum = Services.wm.getEnumerator("navigator:browser");
522      while (windowsEnum.hasMoreElements()) {
523        let window = windowsEnum.getNext();
524        let windowState = this._windows[window.__SSi];
525        if (windowState && windowState._closedTabs[0]) {
526          tabTimestamps.push(windowState._closedTabs[0].closedAt);
527        }
528      }
529      if (!tabTimestamps.length ||
530          (tabTimestamps.sort((a, b) => b - a)[0] < this._closedWindows[0].closedAt)) {
531        return "window";
532      }
533    }
534    return "tab";
535  },
536
537  /**
538   * Initialize the sessionstore service.
539   */
540  init: function () {
541    if (this._initialized) {
542      throw new Error("SessionStore.init() must only be called once!");
543    }
544
545    TelemetryTimestamps.add("sessionRestoreInitialized");
546    OBSERVING.forEach(function(aTopic) {
547      Services.obs.addObserver(this, aTopic, true);
548    }, this);
549
550    this._initPrefs();
551    this._initialized = true;
552  },
553
554  /**
555   * Initialize the session using the state provided by SessionStartup
556   */
557  initSession: function () {
558    TelemetryStopwatch.start("FX_SESSION_RESTORE_STARTUP_INIT_SESSION_MS");
559    let state;
560    let ss = gSessionStartup;
561
562    if (ss.doRestore() ||
563        ss.sessionType == Ci.nsISessionStartup.DEFER_SESSION) {
564      state = ss.state;
565    }
566
567    if (state) {
568      try {
569        // If we're doing a DEFERRED session, then we want to pull pinned tabs
570        // out so they can be restored.
571        if (ss.sessionType == Ci.nsISessionStartup.DEFER_SESSION) {
572          let [iniState, remainingState] = this._prepDataForDeferredRestore(state);
573          // If we have a iniState with windows, that means that we have windows
574          // with app tabs to restore.
575          if (iniState.windows.length)
576            state = iniState;
577          else
578            state = null;
579
580          if (remainingState.windows.length) {
581            LastSession.setState(remainingState);
582          }
583        }
584        else {
585          // Get the last deferred session in case the user still wants to
586          // restore it
587          LastSession.setState(state.lastSessionState);
588
589          if (ss.previousSessionCrashed) {
590            this._recentCrashes = (state.session &&
591                                   state.session.recentCrashes || 0) + 1;
592
593            if (this._needsRestorePage(state, this._recentCrashes)) {
594              // replace the crashed session with a restore-page-only session
595              let url = "about:sessionrestore";
596              let formdata = {id: {sessionData: state}, url};
597              state = { windows: [{ tabs: [{ entries: [{url}], formdata }] }] };
598            } else if (this._hasSingleTabWithURL(state.windows,
599                                                 "about:welcomeback")) {
600              // On a single about:welcomeback URL that crashed, replace about:welcomeback
601              // with about:sessionrestore, to make clear to the user that we crashed.
602              state.windows[0].tabs[0].entries[0].url = "about:sessionrestore";
603            }
604          }
605
606          // Update the session start time using the restored session state.
607          this._updateSessionStartTime(state);
608
609          // make sure that at least the first window doesn't have anything hidden
610          delete state.windows[0].hidden;
611          // Since nothing is hidden in the first window, it cannot be a popup
612          delete state.windows[0].isPopup;
613          // We don't want to minimize and then open a window at startup.
614          if (state.windows[0].sizemode == "minimized")
615            state.windows[0].sizemode = "normal";
616          // clear any lastSessionWindowID attributes since those don't matter
617          // during normal restore
618          state.windows.forEach(function(aWindow) {
619            delete aWindow.__lastSessionWindowID;
620          });
621        }
622      }
623      catch (ex) { debug("The session file is invalid: " + ex); }
624    }
625
626    // at this point, we've as good as resumed the session, so we can
627    // clear the resume_session_once flag, if it's set
628    if (!RunState.isQuitting &&
629        this._prefBranch.getBoolPref("sessionstore.resume_session_once"))
630      this._prefBranch.setBoolPref("sessionstore.resume_session_once", false);
631
632    TelemetryStopwatch.finish("FX_SESSION_RESTORE_STARTUP_INIT_SESSION_MS");
633    return state;
634  },
635
636  _initPrefs : function() {
637    this._prefBranch = Services.prefs.getBranch("browser.");
638
639    gDebuggingEnabled = this._prefBranch.getBoolPref("sessionstore.debug");
640
641    Services.prefs.addObserver("browser.sessionstore.debug", () => {
642      gDebuggingEnabled = this._prefBranch.getBoolPref("sessionstore.debug");
643    }, false);
644
645    this._max_tabs_undo = this._prefBranch.getIntPref("sessionstore.max_tabs_undo");
646    this._prefBranch.addObserver("sessionstore.max_tabs_undo", this, true);
647
648    this._max_windows_undo = this._prefBranch.getIntPref("sessionstore.max_windows_undo");
649    this._prefBranch.addObserver("sessionstore.max_windows_undo", this, true);
650  },
651
652  /**
653   * Called on application shutdown, after notifications:
654   * quit-application-granted, quit-application
655   */
656  _uninit: function ssi_uninit() {
657    if (!this._initialized) {
658      throw new Error("SessionStore is not initialized.");
659    }
660
661    // Prepare to close the session file and write the last state.
662    RunState.setClosing();
663
664    // save all data for session resuming
665    if (this._sessionInitialized) {
666      SessionSaver.run();
667    }
668
669    // clear out priority queue in case it's still holding refs
670    TabRestoreQueue.reset();
671
672    // Make sure to cancel pending saves.
673    SessionSaver.cancel();
674  },
675
676  /**
677   * Handle notifications
678   */
679  observe: function ssi_observe(aSubject, aTopic, aData) {
680    switch (aTopic) {
681      case "browser-window-before-show": // catch new windows
682        this.onBeforeBrowserWindowShown(aSubject);
683        break;
684      case "domwindowclosed": // catch closed windows
685        this.onClose(aSubject);
686        break;
687      case "quit-application-granted":
688        let syncShutdown = aData == "syncShutdown";
689        this.onQuitApplicationGranted(syncShutdown);
690        break;
691      case "browser-lastwindow-close-granted":
692        this.onLastWindowCloseGranted();
693        break;
694      case "quit-application":
695        this.onQuitApplication(aData);
696        break;
697      case "browser:purge-session-history": // catch sanitization
698        this.onPurgeSessionHistory();
699        break;
700      case "browser:purge-domain-data":
701        this.onPurgeDomainData(aData);
702        break;
703      case "nsPref:changed": // catch pref changes
704        this.onPrefChange(aData);
705        break;
706      case "idle-daily":
707        this.onIdleDaily();
708        break;
709    }
710  },
711
712  /**
713   * This method handles incoming messages sent by the session store content
714   * script via the Frame Message Manager or Parent Process Message Manager,
715   * and thus enables communication with OOP tabs.
716   */
717  receiveMessage(aMessage) {
718    // If we got here, that means we're dealing with a frame message
719    // manager message, so the target will be a <xul:browser>.
720    var browser = aMessage.target;
721    let win = browser.ownerGlobal;
722    let tab = win ? win.gBrowser.getTabForBrowser(browser) : null;
723
724    // Ensure we receive only specific messages from <xul:browser>s that
725    // have no tab or window assigned, e.g. the ones that preload
726    // about:newtab pages, or windows that have closed.
727    if (!tab && !NOTAB_MESSAGES.has(aMessage.name)) {
728      throw new Error(`received unexpected message '${aMessage.name}' ` +
729                      `from a browser that has no tab or window`);
730    }
731
732    let data = aMessage.data || {};
733    let hasEpoch = data.hasOwnProperty("epoch");
734
735    // Most messages sent by frame scripts require to pass an epoch.
736    if (!hasEpoch && !NOEPOCH_MESSAGES.has(aMessage.name)) {
737      throw new Error(`received message '${aMessage.name}' without an epoch`);
738    }
739
740    // Ignore messages from previous epochs.
741    if (hasEpoch && !this.isCurrentEpoch(browser, data.epoch)) {
742      return;
743    }
744
745    switch (aMessage.name) {
746      case "SessionStore:update":
747        // |browser.frameLoader| might be empty if the browser was already
748        // destroyed and its tab removed. In that case we still have the last
749        // frameLoader we know about to compare.
750        let frameLoader = browser.frameLoader ||
751                          this._lastKnownFrameLoader.get(browser.permanentKey);
752
753        // If the message isn't targeting the latest frameLoader discard it.
754        if (frameLoader != aMessage.targetFrameLoader) {
755          return;
756        }
757
758        if (aMessage.data.isFinal) {
759          // If this the final message we need to resolve all pending flush
760          // requests for the given browser as they might have been sent too
761          // late and will never respond. If they have been sent shortly after
762          // switching a browser's remoteness there isn't too much data to skip.
763          TabStateFlusher.resolveAll(browser);
764        } else if (aMessage.data.flushID) {
765          // This is an update kicked off by an async flush request. Notify the
766          // TabStateFlusher so that it can finish the request and notify its
767          // consumer that's waiting for the flush to be done.
768          TabStateFlusher.resolve(browser, aMessage.data.flushID);
769        }
770
771        // Ignore messages from <browser> elements that have crashed
772        // and not yet been revived.
773        if (this._crashedBrowsers.has(browser.permanentKey)) {
774          return;
775        }
776
777        // Record telemetry measurements done in the child and update the tab's
778        // cached state. Mark the window as dirty and trigger a delayed write.
779        this.recordTelemetry(aMessage.data.telemetry);
780        TabState.update(browser, aMessage.data);
781        this.saveStateDelayed(win);
782
783        // Handle any updates sent by the child after the tab was closed. This
784        // might be the final update as sent by the "unload" handler but also
785        // any async update message that was sent before the child unloaded.
786        if (this._closedTabs.has(browser.permanentKey)) {
787          let {closedTabs, tabData} = this._closedTabs.get(browser.permanentKey);
788
789          // Update the closed tab's state. This will be reflected in its
790          // window's list of closed tabs as that refers to the same object.
791          TabState.copyFromCache(browser, tabData.state);
792
793          // Is this the tab's final message?
794          if (aMessage.data.isFinal) {
795            // We expect no further updates.
796            this._closedTabs.delete(browser.permanentKey);
797            // The tab state no longer needs this reference.
798            delete tabData.permanentKey;
799
800            // Determine whether the tab state is worth saving.
801            let shouldSave = this._shouldSaveTabState(tabData.state);
802            let index = closedTabs.indexOf(tabData);
803
804            if (shouldSave && index == -1) {
805              // If the tab state is worth saving and we didn't push it onto
806              // the list of closed tabs when it was closed (because we deemed
807              // the state not worth saving) then add it to the window's list
808              // of closed tabs now.
809              this.saveClosedTabData(closedTabs, tabData);
810            } else if (!shouldSave && index > -1) {
811              // Remove from the list of closed tabs. The update messages sent
812              // after the tab was closed changed enough state so that we no
813              // longer consider its data interesting enough to keep around.
814              this.removeClosedTabData(closedTabs, index);
815            }
816          }
817        }
818        break;
819      case "SessionStore:restoreHistoryComplete":
820        // Notify the tabbrowser that the tab chrome has been restored.
821        let tabData = TabState.collect(tab);
822
823        // wall-paper fix for bug 439675: make sure that the URL to be loaded
824        // is always visible in the address bar if no other value is present
825        let activePageData = tabData.entries[tabData.index - 1] || null;
826        let uri = activePageData ? activePageData.url || null : null;
827        // NB: we won't set initial URIs (about:home, about:newtab, etc.) here
828        // because their load will not normally trigger a location bar clearing
829        // when they finish loading (to avoid race conditions where we then
830        // clear user input instead), so we shouldn't set them here either.
831        // They also don't fall under the issues in bug 439675 where user input
832        // needs to be preserved if the load doesn't succeed.
833        // We also don't do this for remoteness updates, where it should not
834        // be necessary.
835        if (!browser.userTypedValue && uri && !data.isRemotenessUpdate &&
836            !win.gInitialPages.includes(uri)) {
837          browser.userTypedValue = uri;
838        }
839
840        // If the page has a title, set it.
841        if (activePageData) {
842          if (activePageData.title) {
843            tab.label = activePageData.title;
844            tab.crop = "end";
845          } else if (activePageData.url != "about:blank") {
846            tab.label = activePageData.url;
847            tab.crop = "center";
848          }
849        } else if (tab.hasAttribute("customizemode")) {
850          win.gCustomizeMode.setTab(tab);
851        }
852
853        // Restore the tab icon.
854        if ("image" in tabData) {
855          // Use the serialized contentPrincipal with the new icon load.
856          let loadingPrincipal = Utils.deserializePrincipal(tabData.iconLoadingPrincipal);
857          win.gBrowser.setIcon(tab, tabData.image, loadingPrincipal);
858          TabStateCache.update(browser, { image: null, iconLoadingPrincipal: null });
859        }
860
861        let event = win.document.createEvent("Events");
862        event.initEvent("SSTabRestoring", true, false);
863        tab.dispatchEvent(event);
864        break;
865      case "SessionStore:restoreTabContentStarted":
866        if (browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
867          // If a load not initiated by sessionstore was started in a
868          // previously pending tab. Mark the tab as no longer pending.
869          this.markTabAsRestoring(tab);
870        } else if (!data.isRemotenessUpdate) {
871          // If the user was typing into the URL bar when we crashed, but hadn't hit
872          // enter yet, then we just need to write that value to the URL bar without
873          // loading anything. This must happen after the load, as the load will clear
874          // userTypedValue.
875          let tabData = TabState.collect(tab);
876          if (tabData.userTypedValue && !tabData.userTypedClear && !browser.userTypedValue) {
877            browser.userTypedValue = tabData.userTypedValue;
878            win.URLBarSetURI();
879          }
880
881          // Remove state we don't need any longer.
882          TabStateCache.update(browser, {
883            userTypedValue: null, userTypedClear: null
884          });
885        }
886        break;
887      case "SessionStore:restoreTabContentComplete":
888        // This callback is used exclusively by tests that want to
889        // monitor the progress of network loads.
890        if (gDebuggingEnabled) {
891          Services.obs.notifyObservers(browser, NOTIFY_TAB_RESTORED, null);
892        }
893
894        SessionStoreInternal._resetLocalTabRestoringState(tab);
895        SessionStoreInternal.restoreNextTab();
896
897        this._sendTabRestoredNotification(tab, data.isRemotenessUpdate);
898        break;
899      case "SessionStore:crashedTabRevived":
900        // The browser was revived by navigating to a different page
901        // manually, so we remove it from the ignored browser set.
902        this._crashedBrowsers.delete(browser.permanentKey);
903        break;
904      case "SessionStore:error":
905        this.reportInternalError(data);
906        TabStateFlusher.resolveAll(browser, false, "Received error from the content process");
907        break;
908      default:
909        throw new Error(`received unknown message '${aMessage.name}'`);
910        break;
911    }
912  },
913
914  /**
915   * Record telemetry measurements stored in an object.
916   * @param telemetry
917   *        {histogramID: value, ...} An object mapping histogramIDs to the
918   *        value to be recorded for that ID,
919   */
920  recordTelemetry: function (telemetry) {
921    for (let histogramId in telemetry){
922      Telemetry.getHistogramById(histogramId).add(telemetry[histogramId]);
923    }
924  },
925
926  /* ........ Window Event Handlers .............. */
927
928  /**
929   * Implement nsIDOMEventListener for handling various window and tab events
930   */
931  handleEvent: function ssi_handleEvent(aEvent) {
932    let win = aEvent.currentTarget.ownerGlobal;
933    let target = aEvent.originalTarget;
934    switch (aEvent.type) {
935      case "TabOpen":
936        this.onTabAdd(win);
937        break;
938      case "TabBrowserInserted":
939        this.onTabBrowserInserted(win, target);
940        break;
941      case "TabClose":
942        // `adoptedBy` will be set if the tab was closed because it is being
943        // moved to a new window.
944        if (!aEvent.detail.adoptedBy)
945          this.onTabClose(win, target);
946        this.onTabRemove(win, target);
947        break;
948      case "TabSelect":
949        this.onTabSelect(win);
950        break;
951      case "TabShow":
952        this.onTabShow(win, target);
953        break;
954      case "TabHide":
955        this.onTabHide(win, target);
956        break;
957      case "TabPinned":
958      case "TabUnpinned":
959      case "SwapDocShells":
960        this.saveStateDelayed(win);
961        break;
962      case "oop-browser-crashed":
963        this.onBrowserCrashed(target);
964        break;
965      case "XULFrameLoaderCreated":
966        if (target.namespaceURI == NS_XUL &&
967            target.localName == "browser" &&
968            target.frameLoader &&
969            target.permanentKey) {
970          this._lastKnownFrameLoader.set(target.permanentKey, target.frameLoader);
971          this.resetEpoch(target);
972        }
973        break;
974      default:
975        throw new Error(`unhandled event ${aEvent.type}?`);
976    }
977    this._clearRestoringWindows();
978  },
979
980  /**
981   * Generate a unique window identifier
982   * @return string
983   *         A unique string to identify a window
984   */
985  _generateWindowID: function ssi_generateWindowID() {
986    return "window" + (this._nextWindowID++);
987  },
988
989  /**
990   * Registers and tracks a given window.
991   *
992   * @param aWindow
993   *        Window reference
994   */
995  onLoad(aWindow) {
996    // return if window has already been initialized
997    if (aWindow && aWindow.__SSi && this._windows[aWindow.__SSi])
998      return;
999
1000    // ignore windows opened while shutting down
1001    if (RunState.isQuitting)
1002      return;
1003
1004    // Assign the window a unique identifier we can use to reference
1005    // internal data about the window.
1006    aWindow.__SSi = this._generateWindowID();
1007
1008    let mm = aWindow.getGroupMessageManager("browsers");
1009    MESSAGES.forEach(msg => {
1010      let listenWhenClosed = CLOSED_MESSAGES.has(msg);
1011      mm.addMessageListener(msg, this, listenWhenClosed);
1012    });
1013
1014    // Load the frame script after registering listeners.
1015    mm.loadFrameScript("chrome://browser/content/content-sessionStore.js", true);
1016
1017    // and create its data object
1018    this._windows[aWindow.__SSi] = { tabs: [], selected: 0, _closedTabs: [], busy: false };
1019
1020    if (PrivateBrowsingUtils.isWindowPrivate(aWindow))
1021      this._windows[aWindow.__SSi].isPrivate = true;
1022    if (!this._isWindowLoaded(aWindow))
1023      this._windows[aWindow.__SSi]._restoring = true;
1024    if (!aWindow.toolbar.visible)
1025      this._windows[aWindow.__SSi].isPopup = true;
1026
1027    let tabbrowser = aWindow.gBrowser;
1028
1029    // add tab change listeners to all already existing tabs
1030    for (let i = 0; i < tabbrowser.tabs.length; i++) {
1031      this.onTabBrowserInserted(aWindow, tabbrowser.tabs[i]);
1032    }
1033    // notification of tab add/remove/selection/show/hide
1034    TAB_EVENTS.forEach(function(aEvent) {
1035      tabbrowser.tabContainer.addEventListener(aEvent, this, true);
1036    }, this);
1037
1038    // Keep track of a browser's latest frameLoader.
1039    aWindow.gBrowser.addEventListener("XULFrameLoaderCreated", this);
1040  },
1041
1042  /**
1043   * Initializes a given window.
1044   *
1045   * Windows are registered as soon as they are created but we need to wait for
1046   * the session file to load, and the initial window's delayed startup to
1047   * finish before initializing a window, i.e. restoring data into it.
1048   *
1049   * @param aWindow
1050   *        Window reference
1051   * @param aInitialState
1052   *        The initial state to be loaded after startup (optional)
1053   */
1054  initializeWindow(aWindow, aInitialState = null) {
1055    let isPrivateWindow = PrivateBrowsingUtils.isWindowPrivate(aWindow);
1056
1057    // perform additional initialization when the first window is loading
1058    if (RunState.isStopped) {
1059      RunState.setRunning();
1060
1061      // restore a crashed session resp. resume the last session if requested
1062      if (aInitialState) {
1063        // Don't write to disk right after startup. Set the last time we wrote
1064        // to disk to NOW() to enforce a full interval before the next write.
1065        SessionSaver.updateLastSaveTime();
1066
1067        if (isPrivateWindow) {
1068          // We're starting with a single private window. Save the state we
1069          // actually wanted to restore so that we can do it later in case
1070          // the user opens another, non-private window.
1071          this._deferredInitialState = gSessionStartup.state;
1072
1073          // Nothing to restore now, notify observers things are complete.
1074          Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED, "");
1075        } else {
1076          TelemetryTimestamps.add("sessionRestoreRestoring");
1077          this._restoreCount = aInitialState.windows ? aInitialState.windows.length : 0;
1078
1079          // global data must be restored before restoreWindow is called so that
1080          // it happens before observers are notified
1081          this._globalState.setFromState(aInitialState);
1082
1083          let overwrite = this._isCmdLineEmpty(aWindow, aInitialState);
1084          let options = {firstWindow: true, overwriteTabs: overwrite};
1085          this.restoreWindows(aWindow, aInitialState, options);
1086        }
1087      }
1088      else {
1089        // Nothing to restore, notify observers things are complete.
1090        Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED, "");
1091      }
1092    }
1093    // this window was opened by _openWindowWithState
1094    else if (!this._isWindowLoaded(aWindow)) {
1095      let state = this._statesToRestore[aWindow.__SS_restoreID];
1096      let options = {overwriteTabs: true, isFollowUp: state.windows.length == 1};
1097      this.restoreWindow(aWindow, state.windows[0], options);
1098    }
1099    // The user opened another, non-private window after starting up with
1100    // a single private one. Let's restore the session we actually wanted to
1101    // restore at startup.
1102    else if (this._deferredInitialState && !isPrivateWindow &&
1103             aWindow.toolbar.visible) {
1104
1105      // global data must be restored before restoreWindow is called so that
1106      // it happens before observers are notified
1107      this._globalState.setFromState(this._deferredInitialState);
1108
1109      this._restoreCount = this._deferredInitialState.windows ?
1110        this._deferredInitialState.windows.length : 0;
1111      this.restoreWindows(aWindow, this._deferredInitialState, {firstWindow: true});
1112      this._deferredInitialState = null;
1113    }
1114    else if (this._restoreLastWindow && aWindow.toolbar.visible &&
1115             this._closedWindows.length && !isPrivateWindow) {
1116
1117      // default to the most-recently closed window
1118      // don't use popup windows
1119      let closedWindowState = null;
1120      let closedWindowIndex;
1121      for (let i = 0; i < this._closedWindows.length; i++) {
1122        // Take the first non-popup, point our object at it, and break out.
1123        if (!this._closedWindows[i].isPopup) {
1124          closedWindowState = this._closedWindows[i];
1125          closedWindowIndex = i;
1126          break;
1127        }
1128      }
1129
1130      if (closedWindowState) {
1131        let newWindowState;
1132        if (AppConstants.platform == "macosx" || !this._doResumeSession()) {
1133          // We want to split the window up into pinned tabs and unpinned tabs.
1134          // Pinned tabs should be restored. If there are any remaining tabs,
1135          // they should be added back to _closedWindows.
1136          // We'll cheat a little bit and reuse _prepDataForDeferredRestore
1137          // even though it wasn't built exactly for this.
1138          let [appTabsState, normalTabsState] =
1139            this._prepDataForDeferredRestore({ windows: [closedWindowState] });
1140
1141          // These are our pinned tabs, which we should restore
1142          if (appTabsState.windows.length) {
1143            newWindowState = appTabsState.windows[0];
1144            delete newWindowState.__lastSessionWindowID;
1145          }
1146
1147          // In case there were no unpinned tabs, remove the window from _closedWindows
1148          if (!normalTabsState.windows.length) {
1149            this._closedWindows.splice(closedWindowIndex, 1);
1150          }
1151          // Or update _closedWindows with the modified state
1152          else {
1153            delete normalTabsState.windows[0].__lastSessionWindowID;
1154            this._closedWindows[closedWindowIndex] = normalTabsState.windows[0];
1155          }
1156        }
1157        else {
1158          // If we're just restoring the window, make sure it gets removed from
1159          // _closedWindows.
1160          this._closedWindows.splice(closedWindowIndex, 1);
1161          newWindowState = closedWindowState;
1162          delete newWindowState.hidden;
1163        }
1164
1165        if (newWindowState) {
1166          // Ensure that the window state isn't hidden
1167          this._restoreCount = 1;
1168          let state = { windows: [newWindowState] };
1169          let options = {overwriteTabs: this._isCmdLineEmpty(aWindow, state)};
1170          this.restoreWindow(aWindow, newWindowState, options);
1171        }
1172      }
1173      // we actually restored the session just now.
1174      this._prefBranch.setBoolPref("sessionstore.resume_session_once", false);
1175    }
1176    if (this._restoreLastWindow && aWindow.toolbar.visible) {
1177      // always reset (if not a popup window)
1178      // we don't want to restore a window directly after, for example,
1179      // undoCloseWindow was executed.
1180      this._restoreLastWindow = false;
1181    }
1182  },
1183
1184  /**
1185   * Called right before a new browser window is shown.
1186   * @param aWindow
1187   *        Window reference
1188   */
1189  onBeforeBrowserWindowShown: function (aWindow) {
1190    // Register the window.
1191    this.onLoad(aWindow);
1192
1193    // Just call initializeWindow() directly if we're initialized already.
1194    if (this._sessionInitialized) {
1195      this.initializeWindow(aWindow);
1196      return;
1197    }
1198
1199    // The very first window that is opened creates a promise that is then
1200    // re-used by all subsequent windows. The promise will be used to tell
1201    // when we're ready for initialization.
1202    if (!this._promiseReadyForInitialization) {
1203      // Wait for the given window's delayed startup to be finished.
1204      let promise = new Promise(resolve => {
1205        Services.obs.addObserver(function obs(subject, topic) {
1206          if (aWindow == subject) {
1207            Services.obs.removeObserver(obs, topic);
1208            resolve();
1209          }
1210        }, "browser-delayed-startup-finished", false);
1211      });
1212
1213      // We are ready for initialization as soon as the session file has been
1214      // read from disk and the initial window's delayed startup has finished.
1215      this._promiseReadyForInitialization =
1216        Promise.all([promise, gSessionStartup.onceInitialized]);
1217    }
1218
1219    // We can't call this.onLoad since initialization
1220    // hasn't completed, so we'll wait until it is done.
1221    // Even if additional windows are opened and wait
1222    // for initialization as well, the first opened
1223    // window should execute first, and this.onLoad
1224    // will be called with the initialState.
1225    this._promiseReadyForInitialization.then(() => {
1226      if (aWindow.closed) {
1227        return;
1228      }
1229
1230      if (this._sessionInitialized) {
1231        this.initializeWindow(aWindow);
1232      } else {
1233        let initialState = this.initSession();
1234        this._sessionInitialized = true;
1235
1236        if (initialState) {
1237          Services.obs.notifyObservers(null, NOTIFY_RESTORING_ON_STARTUP, "");
1238        }
1239        TelemetryStopwatch.start("FX_SESSION_RESTORE_STARTUP_ONLOAD_INITIAL_WINDOW_MS");
1240        this.initializeWindow(aWindow, initialState);
1241        TelemetryStopwatch.finish("FX_SESSION_RESTORE_STARTUP_ONLOAD_INITIAL_WINDOW_MS");
1242
1243        // Let everyone know we're done.
1244        this._deferredInitialized.resolve();
1245      }
1246    }, console.error);
1247  },
1248
1249  /**
1250   * On window close...
1251   * - remove event listeners from tabs
1252   * - save all window data
1253   * @param aWindow
1254   *        Window reference
1255   */
1256  onClose: function ssi_onClose(aWindow) {
1257    // this window was about to be restored - conserve its original data, if any
1258    let isFullyLoaded = this._isWindowLoaded(aWindow);
1259    if (!isFullyLoaded) {
1260      if (!aWindow.__SSi) {
1261        aWindow.__SSi = this._generateWindowID();
1262      }
1263
1264      this._windows[aWindow.__SSi] = this._statesToRestore[aWindow.__SS_restoreID];
1265      delete this._statesToRestore[aWindow.__SS_restoreID];
1266      delete aWindow.__SS_restoreID;
1267    }
1268
1269    // ignore windows not tracked by SessionStore
1270    if (!aWindow.__SSi || !this._windows[aWindow.__SSi]) {
1271      return;
1272    }
1273
1274    // notify that the session store will stop tracking this window so that
1275    // extensions can store any data about this window in session store before
1276    // that's not possible anymore
1277    let event = aWindow.document.createEvent("Events");
1278    event.initEvent("SSWindowClosing", true, false);
1279    aWindow.dispatchEvent(event);
1280
1281    if (this.windowToFocus && this.windowToFocus == aWindow) {
1282      delete this.windowToFocus;
1283    }
1284
1285    var tabbrowser = aWindow.gBrowser;
1286
1287    let browsers = Array.from(tabbrowser.browsers);
1288
1289    TAB_EVENTS.forEach(function(aEvent) {
1290      tabbrowser.tabContainer.removeEventListener(aEvent, this, true);
1291    }, this);
1292
1293    aWindow.gBrowser.removeEventListener("XULFrameLoaderCreated", this);
1294
1295    let winData = this._windows[aWindow.__SSi];
1296
1297    // Collect window data only when *not* closed during shutdown.
1298    if (RunState.isRunning) {
1299      // Grab the most recent window data. The tab data will be updated
1300      // once we finish flushing all of the messages from the tabs.
1301      let tabMap = this._collectWindowData(aWindow);
1302
1303      for (let [tab, tabData] of tabMap) {
1304        let permanentKey = tab.linkedBrowser.permanentKey;
1305        this._closedWindowTabs.set(permanentKey, tabData);
1306      }
1307
1308      if (isFullyLoaded) {
1309        winData.title = tabbrowser.selectedBrowser.contentTitle || tabbrowser.selectedTab.label;
1310        winData.title = this._replaceLoadingTitle(winData.title, tabbrowser,
1311                                                  tabbrowser.selectedTab);
1312        SessionCookies.update([winData]);
1313      }
1314
1315      if (AppConstants.platform != "macosx") {
1316        // Until we decide otherwise elsewhere, this window is part of a series
1317        // of closing windows to quit.
1318        winData._shouldRestore = true;
1319      }
1320
1321      // Store the window's close date to figure out when each individual tab
1322      // was closed. This timestamp should allow re-arranging data based on how
1323      // recently something was closed.
1324      winData.closedAt = Date.now();
1325
1326      // we don't want to save the busy state
1327      delete winData.busy;
1328
1329      // When closing windows one after the other until Firefox quits, we
1330      // will move those closed in series back to the "open windows" bucket
1331      // before writing to disk. If however there is only a single window
1332      // with tabs we deem not worth saving then we might end up with a
1333      // random closed or even a pop-up window re-opened. To prevent that
1334      // we explicitly allow saving an "empty" window state.
1335      let isLastWindow =
1336        Object.keys(this._windows).length == 1 &&
1337        !this._closedWindows.some(win => win._shouldRestore || false);
1338
1339      // clear this window from the list, since it has definitely been closed.
1340      delete this._windows[aWindow.__SSi];
1341
1342      // This window has the potential to be saved in the _closedWindows
1343      // array (maybeSaveClosedWindows gets the final call on that).
1344      this._saveableClosedWindowData.add(winData);
1345
1346      // Now we have to figure out if this window is worth saving in the _closedWindows
1347      // Object.
1348      //
1349      // We're about to flush the tabs from this window, but it's possible that we
1350      // might never hear back from the content process(es) in time before the user
1351      // chooses to restore the closed window. So we do the following:
1352      //
1353      // 1) Use the tab state cache to determine synchronously if the window is
1354      //    worth stashing in _closedWindows.
1355      // 2) Flush the window.
1356      // 3) When the flush is complete, revisit our decision to store the window
1357      //    in _closedWindows, and add/remove as necessary.
1358      if (!winData.isPrivate) {
1359        // Remove any open private tabs the window may contain.
1360        PrivacyFilter.filterPrivateTabs(winData);
1361        this.maybeSaveClosedWindow(winData, isLastWindow);
1362      }
1363
1364      TabStateFlusher.flushWindow(aWindow).then(() => {
1365        // At this point, aWindow is closed! You should probably not try to
1366        // access any DOM elements from aWindow within this callback unless
1367        // you're holding on to them in the closure.
1368
1369        for (let browser of browsers) {
1370          if (this._closedWindowTabs.has(browser.permanentKey)) {
1371            let tabData = this._closedWindowTabs.get(browser.permanentKey);
1372            TabState.copyFromCache(browser, tabData);
1373            this._closedWindowTabs.delete(browser.permanentKey);
1374          }
1375        }
1376
1377        // Save non-private windows if they have at
1378        // least one saveable tab or are the last window.
1379        if (!winData.isPrivate) {
1380          // It's possible that a tab switched its privacy state at some point
1381          // before our flush, so we need to filter again.
1382          PrivacyFilter.filterPrivateTabs(winData);
1383          this.maybeSaveClosedWindow(winData, isLastWindow);
1384        }
1385
1386        // Update the tabs data now that we've got the most
1387        // recent information.
1388        this.cleanUpWindow(aWindow, winData, browsers);
1389
1390        // save the state without this window to disk
1391        this.saveStateDelayed();
1392      });
1393    } else {
1394      this.cleanUpWindow(aWindow, winData, browsers);
1395    }
1396
1397    for (let i = 0; i < tabbrowser.tabs.length; i++) {
1398      this.onTabRemove(aWindow, tabbrowser.tabs[i], true);
1399    }
1400  },
1401
1402  /**
1403   * Clean up the message listeners on a window that has finally
1404   * gone away. Call this once you're sure you don't want to hear
1405   * from any of this windows tabs from here forward.
1406   *
1407   * @param aWindow
1408   *        The browser window we're cleaning up.
1409   * @param winData
1410   *        The data for the window that we should hold in the
1411   *        DyingWindowCache in case anybody is still holding a
1412   *        reference to it.
1413   */
1414  cleanUpWindow(aWindow, winData, browsers) {
1415    // Any leftover TabStateFlusher Promises need to be resolved now,
1416    // since we're about to remove the message listeners.
1417    for (let browser of browsers) {
1418      TabStateFlusher.resolveAll(browser);
1419    }
1420
1421    // Cache the window state until it is completely gone.
1422    DyingWindowCache.set(aWindow, winData);
1423
1424    let mm = aWindow.getGroupMessageManager("browsers");
1425    MESSAGES.forEach(msg => mm.removeMessageListener(msg, this));
1426
1427    this._saveableClosedWindowData.delete(winData);
1428    delete aWindow.__SSi;
1429  },
1430
1431  /**
1432   * Decides whether or not a closed window should be put into the
1433   * _closedWindows Object. This might be called multiple times per
1434   * window, and will do the right thing of moving the window data
1435   * in or out of _closedWindows if the winData indicates that our
1436   * need for saving it has changed.
1437   *
1438   * @param winData
1439   *        The data for the closed window that we might save.
1440   * @param isLastWindow
1441   *        Whether or not the window being closed is the last
1442   *        browser window. Callers of this function should pass
1443   *        in the value of SessionStoreInternal.atLastWindow for
1444   *        this argument, and pass in the same value if they happen
1445   *        to call this method again asynchronously (for example, after
1446   *        a window flush).
1447   */
1448  maybeSaveClosedWindow(winData, isLastWindow) {
1449    // Make sure SessionStore is still running, and make sure that we
1450    // haven't chosen to forget this window.
1451    if (RunState.isRunning && this._saveableClosedWindowData.has(winData)) {
1452      // Determine whether the window has any tabs worth saving.
1453      let hasSaveableTabs = winData.tabs.some(this._shouldSaveTabState);
1454
1455      // Note that we might already have this window stored in
1456      // _closedWindows from a previous call to this function.
1457      let winIndex = this._closedWindows.indexOf(winData);
1458      let alreadyStored = (winIndex != -1);
1459      let shouldStore = (hasSaveableTabs || isLastWindow);
1460
1461      if (shouldStore && !alreadyStored) {
1462        let index = this._closedWindows.findIndex(win => {
1463          return win.closedAt < winData.closedAt;
1464        });
1465
1466        // If we found no tab closed before our
1467        // tab then just append it to the list.
1468        if (index == -1) {
1469          index = this._closedWindows.length;
1470        }
1471
1472        // About to save the closed window, add a unique ID.
1473        winData.closedId = this._nextClosedId++;
1474
1475        // Insert tabData at the right position.
1476        this._closedWindows.splice(index, 0, winData);
1477        this._capClosedWindows();
1478      } else if (!shouldStore && alreadyStored) {
1479        this._closedWindows.splice(winIndex, 1);
1480      }
1481    }
1482  },
1483
1484  /**
1485   * On quit application granted
1486   */
1487  onQuitApplicationGranted: function ssi_onQuitApplicationGranted(syncShutdown=false) {
1488    // Collect an initial snapshot of window data before we do the flush
1489    this._forEachBrowserWindow((win) => {
1490      this._collectWindowData(win);
1491    });
1492
1493    // Now add an AsyncShutdown blocker that'll spin the event loop
1494    // until the windows have all been flushed.
1495
1496    // This progress object will track the state of async window flushing
1497    // and will help us debug things that go wrong with our AsyncShutdown
1498    // blocker.
1499    let progress = { total: -1, current: -1 };
1500
1501    // We're going down! Switch state so that we treat closing windows and
1502    // tabs correctly.
1503    RunState.setQuitting();
1504
1505    if (!syncShutdown) {
1506      // We've got some time to shut down, so let's do this properly.
1507      // To prevent blocker from breaking the 60 sec limit(which will cause a
1508      // crash) of async shutdown during flushing all windows, we resolve the
1509      // promise passed to blocker once:
1510      // 1. the flushing exceed 50 sec, or
1511      // 2. 'oop-frameloader-crashed' or 'ipc:content-shutdown' is observed.
1512      // Thus, Firefox still can open the last session on next startup.
1513      AsyncShutdown.quitApplicationGranted.addBlocker(
1514        "SessionStore: flushing all windows",
1515        () => {
1516          var promises = [];
1517          promises.push(this.flushAllWindowsAsync(progress));
1518          promises.push(this.looseTimer(50000));
1519
1520          var promiseOFC = new Promise(resolve => {
1521            Services.obs.addObserver(function obs(subject, topic) {
1522              Services.obs.removeObserver(obs, topic);
1523              resolve();
1524            }, "oop-frameloader-crashed", false);
1525          });
1526          promises.push(promiseOFC);
1527
1528          var promiseICS = new Promise(resolve => {
1529            Services.obs.addObserver(function obs(subject, topic) {
1530              Services.obs.removeObserver(obs, topic);
1531              resolve();
1532            }, "ipc:content-shutdown", false);
1533          });
1534          promises.push(promiseICS);
1535
1536          return Promise.race(promises);
1537        },
1538        () => progress);
1539    } else {
1540      // We have to shut down NOW, which means we only get to save whatever
1541      // we already had cached.
1542    }
1543  },
1544
1545  /**
1546   * An async Task that iterates all open browser windows and flushes
1547   * any outstanding messages from their tabs. This will also close
1548   * all of the currently open windows while we wait for the flushes
1549   * to complete.
1550   *
1551   * @param progress (Object)
1552   *        Optional progress object that will be updated as async
1553   *        window flushing progresses. flushAllWindowsSync will
1554   *        write to the following properties:
1555   *
1556   *        total (int):
1557   *          The total number of windows to be flushed.
1558   *        current (int):
1559   *          The current window that we're waiting for a flush on.
1560   *
1561   * @return Promise
1562   */
1563  flushAllWindowsAsync: Task.async(function*(progress={}) {
1564    let windowPromises = new Map();
1565    // We collect flush promises and close each window immediately so that
1566    // the user can't start changing any window state while we're waiting
1567    // for the flushes to finish.
1568    this._forEachBrowserWindow((win) => {
1569      windowPromises.set(win, TabStateFlusher.flushWindow(win));
1570
1571      // We have to wait for these messages to come up from
1572      // each window and each browser. In the meantime, hide
1573      // the windows to improve perceived shutdown speed.
1574      let baseWin = win.QueryInterface(Ci.nsIInterfaceRequestor)
1575                       .getInterface(Ci.nsIDocShell)
1576                       .QueryInterface(Ci.nsIDocShellTreeItem)
1577                       .treeOwner
1578                       .QueryInterface(Ci.nsIBaseWindow);
1579      baseWin.visibility = false;
1580    });
1581
1582    progress.total = windowPromises.size;
1583    progress.current = 0;
1584
1585    // We'll iterate through the Promise array, yielding each one, so as to
1586    // provide useful progress information to AsyncShutdown.
1587    for (let [win, promise] of windowPromises) {
1588      yield promise;
1589      this._collectWindowData(win);
1590      progress.current++;
1591    };
1592
1593    // We must cache this because _getMostRecentBrowserWindow will always
1594    // return null by the time quit-application occurs.
1595    var activeWindow = this._getMostRecentBrowserWindow();
1596    if (activeWindow)
1597      this.activeWindowSSiCache = activeWindow.__SSi || "";
1598    DirtyWindows.clear();
1599  }),
1600
1601  /**
1602   * On last browser window close
1603   */
1604  onLastWindowCloseGranted: function ssi_onLastWindowCloseGranted() {
1605    // last browser window is quitting.
1606    // remember to restore the last window when another browser window is opened
1607    // do not account for pref(resume_session_once) at this point, as it might be
1608    // set by another observer getting this notice after us
1609    this._restoreLastWindow = true;
1610  },
1611
1612  /**
1613   * On quitting application
1614   * @param aData
1615   *        String type of quitting
1616   */
1617  onQuitApplication: function ssi_onQuitApplication(aData) {
1618    if (aData == "restart") {
1619      this._prefBranch.setBoolPref("sessionstore.resume_session_once", true);
1620      // The browser:purge-session-history notification fires after the
1621      // quit-application notification so unregister the
1622      // browser:purge-session-history notification to prevent clearing
1623      // session data on disk on a restart.  It is also unnecessary to
1624      // perform any other sanitization processing on a restart as the
1625      // browser is about to exit anyway.
1626      Services.obs.removeObserver(this, "browser:purge-session-history");
1627    }
1628
1629    if (aData != "restart") {
1630      // Throw away the previous session on shutdown
1631      LastSession.clear();
1632    }
1633
1634    this._uninit();
1635  },
1636
1637  /**
1638   * On purge of session history
1639   */
1640  onPurgeSessionHistory: function ssi_onPurgeSessionHistory() {
1641    SessionFile.wipe();
1642    // If the browser is shutting down, simply return after clearing the
1643    // session data on disk as this notification fires after the
1644    // quit-application notification so the browser is about to exit.
1645    if (RunState.isQuitting)
1646      return;
1647    LastSession.clear();
1648
1649    let openWindows = {};
1650    // Collect open windows.
1651    this._forEachBrowserWindow(({__SSi: id}) => openWindows[id] = true);
1652
1653    // also clear all data about closed tabs and windows
1654    for (let ix in this._windows) {
1655      if (ix in openWindows) {
1656        this._windows[ix]._closedTabs = [];
1657      } else {
1658        delete this._windows[ix];
1659      }
1660    }
1661    // also clear all data about closed windows
1662    this._closedWindows = [];
1663    // give the tabbrowsers a chance to clear their histories first
1664    var win = this._getMostRecentBrowserWindow();
1665    if (win) {
1666      win.setTimeout(() => SessionSaver.run(), 0);
1667    } else if (RunState.isRunning) {
1668      SessionSaver.run();
1669    }
1670
1671    this._clearRestoringWindows();
1672    this._saveableClosedWindowData = new WeakSet();
1673  },
1674
1675  /**
1676   * On purge of domain data
1677   * @param aData
1678   *        String domain data
1679   */
1680  onPurgeDomainData: function ssi_onPurgeDomainData(aData) {
1681    // does a session history entry contain a url for the given domain?
1682    function containsDomain(aEntry) {
1683      if (Utils.hasRootDomain(aEntry.url, aData)) {
1684        return true;
1685      }
1686      return aEntry.children && aEntry.children.some(containsDomain, this);
1687    }
1688    // remove all closed tabs containing a reference to the given domain
1689    for (let ix in this._windows) {
1690      let closedTabs = this._windows[ix]._closedTabs;
1691      for (let i = closedTabs.length - 1; i >= 0; i--) {
1692        if (closedTabs[i].state.entries.some(containsDomain, this))
1693          closedTabs.splice(i, 1);
1694      }
1695    }
1696    // remove all open & closed tabs containing a reference to the given
1697    // domain in closed windows
1698    for (let ix = this._closedWindows.length - 1; ix >= 0; ix--) {
1699      let closedTabs = this._closedWindows[ix]._closedTabs;
1700      let openTabs = this._closedWindows[ix].tabs;
1701      let openTabCount = openTabs.length;
1702      for (let i = closedTabs.length - 1; i >= 0; i--)
1703        if (closedTabs[i].state.entries.some(containsDomain, this))
1704          closedTabs.splice(i, 1);
1705      for (let j = openTabs.length - 1; j >= 0; j--) {
1706        if (openTabs[j].entries.some(containsDomain, this)) {
1707          openTabs.splice(j, 1);
1708          if (this._closedWindows[ix].selected > j)
1709            this._closedWindows[ix].selected--;
1710        }
1711      }
1712      if (openTabs.length == 0) {
1713        this._closedWindows.splice(ix, 1);
1714      }
1715      else if (openTabs.length != openTabCount) {
1716        // Adjust the window's title if we removed an open tab
1717        let selectedTab = openTabs[this._closedWindows[ix].selected - 1];
1718        // some duplication from restoreHistory - make sure we get the correct title
1719        let activeIndex = (selectedTab.index || selectedTab.entries.length) - 1;
1720        if (activeIndex >= selectedTab.entries.length)
1721          activeIndex = selectedTab.entries.length - 1;
1722        this._closedWindows[ix].title = selectedTab.entries[activeIndex].title;
1723      }
1724    }
1725
1726    if (RunState.isRunning) {
1727      SessionSaver.run();
1728    }
1729
1730    this._clearRestoringWindows();
1731  },
1732
1733  /**
1734   * On preference change
1735   * @param aData
1736   *        String preference changed
1737   */
1738  onPrefChange: function ssi_onPrefChange(aData) {
1739    switch (aData) {
1740      // if the user decreases the max number of closed tabs they want
1741      // preserved update our internal states to match that max
1742      case "sessionstore.max_tabs_undo":
1743        this._max_tabs_undo = this._prefBranch.getIntPref("sessionstore.max_tabs_undo");
1744        for (let ix in this._windows) {
1745          this._windows[ix]._closedTabs.splice(this._max_tabs_undo, this._windows[ix]._closedTabs.length);
1746        }
1747        break;
1748      case "sessionstore.max_windows_undo":
1749        this._max_windows_undo = this._prefBranch.getIntPref("sessionstore.max_windows_undo");
1750        this._capClosedWindows();
1751        break;
1752    }
1753  },
1754
1755  /**
1756   * save state when new tab is added
1757   * @param aWindow
1758   *        Window reference
1759   */
1760  onTabAdd: function ssi_onTabAdd(aWindow) {
1761    this.saveStateDelayed(aWindow);
1762  },
1763
1764  /**
1765   * set up listeners for a new tab
1766   * @param aWindow
1767   *        Window reference
1768   * @param aTab
1769   *        Tab reference
1770   */
1771  onTabBrowserInserted: function ssi_onTabBrowserInserted(aWindow, aTab) {
1772    let browser = aTab.linkedBrowser;
1773    browser.addEventListener("SwapDocShells", this);
1774    browser.addEventListener("oop-browser-crashed", this);
1775
1776    if (browser.frameLoader) {
1777      this._lastKnownFrameLoader.set(browser.permanentKey, browser.frameLoader);
1778    }
1779  },
1780
1781  /**
1782   * remove listeners for a tab
1783   * @param aWindow
1784   *        Window reference
1785   * @param aTab
1786   *        Tab reference
1787   * @param aNoNotification
1788   *        bool Do not save state if we're updating an existing tab
1789   */
1790  onTabRemove: function ssi_onTabRemove(aWindow, aTab, aNoNotification) {
1791    let browser = aTab.linkedBrowser;
1792    browser.removeEventListener("SwapDocShells", this);
1793    browser.removeEventListener("oop-browser-crashed", this);
1794
1795    // If this tab was in the middle of restoring or still needs to be restored,
1796    // we need to reset that state. If the tab was restoring, we will attempt to
1797    // restore the next tab.
1798    let previousState = browser.__SS_restoreState;
1799    if (previousState) {
1800      this._resetTabRestoringState(aTab);
1801      if (previousState == TAB_STATE_RESTORING)
1802        this.restoreNextTab();
1803    }
1804
1805    if (!aNoNotification) {
1806      this.saveStateDelayed(aWindow);
1807    }
1808  },
1809
1810  /**
1811   * When a tab closes, collect its properties
1812   * @param aWindow
1813   *        Window reference
1814   * @param aTab
1815   *        Tab reference
1816   */
1817  onTabClose: function ssi_onTabClose(aWindow, aTab) {
1818    // notify the tabbrowser that the tab state will be retrieved for the last time
1819    // (so that extension authors can easily set data on soon-to-be-closed tabs)
1820    var event = aWindow.document.createEvent("Events");
1821    event.initEvent("SSTabClosing", true, false);
1822    aTab.dispatchEvent(event);
1823
1824    // don't update our internal state if we don't have to
1825    if (this._max_tabs_undo == 0) {
1826      return;
1827    }
1828
1829    // Get the latest data for this tab (generally, from the cache)
1830    let tabState = TabState.collect(aTab);
1831
1832    // Don't save private tabs
1833    let isPrivateWindow = PrivateBrowsingUtils.isWindowPrivate(aWindow);
1834    if (!isPrivateWindow && tabState.isPrivate) {
1835      return;
1836    }
1837
1838    // Store closed-tab data for undo.
1839    let tabbrowser = aWindow.gBrowser;
1840    let tabTitle = this._replaceLoadingTitle(aTab.label, tabbrowser, aTab);
1841    let {permanentKey} = aTab.linkedBrowser;
1842
1843    let tabData = {
1844      permanentKey,
1845      state: tabState,
1846      title: tabTitle,
1847      image: tabbrowser.getIcon(aTab),
1848      iconLoadingPrincipal: Utils.serializePrincipal(aTab.linkedBrowser.contentPrincipal),
1849      pos: aTab._tPos,
1850      closedAt: Date.now()
1851    };
1852
1853    let closedTabs = this._windows[aWindow.__SSi]._closedTabs;
1854
1855    // Determine whether the tab contains any information worth saving. Note
1856    // that there might be pending state changes queued in the child that
1857    // didn't reach the parent yet. If a tab is emptied before closing then we
1858    // might still remove it from the list of closed tabs later.
1859    if (this._shouldSaveTabState(tabState)) {
1860      // Save the tab state, for now. We might push a valid tab out
1861      // of the list but those cases should be extremely rare and
1862      // do probably never occur when using the browser normally.
1863      // (Tests or add-ons might do weird things though.)
1864      this.saveClosedTabData(closedTabs, tabData);
1865    }
1866
1867    // Remember the closed tab to properly handle any last updates included in
1868    // the final "update" message sent by the frame script's unload handler.
1869    this._closedTabs.set(permanentKey, {closedTabs, tabData});
1870  },
1871
1872  /**
1873   * Insert a given |tabData| object into the list of |closedTabs|. We will
1874   * determine the right insertion point based on the .closedAt properties of
1875   * all tabs already in the list. The list will be truncated to contain a
1876   * maximum of |this._max_tabs_undo| entries.
1877   *
1878   * @param closedTabs (array)
1879   *        The list of closed tabs for a window.
1880   * @param tabData (object)
1881   *        The tabData to be inserted.
1882   */
1883  saveClosedTabData(closedTabs, tabData) {
1884    // Find the index of the first tab in the list
1885    // of closed tabs that was closed before our tab.
1886    let index = closedTabs.findIndex(tab => {
1887      return tab.closedAt < tabData.closedAt;
1888    });
1889
1890    // If we found no tab closed before our
1891    // tab then just append it to the list.
1892    if (index == -1) {
1893      index = closedTabs.length;
1894    }
1895
1896    // About to save the closed tab, add a unique ID.
1897    tabData.closedId = this._nextClosedId++;
1898
1899    // Insert tabData at the right position.
1900    closedTabs.splice(index, 0, tabData);
1901
1902    // Truncate the list of closed tabs, if needed.
1903    if (closedTabs.length > this._max_tabs_undo) {
1904      closedTabs.splice(this._max_tabs_undo, closedTabs.length);
1905    }
1906  },
1907
1908  /**
1909   * Remove the closed tab data at |index| from the list of |closedTabs|. If
1910   * the tab's final message is still pending we will simply discard it when
1911   * it arrives so that the tab doesn't reappear in the list.
1912   *
1913   * @param closedTabs (array)
1914   *        The list of closed tabs for a window.
1915   * @param index (uint)
1916   *        The index of the tab to remove.
1917   */
1918  removeClosedTabData(closedTabs, index) {
1919    // Remove the given index from the list.
1920    let [closedTab] = closedTabs.splice(index, 1);
1921
1922    // If the closed tab's state still has a .permanentKey property then we
1923    // haven't seen its final update message yet. Remove it from the map of
1924    // closed tabs so that we will simply discard its last messages and will
1925    // not add it back to the list of closed tabs again.
1926    if (closedTab.permanentKey) {
1927      this._closedTabs.delete(closedTab.permanentKey);
1928      this._closedWindowTabs.delete(closedTab.permanentKey);
1929      delete closedTab.permanentKey;
1930    }
1931
1932    return closedTab;
1933  },
1934
1935  /**
1936   * When a tab is selected, save session data
1937   * @param aWindow
1938   *        Window reference
1939   */
1940  onTabSelect: function ssi_onTabSelect(aWindow) {
1941    if (RunState.isRunning) {
1942      this._windows[aWindow.__SSi].selected = aWindow.gBrowser.tabContainer.selectedIndex;
1943
1944      let tab = aWindow.gBrowser.selectedTab;
1945      let browser = tab.linkedBrowser;
1946
1947      if (browser.__SS_restoreState &&
1948          browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
1949        // If __SS_restoreState is still on the browser and it is
1950        // TAB_STATE_NEEDS_RESTORE, then then we haven't restored
1951        // this tab yet.
1952        //
1953        // It's possible that this tab was recently revived, and that
1954        // we've deferred showing the tab crashed page for it (if the
1955        // tab crashed in the background). If so, we need to re-enter
1956        // the crashed state, since we'll be showing the tab crashed
1957        // page.
1958        if (TabCrashHandler.willShowCrashedTab(browser)) {
1959          this.enterCrashedState(browser);
1960        } else {
1961          this.restoreTabContent(tab);
1962        }
1963      }
1964    }
1965  },
1966
1967  onTabShow: function ssi_onTabShow(aWindow, aTab) {
1968    // If the tab hasn't been restored yet, move it into the right bucket
1969    if (aTab.linkedBrowser.__SS_restoreState &&
1970        aTab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
1971      TabRestoreQueue.hiddenToVisible(aTab);
1972
1973      // let's kick off tab restoration again to ensure this tab gets restored
1974      // with "restore_hidden_tabs" == false (now that it has become visible)
1975      this.restoreNextTab();
1976    }
1977
1978    // Default delay of 2 seconds gives enough time to catch multiple TabShow
1979    // events. This used to be due to changing groups in 'tab groups'. We
1980    // might be able to get rid of this now?
1981    this.saveStateDelayed(aWindow);
1982  },
1983
1984  onTabHide: function ssi_onTabHide(aWindow, aTab) {
1985    // If the tab hasn't been restored yet, move it into the right bucket
1986    if (aTab.linkedBrowser.__SS_restoreState &&
1987        aTab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
1988      TabRestoreQueue.visibleToHidden(aTab);
1989    }
1990
1991    // Default delay of 2 seconds gives enough time to catch multiple TabHide
1992    // events. This used to be due to changing groups in 'tab groups'. We
1993    // might be able to get rid of this now?
1994    this.saveStateDelayed(aWindow);
1995  },
1996
1997  /**
1998   * Handler for the event that is fired when a <xul:browser> crashes.
1999   *
2000   * @param aWindow
2001   *        The window that the crashed browser belongs to.
2002   * @param aBrowser
2003   *        The <xul:browser> that is now in the crashed state.
2004   */
2005  onBrowserCrashed: function(aBrowser) {
2006    NS_ASSERT(aBrowser.isRemoteBrowser,
2007              "Only remote browsers should be able to crash");
2008
2009    this.enterCrashedState(aBrowser);
2010    // The browser crashed so we might never receive flush responses.
2011    // Resolve all pending flush requests for the crashed browser.
2012    TabStateFlusher.resolveAll(aBrowser);
2013  },
2014
2015  /**
2016   * Called when a browser is showing or is about to show the tab
2017   * crashed page. This method causes SessionStore to ignore the
2018   * tab until it's restored.
2019   *
2020   * @param browser
2021   *        The <xul:browser> that is about to show the crashed page.
2022   */
2023  enterCrashedState(browser) {
2024    this._crashedBrowsers.add(browser.permanentKey);
2025
2026    let win = browser.ownerGlobal;
2027
2028    // If we hadn't yet restored, or were still in the midst of
2029    // restoring this browser at the time of the crash, we need
2030    // to reset its state so that we can try to restore it again
2031    // when the user revives the tab from the crash.
2032    if (browser.__SS_restoreState) {
2033      let tab = win.gBrowser.getTabForBrowser(browser);
2034      this._resetLocalTabRestoringState(tab);
2035    }
2036  },
2037
2038  // Clean up data that has been closed a long time ago.
2039  // Do not reschedule a save. This will wait for the next regular
2040  // save.
2041  onIdleDaily: function() {
2042    // Remove old closed windows
2043    this._cleanupOldData([this._closedWindows]);
2044
2045    // Remove closed tabs of closed windows
2046    this._cleanupOldData(this._closedWindows.map((winData) => winData._closedTabs));
2047
2048    // Remove closed tabs of open windows
2049    this._cleanupOldData(Object.keys(this._windows).map((key) => this._windows[key]._closedTabs));
2050  },
2051
2052  // Remove "old" data from an array
2053  _cleanupOldData: function(targets) {
2054    const TIME_TO_LIVE = this._prefBranch.getIntPref("sessionstore.cleanup.forget_closed_after");
2055    const now = Date.now();
2056
2057    for (let array of targets) {
2058      for (let i = array.length - 1; i >= 0; --i)  {
2059        let data = array[i];
2060        // Make sure that we have a timestamp to tell us when the target
2061        // has been closed. If we don't have a timestamp, default to a
2062        // safe timestamp: just now.
2063        data.closedAt = data.closedAt || now;
2064        if (now - data.closedAt > TIME_TO_LIVE) {
2065          array.splice(i, 1);
2066        }
2067      }
2068    }
2069  },
2070
2071  /* ........ nsISessionStore API .............. */
2072
2073  getBrowserState: function ssi_getBrowserState() {
2074    let state = this.getCurrentState();
2075
2076    // Don't include the last session state in getBrowserState().
2077    delete state.lastSessionState;
2078
2079    // Don't include any deferred initial state.
2080    delete state.deferredInitialState;
2081
2082    return JSON.stringify(state);
2083  },
2084
2085  setBrowserState: function ssi_setBrowserState(aState) {
2086    this._handleClosedWindows();
2087
2088    try {
2089      var state = JSON.parse(aState);
2090    }
2091    catch (ex) { /* invalid state object - don't restore anything */ }
2092    if (!state) {
2093      throw Components.Exception("Invalid state string: not JSON", Cr.NS_ERROR_INVALID_ARG);
2094    }
2095    if (!state.windows) {
2096      throw Components.Exception("No windows", Cr.NS_ERROR_INVALID_ARG);
2097    }
2098
2099    this._browserSetState = true;
2100
2101    // Make sure the priority queue is emptied out
2102    this._resetRestoringState();
2103
2104    var window = this._getMostRecentBrowserWindow();
2105    if (!window) {
2106      this._restoreCount = 1;
2107      this._openWindowWithState(state);
2108      return;
2109    }
2110
2111    // close all other browser windows
2112    this._forEachBrowserWindow(function(aWindow) {
2113      if (aWindow != window) {
2114        aWindow.close();
2115        this.onClose(aWindow);
2116      }
2117    });
2118
2119    // make sure closed window data isn't kept
2120    this._closedWindows = [];
2121
2122    // determine how many windows are meant to be restored
2123    this._restoreCount = state.windows ? state.windows.length : 0;
2124
2125    // global data must be restored before restoreWindow is called so that
2126    // it happens before observers are notified
2127    this._globalState.setFromState(state);
2128
2129    // restore to the given state
2130    this.restoreWindows(window, state, {overwriteTabs: true});
2131  },
2132
2133  getWindowState: function ssi_getWindowState(aWindow) {
2134    if ("__SSi" in aWindow) {
2135      return JSON.stringify(this._getWindowState(aWindow));
2136    }
2137
2138    if (DyingWindowCache.has(aWindow)) {
2139      let data = DyingWindowCache.get(aWindow);
2140      return JSON.stringify({ windows: [data] });
2141    }
2142
2143    throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
2144  },
2145
2146  setWindowState: function ssi_setWindowState(aWindow, aState, aOverwrite) {
2147    if (!aWindow.__SSi) {
2148      throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
2149    }
2150
2151    this.restoreWindows(aWindow, aState, {overwriteTabs: aOverwrite});
2152  },
2153
2154  getTabState: function ssi_getTabState(aTab) {
2155    if (!aTab.ownerGlobal.__SSi) {
2156      throw Components.Exception("Default view is not tracked", Cr.NS_ERROR_INVALID_ARG);
2157    }
2158
2159    let tabState = TabState.collect(aTab);
2160
2161    return JSON.stringify(tabState);
2162  },
2163
2164  setTabState(aTab, aState) {
2165    // Remove the tab state from the cache.
2166    // Note that we cannot simply replace the contents of the cache
2167    // as |aState| can be an incomplete state that will be completed
2168    // by |restoreTabs|.
2169    let tabState = JSON.parse(aState);
2170    if (!tabState) {
2171      throw Components.Exception("Invalid state string: not JSON", Cr.NS_ERROR_INVALID_ARG);
2172    }
2173    if (typeof tabState != "object") {
2174      throw Components.Exception("Not an object", Cr.NS_ERROR_INVALID_ARG);
2175    }
2176    if (!("entries" in tabState)) {
2177      throw Components.Exception("Invalid state object: no entries", Cr.NS_ERROR_INVALID_ARG);
2178    }
2179
2180    let window = aTab.ownerGlobal;
2181    if (!("__SSi" in window)) {
2182      throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
2183    }
2184
2185    if (aTab.linkedBrowser.__SS_restoreState) {
2186      this._resetTabRestoringState(aTab);
2187    }
2188
2189    this.restoreTab(aTab, tabState);
2190  },
2191
2192  duplicateTab: function ssi_duplicateTab(aWindow, aTab, aDelta = 0, aRestoreImmediately = true) {
2193    if (!aTab.ownerGlobal.__SSi) {
2194      throw Components.Exception("Default view is not tracked", Cr.NS_ERROR_INVALID_ARG);
2195    }
2196    if (!aWindow.gBrowser) {
2197      throw Components.Exception("Invalid window object: no gBrowser", Cr.NS_ERROR_INVALID_ARG);
2198    }
2199
2200    // Create a new tab.
2201    let userContextId = aTab.getAttribute("usercontextid");
2202    let newTab = aTab == aWindow.gBrowser.selectedTab ?
2203      aWindow.gBrowser.addTab(null, {relatedToCurrent: true, ownerTab: aTab, userContextId}) :
2204      aWindow.gBrowser.addTab(null, {userContextId});
2205
2206    // Set tab title to "Connecting..." and start the throbber to pretend we're
2207    // doing something while actually waiting for data from the frame script.
2208    aWindow.gBrowser.setTabTitleLoading(newTab);
2209    newTab.setAttribute("busy", "true");
2210
2211    // Collect state before flushing.
2212    let tabState = TabState.clone(aTab);
2213
2214    // Flush to get the latest tab state to duplicate.
2215    let browser = aTab.linkedBrowser;
2216    TabStateFlusher.flush(browser).then(() => {
2217      // The new tab might have been closed in the meantime.
2218      if (newTab.closing || !newTab.linkedBrowser) {
2219        return;
2220      }
2221
2222      let window = newTab.ownerGlobal;
2223
2224      // The tab or its window might be gone.
2225      if (!window || !window.__SSi) {
2226        return;
2227      }
2228
2229      // Update state with flushed data. We can't use TabState.clone() here as
2230      // the tab to duplicate may have already been closed. In that case we
2231      // only have access to the <xul:browser>.
2232      let options = {includePrivateData: true};
2233      TabState.copyFromCache(browser, tabState, options);
2234
2235      tabState.index += aDelta;
2236      tabState.index = Math.max(1, Math.min(tabState.index, tabState.entries.length));
2237      tabState.pinned = false;
2238
2239      // Restore the state into the new tab.
2240      this.restoreTab(newTab, tabState, {
2241        restoreImmediately: aRestoreImmediately
2242      });
2243    });
2244
2245    return newTab;
2246  },
2247
2248  getClosedTabCount: function ssi_getClosedTabCount(aWindow) {
2249    if ("__SSi" in aWindow) {
2250      return this._windows[aWindow.__SSi]._closedTabs.length;
2251    }
2252
2253    if (!DyingWindowCache.has(aWindow)) {
2254      throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
2255    }
2256
2257    return DyingWindowCache.get(aWindow)._closedTabs.length;
2258  },
2259
2260  getClosedTabData: function ssi_getClosedTabData(aWindow, aAsString = true) {
2261    if ("__SSi" in aWindow) {
2262      return aAsString ?
2263        JSON.stringify(this._windows[aWindow.__SSi]._closedTabs) :
2264        Cu.cloneInto(this._windows[aWindow.__SSi]._closedTabs, {});
2265    }
2266
2267    if (!DyingWindowCache.has(aWindow)) {
2268      throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
2269    }
2270
2271    let data = DyingWindowCache.get(aWindow);
2272    return aAsString ? JSON.stringify(data._closedTabs) : Cu.cloneInto(data._closedTabs, {});
2273  },
2274
2275  undoCloseTab: function ssi_undoCloseTab(aWindow, aIndex) {
2276    if (!aWindow.__SSi) {
2277      throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
2278    }
2279
2280    var closedTabs = this._windows[aWindow.__SSi]._closedTabs;
2281
2282    // default to the most-recently closed tab
2283    aIndex = aIndex || 0;
2284    if (!(aIndex in closedTabs)) {
2285      throw Components.Exception("Invalid index: not in the closed tabs", Cr.NS_ERROR_INVALID_ARG);
2286    }
2287
2288    // fetch the data of closed tab, while removing it from the array
2289    let {state, pos} = this.removeClosedTabData(closedTabs, aIndex);
2290
2291    // create a new tab
2292    let tabbrowser = aWindow.gBrowser;
2293    let tab = tabbrowser.selectedTab = tabbrowser.addTab(null, state);
2294
2295    // restore tab content
2296    this.restoreTab(tab, state);
2297
2298    // restore the tab's position
2299    tabbrowser.moveTabTo(tab, pos);
2300
2301    // focus the tab's content area (bug 342432)
2302    tab.linkedBrowser.focus();
2303
2304    return tab;
2305  },
2306
2307  forgetClosedTab: function ssi_forgetClosedTab(aWindow, aIndex) {
2308    if (!aWindow.__SSi) {
2309      throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
2310    }
2311
2312    var closedTabs = this._windows[aWindow.__SSi]._closedTabs;
2313
2314    // default to the most-recently closed tab
2315    aIndex = aIndex || 0;
2316    if (!(aIndex in closedTabs)) {
2317      throw Components.Exception("Invalid index: not in the closed tabs", Cr.NS_ERROR_INVALID_ARG);
2318    }
2319
2320    // remove closed tab from the array
2321    this.removeClosedTabData(closedTabs, aIndex);
2322  },
2323
2324  getClosedWindowCount: function ssi_getClosedWindowCount() {
2325    return this._closedWindows.length;
2326  },
2327
2328  getClosedWindowData: function ssi_getClosedWindowData(aAsString = true) {
2329    return aAsString ? JSON.stringify(this._closedWindows) : Cu.cloneInto(this._closedWindows, {});
2330  },
2331
2332  undoCloseWindow: function ssi_undoCloseWindow(aIndex) {
2333    if (!(aIndex in this._closedWindows)) {
2334      throw Components.Exception("Invalid index: not in the closed windows", Cr.NS_ERROR_INVALID_ARG);
2335    }
2336
2337    // reopen the window
2338    let state = { windows: this._closedWindows.splice(aIndex, 1) };
2339    delete state.windows[0].closedAt; // Window is now open.
2340
2341    let window = this._openWindowWithState(state);
2342    this.windowToFocus = window;
2343    return window;
2344  },
2345
2346  forgetClosedWindow: function ssi_forgetClosedWindow(aIndex) {
2347    // default to the most-recently closed window
2348    aIndex = aIndex || 0;
2349    if (!(aIndex in this._closedWindows)) {
2350      throw Components.Exception("Invalid index: not in the closed windows", Cr.NS_ERROR_INVALID_ARG);
2351    }
2352
2353    // remove closed window from the array
2354    let winData = this._closedWindows[aIndex];
2355    this._closedWindows.splice(aIndex, 1);
2356    this._saveableClosedWindowData.delete(winData);
2357  },
2358
2359  getWindowValue: function ssi_getWindowValue(aWindow, aKey) {
2360    if ("__SSi" in aWindow) {
2361      var data = this._windows[aWindow.__SSi].extData || {};
2362      return data[aKey] || "";
2363    }
2364
2365    if (DyingWindowCache.has(aWindow)) {
2366      let data = DyingWindowCache.get(aWindow).extData || {};
2367      return data[aKey] || "";
2368    }
2369
2370    throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
2371  },
2372
2373  setWindowValue: function ssi_setWindowValue(aWindow, aKey, aStringValue) {
2374    if (typeof aStringValue != "string") {
2375      throw new TypeError("setWindowValue only accepts string values");
2376    }
2377
2378    if (!("__SSi" in aWindow)) {
2379      throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
2380    }
2381    if (!this._windows[aWindow.__SSi].extData) {
2382      this._windows[aWindow.__SSi].extData = {};
2383    }
2384    this._windows[aWindow.__SSi].extData[aKey] = aStringValue;
2385    this.saveStateDelayed(aWindow);
2386  },
2387
2388  deleteWindowValue: function ssi_deleteWindowValue(aWindow, aKey) {
2389    if (aWindow.__SSi && this._windows[aWindow.__SSi].extData &&
2390        this._windows[aWindow.__SSi].extData[aKey])
2391      delete this._windows[aWindow.__SSi].extData[aKey];
2392    this.saveStateDelayed(aWindow);
2393  },
2394
2395  getTabValue: function ssi_getTabValue(aTab, aKey) {
2396    return (aTab.__SS_extdata || {})[aKey] || "";
2397  },
2398
2399  setTabValue: function ssi_setTabValue(aTab, aKey, aStringValue) {
2400    if (typeof aStringValue != "string") {
2401      throw new TypeError("setTabValue only accepts string values");
2402    }
2403
2404    // If the tab hasn't been restored, then set the data there, otherwise we
2405    // could lose newly added data.
2406    if (!aTab.__SS_extdata) {
2407      aTab.__SS_extdata = {};
2408    }
2409
2410    aTab.__SS_extdata[aKey] = aStringValue;
2411    this.saveStateDelayed(aTab.ownerGlobal);
2412  },
2413
2414  deleteTabValue: function ssi_deleteTabValue(aTab, aKey) {
2415    if (aTab.__SS_extdata && aKey in aTab.__SS_extdata) {
2416      delete aTab.__SS_extdata[aKey];
2417      this.saveStateDelayed(aTab.ownerGlobal);
2418    }
2419  },
2420
2421  getGlobalValue: function ssi_getGlobalValue(aKey) {
2422    return this._globalState.get(aKey);
2423  },
2424
2425  setGlobalValue: function ssi_setGlobalValue(aKey, aStringValue) {
2426    if (typeof aStringValue != "string") {
2427      throw new TypeError("setGlobalValue only accepts string values");
2428    }
2429
2430    this._globalState.set(aKey, aStringValue);
2431    this.saveStateDelayed();
2432  },
2433
2434  deleteGlobalValue: function ssi_deleteGlobalValue(aKey) {
2435    this._globalState.delete(aKey);
2436    this.saveStateDelayed();
2437  },
2438
2439  persistTabAttribute: function ssi_persistTabAttribute(aName) {
2440    if (TabAttributes.persist(aName)) {
2441      this.saveStateDelayed();
2442    }
2443  },
2444
2445
2446  /**
2447   * Undoes the closing of a tab or window which corresponds
2448   * to the closedId passed in.
2449   *
2450   * @param aClosedId
2451   *        The closedId of the tab or window
2452   *
2453   * @returns a tab or window object
2454   */
2455  undoCloseById(aClosedId) {
2456    // Check for a window first.
2457    for (let i = 0, l = this._closedWindows.length; i < l; i++) {
2458      if (this._closedWindows[i].closedId == aClosedId) {
2459        return this.undoCloseWindow(i);
2460      }
2461    }
2462
2463    // Check for a tab.
2464    let windowsEnum = Services.wm.getEnumerator("navigator:browser");
2465    while (windowsEnum.hasMoreElements()) {
2466      let window = windowsEnum.getNext();
2467      let windowState = this._windows[window.__SSi];
2468      if (windowState) {
2469        for (let j = 0, l = windowState._closedTabs.length; j < l; j++) {
2470          if (windowState._closedTabs[j].closedId == aClosedId) {
2471            return this.undoCloseTab(window, j);
2472          }
2473        }
2474      }
2475    }
2476
2477    // Neither a tab nor a window was found, return undefined and let the caller decide what to do about it.
2478    return undefined;
2479  },
2480
2481  /**
2482   * Restores the session state stored in LastSession. This will attempt
2483   * to merge data into the current session. If a window was opened at startup
2484   * with pinned tab(s), then the remaining data from the previous session for
2485   * that window will be opened into that window. Otherwise new windows will
2486   * be opened.
2487   */
2488  restoreLastSession: function ssi_restoreLastSession() {
2489    // Use the public getter since it also checks PB mode
2490    if (!this.canRestoreLastSession) {
2491      throw Components.Exception("Last session can not be restored");
2492    }
2493
2494    Services.obs.notifyObservers(null, NOTIFY_INITIATING_MANUAL_RESTORE, "");
2495
2496    // First collect each window with its id...
2497    let windows = {};
2498    this._forEachBrowserWindow(function(aWindow) {
2499      if (aWindow.__SS_lastSessionWindowID)
2500        windows[aWindow.__SS_lastSessionWindowID] = aWindow;
2501    });
2502
2503    let lastSessionState = LastSession.getState();
2504
2505    // This shouldn't ever be the case...
2506    if (!lastSessionState.windows.length) {
2507      throw Components.Exception("lastSessionState has no windows", Cr.NS_ERROR_UNEXPECTED);
2508    }
2509
2510    // We're technically doing a restore, so set things up so we send the
2511    // notification when we're done. We want to send "sessionstore-browser-state-restored".
2512    this._restoreCount = lastSessionState.windows.length;
2513    this._browserSetState = true;
2514
2515    // We want to re-use the last opened window instead of opening a new one in
2516    // the case where it's "empty" and not associated with a window in the session.
2517    // We will do more processing via _prepWindowToRestoreInto if we need to use
2518    // the lastWindow.
2519    let lastWindow = this._getMostRecentBrowserWindow();
2520    let canUseLastWindow = lastWindow &&
2521                           !lastWindow.__SS_lastSessionWindowID;
2522
2523    // global data must be restored before restoreWindow is called so that
2524    // it happens before observers are notified
2525    this._globalState.setFromState(lastSessionState);
2526
2527    // Restore into windows or open new ones as needed.
2528    for (let i = 0; i < lastSessionState.windows.length; i++) {
2529      let winState = lastSessionState.windows[i];
2530      let lastSessionWindowID = winState.__lastSessionWindowID;
2531      // delete lastSessionWindowID so we don't add that to the window again
2532      delete winState.__lastSessionWindowID;
2533
2534      // See if we can use an open window. First try one that is associated with
2535      // the state we're trying to restore and then fallback to the last selected
2536      // window.
2537      let windowToUse = windows[lastSessionWindowID];
2538      if (!windowToUse && canUseLastWindow) {
2539        windowToUse = lastWindow;
2540        canUseLastWindow = false;
2541      }
2542
2543      let [canUseWindow, canOverwriteTabs] = this._prepWindowToRestoreInto(windowToUse);
2544
2545      // If there's a window already open that we can restore into, use that
2546      if (canUseWindow) {
2547        // Since we're not overwriting existing tabs, we want to merge _closedTabs,
2548        // putting existing ones first. Then make sure we're respecting the max pref.
2549        if (winState._closedTabs && winState._closedTabs.length) {
2550          let curWinState = this._windows[windowToUse.__SSi];
2551          curWinState._closedTabs = curWinState._closedTabs.concat(winState._closedTabs);
2552          curWinState._closedTabs.splice(this._max_tabs_undo, curWinState._closedTabs.length);
2553        }
2554
2555        // Restore into that window - pretend it's a followup since we'll already
2556        // have a focused window.
2557        //XXXzpao This is going to merge extData together (taking what was in
2558        //        winState over what is in the window already.
2559        let options = {overwriteTabs: canOverwriteTabs, isFollowUp: true};
2560        this.restoreWindow(windowToUse, winState, options);
2561      }
2562      else {
2563        this._openWindowWithState({ windows: [winState] });
2564      }
2565    }
2566
2567    // Merge closed windows from this session with ones from last session
2568    if (lastSessionState._closedWindows) {
2569      this._closedWindows = this._closedWindows.concat(lastSessionState._closedWindows);
2570      this._capClosedWindows();
2571    }
2572
2573    if (lastSessionState.scratchpads) {
2574      ScratchpadManager.restoreSession(lastSessionState.scratchpads);
2575    }
2576
2577    // Set data that persists between sessions
2578    this._recentCrashes = lastSessionState.session &&
2579                          lastSessionState.session.recentCrashes || 0;
2580
2581    // Update the session start time using the restored session state.
2582    this._updateSessionStartTime(lastSessionState);
2583
2584    LastSession.clear();
2585  },
2586
2587  /**
2588   * Revive a crashed tab and restore its state from before it crashed.
2589   *
2590   * @param aTab
2591   *        A <xul:tab> linked to a crashed browser. This is a no-op if the
2592   *        browser hasn't actually crashed, or is not associated with a tab.
2593   *        This function will also throw if the browser happens to be remote.
2594   */
2595  reviveCrashedTab(aTab) {
2596    if (!aTab) {
2597      throw new Error("SessionStore.reviveCrashedTab expected a tab, but got null.");
2598    }
2599
2600    let browser = aTab.linkedBrowser;
2601    if (!this._crashedBrowsers.has(browser.permanentKey)) {
2602      return;
2603    }
2604
2605    // Sanity check - the browser to be revived should not be remote
2606    // at this point.
2607    if (browser.isRemoteBrowser) {
2608      throw new Error("SessionStore.reviveCrashedTab: " +
2609                      "Somehow a crashed browser is still remote.")
2610    }
2611
2612    // We put the browser at about:blank in case the user is
2613    // restoring tabs on demand. This way, the user won't see
2614    // a flash of the about:tabcrashed page after selecting
2615    // the revived tab.
2616    aTab.removeAttribute("crashed");
2617    browser.loadURI("about:blank", null, null);
2618
2619    let data = TabState.collect(aTab);
2620    this.restoreTab(aTab, data, {
2621      forceOnDemand: true,
2622    });
2623  },
2624
2625  /**
2626   * Revive all crashed tabs and reset the crashed tabs count to 0.
2627   */
2628  reviveAllCrashedTabs() {
2629    let windowsEnum = Services.wm.getEnumerator("navigator:browser");
2630    while (windowsEnum.hasMoreElements()) {
2631      let window = windowsEnum.getNext();
2632      for (let tab of window.gBrowser.tabs) {
2633        this.reviveCrashedTab(tab);
2634      }
2635    }
2636  },
2637
2638  /**
2639   * Navigate the given |tab| by first collecting its current state and then
2640   * either changing only the index of the currently shown history entry,
2641   * or restoring the exact same state again and passing the new URL to load
2642   * in |loadArguments|. Use this method to seamlessly switch between pages
2643   * loaded in the parent and pages loaded in the child process.
2644   *
2645   * This method might be called multiple times before it has finished
2646   * flushing the browser tab. If that occurs, the loadArguments from
2647   * the most recent call to navigateAndRestore will be used once the
2648   * flush has finished.
2649   */
2650  navigateAndRestore(tab, loadArguments, historyIndex) {
2651    let window = tab.ownerGlobal;
2652    NS_ASSERT(window.__SSi, "tab's window must be tracked");
2653    let browser = tab.linkedBrowser;
2654
2655    // Were we already waiting for a flush from a previous call to
2656    // navigateAndRestore on this tab?
2657    let alreadyRestoring =
2658      this._remotenessChangingBrowsers.has(browser.permanentKey);
2659
2660    // Stash the most recent loadArguments in this WeakMap so that
2661    // we know to use it when the TabStateFlusher.flush resolves.
2662    this._remotenessChangingBrowsers.set(browser.permanentKey, loadArguments);
2663
2664    if (alreadyRestoring) {
2665      // This tab was already being restored to run in the
2666      // correct process. We're done here.
2667      return;
2668    }
2669
2670    // Set tab title to "Connecting..." and start the throbber to pretend we're
2671    // doing something while actually waiting for data from the frame script.
2672    window.gBrowser.setTabTitleLoading(tab);
2673    tab.setAttribute("busy", "true");
2674
2675    // Flush to get the latest tab state.
2676    TabStateFlusher.flush(browser).then(() => {
2677      // loadArguments might have been overwritten by multiple calls
2678      // to navigateAndRestore while we waited for the tab to flush,
2679      // so we use the most recently stored one.
2680      let recentLoadArguments =
2681        this._remotenessChangingBrowsers.get(browser.permanentKey);
2682      this._remotenessChangingBrowsers.delete(browser.permanentKey);
2683
2684      // The tab might have been closed/gone in the meantime.
2685      if (tab.closing || !tab.linkedBrowser) {
2686        return;
2687      }
2688
2689      let window = tab.ownerGlobal;
2690
2691      // The tab or its window might be gone.
2692      if (!window || !window.__SSi || window.closed) {
2693        return;
2694      }
2695
2696      let tabState = TabState.clone(tab);
2697      let options = {
2698        restoreImmediately: true,
2699        // We want to make sure that this information is passed to restoreTab
2700        // whether or not a historyIndex is passed in. Thus, we extract it from
2701        // the loadArguments.
2702        reloadInFreshProcess: !!recentLoadArguments.reloadInFreshProcess,
2703      };
2704
2705      if (historyIndex >= 0) {
2706        tabState.index = historyIndex + 1;
2707        tabState.index = Math.max(1, Math.min(tabState.index, tabState.entries.length));
2708      } else {
2709        options.loadArguments = recentLoadArguments;
2710      }
2711
2712      // Need to reset restoring tabs.
2713      if (tab.linkedBrowser.__SS_restoreState) {
2714        this._resetLocalTabRestoringState(tab);
2715      }
2716
2717      // Restore the state into the tab.
2718      this.restoreTab(tab, tabState, options);
2719    });
2720
2721    tab.linkedBrowser.__SS_restoreState = TAB_STATE_WILL_RESTORE;
2722  },
2723
2724  /**
2725   * Retrieves the latest session history information for a tab. The cached data
2726   * is returned immediately, but a callback may be provided that supplies
2727   * up-to-date data when or if it is available. The callback is passed a single
2728   * argument with data in the same format as the return value.
2729   *
2730   * @param tab tab to retrieve the session history for
2731   * @param updatedCallback function to call with updated data as the single argument
2732   * @returns a object containing 'index' specifying the current index, and an
2733   * array 'entries' containing an object for each history item.
2734   */
2735  getSessionHistory(tab, updatedCallback) {
2736    if (updatedCallback) {
2737      TabStateFlusher.flush(tab.linkedBrowser).then(() => {
2738        let sessionHistory = this.getSessionHistory(tab);
2739        if (sessionHistory) {
2740          updatedCallback(sessionHistory);
2741        }
2742      });
2743    }
2744
2745    // Don't continue if the tab was closed before TabStateFlusher.flush resolves.
2746    if (tab.linkedBrowser) {
2747      let tabState = TabState.collect(tab);
2748      return { index: tabState.index - 1, entries: tabState.entries }
2749    }
2750  },
2751
2752  /**
2753   * See if aWindow is usable for use when restoring a previous session via
2754   * restoreLastSession. If usable, prepare it for use.
2755   *
2756   * @param aWindow
2757   *        the window to inspect & prepare
2758   * @returns [canUseWindow, canOverwriteTabs]
2759   *          canUseWindow: can the window be used to restore into
2760   *          canOverwriteTabs: all of the current tabs are home pages and we
2761   *                            can overwrite them
2762   */
2763  _prepWindowToRestoreInto: function ssi_prepWindowToRestoreInto(aWindow) {
2764    if (!aWindow)
2765      return [false, false];
2766
2767    // We might be able to overwrite the existing tabs instead of just adding
2768    // the previous session's tabs to the end. This will be set if possible.
2769    let canOverwriteTabs = false;
2770
2771    // Look at the open tabs in comparison to home pages. If all the tabs are
2772    // home pages then we'll end up overwriting all of them. Otherwise we'll
2773    // just close the tabs that match home pages. Tabs with the about:blank
2774    // URI will always be overwritten.
2775    let homePages = ["about:blank"];
2776    let removableTabs = [];
2777    let tabbrowser = aWindow.gBrowser;
2778    let normalTabsLen = tabbrowser.tabs.length - tabbrowser._numPinnedTabs;
2779    let startupPref = this._prefBranch.getIntPref("startup.page");
2780    if (startupPref == 1)
2781      homePages = homePages.concat(aWindow.gHomeButton.getHomePage().split("|"));
2782
2783    for (let i = tabbrowser._numPinnedTabs; i < tabbrowser.tabs.length; i++) {
2784      let tab = tabbrowser.tabs[i];
2785      if (homePages.indexOf(tab.linkedBrowser.currentURI.spec) != -1) {
2786        removableTabs.push(tab);
2787      }
2788    }
2789
2790    if (tabbrowser.tabs.length == removableTabs.length) {
2791      canOverwriteTabs = true;
2792    }
2793    else {
2794      // If we're not overwriting all of the tabs, then close the home tabs.
2795      for (let i = removableTabs.length - 1; i >= 0; i--) {
2796        tabbrowser.removeTab(removableTabs.pop(), { animate: false });
2797      }
2798    }
2799
2800    return [true, canOverwriteTabs];
2801  },
2802
2803  /* ........ Saving Functionality .............. */
2804
2805  /**
2806   * Store window dimensions, visibility, sidebar
2807   * @param aWindow
2808   *        Window reference
2809   */
2810  _updateWindowFeatures: function ssi_updateWindowFeatures(aWindow) {
2811    var winData = this._windows[aWindow.__SSi];
2812
2813    WINDOW_ATTRIBUTES.forEach(function(aAttr) {
2814      winData[aAttr] = this._getWindowDimension(aWindow, aAttr);
2815    }, this);
2816
2817    var hidden = WINDOW_HIDEABLE_FEATURES.filter(function(aItem) {
2818      return aWindow[aItem] && !aWindow[aItem].visible;
2819    });
2820    if (hidden.length != 0)
2821      winData.hidden = hidden.join(",");
2822    else if (winData.hidden)
2823      delete winData.hidden;
2824
2825    var sidebar = aWindow.document.getElementById("sidebar-box").getAttribute("sidebarcommand");
2826    if (sidebar)
2827      winData.sidebar = sidebar;
2828    else if (winData.sidebar)
2829      delete winData.sidebar;
2830  },
2831
2832  /**
2833   * gather session data as object
2834   * @param aUpdateAll
2835   *        Bool update all windows
2836   * @returns object
2837   */
2838  getCurrentState: function (aUpdateAll) {
2839    this._handleClosedWindows();
2840
2841    var activeWindow = this._getMostRecentBrowserWindow();
2842
2843    TelemetryStopwatch.start("FX_SESSION_RESTORE_COLLECT_ALL_WINDOWS_DATA_MS");
2844    if (RunState.isRunning) {
2845      // update the data for all windows with activities since the last save operation
2846      this._forEachBrowserWindow(function(aWindow) {
2847        if (!this._isWindowLoaded(aWindow)) // window data is still in _statesToRestore
2848          return;
2849        if (aUpdateAll || DirtyWindows.has(aWindow) || aWindow == activeWindow) {
2850          this._collectWindowData(aWindow);
2851        }
2852        else { // always update the window features (whose change alone never triggers a save operation)
2853          this._updateWindowFeatures(aWindow);
2854        }
2855      });
2856      DirtyWindows.clear();
2857    }
2858    TelemetryStopwatch.finish("FX_SESSION_RESTORE_COLLECT_ALL_WINDOWS_DATA_MS");
2859
2860    // An array that at the end will hold all current window data.
2861    var total = [];
2862    // The ids of all windows contained in 'total' in the same order.
2863    var ids = [];
2864    // The number of window that are _not_ popups.
2865    var nonPopupCount = 0;
2866    var ix;
2867
2868    // collect the data for all windows
2869    for (ix in this._windows) {
2870      if (this._windows[ix]._restoring) // window data is still in _statesToRestore
2871        continue;
2872      total.push(this._windows[ix]);
2873      ids.push(ix);
2874      if (!this._windows[ix].isPopup)
2875        nonPopupCount++;
2876    }
2877
2878    TelemetryStopwatch.start("FX_SESSION_RESTORE_COLLECT_COOKIES_MS");
2879    SessionCookies.update(total);
2880    TelemetryStopwatch.finish("FX_SESSION_RESTORE_COLLECT_COOKIES_MS");
2881
2882    // collect the data for all windows yet to be restored
2883    for (ix in this._statesToRestore) {
2884      for (let winData of this._statesToRestore[ix].windows) {
2885        total.push(winData);
2886        if (!winData.isPopup)
2887          nonPopupCount++;
2888      }
2889    }
2890
2891    // shallow copy this._closedWindows to preserve current state
2892    let lastClosedWindowsCopy = this._closedWindows.slice();
2893
2894    if (AppConstants.platform != "macosx") {
2895      // If no non-popup browser window remains open, return the state of the last
2896      // closed window(s). We only want to do this when we're actually "ending"
2897      // the session.
2898      //XXXzpao We should do this for _restoreLastWindow == true, but that has
2899      //        its own check for popups. c.f. bug 597619
2900      if (nonPopupCount == 0 && lastClosedWindowsCopy.length > 0 &&
2901          RunState.isQuitting) {
2902        // prepend the last non-popup browser window, so that if the user loads more tabs
2903        // at startup we don't accidentally add them to a popup window
2904        do {
2905          total.unshift(lastClosedWindowsCopy.shift())
2906        } while (total[0].isPopup && lastClosedWindowsCopy.length > 0)
2907      }
2908    }
2909
2910    if (activeWindow) {
2911      this.activeWindowSSiCache = activeWindow.__SSi || "";
2912    }
2913    ix = ids.indexOf(this.activeWindowSSiCache);
2914    // We don't want to restore focus to a minimized window or a window which had all its
2915    // tabs stripped out (doesn't exist).
2916    if (ix != -1 && total[ix] && total[ix].sizemode == "minimized")
2917      ix = -1;
2918
2919    let session = {
2920      lastUpdate: Date.now(),
2921      startTime: this._sessionStartTime,
2922      recentCrashes: this._recentCrashes
2923    };
2924
2925    let state = {
2926      version: ["sessionrestore", FORMAT_VERSION],
2927      windows: total,
2928      selectedWindow: ix + 1,
2929      _closedWindows: lastClosedWindowsCopy,
2930      session: session,
2931      global: this._globalState.getState()
2932    };
2933
2934    if (Cu.isModuleLoaded("resource://devtools/client/scratchpad/scratchpad-manager.jsm")) {
2935      // get open Scratchpad window states too
2936      let scratchpads = ScratchpadManager.getSessionState();
2937      if (scratchpads && scratchpads.length) {
2938        state.scratchpads = scratchpads;
2939      }
2940    }
2941
2942    // Persist the last session if we deferred restoring it
2943    if (LastSession.canRestore) {
2944      state.lastSessionState = LastSession.getState();
2945    }
2946
2947    // If we were called by the SessionSaver and started with only a private
2948    // window we want to pass the deferred initial state to not lose the
2949    // previous session.
2950    if (this._deferredInitialState) {
2951      state.deferredInitialState = this._deferredInitialState;
2952    }
2953
2954    return state;
2955  },
2956
2957  /**
2958   * serialize session data for a window
2959   * @param aWindow
2960   *        Window reference
2961   * @returns string
2962   */
2963  _getWindowState: function ssi_getWindowState(aWindow) {
2964    if (!this._isWindowLoaded(aWindow))
2965      return this._statesToRestore[aWindow.__SS_restoreID];
2966
2967    if (RunState.isRunning) {
2968      this._collectWindowData(aWindow);
2969    }
2970
2971    let windows = [this._windows[aWindow.__SSi]];
2972    SessionCookies.update(windows);
2973
2974    return { windows: windows };
2975  },
2976
2977  /**
2978   * Gathers data about a window and its tabs, and updates its
2979   * entry in this._windows.
2980   *
2981   * @param aWindow
2982   *        Window references.
2983   * @returns a Map mapping the browser tabs from aWindow to the tab
2984   *          entry that was put into the window data in this._windows.
2985   */
2986  _collectWindowData: function ssi_collectWindowData(aWindow) {
2987    let tabMap = new Map();
2988
2989    if (!this._isWindowLoaded(aWindow))
2990      return tabMap;
2991
2992    let tabbrowser = aWindow.gBrowser;
2993    let tabs = tabbrowser.tabs;
2994    let winData = this._windows[aWindow.__SSi];
2995    let tabsData = winData.tabs = [];
2996
2997    // update the internal state data for this window
2998    for (let tab of tabs) {
2999      let tabData = TabState.collect(tab);
3000      tabMap.set(tab, tabData);
3001      tabsData.push(tabData);
3002    }
3003    winData.selected = tabbrowser.mTabBox.selectedIndex + 1;
3004
3005    this._updateWindowFeatures(aWindow);
3006
3007    // Make sure we keep __SS_lastSessionWindowID around for cases like entering
3008    // or leaving PB mode.
3009    if (aWindow.__SS_lastSessionWindowID)
3010      this._windows[aWindow.__SSi].__lastSessionWindowID =
3011        aWindow.__SS_lastSessionWindowID;
3012
3013    DirtyWindows.remove(aWindow);
3014    return tabMap;
3015  },
3016
3017  /* ........ Restoring Functionality .............. */
3018
3019  /**
3020   * restore features to a single window
3021   * @param aWindow
3022   *        Window reference to the window to use for restoration
3023   * @param winData
3024   *        JS object
3025   * @param aOptions
3026   *        {overwriteTabs: true} to overwrite existing tabs w/ new ones
3027   *        {isFollowUp: true} if this is not the restoration of the 1st window
3028   *        {firstWindow: true} if this is the first non-private window we're
3029   *                            restoring in this session, that might open an
3030   *                            external link as well
3031   */
3032  restoreWindow: function ssi_restoreWindow(aWindow, winData, aOptions = {}) {
3033    let overwriteTabs = aOptions && aOptions.overwriteTabs;
3034    let isFollowUp = aOptions && aOptions.isFollowUp;
3035    let firstWindow = aOptions && aOptions.firstWindow;
3036
3037    if (isFollowUp) {
3038      this.windowToFocus = aWindow;
3039    }
3040
3041    // initialize window if necessary
3042    if (aWindow && (!aWindow.__SSi || !this._windows[aWindow.__SSi]))
3043      this.onLoad(aWindow);
3044
3045    TelemetryStopwatch.start("FX_SESSION_RESTORE_RESTORE_WINDOW_MS");
3046
3047    // We're not returning from this before we end up calling restoreTabs
3048    // for this window, so make sure we send the SSWindowStateBusy event.
3049    this._setWindowStateBusy(aWindow);
3050
3051    if (!winData.tabs) {
3052      winData.tabs = [];
3053    }
3054
3055    // don't restore a single blank tab when we've had an external
3056    // URL passed in for loading at startup (cf. bug 357419)
3057    else if (firstWindow && !overwriteTabs && winData.tabs.length == 1 &&
3058             (!winData.tabs[0].entries || winData.tabs[0].entries.length == 0)) {
3059      winData.tabs = [];
3060    }
3061
3062    var tabbrowser = aWindow.gBrowser;
3063    var openTabCount = overwriteTabs ? tabbrowser.browsers.length : -1;
3064    var newTabCount = winData.tabs.length;
3065    var tabs = [];
3066
3067    // disable smooth scrolling while adding, moving, removing and selecting tabs
3068    var tabstrip = tabbrowser.tabContainer.mTabstrip;
3069    var smoothScroll = tabstrip.smoothScroll;
3070    tabstrip.smoothScroll = false;
3071
3072    // unpin all tabs to ensure they are not reordered in the next loop
3073    if (overwriteTabs) {
3074      for (let t = tabbrowser._numPinnedTabs - 1; t > -1; t--)
3075        tabbrowser.unpinTab(tabbrowser.tabs[t]);
3076    }
3077
3078    // We need to keep track of the initially open tabs so that they
3079    // can be moved to the end of the restored tabs.
3080    let initialTabs = [];
3081    if (!overwriteTabs && firstWindow) {
3082      initialTabs = Array.slice(tabbrowser.tabs);
3083    }
3084
3085    // make sure that the selected tab won't be closed in order to
3086    // prevent unnecessary flickering
3087    if (overwriteTabs && tabbrowser.selectedTab._tPos >= newTabCount)
3088      tabbrowser.moveTabTo(tabbrowser.selectedTab, newTabCount - 1);
3089
3090    let numVisibleTabs = 0;
3091
3092    for (var t = 0; t < newTabCount; t++) {
3093      // When trying to restore into existing tab, we also take the userContextId
3094      // into account if present.
3095      let userContextId = winData.tabs[t].userContextId;
3096      let reuseExisting = t < openTabCount &&
3097                          (tabbrowser.tabs[t].getAttribute("usercontextid") == (userContextId || ""));
3098      // If the tab is pinned, then we'll be loading it right away, and
3099      // there's no need to cause a remoteness flip by loading it initially
3100      // non-remote.
3101      let forceNotRemote = !winData.tabs[t].pinned;
3102      let tab = reuseExisting ? tabbrowser.tabs[t] :
3103                                tabbrowser.addTab("about:blank",
3104                                                  {skipAnimation: true,
3105                                                   forceNotRemote,
3106                                                   userContextId});
3107
3108      // If we inserted a new tab because the userContextId didn't match with the
3109      // open tab, even though `t < openTabCount`, we need to remove that open tab
3110      // and put the newly added tab in its place.
3111      if (!reuseExisting && t < openTabCount) {
3112        tabbrowser.removeTab(tabbrowser.tabs[t]);
3113        tabbrowser.moveTabTo(tab, t);
3114      }
3115
3116      tabs.push(tab);
3117
3118      if (winData.tabs[t].pinned)
3119        tabbrowser.pinTab(tabs[t]);
3120
3121      if (winData.tabs[t].hidden) {
3122        tabbrowser.hideTab(tabs[t]);
3123      }
3124      else {
3125        tabbrowser.showTab(tabs[t]);
3126        numVisibleTabs++;
3127      }
3128
3129      if (!!winData.tabs[t].muted != tabs[t].linkedBrowser.audioMuted) {
3130        tabs[t].toggleMuteAudio(winData.tabs[t].muteReason);
3131      }
3132    }
3133
3134    if (!overwriteTabs && firstWindow) {
3135      // Move the originally open tabs to the end
3136      let endPosition = tabbrowser.tabs.length - 1;
3137      for (let i = 0; i < initialTabs.length; i++) {
3138        tabbrowser.moveTabTo(initialTabs[i], endPosition);
3139      }
3140    }
3141
3142    // if all tabs to be restored are hidden, make the first one visible
3143    if (!numVisibleTabs && winData.tabs.length) {
3144      winData.tabs[0].hidden = false;
3145      tabbrowser.showTab(tabs[0]);
3146    }
3147
3148    // If overwriting tabs, we want to reset each tab's "restoring" state. Since
3149    // we're overwriting those tabs, they should no longer be restoring. The
3150    // tabs will be rebuilt and marked if they need to be restored after loading
3151    // state (in restoreTabs).
3152    if (overwriteTabs) {
3153      for (let i = 0; i < tabbrowser.tabs.length; i++) {
3154        let tab = tabbrowser.tabs[i];
3155        if (tabbrowser.browsers[i].__SS_restoreState)
3156          this._resetTabRestoringState(tab);
3157      }
3158    }
3159
3160    // We want to correlate the window with data from the last session, so
3161    // assign another id if we have one. Otherwise clear so we don't do
3162    // anything with it.
3163    delete aWindow.__SS_lastSessionWindowID;
3164    if (winData.__lastSessionWindowID)
3165      aWindow.__SS_lastSessionWindowID = winData.__lastSessionWindowID;
3166
3167    // when overwriting tabs, remove all superflous ones
3168    if (overwriteTabs && newTabCount < openTabCount) {
3169      Array.slice(tabbrowser.tabs, newTabCount, openTabCount)
3170           .forEach(tabbrowser.removeTab, tabbrowser);
3171    }
3172
3173    if (overwriteTabs) {
3174      this.restoreWindowFeatures(aWindow, winData);
3175      delete this._windows[aWindow.__SSi].extData;
3176    }
3177    if (winData.cookies) {
3178      SessionCookies.restore(winData.cookies);
3179    }
3180    if (winData.extData) {
3181      if (!this._windows[aWindow.__SSi].extData) {
3182        this._windows[aWindow.__SSi].extData = {};
3183      }
3184      for (var key in winData.extData) {
3185        this._windows[aWindow.__SSi].extData[key] = winData.extData[key];
3186      }
3187    }
3188
3189    let newClosedTabsData = winData._closedTabs || [];
3190
3191    if (overwriteTabs || firstWindow) {
3192      // Overwrite existing closed tabs data when overwriteTabs=true
3193      // or we're the first window to be restored.
3194      this._windows[aWindow.__SSi]._closedTabs = newClosedTabsData;
3195    } else if (this._max_tabs_undo > 0) {
3196      // If we merge tabs, we also want to merge closed tabs data. We'll assume
3197      // the restored tabs were closed more recently and append the current list
3198      // of closed tabs to the new one...
3199      newClosedTabsData =
3200        newClosedTabsData.concat(this._windows[aWindow.__SSi]._closedTabs);
3201
3202      // ... and make sure that we don't exceed the max number of closed tabs
3203      // we can restore.
3204      this._windows[aWindow.__SSi]._closedTabs =
3205        newClosedTabsData.slice(0, this._max_tabs_undo);
3206    }
3207
3208    // Restore tabs, if any.
3209    if (winData.tabs.length) {
3210      this.restoreTabs(aWindow, tabs, winData.tabs,
3211        (overwriteTabs ? (parseInt(winData.selected || "1")) : 0));
3212    }
3213
3214    // set smoothScroll back to the original value
3215    tabstrip.smoothScroll = smoothScroll;
3216
3217    TelemetryStopwatch.finish("FX_SESSION_RESTORE_RESTORE_WINDOW_MS");
3218
3219    this._setWindowStateReady(aWindow);
3220
3221    this._sendWindowRestoredNotification(aWindow);
3222
3223    Services.obs.notifyObservers(aWindow, NOTIFY_SINGLE_WINDOW_RESTORED, "");
3224
3225    this._sendRestoreCompletedNotifications();
3226  },
3227
3228  /**
3229   * Restore multiple windows using the provided state.
3230   * @param aWindow
3231   *        Window reference to the first window to use for restoration.
3232   *        Additionally required windows will be opened.
3233   * @param aState
3234   *        JS object or JSON string
3235   * @param aOptions
3236   *        {overwriteTabs: true} to overwrite existing tabs w/ new ones
3237   *        {isFollowUp: true} if this is not the restoration of the 1st window
3238   *        {firstWindow: true} if this is the first non-private window we're
3239   *                            restoring in this session, that might open an
3240   *                            external link as well
3241   */
3242  restoreWindows: function ssi_restoreWindows(aWindow, aState, aOptions = {}) {
3243    let isFollowUp = aOptions && aOptions.isFollowUp;
3244
3245    if (isFollowUp) {
3246      this.windowToFocus = aWindow;
3247    }
3248
3249    // initialize window if necessary
3250    if (aWindow && (!aWindow.__SSi || !this._windows[aWindow.__SSi]))
3251      this.onLoad(aWindow);
3252
3253    let root;
3254    try {
3255      root = (typeof aState == "string") ? JSON.parse(aState) : aState;
3256    }
3257    catch (ex) { // invalid state object - don't restore anything
3258      debug(ex);
3259      this._sendRestoreCompletedNotifications();
3260      return;
3261    }
3262
3263    // Restore closed windows if any.
3264    if (root._closedWindows) {
3265      this._closedWindows = root._closedWindows;
3266    }
3267
3268    // We're done here if there are no windows.
3269    if (!root.windows || !root.windows.length) {
3270      this._sendRestoreCompletedNotifications();
3271      return;
3272    }
3273
3274    if (!root.selectedWindow || root.selectedWindow > root.windows.length) {
3275      root.selectedWindow = 0;
3276    }
3277
3278    // open new windows for all further window entries of a multi-window session
3279    // (unless they don't contain any tab data)
3280    let winData;
3281    for (var w = 1; w < root.windows.length; w++) {
3282      winData = root.windows[w];
3283      if (winData && winData.tabs && winData.tabs[0]) {
3284        var window = this._openWindowWithState({ windows: [winData] });
3285        if (w == root.selectedWindow - 1) {
3286          this.windowToFocus = window;
3287        }
3288      }
3289    }
3290
3291    this.restoreWindow(aWindow, root.windows[0], aOptions);
3292
3293    if (aState.scratchpads) {
3294      ScratchpadManager.restoreSession(aState.scratchpads);
3295    }
3296  },
3297
3298  /**
3299   * Manage history restoration for a window
3300   * @param aWindow
3301   *        Window to restore the tabs into
3302   * @param aTabs
3303   *        Array of tab references
3304   * @param aTabData
3305   *        Array of tab data
3306   * @param aSelectTab
3307   *        Index of the tab to select. This is a 1-based index where "1"
3308   *        indicates the first tab should be selected, and "0" indicates that
3309   *        the currently selected tab will not be changed.
3310   */
3311  restoreTabs(aWindow, aTabs, aTabData, aSelectTab) {
3312    var tabbrowser = aWindow.gBrowser;
3313
3314    if (!this._isWindowLoaded(aWindow)) {
3315      // from now on, the data will come from the actual window
3316      delete this._statesToRestore[aWindow.__SS_restoreID];
3317      delete aWindow.__SS_restoreID;
3318      delete this._windows[aWindow.__SSi]._restoring;
3319    }
3320
3321    let numTabsToRestore = aTabs.length;
3322    let numTabsInWindow = tabbrowser.tabs.length;
3323    let tabsDataArray = this._windows[aWindow.__SSi].tabs;
3324
3325    // Update the window state in case we shut down without being notified.
3326    // Individual tab states will be taken care of by restoreTab() below.
3327    if (numTabsInWindow == numTabsToRestore) {
3328      // Remove all previous tab data.
3329      tabsDataArray.length = 0;
3330    } else {
3331      // Remove all previous tab data except tabs that should not be overriden.
3332      tabsDataArray.splice(numTabsInWindow - numTabsToRestore);
3333    }
3334
3335    // Let the tab data array have the right number of slots.
3336    tabsDataArray.length = numTabsInWindow;
3337
3338    // If provided, set the selected tab.
3339    if (aSelectTab > 0 && aSelectTab <= aTabs.length) {
3340      tabbrowser.selectedTab = aTabs[aSelectTab - 1];
3341
3342      // Update the window state in case we shut down without being notified.
3343      this._windows[aWindow.__SSi].selected = aSelectTab;
3344    }
3345
3346    // Restore all tabs.
3347    for (let t = 0; t < aTabs.length; t++) {
3348      this.restoreTab(aTabs[t], aTabData[t]);
3349    }
3350  },
3351
3352  // Restores the given tab state for a given tab.
3353  restoreTab(tab, tabData, options = {}) {
3354    NS_ASSERT(!tab.linkedBrowser.__SS_restoreState,
3355              "must reset tab before calling restoreTab()");
3356
3357    let restoreImmediately = options.restoreImmediately;
3358    let loadArguments = options.loadArguments;
3359    let browser = tab.linkedBrowser;
3360    let window = tab.ownerGlobal;
3361    let tabbrowser = window.gBrowser;
3362    let forceOnDemand = options.forceOnDemand;
3363    let reloadInFreshProcess = options.reloadInFreshProcess;
3364
3365    let willRestoreImmediately = restoreImmediately ||
3366                                 tabbrowser.selectedBrowser == browser ||
3367                                 loadArguments;
3368
3369    if (!willRestoreImmediately && !forceOnDemand) {
3370      TabRestoreQueue.add(tab);
3371    }
3372
3373    this._maybeUpdateBrowserRemoteness({ tabbrowser, tab,
3374                                         willRestoreImmediately });
3375
3376    // Increase the busy state counter before modifying the tab.
3377    this._setWindowStateBusy(window);
3378
3379    // It's important to set the window state to dirty so that
3380    // we collect their data for the first time when saving state.
3381    DirtyWindows.add(window);
3382
3383    // In case we didn't collect/receive data for any tabs yet we'll have to
3384    // fill the array with at least empty tabData objects until |_tPos| or
3385    // we'll end up with |null| entries.
3386    for (let otherTab of Array.slice(tabbrowser.tabs, 0, tab._tPos)) {
3387      let emptyState = {entries: [], lastAccessed: otherTab.lastAccessed};
3388      this._windows[window.__SSi].tabs.push(emptyState);
3389    }
3390
3391    // Update the tab state in case we shut down without being notified.
3392    this._windows[window.__SSi].tabs[tab._tPos] = tabData;
3393
3394    // Prepare the tab so that it can be properly restored. We'll pin/unpin
3395    // and show/hide tabs as necessary. We'll also attach a copy of the tab's
3396    // data in case we close it before it's been restored.
3397    if (tabData.pinned) {
3398      tabbrowser.pinTab(tab);
3399    } else {
3400      tabbrowser.unpinTab(tab);
3401    }
3402
3403    if (tabData.hidden) {
3404      tabbrowser.hideTab(tab);
3405    } else {
3406      tabbrowser.showTab(tab);
3407    }
3408
3409    if (!!tabData.muted != browser.audioMuted) {
3410      tab.toggleMuteAudio(tabData.muteReason);
3411    }
3412
3413    if (tabData.lastAccessed) {
3414      tab.updateLastAccessed(tabData.lastAccessed);
3415    }
3416
3417    if ("attributes" in tabData) {
3418      // Ensure that we persist tab attributes restored from previous sessions.
3419      Object.keys(tabData.attributes).forEach(a => TabAttributes.persist(a));
3420    }
3421
3422    if (!tabData.entries) {
3423      tabData.entries = [];
3424    }
3425    if (tabData.extData) {
3426      tab.__SS_extdata = Cu.cloneInto(tabData.extData, {});
3427    } else {
3428      delete tab.__SS_extdata;
3429    }
3430
3431    // Tab is now open.
3432    delete tabData.closedAt;
3433
3434    // Ensure the index is in bounds.
3435    let activeIndex = (tabData.index || tabData.entries.length) - 1;
3436    activeIndex = Math.min(activeIndex, tabData.entries.length - 1);
3437    activeIndex = Math.max(activeIndex, 0);
3438
3439    // Save the index in case we updated it above.
3440    tabData.index = activeIndex + 1;
3441
3442    // Start a new epoch to discard all frame script messages relating to a
3443    // previous epoch. All async messages that are still on their way to chrome
3444    // will be ignored and don't override any tab data set when restoring.
3445    let epoch = this.startNextEpoch(browser);
3446
3447    // keep the data around to prevent dataloss in case
3448    // a tab gets closed before it's been properly restored
3449    browser.__SS_restoreState = TAB_STATE_NEEDS_RESTORE;
3450    browser.setAttribute("pending", "true");
3451    tab.setAttribute("pending", "true");
3452
3453    // If we're restoring this tab, it certainly shouldn't be in
3454    // the ignored set anymore.
3455    this._crashedBrowsers.delete(browser.permanentKey);
3456
3457    // Update the persistent tab state cache with |tabData| information.
3458    TabStateCache.update(browser, {
3459      history: {entries: tabData.entries, index: tabData.index},
3460      scroll: tabData.scroll || null,
3461      storage: tabData.storage || null,
3462      formdata: tabData.formdata || null,
3463      disallow: tabData.disallow || null,
3464      pageStyle: tabData.pageStyle || null,
3465
3466      // This information is only needed until the tab has finished restoring.
3467      // When that's done it will be removed from the cache and we always
3468      // collect it in TabState._collectBaseTabData().
3469      image: tabData.image || "",
3470      iconLoadingPrincipal: tabData.iconLoadingPrincipal || null,
3471      userTypedValue: tabData.userTypedValue || "",
3472      userTypedClear: tabData.userTypedClear || 0
3473    });
3474
3475    browser.messageManager.sendAsyncMessage("SessionStore:restoreHistory",
3476                                            {tabData: tabData, epoch: epoch, loadArguments});
3477
3478    // Restore tab attributes.
3479    if ("attributes" in tabData) {
3480      TabAttributes.set(tab, tabData.attributes);
3481    }
3482
3483    // This could cause us to ignore MAX_CONCURRENT_TAB_RESTORES a bit, but
3484    // it ensures each window will have its selected tab loaded.
3485    if (willRestoreImmediately) {
3486      this.restoreTabContent(tab, loadArguments, reloadInFreshProcess);
3487    } else if (!forceOnDemand) {
3488      this.restoreNextTab();
3489    }
3490
3491    // Decrease the busy state counter after we're done.
3492    this._setWindowStateReady(window);
3493  },
3494
3495  /**
3496   * Kicks off restoring the given tab.
3497   *
3498   * @param aTab
3499   *        the tab to restore
3500   * @param aLoadArguments
3501   *        optional load arguments used for loadURI()
3502   * @param aReloadInFreshProcess
3503   *        true if we want to reload into a fresh process
3504   */
3505  restoreTabContent: function (aTab, aLoadArguments = null, aReloadInFreshProcess = false) {
3506    if (aTab.hasAttribute("customizemode") && !aLoadArguments) {
3507      return;
3508    }
3509
3510    let browser = aTab.linkedBrowser;
3511    let window = aTab.ownerGlobal;
3512    let tabbrowser = window.gBrowser;
3513    let tabData = TabState.clone(aTab);
3514    let activeIndex = tabData.index - 1;
3515    let activePageData = tabData.entries[activeIndex] || null;
3516    let uri = activePageData ? activePageData.url || null : null;
3517    if (aLoadArguments) {
3518      uri = aLoadArguments.uri;
3519      if (aLoadArguments.userContextId) {
3520        browser.setAttribute("usercontextid", aLoadArguments.userContextId);
3521      }
3522    }
3523
3524    // We have to mark this tab as restoring first, otherwise
3525    // the "pending" attribute will be applied to the linked
3526    // browser, which removes it from the display list. We cannot
3527    // flip the remoteness of any browser that is not being displayed.
3528    this.markTabAsRestoring(aTab);
3529
3530    let isRemotenessUpdate = false;
3531    if (aReloadInFreshProcess) {
3532      isRemotenessUpdate = tabbrowser.switchBrowserIntoFreshProcess(browser);
3533    } else {
3534      isRemotenessUpdate = tabbrowser.updateBrowserRemotenessByURL(browser, uri);
3535    }
3536
3537    if (isRemotenessUpdate) {
3538      // We updated the remoteness, so we need to send the history down again.
3539      //
3540      // Start a new epoch to discard all frame script messages relating to a
3541      // previous epoch. All async messages that are still on their way to chrome
3542      // will be ignored and don't override any tab data set when restoring.
3543      let epoch = this.startNextEpoch(browser);
3544
3545      browser.messageManager.sendAsyncMessage("SessionStore:restoreHistory", {
3546        tabData: tabData,
3547        epoch: epoch,
3548        loadArguments: aLoadArguments,
3549        isRemotenessUpdate,
3550      });
3551
3552    }
3553
3554    // If the restored browser wants to show view source content, start up a
3555    // view source browser that will load the required frame script.
3556    if (uri && ViewSourceBrowser.isViewSource(uri)) {
3557      new ViewSourceBrowser(browser);
3558    }
3559
3560    browser.messageManager.sendAsyncMessage("SessionStore:restoreTabContent",
3561      {loadArguments: aLoadArguments, isRemotenessUpdate});
3562  },
3563
3564  /**
3565   * Marks a given pending tab as restoring.
3566   *
3567   * @param aTab
3568   *        the pending tab to mark as restoring
3569   */
3570  markTabAsRestoring(aTab) {
3571    let browser = aTab.linkedBrowser;
3572    if (browser.__SS_restoreState != TAB_STATE_NEEDS_RESTORE) {
3573      throw new Error("Given tab is not pending.");
3574    }
3575
3576    // Make sure that this tab is removed from the priority queue.
3577    TabRestoreQueue.remove(aTab);
3578
3579    // Increase our internal count.
3580    this._tabsRestoringCount++;
3581
3582    // Set this tab's state to restoring
3583    browser.__SS_restoreState = TAB_STATE_RESTORING;
3584    browser.removeAttribute("pending");
3585    aTab.removeAttribute("pending");
3586  },
3587
3588  /**
3589   * This _attempts_ to restore the next available tab. If the restore fails,
3590   * then we will attempt the next one.
3591   * There are conditions where this won't do anything:
3592   *   if we're in the process of quitting
3593   *   if there are no tabs to restore
3594   *   if we have already reached the limit for number of tabs to restore
3595   */
3596  restoreNextTab: function ssi_restoreNextTab() {
3597    // If we call in here while quitting, we don't actually want to do anything
3598    if (RunState.isQuitting)
3599      return;
3600
3601    // Don't exceed the maximum number of concurrent tab restores.
3602    if (this._tabsRestoringCount >= MAX_CONCURRENT_TAB_RESTORES)
3603      return;
3604
3605    let tab = TabRestoreQueue.shift();
3606    if (tab) {
3607      this.restoreTabContent(tab);
3608    }
3609  },
3610
3611  /**
3612   * Restore visibility and dimension features to a window
3613   * @param aWindow
3614   *        Window reference
3615   * @param aWinData
3616   *        Object containing session data for the window
3617   */
3618  restoreWindowFeatures: function ssi_restoreWindowFeatures(aWindow, aWinData) {
3619    var hidden = (aWinData.hidden)?aWinData.hidden.split(","):[];
3620    WINDOW_HIDEABLE_FEATURES.forEach(function(aItem) {
3621      aWindow[aItem].visible = hidden.indexOf(aItem) == -1;
3622    });
3623
3624    if (aWinData.isPopup) {
3625      this._windows[aWindow.__SSi].isPopup = true;
3626      if (aWindow.gURLBar) {
3627        aWindow.gURLBar.readOnly = true;
3628        aWindow.gURLBar.setAttribute("enablehistory", "false");
3629      }
3630    }
3631    else {
3632      delete this._windows[aWindow.__SSi].isPopup;
3633      if (aWindow.gURLBar) {
3634        aWindow.gURLBar.readOnly = false;
3635        aWindow.gURLBar.setAttribute("enablehistory", "true");
3636      }
3637    }
3638
3639    var _this = this;
3640    aWindow.setTimeout(function() {
3641      _this.restoreDimensions.apply(_this, [aWindow,
3642        +(aWinData.width || 0),
3643        +(aWinData.height || 0),
3644        "screenX" in aWinData ? +aWinData.screenX : NaN,
3645        "screenY" in aWinData ? +aWinData.screenY : NaN,
3646        aWinData.sizemode || "", aWinData.sidebar || ""]);
3647    }, 0);
3648  },
3649
3650  /**
3651   * Restore a window's dimensions
3652   * @param aWidth
3653   *        Window width
3654   * @param aHeight
3655   *        Window height
3656   * @param aLeft
3657   *        Window left
3658   * @param aTop
3659   *        Window top
3660   * @param aSizeMode
3661   *        Window size mode (eg: maximized)
3662   * @param aSidebar
3663   *        Sidebar command
3664   */
3665  restoreDimensions: function ssi_restoreDimensions(aWindow, aWidth, aHeight, aLeft, aTop, aSizeMode, aSidebar) {
3666    var win = aWindow;
3667    var _this = this;
3668    function win_(aName) { return _this._getWindowDimension(win, aName); }
3669
3670    // find available space on the screen where this window is being placed
3671    let screen = gScreenManager.screenForRect(aLeft, aTop, aWidth, aHeight);
3672    if (screen) {
3673      let screenLeft = {}, screenTop = {}, screenWidth = {}, screenHeight = {};
3674      screen.GetAvailRectDisplayPix(screenLeft, screenTop, screenWidth, screenHeight);
3675      // screenX/Y are based on the origin of the screen's desktop-pixel coordinate space
3676      let screenLeftCss = screenLeft.value;
3677      let screenTopCss = screenTop.value;
3678      // convert screen's device pixel dimensions to CSS px dimensions
3679      screen.GetAvailRect(screenLeft, screenTop, screenWidth, screenHeight);
3680      let cssToDevScale = screen.defaultCSSScaleFactor;
3681      let screenRightCss = screenLeftCss + screenWidth.value / cssToDevScale;
3682      let screenBottomCss = screenTopCss + screenHeight.value / cssToDevScale;
3683
3684      // Pull the window within the screen's bounds (allowing a little slop
3685      // for windows that may be deliberately placed with their border off-screen
3686      // as when Win10 "snaps" a window to the left/right edge -- bug 1276516).
3687      // First, ensure the left edge is large enough...
3688      if (aLeft < screenLeftCss - SCREEN_EDGE_SLOP) {
3689        aLeft = screenLeftCss;
3690      }
3691      // Then check the resulting right edge, and reduce it if necessary.
3692      let right = aLeft + aWidth;
3693      if (right > screenRightCss + SCREEN_EDGE_SLOP) {
3694        right = screenRightCss;
3695        // See if we can move the left edge leftwards to maintain width.
3696        if (aLeft > screenLeftCss) {
3697          aLeft = Math.max(right - aWidth, screenLeftCss);
3698        }
3699      }
3700      // Finally, update aWidth to account for the adjusted left and right edges.
3701      aWidth = right - aLeft;
3702
3703      // And do the same in the vertical dimension.
3704      if (aTop < screenTopCss - SCREEN_EDGE_SLOP) {
3705        aTop = screenTopCss;
3706      }
3707      let bottom = aTop + aHeight;
3708      if (bottom > screenBottomCss + SCREEN_EDGE_SLOP) {
3709        bottom = screenBottomCss;
3710        if (aTop > screenTopCss) {
3711          aTop = Math.max(bottom - aHeight, screenTopCss);
3712        }
3713      }
3714      aHeight = bottom - aTop;
3715    }
3716
3717    // only modify those aspects which aren't correct yet
3718    if (!isNaN(aLeft) && !isNaN(aTop) && (aLeft != win_("screenX") || aTop != win_("screenY"))) {
3719      aWindow.moveTo(aLeft, aTop);
3720    }
3721    if (aWidth && aHeight && (aWidth != win_("width") || aHeight != win_("height"))) {
3722      // Don't resize the window if it's currently maximized and we would
3723      // maximize it again shortly after.
3724      if (aSizeMode != "maximized" || win_("sizemode") != "maximized") {
3725        aWindow.resizeTo(aWidth, aHeight);
3726      }
3727    }
3728    if (aSizeMode && win_("sizemode") != aSizeMode)
3729    {
3730      switch (aSizeMode)
3731      {
3732      case "maximized":
3733        aWindow.maximize();
3734        break;
3735      case "minimized":
3736        aWindow.minimize();
3737        break;
3738      case "normal":
3739        aWindow.restore();
3740        break;
3741      }
3742    }
3743    var sidebar = aWindow.document.getElementById("sidebar-box");
3744    if (sidebar.getAttribute("sidebarcommand") != aSidebar) {
3745      aWindow.SidebarUI.show(aSidebar);
3746    }
3747    // since resizing/moving a window brings it to the foreground,
3748    // we might want to re-focus the last focused window
3749    if (this.windowToFocus) {
3750      this.windowToFocus.focus();
3751    }
3752  },
3753
3754  /* ........ Disk Access .............. */
3755
3756  /**
3757   * Save the current session state to disk, after a delay.
3758   *
3759   * @param aWindow (optional)
3760   *        Will mark the given window as dirty so that we will recollect its
3761   *        data before we start writing.
3762   */
3763  saveStateDelayed: function (aWindow = null) {
3764    if (aWindow) {
3765      DirtyWindows.add(aWindow);
3766    }
3767
3768    SessionSaver.runDelayed();
3769  },
3770
3771  /* ........ Auxiliary Functions .............. */
3772
3773  /**
3774   * Determines whether or not a tab that is being restored needs
3775   * to have its remoteness flipped first.
3776   *
3777   * @param (object) with the following properties:
3778   *
3779   *        tabbrowser (<xul:tabbrowser>):
3780   *          The tabbrowser that the browser belongs to.
3781   *
3782   *        tab (<xul:tab>):
3783   *          The tab being restored
3784   *
3785   *        willRestoreImmediately (bool):
3786   *          true if the tab is going to have its content
3787   *          restored immediately by the caller.
3788   *
3789   */
3790  _maybeUpdateBrowserRemoteness({ tabbrowser, tab,
3791                                  willRestoreImmediately }) {
3792    // If the browser we're attempting to restore happens to be
3793    // remote, we need to flip it back to non-remote if it's going
3794    // to go into the pending background tab state. This is to make
3795    // sure that a background tab can't crash if it hasn't yet
3796    // been restored.
3797    //
3798    // Normally, when a window is restored, the tabs that SessionStore
3799    // inserts are non-remote - but the initial browser is, by default,
3800    // remote, so this check and flip covers this case. The other case
3801    // is when window state is overwriting the state of an existing
3802    // window with some remote tabs.
3803    let browser = tab.linkedBrowser;
3804
3805    // There are two ways that a tab might start restoring its content
3806    // very soon - either the caller is going to restore the content
3807    // immediately, or the TabRestoreQueue is set up so that the tab
3808    // content is going to be restored in the very near future. In
3809    // either case, we don't want to flip remoteness, since the browser
3810    // will soon be loading content.
3811    let willRestore = willRestoreImmediately ||
3812                      TabRestoreQueue.willRestoreSoon(tab);
3813
3814    if (browser.isRemoteBrowser && !willRestore) {
3815      tabbrowser.updateBrowserRemoteness(browser, false);
3816    }
3817  },
3818
3819  /**
3820   * Update the session start time and send a telemetry measurement
3821   * for the number of days elapsed since the session was started.
3822   *
3823   * @param state
3824   *        The session state.
3825   */
3826  _updateSessionStartTime: function ssi_updateSessionStartTime(state) {
3827    // Attempt to load the session start time from the session state
3828    if (state.session && state.session.startTime) {
3829      this._sessionStartTime = state.session.startTime;
3830    }
3831  },
3832
3833  /**
3834   * call a callback for all currently opened browser windows
3835   * (might miss the most recent one)
3836   * @param aFunc
3837   *        Callback each window is passed to
3838   */
3839  _forEachBrowserWindow: function ssi_forEachBrowserWindow(aFunc) {
3840    var windowsEnum = Services.wm.getEnumerator("navigator:browser");
3841
3842    while (windowsEnum.hasMoreElements()) {
3843      var window = windowsEnum.getNext();
3844      if (window.__SSi && !window.closed) {
3845        aFunc.call(this, window);
3846      }
3847    }
3848  },
3849
3850  /**
3851   * Returns most recent window
3852   * @returns Window reference
3853   */
3854  _getMostRecentBrowserWindow: function ssi_getMostRecentBrowserWindow() {
3855    return RecentWindow.getMostRecentBrowserWindow({ allowPopups: true });
3856  },
3857
3858  /**
3859   * Calls onClose for windows that are determined to be closed but aren't
3860   * destroyed yet, which would otherwise cause getBrowserState and
3861   * setBrowserState to treat them as open windows.
3862   */
3863  _handleClosedWindows: function ssi_handleClosedWindows() {
3864    var windowsEnum = Services.wm.getEnumerator("navigator:browser");
3865
3866    while (windowsEnum.hasMoreElements()) {
3867      var window = windowsEnum.getNext();
3868      if (window.closed) {
3869        this.onClose(window);
3870      }
3871    }
3872  },
3873
3874  /**
3875   * open a new browser window for a given session state
3876   * called when restoring a multi-window session
3877   * @param aState
3878   *        Object containing session data
3879   */
3880  _openWindowWithState: function ssi_openWindowWithState(aState) {
3881    var argString = Cc["@mozilla.org/supports-string;1"].
3882                    createInstance(Ci.nsISupportsString);
3883    argString.data = "";
3884
3885    // Build feature string
3886    let features = "chrome,dialog=no,macsuppressanimation,all";
3887    let winState = aState.windows[0];
3888    WINDOW_ATTRIBUTES.forEach(function(aFeature) {
3889      // Use !isNaN as an easy way to ignore sizemode and check for numbers
3890      if (aFeature in winState && !isNaN(winState[aFeature]))
3891        features += "," + aFeature + "=" + winState[aFeature];
3892    });
3893
3894    if (winState.isPrivate) {
3895      features += ",private";
3896    }
3897
3898    var window =
3899      Services.ww.openWindow(null, this._prefBranch.getCharPref("chromeURL"),
3900                             "_blank", features, argString);
3901
3902    do {
3903      var ID = "window" + Math.random();
3904    } while (ID in this._statesToRestore);
3905    this._statesToRestore[(window.__SS_restoreID = ID)] = aState;
3906
3907    return window;
3908  },
3909
3910  /**
3911   * Whether or not to resume session, if not recovering from a crash.
3912   * @returns bool
3913   */
3914  _doResumeSession: function ssi_doResumeSession() {
3915    return this._prefBranch.getIntPref("startup.page") == 3 ||
3916           this._prefBranch.getBoolPref("sessionstore.resume_session_once");
3917  },
3918
3919  /**
3920   * whether the user wants to load any other page at startup
3921   * (except the homepage) - needed for determining whether to overwrite the current tabs
3922   * C.f.: nsBrowserContentHandler's defaultArgs implementation.
3923   * @returns bool
3924   */
3925  _isCmdLineEmpty: function ssi_isCmdLineEmpty(aWindow, aState) {
3926    var pinnedOnly = aState.windows &&
3927                     aState.windows.every(win =>
3928                       win.tabs.every(tab => tab.pinned));
3929
3930    let hasFirstArgument = aWindow.arguments && aWindow.arguments[0];
3931    if (!pinnedOnly) {
3932      let defaultArgs = Cc["@mozilla.org/browser/clh;1"].
3933                        getService(Ci.nsIBrowserHandler).defaultArgs;
3934      if (aWindow.arguments &&
3935          aWindow.arguments[0] &&
3936          aWindow.arguments[0] == defaultArgs)
3937        hasFirstArgument = false;
3938    }
3939
3940    return !hasFirstArgument;
3941  },
3942
3943  /**
3944   * on popup windows, the XULWindow's attributes seem not to be set correctly
3945   * we use thus JSDOMWindow attributes for sizemode and normal window attributes
3946   * (and hope for reasonable values when maximized/minimized - since then
3947   * outerWidth/outerHeight aren't the dimensions of the restored window)
3948   * @param aWindow
3949   *        Window reference
3950   * @param aAttribute
3951   *        String sizemode | width | height | other window attribute
3952   * @returns string
3953   */
3954  _getWindowDimension: function ssi_getWindowDimension(aWindow, aAttribute) {
3955    if (aAttribute == "sizemode") {
3956      switch (aWindow.windowState) {
3957      case aWindow.STATE_FULLSCREEN:
3958      case aWindow.STATE_MAXIMIZED:
3959        return "maximized";
3960      case aWindow.STATE_MINIMIZED:
3961        return "minimized";
3962      default:
3963        return "normal";
3964      }
3965    }
3966
3967    var dimension;
3968    switch (aAttribute) {
3969    case "width":
3970      dimension = aWindow.outerWidth;
3971      break;
3972    case "height":
3973      dimension = aWindow.outerHeight;
3974      break;
3975    default:
3976      dimension = aAttribute in aWindow ? aWindow[aAttribute] : "";
3977      break;
3978    }
3979
3980    if (aWindow.windowState == aWindow.STATE_NORMAL) {
3981      return dimension;
3982    }
3983    return aWindow.document.documentElement.getAttribute(aAttribute) || dimension;
3984  },
3985
3986  /**
3987   * @param aState is a session state
3988   * @param aRecentCrashes is the number of consecutive crashes
3989   * @returns whether a restore page will be needed for the session state
3990   */
3991  _needsRestorePage: function ssi_needsRestorePage(aState, aRecentCrashes) {
3992    const SIX_HOURS_IN_MS = 6 * 60 * 60 * 1000;
3993
3994    // don't display the page when there's nothing to restore
3995    let winData = aState.windows || null;
3996    if (!winData || winData.length == 0)
3997      return false;
3998
3999    // don't wrap a single about:sessionrestore page
4000    if (this._hasSingleTabWithURL(winData, "about:sessionrestore") ||
4001        this._hasSingleTabWithURL(winData, "about:welcomeback")) {
4002      return false;
4003    }
4004
4005    // don't automatically restore in Safe Mode
4006    if (Services.appinfo.inSafeMode)
4007      return true;
4008
4009    let max_resumed_crashes =
4010      this._prefBranch.getIntPref("sessionstore.max_resumed_crashes");
4011    let sessionAge = aState.session && aState.session.lastUpdate &&
4012                     (Date.now() - aState.session.lastUpdate);
4013
4014    return max_resumed_crashes != -1 &&
4015           (aRecentCrashes > max_resumed_crashes ||
4016            sessionAge && sessionAge >= SIX_HOURS_IN_MS);
4017  },
4018
4019  /**
4020   * @param aWinData is the set of windows in session state
4021   * @param aURL is the single URL we're looking for
4022   * @returns whether the window data contains only the single URL passed
4023   */
4024  _hasSingleTabWithURL: function(aWinData, aURL) {
4025    if (aWinData &&
4026        aWinData.length == 1 &&
4027        aWinData[0].tabs &&
4028        aWinData[0].tabs.length == 1 &&
4029        aWinData[0].tabs[0].entries &&
4030        aWinData[0].tabs[0].entries.length == 1) {
4031      return aURL == aWinData[0].tabs[0].entries[0].url;
4032    }
4033    return false;
4034  },
4035
4036  /**
4037   * Determine if the tab state we're passed is something we should save. This
4038   * is used when closing a tab or closing a window with a single tab
4039   *
4040   * @param aTabState
4041   *        The current tab state
4042   * @returns boolean
4043   */
4044  _shouldSaveTabState: function ssi_shouldSaveTabState(aTabState) {
4045    // If the tab has only a transient about: history entry, no other
4046    // session history, and no userTypedValue, then we don't actually want to
4047    // store this tab's data.
4048    return aTabState.entries.length &&
4049           !(aTabState.entries.length == 1 &&
4050                (aTabState.entries[0].url == "about:blank" ||
4051                 aTabState.entries[0].url == "about:newtab" ||
4052                 aTabState.entries[0].url == "about:privatebrowsing") &&
4053                 !aTabState.userTypedValue);
4054  },
4055
4056  /**
4057   * This is going to take a state as provided at startup (via
4058   * nsISessionStartup.state) and split it into 2 parts. The first part
4059   * (defaultState) will be a state that should still be restored at startup,
4060   * while the second part (state) is a state that should be saved for later.
4061   * defaultState will be comprised of windows with only pinned tabs, extracted
4062   * from state. It will contain the cookies that go along with the history
4063   * entries in those tabs. It will also contain window position information.
4064   *
4065   * defaultState will be restored at startup. state will be passed into
4066   * LastSession and will be kept in case the user explicitly wants
4067   * to restore the previous session (publicly exposed as restoreLastSession).
4068   *
4069   * @param state
4070   *        The state, presumably from nsISessionStartup.state
4071   * @returns [defaultState, state]
4072   */
4073  _prepDataForDeferredRestore: function ssi_prepDataForDeferredRestore(state) {
4074    // Make sure that we don't modify the global state as provided by
4075    // nsSessionStartup.state.
4076    state = Cu.cloneInto(state, {});
4077
4078    let defaultState = { windows: [], selectedWindow: 1 };
4079
4080    state.selectedWindow = state.selectedWindow || 1;
4081
4082    // Look at each window, remove pinned tabs, adjust selectedindex,
4083    // remove window if necessary.
4084    for (let wIndex = 0; wIndex < state.windows.length;) {
4085      let window = state.windows[wIndex];
4086      window.selected = window.selected || 1;
4087      // We're going to put the state of the window into this object
4088      let pinnedWindowState = { tabs: [], cookies: []};
4089      for (let tIndex = 0; tIndex < window.tabs.length;) {
4090        if (window.tabs[tIndex].pinned) {
4091          // Adjust window.selected
4092          if (tIndex + 1 < window.selected)
4093            window.selected -= 1;
4094          else if (tIndex + 1 == window.selected)
4095            pinnedWindowState.selected = pinnedWindowState.tabs.length + 2;
4096            // + 2 because the tab isn't actually in the array yet
4097
4098          // Now add the pinned tab to our window
4099          pinnedWindowState.tabs =
4100            pinnedWindowState.tabs.concat(window.tabs.splice(tIndex, 1));
4101          // We don't want to increment tIndex here.
4102          continue;
4103        }
4104        tIndex++;
4105      }
4106
4107      // At this point the window in the state object has been modified (or not)
4108      // We want to build the rest of this new window object if we have pinnedTabs.
4109      if (pinnedWindowState.tabs.length) {
4110        // First get the other attributes off the window
4111        WINDOW_ATTRIBUTES.forEach(function(attr) {
4112          if (attr in window) {
4113            pinnedWindowState[attr] = window[attr];
4114            delete window[attr];
4115          }
4116        });
4117        // We're just copying position data into the pinned window.
4118        // Not copying over:
4119        // - _closedTabs
4120        // - extData
4121        // - isPopup
4122        // - hidden
4123
4124        // Assign a unique ID to correlate the window to be opened with the
4125        // remaining data
4126        window.__lastSessionWindowID = pinnedWindowState.__lastSessionWindowID
4127                                     = "" + Date.now() + Math.random();
4128
4129        // Extract the cookies that belong with each pinned tab
4130        this._splitCookiesFromWindow(window, pinnedWindowState);
4131
4132        // Actually add this window to our defaultState
4133        defaultState.windows.push(pinnedWindowState);
4134        // Remove the window from the state if it doesn't have any tabs
4135        if (!window.tabs.length) {
4136          if (wIndex + 1 <= state.selectedWindow)
4137            state.selectedWindow -= 1;
4138          else if (wIndex + 1 == state.selectedWindow)
4139            defaultState.selectedIndex = defaultState.windows.length + 1;
4140
4141          state.windows.splice(wIndex, 1);
4142          // We don't want to increment wIndex here.
4143          continue;
4144        }
4145
4146
4147      }
4148      wIndex++;
4149    }
4150
4151    return [defaultState, state];
4152  },
4153
4154  /**
4155   * Splits out the cookies from aWinState into aTargetWinState based on the
4156   * tabs that are in aTargetWinState.
4157   * This alters the state of aWinState and aTargetWinState.
4158   */
4159  _splitCookiesFromWindow:
4160    function ssi_splitCookiesFromWindow(aWinState, aTargetWinState) {
4161    if (!aWinState.cookies || !aWinState.cookies.length)
4162      return;
4163
4164    // Get the hosts for history entries in aTargetWinState
4165    let cookieHosts = SessionCookies.getHostsForWindow(aTargetWinState);
4166
4167    // By creating a regex we reduce overhead and there is only one loop pass
4168    // through either array (cookieHosts and aWinState.cookies).
4169    let hosts = Object.keys(cookieHosts).join("|").replace(/\./g, "\\.");
4170    // If we don't actually have any hosts, then we don't want to do anything.
4171    if (!hosts.length)
4172      return;
4173    let cookieRegex = new RegExp(".*(" + hosts + ")");
4174    for (let cIndex = 0; cIndex < aWinState.cookies.length;) {
4175      if (cookieRegex.test(aWinState.cookies[cIndex].host)) {
4176        aTargetWinState.cookies =
4177          aTargetWinState.cookies.concat(aWinState.cookies.splice(cIndex, 1));
4178        continue;
4179      }
4180      cIndex++;
4181    }
4182  },
4183
4184  _sendRestoreCompletedNotifications: function ssi_sendRestoreCompletedNotifications() {
4185    // not all windows restored, yet
4186    if (this._restoreCount > 1) {
4187      this._restoreCount--;
4188      return;
4189    }
4190
4191    // observers were already notified
4192    if (this._restoreCount == -1)
4193      return;
4194
4195    // This was the last window restored at startup, notify observers.
4196    Services.obs.notifyObservers(null,
4197      this._browserSetState ? NOTIFY_BROWSER_STATE_RESTORED : NOTIFY_WINDOWS_RESTORED,
4198      "");
4199
4200    this._browserSetState = false;
4201    this._restoreCount = -1;
4202  },
4203
4204   /**
4205   * Set the given window's busy state
4206   * @param aWindow the window
4207   * @param aValue the window's busy state
4208   */
4209  _setWindowStateBusyValue:
4210    function ssi_changeWindowStateBusyValue(aWindow, aValue) {
4211
4212    this._windows[aWindow.__SSi].busy = aValue;
4213
4214    // Keep the to-be-restored state in sync because that is returned by
4215    // getWindowState() as long as the window isn't loaded, yet.
4216    if (!this._isWindowLoaded(aWindow)) {
4217      let stateToRestore = this._statesToRestore[aWindow.__SS_restoreID].windows[0];
4218      stateToRestore.busy = aValue;
4219    }
4220  },
4221
4222  /**
4223   * Set the given window's state to 'not busy'.
4224   * @param aWindow the window
4225   */
4226  _setWindowStateReady: function ssi_setWindowStateReady(aWindow) {
4227    let newCount = (this._windowBusyStates.get(aWindow) || 0) - 1;
4228    if (newCount < 0) {
4229      throw new Error("Invalid window busy state (less than zero).");
4230    }
4231    this._windowBusyStates.set(aWindow, newCount);
4232
4233    if (newCount == 0) {
4234      this._setWindowStateBusyValue(aWindow, false);
4235      this._sendWindowStateEvent(aWindow, "Ready");
4236    }
4237  },
4238
4239  /**
4240   * Set the given window's state to 'busy'.
4241   * @param aWindow the window
4242   */
4243  _setWindowStateBusy: function ssi_setWindowStateBusy(aWindow) {
4244    let newCount = (this._windowBusyStates.get(aWindow) || 0) + 1;
4245    this._windowBusyStates.set(aWindow, newCount);
4246
4247    if (newCount == 1) {
4248      this._setWindowStateBusyValue(aWindow, true);
4249      this._sendWindowStateEvent(aWindow, "Busy");
4250    }
4251  },
4252
4253  /**
4254   * Dispatch an SSWindowState_____ event for the given window.
4255   * @param aWindow the window
4256   * @param aType the type of event, SSWindowState will be prepended to this string
4257   */
4258  _sendWindowStateEvent: function ssi_sendWindowStateEvent(aWindow, aType) {
4259    let event = aWindow.document.createEvent("Events");
4260    event.initEvent("SSWindowState" + aType, true, false);
4261    aWindow.dispatchEvent(event);
4262  },
4263
4264  /**
4265   * Dispatch the SSWindowRestored event for the given window.
4266   * @param aWindow
4267   *        The window which has been restored
4268   */
4269  _sendWindowRestoredNotification(aWindow) {
4270    let event = aWindow.document.createEvent("Events");
4271    event.initEvent("SSWindowRestored", true, false);
4272    aWindow.dispatchEvent(event);
4273  },
4274
4275  /**
4276   * Dispatch the SSTabRestored event for the given tab.
4277   * @param aTab
4278   *        The tab which has been restored
4279   * @param aIsRemotenessUpdate
4280   *        True if this tab was restored due to flip from running from
4281   *        out-of-main-process to in-main-process or vice-versa.
4282   */
4283  _sendTabRestoredNotification(aTab, aIsRemotenessUpdate) {
4284    let event = aTab.ownerDocument.createEvent("CustomEvent");
4285    event.initCustomEvent("SSTabRestored", true, false, {
4286      isRemotenessUpdate: aIsRemotenessUpdate,
4287    });
4288    aTab.dispatchEvent(event);
4289  },
4290
4291  /**
4292   * @param aWindow
4293   *        Window reference
4294   * @returns whether this window's data is still cached in _statesToRestore
4295   *          because it's not fully loaded yet
4296   */
4297  _isWindowLoaded: function ssi_isWindowLoaded(aWindow) {
4298    return !aWindow.__SS_restoreID;
4299  },
4300
4301  /**
4302   * Replace "Loading..." with the tab label (with minimal side-effects)
4303   * @param aString is the string the title is stored in
4304   * @param aTabbrowser is a tabbrowser object, containing aTab
4305   * @param aTab is the tab whose title we're updating & using
4306   *
4307   * @returns aString that has been updated with the new title
4308   */
4309  _replaceLoadingTitle : function ssi_replaceLoadingTitle(aString, aTabbrowser, aTab) {
4310    if (aString == aTabbrowser.mStringBundle.getString("tabs.connecting")) {
4311      aTabbrowser.setTabTitle(aTab);
4312      [aString, aTab.label] = [aTab.label, aString];
4313    }
4314    return aString;
4315  },
4316
4317  /**
4318   * Resize this._closedWindows to the value of the pref, except in the case
4319   * where we don't have any non-popup windows on Windows and Linux. Then we must
4320   * resize such that we have at least one non-popup window.
4321   */
4322  _capClosedWindows : function ssi_capClosedWindows() {
4323    if (this._closedWindows.length <= this._max_windows_undo)
4324      return;
4325    let spliceTo = this._max_windows_undo;
4326    if (AppConstants.platform != "macosx") {
4327      let normalWindowIndex = 0;
4328      // try to find a non-popup window in this._closedWindows
4329      while (normalWindowIndex < this._closedWindows.length &&
4330             !!this._closedWindows[normalWindowIndex].isPopup)
4331        normalWindowIndex++;
4332      if (normalWindowIndex >= this._max_windows_undo)
4333        spliceTo = normalWindowIndex + 1;
4334    }
4335    this._closedWindows.splice(spliceTo, this._closedWindows.length);
4336  },
4337
4338  /**
4339   * Clears the set of windows that are "resurrected" before writing to disk to
4340   * make closing windows one after the other until shutdown work as expected.
4341   *
4342   * This function should only be called when we are sure that there has been
4343   * a user action that indicates the browser is actively being used and all
4344   * windows that have been closed before are not part of a series of closing
4345   * windows.
4346   */
4347  _clearRestoringWindows: function ssi_clearRestoringWindows() {
4348    for (let i = 0; i < this._closedWindows.length; i++) {
4349      delete this._closedWindows[i]._shouldRestore;
4350    }
4351  },
4352
4353  /**
4354   * Reset state to prepare for a new session state to be restored.
4355   */
4356  _resetRestoringState: function ssi_initRestoringState() {
4357    TabRestoreQueue.reset();
4358    this._tabsRestoringCount = 0;
4359  },
4360
4361  /**
4362   * Reset the restoring state for a particular tab. This will be called when
4363   * removing a tab or when a tab needs to be reset (it's being overwritten).
4364   *
4365   * @param aTab
4366   *        The tab that will be "reset"
4367   */
4368  _resetLocalTabRestoringState: function (aTab) {
4369    NS_ASSERT(aTab.linkedBrowser.__SS_restoreState,
4370              "given tab is not restoring");
4371
4372    let browser = aTab.linkedBrowser;
4373
4374    // Keep the tab's previous state for later in this method
4375    let previousState = browser.__SS_restoreState;
4376
4377    // The browser is no longer in any sort of restoring state.
4378    delete browser.__SS_restoreState;
4379
4380    aTab.removeAttribute("pending");
4381    browser.removeAttribute("pending");
4382
4383    if (previousState == TAB_STATE_RESTORING) {
4384      if (this._tabsRestoringCount)
4385        this._tabsRestoringCount--;
4386    } else if (previousState == TAB_STATE_NEEDS_RESTORE) {
4387      // Make sure that the tab is removed from the list of tabs to restore.
4388      // Again, this is normally done in restoreTabContent, but that isn't being called
4389      // for this tab.
4390      TabRestoreQueue.remove(aTab);
4391    }
4392  },
4393
4394  _resetTabRestoringState: function (tab) {
4395    NS_ASSERT(tab.linkedBrowser.__SS_restoreState,
4396              "given tab is not restoring");
4397
4398    let browser = tab.linkedBrowser;
4399    browser.messageManager.sendAsyncMessage("SessionStore:resetRestore", {});
4400    this._resetLocalTabRestoringState(tab);
4401  },
4402
4403  /**
4404   * Each fresh tab starts out with epoch=0. This function can be used to
4405   * start a next epoch by incrementing the current value. It will enables us
4406   * to ignore stale messages sent from previous epochs. The function returns
4407   * the new epoch ID for the given |browser|.
4408   */
4409  startNextEpoch(browser) {
4410    let next = this.getCurrentEpoch(browser) + 1;
4411    this._browserEpochs.set(browser.permanentKey, next);
4412    return next;
4413  },
4414
4415  /**
4416   * Returns the current epoch for the given <browser>. If we haven't assigned
4417   * a new epoch this will default to zero for new tabs.
4418   */
4419  getCurrentEpoch(browser) {
4420    return this._browserEpochs.get(browser.permanentKey) || 0;
4421  },
4422
4423  /**
4424   * Each time a <browser> element is restored, we increment its "epoch". To
4425   * check if a message from content-sessionStore.js is out of date, we can
4426   * compare the epoch received with the message to the <browser> element's
4427   * epoch. This function does that, and returns true if |epoch| is up-to-date
4428   * with respect to |browser|.
4429   */
4430  isCurrentEpoch: function (browser, epoch) {
4431    return this.getCurrentEpoch(browser) == epoch;
4432  },
4433
4434  /**
4435   * Resets the epoch for a given <browser>. We need to this every time we
4436   * receive a hint that a new docShell has been loaded into the browser as
4437   * the frame script starts out with epoch=0.
4438   */
4439  resetEpoch(browser) {
4440    this._browserEpochs.delete(browser.permanentKey);
4441  },
4442
4443  /**
4444   * Handle an error report from a content process.
4445   */
4446  reportInternalError(data) {
4447    // For the moment, we only report errors through Telemetry.
4448    if (data.telemetry) {
4449      for (let key of Object.keys(data.telemetry)) {
4450        let histogram = Telemetry.getHistogramById(key);
4451        histogram.add(data.telemetry[key]);
4452      }
4453    }
4454  },
4455
4456  /**
4457   * Countdown for a given duration, skipping beats if the computer is too busy,
4458   * sleeping or otherwise unavailable.
4459   *
4460   * @param {number} delay An approximate delay to wait in milliseconds (rounded
4461   * up to the closest second).
4462   *
4463   * @return Promise
4464   */
4465  looseTimer(delay) {
4466    let DELAY_BEAT = 1000;
4467    let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
4468    let beats = Math.ceil(delay / DELAY_BEAT);
4469    let promise =  new Promise(resolve => {
4470      timer.initWithCallback(function() {
4471        if (beats <= 0) {
4472          resolve();
4473        }
4474        --beats;
4475      }, DELAY_BEAT, Ci.nsITimer.TYPE_REPEATING_PRECISE_CAN_SKIP);
4476    });
4477    // Ensure that the timer is both canceled once we are done with it
4478    // and not garbage-collected until then.
4479    promise.then(() => timer.cancel(), () => timer.cancel());
4480    return promise;
4481  }
4482};
4483
4484/**
4485 * Priority queue that keeps track of a list of tabs to restore and returns
4486 * the tab we should restore next, based on priority rules. We decide between
4487 * pinned, visible and hidden tabs in that and FIFO order. Hidden tabs are only
4488 * restored with restore_hidden_tabs=true.
4489 */
4490var TabRestoreQueue = {
4491  // The separate buckets used to store tabs.
4492  tabs: {priority: [], visible: [], hidden: []},
4493
4494  // Preferences used by the TabRestoreQueue to determine which tabs
4495  // are restored automatically and which tabs will be on-demand.
4496  prefs: {
4497    // Lazy getter that returns whether tabs are restored on demand.
4498    get restoreOnDemand() {
4499      let updateValue = () => {
4500        let value = Services.prefs.getBoolPref(PREF);
4501        let definition = {value: value, configurable: true};
4502        Object.defineProperty(this, "restoreOnDemand", definition);
4503        return value;
4504      }
4505
4506      const PREF = "browser.sessionstore.restore_on_demand";
4507      Services.prefs.addObserver(PREF, updateValue, false);
4508      return updateValue();
4509    },
4510
4511    // Lazy getter that returns whether pinned tabs are restored on demand.
4512    get restorePinnedTabsOnDemand() {
4513      let updateValue = () => {
4514        let value = Services.prefs.getBoolPref(PREF);
4515        let definition = {value: value, configurable: true};
4516        Object.defineProperty(this, "restorePinnedTabsOnDemand", definition);
4517        return value;
4518      }
4519
4520      const PREF = "browser.sessionstore.restore_pinned_tabs_on_demand";
4521      Services.prefs.addObserver(PREF, updateValue, false);
4522      return updateValue();
4523    },
4524
4525    // Lazy getter that returns whether we should restore hidden tabs.
4526    get restoreHiddenTabs() {
4527      let updateValue = () => {
4528        let value = Services.prefs.getBoolPref(PREF);
4529        let definition = {value: value, configurable: true};
4530        Object.defineProperty(this, "restoreHiddenTabs", definition);
4531        return value;
4532      }
4533
4534      const PREF = "browser.sessionstore.restore_hidden_tabs";
4535      Services.prefs.addObserver(PREF, updateValue, false);
4536      return updateValue();
4537    }
4538  },
4539
4540  // Resets the queue and removes all tabs.
4541  reset: function () {
4542    this.tabs = {priority: [], visible: [], hidden: []};
4543  },
4544
4545  // Adds a tab to the queue and determines its priority bucket.
4546  add: function (tab) {
4547    let {priority, hidden, visible} = this.tabs;
4548
4549    if (tab.pinned) {
4550      priority.push(tab);
4551    } else if (tab.hidden) {
4552      hidden.push(tab);
4553    } else {
4554      visible.push(tab);
4555    }
4556  },
4557
4558  // Removes a given tab from the queue, if it's in there.
4559  remove: function (tab) {
4560    let {priority, hidden, visible} = this.tabs;
4561
4562    // We'll always check priority first since we don't
4563    // have an indicator if a tab will be there or not.
4564    let set = priority;
4565    let index = set.indexOf(tab);
4566
4567    if (index == -1) {
4568      set = tab.hidden ? hidden : visible;
4569      index = set.indexOf(tab);
4570    }
4571
4572    if (index > -1) {
4573      set.splice(index, 1);
4574    }
4575  },
4576
4577  // Returns and removes the tab with the highest priority.
4578  shift: function () {
4579    let set;
4580    let {priority, hidden, visible} = this.tabs;
4581
4582    let {restoreOnDemand, restorePinnedTabsOnDemand} = this.prefs;
4583    let restorePinned = !(restoreOnDemand && restorePinnedTabsOnDemand);
4584    if (restorePinned && priority.length) {
4585      set = priority;
4586    } else if (!restoreOnDemand) {
4587      if (visible.length) {
4588        set = visible;
4589      } else if (this.prefs.restoreHiddenTabs && hidden.length) {
4590        set = hidden;
4591      }
4592    }
4593
4594    return set && set.shift();
4595  },
4596
4597  // Moves a given tab from the 'hidden' to the 'visible' bucket.
4598  hiddenToVisible: function (tab) {
4599    let {hidden, visible} = this.tabs;
4600    let index = hidden.indexOf(tab);
4601
4602    if (index > -1) {
4603      hidden.splice(index, 1);
4604      visible.push(tab);
4605    }
4606  },
4607
4608  // Moves a given tab from the 'visible' to the 'hidden' bucket.
4609  visibleToHidden: function (tab) {
4610    let {visible, hidden} = this.tabs;
4611    let index = visible.indexOf(tab);
4612
4613    if (index > -1) {
4614      visible.splice(index, 1);
4615      hidden.push(tab);
4616    }
4617  },
4618
4619  /**
4620   * Returns true if the passed tab is in one of the sets that we're
4621   * restoring content in automatically.
4622   *
4623   * @param tab (<xul:tab>)
4624   *        The tab to check
4625   * @returns bool
4626   */
4627  willRestoreSoon: function (tab) {
4628    let { priority, hidden, visible } = this.tabs;
4629    let { restoreOnDemand, restorePinnedTabsOnDemand,
4630          restoreHiddenTabs } = this.prefs;
4631    let restorePinned = !(restoreOnDemand && restorePinnedTabsOnDemand);
4632    let candidateSet = [];
4633
4634    if (restorePinned && priority.length)
4635      candidateSet.push(...priority);
4636
4637    if (!restoreOnDemand) {
4638      if (visible.length)
4639        candidateSet.push(...visible);
4640
4641      if (restoreHiddenTabs && hidden.length)
4642        candidateSet.push(...hidden);
4643    }
4644
4645    return candidateSet.indexOf(tab) > -1;
4646  },
4647};
4648
4649// A map storing a closed window's state data until it goes aways (is GC'ed).
4650// This ensures that API clients can still read (but not write) states of
4651// windows they still hold a reference to but we don't.
4652var DyingWindowCache = {
4653  _data: new WeakMap(),
4654
4655  has: function (window) {
4656    return this._data.has(window);
4657  },
4658
4659  get: function (window) {
4660    return this._data.get(window);
4661  },
4662
4663  set: function (window, data) {
4664    this._data.set(window, data);
4665  },
4666
4667  remove: function (window) {
4668    this._data.delete(window);
4669  }
4670};
4671
4672// A weak set of dirty windows. We use it to determine which windows we need to
4673// recollect data for when getCurrentState() is called.
4674var DirtyWindows = {
4675  _data: new WeakMap(),
4676
4677  has: function (window) {
4678    return this._data.has(window);
4679  },
4680
4681  add: function (window) {
4682    return this._data.set(window, true);
4683  },
4684
4685  remove: function (window) {
4686    this._data.delete(window);
4687  },
4688
4689  clear: function (window) {
4690    this._data = new WeakMap();
4691  }
4692};
4693
4694// The state from the previous session (after restoring pinned tabs). This
4695// state is persisted and passed through to the next session during an app
4696// restart to make the third party add-on warning not trash the deferred
4697// session
4698var LastSession = {
4699  _state: null,
4700
4701  get canRestore() {
4702    return !!this._state;
4703  },
4704
4705  getState: function () {
4706    return this._state;
4707  },
4708
4709  setState: function (state) {
4710    this._state = state;
4711  },
4712
4713  clear: function () {
4714    if (this._state) {
4715      this._state = null;
4716      Services.obs.notifyObservers(null, NOTIFY_LAST_SESSION_CLEARED, null);
4717    }
4718  }
4719};
4720