/* 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]) };