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

     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/. */
     5 this.EXPORTED_SYMBOLS = ['TabEngine', 'TabSetRecord'];
     7 const Cc = Components.classes;
     8 const Ci = Components.interfaces;
     9 const Cu = Components.utils;
    11 const TABS_TTL = 604800; // 7 days
    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");
    21 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
    22   "resource://gre/modules/PrivateBrowsingUtils.jsm");
    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 };
    33 Utils.deferGetSet(TabSetRecord, "cleartext", ["clientName", "tabs"]);
    36 this.TabEngine = function TabEngine(service) {
    37   SyncEngine.call(this, "Tabs", service);
    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,
    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   },
    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   },
    61   getClientById: function TabEngine_getClientById(id) {
    62     return this._store._remoteClients[id];
    63   },
    65   _resetClient: function TabEngine__resetClient() {
    66     SyncEngine.prototype._resetClient.call(this);
    67     this._store.wipe();
    68     this._tracker.modified = true;
    69   },
    71   removeClientData: function removeClientData() {
    72     let url = this.engineURL + "/" + this.service.clientsEngine.localID;
    73     this.service.resource(url).delete();
    74   },
    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 };
    89 function TabStore(name, engine) {
    90   Store.call(this, name, engine);
    91 }
    92 TabStore.prototype = {
    93   __proto__: Store.prototype,
    95   itemExists: function TabStore_itemExists(id) {
    96     return id == this.engine.service.clientsEngine.localID;
    97   },
    99   getWindowEnumerator: function () {
   100     return Services.wm.getEnumerator("navigator:browser");
   101   },
   103   shouldSkipWindow: function (win) {
   104     return win.closed ||
   105            PrivateBrowsingUtils.isWindowPrivate(win);
   106   },
   108   getTabState: function (tab) {
   109     return JSON.parse(Svc.Session.getTabState(tab));
   110   },
   112   getAllTabs: function (filter) {
   113     let filteredUrls = new RegExp(Svc.Prefs.get("engine.tabs.filteredUrls"), "i");
   115     let allTabs = [];
   117     let winEnum = this.getWindowEnumerator();
   118     while (winEnum.hasMoreElements()) {
   119       let win = winEnum.getNext();
   120       if (this.shouldSkipWindow(win)) {
   121         continue;
   122       }
   124       for (let tab of win.gBrowser.tabs) {
   125         tabState = this.getTabState(tab);
   127         // Make sure there are history entries to look at.
   128         if (!tabState || !tabState.entries.length) {
   129           continue;
   130         }
   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];
   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         }
   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     }
   153     return allTabs;
   154   },
   156   createRecord: function createRecord(id, collection) {
   157     let record = new TabSetRecord(collection, id);
   158     record.clientName = this.engine.service.clientsEngine.localName;
   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     });
   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);
   175       // Keep dropping off the last entry until the data fits
   176       while (JSON.stringify(tabs).length > MAX_TAB_SIZE)
   177         tabs.pop();
   178     }
   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);
   185     record.tabs = tabs;
   186     return record;
   187   },
   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     }
   206     if (allWindowsArePrivate &&
   207         !PrivateBrowsingUtils.permanentPrivateBrowsing) {
   208       return ids;
   209     }
   211     ids[this.engine.service.clientsEngine.localID] = true;
   212     return ids;
   213   },
   215   wipe: function TabStore_wipe() {
   216     this._remoteClients = {};
   217   },
   219   create: function TabStore_create(record) {
   220     this._log.debug("Adding remote tabs from " + record.clientName);
   221     this._remoteClients[record.id] = record.cleartext;
   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   },
   237   update: function update(record) {
   238     this._log.trace("Ignoring tab updates as local ones win");
   239   }
   240 };
   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);
   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,
   255   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
   257   loadChangedIDs: function loadChangedIDs() {
   258     // Don't read changed IDs from disk at start up.
   259   },
   261   clearChangedIDs: function clearChangedIDs() {
   262     this.modified = false;
   263   },
   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   },
   274   _unregisterListeners: function unregisterListeners(event) {
   275     this._unregisterListenersForWindow(event.target);
   276   },
   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   },
   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   },
   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   },
   302   observe: function (subject, topic, data) {
   303     Tracker.prototype.observe.call(this, subject, topic, data);
   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         };
   313         // Add tab listeners now that a window has opened.
   314         subject.addEventListener("load", onLoad, false);
   315         break;
   316     }
   317   },
   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     }
   329     this._log.trace("onTab event: " + event.type);
   330     this.modified = true;
   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 }

mercurial