SessionStore.sys.mjs (297557B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 // Current version of the format used by Session Restore. 6 const FORMAT_VERSION = 1; 7 8 const PERSIST_SESSIONS = Services.prefs.getBoolPref( 9 "browser.sessionstore.persist_closed_tabs_between_sessions" 10 ); 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(); 20 21 // A new window has just been restored. At this stage, tabs are generally 22 // not restored. 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_LAST_SESSION_RE_ENABLED = "sessionstore-last-session-re-enable"; 28 const NOTIFY_RESTORING_ON_STARTUP = "sessionstore-restoring-on-startup"; 29 const NOTIFY_INITIATING_MANUAL_RESTORE = 30 "sessionstore-initiating-manual-restore"; 31 const NOTIFY_CLOSED_OBJECTS_CHANGED = "sessionstore-closed-objects-changed"; 32 const NOTIFY_SAVED_TAB_GROUPS_CHANGED = "sessionstore-saved-tab-groups-changed"; 33 34 const NOTIFY_TAB_RESTORED = "sessionstore-debug-tab-restored"; // WARNING: debug-only 35 const NOTIFY_DOMWINDOWCLOSED_HANDLED = 36 "sessionstore-debug-domwindowclosed-handled"; // WARNING: debug-only 37 38 const NOTIFY_BROWSER_SHUTDOWN_FLUSH = "sessionstore-browser-shutdown-flush"; 39 40 // Maximum number of tabs to restore simultaneously. Previously controlled by 41 // the browser.sessionstore.max_concurrent_tabs pref. 42 const MAX_CONCURRENT_TAB_RESTORES = 3; 43 44 // Minimum amount (in CSS px) by which we allow window edges to be off-screen 45 // when restoring a window, before we override the saved position to pull the 46 // window back within the available screen area. 47 const MIN_SCREEN_EDGE_SLOP = 8; 48 49 // global notifications observed 50 const OBSERVING = [ 51 "browser-window-before-show", 52 "domwindowclosed", 53 "quit-application-granted", 54 "browser-lastwindow-close-granted", 55 "quit-application", 56 "browser:purge-session-history", 57 "browser:purge-session-history-for-domain", 58 "idle-daily", 59 "clear-origin-attributes-data", 60 "browsing-context-did-set-embedder", 61 "browsing-context-discarded", 62 "browser-shutdown-tabstate-updated", 63 ]; 64 65 // XUL Window properties to (re)store 66 // Restored in restoreDimensions() 67 const WINDOW_ATTRIBUTES = ["width", "height", "screenX", "screenY", "sizemode"]; 68 69 const CHROME_FLAGS_MAP = [ 70 [Ci.nsIWebBrowserChrome.CHROME_TITLEBAR, "titlebar"], 71 [Ci.nsIWebBrowserChrome.CHROME_WINDOW_CLOSE, "close"], 72 [Ci.nsIWebBrowserChrome.CHROME_TOOLBAR, "toolbar"], 73 [Ci.nsIWebBrowserChrome.CHROME_LOCATIONBAR, "location"], 74 [Ci.nsIWebBrowserChrome.CHROME_PERSONAL_TOOLBAR, "personalbar"], 75 [Ci.nsIWebBrowserChrome.CHROME_STATUSBAR, "status"], 76 [Ci.nsIWebBrowserChrome.CHROME_MENUBAR, "menubar"], 77 [Ci.nsIWebBrowserChrome.CHROME_WINDOW_RESIZE, "resizable"], 78 [Ci.nsIWebBrowserChrome.CHROME_WINDOW_MINIMIZE, "minimizable"], 79 [Ci.nsIWebBrowserChrome.CHROME_SCROLLBARS, "", "scrollbars=0"], 80 [Ci.nsIWebBrowserChrome.CHROME_PRIVATE_WINDOW, "private"], 81 [Ci.nsIWebBrowserChrome.CHROME_NON_PRIVATE_WINDOW, "non-private"], 82 // Do not inherit remoteness and fissionness from the previous session. 83 //[Ci.nsIWebBrowserChrome.CHROME_REMOTE_WINDOW, "remote", "non-remote"], 84 //[Ci.nsIWebBrowserChrome.CHROME_FISSION_WINDOW, "fission", "non-fission"], 85 // "chrome" and "suppressanimation" are always set. 86 //[Ci.nsIWebBrowserChrome.CHROME_SUPPRESS_ANIMATION, "suppressanimation"], 87 [Ci.nsIWebBrowserChrome.CHROME_ALWAYS_ON_TOP, "alwaysontop"], 88 //[Ci.nsIWebBrowserChrome.CHROME_OPENAS_CHROME, "chrome", "chrome=0"], 89 [Ci.nsIWebBrowserChrome.CHROME_EXTRA, "extrachrome"], 90 [Ci.nsIWebBrowserChrome.CHROME_CENTER_SCREEN, "centerscreen"], 91 [Ci.nsIWebBrowserChrome.CHROME_DEPENDENT, "dependent"], 92 [Ci.nsIWebBrowserChrome.CHROME_MODAL, "modal"], 93 [Ci.nsIWebBrowserChrome.CHROME_OPENAS_DIALOG, "dialog", "dialog=0"], 94 ]; 95 96 // Hideable window features to (re)store 97 // Restored in restoreWindowFeatures() 98 const WINDOW_HIDEABLE_FEATURES = [ 99 "menubar", 100 "toolbar", 101 "locationbar", 102 "personalbar", 103 "statusbar", 104 "scrollbars", 105 ]; 106 107 const WINDOW_OPEN_FEATURES_MAP = { 108 locationbar: "location", 109 statusbar: "status", 110 }; 111 112 // These are tab events that we listen to. 113 const TAB_EVENTS = [ 114 "TabOpen", 115 "TabBrowserInserted", 116 "TabClose", 117 "TabSelect", 118 "TabShow", 119 "TabHide", 120 "TabPinned", 121 "TabUnpinned", 122 "TabGroupCreate", 123 "TabGroupRemoveRequested", 124 "TabGroupRemoved", 125 "TabGrouped", 126 "TabUngrouped", 127 "TabGroupCollapse", 128 "TabGroupExpand", 129 "TabSplitViewActivate", 130 ]; 131 132 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; 133 134 /** 135 * When calling restoreTabContent, we can supply a reason why 136 * the content is being restored. These are those reasons. 137 */ 138 const RESTORE_TAB_CONTENT_REASON = { 139 /** 140 * SET_STATE: 141 * We're restoring this tab's content because we're setting 142 * state inside this browser tab, probably because the user 143 * has asked us to restore a tab (or window, or entire session). 144 */ 145 SET_STATE: 0, 146 /** 147 * NAVIGATE_AND_RESTORE: 148 * We're restoring this tab's content because a navigation caused 149 * us to do a remoteness-flip. 150 */ 151 NAVIGATE_AND_RESTORE: 1, 152 }; 153 154 // 'browser.startup.page' preference value to resume the previous session. 155 const BROWSER_STARTUP_RESUME_SESSION = 3; 156 157 // Used by SessionHistoryListener. 158 const kNoIndex = Number.MAX_SAFE_INTEGER; 159 const kLastIndex = Number.MAX_SAFE_INTEGER - 1; 160 161 import { PrivateBrowsingUtils } from "resource://gre/modules/PrivateBrowsingUtils.sys.mjs"; 162 163 import { TabMetrics } from "moz-src:///browser/components/tabbrowser/TabMetrics.sys.mjs"; 164 import { TelemetryTimestamps } from "resource://gre/modules/TelemetryTimestamps.sys.mjs"; 165 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 166 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; 167 import { GlobalState } from "resource:///modules/sessionstore/GlobalState.sys.mjs"; 168 169 const lazy = {}; 170 171 XPCOMUtils.defineLazyServiceGetters(lazy, { 172 gScreenManager: ["@mozilla.org/gfx/screenmanager;1", Ci.nsIScreenManager], 173 }); 174 175 ChromeUtils.defineESModuleGetters(lazy, { 176 AIWindow: 177 "moz-src:///browser/components/aiwindow/ui/modules/AIWindow.sys.mjs", 178 AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs", 179 BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", 180 DevToolsShim: "chrome://devtools-startup/content/DevToolsShim.sys.mjs", 181 E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs", 182 HomePage: "resource:///modules/HomePage.sys.mjs", 183 JsonSchema: "resource://gre/modules/JsonSchema.sys.mjs", 184 PrivacyFilter: "resource://gre/modules/sessionstore/PrivacyFilter.sys.mjs", 185 sessionStoreLogger: "resource:///modules/sessionstore/SessionLogger.sys.mjs", 186 RunState: "resource:///modules/sessionstore/RunState.sys.mjs", 187 SessionCookies: "resource:///modules/sessionstore/SessionCookies.sys.mjs", 188 SessionFile: "resource:///modules/sessionstore/SessionFile.sys.mjs", 189 SessionHistory: "resource://gre/modules/sessionstore/SessionHistory.sys.mjs", 190 SessionSaver: "resource:///modules/sessionstore/SessionSaver.sys.mjs", 191 SessionStartup: "resource:///modules/sessionstore/SessionStartup.sys.mjs", 192 SessionStoreHelper: 193 "resource://gre/modules/sessionstore/SessionStoreHelper.sys.mjs", 194 TabAttributes: "resource:///modules/sessionstore/TabAttributes.sys.mjs", 195 TabCrashHandler: "resource:///modules/ContentCrashHandlers.sys.mjs", 196 TabGroupState: "resource:///modules/sessionstore/TabGroupState.sys.mjs", 197 TabState: "resource:///modules/sessionstore/TabState.sys.mjs", 198 TabStateCache: "resource:///modules/sessionstore/TabStateCache.sys.mjs", 199 TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs", 200 setTimeout: "resource://gre/modules/Timer.sys.mjs", 201 }); 202 203 ChromeUtils.defineLazyGetter(lazy, "blankURI", () => { 204 return Services.io.newURI("about:blank"); 205 }); 206 207 XPCOMUtils.defineLazyPreferenceGetter( 208 lazy, 209 "gRestoreWindowsToVirtualDesktop", 210 "browser.sessionstore.restore_windows_to_virtual_desktop" 211 ); 212 213 /** 214 * |true| if we are in debug mode, |false| otherwise. 215 * Debug mode is controlled by preference browser.sessionstore.debug 216 */ 217 var gDebuggingEnabled = false; 218 219 /** 220 * @namespace SessionStore 221 */ 222 export var SessionStore = { 223 get logger() { 224 return SessionStoreInternal._log; 225 }, 226 get promiseInitialized() { 227 return SessionStoreInternal.promiseInitialized; 228 }, 229 230 get promiseAllWindowsRestored() { 231 return SessionStoreInternal.promiseAllWindowsRestored; 232 }, 233 234 get canRestoreLastSession() { 235 return SessionStoreInternal.canRestoreLastSession; 236 }, 237 238 set canRestoreLastSession(val) { 239 SessionStoreInternal.canRestoreLastSession = val; 240 }, 241 242 get lastClosedObjectType() { 243 return SessionStoreInternal.lastClosedObjectType; 244 }, 245 246 get lastClosedActions() { 247 return [...SessionStoreInternal._lastClosedActions]; 248 }, 249 250 get LAST_ACTION_CLOSED_TAB() { 251 return SessionStoreInternal._LAST_ACTION_CLOSED_TAB; 252 }, 253 254 get LAST_ACTION_CLOSED_WINDOW() { 255 return SessionStoreInternal._LAST_ACTION_CLOSED_WINDOW; 256 }, 257 258 get savedGroups() { 259 return SessionStoreInternal._savedGroups; 260 }, 261 262 get willAutoRestore() { 263 return SessionStoreInternal.willAutoRestore; 264 }, 265 266 get shouldRestoreLastSession() { 267 return SessionStoreInternal._shouldRestoreLastSession; 268 }, 269 270 init: function ss_init() { 271 SessionStoreInternal.init(); 272 }, 273 274 /** 275 * Get the collection of all matching windows tracked by SessionStore 276 * 277 * @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 278 * @param {boolean} [aWindowOrOptions.private] Determine if we should filter for private or non-private windows 279 */ 280 getWindows(aWindowOrOptions) { 281 return SessionStoreInternal.getWindows(aWindowOrOptions); 282 }, 283 284 /** 285 * Get window a given closed tab belongs to 286 * 287 * @param {integer} aClosedId The closedId of the tab whose window we want to find 288 * @param {boolean} [aIncludePrivate] Optionally include private windows when searching for the closed tab 289 */ 290 getWindowForTabClosedId(aClosedId, aIncludePrivate) { 291 return SessionStoreInternal.getWindowForTabClosedId( 292 aClosedId, 293 aIncludePrivate 294 ); 295 }, 296 297 getBrowserState: function ss_getBrowserState() { 298 return SessionStoreInternal.getBrowserState(); 299 }, 300 301 setBrowserState: function ss_setBrowserState(aState) { 302 SessionStoreInternal.setBrowserState(aState); 303 }, 304 305 getWindowState: function ss_getWindowState(aWindow) { 306 return SessionStoreInternal.getWindowState(aWindow); 307 }, 308 309 setWindowState: function ss_setWindowState(aWindow, aState, aOverwrite) { 310 SessionStoreInternal.setWindowState(aWindow, aState, aOverwrite); 311 }, 312 313 getTabState: function ss_getTabState(aTab) { 314 return SessionStoreInternal.getTabState(aTab); 315 }, 316 317 setTabState: function ss_setTabState(aTab, aState) { 318 SessionStoreInternal.setTabState(aTab, aState); 319 }, 320 321 // Return whether a tab is restoring. 322 isTabRestoring(aTab) { 323 return TAB_STATE_FOR_BROWSER.has(aTab.linkedBrowser); 324 }, 325 326 getInternalObjectState(obj) { 327 return SessionStoreInternal.getInternalObjectState(obj); 328 }, 329 330 duplicateTab: function ss_duplicateTab( 331 aWindow, 332 aTab, 333 aDelta = 0, 334 aRestoreImmediately = true, 335 aOptions = {} 336 ) { 337 return SessionStoreInternal.duplicateTab( 338 aWindow, 339 aTab, 340 aDelta, 341 aRestoreImmediately, 342 aOptions 343 ); 344 }, 345 346 /** 347 * How many tabs were last closed. If multiple tabs were selected and closed together, 348 * we'll return that number. Normally the count is 1, or 0 if no tabs have been 349 * recently closed in this window. 350 * 351 * @returns the number of tabs that were last closed. 352 */ 353 getLastClosedTabCount(aWindow) { 354 return SessionStoreInternal.getLastClosedTabCount(aWindow); 355 }, 356 357 resetLastClosedTabCount(aWindow) { 358 SessionStoreInternal.resetLastClosedTabCount(aWindow); 359 }, 360 361 /** 362 * Get the number of closed tabs associated with a specific window 363 * 364 * @param {Window} aWindow 365 */ 366 getClosedTabCountForWindow: function ss_getClosedTabCountForWindow(aWindow) { 367 return SessionStoreInternal.getClosedTabCountForWindow(aWindow); 368 }, 369 370 /** 371 * Get the number of closed tabs associated with all matching windows 372 * 373 * @param {Window | object} [aOptions] 374 * Either a DOMWindow (see aOptions.sourceWindow) or an object with properties 375 to identify which closed tabs to include in the count. 376 * @param {Window} aOptions.sourceWindow 377 A browser window used to identity privateness. 378 When closedTabsFromAllWindows is false, we only count closed tabs assocated with this window. 379 * @param {boolean} [aOptions.private = false] 380 Explicit indicator to constrain tab count to only private or non-private windows, 381 * @param {boolean} [aOptions.closedTabsFromAllWindows] 382 Override the value of the closedTabsFromAllWindows preference. 383 * @param {boolean} [aOptions.closedTabsFromClosedWindows] 384 Override the value of the closedTabsFromClosedWindows preference. 385 */ 386 getClosedTabCount: function ss_getClosedTabCount(aOptions) { 387 return SessionStoreInternal.getClosedTabCount(aOptions); 388 }, 389 390 /** 391 * Get the number of closed tabs from recently closed window 392 * 393 * This is normally only relevant in a non-private window context, as we don't 394 * keep data from closed private windows. 395 */ 396 getClosedTabCountFromClosedWindows: 397 function ss_getClosedTabCountFromClosedWindows() { 398 return SessionStoreInternal.getClosedTabCountFromClosedWindows(); 399 }, 400 401 /** 402 * Get the closed tab data associated with this window 403 * 404 * @param {Window} aWindow 405 */ 406 getClosedTabDataForWindow: function ss_getClosedTabDataForWindow(aWindow) { 407 return SessionStoreInternal.getClosedTabDataForWindow(aWindow); 408 }, 409 410 /** 411 * Get the closed tab data associated with all matching windows 412 * 413 * @param {Window | object} [aOptions] 414 * Either a DOMWindow (see aOptions.sourceWindow) or an object with properties 415 to identify which closed tabs to get data from 416 * @param {Window} aOptions.sourceWindow 417 A browser window used to identity privateness. 418 When closedTabsFromAllWindows is false, we only include closed tabs assocated with this window. 419 * @param {boolean} [aOptions.private = false] 420 Explicit indicator to constrain tab data to only private or non-private windows, 421 * @param {boolean} [aOptions.closedTabsFromAllWindows] 422 Override the value of the closedTabsFromAllWindows preference. 423 * @param {boolean} [aOptions.closedTabsFromClosedWindows] 424 Override the value of the closedTabsFromClosedWindows preference. 425 */ 426 getClosedTabData: function ss_getClosedTabData(aOptions) { 427 return SessionStoreInternal.getClosedTabData(aOptions); 428 }, 429 430 /** 431 * Get the closed tab data associated with all closed windows 432 * 433 * @returns an un-sorted array of tabData for closed tabs from closed windows 434 */ 435 getClosedTabDataFromClosedWindows: 436 function ss_getClosedTabDataFromClosedWindows() { 437 return SessionStoreInternal.getClosedTabDataFromClosedWindows(); 438 }, 439 440 /** 441 * Get the closed tab group data associated with all matching windows 442 * 443 * @param {Window|object} aOptions 444 * Either a DOMWindow (see aOptions.sourceWindow) or an object with properties 445 to identify the window source of the closed tab groups 446 * @param {Window} [aOptions.sourceWindow] 447 A browser window used to identity privateness. 448 When closedTabsFromAllWindows is false, we only include closed tab groups assocated with this window. 449 * @param {boolean} [aOptions.private = false] 450 Explicit indicator to constrain tab group data to only private or non-private windows, 451 * @param {boolean} [aOptions.closedTabsFromAllWindows] 452 Override the value of the closedTabsFromAllWindows preference. 453 * @param {boolean} [aOptions.closedTabsFromClosedWindows] 454 Override the value of the closedTabsFromClosedWindows preference. 455 * @returns {ClosedTabGroupStateData[]} 456 */ 457 getClosedTabGroups: function ss_getClosedTabGroups(aOptions) { 458 return SessionStoreInternal.getClosedTabGroups(aOptions); 459 }, 460 461 /** 462 * Get the last closed tab ID associated with a specific window 463 * 464 * @param {Window} aWindow 465 */ 466 getLastClosedTabGroupId(window) { 467 return SessionStoreInternal.getLastClosedTabGroupId(window); 468 }, 469 470 /** 471 * Re-open a closed tab 472 * 473 * @param {Window | object} aSource 474 * Either a DOMWindow or an object with properties to resolve to the window 475 * the tab was previously open in. 476 * @param {string} aSource.sourceWindowId 477 A SessionStore window id used to look up the window where the tab was closed 478 * @param {number} aSource.sourceClosedId 479 The closedId used to look up the closed window where the tab was closed 480 * @param {Integer} [aIndex = 0] 481 * The index of the tab in the closedTabs array (via SessionStore.getClosedTabData), where 0 is most recent. 482 * @param {Window} [aTargetWindow = aWindow] Optional window to open the tab into, defaults to current (topWindow). 483 * @returns a reference to the reopened tab. 484 */ 485 undoCloseTab: function ss_undoCloseTab(aSource, aIndex, aTargetWindow) { 486 return SessionStoreInternal.undoCloseTab(aSource, aIndex, aTargetWindow); 487 }, 488 489 /** 490 * Re-open a tab from a closed window, which corresponds to the closedId 491 * 492 * @param {Window | object} aSource 493 * Either a DOMWindow or an object with properties to resolve to the window 494 * the tab was previously open in. 495 * @param {string} aSource.sourceWindowId 496 A SessionStore window id used to look up the window where the tab was closed 497 * @param {number} aSource.sourceClosedId 498 The closedId used to look up the closed window where the tab was closed 499 * @param {integer} aClosedId 500 * The closedId of the tab or window 501 * @param {Window} [aTargetWindow = aWindow] Optional window to open the tab into, defaults to current (topWindow). 502 * @returns a reference to the reopened tab. 503 */ 504 undoClosedTabFromClosedWindow: function ss_undoClosedTabFromClosedWindow( 505 aSource, 506 aClosedId, 507 aTargetWindow 508 ) { 509 return SessionStoreInternal.undoClosedTabFromClosedWindow( 510 aSource, 511 aClosedId, 512 aTargetWindow 513 ); 514 }, 515 516 /** 517 * Forget a closed tab associated with a given window 518 * Removes the record at the given index so it cannot be un-closed or appear 519 * in a list of recently-closed tabs 520 * 521 * @param {Window | object} aSource 522 * Either a DOMWindow or an object with properties to resolve to the window 523 * the tab was previously open in. 524 * @param {string} aSource.sourceWindowId 525 A SessionStore window id used to look up the window where the tab was closed 526 * @param {number} aSource.sourceClosedId 527 The closedId used to look up the closed window where the tab was closed 528 * @param {Integer} [aIndex = 0] 529 * The index into the window's list of closed tabs 530 * @throws {InvalidArgumentError} if the window is not tracked by SessionStore, or index is out of bounds 531 */ 532 forgetClosedTab: function ss_forgetClosedTab(aSource, aIndex) { 533 return SessionStoreInternal.forgetClosedTab(aSource, aIndex); 534 }, 535 536 /** 537 * Forget a closed tab group associated with a given window 538 * Removes the record at the given index so it cannot be un-closed or appear 539 * in a list of recently-closed tabs 540 * 541 * @param {Window | object} aSource 542 * Either a DOMWindow or an object with properties to resolve to the window 543 * the tab was previously open in. 544 * @param {string} aSource.sourceWindowId 545 A SessionStore window id used to look up the window where the tab group was closed 546 * @param {number} aSource.sourceClosedId 547 The closedId used to look up the closed window where the tab group was closed 548 * @param {string} tabGroupId 549 * The tab group ID of the closed tab group 550 * @throws {InvalidArgumentError} 551 * if the window or tab group is not tracked by SessionStore 552 */ 553 forgetClosedTabGroup: function ss_forgetClosedTabGroup(aSource, tabGroupId) { 554 return SessionStoreInternal.forgetClosedTabGroup(aSource, tabGroupId); 555 }, 556 557 /** 558 * Forget a closed tab that corresponds to the closedId 559 * Removes the record with this closedId so it cannot be un-closed or appear 560 * in a list of recently-closed tabs 561 * 562 * @param {integer} aClosedId 563 * The closedId of the tab 564 * @param {Window | object} aSourceOptions 565 * Either a DOMWindow or an object with properties to resolve to the window 566 * the tab was previously open in. 567 * @param {boolean} [aSourceOptions.includePrivate = true] 568 If no other means of resolving a source window is given, this flag is used to 569 constrain a search across all open window's closed tabs. 570 * @param {string} aSourceOptions.sourceWindowId 571 A SessionStore window id used to look up the window where the tab was closed 572 * @param {number} aSourceOptions.sourceClosedId 573 The closedId used to look up the closed window where the tab was closed 574 * @throws {InvalidArgumentError} if the closedId doesnt match a closed tab in any window 575 */ 576 forgetClosedTabById: function ss_forgetClosedTabById( 577 aClosedId, 578 aSourceOptions 579 ) { 580 SessionStoreInternal.forgetClosedTabById(aClosedId, aSourceOptions); 581 }, 582 583 /** 584 * Forget a closed window. 585 * Removes the record with this closedId so it cannot be un-closed or appear 586 * in a list of recently-closed windows 587 * 588 * @param {integer} aClosedId 589 * The closedId of the window 590 * @throws {InvalidArgumentError} if the closedId doesnt match a closed window 591 */ 592 forgetClosedWindowById: function ss_forgetClosedWindowById(aClosedId) { 593 SessionStoreInternal.forgetClosedWindowById(aClosedId); 594 }, 595 596 /** 597 * Look up the object type ("tab" or "window") for a given closedId 598 * 599 * @param {integer} aClosedId 600 */ 601 getObjectTypeForClosedId(aClosedId) { 602 return SessionStoreInternal.getObjectTypeForClosedId(aClosedId); 603 }, 604 605 /** 606 * Look up a window tracked by SessionStore by its id 607 * 608 * @param {string} aSessionStoreId 609 */ 610 getWindowById: function ss_getWindowById(aSessionStoreId) { 611 return SessionStoreInternal.getWindowById(aSessionStoreId); 612 }, 613 614 getClosedWindowCount: function ss_getClosedWindowCount() { 615 return SessionStoreInternal.getClosedWindowCount(); 616 }, 617 618 // this should only be used by one caller (currently restoreLastClosedTabOrWindowOrSession in browser.js) 619 popLastClosedAction: function ss_popLastClosedAction() { 620 return SessionStoreInternal._lastClosedActions.pop(); 621 }, 622 623 // for testing purposes 624 resetLastClosedActions: function ss_resetLastClosedActions() { 625 SessionStoreInternal._lastClosedActions = []; 626 }, 627 628 getClosedWindowData: function ss_getClosedWindowData() { 629 return SessionStoreInternal.getClosedWindowData(); 630 }, 631 632 maybeDontRestoreTabs(aWindow) { 633 SessionStoreInternal.maybeDontRestoreTabs(aWindow); 634 }, 635 636 undoCloseWindow: function ss_undoCloseWindow(aIndex) { 637 return SessionStoreInternal.undoCloseWindow(aIndex); 638 }, 639 640 forgetClosedWindow: function ss_forgetClosedWindow(aIndex) { 641 return SessionStoreInternal.forgetClosedWindow(aIndex); 642 }, 643 644 getCustomWindowValue(aWindow, aKey) { 645 return SessionStoreInternal.getCustomWindowValue(aWindow, aKey); 646 }, 647 648 setCustomWindowValue(aWindow, aKey, aStringValue) { 649 SessionStoreInternal.setCustomWindowValue(aWindow, aKey, aStringValue); 650 }, 651 652 deleteCustomWindowValue(aWindow, aKey) { 653 SessionStoreInternal.deleteCustomWindowValue(aWindow, aKey); 654 }, 655 656 getCustomTabValue(aTab, aKey) { 657 return SessionStoreInternal.getCustomTabValue(aTab, aKey); 658 }, 659 660 setCustomTabValue(aTab, aKey, aStringValue) { 661 SessionStoreInternal.setCustomTabValue(aTab, aKey, aStringValue); 662 }, 663 664 deleteCustomTabValue(aTab, aKey) { 665 SessionStoreInternal.deleteCustomTabValue(aTab, aKey); 666 }, 667 668 getLazyTabValue(aTab, aKey) { 669 return SessionStoreInternal.getLazyTabValue(aTab, aKey); 670 }, 671 672 getCustomGlobalValue(aKey) { 673 return SessionStoreInternal.getCustomGlobalValue(aKey); 674 }, 675 676 setCustomGlobalValue(aKey, aStringValue) { 677 SessionStoreInternal.setCustomGlobalValue(aKey, aStringValue); 678 }, 679 680 deleteCustomGlobalValue(aKey) { 681 SessionStoreInternal.deleteCustomGlobalValue(aKey); 682 }, 683 684 restoreLastSession: function ss_restoreLastSession() { 685 SessionStoreInternal.restoreLastSession(); 686 }, 687 688 speculativeConnectOnTabHover(tab) { 689 SessionStoreInternal.speculativeConnectOnTabHover(tab); 690 }, 691 692 getCurrentState(aUpdateAll) { 693 return SessionStoreInternal.getCurrentState(aUpdateAll); 694 }, 695 696 reviveCrashedTab(aTab) { 697 return SessionStoreInternal.reviveCrashedTab(aTab); 698 }, 699 700 reviveAllCrashedTabs() { 701 return SessionStoreInternal.reviveAllCrashedTabs(); 702 }, 703 704 updateSessionStoreFromTablistener( 705 aBrowser, 706 aBrowsingContext, 707 aPermanentKey, 708 aData, 709 aForStorage 710 ) { 711 return SessionStoreInternal.updateSessionStoreFromTablistener( 712 aBrowser, 713 aBrowsingContext, 714 aPermanentKey, 715 aData, 716 aForStorage 717 ); 718 }, 719 720 getSessionHistory(tab, updatedCallback) { 721 return SessionStoreInternal.getSessionHistory(tab, updatedCallback); 722 }, 723 724 /** 725 * Re-open a tab or window which corresponds to the closedId 726 * 727 * @param {integer} aClosedId 728 * The closedId of the tab or window 729 * @param {boolean} [aIncludePrivate = true] 730 * Whether to match the aClosedId to only closed private tabs/windows or non-private 731 * @param {Window} [aTargetWindow] 732 * When aClosedId is for a closed tab, which window to re-open the tab into. 733 * Defaults to current (topWindow). 734 * 735 * @returns a tab or window object 736 */ 737 undoCloseById(aClosedId, aIncludePrivate, aTargetWindow) { 738 return SessionStoreInternal.undoCloseById( 739 aClosedId, 740 aIncludePrivate, 741 aTargetWindow 742 ); 743 }, 744 745 resetBrowserToLazyState(tab) { 746 return SessionStoreInternal.resetBrowserToLazyState(tab); 747 }, 748 749 maybeExitCrashedState(browser) { 750 SessionStoreInternal.maybeExitCrashedState(browser); 751 }, 752 753 isBrowserInCrashedSet(browser) { 754 return SessionStoreInternal.isBrowserInCrashedSet(browser); 755 }, 756 757 // this is used for testing purposes 758 resetNextClosedId() { 759 SessionStoreInternal._nextClosedId = 0; 760 }, 761 762 /** 763 * Ensures that session store has registered and started tracking a given window. 764 * 765 * @param window 766 * Window reference 767 */ 768 ensureInitialized(window) { 769 if (SessionStoreInternal._sessionInitialized && !window.__SSi) { 770 /* 771 We need to check that __SSi is not defined on the window so that if 772 onLoad function is in the middle of executing we don't enter the function 773 again and try to redeclare the ContentSessionStore script. 774 */ 775 SessionStoreInternal.onLoad(window); 776 } 777 }, 778 779 getCurrentEpoch(browser) { 780 return SessionStoreInternal.getCurrentEpoch(browser.permanentKey); 781 }, 782 783 /** 784 * Determines whether the passed version number is compatible with 785 * the current version number of the SessionStore. 786 * 787 * @param version The format and version of the file, as an array, e.g. 788 * ["sessionrestore", 1] 789 */ 790 isFormatVersionCompatible(version) { 791 if (!version) { 792 return false; 793 } 794 if (!Array.isArray(version)) { 795 // Improper format. 796 return false; 797 } 798 if (version[0] != "sessionrestore") { 799 // Not a Session Restore file. 800 return false; 801 } 802 let number = Number.parseFloat(version[1]); 803 if (Number.isNaN(number)) { 804 return false; 805 } 806 return number <= FORMAT_VERSION; 807 }, 808 809 /** 810 * Filters out not worth-saving tabs from a given browser state object. 811 * 812 * @param aState (object) 813 * The browser state for which we remove worth-saving tabs. 814 * The given object will be modified. 815 */ 816 keepOnlyWorthSavingTabs(aState) { 817 let closedWindowShouldRestore = null; 818 for (let i = aState.windows.length - 1; i >= 0; i--) { 819 let win = aState.windows[i]; 820 for (let j = win.tabs.length - 1; j >= 0; j--) { 821 let tab = win.tabs[j]; 822 if (!SessionStoreInternal._shouldSaveTab(tab)) { 823 win.tabs.splice(j, 1); 824 if (win.selected > j) { 825 win.selected--; 826 } 827 } 828 } 829 830 // If it's the last window (and no closedWindow that will restore), keep the window state with no tabs. 831 if ( 832 !win.tabs.length && 833 (aState.windows.length > 1 || 834 closedWindowShouldRestore || 835 (closedWindowShouldRestore == null && 836 (closedWindowShouldRestore = aState._closedWindows.some( 837 w => w._shouldRestore 838 )))) 839 ) { 840 aState.windows.splice(i, 1); 841 if (aState.selectedWindow > i) { 842 aState.selectedWindow--; 843 } 844 } 845 } 846 }, 847 848 /** 849 * Clear session store data for a given private browsing window. 850 * 851 * @param {ChromeWindow} win - Open private browsing window to clear data for. 852 */ 853 purgeDataForPrivateWindow(win) { 854 return SessionStoreInternal.purgeDataForPrivateWindow(win); 855 }, 856 857 /** 858 * Add a tab group to the session's saved group list. 859 * 860 * @param {MozTabbrowserTabGroup} tabGroup - The group to save 861 */ 862 addSavedTabGroup(tabGroup) { 863 return SessionStoreInternal.addSavedTabGroup(tabGroup); 864 }, 865 866 /** 867 * Add tabs to an existing saved tab group. 868 * 869 * @param {string} tabGroupId - The ID of the group to save to 870 * @param {MozTabbrowserTab[]} tabs - The list of tabs to add to the group 871 * @param {TabMetricsContext} [metricsContext] 872 * Optional context to record for metrics purposes. 873 * @returns {SavedTabGroupStateData} 874 */ 875 addTabsToSavedGroup(tabGroupId, tabs, metricsContext) { 876 return SessionStoreInternal.addTabsToSavedGroup( 877 tabGroupId, 878 tabs, 879 metricsContext 880 ); 881 }, 882 883 /** 884 * Retrieve the tab group state of a saved tab group by ID. 885 * 886 * @param {string} tabGroupId 887 * @returns {SavedTabGroupStateData|undefined} 888 */ 889 getSavedTabGroup(tabGroupId) { 890 return SessionStoreInternal.getSavedTabGroup(tabGroupId); 891 }, 892 893 /** 894 * Returns all tab groups that were saved in this session. 895 * 896 * @returns {SavedTabGroupStateData[]} 897 */ 898 getSavedTabGroups() { 899 return SessionStoreInternal.getSavedTabGroups(); 900 }, 901 902 /** 903 * Remove a tab group from the session's saved tab group list. 904 * 905 * @param {string} tabGroupId 906 * The ID of the tab group to remove 907 */ 908 forgetSavedTabGroup(tabGroupId) { 909 return SessionStoreInternal.forgetSavedTabGroup(tabGroupId); 910 }, 911 912 /** 913 * Re-open a closed tab group 914 * 915 * @param {Window | object} source 916 * Either a DOMWindow or an object with properties to resolve to the window 917 * the tab was previously open in. 918 * @param {string} source.sourceWindowId 919 A SessionStore window id used to look up the window where the tab was closed. 920 * @param {number} source.sourceClosedId 921 The closedId used to look up the closed window where the tab was closed. 922 * @param {string} tabGroupId 923 * The unique ID of the group to restore. 924 * @param {Window} [targetWindow] defaults to the top window if not specified. 925 * @returns {MozTabbrowserTabGroup} 926 * a reference to the restored tab group in a browser window. 927 */ 928 undoCloseTabGroup(source, tabGroupId, targetWindow) { 929 return SessionStoreInternal.undoCloseTabGroup( 930 source, 931 tabGroupId, 932 targetWindow 933 ); 934 }, 935 936 /** 937 * Re-open a saved tab group. 938 * Note that this method does not require passing a window source, as saved 939 * tab groups are independent of windows. 940 * Attempting to open a saved tab group in a private window will raise an error. 941 * 942 * @param {string} tabGroupId 943 * The unique ID of the group to restore. 944 * @param {Window} [targetWindow] defaults to the top window if not specified. 945 * @returns {MozTabbrowserTabGroup} 946 * a reference to the restored tab group in a browser window. 947 */ 948 openSavedTabGroup( 949 tabGroupId, 950 targetWindow, 951 { source = TabMetrics.METRIC_SOURCE.UNKNOWN } = {} 952 ) { 953 let isVerticalMode = targetWindow.gBrowser.tabContainer.verticalMode; 954 Glean.tabgroup.reopen.record({ 955 id: tabGroupId, 956 source, 957 layout: isVerticalMode 958 ? TabMetrics.METRIC_TABS_LAYOUT.VERTICAL 959 : TabMetrics.METRIC_TABS_LAYOUT.HORIZONTAL, 960 type: TabMetrics.METRIC_REOPEN_TYPE.SAVED, 961 }); 962 if (source == TabMetrics.METRIC_SOURCE.SUGGEST) { 963 Glean.tabgroup.groupInteractions.open_suggest.add(1); 964 } else if (source == TabMetrics.METRIC_SOURCE.TAB_OVERFLOW_MENU) { 965 Glean.tabgroup.groupInteractions.open_tabmenu.add(1); 966 } else if (source == TabMetrics.METRIC_SOURCE.RECENT_TABS) { 967 Glean.tabgroup.groupInteractions.open_recent.add(1); 968 } 969 970 return SessionStoreInternal.openSavedTabGroup(tabGroupId, targetWindow); 971 }, 972 973 /** 974 * Determine whether a list of tabs should be considered saveable. 975 * A list of tabs is considered saveable if any of the tabs in the list 976 * are worth saving. 977 * 978 * This is used to determine if a tab group should be saved, or if any active 979 * tabs in a selection are eligible to be added to an existing saved group. 980 * 981 * @param {MozTabbrowserTab[]} tabs - the list of tabs to check 982 * @returns {boolean} true if any of the tabs are saveable. 983 */ 984 shouldSaveTabsToGroup(tabs) { 985 return SessionStoreInternal.shouldSaveTabsToGroup(tabs); 986 }, 987 988 /** 989 * Convert tab state into a saved group tab state. Used to convert a 990 * closed tab group into a saved tab group. 991 * 992 * @param {TabState} tabState closed tab state 993 */ 994 formatTabStateForSavedGroup(tab) { 995 return SessionStoreInternal._formatTabStateForSavedGroup(tab); 996 }, 997 998 /** 999 * Validates that a state object matches the schema 1000 * defined in browser/components/sessionstore/session.schema.json 1001 * 1002 * @param {object} [state] State object to validate. If not provided, 1003 * will validate the current session state. 1004 * @returns {Promise} A promise which resolves to a validation result object 1005 */ 1006 validateState(state) { 1007 return SessionStoreInternal.validateState(state); 1008 }, 1009 }; 1010 1011 // Freeze the SessionStore object. We don't want anyone to modify it. 1012 Object.freeze(SessionStore); 1013 1014 /** 1015 * @namespace SessionStoreInternal 1016 * 1017 * @description Internal implementations and helpers for the public SessionStore methods 1018 */ 1019 var SessionStoreInternal = { 1020 QueryInterface: ChromeUtils.generateQI([ 1021 "nsIObserver", 1022 "nsISupportsWeakReference", 1023 ]), 1024 1025 _globalState: new GlobalState(), 1026 1027 // A counter to be used to generate a unique ID for each closed tab or window. 1028 _nextClosedId: 0, 1029 1030 // During the initial restore and setBrowserState calls tracks the number of 1031 // windows yet to be restored 1032 _restoreCount: -1, 1033 1034 // For each <browser> element, records the SHistoryListener. 1035 _browserSHistoryListener: new WeakMap(), 1036 1037 // Tracks the various listeners that are used throughout the restore. 1038 _restoreListeners: new WeakMap(), 1039 1040 // Records the promise created in _restoreHistory, which is used to track 1041 // the completion of the first phase of the restore. 1042 _tabStateRestorePromises: new WeakMap(), 1043 1044 // The history data needed to be restored in the parent. 1045 _tabStateToRestore: new WeakMap(), 1046 1047 // For each <browser> element, records the current epoch. 1048 _browserEpochs: new WeakMap(), 1049 1050 // Any browsers that fires the oop-browser-crashed event gets stored in 1051 // here - that way we know which browsers to ignore messages from (until 1052 // they get restored). 1053 _crashedBrowsers: new WeakSet(), 1054 1055 // A map (xul:browser -> FrameLoader) that maps a browser to the last 1056 // associated frameLoader we heard about. 1057 _lastKnownFrameLoader: new WeakMap(), 1058 1059 // A map (xul:browser -> object) that maps a browser associated with a 1060 // recently closed tab to all its necessary state information we need to 1061 // properly handle final update message. 1062 _closingTabMap: new WeakMap(), 1063 1064 // A map (xul:browser -> object) that maps a browser associated with a 1065 // recently closed tab due to a window closure to the tab state information 1066 // that is being stored in _closedWindows for that tab. 1067 _tabClosingByWindowMap: new WeakMap(), 1068 1069 // A set of window data that has the potential to be saved in the _closedWindows 1070 // array for the session. We will remove window data from this set whenever 1071 // forgetClosedWindow is called for the window, or when session history is 1072 // purged, so that we don't accidentally save that data after the flush has 1073 // completed. Closed tabs use a more complicated mechanism for this particular 1074 // problem. When forgetClosedTab is called, the browser is removed from the 1075 // _closingTabMap, so its data is not recorded. In the purge history case, 1076 // the closedTabs array per window is overwritten so that once the flush is 1077 // complete, the tab would only ever add itself to an array that SessionStore 1078 // no longer cares about. Bug 1230636 has been filed to make the tab case 1079 // work more like the window case, which is more explicit, and easier to 1080 // reason about. 1081 _saveableClosedWindowData: new WeakSet(), 1082 1083 // whether a setBrowserState call is in progress 1084 _browserSetState: false, 1085 1086 // time in milliseconds when the session was started (saved across sessions), 1087 // defaults to now if no session was restored or timestamp doesn't exist 1088 _sessionStartTime: Date.now(), 1089 1090 /** 1091 * states for all currently opened windows 1092 * 1093 * @type {{[key: WindowID]: WindowStateData}} 1094 */ 1095 _windows: {}, 1096 1097 // counter for creating unique window IDs 1098 _nextWindowID: 0, 1099 1100 // states for all recently closed windows 1101 _closedWindows: [], 1102 1103 /** @type {SavedTabGroupStateData[]} states for all saved+closed tab groups */ 1104 _savedGroups: [], 1105 1106 // collection of session states yet to be restored 1107 _statesToRestore: {}, 1108 1109 // counts the number of crashes since the last clean start 1110 _recentCrashes: 0, 1111 1112 // whether the last window was closed and should be restored 1113 _restoreLastWindow: false, 1114 1115 // whether we should restore last session on the next launch 1116 // of a regular Firefox window. This scenario is triggered 1117 // when a user closes all regular Firefox windows but the session is not over 1118 _shouldRestoreLastSession: false, 1119 1120 // whether we will potentially be restoring the session 1121 // more than once without Firefox restarting in between 1122 _restoreWithoutRestart: false, 1123 1124 // number of tabs currently restoring 1125 _tabsRestoringCount: 0, 1126 1127 /** 1128 * @typedef {object} CloseAction 1129 * @property {string} type 1130 * What the close action acted upon. One of either _LAST_ACTION_CLOSED_TAB or 1131 * _LAST_ACTION_CLOSED_WINDOW 1132 * @property {number} closedId 1133 * The unique ID of the item that closed. 1134 */ 1135 1136 /** 1137 * An in-order stack of close actions for tabs and windows. 1138 * 1139 * @type {CloseAction[]} 1140 */ 1141 _lastClosedActions: [], 1142 1143 /** 1144 * Removes an object from the _lastClosedActions list 1145 * 1146 * @param closedAction 1147 * Either _LAST_ACTION_CLOSED_TAB or _LAST_ACTION_CLOSED_WINDOW 1148 * @param {integer} closedId 1149 * The closedId of a tab or window 1150 */ 1151 _removeClosedAction(closedAction, closedId) { 1152 let closedActionIndex = this._lastClosedActions.findIndex( 1153 obj => obj.type == closedAction && obj.closedId == closedId 1154 ); 1155 1156 if (closedActionIndex > -1) { 1157 this._lastClosedActions.splice(closedActionIndex, 1); 1158 } 1159 }, 1160 1161 /** 1162 * Add an object to the _lastClosedActions list and truncates the list if needed 1163 * 1164 * @param closedAction 1165 * Either _LAST_ACTION_CLOSED_TAB or _LAST_ACTION_CLOSED_WINDOW 1166 * @param {integer} closedId 1167 * The closedId of a tab or window 1168 */ 1169 _addClosedAction(closedAction, closedId) { 1170 this._lastClosedActions.push({ 1171 type: closedAction, 1172 closedId, 1173 }); 1174 let maxLength = this._max_tabs_undo * this._max_windows_undo; 1175 1176 if (this._lastClosedActions.length > maxLength) { 1177 this._lastClosedActions = this._lastClosedActions.slice(-maxLength); 1178 } 1179 }, 1180 1181 _LAST_ACTION_CLOSED_TAB: "tab", 1182 1183 _LAST_ACTION_CLOSED_WINDOW: "window", 1184 1185 _log: null, 1186 1187 // When starting Firefox with a single private window or web app window, this is the place 1188 // where we keep the session we actually wanted to restore in case the user 1189 // decides to later open a non-private window as well. 1190 _deferredInitialState: null, 1191 1192 // Keeps track of whether a notification needs to be sent that closed objects have changed. 1193 _closedObjectsChanged: false, 1194 1195 // A promise resolved once initialization is complete 1196 _deferredInitialized: Promise.withResolvers(), 1197 1198 // Whether session has been initialized 1199 _sessionInitialized: false, 1200 1201 // A promise resolved once all windows are restored. 1202 _deferredAllWindowsRestored: Promise.withResolvers(), 1203 1204 get promiseAllWindowsRestored() { 1205 return this._deferredAllWindowsRestored.promise; 1206 }, 1207 1208 // Promise that is resolved when we're ready to initialize 1209 // and restore the session. 1210 _promiseReadyForInitialization: null, 1211 1212 // Keep busy state counters per window. 1213 _windowBusyStates: new WeakMap(), 1214 1215 /** 1216 * A promise fulfilled once initialization is complete. 1217 */ 1218 get promiseInitialized() { 1219 return this._deferredInitialized.promise; 1220 }, 1221 1222 get canRestoreLastSession() { 1223 return LastSession.canRestore; 1224 }, 1225 1226 set canRestoreLastSession(val) { 1227 // Cheat a bit; only allow false. 1228 if (!val) { 1229 LastSession.clear(); 1230 } 1231 }, 1232 1233 /** 1234 * Returns a string describing the last closed object, either "tab" or "window". 1235 * 1236 * This was added to support the sessions.restore WebExtensions API. 1237 */ 1238 get lastClosedObjectType() { 1239 if (this._closedWindows.length) { 1240 // Since there are closed windows, we need to check if there's a closed tab 1241 // in one of the currently open windows that was closed after the 1242 // last-closed window. 1243 let tabTimestamps = []; 1244 for (let window of Services.wm.getEnumerator("navigator:browser")) { 1245 let windowState = this._windows[window.__SSi]; 1246 if (windowState && windowState._closedTabs[0]) { 1247 tabTimestamps.push(windowState._closedTabs[0].closedAt); 1248 } 1249 } 1250 if ( 1251 !tabTimestamps.length || 1252 tabTimestamps.sort((a, b) => b - a)[0] < this._closedWindows[0].closedAt 1253 ) { 1254 return this._LAST_ACTION_CLOSED_WINDOW; 1255 } 1256 } 1257 return this._LAST_ACTION_CLOSED_TAB; 1258 }, 1259 1260 /** 1261 * Returns a boolean that determines whether the session will be automatically 1262 * restored upon the _next_ startup or a restart. 1263 */ 1264 get willAutoRestore() { 1265 return ( 1266 !PrivateBrowsingUtils.permanentPrivateBrowsing && 1267 (Services.prefs.getBoolPref("browser.sessionstore.resume_session_once") || 1268 Services.prefs.getIntPref("browser.startup.page") == 1269 BROWSER_STARTUP_RESUME_SESSION) 1270 ); 1271 }, 1272 1273 /** 1274 * Initialize the sessionstore service. 1275 */ 1276 init() { 1277 if (this._initialized) { 1278 throw new Error("SessionStore.init() must only be called once!"); 1279 } 1280 1281 TelemetryTimestamps.add("sessionRestoreInitialized"); 1282 Glean.sessionRestore.startupTimeline.sessionRestoreInitialized.set( 1283 Services.telemetry.msSinceProcessStart() 1284 ); 1285 OBSERVING.forEach(function (aTopic) { 1286 Services.obs.addObserver(this, aTopic, true); 1287 }, this); 1288 1289 this._initPrefs(); 1290 this._initialized = true; 1291 1292 this.promiseAllWindowsRestored.finally(() => () => { 1293 this._log.debug("promiseAllWindowsRestored finalized"); 1294 }); 1295 }, 1296 1297 /** 1298 * Initialize the session using the state provided by SessionStartup 1299 */ 1300 initSession() { 1301 let timerId = Glean.sessionRestore.startupInitSession.start(); 1302 let state; 1303 let ss = lazy.SessionStartup; 1304 let willRestore = ss.willRestore(); 1305 if (willRestore || ss.sessionType == ss.DEFER_SESSION) { 1306 state = ss.state; 1307 } 1308 this._log.debug( 1309 `initSession willRestore: ${willRestore}, SessionStartup.sessionType: ${ss.sessionType}` 1310 ); 1311 1312 if (state) { 1313 try { 1314 // If we're doing a DEFERRED session, then we want to pull pinned tabs 1315 // out so they can be restored, and save any open groups so they are 1316 // available to the user. 1317 if (ss.sessionType == ss.DEFER_SESSION) { 1318 let [iniState, remainingState] = 1319 this._prepDataForDeferredRestore(state); 1320 // If we have an iniState with windows, that means that we have windows 1321 // with pinned tabs to restore. If we have an iniState with saved 1322 // groups, we need to preserve those in the new state. 1323 if (iniState.windows.length || iniState.savedGroups) { 1324 state = iniState; 1325 } else { 1326 state = null; 1327 } 1328 this._log.debug( 1329 `initSession deferred restore with ${iniState.windows.length} initial windows, ${remainingState.windows.length} remaining windows` 1330 ); 1331 1332 if (remainingState.windows.length) { 1333 LastSession.setState(remainingState); 1334 } 1335 Glean.browserEngagement.sessionrestoreInterstitial.deferred_restore.add( 1336 1 1337 ); 1338 } else { 1339 // Get the last deferred session in case the user still wants to 1340 // restore it 1341 LastSession.setState(state.lastSessionState); 1342 1343 let restoreAsCrashed = ss.willRestoreAsCrashed(); 1344 if (restoreAsCrashed) { 1345 this._recentCrashes = 1346 ((state.session && state.session.recentCrashes) || 0) + 1; 1347 this._log.debug( 1348 `initSession, restoreAsCrashed, crashes: ${this._recentCrashes}` 1349 ); 1350 1351 // _needsRestorePage will record sessionrestore_interstitial, 1352 // including the specific reason we decided we needed to show 1353 // about:sessionrestore, if that's what we do. 1354 if (this._needsRestorePage(state, this._recentCrashes)) { 1355 // replace the crashed session with a restore-page-only session 1356 let url = "about:sessionrestore"; 1357 let formdata = { id: { sessionData: state }, url }; 1358 let entry = { 1359 url, 1360 triggeringPrincipal_base64: 1361 lazy.E10SUtils.SERIALIZED_SYSTEMPRINCIPAL, 1362 }; 1363 state = { windows: [{ tabs: [{ entries: [entry], formdata }] }] }; 1364 this._log.debug("initSession, will show about:sessionrestore"); 1365 } else if ( 1366 this._hasSingleTabWithURL(state.windows, "about:welcomeback") 1367 ) { 1368 this._log.debug("initSession, will show about:welcomeback"); 1369 Glean.browserEngagement.sessionrestoreInterstitial.shown_only_about_welcomeback.add( 1370 1 1371 ); 1372 // On a single about:welcomeback URL that crashed, replace about:welcomeback 1373 // with about:sessionrestore, to make clear to the user that we crashed. 1374 state.windows[0].tabs[0].entries[0].url = "about:sessionrestore"; 1375 state.windows[0].tabs[0].entries[0].triggeringPrincipal_base64 = 1376 lazy.E10SUtils.SERIALIZED_SYSTEMPRINCIPAL; 1377 } else { 1378 restoreAsCrashed = false; 1379 } 1380 } 1381 1382 // If we didn't use about:sessionrestore, record that: 1383 if (!restoreAsCrashed) { 1384 Glean.browserEngagement.sessionrestoreInterstitial.autorestore.add( 1385 1 1386 ); 1387 this._log.debug("initSession, will autorestore"); 1388 this._removeExplicitlyClosedTabs(state); 1389 } 1390 1391 // Update the session start time using the restored session state. 1392 this._updateSessionStartTime(state); 1393 1394 if (state.windows.length) { 1395 // Make sure that at least the first window doesn't have anything hidden. 1396 delete state.windows[0].hidden; 1397 // Since nothing is hidden in the first window, it cannot be a popup. 1398 delete state.windows[0].isPopup; 1399 // We don't want to minimize and then open a window at startup. 1400 if (state.windows[0].sizemode == "minimized") { 1401 state.windows[0].sizemode = "normal"; 1402 } 1403 } 1404 1405 // clear any lastSessionWindowID attributes since those don't matter 1406 // during normal restore 1407 state.windows.forEach(function (aWindow) { 1408 delete aWindow.__lastSessionWindowID; 1409 }); 1410 } 1411 1412 // clear _maybeDontRestoreTabs because we have restored (or not) 1413 // windows and so they don't matter 1414 state?.windows?.forEach(win => delete win._maybeDontRestoreTabs); 1415 state?._closedWindows?.forEach(win => delete win._maybeDontRestoreTabs); 1416 1417 this._savedGroups = state?.savedGroups ?? []; 1418 } catch (ex) { 1419 this._log.error("The session file is invalid: ", ex); 1420 } 1421 } 1422 1423 // at this point, we've as good as resumed the session, so we can 1424 // clear the resume_session_once flag, if it's set 1425 if ( 1426 !lazy.RunState.isQuitting && 1427 this._prefBranch.getBoolPref("sessionstore.resume_session_once") 1428 ) { 1429 this._prefBranch.setBoolPref("sessionstore.resume_session_once", false); 1430 } 1431 1432 Glean.sessionRestore.startupInitSession.stopAndAccumulate(timerId); 1433 return state; 1434 }, 1435 1436 /** 1437 * When initializing session, if we are restoring the last session at startup, 1438 * close open tabs or close windows marked _maybeDontRestoreTabs (if they were closed 1439 * by closing remaining tabs). 1440 * See bug 490136 1441 */ 1442 _removeExplicitlyClosedTabs(state) { 1443 // Don't restore tabs that has been explicitly closed 1444 for (let i = 0; i < state.windows.length; ) { 1445 const winData = state.windows[i]; 1446 if (winData._maybeDontRestoreTabs) { 1447 if (state.windows.length == 1) { 1448 // it's the last window, we just want to close tabs 1449 let j = 0; 1450 // reset close group (we don't want to append tabs to existing group close). 1451 winData._lastClosedTabGroupCount = -1; 1452 while (winData.tabs.length) { 1453 const tabState = winData.tabs.pop(); 1454 1455 // Ensure the index is in bounds. 1456 let activeIndex = (tabState.index || tabState.entries.length) - 1; 1457 activeIndex = Math.min(activeIndex, tabState.entries.length - 1); 1458 activeIndex = Math.max(activeIndex, 0); 1459 1460 let title = ""; 1461 if (activeIndex in tabState.entries) { 1462 title = 1463 tabState.entries[activeIndex].title || 1464 tabState.entries[activeIndex].url; 1465 } 1466 1467 const tabData = { 1468 state: tabState, 1469 title, 1470 image: tabState.image, 1471 pos: j++, 1472 closedAt: Date.now(), 1473 closedInGroup: true, 1474 }; 1475 if (this._shouldSaveTabState(tabState)) { 1476 this.saveClosedTabData(winData, winData._closedTabs, tabData); 1477 } 1478 } 1479 } else { 1480 // We can remove the window since it doesn't have any 1481 // tabs that we should restore and it's not the only window 1482 if (winData.tabs.some(this._shouldSaveTabState)) { 1483 winData.closedAt = Date.now(); 1484 state._closedWindows.unshift(winData); 1485 } 1486 state.windows.splice(i, 1); 1487 continue; // we don't want to increment the index 1488 } 1489 } 1490 i++; 1491 } 1492 }, 1493 1494 _initPrefs() { 1495 this._prefBranch = Services.prefs.getBranch("browser."); 1496 1497 gDebuggingEnabled = this._prefBranch.getBoolPref("sessionstore.debug"); 1498 1499 Services.prefs.addObserver("browser.sessionstore.debug", () => { 1500 gDebuggingEnabled = this._prefBranch.getBoolPref("sessionstore.debug"); 1501 }); 1502 1503 this._log = lazy.sessionStoreLogger; 1504 1505 this._max_tabs_undo = this._prefBranch.getIntPref( 1506 "sessionstore.max_tabs_undo" 1507 ); 1508 this._prefBranch.addObserver("sessionstore.max_tabs_undo", this, true); 1509 1510 this._closedTabsFromAllWindowsEnabled = this._prefBranch.getBoolPref( 1511 "sessionstore.closedTabsFromAllWindows" 1512 ); 1513 this._prefBranch.addObserver( 1514 "sessionstore.closedTabsFromAllWindows", 1515 this, 1516 true 1517 ); 1518 1519 this._closedTabsFromClosedWindowsEnabled = this._prefBranch.getBoolPref( 1520 "sessionstore.closedTabsFromClosedWindows" 1521 ); 1522 this._prefBranch.addObserver( 1523 "sessionstore.closedTabsFromClosedWindows", 1524 this, 1525 true 1526 ); 1527 1528 this._max_windows_undo = this._prefBranch.getIntPref( 1529 "sessionstore.max_windows_undo" 1530 ); 1531 this._prefBranch.addObserver("sessionstore.max_windows_undo", this, true); 1532 1533 this._restore_on_demand = this._prefBranch.getBoolPref( 1534 "sessionstore.restore_on_demand" 1535 ); 1536 this._prefBranch.addObserver("sessionstore.restore_on_demand", this, true); 1537 }, 1538 1539 /** 1540 * Called on application shutdown, after notifications: 1541 * quit-application-granted, quit-application 1542 */ 1543 _uninit: function ssi_uninit() { 1544 if (!this._initialized) { 1545 throw new Error("SessionStore is not initialized."); 1546 } 1547 1548 // Prepare to close the session file and write the last state. 1549 lazy.RunState.setClosing(); 1550 1551 // save all data for session resuming 1552 if (this._sessionInitialized) { 1553 lazy.SessionSaver.run(); 1554 } 1555 1556 // clear out priority queue in case it's still holding refs 1557 TabRestoreQueue.reset(); 1558 1559 // Make sure to cancel pending saves. 1560 lazy.SessionSaver.cancel(); 1561 }, 1562 1563 /** 1564 * Handle notifications 1565 */ 1566 observe: function ssi_observe(aSubject, aTopic, aData) { 1567 switch (aTopic) { 1568 case "browser-window-before-show": // catch new windows 1569 this.onBeforeBrowserWindowShown(aSubject); 1570 break; 1571 case "domwindowclosed": // catch closed windows 1572 this.onClose(aSubject).then(() => { 1573 this._notifyOfClosedObjectsChange(); 1574 }); 1575 if (gDebuggingEnabled) { 1576 Services.obs.notifyObservers(null, NOTIFY_DOMWINDOWCLOSED_HANDLED); 1577 } 1578 break; 1579 case "quit-application-granted": { 1580 let syncShutdown = aData == "syncShutdown"; 1581 this.onQuitApplicationGranted(syncShutdown); 1582 break; 1583 } 1584 case "browser-lastwindow-close-granted": 1585 this.onLastWindowCloseGranted(); 1586 break; 1587 case "quit-application": 1588 this.onQuitApplication(aData); 1589 break; 1590 case "browser:purge-session-history": // catch sanitization 1591 this.onPurgeSessionHistory(); 1592 this._notifyOfClosedObjectsChange(); 1593 break; 1594 case "browser:purge-session-history-for-domain": 1595 this.onPurgeDomainData(aData); 1596 this._notifyOfClosedObjectsChange(); 1597 break; 1598 case "nsPref:changed": // catch pref changes 1599 this.onPrefChange(aData); 1600 this._notifyOfClosedObjectsChange(); 1601 break; 1602 case "idle-daily": 1603 this.onIdleDaily(); 1604 this._notifyOfClosedObjectsChange(); 1605 break; 1606 case "clear-origin-attributes-data": { 1607 let userContextId = 0; 1608 try { 1609 userContextId = JSON.parse(aData).userContextId; 1610 } catch (e) {} 1611 if (userContextId) { 1612 this._forgetTabsWithUserContextId(userContextId); 1613 } 1614 break; 1615 } 1616 case "browsing-context-did-set-embedder": 1617 if (aSubject === aSubject.top && aSubject.isContent) { 1618 const permanentKey = aSubject.embedderElement?.permanentKey; 1619 if (permanentKey) { 1620 this.maybeRecreateSHistoryListener(permanentKey, aSubject); 1621 } 1622 } 1623 break; 1624 case "browsing-context-discarded": { 1625 let permanentKey = aSubject?.embedderElement?.permanentKey; 1626 if (permanentKey) { 1627 this._browserSHistoryListener.get(permanentKey)?.unregister(); 1628 } 1629 break; 1630 } 1631 case "browser-shutdown-tabstate-updated": 1632 this.onFinalTabStateUpdateComplete(aSubject); 1633 this._notifyOfClosedObjectsChange(); 1634 break; 1635 } 1636 }, 1637 1638 getOrCreateSHistoryListener(permanentKey, browsingContext) { 1639 if (!permanentKey || browsingContext !== browsingContext.top) { 1640 return null; 1641 } 1642 1643 const listener = this._browserSHistoryListener.get(permanentKey); 1644 if (listener) { 1645 return listener; 1646 } 1647 1648 return this.createSHistoryListener(permanentKey, browsingContext, false); 1649 }, 1650 1651 maybeRecreateSHistoryListener(permanentKey, browsingContext) { 1652 const listener = this._browserSHistoryListener.get(permanentKey); 1653 if (!listener || listener._browserId != browsingContext.browserId) { 1654 listener?.unregister(permanentKey); 1655 this.createSHistoryListener(permanentKey, browsingContext, true); 1656 } 1657 }, 1658 1659 createSHistoryListener(permanentKey, browsingContext, collectImmediately) { 1660 class SHistoryListener { 1661 constructor() { 1662 this.QueryInterface = ChromeUtils.generateQI([ 1663 "nsISHistoryListener", 1664 "nsISupportsWeakReference", 1665 ]); 1666 1667 this._browserId = browsingContext.browserId; 1668 this._fromIndex = kNoIndex; 1669 } 1670 1671 unregister() { 1672 let bc = BrowsingContext.getCurrentTopByBrowserId(this._browserId); 1673 bc?.sessionHistory?.removeSHistoryListener(this); 1674 SessionStoreInternal._browserSHistoryListener.delete(permanentKey); 1675 } 1676 1677 collect( 1678 permanentKey, // eslint-disable-line no-shadow 1679 browsingContext, // eslint-disable-line no-shadow 1680 { collectFull = true, writeToCache = false } 1681 ) { 1682 // Don't bother doing anything if we haven't seen any navigations. 1683 if (!collectFull && this._fromIndex === kNoIndex) { 1684 return null; 1685 } 1686 1687 let timerId = Glean.sessionRestore.collectSessionHistory.start(); 1688 1689 let fromIndex = collectFull ? -1 : this._fromIndex; 1690 this._fromIndex = kNoIndex; 1691 1692 let historychange = lazy.SessionHistory.collectFromParent( 1693 browsingContext.currentURI?.spec, 1694 true, // Bug 1704574 1695 browsingContext.sessionHistory, 1696 fromIndex 1697 ); 1698 1699 if (writeToCache) { 1700 let win = 1701 browsingContext.embedderElement?.ownerGlobal || 1702 browsingContext.currentWindowGlobal?.browsingContext?.window; 1703 1704 SessionStoreInternal.onTabStateUpdate(permanentKey, win, { 1705 data: { historychange }, 1706 }); 1707 } 1708 1709 Glean.sessionRestore.collectSessionHistory.stopAndAccumulate(timerId); 1710 1711 return historychange; 1712 } 1713 1714 collectFrom(index) { 1715 if (this._fromIndex <= index) { 1716 // If we already know that we need to update history from index N we 1717 // can ignore any changes that happened with an element with index 1718 // larger than N. 1719 // 1720 // Note: initially we use kNoIndex which is MAX_SAFE_INTEGER which 1721 // means we don't ignore anything here, and in case of navigation in 1722 // the history back and forth cases we use kLastIndex which ignores 1723 // only the subsequent navigations, but not any new elements added. 1724 return; 1725 } 1726 1727 let bc = BrowsingContext.getCurrentTopByBrowserId(this._browserId); 1728 if (bc?.embedderElement?.frameLoader) { 1729 this._fromIndex = index; 1730 1731 // Queue a tab state update on the |browser.sessionstore.interval| 1732 // timer. We'll call this.collect() when we receive the update. 1733 bc.embedderElement.frameLoader.requestSHistoryUpdate(); 1734 } 1735 } 1736 1737 OnHistoryNewEntry(newURI, oldIndex) { 1738 // We use oldIndex - 1 to collect the current entry as well. This makes 1739 // sure to collect any changes that were made to the entry while the 1740 // document was active. 1741 this.collectFrom(oldIndex == -1 ? oldIndex : oldIndex - 1); 1742 } 1743 OnHistoryGotoIndex() { 1744 this.collectFrom(kLastIndex); 1745 } 1746 OnHistoryPurge() { 1747 this.collectFrom(-1); 1748 } 1749 OnHistoryReload() { 1750 this.collectFrom(-1); 1751 return true; 1752 } 1753 OnHistoryReplaceEntry() { 1754 this.collectFrom(-1); 1755 } 1756 } 1757 1758 let sessionHistory = browsingContext.sessionHistory; 1759 if (!sessionHistory) { 1760 return null; 1761 } 1762 1763 const listener = new SHistoryListener(); 1764 sessionHistory.addSHistoryListener(listener); 1765 this._browserSHistoryListener.set(permanentKey, listener); 1766 1767 let isAboutBlank = browsingContext.currentURI?.spec === "about:blank"; 1768 1769 if (collectImmediately && (!isAboutBlank || sessionHistory.count !== 0)) { 1770 listener.collect(permanentKey, browsingContext, { writeToCache: true }); 1771 } 1772 1773 return listener; 1774 }, 1775 1776 onTabStateUpdate(permanentKey, win, update) { 1777 // Ignore messages from <browser> elements that have crashed 1778 // and not yet been revived. 1779 if (this._crashedBrowsers.has(permanentKey)) { 1780 return; 1781 } 1782 1783 lazy.TabState.update(permanentKey, update); 1784 this.saveStateDelayed(win); 1785 1786 // Handle any updates sent by the child after the tab was closed. This 1787 // might be the final update as sent by the "unload" handler but also 1788 // any async update message that was sent before the child unloaded. 1789 let closedTab = this._closingTabMap.get(permanentKey); 1790 if (closedTab) { 1791 // Update the closed tab's state. This will be reflected in its 1792 // window's list of closed tabs as that refers to the same object. 1793 lazy.TabState.copyFromCache(permanentKey, closedTab.tabData.state); 1794 } 1795 }, 1796 1797 onFinalTabStateUpdateComplete(browser) { 1798 let permanentKey = browser.permanentKey; 1799 if ( 1800 this._closingTabMap.has(permanentKey) && 1801 !this._crashedBrowsers.has(permanentKey) 1802 ) { 1803 let { winData, closedTabs, tabData } = 1804 this._closingTabMap.get(permanentKey); 1805 1806 // We expect no further updates. 1807 this._closingTabMap.delete(permanentKey); 1808 1809 // The tab state no longer needs this reference. 1810 delete tabData.permanentKey; 1811 1812 // Determine whether the tab state is worth saving. 1813 let shouldSave = this._shouldSaveTabState(tabData.state); 1814 let index = closedTabs.indexOf(tabData); 1815 1816 if (shouldSave && index == -1) { 1817 // If the tab state is worth saving and we didn't push it onto 1818 // the list of closed tabs when it was closed (because we deemed 1819 // the state not worth saving) then add it to the window's list 1820 // of closed tabs now. 1821 this.saveClosedTabData(winData, closedTabs, tabData); 1822 } else if (!shouldSave && index > -1) { 1823 // Remove from the list of closed tabs. The update messages sent 1824 // after the tab was closed changed enough state so that we no 1825 // longer consider its data interesting enough to keep around. 1826 this.removeClosedTabData(winData, closedTabs, index); 1827 } 1828 1829 this._cleanupOrphanedClosedGroups(winData); 1830 } 1831 1832 // If this the final message we need to resolve all pending flush 1833 // requests for the given browser as they might have been sent too 1834 // late and will never respond. If they have been sent shortly after 1835 // switching a browser's remoteness there isn't too much data to skip. 1836 lazy.TabStateFlusher.resolveAll(browser); 1837 1838 this._browserSHistoryListener.get(permanentKey)?.unregister(); 1839 this._restoreListeners.get(permanentKey)?.unregister(); 1840 1841 Services.obs.notifyObservers(browser, NOTIFY_BROWSER_SHUTDOWN_FLUSH); 1842 }, 1843 1844 updateSessionStoreFromTablistener( 1845 browser, 1846 browsingContext, 1847 permanentKey, 1848 update, 1849 forStorage = false 1850 ) { 1851 permanentKey = browser?.permanentKey ?? permanentKey; 1852 if (!permanentKey) { 1853 return; 1854 } 1855 1856 // Ignore sessionStore update from previous epochs 1857 if (!this.isCurrentEpoch(permanentKey, update.epoch)) { 1858 return; 1859 } 1860 1861 if (browsingContext.isReplaced) { 1862 return; 1863 } 1864 1865 let listener = this.getOrCreateSHistoryListener( 1866 permanentKey, 1867 browsingContext 1868 ); 1869 1870 if (listener) { 1871 let historychange = 1872 // If it is not the scheduled update (tab closed, window closed etc), 1873 // try to store the loading non-web-controlled page opened in _blank 1874 // first. 1875 (forStorage && 1876 lazy.SessionHistory.collectNonWebControlledBlankLoadingSession( 1877 browsingContext 1878 )) || 1879 listener.collect(permanentKey, browsingContext, { 1880 collectFull: !!update.sHistoryNeeded, 1881 writeToCache: false, 1882 }); 1883 1884 if (historychange) { 1885 update.data.historychange = historychange; 1886 } 1887 } 1888 1889 let win = 1890 browser?.ownerGlobal ?? 1891 browsingContext.currentWindowGlobal?.browsingContext?.window; 1892 1893 this.onTabStateUpdate(permanentKey, win, update); 1894 }, 1895 1896 /* ........ Window Event Handlers .............. */ 1897 1898 /** 1899 * Implement EventListener for handling various window and tab events 1900 */ 1901 handleEvent: function ssi_handleEvent(aEvent) { 1902 let win = aEvent.currentTarget.ownerGlobal; 1903 let target = aEvent.originalTarget; 1904 switch (aEvent.type) { 1905 case "TabOpen": 1906 this.onTabAdd(win); 1907 if (aEvent.detail.adoptedTab) { 1908 this.moveCustomTabValue(aEvent.detail.adoptedTab, target); 1909 } 1910 break; 1911 case "TabBrowserInserted": 1912 this.onTabBrowserInserted(win, target); 1913 break; 1914 case "TabClose": 1915 // `adoptedBy` will be set if the tab was closed because it is being 1916 // moved to a new window. 1917 if (aEvent.detail.adoptedBy) { 1918 this.moveCustomTabValue(target, aEvent.detail.adoptedBy); 1919 this.onMoveToNewWindow( 1920 target.linkedBrowser, 1921 aEvent.detail.adoptedBy.linkedBrowser 1922 ); 1923 } else if (!aEvent.detail.skipSessionStore) { 1924 // `skipSessionStore` is set by tab close callers to indicate that we 1925 // shouldn't record the closed tab. 1926 this.onTabClose(win, target); 1927 } 1928 this.onTabRemove(win, target); 1929 this._notifyOfClosedObjectsChange(); 1930 break; 1931 case "TabSelect": 1932 this.onTabSelect(win); 1933 break; 1934 case "TabShow": 1935 this.onTabShow(win, target); 1936 break; 1937 case "TabHide": 1938 this.onTabHide(win, target); 1939 break; 1940 case "TabPinned": 1941 case "TabUnpinned": 1942 case "SwapDocShells": 1943 this.saveStateDelayed(win); 1944 break; 1945 case "TabGroupCreate": 1946 case "TabGroupRemoved": 1947 case "TabGrouped": 1948 case "TabUngrouped": 1949 case "TabGroupCollapse": 1950 case "TabGroupExpand": 1951 this.saveStateDelayed(win); 1952 break; 1953 case "TabGroupRemoveRequested": 1954 if (!aEvent.detail?.skipSessionStore) { 1955 this.onTabGroupRemoveRequested(win, target); 1956 this._notifyOfClosedObjectsChange(); 1957 } 1958 break; 1959 case "TabSplitViewActivate": 1960 for (const tab of aEvent.detail.tabs) { 1961 this.maybeRestoreTabContent(tab); 1962 } 1963 break; 1964 case "oop-browser-crashed": 1965 case "oop-browser-buildid-mismatch": 1966 if (aEvent.isTopFrame) { 1967 this.onBrowserCrashed(target); 1968 } 1969 break; 1970 case "XULFrameLoaderCreated": 1971 if ( 1972 target.namespaceURI == XUL_NS && 1973 target.localName == "browser" && 1974 target.frameLoader && 1975 target.permanentKey 1976 ) { 1977 this._lastKnownFrameLoader.set( 1978 target.permanentKey, 1979 target.frameLoader 1980 ); 1981 this.resetEpoch(target.permanentKey, target.frameLoader); 1982 } 1983 break; 1984 default: 1985 throw new Error(`unhandled event ${aEvent.type}?`); 1986 } 1987 this._clearRestoringWindows(); 1988 }, 1989 1990 /** 1991 * Generate a unique window identifier 1992 * 1993 * @return string 1994 * A unique string to identify a window 1995 */ 1996 _generateWindowID: function ssi_generateWindowID() { 1997 return "window" + this._nextWindowID++; 1998 }, 1999 2000 /** 2001 * Registers and tracks a given window. 2002 * 2003 * @param aWindow 2004 * Window reference 2005 */ 2006 onLoad(aWindow) { 2007 // return if window has already been initialized 2008 if (aWindow && aWindow.__SSi && this._windows[aWindow.__SSi]) { 2009 return; 2010 } 2011 2012 // ignore windows opened while shutting down 2013 if (lazy.RunState.isQuitting) { 2014 return; 2015 } 2016 2017 // Assign the window a unique identifier we can use to reference 2018 // internal data about the window. 2019 aWindow.__SSi = this._generateWindowID(); 2020 2021 // and create its data object 2022 this._windows[aWindow.__SSi] = { 2023 tabs: [], 2024 groups: [], 2025 closedGroups: [], 2026 selected: 0, 2027 _closedTabs: [], 2028 // NOTE: this naming refers to the number of tabs in a *multiselection*, not in a tab group. 2029 // This naming was chosen before the introduction of tab groups proper. 2030 // TODO: choose more distinct naming in bug1928424 2031 _lastClosedTabGroupCount: -1, 2032 lastClosedTabGroupId: null, 2033 busy: false, 2034 chromeFlags: aWindow.docShell.treeOwner 2035 .QueryInterface(Ci.nsIInterfaceRequestor) 2036 .getInterface(Ci.nsIAppWindow).chromeFlags, 2037 }; 2038 2039 if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) { 2040 this._windows[aWindow.__SSi].isPrivate = true; 2041 } 2042 if (!this._isWindowLoaded(aWindow)) { 2043 this._windows[aWindow.__SSi]._restoring = true; 2044 } 2045 if (!aWindow.toolbar.visible) { 2046 this._windows[aWindow.__SSi].isPopup = true; 2047 } 2048 2049 if (aWindow.document.documentElement.hasAttribute("taskbartab")) { 2050 this._windows[aWindow.__SSi].isTaskbarTab = true; 2051 } 2052 2053 if (lazy.AIWindow.isAIWindowActiveAndEnabled(aWindow)) { 2054 this._windows[aWindow.__SSi].isAIWindow = true; 2055 } 2056 2057 let tabbrowser = aWindow.gBrowser; 2058 2059 // add tab change listeners to all already existing tabs 2060 for (let i = 0; i < tabbrowser.tabs.length; i++) { 2061 this.onTabBrowserInserted(aWindow, tabbrowser.tabs[i]); 2062 } 2063 // notification of tab add/remove/selection/show/hide 2064 TAB_EVENTS.forEach(function (aEvent) { 2065 tabbrowser.tabContainer.addEventListener(aEvent, this, true); 2066 }, this); 2067 2068 // Keep track of a browser's latest frameLoader. 2069 aWindow.gBrowser.addEventListener("XULFrameLoaderCreated", this); 2070 }, 2071 2072 /** 2073 * Initializes a given window. 2074 * 2075 * Windows are registered as soon as they are created but we need to wait for 2076 * the session file to load, and the initial window's delayed startup to 2077 * finish before initializing a window, i.e. restoring data into it. 2078 * 2079 * @param aWindow 2080 * Window reference 2081 * @param aInitialState 2082 * The initial state to be loaded after startup (optional) 2083 */ 2084 initializeWindow(aWindow, aInitialState = null) { 2085 let isPrivateWindow = PrivateBrowsingUtils.isWindowPrivate(aWindow); 2086 let isTaskbarTab = this._windows[aWindow.__SSi].isTaskbarTab; 2087 // A regular window is not a private window, taskbar tab window, or popup window 2088 let isRegularWindow = 2089 !isPrivateWindow && !isTaskbarTab && aWindow.toolbar.visible; 2090 2091 // perform additional initialization when the first window is loading 2092 if (lazy.RunState.isStopped) { 2093 lazy.RunState.setRunning(); 2094 2095 // restore a crashed session resp. resume the last session if requested 2096 if (aInitialState) { 2097 // Don't write to disk right after startup. Set the last time we wrote 2098 // to disk to NOW() to enforce a full interval before the next write. 2099 lazy.SessionSaver.updateLastSaveTime(); 2100 2101 if (isPrivateWindow || isTaskbarTab) { 2102 this._log.debug( 2103 "initializeWindow, the window is private or a web app. Saving SessionStartup.state for possibly restoring later" 2104 ); 2105 // We're starting with a single private window. Save the state we 2106 // actually wanted to restore so that we can do it later in case 2107 // the user opens another, non-private window. 2108 this._deferredInitialState = lazy.SessionStartup.state; 2109 2110 // Nothing to restore now, notify observers things are complete. 2111 Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED); 2112 Services.obs.notifyObservers( 2113 null, 2114 "sessionstore-one-or-no-tab-restored" 2115 ); 2116 this._deferredAllWindowsRestored.resolve(); 2117 } else { 2118 TelemetryTimestamps.add("sessionRestoreRestoring"); 2119 Glean.sessionRestore.startupTimeline.sessionRestoreRestoring.set( 2120 Services.telemetry.msSinceProcessStart() 2121 ); 2122 this._restoreCount = aInitialState.windows 2123 ? aInitialState.windows.length 2124 : 0; 2125 2126 // global data must be restored before restoreWindow is called so that 2127 // it happens before observers are notified 2128 this._globalState.setFromState(aInitialState); 2129 2130 // Restore session cookies before loading any tabs. 2131 lazy.SessionCookies.restore(aInitialState.cookies || []); 2132 2133 let overwrite = this._isCmdLineEmpty(aWindow, aInitialState); 2134 let options = { firstWindow: true, overwriteTabs: overwrite }; 2135 this.restoreWindows(aWindow, aInitialState, options); 2136 } 2137 } else { 2138 // Nothing to restore, notify observers things are complete. 2139 Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED); 2140 Services.obs.notifyObservers( 2141 null, 2142 "sessionstore-one-or-no-tab-restored" 2143 ); 2144 this._deferredAllWindowsRestored.resolve(); 2145 } 2146 // this window was opened by _openWindowWithState 2147 } else if (!this._isWindowLoaded(aWindow)) { 2148 // We want to restore windows after all windows have opened (since bug 2149 // 1034036), so bail out here. 2150 return; 2151 // The user opened another window that is not a popup, private window, or web app, 2152 // after starting up with a single private or web app window. 2153 // Let's restore the session we actually wanted to restore at startup. 2154 } else if (this._deferredInitialState && isRegularWindow) { 2155 // Only restore the deferred session if SessionStartup indicates we should 2156 // restore (e.g., crash recovery or user preference to restore sessions). 2157 // This prevents incorrect session restoration when a private window was 2158 // opened first followed by a normal window. See Bug 1938752. 2159 if (lazy.SessionStartup.willRestore()) { 2160 // global data must be restored before restoreWindow is called so that 2161 // it happens before observers are notified 2162 this._globalState.setFromState(this._deferredInitialState); 2163 this._restoreCount = this._deferredInitialState.windows 2164 ? this._deferredInitialState.windows.length 2165 : 0; 2166 this.restoreWindows(aWindow, this._deferredInitialState, { 2167 firstWindow: true, 2168 }); 2169 } 2170 this._deferredInitialState = null; 2171 } else if ( 2172 this._restoreLastWindow && 2173 aWindow.toolbar.visible && 2174 this._closedWindows.length && 2175 !isPrivateWindow 2176 ) { 2177 // default to the most-recently closed window 2178 // don't use popup windows 2179 let closedWindowState = null; 2180 let closedWindowIndex; 2181 for (let i = 0; i < this._closedWindows.length; i++) { 2182 // Take the first non-popup, point our object at it, and break out. 2183 if (!this._closedWindows[i].isPopup) { 2184 closedWindowState = this._closedWindows[i]; 2185 closedWindowIndex = i; 2186 break; 2187 } 2188 } 2189 2190 if (closedWindowState) { 2191 let newWindowState; 2192 if ( 2193 AppConstants.platform == "macosx" || 2194 !lazy.SessionStartup.willRestore() 2195 ) { 2196 // We want to split the window up into pinned tabs and unpinned tabs. 2197 // Pinned tabs should be restored. If there are any remaining tabs, 2198 // they should be added back to _closedWindows. 2199 // We'll cheat a little bit and reuse _prepDataForDeferredRestore 2200 // even though it wasn't built exactly for this. 2201 let [appTabsState, normalTabsState] = 2202 this._prepDataForDeferredRestore({ 2203 windows: [closedWindowState], 2204 }); 2205 2206 // These are our pinned tabs and sidebar attributes, which we should restore 2207 if (appTabsState.windows.length) { 2208 newWindowState = appTabsState.windows[0]; 2209 delete newWindowState.__lastSessionWindowID; 2210 } 2211 2212 // In case there were no unpinned tabs, remove the window from _closedWindows 2213 if (!normalTabsState.windows.length) { 2214 this._removeClosedWindow(closedWindowIndex); 2215 // Or update _closedWindows with the modified state 2216 } else { 2217 delete normalTabsState.windows[0].__lastSessionWindowID; 2218 this._closedWindows[closedWindowIndex] = normalTabsState.windows[0]; 2219 } 2220 } else { 2221 // If we're just restoring the window, make sure it gets removed from 2222 // _closedWindows. 2223 this._removeClosedWindow(closedWindowIndex); 2224 newWindowState = closedWindowState; 2225 delete newWindowState.hidden; 2226 } 2227 2228 if (newWindowState) { 2229 // Ensure that the window state isn't hidden 2230 this._restoreCount = 1; 2231 let state = { windows: [newWindowState] }; 2232 let options = { overwriteTabs: this._isCmdLineEmpty(aWindow, state) }; 2233 this.restoreWindow(aWindow, newWindowState, options); 2234 } 2235 } 2236 // we actually restored the session just now. 2237 this._prefBranch.setBoolPref("sessionstore.resume_session_once", false); 2238 } 2239 // This is a taskbar-tab specific scenario. If an user closes 2240 // all regular Firefox windows except for taskbar tabs and has 2241 // auto restore on startup enabled, _shouldRestoreLastSession 2242 // will be set to true. We should then restore when a 2243 // regular Firefox window is opened. 2244 else if ( 2245 Services.prefs.getBoolPref("browser.taskbarTabs.enabled", false) && 2246 this._shouldRestoreLastSession && 2247 isRegularWindow 2248 ) { 2249 let lastSessionState = LastSession.getState(); 2250 this._globalState.setFromState(lastSessionState); 2251 lazy.SessionCookies.restore(lastSessionState.cookies || []); 2252 this.restoreWindows(aWindow, lastSessionState, { 2253 firstWindow: true, 2254 }); 2255 this._shouldRestoreLastSession = false; 2256 } 2257 2258 if (this._restoreLastWindow && aWindow.toolbar.visible) { 2259 // always reset (if not a popup window) 2260 // we don't want to restore a window directly after, for example, 2261 // undoCloseWindow was executed. 2262 this._restoreLastWindow = false; 2263 } 2264 }, 2265 2266 /** 2267 * Called right before a new browser window is shown. 2268 * 2269 * @param aWindow 2270 * Window reference 2271 */ 2272 onBeforeBrowserWindowShown(aWindow) { 2273 // Do not track Document Picture-in-Picture windows since these are 2274 // ephemeral and tied to a specific tab's browser document. 2275 if (aWindow.browsingContext.isDocumentPiP) { 2276 return; 2277 } 2278 2279 // Register the window. 2280 this.onLoad(aWindow); 2281 2282 // Some are waiting for this window to be shown, which is now, so let's resolve 2283 // the deferred operation. 2284 let deferred = WINDOW_SHOWING_PROMISES.get(aWindow); 2285 if (deferred) { 2286 deferred.resolve(aWindow); 2287 WINDOW_SHOWING_PROMISES.delete(aWindow); 2288 } 2289 2290 // Just call initializeWindow() directly if we're initialized already. 2291 if (this._sessionInitialized) { 2292 this._log.debug( 2293 "onBeforeBrowserWindowShown, session already initialized, initializing window" 2294 ); 2295 this.initializeWindow(aWindow); 2296 return; 2297 } 2298 2299 // The very first window that is opened creates a promise that is then 2300 // re-used by all subsequent windows. The promise will be used to tell 2301 // when we're ready for initialization. 2302 if (!this._promiseReadyForInitialization) { 2303 // Wait for the given window's delayed startup to be finished. 2304 let promise = new Promise(resolve => { 2305 Services.obs.addObserver(function obs(subject, topic) { 2306 if (aWindow == subject) { 2307 Services.obs.removeObserver(obs, topic); 2308 resolve(); 2309 } 2310 }, "browser-delayed-startup-finished"); 2311 }); 2312 2313 // We are ready for initialization as soon as the session file has been 2314 // read from disk and the initial window's delayed startup has finished. 2315 this._promiseReadyForInitialization = Promise.all([ 2316 promise, 2317 lazy.SessionStartup.onceInitialized, 2318 ]); 2319 } 2320 2321 // We can't call this.onLoad since initialization 2322 // hasn't completed, so we'll wait until it is done. 2323 // Even if additional windows are opened and wait 2324 // for initialization as well, the first opened 2325 // window should execute first, and this.onLoad 2326 // will be called with the initialState. 2327 this._promiseReadyForInitialization 2328 .then(() => { 2329 if (aWindow.closed) { 2330 this._log.debug( 2331 "When _promiseReadyForInitialization resolved, the window was closed" 2332 ); 2333 return; 2334 } 2335 2336 if (this._sessionInitialized) { 2337 this.initializeWindow(aWindow); 2338 } else { 2339 let initialState = this.initSession(); 2340 this._sessionInitialized = true; 2341 2342 if (initialState) { 2343 Services.obs.notifyObservers(null, NOTIFY_RESTORING_ON_STARTUP); 2344 } 2345 let timerId = Glean.sessionRestore.startupOnloadInitialWindow.start(); 2346 this.initializeWindow(aWindow, initialState); 2347 Glean.sessionRestore.startupOnloadInitialWindow.stopAndAccumulate( 2348 timerId 2349 ); 2350 2351 // Let everyone know we're done. 2352 this._deferredInitialized.resolve(); 2353 } 2354 }) 2355 .catch(ex => { 2356 this._log.error( 2357 "Exception when handling _promiseReadyForInitialization resolution:", 2358 ex 2359 ); 2360 }); 2361 }, 2362 2363 /** 2364 * On window close... 2365 * - remove event listeners from tabs 2366 * - save all window data 2367 * 2368 * @param aWindow 2369 * Window reference 2370 * 2371 * @returns a Promise 2372 */ 2373 onClose: function ssi_onClose(aWindow) { 2374 let completionPromise = Promise.resolve(); 2375 // this window was about to be restored - conserve its original data, if any 2376 let isFullyLoaded = this._isWindowLoaded(aWindow); 2377 if (!isFullyLoaded) { 2378 if (!aWindow.__SSi) { 2379 aWindow.__SSi = this._generateWindowID(); 2380 } 2381 2382 let restoreID = WINDOW_RESTORE_IDS.get(aWindow); 2383 this._windows[aWindow.__SSi] = 2384 this._statesToRestore[restoreID].windows[0]; 2385 delete this._statesToRestore[restoreID]; 2386 WINDOW_RESTORE_IDS.delete(aWindow); 2387 } 2388 2389 // ignore windows not tracked by SessionStore 2390 if (!aWindow.__SSi || !this._windows[aWindow.__SSi]) { 2391 return completionPromise; 2392 } 2393 2394 // notify that the session store will stop tracking this window so that 2395 // extensions can store any data about this window in session store before 2396 // that's not possible anymore 2397 let event = aWindow.document.createEvent("Events"); 2398 event.initEvent("SSWindowClosing", true, false); 2399 aWindow.dispatchEvent(event); 2400 2401 if (this.windowToFocus && this.windowToFocus == aWindow) { 2402 delete this.windowToFocus; 2403 } 2404 2405 var tabbrowser = aWindow.gBrowser; 2406 2407 let browsers = Array.from(tabbrowser.browsers); 2408 2409 TAB_EVENTS.forEach(function (aEvent) { 2410 tabbrowser.tabContainer.removeEventListener(aEvent, this, true); 2411 }, this); 2412 2413 aWindow.gBrowser.removeEventListener("XULFrameLoaderCreated", this); 2414 2415 let winData = this._windows[aWindow.__SSi]; 2416 2417 // Collect window data only when *not* closed during shutdown. 2418 if (lazy.RunState.isRunning) { 2419 // Grab the most recent window data. The tab data will be updated 2420 // once we finish flushing all of the messages from the tabs. 2421 let tabMap = this._collectWindowData(aWindow); 2422 2423 for (let [tab, tabData] of tabMap) { 2424 let permanentKey = tab.linkedBrowser.permanentKey; 2425 this._tabClosingByWindowMap.set(permanentKey, tabData); 2426 } 2427 2428 if (isFullyLoaded && !winData.title) { 2429 winData.title = 2430 tabbrowser.selectedBrowser.contentTitle || 2431 tabbrowser.selectedTab.label; 2432 } 2433 2434 if (AppConstants.platform != "macosx") { 2435 // Until we decide otherwise elsewhere, this window is part of a series 2436 // of closing windows to quit. 2437 winData._shouldRestore = true; 2438 } 2439 2440 // Store the window's close date to figure out when each individual tab 2441 // was closed. This timestamp should allow re-arranging data based on how 2442 // recently something was closed. 2443 winData.closedAt = Date.now(); 2444 2445 // we don't want to save the busy state 2446 delete winData.busy; 2447 2448 // When closing windows one after the other until Firefox quits, we 2449 // will move those closed in series back to the "open windows" bucket 2450 // before writing to disk. If however there is only a single window 2451 // with tabs we deem not worth saving then we might end up with a 2452 // random closed or even a pop-up window re-opened. To prevent that 2453 // we explicitly allow saving an "empty" window state. 2454 let isLastWindow = this.isLastRestorableWindow(); 2455 2456 let isLastRegularWindow = 2457 Object.values(this._windows).filter( 2458 wData => !wData.isPrivate && !wData.isTaskbarTab 2459 ).length == 1; 2460 this._log.debug( 2461 `onClose, closing window isLastRegularWindow? ${isLastRegularWindow}` 2462 ); 2463 2464 let taskbarTabsRemains = Object.values(this._windows).some( 2465 wData => wData.isTaskbarTab 2466 ); 2467 2468 // Closing the last regular Firefox window with 2469 // at least one taskbar tab window still active. 2470 // The session is considered over and we need to restore 2471 // the next time a non-private, non-taskbar-tab window 2472 // is opened. 2473 if ( 2474 Services.prefs.getBoolPref("browser.taskbarTabs.enabled", false) && 2475 isLastRegularWindow && 2476 !winData.isTaskbarTab && 2477 !winData.isPrivate && 2478 taskbarTabsRemains 2479 ) { 2480 // If the setting is enabled, Firefox should auto-restore 2481 // the next time a regular window is opened 2482 if (this.willAutoRestore) { 2483 this._shouldRestoreLastSession = true; 2484 // Otherwise, we want "restore last session" button 2485 // to be avaliable in the hamburger menu 2486 } else { 2487 Services.obs.notifyObservers(null, NOTIFY_LAST_SESSION_RE_ENABLED); 2488 } 2489 2490 let savedState = this.getCurrentState(true); 2491 lazy.PrivacyFilter.filterPrivateWindowsAndTabs(savedState); 2492 LastSession.setState(savedState); 2493 this._restoreWithoutRestart = true; 2494 } 2495 2496 // clear this window from the list, since it has definitely been closed. 2497 delete this._windows[aWindow.__SSi]; 2498 2499 // This window has the potential to be saved in the _closedWindows 2500 // array (maybeSaveClosedWindows gets the final call on that). 2501 this._saveableClosedWindowData.add(winData); 2502 2503 // Now we have to figure out if this window is worth saving in the _closedWindows 2504 // Object. 2505 // 2506 // We're about to flush the tabs from this window, but it's possible that we 2507 // might never hear back from the content process(es) in time before the user 2508 // chooses to restore the closed window. So we do the following: 2509 // 2510 // 1) Use the tab state cache to determine synchronously if the window is 2511 // worth stashing in _closedWindows. 2512 // 2) Flush the window. 2513 // 3) When the flush is complete, revisit our decision to store the window 2514 // in _closedWindows, and add/remove as necessary. 2515 if (!winData.isPrivate && !winData.isTaskbarTab) { 2516 this.maybeSaveClosedWindow(winData, isLastWindow); 2517 } 2518 2519 completionPromise = lazy.TabStateFlusher.flushWindow(aWindow).then(() => { 2520 // At this point, aWindow is closed! You should probably not try to 2521 // access any DOM elements from aWindow within this callback unless 2522 // you're holding on to them in the closure. 2523 2524 WINDOW_FLUSHING_PROMISES.delete(aWindow); 2525 2526 for (let browser of browsers) { 2527 if (this._tabClosingByWindowMap.has(browser.permanentKey)) { 2528 let tabData = this._tabClosingByWindowMap.get(browser.permanentKey); 2529 lazy.TabState.copyFromCache(browser.permanentKey, tabData); 2530 this._tabClosingByWindowMap.delete(browser.permanentKey); 2531 } 2532 } 2533 2534 // Save non-private windows if they have at 2535 // least one saveable tab or are the last window. 2536 if (!winData.isPrivate && !winData.isTaskbarTab) { 2537 this.maybeSaveClosedWindow(winData, isLastWindow); 2538 2539 if (!isLastWindow && winData.closedId > -1) { 2540 this._addClosedAction( 2541 this._LAST_ACTION_CLOSED_WINDOW, 2542 winData.closedId 2543 ); 2544 } 2545 } 2546 2547 // Update the tabs data now that we've got the most 2548 // recent information. 2549 this.cleanUpWindow(aWindow, winData, browsers); 2550 2551 // save the state without this window to disk 2552 this.saveStateDelayed(); 2553 }); 2554 2555 // Here we might override a flush already in flight, but that's fine 2556 // because `completionPromise` will always resolve after the old flush 2557 // resolves. 2558 WINDOW_FLUSHING_PROMISES.set(aWindow, completionPromise); 2559 } else { 2560 this.cleanUpWindow(aWindow, winData, browsers); 2561 } 2562 2563 for (let i = 0; i < tabbrowser.tabs.length; i++) { 2564 this.onTabRemove(aWindow, tabbrowser.tabs[i], true); 2565 } 2566 2567 return completionPromise; 2568 }, 2569 2570 /** 2571 * Clean up the message listeners on a window that has finally 2572 * gone away. Call this once you're sure you don't want to hear 2573 * from any of this windows tabs from here forward. 2574 * 2575 * @param aWindow 2576 * The browser window we're cleaning up. 2577 * @param winData 2578 * The data for the window that we should hold in the 2579 * DyingWindowCache in case anybody is still holding a 2580 * reference to it. 2581 */ 2582 cleanUpWindow(aWindow, winData, browsers) { 2583 // Any leftover TabStateFlusher Promises need to be resolved now, 2584 // since we're about to remove the message listeners. 2585 for (let browser of browsers) { 2586 lazy.TabStateFlusher.resolveAll(browser); 2587 } 2588 2589 // Cache the window state until it is completely gone. 2590 DyingWindowCache.set(aWindow, winData); 2591 2592 this._saveableClosedWindowData.delete(winData); 2593 delete aWindow.__SSi; 2594 }, 2595 2596 /** 2597 * Decides whether or not a closed window should be put into the 2598 * _closedWindows Object. This might be called multiple times per 2599 * window, and will do the right thing of moving the window data 2600 * in or out of _closedWindows if the winData indicates that our 2601 * need for saving it has changed. 2602 * 2603 * @param winData 2604 * The data for the closed window that we might save. 2605 * @param isLastWindow 2606 * Whether or not the window being closed is the last 2607 * browser window. Callers of this function should pass 2608 * in the value of SessionStoreInternal.atLastWindow for 2609 * this argument, and pass in the same value if they happen 2610 * to call this method again asynchronously (for example, after 2611 * a window flush). 2612 */ 2613 maybeSaveClosedWindow(winData, isLastWindow) { 2614 // Make sure SessionStore is still running, and make sure that we 2615 // haven't chosen to forget this window. 2616 if ( 2617 lazy.RunState.isRunning && 2618 this._saveableClosedWindowData.has(winData) 2619 ) { 2620 // Determine whether the window has any tabs worth saving. 2621 // Note: We currently ignore the possibility of useful _closedTabs here. 2622 // A window with 0 worth-keeping open tabs will not have its state saved, and 2623 // any _closedTabs will be lost. 2624 let hasSaveableTabs = winData.tabs.some(this._shouldSaveTabState); 2625 2626 // Note that we might already have this window stored in 2627 // _closedWindows from a previous call to this function. 2628 let winIndex = this._closedWindows.indexOf(winData); 2629 let alreadyStored = winIndex != -1; 2630 // If sidebar command is truthy, i.e. sidebar is open, store sidebar settings 2631 let shouldStore = hasSaveableTabs || isLastWindow; 2632 2633 if (shouldStore && !alreadyStored) { 2634 let index = this._closedWindows.findIndex(win => { 2635 return win.closedAt < winData.closedAt; 2636 }); 2637 2638 // If we found no window closed before our 2639 // window then just append it to the list. 2640 if (index == -1) { 2641 index = this._closedWindows.length; 2642 } 2643 2644 // About to save the closed window, add a unique ID. 2645 winData.closedId = this._nextClosedId++; 2646 2647 // Insert winData at the right position. 2648 this._closedWindows.splice(index, 0, winData); 2649 this._capClosedWindows(); 2650 this._saveOpenTabGroupsOnClose(winData); 2651 this._closedObjectsChanged = true; 2652 this._log.debug( 2653 `Saved closed window:${winData.closedId} with ${winData.tabs.length} open tabs, ${winData._closedTabs.length} closed tabs` 2654 ); 2655 2656 // The first time we close a window, ensure it can be restored from the 2657 // hidden window. 2658 if ( 2659 AppConstants.platform == "macosx" && 2660 this._closedWindows.length == 1 2661 ) { 2662 // Fake a popupshowing event so shortcuts work: 2663 let window = Services.appShell.hiddenDOMWindow; 2664 let historyMenu = window.document.getElementById("history-menu"); 2665 let evt = new window.CustomEvent("popupshowing", { bubbles: true }); 2666 historyMenu.menupopup.dispatchEvent(evt); 2667 } 2668 } else if (!shouldStore) { 2669 if ( 2670 winData._closedTabs.length && 2671 this._closedTabsFromAllWindowsEnabled 2672 ) { 2673 // we are going to lose closed tabs, so any observers should be notified 2674 this._closedObjectsChanged = true; 2675 } 2676 if (alreadyStored) { 2677 this._removeClosedWindow(winIndex); 2678 return; 2679 } 2680 this._log.warn( 2681 `Discarding window:${winData.closedId} with 0 saveable tabs and ${winData._closedTabs.length} closed tabs` 2682 ); 2683 } 2684 } 2685 }, 2686 2687 /** 2688 * If there are any open tab groups in this closing window, move those 2689 * tab groups to the list of saved tab groups so that the user doesn't 2690 * lose them. 2691 * 2692 * The normal API for saving a tab group is `this.addSavedTabGroup`. 2693 * `this.addSavedTabGroup` relies on a MozTabbrowserTabGroup DOM element 2694 * and relies on passing the tab group's MozTabbrowserTab DOM elements to 2695 * `this.maybeSaveClosedTab`. Since this method might be dealing with a closed 2696 * window that has no DOM, this method has a separate but similar 2697 * implementation to `this.addSavedTabGroup` and `this.maybeSaveClosedTab`. 2698 * 2699 * @param {WindowStateData} closedWinData 2700 * @returns {void} 2701 */ 2702 _saveOpenTabGroupsOnClose(closedWinData) { 2703 /** @type Map<string, SavedTabGroupStateData> */ 2704 let newlySavedTabGroups = new Map(); 2705 // Convert any open tab groups into saved tab groups in place 2706 closedWinData.groups = closedWinData.groups.map(tabGroupState => 2707 lazy.TabGroupState.savedInClosedWindow( 2708 tabGroupState, 2709 closedWinData.closedId 2710 ) 2711 ); 2712 for (let tabGroupState of closedWinData.groups) { 2713 if (!tabGroupState.saveOnWindowClose) { 2714 continue; 2715 } 2716 newlySavedTabGroups.set(tabGroupState.id, tabGroupState); 2717 } 2718 for (let tIndex = 0; tIndex < closedWinData.tabs.length; tIndex++) { 2719 let tabState = closedWinData.tabs[tIndex]; 2720 if (!tabState.groupId) { 2721 continue; 2722 } 2723 if (!newlySavedTabGroups.has(tabState.groupId)) { 2724 continue; 2725 } 2726 2727 if (this._shouldSaveTabState(tabState)) { 2728 let tabData = this._formatTabStateForSavedGroup(tabState); 2729 if (!tabData) { 2730 continue; 2731 } 2732 newlySavedTabGroups.get(tabState.groupId).tabs.push(tabData); 2733 } 2734 } 2735 2736 // Add saved tab group references to saved tab group state. 2737 for (let tabGroupToSave of newlySavedTabGroups.values()) { 2738 this._recordSavedTabGroupState(tabGroupToSave); 2739 } 2740 }, 2741 2742 /** 2743 * Convert tab state into a saved group tab state. Used to convert a 2744 * closed tab group into a saved tab group. 2745 * 2746 * @param {TabState} tabState closed tab state 2747 */ 2748 _formatTabStateForSavedGroup(tabState) { 2749 // Ensure the index is in bounds. 2750 let activeIndex = tabState.index; 2751 activeIndex = Math.min(activeIndex, tabState.entries.length - 1); 2752 activeIndex = Math.max(activeIndex, 0); 2753 if (!(activeIndex in tabState.entries)) { 2754 return {}; 2755 } 2756 let title = 2757 tabState.entries[activeIndex].title || tabState.entries[activeIndex].url; 2758 return { 2759 state: tabState, 2760 title, 2761 image: tabState.image, 2762 pos: tabState.pos, 2763 closedAt: Date.now(), 2764 closedId: this._nextClosedId++, 2765 }; 2766 }, 2767 2768 /** 2769 * On quit application granted 2770 */ 2771 onQuitApplicationGranted: function ssi_onQuitApplicationGranted( 2772 syncShutdown = false 2773 ) { 2774 // Collect an initial snapshot of window data before we do the flush. 2775 let index = 0; 2776 for (let window of this._orderedBrowserWindows) { 2777 this._collectWindowData(window); 2778 this._windows[window.__SSi].zIndex = ++index; 2779 } 2780 this._log.debug( 2781 `onQuitApplicationGranted, shutdown of ${index} windows will be sync? ${syncShutdown}` 2782 ); 2783 this._log.debug( 2784 `Last session save attempt: ${Date.now() - lazy.SessionSaver.lastSaveTime}ms ago` 2785 ); 2786 2787 // Now add an AsyncShutdown blocker that'll spin the event loop 2788 // until the windows have all been flushed. 2789 2790 // This progress object will track the state of async window flushing 2791 // and will help us debug things that go wrong with our AsyncShutdown 2792 // blocker. 2793 let progress = { total: -1, current: -1 }; 2794 2795 // We're going down! Switch state so that we treat closing windows and 2796 // tabs correctly. 2797 lazy.RunState.setQuitting(); 2798 2799 if (!syncShutdown) { 2800 // We've got some time to shut down, so let's do this properly that there 2801 // will be a complete session available upon next startup. 2802 // We use our own timer and spin the event loop ourselves, as we do not 2803 // want to crash on timeout and as we need to run in response to 2804 // "quit-application-granted", which is not yet a real shutdown phase. 2805 // 2806 // We end spinning once: 2807 // 1. the flush duration exceeds 10 seconds before DELAY_CRASH_MS, or 2808 // 2. 'oop-frameloader-crashed' (issued by BrowserParent::ActorDestroy 2809 // on abnormal frame shutdown) is observed, or 2810 // 3. 'ipc:content-shutdown' (issued by ContentParent::ActorDestroy on 2811 // abnormal shutdown) is observed, or 2812 // 4. flushAllWindowsAsync completes (hopefully the normal case). 2813 2814 Glean.sessionRestore.shutdownType.async.add(1); 2815 2816 // Set up the list of promises that will signal a complete sessionstore 2817 // shutdown: either all data is saved, or we crashed or the message IPC 2818 // channel went away in the meantime. 2819 let promises = [this.flushAllWindowsAsync(progress)]; 2820 2821 const observeTopic = topic => { 2822 let deferred = Promise.withResolvers(); 2823 const observer = subject => { 2824 // Skip abort on ipc:content-shutdown if not abnormal/crashed 2825 subject.QueryInterface(Ci.nsIPropertyBag2); 2826 switch (topic) { 2827 case "ipc:content-shutdown": 2828 if (subject.get("abnormal")) { 2829 this._log.debug( 2830 "Observed abnormal ipc:content-shutdown during shutdown" 2831 ); 2832 Glean.sessionRestore.shutdownFlushAllOutcomes.abnormal_content_shutdown.add( 2833 1 2834 ); 2835 deferred.resolve(); 2836 } 2837 break; 2838 case "oop-frameloader-crashed": 2839 this._log.debug(`Observed topic: ${topic} during shutdown`); 2840 Glean.sessionRestore.shutdownFlushAllOutcomes.oop_frameloader_crashed.add( 2841 1 2842 ); 2843 deferred.resolve(); 2844 break; 2845 } 2846 }; 2847 const cleanup = () => { 2848 try { 2849 Services.obs.removeObserver(observer, topic); 2850 } catch (ex) { 2851 this._log.error("Exception whilst flushing all windows", ex); 2852 } 2853 }; 2854 Services.obs.addObserver(observer, topic); 2855 deferred.promise.then(cleanup, cleanup); 2856 return deferred; 2857 }; 2858 2859 // Build a list of deferred executions that require cleanup once the 2860 // Promise race is won. 2861 // Ensure that the timer fires earlier than the AsyncShutdown crash timer. 2862 let waitTimeMaxMs = Math.max( 2863 0, 2864 lazy.AsyncShutdown.DELAY_CRASH_MS - 10000 2865 ); 2866 let defers = [ 2867 this.looseTimer(waitTimeMaxMs), 2868 2869 // FIXME: We should not be aborting *all* flushes when a single 2870 // content process crashes here. 2871 observeTopic("oop-frameloader-crashed"), 2872 observeTopic("ipc:content-shutdown"), 2873 ]; 2874 // Add these monitors to the list of Promises to start the race. 2875 promises.push(...defers.map(deferred => deferred.promise)); 2876 2877 let isDone = false; 2878 Promise.race(promises) 2879 .then(() => { 2880 // When a Promise won the race, make sure we clean up the running 2881 // monitors. 2882 defers.forEach(deferred => deferred.reject()); 2883 }) 2884 .finally(() => { 2885 isDone = true; 2886 }); 2887 Services.tm.spinEventLoopUntil( 2888 "Wait until SessionStoreInternal.flushAllWindowsAsync finishes.", 2889 () => isDone 2890 ); 2891 } else { 2892 Glean.sessionRestore.shutdownType.sync.add(1); 2893 // We have to shut down NOW, which means we only get to save whatever 2894 // we already had cached. 2895 } 2896 }, 2897 2898 /** 2899 * An async Task that iterates all open browser windows and flushes 2900 * any outstanding messages from their tabs. This will also close 2901 * all of the currently open windows while we wait for the flushes 2902 * to complete. 2903 * 2904 * @param progress (Object) 2905 * Optional progress object that will be updated as async 2906 * window flushing progresses. flushAllWindowsSync will 2907 * write to the following properties: 2908 * 2909 * total (int): 2910 * The total number of windows to be flushed. 2911 * current (int): 2912 * The current window that we're waiting for a flush on. 2913 * 2914 * @return Promise 2915 */ 2916 async flushAllWindowsAsync(progress = {}) { 2917 let windowPromises = new Map(WINDOW_FLUSHING_PROMISES); 2918 WINDOW_FLUSHING_PROMISES.clear(); 2919 2920 // We collect flush promises and close each window immediately so that 2921 // the user can't start changing any window state while we're waiting 2922 // for the flushes to finish. 2923 for (let window of this._browserWindows) { 2924 windowPromises.set(window, lazy.TabStateFlusher.flushWindow(window)); 2925 2926 // We have to wait for these messages to come up from 2927 // each window and each browser. In the meantime, hide 2928 // the windows to improve perceived shutdown speed. 2929 let baseWin = window.docShell.treeOwner.QueryInterface(Ci.nsIBaseWindow); 2930 baseWin.visibility = false; 2931 } 2932 2933 progress.total = windowPromises.size; 2934 progress.current = 0; 2935 2936 // We'll iterate through the Promise array, yielding each one, so as to 2937 // provide useful progress information to AsyncShutdown. 2938 for (let [win, promise] of windowPromises) { 2939 await promise; 2940 2941 // We may have already stopped tracking this window in onClose, which is 2942 // fine as we would've collected window data there as well. 2943 if (win.__SSi && this._windows[win.__SSi]) { 2944 this._collectWindowData(win); 2945 } 2946 2947 progress.current++; 2948 } 2949 2950 // We must cache this because _getTopWindow will always 2951 // return null by the time quit-application occurs. 2952 var activeWindow = this._getTopWindow(); 2953 if (activeWindow) { 2954 this.activeWindowSSiCache = activeWindow.__SSi || ""; 2955 } 2956 DirtyWindows.clear(); 2957 Glean.sessionRestore.shutdownFlushAllOutcomes.complete.add(1); 2958 }, 2959 2960 /** 2961 * On last browser window close 2962 */ 2963 onLastWindowCloseGranted: function ssi_onLastWindowCloseGranted() { 2964 // last browser window is quitting. 2965 // remember to restore the last window when another browser window is opened 2966 // do not account for pref(resume_session_once) at this point, as it might be 2967 // set by another observer getting this notice after us 2968 this._restoreLastWindow = true; 2969 }, 2970 2971 /** 2972 * On quitting application 2973 * 2974 * @param aData 2975 * String type of quitting 2976 */ 2977 onQuitApplication: function ssi_onQuitApplication(aData) { 2978 if (aData == "restart" || aData == "os-restart") { 2979 if (!PrivateBrowsingUtils.permanentPrivateBrowsing) { 2980 if ( 2981 aData == "os-restart" && 2982 !this._prefBranch.getBoolPref("sessionstore.resume_session_once") 2983 ) { 2984 this._prefBranch.setBoolPref( 2985 "sessionstore.resuming_after_os_restart", 2986 true 2987 ); 2988 } 2989 this._prefBranch.setBoolPref("sessionstore.resume_session_once", true); 2990 } 2991 2992 // The browser:purge-session-history notification fires after the 2993 // quit-application notification so unregister the 2994 // browser:purge-session-history notification to prevent clearing 2995 // session data on disk on a restart. It is also unnecessary to 2996 // perform any other sanitization processing on a restart as the 2997 // browser is about to exit anyway. 2998 Services.obs.removeObserver(this, "browser:purge-session-history"); 2999 } 3000 3001 if (aData != "restart") { 3002 // Throw away the previous session on shutdown without notification 3003 LastSession.clear(true); 3004 } 3005 3006 this._uninit(); 3007 }, 3008 3009 /** 3010 * Clear session store data for a given private browsing window. 3011 * 3012 * @param {ChromeWindow} win - Open private browsing window to clear data for. 3013 */ 3014 purgeDataForPrivateWindow(win) { 3015 // No need to clear data if already shutting down. 3016 if (lazy.RunState.isQuitting) { 3017 return; 3018 } 3019 3020 // Check if we have data for the given window. 3021 let windowData = this._windows[win.__SSi]; 3022 if (!windowData) { 3023 return; 3024 } 3025 3026 // Clear closed tab data. 3027 if (windowData._closedTabs.length) { 3028 // Remove all of the closed tabs data. 3029 // This also clears out the permenentKey-mapped data for pending state updates 3030 // and removes the tabs from from the _lastClosedActions list 3031 while (windowData._closedTabs.length) { 3032 this.removeClosedTabData(windowData, windowData._closedTabs, 0); 3033 } 3034 // Reset the closed tab list. 3035 windowData._closedTabs = []; 3036 windowData._lastClosedTabGroupCount = -1; 3037 windowData.lastClosedTabGroupId = null; 3038 this._closedObjectsChanged = true; 3039 } 3040 3041 // Clear closed tab groups 3042 if (windowData.closedGroups.length) { 3043 for (let closedGroup of windowData.closedGroups) { 3044 while (closedGroup.tabs.length) { 3045 this.removeClosedTabData(windowData, closedGroup.tabs, 0); 3046 } 3047 } 3048 windowData.closedGroups = []; 3049 this._closedObjectsChanged = true; 3050 } 3051 }, 3052 3053 /** 3054 * On purge of session history 3055 */ 3056 onPurgeSessionHistory: function ssi_onPurgeSessionHistory() { 3057 lazy.SessionFile.wipe(); 3058 // If the browser is shutting down, simply return after clearing the 3059 // session data on disk as this notification fires after the 3060 // quit-application notification so the browser is about to exit. 3061 if (lazy.RunState.isQuitting) { 3062 return; 3063 } 3064 LastSession.clear(); 3065 3066 let openWindows = {}; 3067 // Collect open windows. 3068 for (let window of this._browserWindows) { 3069 openWindows[window.__SSi] = true; 3070 } 3071 3072 // also clear all data about closed tabs and windows 3073 for (let ix in this._windows) { 3074 if (ix in openWindows) { 3075 if (this._windows[ix]._closedTabs.length) { 3076 this._windows[ix]._closedTabs = []; 3077 this._closedObjectsChanged = true; 3078 } 3079 if (this._windows[ix].closedGroups.length) { 3080 this._windows[ix].closedGroups = []; 3081 this._closedObjectsChanged = true; 3082 } 3083 } else { 3084 delete this._windows[ix]; 3085 } 3086 } 3087 // also clear all data about closed windows 3088 if (this._closedWindows.length) { 3089 this._closedWindows = []; 3090 this._closedObjectsChanged = true; 3091 } 3092 // give the tabbrowsers a chance to clear their histories first 3093 var win = this._getTopWindow(); 3094 if (win) { 3095 win.setTimeout(() => lazy.SessionSaver.run(), 0); 3096 } else if (lazy.RunState.isRunning) { 3097 lazy.SessionSaver.run(); 3098 } 3099 3100 this._clearRestoringWindows(); 3101 this._saveableClosedWindowData = new WeakSet(); 3102 this._lastClosedActions = []; 3103 }, 3104 3105 /** 3106 * On purge of domain data 3107 * 3108 * @param {string} aDomain 3109 * The domain we want to purge data for 3110 */ 3111 onPurgeDomainData: function ssi_onPurgeDomainData(aDomain) { 3112 // does a session history entry contain a url for the given domain? 3113 function containsDomain(aEntry) { 3114 let host; 3115 try { 3116 host = Services.io.newURI(aEntry.url).host; 3117 } catch (e) { 3118 // The given URL probably doesn't have a host. 3119 } 3120 if (host && Services.eTLD.hasRootDomain(host, aDomain)) { 3121 return true; 3122 } 3123 return aEntry.children && aEntry.children.some(containsDomain, this); 3124 } 3125 // remove all closed tabs containing a reference to the given domain 3126 for (let ix in this._windows) { 3127 let closedTabsLists = [ 3128 this._windows[ix]._closedTabs, 3129 ...this._windows[ix].closedGroups.map(g => g.tabs), 3130 ]; 3131 3132 for (let closedTabs of closedTabsLists) { 3133 for (let i = closedTabs.length - 1; i >= 0; i--) { 3134 if (closedTabs[i].state.entries.some(containsDomain, this)) { 3135 closedTabs.splice(i, 1); 3136 this._closedObjectsChanged = true; 3137 } 3138 } 3139 } 3140 } 3141 // remove all open & closed tabs containing a reference to the given 3142 // domain in closed windows 3143 for (let ix = this._closedWindows.length - 1; ix >= 0; ix--) { 3144 let closedTabsLists = [ 3145 this._closedWindows[ix]._closedTabs, 3146 ...this._closedWindows[ix].closedGroups.map(g => g.tabs), 3147 ]; 3148 let openTabs = this._closedWindows[ix].tabs; 3149 let openTabCount = openTabs.length; 3150 3151 for (let closedTabs of closedTabsLists) { 3152 for (let i = closedTabs.length - 1; i >= 0; i--) { 3153 if (closedTabs[i].state.entries.some(containsDomain, this)) { 3154 closedTabs.splice(i, 1); 3155 } 3156 } 3157 } 3158 for (let j = openTabs.length - 1; j >= 0; j--) { 3159 if (openTabs[j].entries.some(containsDomain, this)) { 3160 openTabs.splice(j, 1); 3161 if (this._closedWindows[ix].selected > j) { 3162 this._closedWindows[ix].selected--; 3163 } 3164 } 3165 } 3166 if (!openTabs.length) { 3167 this._closedWindows.splice(ix, 1); 3168 } else if (openTabs.length != openTabCount) { 3169 // Adjust the window's title if we removed an open tab 3170 let selectedTab = openTabs[this._closedWindows[ix].selected - 1]; 3171 // some duplication from restoreHistory - make sure we get the correct title 3172 let activeIndex = (selectedTab.index || selectedTab.entries.length) - 1; 3173 if (activeIndex >= selectedTab.entries.length) { 3174 activeIndex = selectedTab.entries.length - 1; 3175 } 3176 this._closedWindows[ix].title = selectedTab.entries[activeIndex].title; 3177 } 3178 } 3179 3180 if (lazy.RunState.isRunning) { 3181 lazy.SessionSaver.run(); 3182 } 3183 3184 this._clearRestoringWindows(); 3185 }, 3186 3187 /** 3188 * On preference change 3189 * 3190 * @param aData 3191 * String preference changed 3192 */ 3193 onPrefChange: function ssi_onPrefChange(aData) { 3194 switch (aData) { 3195 // if the user decreases the max number of closed tabs they want 3196 // preserved update our internal states to match that max 3197 case "sessionstore.max_tabs_undo": 3198 this._max_tabs_undo = this._prefBranch.getIntPref( 3199 "sessionstore.max_tabs_undo" 3200 ); 3201 for (let ix in this._windows) { 3202 if (this._windows[ix]._closedTabs.length > this._max_tabs_undo) { 3203 this._windows[ix]._closedTabs.splice( 3204 this._max_tabs_undo, 3205 this._windows[ix]._closedTabs.length 3206 ); 3207 this._closedObjectsChanged = true; 3208 } 3209 } 3210 break; 3211 case "sessionstore.max_windows_undo": 3212 this._max_windows_undo = this._prefBranch.getIntPref( 3213 "sessionstore.max_windows_undo" 3214 ); 3215 this._capClosedWindows(); 3216 break; 3217 case "sessionstore.restore_on_demand": 3218 this._restore_on_demand = this._prefBranch.getBoolPref( 3219 "sessionstore.restore_on_demand" 3220 ); 3221 break; 3222 case "sessionstore.closedTabsFromAllWindows": 3223 this._closedTabsFromAllWindowsEnabled = this._prefBranch.getBoolPref( 3224 "sessionstore.closedTabsFromAllWindows" 3225 ); 3226 this._closedObjectsChanged = true; 3227 break; 3228 case "sessionstore.closedTabsFromClosedWindows": 3229 this._closedTabsFromClosedWindowsEnabled = this._prefBranch.getBoolPref( 3230 "sessionstore.closedTabsFromClosedWindows" 3231 ); 3232 this._closedObjectsChanged = true; 3233 break; 3234 } 3235 }, 3236 3237 /** 3238 * save state when new tab is added 3239 * 3240 * @param aWindow 3241 * Window reference 3242 */ 3243 onTabAdd: function ssi_onTabAdd(aWindow) { 3244 this.saveStateDelayed(aWindow); 3245 }, 3246 3247 /** 3248 * set up listeners for a new tab 3249 * 3250 * @param aWindow 3251 * Window reference 3252 * @param aTab 3253 * Tab reference 3254 */ 3255 onTabBrowserInserted: function ssi_onTabBrowserInserted(aWindow, aTab) { 3256 let browser = aTab.linkedBrowser; 3257 browser.addEventListener("SwapDocShells", this); 3258 browser.addEventListener("oop-browser-crashed", this); 3259 browser.addEventListener("oop-browser-buildid-mismatch", this); 3260 3261 if (browser.frameLoader) { 3262 this._lastKnownFrameLoader.set(browser.permanentKey, browser.frameLoader); 3263 } 3264 3265 // Only restore if browser has been lazy. 3266 if ( 3267 TAB_LAZY_STATES.has(aTab) && 3268 !TAB_STATE_FOR_BROWSER.has(browser) && 3269 lazy.TabStateCache.get(browser.permanentKey) 3270 ) { 3271 let tabState = lazy.TabState.clone(aTab, TAB_CUSTOM_VALUES.get(aTab)); 3272 this.restoreTab(aTab, tabState); 3273 } 3274 3275 // The browser has been inserted now, so lazy data is no longer relevant. 3276 TAB_LAZY_STATES.delete(aTab); 3277 }, 3278 3279 /** 3280 * remove listeners for a tab 3281 * 3282 * @param aWindow 3283 * Window reference 3284 * @param aTab 3285 * Tab reference 3286 * @param aNoNotification 3287 * bool Do not save state if we're updating an existing tab 3288 */ 3289 onTabRemove: function ssi_onTabRemove(aWindow, aTab, aNoNotification) { 3290 this.cleanUpRemovedBrowser(aTab); 3291 3292 if (!aNoNotification) { 3293 this.saveStateDelayed(aWindow); 3294 } 3295 }, 3296 3297 /** 3298 * When a tab closes, collect its properties 3299 * 3300 * @param {Window} aWindow 3301 * Window reference 3302 * @param {MozTabbrowserTab} aTab 3303 * Tab reference 3304 */ 3305 onTabClose: function ssi_onTabClose(aWindow, aTab) { 3306 // don't update our internal state if we don't have to 3307 if (this._max_tabs_undo == 0) { 3308 return; 3309 } 3310 3311 // Get the latest data for this tab (generally, from the cache) 3312 let tabState = lazy.TabState.collect(aTab, TAB_CUSTOM_VALUES.get(aTab)); 3313 3314 // Store closed-tab data for undo. 3315 this.maybeSaveClosedTab(aWindow, aTab, tabState); 3316 }, 3317 3318 onTabGroupRemoveRequested: function ssi_onTabGroupRemoveRequested( 3319 win, 3320 tabGroup 3321 ) { 3322 // don't update our internal state if we don't have to 3323 if (this._max_tabs_undo == 0) { 3324 return; 3325 } 3326 3327 if (this.getSavedTabGroup(tabGroup.id)) { 3328 // If a tab group is being removed from the tab strip but it's already 3329 // saved, then this is a "save and close" action; the saved tab group 3330 // should be stored in global session state rather than in this window's 3331 // closed tab groups. 3332 return; 3333 } 3334 3335 let closedGroups = this._windows[win.__SSi].closedGroups; 3336 let tabGroupState = lazy.TabGroupState.closed(tabGroup, win.__SSi); 3337 tabGroupState.tabs = this._collectClosedTabsForTabGroup(tabGroup.tabs, win); 3338 3339 // TODO(jswinarton) it's unclear if updating lastClosedTabGroupCount is 3340 // necessary when restoring tab groups — it largely depends on how we 3341 // decide to do the restore. 3342 // To address in bug1915174 3343 this._windows[win.__SSi]._lastClosedTabGroupCount = 3344 tabGroupState.tabs.length; 3345 closedGroups.unshift(tabGroupState); 3346 this._closedObjectsChanged = true; 3347 }, 3348 3349 /** 3350 * Collect closed tab states for a tab group that is about to be 3351 * saved and/or closed. 3352 * 3353 * The `TabGroupState` module is generally responsible for collecting 3354 * tab group state data, but the session store has additional requirements 3355 * for closed tabs that are currently only implemented in 3356 * `SessionStoreInternal.maybeSaveClosedTab`. This method converts the tabs 3357 * in a tab group into the closed tab data schema format required for 3358 * closed or saved groups. 3359 * 3360 * @param {MozTabbrowserTab[]} tabs 3361 * @param {Window} win 3362 * @param {object} [options] 3363 * @param {string} [options.updateTabGroupId] 3364 * Manually set a tab group id on the the tab state for each tab. 3365 * This is mainly used to add closing tabs to pre-existing 3366 * saved groups. 3367 * @returns {ClosedTabStateData[]} 3368 */ 3369 _collectClosedTabsForTabGroup(tabs, win, { updateTabGroupId } = {}) { 3370 let closedTabs = []; 3371 tabs.forEach(tab => { 3372 let tabState = lazy.TabState.collect(tab, TAB_CUSTOM_VALUES.get(tab)); 3373 if (updateTabGroupId) { 3374 tabState.groupId = updateTabGroupId; 3375 } 3376 this.maybeSaveClosedTab(win, tab, tabState, { 3377 closedTabsArray: closedTabs, 3378 closedInTabGroup: true, 3379 }); 3380 }); 3381 return closedTabs; 3382 }, 3383 3384 /** 3385 * Flush and copy tab state when moving a tab to a new window. 3386 * 3387 * @param aFromBrowser 3388 * Browser reference. 3389 * @param aToBrowser 3390 * Browser reference. 3391 */ 3392 onMoveToNewWindow(aFromBrowser, aToBrowser) { 3393 lazy.TabStateFlusher.flush(aFromBrowser).then(() => { 3394 let tabState = lazy.TabStateCache.get(aFromBrowser.permanentKey); 3395 if (!tabState) { 3396 throw new Error( 3397 "Unexpected undefined tabState for onMoveToNewWindow aFromBrowser" 3398 ); 3399 } 3400 lazy.TabStateCache.update(aToBrowser.permanentKey, tabState); 3401 }); 3402 }, 3403 3404 /** 3405 * Save a closed tab if needed. 3406 * 3407 * @param {Window} aWindow 3408 * Window reference. 3409 * @param {MozTabbrowserTab} aTab 3410 * Tab reference. 3411 * @param {TabStateData} tabState 3412 * Tab state. 3413 * @param {object} [options] 3414 * @param {TabStateData[]} [options.closedTabsArray] 3415 * The array of closed tabs to save to. This could be a 3416 * window's _closedTabs array or the tab list of a 3417 * closed tab group. 3418 * @param {boolean} [options.closedInTabGroup=false] 3419 * If this tab was closed due to the closing of a tab group. 3420 */ 3421 maybeSaveClosedTab( 3422 aWindow, 3423 aTab, 3424 tabState, 3425 { closedTabsArray, closedInTabGroup = false } = {} 3426 ) { 3427 // Don't save private tabs 3428 let isPrivateWindow = PrivateBrowsingUtils.isWindowPrivate(aWindow); 3429 if (!isPrivateWindow && tabState.isPrivate) { 3430 return; 3431 } 3432 if (aTab == aWindow.FirefoxViewHandler.tab) { 3433 return; 3434 } 3435 3436 let permanentKey = aTab.linkedBrowser.permanentKey; 3437 3438 let tabData = { 3439 permanentKey, 3440 state: tabState, 3441 title: aTab.label, 3442 image: aWindow.gBrowser.getIcon(aTab), 3443 pos: aTab._tPos, 3444 closedAt: Date.now(), 3445 closedInGroup: aTab._closedInMultiselection, 3446 closedInTabGroupId: closedInTabGroup ? tabState.groupId : null, 3447 sourceWindowId: aWindow.__SSi, 3448 }; 3449 3450 let winData = this._windows[aWindow.__SSi]; 3451 let closedTabs = closedTabsArray || winData._closedTabs; 3452 3453 // Determine whether the tab contains any information worth saving. Note 3454 // that there might be pending state changes queued in the child that 3455 // didn't reach the parent yet. If a tab is emptied before closing then we 3456 // might still remove it from the list of closed tabs later. 3457 if (this._shouldSaveTabState(tabState)) { 3458 // Save the tab state, for now. We might push a valid tab out 3459 // of the list but those cases should be extremely rare and 3460 // do probably never occur when using the browser normally. 3461 // (Tests or add-ons might do weird things though.) 3462 this.saveClosedTabData(winData, closedTabs, tabData); 3463 } 3464 3465 // Remember the closed tab to properly handle any last updates included in 3466 // the final "update" message sent by the frame script's unload handler. 3467 this._closingTabMap.set(permanentKey, { 3468 winData, 3469 closedTabs, 3470 tabData, 3471 }); 3472 }, 3473 3474 /** 3475 * Remove listeners which were added when browser was inserted and reset restoring state. 3476 * Also re-instate lazy data and basically revert tab to its lazy browser state. 3477 * 3478 * @param aTab 3479 * Tab reference 3480 */ 3481 resetBrowserToLazyState(aTab) { 3482 let browser = aTab.linkedBrowser; 3483 // Browser is already lazy so don't do anything. 3484 if (!browser.isConnected) { 3485 return; 3486 } 3487 3488 this.cleanUpRemovedBrowser(aTab); 3489 3490 aTab.setAttribute("pending", "true"); 3491 3492 this._lastKnownFrameLoader.delete(browser.permanentKey); 3493 this._crashedBrowsers.delete(browser.permanentKey); 3494 aTab.removeAttribute("crashed"); 3495 3496 let { userTypedValue = null, userTypedClear = 0 } = browser; 3497 let hasStartedLoad = browser.didStartLoadSinceLastUserTyping(); 3498 3499 let cacheState = lazy.TabStateCache.get(browser.permanentKey); 3500 3501 // Cache the browser userTypedValue either if there is no cache state 3502 // at all (e.g. if it was already discarded before we got to cache its state) 3503 // or it may have been created but not including a userTypedValue (e.g. 3504 // for a private tab we will cache `isPrivate: true` as soon as the tab 3505 // is opened). 3506 // 3507 // But only if: 3508 // 3509 // - if there is no cache state yet (which is unfortunately required 3510 // for tabs discarded immediately after creation by extensions, see 3511 // Bug 1422588). 3512 // 3513 // - or the user typed value was already being loaded (otherwise the lazy 3514 // tab will not be restored with the expected url once activated again, 3515 // see Bug 1724205). 3516 let shouldUpdateCacheState = 3517 userTypedValue && 3518 (!cacheState || (hasStartedLoad && !cacheState.userTypedValue)); 3519 3520 if (shouldUpdateCacheState) { 3521 // Discard was likely called before state can be cached. Update 3522 // the persistent tab state cache with browser information so a 3523 // restore will be successful. This information is necessary for 3524 // restoreTabContent in ContentRestore.sys.mjs to work properly. 3525 lazy.TabStateCache.update(browser.permanentKey, { 3526 userTypedValue, 3527 userTypedClear: 1, 3528 }); 3529 } 3530 3531 TAB_LAZY_STATES.set(aTab, { 3532 url: browser.currentURI.spec, 3533 title: aTab.label, 3534 userTypedValue, 3535 userTypedClear, 3536 }); 3537 }, 3538 3539 /** 3540 * Check if we are dealing with a crashed browser. If so, then the corresponding 3541 * crashed tab was revived by navigating to a different page. Remove the browser 3542 * from the list of crashed browsers to stop ignoring its messages. 3543 * 3544 * @param aBrowser 3545 * Browser reference 3546 */ 3547 maybeExitCrashedState(aBrowser) { 3548 let uri = aBrowser.documentURI; 3549 if (uri?.spec?.startsWith("about:tabcrashed")) { 3550 this._crashedBrowsers.delete(aBrowser.permanentKey); 3551 } 3552 }, 3553 3554 /** 3555 * A debugging-only function to check if a browser is in _crashedBrowsers. 3556 * 3557 * @param aBrowser 3558 * Browser reference 3559 */ 3560 isBrowserInCrashedSet(aBrowser) { 3561 if (gDebuggingEnabled) { 3562 return this._crashedBrowsers.has(aBrowser.permanentKey); 3563 } 3564 throw new Error( 3565 "SessionStore.isBrowserInCrashedSet() should only be called in debug mode!" 3566 ); 3567 }, 3568 3569 /** 3570 * When a tab is removed or suspended, remove listeners and reset restoring state. 3571 * 3572 * @param aBrowser 3573 * Browser reference 3574 */ 3575 cleanUpRemovedBrowser(aTab) { 3576 let browser = aTab.linkedBrowser; 3577 3578 browser.removeEventListener("SwapDocShells", this); 3579 browser.removeEventListener("oop-browser-crashed", this); 3580 browser.removeEventListener("oop-browser-buildid-mismatch", this); 3581 3582 // If this tab was in the middle of restoring or still needs to be restored, 3583 // we need to reset that state. If the tab was restoring, we will attempt to 3584 // restore the next tab. 3585 let previousState = TAB_STATE_FOR_BROWSER.get(browser); 3586 if (previousState) { 3587 this._resetTabRestoringState(aTab); 3588 if (previousState == TAB_STATE_RESTORING) { 3589 this.restoreNextTab(); 3590 } 3591 } 3592 }, 3593 3594 /** 3595 * Insert a given |tabData| object into the list of |closedTabs|. We will 3596 * determine the right insertion point based on the .closedAt properties of 3597 * all tabs already in the list. The list will be truncated to contain a 3598 * maximum of |this._max_tabs_undo| entries if required. 3599 * 3600 * @param {WindowStateData} winData 3601 * @param {ClosedTabStateData[]} closedTabs 3602 * The list of closed tabs for a window or tab group. 3603 * @param {ClosedTabStateData} tabData 3604 * The closed tab that should be inserted into `closedTabs` 3605 * @param {boolean} [saveAction=true] 3606 * Whether or not to add an action to the closed actions stack on save. 3607 */ 3608 saveClosedTabData(winData, closedTabs, tabData, saveAction = true) { 3609 // Find the index of the first tab in the list 3610 // of closed tabs that was closed before our tab. 3611 let index = tabData.closedInTabGroupId 3612 ? closedTabs.length 3613 : closedTabs.findIndex(tab => { 3614 return tab.closedAt < tabData.closedAt; 3615 }); 3616 3617 // If we found no tab closed before our 3618 // tab then just append it to the list. 3619 if (index == -1) { 3620 index = closedTabs.length; 3621 } 3622 3623 // About to save the closed tab, add a unique ID. 3624 tabData.closedId = this._nextClosedId++; 3625 3626 // Insert tabData at the right position. 3627 closedTabs.splice(index, 0, tabData); 3628 this._closedObjectsChanged = true; 3629 3630 if (tabData.closedInGroup) { 3631 if (winData._lastClosedTabGroupCount < this._max_tabs_undo) { 3632 if (winData._lastClosedTabGroupCount < 0) { 3633 winData._lastClosedTabGroupCount = 1; 3634 } else { 3635 winData._lastClosedTabGroupCount++; 3636 } 3637 } 3638 } else { 3639 winData._lastClosedTabGroupCount = -1; 3640 } 3641 3642 winData.lastClosedTabGroupId = tabData.closedInTabGroupId || null; 3643 3644 if (saveAction) { 3645 this._addClosedAction(this._LAST_ACTION_CLOSED_TAB, tabData.closedId); 3646 } 3647 3648 // Truncate the list of closed tabs, if needed. For closed tabs within tab 3649 // groups, always keep all closed tabs because users expect tab groups to 3650 // be intact. 3651 if ( 3652 !tabData.closedInTabGroupId && 3653 closedTabs.length > this._max_tabs_undo 3654 ) { 3655 closedTabs.splice(this._max_tabs_undo, closedTabs.length); 3656 } 3657 }, 3658 3659 /** 3660 * Remove the closed tab data at |index| from the list of |closedTabs|. If 3661 * the tab's final message is still pending we will simply discard it when 3662 * it arrives so that the tab doesn't reappear in the list. 3663 * 3664 * @param winData (object) 3665 * The data of the window. 3666 * @param index (uint) 3667 * The index of the tab to remove. 3668 * @param closedTabs (array) 3669 * The list of closed tabs for a window. 3670 */ 3671 removeClosedTabData(winData, closedTabs, index) { 3672 // Remove the given index from the list. 3673 let [closedTab] = closedTabs.splice(index, 1); 3674 this._closedObjectsChanged = true; 3675 3676 // If the tab is part of the last closed multiselected tab set, 3677 // we need to deduct the tab from the count. 3678 if (index < winData._lastClosedTabGroupCount) { 3679 winData._lastClosedTabGroupCount--; 3680 } 3681 3682 // If the closed tab's state still has a .permanentKey property then we 3683 // haven't seen its final update message yet. Remove it from the map of 3684 // closed tabs so that we will simply discard its last messages and will 3685 // not add it back to the list of closed tabs again. 3686 if (closedTab.permanentKey) { 3687 this._closingTabMap.delete(closedTab.permanentKey); 3688 this._tabClosingByWindowMap.delete(closedTab.permanentKey); 3689 delete closedTab.permanentKey; 3690 } 3691 3692 this._removeClosedAction(this._LAST_ACTION_CLOSED_TAB, closedTab.closedId); 3693 3694 return closedTab; 3695 }, 3696 3697 /** 3698 * When a tab is selected, save session data 3699 * 3700 * @param aWindow 3701 * Window reference 3702 */ 3703 onTabSelect: function ssi_onTabSelect(aWindow) { 3704 if (lazy.RunState.isRunning) { 3705 this._windows[aWindow.__SSi].selected = 3706 aWindow.gBrowser.tabContainer.selectedIndex; 3707 3708 let tab = aWindow.gBrowser.selectedTab; 3709 this.maybeRestoreTabContent(tab); 3710 } 3711 }, 3712 3713 maybeRestoreTabContent(tab) { 3714 let browser = tab.linkedBrowser; 3715 3716 if (TAB_STATE_FOR_BROWSER.get(browser) == TAB_STATE_NEEDS_RESTORE) { 3717 // If BROWSER_STATE is still available for the browser and it is 3718 // If __SS_restoreState is still on the browser and it is 3719 // TAB_STATE_NEEDS_RESTORE, then we haven't restored this tab yet. 3720 // 3721 // It's possible that this tab was recently revived, and that 3722 // we've deferred showing the tab crashed page for it (if the 3723 // tab crashed in the background). If so, we need to re-enter 3724 // the crashed state, since we'll be showing the tab crashed 3725 // page. 3726 if (lazy.TabCrashHandler.willShowCrashedTab(browser)) { 3727 this.enterCrashedState(browser); 3728 } else { 3729 this.restoreTabContent(tab); 3730 } 3731 } 3732 }, 3733 3734 onTabShow: function ssi_onTabShow(aWindow, aTab) { 3735 // If the tab hasn't been restored yet, move it into the right bucket 3736 if ( 3737 TAB_STATE_FOR_BROWSER.get(aTab.linkedBrowser) == TAB_STATE_NEEDS_RESTORE 3738 ) { 3739 TabRestoreQueue.hiddenToVisible(aTab); 3740 3741 // let's kick off tab restoration again to ensure this tab gets restored 3742 // with "restore_hidden_tabs" == false (now that it has become visible) 3743 this.restoreNextTab(); 3744 } 3745 3746 // Default delay of 2 seconds gives enough time to catch multiple TabShow 3747 // events. This used to be due to changing groups in 'tab groups'. We 3748 // might be able to get rid of this now? 3749 this.saveStateDelayed(aWindow); 3750 }, 3751 3752 onTabHide: function ssi_onTabHide(aWindow, aTab) { 3753 // If the tab hasn't been restored yet, move it into the right bucket 3754 if ( 3755 TAB_STATE_FOR_BROWSER.get(aTab.linkedBrowser) == TAB_STATE_NEEDS_RESTORE 3756 ) { 3757 TabRestoreQueue.visibleToHidden(aTab); 3758 } 3759 3760 // Default delay of 2 seconds gives enough time to catch multiple TabHide 3761 // events. This used to be due to changing groups in 'tab groups'. We 3762 // might be able to get rid of this now? 3763 this.saveStateDelayed(aWindow); 3764 }, 3765 3766 /** 3767 * Handler for the event that is fired when a <xul:browser> crashes. 3768 * 3769 * @param aWindow 3770 * The window that the crashed browser belongs to. 3771 * @param aBrowser 3772 * The <xul:browser> that is now in the crashed state. 3773 */ 3774 onBrowserCrashed(aBrowser) { 3775 this.enterCrashedState(aBrowser); 3776 // The browser crashed so we might never receive flush responses. 3777 // Resolve all pending flush requests for the crashed browser. 3778 lazy.TabStateFlusher.resolveAll(aBrowser); 3779 }, 3780 3781 /** 3782 * Called when a browser is showing or is about to show the tab 3783 * crashed page. This method causes SessionStore to ignore the 3784 * tab until it's restored. 3785 * 3786 * @param browser 3787 * The <xul:browser> that is about to show the crashed page. 3788 */ 3789 enterCrashedState(browser) { 3790 this._crashedBrowsers.add(browser.permanentKey); 3791 3792 let win = browser.ownerGlobal; 3793 3794 // If we hadn't yet restored, or were still in the midst of 3795 // restoring this browser at the time of the crash, we need 3796 // to reset its state so that we can try to restore it again 3797 // when the user revives the tab from the crash. 3798 if (TAB_STATE_FOR_BROWSER.has(browser)) { 3799 let tab = win.gBrowser.getTabForBrowser(browser); 3800 if (tab) { 3801 this._resetLocalTabRestoringState(tab); 3802 } 3803 } 3804 }, 3805 3806 // Clean up data that has been closed a long time ago. 3807 // Do not reschedule a save. This will wait for the next regular 3808 // save. 3809 onIdleDaily() { 3810 // Remove old closed windows 3811 this._cleanupOldData([this._closedWindows]); 3812 3813 // Remove closed tabs of closed windows 3814 this._cleanupOldData( 3815 this._closedWindows.map(winData => winData._closedTabs) 3816 ); 3817 3818 // Remove closed groups of closed windows 3819 this._cleanupOldData( 3820 this._closedWindows.map(winData => winData.closedGroups) 3821 ); 3822 3823 // Remove closed tabs of open windows 3824 this._cleanupOldData( 3825 Object.keys(this._windows).map(key => this._windows[key]._closedTabs) 3826 ); 3827 3828 // Remove closed groups of open windows 3829 this._cleanupOldData( 3830 Object.keys(this._windows).map(key => this._windows[key].closedGroups) 3831 ); 3832 3833 this._notifyOfClosedObjectsChange(); 3834 }, 3835 3836 // Remove "old" data from an array 3837 _cleanupOldData(targets) { 3838 const TIME_TO_LIVE = this._prefBranch.getIntPref( 3839 "sessionstore.cleanup.forget_closed_after" 3840 ); 3841 const now = Date.now(); 3842 3843 for (let array of targets) { 3844 for (let i = array.length - 1; i >= 0; --i) { 3845 let data = array[i]; 3846 // Make sure that we have a timestamp to tell us when the target 3847 // has been closed. If we don't have a timestamp, default to a 3848 // safe timestamp: just now. 3849 data.closedAt = data.closedAt || now; 3850 if (now - data.closedAt > TIME_TO_LIVE) { 3851 array.splice(i, 1); 3852 this._closedObjectsChanged = true; 3853 } 3854 } 3855 } 3856 }, 3857 3858 /* ........ nsISessionStore API .............. */ 3859 3860 getBrowserState: function ssi_getBrowserState() { 3861 let state = this.getCurrentState(); 3862 3863 // Don't include the last session state in getBrowserState(). 3864 delete state.lastSessionState; 3865 3866 // Don't include any deferred initial state. 3867 delete state.deferredInitialState; 3868 3869 return JSON.stringify(state); 3870 }, 3871 3872 setBrowserState: function ssi_setBrowserState(aState) { 3873 this._handleClosedWindows(); 3874 3875 try { 3876 var state = JSON.parse(aState); 3877 } catch (ex) { 3878 /* invalid state object - don't restore anything */ 3879 } 3880 if (!state) { 3881 throw Components.Exception( 3882 "Invalid state string: not JSON", 3883 Cr.NS_ERROR_INVALID_ARG 3884 ); 3885 } 3886 if (!state.windows) { 3887 throw Components.Exception("No windows", Cr.NS_ERROR_INVALID_ARG); 3888 } 3889 3890 this._browserSetState = true; 3891 3892 // Make sure the priority queue is emptied out 3893 this._resetRestoringState(); 3894 3895 var window = this._getTopWindow(); 3896 if (!window) { 3897 this._restoreCount = 1; 3898 this._openWindowWithState(state); 3899 return; 3900 } 3901 3902 // close all other browser windows 3903 for (let otherWin of this._browserWindows) { 3904 if (otherWin != window) { 3905 otherWin.close(); 3906 this.onClose(otherWin); 3907 } 3908 } 3909 3910 // make sure closed window data isn't kept 3911 if (this._closedWindows.length) { 3912 this._closedWindows = []; 3913 this._closedObjectsChanged = true; 3914 } 3915 3916 // determine how many windows are meant to be restored 3917 this._restoreCount = state.windows ? state.windows.length : 0; 3918 3919 // global data must be restored before restoreWindow is called so that 3920 // it happens before observers are notified 3921 this._globalState.setFromState(state); 3922 3923 // Restore session cookies. 3924 lazy.SessionCookies.restore(state.cookies || []); 3925 3926 // restore to the given state 3927 this.restoreWindows(window, state, { overwriteTabs: true }); 3928 3929 // Notify of changes to closed objects. 3930 this._notifyOfClosedObjectsChange(); 3931 }, 3932 3933 /** 3934 * @param {Window} aWindow 3935 * Window reference 3936 * @returns {{windows: WindowStateData[]}} 3937 */ 3938 getWindowState: function ssi_getWindowState(aWindow) { 3939 if ("__SSi" in aWindow) { 3940 return Cu.cloneInto(this._getWindowState(aWindow), {}); 3941 } 3942 3943 if (DyingWindowCache.has(aWindow)) { 3944 let data = DyingWindowCache.get(aWindow); 3945 return Cu.cloneInto({ windows: [data] }, {}); 3946 } 3947 3948 throw Components.Exception( 3949 "Window is not tracked", 3950 Cr.NS_ERROR_INVALID_ARG 3951 ); 3952 }, 3953 3954 setWindowState: function ssi_setWindowState(aWindow, aState, aOverwrite) { 3955 if (!aWindow.__SSi) { 3956 throw Components.Exception( 3957 "Window is not tracked", 3958 Cr.NS_ERROR_INVALID_ARG 3959 ); 3960 } 3961 3962 this.restoreWindows(aWindow, aState, { overwriteTabs: aOverwrite }); 3963 3964 // Notify of changes to closed objects. 3965 this._notifyOfClosedObjectsChange(); 3966 }, 3967 3968 getTabState: function ssi_getTabState(aTab) { 3969 if (!aTab || !aTab.ownerGlobal) { 3970 throw Components.Exception("Need a valid tab", Cr.NS_ERROR_INVALID_ARG); 3971 } 3972 if (!aTab.ownerGlobal.__SSi) { 3973 throw Components.Exception( 3974 "Default view is not tracked", 3975 Cr.NS_ERROR_INVALID_ARG 3976 ); 3977 } 3978 3979 let tabState = lazy.TabState.collect(aTab, TAB_CUSTOM_VALUES.get(aTab)); 3980 3981 return JSON.stringify(tabState); 3982 }, 3983 3984 setTabState(aTab, aState) { 3985 // Remove the tab state from the cache. 3986 // Note that we cannot simply replace the contents of the cache 3987 // as |aState| can be an incomplete state that will be completed 3988 // by |restoreTabs|. 3989 let tabState = aState; 3990 if (typeof tabState == "string") { 3991 tabState = JSON.parse(aState); 3992 } 3993 if (!tabState) { 3994 throw Components.Exception( 3995 "Invalid state string: not JSON", 3996 Cr.NS_ERROR_INVALID_ARG 3997 ); 3998 } 3999 if (typeof tabState != "object") { 4000 throw Components.Exception("Not an object", Cr.NS_ERROR_INVALID_ARG); 4001 } 4002 if (!("entries" in tabState)) { 4003 throw Components.Exception( 4004 "Invalid state object: no entries", 4005 Cr.NS_ERROR_INVALID_ARG 4006 ); 4007 } 4008 4009 let window = aTab.ownerGlobal; 4010 if (!window || !("__SSi" in window)) { 4011 throw Components.Exception( 4012 "Window is not tracked", 4013 Cr.NS_ERROR_INVALID_ARG 4014 ); 4015 } 4016 4017 if (TAB_STATE_FOR_BROWSER.has(aTab.linkedBrowser)) { 4018 this._resetTabRestoringState(aTab); 4019 } 4020 4021 this._ensureNoNullsInTabDataList( 4022 window.gBrowser.tabs, 4023 this._windows[window.__SSi].tabs, 4024 aTab._tPos 4025 ); 4026 this.restoreTab(aTab, tabState); 4027 4028 // Notify of changes to closed objects. 4029 this._notifyOfClosedObjectsChange(); 4030 }, 4031 4032 getInternalObjectState(obj) { 4033 if (obj.__SSi) { 4034 return this._windows[obj.__SSi]; 4035 } 4036 return obj.loadURI 4037 ? TAB_STATE_FOR_BROWSER.get(obj) 4038 : TAB_CUSTOM_VALUES.get(obj); 4039 }, 4040 4041 getObjectTypeForClosedId(aClosedId) { 4042 // check if matches a window first 4043 if (this.getClosedWindowDataByClosedId(aClosedId)) { 4044 return this._LAST_ACTION_CLOSED_WINDOW; 4045 } 4046 return this._LAST_ACTION_CLOSED_TAB; 4047 }, 4048 4049 /** 4050 * @param {number} aClosedId 4051 * @returns {WindowStateData|undefined} 4052 */ 4053 getClosedWindowDataByClosedId: function ssi_getClosedWindowDataByClosedId( 4054 aClosedId 4055 ) { 4056 return this._closedWindows.find( 4057 closedData => closedData.closedId == aClosedId 4058 ); 4059 }, 4060 4061 getWindowById: function ssi_getWindowById(aSessionStoreId) { 4062 let resultWindow; 4063 for (let window of this._browserWindows) { 4064 if (window.__SSi === aSessionStoreId) { 4065 resultWindow = window; 4066 break; 4067 } 4068 } 4069 return resultWindow; 4070 }, 4071 4072 duplicateTab: function ssi_duplicateTab( 4073 aWindow, 4074 aTab, 4075 aDelta = 0, 4076 aRestoreImmediately = true, 4077 { inBackground, tabIndex } = {} 4078 ) { 4079 if (!aTab || !aTab.ownerGlobal) { 4080 throw Components.Exception("Need a valid tab", Cr.NS_ERROR_INVALID_ARG); 4081 } 4082 if (!aTab.ownerGlobal.__SSi) { 4083 throw Components.Exception( 4084 "Default view is not tracked", 4085 Cr.NS_ERROR_INVALID_ARG 4086 ); 4087 } 4088 if (!aWindow.gBrowser) { 4089 throw Components.Exception( 4090 "Invalid window object: no gBrowser", 4091 Cr.NS_ERROR_INVALID_ARG 4092 ); 4093 } 4094 4095 // Create a new tab. 4096 let userContextId = aTab.getAttribute("usercontextid") || ""; 4097 4098 let tabOptions = { 4099 userContextId, 4100 tabIndex, 4101 ...(aTab == aWindow.gBrowser.selectedTab 4102 ? { relatedToCurrent: true, ownerTab: aTab } 4103 : {}), 4104 skipLoad: true, 4105 preferredRemoteType: aTab.linkedBrowser.remoteType, 4106 tabGroup: aTab.group, 4107 }; 4108 let newTab = aWindow.gBrowser.addTrustedTab(null, tabOptions); 4109 4110 // Start the throbber to pretend we're doing something while actually 4111 // waiting for data from the frame script. This throbber is disabled 4112 // if the URI is a local about: URI. 4113 let uriObj = aTab.linkedBrowser.currentURI; 4114 if (!uriObj || (uriObj && !uriObj.schemeIs("about"))) { 4115 newTab.setAttribute("busy", "true"); 4116 } 4117 4118 // Hack to ensure that the about:home, about:newtab, and about:welcome 4119 // favicon is loaded instantaneously, to avoid flickering and improve 4120 // perceived performance. 4121 aWindow.gBrowser.setDefaultIcon(newTab, uriObj); 4122 4123 // Collect state before flushing. 4124 let tabState = lazy.TabState.collect(aTab, TAB_CUSTOM_VALUES.get(aTab)); 4125 4126 // Flush to get the latest tab state to duplicate. 4127 let browser = aTab.linkedBrowser; 4128 lazy.TabStateFlusher.flush(browser).then(() => { 4129 // The new tab might have been closed in the meantime. 4130 if (newTab.closing || !newTab.linkedBrowser) { 4131 return; 4132 } 4133 4134 let window = newTab.ownerGlobal; 4135 4136 // The tab or its window might be gone. 4137 if (!window || !window.__SSi) { 4138 return; 4139 } 4140 4141 // Update state with flushed data. We can't use TabState.clone() here as 4142 // the tab to duplicate may have already been closed. In that case we 4143 // only have access to the <xul:browser>. 4144 let options = { includePrivateData: true }; 4145 lazy.TabState.copyFromCache(browser.permanentKey, tabState, options); 4146 4147 tabState.index += aDelta; 4148 tabState.index = Math.max( 4149 1, 4150 Math.min(tabState.index, tabState.entries.length) 4151 ); 4152 tabState.pinned = false; 4153 4154 if (inBackground === false) { 4155 aWindow.gBrowser.selectedTab = newTab; 4156 } 4157 4158 // Restore the state into the new tab. 4159 this.restoreTab(newTab, tabState, { 4160 restoreImmediately: aRestoreImmediately, 4161 }); 4162 }); 4163 4164 return newTab; 4165 }, 4166 4167 getWindows(aWindowOrOptions) { 4168 let isPrivate; 4169 if (!aWindowOrOptions) { 4170 aWindowOrOptions = this._getTopWindow(); 4171 } 4172 if (aWindowOrOptions instanceof Ci.nsIDOMWindow) { 4173 isPrivate = PrivateBrowsingUtils.isBrowserPrivate(aWindowOrOptions); 4174 } else { 4175 isPrivate = Boolean(aWindowOrOptions.private); 4176 } 4177 4178 const browserWindows = Array.from(this._browserWindows).filter(win => { 4179 return PrivateBrowsingUtils.isBrowserPrivate(win) === isPrivate; 4180 }); 4181 return browserWindows; 4182 }, 4183 4184 getWindowForTabClosedId(aClosedId, aIncludePrivate) { 4185 // check non-private windows first, and then only check private windows if 4186 // aIncludePrivate was true 4187 const privateValues = aIncludePrivate ? [false, true] : [false]; 4188 for (let privateness of privateValues) { 4189 for (let window of this.getWindows({ private: privateness })) { 4190 const windowState = this._windows[window.__SSi]; 4191 const closedTabs = 4192 this._getStateForClosedTabsAndClosedGroupTabs(windowState); 4193 if (!closedTabs.length) { 4194 continue; 4195 } 4196 if (closedTabs.find(tab => tab.closedId === aClosedId)) { 4197 return window; 4198 } 4199 } 4200 } 4201 return undefined; 4202 }, 4203 4204 getLastClosedTabCount(aWindow) { 4205 if ("__SSi" in aWindow) { 4206 return Math.min( 4207 Math.max(this._windows[aWindow.__SSi]._lastClosedTabGroupCount, 1), 4208 this.getClosedTabCountForWindow(aWindow) 4209 ); 4210 } 4211 4212 throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); 4213 }, 4214 4215 resetLastClosedTabCount(aWindow) { 4216 if ("__SSi" in aWindow) { 4217 this._windows[aWindow.__SSi]._lastClosedTabGroupCount = -1; 4218 this._windows[aWindow.__SSi].lastClosedTabGroupId = null; 4219 } else { 4220 throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); 4221 } 4222 }, 4223 4224 getClosedTabCountForWindow: function ssi_getClosedTabCountForWindow(aWindow) { 4225 if ("__SSi" in aWindow) { 4226 return this._getStateForClosedTabsAndClosedGroupTabs( 4227 this._windows[aWindow.__SSi] 4228 ).length; 4229 } 4230 4231 if (!DyingWindowCache.has(aWindow)) { 4232 throw Components.Exception( 4233 "Window is not tracked", 4234 Cr.NS_ERROR_INVALID_ARG 4235 ); 4236 } 4237 4238 return this._getStateForClosedTabsAndClosedGroupTabs( 4239 DyingWindowCache.get(aWindow) 4240 ).length; 4241 }, 4242 4243 _prepareClosedTabOptions(aOptions = {}) { 4244 const sourceOptions = Object.assign( 4245 { 4246 closedTabsFromAllWindows: this._closedTabsFromAllWindowsEnabled, 4247 closedTabsFromClosedWindows: this._closedTabsFromClosedWindowsEnabled, 4248 sourceWindow: null, 4249 }, 4250 aOptions instanceof Ci.nsIDOMWindow 4251 ? { sourceWindow: aOptions } 4252 : aOptions 4253 ); 4254 if (!sourceOptions.sourceWindow) { 4255 sourceOptions.sourceWindow = this._getTopWindow(sourceOptions.private); 4256 } 4257 /* 4258 _getTopWindow may return null on MacOS when the last window has been closed. 4259 Since private browsing windows are irrelevant after they have been closed we 4260 don't need to check if it was a private browsing window. 4261 */ 4262 if (!sourceOptions.sourceWindow) { 4263 sourceOptions.private = false; 4264 } 4265 if (!sourceOptions.hasOwnProperty("private")) { 4266 sourceOptions.private = PrivateBrowsingUtils.isWindowPrivate( 4267 sourceOptions.sourceWindow 4268 ); 4269 } 4270 return sourceOptions; 4271 }, 4272 4273 getClosedTabCount(aOptions) { 4274 const sourceOptions = this._prepareClosedTabOptions(aOptions); 4275 let tabCount = 0; 4276 4277 if (sourceOptions.closedTabsFromAllWindows) { 4278 tabCount += this.getWindows({ private: sourceOptions.private }) 4279 .map(win => this.getClosedTabCountForWindow(win)) 4280 .reduce((total, count) => total + count, 0); 4281 } else { 4282 tabCount += this.getClosedTabCountForWindow(sourceOptions.sourceWindow); 4283 } 4284 4285 if (!sourceOptions.private && sourceOptions.closedTabsFromClosedWindows) { 4286 tabCount += this.getClosedTabCountFromClosedWindows(); 4287 } 4288 return tabCount; 4289 }, 4290 4291 getClosedTabCountFromClosedWindows: 4292 function ssi_getClosedTabCountFromClosedWindows() { 4293 const tabCount = this._closedWindows 4294 .map( 4295 winData => 4296 this._getStateForClosedTabsAndClosedGroupTabs(winData).length 4297 ) 4298 .reduce((total, count) => total + count, 0); 4299 return tabCount; 4300 }, 4301 4302 getClosedTabDataForWindow: function ssi_getClosedTabDataForWindow(aWindow) { 4303 return this._getClonedDataForWindow( 4304 aWindow, 4305 this._getStateForClosedTabsAndClosedGroupTabs 4306 ); 4307 }, 4308 4309 getClosedTabData: function ssi_getClosedTabData(aOptions) { 4310 const sourceOptions = this._prepareClosedTabOptions(aOptions); 4311 const closedTabData = []; 4312 if (sourceOptions.closedTabsFromAllWindows) { 4313 for (let win of this.getWindows({ private: sourceOptions.private })) { 4314 closedTabData.push(...this.getClosedTabDataForWindow(win)); 4315 } 4316 } else { 4317 closedTabData.push( 4318 ...this.getClosedTabDataForWindow(sourceOptions.sourceWindow) 4319 ); 4320 } 4321 return closedTabData; 4322 }, 4323 4324 getClosedTabDataFromClosedWindows: 4325 function ssi_getClosedTabDataFromClosedWindows() { 4326 const closedTabData = []; 4327 for (let winData of this._closedWindows) { 4328 const sourceClosedId = winData.closedId; 4329 const closedTabs = Cu.cloneInto( 4330 this._getStateForClosedTabsAndClosedGroupTabs(winData), 4331 {} 4332 ); 4333 // Add a property pointing back to the closed window source 4334 for (let tabData of closedTabs) { 4335 tabData.sourceClosedId = sourceClosedId; 4336 } 4337 closedTabData.push(...closedTabs); 4338 } 4339 // sorting is left to the caller 4340 return closedTabData; 4341 }, 4342 4343 /** 4344 * @param {Window|object} aOptions 4345 * @param {Window} [aOptions.sourceWindow] 4346 * @param {boolean} [aOptions.private = false] 4347 * @param {boolean} [aOptions.closedTabsFromAllWindows] 4348 * @param {boolean} [aOptions.closedTabsFromClosedWindows] 4349 * @returns {ClosedTabGroupStateData[]} 4350 */ 4351 getClosedTabGroups: function ssi_getClosedTabGroups(aOptions) { 4352 const sourceOptions = this._prepareClosedTabOptions(aOptions); 4353 const closedTabGroups = []; 4354 if (sourceOptions.closedTabsFromAllWindows) { 4355 for (let win of this.getWindows({ private: sourceOptions.private })) { 4356 closedTabGroups.push( 4357 ...this._getClonedDataForWindow(win, w => w.closedGroups ?? []) 4358 ); 4359 } 4360 } else if (sourceOptions.sourceWindow.closedGroups) { 4361 closedTabGroups.push( 4362 ...this._getClonedDataForWindow( 4363 sourceOptions.sourceWindow, 4364 w => w.closedGroups ?? [] 4365 ) 4366 ); 4367 } 4368 4369 if (sourceOptions.closedTabsFromClosedWindows) { 4370 for (let winData of this.getClosedWindowData()) { 4371 if (!winData.closedGroups) { 4372 continue; 4373 } 4374 // Add a property pointing back to the closed window source 4375 for (let groupData of winData.closedGroups) { 4376 for (let tabData of groupData.tabs) { 4377 tabData.sourceClosedId = winData.closedId; 4378 } 4379 } 4380 closedTabGroups.push(...winData.closedGroups); 4381 } 4382 } 4383 return closedTabGroups; 4384 }, 4385 4386 getLastClosedTabGroupId(aWindow) { 4387 if ("__SSi" in aWindow) { 4388 return this._windows[aWindow.__SSi].lastClosedTabGroupId; 4389 } 4390 4391 throw new Error("Window is not tracked"); 4392 }, 4393 4394 /** 4395 * Returns a clone of some subset of a window's state data. 4396 * 4397 * @template D 4398 * @param {Window} aWindow 4399 * @param {function(WindowStateData):D} selector 4400 * A function that returns the desired data located within 4401 * a supplied window state. 4402 * @returns {D} 4403 */ 4404 _getClonedDataForWindow: function ssi_getClonedDataForWindow( 4405 aWindow, 4406 selector 4407 ) { 4408 // We need to enable wrapping reflectors in order to allow the cloning of 4409 // objects containing FormDatas, which could be stored by 4410 // form-associated custom elements. 4411 let options = { wrapReflectors: true }; 4412 /** @type {WindowStateData} */ 4413 let winData; 4414 4415 if ("__SSi" in aWindow) { 4416 winData = this._windows[aWindow.__SSi]; 4417 } 4418 4419 if (!winData && !DyingWindowCache.has(aWindow)) { 4420 throw Components.Exception( 4421 "Window is not tracked", 4422 Cr.NS_ERROR_INVALID_ARG 4423 ); 4424 } 4425 4426 winData ??= DyingWindowCache.get(aWindow); 4427 let data = selector(winData); 4428 return Cu.cloneInto(data, {}, options); 4429 }, 4430 4431 /** 4432 * Returns either a unified list of closed tabs from both 4433 * `_closedTabs` and `closedGroups` or else, when supplying an index, 4434 * returns the specific closed tab from that unified list. 4435 * 4436 * This bridges the gap between callers that want a unified list of all closed tabs 4437 * from all contexts vs. callers that want a specific list of closed tabs from a 4438 * specific context (e.g. only closed tabs from a specific closed tab group). 4439 * 4440 * @param {WindowStateData} winData 4441 * @param {number} [aIndex] 4442 * If not supplied, returns all closed tabs and tabs from closed tab groups. 4443 * If supplied, returns the single closed tab with the given index. 4444 * @returns {TabStateData|TabStateData[]} 4445 */ 4446 _getStateForClosedTabsAndClosedGroupTabs: 4447 function ssi_getStateForClosedTabsAndClosedGroupTabs(winData, aIndex) { 4448 const closedGroups = winData.closedGroups ?? []; 4449 const closedTabs = winData._closedTabs ?? []; 4450 4451 // Merge tabs and groups into a single sorted array of tabs sorted by 4452 // closedAt 4453 let result = []; 4454 let groupIdx = 0; 4455 let tabIdx = 0; 4456 let current = 0; 4457 let totalLength = closedGroups.length + closedTabs.length; 4458 4459 while (current < totalLength) { 4460 let group = closedGroups[groupIdx]; 4461 let tab = closedTabs[tabIdx]; 4462 4463 if ( 4464 groupIdx < closedGroups.length && 4465 (tabIdx >= closedTabs.length || group?.closedAt > tab?.closedAt) 4466 ) { 4467 group.tabs.forEach((groupTab, idx) => { 4468 groupTab._originalStateIndex = idx; 4469 groupTab._originalGroupStateIndex = groupIdx; 4470 result.push(groupTab); 4471 }); 4472 groupIdx++; 4473 } else { 4474 tab._originalStateIndex = tabIdx; 4475 result.push(tab); 4476 tabIdx++; 4477 } 4478 4479 current++; 4480 if (current > aIndex) { 4481 break; 4482 } 4483 } 4484 4485 if (aIndex !== undefined) { 4486 return result[aIndex]; 4487 } 4488 4489 return result; 4490 }, 4491 4492 /** 4493 * For a given closed tab that was retrieved by `_getStateForClosedTabsAndClosedGroupTabs`, 4494 * returns the specific closed tab list data source and the index within that data source 4495 * where the closed tab can be found. 4496 * 4497 * This bridges the gap between callers that want a unified list of all closed tabs 4498 * from all contexts vs. callers that want a specific list of closed tabs from a 4499 * specific context (e.g. only closed tabs from a specific closed tab group). 4500 * 4501 * @param {WindowState} sourceWinData 4502 * @param {TabStateData} tabState 4503 * @returns {{closedTabSet: TabStateData[], closedTabIndex: number}} 4504 */ 4505 _getClosedTabStateFromUnifiedIndex: function ssi_getClosedTabForUnifiedIndex( 4506 sourceWinData, 4507 tabState 4508 ) { 4509 let closedTabSet, closedTabIndex; 4510 if (tabState._originalGroupStateIndex == null) { 4511 closedTabSet = sourceWinData._closedTabs; 4512 } else { 4513 closedTabSet = 4514 sourceWinData.closedGroups[tabState._originalGroupStateIndex].tabs; 4515 } 4516 closedTabIndex = tabState._originalStateIndex; 4517 4518 return { closedTabSet, closedTabIndex }; 4519 }, 4520 4521 undoCloseTab: function ssi_undoCloseTab(aSource, aIndex, aTargetWindow) { 4522 const sourceWinData = this._resolveClosedDataSource(aSource); 4523 const isPrivateSource = Boolean(sourceWinData.isPrivate); 4524 if (aTargetWindow && !aTargetWindow.__SSi) { 4525 throw Components.Exception( 4526 "Target window is not tracked", 4527 Cr.NS_ERROR_INVALID_ARG 4528 ); 4529 } else if (!aTargetWindow) { 4530 aTargetWindow = this._getTopWindow(isPrivateSource); 4531 } 4532 if ( 4533 isPrivateSource !== PrivateBrowsingUtils.isWindowPrivate(aTargetWindow) 4534 ) { 4535 throw Components.Exception( 4536 "Target window doesn't have the same privateness as the source window", 4537 Cr.NS_ERROR_INVALID_ARG 4538 ); 4539 } 4540 4541 // default to the most-recently closed tab 4542 aIndex = aIndex || 0; 4543 4544 const closedTabState = this._getStateForClosedTabsAndClosedGroupTabs( 4545 sourceWinData, 4546 aIndex 4547 ); 4548 if (!closedTabState) { 4549 throw Components.Exception( 4550 "Invalid index: not in the closed tabs", 4551 Cr.NS_ERROR_INVALID_ARG 4552 ); 4553 } 4554 let { closedTabSet, closedTabIndex } = 4555 this._getClosedTabStateFromUnifiedIndex(sourceWinData, closedTabState); 4556 4557 // fetch the data of closed tab, while removing it from the array 4558 let { state, pos } = this.removeClosedTabData( 4559 sourceWinData, 4560 closedTabSet, 4561 closedTabIndex 4562 ); 4563 this._cleanupOrphanedClosedGroups(sourceWinData); 4564 4565 // Predict the remote type to use for the load to avoid unnecessary process 4566 // switches. 4567 let preferredRemoteType = lazy.E10SUtils.DEFAULT_REMOTE_TYPE; 4568 let url; 4569 if (state.entries?.length) { 4570 let activeIndex = (state.index || state.entries.length) - 1; 4571 activeIndex = Math.min(activeIndex, state.entries.length - 1); 4572 activeIndex = Math.max(activeIndex, 0); 4573 url = state.entries[activeIndex].url; 4574 } 4575 if (url) { 4576 preferredRemoteType = this.getPreferredRemoteType( 4577 url, 4578 aTargetWindow, 4579 state.userContextId 4580 ); 4581 } 4582 4583 // create a new tab 4584 let tabbrowser = aTargetWindow.gBrowser; 4585 let tab = (tabbrowser.selectedTab = tabbrowser.addTrustedTab(null, { 4586 // Append the tab if we're opening into a different window, 4587 tabIndex: aSource == aTargetWindow ? pos : Infinity, 4588 pinned: state.pinned, 4589 userContextId: state.userContextId, 4590 skipLoad: true, 4591 preferredRemoteType, 4592 tabGroup: tabbrowser.tabGroups.find(g => g.id == state.groupId), 4593 })); 4594 4595 // restore tab content 4596 this.restoreTab(tab, state); 4597 4598 // Notify of changes to closed objects. 4599 this._notifyOfClosedObjectsChange(); 4600 4601 return tab; 4602 }, 4603 4604 undoClosedTabFromClosedWindow: function ssi_undoClosedTabFromClosedWindow( 4605 aSource, 4606 aClosedId, 4607 aTargetWindow 4608 ) { 4609 const sourceWinData = this._resolveClosedDataSource(aSource); 4610 const closedTabs = 4611 this._getStateForClosedTabsAndClosedGroupTabs(sourceWinData); 4612 const closedIndex = closedTabs.findIndex( 4613 tabData => tabData.closedId == aClosedId 4614 ); 4615 if (closedIndex >= 0) { 4616 return this.undoCloseTab(aSource, closedIndex, aTargetWindow); 4617 } 4618 throw Components.Exception( 4619 "Invalid closedId: not in the closed tabs", 4620 Cr.NS_ERROR_INVALID_ARG 4621 ); 4622 }, 4623 4624 getPreferredRemoteType(url, aWindow, userContextId) { 4625 return lazy.E10SUtils.getRemoteTypeForURI( 4626 url, 4627 aWindow.gMultiProcessBrowser, 4628 aWindow.gFissionBrowser, 4629 lazy.E10SUtils.DEFAULT_REMOTE_TYPE, 4630 null, 4631 lazy.E10SUtils.predictOriginAttributes({ 4632 window: aWindow, 4633 userContextId, 4634 }) 4635 ); 4636 }, 4637 4638 /** 4639 * @param {Window|{sourceWindow: Window}|{sourceClosedId: number}|{sourceWindowId: string}} aSource 4640 * @returns {WindowStateData} 4641 */ 4642 _resolveClosedDataSource(aSource) { 4643 let winData; 4644 if (aSource instanceof Ci.nsIDOMWindow) { 4645 winData = this.getWindowStateData(aSource); 4646 } else if (aSource.sourceWindow instanceof Ci.nsIDOMWindow) { 4647 winData = this.getWindowStateData(aSource.sourceWindow); 4648 } else if (typeof aSource.sourceClosedId == "number") { 4649 winData = this.getClosedWindowDataByClosedId(aSource.sourceClosedId); 4650 if (!winData) { 4651 throw Components.Exception( 4652 "No such closed window", 4653 Cr.NS_ERROR_INVALID_ARG 4654 ); 4655 } 4656 } else if (typeof aSource.sourceWindowId == "string") { 4657 let win = this.getWindowById(aSource.sourceWindowId); 4658 winData = this.getWindowStateData(win); 4659 } else { 4660 throw Components.Exception( 4661 "Invalid source object", 4662 Cr.NS_ERROR_INVALID_ARG 4663 ); 4664 } 4665 return winData; 4666 }, 4667 4668 forgetClosedTab: function ssi_forgetClosedTab(aSource, aIndex) { 4669 const winData = this._resolveClosedDataSource(aSource); 4670 // default to the most-recently closed tab 4671 aIndex = aIndex || 0; 4672 if (!(aIndex in winData._closedTabs)) { 4673 throw Components.Exception( 4674 "Invalid index: not in the closed tabs", 4675 Cr.NS_ERROR_INVALID_ARG 4676 ); 4677 } 4678 4679 // remove closed tab from the array 4680 this.removeClosedTabData(winData, winData._closedTabs, aIndex); 4681 4682 // Notify of changes to closed objects. 4683 this._notifyOfClosedObjectsChange(); 4684 }, 4685 4686 forgetClosedTabGroup: function ssi_forgetClosedTabGroup(aSource, tabGroupId) { 4687 const winData = this._resolveClosedDataSource(aSource); 4688 let closedGroupIndex = winData.closedGroups.findIndex( 4689 closedTabGroup => closedTabGroup.id == tabGroupId 4690 ); 4691 // let closedTabGroup = this.getClosedTabGroup(aSource, tabGroupId); 4692 if (closedGroupIndex < 0) { 4693 throw Components.Exception( 4694 "Closed tab group not found", 4695 Cr.NS_ERROR_INVALID_ARG 4696 ); 4697 } 4698 4699 let closedGroup = winData.closedGroups[closedGroupIndex]; 4700 while (closedGroup.tabs.length) { 4701 this.removeClosedTabData(winData, closedGroup.tabs, 0); 4702 } 4703 winData.closedGroups.splice(closedGroupIndex, 1); 4704 4705 // Notify of changes to closed objects. 4706 this._notifyOfClosedObjectsChange(); 4707 }, 4708 4709 /** 4710 * @param {string} savedTabGroupId 4711 */ 4712 forgetSavedTabGroup: function ssi_forgetSavedTabGroup(savedTabGroupId) { 4713 let savedGroupIndex = this._savedGroups.findIndex( 4714 savedTabGroup => savedTabGroup.id == savedTabGroupId 4715 ); 4716 if (savedGroupIndex < 0) { 4717 throw Components.Exception( 4718 "Saved tab group not found", 4719 Cr.NS_ERROR_INVALID_ARG 4720 ); 4721 } 4722 4723 let savedGroup = this._savedGroups[savedGroupIndex]; 4724 for (let i = 0; i < savedGroup.tabs.length; i++) { 4725 this.removeClosedTabData({}, savedGroup.tabs, i); 4726 } 4727 this._savedGroups.splice(savedGroupIndex, 1); 4728 this._notifyOfSavedTabGroupsChange(); 4729 4730 // Notify of changes to closed objects. 4731 this._closedObjectsChanged = true; 4732 this._notifyOfClosedObjectsChange(); 4733 }, 4734 4735 forgetClosedWindowById(aClosedId) { 4736 // We don't keep any record for closed private windows so privateness is not relevant here 4737 let closedIndex = this._closedWindows.findIndex( 4738 windowState => windowState.closedId == aClosedId 4739 ); 4740 if (closedIndex < 0) { 4741 throw Components.Exception( 4742 "Invalid closedId: not in the closed windows", 4743 Cr.NS_ERROR_INVALID_ARG 4744 ); 4745 } 4746 this.forgetClosedWindow(closedIndex); 4747 }, 4748 4749 forgetClosedTabById(aClosedId, aSourceOptions = {}) { 4750 let sourceWindowsData; 4751 let searchPrivateWindows = aSourceOptions.includePrivate ?? true; 4752 if ( 4753 aSourceOptions instanceof Ci.nsIDOMWindow || 4754 "sourceWindowId" in aSourceOptions || 4755 "sourceClosedId" in aSourceOptions 4756 ) { 4757 sourceWindowsData = [this._resolveClosedDataSource(aSourceOptions)]; 4758 } else { 4759 // Get the windows we'll look for the closed tab in, filtering out private 4760 // windows if necessary 4761 let browserWindows = Array.from(this._browserWindows); 4762 sourceWindowsData = []; 4763 for (let win of browserWindows) { 4764 if ( 4765 !searchPrivateWindows && 4766 PrivateBrowsingUtils.isBrowserPrivate(win) 4767 ) { 4768 continue; 4769 } 4770 sourceWindowsData.push(this._windows[win.__SSi]); 4771 } 4772 } 4773 4774 // See if the aClosedId matches a closed tab in any window data 4775 for (let winData of sourceWindowsData) { 4776 let closedTabs = this._getStateForClosedTabsAndClosedGroupTabs(winData); 4777 let closedTabState = closedTabs.find( 4778 tabData => tabData.closedId == aClosedId 4779 ); 4780 4781 if (closedTabState) { 4782 let { closedTabSet, closedTabIndex } = 4783 this._getClosedTabStateFromUnifiedIndex(winData, closedTabState); 4784 // remove closed tab from the array 4785 this.removeClosedTabData(winData, closedTabSet, closedTabIndex); 4786 // Notify of changes to closed objects. 4787 this._notifyOfClosedObjectsChange(); 4788 return; 4789 } 4790 } 4791 4792 throw Components.Exception( 4793 "Invalid closedId: not found in the closed tabs of any window", 4794 Cr.NS_ERROR_INVALID_ARG 4795 ); 4796 }, 4797 4798 getClosedWindowCount: function ssi_getClosedWindowCount() { 4799 return this._closedWindows.length; 4800 }, 4801 4802 /** 4803 * @returns {WindowStateData[]} 4804 */ 4805 getClosedWindowData: function ssi_getClosedWindowData() { 4806 let closedWindows = Cu.cloneInto(this._closedWindows, {}); 4807 for (let closedWinData of closedWindows) { 4808 this._trimSavedTabGroupMetadataInClosedWindow(closedWinData); 4809 } 4810 return closedWindows; 4811 }, 4812 4813 /** 4814 * If a closed window has a saved tab group inside of it, the closed window's 4815 * `groups` array entry will be a reference to a saved tab group entry. 4816 * However, since saved tab groups contain a lot of extra and duplicate 4817 * information, like their `tabs`, we only want to surface some of the 4818 * metadata about the saved tab groups to outside clients. 4819 * 4820 * @param {WindowStateData} closedWinData 4821 * @returns {void} mutates the argument `closedWinData` 4822 */ 4823 _trimSavedTabGroupMetadataInClosedWindow(closedWinData) { 4824 let abbreviatedGroups = closedWinData.groups?.map(tabGroup => 4825 lazy.TabGroupState.abbreviated(tabGroup) 4826 ); 4827 closedWinData.groups = Cu.cloneInto(abbreviatedGroups, {}); 4828 }, 4829 4830 maybeDontRestoreTabs(aWindow) { 4831 // Don't restore the tabs if we restore the session at startup 4832 this._windows[aWindow.__SSi]._maybeDontRestoreTabs = true; 4833 }, 4834 4835 isLastRestorableWindow() { 4836 return ( 4837 Object.values(this._windows).filter(winData => !winData.isPrivate) 4838 .length == 1 && 4839 !this._closedWindows.some(win => win._shouldRestore || false) 4840 ); 4841 }, 4842 4843 undoCloseWindow: function ssi_undoCloseWindow(aIndex) { 4844 if (!(aIndex in this._closedWindows)) { 4845 throw Components.Exception( 4846 "Invalid index: not in the closed windows", 4847 Cr.NS_ERROR_INVALID_ARG 4848 ); 4849 } 4850 // reopen the window 4851 let state = { windows: this._removeClosedWindow(aIndex) }; 4852 delete state.windows[0].closedAt; // Window is now open. 4853 4854 // If any saved tab groups are in the closed window, convert the saved tab 4855 // groups into open tab groups in the closed window and then forget the saved 4856 // tab groups. This should have the effect of "moving" the saved tab groups 4857 // into the window that's about to be restored. 4858 this._trimSavedTabGroupMetadataInClosedWindow(state.windows[0]); 4859 for (let tabGroup of state.windows[0].groups ?? []) { 4860 if (this.getSavedTabGroup(tabGroup.id)) { 4861 this.forgetSavedTabGroup(tabGroup.id); 4862 } 4863 } 4864 4865 let window = this._openWindowWithState(state); 4866 this.windowToFocus = window; 4867 WINDOW_SHOWING_PROMISES.get(window).promise.then(win => 4868 this.restoreWindows(win, state, { overwriteTabs: true }) 4869 ); 4870 4871 // Notify of changes to closed objects. 4872 this._notifyOfClosedObjectsChange(); 4873 4874 return window; 4875 }, 4876 4877 forgetClosedWindow: function ssi_forgetClosedWindow(aIndex) { 4878 // default to the most-recently closed window 4879 aIndex = aIndex || 0; 4880 if (!(aIndex in this._closedWindows)) { 4881 throw Components.Exception( 4882 "Invalid index: not in the closed windows", 4883 Cr.NS_ERROR_INVALID_ARG 4884 ); 4885 } 4886 4887 // remove closed window from the array 4888 let winData = this._closedWindows[aIndex]; 4889 this._removeClosedWindow(aIndex); 4890 this._saveableClosedWindowData.delete(winData); 4891 4892 // Notify of changes to closed objects. 4893 this._notifyOfClosedObjectsChange(); 4894 }, 4895 4896 getCustomWindowValue(aWindow, aKey) { 4897 if ("__SSi" in aWindow) { 4898 let data = this._windows[aWindow.__SSi].extData || {}; 4899 return data[aKey] || ""; 4900 } 4901 4902 if (DyingWindowCache.has(aWindow)) { 4903 let data = DyingWindowCache.get(aWindow).extData || {}; 4904 return data[aKey] || ""; 4905 } 4906 4907 throw Components.Exception( 4908 "Window is not tracked", 4909 Cr.NS_ERROR_INVALID_ARG 4910 ); 4911 }, 4912 4913 setCustomWindowValue(aWindow, aKey, aStringValue) { 4914 if (typeof aStringValue != "string") { 4915 throw new TypeError("setCustomWindowValue only accepts string values"); 4916 } 4917 4918 if (!("__SSi" in aWindow)) { 4919 throw Components.Exception( 4920 "Window is not tracked", 4921 Cr.NS_ERROR_INVALID_ARG 4922 ); 4923 } 4924 if (!this._windows[aWindow.__SSi].extData) { 4925 this._windows[aWindow.__SSi].extData = {}; 4926 } 4927 this._windows[aWindow.__SSi].extData[aKey] = aStringValue; 4928 this.saveStateDelayed(aWindow); 4929 }, 4930 4931 deleteCustomWindowValue(aWindow, aKey) { 4932 if ( 4933 aWindow.__SSi && 4934 this._windows[aWindow.__SSi].extData && 4935 this._windows[aWindow.__SSi].extData[aKey] 4936 ) { 4937 delete this._windows[aWindow.__SSi].extData[aKey]; 4938 } 4939 this.saveStateDelayed(aWindow); 4940 }, 4941 4942 getCustomTabValue(aTab, aKey) { 4943 return (TAB_CUSTOM_VALUES.get(aTab) || {})[aKey] || ""; 4944 }, 4945 4946 setCustomTabValue(aTab, aKey, aStringValue) { 4947 if (typeof aStringValue != "string") { 4948 throw new TypeError("setCustomTabValue only accepts string values"); 4949 } 4950 4951 // If the tab hasn't been restored, then set the data there, otherwise we 4952 // could lose newly added data. 4953 if (!TAB_CUSTOM_VALUES.has(aTab)) { 4954 TAB_CUSTOM_VALUES.set(aTab, {}); 4955 } 4956 4957 TAB_CUSTOM_VALUES.get(aTab)[aKey] = aStringValue; 4958 this.saveStateDelayed(aTab.ownerGlobal); 4959 }, 4960 4961 deleteCustomTabValue(aTab, aKey) { 4962 let state = TAB_CUSTOM_VALUES.get(aTab); 4963 if (state && aKey in state) { 4964 delete state[aKey]; 4965 this.saveStateDelayed(aTab.ownerGlobal); 4966 } 4967 }, 4968 4969 moveCustomTabValue(aFromTab, aToTab) { 4970 let state = TAB_CUSTOM_VALUES.get(aFromTab); 4971 if (state) { 4972 TAB_CUSTOM_VALUES.set(aToTab, state); 4973 TAB_CUSTOM_VALUES.delete(aFromTab); 4974 // No saveStateDelayed calls for either window here, because the callers 4975 // of moveCustomTabValue already call saveStateDelayed for both windows 4976 // as needed, from onTabAdd and onTabRemove. 4977 } 4978 }, 4979 4980 /** 4981 * Retrieves data specific to lazy-browser tabs. If tab is not lazy, 4982 * will return undefined. 4983 * 4984 * @param aTab (xul:tab) 4985 * The tabbrowser-tab the data is for. 4986 * @param aKey (string) 4987 * The key which maps to the desired data. 4988 */ 4989 getLazyTabValue(aTab, aKey) { 4990 return (TAB_LAZY_STATES.get(aTab) || {})[aKey]; 4991 }, 4992 4993 getCustomGlobalValue(aKey) { 4994 return this._globalState.get(aKey); 4995 }, 4996 4997 setCustomGlobalValue(aKey, aStringValue) { 4998 if (typeof aStringValue != "string") { 4999 throw new TypeError("setCustomGlobalValue only accepts string values"); 5000 } 5001 5002 this._globalState.set(aKey, aStringValue); 5003 this.saveStateDelayed(); 5004 }, 5005 5006 deleteCustomGlobalValue(aKey) { 5007 this._globalState.delete(aKey); 5008 this.saveStateDelayed(); 5009 }, 5010 5011 /** 5012 * Undoes the closing of a tab or window which corresponds 5013 * to the closedId passed in. 5014 * 5015 * @param {integer} aClosedId 5016 * The closedId of the tab or window 5017 * @param {boolean} [aIncludePrivate = true] 5018 * Whether to restore private tabs or windows. Defaults to true 5019 * @param {Window} [aTargetWindow] 5020 * When aClosedId is for a closed tab, which window to re-open the tab into. 5021 * Defaults to current (topWindow). 5022 * 5023 * @returns a tab or window object 5024 */ 5025 undoCloseById(aClosedId, aIncludePrivate = true, aTargetWindow) { 5026 // Check if we are re-opening a window first. 5027 for (let i = 0, l = this._closedWindows.length; i < l; i++) { 5028 if (this._closedWindows[i].closedId == aClosedId) { 5029 return this.undoCloseWindow(i); 5030 } 5031 } 5032 5033 // See if the aCloseId matches a tab in an open window 5034 // Check for a tab. 5035 for (let sourceWindow of Services.wm.getEnumerator("navigator:browser")) { 5036 if ( 5037 !aIncludePrivate && 5038 PrivateBrowsingUtils.isWindowPrivate(sourceWindow) 5039 ) { 5040 continue; 5041 } 5042 let windowState = this._windows[sourceWindow.__SSi]; 5043 if (windowState) { 5044 let closedTabs = 5045 this._getStateForClosedTabsAndClosedGroupTabs(windowState); 5046 for (let j = 0, l = closedTabs.length; j < l; j++) { 5047 if (closedTabs[j].closedId == aClosedId) { 5048 return this.undoCloseTab(sourceWindow, j, aTargetWindow); 5049 } 5050 } 5051 } 5052 } 5053 5054 // Neither a tab nor a window was found, return undefined and let the caller decide what to do about it. 5055 return undefined; 5056 }, 5057 5058 /** 5059 * Updates the label and icon for a <xul:tab> using the data from 5060 * tabData. 5061 * 5062 * @param tab 5063 * The <xul:tab> to update. 5064 * @param tabData (optional) 5065 * The tabData to use to update the tab. If the argument is 5066 * not supplied, the data will be retrieved from the cache. 5067 */ 5068 updateTabLabelAndIcon(tab, tabData = null) { 5069 if (tab.hasAttribute("customizemode")) { 5070 return; 5071 } 5072 5073 let browser = tab.linkedBrowser; 5074 let win = browser.ownerGlobal; 5075 5076 if (!tabData) { 5077 tabData = lazy.TabState.collect(tab, TAB_CUSTOM_VALUES.get(tab)); 5078 if (!tabData) { 5079 throw new Error("tabData not found for given tab"); 5080 } 5081 } 5082 5083 let activePageData = tabData.entries[tabData.index - 1] || null; 5084 5085 // If the page has a title, set it. 5086 if (activePageData) { 5087 if (activePageData.title && activePageData.title != activePageData.url) { 5088 win.gBrowser.setInitialTabTitle(tab, activePageData.title, { 5089 isContentTitle: true, 5090 }); 5091 } else { 5092 win.gBrowser.setInitialTabTitle(tab, activePageData.url); 5093 } 5094 } 5095 5096 // Restore the tab icon. 5097 if ("image" in tabData) { 5098 // We know that about:blank is safe to load in any remote type. Since 5099 // SessionStore is triggered with about:blank, there must be a process 5100 // flip. We will ignore the first about:blank load to prevent resetting the 5101 // favicon that we have set earlier to avoid flickering and improve 5102 // perceived performance. 5103 if ( 5104 !activePageData || 5105 (activePageData && activePageData.url != "about:blank") 5106 ) { 5107 win.gBrowser.setIcon(tab, tabData.image); 5108 } 5109 lazy.TabStateCache.update(browser.permanentKey, { 5110 image: null, 5111 }); 5112 } 5113 }, 5114 5115 // This method deletes all the closedTabs matching userContextId. 5116 _forgetTabsWithUserContextId(userContextId) { 5117 for (let window of Services.wm.getEnumerator("navigator:browser")) { 5118 let windowState = this._windows[window.__SSi]; 5119 if (windowState) { 5120 // In order to remove the tabs in the correct order, we store the 5121 // indexes, into an array, then we revert the array and remove closed 5122 // data from the last one going backward. 5123 let indexes = []; 5124 windowState._closedTabs.forEach((closedTab, index) => { 5125 if (closedTab.state.userContextId == userContextId) { 5126 indexes.push(index); 5127 } 5128 }); 5129 5130 for (let index of indexes.reverse()) { 5131 this.removeClosedTabData(windowState, windowState._closedTabs, index); 5132 } 5133 } 5134 } 5135 5136 // Notify of changes to closed objects. 5137 this._notifyOfClosedObjectsChange(); 5138 }, 5139 5140 /** 5141 * Restores the session state stored in LastSession. This will attempt 5142 * to merge data into the current session. If a window was opened at startup 5143 * with pinned tab(s), then the remaining data from the previous session for 5144 * that window will be opened into that window. Otherwise new windows will 5145 * be opened. 5146 */ 5147 restoreLastSession: function ssi_restoreLastSession() { 5148 // Use the public getter since it also checks PB mode 5149 if (!this.canRestoreLastSession) { 5150 throw Components.Exception("Last session can not be restored"); 5151 } 5152 5153 Services.obs.notifyObservers(null, NOTIFY_INITIATING_MANUAL_RESTORE); 5154 5155 // First collect each window with its id... 5156 let windows = {}; 5157 for (let window of this._browserWindows) { 5158 if (window.__SS_lastSessionWindowID) { 5159 windows[window.__SS_lastSessionWindowID] = window; 5160 } 5161 } 5162 5163 let lastSessionState = LastSession.getState(); 5164 5165 // This shouldn't ever be the case... 5166 if (!lastSessionState.windows.length) { 5167 throw Components.Exception( 5168 "lastSessionState has no windows", 5169 Cr.NS_ERROR_UNEXPECTED 5170 ); 5171 } 5172 5173 // We're technically doing a restore, so set things up so we send the 5174 // notification when we're done. We want to send "sessionstore-browser-state-restored". 5175 this._restoreCount = lastSessionState.windows.length; 5176 this._browserSetState = true; 5177 5178 // We want to re-use the last opened window instead of opening a new one in 5179 // the case where it's "empty" and not associated with a window in the session. 5180 // We will do more processing via _prepWindowToRestoreInto if we need to use 5181 // the lastWindow. 5182 let lastWindow = this._getTopWindow(); 5183 let canUseLastWindow = lastWindow && !lastWindow.__SS_lastSessionWindowID; 5184 5185 // global data must be restored before restoreWindow is called so that 5186 // it happens before observers are notified 5187 this._globalState.setFromState(lastSessionState); 5188 5189 let openWindows = []; 5190 let windowsToOpen = []; 5191 5192 // Restore session cookies. 5193 lazy.SessionCookies.restore(lastSessionState.cookies || []); 5194 5195 // Restore into windows or open new ones as needed. 5196 for (let i = 0; i < lastSessionState.windows.length; i++) { 5197 let winState = lastSessionState.windows[i]; 5198 5199 // If we're restoring multiple times without 5200 // Firefox restarting, we need to remove 5201 // the window being restored from "previously closed windows" 5202 if (this._restoreWithoutRestart) { 5203 let restoreIndex = this._closedWindows.findIndex(win => { 5204 return win.closedId == winState.closedId; 5205 }); 5206 if (restoreIndex > -1) { 5207 this._closedWindows.splice(restoreIndex, 1); 5208 } 5209 } 5210 5211 let lastSessionWindowID = winState.__lastSessionWindowID; 5212 // delete lastSessionWindowID so we don't add that to the window again 5213 delete winState.__lastSessionWindowID; 5214 5215 // See if we can use an open window. First try one that is associated with 5216 // the state we're trying to restore and then fallback to the last selected 5217 // window. 5218 let windowToUse = windows[lastSessionWindowID]; 5219 if (!windowToUse && canUseLastWindow) { 5220 windowToUse = lastWindow; 5221 canUseLastWindow = false; 5222 } 5223 5224 let [canUseWindow, canOverwriteTabs] = 5225 this._prepWindowToRestoreInto(windowToUse); 5226 5227 // If there's a window already open that we can restore into, use that 5228 if (canUseWindow) { 5229 if (!PERSIST_SESSIONS) { 5230 // Since we're not overwriting existing tabs, we want to merge _closedTabs, 5231 // putting existing ones first. Then make sure we're respecting the max pref. 5232 if (winState._closedTabs && winState._closedTabs.length) { 5233 let curWinState = this._windows[windowToUse.__SSi]; 5234 curWinState._closedTabs = curWinState._closedTabs.concat( 5235 winState._closedTabs 5236 ); 5237 curWinState._closedTabs.splice( 5238 this._max_tabs_undo, 5239 curWinState._closedTabs.length 5240 ); 5241 } 5242 } 5243 // We don't restore window right away, just store its data. 5244 // Later, these windows will be restored with newly opened windows. 5245 this._updateWindowRestoreState(windowToUse, { 5246 windows: [winState], 5247 options: { overwriteTabs: canOverwriteTabs }, 5248 }); 5249 openWindows.push(windowToUse); 5250 } else { 5251 windowsToOpen.push(winState); 5252 } 5253 } 5254 5255 // Actually restore windows in reversed z-order. 5256 this._openWindows({ windows: windowsToOpen }).then(openedWindows => 5257 this._restoreWindowsInReversedZOrder(openWindows.concat(openedWindows)) 5258 ); 5259 5260 if (this._restoreWithoutRestart) { 5261 this.removeDuplicateClosedWindows(lastSessionState); 5262 } 5263 5264 // Merge closed windows from this session with ones from last session 5265 if (lastSessionState._closedWindows) { 5266 // reset window closedIds and any references to them from closed tabs 5267 for (let closedWindow of lastSessionState._closedWindows) { 5268 closedWindow.closedId = this._nextClosedId++; 5269 if (closedWindow._closedTabs?.length) { 5270 this._resetClosedTabIds( 5271 closedWindow._closedTabs, 5272 closedWindow.closedId 5273 ); 5274 } 5275 } 5276 this._closedWindows = this._closedWindows.concat( 5277 lastSessionState._closedWindows 5278 ); 5279 this._capClosedWindows(); 5280 this._closedObjectsChanged = true; 5281 } 5282 5283 lazy.DevToolsShim.restoreDevToolsSession(lastSessionState); 5284 5285 // When the deferred session was created, open tab groups were converted to saved groups. 5286 // Now that they have been restored, they need to be removed from the saved groups list. 5287 let groupsToRemove = this._savedGroups.filter( 5288 group => group.removeAfterRestore 5289 ); 5290 for (let group of groupsToRemove) { 5291 this.forgetSavedTabGroup(group.id); 5292 } 5293 5294 // Set data that persists between sessions 5295 this._recentCrashes = 5296 (lastSessionState.session && lastSessionState.session.recentCrashes) || 0; 5297 5298 // Update the session start time using the restored session state. 5299 this._updateSessionStartTime(lastSessionState); 5300 5301 LastSession.clear(); 5302 5303 // Notify of changes to closed objects. 5304 this._notifyOfClosedObjectsChange(); 5305 }, 5306 5307 /** 5308 * There might be duplicates in these two arrays if we 5309 * restore multiple times without restarting in between. 5310 * We will keep the contents of the more recent _closedWindows array 5311 * 5312 * @param lastSessionState 5313 * An object containing information about the previous browsing session 5314 */ 5315 removeDuplicateClosedWindows(lastSessionState) { 5316 // A set of closedIDs for the most recent list of closed windows 5317 let currentClosedIds = new Set( 5318 this._closedWindows.map(window => window.closedId) 5319 ); 5320 5321 // Remove closed windows that are present in both current and last session 5322 lastSessionState._closedWindows = lastSessionState._closedWindows.filter( 5323 win => !currentClosedIds.has(win.closedId) 5324 ); 5325 }, 5326 5327 /** 5328 * Revive a crashed tab and restore its state from before it crashed. 5329 * 5330 * @param aTab 5331 * A <xul:tab> linked to a crashed browser. This is a no-op if the 5332 * browser hasn't actually crashed, or is not associated with a tab. 5333 * This function will also throw if the browser happens to be remote. 5334 */ 5335 reviveCrashedTab(aTab) { 5336 if (!aTab) { 5337 throw new Error( 5338 "SessionStore.reviveCrashedTab expected a tab, but got null." 5339 ); 5340 } 5341 5342 let browser = aTab.linkedBrowser; 5343 if (!this._crashedBrowsers.has(browser.permanentKey)) { 5344 return; 5345 } 5346 5347 // Sanity check - the browser to be revived should not be remote 5348 // at this point. 5349 if (browser.isRemoteBrowser) { 5350 throw new Error( 5351 "SessionStore.reviveCrashedTab: " + 5352 "Somehow a crashed browser is still remote." 5353 ); 5354 } 5355 5356 // We put the browser at about:blank in case the user is 5357 // restoring tabs on demand. This way, the user won't see 5358 // a flash of the about:tabcrashed page after selecting 5359 // the revived tab. 5360 aTab.removeAttribute("crashed"); 5361 5362 browser.loadURI(lazy.blankURI, { 5363 triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({ 5364 userContextId: aTab.userContextId, 5365 }), 5366 remoteTypeOverride: lazy.E10SUtils.NOT_REMOTE, 5367 }); 5368 5369 let data = lazy.TabState.collect(aTab, TAB_CUSTOM_VALUES.get(aTab)); 5370 this.restoreTab(aTab, data, { 5371 forceOnDemand: true, 5372 }); 5373 }, 5374 5375 /** 5376 * Revive all crashed tabs and reset the crashed tabs count to 0. 5377 */ 5378 reviveAllCrashedTabs() { 5379 for (let window of Services.wm.getEnumerator("navigator:browser")) { 5380 for (let tab of window.gBrowser.tabs) { 5381 this.reviveCrashedTab(tab); 5382 } 5383 } 5384 }, 5385 5386 /** 5387 * Retrieves the latest session history information for a tab. The cached data 5388 * is returned immediately, but a callback may be provided that supplies 5389 * up-to-date data when or if it is available. The callback is passed a single 5390 * argument with data in the same format as the return value. 5391 * 5392 * @param tab tab to retrieve the session history for 5393 * @param updatedCallback function to call with updated data as the single argument 5394 * @returns a object containing 'index' specifying the current index, and an 5395 * array 'entries' containing an object for each history item. 5396 */ 5397 getSessionHistory(tab, updatedCallback) { 5398 if (updatedCallback) { 5399 lazy.TabStateFlusher.flush(tab.linkedBrowser).then(() => { 5400 let sessionHistory = this.getSessionHistory(tab); 5401 if (sessionHistory) { 5402 updatedCallback(sessionHistory); 5403 } 5404 }); 5405 } 5406 5407 // Don't continue if the tab was closed before TabStateFlusher.flush resolves. 5408 if (tab.linkedBrowser) { 5409 let tabState = lazy.TabState.collect(tab, TAB_CUSTOM_VALUES.get(tab)); 5410 return { index: tabState.index - 1, entries: tabState.entries }; 5411 } 5412 return null; 5413 }, 5414 5415 /** 5416 * See if aWindow is usable for use when restoring a previous session via 5417 * restoreLastSession. If usable, prepare it for use. 5418 * 5419 * @param aWindow 5420 * the window to inspect & prepare 5421 * @returns [canUseWindow, canOverwriteTabs] 5422 * canUseWindow: can the window be used to restore into 5423 * canOverwriteTabs: all of the current tabs are home pages and we 5424 * can overwrite them 5425 */ 5426 _prepWindowToRestoreInto: function ssi_prepWindowToRestoreInto(aWindow) { 5427 if (!aWindow) { 5428 return [false, false]; 5429 } 5430 5431 // We might be able to overwrite the existing tabs instead of just adding 5432 // the previous session's tabs to the end. This will be set if possible. 5433 let canOverwriteTabs = false; 5434 5435 // Look at the open tabs in comparison to home pages. If all the tabs are 5436 // home pages then we'll end up overwriting all of them. Otherwise we'll 5437 // just close the tabs that match home pages. Tabs with the about:blank 5438 // URI will always be overwritten. 5439 let homePages = ["about:blank"]; 5440 let removableTabs = []; 5441 let tabbrowser = aWindow.gBrowser; 5442 let startupPref = this._prefBranch.getIntPref("startup.page"); 5443 if (startupPref == 1) { 5444 homePages = homePages.concat(lazy.HomePage.get(aWindow).split("|")); 5445 } 5446 5447 for (let i = tabbrowser.pinnedTabCount; i < tabbrowser.tabs.length; i++) { 5448 let tab = tabbrowser.tabs[i]; 5449 if (homePages.includes(tab.linkedBrowser.currentURI.spec)) { 5450 removableTabs.push(tab); 5451 } 5452 } 5453 5454 if ( 5455 tabbrowser.tabs.length > tabbrowser.visibleTabs.length && 5456 tabbrowser.visibleTabs.length === removableTabs.length 5457 ) { 5458 // If all the visible tabs are also removable and the selected tab is hidden or removeable, we will later remove 5459 // all "removable" tabs causing the browser to automatically close because the only tab left is hidden. 5460 // To prevent the browser from automatically closing, we will leave one other visible tab open. 5461 removableTabs.shift(); 5462 } 5463 5464 if (tabbrowser.tabs.length == removableTabs.length) { 5465 canOverwriteTabs = true; 5466 } else { 5467 // If we're not overwriting all of the tabs, then close the home tabs. 5468 for (let i = removableTabs.length - 1; i >= 0; i--) { 5469 tabbrowser.removeTab(removableTabs.pop(), { animate: false }); 5470 } 5471 } 5472 5473 return [true, canOverwriteTabs]; 5474 }, 5475 5476 /* ........ Saving Functionality .............. */ 5477 5478 /** 5479 * Store window dimensions, visibility, sidebar 5480 * 5481 * @param aWindow 5482 * Window reference 5483 */ 5484 _updateWindowFeatures: function ssi_updateWindowFeatures(aWindow) { 5485 var winData = this._windows[aWindow.__SSi]; 5486 5487 WINDOW_ATTRIBUTES.forEach(function (aAttr) { 5488 winData[aAttr] = this._getWindowDimension(aWindow, aAttr); 5489 }, this); 5490 5491 if (winData.sizemode != "minimized") { 5492 winData.sizemodeBeforeMinimized = winData.sizemode; 5493 } 5494 5495 var hidden = WINDOW_HIDEABLE_FEATURES.filter(function (aItem) { 5496 return aWindow[aItem] && !aWindow[aItem].visible; 5497 }); 5498 if (hidden.length) { 5499 winData.hidden = hidden.join(","); 5500 } else if (winData.hidden) { 5501 delete winData.hidden; 5502 } 5503 5504 const sidebarUIState = aWindow.SidebarController.getUIState(); 5505 if (sidebarUIState) { 5506 winData.sidebar = structuredClone(sidebarUIState); 5507 } 5508 5509 let workspaceID = aWindow.getWorkspaceID(); 5510 if (workspaceID) { 5511 winData.workspaceID = workspaceID; 5512 } 5513 5514 winData.isAIWindow = lazy.AIWindow.isAIWindowActiveAndEnabled(aWindow); 5515 }, 5516 5517 /** 5518 * gather session data as object 5519 * 5520 * @param aUpdateAll 5521 * Bool update all windows 5522 * @returns object 5523 */ 5524 getCurrentState(aUpdateAll) { 5525 this._handleClosedWindows().then(() => { 5526 this._notifyOfClosedObjectsChange(); 5527 }); 5528 5529 var activeWindow = this._getTopWindow(); 5530 5531 let timerId = Glean.sessionRestore.collectAllWindowsData.start(); 5532 if (lazy.RunState.isRunning) { 5533 // update the data for all windows with activities since the last save operation. 5534 let index = 0; 5535 for (let window of this._orderedBrowserWindows) { 5536 if (!this._isWindowLoaded(window)) { 5537 // window data is still in _statesToRestore 5538 continue; 5539 } 5540 if (aUpdateAll || DirtyWindows.has(window) || window == activeWindow) { 5541 this._collectWindowData(window); 5542 } else { 5543 // always update the window features (whose change alone never triggers a save operation) 5544 this._updateWindowFeatures(window); 5545 } 5546 this._windows[window.__SSi].zIndex = ++index; 5547 } 5548 DirtyWindows.clear(); 5549 } 5550 Glean.sessionRestore.collectAllWindowsData.stopAndAccumulate(timerId); 5551 5552 // An array that at the end will hold all current window data. 5553 var total = []; 5554 // The ids of all windows contained in 'total' in the same order. 5555 var ids = []; 5556 // The number of window that are _not_ popups. 5557 var nonPopupCount = 0; 5558 var ix; 5559 5560 // collect the data for all windows 5561 for (ix in this._windows) { 5562 if (this._windows[ix]._restoring || this._windows[ix].isTaskbarTab) { 5563 // window data is still in _statesToRestore 5564 continue; 5565 } 5566 total.push(this._windows[ix]); 5567 ids.push(ix); 5568 if (!this._windows[ix].isPopup) { 5569 nonPopupCount++; 5570 } 5571 } 5572 5573 // collect the data for all windows yet to be restored 5574 for (ix in this._statesToRestore) { 5575 for (let winData of this._statesToRestore[ix].windows) { 5576 total.push(winData); 5577 if (!winData.isPopup) { 5578 nonPopupCount++; 5579 } 5580 } 5581 } 5582 5583 // shallow copy this._closedWindows to preserve current state 5584 let lastClosedWindowsCopy = this._closedWindows.slice(); 5585 5586 if (AppConstants.platform != "macosx") { 5587 // If no non-popup browser window remains open, return the state of the last 5588 // closed window(s). We only want to do this when we're actually "ending" 5589 // the session. 5590 // XXXzpao We should do this for _restoreLastWindow == true, but that has 5591 // its own check for popups. c.f. bug 597619 5592 if ( 5593 nonPopupCount == 0 && 5594 !!lastClosedWindowsCopy.length && 5595 lazy.RunState.isQuitting 5596 ) { 5597 // prepend the last non-popup browser window, so that if the user loads more tabs 5598 // at startup we don't accidentally add them to a popup window 5599 do { 5600 total.unshift(lastClosedWindowsCopy.shift()); 5601 } while (total[0].isPopup && lastClosedWindowsCopy.length); 5602 } 5603 } 5604 5605 if (activeWindow) { 5606 this.activeWindowSSiCache = activeWindow.__SSi || ""; 5607 } 5608 ix = ids.indexOf(this.activeWindowSSiCache); 5609 // We don't want to restore focus to a minimized window or a window which had all its 5610 // tabs stripped out (doesn't exist). 5611 if (ix != -1 && total[ix] && total[ix].sizemode == "minimized") { 5612 ix = -1; 5613 } 5614 5615 let session = { 5616 lastUpdate: Date.now(), 5617 startTime: this._sessionStartTime, 5618 recentCrashes: this._recentCrashes, 5619 }; 5620 5621 let state = { 5622 version: ["sessionrestore", FORMAT_VERSION], 5623 windows: total, 5624 selectedWindow: ix + 1, 5625 _closedWindows: lastClosedWindowsCopy, 5626 savedGroups: this._savedGroups, 5627 session, 5628 global: this._globalState.getState(), 5629 }; 5630 5631 // Collect and store session cookies. 5632 state.cookies = lazy.SessionCookies.collect(); 5633 5634 lazy.DevToolsShim.saveDevToolsSession(state); 5635 5636 // Persist the last session if we deferred restoring it 5637 if (LastSession.canRestore) { 5638 state.lastSessionState = LastSession.getState(); 5639 } 5640 5641 // If we were called by the SessionSaver and started with only a private 5642 // window we want to pass the deferred initial state to not lose the 5643 // previous session. 5644 if (this._deferredInitialState) { 5645 state.deferredInitialState = this._deferredInitialState; 5646 } 5647 5648 return state; 5649 }, 5650 5651 /** 5652 * serialize session data for a window 5653 * 5654 * @param {Window} aWindow 5655 * Window reference 5656 * @returns {{windows: [WindowStateData]}} 5657 */ 5658 _getWindowState: function ssi_getWindowState(aWindow) { 5659 if (!this._isWindowLoaded(aWindow)) { 5660 return this._statesToRestore[WINDOW_RESTORE_IDS.get(aWindow)]; 5661 } 5662 5663 if (lazy.RunState.isRunning) { 5664 this._collectWindowData(aWindow); 5665 } 5666 5667 return { windows: [this._windows[aWindow.__SSi]] }; 5668 }, 5669 5670 /** 5671 * Retrieves window data for an active session. 5672 * 5673 * @param {Window} aWindow 5674 * @returns {WindowStateData} 5675 * @throws {Error} if `aWindow` is not being managed in the session store. 5676 */ 5677 getWindowStateData: function ssi_getWindowStateData(aWindow) { 5678 if (!aWindow.__SSi || !(aWindow.__SSi in this._windows)) { 5679 throw Components.Exception( 5680 "Window is not tracked", 5681 Cr.NS_ERROR_INVALID_ARG 5682 ); 5683 } 5684 5685 return this._windows[aWindow.__SSi]; 5686 }, 5687 5688 /** 5689 * Gathers data about a window and its tabs, and updates its 5690 * entry in this._windows. 5691 * 5692 * @param aWindow 5693 * Window references. 5694 * @returns a Map mapping the browser tabs from aWindow to the tab 5695 * entry that was put into the window data in this._windows. 5696 */ 5697 _collectWindowData: function ssi_collectWindowData(aWindow) { 5698 let tabMap = new Map(); 5699 5700 if (!this._isWindowLoaded(aWindow)) { 5701 return tabMap; 5702 } 5703 5704 let tabbrowser = aWindow.gBrowser; 5705 let tabs = tabbrowser.tabs; 5706 /** @type {WindowStateData} */ 5707 let winData = this._windows[aWindow.__SSi]; 5708 let tabsData = (winData.tabs = []); 5709 5710 // update the internal state data for this window 5711 for (let tab of tabs) { 5712 if (tab == aWindow.FirefoxViewHandler.tab) { 5713 continue; 5714 } 5715 let tabData = lazy.TabState.collect(tab, TAB_CUSTOM_VALUES.get(tab)); 5716 tabMap.set(tab, tabData); 5717 tabsData.push(tabData); 5718 } 5719 5720 // update tab group state for this window 5721 winData.groups = []; 5722 for (let tabGroup of aWindow.gBrowser.tabGroups) { 5723 let tabGroupData = lazy.TabGroupState.collect(tabGroup); 5724 winData.groups.push(tabGroupData); 5725 } 5726 5727 let selectedIndex = tabbrowser.tabbox.selectedIndex + 1; 5728 // We don't store the Firefox View tab in Session Store, so if it was the last selected "tab" when 5729 // a window is closed, point to the first item in the tab strip instead (it will never be the Firefox View tab, 5730 // since it's only inserted into the tab strip after it's selected). 5731 if (aWindow.FirefoxViewHandler.tab?.selected) { 5732 selectedIndex = 1; 5733 winData.title = tabbrowser.tabs[0].label; 5734 } 5735 winData.selected = selectedIndex; 5736 5737 this._updateWindowFeatures(aWindow); 5738 5739 // Make sure we keep __SS_lastSessionWindowID around for cases like entering 5740 // or leaving PB mode. 5741 if (aWindow.__SS_lastSessionWindowID) { 5742 this._windows[aWindow.__SSi].__lastSessionWindowID = 5743 aWindow.__SS_lastSessionWindowID; 5744 } 5745 5746 DirtyWindows.remove(aWindow); 5747 return tabMap; 5748 }, 5749 5750 /* ........ Restoring Functionality .............. */ 5751 5752 /** 5753 * Open windows with data 5754 * 5755 * @param root 5756 * Windows data 5757 * @returns a promise resolved when all windows have been opened 5758 */ 5759 _openWindows(root) { 5760 let windowsOpened = []; 5761 for (let winData of root.windows) { 5762 if (!winData || !winData.tabs || !winData.tabs[0]) { 5763 this._log.debug(`_openWindows, skipping window with no tabs data`); 5764 this._restoreCount--; 5765 continue; 5766 } 5767 windowsOpened.push(this._openWindowWithState({ windows: [winData] })); 5768 } 5769 let windowOpenedPromises = []; 5770 for (const openedWindow of windowsOpened) { 5771 let deferred = WINDOW_SHOWING_PROMISES.get(openedWindow); 5772 windowOpenedPromises.push(deferred.promise); 5773 } 5774 return Promise.all(windowOpenedPromises); 5775 }, 5776 5777 /** 5778 * Reset closedId's from previous sessions to ensure these IDs are unique 5779 * 5780 * @param tabData 5781 * an array of data to be restored 5782 * @param {string} windowId 5783 * The SessionStore id for the window these tabs should be associated with 5784 * @returns the updated tabData array 5785 */ 5786 _resetClosedTabIds(tabData, windowId) { 5787 for (let entry of tabData) { 5788 entry.closedId = this._nextClosedId++; 5789 entry.sourceWindowId = windowId; 5790 } 5791 return tabData; 5792 }, 5793 /** 5794 * restore features to a single window 5795 * 5796 * @param aWindow 5797 * Window reference to the window to use for restoration 5798 * @param winData 5799 * JS object 5800 * @param aOptions.overwriteTabs 5801 * to overwrite existing tabs w/ new ones 5802 * @param aOptions.firstWindow 5803 * if this is the first non-private window we're 5804 * restoring in this session, that might open an 5805 * external link as well 5806 */ 5807 restoreWindow: function ssi_restoreWindow(aWindow, winData, aOptions = {}) { 5808 let overwriteTabs = aOptions && aOptions.overwriteTabs; 5809 let firstWindow = aOptions && aOptions.firstWindow; 5810 5811 this.restoreSidebar(aWindow, winData.sidebar, winData.isPopup); 5812 5813 // initialize window if necessary 5814 if (aWindow && (!aWindow.__SSi || !this._windows[aWindow.__SSi])) { 5815 this.onLoad(aWindow); 5816 } 5817 5818 let timerId = Glean.sessionRestore.restoreWindow.start(); 5819 5820 // We're not returning from this before we end up calling restoreTabs 5821 // for this window, so make sure we send the SSWindowStateBusy event. 5822 this._sendWindowRestoringNotification(aWindow); 5823 this._setWindowStateBusy(aWindow); 5824 5825 if (winData.workspaceID && lazy.gRestoreWindowsToVirtualDesktop) { 5826 this._log.debug(`Moving window to workspace: ${winData.workspaceID}`); 5827 aWindow.moveToWorkspace(winData.workspaceID); 5828 } 5829 5830 if (!winData.tabs) { 5831 winData.tabs = []; 5832 // don't restore a single blank tab when we've had an external 5833 // URL passed in for loading at startup (cf. bug 357419) 5834 } else if ( 5835 firstWindow && 5836 !overwriteTabs && 5837 winData.tabs.length == 1 && 5838 (!winData.tabs[0].entries || !winData.tabs[0].entries.length) 5839 ) { 5840 winData.tabs = []; 5841 } 5842 5843 // See SessionStoreInternal.restoreTabs for a description of what 5844 // selectTab represents. 5845 let selectTab = 0; 5846 if (overwriteTabs) { 5847 selectTab = parseInt(winData.selected || 1, 10); 5848 selectTab = Math.max(selectTab, 1); 5849 selectTab = Math.min(selectTab, winData.tabs.length); 5850 } 5851 5852 let tabbrowser = aWindow.gBrowser; 5853 5854 // disable smooth scrolling while adding, moving, removing and selecting tabs 5855 let arrowScrollbox = tabbrowser.tabContainer.arrowScrollbox; 5856 let smoothScroll = arrowScrollbox.smoothScroll; 5857 arrowScrollbox.smoothScroll = false; 5858 5859 // We need to keep track of the initially open tabs so that they 5860 // can be moved to the end of the restored tabs. 5861 let initialTabs; 5862 if (!overwriteTabs && firstWindow) { 5863 initialTabs = Array.from(tabbrowser.tabs); 5864 } 5865 5866 // Get rid of tabs that aren't needed anymore. 5867 if (overwriteTabs) { 5868 for (let i = tabbrowser.browsers.length - 1; i >= 0; i--) { 5869 if (!tabbrowser.tabs[i].selected) { 5870 tabbrowser.removeTab(tabbrowser.tabs[i]); 5871 } 5872 } 5873 } 5874 5875 let restoreTabsLazily = 5876 this._prefBranch.getBoolPref("sessionstore.restore_tabs_lazily") && 5877 this._restore_on_demand; 5878 5879 this._log.debug( 5880 `restoreWindow, will restore ${winData.tabs.length} tabs and ${ 5881 winData.groups?.length ?? 0 5882 } tab groups, restoreTabsLazily: ${restoreTabsLazily}` 5883 ); 5884 if (winData.tabs.length) { 5885 var tabs = tabbrowser.createTabsForSessionRestore( 5886 restoreTabsLazily, 5887 selectTab, 5888 winData.tabs, 5889 winData.groups ?? [] 5890 ); 5891 this._log.debug( 5892 `restoreWindow, createTabsForSessionRestore returned ${tabs.length} tabs` 5893 ); 5894 // If restoring this window resulted in reopening any saved tab groups, 5895 // we no longer need to track those saved tab groups. 5896 const openTabGroupIdsInWindow = new Set( 5897 tabbrowser.tabGroups.map(group => group.id) 5898 ); 5899 this._savedGroups = this._savedGroups.filter( 5900 savedTabGroup => !openTabGroupIdsInWindow.has(savedTabGroup.id) 5901 ); 5902 } 5903 5904 // Move the originally open tabs to the end. 5905 if (initialTabs) { 5906 let endPosition = tabbrowser.tabs.length - 1; 5907 for (let i = 0; i < initialTabs.length; i++) { 5908 tabbrowser.unpinTab(initialTabs[i]); 5909 tabbrowser.moveTabTo(initialTabs[i], { 5910 tabIndex: endPosition, 5911 forceUngrouped: true, 5912 }); 5913 } 5914 } 5915 5916 // We want to correlate the window with data from the last session, so 5917 // assign another id if we have one. Otherwise clear so we don't do 5918 // anything with it. 5919 delete aWindow.__SS_lastSessionWindowID; 5920 if (winData.__lastSessionWindowID) { 5921 aWindow.__SS_lastSessionWindowID = winData.__lastSessionWindowID; 5922 } 5923 5924 if (overwriteTabs) { 5925 delete this._windows[aWindow.__SSi].extData; 5926 } 5927 5928 // Restore cookies from legacy sessions, i.e. before bug 912717. 5929 lazy.SessionCookies.restore(winData.cookies || []); 5930 5931 if (winData.extData) { 5932 if (!this._windows[aWindow.__SSi].extData) { 5933 this._windows[aWindow.__SSi].extData = {}; 5934 } 5935 for (var key in winData.extData) { 5936 this._windows[aWindow.__SSi].extData[key] = winData.extData[key]; 5937 } 5938 } 5939 5940 let newClosedTabsData; 5941 if (winData._closedTabs) { 5942 newClosedTabsData = winData._closedTabs; 5943 this._resetClosedTabIds(newClosedTabsData, aWindow.__SSi); 5944 } else { 5945 newClosedTabsData = []; 5946 } 5947 5948 let newLastClosedTabGroupCount = winData._lastClosedTabGroupCount || -1; 5949 5950 if (overwriteTabs || firstWindow) { 5951 // Overwrite existing closed tabs data when overwriteTabs=true 5952 // or we're the first window to be restored. 5953 this._windows[aWindow.__SSi]._closedTabs = newClosedTabsData; 5954 } else if (this._max_tabs_undo > 0) { 5955 // We preserve tabs between sessions so we just want to filter out any previously open tabs that 5956 // were added to the _closedTabs list prior to restoreLastSession 5957 if (PERSIST_SESSIONS) { 5958 newClosedTabsData = this._windows[aWindow.__SSi]._closedTabs.filter( 5959 tab => !tab.removeAfterRestore 5960 ); 5961 } else { 5962 newClosedTabsData = newClosedTabsData.concat( 5963 this._windows[aWindow.__SSi]._closedTabs 5964 ); 5965 } 5966 5967 // ... and make sure that we don't exceed the max number of closed tabs 5968 // we can restore. 5969 this._windows[aWindow.__SSi]._closedTabs = newClosedTabsData.slice( 5970 0, 5971 this._max_tabs_undo 5972 ); 5973 } 5974 // Because newClosedTabsData are put in first, we need to 5975 // copy also the _lastClosedTabGroupCount. 5976 this._windows[aWindow.__SSi]._lastClosedTabGroupCount = 5977 newLastClosedTabGroupCount; 5978 5979 // Copy over closed tab groups from the previous session, 5980 // and reset closed tab ids for tabs within each group. 5981 let newClosedTabGroupsData = winData.closedGroups || []; 5982 newClosedTabGroupsData.forEach(group => { 5983 this._resetClosedTabIds(group.tabs, aWindow.__SSi); 5984 }); 5985 this._windows[aWindow.__SSi].closedGroups = newClosedTabGroupsData; 5986 this._windows[aWindow.__SSi].lastClosedTabGroupId = 5987 winData.lastClosedTabGroupId || null; 5988 5989 if (!this._isWindowLoaded(aWindow)) { 5990 // from now on, the data will come from the actual window 5991 delete this._statesToRestore[WINDOW_RESTORE_IDS.get(aWindow)]; 5992 WINDOW_RESTORE_IDS.delete(aWindow); 5993 delete this._windows[aWindow.__SSi]._restoring; 5994 } 5995 5996 // Restore tabs, if any. 5997 if (winData.tabs.length) { 5998 this.restoreTabs(aWindow, tabs, winData.tabs, selectTab); 5999 } 6000 6001 // set smoothScroll back to the original value 6002 arrowScrollbox.smoothScroll = smoothScroll; 6003 6004 Glean.sessionRestore.restoreWindow.stopAndAccumulate(timerId); 6005 }, 6006 6007 /** 6008 * Prepare connection to host beforehand. 6009 * 6010 * @param tab 6011 * Tab we are loading from. 6012 * @param url 6013 * URL of a host. 6014 * @returns a flag indicates whether a connection has been made 6015 */ 6016 prepareConnectionToHost(tab, url) { 6017 if (url && !url.startsWith("about:")) { 6018 let principal = Services.scriptSecurityManager.createNullPrincipal({ 6019 userContextId: tab.userContextId, 6020 }); 6021 let sc = Services.io.QueryInterface(Ci.nsISpeculativeConnect); 6022 let uri = Services.io.newURI(url); 6023 try { 6024 sc.speculativeConnect(uri, principal, null, false); 6025 return true; 6026 } catch (error) { 6027 // Can't setup speculative connection for this url. 6028 return false; 6029 } 6030 } 6031 return false; 6032 }, 6033 6034 /** 6035 * Make a connection to a host when users hover mouse on a tab. 6036 * This will also set a flag in the tab to prevent us from speculatively 6037 * connecting a second time. 6038 * 6039 * @param tab 6040 * a tab to speculatively connect on mouse hover. 6041 */ 6042 speculativeConnectOnTabHover(tab) { 6043 let tabState = TAB_LAZY_STATES.get(tab); 6044 if (tabState && !tabState.connectionPrepared) { 6045 let url = this.getLazyTabValue(tab, "url"); 6046 let prepared = this.prepareConnectionToHost(tab, url); 6047 // This is used to test if a connection has been made beforehand. 6048 if (gDebuggingEnabled) { 6049 tab.__test_connection_prepared = prepared; 6050 tab.__test_connection_url = url; 6051 } 6052 // A flag indicate that we've prepared a connection for this tab and 6053 // if is called again, we shouldn't prepare another connection. 6054 tabState.connectionPrepared = true; 6055 } 6056 }, 6057 6058 /** 6059 * This function will restore window features and then restore window data. 6060 * 6061 * @param windows 6062 * ordered array of windows to restore 6063 */ 6064 _restoreWindowsFeaturesAndTabs(windows) { 6065 // First, we restore window features, so that when users start interacting 6066 // with a window, we don't steal the window focus. 6067 let resizePromises = []; 6068 for (let window of windows) { 6069 let state = this._statesToRestore[WINDOW_RESTORE_IDS.get(window)]; 6070 // Wait for these promises after we've restored data into them below. 6071 resizePromises.push(this.restoreWindowFeatures(window, state.windows[0])); 6072 } 6073 6074 // Then we restore data into windows. 6075 for (let window of windows) { 6076 let state = this._statesToRestore[WINDOW_RESTORE_IDS.get(window)]; 6077 this.restoreWindow( 6078 window, 6079 state.windows[0], 6080 state.options || { overwriteTabs: true } 6081 ); 6082 WINDOW_RESTORE_ZINDICES.delete(window); 6083 } 6084 for (let resizePromise of resizePromises) { 6085 resizePromise.then(resizedWindow => { 6086 this._setWindowStateReady(resizedWindow); 6087 6088 this._sendWindowRestoredNotification(resizedWindow); 6089 6090 Services.obs.notifyObservers( 6091 resizedWindow, 6092 NOTIFY_SINGLE_WINDOW_RESTORED 6093 ); 6094 6095 this._sendRestoreCompletedNotifications(); 6096 }); 6097 } 6098 }, 6099 6100 /** 6101 * This function will restore window in reversed z-index, so that users will 6102 * be presented with most recently used window first. 6103 * 6104 * @param windows 6105 * unordered array of windows to restore 6106 */ 6107 _restoreWindowsInReversedZOrder(windows) { 6108 windows.sort( 6109 (a, b) => 6110 (WINDOW_RESTORE_ZINDICES.get(a) || 0) - 6111 (WINDOW_RESTORE_ZINDICES.get(b) || 0) 6112 ); 6113 6114 this.windowToFocus = windows[0]; 6115 this._restoreWindowsFeaturesAndTabs(windows); 6116 }, 6117 6118 /** 6119 * Restore multiple windows using the provided state. 6120 * 6121 * @param aWindow 6122 * Window reference to the first window to use for restoration. 6123 * Additionally required windows will be opened. 6124 * @param aState 6125 * JS object or JSON string 6126 * @param aOptions.overwriteTabs 6127 * to overwrite existing tabs w/ new ones 6128 * @param aOptions.firstWindow 6129 * if this is the first non-private window we're 6130 * restoring in this session, that might open an 6131 * external link as well 6132 */ 6133 restoreWindows: function ssi_restoreWindows(aWindow, aState, aOptions = {}) { 6134 // initialize window if necessary 6135 if (aWindow && (!aWindow.__SSi || !this._windows[aWindow.__SSi])) { 6136 this.onLoad(aWindow); 6137 } 6138 6139 let root; 6140 try { 6141 root = typeof aState == "string" ? JSON.parse(aState) : aState; 6142 } catch (ex) { 6143 // invalid state object - don't restore anything 6144 this._log.debug(`restoreWindows failed to parse ${typeof aState} state`); 6145 this._log.error(ex); 6146 this._sendRestoreCompletedNotifications(); 6147 return; 6148 } 6149 6150 // Restore closed windows if any. 6151 if (root._closedWindows) { 6152 this._closedWindows = root._closedWindows; 6153 // reset window closedIds and any references to them from closed tabs 6154 for (let closedWindow of this._closedWindows) { 6155 closedWindow.closedId = this._nextClosedId++; 6156 if (closedWindow._closedTabs?.length) { 6157 this._resetClosedTabIds( 6158 closedWindow._closedTabs, 6159 closedWindow.closedId 6160 ); 6161 } 6162 } 6163 this._log.debug(`Restored ${this._closedWindows.length} closed windows`); 6164 this._closedObjectsChanged = true; 6165 } 6166 6167 this._log.debug( 6168 `restoreWindows will restore ${root.windows?.length} windows` 6169 ); 6170 // We're done here if there are no windows. 6171 if (!root.windows || !root.windows.length) { 6172 this._sendRestoreCompletedNotifications(); 6173 return; 6174 } 6175 6176 let firstWindowData = root.windows.splice(0, 1); 6177 // Store the restore state and restore option of the current window, 6178 // so that the window can be restored in reversed z-order. 6179 this._updateWindowRestoreState(aWindow, { 6180 windows: firstWindowData, 6181 options: aOptions, 6182 }); 6183 6184 // Begin the restoration: First open all windows in creation order. After all 6185 // windows have opened, we restore states to windows in reversed z-order. 6186 this._openWindows(root).then(windows => { 6187 // We want to add current window to opened window, so that this window will be 6188 // restored in reversed z-order. (We add the window to first position, in case 6189 // no z-indices are found, that window will be restored first.) 6190 windows.unshift(aWindow); 6191 6192 this._restoreWindowsInReversedZOrder(windows); 6193 }); 6194 6195 lazy.DevToolsShim.restoreDevToolsSession(aState); 6196 }, 6197 6198 /** 6199 * Manage history restoration for a window 6200 * 6201 * @param aWindow 6202 * Window to restore the tabs into 6203 * @param aTabs 6204 * Array of tab references 6205 * @param aTabData 6206 * Array of tab data 6207 * @param aSelectTab 6208 * Index of the tab to select. This is a 1-based index where "1" 6209 * indicates the first tab should be selected, and "0" indicates that 6210 * the currently selected tab will not be changed. 6211 */ 6212 restoreTabs(aWindow, aTabs, aTabData, aSelectTab) { 6213 var tabbrowser = aWindow.gBrowser; 6214 6215 let numTabsToRestore = aTabs.length; 6216 let numTabsInWindow = tabbrowser.tabs.length; 6217 let tabsDataArray = this._windows[aWindow.__SSi].tabs; 6218 6219 // Update the window state in case we shut down without being notified. 6220 // Individual tab states will be taken care of by restoreTab() below. 6221 if (numTabsInWindow == numTabsToRestore) { 6222 // Remove all previous tab data. 6223 tabsDataArray.length = 0; 6224 } else { 6225 // Remove all previous tab data except tabs that should not be overriden. 6226 tabsDataArray.splice(numTabsInWindow - numTabsToRestore); 6227 } 6228 6229 // Remove items from aTabData if there is no corresponding tab: 6230 if (numTabsInWindow < tabsDataArray.length) { 6231 tabsDataArray.length = numTabsInWindow; 6232 } 6233 6234 // Ensure the tab data array has items for each of the tabs 6235 this._ensureNoNullsInTabDataList( 6236 tabbrowser.tabs, 6237 tabsDataArray, 6238 numTabsInWindow - 1 6239 ); 6240 6241 if (aSelectTab > 0 && aSelectTab <= aTabs.length) { 6242 // Update the window state in case we shut down without being notified. 6243 this._windows[aWindow.__SSi].selected = aSelectTab; 6244 } 6245 6246 // If we restore the selected tab, make sure it goes first. 6247 let selectedIndex = aTabs.indexOf(tabbrowser.selectedTab); 6248 if (selectedIndex > -1) { 6249 this.restoreTab(tabbrowser.selectedTab, aTabData[selectedIndex]); 6250 } 6251 6252 // Restore all tabs. 6253 for (let t = 0; t < aTabs.length; t++) { 6254 if (t != selectedIndex) { 6255 this.restoreTab(aTabs[t], aTabData[t]); 6256 } 6257 } 6258 }, 6259 6260 // In case we didn't collect/receive data for any tabs yet we'll have to 6261 // fill the array with at least empty tabData objects until |_tPos| or 6262 // we'll end up with |null| entries. 6263 _ensureNoNullsInTabDataList(tabElements, tabDataList, changedTabPos) { 6264 let initialDataListLength = tabDataList.length; 6265 if (changedTabPos < initialDataListLength) { 6266 return; 6267 } 6268 // Add items to the end. 6269 while (tabDataList.length < changedTabPos) { 6270 let existingTabEl = tabElements[tabDataList.length]; 6271 tabDataList.push({ 6272 entries: [], 6273 lastAccessed: existingTabEl.lastAccessed, 6274 }); 6275 } 6276 // Ensure the pre-existing items are non-null. 6277 for (let i = 0; i < initialDataListLength; i++) { 6278 if (!tabDataList[i]) { 6279 let existingTabEl = tabElements[i]; 6280 tabDataList[i] = { 6281 entries: [], 6282 lastAccessed: existingTabEl.lastAccessed, 6283 }; 6284 } 6285 } 6286 }, 6287 6288 // Restores the given tab state for a given tab. 6289 restoreTab(tab, tabData, options = {}) { 6290 let browser = tab.linkedBrowser; 6291 6292 if (TAB_STATE_FOR_BROWSER.has(browser)) { 6293 this._log.warn("Must reset tab before calling restoreTab."); 6294 return; 6295 } 6296 6297 let loadArguments = options.loadArguments; 6298 let window = tab.ownerGlobal; 6299 let tabbrowser = window.gBrowser; 6300 let forceOnDemand = options.forceOnDemand; 6301 let isRemotenessUpdate = options.isRemotenessUpdate; 6302 6303 let willRestoreImmediately = 6304 options.restoreImmediately || tabbrowser.selectedBrowser == browser; 6305 6306 let isBrowserInserted = browser.isConnected; 6307 6308 // Increase the busy state counter before modifying the tab. 6309 this._setWindowStateBusy(window); 6310 6311 // It's important to set the window state to dirty so that 6312 // we collect their data for the first time when saving state. 6313 DirtyWindows.add(window); 6314 6315 if (!tab.hasOwnProperty("_tPos")) { 6316 throw new Error( 6317 "Shouldn't be trying to restore a tab that has no position" 6318 ); 6319 } 6320 // Update the tab state in case we shut down without being notified. 6321 this._windows[window.__SSi].tabs[tab._tPos] = tabData; 6322 6323 // Prepare the tab so that it can be properly restored. We'll also attach 6324 // a copy of the tab's data in case we close it before it's been restored. 6325 // Anything that dispatches an event to external consumers must happen at 6326 // the end of this method, to make sure that the tab/browser object is in a 6327 // reliable and consistent state. 6328 6329 if (tabData.lastAccessed) { 6330 tab.updateLastAccessed(tabData.lastAccessed); 6331 } 6332 6333 if (!tabData.entries) { 6334 tabData.entries = []; 6335 } 6336 if (tabData.extData) { 6337 TAB_CUSTOM_VALUES.set(tab, Cu.cloneInto(tabData.extData, {})); 6338 } else { 6339 TAB_CUSTOM_VALUES.delete(tab); 6340 } 6341 6342 // Tab is now open. 6343 delete tabData.closedAt; 6344 6345 // Ensure the index is in bounds. 6346 let activeIndex = (tabData.index || tabData.entries.length) - 1; 6347 activeIndex = Math.min(activeIndex, tabData.entries.length - 1); 6348 activeIndex = Math.max(activeIndex, 0); 6349 6350 // Save the index in case we updated it above. 6351 tabData.index = activeIndex + 1; 6352 6353 tab.setAttribute("pending", "true"); 6354 6355 // If we're restoring this tab, it certainly shouldn't be in 6356 // the ignored set anymore. 6357 this._crashedBrowsers.delete(browser.permanentKey); 6358 6359 // If we're in the midst of performing a process flip, then we must 6360 // have initiated a navigation. This means that these userTyped* 6361 // values are now out of date. 6362 if ( 6363 options.restoreContentReason == 6364 RESTORE_TAB_CONTENT_REASON.NAVIGATE_AND_RESTORE 6365 ) { 6366 delete tabData.userTypedValue; 6367 delete tabData.userTypedClear; 6368 } 6369 6370 // Update the persistent tab state cache with |tabData| information. 6371 lazy.TabStateCache.update(browser.permanentKey, { 6372 // NOTE: Copy the entries array shallowly, so as to not screw with the 6373 // original tabData's history when getting history updates. 6374 history: { entries: [...tabData.entries], index: tabData.index }, 6375 scroll: tabData.scroll || null, 6376 storage: tabData.storage || null, 6377 formdata: tabData.formdata || null, 6378 disallow: tabData.disallow || null, 6379 userContextId: tabData.userContextId || 0, 6380 6381 // This information is only needed until the tab has finished restoring. 6382 // When that's done it will be removed from the cache and we always 6383 // collect it in TabState._collectBaseTabData(). 6384 image: tabData.image || "", 6385 searchMode: tabData.searchMode || null, 6386 userTypedValue: tabData.userTypedValue || "", 6387 userTypedClear: tabData.userTypedClear || 0, 6388 }); 6389 6390 // Restore tab attributes. 6391 if ("attributes" in tabData) { 6392 lazy.TabAttributes.set(tab, tabData.attributes); 6393 } 6394 6395 if (isBrowserInserted) { 6396 // Start a new epoch to discard all frame script messages relating to a 6397 // previous epoch. All async messages that are still on their way to chrome 6398 // will be ignored and don't override any tab data set when restoring. 6399 let epoch = this.startNextEpoch(browser.permanentKey); 6400 6401 // Ensure that the tab will get properly restored in the event the tab 6402 // crashes while restoring. But don't set this on lazy browsers as 6403 // restoreTab will get called again when the browser is instantiated. 6404 TAB_STATE_FOR_BROWSER.set(browser, TAB_STATE_NEEDS_RESTORE); 6405 6406 this._sendRestoreHistory(browser, { 6407 tabData, 6408 epoch, 6409 loadArguments, 6410 isRemotenessUpdate, 6411 }); 6412 6413 // This could cause us to ignore MAX_CONCURRENT_TAB_RESTORES a bit, but 6414 // it ensures each window will have its selected tab loaded. 6415 if (willRestoreImmediately) { 6416 this.restoreTabContent(tab, options); 6417 } else if (!forceOnDemand) { 6418 TabRestoreQueue.add(tab); 6419 // Check if a tab is in queue and will be restored 6420 // after the currently loading tabs. If so, prepare 6421 // a connection to host to speed up page loading. 6422 if (TabRestoreQueue.willRestoreSoon(tab)) { 6423 if (activeIndex in tabData.entries) { 6424 let url = tabData.entries[activeIndex].url; 6425 let prepared = this.prepareConnectionToHost(tab, url); 6426 if (gDebuggingEnabled) { 6427 tab.__test_connection_prepared = prepared; 6428 tab.__test_connection_url = url; 6429 } 6430 } 6431 } 6432 this.restoreNextTab(); 6433 } 6434 } else { 6435 // TAB_LAZY_STATES holds data for lazy-browser tabs to proxy for 6436 // data unobtainable from the unbound browser. This only applies to lazy 6437 // browsers and will be removed once the browser is inserted in the document. 6438 // This must preceed `updateTabLabelAndIcon` call for required data to be present. 6439 let url = "about:blank"; 6440 let title = ""; 6441 6442 if (activeIndex in tabData.entries) { 6443 url = tabData.entries[activeIndex].url; 6444 title = tabData.entries[activeIndex].title || url; 6445 } 6446 TAB_LAZY_STATES.set(tab, { 6447 url, 6448 title, 6449 userTypedValue: tabData.userTypedValue || "", 6450 userTypedClear: tabData.userTypedClear || 0, 6451 }); 6452 } 6453 6454 // Most of tabData has been restored, now continue with restoring 6455 // attributes that may trigger external events. 6456 6457 if (tabData.pinned) { 6458 tabbrowser.pinTab(tab); 6459 } else { 6460 tabbrowser.unpinTab(tab); 6461 } 6462 6463 if (tabData.hidden) { 6464 tabbrowser.hideTab(tab); 6465 } else { 6466 tabbrowser.showTab(tab); 6467 } 6468 6469 if (!!tabData.muted != browser.audioMuted) { 6470 tab.toggleMuteAudio(tabData.muteReason); 6471 } 6472 6473 if (tab.hasAttribute("customizemode")) { 6474 window.gCustomizeMode.setTab(tab); 6475 } 6476 6477 // Update tab label and icon to show something 6478 // while we wait for the messages to be processed. 6479 this.updateTabLabelAndIcon(tab, tabData); 6480 6481 // Decrease the busy state counter after we're done. 6482 this._setWindowStateReady(window); 6483 }, 6484 6485 /** 6486 * Kicks off restoring the given tab. 6487 * 6488 * @param aTab 6489 * the tab to restore 6490 * @param aOptions 6491 * optional arguments used when performing process switch during load 6492 */ 6493 restoreTabContent(aTab, aOptions = {}) { 6494 let loadArguments = aOptions.loadArguments; 6495 if (aTab.hasAttribute("customizemode") && !loadArguments) { 6496 return; 6497 } 6498 6499 let browser = aTab.linkedBrowser; 6500 let window = aTab.ownerGlobal; 6501 let tabData = lazy.TabState.clone(aTab, TAB_CUSTOM_VALUES.get(aTab)); 6502 let activeIndex = tabData.index - 1; 6503 let activePageData = tabData.entries[activeIndex] || null; 6504 let uri = activePageData ? activePageData.url || null : null; 6505 6506 this.markTabAsRestoring(aTab); 6507 6508 this._sendRestoreTabContent(browser, { 6509 loadArguments, 6510 isRemotenessUpdate: aOptions.isRemotenessUpdate, 6511 reason: 6512 aOptions.restoreContentReason || RESTORE_TAB_CONTENT_REASON.SET_STATE, 6513 }); 6514 6515 // Focus the tab's content area, unless the restore is for a new tab URL or 6516 // was triggered by a DocumentChannel process switch. 6517 if ( 6518 aTab.selected && 6519 !window.isBlankPageURL(uri) && 6520 !aOptions.isRemotenessUpdate 6521 ) { 6522 browser.focus(); 6523 } 6524 }, 6525 6526 /** 6527 * Marks a given pending tab as restoring. 6528 * 6529 * @param aTab 6530 * the pending tab to mark as restoring 6531 */ 6532 markTabAsRestoring(aTab) { 6533 let browser = aTab.linkedBrowser; 6534 if (TAB_STATE_FOR_BROWSER.get(browser) != TAB_STATE_NEEDS_RESTORE) { 6535 throw new Error("Given tab is not pending."); 6536 } 6537 6538 // Make sure that this tab is removed from the priority queue. 6539 TabRestoreQueue.remove(aTab); 6540 6541 // Increase our internal count. 6542 this._tabsRestoringCount++; 6543 6544 // Set this tab's state to restoring 6545 TAB_STATE_FOR_BROWSER.set(browser, TAB_STATE_RESTORING); 6546 aTab.removeAttribute("pending"); 6547 aTab.removeAttribute("discarded"); 6548 }, 6549 6550 /** 6551 * This _attempts_ to restore the next available tab. If the restore fails, 6552 * then we will attempt the next one. 6553 * There are conditions where this won't do anything: 6554 * if we're in the process of quitting 6555 * if there are no tabs to restore 6556 * if we have already reached the limit for number of tabs to restore 6557 */ 6558 restoreNextTab: function ssi_restoreNextTab() { 6559 // If we call in here while quitting, we don't actually want to do anything 6560 if (lazy.RunState.isQuitting) { 6561 return; 6562 } 6563 6564 // Don't exceed the maximum number of concurrent tab restores. 6565 if (this._tabsRestoringCount >= MAX_CONCURRENT_TAB_RESTORES) { 6566 return; 6567 } 6568 6569 let tab = TabRestoreQueue.shift(); 6570 if (tab) { 6571 this.restoreTabContent(tab); 6572 } 6573 }, 6574 6575 /** 6576 * Restore visibility and dimension features to a window 6577 * 6578 * @param aWindow 6579 * Window reference 6580 * @param aWinData 6581 * Object containing session data for the window 6582 */ 6583 restoreWindowFeatures: function ssi_restoreWindowFeatures(aWindow, aWinData) { 6584 var hidden = aWinData.hidden ? aWinData.hidden.split(",") : []; 6585 var isTaskbarTab = 6586 aWindow.document.documentElement.hasAttribute("taskbartab"); 6587 if (!isTaskbarTab) { 6588 WINDOW_HIDEABLE_FEATURES.forEach(function (aItem) { 6589 aWindow[aItem].visible = !hidden.includes(aItem); 6590 }); 6591 } 6592 6593 if (aWinData.isPopup) { 6594 this._windows[aWindow.__SSi].isPopup = true; 6595 if (aWindow.gURLBar) { 6596 aWindow.gURLBar.readOnly = true; 6597 } 6598 } else { 6599 delete this._windows[aWindow.__SSi].isPopup; 6600 if (aWindow.gURLBar && !isTaskbarTab) { 6601 aWindow.gURLBar.readOnly = false; 6602 } 6603 } 6604 6605 let promiseParts = Promise.withResolvers(); 6606 aWindow.setTimeout(() => { 6607 this.restoreDimensions( 6608 aWindow, 6609 +(aWinData.width || 0), 6610 +(aWinData.height || 0), 6611 "screenX" in aWinData ? +aWinData.screenX : NaN, 6612 "screenY" in aWinData ? +aWinData.screenY : NaN, 6613 aWinData.sizemode || "", 6614 aWinData.sizemodeBeforeMinimized || "" 6615 ); 6616 this.restoreSidebar(aWindow, aWinData.sidebar, aWinData.isPopup); 6617 promiseParts.resolve(aWindow); 6618 }, 0); 6619 return promiseParts.promise; 6620 }, 6621 6622 /** 6623 * @param aWindow 6624 * Window reference 6625 * @param aSidebar 6626 * Object containing command (sidebarcommand/category) and styles 6627 */ 6628 restoreSidebar(aWindow, aSidebar, isPopup) { 6629 if (!aSidebar || isPopup) { 6630 return; 6631 } 6632 aWindow.SidebarController.initializeUIState(aSidebar); 6633 }, 6634 6635 /** 6636 * Restore a window's dimensions 6637 * 6638 * @param aWidth 6639 * Window width in desktop pixels 6640 * @param aHeight 6641 * Window height in desktop pixels 6642 * @param aLeft 6643 * Window left in desktop pixels 6644 * @param aTop 6645 * Window top in desktop pixels 6646 * @param aSizeMode 6647 * Window size mode (eg: maximized) 6648 * @param aSizeModeBeforeMinimized 6649 * Window size mode before window got minimized (eg: maximized) 6650 */ 6651 restoreDimensions: function ssi_restoreDimensions( 6652 aWindow, 6653 aWidth, 6654 aHeight, 6655 aLeft, 6656 aTop, 6657 aSizeMode, 6658 aSizeModeBeforeMinimized 6659 ) { 6660 var win = aWindow; 6661 var _this = this; 6662 function win_(aName) { 6663 return _this._getWindowDimension(win, aName); 6664 } 6665 6666 const dwu = win.windowUtils; 6667 // find available space on the screen where this window is being placed 6668 let screen = lazy.gScreenManager.screenForRect( 6669 aLeft, 6670 aTop, 6671 aWidth, 6672 aHeight 6673 ); 6674 if (screen) { 6675 let screenLeft = {}, 6676 screenTop = {}, 6677 screenWidth = {}, 6678 screenHeight = {}; 6679 screen.GetAvailRectDisplayPix( 6680 screenLeft, 6681 screenTop, 6682 screenWidth, 6683 screenHeight 6684 ); 6685 6686 // We store aLeft / aTop (screenX/Y) in desktop pixels, see 6687 // _getWindowDimension. 6688 screenLeft = screenLeft.value; 6689 screenTop = screenTop.value; 6690 screenWidth = screenWidth.value; 6691 screenHeight = screenHeight.value; 6692 6693 let screenBottom = screenTop + screenHeight; 6694 let screenRight = screenLeft + screenWidth; 6695 6696 // NOTE: contentsScaleFactor is the desktopToDeviceScale of the screen. 6697 // Naming could be more consistent here. 6698 let cssToDesktopScale = 6699 screen.defaultCSSScaleFactor / screen.contentsScaleFactor; 6700 6701 let winSlopX = win.screenEdgeSlopX * cssToDesktopScale; 6702 let winSlopY = win.screenEdgeSlopY * cssToDesktopScale; 6703 6704 let minSlop = MIN_SCREEN_EDGE_SLOP * cssToDesktopScale; 6705 let slopX = Math.max(minSlop, winSlopX); 6706 let slopY = Math.max(minSlop, winSlopY); 6707 6708 // Pull the window within the screen's bounds (allowing a little slop 6709 // for windows that may be deliberately placed with their border off-screen 6710 // as when Win10 "snaps" a window to the left/right edge -- bug 1276516). 6711 // First, ensure the left edge is large enough... 6712 if (aLeft < screenLeft - slopX) { 6713 aLeft = screenLeft - winSlopX; 6714 } 6715 // Then check the resulting right edge, and reduce it if necessary. 6716 let right = aLeft + aWidth * cssToDesktopScale; 6717 if (right > screenRight + slopX) { 6718 right = screenRight + winSlopX; 6719 // See if we can move the left edge leftwards to maintain width. 6720 if (aLeft > screenLeft) { 6721 aLeft = Math.max( 6722 right - aWidth * cssToDesktopScale, 6723 screenLeft - winSlopX 6724 ); 6725 } 6726 } 6727 // Finally, update aWidth to account for the adjusted left and right 6728 // edges, and convert it back to CSS pixels on the target screen. 6729 aWidth = (right - aLeft) / cssToDesktopScale; 6730 6731 // And do the same in the vertical dimension. 6732 if (aTop < screenTop - slopY) { 6733 aTop = screenTop - winSlopY; 6734 } 6735 let bottom = aTop + aHeight * cssToDesktopScale; 6736 if (bottom > screenBottom + slopY) { 6737 bottom = screenBottom + winSlopY; 6738 if (aTop > screenTop) { 6739 aTop = Math.max( 6740 bottom - aHeight * cssToDesktopScale, 6741 screenTop - winSlopY 6742 ); 6743 } 6744 } 6745 aHeight = (bottom - aTop) / cssToDesktopScale; 6746 } 6747 6748 // Suppress animations. 6749 dwu.suppressAnimation(true); 6750 6751 // We want to make sure users will get their animations back in case an exception is thrown. 6752 try { 6753 // only modify those aspects which aren't correct yet 6754 if ( 6755 !isNaN(aLeft) && 6756 !isNaN(aTop) && 6757 (aLeft != win_("screenX") || aTop != win_("screenY")) 6758 ) { 6759 // moveTo uses CSS pixels relative to aWindow, while aLeft and aRight 6760 // are on desktop pixels, undo the conversion we do in 6761 // _getWindowDimension. 6762 let desktopToCssScale = 6763 aWindow.desktopToDeviceScale / aWindow.devicePixelRatio; 6764 aWindow.moveTo(aLeft * desktopToCssScale, aTop * desktopToCssScale); 6765 } 6766 if ( 6767 aWidth && 6768 aHeight && 6769 (aWidth != win_("width") || aHeight != win_("height")) && 6770 !ChromeUtils.shouldResistFingerprinting("RoundWindowSize", null) 6771 ) { 6772 // Don't resize the window if it's currently maximized and we would 6773 // maximize it again shortly after. 6774 if (aSizeMode != "maximized" || win_("sizemode") != "maximized") { 6775 aWindow.resizeTo(aWidth, aHeight); 6776 } 6777 } 6778 this._windows[aWindow.__SSi].sizemodeBeforeMinimized = 6779 aSizeModeBeforeMinimized; 6780 if ( 6781 aSizeMode && 6782 win_("sizemode") != aSizeMode && 6783 !ChromeUtils.shouldResistFingerprinting("RoundWindowSize", null) 6784 ) { 6785 switch (aSizeMode) { 6786 case "maximized": 6787 aWindow.maximize(); 6788 break; 6789 case "minimized": 6790 if (aSizeModeBeforeMinimized == "maximized") { 6791 aWindow.maximize(); 6792 } 6793 aWindow.minimize(); 6794 break; 6795 case "normal": 6796 aWindow.restore(); 6797 break; 6798 } 6799 } 6800 // since resizing/moving a window brings it to the foreground, 6801 // we might want to re-focus the last focused window 6802 if (this.windowToFocus) { 6803 this.windowToFocus.focus(); 6804 } 6805 } finally { 6806 // Enable animations. 6807 dwu.suppressAnimation(false); 6808 } 6809 }, 6810 6811 /* ........ Disk Access .............. */ 6812 6813 /** 6814 * Save the current session state to disk, after a delay. 6815 * 6816 * @param aWindow (optional) 6817 * Will mark the given window as dirty so that we will recollect its 6818 * data before we start writing. 6819 */ 6820 saveStateDelayed(aWindow = null) { 6821 if (aWindow) { 6822 DirtyWindows.add(aWindow); 6823 } 6824 6825 lazy.SessionSaver.runDelayed(); 6826 }, 6827 6828 /* ........ Auxiliary Functions .............. */ 6829 6830 /** 6831 * Remove a closed window from the list of closed windows and indicate that 6832 * the change should be notified. 6833 * 6834 * @param index 6835 * The index of the window in this._closedWindows. 6836 * 6837 * @returns Array of closed windows. 6838 */ 6839 _removeClosedWindow(index) { 6840 // remove all of the closed tabs from the _lastClosedActions list 6841 // before removing the window from it 6842 for (let closedTab of this._closedWindows[index]._closedTabs) { 6843 this._removeClosedAction( 6844 this._LAST_ACTION_CLOSED_TAB, 6845 closedTab.closedId 6846 ); 6847 } 6848 this._removeClosedAction( 6849 this._LAST_ACTION_CLOSED_WINDOW, 6850 this._closedWindows[index].closedId 6851 ); 6852 let windows = this._closedWindows.splice(index, 1); 6853 this._closedObjectsChanged = true; 6854 return windows; 6855 }, 6856 6857 /** 6858 * Notifies observers that the list of closed tabs and/or windows has changed. 6859 * Waits a tick to allow SessionStorage a chance to register the change. 6860 */ 6861 _notifyOfClosedObjectsChange() { 6862 if (!this._closedObjectsChanged) { 6863 return; 6864 } 6865 this._closedObjectsChanged = false; 6866 lazy.setTimeout(() => { 6867 Services.obs.notifyObservers(null, NOTIFY_CLOSED_OBJECTS_CHANGED); 6868 }, 0); 6869 }, 6870 6871 /** 6872 * Notifies observers that the list of saved tab groups has changed. 6873 * Waits a tick to allow SessionStorage a chance to register the change. 6874 */ 6875 _notifyOfSavedTabGroupsChange() { 6876 lazy.setTimeout(() => { 6877 Services.obs.notifyObservers(null, NOTIFY_SAVED_TAB_GROUPS_CHANGED); 6878 }, 0); 6879 }, 6880 6881 /** 6882 * Update the session start time and send a telemetry measurement 6883 * for the number of days elapsed since the session was started. 6884 * 6885 * @param state 6886 * The session state. 6887 */ 6888 _updateSessionStartTime: function ssi_updateSessionStartTime(state) { 6889 // Attempt to load the session start time from the session state 6890 if (state.session && state.session.startTime) { 6891 this._sessionStartTime = state.session.startTime; 6892 } 6893 }, 6894 6895 /** 6896 * Iterator that yields all currently opened browser windows. 6897 * (Might miss the most recent one.) 6898 * This list is in focus order, but may include minimized windows 6899 * before non-minimized windows. 6900 */ 6901 _browserWindows: { 6902 *[Symbol.iterator]() { 6903 for (let window of lazy.BrowserWindowTracker.orderedWindows) { 6904 if (window.__SSi && !window.closed) { 6905 yield window; 6906 } 6907 } 6908 }, 6909 }, 6910 6911 /** 6912 * Iterator that yields all currently opened browser windows, 6913 * with minimized windows last. 6914 * (Might miss the most recent window.) 6915 */ 6916 _orderedBrowserWindows: { 6917 *[Symbol.iterator]() { 6918 let windows = lazy.BrowserWindowTracker.orderedWindows; 6919 windows.sort((a, b) => { 6920 if ( 6921 a.windowState == a.STATE_MINIMIZED && 6922 b.windowState != b.STATE_MINIMIZED 6923 ) { 6924 return 1; 6925 } 6926 if ( 6927 a.windowState != a.STATE_MINIMIZED && 6928 b.windowState == b.STATE_MINIMIZED 6929 ) { 6930 return -1; 6931 } 6932 return 0; 6933 }); 6934 for (let window of windows) { 6935 if (window.__SSi && !window.closed) { 6936 yield window; 6937 } 6938 } 6939 }, 6940 }, 6941 6942 /** 6943 * Returns most recent window 6944 * 6945 * @param {boolean} [isPrivate] 6946 * Optional boolean to get only non-private or private windows 6947 * When omitted, we'll return whatever the top-most window is regardless of privateness 6948 * @returns Window reference 6949 */ 6950 _getTopWindow: function ssi_getTopWindow(isPrivate) { 6951 const options = { allowPopups: true }; 6952 if (typeof isPrivate !== "undefined") { 6953 options.private = isPrivate; 6954 } 6955 return lazy.BrowserWindowTracker.getTopWindow(options); 6956 }, 6957 6958 /** 6959 * Calls onClose for windows that are determined to be closed but aren't 6960 * destroyed yet, which would otherwise cause getBrowserState and 6961 * setBrowserState to treat them as open windows. 6962 */ 6963 _handleClosedWindows: function ssi_handleClosedWindows() { 6964 let promises = []; 6965 for (let window of Services.wm.getEnumerator("navigator:browser")) { 6966 if (window.closed) { 6967 promises.push(this.onClose(window)); 6968 } 6969 } 6970 return Promise.all(promises); 6971 }, 6972 6973 /** 6974 * Store a restore state of a window to this._statesToRestore. The window 6975 * will be given an id that can be used to get the restore state from 6976 * this._statesToRestore. 6977 * 6978 * @param window 6979 * a reference to a window that has a state to restore 6980 * @param state 6981 * an object containing session data 6982 */ 6983 _updateWindowRestoreState(window, state) { 6984 // Store z-index, so that windows can be restored in reversed z-order. 6985 if ("zIndex" in state.windows[0]) { 6986 WINDOW_RESTORE_ZINDICES.set(window, state.windows[0].zIndex); 6987 } 6988 do { 6989 var ID = "window" + Math.random(); 6990 } while (ID in this._statesToRestore); 6991 WINDOW_RESTORE_IDS.set(window, ID); 6992 this._statesToRestore[ID] = state; 6993 }, 6994 6995 /** 6996 * open a new browser window for a given session state 6997 * called when restoring a multi-window session 6998 * 6999 * @param aState 7000 * Object containing session data 7001 */ 7002 _openWindowWithState: function ssi_openWindowWithState(aState) { 7003 // Build arguments string 7004 let argString; 7005 // Build feature string 7006 let features; 7007 let winState = aState.windows[0]; 7008 if (winState.chromeFlags) { 7009 features = ["chrome", "suppressanimation"]; 7010 let chromeFlags = winState.chromeFlags; 7011 const allFlags = Ci.nsIWebBrowserChrome.CHROME_ALL; 7012 const hasAll = (chromeFlags & allFlags) == allFlags; 7013 if (hasAll) { 7014 features.push("all"); 7015 } 7016 for (let [flag, onValue, offValue] of CHROME_FLAGS_MAP) { 7017 if (hasAll && allFlags & flag) { 7018 continue; 7019 } 7020 let value = chromeFlags & flag ? onValue : offValue; 7021 if (value) { 7022 features.push(value); 7023 } 7024 } 7025 } else { 7026 // |chromeFlags| is not found. Fallbacks to the old method. 7027 features = ["chrome", "dialog=no", "suppressanimation"]; 7028 let hidden = winState.hidden?.split(",") || []; 7029 if (!hidden.length) { 7030 features.push("all"); 7031 } else { 7032 features.push("resizable"); 7033 WINDOW_HIDEABLE_FEATURES.forEach(aFeature => { 7034 if (!hidden.includes(aFeature)) { 7035 features.push(WINDOW_OPEN_FEATURES_MAP[aFeature] || aFeature); 7036 } 7037 }); 7038 } 7039 } 7040 WINDOW_ATTRIBUTES.forEach(aFeature => { 7041 // Use !isNaN as an easy way to ignore sizemode and check for numbers 7042 if (aFeature in winState && !isNaN(winState[aFeature])) { 7043 features.push(aFeature + "=" + winState[aFeature]); 7044 } 7045 }); 7046 7047 // A window CANNOT be both a Private Window and an AI Window 7048 if (winState.isPrivate) { 7049 features.push("private"); 7050 } else if (winState.isAIWindow) { 7051 argString = lazy.AIWindow.handleAIWindowOptions({ 7052 openerWindow: null, 7053 args: argString, 7054 aiWindow: winState.isAIWindow, 7055 restoreSession: true, 7056 }); 7057 } 7058 7059 if (!argString) { 7060 argString = Cc["@mozilla.org/supports-string;1"].createInstance( 7061 Ci.nsISupportsString 7062 ); 7063 argString.data = ""; 7064 } 7065 7066 this._log.debug( 7067 `Opening window:${winState.closedId} with features: ${features.join( 7068 "," 7069 )}, argString: ${argString}.` 7070 ); 7071 var window = Services.ww.openWindow( 7072 null, 7073 AppConstants.BROWSER_CHROME_URL, 7074 "_blank", 7075 features.join(","), 7076 argString 7077 ); 7078 7079 this._updateWindowRestoreState(window, aState); 7080 WINDOW_SHOWING_PROMISES.set(window, Promise.withResolvers()); 7081 7082 return window; 7083 }, 7084 7085 /** 7086 * whether the user wants to load any other page at startup 7087 * (except the homepage) - needed for determining whether to overwrite the current tabs 7088 * C.f.: nsBrowserContentHandler's defaultArgs implementation. 7089 * 7090 * @returns bool 7091 */ 7092 _isCmdLineEmpty: function ssi_isCmdLineEmpty(aWindow, aState) { 7093 var pinnedOnly = 7094 aState.windows && 7095 aState.windows.every(win => win.tabs.every(tab => tab.pinned)); 7096 7097 let hasFirstArgument = aWindow.arguments && aWindow.arguments[0]; 7098 if (!pinnedOnly) { 7099 let defaultArgs = Cc["@mozilla.org/browser/clh;1"].getService( 7100 Ci.nsIBrowserHandler 7101 ).defaultArgs; 7102 if ( 7103 aWindow.arguments && 7104 aWindow.arguments[0] && 7105 aWindow.arguments[0] == defaultArgs 7106 ) { 7107 hasFirstArgument = false; 7108 } 7109 } 7110 7111 return !hasFirstArgument; 7112 }, 7113 7114 /** 7115 * on popup windows, the AppWindow's attributes seem not to be set correctly 7116 * we use thus JSDOMWindow attributes for sizemode and normal window attributes 7117 * (and hope for reasonable values when maximized/minimized - since then 7118 * outerWidth/outerHeight aren't the dimensions of the restored window) 7119 * 7120 * @param aWindow 7121 * Window reference 7122 * @param aAttribute 7123 * String sizemode | width | height | other window attribute 7124 * @returns string 7125 */ 7126 _getWindowDimension: function ssi_getWindowDimension(aWindow, aAttribute) { 7127 if (aAttribute == "sizemode") { 7128 switch (aWindow.windowState) { 7129 case aWindow.STATE_FULLSCREEN: 7130 case aWindow.STATE_MAXIMIZED: 7131 return "maximized"; 7132 case aWindow.STATE_MINIMIZED: 7133 return "minimized"; 7134 default: 7135 return "normal"; 7136 } 7137 } 7138 7139 // We want to persist the size / position in normal state, so that 7140 // we can restore to them even if the window is currently maximized 7141 // or minimized. However, attributes on window object only reflect 7142 // the current state of the window, so when it isn't in the normal 7143 // sizemode, their values aren't what we want the window to restore 7144 // to. In that case, try to read from the attributes of the root 7145 // element first instead. 7146 if (aWindow.windowState != aWindow.STATE_NORMAL) { 7147 let docElem = aWindow.document.documentElement; 7148 let attr = parseInt(docElem.getAttribute(aAttribute), 10); 7149 if (attr) { 7150 if (aAttribute != "width" && aAttribute != "height") { 7151 return attr; 7152 } 7153 // Width and height attribute report the inner size, but we want 7154 // to store the outer size, so add the difference. 7155 let appWin = aWindow.docShell.treeOwner 7156 .QueryInterface(Ci.nsIInterfaceRequestor) 7157 .getInterface(Ci.nsIAppWindow); 7158 let diff = 7159 aAttribute == "width" 7160 ? appWin.outerToInnerWidthDifferenceInCSSPixels 7161 : appWin.outerToInnerHeightDifferenceInCSSPixels; 7162 return attr + diff; 7163 } 7164 } 7165 7166 switch (aAttribute) { 7167 case "width": 7168 return aWindow.outerWidth; 7169 case "height": 7170 return aWindow.outerHeight; 7171 case "screenX": 7172 case "screenY": 7173 // We use desktop pixels rather than CSS pixels to store window 7174 // positions, see bug 1247335. This allows proper multi-monitor 7175 // positioning in mixed-DPI situations. 7176 // screenX/Y are in CSS pixels for the current window, so, convert them 7177 // to desktop pixels. 7178 return ( 7179 (aWindow[aAttribute] * aWindow.devicePixelRatio) / 7180 aWindow.desktopToDeviceScale 7181 ); 7182 default: 7183 return aAttribute in aWindow ? aWindow[aAttribute] : ""; 7184 } 7185 }, 7186 7187 /** 7188 * @param aState is a session state 7189 * @param aRecentCrashes is the number of consecutive crashes 7190 * @returns whether a restore page will be needed for the session state 7191 */ 7192 _needsRestorePage: function ssi_needsRestorePage(aState, aRecentCrashes) { 7193 const SIX_HOURS_IN_MS = 6 * 60 * 60 * 1000; 7194 7195 // don't display the page when there's nothing to restore 7196 let winData = aState.windows || null; 7197 if (!winData || !winData.length) { 7198 return false; 7199 } 7200 7201 // don't wrap a single about:sessionrestore page 7202 if ( 7203 this._hasSingleTabWithURL(winData, "about:sessionrestore") || 7204 this._hasSingleTabWithURL(winData, "about:welcomeback") 7205 ) { 7206 return false; 7207 } 7208 7209 // don't automatically restore in Safe Mode 7210 if (Services.appinfo.inSafeMode) { 7211 return true; 7212 } 7213 7214 let max_resumed_crashes = this._prefBranch.getIntPref( 7215 "sessionstore.max_resumed_crashes" 7216 ); 7217 let sessionAge = 7218 aState.session && 7219 aState.session.lastUpdate && 7220 Date.now() - aState.session.lastUpdate; 7221 7222 let decision = 7223 max_resumed_crashes != -1 && 7224 (aRecentCrashes > max_resumed_crashes || 7225 (sessionAge && sessionAge >= SIX_HOURS_IN_MS)); 7226 if (decision) { 7227 let key; 7228 if (aRecentCrashes > max_resumed_crashes) { 7229 if (sessionAge && sessionAge >= SIX_HOURS_IN_MS) { 7230 key = "shown_many_crashes_old_session"; 7231 } else { 7232 key = "shown_many_crashes"; 7233 } 7234 } else { 7235 key = "shown_old_session"; 7236 } 7237 Glean.browserEngagement.sessionrestoreInterstitial[key].add(1); 7238 } 7239 return decision; 7240 }, 7241 7242 /** 7243 * @param aWinData is the set of windows in session state 7244 * @param aURL is the single URL we're looking for 7245 * @returns whether the window data contains only the single URL passed 7246 */ 7247 _hasSingleTabWithURL(aWinData, aURL) { 7248 if ( 7249 aWinData && 7250 aWinData.length == 1 && 7251 aWinData[0].tabs && 7252 aWinData[0].tabs.length == 1 && 7253 aWinData[0].tabs[0].entries && 7254 aWinData[0].tabs[0].entries.length == 1 7255 ) { 7256 return aURL == aWinData[0].tabs[0].entries[0].url; 7257 } 7258 return false; 7259 }, 7260 7261 /** 7262 * Determine if the tab state we're passed is something we should save. This 7263 * is used when closing a tab, tab group, or closing a window with a single tab 7264 * 7265 * @param aTabState 7266 * The current tab state 7267 * @returns boolean 7268 */ 7269 _shouldSaveTabState: function ssi_shouldSaveTabState(aTabState) { 7270 // If the tab has only a transient about: history entry, no other 7271 // session history, and no userTypedValue, then we don't actually want to 7272 // store this tab's data. 7273 const entryUrl = aTabState.entries[0]?.url; 7274 return ( 7275 entryUrl && 7276 !( 7277 aTabState.entries.length == 1 && 7278 (entryUrl == "about:blank" || 7279 entryUrl == "about:home" || 7280 entryUrl == "about:newtab" || 7281 entryUrl == "about:privatebrowsing") && 7282 !aTabState.userTypedValue 7283 ) 7284 ); 7285 }, 7286 7287 /** 7288 * @param {MozTabbrowserTab[]} tabs 7289 * @returns {boolean} 7290 */ 7291 shouldSaveTabsToGroup: function ssi_shouldSaveTabsToGroup(tabs) { 7292 if (!tabs) { 7293 return false; 7294 } 7295 for (let tab of tabs) { 7296 let tabState = lazy.TabState.collect(tab); 7297 if (this._shouldSaveTabState(tabState)) { 7298 return true; 7299 } 7300 } 7301 return false; 7302 }, 7303 7304 /** 7305 * Determine if the tab state we're passed is something we should keep to be 7306 * reopened at session restore. This is used when we are saving the current 7307 * session state to disk. This method is very similar to _shouldSaveTabState, 7308 * however, "about:blank" and "about:newtab" tabs will still be saved to disk. 7309 * 7310 * @param aTabState 7311 * The current tab state 7312 * @returns boolean 7313 */ 7314 _shouldSaveTab: function ssi_shouldSaveTab(aTabState) { 7315 // If the tab has one of the following transient about: history entry, no 7316 // userTypedValue, and no customizemode attribute, then we don't actually 7317 // want to write this tab's data to disk. 7318 return ( 7319 aTabState.userTypedValue || 7320 (aTabState.attributes && aTabState.attributes.customizemode == "true") || 7321 (aTabState.entries.length && 7322 aTabState.entries[0].url != "about:privatebrowsing") 7323 ); 7324 }, 7325 7326 /** 7327 * This is going to take a state as provided at startup (via 7328 * SessionStartup.state) and split it into 2 parts. The first part 7329 * (defaultState) will be a state that should still be restored at startup, 7330 * while the second part (state) is a state that should be saved for later. 7331 * defaultState is derived from a clone of startupState, 7332 * and will be comprised of: 7333 * - windows with only pinned tabs, 7334 * - window position information, and 7335 * - saved groups, including groups that were open at last shutdown. 7336 * 7337 * defaultState will be restored at startup. state will be passed into 7338 * LastSession and will be kept in case the user explicitly wants 7339 * to restore the previous session (publicly exposed as restoreLastSession). 7340 * 7341 * @param state 7342 * The startupState, presumably from SessionStartup.state 7343 * @returns [defaultState, state] 7344 */ 7345 _prepDataForDeferredRestore: function ssi_prepDataForDeferredRestore( 7346 startupState 7347 ) { 7348 // Make sure that we don't modify the global state as provided by 7349 // SessionStartup.state. 7350 let state = Cu.cloneInto(startupState, {}); 7351 let hasPinnedTabs = false; 7352 let defaultState = { 7353 windows: [], 7354 selectedWindow: 1, 7355 savedGroups: state.savedGroups || [], 7356 }; 7357 state.selectedWindow = state.selectedWindow || 1; 7358 7359 // Fixes bug1954488 7360 // This solves a case where a user had open tab groups and then quit and 7361 // restarted the browser at least twice. In this case the saved groups 7362 // would still be marked as removeAfterRestore groups even though there was 7363 // no longer an open group associated with them in the lastSessionState. 7364 // To fix this we clear this property if we see it on saved groups, 7365 // converting them into permanently saved groups. 7366 for (let group of defaultState.savedGroups) { 7367 delete group.removeAfterRestore; 7368 } 7369 7370 // Look at each window, remove pinned tabs, adjust selectedindex, 7371 // remove window if necessary. 7372 for (let wIndex = 0; wIndex < state.windows.length; ) { 7373 let window = state.windows[wIndex]; 7374 window.selected = window.selected || 1; 7375 // We're going to put the state of the window into this object, but for closedTabs 7376 // we want to preserve the original closedTabs since that will be saved as the lastSessionState 7377 let newWindowState = { 7378 tabs: [], 7379 }; 7380 if (PERSIST_SESSIONS) { 7381 newWindowState._closedTabs = Cu.cloneInto(window._closedTabs, {}); 7382 newWindowState.closedGroups = Cu.cloneInto(window.closedGroups, {}); 7383 } 7384 7385 // We want to preserve the sidebar if previously open in the window 7386 if (window.sidebar) { 7387 newWindowState.sidebar = window.sidebar; 7388 } 7389 7390 let groupsToSave = new Map(); 7391 for (let tIndex = 0; tIndex < window.tabs.length; ) { 7392 if (window.tabs[tIndex].pinned) { 7393 // Adjust window.selected 7394 if (tIndex + 1 < window.selected) { 7395 window.selected -= 1; 7396 } else if (tIndex + 1 == window.selected) { 7397 newWindowState.selected = newWindowState.tabs.length + 1; 7398 } 7399 // + 1 because the tab isn't actually in the array yet 7400 7401 // Now add the pinned tab to our window 7402 newWindowState.tabs = newWindowState.tabs.concat( 7403 window.tabs.splice(tIndex, 1) 7404 ); 7405 // We don't want to increment tIndex here. 7406 continue; 7407 } else if (window.tabs[tIndex].groupId) { 7408 // Convert any open groups into saved groups. 7409 let groupStateToSave = window.groups.find( 7410 groupState => groupState.id == window.tabs[tIndex].groupId 7411 ); 7412 let groupToSave = groupsToSave.get(groupStateToSave.id); 7413 if (!groupToSave) { 7414 groupToSave = 7415 lazy.TabGroupState.savedInClosedWindow(groupStateToSave); 7416 // If the session is manually restored, these groups will be removed from the saved groups list 7417 // to prevent duplication. 7418 groupToSave.removeAfterRestore = true; 7419 groupsToSave.set(groupStateToSave.id, groupToSave); 7420 } 7421 let tabToAdd = window.tabs[tIndex]; 7422 groupToSave.tabs.push(this._formatTabStateForSavedGroup(tabToAdd)); 7423 } else if (!window.tabs[tIndex].hidden && PERSIST_SESSIONS) { 7424 // Add any previously open tabs that aren't pinned or hidden to the recently closed tabs list 7425 // which we want to persist between sessions; if the session is manually restored, they will 7426 // be filtered out of the closed tabs list (due to removeAfterRestore property) and reopened 7427 // per expected session restore behavior. 7428 7429 let tabState = window.tabs[tIndex]; 7430 7431 // Ensure the index is in bounds. 7432 let activeIndex = tabState.index; 7433 activeIndex = Math.min(activeIndex, tabState.entries.length - 1); 7434 activeIndex = Math.max(activeIndex, 0); 7435 7436 if (activeIndex in tabState.entries) { 7437 let title = 7438 tabState.entries[activeIndex].title || 7439 tabState.entries[activeIndex].url; 7440 7441 let tabData = { 7442 state: tabState, 7443 title, 7444 image: tabState.image, 7445 pos: tIndex, 7446 closedAt: Date.now(), 7447 closedInGroup: false, 7448 removeAfterRestore: true, 7449 }; 7450 7451 if (this._shouldSaveTabState(tabState)) { 7452 let closedTabsList = newWindowState._closedTabs; 7453 this.saveClosedTabData(window, closedTabsList, tabData, false); 7454 } 7455 } 7456 } 7457 tIndex++; 7458 } 7459 7460 // Any tab groups that were in the tab strip at the end of the last 7461 // session should be saved. If any tab groups were present in both 7462 // saved groups and open groups in the last session, set the saved 7463 // group's `removeAfterRestore` so that if the last session is restored, 7464 // the group will be opened to the tab strip and removed from the list 7465 // of saved tab groups. 7466 groupsToSave.forEach(groupState => { 7467 const alreadySavedGroup = defaultState.savedGroups.find( 7468 existingGroup => existingGroup.id == groupState.id 7469 ); 7470 if (alreadySavedGroup) { 7471 alreadySavedGroup.removeAfterRestore = true; 7472 } else { 7473 defaultState.savedGroups.push(groupState); 7474 } 7475 }); 7476 7477 hasPinnedTabs ||= !!newWindowState.tabs.length; 7478 7479 // Only transfer over window attributes for pinned tabs, which has 7480 // already been extracted into newWindowState.tabs. 7481 if (newWindowState.tabs.length) { 7482 WINDOW_ATTRIBUTES.forEach(function (attr) { 7483 if (attr in window) { 7484 newWindowState[attr] = window[attr]; 7485 delete window[attr]; 7486 } 7487 }); 7488 // We're just copying position data into the window for pinned tabs. 7489 // Not copying over: 7490 // - extData 7491 // - isPopup 7492 // - hidden 7493 7494 // Assign a unique ID to correlate the window to be opened with the 7495 // remaining data 7496 window.__lastSessionWindowID = newWindowState.__lastSessionWindowID = 7497 "" + Date.now() + Math.random(); 7498 } 7499 7500 // If this newWindowState contains pinned tabs (stored in tabs) or 7501 // closed tabs, add it to the defaultState so they're available immediately. 7502 if ( 7503 newWindowState.tabs.length || 7504 (PERSIST_SESSIONS && 7505 (newWindowState._closedTabs.length || 7506 newWindowState.closedGroups.length)) 7507 ) { 7508 defaultState.windows.push(newWindowState); 7509 // Remove the window from the state if it doesn't have any tabs 7510 if (!window.tabs.length) { 7511 if (wIndex + 1 <= state.selectedWindow) { 7512 state.selectedWindow -= 1; 7513 } else if (wIndex + 1 == state.selectedWindow) { 7514 defaultState.selectedIndex = defaultState.windows.length + 1; 7515 } 7516 7517 state.windows.splice(wIndex, 1); 7518 // We don't want to increment wIndex here. 7519 continue; 7520 } 7521 } 7522 wIndex++; 7523 } 7524 7525 if (hasPinnedTabs) { 7526 // Move cookies over from so that they're restored right away and pinned tabs will load correctly. 7527 defaultState.cookies = state.cookies; 7528 delete state.cookies; 7529 } 7530 // we return state here rather than startupState so as to avoid duplicating 7531 // pinned tabs that we add to the defaultState (when a user restores a session) 7532 return [defaultState, state]; 7533 }, 7534 7535 _sendRestoreCompletedNotifications: 7536 function ssi_sendRestoreCompletedNotifications() { 7537 // not all windows restored, yet 7538 if (this._restoreCount > 1) { 7539 this._restoreCount--; 7540 this._log.warn( 7541 `waiting on ${this._restoreCount} windows to be restored before sending restore complete notifications.` 7542 ); 7543 return; 7544 } 7545 7546 // observers were already notified 7547 if (this._restoreCount == -1) { 7548 return; 7549 } 7550 7551 // This was the last window restored at startup, notify observers. 7552 if (!this._browserSetState) { 7553 Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED); 7554 this._log.debug(`All ${this._restoreCount} windows restored`); 7555 this._deferredAllWindowsRestored.resolve(); 7556 } else { 7557 // _browserSetState is used only by tests, and it uses an alternate 7558 // notification in order not to retrigger startup observers that 7559 // are listening for NOTIFY_WINDOWS_RESTORED. 7560 Services.obs.notifyObservers(null, NOTIFY_BROWSER_STATE_RESTORED); 7561 } 7562 7563 // If all windows are on other virtual desktops (on Windows), open a new 7564 // window on this desktop so the user isn't left wondering where their 7565 // session went. See bug 1812489. 7566 let anyWindowNotCloaked = this._browserWindows[Symbol.iterator]().some( 7567 window => !window.isCloaked 7568 ); 7569 if (!anyWindowNotCloaked) { 7570 lazy.BrowserWindowTracker.openWindow(); 7571 } 7572 7573 this._browserSetState = false; 7574 this._restoreCount = -1; 7575 }, 7576 7577 /** 7578 * Set the given window's busy state 7579 * 7580 * @param aWindow the window 7581 * @param aValue the window's busy state 7582 */ 7583 _setWindowStateBusyValue: function ssi_changeWindowStateBusyValue( 7584 aWindow, 7585 aValue 7586 ) { 7587 this._windows[aWindow.__SSi].busy = aValue; 7588 7589 // Keep the to-be-restored state in sync because that is returned by 7590 // getWindowState() as long as the window isn't loaded, yet. 7591 if (!this._isWindowLoaded(aWindow)) { 7592 let stateToRestore = 7593 this._statesToRestore[WINDOW_RESTORE_IDS.get(aWindow)].windows[0]; 7594 stateToRestore.busy = aValue; 7595 } 7596 }, 7597 7598 /** 7599 * Set the given window's state to 'not busy'. 7600 * 7601 * @param aWindow the window 7602 */ 7603 _setWindowStateReady: function ssi_setWindowStateReady(aWindow) { 7604 let newCount = (this._windowBusyStates.get(aWindow) || 0) - 1; 7605 if (newCount < 0) { 7606 throw new Error("Invalid window busy state (less than zero)."); 7607 } 7608 this._windowBusyStates.set(aWindow, newCount); 7609 7610 if (newCount == 0) { 7611 this._setWindowStateBusyValue(aWindow, false); 7612 this._sendWindowStateReadyEvent(aWindow); 7613 } 7614 }, 7615 7616 /** 7617 * Set the given window's state to 'busy'. 7618 * 7619 * @param aWindow the window 7620 */ 7621 _setWindowStateBusy: function ssi_setWindowStateBusy(aWindow) { 7622 let newCount = (this._windowBusyStates.get(aWindow) || 0) + 1; 7623 this._windowBusyStates.set(aWindow, newCount); 7624 7625 if (newCount == 1) { 7626 this._setWindowStateBusyValue(aWindow, true); 7627 this._sendWindowStateBusyEvent(aWindow); 7628 } 7629 }, 7630 7631 /** 7632 * Dispatch an SSWindowStateReady event for the given window. 7633 * 7634 * @param aWindow the window 7635 */ 7636 _sendWindowStateReadyEvent: function ssi_sendWindowStateReadyEvent(aWindow) { 7637 let event = aWindow.document.createEvent("Events"); 7638 event.initEvent("SSWindowStateReady", true, false); 7639 aWindow.dispatchEvent(event); 7640 }, 7641 7642 /** 7643 * Dispatch an SSWindowStateBusy event for the given window. 7644 * 7645 * @param aWindow the window 7646 */ 7647 _sendWindowStateBusyEvent: function ssi_sendWindowStateBusyEvent(aWindow) { 7648 let event = aWindow.document.createEvent("Events"); 7649 event.initEvent("SSWindowStateBusy", true, false); 7650 aWindow.dispatchEvent(event); 7651 }, 7652 7653 /** 7654 * Dispatch the SSWindowRestoring event for the given window. 7655 * 7656 * @param aWindow 7657 * The window which is going to be restored 7658 */ 7659 _sendWindowRestoringNotification(aWindow) { 7660 let event = aWindow.document.createEvent("Events"); 7661 event.initEvent("SSWindowRestoring", true, false); 7662 aWindow.dispatchEvent(event); 7663 }, 7664 7665 /** 7666 * Dispatch the SSWindowRestored event for the given window. 7667 * 7668 * @param aWindow 7669 * The window which has been restored 7670 */ 7671 _sendWindowRestoredNotification(aWindow) { 7672 let event = aWindow.document.createEvent("Events"); 7673 event.initEvent("SSWindowRestored", true, false); 7674 aWindow.dispatchEvent(event); 7675 }, 7676 7677 /** 7678 * Dispatch the SSTabRestored event for the given tab. 7679 * 7680 * @param aTab 7681 * The tab which has been restored 7682 * @param aIsRemotenessUpdate 7683 * True if this tab was restored due to flip from running from 7684 * out-of-main-process to in-main-process or vice-versa. 7685 */ 7686 _sendTabRestoredNotification(aTab, aIsRemotenessUpdate) { 7687 let event = aTab.ownerDocument.createEvent("CustomEvent"); 7688 event.initCustomEvent("SSTabRestored", true, false, { 7689 isRemotenessUpdate: aIsRemotenessUpdate, 7690 }); 7691 aTab.dispatchEvent(event); 7692 }, 7693 7694 /** 7695 * @param aWindow 7696 * Window reference 7697 * @returns whether this window's data is still cached in _statesToRestore 7698 * because it's not fully loaded yet 7699 */ 7700 _isWindowLoaded: function ssi_isWindowLoaded(aWindow) { 7701 return !WINDOW_RESTORE_IDS.has(aWindow); 7702 }, 7703 7704 /** 7705 * Resize this._closedWindows to the value of the pref, except in the case 7706 * where we don't have any non-popup windows on Windows and Linux. Then we must 7707 * resize such that we have at least one non-popup window. 7708 */ 7709 _capClosedWindows: function ssi_capClosedWindows() { 7710 if (this._closedWindows.length <= this._max_windows_undo) { 7711 return; 7712 } 7713 let spliceTo = this._max_windows_undo; 7714 if (AppConstants.platform != "macosx") { 7715 let normalWindowIndex = 0; 7716 // try to find a non-popup window in this._closedWindows 7717 while ( 7718 normalWindowIndex < this._closedWindows.length && 7719 !!this._closedWindows[normalWindowIndex].isPopup 7720 ) { 7721 normalWindowIndex++; 7722 } 7723 if (normalWindowIndex >= this._max_windows_undo) { 7724 spliceTo = normalWindowIndex + 1; 7725 } 7726 } 7727 if (spliceTo < this._closedWindows.length) { 7728 this._closedWindows.splice(spliceTo, this._closedWindows.length); 7729 this._closedObjectsChanged = true; 7730 } 7731 }, 7732 7733 /** 7734 * Clears the set of windows that are "resurrected" before writing to disk to 7735 * make closing windows one after the other until shutdown work as expected. 7736 * 7737 * This function should only be called when we are sure that there has been 7738 * a user action that indicates the browser is actively being used and all 7739 * windows that have been closed before are not part of a series of closing 7740 * windows. 7741 */ 7742 _clearRestoringWindows: function ssi_clearRestoringWindows() { 7743 for (let i = 0; i < this._closedWindows.length; i++) { 7744 delete this._closedWindows[i]._shouldRestore; 7745 } 7746 }, 7747 7748 /** 7749 * Reset state to prepare for a new session state to be restored. 7750 */ 7751 _resetRestoringState: function ssi_initRestoringState() { 7752 TabRestoreQueue.reset(); 7753 this._tabsRestoringCount = 0; 7754 }, 7755 7756 /** 7757 * Reset the restoring state for a particular tab. This will be called when 7758 * removing a tab or when a tab needs to be reset (it's being overwritten). 7759 * 7760 * @param aTab 7761 * The tab that will be "reset" 7762 */ 7763 _resetLocalTabRestoringState(aTab) { 7764 let browser = aTab.linkedBrowser; 7765 7766 // Keep the tab's previous state for later in this method 7767 let previousState = TAB_STATE_FOR_BROWSER.get(browser); 7768 7769 if (!previousState) { 7770 console.error("Given tab is not restoring."); 7771 return; 7772 } 7773 7774 // The browser is no longer in any sort of restoring state. 7775 TAB_STATE_FOR_BROWSER.delete(browser); 7776 7777 this._restoreListeners.get(browser.permanentKey)?.unregister(); 7778 browser.browsingContext.clearRestoreState(); 7779 7780 aTab.removeAttribute("pending"); 7781 aTab.removeAttribute("discarded"); 7782 7783 if (previousState == TAB_STATE_RESTORING) { 7784 if (this._tabsRestoringCount) { 7785 this._tabsRestoringCount--; 7786 } 7787 } else if (previousState == TAB_STATE_NEEDS_RESTORE) { 7788 // Make sure that the tab is removed from the list of tabs to restore. 7789 // Again, this is normally done in restoreTabContent, but that isn't being called 7790 // for this tab. 7791 TabRestoreQueue.remove(aTab); 7792 } 7793 }, 7794 7795 _resetTabRestoringState(tab) { 7796 let browser = tab.linkedBrowser; 7797 7798 if (!TAB_STATE_FOR_BROWSER.has(browser)) { 7799 console.error("Given tab is not restoring."); 7800 return; 7801 } 7802 7803 this._resetLocalTabRestoringState(tab); 7804 }, 7805 7806 /** 7807 * Each fresh tab starts out with epoch=0. This function can be used to 7808 * start a next epoch by incrementing the current value. It will enables us 7809 * to ignore stale messages sent from previous epochs. The function returns 7810 * the new epoch ID for the given |browser|. 7811 */ 7812 startNextEpoch(permanentKey) { 7813 let next = this.getCurrentEpoch(permanentKey) + 1; 7814 this._browserEpochs.set(permanentKey, next); 7815 return next; 7816 }, 7817 7818 /** 7819 * Returns the current epoch for the given <browser>. If we haven't assigned 7820 * a new epoch this will default to zero for new tabs. 7821 */ 7822 getCurrentEpoch(permanentKey) { 7823 return this._browserEpochs.get(permanentKey) || 0; 7824 }, 7825 7826 /** 7827 * Each time a <browser> element is restored, we increment its "epoch". To 7828 * check if a message from content-sessionStore.js is out of date, we can 7829 * compare the epoch received with the message to the <browser> element's 7830 * epoch. This function does that, and returns true if |epoch| is up-to-date 7831 * with respect to |browser|. 7832 */ 7833 isCurrentEpoch(permanentKey, epoch) { 7834 return this.getCurrentEpoch(permanentKey) == epoch; 7835 }, 7836 7837 /** 7838 * Resets the epoch for a given <browser>. We need to this every time we 7839 * receive a hint that a new docShell has been loaded into the browser as 7840 * the frame script starts out with epoch=0. 7841 */ 7842 resetEpoch(permanentKey, frameLoader = null) { 7843 this._browserEpochs.delete(permanentKey); 7844 if (frameLoader) { 7845 frameLoader.requestEpochUpdate(0); 7846 } 7847 }, 7848 7849 /** 7850 * Countdown for a given duration, skipping beats if the computer is too busy, 7851 * sleeping or otherwise unavailable. 7852 * 7853 * @param {number} delay An approximate delay to wait in milliseconds (rounded 7854 * up to the closest second). 7855 * 7856 * @return Promise 7857 */ 7858 looseTimer(delay) { 7859 let DELAY_BEAT = 1000; 7860 let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); 7861 let beats = Math.ceil(delay / DELAY_BEAT); 7862 let deferred = Promise.withResolvers(); 7863 timer.initWithCallback( 7864 function () { 7865 if (beats <= 0) { 7866 this._log.debug(`looseTimer of ${delay} timed out`); 7867 Glean.sessionRestore.shutdownFlushAllOutcomes.timed_out.add(1); 7868 deferred.resolve(); 7869 } 7870 --beats; 7871 }, 7872 DELAY_BEAT, 7873 Ci.nsITimer.TYPE_REPEATING_PRECISE_CAN_SKIP 7874 ); 7875 // Ensure that the timer is both canceled once we are done with it 7876 // and not garbage-collected until then. 7877 deferred.promise.then( 7878 () => timer.cancel(), 7879 () => timer.cancel() 7880 ); 7881 return deferred; 7882 }, 7883 7884 _waitForStateStop(browser, expectedURL = null) { 7885 const deferred = Promise.withResolvers(); 7886 7887 const listener = { 7888 unregister(reject = true) { 7889 if (reject) { 7890 deferred.reject(); 7891 } 7892 7893 SessionStoreInternal._restoreListeners.delete(browser.permanentKey); 7894 7895 try { 7896 browser.removeProgressListener( 7897 this, 7898 Ci.nsIWebProgress.NOTIFY_STATE_WINDOW 7899 ); 7900 } catch {} // May have already gotten rid of the browser's webProgress. 7901 }, 7902 7903 onStateChange(webProgress, request, stateFlags) { 7904 if ( 7905 webProgress.isTopLevel && 7906 stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW && 7907 stateFlags & Ci.nsIWebProgressListener.STATE_STOP 7908 ) { 7909 // FIXME: We sometimes see spurious STATE_STOP events for about:blank 7910 // loads, so we have to account for that here. 7911 let aboutBlankOK = !expectedURL || expectedURL === "about:blank"; 7912 let url = request.QueryInterface(Ci.nsIChannel).originalURI.spec; 7913 if (url !== "about:blank" || aboutBlankOK) { 7914 this.unregister(false); 7915 deferred.resolve(); 7916 } 7917 } 7918 }, 7919 7920 QueryInterface: ChromeUtils.generateQI([ 7921 "nsIWebProgressListener", 7922 "nsISupportsWeakReference", 7923 ]), 7924 }; 7925 7926 this._restoreListeners.get(browser.permanentKey)?.unregister(); 7927 this._restoreListeners.set(browser.permanentKey, listener); 7928 7929 browser.addProgressListener( 7930 listener, 7931 Ci.nsIWebProgress.NOTIFY_STATE_WINDOW 7932 ); 7933 7934 return deferred.promise; 7935 }, 7936 7937 _listenForNavigations(browser, callbacks) { 7938 const listener = { 7939 unregister() { 7940 browser.browsingContext?.sessionHistory?.removeSHistoryListener(this); 7941 7942 try { 7943 browser.removeProgressListener( 7944 this, 7945 Ci.nsIWebProgress.NOTIFY_STATE_WINDOW 7946 ); 7947 } catch {} // May have already gotten rid of the browser's webProgress. 7948 7949 SessionStoreInternal._restoreListeners.delete(browser.permanentKey); 7950 }, 7951 7952 OnHistoryReload() { 7953 this.unregister(); 7954 return callbacks.onHistoryReload(); 7955 }, 7956 7957 // TODO(kashav): ContentRestore.sys.mjs handles OnHistoryNewEntry 7958 // separately, so we should eventually support that here as well. 7959 OnHistoryNewEntry() {}, 7960 OnHistoryGotoIndex() {}, 7961 OnHistoryPurge() {}, 7962 OnHistoryReplaceEntry() {}, 7963 7964 onStateChange(webProgress, request, stateFlags) { 7965 if ( 7966 webProgress.isTopLevel && 7967 stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW && 7968 stateFlags & Ci.nsIWebProgressListener.STATE_START 7969 ) { 7970 this.unregister(); 7971 callbacks.onStartRequest(); 7972 } 7973 }, 7974 7975 QueryInterface: ChromeUtils.generateQI([ 7976 "nsISHistoryListener", 7977 "nsIWebProgressListener", 7978 "nsISupportsWeakReference", 7979 ]), 7980 }; 7981 7982 this._restoreListeners.get(browser.permanentKey)?.unregister(); 7983 this._restoreListeners.set(browser.permanentKey, listener); 7984 7985 browser.browsingContext?.sessionHistory?.addSHistoryListener(listener); 7986 7987 browser.addProgressListener( 7988 listener, 7989 Ci.nsIWebProgress.NOTIFY_STATE_WINDOW 7990 ); 7991 }, 7992 7993 /** 7994 * This mirrors ContentRestore.restoreHistory() for parent process session 7995 * history restores. 7996 */ 7997 _restoreHistory(browser, data) { 7998 this._tabStateToRestore.set(browser.permanentKey, data); 7999 8000 // In case about:blank isn't done yet. 8001 // XXX(kashav): Does this actually accomplish anything? Can we remove? 8002 browser.stop(); 8003 8004 lazy.SessionHistory.restoreFromParent( 8005 browser.browsingContext.sessionHistory, 8006 data.tabData 8007 ); 8008 8009 let url = data.tabData?.entries[data.tabData.index - 1]?.url; 8010 let disallow = data.tabData?.disallow; 8011 8012 let promise = SessionStoreUtils.restoreDocShellState( 8013 browser.browsingContext, 8014 url, 8015 disallow 8016 ); 8017 this._tabStateRestorePromises.set(browser.permanentKey, promise); 8018 8019 const onResolve = () => { 8020 if (TAB_STATE_FOR_BROWSER.get(browser) !== TAB_STATE_RESTORING) { 8021 this._listenForNavigations(browser, { 8022 // The history entry was reloaded before we began restoring tab 8023 // content, just proceed as we would normally. 8024 onHistoryReload: () => { 8025 this._restoreTabContent(browser); 8026 return false; 8027 }, 8028 8029 // Some foreign code, like an extension, loaded a new URI on the 8030 // browser. We no longer want to restore saved tab data, but may 8031 // still have browser state that needs to be restored. 8032 onStartRequest: () => { 8033 this._tabStateToRestore.delete(browser.permanentKey); 8034 this._restoreTabContent(browser); 8035 }, 8036 }); 8037 } 8038 8039 this._tabStateRestorePromises.delete(browser.permanentKey); 8040 8041 this._restoreHistoryComplete(browser); 8042 }; 8043 8044 promise.then(onResolve).catch(() => {}); 8045 }, 8046 8047 /** 8048 * Either load the saved typed value or restore the active history entry. 8049 * If neither is possible, just load an empty document. 8050 */ 8051 _restoreTabEntry(browser, tabData) { 8052 let haveUserTypedValue = tabData.userTypedValue && tabData.userTypedClear; 8053 // First take care of the common case where we load the history entry. 8054 if (!haveUserTypedValue && tabData.entries.length) { 8055 return SessionStoreUtils.initializeRestore( 8056 browser.browsingContext, 8057 lazy.SessionStoreHelper.buildRestoreData( 8058 tabData.formdata, 8059 tabData.scroll 8060 ) 8061 ); 8062 } 8063 // Here, we need to load user data or about:blank instead. 8064 // As it's user-typed (or blank), it gets system triggering principal: 8065 let triggeringPrincipal = 8066 Services.scriptSecurityManager.getSystemPrincipal(); 8067 // Bypass all the fixup goop for about:blank: 8068 if (!haveUserTypedValue) { 8069 let blankPromise = this._waitForStateStop(browser, "about:blank"); 8070 browser.browsingContext.loadURI(lazy.blankURI, { 8071 triggeringPrincipal, 8072 loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY, 8073 }); 8074 return blankPromise; 8075 } 8076 8077 // We have a user typed value, load that with fixup: 8078 let loadPromise = this._waitForStateStop(browser, tabData.userTypedValue); 8079 browser.browsingContext.fixupAndLoadURIString(tabData.userTypedValue, { 8080 loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP, 8081 triggeringPrincipal, 8082 }); 8083 8084 return loadPromise; 8085 }, 8086 8087 /** 8088 * This mirrors ContentRestore.restoreTabContent() for parent process session 8089 * history restores. 8090 */ 8091 _restoreTabContent(browser, options = {}) { 8092 this._restoreListeners.get(browser.permanentKey)?.unregister(); 8093 8094 this._restoreTabContentStarted(browser, options); 8095 8096 let state = this._tabStateToRestore.get(browser.permanentKey); 8097 this._tabStateToRestore.delete(browser.permanentKey); 8098 8099 let promises = [this._tabStateRestorePromises.get(browser.permanentKey)]; 8100 8101 if (state) { 8102 promises.push(this._restoreTabEntry(browser, state.tabData)); 8103 } else { 8104 // The browser started another load, so we decided to not restore 8105 // saved tab data. We should still wait for that new load to finish 8106 // before proceeding. 8107 promises.push(this._waitForStateStop(browser)); 8108 } 8109 8110 Promise.allSettled(promises).then(() => { 8111 this._restoreTabContentComplete(browser, options); 8112 }); 8113 }, 8114 8115 _sendRestoreTabContent(browser, options) { 8116 this._restoreTabContent(browser, options); 8117 }, 8118 8119 _restoreHistoryComplete(browser) { 8120 let win = browser.ownerGlobal; 8121 let tab = win?.gBrowser.getTabForBrowser(browser); 8122 if (!tab) { 8123 return; 8124 } 8125 8126 // Notify the tabbrowser that the tab chrome has been restored. 8127 let tabData = lazy.TabState.collect(tab, TAB_CUSTOM_VALUES.get(tab)); 8128 8129 // Update tab label and icon again after the tab history was updated. 8130 this.updateTabLabelAndIcon(tab, tabData); 8131 8132 let event = win.document.createEvent("Events"); 8133 event.initEvent("SSTabRestoring", true, false); 8134 tab.dispatchEvent(event); 8135 }, 8136 8137 _restoreTabContentStarted(browser, data) { 8138 let win = browser.ownerGlobal; 8139 let tab = win?.gBrowser.getTabForBrowser(browser); 8140 if (!tab) { 8141 return; 8142 } 8143 8144 let initiatedBySessionStore = 8145 TAB_STATE_FOR_BROWSER.get(browser) != TAB_STATE_NEEDS_RESTORE; 8146 let isNavigateAndRestore = 8147 data.reason == RESTORE_TAB_CONTENT_REASON.NAVIGATE_AND_RESTORE; 8148 8149 // We need to be careful when restoring the urlbar's search mode because 8150 // we race a call to gURLBar.setURI due to the location change. setURI 8151 // will exit search mode and set gURLBar.value to the restored URL, 8152 // clobbering any search mode and userTypedValue we restore here. If 8153 // this is a typical restore -- restoring on startup or restoring a 8154 // closed tab for example -- then we need to restore search mode after 8155 // that setURI call, and so we wait until restoreTabContentComplete, at 8156 // which point setURI will have been called. If this is not a typical 8157 // restore -- it was not initiated by session store or it's due to a 8158 // remoteness change -- then we do not want to restore search mode at 8159 // all, and so we remove it from the tab state cache. In particular, if 8160 // the restore is due to a remoteness change, then the user is loading a 8161 // new URL and the current search mode should not be carried over to it. 8162 let cacheState = lazy.TabStateCache.get(browser.permanentKey); 8163 if (cacheState.searchMode) { 8164 if (!initiatedBySessionStore || isNavigateAndRestore) { 8165 lazy.TabStateCache.update(browser.permanentKey, { 8166 searchMode: null, 8167 userTypedValue: null, 8168 }); 8169 } 8170 return; 8171 } 8172 8173 if (!initiatedBySessionStore) { 8174 // If a load not initiated by sessionstore was started in a 8175 // previously pending tab. Mark the tab as no longer pending. 8176 this.markTabAsRestoring(tab); 8177 } else if (!isNavigateAndRestore) { 8178 // If the user was typing into the URL bar when we crashed, but hadn't hit 8179 // enter yet, then we just need to write that value to the URL bar without 8180 // loading anything. This must happen after the load, as the load will clear 8181 // userTypedValue. 8182 // 8183 // Note that we only want to do that if we're restoring state for reasons 8184 // _other_ than a navigateAndRestore remoteness-flip, as such a flip 8185 // implies that the user was navigating. 8186 let tabData = lazy.TabState.collect(tab, TAB_CUSTOM_VALUES.get(tab)); 8187 if ( 8188 tabData.userTypedValue && 8189 !tabData.userTypedClear && 8190 !browser.userTypedValue 8191 ) { 8192 browser.userTypedValue = tabData.userTypedValue; 8193 if (tab.selected) { 8194 win.gURLBar.setURI(); 8195 } 8196 } 8197 8198 // Remove state we don't need any longer. 8199 lazy.TabStateCache.update(browser.permanentKey, { 8200 userTypedValue: null, 8201 userTypedClear: null, 8202 }); 8203 } 8204 }, 8205 8206 _restoreTabContentComplete(browser, data) { 8207 let win = browser.ownerGlobal; 8208 let tab = browser.ownerGlobal?.gBrowser.getTabForBrowser(browser); 8209 if (!tab) { 8210 return; 8211 } 8212 // Restore search mode and its search string in userTypedValue, if 8213 // appropriate. 8214 let cacheState = lazy.TabStateCache.get(browser.permanentKey); 8215 if (cacheState.searchMode) { 8216 win.gURLBar.setSearchMode(cacheState.searchMode, browser); 8217 browser.userTypedValue = cacheState.userTypedValue; 8218 if (tab.selected) { 8219 win.gURLBar.setURI(); 8220 } 8221 lazy.TabStateCache.update(browser.permanentKey, { 8222 searchMode: null, 8223 userTypedValue: null, 8224 }); 8225 } 8226 8227 // This callback is used exclusively by tests that want to 8228 // monitor the progress of network loads. 8229 if (gDebuggingEnabled) { 8230 Services.obs.notifyObservers(browser, NOTIFY_TAB_RESTORED); 8231 } 8232 8233 SessionStoreInternal._resetLocalTabRestoringState(tab); 8234 SessionStoreInternal.restoreNextTab(); 8235 8236 this._sendTabRestoredNotification(tab, data.isRemotenessUpdate); 8237 8238 Services.obs.notifyObservers(null, "sessionstore-one-or-no-tab-restored"); 8239 }, 8240 8241 /** 8242 * Send the "SessionStore:restoreHistory" message to content, triggering a 8243 * content restore. This method is intended to be used internally by 8244 * SessionStore, as it also ensures that permissions are avaliable in the 8245 * content process before triggering the history restore in the content 8246 * process. 8247 * 8248 * @param browser The browser to transmit the permissions for 8249 * @param options The options data to send to content. 8250 */ 8251 _sendRestoreHistory(browser, options) { 8252 if (options.tabData.storage) { 8253 SessionStoreUtils.restoreSessionStorageFromParent( 8254 browser.browsingContext, 8255 options.tabData.storage 8256 ); 8257 delete options.tabData.storage; 8258 } 8259 8260 this._restoreHistory(browser, options); 8261 8262 if (browser && browser.frameLoader) { 8263 browser.frameLoader.requestEpochUpdate(options.epoch); 8264 } 8265 }, 8266 8267 /** 8268 * @param {MozTabbrowserTabGroup} tabGroup 8269 */ 8270 addSavedTabGroup(tabGroup) { 8271 if (PrivateBrowsingUtils.isWindowPrivate(tabGroup.ownerGlobal)) { 8272 throw new Error("Refusing to save tab group from private window"); 8273 } 8274 8275 let tabGroupState = lazy.TabGroupState.savedInOpenWindow( 8276 tabGroup, 8277 tabGroup.ownerGlobal.__SSi 8278 ); 8279 tabGroupState.tabs = this._collectClosedTabsForTabGroup( 8280 tabGroup.tabs, 8281 tabGroup.ownerGlobal 8282 ); 8283 this._recordSavedTabGroupState(tabGroupState); 8284 }, 8285 8286 /** 8287 * @param {string} tabGroupId 8288 * @param {MozTabbrowserTab[]} tabs 8289 * @param {TabMetricsContext} [metricsContext] 8290 * @returns {SavedTabGroupStateData} 8291 */ 8292 addTabsToSavedGroup(tabGroupId, tabs, metricsContext) { 8293 let tabGroupState = this.getSavedTabGroup(tabGroupId); 8294 if (!tabGroupState) { 8295 throw new Error(`No tab group found with id ${tabGroupId}`); 8296 } 8297 8298 const win = tabs[0].ownerGlobal; 8299 if (!tabs.every(tab => tab.ownerGlobal === win)) { 8300 throw new Error(`All tabs must be part of the same window`); 8301 } 8302 8303 if (PrivateBrowsingUtils.isWindowPrivate(win)) { 8304 throw new Error( 8305 "Refusing to add tabs from private window to a saved tab group" 8306 ); 8307 } 8308 8309 let newTabState = this._collectClosedTabsForTabGroup(tabs, win, { 8310 updateTabGroupId: tabGroupId, 8311 }); 8312 tabGroupState.tabs.push(...newTabState); 8313 8314 let isVerticalMode = win.gBrowser.tabContainer.verticalMode; 8315 Glean.tabgroup.addTab.record({ 8316 source: 8317 metricsContext?.telemetrySource || TabMetrics.METRIC_SOURCE.UNKNOWN, 8318 tabs: tabs.length, 8319 layout: isVerticalMode 8320 ? TabMetrics.METRIC_TABS_LAYOUT.VERTICAL 8321 : TabMetrics.METRIC_TABS_LAYOUT.HORIZONTAL, 8322 group_type: TabMetrics.METRIC_GROUP_TYPE.SAVED, 8323 }); 8324 8325 this._notifyOfSavedTabGroupsChange(); 8326 return tabGroupState; 8327 }, 8328 8329 /** 8330 * @param {SavedTabGroupStateData} savedTabGroupState 8331 * @returns {void} 8332 */ 8333 _recordSavedTabGroupState(savedTabGroupState) { 8334 if ( 8335 !savedTabGroupState.tabs.length || 8336 this.getSavedTabGroup(savedTabGroupState.id) 8337 ) { 8338 return; 8339 } 8340 this._savedGroups.push(savedTabGroupState); 8341 this._notifyOfSavedTabGroupsChange(); 8342 }, 8343 8344 /** 8345 * @param {string} tabGroupId 8346 * @returns {SavedTabGroupStateData|undefined} 8347 */ 8348 getSavedTabGroup(tabGroupId) { 8349 return this._savedGroups.find( 8350 savedTabGroup => savedTabGroup.id == tabGroupId 8351 ); 8352 }, 8353 8354 /** 8355 * Returns all tab groups that were saved in this session. 8356 * 8357 * @returns {SavedTabGroupStateData[]} 8358 */ 8359 getSavedTabGroups() { 8360 return Cu.cloneInto(this._savedGroups, {}); 8361 }, 8362 8363 /** 8364 * @param {Window|{sourceWindowId: string}|{sourceClosedId: number}} source 8365 * @param {string} tabGroupId 8366 * @returns {ClosedTabGroupStateData|undefined} 8367 */ 8368 getClosedTabGroup(source, tabGroupId) { 8369 let winData = this._resolveClosedDataSource(source); 8370 return winData?.closedGroups.find( 8371 closedGroup => closedGroup.id == tabGroupId 8372 ); 8373 }, 8374 8375 /** 8376 * @param {Window | object} source 8377 * @param {string} tabGroupId 8378 * @param {Window} [targetWindow] 8379 * @returns {MozTabbrowserTabGroup} 8380 */ 8381 undoCloseTabGroup(source, tabGroupId, targetWindow) { 8382 const sourceWinData = this._resolveClosedDataSource(source); 8383 const isPrivateSource = Boolean(sourceWinData.isPrivate); 8384 if (targetWindow && !targetWindow.__SSi) { 8385 throw Components.Exception( 8386 "Target window is not tracked", 8387 Cr.NS_ERROR_INVALID_ARG 8388 ); 8389 } else if (!targetWindow) { 8390 targetWindow = this._getTopWindow(isPrivateSource); 8391 } 8392 if ( 8393 isPrivateSource !== PrivateBrowsingUtils.isWindowPrivate(targetWindow) 8394 ) { 8395 throw Components.Exception( 8396 "Target window doesn't have the same privateness as the source window", 8397 Cr.NS_ERROR_INVALID_ARG 8398 ); 8399 } 8400 8401 let tabGroupData = this.getClosedTabGroup(source, tabGroupId); 8402 if (!tabGroupData) { 8403 throw Components.Exception( 8404 "Tab group not found in source", 8405 Cr.NS_ERROR_INVALID_ARG 8406 ); 8407 } 8408 8409 let group = this._createTabsForSavedOrClosedTabGroup( 8410 tabGroupData, 8411 targetWindow 8412 ); 8413 this.forgetClosedTabGroup(source, tabGroupId); 8414 sourceWinData.lastClosedTabGroupId = null; 8415 8416 Glean.tabgroup.groupInteractions.open_recent.add(1); 8417 8418 let isVerticalMode = targetWindow.gBrowser.tabContainer.verticalMode; 8419 Glean.tabgroup.reopen.record({ 8420 id: tabGroupId, 8421 source: TabMetrics.METRIC_SOURCE.RECENT_TABS, 8422 type: TabMetrics.METRIC_REOPEN_TYPE.DELETED, 8423 layout: isVerticalMode 8424 ? TabMetrics.METRIC_TABS_LAYOUT.VERTICAL 8425 : TabMetrics.METRIC_TABS_LAYOUT.HORIZONTAL, 8426 }); 8427 8428 group.select(); 8429 return group; 8430 }, 8431 8432 /** 8433 * @param {string} tabGroupId 8434 * @param {Window} [targetWindow] 8435 * @returns {MozTabbrowserTabGroup} 8436 */ 8437 openSavedTabGroup(tabGroupId, targetWindow) { 8438 if (!targetWindow) { 8439 targetWindow = this._getTopWindow(); 8440 } 8441 if (!targetWindow.__SSi) { 8442 throw Components.Exception( 8443 "Target window is not tracked", 8444 Cr.NS_ERROR_INVALID_ARG 8445 ); 8446 } 8447 if (PrivateBrowsingUtils.isWindowPrivate(targetWindow)) { 8448 throw Components.Exception( 8449 "Cannot open a saved tab group in a private window", 8450 Cr.NS_ERROR_INVALID_ARG 8451 ); 8452 } 8453 8454 let tabGroupData = this.getSavedTabGroup(tabGroupId); 8455 if (!tabGroupData) { 8456 throw Components.Exception( 8457 "No saved tab group with specified id", 8458 Cr.NS_ERROR_INVALID_ARG 8459 ); 8460 } 8461 8462 // If this saved tab group is present in a closed window, then we need to 8463 // remove references to this saved tab group from that closed window. The 8464 // result should be as if the saved tab group "moved" from the closed window 8465 // into the `targetWindow`. 8466 if (tabGroupData.windowClosedId) { 8467 let closedWinData = this.getClosedWindowDataByClosedId( 8468 tabGroupData.windowClosedId 8469 ); 8470 if (closedWinData) { 8471 this._removeSavedTabGroupFromClosedWindow( 8472 closedWinData, 8473 tabGroupData.id 8474 ); 8475 } 8476 } 8477 8478 let group = this._createTabsForSavedOrClosedTabGroup( 8479 tabGroupData, 8480 targetWindow 8481 ); 8482 this.forgetSavedTabGroup(tabGroupId); 8483 8484 group.select(); 8485 return group; 8486 }, 8487 8488 /** 8489 * @param {ClosedTabGroupStateData|SavedTabGroupStateData} tabGroupData 8490 * @param {Window} targetWindow 8491 * @returns {MozTabbrowserTabGroup} 8492 */ 8493 _createTabsForSavedOrClosedTabGroup(tabGroupData, targetWindow) { 8494 let tabDataList = tabGroupData.tabs.map(tab => tab.state); 8495 let tabs = targetWindow.gBrowser.createTabsForSessionRestore( 8496 true, 8497 0, // TODO Bug 1933113 - Save tab group position and selected tab with saved tab group data 8498 tabDataList, 8499 [tabGroupData] 8500 ); 8501 8502 this.restoreTabs(targetWindow, tabs, tabDataList, 0); 8503 return tabs[0].group; 8504 }, 8505 8506 /** 8507 * Remove tab groups from the closedGroups list that have no tabs associated 8508 * with them. 8509 * 8510 * This can sometimes happen because tab groups are immediately 8511 * added to closedGroups on closing, before the complete history of the tabs 8512 * within the group have been processed. If it is later determined that none 8513 * of the tabs in the group were "worth saving", the group will be empty. 8514 * This can also happen if a user "undoes" the last closed tab in a closed tab 8515 * group. 8516 * 8517 * See: bug1933966, bug1933485 8518 * 8519 * @param {WindowStateData} winData 8520 */ 8521 _cleanupOrphanedClosedGroups(winData) { 8522 if (!winData.closedGroups) { 8523 return; 8524 } 8525 for (let index = winData.closedGroups.length - 1; index >= 0; index--) { 8526 if (winData.closedGroups[index].tabs.length === 0) { 8527 winData.closedGroups.splice(index, 1); 8528 this._closedObjectsChanged = true; 8529 } 8530 } 8531 }, 8532 8533 /** 8534 * @param {WindowStateData} closedWinData 8535 * @param {string} tabGroupId 8536 * @returns {void} modifies the data in argument `closedWinData` 8537 */ 8538 _removeSavedTabGroupFromClosedWindow(closedWinData, tabGroupId) { 8539 removeWhere(closedWinData.groups, tabGroup => tabGroup.id == tabGroupId); 8540 removeWhere(closedWinData.tabs, tab => tab.groupId == tabGroupId); 8541 this._closedObjectsChanged = true; 8542 }, 8543 8544 /** 8545 * Validates that a state object matches the schema 8546 * defined in browser/components/sessionstore/session.schema.json 8547 * 8548 * @param {object} [state] State object to validate. If not provided, 8549 * will validate the current session state. 8550 * @returns {Promise} A promise which resolves to a validation result object 8551 */ 8552 async validateState(state) { 8553 if (!state) { 8554 state = this.getCurrentState(); 8555 // Don't include the last session state in getBrowserState(). 8556 delete state.lastSessionState; 8557 // Don't include any deferred initial state. 8558 delete state.deferredInitialState; 8559 } 8560 const schema = await fetch( 8561 "resource:///modules/sessionstore/session.schema.json" 8562 ).then(rsp => rsp.json()); 8563 8564 let result; 8565 try { 8566 result = lazy.JsonSchema.validate(state, schema); 8567 if (!result.valid) { 8568 console.warn( 8569 "Session state didn't validate against the schema", 8570 result.errors 8571 ); 8572 } 8573 } catch (ex) { 8574 console.error(`Error validating session state: ${ex.message}`, ex); 8575 } 8576 return result; 8577 }, 8578 }; 8579 8580 /** 8581 * Priority queue that keeps track of a list of tabs to restore and returns 8582 * the tab we should restore next, based on priority rules. We decide between 8583 * pinned, visible and hidden tabs in that and FIFO order. Hidden tabs are only 8584 * restored with restore_hidden_tabs=true. 8585 */ 8586 var TabRestoreQueue = { 8587 // The separate buckets used to store tabs. 8588 tabs: { priority: [], visible: [], hidden: [] }, 8589 8590 // Preferences used by the TabRestoreQueue to determine which tabs 8591 // are restored automatically and which tabs will be on-demand. 8592 prefs: { 8593 // Lazy getter that returns whether tabs are restored on demand. 8594 get restoreOnDemand() { 8595 let updateValue = () => { 8596 let value = Services.prefs.getBoolPref(PREF); 8597 let definition = { value, configurable: true }; 8598 Object.defineProperty(this, "restoreOnDemand", definition); 8599 return value; 8600 }; 8601 8602 const PREF = "browser.sessionstore.restore_on_demand"; 8603 Services.prefs.addObserver(PREF, updateValue); 8604 return updateValue(); 8605 }, 8606 8607 // Lazy getter that returns whether pinned tabs are restored on demand. 8608 get restorePinnedTabsOnDemand() { 8609 let updateValue = () => { 8610 let value = Services.prefs.getBoolPref(PREF); 8611 let definition = { value, configurable: true }; 8612 Object.defineProperty(this, "restorePinnedTabsOnDemand", definition); 8613 return value; 8614 }; 8615 8616 const PREF = "browser.sessionstore.restore_pinned_tabs_on_demand"; 8617 Services.prefs.addObserver(PREF, updateValue); 8618 return updateValue(); 8619 }, 8620 8621 // Lazy getter that returns whether we should restore hidden tabs. 8622 get restoreHiddenTabs() { 8623 let updateValue = () => { 8624 let value = Services.prefs.getBoolPref(PREF); 8625 let definition = { value, configurable: true }; 8626 Object.defineProperty(this, "restoreHiddenTabs", definition); 8627 return value; 8628 }; 8629 8630 const PREF = "browser.sessionstore.restore_hidden_tabs"; 8631 Services.prefs.addObserver(PREF, updateValue); 8632 return updateValue(); 8633 }, 8634 }, 8635 8636 // Resets the queue and removes all tabs. 8637 reset() { 8638 this.tabs = { priority: [], visible: [], hidden: [] }; 8639 }, 8640 8641 // Adds a tab to the queue and determines its priority bucket. 8642 add(tab) { 8643 let { priority, hidden, visible } = this.tabs; 8644 8645 if (tab.pinned) { 8646 priority.push(tab); 8647 } else if (tab.hidden) { 8648 hidden.push(tab); 8649 } else { 8650 visible.push(tab); 8651 } 8652 }, 8653 8654 // Removes a given tab from the queue, if it's in there. 8655 remove(tab) { 8656 let { priority, hidden, visible } = this.tabs; 8657 8658 // We'll always check priority first since we don't 8659 // have an indicator if a tab will be there or not. 8660 let set = priority; 8661 let index = set.indexOf(tab); 8662 8663 if (index == -1) { 8664 set = tab.hidden ? hidden : visible; 8665 index = set.indexOf(tab); 8666 } 8667 8668 if (index > -1) { 8669 set.splice(index, 1); 8670 } 8671 }, 8672 8673 // Returns and removes the tab with the highest priority. 8674 shift() { 8675 let set; 8676 let { priority, hidden, visible } = this.tabs; 8677 8678 let { restoreOnDemand, restorePinnedTabsOnDemand } = this.prefs; 8679 let restorePinned = !(restoreOnDemand && restorePinnedTabsOnDemand); 8680 if (restorePinned && priority.length) { 8681 set = priority; 8682 } else if (!restoreOnDemand) { 8683 if (visible.length) { 8684 set = visible; 8685 } else if (this.prefs.restoreHiddenTabs && hidden.length) { 8686 set = hidden; 8687 } 8688 } 8689 8690 return set && set.shift(); 8691 }, 8692 8693 // Moves a given tab from the 'hidden' to the 'visible' bucket. 8694 hiddenToVisible(tab) { 8695 let { hidden, visible } = this.tabs; 8696 let index = hidden.indexOf(tab); 8697 8698 if (index > -1) { 8699 hidden.splice(index, 1); 8700 visible.push(tab); 8701 } 8702 }, 8703 8704 // Moves a given tab from the 'visible' to the 'hidden' bucket. 8705 visibleToHidden(tab) { 8706 let { visible, hidden } = this.tabs; 8707 let index = visible.indexOf(tab); 8708 8709 if (index > -1) { 8710 visible.splice(index, 1); 8711 hidden.push(tab); 8712 } 8713 }, 8714 8715 /** 8716 * Returns true if the passed tab is in one of the sets that we're 8717 * restoring content in automatically. 8718 * 8719 * @param tab (<xul:tab>) 8720 * The tab to check 8721 * @returns bool 8722 */ 8723 willRestoreSoon(tab) { 8724 let { priority, hidden, visible } = this.tabs; 8725 let { restoreOnDemand, restorePinnedTabsOnDemand, restoreHiddenTabs } = 8726 this.prefs; 8727 let restorePinned = !(restoreOnDemand && restorePinnedTabsOnDemand); 8728 let candidateSet = []; 8729 8730 if (restorePinned && priority.length) { 8731 candidateSet.push(...priority); 8732 } 8733 8734 if (!restoreOnDemand) { 8735 if (visible.length) { 8736 candidateSet.push(...visible); 8737 } 8738 8739 if (restoreHiddenTabs && hidden.length) { 8740 candidateSet.push(...hidden); 8741 } 8742 } 8743 8744 return candidateSet.indexOf(tab) > -1; 8745 }, 8746 }; 8747 8748 // A map storing a closed window's state data until it goes aways (is GC'ed). 8749 // This ensures that API clients can still read (but not write) states of 8750 // windows they still hold a reference to but we don't. 8751 var DyingWindowCache = { 8752 _data: new WeakMap(), 8753 8754 has(window) { 8755 return this._data.has(window); 8756 }, 8757 8758 get(window) { 8759 return this._data.get(window); 8760 }, 8761 8762 set(window, data) { 8763 this._data.set(window, data); 8764 }, 8765 8766 remove(window) { 8767 this._data.delete(window); 8768 }, 8769 }; 8770 8771 // A weak set of dirty windows. We use it to determine which windows we need to 8772 // recollect data for when getCurrentState() is called. 8773 var DirtyWindows = { 8774 _data: new WeakMap(), 8775 8776 has(window) { 8777 return this._data.has(window); 8778 }, 8779 8780 add(window) { 8781 return this._data.set(window, true); 8782 }, 8783 8784 remove(window) { 8785 this._data.delete(window); 8786 }, 8787 8788 clear(_window) { 8789 this._data = new WeakMap(); 8790 }, 8791 }; 8792 8793 // The state from the previous session (after restoring pinned tabs). This 8794 // state is persisted and passed through to the next session during an app 8795 // restart to make the third party add-on warning not trash the deferred 8796 // session 8797 var LastSession = { 8798 _state: null, 8799 8800 get canRestore() { 8801 return !!this._state; 8802 }, 8803 8804 getState() { 8805 return this._state; 8806 }, 8807 8808 setState(state) { 8809 this._state = state; 8810 }, 8811 8812 clear(silent = false) { 8813 if (this._state) { 8814 this._state = null; 8815 if (!silent) { 8816 Services.obs.notifyObservers(null, NOTIFY_LAST_SESSION_CLEARED); 8817 } 8818 } 8819 }, 8820 }; 8821 8822 /** 8823 * @template T 8824 * @param {T[]} array 8825 * @param {function(T):boolean} predicate 8826 */ 8827 function removeWhere(array, predicate) { 8828 for (let i = array.length - 1; i >= 0; i--) { 8829 if (predicate(array[i])) { 8830 array.splice(i, 1); 8831 } 8832 } 8833 } 8834 8835 // Exposed for tests 8836 export const _LastSession = LastSession;