|
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 = ["TabState"]; |
|
8 |
|
9 const Cu = Components.utils; |
|
10 |
|
11 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); |
|
12 Cu.import("resource://gre/modules/Promise.jsm", this); |
|
13 Cu.import("resource://gre/modules/Task.jsm", this); |
|
14 |
|
15 XPCOMUtils.defineLazyModuleGetter(this, "console", |
|
16 "resource://gre/modules/devtools/Console.jsm"); |
|
17 XPCOMUtils.defineLazyModuleGetter(this, "PrivacyFilter", |
|
18 "resource:///modules/sessionstore/PrivacyFilter.jsm"); |
|
19 XPCOMUtils.defineLazyModuleGetter(this, "TabStateCache", |
|
20 "resource:///modules/sessionstore/TabStateCache.jsm"); |
|
21 XPCOMUtils.defineLazyModuleGetter(this, "TabAttributes", |
|
22 "resource:///modules/sessionstore/TabAttributes.jsm"); |
|
23 XPCOMUtils.defineLazyModuleGetter(this, "Utils", |
|
24 "resource:///modules/sessionstore/Utils.jsm"); |
|
25 |
|
26 /** |
|
27 * Module that contains tab state collection methods. |
|
28 */ |
|
29 this.TabState = Object.freeze({ |
|
30 setSyncHandler: function (browser, handler) { |
|
31 TabStateInternal.setSyncHandler(browser, handler); |
|
32 }, |
|
33 |
|
34 update: function (browser, data) { |
|
35 TabStateInternal.update(browser, data); |
|
36 }, |
|
37 |
|
38 flush: function (browser) { |
|
39 TabStateInternal.flush(browser); |
|
40 }, |
|
41 |
|
42 flushWindow: function (window) { |
|
43 TabStateInternal.flushWindow(window); |
|
44 }, |
|
45 |
|
46 collect: function (tab) { |
|
47 return TabStateInternal.collect(tab); |
|
48 }, |
|
49 |
|
50 clone: function (tab) { |
|
51 return TabStateInternal.clone(tab); |
|
52 } |
|
53 }); |
|
54 |
|
55 let TabStateInternal = { |
|
56 // A map (xul:browser -> handler) that maps a tab to the |
|
57 // synchronous collection handler object for that tab. |
|
58 // See SyncHandler in content-sessionStore.js. |
|
59 _syncHandlers: new WeakMap(), |
|
60 |
|
61 // A map (xul:browser -> int) that maps a browser to the |
|
62 // last "SessionStore:update" message ID we received for it. |
|
63 _latestMessageID: new WeakMap(), |
|
64 |
|
65 /** |
|
66 * Install the sync handler object from a given tab. |
|
67 */ |
|
68 setSyncHandler: function (browser, handler) { |
|
69 this._syncHandlers.set(browser.permanentKey, handler); |
|
70 this._latestMessageID.set(browser.permanentKey, 0); |
|
71 }, |
|
72 |
|
73 /** |
|
74 * Processes a data update sent by the content script. |
|
75 */ |
|
76 update: function (browser, {id, data}) { |
|
77 // Only ever process messages that have an ID higher than the last one we |
|
78 // saw. This ensures we don't use stale data that has already been received |
|
79 // synchronously. |
|
80 if (id > this._latestMessageID.get(browser.permanentKey)) { |
|
81 this._latestMessageID.set(browser.permanentKey, id); |
|
82 TabStateCache.update(browser, data); |
|
83 } |
|
84 }, |
|
85 |
|
86 /** |
|
87 * Flushes all data currently queued in the given browser's content script. |
|
88 */ |
|
89 flush: function (browser) { |
|
90 if (this._syncHandlers.has(browser.permanentKey)) { |
|
91 let lastID = this._latestMessageID.get(browser.permanentKey); |
|
92 this._syncHandlers.get(browser.permanentKey).flush(lastID); |
|
93 } |
|
94 }, |
|
95 |
|
96 /** |
|
97 * Flushes queued content script data for all browsers of a given window. |
|
98 */ |
|
99 flushWindow: function (window) { |
|
100 for (let browser of window.gBrowser.browsers) { |
|
101 this.flush(browser); |
|
102 } |
|
103 }, |
|
104 |
|
105 /** |
|
106 * Collect data related to a single tab, synchronously. |
|
107 * |
|
108 * @param tab |
|
109 * tabbrowser tab |
|
110 * |
|
111 * @returns {TabData} An object with the data for this tab. If the |
|
112 * tab has not been invalidated since the last call to |
|
113 * collect(aTab), the same object is returned. |
|
114 */ |
|
115 collect: function (tab) { |
|
116 return this._collectBaseTabData(tab); |
|
117 }, |
|
118 |
|
119 /** |
|
120 * Collect data related to a single tab, including private data. |
|
121 * Use with caution. |
|
122 * |
|
123 * @param tab |
|
124 * tabbrowser tab |
|
125 * |
|
126 * @returns {object} An object with the data for this tab. This data is never |
|
127 * cached, it will always be read from the tab and thus be |
|
128 * up-to-date. |
|
129 */ |
|
130 clone: function (tab) { |
|
131 return this._collectBaseTabData(tab, {includePrivateData: true}); |
|
132 }, |
|
133 |
|
134 /** |
|
135 * Collects basic tab data for a given tab. |
|
136 * |
|
137 * @param tab |
|
138 * tabbrowser tab |
|
139 * @param options (object) |
|
140 * {includePrivateData: true} to always include private data |
|
141 * |
|
142 * @returns {object} An object with the basic data for this tab. |
|
143 */ |
|
144 _collectBaseTabData: function (tab, options) { |
|
145 let tabData = {entries: [], lastAccessed: tab.lastAccessed }; |
|
146 let browser = tab.linkedBrowser; |
|
147 |
|
148 if (!browser || !browser.currentURI) { |
|
149 // can happen when calling this function right after .addTab() |
|
150 return tabData; |
|
151 } |
|
152 if (browser.__SS_data) { |
|
153 // Use the data to be restored when the tab hasn't been |
|
154 // completely loaded. We clone the data, since we're updating it |
|
155 // here and the caller may update it further. |
|
156 tabData = Utils.shallowCopy(browser.__SS_data); |
|
157 if (tab.pinned) |
|
158 tabData.pinned = true; |
|
159 else |
|
160 delete tabData.pinned; |
|
161 tabData.hidden = tab.hidden; |
|
162 |
|
163 // If __SS_extdata is set then we'll use that since it might be newer. |
|
164 if (tab.__SS_extdata) |
|
165 tabData.extData = tab.__SS_extdata; |
|
166 // If it exists but is empty then a key was likely deleted. In that case just |
|
167 // delete extData. |
|
168 if (tabData.extData && !Object.keys(tabData.extData).length) |
|
169 delete tabData.extData; |
|
170 return tabData; |
|
171 } |
|
172 |
|
173 // If there is a userTypedValue set, then either the user has typed something |
|
174 // in the URL bar, or a new tab was opened with a URI to load. userTypedClear |
|
175 // is used to indicate whether the tab was in some sort of loading state with |
|
176 // userTypedValue. |
|
177 if (browser.userTypedValue) { |
|
178 tabData.userTypedValue = browser.userTypedValue; |
|
179 tabData.userTypedClear = browser.userTypedClear; |
|
180 } else { |
|
181 delete tabData.userTypedValue; |
|
182 delete tabData.userTypedClear; |
|
183 } |
|
184 |
|
185 if (tab.pinned) |
|
186 tabData.pinned = true; |
|
187 else |
|
188 delete tabData.pinned; |
|
189 tabData.hidden = tab.hidden; |
|
190 |
|
191 // Save tab attributes. |
|
192 tabData.attributes = TabAttributes.get(tab); |
|
193 |
|
194 // Store the tab icon. |
|
195 let tabbrowser = tab.ownerDocument.defaultView.gBrowser; |
|
196 tabData.image = tabbrowser.getIcon(tab); |
|
197 |
|
198 if (tab.__SS_extdata) |
|
199 tabData.extData = tab.__SS_extdata; |
|
200 else if (tabData.extData) |
|
201 delete tabData.extData; |
|
202 |
|
203 // Copy data from the tab state cache only if the tab has fully finished |
|
204 // restoring. We don't want to overwrite data contained in __SS_data. |
|
205 this._copyFromCache(tab, tabData, options); |
|
206 |
|
207 return tabData; |
|
208 }, |
|
209 |
|
210 /** |
|
211 * Copy tab data for the given |tab| from the cache to |tabData|. |
|
212 * |
|
213 * @param tab (xul:tab) |
|
214 * The tab belonging to the given |tabData| object. |
|
215 * @param tabData (object) |
|
216 * The tab data belonging to the given |tab|. |
|
217 * @param options (object) |
|
218 * {includePrivateData: true} to always include private data |
|
219 */ |
|
220 _copyFromCache: function (tab, tabData, options = {}) { |
|
221 let data = TabStateCache.get(tab.linkedBrowser); |
|
222 if (!data) { |
|
223 return; |
|
224 } |
|
225 |
|
226 // The caller may explicitly request to omit privacy checks. |
|
227 let includePrivateData = options && options.includePrivateData; |
|
228 |
|
229 for (let key of Object.keys(data)) { |
|
230 let value = data[key]; |
|
231 |
|
232 // Filter sensitive data according to the current privacy level. |
|
233 if (!includePrivateData) { |
|
234 if (key === "storage") { |
|
235 value = PrivacyFilter.filterSessionStorageData(value, tab.pinned); |
|
236 } else if (key === "formdata") { |
|
237 value = PrivacyFilter.filterFormData(value, tab.pinned); |
|
238 } |
|
239 } |
|
240 |
|
241 if (key === "history") { |
|
242 tabData.entries = value.entries; |
|
243 |
|
244 if (value.hasOwnProperty("index")) { |
|
245 tabData.index = value.index; |
|
246 } |
|
247 } else if (value) { |
|
248 tabData[key] = value; |
|
249 } |
|
250 } |
|
251 } |
|
252 }; |