diff --git a/browser/components/sessionstore/content/content-sessionStore.js b/browser/components/sessionstore/content/content-sessionStore.js index 4810ac0dae4..2d3a8aa3ff1 100644 --- a/browser/components/sessionstore/content/content-sessionStore.js +++ b/browser/components/sessionstore/content/content-sessionStore.js @@ -333,12 +333,12 @@ let SessionStorageListener = { init: function () { addEventListener("MozStorageChanged", this); Services.obs.addObserver(this, "browser:purge-domain-data", true); - Services.obs.addObserver(this, "browser:purge-session-history", true); + gFrameTree.addObserver(this); }, handleEvent: function (event) { // Ignore events triggered by localStorage or globalStorage changes. - if (isSessionStorageEvent(event)) { + if (gFrameTree.contains(event.target) && isSessionStorageEvent(event)) { this.collect(); } }, @@ -350,7 +350,17 @@ let SessionStorageListener = { }, collect: function () { - MessageQueue.push("storage", () => SessionStorage.collect(docShell)); + if (docShell) { + MessageQueue.push("storage", () => SessionStorage.collect(docShell, gFrameTree)); + } + }, + + onFrameTreeCollected: function () { + this.collect(); + }, + + onFrameTreeReset: function () { + this.collect(); }, QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, diff --git a/browser/components/sessionstore/src/FrameTree.jsm b/browser/components/sessionstore/src/FrameTree.jsm index 5f3c3ac0c95..e03e56f3c32 100644 --- a/browser/components/sessionstore/src/FrameTree.jsm +++ b/browser/components/sessionstore/src/FrameTree.jsm @@ -11,7 +11,7 @@ const Ci = Components.interfaces; Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); -const EXPORTED_METHODS = ["addObserver", "contains", "map"]; +const EXPORTED_METHODS = ["addObserver", "contains", "map", "forEach"]; /** * A FrameTree represents all frames that were reachable when the document @@ -161,6 +161,34 @@ FrameTreeInternal.prototype = { return walk(this.content); }, + /** + * Applies the given function |cb| to all frames stored in the tree. Use this + * method if |map()| doesn't suit your needs and you want more control over + * how data is collected. + * + * @param cb (function) + * This callback receives the current frame as the only argument. + */ + forEach: function (cb) { + let frames = this._frames; + + function walk(frame) { + cb(frame); + + if (!frames.has(frame)) { + return; + } + + Array.forEach(frame.frames, subframe => { + if (frames.has(subframe)) { + cb(subframe); + } + }); + } + + walk(this.content); + }, + /** * Stores a given |frame| and its children in the frame tree. * diff --git a/browser/components/sessionstore/src/SessionStorage.jsm b/browser/components/sessionstore/src/SessionStorage.jsm index fad04f52d4d..501a4d827c7 100644 --- a/browser/components/sessionstore/src/SessionStorage.jsm +++ b/browser/components/sessionstore/src/SessionStorage.jsm @@ -14,20 +14,28 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "console", "resource://gre/modules/devtools/Console.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel", - "resource:///modules/sessionstore/PrivacyLevel.jsm"); + +// Returns the principal for a given |frame| contained in a given |docShell|. +function getPrincipalForFrame(docShell, frame) { + let ssm = Services.scriptSecurityManager; + let doc = frame && frame.document; + let uri = Services.io.newURI(doc.documentURI, null, null); + return ssm.getDocShellCodebasePrincipal(uri, docShell); +} this.SessionStorage = Object.freeze({ /** * Updates all sessionStorage "super cookies" - * @param aDocShell + * @param docShell * That tab's docshell (containing the sessionStorage) + * @param frameTree + * The docShell's FrameTree instance. * @return Returns a nested object that will have hosts as keys and per-host * session storage data as values. For example: * {"example.com": {"key": "value", "my_number": 123}} */ - collect: function (aDocShell) { - return SessionStorageInternal.collect(aDocShell); + collect: function (docShell, frameTree) { + return SessionStorageInternal.collect(docShell, frameTree); }, /** @@ -47,36 +55,40 @@ this.SessionStorage = Object.freeze({ let SessionStorageInternal = { /** * Reads all session storage data from the given docShell. - * @param aDocShell + * @param docShell * A tab's docshell (containing the sessionStorage) + * @param frameTree + * The docShell's FrameTree instance. * @return Returns a nested object that will have hosts as keys and per-host * session storage data as values. For example: * {"example.com": {"key": "value", "my_number": 123}} */ - collect: function (aDocShell) { + collect: function (docShell, frameTree) { let data = {}; - let webNavigation = aDocShell.QueryInterface(Ci.nsIWebNavigation); - let shistory = webNavigation.sessionHistory; + let visitedOrigins = new Set(); - for (let i = 0; shistory && i < shistory.count; i++) { - let principal = History.getPrincipalForEntry(shistory, i, aDocShell); + frameTree.forEach(frame => { + let principal = getPrincipalForFrame(docShell, frame); if (!principal) { - continue; + return; } // Get the root domain of the current history entry // and use that as a key for the per-host storage data. let origin = principal.jarPrefix + principal.origin; - if (data.hasOwnProperty(origin)) { + if (visitedOrigins.has(origin)) { // Don't read a host twice. - continue; + return; } - let originData = this._readEntry(principal, aDocShell); + // Mark the current origin as visited. + visitedOrigins.add(origin); + + let originData = this._readEntry(principal, docShell); if (Object.keys(originData).length) { data[origin] = originData; } - } + }); return Object.keys(data).length ? data : null; }, @@ -91,10 +103,11 @@ let SessionStorageInternal = { * {"example.com": {"key": "value", "my_number": 123}} */ restore: function (aDocShell, aStorageData) { - for (let [host, data] in Iterator(aStorageData)) { + for (let host of Object.keys(aStorageData)) { + let data = aStorageData[host]; let uri = Services.io.newURI(host, null, null); let principal = Services.scriptSecurityManager.getDocShellCodebasePrincipal(uri, aDocShell); - let storageManager = aDocShell.QueryInterface(Components.interfaces.nsIDOMStorageManager); + let storageManager = aDocShell.QueryInterface(Ci.nsIDOMStorageManager); // There is no need to pass documentURI, it's only used to fill documentURI property of // domstorage event, which in this case has no consumer. Prevention of events in case @@ -124,7 +137,7 @@ let SessionStorageInternal = { let storage; try { - let storageManager = aDocShell.QueryInterface(Components.interfaces.nsIDOMStorageManager); + let storageManager = aDocShell.QueryInterface(Ci.nsIDOMStorageManager); storage = storageManager.getStorage(aPrincipal); } catch (e) { // sessionStorage might throw if it's turned off, see bug 458954 @@ -144,25 +157,3 @@ let SessionStorageInternal = { return hostData; } }; - -let History = { - /** - * Returns a given history entry's URI. - * @param aHistory - * That tab's session history - * @param aIndex - * The history entry's index - * @param aDocShell - * That tab's docshell - */ - getPrincipalForEntry: function History_getPrincipalForEntry(aHistory, - aIndex, - aDocShell) { - try { - return Services.scriptSecurityManager.getDocShellCodebasePrincipal( - aHistory.getEntryAtIndex(aIndex, false).URI, aDocShell); - } catch (e) { - // This might throw for some reason. - } - }, -}; diff --git a/browser/components/sessionstore/src/TabState.jsm b/browser/components/sessionstore/src/TabState.jsm index 5a6591b9b43..387bc7e14cf 100644 --- a/browser/components/sessionstore/src/TabState.jsm +++ b/browser/components/sessionstore/src/TabState.jsm @@ -366,7 +366,7 @@ let TabStateInternal = { if (key != "storage" || includePrivateData) { tabData[key] = data[key]; } else { - tabData.storage = {}; + let storage = {}; let isPinned = tab.pinned; // If we're not allowed to include private data, let's filter out hosts @@ -374,9 +374,13 @@ let TabStateInternal = { for (let host of Object.keys(data.storage)) { let isHttps = host.startsWith("https:"); if (PrivacyLevel.canSave({isHttps: isHttps, isPinned: isPinned})) { - tabData.storage[host] = data.storage[host]; + storage[host] = data.storage[host]; } } + + if (Object.keys(storage).length) { + tabData.storage = storage; + } } } }, diff --git a/browser/components/sessionstore/test/browser.ini b/browser/components/sessionstore/test/browser.ini index c5a6b9b072e..8c94ff96030 100644 --- a/browser/components/sessionstore/test/browser.ini +++ b/browser/components/sessionstore/test/browser.ini @@ -20,6 +20,7 @@ support-files = browser_pageStyle_sample_nested.html browser_scrollPositions_sample.html browser_scrollPositions_sample_frameset.html + browser_sessionStorage.html browser_248970_b_sample.html browser_339445_sample.html browser_346337_sample.html diff --git a/browser/components/sessionstore/test/browser_sessionStorage.html b/browser/components/sessionstore/test/browser_sessionStorage.html new file mode 100644 index 00000000000..69d72015864 --- /dev/null +++ b/browser/components/sessionstore/test/browser_sessionStorage.html @@ -0,0 +1,26 @@ + + + + + browser_sessionStorage.html + + + + + diff --git a/browser/components/sessionstore/test/browser_sessionStorage.js b/browser/components/sessionstore/test/browser_sessionStorage.js index 1a77592a1d1..41d6700c306 100644 --- a/browser/components/sessionstore/test/browser_sessionStorage.js +++ b/browser/components/sessionstore/test/browser_sessionStorage.js @@ -3,35 +3,39 @@ "use strict"; -let tmp = {}; -Cu.import("resource://gre/modules/Promise.jsm", tmp); -let {Promise} = tmp; +const RAND = Math.random(); +const URL = "http://mochi.test:8888/browser/" + + "browser/components/sessionstore/test/browser_sessionStorage.html" + + "?" + RAND; -const INITIAL_VALUE = "initial-value-" + Date.now(); +const OUTER_VALUE = "outer-value-" + RAND; +const INNER_VALUE = "inner-value-" + RAND; /** * This test ensures that setting, modifying and restoring sessionStorage data * works as expected. */ add_task(function session_storage() { - let tab = yield createTabWithStorageData(["http://example.com", "http://mochi.test:8888"]); + let tab = gBrowser.addTab(URL); let browser = tab.linkedBrowser; + yield promiseBrowserLoaded(browser); // Flush to make sure chrome received all data. SyncHandlers.get(browser).flush(); let {storage} = JSON.parse(ss.getTabState(tab)); - is(storage["http://example.com"].test, INITIAL_VALUE, + is(storage["http://example.com"].test, INNER_VALUE, "sessionStorage data for example.com has been serialized correctly"); - is(storage["http://mochi.test:8888"].test, INITIAL_VALUE, + is(storage["http://mochi.test:8888"].test, OUTER_VALUE, "sessionStorage data for mochi.test has been serialized correctly"); // Ensure that modifying sessionStore values works. yield modifySessionStorage(browser, {test: "modified"}); + yield modifySessionStorage2(browser, {test: "modified2"}); SyncHandlers.get(browser).flush(); let {storage} = JSON.parse(ss.getTabState(tab)); - is(storage["http://example.com"].test, INITIAL_VALUE, + is(storage["http://example.com"].test, "modified2", "sessionStorage data for example.com has been serialized correctly"); is(storage["http://mochi.test:8888"].test, "modified", "sessionStorage data for mochi.test has been serialized correctly"); @@ -45,22 +49,40 @@ add_task(function session_storage() { SyncHandlers.get(browser2).flush(); let {storage} = JSON.parse(ss.getTabState(tab2)); - is(storage["http://example.com"].test, INITIAL_VALUE, + is(storage["http://example.com"].test, "modified2", "sessionStorage data for example.com has been duplicated correctly"); is(storage["http://mochi.test:8888"].test, "modified", "sessionStorage data for mochi.test has been duplicated correctly"); // Ensure that the content script retains restored data - // (by e.g. duplicateTab) and send it along with new data. - yield modifySessionStorage(browser2, {test: "modified2"}); + // (by e.g. duplicateTab) and sends it along with new data. + yield modifySessionStorage(browser2, {test: "modified3"}); SyncHandlers.get(browser2).flush(); let {storage} = JSON.parse(ss.getTabState(tab2)); - is(storage["http://example.com"].test, INITIAL_VALUE, + is(storage["http://example.com"].test, "modified2", "sessionStorage data for example.com has been duplicated correctly"); - is(storage["http://mochi.test:8888"].test, "modified2", + is(storage["http://mochi.test:8888"].test, "modified3", "sessionStorage data for mochi.test has been duplicated correctly"); + // Check that loading a new URL discards data. + browser2.loadURI("http://mochi.test:8888/"); + yield promiseBrowserLoaded(browser2); + SyncHandlers.get(browser2).flush(); + + let {storage} = JSON.parse(ss.getTabState(tab2)); + is(storage["http://mochi.test:8888"].test, "modified3", + "navigating retains correct storage data"); + ok(!storage["http://example.com"], "storage data was discarded"); + + // Check that loading a new URL discards data. + browser2.loadURI("about:mozilla"); + yield promiseBrowserLoaded(browser2); + SyncHandlers.get(browser2).flush(); + + let state = JSON.parse(ss.getTabState(tab2)); + ok(!state.hasOwnProperty("storage"), "storage data was discarded"); + // Clean up. gBrowser.removeTab(tab); gBrowser.removeTab(tab2); @@ -71,10 +93,12 @@ add_task(function session_storage() { * sessionStorage data collected for tabs. */ add_task(function purge_domain() { - let tab = yield createTabWithStorageData(["http://example.com", "http://mochi.test:8888"]); + let tab = gBrowser.addTab(URL); let browser = tab.linkedBrowser; + yield promiseBrowserLoaded(browser); - yield notifyObservers(browser, "browser:purge-domain-data", "mochi.test"); + // Purge data for "mochi.test". + yield purgeDomainData(browser, "mochi.test"); // Flush to make sure chrome received all data. SyncHandlers.get(browser).flush(); @@ -82,56 +106,36 @@ add_task(function purge_domain() { let {storage} = JSON.parse(ss.getTabState(tab)); ok(!storage["http://mochi.test:8888"], "sessionStorage data for mochi.test has been purged"); - is(storage["http://example.com"].test, INITIAL_VALUE, + is(storage["http://example.com"].test, INNER_VALUE, "sessionStorage data for example.com has been preserved"); gBrowser.removeTab(tab); }); -/** - * This test ensures that purging session history data also purges data from - * sessionStorage data collected for tabs - */ -add_task(function purge_shistory() { - let tab = yield createTabWithStorageData(["http://example.com", "http://mochi.test:8888"]); - let browser = tab.linkedBrowser; - - yield notifyObservers(browser, "browser:purge-session-history"); - - // Flush to make sure chrome received all data. - SyncHandlers.get(browser).flush(); - - let {storage} = JSON.parse(ss.getTabState(tab)); - ok(!storage["http://example.com"], - "sessionStorage data for example.com has been purged"); - is(storage["http://mochi.test:8888"].test, INITIAL_VALUE, - "sessionStorage data for mochi.test has been preserved"); - - gBrowser.removeTab(tab); -}); - /** * This test ensures that collecting sessionStorage data respects the privacy * levels as set by the user. */ add_task(function respect_privacy_level() { - let tab = yield createTabWithStorageData(["http://example.com", "https://example.com"]); + let tab = gBrowser.addTab(URL + "&secure"); + yield promiseBrowserLoaded(tab.linkedBrowser); gBrowser.removeTab(tab); let [{state: {storage}}] = JSON.parse(ss.getClosedTabData(window)); - is(storage["http://example.com"].test, INITIAL_VALUE, + is(storage["http://mochi.test:8888"].test, OUTER_VALUE, "http sessionStorage data has been saved"); - is(storage["https://example.com"].test, INITIAL_VALUE, + is(storage["https://example.com"].test, INNER_VALUE, "https sessionStorage data has been saved"); // Disable saving data for encrypted sites. Services.prefs.setIntPref("browser.sessionstore.privacy_level", 1); - let tab = yield createTabWithStorageData(["http://example.com", "https://example.com"]); + let tab = gBrowser.addTab(URL + "&secure"); + yield promiseBrowserLoaded(tab.linkedBrowser); gBrowser.removeTab(tab); let [{state: {storage}}] = JSON.parse(ss.getClosedTabData(window)); - is(storage["http://example.com"].test, INITIAL_VALUE, + is(storage["http://mochi.test:8888"].test, OUTER_VALUE, "http sessionStorage data has been saved"); ok(!storage["https://example.com"], "https sessionStorage data has *not* been saved"); @@ -140,17 +144,15 @@ add_task(function respect_privacy_level() { Services.prefs.setIntPref("browser.sessionstore.privacy_level", 2); // Check that duplicating a tab copies all private data. - let tab = yield createTabWithStorageData(["http://example.com", "https://example.com"]); + let tab = gBrowser.addTab(URL + "&secure"); + yield promiseBrowserLoaded(tab.linkedBrowser); let tab2 = gBrowser.duplicateTab(tab); - yield promiseBrowserLoaded(tab2.linkedBrowser); + yield promiseTabRestored(tab2); gBrowser.removeTab(tab); // With privacy_level=2 the |tab| shouldn't have any sessionStorage data. let [{state: {storage}}] = JSON.parse(ss.getClosedTabData(window)); - ok(!storage["http://example.com"], - "http sessionStorage data has *not* been saved"); - ok(!storage["https://example.com"], - "https sessionStorage data has *not* been saved"); + ok(!storage, "sessionStorage data has *not* been saved"); // Restore the default privacy level and close the duplicated tab. Services.prefs.clearUserPref("browser.sessionstore.privacy_level"); @@ -158,42 +160,26 @@ add_task(function respect_privacy_level() { // With privacy_level=0 the duplicated |tab2| should persist all data. let [{state: {storage}}] = JSON.parse(ss.getClosedTabData(window)); - is(storage["http://example.com"].test, INITIAL_VALUE, + is(storage["http://mochi.test:8888"].test, OUTER_VALUE, "http sessionStorage data has been saved"); - is(storage["https://example.com"].test, INITIAL_VALUE, + is(storage["https://example.com"].test, INNER_VALUE, "https sessionStorage data has been saved"); }); -function createTabWithStorageData(urls) { - return Task.spawn(function task() { - let tab = gBrowser.addTab(); - let browser = tab.linkedBrowser; - - for (let url of urls) { - browser.loadURI(url); - yield promiseBrowserLoaded(browser); - yield modifySessionStorage(browser, {test: INITIAL_VALUE}); - } - - throw new Task.Result(tab); - }); -} - function waitForStorageEvent(browser) { return promiseContentMessage(browser, "ss-test:MozStorageChanged"); } -function waitForUpdateMessage(browser) { - return promiseContentMessage(browser, "SessionStore:update"); -} - function modifySessionStorage(browser, data) { browser.messageManager.sendAsyncMessage("ss-test:modifySessionStorage", data); return waitForStorageEvent(browser); } -function notifyObservers(browser, topic, data) { - let msg = {topic: topic, data: data}; - browser.messageManager.sendAsyncMessage("ss-test:notifyObservers", msg); - return waitForUpdateMessage(browser); +function modifySessionStorage2(browser, data) { + browser.messageManager.sendAsyncMessage("ss-test:modifySessionStorage2", data); + return waitForStorageEvent(browser); +} + +function purgeDomainData(browser, domain) { + return sendMessage(browser, "ss-test:purgeDomainData", domain); } diff --git a/browser/components/sessionstore/test/content.js b/browser/components/sessionstore/test/content.js index 41ad3595d1b..dab506aeb1e 100644 --- a/browser/components/sessionstore/test/content.js +++ b/browser/components/sessionstore/test/content.js @@ -33,8 +33,15 @@ addMessageListener("ss-test:modifySessionStorage", function (msg) { } }); -addMessageListener("ss-test:notifyObservers", function ({data: {topic, data}}) { - Services.obs.notifyObservers(null, topic, data || ""); +addMessageListener("ss-test:modifySessionStorage2", function (msg) { + for (let key of Object.keys(msg.data)) { + content.frames[0].sessionStorage[key] = msg.data[key]; + } +}); + +addMessageListener("ss-test:purgeDomainData", function ({data: domain}) { + Services.obs.notifyObservers(null, "browser:purge-domain-data", domain); + content.setTimeout(() => sendAsyncMessage("ss-test:purgeDomainData")); }); addMessageListener("ss-test:getStyleSheets", function (msg) {