/* 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/. */ /** * This module exports a provider, returning open tabs matches for the urlbar. * It is also used to register and unregister open tabs. */ import { UrlbarProvider, UrlbarUtils, } from "resource:///modules/UrlbarUtils.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs", UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs", }); const PRIVATE_USER_CONTEXT_ID = -1; /** * Class used to create the provider. */ export class UrlbarProviderOpenTabs extends UrlbarProvider { constructor() { super(); } /** * Returns the name of this provider. * * @returns {string} the name of this provider. */ get name() { return "OpenTabs"; } /** * Returns the type of this provider. * * @returns {integer} one of the types from UrlbarUtils.PROVIDER_TYPE.* */ get type() { return UrlbarUtils.PROVIDER_TYPE.PROFILE; } /** * Whether this provider should be invoked for the given context. * If this method returns false, the providers manager won't start a query * with this provider, to save on resources. * * @param {UrlbarQueryContext} queryContext The query context object * @returns {boolean} Whether this provider should be invoked for the search. */ isActive(queryContext) { // For now we don't actually use this provider to query open tabs, instead // we join the temp table in UrlbarProviderPlaces. return false; } /** * Tracks whether the memory tables have been initialized yet. Until this * happens tabs are only stored in openTabs and later copied over to the * memory table. */ static memoryTableInitialized = false; /** * Maps the open tabs by userContextId. */ static _openTabs = new Map(); /** * Return urls that is opening on given user context id. * * @param {integer} userContextId Containers user context id * @param {boolean} isInPrivateWindow In private browsing window or not * @returns {Array} urls */ static getOpenTabs(userContextId, isInPrivateWindow) { userContextId = UrlbarProviderOpenTabs.getUserContextIdForOpenPagesTable( userContextId, isInPrivateWindow ); return UrlbarProviderOpenTabs._openTabs.get(userContextId); } /** * Return userContextId that will be used in moz_openpages_temp table. * * @param {integer} userContextId Containers user context id * @param {boolean} isInPrivateWindow In private browsing window or not * @returns {interger} userContextId */ static getUserContextIdForOpenPagesTable(userContextId, isInPrivateWindow) { return isInPrivateWindow ? PRIVATE_USER_CONTEXT_ID : userContextId; } /** * Copy over cached open tabs to the memory table once the Urlbar * connection has been initialized. */ static promiseDBPopulated = lazy.PlacesUtils.largeCacheDBConnDeferred.promise.then( async () => { // Must be set before populating. UrlbarProviderOpenTabs.memoryTableInitialized = true; // Populate the table with the current cached tabs. for (let [userContextId, urls] of UrlbarProviderOpenTabs._openTabs) { for (let url of urls) { await addToMemoryTable(url, userContextId).catch(Cu.reportError); } } } ); /** * Registers a tab as open. * * @param {string} url Address of the tab * @param {integer} userContextId Containers user context id * @param {boolean} isInPrivateWindow In private browsing window or not */ static async registerOpenTab(url, userContextId, isInPrivateWindow) { userContextId = UrlbarProviderOpenTabs.getUserContextIdForOpenPagesTable( userContextId, isInPrivateWindow ); if (!UrlbarProviderOpenTabs._openTabs.has(userContextId)) { UrlbarProviderOpenTabs._openTabs.set(userContextId, []); } UrlbarProviderOpenTabs._openTabs.get(userContextId).push(url); await addToMemoryTable(url, userContextId).catch(Cu.reportError); } /** * Unregisters a previously registered open tab. * * @param {string} url Address of the tab * @param {integer} userContextId Containers user context id * @param {boolean} isInPrivateWindow In private browsing window or not */ static async unregisterOpenTab(url, userContextId, isInPrivateWindow) { userContextId = UrlbarProviderOpenTabs.getUserContextIdForOpenPagesTable( userContextId, isInPrivateWindow ); let openTabs = UrlbarProviderOpenTabs._openTabs.get(userContextId); if (openTabs) { let index = openTabs.indexOf(url); if (index != -1) { openTabs.splice(index, 1); await removeFromMemoryTable(url, userContextId).catch(Cu.reportError); } } } /** * Starts querying. * * @param {object} queryContext The query context object * @param {Function} addCallback Callback invoked by the provider to add a new * match. * @returns {Promise} resolved when the query stops. */ async startQuery(queryContext, addCallback) { // Note: this is not actually expected to be used as an internal provider, // because normal history search will already coalesce with the open tabs // temp table to return proper frecency. // TODO: // * properly search and handle tokens, this is just a mock for now. let instance = this.queryInstance; let conn = await lazy.PlacesUtils.promiseLargeCacheDBConnection(); await UrlbarProviderOpenTabs.promiseDBPopulated; await conn.executeCached( ` SELECT url, userContextId FROM moz_openpages_temp `, {}, (row, cancel) => { if (instance != this.queryInstance) { cancel(); return; } addCallback( this, new lazy.UrlbarResult( UrlbarUtils.RESULT_TYPE.TAB_SWITCH, UrlbarUtils.RESULT_SOURCE.TABS, { url: row.getResultByName("url"), userContextId: row.getResultByName("userContextId"), } ) ); } ); } } /** * Adds an open page to the memory table. * * @param {string} url Address of the page * @param {number} userContextId Containers user context id * @returns {Promise} resolved after the addition. */ async function addToMemoryTable(url, userContextId) { if (!UrlbarProviderOpenTabs.memoryTableInitialized) { return; } await lazy.UrlbarProvidersManager.runInCriticalSection(async () => { let conn = await lazy.PlacesUtils.promiseLargeCacheDBConnection(); await conn.executeCached( ` INSERT OR REPLACE INTO moz_openpages_temp (url, userContextId, open_count) VALUES ( :url, :userContextId, IFNULL( ( SELECT open_count + 1 FROM moz_openpages_temp WHERE url = :url AND userContextId = :userContextId ), 1 ) ) `, { url, userContextId } ); }); } /** * Removes an open page from the memory table. * * @param {string} url Address of the page * @param {number} userContextId Containers user context id * @returns {Promise} resolved after the removal. */ async function removeFromMemoryTable(url, userContextId) { if (!UrlbarProviderOpenTabs.memoryTableInitialized) { return; } await lazy.UrlbarProvidersManager.runInCriticalSection(async () => { let conn = await lazy.PlacesUtils.promiseLargeCacheDBConnection(); await conn.executeCached( ` UPDATE moz_openpages_temp SET open_count = open_count - 1 WHERE url = :url AND userContextId = :userContextId `, { url, userContextId } ); }); }