diff options
Diffstat (limited to 'browser/components/sessionstore/FrameTree.jsm')
-rw-r--r-- | browser/components/sessionstore/FrameTree.jsm | 254 |
1 files changed, 254 insertions, 0 deletions
diff --git a/browser/components/sessionstore/FrameTree.jsm b/browser/components/sessionstore/FrameTree.jsm new file mode 100644 index 000000000..e8ed12a8f --- /dev/null +++ b/browser/components/sessionstore/FrameTree.jsm @@ -0,0 +1,254 @@ +/* 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: function (obs) { + this._observers.add(obs); + }, + + /** + * Notifies all observers that implement the given |method|. + * + * @param method (string) + */ + notifyObservers: function (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: function (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: function (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: function (cb) { + let frames = this._frames; + + function walk(frame) { + cb(frame); + + if (!frames.has(frame)) { + return; + } + + Array.forEach(frame.frames, subframe => { + if (frames.has(subframe)) { + cb(subframe); + } + }); + } + + walk(this.content); + }, + + /** + * Stores a given |frame| and its children in the frame tree. + * + * @param frame (nsIDOMWindow) + * @param index (int) + * The index in the given frame's parent's child list. + */ + collect: function (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: function (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"); + } + }, + + // Unused nsIWebProgressListener methods. + onLocationChange: function () {}, + onProgressChange: function () {}, + onSecurityChange: function () {}, + onStatusChange: function () {}, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, + Ci.nsISupportsWeakReference]) +}; |