Wed, 31 Dec 2014 06:09:35 +0100
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 | }; |