tor-browser

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

UrlbarProviderOpenTabs.sys.mjs (13477B)


      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, returning open tabs matches for the urlbar.
      7 * It is also used to register and unregister open tabs.
      8 */
      9 
     10 import {
     11  UrlbarProvider,
     12  UrlbarUtils,
     13 } from "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs";
     14 
     15 const lazy = {};
     16 
     17 ChromeUtils.defineESModuleGetters(lazy, {
     18  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
     19  ProvidersManager:
     20    "moz-src:///browser/components/urlbar/UrlbarProvidersManager.sys.mjs",
     21  UrlbarResult: "moz-src:///browser/components/urlbar/UrlbarResult.sys.mjs",
     22 });
     23 
     24 ChromeUtils.defineLazyGetter(lazy, "logger", () =>
     25  UrlbarUtils.getLogger({ prefix: "Provider.OpenTabs" })
     26 );
     27 
     28 const PRIVATE_USER_CONTEXT_ID = -1;
     29 
     30 /**
     31 * Maps the open tabs by userContextId, then by groupId.
     32 * It is a nested map structure as follows:
     33 *   Map(userContextId => Map(groupId | null => Map(url => count)))
     34 */
     35 var gOpenTabUrls = new Map();
     36 
     37 /**
     38 * Class used to create the provider.
     39 */
     40 export class UrlbarProviderOpenTabs extends UrlbarProvider {
     41  constructor() {
     42    super();
     43  }
     44 
     45  /**
     46   * @returns {Values<typeof UrlbarUtils.PROVIDER_TYPE>}
     47   */
     48  get type() {
     49    return UrlbarUtils.PROVIDER_TYPE.PROFILE;
     50  }
     51 
     52  /**
     53   * Whether this provider should be invoked for the given context.
     54   * If this method returns false, the providers manager won't start a query
     55   * with this provider, to save on resources.
     56   */
     57  async isActive() {
     58    // For now we don't actually use this provider to query open tabs, instead
     59    // we join the temp table in UrlbarProviderPlaces.
     60    return false;
     61  }
     62 
     63  /**
     64   * Tracks whether the memory tables have been initialized yet. Until this
     65   * happens tabs are only stored in openTabs and later copied over to the
     66   * memory table.
     67   */
     68  static memoryTableInitialized = false;
     69 
     70  /**
     71   * Return unique urls that are open for given user context id.
     72   *
     73   * @param {number|string} userContextId Containers user context id
     74   * @param {boolean} [isInPrivateWindow] In private browsing window or not
     75   * @returns {Array} [url, userContextId, groupId | null]
     76   */
     77  static getOpenTabUrlsForUserContextId(
     78    userContextId,
     79    isInPrivateWindow = false
     80  ) {
     81    // It's fairly common to retrieve the value from an HTML attribute, that
     82    // means we're getting sometimes a string, sometimes an integer. As we're
     83    // using this as key of a Map, we must treat it consistently.
     84    userContextId = parseInt(`${userContextId}`);
     85    userContextId = UrlbarProviderOpenTabs.getUserContextIdForOpenPagesTable(
     86      userContextId,
     87      isInPrivateWindow
     88    );
     89 
     90    let groupEntries = gOpenTabUrls.get(userContextId);
     91    if (!groupEntries) {
     92      return [];
     93    }
     94 
     95    let result = new Set();
     96    groupEntries.forEach((urls, groupId) => {
     97      for (let url of urls.keys()) {
     98        result.add([url, userContextId, groupId]);
     99      }
    100    });
    101    return Array.from(result);
    102  }
    103 
    104  /**
    105   * Return unique urls that are open, along with their user context id and group id.
    106   *
    107   * @param {boolean} [isInPrivateWindow] Whether it's for a private browsing window
    108   * @returns {Map} { url => Set([userContextId, groupId]) }
    109   */
    110  static getOpenTabUrls(isInPrivateWindow = false) {
    111    let uniqueUrls = new Map();
    112    if (isInPrivateWindow) {
    113      let urlInfo = UrlbarProviderOpenTabs.getOpenTabUrlsForUserContextId(
    114        PRIVATE_USER_CONTEXT_ID,
    115        true
    116      );
    117      for (let [url, contextId, groupId] of urlInfo) {
    118        uniqueUrls.set(url, new Set([[contextId, groupId]]));
    119      }
    120    } else {
    121      gOpenTabUrls.forEach((groups, userContextId) => {
    122        if (userContextId == PRIVATE_USER_CONTEXT_ID) {
    123          return;
    124        }
    125 
    126        groups.forEach((urls, groupId) => {
    127          for (let url of urls.keys()) {
    128            let userContextAndGroupIds = uniqueUrls.get(url);
    129            if (!userContextAndGroupIds) {
    130              userContextAndGroupIds = new Set();
    131              uniqueUrls.set(url, userContextAndGroupIds);
    132            }
    133            userContextAndGroupIds.add([userContextId, groupId]);
    134          }
    135        });
    136      });
    137    }
    138    return uniqueUrls;
    139  }
    140 
    141  /**
    142   * Return urls registered in the memory table.
    143   * This is mostly for testing purposes.
    144   *
    145   * @returns {Promise<{url: string, userContextId: number, groupId: string | null, count: number}[]>}
    146   */
    147  static async getDatabaseRegisteredOpenTabsForTests() {
    148    let conn = await lazy.PlacesUtils.promiseLargeCacheDBConnection();
    149    let rows = await conn.execute(
    150      "SELECT url, userContextId, NULLIF(groupId, '') groupId, open_count" +
    151        " FROM moz_openpages_temp ORDER BY url, userContextId, groupId"
    152    );
    153    return rows.map(r => ({
    154      url: r.getResultByName("url"),
    155      userContextId: r.getResultByName("userContextId"),
    156      tabGroup: r.getResultByName("groupId"),
    157      count: r.getResultByName("open_count"),
    158    }));
    159  }
    160 
    161  /**
    162   * Return userContextId that is used in the moz_openpages_temp table and
    163   * returned as part of the payload. It differs only for private windows.
    164   *
    165   * @param {number} userContextId Containers user context id
    166   * @param {boolean} isInPrivateWindow In private browsing window or not
    167   * @returns {number} userContextId
    168   */
    169  static getUserContextIdForOpenPagesTable(userContextId, isInPrivateWindow) {
    170    return isInPrivateWindow ? PRIVATE_USER_CONTEXT_ID : userContextId;
    171  }
    172 
    173  /**
    174   * Return whether the provided userContextId is for a non-private tab.
    175   *
    176   * @param {number} userContextId the userContextId to evaluate
    177   * @returns {boolean}
    178   */
    179  static isNonPrivateUserContextId(userContextId) {
    180    return userContextId != PRIVATE_USER_CONTEXT_ID;
    181  }
    182 
    183  /**
    184   * Return whether the provided userContextId is for a container.
    185   *
    186   * @param {number} userContextId the userContextId to evaluate
    187   * @returns {boolean}
    188   */
    189  static isContainerUserContextId(userContextId) {
    190    return userContextId > 0;
    191  }
    192 
    193  /**
    194   * Copy over cached open tabs to the memory table once the Urlbar
    195   * connection has been initialized.
    196   */
    197  static promiseDBPopulated =
    198    lazy.PlacesUtils.largeCacheDBConnDeferred.promise.then(async () => {
    199      // Must be set before populating.
    200      UrlbarProviderOpenTabs.memoryTableInitialized = true;
    201      // Populate the table with the current cached tabs.
    202      for (let [userContextId, groupEntries] of gOpenTabUrls) {
    203        for (let [groupId, entries] of groupEntries) {
    204          for (let [url, count] of entries) {
    205            await addToMemoryTable(url, userContextId, groupId, count).catch(
    206              console.error
    207            );
    208          }
    209        }
    210      }
    211    });
    212 
    213  /**
    214   * Registers a tab as open.
    215   *
    216   * @param {string} url Address of the tab
    217   * @param {number|string} userContextId Containers user context id
    218   * @param {?string} groupId The id of the group the tab belongs to
    219   * @param {boolean} isInPrivateWindow In private browsing window or not
    220   */
    221  static async registerOpenTab(url, userContextId, groupId, isInPrivateWindow) {
    222    // It's fairly common to retrieve the value from an HTML attribute, that
    223    // means we're getting sometimes a string, sometimes an integer. As we're
    224    // using this as key of a Map, we must treat it consistently.
    225    userContextId = parseInt(`${userContextId}`);
    226    groupId = groupId ?? null;
    227    if (!Number.isInteger(userContextId)) {
    228      lazy.logger.error("Invalid userContextId while registering openTab: ", {
    229        url,
    230        userContextId,
    231        isInPrivateWindow,
    232      });
    233      return;
    234    }
    235    lazy.logger.info("Registering openTab: ", {
    236      url,
    237      userContextId,
    238      groupId,
    239      isInPrivateWindow,
    240    });
    241    userContextId = UrlbarProviderOpenTabs.getUserContextIdForOpenPagesTable(
    242      userContextId,
    243      isInPrivateWindow
    244    );
    245 
    246    let contextEntries = gOpenTabUrls.get(userContextId);
    247    if (!contextEntries) {
    248      contextEntries = new Map();
    249      gOpenTabUrls.set(userContextId, contextEntries);
    250    }
    251 
    252    let groupEntries = contextEntries.get(groupId);
    253    if (!groupEntries) {
    254      groupEntries = new Map();
    255      contextEntries.set(groupId, groupEntries);
    256    }
    257 
    258    groupEntries.set(url, (groupEntries.get(url) ?? 0) + 1);
    259    await addToMemoryTable(url, userContextId, groupId).catch(console.error);
    260  }
    261 
    262  /**
    263   * Unregisters a previously registered open tab.
    264   *
    265   * @param {string} url Address of the tab
    266   * @param {number|string} userContextId Containers user context id
    267   * @param {?string} groupId The id of the group the tab belongs to
    268   * @param {boolean} isInPrivateWindow In private browsing window or not
    269   */
    270  static async unregisterOpenTab(
    271    url,
    272    userContextId,
    273    groupId,
    274    isInPrivateWindow
    275  ) {
    276    // It's fairly common to retrieve the value from an HTML attribute, that
    277    // means we're getting sometimes a string, sometimes an integer. As we're
    278    // using this as key of a Map, we must treat it consistently.
    279    userContextId = parseInt(`${userContextId}`);
    280    groupId = groupId ?? null;
    281    lazy.logger.info("Unregistering openTab: ", {
    282      url,
    283      userContextId,
    284      groupId,
    285      isInPrivateWindow,
    286    });
    287    userContextId = UrlbarProviderOpenTabs.getUserContextIdForOpenPagesTable(
    288      userContextId,
    289      isInPrivateWindow
    290    );
    291 
    292    let contextEntries = gOpenTabUrls.get(userContextId);
    293    if (contextEntries) {
    294      let groupEntries = contextEntries.get(groupId);
    295      if (groupEntries) {
    296        let oldCount = groupEntries.get(url);
    297        if (oldCount == 0) {
    298          console.error("Tried to unregister a non registered open tab");
    299          return;
    300        }
    301        if (oldCount == 1) {
    302          groupEntries.delete(url);
    303          // Note: `groupEntries` might be an empty Map now, though we don't remove it
    304          // from `gOpenTabUrls` as it's likely to be reused later.
    305        } else {
    306          groupEntries.set(url, oldCount - 1);
    307        }
    308        await removeFromMemoryTable(url, userContextId, groupId).catch(
    309          console.error
    310        );
    311      }
    312    }
    313  }
    314 
    315  /**
    316   * Starts querying.
    317   *
    318   * @param {UrlbarQueryContext} queryContext
    319   * @param {(provider: UrlbarProvider, result: UrlbarResult) => void} addCallback
    320   *   Callback invoked by the provider to add a new result.
    321   */
    322  async startQuery(queryContext, addCallback) {
    323    // Note: this is not actually expected to be used as an internal provider,
    324    // because normal history search will already coalesce with the open tabs
    325    // temp table to return proper frecency.
    326    // TODO:
    327    //  * properly search and handle tokens, this is just a mock for now.
    328    let instance = this.queryInstance;
    329    let conn = await lazy.PlacesUtils.promiseLargeCacheDBConnection();
    330    await UrlbarProviderOpenTabs.promiseDBPopulated;
    331    await conn.executeCached(
    332      `
    333      SELECT url, userContextId, NULLIF(groupId, '') groupId
    334      FROM moz_openpages_temp
    335    `,
    336      {},
    337      (row, cancel) => {
    338        if (instance != this.queryInstance) {
    339          cancel();
    340          return;
    341        }
    342        addCallback(
    343          this,
    344          new lazy.UrlbarResult({
    345            type: UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
    346            source: UrlbarUtils.RESULT_SOURCE.TABS,
    347            payload: {
    348              url: row.getResultByName("url"),
    349              userContextId: row.getResultByName("userContextId"),
    350              tabGroup: row.getResultByName("groupId"),
    351            },
    352          })
    353        );
    354      }
    355    );
    356  }
    357 }
    358 
    359 /**
    360 * Adds an open page to the memory table.
    361 *
    362 * @param {string} url Address of the page
    363 * @param {number} userContextId Containers user context id
    364 * @param {?string} groupId The id of the group the tab belongs to
    365 * @param {number} [count] The number of times the page is open
    366 * @returns {Promise} resolved after the addition.
    367 */
    368 async function addToMemoryTable(url, userContextId, groupId, count = 1) {
    369  if (!UrlbarProviderOpenTabs.memoryTableInitialized) {
    370    return;
    371  }
    372  await lazy.ProvidersManager.runInCriticalSection(async () => {
    373    let conn = await lazy.PlacesUtils.promiseLargeCacheDBConnection();
    374    await conn.executeCached(
    375      `
    376      INSERT INTO moz_openpages_temp (url, userContextId, groupId, open_count)
    377      VALUES ( :url,
    378               :userContextId,
    379               IFNULL(:groupId, ''),
    380               :count
    381             )
    382      ON CONFLICT DO UPDATE SET open_count = open_count + 1
    383    `,
    384      { url, userContextId, groupId, count }
    385    );
    386  });
    387 }
    388 
    389 /**
    390 * Removes an open page from the memory table.
    391 *
    392 * @param {string} url Address of the page
    393 * @param {number} userContextId Containers user context id
    394 * @param {?string} groupId The id of the group the tab belongs to
    395 * @returns {Promise} resolved after the removal.
    396 */
    397 async function removeFromMemoryTable(url, userContextId, groupId) {
    398  if (!UrlbarProviderOpenTabs.memoryTableInitialized) {
    399    return;
    400  }
    401  await lazy.ProvidersManager.runInCriticalSection(async () => {
    402    let conn = await lazy.PlacesUtils.promiseLargeCacheDBConnection();
    403    await conn.executeCached(
    404      `
    405      UPDATE moz_openpages_temp
    406      SET open_count = open_count - 1
    407      WHERE url = :url
    408        AND userContextId = :userContextId
    409        AND groupId = IFNULL(:groupId, '')
    410    `,
    411      { url, userContextId, groupId }
    412    );
    413  });
    414 }