diff --git a/browser/components/sessionstore/content/content-sessionStore.js b/browser/components/sessionstore/content/content-sessionStore.js index bdf0fef0c142..43bf6ff6da15 100644 --- a/browser/components/sessionstore/content/content-sessionStore.js +++ b/browser/components/sessionstore/content/content-sessionStore.js @@ -13,7 +13,7 @@ function debug(msg) { let EventListener = { DOM_EVENTS: [ - "pageshow", "change", "input" + "pageshow", "change", "input", "MozStorageChanged" ], init: function () { @@ -30,6 +30,24 @@ let EventListener = { case "change": sendAsyncMessage("SessionStore:input"); break; + case "MozStorageChanged": + { + let isSessionStorage = true; + // We are only interested in sessionStorage events + try { + if (event.storageArea != content.sessionStorage) { + isSessionStorage = false; + } + } catch (ex) { + // This page does not even have sessionStorage + // (this is typically the case of about: pages) + isSessionStorage = false; + } + if (isSessionStorage) { + sendAsyncMessage("SessionStore:MozStorageChanged"); + } + break; + } default: debug("received unknown event '" + event.type + "'"); break; diff --git a/browser/components/sessionstore/src/SessionStore.jsm b/browser/components/sessionstore/src/SessionStore.jsm index 539687e9f047..4b7213fa4bb3 100644 --- a/browser/components/sessionstore/src/SessionStore.jsm +++ b/browser/components/sessionstore/src/SessionStore.jsm @@ -58,7 +58,11 @@ const MESSAGES = [ // 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" + "SessionStore:pageshow", + + // The content script has received a MozStorageChanged event dealing + // with a change in the contents of the sessionStorage. + "SessionStore:MozStorageChanged" ]; // These are tab events that we listen to. @@ -120,9 +124,16 @@ XPCOMUtils.defineLazyServiceGetter(this, "CrashReporter", "@mozilla.org/xre/app-info;1", "nsICrashReporter"); #endif +/** + * |true| if we are in debug mode, |false| otherwise. + * Debug mode is controlled by preference browser.sessionstore.debug + */ +let gDebuggingEnabled = false; function debug(aMsg) { - aMsg = ("SessionStore: " + aMsg).replace(/\S{80}/g, "$&\n"); - Services.console.logStringMessage(aMsg); + if (gDebuggingEnabled) { + aMsg = ("SessionStore: " + aMsg).replace(/\S{80}/g, "$&\n"); + Services.console.logStringMessage(aMsg); + } } this.SessionStore = { @@ -537,9 +548,13 @@ let SessionStoreInternal = { }, _initPrefs : function() { - XPCOMUtils.defineLazyGetter(this, "_prefBranch", function () { - return Services.prefs.getBranch("browser."); - }); + this._prefBranch = Services.prefs.getBranch("browser."); + + gDebuggingEnabled = this._prefBranch.getBoolPref("sessionstore.debug"); + + Services.prefs.addObserver("browser.sessionstore.debug", () => { + gDebuggingEnabled = this._prefBranch.getBoolPref("sessionstore.debug"); + }, false); // minimal interval between two save operations (in milliseconds) XPCOMUtils.defineLazyGetter(this, "_interval", function () { @@ -612,37 +627,43 @@ let SessionStoreInternal = { 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; + try { + 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; + } + } catch (ex) { + debug("Uncaught error during observe"); + debug(ex); + debug(ex.stack); } }, @@ -661,6 +682,10 @@ let SessionStoreInternal = { case "SessionStore:input": this.onTabInput(win, browser); break; + case "SessionStore:MozStorageChanged": + TabStateCache.delete(browser); + this.saveStateDelayed(win); + break; default: debug("received unknown message '" + aMessage.name + "'"); break; @@ -685,6 +710,7 @@ let SessionStoreInternal = { // (form data, scrolling, etc.). This will only happen when a tab is // first restored. let browser = aEvent.currentTarget; + TabStateCache.delete(browser); if (browser.__SS_restore_data) this.restoreDocument(win, browser, aEvent); this.onTabLoad(win, browser); @@ -708,11 +734,16 @@ let SessionStoreInternal = { this.onTabHide(win, aEvent.originalTarget); break; case "TabPinned": + // If possible, update cached data without having to invalidate it + TabStateCache.update(aEvent.originalTarget, "pinned", true); + this.saveStateDelayed(win); + break; case "TabUnpinned": + // If possible, update cached data without having to invalidate it + TabStateCache.update(aEvent.originalTarget, "pinned", false); this.saveStateDelayed(win); break; } - this._clearRestoringWindows(); }, @@ -1082,6 +1113,7 @@ let SessionStoreInternal = { let openWindows = {}; this._forEachBrowserWindow(function(aWindow) { Array.forEach(aWindow.gBrowser.tabs, function(aTab) { + TabStateCache.delete(aTab); delete aTab.linkedBrowser.__SS_data; delete aTab.linkedBrowser.__SS_tabStillLoading; delete aTab.linkedBrowser.__SS_formDataSaved; @@ -1305,9 +1337,8 @@ let SessionStoreInternal = { return; } - // make sure that the tab related data is up-to-date - var tabState = this._collectTabData(aTab); - this._updateTextAndScrollDataForTab(aWindow, aTab.linkedBrowser, tabState); + // Get the latest data for this tab (generally, from the cache) + let tabState = this._collectTabData(aTab); // store closed-tab data for undo if (this._shouldSaveTabState(tabState)) { @@ -1328,7 +1359,8 @@ let SessionStoreInternal = { }, /** - * When a tab loads, save state. + * When a tab loads, invalidate its cached state, trigger async save. + * * @param aWindow * Window reference * @param aBrowser @@ -1344,6 +1376,8 @@ let SessionStoreInternal = { return; } + TabStateCache.delete(aBrowser); + delete aBrowser.__SS_data; delete aBrowser.__SS_tabStillLoading; delete aBrowser.__SS_formDataSaved; @@ -1364,6 +1398,8 @@ let SessionStoreInternal = { // deleting __SS_formDataSaved will cause us to recollect form data delete aBrowser.__SS_formDataSaved; + TabStateCache.delete(aBrowser); + this.saveStateDelayed(aWindow, 3000); }, @@ -1400,6 +1436,9 @@ let SessionStoreInternal = { this.restoreNextTab(); } + // If possible, update cached data without having to invalidate it + TabStateCache.update(aTab, "hidden", false); + // Default delay of 2 seconds gives enough time to catch multiple TabShow // events due to changing groups in Panorama. this.saveStateDelayed(aWindow); @@ -1412,6 +1451,9 @@ let SessionStoreInternal = { TabRestoreQueue.visibleToHidden(aTab); } + // If possible, update cached data without having to invalidate it + TabStateCache.update(aTab, "hidden", true); + // Default delay of 2 seconds gives enough time to catch multiple TabHide // events due to changing groups in Panorama. this.saveStateDelayed(aWindow); @@ -1487,20 +1529,41 @@ let SessionStoreInternal = { 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); + let tabState = this._collectTabData(aTab); return this._toJSONString(tabState); }, setTabState: function ssi_setTabState(aTab, aState) { - var tabState = JSON.parse(aState); - if (!tabState.entries || !aTab.ownerDocument || !aTab.ownerDocument.defaultView.__SSi) + // Remove the tab state from the cache. + // Note that we cannot simply replace the contents of the cache + // as |aState| can be an incomplete state that will be completed + // by |restoreHistoryPrecursor|. + let tabState = JSON.parse(aState); + if (!tabState) { + debug("Empty state argument"); throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + } + if (typeof tabState != "object") { + debug("State argument does not represent an object"); + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + } + if (!("entries" in tabState)) { + debug("State argument must contain field 'entries'"); + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + } + if (!aTab.ownerDocument) { + debug("Tab argument must have an owner document"); + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + } - var window = aTab.ownerDocument.defaultView; + let window = aTab.ownerDocument.defaultView; + if (!("__SSi" in window)) { + debug("Default view of ownerDocument must have a unique identifier"); + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + } + + TabStateCache.delete(aTab); this._setWindowStateBusy(window); this.restoreHistoryPrecursor(window, [aTab], [tabState], 0, 0, 0); }, @@ -1510,9 +1573,9 @@ let SessionStoreInternal = { !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); + // Duplicate the tab state + let tabState = this._cloneFullTabData(aTab); + tabState.index += aDelta; tabState.index = Math.max(1, Math.min(tabState.index, tabState.entries.length)); tabState.pinned = false; @@ -1682,6 +1745,7 @@ let SessionStoreInternal = { if (aWindow.__SSi && this._windows[aWindow.__SSi].extData && this._windows[aWindow.__SSi].extData[aKey]) delete this._windows[aWindow.__SSi].extData[aKey]; + this.saveStateDelayed(aWindow); }, getTabValue: function ssi_getTabValue(aTab, aKey) { @@ -1697,6 +1761,7 @@ let SessionStoreInternal = { }, setTabValue: function ssi_setTabValue(aTab, aKey, aStringValue) { + TabStateCache.delete(aTab); // If the tab hasn't been restored, then set the data there, otherwise we // could lose newly added data. let saveTo; @@ -1715,6 +1780,7 @@ let SessionStoreInternal = { }, deleteTabValue: function ssi_deleteTabValue(aTab, aKey) { + TabStateCache.delete(aTab); // 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. @@ -1728,10 +1794,12 @@ let SessionStoreInternal = { if (deleteFrom && deleteFrom[aKey]) delete deleteFrom[aKey]; + this.saveStateDelayed(aTab.ownerDocument.defaultView); }, persistTabAttribute: function ssi_persistTabAttribute(aName) { if (TabAttributes.persist(aName)) { + TabStateCache.clear(); this.saveStateDelayed(); } }, @@ -1905,37 +1973,56 @@ let SessionStoreInternal = { /* ........ Saving Functionality .............. */ /** - * Store all session data for a window - * @param aWindow - * Window reference + * Collect data related to a single tab + * + * @param aTab + * tabbrowser tab + * + * @returns {TabData} An object with the data for this tab. If the + * tab has not been invalidated since the last call to + * _collectTabData(aTab), the same object is returned. */ - _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; + _collectTabData: function ssi_collectTabData(aTab) { + if (!aTab) { + throw new TypeError("Expecting a tab"); + } + let tabData; + if ((tabData = TabStateCache.get(aTab))) { + return tabData; + } + tabData = new TabData(this._collectBaseTabData(aTab)); + if (this._updateTextAndScrollDataForTab(aTab, tabData)) { + TabStateCache.set(aTab, tabData); + } + return tabData; }, /** - * Collect data related to a single tab + * Collect data related to a single tab, including private data. + * Use with caution. + * * @param aTab * tabbrowser tab - * @param aFullData - * always return privacy sensitive data (use with care) - * @returns object + * + * @returns {object} An object with the data for this tab. This object + * is recomputed at every call. */ - _collectTabData: function ssi_collectTabData(aTab, aFullData) { - var tabData = { entries: [], lastAccessed: aTab.lastAccessed }; - var browser = aTab.linkedBrowser; + _cloneFullTabData: function ssi_cloneFullTabData(aTab) { + let options = { includePrivateData: true }; + let tabData = this._collectBaseTabData(aTab, options); + this._updateTextAndScrollDataForTab(aTab, tabData, options); + return tabData; + }, - if (!browser || !browser.currentURI) + _collectBaseTabData: function ssi_collectBaseTabData(aTab, aOptions = null) { + let includePrivateData = aOptions && aOptions.includePrivateData; + let tabData = {entries: [], lastAccessed: aTab.lastAccessed }; + let 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) { + } + 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) @@ -1965,7 +2052,7 @@ let SessionStoreInternal = { 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) { + history.index < this._sessionhistory_max_entries - 1 && !includePrivateData) { tabData = browser.__SS_data; tabData.index = history.index + 1; } @@ -1974,7 +2061,7 @@ let SessionStoreInternal = { try { for (var j = 0; j < history.count; j++) { let entry = this._serializeHistoryEntry(history.getEntryAtIndex(j, false), - aFullData, aTab.pinned, browser.__SS_hostSchemeData); + includePrivateData, aTab.pinned, browser.__SS_hostSchemeData); tabData.entries.push(entry); } // If we make it through the for loop, then we're ok and we should clear @@ -2000,7 +2087,7 @@ let SessionStoreInternal = { tabData.index = history.index + 1; // make sure not to cache privacy sensitive data which shouldn't get out - if (!aFullData) + if (!includePrivateData) browser.__SS_data = tabData; } else if (browser.currentURI.spec != "about:blank" || @@ -2049,7 +2136,7 @@ let SessionStoreInternal = { delete tabData.extData; if (history && browser.docShell instanceof Ci.nsIDocShell) { - let storageData = SessionStorage.serialize(browser.docShell, aFullData) + let storageData = SessionStorage.serialize(browser.docShell, includePrivateData) if (Object.keys(storageData).length) tabData.storage = storageData; } @@ -2062,7 +2149,7 @@ let SessionStoreInternal = { * Used for data storage * @param aEntry * nsISHEntry instance - * @param aFullData + * @param aIncludePrivateData * always return privacy sensitive data (use with care) * @param aIsPinned * the tab is pinned and should be treated differently for privacy @@ -2071,7 +2158,7 @@ let SessionStoreInternal = { * @returns object */ _serializeHistoryEntry: - function ssi_serializeHistoryEntry(aEntry, aFullData, aIsPinned, aHostSchemeData) { + function ssi_serializeHistoryEntry(aEntry, aIncludePrivateData, aIsPinned, aHostSchemeData) { var entry = { url: aEntry.URI.spec }; try { @@ -2122,7 +2209,7 @@ let SessionStoreInternal = { try { var prefPostdata = this._prefBranch.getIntPref("sessionstore.postdata"); - if (aEntry.postData && (aFullData || prefPostdata && + if (aEntry.postData && (aIncludePrivateData || prefPostdata && this.checkPrivacyLevel(aEntry.URI.schemeIs("https"), aIsPinned))) { aEntry.postData.QueryInterface(Ci.nsISeekableStream). seek(Ci.nsISeekableStream.NS_SEEK_SET, 0); @@ -2131,7 +2218,7 @@ let SessionStoreInternal = { stream.setInputStream(aEntry.postData); var postBytes = stream.readByteArray(stream.available()); var postdata = String.fromCharCode.apply(null, postBytes); - if (aFullData || prefPostdata == -1 || + if (aIncludePrivateData || prefPostdata == -1 || postdata.replace(/^(Content-.*\r\n)+(\r\n)*/, "").length <= prefPostdata) { // We can stop doing base64 encoding once our serialization into JSON @@ -2192,7 +2279,7 @@ let SessionStoreInternal = { break; } - children.push(this._serializeHistoryEntry(child, aFullData, + children.push(this._serializeHistoryEntry(child, aIncludePrivateData, aIsPinned, aHostSchemeData)); } } @@ -2205,63 +2292,57 @@ let SessionStoreInternal = { }, /** - * go through all tabs and store the current scroll positions + * Go through all frames 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 aTab + * tabbrowser tab * @param aTabData * tabData object to add the information to - * @param aFullData - * always return privacy sensitive data (use with care) + * @param options + * An optional object that may contain the following field: + * - includePrivateData: always return privacy sensitive data + * (use with care) + * @return false if data should not be cached because the tab + * has not been fully initialized yet. */ _updateTextAndScrollDataForTab: - function ssi_updateTextAndScrollDataForTab(aWindow, aBrowser, aTabData, aFullData) { + function ssi_updateTextAndScrollDataForTab(aTab, aTabData, aOptions = null) { + let includePrivateData = aOptions && aOptions.includePrivateData; + let window = aTab.ownerDocument.defaultView; + let browser = aTab.linkedBrowser; // we shouldn't update data for incompletely initialized tabs - if (aBrowser.__SS_data && aBrowser.__SS_tabStillLoading) - return; + if (!browser.currentURI + || (browser.__SS_data && browser.__SS_tabStillLoading)) { + return false; + } - var tabIndex = (aTabData.index || aTabData.entries.length) - 1; + let 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; + if (!aTabData.entries[tabIndex]) { + return false; + } - let selectedPageStyle = aBrowser.markupDocumentViewer.authorStyleDisabled ? "_nostyle" : - this._getSelectedPageStyle(aBrowser.contentWindow); + let selectedPageStyle = browser.markupDocumentViewer.authorStyleDisabled ? "_nostyle" : + this._getSelectedPageStyle(browser.contentWindow); if (selectedPageStyle) aTabData.pageStyle = selectedPageStyle; else if (aTabData.pageStyle) delete aTabData.pageStyle; - this._updateTextAndScrollDataForFrame(aWindow, aBrowser.contentWindow, + this._updateTextAndScrollDataForFrame(window, browser.contentWindow, aTabData.entries[tabIndex], - !aBrowser.__SS_formDataSaved, aFullData, + !browser.__SS_formDataSaved, includePrivateData, !!aTabData.pinned); - aBrowser.__SS_formDataSaved = true; - if (aBrowser.currentURI.spec == "about:config") + browser.__SS_formDataSaved = true; + if (browser.currentURI.spec == "about:config") aTabData.entries[tabIndex].formdata = { id: { - "textbox": aBrowser.contentDocument.getElementById("textbox").value + "textbox": browser.contentDocument.getElementById("textbox").value }, xpath: {} }; + return true; }, /** @@ -2275,26 +2356,26 @@ let SessionStoreInternal = { * part of a tabData object to add the information to * @param aUpdateFormData * update all form data for this tab - * @param aFullData + * @param aIncludePrivateData * 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) { + aUpdateFormData, aIncludePrivateData, 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); + aIncludePrivateData, aIsPinned); } var isHTTPS = this._getURIFromString((aContent.parent || aContent). document.location.href).schemeIs("https"); let topURL = aContent.top.document.location.href; let isAboutSR = topURL == "about:sessionrestore" || topURL == "about:welcomeback"; - if (aFullData || this.checkPrivacyLevel(isHTTPS, aIsPinned) || isAboutSR) { - if (aFullData || aUpdateFormData) { + if (aIncludePrivateData || this.checkPrivacyLevel(isHTTPS, aIsPinned) || isAboutSR) { + if (aIncludePrivateData || aUpdateFormData) { let formData = DocumentUtils.getFormData(aContent.document); // We want to avoid saving data for about:sessionrestore as a string. @@ -2417,28 +2498,6 @@ let SessionStoreInternal = { } }, - /** - * 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 @@ -2676,10 +2735,29 @@ let SessionStoreInternal = { if (!this._isWindowLoaded(aWindow)) return; + let tabbrowser = aWindow.gBrowser; + let tabs = tabbrowser.tabs; + let winData = this._windows[aWindow.__SSi]; + let tabsData = winData.tabs = []; + let hosts = this._internalWindows[aWindow.__SSi].hosts = {}; + // update the internal state data for this window - this._saveWindowHistory(aWindow); - this._updateTextAndScrollData(aWindow); - this._updateCookieHosts(aWindow); + for (let tab of tabs) { + tabsData.push(this._collectTabData(tab)); + + // Since we are only ever called for open + // windows during a session, we can call into + // _extractHostsForCookiesFromHostScheme directly using data + // that is attached to each browser. + 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); + } + } + winData.selected = tabbrowser.mTabBox.selectedIndex + 1; + this._updateWindowFeatures(aWindow); // Make sure we keep __SS_lastSessionWindowID around for cases like entering @@ -2813,10 +2891,13 @@ let SessionStoreInternal = { // 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). + // We also want to invalidate any cached information on the tab state. if (aOverwriteTabs) { for (let i = 0; i < tabbrowser.tabs.length; i++) { + let tab = tabbrowser.tabs[i]; + TabStateCache.delete(tab); if (tabbrowser.browsers[i].__SS_restoreState) - this._resetTabRestoringState(tabbrowser.tabs[i]); + this._resetTabRestoringState(tab); } } @@ -2978,6 +3059,7 @@ let SessionStoreInternal = { 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 @@ -2993,7 +3075,7 @@ let SessionStoreInternal = { var restoreHistoryFunc = function(self) { self.restoreHistoryPrecursor(aWindow, aTabs, aTabData, aSelectTab, aIx, aCount + 1, aRestoreImmediately); - } + }; aWindow.setTimeout(restoreHistoryFunc, 100, this); return; } @@ -3129,7 +3211,6 @@ let SessionStoreInternal = { var tab = aTabs.shift(); var tabData = aTabData.shift(); - var browser = aWindow.gBrowser.getBrowserForTab(tab); var history = browser.webNavigation.sessionHistory; @@ -3694,7 +3775,7 @@ let SessionStoreInternal = { * @param aDelay * Milliseconds to delay */ - saveStateDelayed: function ssi_saveStateDelayed(aWindow, aDelay) { + saveStateDelayed: function ssi_saveStateDelayed(aWindow = null, aDelay = 2000) { if (aWindow) { this._dirtyWindows[aWindow.__SSi] = true; } @@ -3704,7 +3785,7 @@ let SessionStoreInternal = { 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); + aDelay = Math.max(minimalDelay, aDelay); if (aDelay > 0) { this._saveTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); this._saveTimer.init(this, aDelay, Ci.nsITimer.TYPE_ONE_SHOT); @@ -3976,6 +4057,7 @@ let SessionStoreInternal = { if (tab.linkedBrowser == aBrowser) return tab; } + return undefined; }, /** @@ -4798,11 +4880,25 @@ SessionStoreSHistoryListener.prototype = { 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; }, +// The following events (with the exception of OnHistoryPurge) +// accompany either a "load" or a "pageshow" which will in turn cause +// invalidations. + OnHistoryNewEntry: function(aNewURI) { + + }, + OnHistoryGoBack: function(aBackURI) { + return true; + }, + OnHistoryGoForward: function(aForwardURI) { + return true; + }, + OnHistoryGotoIndex: function(aIndex, aGotoURI) { + return true; + }, + OnHistoryPurge: function(aNumEntries) { + TabStateCache.delete(this.tab); + 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 @@ -4825,4 +4921,106 @@ String.prototype.hasRootDomain = function hasRootDomain(aDomain) { let prevChar = this[index - 1]; return (index == (this.length - aDomain.length)) && (prevChar == "." || prevChar == "/"); +}; + +function TabData(obj = null) { + if (obj) { + if (obj instanceof TabData) { + // FIXME: Can we get rid of this? + return obj; + } + for (let [key, value] in Iterator(obj)) { + this[key] = value; + } + } + return this; } + +/** + * A cache for tabs data. + * + * This cache implements a weak map from tabs (as XUL elements) + * to tab data (as instances of TabData). + * + * Note that we should never cache private data, as: + * - that data is used very seldom by SessionStore; + * - caching private data in addition to public data is memory consuming. + */ +let TabStateCache = { + _data: new WeakMap(), + + /** + * Add or replace an entry in the cache. + * + * @param {XULElement} aTab The key, which may be either a tab + * or the corresponding browser. The binding will disappear + * if the tab/browser is destroyed. + * @param {TabData} aValue The data associated to |aTab|. + */ + set: function(aTab, aValue) { + let key = this._normalizeToBrowser(aTab); + if (!(aValue instanceof TabData)) { + throw new TypeError("Attempting to cache a non TabData"); + } + this._data.set(key, aValue); + }, + + /** + * Return the tab data associated with a tab. + * + * @param {XULElement} aKey The tab or the associated browser. + * + * @return {TabData|undefined} The data if available, |undefined| + * otherwise. + */ + get: function(aKey) { + let key = this._normalizeToBrowser(aKey); + return this._data.get(key); + }, + + /** + * Delete the tab data associated with a tab. + * + * @param {XULElement} aKey The tab or the associated browser. + * + * Noop of there is no tab data associated with the tab. + */ + delete: function(aKey) { + let key = this._normalizeToBrowser(aKey); + this._data.delete(key); + }, + + /** + * Delete all tab data. + */ + clear: function() { + this._data.clear(); + }, + + /** + * Update in place a piece of data. + * + * @param {XULElement} aKey The tab or the associated browser. + * If the tab/browser is not present, do nothing. + * @param {string} aField The field to update. + * @param {*} aValue The new value to place in the field. + */ + update: function(aKey, aField, aValue) { + let key = this._normalizeToBrowser(aKey); + let data = this._data.get(key); + if (data) { + data[aField] = aValue; + } + }, + + _normalizeToBrowser: function(aKey) { + let nodeName = aKey.localName; + if (nodeName == "tab") { + return aKey.linkedBrowser; + } + if (nodeName == "browser") { + return aKey; + } + throw new TypeError("Key is neither a tab nor a browser: " + nodeName); + } +};