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 }