/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ this.EXPORTED_SYMBOLS = ["SessionStore"]; const Cu = Components.utils; const Cc = Components.classes; const Ci = Components.interfaces; const Cr = Components.results; const STATE_STOPPED = 0; const STATE_RUNNING = 1; const STATE_QUITTING = -1; const STATE_STOPPED_STR = "stopped"; const STATE_RUNNING_STR = "running"; const TAB_STATE_NEEDS_RESTORE = 1; const TAB_STATE_RESTORING = 2; const PRIVACY_NONE = 0; const PRIVACY_ENCRYPTED = 1; const PRIVACY_FULL = 2; const NOTIFY_WINDOWS_RESTORED = "sessionstore-windows-restored"; const NOTIFY_BROWSER_STATE_RESTORED = "sessionstore-browser-state-restored"; // Default maximum number of tabs to restore simultaneously. Controlled by // the browser.sessionstore.max_concurrent_tabs pref. const DEFAULT_MAX_CONCURRENT_TAB_RESTORES = 3; // global notifications observed const OBSERVING = [ "domwindowopened", "domwindowclosed", "quit-application-requested", "quit-application-granted", "browser-lastwindow-close-granted", "quit-application", "browser:purge-session-history", "browser:purge-domain-data" ]; // XUL Window properties to (re)store // Restored in restoreDimensions() const WINDOW_ATTRIBUTES = ["width", "height", "screenX", "screenY", "sizemode"]; // Hideable window features to (re)store // Restored in restoreWindowFeatures() const WINDOW_HIDEABLE_FEATURES = [ "menubar", "toolbar", "locationbar", "personalbar", "statusbar", "scrollbars" ]; const MESSAGES = [ // The content script tells us that its form data (or that of one of its // subframes) might have changed. This can be the contents or values of // standard form fields or of ContentEditables. "SessionStore:input", // The content script has received a pageshow event. This happens when a // page is loaded from bfcache without any network activity, i.e. when // clicking the back or forward button. "SessionStore:pageshow" ]; // These are tab events that we listen to. const TAB_EVENTS = [ "TabOpen", "TabClose", "TabSelect", "TabShow", "TabHide", "TabPinned", "TabUnpinned" ]; #ifndef XP_WIN #define BROKEN_WM_Z_ORDER #endif Cu.import("resource://gre/modules/Services.jsm", this); Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); // debug.js adds NS_ASSERT. cf. bug 669196 Cu.import("resource://gre/modules/debug.js", this); Cu.import("resource://gre/modules/osfile.jsm", this); Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm", this); Cu.import("resource://gre/modules/Promise.jsm", this); XPCOMUtils.defineLazyServiceGetter(this, "gSessionStartup", "@mozilla.org/browser/sessionstartup;1", "nsISessionStartup"); XPCOMUtils.defineLazyServiceGetter(this, "gScreenManager", "@mozilla.org/gfx/screenmanager;1", "nsIScreenManager"); // List of docShell capabilities to (re)store. These are automatically // retrieved from a given docShell if not already collected before. // This is made so they're automatically in sync with all nsIDocShell.allow* // properties. var gDocShellCapabilities = (function() { let caps; return docShell => { if (!caps) { let keys = Object.keys(docShell); caps = keys.filter(k => k.startsWith("allow")).map(k => k.slice(5)); } return caps; }; })(); XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm"); #ifdef MOZ_DEVTOOLS XPCOMUtils.defineLazyModuleGetter(this, "ScratchpadManager", "resource://devtools/client/scratchpad/scratchpad-manager.jsm"); Object.defineProperty(this, "HUDService", { get: function() { let devtools = Cu.import("resource://devtools/shared/Loader.jsm", {}).devtools; return devtools.require("devtools/client/webconsole/hudservice").HUDService; }, configurable: true, enumerable: true }); #endif XPCOMUtils.defineLazyModuleGetter(this, "DocumentUtils", "resource:///modules/sessionstore/DocumentUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage", "resource:///modules/sessionstore/SessionStorage.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "_SessionFile", "resource:///modules/sessionstore/_SessionFile.jsm"); function debug(aMsg) { aMsg = ("SessionStore: " + aMsg).replace(/\S{80}/g, "$&\n"); Services.console.logStringMessage(aMsg); } this.SessionStore = { get promiseInitialized() { return SessionStoreInternal.promiseInitialized.promise; }, get canRestoreLastSession() { return SessionStoreInternal.canRestoreLastSession; }, set canRestoreLastSession(val) { SessionStoreInternal.canRestoreLastSession = val; }, init: function(aWindow) { return SessionStoreInternal.init(aWindow); }, getBrowserState: function() { return SessionStoreInternal.getBrowserState(); }, setBrowserState: function(aState) { SessionStoreInternal.setBrowserState(aState); }, getWindowState: function(aWindow) { return SessionStoreInternal.getWindowState(aWindow); }, setWindowState: function(aWindow, aState, aOverwrite) { SessionStoreInternal.setWindowState(aWindow, aState, aOverwrite); }, getTabState: function(aTab) { return SessionStoreInternal.getTabState(aTab); }, setTabState: function(aTab, aState) { SessionStoreInternal.setTabState(aTab, aState); }, duplicateTab: function(aWindow, aTab, aDelta) { return SessionStoreInternal.duplicateTab(aWindow, aTab, aDelta); }, getClosedTabCount: function(aWindow) { return SessionStoreInternal.getClosedTabCount(aWindow); }, getClosedTabData: function(aWindow) { return SessionStoreInternal.getClosedTabData(aWindow); }, undoCloseTab: function(aWindow, aIndex) { return SessionStoreInternal.undoCloseTab(aWindow, aIndex); }, forgetClosedTab: function(aWindow, aIndex) { return SessionStoreInternal.forgetClosedTab(aWindow, aIndex); }, getClosedWindowCount: function() { return SessionStoreInternal.getClosedWindowCount(); }, getClosedWindowData: function() { return SessionStoreInternal.getClosedWindowData(); }, undoCloseWindow: function(aIndex) { return SessionStoreInternal.undoCloseWindow(aIndex); }, forgetClosedWindow: function(aIndex) { return SessionStoreInternal.forgetClosedWindow(aIndex); }, getWindowValue: function(aWindow, aKey) { return SessionStoreInternal.getWindowValue(aWindow, aKey); }, setWindowValue: function(aWindow, aKey, aStringValue) { SessionStoreInternal.setWindowValue(aWindow, aKey, aStringValue); }, deleteWindowValue: function(aWindow, aKey) { SessionStoreInternal.deleteWindowValue(aWindow, aKey); }, getTabValue: function(aTab, aKey) { return SessionStoreInternal.getTabValue(aTab, aKey); }, setTabValue: function(aTab, aKey, aStringValue) { SessionStoreInternal.setTabValue(aTab, aKey, aStringValue); }, deleteTabValue: function(aTab, aKey) { SessionStoreInternal.deleteTabValue(aTab, aKey); }, persistTabAttribute: function(aName) { SessionStoreInternal.persistTabAttribute(aName); }, restoreLastSession: function() { SessionStoreInternal.restoreLastSession(); }, checkPrivacyLevel: function(aIsHTTPS, aUseDefaultPref) { return SessionStoreInternal.checkPrivacyLevel(aIsHTTPS, aUseDefaultPref); } }; // Freeze the SessionStore object. We don't want anyone to modify it. Object.freeze(SessionStore); var SessionStoreInternal = { QueryInterface: XPCOMUtils.generateQI([ Ci.nsIDOMEventListener, Ci.nsIObserver, Ci.nsISupportsWeakReference ]), // set default load state _loadState: STATE_STOPPED, // During the initial restore and setBrowserState calls tracks the number of // windows yet to be restored _restoreCount: -1, // whether a setBrowserState call is in progress _browserSetState: false, // time in milliseconds (Date.now()) when the session was last written to file _lastSaveTime: 0, // time in milliseconds when the session was started (saved across sessions), // defaults to now if no session was restored or timestamp doesn't exist _sessionStartTime: Date.now(), // states for all currently opened windows _windows: {}, // internal states for all open windows (data we need to associate, // but not write to disk) _internalWindows: {}, // states for all recently closed windows _closedWindows: [], // not-"dirty" windows usually don't need to have their data updated _dirtyWindows: {}, // collection of session states yet to be restored _statesToRestore: {}, // counts the number of crashes since the last clean start _recentCrashes: 0, // whether the last window was closed and should be restored _restoreLastWindow: false, // number of tabs currently restoring _tabsRestoringCount: 0, // max number of tabs to restore concurrently _maxConcurrentTabRestores: DEFAULT_MAX_CONCURRENT_TAB_RESTORES, // whether restored tabs load cached versions or force a reload _cacheBehavior: 0, // The state from the previous session (after restoring pinned tabs). This // state is persisted and passed through to the next session during an app // restart to make the third party add-on warning not trash the deferred // session _lastSessionState: null, // When starting Firefox with a single private window, this is the place // where we keep the session we actually wanted to restore in case the user // decides to later open a non-private window as well. _deferredInitialState: null, // A promise resolved once initialization is complete _promiseInitialization: Promise.defer(), // Whether session has been initialized _sessionInitialized: false, // True if session store is disabled by multi-process browsing. // See bug 516755. _disabledForMultiProcess: false, // The original "sessionstore.resume_session_once" preference value before it // was modified by saveState. saveState will set the // "sessionstore.resume_session_once" to true when the // the "sessionstore.resume_from_crash" preference is false (crash recovery // is disabled) so that pinned tabs will be restored in the case of a // crash. This variable is used to restore the original value so the // previous session is not always restored when // "sessionstore.resume_from_crash" is true. _resume_session_once_on_shutdown: null, /** * A promise fulfilled once initialization is complete. */ get promiseInitialized() { return this._promiseInitialization; }, /* ........ Public Getters .............. */ get canRestoreLastSession() { return this._lastSessionState; }, set canRestoreLastSession(val) { this._lastSessionState = null; }, /* ........ Global Event Handlers .............. */ /** * Initialize the component */ initService: function() { if (this._sessionInitialized) { return; } OBSERVING.forEach(function(aTopic) { Services.obs.addObserver(this, aTopic, true); }, this); this._initPrefs(); this._disabledForMultiProcess = false; // this pref is only read at startup, so no need to observe it this._sessionhistory_max_entries = this._prefBranch.getIntPref("sessionhistory.max_entries"); gSessionStartup.onceInitialized.then( this.initSession.bind(this) ); }, initSession: function() { let ss = gSessionStartup; try { if (ss.doRestore() || ss.sessionType == Ci.nsISessionStartup.DEFER_SESSION) this._initialState = ss.state; } catch(ex) { dump(ex + "\n"); } // no state to restore, which is ok if (this._initialState) { try { // If we're doing a DEFERRED session, then we want to pull pinned tabs // out so they can be restored. if (ss.sessionType == Ci.nsISessionStartup.DEFER_SESSION) { let [iniState, remainingState] = this._prepDataForDeferredRestore(this._initialState); // If we have a iniState with windows, that means that we have windows // with app tabs to restore. if (iniState.windows.length) this._initialState = iniState; else this._initialState = null; if (remainingState.windows.length) this._lastSessionState = remainingState; } else { // Get the last deferred session in case the user still wants to // restore it this._lastSessionState = this._initialState.lastSessionState; let lastSessionCrashed = this._initialState.session && this._initialState.session.state && this._initialState.session.state == STATE_RUNNING_STR; if (lastSessionCrashed) { this._recentCrashes = (this._initialState.session && this._initialState.session.recentCrashes || 0) + 1; if (this._needsRestorePage(this._initialState, this._recentCrashes)) { // replace the crashed session with a restore-page-only session let pageData = { url: "about:sessionrestore", formdata: { id: { "sessionData": this._initialState }, xpath: {} } }; this._initialState = { windows: [{ tabs: [{ entries: [pageData] }] }] }; } } // Load the session start time from the previous state this._sessionStartTime = this._initialState.session && this._initialState.session.startTime || this._sessionStartTime; // make sure that at least the first window doesn't have anything hidden delete this._initialState.windows[0].hidden; // Since nothing is hidden in the first window, it cannot be a popup delete this._initialState.windows[0].isPopup; // We don't want to minimize and then open a window at startup. if (this._initialState.windows[0].sizemode == "minimized") this._initialState.windows[0].sizemode = "normal"; // clear any lastSessionWindowID attributes since those don't matter // during normal restore this._initialState.windows.forEach(function(aWindow) { delete aWindow.__lastSessionWindowID; }); } } catch (ex) { debug("The session file is invalid: " + ex); } } // A Lazy getter for the sessionstore.js backup promise. XPCOMUtils.defineLazyGetter(this, "_backupSessionFileOnce", function() { return _SessionFile.createBackupCopy(); }); // at this point, we've as good as resumed the session, so we can // clear the resume_session_once flag, if it's set if (this._loadState != STATE_QUITTING && this._prefBranch.getBoolPref("sessionstore.resume_session_once")) this._prefBranch.setBoolPref("sessionstore.resume_session_once", false); this._initEncoding(); // Session is ready. this._sessionInitialized = true; this._promiseInitialization.resolve(); }, _initEncoding : function() { // The (UTF-8) encoder used to write to files. XPCOMUtils.defineLazyGetter(this, "_writeFileEncoder", function() { return new TextEncoder(); }); }, _initPrefs : function() { XPCOMUtils.defineLazyGetter(this, "_prefBranch", function() { return Services.prefs.getBranch("browser."); }); // minimal interval between two save operations (in milliseconds) XPCOMUtils.defineLazyGetter(this, "_interval", function() { // used often, so caching/observing instead of fetching on-demand this._prefBranch.addObserver("sessionstore.interval", this, true); return this._prefBranch.getIntPref("sessionstore.interval"); }); // when crash recovery is disabled, session data is not written to disk XPCOMUtils.defineLazyGetter(this, "_resume_from_crash", function() { // get crash recovery state from prefs and allow for proper reaction to state changes this._prefBranch.addObserver("sessionstore.resume_from_crash", this, true); return this._prefBranch.getBoolPref("sessionstore.resume_from_crash"); }); this._max_tabs_undo = this._prefBranch.getIntPref("sessionstore.max_tabs_undo"); this._prefBranch.addObserver("sessionstore.max_tabs_undo", this, true); this._max_windows_undo = this._prefBranch.getIntPref("sessionstore.max_windows_undo"); this._prefBranch.addObserver("sessionstore.max_windows_undo", this, true); // Straight-up collect the following one-time prefs this._maxConcurrentTabRestores = Services.prefs.getIntPref("browser.sessionstore.max_concurrent_tabs"); // ensure a sane value for concurrency, ignore and set default otherwise if (this._maxConcurrentTabRestores < 1 || this._maxConcurrentTabRestores > 10) { this._maxConcurrentTabRestores = DEFAULT_MAX_CONCURRENT_TAB_RESTORES; } this._cacheBehavior = Services.prefs.getIntPref("browser.sessionstore.cache_behavior"); }, _initWindow: function(aWindow) { if (aWindow) { this.onLoad(aWindow); } else if (this._loadState == STATE_STOPPED) { // If init is being called with a null window, it's possible that we // just want to tell sessionstore that a session is live (as is the case // with starting Firefox with -private, for example; see bug 568816), // so we should mark the load state as running to make sure that // things like setBrowserState calls will succeed in restoring the session. this._loadState = STATE_RUNNING; } }, /** * Start tracking a window. * * This function also initializes the component if it is not * initialized yet. */ init: function(aWindow) { let self = this; this.initService(); return this._promiseInitialization.promise.then( function onSuccess() { self._initWindow(aWindow); } ); }, /** * Called on application shutdown, after notifications: * quit-application-granted, quit-application */ _uninit: function() { // save all data for session resuming if (this._sessionInitialized) this.saveState(true); // clear out priority queue in case it's still holding refs TabRestoreQueue.reset(); // Make sure to break our cycle with the save timer if (this._saveTimer) { this._saveTimer.cancel(); this._saveTimer = null; } }, /** * Handle notifications */ observe: function(aSubject, aTopic, aData) { if (this._disabledForMultiProcess) return; switch (aTopic) { case "domwindowopened": // catch new windows this.onOpen(aSubject); break; case "domwindowclosed": // catch closed windows this.onClose(aSubject); break; case "quit-application-requested": this.onQuitApplicationRequested(); break; case "quit-application-granted": this.onQuitApplicationGranted(); break; case "browser-lastwindow-close-granted": this.onLastWindowCloseGranted(); break; case "quit-application": this.onQuitApplication(aData); break; case "browser:purge-session-history": // catch sanitization this.onPurgeSessionHistory(); break; case "browser:purge-domain-data": this.onPurgeDomainData(aData); break; case "nsPref:changed": // catch pref changes this.onPrefChange(aData); break; case "timer-callback": // timer call back for delayed saving this.onTimerCallback(); break; } }, /** * This method handles incoming messages sent by the session store content * script and thus enables communication with OOP tabs. */ receiveMessage: function(aMessage) { var browser = aMessage.target; var win = browser.ownerDocument.defaultView; switch (aMessage.name) { case "SessionStore:pageshow": this.onTabLoad(win, browser); break; case "SessionStore:input": this.onTabInput(win, browser); break; default: debug("received unknown message '" + aMessage.name + "'"); break; } this._clearRestoringWindows(); }, /* ........ Window Event Handlers .............. */ /** * Implement nsIDOMEventListener for handling various window and tab events */ handleEvent: function(aEvent) { if (this._disabledForMultiProcess) return; var win = aEvent.currentTarget.ownerDocument.defaultView; switch (aEvent.type) { case "load": // If __SS_restore_data is set, then we need to restore the document // (form data, scrolling, etc.). This will only happen when a tab is // first restored. let browser = aEvent.currentTarget; if (browser.__SS_restore_data) this.restoreDocument(win, browser, aEvent); this.onTabLoad(win, browser); break; case "TabOpen": this.onTabAdd(win, aEvent.originalTarget); break; case "TabClose": // aEvent.detail determines if the tab was closed by moving to a different window if (!aEvent.detail) this.onTabClose(win, aEvent.originalTarget); this.onTabRemove(win, aEvent.originalTarget); break; case "TabSelect": this.onTabSelect(win); break; case "TabShow": this.onTabShow(win, aEvent.originalTarget); break; case "TabHide": this.onTabHide(win, aEvent.originalTarget); break; case "TabPinned": case "TabUnpinned": this.saveStateDelayed(win); break; } this._clearRestoringWindows(); }, /** * If it's the first window load since app start... * - determine if we're reloading after a crash or a forced-restart * - restore window state * - restart downloads * Set up event listeners for this window's tabs * @param aWindow * Window reference */ onLoad: function(aWindow) { // return if window has already been initialized if (aWindow && aWindow.__SSi && this._windows[aWindow.__SSi]) return; // ignore non-browser windows and windows opened while shutting down if (aWindow.document.documentElement.getAttribute("windowtype") != "navigator:browser" || this._loadState == STATE_QUITTING) return; // assign it a unique identifier (timestamp) aWindow.__SSi = "window" + Date.now(); // and create its data object this._windows[aWindow.__SSi] = { tabs: [], selected: 0, _closedTabs: [], busy: false }; // and create its internal data object this._internalWindows[aWindow.__SSi] = { hosts: {} } let isPrivateWindow = false; if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) this._windows[aWindow.__SSi].isPrivate = isPrivateWindow = true; if (!this._isWindowLoaded(aWindow)) this._windows[aWindow.__SSi]._restoring = true; if (!aWindow.toolbar.visible) this._windows[aWindow.__SSi].isPopup = true; // perform additional initialization when the first window is loading if (this._loadState == STATE_STOPPED) { this._loadState = STATE_RUNNING; this._lastSaveTime = Date.now(); // restore a crashed session resp. resume the last session if requested if (this._initialState) { if (isPrivateWindow) { // We're starting with a single private window. Save the state we // actually wanted to restore so that we can do it later in case // the user opens another, non-private window. this._deferredInitialState = gSessionStartup.state; delete this._initialState; // Nothing to restore now, notify observers things are complete. Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED, ""); } else { // make sure that the restored tabs are first in the window this._initialState._firstTabs = true; this._restoreCount = this._initialState.windows ? this._initialState.windows.length : 0; this.restoreWindow(aWindow, this._initialState, this._isCmdLineEmpty(aWindow, this._initialState)); delete this._initialState; // _loadState changed from "stopped" to "running" // force a save operation so that crashes happening during startup are correctly counted this.saveState(true); } } else { // Nothing to restore, notify observers things are complete. Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED, ""); // the next delayed save request should execute immediately this._lastSaveTime -= this._interval; } } // this window was opened by _openWindowWithState else if (!this._isWindowLoaded(aWindow)) { let followUp = this._statesToRestore[aWindow.__SS_restoreID].windows.length == 1; this.restoreWindow(aWindow, this._statesToRestore[aWindow.__SS_restoreID], true, followUp); } // The user opened another, non-private window after starting up with // a single private one. Let's restore the session we actually wanted to // restore at startup. else if (this._deferredInitialState && !isPrivateWindow && aWindow.toolbar.visible) { this._deferredInitialState._firstTabs = true; this._restoreCount = this._deferredInitialState.windows ? this._deferredInitialState.windows.length : 0; this.restoreWindow(aWindow, this._deferredInitialState, false); this._deferredInitialState = null; } else if (this._restoreLastWindow && aWindow.toolbar.visible && this._closedWindows.length && !isPrivateWindow) { // default to the most-recently closed window // don't use popup windows let closedWindowState = null; let closedWindowIndex; for (let i = 0; i < this._closedWindows.length; i++) { // Take the first non-popup, point our object at it, and break out. if (!this._closedWindows[i].isPopup) { closedWindowState = this._closedWindows[i]; closedWindowIndex = i; break; } } if (closedWindowState) { let newWindowState; #ifndef XP_MACOSX if (!this._doResumeSession()) { #endif // We want to split the window up into pinned tabs and unpinned tabs. // Pinned tabs should be restored. If there are any remaining tabs, // they should be added back to _closedWindows. // We'll cheat a little bit and reuse _prepDataForDeferredRestore // even though it wasn't built exactly for this. let [appTabsState, normalTabsState] = this._prepDataForDeferredRestore({ windows: [closedWindowState] }); // These are our pinned tabs, which we should restore if (appTabsState.windows.length) { newWindowState = appTabsState.windows[0]; delete newWindowState.__lastSessionWindowID; } // In case there were no unpinned tabs, remove the window from _closedWindows if (!normalTabsState.windows.length) { this._closedWindows.splice(closedWindowIndex, 1); } // Or update _closedWindows with the modified state else { delete normalTabsState.windows[0].__lastSessionWindowID; this._closedWindows[closedWindowIndex] = normalTabsState.windows[0]; } #ifndef XP_MACOSX } else { // If we're just restoring the window, make sure it gets removed from // _closedWindows. this._closedWindows.splice(closedWindowIndex, 1); newWindowState = closedWindowState; delete newWindowState.hidden; } #endif if (newWindowState) { // Ensure that the window state isn't hidden this._restoreCount = 1; let state = { windows: [newWindowState] }; this.restoreWindow(aWindow, state, this._isCmdLineEmpty(aWindow, state)); } } // we actually restored the session just now. this._prefBranch.setBoolPref("sessionstore.resume_session_once", false); } if (this._restoreLastWindow && aWindow.toolbar.visible) { // always reset (if not a popup window) // we don't want to restore a window directly after, for example, // undoCloseWindow was executed. this._restoreLastWindow = false; } var tabbrowser = aWindow.gBrowser; // add tab change listeners to all already existing tabs for (let i = 0; i < tabbrowser.tabs.length; i++) { this.onTabAdd(aWindow, tabbrowser.tabs[i], true); } // notification of tab add/remove/selection/show/hide TAB_EVENTS.forEach(function(aEvent) { tabbrowser.tabContainer.addEventListener(aEvent, this, true); }, this); }, /** * On window open * @param aWindow * Window reference */ onOpen: function(aWindow) { var _this = this; aWindow.addEventListener("load", function(aEvent) { aEvent.currentTarget.removeEventListener("load", arguments.callee, false); _this.onLoad(aEvent.currentTarget); }, false); return; }, /** * On window close... * - remove event listeners from tabs * - save all window data * @param aWindow * Window reference */ onClose: function(aWindow) { // this window was about to be restored - conserve its original data, if any let isFullyLoaded = this._isWindowLoaded(aWindow); if (!isFullyLoaded) { if (!aWindow.__SSi) aWindow.__SSi = "window" + Date.now(); this._windows[aWindow.__SSi] = this._statesToRestore[aWindow.__SS_restoreID]; delete this._statesToRestore[aWindow.__SS_restoreID]; delete aWindow.__SS_restoreID; } // ignore windows not tracked by SessionStore if (!aWindow.__SSi || !this._windows[aWindow.__SSi]) { return; } // notify that the session store will stop tracking this window so that // extensions can store any data about this window in session store before // that's not possible anymore let event = aWindow.document.createEvent("Events"); event.initEvent("SSWindowClosing", true, false); aWindow.dispatchEvent(event); if (this.windowToFocus && this.windowToFocus == aWindow) { delete this.windowToFocus; } var tabbrowser = aWindow.gBrowser; TAB_EVENTS.forEach(function(aEvent) { tabbrowser.tabContainer.removeEventListener(aEvent, this, true); }, this); // remove the progress listener for this window tabbrowser.removeTabsProgressListener(gRestoreTabsProgressListener); let winData = this._windows[aWindow.__SSi]; if (this._loadState == STATE_RUNNING) { // window not closed during a regular shut-down // update all window data for a last time this._collectWindowData(aWindow); if (isFullyLoaded) { winData.title = aWindow.content.document.title || tabbrowser.selectedTab.label; winData.title = this._replaceLoadingTitle(winData.title, tabbrowser, tabbrowser.selectedTab); let windows = {}; windows[aWindow.__SSi] = winData; this._updateCookies(windows); } #ifndef XP_MACOSX // Until we decide otherwise elsewhere, this window is part of a series // of closing windows to quit. winData._shouldRestore = true; #endif // Save the window if it has multiple tabs or a single saveable tab and // it's not private. if (!winData.isPrivate && (winData.tabs.length > 1 || (winData.tabs.length == 1 && this._shouldSaveTabState(winData.tabs[0])))) { // we don't want to save the busy state delete winData.busy; this._closedWindows.unshift(winData); this._capClosedWindows(); } // clear this window from the list delete this._windows[aWindow.__SSi]; delete this._internalWindows[aWindow.__SSi]; // save the state without this window to disk this.saveStateDelayed(); } for (let i = 0; i < tabbrowser.tabs.length; i++) { this.onTabRemove(aWindow, tabbrowser.tabs[i], true); } // Cache the window state until it is completely gone. DyingWindowCache.set(aWindow, winData); delete aWindow.__SSi; }, /** * On quit application requested */ onQuitApplicationRequested: function() { // get a current snapshot of all windows this._forEachBrowserWindow(function(aWindow) { this._collectWindowData(aWindow); }); // we must cache this because _getMostRecentBrowserWindow will always // return null by the time quit-application occurs var activeWindow = this._getMostRecentBrowserWindow(); if (activeWindow) this.activeWindowSSiCache = activeWindow.__SSi || ""; this._dirtyWindows = []; }, /** * On quit application granted */ onQuitApplicationGranted: function() { // freeze the data at what we've got (ignoring closing windows) this._loadState = STATE_QUITTING; }, /** * On last browser window close */ onLastWindowCloseGranted: function() { // last browser window is quitting. // remember to restore the last window when another browser window is opened // do not account for pref(resume_session_once) at this point, as it might be // set by another observer getting this notice after us this._restoreLastWindow = true; }, /** * On quitting application * @param aData * String type of quitting */ onQuitApplication: function(aData) { if (aData == "restart") { this._prefBranch.setBoolPref("sessionstore.resume_session_once", true); // The browser:purge-session-history notification fires after the // quit-application notification so unregister the // browser:purge-session-history notification to prevent clearing // session data on disk on a restart. It is also unnecessary to // perform any other sanitization processing on a restart as the // browser is about to exit anyway. Services.obs.removeObserver(this, "browser:purge-session-history"); } else if (this._resume_session_once_on_shutdown != null) { // if the sessionstore.resume_session_once preference was changed by // saveState because crash recovery is disabled then restore the // preference back to the value it was prior to that. This will prevent // SessionStore from always restoring the session when crash recovery is // disabled. this._prefBranch.setBoolPref("sessionstore.resume_session_once", this._resume_session_once_on_shutdown); } if (aData != "restart") { // Throw away the previous session on shutdown this._lastSessionState = null; } this._loadState = STATE_QUITTING; // just to be sure this._uninit(); }, /** * On purge of session history */ onPurgeSessionHistory: function() { var _this = this; _SessionFile.wipe(); // If the browser is shutting down, simply return after clearing the // session data on disk as this notification fires after the // quit-application notification so the browser is about to exit. if (this._loadState == STATE_QUITTING) return; this._lastSessionState = null; let openWindows = {}; this._forEachBrowserWindow(function(aWindow) { Array.forEach(aWindow.gBrowser.tabs, function(aTab) { delete aTab.linkedBrowser.__SS_data; delete aTab.linkedBrowser.__SS_tabStillLoading; delete aTab.linkedBrowser.__SS_formDataSaved; delete aTab.linkedBrowser.__SS_hostSchemeData; if (aTab.linkedBrowser.__SS_restoreState) this._resetTabRestoringState(aTab); }, this); openWindows[aWindow.__SSi] = true; }); // also clear all data about closed tabs and windows for (let ix in this._windows) { if (ix in openWindows) { this._windows[ix]._closedTabs = []; } else { delete this._windows[ix]; delete this._internalWindows[ix]; } } // also clear all data about closed windows this._closedWindows = []; // give the tabbrowsers a chance to clear their histories first var win = this._getMostRecentBrowserWindow(); if (win) win.setTimeout(function() { _this.saveState(true); }, 0); else if (this._loadState == STATE_RUNNING) this.saveState(true); // Delete the private browsing backed up state, if any if ("_stateBackup" in this) delete this._stateBackup; this._clearRestoringWindows(); }, /** * On purge of domain data * @param aData * String domain data */ onPurgeDomainData: function(aData) { // does a session history entry contain a url for the given domain? function containsDomain(aEntry) { try { if (this._getURIFromString(aEntry.url).host.hasRootDomain(aData)) return true; } catch (ex) { /* url had no host at all */ } return aEntry.children && aEntry.children.some(containsDomain, this); } // remove all closed tabs containing a reference to the given domain for (let ix in this._windows) { let closedTabs = this._windows[ix]._closedTabs; for (let i = closedTabs.length - 1; i >= 0; i--) { if (closedTabs[i].state.entries.some(containsDomain, this)) closedTabs.splice(i, 1); } } // remove all open & closed tabs containing a reference to the given // domain in closed windows for (let ix = this._closedWindows.length - 1; ix >= 0; ix--) { let closedTabs = this._closedWindows[ix]._closedTabs; let openTabs = this._closedWindows[ix].tabs; let openTabCount = openTabs.length; for (let i = closedTabs.length - 1; i >= 0; i--) if (closedTabs[i].state.entries.some(containsDomain, this)) closedTabs.splice(i, 1); for (let j = openTabs.length - 1; j >= 0; j--) { if (openTabs[j].entries.some(containsDomain, this)) { openTabs.splice(j, 1); if (this._closedWindows[ix].selected > j) this._closedWindows[ix].selected--; } } if (openTabs.length == 0) { this._closedWindows.splice(ix, 1); } else if (openTabs.length != openTabCount) { // Adjust the window's title if we removed an open tab let selectedTab = openTabs[this._closedWindows[ix].selected - 1]; // some duplication from restoreHistory - make sure we get the correct title let activeIndex = (selectedTab.index || selectedTab.entries.length) - 1; if (activeIndex >= selectedTab.entries.length) activeIndex = selectedTab.entries.length - 1; this._closedWindows[ix].title = selectedTab.entries[activeIndex].title; } } if (this._loadState == STATE_RUNNING) this.saveState(true); this._clearRestoringWindows(); }, /** * On preference change * @param aData * String preference changed */ onPrefChange: function(aData) { switch (aData) { // if the user decreases the max number of closed tabs they want // preserved update our internal states to match that max case "sessionstore.max_tabs_undo": this._max_tabs_undo = this._prefBranch.getIntPref("sessionstore.max_tabs_undo"); for (let ix in this._windows) { this._windows[ix]._closedTabs.splice(this._max_tabs_undo, this._windows[ix]._closedTabs.length); } break; case "sessionstore.max_windows_undo": this._max_windows_undo = this._prefBranch.getIntPref("sessionstore.max_windows_undo"); this._capClosedWindows(); break; case "sessionstore.interval": this._interval = this._prefBranch.getIntPref("sessionstore.interval"); // reset timer and save if (this._saveTimer) { this._saveTimer.cancel(); this._saveTimer = null; } this.saveStateDelayed(null, -1); break; case "sessionstore.resume_from_crash": this._resume_from_crash = this._prefBranch.getBoolPref("sessionstore.resume_from_crash"); // restore original resume_session_once preference if set in saveState if (this._resume_session_once_on_shutdown != null) { this._prefBranch.setBoolPref("sessionstore.resume_session_once", this._resume_session_once_on_shutdown); this._resume_session_once_on_shutdown = null; } // either create the file with crash recovery information or remove it // (when _loadState is not STATE_RUNNING, that file is used for session resuming instead) if (!this._resume_from_crash) _SessionFile.wipe(); this.saveState(true); break; } }, /** * On timer callback */ onTimerCallback: function() { this._saveTimer = null; this.saveState(); }, /** * set up listeners for a new tab * @param aWindow * Window reference * @param aTab * Tab reference * @param aNoNotification * bool Do not save state if we're updating an existing tab */ onTabAdd: function(aWindow, aTab, aNoNotification) { let browser = aTab.linkedBrowser; browser.addEventListener("load", this, true); let mm = browser.messageManager; MESSAGES.forEach(msg => mm.addMessageListener(msg, this)); if (!aNoNotification) { this.saveStateDelayed(aWindow); } }, /** * remove listeners for a tab * @param aWindow * Window reference * @param aTab * Tab reference * @param aNoNotification * bool Do not save state if we're updating an existing tab */ onTabRemove: function(aWindow, aTab, aNoNotification) { let browser = aTab.linkedBrowser; browser.removeEventListener("load", this, true); let mm = browser.messageManager; MESSAGES.forEach(msg => mm.removeMessageListener(msg, this)); delete browser.__SS_data; delete browser.__SS_tabStillLoading; delete browser.__SS_formDataSaved; delete browser.__SS_hostSchemeData; // If this tab was in the middle of restoring or still needs to be restored, // we need to reset that state. If the tab was restoring, we will attempt to // restore the next tab. let previousState = browser.__SS_restoreState; if (previousState) { this._resetTabRestoringState(aTab); if (previousState == TAB_STATE_RESTORING) this.restoreNextTab(); } if (!aNoNotification) { this.saveStateDelayed(aWindow); } }, /** * When a tab closes, collect its properties * @param aWindow * Window reference * @param aTab * Tab reference */ onTabClose: function(aWindow, aTab) { // notify the tabbrowser that the tab state will be retrieved for the last time // (so that extension authors can easily set data on soon-to-be-closed tabs) var event = aWindow.document.createEvent("Events"); event.initEvent("SSTabClosing", true, false); aTab.dispatchEvent(event); // don't update our internal state if we don't have to if (this._max_tabs_undo == 0) { return; } // make sure that the tab related data is up-to-date var tabState = this._collectTabData(aTab); this._updateTextAndScrollDataForTab(aWindow, aTab.linkedBrowser, tabState); // store closed-tab data for undo if (this._shouldSaveTabState(tabState)) { let tabTitle = aTab.label; let tabbrowser = aWindow.gBrowser; tabTitle = this._replaceLoadingTitle(tabTitle, tabbrowser, aTab); this._windows[aWindow.__SSi]._closedTabs.unshift({ state: tabState, title: tabTitle, image: tabbrowser.getIcon(aTab), pos: aTab._tPos }); var length = this._windows[aWindow.__SSi]._closedTabs.length; if (length > this._max_tabs_undo) this._windows[aWindow.__SSi]._closedTabs.splice(this._max_tabs_undo, length - this._max_tabs_undo); } }, /** * When a tab loads, save state. * @param aWindow * Window reference * @param aBrowser * Browser reference */ onTabLoad: function(aWindow, aBrowser) { // react on "load" and solitary "pageshow" events (the first "pageshow" // following "load" is too late for deleting the data caches) // It's possible to get a load event after calling stop on a browser (when // overwriting tabs). We want to return early if the tab hasn't been restored yet. if (aBrowser.__SS_restoreState && aBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) { return; } delete aBrowser.__SS_data; delete aBrowser.__SS_tabStillLoading; delete aBrowser.__SS_formDataSaved; this.saveStateDelayed(aWindow); }, /** * Called when a browser sends the "input" notification * @param aWindow * Window reference * @param aBrowser * Browser reference */ onTabInput: function(aWindow, aBrowser) { // deleting __SS_formDataSaved will cause us to recollect form data delete aBrowser.__SS_formDataSaved; this.saveStateDelayed(aWindow, 3000); }, /** * When a tab is selected, save session data * @param aWindow * Window reference */ onTabSelect: function(aWindow) { if (this._loadState == STATE_RUNNING) { this._windows[aWindow.__SSi].selected = aWindow.gBrowser.tabContainer.selectedIndex; let tab = aWindow.gBrowser.selectedTab; // If __SS_restoreState is still on the browser and it is // TAB_STATE_NEEDS_RESTORE, then then we haven't restored // this tab yet. Explicitly call restoreTab to kick off the restore. if (tab.linkedBrowser.__SS_restoreState && tab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) this.restoreTab(tab); } }, onTabShow: function(aWindow, aTab) { // If the tab hasn't been restored yet, move it into the right bucket if (aTab.linkedBrowser.__SS_restoreState && aTab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) { TabRestoreQueue.hiddenToVisible(aTab); // let's kick off tab restoration again to ensure this tab gets restored // with "restore_hidden_tabs" == false (now that it has become visible) this.restoreNextTab(); } // Default delay of 2 seconds gives enough time to catch multiple TabShow // events due to changing groups in Panorama. this.saveStateDelayed(aWindow); }, onTabHide: function(aWindow, aTab) { // If the tab hasn't been restored yet, move it into the right bucket if (aTab.linkedBrowser.__SS_restoreState && aTab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) { TabRestoreQueue.visibleToHidden(aTab); } // Default delay of 2 seconds gives enough time to catch multiple TabHide // events due to changing groups in Panorama. this.saveStateDelayed(aWindow); }, /* ........ nsISessionStore API .............. */ getBrowserState: function() { return this._toJSONString(this._getCurrentState()); }, setBrowserState: function(aState) { this._handleClosedWindows(); try { var state = JSON.parse(aState); } catch (ex) { /* invalid state object - don't restore anything */ } if (!state || !state.windows) throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); this._browserSetState = true; // Make sure the priority queue is emptied out this._resetRestoringState(); var window = this._getMostRecentBrowserWindow(); if (!window) { this._restoreCount = 1; this._openWindowWithState(state); return; } // close all other browser windows this._forEachBrowserWindow(function(aWindow) { if (aWindow != window) { aWindow.close(); this.onClose(aWindow); } }); // make sure closed window data isn't kept this._closedWindows = []; // determine how many windows are meant to be restored this._restoreCount = state.windows ? state.windows.length : 0; // restore to the given state this.restoreWindow(window, state, true); }, getWindowState: function(aWindow) { if ("__SSi" in aWindow) { return this._toJSONString(this._getWindowState(aWindow)); } if (DyingWindowCache.has(aWindow)) { let data = DyingWindowCache.get(aWindow); return this._toJSONString({ windows: [data] }); } throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); }, setWindowState: function(aWindow, aState, aOverwrite) { if (!aWindow.__SSi) throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); this.restoreWindow(aWindow, aState, aOverwrite); }, getTabState: function(aTab) { if (!aTab.ownerDocument || !aTab.ownerDocument.defaultView.__SSi) throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); var tabState = this._collectTabData(aTab); var window = aTab.ownerDocument.defaultView; this._updateTextAndScrollDataForTab(window, aTab.linkedBrowser, tabState); return this._toJSONString(tabState); }, setTabState: function(aTab, aState) { var tabState = JSON.parse(aState); if (!tabState.entries || !aTab.ownerDocument || !aTab.ownerDocument.defaultView.__SSi) throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); var window = aTab.ownerDocument.defaultView; this._setWindowStateBusy(window); this.restoreHistoryPrecursor(window, [aTab], [tabState], 0, 0, 0); }, duplicateTab: function(aWindow, aTab, aDelta) { if (!aTab.ownerDocument || !aTab.ownerDocument.defaultView.__SSi || !aWindow.getBrowser) throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); var tabState = this._collectTabData(aTab, true); var sourceWindow = aTab.ownerDocument.defaultView; this._updateTextAndScrollDataForTab(sourceWindow, aTab.linkedBrowser, tabState, true); tabState.index += aDelta; tabState.index = Math.max(1, Math.min(tabState.index, tabState.entries.length)); tabState.pinned = false; this._setWindowStateBusy(aWindow); let newTab = aTab == aWindow.gBrowser.selectedTab ? aWindow.gBrowser.addTab(null, {relatedToCurrent: true, ownerTab: aTab}) : aWindow.gBrowser.addTab(); this.restoreHistoryPrecursor(aWindow, [newTab], [tabState], 0, 0, 0, true /* Load this tab right away. */); return newTab; }, getClosedTabCount: function(aWindow) { if ("__SSi" in aWindow) { return this._windows[aWindow.__SSi]._closedTabs.length; } if (DyingWindowCache.has(aWindow)) { return DyingWindowCache.get(aWindow)._closedTabs.length; } throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); }, getClosedTabData: function(aWindow) { if ("__SSi" in aWindow) { return this._toJSONString(this._windows[aWindow.__SSi]._closedTabs); } if (DyingWindowCache.has(aWindow)) { let data = DyingWindowCache.get(aWindow); return this._toJSONString(data._closedTabs); } throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); }, undoCloseTab: function(aWindow, aIndex) { if (!aWindow.__SSi) throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); var closedTabs = this._windows[aWindow.__SSi]._closedTabs; // default to the most-recently closed tab aIndex = aIndex || 0; if (!(aIndex in closedTabs)) throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); // fetch the data of closed tab, while removing it from the array let closedTab = closedTabs.splice(aIndex, 1).shift(); let closedTabState = closedTab.state; this._setWindowStateBusy(aWindow); // create a new tab let tabbrowser = aWindow.gBrowser; let tab = tabbrowser.addTab(); // restore tab content this.restoreHistoryPrecursor(aWindow, [tab], [closedTabState], 1, 0, 0); // restore the tab's position tabbrowser.moveTabTo(tab, closedTab.pos); // focus the tab's content area (bug 342432) tab.linkedBrowser.focus(); return tab; }, forgetClosedTab: function(aWindow, aIndex) { if (!aWindow.__SSi) throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); var closedTabs = this._windows[aWindow.__SSi]._closedTabs; // default to the most-recently closed tab aIndex = aIndex || 0; if (!(aIndex in closedTabs)) throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); // remove closed tab from the array closedTabs.splice(aIndex, 1); }, getClosedWindowCount: function() { return this._closedWindows.length; }, getClosedWindowData: function() { return this._toJSONString(this._closedWindows); }, undoCloseWindow: function(aIndex) { if (!(aIndex in this._closedWindows)) throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); // reopen the window let state = { windows: this._closedWindows.splice(aIndex, 1) }; let window = this._openWindowWithState(state); this.windowToFocus = window; return window; }, forgetClosedWindow: function(aIndex) { // default to the most-recently closed window aIndex = aIndex || 0; if (!(aIndex in this._closedWindows)) throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); // remove closed window from the array this._closedWindows.splice(aIndex, 1); }, getWindowValue: function(aWindow, aKey) { if ("__SSi" in aWindow) { var data = this._windows[aWindow.__SSi].extData || {}; return data[aKey] || ""; } if (DyingWindowCache.has(aWindow)) { let data = DyingWindowCache.get(aWindow).extData || {}; return data[aKey] || ""; } throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); }, setWindowValue: function(aWindow, aKey, aStringValue) { if (aWindow.__SSi) { if (!this._windows[aWindow.__SSi].extData) { this._windows[aWindow.__SSi].extData = {}; } this._windows[aWindow.__SSi].extData[aKey] = aStringValue; this.saveStateDelayed(aWindow); } else { throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); } }, deleteWindowValue: function(aWindow, aKey) { if (aWindow.__SSi && this._windows[aWindow.__SSi].extData && this._windows[aWindow.__SSi].extData[aKey]) delete this._windows[aWindow.__SSi].extData[aKey]; }, getTabValue: function(aTab, aKey) { let data = {}; if (aTab.__SS_extdata) { data = aTab.__SS_extdata; } else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) { // If the tab hasn't been fully restored, get the data from the to-be-restored data data = aTab.linkedBrowser.__SS_data.extData; } return data[aKey] || ""; }, setTabValue: function(aTab, aKey, aStringValue) { // If the tab hasn't been restored, then set the data there, otherwise we // could lose newly added data. let saveTo; if (aTab.__SS_extdata) { saveTo = aTab.__SS_extdata; } else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) { saveTo = aTab.linkedBrowser.__SS_data.extData; } else { aTab.__SS_extdata = {}; saveTo = aTab.__SS_extdata; } saveTo[aKey] = aStringValue; this.saveStateDelayed(aTab.ownerDocument.defaultView); }, deleteTabValue: function(aTab, aKey) { // We want to make sure that if data is accessed early, we attempt to delete // that data from __SS_data as well. Otherwise we'll throw in cases where // data can be set or read. let deleteFrom; if (aTab.__SS_extdata) { deleteFrom = aTab.__SS_extdata; } else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) { deleteFrom = aTab.linkedBrowser.__SS_data.extData; } if (deleteFrom && deleteFrom[aKey]) delete deleteFrom[aKey]; }, persistTabAttribute: function(aName) { if (TabAttributes.persist(aName)) { this.saveStateDelayed(); } }, /** * Restores the session state stored in _lastSessionState. This will attempt * to merge data into the current session. If a window was opened at startup * with pinned tab(s), then the remaining data from the previous session for * that window will be opened into that winddow. Otherwise new windows will * be opened. */ restoreLastSession: function() { // Use the public getter since it also checks PB mode if (!this.canRestoreLastSession) throw (Components.returnCode = Cr.NS_ERROR_FAILURE); // First collect each window with its id... let windows = {}; this._forEachBrowserWindow(function(aWindow) { if (aWindow.__SS_lastSessionWindowID) windows[aWindow.__SS_lastSessionWindowID] = aWindow; }); let lastSessionState = this._lastSessionState; // This shouldn't ever be the case... if (!lastSessionState.windows.length) throw (Components.returnCode = Cr.NS_ERROR_UNEXPECTED); // We're technically doing a restore, so set things up so we send the // notification when we're done. We want to send "sessionstore-browser-state-restored". this._restoreCount = lastSessionState.windows.length; this._browserSetState = true; // We want to re-use the last opened window instead of opening a new one in // the case where it's "empty" and not associated with a window in the session. // We will do more processing via _prepWindowToRestoreInto if we need to use // the lastWindow. let lastWindow = this._getMostRecentBrowserWindow(); let canUseLastWindow = lastWindow && !lastWindow.__SS_lastSessionWindowID; // Restore into windows or open new ones as needed. for (let i = 0; i < lastSessionState.windows.length; i++) { let winState = lastSessionState.windows[i]; let lastSessionWindowID = winState.__lastSessionWindowID; // delete lastSessionWindowID so we don't add that to the window again delete winState.__lastSessionWindowID; // See if we can use an open window. First try one that is associated with // the state we're trying to restore and then fallback to the last selected // window. let windowToUse = windows[lastSessionWindowID]; if (!windowToUse && canUseLastWindow) { windowToUse = lastWindow; canUseLastWindow = false; } let [canUseWindow, canOverwriteTabs] = this._prepWindowToRestoreInto(windowToUse); // If there's a window already open that we can restore into, use that if (canUseWindow) { // Since we're not overwriting existing tabs, we want to merge _closedTabs, // putting existing ones first. Then make sure we're respecting the max pref. if (winState._closedTabs && winState._closedTabs.length) { let curWinState = this._windows[windowToUse.__SSi]; curWinState._closedTabs = curWinState._closedTabs.concat(winState._closedTabs); curWinState._closedTabs.splice(this._prefBranch.getIntPref("sessionstore.max_tabs_undo"), curWinState._closedTabs.length); } // Restore into that window - pretend it's a followup since we'll already // have a focused window. //XXXzpao This is going to merge extData together (taking what was in // winState over what is in the window already. The hack we have // in _preWindowToRestoreInto will prevent most (all?) Panorama // weirdness but we will still merge other extData. // Bug 588217 should make this go away by merging the group data. this.restoreWindow(windowToUse, { windows: [winState] }, canOverwriteTabs, true); } else { this._openWindowWithState({ windows: [winState] }); } } // Merge closed windows from this session with ones from last session if (lastSessionState._closedWindows) { this._closedWindows = this._closedWindows.concat(lastSessionState._closedWindows); this._capClosedWindows(); } #ifdef MOZ_DEVTOOLS // Scratchpad if (lastSessionState.scratchpads) { ScratchpadManager.restoreSession(lastSessionState.scratchpads); } // The Browser Console if (lastSessionState.browserConsole) { HUDService.restoreBrowserConsoleSession(); } #endif // Set data that persists between sessions this._recentCrashes = lastSessionState.session && lastSessionState.session.recentCrashes || 0; this._sessionStartTime = lastSessionState.session && lastSessionState.session.startTime || this._sessionStartTime; this._lastSessionState = null; }, /** * See if aWindow is usable for use when restoring a previous session via * restoreLastSession. If usable, prepare it for use. * * @param aWindow * the window to inspect & prepare * @returns [canUseWindow, canOverwriteTabs] * canUseWindow: can the window be used to restore into * canOverwriteTabs: all of the current tabs are home pages and we * can overwrite them */ _prepWindowToRestoreInto: function(aWindow) { if (!aWindow) return [false, false]; let event = aWindow.document.createEvent("Events"); event.initEvent("SSRestoreIntoWindow", true, true); // Check if we can use the window. if (!aWindow.dispatchEvent(event)) return [false, false]; // We might be able to overwrite the existing tabs instead of just adding // the previous session's tabs to the end. This will be set if possible. let canOverwriteTabs = false; // Look at the open tabs in comparison to home pages. If all the tabs are // home pages then we'll end up overwriting all of them. Otherwise we'll // just close the tabs that match home pages. Tabs with the about:blank // URI will always be overwritten. let homePages = ["about:blank"]; let removableTabs = []; let tabbrowser = aWindow.gBrowser; let normalTabsLen = tabbrowser.tabs.length - tabbrowser._numPinnedTabs; let startupPref = this._prefBranch.getIntPref("startup.page"); if (startupPref == 1) homePages = homePages.concat(aWindow.gHomeButton.getHomePage().split("|")); for (let i = tabbrowser._numPinnedTabs; i < tabbrowser.tabs.length; i++) { let tab = tabbrowser.tabs[i]; if (homePages.indexOf(tab.linkedBrowser.currentURI.spec) != -1) { removableTabs.push(tab); } } if (tabbrowser.tabs.length == removableTabs.length) { canOverwriteTabs = true; } else { // If we're not overwriting all of the tabs, then close the home tabs. for (let i = removableTabs.length - 1; i >= 0; i--) { tabbrowser.removeTab(removableTabs.pop(), { animate: false }); } } return [true, canOverwriteTabs]; }, /* ........ Saving Functionality .............. */ /** * Store all session data for a window * @param aWindow * Window reference */ _saveWindowHistory: function(aWindow) { var tabbrowser = aWindow.gBrowser; var tabs = tabbrowser.tabs; var tabsData = this._windows[aWindow.__SSi].tabs = []; for (var i = 0; i < tabs.length; i++) tabsData.push(this._collectTabData(tabs[i])); this._windows[aWindow.__SSi].selected = tabbrowser.mTabBox.selectedIndex + 1; }, /** * Collect data related to a single tab * @param aTab * tabbrowser tab * @param aFullData * always return privacy sensitive data (use with care) * @returns object */ _collectTabData: function(aTab, aFullData) { var tabData = { entries: [], lastAccessed: aTab.lastAccessed }; var browser = aTab.linkedBrowser; if (!browser || !browser.currentURI) // can happen when calling this function right after .addTab() return tabData; else if (browser.__SS_data && browser.__SS_tabStillLoading) { // use the data to be restored when the tab hasn't been completely loaded tabData = browser.__SS_data; if (aTab.pinned) tabData.pinned = true; else delete tabData.pinned; tabData.hidden = aTab.hidden; // If __SS_extdata is set then we'll use that since it might be newer. if (aTab.__SS_extdata) tabData.extData = aTab.__SS_extdata; // If it exists but is empty then a key was likely deleted. In that case just // delete extData. if (tabData.extData && !Object.keys(tabData.extData).length) delete tabData.extData; return tabData; } var history = null; try { history = browser.sessionHistory; } catch (ex) { } // this could happen if we catch a tab during (de)initialization // Limit number of back/forward button history entries to save let oldest, newest; let maxSerializeBack = this._prefBranch.getIntPref("sessionstore.max_serialize_back"); if (maxSerializeBack >= 0) { oldest = Math.max(0, history.index - maxSerializeBack); } else { // History.getEntryAtIndex(0, ...) is the oldest. oldest = 0; } let maxSerializeFwd = this._prefBranch.getIntPref("sessionstore.max_serialize_forward"); if (maxSerializeFwd >= 0) { newest = Math.min(history.count - 1, history.index + maxSerializeFwd); } else { // History.getEntryAtIndex(history.count - 1, ...) is the newest. newest = history.count - 1; } // XXXzeniko anchor navigation doesn't reset __SS_data, so we could reuse // data even when we shouldn't (e.g. Back, different anchor) // Warning: this is required to save form data and scrolling position! if (history && browser.__SS_data && browser.__SS_data.entries[history.index] && browser.__SS_data.entries[history.index].url == browser.currentURI.spec && history.index < this._sessionhistory_max_entries - 1 && !aFullData) { try { tabData.entries = browser.__SS_data.entries.slice(oldest, newest + 1); } catch (ex) { // No errors are expected above, but we use try-catch to keep sessionstore.js safe NS_ASSERT(false, "SessionStore failed to slice history from browser.__SS_data"); } // Set the one-based index of the currently active tab, ensuring it isn't out of bounds tabData.index = Math.min(history.index - oldest + 1, tabData.entries.length); } else if (history && history.count > 0) { browser.__SS_hostSchemeData = []; try { for (var j = oldest; j <= newest; j++) { let entry = this._serializeHistoryEntry(history.getEntryAtIndex(j, false), aFullData, aTab.pinned, browser.__SS_hostSchemeData); tabData.entries.push(entry); } } catch (ex) { // In some cases, getEntryAtIndex will throw. This seems to be due to // history.count being higher than it should be. By doing this in a // try-catch, we'll update history to where it breaks, assert for // non-release builds, and still save sessionstore.js. NS_ASSERT(false, "SessionStore failed gathering complete history " + "for the focused window/tab. See bug 669196."); } // Set the one-based index of the currently active tab, ensuring it isn't out of bounds tabData.index = Math.min(history.index - oldest + 1, tabData.entries.length); // make sure not to cache privacy sensitive data which shouldn't get out if (!aFullData) browser.__SS_data = tabData; } else if (browser.currentURI.spec != "about:blank" || browser.contentDocument.body.hasChildNodes()) { tabData.entries[0] = { url: browser.currentURI.spec }; tabData.index = 1; } // If there is a userTypedValue set, then either the user has typed something // in the URL bar, or a new tab was opened with a URI to load. userTypedClear // is used to indicate whether the tab was in some sort of loading state with // userTypedValue. if (browser.userTypedValue) { tabData.userTypedValue = browser.userTypedValue; // We always used to keep track of the loading state as an integer, where // '0' indicated the user had typed since the last load (or no load was // ongoing), and any positive value indicated we had started a load since // the last time the user typed in the URL bar. Mimic this to keep the // session store representation in sync, even though we now represent this // more explicitly: tabData.userTypedClear = browser.didStartLoadSinceLastUserTyping() ? 1 : 0; } else { delete tabData.userTypedValue; delete tabData.userTypedClear; } if (aTab.pinned) tabData.pinned = true; else delete tabData.pinned; tabData.hidden = aTab.hidden; var disallow = []; for (let cap of gDocShellCapabilities(browser.docShell)) if (!browser.docShell["allow" + cap]) disallow.push(cap); if (disallow.length > 0) tabData.disallow = disallow.join(","); else if (tabData.disallow) delete tabData.disallow; // Save tab attributes. tabData.attributes = TabAttributes.get(aTab); // Store the tab icon. let tabbrowser = aTab.ownerDocument.defaultView.gBrowser; tabData.image = tabbrowser.getIcon(aTab); if (aTab.__SS_extdata) tabData.extData = aTab.__SS_extdata; else if (tabData.extData) delete tabData.extData; if (history && browser.docShell instanceof Ci.nsIDocShell) { let storageData = SessionStorage.serialize(browser.docShell, aFullData) if (Object.keys(storageData).length) tabData.storage = storageData; } return tabData; }, /** * Get an object that is a serialized representation of a History entry * Used for data storage * @param aEntry * nsISHEntry instance * @param aFullData * always return privacy sensitive data (use with care) * @param aIsPinned * the tab is pinned and should be treated differently for privacy * @param aHostSchemeData * an array of objects with host & scheme keys * @returns object */ _serializeHistoryEntry: function(aEntry, aFullData, aIsPinned, aHostSchemeData) { var entry = { url: aEntry.URI.spec }; try { // throwing is expensive, we know that about: pages will throw if (entry.url.indexOf("about:") != 0) aHostSchemeData.push({ host: aEntry.URI.host, scheme: aEntry.URI.scheme }); } catch (ex) { // We just won't attempt to get cookies for this entry. } if (aEntry.title && aEntry.title != entry.url) { entry.title = aEntry.title; } if (aEntry.isSubFrame) { entry.subframe = true; } if (!(aEntry instanceof Ci.nsISHEntry)) { return entry; } var cacheKey = aEntry.cacheKey; if (cacheKey && cacheKey instanceof Ci.nsISupportsPRUint32 && cacheKey.data != 0) { // XXXbz would be better to have cache keys implement // nsISerializable or something. entry.cacheKey = cacheKey.data; } entry.ID = aEntry.ID; entry.docshellID = aEntry.docshellID; if (aEntry.referrerURI) entry.referrer = aEntry.referrerURI.spec; if (aEntry.srcdocData) entry.srcdocData = aEntry.srcdocData; if (aEntry.isSrcdocEntry) entry.isSrcdocEntry = aEntry.isSrcdocEntry; if (aEntry.contentType) entry.contentType = aEntry.contentType; var x = {}, y = {}; aEntry.getScrollPosition(x, y); if (x.value != 0 || y.value != 0) entry.scroll = x.value + "," + y.value; try { var prefPostdata = this._prefBranch.getIntPref("sessionstore.postdata"); if (aEntry.postData && (aFullData || prefPostdata && this.checkPrivacyLevel(aEntry.URI.schemeIs("https"), aIsPinned))) { aEntry.postData.QueryInterface(Ci.nsISeekableStream). seek(Ci.nsISeekableStream.NS_SEEK_SET, 0); var stream = Cc["@mozilla.org/binaryinputstream;1"]. createInstance(Ci.nsIBinaryInputStream); stream.setInputStream(aEntry.postData); var postBytes = stream.readByteArray(stream.available()); var postdata = String.fromCharCode.apply(null, postBytes); if (aFullData || prefPostdata == -1 || postdata.replace(/^(Content-.*\r\n)+(\r\n)*/, "").length <= prefPostdata) { // We can stop doing base64 encoding once our serialization into JSON // is guaranteed to handle all chars in strings, including embedded // nulls. entry.postdata_b64 = btoa(postdata); } } } catch (ex) { debug(ex); } // POSTDATA is tricky - especially since some extensions don't get it right if (aEntry.triggeringPrincipal) { // Not catching anything specific here, just possible errors // from writeCompoundObject and the like. try { var binaryStream = Cc["@mozilla.org/binaryoutputstream;1"]. createInstance(Ci.nsIObjectOutputStream); var pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe); pipe.init(false, false, 0, 0xffffffff, null); binaryStream.setOutputStream(pipe.outputStream); binaryStream.writeCompoundObject(aEntry.triggeringPrincipal, Ci.nsIPrincipal, true); binaryStream.close(); // Now we want to read the data from the pipe's input end and encode it. var scriptableStream = Cc["@mozilla.org/binaryinputstream;1"]. createInstance(Ci.nsIBinaryInputStream); scriptableStream.setInputStream(pipe.inputStream); var triggeringPrincipalBytes = scriptableStream.readByteArray(scriptableStream.available()); // We can stop doing base64 encoding once our serialization into JSON // is guaranteed to handle all chars in strings, including embedded // nulls. entry.triggeringPrincipal_b64 = btoa(String.fromCharCode.apply(null, triggeringPrincipalBytes)); } catch (ex) { debug(ex); } } entry.docIdentifier = aEntry.BFCacheEntry.ID; if (aEntry.stateData != null) { entry.structuredCloneState = aEntry.stateData.getDataAsBase64(); entry.structuredCloneVersion = aEntry.stateData.formatVersion; } if (!(aEntry instanceof Ci.nsISHContainer)) { return entry; } if (aEntry.childCount > 0) { let children = []; for (var i = 0; i < aEntry.childCount; i++) { var child = aEntry.GetChildAt(i); if (child) { // don't try to restore framesets containing wyciwyg URLs (cf. bug 424689 and bug 450595) if (child.URI.schemeIs("wyciwyg")) { children = []; break; } children.push(this._serializeHistoryEntry(child, aFullData, aIsPinned, aHostSchemeData)); } } if (children.length) entry.children = children; } return entry; }, /** * go through all tabs and store the current scroll positions * and innerHTML content of WYSIWYG editors * @param aWindow * Window reference */ _updateTextAndScrollData: function(aWindow) { var browsers = aWindow.gBrowser.browsers; this._windows[aWindow.__SSi].tabs.forEach(function(tabData, i) { try { this._updateTextAndScrollDataForTab(aWindow, browsers[i], tabData); } catch (ex) { debug(ex); } // get as much data as possible, ignore failures (might succeed the next time) }, this); }, /** * go through all frames and store the current scroll positions * and innerHTML content of WYSIWYG editors * @param aWindow * Window reference * @param aBrowser * single browser reference * @param aTabData * tabData object to add the information to * @param aFullData * always return privacy sensitive data (use with care) */ _updateTextAndScrollDataForTab: function(aWindow, aBrowser, aTabData, aFullData) { // we shouldn't update data for incompletely initialized tabs if (aBrowser.__SS_data && aBrowser.__SS_tabStillLoading) return; var tabIndex = (aTabData.index || aTabData.entries.length) - 1; // entry data needn't exist for tabs just initialized with an incomplete session state if (!aTabData.entries[tabIndex]) return; let selectedPageStyle = aBrowser.markupDocumentViewer.authorStyleDisabled ? "_nostyle" : this._getSelectedPageStyle(aBrowser.contentWindow); if (selectedPageStyle) aTabData.pageStyle = selectedPageStyle; else if (aTabData.pageStyle) delete aTabData.pageStyle; this._updateTextAndScrollDataForFrame(aWindow, aBrowser.contentWindow, aTabData.entries[tabIndex], !aBrowser.__SS_formDataSaved, aFullData, !!aTabData.pinned); aBrowser.__SS_formDataSaved = true; if (aBrowser.currentURI.spec == "about:config") aTabData.entries[tabIndex].formdata = { id: { "textbox": aBrowser.contentDocument.getElementById("textbox").value }, xpath: {} }; }, /** * go through all subframes and store all form data, the current * scroll positions and innerHTML content of WYSIWYG editors * @param aWindow * Window reference * @param aContent * frame reference * @param aData * part of a tabData object to add the information to * @param aUpdateFormData * update all form data for this tab * @param aFullData * always return privacy sensitive data (use with care) * @param aIsPinned * the tab is pinned and should be treated differently for privacy */ _updateTextAndScrollDataForFrame: function(aWindow, aContent, aData, aUpdateFormData, aFullData, aIsPinned) { for (var i = 0; i < aContent.frames.length; i++) { if (aData.children && aData.children[i]) this._updateTextAndScrollDataForFrame(aWindow, aContent.frames[i], aData.children[i], aUpdateFormData, aFullData, aIsPinned); } var isHTTPS = this._getURIFromString((aContent.parent || aContent). document.location.href).schemeIs("https"); let isAboutSR = aContent.top.document.location.href == "about:sessionrestore"; if (aFullData || this.checkPrivacyLevel(isHTTPS, aIsPinned) || isAboutSR) { if (aFullData || aUpdateFormData) { let formData = DocumentUtils.getFormData(aContent.document); // We want to avoid saving data for about:sessionrestore as a string. // Since it's stored in the form as stringified JSON, stringifying further // causes an explosion of escape characters. cf. bug 467409 if (formData && isAboutSR) { formData.id["sessionData"] = JSON.parse(formData.id["sessionData"]); } if (Object.keys(formData.id).length || Object.keys(formData.xpath).length) { aData.formdata = formData; } else if (aData.formdata) { delete aData.formdata; } } // designMode is undefined e.g. for XUL documents (as about:config) if ((aContent.document.designMode || "") == "on" && aContent.document.body) aData.innerHTML = aContent.document.body.innerHTML; } // get scroll position from nsIDOMWindowUtils, since it allows avoiding a // flush of layout let domWindowUtils = aContent.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils); let scrollX = {}, scrollY = {}; domWindowUtils.getScrollXY(false, scrollX, scrollY); aData.scroll = scrollX.value + "," + scrollY.value; }, /** * determine the title of the currently enabled style sheet (if any) * and recurse through the frameset if necessary * @param aContent is a frame reference * @returns the title style sheet determined to be enabled (empty string if none) */ _getSelectedPageStyle: function(aContent) { const forScreen = /(?:^|,)\s*(?:all|screen)\s*(?:,|$)/i; for (let i = 0; i < aContent.document.styleSheets.length; i++) { let ss = aContent.document.styleSheets[i]; let media = ss.media.mediaText; if (!ss.disabled && ss.title && (!media || forScreen.test(media))) return ss.title } for (let i = 0; i < aContent.frames.length; i++) { let selectedPageStyle = this._getSelectedPageStyle(aContent.frames[i]); if (selectedPageStyle) return selectedPageStyle; } return ""; }, /** * extract the base domain from a history entry and its children * @param aEntry * the history entry, serialized * @param aHosts * the hash that will be used to store hosts eg, { hostname: true } * @param aCheckPrivacy * should we check the privacy level for https * @param aIsPinned * is the entry we're evaluating for a pinned tab; used only if * aCheckPrivacy */ _extractHostsForCookiesFromEntry: function(aEntry, aHosts, aCheckPrivacy, aIsPinned) { let host = aEntry._host, scheme = aEntry._scheme; // If host & scheme aren't defined, then we are likely here in the startup // process via _splitCookiesFromWindow. In that case, we'll turn aEntry.url // into an nsIURI and get host/scheme from that. This will throw for about: // urls in which case we don't need to do anything. if (!host && !scheme) { try { let uri = this._getURIFromString(aEntry.url); host = uri.host; scheme = uri.scheme; this._extractHostsForCookiesFromHostScheme(host, scheme, aHosts, aCheckPrivacy, aIsPinned); } catch(ex) { } } if (aEntry.children) { aEntry.children.forEach(function(entry) { this._extractHostsForCookiesFromEntry(entry, aHosts, aCheckPrivacy, aIsPinned); }, this); } }, /** * extract the base domain from a host & scheme * @param aHost * the host of a uri (usually via nsIURI.host) * @param aScheme * the scheme of a uri (usually via nsIURI.scheme) * @param aHosts * the hash that will be used to store hosts eg, { hostname: true } * @param aCheckPrivacy * should we check the privacy level for https * @param aIsPinned * is the entry we're evaluating for a pinned tab; used only if * aCheckPrivacy */ _extractHostsForCookiesFromHostScheme: function(aHost, aScheme, aHosts, aCheckPrivacy, aIsPinned) { // host and scheme may not be set (for about: urls for example), in which // case testing scheme will be sufficient. if (/https?/.test(aScheme) && !aHosts[aHost] && (!aCheckPrivacy || this.checkPrivacyLevel(aScheme == "https", aIsPinned))) { // By setting this to true or false, we can determine when looking at // the host in _updateCookies if we should check for privacy. aHosts[aHost] = aIsPinned; } else if (aScheme == "file") { aHosts[aHost] = true; } }, /** * store all hosts for a URL * @param aWindow * Window reference */ _updateCookieHosts: function(aWindow) { var hosts = this._internalWindows[aWindow.__SSi].hosts = {}; // Since _updateCookiesHosts is only ever called for open windows during a // session, we can call into _extractHostsForCookiesFromHostScheme directly // using data that is attached to each browser. for (let i = 0; i < aWindow.gBrowser.tabs.length; i++) { let tab = aWindow.gBrowser.tabs[i]; let hostSchemeData = tab.linkedBrowser.__SS_hostSchemeData || []; for (let j = 0; j < hostSchemeData.length; j++) { this._extractHostsForCookiesFromHostScheme(hostSchemeData[j].host, hostSchemeData[j].scheme, hosts, true, tab.pinned); } } }, /** * Serialize cookie data * @param aWindows * JS object containing window data references * { id: winData, etc. } */ _updateCookies: function(aWindows) { function addCookieToHash(aHash, aHost, aPath, aName, aCookie) { // lazily build up a 3-dimensional hash, with // aHost, aPath, and aName as keys if (!aHash[aHost]) aHash[aHost] = {}; if (!aHash[aHost][aPath]) aHash[aHost][aPath] = {}; aHash[aHost][aPath][aName] = aCookie; } var jscookies = {}; var _this = this; // MAX_EXPIRY should be 2^63-1, but JavaScript can't handle that precision var MAX_EXPIRY = Math.pow(2, 62); for (let [id, window] in Iterator(aWindows)) { window.cookies = []; let internalWindow = this._internalWindows[id]; if (!internalWindow.hosts) return; for (var [host, isPinned] in Iterator(internalWindow.hosts)) { let list; try { list = Services.cookies.getCookiesFromHost(host, {}); } catch (ex) { debug("getCookiesFromHost failed. Host: " + host); } while (list && list.hasMoreElements()) { var cookie = list.getNext().QueryInterface(Ci.nsICookie2); // window._hosts will only have hosts with the right privacy rules, // so there is no need to do anything special with this call to // checkPrivacyLevel. if (cookie.isSession && _this.checkPrivacyLevel(cookie.isSecure, isPinned)) { // use the cookie's host, path, and name as keys into a hash, // to make sure we serialize each cookie only once if (!(cookie.host in jscookies && cookie.path in jscookies[cookie.host] && cookie.name in jscookies[cookie.host][cookie.path])) { var jscookie = { "host": cookie.host, "value": cookie.value }; // only add attributes with non-default values (saving a few bits) if (cookie.path) jscookie.path = cookie.path; if (cookie.name) jscookie.name = cookie.name; if (cookie.isSecure) jscookie.secure = true; if (cookie.isHttpOnly) jscookie.httponly = true; if (cookie.expiry < MAX_EXPIRY) jscookie.expiry = cookie.expiry; addCookieToHash(jscookies, cookie.host, cookie.path, cookie.name, jscookie); } window.cookies.push(jscookies[cookie.host][cookie.path][cookie.name]); } } } // don't include empty cookie sections if (!window.cookies.length) delete window.cookies; } }, /** * Store window dimensions, visibility, sidebar * @param aWindow * Window reference */ _updateWindowFeatures: function(aWindow) { var winData = this._windows[aWindow.__SSi]; WINDOW_ATTRIBUTES.forEach(function(aAttr) { winData[aAttr] = this._getWindowDimension(aWindow, aAttr); }, this); var hidden = WINDOW_HIDEABLE_FEATURES.filter(function(aItem) { return aWindow[aItem] && !aWindow[aItem].visible; }); if (hidden.length != 0) winData.hidden = hidden.join(","); else if (winData.hidden) delete winData.hidden; var sidebar = aWindow.document.getElementById("sidebar-box").getAttribute("sidebarcommand"); if (sidebar) winData.sidebar = sidebar; else if (winData.sidebar) delete winData.sidebar; }, /** * gather session data as object * @param aUpdateAll * Bool update all windows * @param aPinnedOnly * Bool collect pinned tabs only * @returns object */ _getCurrentState: function(aUpdateAll, aPinnedOnly) { this._handleClosedWindows(); var activeWindow = this._getMostRecentBrowserWindow(); if (this._loadState == STATE_RUNNING) { // update the data for all windows with activities since the last save operation this._forEachBrowserWindow(function(aWindow) { if (!this._isWindowLoaded(aWindow)) // window data is still in _statesToRestore return; if (aUpdateAll || this._dirtyWindows[aWindow.__SSi] || aWindow == activeWindow) { this._collectWindowData(aWindow); } else { // always update the window features (whose change alone never triggers a save operation) this._updateWindowFeatures(aWindow); } }); this._dirtyWindows = []; } // collect the data for all windows var total = [], windows = {}, ids = []; var nonPopupCount = 0; var ix; for (ix in this._windows) { if (this._windows[ix]._restoring) // window data is still in _statesToRestore continue; total.push(this._windows[ix]); ids.push(ix); windows[ix] = this._windows[ix]; if (!this._windows[ix].isPopup) nonPopupCount++; } this._updateCookies(windows); // collect the data for all windows yet to be restored for (ix in this._statesToRestore) { for each (let winData in this._statesToRestore[ix].windows) { total.push(winData); if (!winData.isPopup) nonPopupCount++; } } // shallow copy this._closedWindows to preserve current state let lastClosedWindowsCopy = this._closedWindows.slice(); #ifndef XP_MACOSX // If no non-popup browser window remains open, return the state of the last // closed window(s). We only want to do this when we're actually "ending" // the session. //XXXzpao We should do this for _restoreLastWindow == true, but that has // its own check for popups. c.f. bug 597619 if (nonPopupCount == 0 && lastClosedWindowsCopy.length > 0 && this._loadState == STATE_QUITTING) { // prepend the last non-popup browser window, so that if the user loads more tabs // at startup we don't accidentally add them to a popup window do { total.unshift(lastClosedWindowsCopy.shift()) } while (total[0].isPopup && lastClosedWindowsCopy.length > 0) } #endif if (aPinnedOnly) { // perform a deep copy so that existing session variables are not changed. total = JSON.parse(this._toJSONString(total)); total = total.filter(function(win) { win.tabs = win.tabs.filter(function(tab) tab.pinned); // remove closed tabs win._closedTabs = []; // correct selected tab index if it was stripped out if (win.selected > win.tabs.length) win.selected = 1; return win.tabs.length > 0; }); if (total.length == 0) return null; lastClosedWindowsCopy = []; } if (activeWindow) { this.activeWindowSSiCache = activeWindow.__SSi || ""; } ix = ids.indexOf(this.activeWindowSSiCache); // We don't want to restore focus to a minimized window or a window which had all its // tabs stripped out (doesn't exist). if (ix != -1 && total[ix] && total[ix].sizemode == "minimized") ix = -1; let session = { state: this._loadState == STATE_RUNNING ? STATE_RUNNING_STR : STATE_STOPPED_STR, lastUpdate: Date.now(), startTime: this._sessionStartTime, recentCrashes: this._recentCrashes }; var scratchpads = null; var browserConsole = null; #ifdef MOZ_DEVTOOLS // Scratchpad // get open Scratchpad window states too scratchpads = ScratchpadManager.getSessionState(); // The Browser Console browserConsole = HUDService.getBrowserConsoleSessionState(); #endif return { windows: total, selectedWindow: ix + 1, _closedWindows: lastClosedWindowsCopy, #ifdef MOZ_DEVTOOLS session: session, scratchpads: scratchpads, browserConsole: browserConsole #else session: session #endif }; }, /** * serialize session data for a window * @param aWindow * Window reference * @returns string */ _getWindowState: function(aWindow) { if (!this._isWindowLoaded(aWindow)) return this._statesToRestore[aWindow.__SS_restoreID]; if (this._loadState == STATE_RUNNING) { this._collectWindowData(aWindow); } var winData = this._windows[aWindow.__SSi]; let windows = {}; windows[aWindow.__SSi] = winData; this._updateCookies(windows); return { windows: [winData] }; }, _collectWindowData: function(aWindow) { if (!this._isWindowLoaded(aWindow)) return; // update the internal state data for this window this._saveWindowHistory(aWindow); this._updateTextAndScrollData(aWindow); this._updateCookieHosts(aWindow); this._updateWindowFeatures(aWindow); // Make sure we keep __SS_lastSessionWindowID around for cases like entering // or leaving PB mode. if (aWindow.__SS_lastSessionWindowID) this._windows[aWindow.__SSi].__lastSessionWindowID = aWindow.__SS_lastSessionWindowID; this._dirtyWindows[aWindow.__SSi] = false; }, /* ........ Restoring Functionality .............. */ /** * restore features to a single window * @param aWindow * Window reference * @param aState * JS object or its eval'able source * @param aOverwriteTabs * bool overwrite existing tabs w/ new ones * @param aFollowUp * bool this isn't the restoration of the first window */ restoreWindow: function(aWindow, aState, aOverwriteTabs, aFollowUp) { if (!aFollowUp) { this.windowToFocus = aWindow; } // initialize window if necessary if (aWindow && (!aWindow.__SSi || !this._windows[aWindow.__SSi])) this.onLoad(aWindow); try { var root = typeof aState == "string" ? JSON.parse(aState) : aState; if (!root.windows[0]) { this._sendRestoreCompletedNotifications(); return; // nothing to restore } } catch (ex) { // invalid state object - don't restore anything debug(ex); this._sendRestoreCompletedNotifications(); return; } // We're not returning from this before we end up calling restoreHistoryPrecursor // for this window, so make sure we send the SSWindowStateBusy event. this._setWindowStateBusy(aWindow); if (root._closedWindows) this._closedWindows = root._closedWindows; var winData; if (!root.selectedWindow || root.selectedWindow > root.windows.length) { root.selectedWindow = 0; } // open new windows for all further window entries of a multi-window session // (unless they don't contain any tab data) for (var w = 1; w < root.windows.length; w++) { winData = root.windows[w]; if (winData && winData.tabs && winData.tabs[0]) { var window = this._openWindowWithState({ windows: [winData] }); if (w == root.selectedWindow - 1) { this.windowToFocus = window; } } } winData = root.windows[0]; if (!winData.tabs) { winData.tabs = []; } // don't restore a single blank tab when we've had an external // URL passed in for loading at startup (cf. bug 357419) else if (root._firstTabs && !aOverwriteTabs && winData.tabs.length == 1 && (!winData.tabs[0].entries || winData.tabs[0].entries.length == 0)) { winData.tabs = []; } var tabbrowser = aWindow.gBrowser; var openTabCount = aOverwriteTabs ? tabbrowser.browsers.length : -1; var newTabCount = winData.tabs.length; var tabs = []; // disable smooth scrolling while adding, moving, removing and selecting tabs var tabstrip = tabbrowser.tabContainer.mTabstrip; var smoothScroll = tabstrip.smoothScroll; tabstrip.smoothScroll = false; // unpin all tabs to ensure they are not reordered in the next loop if (aOverwriteTabs) { for (let t = tabbrowser._numPinnedTabs - 1; t > -1; t--) tabbrowser.unpinTab(tabbrowser.tabs[t]); } // make sure that the selected tab won't be closed in order to // prevent unnecessary flickering if (aOverwriteTabs && tabbrowser.selectedTab._tPos >= newTabCount) tabbrowser.moveTabTo(tabbrowser.selectedTab, newTabCount - 1); let numVisibleTabs = 0; for (var t = 0; t < newTabCount; t++) { tabs.push(t < openTabCount ? tabbrowser.tabs[t] : tabbrowser.addTab("about:blank", {skipAnimation: true, skipBackgroundNotify: true})); // when resuming at startup: add additionally requested pages to the end if (!aOverwriteTabs && root._firstTabs) { tabbrowser.moveTabTo(tabs[t], t); } if (winData.tabs[t].pinned) tabbrowser.pinTab(tabs[t]); if (winData.tabs[t].hidden) { tabbrowser.hideTab(tabs[t]); } else { tabbrowser.showTab(tabs[t]); numVisibleTabs++; } } // if all tabs to be restored are hidden, make the first one visible if (!numVisibleTabs && winData.tabs.length) { winData.tabs[0].hidden = false; tabbrowser.showTab(tabs[0]); } // If overwriting tabs, we want to reset each tab's "restoring" state. Since // we're overwriting those tabs, they should no longer be restoring. The // tabs will be rebuilt and marked if they need to be restored after loading // state (in restoreHistoryPrecursor). if (aOverwriteTabs) { for (let i = 0; i < tabbrowser.tabs.length; i++) { if (tabbrowser.browsers[i].__SS_restoreState) this._resetTabRestoringState(tabbrowser.tabs[i]); } } // We want to set up a counter on the window that indicates how many tabs // in this window are unrestored. This will be used in restoreNextTab to // determine if gRestoreTabsProgressListener should be removed from the window. // If we aren't overwriting existing tabs, then we want to add to the existing // count in case there are still tabs restoring. if (!aWindow.__SS_tabsToRestore) aWindow.__SS_tabsToRestore = 0; if (aOverwriteTabs) aWindow.__SS_tabsToRestore = newTabCount; else aWindow.__SS_tabsToRestore += newTabCount; // We want to correlate the window with data from the last session, so // assign another id if we have one. Otherwise clear so we don't do // anything with it. delete aWindow.__SS_lastSessionWindowID; if (winData.__lastSessionWindowID) aWindow.__SS_lastSessionWindowID = winData.__lastSessionWindowID; // when overwriting tabs, remove all superflous ones if (aOverwriteTabs && newTabCount < openTabCount) { Array.slice(tabbrowser.tabs, newTabCount, openTabCount) .forEach(tabbrowser.removeTab, tabbrowser); } if (aOverwriteTabs) { this.restoreWindowFeatures(aWindow, winData); delete this._windows[aWindow.__SSi].extData; } if (winData.cookies) { this.restoreCookies(winData.cookies); } if (winData.extData) { if (!this._windows[aWindow.__SSi].extData) { this._windows[aWindow.__SSi].extData = {}; } for (var key in winData.extData) { this._windows[aWindow.__SSi].extData[key] = winData.extData[key]; } } if (aOverwriteTabs || root._firstTabs) { this._windows[aWindow.__SSi]._closedTabs = winData._closedTabs || []; } this.restoreHistoryPrecursor(aWindow, tabs, winData.tabs, (aOverwriteTabs ? (parseInt(winData.selected) || 1) : 0), 0, 0); #ifdef MOZ_DEVTOOLS if (aState.scratchpads) { ScratchpadManager.restoreSession(aState.scratchpads); } // The Browser Console if (aState.browserConsole) { HUDService.restoreBrowserConsoleSession(); } #endif // set smoothScroll back to the original value tabstrip.smoothScroll = smoothScroll; this._sendRestoreCompletedNotifications(); }, /** * Sets the tabs restoring order with the following priority: * Selected tab, pinned tabs, optimized visible tabs, other visible tabs and * hidden tabs. * @param aTabBrowser * Tab browser object * @param aTabs * Array of tab references * @param aTabData * Array of tab data * @param aSelectedTab * Index of selected tab (1 is first tab, 0 no selected tab) */ _setTabsRestoringOrder : function( aTabBrowser, aTabs, aTabData, aSelectedTab) { // Store the selected tab. Need to substract one to get the index in aTabs. let selectedTab; if (aSelectedTab > 0 && aTabs[aSelectedTab - 1]) { selectedTab = aTabs[aSelectedTab - 1]; } // Store the pinned tabs and hidden tabs. let pinnedTabs = []; let pinnedTabsData = []; let hiddenTabs = []; let hiddenTabsData = []; if (aTabs.length > 1) { for (let t = aTabs.length - 1; t >= 0; t--) { if (aTabData[t].pinned) { pinnedTabs.unshift(aTabs.splice(t, 1)[0]); pinnedTabsData.unshift(aTabData.splice(t, 1)[0]); } else if (aTabData[t].hidden) { hiddenTabs.unshift(aTabs.splice(t, 1)[0]); hiddenTabsData.unshift(aTabData.splice(t, 1)[0]); } } } // Optimize the visible tabs only if there is a selected tab. if (selectedTab) { let selectedTabIndex = aTabs.indexOf(selectedTab); if (selectedTabIndex > 0) { let scrollSize = aTabBrowser.tabContainer.mTabstrip.scrollClientSize; let tabWidth = aTabs[0].getBoundingClientRect().width; let maxVisibleTabs = Math.ceil(scrollSize / tabWidth); if (maxVisibleTabs < aTabs.length) { let firstVisibleTab = 0; let nonVisibleTabsCount = aTabs.length - maxVisibleTabs; if (nonVisibleTabsCount >= selectedTabIndex) { // Selected tab is leftmost since we scroll to it when possible. firstVisibleTab = selectedTabIndex; } else { // Selected tab is rightmost or no more room to scroll right. firstVisibleTab = nonVisibleTabsCount; } aTabs = aTabs.splice(firstVisibleTab, maxVisibleTabs).concat(aTabs); aTabData = aTabData.splice(firstVisibleTab, maxVisibleTabs).concat(aTabData); } } } // Merge the stored tabs in order. aTabs = pinnedTabs.concat(aTabs, hiddenTabs); aTabData = pinnedTabsData.concat(aTabData, hiddenTabsData); // Load the selected tab to the first position and select it. if (selectedTab) { let selectedTabIndex = aTabs.indexOf(selectedTab); if (selectedTabIndex > 0) { aTabs = aTabs.splice(selectedTabIndex, 1).concat(aTabs); aTabData = aTabData.splice(selectedTabIndex, 1).concat(aTabData); } aTabBrowser.selectedTab = selectedTab; } return [aTabs, aTabData]; }, /** * Manage history restoration for a window * @param aWindow * Window to restore the tabs into * @param aTabs * Array of tab references * @param aTabData * Array of tab data * @param aSelectTab * Index of selected tab * @param aIx * Index of the next tab to check readyness for * @param aCount * Counter for number of times delaying b/c browser or history aren't ready * @param aRestoreImmediately * Flag to indicate whether the given set of tabs aTabs should be * restored/loaded immediately even if restore_on_demand = true */ restoreHistoryPrecursor: function(aWindow, aTabs, aTabData, aSelectTab, aIx, aCount, aRestoreImmediately = false) { var tabbrowser = aWindow.gBrowser; // make sure that all browsers and their histories are available // - if one's not, resume this check in 100ms (repeat at most 10 times) for (var t = aIx; t < aTabs.length; t++) { try { if (!tabbrowser.getBrowserForTab(aTabs[t]).webNavigation.sessionHistory) { throw new Error(); } } catch (ex) { // in case browser or history aren't ready yet if (aCount < 10) { var restoreHistoryFunc = function(self) { self.restoreHistoryPrecursor(aWindow, aTabs, aTabData, aSelectTab, aIx, aCount + 1, aRestoreImmediately); } aWindow.setTimeout(restoreHistoryFunc, 100, this); return; } } } if (!this._isWindowLoaded(aWindow)) { // from now on, the data will come from the actual window delete this._statesToRestore[aWindow.__SS_restoreID]; delete aWindow.__SS_restoreID; delete this._windows[aWindow.__SSi]._restoring; // It's important to set the window state to dirty so that // we collect their data for the first time when saving state. this._dirtyWindows[aWindow.__SSi] = true; } if (aTabs.length == 0) { // this is normally done in restoreHistory() but as we're returning early // here we need to take care of it. this._setWindowStateReady(aWindow); return; } // Sets the tabs restoring order. [aTabs, aTabData] = this._setTabsRestoringOrder(tabbrowser, aTabs, aTabData, aSelectTab); // Prepare the tabs so that they can be properly restored. We'll pin/unpin // and show/hide tabs as necessary. We'll also set the labels, user typed // value, and attach a copy of the tab's data in case we close it before // it's been restored. for (t = 0; t < aTabs.length; t++) { let tab = aTabs[t]; let browser = tabbrowser.getBrowserForTab(tab); let tabData = aTabData[t]; if (tabData.pinned) tabbrowser.pinTab(tab); else tabbrowser.unpinTab(tab); if (tabData.hidden) tabbrowser.hideTab(tab); else tabbrowser.showTab(tab); if ("attributes" in tabData) { // Ensure that we persist tab attributes restored from previous sessions. Object.keys(tabData.attributes).forEach(a => TabAttributes.persist(a)); } browser.__SS_tabStillLoading = true; // keep the data around to prevent dataloss in case // a tab gets closed before it's been properly restored browser.__SS_data = tabData; browser.__SS_restoreState = TAB_STATE_NEEDS_RESTORE; browser.setAttribute("pending", "true"); tab.setAttribute("pending", "true"); // Make sure that set/getTabValue will set/read the correct data by // wiping out any current value in tab.__SS_extdata. delete tab.__SS_extdata; if (!tabData.entries || tabData.entries.length == 0) { // make sure to blank out this tab's content // (just purging the tab's history won't be enough) browser.contentDocument.location = "about:blank"; continue; } browser.stop(); // in case about:blank isn't done yet // wall-paper fix for bug 439675: make sure that the URL to be loaded // is always visible in the address bar let activeIndex = (tabData.index || tabData.entries.length) - 1; let activePageData = tabData.entries[activeIndex] || null; let uri = activePageData ? activePageData.url || null : null; browser.userTypedValue = uri; // Also make sure currentURI is set so that switch-to-tab works before // the tab is restored. We'll reset this to about:blank when we try to // restore the tab to ensure that docshell doeesn't get confused. if (uri) browser.docShell.setCurrentURI(this._getURIFromString(uri)); // If the page has a title, set it. if (activePageData) { if (activePageData.title) { tab.label = activePageData.title; tab.crop = "end"; } else if (activePageData.url != "about:blank") { tab.label = activePageData.url; tab.crop = "center"; } } } // helper hashes for ensuring unique frame IDs and unique document // identifiers. var idMap = { used: {} }; var docIdentMap = {}; this.restoreHistory(aWindow, aTabs, aTabData, idMap, docIdentMap, aRestoreImmediately); }, /** * Restore history for a window * @param aWindow * Window reference * @param aTabs * Array of tab references * @param aTabData * Array of tab data * @param aIdMap * Hash for ensuring unique frame IDs * @param aRestoreImmediately * Flag to indicate whether the given set of tabs aTabs should be * restored/loaded immediately even if restore_on_demand = true */ restoreHistory: function(aWindow, aTabs, aTabData, aIdMap, aDocIdentMap, aRestoreImmediately) { var _this = this; // if the tab got removed before being completely restored, then skip it while (aTabs.length > 0 && !(this._canRestoreTabHistory(aTabs[0]))) { aTabs.shift(); aTabData.shift(); } if (aTabs.length == 0) { // At this point we're essentially ready for consumers to read/write data // via the sessionstore API so we'll send the SSWindowStateReady event. this._setWindowStateReady(aWindow); return; // no more tabs to restore } var tab = aTabs.shift(); var tabData = aTabData.shift(); var browser = aWindow.gBrowser.getBrowserForTab(tab); var history = browser.webNavigation.sessionHistory; if (history.count > 0) { history.PurgeHistory(history.count); } history.QueryInterface(Ci.nsISHistoryInternal); browser.__SS_shistoryListener = new SessionStoreSHistoryListener(tab); history.addSHistoryListener(browser.__SS_shistoryListener); if (!tabData.entries) { tabData.entries = []; } if (tabData.extData) { tab.__SS_extdata = {}; for (let key in tabData.extData) tab.__SS_extdata[key] = tabData.extData[key]; } else delete tab.__SS_extdata; for (var i = 0; i < tabData.entries.length; i++) { //XXXzpao Wallpaper patch for bug 514751 if (!tabData.entries[i].url) continue; history.addEntry(this._deserializeHistoryEntry(tabData.entries[i], aIdMap, aDocIdentMap), true); } // make sure to reset the capabilities and attributes, in case this tab gets reused let disallow = new Set(tabData.disallow && tabData.disallow.split(",")); for (let cap of gDocShellCapabilities(browser.docShell)) browser.docShell["allow" + cap] = !disallow.has(cap); // Restore tab attributes. if ("attributes" in tabData) { TabAttributes.set(tab, tabData.attributes); } // Restore the tab icon. if ("image" in tabData) { // Using null as the loadingPrincipal because serializing // the principal would be overkill. Within SetIcon we // default to the systemPrincipal if aLoadingPrincipal is // null which will allow the favicon to load. aWindow.gBrowser.setIcon(tab, tabData.image, null); } if (tabData.storage && browser.docShell instanceof Ci.nsIDocShell) SessionStorage.deserialize(browser.docShell, tabData.storage); // notify the tabbrowser that the tab chrome has been restored var event = aWindow.document.createEvent("Events"); event.initEvent("SSTabRestoring", true, false); tab.dispatchEvent(event); // Restore the history in the next tab aWindow.setTimeout(function(){ _this.restoreHistory(aWindow, aTabs, aTabData, aIdMap, aDocIdentMap, aRestoreImmediately); }, 0); // This could cause us to ignore max_concurrent_tabs pref a bit, but // it ensures each window will have its selected tab loaded. if (aRestoreImmediately || aWindow.gBrowser.selectedBrowser == browser) { this.restoreTab(tab); } else { TabRestoreQueue.add(tab); this.restoreNextTab(); } }, /** * Restores the specified tab. If the tab can't be restored (eg, no history or * calling gotoIndex fails), then state changes will be rolled back. * This method will check if gTabsProgressListener is attached to the tab's * window, ensuring that we don't get caught without one. * This method removes the session history listener right before starting to * attempt a load. This will prevent cases of "stuck" listeners. * If this method returns false, then it is up to the caller to decide what to * do. In the common case (restoreNextTab), we will want to then attempt to * restore the next tab. In the other case (selecting the tab, reloading the * tab), the caller doesn't actually want to do anything if no page is loaded. * * @param aTab * the tab to restore * * @returns true/false indicating whether or not a load actually happened */ restoreTab: function(aTab) { let window = aTab.ownerDocument.defaultView; let browser = aTab.linkedBrowser; let tabData = browser.__SS_data; // There are cases within where we haven't actually started a load. In that // that case we'll reset state changes we made and return false to the caller // can handle appropriately. let didStartLoad = false; // Make sure that the tabs progress listener is attached to this window this._ensureTabsProgressListener(window); // Make sure that this tab is removed from the priority queue. TabRestoreQueue.remove(aTab); // Increase our internal count. this._tabsRestoringCount++; // Set this tab's state to restoring browser.__SS_restoreState = TAB_STATE_RESTORING; browser.removeAttribute("pending"); aTab.removeAttribute("pending"); // Remove the history listener, since we no longer need it once we start restoring this._removeSHistoryListener(aTab); let activeIndex = (tabData.index || tabData.entries.length) - 1; if (activeIndex >= tabData.entries.length) activeIndex = tabData.entries.length - 1; // Reset currentURI. This creates a new session history entry with a new // doc identifier, so we need to explicitly save and restore the old doc // identifier (corresponding to the SHEntry at activeIndex) below. browser.webNavigation.setCurrentURI(this._getURIFromString("about:blank")); // Attach data that will be restored on "load" event, after tab is restored. if (activeIndex > -1) { // restore those aspects of the currently active documents which are not // preserved in the plain history entries (mainly scroll state and text data) browser.__SS_restore_data = tabData.entries[activeIndex] || {}; browser.__SS_restore_pageStyle = tabData.pageStyle || ""; browser.__SS_restore_tab = aTab; didStartLoad = true; try { // In order to work around certain issues in session history, we need to // force session history to update its internal index and call reload // instead of gotoIndex. See bug 597315. browser.webNavigation.sessionHistory.getEntryAtIndex(activeIndex, true); browser.webNavigation.sessionHistory.reloadCurrentEntry(); // If the user prefers it, bypass cache and always load from the network, // but only if restoring on demand, to prevent request flooding (since // reloading will override the max tabs to restore concurrently mechanism). // See Issue #1772 if (TabRestoreQueue.prefs.restoreOnDemand) { let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE; switch (this._cacheBehavior) { case 2: // hard refresh flags = Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY | Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE; browser.webNavigation.reload(flags); break; case 1: // soft refresh browser.webNavigation.reload(flags); break; default: // 0 or other: use cache, so do nothing. break; } } } catch (ex) { // ignore page load errors aTab.removeAttribute("busy"); didStartLoad = false; } } // Handle userTypedValue. Setting userTypedValue seems to update gURLbar // as needed. Calling loadURI will cancel form filling in restoreDocument if (tabData.userTypedValue) { browser.userTypedValue = tabData.userTypedValue; if (tabData.userTypedClear) { // Make it so that we'll enter restoreDocument on page load. We will // fire SSTabRestored from there. We don't have any form data to restore // so we can just set the URL to null. browser.__SS_restore_data = { url: null }; browser.__SS_restore_tab = aTab; if (didStartLoad) browser.stop(); didStartLoad = true; browser.loadURIWithFlags(tabData.userTypedValue, Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP); } } // If we didn't start a load, then we won't reset this tab through the usual // channel (via the progress listener), so reset the tab ourselves. We will // also send SSTabRestored since this tab has technically been restored. if (!didStartLoad) { this._sendTabRestoredNotification(aTab); this._resetTabRestoringState(aTab); } return didStartLoad; }, /** * This _attempts_ to restore the next available tab. If the restore fails, * then we will attempt the next one. * There are conditions where this won't do anything: * if we're in the process of quitting * if there are no tabs to restore * if we have already reached the limit for number of tabs to restore */ restoreNextTab: function() { // If we call in here while quitting, we don't actually want to do anything if (this._loadState == STATE_QUITTING) return; // Don't exceed the maximum number of concurrent tab restores. if (this._tabsRestoringCount >= this._maxConcurrentTabRestores) return; let tab = TabRestoreQueue.shift(); if (tab) { let didStartLoad = this.restoreTab(tab); // If we don't start a load in the restored tab (eg, no entries) then we // want to attempt to restore the next tab. if (!didStartLoad) this.restoreNextTab(); } }, /** * expands serialized history data into a session-history-entry instance * @param aEntry * Object containing serialized history data for a URL * @param aIdMap * Hash for ensuring unique frame IDs * @returns nsISHEntry */ _deserializeHistoryEntry: function(aEntry, aIdMap, aDocIdentMap) { var shEntry = Cc["@mozilla.org/browser/session-history-entry;1"]. createInstance(Ci.nsISHEntry); shEntry.setURI(this._getURIFromString(aEntry.url)); shEntry.setTitle(aEntry.title || aEntry.url); if (aEntry.subframe) shEntry.setIsSubFrame(aEntry.subframe || false); shEntry.loadType = Ci.nsIDocShellLoadInfo.loadHistory; if (aEntry.contentType) shEntry.contentType = aEntry.contentType; if (aEntry.referrer) shEntry.referrerURI = this._getURIFromString(aEntry.referrer); if (aEntry.isSrcdocEntry) shEntry.srcdocData = aEntry.srcdocData; if (aEntry.cacheKey) { var cacheKey = Cc["@mozilla.org/supports-PRUint32;1"]. createInstance(Ci.nsISupportsPRUint32); cacheKey.data = aEntry.cacheKey; shEntry.cacheKey = cacheKey; } if (aEntry.ID) { // get a new unique ID for this frame (since the one from the last // start might already be in use) var id = aIdMap[aEntry.ID] || 0; if (!id) { for (id = Date.now(); id in aIdMap.used; id++); aIdMap[aEntry.ID] = id; aIdMap.used[id] = true; } shEntry.ID = id; } if (aEntry.docshellID) shEntry.docshellID = aEntry.docshellID; if (aEntry.structuredCloneState && aEntry.structuredCloneVersion) { shEntry.stateData = Cc["@mozilla.org/docshell/structured-clone-container;1"]. createInstance(Ci.nsIStructuredCloneContainer); shEntry.stateData.initFromBase64(aEntry.structuredCloneState, aEntry.structuredCloneVersion); } if (aEntry.scroll) { var scrollPos = (aEntry.scroll || "0,0").split(","); scrollPos = [parseInt(scrollPos[0]) || 0, parseInt(scrollPos[1]) || 0]; shEntry.setScrollPosition(scrollPos[0], scrollPos[1]); } if (aEntry.postdata_b64) { var postdata = atob(aEntry.postdata_b64); var stream = Cc["@mozilla.org/io/string-input-stream;1"]. createInstance(Ci.nsIStringInputStream); stream.setData(postdata, postdata.length); shEntry.postData = stream; } let childDocIdents = {}; if (aEntry.docIdentifier) { // If we have a serialized document identifier, try to find an SHEntry // which matches that doc identifier and adopt that SHEntry's // BFCacheEntry. If we don't find a match, insert shEntry as the match // for the document identifier. let matchingEntry = aDocIdentMap[aEntry.docIdentifier]; if (!matchingEntry) { matchingEntry = {shEntry: shEntry, childDocIdents: childDocIdents}; aDocIdentMap[aEntry.docIdentifier] = matchingEntry; } else { shEntry.adoptBFCacheEntry(matchingEntry.shEntry); childDocIdents = matchingEntry.childDocIdents; } } // The field aEntry.owner_b64 got renamed to aEntry.triggeringPricipal_b64 in // Bug 1286472. To remain backward compatible we still have to support that // field for a few cycles before we can remove it within Bug 1289785. if (aEntry.owner_b64) { aEntry.triggeringPrincipal_b64 = aEntry.owner_b64; delete aEntry.owner_b64; } if (aEntry.triggeringPrincipal_b64) { var triggeringPrincipalInput = Cc["@mozilla.org/io/string-input-stream;1"]. createInstance(Ci.nsIStringInputStream); var binaryData = atob(aEntry.triggeringPrincipal_b64); triggeringPrincipalInput.setData(binaryData, binaryData.length); var binaryStream = Cc["@mozilla.org/binaryinputstream;1"]. createInstance(Ci.nsIObjectInputStream); binaryStream.setInputStream(triggeringPrincipalInput); try { // Catch possible deserialization exceptions shEntry.triggeringPrincipal = binaryStream.readObject(true); } catch (ex) { debug(ex); } } if (aEntry.children && shEntry instanceof Ci.nsISHContainer) { for (var i = 0; i < aEntry.children.length; i++) { //XXXzpao Wallpaper patch for bug 514751 if (!aEntry.children[i].url) continue; // We're getting sessionrestore.js files with a cycle in the // doc-identifier graph, likely due to bug 698656. (That is, we have // an entry where doc identifier A is an ancestor of doc identifier B, // and another entry where doc identifier B is an ancestor of A.) // // If we were to respect these doc identifiers, we'd create a cycle in // the SHEntries themselves, which causes the docshell to loop forever // when it looks for the root SHEntry. // // So as a hack to fix this, we restrict the scope of a doc identifier // to be a node's siblings and cousins, and pass childDocIdents, not // aDocIdents, to _deserializeHistoryEntry. That is, we say that two // SHEntries with the same doc identifier have the same document iff // they have the same parent or their parents have the same document. shEntry.AddChild(this._deserializeHistoryEntry(aEntry.children[i], aIdMap, childDocIdents), i); } } return shEntry; }, /** * Restore properties to a loaded document */ restoreDocument: function(aWindow, aBrowser, aEvent) { // wait for the top frame to be loaded completely if (!aEvent || !aEvent.originalTarget || !aEvent.originalTarget.defaultView || aEvent.originalTarget.defaultView != aEvent.originalTarget.defaultView.top) { return; } // always call this before injecting content into a document! function hasExpectedURL(aDocument, aURL) !aURL || aURL.replace(/#.*/, "") == aDocument.location.href.replace(/#.*/, ""); let selectedPageStyle = aBrowser.__SS_restore_pageStyle; function restoreTextDataAndScrolling(aContent, aData, aPrefix) { if (aData.formdata && hasExpectedURL(aContent.document, aData.url)) { let formdata = aData.formdata; // handle backwards compatibility // this is a migration from pre-firefox 15. cf. bug 742051 if (!("xpath" in formdata || "id" in formdata)) { formdata = { xpath: {}, id: {} }; for each (let [key, value] in Iterator(aData.formdata)) { if (key.charAt(0) == "#") { formdata.id[key.slice(1)] = value; } else { formdata.xpath[key] = value; } } } // for about:sessionrestore we saved the field as JSON to avoid // nested instances causing humongous sessionstore.js files. // cf. bug 467409 if (aData.url == "about:sessionrestore" && "sessionData" in formdata.id && typeof formdata.id["sessionData"] == "object") { formdata.id["sessionData"] = JSON.stringify(formdata.id["sessionData"]); } // update the formdata aData.formdata = formdata; // merge the formdata DocumentUtils.mergeFormData(aContent.document, formdata); } if (aData.innerHTML) { aWindow.setTimeout(function() { if (aContent.document.designMode == "on" && hasExpectedURL(aContent.document, aData.url) && aContent.document.body) { aContent.document.body.innerHTML = aData.innerHTML; } }, 0); } var match; if (aData.scroll && (match = /(\d+),(\d+)/.exec(aData.scroll)) != null) { aContent.scrollTo(match[1], match[2]); } Array.forEach(aContent.document.styleSheets, function(aSS) { aSS.disabled = aSS.title && aSS.title != selectedPageStyle; }); for (var i = 0; i < aContent.frames.length; i++) { if (aData.children && aData.children[i] && hasExpectedURL(aContent.document, aData.url)) { restoreTextDataAndScrolling(aContent.frames[i], aData.children[i], aPrefix + i + "|"); } } } // don't restore text data and scrolling state if the user has navigated // away before the loading completed (except for in-page navigation) if (hasExpectedURL(aEvent.originalTarget, aBrowser.__SS_restore_data.url)) { var content = aEvent.originalTarget.defaultView; restoreTextDataAndScrolling(content, aBrowser.__SS_restore_data, ""); aBrowser.markupDocumentViewer.authorStyleDisabled = selectedPageStyle == "_nostyle"; } // notify the tabbrowser that this document has been completely restored this._sendTabRestoredNotification(aBrowser.__SS_restore_tab); delete aBrowser.__SS_restore_data; delete aBrowser.__SS_restore_pageStyle; delete aBrowser.__SS_restore_tab; }, /** * Restore visibility and dimension features to a window * @param aWindow * Window reference * @param aWinData * Object containing session data for the window */ restoreWindowFeatures: function(aWindow, aWinData) { var hidden = (aWinData.hidden)?aWinData.hidden.split(","):[]; WINDOW_HIDEABLE_FEATURES.forEach(function(aItem) { aWindow[aItem].visible = hidden.indexOf(aItem) == -1; }); if (aWinData.isPopup) { this._windows[aWindow.__SSi].isPopup = true; if (aWindow.gURLBar) { aWindow.gURLBar.readOnly = true; aWindow.gURLBar.setAttribute("enablehistory", "false"); } } else { delete this._windows[aWindow.__SSi].isPopup; if (aWindow.gURLBar) { aWindow.gURLBar.readOnly = false; aWindow.gURLBar.setAttribute("enablehistory", "true"); } } var _this = this; aWindow.setTimeout(function() { _this.restoreDimensions.apply(_this, [aWindow, +aWinData.width || 0, +aWinData.height || 0, "screenX" in aWinData ? +aWinData.screenX : NaN, "screenY" in aWinData ? +aWinData.screenY : NaN, aWinData.sizemode || "", aWinData.sidebar || ""]); }, 0); }, /** * Restore a window's dimensions * @param aWidth * Window width * @param aHeight * Window height * @param aLeft * Window left * @param aTop * Window top * @param aSizeMode * Window size mode (eg: maximized) * @param aSidebar * Sidebar command */ restoreDimensions: function(aWindow, aWidth, aHeight, aLeft, aTop, aSizeMode, aSidebar) { var win = aWindow; var _this = this; function win_(aName) { return _this._getWindowDimension(win, aName); } // Find available space on the screen where this window is being placed let screen = gScreenManager.screenForRect(aLeft, aTop, aWidth, aHeight); if (screen && !this._prefBranch.getBoolPref("sessionstore.exactPos")) { let screenLeft = {}, screenTop = {}, screenWidth = {}, screenHeight = {}; screen.GetAvailRectDisplayPix(screenLeft, screenTop, screenWidth, screenHeight); // Screen X/Y are based on the origin of the screen's desktop-pixel coordinate space let screenLeftCss = screenLeft.value; let screenTopCss = screenTop.value; // Convert the screen's device pixel dimensions to CSS px dimensions screen.GetAvailRect(screenLeft, screenTop, screenWidth, screenHeight); let cssToDevScale = screen.defaultCSSScaleFactor; let screenRightCss = screenLeftCss + screenWidth.value / cssToDevScale; let screenBottomCss = screenTopCss + screenHeight.value / cssToDevScale; // Pull the window within the screen's bounds. // First, ensure the left edge is on-screen if (aLeft < screenLeftCss) { aLeft = screenLeftCss; } // Then check the resulting right edge, and reduce it if necessary. let right = aLeft + aWidth; if (right > screenRightCss) { right = screenRightCss; // See if we can move the left edge leftwards to maintain width. if (aLeft > screenLeftCss) { aLeft = Math.max(right - aWidth, screenLeftCss); } } // Finally, update aWidth to account for the adjusted left and right edges. aWidth = right - aLeft; // Do the same in the vertical dimension. // First, ensure the top edge is on-screen if (aTop < screenTopCss) { aTop = screenTopCss; } // Then check the resulting right edge, and reduce it if necessary. let bottom = aTop + aHeight; if (bottom > screenBottomCss) { bottom = screenBottomCss; // See if we can move the top edge upwards to maintain height. if (aTop > screenTopCss) { aTop = Math.max(bottom - aHeight, screenTopCss); } } // Finally, update aHeight to account for the adjusted top and bottom edges. aHeight = bottom - aTop; } // Only modify those aspects which aren't correct yet if (!isNaN(aLeft) && !isNaN(aTop) && (aLeft != win_("screenX") || aTop != win_("screenY"))) { aWindow.moveTo(aLeft, aTop); } if (aWidth && aHeight && (aWidth != win_("width") || aHeight != win_("height"))) { // Don't resize the window if it's currently maximized and we would // maximize it again shortly after. if (aSizeMode != "maximized" || win_("sizemode") != "maximized") { aWindow.resizeTo(aWidth, aHeight); } } // Restore window state if (aSizeMode && win_("sizemode") != aSizeMode) { switch (aSizeMode) { case "maximized": aWindow.maximize(); break; case "minimized": aWindow.minimize(); break; case "normal": aWindow.restore(); break; } } var sidebar = aWindow.document.getElementById("sidebar-box"); if (sidebar.getAttribute("sidebarcommand") != aSidebar) { aWindow.toggleSidebar(aSidebar); } // since resizing/moving a window brings it to the foreground, // we might want to re-focus the last focused window if (this.windowToFocus) { this.windowToFocus.focus(); } }, /** * Restores cookies * @param aCookies * Array of cookie objects */ restoreCookies: function(aCookies) { // MAX_EXPIRY should be 2^63-1, but JavaScript can't handle that precision var MAX_EXPIRY = Math.pow(2, 62); for (let i = 0; i < aCookies.length; i++) { var cookie = aCookies[i]; try { Services.cookies.add(cookie.host, cookie.path || "", cookie.name || "", cookie.value, !!cookie.secure, !!cookie.httponly, true, "expiry" in cookie ? cookie.expiry : MAX_EXPIRY, {}); } catch (ex) { Cu.reportError(ex); } // don't let a single cookie stop recovering } }, /* ........ Disk Access .............. */ /** * save state delayed by N ms * marks window as dirty (i.e. data update can't be skipped) * @param aWindow * Window reference * @param aDelay * Milliseconds to delay */ saveStateDelayed: function(aWindow, aDelay) { if (aWindow) { this._dirtyWindows[aWindow.__SSi] = true; } if (!this._saveTimer) { // interval until the next disk operation is allowed var minimalDelay = this._lastSaveTime + this._interval - Date.now(); // if we have to wait, set a timer, otherwise saveState directly aDelay = Math.max(minimalDelay, aDelay || 2000); if (aDelay > 0) { this._saveTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); this._saveTimer.init(this, aDelay, Ci.nsITimer.TYPE_ONE_SHOT); } else { this.saveState(); } } }, /** * save state to disk * @param aUpdateAll * Bool update all windows */ saveState: function(aUpdateAll) { // If crash recovery is disabled, we only want to resume with pinned tabs // if we crash. let pinnedOnly = this._loadState == STATE_RUNNING && !this._resume_from_crash; var oState = this._getCurrentState(aUpdateAll, pinnedOnly); if (!oState) { return; } // Forget about private windows. for (let i = oState.windows.length - 1; i >= 0; i--) { if (oState.windows[i].isPrivate) { oState.windows.splice(i, 1); if (oState.selectedWindow >= i) { oState.selectedWindow--; } } } for (let i = oState._closedWindows.length - 1; i >= 0; i--) { if (oState._closedWindows[i].isPrivate) { oState._closedWindows.splice(i, 1); } } #ifndef XP_MACOSX // We want to restore closed windows that are marked with _shouldRestore. // We're doing this here because we want to control this only when saving // the file. while (oState._closedWindows.length) { let i = oState._closedWindows.length - 1; if (oState._closedWindows[i]._shouldRestore) { delete oState._closedWindows[i]._shouldRestore; oState.windows.unshift(oState._closedWindows.pop()); } else { // We only need to go until we hit !needsRestore since we're going in reverse break; } } #endif if (pinnedOnly) { // Save original resume_session_once preference for when quiting browser, // otherwise session will be restored next time browser starts and we // only want it to be restored in the case of a crash. if (this._resume_session_once_on_shutdown == null) { this._resume_session_once_on_shutdown = this._prefBranch.getBoolPref("sessionstore.resume_session_once"); this._prefBranch.setBoolPref("sessionstore.resume_session_once", true); // flush the preference file so preference will be saved in case of a crash Services.prefs.savePrefFile(null); } } // Persist the last session if we deferred restoring it if (this._lastSessionState) oState.lastSessionState = this._lastSessionState; // Make sure that we keep the previous session if we started with a single // private window and no non-private windows have been opened, yet. if (this._deferredInitialState) { oState.windows = this._deferredInitialState.windows || []; } this._saveStateObject(oState); }, /** * write a state object to disk */ _saveStateObject: function(aStateObj) { let data = this._toJSONString(aStateObj); let stateString = this._createSupportsString(data); Services.obs.notifyObservers(stateString, "sessionstore-state-write", ""); data = stateString.data; // Don't touch the file if an observer has deleted all state data. if (!data) { return; } let promise; // If "sessionstore.resume_from_crash" is true, attempt to backup the // session file first, before writing to it. if (this._resume_from_crash) { // Note that we do not have race conditions here as _SessionFile // guarantees that any I/O operation is completed before proceeding to // the next I/O operation. // Note backup happens only once, on initial save. promise = this._backupSessionFileOnce; } else { promise = Promise.resolve(); } // Attempt to write to the session file (potentially, depending on // "sessionstore.resume_from_crash" preference, after successful backup). promise = promise.then(function onSuccess() { // Write (atomically) to a session file, using a tmp file. return _SessionFile.write(data); }); // Once the session file is successfully updated, save the time stamp of the // last save and notify the observers. promise = promise.then(() => { this._lastSaveTime = Date.now(); Services.obs.notifyObservers(null, "sessionstore-state-write-complete", ""); }); }, /* ........ Auxiliary Functions .............. */ // Wrap a string as a nsISupports _createSupportsString: function(aData) { let string = Cc["@mozilla.org/supports-string;1"] .createInstance(Ci.nsISupportsString); string.data = aData; return string; }, /** * call a callback for all currently opened browser windows * (might miss the most recent one) * @param aFunc * Callback each window is passed to */ _forEachBrowserWindow: function(aFunc) { var windowsEnum = Services.wm.getEnumerator("navigator:browser"); while (windowsEnum.hasMoreElements()) { var window = windowsEnum.getNext(); if (window.__SSi && !window.closed) { aFunc.call(this, window); } } }, /** * Returns most recent window * @returns Window reference */ _getMostRecentBrowserWindow: function() { var win = Services.wm.getMostRecentWindow("navigator:browser"); if (!win) return null; if (!win.closed) return win; #ifdef BROKEN_WM_Z_ORDER win = null; var windowsEnum = Services.wm.getEnumerator("navigator:browser"); // this is oldest to newest, so this gets a bit ugly while (windowsEnum.hasMoreElements()) { let nextWin = windowsEnum.getNext(); if (!nextWin.closed) win = nextWin; } return win; #else var windowsEnum = Services.wm.getZOrderDOMWindowEnumerator("navigator:browser", true); while (windowsEnum.hasMoreElements()) { win = windowsEnum.getNext(); if (!win.closed) return win; } return null; #endif }, /** * Calls onClose for windows that are determined to be closed but aren't * destroyed yet, which would otherwise cause getBrowserState and * setBrowserState to treat them as open windows. */ _handleClosedWindows: function() { var windowsEnum = Services.wm.getEnumerator("navigator:browser"); while (windowsEnum.hasMoreElements()) { var window = windowsEnum.getNext(); if (window.closed) { this.onClose(window); } } }, /** * open a new browser window for a given session state * called when restoring a multi-window session * @param aState * Object containing session data */ _openWindowWithState: function(aState) { var argString = Cc["@mozilla.org/supports-string;1"]. createInstance(Ci.nsISupportsString); argString.data = ""; // Build feature string let features = "chrome,dialog=no,macsuppressanimation,all"; let winState = aState.windows[0]; WINDOW_ATTRIBUTES.forEach(function(aFeature) { // Use !isNaN as an easy way to ignore sizemode and check for numbers if (aFeature in winState && !isNaN(winState[aFeature])) features += "," + aFeature + "=" + winState[aFeature]; }); if (winState.isPrivate) { features += ",private"; } var window = Services.ww.openWindow(null, this._prefBranch.getCharPref("chromeURL"), "_blank", features, argString); do { var ID = "window" + Math.random(); } while (ID in this._statesToRestore); this._statesToRestore[(window.__SS_restoreID = ID)] = aState; return window; }, /** * Whether or not to resume session, if not recovering from a crash. * @returns bool */ _doResumeSession: function() { return this._prefBranch.getIntPref("startup.page") == 3 || this._prefBranch.getBoolPref("sessionstore.resume_session_once"); }, /** * whether the user wants to load any other page at startup * (except the homepage) - needed for determining whether to overwrite the current tabs * C.f.: nsBrowserContentHandler's defaultArgs implementation. * @returns bool */ _isCmdLineEmpty: function(aWindow, aState) { var pinnedOnly = aState.windows && aState.windows.every(function(win) win.tabs.every(function(tab) tab.pinned)); let hasFirstArgument = aWindow.arguments && aWindow.arguments[0]; if (!pinnedOnly) { let defaultArgs = Cc["@mozilla.org/browser/clh;1"]. getService(Ci.nsIBrowserHandler).defaultArgs; if (aWindow.arguments && aWindow.arguments[0] && aWindow.arguments[0] == defaultArgs) hasFirstArgument = false; } return !hasFirstArgument; }, /** * don't save sensitive data if the user doesn't want to * (distinguishes between encrypted and non-encrypted sites) * @param aIsHTTPS * Bool is encrypted * @param aUseDefaultPref * don't do normal check for deferred * @returns bool */ checkPrivacyLevel: function(aIsHTTPS, aUseDefaultPref) { let pref = "sessionstore.privacy_level"; // If we're in the process of quitting and we're not autoresuming the session // then we should treat it as a deferred session. We have a different privacy // pref for that case. if (!aUseDefaultPref && this._loadState == STATE_QUITTING && !this._doResumeSession()) pref = "sessionstore.privacy_level_deferred"; return this._prefBranch.getIntPref(pref) < (aIsHTTPS ? PRIVACY_ENCRYPTED : PRIVACY_FULL); }, /** * on popup windows, the XULWindow's attributes seem not to be set correctly * we use thus JSDOMWindow attributes for sizemode and normal window attributes * (and hope for reasonable values when maximized/minimized - since then * outerWidth/outerHeight aren't the dimensions of the restored window) * @param aWindow * Window reference * @param aAttribute * String sizemode | width | height | other window attribute * @returns string */ _getWindowDimension: function(aWindow, aAttribute) { if (aAttribute == "sizemode") { switch (aWindow.windowState) { case aWindow.STATE_FULLSCREEN: case aWindow.STATE_MAXIMIZED: return "maximized"; case aWindow.STATE_MINIMIZED: return "minimized"; default: return "normal"; } } var dimension; switch (aAttribute) { case "width": dimension = aWindow.outerWidth; break; case "height": dimension = aWindow.outerHeight; break; default: dimension = aAttribute in aWindow ? aWindow[aAttribute] : ""; break; } if (aWindow.windowState == aWindow.STATE_NORMAL) { return dimension; } return aWindow.document.documentElement.getAttribute(aAttribute) || dimension; }, /** * Get nsIURI from string * @param string * @returns nsIURI */ _getURIFromString: function(aString) { return Services.io.newURI(aString, null, null); }, /** * @param aState is a session state * @param aRecentCrashes is the number of consecutive crashes * @returns whether a restore page will be needed for the session state */ _needsRestorePage: function(aState, aRecentCrashes) { const SIX_HOURS_IN_MS = 6 * 60 * 60 * 1000; // don't display the page when there's nothing to restore let winData = aState.windows || null; if (!winData || winData.length == 0) return false; // don't wrap a single about:sessionrestore page if (winData.length == 1 && winData[0].tabs && winData[0].tabs.length == 1 && winData[0].tabs[0].entries && winData[0].tabs[0].entries.length == 1 && winData[0].tabs[0].entries[0].url == "about:sessionrestore") return false; // don't automatically restore in Safe Mode if (Services.appinfo.inSafeMode) return true; let max_resumed_crashes = this._prefBranch.getIntPref("sessionstore.max_resumed_crashes"); let sessionAge = aState.session && aState.session.lastUpdate && (Date.now() - aState.session.lastUpdate); return max_resumed_crashes != -1 && (aRecentCrashes > max_resumed_crashes || sessionAge && sessionAge >= SIX_HOURS_IN_MS); }, /** * Determine if the tab state we're passed is something we should save. This * is used when closing a tab or closing a window with a single tab * * @param aTabState * The current tab state * @returns boolean */ _shouldSaveTabState: function(aTabState) { // If the tab has only a transient about: history entry, no other // session history, and no userTypedValue, then we don't actually want to // store this tab's data. return aTabState.entries.length && !(aTabState.entries.length == 1 && (aTabState.entries[0].url == "about:blank" || aTabState.entries[0].url == "about:newtab") && !aTabState.userTypedValue); }, /** * Determine if we can restore history into this tab. * This will be false when a tab has been removed (usually between * restoreHistoryPrecursor && restoreHistory) or if the tab is still marked * as loading. * * @param aTab * @returns boolean */ _canRestoreTabHistory: function(aTab) { return aTab.parentNode && aTab.linkedBrowser && aTab.linkedBrowser.__SS_tabStillLoading; }, /** * This is going to take a state as provided at startup (via * nsISessionStartup.state) and split it into 2 parts. The first part * (defaultState) will be a state that should still be restored at startup, * while the second part (state) is a state that should be saved for later. * defaultState will be comprised of windows with only pinned tabs, extracted * from state. It will contain the cookies that go along with the history * entries in those tabs. It will also contain window position information. * * defaultState will be restored at startup. state will be placed into * this._lastSessionState and will be kept in case the user explicitly wants * to restore the previous session (publicly exposed as restoreLastSession). * * @param state * The state, presumably from nsISessionStartup.state * @returns [defaultState, state] */ _prepDataForDeferredRestore: function(state) { // Make sure that we don't modify the global state as provided by // nsSessionStartup.state. Converting the object to a JSON string and // parsing it again is the easiest way to do that, although not the most // efficient one. Deferred sessions that don't have automatic session // restore enabled tend to be a lot smaller though so that this shouldn't // be a big perf hit. state = JSON.parse(JSON.stringify(state)); let defaultState = { windows: [], selectedWindow: 1 }; state.selectedWindow = state.selectedWindow || 1; // Look at each window, remove pinned tabs, adjust selectedindex, // remove window if necessary. for (let wIndex = 0; wIndex < state.windows.length;) { let window = state.windows[wIndex]; window.selected = window.selected || 1; // We're going to put the state of the window into this object let pinnedWindowState = { tabs: [], cookies: []}; for (let tIndex = 0; tIndex < window.tabs.length;) { if (window.tabs[tIndex].pinned) { // Adjust window.selected if (tIndex + 1 < window.selected) window.selected -= 1; else if (tIndex + 1 == window.selected) pinnedWindowState.selected = pinnedWindowState.tabs.length + 2; // + 2 because the tab isn't actually in the array yet // Now add the pinned tab to our window pinnedWindowState.tabs = pinnedWindowState.tabs.concat(window.tabs.splice(tIndex, 1)); // We don't want to increment tIndex here. continue; } tIndex++; } // At this point the window in the state object has been modified (or not) // We want to build the rest of this new window object if we have pinnedTabs. if (pinnedWindowState.tabs.length) { // First get the other attributes off the window WINDOW_ATTRIBUTES.forEach(function(attr) { if (attr in window) { pinnedWindowState[attr] = window[attr]; delete window[attr]; } }); // We're just copying position data into the pinned window. // Not copying over: // - _closedTabs // - extData // - isPopup // - hidden // Assign a unique ID to correlate the window to be opened with the // remaining data window.__lastSessionWindowID = pinnedWindowState.__lastSessionWindowID = "" + Date.now() + Math.random(); // Extract the cookies that belong with each pinned tab this._splitCookiesFromWindow(window, pinnedWindowState); // Actually add this window to our defaultState defaultState.windows.push(pinnedWindowState); // Remove the window from the state if it doesn't have any tabs if (!window.tabs.length) { if (wIndex + 1 <= state.selectedWindow) state.selectedWindow -= 1; else if (wIndex + 1 == state.selectedWindow) defaultState.selectedIndex = defaultState.windows.length + 1; state.windows.splice(wIndex, 1); // We don't want to increment wIndex here. continue; } } wIndex++; } return [defaultState, state]; }, /** * Splits out the cookies from aWinState into aTargetWinState based on the * tabs that are in aTargetWinState. * This alters the state of aWinState and aTargetWinState. */ _splitCookiesFromWindow: function(aWinState, aTargetWinState) { if (!aWinState.cookies || !aWinState.cookies.length) return; // Get the hosts for history entries in aTargetWinState let cookieHosts = {}; aTargetWinState.tabs.forEach(function(tab) { tab.entries.forEach(function(entry) { this._extractHostsForCookiesFromEntry(entry, cookieHosts, false); }, this); }, this); // By creating a regex we reduce overhead and there is only one loop pass // through either array (cookieHosts and aWinState.cookies). let hosts = Object.keys(cookieHosts).join("|").replace("\\.", "\\.", "g"); // If we don't actually have any hosts, then we don't want to do anything. if (!hosts.length) return; let cookieRegex = new RegExp(".*(" + hosts + ")"); for (let cIndex = 0; cIndex < aWinState.cookies.length;) { if (cookieRegex.test(aWinState.cookies[cIndex].host)) { aTargetWinState.cookies = aTargetWinState.cookies.concat(aWinState.cookies.splice(cIndex, 1)); continue; } cIndex++; } }, /** * Converts a JavaScript object into a JSON string * (see http://www.json.org/ for more information). * * The inverse operation consists of JSON.parse(JSON_string). * * @param aJSObject is the object to be converted * @returns the object's JSON representation */ _toJSONString: function(aJSObject) { return JSON.stringify(aJSObject); }, _sendRestoreCompletedNotifications: function() { // not all windows restored, yet if (this._restoreCount > 1) { this._restoreCount--; return; } // observers were already notified if (this._restoreCount == -1) return; // This was the last window restored at startup, notify observers. Services.obs.notifyObservers(null, this._browserSetState ? NOTIFY_BROWSER_STATE_RESTORED : NOTIFY_WINDOWS_RESTORED, ""); this._browserSetState = false; this._restoreCount = -1; }, /** * Set the given window's busy state * @param aWindow the window * @param aValue the window's busy state */ _setWindowStateBusyValue: function(aWindow, aValue) { this._windows[aWindow.__SSi].busy = aValue; // Keep the to-be-restored state in sync because that is returned by // getWindowState() as long as the window isn't loaded, yet. if (!this._isWindowLoaded(aWindow)) { let stateToRestore = this._statesToRestore[aWindow.__SS_restoreID].windows[0]; stateToRestore.busy = aValue; } }, /** * Set the given window's state to 'not busy'. * @param aWindow the window */ _setWindowStateReady: function(aWindow) { this._setWindowStateBusyValue(aWindow, false); this._sendWindowStateEvent(aWindow, "Ready"); }, /** * Set the given window's state to 'busy'. * @param aWindow the window */ _setWindowStateBusy: function(aWindow) { this._setWindowStateBusyValue(aWindow, true); this._sendWindowStateEvent(aWindow, "Busy"); }, /** * Dispatch an SSWindowState_____ event for the given window. * @param aWindow the window * @param aType the type of event, SSWindowState will be prepended to this string */ _sendWindowStateEvent: function(aWindow, aType) { let event = aWindow.document.createEvent("Events"); event.initEvent("SSWindowState" + aType, true, false); aWindow.dispatchEvent(event); }, /** * Dispatch the SSTabRestored event for the given tab. * @param aTab the which has been restored */ _sendTabRestoredNotification: function(aTab) { let event = aTab.ownerDocument.createEvent("Events"); event.initEvent("SSTabRestored", true, false); aTab.dispatchEvent(event); }, /** * @param aWindow * Window reference * @returns whether this window's data is still cached in _statesToRestore * because it's not fully loaded yet */ _isWindowLoaded: function(aWindow) { return !aWindow.__SS_restoreID; }, /** * Replace "Loading..." with the tab label (with minimal side-effects) * @param aString is the string the title is stored in * @param aTabbrowser is a tabbrowser object, containing aTab * @param aTab is the tab whose title we're updating & using * * @returns aString that has been updated with the new title */ _replaceLoadingTitle : function(aString, aTabbrowser, aTab) { if (aString == aTabbrowser.mStringBundle.getString("tabs.connecting")) { aTabbrowser.setTabTitle(aTab); [aString, aTab.label] = [aTab.label, aString]; } return aString; }, /** * Resize this._closedWindows to the value of the pref, except in the case * where we don't have any non-popup windows on Windows and Linux. Then we must * resize such that we have at least one non-popup window. */ _capClosedWindows : function() { if (this._closedWindows.length <= this._max_windows_undo) return; let spliceTo = this._max_windows_undo; #ifndef XP_MACOSX let normalWindowIndex = 0; // try to find a non-popup window in this._closedWindows while (normalWindowIndex < this._closedWindows.length && !!this._closedWindows[normalWindowIndex].isPopup) normalWindowIndex++; if (normalWindowIndex >= this._max_windows_undo) spliceTo = normalWindowIndex + 1; #endif this._closedWindows.splice(spliceTo, this._closedWindows.length); }, _clearRestoringWindows: function() { for (let i = 0; i < this._closedWindows.length; i++) { delete this._closedWindows[i]._shouldRestore; } }, /** * Reset state to prepare for a new session state to be restored. */ _resetRestoringState: function() { TabRestoreQueue.reset(); this._tabsRestoringCount = 0; }, /** * Reset the restoring state for a particular tab. This will be called when * removing a tab or when a tab needs to be reset (it's being overwritten). * * @param aTab * The tab that will be "reset" */ _resetTabRestoringState: function(aTab) { let window = aTab.ownerDocument.defaultView; let browser = aTab.linkedBrowser; // Keep the tab's previous state for later in this method let previousState = browser.__SS_restoreState; // The browser is no longer in any sort of restoring state. delete browser.__SS_restoreState; aTab.removeAttribute("pending"); browser.removeAttribute("pending"); // We want to decrement window.__SS_tabsToRestore here so that we always // decrement it AFTER a tab is done restoring or when a tab gets "reset". window.__SS_tabsToRestore--; // Remove the progress listener if we should. this._removeTabsProgressListener(window); if (previousState == TAB_STATE_RESTORING) { if (this._tabsRestoringCount) this._tabsRestoringCount--; } else if (previousState == TAB_STATE_NEEDS_RESTORE) { // Make sure the session history listener is removed. This is normally // done in restoreTab, but this tab is being removed before that gets called. this._removeSHistoryListener(aTab); // Make sure that the tab is removed from the list of tabs to restore. // Again, this is normally done in restoreTab, but that isn't being called // for this tab. TabRestoreQueue.remove(aTab); } }, /** * Add the tabs progress listener to the window if it isn't already * * @param aWindow * The window to add our progress listener to */ _ensureTabsProgressListener: function(aWindow) { let tabbrowser = aWindow.gBrowser; if (tabbrowser.mTabsProgressListeners.indexOf(gRestoreTabsProgressListener) == -1) tabbrowser.addTabsProgressListener(gRestoreTabsProgressListener); }, /** * Attempt to remove the tabs progress listener from the window. * * @param aWindow * The window from which to remove our progress listener from */ _removeTabsProgressListener: function(aWindow) { // If there are no tabs left to restore (or restoring) in this window, then // we can safely remove the progress listener from this window. if (!aWindow.__SS_tabsToRestore) aWindow.gBrowser.removeTabsProgressListener(gRestoreTabsProgressListener); }, /** * Remove the session history listener from the tab's browser if there is one. * * @param aTab * The tab who's browser to remove the listener */ _removeSHistoryListener: function(aTab) { let browser = aTab.linkedBrowser; if (browser.__SS_shistoryListener) { browser.webNavigation.sessionHistory. removeSHistoryListener(browser.__SS_shistoryListener); delete browser.__SS_shistoryListener; } } }; /** * Priority queue that keeps track of a list of tabs to restore and returns * the tab we should restore next, based on priority rules. We decide between * pinned, visible and hidden tabs in that and FIFO order. Hidden tabs are only * restored with restore_hidden_tabs=true. */ var TabRestoreQueue = { // The separate buckets used to store tabs. tabs: {priority: [], visible: [], hidden: []}, // Preferences used by the TabRestoreQueue to determine which tabs // are restored automatically and which tabs will be on-demand. prefs: { // Lazy getter that returns whether tabs are restored on demand. get restoreOnDemand() { let updateValue = () => { let value = Services.prefs.getBoolPref(PREF); let definition = {value: value, configurable: true}; Object.defineProperty(this, "restoreOnDemand", definition); return value; } const PREF = "browser.sessionstore.restore_on_demand"; Services.prefs.addObserver(PREF, updateValue, false); return updateValue(); }, // Lazy getter that returns whether pinned tabs are restored on demand. get restorePinnedTabsOnDemand() { let updateValue = () => { let value = Services.prefs.getBoolPref(PREF); let definition = {value: value, configurable: true}; Object.defineProperty(this, "restorePinnedTabsOnDemand", definition); return value; } const PREF = "browser.sessionstore.restore_pinned_tabs_on_demand"; Services.prefs.addObserver(PREF, updateValue, false); return updateValue(); }, // Lazy getter that returns whether we should restore hidden tabs. get restoreHiddenTabs() { let updateValue = () => { let value = Services.prefs.getBoolPref(PREF); let definition = {value: value, configurable: true}; Object.defineProperty(this, "restoreHiddenTabs", definition); return value; } const PREF = "browser.sessionstore.restore_hidden_tabs"; Services.prefs.addObserver(PREF, updateValue, false); return updateValue(); } }, // Resets the queue and removes all tabs. reset: function() { this.tabs = {priority: [], visible: [], hidden: []}; }, // Adds a tab to the queue and determines its priority bucket. add: function(tab) { let {priority, hidden, visible} = this.tabs; if (tab.pinned) { priority.push(tab); } else if (tab.hidden) { hidden.push(tab); } else { visible.push(tab); } }, // Removes a given tab from the queue, if it's in there. remove: function(tab) { let {priority, hidden, visible} = this.tabs; // We'll always check priority first since we don't // have an indicator if a tab will be there or not. let set = priority; let index = set.indexOf(tab); if (index == -1) { set = tab.hidden ? hidden : visible; index = set.indexOf(tab); } if (index > -1) { set.splice(index, 1); } }, // Returns and removes the tab with the highest priority. shift: function() { let set; let {priority, hidden, visible} = this.tabs; let {restoreOnDemand, restorePinnedTabsOnDemand} = this.prefs; let restorePinned = !(restoreOnDemand && restorePinnedTabsOnDemand); if (restorePinned && priority.length) { set = priority; } else if (!restoreOnDemand) { if (visible.length) { set = visible; } else if (this.prefs.restoreHiddenTabs && hidden.length) { set = hidden; } } return set && set.shift(); }, // Moves a given tab from the 'hidden' to the 'visible' bucket. hiddenToVisible: function(tab) { let {hidden, visible} = this.tabs; let index = hidden.indexOf(tab); if (index > -1) { hidden.splice(index, 1); visible.push(tab); } else { throw new Error("restore queue: hidden tab not found"); } }, // Moves a given tab from the 'visible' to the 'hidden' bucket. visibleToHidden: function(tab) { let {visible, hidden} = this.tabs; let index = visible.indexOf(tab); if (index > -1) { visible.splice(index, 1); hidden.push(tab); } else { throw new Error("restore queue: visible tab not found"); } } }; // A map storing a closed window's state data until it goes aways (is GC'ed). // This ensures that API clients can still read (but not write) states of // windows they still hold a reference to but we don't. var DyingWindowCache = { _data: new WeakMap(), has: function(window) { return this._data.has(window); }, get: function(window) { return this._data.get(window); }, set: function(window, data) { this._data.set(window, data); }, remove: function(window) { this._data.delete(window); } }; // A set of tab attributes to persist. We will read a given list of tab // attributes when collecting tab data and will re-set those attributes when // the given tab data is restored to a new tab. var TabAttributes = { _attrs: new Set(), // We never want to directly read or write those attributes. // 'image' should not be accessed directly but handled by using the // gBrowser.getIcon()/setIcon() methods. // 'pending' is used internal by sessionstore and managed accordingly. // 'skipbackgroundnotify' is used internal by tabbrowser.xml. _skipAttrs: new Set(["image", "pending", "skipbackgroundnotify"]), persist: function(name) { if (this._attrs.has(name) || this._skipAttrs.has(name)) { return false; } this._attrs.add(name); return true; }, get: function(tab) { let data = {}; for (let name of this._attrs) { if (tab.hasAttribute(name)) { data[name] = tab.getAttribute(name); } } return data; }, set: function(tab, data = {}) { // Clear attributes. for (let name of this._attrs) { tab.removeAttribute(name); } // Set attributes. for (let name in data) { tab.setAttribute(name, data[name]); } } }; // This is used to help meter the number of restoring tabs. This is the control // point for telling the next tab to restore. It gets attached to each gBrowser // via gBrowser.addTabsProgressListener var gRestoreTabsProgressListener = { onStateChange: function(aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) { // Ignore state changes on browsers that we've already restored and state // changes that aren't applicable. if (aBrowser.__SS_restoreState && aBrowser.__SS_restoreState == TAB_STATE_RESTORING && aStateFlags & Ci.nsIWebProgressListener.STATE_STOP && aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK && aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) { // We need to reset the tab before starting the next restore. let win = aBrowser.ownerDocument.defaultView; let tab = win.gBrowser.getTabForBrowser(aBrowser); SessionStoreInternal._resetTabRestoringState(tab); SessionStoreInternal.restoreNextTab(); } } }; // A SessionStoreSHistoryListener will be attached to each browser before it is // restored. We need to catch reloads that occur before the tab is restored // because otherwise, docShell will reload an old URI (usually about:blank). function SessionStoreSHistoryListener(aTab) { this.tab = aTab; } SessionStoreSHistoryListener.prototype = { QueryInterface: XPCOMUtils.generateQI([ Ci.nsISHistoryListener, Ci.nsISupportsWeakReference ]), browser: null, OnHistoryNewEntry: function(aNewURI) { }, OnHistoryGoBack: function(aBackURI) { return true; }, OnHistoryGoForward: function(aForwardURI) { return true; }, OnHistoryGotoIndex: function(aIndex, aGotoURI) { return true; }, OnHistoryPurge: function(aNumEntries) { return true; }, OnHistoryReload: function(aReloadURI, aReloadFlags) { // On reload, we want to make sure that session history loads the right // URI. In order to do that, we will juet call restoreTab. That will remove // the history listener and load the right URI. SessionStoreInternal.restoreTab(this.tab); // Returning false will stop the load that docshell is attempting. return false; } } // See toolkit/forgetaboutsite/ForgetAboutSite.jsm String.prototype.hasRootDomain = function hasRootDomain(aDomain) { let index = this.indexOf(aDomain); if (index == -1) return false; if (this == aDomain) return true; let prevChar = this[index - 1]; return (index == (this.length - aDomain.length)) && (prevChar == "." || prevChar == "/"); }