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