/* 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/. */ /** * Session Storage and Restoration * * Overview * This service keeps track of a user's session, storing the various bits * required to return the browser to its current state. The relevant data is * stored in memory, and is periodically saved to disk in a file in the * profile directory. The service is started at first window load, in * delayedStartup, and will restore the session from the data received from * the nsSessionStartup service. */ /* :::::::: Constants and Helpers ::::::::::::::: */ 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"; // global notifications observed const OBSERVING = [ "domwindowclosed", "quit-application-requested", "quit-application-granted", "quit-application", "browser-lastwindow-close-granted", "browser:purge-session-history" ]; /* XUL Window properties to (re)store Restored in restoreDimensions() */ const WINDOW_ATTRIBUTES = { width: "outerWidth", height: "outerHeight", screenX: "screenX", screenY: "screenY", sizemode: "windowState" }; /* Hideable window features to (re)store Restored in restoreWindowFeatures() */ const WINDOW_HIDEABLE_FEATURES = [ "menubar", "toolbar", "locationbar", "personalbar", "statusbar", "scrollbars" ]; /* docShell capabilities to (re)store Restored in restoreHistory() eg: browser.docShell["allow" + aCapability] = false; XXX keep these in sync with all the attributes starting with "allow" in /docshell/base/nsIDocShell.idl */ const CAPABILITIES = [ "Subframes", "Plugins", "Javascript", "MetaRedirects", "Images", "DNSPrefetch", "Auth", "WindowControl" ]; // These are tab events that we listen to. const TAB_EVENTS = ["TabOpen", "TabClose", "TabSelect", "TabShow", "TabHide"]; var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); var {NetUtil} = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); XPCOMUtils.defineLazyServiceGetter(this, "SecMan", "@mozilla.org/scriptsecuritymanager;1", "nsIScriptSecurityManager"); XPCOMUtils.defineLazyServiceGetter(this, "gScreenManager", "@mozilla.org/gfx/screenmanager;1", "nsIScreenManager"); XPCOMUtils.defineLazyServiceGetter(this, "uuidGenerator", "@mozilla.org/uuid-generator;1", "nsIUUIDGenerator"); ChromeUtils.defineModuleGetter(this, "AppConstants", "resource://gre/modules/AppConstants.jsm"); ChromeUtils.defineModuleGetter(this, "Utils", "resource://gre/modules/sessionstore/Utils.jsm"); ChromeUtils.defineModuleGetter(this, "XPathGenerator", "resource:///modules/sessionstore/XPathGenerator.jsm"); function debug(aMsg) { Services.console.logStringMessage("SessionStore: " + aMsg); } /* :::::::: The Service ::::::::::::::: */ function SessionStoreService() { 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"); }); } SessionStoreService.prototype = { classID: Components.ID("{d37ccdf1-496f-4135-9575-037180af010d}"), QueryInterface: XPCOMUtils.generateQI([Ci.nsISessionStore, Ci.nsIDOMEventListener, Ci.nsIObserver, Ci.nsISupportsWeakReference]), // xul:tab attributes to (re)store (extensions might want to hook in here); // the favicon is always saved for the about:sessionrestore page xulAttributes: {"image": true}, // 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: {}, // states for all recently closed windows _closedWindows: [], // 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, // tabs to restore in order _tabsToRestore: { visible: [], hidden: [] }, _tabsRestoringCount: 0, // number of tabs to restore concurrently, pref controlled. _maxConcurrentTabRestores: null, // 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, // Whether we've been initialized _initialized: false, // Mapping from legacy docshellIDs to docshellUUIDs. _docshellUUIDMap: new Map(), /* ........ Public Getters .............. */ get canRestoreLastSession() { // Always disallow restoring the previous session when in private browsing return this._lastSessionState; }, set canRestoreLastSession(val) { // Cheat a bit; only allow false. if (!val) this._lastSessionState = null; }, /* ........ Global Event Handlers .............. */ /** * Initialize the component */ initService: function() { OBSERVING.forEach(function(aTopic) { Services.obs.addObserver(this, aTopic, true); }, this); 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); // this pref is only read at startup, so no need to observe it this._sessionhistory_max_entries = this._prefBranch.getIntPref("sessionhistory.max_entries"); this._maxConcurrentTabRestores = this._prefBranch.getIntPref("sessionstore.max_concurrent_tabs"); this._prefBranch.addObserver("sessionstore.max_concurrent_tabs", this, true); // Make sure gRestoreTabsProgressListener has a reference to sessionstore // so that it can make calls back in gRestoreTabsProgressListener.ss = this; // get file references this._sessionFile = Services.dirsvc.get("ProfD", Ci.nsIFile); this._sessionFileBackup = this._sessionFile.clone(); this._sessionFile.append("sessionstore.json"); this._sessionFileBackup.append("sessionstore.bak"); // get string containing session state var ss = Cc["@mozilla.org/suite/sessionstartup;1"] .getService(Ci.nsISessionStartup); try { if (ss.sessionType != Ci.nsISessionStartup.NO_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", triggeringPrincipal_base64: Utils.SERIALIZED_SYSTEMPRINCIPAL, formdata: { "#sessionData": JSON.stringify(this._initialState) } }; 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; // 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); } } if (this._resume_from_crash) { // create a backup if the session data file exists try { if (this._sessionFileBackup.exists()) this._sessionFileBackup.remove(false); if (this._sessionFile.exists()) this._sessionFile.copyTo(null, this._sessionFileBackup.leafName); } catch (ex) { Cu.reportError(ex); } // file was write-locked? } // 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._initialized = true; }, /** * Start tracking a window. * This function also initializes the component if it's not already * initialized. */ init: function sss_init(aWindow) { // Initialize the service if needed. if (!this._initialized) this.initService(); 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; } }, /** * Called on application shutdown, after notifications: * quit-application-granted, quit-application */ _uninit: function sss_uninit() { // save all data for session resuming this.saveState(true); // clear out _tabsToRestore in case it's still holding refs this._tabsToRestore.visible = null; this._tabsToRestore.hidden = null; // remove the ref to us from the progress listener gRestoreTabsProgressListener.ss = null; // Make sure to break our cycle with the save timer if (this._saveTimer) { this._saveTimer.cancel(); this._saveTimer = null; } }, /** * Handle notifications */ observe: function sss_observe(aSubject, aTopic, aData) { switch (aTopic) { case "domwindowclosed": // catch closed windows this.onClose(aSubject); break; case "quit-application-requested": // get a current snapshot of all windows this._forEachBrowserWindow(function(aWindow) { this._collectWindowData(aWindow); }); DirtyWindows.clear(); break; case "quit-application-granted": // freeze the data at what we've got (ignoring closing windows) this._loadState = STATE_QUITTING; break; case "browser-lastwindow-close-granted": // last browser window is quitting. // remember to restore the last window when another browser window is openend // 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; break; case "quit-application": if (aData == "restart" && !this._isSwitchingProfile()) { 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"); } if (aData != "restart") { // Throw away the previous session on shutdown this._lastSessionState = null; } this._loadState = STATE_QUITTING; // just to be sure this._uninit(); break; case "browser:purge-session-history": // catch sanitization this._clearDisk(); // 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) { //Hide "Restore Last Session" menu item let restoreItem = aWindow.document.getElementById("historyRestoreLastSession"); restoreItem.setAttribute("disabled", "true"); Array.from(aWindow.getBrowser().tabs).forEach(function(aTab) { delete aTab.linkedBrowser.__SS_data; delete aTab.linkedBrowser.__SS_formDataSaved; if (aTab.linkedBrowser.__SS_restoreState) this._resetTabRestoringState(aTab); }); 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]; } } // also clear all data about closed windows this._closedWindows = []; // give the tabbrowsers a chance to clear their histories first if (this._getMostRecentBrowserWindow()) Services.tm.mainThread.dispatch(this.saveState.bind(this, true), Ci.nsIThread.DISPATCH_NORMAL); else if (this._loadState == STATE_RUNNING) this.saveState(true); break; case "nsPref:changed": // catch pref changes 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"); // 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) this.saveState(true); else if (this._loadState == STATE_RUNNING) this._clearDisk(); break; case "sessionstore.max_concurrent_tabs": this._maxConcurrentTabRestores = this._prefBranch.getIntPref("sessionstore.max_concurrent_tabs"); break; } break; case "timer-callback": // timer call back for delayed saving this._saveTimer = null; this.saveState(); break; } }, /* ........ Window Event Handlers .............. */ /** * Implement nsIDOMEventListener for handling various window and tab events */ handleEvent: function sss_handleEvent(aEvent) { 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. if (aEvent.currentTarget.__SS_restore_data) this.restoreDocument(win, aEvent.currentTarget, aEvent); // We still need to call onTabLoad, so fall through to "pageshow" case. case "pageshow": this.onTabLoad(win, aEvent.currentTarget, aEvent); break; case "input": case "DOMAutoComplete": this.onTabInput(win, aEvent.currentTarget); 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(aEvent.originalTarget); break; case "TabHide": this.onTabHide(aEvent.originalTarget); break; } }, /** * 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 sss_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: [] }; 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) { // 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)); 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. this.windowToFocus = aWindow; Services.tm.mainThread.dispatch(this, Ci.nsIThread.DISPATCH_NORMAL); // 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); } else if (this._restoreLastWindow && aWindow.toolbar.visible && this._closedWindows.length) { // 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; if (AppConstants.platform == "macosx" || !this._doResumeSession()) { // 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]; } } 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; } if (newWindowState) { // Ensure that the window state isn't hidden this._restoreCount = 1; let state = { windows: [newWindowState] }; this.restoreWindow(aWindow, state, this._isCmdLineEmpty(aWindow)); } } // 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.getBrowser(); // 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 close... * - remove event listeners from tabs * - save all window data * @param aWindow * Window reference */ onClose: function sss_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; } if (this.windowToFocus && this.windowToFocus == aWindow) { delete this.windowToFocus; } var tabbrowser = aWindow.getBrowser(); TAB_EVENTS.forEach(function(aEvent) { tabbrowser.tabContainer.removeEventListener(aEvent, this, true); }, this); // remove the progress listener for this window try { tabbrowser.removeTabsProgressListener(gRestoreTabsProgressListener); } catch (ex) {}; 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); this._updateCookies([winData]); } // save the window if it has multiple tabs or a single saveable tab if (winData.tabs.length > 1 || (winData.tabs.length == 1 && this._shouldSaveTabState(winData.tabs[0]))) { this._closedWindows.unshift(winData); this._capClosedWindows(); } // clear this window from the list delete this._windows[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; }, /** * 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 sss_onTabAdd(aWindow, aTab, aNoNotification) { let browser = aTab.linkedBrowser; browser.addEventListener("load", this, true); browser.addEventListener("pageshow", this, true); browser.addEventListener("input", this, true); browser.addEventListener("DOMAutoComplete", this, true); if (!aNoNotification) { this.saveStateDelayed(aWindow); } this._updateCrashReportURL(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 sss_onTabRemove(aWindow, aTab, aNoNotification) { let browser = aTab.linkedBrowser; browser.removeEventListener("load", this, true); browser.removeEventListener("pageshow", this, true); browser.removeEventListener("change", this, true); browser.removeEventListener("input", this, true); browser.removeEventListener("DOMAutoComplete", this, true); delete browser.__SS_data; // 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 sss_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)) { aTab.tabData = { state: tabState }; var closedTabs = this._windows[aWindow.__SSi]._closedTabs; closedTabs.unshift(aTab.tabData); if (closedTabs.length > this._max_tabs_undo) closedTabs.length = this._max_tabs_undo; }; }, /** * When a tab loads, save state. * @param aWindow * Window reference * @param aBrowser * Browser reference * @param aEvent * Event obj */ onTabLoad: function sss_onTabLoad(aWindow, aBrowser, aEvent) { // 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 ((aEvent.type != "load" && !aEvent.persisted) || (aBrowser.__SS_restoreState && aBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE)) { return; } delete aBrowser.__SS_data; this.saveStateDelayed(aWindow); // attempt to update the current URL we send in a crash report this._updateCrashReportURL(aWindow); }, /** * Called when a browser sends the "input" notification * @param aWindow * Window reference * @param aBrowser * Browser reference */ onTabInput: function sss_onTabInput(aWindow, aBrowser) { this.saveStateDelayed(aWindow, 3000); }, /** * When a tab is selected, save session data * @param aWindow * Window reference */ onTabSelect: function sss_onTabSelect(aWindow) { if (this._loadState == STATE_RUNNING) { this._windows[aWindow.__SSi].selected = aWindow.getBrowser().tabContainer.selectedIndex; let tab = aWindow.getBrowser().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); // attempt to update the current URL we send in a crash report this._updateCrashReportURL(aWindow); } }, onTabShow: function sss_onTabShow(aTab) { // If the tab hasn't been restored yet, move it into the right _tabsToRestore bucket if (aTab.linkedBrowser.__SS_restoreState && aTab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) { this._tabsToRestore.hidden.splice(this._tabsToRestore.hidden.indexOf(aTab), 1); // Just put it at the end of the list of visible tabs; this._tabsToRestore.visible.push(aTab); } }, onTabHide: function sss_onTabHide(aTab) { // If the tab hasn't been restored yet, move it into the right _tabsToRestore bucket if (aTab.linkedBrowser.__SS_restoreState && aTab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) { this._tabsToRestore.visible.splice(this._tabsToRestore.visible.indexOf(aTab), 1); // Just put it at the end of the list of hidden tabs; this._tabsToRestore.hidden.push(aTab); } }, /* ........ nsISessionStore API .............. */ getBrowserState: function sss_getBrowserState() { return this._toJSONString(this._getCurrentState()); }, setBrowserState: function sss_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 _tabsToRestore 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 sss_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 sss_setWindowState(aWindow, aState, aOverwrite) { if (!aWindow.__SSi) throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); this.restoreWindow(aWindow, aState, aOverwrite); }, getTabState: function sss_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 sss_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._sendWindowStateEvent(window, "Busy"); this.restoreHistoryPrecursor(window, [aTab], [tabState], 0, 0, 0); }, duplicateTab: function sss_duplicateTab(aWindow, aTab, aDelta, aRelated) { if (!aTab.ownerDocument || !aTab.ownerDocument.defaultView.__SSi || aWindow && !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)); if (aWindow) { this._sendWindowStateEvent(aWindow, "Busy"); var newTab = aWindow.getBrowser() .addTab(null, { relatedToCurrent: aRelated }); this.restoreHistoryPrecursor(aWindow, [newTab], [tabState], 0, 0, 0); return newTab; } var state = { windows: [{ tabs: [tabState] }] }; this.windowToFocus = this._openWindowWithState(state); return null; }, _getClosedTabs: function sss_getClosedTabs(aWindow) { if (!aWindow.__SSi) return this._toJSONString(aWindow.__SS_dyingCache._closedTabs); var closedTabs = this._windows[aWindow.__SSi]._closedTabs; closedTabs = closedTabs.concat(aWindow.getBrowser().savedBrowsers); closedTabs = closedTabs.filter(function(aTabData, aIndex, aArray) { return aArray.indexOf(aTabData) == aIndex; }); return closedTabs; }, getClosedTabCount: function sss_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 sss_getClosedTabData(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 sss_undoCloseTab(aWindow, aIndex) { if (!aWindow.__SSi) throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); var closedTabs = this._getClosedTabs(aWindow); 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[aIndex]; if (aIndex in this._windows[aWindow.__SSi]._closedTabs) this._windows[aWindow.__SSi]._closedTabs.splice(aIndex, 1); var tabbrowser = aWindow.getBrowser(); var index = tabbrowser.savedBrowsers.indexOf(closedTab); this._sendWindowStateEvent(aWindow, "Busy"); if (index != -1) // SeaMonkey has its own undoclosetab functionality return tabbrowser.restoreTab(index); // create a new tab var tab = tabbrowser.addTab(); // restore the tab's position tabbrowser.moveTabTo(tab, closedTab.pos); // restore tab content this.restoreHistoryPrecursor(aWindow, [tab], [closedTab.state], 1, 0, 0); // focus the tab's content area (bug 342432) tab.linkedBrowser.focus(); return tab; }, forgetClosedTab: function sss_forgetClosedTab(aWindow, aIndex) { if (!aWindow.__SSi) throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); var closedTabs = this._getClosedTabs(aWindow); if (!(aIndex in closedTabs)) throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); // remove closed tab from the array var closedTab = closedTabs[aIndex]; if (aIndex in this._windows[aWindow.__SSi]._closedTabs) this._windows[aWindow.__SSi]._closedTabs.splice(aIndex, 1); var tabbrowser = aWindow.getBrowser(); var index = tabbrowser.savedBrowsers.indexOf(closedTab); if (index != -1) tabbrowser.forgetSavedBrowser(aIndex); }, getClosedWindowCount: function sss_getClosedWindowCount() { return this._closedWindows.length; }, getClosedWindowData: function sss_getClosedWindowData() { return this._toJSONString(this._closedWindows); }, undoCloseWindow: function sss_undoCloseWindow(aIndex) { if (!(aIndex in this._closedWindows)) return null; // reopen the window let state = { windows: this._closedWindows.splice(aIndex, 1) }; let window = this._openWindowWithState(state); this.windowToFocus = window; return window; }, forgetClosedWindow: function sss_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 sss_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 sss_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 sss_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 sss_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 sss_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 sss_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 = null; 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 sss_persistTabAttribute(aName) { if (aName in this.xulAttributes) return; // this attribute is already being tracked this.xulAttributes[aName] = true; this.saveStateDelayed(); }, doRestoreLastWindow: function sss_doRestoreLastWindow() { let state = null; this._closedWindows.forEach(function(aWinState) { if (!state && !aWinState.isPopup) { state = aWinState; } }); return (this._restoreLastWindow && state && this._doResumeSession()); }, /** * 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 sss_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._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(); } // 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 sss__prepWindowToRestoreInto(aWindow) { if (!aWindow) 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; // Step 1 of processing: // Inspect extData for Panorama identifiers. If found, then we want to // inspect further. If there is a single group, then we can use this // window. If there are multiple groups then we won't use this window. let data = this.getWindowValue(aWindow, "tabview-group"); if (data) { data = JSON.parse(data); // Multiple keys means multiple groups, which means we don't want to use this window. if (Object.keys(data).length > 1) { return [false, false]; } else { // If there is only one group, then we want to ensure that its group id // is 0. This is how Panorama forces group merging when new tabs are opened. //XXXzpao This is a hack and the proper fix really belongs in Panorama. let groupKey = Object.keys(data)[0]; if (groupKey !== "0") { data["0"] = data[groupKey]; delete data[groupKey]; this.setWindowValue(aWindow, "tabview-groups", JSON.stringify(data)); } } } // Step 2 of processing: // If we're still here, then the window is usable. 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. let homePages = aWindow.getHomePage(); let removableTabs = []; let tabbrowser = aWindow.getBrowser(); let normalTabsLen = tabbrowser.tabs.length - tabbrowser._numPinnedTabs; for (let i = 0; i < tabbrowser.tabs.length; i++) { let tab = tabbrowser.tabs[i]; if (homePages.includes(tab.linkedBrowser.currentURI.spec)) { 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 sss_saveWindowHistory(aWindow) { var tabbrowser = aWindow.getBrowser(); 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 sss_collectTabData(aTab, aFullData) { var tabData = { entries: [] }; 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_restoreState == TAB_STATE_NEEDS_RESTORE) { // 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 // XXXzeniko anchor navigation doesn't reset __SS_data, so we could reuse // data even when we shouldn't (e.g. Back, different anchor) 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) { tabData = browser.__SS_data; tabData.index = history.index + 1; } else if (history && history.count > 0) { try { for (var j = 0; j < history.count; j++) { let entry = this._serializeHistoryEntry(history.getEntryAtIndex(j), aFullData, aTab.pinned); tabData.entries.push(entry); } // If we make it through the for loop, then we're ok and we should clear // any indicator of brokenness. delete aTab.__SS_broken_history; } 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. We'll track if // we've shown the assert for this tab so we only show it once. // cf. bug 669196. if (!aTab.__SS_broken_history) { // First Focus the window & tab we're having trouble with. aTab.ownerDocument.defaultView.focus(); aTab.ownerDocument.defaultView.getBrowser().selectedTab = aTab; debug("SessionStore failed gathering complete history " + "for the focused window/tab. See bug 669196."); aTab.__SS_broken_history = true; } } tabData.index = history.index + 1; // 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, triggeringPrincipal_base64: Utils.SERIALIZED_SYSTEMPRINCIPAL }; 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; tabData.userTypedClear = browser.userTypedClear; } else { delete tabData.userTypedValue; delete tabData.userTypedClear; } var disallow = []; for (var i = 0; i < CAPABILITIES.length; i++) if (!browser.docShell["allow" + CAPABILITIES[i]]) disallow.push(CAPABILITIES[i]); if (disallow.length > 0) tabData.disallow = disallow.join(","); else if (tabData.disallow) delete tabData.disallow; tabData.attributes = {}; for (let name in this.xulAttributes) { if (aTab.hasAttribute(name)) tabData.attributes[name] = aTab.getAttribute(name); } if (aTab.__SS_extdata) tabData.extData = aTab.__SS_extdata; else if (tabData.extData) delete tabData.extData; if (history && browser.docShell instanceof Ci.nsIDocShell) this._serializeSessionStorage(tabData, history, browser.docShell, aFullData, false); 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 * @returns object */ _serializeHistoryEntry: function sss_serializeHistoryEntry(aEntry, aFullData, aIsPinned) { var entry = { url: aEntry.URI.spec, triggeringPrincipal_base64: Utils.SERIALIZED_SYSTEMPRINCIPAL }; 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.docshellUUID = aEntry.docshellID.toString(); if (aEntry.referrerURI) entry.referrer = aEntry.referrerURI.spec; 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 // Collect triggeringPrincipal data for the current history entry. // Please note that before Bug 1297338 there was no concept of a // principalToInherit. To remain backward/forward compatible we // serialize the principalToInherit as triggeringPrincipal_b64. // Once principalToInherit is well established (within Gecko 55) // we can update this code, remove triggeringPrincipal_b64 and // just keep triggeringPrincipal_base64 as well as // principalToInherit_base64. if (aEntry.principalToInherit) { try { let principalToInherit = Utils.serializePrincipal(aEntry.principalToInherit); if (principalToInherit) { entry.triggeringPrincipal_b64 = principalToInherit; entry.principalToInherit_base64 = principalToInherit; } } catch (e) { debug(e); } } if (aEntry.triggeringPrincipal) { try { let triggeringPrincipal = Utils.serializePrincipal(aEntry.triggeringPrincipal); if (triggeringPrincipal) { entry.triggeringPrincipal_base64 = triggeringPrincipal; } } catch (e) { debug(e); } } entry.docIdentifier = aEntry.BFCacheEntry.ID; if (aEntry.stateData) { entry.structuredCloneState = aEntry.stateData.getDataAsBase64(); entry.structuredCloneVersion = aEntry.stateData.formatVersion; } if (!(aEntry instanceof Ci.nsISHContainer)) { return entry; } if (aEntry.childCount > 0) { entry.children = []; for (var i = 0; i < aEntry.childCount; i++) { var child = aEntry.GetChildAt(i); if (child) { entry.children.push(this._serializeHistoryEntry(child, aFullData, aIsPinned)); } else { // to maintain the correct frame order, insert a dummy entry entry.children.push({ url: "about:blank", triggeringPrincipal_base64: Utils.SERIALIZED_SYSTEMPRINCIPAL}); } // don't try to restore framesets containing wyciwyg URLs (cf. bug 424689 and bug 450595) if (/^wyciwyg:\/\//.test(entry.children[i].url)) { delete entry.children; break; } } } return entry; }, /** * Updates all sessionStorage "super cookies" * @param aTabData * The data object for a specific tab * @param aHistory * That tab's session history * @param aDocShell * That tab's docshell (containing the sessionStorage) * @param aFullData * always return privacy sensitive data (use with care) * @param aIsPinned * the tab is pinned and should be treated differently for privacy */ _serializeSessionStorage: function sss_serializeSessionStorage(aTabData, aHistory, aDocShell, aFullData, aIsPinned) { let storageData = {}; let hasContent = false; for (let i = 0; i < aHistory.count; i++) { let principal; try { let uri = aHistory.getEntryAtIndex(i).URI; principal = SecMan.getDocShellCodebasePrincipal(uri, aDocShell); } catch (ex) { // Chances are that this is getEntryAtIndex throwing, as seen in bug 669196. // We've already asserted in _collectTabData, so we won't show that again. continue; } // sessionStorage is saved per principal (cf. nsGlobalWindow::GetSessionStorage) let origin; try { origin = principal.origin; } catch (ex) { origin = principal.URI.spec; } if (storageData[origin]) continue; let isHTTPS = principal.URI && principal.URI.schemeIs("https"); if (!(aFullData || this._checkPrivacyLevel(isHTTPS, aIsPinned))) continue; let storage, storageItemCount = 0; let window = aDocShell.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindow); try { let storageManager = aDocShell.QueryInterface(Ci.nsIDOMStorageManager); storage = storageManager.getStorage(window, principal); // See Bug 1232955 - storage.length can throw, catch that failure here inside the try. if (storage) storageItemCount = storage.length; } catch (ex) { /* sessionStorage might throw if it's turned off, see bug 458954 */ } if (storageItemCount == 0) continue; let data = storageData[origin] = {}; for (let j = 0; j < storageItemCount; j++) { try { let key = storage.key(j); data[key] = storage.getItem(key); } catch (ex) { /* XXXzeniko this currently throws for secured items (cf. bug 442048) */ } } hasContent = true; } if (hasContent) aTabData.storage = storageData; }, /** * go through all tabs and store the current scroll positions * and innerHTML content of WYSIWYG editors * @param aWindow * Window reference */ _updateTextAndScrollData: function sss_updateTextAndScrollData(aWindow) { var browsers = aWindow.getBrowser().browsers; for (var i = 0; i < browsers.length; i++) { try { var tabData = this._windows[aWindow.__SSi].tabs[i]; if (browsers[i].__SS_data && browsers[i].__SS_restoreState == TAB_STATE_NEEDS_RESTORE) continue; // ignore incompletely initialized tabs this._updateTextAndScrollDataForTab(aWindow, browsers[i], tabData); } catch (ex) { debug(ex); } // get as much data as possible, ignore failures (might succeed the next time) } }, /** * 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 sss_updateTextAndScrollDataForTab(aWindow, aBrowser, aTabData, aFullData) { 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], aFullData, !!aTabData.pinned); if (aBrowser.currentURI.spec == "about:config") aTabData.entries[tabIndex].formdata = { "#textbox": aBrowser.contentDocument.getElementById("textbox").value }; }, /** * 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 aFullData * always return privacy sensitive data (use with care) * @param aIsPinned * the tab is pinned and should be treated differently for privacy */ _updateTextAndScrollDataForFrame: function sss_updateTextAndScrollDataForFrame(aWindow, aContent, aData, 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], aFullData, aIsPinned); } var isHTTPS = this._getURIFromString((aContent.parent || aContent). document.location.href).schemeIs("https"); if (aFullData || this._checkPrivacyLevel(isHTTPS, aIsPinned) || aContent.top.document.location.href == "about:sessionrestore") { let formData = this._collectFormDataForFrame(aContent.document); if (formData) 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") { if (aData.innerHTML === undefined && !aFullData) { // we get no "input" events from iframes - listen for keypress here aContent.addEventListener("keypress", this.saveStateDelayed.bind(this, aWindow, 3000), true); } 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 sss_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 ""; }, /** * collect the state of all form elements * @param aDocument * document reference */ _collectFormDataForFrame: function sss_collectFormDataForFrame(aDocument) { let formNodes = aDocument.evaluate(XPathGenerator.restorableFormNodes, aDocument, XPathGenerator.resolveNS, aDocument.defaultView.XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null); let node = formNodes.iterateNext(); if (!node) return null; const MAX_GENERATED_XPATHS = 100; let generatedCount = 0; let data = {}; do { let nId = node.id; let hasDefaultValue = true; let value; // Only generate a limited number of XPath expressions for perf reasons (cf. bug 477564) if (!nId && generatedCount > MAX_GENERATED_XPATHS) continue; if (ChromeUtils.getClassName(node) === "HTMLInputElement" || ChromeUtils.getClassName(node) === "HTMLTextAreaElement") { switch (node.type) { case "checkbox": case "radio": value = node.checked; hasDefaultValue = value == node.defaultChecked; break; case "file": value = { type: "file", fileList: node.mozGetFileNameArray() }; hasDefaultValue = !value.fileList.length; break; default: // text, textarea value = node.value; hasDefaultValue = value == node.defaultValue; break; } } else if (!node.multiple) { // s with the multiple attribute are easier to determine the // default value since each