diff options
Diffstat (limited to 'services/cloudsync/CloudSyncTabs.jsm')
-rw-r--r-- | services/cloudsync/CloudSyncTabs.jsm | 318 |
1 files changed, 318 insertions, 0 deletions
diff --git a/services/cloudsync/CloudSyncTabs.jsm b/services/cloudsync/CloudSyncTabs.jsm new file mode 100644 index 000000000..7debc2678 --- /dev/null +++ b/services/cloudsync/CloudSyncTabs.jsm @@ -0,0 +1,318 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["Tabs"]; + +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/CloudSyncEventSource.jsm"); +Cu.import("resource://gre/modules/Promise.jsm"); +Cu.import("resource://services-common/observers.js"); + +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"); +XPCOMUtils.defineLazyServiceGetter(this, "Session", "@mozilla.org/browser/sessionstore;1", "nsISessionStore"); + +const DATA_VERSION = 1; + +var ClientRecord = function (params) { + this.id = params.id; + this.name = params.name || "?"; + this.tabs = new Set(); +} + +ClientRecord.prototype = { + version: DATA_VERSION, + + update: function (params) { + if (this.id !== params.id) { + throw new Error("expected " + this.id + " to equal " + params.id); + } + + this.name = params.name; + } +}; + +var TabRecord = function (params) { + this.url = params.url || ""; + this.update(params); +}; + +TabRecord.prototype = { + version: DATA_VERSION, + + update: function (params) { + if (this.url && this.url !== params.url) { + throw new Error("expected " + this.url + " to equal " + params.url); + } + + if (params.lastUsed && params.lastUsed < this.lastUsed) { + return; + } + + this.title = params.title || ""; + this.icon = params.icon || ""; + this.lastUsed = params.lastUsed || 0; + }, +}; + +var TabCache = function () { + this.tabs = new Map(); + this.clients = new Map(); +}; + +TabCache.prototype = { + merge: function (client, tabs) { + if (!client || !client.id) { + return; + } + + if (!tabs) { + return; + } + + let cRecord; + if (this.clients.has(client.id)) { + try { + cRecord = this.clients.get(client.id); + } catch (e) { + throw new Error("unable to update client: " + e); + } + } else { + cRecord = new ClientRecord(client); + this.clients.set(cRecord.id, cRecord); + } + + for (let tab of tabs) { + if (!tab || 'object' !== typeof(tab)) { + continue; + } + + let tRecord; + if (this.tabs.has(tab.url)) { + tRecord = this.tabs.get(tab.url); + try { + tRecord.update(tab); + } catch (e) { + throw new Error("unable to update tab: " + e); + } + } else { + tRecord = new TabRecord(tab); + this.tabs.set(tRecord.url, tRecord); + } + + if (tab.deleted) { + cRecord.tabs.delete(tRecord); + } else { + cRecord.tabs.add(tRecord); + } + } + }, + + clear: function (client) { + if (client) { + this.clients.delete(client.id); + } else { + this.clients = new Map(); + this.tabs = new Map(); + } + }, + + get: function () { + let results = []; + for (let client of this.clients.values()) { + results.push(client); + } + return results; + }, + + isEmpty: function () { + return 0 == this.clients.size; + }, + +}; + +this.Tabs = function () { + let suspended = true; + + let topics = [ + "pageshow", + "TabOpen", + "TabClose", + "TabSelect", + ]; + + let update = function (event) { + if (event.originalTarget.linkedBrowser) { + if (PrivateBrowsingUtils.isBrowserPrivate(event.originalTarget.linkedBrowser) && + !PrivateBrowsingUtils.permanentPrivateBrowsing) { + return; + } + } + + eventSource.emit("change"); + }; + + let registerListenersForWindow = function (window) { + for (let topic of topics) { + window.addEventListener(topic, update, false); + } + window.addEventListener("unload", unregisterListeners, false); + }; + + let unregisterListenersForWindow = function (window) { + window.removeEventListener("unload", unregisterListeners, false); + for (let topic of topics) { + window.removeEventListener(topic, update, false); + } + }; + + let unregisterListeners = function (event) { + unregisterListenersForWindow(event.target); + }; + + let observer = { + observe: function (subject, topic, data) { + switch (topic) { + case "domwindowopened": + let onLoad = () => { + subject.removeEventListener("load", onLoad, false); + // Only register after the window is done loading to avoid unloads. + registerListenersForWindow(subject); + }; + + // Add tab listeners now that a window has opened. + subject.addEventListener("load", onLoad, false); + break; + } + } + }; + + let resume = function () { + if (suspended) { + Observers.add("domwindowopened", observer); + let wins = Services.wm.getEnumerator("navigator:browser"); + while (wins.hasMoreElements()) { + registerListenersForWindow(wins.getNext()); + } + } + }.bind(this); + + let suspend = function () { + if (!suspended) { + Observers.remove("domwindowopened", observer); + let wins = Services.wm.getEnumerator("navigator:browser"); + while (wins.hasMoreElements()) { + unregisterListenersForWindow(wins.getNext()); + } + } + }.bind(this); + + let eventTypes = [ + "change", + ]; + + let eventSource = new EventSource(eventTypes, suspend, resume); + + let tabCache = new TabCache(); + + let getWindowEnumerator = function () { + return Services.wm.getEnumerator("navigator:browser"); + }; + + let shouldSkipWindow = function (win) { + return win.closed || + PrivateBrowsingUtils.isWindowPrivate(win); + }; + + let getTabState = function (tab) { + return JSON.parse(Session.getTabState(tab)); + }; + + let getLocalTabs = function (filter) { + let deferred = Promise.defer(); + + filter = (undefined === filter) ? true : filter; + let filteredUrls = new RegExp("^(about:.*|chrome://weave/.*|wyciwyg:.*|file:.*)$"); // FIXME: should be a pref (B#1044304) + + let allTabs = []; + + let currentState = JSON.parse(Session.getBrowserState()); + currentState.windows.forEach(function (window) { + if (window.isPrivate) { + return; + } + window.tabs.forEach(function (tab) { + if (!tab.entries.length) { + return; + } + + // Get only the latest entry + // FIXME: support full history (B#1044306) + let entry = tab.entries[tab.index - 1]; + + if (!entry.url || filter && filteredUrls.test(entry.url)) { + return; + } + + allTabs.push(new TabRecord({ + title: entry.title, + url: entry.url, + icon: tab.attributes && tab.attributes.image || "", + lastUsed: tab.lastAccessed, + })); + }); + }); + + deferred.resolve(allTabs); + + return deferred.promise; + }; + + let mergeRemoteTabs = function (client, tabs) { + let deferred = Promise.defer(); + + deferred.resolve(tabCache.merge(client, tabs)); + Observers.notify("cloudsync:tabs:update"); + + return deferred.promise; + }; + + let clearRemoteTabs = function (client) { + let deferred = Promise.defer(); + + deferred.resolve(tabCache.clear(client)); + Observers.notify("cloudsync:tabs:update"); + + return deferred.promise; + }; + + let getRemoteTabs = function () { + let deferred = Promise.defer(); + + deferred.resolve(tabCache.get()); + + return deferred.promise; + }; + + let hasRemoteTabs = function () { + return !tabCache.isEmpty(); + }; + + /* PUBLIC API */ + this.addEventListener = eventSource.addEventListener; + this.removeEventListener = eventSource.removeEventListener; + this.getLocalTabs = getLocalTabs.bind(this); + this.mergeRemoteTabs = mergeRemoteTabs.bind(this); + this.clearRemoteTabs = clearRemoteTabs.bind(this); + this.getRemoteTabs = getRemoteTabs.bind(this); + this.hasRemoteTabs = hasRemoteTabs.bind(this); +}; + +Tabs.prototype = { +}; +this.Tabs = Tabs; |