browser/components/sessionstore/src/SessionStore.jsm

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

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

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 "use strict";
michael@0 6
michael@0 7 this.EXPORTED_SYMBOLS = ["SessionStore"];
michael@0 8
michael@0 9 const Cu = Components.utils;
michael@0 10 const Cc = Components.classes;
michael@0 11 const Ci = Components.interfaces;
michael@0 12 const Cr = Components.results;
michael@0 13
michael@0 14 const STATE_STOPPED = 0;
michael@0 15 const STATE_RUNNING = 1;
michael@0 16 const STATE_QUITTING = -1;
michael@0 17
michael@0 18 const TAB_STATE_NEEDS_RESTORE = 1;
michael@0 19 const TAB_STATE_RESTORING = 2;
michael@0 20
michael@0 21 const NOTIFY_WINDOWS_RESTORED = "sessionstore-windows-restored";
michael@0 22 const NOTIFY_BROWSER_STATE_RESTORED = "sessionstore-browser-state-restored";
michael@0 23 const NOTIFY_LAST_SESSION_CLEARED = "sessionstore-last-session-cleared";
michael@0 24
michael@0 25 const NOTIFY_TAB_RESTORED = "sessionstore-debug-tab-restored"; // WARNING: debug-only
michael@0 26
michael@0 27 // Maximum number of tabs to restore simultaneously. Previously controlled by
michael@0 28 // the browser.sessionstore.max_concurrent_tabs pref.
michael@0 29 const MAX_CONCURRENT_TAB_RESTORES = 3;
michael@0 30
michael@0 31 // global notifications observed
michael@0 32 const OBSERVING = [
michael@0 33 "domwindowopened", "domwindowclosed",
michael@0 34 "quit-application-requested", "quit-application-granted",
michael@0 35 "browser-lastwindow-close-granted",
michael@0 36 "quit-application", "browser:purge-session-history",
michael@0 37 "browser:purge-domain-data",
michael@0 38 "gather-telemetry",
michael@0 39 ];
michael@0 40
michael@0 41 // XUL Window properties to (re)store
michael@0 42 // Restored in restoreDimensions()
michael@0 43 const WINDOW_ATTRIBUTES = ["width", "height", "screenX", "screenY", "sizemode"];
michael@0 44
michael@0 45 // Hideable window features to (re)store
michael@0 46 // Restored in restoreWindowFeatures()
michael@0 47 const WINDOW_HIDEABLE_FEATURES = [
michael@0 48 "menubar", "toolbar", "locationbar", "personalbar", "statusbar", "scrollbars"
michael@0 49 ];
michael@0 50
michael@0 51 const MESSAGES = [
michael@0 52 // The content script gives us a reference to an object that performs
michael@0 53 // synchronous collection of session data.
michael@0 54 "SessionStore:setupSyncHandler",
michael@0 55
michael@0 56 // The content script sends us data that has been invalidated and needs to
michael@0 57 // be saved to disk.
michael@0 58 "SessionStore:update",
michael@0 59
michael@0 60 // The restoreHistory code has run. This is a good time to run SSTabRestoring.
michael@0 61 "SessionStore:restoreHistoryComplete",
michael@0 62
michael@0 63 // The load for the restoring tab has begun. We update the URL bar at this
michael@0 64 // time; if we did it before, the load would overwrite it.
michael@0 65 "SessionStore:restoreTabContentStarted",
michael@0 66
michael@0 67 // All network loads for a restoring tab are done, so we should consider
michael@0 68 // restoring another tab in the queue.
michael@0 69 "SessionStore:restoreTabContentComplete",
michael@0 70
michael@0 71 // The document has been restored, so the restore is done. We trigger
michael@0 72 // SSTabRestored at this time.
michael@0 73 "SessionStore:restoreDocumentComplete",
michael@0 74
michael@0 75 // A tab that is being restored was reloaded. We call restoreTabContent to
michael@0 76 // finish restoring it right away.
michael@0 77 "SessionStore:reloadPendingTab",
michael@0 78 ];
michael@0 79
michael@0 80 // These are tab events that we listen to.
michael@0 81 const TAB_EVENTS = [
michael@0 82 "TabOpen", "TabClose", "TabSelect", "TabShow", "TabHide", "TabPinned",
michael@0 83 "TabUnpinned"
michael@0 84 ];
michael@0 85
michael@0 86 // The number of milliseconds in a day
michael@0 87 const MS_PER_DAY = 1000.0 * 60.0 * 60.0 * 24.0;
michael@0 88
michael@0 89 Cu.import("resource://gre/modules/Services.jsm", this);
michael@0 90 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
michael@0 91 Cu.import("resource://gre/modules/TelemetryTimestamps.jsm", this);
michael@0 92 Cu.import("resource://gre/modules/TelemetryStopwatch.jsm", this);
michael@0 93 Cu.import("resource://gre/modules/osfile.jsm", this);
michael@0 94 Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm", this);
michael@0 95 Cu.import("resource://gre/modules/Promise.jsm", this);
michael@0 96 Cu.import("resource://gre/modules/Task.jsm", this);
michael@0 97
michael@0 98 XPCOMUtils.defineLazyServiceGetter(this, "gSessionStartup",
michael@0 99 "@mozilla.org/browser/sessionstartup;1", "nsISessionStartup");
michael@0 100 XPCOMUtils.defineLazyServiceGetter(this, "gScreenManager",
michael@0 101 "@mozilla.org/gfx/screenmanager;1", "nsIScreenManager");
michael@0 102 XPCOMUtils.defineLazyServiceGetter(this, "Telemetry",
michael@0 103 "@mozilla.org/base/telemetry;1", "nsITelemetry");
michael@0 104
michael@0 105 XPCOMUtils.defineLazyModuleGetter(this, "console",
michael@0 106 "resource://gre/modules/devtools/Console.jsm");
michael@0 107 XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
michael@0 108 "resource:///modules/RecentWindow.jsm");
michael@0 109
michael@0 110 XPCOMUtils.defineLazyModuleGetter(this, "GlobalState",
michael@0 111 "resource:///modules/sessionstore/GlobalState.jsm");
michael@0 112 XPCOMUtils.defineLazyModuleGetter(this, "PrivacyFilter",
michael@0 113 "resource:///modules/sessionstore/PrivacyFilter.jsm");
michael@0 114 XPCOMUtils.defineLazyModuleGetter(this, "ScratchpadManager",
michael@0 115 "resource:///modules/devtools/scratchpad-manager.jsm");
michael@0 116 XPCOMUtils.defineLazyModuleGetter(this, "SessionSaver",
michael@0 117 "resource:///modules/sessionstore/SessionSaver.jsm");
michael@0 118 XPCOMUtils.defineLazyModuleGetter(this, "SessionCookies",
michael@0 119 "resource:///modules/sessionstore/SessionCookies.jsm");
michael@0 120 XPCOMUtils.defineLazyModuleGetter(this, "SessionFile",
michael@0 121 "resource:///modules/sessionstore/SessionFile.jsm");
michael@0 122 XPCOMUtils.defineLazyModuleGetter(this, "TabAttributes",
michael@0 123 "resource:///modules/sessionstore/TabAttributes.jsm");
michael@0 124 XPCOMUtils.defineLazyModuleGetter(this, "TabState",
michael@0 125 "resource:///modules/sessionstore/TabState.jsm");
michael@0 126 XPCOMUtils.defineLazyModuleGetter(this, "TabStateCache",
michael@0 127 "resource:///modules/sessionstore/TabStateCache.jsm");
michael@0 128 XPCOMUtils.defineLazyModuleGetter(this, "Utils",
michael@0 129 "resource:///modules/sessionstore/Utils.jsm");
michael@0 130
michael@0 131 /**
michael@0 132 * |true| if we are in debug mode, |false| otherwise.
michael@0 133 * Debug mode is controlled by preference browser.sessionstore.debug
michael@0 134 */
michael@0 135 let gDebuggingEnabled = false;
michael@0 136 function debug(aMsg) {
michael@0 137 if (gDebuggingEnabled) {
michael@0 138 aMsg = ("SessionStore: " + aMsg).replace(/\S{80}/g, "$&\n");
michael@0 139 Services.console.logStringMessage(aMsg);
michael@0 140 }
michael@0 141 }
michael@0 142
michael@0 143 this.SessionStore = {
michael@0 144 get promiseInitialized() {
michael@0 145 return SessionStoreInternal.promiseInitialized;
michael@0 146 },
michael@0 147
michael@0 148 get canRestoreLastSession() {
michael@0 149 return SessionStoreInternal.canRestoreLastSession;
michael@0 150 },
michael@0 151
michael@0 152 set canRestoreLastSession(val) {
michael@0 153 SessionStoreInternal.canRestoreLastSession = val;
michael@0 154 },
michael@0 155
michael@0 156 init: function ss_init() {
michael@0 157 SessionStoreInternal.init();
michael@0 158 },
michael@0 159
michael@0 160 getBrowserState: function ss_getBrowserState() {
michael@0 161 return SessionStoreInternal.getBrowserState();
michael@0 162 },
michael@0 163
michael@0 164 setBrowserState: function ss_setBrowserState(aState) {
michael@0 165 SessionStoreInternal.setBrowserState(aState);
michael@0 166 },
michael@0 167
michael@0 168 getWindowState: function ss_getWindowState(aWindow) {
michael@0 169 return SessionStoreInternal.getWindowState(aWindow);
michael@0 170 },
michael@0 171
michael@0 172 setWindowState: function ss_setWindowState(aWindow, aState, aOverwrite) {
michael@0 173 SessionStoreInternal.setWindowState(aWindow, aState, aOverwrite);
michael@0 174 },
michael@0 175
michael@0 176 getTabState: function ss_getTabState(aTab) {
michael@0 177 return SessionStoreInternal.getTabState(aTab);
michael@0 178 },
michael@0 179
michael@0 180 setTabState: function ss_setTabState(aTab, aState) {
michael@0 181 SessionStoreInternal.setTabState(aTab, aState);
michael@0 182 },
michael@0 183
michael@0 184 duplicateTab: function ss_duplicateTab(aWindow, aTab, aDelta = 0) {
michael@0 185 return SessionStoreInternal.duplicateTab(aWindow, aTab, aDelta);
michael@0 186 },
michael@0 187
michael@0 188 getClosedTabCount: function ss_getClosedTabCount(aWindow) {
michael@0 189 return SessionStoreInternal.getClosedTabCount(aWindow);
michael@0 190 },
michael@0 191
michael@0 192 getClosedTabData: function ss_getClosedTabDataAt(aWindow) {
michael@0 193 return SessionStoreInternal.getClosedTabData(aWindow);
michael@0 194 },
michael@0 195
michael@0 196 undoCloseTab: function ss_undoCloseTab(aWindow, aIndex) {
michael@0 197 return SessionStoreInternal.undoCloseTab(aWindow, aIndex);
michael@0 198 },
michael@0 199
michael@0 200 forgetClosedTab: function ss_forgetClosedTab(aWindow, aIndex) {
michael@0 201 return SessionStoreInternal.forgetClosedTab(aWindow, aIndex);
michael@0 202 },
michael@0 203
michael@0 204 getClosedWindowCount: function ss_getClosedWindowCount() {
michael@0 205 return SessionStoreInternal.getClosedWindowCount();
michael@0 206 },
michael@0 207
michael@0 208 getClosedWindowData: function ss_getClosedWindowData() {
michael@0 209 return SessionStoreInternal.getClosedWindowData();
michael@0 210 },
michael@0 211
michael@0 212 undoCloseWindow: function ss_undoCloseWindow(aIndex) {
michael@0 213 return SessionStoreInternal.undoCloseWindow(aIndex);
michael@0 214 },
michael@0 215
michael@0 216 forgetClosedWindow: function ss_forgetClosedWindow(aIndex) {
michael@0 217 return SessionStoreInternal.forgetClosedWindow(aIndex);
michael@0 218 },
michael@0 219
michael@0 220 getWindowValue: function ss_getWindowValue(aWindow, aKey) {
michael@0 221 return SessionStoreInternal.getWindowValue(aWindow, aKey);
michael@0 222 },
michael@0 223
michael@0 224 setWindowValue: function ss_setWindowValue(aWindow, aKey, aStringValue) {
michael@0 225 SessionStoreInternal.setWindowValue(aWindow, aKey, aStringValue);
michael@0 226 },
michael@0 227
michael@0 228 deleteWindowValue: function ss_deleteWindowValue(aWindow, aKey) {
michael@0 229 SessionStoreInternal.deleteWindowValue(aWindow, aKey);
michael@0 230 },
michael@0 231
michael@0 232 getTabValue: function ss_getTabValue(aTab, aKey) {
michael@0 233 return SessionStoreInternal.getTabValue(aTab, aKey);
michael@0 234 },
michael@0 235
michael@0 236 setTabValue: function ss_setTabValue(aTab, aKey, aStringValue) {
michael@0 237 SessionStoreInternal.setTabValue(aTab, aKey, aStringValue);
michael@0 238 },
michael@0 239
michael@0 240 deleteTabValue: function ss_deleteTabValue(aTab, aKey) {
michael@0 241 SessionStoreInternal.deleteTabValue(aTab, aKey);
michael@0 242 },
michael@0 243
michael@0 244 getGlobalValue: function ss_getGlobalValue(aKey) {
michael@0 245 return SessionStoreInternal.getGlobalValue(aKey);
michael@0 246 },
michael@0 247
michael@0 248 setGlobalValue: function ss_setGlobalValue(aKey, aStringValue) {
michael@0 249 SessionStoreInternal.setGlobalValue(aKey, aStringValue);
michael@0 250 },
michael@0 251
michael@0 252 deleteGlobalValue: function ss_deleteGlobalValue(aKey) {
michael@0 253 SessionStoreInternal.deleteGlobalValue(aKey);
michael@0 254 },
michael@0 255
michael@0 256 persistTabAttribute: function ss_persistTabAttribute(aName) {
michael@0 257 SessionStoreInternal.persistTabAttribute(aName);
michael@0 258 },
michael@0 259
michael@0 260 restoreLastSession: function ss_restoreLastSession() {
michael@0 261 SessionStoreInternal.restoreLastSession();
michael@0 262 },
michael@0 263
michael@0 264 getCurrentState: function (aUpdateAll) {
michael@0 265 return SessionStoreInternal.getCurrentState(aUpdateAll);
michael@0 266 },
michael@0 267
michael@0 268 /**
michael@0 269 * Backstage pass to implementation details, used for testing purpose.
michael@0 270 * Controlled by preference "browser.sessionstore.testmode".
michael@0 271 */
michael@0 272 get _internal() {
michael@0 273 if (Services.prefs.getBoolPref("browser.sessionstore.debug")) {
michael@0 274 return SessionStoreInternal;
michael@0 275 }
michael@0 276 return undefined;
michael@0 277 },
michael@0 278 };
michael@0 279
michael@0 280 // Freeze the SessionStore object. We don't want anyone to modify it.
michael@0 281 Object.freeze(SessionStore);
michael@0 282
michael@0 283 let SessionStoreInternal = {
michael@0 284 QueryInterface: XPCOMUtils.generateQI([
michael@0 285 Ci.nsIDOMEventListener,
michael@0 286 Ci.nsIObserver,
michael@0 287 Ci.nsISupportsWeakReference
michael@0 288 ]),
michael@0 289
michael@0 290 // set default load state
michael@0 291 _loadState: STATE_STOPPED,
michael@0 292
michael@0 293 _globalState: new GlobalState(),
michael@0 294
michael@0 295 // During the initial restore and setBrowserState calls tracks the number of
michael@0 296 // windows yet to be restored
michael@0 297 _restoreCount: -1,
michael@0 298
michael@0 299 // This number gets incremented each time we start to restore a tab.
michael@0 300 _nextRestoreEpoch: 1,
michael@0 301
michael@0 302 // For each <browser> element being restored, records the current epoch.
michael@0 303 _browserEpochs: new WeakMap(),
michael@0 304
michael@0 305 // whether a setBrowserState call is in progress
michael@0 306 _browserSetState: false,
michael@0 307
michael@0 308 // time in milliseconds when the session was started (saved across sessions),
michael@0 309 // defaults to now if no session was restored or timestamp doesn't exist
michael@0 310 _sessionStartTime: Date.now(),
michael@0 311
michael@0 312 // states for all currently opened windows
michael@0 313 _windows: {},
michael@0 314
michael@0 315 // counter for creating unique window IDs
michael@0 316 _nextWindowID: 0,
michael@0 317
michael@0 318 // states for all recently closed windows
michael@0 319 _closedWindows: [],
michael@0 320
michael@0 321 // collection of session states yet to be restored
michael@0 322 _statesToRestore: {},
michael@0 323
michael@0 324 // counts the number of crashes since the last clean start
michael@0 325 _recentCrashes: 0,
michael@0 326
michael@0 327 // whether the last window was closed and should be restored
michael@0 328 _restoreLastWindow: false,
michael@0 329
michael@0 330 // number of tabs currently restoring
michael@0 331 _tabsRestoringCount: 0,
michael@0 332
michael@0 333 // When starting Firefox with a single private window, this is the place
michael@0 334 // where we keep the session we actually wanted to restore in case the user
michael@0 335 // decides to later open a non-private window as well.
michael@0 336 _deferredInitialState: null,
michael@0 337
michael@0 338 // A promise resolved once initialization is complete
michael@0 339 _deferredInitialized: Promise.defer(),
michael@0 340
michael@0 341 // Whether session has been initialized
michael@0 342 _sessionInitialized: false,
michael@0 343
michael@0 344 // Promise that is resolved when we're ready to initialize
michael@0 345 // and restore the session.
michael@0 346 _promiseReadyForInitialization: null,
michael@0 347
michael@0 348 /**
michael@0 349 * A promise fulfilled once initialization is complete.
michael@0 350 */
michael@0 351 get promiseInitialized() {
michael@0 352 return this._deferredInitialized.promise;
michael@0 353 },
michael@0 354
michael@0 355 get canRestoreLastSession() {
michael@0 356 return LastSession.canRestore;
michael@0 357 },
michael@0 358
michael@0 359 set canRestoreLastSession(val) {
michael@0 360 // Cheat a bit; only allow false.
michael@0 361 if (!val) {
michael@0 362 LastSession.clear();
michael@0 363 }
michael@0 364 },
michael@0 365
michael@0 366 /**
michael@0 367 * Initialize the sessionstore service.
michael@0 368 */
michael@0 369 init: function () {
michael@0 370 if (this._initialized) {
michael@0 371 throw new Error("SessionStore.init() must only be called once!");
michael@0 372 }
michael@0 373
michael@0 374 TelemetryTimestamps.add("sessionRestoreInitialized");
michael@0 375 OBSERVING.forEach(function(aTopic) {
michael@0 376 Services.obs.addObserver(this, aTopic, true);
michael@0 377 }, this);
michael@0 378
michael@0 379 this._initPrefs();
michael@0 380 this._initialized = true;
michael@0 381 },
michael@0 382
michael@0 383 /**
michael@0 384 * Initialize the session using the state provided by SessionStartup
michael@0 385 */
michael@0 386 initSession: function () {
michael@0 387 let state;
michael@0 388 let ss = gSessionStartup;
michael@0 389
michael@0 390 try {
michael@0 391 if (ss.doRestore() ||
michael@0 392 ss.sessionType == Ci.nsISessionStartup.DEFER_SESSION)
michael@0 393 state = ss.state;
michael@0 394 }
michael@0 395 catch(ex) { dump(ex + "\n"); } // no state to restore, which is ok
michael@0 396
michael@0 397 if (state) {
michael@0 398 try {
michael@0 399 // If we're doing a DEFERRED session, then we want to pull pinned tabs
michael@0 400 // out so they can be restored.
michael@0 401 if (ss.sessionType == Ci.nsISessionStartup.DEFER_SESSION) {
michael@0 402 let [iniState, remainingState] = this._prepDataForDeferredRestore(state);
michael@0 403 // If we have a iniState with windows, that means that we have windows
michael@0 404 // with app tabs to restore.
michael@0 405 if (iniState.windows.length)
michael@0 406 state = iniState;
michael@0 407 else
michael@0 408 state = null;
michael@0 409
michael@0 410 if (remainingState.windows.length) {
michael@0 411 LastSession.setState(remainingState);
michael@0 412 }
michael@0 413 }
michael@0 414 else {
michael@0 415 // Get the last deferred session in case the user still wants to
michael@0 416 // restore it
michael@0 417 LastSession.setState(state.lastSessionState);
michael@0 418
michael@0 419 if (ss.previousSessionCrashed) {
michael@0 420 this._recentCrashes = (state.session &&
michael@0 421 state.session.recentCrashes || 0) + 1;
michael@0 422
michael@0 423 if (this._needsRestorePage(state, this._recentCrashes)) {
michael@0 424 // replace the crashed session with a restore-page-only session
michael@0 425 let pageData = {
michael@0 426 url: "about:sessionrestore",
michael@0 427 formdata: {
michael@0 428 id: { "sessionData": state },
michael@0 429 xpath: {}
michael@0 430 }
michael@0 431 };
michael@0 432 state = { windows: [{ tabs: [{ entries: [pageData] }] }] };
michael@0 433 } else if (this._hasSingleTabWithURL(state.windows,
michael@0 434 "about:welcomeback")) {
michael@0 435 // On a single about:welcomeback URL that crashed, replace about:welcomeback
michael@0 436 // with about:sessionrestore, to make clear to the user that we crashed.
michael@0 437 state.windows[0].tabs[0].entries[0].url = "about:sessionrestore";
michael@0 438 }
michael@0 439 }
michael@0 440
michael@0 441 // Update the session start time using the restored session state.
michael@0 442 this._updateSessionStartTime(state);
michael@0 443
michael@0 444 // make sure that at least the first window doesn't have anything hidden
michael@0 445 delete state.windows[0].hidden;
michael@0 446 // Since nothing is hidden in the first window, it cannot be a popup
michael@0 447 delete state.windows[0].isPopup;
michael@0 448 // We don't want to minimize and then open a window at startup.
michael@0 449 if (state.windows[0].sizemode == "minimized")
michael@0 450 state.windows[0].sizemode = "normal";
michael@0 451 // clear any lastSessionWindowID attributes since those don't matter
michael@0 452 // during normal restore
michael@0 453 state.windows.forEach(function(aWindow) {
michael@0 454 delete aWindow.__lastSessionWindowID;
michael@0 455 });
michael@0 456 }
michael@0 457 }
michael@0 458 catch (ex) { debug("The session file is invalid: " + ex); }
michael@0 459 }
michael@0 460
michael@0 461 // at this point, we've as good as resumed the session, so we can
michael@0 462 // clear the resume_session_once flag, if it's set
michael@0 463 if (this._loadState != STATE_QUITTING &&
michael@0 464 this._prefBranch.getBoolPref("sessionstore.resume_session_once"))
michael@0 465 this._prefBranch.setBoolPref("sessionstore.resume_session_once", false);
michael@0 466
michael@0 467 this._performUpgradeBackup();
michael@0 468
michael@0 469 return state;
michael@0 470 },
michael@0 471
michael@0 472 /**
michael@0 473 * If this is the first time we launc this build of Firefox,
michael@0 474 * backup sessionstore.js.
michael@0 475 */
michael@0 476 _performUpgradeBackup: function ssi_performUpgradeBackup() {
michael@0 477 // Perform upgrade backup, if necessary
michael@0 478 const PREF_UPGRADE = "sessionstore.upgradeBackup.latestBuildID";
michael@0 479
michael@0 480 let buildID = Services.appinfo.platformBuildID;
michael@0 481 let latestBackup = this._prefBranch.getCharPref(PREF_UPGRADE);
michael@0 482 if (latestBackup == buildID) {
michael@0 483 return Promise.resolve();
michael@0 484 }
michael@0 485 return Task.spawn(function task() {
michael@0 486 try {
michael@0 487 // Perform background backup
michael@0 488 yield SessionFile.createBackupCopy("-" + buildID);
michael@0 489
michael@0 490 this._prefBranch.setCharPref(PREF_UPGRADE, buildID);
michael@0 491
michael@0 492 // In case of success, remove previous backup.
michael@0 493 yield SessionFile.removeBackupCopy("-" + latestBackup);
michael@0 494 } catch (ex) {
michael@0 495 debug("Could not perform upgrade backup " + ex);
michael@0 496 debug(ex.stack);
michael@0 497 }
michael@0 498 }.bind(this));
michael@0 499 },
michael@0 500
michael@0 501 _initPrefs : function() {
michael@0 502 this._prefBranch = Services.prefs.getBranch("browser.");
michael@0 503
michael@0 504 gDebuggingEnabled = this._prefBranch.getBoolPref("sessionstore.debug");
michael@0 505
michael@0 506 Services.prefs.addObserver("browser.sessionstore.debug", () => {
michael@0 507 gDebuggingEnabled = this._prefBranch.getBoolPref("sessionstore.debug");
michael@0 508 }, false);
michael@0 509
michael@0 510 this._max_tabs_undo = this._prefBranch.getIntPref("sessionstore.max_tabs_undo");
michael@0 511 this._prefBranch.addObserver("sessionstore.max_tabs_undo", this, true);
michael@0 512
michael@0 513 this._max_windows_undo = this._prefBranch.getIntPref("sessionstore.max_windows_undo");
michael@0 514 this._prefBranch.addObserver("sessionstore.max_windows_undo", this, true);
michael@0 515 },
michael@0 516
michael@0 517 /**
michael@0 518 * Called on application shutdown, after notifications:
michael@0 519 * quit-application-granted, quit-application
michael@0 520 */
michael@0 521 _uninit: function ssi_uninit() {
michael@0 522 if (!this._initialized) {
michael@0 523 throw new Error("SessionStore is not initialized.");
michael@0 524 }
michael@0 525
michael@0 526 // save all data for session resuming
michael@0 527 if (this._sessionInitialized) {
michael@0 528 SessionSaver.run();
michael@0 529 }
michael@0 530
michael@0 531 // clear out priority queue in case it's still holding refs
michael@0 532 TabRestoreQueue.reset();
michael@0 533
michael@0 534 // Make sure to cancel pending saves.
michael@0 535 SessionSaver.cancel();
michael@0 536 },
michael@0 537
michael@0 538 /**
michael@0 539 * Handle notifications
michael@0 540 */
michael@0 541 observe: function ssi_observe(aSubject, aTopic, aData) {
michael@0 542 switch (aTopic) {
michael@0 543 case "domwindowopened": // catch new windows
michael@0 544 this.onOpen(aSubject);
michael@0 545 break;
michael@0 546 case "domwindowclosed": // catch closed windows
michael@0 547 this.onClose(aSubject);
michael@0 548 break;
michael@0 549 case "quit-application-requested":
michael@0 550 this.onQuitApplicationRequested();
michael@0 551 break;
michael@0 552 case "quit-application-granted":
michael@0 553 this.onQuitApplicationGranted();
michael@0 554 break;
michael@0 555 case "browser-lastwindow-close-granted":
michael@0 556 this.onLastWindowCloseGranted();
michael@0 557 break;
michael@0 558 case "quit-application":
michael@0 559 this.onQuitApplication(aData);
michael@0 560 break;
michael@0 561 case "browser:purge-session-history": // catch sanitization
michael@0 562 this.onPurgeSessionHistory();
michael@0 563 break;
michael@0 564 case "browser:purge-domain-data":
michael@0 565 this.onPurgeDomainData(aData);
michael@0 566 break;
michael@0 567 case "nsPref:changed": // catch pref changes
michael@0 568 this.onPrefChange(aData);
michael@0 569 break;
michael@0 570 case "gather-telemetry":
michael@0 571 this.onGatherTelemetry();
michael@0 572 break;
michael@0 573 }
michael@0 574 },
michael@0 575
michael@0 576 /**
michael@0 577 * This method handles incoming messages sent by the session store content
michael@0 578 * script and thus enables communication with OOP tabs.
michael@0 579 */
michael@0 580 receiveMessage: function ssi_receiveMessage(aMessage) {
michael@0 581 var browser = aMessage.target;
michael@0 582 var win = browser.ownerDocument.defaultView;
michael@0 583 let tab = this._getTabForBrowser(browser);
michael@0 584 if (!tab) {
michael@0 585 // Ignore messages from <browser> elements that are not tabs.
michael@0 586 return;
michael@0 587 }
michael@0 588
michael@0 589 switch (aMessage.name) {
michael@0 590 case "SessionStore:setupSyncHandler":
michael@0 591 TabState.setSyncHandler(browser, aMessage.objects.handler);
michael@0 592 break;
michael@0 593 case "SessionStore:update":
michael@0 594 this.recordTelemetry(aMessage.data.telemetry);
michael@0 595 TabState.update(browser, aMessage.data);
michael@0 596 this.saveStateDelayed(win);
michael@0 597 break;
michael@0 598 case "SessionStore:restoreHistoryComplete":
michael@0 599 if (this.isCurrentEpoch(browser, aMessage.data.epoch)) {
michael@0 600 // Notify the tabbrowser that the tab chrome has been restored.
michael@0 601 let tabData = browser.__SS_data;
michael@0 602
michael@0 603 // wall-paper fix for bug 439675: make sure that the URL to be loaded
michael@0 604 // is always visible in the address bar
michael@0 605 let activePageData = tabData.entries[tabData.index - 1] || null;
michael@0 606 let uri = activePageData ? activePageData.url || null : null;
michael@0 607 browser.userTypedValue = uri;
michael@0 608
michael@0 609 // If the page has a title, set it.
michael@0 610 if (activePageData) {
michael@0 611 if (activePageData.title) {
michael@0 612 tab.label = activePageData.title;
michael@0 613 tab.crop = "end";
michael@0 614 } else if (activePageData.url != "about:blank") {
michael@0 615 tab.label = activePageData.url;
michael@0 616 tab.crop = "center";
michael@0 617 }
michael@0 618 }
michael@0 619
michael@0 620 // Restore the tab icon.
michael@0 621 if ("image" in tabData) {
michael@0 622 win.gBrowser.setIcon(tab, tabData.image);
michael@0 623 }
michael@0 624
michael@0 625 let event = win.document.createEvent("Events");
michael@0 626 event.initEvent("SSTabRestoring", true, false);
michael@0 627 tab.dispatchEvent(event);
michael@0 628 }
michael@0 629 break;
michael@0 630 case "SessionStore:restoreTabContentStarted":
michael@0 631 if (this.isCurrentEpoch(browser, aMessage.data.epoch)) {
michael@0 632 // If the user was typing into the URL bar when we crashed, but hadn't hit
michael@0 633 // enter yet, then we just need to write that value to the URL bar without
michael@0 634 // loading anything. This must happen after the load, since it will clear
michael@0 635 // userTypedValue.
michael@0 636 let tabData = browser.__SS_data;
michael@0 637 if (tabData.userTypedValue && !tabData.userTypedClear) {
michael@0 638 browser.userTypedValue = tabData.userTypedValue;
michael@0 639 win.URLBarSetURI();
michael@0 640 }
michael@0 641 }
michael@0 642 break;
michael@0 643 case "SessionStore:restoreTabContentComplete":
michael@0 644 if (this.isCurrentEpoch(browser, aMessage.data.epoch)) {
michael@0 645 // This callback is used exclusively by tests that want to
michael@0 646 // monitor the progress of network loads.
michael@0 647 if (gDebuggingEnabled) {
michael@0 648 Services.obs.notifyObservers(browser, NOTIFY_TAB_RESTORED, null);
michael@0 649 }
michael@0 650
michael@0 651 if (tab) {
michael@0 652 SessionStoreInternal._resetLocalTabRestoringState(tab);
michael@0 653 SessionStoreInternal.restoreNextTab();
michael@0 654 }
michael@0 655 }
michael@0 656 break;
michael@0 657 case "SessionStore:restoreDocumentComplete":
michael@0 658 if (this.isCurrentEpoch(browser, aMessage.data.epoch)) {
michael@0 659 // Document has been restored. Delete all the state associated
michael@0 660 // with it and trigger SSTabRestored.
michael@0 661 let tab = browser.__SS_restore_tab;
michael@0 662
michael@0 663 delete browser.__SS_restore_data;
michael@0 664 delete browser.__SS_restore_tab;
michael@0 665 delete browser.__SS_data;
michael@0 666
michael@0 667 this._sendTabRestoredNotification(tab);
michael@0 668 }
michael@0 669 break;
michael@0 670 case "SessionStore:reloadPendingTab":
michael@0 671 if (this.isCurrentEpoch(browser, aMessage.data.epoch)) {
michael@0 672 if (tab && browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
michael@0 673 this.restoreTabContent(tab);
michael@0 674 }
michael@0 675 }
michael@0 676 break;
michael@0 677 default:
michael@0 678 debug("received unknown message '" + aMessage.name + "'");
michael@0 679 break;
michael@0 680 }
michael@0 681 },
michael@0 682
michael@0 683 /**
michael@0 684 * Record telemetry measurements stored in an object.
michael@0 685 * @param telemetry
michael@0 686 * {histogramID: value, ...} An object mapping histogramIDs to the
michael@0 687 * value to be recorded for that ID,
michael@0 688 */
michael@0 689 recordTelemetry: function (telemetry) {
michael@0 690 for (let histogramId in telemetry){
michael@0 691 Telemetry.getHistogramById(histogramId).add(telemetry[histogramId]);
michael@0 692 }
michael@0 693 },
michael@0 694
michael@0 695 /* ........ Window Event Handlers .............. */
michael@0 696
michael@0 697 /**
michael@0 698 * Implement nsIDOMEventListener for handling various window and tab events
michael@0 699 */
michael@0 700 handleEvent: function ssi_handleEvent(aEvent) {
michael@0 701 var win = aEvent.currentTarget.ownerDocument.defaultView;
michael@0 702 let browser;
michael@0 703 switch (aEvent.type) {
michael@0 704 case "TabOpen":
michael@0 705 this.onTabAdd(win, aEvent.originalTarget);
michael@0 706 break;
michael@0 707 case "TabClose":
michael@0 708 // aEvent.detail determines if the tab was closed by moving to a different window
michael@0 709 if (!aEvent.detail)
michael@0 710 this.onTabClose(win, aEvent.originalTarget);
michael@0 711 this.onTabRemove(win, aEvent.originalTarget);
michael@0 712 break;
michael@0 713 case "TabSelect":
michael@0 714 this.onTabSelect(win);
michael@0 715 break;
michael@0 716 case "TabShow":
michael@0 717 this.onTabShow(win, aEvent.originalTarget);
michael@0 718 break;
michael@0 719 case "TabHide":
michael@0 720 this.onTabHide(win, aEvent.originalTarget);
michael@0 721 break;
michael@0 722 case "TabPinned":
michael@0 723 case "TabUnpinned":
michael@0 724 this.saveStateDelayed(win);
michael@0 725 break;
michael@0 726 }
michael@0 727 this._clearRestoringWindows();
michael@0 728 },
michael@0 729
michael@0 730 /**
michael@0 731 * Generate a unique window identifier
michael@0 732 * @return string
michael@0 733 * A unique string to identify a window
michael@0 734 */
michael@0 735 _generateWindowID: function ssi_generateWindowID() {
michael@0 736 return "window" + (this._nextWindowID++);
michael@0 737 },
michael@0 738
michael@0 739 /**
michael@0 740 * If it's the first window load since app start...
michael@0 741 * - determine if we're reloading after a crash or a forced-restart
michael@0 742 * - restore window state
michael@0 743 * - restart downloads
michael@0 744 * Set up event listeners for this window's tabs
michael@0 745 * @param aWindow
michael@0 746 * Window reference
michael@0 747 * @param aInitialState
michael@0 748 * The initial state to be loaded after startup (optional)
michael@0 749 */
michael@0 750 onLoad: function ssi_onLoad(aWindow, aInitialState = null) {
michael@0 751 // return if window has already been initialized
michael@0 752 if (aWindow && aWindow.__SSi && this._windows[aWindow.__SSi])
michael@0 753 return;
michael@0 754
michael@0 755 // ignore windows opened while shutting down
michael@0 756 if (this._loadState == STATE_QUITTING)
michael@0 757 return;
michael@0 758
michael@0 759 // Assign the window a unique identifier we can use to reference
michael@0 760 // internal data about the window.
michael@0 761 aWindow.__SSi = this._generateWindowID();
michael@0 762
michael@0 763 let mm = aWindow.messageManager;
michael@0 764 MESSAGES.forEach(msg => mm.addMessageListener(msg, this));
michael@0 765
michael@0 766 // Load the frame script after registering listeners.
michael@0 767 mm.loadFrameScript("chrome://browser/content/content-sessionStore.js", true);
michael@0 768
michael@0 769 // and create its data object
michael@0 770 this._windows[aWindow.__SSi] = { tabs: [], selected: 0, _closedTabs: [], busy: false };
michael@0 771
michael@0 772 let isPrivateWindow = false;
michael@0 773 if (PrivateBrowsingUtils.isWindowPrivate(aWindow))
michael@0 774 this._windows[aWindow.__SSi].isPrivate = isPrivateWindow = true;
michael@0 775 if (!this._isWindowLoaded(aWindow))
michael@0 776 this._windows[aWindow.__SSi]._restoring = true;
michael@0 777 if (!aWindow.toolbar.visible)
michael@0 778 this._windows[aWindow.__SSi].isPopup = true;
michael@0 779
michael@0 780 // perform additional initialization when the first window is loading
michael@0 781 if (this._loadState == STATE_STOPPED) {
michael@0 782 this._loadState = STATE_RUNNING;
michael@0 783 SessionSaver.updateLastSaveTime();
michael@0 784
michael@0 785 // restore a crashed session resp. resume the last session if requested
michael@0 786 if (aInitialState) {
michael@0 787 if (isPrivateWindow) {
michael@0 788 // We're starting with a single private window. Save the state we
michael@0 789 // actually wanted to restore so that we can do it later in case
michael@0 790 // the user opens another, non-private window.
michael@0 791 this._deferredInitialState = gSessionStartup.state;
michael@0 792
michael@0 793 // Nothing to restore now, notify observers things are complete.
michael@0 794 Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED, "");
michael@0 795 } else {
michael@0 796 TelemetryTimestamps.add("sessionRestoreRestoring");
michael@0 797 this._restoreCount = aInitialState.windows ? aInitialState.windows.length : 0;
michael@0 798
michael@0 799 // global data must be restored before restoreWindow is called so that
michael@0 800 // it happens before observers are notified
michael@0 801 this._globalState.setFromState(aInitialState);
michael@0 802
michael@0 803 let overwrite = this._isCmdLineEmpty(aWindow, aInitialState);
michael@0 804 let options = {firstWindow: true, overwriteTabs: overwrite};
michael@0 805 this.restoreWindow(aWindow, aInitialState, options);
michael@0 806 }
michael@0 807 }
michael@0 808 else {
michael@0 809 // Nothing to restore, notify observers things are complete.
michael@0 810 Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED, "");
michael@0 811
michael@0 812 // The next delayed save request should execute immediately.
michael@0 813 SessionSaver.clearLastSaveTime();
michael@0 814 }
michael@0 815 }
michael@0 816 // this window was opened by _openWindowWithState
michael@0 817 else if (!this._isWindowLoaded(aWindow)) {
michael@0 818 let state = this._statesToRestore[aWindow.__SS_restoreID];
michael@0 819 let options = {overwriteTabs: true, isFollowUp: state.windows.length == 1};
michael@0 820 this.restoreWindow(aWindow, state, options);
michael@0 821 }
michael@0 822 // The user opened another, non-private window after starting up with
michael@0 823 // a single private one. Let's restore the session we actually wanted to
michael@0 824 // restore at startup.
michael@0 825 else if (this._deferredInitialState && !isPrivateWindow &&
michael@0 826 aWindow.toolbar.visible) {
michael@0 827
michael@0 828 // global data must be restored before restoreWindow is called so that
michael@0 829 // it happens before observers are notified
michael@0 830 this._globalState.setFromState(this._deferredInitialState);
michael@0 831
michael@0 832 this._restoreCount = this._deferredInitialState.windows ?
michael@0 833 this._deferredInitialState.windows.length : 0;
michael@0 834 this.restoreWindow(aWindow, this._deferredInitialState, {firstWindow: true});
michael@0 835 this._deferredInitialState = null;
michael@0 836 }
michael@0 837 else if (this._restoreLastWindow && aWindow.toolbar.visible &&
michael@0 838 this._closedWindows.length && !isPrivateWindow) {
michael@0 839
michael@0 840 // default to the most-recently closed window
michael@0 841 // don't use popup windows
michael@0 842 let closedWindowState = null;
michael@0 843 let closedWindowIndex;
michael@0 844 for (let i = 0; i < this._closedWindows.length; i++) {
michael@0 845 // Take the first non-popup, point our object at it, and break out.
michael@0 846 if (!this._closedWindows[i].isPopup) {
michael@0 847 closedWindowState = this._closedWindows[i];
michael@0 848 closedWindowIndex = i;
michael@0 849 break;
michael@0 850 }
michael@0 851 }
michael@0 852
michael@0 853 if (closedWindowState) {
michael@0 854 let newWindowState;
michael@0 855 #ifndef XP_MACOSX
michael@0 856 if (!this._doResumeSession()) {
michael@0 857 #endif
michael@0 858 // We want to split the window up into pinned tabs and unpinned tabs.
michael@0 859 // Pinned tabs should be restored. If there are any remaining tabs,
michael@0 860 // they should be added back to _closedWindows.
michael@0 861 // We'll cheat a little bit and reuse _prepDataForDeferredRestore
michael@0 862 // even though it wasn't built exactly for this.
michael@0 863 let [appTabsState, normalTabsState] =
michael@0 864 this._prepDataForDeferredRestore({ windows: [closedWindowState] });
michael@0 865
michael@0 866 // These are our pinned tabs, which we should restore
michael@0 867 if (appTabsState.windows.length) {
michael@0 868 newWindowState = appTabsState.windows[0];
michael@0 869 delete newWindowState.__lastSessionWindowID;
michael@0 870 }
michael@0 871
michael@0 872 // In case there were no unpinned tabs, remove the window from _closedWindows
michael@0 873 if (!normalTabsState.windows.length) {
michael@0 874 this._closedWindows.splice(closedWindowIndex, 1);
michael@0 875 }
michael@0 876 // Or update _closedWindows with the modified state
michael@0 877 else {
michael@0 878 delete normalTabsState.windows[0].__lastSessionWindowID;
michael@0 879 this._closedWindows[closedWindowIndex] = normalTabsState.windows[0];
michael@0 880 }
michael@0 881 #ifndef XP_MACOSX
michael@0 882 }
michael@0 883 else {
michael@0 884 // If we're just restoring the window, make sure it gets removed from
michael@0 885 // _closedWindows.
michael@0 886 this._closedWindows.splice(closedWindowIndex, 1);
michael@0 887 newWindowState = closedWindowState;
michael@0 888 delete newWindowState.hidden;
michael@0 889 }
michael@0 890 #endif
michael@0 891 if (newWindowState) {
michael@0 892 // Ensure that the window state isn't hidden
michael@0 893 this._restoreCount = 1;
michael@0 894 let state = { windows: [newWindowState] };
michael@0 895 let options = {overwriteTabs: this._isCmdLineEmpty(aWindow, state)};
michael@0 896 this.restoreWindow(aWindow, state, options);
michael@0 897 }
michael@0 898 }
michael@0 899 // we actually restored the session just now.
michael@0 900 this._prefBranch.setBoolPref("sessionstore.resume_session_once", false);
michael@0 901 }
michael@0 902 if (this._restoreLastWindow && aWindow.toolbar.visible) {
michael@0 903 // always reset (if not a popup window)
michael@0 904 // we don't want to restore a window directly after, for example,
michael@0 905 // undoCloseWindow was executed.
michael@0 906 this._restoreLastWindow = false;
michael@0 907 }
michael@0 908
michael@0 909 var tabbrowser = aWindow.gBrowser;
michael@0 910
michael@0 911 // add tab change listeners to all already existing tabs
michael@0 912 for (let i = 0; i < tabbrowser.tabs.length; i++) {
michael@0 913 this.onTabAdd(aWindow, tabbrowser.tabs[i], true);
michael@0 914 }
michael@0 915 // notification of tab add/remove/selection/show/hide
michael@0 916 TAB_EVENTS.forEach(function(aEvent) {
michael@0 917 tabbrowser.tabContainer.addEventListener(aEvent, this, true);
michael@0 918 }, this);
michael@0 919 },
michael@0 920
michael@0 921 /**
michael@0 922 * On window open
michael@0 923 * @param aWindow
michael@0 924 * Window reference
michael@0 925 */
michael@0 926 onOpen: function ssi_onOpen(aWindow) {
michael@0 927 let onload = () => {
michael@0 928 aWindow.removeEventListener("load", onload);
michael@0 929
michael@0 930 let windowType = aWindow.document.documentElement.getAttribute("windowtype");
michael@0 931
michael@0 932 // Ignore non-browser windows.
michael@0 933 if (windowType != "navigator:browser") {
michael@0 934 return;
michael@0 935 }
michael@0 936
michael@0 937 if (this._sessionInitialized) {
michael@0 938 this.onLoad(aWindow);
michael@0 939 return;
michael@0 940 }
michael@0 941
michael@0 942 // The very first window that is opened creates a promise that is then
michael@0 943 // re-used by all subsequent windows. The promise will be used to tell
michael@0 944 // when we're ready for initialization.
michael@0 945 if (!this._promiseReadyForInitialization) {
michael@0 946 let deferred = Promise.defer();
michael@0 947
michael@0 948 // Wait for the given window's delayed startup to be finished.
michael@0 949 Services.obs.addObserver(function obs(subject, topic) {
michael@0 950 if (aWindow == subject) {
michael@0 951 Services.obs.removeObserver(obs, topic);
michael@0 952 deferred.resolve();
michael@0 953 }
michael@0 954 }, "browser-delayed-startup-finished", false);
michael@0 955
michael@0 956 // We are ready for initialization as soon as the session file has been
michael@0 957 // read from disk and the initial window's delayed startup has finished.
michael@0 958 this._promiseReadyForInitialization =
michael@0 959 Promise.all([deferred.promise, gSessionStartup.onceInitialized]);
michael@0 960 }
michael@0 961
michael@0 962 // We can't call this.onLoad since initialization
michael@0 963 // hasn't completed, so we'll wait until it is done.
michael@0 964 // Even if additional windows are opened and wait
michael@0 965 // for initialization as well, the first opened
michael@0 966 // window should execute first, and this.onLoad
michael@0 967 // will be called with the initialState.
michael@0 968 this._promiseReadyForInitialization.then(() => {
michael@0 969 if (aWindow.closed) {
michael@0 970 return;
michael@0 971 }
michael@0 972
michael@0 973 if (this._sessionInitialized) {
michael@0 974 this.onLoad(aWindow);
michael@0 975 } else {
michael@0 976 let initialState = this.initSession();
michael@0 977 this._sessionInitialized = true;
michael@0 978 this.onLoad(aWindow, initialState);
michael@0 979
michael@0 980 // Let everyone know we're done.
michael@0 981 this._deferredInitialized.resolve();
michael@0 982 }
michael@0 983 }, console.error);
michael@0 984 };
michael@0 985
michael@0 986 aWindow.addEventListener("load", onload);
michael@0 987 },
michael@0 988
michael@0 989 /**
michael@0 990 * On window close...
michael@0 991 * - remove event listeners from tabs
michael@0 992 * - save all window data
michael@0 993 * @param aWindow
michael@0 994 * Window reference
michael@0 995 */
michael@0 996 onClose: function ssi_onClose(aWindow) {
michael@0 997 // this window was about to be restored - conserve its original data, if any
michael@0 998 let isFullyLoaded = this._isWindowLoaded(aWindow);
michael@0 999 if (!isFullyLoaded) {
michael@0 1000 if (!aWindow.__SSi) {
michael@0 1001 aWindow.__SSi = this._generateWindowID();
michael@0 1002 }
michael@0 1003
michael@0 1004 this._windows[aWindow.__SSi] = this._statesToRestore[aWindow.__SS_restoreID];
michael@0 1005 delete this._statesToRestore[aWindow.__SS_restoreID];
michael@0 1006 delete aWindow.__SS_restoreID;
michael@0 1007 }
michael@0 1008
michael@0 1009 // ignore windows not tracked by SessionStore
michael@0 1010 if (!aWindow.__SSi || !this._windows[aWindow.__SSi]) {
michael@0 1011 return;
michael@0 1012 }
michael@0 1013
michael@0 1014 // notify that the session store will stop tracking this window so that
michael@0 1015 // extensions can store any data about this window in session store before
michael@0 1016 // that's not possible anymore
michael@0 1017 let event = aWindow.document.createEvent("Events");
michael@0 1018 event.initEvent("SSWindowClosing", true, false);
michael@0 1019 aWindow.dispatchEvent(event);
michael@0 1020
michael@0 1021 if (this.windowToFocus && this.windowToFocus == aWindow) {
michael@0 1022 delete this.windowToFocus;
michael@0 1023 }
michael@0 1024
michael@0 1025 var tabbrowser = aWindow.gBrowser;
michael@0 1026
michael@0 1027 TAB_EVENTS.forEach(function(aEvent) {
michael@0 1028 tabbrowser.tabContainer.removeEventListener(aEvent, this, true);
michael@0 1029 }, this);
michael@0 1030
michael@0 1031 let winData = this._windows[aWindow.__SSi];
michael@0 1032
michael@0 1033 // Collect window data only when *not* closed during shutdown.
michael@0 1034 if (this._loadState == STATE_RUNNING) {
michael@0 1035 // Flush all data queued in the content script before the window is gone.
michael@0 1036 TabState.flushWindow(aWindow);
michael@0 1037
michael@0 1038 // update all window data for a last time
michael@0 1039 this._collectWindowData(aWindow);
michael@0 1040
michael@0 1041 if (isFullyLoaded) {
michael@0 1042 winData.title = aWindow.content.document.title || tabbrowser.selectedTab.label;
michael@0 1043 winData.title = this._replaceLoadingTitle(winData.title, tabbrowser,
michael@0 1044 tabbrowser.selectedTab);
michael@0 1045 SessionCookies.update([winData]);
michael@0 1046 }
michael@0 1047
michael@0 1048 #ifndef XP_MACOSX
michael@0 1049 // Until we decide otherwise elsewhere, this window is part of a series
michael@0 1050 // of closing windows to quit.
michael@0 1051 winData._shouldRestore = true;
michael@0 1052 #endif
michael@0 1053
michael@0 1054 // Store the window's close date to figure out when each individual tab
michael@0 1055 // was closed. This timestamp should allow re-arranging data based on how
michael@0 1056 // recently something was closed.
michael@0 1057 winData.closedAt = Date.now();
michael@0 1058
michael@0 1059 // Save non-private windows if they have at
michael@0 1060 // least one saveable tab or are the last window.
michael@0 1061 if (!winData.isPrivate) {
michael@0 1062 // Remove any open private tabs the window may contain.
michael@0 1063 PrivacyFilter.filterPrivateTabs(winData);
michael@0 1064
michael@0 1065 // Determine whether the window has any tabs worth saving.
michael@0 1066 let hasSaveableTabs = winData.tabs.some(this._shouldSaveTabState);
michael@0 1067
michael@0 1068 // When closing windows one after the other until Firefox quits, we
michael@0 1069 // will move those closed in series back to the "open windows" bucket
michael@0 1070 // before writing to disk. If however there is only a single window
michael@0 1071 // with tabs we deem not worth saving then we might end up with a
michael@0 1072 // random closed or even a pop-up window re-opened. To prevent that
michael@0 1073 // we explicitly allow saving an "empty" window state.
michael@0 1074 let isLastWindow =
michael@0 1075 Object.keys(this._windows).length == 1 &&
michael@0 1076 !this._closedWindows.some(win => win._shouldRestore || false);
michael@0 1077
michael@0 1078 if (hasSaveableTabs || isLastWindow) {
michael@0 1079 // we don't want to save the busy state
michael@0 1080 delete winData.busy;
michael@0 1081
michael@0 1082 this._closedWindows.unshift(winData);
michael@0 1083 this._capClosedWindows();
michael@0 1084 }
michael@0 1085 }
michael@0 1086
michael@0 1087 // clear this window from the list
michael@0 1088 delete this._windows[aWindow.__SSi];
michael@0 1089
michael@0 1090 // save the state without this window to disk
michael@0 1091 this.saveStateDelayed();
michael@0 1092 }
michael@0 1093
michael@0 1094 for (let i = 0; i < tabbrowser.tabs.length; i++) {
michael@0 1095 this.onTabRemove(aWindow, tabbrowser.tabs[i], true);
michael@0 1096 }
michael@0 1097
michael@0 1098 // Cache the window state until it is completely gone.
michael@0 1099 DyingWindowCache.set(aWindow, winData);
michael@0 1100
michael@0 1101 let mm = aWindow.messageManager;
michael@0 1102 MESSAGES.forEach(msg => mm.removeMessageListener(msg, this));
michael@0 1103
michael@0 1104 delete aWindow.__SSi;
michael@0 1105 },
michael@0 1106
michael@0 1107 /**
michael@0 1108 * On quit application requested
michael@0 1109 */
michael@0 1110 onQuitApplicationRequested: function ssi_onQuitApplicationRequested() {
michael@0 1111 // get a current snapshot of all windows
michael@0 1112 this._forEachBrowserWindow(function(aWindow) {
michael@0 1113 // Flush all data queued in the content script to not lose it when
michael@0 1114 // shutting down.
michael@0 1115 TabState.flushWindow(aWindow);
michael@0 1116 this._collectWindowData(aWindow);
michael@0 1117 });
michael@0 1118 // we must cache this because _getMostRecentBrowserWindow will always
michael@0 1119 // return null by the time quit-application occurs
michael@0 1120 var activeWindow = this._getMostRecentBrowserWindow();
michael@0 1121 if (activeWindow)
michael@0 1122 this.activeWindowSSiCache = activeWindow.__SSi || "";
michael@0 1123 DirtyWindows.clear();
michael@0 1124 },
michael@0 1125
michael@0 1126 /**
michael@0 1127 * On quit application granted
michael@0 1128 */
michael@0 1129 onQuitApplicationGranted: function ssi_onQuitApplicationGranted() {
michael@0 1130 // freeze the data at what we've got (ignoring closing windows)
michael@0 1131 this._loadState = STATE_QUITTING;
michael@0 1132 },
michael@0 1133
michael@0 1134 /**
michael@0 1135 * On last browser window close
michael@0 1136 */
michael@0 1137 onLastWindowCloseGranted: function ssi_onLastWindowCloseGranted() {
michael@0 1138 // last browser window is quitting.
michael@0 1139 // remember to restore the last window when another browser window is opened
michael@0 1140 // do not account for pref(resume_session_once) at this point, as it might be
michael@0 1141 // set by another observer getting this notice after us
michael@0 1142 this._restoreLastWindow = true;
michael@0 1143 },
michael@0 1144
michael@0 1145 /**
michael@0 1146 * On quitting application
michael@0 1147 * @param aData
michael@0 1148 * String type of quitting
michael@0 1149 */
michael@0 1150 onQuitApplication: function ssi_onQuitApplication(aData) {
michael@0 1151 if (aData == "restart") {
michael@0 1152 this._prefBranch.setBoolPref("sessionstore.resume_session_once", true);
michael@0 1153 // The browser:purge-session-history notification fires after the
michael@0 1154 // quit-application notification so unregister the
michael@0 1155 // browser:purge-session-history notification to prevent clearing
michael@0 1156 // session data on disk on a restart. It is also unnecessary to
michael@0 1157 // perform any other sanitization processing on a restart as the
michael@0 1158 // browser is about to exit anyway.
michael@0 1159 Services.obs.removeObserver(this, "browser:purge-session-history");
michael@0 1160 }
michael@0 1161
michael@0 1162 if (aData != "restart") {
michael@0 1163 // Throw away the previous session on shutdown
michael@0 1164 LastSession.clear();
michael@0 1165 }
michael@0 1166
michael@0 1167 this._loadState = STATE_QUITTING; // just to be sure
michael@0 1168 this._uninit();
michael@0 1169 },
michael@0 1170
michael@0 1171 /**
michael@0 1172 * On purge of session history
michael@0 1173 */
michael@0 1174 onPurgeSessionHistory: function ssi_onPurgeSessionHistory() {
michael@0 1175 SessionFile.wipe();
michael@0 1176 // If the browser is shutting down, simply return after clearing the
michael@0 1177 // session data on disk as this notification fires after the
michael@0 1178 // quit-application notification so the browser is about to exit.
michael@0 1179 if (this._loadState == STATE_QUITTING)
michael@0 1180 return;
michael@0 1181 LastSession.clear();
michael@0 1182 let openWindows = {};
michael@0 1183 this._forEachBrowserWindow(function(aWindow) {
michael@0 1184 Array.forEach(aWindow.gBrowser.tabs, function(aTab) {
michael@0 1185 delete aTab.linkedBrowser.__SS_data;
michael@0 1186 if (aTab.linkedBrowser.__SS_restoreState)
michael@0 1187 this._resetTabRestoringState(aTab);
michael@0 1188 }, this);
michael@0 1189 openWindows[aWindow.__SSi] = true;
michael@0 1190 });
michael@0 1191 // also clear all data about closed tabs and windows
michael@0 1192 for (let ix in this._windows) {
michael@0 1193 if (ix in openWindows) {
michael@0 1194 this._windows[ix]._closedTabs = [];
michael@0 1195 } else {
michael@0 1196 delete this._windows[ix];
michael@0 1197 }
michael@0 1198 }
michael@0 1199 // also clear all data about closed windows
michael@0 1200 this._closedWindows = [];
michael@0 1201 // give the tabbrowsers a chance to clear their histories first
michael@0 1202 var win = this._getMostRecentBrowserWindow();
michael@0 1203 if (win) {
michael@0 1204 win.setTimeout(() => SessionSaver.run(), 0);
michael@0 1205 } else if (this._loadState == STATE_RUNNING) {
michael@0 1206 SessionSaver.run();
michael@0 1207 }
michael@0 1208
michael@0 1209 this._clearRestoringWindows();
michael@0 1210 },
michael@0 1211
michael@0 1212 /**
michael@0 1213 * On purge of domain data
michael@0 1214 * @param aData
michael@0 1215 * String domain data
michael@0 1216 */
michael@0 1217 onPurgeDomainData: function ssi_onPurgeDomainData(aData) {
michael@0 1218 // does a session history entry contain a url for the given domain?
michael@0 1219 function containsDomain(aEntry) {
michael@0 1220 if (Utils.hasRootDomain(aEntry.url, aData)) {
michael@0 1221 return true;
michael@0 1222 }
michael@0 1223 return aEntry.children && aEntry.children.some(containsDomain, this);
michael@0 1224 }
michael@0 1225 // remove all closed tabs containing a reference to the given domain
michael@0 1226 for (let ix in this._windows) {
michael@0 1227 let closedTabs = this._windows[ix]._closedTabs;
michael@0 1228 for (let i = closedTabs.length - 1; i >= 0; i--) {
michael@0 1229 if (closedTabs[i].state.entries.some(containsDomain, this))
michael@0 1230 closedTabs.splice(i, 1);
michael@0 1231 }
michael@0 1232 }
michael@0 1233 // remove all open & closed tabs containing a reference to the given
michael@0 1234 // domain in closed windows
michael@0 1235 for (let ix = this._closedWindows.length - 1; ix >= 0; ix--) {
michael@0 1236 let closedTabs = this._closedWindows[ix]._closedTabs;
michael@0 1237 let openTabs = this._closedWindows[ix].tabs;
michael@0 1238 let openTabCount = openTabs.length;
michael@0 1239 for (let i = closedTabs.length - 1; i >= 0; i--)
michael@0 1240 if (closedTabs[i].state.entries.some(containsDomain, this))
michael@0 1241 closedTabs.splice(i, 1);
michael@0 1242 for (let j = openTabs.length - 1; j >= 0; j--) {
michael@0 1243 if (openTabs[j].entries.some(containsDomain, this)) {
michael@0 1244 openTabs.splice(j, 1);
michael@0 1245 if (this._closedWindows[ix].selected > j)
michael@0 1246 this._closedWindows[ix].selected--;
michael@0 1247 }
michael@0 1248 }
michael@0 1249 if (openTabs.length == 0) {
michael@0 1250 this._closedWindows.splice(ix, 1);
michael@0 1251 }
michael@0 1252 else if (openTabs.length != openTabCount) {
michael@0 1253 // Adjust the window's title if we removed an open tab
michael@0 1254 let selectedTab = openTabs[this._closedWindows[ix].selected - 1];
michael@0 1255 // some duplication from restoreHistory - make sure we get the correct title
michael@0 1256 let activeIndex = (selectedTab.index || selectedTab.entries.length) - 1;
michael@0 1257 if (activeIndex >= selectedTab.entries.length)
michael@0 1258 activeIndex = selectedTab.entries.length - 1;
michael@0 1259 this._closedWindows[ix].title = selectedTab.entries[activeIndex].title;
michael@0 1260 }
michael@0 1261 }
michael@0 1262
michael@0 1263 if (this._loadState == STATE_RUNNING) {
michael@0 1264 SessionSaver.run();
michael@0 1265 }
michael@0 1266
michael@0 1267 this._clearRestoringWindows();
michael@0 1268 },
michael@0 1269
michael@0 1270 /**
michael@0 1271 * On preference change
michael@0 1272 * @param aData
michael@0 1273 * String preference changed
michael@0 1274 */
michael@0 1275 onPrefChange: function ssi_onPrefChange(aData) {
michael@0 1276 switch (aData) {
michael@0 1277 // if the user decreases the max number of closed tabs they want
michael@0 1278 // preserved update our internal states to match that max
michael@0 1279 case "sessionstore.max_tabs_undo":
michael@0 1280 this._max_tabs_undo = this._prefBranch.getIntPref("sessionstore.max_tabs_undo");
michael@0 1281 for (let ix in this._windows) {
michael@0 1282 this._windows[ix]._closedTabs.splice(this._max_tabs_undo, this._windows[ix]._closedTabs.length);
michael@0 1283 }
michael@0 1284 break;
michael@0 1285 case "sessionstore.max_windows_undo":
michael@0 1286 this._max_windows_undo = this._prefBranch.getIntPref("sessionstore.max_windows_undo");
michael@0 1287 this._capClosedWindows();
michael@0 1288 break;
michael@0 1289 }
michael@0 1290 },
michael@0 1291
michael@0 1292 /**
michael@0 1293 * set up listeners for a new tab
michael@0 1294 * @param aWindow
michael@0 1295 * Window reference
michael@0 1296 * @param aTab
michael@0 1297 * Tab reference
michael@0 1298 * @param aNoNotification
michael@0 1299 * bool Do not save state if we're updating an existing tab
michael@0 1300 */
michael@0 1301 onTabAdd: function ssi_onTabAdd(aWindow, aTab, aNoNotification) {
michael@0 1302 if (!aNoNotification) {
michael@0 1303 this.saveStateDelayed(aWindow);
michael@0 1304 }
michael@0 1305 },
michael@0 1306
michael@0 1307 /**
michael@0 1308 * remove listeners for a tab
michael@0 1309 * @param aWindow
michael@0 1310 * Window reference
michael@0 1311 * @param aTab
michael@0 1312 * Tab reference
michael@0 1313 * @param aNoNotification
michael@0 1314 * bool Do not save state if we're updating an existing tab
michael@0 1315 */
michael@0 1316 onTabRemove: function ssi_onTabRemove(aWindow, aTab, aNoNotification) {
michael@0 1317 let browser = aTab.linkedBrowser;
michael@0 1318 delete browser.__SS_data;
michael@0 1319
michael@0 1320 // If this tab was in the middle of restoring or still needs to be restored,
michael@0 1321 // we need to reset that state. If the tab was restoring, we will attempt to
michael@0 1322 // restore the next tab.
michael@0 1323 let previousState = browser.__SS_restoreState;
michael@0 1324 if (previousState) {
michael@0 1325 this._resetTabRestoringState(aTab);
michael@0 1326 if (previousState == TAB_STATE_RESTORING)
michael@0 1327 this.restoreNextTab();
michael@0 1328 }
michael@0 1329
michael@0 1330 if (!aNoNotification) {
michael@0 1331 this.saveStateDelayed(aWindow);
michael@0 1332 }
michael@0 1333 },
michael@0 1334
michael@0 1335 /**
michael@0 1336 * When a tab closes, collect its properties
michael@0 1337 * @param aWindow
michael@0 1338 * Window reference
michael@0 1339 * @param aTab
michael@0 1340 * Tab reference
michael@0 1341 */
michael@0 1342 onTabClose: function ssi_onTabClose(aWindow, aTab) {
michael@0 1343 // notify the tabbrowser that the tab state will be retrieved for the last time
michael@0 1344 // (so that extension authors can easily set data on soon-to-be-closed tabs)
michael@0 1345 var event = aWindow.document.createEvent("Events");
michael@0 1346 event.initEvent("SSTabClosing", true, false);
michael@0 1347 aTab.dispatchEvent(event);
michael@0 1348
michael@0 1349 // don't update our internal state if we don't have to
michael@0 1350 if (this._max_tabs_undo == 0) {
michael@0 1351 return;
michael@0 1352 }
michael@0 1353
michael@0 1354 // Flush all data queued in the content script before the tab is gone.
michael@0 1355 TabState.flush(aTab.linkedBrowser);
michael@0 1356
michael@0 1357 // Get the latest data for this tab (generally, from the cache)
michael@0 1358 let tabState = TabState.collect(aTab);
michael@0 1359
michael@0 1360 // Don't save private tabs
michael@0 1361 let isPrivateWindow = PrivateBrowsingUtils.isWindowPrivate(aWindow);
michael@0 1362 if (!isPrivateWindow && tabState.isPrivate) {
michael@0 1363 return;
michael@0 1364 }
michael@0 1365
michael@0 1366 // store closed-tab data for undo
michael@0 1367 if (this._shouldSaveTabState(tabState)) {
michael@0 1368 let tabTitle = aTab.label;
michael@0 1369 let tabbrowser = aWindow.gBrowser;
michael@0 1370 tabTitle = this._replaceLoadingTitle(tabTitle, tabbrowser, aTab);
michael@0 1371
michael@0 1372 this._windows[aWindow.__SSi]._closedTabs.unshift({
michael@0 1373 state: tabState,
michael@0 1374 title: tabTitle,
michael@0 1375 image: tabbrowser.getIcon(aTab),
michael@0 1376 pos: aTab._tPos,
michael@0 1377 closedAt: Date.now()
michael@0 1378 });
michael@0 1379 var length = this._windows[aWindow.__SSi]._closedTabs.length;
michael@0 1380 if (length > this._max_tabs_undo)
michael@0 1381 this._windows[aWindow.__SSi]._closedTabs.splice(this._max_tabs_undo, length - this._max_tabs_undo);
michael@0 1382 }
michael@0 1383 },
michael@0 1384
michael@0 1385 /**
michael@0 1386 * When a tab is selected, save session data
michael@0 1387 * @param aWindow
michael@0 1388 * Window reference
michael@0 1389 */
michael@0 1390 onTabSelect: function ssi_onTabSelect(aWindow) {
michael@0 1391 if (this._loadState == STATE_RUNNING) {
michael@0 1392 this._windows[aWindow.__SSi].selected = aWindow.gBrowser.tabContainer.selectedIndex;
michael@0 1393
michael@0 1394 let tab = aWindow.gBrowser.selectedTab;
michael@0 1395 // If __SS_restoreState is still on the browser and it is
michael@0 1396 // TAB_STATE_NEEDS_RESTORE, then then we haven't restored
michael@0 1397 // this tab yet. Explicitly call restoreTabContent to kick off the restore.
michael@0 1398 if (tab.linkedBrowser.__SS_restoreState &&
michael@0 1399 tab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE)
michael@0 1400 this.restoreTabContent(tab);
michael@0 1401 }
michael@0 1402 },
michael@0 1403
michael@0 1404 onTabShow: function ssi_onTabShow(aWindow, aTab) {
michael@0 1405 // If the tab hasn't been restored yet, move it into the right bucket
michael@0 1406 if (aTab.linkedBrowser.__SS_restoreState &&
michael@0 1407 aTab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
michael@0 1408 TabRestoreQueue.hiddenToVisible(aTab);
michael@0 1409
michael@0 1410 // let's kick off tab restoration again to ensure this tab gets restored
michael@0 1411 // with "restore_hidden_tabs" == false (now that it has become visible)
michael@0 1412 this.restoreNextTab();
michael@0 1413 }
michael@0 1414
michael@0 1415 // Default delay of 2 seconds gives enough time to catch multiple TabShow
michael@0 1416 // events due to changing groups in Panorama.
michael@0 1417 this.saveStateDelayed(aWindow);
michael@0 1418 },
michael@0 1419
michael@0 1420 onTabHide: function ssi_onTabHide(aWindow, aTab) {
michael@0 1421 // If the tab hasn't been restored yet, move it into the right bucket
michael@0 1422 if (aTab.linkedBrowser.__SS_restoreState &&
michael@0 1423 aTab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
michael@0 1424 TabRestoreQueue.visibleToHidden(aTab);
michael@0 1425 }
michael@0 1426
michael@0 1427 // Default delay of 2 seconds gives enough time to catch multiple TabHide
michael@0 1428 // events due to changing groups in Panorama.
michael@0 1429 this.saveStateDelayed(aWindow);
michael@0 1430 },
michael@0 1431
michael@0 1432 onGatherTelemetry: function() {
michael@0 1433 // On the first gather-telemetry notification of the session,
michael@0 1434 // gather telemetry data.
michael@0 1435 Services.obs.removeObserver(this, "gather-telemetry");
michael@0 1436 let stateString = SessionStore.getBrowserState();
michael@0 1437 return SessionFile.gatherTelemetry(stateString);
michael@0 1438 },
michael@0 1439
michael@0 1440 /* ........ nsISessionStore API .............. */
michael@0 1441
michael@0 1442 getBrowserState: function ssi_getBrowserState() {
michael@0 1443 let state = this.getCurrentState();
michael@0 1444
michael@0 1445 // Don't include the last session state in getBrowserState().
michael@0 1446 delete state.lastSessionState;
michael@0 1447
michael@0 1448 // Don't include any deferred initial state.
michael@0 1449 delete state.deferredInitialState;
michael@0 1450
michael@0 1451 return this._toJSONString(state);
michael@0 1452 },
michael@0 1453
michael@0 1454 setBrowserState: function ssi_setBrowserState(aState) {
michael@0 1455 this._handleClosedWindows();
michael@0 1456
michael@0 1457 try {
michael@0 1458 var state = JSON.parse(aState);
michael@0 1459 }
michael@0 1460 catch (ex) { /* invalid state object - don't restore anything */ }
michael@0 1461 if (!state) {
michael@0 1462 throw Components.Exception("Invalid state string: not JSON", Cr.NS_ERROR_INVALID_ARG);
michael@0 1463 }
michael@0 1464 if (!state.windows) {
michael@0 1465 throw Components.Exception("No windows", Cr.NS_ERROR_INVALID_ARG);
michael@0 1466 }
michael@0 1467
michael@0 1468 this._browserSetState = true;
michael@0 1469
michael@0 1470 // Make sure the priority queue is emptied out
michael@0 1471 this._resetRestoringState();
michael@0 1472
michael@0 1473 var window = this._getMostRecentBrowserWindow();
michael@0 1474 if (!window) {
michael@0 1475 this._restoreCount = 1;
michael@0 1476 this._openWindowWithState(state);
michael@0 1477 return;
michael@0 1478 }
michael@0 1479
michael@0 1480 // close all other browser windows
michael@0 1481 this._forEachBrowserWindow(function(aWindow) {
michael@0 1482 if (aWindow != window) {
michael@0 1483 aWindow.close();
michael@0 1484 this.onClose(aWindow);
michael@0 1485 }
michael@0 1486 });
michael@0 1487
michael@0 1488 // make sure closed window data isn't kept
michael@0 1489 this._closedWindows = [];
michael@0 1490
michael@0 1491 // determine how many windows are meant to be restored
michael@0 1492 this._restoreCount = state.windows ? state.windows.length : 0;
michael@0 1493
michael@0 1494 // global data must be restored before restoreWindow is called so that
michael@0 1495 // it happens before observers are notified
michael@0 1496 this._globalState.setFromState(state);
michael@0 1497
michael@0 1498 // restore to the given state
michael@0 1499 this.restoreWindow(window, state, {overwriteTabs: true});
michael@0 1500 },
michael@0 1501
michael@0 1502 getWindowState: function ssi_getWindowState(aWindow) {
michael@0 1503 if ("__SSi" in aWindow) {
michael@0 1504 return this._toJSONString(this._getWindowState(aWindow));
michael@0 1505 }
michael@0 1506
michael@0 1507 if (DyingWindowCache.has(aWindow)) {
michael@0 1508 let data = DyingWindowCache.get(aWindow);
michael@0 1509 return this._toJSONString({ windows: [data] });
michael@0 1510 }
michael@0 1511
michael@0 1512 throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
michael@0 1513 },
michael@0 1514
michael@0 1515 setWindowState: function ssi_setWindowState(aWindow, aState, aOverwrite) {
michael@0 1516 if (!aWindow.__SSi) {
michael@0 1517 throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
michael@0 1518 }
michael@0 1519
michael@0 1520 this.restoreWindow(aWindow, aState, {overwriteTabs: aOverwrite});
michael@0 1521 },
michael@0 1522
michael@0 1523 getTabState: function ssi_getTabState(aTab) {
michael@0 1524 if (!aTab.ownerDocument) {
michael@0 1525 throw Components.Exception("Invalid tab object: no ownerDocument", Cr.NS_ERROR_INVALID_ARG);
michael@0 1526 }
michael@0 1527 if (!aTab.ownerDocument.defaultView.__SSi) {
michael@0 1528 throw Components.Exception("Default view is not tracked", Cr.NS_ERROR_INVALID_ARG);
michael@0 1529 }
michael@0 1530
michael@0 1531 let tabState = TabState.collect(aTab);
michael@0 1532
michael@0 1533 return this._toJSONString(tabState);
michael@0 1534 },
michael@0 1535
michael@0 1536 setTabState: function ssi_setTabState(aTab, aState) {
michael@0 1537 // Remove the tab state from the cache.
michael@0 1538 // Note that we cannot simply replace the contents of the cache
michael@0 1539 // as |aState| can be an incomplete state that will be completed
michael@0 1540 // by |restoreTabs|.
michael@0 1541 let tabState = JSON.parse(aState);
michael@0 1542 if (!tabState) {
michael@0 1543 throw Components.Exception("Invalid state string: not JSON", Cr.NS_ERROR_INVALID_ARG);
michael@0 1544 }
michael@0 1545 if (typeof tabState != "object") {
michael@0 1546 throw Components.Exception("Not an object", Cr.NS_ERROR_INVALID_ARG);
michael@0 1547 }
michael@0 1548 if (!("entries" in tabState)) {
michael@0 1549 throw Components.Exception("Invalid state object: no entries", Cr.NS_ERROR_INVALID_ARG);
michael@0 1550 }
michael@0 1551 if (!aTab.ownerDocument) {
michael@0 1552 throw Components.Exception("Invalid tab object: no ownerDocument", Cr.NS_ERROR_INVALID_ARG);
michael@0 1553 }
michael@0 1554
michael@0 1555 let window = aTab.ownerDocument.defaultView;
michael@0 1556 if (!("__SSi" in window)) {
michael@0 1557 throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
michael@0 1558 }
michael@0 1559
michael@0 1560 if (aTab.linkedBrowser.__SS_restoreState) {
michael@0 1561 this._resetTabRestoringState(aTab);
michael@0 1562 }
michael@0 1563
michael@0 1564 this._setWindowStateBusy(window);
michael@0 1565 this.restoreTabs(window, [aTab], [tabState], 0);
michael@0 1566 },
michael@0 1567
michael@0 1568 duplicateTab: function ssi_duplicateTab(aWindow, aTab, aDelta = 0) {
michael@0 1569 if (!aTab.ownerDocument) {
michael@0 1570 throw Components.Exception("Invalid tab object: no ownerDocument", Cr.NS_ERROR_INVALID_ARG);
michael@0 1571 }
michael@0 1572 if (!aTab.ownerDocument.defaultView.__SSi) {
michael@0 1573 throw Components.Exception("Default view is not tracked", Cr.NS_ERROR_INVALID_ARG);
michael@0 1574 }
michael@0 1575 if (!aWindow.getBrowser) {
michael@0 1576 throw Components.Exception("Invalid window object: no getBrowser", Cr.NS_ERROR_INVALID_ARG);
michael@0 1577 }
michael@0 1578
michael@0 1579 // Flush all data queued in the content script because we will need that
michael@0 1580 // state to properly duplicate the given tab.
michael@0 1581 TabState.flush(aTab.linkedBrowser);
michael@0 1582
michael@0 1583 // Duplicate the tab state
michael@0 1584 let tabState = TabState.clone(aTab);
michael@0 1585
michael@0 1586 tabState.index += aDelta;
michael@0 1587 tabState.index = Math.max(1, Math.min(tabState.index, tabState.entries.length));
michael@0 1588 tabState.pinned = false;
michael@0 1589
michael@0 1590 this._setWindowStateBusy(aWindow);
michael@0 1591 let newTab = aTab == aWindow.gBrowser.selectedTab ?
michael@0 1592 aWindow.gBrowser.addTab(null, {relatedToCurrent: true, ownerTab: aTab}) :
michael@0 1593 aWindow.gBrowser.addTab();
michael@0 1594
michael@0 1595 this.restoreTabs(aWindow, [newTab], [tabState], 0,
michael@0 1596 true /* Load this tab right away. */);
michael@0 1597
michael@0 1598 return newTab;
michael@0 1599 },
michael@0 1600
michael@0 1601 getClosedTabCount: function ssi_getClosedTabCount(aWindow) {
michael@0 1602 if ("__SSi" in aWindow) {
michael@0 1603 return this._windows[aWindow.__SSi]._closedTabs.length;
michael@0 1604 }
michael@0 1605
michael@0 1606 if (!DyingWindowCache.has(aWindow)) {
michael@0 1607 throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
michael@0 1608 }
michael@0 1609
michael@0 1610 return DyingWindowCache.get(aWindow)._closedTabs.length;
michael@0 1611 },
michael@0 1612
michael@0 1613 getClosedTabData: function ssi_getClosedTabDataAt(aWindow) {
michael@0 1614 if ("__SSi" in aWindow) {
michael@0 1615 return this._toJSONString(this._windows[aWindow.__SSi]._closedTabs);
michael@0 1616 }
michael@0 1617
michael@0 1618 if (!DyingWindowCache.has(aWindow)) {
michael@0 1619 throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
michael@0 1620 }
michael@0 1621
michael@0 1622 let data = DyingWindowCache.get(aWindow);
michael@0 1623 return this._toJSONString(data._closedTabs);
michael@0 1624 },
michael@0 1625
michael@0 1626 undoCloseTab: function ssi_undoCloseTab(aWindow, aIndex) {
michael@0 1627 if (!aWindow.__SSi) {
michael@0 1628 throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
michael@0 1629 }
michael@0 1630
michael@0 1631 var closedTabs = this._windows[aWindow.__SSi]._closedTabs;
michael@0 1632
michael@0 1633 // default to the most-recently closed tab
michael@0 1634 aIndex = aIndex || 0;
michael@0 1635 if (!(aIndex in closedTabs)) {
michael@0 1636 throw Components.Exception("Invalid index: not in the closed tabs", Cr.NS_ERROR_INVALID_ARG);
michael@0 1637 }
michael@0 1638
michael@0 1639 // fetch the data of closed tab, while removing it from the array
michael@0 1640 let closedTab = closedTabs.splice(aIndex, 1).shift();
michael@0 1641 let closedTabState = closedTab.state;
michael@0 1642
michael@0 1643 this._setWindowStateBusy(aWindow);
michael@0 1644 // create a new tab
michael@0 1645 let tabbrowser = aWindow.gBrowser;
michael@0 1646 let tab = tabbrowser.addTab();
michael@0 1647
michael@0 1648 // restore tab content
michael@0 1649 this.restoreTabs(aWindow, [tab], [closedTabState], 1);
michael@0 1650
michael@0 1651 // restore the tab's position
michael@0 1652 tabbrowser.moveTabTo(tab, closedTab.pos);
michael@0 1653
michael@0 1654 // focus the tab's content area (bug 342432)
michael@0 1655 tab.linkedBrowser.focus();
michael@0 1656
michael@0 1657 return tab;
michael@0 1658 },
michael@0 1659
michael@0 1660 forgetClosedTab: function ssi_forgetClosedTab(aWindow, aIndex) {
michael@0 1661 if (!aWindow.__SSi) {
michael@0 1662 throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
michael@0 1663 }
michael@0 1664
michael@0 1665 var closedTabs = this._windows[aWindow.__SSi]._closedTabs;
michael@0 1666
michael@0 1667 // default to the most-recently closed tab
michael@0 1668 aIndex = aIndex || 0;
michael@0 1669 if (!(aIndex in closedTabs)) {
michael@0 1670 throw Components.Exception("Invalid index: not in the closed tabs", Cr.NS_ERROR_INVALID_ARG);
michael@0 1671 }
michael@0 1672
michael@0 1673 // remove closed tab from the array
michael@0 1674 closedTabs.splice(aIndex, 1);
michael@0 1675 },
michael@0 1676
michael@0 1677 getClosedWindowCount: function ssi_getClosedWindowCount() {
michael@0 1678 return this._closedWindows.length;
michael@0 1679 },
michael@0 1680
michael@0 1681 getClosedWindowData: function ssi_getClosedWindowData() {
michael@0 1682 return this._toJSONString(this._closedWindows);
michael@0 1683 },
michael@0 1684
michael@0 1685 undoCloseWindow: function ssi_undoCloseWindow(aIndex) {
michael@0 1686 if (!(aIndex in this._closedWindows)) {
michael@0 1687 throw Components.Exception("Invalid index: not in the closed windows", Cr.NS_ERROR_INVALID_ARG);
michael@0 1688 }
michael@0 1689
michael@0 1690 // reopen the window
michael@0 1691 let state = { windows: this._closedWindows.splice(aIndex, 1) };
michael@0 1692 let window = this._openWindowWithState(state);
michael@0 1693 this.windowToFocus = window;
michael@0 1694 return window;
michael@0 1695 },
michael@0 1696
michael@0 1697 forgetClosedWindow: function ssi_forgetClosedWindow(aIndex) {
michael@0 1698 // default to the most-recently closed window
michael@0 1699 aIndex = aIndex || 0;
michael@0 1700 if (!(aIndex in this._closedWindows)) {
michael@0 1701 throw Components.Exception("Invalid index: not in the closed windows", Cr.NS_ERROR_INVALID_ARG);
michael@0 1702 }
michael@0 1703
michael@0 1704 // remove closed window from the array
michael@0 1705 this._closedWindows.splice(aIndex, 1);
michael@0 1706 },
michael@0 1707
michael@0 1708 getWindowValue: function ssi_getWindowValue(aWindow, aKey) {
michael@0 1709 if ("__SSi" in aWindow) {
michael@0 1710 var data = this._windows[aWindow.__SSi].extData || {};
michael@0 1711 return data[aKey] || "";
michael@0 1712 }
michael@0 1713
michael@0 1714 if (DyingWindowCache.has(aWindow)) {
michael@0 1715 let data = DyingWindowCache.get(aWindow).extData || {};
michael@0 1716 return data[aKey] || "";
michael@0 1717 }
michael@0 1718
michael@0 1719 throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
michael@0 1720 },
michael@0 1721
michael@0 1722 setWindowValue: function ssi_setWindowValue(aWindow, aKey, aStringValue) {
michael@0 1723 if (typeof aStringValue != "string") {
michael@0 1724 throw new TypeError("setWindowValue only accepts string values");
michael@0 1725 }
michael@0 1726
michael@0 1727 if (!("__SSi" in aWindow)) {
michael@0 1728 throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
michael@0 1729 }
michael@0 1730 if (!this._windows[aWindow.__SSi].extData) {
michael@0 1731 this._windows[aWindow.__SSi].extData = {};
michael@0 1732 }
michael@0 1733 this._windows[aWindow.__SSi].extData[aKey] = aStringValue;
michael@0 1734 this.saveStateDelayed(aWindow);
michael@0 1735 },
michael@0 1736
michael@0 1737 deleteWindowValue: function ssi_deleteWindowValue(aWindow, aKey) {
michael@0 1738 if (aWindow.__SSi && this._windows[aWindow.__SSi].extData &&
michael@0 1739 this._windows[aWindow.__SSi].extData[aKey])
michael@0 1740 delete this._windows[aWindow.__SSi].extData[aKey];
michael@0 1741 this.saveStateDelayed(aWindow);
michael@0 1742 },
michael@0 1743
michael@0 1744 getTabValue: function ssi_getTabValue(aTab, aKey) {
michael@0 1745 let data = {};
michael@0 1746 if (aTab.__SS_extdata) {
michael@0 1747 data = aTab.__SS_extdata;
michael@0 1748 }
michael@0 1749 else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) {
michael@0 1750 // If the tab hasn't been fully restored, get the data from the to-be-restored data
michael@0 1751 data = aTab.linkedBrowser.__SS_data.extData;
michael@0 1752 }
michael@0 1753 return data[aKey] || "";
michael@0 1754 },
michael@0 1755
michael@0 1756 setTabValue: function ssi_setTabValue(aTab, aKey, aStringValue) {
michael@0 1757 if (typeof aStringValue != "string") {
michael@0 1758 throw new TypeError("setTabValue only accepts string values");
michael@0 1759 }
michael@0 1760
michael@0 1761 // If the tab hasn't been restored, then set the data there, otherwise we
michael@0 1762 // could lose newly added data.
michael@0 1763 let saveTo;
michael@0 1764 if (aTab.__SS_extdata) {
michael@0 1765 saveTo = aTab.__SS_extdata;
michael@0 1766 }
michael@0 1767 else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) {
michael@0 1768 saveTo = aTab.linkedBrowser.__SS_data.extData;
michael@0 1769 }
michael@0 1770 else {
michael@0 1771 aTab.__SS_extdata = {};
michael@0 1772 saveTo = aTab.__SS_extdata;
michael@0 1773 }
michael@0 1774
michael@0 1775 saveTo[aKey] = aStringValue;
michael@0 1776 this.saveStateDelayed(aTab.ownerDocument.defaultView);
michael@0 1777 },
michael@0 1778
michael@0 1779 deleteTabValue: function ssi_deleteTabValue(aTab, aKey) {
michael@0 1780 // We want to make sure that if data is accessed early, we attempt to delete
michael@0 1781 // that data from __SS_data as well. Otherwise we'll throw in cases where
michael@0 1782 // data can be set or read.
michael@0 1783 let deleteFrom;
michael@0 1784 if (aTab.__SS_extdata) {
michael@0 1785 deleteFrom = aTab.__SS_extdata;
michael@0 1786 }
michael@0 1787 else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) {
michael@0 1788 deleteFrom = aTab.linkedBrowser.__SS_data.extData;
michael@0 1789 }
michael@0 1790
michael@0 1791 if (deleteFrom && aKey in deleteFrom) {
michael@0 1792 delete deleteFrom[aKey];
michael@0 1793 this.saveStateDelayed(aTab.ownerDocument.defaultView);
michael@0 1794 }
michael@0 1795 },
michael@0 1796
michael@0 1797 getGlobalValue: function ssi_getGlobalValue(aKey) {
michael@0 1798 return this._globalState.get(aKey);
michael@0 1799 },
michael@0 1800
michael@0 1801 setGlobalValue: function ssi_setGlobalValue(aKey, aStringValue) {
michael@0 1802 if (typeof aStringValue != "string") {
michael@0 1803 throw new TypeError("setGlobalValue only accepts string values");
michael@0 1804 }
michael@0 1805
michael@0 1806 this._globalState.set(aKey, aStringValue);
michael@0 1807 this.saveStateDelayed();
michael@0 1808 },
michael@0 1809
michael@0 1810 deleteGlobalValue: function ssi_deleteGlobalValue(aKey) {
michael@0 1811 this._globalState.delete(aKey);
michael@0 1812 this.saveStateDelayed();
michael@0 1813 },
michael@0 1814
michael@0 1815 persistTabAttribute: function ssi_persistTabAttribute(aName) {
michael@0 1816 if (TabAttributes.persist(aName)) {
michael@0 1817 this.saveStateDelayed();
michael@0 1818 }
michael@0 1819 },
michael@0 1820
michael@0 1821 /**
michael@0 1822 * Restores the session state stored in LastSession. This will attempt
michael@0 1823 * to merge data into the current session. If a window was opened at startup
michael@0 1824 * with pinned tab(s), then the remaining data from the previous session for
michael@0 1825 * that window will be opened into that winddow. Otherwise new windows will
michael@0 1826 * be opened.
michael@0 1827 */
michael@0 1828 restoreLastSession: function ssi_restoreLastSession() {
michael@0 1829 // Use the public getter since it also checks PB mode
michael@0 1830 if (!this.canRestoreLastSession) {
michael@0 1831 throw Components.Exception("Last session can not be restored");
michael@0 1832 }
michael@0 1833
michael@0 1834 // First collect each window with its id...
michael@0 1835 let windows = {};
michael@0 1836 this._forEachBrowserWindow(function(aWindow) {
michael@0 1837 if (aWindow.__SS_lastSessionWindowID)
michael@0 1838 windows[aWindow.__SS_lastSessionWindowID] = aWindow;
michael@0 1839 });
michael@0 1840
michael@0 1841 let lastSessionState = LastSession.getState();
michael@0 1842
michael@0 1843 // This shouldn't ever be the case...
michael@0 1844 if (!lastSessionState.windows.length) {
michael@0 1845 throw Components.Exception("lastSessionState has no windows", Cr.NS_ERROR_UNEXPECTED);
michael@0 1846 }
michael@0 1847
michael@0 1848 // We're technically doing a restore, so set things up so we send the
michael@0 1849 // notification when we're done. We want to send "sessionstore-browser-state-restored".
michael@0 1850 this._restoreCount = lastSessionState.windows.length;
michael@0 1851 this._browserSetState = true;
michael@0 1852
michael@0 1853 // We want to re-use the last opened window instead of opening a new one in
michael@0 1854 // the case where it's "empty" and not associated with a window in the session.
michael@0 1855 // We will do more processing via _prepWindowToRestoreInto if we need to use
michael@0 1856 // the lastWindow.
michael@0 1857 let lastWindow = this._getMostRecentBrowserWindow();
michael@0 1858 let canUseLastWindow = lastWindow &&
michael@0 1859 !lastWindow.__SS_lastSessionWindowID;
michael@0 1860
michael@0 1861 // global data must be restored before restoreWindow is called so that
michael@0 1862 // it happens before observers are notified
michael@0 1863 this._globalState.setFromState(lastSessionState);
michael@0 1864
michael@0 1865 // Restore into windows or open new ones as needed.
michael@0 1866 for (let i = 0; i < lastSessionState.windows.length; i++) {
michael@0 1867 let winState = lastSessionState.windows[i];
michael@0 1868 let lastSessionWindowID = winState.__lastSessionWindowID;
michael@0 1869 // delete lastSessionWindowID so we don't add that to the window again
michael@0 1870 delete winState.__lastSessionWindowID;
michael@0 1871
michael@0 1872 // See if we can use an open window. First try one that is associated with
michael@0 1873 // the state we're trying to restore and then fallback to the last selected
michael@0 1874 // window.
michael@0 1875 let windowToUse = windows[lastSessionWindowID];
michael@0 1876 if (!windowToUse && canUseLastWindow) {
michael@0 1877 windowToUse = lastWindow;
michael@0 1878 canUseLastWindow = false;
michael@0 1879 }
michael@0 1880
michael@0 1881 let [canUseWindow, canOverwriteTabs] = this._prepWindowToRestoreInto(windowToUse);
michael@0 1882
michael@0 1883 // If there's a window already open that we can restore into, use that
michael@0 1884 if (canUseWindow) {
michael@0 1885 // Since we're not overwriting existing tabs, we want to merge _closedTabs,
michael@0 1886 // putting existing ones first. Then make sure we're respecting the max pref.
michael@0 1887 if (winState._closedTabs && winState._closedTabs.length) {
michael@0 1888 let curWinState = this._windows[windowToUse.__SSi];
michael@0 1889 curWinState._closedTabs = curWinState._closedTabs.concat(winState._closedTabs);
michael@0 1890 curWinState._closedTabs.splice(this._max_tabs_undo, curWinState._closedTabs.length);
michael@0 1891 }
michael@0 1892
michael@0 1893 // Restore into that window - pretend it's a followup since we'll already
michael@0 1894 // have a focused window.
michael@0 1895 //XXXzpao This is going to merge extData together (taking what was in
michael@0 1896 // winState over what is in the window already. The hack we have
michael@0 1897 // in _preWindowToRestoreInto will prevent most (all?) Panorama
michael@0 1898 // weirdness but we will still merge other extData.
michael@0 1899 // Bug 588217 should make this go away by merging the group data.
michael@0 1900 let options = {overwriteTabs: canOverwriteTabs, isFollowUp: true};
michael@0 1901 this.restoreWindow(windowToUse, { windows: [winState] }, options);
michael@0 1902 }
michael@0 1903 else {
michael@0 1904 this._openWindowWithState({ windows: [winState] });
michael@0 1905 }
michael@0 1906 }
michael@0 1907
michael@0 1908 // Merge closed windows from this session with ones from last session
michael@0 1909 if (lastSessionState._closedWindows) {
michael@0 1910 this._closedWindows = this._closedWindows.concat(lastSessionState._closedWindows);
michael@0 1911 this._capClosedWindows();
michael@0 1912 }
michael@0 1913
michael@0 1914 if (lastSessionState.scratchpads) {
michael@0 1915 ScratchpadManager.restoreSession(lastSessionState.scratchpads);
michael@0 1916 }
michael@0 1917
michael@0 1918 // Set data that persists between sessions
michael@0 1919 this._recentCrashes = lastSessionState.session &&
michael@0 1920 lastSessionState.session.recentCrashes || 0;
michael@0 1921
michael@0 1922 // Update the session start time using the restored session state.
michael@0 1923 this._updateSessionStartTime(lastSessionState);
michael@0 1924
michael@0 1925 LastSession.clear();
michael@0 1926 },
michael@0 1927
michael@0 1928 /**
michael@0 1929 * See if aWindow is usable for use when restoring a previous session via
michael@0 1930 * restoreLastSession. If usable, prepare it for use.
michael@0 1931 *
michael@0 1932 * @param aWindow
michael@0 1933 * the window to inspect & prepare
michael@0 1934 * @returns [canUseWindow, canOverwriteTabs]
michael@0 1935 * canUseWindow: can the window be used to restore into
michael@0 1936 * canOverwriteTabs: all of the current tabs are home pages and we
michael@0 1937 * can overwrite them
michael@0 1938 */
michael@0 1939 _prepWindowToRestoreInto: function ssi_prepWindowToRestoreInto(aWindow) {
michael@0 1940 if (!aWindow)
michael@0 1941 return [false, false];
michael@0 1942
michael@0 1943 // We might be able to overwrite the existing tabs instead of just adding
michael@0 1944 // the previous session's tabs to the end. This will be set if possible.
michael@0 1945 let canOverwriteTabs = false;
michael@0 1946
michael@0 1947 // Step 1 of processing:
michael@0 1948 // Inspect extData for Panorama identifiers. If found, then we want to
michael@0 1949 // inspect further. If there is a single group, then we can use this
michael@0 1950 // window. If there are multiple groups then we won't use this window.
michael@0 1951 let groupsData = this.getWindowValue(aWindow, "tabview-groups");
michael@0 1952 if (groupsData) {
michael@0 1953 groupsData = JSON.parse(groupsData);
michael@0 1954
michael@0 1955 // If there are multiple groups, we don't want to use this window.
michael@0 1956 if (groupsData.totalNumber > 1)
michael@0 1957 return [false, false];
michael@0 1958 }
michael@0 1959
michael@0 1960 // Step 2 of processing:
michael@0 1961 // If we're still here, then the window is usable. Look at the open tabs in
michael@0 1962 // comparison to home pages. If all the tabs are home pages then we'll end
michael@0 1963 // up overwriting all of them. Otherwise we'll just close the tabs that
michael@0 1964 // match home pages. Tabs with the about:blank URI will always be
michael@0 1965 // overwritten.
michael@0 1966 let homePages = ["about:blank"];
michael@0 1967 let removableTabs = [];
michael@0 1968 let tabbrowser = aWindow.gBrowser;
michael@0 1969 let normalTabsLen = tabbrowser.tabs.length - tabbrowser._numPinnedTabs;
michael@0 1970 let startupPref = this._prefBranch.getIntPref("startup.page");
michael@0 1971 if (startupPref == 1)
michael@0 1972 homePages = homePages.concat(aWindow.gHomeButton.getHomePage().split("|"));
michael@0 1973
michael@0 1974 for (let i = tabbrowser._numPinnedTabs; i < tabbrowser.tabs.length; i++) {
michael@0 1975 let tab = tabbrowser.tabs[i];
michael@0 1976 if (homePages.indexOf(tab.linkedBrowser.currentURI.spec) != -1) {
michael@0 1977 removableTabs.push(tab);
michael@0 1978 }
michael@0 1979 }
michael@0 1980
michael@0 1981 if (tabbrowser.tabs.length == removableTabs.length) {
michael@0 1982 canOverwriteTabs = true;
michael@0 1983 }
michael@0 1984 else {
michael@0 1985 // If we're not overwriting all of the tabs, then close the home tabs.
michael@0 1986 for (let i = removableTabs.length - 1; i >= 0; i--) {
michael@0 1987 tabbrowser.removeTab(removableTabs.pop(), { animate: false });
michael@0 1988 }
michael@0 1989 }
michael@0 1990
michael@0 1991 return [true, canOverwriteTabs];
michael@0 1992 },
michael@0 1993
michael@0 1994 /* ........ Saving Functionality .............. */
michael@0 1995
michael@0 1996 /**
michael@0 1997 * Store window dimensions, visibility, sidebar
michael@0 1998 * @param aWindow
michael@0 1999 * Window reference
michael@0 2000 */
michael@0 2001 _updateWindowFeatures: function ssi_updateWindowFeatures(aWindow) {
michael@0 2002 var winData = this._windows[aWindow.__SSi];
michael@0 2003
michael@0 2004 WINDOW_ATTRIBUTES.forEach(function(aAttr) {
michael@0 2005 winData[aAttr] = this._getWindowDimension(aWindow, aAttr);
michael@0 2006 }, this);
michael@0 2007
michael@0 2008 var hidden = WINDOW_HIDEABLE_FEATURES.filter(function(aItem) {
michael@0 2009 return aWindow[aItem] && !aWindow[aItem].visible;
michael@0 2010 });
michael@0 2011 if (hidden.length != 0)
michael@0 2012 winData.hidden = hidden.join(",");
michael@0 2013 else if (winData.hidden)
michael@0 2014 delete winData.hidden;
michael@0 2015
michael@0 2016 var sidebar = aWindow.document.getElementById("sidebar-box").getAttribute("sidebarcommand");
michael@0 2017 if (sidebar)
michael@0 2018 winData.sidebar = sidebar;
michael@0 2019 else if (winData.sidebar)
michael@0 2020 delete winData.sidebar;
michael@0 2021 },
michael@0 2022
michael@0 2023 /**
michael@0 2024 * gather session data as object
michael@0 2025 * @param aUpdateAll
michael@0 2026 * Bool update all windows
michael@0 2027 * @returns object
michael@0 2028 */
michael@0 2029 getCurrentState: function (aUpdateAll) {
michael@0 2030 this._handleClosedWindows();
michael@0 2031
michael@0 2032 var activeWindow = this._getMostRecentBrowserWindow();
michael@0 2033
michael@0 2034 TelemetryStopwatch.start("FX_SESSION_RESTORE_COLLECT_ALL_WINDOWS_DATA_MS");
michael@0 2035 if (this._loadState == STATE_RUNNING) {
michael@0 2036 // update the data for all windows with activities since the last save operation
michael@0 2037 this._forEachBrowserWindow(function(aWindow) {
michael@0 2038 if (!this._isWindowLoaded(aWindow)) // window data is still in _statesToRestore
michael@0 2039 return;
michael@0 2040 if (aUpdateAll || DirtyWindows.has(aWindow) || aWindow == activeWindow) {
michael@0 2041 this._collectWindowData(aWindow);
michael@0 2042 }
michael@0 2043 else { // always update the window features (whose change alone never triggers a save operation)
michael@0 2044 this._updateWindowFeatures(aWindow);
michael@0 2045 }
michael@0 2046 });
michael@0 2047 DirtyWindows.clear();
michael@0 2048 }
michael@0 2049 TelemetryStopwatch.finish("FX_SESSION_RESTORE_COLLECT_ALL_WINDOWS_DATA_MS");
michael@0 2050
michael@0 2051 // An array that at the end will hold all current window data.
michael@0 2052 var total = [];
michael@0 2053 // The ids of all windows contained in 'total' in the same order.
michael@0 2054 var ids = [];
michael@0 2055 // The number of window that are _not_ popups.
michael@0 2056 var nonPopupCount = 0;
michael@0 2057 var ix;
michael@0 2058
michael@0 2059 // collect the data for all windows
michael@0 2060 for (ix in this._windows) {
michael@0 2061 if (this._windows[ix]._restoring) // window data is still in _statesToRestore
michael@0 2062 continue;
michael@0 2063 total.push(this._windows[ix]);
michael@0 2064 ids.push(ix);
michael@0 2065 if (!this._windows[ix].isPopup)
michael@0 2066 nonPopupCount++;
michael@0 2067 }
michael@0 2068
michael@0 2069 TelemetryStopwatch.start("FX_SESSION_RESTORE_COLLECT_COOKIES_MS");
michael@0 2070 SessionCookies.update(total);
michael@0 2071 TelemetryStopwatch.finish("FX_SESSION_RESTORE_COLLECT_COOKIES_MS");
michael@0 2072
michael@0 2073 // collect the data for all windows yet to be restored
michael@0 2074 for (ix in this._statesToRestore) {
michael@0 2075 for each (let winData in this._statesToRestore[ix].windows) {
michael@0 2076 total.push(winData);
michael@0 2077 if (!winData.isPopup)
michael@0 2078 nonPopupCount++;
michael@0 2079 }
michael@0 2080 }
michael@0 2081
michael@0 2082 // shallow copy this._closedWindows to preserve current state
michael@0 2083 let lastClosedWindowsCopy = this._closedWindows.slice();
michael@0 2084
michael@0 2085 #ifndef XP_MACOSX
michael@0 2086 // If no non-popup browser window remains open, return the state of the last
michael@0 2087 // closed window(s). We only want to do this when we're actually "ending"
michael@0 2088 // the session.
michael@0 2089 //XXXzpao We should do this for _restoreLastWindow == true, but that has
michael@0 2090 // its own check for popups. c.f. bug 597619
michael@0 2091 if (nonPopupCount == 0 && lastClosedWindowsCopy.length > 0 &&
michael@0 2092 this._loadState == STATE_QUITTING) {
michael@0 2093 // prepend the last non-popup browser window, so that if the user loads more tabs
michael@0 2094 // at startup we don't accidentally add them to a popup window
michael@0 2095 do {
michael@0 2096 total.unshift(lastClosedWindowsCopy.shift())
michael@0 2097 } while (total[0].isPopup && lastClosedWindowsCopy.length > 0)
michael@0 2098 }
michael@0 2099 #endif
michael@0 2100
michael@0 2101 if (activeWindow) {
michael@0 2102 this.activeWindowSSiCache = activeWindow.__SSi || "";
michael@0 2103 }
michael@0 2104 ix = ids.indexOf(this.activeWindowSSiCache);
michael@0 2105 // We don't want to restore focus to a minimized window or a window which had all its
michael@0 2106 // tabs stripped out (doesn't exist).
michael@0 2107 if (ix != -1 && total[ix] && total[ix].sizemode == "minimized")
michael@0 2108 ix = -1;
michael@0 2109
michael@0 2110 let session = {
michael@0 2111 lastUpdate: Date.now(),
michael@0 2112 startTime: this._sessionStartTime,
michael@0 2113 recentCrashes: this._recentCrashes
michael@0 2114 };
michael@0 2115
michael@0 2116 // get open Scratchpad window states too
michael@0 2117 let scratchpads = ScratchpadManager.getSessionState();
michael@0 2118
michael@0 2119 let state = {
michael@0 2120 windows: total,
michael@0 2121 selectedWindow: ix + 1,
michael@0 2122 _closedWindows: lastClosedWindowsCopy,
michael@0 2123 session: session,
michael@0 2124 scratchpads: scratchpads,
michael@0 2125 global: this._globalState.getState()
michael@0 2126 };
michael@0 2127
michael@0 2128 // Persist the last session if we deferred restoring it
michael@0 2129 if (LastSession.canRestore) {
michael@0 2130 state.lastSessionState = LastSession.getState();
michael@0 2131 }
michael@0 2132
michael@0 2133 // If we were called by the SessionSaver and started with only a private
michael@0 2134 // window we want to pass the deferred initial state to not lose the
michael@0 2135 // previous session.
michael@0 2136 if (this._deferredInitialState) {
michael@0 2137 state.deferredInitialState = this._deferredInitialState;
michael@0 2138 }
michael@0 2139
michael@0 2140 return state;
michael@0 2141 },
michael@0 2142
michael@0 2143 /**
michael@0 2144 * serialize session data for a window
michael@0 2145 * @param aWindow
michael@0 2146 * Window reference
michael@0 2147 * @returns string
michael@0 2148 */
michael@0 2149 _getWindowState: function ssi_getWindowState(aWindow) {
michael@0 2150 if (!this._isWindowLoaded(aWindow))
michael@0 2151 return this._statesToRestore[aWindow.__SS_restoreID];
michael@0 2152
michael@0 2153 if (this._loadState == STATE_RUNNING) {
michael@0 2154 this._collectWindowData(aWindow);
michael@0 2155 }
michael@0 2156
michael@0 2157 let windows = [this._windows[aWindow.__SSi]];
michael@0 2158 SessionCookies.update(windows);
michael@0 2159
michael@0 2160 return { windows: windows };
michael@0 2161 },
michael@0 2162
michael@0 2163 _collectWindowData: function ssi_collectWindowData(aWindow) {
michael@0 2164 if (!this._isWindowLoaded(aWindow))
michael@0 2165 return;
michael@0 2166 TelemetryStopwatch.start("FX_SESSION_RESTORE_COLLECT_SINGLE_WINDOW_DATA_MS");
michael@0 2167
michael@0 2168 let tabbrowser = aWindow.gBrowser;
michael@0 2169 let tabs = tabbrowser.tabs;
michael@0 2170 let winData = this._windows[aWindow.__SSi];
michael@0 2171 let tabsData = winData.tabs = [];
michael@0 2172
michael@0 2173 // update the internal state data for this window
michael@0 2174 for (let tab of tabs) {
michael@0 2175 tabsData.push(TabState.collect(tab));
michael@0 2176 }
michael@0 2177 winData.selected = tabbrowser.mTabBox.selectedIndex + 1;
michael@0 2178
michael@0 2179 this._updateWindowFeatures(aWindow);
michael@0 2180
michael@0 2181 // Make sure we keep __SS_lastSessionWindowID around for cases like entering
michael@0 2182 // or leaving PB mode.
michael@0 2183 if (aWindow.__SS_lastSessionWindowID)
michael@0 2184 this._windows[aWindow.__SSi].__lastSessionWindowID =
michael@0 2185 aWindow.__SS_lastSessionWindowID;
michael@0 2186
michael@0 2187 DirtyWindows.remove(aWindow);
michael@0 2188 TelemetryStopwatch.finish("FX_SESSION_RESTORE_COLLECT_SINGLE_WINDOW_DATA_MS");
michael@0 2189 },
michael@0 2190
michael@0 2191 /* ........ Restoring Functionality .............. */
michael@0 2192
michael@0 2193 /**
michael@0 2194 * restore features to a single window
michael@0 2195 * @param aWindow
michael@0 2196 * Window reference
michael@0 2197 * @param aState
michael@0 2198 * JS object or its eval'able source
michael@0 2199 * @param aOptions
michael@0 2200 * {overwriteTabs: true} to overwrite existing tabs w/ new ones
michael@0 2201 * {isFollowUp: true} if this is not the restoration of the 1st window
michael@0 2202 * {firstWindow: true} if this is the first non-private window we're
michael@0 2203 * restoring in this session, that might open an
michael@0 2204 * external link as well
michael@0 2205 */
michael@0 2206 restoreWindow: function ssi_restoreWindow(aWindow, aState, aOptions = {}) {
michael@0 2207 let overwriteTabs = aOptions && aOptions.overwriteTabs;
michael@0 2208 let isFollowUp = aOptions && aOptions.isFollowUp;
michael@0 2209 let firstWindow = aOptions && aOptions.firstWindow;
michael@0 2210
michael@0 2211 if (isFollowUp) {
michael@0 2212 this.windowToFocus = aWindow;
michael@0 2213 }
michael@0 2214 // initialize window if necessary
michael@0 2215 if (aWindow && (!aWindow.__SSi || !this._windows[aWindow.__SSi]))
michael@0 2216 this.onLoad(aWindow);
michael@0 2217
michael@0 2218 try {
michael@0 2219 var root = typeof aState == "string" ? JSON.parse(aState) : aState;
michael@0 2220 if (!root.windows[0]) {
michael@0 2221 this._sendRestoreCompletedNotifications();
michael@0 2222 return; // nothing to restore
michael@0 2223 }
michael@0 2224 }
michael@0 2225 catch (ex) { // invalid state object - don't restore anything
michael@0 2226 debug(ex);
michael@0 2227 this._sendRestoreCompletedNotifications();
michael@0 2228 return;
michael@0 2229 }
michael@0 2230
michael@0 2231 TelemetryStopwatch.start("FX_SESSION_RESTORE_RESTORE_WINDOW_MS");
michael@0 2232
michael@0 2233 // We're not returning from this before we end up calling restoreTabs
michael@0 2234 // for this window, so make sure we send the SSWindowStateBusy event.
michael@0 2235 this._setWindowStateBusy(aWindow);
michael@0 2236
michael@0 2237 if (root._closedWindows)
michael@0 2238 this._closedWindows = root._closedWindows;
michael@0 2239
michael@0 2240 var winData;
michael@0 2241 if (!root.selectedWindow || root.selectedWindow > root.windows.length) {
michael@0 2242 root.selectedWindow = 0;
michael@0 2243 }
michael@0 2244
michael@0 2245 // open new windows for all further window entries of a multi-window session
michael@0 2246 // (unless they don't contain any tab data)
michael@0 2247 for (var w = 1; w < root.windows.length; w++) {
michael@0 2248 winData = root.windows[w];
michael@0 2249 if (winData && winData.tabs && winData.tabs[0]) {
michael@0 2250 var window = this._openWindowWithState({ windows: [winData] });
michael@0 2251 if (w == root.selectedWindow - 1) {
michael@0 2252 this.windowToFocus = window;
michael@0 2253 }
michael@0 2254 }
michael@0 2255 }
michael@0 2256 winData = root.windows[0];
michael@0 2257 if (!winData.tabs) {
michael@0 2258 winData.tabs = [];
michael@0 2259 }
michael@0 2260 // don't restore a single blank tab when we've had an external
michael@0 2261 // URL passed in for loading at startup (cf. bug 357419)
michael@0 2262 else if (firstWindow && !overwriteTabs && winData.tabs.length == 1 &&
michael@0 2263 (!winData.tabs[0].entries || winData.tabs[0].entries.length == 0)) {
michael@0 2264 winData.tabs = [];
michael@0 2265 }
michael@0 2266
michael@0 2267 var tabbrowser = aWindow.gBrowser;
michael@0 2268 var openTabCount = overwriteTabs ? tabbrowser.browsers.length : -1;
michael@0 2269 var newTabCount = winData.tabs.length;
michael@0 2270 var tabs = [];
michael@0 2271
michael@0 2272 // disable smooth scrolling while adding, moving, removing and selecting tabs
michael@0 2273 var tabstrip = tabbrowser.tabContainer.mTabstrip;
michael@0 2274 var smoothScroll = tabstrip.smoothScroll;
michael@0 2275 tabstrip.smoothScroll = false;
michael@0 2276
michael@0 2277 // unpin all tabs to ensure they are not reordered in the next loop
michael@0 2278 if (overwriteTabs) {
michael@0 2279 for (let t = tabbrowser._numPinnedTabs - 1; t > -1; t--)
michael@0 2280 tabbrowser.unpinTab(tabbrowser.tabs[t]);
michael@0 2281 }
michael@0 2282
michael@0 2283 // We need to keep track of the initially open tabs so that they
michael@0 2284 // can be moved to the end of the restored tabs.
michael@0 2285 let initialTabs = [];
michael@0 2286 if (!overwriteTabs && firstWindow) {
michael@0 2287 initialTabs = Array.slice(tabbrowser.tabs);
michael@0 2288 }
michael@0 2289
michael@0 2290 // make sure that the selected tab won't be closed in order to
michael@0 2291 // prevent unnecessary flickering
michael@0 2292 if (overwriteTabs && tabbrowser.selectedTab._tPos >= newTabCount)
michael@0 2293 tabbrowser.moveTabTo(tabbrowser.selectedTab, newTabCount - 1);
michael@0 2294
michael@0 2295 let numVisibleTabs = 0;
michael@0 2296
michael@0 2297 for (var t = 0; t < newTabCount; t++) {
michael@0 2298 tabs.push(t < openTabCount ?
michael@0 2299 tabbrowser.tabs[t] :
michael@0 2300 tabbrowser.addTab("about:blank", {skipAnimation: true}));
michael@0 2301
michael@0 2302 if (winData.tabs[t].pinned)
michael@0 2303 tabbrowser.pinTab(tabs[t]);
michael@0 2304
michael@0 2305 if (winData.tabs[t].hidden) {
michael@0 2306 tabbrowser.hideTab(tabs[t]);
michael@0 2307 }
michael@0 2308 else {
michael@0 2309 tabbrowser.showTab(tabs[t]);
michael@0 2310 numVisibleTabs++;
michael@0 2311 }
michael@0 2312 }
michael@0 2313
michael@0 2314 if (!overwriteTabs && firstWindow) {
michael@0 2315 // Move the originally open tabs to the end
michael@0 2316 let endPosition = tabbrowser.tabs.length - 1;
michael@0 2317 for (let i = 0; i < initialTabs.length; i++) {
michael@0 2318 tabbrowser.moveTabTo(initialTabs[i], endPosition);
michael@0 2319 }
michael@0 2320 }
michael@0 2321
michael@0 2322 // if all tabs to be restored are hidden, make the first one visible
michael@0 2323 if (!numVisibleTabs && winData.tabs.length) {
michael@0 2324 winData.tabs[0].hidden = false;
michael@0 2325 tabbrowser.showTab(tabs[0]);
michael@0 2326 }
michael@0 2327
michael@0 2328 // If overwriting tabs, we want to reset each tab's "restoring" state. Since
michael@0 2329 // we're overwriting those tabs, they should no longer be restoring. The
michael@0 2330 // tabs will be rebuilt and marked if they need to be restored after loading
michael@0 2331 // state (in restoreTabs).
michael@0 2332 if (overwriteTabs) {
michael@0 2333 for (let i = 0; i < tabbrowser.tabs.length; i++) {
michael@0 2334 let tab = tabbrowser.tabs[i];
michael@0 2335 if (tabbrowser.browsers[i].__SS_restoreState)
michael@0 2336 this._resetTabRestoringState(tab);
michael@0 2337 }
michael@0 2338 }
michael@0 2339
michael@0 2340 // We want to correlate the window with data from the last session, so
michael@0 2341 // assign another id if we have one. Otherwise clear so we don't do
michael@0 2342 // anything with it.
michael@0 2343 delete aWindow.__SS_lastSessionWindowID;
michael@0 2344 if (winData.__lastSessionWindowID)
michael@0 2345 aWindow.__SS_lastSessionWindowID = winData.__lastSessionWindowID;
michael@0 2346
michael@0 2347 // when overwriting tabs, remove all superflous ones
michael@0 2348 if (overwriteTabs && newTabCount < openTabCount) {
michael@0 2349 Array.slice(tabbrowser.tabs, newTabCount, openTabCount)
michael@0 2350 .forEach(tabbrowser.removeTab, tabbrowser);
michael@0 2351 }
michael@0 2352
michael@0 2353 if (overwriteTabs) {
michael@0 2354 this.restoreWindowFeatures(aWindow, winData);
michael@0 2355 delete this._windows[aWindow.__SSi].extData;
michael@0 2356 }
michael@0 2357 if (winData.cookies) {
michael@0 2358 this.restoreCookies(winData.cookies);
michael@0 2359 }
michael@0 2360 if (winData.extData) {
michael@0 2361 if (!this._windows[aWindow.__SSi].extData) {
michael@0 2362 this._windows[aWindow.__SSi].extData = {};
michael@0 2363 }
michael@0 2364 for (var key in winData.extData) {
michael@0 2365 this._windows[aWindow.__SSi].extData[key] = winData.extData[key];
michael@0 2366 }
michael@0 2367 }
michael@0 2368
michael@0 2369 let newClosedTabsData = winData._closedTabs || [];
michael@0 2370
michael@0 2371 if (overwriteTabs || firstWindow) {
michael@0 2372 // Overwrite existing closed tabs data when overwriteTabs=true
michael@0 2373 // or we're the first window to be restored.
michael@0 2374 this._windows[aWindow.__SSi]._closedTabs = newClosedTabsData;
michael@0 2375 } else if (this._max_tabs_undo > 0) {
michael@0 2376 // If we merge tabs, we also want to merge closed tabs data. We'll assume
michael@0 2377 // the restored tabs were closed more recently and append the current list
michael@0 2378 // of closed tabs to the new one...
michael@0 2379 newClosedTabsData =
michael@0 2380 newClosedTabsData.concat(this._windows[aWindow.__SSi]._closedTabs);
michael@0 2381
michael@0 2382 // ... and make sure that we don't exceed the max number of closed tabs
michael@0 2383 // we can restore.
michael@0 2384 this._windows[aWindow.__SSi]._closedTabs =
michael@0 2385 newClosedTabsData.slice(0, this._max_tabs_undo);
michael@0 2386 }
michael@0 2387
michael@0 2388 this.restoreTabs(aWindow, tabs, winData.tabs,
michael@0 2389 (overwriteTabs ? (parseInt(winData.selected || "1")) : 0));
michael@0 2390
michael@0 2391 if (aState.scratchpads) {
michael@0 2392 ScratchpadManager.restoreSession(aState.scratchpads);
michael@0 2393 }
michael@0 2394
michael@0 2395 // set smoothScroll back to the original value
michael@0 2396 tabstrip.smoothScroll = smoothScroll;
michael@0 2397
michael@0 2398 TelemetryStopwatch.finish("FX_SESSION_RESTORE_RESTORE_WINDOW_MS");
michael@0 2399
michael@0 2400 this._sendRestoreCompletedNotifications();
michael@0 2401 },
michael@0 2402
michael@0 2403 /**
michael@0 2404 * Sets the tabs restoring order with the following priority:
michael@0 2405 * Selected tab, pinned tabs, optimized visible tabs, other visible tabs and
michael@0 2406 * hidden tabs.
michael@0 2407 * @param aTabBrowser
michael@0 2408 * Tab browser object
michael@0 2409 * @param aTabs
michael@0 2410 * Array of tab references
michael@0 2411 * @param aTabData
michael@0 2412 * Array of tab data
michael@0 2413 * @param aSelectedTab
michael@0 2414 * Index of selected tab (1 is first tab, 0 no selected tab)
michael@0 2415 */
michael@0 2416 _setTabsRestoringOrder : function ssi__setTabsRestoringOrder(
michael@0 2417 aTabBrowser, aTabs, aTabData, aSelectedTab) {
michael@0 2418
michael@0 2419 // Store the selected tab. Need to substract one to get the index in aTabs.
michael@0 2420 let selectedTab;
michael@0 2421 if (aSelectedTab > 0 && aTabs[aSelectedTab - 1]) {
michael@0 2422 selectedTab = aTabs[aSelectedTab - 1];
michael@0 2423 }
michael@0 2424
michael@0 2425 // Store the pinned tabs and hidden tabs.
michael@0 2426 let pinnedTabs = [];
michael@0 2427 let pinnedTabsData = [];
michael@0 2428 let hiddenTabs = [];
michael@0 2429 let hiddenTabsData = [];
michael@0 2430 if (aTabs.length > 1) {
michael@0 2431 for (let t = aTabs.length - 1; t >= 0; t--) {
michael@0 2432 if (aTabData[t].pinned) {
michael@0 2433 pinnedTabs.unshift(aTabs.splice(t, 1)[0]);
michael@0 2434 pinnedTabsData.unshift(aTabData.splice(t, 1)[0]);
michael@0 2435 } else if (aTabData[t].hidden) {
michael@0 2436 hiddenTabs.unshift(aTabs.splice(t, 1)[0]);
michael@0 2437 hiddenTabsData.unshift(aTabData.splice(t, 1)[0]);
michael@0 2438 }
michael@0 2439 }
michael@0 2440 }
michael@0 2441
michael@0 2442 // Optimize the visible tabs only if there is a selected tab.
michael@0 2443 if (selectedTab) {
michael@0 2444 let selectedTabIndex = aTabs.indexOf(selectedTab);
michael@0 2445 if (selectedTabIndex > 0) {
michael@0 2446 let scrollSize = aTabBrowser.tabContainer.mTabstrip.scrollClientSize;
michael@0 2447 let tabWidth = aTabs[0].getBoundingClientRect().width;
michael@0 2448 let maxVisibleTabs = Math.ceil(scrollSize / tabWidth);
michael@0 2449 if (maxVisibleTabs < aTabs.length) {
michael@0 2450 let firstVisibleTab = 0;
michael@0 2451 let nonVisibleTabsCount = aTabs.length - maxVisibleTabs;
michael@0 2452 if (nonVisibleTabsCount >= selectedTabIndex) {
michael@0 2453 // Selected tab is leftmost since we scroll to it when possible.
michael@0 2454 firstVisibleTab = selectedTabIndex;
michael@0 2455 } else {
michael@0 2456 // Selected tab is rightmost or no more room to scroll right.
michael@0 2457 firstVisibleTab = nonVisibleTabsCount;
michael@0 2458 }
michael@0 2459 aTabs = aTabs.splice(firstVisibleTab, maxVisibleTabs).concat(aTabs);
michael@0 2460 aTabData =
michael@0 2461 aTabData.splice(firstVisibleTab, maxVisibleTabs).concat(aTabData);
michael@0 2462 }
michael@0 2463 }
michael@0 2464 }
michael@0 2465
michael@0 2466 // Merge the stored tabs in order.
michael@0 2467 aTabs = pinnedTabs.concat(aTabs, hiddenTabs);
michael@0 2468 aTabData = pinnedTabsData.concat(aTabData, hiddenTabsData);
michael@0 2469
michael@0 2470 // Load the selected tab to the first position and select it.
michael@0 2471 if (selectedTab) {
michael@0 2472 let selectedTabIndex = aTabs.indexOf(selectedTab);
michael@0 2473 if (selectedTabIndex > 0) {
michael@0 2474 aTabs = aTabs.splice(selectedTabIndex, 1).concat(aTabs);
michael@0 2475 aTabData = aTabData.splice(selectedTabIndex, 1).concat(aTabData);
michael@0 2476 }
michael@0 2477 aTabBrowser.selectedTab = selectedTab;
michael@0 2478 }
michael@0 2479
michael@0 2480 return [aTabs, aTabData];
michael@0 2481 },
michael@0 2482
michael@0 2483 /**
michael@0 2484 * Manage history restoration for a window
michael@0 2485 * @param aWindow
michael@0 2486 * Window to restore the tabs into
michael@0 2487 * @param aTabs
michael@0 2488 * Array of tab references
michael@0 2489 * @param aTabData
michael@0 2490 * Array of tab data
michael@0 2491 * @param aSelectTab
michael@0 2492 * Index of selected tab
michael@0 2493 * @param aRestoreImmediately
michael@0 2494 * Flag to indicate whether the given set of tabs aTabs should be
michael@0 2495 * restored/loaded immediately even if restore_on_demand = true
michael@0 2496 */
michael@0 2497 restoreTabs: function (aWindow, aTabs, aTabData, aSelectTab,
michael@0 2498 aRestoreImmediately = false)
michael@0 2499 {
michael@0 2500
michael@0 2501 var tabbrowser = aWindow.gBrowser;
michael@0 2502
michael@0 2503 if (!this._isWindowLoaded(aWindow)) {
michael@0 2504 // from now on, the data will come from the actual window
michael@0 2505 delete this._statesToRestore[aWindow.__SS_restoreID];
michael@0 2506 delete aWindow.__SS_restoreID;
michael@0 2507 delete this._windows[aWindow.__SSi]._restoring;
michael@0 2508 }
michael@0 2509
michael@0 2510 // It's important to set the window state to dirty so that
michael@0 2511 // we collect their data for the first time when saving state.
michael@0 2512 DirtyWindows.add(aWindow);
michael@0 2513
michael@0 2514 // Set the state to restore as the window's current state. Normally, this
michael@0 2515 // will just be overridden the next time we collect state but we need this
michael@0 2516 // as a fallback should Firefox be shutdown early without notifying us
michael@0 2517 // beforehand.
michael@0 2518 this._windows[aWindow.__SSi].tabs = aTabData.slice();
michael@0 2519 this._windows[aWindow.__SSi].selected = aSelectTab;
michael@0 2520
michael@0 2521 if (aTabs.length == 0) {
michael@0 2522 // This is normally done later, but as we're returning early
michael@0 2523 // here we need to take care of it.
michael@0 2524 this._setWindowStateReady(aWindow);
michael@0 2525 return;
michael@0 2526 }
michael@0 2527
michael@0 2528 // Sets the tabs restoring order.
michael@0 2529 [aTabs, aTabData] =
michael@0 2530 this._setTabsRestoringOrder(tabbrowser, aTabs, aTabData, aSelectTab);
michael@0 2531
michael@0 2532 // Prepare the tabs so that they can be properly restored. We'll pin/unpin
michael@0 2533 // and show/hide tabs as necessary. We'll also set the labels, user typed
michael@0 2534 // value, and attach a copy of the tab's data in case we close it before
michael@0 2535 // it's been restored.
michael@0 2536 for (let t = 0; t < aTabs.length; t++) {
michael@0 2537 let tab = aTabs[t];
michael@0 2538 let browser = tabbrowser.getBrowserForTab(tab);
michael@0 2539 let tabData = aTabData[t];
michael@0 2540
michael@0 2541 if (tabData.pinned)
michael@0 2542 tabbrowser.pinTab(tab);
michael@0 2543 else
michael@0 2544 tabbrowser.unpinTab(tab);
michael@0 2545
michael@0 2546 if (tabData.hidden)
michael@0 2547 tabbrowser.hideTab(tab);
michael@0 2548 else
michael@0 2549 tabbrowser.showTab(tab);
michael@0 2550
michael@0 2551 if (tabData.lastAccessed) {
michael@0 2552 tab.lastAccessed = tabData.lastAccessed;
michael@0 2553 }
michael@0 2554
michael@0 2555 if ("attributes" in tabData) {
michael@0 2556 // Ensure that we persist tab attributes restored from previous sessions.
michael@0 2557 Object.keys(tabData.attributes).forEach(a => TabAttributes.persist(a));
michael@0 2558 }
michael@0 2559
michael@0 2560 if (!tabData.entries) {
michael@0 2561 tabData.entries = [];
michael@0 2562 }
michael@0 2563 if (tabData.extData) {
michael@0 2564 tab.__SS_extdata = {};
michael@0 2565 for (let key in tabData.extData)
michael@0 2566 tab.__SS_extdata[key] = tabData.extData[key];
michael@0 2567 } else {
michael@0 2568 delete tab.__SS_extdata;
michael@0 2569 }
michael@0 2570
michael@0 2571 // Flush all data from the content script synchronously. This is done so
michael@0 2572 // that all async messages that are still on their way to chrome will
michael@0 2573 // be ignored and don't override any tab data set when restoring.
michael@0 2574 TabState.flush(tab.linkedBrowser);
michael@0 2575
michael@0 2576 // Ensure the index is in bounds.
michael@0 2577 let activeIndex = (tabData.index || tabData.entries.length) - 1;
michael@0 2578 activeIndex = Math.min(activeIndex, tabData.entries.length - 1);
michael@0 2579 activeIndex = Math.max(activeIndex, 0);
michael@0 2580
michael@0 2581 // Save the index in case we updated it above.
michael@0 2582 tabData.index = activeIndex + 1;
michael@0 2583
michael@0 2584 // In electrolysis, we may need to change the browser's remote
michael@0 2585 // attribute so that it runs in a content process.
michael@0 2586 let activePageData = tabData.entries[activeIndex] || null;
michael@0 2587 let uri = activePageData ? activePageData.url || null : null;
michael@0 2588 tabbrowser.updateBrowserRemoteness(browser, uri);
michael@0 2589
michael@0 2590 // Start a new epoch and include the epoch in the restoreHistory
michael@0 2591 // message. If a message is received that relates to a previous epoch, we
michael@0 2592 // discard it.
michael@0 2593 let epoch = this._nextRestoreEpoch++;
michael@0 2594 this._browserEpochs.set(browser.permanentKey, epoch);
michael@0 2595
michael@0 2596 // keep the data around to prevent dataloss in case
michael@0 2597 // a tab gets closed before it's been properly restored
michael@0 2598 browser.__SS_data = tabData;
michael@0 2599 browser.__SS_restoreState = TAB_STATE_NEEDS_RESTORE;
michael@0 2600 browser.setAttribute("pending", "true");
michael@0 2601 tab.setAttribute("pending", "true");
michael@0 2602
michael@0 2603 // Update the persistent tab state cache with |tabData| information.
michael@0 2604 TabStateCache.update(browser, {
michael@0 2605 history: {entries: tabData.entries, index: tabData.index},
michael@0 2606 scroll: tabData.scroll || null,
michael@0 2607 storage: tabData.storage || null,
michael@0 2608 formdata: tabData.formdata || null,
michael@0 2609 disallow: tabData.disallow || null,
michael@0 2610 pageStyle: tabData.pageStyle || null
michael@0 2611 });
michael@0 2612
michael@0 2613 browser.messageManager.sendAsyncMessage("SessionStore:restoreHistory",
michael@0 2614 {tabData: tabData, epoch: epoch});
michael@0 2615
michael@0 2616 // Restore tab attributes.
michael@0 2617 if ("attributes" in tabData) {
michael@0 2618 TabAttributes.set(tab, tabData.attributes);
michael@0 2619 }
michael@0 2620
michael@0 2621 // This could cause us to ignore MAX_CONCURRENT_TAB_RESTORES a bit, but
michael@0 2622 // it ensures each window will have its selected tab loaded.
michael@0 2623 if (aRestoreImmediately || tabbrowser.selectedBrowser == browser) {
michael@0 2624 this.restoreTabContent(tab);
michael@0 2625 } else {
michael@0 2626 TabRestoreQueue.add(tab);
michael@0 2627 this.restoreNextTab();
michael@0 2628 }
michael@0 2629 }
michael@0 2630
michael@0 2631 this._setWindowStateReady(aWindow);
michael@0 2632 },
michael@0 2633
michael@0 2634 /**
michael@0 2635 * Restores the specified tab. If the tab can't be restored (eg, no history or
michael@0 2636 * calling gotoIndex fails), then state changes will be rolled back.
michael@0 2637 * This method will check if gTabsProgressListener is attached to the tab's
michael@0 2638 * window, ensuring that we don't get caught without one.
michael@0 2639 * This method removes the session history listener right before starting to
michael@0 2640 * attempt a load. This will prevent cases of "stuck" listeners.
michael@0 2641 * If this method returns false, then it is up to the caller to decide what to
michael@0 2642 * do. In the common case (restoreNextTab), we will want to then attempt to
michael@0 2643 * restore the next tab. In the other case (selecting the tab, reloading the
michael@0 2644 * tab), the caller doesn't actually want to do anything if no page is loaded.
michael@0 2645 *
michael@0 2646 * @param aTab
michael@0 2647 * the tab to restore
michael@0 2648 *
michael@0 2649 * @returns true/false indicating whether or not a load actually happened
michael@0 2650 */
michael@0 2651 restoreTabContent: function (aTab) {
michael@0 2652 let window = aTab.ownerDocument.defaultView;
michael@0 2653 let browser = aTab.linkedBrowser;
michael@0 2654 let tabData = browser.__SS_data;
michael@0 2655
michael@0 2656 // Make sure that this tab is removed from the priority queue.
michael@0 2657 TabRestoreQueue.remove(aTab);
michael@0 2658
michael@0 2659 // Increase our internal count.
michael@0 2660 this._tabsRestoringCount++;
michael@0 2661
michael@0 2662 // Set this tab's state to restoring
michael@0 2663 browser.__SS_restoreState = TAB_STATE_RESTORING;
michael@0 2664 browser.removeAttribute("pending");
michael@0 2665 aTab.removeAttribute("pending");
michael@0 2666
michael@0 2667 let activeIndex = tabData.index - 1;
michael@0 2668
michael@0 2669 // Attach data that will be restored on "load" event, after tab is restored.
michael@0 2670 if (tabData.entries.length) {
michael@0 2671 // restore those aspects of the currently active documents which are not
michael@0 2672 // preserved in the plain history entries (mainly scroll state and text data)
michael@0 2673 browser.__SS_restore_data = tabData.entries[activeIndex] || {};
michael@0 2674 } else {
michael@0 2675 browser.__SS_restore_data = {};
michael@0 2676 }
michael@0 2677
michael@0 2678 browser.__SS_restore_tab = aTab;
michael@0 2679
michael@0 2680 browser.messageManager.sendAsyncMessage("SessionStore:restoreTabContent");
michael@0 2681 },
michael@0 2682
michael@0 2683 /**
michael@0 2684 * This _attempts_ to restore the next available tab. If the restore fails,
michael@0 2685 * then we will attempt the next one.
michael@0 2686 * There are conditions where this won't do anything:
michael@0 2687 * if we're in the process of quitting
michael@0 2688 * if there are no tabs to restore
michael@0 2689 * if we have already reached the limit for number of tabs to restore
michael@0 2690 */
michael@0 2691 restoreNextTab: function ssi_restoreNextTab() {
michael@0 2692 // If we call in here while quitting, we don't actually want to do anything
michael@0 2693 if (this._loadState == STATE_QUITTING)
michael@0 2694 return;
michael@0 2695
michael@0 2696 // Don't exceed the maximum number of concurrent tab restores.
michael@0 2697 if (this._tabsRestoringCount >= MAX_CONCURRENT_TAB_RESTORES)
michael@0 2698 return;
michael@0 2699
michael@0 2700 let tab = TabRestoreQueue.shift();
michael@0 2701 if (tab) {
michael@0 2702 this.restoreTabContent(tab);
michael@0 2703 }
michael@0 2704 },
michael@0 2705
michael@0 2706 /**
michael@0 2707 * Restore visibility and dimension features to a window
michael@0 2708 * @param aWindow
michael@0 2709 * Window reference
michael@0 2710 * @param aWinData
michael@0 2711 * Object containing session data for the window
michael@0 2712 */
michael@0 2713 restoreWindowFeatures: function ssi_restoreWindowFeatures(aWindow, aWinData) {
michael@0 2714 var hidden = (aWinData.hidden)?aWinData.hidden.split(","):[];
michael@0 2715 WINDOW_HIDEABLE_FEATURES.forEach(function(aItem) {
michael@0 2716 aWindow[aItem].visible = hidden.indexOf(aItem) == -1;
michael@0 2717 });
michael@0 2718
michael@0 2719 if (aWinData.isPopup) {
michael@0 2720 this._windows[aWindow.__SSi].isPopup = true;
michael@0 2721 if (aWindow.gURLBar) {
michael@0 2722 aWindow.gURLBar.readOnly = true;
michael@0 2723 aWindow.gURLBar.setAttribute("enablehistory", "false");
michael@0 2724 }
michael@0 2725 }
michael@0 2726 else {
michael@0 2727 delete this._windows[aWindow.__SSi].isPopup;
michael@0 2728 if (aWindow.gURLBar) {
michael@0 2729 aWindow.gURLBar.readOnly = false;
michael@0 2730 aWindow.gURLBar.setAttribute("enablehistory", "true");
michael@0 2731 }
michael@0 2732 }
michael@0 2733
michael@0 2734 var _this = this;
michael@0 2735 aWindow.setTimeout(function() {
michael@0 2736 _this.restoreDimensions.apply(_this, [aWindow,
michael@0 2737 +aWinData.width || 0,
michael@0 2738 +aWinData.height || 0,
michael@0 2739 "screenX" in aWinData ? +aWinData.screenX : NaN,
michael@0 2740 "screenY" in aWinData ? +aWinData.screenY : NaN,
michael@0 2741 aWinData.sizemode || "", aWinData.sidebar || ""]);
michael@0 2742 }, 0);
michael@0 2743 },
michael@0 2744
michael@0 2745 /**
michael@0 2746 * Restore a window's dimensions
michael@0 2747 * @param aWidth
michael@0 2748 * Window width
michael@0 2749 * @param aHeight
michael@0 2750 * Window height
michael@0 2751 * @param aLeft
michael@0 2752 * Window left
michael@0 2753 * @param aTop
michael@0 2754 * Window top
michael@0 2755 * @param aSizeMode
michael@0 2756 * Window size mode (eg: maximized)
michael@0 2757 * @param aSidebar
michael@0 2758 * Sidebar command
michael@0 2759 */
michael@0 2760 restoreDimensions: function ssi_restoreDimensions(aWindow, aWidth, aHeight, aLeft, aTop, aSizeMode, aSidebar) {
michael@0 2761 var win = aWindow;
michael@0 2762 var _this = this;
michael@0 2763 function win_(aName) { return _this._getWindowDimension(win, aName); }
michael@0 2764
michael@0 2765 // find available space on the screen where this window is being placed
michael@0 2766 let screen = gScreenManager.screenForRect(aLeft, aTop, aWidth, aHeight);
michael@0 2767 if (screen) {
michael@0 2768 let screenLeft = {}, screenTop = {}, screenWidth = {}, screenHeight = {};
michael@0 2769 screen.GetAvailRectDisplayPix(screenLeft, screenTop, screenWidth, screenHeight);
michael@0 2770 // constrain the dimensions to the actual space available
michael@0 2771 if (aWidth > screenWidth.value) {
michael@0 2772 aWidth = screenWidth.value;
michael@0 2773 }
michael@0 2774 if (aHeight > screenHeight.value) {
michael@0 2775 aHeight = screenHeight.value;
michael@0 2776 }
michael@0 2777 // and then pull the window within the screen's bounds
michael@0 2778 if (aLeft < screenLeft.value) {
michael@0 2779 aLeft = screenLeft.value;
michael@0 2780 } else if (aLeft + aWidth > screenLeft.value + screenWidth.value) {
michael@0 2781 aLeft = screenLeft.value + screenWidth.value - aWidth;
michael@0 2782 }
michael@0 2783 if (aTop < screenTop.value) {
michael@0 2784 aTop = screenTop.value;
michael@0 2785 } else if (aTop + aHeight > screenTop.value + screenHeight.value) {
michael@0 2786 aTop = screenTop.value + screenHeight.value - aHeight;
michael@0 2787 }
michael@0 2788 }
michael@0 2789
michael@0 2790 // only modify those aspects which aren't correct yet
michael@0 2791 if (aWidth && aHeight && (aWidth != win_("width") || aHeight != win_("height"))) {
michael@0 2792 // Don't resize the window if it's currently maximized and we would
michael@0 2793 // maximize it again shortly after.
michael@0 2794 if (aSizeMode != "maximized" || win_("sizemode") != "maximized") {
michael@0 2795 aWindow.resizeTo(aWidth, aHeight);
michael@0 2796 }
michael@0 2797 }
michael@0 2798 if (!isNaN(aLeft) && !isNaN(aTop) && (aLeft != win_("screenX") || aTop != win_("screenY"))) {
michael@0 2799 aWindow.moveTo(aLeft, aTop);
michael@0 2800 }
michael@0 2801 if (aSizeMode && win_("sizemode") != aSizeMode)
michael@0 2802 {
michael@0 2803 switch (aSizeMode)
michael@0 2804 {
michael@0 2805 case "maximized":
michael@0 2806 aWindow.maximize();
michael@0 2807 break;
michael@0 2808 case "minimized":
michael@0 2809 aWindow.minimize();
michael@0 2810 break;
michael@0 2811 case "normal":
michael@0 2812 aWindow.restore();
michael@0 2813 break;
michael@0 2814 }
michael@0 2815 }
michael@0 2816 var sidebar = aWindow.document.getElementById("sidebar-box");
michael@0 2817 if (sidebar.getAttribute("sidebarcommand") != aSidebar) {
michael@0 2818 aWindow.toggleSidebar(aSidebar);
michael@0 2819 }
michael@0 2820 // since resizing/moving a window brings it to the foreground,
michael@0 2821 // we might want to re-focus the last focused window
michael@0 2822 if (this.windowToFocus) {
michael@0 2823 this.windowToFocus.focus();
michael@0 2824 }
michael@0 2825 },
michael@0 2826
michael@0 2827 /**
michael@0 2828 * Restores cookies
michael@0 2829 * @param aCookies
michael@0 2830 * Array of cookie objects
michael@0 2831 */
michael@0 2832 restoreCookies: function ssi_restoreCookies(aCookies) {
michael@0 2833 // MAX_EXPIRY should be 2^63-1, but JavaScript can't handle that precision
michael@0 2834 var MAX_EXPIRY = Math.pow(2, 62);
michael@0 2835 for (let i = 0; i < aCookies.length; i++) {
michael@0 2836 var cookie = aCookies[i];
michael@0 2837 try {
michael@0 2838 Services.cookies.add(cookie.host, cookie.path || "", cookie.name || "",
michael@0 2839 cookie.value, !!cookie.secure, !!cookie.httponly, true,
michael@0 2840 "expiry" in cookie ? cookie.expiry : MAX_EXPIRY);
michael@0 2841 }
michael@0 2842 catch (ex) { console.error(ex); } // don't let a single cookie stop recovering
michael@0 2843 }
michael@0 2844 },
michael@0 2845
michael@0 2846 /* ........ Disk Access .............. */
michael@0 2847
michael@0 2848 /**
michael@0 2849 * Save the current session state to disk, after a delay.
michael@0 2850 *
michael@0 2851 * @param aWindow (optional)
michael@0 2852 * Will mark the given window as dirty so that we will recollect its
michael@0 2853 * data before we start writing.
michael@0 2854 */
michael@0 2855 saveStateDelayed: function (aWindow = null) {
michael@0 2856 if (aWindow) {
michael@0 2857 DirtyWindows.add(aWindow);
michael@0 2858 }
michael@0 2859
michael@0 2860 SessionSaver.runDelayed();
michael@0 2861 },
michael@0 2862
michael@0 2863 /* ........ Auxiliary Functions .............. */
michael@0 2864
michael@0 2865 /**
michael@0 2866 * Update the session start time and send a telemetry measurement
michael@0 2867 * for the number of days elapsed since the session was started.
michael@0 2868 *
michael@0 2869 * @param state
michael@0 2870 * The session state.
michael@0 2871 */
michael@0 2872 _updateSessionStartTime: function ssi_updateSessionStartTime(state) {
michael@0 2873 // Attempt to load the session start time from the session state
michael@0 2874 if (state.session && state.session.startTime) {
michael@0 2875 this._sessionStartTime = state.session.startTime;
michael@0 2876
michael@0 2877 // ms to days
michael@0 2878 let sessionLength = (Date.now() - this._sessionStartTime) / MS_PER_DAY;
michael@0 2879
michael@0 2880 if (sessionLength > 0) {
michael@0 2881 // Submit the session length telemetry measurement
michael@0 2882 Services.telemetry.getHistogramById("FX_SESSION_RESTORE_SESSION_LENGTH").add(sessionLength);
michael@0 2883 }
michael@0 2884 }
michael@0 2885 },
michael@0 2886
michael@0 2887 /**
michael@0 2888 * call a callback for all currently opened browser windows
michael@0 2889 * (might miss the most recent one)
michael@0 2890 * @param aFunc
michael@0 2891 * Callback each window is passed to
michael@0 2892 */
michael@0 2893 _forEachBrowserWindow: function ssi_forEachBrowserWindow(aFunc) {
michael@0 2894 var windowsEnum = Services.wm.getEnumerator("navigator:browser");
michael@0 2895
michael@0 2896 while (windowsEnum.hasMoreElements()) {
michael@0 2897 var window = windowsEnum.getNext();
michael@0 2898 if (window.__SSi && !window.closed) {
michael@0 2899 aFunc.call(this, window);
michael@0 2900 }
michael@0 2901 }
michael@0 2902 },
michael@0 2903
michael@0 2904 /**
michael@0 2905 * Returns most recent window
michael@0 2906 * @returns Window reference
michael@0 2907 */
michael@0 2908 _getMostRecentBrowserWindow: function ssi_getMostRecentBrowserWindow() {
michael@0 2909 return RecentWindow.getMostRecentBrowserWindow({ allowPopups: true });
michael@0 2910 },
michael@0 2911
michael@0 2912 /**
michael@0 2913 * Calls onClose for windows that are determined to be closed but aren't
michael@0 2914 * destroyed yet, which would otherwise cause getBrowserState and
michael@0 2915 * setBrowserState to treat them as open windows.
michael@0 2916 */
michael@0 2917 _handleClosedWindows: function ssi_handleClosedWindows() {
michael@0 2918 var windowsEnum = Services.wm.getEnumerator("navigator:browser");
michael@0 2919
michael@0 2920 while (windowsEnum.hasMoreElements()) {
michael@0 2921 var window = windowsEnum.getNext();
michael@0 2922 if (window.closed) {
michael@0 2923 this.onClose(window);
michael@0 2924 }
michael@0 2925 }
michael@0 2926 },
michael@0 2927
michael@0 2928 /**
michael@0 2929 * open a new browser window for a given session state
michael@0 2930 * called when restoring a multi-window session
michael@0 2931 * @param aState
michael@0 2932 * Object containing session data
michael@0 2933 */
michael@0 2934 _openWindowWithState: function ssi_openWindowWithState(aState) {
michael@0 2935 var argString = Cc["@mozilla.org/supports-string;1"].
michael@0 2936 createInstance(Ci.nsISupportsString);
michael@0 2937 argString.data = "";
michael@0 2938
michael@0 2939 // Build feature string
michael@0 2940 let features = "chrome,dialog=no,macsuppressanimation,all";
michael@0 2941 let winState = aState.windows[0];
michael@0 2942 WINDOW_ATTRIBUTES.forEach(function(aFeature) {
michael@0 2943 // Use !isNaN as an easy way to ignore sizemode and check for numbers
michael@0 2944 if (aFeature in winState && !isNaN(winState[aFeature]))
michael@0 2945 features += "," + aFeature + "=" + winState[aFeature];
michael@0 2946 });
michael@0 2947
michael@0 2948 if (winState.isPrivate) {
michael@0 2949 features += ",private";
michael@0 2950 }
michael@0 2951
michael@0 2952 var window =
michael@0 2953 Services.ww.openWindow(null, this._prefBranch.getCharPref("chromeURL"),
michael@0 2954 "_blank", features, argString);
michael@0 2955
michael@0 2956 do {
michael@0 2957 var ID = "window" + Math.random();
michael@0 2958 } while (ID in this._statesToRestore);
michael@0 2959 this._statesToRestore[(window.__SS_restoreID = ID)] = aState;
michael@0 2960
michael@0 2961 return window;
michael@0 2962 },
michael@0 2963
michael@0 2964 /**
michael@0 2965 * Gets the tab for the given browser. This should be marginally better
michael@0 2966 * than using tabbrowser's getTabForContentWindow. This assumes the browser
michael@0 2967 * is the linkedBrowser of a tab, not a dangling browser.
michael@0 2968 *
michael@0 2969 * @param aBrowser
michael@0 2970 * The browser from which to get the tab.
michael@0 2971 */
michael@0 2972 _getTabForBrowser: function ssi_getTabForBrowser(aBrowser) {
michael@0 2973 let window = aBrowser.ownerDocument.defaultView;
michael@0 2974 for (let i = 0; i < window.gBrowser.tabs.length; i++) {
michael@0 2975 let tab = window.gBrowser.tabs[i];
michael@0 2976 if (tab.linkedBrowser == aBrowser)
michael@0 2977 return tab;
michael@0 2978 }
michael@0 2979 return undefined;
michael@0 2980 },
michael@0 2981
michael@0 2982 /**
michael@0 2983 * Whether or not to resume session, if not recovering from a crash.
michael@0 2984 * @returns bool
michael@0 2985 */
michael@0 2986 _doResumeSession: function ssi_doResumeSession() {
michael@0 2987 return this._prefBranch.getIntPref("startup.page") == 3 ||
michael@0 2988 this._prefBranch.getBoolPref("sessionstore.resume_session_once");
michael@0 2989 },
michael@0 2990
michael@0 2991 /**
michael@0 2992 * whether the user wants to load any other page at startup
michael@0 2993 * (except the homepage) - needed for determining whether to overwrite the current tabs
michael@0 2994 * C.f.: nsBrowserContentHandler's defaultArgs implementation.
michael@0 2995 * @returns bool
michael@0 2996 */
michael@0 2997 _isCmdLineEmpty: function ssi_isCmdLineEmpty(aWindow, aState) {
michael@0 2998 var pinnedOnly = aState.windows &&
michael@0 2999 aState.windows.every(function (win)
michael@0 3000 win.tabs.every(function (tab) tab.pinned));
michael@0 3001
michael@0 3002 let hasFirstArgument = aWindow.arguments && aWindow.arguments[0];
michael@0 3003 if (!pinnedOnly) {
michael@0 3004 let defaultArgs = Cc["@mozilla.org/browser/clh;1"].
michael@0 3005 getService(Ci.nsIBrowserHandler).defaultArgs;
michael@0 3006 if (aWindow.arguments &&
michael@0 3007 aWindow.arguments[0] &&
michael@0 3008 aWindow.arguments[0] == defaultArgs)
michael@0 3009 hasFirstArgument = false;
michael@0 3010 }
michael@0 3011
michael@0 3012 return !hasFirstArgument;
michael@0 3013 },
michael@0 3014
michael@0 3015 /**
michael@0 3016 * on popup windows, the XULWindow's attributes seem not to be set correctly
michael@0 3017 * we use thus JSDOMWindow attributes for sizemode and normal window attributes
michael@0 3018 * (and hope for reasonable values when maximized/minimized - since then
michael@0 3019 * outerWidth/outerHeight aren't the dimensions of the restored window)
michael@0 3020 * @param aWindow
michael@0 3021 * Window reference
michael@0 3022 * @param aAttribute
michael@0 3023 * String sizemode | width | height | other window attribute
michael@0 3024 * @returns string
michael@0 3025 */
michael@0 3026 _getWindowDimension: function ssi_getWindowDimension(aWindow, aAttribute) {
michael@0 3027 if (aAttribute == "sizemode") {
michael@0 3028 switch (aWindow.windowState) {
michael@0 3029 case aWindow.STATE_FULLSCREEN:
michael@0 3030 case aWindow.STATE_MAXIMIZED:
michael@0 3031 return "maximized";
michael@0 3032 case aWindow.STATE_MINIMIZED:
michael@0 3033 return "minimized";
michael@0 3034 default:
michael@0 3035 return "normal";
michael@0 3036 }
michael@0 3037 }
michael@0 3038
michael@0 3039 var dimension;
michael@0 3040 switch (aAttribute) {
michael@0 3041 case "width":
michael@0 3042 dimension = aWindow.outerWidth;
michael@0 3043 break;
michael@0 3044 case "height":
michael@0 3045 dimension = aWindow.outerHeight;
michael@0 3046 break;
michael@0 3047 default:
michael@0 3048 dimension = aAttribute in aWindow ? aWindow[aAttribute] : "";
michael@0 3049 break;
michael@0 3050 }
michael@0 3051
michael@0 3052 if (aWindow.windowState == aWindow.STATE_NORMAL) {
michael@0 3053 return dimension;
michael@0 3054 }
michael@0 3055 return aWindow.document.documentElement.getAttribute(aAttribute) || dimension;
michael@0 3056 },
michael@0 3057
michael@0 3058 /**
michael@0 3059 * Get nsIURI from string
michael@0 3060 * @param string
michael@0 3061 * @returns nsIURI
michael@0 3062 */
michael@0 3063 _getURIFromString: function ssi_getURIFromString(aString) {
michael@0 3064 return Services.io.newURI(aString, null, null);
michael@0 3065 },
michael@0 3066
michael@0 3067 /**
michael@0 3068 * @param aState is a session state
michael@0 3069 * @param aRecentCrashes is the number of consecutive crashes
michael@0 3070 * @returns whether a restore page will be needed for the session state
michael@0 3071 */
michael@0 3072 _needsRestorePage: function ssi_needsRestorePage(aState, aRecentCrashes) {
michael@0 3073 const SIX_HOURS_IN_MS = 6 * 60 * 60 * 1000;
michael@0 3074
michael@0 3075 // don't display the page when there's nothing to restore
michael@0 3076 let winData = aState.windows || null;
michael@0 3077 if (!winData || winData.length == 0)
michael@0 3078 return false;
michael@0 3079
michael@0 3080 // don't wrap a single about:sessionrestore page
michael@0 3081 if (this._hasSingleTabWithURL(winData, "about:sessionrestore") ||
michael@0 3082 this._hasSingleTabWithURL(winData, "about:welcomeback")) {
michael@0 3083 return false;
michael@0 3084 }
michael@0 3085
michael@0 3086 // don't automatically restore in Safe Mode
michael@0 3087 if (Services.appinfo.inSafeMode)
michael@0 3088 return true;
michael@0 3089
michael@0 3090 let max_resumed_crashes =
michael@0 3091 this._prefBranch.getIntPref("sessionstore.max_resumed_crashes");
michael@0 3092 let sessionAge = aState.session && aState.session.lastUpdate &&
michael@0 3093 (Date.now() - aState.session.lastUpdate);
michael@0 3094
michael@0 3095 return max_resumed_crashes != -1 &&
michael@0 3096 (aRecentCrashes > max_resumed_crashes ||
michael@0 3097 sessionAge && sessionAge >= SIX_HOURS_IN_MS);
michael@0 3098 },
michael@0 3099
michael@0 3100 /**
michael@0 3101 * @param aWinData is the set of windows in session state
michael@0 3102 * @param aURL is the single URL we're looking for
michael@0 3103 * @returns whether the window data contains only the single URL passed
michael@0 3104 */
michael@0 3105 _hasSingleTabWithURL: function(aWinData, aURL) {
michael@0 3106 if (aWinData &&
michael@0 3107 aWinData.length == 1 &&
michael@0 3108 aWinData[0].tabs &&
michael@0 3109 aWinData[0].tabs.length == 1 &&
michael@0 3110 aWinData[0].tabs[0].entries &&
michael@0 3111 aWinData[0].tabs[0].entries.length == 1) {
michael@0 3112 return aURL == aWinData[0].tabs[0].entries[0].url;
michael@0 3113 }
michael@0 3114 return false;
michael@0 3115 },
michael@0 3116
michael@0 3117 /**
michael@0 3118 * Determine if the tab state we're passed is something we should save. This
michael@0 3119 * is used when closing a tab or closing a window with a single tab
michael@0 3120 *
michael@0 3121 * @param aTabState
michael@0 3122 * The current tab state
michael@0 3123 * @returns boolean
michael@0 3124 */
michael@0 3125 _shouldSaveTabState: function ssi_shouldSaveTabState(aTabState) {
michael@0 3126 // If the tab has only a transient about: history entry, no other
michael@0 3127 // session history, and no userTypedValue, then we don't actually want to
michael@0 3128 // store this tab's data.
michael@0 3129 return aTabState.entries.length &&
michael@0 3130 !(aTabState.entries.length == 1 &&
michael@0 3131 (aTabState.entries[0].url == "about:blank" ||
michael@0 3132 aTabState.entries[0].url == "about:newtab") &&
michael@0 3133 !aTabState.userTypedValue);
michael@0 3134 },
michael@0 3135
michael@0 3136 /**
michael@0 3137 * This is going to take a state as provided at startup (via
michael@0 3138 * nsISessionStartup.state) and split it into 2 parts. The first part
michael@0 3139 * (defaultState) will be a state that should still be restored at startup,
michael@0 3140 * while the second part (state) is a state that should be saved for later.
michael@0 3141 * defaultState will be comprised of windows with only pinned tabs, extracted
michael@0 3142 * from state. It will contain the cookies that go along with the history
michael@0 3143 * entries in those tabs. It will also contain window position information.
michael@0 3144 *
michael@0 3145 * defaultState will be restored at startup. state will be passed into
michael@0 3146 * LastSession and will be kept in case the user explicitly wants
michael@0 3147 * to restore the previous session (publicly exposed as restoreLastSession).
michael@0 3148 *
michael@0 3149 * @param state
michael@0 3150 * The state, presumably from nsISessionStartup.state
michael@0 3151 * @returns [defaultState, state]
michael@0 3152 */
michael@0 3153 _prepDataForDeferredRestore: function ssi_prepDataForDeferredRestore(state) {
michael@0 3154 // Make sure that we don't modify the global state as provided by
michael@0 3155 // nsSessionStartup.state.
michael@0 3156 state = Cu.cloneInto(state, {});
michael@0 3157
michael@0 3158 let defaultState = { windows: [], selectedWindow: 1 };
michael@0 3159
michael@0 3160 state.selectedWindow = state.selectedWindow || 1;
michael@0 3161
michael@0 3162 // Look at each window, remove pinned tabs, adjust selectedindex,
michael@0 3163 // remove window if necessary.
michael@0 3164 for (let wIndex = 0; wIndex < state.windows.length;) {
michael@0 3165 let window = state.windows[wIndex];
michael@0 3166 window.selected = window.selected || 1;
michael@0 3167 // We're going to put the state of the window into this object
michael@0 3168 let pinnedWindowState = { tabs: [], cookies: []};
michael@0 3169 for (let tIndex = 0; tIndex < window.tabs.length;) {
michael@0 3170 if (window.tabs[tIndex].pinned) {
michael@0 3171 // Adjust window.selected
michael@0 3172 if (tIndex + 1 < window.selected)
michael@0 3173 window.selected -= 1;
michael@0 3174 else if (tIndex + 1 == window.selected)
michael@0 3175 pinnedWindowState.selected = pinnedWindowState.tabs.length + 2;
michael@0 3176 // + 2 because the tab isn't actually in the array yet
michael@0 3177
michael@0 3178 // Now add the pinned tab to our window
michael@0 3179 pinnedWindowState.tabs =
michael@0 3180 pinnedWindowState.tabs.concat(window.tabs.splice(tIndex, 1));
michael@0 3181 // We don't want to increment tIndex here.
michael@0 3182 continue;
michael@0 3183 }
michael@0 3184 tIndex++;
michael@0 3185 }
michael@0 3186
michael@0 3187 // At this point the window in the state object has been modified (or not)
michael@0 3188 // We want to build the rest of this new window object if we have pinnedTabs.
michael@0 3189 if (pinnedWindowState.tabs.length) {
michael@0 3190 // First get the other attributes off the window
michael@0 3191 WINDOW_ATTRIBUTES.forEach(function(attr) {
michael@0 3192 if (attr in window) {
michael@0 3193 pinnedWindowState[attr] = window[attr];
michael@0 3194 delete window[attr];
michael@0 3195 }
michael@0 3196 });
michael@0 3197 // We're just copying position data into the pinned window.
michael@0 3198 // Not copying over:
michael@0 3199 // - _closedTabs
michael@0 3200 // - extData
michael@0 3201 // - isPopup
michael@0 3202 // - hidden
michael@0 3203
michael@0 3204 // Assign a unique ID to correlate the window to be opened with the
michael@0 3205 // remaining data
michael@0 3206 window.__lastSessionWindowID = pinnedWindowState.__lastSessionWindowID
michael@0 3207 = "" + Date.now() + Math.random();
michael@0 3208
michael@0 3209 // Extract the cookies that belong with each pinned tab
michael@0 3210 this._splitCookiesFromWindow(window, pinnedWindowState);
michael@0 3211
michael@0 3212 // Actually add this window to our defaultState
michael@0 3213 defaultState.windows.push(pinnedWindowState);
michael@0 3214 // Remove the window from the state if it doesn't have any tabs
michael@0 3215 if (!window.tabs.length) {
michael@0 3216 if (wIndex + 1 <= state.selectedWindow)
michael@0 3217 state.selectedWindow -= 1;
michael@0 3218 else if (wIndex + 1 == state.selectedWindow)
michael@0 3219 defaultState.selectedIndex = defaultState.windows.length + 1;
michael@0 3220
michael@0 3221 state.windows.splice(wIndex, 1);
michael@0 3222 // We don't want to increment wIndex here.
michael@0 3223 continue;
michael@0 3224 }
michael@0 3225
michael@0 3226
michael@0 3227 }
michael@0 3228 wIndex++;
michael@0 3229 }
michael@0 3230
michael@0 3231 return [defaultState, state];
michael@0 3232 },
michael@0 3233
michael@0 3234 /**
michael@0 3235 * Splits out the cookies from aWinState into aTargetWinState based on the
michael@0 3236 * tabs that are in aTargetWinState.
michael@0 3237 * This alters the state of aWinState and aTargetWinState.
michael@0 3238 */
michael@0 3239 _splitCookiesFromWindow:
michael@0 3240 function ssi_splitCookiesFromWindow(aWinState, aTargetWinState) {
michael@0 3241 if (!aWinState.cookies || !aWinState.cookies.length)
michael@0 3242 return;
michael@0 3243
michael@0 3244 // Get the hosts for history entries in aTargetWinState
michael@0 3245 let cookieHosts = SessionCookies.getHostsForWindow(aTargetWinState);
michael@0 3246
michael@0 3247 // By creating a regex we reduce overhead and there is only one loop pass
michael@0 3248 // through either array (cookieHosts and aWinState.cookies).
michael@0 3249 let hosts = Object.keys(cookieHosts).join("|").replace("\\.", "\\.", "g");
michael@0 3250 // If we don't actually have any hosts, then we don't want to do anything.
michael@0 3251 if (!hosts.length)
michael@0 3252 return;
michael@0 3253 let cookieRegex = new RegExp(".*(" + hosts + ")");
michael@0 3254 for (let cIndex = 0; cIndex < aWinState.cookies.length;) {
michael@0 3255 if (cookieRegex.test(aWinState.cookies[cIndex].host)) {
michael@0 3256 aTargetWinState.cookies =
michael@0 3257 aTargetWinState.cookies.concat(aWinState.cookies.splice(cIndex, 1));
michael@0 3258 continue;
michael@0 3259 }
michael@0 3260 cIndex++;
michael@0 3261 }
michael@0 3262 },
michael@0 3263
michael@0 3264 /**
michael@0 3265 * Converts a JavaScript object into a JSON string
michael@0 3266 * (see http://www.json.org/ for more information).
michael@0 3267 *
michael@0 3268 * The inverse operation consists of JSON.parse(JSON_string).
michael@0 3269 *
michael@0 3270 * @param aJSObject is the object to be converted
michael@0 3271 * @returns the object's JSON representation
michael@0 3272 */
michael@0 3273 _toJSONString: function ssi_toJSONString(aJSObject) {
michael@0 3274 return JSON.stringify(aJSObject);
michael@0 3275 },
michael@0 3276
michael@0 3277 _sendRestoreCompletedNotifications: function ssi_sendRestoreCompletedNotifications() {
michael@0 3278 // not all windows restored, yet
michael@0 3279 if (this._restoreCount > 1) {
michael@0 3280 this._restoreCount--;
michael@0 3281 return;
michael@0 3282 }
michael@0 3283
michael@0 3284 // observers were already notified
michael@0 3285 if (this._restoreCount == -1)
michael@0 3286 return;
michael@0 3287
michael@0 3288 // This was the last window restored at startup, notify observers.
michael@0 3289 Services.obs.notifyObservers(null,
michael@0 3290 this._browserSetState ? NOTIFY_BROWSER_STATE_RESTORED : NOTIFY_WINDOWS_RESTORED,
michael@0 3291 "");
michael@0 3292
michael@0 3293 this._browserSetState = false;
michael@0 3294 this._restoreCount = -1;
michael@0 3295 },
michael@0 3296
michael@0 3297 /**
michael@0 3298 * Set the given window's busy state
michael@0 3299 * @param aWindow the window
michael@0 3300 * @param aValue the window's busy state
michael@0 3301 */
michael@0 3302 _setWindowStateBusyValue:
michael@0 3303 function ssi_changeWindowStateBusyValue(aWindow, aValue) {
michael@0 3304
michael@0 3305 this._windows[aWindow.__SSi].busy = aValue;
michael@0 3306
michael@0 3307 // Keep the to-be-restored state in sync because that is returned by
michael@0 3308 // getWindowState() as long as the window isn't loaded, yet.
michael@0 3309 if (!this._isWindowLoaded(aWindow)) {
michael@0 3310 let stateToRestore = this._statesToRestore[aWindow.__SS_restoreID].windows[0];
michael@0 3311 stateToRestore.busy = aValue;
michael@0 3312 }
michael@0 3313 },
michael@0 3314
michael@0 3315 /**
michael@0 3316 * Set the given window's state to 'not busy'.
michael@0 3317 * @param aWindow the window
michael@0 3318 */
michael@0 3319 _setWindowStateReady: function ssi_setWindowStateReady(aWindow) {
michael@0 3320 this._setWindowStateBusyValue(aWindow, false);
michael@0 3321 this._sendWindowStateEvent(aWindow, "Ready");
michael@0 3322 },
michael@0 3323
michael@0 3324 /**
michael@0 3325 * Set the given window's state to 'busy'.
michael@0 3326 * @param aWindow the window
michael@0 3327 */
michael@0 3328 _setWindowStateBusy: function ssi_setWindowStateBusy(aWindow) {
michael@0 3329 this._setWindowStateBusyValue(aWindow, true);
michael@0 3330 this._sendWindowStateEvent(aWindow, "Busy");
michael@0 3331 },
michael@0 3332
michael@0 3333 /**
michael@0 3334 * Dispatch an SSWindowState_____ event for the given window.
michael@0 3335 * @param aWindow the window
michael@0 3336 * @param aType the type of event, SSWindowState will be prepended to this string
michael@0 3337 */
michael@0 3338 _sendWindowStateEvent: function ssi_sendWindowStateEvent(aWindow, aType) {
michael@0 3339 let event = aWindow.document.createEvent("Events");
michael@0 3340 event.initEvent("SSWindowState" + aType, true, false);
michael@0 3341 aWindow.dispatchEvent(event);
michael@0 3342 },
michael@0 3343
michael@0 3344 /**
michael@0 3345 * Dispatch the SSTabRestored event for the given tab.
michael@0 3346 * @param aTab the which has been restored
michael@0 3347 */
michael@0 3348 _sendTabRestoredNotification: function ssi_sendTabRestoredNotification(aTab) {
michael@0 3349 let event = aTab.ownerDocument.createEvent("Events");
michael@0 3350 event.initEvent("SSTabRestored", true, false);
michael@0 3351 aTab.dispatchEvent(event);
michael@0 3352 },
michael@0 3353
michael@0 3354 /**
michael@0 3355 * @param aWindow
michael@0 3356 * Window reference
michael@0 3357 * @returns whether this window's data is still cached in _statesToRestore
michael@0 3358 * because it's not fully loaded yet
michael@0 3359 */
michael@0 3360 _isWindowLoaded: function ssi_isWindowLoaded(aWindow) {
michael@0 3361 return !aWindow.__SS_restoreID;
michael@0 3362 },
michael@0 3363
michael@0 3364 /**
michael@0 3365 * Replace "Loading..." with the tab label (with minimal side-effects)
michael@0 3366 * @param aString is the string the title is stored in
michael@0 3367 * @param aTabbrowser is a tabbrowser object, containing aTab
michael@0 3368 * @param aTab is the tab whose title we're updating & using
michael@0 3369 *
michael@0 3370 * @returns aString that has been updated with the new title
michael@0 3371 */
michael@0 3372 _replaceLoadingTitle : function ssi_replaceLoadingTitle(aString, aTabbrowser, aTab) {
michael@0 3373 if (aString == aTabbrowser.mStringBundle.getString("tabs.connecting")) {
michael@0 3374 aTabbrowser.setTabTitle(aTab);
michael@0 3375 [aString, aTab.label] = [aTab.label, aString];
michael@0 3376 }
michael@0 3377 return aString;
michael@0 3378 },
michael@0 3379
michael@0 3380 /**
michael@0 3381 * Resize this._closedWindows to the value of the pref, except in the case
michael@0 3382 * where we don't have any non-popup windows on Windows and Linux. Then we must
michael@0 3383 * resize such that we have at least one non-popup window.
michael@0 3384 */
michael@0 3385 _capClosedWindows : function ssi_capClosedWindows() {
michael@0 3386 if (this._closedWindows.length <= this._max_windows_undo)
michael@0 3387 return;
michael@0 3388 let spliceTo = this._max_windows_undo;
michael@0 3389 #ifndef XP_MACOSX
michael@0 3390 let normalWindowIndex = 0;
michael@0 3391 // try to find a non-popup window in this._closedWindows
michael@0 3392 while (normalWindowIndex < this._closedWindows.length &&
michael@0 3393 !!this._closedWindows[normalWindowIndex].isPopup)
michael@0 3394 normalWindowIndex++;
michael@0 3395 if (normalWindowIndex >= this._max_windows_undo)
michael@0 3396 spliceTo = normalWindowIndex + 1;
michael@0 3397 #endif
michael@0 3398 this._closedWindows.splice(spliceTo, this._closedWindows.length);
michael@0 3399 },
michael@0 3400
michael@0 3401 /**
michael@0 3402 * Clears the set of windows that are "resurrected" before writing to disk to
michael@0 3403 * make closing windows one after the other until shutdown work as expected.
michael@0 3404 *
michael@0 3405 * This function should only be called when we are sure that there has been
michael@0 3406 * a user action that indicates the browser is actively being used and all
michael@0 3407 * windows that have been closed before are not part of a series of closing
michael@0 3408 * windows.
michael@0 3409 */
michael@0 3410 _clearRestoringWindows: function ssi_clearRestoringWindows() {
michael@0 3411 for (let i = 0; i < this._closedWindows.length; i++) {
michael@0 3412 delete this._closedWindows[i]._shouldRestore;
michael@0 3413 }
michael@0 3414 },
michael@0 3415
michael@0 3416 /**
michael@0 3417 * Reset state to prepare for a new session state to be restored.
michael@0 3418 */
michael@0 3419 _resetRestoringState: function ssi_initRestoringState() {
michael@0 3420 TabRestoreQueue.reset();
michael@0 3421 this._tabsRestoringCount = 0;
michael@0 3422 },
michael@0 3423
michael@0 3424 /**
michael@0 3425 * Reset the restoring state for a particular tab. This will be called when
michael@0 3426 * removing a tab or when a tab needs to be reset (it's being overwritten).
michael@0 3427 *
michael@0 3428 * @param aTab
michael@0 3429 * The tab that will be "reset"
michael@0 3430 */
michael@0 3431 _resetLocalTabRestoringState: function (aTab) {
michael@0 3432 let window = aTab.ownerDocument.defaultView;
michael@0 3433 let browser = aTab.linkedBrowser;
michael@0 3434
michael@0 3435 // Keep the tab's previous state for later in this method
michael@0 3436 let previousState = browser.__SS_restoreState;
michael@0 3437
michael@0 3438 // The browser is no longer in any sort of restoring state.
michael@0 3439 delete browser.__SS_restoreState;
michael@0 3440 this._browserEpochs.delete(browser.permanentKey);
michael@0 3441
michael@0 3442 aTab.removeAttribute("pending");
michael@0 3443 browser.removeAttribute("pending");
michael@0 3444
michael@0 3445 if (previousState == TAB_STATE_RESTORING) {
michael@0 3446 if (this._tabsRestoringCount)
michael@0 3447 this._tabsRestoringCount--;
michael@0 3448 } else if (previousState == TAB_STATE_NEEDS_RESTORE) {
michael@0 3449 // Make sure that the tab is removed from the list of tabs to restore.
michael@0 3450 // Again, this is normally done in restoreTabContent, but that isn't being called
michael@0 3451 // for this tab.
michael@0 3452 TabRestoreQueue.remove(aTab);
michael@0 3453 }
michael@0 3454 },
michael@0 3455
michael@0 3456 _resetTabRestoringState: function (tab) {
michael@0 3457 let browser = tab.linkedBrowser;
michael@0 3458 if (browser.__SS_restoreState) {
michael@0 3459 browser.messageManager.sendAsyncMessage("SessionStore:resetRestore", {});
michael@0 3460 }
michael@0 3461 this._resetLocalTabRestoringState(tab);
michael@0 3462 },
michael@0 3463
michael@0 3464 /**
michael@0 3465 * Each time a <browser> element is restored, we increment its "epoch". To
michael@0 3466 * check if a message from content-sessionStore.js is out of date, we can
michael@0 3467 * compare the epoch received with the message to the <browser> element's
michael@0 3468 * epoch. This function does that, and returns true if |epoch| is up-to-date
michael@0 3469 * with respect to |browser|.
michael@0 3470 */
michael@0 3471 isCurrentEpoch: function (browser, epoch) {
michael@0 3472 return this._browserEpochs.get(browser.permanentKey, 0) == epoch;
michael@0 3473 },
michael@0 3474
michael@0 3475 };
michael@0 3476
michael@0 3477 /**
michael@0 3478 * Priority queue that keeps track of a list of tabs to restore and returns
michael@0 3479 * the tab we should restore next, based on priority rules. We decide between
michael@0 3480 * pinned, visible and hidden tabs in that and FIFO order. Hidden tabs are only
michael@0 3481 * restored with restore_hidden_tabs=true.
michael@0 3482 */
michael@0 3483 let TabRestoreQueue = {
michael@0 3484 // The separate buckets used to store tabs.
michael@0 3485 tabs: {priority: [], visible: [], hidden: []},
michael@0 3486
michael@0 3487 // Preferences used by the TabRestoreQueue to determine which tabs
michael@0 3488 // are restored automatically and which tabs will be on-demand.
michael@0 3489 prefs: {
michael@0 3490 // Lazy getter that returns whether tabs are restored on demand.
michael@0 3491 get restoreOnDemand() {
michael@0 3492 let updateValue = () => {
michael@0 3493 let value = Services.prefs.getBoolPref(PREF);
michael@0 3494 let definition = {value: value, configurable: true};
michael@0 3495 Object.defineProperty(this, "restoreOnDemand", definition);
michael@0 3496 return value;
michael@0 3497 }
michael@0 3498
michael@0 3499 const PREF = "browser.sessionstore.restore_on_demand";
michael@0 3500 Services.prefs.addObserver(PREF, updateValue, false);
michael@0 3501 return updateValue();
michael@0 3502 },
michael@0 3503
michael@0 3504 // Lazy getter that returns whether pinned tabs are restored on demand.
michael@0 3505 get restorePinnedTabsOnDemand() {
michael@0 3506 let updateValue = () => {
michael@0 3507 let value = Services.prefs.getBoolPref(PREF);
michael@0 3508 let definition = {value: value, configurable: true};
michael@0 3509 Object.defineProperty(this, "restorePinnedTabsOnDemand", definition);
michael@0 3510 return value;
michael@0 3511 }
michael@0 3512
michael@0 3513 const PREF = "browser.sessionstore.restore_pinned_tabs_on_demand";
michael@0 3514 Services.prefs.addObserver(PREF, updateValue, false);
michael@0 3515 return updateValue();
michael@0 3516 },
michael@0 3517
michael@0 3518 // Lazy getter that returns whether we should restore hidden tabs.
michael@0 3519 get restoreHiddenTabs() {
michael@0 3520 let updateValue = () => {
michael@0 3521 let value = Services.prefs.getBoolPref(PREF);
michael@0 3522 let definition = {value: value, configurable: true};
michael@0 3523 Object.defineProperty(this, "restoreHiddenTabs", definition);
michael@0 3524 return value;
michael@0 3525 }
michael@0 3526
michael@0 3527 const PREF = "browser.sessionstore.restore_hidden_tabs";
michael@0 3528 Services.prefs.addObserver(PREF, updateValue, false);
michael@0 3529 return updateValue();
michael@0 3530 }
michael@0 3531 },
michael@0 3532
michael@0 3533 // Resets the queue and removes all tabs.
michael@0 3534 reset: function () {
michael@0 3535 this.tabs = {priority: [], visible: [], hidden: []};
michael@0 3536 },
michael@0 3537
michael@0 3538 // Adds a tab to the queue and determines its priority bucket.
michael@0 3539 add: function (tab) {
michael@0 3540 let {priority, hidden, visible} = this.tabs;
michael@0 3541
michael@0 3542 if (tab.pinned) {
michael@0 3543 priority.push(tab);
michael@0 3544 } else if (tab.hidden) {
michael@0 3545 hidden.push(tab);
michael@0 3546 } else {
michael@0 3547 visible.push(tab);
michael@0 3548 }
michael@0 3549 },
michael@0 3550
michael@0 3551 // Removes a given tab from the queue, if it's in there.
michael@0 3552 remove: function (tab) {
michael@0 3553 let {priority, hidden, visible} = this.tabs;
michael@0 3554
michael@0 3555 // We'll always check priority first since we don't
michael@0 3556 // have an indicator if a tab will be there or not.
michael@0 3557 let set = priority;
michael@0 3558 let index = set.indexOf(tab);
michael@0 3559
michael@0 3560 if (index == -1) {
michael@0 3561 set = tab.hidden ? hidden : visible;
michael@0 3562 index = set.indexOf(tab);
michael@0 3563 }
michael@0 3564
michael@0 3565 if (index > -1) {
michael@0 3566 set.splice(index, 1);
michael@0 3567 }
michael@0 3568 },
michael@0 3569
michael@0 3570 // Returns and removes the tab with the highest priority.
michael@0 3571 shift: function () {
michael@0 3572 let set;
michael@0 3573 let {priority, hidden, visible} = this.tabs;
michael@0 3574
michael@0 3575 let {restoreOnDemand, restorePinnedTabsOnDemand} = this.prefs;
michael@0 3576 let restorePinned = !(restoreOnDemand && restorePinnedTabsOnDemand);
michael@0 3577 if (restorePinned && priority.length) {
michael@0 3578 set = priority;
michael@0 3579 } else if (!restoreOnDemand) {
michael@0 3580 if (visible.length) {
michael@0 3581 set = visible;
michael@0 3582 } else if (this.prefs.restoreHiddenTabs && hidden.length) {
michael@0 3583 set = hidden;
michael@0 3584 }
michael@0 3585 }
michael@0 3586
michael@0 3587 return set && set.shift();
michael@0 3588 },
michael@0 3589
michael@0 3590 // Moves a given tab from the 'hidden' to the 'visible' bucket.
michael@0 3591 hiddenToVisible: function (tab) {
michael@0 3592 let {hidden, visible} = this.tabs;
michael@0 3593 let index = hidden.indexOf(tab);
michael@0 3594
michael@0 3595 if (index > -1) {
michael@0 3596 hidden.splice(index, 1);
michael@0 3597 visible.push(tab);
michael@0 3598 } else {
michael@0 3599 throw new Error("restore queue: hidden tab not found");
michael@0 3600 }
michael@0 3601 },
michael@0 3602
michael@0 3603 // Moves a given tab from the 'visible' to the 'hidden' bucket.
michael@0 3604 visibleToHidden: function (tab) {
michael@0 3605 let {visible, hidden} = this.tabs;
michael@0 3606 let index = visible.indexOf(tab);
michael@0 3607
michael@0 3608 if (index > -1) {
michael@0 3609 visible.splice(index, 1);
michael@0 3610 hidden.push(tab);
michael@0 3611 } else {
michael@0 3612 throw new Error("restore queue: visible tab not found");
michael@0 3613 }
michael@0 3614 }
michael@0 3615 };
michael@0 3616
michael@0 3617 // A map storing a closed window's state data until it goes aways (is GC'ed).
michael@0 3618 // This ensures that API clients can still read (but not write) states of
michael@0 3619 // windows they still hold a reference to but we don't.
michael@0 3620 let DyingWindowCache = {
michael@0 3621 _data: new WeakMap(),
michael@0 3622
michael@0 3623 has: function (window) {
michael@0 3624 return this._data.has(window);
michael@0 3625 },
michael@0 3626
michael@0 3627 get: function (window) {
michael@0 3628 return this._data.get(window);
michael@0 3629 },
michael@0 3630
michael@0 3631 set: function (window, data) {
michael@0 3632 this._data.set(window, data);
michael@0 3633 },
michael@0 3634
michael@0 3635 remove: function (window) {
michael@0 3636 this._data.delete(window);
michael@0 3637 }
michael@0 3638 };
michael@0 3639
michael@0 3640 // A weak set of dirty windows. We use it to determine which windows we need to
michael@0 3641 // recollect data for when getCurrentState() is called.
michael@0 3642 let DirtyWindows = {
michael@0 3643 _data: new WeakMap(),
michael@0 3644
michael@0 3645 has: function (window) {
michael@0 3646 return this._data.has(window);
michael@0 3647 },
michael@0 3648
michael@0 3649 add: function (window) {
michael@0 3650 return this._data.set(window, true);
michael@0 3651 },
michael@0 3652
michael@0 3653 remove: function (window) {
michael@0 3654 this._data.delete(window);
michael@0 3655 },
michael@0 3656
michael@0 3657 clear: function (window) {
michael@0 3658 this._data.clear();
michael@0 3659 }
michael@0 3660 };
michael@0 3661
michael@0 3662 // The state from the previous session (after restoring pinned tabs). This
michael@0 3663 // state is persisted and passed through to the next session during an app
michael@0 3664 // restart to make the third party add-on warning not trash the deferred
michael@0 3665 // session
michael@0 3666 let LastSession = {
michael@0 3667 _state: null,
michael@0 3668
michael@0 3669 get canRestore() {
michael@0 3670 return !!this._state;
michael@0 3671 },
michael@0 3672
michael@0 3673 getState: function () {
michael@0 3674 return this._state;
michael@0 3675 },
michael@0 3676
michael@0 3677 setState: function (state) {
michael@0 3678 this._state = state;
michael@0 3679 },
michael@0 3680
michael@0 3681 clear: function () {
michael@0 3682 if (this._state) {
michael@0 3683 this._state = null;
michael@0 3684 Services.obs.notifyObservers(null, NOTIFY_LAST_SESSION_CLEARED, null);
michael@0 3685 }
michael@0 3686 }
michael@0 3687 };

mercurial