Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  SyncedTabs.sys.mjs   Sprache: unbekannt

 
/* 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";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  CLIENT_NOT_CONFIGURED: "resource://services-sync/constants.sys.mjs",
  Weave: "resource://services-sync/main.sys.mjs",
  getRemoteCommandStore: "resource://services-sync/TabsStore.sys.mjs",
  RemoteCommand: "resource://services-sync/TabsStore.sys.mjs",
  FxAccounts: "resource://gre/modules/FxAccounts.sys.mjs",
});

// The Sync XPCOM service
ChromeUtils.defineLazyGetter(lazy, "weaveXPCService", function () {
  return Cc["@mozilla.org/weave/service;1"].getService(Ci.nsISupports)
    .wrappedJSObject;
});

ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => {
  return ChromeUtils.importESModule(
    "resource://gre/modules/FxAccounts.sys.mjs"
  ).getFxAccountsSingleton();
});

// 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";

// A topic we fire whenever we have queued a new remote tabs command.
const TOPIC_TABS_COMMAND_QUEUED = "services.sync.tabs.command-queued";

// 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;

ChromeUtils.defineLazyGetter(lazy, "log", () => {
  const { Log } = ChromeUtils.importESModule(
    "resource://gre/modules/Log.sys.mjs"
  );
  let log = Log.repository.getLogger("Sync.RemoteTabs");
  log.manageLevelFromPref("services.sync.log.logger.tabs");
  return log;
});

// We allow some test preferences to simulate many and inactive tabs.
XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "NUM_FAKE_INACTIVE_TABS",
  "services.sync.syncedTabs.numFakeInactiveTabs",
  0
);

XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "NUM_FAKE_ACTIVE_TABS",
  "services.sync.syncedTabs.numFakeActiveTabs",
  0
);

// 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,
      inactive: tab.inactive,
    };
  },

  /* 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);
  },

  // A wrapper for grabbing the fxaDeviceId, to make it easier for stubbing
  // for tests
  _getClientFxaDeviceId(clientId) {
    return lazy.Weave.Service.clientsEngine.getClientFxaDeviceId(clientId);
  },

  _createRecentTabsList(
    clients,
    maxCount,
    extraParams = { removeAllDupes: true, removeDeviceDupes: false }
  ) {
    let tabs = [];

    for (let client of clients) {
      if (extraParams.removeDeviceDupes) {
        client.tabs = this._filterRecentTabsDupes(client.tabs);
      }

      // We have the client obj but we need the FxA device obj so we use the clients
      // engine to get us the FxA device
      let device =
        lazy.fxAccounts.device.recentDeviceList &&
        lazy.fxAccounts.device.recentDeviceList.find(
          d => d.id === this._getClientFxaDeviceId(client.id)
        );

      for (let tab of client.tabs) {
        tab.device = client.name;
        tab.deviceType = client.clientType;
        // Surface broadcasted commmands for things like close remote tab
        tab.fxaDeviceId = device.id;
        tab.availableCommands = device.availableCommands;
      }
      tabs = [...tabs, ...client.tabs.reverse()];
    }
    if (extraParams.removeAllDupes) {
      tabs = this._filterRecentTabsDupes(tabs);
    }
    tabs = tabs.sort((a, b) => b.lastUsed - a.lastUsed).slice(0, maxCount);
    return tabs;
  },

  // Filter out any tabs with duplicate URLs preserving
  // the duplicate with the most recent lastUsed value
  _filterRecentTabsDupes(tabs) {
    const tabMap = new Map();
    for (const tab of tabs) {
      const existingTab = tabMap.get(tab.url);
      if (!existingTab || tab.lastUsed > existingTab.lastUsed) {
        tabMap.set(tab.url, tab);
      }
    }
    return Array.from(tabMap.values());
  },

  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 = Services.prefs.getBoolPref(
      "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);

      let tabs = Array.from(client.tabs); // avoid modifying in-place.
      // For QA, UX, etc, we allow "fake tabs" to be added to each device.
      for (let i = 0; i < lazy.NUM_FAKE_INACTIVE_TABS; i++) {
        tabs.push({
          icon: null,
          lastUsed: 1000,
          title: `Fake inactive tab ${i}`,
          urlHistory: [`https://example.com/inactive/${i}`],
          inactive: true,
        });
      }
      for (let i = 0; i < lazy.NUM_FAKE_ACTIVE_TABS; i++) {
        tabs.push({
          icon: null,
          lastUsed: Date.now() - 1000 + i,
          title: `Fake tab ${i}`,
          urlHistory: [`https://example.com/${i}`],
        });
      }

      for (let tab of 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);
      }

      // Filter out duplicate tabs based on URL
      clientRepr.tabs = this._filterRecentTabsDupes(clientRepr.tabs);

      // 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 = Services.prefs.getIntPref(
        "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.
        Services.prefs.setIntPref(
          "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.
        Services.prefs.clearUserPref("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);
  },

  createRecentTabsList(clients, maxCount, extraParams) {
    return this._internal._createRecentTabsList(clients, maxCount, extraParams);
  },

  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) {
    if (
      !["fxa_avatar_menu", "fxa_app_menu", "synced_tabs_sidebar"].includes(
        object
      )
    ) {
      return;
    }
    object = object
      .split("_")
      .map(word => word[0].toUpperCase() + word.slice(1))
      .join("");
    Glean.syncedTabs[tabEvent + object].record(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, extraParams) {
    let clients = await this.getTabClients();
    return this._internal._createRecentTabsList(clients, maxCount, extraParams);
  },
};

// Remote tab management public interface.
export var SyncedTabsManagement = {
  // A mock-point for tests.
  async _getStore() {
    return await lazy.getRemoteCommandStore();
  },

  /// Enqueue a tab to close on a remote device.
  async enqueueTabToClose(deviceId, url) {
    let store = await this._getStore();
    let command = new lazy.RemoteCommand.CloseTab(url);
    if (!store.addRemoteCommand(deviceId, command)) {
      lazy.log.warn(
        "Could not queue a remote tab close - it was already queued"
      );
    } else {
      lazy.log.info("Queued remote tab close command.");
    }
    // fxAccounts commands infrastructure is lazily initialized, at which point
    // it registers observers etc - make sure it's initialized;
    lazy.FxAccounts.commands;
    Services.obs.notifyObservers(null, TOPIC_TABS_COMMAND_QUEUED);
  },

  /// Remove a tab from the queue of commands for a remote device.
  async removePendingTabToClose(deviceId, url) {
    let store = await this._getStore();
    let command = new lazy.RemoteCommand.CloseTab(url);
    if (!store.removeRemoteCommand(deviceId, command)) {
      lazy.log.warn("Could not remove a remote tab close - it was not queued");
    } else {
      lazy.log.info("Removed queued remote tab close command.");
    }
  },
};

[ zur Elbe Produktseite wechseln0.56Quellennavigators  Analyse erneut starten  ]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge