1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/services/sync/modules/engines/tabs.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,338 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +this.EXPORTED_SYMBOLS = ['TabEngine', 'TabSetRecord']; 1.9 + 1.10 +const Cc = Components.classes; 1.11 +const Ci = Components.interfaces; 1.12 +const Cu = Components.utils; 1.13 + 1.14 +const TABS_TTL = 604800; // 7 days 1.15 + 1.16 +Cu.import("resource://gre/modules/Preferences.jsm"); 1.17 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.18 +Cu.import("resource://services-sync/engines.js"); 1.19 +Cu.import("resource://services-sync/engines/clients.js"); 1.20 +Cu.import("resource://services-sync/record.js"); 1.21 +Cu.import("resource://services-sync/util.js"); 1.22 +Cu.import("resource://services-sync/constants.js"); 1.23 + 1.24 +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", 1.25 + "resource://gre/modules/PrivateBrowsingUtils.jsm"); 1.26 + 1.27 +this.TabSetRecord = function TabSetRecord(collection, id) { 1.28 + CryptoWrapper.call(this, collection, id); 1.29 +} 1.30 +TabSetRecord.prototype = { 1.31 + __proto__: CryptoWrapper.prototype, 1.32 + _logName: "Sync.Record.Tabs", 1.33 + ttl: TABS_TTL 1.34 +}; 1.35 + 1.36 +Utils.deferGetSet(TabSetRecord, "cleartext", ["clientName", "tabs"]); 1.37 + 1.38 + 1.39 +this.TabEngine = function TabEngine(service) { 1.40 + SyncEngine.call(this, "Tabs", service); 1.41 + 1.42 + // Reset the client on every startup so that we fetch recent tabs 1.43 + this._resetClient(); 1.44 +} 1.45 +TabEngine.prototype = { 1.46 + __proto__: SyncEngine.prototype, 1.47 + _storeObj: TabStore, 1.48 + _trackerObj: TabTracker, 1.49 + _recordObj: TabSetRecord, 1.50 + 1.51 + getChangedIDs: function getChangedIDs() { 1.52 + // No need for a proper timestamp (no conflict resolution needed). 1.53 + let changedIDs = {}; 1.54 + if (this._tracker.modified) 1.55 + changedIDs[this.service.clientsEngine.localID] = 0; 1.56 + return changedIDs; 1.57 + }, 1.58 + 1.59 + // API for use by Weave UI code to give user choices of tabs to open: 1.60 + getAllClients: function TabEngine_getAllClients() { 1.61 + return this._store._remoteClients; 1.62 + }, 1.63 + 1.64 + getClientById: function TabEngine_getClientById(id) { 1.65 + return this._store._remoteClients[id]; 1.66 + }, 1.67 + 1.68 + _resetClient: function TabEngine__resetClient() { 1.69 + SyncEngine.prototype._resetClient.call(this); 1.70 + this._store.wipe(); 1.71 + this._tracker.modified = true; 1.72 + }, 1.73 + 1.74 + removeClientData: function removeClientData() { 1.75 + let url = this.engineURL + "/" + this.service.clientsEngine.localID; 1.76 + this.service.resource(url).delete(); 1.77 + }, 1.78 + 1.79 + /** 1.80 + * Return a Set of open URLs. 1.81 + */ 1.82 + getOpenURLs: function () { 1.83 + let urls = new Set(); 1.84 + for (let entry of this._store.getAllTabs()) { 1.85 + urls.add(entry.urlHistory[0]); 1.86 + } 1.87 + return urls; 1.88 + } 1.89 +}; 1.90 + 1.91 + 1.92 +function TabStore(name, engine) { 1.93 + Store.call(this, name, engine); 1.94 +} 1.95 +TabStore.prototype = { 1.96 + __proto__: Store.prototype, 1.97 + 1.98 + itemExists: function TabStore_itemExists(id) { 1.99 + return id == this.engine.service.clientsEngine.localID; 1.100 + }, 1.101 + 1.102 + getWindowEnumerator: function () { 1.103 + return Services.wm.getEnumerator("navigator:browser"); 1.104 + }, 1.105 + 1.106 + shouldSkipWindow: function (win) { 1.107 + return win.closed || 1.108 + PrivateBrowsingUtils.isWindowPrivate(win); 1.109 + }, 1.110 + 1.111 + getTabState: function (tab) { 1.112 + return JSON.parse(Svc.Session.getTabState(tab)); 1.113 + }, 1.114 + 1.115 + getAllTabs: function (filter) { 1.116 + let filteredUrls = new RegExp(Svc.Prefs.get("engine.tabs.filteredUrls"), "i"); 1.117 + 1.118 + let allTabs = []; 1.119 + 1.120 + let winEnum = this.getWindowEnumerator(); 1.121 + while (winEnum.hasMoreElements()) { 1.122 + let win = winEnum.getNext(); 1.123 + if (this.shouldSkipWindow(win)) { 1.124 + continue; 1.125 + } 1.126 + 1.127 + for (let tab of win.gBrowser.tabs) { 1.128 + tabState = this.getTabState(tab); 1.129 + 1.130 + // Make sure there are history entries to look at. 1.131 + if (!tabState || !tabState.entries.length) { 1.132 + continue; 1.133 + } 1.134 + 1.135 + // Until we store full or partial history, just grab the current entry. 1.136 + // index is 1 based, so make sure we adjust. 1.137 + let entry = tabState.entries[tabState.index - 1]; 1.138 + 1.139 + // Filter out some urls if necessary. SessionStore can return empty 1.140 + // tabs in some cases - easiest thing is to just ignore them for now. 1.141 + if (!entry.url || filter && filteredUrls.test(entry.url)) { 1.142 + continue; 1.143 + } 1.144 + 1.145 + // I think it's also possible that attributes[.image] might not be set 1.146 + // so handle that as well. 1.147 + allTabs.push({ 1.148 + title: entry.title || "", 1.149 + urlHistory: [entry.url], 1.150 + icon: tabState.attributes && tabState.attributes.image || "", 1.151 + lastUsed: Math.floor((tabState.lastAccessed || 0) / 1000) 1.152 + }); 1.153 + } 1.154 + } 1.155 + 1.156 + return allTabs; 1.157 + }, 1.158 + 1.159 + createRecord: function createRecord(id, collection) { 1.160 + let record = new TabSetRecord(collection, id); 1.161 + record.clientName = this.engine.service.clientsEngine.localName; 1.162 + 1.163 + // Sort tabs in descending-used order to grab the most recently used 1.164 + let tabs = this.getAllTabs(true).sort(function (a, b) { 1.165 + return b.lastUsed - a.lastUsed; 1.166 + }); 1.167 + 1.168 + // Figure out how many tabs we can pack into a payload. Starting with a 28KB 1.169 + // payload, we can estimate various overheads from encryption/JSON/WBO. 1.170 + let size = JSON.stringify(tabs).length; 1.171 + let origLength = tabs.length; 1.172 + const MAX_TAB_SIZE = 20000; 1.173 + if (size > MAX_TAB_SIZE) { 1.174 + // Estimate a little more than the direct fraction to maximize packing 1.175 + let cutoff = Math.ceil(tabs.length * MAX_TAB_SIZE / size); 1.176 + tabs = tabs.slice(0, cutoff + 1); 1.177 + 1.178 + // Keep dropping off the last entry until the data fits 1.179 + while (JSON.stringify(tabs).length > MAX_TAB_SIZE) 1.180 + tabs.pop(); 1.181 + } 1.182 + 1.183 + this._log.trace("Created tabs " + tabs.length + " of " + origLength); 1.184 + tabs.forEach(function (tab) { 1.185 + this._log.trace("Wrapping tab: " + JSON.stringify(tab)); 1.186 + }, this); 1.187 + 1.188 + record.tabs = tabs; 1.189 + return record; 1.190 + }, 1.191 + 1.192 + getAllIDs: function TabStore_getAllIds() { 1.193 + // Don't report any tabs if all windows are in private browsing for 1.194 + // first syncs. 1.195 + let ids = {}; 1.196 + let allWindowsArePrivate = false; 1.197 + let wins = Services.wm.getEnumerator("navigator:browser"); 1.198 + while (wins.hasMoreElements()) { 1.199 + if (PrivateBrowsingUtils.isWindowPrivate(wins.getNext())) { 1.200 + // Ensure that at least there is a private window. 1.201 + allWindowsArePrivate = true; 1.202 + } else { 1.203 + // If there is a not private windown then finish and continue. 1.204 + allWindowsArePrivate = false; 1.205 + break; 1.206 + } 1.207 + } 1.208 + 1.209 + if (allWindowsArePrivate && 1.210 + !PrivateBrowsingUtils.permanentPrivateBrowsing) { 1.211 + return ids; 1.212 + } 1.213 + 1.214 + ids[this.engine.service.clientsEngine.localID] = true; 1.215 + return ids; 1.216 + }, 1.217 + 1.218 + wipe: function TabStore_wipe() { 1.219 + this._remoteClients = {}; 1.220 + }, 1.221 + 1.222 + create: function TabStore_create(record) { 1.223 + this._log.debug("Adding remote tabs from " + record.clientName); 1.224 + this._remoteClients[record.id] = record.cleartext; 1.225 + 1.226 + // Lose some precision, but that's good enough (seconds) 1.227 + let roundModify = Math.floor(record.modified / 1000); 1.228 + let notifyState = Svc.Prefs.get("notifyTabState"); 1.229 + // If there's no existing pref, save this first modified time 1.230 + if (notifyState == null) 1.231 + Svc.Prefs.set("notifyTabState", roundModify); 1.232 + // Don't change notifyState if it's already 0 (don't notify) 1.233 + else if (notifyState == 0) 1.234 + return; 1.235 + // We must have gotten a new tab that isn't the same as last time 1.236 + else if (notifyState != roundModify) 1.237 + Svc.Prefs.set("notifyTabState", 0); 1.238 + }, 1.239 + 1.240 + update: function update(record) { 1.241 + this._log.trace("Ignoring tab updates as local ones win"); 1.242 + } 1.243 +}; 1.244 + 1.245 + 1.246 +function TabTracker(name, engine) { 1.247 + Tracker.call(this, name, engine); 1.248 + Svc.Obs.add("weave:engine:start-tracking", this); 1.249 + Svc.Obs.add("weave:engine:stop-tracking", this); 1.250 + 1.251 + // Make sure "this" pointer is always set correctly for event listeners 1.252 + this.onTab = Utils.bind2(this, this.onTab); 1.253 + this._unregisterListeners = Utils.bind2(this, this._unregisterListeners); 1.254 +} 1.255 +TabTracker.prototype = { 1.256 + __proto__: Tracker.prototype, 1.257 + 1.258 + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), 1.259 + 1.260 + loadChangedIDs: function loadChangedIDs() { 1.261 + // Don't read changed IDs from disk at start up. 1.262 + }, 1.263 + 1.264 + clearChangedIDs: function clearChangedIDs() { 1.265 + this.modified = false; 1.266 + }, 1.267 + 1.268 + _topics: ["pageshow", "TabOpen", "TabClose", "TabSelect"], 1.269 + _registerListenersForWindow: function registerListenersFW(window) { 1.270 + this._log.trace("Registering tab listeners in window"); 1.271 + for each (let topic in this._topics) { 1.272 + window.addEventListener(topic, this.onTab, false); 1.273 + } 1.274 + window.addEventListener("unload", this._unregisterListeners, false); 1.275 + }, 1.276 + 1.277 + _unregisterListeners: function unregisterListeners(event) { 1.278 + this._unregisterListenersForWindow(event.target); 1.279 + }, 1.280 + 1.281 + _unregisterListenersForWindow: function unregisterListenersFW(window) { 1.282 + this._log.trace("Removing tab listeners in window"); 1.283 + window.removeEventListener("unload", this._unregisterListeners, false); 1.284 + for each (let topic in this._topics) { 1.285 + window.removeEventListener(topic, this.onTab, false); 1.286 + } 1.287 + }, 1.288 + 1.289 + startTracking: function () { 1.290 + Svc.Obs.add("domwindowopened", this); 1.291 + let wins = Services.wm.getEnumerator("navigator:browser"); 1.292 + while (wins.hasMoreElements()) { 1.293 + this._registerListenersForWindow(wins.getNext()); 1.294 + } 1.295 + }, 1.296 + 1.297 + stopTracking: function () { 1.298 + Svc.Obs.remove("domwindowopened", this); 1.299 + let wins = Services.wm.getEnumerator("navigator:browser"); 1.300 + while (wins.hasMoreElements()) { 1.301 + this._unregisterListenersForWindow(wins.getNext()); 1.302 + } 1.303 + }, 1.304 + 1.305 + observe: function (subject, topic, data) { 1.306 + Tracker.prototype.observe.call(this, subject, topic, data); 1.307 + 1.308 + switch (topic) { 1.309 + case "domwindowopened": 1.310 + let onLoad = () => { 1.311 + subject.removeEventListener("load", onLoad, false); 1.312 + // Only register after the window is done loading to avoid unloads. 1.313 + this._registerListenersForWindow(subject); 1.314 + }; 1.315 + 1.316 + // Add tab listeners now that a window has opened. 1.317 + subject.addEventListener("load", onLoad, false); 1.318 + break; 1.319 + } 1.320 + }, 1.321 + 1.322 + onTab: function onTab(event) { 1.323 + if (event.originalTarget.linkedBrowser) { 1.324 + let win = event.originalTarget.linkedBrowser.contentWindow; 1.325 + if (PrivateBrowsingUtils.isWindowPrivate(win) && 1.326 + !PrivateBrowsingUtils.permanentPrivateBrowsing) { 1.327 + this._log.trace("Ignoring tab event from private browsing."); 1.328 + return; 1.329 + } 1.330 + } 1.331 + 1.332 + this._log.trace("onTab event: " + event.type); 1.333 + this.modified = true; 1.334 + 1.335 + // For page shows, bump the score 10% of the time, emulating a partial 1.336 + // score. We don't want to sync too frequently. For all other page 1.337 + // events, always bump the score. 1.338 + if (event.type != "pageshow" || Math.random() < .1) 1.339 + this.score += SCORE_INCREMENT_SMALL; 1.340 + }, 1.341 +}