mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-19 08:15:31 +00:00
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:
parent
9975089d4d
commit
36e075eddf
@ -23,5 +23,6 @@ LOCAL_INCLUDES += [
|
||||
'../dirprovider',
|
||||
'../feeds',
|
||||
'../migration',
|
||||
'../sessionstore',
|
||||
'../shell',
|
||||
]
|
||||
|
@ -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 },
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -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])
|
||||
};
|
@ -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;
|
||||
|
@ -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.
|
||||
});
|
||||
|
@ -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')
|
||||
|
46
browser/components/sessionstore/nsISessionStoreUtils.idl
Normal file
46
browser/components/sessionstore/nsISessionStoreUtils.idl
Normal 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);
|
||||
};
|
122
browser/components/sessionstore/nsSessionStoreUtils.cpp
Normal file
122
browser/components/sessionstore/nsSessionStoreUtils.cpp
Normal 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;
|
||||
}
|
29
browser/components/sessionstore/nsSessionStoreUtils.h
Normal file
29
browser/components/sessionstore/nsSessionStoreUtils.h
Normal 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
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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(",");
|
||||
});
|
||||
}
|
||||
|
@ -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>
|
@ -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.
|
||||
|
@ -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) {
|
||||
|
@ -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");
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user