2014-09-04 12:44:00 -07:00
|
|
|
/* 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;
|
|
|
|
|
2015-09-15 11:19:45 -07:00
|
|
|
var ClientRecord = function (params) {
|
2014-09-04 12:44:00 -07:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2015-09-15 11:19:45 -07:00
|
|
|
var TabRecord = function (params) {
|
2014-09-04 12:44:00 -07:00
|
|
|
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;
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2015-09-15 11:19:45 -07:00
|
|
|
var TabCache = function () {
|
2014-09-04 12:44:00 -07:00
|
|
|
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) {
|
2014-09-18 08:56:55 -07:00
|
|
|
if (PrivateBrowsingUtils.isBrowserPrivate(event.originalTarget.linkedBrowser) &&
|
2014-09-04 12:44:00 -07:00
|
|
|
!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;
|