Bug 1373672 - Part 1: Filter events from dynamic docShells in Gecko before they reach SessionStore event handlers r=smaug,mystor

This commit is contained in:
Tim Taubert 2017-07-21 14:57:30 +02:00
parent 9975089d4d
commit 36e075eddf
20 changed files with 532 additions and 482 deletions

View File

@ -23,5 +23,6 @@ LOCAL_INCLUDES += [
'../dirprovider',
'../feeds',
'../migration',
'../sessionstore',
'../shell',
]

View File

@ -24,6 +24,7 @@
#include "nsFeedSniffer.h"
#include "AboutRedirector.h"
#include "nsIAboutModule.h"
#include "nsSessionStoreUtils.h"
#include "nsNetCID.h"
@ -60,6 +61,9 @@ NS_DEFINE_NAMED_CID(NS_WINIEHISTORYENUMERATOR_CID);
NS_DEFINE_NAMED_CID(NS_SHELLSERVICE_CID);
#endif
NS_GENERIC_FACTORY_CONSTRUCTOR(nsSessionStoreUtils)
NS_DEFINE_NAMED_CID(NS_SESSIONSTOREUTILS_CID);
static const mozilla::Module::CIDEntry kBrowserCIDs[] = {
{ &kNS_BROWSERDIRECTORYPROVIDER_CID, false, nullptr, DirectoryProviderConstructor },
#if defined(XP_WIN)
@ -74,6 +78,7 @@ static const mozilla::Module::CIDEntry kBrowserCIDs[] = {
#elif defined(XP_MACOSX)
{ &kNS_SHELLSERVICE_CID, false, nullptr, nsMacShellServiceConstructor },
#endif
{ &kNS_SESSIONSTOREUTILS_CID, false, nullptr, nsSessionStoreUtilsConstructor },
{ nullptr }
};
@ -85,6 +90,7 @@ static const mozilla::Module::ContractIDEntry kBrowserContracts[] = {
{ NS_SHELLSERVICE_CONTRACTID, &kNS_SHELLSERVICE_CID },
#endif
{ NS_FEEDSNIFFER_CONTRACTID, &kNS_FEEDSNIFFER_CID },
{ NS_SESSIONSTOREUTILS_CONTRACTID, &kNS_SESSIONSTOREUTILS_CID },
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "blocked", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "certerror", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "socialerror", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },

View File

@ -7,6 +7,7 @@
this.EXPORTED_SYMBOLS = ["ContentRestore"];
const Cu = Components.utils;
const Cc = Components.classes;
const Ci = Components.interfaces;
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
@ -25,6 +26,33 @@ XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage",
XPCOMUtils.defineLazyModuleGetter(this, "Utils",
"resource://gre/modules/sessionstore/Utils.jsm");
const ssu = Cc["@mozilla.org/browser/sessionstore/utils;1"]
.getService(Ci.nsISessionStoreUtils);
/**
* Restores frame tree |data|, starting at the given root |frame|. As the
* function recurses into descendant frames it will call cb(frame, data) for
* each frame it encounters, starting with the given root.
*/
function restoreFrameTreeData(frame, data, cb) {
// Restore data for the root frame.
// The callback can abort by returning false.
if (cb(frame, data) === false) {
return;
}
if (!data.hasOwnProperty("children")) {
return;
}
// Recurse into child frames.
ssu.forEachNonDynamicChildFrame(frame, (subframe, index) => {
if (data.children[index]) {
restoreFrameTreeData(subframe, data.children[index], cb);
}
});
}
/**
* This module implements the content side of session restoration. The chrome
* side is handled by SessionStore.jsm. The functions in this module are called
@ -294,8 +322,20 @@ ContentRestoreInternal.prototype = {
let window = this.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow);
FormData.restoreTree(window, formdata);
ScrollPosition.restoreTree(window, scrollPositions);
// Restore form data.
restoreFrameTreeData(window, formdata, (frame, data) => {
// restore() will return false, and thus abort restoration for the
// current |frame| and its descendants, if |data.url| is given but
// doesn't match the loaded document's URL.
return FormData.restore(frame, data);
});
// Restore scroll data.
restoreFrameTreeData(window, scrollPositions, (frame, data) => {
if (data.scroll) {
ScrollPosition.restore(frame, data.scroll);
}
});
},
/**

View File

@ -1,248 +0,0 @@
/* 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/. */
"use strict";
this.EXPORTED_SYMBOLS = ["FrameTree"];
const Cu = Components.utils;
const Ci = Components.interfaces;
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
const EXPORTED_METHODS = ["addObserver", "contains", "map", "forEach"];
/**
* A FrameTree represents all frames that were reachable when the document
* was loaded. We use this information to ignore frames when collecting
* sessionstore data as we can't currently restore anything for frames that
* have been created dynamically after or at the load event.
*
* @constructor
*/
function FrameTree(chromeGlobal) {
let internal = new FrameTreeInternal(chromeGlobal);
let external = {};
for (let method of EXPORTED_METHODS) {
external[method] = internal[method].bind(internal);
}
return Object.freeze(external);
}
/**
* The internal frame tree API that the public one points to.
*
* @constructor
*/
function FrameTreeInternal(chromeGlobal) {
// A WeakMap that uses frames (DOMWindows) as keys and their initial indices
// in their parents' child lists as values. Suppose we have a root frame with
// three subframes i.e. a page with three iframes. The WeakMap would have
// four entries and look as follows:
//
// root -> 0
// subframe1 -> 0
// subframe2 -> 1
// subframe3 -> 2
//
// Should one of the subframes disappear we will stop collecting data for it
// as |this._frames.has(frame) == false|. All other subframes will maintain
// their initial indices to ensure we can restore frame data appropriately.
this._frames = new WeakMap();
// The Set of observers that will be notified when the frame changes.
this._observers = new Set();
// The chrome global we use to retrieve the current DOMWindow.
this._chromeGlobal = chromeGlobal;
// Register a web progress listener to be notified about new page loads.
let docShell = chromeGlobal.docShell;
let ifreq = docShell.QueryInterface(Ci.nsIInterfaceRequestor);
let webProgress = ifreq.getInterface(Ci.nsIWebProgress);
webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
}
FrameTreeInternal.prototype = {
// Returns the docShell's current global.
get content() {
return this._chromeGlobal.content;
},
/**
* Adds a given observer |obs| to the set of observers that will be notified
* when the frame tree is reset (when a new document starts loading) or
* recollected (when a document finishes loading).
*
* @param obs (object)
*/
addObserver(obs) {
this._observers.add(obs);
},
/**
* Notifies all observers that implement the given |method|.
*
* @param method (string)
*/
notifyObservers(method) {
for (let obs of this._observers) {
if (obs.hasOwnProperty(method)) {
obs[method]();
}
}
},
/**
* Checks whether a given |frame| is contained in the collected frame tree.
* If it is not, this indicates that we should not collect data for it.
*
* @param frame (nsIDOMWindow)
* @return bool
*/
contains(frame) {
return this._frames.has(frame);
},
/**
* Recursively applies the given function |cb| to the stored frame tree. Use
* this method to collect sessionstore data for all reachable frames stored
* in the frame tree.
*
* If a given function |cb| returns a value, it must be an object. It may
* however return "null" to indicate that there is no data to be stored for
* the given frame.
*
* The object returned by |cb| cannot have any property named "children" as
* that is used to store information about subframes in the tree returned
* by |map()| and might be overridden.
*
* @param cb (function)
* @return object
*/
map(cb) {
let frames = this._frames;
function walk(frame) {
let obj = cb(frame) || {};
if (frames.has(frame)) {
let children = [];
Array.forEach(frame.frames, subframe => {
// Don't collect any data if the frame is not contained in the
// initial frame tree. It's a dynamic frame added later.
if (!frames.has(subframe)) {
return;
}
// Retrieve the frame's original position in its parent's child list.
let index = frames.get(subframe);
// Recursively collect data for the current subframe.
let result = walk(subframe, cb);
if (result && Object.keys(result).length) {
children[index] = result;
}
});
if (children.length) {
obj.children = children;
}
}
return Object.keys(obj).length ? obj : null;
}
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(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.
*
* @param frame (nsIDOMWindow)
* @param index (int)
* The index in the given frame's parent's child list.
*/
collect(frame, index = 0) {
// Mark the given frame as contained in the frame tree.
this._frames.set(frame, index);
// Mark the given frame's subframes as contained in the tree.
Array.forEach(frame.frames, this.collect, this);
},
/**
* @see nsIWebProgressListener.onStateChange
*
* We want to be notified about:
* - new documents that start loading to clear the current frame tree;
* - completed document loads to recollect reachable frames.
*/
onStateChange(webProgress, request, stateFlags, status) {
// Ignore state changes for subframes because we're only interested in the
// top-document starting or stopping its load. We thus only care about any
// changes to the root of the frame tree, not to any of its nodes/leafs.
if (!webProgress.isTopLevel || webProgress.DOMWindow != this.content) {
return;
}
// onStateChange will be fired when loading the initial about:blank URI for
// a browser, which we don't actually care about. This is particularly for
// the case of unrestored background tabs, where the content has not yet
// been restored: we don't want to accidentally send any updates to the
// parent when the about:blank placeholder page has loaded.
if (!this._chromeGlobal.docShell.hasLoadedNonBlankURI) {
return;
}
if (stateFlags & Ci.nsIWebProgressListener.STATE_START) {
// Clear the list of frames until we can recollect it.
this._frames = new WeakMap();
// Notify observers that the frame tree has been reset.
this.notifyObservers("onFrameTreeReset");
} else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
// The document and its resources have finished loading.
this.collect(webProgress.DOMWindow);
// Notify observers that the frame tree has been reset.
this.notifyObservers("onFrameTreeCollected");
}
},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
Ci.nsISupportsWeakReference])
};

View File

@ -7,6 +7,7 @@
this.EXPORTED_SYMBOLS = ["SessionStorage"];
const Cu = Components.utils;
const Cc = Components.classes;
const Ci = Components.interfaces;
Cu.import("resource://gre/modules/Services.jsm");
@ -15,6 +16,9 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "console",
"resource://gre/modules/Console.jsm");
const ssu = Cc["@mozilla.org/browser/sessionstore/utils;1"]
.createInstance(Ci.nsISessionStoreUtils);
// A bound to the size of data to store for DOM Storage.
const DOM_STORAGE_LIMIT_PREF = "browser.sessionstore.dom_storage_limit";
@ -54,22 +58,36 @@ this.SessionStorage = Object.freeze({
},
});
/**
* Calls the given callback |cb|, passing |frame| and each of its descendants.
*/
function forEachNonDynamicChildFrame(frame, cb) {
// Call for current frame.
cb(frame);
// Call the callback recursively for each descendant.
ssu.forEachNonDynamicChildFrame(frame, subframe => {
return forEachNonDynamicChildFrame(subframe, cb);
});
}
var SessionStorageInternal = {
/**
* Reads all session storage data from the given docShell.
* @param docShell
* A tab's docshell (containing the sessionStorage)
* @param frameTree
* The docShell's FrameTree instance.
* @param content
* A tab's global, i.e. the root frame we want to collect for.
* @return Returns a nested object that will have hosts as keys and per-origin
* session storage data as strings. For example:
* {"https://example.com^userContextId=1": {"key": "value", "my_number": "123"}}
*/
collect(docShell, frameTree) {
collect(content) {
let data = {};
let visitedOrigins = new Set();
let docShell = content.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell);
frameTree.forEach(frame => {
forEachNonDynamicChildFrame(content, frame => {
let principal = getPrincipalForFrame(docShell, frame);
if (!principal) {
return;

View File

@ -34,13 +34,13 @@ XPCOMUtils.defineLazyModuleGetter(this, "SessionHistory",
XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage",
"resource:///modules/sessionstore/SessionStorage.jsm");
Cu.import("resource:///modules/sessionstore/FrameTree.jsm", this);
var gFrameTree = new FrameTree(this);
Cu.import("resource:///modules/sessionstore/ContentRestore.jsm", this);
XPCOMUtils.defineLazyGetter(this, "gContentRestore",
() => { return new ContentRestore(this) });
const ssu = Cc["@mozilla.org/browser/sessionstore/utils;1"]
.getService(Ci.nsISessionStoreUtils);
// The current epoch.
var gCurrentEpoch = 0;
@ -72,6 +72,98 @@ function createLazy(fn) {
};
}
/**
* A function that will recursively call |cb| to collected data for all
* non-dynamic frames in the current frame/docShell tree.
*/
function mapFrameTree(cb) {
return (function map(frame, cb) {
// Collect data for the current frame.
let obj = cb(frame) || {};
let children = [];
// Recurse into child frames.
ssu.forEachNonDynamicChildFrame(frame, (subframe, index) => {
let result = map(subframe, cb);
if (result && Object.keys(result).length) {
children[index] = result;
}
});
if (children.length) {
obj.children = children;
}
return Object.keys(obj).length ? obj : null;
})(content, cb);
}
/**
* Listens for state change notifcations from webProgress and notifies each
* registered observer for either the start of a page load, or its completion.
*/
var StateChangeNotifier = {
init() {
this._observers = new Set();
let ifreq = docShell.QueryInterface(Ci.nsIInterfaceRequestor);
let webProgress = ifreq.getInterface(Ci.nsIWebProgress);
webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
},
/**
* Adds a given observer |obs| to the set of observers that will be notified
* when when a new document starts or finishes loading.
*
* @param obs (object)
*/
addObserver(obs) {
this._observers.add(obs);
},
/**
* Notifies all observers that implement the given |method|.
*
* @param method (string)
*/
notifyObservers(method) {
for (let obs of this._observers) {
if (obs.hasOwnProperty(method)) {
obs[method]();
}
}
},
/**
* @see nsIWebProgressListener.onStateChange
*/
onStateChange(webProgress, request, stateFlags, status) {
// Ignore state changes for subframes because we're only interested in the
// top-document starting or stopping its load.
if (!webProgress.isTopLevel || webProgress.DOMWindow != content) {
return;
}
// onStateChange will be fired when loading the initial about:blank URI for
// a browser, which we don't actually care about. This is particularly for
// the case of unrestored background tabs, where the content has not yet
// been restored: we don't want to accidentally send any updates to the
// parent when the about:blank placeholder page has loaded.
if (!docShell.hasLoadedNonBlankURI) {
return;
}
if (stateFlags & Ci.nsIWebProgressListener.STATE_START) {
this.notifyObservers("onPageLoadStarted");
} else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
this.notifyObservers("onPageLoadCompleted");
}
},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
Ci.nsISupportsWeakReference])
};
/**
* Listens for and handles content events that we need for the
* session store service to be notified of state changes in content.
@ -79,7 +171,7 @@ function createLazy(fn) {
var EventListener = {
init() {
addEventListener("load", this, true);
addEventListener("load", ssu.createDynamicFrameEventFilter(this), true);
},
handleEvent(event) {
@ -248,10 +340,10 @@ var MessageListener = {
*/
var SessionHistoryListener = {
init() {
// The frame tree observer is needed to handle initial subframe loads.
// The state change observer is needed to handle initial subframe loads.
// It will redundantly invalidate with the SHistoryListener in some cases
// but these invalidations are very cheap.
gFrameTree.addObserver(this);
StateChangeNotifier.addObserver(this);
// By adding the SHistoryListener immediately, we will unfortunately be
// notified of every history entry as the tab is restored. We don't bother
@ -329,11 +421,11 @@ var SessionHistoryListener = {
this.collect();
},
onFrameTreeCollected() {
onPageLoadCompleted() {
this.collect();
},
onFrameTreeReset() {
onPageLoadStarted() {
this.collect();
},
@ -402,30 +494,24 @@ var SessionHistoryListener = {
*/
var ScrollPositionListener = {
init() {
addEventListener("scroll", this);
gFrameTree.addObserver(this);
addEventListener("scroll", ssu.createDynamicFrameEventFilter(this));
StateChangeNotifier.addObserver(this);
},
handleEvent(event) {
let frame = event.target.defaultView;
// Don't collect scroll data for frames created at or after the load event
// as SessionStore can't restore scroll data for those.
if (gFrameTree.contains(frame)) {
MessageQueue.push("scroll", () => this.collect());
}
},
onFrameTreeCollected() {
handleEvent() {
MessageQueue.push("scroll", () => this.collect());
},
onFrameTreeReset() {
onPageLoadCompleted() {
MessageQueue.push("scroll", () => this.collect());
},
onPageLoadStarted() {
MessageQueue.push("scroll", () => null);
},
collect() {
return gFrameTree.map(ScrollPosition.collect);
return mapFrameTree(ScrollPosition.collect);
}
};
@ -448,26 +534,20 @@ var ScrollPositionListener = {
*/
var FormDataListener = {
init() {
addEventListener("input", this, true);
gFrameTree.addObserver(this);
addEventListener("input", ssu.createDynamicFrameEventFilter(this), true);
StateChangeNotifier.addObserver(this);
},
handleEvent(event) {
let frame = event.target.ownerGlobal;
// Don't collect form data for frames created at or after the load event
// as SessionStore can't restore form data for those.
if (gFrameTree.contains(frame)) {
MessageQueue.push("formdata", () => this.collect());
}
handleEvent() {
MessageQueue.push("formdata", () => this.collect());
},
onFrameTreeReset() {
onPageLoadStarted() {
MessageQueue.push("formdata", () => null);
},
collect() {
return gFrameTree.map(FormData.collect);
return mapFrameTree(FormData.collect);
}
};
@ -488,13 +568,10 @@ var DocShellCapabilitiesListener = {
_latestCapabilities: "",
init() {
gFrameTree.addObserver(this);
StateChangeNotifier.addObserver(this);
},
/**
* onFrameTreeReset() is called as soon as we start loading a page.
*/
onFrameTreeReset() {
onPageLoadStarted() {
// The order of docShell capabilities cannot change while we're running
// so calling join() without sorting before is totally sufficient.
let caps = DocShellCapabilities.collect(docShell).join(",");
@ -518,21 +595,16 @@ var DocShellCapabilitiesListener = {
*/
var SessionStorageListener = {
init() {
addEventListener("MozSessionStorageChanged", this, true);
let filter = ssu.createDynamicFrameEventFilter(this);
addEventListener("MozSessionStorageChanged", filter, true);
Services.obs.addObserver(this, "browser:purge-domain-data");
gFrameTree.addObserver(this);
StateChangeNotifier.addObserver(this);
},
uninit() {
Services.obs.removeObserver(this, "browser:purge-domain-data");
},
handleEvent(event) {
if (gFrameTree.contains(event.target)) {
this.collectFromEvent(event);
}
},
observe() {
// Collect data on the next tick so that any other observer
// that needs to purge data can do its work first.
@ -550,7 +622,7 @@ var SessionStorageListener = {
this._changes = undefined;
},
collectFromEvent(event) {
handleEvent(event) {
if (!docShell) {
return;
}
@ -598,16 +670,14 @@ var SessionStorageListener = {
// messages.
this.resetChanges();
MessageQueue.push("storage", () => {
return SessionStorage.collect(docShell, gFrameTree);
});
MessageQueue.push("storage", () => SessionStorage.collect(content));
},
onFrameTreeCollected() {
onPageLoadCompleted() {
this.collect();
},
onFrameTreeReset() {
onPageLoadStarted() {
this.collect();
}
};
@ -795,6 +865,7 @@ var MessageQueue = {
},
};
StateChangeNotifier.init();
EventListener.init();
MessageListener.init();
FormDataListener.init();
@ -849,7 +920,7 @@ addEventListener("unload", () => {
// Remove progress listeners.
gContentRestore.resetRestore();
// We don't need to take care of any gFrameTree observers as the gFrameTree
// We don't need to take care of any StateChangeNotifier observers as they
// will die with the content script. The same goes for the privacy transition
// observer that will die with the docShell when the tab is closed.
});

View File

@ -12,6 +12,7 @@ JAR_MANIFESTS += ['jar.mn']
XPIDL_SOURCES += [
'nsISessionStartup.idl',
'nsISessionStore.idl',
'nsISessionStoreUtils.idl',
]
XPIDL_MODULE = 'sessionstore'
@ -25,7 +26,6 @@ EXTRA_COMPONENTS += [
EXTRA_JS_MODULES.sessionstore = [
'ContentRestore.jsm',
'DocShellCapabilities.jsm',
'FrameTree.jsm',
'GlobalState.jsm',
'PrivacyFilter.jsm',
'RecentlyClosedTabsAndWindowsMenuUtils.jsm',
@ -45,5 +45,11 @@ EXTRA_JS_MODULES.sessionstore = [
'TabStateFlusher.jsm',
]
UNIFIED_SOURCES += [
'nsSessionStoreUtils.cpp',
]
FINAL_LIBRARY = 'browsercomps'
with Files('**'):
BUG_COMPONENT = ('Firefox', 'Session Restore')

View File

@ -0,0 +1,46 @@
/* 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/. */
#include "nsISupports.idl"
interface mozIDOMWindowProxy;
interface nsIDOMEventListener;
/**
* A callback passed to nsISessionStoreUtils.forEachNonDynamicChildFrame().
*/
[function, scriptable, uuid(8199ebf7-76c0-43d6-bcbe-913dd3de3ebf)]
interface nsISessionStoreUtilsFrameCallback : nsISupports
{
/**
* handleFrame() will be called once for each non-dynamic child frame of the
* given parent |frame|. The second argument is the |index| of the frame in
* the list of all child frames.
*/
void handleFrame(in mozIDOMWindowProxy frame, in unsigned long index);
};
/**
* SessionStore utility functions implemented in C++ for performance reasons.
*/
[scriptable, uuid(2be448ef-c783-45de-a0df-442bccbb4532)]
interface nsISessionStoreUtils : nsISupports
{
/**
* Calls the given |callback| once for each non-dynamic child frame of the
* given |window|.
*/
void forEachNonDynamicChildFrame(in mozIDOMWindowProxy window,
in nsISessionStoreUtilsFrameCallback callback);
/**
* Creates and returns an event listener that filters events from dynamic
* docShells. It forwards those from non-dynamic docShells to the given
* |listener|.
*
* This is implemented as a native filter, rather than a JS-based one, for
* performance reasons.
*/
nsIDOMEventListener createDynamicFrameEventFilter(in nsIDOMEventListener listener);
};

View File

@ -0,0 +1,122 @@
/* 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/. */
#include "nsSessionStoreUtils.h"
#include "mozilla/dom/Event.h"
#include "nsPIDOMWindow.h"
#include "nsIDocShell.h"
using namespace mozilla::dom;
namespace {
class DynamicFrameEventFilter final : public nsIDOMEventListener
{
public:
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
NS_DECL_CYCLE_COLLECTION_CLASS(DynamicFrameEventFilter)
DynamicFrameEventFilter(nsIDOMEventListener* aListener)
: mListener(aListener)
{ }
NS_IMETHODIMP HandleEvent(nsIDOMEvent* aEvent) override
{
if (mListener && TargetInNonDynamicDocShell(aEvent)) {
mListener->HandleEvent(aEvent);
}
return NS_OK;
}
private:
~DynamicFrameEventFilter() { }
bool TargetInNonDynamicDocShell(nsIDOMEvent* aEvent)
{
EventTarget* target = aEvent->InternalDOMEvent()->GetTarget();
if (!target) {
return false;
}
nsPIDOMWindowOuter* outer = target->GetOwnerGlobalForBindings();
if (!outer) {
return false;
}
nsIDocShell* docShell = outer->GetDocShell();
if (!docShell) {
return false;
}
bool isDynamic = false;
nsresult rv = docShell->GetCreatedDynamically(&isDynamic);
return NS_SUCCEEDED(rv) && !isDynamic;
}
nsCOMPtr<nsIDOMEventListener> mListener;
};
NS_IMPL_CYCLE_COLLECTION(DynamicFrameEventFilter, mListener)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DynamicFrameEventFilter)
NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
NS_INTERFACE_MAP_END
NS_IMPL_CYCLE_COLLECTING_ADDREF(DynamicFrameEventFilter)
NS_IMPL_CYCLE_COLLECTING_RELEASE(DynamicFrameEventFilter)
} // anonymous namespace
NS_IMPL_ISUPPORTS(nsSessionStoreUtils, nsISessionStoreUtils)
NS_IMETHODIMP
nsSessionStoreUtils::ForEachNonDynamicChildFrame(mozIDOMWindowProxy* aWindow,
nsISessionStoreUtilsFrameCallback* aCallback)
{
NS_ENSURE_TRUE(aWindow, NS_ERROR_INVALID_ARG);
nsCOMPtr<nsPIDOMWindowOuter> outer = nsPIDOMWindowOuter::From(aWindow);
NS_ENSURE_TRUE(outer, NS_ERROR_FAILURE);
nsCOMPtr<nsIDocShell> docShell = outer->GetDocShell();
NS_ENSURE_TRUE(docShell, NS_ERROR_FAILURE);
int32_t length;
nsresult rv = docShell->GetChildCount(&length);
NS_ENSURE_SUCCESS(rv, rv);
for (int32_t i = 0, idx = 0; i < length; ++i) {
nsCOMPtr<nsIDocShellTreeItem> item;
docShell->GetChildAt(i, getter_AddRefs(item));
NS_ENSURE_TRUE(item, NS_ERROR_FAILURE);
nsCOMPtr<nsIDocShell> childDocShell(do_QueryInterface(item));
NS_ENSURE_TRUE(childDocShell, NS_ERROR_FAILURE);
bool isDynamic = false;
nsresult rv = childDocShell->GetCreatedDynamically(&isDynamic);
if (NS_SUCCEEDED(rv) && isDynamic) {
continue;
}
aCallback->HandleFrame(item->GetWindow(), idx++);
}
return NS_OK;
}
NS_IMETHODIMP
nsSessionStoreUtils::CreateDynamicFrameEventFilter(nsIDOMEventListener* aListener,
nsIDOMEventListener** aResult)
{
NS_ENSURE_TRUE(aListener, NS_ERROR_INVALID_ARG);
nsCOMPtr<nsIDOMEventListener> filter(new DynamicFrameEventFilter(aListener));
filter.forget(aResult);
return NS_OK;
}

View File

@ -0,0 +1,29 @@
/* 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/. */
#ifndef nsSessionStoreUtils_h
#define nsSessionStoreUtils_h
#include "nsCycleCollectionParticipant.h"
#include "nsISessionStoreUtils.h"
#include "nsIDOMEventListener.h"
#include "nsCOMPtr.h"
#define NS_SESSIONSTOREUTILS_CID \
{0xd713b4be, 0x8285, 0x4cab, {0x9c, 0x0e, 0x0b, 0xbc, 0x38, 0xbf, 0xb9, 0x3c}}
#define NS_SESSIONSTOREUTILS_CONTRACTID \
"@mozilla.org/browser/sessionstore/utils;1"
class nsSessionStoreUtils final : public nsISessionStoreUtils
{
public:
NS_DECL_NSISESSIONSTOREUTILS
NS_DECL_ISUPPORTS
private:
~nsSessionStoreUtils() { }
};
#endif // nsSessionStoreUtils_h

View File

@ -17,6 +17,7 @@ support-files =
browser_formdata_xpath_sample.html
browser_frametree_sample.html
browser_frametree_sample_frameset.html
browser_frametree_sample_iframes.html
browser_frame_history_index.html
browser_frame_history_index2.html
browser_frame_history_index_blank.html

View File

@ -6,14 +6,14 @@
</head>
<body>
<input id="txt" />
<iframe id="iframe"></iframe>
<script type="text/javascript">
let isOuter = window == window.top;
if (isOuter) {
let iframe = document.createElement("iframe");
let iframe = document.getElementById("iframe");
iframe.setAttribute("src", "https://example.com" + location.pathname);
document.body.appendChild(iframe);
}
</script>
</body>

View File

@ -5,127 +5,111 @@
const URL = HTTPROOT + "browser_frametree_sample.html";
const URL_FRAMESET = HTTPROOT + "browser_frametree_sample_frameset.html";
const URL_IFRAMES = HTTPROOT + "browser_frametree_sample_iframes.html";
/**
* This ensures that loading a page normally, aborting a page load, reloading
* a page, navigating using the bfcache, and ignoring frames that were
* created dynamically work as expect. We expect the frame tree to be reset
* when a page starts loading and we also expect a valid frame tree to exist
* when it has stopped loading.
* Check that we correctly enumerate non-dynamic child frames.
*/
add_task(async function test_frametree() {
const FRAME_TREE_SINGLE = { href: URL };
const FRAME_TREE_FRAMESET = {
href: URL_FRAMESET,
children: [{href: URL}, {href: URL}, {href: URL}]
};
// Create a tab with a single frame.
let tab = BrowserTestUtils.addTab(gBrowser, URL);
let browser = tab.linkedBrowser;
await promiseNewFrameTree(browser);
await checkFrameTree(browser, FRAME_TREE_SINGLE,
"loading a page resets and creates the frame tree correctly");
// Load the frameset and create two frames dynamically, the first on
// DOMContentLoaded and the second on load.
await sendMessage(browser, "ss-test:createDynamicFrames", {id: "frames", url: URL});
browser.loadURI(URL_FRAMESET);
await promiseNewFrameTree(browser);
await checkFrameTree(browser, FRAME_TREE_FRAMESET,
"dynamic frames created on or after the load event are ignored");
// Go back to the previous single-frame page. There will be no load event as
// the page is still in the bfcache. We thus make sure this type of navigation
// resets the frame tree.
browser.goBack();
await promiseNewFrameTree(browser);
await checkFrameTree(browser, FRAME_TREE_SINGLE,
"loading from bfache resets and creates the frame tree correctly");
// Load the frameset again but abort the load early.
// The frame tree should still be reset and created.
browser.loadURI(URL_FRAMESET);
executeSoon(() => browser.stop());
await promiseNewFrameTree(browser);
// Load the frameset and check the tree again.
await sendMessage(browser, "ss-test:createDynamicFrames", {id: "frames", url: URL});
browser.loadURI(URL_FRAMESET);
await promiseNewFrameTree(browser);
await checkFrameTree(browser, FRAME_TREE_FRAMESET,
"reloading a page resets and creates the frame tree correctly");
// Cleanup.
gBrowser.removeTab(tab);
});
/**
* This test ensures that we ignore frames that were created dynamically at or
* after the load event. SessionStore can't handle these and will not restore
* or collect any data for them.
*/
add_task(async function test_frametree_dynamic() {
// The frame tree as expected. The first two frames are static
// and the third one was created on DOMContentLoaded.
const FRAME_TREE = {
href: URL_FRAMESET,
children: [{href: URL}, {href: URL}, {href: URL}]
};
const FRAME_TREE_REMOVED = {
href: URL_FRAMESET,
children: [{href: URL}, {href: URL}]
};
// Add an empty tab for a start.
let tab = BrowserTestUtils.addTab(gBrowser, "about:blank");
let tab = BrowserTestUtils.addTab(gBrowser, URL);
let browser = tab.linkedBrowser;
await promiseBrowserLoaded(browser);
// Create dynamic frames on "DOMContentLoaded" and on "load".
await sendMessage(browser, "ss-test:createDynamicFrames", {id: "frames", url: URL});
// The page is a single frame with no children.
is(await countNonDynamicFrames(browser), 0, "no child frames");
// Navigate to a frameset.
browser.loadURI(URL_FRAMESET);
await promiseNewFrameTree(browser);
await promiseBrowserLoaded(browser);
// Check that the frame tree does not contain the frame created on "load".
// The two static frames and the one created on DOMContentLoaded must be in
// the tree.
await checkFrameTree(browser, FRAME_TREE,
"frame tree contains first four frames");
// The frameset has two frames.
is(await countNonDynamicFrames(browser), 2, "two non-dynamic child frames");
// Remove the last frame in the frameset.
await sendMessage(browser, "ss-test:removeLastFrame", {id: "frames"});
// Check that the frame tree didn't change.
await checkFrameTree(browser, FRAME_TREE,
"frame tree contains first four frames");
// Go back in history.
let pageShowPromise = ContentTask.spawn(browser, null, async () => {
return ContentTaskUtils.waitForEvent(this, "pageshow", true);
});
browser.goBack();
await pageShowPromise;
// Remove the last frame in the frameset.
await sendMessage(browser, "ss-test:removeLastFrame", {id: "frames"});
// Check that the frame tree excludes the removed frame.
await checkFrameTree(browser, FRAME_TREE_REMOVED,
"frame tree contains first three frames");
// We're at page one again.
is(await countNonDynamicFrames(browser), 0, "no child frames");
// Append a dynamic frame.
await ContentTask.spawn(browser, URL, async ([url]) => {
let frame = content.document.createElement("iframe");
frame.setAttribute("src", url);
content.document.body.appendChild(frame);
return ContentTaskUtils.waitForEvent(frame, "load");
});
// The dynamic frame should be ignored.
is(await countNonDynamicFrames(browser), 0, "we still have a single root frame");
// Cleanup.
gBrowser.removeTab(tab);
await promiseRemoveTab(tab);
});
/**
* Checks whether the current frame hierarchy of a given |browser| matches the
* |expected| frame hierarchy.
* Check that we correctly enumerate non-dynamic child frames.
*/
function checkFrameTree(browser, expected, msg) {
return sendMessage(browser, "ss-test:mapFrameTree").then(tree => {
is(JSON.stringify(tree), JSON.stringify(expected), msg);
add_task(async function test_frametree_dynamic() {
// Add an empty tab for a start.
let tab = BrowserTestUtils.addTab(gBrowser, URL_IFRAMES);
let browser = tab.linkedBrowser;
await promiseBrowserLoaded(browser);
// The page has two iframes.
is(await countNonDynamicFrames(browser), 2, "two non-dynamic child frames");
is(await enumerateIndexes(browser), "0,1", "correct indexes 0 and 1");
// Insert a dynamic frame.
await ContentTask.spawn(browser, URL, async ([url]) => {
let frame = content.document.createElement("iframe");
frame.setAttribute("src", url);
content.document.body.insertBefore(frame, content.document.getElementsByTagName("iframe")[1]);
return ContentTaskUtils.waitForEvent(frame, "load");
});
// The page still has two iframes.
is(await countNonDynamicFrames(browser), 2, "two non-dynamic child frames");
is(await enumerateIndexes(browser), "0,1", "correct indexes 0 and 1");
// Append a dynamic frame.
await ContentTask.spawn(browser, URL, async ([url]) => {
let frame = content.document.createElement("iframe");
frame.setAttribute("src", url);
content.document.body.appendChild(frame);
return ContentTaskUtils.waitForEvent(frame, "load");
});
// The page still has two iframes.
is(await countNonDynamicFrames(browser), 2, "two non-dynamic child frames");
is(await enumerateIndexes(browser), "0,1", "correct indexes 0 and 1");
// Cleanup.
await promiseRemoveTab(tab);
});
async function countNonDynamicFrames(browser) {
return await ContentTask.spawn(browser, null, async () => {
const ssu = Cc["@mozilla.org/browser/sessionstore/utils;1"]
.getService(Ci.nsISessionStoreUtils);
let count = 0;
ssu.forEachNonDynamicChildFrame(content, () => count++);
return count;
});
}
/**
* Returns a promise that will be resolved when the given |browser| has loaded
* and we received messages saying that its frame tree has been reset and
* recollected.
*/
function promiseNewFrameTree(browser) {
let reset = promiseContentMessage(browser, "ss-test:onFrameTreeCollected");
let collect = promiseContentMessage(browser, "ss-test:onFrameTreeCollected");
return Promise.all([reset, collect]);
async function enumerateIndexes(browser) {
return await ContentTask.spawn(browser, null, async () => {
const ssu = Cc["@mozilla.org/browser/sessionstore/utils;1"]
.getService(Ci.nsISessionStoreUtils);
let indexes = [];
ssu.forEachNonDynamicChildFrame(content, (frame, i) => indexes.push(i));
return indexes.join(",");
});
}

View File

@ -0,0 +1,9 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN">
<html lang="en">
<head>
<meta charset="utf-8">
<title>browser_frametree_sample_iframes.html</title>
</head>
<iframe src="browser_frametree_sample.html"></iframe>
<iframe src="browser_frametree_sample.html"></iframe>
</html>

View File

@ -78,9 +78,14 @@ add_task(async function test_pageshow() {
browser.loadURI(URL2);
await promiseBrowserLoaded(browser);
// Wait until shistory changes.
let pageShowPromise = ContentTask.spawn(browser, null, async () => {
return ContentTaskUtils.waitForEvent(this, "pageshow", true);
});
// Go back to the previous url which is loaded from the bfcache.
browser.goBack();
await promiseContentMessage(browser, "ss-test:onFrameTreeCollected");
await pageShowPromise;
is(browser.currentURI.spec, URL, "correct url after going back");
// Check that loading from bfcache did invalidate shistory.

View File

@ -5,17 +5,18 @@
<title>browser_sessionStorage.html</title>
</head>
<body>
<iframe id="iframe"></iframe>
<script type="text/javascript">
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 iframe = document.getElementById("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) {

View File

@ -10,23 +10,11 @@ var Cu = Components.utils;
var Ci = Components.interfaces;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource:///modules/sessionstore/FrameTree.jsm", this);
var gFrameTree = new FrameTree(this);
function executeSoon(callback) {
Services.tm.dispatchToMainThread(callback);
}
gFrameTree.addObserver({
onFrameTreeReset() {
sendAsyncMessage("ss-test:onFrameTreeReset");
},
onFrameTreeCollected() {
sendAsyncMessage("ss-test:onFrameTreeCollected");
}
});
var historyListener = {
OnHistoryNewEntry() {
sendAsyncMessage("ss-test:OnHistoryNewEntry");
@ -171,48 +159,6 @@ addMessageListener("ss-test:setScrollPosition", function(msg) {
});
});
addMessageListener("ss-test:createDynamicFrames", function({data}) {
function createIFrame(rows) {
let frames = content.document.getElementById(data.id);
frames.setAttribute("rows", rows);
let frame = content.document.createElement("frame");
frame.setAttribute("src", data.url);
frames.appendChild(frame);
}
addEventListener("DOMContentLoaded", function onContentLoaded(event) {
if (content.document == event.target) {
removeEventListener("DOMContentLoaded", onContentLoaded, true);
// DOMContentLoaded is fired right after we finished parsing the document.
createIFrame("33%, 33%, 33%");
}
}, true);
addEventListener("load", function onLoad(event) {
if (content.document == event.target) {
removeEventListener("load", onLoad, true);
// Creating this frame on the same tick as the load event
// means that it must not be included in the frame tree.
createIFrame("25%, 25%, 25%, 25%");
}
}, true);
sendAsyncMessage("ss-test:createDynamicFrames");
});
addMessageListener("ss-test:removeLastFrame", function({data}) {
let frames = content.document.getElementById(data.id);
frames.lastElementChild.remove();
sendAsyncMessage("ss-test:removeLastFrame");
});
addMessageListener("ss-test:mapFrameTree", function(msg) {
let result = gFrameTree.map(frame => ({href: frame.location.href}));
sendAsyncMessage("ss-test:mapFrameTree", result);
});
addMessageListener("ss-test:click", function({data}) {
content.document.getElementById(data.id).click();
sendAsyncMessage("ss-test:click");

View File

@ -2412,6 +2412,10 @@ nsFrameLoader::MaybeCreateDocShell()
mIsTopLevelContent =
AddTreeItemToTreeOwner(mDocShell, parentTreeOwner, parentType, docShell);
if (mIsTopLevelContent) {
mDocShell->SetCreatedDynamically(false);
}
// Make sure all shells have links back to the content element
// in the nearest enclosing chrome shell.
nsCOMPtr<nsIDOMEventTarget> chromeEventHandler;

View File

@ -101,6 +101,10 @@ this.FormData = Object.freeze({
return FormDataInternal.collect(frame);
},
restore(frame, data) {
return FormDataInternal.restore(frame, data);
},
restoreTree(root, data) {
FormDataInternal.restoreTree(root, data);
}
@ -286,10 +290,14 @@ var FormDataInternal = {
* An object holding form data.
*/
restore({document: doc}, data) {
if (!data.url) {
return;
}
// Don't restore any data for the given frame if the URL
// stored in the form data doesn't match its current URL.
if (!data.url || data.url != getDocumentURI(doc)) {
return;
if (data.url != getDocumentURI(doc)) {
return false;
}
// For about:{sessionrestore,welcomeback} we saved the field as JSON to
@ -441,16 +449,13 @@ var FormDataInternal = {
* }
*/
restoreTree(root, data) {
// Don't restore any data for the root frame and its subframes if there
// is a URL stored in the form data and it doesn't match its current URL.
if (data.url && data.url != getDocumentURI(root.document)) {
// Restore data for the given |root| frame and its descendants. If restore()
// returns false this indicates the |data.url| doesn't match the loaded
// document URI. We then must ignore this branch for security reasons.
if (this.restore(root, data) === false) {
return;
}
if (data.url) {
this.restore(root, data);
}
if (!data.hasOwnProperty("children")) {
return;
}

View File

@ -19,6 +19,10 @@ this.ScrollPosition = Object.freeze({
return ScrollPositionInternal.collect(frame);
},
restore(frame, value) {
ScrollPositionInternal.restore(frame, value);
},
restoreTree(root, data) {
ScrollPositionInternal.restoreTree(root, data);
}