1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 // Current version of the format used by Session Restore.
6 const FORMAT_VERSION = 1;
8 const PERSIST_SESSIONS = Services.prefs.getBoolPref(
9 "browser.sessionstore.persist_closed_tabs_between_sessions"
11 const TAB_CUSTOM_VALUES = new WeakMap();
12 const TAB_LAZY_STATES = new WeakMap();
13 const TAB_STATE_NEEDS_RESTORE = 1;
14 const TAB_STATE_RESTORING = 2;
15 const TAB_STATE_FOR_BROWSER = new WeakMap();
16 const WINDOW_RESTORE_IDS = new WeakMap();
17 const WINDOW_RESTORE_ZINDICES = new WeakMap();
18 const WINDOW_SHOWING_PROMISES = new Map();
19 const WINDOW_FLUSHING_PROMISES = new Map();
21 // A new window has just been restored. At this stage, tabs are generally
23 const NOTIFY_SINGLE_WINDOW_RESTORED = "sessionstore-single-window-restored";
24 const NOTIFY_WINDOWS_RESTORED = "sessionstore-windows-restored";
25 const NOTIFY_BROWSER_STATE_RESTORED = "sessionstore-browser-state-restored";
26 const NOTIFY_LAST_SESSION_CLEARED = "sessionstore-last-session-cleared";
27 const NOTIFY_RESTORING_ON_STARTUP = "sessionstore-restoring-on-startup";
28 const NOTIFY_INITIATING_MANUAL_RESTORE =
29 "sessionstore-initiating-manual-restore";
30 const NOTIFY_CLOSED_OBJECTS_CHANGED = "sessionstore-closed-objects-changed";
32 const NOTIFY_TAB_RESTORED = "sessionstore-debug-tab-restored"; // WARNING: debug-only
33 const NOTIFY_DOMWINDOWCLOSED_HANDLED =
34 "sessionstore-debug-domwindowclosed-handled"; // WARNING: debug-only
36 const NOTIFY_BROWSER_SHUTDOWN_FLUSH = "sessionstore-browser-shutdown-flush";
38 // Maximum number of tabs to restore simultaneously. Previously controlled by
39 // the browser.sessionstore.max_concurrent_tabs pref.
40 const MAX_CONCURRENT_TAB_RESTORES = 3;
42 // Minimum amount (in CSS px) by which we allow window edges to be off-screen
43 // when restoring a window, before we override the saved position to pull the
44 // window back within the available screen area.
45 const MIN_SCREEN_EDGE_SLOP = 8;
47 // global notifications observed
49 "browser-window-before-show",
51 "quit-application-granted",
52 "browser-lastwindow-close-granted",
54 "browser:purge-session-history",
55 "browser:purge-session-history-for-domain",
57 "clear-origin-attributes-data",
58 "browsing-context-did-set-embedder",
59 "browsing-context-discarded",
60 "browser-shutdown-tabstate-updated",
63 // XUL Window properties to (re)store
64 // Restored in restoreDimensions()
65 const WINDOW_ATTRIBUTES = ["width", "height", "screenX", "screenY", "sizemode"];
67 const CHROME_FLAGS_MAP = [
68 [Ci.nsIWebBrowserChrome.CHROME_TITLEBAR, "titlebar"],
69 [Ci.nsIWebBrowserChrome.CHROME_WINDOW_CLOSE, "close"],
70 [Ci.nsIWebBrowserChrome.CHROME_TOOLBAR, "toolbar"],
71 [Ci.nsIWebBrowserChrome.CHROME_LOCATIONBAR, "location"],
72 [Ci.nsIWebBrowserChrome.CHROME_PERSONAL_TOOLBAR, "personalbar"],
73 [Ci.nsIWebBrowserChrome.CHROME_STATUSBAR, "status"],
74 [Ci.nsIWebBrowserChrome.CHROME_MENUBAR, "menubar"],
75 [Ci.nsIWebBrowserChrome.CHROME_WINDOW_RESIZE, "resizable"],
76 [Ci.nsIWebBrowserChrome.CHROME_WINDOW_MINIMIZE, "minimizable"],
77 [Ci.nsIWebBrowserChrome.CHROME_SCROLLBARS, "", "scrollbars=0"],
78 [Ci.nsIWebBrowserChrome.CHROME_PRIVATE_WINDOW, "private"],
79 [Ci.nsIWebBrowserChrome.CHROME_NON_PRIVATE_WINDOW, "non-private"],
80 // Do not inherit remoteness and fissionness from the previous session.
81 //[Ci.nsIWebBrowserChrome.CHROME_REMOTE_WINDOW, "remote", "non-remote"],
82 //[Ci.nsIWebBrowserChrome.CHROME_FISSION_WINDOW, "fission", "non-fission"],
83 // "chrome" and "suppressanimation" are always set.
84 //[Ci.nsIWebBrowserChrome.CHROME_SUPPRESS_ANIMATION, "suppressanimation"],
85 [Ci.nsIWebBrowserChrome.CHROME_ALWAYS_ON_TOP, "alwaysontop"],
86 //[Ci.nsIWebBrowserChrome.CHROME_OPENAS_CHROME, "chrome", "chrome=0"],
87 [Ci.nsIWebBrowserChrome.CHROME_EXTRA, "extrachrome"],
88 [Ci.nsIWebBrowserChrome.CHROME_CENTER_SCREEN, "centerscreen"],
89 [Ci.nsIWebBrowserChrome.CHROME_DEPENDENT, "dependent"],
90 [Ci.nsIWebBrowserChrome.CHROME_MODAL, "modal"],
91 [Ci.nsIWebBrowserChrome.CHROME_OPENAS_DIALOG, "dialog", "dialog=0"],
94 // Hideable window features to (re)store
95 // Restored in restoreWindowFeatures()
96 const WINDOW_HIDEABLE_FEATURES = [
105 const WINDOW_OPEN_FEATURES_MAP = {
106 locationbar: "location",
110 // These are tab events that we listen to.
113 "TabBrowserInserted",
121 "TabGroupRemoveRequested",
129 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
132 * When calling restoreTabContent, we can supply a reason why
133 * the content is being restored. These are those reasons.
135 const RESTORE_TAB_CONTENT_REASON = {
138 * We're restoring this tab's content because we're setting
139 * state inside this browser tab, probably because the user
140 * has asked us to restore a tab (or window, or entire session).
144 * NAVIGATE_AND_RESTORE:
145 * We're restoring this tab's content because a navigation caused
146 * us to do a remoteness-flip.
148 NAVIGATE_AND_RESTORE: 1,
151 // 'browser.startup.page' preference value to resume the previous session.
152 const BROWSER_STARTUP_RESUME_SESSION = 3;
154 // Used by SessionHistoryListener.
155 const kNoIndex = Number.MAX_SAFE_INTEGER;
156 const kLastIndex = Number.MAX_SAFE_INTEGER - 1;
158 import { PrivateBrowsingUtils } from "resource://gre/modules/PrivateBrowsingUtils.sys.mjs";
160 import { TelemetryTimestamps } from "resource://gre/modules/TelemetryTimestamps.sys.mjs";
161 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
162 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
163 import { GlobalState } from "resource:///modules/sessionstore/GlobalState.sys.mjs";
167 XPCOMUtils.defineLazyServiceGetters(lazy, {
168 gScreenManager: ["@mozilla.org/gfx/screenmanager;1", "nsIScreenManager"],
171 ChromeUtils.defineESModuleGetters(lazy, {
172 AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
173 BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
174 DevToolsShim: "chrome://devtools-startup/content/DevToolsShim.sys.mjs",
175 E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
176 HomePage: "resource:///modules/HomePage.sys.mjs",
177 sessionStoreLogger: "resource:///modules/sessionstore/SessionLogger.sys.mjs",
178 RunState: "resource:///modules/sessionstore/RunState.sys.mjs",
179 SessionCookies: "resource:///modules/sessionstore/SessionCookies.sys.mjs",
180 SessionFile: "resource:///modules/sessionstore/SessionFile.sys.mjs",
181 SessionHistory: "resource://gre/modules/sessionstore/SessionHistory.sys.mjs",
182 SessionSaver: "resource:///modules/sessionstore/SessionSaver.sys.mjs",
183 SessionStartup: "resource:///modules/sessionstore/SessionStartup.sys.mjs",
185 "resource://gre/modules/sessionstore/SessionStoreHelper.sys.mjs",
186 TabAttributes: "resource:///modules/sessionstore/TabAttributes.sys.mjs",
187 TabCrashHandler: "resource:///modules/ContentCrashHandlers.sys.mjs",
188 TabGroupState: "resource:///modules/sessionstore/TabGroupState.sys.mjs",
189 TabState: "resource:///modules/sessionstore/TabState.sys.mjs",
190 TabStateCache: "resource:///modules/sessionstore/TabStateCache.sys.mjs",
191 TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs",
192 setTimeout: "resource://gre/modules/Timer.sys.mjs",
195 ChromeUtils.defineLazyGetter(lazy, "blankURI", () => {
196 return Services.io.newURI("about:blank");
200 * |true| if we are in debug mode, |false| otherwise.
201 * Debug mode is controlled by preference browser.sessionstore.debug
203 var gDebuggingEnabled = false;
206 * @namespace SessionStore
208 export var SessionStore = {
210 return SessionStoreInternal._log;
212 get promiseInitialized() {
213 return SessionStoreInternal.promiseInitialized;
216 get promiseAllWindowsRestored() {
217 return SessionStoreInternal.promiseAllWindowsRestored;
220 get canRestoreLastSession() {
221 return SessionStoreInternal.canRestoreLastSession;
224 set canRestoreLastSession(val) {
225 SessionStoreInternal.canRestoreLastSession = val;
228 get lastClosedObjectType() {
229 return SessionStoreInternal.lastClosedObjectType;
232 get lastClosedActions() {
233 return [...SessionStoreInternal._lastClosedActions];
236 get LAST_ACTION_CLOSED_TAB() {
237 return SessionStoreInternal._LAST_ACTION_CLOSED_TAB;
240 get LAST_ACTION_CLOSED_WINDOW() {
241 return SessionStoreInternal._LAST_ACTION_CLOSED_WINDOW;
245 return SessionStoreInternal._savedGroups;
248 get willAutoRestore() {
249 return SessionStoreInternal.willAutoRestore;
252 init: function ss_init() {
253 SessionStoreInternal.init();
257 * Get the collection of all matching windows tracked by SessionStore
258 * @param {Window|Object} [aWindowOrOptions] Optionally an options object or a window to used to determine if we're filtering for private or non-private windows
259 * @param {boolean} [aWindowOrOptions.private] Determine if we should filter for private or non-private windows
261 getWindows(aWindowOrOptions) {
262 return SessionStoreInternal.getWindows(aWindowOrOptions);
266 * Get window a given closed tab belongs to
267 * @param {integer} aClosedId The closedId of the tab whose window we want to find
268 * @param {boolean} [aIncludePrivate] Optionally include private windows when searching for the closed tab
270 getWindowForTabClosedId(aClosedId, aIncludePrivate) {
271 return SessionStoreInternal.getWindowForTabClosedId(
277 getBrowserState: function ss_getBrowserState() {
278 return SessionStoreInternal.getBrowserState();
281 setBrowserState: function ss_setBrowserState(aState) {
282 SessionStoreInternal.setBrowserState(aState);
285 getWindowState: function ss_getWindowState(aWindow) {
286 return SessionStoreInternal.getWindowState(aWindow);
289 setWindowState: function ss_setWindowState(aWindow, aState, aOverwrite) {
290 SessionStoreInternal.setWindowState(aWindow, aState, aOverwrite);
293 getTabState: function ss_getTabState(aTab) {
294 return SessionStoreInternal.getTabState(aTab);
297 setTabState: function ss_setTabState(aTab, aState) {
298 SessionStoreInternal.setTabState(aTab, aState);
301 // Return whether a tab is restoring.
302 isTabRestoring(aTab) {
303 return TAB_STATE_FOR_BROWSER.has(aTab.linkedBrowser);
306 getInternalObjectState(obj) {
307 return SessionStoreInternal.getInternalObjectState(obj);
310 duplicateTab: function ss_duplicateTab(
314 aRestoreImmediately = true,
317 return SessionStoreInternal.duplicateTab(
327 * How many tabs were last closed. If multiple tabs were selected and closed together,
328 * we'll return that number. Normally the count is 1, or 0 if no tabs have been
329 * recently closed in this window.
330 * @returns the number of tabs that were last closed.
332 getLastClosedTabCount(aWindow) {
333 return SessionStoreInternal.getLastClosedTabCount(aWindow);
336 resetLastClosedTabCount(aWindow) {
337 SessionStoreInternal.resetLastClosedTabCount(aWindow);
341 * Get the number of closed tabs associated with a specific window
342 * @param {Window} aWindow
344 getClosedTabCountForWindow: function ss_getClosedTabCountForWindow(aWindow) {
345 return SessionStoreInternal.getClosedTabCountForWindow(aWindow);
349 * Get the number of closed tabs associated with all matching windows
350 * @param {Window|Object} [aOptions]
351 * Either a DOMWindow (see aOptions.sourceWindow) or an object with properties
352 to identify which closed tabs to include in the count.
353 * @param {Window} aOptions.sourceWindow
354 A browser window used to identity privateness.
355 When closedTabsFromAllWindows is false, we only count closed tabs assocated with this window.
356 * @param {boolean} [aOptions.private = false]
357 Explicit indicator to constrain tab count to only private or non-private windows,
358 * @param {boolean} [aOptions.closedTabsFromAllWindows]
359 Override the value of the closedTabsFromAllWindows preference.
360 * @param {boolean} [aOptions.closedTabsFromClosedWindows]
361 Override the value of the closedTabsFromClosedWindows preference.
363 getClosedTabCount: function ss_getClosedTabCount(aOptions) {
364 return SessionStoreInternal.getClosedTabCount(aOptions);
368 * Get the number of closed tabs from recently closed window
370 * This is normally only relevant in a non-private window context, as we don't
371 * keep data from closed private windows.
373 getClosedTabCountFromClosedWindows:
374 function ss_getClosedTabCountFromClosedWindows() {
375 return SessionStoreInternal.getClosedTabCountFromClosedWindows();
379 * Get the closed tab data associated with this window
380 * @param {Window} aWindow
382 getClosedTabDataForWindow: function ss_getClosedTabDataForWindow(aWindow) {
383 return SessionStoreInternal.getClosedTabDataForWindow(aWindow);
387 * Get the closed tab data associated with all matching windows
388 * @param {Window|Object} [aOptions]
389 * Either a DOMWindow (see aOptions.sourceWindow) or an object with properties
390 to identify which closed tabs to get data from
391 * @param {Window} aOptions.sourceWindow
392 A browser window used to identity privateness.
393 When closedTabsFromAllWindows is false, we only include closed tabs assocated with this window.
394 * @param {boolean} [aOptions.private = false]
395 Explicit indicator to constrain tab data to only private or non-private windows,
396 * @param {boolean} [aOptions.closedTabsFromAllWindows]
397 Override the value of the closedTabsFromAllWindows preference.
398 * @param {boolean} [aOptions.closedTabsFromClosedWindows]
399 Override the value of the closedTabsFromClosedWindows preference.
401 getClosedTabData: function ss_getClosedTabData(aOptions) {
402 return SessionStoreInternal.getClosedTabData(aOptions);
406 * Get the closed tab data associated with all closed windows
407 * @returns an un-sorted array of tabData for closed tabs from closed windows
409 getClosedTabDataFromClosedWindows:
410 function ss_getClosedTabDataFromClosedWindows() {
411 return SessionStoreInternal.getClosedTabDataFromClosedWindows();
415 * Get the closed tab group data associated with all matching windows
416 * @param {Window|object} aOptions
417 * Either a DOMWindow (see aOptions.sourceWindow) or an object with properties
418 to identify the window source of the closed tab groups
419 * @param {Window} [aOptions.sourceWindow]
420 A browser window used to identity privateness.
421 When closedTabsFromAllWindows is false, we only include closed tab groups assocated with this window.
422 * @param {boolean} [aOptions.private = false]
423 Explicit indicator to constrain tab group data to only private or non-private windows,
424 * @param {boolean} [aOptions.closedTabsFromAllWindows]
425 Override the value of the closedTabsFromAllWindows preference.
426 * @param {boolean} [aOptions.closedTabsFromClosedWindows]
427 Override the value of the closedTabsFromClosedWindows preference.
428 * @returns {ClosedTabGroupStateData[]}
430 getClosedTabGroups: function ss_getClosedTabGroups(aOptions) {
431 return SessionStoreInternal.getClosedTabGroups(aOptions);
435 * Get the last closed tab ID associated with a specific window
436 * @param {Window} aWindow
438 getLastClosedTabGroupId(window) {
439 return SessionStoreInternal.getLastClosedTabGroupId(window);
443 * Re-open a closed tab
444 * @param {Window|Object} aSource
445 * Either a DOMWindow or an object with properties to resolve to the window
446 * the tab was previously open in.
447 * @param {String} aSource.sourceWindowId
448 A SessionStore window id used to look up the window where the tab was closed
449 * @param {number} aSource.sourceClosedId
450 The closedId used to look up the closed window where the tab was closed
451 * @param {Integer} [aIndex = 0]
452 * The index of the tab in the closedTabs array (via SessionStore.getClosedTabData), where 0 is most recent.
453 * @param {Window} [aTargetWindow = aWindow] Optional window to open the tab into, defaults to current (topWindow).
454 * @returns a reference to the reopened tab.
456 undoCloseTab: function ss_undoCloseTab(aSource, aIndex, aTargetWindow) {
457 return SessionStoreInternal.undoCloseTab(aSource, aIndex, aTargetWindow);
461 * Re-open a tab from a closed window, which corresponds to the closedId
462 * @param {Window|Object} aSource
463 * Either a DOMWindow or an object with properties to resolve to the window
464 * the tab was previously open in.
465 * @param {String} aSource.sourceWindowId
466 A SessionStore window id used to look up the window where the tab was closed
467 * @param {number} aSource.sourceClosedId
468 The closedId used to look up the closed window where the tab was closed
469 * @param {integer} aClosedId
470 * The closedId of the tab or window
471 * @param {Window} [aTargetWindow = aWindow] Optional window to open the tab into, defaults to current (topWindow).
472 * @returns a reference to the reopened tab.
474 undoClosedTabFromClosedWindow: function ss_undoClosedTabFromClosedWindow(
479 return SessionStoreInternal.undoClosedTabFromClosedWindow(
487 * Forget a closed tab associated with a given window
488 * Removes the record at the given index so it cannot be un-closed or appear
489 * in a list of recently-closed tabs
491 * @param {Window|Object} aSource
492 * Either a DOMWindow or an object with properties to resolve to the window
493 * the tab was previously open in.
494 * @param {String} aSource.sourceWindowId
495 A SessionStore window id used to look up the window where the tab was closed
496 * @param {number} aSource.sourceClosedId
497 The closedId used to look up the closed window where the tab was closed
498 * @param {Integer} [aIndex = 0]
499 * The index into the window's list of closed tabs
500 * @throws {InvalidArgumentError} if the window is not tracked by SessionStore, or index is out of bounds
502 forgetClosedTab: function ss_forgetClosedTab(aSource, aIndex) {
503 return SessionStoreInternal.forgetClosedTab(aSource, aIndex);
507 * Forget a closed tab group associated with a given window
508 * Removes the record at the given index so it cannot be un-closed or appear
509 * in a list of recently-closed tabs
511 * @param {Window|Object} aSource
512 * Either a DOMWindow or an object with properties to resolve to the window
513 * the tab was previously open in.
514 * @param {String} aSource.sourceWindowId
515 A SessionStore window id used to look up the window where the tab group was closed
516 * @param {number} aSource.sourceClosedId
517 The closedId used to look up the closed window where the tab group was closed
518 * @param {string} tabGroupId
519 * The tab group ID of the closed tab group
520 * @throws {InvalidArgumentError}
521 * if the window or tab group is not tracked by SessionStore
523 forgetClosedTabGroup: function ss_forgetClosedTabGroup(aSource, tabGroupId) {
524 return SessionStoreInternal.forgetClosedTabGroup(aSource, tabGroupId);
528 * Forget a closed tab that corresponds to the closedId
529 * Removes the record with this closedId so it cannot be un-closed or appear
530 * in a list of recently-closed tabs
532 * @param {integer} aClosedId
533 * The closedId of the tab
534 * @param {Window|Object} aSourceOptions
535 * Either a DOMWindow or an object with properties to resolve to the window
536 * the tab was previously open in.
537 * @param {boolean} [aSourceOptions.includePrivate = true]
538 If no other means of resolving a source window is given, this flag is used to
539 constrain a search across all open window's closed tabs.
540 * @param {String} aSourceOptions.sourceWindowId
541 A SessionStore window id used to look up the window where the tab was closed
542 * @param {number} aSourceOptions.sourceClosedId
543 The closedId used to look up the closed window where the tab was closed
544 * @throws {InvalidArgumentError} if the closedId doesnt match a closed tab in any window
546 forgetClosedTabById: function ss_forgetClosedTabById(
550 SessionStoreInternal.forgetClosedTabById(aClosedId, aSourceOptions);
554 * Forget a closed window.
555 * Removes the record with this closedId so it cannot be un-closed or appear
556 * in a list of recently-closed windows
558 * @param {integer} aClosedId
559 * The closedId of the window
560 * @throws {InvalidArgumentError} if the closedId doesnt match a closed window
562 forgetClosedWindowById: function ss_forgetClosedWindowById(aClosedId) {
563 SessionStoreInternal.forgetClosedWindowById(aClosedId);
567 * Look up the object type ("tab" or "window") for a given closedId
568 * @param {integer} aClosedId
570 getObjectTypeForClosedId(aClosedId) {
571 return SessionStoreInternal.getObjectTypeForClosedId(aClosedId);
575 * Look up a window tracked by SessionStore by its id
576 * @param {String} aSessionStoreId
578 getWindowById: function ss_getWindowById(aSessionStoreId) {
579 return SessionStoreInternal.getWindowById(aSessionStoreId);
582 getClosedWindowCount: function ss_getClosedWindowCount() {
583 return SessionStoreInternal.getClosedWindowCount();
586 // this should only be used by one caller (currently restoreLastClosedTabOrWindowOrSession in browser.js)
587 popLastClosedAction: function ss_popLastClosedAction() {
588 return SessionStoreInternal._lastClosedActions.pop();
591 // for testing purposes
592 resetLastClosedActions: function ss_resetLastClosedActions() {
593 SessionStoreInternal._lastClosedActions = [];
596 getClosedWindowData: function ss_getClosedWindowData() {
597 return SessionStoreInternal.getClosedWindowData();
600 maybeDontRestoreTabs(aWindow) {
601 SessionStoreInternal.maybeDontRestoreTabs(aWindow);
604 undoCloseWindow: function ss_undoCloseWindow(aIndex) {
605 return SessionStoreInternal.undoCloseWindow(aIndex);
608 forgetClosedWindow: function ss_forgetClosedWindow(aIndex) {
609 return SessionStoreInternal.forgetClosedWindow(aIndex);
612 getCustomWindowValue(aWindow, aKey) {
613 return SessionStoreInternal.getCustomWindowValue(aWindow, aKey);
616 setCustomWindowValue(aWindow, aKey, aStringValue) {
617 SessionStoreInternal.setCustomWindowValue(aWindow, aKey, aStringValue);
620 deleteCustomWindowValue(aWindow, aKey) {
621 SessionStoreInternal.deleteCustomWindowValue(aWindow, aKey);
624 getCustomTabValue(aTab, aKey) {
625 return SessionStoreInternal.getCustomTabValue(aTab, aKey);
628 setCustomTabValue(aTab, aKey, aStringValue) {
629 SessionStoreInternal.setCustomTabValue(aTab, aKey, aStringValue);
632 deleteCustomTabValue(aTab, aKey) {
633 SessionStoreInternal.deleteCustomTabValue(aTab, aKey);
636 getLazyTabValue(aTab, aKey) {
637 return SessionStoreInternal.getLazyTabValue(aTab, aKey);
640 getCustomGlobalValue(aKey) {
641 return SessionStoreInternal.getCustomGlobalValue(aKey);
644 setCustomGlobalValue(aKey, aStringValue) {
645 SessionStoreInternal.setCustomGlobalValue(aKey, aStringValue);
648 deleteCustomGlobalValue(aKey) {
649 SessionStoreInternal.deleteCustomGlobalValue(aKey);
652 restoreLastSession: function ss_restoreLastSession() {
653 SessionStoreInternal.restoreLastSession();
656 speculativeConnectOnTabHover(tab) {
657 SessionStoreInternal.speculativeConnectOnTabHover(tab);
660 getCurrentState(aUpdateAll) {
661 return SessionStoreInternal.getCurrentState(aUpdateAll);
664 reviveCrashedTab(aTab) {
665 return SessionStoreInternal.reviveCrashedTab(aTab);
668 reviveAllCrashedTabs() {
669 return SessionStoreInternal.reviveAllCrashedTabs();
672 updateSessionStoreFromTablistener(
679 return SessionStoreInternal.updateSessionStoreFromTablistener(
688 getSessionHistory(tab, updatedCallback) {
689 return SessionStoreInternal.getSessionHistory(tab, updatedCallback);
693 * Re-open a tab or window which corresponds to the closedId
695 * @param {integer} aClosedId
696 * The closedId of the tab or window
697 * @param {boolean} [aIncludePrivate = true]
698 * Whether to match the aClosedId to only closed private tabs/windows or non-private
699 * @param {Window} [aTargetWindow]
700 * When aClosedId is for a closed tab, which window to re-open the tab into.
701 * Defaults to current (topWindow).
703 * @returns a tab or window object
705 undoCloseById(aClosedId, aIncludePrivate, aTargetWindow) {
706 return SessionStoreInternal.undoCloseById(
713 resetBrowserToLazyState(tab) {
714 return SessionStoreInternal.resetBrowserToLazyState(tab);
717 maybeExitCrashedState(browser) {
718 SessionStoreInternal.maybeExitCrashedState(browser);
721 isBrowserInCrashedSet(browser) {
722 return SessionStoreInternal.isBrowserInCrashedSet(browser);
725 // this is used for testing purposes
726 resetNextClosedId() {
727 SessionStoreInternal._nextClosedId = 0;
731 * Ensures that session store has registered and started tracking a given window.
735 ensureInitialized(window) {
736 if (SessionStoreInternal._sessionInitialized && !window.__SSi) {
738 We need to check that __SSi is not defined on the window so that if
739 onLoad function is in the middle of executing we don't enter the function
740 again and try to redeclare the ContentSessionStore script.
742 SessionStoreInternal.onLoad(window);
746 getCurrentEpoch(browser) {
747 return SessionStoreInternal.getCurrentEpoch(browser.permanentKey);
751 * Determines whether the passed version number is compatible with
752 * the current version number of the SessionStore.
754 * @param version The format and version of the file, as an array, e.g.
755 * ["sessionrestore", 1]
757 isFormatVersionCompatible(version) {
761 if (!Array.isArray(version)) {
765 if (version[0] != "sessionrestore") {
766 // Not a Session Restore file.
769 let number = Number.parseFloat(version[1]);
770 if (Number.isNaN(number)) {
773 return number <= FORMAT_VERSION;
777 * Filters out not worth-saving tabs from a given browser state object.
779 * @param aState (object)
780 * The browser state for which we remove worth-saving tabs.
781 * The given object will be modified.
783 keepOnlyWorthSavingTabs(aState) {
784 let closedWindowShouldRestore = null;
785 for (let i = aState.windows.length - 1; i >= 0; i--) {
786 let win = aState.windows[i];
787 for (let j = win.tabs.length - 1; j >= 0; j--) {
788 let tab = win.tabs[j];
789 if (!SessionStoreInternal._shouldSaveTab(tab)) {
790 win.tabs.splice(j, 1);
791 if (win.selected > j) {
797 // If it's the last window (and no closedWindow that will restore), keep the window state with no tabs.
800 (aState.windows.length > 1 ||
801 closedWindowShouldRestore ||
802 (closedWindowShouldRestore == null &&
803 (closedWindowShouldRestore = aState._closedWindows.some(
804 w => w._shouldRestore
807 aState.windows.splice(i, 1);
808 if (aState.selectedWindow > i) {
809 aState.selectedWindow--;
816 * Clear session store data for a given private browsing window.
817 * @param {ChromeWindow} win - Open private browsing window to clear data for.
819 purgeDataForPrivateWindow(win) {
820 return SessionStoreInternal.purgeDataForPrivateWindow(win);
824 * Add a tab group to the session's saved group list.
825 * @param {MozTabbrowserTabGroup} tabGroup - The group to save
827 addSavedTabGroup(tabGroup) {
828 return SessionStoreInternal.addSavedTabGroup(tabGroup);
832 * Retrieve the tab group state of a saved tab group by ID.
834 * @param {string} tabGroupId
835 * @returns {SavedTabGroupStateData|undefined}
837 getSavedTabGroup(tabGroupId) {
838 return SessionStoreInternal.getSavedTabGroup(tabGroupId);
842 * Returns all tab groups that were saved in this session.
843 * @returns {SavedTabGroupStateData[]}
845 getSavedTabGroups() {
846 return SessionStoreInternal.getSavedTabGroups();
850 * Remove a tab group from the session's saved tab group list.
851 * @param {string} tabGroupId
852 * The ID of the tab group to remove
854 forgetSavedTabGroup(tabGroupId) {
855 return SessionStoreInternal.forgetSavedTabGroup(tabGroupId);
859 * Re-open a closed tab group
860 * @param {Window|Object} source
861 * Either a DOMWindow or an object with properties to resolve to the window
862 * the tab was previously open in.
863 * @param {string} source.sourceWindowId
864 A SessionStore window id used to look up the window where the tab was closed.
865 * @param {number} source.sourceClosedId
866 The closedId used to look up the closed window where the tab was closed.
867 * @param {string} tabGroupId
868 * The unique ID of the group to restore.
869 * @param {Window} [targetWindow] defaults to the top window if not specified.
870 * @returns {MozTabbrowserTabGroup}
871 * a reference to the restored tab group in a browser window.
873 undoCloseTabGroup(source, tabGroupId, targetWindow) {
874 return SessionStoreInternal.undoCloseTabGroup(
882 * Re-open a saved tab group.
883 * Note that this method does not require passing a window source, as saved
884 * tab groups are independent of windows.
885 * Attempting to open a saved tab group in a private window will raise an error.
886 * @param {string} tabGroupId
887 * The unique ID of the group to restore.
888 * @param {Window} [targetWindow] defaults to the top window if not specified.
889 * @returns {MozTabbrowserTabGroup}
890 * a reference to the restored tab group in a browser window.
892 openSavedTabGroup(tabGroupId, targetWindow) {
893 return SessionStoreInternal.openSavedTabGroup(tabGroupId, targetWindow);
897 * Determine whether a group is saveable, based on whether any of its tabs
898 * are saveable per ssi_shouldSaveTabState.
899 * @param {MozTabbrowserTabGroup} group the tab group to check
900 * @returns {boolean} true if the group can be saved, false if it should
903 shouldSaveTabGroup(group) {
904 return SessionStoreInternal.shouldSaveTabGroup(group);
908 // Freeze the SessionStore object. We don't want anyone to modify it.
909 Object.freeze(SessionStore);
912 * @namespace SessionStoreInternal
914 * @description Internal implementations and helpers for the public SessionStore methods
916 var SessionStoreInternal = {
917 QueryInterface: ChromeUtils.generateQI([
919 "nsISupportsWeakReference",
922 _globalState: new GlobalState(),
924 // A counter to be used to generate a unique ID for each closed tab or window.
927 // During the initial restore and setBrowserState calls tracks the number of
928 // windows yet to be restored
931 // For each <browser> element, records the SHistoryListener.
932 _browserSHistoryListener: new WeakMap(),
934 // Tracks the various listeners that are used throughout the restore.
935 _restoreListeners: new WeakMap(),
937 // Records the promise created in _restoreHistory, which is used to track
938 // the completion of the first phase of the restore.
939 _tabStateRestorePromises: new WeakMap(),
941 // The history data needed to be restored in the parent.
942 _tabStateToRestore: new WeakMap(),
944 // For each <browser> element, records the current epoch.
945 _browserEpochs: new WeakMap(),
947 // Any browsers that fires the oop-browser-crashed event gets stored in
948 // here - that way we know which browsers to ignore messages from (until
949 // they get restored).
950 _crashedBrowsers: new WeakSet(),
952 // A map (xul:browser -> FrameLoader) that maps a browser to the last
953 // associated frameLoader we heard about.
954 _lastKnownFrameLoader: new WeakMap(),
956 // A map (xul:browser -> object) that maps a browser associated with a
957 // recently closed tab to all its necessary state information we need to
958 // properly handle final update message.
959 _closingTabMap: new WeakMap(),
961 // A map (xul:browser -> object) that maps a browser associated with a
962 // recently closed tab due to a window closure to the tab state information
963 // that is being stored in _closedWindows for that tab.
964 _tabClosingByWindowMap: new WeakMap(),
966 // A set of window data that has the potential to be saved in the _closedWindows
967 // array for the session. We will remove window data from this set whenever
968 // forgetClosedWindow is called for the window, or when session history is
969 // purged, so that we don't accidentally save that data after the flush has
970 // completed. Closed tabs use a more complicated mechanism for this particular
971 // problem. When forgetClosedTab is called, the browser is removed from the
972 // _closingTabMap, so its data is not recorded. In the purge history case,
973 // the closedTabs array per window is overwritten so that once the flush is
974 // complete, the tab would only ever add itself to an array that SessionStore
975 // no longer cares about. Bug 1230636 has been filed to make the tab case
976 // work more like the window case, which is more explicit, and easier to
978 _saveableClosedWindowData: new WeakSet(),
980 // whether a setBrowserState call is in progress
981 _browserSetState: false,
983 // time in milliseconds when the session was started (saved across sessions),
984 // defaults to now if no session was restored or timestamp doesn't exist
985 _sessionStartTime: Date.now(),
988 * states for all currently opened windows
989 * @type {object.<WindowID, WindowStateData>}
993 // counter for creating unique window IDs
996 // states for all recently closed windows
999 /** @type {SavedTabGroupStateData[]} states for all saved+closed tab groups */
1002 // collection of session states yet to be restored
1003 _statesToRestore: {},
1005 // counts the number of crashes since the last clean start
1008 // whether the last window was closed and should be restored
1009 _restoreLastWindow: false,
1011 // number of tabs currently restoring
1012 _tabsRestoringCount: 0,
1015 * @typedef {Object} CloseAction
1016 * @property {string} type
1017 * What the close action acted upon. One of either _LAST_ACTION_CLOSED_TAB or
1018 * _LAST_ACTION_CLOSED_WINDOW
1019 * @property {number} closedId
1020 * The unique ID of the item that closed.
1024 * An in-order stack of close actions for tabs and windows.
1025 * @type {CloseAction[]}
1027 _lastClosedActions: [],
1030 * Removes an object from the _lastClosedActions list
1032 * @param closedAction
1033 * Either _LAST_ACTION_CLOSED_TAB or _LAST_ACTION_CLOSED_WINDOW
1034 * @param {integer} closedId
1035 * The closedId of a tab or window
1037 _removeClosedAction(closedAction, closedId) {
1038 let closedActionIndex = this._lastClosedActions.findIndex(
1039 obj => obj.type == closedAction && obj.closedId == closedId
1042 if (closedActionIndex > -1) {
1043 this._lastClosedActions.splice(closedActionIndex, 1);
1048 * Add an object to the _lastClosedActions list and truncates the list if needed
1050 * @param closedAction
1051 * Either _LAST_ACTION_CLOSED_TAB or _LAST_ACTION_CLOSED_WINDOW
1052 * @param {integer} closedId
1053 * The closedId of a tab or window
1055 _addClosedAction(closedAction, closedId) {
1056 this._lastClosedActions.push({
1060 let maxLength = this._max_tabs_undo * this._max_windows_undo;
1062 if (this._lastClosedActions.length > maxLength) {
1063 this._lastClosedActions = this._lastClosedActions.slice(-maxLength);
1067 _LAST_ACTION_CLOSED_TAB: "tab",
1069 _LAST_ACTION_CLOSED_WINDOW: "window",
1073 // When starting Firefox with a single private window, this is the place
1074 // where we keep the session we actually wanted to restore in case the user
1075 // decides to later open a non-private window as well.
1076 _deferredInitialState: null,
1078 // Keeps track of whether a notification needs to be sent that closed objects have changed.
1079 _closedObjectsChanged: false,
1081 // A promise resolved once initialization is complete
1082 _deferredInitialized: (function () {
1085 deferred.promise = new Promise((resolve, reject) => {
1086 deferred.resolve = resolve;
1087 deferred.reject = reject;
1093 // Whether session has been initialized
1094 _sessionInitialized: false,
1096 // A promise resolved once all windows are restored.
1097 _deferredAllWindowsRestored: (function () {
1100 deferred.promise = new Promise((resolve, reject) => {
1101 deferred.resolve = resolve;
1102 deferred.reject = reject;
1108 get promiseAllWindowsRestored() {
1109 return this._deferredAllWindowsRestored.promise;
1112 // Promise that is resolved when we're ready to initialize
1113 // and restore the session.
1114 _promiseReadyForInitialization: null,
1116 // Keep busy state counters per window.
1117 _windowBusyStates: new WeakMap(),
1120 * A promise fulfilled once initialization is complete.
1122 get promiseInitialized() {
1123 return this._deferredInitialized.promise;
1126 get canRestoreLastSession() {
1127 return LastSession.canRestore;
1130 set canRestoreLastSession(val) {
1131 // Cheat a bit; only allow false.
1133 LastSession.clear();
1138 * Returns a string describing the last closed object, either "tab" or "window".
1140 * This was added to support the sessions.restore WebExtensions API.
1142 get lastClosedObjectType() {
1143 if (this._closedWindows.length) {
1144 // Since there are closed windows, we need to check if there's a closed tab
1145 // in one of the currently open windows that was closed after the
1146 // last-closed window.
1147 let tabTimestamps = [];
1148 for (let window of Services.wm.getEnumerator("navigator:browser")) {
1149 let windowState = this._windows[window.__SSi];
1150 if (windowState && windowState._closedTabs[0]) {
1151 tabTimestamps.push(windowState._closedTabs[0].closedAt);
1155 !tabTimestamps.length ||
1156 tabTimestamps.sort((a, b) => b - a)[0] < this._closedWindows[0].closedAt
1158 return this._LAST_ACTION_CLOSED_WINDOW;
1161 return this._LAST_ACTION_CLOSED_TAB;
1165 * Returns a boolean that determines whether the session will be automatically
1166 * restored upon the _next_ startup or a restart.
1168 get willAutoRestore() {
1170 !PrivateBrowsingUtils.permanentPrivateBrowsing &&
1171 (Services.prefs.getBoolPref("browser.sessionstore.resume_session_once") ||
1172 Services.prefs.getIntPref("browser.startup.page") ==
1173 BROWSER_STARTUP_RESUME_SESSION)
1178 * Initialize the sessionstore service.
1181 if (this._initialized) {
1182 throw new Error("SessionStore.init() must only be called once!");
1185 TelemetryTimestamps.add("sessionRestoreInitialized");
1186 OBSERVING.forEach(function (aTopic) {
1187 Services.obs.addObserver(this, aTopic, true);
1191 this._initialized = true;
1193 this.promiseAllWindowsRestored.finally(() => () => {
1194 this._log.debug("promiseAllWindowsRestored finalized");
1199 * Initialize the session using the state provided by SessionStartup
1202 TelemetryStopwatch.start("FX_SESSION_RESTORE_STARTUP_INIT_SESSION_MS");
1204 let ss = lazy.SessionStartup;
1205 let willRestore = ss.willRestore();
1206 if (willRestore || ss.sessionType == ss.DEFER_SESSION) {
1210 `initSession willRestore: ${willRestore}, SessionStartup.sessionType: ${ss.sessionType}`
1215 // If we're doing a DEFERRED session, then we want to pull pinned tabs
1216 // out so they can be restored.
1217 if (ss.sessionType == ss.DEFER_SESSION) {
1218 let [iniState, remainingState] =
1219 this._prepDataForDeferredRestore(state);
1220 // If we have a iniState with windows, that means that we have windows
1221 // with pinned tabs to restore.
1222 if (iniState.windows.length) {
1228 `initSession deferred restore with ${iniState.windows.length} initial windows, ${remainingState.windows.length} remaining windows`
1231 if (remainingState.windows.length) {
1232 LastSession.setState(remainingState);
1234 Glean.browserEngagement.sessionrestoreInterstitial.deferred_restore.add(
1238 // Get the last deferred session in case the user still wants to
1240 LastSession.setState(state.lastSessionState);
1242 let restoreAsCrashed = ss.willRestoreAsCrashed();
1243 if (restoreAsCrashed) {
1244 this._recentCrashes =
1245 ((state.session && state.session.recentCrashes) || 0) + 1;
1247 `initSession, restoreAsCrashed, crashes: ${this._recentCrashes}`
1250 // _needsRestorePage will record sessionrestore_interstitial,
1251 // including the specific reason we decided we needed to show
1252 // about:sessionrestore, if that's what we do.
1253 if (this._needsRestorePage(state, this._recentCrashes)) {
1254 // replace the crashed session with a restore-page-only session
1255 let url = "about:sessionrestore";
1256 let formdata = { id: { sessionData: state }, url };
1259 triggeringPrincipal_base64:
1260 lazy.E10SUtils.SERIALIZED_SYSTEMPRINCIPAL,
1262 state = { windows: [{ tabs: [{ entries: [entry], formdata }] }] };
1263 this._log.debug("initSession, will show about:sessionrestore");
1265 this._hasSingleTabWithURL(state.windows, "about:welcomeback")
1267 this._log.debug("initSession, will show about:welcomeback");
1268 Glean.browserEngagement.sessionrestoreInterstitial.shown_only_about_welcomeback.add(
1271 // On a single about:welcomeback URL that crashed, replace about:welcomeback
1272 // with about:sessionrestore, to make clear to the user that we crashed.
1273 state.windows[0].tabs[0].entries[0].url = "about:sessionrestore";
1274 state.windows[0].tabs[0].entries[0].triggeringPrincipal_base64 =
1275 lazy.E10SUtils.SERIALIZED_SYSTEMPRINCIPAL;
1277 restoreAsCrashed = false;
1281 // If we didn't use about:sessionrestore, record that:
1282 if (!restoreAsCrashed) {
1283 Glean.browserEngagement.sessionrestoreInterstitial.autorestore.add(
1286 this._log.debug("initSession, will autorestore");
1287 this._removeExplicitlyClosedTabs(state);
1290 // Update the session start time using the restored session state.
1291 this._updateSessionStartTime(state);
1293 // Make sure that at least the first window doesn't have anything hidden.
1294 delete state.windows[0].hidden;
1295 // Since nothing is hidden in the first window, it cannot be a popup.
1296 delete state.windows[0].isPopup;
1297 // We don't want to minimize and then open a window at startup.
1298 if (state.windows[0].sizemode == "minimized") {
1299 state.windows[0].sizemode = "normal";
1302 // clear any lastSessionWindowID attributes since those don't matter
1303 // during normal restore
1304 state.windows.forEach(function (aWindow) {
1305 delete aWindow.__lastSessionWindowID;
1309 // clear _maybeDontRestoreTabs because we have restored (or not)
1310 // windows and so they don't matter
1311 state?.windows?.forEach(win => delete win._maybeDontRestoreTabs);
1312 state?._closedWindows?.forEach(win => delete win._maybeDontRestoreTabs);
1314 this._savedGroups = state?.savedGroups ?? [];
1316 this._log.error("The session file is invalid: ", ex);
1320 // at this point, we've as good as resumed the session, so we can
1321 // clear the resume_session_once flag, if it's set
1323 !lazy.RunState.isQuitting &&
1324 this._prefBranch.getBoolPref("sessionstore.resume_session_once")
1326 this._prefBranch.setBoolPref("sessionstore.resume_session_once", false);
1329 TelemetryStopwatch.finish("FX_SESSION_RESTORE_STARTUP_INIT_SESSION_MS");
1334 * When initializing session, if we are restoring the last session at startup,
1335 * close open tabs or close windows marked _maybeDontRestoreTabs (if they were closed
1336 * by closing remaining tabs).
1339 _removeExplicitlyClosedTabs(state) {
1340 // Don't restore tabs that has been explicitly closed
1341 for (let i = 0; i < state.windows.length; ) {
1342 const winData = state.windows[i];
1343 if (winData._maybeDontRestoreTabs) {
1344 if (state.windows.length == 1) {
1345 // it's the last window, we just want to close tabs
1347 // reset close group (we don't want to append tabs to existing group close).
1348 winData._lastClosedTabGroupCount = -1;
1349 while (winData.tabs.length) {
1350 const tabState = winData.tabs.pop();
1352 // Ensure the index is in bounds.
1353 let activeIndex = (tabState.index || tabState.entries.length) - 1;
1354 activeIndex = Math.min(activeIndex, tabState.entries.length - 1);
1355 activeIndex = Math.max(activeIndex, 0);
1358 if (activeIndex in tabState.entries) {
1360 tabState.entries[activeIndex].title ||
1361 tabState.entries[activeIndex].url;
1367 image: tabState.image,
1369 closedAt: Date.now(),
1370 closedInGroup: true,
1372 if (this._shouldSaveTabState(tabState)) {
1373 this.saveClosedTabData(winData, winData._closedTabs, tabData);
1377 // We can remove the window since it doesn't have any
1378 // tabs that we should restore and it's not the only window
1379 if (winData.tabs.some(this._shouldSaveTabState)) {
1380 winData.closedAt = Date.now();
1381 state._closedWindows.unshift(winData);
1383 state.windows.splice(i, 1);
1384 continue; // we don't want to increment the index
1392 this._prefBranch = Services.prefs.getBranch("browser.");
1394 gDebuggingEnabled = this._prefBranch.getBoolPref("sessionstore.debug");
1396 Services.prefs.addObserver("browser.sessionstore.debug", () => {
1397 gDebuggingEnabled = this._prefBranch.getBoolPref("sessionstore.debug");
1400 this._log = lazy.sessionStoreLogger;
1402 this._max_tabs_undo = this._prefBranch.getIntPref(
1403 "sessionstore.max_tabs_undo"
1405 this._prefBranch.addObserver("sessionstore.max_tabs_undo", this, true);
1407 this._closedTabsFromAllWindowsEnabled = this._prefBranch.getBoolPref(
1408 "sessionstore.closedTabsFromAllWindows"
1410 this._prefBranch.addObserver(
1411 "sessionstore.closedTabsFromAllWindows",
1416 this._closedTabsFromClosedWindowsEnabled = this._prefBranch.getBoolPref(
1417 "sessionstore.closedTabsFromClosedWindows"
1419 this._prefBranch.addObserver(
1420 "sessionstore.closedTabsFromClosedWindows",
1425 this._max_windows_undo = this._prefBranch.getIntPref(
1426 "sessionstore.max_windows_undo"
1428 this._prefBranch.addObserver("sessionstore.max_windows_undo", this, true);
1430 this._restore_on_demand = this._prefBranch.getBoolPref(
1431 "sessionstore.restore_on_demand"
1433 this._prefBranch.addObserver("sessionstore.restore_on_demand", this, true);
1437 * Called on application shutdown, after notifications:
1438 * quit-application-granted, quit-application
1440 _uninit: function ssi_uninit() {
1441 if (!this._initialized) {
1442 throw new Error("SessionStore is not initialized.");
1445 // Prepare to close the session file and write the last state.
1446 lazy.RunState.setClosing();
1448 // save all data for session resuming
1449 if (this._sessionInitialized) {
1450 lazy.SessionSaver.run();
1453 // clear out priority queue in case it's still holding refs
1454 TabRestoreQueue.reset();
1456 // Make sure to cancel pending saves.
1457 lazy.SessionSaver.cancel();
1461 * Handle notifications
1463 observe: function ssi_observe(aSubject, aTopic, aData) {
1465 case "browser-window-before-show": // catch new windows
1466 this.onBeforeBrowserWindowShown(aSubject);
1468 case "domwindowclosed": // catch closed windows
1469 this.onClose(aSubject).then(() => {
1470 this._notifyOfClosedObjectsChange();
1472 if (gDebuggingEnabled) {
1473 Services.obs.notifyObservers(null, NOTIFY_DOMWINDOWCLOSED_HANDLED);
1476 case "quit-application-granted": {
1477 let syncShutdown = aData == "syncShutdown";
1478 this.onQuitApplicationGranted(syncShutdown);
1481 case "browser-lastwindow-close-granted":
1482 this.onLastWindowCloseGranted();
1484 case "quit-application":
1485 this.onQuitApplication(aData);
1487 case "browser:purge-session-history": // catch sanitization
1488 this.onPurgeSessionHistory();
1489 this._notifyOfClosedObjectsChange();
1491 case "browser:purge-session-history-for-domain":
1492 this.onPurgeDomainData(aData);
1493 this._notifyOfClosedObjectsChange();
1495 case "nsPref:changed": // catch pref changes
1496 this.onPrefChange(aData);
1497 this._notifyOfClosedObjectsChange();
1501 this._notifyOfClosedObjectsChange();
1503 case "clear-origin-attributes-data": {
1504 let userContextId = 0;
1506 userContextId = JSON.parse(aData).userContextId;
1508 if (userContextId) {
1509 this._forgetTabsWithUserContextId(userContextId);
1513 case "browsing-context-did-set-embedder":
1514 if (aSubject === aSubject.top && aSubject.isContent) {
1515 const permanentKey = aSubject.embedderElement?.permanentKey;
1517 this.maybeRecreateSHistoryListener(permanentKey, aSubject);
1521 case "browsing-context-discarded": {
1522 let permanentKey = aSubject?.embedderElement?.permanentKey;
1524 this._browserSHistoryListener.get(permanentKey)?.unregister();
1528 case "browser-shutdown-tabstate-updated":
1529 this.onFinalTabStateUpdateComplete(aSubject);
1530 this._notifyOfClosedObjectsChange();
1535 getOrCreateSHistoryListener(permanentKey, browsingContext) {
1536 if (!permanentKey || browsingContext !== browsingContext.top) {
1540 const listener = this._browserSHistoryListener.get(permanentKey);
1545 return this.createSHistoryListener(permanentKey, browsingContext, false);
1548 maybeRecreateSHistoryListener(permanentKey, browsingContext) {
1549 const listener = this._browserSHistoryListener.get(permanentKey);
1550 if (!listener || listener._browserId != browsingContext.browserId) {
1551 listener?.unregister(permanentKey);
1552 this.createSHistoryListener(permanentKey, browsingContext, true);
1556 createSHistoryListener(permanentKey, browsingContext, collectImmediately) {
1557 class SHistoryListener {
1559 this.QueryInterface = ChromeUtils.generateQI([
1560 "nsISHistoryListener",
1561 "nsISupportsWeakReference",
1564 this._browserId = browsingContext.browserId;
1565 this._fromIndex = kNoIndex;
1569 let bc = BrowsingContext.getCurrentTopByBrowserId(this._browserId);
1570 bc?.sessionHistory?.removeSHistoryListener(this);
1571 SessionStoreInternal._browserSHistoryListener.delete(permanentKey);
1575 permanentKey, // eslint-disable-line no-shadow
1576 browsingContext, // eslint-disable-line no-shadow
1577 { collectFull = true, writeToCache = false }
1579 // Don't bother doing anything if we haven't seen any navigations.
1580 if (!collectFull && this._fromIndex === kNoIndex) {
1584 TelemetryStopwatch.start(
1585 "FX_SESSION_RESTORE_COLLECT_SESSION_HISTORY_MS"
1588 let fromIndex = collectFull ? -1 : this._fromIndex;
1589 this._fromIndex = kNoIndex;
1591 let historychange = lazy.SessionHistory.collectFromParent(
1592 browsingContext.currentURI?.spec,
1593 true, // Bug 1704574
1594 browsingContext.sessionHistory,
1600 browsingContext.embedderElement?.ownerGlobal ||
1601 browsingContext.currentWindowGlobal?.browsingContext?.window;
1603 SessionStoreInternal.onTabStateUpdate(permanentKey, win, {
1604 data: { historychange },
1608 TelemetryStopwatch.finish(
1609 "FX_SESSION_RESTORE_COLLECT_SESSION_HISTORY_MS"
1612 return historychange;
1615 collectFrom(index) {
1616 if (this._fromIndex <= index) {
1617 // If we already know that we need to update history from index N we
1618 // can ignore any changes that happened with an element with index
1621 // Note: initially we use kNoIndex which is MAX_SAFE_INTEGER which
1622 // means we don't ignore anything here, and in case of navigation in
1623 // the history back and forth cases we use kLastIndex which ignores
1624 // only the subsequent navigations, but not any new elements added.
1628 let bc = BrowsingContext.getCurrentTopByBrowserId(this._browserId);
1629 if (bc?.embedderElement?.frameLoader) {
1630 this._fromIndex = index;
1632 // Queue a tab state update on the |browser.sessionstore.interval|
1633 // timer. We'll call this.collect() when we receive the update.
1634 bc.embedderElement.frameLoader.requestSHistoryUpdate();
1638 OnHistoryNewEntry(newURI, oldIndex) {
1639 // We use oldIndex - 1 to collect the current entry as well. This makes
1640 // sure to collect any changes that were made to the entry while the
1641 // document was active.
1642 this.collectFrom(oldIndex == -1 ? oldIndex : oldIndex - 1);
1644 OnHistoryGotoIndex() {
1645 this.collectFrom(kLastIndex);
1648 this.collectFrom(-1);
1651 this.collectFrom(-1);
1654 OnHistoryReplaceEntry() {
1655 this.collectFrom(-1);
1659 let sessionHistory = browsingContext.sessionHistory;
1660 if (!sessionHistory) {
1664 const listener = new SHistoryListener();
1665 sessionHistory.addSHistoryListener(listener);
1666 this._browserSHistoryListener.set(permanentKey, listener);
1668 let isAboutBlank = browsingContext.currentURI?.spec === "about:blank";
1670 if (collectImmediately && (!isAboutBlank || sessionHistory.count !== 0)) {
1671 listener.collect(permanentKey, browsingContext, { writeToCache: true });
1677 onTabStateUpdate(permanentKey, win, update) {
1678 // Ignore messages from <browser> elements that have crashed
1679 // and not yet been revived.
1680 if (this._crashedBrowsers.has(permanentKey)) {
1684 lazy.TabState.update(permanentKey, update);
1685 this.saveStateDelayed(win);
1687 // Handle any updates sent by the child after the tab was closed. This
1688 // might be the final update as sent by the "unload" handler but also
1689 // any async update message that was sent before the child unloaded.
1690 let closedTab = this._closingTabMap.get(permanentKey);
1692 // Update the closed tab's state. This will be reflected in its
1693 // window's list of closed tabs as that refers to the same object.
1694 lazy.TabState.copyFromCache(permanentKey, closedTab.tabData.state);
1698 onFinalTabStateUpdateComplete(browser) {
1699 let permanentKey = browser.permanentKey;
1701 this._closingTabMap.has(permanentKey) &&
1702 !this._crashedBrowsers.has(permanentKey)
1704 let { winData, closedTabs, tabData } =
1705 this._closingTabMap.get(permanentKey);
1707 // We expect no further updates.
1708 this._closingTabMap.delete(permanentKey);
1710 // The tab state no longer needs this reference.
1711 delete tabData.permanentKey;
1713 // Determine whether the tab state is worth saving.
1714 let shouldSave = this._shouldSaveTabState(tabData.state);
1715 let index = closedTabs.indexOf(tabData);
1717 if (shouldSave && index == -1) {
1718 // If the tab state is worth saving and we didn't push it onto
1719 // the list of closed tabs when it was closed (because we deemed
1720 // the state not worth saving) then add it to the window's list
1721 // of closed tabs now.
1722 this.saveClosedTabData(winData, closedTabs, tabData);
1723 } else if (!shouldSave && index > -1) {
1724 // Remove from the list of closed tabs. The update messages sent
1725 // after the tab was closed changed enough state so that we no
1726 // longer consider its data interesting enough to keep around.
1727 this.removeClosedTabData(winData, closedTabs, index);
1730 this._cleanupOrphanedClosedGroups(winData);
1733 // If this the final message we need to resolve all pending flush
1734 // requests for the given browser as they might have been sent too
1735 // late and will never respond. If they have been sent shortly after
1736 // switching a browser's remoteness there isn't too much data to skip.
1737 lazy.TabStateFlusher.resolveAll(browser);
1739 this._browserSHistoryListener.get(permanentKey)?.unregister();
1740 this._restoreListeners.get(permanentKey)?.unregister();
1742 Services.obs.notifyObservers(browser, NOTIFY_BROWSER_SHUTDOWN_FLUSH);
1745 updateSessionStoreFromTablistener(
1752 permanentKey = browser?.permanentKey ?? permanentKey;
1753 if (!permanentKey) {
1757 // Ignore sessionStore update from previous epochs
1758 if (!this.isCurrentEpoch(permanentKey, update.epoch)) {
1762 if (browsingContext.isReplaced) {
1766 let listener = this.getOrCreateSHistoryListener(
1773 // If it is not the scheduled update (tab closed, window closed etc),
1774 // try to store the loading non-web-controlled page opened in _blank
1777 lazy.SessionHistory.collectNonWebControlledBlankLoadingSession(
1780 listener.collect(permanentKey, browsingContext, {
1781 collectFull: !!update.sHistoryNeeded,
1782 writeToCache: false,
1785 if (historychange) {
1786 update.data.historychange = historychange;
1791 browser?.ownerGlobal ??
1792 browsingContext.currentWindowGlobal?.browsingContext?.window;
1794 this.onTabStateUpdate(permanentKey, win, update);
1797 /* ........ Window Event Handlers .............. */
1800 * Implement EventListener for handling various window and tab events
1802 handleEvent: function ssi_handleEvent(aEvent) {
1803 let win = aEvent.currentTarget.ownerGlobal;
1804 let target = aEvent.originalTarget;
1805 switch (aEvent.type) {
1809 case "TabBrowserInserted":
1810 this.onTabBrowserInserted(win, target);
1813 // `adoptedBy` will be set if the tab was closed because it is being
1814 // moved to a new window.
1815 if (aEvent.detail.adoptedBy) {
1816 this.onMoveToNewWindow(
1817 target.linkedBrowser,
1818 aEvent.detail.adoptedBy.linkedBrowser
1820 } else if (!aEvent.detail.skipSessionStore) {
1821 // `skipSessionStore` is set by tab close callers to indicate that we
1822 // shouldn't record the closed tab.
1823 this.onTabClose(win, target);
1825 this.onTabRemove(win, target);
1826 this._notifyOfClosedObjectsChange();
1829 this.onTabSelect(win);
1832 this.onTabShow(win, target);
1835 this.onTabHide(win, target);
1839 case "SwapDocShells":
1840 this.saveStateDelayed(win);
1842 case "TabGroupCreate":
1843 case "TabGroupRemoved":
1845 case "TabUngrouped":
1846 case "TabGroupCollapse":
1847 case "TabGroupExpand":
1848 this.saveStateDelayed(win);
1850 case "TabGroupRemoveRequested":
1851 this.onTabGroupRemoveRequested(win, target);
1852 this._notifyOfClosedObjectsChange();
1854 case "oop-browser-crashed":
1855 case "oop-browser-buildid-mismatch":
1856 if (aEvent.isTopFrame) {
1857 this.onBrowserCrashed(target);
1860 case "XULFrameLoaderCreated":
1862 target.namespaceURI == XUL_NS &&
1863 target.localName == "browser" &&
1864 target.frameLoader &&
1867 this._lastKnownFrameLoader.set(
1868 target.permanentKey,
1871 this.resetEpoch(target.permanentKey, target.frameLoader);
1875 throw new Error(`unhandled event ${aEvent.type}?`);
1877 this._clearRestoringWindows();
1881 * Generate a unique window identifier
1883 * A unique string to identify a window
1885 _generateWindowID: function ssi_generateWindowID() {
1886 return "window" + this._nextWindowID++;
1890 * Registers and tracks a given window.
1896 // return if window has already been initialized
1897 if (aWindow && aWindow.__SSi && this._windows[aWindow.__SSi]) {
1901 // ignore windows opened while shutting down
1902 if (lazy.RunState.isQuitting) {
1906 // Assign the window a unique identifier we can use to reference
1907 // internal data about the window.
1908 aWindow.__SSi = this._generateWindowID();
1910 // and create its data object
1911 this._windows[aWindow.__SSi] = {
1917 // NOTE: this naming refers to the number of tabs in a *multiselection*, not in a tab group.
1918 // This naming was chosen before the introduction of tab groups proper.
1919 // TODO: choose more distinct naming in bug1928424
1920 _lastClosedTabGroupCount: -1,
1921 lastClosedTabGroupId: null,
1923 chromeFlags: aWindow.docShell.treeOwner
1924 .QueryInterface(Ci.nsIInterfaceRequestor)
1925 .getInterface(Ci.nsIAppWindow).chromeFlags,
1928 if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) {
1929 this._windows[aWindow.__SSi].isPrivate = true;
1931 if (!this._isWindowLoaded(aWindow)) {
1932 this._windows[aWindow.__SSi]._restoring = true;
1934 if (!aWindow.toolbar.visible) {
1935 this._windows[aWindow.__SSi].isPopup = true;
1938 let tabbrowser = aWindow.gBrowser;
1940 // add tab change listeners to all already existing tabs
1941 for (let i = 0; i < tabbrowser.tabs.length; i++) {
1942 this.onTabBrowserInserted(aWindow, tabbrowser.tabs[i]);
1944 // notification of tab add/remove/selection/show/hide
1945 TAB_EVENTS.forEach(function (aEvent) {
1946 tabbrowser.tabContainer.addEventListener(aEvent, this, true);
1949 // Keep track of a browser's latest frameLoader.
1950 aWindow.gBrowser.addEventListener("XULFrameLoaderCreated", this);
1954 * Initializes a given window.
1956 * Windows are registered as soon as they are created but we need to wait for
1957 * the session file to load, and the initial window's delayed startup to
1958 * finish before initializing a window, i.e. restoring data into it.
1962 * @param aInitialState
1963 * The initial state to be loaded after startup (optional)
1965 initializeWindow(aWindow, aInitialState = null) {
1966 let isPrivateWindow = PrivateBrowsingUtils.isWindowPrivate(aWindow);
1968 // perform additional initialization when the first window is loading
1969 if (lazy.RunState.isStopped) {
1970 lazy.RunState.setRunning();
1972 // restore a crashed session resp. resume the last session if requested
1973 if (aInitialState) {
1974 // Don't write to disk right after startup. Set the last time we wrote
1975 // to disk to NOW() to enforce a full interval before the next write.
1976 lazy.SessionSaver.updateLastSaveTime();
1978 if (isPrivateWindow) {
1980 "initializeWindow, the window is private. Saving SessionStartup.state for possibly restoring later"
1982 // We're starting with a single private window. Save the state we
1983 // actually wanted to restore so that we can do it later in case
1984 // the user opens another, non-private window.
1985 this._deferredInitialState = lazy.SessionStartup.state;
1987 // Nothing to restore now, notify observers things are complete.
1988 Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED);
1989 Services.obs.notifyObservers(
1991 "sessionstore-one-or-no-tab-restored"
1993 this._deferredAllWindowsRestored.resolve();
1995 TelemetryTimestamps.add("sessionRestoreRestoring");
1996 this._restoreCount = aInitialState.windows
1997 ? aInitialState.windows.length
2000 // global data must be restored before restoreWindow is called so that
2001 // it happens before observers are notified
2002 this._globalState.setFromState(aInitialState);
2004 // Restore session cookies before loading any tabs.
2005 lazy.SessionCookies.restore(aInitialState.cookies || []);
2007 let overwrite = this._isCmdLineEmpty(aWindow, aInitialState);
2008 let options = { firstWindow: true, overwriteTabs: overwrite };
2009 this.restoreWindows(aWindow, aInitialState, options);
2012 // Nothing to restore, notify observers things are complete.
2013 Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED);
2014 Services.obs.notifyObservers(
2016 "sessionstore-one-or-no-tab-restored"
2018 this._deferredAllWindowsRestored.resolve();
2020 // this window was opened by _openWindowWithState
2021 } else if (!this._isWindowLoaded(aWindow)) {
2022 // We want to restore windows after all windows have opened (since bug
2023 // 1034036), so bail out here.
2025 // The user opened another, non-private window after starting up with
2026 // a single private one. Let's restore the session we actually wanted to
2027 // restore at startup.
2029 this._deferredInitialState &&
2031 aWindow.toolbar.visible
2033 // global data must be restored before restoreWindow is called so that
2034 // it happens before observers are notified
2035 this._globalState.setFromState(this._deferredInitialState);
2037 this._restoreCount = this._deferredInitialState.windows
2038 ? this._deferredInitialState.windows.length
2040 this.restoreWindows(aWindow, this._deferredInitialState, {
2043 this._deferredInitialState = null;
2045 this._restoreLastWindow &&
2046 aWindow.toolbar.visible &&
2047 this._closedWindows.length &&
2050 // default to the most-recently closed window
2051 // don't use popup windows
2052 let closedWindowState = null;
2053 let closedWindowIndex;
2054 for (let i = 0; i < this._closedWindows.length; i++) {
2055 // Take the first non-popup, point our object at it, and break out.
2056 if (!this._closedWindows[i].isPopup) {
2057 closedWindowState = this._closedWindows[i];
2058 closedWindowIndex = i;
2063 if (closedWindowState) {
2066 AppConstants.platform == "macosx" ||
2067 !lazy.SessionStartup.willRestore()
2069 // We want to split the window up into pinned tabs and unpinned tabs.
2070 // Pinned tabs should be restored. If there are any remaining tabs,
2071 // they should be added back to _closedWindows.
2072 // We'll cheat a little bit and reuse _prepDataForDeferredRestore
2073 // even though it wasn't built exactly for this.
2074 let [appTabsState, normalTabsState] =
2075 this._prepDataForDeferredRestore({
2076 windows: [closedWindowState],
2079 // These are our pinned tabs and sidebar attributes, which we should restore
2080 if (appTabsState.windows.length) {
2081 newWindowState = appTabsState.windows[0];
2082 delete newWindowState.__lastSessionWindowID;
2085 // In case there were no unpinned tabs, remove the window from _closedWindows
2086 if (!normalTabsState.windows.length) {
2087 this._removeClosedWindow(closedWindowIndex);
2088 // Or update _closedWindows with the modified state
2090 delete normalTabsState.windows[0].__lastSessionWindowID;
2091 this._closedWindows[closedWindowIndex] = normalTabsState.windows[0];
2094 // If we're just restoring the window, make sure it gets removed from
2096 this._removeClosedWindow(closedWindowIndex);
2097 newWindowState = closedWindowState;
2098 delete newWindowState.hidden;
2101 if (newWindowState) {
2102 // Ensure that the window state isn't hidden
2103 this._restoreCount = 1;
2104 let state = { windows: [newWindowState] };
2105 let options = { overwriteTabs: this._isCmdLineEmpty(aWindow, state) };
2106 this.restoreWindow(aWindow, newWindowState, options);
2109 // we actually restored the session just now.
2110 this._prefBranch.setBoolPref("sessionstore.resume_session_once", false);
2113 if (this._restoreLastWindow && aWindow.toolbar.visible) {
2114 // always reset (if not a popup window)
2115 // we don't want to restore a window directly after, for example,
2116 // undoCloseWindow was executed.
2117 this._restoreLastWindow = false;
2122 * Called right before a new browser window is shown.
2126 onBeforeBrowserWindowShown(aWindow) {
2127 // Register the window.
2128 this.onLoad(aWindow);
2130 // Some are waiting for this window to be shown, which is now, so let's resolve
2131 // the deferred operation.
2132 let deferred = WINDOW_SHOWING_PROMISES.get(aWindow);
2134 deferred.resolve(aWindow);
2135 WINDOW_SHOWING_PROMISES.delete(aWindow);
2138 // Just call initializeWindow() directly if we're initialized already.
2139 if (this._sessionInitialized) {
2141 "onBeforeBrowserWindowShown, session already initialized, initializing window"
2143 this.initializeWindow(aWindow);
2147 // The very first window that is opened creates a promise that is then
2148 // re-used by all subsequent windows. The promise will be used to tell
2149 // when we're ready for initialization.
2150 if (!this._promiseReadyForInitialization) {
2151 // Wait for the given window's delayed startup to be finished.
2152 let promise = new Promise(resolve => {
2153 Services.obs.addObserver(function obs(subject, topic) {
2154 if (aWindow == subject) {
2155 Services.obs.removeObserver(obs, topic);
2158 }, "browser-delayed-startup-finished");
2161 // We are ready for initialization as soon as the session file has been
2162 // read from disk and the initial window's delayed startup has finished.
2163 this._promiseReadyForInitialization = Promise.all([
2165 lazy.SessionStartup.onceInitialized,
2169 // We can't call this.onLoad since initialization
2170 // hasn't completed, so we'll wait until it is done.
2171 // Even if additional windows are opened and wait
2172 // for initialization as well, the first opened
2173 // window should execute first, and this.onLoad
2174 // will be called with the initialState.
2175 this._promiseReadyForInitialization
2177 if (aWindow.closed) {
2179 "When _promiseReadyForInitialization resolved, the window was closed"
2184 if (this._sessionInitialized) {
2185 this.initializeWindow(aWindow);
2187 let initialState = this.initSession();
2188 this._sessionInitialized = true;
2191 Services.obs.notifyObservers(null, NOTIFY_RESTORING_ON_STARTUP);
2193 TelemetryStopwatch.start(
2194 "FX_SESSION_RESTORE_STARTUP_ONLOAD_INITIAL_WINDOW_MS"
2196 this.initializeWindow(aWindow, initialState);
2197 TelemetryStopwatch.finish(
2198 "FX_SESSION_RESTORE_STARTUP_ONLOAD_INITIAL_WINDOW_MS"
2201 // Let everyone know we're done.
2202 this._deferredInitialized.resolve();
2207 "Exception when handling _promiseReadyForInitialization resolution:",
2214 * On window close...
2215 * - remove event listeners from tabs
2216 * - save all window data
2220 * @returns a Promise
2222 onClose: function ssi_onClose(aWindow) {
2223 let completionPromise = Promise.resolve();
2224 // this window was about to be restored - conserve its original data, if any
2225 let isFullyLoaded = this._isWindowLoaded(aWindow);
2226 if (!isFullyLoaded) {
2227 if (!aWindow.__SSi) {
2228 aWindow.__SSi = this._generateWindowID();
2231 let restoreID = WINDOW_RESTORE_IDS.get(aWindow);
2232 this._windows[aWindow.__SSi] =
2233 this._statesToRestore[restoreID].windows[0];
2234 delete this._statesToRestore[restoreID];
2235 WINDOW_RESTORE_IDS.delete(aWindow);
2238 // ignore windows not tracked by SessionStore
2239 if (!aWindow.__SSi || !this._windows[aWindow.__SSi]) {
2240 return completionPromise;
2243 // notify that the session store will stop tracking this window so that
2244 // extensions can store any data about this window in session store before
2245 // that's not possible anymore
2246 let event = aWindow.document.createEvent("Events");
2247 event.initEvent("SSWindowClosing", true, false);
2248 aWindow.dispatchEvent(event);
2250 if (this.windowToFocus && this.windowToFocus == aWindow) {
2251 delete this.windowToFocus;
2254 var tabbrowser = aWindow.gBrowser;
2256 let browsers = Array.from(tabbrowser.browsers);
2258 TAB_EVENTS.forEach(function (aEvent) {
2259 tabbrowser.tabContainer.removeEventListener(aEvent, this, true);
2262 aWindow.gBrowser.removeEventListener("XULFrameLoaderCreated", this);
2264 let winData = this._windows[aWindow.__SSi];
2266 // Collect window data only when *not* closed during shutdown.
2267 if (lazy.RunState.isRunning) {
2268 // Grab the most recent window data. The tab data will be updated
2269 // once we finish flushing all of the messages from the tabs.
2270 let tabMap = this._collectWindowData(aWindow);
2272 for (let [tab, tabData] of tabMap) {
2273 let permanentKey = tab.linkedBrowser.permanentKey;
2274 this._tabClosingByWindowMap.set(permanentKey, tabData);
2277 if (isFullyLoaded && !winData.title) {
2279 tabbrowser.selectedBrowser.contentTitle ||
2280 tabbrowser.selectedTab.label;
2283 if (AppConstants.platform != "macosx") {
2284 // Until we decide otherwise elsewhere, this window is part of a series
2285 // of closing windows to quit.
2286 winData._shouldRestore = true;
2289 // Store the window's close date to figure out when each individual tab
2290 // was closed. This timestamp should allow re-arranging data based on how
2291 // recently something was closed.
2292 winData.closedAt = Date.now();
2294 // we don't want to save the busy state
2295 delete winData.busy;
2297 // When closing windows one after the other until Firefox quits, we
2298 // will move those closed in series back to the "open windows" bucket
2299 // before writing to disk. If however there is only a single window
2300 // with tabs we deem not worth saving then we might end up with a
2301 // random closed or even a pop-up window re-opened. To prevent that
2302 // we explicitly allow saving an "empty" window state.
2303 let isLastWindow = this.isLastRestorableWindow();
2305 // clear this window from the list, since it has definitely been closed.
2306 delete this._windows[aWindow.__SSi];
2308 // This window has the potential to be saved in the _closedWindows
2309 // array (maybeSaveClosedWindows gets the final call on that).
2310 this._saveableClosedWindowData.add(winData);
2312 // Now we have to figure out if this window is worth saving in the _closedWindows
2315 // We're about to flush the tabs from this window, but it's possible that we
2316 // might never hear back from the content process(es) in time before the user
2317 // chooses to restore the closed window. So we do the following:
2319 // 1) Use the tab state cache to determine synchronously if the window is
2320 // worth stashing in _closedWindows.
2321 // 2) Flush the window.
2322 // 3) When the flush is complete, revisit our decision to store the window
2323 // in _closedWindows, and add/remove as necessary.
2324 if (!winData.isPrivate) {
2325 this.maybeSaveClosedWindow(winData, isLastWindow);
2328 completionPromise = lazy.TabStateFlusher.flushWindow(aWindow).then(() => {
2329 // At this point, aWindow is closed! You should probably not try to
2330 // access any DOM elements from aWindow within this callback unless
2331 // you're holding on to them in the closure.
2333 WINDOW_FLUSHING_PROMISES.delete(aWindow);
2335 for (let browser of browsers) {
2336 if (this._tabClosingByWindowMap.has(browser.permanentKey)) {
2337 let tabData = this._tabClosingByWindowMap.get(browser.permanentKey);
2338 lazy.TabState.copyFromCache(browser.permanentKey, tabData);
2339 this._tabClosingByWindowMap.delete(browser.permanentKey);
2343 // Save non-private windows if they have at
2344 // least one saveable tab or are the last window.
2345 if (!winData.isPrivate) {
2346 this.maybeSaveClosedWindow(winData, isLastWindow);
2348 if (!isLastWindow && winData.closedId > -1) {
2349 this._addClosedAction(
2350 this._LAST_ACTION_CLOSED_WINDOW,
2356 // Update the tabs data now that we've got the most
2357 // recent information.
2358 this.cleanUpWindow(aWindow, winData, browsers);
2360 // save the state without this window to disk
2361 this.saveStateDelayed();
2364 // Here we might override a flush already in flight, but that's fine
2365 // because `completionPromise` will always resolve after the old flush
2367 WINDOW_FLUSHING_PROMISES.set(aWindow, completionPromise);
2369 this.cleanUpWindow(aWindow, winData, browsers);
2372 for (let i = 0; i < tabbrowser.tabs.length; i++) {
2373 this.onTabRemove(aWindow, tabbrowser.tabs[i], true);
2376 return completionPromise;
2380 * Clean up the message listeners on a window that has finally
2381 * gone away. Call this once you're sure you don't want to hear
2382 * from any of this windows tabs from here forward.
2385 * The browser window we're cleaning up.
2387 * The data for the window that we should hold in the
2388 * DyingWindowCache in case anybody is still holding a
2391 cleanUpWindow(aWindow, winData, browsers) {
2392 // Any leftover TabStateFlusher Promises need to be resolved now,
2393 // since we're about to remove the message listeners.
2394 for (let browser of browsers) {
2395 lazy.TabStateFlusher.resolveAll(browser);
2398 // Cache the window state until it is completely gone.
2399 DyingWindowCache.set(aWindow, winData);
2401 this._saveableClosedWindowData.delete(winData);
2402 delete aWindow.__SSi;
2406 * Decides whether or not a closed window should be put into the
2407 * _closedWindows Object. This might be called multiple times per
2408 * window, and will do the right thing of moving the window data
2409 * in or out of _closedWindows if the winData indicates that our
2410 * need for saving it has changed.
2413 * The data for the closed window that we might save.
2414 * @param isLastWindow
2415 * Whether or not the window being closed is the last
2416 * browser window. Callers of this function should pass
2417 * in the value of SessionStoreInternal.atLastWindow for
2418 * this argument, and pass in the same value if they happen
2419 * to call this method again asynchronously (for example, after
2422 maybeSaveClosedWindow(winData, isLastWindow) {
2423 // Make sure SessionStore is still running, and make sure that we
2424 // haven't chosen to forget this window.
2426 lazy.RunState.isRunning &&
2427 this._saveableClosedWindowData.has(winData)
2429 // Determine whether the window has any tabs worth saving.
2430 // Note: We currently ignore the possibility of useful _closedTabs here.
2431 // A window with 0 worth-keeping open tabs will not have its state saved, and
2432 // any _closedTabs will be lost.
2433 let hasSaveableTabs = winData.tabs.some(this._shouldSaveTabState);
2435 // Note that we might already have this window stored in
2436 // _closedWindows from a previous call to this function.
2437 let winIndex = this._closedWindows.indexOf(winData);
2438 let alreadyStored = winIndex != -1;
2439 // If sidebar command is truthy, i.e. sidebar is open, store sidebar settings
2440 let shouldStore = hasSaveableTabs || isLastWindow;
2442 if (shouldStore && !alreadyStored) {
2443 let index = this._closedWindows.findIndex(win => {
2444 return win.closedAt < winData.closedAt;
2447 // If we found no window closed before our
2448 // window then just append it to the list.
2450 index = this._closedWindows.length;
2453 // About to save the closed window, add a unique ID.
2454 winData.closedId = this._nextClosedId++;
2456 // Insert winData at the right position.
2457 this._closedWindows.splice(index, 0, winData);
2458 this._capClosedWindows();
2459 this._saveOpenTabGroupsOnClose(winData);
2460 this._closedObjectsChanged = true;
2461 // The first time we close a window, ensure it can be restored from the
2464 AppConstants.platform == "macosx" &&
2465 this._closedWindows.length == 1
2467 // Fake a popupshowing event so shortcuts work:
2468 let window = Services.appShell.hiddenDOMWindow;
2469 let historyMenu = window.document.getElementById("history-menu");
2470 let evt = new window.CustomEvent("popupshowing", { bubbles: true });
2471 historyMenu.menupopup.dispatchEvent(evt);
2473 } else if (!shouldStore) {
2475 winData._closedTabs.length &&
2476 this._closedTabsFromAllWindowsEnabled
2478 // we are going to lose closed tabs, so any observers should be notified
2479 this._closedObjectsChanged = true;
2481 if (alreadyStored) {
2482 this._removeClosedWindow(winIndex);
2486 `Discarding window with 0 saveable tabs and ${winData._closedTabs.length} closed tabs`
2493 * If there are any open tab groups in this closing window, move those
2494 * tab groups to the list of saved tab groups so that the user doesn't
2497 * The normal API for saving a tab group is `this.addSavedTabGroup`.
2498 * `this.addSavedTabGroup` relies on a MozTabbrowserTabGroup DOM element
2499 * and relies on passing the tab group's MozTabbrowserTab DOM elements to
2500 * `this.maybeSaveClosedTab`. Since this method might be dealing with a closed
2501 * window that has no DOM, this method has a separate but similar
2502 * implementation to `this.addSavedTabGroup` and `this.maybeSaveClosedTab`.
2504 * @param {WindowStateData} closedWinData
2507 _saveOpenTabGroupsOnClose(closedWinData) {
2508 /** @type Map<string, SavedTabGroupStateData> */
2509 let newlySavedTabGroups = new Map();
2510 // Convert any open tab groups into saved tab groups in place
2511 closedWinData.groups = closedWinData.groups.map(tabGroupState =>
2512 lazy.TabGroupState.savedInClosedWindow(
2514 closedWinData.closedId
2517 for (let tabGroupState of closedWinData.groups) {
2518 newlySavedTabGroups.set(tabGroupState.id, tabGroupState);
2520 for (let tIndex = 0; tIndex < closedWinData.tabs.length; tIndex++) {
2521 let tabState = closedWinData.tabs[tIndex];
2522 if (!tabState.groupId) {
2525 if (!newlySavedTabGroups.has(tabState.groupId)) {
2529 if (this._shouldSaveTabState(tabState)) {
2530 // Ensure the index is in bounds.
2531 let activeIndex = tabState.index;
2532 activeIndex = Math.min(activeIndex, tabState.entries.length - 1);
2533 activeIndex = Math.max(activeIndex, 0);
2534 if (!(activeIndex in tabState.entries)) {
2538 tabState.entries[activeIndex].title ||
2539 tabState.entries[activeIndex].url;
2543 image: tabState.image,
2545 closedAt: Date.now(),
2546 closedId: this._nextClosedId++,
2548 newlySavedTabGroups.get(tabState.groupId).tabs.push(tabData);
2552 // Add saved tab group references to saved tab group state.
2553 for (let tabGroupToSave of newlySavedTabGroups.values()) {
2554 this._recordSavedTabGroupState(tabGroupToSave);
2559 * On quit application granted
2561 onQuitApplicationGranted: function ssi_onQuitApplicationGranted(
2562 syncShutdown = false
2564 // Collect an initial snapshot of window data before we do the flush.
2566 for (let window of this._orderedBrowserWindows) {
2567 this._collectWindowData(window);
2568 this._windows[window.__SSi].zIndex = ++index;
2571 // Now add an AsyncShutdown blocker that'll spin the event loop
2572 // until the windows have all been flushed.
2574 // This progress object will track the state of async window flushing
2575 // and will help us debug things that go wrong with our AsyncShutdown
2577 let progress = { total: -1, current: -1 };
2579 // We're going down! Switch state so that we treat closing windows and
2581 lazy.RunState.setQuitting();
2583 if (!syncShutdown) {
2584 // We've got some time to shut down, so let's do this properly that there
2585 // will be a complete session available upon next startup.
2586 // To prevent a blocker from taking longer than the DELAY_CRASH_MS limit
2587 // (which will cause a crash) of AsyncShutdown whilst flushing all windows,
2588 // we resolve the Promise blocker once:
2589 // 1. the flush duration exceeds 10 seconds before DELAY_CRASH_MS, or
2590 // 2. 'oop-frameloader-crashed', or
2591 // 3. 'ipc:content-shutdown' is observed.
2592 lazy.AsyncShutdown.quitApplicationGranted.addBlocker(
2593 "SessionStore: flushing all windows",
2595 // Set up the list of promises that will signal a complete sessionstore
2596 // shutdown: either all data is saved, or we crashed or the message IPC
2597 // channel went away in the meantime.
2598 let promises = [this.flushAllWindowsAsync(progress)];
2600 const observeTopic = topic => {
2601 let deferred = Promise.withResolvers();
2602 const observer = subject => {
2603 // Skip abort on ipc:content-shutdown if not abnormal/crashed
2604 subject.QueryInterface(Ci.nsIPropertyBag2);
2606 !(topic == "ipc:content-shutdown" && !subject.get("abnormal"))
2611 const cleanup = () => {
2613 Services.obs.removeObserver(observer, topic);
2616 "SessionStore: exception whilst flushing all windows: ",
2621 Services.obs.addObserver(observer, topic);
2622 deferred.promise.then(cleanup, cleanup);
2626 // Build a list of deferred executions that require cleanup once the
2627 // Promise race is won.
2628 // Ensure that the timer fires earlier than the AsyncShutdown crash timer.
2629 let waitTimeMaxMs = Math.max(
2631 lazy.AsyncShutdown.DELAY_CRASH_MS - 10000
2634 this.looseTimer(waitTimeMaxMs),
2636 // FIXME: We should not be aborting *all* flushes when a single
2637 // content process crashes here.
2638 observeTopic("oop-frameloader-crashed"),
2639 observeTopic("ipc:content-shutdown"),
2641 // Add these monitors to the list of Promises to start the race.
2642 promises.push(...defers.map(deferred => deferred.promise));
2644 return Promise.race(promises).then(() => {
2645 // When a Promise won the race, make sure we clean up the running
2647 defers.forEach(deferred => deferred.reject());
2653 // We have to shut down NOW, which means we only get to save whatever
2654 // we already had cached.
2659 * An async Task that iterates all open browser windows and flushes
2660 * any outstanding messages from their tabs. This will also close
2661 * all of the currently open windows while we wait for the flushes
2664 * @param progress (Object)
2665 * Optional progress object that will be updated as async
2666 * window flushing progresses. flushAllWindowsSync will
2667 * write to the following properties:
2670 * The total number of windows to be flushed.
2672 * The current window that we're waiting for a flush on.
2676 async flushAllWindowsAsync(progress = {}) {
2677 let windowPromises = new Map(WINDOW_FLUSHING_PROMISES);
2678 WINDOW_FLUSHING_PROMISES.clear();
2680 // We collect flush promises and close each window immediately so that
2681 // the user can't start changing any window state while we're waiting
2682 // for the flushes to finish.
2683 for (let window of this._browserWindows) {
2684 windowPromises.set(window, lazy.TabStateFlusher.flushWindow(window));
2686 // We have to wait for these messages to come up from
2687 // each window and each browser. In the meantime, hide
2688 // the windows to improve perceived shutdown speed.
2689 let baseWin = window.docShell.treeOwner.QueryInterface(Ci.nsIBaseWindow);
2690 baseWin.visibility = false;
2693 progress.total = windowPromises.size;
2694 progress.current = 0;
2696 // We'll iterate through the Promise array, yielding each one, so as to
2697 // provide useful progress information to AsyncShutdown.
2698 for (let [win, promise] of windowPromises) {
2701 // We may have already stopped tracking this window in onClose, which is
2702 // fine as we would've collected window data there as well.
2703 if (win.__SSi && this._windows[win.__SSi]) {
2704 this._collectWindowData(win);
2710 // We must cache this because _getTopWindow will always
2711 // return null by the time quit-application occurs.
2712 var activeWindow = this._getTopWindow();
2714 this.activeWindowSSiCache = activeWindow.__SSi || "";
2716 DirtyWindows.clear();
2720 * On last browser window close
2722 onLastWindowCloseGranted: function ssi_onLastWindowCloseGranted() {
2723 // last browser window is quitting.
2724 // remember to restore the last window when another browser window is opened
2725 // do not account for pref(resume_session_once) at this point, as it might be
2726 // set by another observer getting this notice after us
2727 this._restoreLastWindow = true;
2731 * On quitting application
2733 * String type of quitting
2735 onQuitApplication: function ssi_onQuitApplication(aData) {
2736 if (aData == "restart" || aData == "os-restart") {
2737 if (!PrivateBrowsingUtils.permanentPrivateBrowsing) {
2739 aData == "os-restart" &&
2740 !this._prefBranch.getBoolPref("sessionstore.resume_session_once")
2742 this._prefBranch.setBoolPref(
2743 "sessionstore.resuming_after_os_restart",
2747 this._prefBranch.setBoolPref("sessionstore.resume_session_once", true);
2750 // The browser:purge-session-history notification fires after the
2751 // quit-application notification so unregister the
2752 // browser:purge-session-history notification to prevent clearing
2753 // session data on disk on a restart. It is also unnecessary to
2754 // perform any other sanitization processing on a restart as the
2755 // browser is about to exit anyway.
2756 Services.obs.removeObserver(this, "browser:purge-session-history");
2759 if (aData != "restart") {
2760 // Throw away the previous session on shutdown without notification
2761 LastSession.clear(true);
2768 * Clear session store data for a given private browsing window.
2769 * @param {ChromeWindow} win - Open private browsing window to clear data for.
2771 purgeDataForPrivateWindow(win) {
2772 // No need to clear data if already shutting down.
2773 if (lazy.RunState.isQuitting) {
2777 // Check if we have data for the given window.
2778 let windowData = this._windows[win.__SSi];
2783 // Clear closed tab data.
2784 if (windowData._closedTabs.length) {
2785 // Remove all of the closed tabs data.
2786 // This also clears out the permenentKey-mapped data for pending state updates
2787 // and removes the tabs from from the _lastClosedActions list
2788 while (windowData._closedTabs.length) {
2789 this.removeClosedTabData(windowData, windowData._closedTabs, 0);
2791 // Reset the closed tab list.
2792 windowData._closedTabs = [];
2793 windowData._lastClosedTabGroupCount = -1;
2794 windowData.lastClosedTabGroupId = null;
2795 this._closedObjectsChanged = true;
2798 // Clear closed tab groups
2799 if (windowData.closedGroups.length) {
2800 for (let closedGroup of windowData.closedGroups) {
2801 while (closedGroup.tabs.length) {
2802 this.removeClosedTabData(windowData, closedGroup.tabs, 0);
2805 windowData.closedGroups = [];
2806 this._closedObjectsChanged = true;
2811 * On purge of session history
2813 onPurgeSessionHistory: function ssi_onPurgeSessionHistory() {
2814 lazy.SessionFile.wipe();
2815 // If the browser is shutting down, simply return after clearing the
2816 // session data on disk as this notification fires after the
2817 // quit-application notification so the browser is about to exit.
2818 if (lazy.RunState.isQuitting) {
2821 LastSession.clear();
2823 let openWindows = {};
2824 // Collect open windows.
2825 for (let window of this._browserWindows) {
2826 openWindows[window.__SSi] = true;
2829 // also clear all data about closed tabs and windows
2830 for (let ix in this._windows) {
2831 if (ix in openWindows) {
2832 if (this._windows[ix]._closedTabs.length) {
2833 this._windows[ix]._closedTabs = [];
2834 this._closedObjectsChanged = true;
2836 if (this._windows[ix].closedGroups.length) {
2837 this._windows[ix].closedGroups = [];
2838 this._closedObjectsChanged = true;
2841 delete this._windows[ix];
2844 // also clear all data about closed windows
2845 if (this._closedWindows.length) {
2846 this._closedWindows = [];
2847 this._closedObjectsChanged = true;
2849 // give the tabbrowsers a chance to clear their histories first
2850 var win = this._getTopWindow();
2852 win.setTimeout(() => lazy.SessionSaver.run(), 0);
2853 } else if (lazy.RunState.isRunning) {
2854 lazy.SessionSaver.run();
2857 this._clearRestoringWindows();
2858 this._saveableClosedWindowData = new WeakSet();
2859 this._lastClosedActions = [];
2863 * On purge of domain data
2864 * @param {string} aDomain
2865 * The domain we want to purge data for
2867 onPurgeDomainData: function ssi_onPurgeDomainData(aDomain) {
2868 // does a session history entry contain a url for the given domain?
2869 function containsDomain(aEntry) {
2872 host = Services.io.newURI(aEntry.url).host;
2874 // The given URL probably doesn't have a host.
2876 if (host && Services.eTLD.hasRootDomain(host, aDomain)) {
2879 return aEntry.children && aEntry.children.some(containsDomain, this);
2881 // remove all closed tabs containing a reference to the given domain
2882 for (let ix in this._windows) {
2883 let closedTabsLists = [
2884 this._windows[ix]._closedTabs,
2885 ...this._windows[ix].closedGroups.map(g => g.tabs),
2888 for (let closedTabs of closedTabsLists) {
2889 for (let i = closedTabs.length - 1; i >= 0; i--) {
2890 if (closedTabs[i].state.entries.some(containsDomain, this)) {
2891 closedTabs.splice(i, 1);
2892 this._closedObjectsChanged = true;
2897 // remove all open & closed tabs containing a reference to the given
2898 // domain in closed windows
2899 for (let ix = this._closedWindows.length - 1; ix >= 0; ix--) {
2900 let closedTabsLists = [
2901 this._closedWindows[ix]._closedTabs,
2902 ...this._closedWindows[ix].closedGroups.map(g => g.tabs),
2904 let openTabs = this._closedWindows[ix].tabs;
2905 let openTabCount = openTabs.length;
2907 for (let closedTabs of closedTabsLists) {
2908 for (let i = closedTabs.length - 1; i >= 0; i--) {
2909 if (closedTabs[i].state.entries.some(containsDomain, this)) {
2910 closedTabs.splice(i, 1);
2914 for (let j = openTabs.length - 1; j >= 0; j--) {
2915 if (openTabs[j].entries.some(containsDomain, this)) {
2916 openTabs.splice(j, 1);
2917 if (this._closedWindows[ix].selected > j) {
2918 this._closedWindows[ix].selected--;
2922 if (!openTabs.length) {
2923 this._closedWindows.splice(ix, 1);
2924 } else if (openTabs.length != openTabCount) {
2925 // Adjust the window's title if we removed an open tab
2926 let selectedTab = openTabs[this._closedWindows[ix].selected - 1];
2927 // some duplication from restoreHistory - make sure we get the correct title
2928 let activeIndex = (selectedTab.index || selectedTab.entries.length) - 1;
2929 if (activeIndex >= selectedTab.entries.length) {
2930 activeIndex = selectedTab.entries.length - 1;
2932 this._closedWindows[ix].title = selectedTab.entries[activeIndex].title;
2936 if (lazy.RunState.isRunning) {
2937 lazy.SessionSaver.run();
2940 this._clearRestoringWindows();
2944 * On preference change
2946 * String preference changed
2948 onPrefChange: function ssi_onPrefChange(aData) {
2950 // if the user decreases the max number of closed tabs they want
2951 // preserved update our internal states to match that max
2952 case "sessionstore.max_tabs_undo":
2953 this._max_tabs_undo = this._prefBranch.getIntPref(
2954 "sessionstore.max_tabs_undo"
2956 for (let ix in this._windows) {
2957 if (this._windows[ix]._closedTabs.length > this._max_tabs_undo) {
2958 this._windows[ix]._closedTabs.splice(
2959 this._max_tabs_undo,
2960 this._windows[ix]._closedTabs.length
2962 this._closedObjectsChanged = true;
2966 case "sessionstore.max_windows_undo":
2967 this._max_windows_undo = this._prefBranch.getIntPref(
2968 "sessionstore.max_windows_undo"
2970 this._capClosedWindows();
2972 case "sessionstore.restore_on_demand":
2973 this._restore_on_demand = this._prefBranch.getBoolPref(
2974 "sessionstore.restore_on_demand"
2977 case "sessionstore.closedTabsFromAllWindows":
2978 this._closedTabsFromAllWindowsEnabled = this._prefBranch.getBoolPref(
2979 "sessionstore.closedTabsFromAllWindows"
2981 this._closedObjectsChanged = true;
2983 case "sessionstore.closedTabsFromClosedWindows":
2984 this._closedTabsFromClosedWindowsEnabled = this._prefBranch.getBoolPref(
2985 "sessionstore.closedTabsFromClosedWindows"
2987 this._closedObjectsChanged = true;
2993 * save state when new tab is added
2997 onTabAdd: function ssi_onTabAdd(aWindow) {
2998 this.saveStateDelayed(aWindow);
3002 * set up listeners for a new tab
3008 onTabBrowserInserted: function ssi_onTabBrowserInserted(aWindow, aTab) {
3009 let browser = aTab.linkedBrowser;
3010 browser.addEventListener("SwapDocShells", this);
3011 browser.addEventListener("oop-browser-crashed", this);
3012 browser.addEventListener("oop-browser-buildid-mismatch", this);
3014 if (browser.frameLoader) {
3015 this._lastKnownFrameLoader.set(browser.permanentKey, browser.frameLoader);
3018 // Only restore if browser has been lazy.
3020 TAB_LAZY_STATES.has(aTab) &&
3021 !TAB_STATE_FOR_BROWSER.has(browser) &&
3022 lazy.TabStateCache.get(browser.permanentKey)
3024 let tabState = lazy.TabState.clone(aTab, TAB_CUSTOM_VALUES.get(aTab));
3025 this.restoreTab(aTab, tabState);
3028 // The browser has been inserted now, so lazy data is no longer relevant.
3029 TAB_LAZY_STATES.delete(aTab);
3033 * remove listeners for a tab
3038 * @param aNoNotification
3039 * bool Do not save state if we're updating an existing tab
3041 onTabRemove: function ssi_onTabRemove(aWindow, aTab, aNoNotification) {
3042 this.cleanUpRemovedBrowser(aTab);
3044 if (!aNoNotification) {
3045 this.saveStateDelayed(aWindow);
3050 * When a tab closes, collect its properties
3051 * @param {Window} aWindow
3053 * @param {MozTabbrowserTab} aTab
3056 onTabClose: function ssi_onTabClose(aWindow, aTab) {
3057 // don't update our internal state if we don't have to
3058 if (this._max_tabs_undo == 0) {
3062 // Get the latest data for this tab (generally, from the cache)
3063 let tabState = lazy.TabState.collect(aTab, TAB_CUSTOM_VALUES.get(aTab));
3065 // Store closed-tab data for undo.
3066 this.maybeSaveClosedTab(aWindow, aTab, tabState);
3069 onTabGroupRemoveRequested: function ssi_onTabGroupRemoveRequested(
3073 // don't update our internal state if we don't have to
3074 if (this._max_tabs_undo == 0) {
3078 if (this.getSavedTabGroup(tabGroup.id)) {
3079 // If a tab group is being removed from the tab strip but it's already
3080 // saved, then this is a "save and close" action; the saved tab group
3081 // should be stored in global session state rather than in this window's
3082 // closed tab groups.
3086 let closedGroups = this._windows[win.__SSi].closedGroups;
3087 let tabGroupState = lazy.TabGroupState.closed(tabGroup, win.__SSi);
3088 tabGroupState.tabs = this._collectClosedTabsForTabGroup(tabGroup.tabs, win);
3090 // TODO(jswinarton) it's unclear if updating lastClosedTabGroupCount is
3091 // necessary when restoring tab groups — it largely depends on how we
3092 // decide to do the restore.
3093 // To address in bug1915174
3094 this._windows[win.__SSi]._lastClosedTabGroupCount =
3095 tabGroupState.tabs.length;
3096 closedGroups.unshift(tabGroupState);
3097 this._closedObjectsChanged = true;
3101 * Collect closed tab states for a tab group that is about to be
3102 * saved and/or closed.
3104 * The `TabGroupState` module is generally responsible for collecting
3105 * tab group state data, but the session store has additional requirements
3106 * for closed tabs that are currently only implemented in
3107 * `SessionStoreInternal.maybeSaveClosedTab`. This method converts the tabs
3108 * in a tab group into the closed tab data schema format required for
3109 * closed or saved groups.
3111 * @param {MozTabbrowserTab[]} tabs
3112 * @param {Window} win
3113 * @returns {ClosedTabStateData[]}
3115 _collectClosedTabsForTabGroup(tabs, win) {
3116 let closedTabs = [];
3117 tabs.forEach(tab => {
3118 let tabState = lazy.TabState.collect(tab, TAB_CUSTOM_VALUES.get(tab));
3119 this.maybeSaveClosedTab(win, tab, tabState, {
3120 closedTabsArray: closedTabs,
3121 closedInTabGroup: true,
3128 * Flush and copy tab state when moving a tab to a new window.
3129 * @param aFromBrowser
3130 * Browser reference.
3132 * Browser reference.
3134 onMoveToNewWindow(aFromBrowser, aToBrowser) {
3135 lazy.TabStateFlusher.flush(aFromBrowser).then(() => {
3136 let tabState = lazy.TabStateCache.get(aFromBrowser.permanentKey);
3139 "Unexpected undefined tabState for onMoveToNewWindow aFromBrowser"
3142 lazy.TabStateCache.update(aToBrowser.permanentKey, tabState);
3147 * Save a closed tab if needed.
3149 * @param {Window} aWindow
3151 * @param {MozTabbrowserTab} aTab
3153 * @param {TabStateData} tabState
3155 * @param {object} [options]
3156 * @param {TabStateData[]} [options.closedTabsArray]
3157 * The array of closed tabs to save to. This could be a
3158 * window's _closedTabs array or the tab list of a
3160 * @param {boolean} [options.closedInTabGroup]
3161 * If this tab was closed due to the closing of a tab group.
3167 { closedTabsArray, closedInTabGroup = false } = {}
3169 // Don't save private tabs
3170 let isPrivateWindow = PrivateBrowsingUtils.isWindowPrivate(aWindow);
3171 if (!isPrivateWindow && tabState.isPrivate) {
3174 if (aTab == aWindow.FirefoxViewHandler.tab) {
3178 let permanentKey = aTab.linkedBrowser.permanentKey;
3184 image: aWindow.gBrowser.getIcon(aTab),
3186 closedAt: Date.now(),
3187 closedInGroup: aTab._closedInMultiselection,
3188 closedInTabGroupId: closedInTabGroup ? aTab.group.id : null,
3189 sourceWindowId: aWindow.__SSi,
3192 let winData = this._windows[aWindow.__SSi];
3193 let closedTabs = closedTabsArray || winData._closedTabs;
3195 // Determine whether the tab contains any information worth saving. Note
3196 // that there might be pending state changes queued in the child that
3197 // didn't reach the parent yet. If a tab is emptied before closing then we
3198 // might still remove it from the list of closed tabs later.
3199 if (this._shouldSaveTabState(tabState)) {
3200 // Save the tab state, for now. We might push a valid tab out
3201 // of the list but those cases should be extremely rare and
3202 // do probably never occur when using the browser normally.
3203 // (Tests or add-ons might do weird things though.)
3204 this.saveClosedTabData(winData, closedTabs, tabData);
3207 // Remember the closed tab to properly handle any last updates included in
3208 // the final "update" message sent by the frame script's unload handler.
3209 this._closingTabMap.set(permanentKey, {
3217 * Remove listeners which were added when browser was inserted and reset restoring state.
3218 * Also re-instate lazy data and basically revert tab to its lazy browser state.
3222 resetBrowserToLazyState(aTab) {
3223 let browser = aTab.linkedBrowser;
3224 // Browser is already lazy so don't do anything.
3225 if (!browser.isConnected) {
3229 this.cleanUpRemovedBrowser(aTab);
3231 aTab.setAttribute("pending", "true");
3233 this._lastKnownFrameLoader.delete(browser.permanentKey);
3234 this._crashedBrowsers.delete(browser.permanentKey);
3235 aTab.removeAttribute("crashed");
3237 let { userTypedValue = null, userTypedClear = 0 } = browser;
3238 let hasStartedLoad = browser.didStartLoadSinceLastUserTyping();
3240 let cacheState = lazy.TabStateCache.get(browser.permanentKey);
3242 // Cache the browser userTypedValue either if there is no cache state
3243 // at all (e.g. if it was already discarded before we got to cache its state)
3244 // or it may have been created but not including a userTypedValue (e.g.
3245 // for a private tab we will cache `isPrivate: true` as soon as the tab
3250 // - if there is no cache state yet (which is unfortunately required
3251 // for tabs discarded immediately after creation by extensions, see
3254 // - or the user typed value was already being loaded (otherwise the lazy
3255 // tab will not be restored with the expected url once activated again,
3256 // see Bug 1724205).
3257 let shouldUpdateCacheState =
3259 (!cacheState || (hasStartedLoad && !cacheState.userTypedValue));
3261 if (shouldUpdateCacheState) {
3262 // Discard was likely called before state can be cached. Update
3263 // the persistent tab state cache with browser information so a
3264 // restore will be successful. This information is necessary for
3265 // restoreTabContent in ContentRestore.sys.mjs to work properly.
3266 lazy.TabStateCache.update(browser.permanentKey, {
3272 TAB_LAZY_STATES.set(aTab, {
3273 url: browser.currentURI.spec,
3281 * Check if we are dealing with a crashed browser. If so, then the corresponding
3282 * crashed tab was revived by navigating to a different page. Remove the browser
3283 * from the list of crashed browsers to stop ignoring its messages.
3287 maybeExitCrashedState(aBrowser) {
3288 let uri = aBrowser.documentURI;
3289 if (uri?.spec?.startsWith("about:tabcrashed")) {
3290 this._crashedBrowsers.delete(aBrowser.permanentKey);
3295 * A debugging-only function to check if a browser is in _crashedBrowsers.
3299 isBrowserInCrashedSet(aBrowser) {
3300 if (gDebuggingEnabled) {
3301 return this._crashedBrowsers.has(aBrowser.permanentKey);
3304 "SessionStore.isBrowserInCrashedSet() should only be called in debug mode!"
3309 * When a tab is removed or suspended, remove listeners and reset restoring state.
3313 cleanUpRemovedBrowser(aTab) {
3314 let browser = aTab.linkedBrowser;
3316 browser.removeEventListener("SwapDocShells", this);
3317 browser.removeEventListener("oop-browser-crashed", this);
3318 browser.removeEventListener("oop-browser-buildid-mismatch", this);
3320 // If this tab was in the middle of restoring or still needs to be restored,
3321 // we need to reset that state. If the tab was restoring, we will attempt to
3322 // restore the next tab.
3323 let previousState = TAB_STATE_FOR_BROWSER.get(browser);
3324 if (previousState) {
3325 this._resetTabRestoringState(aTab);
3326 if (previousState == TAB_STATE_RESTORING) {
3327 this.restoreNextTab();
3333 * Insert a given |tabData| object into the list of |closedTabs|. We will
3334 * determine the right insertion point based on the .closedAt properties of
3335 * all tabs already in the list. The list will be truncated to contain a
3336 * maximum of |this._max_tabs_undo| entries.
3338 * @param winData (object)
3339 * The data of the window.
3340 * @param tabData (object)
3341 * The tabData to be inserted.
3342 * @param closedTabs (array)
3343 * The list of closed tabs for a window.
3344 * @param saveAction (boolean)
3345 * Whether or not to add an action to the closed actions stack on save.
3347 saveClosedTabData(winData, closedTabs, tabData, saveAction = true) {
3348 // Find the index of the first tab in the list
3349 // of closed tabs that was closed before our tab.
3350 let index = closedTabs.findIndex(tab => {
3351 return tab.closedAt < tabData.closedAt;
3354 // If we found no tab closed before our
3355 // tab then just append it to the list.
3357 index = closedTabs.length;
3360 // About to save the closed tab, add a unique ID.
3361 tabData.closedId = this._nextClosedId++;
3363 // Insert tabData at the right position.
3364 closedTabs.splice(index, 0, tabData);
3365 this._closedObjectsChanged = true;
3367 if (tabData.closedInGroup) {
3368 if (winData._lastClosedTabGroupCount < this._max_tabs_undo) {
3369 if (winData._lastClosedTabGroupCount < 0) {
3370 winData._lastClosedTabGroupCount = 1;
3372 winData._lastClosedTabGroupCount++;
3376 winData._lastClosedTabGroupCount = -1;
3379 winData.lastClosedTabGroupId = tabData.closedInTabGroupId || null;
3382 this._addClosedAction(this._LAST_ACTION_CLOSED_TAB, tabData.closedId);
3385 // Truncate the list of closed tabs, if needed.
3386 if (closedTabs.length > this._max_tabs_undo) {
3387 closedTabs.splice(this._max_tabs_undo, closedTabs.length);
3392 * Remove the closed tab data at |index| from the list of |closedTabs|. If
3393 * the tab's final message is still pending we will simply discard it when
3394 * it arrives so that the tab doesn't reappear in the list.
3396 * @param winData (object)
3397 * The data of the window.
3398 * @param index (uint)
3399 * The index of the tab to remove.
3400 * @param closedTabs (array)
3401 * The list of closed tabs for a window.
3403 removeClosedTabData(winData, closedTabs, index) {
3404 // Remove the given index from the list.
3405 let [closedTab] = closedTabs.splice(index, 1);
3406 this._closedObjectsChanged = true;
3408 // If the tab is part of the last closed multiselected tab set,
3409 // we need to deduct the tab from the count.
3410 if (index < winData._lastClosedTabGroupCount) {
3411 winData._lastClosedTabGroupCount--;
3414 // If the closed tab's state still has a .permanentKey property then we
3415 // haven't seen its final update message yet. Remove it from the map of
3416 // closed tabs so that we will simply discard its last messages and will
3417 // not add it back to the list of closed tabs again.
3418 if (closedTab.permanentKey) {
3419 this._closingTabMap.delete(closedTab.permanentKey);
3420 this._tabClosingByWindowMap.delete(closedTab.permanentKey);
3421 delete closedTab.permanentKey;
3424 this._removeClosedAction(this._LAST_ACTION_CLOSED_TAB, closedTab.closedId);
3430 * When a tab is selected, save session data
3434 onTabSelect: function ssi_onTabSelect(aWindow) {
3435 if (lazy.RunState.isRunning) {
3436 this._windows[aWindow.__SSi].selected =
3437 aWindow.gBrowser.tabContainer.selectedIndex;
3439 let tab = aWindow.gBrowser.selectedTab;
3440 let browser = tab.linkedBrowser;
3442 if (TAB_STATE_FOR_BROWSER.get(browser) == TAB_STATE_NEEDS_RESTORE) {
3443 // If BROWSER_STATE is still available for the browser and it is
3444 // If __SS_restoreState is still on the browser and it is
3445 // TAB_STATE_NEEDS_RESTORE, then we haven't restored this tab yet.
3447 // It's possible that this tab was recently revived, and that
3448 // we've deferred showing the tab crashed page for it (if the
3449 // tab crashed in the background). If so, we need to re-enter
3450 // the crashed state, since we'll be showing the tab crashed
3452 if (lazy.TabCrashHandler.willShowCrashedTab(browser)) {
3453 this.enterCrashedState(browser);
3455 this.restoreTabContent(tab);
3461 onTabShow: function ssi_onTabShow(aWindow, aTab) {
3462 // If the tab hasn't been restored yet, move it into the right bucket
3464 TAB_STATE_FOR_BROWSER.get(aTab.linkedBrowser) == TAB_STATE_NEEDS_RESTORE
3466 TabRestoreQueue.hiddenToVisible(aTab);
3468 // let's kick off tab restoration again to ensure this tab gets restored
3469 // with "restore_hidden_tabs" == false (now that it has become visible)
3470 this.restoreNextTab();
3473 // Default delay of 2 seconds gives enough time to catch multiple TabShow
3474 // events. This used to be due to changing groups in 'tab groups'. We
3475 // might be able to get rid of this now?
3476 this.saveStateDelayed(aWindow);
3479 onTabHide: function ssi_onTabHide(aWindow, aTab) {
3480 // If the tab hasn't been restored yet, move it into the right bucket
3482 TAB_STATE_FOR_BROWSER.get(aTab.linkedBrowser) == TAB_STATE_NEEDS_RESTORE
3484 TabRestoreQueue.visibleToHidden(aTab);
3487 // Default delay of 2 seconds gives enough time to catch multiple TabHide
3488 // events. This used to be due to changing groups in 'tab groups'. We
3489 // might be able to get rid of this now?
3490 this.saveStateDelayed(aWindow);
3494 * Handler for the event that is fired when a <xul:browser> crashes.
3497 * The window that the crashed browser belongs to.
3499 * The <xul:browser> that is now in the crashed state.
3501 onBrowserCrashed(aBrowser) {
3502 this.enterCrashedState(aBrowser);
3503 // The browser crashed so we might never receive flush responses.
3504 // Resolve all pending flush requests for the crashed browser.
3505 lazy.TabStateFlusher.resolveAll(aBrowser);
3509 * Called when a browser is showing or is about to show the tab
3510 * crashed page. This method causes SessionStore to ignore the
3511 * tab until it's restored.
3514 * The <xul:browser> that is about to show the crashed page.
3516 enterCrashedState(browser) {
3517 this._crashedBrowsers.add(browser.permanentKey);
3519 let win = browser.ownerGlobal;
3521 // If we hadn't yet restored, or were still in the midst of
3522 // restoring this browser at the time of the crash, we need
3523 // to reset its state so that we can try to restore it again
3524 // when the user revives the tab from the crash.
3525 if (TAB_STATE_FOR_BROWSER.has(browser)) {
3526 let tab = win.gBrowser.getTabForBrowser(browser);
3528 this._resetLocalTabRestoringState(tab);
3533 // Clean up data that has been closed a long time ago.
3534 // Do not reschedule a save. This will wait for the next regular
3537 // Remove old closed windows
3538 this._cleanupOldData([this._closedWindows]);
3540 // Remove closed tabs of closed windows
3541 this._cleanupOldData(
3542 this._closedWindows.map(winData => winData._closedTabs)
3545 // Remove closed groups of closed windows
3546 this._cleanupOldData(
3547 this._closedWindows.map(winData => winData.closedGroups)
3550 // Remove closed tabs of open windows
3551 this._cleanupOldData(
3552 Object.keys(this._windows).map(key => this._windows[key]._closedTabs)
3555 // Remove closed groups of open windows
3556 this._cleanupOldData(
3557 Object.keys(this._windows).map(key => this._windows[key].closedGroups)
3560 this._notifyOfClosedObjectsChange();
3563 // Remove "old" data from an array
3564 _cleanupOldData(targets) {
3565 const TIME_TO_LIVE = this._prefBranch.getIntPref(
3566 "sessionstore.cleanup.forget_closed_after"
3568 const now = Date.now();
3570 for (let array of targets) {
3571 for (let i = array.length - 1; i >= 0; --i) {
3572 let data = array[i];
3573 // Make sure that we have a timestamp to tell us when the target
3574 // has been closed. If we don't have a timestamp, default to a
3575 // safe timestamp: just now.
3576 data.closedAt = data.closedAt || now;
3577 if (now - data.closedAt > TIME_TO_LIVE) {
3579 this._closedObjectsChanged = true;
3585 /* ........ nsISessionStore API .............. */
3587 getBrowserState: function ssi_getBrowserState() {
3588 let state = this.getCurrentState();
3590 // Don't include the last session state in getBrowserState().
3591 delete state.lastSessionState;
3593 // Don't include any deferred initial state.
3594 delete state.deferredInitialState;
3596 return JSON.stringify(state);
3599 setBrowserState: function ssi_setBrowserState(aState) {
3600 this._handleClosedWindows();
3603 var state = JSON.parse(aState);
3605 /* invalid state object - don't restore anything */
3608 throw Components.Exception(
3609 "Invalid state string: not JSON",
3610 Cr.NS_ERROR_INVALID_ARG
3613 if (!state.windows) {
3614 throw Components.Exception("No windows", Cr.NS_ERROR_INVALID_ARG);
3617 this._browserSetState = true;
3619 // Make sure the priority queue is emptied out
3620 this._resetRestoringState();
3622 var window = this._getTopWindow();
3624 this._restoreCount = 1;
3625 this._openWindowWithState(state);
3629 // close all other browser windows
3630 for (let otherWin of this._browserWindows) {
3631 if (otherWin != window) {
3633 this.onClose(otherWin);
3637 // make sure closed window data isn't kept
3638 if (this._closedWindows.length) {
3639 this._closedWindows = [];
3640 this._closedObjectsChanged = true;
3643 // determine how many windows are meant to be restored
3644 this._restoreCount = state.windows ? state.windows.length : 0;
3646 // global data must be restored before restoreWindow is called so that
3647 // it happens before observers are notified
3648 this._globalState.setFromState(state);
3650 // Restore session cookies.
3651 lazy.SessionCookies.restore(state.cookies || []);
3653 // restore to the given state
3654 this.restoreWindows(window, state, { overwriteTabs: true });
3656 // Notify of changes to closed objects.
3657 this._notifyOfClosedObjectsChange();
3661 * @param {Window} aWindow
3663 * @returns {{windows: WindowStateData[]}}
3665 getWindowState: function ssi_getWindowState(aWindow) {
3666 if ("__SSi" in aWindow) {
3667 return Cu.cloneInto(this._getWindowState(aWindow), {});
3670 if (DyingWindowCache.has(aWindow)) {
3671 let data = DyingWindowCache.get(aWindow);
3672 return Cu.cloneInto({ windows: [data] }, {});
3675 throw Components.Exception(
3676 "Window is not tracked",
3677 Cr.NS_ERROR_INVALID_ARG
3681 setWindowState: function ssi_setWindowState(aWindow, aState, aOverwrite) {
3682 if (!aWindow.__SSi) {
3683 throw Components.Exception(
3684 "Window is not tracked",
3685 Cr.NS_ERROR_INVALID_ARG
3689 this.restoreWindows(aWindow, aState, { overwriteTabs: aOverwrite });
3691 // Notify of changes to closed objects.
3692 this._notifyOfClosedObjectsChange();
3695 getTabState: function ssi_getTabState(aTab) {
3696 if (!aTab || !aTab.ownerGlobal) {
3697 throw Components.Exception("Need a valid tab", Cr.NS_ERROR_INVALID_ARG);
3699 if (!aTab.ownerGlobal.__SSi) {
3700 throw Components.Exception(
3701 "Default view is not tracked",
3702 Cr.NS_ERROR_INVALID_ARG
3706 let tabState = lazy.TabState.collect(aTab, TAB_CUSTOM_VALUES.get(aTab));
3708 return JSON.stringify(tabState);
3711 setTabState(aTab, aState) {
3712 // Remove the tab state from the cache.
3713 // Note that we cannot simply replace the contents of the cache
3714 // as |aState| can be an incomplete state that will be completed
3715 // by |restoreTabs|.
3716 let tabState = aState;
3717 if (typeof tabState == "string") {
3718 tabState = JSON.parse(aState);
3721 throw Components.Exception(
3722 "Invalid state string: not JSON",
3723 Cr.NS_ERROR_INVALID_ARG
3726 if (typeof tabState != "object") {
3727 throw Components.Exception("Not an object", Cr.NS_ERROR_INVALID_ARG);
3729 if (!("entries" in tabState)) {
3730 throw Components.Exception(
3731 "Invalid state object: no entries",
3732 Cr.NS_ERROR_INVALID_ARG
3736 let window = aTab.ownerGlobal;
3737 if (!window || !("__SSi" in window)) {
3738 throw Components.Exception(
3739 "Window is not tracked",
3740 Cr.NS_ERROR_INVALID_ARG
3744 if (TAB_STATE_FOR_BROWSER.has(aTab.linkedBrowser)) {
3745 this._resetTabRestoringState(aTab);
3748 this._ensureNoNullsInTabDataList(
3749 window.gBrowser.tabs,
3750 this._windows[window.__SSi].tabs,
3753 this.restoreTab(aTab, tabState);
3755 // Notify of changes to closed objects.
3756 this._notifyOfClosedObjectsChange();
3759 getInternalObjectState(obj) {
3761 return this._windows[obj.__SSi];
3764 ? TAB_STATE_FOR_BROWSER.get(obj)
3765 : TAB_CUSTOM_VALUES.get(obj);
3768 getObjectTypeForClosedId(aClosedId) {
3769 // check if matches a window first
3770 if (this.getClosedWindowDataByClosedId(aClosedId)) {
3771 return this._LAST_ACTION_CLOSED_WINDOW;
3773 return this._LAST_ACTION_CLOSED_TAB;
3777 * @param {number} aClosedId
3778 * @returns {WindowStateData|undefined}
3780 getClosedWindowDataByClosedId: function ssi_getClosedWindowDataByClosedId(
3783 return this._closedWindows.find(
3784 closedData => closedData.closedId == aClosedId
3788 getWindowById: function ssi_getWindowById(aSessionStoreId) {
3790 for (let window of this._browserWindows) {
3791 if (window.__SSi === aSessionStoreId) {
3792 resultWindow = window;
3796 return resultWindow;
3799 duplicateTab: function ssi_duplicateTab(
3803 aRestoreImmediately = true,
3804 { inBackground, index } = {}
3806 if (!aTab || !aTab.ownerGlobal) {
3807 throw Components.Exception("Need a valid tab", Cr.NS_ERROR_INVALID_ARG);
3809 if (!aTab.ownerGlobal.__SSi) {
3810 throw Components.Exception(
3811 "Default view is not tracked",
3812 Cr.NS_ERROR_INVALID_ARG
3815 if (!aWindow.gBrowser) {
3816 throw Components.Exception(
3817 "Invalid window object: no gBrowser",
3818 Cr.NS_ERROR_INVALID_ARG
3822 // Create a new tab.
3823 let userContextId = aTab.getAttribute("usercontextid") || "";
3828 ...(aTab == aWindow.gBrowser.selectedTab
3829 ? { relatedToCurrent: true, ownerTab: aTab }
3832 preferredRemoteType: aTab.linkedBrowser.remoteType,
3834 let newTab = aWindow.gBrowser.addTrustedTab(null, tabOptions);
3836 // Start the throbber to pretend we're doing something while actually
3837 // waiting for data from the frame script. This throbber is disabled
3838 // if the URI is a local about: URI.
3839 let uriObj = aTab.linkedBrowser.currentURI;
3840 if (!uriObj || (uriObj && !uriObj.schemeIs("about"))) {
3841 newTab.setAttribute("busy", "true");
3844 // Hack to ensure that the about:home, about:newtab, and about:welcome
3845 // favicon is loaded instantaneously, to avoid flickering and improve
3846 // perceived performance.
3847 aWindow.gBrowser.setDefaultIcon(newTab, uriObj);
3849 // Collect state before flushing.
3850 let tabState = lazy.TabState.collect(aTab, TAB_CUSTOM_VALUES.get(aTab));
3852 // Flush to get the latest tab state to duplicate.
3853 let browser = aTab.linkedBrowser;
3854 lazy.TabStateFlusher.flush(browser).then(() => {
3855 // The new tab might have been closed in the meantime.
3856 if (newTab.closing || !newTab.linkedBrowser) {
3860 let window = newTab.ownerGlobal;
3862 // The tab or its window might be gone.
3863 if (!window || !window.__SSi) {
3867 // Update state with flushed data. We can't use TabState.clone() here as
3868 // the tab to duplicate may have already been closed. In that case we
3869 // only have access to the <xul:browser>.
3870 let options = { includePrivateData: true };
3871 lazy.TabState.copyFromCache(browser.permanentKey, tabState, options);
3873 tabState.index += aDelta;
3874 tabState.index = Math.max(
3876 Math.min(tabState.index, tabState.entries.length)
3878 tabState.pinned = false;
3880 if (inBackground === false) {
3881 aWindow.gBrowser.selectedTab = newTab;
3884 // Restore the state into the new tab.
3885 this.restoreTab(newTab, tabState, {
3886 restoreImmediately: aRestoreImmediately,
3893 getWindows(aWindowOrOptions) {
3895 if (!aWindowOrOptions) {
3896 aWindowOrOptions = this._getTopWindow();
3898 if (aWindowOrOptions instanceof Ci.nsIDOMWindow) {
3899 isPrivate = PrivateBrowsingUtils.isBrowserPrivate(aWindowOrOptions);
3901 isPrivate = Boolean(aWindowOrOptions.private);
3904 const browserWindows = Array.from(this._browserWindows).filter(win => {
3905 return PrivateBrowsingUtils.isBrowserPrivate(win) === isPrivate;
3907 return browserWindows;
3910 getWindowForTabClosedId(aClosedId, aIncludePrivate) {
3911 // check non-private windows first, and then only check private windows if
3912 // aIncludePrivate was true
3913 const privateValues = aIncludePrivate ? [false, true] : [false];
3914 for (let privateness of privateValues) {
3915 for (let window of this.getWindows({ private: privateness })) {
3916 const windowState = this._windows[window.__SSi];
3918 this._getStateForClosedTabsAndClosedGroupTabs(windowState);
3919 if (!closedTabs.length) {
3922 if (closedTabs.find(tab => tab.closedId === aClosedId)) {
3930 getLastClosedTabCount(aWindow) {
3931 if ("__SSi" in aWindow) {
3933 Math.max(this._windows[aWindow.__SSi]._lastClosedTabGroupCount, 1),
3934 this.getClosedTabCountForWindow(aWindow)
3938 throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
3941 resetLastClosedTabCount(aWindow) {
3942 if ("__SSi" in aWindow) {
3943 this._windows[aWindow.__SSi]._lastClosedTabGroupCount = -1;
3944 this._windows[aWindow.__SSi].lastClosedTabGroupId = null;
3946 throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
3950 getClosedTabCountForWindow: function ssi_getClosedTabCountForWindow(aWindow) {
3951 if ("__SSi" in aWindow) {
3952 return this._getStateForClosedTabsAndClosedGroupTabs(
3953 this._windows[aWindow.__SSi]
3957 if (!DyingWindowCache.has(aWindow)) {
3958 throw Components.Exception(
3959 "Window is not tracked",
3960 Cr.NS_ERROR_INVALID_ARG
3964 return this._getStateForClosedTabsAndClosedGroupTabs(
3965 DyingWindowCache.get(aWindow)
3969 _prepareClosedTabOptions(aOptions = {}) {
3970 const sourceOptions = Object.assign(
3972 closedTabsFromAllWindows: this._closedTabsFromAllWindowsEnabled,
3973 closedTabsFromClosedWindows: this._closedTabsFromClosedWindowsEnabled,
3976 aOptions instanceof Ci.nsIDOMWindow
3977 ? { sourceWindow: aOptions }
3980 if (!sourceOptions.sourceWindow) {
3981 sourceOptions.sourceWindow = this._getTopWindow(sourceOptions.private);
3984 _getTopWindow may return null on MacOS when the last window has been closed.
3985 Since private browsing windows are irrelevant after they have been closed we
3986 don't need to check if it was a private browsing window.
3988 if (!sourceOptions.sourceWindow) {
3989 sourceOptions.private = false;
3991 if (!sourceOptions.hasOwnProperty("private")) {
3992 sourceOptions.private = PrivateBrowsingUtils.isWindowPrivate(
3993 sourceOptions.sourceWindow
3996 return sourceOptions;
3999 getClosedTabCount(aOptions) {
4000 const sourceOptions = this._prepareClosedTabOptions(aOptions);
4003 if (sourceOptions.closedTabsFromAllWindows) {
4004 tabCount += this.getWindows({ private: sourceOptions.private })
4005 .map(win => this.getClosedTabCountForWindow(win))
4006 .reduce((total, count) => total + count, 0);
4008 tabCount += this.getClosedTabCountForWindow(sourceOptions.sourceWindow);
4011 if (!sourceOptions.private && sourceOptions.closedTabsFromClosedWindows) {
4012 tabCount += this.getClosedTabCountFromClosedWindows();
4017 getClosedTabCountFromClosedWindows:
4018 function ssi_getClosedTabCountFromClosedWindows() {
4019 const tabCount = this._closedWindows
4022 this._getStateForClosedTabsAndClosedGroupTabs(winData).length
4024 .reduce((total, count) => total + count, 0);
4028 getClosedTabDataForWindow: function ssi_getClosedTabDataForWindow(aWindow) {
4029 return this._getClonedDataForWindow(
4031 this._getStateForClosedTabsAndClosedGroupTabs
4035 getClosedTabData: function ssi_getClosedTabData(aOptions) {
4036 const sourceOptions = this._prepareClosedTabOptions(aOptions);
4037 const closedTabData = [];
4038 if (sourceOptions.closedTabsFromAllWindows) {
4039 for (let win of this.getWindows({ private: sourceOptions.private })) {
4040 closedTabData.push(...this.getClosedTabDataForWindow(win));
4044 ...this.getClosedTabDataForWindow(sourceOptions.sourceWindow)
4047 return closedTabData;
4050 getClosedTabDataFromClosedWindows:
4051 function ssi_getClosedTabDataFromClosedWindows() {
4052 const closedTabData = [];
4053 for (let winData of this._closedWindows) {
4054 const sourceClosedId = winData.closedId;
4055 const closedTabs = Cu.cloneInto(
4056 this._getStateForClosedTabsAndClosedGroupTabs(winData),
4059 // Add a property pointing back to the closed window source
4060 for (let tabData of closedTabs) {
4061 tabData.sourceClosedId = sourceClosedId;
4063 closedTabData.push(...closedTabs);
4065 // sorting is left to the caller
4066 return closedTabData;
4070 * @param {Window|object} aOptions
4071 * @param {Window} [aOptions.sourceWindow]
4072 * @param {boolean} [aOptions.private = false]
4073 * @param {boolean} [aOptions.closedTabsFromAllWindows]
4074 * @param {boolean} [aOptions.closedTabsFromClosedWindows]
4075 * @returns {ClosedTabGroupStateData[]}
4077 getClosedTabGroups: function ssi_getClosedTabGroups(aOptions) {
4078 const sourceOptions = this._prepareClosedTabOptions(aOptions);
4079 const closedTabGroups = [];
4080 if (sourceOptions.closedTabsFromAllWindows) {
4081 for (let win of this.getWindows({ private: sourceOptions.private })) {
4082 closedTabGroups.push(
4083 ...this._getClonedDataForWindow(win, w => w.closedGroups)
4087 closedTabGroups.push(
4088 ...this._getClonedDataForWindow(
4089 sourceOptions.sourceWindow,
4094 if (sourceOptions.closedTabsFromClosedWindows) {
4095 for (let winData of this.getClosedWindowData()) {
4096 // Add a property pointing back to the closed window source
4097 for (let groupData of winData.closedGroups) {
4098 for (let tabData of groupData.tabs) {
4099 tabData.sourceClosedId = winData.closedId;
4102 closedTabGroups.push(...winData.closedGroups);
4105 return closedTabGroups;
4108 getLastClosedTabGroupId(aWindow) {
4109 if ("__SSi" in aWindow) {
4110 return this._windows[aWindow.__SSi].lastClosedTabGroupId;
4113 throw new Error("Window is not tracked");
4117 * Returns a clone of some subset of a window's state data.
4120 * @param {Window} aWindow
4121 * @param {function(WindowStateData):D} selector
4122 * A function that returns the desired data located within
4123 * a supplied window state.
4126 _getClonedDataForWindow: function ssi_getClonedDataForWindow(
4130 // We need to enable wrapping reflectors in order to allow the cloning of
4131 // objects containing FormDatas, which could be stored by
4132 // form-associated custom elements.
4133 let options = { wrapReflectors: true };
4134 /** @type {WindowStateData} */
4137 if ("__SSi" in aWindow) {
4138 winData = this._windows[aWindow.__SSi];
4141 if (!winData && !DyingWindowCache.has(aWindow)) {
4142 throw Components.Exception(
4143 "Window is not tracked",
4144 Cr.NS_ERROR_INVALID_ARG
4148 winData ??= DyingWindowCache.get(aWindow);
4149 let data = selector(winData);
4150 return Cu.cloneInto(data, {}, options);
4154 * Returns either a unified list of closed tabs from both
4155 * `_closedTabs` and `closedGroups` or else, when supplying an index,
4156 * returns the specific closed tab from that unified list.
4158 * This bridges the gap between callers that want a unified list of all closed tabs
4159 * from all contexts vs. callers that want a specific list of closed tabs from a
4160 * specific context (e.g. only closed tabs from a specific closed tab group).
4162 * @param {WindowStateData} winData
4163 * @param {number} [aIndex]
4164 * If not supplied, returns all closed tabs and tabs from closed tab groups.
4165 * If supplied, returns the single closed tab with the given index.
4166 * @returns {TabStateData|TabStateData[]}
4168 _getStateForClosedTabsAndClosedGroupTabs:
4169 function ssi_getStateForClosedTabsAndClosedGroupTabs(winData, aIndex) {
4170 const closedGroups = winData.closedGroups ?? [];
4171 const closedTabs = winData._closedTabs ?? [];
4173 // Merge tabs and groups into a single sorted array of tabs sorted by
4179 let totalLength = closedGroups.length + closedTabs.length;
4181 while (current < totalLength) {
4182 let group = closedGroups[groupIdx];
4183 let tab = closedTabs[tabIdx];
4186 groupIdx < closedGroups.length &&
4187 (tabIdx >= closedTabs.length || group?.closedAt > tab?.closedAt)
4189 group.tabs.forEach((groupTab, idx) => {
4190 groupTab._originalStateIndex = idx;
4191 groupTab._originalGroupStateIndex = groupIdx;
4192 result.push(groupTab);
4196 tab._originalStateIndex = tabIdx;
4202 if (current > aIndex) {
4207 if (aIndex !== undefined) {
4208 return result[aIndex];
4215 * For a given closed tab that was retrieved by `_getStateForClosedTabsAndClosedGroupTabs`,
4216 * returns the specific closed tab list data source and the index within that data source
4217 * where the closed tab can be found.
4219 * This bridges the gap between callers that want a unified list of all closed tabs
4220 * from all contexts vs. callers that want a specific list of closed tabs from a
4221 * specific context (e.g. only closed tabs from a specific closed tab group).
4223 * @param {WindowState} sourceWinData
4224 * @param {TabStateData} tabState
4225 * @returns {{closedTabSet: TabStateData[], closedTabIndex: number}}
4227 _getClosedTabStateFromUnifiedIndex: function ssi_getClosedTabForUnifiedIndex(
4231 let closedTabSet, closedTabIndex;
4232 if (tabState._originalGroupStateIndex == null) {
4233 closedTabSet = sourceWinData._closedTabs;
4236 sourceWinData.closedGroups[tabState._originalGroupStateIndex].tabs;
4238 closedTabIndex = tabState._originalStateIndex;
4240 return { closedTabSet, closedTabIndex };
4243 undoCloseTab: function ssi_undoCloseTab(aSource, aIndex, aTargetWindow) {
4244 const sourceWinData = this._resolveClosedDataSource(aSource);
4245 const isPrivateSource = Boolean(sourceWinData.isPrivate);
4246 if (aTargetWindow && !aTargetWindow.__SSi) {
4247 throw Components.Exception(
4248 "Target window is not tracked",
4249 Cr.NS_ERROR_INVALID_ARG
4251 } else if (!aTargetWindow) {
4252 aTargetWindow = this._getTopWindow(isPrivateSource);
4255 isPrivateSource !== PrivateBrowsingUtils.isWindowPrivate(aTargetWindow)
4257 throw Components.Exception(
4258 "Target window doesn't have the same privateness as the source window",
4259 Cr.NS_ERROR_INVALID_ARG
4263 // default to the most-recently closed tab
4264 aIndex = aIndex || 0;
4266 const closedTabState = this._getStateForClosedTabsAndClosedGroupTabs(
4270 if (!closedTabState) {
4271 throw Components.Exception(
4272 "Invalid index: not in the closed tabs",
4273 Cr.NS_ERROR_INVALID_ARG
4276 let { closedTabSet, closedTabIndex } =
4277 this._getClosedTabStateFromUnifiedIndex(sourceWinData, closedTabState);
4279 // fetch the data of closed tab, while removing it from the array
4280 let { state, pos } = this.removeClosedTabData(
4285 this._cleanupOrphanedClosedGroups(sourceWinData);
4287 // Predict the remote type to use for the load to avoid unnecessary process
4289 let preferredRemoteType = lazy.E10SUtils.DEFAULT_REMOTE_TYPE;
4291 if (state.entries?.length) {
4292 let activeIndex = (state.index || state.entries.length) - 1;
4293 activeIndex = Math.min(activeIndex, state.entries.length - 1);
4294 activeIndex = Math.max(activeIndex, 0);
4295 url = state.entries[activeIndex].url;
4298 preferredRemoteType = this.getPreferredRemoteType(
4306 let tabbrowser = aTargetWindow.gBrowser;
4307 let tab = (tabbrowser.selectedTab = tabbrowser.addTrustedTab(null, {
4308 // Append the tab if we're opening into a different window,
4309 index: aSource == aTargetWindow ? pos : Infinity,
4310 pinned: state.pinned,
4311 userContextId: state.userContextId,
4313 preferredRemoteType,
4314 tabGroup: tabbrowser.tabGroups.find(g => g.id == state.groupId),
4317 // restore tab content
4318 this.restoreTab(tab, state);
4320 // Notify of changes to closed objects.
4321 this._notifyOfClosedObjectsChange();
4326 undoClosedTabFromClosedWindow: function ssi_undoClosedTabFromClosedWindow(
4331 const sourceWinData = this._resolveClosedDataSource(aSource);
4333 this._getStateForClosedTabsAndClosedGroupTabs(sourceWinData);
4334 const closedIndex = closedTabs.findIndex(
4335 tabData => tabData.closedId == aClosedId
4337 if (closedIndex >= 0) {
4338 return this.undoCloseTab(aSource, closedIndex, aTargetWindow);
4340 throw Components.Exception(
4341 "Invalid closedId: not in the closed tabs",
4342 Cr.NS_ERROR_INVALID_ARG
4346 getPreferredRemoteType(url, aWindow, userContextId) {
4347 return lazy.E10SUtils.getRemoteTypeForURI(
4349 aWindow.gMultiProcessBrowser,
4350 aWindow.gFissionBrowser,
4351 lazy.E10SUtils.DEFAULT_REMOTE_TYPE,
4353 lazy.E10SUtils.predictOriginAttributes({
4361 * @param {Window|{sourceWindow: Window}|{sourceClosedId: number}|{sourceWindowId: string}} aSource
4362 * @returns {WindowStateData}
4364 _resolveClosedDataSource(aSource) {
4366 if (aSource instanceof Ci.nsIDOMWindow) {
4367 winData = this.getWindowStateData(aSource);
4368 } else if (aSource.sourceWindow instanceof Ci.nsIDOMWindow) {
4369 winData = this.getWindowStateData(aSource.sourceWindow);
4370 } else if (typeof aSource.sourceClosedId == "number") {
4371 winData = this.getClosedWindowDataByClosedId(aSource.sourceClosedId);
4373 throw Components.Exception(
4374 "No such closed window",
4375 Cr.NS_ERROR_INVALID_ARG
4378 } else if (typeof aSource.sourceWindowId == "string") {
4379 let win = this.getWindowById(aSource.sourceWindowId);
4380 winData = this.getWindowStateData(win);
4382 throw Components.Exception(
4383 "Invalid source object",
4384 Cr.NS_ERROR_INVALID_ARG
4390 forgetClosedTab: function ssi_forgetClosedTab(aSource, aIndex) {
4391 const winData = this._resolveClosedDataSource(aSource);
4392 // default to the most-recently closed tab
4393 aIndex = aIndex || 0;
4394 if (!(aIndex in winData._closedTabs)) {
4395 throw Components.Exception(
4396 "Invalid index: not in the closed tabs",
4397 Cr.NS_ERROR_INVALID_ARG
4401 // remove closed tab from the array
4402 this.removeClosedTabData(winData, winData._closedTabs, aIndex);
4404 // Notify of changes to closed objects.
4405 this._notifyOfClosedObjectsChange();
4408 forgetClosedTabGroup: function ssi_forgetClosedTabGroup(aSource, tabGroupId) {
4409 const winData = this._resolveClosedDataSource(aSource);
4410 let closedGroupIndex = winData.closedGroups.findIndex(
4411 closedTabGroup => closedTabGroup.id == tabGroupId
4413 // let closedTabGroup = this.getClosedTabGroup(aSource, tabGroupId);
4414 if (closedGroupIndex < 0) {
4415 throw Components.Exception(
4416 "Closed tab group not found",
4417 Cr.NS_ERROR_INVALID_ARG
4421 let closedGroup = winData.closedGroups[closedGroupIndex];
4422 while (closedGroup.tabs.length) {
4423 this.removeClosedTabData(winData, closedGroup.tabs, 0);
4425 winData.closedGroups.splice(closedGroupIndex, 1);
4427 // Notify of changes to closed objects.
4428 this._notifyOfClosedObjectsChange();
4432 * @param {string} savedTabGroupId
4434 forgetSavedTabGroup: function ssi_forgetSavedTabGroup(savedTabGroupId) {
4435 let savedGroupIndex = this._savedGroups.findIndex(
4436 savedTabGroup => savedTabGroup.id == savedTabGroupId
4438 if (savedGroupIndex < 0) {
4439 throw Components.Exception(
4440 "Saved tab group not found",
4441 Cr.NS_ERROR_INVALID_ARG
4445 let savedGroup = this._savedGroups[savedGroupIndex];
4446 for (let i = 0; i < savedGroup.tabs.length; i++) {
4447 this.removeClosedTabData({}, savedGroup.tabs, i);
4449 this._savedGroups.splice(savedGroupIndex, 1);
4451 // Notify of changes to closed objects.
4452 this._closedObjectsChanged = true;
4453 this._notifyOfClosedObjectsChange();
4456 forgetClosedWindowById(aClosedId) {
4457 // We don't keep any record for closed private windows so privateness is not relevant here
4458 let closedIndex = this._closedWindows.findIndex(
4459 windowState => windowState.closedId == aClosedId
4461 if (closedIndex < 0) {
4462 throw Components.Exception(
4463 "Invalid closedId: not in the closed windows",
4464 Cr.NS_ERROR_INVALID_ARG
4467 this.forgetClosedWindow(closedIndex);
4470 forgetClosedTabById(aClosedId, aSourceOptions = {}) {
4471 let sourceWindowsData;
4472 let searchPrivateWindows = aSourceOptions.includePrivate ?? true;
4474 aSourceOptions instanceof Ci.nsIDOMWindow ||
4475 "sourceWindowId" in aSourceOptions ||
4476 "sourceClosedId" in aSourceOptions
4478 sourceWindowsData = [this._resolveClosedDataSource(aSourceOptions)];
4480 // Get the windows we'll look for the closed tab in, filtering out private
4481 // windows if necessary
4482 let browserWindows = Array.from(this._browserWindows);
4483 sourceWindowsData = [];
4484 for (let win of browserWindows) {
4486 !searchPrivateWindows &&
4487 PrivateBrowsingUtils.isBrowserPrivate(win)
4491 sourceWindowsData.push(this._windows[win.__SSi]);
4495 // See if the aCloseId matches a closed tab in any window data
4496 for (let winData of sourceWindowsData) {
4497 let closedIndex = winData._closedTabs.findIndex(
4498 tabData => tabData.closedId == aClosedId
4500 if (closedIndex >= 0) {
4501 // remove closed tab from the array
4502 this.removeClosedTabData(winData, winData._closedTabs, closedIndex);
4503 // Notify of changes to closed objects.
4504 this._notifyOfClosedObjectsChange();
4508 throw Components.Exception(
4509 "Invalid closedId: not found in the closed tabs of any window",
4510 Cr.NS_ERROR_INVALID_ARG
4514 getClosedWindowCount: function ssi_getClosedWindowCount() {
4515 return this._closedWindows.length;
4519 * @returns {WindowStateData[]}
4521 getClosedWindowData: function ssi_getClosedWindowData() {
4522 let closedWindows = Cu.cloneInto(this._closedWindows, {});
4523 for (let closedWinData of closedWindows) {
4524 this._trimSavedTabGroupMetadataInClosedWindow(closedWinData);
4526 return closedWindows;
4530 * If a closed window has a saved tab group inside of it, the closed window's
4531 * `groups` array entry will be a reference to a saved tab group entry.
4532 * However, since saved tab groups contain a lot of extra and duplicate
4533 * information, like their `tabs`, we only want to surface some of the
4534 * metadata about the saved tab groups to outside clients.
4536 * @param {WindowStateData} closedWinData
4537 * @returns {void} mutates the argument `closedWinData`
4539 _trimSavedTabGroupMetadataInClosedWindow(closedWinData) {
4540 let abbreviatedGroups = closedWinData.groups?.map(tabGroup =>
4541 lazy.TabGroupState.abbreviated(tabGroup)
4543 closedWinData.groups = Cu.cloneInto(abbreviatedGroups, {});
4546 maybeDontRestoreTabs(aWindow) {
4547 // Don't restore the tabs if we restore the session at startup
4548 this._windows[aWindow.__SSi]._maybeDontRestoreTabs = true;
4551 isLastRestorableWindow() {
4553 Object.values(this._windows).filter(winData => !winData.isPrivate)
4555 !this._closedWindows.some(win => win._shouldRestore || false)
4559 undoCloseWindow: function ssi_undoCloseWindow(aIndex) {
4560 if (!(aIndex in this._closedWindows)) {
4561 throw Components.Exception(
4562 "Invalid index: not in the closed windows",
4563 Cr.NS_ERROR_INVALID_ARG
4566 // reopen the window
4567 let state = { windows: this._removeClosedWindow(aIndex) };
4568 delete state.windows[0].closedAt; // Window is now open.
4570 // If any saved tab groups are in the closed window, convert the saved tab
4571 // groups into open tab groups in the closed window and then forget the saved
4572 // tab groups. This should have the effect of "moving" the saved tab groups
4573 // into the window that's about to be restored.
4574 this._trimSavedTabGroupMetadataInClosedWindow(state.windows[0]);
4575 for (let tabGroup of state.windows[0].groups) {
4576 if (this.getSavedTabGroup(tabGroup.id)) {
4577 this.forgetSavedTabGroup(tabGroup.id);
4581 let window = this._openWindowWithState(state);
4582 this.windowToFocus = window;
4583 WINDOW_SHOWING_PROMISES.get(window).promise.then(win =>
4584 this.restoreWindows(win, state, { overwriteTabs: true })
4587 // Notify of changes to closed objects.
4588 this._notifyOfClosedObjectsChange();
4593 forgetClosedWindow: function ssi_forgetClosedWindow(aIndex) {
4594 // default to the most-recently closed window
4595 aIndex = aIndex || 0;
4596 if (!(aIndex in this._closedWindows)) {
4597 throw Components.Exception(
4598 "Invalid index: not in the closed windows",
4599 Cr.NS_ERROR_INVALID_ARG
4603 // remove closed window from the array
4604 let winData = this._closedWindows[aIndex];
4605 this._removeClosedWindow(aIndex);
4606 this._saveableClosedWindowData.delete(winData);
4608 // Notify of changes to closed objects.
4609 this._notifyOfClosedObjectsChange();
4612 getCustomWindowValue(aWindow, aKey) {
4613 if ("__SSi" in aWindow) {
4614 let data = this._windows[aWindow.__SSi].extData || {};
4615 return data[aKey] || "";
4618 if (DyingWindowCache.has(aWindow)) {
4619 let data = DyingWindowCache.get(aWindow).extData || {};
4620 return data[aKey] || "";
4623 throw Components.Exception(
4624 "Window is not tracked",
4625 Cr.NS_ERROR_INVALID_ARG
4629 setCustomWindowValue(aWindow, aKey, aStringValue) {
4630 if (typeof aStringValue != "string") {
4631 throw new TypeError("setCustomWindowValue only accepts string values");
4634 if (!("__SSi" in aWindow)) {
4635 throw Components.Exception(
4636 "Window is not tracked",
4637 Cr.NS_ERROR_INVALID_ARG
4640 if (!this._windows[aWindow.__SSi].extData) {
4641 this._windows[aWindow.__SSi].extData = {};
4643 this._windows[aWindow.__SSi].extData[aKey] = aStringValue;
4644 this.saveStateDelayed(aWindow);
4647 deleteCustomWindowValue(aWindow, aKey) {
4650 this._windows[aWindow.__SSi].extData &&
4651 this._windows[aWindow.__SSi].extData[aKey]
4653 delete this._windows[aWindow.__SSi].extData[aKey];
4655 this.saveStateDelayed(aWindow);
4658 getCustomTabValue(aTab, aKey) {
4659 return (TAB_CUSTOM_VALUES.get(aTab) || {})[aKey] || "";
4662 setCustomTabValue(aTab, aKey, aStringValue) {
4663 if (typeof aStringValue != "string") {
4664 throw new TypeError("setCustomTabValue only accepts string values");
4667 // If the tab hasn't been restored, then set the data there, otherwise we
4668 // could lose newly added data.
4669 if (!TAB_CUSTOM_VALUES.has(aTab)) {
4670 TAB_CUSTOM_VALUES.set(aTab, {});
4673 TAB_CUSTOM_VALUES.get(aTab)[aKey] = aStringValue;
4674 this.saveStateDelayed(aTab.ownerGlobal);
4677 deleteCustomTabValue(aTab, aKey) {
4678 let state = TAB_CUSTOM_VALUES.get(aTab);
4679 if (state && aKey in state) {
4681 this.saveStateDelayed(aTab.ownerGlobal);
4686 * Retrieves data specific to lazy-browser tabs. If tab is not lazy,
4687 * will return undefined.
4689 * @param aTab (xul:tab)
4690 * The tabbrowser-tab the data is for.
4691 * @param aKey (string)
4692 * The key which maps to the desired data.
4694 getLazyTabValue(aTab, aKey) {
4695 return (TAB_LAZY_STATES.get(aTab) || {})[aKey];
4698 getCustomGlobalValue(aKey) {
4699 return this._globalState.get(aKey);
4702 setCustomGlobalValue(aKey, aStringValue) {
4703 if (typeof aStringValue != "string") {
4704 throw new TypeError("setCustomGlobalValue only accepts string values");
4707 this._globalState.set(aKey, aStringValue);
4708 this.saveStateDelayed();
4711 deleteCustomGlobalValue(aKey) {
4712 this._globalState.delete(aKey);
4713 this.saveStateDelayed();
4717 * Undoes the closing of a tab or window which corresponds
4718 * to the closedId passed in.
4720 * @param {integer} aClosedId
4721 * The closedId of the tab or window
4722 * @param {boolean} [aIncludePrivate = true]
4723 * Whether to restore private tabs or windows. Defaults to true
4724 * @param {Window} [aTargetWindow]
4725 * When aClosedId is for a closed tab, which window to re-open the tab into.
4726 * Defaults to current (topWindow).
4728 * @returns a tab or window object
4730 undoCloseById(aClosedId, aIncludePrivate = true, aTargetWindow) {
4731 // Check if we are re-opening a window first.
4732 for (let i = 0, l = this._closedWindows.length; i < l; i++) {
4733 if (this._closedWindows[i].closedId == aClosedId) {
4734 return this.undoCloseWindow(i);
4738 // See if the aCloseId matches a tab in an open window
4740 for (let sourceWindow of Services.wm.getEnumerator("navigator:browser")) {
4743 PrivateBrowsingUtils.isWindowPrivate(sourceWindow)
4747 let windowState = this._windows[sourceWindow.__SSi];
4749 for (let j = 0, l = windowState._closedTabs.length; j < l; j++) {
4750 if (windowState._closedTabs[j].closedId == aClosedId) {
4751 return this.undoCloseTab(sourceWindow, j, aTargetWindow);
4757 // Neither a tab nor a window was found, return undefined and let the caller decide what to do about it.
4762 * Updates the label and icon for a <xul:tab> using the data from
4766 * The <xul:tab> to update.
4767 * @param tabData (optional)
4768 * The tabData to use to update the tab. If the argument is
4769 * not supplied, the data will be retrieved from the cache.
4771 updateTabLabelAndIcon(tab, tabData = null) {
4772 if (tab.hasAttribute("customizemode")) {
4776 let browser = tab.linkedBrowser;
4777 let win = browser.ownerGlobal;
4780 tabData = lazy.TabState.collect(tab, TAB_CUSTOM_VALUES.get(tab));
4782 throw new Error("tabData not found for given tab");
4786 let activePageData = tabData.entries[tabData.index - 1] || null;
4788 // If the page has a title, set it.
4789 if (activePageData) {
4790 if (activePageData.title && activePageData.title != activePageData.url) {
4791 win.gBrowser.setInitialTabTitle(tab, activePageData.title, {
4792 isContentTitle: true,
4795 win.gBrowser.setInitialTabTitle(tab, activePageData.url);
4799 // Restore the tab icon.
4800 if ("image" in tabData) {
4801 // We know that about:blank is safe to load in any remote type. Since
4802 // SessionStore is triggered with about:blank, there must be a process
4803 // flip. We will ignore the first about:blank load to prevent resetting the
4804 // favicon that we have set earlier to avoid flickering and improve
4805 // perceived performance.
4808 (activePageData && activePageData.url != "about:blank")
4810 win.gBrowser.setIcon(
4814 tabData.iconLoadingPrincipal
4817 lazy.TabStateCache.update(browser.permanentKey, {
4819 iconLoadingPrincipal: null,
4824 // This method deletes all the closedTabs matching userContextId.
4825 _forgetTabsWithUserContextId(userContextId) {
4826 for (let window of Services.wm.getEnumerator("navigator:browser")) {
4827 let windowState = this._windows[window.__SSi];
4829 // In order to remove the tabs in the correct order, we store the
4830 // indexes, into an array, then we revert the array and remove closed
4831 // data from the last one going backward.
4833 windowState._closedTabs.forEach((closedTab, index) => {
4834 if (closedTab.state.userContextId == userContextId) {
4835 indexes.push(index);
4839 for (let index of indexes.reverse()) {
4840 this.removeClosedTabData(windowState, windowState._closedTabs, index);
4845 // Notify of changes to closed objects.
4846 this._notifyOfClosedObjectsChange();
4850 * Restores the session state stored in LastSession. This will attempt
4851 * to merge data into the current session. If a window was opened at startup
4852 * with pinned tab(s), then the remaining data from the previous session for
4853 * that window will be opened into that window. Otherwise new windows will
4856 restoreLastSession: function ssi_restoreLastSession() {
4857 // Use the public getter since it also checks PB mode
4858 if (!this.canRestoreLastSession) {
4859 throw Components.Exception("Last session can not be restored");
4862 Services.obs.notifyObservers(null, NOTIFY_INITIATING_MANUAL_RESTORE);
4864 // First collect each window with its id...
4866 for (let window of this._browserWindows) {
4867 if (window.__SS_lastSessionWindowID) {
4868 windows[window.__SS_lastSessionWindowID] = window;
4872 let lastSessionState = LastSession.getState();
4874 // This shouldn't ever be the case...
4875 if (!lastSessionState.windows.length) {
4876 throw Components.Exception(
4877 "lastSessionState has no windows",
4878 Cr.NS_ERROR_UNEXPECTED
4882 // We're technically doing a restore, so set things up so we send the
4883 // notification when we're done. We want to send "sessionstore-browser-state-restored".
4884 this._restoreCount = lastSessionState.windows.length;
4885 this._browserSetState = true;
4887 // We want to re-use the last opened window instead of opening a new one in
4888 // the case where it's "empty" and not associated with a window in the session.
4889 // We will do more processing via _prepWindowToRestoreInto if we need to use
4891 let lastWindow = this._getTopWindow();
4892 let canUseLastWindow = lastWindow && !lastWindow.__SS_lastSessionWindowID;
4894 // global data must be restored before restoreWindow is called so that
4895 // it happens before observers are notified
4896 this._globalState.setFromState(lastSessionState);
4898 let openWindows = [];
4899 let windowsToOpen = [];
4901 // Restore session cookies.
4902 lazy.SessionCookies.restore(lastSessionState.cookies || []);
4904 // Restore into windows or open new ones as needed.
4905 for (let i = 0; i < lastSessionState.windows.length; i++) {
4906 let winState = lastSessionState.windows[i];
4907 let lastSessionWindowID = winState.__lastSessionWindowID;
4908 // delete lastSessionWindowID so we don't add that to the window again
4909 delete winState.__lastSessionWindowID;
4911 // See if we can use an open window. First try one that is associated with
4912 // the state we're trying to restore and then fallback to the last selected
4914 let windowToUse = windows[lastSessionWindowID];
4915 if (!windowToUse && canUseLastWindow) {
4916 windowToUse = lastWindow;
4917 canUseLastWindow = false;
4920 let [canUseWindow, canOverwriteTabs] =
4921 this._prepWindowToRestoreInto(windowToUse);
4923 // If there's a window already open that we can restore into, use that
4925 if (!PERSIST_SESSIONS) {
4926 // Since we're not overwriting existing tabs, we want to merge _closedTabs,
4927 // putting existing ones first. Then make sure we're respecting the max pref.
4928 if (winState._closedTabs && winState._closedTabs.length) {
4929 let curWinState = this._windows[windowToUse.__SSi];
4930 curWinState._closedTabs = curWinState._closedTabs.concat(
4931 winState._closedTabs
4933 curWinState._closedTabs.splice(
4934 this._max_tabs_undo,
4935 curWinState._closedTabs.length
4939 // We don't restore window right away, just store its data.
4940 // Later, these windows will be restored with newly opened windows.
4941 this._updateWindowRestoreState(windowToUse, {
4942 windows: [winState],
4943 options: { overwriteTabs: canOverwriteTabs },
4945 openWindows.push(windowToUse);
4947 windowsToOpen.push(winState);
4951 // Actually restore windows in reversed z-order.
4952 this._openWindows({ windows: windowsToOpen }).then(openedWindows =>
4953 this._restoreWindowsInReversedZOrder(openWindows.concat(openedWindows))
4956 // Merge closed windows from this session with ones from last session
4957 if (lastSessionState._closedWindows) {
4958 // reset window closedIds and any references to them from closed tabs
4959 for (let closedWindow of lastSessionState._closedWindows) {
4960 closedWindow.closedId = this._nextClosedId++;
4961 if (closedWindow._closedTabs?.length) {
4962 this._resetClosedTabIds(
4963 closedWindow._closedTabs,
4964 closedWindow.closedId
4968 this._closedWindows = this._closedWindows.concat(
4969 lastSessionState._closedWindows
4971 this._capClosedWindows();
4972 this._closedObjectsChanged = true;
4975 lazy.DevToolsShim.restoreDevToolsSession(lastSessionState);
4977 // Set data that persists between sessions
4978 this._recentCrashes =
4979 (lastSessionState.session && lastSessionState.session.recentCrashes) || 0;
4981 // Update the session start time using the restored session state.
4982 this._updateSessionStartTime(lastSessionState);
4984 LastSession.clear();
4986 // Notify of changes to closed objects.
4987 this._notifyOfClosedObjectsChange();
4991 * Revive a crashed tab and restore its state from before it crashed.
4994 * A <xul:tab> linked to a crashed browser. This is a no-op if the
4995 * browser hasn't actually crashed, or is not associated with a tab.
4996 * This function will also throw if the browser happens to be remote.
4998 reviveCrashedTab(aTab) {
5001 "SessionStore.reviveCrashedTab expected a tab, but got null."
5005 let browser = aTab.linkedBrowser;
5006 if (!this._crashedBrowsers.has(browser.permanentKey)) {
5010 // Sanity check - the browser to be revived should not be remote
5012 if (browser.isRemoteBrowser) {
5014 "SessionStore.reviveCrashedTab: " +
5015 "Somehow a crashed browser is still remote."
5019 // We put the browser at about:blank in case the user is
5020 // restoring tabs on demand. This way, the user won't see
5021 // a flash of the about:tabcrashed page after selecting
5023 aTab.removeAttribute("crashed");
5025 browser.loadURI(lazy.blankURI, {
5026 triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({
5027 userContextId: aTab.userContextId,
5029 remoteTypeOverride: lazy.E10SUtils.NOT_REMOTE,
5032 let data = lazy.TabState.collect(aTab, TAB_CUSTOM_VALUES.get(aTab));
5033 this.restoreTab(aTab, data, {
5034 forceOnDemand: true,
5039 * Revive all crashed tabs and reset the crashed tabs count to 0.
5041 reviveAllCrashedTabs() {
5042 for (let window of Services.wm.getEnumerator("navigator:browser")) {
5043 for (let tab of window.gBrowser.tabs) {
5044 this.reviveCrashedTab(tab);
5050 * Retrieves the latest session history information for a tab. The cached data
5051 * is returned immediately, but a callback may be provided that supplies
5052 * up-to-date data when or if it is available. The callback is passed a single
5053 * argument with data in the same format as the return value.
5055 * @param tab tab to retrieve the session history for
5056 * @param updatedCallback function to call with updated data as the single argument
5057 * @returns a object containing 'index' specifying the current index, and an
5058 * array 'entries' containing an object for each history item.
5060 getSessionHistory(tab, updatedCallback) {
5061 if (updatedCallback) {
5062 lazy.TabStateFlusher.flush(tab.linkedBrowser).then(() => {
5063 let sessionHistory = this.getSessionHistory(tab);
5064 if (sessionHistory) {
5065 updatedCallback(sessionHistory);
5070 // Don't continue if the tab was closed before TabStateFlusher.flush resolves.
5071 if (tab.linkedBrowser) {
5072 let tabState = lazy.TabState.collect(tab, TAB_CUSTOM_VALUES.get(tab));
5073 return { index: tabState.index - 1, entries: tabState.entries };
5079 * See if aWindow is usable for use when restoring a previous session via
5080 * restoreLastSession. If usable, prepare it for use.
5083 * the window to inspect & prepare
5084 * @returns [canUseWindow, canOverwriteTabs]
5085 * canUseWindow: can the window be used to restore into
5086 * canOverwriteTabs: all of the current tabs are home pages and we
5087 * can overwrite them
5089 _prepWindowToRestoreInto: function ssi_prepWindowToRestoreInto(aWindow) {
5091 return [false, false];
5094 // We might be able to overwrite the existing tabs instead of just adding
5095 // the previous session's tabs to the end. This will be set if possible.
5096 let canOverwriteTabs = false;
5098 // Look at the open tabs in comparison to home pages. If all the tabs are
5099 // home pages then we'll end up overwriting all of them. Otherwise we'll
5100 // just close the tabs that match home pages. Tabs with the about:blank
5101 // URI will always be overwritten.
5102 let homePages = ["about:blank"];
5103 let removableTabs = [];
5104 let tabbrowser = aWindow.gBrowser;
5105 let startupPref = this._prefBranch.getIntPref("startup.page");
5106 if (startupPref == 1) {
5107 homePages = homePages.concat(lazy.HomePage.get(aWindow).split("|"));
5110 for (let i = tabbrowser.pinnedTabCount; i < tabbrowser.tabs.length; i++) {
5111 let tab = tabbrowser.tabs[i];
5112 if (homePages.includes(tab.linkedBrowser.currentURI.spec)) {
5113 removableTabs.push(tab);
5118 tabbrowser.tabs.length > tabbrowser.visibleTabs.length &&
5119 tabbrowser.visibleTabs.length === removableTabs.length
5121 // If all the visible tabs are also removable and the selected tab is hidden or removeable, we will later remove
5122 // all "removable" tabs causing the browser to automatically close because the only tab left is hidden.
5123 // To prevent the browser from automatically closing, we will leave one other visible tab open.
5124 removableTabs.shift();
5127 if (tabbrowser.tabs.length == removableTabs.length) {
5128 canOverwriteTabs = true;
5130 // If we're not overwriting all of the tabs, then close the home tabs.
5131 for (let i = removableTabs.length - 1; i >= 0; i--) {
5132 tabbrowser.removeTab(removableTabs.pop(), { animate: false });
5136 return [true, canOverwriteTabs];
5139 /* ........ Saving Functionality .............. */
5142 * Store window dimensions, visibility, sidebar
5146 _updateWindowFeatures: function ssi_updateWindowFeatures(aWindow) {
5147 var winData = this._windows[aWindow.__SSi];
5149 WINDOW_ATTRIBUTES.forEach(function (aAttr) {
5150 winData[aAttr] = this._getWindowDimension(aWindow, aAttr);
5153 if (winData.sizemode != "minimized") {
5154 winData.sizemodeBeforeMinimized = winData.sizemode;
5157 var hidden = WINDOW_HIDEABLE_FEATURES.filter(function (aItem) {
5158 return aWindow[aItem] && !aWindow[aItem].visible;
5160 if (hidden.length) {
5161 winData.hidden = hidden.join(",");
5162 } else if (winData.hidden) {
5163 delete winData.hidden;
5166 const sidebarUIState = aWindow.SidebarController.getUIState();
5167 if (sidebarUIState) {
5168 winData.sidebar = structuredClone(sidebarUIState);
5171 let workspaceID = aWindow.getWorkspaceID();
5173 winData.workspaceID = workspaceID;
5178 * gather session data as object
5180 * Bool update all windows
5183 getCurrentState(aUpdateAll) {
5184 this._handleClosedWindows().then(() => {
5185 this._notifyOfClosedObjectsChange();
5188 var activeWindow = this._getTopWindow();
5190 TelemetryStopwatch.start("FX_SESSION_RESTORE_COLLECT_ALL_WINDOWS_DATA_MS");
5191 if (lazy.RunState.isRunning) {
5192 // update the data for all windows with activities since the last save operation.
5194 for (let window of this._orderedBrowserWindows) {
5195 if (!this._isWindowLoaded(window)) {
5196 // window data is still in _statesToRestore
5199 if (aUpdateAll || DirtyWindows.has(window) || window == activeWindow) {
5200 this._collectWindowData(window);
5202 // always update the window features (whose change alone never triggers a save operation)
5203 this._updateWindowFeatures(window);
5205 this._windows[window.__SSi].zIndex = ++index;
5207 DirtyWindows.clear();
5209 TelemetryStopwatch.finish("FX_SESSION_RESTORE_COLLECT_ALL_WINDOWS_DATA_MS");
5211 // An array that at the end will hold all current window data.
5213 // The ids of all windows contained in 'total' in the same order.
5215 // The number of window that are _not_ popups.
5216 var nonPopupCount = 0;
5219 // collect the data for all windows
5220 for (ix in this._windows) {
5221 if (this._windows[ix]._restoring) {
5222 // window data is still in _statesToRestore
5225 total.push(this._windows[ix]);
5227 if (!this._windows[ix].isPopup) {
5232 // collect the data for all windows yet to be restored
5233 for (ix in this._statesToRestore) {
5234 for (let winData of this._statesToRestore[ix].windows) {
5235 total.push(winData);
5236 if (!winData.isPopup) {
5242 // shallow copy this._closedWindows to preserve current state
5243 let lastClosedWindowsCopy = this._closedWindows.slice();
5245 if (AppConstants.platform != "macosx") {
5246 // If no non-popup browser window remains open, return the state of the last
5247 // closed window(s). We only want to do this when we're actually "ending"
5249 // XXXzpao We should do this for _restoreLastWindow == true, but that has
5250 // its own check for popups. c.f. bug 597619
5252 nonPopupCount == 0 &&
5253 !!lastClosedWindowsCopy.length &&
5254 lazy.RunState.isQuitting
5256 // prepend the last non-popup browser window, so that if the user loads more tabs
5257 // at startup we don't accidentally add them to a popup window
5259 total.unshift(lastClosedWindowsCopy.shift());
5260 } while (total[0].isPopup && lastClosedWindowsCopy.length);
5265 this.activeWindowSSiCache = activeWindow.__SSi || "";
5267 ix = ids.indexOf(this.activeWindowSSiCache);
5268 // We don't want to restore focus to a minimized window or a window which had all its
5269 // tabs stripped out (doesn't exist).
5270 if (ix != -1 && total[ix] && total[ix].sizemode == "minimized") {
5275 lastUpdate: Date.now(),
5276 startTime: this._sessionStartTime,
5277 recentCrashes: this._recentCrashes,
5281 version: ["sessionrestore", FORMAT_VERSION],
5283 selectedWindow: ix + 1,
5284 _closedWindows: lastClosedWindowsCopy,
5285 savedGroups: this._savedGroups,
5287 global: this._globalState.getState(),
5290 // Collect and store session cookies.
5291 state.cookies = lazy.SessionCookies.collect();
5293 lazy.DevToolsShim.saveDevToolsSession(state);
5295 // Persist the last session if we deferred restoring it
5296 if (LastSession.canRestore) {
5297 state.lastSessionState = LastSession.getState();
5300 // If we were called by the SessionSaver and started with only a private
5301 // window we want to pass the deferred initial state to not lose the
5302 // previous session.
5303 if (this._deferredInitialState) {
5304 state.deferredInitialState = this._deferredInitialState;
5311 * serialize session data for a window
5312 * @param {Window} aWindow
5314 * @returns {{windows: [WindowStateData]}}
5316 _getWindowState: function ssi_getWindowState(aWindow) {
5317 if (!this._isWindowLoaded(aWindow)) {
5318 return this._statesToRestore[WINDOW_RESTORE_IDS.get(aWindow)];
5321 if (lazy.RunState.isRunning) {
5322 this._collectWindowData(aWindow);
5325 return { windows: [this._windows[aWindow.__SSi]] };
5329 * Retrieves window data for an active session.
5331 * @param {Window} aWindow
5332 * @returns {WindowStateData}
5333 * @throws {Error} if `aWindow` is not being managed in the session store.
5335 getWindowStateData: function ssi_getWindowStateData(aWindow) {
5336 if (!aWindow.__SSi || !(aWindow.__SSi in this._windows)) {
5337 throw Components.Exception(
5338 "Window is not tracked",
5339 Cr.NS_ERROR_INVALID_ARG
5343 return this._windows[aWindow.__SSi];
5347 * Gathers data about a window and its tabs, and updates its
5348 * entry in this._windows.
5351 * Window references.
5352 * @returns a Map mapping the browser tabs from aWindow to the tab
5353 * entry that was put into the window data in this._windows.
5355 _collectWindowData: function ssi_collectWindowData(aWindow) {
5356 let tabMap = new Map();
5358 if (!this._isWindowLoaded(aWindow)) {
5362 let tabbrowser = aWindow.gBrowser;
5363 let tabs = tabbrowser.tabs;
5364 /** @type {WindowStateData} */
5365 let winData = this._windows[aWindow.__SSi];
5366 let tabsData = (winData.tabs = []);
5368 // update the internal state data for this window
5369 for (let tab of tabs) {
5370 if (tab == aWindow.FirefoxViewHandler.tab) {
5373 let tabData = lazy.TabState.collect(tab, TAB_CUSTOM_VALUES.get(tab));
5374 tabMap.set(tab, tabData);
5375 tabsData.push(tabData);
5378 // update tab group state for this window
5379 winData.groups = [];
5380 for (let tabGroup of aWindow.gBrowser.tabGroups) {
5381 let tabGroupData = lazy.TabGroupState.collect(tabGroup);
5382 winData.groups.push(tabGroupData);
5385 let selectedIndex = tabbrowser.tabbox.selectedIndex + 1;
5386 // We don't store the Firefox View tab in Session Store, so if it was the last selected "tab" when
5387 // a window is closed, point to the first item in the tab strip instead (it will never be the Firefox View tab,
5388 // since it's only inserted into the tab strip after it's selected).
5389 if (aWindow.FirefoxViewHandler.tab?.selected) {
5391 winData.title = tabbrowser.tabs[0].label;
5393 winData.selected = selectedIndex;
5395 this._updateWindowFeatures(aWindow);
5397 // Make sure we keep __SS_lastSessionWindowID around for cases like entering
5398 // or leaving PB mode.
5399 if (aWindow.__SS_lastSessionWindowID) {
5400 this._windows[aWindow.__SSi].__lastSessionWindowID =
5401 aWindow.__SS_lastSessionWindowID;
5404 DirtyWindows.remove(aWindow);
5408 /* ........ Restoring Functionality .............. */
5411 * Open windows with data
5415 * @returns a promise resolved when all windows have been opened
5417 _openWindows(root) {
5418 let windowsOpened = [];
5419 for (let winData of root.windows) {
5420 if (!winData || !winData.tabs || !winData.tabs[0]) {
5421 this._log.debug(`_openWindows, skipping window with no tabs data`);
5422 this._restoreCount--;
5425 windowsOpened.push(this._openWindowWithState({ windows: [winData] }));
5427 let windowOpenedPromises = [];
5428 for (const openedWindow of windowsOpened) {
5429 let deferred = WINDOW_SHOWING_PROMISES.get(openedWindow);
5430 windowOpenedPromises.push(deferred.promise);
5432 return Promise.all(windowOpenedPromises);
5435 /** reset closedId's from previous sessions to ensure these IDs are unique
5437 * an array of data to be restored
5438 * @param {String} windowId
5439 * The SessionStore id for the window these tabs should be associated with
5440 * @returns the updated tabData array
5442 _resetClosedTabIds(tabData, windowId) {
5443 for (let entry of tabData) {
5444 entry.closedId = this._nextClosedId++;
5445 entry.sourceWindowId = windowId;
5450 * restore features to a single window
5452 * Window reference to the window to use for restoration
5455 * @param aOptions.overwriteTabs
5456 * to overwrite existing tabs w/ new ones
5457 * @param aOptions.firstWindow
5458 * if this is the first non-private window we're
5459 * restoring in this session, that might open an
5460 * external link as well
5462 restoreWindow: function ssi_restoreWindow(aWindow, winData, aOptions = {}) {
5463 let overwriteTabs = aOptions && aOptions.overwriteTabs;
5464 let firstWindow = aOptions && aOptions.firstWindow;
5466 this.restoreSidebar(aWindow, winData.sidebar, winData.isPopup);
5468 // initialize window if necessary
5469 if (aWindow && (!aWindow.__SSi || !this._windows[aWindow.__SSi])) {
5470 this.onLoad(aWindow);
5473 TelemetryStopwatch.start("FX_SESSION_RESTORE_RESTORE_WINDOW_MS");
5475 // We're not returning from this before we end up calling restoreTabs
5476 // for this window, so make sure we send the SSWindowStateBusy event.
5477 this._sendWindowRestoringNotification(aWindow);
5478 this._setWindowStateBusy(aWindow);
5480 if (winData.workspaceID) {
5481 this._log.debug(`Moving window to workspace: ${winData.workspaceID}`);
5482 aWindow.moveToWorkspace(winData.workspaceID);
5485 if (!winData.tabs) {
5487 // don't restore a single blank tab when we've had an external
5488 // URL passed in for loading at startup (cf. bug 357419)
5492 winData.tabs.length == 1 &&
5493 (!winData.tabs[0].entries || !winData.tabs[0].entries.length)
5498 // See SessionStoreInternal.restoreTabs for a description of what
5499 // selectTab represents.
5501 if (overwriteTabs) {
5502 selectTab = parseInt(winData.selected || 1, 10);
5503 selectTab = Math.max(selectTab, 1);
5504 selectTab = Math.min(selectTab, winData.tabs.length);
5507 let tabbrowser = aWindow.gBrowser;
5509 // disable smooth scrolling while adding, moving, removing and selecting tabs
5510 let arrowScrollbox = tabbrowser.tabContainer.arrowScrollbox;
5511 let smoothScroll = arrowScrollbox.smoothScroll;
5512 arrowScrollbox.smoothScroll = false;
5514 // We need to keep track of the initially open tabs so that they
5515 // can be moved to the end of the restored tabs.
5517 if (!overwriteTabs && firstWindow) {
5518 initialTabs = Array.from(tabbrowser.tabs);
5521 // Get rid of tabs that aren't needed anymore.
5522 if (overwriteTabs) {
5523 for (let i = tabbrowser.browsers.length - 1; i >= 0; i--) {
5524 if (!tabbrowser.tabs[i].selected) {
5525 tabbrowser.removeTab(tabbrowser.tabs[i]);
5530 let restoreTabsLazily =
5531 this._prefBranch.getBoolPref("sessionstore.restore_tabs_lazily") &&
5532 this._restore_on_demand;
5535 `restoreWindow, will restore ${winData.tabs.length} tabs and ${
5536 winData.groups?.length ?? 0
5537 } tab groups, restoreTabsLazily: ${restoreTabsLazily}`
5539 if (winData.tabs.length) {
5540 var tabs = tabbrowser.createTabsForSessionRestore(
5544 winData.groups ?? []
5547 `restoreWindow, createTabsForSessionRestore returned ${tabs.length} tabs`
5551 // Move the originally open tabs to the end.
5553 let endPosition = tabbrowser.tabs.length - 1;
5554 for (let i = 0; i < initialTabs.length; i++) {
5555 tabbrowser.unpinTab(initialTabs[i]);
5556 tabbrowser.moveTabTo(initialTabs[i], endPosition, {
5557 forceStandaloneTab: true,
5562 // We want to correlate the window with data from the last session, so
5563 // assign another id if we have one. Otherwise clear so we don't do
5564 // anything with it.
5565 delete aWindow.__SS_lastSessionWindowID;
5566 if (winData.__lastSessionWindowID) {
5567 aWindow.__SS_lastSessionWindowID = winData.__lastSessionWindowID;
5570 if (overwriteTabs) {
5571 delete this._windows[aWindow.__SSi].extData;
5574 // Restore cookies from legacy sessions, i.e. before bug 912717.
5575 lazy.SessionCookies.restore(winData.cookies || []);
5577 if (winData.extData) {
5578 if (!this._windows[aWindow.__SSi].extData) {
5579 this._windows[aWindow.__SSi].extData = {};
5581 for (var key in winData.extData) {
5582 this._windows[aWindow.__SSi].extData[key] = winData.extData[key];
5586 let newClosedTabsData;
5587 if (winData._closedTabs) {
5588 newClosedTabsData = winData._closedTabs;
5589 this._resetClosedTabIds(newClosedTabsData, aWindow.__SSi);
5591 newClosedTabsData = [];
5594 let newLastClosedTabGroupCount = winData._lastClosedTabGroupCount || -1;
5596 if (overwriteTabs || firstWindow) {
5597 // Overwrite existing closed tabs data when overwriteTabs=true
5598 // or we're the first window to be restored.
5599 this._windows[aWindow.__SSi]._closedTabs = newClosedTabsData;
5600 } else if (this._max_tabs_undo > 0) {
5601 // We preserve tabs between sessions so we just want to filter out any previously open tabs that
5602 // were added to the _closedTabs list prior to restoreLastSession
5603 if (PERSIST_SESSIONS) {
5604 newClosedTabsData = this._windows[aWindow.__SSi]._closedTabs.filter(
5605 tab => !tab.removeAfterRestore
5608 newClosedTabsData = newClosedTabsData.concat(
5609 this._windows[aWindow.__SSi]._closedTabs
5613 // ... and make sure that we don't exceed the max number of closed tabs
5615 this._windows[aWindow.__SSi]._closedTabs = newClosedTabsData.slice(
5620 // Because newClosedTabsData are put in first, we need to
5621 // copy also the _lastClosedTabGroupCount.
5622 this._windows[aWindow.__SSi]._lastClosedTabGroupCount =
5623 newLastClosedTabGroupCount;
5625 // Copy over closed tab groups from the previous session,
5626 // and reset closed tab ids for tabs within each group.
5627 let newClosedTabGroupsData = winData.closedGroups || [];
5628 newClosedTabGroupsData.forEach(group => {
5629 this._resetClosedTabIds(group.tabs, aWindow.__SSi);
5631 this._windows[aWindow.__SSi].closedGroups = newClosedTabGroupsData;
5632 this._windows[aWindow.__SSi].lastClosedTabGroupId =
5633 winData.lastClosedTabGroupId || null;
5635 if (!this._isWindowLoaded(aWindow)) {
5636 // from now on, the data will come from the actual window
5637 delete this._statesToRestore[WINDOW_RESTORE_IDS.get(aWindow)];
5638 WINDOW_RESTORE_IDS.delete(aWindow);
5639 delete this._windows[aWindow.__SSi]._restoring;
5642 // Restore tabs, if any.
5643 if (winData.tabs.length) {
5644 this.restoreTabs(aWindow, tabs, winData.tabs, selectTab);
5647 // set smoothScroll back to the original value
5648 arrowScrollbox.smoothScroll = smoothScroll;
5650 TelemetryStopwatch.finish("FX_SESSION_RESTORE_RESTORE_WINDOW_MS");
5652 this._setWindowStateReady(aWindow);
5654 this._sendWindowRestoredNotification(aWindow);
5656 Services.obs.notifyObservers(aWindow, NOTIFY_SINGLE_WINDOW_RESTORED);
5658 this._sendRestoreCompletedNotifications();
5662 * Prepare connection to host beforehand.
5665 * Tab we are loading from.
5668 * @returns a flag indicates whether a connection has been made
5670 prepareConnectionToHost(tab, url) {
5671 if (url && !url.startsWith("about:")) {
5672 let principal = Services.scriptSecurityManager.createNullPrincipal({
5673 userContextId: tab.userContextId,
5675 let sc = Services.io.QueryInterface(Ci.nsISpeculativeConnect);
5676 let uri = Services.io.newURI(url);
5678 sc.speculativeConnect(uri, principal, null, false);
5681 // Can't setup speculative connection for this url.
5682 console.error(error);
5690 * Make a connection to a host when users hover mouse on a tab.
5691 * This will also set a flag in the tab to prevent us from speculatively
5692 * connecting a second time.
5695 * a tab to speculatively connect on mouse hover.
5697 speculativeConnectOnTabHover(tab) {
5698 let tabState = TAB_LAZY_STATES.get(tab);
5699 if (tabState && !tabState.connectionPrepared) {
5700 let url = this.getLazyTabValue(tab, "url");
5701 let prepared = this.prepareConnectionToHost(tab, url);
5702 // This is used to test if a connection has been made beforehand.
5703 if (gDebuggingEnabled) {
5704 tab.__test_connection_prepared = prepared;
5705 tab.__test_connection_url = url;
5707 // A flag indicate that we've prepared a connection for this tab and
5708 // if is called again, we shouldn't prepare another connection.
5709 tabState.connectionPrepared = true;
5714 * This function will restore window features and then restore window data.
5717 * ordered array of windows to restore
5719 _restoreWindowsFeaturesAndTabs(windows) {
5720 // First, we restore window features, so that when users start interacting
5721 // with a window, we don't steal the window focus.
5722 for (let window of windows) {
5723 let state = this._statesToRestore[WINDOW_RESTORE_IDS.get(window)];
5724 this.restoreWindowFeatures(window, state.windows[0]);
5727 // Then we restore data into windows.
5728 for (let window of windows) {
5729 let state = this._statesToRestore[WINDOW_RESTORE_IDS.get(window)];
5733 state.options || { overwriteTabs: true }
5735 WINDOW_RESTORE_ZINDICES.delete(window);
5740 * This function will restore window in reversed z-index, so that users will
5741 * be presented with most recently used window first.
5744 * unordered array of windows to restore
5746 _restoreWindowsInReversedZOrder(windows) {
5749 (WINDOW_RESTORE_ZINDICES.get(a) || 0) -
5750 (WINDOW_RESTORE_ZINDICES.get(b) || 0)
5753 this.windowToFocus = windows[0];
5754 this._restoreWindowsFeaturesAndTabs(windows);
5758 * Restore multiple windows using the provided state.
5760 * Window reference to the first window to use for restoration.
5761 * Additionally required windows will be opened.
5763 * JS object or JSON string
5764 * @param aOptions.overwriteTabs
5765 * to overwrite existing tabs w/ new ones
5766 * @param aOptions.firstWindow
5767 * if this is the first non-private window we're
5768 * restoring in this session, that might open an
5769 * external link as well
5771 restoreWindows: function ssi_restoreWindows(aWindow, aState, aOptions = {}) {
5772 // initialize window if necessary
5773 if (aWindow && (!aWindow.__SSi || !this._windows[aWindow.__SSi])) {
5774 this.onLoad(aWindow);
5779 root = typeof aState == "string" ? JSON.parse(aState) : aState;
5781 // invalid state object - don't restore anything
5782 this._log.debug(`restoreWindows failed to parse ${typeof aState} state`);
5783 this._log.error(ex);
5784 this._sendRestoreCompletedNotifications();
5788 // Restore closed windows if any.
5789 if (root._closedWindows) {
5790 this._closedWindows = root._closedWindows;
5791 // reset window closedIds and any references to them from closed tabs
5792 for (let closedWindow of this._closedWindows) {
5793 closedWindow.closedId = this._nextClosedId++;
5794 if (closedWindow._closedTabs?.length) {
5795 this._resetClosedTabIds(
5796 closedWindow._closedTabs,
5797 closedWindow.closedId
5801 this._log.debug(`Restored ${this._closedWindows.length} closed windows`);
5802 this._closedObjectsChanged = true;
5806 `restoreWindows will restore ${root.windows?.length} windows`
5808 // We're done here if there are no windows.
5809 if (!root.windows || !root.windows.length) {
5810 this._sendRestoreCompletedNotifications();
5814 let firstWindowData = root.windows.splice(0, 1);
5815 // Store the restore state and restore option of the current window,
5816 // so that the window can be restored in reversed z-order.
5817 this._updateWindowRestoreState(aWindow, {
5818 windows: firstWindowData,
5822 // Begin the restoration: First open all windows in creation order. After all
5823 // windows have opened, we restore states to windows in reversed z-order.
5824 this._openWindows(root).then(windows => {
5825 // We want to add current window to opened window, so that this window will be
5826 // restored in reversed z-order. (We add the window to first position, in case
5827 // no z-indices are found, that window will be restored first.)
5828 windows.unshift(aWindow);
5830 this._restoreWindowsInReversedZOrder(windows);
5833 lazy.DevToolsShim.restoreDevToolsSession(aState);
5837 * Manage history restoration for a window
5839 * Window to restore the tabs into
5841 * Array of tab references
5845 * Index of the tab to select. This is a 1-based index where "1"
5846 * indicates the first tab should be selected, and "0" indicates that
5847 * the currently selected tab will not be changed.
5849 restoreTabs(aWindow, aTabs, aTabData, aSelectTab) {
5850 var tabbrowser = aWindow.gBrowser;
5852 let numTabsToRestore = aTabs.length;
5853 let numTabsInWindow = tabbrowser.tabs.length;
5854 let tabsDataArray = this._windows[aWindow.__SSi].tabs;
5856 // Update the window state in case we shut down without being notified.
5857 // Individual tab states will be taken care of by restoreTab() below.
5858 if (numTabsInWindow == numTabsToRestore) {
5859 // Remove all previous tab data.
5860 tabsDataArray.length = 0;
5862 // Remove all previous tab data except tabs that should not be overriden.
5863 tabsDataArray.splice(numTabsInWindow - numTabsToRestore);
5866 // Remove items from aTabData if there is no corresponding tab:
5867 if (numTabsInWindow < tabsDataArray.length) {
5868 tabsDataArray.length = numTabsInWindow;
5871 // Ensure the tab data array has items for each of the tabs
5872 this._ensureNoNullsInTabDataList(
5878 if (aSelectTab > 0 && aSelectTab <= aTabs.length) {
5879 // Update the window state in case we shut down without being notified.
5880 this._windows[aWindow.__SSi].selected = aSelectTab;
5883 // If we restore the selected tab, make sure it goes first.
5884 let selectedIndex = aTabs.indexOf(tabbrowser.selectedTab);
5885 if (selectedIndex > -1) {
5886 this.restoreTab(tabbrowser.selectedTab, aTabData[selectedIndex]);
5889 // Restore all tabs.
5890 for (let t = 0; t < aTabs.length; t++) {
5891 if (t != selectedIndex) {
5892 this.restoreTab(aTabs[t], aTabData[t]);
5897 // In case we didn't collect/receive data for any tabs yet we'll have to
5898 // fill the array with at least empty tabData objects until |_tPos| or
5899 // we'll end up with |null| entries.
5900 _ensureNoNullsInTabDataList(tabElements, tabDataList, changedTabPos) {
5901 let initialDataListLength = tabDataList.length;
5902 if (changedTabPos < initialDataListLength) {
5905 // Add items to the end.
5906 while (tabDataList.length < changedTabPos) {
5907 let existingTabEl = tabElements[tabDataList.length];
5910 lastAccessed: existingTabEl.lastAccessed,
5913 // Ensure the pre-existing items are non-null.
5914 for (let i = 0; i < initialDataListLength; i++) {
5915 if (!tabDataList[i]) {
5916 let existingTabEl = tabElements[i];
5919 lastAccessed: existingTabEl.lastAccessed,
5925 // Restores the given tab state for a given tab.
5926 restoreTab(tab, tabData, options = {}) {
5927 let browser = tab.linkedBrowser;
5929 if (TAB_STATE_FOR_BROWSER.has(browser)) {
5930 this._log.warn("Must reset tab before calling restoreTab.");
5934 let loadArguments = options.loadArguments;
5935 let window = tab.ownerGlobal;
5936 let tabbrowser = window.gBrowser;
5937 let forceOnDemand = options.forceOnDemand;
5938 let isRemotenessUpdate = options.isRemotenessUpdate;
5940 let willRestoreImmediately =
5941 options.restoreImmediately || tabbrowser.selectedBrowser == browser;
5943 let isBrowserInserted = browser.isConnected;
5945 // Increase the busy state counter before modifying the tab.
5946 this._setWindowStateBusy(window);
5948 // It's important to set the window state to dirty so that
5949 // we collect their data for the first time when saving state.
5950 DirtyWindows.add(window);
5952 if (!tab.hasOwnProperty("_tPos")) {
5954 "Shouldn't be trying to restore a tab that has no position"
5957 // Update the tab state in case we shut down without being notified.
5958 this._windows[window.__SSi].tabs[tab._tPos] = tabData;
5960 // Prepare the tab so that it can be properly restored. We'll also attach
5961 // a copy of the tab's data in case we close it before it's been restored.
5962 // Anything that dispatches an event to external consumers must happen at
5963 // the end of this method, to make sure that the tab/browser object is in a
5964 // reliable and consistent state.
5966 if (tabData.lastAccessed) {
5967 tab.updateLastAccessed(tabData.lastAccessed);
5970 if (!tabData.entries) {
5971 tabData.entries = [];
5973 if (tabData.extData) {
5974 TAB_CUSTOM_VALUES.set(tab, Cu.cloneInto(tabData.extData, {}));
5976 TAB_CUSTOM_VALUES.delete(tab);
5980 delete tabData.closedAt;
5982 // Ensure the index is in bounds.
5983 let activeIndex = (tabData.index || tabData.entries.length) - 1;
5984 activeIndex = Math.min(activeIndex, tabData.entries.length - 1);
5985 activeIndex = Math.max(activeIndex, 0);
5987 // Save the index in case we updated it above.
5988 tabData.index = activeIndex + 1;
5990 tab.setAttribute("pending", "true");
5992 // If we're restoring this tab, it certainly shouldn't be in
5993 // the ignored set anymore.
5994 this._crashedBrowsers.delete(browser.permanentKey);
5996 // If we're in the midst of performing a process flip, then we must
5997 // have initiated a navigation. This means that these userTyped*
5998 // values are now out of date.
6000 options.restoreContentReason ==
6001 RESTORE_TAB_CONTENT_REASON.NAVIGATE_AND_RESTORE
6003 delete tabData.userTypedValue;
6004 delete tabData.userTypedClear;
6007 // Update the persistent tab state cache with |tabData| information.
6008 lazy.TabStateCache.update(browser.permanentKey, {
6009 // NOTE: Copy the entries array shallowly, so as to not screw with the
6010 // original tabData's history when getting history updates.
6011 history: { entries: [...tabData.entries], index: tabData.index },
6012 scroll: tabData.scroll || null,
6013 storage: tabData.storage || null,
6014 formdata: tabData.formdata || null,
6015 disallow: tabData.disallow || null,
6016 userContextId: tabData.userContextId || 0,
6018 // This information is only needed until the tab has finished restoring.
6019 // When that's done it will be removed from the cache and we always
6020 // collect it in TabState._collectBaseTabData().
6021 image: tabData.image || "",
6022 iconLoadingPrincipal: tabData.iconLoadingPrincipal || null,
6023 searchMode: tabData.searchMode || null,
6024 userTypedValue: tabData.userTypedValue || "",
6025 userTypedClear: tabData.userTypedClear || 0,
6028 // Restore tab attributes.
6029 if ("attributes" in tabData) {
6030 lazy.TabAttributes.set(tab, tabData.attributes);
6033 if (isBrowserInserted) {
6034 // Start a new epoch to discard all frame script messages relating to a
6035 // previous epoch. All async messages that are still on their way to chrome
6036 // will be ignored and don't override any tab data set when restoring.
6037 let epoch = this.startNextEpoch(browser.permanentKey);
6039 // Ensure that the tab will get properly restored in the event the tab
6040 // crashes while restoring. But don't set this on lazy browsers as
6041 // restoreTab will get called again when the browser is instantiated.
6042 TAB_STATE_FOR_BROWSER.set(browser, TAB_STATE_NEEDS_RESTORE);
6044 this._sendRestoreHistory(browser, {
6051 // This could cause us to ignore MAX_CONCURRENT_TAB_RESTORES a bit, but
6052 // it ensures each window will have its selected tab loaded.
6053 if (willRestoreImmediately) {
6054 this.restoreTabContent(tab, options);
6055 } else if (!forceOnDemand) {
6056 TabRestoreQueue.add(tab);
6057 // Check if a tab is in queue and will be restored
6058 // after the currently loading tabs. If so, prepare
6059 // a connection to host to speed up page loading.
6060 if (TabRestoreQueue.willRestoreSoon(tab)) {
6061 if (activeIndex in tabData.entries) {
6062 let url = tabData.entries[activeIndex].url;
6063 let prepared = this.prepareConnectionToHost(tab, url);
6064 if (gDebuggingEnabled) {
6065 tab.__test_connection_prepared = prepared;
6066 tab.__test_connection_url = url;
6070 this.restoreNextTab();
6073 // TAB_LAZY_STATES holds data for lazy-browser tabs to proxy for
6074 // data unobtainable from the unbound browser. This only applies to lazy
6075 // browsers and will be removed once the browser is inserted in the document.
6076 // This must preceed `updateTabLabelAndIcon` call for required data to be present.
6077 let url = "about:blank";
6080 if (activeIndex in tabData.entries) {
6081 url = tabData.entries[activeIndex].url;
6082 title = tabData.entries[activeIndex].title || url;
6084 TAB_LAZY_STATES.set(tab, {
6087 userTypedValue: tabData.userTypedValue || "",
6088 userTypedClear: tabData.userTypedClear || 0,
6092 // Most of tabData has been restored, now continue with restoring
6093 // attributes that may trigger external events.
6095 if (tabData.pinned) {
6096 tabbrowser.pinTab(tab);
6098 tabbrowser.unpinTab(tab);
6101 if (tabData.hidden) {
6102 tabbrowser.hideTab(tab);
6104 tabbrowser.showTab(tab);
6107 if (!!tabData.muted != browser.audioMuted) {
6108 tab.toggleMuteAudio(tabData.muteReason);
6111 if (tab.hasAttribute("customizemode")) {
6112 window.gCustomizeMode.setTab(tab);
6115 // Update tab label and icon to show something
6116 // while we wait for the messages to be processed.
6117 this.updateTabLabelAndIcon(tab, tabData);
6119 // Decrease the busy state counter after we're done.
6120 this._setWindowStateReady(window);
6124 * Kicks off restoring the given tab.
6127 * the tab to restore
6129 * optional arguments used when performing process switch during load
6131 restoreTabContent(aTab, aOptions = {}) {
6132 let loadArguments = aOptions.loadArguments;
6133 if (aTab.hasAttribute("customizemode") && !loadArguments) {
6137 let browser = aTab.linkedBrowser;
6138 let window = aTab.ownerGlobal;
6139 let tabData = lazy.TabState.clone(aTab, TAB_CUSTOM_VALUES.get(aTab));
6140 let activeIndex = tabData.index - 1;
6141 let activePageData = tabData.entries[activeIndex] || null;
6142 let uri = activePageData ? activePageData.url || null : null;
6144 this.markTabAsRestoring(aTab);
6146 this._sendRestoreTabContent(browser, {
6148 isRemotenessUpdate: aOptions.isRemotenessUpdate,
6150 aOptions.restoreContentReason || RESTORE_TAB_CONTENT_REASON.SET_STATE,
6153 // Focus the tab's content area, unless the restore is for a new tab URL or
6154 // was triggered by a DocumentChannel process switch.
6157 !window.isBlankPageURL(uri) &&
6158 !aOptions.isRemotenessUpdate
6165 * Marks a given pending tab as restoring.
6168 * the pending tab to mark as restoring
6170 markTabAsRestoring(aTab) {
6171 let browser = aTab.linkedBrowser;
6172 if (TAB_STATE_FOR_BROWSER.get(browser) != TAB_STATE_NEEDS_RESTORE) {
6173 throw new Error("Given tab is not pending.");
6176 // Make sure that this tab is removed from the priority queue.
6177 TabRestoreQueue.remove(aTab);
6179 // Increase our internal count.
6180 this._tabsRestoringCount++;
6182 // Set this tab's state to restoring
6183 TAB_STATE_FOR_BROWSER.set(browser, TAB_STATE_RESTORING);
6184 aTab.removeAttribute("pending");
6188 * This _attempts_ to restore the next available tab. If the restore fails,
6189 * then we will attempt the next one.
6190 * There are conditions where this won't do anything:
6191 * if we're in the process of quitting
6192 * if there are no tabs to restore
6193 * if we have already reached the limit for number of tabs to restore
6195 restoreNextTab: function ssi_restoreNextTab() {
6196 // If we call in here while quitting, we don't actually want to do anything
6197 if (lazy.RunState.isQuitting) {
6201 // Don't exceed the maximum number of concurrent tab restores.
6202 if (this._tabsRestoringCount >= MAX_CONCURRENT_TAB_RESTORES) {
6206 let tab = TabRestoreQueue.shift();
6208 this.restoreTabContent(tab);
6213 * Restore visibility and dimension features to a window
6217 * Object containing session data for the window
6219 restoreWindowFeatures: function ssi_restoreWindowFeatures(aWindow, aWinData) {
6220 var hidden = aWinData.hidden ? aWinData.hidden.split(",") : [];
6221 WINDOW_HIDEABLE_FEATURES.forEach(function (aItem) {
6222 aWindow[aItem].visible = !hidden.includes(aItem);
6225 if (aWinData.isPopup) {
6226 this._windows[aWindow.__SSi].isPopup = true;
6227 if (aWindow.gURLBar) {
6228 aWindow.gURLBar.readOnly = true;
6231 delete this._windows[aWindow.__SSi].isPopup;
6232 if (aWindow.gURLBar) {
6233 aWindow.gURLBar.readOnly = false;
6237 aWindow.setTimeout(() => {
6238 this.restoreDimensions(
6240 +(aWinData.width || 0),
6241 +(aWinData.height || 0),
6242 "screenX" in aWinData ? +aWinData.screenX : NaN,
6243 "screenY" in aWinData ? +aWinData.screenY : NaN,
6244 aWinData.sizemode || "",
6245 aWinData.sizemodeBeforeMinimized || ""
6247 this.restoreSidebar(aWindow, aWinData.sidebar, aWinData.isPopup);
6255 * Object containing command (sidebarcommand/category) and styles
6257 restoreSidebar(aWindow, aSidebar, isPopup) {
6258 if (!aSidebar || isPopup) {
6261 aWindow.SidebarController.initializeUIState(aSidebar);
6265 * Restore a window's dimensions
6267 * Window width in desktop pixels
6269 * Window height in desktop pixels
6271 * Window left in desktop pixels
6273 * Window top in desktop pixels
6275 * Window size mode (eg: maximized)
6276 * @param aSizeModeBeforeMinimized
6277 * Window size mode before window got minimized (eg: maximized)
6279 restoreDimensions: function ssi_restoreDimensions(
6286 aSizeModeBeforeMinimized
6290 function win_(aName) {
6291 return _this._getWindowDimension(win, aName);
6294 const dwu = win.windowUtils;
6295 // find available space on the screen where this window is being placed
6296 let screen = lazy.gScreenManager.screenForRect(
6303 let screenLeft = {},
6307 screen.GetAvailRectDisplayPix(
6314 // We store aLeft / aTop (screenX/Y) in desktop pixels, see
6315 // _getWindowDimension.
6316 screenLeft = screenLeft.value;
6317 screenTop = screenTop.value;
6318 screenWidth = screenWidth.value;
6319 screenHeight = screenHeight.value;
6321 let screenBottom = screenTop + screenHeight;
6322 let screenRight = screenLeft + screenWidth;
6324 // NOTE: contentsScaleFactor is the desktopToDeviceScale of the screen.
6325 // Naming could be more consistent here.
6326 let cssToDesktopScale =
6327 screen.defaultCSSScaleFactor / screen.contentsScaleFactor;
6329 let winSlopX = win.screenEdgeSlopX * cssToDesktopScale;
6330 let winSlopY = win.screenEdgeSlopY * cssToDesktopScale;
6332 let minSlop = MIN_SCREEN_EDGE_SLOP * cssToDesktopScale;
6333 let slopX = Math.max(minSlop, winSlopX);
6334 let slopY = Math.max(minSlop, winSlopY);
6336 // Pull the window within the screen's bounds (allowing a little slop
6337 // for windows that may be deliberately placed with their border off-screen
6338 // as when Win10 "snaps" a window to the left/right edge -- bug 1276516).
6339 // First, ensure the left edge is large enough...
6340 if (aLeft < screenLeft - slopX) {
6341 aLeft = screenLeft - winSlopX;
6343 // Then check the resulting right edge, and reduce it if necessary.
6344 let right = aLeft + aWidth * cssToDesktopScale;
6345 if (right > screenRight + slopX) {
6346 right = screenRight + winSlopX;
6347 // See if we can move the left edge leftwards to maintain width.
6348 if (aLeft > screenLeft) {
6350 right - aWidth * cssToDesktopScale,
6351 screenLeft - winSlopX
6355 // Finally, update aWidth to account for the adjusted left and right
6356 // edges, and convert it back to CSS pixels on the target screen.
6357 aWidth = (right - aLeft) / cssToDesktopScale;
6359 // And do the same in the vertical dimension.
6360 if (aTop < screenTop - slopY) {
6361 aTop = screenTop - winSlopY;
6363 let bottom = aTop + aHeight * cssToDesktopScale;
6364 if (bottom > screenBottom + slopY) {
6365 bottom = screenBottom + winSlopY;
6366 if (aTop > screenTop) {
6368 bottom - aHeight * cssToDesktopScale,
6369 screenTop - winSlopY
6373 aHeight = (bottom - aTop) / cssToDesktopScale;
6376 // Suppress animations.
6377 dwu.suppressAnimation(true);
6379 // We want to make sure users will get their animations back in case an exception is thrown.
6381 // only modify those aspects which aren't correct yet
6385 (aLeft != win_("screenX") || aTop != win_("screenY"))
6387 // moveTo uses CSS pixels relative to aWindow, while aLeft and aRight
6388 // are on desktop pixels, undo the conversion we do in
6389 // _getWindowDimension.
6390 let desktopToCssScale =
6391 aWindow.desktopToDeviceScale / aWindow.devicePixelRatio;
6392 aWindow.moveTo(aLeft * desktopToCssScale, aTop * desktopToCssScale);
6397 (aWidth != win_("width") || aHeight != win_("height")) &&
6398 !ChromeUtils.shouldResistFingerprinting("RoundWindowSize", null)
6400 // Don't resize the window if it's currently maximized and we would
6401 // maximize it again shortly after.
6402 if (aSizeMode != "maximized" || win_("sizemode") != "maximized") {
6403 aWindow.resizeTo(aWidth, aHeight);
6406 this._windows[aWindow.__SSi].sizemodeBeforeMinimized =
6407 aSizeModeBeforeMinimized;
6410 win_("sizemode") != aSizeMode &&
6411 !ChromeUtils.shouldResistFingerprinting("RoundWindowSize", null)
6413 switch (aSizeMode) {
6418 if (aSizeModeBeforeMinimized == "maximized") {
6428 // since resizing/moving a window brings it to the foreground,
6429 // we might want to re-focus the last focused window
6430 if (this.windowToFocus) {
6431 this.windowToFocus.focus();
6434 // Enable animations.
6435 dwu.suppressAnimation(false);
6439 /* ........ Disk Access .............. */
6442 * Save the current session state to disk, after a delay.
6444 * @param aWindow (optional)
6445 * Will mark the given window as dirty so that we will recollect its
6446 * data before we start writing.
6448 saveStateDelayed(aWindow = null) {
6450 DirtyWindows.add(aWindow);
6453 lazy.SessionSaver.runDelayed();
6456 /* ........ Auxiliary Functions .............. */
6459 * Remove a closed window from the list of closed windows and indicate that
6460 * the change should be notified.
6463 * The index of the window in this._closedWindows.
6465 * @returns Array of closed windows.
6467 _removeClosedWindow(index) {
6468 // remove all of the closed tabs from the _lastClosedActions list
6469 // before removing the window from it
6470 for (let closedTab of this._closedWindows[index]._closedTabs) {
6471 this._removeClosedAction(
6472 this._LAST_ACTION_CLOSED_TAB,
6476 this._removeClosedAction(
6477 this._LAST_ACTION_CLOSED_WINDOW,
6478 this._closedWindows[index].closedId
6480 let windows = this._closedWindows.splice(index, 1);
6481 this._closedObjectsChanged = true;
6486 * Notifies observers that the list of closed tabs and/or windows has changed.
6487 * Waits a tick to allow SessionStorage a chance to register the change.
6489 _notifyOfClosedObjectsChange() {
6490 if (!this._closedObjectsChanged) {
6493 this._closedObjectsChanged = false;
6494 lazy.setTimeout(() => {
6495 Services.obs.notifyObservers(null, NOTIFY_CLOSED_OBJECTS_CHANGED);
6500 * Update the session start time and send a telemetry measurement
6501 * for the number of days elapsed since the session was started.
6504 * The session state.
6506 _updateSessionStartTime: function ssi_updateSessionStartTime(state) {
6507 // Attempt to load the session start time from the session state
6508 if (state.session && state.session.startTime) {
6509 this._sessionStartTime = state.session.startTime;
6514 * Iterator that yields all currently opened browser windows.
6515 * (Might miss the most recent one.)
6516 * This list is in focus order, but may include minimized windows
6517 * before non-minimized windows.
6520 *[Symbol.iterator]() {
6521 for (let window of lazy.BrowserWindowTracker.orderedWindows) {
6522 if (window.__SSi && !window.closed) {
6530 * Iterator that yields all currently opened browser windows,
6531 * with minimized windows last.
6532 * (Might miss the most recent window.)
6534 _orderedBrowserWindows: {
6535 *[Symbol.iterator]() {
6536 let windows = lazy.BrowserWindowTracker.orderedWindows;
6537 windows.sort((a, b) => {
6539 a.windowState == a.STATE_MINIMIZED &&
6540 b.windowState != b.STATE_MINIMIZED
6545 a.windowState != a.STATE_MINIMIZED &&
6546 b.windowState == b.STATE_MINIMIZED
6552 for (let window of windows) {
6553 if (window.__SSi && !window.closed) {
6561 * Returns most recent window
6562 * @param {boolean} [isPrivate]
6563 * Optional boolean to get only non-private or private windows
6564 * When omitted, we'll return whatever the top-most window is regardless of privateness
6565 * @returns Window reference
6567 _getTopWindow: function ssi_getTopWindow(isPrivate) {
6568 const options = { allowPopups: true };
6569 if (typeof isPrivate !== "undefined") {
6570 options.private = isPrivate;
6572 return lazy.BrowserWindowTracker.getTopWindow(options);
6576 * Calls onClose for windows that are determined to be closed but aren't
6577 * destroyed yet, which would otherwise cause getBrowserState and
6578 * setBrowserState to treat them as open windows.
6580 _handleClosedWindows: function ssi_handleClosedWindows() {
6582 for (let window of Services.wm.getEnumerator("navigator:browser")) {
6583 if (window.closed) {
6584 promises.push(this.onClose(window));
6587 return Promise.all(promises);
6591 * Store a restore state of a window to this._statesToRestore. The window
6592 * will be given an id that can be used to get the restore state from
6593 * this._statesToRestore.
6596 * a reference to a window that has a state to restore
6598 * an object containing session data
6600 _updateWindowRestoreState(window, state) {
6601 // Store z-index, so that windows can be restored in reversed z-order.
6602 if ("zIndex" in state.windows[0]) {
6603 WINDOW_RESTORE_ZINDICES.set(window, state.windows[0].zIndex);
6606 var ID = "window" + Math.random();
6607 } while (ID in this._statesToRestore);
6608 WINDOW_RESTORE_IDS.set(window, ID);
6609 this._statesToRestore[ID] = state;
6613 * open a new browser window for a given session state
6614 * called when restoring a multi-window session
6616 * Object containing session data
6618 _openWindowWithState: function ssi_openWindowWithState(aState) {
6619 var argString = Cc["@mozilla.org/supports-string;1"].createInstance(
6620 Ci.nsISupportsString
6622 argString.data = "";
6624 // Build feature string
6626 let winState = aState.windows[0];
6627 if (winState.chromeFlags) {
6628 features = ["chrome", "suppressanimation"];
6629 let chromeFlags = winState.chromeFlags;
6630 const allFlags = Ci.nsIWebBrowserChrome.CHROME_ALL;
6631 const hasAll = (chromeFlags & allFlags) == allFlags;
6633 features.push("all");
6635 for (let [flag, onValue, offValue] of CHROME_FLAGS_MAP) {
6636 if (hasAll && allFlags & flag) {
6639 let value = chromeFlags & flag ? onValue : offValue;
6641 features.push(value);
6645 // |chromeFlags| is not found. Fallbacks to the old method.
6646 features = ["chrome", "dialog=no", "suppressanimation"];
6647 let hidden = winState.hidden?.split(",") || [];
6648 if (!hidden.length) {
6649 features.push("all");
6651 features.push("resizable");
6652 WINDOW_HIDEABLE_FEATURES.forEach(aFeature => {
6653 if (!hidden.includes(aFeature)) {
6654 features.push(WINDOW_OPEN_FEATURES_MAP[aFeature] || aFeature);
6659 WINDOW_ATTRIBUTES.forEach(aFeature => {
6660 // Use !isNaN as an easy way to ignore sizemode and check for numbers
6661 if (aFeature in winState && !isNaN(winState[aFeature])) {
6662 features.push(aFeature + "=" + winState[aFeature]);
6666 if (winState.isPrivate) {
6667 features.push("private");
6671 `Opening window with features: ${features.join(
6673 )}, argString: ${argString}.`
6675 var window = Services.ww.openWindow(
6677 AppConstants.BROWSER_CHROME_URL,
6683 this._updateWindowRestoreState(window, aState);
6684 WINDOW_SHOWING_PROMISES.set(window, Promise.withResolvers());
6690 * whether the user wants to load any other page at startup
6691 * (except the homepage) - needed for determining whether to overwrite the current tabs
6692 * C.f.: nsBrowserContentHandler's defaultArgs implementation.
6695 _isCmdLineEmpty: function ssi_isCmdLineEmpty(aWindow, aState) {
6698 aState.windows.every(win => win.tabs.every(tab => tab.pinned));
6700 let hasFirstArgument = aWindow.arguments && aWindow.arguments[0];
6702 let defaultArgs = Cc["@mozilla.org/browser/clh;1"].getService(
6703 Ci.nsIBrowserHandler
6706 aWindow.arguments &&
6707 aWindow.arguments[0] &&
6708 aWindow.arguments[0] == defaultArgs
6710 hasFirstArgument = false;
6714 return !hasFirstArgument;
6718 * on popup windows, the AppWindow's attributes seem not to be set correctly
6719 * we use thus JSDOMWindow attributes for sizemode and normal window attributes
6720 * (and hope for reasonable values when maximized/minimized - since then
6721 * outerWidth/outerHeight aren't the dimensions of the restored window)
6725 * String sizemode | width | height | other window attribute
6728 _getWindowDimension: function ssi_getWindowDimension(aWindow, aAttribute) {
6729 if (aAttribute == "sizemode") {
6730 switch (aWindow.windowState) {
6731 case aWindow.STATE_FULLSCREEN:
6732 case aWindow.STATE_MAXIMIZED:
6734 case aWindow.STATE_MINIMIZED:
6741 // We want to persist the size / position in normal state, so that
6742 // we can restore to them even if the window is currently maximized
6743 // or minimized. However, attributes on window object only reflect
6744 // the current state of the window, so when it isn't in the normal
6745 // sizemode, their values aren't what we want the window to restore
6746 // to. In that case, try to read from the attributes of the root
6747 // element first instead.
6748 if (aWindow.windowState != aWindow.STATE_NORMAL) {
6749 let docElem = aWindow.document.documentElement;
6750 let attr = parseInt(docElem.getAttribute(aAttribute), 10);
6752 if (aAttribute != "width" && aAttribute != "height") {
6755 // Width and height attribute report the inner size, but we want
6756 // to store the outer size, so add the difference.
6757 let appWin = aWindow.docShell.treeOwner
6758 .QueryInterface(Ci.nsIInterfaceRequestor)
6759 .getInterface(Ci.nsIAppWindow);
6761 aAttribute == "width"
6762 ? appWin.outerToInnerWidthDifferenceInCSSPixels
6763 : appWin.outerToInnerHeightDifferenceInCSSPixels;
6768 switch (aAttribute) {
6770 return aWindow.outerWidth;
6772 return aWindow.outerHeight;
6775 // We use desktop pixels rather than CSS pixels to store window
6776 // positions, see bug 1247335. This allows proper multi-monitor
6777 // positioning in mixed-DPI situations.
6778 // screenX/Y are in CSS pixels for the current window, so, convert them
6779 // to desktop pixels.
6781 (aWindow[aAttribute] * aWindow.devicePixelRatio) /
6782 aWindow.desktopToDeviceScale
6785 return aAttribute in aWindow ? aWindow[aAttribute] : "";
6790 * @param aState is a session state
6791 * @param aRecentCrashes is the number of consecutive crashes
6792 * @returns whether a restore page will be needed for the session state
6794 _needsRestorePage: function ssi_needsRestorePage(aState, aRecentCrashes) {
6795 const SIX_HOURS_IN_MS = 6 * 60 * 60 * 1000;
6797 // don't display the page when there's nothing to restore
6798 let winData = aState.windows || null;
6799 if (!winData || !winData.length) {
6803 // don't wrap a single about:sessionrestore page
6805 this._hasSingleTabWithURL(winData, "about:sessionrestore") ||
6806 this._hasSingleTabWithURL(winData, "about:welcomeback")
6811 // don't automatically restore in Safe Mode
6812 if (Services.appinfo.inSafeMode) {
6816 let max_resumed_crashes = this._prefBranch.getIntPref(
6817 "sessionstore.max_resumed_crashes"
6821 aState.session.lastUpdate &&
6822 Date.now() - aState.session.lastUpdate;
6825 max_resumed_crashes != -1 &&
6826 (aRecentCrashes > max_resumed_crashes ||
6827 (sessionAge && sessionAge >= SIX_HOURS_IN_MS));
6830 if (aRecentCrashes > max_resumed_crashes) {
6831 if (sessionAge && sessionAge >= SIX_HOURS_IN_MS) {
6832 key = "shown_many_crashes_old_session";
6834 key = "shown_many_crashes";
6837 key = "shown_old_session";
6839 Glean.browserEngagement.sessionrestoreInterstitial[key].add(1);
6845 * @param aWinData is the set of windows in session state
6846 * @param aURL is the single URL we're looking for
6847 * @returns whether the window data contains only the single URL passed
6849 _hasSingleTabWithURL(aWinData, aURL) {
6852 aWinData.length == 1 &&
6854 aWinData[0].tabs.length == 1 &&
6855 aWinData[0].tabs[0].entries &&
6856 aWinData[0].tabs[0].entries.length == 1
6858 return aURL == aWinData[0].tabs[0].entries[0].url;
6864 * Determine if the tab state we're passed is something we should save. This
6865 * is used when closing a tab, tab group, or closing a window with a single tab
6868 * The current tab state
6871 _shouldSaveTabState: function ssi_shouldSaveTabState(aTabState) {
6872 // If the tab has only a transient about: history entry, no other
6873 // session history, and no userTypedValue, then we don't actually want to
6874 // store this tab's data.
6875 const entryUrl = aTabState.entries[0]?.url;
6879 aTabState.entries.length == 1 &&
6880 (entryUrl == "about:blank" ||
6881 entryUrl == "about:home" ||
6882 entryUrl == "about:newtab" ||
6883 entryUrl == "about:privatebrowsing") &&
6884 !aTabState.userTypedValue
6890 * Determine if a tab group should be saved based on whether any of its tabs
6893 * @param {MozTabbrowserTabGroup} group the tab group to check
6894 * @returns {boolean} true if the group is saveable.
6896 shouldSaveTabGroup: function ssi_shouldSaveTabGroup(group) {
6897 for (let tab of group.tabs) {
6898 let tabState = lazy.TabState.collect(tab);
6899 if (this._shouldSaveTabState(tabState)) {
6907 * Determine if the tab state we're passed is something we should keep to be
6908 * reopened at session restore. This is used when we are saving the current
6909 * session state to disk. This method is very similar to _shouldSaveTabState,
6910 * however, "about:blank" and "about:newtab" tabs will still be saved to disk.
6913 * The current tab state
6916 _shouldSaveTab: function ssi_shouldSaveTab(aTabState) {
6917 // If the tab has one of the following transient about: history entry, no
6918 // userTypedValue, and no customizemode attribute, then we don't actually
6919 // want to write this tab's data to disk.
6921 aTabState.userTypedValue ||
6922 (aTabState.attributes && aTabState.attributes.customizemode == "true") ||
6923 (aTabState.entries.length &&
6924 aTabState.entries[0].url != "about:privatebrowsing")
6929 * This is going to take a state as provided at startup (via
6930 * SessionStartup.state) and split it into 2 parts. The first part
6931 * (defaultState) will be a state that should still be restored at startup,
6932 * while the second part (state) is a state that should be saved for later.
6933 * defaultState will be comprised of windows with only pinned tabs, extracted
6934 * from a clone of startupState. It will also contain window position information.
6936 * defaultState will be restored at startup. state will be passed into
6937 * LastSession and will be kept in case the user explicitly wants
6938 * to restore the previous session (publicly exposed as restoreLastSession).
6941 * The startupState, presumably from SessionStartup.state
6942 * @returns [defaultState, state]
6944 _prepDataForDeferredRestore: function ssi_prepDataForDeferredRestore(
6947 // Make sure that we don't modify the global state as provided by
6948 // SessionStartup.state.
6949 let state = Cu.cloneInto(startupState, {});
6950 let hasPinnedTabs = false;
6951 let defaultState = { windows: [], selectedWindow: 1 };
6952 state.selectedWindow = state.selectedWindow || 1;
6954 // Look at each window, remove pinned tabs, adjust selectedindex,
6955 // remove window if necessary.
6956 for (let wIndex = 0; wIndex < state.windows.length; ) {
6957 let window = state.windows[wIndex];
6958 window.selected = window.selected || 1;
6959 // We're going to put the state of the window into this object, but for closedTabs
6960 // we want to preserve the original closedTabs since that will be saved as the lastSessionState
6961 let newWindowState = {
6964 if (PERSIST_SESSIONS) {
6965 newWindowState._closedTabs = Cu.cloneInto(window._closedTabs, {});
6966 newWindowState.closedGroups = Cu.cloneInto(window.closedGroups, {});
6968 // Open groups do not have a tabs list, but closed ones do — we need to
6970 window.groups?.forEach(group => {
6971 group = Cu.cloneInto(group, {});
6973 newWindowState.closedGroups.push(group);
6977 // We want to preserve the sidebar if previously open in the window
6978 if (window.sidebar) {
6979 newWindowState.sidebar = window.sidebar;
6982 for (let tIndex = 0; tIndex < window.tabs.length; ) {
6983 if (window.tabs[tIndex].pinned) {
6984 // Adjust window.selected
6985 if (tIndex + 1 < window.selected) {
6986 window.selected -= 1;
6987 } else if (tIndex + 1 == window.selected) {
6988 newWindowState.selected = newWindowState.tabs.length + 1;
6990 // + 1 because the tab isn't actually in the array yet
6992 // Now add the pinned tab to our window
6993 newWindowState.tabs = newWindowState.tabs.concat(
6994 window.tabs.splice(tIndex, 1)
6996 // We don't want to increment tIndex here.
6998 } else if (!window.tabs[tIndex].hidden && PERSIST_SESSIONS) {
6999 // Add any previously open tabs that aren't pinned or hidden to the recently closed tabs list
7000 // which we want to persist between sessions; if the session is manually restored, they will
7001 // be filtered out of the closed tabs list (due to removeAfterRestore property) and reopened
7002 // per expected session restore behavior.
7004 let tabState = window.tabs[tIndex];
7006 // Ensure the index is in bounds.
7007 let activeIndex = tabState.index;
7008 activeIndex = Math.min(activeIndex, tabState.entries.length - 1);
7009 activeIndex = Math.max(activeIndex, 0);
7011 if (activeIndex in tabState.entries) {
7013 tabState.entries[activeIndex].title ||
7014 tabState.entries[activeIndex].url;
7019 image: tabState.image,
7021 closedAt: Date.now(),
7022 closedInGroup: false,
7023 removeAfterRestore: true,
7026 if (this._shouldSaveTabState(tabState)) {
7027 let closedTabsList = newWindowState._closedTabs;
7028 let groupId = tabData.state.groupId;
7030 closedTabsList = newWindowState.closedGroups.find(
7031 g => g.id === groupId
7035 this.saveClosedTabData(window, closedTabsList, tabData, false);
7042 hasPinnedTabs ||= !!newWindowState.tabs.length;
7044 // Only transfer over window attributes for pinned tabs, which has
7045 // already been extracted into newWindowState.tabs.
7046 if (newWindowState.tabs.length) {
7047 WINDOW_ATTRIBUTES.forEach(function (attr) {
7048 if (attr in window) {
7049 newWindowState[attr] = window[attr];
7050 delete window[attr];
7053 // We're just copying position data into the window for pinned tabs.
7054 // Not copying over:
7059 // Assign a unique ID to correlate the window to be opened with the
7061 window.__lastSessionWindowID = newWindowState.__lastSessionWindowID =
7062 "" + Date.now() + Math.random();
7065 // If this newWindowState contains pinned tabs (stored in tabs) or
7066 // closed tabs, add it to the defaultState so they're available immediately.
7068 newWindowState.tabs.length ||
7069 (PERSIST_SESSIONS &&
7070 (newWindowState._closedTabs.length ||
7071 newWindowState.closedGroups.length))
7073 defaultState.windows.push(newWindowState);
7074 // Remove the window from the state if it doesn't have any tabs
7075 if (!window.tabs.length) {
7076 if (wIndex + 1 <= state.selectedWindow) {
7077 state.selectedWindow -= 1;
7078 } else if (wIndex + 1 == state.selectedWindow) {
7079 defaultState.selectedIndex = defaultState.windows.length + 1;
7082 state.windows.splice(wIndex, 1);
7083 // We don't want to increment wIndex here.
7090 if (hasPinnedTabs) {
7091 // Move cookies over from so that they're restored right away and pinned tabs will load correctly.
7092 defaultState.cookies = state.cookies;
7093 delete state.cookies;
7095 // we return state here rather than startupState so as to avoid duplicating
7096 // pinned tabs that we add to the defaultState (when a user restores a session)
7097 return [defaultState, state];
7100 _sendRestoreCompletedNotifications:
7101 function ssi_sendRestoreCompletedNotifications() {
7102 // not all windows restored, yet
7103 if (this._restoreCount > 1) {
7104 this._restoreCount--;
7106 `waiting on ${this._restoreCount} windows to be restored before sending restore complete notifications.`
7111 // observers were already notified
7112 if (this._restoreCount == -1) {
7116 // This was the last window restored at startup, notify observers.
7117 if (!this._browserSetState) {
7118 Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED);
7119 this._log.debug(`All ${this._restoreCount} windows restored`);
7120 this._deferredAllWindowsRestored.resolve();
7122 // _browserSetState is used only by tests, and it uses an alternate
7123 // notification in order not to retrigger startup observers that
7124 // are listening for NOTIFY_WINDOWS_RESTORED.
7125 Services.obs.notifyObservers(null, NOTIFY_BROWSER_STATE_RESTORED);
7128 this._browserSetState = false;
7129 this._restoreCount = -1;
7133 * Set the given window's busy state
7134 * @param aWindow the window
7135 * @param aValue the window's busy state
7137 _setWindowStateBusyValue: function ssi_changeWindowStateBusyValue(
7141 this._windows[aWindow.__SSi].busy = aValue;
7143 // Keep the to-be-restored state in sync because that is returned by
7144 // getWindowState() as long as the window isn't loaded, yet.
7145 if (!this._isWindowLoaded(aWindow)) {
7146 let stateToRestore =
7147 this._statesToRestore[WINDOW_RESTORE_IDS.get(aWindow)].windows[0];
7148 stateToRestore.busy = aValue;
7153 * Set the given window's state to 'not busy'.
7154 * @param aWindow the window
7156 _setWindowStateReady: function ssi_setWindowStateReady(aWindow) {
7157 let newCount = (this._windowBusyStates.get(aWindow) || 0) - 1;
7159 throw new Error("Invalid window busy state (less than zero).");
7161 this._windowBusyStates.set(aWindow, newCount);
7163 if (newCount == 0) {
7164 this._setWindowStateBusyValue(aWindow, false);
7165 this._sendWindowStateReadyEvent(aWindow);
7170 * Set the given window's state to 'busy'.
7171 * @param aWindow the window
7173 _setWindowStateBusy: function ssi_setWindowStateBusy(aWindow) {
7174 let newCount = (this._windowBusyStates.get(aWindow) || 0) + 1;
7175 this._windowBusyStates.set(aWindow, newCount);
7177 if (newCount == 1) {
7178 this._setWindowStateBusyValue(aWindow, true);
7179 this._sendWindowStateBusyEvent(aWindow);
7184 * Dispatch an SSWindowStateReady event for the given window.
7185 * @param aWindow the window
7187 _sendWindowStateReadyEvent: function ssi_sendWindowStateReadyEvent(aWindow) {
7188 let event = aWindow.document.createEvent("Events");
7189 event.initEvent("SSWindowStateReady", true, false);
7190 aWindow.dispatchEvent(event);
7194 * Dispatch an SSWindowStateBusy event for the given window.
7195 * @param aWindow the window
7197 _sendWindowStateBusyEvent: function ssi_sendWindowStateBusyEvent(aWindow) {
7198 let event = aWindow.document.createEvent("Events");
7199 event.initEvent("SSWindowStateBusy", true, false);
7200 aWindow.dispatchEvent(event);
7204 * Dispatch the SSWindowRestoring event for the given window.
7206 * The window which is going to be restored
7208 _sendWindowRestoringNotification(aWindow) {
7209 let event = aWindow.document.createEvent("Events");
7210 event.initEvent("SSWindowRestoring", true, false);
7211 aWindow.dispatchEvent(event);
7215 * Dispatch the SSWindowRestored event for the given window.
7217 * The window which has been restored
7219 _sendWindowRestoredNotification(aWindow) {
7220 let event = aWindow.document.createEvent("Events");
7221 event.initEvent("SSWindowRestored", true, false);
7222 aWindow.dispatchEvent(event);
7226 * Dispatch the SSTabRestored event for the given tab.
7228 * The tab which has been restored
7229 * @param aIsRemotenessUpdate
7230 * True if this tab was restored due to flip from running from
7231 * out-of-main-process to in-main-process or vice-versa.
7233 _sendTabRestoredNotification(aTab, aIsRemotenessUpdate) {
7234 let event = aTab.ownerDocument.createEvent("CustomEvent");
7235 event.initCustomEvent("SSTabRestored", true, false, {
7236 isRemotenessUpdate: aIsRemotenessUpdate,
7238 aTab.dispatchEvent(event);
7244 * @returns whether this window's data is still cached in _statesToRestore
7245 * because it's not fully loaded yet
7247 _isWindowLoaded: function ssi_isWindowLoaded(aWindow) {
7248 return !WINDOW_RESTORE_IDS.has(aWindow);
7252 * Resize this._closedWindows to the value of the pref, except in the case
7253 * where we don't have any non-popup windows on Windows and Linux. Then we must
7254 * resize such that we have at least one non-popup window.
7256 _capClosedWindows: function ssi_capClosedWindows() {
7257 if (this._closedWindows.length <= this._max_windows_undo) {
7260 let spliceTo = this._max_windows_undo;
7261 if (AppConstants.platform != "macosx") {
7262 let normalWindowIndex = 0;
7263 // try to find a non-popup window in this._closedWindows
7265 normalWindowIndex < this._closedWindows.length &&
7266 !!this._closedWindows[normalWindowIndex].isPopup
7268 normalWindowIndex++;
7270 if (normalWindowIndex >= this._max_windows_undo) {
7271 spliceTo = normalWindowIndex + 1;
7274 if (spliceTo < this._closedWindows.length) {
7275 this._closedWindows.splice(spliceTo, this._closedWindows.length);
7276 this._closedObjectsChanged = true;
7281 * Clears the set of windows that are "resurrected" before writing to disk to
7282 * make closing windows one after the other until shutdown work as expected.
7284 * This function should only be called when we are sure that there has been
7285 * a user action that indicates the browser is actively being used and all
7286 * windows that have been closed before are not part of a series of closing
7289 _clearRestoringWindows: function ssi_clearRestoringWindows() {
7290 for (let i = 0; i < this._closedWindows.length; i++) {
7291 delete this._closedWindows[i]._shouldRestore;
7296 * Reset state to prepare for a new session state to be restored.
7298 _resetRestoringState: function ssi_initRestoringState() {
7299 TabRestoreQueue.reset();
7300 this._tabsRestoringCount = 0;
7304 * Reset the restoring state for a particular tab. This will be called when
7305 * removing a tab or when a tab needs to be reset (it's being overwritten).
7308 * The tab that will be "reset"
7310 _resetLocalTabRestoringState(aTab) {
7311 let browser = aTab.linkedBrowser;
7313 // Keep the tab's previous state for later in this method
7314 let previousState = TAB_STATE_FOR_BROWSER.get(browser);
7316 if (!previousState) {
7317 console.error("Given tab is not restoring.");
7321 // The browser is no longer in any sort of restoring state.
7322 TAB_STATE_FOR_BROWSER.delete(browser);
7324 this._restoreListeners.get(browser.permanentKey)?.unregister();
7325 browser.browsingContext.clearRestoreState();
7327 aTab.removeAttribute("pending");
7329 if (previousState == TAB_STATE_RESTORING) {
7330 if (this._tabsRestoringCount) {
7331 this._tabsRestoringCount--;
7333 } else if (previousState == TAB_STATE_NEEDS_RESTORE) {
7334 // Make sure that the tab is removed from the list of tabs to restore.
7335 // Again, this is normally done in restoreTabContent, but that isn't being called
7337 TabRestoreQueue.remove(aTab);
7341 _resetTabRestoringState(tab) {
7342 let browser = tab.linkedBrowser;
7344 if (!TAB_STATE_FOR_BROWSER.has(browser)) {
7345 console.error("Given tab is not restoring.");
7349 this._resetLocalTabRestoringState(tab);
7353 * Each fresh tab starts out with epoch=0. This function can be used to
7354 * start a next epoch by incrementing the current value. It will enables us
7355 * to ignore stale messages sent from previous epochs. The function returns
7356 * the new epoch ID for the given |browser|.
7358 startNextEpoch(permanentKey) {
7359 let next = this.getCurrentEpoch(permanentKey) + 1;
7360 this._browserEpochs.set(permanentKey, next);
7365 * Returns the current epoch for the given <browser>. If we haven't assigned
7366 * a new epoch this will default to zero for new tabs.
7368 getCurrentEpoch(permanentKey) {
7369 return this._browserEpochs.get(permanentKey) || 0;
7373 * Each time a <browser> element is restored, we increment its "epoch". To
7374 * check if a message from content-sessionStore.js is out of date, we can
7375 * compare the epoch received with the message to the <browser> element's
7376 * epoch. This function does that, and returns true if |epoch| is up-to-date
7377 * with respect to |browser|.
7379 isCurrentEpoch(permanentKey, epoch) {
7380 return this.getCurrentEpoch(permanentKey) == epoch;
7384 * Resets the epoch for a given <browser>. We need to this every time we
7385 * receive a hint that a new docShell has been loaded into the browser as
7386 * the frame script starts out with epoch=0.
7388 resetEpoch(permanentKey, frameLoader = null) {
7389 this._browserEpochs.delete(permanentKey);
7391 frameLoader.requestEpochUpdate(0);
7396 * Countdown for a given duration, skipping beats if the computer is too busy,
7397 * sleeping or otherwise unavailable.
7399 * @param {number} delay An approximate delay to wait in milliseconds (rounded
7400 * up to the closest second).
7405 let DELAY_BEAT = 1000;
7406 let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
7407 let beats = Math.ceil(delay / DELAY_BEAT);
7408 let deferred = Promise.withResolvers();
7409 timer.initWithCallback(
7417 Ci.nsITimer.TYPE_REPEATING_PRECISE_CAN_SKIP
7419 // Ensure that the timer is both canceled once we are done with it
7420 // and not garbage-collected until then.
7421 deferred.promise.then(
7422 () => timer.cancel(),
7423 () => timer.cancel()
7428 _waitForStateStop(browser, expectedURL = null) {
7429 const deferred = Promise.withResolvers();
7432 unregister(reject = true) {
7437 SessionStoreInternal._restoreListeners.delete(browser.permanentKey);
7440 browser.removeProgressListener(
7442 Ci.nsIWebProgress.NOTIFY_STATE_WINDOW
7444 } catch {} // May have already gotten rid of the browser's webProgress.
7447 onStateChange(webProgress, request, stateFlags) {
7449 webProgress.isTopLevel &&
7450 stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW &&
7451 stateFlags & Ci.nsIWebProgressListener.STATE_STOP
7453 // FIXME: We sometimes see spurious STATE_STOP events for about:blank
7454 // loads, so we have to account for that here.
7455 let aboutBlankOK = !expectedURL || expectedURL === "about:blank";
7456 let url = request.QueryInterface(Ci.nsIChannel).originalURI.spec;
7457 if (url !== "about:blank" || aboutBlankOK) {
7458 this.unregister(false);
7464 QueryInterface: ChromeUtils.generateQI([
7465 "nsIWebProgressListener",
7466 "nsISupportsWeakReference",
7470 this._restoreListeners.get(browser.permanentKey)?.unregister();
7471 this._restoreListeners.set(browser.permanentKey, listener);
7473 browser.addProgressListener(
7475 Ci.nsIWebProgress.NOTIFY_STATE_WINDOW
7478 return deferred.promise;
7481 _listenForNavigations(browser, callbacks) {
7484 browser.browsingContext?.sessionHistory?.removeSHistoryListener(this);
7487 browser.removeProgressListener(
7489 Ci.nsIWebProgress.NOTIFY_STATE_WINDOW
7491 } catch {} // May have already gotten rid of the browser's webProgress.
7493 SessionStoreInternal._restoreListeners.delete(browser.permanentKey);
7498 return callbacks.onHistoryReload();
7501 // TODO(kashav): ContentRestore.sys.mjs handles OnHistoryNewEntry
7502 // separately, so we should eventually support that here as well.
7503 OnHistoryNewEntry() {},
7504 OnHistoryGotoIndex() {},
7505 OnHistoryPurge() {},
7506 OnHistoryReplaceEntry() {},
7508 onStateChange(webProgress, request, stateFlags) {
7510 webProgress.isTopLevel &&
7511 stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW &&
7512 stateFlags & Ci.nsIWebProgressListener.STATE_START
7515 callbacks.onStartRequest();
7519 QueryInterface: ChromeUtils.generateQI([
7520 "nsISHistoryListener",
7521 "nsIWebProgressListener",
7522 "nsISupportsWeakReference",
7526 this._restoreListeners.get(browser.permanentKey)?.unregister();
7527 this._restoreListeners.set(browser.permanentKey, listener);
7529 browser.browsingContext?.sessionHistory?.addSHistoryListener(listener);
7531 browser.addProgressListener(
7533 Ci.nsIWebProgress.NOTIFY_STATE_WINDOW
7538 * This mirrors ContentRestore.restoreHistory() for parent process session
7541 _restoreHistory(browser, data) {
7542 this._tabStateToRestore.set(browser.permanentKey, data);
7544 // In case about:blank isn't done yet.
7545 // XXX(kashav): Does this actually accomplish anything? Can we remove?
7548 lazy.SessionHistory.restoreFromParent(
7549 browser.browsingContext.sessionHistory,
7553 let url = data.tabData?.entries[data.tabData.index - 1]?.url;
7554 let disallow = data.tabData?.disallow;
7556 let promise = SessionStoreUtils.restoreDocShellState(
7557 browser.browsingContext,
7561 this._tabStateRestorePromises.set(browser.permanentKey, promise);
7563 const onResolve = () => {
7564 if (TAB_STATE_FOR_BROWSER.get(browser) !== TAB_STATE_RESTORING) {
7565 this._listenForNavigations(browser, {
7566 // The history entry was reloaded before we began restoring tab
7567 // content, just proceed as we would normally.
7568 onHistoryReload: () => {
7569 this._restoreTabContent(browser);
7573 // Some foreign code, like an extension, loaded a new URI on the
7574 // browser. We no longer want to restore saved tab data, but may
7575 // still have browser state that needs to be restored.
7576 onStartRequest: () => {
7577 this._tabStateToRestore.delete(browser.permanentKey);
7578 this._restoreTabContent(browser);
7583 this._tabStateRestorePromises.delete(browser.permanentKey);
7585 this._restoreHistoryComplete(browser);
7588 promise.then(onResolve).catch(() => {});
7592 * Either load the saved typed value or restore the active history entry.
7593 * If neither is possible, just load an empty document.
7595 _restoreTabEntry(browser, tabData) {
7596 let haveUserTypedValue = tabData.userTypedValue && tabData.userTypedClear;
7597 // First take care of the common case where we load the history entry.
7598 if (!haveUserTypedValue && tabData.entries.length) {
7599 return SessionStoreUtils.initializeRestore(
7600 browser.browsingContext,
7601 lazy.SessionStoreHelper.buildRestoreData(
7607 // Here, we need to load user data or about:blank instead.
7608 // As it's user-typed (or blank), it gets system triggering principal:
7609 let triggeringPrincipal =
7610 Services.scriptSecurityManager.getSystemPrincipal();
7611 // Bypass all the fixup goop for about:blank:
7612 if (!haveUserTypedValue) {
7613 let blankPromise = this._waitForStateStop(browser, "about:blank");
7614 browser.browsingContext.loadURI(lazy.blankURI, {
7615 triggeringPrincipal,
7616 loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY,
7618 return blankPromise;
7621 // We have a user typed value, load that with fixup:
7622 let loadPromise = this._waitForStateStop(browser, tabData.userTypedValue);
7623 browser.browsingContext.fixupAndLoadURIString(tabData.userTypedValue, {
7624 loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP,
7625 triggeringPrincipal,
7632 * This mirrors ContentRestore.restoreTabContent() for parent process session
7635 _restoreTabContent(browser, options = {}) {
7636 this._restoreListeners.get(browser.permanentKey)?.unregister();
7638 this._restoreTabContentStarted(browser, options);
7640 let state = this._tabStateToRestore.get(browser.permanentKey);
7641 this._tabStateToRestore.delete(browser.permanentKey);
7643 let promises = [this._tabStateRestorePromises.get(browser.permanentKey)];
7646 promises.push(this._restoreTabEntry(browser, state.tabData));
7648 // The browser started another load, so we decided to not restore
7649 // saved tab data. We should still wait for that new load to finish
7650 // before proceeding.
7651 promises.push(this._waitForStateStop(browser));
7654 Promise.allSettled(promises).then(() => {
7655 this._restoreTabContentComplete(browser, options);
7659 _sendRestoreTabContent(browser, options) {
7660 this._restoreTabContent(browser, options);
7663 _restoreHistoryComplete(browser) {
7664 let win = browser.ownerGlobal;
7665 let tab = win?.gBrowser.getTabForBrowser(browser);
7670 // Notify the tabbrowser that the tab chrome has been restored.
7671 let tabData = lazy.TabState.collect(tab, TAB_CUSTOM_VALUES.get(tab));
7673 // Update tab label and icon again after the tab history was updated.
7674 this.updateTabLabelAndIcon(tab, tabData);
7676 let event = win.document.createEvent("Events");
7677 event.initEvent("SSTabRestoring", true, false);
7678 tab.dispatchEvent(event);
7681 _restoreTabContentStarted(browser, data) {
7682 let win = browser.ownerGlobal;
7683 let tab = win?.gBrowser.getTabForBrowser(browser);
7688 let initiatedBySessionStore =
7689 TAB_STATE_FOR_BROWSER.get(browser) != TAB_STATE_NEEDS_RESTORE;
7690 let isNavigateAndRestore =
7691 data.reason == RESTORE_TAB_CONTENT_REASON.NAVIGATE_AND_RESTORE;
7693 // We need to be careful when restoring the urlbar's search mode because
7694 // we race a call to gURLBar.setURI due to the location change. setURI
7695 // will exit search mode and set gURLBar.value to the restored URL,
7696 // clobbering any search mode and userTypedValue we restore here. If
7697 // this is a typical restore -- restoring on startup or restoring a
7698 // closed tab for example -- then we need to restore search mode after
7699 // that setURI call, and so we wait until restoreTabContentComplete, at
7700 // which point setURI will have been called. If this is not a typical
7701 // restore -- it was not initiated by session store or it's due to a
7702 // remoteness change -- then we do not want to restore search mode at
7703 // all, and so we remove it from the tab state cache. In particular, if
7704 // the restore is due to a remoteness change, then the user is loading a
7705 // new URL and the current search mode should not be carried over to it.
7706 let cacheState = lazy.TabStateCache.get(browser.permanentKey);
7707 if (cacheState.searchMode) {
7708 if (!initiatedBySessionStore || isNavigateAndRestore) {
7709 lazy.TabStateCache.update(browser.permanentKey, {
7711 userTypedValue: null,
7717 if (!initiatedBySessionStore) {
7718 // If a load not initiated by sessionstore was started in a
7719 // previously pending tab. Mark the tab as no longer pending.
7720 this.markTabAsRestoring(tab);
7721 } else if (!isNavigateAndRestore) {
7722 // If the user was typing into the URL bar when we crashed, but hadn't hit
7723 // enter yet, then we just need to write that value to the URL bar without
7724 // loading anything. This must happen after the load, as the load will clear
7727 // Note that we only want to do that if we're restoring state for reasons
7728 // _other_ than a navigateAndRestore remoteness-flip, as such a flip
7729 // implies that the user was navigating.
7730 let tabData = lazy.TabState.collect(tab, TAB_CUSTOM_VALUES.get(tab));
7732 tabData.userTypedValue &&
7733 !tabData.userTypedClear &&
7734 !browser.userTypedValue
7736 browser.userTypedValue = tabData.userTypedValue;
7738 win.gURLBar.setURI();
7742 // Remove state we don't need any longer.
7743 lazy.TabStateCache.update(browser.permanentKey, {
7744 userTypedValue: null,
7745 userTypedClear: null,
7750 _restoreTabContentComplete(browser, data) {
7751 let win = browser.ownerGlobal;
7752 let tab = browser.ownerGlobal?.gBrowser.getTabForBrowser(browser);
7756 // Restore search mode and its search string in userTypedValue, if
7758 let cacheState = lazy.TabStateCache.get(browser.permanentKey);
7759 if (cacheState.searchMode) {
7760 win.gURLBar.setSearchMode(cacheState.searchMode, browser);
7761 browser.userTypedValue = cacheState.userTypedValue;
7763 win.gURLBar.setURI();
7765 lazy.TabStateCache.update(browser.permanentKey, {
7767 userTypedValue: null,
7771 // This callback is used exclusively by tests that want to
7772 // monitor the progress of network loads.
7773 if (gDebuggingEnabled) {
7774 Services.obs.notifyObservers(browser, NOTIFY_TAB_RESTORED);
7777 SessionStoreInternal._resetLocalTabRestoringState(tab);
7778 SessionStoreInternal.restoreNextTab();
7780 this._sendTabRestoredNotification(tab, data.isRemotenessUpdate);
7782 Services.obs.notifyObservers(null, "sessionstore-one-or-no-tab-restored");
7786 * Send the "SessionStore:restoreHistory" message to content, triggering a
7787 * content restore. This method is intended to be used internally by
7788 * SessionStore, as it also ensures that permissions are avaliable in the
7789 * content process before triggering the history restore in the content
7792 * @param browser The browser to transmit the permissions for
7793 * @param options The options data to send to content.
7795 _sendRestoreHistory(browser, options) {
7796 if (options.tabData.storage) {
7797 SessionStoreUtils.restoreSessionStorageFromParent(
7798 browser.browsingContext,
7799 options.tabData.storage
7801 delete options.tabData.storage;
7804 this._restoreHistory(browser, options);
7806 if (browser && browser.frameLoader) {
7807 browser.frameLoader.requestEpochUpdate(options.epoch);
7812 * @param {MozTabbrowserTabGroup} tabGroup
7814 addSavedTabGroup(tabGroup) {
7815 let tabGroupState = lazy.TabGroupState.savedInOpenWindow(
7817 tabGroup.ownerGlobal.__SSi
7819 tabGroupState.tabs = this._collectClosedTabsForTabGroup(
7821 tabGroup.ownerGlobal
7823 this._recordSavedTabGroupState(tabGroupState);
7827 * @param {SavedTabGroupStateData} savedTabGroupState
7830 _recordSavedTabGroupState(savedTabGroupState) {
7832 !savedTabGroupState.tabs.length ||
7833 this.getSavedTabGroup(savedTabGroupState.id)
7837 this._savedGroups.push(savedTabGroupState);
7841 * @param {string} tabGroupId
7842 * @returns {SavedTabGroupStateData|undefined}
7844 getSavedTabGroup(tabGroupId) {
7845 return this._savedGroups.find(
7846 savedTabGroup => savedTabGroup.id == tabGroupId
7851 * Returns all tab groups that were saved in this session.
7852 * @returns {SavedTabGroupStateData[]}
7854 getSavedTabGroups() {
7855 return Cu.cloneInto(this._savedGroups, {});
7859 * @param {Window|{sourceWindowId: string}|{sourceClosedId: number}} source
7860 * @param {string} tabGroupId
7861 * @returns {ClosedTabGroupStateData|undefined}
7863 getClosedTabGroup(source, tabGroupId) {
7864 let winData = this._resolveClosedDataSource(source);
7865 return winData?.closedGroups.find(
7866 closedGroup => closedGroup.id == tabGroupId
7871 * @param {Window|Object} source
7872 * @param {string} tabGroupId
7873 * @param {Window} [targetWindow]
7874 * @returns {MozTabbrowserTabGroup}
7876 undoCloseTabGroup(source, tabGroupId, targetWindow) {
7877 const sourceWinData = this._resolveClosedDataSource(source);
7878 const isPrivateSource = Boolean(sourceWinData.isPrivate);
7879 if (targetWindow && !targetWindow.__SSi) {
7880 throw Components.Exception(
7881 "Target window is not tracked",
7882 Cr.NS_ERROR_INVALID_ARG
7884 } else if (!targetWindow) {
7885 targetWindow = this._getTopWindow(isPrivateSource);
7888 isPrivateSource !== PrivateBrowsingUtils.isWindowPrivate(targetWindow)
7890 throw Components.Exception(
7891 "Target window doesn't have the same privateness as the source window",
7892 Cr.NS_ERROR_INVALID_ARG
7896 let tabGroupData = this.getClosedTabGroup(source, tabGroupId);
7897 if (!tabGroupData) {
7898 throw Components.Exception(
7899 "Tab group not found in source",
7900 Cr.NS_ERROR_INVALID_ARG
7904 let group = this._createTabsForSavedOrClosedTabGroup(
7908 this.forgetClosedTabGroup(source, tabGroupId);
7909 sourceWinData.lastClosedTabGroupId = null;
7916 * @param {string} tabGroupId
7917 * @param {Window} [targetWindow]
7918 * @returns {MozTabbrowserTabGroup}
7920 openSavedTabGroup(tabGroupId, targetWindow) {
7921 if (!targetWindow) {
7922 targetWindow = this._getTopWindow();
7924 if (!targetWindow.__SSi) {
7925 throw Components.Exception(
7926 "Target window is not tracked",
7927 Cr.NS_ERROR_INVALID_ARG
7930 if (PrivateBrowsingUtils.isWindowPrivate(targetWindow)) {
7931 throw Components.Exception(
7932 "Cannot open a saved tab group in a private window",
7933 Cr.NS_ERROR_INVALID_ARG
7937 let tabGroupData = this.getSavedTabGroup(tabGroupId);
7938 if (!tabGroupData) {
7939 throw Components.Exception(
7940 "No saved tab group with specified id",
7941 Cr.NS_ERROR_INVALID_ARG
7945 // If this saved tab group is present in a closed window, then we need to
7946 // remove references to this saved tab group from that closed window. The
7947 // result should be as if the saved tab group "moved" from the closed window
7948 // into the `targetWindow`.
7949 if (tabGroupData.windowClosedId) {
7950 let closedWinData = this.getClosedWindowDataByClosedId(
7951 tabGroupData.windowClosedId
7953 if (closedWinData) {
7954 this._removeSavedTabGroupFromClosedWindow(
7961 let group = this._createTabsForSavedOrClosedTabGroup(
7965 this.forgetSavedTabGroup(tabGroupId);
7972 * @param {ClosedTabGroupStateData|SavedTabGroupStateData} tabGroupData
7973 * @param {Window} targetWindow
7974 * @returns {MozTabbrowserTabGroup}
7976 _createTabsForSavedOrClosedTabGroup(tabGroupData, targetWindow) {
7977 let tabDataList = tabGroupData.tabs.map(tab => tab.state);
7978 let tabs = targetWindow.gBrowser.createTabsForSessionRestore(
7980 0, // TODO Bug 1933113 - Save tab group position and selected tab with saved tab group data
7985 this.restoreTabs(targetWindow, tabs, tabDataList, 0);
7986 return tabs[0].group;
7990 * Remove tab groups from the closedGroups list that have no tabs associated
7993 * This can sometimes happen because tab groups are immediately
7994 * added to closedGroups on closing, before the complete history of the tabs
7995 * within the group have been processed. If it is later determined that none
7996 * of the tabs in the group were "worth saving", the group will be empty.
7997 * This can also happen if a user "undoes" the last closed tab in a closed tab
8000 * See: bug1933966, bug1933485
8002 * @param {WindowStateData} winData
8004 _cleanupOrphanedClosedGroups(winData) {
8005 for (let index = winData.closedGroups.length - 1; index >= 0; index--) {
8006 if (winData.closedGroups[index].tabs.length === 0) {
8007 winData.closedGroups.splice(index, 1);
8008 this._closedObjectsChanged = true;
8014 * @param {WindowStateData} closedWinData
8015 * @param {string} tabGroupId
8016 * @returns {void} modifies the data in argument `closedWinData`
8018 _removeSavedTabGroupFromClosedWindow(closedWinData, tabGroupId) {
8019 removeWhere(closedWinData.groups, tabGroup => tabGroup.id == tabGroupId);
8020 removeWhere(closedWinData.tabs, tab => tab.groupId == tabGroupId);
8021 this._closedObjectsChanged = true;
8026 * Priority queue that keeps track of a list of tabs to restore and returns
8027 * the tab we should restore next, based on priority rules. We decide between
8028 * pinned, visible and hidden tabs in that and FIFO order. Hidden tabs are only
8029 * restored with restore_hidden_tabs=true.
8031 var TabRestoreQueue = {
8032 // The separate buckets used to store tabs.
8033 tabs: { priority: [], visible: [], hidden: [] },
8035 // Preferences used by the TabRestoreQueue to determine which tabs
8036 // are restored automatically and which tabs will be on-demand.
8038 // Lazy getter that returns whether tabs are restored on demand.
8039 get restoreOnDemand() {
8040 let updateValue = () => {
8041 let value = Services.prefs.getBoolPref(PREF);
8042 let definition = { value, configurable: true };
8043 Object.defineProperty(this, "restoreOnDemand", definition);
8047 const PREF = "browser.sessionstore.restore_on_demand";
8048 Services.prefs.addObserver(PREF, updateValue);
8049 return updateValue();
8052 // Lazy getter that returns whether pinned tabs are restored on demand.
8053 get restorePinnedTabsOnDemand() {
8054 let updateValue = () => {
8055 let value = Services.prefs.getBoolPref(PREF);
8056 let definition = { value, configurable: true };
8057 Object.defineProperty(this, "restorePinnedTabsOnDemand", definition);
8061 const PREF = "browser.sessionstore.restore_pinned_tabs_on_demand";
8062 Services.prefs.addObserver(PREF, updateValue);
8063 return updateValue();
8066 // Lazy getter that returns whether we should restore hidden tabs.
8067 get restoreHiddenTabs() {
8068 let updateValue = () => {
8069 let value = Services.prefs.getBoolPref(PREF);
8070 let definition = { value, configurable: true };
8071 Object.defineProperty(this, "restoreHiddenTabs", definition);
8075 const PREF = "browser.sessionstore.restore_hidden_tabs";
8076 Services.prefs.addObserver(PREF, updateValue);
8077 return updateValue();
8081 // Resets the queue and removes all tabs.
8083 this.tabs = { priority: [], visible: [], hidden: [] };
8086 // Adds a tab to the queue and determines its priority bucket.
8088 let { priority, hidden, visible } = this.tabs;
8092 } else if (tab.hidden) {
8099 // Removes a given tab from the queue, if it's in there.
8101 let { priority, hidden, visible } = this.tabs;
8103 // We'll always check priority first since we don't
8104 // have an indicator if a tab will be there or not.
8106 let index = set.indexOf(tab);
8109 set = tab.hidden ? hidden : visible;
8110 index = set.indexOf(tab);
8114 set.splice(index, 1);
8118 // Returns and removes the tab with the highest priority.
8121 let { priority, hidden, visible } = this.tabs;
8123 let { restoreOnDemand, restorePinnedTabsOnDemand } = this.prefs;
8124 let restorePinned = !(restoreOnDemand && restorePinnedTabsOnDemand);
8125 if (restorePinned && priority.length) {
8127 } else if (!restoreOnDemand) {
8128 if (visible.length) {
8130 } else if (this.prefs.restoreHiddenTabs && hidden.length) {
8135 return set && set.shift();
8138 // Moves a given tab from the 'hidden' to the 'visible' bucket.
8139 hiddenToVisible(tab) {
8140 let { hidden, visible } = this.tabs;
8141 let index = hidden.indexOf(tab);
8144 hidden.splice(index, 1);
8149 // Moves a given tab from the 'visible' to the 'hidden' bucket.
8150 visibleToHidden(tab) {
8151 let { visible, hidden } = this.tabs;
8152 let index = visible.indexOf(tab);
8155 visible.splice(index, 1);
8161 * Returns true if the passed tab is in one of the sets that we're
8162 * restoring content in automatically.
8164 * @param tab (<xul:tab>)
8168 willRestoreSoon(tab) {
8169 let { priority, hidden, visible } = this.tabs;
8170 let { restoreOnDemand, restorePinnedTabsOnDemand, restoreHiddenTabs } =
8172 let restorePinned = !(restoreOnDemand && restorePinnedTabsOnDemand);
8173 let candidateSet = [];
8175 if (restorePinned && priority.length) {
8176 candidateSet.push(...priority);
8179 if (!restoreOnDemand) {
8180 if (visible.length) {
8181 candidateSet.push(...visible);
8184 if (restoreHiddenTabs && hidden.length) {
8185 candidateSet.push(...hidden);
8189 return candidateSet.indexOf(tab) > -1;
8193 // A map storing a closed window's state data until it goes aways (is GC'ed).
8194 // This ensures that API clients can still read (but not write) states of
8195 // windows they still hold a reference to but we don't.
8196 var DyingWindowCache = {
8197 _data: new WeakMap(),
8200 return this._data.has(window);
8204 return this._data.get(window);
8208 this._data.set(window, data);
8212 this._data.delete(window);
8216 // A weak set of dirty windows. We use it to determine which windows we need to
8217 // recollect data for when getCurrentState() is called.
8218 var DirtyWindows = {
8219 _data: new WeakMap(),
8222 return this._data.has(window);
8226 return this._data.set(window, true);
8230 this._data.delete(window);
8234 this._data = new WeakMap();
8238 // The state from the previous session (after restoring pinned tabs). This
8239 // state is persisted and passed through to the next session during an app
8240 // restart to make the third party add-on warning not trash the deferred
8246 return !!this._state;
8254 this._state = state;
8257 clear(silent = false) {
8261 Services.obs.notifyObservers(null, NOTIFY_LAST_SESSION_CLEARED);
8269 * @param {T[]} array
8270 * @param {function(T):boolean} predicate
8272 function removeWhere(array, predicate) {
8273 for (let i = array.length - 1; i >= 0; i--) {
8274 if (predicate(array[i])) {
8280 // Exposed for tests
8281 export const _LastSession = LastSession;