summaryrefslogtreecommitdiffstats
path: root/browser/components/sessionstore/FrameTree.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/sessionstore/FrameTree.jsm')
-rw-r--r--browser/components/sessionstore/FrameTree.jsm254
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])
+};