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