summaryrefslogtreecommitdiffstats
path: root/services/sync/modules/SyncedTabs.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'services/sync/modules/SyncedTabs.sys.mjs')
-rw-r--r--services/sync/modules/SyncedTabs.sys.mjs332
1 files changed, 332 insertions, 0 deletions
diff --git a/services/sync/modules/SyncedTabs.sys.mjs b/services/sync/modules/SyncedTabs.sys.mjs
new file mode 100644
index 0000000000..9af388da8a
--- /dev/null
+++ b/services/sync/modules/SyncedTabs.sys.mjs
@@ -0,0 +1,332 @@
+/* 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/. */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+import { Log } from "resource://gre/modules/Log.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ CLIENT_NOT_CONFIGURED: "resource://services-sync/constants.sys.mjs",
+ Preferences: "resource://gre/modules/Preferences.sys.mjs",
+ Weave: "resource://services-sync/main.sys.mjs",
+});
+
+// The Sync XPCOM service
+XPCOMUtils.defineLazyGetter(lazy, "weaveXPCService", function () {
+ return Cc["@mozilla.org/weave/service;1"].getService(
+ Ci.nsISupports
+ ).wrappedJSObject;
+});
+
+// from MDN...
+function escapeRegExp(string) {
+ return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+}
+
+// A topic we fire whenever we have new tabs available. This might be due
+// to a request made by this module to refresh the tab list, or as the result
+// of a regularly scheduled sync. The intent is that consumers just listen
+// for this notification and update their UI in response.
+const TOPIC_TABS_CHANGED = "services.sync.tabs.changed";
+
+// The interval, in seconds, before which we consider the existing list
+// of tabs "fresh enough" and don't force a new sync.
+const TABS_FRESH_ENOUGH_INTERVAL_SECONDS = 30;
+
+XPCOMUtils.defineLazyGetter(lazy, "log", function () {
+ let log = Log.repository.getLogger("Sync.RemoteTabs");
+ log.manageLevelFromPref("services.sync.log.logger.tabs");
+ return log;
+});
+
+// A private singleton that does the work.
+let SyncedTabsInternal = {
+ /* Make a "tab" record. Returns a promise */
+ async _makeTab(client, tab, url, showRemoteIcons) {
+ let icon;
+ if (showRemoteIcons) {
+ icon = tab.icon;
+ }
+ if (!icon) {
+ // By not specifying a size the favicon service will pick the default,
+ // that is usually set through setDefaultIconURIPreferredSize by the
+ // first browser window. Commonly it's 16px at current dpi.
+ icon = "page-icon:" + url;
+ }
+ return {
+ type: "tab",
+ title: tab.title || url,
+ url,
+ icon,
+ client: client.id,
+ lastUsed: tab.lastUsed,
+ };
+ },
+
+ /* Make a "client" record. Returns a promise for consistency with _makeTab */
+ async _makeClient(client) {
+ return {
+ id: client.id,
+ type: "client",
+ name: lazy.Weave.Service.clientsEngine.getClientName(client.id),
+ clientType: lazy.Weave.Service.clientsEngine.getClientType(client.id),
+ lastModified: client.lastModified * 1000, // sec to ms
+ tabs: [],
+ };
+ },
+
+ _tabMatchesFilter(tab, filter) {
+ let reFilter = new RegExp(escapeRegExp(filter), "i");
+ return reFilter.test(tab.url) || reFilter.test(tab.title);
+ },
+
+ _createRecentTabsList(clients, maxCount) {
+ let tabs = [];
+
+ for (let client of clients) {
+ for (let tab of client.tabs) {
+ tab.device = client.name;
+ tab.deviceType = client.clientType;
+ }
+ tabs = [...tabs, ...client.tabs.reverse()];
+ }
+ tabs = this._filterRecentTabsDupes(tabs);
+ tabs = tabs.sort((a, b) => b.lastUsed - a.lastUsed).slice(0, maxCount);
+ return tabs;
+ },
+
+ _filterRecentTabsDupes(tabs) {
+ // Filter out any tabs with duplicate URLs preserving
+ // the duplicate with the most recent lastUsed value
+ return tabs.filter(tab => {
+ return !tabs.some(t => {
+ return t.url === tab.url && tab.lastUsed < t.lastUsed;
+ });
+ });
+ },
+
+ async getTabClients(filter) {
+ lazy.log.info("Generating tab list with filter", filter);
+ let result = [];
+
+ // If Sync isn't ready, don't try and get anything.
+ if (!lazy.weaveXPCService.ready) {
+ lazy.log.debug("Sync isn't yet ready, so returning an empty tab list");
+ return result;
+ }
+
+ // A boolean that controls whether we should show the icon from the remote tab.
+ const showRemoteIcons = lazy.Preferences.get(
+ "services.sync.syncedTabs.showRemoteIcons",
+ true
+ );
+
+ let engine = lazy.Weave.Service.engineManager.get("tabs");
+
+ let ntabs = 0;
+ let clientTabList = await engine.getAllClients();
+ for (let client of clientTabList) {
+ if (!lazy.Weave.Service.clientsEngine.remoteClientExists(client.id)) {
+ continue;
+ }
+ let clientRepr = await this._makeClient(client);
+ lazy.log.debug("Processing client", clientRepr);
+
+ for (let tab of client.tabs) {
+ let url = tab.urlHistory[0];
+ lazy.log.trace("remote tab", url);
+
+ if (!url) {
+ continue;
+ }
+ let tabRepr = await this._makeTab(client, tab, url, showRemoteIcons);
+ if (filter && !this._tabMatchesFilter(tabRepr, filter)) {
+ continue;
+ }
+ clientRepr.tabs.push(tabRepr);
+ }
+ // We return all clients, even those without tabs - the consumer should
+ // filter it if they care.
+ ntabs += clientRepr.tabs.length;
+ result.push(clientRepr);
+ }
+ lazy.log.info(
+ `Final tab list has ${result.length} clients with ${ntabs} tabs.`
+ );
+ return result;
+ },
+
+ async syncTabs(force) {
+ if (!force) {
+ // Don't bother refetching tabs if we already did so recently
+ let lastFetch = lazy.Preferences.get("services.sync.lastTabFetch", 0);
+ let now = Math.floor(Date.now() / 1000);
+ if (now - lastFetch < TABS_FRESH_ENOUGH_INTERVAL_SECONDS) {
+ lazy.log.info("_refetchTabs was done recently, do not doing it again");
+ return false;
+ }
+ }
+
+ // If Sync isn't configured don't try and sync, else we will get reports
+ // of a login failure.
+ if (lazy.Weave.Status.checkSetup() === lazy.CLIENT_NOT_CONFIGURED) {
+ lazy.log.info(
+ "Sync client is not configured, so not attempting a tab sync"
+ );
+ return false;
+ }
+ // If the primary pass is locked, we should not try to sync
+ if (lazy.Weave.Utils.mpLocked()) {
+ lazy.log.info(
+ "Can't sync tabs due to the primary password being locked",
+ lazy.Weave.Status.login
+ );
+ return false;
+ }
+ // Ask Sync to just do the tabs engine if it can.
+ try {
+ lazy.log.info("Doing a tab sync.");
+ await lazy.Weave.Service.sync({ why: "tabs", engines: ["tabs"] });
+ return true;
+ } catch (ex) {
+ lazy.log.error("Sync failed", ex);
+ throw ex;
+ }
+ },
+
+ observe(subject, topic, data) {
+ lazy.log.trace(`observed topic=${topic}, data=${data}, subject=${subject}`);
+ switch (topic) {
+ case "weave:engine:sync:finish":
+ if (data != "tabs") {
+ return;
+ }
+ // The tabs engine just finished syncing
+ // Set our lastTabFetch pref here so it tracks both explicit sync calls
+ // and normally scheduled ones.
+ lazy.Preferences.set(
+ "services.sync.lastTabFetch",
+ Math.floor(Date.now() / 1000)
+ );
+ Services.obs.notifyObservers(null, TOPIC_TABS_CHANGED);
+ break;
+ case "weave:service:start-over":
+ // start-over needs to notify so consumers find no tabs.
+ lazy.Preferences.reset("services.sync.lastTabFetch");
+ Services.obs.notifyObservers(null, TOPIC_TABS_CHANGED);
+ break;
+ case "nsPref:changed":
+ Services.obs.notifyObservers(null, TOPIC_TABS_CHANGED);
+ break;
+ default:
+ break;
+ }
+ },
+
+ // Returns true if Sync is configured to Sync tabs, false otherwise
+ get isConfiguredToSyncTabs() {
+ if (!lazy.weaveXPCService.ready) {
+ lazy.log.debug("Sync isn't yet ready; assuming tab engine is enabled");
+ return true;
+ }
+
+ let engine = lazy.Weave.Service.engineManager.get("tabs");
+ return engine && engine.enabled;
+ },
+
+ get hasSyncedThisSession() {
+ let engine = lazy.Weave.Service.engineManager.get("tabs");
+ return engine && engine.hasSyncedThisSession;
+ },
+};
+
+Services.obs.addObserver(SyncedTabsInternal, "weave:engine:sync:finish");
+Services.obs.addObserver(SyncedTabsInternal, "weave:service:start-over");
+// Observe the pref the indicates the state of the tabs engine has changed.
+// This will force consumers to re-evaluate the state of sync and update
+// accordingly.
+Services.prefs.addObserver("services.sync.engine.tabs", SyncedTabsInternal);
+
+// The public interface.
+export var SyncedTabs = {
+ // A mock-point for tests.
+ _internal: SyncedTabsInternal,
+
+ // We make the topic for the observer notification public.
+ TOPIC_TABS_CHANGED,
+
+ // Expose the interval used to determine if synced tabs data needs a new sync
+ TABS_FRESH_ENOUGH_INTERVAL_SECONDS,
+
+ // Returns true if Sync is configured to Sync tabs, false otherwise
+ get isConfiguredToSyncTabs() {
+ return this._internal.isConfiguredToSyncTabs;
+ },
+
+ // Returns true if a tab sync has completed once this session. If this
+ // returns false, then getting back no clients/tabs possibly just means we
+ // are waiting for that first sync to complete.
+ get hasSyncedThisSession() {
+ return this._internal.hasSyncedThisSession;
+ },
+
+ // Return a promise that resolves with an array of client records, each with
+ // a .tabs array. Note that part of the contract for this module is that the
+ // returned objects are not shared between invocations, so callers are free
+ // to mutate the returned objects (eg, sort, truncate) however they see fit.
+ getTabClients(query) {
+ return this._internal.getTabClients(query);
+ },
+
+ // Starts a background request to start syncing tabs. Returns a promise that
+ // resolves when the sync is complete, but there's no resolved value -
+ // callers should be listening for TOPIC_TABS_CHANGED.
+ // If |force| is true we always sync. If false, we only sync if the most
+ // recent sync wasn't "recently".
+ syncTabs(force) {
+ return this._internal.syncTabs(force);
+ },
+
+ sortTabClientsByLastUsed(clients) {
+ // First sort the list of tabs for each client. Note that
+ // this module promises that the objects it returns are never
+ // shared, so we are free to mutate those objects directly.
+ for (let client of clients) {
+ let tabs = client.tabs;
+ tabs.sort((a, b) => b.lastUsed - a.lastUsed);
+ }
+ // Now sort the clients - the clients are sorted in the order of the
+ // most recent tab for that client (ie, it is important the tabs for
+ // each client are already sorted.)
+ clients.sort((a, b) => {
+ if (!a.tabs.length) {
+ return 1; // b comes first.
+ }
+ if (!b.tabs.length) {
+ return -1; // a comes first.
+ }
+ return b.tabs[0].lastUsed - a.tabs[0].lastUsed;
+ });
+ },
+
+ recordSyncedTabsTelemetry(object, tabEvent, extraOptions) {
+ Services.telemetry.setEventRecordingEnabled("synced_tabs", true);
+ Services.telemetry.recordEvent(
+ "synced_tabs",
+ tabEvent,
+ object,
+ null,
+ extraOptions
+ );
+ },
+
+ // Get list of synced tabs across all devices/clients
+ // truncated by value of maxCount param, sorted by
+ // lastUsed value, and filtered for duplicate URLs
+ async getRecentTabs(maxCount) {
+ let clients = await this.getTabClients();
+ return this._internal._createRecentTabsList(clients, maxCount);
+ },
+};