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) {