|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
|
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 this.EXPORTED_SYMBOLS = ["FrameTree"]; |
|
8 |
|
9 const Cu = Components.utils; |
|
10 const Ci = Components.interfaces; |
|
11 |
|
12 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); |
|
13 |
|
14 const EXPORTED_METHODS = ["addObserver", "contains", "map", "forEach"]; |
|
15 |
|
16 /** |
|
17 * A FrameTree represents all frames that were reachable when the document |
|
18 * was loaded. We use this information to ignore frames when collecting |
|
19 * sessionstore data as we can't currently restore anything for frames that |
|
20 * have been created dynamically after or at the load event. |
|
21 * |
|
22 * @constructor |
|
23 */ |
|
24 function FrameTree(chromeGlobal) { |
|
25 let internal = new FrameTreeInternal(chromeGlobal); |
|
26 let external = {}; |
|
27 |
|
28 for (let method of EXPORTED_METHODS) { |
|
29 external[method] = internal[method].bind(internal); |
|
30 } |
|
31 |
|
32 return Object.freeze(external); |
|
33 } |
|
34 |
|
35 /** |
|
36 * The internal frame tree API that the public one points to. |
|
37 * |
|
38 * @constructor |
|
39 */ |
|
40 function FrameTreeInternal(chromeGlobal) { |
|
41 // A WeakMap that uses frames (DOMWindows) as keys and their initial indices |
|
42 // in their parents' child lists as values. Suppose we have a root frame with |
|
43 // three subframes i.e. a page with three iframes. The WeakMap would have |
|
44 // four entries and look as follows: |
|
45 // |
|
46 // root -> 0 |
|
47 // subframe1 -> 0 |
|
48 // subframe2 -> 1 |
|
49 // subframe3 -> 2 |
|
50 // |
|
51 // Should one of the subframes disappear we will stop collecting data for it |
|
52 // as |this._frames.has(frame) == false|. All other subframes will maintain |
|
53 // their initial indices to ensure we can restore frame data appropriately. |
|
54 this._frames = new WeakMap(); |
|
55 |
|
56 // The Set of observers that will be notified when the frame changes. |
|
57 this._observers = new Set(); |
|
58 |
|
59 // The chrome global we use to retrieve the current DOMWindow. |
|
60 this._chromeGlobal = chromeGlobal; |
|
61 |
|
62 // Register a web progress listener to be notified about new page loads. |
|
63 let docShell = chromeGlobal.docShell; |
|
64 let ifreq = docShell.QueryInterface(Ci.nsIInterfaceRequestor); |
|
65 let webProgress = ifreq.getInterface(Ci.nsIWebProgress); |
|
66 webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT); |
|
67 } |
|
68 |
|
69 FrameTreeInternal.prototype = { |
|
70 |
|
71 // Returns the docShell's current global. |
|
72 get content() { |
|
73 return this._chromeGlobal.content; |
|
74 }, |
|
75 |
|
76 /** |
|
77 * Adds a given observer |obs| to the set of observers that will be notified |
|
78 * when the frame tree is reset (when a new document starts loading) or |
|
79 * recollected (when a document finishes loading). |
|
80 * |
|
81 * @param obs (object) |
|
82 */ |
|
83 addObserver: function (obs) { |
|
84 this._observers.add(obs); |
|
85 }, |
|
86 |
|
87 /** |
|
88 * Notifies all observers that implement the given |method|. |
|
89 * |
|
90 * @param method (string) |
|
91 */ |
|
92 notifyObservers: function (method) { |
|
93 for (let obs of this._observers) { |
|
94 if (obs.hasOwnProperty(method)) { |
|
95 obs[method](); |
|
96 } |
|
97 } |
|
98 }, |
|
99 |
|
100 /** |
|
101 * Checks whether a given |frame| is contained in the collected frame tree. |
|
102 * If it is not, this indicates that we should not collect data for it. |
|
103 * |
|
104 * @param frame (nsIDOMWindow) |
|
105 * @return bool |
|
106 */ |
|
107 contains: function (frame) { |
|
108 return this._frames.has(frame); |
|
109 }, |
|
110 |
|
111 /** |
|
112 * Recursively applies the given function |cb| to the stored frame tree. Use |
|
113 * this method to collect sessionstore data for all reachable frames stored |
|
114 * in the frame tree. |
|
115 * |
|
116 * If a given function |cb| returns a value, it must be an object. It may |
|
117 * however return "null" to indicate that there is no data to be stored for |
|
118 * the given frame. |
|
119 * |
|
120 * The object returned by |cb| cannot have any property named "children" as |
|
121 * that is used to store information about subframes in the tree returned |
|
122 * by |map()| and might be overridden. |
|
123 * |
|
124 * @param cb (function) |
|
125 * @return object |
|
126 */ |
|
127 map: function (cb) { |
|
128 let frames = this._frames; |
|
129 |
|
130 function walk(frame) { |
|
131 let obj = cb(frame) || {}; |
|
132 |
|
133 if (frames.has(frame)) { |
|
134 let children = []; |
|
135 |
|
136 Array.forEach(frame.frames, subframe => { |
|
137 // Don't collect any data if the frame is not contained in the |
|
138 // initial frame tree. It's a dynamic frame added later. |
|
139 if (!frames.has(subframe)) { |
|
140 return; |
|
141 } |
|
142 |
|
143 // Retrieve the frame's original position in its parent's child list. |
|
144 let index = frames.get(subframe); |
|
145 |
|
146 // Recursively collect data for the current subframe. |
|
147 let result = walk(subframe, cb); |
|
148 if (result && Object.keys(result).length) { |
|
149 children[index] = result; |
|
150 } |
|
151 }); |
|
152 |
|
153 if (children.length) { |
|
154 obj.children = children; |
|
155 } |
|
156 } |
|
157 |
|
158 return Object.keys(obj).length ? obj : null; |
|
159 } |
|
160 |
|
161 return walk(this.content); |
|
162 }, |
|
163 |
|
164 /** |
|
165 * Applies the given function |cb| to all frames stored in the tree. Use this |
|
166 * method if |map()| doesn't suit your needs and you want more control over |
|
167 * how data is collected. |
|
168 * |
|
169 * @param cb (function) |
|
170 * This callback receives the current frame as the only argument. |
|
171 */ |
|
172 forEach: function (cb) { |
|
173 let frames = this._frames; |
|
174 |
|
175 function walk(frame) { |
|
176 cb(frame); |
|
177 |
|
178 if (!frames.has(frame)) { |
|
179 return; |
|
180 } |
|
181 |
|
182 Array.forEach(frame.frames, subframe => { |
|
183 if (frames.has(subframe)) { |
|
184 cb(subframe); |
|
185 } |
|
186 }); |
|
187 } |
|
188 |
|
189 walk(this.content); |
|
190 }, |
|
191 |
|
192 /** |
|
193 * Stores a given |frame| and its children in the frame tree. |
|
194 * |
|
195 * @param frame (nsIDOMWindow) |
|
196 * @param index (int) |
|
197 * The index in the given frame's parent's child list. |
|
198 */ |
|
199 collect: function (frame, index = 0) { |
|
200 // Mark the given frame as contained in the frame tree. |
|
201 this._frames.set(frame, index); |
|
202 |
|
203 // Mark the given frame's subframes as contained in the tree. |
|
204 Array.forEach(frame.frames, this.collect, this); |
|
205 }, |
|
206 |
|
207 /** |
|
208 * @see nsIWebProgressListener.onStateChange |
|
209 * |
|
210 * We want to be notified about: |
|
211 * - new documents that start loading to clear the current frame tree; |
|
212 * - completed document loads to recollect reachable frames. |
|
213 */ |
|
214 onStateChange: function (webProgress, request, stateFlags, status) { |
|
215 // Ignore state changes for subframes because we're only interested in the |
|
216 // top-document starting or stopping its load. We thus only care about any |
|
217 // changes to the root of the frame tree, not to any of its nodes/leafs. |
|
218 if (!webProgress.isTopLevel || webProgress.DOMWindow != this.content) { |
|
219 return; |
|
220 } |
|
221 |
|
222 if (stateFlags & Ci.nsIWebProgressListener.STATE_START) { |
|
223 // Clear the list of frames until we can recollect it. |
|
224 this._frames.clear(); |
|
225 |
|
226 // Notify observers that the frame tree has been reset. |
|
227 this.notifyObservers("onFrameTreeReset"); |
|
228 } else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) { |
|
229 // The document and its resources have finished loading. |
|
230 this.collect(webProgress.DOMWindow); |
|
231 |
|
232 // Notify observers that the frame tree has been reset. |
|
233 this.notifyObservers("onFrameTreeCollected"); |
|
234 } |
|
235 }, |
|
236 |
|
237 // Unused nsIWebProgressListener methods. |
|
238 onLocationChange: function () {}, |
|
239 onProgressChange: function () {}, |
|
240 onSecurityChange: function () {}, |
|
241 onStatusChange: function () {}, |
|
242 |
|
243 QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, |
|
244 Ci.nsISupportsWeakReference]) |
|
245 }; |