tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

UrlbarProviderRemoteTabs.sys.mjs (7740B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 /**
      6 * This module exports a provider that offers remote tabs.
      7 */
      8 
      9 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
     10 
     11 import {
     12  UrlbarProvider,
     13  UrlbarUtils,
     14 } from "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs";
     15 
     16 const lazy = {};
     17 
     18 ChromeUtils.defineESModuleGetters(lazy, {
     19  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
     20  SyncedTabs: "resource://services-sync/SyncedTabs.sys.mjs",
     21  UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs",
     22  UrlbarResult: "moz-src:///browser/components/urlbar/UrlbarResult.sys.mjs",
     23  UrlbarTokenizer:
     24    "moz-src:///browser/components/urlbar/UrlbarTokenizer.sys.mjs",
     25 });
     26 
     27 // By default, we add remote tabs that have been used more recently than this
     28 // time ago. Any remaining remote tabs are added in queue if no other results
     29 // are found.
     30 const RECENT_REMOTE_TAB_THRESHOLD_MS = 72 * 60 * 60 * 1000; // 72 hours.
     31 
     32 ChromeUtils.defineLazyGetter(lazy, "weaveXPCService", function () {
     33  try {
     34    return Cc["@mozilla.org/weave/service;1"].getService(Ci.nsISupports)
     35      .wrappedJSObject;
     36  } catch (ex) {
     37    // The app didn't build Sync.
     38  }
     39  return null;
     40 });
     41 
     42 XPCOMUtils.defineLazyPreferenceGetter(
     43  lazy,
     44  "showRemoteIconsPref",
     45  "services.sync.syncedTabs.showRemoteIcons",
     46  true
     47 );
     48 
     49 XPCOMUtils.defineLazyPreferenceGetter(
     50  lazy,
     51  "syncUsernamePref",
     52  "services.sync.username"
     53 );
     54 
     55 // from MDN...
     56 function escapeRegExp(string) {
     57  return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
     58 }
     59 
     60 /**
     61 * Singleton class to cache the latest remote tab data.
     62 */
     63 class _cache {
     64  /** @type {{tab: object, client: object}[]} */
     65  #tabsData = null;
     66 
     67  constructor() {
     68    Services.obs.addObserver(
     69      this.observe.bind(this),
     70      "weave:engine:sync:finish"
     71    );
     72    Services.obs.addObserver(
     73      this.observe.bind(this),
     74      "weave:service:start-over"
     75    );
     76  }
     77 
     78  /**
     79   * Build the in-memory structure we use.
     80   */
     81  async #buildItems() {
     82    // This is sorted by most recent client, most recent tab.
     83    let tabsData = [];
     84    // If Sync isn't initialized (either due to lag at startup or due to no user
     85    // being signed in), don't reach in to Weave.Service as that may initialize
     86    // Sync unnecessarily - we'll get an observer notification later when it
     87    // becomes ready and has synced a list of tabs.
     88    if (lazy.weaveXPCService.ready) {
     89      let clients = await lazy.SyncedTabs.getTabClients();
     90      lazy.SyncedTabs.sortTabClientsByLastUsed(clients);
     91      for (let client of clients) {
     92        for (let tab of client.tabs) {
     93          tabsData.push({ tab, client });
     94        }
     95      }
     96    }
     97    this.#tabsData = tabsData;
     98  }
     99 
    100  observe(subject, topic, data) {
    101    switch (topic) {
    102      case "weave:engine:sync:finish":
    103        if (data == "tabs") {
    104          // The tabs engine just finished syncing, so may have a different list
    105          // of tabs then we previously cached.
    106          this.#tabsData = null;
    107        }
    108        break;
    109      case "weave:service:start-over":
    110        // Sync is being reset due to the user disconnecting - we must invalidate
    111        // the cache so we don't supply tabs from a different user.
    112        this.#tabsData = null;
    113        break;
    114      default:
    115        break;
    116    }
    117  }
    118 
    119  /** @type {?_cache} */
    120  static #instance;
    121  /**
    122   * Build (if necessary) and return tabs data.
    123   *
    124   * @returns {Promise<{tab: object, client: object}[]>}
    125   */
    126  static async get() {
    127    _cache.#instance ??= new _cache();
    128 
    129    if (!_cache.#instance.#tabsData) {
    130      await _cache.#instance.#buildItems();
    131    }
    132    return _cache.#instance.#tabsData;
    133  }
    134 }
    135 
    136 /**
    137 * Class used to create the provider.
    138 */
    139 export class UrlbarProviderRemoteTabs extends UrlbarProvider {
    140  constructor() {
    141    super();
    142  }
    143 
    144  /**
    145   * @returns {Values<typeof UrlbarUtils.PROVIDER_TYPE>}
    146   */
    147  get type() {
    148    return UrlbarUtils.PROVIDER_TYPE.NETWORK;
    149  }
    150 
    151  /**
    152   * Whether this provider should be invoked for the given context.
    153   * If this method returns false, the providers manager won't start a query
    154   * with this provider, to save on resources.
    155   *
    156   * @param {UrlbarQueryContext} queryContext The query context object
    157   */
    158  async isActive(queryContext) {
    159    return (
    160      lazy.syncUsernamePref &&
    161      lazy.UrlbarPrefs.get("suggest.remotetab") &&
    162      queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.TABS) &&
    163      lazy.weaveXPCService &&
    164      lazy.weaveXPCService.ready &&
    165      lazy.weaveXPCService.enabled
    166    );
    167  }
    168 
    169  /**
    170   * Starts querying.
    171   *
    172   * @param {UrlbarQueryContext} queryContext
    173   * @param {(provider: UrlbarProvider, result: UrlbarResult) => void} addCallback
    174   *   Callback invoked by the provider to add a new result.
    175   */
    176  async startQuery(queryContext, addCallback) {
    177    let instance = this.queryInstance;
    178 
    179    let searchString = queryContext.tokens.map(t => t.value).join(" ");
    180 
    181    let re = new RegExp(escapeRegExp(searchString), "i");
    182    let tabsData = await _cache.get();
    183    if (instance != this.queryInstance) {
    184      return;
    185    }
    186 
    187    let resultsAdded = 0;
    188    let staleTabs = [];
    189    for (let { tab, client } of tabsData) {
    190      if (
    191        !searchString ||
    192        searchString == lazy.UrlbarTokenizer.RESTRICT.OPENPAGE ||
    193        re.test(tab.url) ||
    194        (tab.title && re.test(tab.title))
    195      ) {
    196        if (lazy.showRemoteIconsPref) {
    197          if (!tab.icon) {
    198            // It's rare that Sync supplies the icon for the page. If it does, it is a
    199            // string URL.
    200            tab.icon = UrlbarUtils.getIconForUrl(tab.url);
    201          } else {
    202            tab.icon = lazy.PlacesUtils.favicons.getFaviconLinkForIcon(
    203              Services.io.newURI(tab.icon)
    204            ).spec;
    205          }
    206        }
    207 
    208        let result = new lazy.UrlbarResult({
    209          type: UrlbarUtils.RESULT_TYPE.REMOTE_TAB,
    210          source: UrlbarUtils.RESULT_SOURCE.TABS,
    211          payload: {
    212            url: tab.url,
    213            title: tab.title,
    214            device: client.name,
    215            icon: lazy.showRemoteIconsPref ? tab.icon : "",
    216            lastUsed: (tab.lastUsed || 0) * 1000,
    217          },
    218          highlights: {
    219            url: UrlbarUtils.HIGHLIGHT.TYPED,
    220            title: UrlbarUtils.HIGHLIGHT.TYPED,
    221          },
    222        });
    223 
    224        // We want to return the most relevant remote tabs and thus the most
    225        // recent ones. While SyncedTabs.sys.mjs returns tabs that are sorted by
    226        // most recent client, then most recent tab, we can do better. For
    227        // example, the most recent client might have one recent tab and then
    228        // many very stale tabs. Those very stale tabs will push out more recent
    229        // tabs from staler clients. This provider first returns tabs from the
    230        // last 72 hours, sorted by client recency. Then, it adds remaining
    231        // tabs. We are not concerned about filling the remote tabs group with
    232        // stale tabs, because the muxer ensures remote tabs flex with other
    233        // results. It will only show the stale tabs if it has nothing else
    234        // to show.
    235        if (
    236          tab.lastUsed <=
    237          (Date.now() - RECENT_REMOTE_TAB_THRESHOLD_MS) / 1000
    238        ) {
    239          staleTabs.push(result);
    240        } else {
    241          addCallback(this, result);
    242          resultsAdded++;
    243        }
    244      }
    245 
    246      if (resultsAdded == queryContext.maxResults) {
    247        break;
    248      }
    249    }
    250 
    251    while (staleTabs.length && resultsAdded < queryContext.maxResults) {
    252      addCallback(this, staleTabs.shift());
    253      resultsAdded++;
    254    }
    255  }
    256 }