gecko-dev/services/cloudsync/CloudSyncTabs.jsm

319 lines
7.7 KiB
JavaScript

/* 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;
let 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;
}
};
let 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;
},
};
let 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 each (let tab in 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 each (let topic in topics) {
window.addEventListener(topic, update, false);
}
window.addEventListener("unload", unregisterListeners, false);
};
let unregisterListenersForWindow = function (window) {
window.removeEventListener("unload", unregisterListeners, false);
for each (let topic in 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;