UrlbarProvidersManager.sys.mjs (35980B)
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 component used to register search providers and manage 7 * the connection between such providers and a UrlbarController. 8 */ 9 10 /** 11 * @import { UrlbarProvider } from "UrlbarUtils.sys.mjs" 12 * @import { UrlbarMuxer } from "UrlbarUtils.sys.mjs" 13 * @import { UrlbarSearchStringTokenData } from "UrlbarTokenizer.sys.mjs" 14 */ 15 16 const lazy = {}; 17 18 ChromeUtils.defineESModuleGetters(lazy, { 19 ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs", 20 PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", 21 Region: "resource://gre/modules/Region.sys.mjs", 22 SkippableTimer: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs", 23 UrlbarMuxer: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs", 24 UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs", 25 UrlbarProvider: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs", 26 UrlbarSearchUtils: 27 "moz-src:///browser/components/urlbar/UrlbarSearchUtils.sys.mjs", 28 UrlbarTokenizer: 29 "moz-src:///browser/components/urlbar/UrlbarTokenizer.sys.mjs", 30 UrlbarUtils: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs", 31 }); 32 33 ChromeUtils.defineLazyGetter(lazy, "logger", () => 34 lazy.UrlbarUtils.getLogger({ prefix: "ProvidersManager" }) 35 ); 36 37 // List of available local providers, each is implemented in its own module and 38 // will track different queries internally by queryContext. 39 // When adding new providers please remember to update the list in metrics.yaml. 40 var localProviderModules = [ 41 { 42 name: "UrlbarProviderAboutPages", 43 module: 44 "moz-src:///browser/components/urlbar/UrlbarProviderAboutPages.sys.mjs", 45 supportedInputTypes: ["urlbar"], 46 }, 47 { 48 name: "UrlbarProviderActionsSearchMode", 49 module: 50 "moz-src:///browser/components/urlbar/UrlbarProviderActionsSearchMode.sys.mjs", 51 supportedInputTypes: ["urlbar"], 52 }, 53 { 54 name: "UrlbarProviderGlobalActions", 55 module: 56 "moz-src:///browser/components/urlbar/UrlbarProviderGlobalActions.sys.mjs", 57 supportedInputTypes: ["urlbar", "searchbar"], 58 }, 59 { 60 name: "UrlbarProviderAliasEngines", 61 module: 62 "moz-src:///browser/components/urlbar/UrlbarProviderAliasEngines.sys.mjs", 63 supportedInputTypes: ["urlbar", "searchbar"], 64 }, 65 { 66 name: "UrlbarProviderAutofill", 67 module: 68 "moz-src:///browser/components/urlbar/UrlbarProviderAutofill.sys.mjs", 69 supportedInputTypes: ["urlbar"], 70 }, 71 { 72 name: "UrlbarProviderBookmarkKeywords", 73 module: 74 "moz-src:///browser/components/urlbar/UrlbarProviderBookmarkKeywords.sys.mjs", 75 supportedInputTypes: ["urlbar"], 76 }, 77 { 78 name: "UrlbarProviderCalculator", 79 module: 80 "moz-src:///browser/components/urlbar/UrlbarProviderCalculator.sys.mjs", 81 supportedInputTypes: ["urlbar", "searchbar"], 82 }, 83 { 84 name: "UrlbarProviderClipboard", 85 module: 86 "moz-src:///browser/components/urlbar/UrlbarProviderClipboard.sys.mjs", 87 supportedInputTypes: ["urlbar"], 88 }, 89 { 90 name: "UrlbarProviderHeuristicFallback", 91 module: 92 "moz-src:///browser/components/urlbar/UrlbarProviderHeuristicFallback.sys.mjs", 93 supportedInputTypes: ["urlbar", "searchbar"], 94 }, 95 { 96 name: "UrlbarProviderHistoryUrlHeuristic", 97 module: 98 "moz-src:///browser/components/urlbar/UrlbarProviderHistoryUrlHeuristic.sys.mjs", 99 supportedInputTypes: ["urlbar"], 100 }, 101 { 102 name: "UrlbarProviderInputHistory", 103 module: 104 "moz-src:///browser/components/urlbar/UrlbarProviderInputHistory.sys.mjs", 105 supportedInputTypes: ["urlbar"], 106 }, 107 // disable UrlbarProviderInterventions as part of tor-browser#41327 108 { 109 name: "UrlbarProviderOmnibox", 110 module: 111 "moz-src:///browser/components/urlbar/UrlbarProviderOmnibox.sys.mjs", 112 supportedInputTypes: ["urlbar"], 113 }, 114 { 115 name: "UrlbarProviderPlaces", 116 module: "moz-src:///browser/components/urlbar/UrlbarProviderPlaces.sys.mjs", 117 supportedInputTypes: ["urlbar"], 118 }, 119 { 120 name: "UrlbarProviderPrivateSearch", 121 module: 122 "moz-src:///browser/components/urlbar/UrlbarProviderPrivateSearch.sys.mjs", 123 supportedInputTypes: ["urlbar", "searchbar"], 124 }, 125 { 126 name: "UrlbarProviderQuickSuggest", 127 module: 128 "moz-src:///browser/components/urlbar/UrlbarProviderQuickSuggest.sys.mjs", 129 supportedInputTypes: ["urlbar"], 130 }, 131 { 132 name: "UrlbarProviderQuickSuggestContextualOptIn", 133 module: 134 "moz-src:///browser/components/urlbar/UrlbarProviderQuickSuggestContextualOptIn.sys.mjs", 135 supportedInputTypes: ["urlbar"], 136 }, 137 { 138 name: "UrlbarProviderRecentSearches", 139 module: 140 "moz-src:///browser/components/urlbar/UrlbarProviderRecentSearches.sys.mjs", 141 supportedInputTypes: ["urlbar", "searchbar"], 142 }, 143 { 144 name: "UrlbarProviderRemoteTabs", 145 module: 146 "moz-src:///browser/components/urlbar/UrlbarProviderRemoteTabs.sys.mjs", 147 supportedInputTypes: ["urlbar"], 148 }, 149 { 150 name: "UrlbarProviderRestrictKeywords", 151 module: 152 "moz-src:///browser/components/urlbar/UrlbarProviderRestrictKeywords.sys.mjs", 153 supportedInputTypes: ["urlbar"], 154 }, 155 { 156 name: "UrlbarProviderRestrictKeywordsAutofill", 157 module: 158 "moz-src:///browser/components/urlbar/UrlbarProviderRestrictKeywordsAutofill.sys.mjs", 159 supportedInputTypes: ["urlbar"], 160 }, 161 { 162 name: "UrlbarProviderSearchTips", 163 module: 164 "moz-src:///browser/components/urlbar/UrlbarProviderSearchTips.sys.mjs", 165 supportedInputTypes: ["urlbar"], 166 }, 167 { 168 name: "UrlbarProviderSearchSuggestions", 169 module: 170 "moz-src:///browser/components/urlbar/UrlbarProviderSearchSuggestions.sys.mjs", 171 supportedInputTypes: ["urlbar", "searchbar"], 172 }, 173 // UrlbarProviderSemanticHistorySearch.sys.mjs is missing. tor-browser#44045. 174 { 175 name: "UrlbarProviderTabToSearch", 176 module: 177 "moz-src:///browser/components/urlbar/UrlbarProviderTabToSearch.sys.mjs", 178 supportedInputTypes: ["urlbar", "searchbar"], 179 }, 180 { 181 name: "UrlbarProviderTokenAliasEngines", 182 module: 183 "moz-src:///browser/components/urlbar/UrlbarProviderTokenAliasEngines.sys.mjs", 184 supportedInputTypes: ["urlbar", "searchbar"], 185 }, 186 { 187 name: "UrlbarProviderTopSites", 188 module: 189 "moz-src:///browser/components/urlbar/UrlbarProviderTopSites.sys.mjs", 190 supportedInputTypes: ["urlbar"], 191 }, 192 { 193 name: "UrlbarProviderUnitConversion", 194 module: 195 "moz-src:///browser/components/urlbar/UrlbarProviderUnitConversion.sys.mjs", 196 supportedInputTypes: ["urlbar", "searchbar"], 197 }, 198 ]; 199 200 // List of available local muxers, each is implemented in its own jsm module. 201 var localMuxerModules = { 202 UrlbarMuxerStandard: 203 "moz-src:///browser/components/urlbar/UrlbarMuxerStandard.sys.mjs", 204 }; 205 206 const DEFAULT_MUXER = "UnifiedComplete"; 207 const DEFAULT_CHUNK_RESULTS_DELAY_MS = 16; 208 209 /** 210 * Class used to create a manager. There always exists one manager instance 211 * per input type. It is responsible to keep a list of provider instances, 212 * instantiate query objects and pass those to the providers. 213 */ 214 export class ProvidersManager { 215 /** 216 * Interrupt() allows to stop any running SQL query, some provider may be 217 * running a query that shouldn't be interrupted, and if so it should 218 * bump this through disableInterrupt and enableInterrupt. 219 */ 220 static interruptLevel = 0; 221 222 /** 223 * @param {object} providerModules 224 * Object with symbol names as keys and module paths as values. 225 * Symbols should be UrlbarProvider classes that will be instanciated. 226 * @param {object} muxerModules 227 * Object with symbol names as keys and module paths as values. 228 * Symbols should be UrlbarMuxer instances. 229 */ 230 constructor(providerModules, muxerModules = localMuxerModules) { 231 /** 232 * Tracks the available providers. This is a sorted array, with HEURISTIC 233 * providers at the front. 234 * 235 * @type {UrlbarProvider[]} 236 */ 237 this.providers = []; 238 /** 239 * @type {{onEngagement: Set<UrlbarProvider>, onImpression: Set<UrlbarProvider>, onAbandonment: Set<UrlbarProvider>, onSearchSessionEnd: Set<UrlbarProvider>}} 240 */ 241 this.providersByNotificationType = { 242 onEngagement: new Set(), 243 onImpression: new Set(), 244 onAbandonment: new Set(), 245 onSearchSessionEnd: new Set(), 246 }; 247 for (let providerInfo of providerModules) { 248 let { [providerInfo.name]: providerClass } = ChromeUtils.importESModule( 249 providerInfo.module 250 ); 251 this.registerProvider(new providerClass()); 252 } 253 254 /** 255 * Tracks ongoing Query instances by queryContext. 256 * 257 * @type {Map<object, Query>} 258 */ 259 this.queries = new Map(); 260 261 /** 262 * This maps muxer names to muxers. 263 * 264 * @type {Map<string, UrlbarMuxer>} 265 */ 266 this.muxers = new Map(); 267 268 for (let [symbol, module] of Object.entries(muxerModules)) { 269 let { [symbol]: muxer } = ChromeUtils.importESModule(module); 270 this.registerMuxer(muxer); 271 } 272 /** 273 * These can be set by tests to increase or reduce the chunk delays. 274 * See _notifyResultsFromProvider for additional details. 275 * To improve dataflow and reduce UI work, when a result is added we may notify 276 * it to the controller after a delay, so that we can chunk results in that 277 * timeframe into a single call. See _notifyResultsFromProvider for details. 278 */ 279 this.CHUNK_RESULTS_DELAY_MS = DEFAULT_CHUNK_RESULTS_DELAY_MS; 280 } 281 282 /** 283 * Registers a provider object with the manager. 284 * 285 * @param {object} provider 286 * The provider object to register. 287 */ 288 registerProvider(provider) { 289 if (!provider || !(provider instanceof lazy.UrlbarProvider)) { 290 throw new Error(`Trying to register an invalid provider`); 291 } 292 if ( 293 !Object.values(lazy.UrlbarUtils.PROVIDER_TYPE).includes(provider.type) 294 ) { 295 throw new Error(`Unknown provider type ${provider.type}`); 296 } 297 lazy.logger.info(`Registering provider ${provider.name}`); 298 let index = -1; 299 if (provider.type == lazy.UrlbarUtils.PROVIDER_TYPE.HEURISTIC) { 300 // Keep heuristic providers in order at the front of the array. Find the 301 // first non-heuristic provider and insert the new provider there. 302 index = this.providers.findIndex( 303 p => p.type != lazy.UrlbarUtils.PROVIDER_TYPE.HEURISTIC 304 ); 305 } 306 if (index < 0) { 307 index = this.providers.length; 308 } 309 this.providers.splice(index, 0, provider); 310 311 for (const notificationType of Object.keys( 312 this.providersByNotificationType 313 )) { 314 if (typeof provider[notificationType] === "function") { 315 this.providersByNotificationType[notificationType].add(provider); 316 } 317 } 318 } 319 320 /** 321 * Unregisters a previously registered provider object. 322 * 323 * @param {object} provider 324 * The provider object to unregister. 325 */ 326 unregisterProvider(provider) { 327 lazy.logger.info(`Unregistering provider ${provider.name}`); 328 let index = this.providers.findIndex(p => p.name == provider.name); 329 if (index != -1) { 330 this.providers.splice(index, 1); 331 } 332 333 Object.values(this.providersByNotificationType).forEach(providers => 334 providers.delete(provider) 335 ); 336 } 337 338 /** 339 * Returns the provider with the given name. 340 * 341 * @param {string} name 342 * The provider name. 343 * @returns {UrlbarProvider | undefined} 344 * The provider. 345 */ 346 getProvider(name) { 347 return this.providers.find(p => p.name == name); 348 } 349 350 /** 351 * Registers a muxer object with the manager. 352 * 353 * @param {UrlbarMuxer} muxer 354 * a UrlbarMuxer object 355 */ 356 registerMuxer(muxer) { 357 if (!muxer || !(muxer instanceof lazy.UrlbarMuxer)) { 358 throw new Error(`Trying to register an invalid muxer`); 359 } 360 lazy.logger.info(`Registering muxer ${muxer.name}`); 361 this.muxers.set(muxer.name, muxer); 362 } 363 364 /** 365 * Unregisters a previously registered muxer object. 366 * 367 * @param {UrlbarMuxer|string} muxer 368 * a UrlbarMuxer object or name. 369 */ 370 unregisterMuxer(muxer) { 371 let muxerName = typeof muxer == "string" ? muxer : muxer.name; 372 lazy.logger.info(`Unregistering muxer ${muxerName}`); 373 this.muxers.delete(muxerName); 374 } 375 376 /** 377 * Starts querying. 378 * 379 * @param {UrlbarQueryContext} queryContext 380 * The query context object 381 * @param {?UrlbarController} [controller] 382 * a UrlbarController instance 383 */ 384 async startQuery(queryContext, controller = null) { 385 lazy.logger.info(`Query start "${queryContext.searchString}"`); 386 387 // Define the muxer to use. 388 let muxerName = queryContext.muxer || DEFAULT_MUXER; 389 lazy.logger.debug(`Using muxer ${muxerName}`); 390 let muxer = this.muxers.get(muxerName); 391 if (!muxer) { 392 throw new Error(`Muxer with name ${muxerName} not found`); 393 } 394 395 // If the queryContext specifies a list of providers to use, filter on it, 396 // otherwise just pass the full list of providers. 397 let providers = queryContext.providers 398 ? this.providers.filter(p => queryContext.providers.includes(p.name)) 399 : this.providers; 400 401 queryContext.canceled = false; 402 try { 403 // The tokenizer needs to synchronously check whether the first token is a 404 // keyword, thus here we must ensure the keywords cache is up. 405 await lazy.PlacesUtils.keywords.ensureCacheInitialized(); 406 } catch (ex) { 407 lazy.logger.error( 408 "Unable to ensure keyword cache is initialization. A keyword may not be \ 409 detected at the beginning of the search string.", 410 ex 411 ); 412 } 413 414 // The query may have been canceled while awaiting for asynchronous work. 415 if (queryContext.canceled) { 416 return; 417 } 418 419 // Apply tokenization. 420 let tokens = lazy.UrlbarTokenizer.tokenize(queryContext); 421 queryContext.tokens = tokens; 422 423 // If there's a single source, we are in restriction mode. 424 if (queryContext.sources && queryContext.sources.length == 1) { 425 queryContext.restrictSource = queryContext.sources[0]; 426 } 427 // Providers can use queryContext.sources to decide whether they want to be 428 // invoked or not. 429 // The sources may be defined in the context, then the whole search string 430 // can be used for searching. Otherwise sources are extracted from prefs and 431 // restriction tokens, then restriction tokens must be filtered out of the 432 // search string. 433 let restrictToken = updateSourcesIfEmpty(queryContext); 434 if (restrictToken) { 435 queryContext.restrictToken = restrictToken; 436 // If the restriction token has an equivalent source, then set it as 437 // restrictSource. 438 if (lazy.UrlbarTokenizer.SEARCH_MODE_RESTRICT.has(restrictToken.value)) { 439 queryContext.restrictSource = queryContext.sources[0]; 440 } 441 } 442 lazy.logger.debug(`Context sources ${queryContext.sources}`); 443 444 let query = new Query(queryContext, controller, muxer, providers); 445 this.queries.set(queryContext, query); 446 447 // The muxer and many providers depend on the search service and our search 448 // utils. Make sure they're initialized now (via UrlbarSearchUtils) so that 449 // all query-related urlbar modules don't need to do it. 450 try { 451 await lazy.UrlbarSearchUtils.init(); 452 } catch { 453 // We continue anyway, because we want the user to be able to search their 454 // history and bookmarks even if search engines are not available. 455 } 456 457 // Some providers depend on Region/Locale info and must access Region.home 458 // synchronously, so we ensure Region is initialized. 459 try { 460 await lazy.Region.init(); 461 } catch (ex) { 462 // We continue anyway, region will be null and providers should handle 463 // that gracefully. 464 } 465 466 if (query.canceled) { 467 return; 468 } 469 470 await query.start(); 471 } 472 473 /** 474 * Cancels a running query. 475 * 476 * @param {UrlbarQueryContext} queryContext The query context object 477 */ 478 cancelQuery(queryContext) { 479 lazy.logger.info(`Query cancel "${queryContext.searchString}"`); 480 queryContext.canceled = true; 481 482 let query = this.queries.get(queryContext); 483 if (!query) { 484 // The query object may have not been created yet, if the query was 485 // canceled immediately. 486 return; 487 } 488 query.cancel(); 489 if (!ProvidersManager.interruptLevel) { 490 try { 491 let db = lazy.PlacesUtils.promiseLargeCacheDBConnection(); 492 db.interrupt(); 493 } catch (ex) {} 494 } 495 this.queries.delete(queryContext); 496 } 497 498 /** 499 * A provider can use this util when it needs to run a SQL query that can't 500 * be interrupted. Otherwise, when a query is canceled any running SQL query 501 * is interrupted abruptly. 502 * 503 * @param {Function} taskFn a Task to execute in the critical section. 504 */ 505 static async runInCriticalSection(taskFn) { 506 this.interruptLevel++; 507 try { 508 await taskFn(); 509 } finally { 510 this.interruptLevel--; 511 } 512 } 513 514 /** 515 * Notifies all providers about changes in user engagement with the urlbar. 516 * This function centralizes the dispatch of engagement-related events to the 517 * appropriate providers based on the current state of interaction. 518 * 519 * @param {"engagement"|"abandonment"} state 520 * The state of the engagement, one of: engagement, abandonment 521 * @param {UrlbarQueryContext} queryContext 522 * The engagement's query context, if available. 523 * @param {object} details 524 * An object that describes the search string and the picked result, if any. 525 * @param {UrlbarController} controller 526 * The controller associated with the engagement 527 */ 528 notifyEngagementChange(state, queryContext, details = {}, controller) { 529 if (!["engagement", "abandonment"].includes(state)) { 530 lazy.logger.error(`Unsupported state for engagement change: ${state}`); 531 return; 532 } 533 534 const visibleResults = controller.view?.visibleResults ?? []; 535 const visibleResultsByProviderName = new Map(); 536 537 visibleResults.forEach((result, index) => { 538 const providerName = result.providerName; 539 let results = visibleResultsByProviderName.get(providerName); 540 if (!results) { 541 results = []; 542 visibleResultsByProviderName.set(providerName, results); 543 } 544 results.push({ index, result }); 545 }); 546 547 if (!details.isSessionOngoing) { 548 this.#notifyImpression( 549 this.providersByNotificationType.onImpression, 550 state, 551 queryContext, 552 controller, 553 visibleResultsByProviderName, 554 state == "engagement" && details.result ? details : null 555 ); 556 } 557 558 if (state === "engagement") { 559 if (details.result) { 560 this.#notifyEngagement( 561 this.providersByNotificationType.onEngagement, 562 queryContext, 563 controller, 564 details 565 ); 566 } 567 } else { 568 this.#notifyAbandonment( 569 this.providersByNotificationType.onAbandonment, 570 queryContext, 571 controller, 572 visibleResultsByProviderName 573 ); 574 } 575 576 if (!details.isSessionOngoing) { 577 this.#notifySearchSessionEnd( 578 this.providersByNotificationType.onSearchSessionEnd, 579 queryContext, 580 controller, 581 details 582 ); 583 } 584 } 585 586 #notifyEngagement(engagementProviders, queryContext, controller, details) { 587 for (const provider of engagementProviders) { 588 if (details.result.providerName == provider.name) { 589 provider.tryMethod("onEngagement", queryContext, controller, details); 590 break; 591 } 592 } 593 } 594 595 #notifyImpression( 596 impressionProviders, 597 state, 598 queryContext, 599 controller, 600 visibleResultsByProviderName, 601 details 602 ) { 603 for (const provider of impressionProviders) { 604 const providerVisibleResults = 605 visibleResultsByProviderName.get(provider.name) ?? []; 606 607 if (providerVisibleResults.length) { 608 provider.tryMethod( 609 "onImpression", 610 state, 611 queryContext, 612 controller, 613 providerVisibleResults, 614 details 615 ); 616 } 617 } 618 } 619 620 #notifyAbandonment( 621 abandomentProviders, 622 queryContext, 623 controller, 624 visibleResultsByProviderName 625 ) { 626 for (const provider of abandomentProviders) { 627 if (visibleResultsByProviderName.has(provider.name)) { 628 provider.tryMethod("onAbandonment", queryContext, controller); 629 } 630 } 631 } 632 633 #notifySearchSessionEnd( 634 searchSessionEndProviders, 635 queryContext, 636 controller, 637 details 638 ) { 639 for (const provider of searchSessionEndProviders) { 640 provider.tryMethod( 641 "onSearchSessionEnd", 642 queryContext, 643 controller, 644 details 645 ); 646 } 647 } 648 } 649 650 export var UrlbarProvidersManager = new ProvidersManager( 651 localProviderModules.filter(info => 652 info.supportedInputTypes.includes("urlbar") 653 ) 654 ); 655 656 export var SearchbarProvidersManager = new ProvidersManager( 657 localProviderModules.filter(info => 658 info.supportedInputTypes.includes("searchbar") 659 ) 660 ); 661 662 /** 663 * Tracks a query status. 664 * Multiple queries can potentially be executed at the same time by different 665 * controllers. Each query has to track its own status and delays separately, 666 * to avoid conflicting with other ones. 667 */ 668 export class Query { 669 /** 670 * Initializes the query object. 671 * 672 * @param {UrlbarQueryContext} queryContext 673 * The query context. 674 * @param {?UrlbarController} controller 675 * The controller to be notified. May be null. 676 * @param {UrlbarMuxer} muxer 677 * The muxer to sort results. 678 * @param {UrlbarProvider[]} providers 679 * Array of all the providers. 680 */ 681 constructor(queryContext, controller, muxer, providers) { 682 this.context = queryContext; 683 this.context.results = []; 684 // Clear any state in the context object, since it could be reused by the 685 // caller and we don't want to port previous query state over. 686 this.context.pendingHeuristicProviders.clear(); 687 this.context.deferUserSelectionProviders.clear(); 688 this.unsortedResults = []; 689 this.muxer = muxer; 690 this.controller = controller; 691 this.providers = providers; 692 this.started = false; 693 this.canceled = false; 694 695 // This is used as a last safety filter in add(), thus we keep an unmodified 696 // copy of it. 697 this.acceptableSources = queryContext.sources.slice(); 698 } 699 700 /** 701 * Starts querying. 702 */ 703 async start() { 704 if (this.started) { 705 throw new Error("This Query has been started already"); 706 } 707 this.started = true; 708 709 // Check which providers should be queried by calling isActive on them. 710 let activeProviders = []; 711 let activePromises = []; 712 let maxPriority = -1; 713 for (let provider of this.providers) { 714 // This can be used by the provider to check the query is still running 715 // after executing async tasks: 716 // let instance = this.queryInstance; 717 // await ... 718 // if (instance != this.queryInstance) { 719 // // Query was canceled or a new one started. 720 // return; 721 // } 722 provider.queryInstance = this; 723 activePromises.push( 724 provider 725 .isActive(this.context, this.controller) 726 .then(isActive => { 727 if (isActive && !this.canceled) { 728 let priority = provider.tryMethod("getPriority", this.context); 729 if (priority >= maxPriority) { 730 // The provider's priority is at least as high as the max. 731 if (priority > maxPriority) { 732 // The provider's priority is higher than the max. Remove all 733 // previously added providers, since their priority is 734 // necessarily lower, by setting length to zero. 735 activeProviders.length = 0; 736 maxPriority = priority; 737 } 738 activeProviders.push(provider); 739 if (provider.deferUserSelection) { 740 this.context.deferUserSelectionProviders.add(provider.name); 741 } 742 } 743 } 744 }) 745 .catch(ex => lazy.logger.error(ex)) 746 ); 747 } 748 749 // We have to wait for all isActive calls to finish because we want to query 750 // only the highest priority active providers as determined by the priority 751 // logic above. 752 await Promise.all(activePromises); 753 754 if (this.canceled) { 755 this.controller = null; 756 return; 757 } 758 759 // Start querying active providers. 760 /** 761 * @type {(provider: UrlbarProvider) => Promise<void>} 762 */ 763 let startQuery = async provider => { 764 provider.logger.debug( 765 `Starting query for "${this.context.searchString}"` 766 ); 767 let addedResult = false; 768 await provider.tryMethod( 769 "startQuery", 770 this.context, 771 /** @type {Parameters<UrlbarProvider['startQuery']>[1]} */ 772 (innerProvider, result) => { 773 addedResult = true; 774 this.add(innerProvider, result); 775 } 776 ); 777 if (!addedResult) { 778 this.context.deferUserSelectionProviders.delete(provider.name); 779 } 780 }; 781 782 let queryPromises = []; 783 for (let provider of activeProviders) { 784 // Track heuristic providers. later we'll use this Set to wait for them 785 // before returning results to the user. 786 if (provider.type == lazy.UrlbarUtils.PROVIDER_TYPE.HEURISTIC) { 787 this.context.pendingHeuristicProviders.add(provider.name); 788 queryPromises.push( 789 startQuery(provider).finally(() => { 790 this.context.pendingHeuristicProviders.delete(provider.name); 791 }) 792 ); 793 continue; 794 } 795 if (!this._sleepTimer) { 796 // Tracks the delay timer. We will fire (in this specific case, cancel 797 // would do the same, since the callback is empty) the timer when the 798 // search is canceled, unblocking start(). 799 this._sleepTimer = new lazy.SkippableTimer({ 800 name: "Query provider timer", 801 time: lazy.UrlbarPrefs.get("delay"), 802 logger: provider.logger, 803 }); 804 } 805 queryPromises.push( 806 this._sleepTimer.promise.then(() => 807 this.canceled ? undefined : startQuery(provider) 808 ) 809 ); 810 } 811 812 lazy.logger.info( 813 `Queried ${queryPromises.length} providers: ${activeProviders.map( 814 p => p.name 815 )}` 816 ); 817 818 // Normally we wait for all the queries, but in case this is canceled we can 819 // return earlier. 820 let cancelPromise = new Promise(resolve => { 821 this._cancelQueries = resolve; 822 }); 823 await Promise.race([Promise.all(queryPromises), cancelPromise]); 824 825 // All the providers are done returning results, so we can stop chunking. 826 if (!this.canceled) { 827 await this._chunkTimer?.fire(); 828 } 829 830 // Break cycles with the controller to avoid leaks. 831 this.controller = null; 832 } 833 834 /** 835 * Cancels this query. Note: Invoking cancel multiple times is a no-op. 836 */ 837 cancel() { 838 if (this.canceled) { 839 return; 840 } 841 this.canceled = true; 842 this.context.deferUserSelectionProviders.clear(); 843 for (let provider of this.providers) { 844 provider.logger.debug( 845 `Canceling query for "${this.context.searchString}"` 846 ); 847 // Mark the instance as no more valid, see start() for details. 848 provider.queryInstance = null; 849 provider.tryMethod("cancelQuery", this.context); 850 } 851 this._chunkTimer?.cancel().catch(ex => lazy.logger.error(ex)); 852 this._sleepTimer?.fire().catch(ex => lazy.logger.error(ex)); 853 this._cancelQueries?.(); 854 } 855 856 /** 857 * Adds a result returned from a provider to the results set. 858 * 859 * @param {UrlbarProvider} provider The provider that returned the result. 860 * @param {UrlbarResult} result The result object. 861 */ 862 add(provider, result) { 863 if (!(provider instanceof lazy.UrlbarProvider)) { 864 throw new Error("Invalid provider passed to the add callback"); 865 } 866 867 // When this set is empty, we can display heuristic results early. We remove 868 // the provider from the list without checking result.heuristic since 869 // heuristic providers don't necessarily have to return heuristic results. 870 // We expect a provider with type HEURISTIC will return its heuristic 871 // result(s) first. 872 this.context.pendingHeuristicProviders.delete(provider.name); 873 874 // Stop returning results as soon as we've been canceled. 875 if (this.canceled) { 876 return; 877 } 878 879 // In search mode, don't allow heuristic results in the following cases 880 // since they don't make sense: 881 // * When the search string is empty, or 882 // * In local search mode, except for autofill results 883 if ( 884 result.heuristic && 885 this.context.searchMode && 886 (!this.context.trimmedSearchString || 887 (!this.context.searchMode.engineName && !result.autofill)) 888 ) { 889 return; 890 } 891 892 // Check if the result source should be filtered out. Pay attention to the 893 // heuristic result though, that is supposed to be added regardless. 894 if ( 895 !this.acceptableSources.includes(result.source) && 896 !result.heuristic && 897 // Treat form history as searches for the purpose of acceptableSources. 898 (result.type != lazy.UrlbarUtils.RESULT_TYPE.SEARCH || 899 result.source != lazy.UrlbarUtils.RESULT_SOURCE.HISTORY || 900 !this.acceptableSources.includes( 901 lazy.UrlbarUtils.RESULT_SOURCE.SEARCH 902 )) && 903 // To enable tab group search in tabs mode, allow actions to bypass 904 // acceptableSources. 905 !( 906 result.source == lazy.UrlbarUtils.RESULT_SOURCE.ACTIONS && 907 this.acceptableSources.includes(lazy.UrlbarUtils.RESULT_SOURCE.TABS) 908 ) 909 ) { 910 return; 911 } 912 913 // Filter out javascript results for safety. The provider is supposed to do 914 // it, but we don't want to risk leaking these out. 915 if ( 916 result.type != lazy.UrlbarUtils.RESULT_TYPE.KEYWORD && 917 result.payload.url && 918 result.payload.url.startsWith("javascript:") && 919 !this.context.searchString.startsWith("javascript:") && 920 lazy.UrlbarPrefs.get("filter.javascript") 921 ) { 922 return; 923 } 924 925 result.providerName = provider.name; 926 result.providerType = provider.type; 927 this.unsortedResults.push(result); 928 929 this._notifyResultsFromProvider(provider); 930 } 931 932 _notifyResultsFromProvider(provider) { 933 // We use a timer to reduce UI flicker, by adding results in chunks. 934 if (!this._chunkTimer || this._chunkTimer.done) { 935 // Either there's no heuristic provider pending at all, or the previous 936 // timer is done, but we're still getting results. Start a short timer 937 // to chunk remaining results. 938 this._chunkTimer = new lazy.SkippableTimer({ 939 name: "chunking", 940 callback: () => this._notifyResults(), 941 time: 942 this.controller?.manager.CHUNK_RESULTS_DELAY_MS ?? 943 DEFAULT_CHUNK_RESULTS_DELAY_MS, 944 logger: provider.logger, 945 }); 946 } else if ( 947 !this.context.pendingHeuristicProviders.size && 948 provider.type == lazy.UrlbarUtils.PROVIDER_TYPE.HEURISTIC 949 ) { 950 // All the active heuristic providers have returned results, we can skip 951 // the heuristic chunk timer and start showing results immediately. 952 this._chunkTimer.fire().catch(ex => lazy.logger.error(ex)); 953 } 954 955 // Otherwise some timer is still ongoing and we'll wait for it. 956 } 957 958 _notifyResults() { 959 this.muxer.sort(this.context, this.unsortedResults); 960 // We don't want to notify consumers if there are no results since they 961 // generally expect at least one result when notified, so bail, but only 962 // after nulling out the chunk timer above so that it will be restarted 963 // the next time results are added. 964 if (!this.context.results.length) { 965 return; 966 } 967 968 this.context.firstResultChanged = !lazy.ObjectUtils.deepEqual( 969 this.context.firstResult, 970 this.context.results[0] 971 ); 972 this.context.firstResult = this.context.results[0]; 973 974 if (this.controller) { 975 this.controller.receiveResults(this.context); 976 } 977 } 978 979 /** 980 * Returns the provider with the given name. 981 * 982 * @param {string} name 983 * The provider name. 984 * @returns {UrlbarProvider | undefined} 985 * The provider. 986 */ 987 getProvider(name) { 988 return this.providers.find(p => p.name == name); 989 } 990 } 991 992 /** 993 * Updates in place the sources for a given UrlbarQueryContext. 994 * 995 * @param {UrlbarQueryContext} context The query context to examine 996 * @returns {UrlbarSearchStringTokenData|undefined} The restriction token that 997 * was used to set sources, or undefined if there's no restriction token. 998 */ 999 function updateSourcesIfEmpty(context) { 1000 if (context.sources && context.sources.length) { 1001 return undefined; 1002 } 1003 let acceptedSources = []; 1004 // There can be only one restrict token per query. 1005 let restrictToken = 1006 context.sapName != "urlbar" 1007 ? undefined 1008 : context.tokens.find(t => 1009 [ 1010 lazy.UrlbarTokenizer.TYPE.RESTRICT_HISTORY, 1011 lazy.UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK, 1012 lazy.UrlbarTokenizer.TYPE.RESTRICT_TAG, 1013 lazy.UrlbarTokenizer.TYPE.RESTRICT_OPENPAGE, 1014 lazy.UrlbarTokenizer.TYPE.RESTRICT_SEARCH, 1015 lazy.UrlbarTokenizer.TYPE.RESTRICT_TITLE, 1016 lazy.UrlbarTokenizer.TYPE.RESTRICT_URL, 1017 lazy.UrlbarTokenizer.TYPE.RESTRICT_ACTION, 1018 ].includes(t.type) 1019 ); 1020 1021 // RESTRICT_TITLE and RESTRICT_URL do not affect query sources. 1022 let restrictTokenType = 1023 restrictToken && 1024 restrictToken.type != lazy.UrlbarTokenizer.TYPE.RESTRICT_TITLE && 1025 restrictToken.type != lazy.UrlbarTokenizer.TYPE.RESTRICT_URL 1026 ? restrictToken.type 1027 : undefined; 1028 1029 for (let source of Object.values(lazy.UrlbarUtils.RESULT_SOURCE)) { 1030 // Check prefs and restriction tokens. 1031 switch (source) { 1032 case lazy.UrlbarUtils.RESULT_SOURCE.BOOKMARKS: 1033 if ( 1034 restrictTokenType === lazy.UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK || 1035 restrictTokenType === lazy.UrlbarTokenizer.TYPE.RESTRICT_TAG || 1036 (!restrictTokenType && lazy.UrlbarPrefs.get("suggest.bookmark")) 1037 ) { 1038 acceptedSources.push(source); 1039 } 1040 break; 1041 case lazy.UrlbarUtils.RESULT_SOURCE.HISTORY: 1042 if ( 1043 restrictTokenType === lazy.UrlbarTokenizer.TYPE.RESTRICT_HISTORY || 1044 (!restrictTokenType && lazy.UrlbarPrefs.get("suggest.history")) 1045 ) { 1046 acceptedSources.push(source); 1047 } 1048 break; 1049 case lazy.UrlbarUtils.RESULT_SOURCE.SEARCH: 1050 if ( 1051 restrictTokenType === lazy.UrlbarTokenizer.TYPE.RESTRICT_SEARCH || 1052 !restrictTokenType 1053 ) { 1054 // We didn't check browser.urlbar.suggest.searches here, because it 1055 // just controls search suggestions. If a search suggestion arrives 1056 // here, we lost already, because we broke user's privacy by hitting 1057 // the network. Thus, it's better to leave things go through and 1058 // notice the bug, rather than hiding it with a filter. 1059 acceptedSources.push(source); 1060 } 1061 break; 1062 case lazy.UrlbarUtils.RESULT_SOURCE.TABS: 1063 if ( 1064 restrictTokenType === lazy.UrlbarTokenizer.TYPE.RESTRICT_OPENPAGE || 1065 (!restrictTokenType && lazy.UrlbarPrefs.get("suggest.openpage")) 1066 ) { 1067 acceptedSources.push(source); 1068 } 1069 break; 1070 case lazy.UrlbarUtils.RESULT_SOURCE.OTHER_NETWORK: 1071 if (!context.isPrivate && !restrictTokenType) { 1072 acceptedSources.push(source); 1073 } 1074 break; 1075 case lazy.UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL: 1076 case lazy.UrlbarUtils.RESULT_SOURCE.ADDON: 1077 default: 1078 if (!restrictTokenType) { 1079 acceptedSources.push(source); 1080 } 1081 break; 1082 } 1083 } 1084 context.sources = acceptedSources; 1085 return restrictToken; 1086 }