michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: this.EXPORTED_SYMBOLS = ['TabEngine', 'TabSetRecord']; michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cu = Components.utils; michael@0: michael@0: const TABS_TTL = 604800; // 7 days michael@0: michael@0: Cu.import("resource://gre/modules/Preferences.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://services-sync/engines.js"); michael@0: Cu.import("resource://services-sync/engines/clients.js"); michael@0: Cu.import("resource://services-sync/record.js"); michael@0: Cu.import("resource://services-sync/util.js"); michael@0: Cu.import("resource://services-sync/constants.js"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", michael@0: "resource://gre/modules/PrivateBrowsingUtils.jsm"); michael@0: michael@0: this.TabSetRecord = function TabSetRecord(collection, id) { michael@0: CryptoWrapper.call(this, collection, id); michael@0: } michael@0: TabSetRecord.prototype = { michael@0: __proto__: CryptoWrapper.prototype, michael@0: _logName: "Sync.Record.Tabs", michael@0: ttl: TABS_TTL michael@0: }; michael@0: michael@0: Utils.deferGetSet(TabSetRecord, "cleartext", ["clientName", "tabs"]); michael@0: michael@0: michael@0: this.TabEngine = function TabEngine(service) { michael@0: SyncEngine.call(this, "Tabs", service); michael@0: michael@0: // Reset the client on every startup so that we fetch recent tabs michael@0: this._resetClient(); michael@0: } michael@0: TabEngine.prototype = { michael@0: __proto__: SyncEngine.prototype, michael@0: _storeObj: TabStore, michael@0: _trackerObj: TabTracker, michael@0: _recordObj: TabSetRecord, michael@0: michael@0: getChangedIDs: function getChangedIDs() { michael@0: // No need for a proper timestamp (no conflict resolution needed). michael@0: let changedIDs = {}; michael@0: if (this._tracker.modified) michael@0: changedIDs[this.service.clientsEngine.localID] = 0; michael@0: return changedIDs; michael@0: }, michael@0: michael@0: // API for use by Weave UI code to give user choices of tabs to open: michael@0: getAllClients: function TabEngine_getAllClients() { michael@0: return this._store._remoteClients; michael@0: }, michael@0: michael@0: getClientById: function TabEngine_getClientById(id) { michael@0: return this._store._remoteClients[id]; michael@0: }, michael@0: michael@0: _resetClient: function TabEngine__resetClient() { michael@0: SyncEngine.prototype._resetClient.call(this); michael@0: this._store.wipe(); michael@0: this._tracker.modified = true; michael@0: }, michael@0: michael@0: removeClientData: function removeClientData() { michael@0: let url = this.engineURL + "/" + this.service.clientsEngine.localID; michael@0: this.service.resource(url).delete(); michael@0: }, michael@0: michael@0: /** michael@0: * Return a Set of open URLs. michael@0: */ michael@0: getOpenURLs: function () { michael@0: let urls = new Set(); michael@0: for (let entry of this._store.getAllTabs()) { michael@0: urls.add(entry.urlHistory[0]); michael@0: } michael@0: return urls; michael@0: } michael@0: }; michael@0: michael@0: michael@0: function TabStore(name, engine) { michael@0: Store.call(this, name, engine); michael@0: } michael@0: TabStore.prototype = { michael@0: __proto__: Store.prototype, michael@0: michael@0: itemExists: function TabStore_itemExists(id) { michael@0: return id == this.engine.service.clientsEngine.localID; michael@0: }, michael@0: michael@0: getWindowEnumerator: function () { michael@0: return Services.wm.getEnumerator("navigator:browser"); michael@0: }, michael@0: michael@0: shouldSkipWindow: function (win) { michael@0: return win.closed || michael@0: PrivateBrowsingUtils.isWindowPrivate(win); michael@0: }, michael@0: michael@0: getTabState: function (tab) { michael@0: return JSON.parse(Svc.Session.getTabState(tab)); michael@0: }, michael@0: michael@0: getAllTabs: function (filter) { michael@0: let filteredUrls = new RegExp(Svc.Prefs.get("engine.tabs.filteredUrls"), "i"); michael@0: michael@0: let allTabs = []; michael@0: michael@0: let winEnum = this.getWindowEnumerator(); michael@0: while (winEnum.hasMoreElements()) { michael@0: let win = winEnum.getNext(); michael@0: if (this.shouldSkipWindow(win)) { michael@0: continue; michael@0: } michael@0: michael@0: for (let tab of win.gBrowser.tabs) { michael@0: tabState = this.getTabState(tab); michael@0: michael@0: // Make sure there are history entries to look at. michael@0: if (!tabState || !tabState.entries.length) { michael@0: continue; michael@0: } michael@0: michael@0: // Until we store full or partial history, just grab the current entry. michael@0: // index is 1 based, so make sure we adjust. michael@0: let entry = tabState.entries[tabState.index - 1]; michael@0: michael@0: // Filter out some urls if necessary. SessionStore can return empty michael@0: // tabs in some cases - easiest thing is to just ignore them for now. michael@0: if (!entry.url || filter && filteredUrls.test(entry.url)) { michael@0: continue; michael@0: } michael@0: michael@0: // I think it's also possible that attributes[.image] might not be set michael@0: // so handle that as well. michael@0: allTabs.push({ michael@0: title: entry.title || "", michael@0: urlHistory: [entry.url], michael@0: icon: tabState.attributes && tabState.attributes.image || "", michael@0: lastUsed: Math.floor((tabState.lastAccessed || 0) / 1000) michael@0: }); michael@0: } michael@0: } michael@0: michael@0: return allTabs; michael@0: }, michael@0: michael@0: createRecord: function createRecord(id, collection) { michael@0: let record = new TabSetRecord(collection, id); michael@0: record.clientName = this.engine.service.clientsEngine.localName; michael@0: michael@0: // Sort tabs in descending-used order to grab the most recently used michael@0: let tabs = this.getAllTabs(true).sort(function (a, b) { michael@0: return b.lastUsed - a.lastUsed; michael@0: }); michael@0: michael@0: // Figure out how many tabs we can pack into a payload. Starting with a 28KB michael@0: // payload, we can estimate various overheads from encryption/JSON/WBO. michael@0: let size = JSON.stringify(tabs).length; michael@0: let origLength = tabs.length; michael@0: const MAX_TAB_SIZE = 20000; michael@0: if (size > MAX_TAB_SIZE) { michael@0: // Estimate a little more than the direct fraction to maximize packing michael@0: let cutoff = Math.ceil(tabs.length * MAX_TAB_SIZE / size); michael@0: tabs = tabs.slice(0, cutoff + 1); michael@0: michael@0: // Keep dropping off the last entry until the data fits michael@0: while (JSON.stringify(tabs).length > MAX_TAB_SIZE) michael@0: tabs.pop(); michael@0: } michael@0: michael@0: this._log.trace("Created tabs " + tabs.length + " of " + origLength); michael@0: tabs.forEach(function (tab) { michael@0: this._log.trace("Wrapping tab: " + JSON.stringify(tab)); michael@0: }, this); michael@0: michael@0: record.tabs = tabs; michael@0: return record; michael@0: }, michael@0: michael@0: getAllIDs: function TabStore_getAllIds() { michael@0: // Don't report any tabs if all windows are in private browsing for michael@0: // first syncs. michael@0: let ids = {}; michael@0: let allWindowsArePrivate = false; michael@0: let wins = Services.wm.getEnumerator("navigator:browser"); michael@0: while (wins.hasMoreElements()) { michael@0: if (PrivateBrowsingUtils.isWindowPrivate(wins.getNext())) { michael@0: // Ensure that at least there is a private window. michael@0: allWindowsArePrivate = true; michael@0: } else { michael@0: // If there is a not private windown then finish and continue. michael@0: allWindowsArePrivate = false; michael@0: break; michael@0: } michael@0: } michael@0: michael@0: if (allWindowsArePrivate && michael@0: !PrivateBrowsingUtils.permanentPrivateBrowsing) { michael@0: return ids; michael@0: } michael@0: michael@0: ids[this.engine.service.clientsEngine.localID] = true; michael@0: return ids; michael@0: }, michael@0: michael@0: wipe: function TabStore_wipe() { michael@0: this._remoteClients = {}; michael@0: }, michael@0: michael@0: create: function TabStore_create(record) { michael@0: this._log.debug("Adding remote tabs from " + record.clientName); michael@0: this._remoteClients[record.id] = record.cleartext; michael@0: michael@0: // Lose some precision, but that's good enough (seconds) michael@0: let roundModify = Math.floor(record.modified / 1000); michael@0: let notifyState = Svc.Prefs.get("notifyTabState"); michael@0: // If there's no existing pref, save this first modified time michael@0: if (notifyState == null) michael@0: Svc.Prefs.set("notifyTabState", roundModify); michael@0: // Don't change notifyState if it's already 0 (don't notify) michael@0: else if (notifyState == 0) michael@0: return; michael@0: // We must have gotten a new tab that isn't the same as last time michael@0: else if (notifyState != roundModify) michael@0: Svc.Prefs.set("notifyTabState", 0); michael@0: }, michael@0: michael@0: update: function update(record) { michael@0: this._log.trace("Ignoring tab updates as local ones win"); michael@0: } michael@0: }; michael@0: michael@0: michael@0: function TabTracker(name, engine) { michael@0: Tracker.call(this, name, engine); michael@0: Svc.Obs.add("weave:engine:start-tracking", this); michael@0: Svc.Obs.add("weave:engine:stop-tracking", this); michael@0: michael@0: // Make sure "this" pointer is always set correctly for event listeners michael@0: this.onTab = Utils.bind2(this, this.onTab); michael@0: this._unregisterListeners = Utils.bind2(this, this._unregisterListeners); michael@0: } michael@0: TabTracker.prototype = { michael@0: __proto__: Tracker.prototype, michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), michael@0: michael@0: loadChangedIDs: function loadChangedIDs() { michael@0: // Don't read changed IDs from disk at start up. michael@0: }, michael@0: michael@0: clearChangedIDs: function clearChangedIDs() { michael@0: this.modified = false; michael@0: }, michael@0: michael@0: _topics: ["pageshow", "TabOpen", "TabClose", "TabSelect"], michael@0: _registerListenersForWindow: function registerListenersFW(window) { michael@0: this._log.trace("Registering tab listeners in window"); michael@0: for each (let topic in this._topics) { michael@0: window.addEventListener(topic, this.onTab, false); michael@0: } michael@0: window.addEventListener("unload", this._unregisterListeners, false); michael@0: }, michael@0: michael@0: _unregisterListeners: function unregisterListeners(event) { michael@0: this._unregisterListenersForWindow(event.target); michael@0: }, michael@0: michael@0: _unregisterListenersForWindow: function unregisterListenersFW(window) { michael@0: this._log.trace("Removing tab listeners in window"); michael@0: window.removeEventListener("unload", this._unregisterListeners, false); michael@0: for each (let topic in this._topics) { michael@0: window.removeEventListener(topic, this.onTab, false); michael@0: } michael@0: }, michael@0: michael@0: startTracking: function () { michael@0: Svc.Obs.add("domwindowopened", this); michael@0: let wins = Services.wm.getEnumerator("navigator:browser"); michael@0: while (wins.hasMoreElements()) { michael@0: this._registerListenersForWindow(wins.getNext()); michael@0: } michael@0: }, michael@0: michael@0: stopTracking: function () { michael@0: Svc.Obs.remove("domwindowopened", this); michael@0: let wins = Services.wm.getEnumerator("navigator:browser"); michael@0: while (wins.hasMoreElements()) { michael@0: this._unregisterListenersForWindow(wins.getNext()); michael@0: } michael@0: }, michael@0: michael@0: observe: function (subject, topic, data) { michael@0: Tracker.prototype.observe.call(this, subject, topic, data); michael@0: michael@0: switch (topic) { michael@0: case "domwindowopened": michael@0: let onLoad = () => { michael@0: subject.removeEventListener("load", onLoad, false); michael@0: // Only register after the window is done loading to avoid unloads. michael@0: this._registerListenersForWindow(subject); michael@0: }; michael@0: michael@0: // Add tab listeners now that a window has opened. michael@0: subject.addEventListener("load", onLoad, false); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: onTab: function onTab(event) { michael@0: if (event.originalTarget.linkedBrowser) { michael@0: let win = event.originalTarget.linkedBrowser.contentWindow; michael@0: if (PrivateBrowsingUtils.isWindowPrivate(win) && michael@0: !PrivateBrowsingUtils.permanentPrivateBrowsing) { michael@0: this._log.trace("Ignoring tab event from private browsing."); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: this._log.trace("onTab event: " + event.type); michael@0: this.modified = true; michael@0: michael@0: // For page shows, bump the score 10% of the time, emulating a partial michael@0: // score. We don't want to sync too frequently. For all other page michael@0: // events, always bump the score. michael@0: if (event.type != "pageshow" || Math.random() < .1) michael@0: this.score += SCORE_INCREMENT_SMALL; michael@0: }, michael@0: }