diff options
Diffstat (limited to 'components/sessionstore/SessionStore.jsm')
-rw-r--r-- | components/sessionstore/SessionStore.jsm | 4786 |
1 files changed, 4786 insertions, 0 deletions
diff --git a/components/sessionstore/SessionStore.jsm b/components/sessionstore/SessionStore.jsm new file mode 100644 index 0000000..e19a578 --- /dev/null +++ b/components/sessionstore/SessionStore.jsm @@ -0,0 +1,4786 @@ +/* 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 HUDService_getter() { + 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 ss_init(aWindow) { + return SessionStoreInternal.init(aWindow); + }, + + getBrowserState: function ss_getBrowserState() { + return SessionStoreInternal.getBrowserState(); + }, + + setBrowserState: function ss_setBrowserState(aState) { + SessionStoreInternal.setBrowserState(aState); + }, + + getWindowState: function ss_getWindowState(aWindow) { + return SessionStoreInternal.getWindowState(aWindow); + }, + + setWindowState: function ss_setWindowState(aWindow, aState, aOverwrite) { + SessionStoreInternal.setWindowState(aWindow, aState, aOverwrite); + }, + + getTabState: function ss_getTabState(aTab) { + return SessionStoreInternal.getTabState(aTab); + }, + + setTabState: function ss_setTabState(aTab, aState) { + SessionStoreInternal.setTabState(aTab, aState); + }, + + duplicateTab: function ss_duplicateTab(aWindow, aTab, aDelta) { + return SessionStoreInternal.duplicateTab(aWindow, aTab, aDelta); + }, + + getClosedTabCount: function ss_getClosedTabCount(aWindow) { + return SessionStoreInternal.getClosedTabCount(aWindow); + }, + + getClosedTabData: function ss_getClosedTabDataAt(aWindow) { + return SessionStoreInternal.getClosedTabData(aWindow); + }, + + undoCloseTab: function ss_undoCloseTab(aWindow, aIndex) { + return SessionStoreInternal.undoCloseTab(aWindow, aIndex); + }, + + forgetClosedTab: function ss_forgetClosedTab(aWindow, aIndex) { + return SessionStoreInternal.forgetClosedTab(aWindow, aIndex); + }, + + getClosedWindowCount: function ss_getClosedWindowCount() { + return SessionStoreInternal.getClosedWindowCount(); + }, + + getClosedWindowData: function ss_getClosedWindowData() { + return SessionStoreInternal.getClosedWindowData(); + }, + + undoCloseWindow: function ss_undoCloseWindow(aIndex) { + return SessionStoreInternal.undoCloseWindow(aIndex); + }, + + forgetClosedWindow: function ss_forgetClosedWindow(aIndex) { + return SessionStoreInternal.forgetClosedWindow(aIndex); + }, + + getWindowValue: function ss_getWindowValue(aWindow, aKey) { + return SessionStoreInternal.getWindowValue(aWindow, aKey); + }, + + setWindowValue: function ss_setWindowValue(aWindow, aKey, aStringValue) { + SessionStoreInternal.setWindowValue(aWindow, aKey, aStringValue); + }, + + deleteWindowValue: function ss_deleteWindowValue(aWindow, aKey) { + SessionStoreInternal.deleteWindowValue(aWindow, aKey); + }, + + getTabValue: function ss_getTabValue(aTab, aKey) { + return SessionStoreInternal.getTabValue(aTab, aKey); + }, + + setTabValue: function ss_setTabValue(aTab, aKey, aStringValue) { + SessionStoreInternal.setTabValue(aTab, aKey, aStringValue); + }, + + deleteTabValue: function ss_deleteTabValue(aTab, aKey) { + SessionStoreInternal.deleteTabValue(aTab, aKey); + }, + + persistTabAttribute: function ss_persistTabAttribute(aName) { + SessionStoreInternal.persistTabAttribute(aName); + }, + + restoreLastSession: function ss_restoreLastSession() { + SessionStoreInternal.restoreLastSession(); + }, + + checkPrivacyLevel: function ss_checkPrivacyLevel(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 ssi_initService() { + 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 ssi_initSession() { + 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 ssi_initEncoding() { + // 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 ssi_initWindow(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 ssi_init(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 ssi_uninit() { + // 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 ssi_observe(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 ssi_receiveMessage(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 ssi_handleEvent(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 ssi_onLoad(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 ssi_onOpen(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 ssi_onClose(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 ssi_onQuitApplicationRequested() { + // 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 ssi_onQuitApplicationGranted() { + // freeze the data at what we've got (ignoring closing windows) + this._loadState = STATE_QUITTING; + }, + + /** + * On last browser window close + */ + onLastWindowCloseGranted: function ssi_onLastWindowCloseGranted() { + // 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 ssi_onQuitApplication(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 ssi_onPurgeSessionHistory() { + 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 ssi_onPurgeDomainData(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 ssi_onPrefChange(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 ssi_onTimerCallback() { + 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 ssi_onTabAdd(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 ssi_onTabRemove(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 ssi_onTabClose(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 ssi_onTabLoad(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 ssi_onTabInput(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 ssi_onTabSelect(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 ssi_onTabShow(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 ssi_onTabHide(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 ssi_getBrowserState() { + return this._toJSONString(this._getCurrentState()); + }, + + setBrowserState: function ssi_setBrowserState(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 ssi_getWindowState(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 ssi_setWindowState(aWindow, aState, aOverwrite) { + if (!aWindow.__SSi) + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + + this.restoreWindow(aWindow, aState, aOverwrite); + }, + + getTabState: function ssi_getTabState(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 ssi_setTabState(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 ssi_duplicateTab(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 ssi_getClosedTabCount(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 ssi_getClosedTabDataAt(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 ssi_undoCloseTab(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 ssi_forgetClosedTab(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 ssi_getClosedWindowCount() { + return this._closedWindows.length; + }, + + getClosedWindowData: function ssi_getClosedWindowData() { + return this._toJSONString(this._closedWindows); + }, + + undoCloseWindow: function ssi_undoCloseWindow(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 ssi_forgetClosedWindow(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 ssi_getWindowValue(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 ssi_setWindowValue(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 ssi_deleteWindowValue(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 ssi_getTabValue(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 ssi_setTabValue(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 ssi_deleteTabValue(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 ssi_persistTabAttribute(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 ssi_restoreLastSession() { + // 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 ssi_prepWindowToRestoreInto(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 ssi_saveWindowHistory(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 ssi_collectTabData(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 ssi_serializeHistoryEntry(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 ssi_updateTextAndScrollData(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 ssi_updateTextAndScrollDataForTab(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 ssi_updateTextAndScrollDataForFrame(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 ssi_getSelectedPageStyle(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 ssi_extractHostsForCookiesFromEntry(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 ssi_extractHostsForCookiesFromHostScheme(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 ssi_updateCookieHosts(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 ssi_updateCookies(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 ssi_updateWindowFeatures(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 ssi_getCurrentState(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 ssi_getWindowState(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 ssi_collectWindowData(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 ssi_restoreWindow(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 ssi__setTabsRestoringOrder( + 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 ssi_restoreHistoryPrecursor(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 ssi_restoreHistory(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 ssi_restoreTab(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. + 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 ssi_restoreNextTab() { + // 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 ssi_deserializeHistoryEntry(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 ssi_restoreDocument(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 ssi_restoreWindowFeatures(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 ssi_restoreDimensions(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 ssi_restoreCookies(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 ssi_saveStateDelayed(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 ssi_saveState(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 ssi_saveStateObject(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 ssi_createSupportsString(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 ssi_forEachBrowserWindow(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 ssi_getMostRecentBrowserWindow() { + 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 ssi_handleClosedWindows() { + 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 ssi_openWindowWithState(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 ssi_doResumeSession() { + 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 ssi_isCmdLineEmpty(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 ssi_checkPrivacyLevel(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 ssi_getWindowDimension(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 ssi_getURIFromString(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 ssi_needsRestorePage(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 ssi_shouldSaveTabState(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 ssi_canRestoreTabHistory(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 ssi_prepDataForDeferredRestore(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 ssi_splitCookiesFromWindow(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 ssi_toJSONString(aJSObject) { + return JSON.stringify(aJSObject); + }, + + _sendRestoreCompletedNotifications: function ssi_sendRestoreCompletedNotifications() { + // 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 ssi_changeWindowStateBusyValue(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 ssi_setWindowStateReady(aWindow) { + this._setWindowStateBusyValue(aWindow, false); + this._sendWindowStateEvent(aWindow, "Ready"); + }, + + /** + * Set the given window's state to 'busy'. + * @param aWindow the window + */ + _setWindowStateBusy: function ssi_setWindowStateBusy(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 ssi_sendWindowStateEvent(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 ssi_sendTabRestoredNotification(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 ssi_isWindowLoaded(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 ssi_replaceLoadingTitle(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 ssi_capClosedWindows() { + 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 ssi_clearRestoringWindows() { + 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 ssi_initRestoringState() { + 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 ssi_resetTabRestoringState(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 ssi_ensureTabsProgressListener(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 ssi_removeTabsProgressListener(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 ssi_removeSHistoryListener(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 == "/"); +} |