Bug 952998 - Use FrameTree to collect DOMSessionStorage data r=yoric

From 594cc2bdabe535ef356276bce49c4b36c73ab3a2 Mon Sep 17 00:00:00 2001
This commit is contained in:
Tim Taubert 2013-12-20 14:23:32 +01:00
parent ad5b3f57fa
commit 17ef08ca03
8 changed files with 176 additions and 123 deletions

View File

@ -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,

View File

@ -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.
*

View File

@ -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.
}
},
};

View File

@ -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;
}
}
}
},

View File

@ -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

View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>browser_sessionStorage.html</title>
</head>
<body>
<script type="text/javascript;version=1.8">
let isOuter = window == window.top;
let args = window.location.search.slice(1).split("&");
let rand = args[0];
if (isOuter) {
let iframe = document.createElement("iframe");
let isSecure = args.indexOf("secure") > -1;
let scheme = isSecure ? "https" : "http";
iframe.setAttribute("src", scheme + "://example.com" + location.pathname + "?" + rand);
document.body.appendChild(iframe);
}
if (sessionStorage.length === 0) {
sessionStorage.test = (isOuter ? "outer" : "inner") + "-value-" + rand;
}
</script>
</body>
</html>

View File

@ -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);
}

View File

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