diff options
Diffstat (limited to 'components/sessionstore/SessionStore.jsm')
-rw-r--r-- | components/sessionstore/SessionStore.jsm | 4786 |
1 files changed, 0 insertions, 4786 deletions
diff --git a/components/sessionstore/SessionStore.jsm b/components/sessionstore/SessionStore.jsm deleted file mode 100644 index e19a578..0000000 --- a/components/sessionstore/SessionStore.jsm +++ /dev/null @@ -1,4786 +0,0 @@ -/* 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 == "/"); -} |