summaryrefslogtreecommitdiffstats
path: root/browser/components/sessionstore/FrameTree.jsm
blob: e8ed12a8fff0d571934d63daa9343e19748dfe43 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
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])
};