summaryrefslogtreecommitdiffstats
path: root/services/sync/modules/engines/tabs.js
diff options
context:
space:
mode:
Diffstat (limited to 'services/sync/modules/engines/tabs.js')
-rw-r--r--services/sync/modules/engines/tabs.js395
1 files changed, 395 insertions, 0 deletions
diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js
new file mode 100644
index 0000000000..99ff59ca8b
--- /dev/null
+++ b/services/sync/modules/engines/tabs.js
@@ -0,0 +1,395 @@
+/* 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/. */
+
+var EXPORTED_SYMBOLS = ["TabEngine", "TabSetRecord"];
+
+const TABS_TTL = 31622400; // 366 days (1 leap year).
+const TAB_ENTRIES_LIMIT = 5; // How many URLs to include in tab history.
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { Log } = ChromeUtils.import("resource://gre/modules/Log.jsm");
+const { Store, SyncEngine, Tracker } = ChromeUtils.import(
+ "resource://services-sync/engines.js"
+);
+const { CryptoWrapper } = ChromeUtils.import(
+ "resource://services-sync/record.js"
+);
+const { Svc, Utils } = ChromeUtils.import("resource://services-sync/util.js");
+const { SCORE_INCREMENT_SMALL, URI_LENGTH_MAX } = ChromeUtils.import(
+ "resource://services-sync/constants.js"
+);
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "SessionStore",
+ "resource:///modules/sessionstore/SessionStore.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
+});
+
+function TabSetRecord(collection, id) {
+ CryptoWrapper.call(this, collection, id);
+}
+TabSetRecord.prototype = {
+ __proto__: CryptoWrapper.prototype,
+ _logName: "Sync.Record.Tabs",
+ ttl: TABS_TTL,
+};
+
+Utils.deferGetSet(TabSetRecord, "cleartext", ["clientName", "tabs"]);
+
+function TabEngine(service) {
+ SyncEngine.call(this, "Tabs", service);
+}
+TabEngine.prototype = {
+ __proto__: SyncEngine.prototype,
+ _storeObj: TabStore,
+ _trackerObj: TabTracker,
+ _recordObj: TabSetRecord,
+
+ syncPriority: 3,
+
+ async initialize() {
+ await SyncEngine.prototype.initialize.call(this);
+
+ // Reset the client on every startup so that we fetch recent tabs.
+ await this._resetClient();
+ },
+
+ async getChangedIDs() {
+ // No need for a proper timestamp (no conflict resolution needed).
+ let changedIDs = {};
+ if (this._tracker.modified) {
+ changedIDs[this.service.clientsEngine.localID] = 0;
+ }
+ return changedIDs;
+ },
+
+ // API for use by Sync UI code to give user choices of tabs to open.
+ getAllClients() {
+ return this._store._remoteClients;
+ },
+
+ getClientById(id) {
+ return this._store._remoteClients[id];
+ },
+
+ async _resetClient() {
+ await SyncEngine.prototype._resetClient.call(this);
+ await this._store.wipe();
+ this._tracker.modified = true;
+ },
+
+ async removeClientData() {
+ let url = this.engineURL + "/" + this.service.clientsEngine.localID;
+ await this.service.resource(url).delete();
+ },
+
+ async _reconcile(item) {
+ // Skip our own record.
+ // TabStore.itemExists tests only against our local client ID.
+ if (await this._store.itemExists(item.id)) {
+ this._log.trace(
+ "Ignoring incoming tab item because of its id: " + item.id
+ );
+ return false;
+ }
+
+ return SyncEngine.prototype._reconcile.call(this, item);
+ },
+
+ async trackRemainingChanges() {
+ if (this._modified.count() > 0) {
+ this._tracker.modified = true;
+ }
+ },
+};
+
+function TabStore(name, engine) {
+ Store.call(this, name, engine);
+}
+TabStore.prototype = {
+ __proto__: Store.prototype,
+
+ async itemExists(id) {
+ return id == this.engine.service.clientsEngine.localID;
+ },
+
+ getWindowEnumerator() {
+ return Services.wm.getEnumerator("navigator:browser");
+ },
+
+ shouldSkipWindow(win) {
+ return win.closed || PrivateBrowsingUtils.isWindowPrivate(win);
+ },
+
+ getTabState(tab) {
+ return JSON.parse(SessionStore.getTabState(tab));
+ },
+
+ async getAllTabs(filter) {
+ let filteredUrls = new RegExp(
+ Svc.Prefs.get("engine.tabs.filteredUrls"),
+ "i"
+ );
+
+ let allTabs = [];
+
+ for (let win of this.getWindowEnumerator()) {
+ if (this.shouldSkipWindow(win)) {
+ continue;
+ }
+
+ for (let tab of win.gBrowser.tabs) {
+ let tabState = this.getTabState(tab);
+
+ // Make sure there are history entries to look at.
+ if (!tabState || !tabState.entries.length) {
+ continue;
+ }
+
+ let acceptable = !filter
+ ? url => url
+ : url => url && !filteredUrls.test(url);
+
+ let entries = tabState.entries;
+ let index = tabState.index;
+ let current = entries[index - 1];
+
+ // We ignore the tab completely if the current entry url is
+ // not acceptable (we need something accurate to open).
+ if (!acceptable(current.url)) {
+ continue;
+ }
+
+ if (current.url.length > URI_LENGTH_MAX) {
+ this._log.trace("Skipping over-long URL.");
+ continue;
+ }
+
+ // The element at `index` is the current page. Previous URLs were
+ // previously visited URLs; subsequent URLs are in the 'forward' stack,
+ // which we can't represent in Sync, so we truncate here.
+ let candidates =
+ entries.length == index ? entries : entries.slice(0, index);
+
+ let urls = candidates
+ .map(entry => entry.url)
+ .filter(acceptable)
+ .reverse(); // Because Sync puts current at index 0, and history after.
+
+ // Truncate if necessary.
+ if (urls.length > TAB_ENTRIES_LIMIT) {
+ urls.length = TAB_ENTRIES_LIMIT;
+ }
+
+ // tabState has .image, but it's a large data: url. So we ask the favicon service for the url.
+ let icon = "";
+ try {
+ let iconData = await PlacesUtils.promiseFaviconData(urls[0]);
+ icon = iconData.uri.spec;
+ } catch (ex) {
+ this._log.trace(`Failed to fetch favicon for ${urls[0]}`, ex);
+ }
+ allTabs.push({
+ title: current.title || "",
+ urlHistory: urls,
+ icon,
+ lastUsed: Math.floor((tabState.lastAccessed || 0) / 1000),
+ });
+ }
+ }
+
+ return allTabs;
+ },
+
+ async createRecord(id, collection) {
+ let record = new TabSetRecord(collection, id);
+ record.clientName = this.engine.service.clientsEngine.localName;
+
+ // Sort tabs in descending-used order to grab the most recently used
+ let tabs = (await this.getAllTabs(true)).sort(function(a, b) {
+ return b.lastUsed - a.lastUsed;
+ });
+ const maxPayloadSize = this.engine.service.getMemcacheMaxRecordPayloadSize();
+ let records = Utils.tryFitItems(tabs, maxPayloadSize);
+
+ if (records.length != tabs.length) {
+ this._log.warn(
+ `Can't fit all tabs in sync payload: have ${tabs.length}, but can only fit ${records.length}.`
+ );
+ }
+
+ if (this._log.level <= Log.Level.Trace) {
+ records.forEach(tab => {
+ this._log.trace("Wrapping tab: ", tab);
+ });
+ }
+
+ record.tabs = records;
+ return record;
+ },
+
+ async getAllIDs() {
+ // Don't report any tabs if all windows are in private browsing for
+ // first syncs.
+ let ids = {};
+ let allWindowsArePrivate = false;
+ for (let win of Services.wm.getEnumerator("navigator:browser")) {
+ if (PrivateBrowsingUtils.isWindowPrivate(win)) {
+ // Ensure that at least there is a private window.
+ allWindowsArePrivate = true;
+ } else {
+ // If there is a not private windown then finish and continue.
+ allWindowsArePrivate = false;
+ break;
+ }
+ }
+
+ if (
+ allWindowsArePrivate &&
+ !PrivateBrowsingUtils.permanentPrivateBrowsing
+ ) {
+ return ids;
+ }
+
+ ids[this.engine.service.clientsEngine.localID] = true;
+ return ids;
+ },
+
+ async wipe() {
+ this._remoteClients = {};
+ },
+
+ async create(record) {
+ this._log.debug("Adding remote tabs from " + record.id);
+ this._remoteClients[record.id] = Object.assign({}, record.cleartext, {
+ lastModified: record.modified,
+ });
+ },
+
+ async update(record) {
+ this._log.trace("Ignoring tab updates as local ones win");
+ },
+};
+
+function TabTracker(name, engine) {
+ Tracker.call(this, name, engine);
+
+ // Make sure "this" pointer is always set correctly for event listeners.
+ this.onTab = Utils.bind2(this, this.onTab);
+ this._unregisterListeners = Utils.bind2(this, this._unregisterListeners);
+}
+TabTracker.prototype = {
+ __proto__: Tracker.prototype,
+
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+
+ clearChangedIDs() {
+ this.modified = false;
+ },
+
+ _topics: ["pageshow", "TabOpen", "TabClose", "TabSelect"],
+
+ _registerListenersForWindow(window) {
+ this._log.trace("Registering tab listeners in window");
+ for (let topic of this._topics) {
+ window.addEventListener(topic, this.onTab);
+ }
+ window.addEventListener("unload", this._unregisterListeners);
+ // If it's got a tab browser we can listen for things like navigation.
+ if (window.gBrowser) {
+ window.gBrowser.addProgressListener(this);
+ }
+ },
+
+ _unregisterListeners(event) {
+ this._unregisterListenersForWindow(event.target);
+ },
+
+ _unregisterListenersForWindow(window) {
+ this._log.trace("Removing tab listeners in window");
+ window.removeEventListener("unload", this._unregisterListeners);
+ for (let topic of this._topics) {
+ window.removeEventListener(topic, this.onTab);
+ }
+ if (window.gBrowser) {
+ window.gBrowser.removeProgressListener(this);
+ }
+ },
+
+ onStart() {
+ Svc.Obs.add("domwindowopened", this.asyncObserver);
+ for (let win of Services.wm.getEnumerator("navigator:browser")) {
+ this._registerListenersForWindow(win);
+ }
+ },
+
+ onStop() {
+ Svc.Obs.remove("domwindowopened", this.asyncObserver);
+ for (let win of Services.wm.getEnumerator("navigator:browser")) {
+ this._unregisterListenersForWindow(win);
+ }
+ },
+
+ async observe(subject, topic, data) {
+ switch (topic) {
+ case "domwindowopened":
+ let onLoad = () => {
+ subject.removeEventListener("load", onLoad);
+ // Only register after the window is done loading to avoid unloads.
+ this._registerListenersForWindow(subject);
+ };
+
+ // Add tab listeners now that a window has opened.
+ subject.addEventListener("load", onLoad);
+ break;
+ }
+ },
+
+ onTab(event) {
+ if (event.originalTarget.linkedBrowser) {
+ let browser = event.originalTarget.linkedBrowser;
+ if (
+ PrivateBrowsingUtils.isBrowserPrivate(browser) &&
+ !PrivateBrowsingUtils.permanentPrivateBrowsing
+ ) {
+ this._log.trace("Ignoring tab event from private browsing.");
+ return;
+ }
+ }
+
+ this._log.trace("onTab event: " + event.type);
+ this.modified = true;
+
+ // For page shows, bump the score 10% of the time, emulating a partial
+ // score. We don't want to sync too frequently. For all other page
+ // events, always bump the score.
+ if (event.type != "pageshow" || Math.random() < 0.1) {
+ this.score += SCORE_INCREMENT_SMALL;
+ }
+ },
+
+ // web progress listeners.
+ onLocationChange(webProgress, request, location, flags) {
+ // We only care about top-level location changes which are not in the same
+ // document.
+ if (
+ webProgress.isTopLevel &&
+ (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) == 0
+ ) {
+ this.modified = true;
+ }
+ },
+};