services/sync/modules/engines/tabs.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

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

mercurial