|
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 |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 this.EXPORTED_SYMBOLS = ['TabEngine', 'TabSetRecord']; |
|
6 |
|
7 const Cc = Components.classes; |
|
8 const Ci = Components.interfaces; |
|
9 const Cu = Components.utils; |
|
10 |
|
11 const TABS_TTL = 604800; // 7 days |
|
12 |
|
13 Cu.import("resource://gre/modules/Preferences.jsm"); |
|
14 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
15 Cu.import("resource://services-sync/engines.js"); |
|
16 Cu.import("resource://services-sync/engines/clients.js"); |
|
17 Cu.import("resource://services-sync/record.js"); |
|
18 Cu.import("resource://services-sync/util.js"); |
|
19 Cu.import("resource://services-sync/constants.js"); |
|
20 |
|
21 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", |
|
22 "resource://gre/modules/PrivateBrowsingUtils.jsm"); |
|
23 |
|
24 this.TabSetRecord = function TabSetRecord(collection, id) { |
|
25 CryptoWrapper.call(this, collection, id); |
|
26 } |
|
27 TabSetRecord.prototype = { |
|
28 __proto__: CryptoWrapper.prototype, |
|
29 _logName: "Sync.Record.Tabs", |
|
30 ttl: TABS_TTL |
|
31 }; |
|
32 |
|
33 Utils.deferGetSet(TabSetRecord, "cleartext", ["clientName", "tabs"]); |
|
34 |
|
35 |
|
36 this.TabEngine = function TabEngine(service) { |
|
37 SyncEngine.call(this, "Tabs", service); |
|
38 |
|
39 // Reset the client on every startup so that we fetch recent tabs |
|
40 this._resetClient(); |
|
41 } |
|
42 TabEngine.prototype = { |
|
43 __proto__: SyncEngine.prototype, |
|
44 _storeObj: TabStore, |
|
45 _trackerObj: TabTracker, |
|
46 _recordObj: TabSetRecord, |
|
47 |
|
48 getChangedIDs: function getChangedIDs() { |
|
49 // No need for a proper timestamp (no conflict resolution needed). |
|
50 let changedIDs = {}; |
|
51 if (this._tracker.modified) |
|
52 changedIDs[this.service.clientsEngine.localID] = 0; |
|
53 return changedIDs; |
|
54 }, |
|
55 |
|
56 // API for use by Weave UI code to give user choices of tabs to open: |
|
57 getAllClients: function TabEngine_getAllClients() { |
|
58 return this._store._remoteClients; |
|
59 }, |
|
60 |
|
61 getClientById: function TabEngine_getClientById(id) { |
|
62 return this._store._remoteClients[id]; |
|
63 }, |
|
64 |
|
65 _resetClient: function TabEngine__resetClient() { |
|
66 SyncEngine.prototype._resetClient.call(this); |
|
67 this._store.wipe(); |
|
68 this._tracker.modified = true; |
|
69 }, |
|
70 |
|
71 removeClientData: function removeClientData() { |
|
72 let url = this.engineURL + "/" + this.service.clientsEngine.localID; |
|
73 this.service.resource(url).delete(); |
|
74 }, |
|
75 |
|
76 /** |
|
77 * Return a Set of open URLs. |
|
78 */ |
|
79 getOpenURLs: function () { |
|
80 let urls = new Set(); |
|
81 for (let entry of this._store.getAllTabs()) { |
|
82 urls.add(entry.urlHistory[0]); |
|
83 } |
|
84 return urls; |
|
85 } |
|
86 }; |
|
87 |
|
88 |
|
89 function TabStore(name, engine) { |
|
90 Store.call(this, name, engine); |
|
91 } |
|
92 TabStore.prototype = { |
|
93 __proto__: Store.prototype, |
|
94 |
|
95 itemExists: function TabStore_itemExists(id) { |
|
96 return id == this.engine.service.clientsEngine.localID; |
|
97 }, |
|
98 |
|
99 getWindowEnumerator: function () { |
|
100 return Services.wm.getEnumerator("navigator:browser"); |
|
101 }, |
|
102 |
|
103 shouldSkipWindow: function (win) { |
|
104 return win.closed || |
|
105 PrivateBrowsingUtils.isWindowPrivate(win); |
|
106 }, |
|
107 |
|
108 getTabState: function (tab) { |
|
109 return JSON.parse(Svc.Session.getTabState(tab)); |
|
110 }, |
|
111 |
|
112 getAllTabs: function (filter) { |
|
113 let filteredUrls = new RegExp(Svc.Prefs.get("engine.tabs.filteredUrls"), "i"); |
|
114 |
|
115 let allTabs = []; |
|
116 |
|
117 let winEnum = this.getWindowEnumerator(); |
|
118 while (winEnum.hasMoreElements()) { |
|
119 let win = winEnum.getNext(); |
|
120 if (this.shouldSkipWindow(win)) { |
|
121 continue; |
|
122 } |
|
123 |
|
124 for (let tab of win.gBrowser.tabs) { |
|
125 tabState = this.getTabState(tab); |
|
126 |
|
127 // Make sure there are history entries to look at. |
|
128 if (!tabState || !tabState.entries.length) { |
|
129 continue; |
|
130 } |
|
131 |
|
132 // Until we store full or partial history, just grab the current entry. |
|
133 // index is 1 based, so make sure we adjust. |
|
134 let entry = tabState.entries[tabState.index - 1]; |
|
135 |
|
136 // Filter out some urls if necessary. SessionStore can return empty |
|
137 // tabs in some cases - easiest thing is to just ignore them for now. |
|
138 if (!entry.url || filter && filteredUrls.test(entry.url)) { |
|
139 continue; |
|
140 } |
|
141 |
|
142 // I think it's also possible that attributes[.image] might not be set |
|
143 // so handle that as well. |
|
144 allTabs.push({ |
|
145 title: entry.title || "", |
|
146 urlHistory: [entry.url], |
|
147 icon: tabState.attributes && tabState.attributes.image || "", |
|
148 lastUsed: Math.floor((tabState.lastAccessed || 0) / 1000) |
|
149 }); |
|
150 } |
|
151 } |
|
152 |
|
153 return allTabs; |
|
154 }, |
|
155 |
|
156 createRecord: function createRecord(id, collection) { |
|
157 let record = new TabSetRecord(collection, id); |
|
158 record.clientName = this.engine.service.clientsEngine.localName; |
|
159 |
|
160 // Sort tabs in descending-used order to grab the most recently used |
|
161 let tabs = this.getAllTabs(true).sort(function (a, b) { |
|
162 return b.lastUsed - a.lastUsed; |
|
163 }); |
|
164 |
|
165 // Figure out how many tabs we can pack into a payload. Starting with a 28KB |
|
166 // payload, we can estimate various overheads from encryption/JSON/WBO. |
|
167 let size = JSON.stringify(tabs).length; |
|
168 let origLength = tabs.length; |
|
169 const MAX_TAB_SIZE = 20000; |
|
170 if (size > MAX_TAB_SIZE) { |
|
171 // Estimate a little more than the direct fraction to maximize packing |
|
172 let cutoff = Math.ceil(tabs.length * MAX_TAB_SIZE / size); |
|
173 tabs = tabs.slice(0, cutoff + 1); |
|
174 |
|
175 // Keep dropping off the last entry until the data fits |
|
176 while (JSON.stringify(tabs).length > MAX_TAB_SIZE) |
|
177 tabs.pop(); |
|
178 } |
|
179 |
|
180 this._log.trace("Created tabs " + tabs.length + " of " + origLength); |
|
181 tabs.forEach(function (tab) { |
|
182 this._log.trace("Wrapping tab: " + JSON.stringify(tab)); |
|
183 }, this); |
|
184 |
|
185 record.tabs = tabs; |
|
186 return record; |
|
187 }, |
|
188 |
|
189 getAllIDs: function TabStore_getAllIds() { |
|
190 // Don't report any tabs if all windows are in private browsing for |
|
191 // first syncs. |
|
192 let ids = {}; |
|
193 let allWindowsArePrivate = false; |
|
194 let wins = Services.wm.getEnumerator("navigator:browser"); |
|
195 while (wins.hasMoreElements()) { |
|
196 if (PrivateBrowsingUtils.isWindowPrivate(wins.getNext())) { |
|
197 // Ensure that at least there is a private window. |
|
198 allWindowsArePrivate = true; |
|
199 } else { |
|
200 // If there is a not private windown then finish and continue. |
|
201 allWindowsArePrivate = false; |
|
202 break; |
|
203 } |
|
204 } |
|
205 |
|
206 if (allWindowsArePrivate && |
|
207 !PrivateBrowsingUtils.permanentPrivateBrowsing) { |
|
208 return ids; |
|
209 } |
|
210 |
|
211 ids[this.engine.service.clientsEngine.localID] = true; |
|
212 return ids; |
|
213 }, |
|
214 |
|
215 wipe: function TabStore_wipe() { |
|
216 this._remoteClients = {}; |
|
217 }, |
|
218 |
|
219 create: function TabStore_create(record) { |
|
220 this._log.debug("Adding remote tabs from " + record.clientName); |
|
221 this._remoteClients[record.id] = record.cleartext; |
|
222 |
|
223 // Lose some precision, but that's good enough (seconds) |
|
224 let roundModify = Math.floor(record.modified / 1000); |
|
225 let notifyState = Svc.Prefs.get("notifyTabState"); |
|
226 // If there's no existing pref, save this first modified time |
|
227 if (notifyState == null) |
|
228 Svc.Prefs.set("notifyTabState", roundModify); |
|
229 // Don't change notifyState if it's already 0 (don't notify) |
|
230 else if (notifyState == 0) |
|
231 return; |
|
232 // We must have gotten a new tab that isn't the same as last time |
|
233 else if (notifyState != roundModify) |
|
234 Svc.Prefs.set("notifyTabState", 0); |
|
235 }, |
|
236 |
|
237 update: function update(record) { |
|
238 this._log.trace("Ignoring tab updates as local ones win"); |
|
239 } |
|
240 }; |
|
241 |
|
242 |
|
243 function TabTracker(name, engine) { |
|
244 Tracker.call(this, name, engine); |
|
245 Svc.Obs.add("weave:engine:start-tracking", this); |
|
246 Svc.Obs.add("weave:engine:stop-tracking", this); |
|
247 |
|
248 // Make sure "this" pointer is always set correctly for event listeners |
|
249 this.onTab = Utils.bind2(this, this.onTab); |
|
250 this._unregisterListeners = Utils.bind2(this, this._unregisterListeners); |
|
251 } |
|
252 TabTracker.prototype = { |
|
253 __proto__: Tracker.prototype, |
|
254 |
|
255 QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), |
|
256 |
|
257 loadChangedIDs: function loadChangedIDs() { |
|
258 // Don't read changed IDs from disk at start up. |
|
259 }, |
|
260 |
|
261 clearChangedIDs: function clearChangedIDs() { |
|
262 this.modified = false; |
|
263 }, |
|
264 |
|
265 _topics: ["pageshow", "TabOpen", "TabClose", "TabSelect"], |
|
266 _registerListenersForWindow: function registerListenersFW(window) { |
|
267 this._log.trace("Registering tab listeners in window"); |
|
268 for each (let topic in this._topics) { |
|
269 window.addEventListener(topic, this.onTab, false); |
|
270 } |
|
271 window.addEventListener("unload", this._unregisterListeners, false); |
|
272 }, |
|
273 |
|
274 _unregisterListeners: function unregisterListeners(event) { |
|
275 this._unregisterListenersForWindow(event.target); |
|
276 }, |
|
277 |
|
278 _unregisterListenersForWindow: function unregisterListenersFW(window) { |
|
279 this._log.trace("Removing tab listeners in window"); |
|
280 window.removeEventListener("unload", this._unregisterListeners, false); |
|
281 for each (let topic in this._topics) { |
|
282 window.removeEventListener(topic, this.onTab, false); |
|
283 } |
|
284 }, |
|
285 |
|
286 startTracking: function () { |
|
287 Svc.Obs.add("domwindowopened", this); |
|
288 let wins = Services.wm.getEnumerator("navigator:browser"); |
|
289 while (wins.hasMoreElements()) { |
|
290 this._registerListenersForWindow(wins.getNext()); |
|
291 } |
|
292 }, |
|
293 |
|
294 stopTracking: function () { |
|
295 Svc.Obs.remove("domwindowopened", this); |
|
296 let wins = Services.wm.getEnumerator("navigator:browser"); |
|
297 while (wins.hasMoreElements()) { |
|
298 this._unregisterListenersForWindow(wins.getNext()); |
|
299 } |
|
300 }, |
|
301 |
|
302 observe: function (subject, topic, data) { |
|
303 Tracker.prototype.observe.call(this, subject, topic, data); |
|
304 |
|
305 switch (topic) { |
|
306 case "domwindowopened": |
|
307 let onLoad = () => { |
|
308 subject.removeEventListener("load", onLoad, false); |
|
309 // Only register after the window is done loading to avoid unloads. |
|
310 this._registerListenersForWindow(subject); |
|
311 }; |
|
312 |
|
313 // Add tab listeners now that a window has opened. |
|
314 subject.addEventListener("load", onLoad, false); |
|
315 break; |
|
316 } |
|
317 }, |
|
318 |
|
319 onTab: function onTab(event) { |
|
320 if (event.originalTarget.linkedBrowser) { |
|
321 let win = event.originalTarget.linkedBrowser.contentWindow; |
|
322 if (PrivateBrowsingUtils.isWindowPrivate(win) && |
|
323 !PrivateBrowsingUtils.permanentPrivateBrowsing) { |
|
324 this._log.trace("Ignoring tab event from private browsing."); |
|
325 return; |
|
326 } |
|
327 } |
|
328 |
|
329 this._log.trace("onTab event: " + event.type); |
|
330 this.modified = true; |
|
331 |
|
332 // For page shows, bump the score 10% of the time, emulating a partial |
|
333 // score. We don't want to sync too frequently. For all other page |
|
334 // events, always bump the score. |
|
335 if (event.type != "pageshow" || Math.random() < .1) |
|
336 this.score += SCORE_INCREMENT_SMALL; |
|
337 }, |
|
338 } |