diff --git a/browser/components/extensions/test/xpcshell/test_ext_urlbar.js b/browser/components/extensions/test/xpcshell/test_ext_urlbar.js index 3cb0511be79d3..ce2e319f4b0ee 100644 --- a/browser/components/extensions/test/xpcshell/test_ext_urlbar.js +++ b/browser/components/extensions/test/xpcshell/test_ext_urlbar.js @@ -188,7 +188,7 @@ add_task(async function test_registerProvider() { }); // Adds a single active provider that returns many kinds of results. This also -// checks that the heuristic result from the built-in HeuristicFallback provider +// checks that the heuristic result from the built-in UnifiedComplete provider // is included. add_task(async function test_onProviderResultsRequested() { let ext = ExtensionTestUtils.loadExtension({ @@ -276,7 +276,7 @@ add_task(async function test_onProviderResultsRequested() { // Check the results. let expectedResults = [ - // The first result should be a search result returned by HeuristicFallback. + // The first result should be a search result returned by UnifiedComplete. { type: UrlbarUtils.RESULT_TYPE.SEARCH, source: UrlbarUtils.RESULT_SOURCE.SEARCH, diff --git a/browser/components/urlbar/UrlbarMuxerUnifiedComplete.jsm b/browser/components/urlbar/UrlbarMuxerUnifiedComplete.jsm index 1f382b2bfb848..53af4a7da7765 100644 --- a/browser/components/urlbar/UrlbarMuxerUnifiedComplete.jsm +++ b/browser/components/urlbar/UrlbarMuxerUnifiedComplete.jsm @@ -41,16 +41,6 @@ function groupFromResult(result) { } } -// Breaks ties among heuristic results. Providers higher up the list are higher -// priority. -const heuristicOrder = [ - // Test providers are handled in sort(), - // Extension providers are handled in sort(), - "UrlbarProviderSearchTips", - "Omnibox", - "UnifiedComplete", - "HeuristicFallback", -]; /** * Class used to create a muxer. * The muxer receives and sorts results in a UrlbarQueryContext. @@ -64,19 +54,11 @@ class MuxerUnifiedComplete extends UrlbarMuxer { return "UnifiedComplete"; } - /* eslint-disable complexity */ /** * Sorts results in the given UrlbarQueryContext. * - * sort() is not suitable to be broken up into smaller functions or to rely - * on more convenience functions. It exists to efficiently group many - * conditions into just three loops. As a result, we must disable complexity - * linting. - * * @param {UrlbarQueryContext} context * The query context. - * @returns {boolean} If results were successfully sorted. This return value - * is a stopgap and can be removed when bug 1648468 lands. */ sort(context) { // This method is called multiple times per keystroke, so it should be as @@ -88,42 +70,19 @@ class MuxerUnifiedComplete extends UrlbarMuxer { // Capture information about the heuristic result to dedupe results from the // heuristic more quickly. - let topHeuristicRank = Infinity; - for (let result of context.allHeuristicResults) { - // Determine the highest-ranking heuristic result. - if (!result.heuristic) { - continue; - } - - // + 2 to reserve the highest-priority slots for test and extension - // providers. - let heuristicRank = heuristicOrder.indexOf(result.providerName) + 2; - // Extension and test provider names vary widely and aren't suitable - // for a static safelist like heuristicOrder. - if (result.providerType == UrlbarUtils.PROVIDER_TYPE.EXTENSION) { - heuristicRank = 1; - } else if (result.providerName.startsWith("TestProvider")) { - heuristicRank = 0; - } else if (heuristicRank - 2 == -1) { - throw new Error( - `Heuristic result returned by unexpected provider: ${result.providerName}` - ); - } - // Replace in case of ties, which would occur if a provider sent two - // heuristic results. - if (heuristicRank <= topHeuristicRank) { - topHeuristicRank = heuristicRank; - context.heuristicResult = result; - } - } - let heuristicResultQuery; + let heuristicResultOmniboxContent; if (context.heuristicResult) { if ( context.heuristicResult.type == UrlbarUtils.RESULT_TYPE.SEARCH && context.heuristicResult.payload.query ) { heuristicResultQuery = context.heuristicResult.payload.query.toLocaleLowerCase(); + } else if ( + context.heuristicResult.type == UrlbarUtils.RESULT_TYPE.OMNIBOX && + context.heuristicResult.payload.content + ) { + heuristicResultOmniboxContent = context.heuristicResult.payload.content.toLocaleLowerCase(); } } @@ -136,17 +95,10 @@ class MuxerUnifiedComplete extends UrlbarMuxer { UrlbarPrefs.get("maxHistoricalSearchSuggestions"), context.maxResults ); - let hasUnifiedComplete = false; // Do the first pass through the results. We only collect info for the // second pass here. for (let result of context.results) { - // Keep track of whether UnifiedComplete has returned results. We will - // quit early if it hasn't. - if (result.providerName == "UnifiedComplete") { - hasUnifiedComplete = true; - } - // The "Search in a Private Window" result should only be shown when there // are other results and all of them are searches. It should not be shown // if the user typed an alias because that's an explicit engine choice. @@ -187,24 +139,9 @@ class MuxerUnifiedComplete extends UrlbarMuxer { } } - // Quit early if we're still waiting on UnifiedComplete. If it turns out - // UnifiedComplete doesn't need to return any results, it will call sort() - // regardless to unblock showing results. - if ( - !hasUnifiedComplete && - context.pendingHeuristicProviders.has("UnifiedComplete") - ) { - return false; - } - // Do the second pass through results to build the list of unsorted results. let unsortedResults = []; for (let result of context.results) { - // Exclude low-ranked heuristic results. - if (result.heuristic && result != context.heuristicResult) { - continue; - } - // Exclude "Search in a Private Window" as determined in the first pass. if ( result.type == UrlbarUtils.RESULT_TYPE.SEARCH && @@ -277,6 +214,15 @@ class MuxerUnifiedComplete extends UrlbarMuxer { } } + // Exclude omnibox results that dupe the heuristic. + if ( + !result.heuristic && + result.type == UrlbarUtils.RESULT_TYPE.OMNIBOX && + result.payload.content == heuristicResultOmniboxContent + ) { + continue; + } + // Include this result. unsortedResults.push(result); } @@ -326,7 +272,6 @@ class MuxerUnifiedComplete extends UrlbarMuxer { } context.results = sortedResults; - return true; } /** diff --git a/browser/components/urlbar/UrlbarProviderHeuristicFallback.jsm b/browser/components/urlbar/UrlbarProviderHeuristicFallback.jsm deleted file mode 100644 index 78b4caab9d4f6..0000000000000 --- a/browser/components/urlbar/UrlbarProviderHeuristicFallback.jsm +++ /dev/null @@ -1,290 +0,0 @@ -/* 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/. */ - -"use strict"; - -/** - * This module exports a provider that provides a heuristic result. The result - * either vists a URL or does a search with the current engine. This result is - * always the ultimate fallback for any query, so this provider is always active. - */ - -var EXPORTED_SYMBOLS = ["UrlbarProviderHeuristicFallback"]; - -const { XPCOMUtils } = ChromeUtils.import( - "resource://gre/modules/XPCOMUtils.jsm" -); -XPCOMUtils.defineLazyModuleGetters(this, { - Log: "resource://gre/modules/Log.jsm", - Services: "resource://gre/modules/Services.jsm", - UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm", - UrlbarProvider: "resource:///modules/UrlbarUtils.jsm", - UrlbarResult: "resource:///modules/UrlbarResult.jsm", - UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.jsm", - UrlbarUtils: "resource:///modules/UrlbarUtils.jsm", -}); - -XPCOMUtils.defineLazyGetter(this, "logger", () => - Log.repository.getLogger("Urlbar.Provider.HeuristicFallback") -); - -/** - * Class used to create the provider. - */ -class ProviderHeuristicFallback extends UrlbarProvider { - constructor() { - super(); - // Maps the running queries by queryContext. - this.queries = new Map(); - } - - /** - * Returns the name of this provider. - * @returns {string} the name of this provider. - */ - get name() { - return "HeuristicFallback"; - } - - /** - * Returns the type of this provider. - * @returns {integer} one of the types from UrlbarUtils.PROVIDER_TYPE.* - */ - get type() { - return UrlbarUtils.PROVIDER_TYPE.HEURISTIC; - } - - /** - * 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) { - return true; - } - - /** - * Gets the provider's priority. - * @param {UrlbarQueryContext} queryContext The query context object - * @returns {number} The provider's priority for the given query. - */ - getPriority(queryContext) { - return 0; - } - - /** - * Starts querying. - * @param {object} queryContext The query context object - * @param {function} addCallback Callback invoked by the provider to add a new - * result. - * @returns {Promise} resolved when the query stops. - */ - async startQuery(queryContext, addCallback) { - logger.info(`Starting query for ${queryContext.searchString}`); - let instance = {}; - this.queries.set(queryContext, instance); - - let result = this._matchUnknownUrl(queryContext); - if (result) { - addCallback(this, result); - // Since we can't tell if this is a real URL and whether the user wants - // to visit or search for it, we provide an alternative searchengine - // match if the string looks like an alphanumeric origin or an e-mail. - let str = queryContext.searchString; - try { - new URL(str); - } catch (ex) { - if ( - UrlbarPrefs.get("keyword.enabled") && - (UrlbarTokenizer.looksLikeOrigin(str, { - noIp: true, - noPort: true, - }) || - UrlbarTokenizer.REGEXP_COMMON_EMAIL.test(str)) - ) { - let searchResult = await this._defaultEngineSearchResult( - queryContext - ); - if (!this.queries.has(queryContext)) { - return; - } - addCallback(this, searchResult); - } - } - } else { - result = await this._defaultEngineSearchResult(queryContext); - if (!result || !this.queries.has(queryContext)) { - return; - } - result.heuristic = true; - addCallback(this, result); - } - - this.queries.delete(queryContext); - } - - /** - * Cancels a running query. - * @param {object} queryContext The query context object - */ - cancelQuery(queryContext) { - logger.info(`Canceling query for ${queryContext.searchString}`); - this.queries.delete(queryContext); - } - - // TODO (bug 1054814): Use visited URLs to inform which scheme to use, if the - // scheme isn't specificed. - _matchUnknownUrl(queryContext) { - let unescapedSearchString = Services.textToSubURI.unEscapeURIForUI( - queryContext.searchString - ); - let [prefix, suffix] = UrlbarUtils.stripURLPrefix(unescapedSearchString); - if (!suffix && prefix) { - // The user just typed a stripped protocol, don't build a non-sense url - // like http://http/ for it. - return null; - } - // The user may have typed something like "word?" to run a search, we should - // not convert that to a url. - if ( - queryContext.restrictSource && - queryContext.restrictSource == UrlbarUtils.RESULT_SOURCE.SEARCH - ) { - return null; - } - - let searchUrl = queryContext.searchString.trim(); - - if (queryContext.fixupError) { - if ( - queryContext.fixupError == Cr.NS_ERROR_MALFORMED_URI && - !UrlbarPrefs.get("keyword.enabled") - ) { - let result = new UrlbarResult( - UrlbarUtils.RESULT_TYPE.URL, - UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, - ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, { - title: [searchUrl, UrlbarUtils.HIGHLIGHT.TYPED], - url: [searchUrl, UrlbarUtils.HIGHLIGHT.TYPED], - icon: "", - }) - ); - result.heuristic = true; - return result; - } - - return null; - } - - // If the URI cannot be fixed or the preferred URI would do a keyword search, - // that basically means this isn't useful to us. Note that - // fixupInfo.keywordAsSent will never be true if the keyword.enabled pref - // is false or there are no engines, so in that case we will always return - // a "visit". - if (!queryContext.fixupInfo?.href || queryContext.fixupInfo?.isSearch) { - return null; - } - - let uri = new URL(queryContext.fixupInfo.href); - // Check the host, as "http:///" is a valid nsIURI, but not useful to us. - // But, some schemes are expected to have no host. So we check just against - // schemes we know should have a host. This allows new schemes to be - // implemented without us accidentally blocking access to them. - let hostExpected = ["http:", "https:", "ftp:", "chrome:"].includes( - uri.protocol - ); - if (hostExpected && !uri.host) { - return null; - } - - // getFixupURIInfo() escaped the URI, so it may not be pretty. Embed the - // escaped URL in the result since that URL should be "canonical". But - // pass the pretty, unescaped URL as the result's title, since it is - // displayed to the user. - let escapedURL = uri.toString(); - let displayURL = decodeURI(uri); - - // We don't know if this url is in Places or not, and checking that would - // be expensive. Thus we also don't know if we may have an icon. - // If we'd just try to fetch the icon for the typed string, we'd cause icon - // flicker, since the url keeps changing while the user types. - // By default we won't provide an icon, but for the subset of urls with a - // host we'll check for a typed slash and set favicon for the host part. - let iconUri = ""; - if (hostExpected && (searchUrl.endsWith("/") || uri.pathname.length > 1)) { - // Look for an icon with the entire URL except for the pathname, including - // scheme, usernames, passwords, hostname, and port. - let pathIndex = uri.toString().lastIndexOf(uri.pathname); - let prePath = uri.toString().slice(0, pathIndex); - iconUri = `page-icon:${prePath}/`; - } - - let result = new UrlbarResult( - UrlbarUtils.RESULT_TYPE.URL, - UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, - ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, { - title: [displayURL, UrlbarUtils.HIGHLIGHT.TYPED], - url: [escapedURL, UrlbarUtils.HIGHLIGHT.TYPED], - icon: iconUri, - }) - ); - result.heuristic = true; - return result; - } - - async _defaultEngineSearchResult(queryContext) { - let engine; - if (queryContext.engineName) { - engine = Services.search.getEngineByName(queryContext.engineName); - } else if (queryContext.isPrivate) { - engine = Services.search.defaultPrivateEngine; - } else { - engine = Services.search.defaultEngine; - } - - if (!engine) { - return null; - } - - // Strip a leading search restriction char, because we prepend it to text - // when the search shortcut is used and it's not user typed. Don't strip - // other restriction chars, so that it's possible to search for things - // including one of those (e.g. "c#"). - let query = queryContext.searchString; - if ( - queryContext.tokens[0] && - queryContext.tokens[0].value === UrlbarTokenizer.RESTRICT.SEARCH - ) { - query = UrlbarUtils.substringAfter( - query, - queryContext.tokens[0].value - ).trim(); - } - - let result = new UrlbarResult( - UrlbarUtils.RESULT_TYPE.SEARCH, - UrlbarUtils.RESULT_SOURCE.SEARCH, - ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, { - engine: [engine.name, UrlbarUtils.HIGHLIGHT.TYPED], - icon: [engine.iconURI?.spec || ""], - query: [query, UrlbarUtils.HIGHLIGHT.NONE], - // We're confident that there is no alias, since UnifiedComplete - // handles heuristic searches with aliases. - keyword: undefined, - keywordOffer: UrlbarUtils.KEYWORD_OFFER.NONE, - // For test interoperabilty with UrlbarProviderSearchSuggestions. - suggestion: undefined, - tailPrefix: undefined, - tail: undefined, - tailOffsetIndex: -1, - isSearchHistory: false, - }) - ); - return result; - } -} - -var UrlbarProviderHeuristicFallback = new ProviderHeuristicFallback(); diff --git a/browser/components/urlbar/UrlbarProviderOmnibox.jsm b/browser/components/urlbar/UrlbarProviderOmnibox.jsm index 0ae009353dd41..795e60e511a23 100644 --- a/browser/components/urlbar/UrlbarProviderOmnibox.jsm +++ b/browser/components/urlbar/UrlbarProviderOmnibox.jsm @@ -55,7 +55,7 @@ class ProviderOmnibox extends UrlbarProvider { * @returns {integer} one of the types from UrlbarUtils.PROVIDER_TYPE.* */ get type() { - return UrlbarUtils.PROVIDER_TYPE.HEURISTIC; + return UrlbarUtils.PROVIDER_TYPE.EXTENSION; } /** @@ -120,25 +120,8 @@ class ProviderOmnibox extends UrlbarProvider { let instance = {}; this.queries.set(queryContext, instance); - // Fetch heuristic result. - let keyword = queryContext.tokens[0].value; - let description = ExtensionSearchHandler.getDescription(keyword); - let heuristicResult = new UrlbarResult( - UrlbarUtils.RESULT_TYPE.OMNIBOX, - UrlbarUtils.RESULT_SOURCE.OTHER_NETWORK, - ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, { - title: [description, UrlbarUtils.HIGHLIGHT.TYPED], - content: [queryContext.searchString, UrlbarUtils.HIGHLIGHT.TYPED], - keyword: [queryContext.tokens[0].value, UrlbarUtils.HIGHLIGHT.TYPED], - icon: UrlbarUtils.ICON.EXTENSION, - }) - ); - heuristicResult.heuristic = true; - addCallback(this, heuristicResult); - - // Fetch non-heuristic results. let data = { - keyword, + keyword: queryContext.tokens[0].value, text: queryContext.searchString, inPrivateWindow: queryContext.isPrivate, }; @@ -147,9 +130,6 @@ class ProviderOmnibox extends UrlbarProvider { suggestions => { for (let suggestion of suggestions) { let content = `${queryContext.tokens[0].value} ${suggestion.content}`; - if (content == heuristicResult.payload.content) { - continue; - } let result = new UrlbarResult( UrlbarUtils.RESULT_TYPE.OMNIBOX, UrlbarUtils.RESULT_SOURCE.OTHER_NETWORK, diff --git a/browser/components/urlbar/UrlbarProviderSearchSuggestions.jsm b/browser/components/urlbar/UrlbarProviderSearchSuggestions.jsm index 788e60de0813a..0301a6bfd6d91 100644 --- a/browser/components/urlbar/UrlbarProviderSearchSuggestions.jsm +++ b/browser/components/urlbar/UrlbarProviderSearchSuggestions.jsm @@ -199,7 +199,10 @@ class ProviderSearchSuggestions extends UrlbarProvider { // Disallow remote suggestions for strings containing tokens that look like // URIs, to avoid disclosing information about networks or passwords. - if (queryContext.fixupInfo?.href && !queryContext.fixupInfo?.isSearch) { + if ( + queryContext.fixupInfo.fixedURI && + !queryContext.fixupInfo.keywordAsSent + ) { return false; } diff --git a/browser/components/urlbar/UrlbarProviderUnifiedComplete.jsm b/browser/components/urlbar/UrlbarProviderUnifiedComplete.jsm index 691cbe46b5c82..b980a164ce32f 100644 --- a/browser/components/urlbar/UrlbarProviderUnifiedComplete.jsm +++ b/browser/components/urlbar/UrlbarProviderUnifiedComplete.jsm @@ -90,17 +90,8 @@ class ProviderUnifiedComplete extends UrlbarProvider { acResult, urls ); - // The Muxer gives UnifiedComplete special status and waits for it to - // return results before sorting them. We need to know that - // UnifiedComplete has finished even if it isn't returning results, so we - // call addCallback with an empty result here, which is something only - // UnifiedComplete is allowed to do. - if (!results.length) { - addCallback(this, null); - } else { - for (let result of results) { - addCallback(this, result); - } + for (let result of results) { + addCallback(this, result); } }); this.queries.delete(queryContext); @@ -263,6 +254,17 @@ function makeUrlbarResult(tokens, info) { }) ); } + case "extension": + return new UrlbarResult( + UrlbarUtils.RESULT_TYPE.OMNIBOX, + UrlbarUtils.RESULT_SOURCE.OTHER_NETWORK, + ...UrlbarResult.payloadAndSimpleHighlights(tokens, { + title: [info.comment, UrlbarUtils.HIGHLIGHT.TYPED], + content: [action.params.content, UrlbarUtils.HIGHLIGHT.TYPED], + keyword: [action.params.keyword, UrlbarUtils.HIGHLIGHT.TYPED], + icon: [info.icon], + }) + ); case "remotetab": return new UrlbarResult( UrlbarUtils.RESULT_TYPE.REMOTE_TAB, diff --git a/browser/components/urlbar/UrlbarProvidersManager.jsm b/browser/components/urlbar/UrlbarProvidersManager.jsm index 4c607647f1b3d..1a0a2c0850527 100644 --- a/browser/components/urlbar/UrlbarProvidersManager.jsm +++ b/browser/components/urlbar/UrlbarProvidersManager.jsm @@ -35,8 +35,6 @@ XPCOMUtils.defineLazyGetter(this, "logger", () => var localProviderModules = { UrlbarProviderUnifiedComplete: "resource:///modules/UrlbarProviderUnifiedComplete.jsm", - UrlbarProviderHeuristicFallback: - "resource:///modules/UrlbarProviderHeuristicFallback.jsm", UrlbarProviderInterventions: "resource:///modules/UrlbarProviderInterventions.jsm", UrlbarProviderOmnibox: "resource:///modules/UrlbarProviderOmnibox.jsm", @@ -366,7 +364,6 @@ class Query { let queryPromises = []; for (let provider of activeProviders) { if (provider.type == UrlbarUtils.PROVIDER_TYPE.HEURISTIC) { - this.context.pendingHeuristicProviders.add(provider.name); queryPromises.push( provider.tryMethod("startQuery", this.context, this.add.bind(this)) ); @@ -437,29 +434,13 @@ class Query { if (!(provider instanceof UrlbarProvider)) { throw new Error("Invalid provider passed to the add callback"); } - - // When this set is empty, we can display heuristic results early. We remove - // the provider from the list without checking result.heuristic since - // heuristic providers don't necessarily have to return heuristic results. - // We expect a provider with type HEURISTIC will return its heuristic - // result(s) first. - this.context.pendingHeuristicProviders.delete(provider.name); - // Stop returning results as soon as we've been canceled. if (this.canceled) { return; } - - let addResult = true; - - if (!result) { - addResult = false; - } - // Check if the result source should be filtered out. Pay attention to the // heuristic result though, that is supposed to be added regardless. if ( - addResult && !this.acceptableSources.includes(result.source) && !result.heuristic && // Treat form history as searches for the purpose of acceptableSources. @@ -467,60 +448,35 @@ class Query { result.source != UrlbarUtils.RESULT_SOURCE.HISTORY || !this.acceptableSources.includes(UrlbarUtils.RESULT_SOURCE.SEARCH)) ) { - addResult = false; + return; } // Filter out javascript results for safety. The provider is supposed to do // it, but we don't want to risk leaking these out. if ( - addResult && result.type != UrlbarUtils.RESULT_TYPE.KEYWORD && result.payload.url && result.payload.url.startsWith("javascript:") && !this.context.searchString.startsWith("javascript:") && UrlbarPrefs.get("filter.javascript") ) { - addResult = false; - } - - // We wait on UnifiedComplete to return a heuristic result. If it has no - // heuristic result to return, we still need to sort and display results, so - // we follow the usual codepath to muxer.sort. We only offer this for - // UnifiedComplete, so we check the provider's name here. This is a stopgap - // measure until bug 1648468 lands. - if (!addResult) { - if (provider.name == "UnifiedComplete") { - this._notifyResultsFromProvider(provider); - } return; } + result.providerName = provider.name; - result.providerType = provider.type; this.context.results.push(result); if (result.heuristic) { - this.context.allHeuristicResults.push(result); + this.context.heuristicResult = result; } this._notifyResultsFromProvider(provider); } _notifyResultsFromProvider(provider) { - // We create two chunking timers: one for heuristic results, and one for - // other results. We expect heuristic providers to return their heuristic - // results before other results/providers in most cases. When all heuristic - // providers have returned some results, we fire the heuristic timer early. - // If the timer fires first, we stop waiting on the remaining heuristic - // providers. - // Both timers are used to reduce UI flicker. + // If the provider is not of heuristic type, chunk results, to improve the + // dataflow and reduce UI flicker. if (provider.type == UrlbarUtils.PROVIDER_TYPE.HEURISTIC) { - if (!this._heuristicProviderTimer) { - this._heuristicProviderTimer = new SkippableTimer({ - name: "Heuristic provider timer", - callback: () => this._notifyResults(), - time: CHUNK_RESULTS_DELAY_MS, - logger, - }); - } + this._notifyResults(); } else if (!this._chunkTimer) { this._chunkTimer = new SkippableTimer({ name: "Query chunk timer", @@ -529,33 +485,16 @@ class Query { logger, }); } - // If all active heuristic providers have returned results, we can skip the - // heuristic results timer and start showing results immediately. - if ( - this._heuristicProviderTimer && - !this.context.pendingHeuristicProviders.size - ) { - this._heuristicProviderTimer.fire().catch(Cu.reportError); - } } _notifyResults() { - let sorted = this.muxer.sort(this.context); - - if (this._heuristicProviderTimer) { - this._heuristicProviderTimer.cancel().catch(Cu.reportError); - this._heuristicProviderTimer = null; - } + this.muxer.sort(this.context); if (this._chunkTimer) { this._chunkTimer.cancel().catch(Cu.reportError); this._chunkTimer = null; } - if (!sorted) { - return; - } - // Before the muxer.sort call above, this.context.results should never be // empty since this method is called when results are added. But the muxer // may have excluded one or more results from the final sorted list. For diff --git a/browser/components/urlbar/UrlbarUtils.jsm b/browser/components/urlbar/UrlbarUtils.jsm index 4094083696442..d521e0407e140 100644 --- a/browser/components/urlbar/UrlbarUtils.jsm +++ b/browser/components/urlbar/UrlbarUtils.jsm @@ -607,28 +607,6 @@ var UrlbarUtils = { return index < 0 ? "" : sourceStr.substr(index + targetStr.length); }, - /** - * Strips the prefix from a URL and returns the prefix and the remainder of the - * URL. "Prefix" is defined to be the scheme and colon, plus, if present, two - * slashes. If the given string is not actually a URL, then an empty prefix and - * the string itself is returned. - * - * @param {string} str The possible URL to strip. - * @returns {array} If `str` is a URL, then [prefix, remainder]. Otherwise, ["", str]. - */ - stripURLPrefix(str) { - const REGEXP_STRIP_PREFIX = /^[a-z]+:(?:\/){0,2}/i; - let match = REGEXP_STRIP_PREFIX.exec(str); - if (!match) { - return ["", str]; - } - let prefix = match[0]; - if (prefix.length < str.length && str[prefix.length] == " ") { - return ["", str]; - } - return [prefix, str.substr(prefix.length)]; - }, - /** * Runs a search for the given string, and returns the heuristic result. * @param {string} searchString The string to search for. @@ -651,7 +629,7 @@ var UrlbarUtils = { "usercontextid" ), allowSearchSuggestions: false, - providers: ["UnifiedComplete", "HeuristicFallback"], + providers: ["UnifiedComplete"], }); await UrlbarProvidersManager.startQuery(context); if (!context.heuristicResult) { @@ -1000,8 +978,6 @@ class UrlbarQueryContext { } this.lastResultCount = 0; - this.allHeuristicResults = []; - this.pendingHeuristicProviders = new Set(); this.userContextId = options.userContextId || Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID; @@ -1027,12 +1003,9 @@ class UrlbarQueryContext { /** * Caches and returns fixup info from URIFixup for the current search string. - * Only returns a subset of the properties from URIFixup. This is both to - * reduce the memory footprint of UrlbarQueryContexts and to keep them - * serializable so they can be sent to extensions. */ get fixupInfo() { - if (this.searchString.trim() && !this._fixupInfo) { + if (this.searchString && !this._fixupInfo) { let flags = Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS | Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP; @@ -1040,35 +1013,14 @@ class UrlbarQueryContext { flags |= Ci.nsIURIFixup.FIXUP_FLAG_PRIVATE_CONTEXT; } - try { - let info = Services.uriFixup.getFixupURIInfo( - this.searchString.trim(), - flags - ); - this._fixupInfo = { - href: info.fixedURI.spec, - isSearch: !!info.keywordAsSent, - }; - } catch (ex) { - this._fixupError = ex.result; - } + this._fixupInfo = Services.uriFixup.getFixupURIInfo( + this.searchString.trim(), + flags + ); } return this._fixupInfo || null; } - - /** - * Returns the error that was thrown when fixupInfo was fetched, if any. If - * fixupInfo has not yet been fetched for this queryContext, it is fetched - * here. - */ - get fixupError() { - if (!this.fixupInfo) { - return this._fixupError; - } - - return null; - } } /** diff --git a/browser/components/urlbar/moz.build b/browser/components/urlbar/moz.build index 556eb701a1e4a..e06e4157d3944 100644 --- a/browser/components/urlbar/moz.build +++ b/browser/components/urlbar/moz.build @@ -12,7 +12,6 @@ EXTRA_JS_MODULES += [ 'UrlbarMuxerUnifiedComplete.jsm', 'UrlbarPrefs.jsm', 'UrlbarProviderExtension.jsm', - 'UrlbarProviderHeuristicFallback.jsm', 'UrlbarProviderInterventions.jsm', 'UrlbarProviderOmnibox.jsm', 'UrlbarProviderOpenTabs.jsm', diff --git a/browser/components/urlbar/tests/UrlbarTestUtils.jsm b/browser/components/urlbar/tests/UrlbarTestUtils.jsm index 016b6cca4dd9e..617dc4e76271a 100644 --- a/browser/components/urlbar/tests/UrlbarTestUtils.jsm +++ b/browser/components/urlbar/tests/UrlbarTestUtils.jsm @@ -598,7 +598,7 @@ class TestProvider extends UrlbarProvider { */ constructor({ results, - name = Math.floor(Math.random() * 100000), + name = "TestProvider" + Math.floor(Math.random() * 100000), type = UrlbarUtils.PROVIDER_TYPE.PROFILE, priority = 0, addTimeout = 0, @@ -613,7 +613,7 @@ class TestProvider extends UrlbarProvider { this._onCancel = onCancel; } get name() { - return "TestProvider" + this._name; + return this._name; } get type() { return this._type; diff --git a/browser/components/urlbar/tests/browser/browser_updateRows.js b/browser/components/urlbar/tests/browser/browser_updateRows.js index b764c166bf6d9..b1d6e983c7af5 100644 --- a/browser/components/urlbar/tests/browser/browser_updateRows.js +++ b/browser/components/urlbar/tests/browser/browser_updateRows.js @@ -21,19 +21,19 @@ add_task(async function urlToTip() { ]); // Add a provider that returns a tip result when the search string is "testx". - let tipResult = new UrlbarResult( - UrlbarUtils.RESULT_TYPE.TIP, - UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, - { - text: "This is a test tip.", - buttonText: "OK", - helpUrl: "http://example.com/", - type: "test", - } - ); - tipResult.suggestedIndex = 1; let provider = new UrlbarTestUtils.TestProvider({ - results: [tipResult], + results: [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TIP, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + text: "This is a test tip.", + buttonText: "OK", + helpUrl: "http://example.com/", + type: "test", + } + ), + ], }); provider.isActive = context => context.searchString == "testx"; UrlbarProvidersManager.registerProvider(provider); @@ -122,19 +122,19 @@ add_task(async function tipToURL() { // Add a provider that returns a tip result when the search string is "test" // or "testxx". - let tipResult = new UrlbarResult( - UrlbarUtils.RESULT_TYPE.TIP, - UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, - { - text: "This is a test tip.", - buttonText: "OK", - helpUrl: "http://example.com/", - type: "test", - } - ); - tipResult.suggestedIndex = 1; let provider = new UrlbarTestUtils.TestProvider({ - results: [tipResult], + results: [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TIP, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + text: "This is a test tip.", + buttonText: "OK", + helpUrl: "http://example.com/", + type: "test", + } + ), + ], }); provider.isActive = context => ["test", "testxx"].includes(context.searchString); diff --git a/browser/components/urlbar/tests/unit/test_providerHeuristicFallback.js b/browser/components/urlbar/tests/unit/test_providerHeuristicFallback.js deleted file mode 100644 index 9dd9c02a501b0..0000000000000 --- a/browser/components/urlbar/tests/unit/test_providerHeuristicFallback.js +++ /dev/null @@ -1,536 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - http://creativecommons.org/publicdomain/zero/1.0/ */ - -/** - * Tests that visit-url and search engine heuristic results are returned by - * UrlbarProviderHeuristicFallback. - */ - -const ENGINE_NAME = "engine-suggestions.xml"; -const SUGGEST_PREF = "browser.urlbar.suggest.searches"; -const SUGGEST_ENABLED_PREF = "browser.search.suggest.enabled"; -const PRIVATE_SEARCH_PREF = "browser.search.separatePrivateDefault.ui.enabled"; - -add_task(async function setup() { - // Install a test engine so we're sure of ENGINE_NAME. - let engine = await addTestSuggestionsEngine(); - - // Install the test engine. - let oldDefaultEngine = await Services.search.getDefault(); - registerCleanupFunction(async () => { - Services.search.setDefault(oldDefaultEngine); - Services.prefs.clearUserPref(SUGGEST_PREF); - Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF); - Services.prefs.clearUserPref(PRIVATE_SEARCH_PREF); - }); - Services.search.setDefault(engine); - Services.prefs.setBoolPref(SUGGEST_PREF, false); - Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, false); - Services.prefs.setBoolPref(PRIVATE_SEARCH_PREF, false); -}); - -add_task(async function() { - info("visit url, no protocol"); - let query = "mozilla.org"; - let context = createContext(query, { isPrivate: false }); - await check_results({ - context, - matches: [ - makeVisitResult(context, { - uri: `http://${query}/`, - title: `http://${query}/`, - iconUri: "", - heuristic: true, - }), - makeSearchResult(context, { - engineName: ENGINE_NAME, - }), - ], - }); - - info("visit url, no protocol but with 2 dots"); - query = "www.mozilla.org"; - context = createContext(query, { isPrivate: false }); - await check_results({ - context, - matches: [ - makeVisitResult(context, { - uri: `http://${query}/`, - title: `http://${query}/`, - iconUri: "", - heuristic: true, - }), - makeSearchResult(context, { - engineName: ENGINE_NAME, - }), - ], - }); - - info("visit url, no protocol, e-mail like"); - query = "a@b.com"; - context = createContext(query, { isPrivate: false }); - await check_results({ - context, - matches: [ - makeVisitResult(context, { - uri: `http://${query}/`, - title: `http://${query}/`, - iconUri: "", - heuristic: true, - }), - makeSearchResult(context, { - engineName: ENGINE_NAME, - }), - ], - }); - - info("visit url, with protocol but with 2 dots"); - query = "https://www.mozilla.org"; - context = createContext(query, { isPrivate: false }); - await check_results({ - context, - matches: [ - makeVisitResult(context, { - uri: `${query}/`, - title: `${query}/`, - iconUri: "", - heuristic: true, - }), - ], - }); - - // info("visit url, with protocol but with 3 dots"); - query = "https://www.mozilla.org.tw"; - context = createContext(query, { isPrivate: false }); - await check_results({ - context, - matches: [ - makeVisitResult(context, { - uri: `${query}/`, - title: `${query}/`, - iconUri: "", - heuristic: true, - }), - ], - }); - - info("visit url, with protocol"); - query = "https://mozilla.org"; - context = createContext(query, { isPrivate: false }); - await check_results({ - context, - matches: [ - makeVisitResult(context, { - uri: `${query}/`, - title: `${query}/`, - iconUri: "", - heuristic: true, - }), - ], - }); - - info("visit url, about: protocol (no host)"); - query = "about:nonexistent"; - context = createContext(query, { isPrivate: false }); - await check_results({ - context, - matches: [ - makeVisitResult(context, { - uri: query, - title: query, - iconUri: "", - heuristic: true, - }), - ], - }); - - info("visit url, with non-standard whitespace"); - query = "https://mozilla.org"; - context = createContext(`${query}\u2028`, { isPrivate: false }); - await check_results({ - context, - matches: [ - makeVisitResult(context, { - uri: `${query}/`, - title: `${query}/`, - iconUri: "", - heuristic: true, - }), - ], - }); - - // This is distinct because of how we predict being able to url autofill via - // host lookups. - info("visit url, host matching visited host but not visited url"); - await PlacesTestUtils.addVisits([ - { - uri: Services.io.newURI("http://mozilla.org/wine/"), - title: "Mozilla Wine", - transition: PlacesUtils.history.TRANSITION_TYPED, - }, - ]); - query = "mozilla.org/rum"; - context = createContext(`${query}\u2028`, { isPrivate: false }); - await check_results({ - context, - matches: [ - makeVisitResult(context, { - uri: `http://${query}`, - title: `http://${query}`, - iconUri: "page-icon:http://mozilla.org/", - heuristic: true, - }), - ], - }); - await PlacesUtils.history.clear(); - - // And hosts with no dot in them are special, due to requiring safelisting. - info("unknown host"); - query = "firefox"; - context = createContext(query, { isPrivate: false }); - await check_results({ - context, - matches: [ - makeSearchResult(context, { - engineName: ENGINE_NAME, - heuristic: true, - }), - ], - }); - - info("string with known host"); - Services.prefs.setBoolPref("browser.fixup.defaultToSearch", true); - query = "firefox/get"; - context = createContext(query, { isPrivate: false }); - await check_results({ - context, - matches: [ - makeSearchResult(context, { - engineName: ENGINE_NAME, - heuristic: true, - }), - ], - }); - - Services.prefs.setBoolPref("browser.fixup.defaultToSearch", false); - query = "firefox/get"; - context = createContext(query, { isPrivate: false }); - await check_results({ - context, - matches: [ - makeVisitResult(context, { - uri: `http://${query}`, - title: `http://${query}`, - iconUri: "page-icon:http://firefox/", - heuristic: true, - }), - ], - }); - Services.prefs.clearUserPref("browser.fixup.defaultToSearch"); - - Services.prefs.setBoolPref("browser.fixup.domainwhitelist.firefox", true); - registerCleanupFunction(() => { - Services.prefs.clearUserPref("browser.fixup.domainwhitelist.firefox"); - }); - - info("known host"); - query = "firefox"; - context = createContext(query, { isPrivate: false }); - await check_results({ - context, - matches: [ - makeVisitResult(context, { - uri: `http://${query}/`, - title: `http://${query}/`, - iconUri: "", - heuristic: true, - }), - makeSearchResult(context, { - engineName: ENGINE_NAME, - }), - ], - }); - - info("url with known host"); - query = "firefox/get"; - context = createContext(query, { isPrivate: false }); - await check_results({ - context, - matches: [ - makeVisitResult(context, { - uri: `http://${query}`, - title: `http://${query}`, - iconUri: "page-icon:http://firefox/", - heuristic: true, - }), - ], - }); - - info("visit url, host matching visited host but not visited url, known host"); - Services.prefs.setBoolPref("browser.fixup.domainwhitelist.mozilla", true); - registerCleanupFunction(() => { - Services.prefs.clearUserPref("browser.fixup.domainwhitelist.mozilla"); - }); - query = "mozilla/rum"; - context = createContext(query, { isPrivate: false }); - await check_results({ - context, - matches: [ - makeVisitResult(context, { - uri: `http://${query}`, - title: `http://${query}`, - iconUri: "page-icon:http://mozilla/", - heuristic: true, - }), - ], - }); - - // ipv4 and ipv6 literal addresses should offer to visit. - info("visit url, ipv4 literal"); - query = "127.0.0.1"; - context = createContext(query, { isPrivate: false }); - await check_results({ - context, - matches: [ - makeVisitResult(context, { - uri: `http://${query}/`, - title: `http://${query}/`, - iconUri: "", - heuristic: true, - }), - ], - }); - - info("visit url, ipv6 literal"); - query = "[2001:db8::1]"; - context = createContext(query, { isPrivate: false }); - await check_results({ - context, - matches: [ - makeVisitResult(context, { - uri: `http://${query}/`, - title: `http://${query}/`, - iconUri: "", - heuristic: true, - }), - ], - }); - - // Setting keyword.enabled to false should always try to visit. - let keywordEnabled = Services.prefs.getBoolPref("keyword.enabled"); - Services.prefs.setBoolPref("keyword.enabled", false); - registerCleanupFunction(() => { - Services.prefs.clearUserPref("keyword.enabled"); - }); - info("visit url, keyword.enabled = false"); - query = "bacon"; - context = createContext(query, { isPrivate: false }); - await check_results({ - context, - matches: [ - makeVisitResult(context, { - uri: `http://${query}/`, - title: `http://${query}/`, - iconUri: "", - heuristic: true, - }), - ], - }); - - info("visit two word query, keyword.enabled = false"); - query = "bacon lovers"; - context = createContext(query, { isPrivate: false }); - await check_results({ - context, - matches: [ - makeVisitResult(context, { - uri: query, - title: query, - iconUri: "", - heuristic: true, - }), - ], - }); - Services.prefs.setBoolPref("keyword.enabled", true); - info("visit two word query, keyword.enabled = true"); - query = "bacon lovers"; - context = createContext(query, { isPrivate: false }); - await check_results({ - context, - matches: [ - makeSearchResult(context, { - engineName: ENGINE_NAME, - heuristic: true, - }), - ], - }); - - Services.prefs.setBoolPref("keyword.enabled", keywordEnabled); - - info("visit url, scheme+host"); - query = "http://example"; - context = createContext(query, { isPrivate: false }); - await check_results({ - context, - matches: [ - makeVisitResult(context, { - uri: `${query}/`, - title: `${query}/`, - iconUri: "", - heuristic: true, - }), - ], - }); - - info("visit url, scheme+host"); - query = "ftp://example"; - context = createContext(query, { isPrivate: false }); - await check_results({ - context, - matches: [ - makeVisitResult(context, { - uri: `${query}/`, - title: `${query}/`, - iconUri: "", - heuristic: true, - }), - ], - }); - - info("visit url, host+port"); - query = "example:8080"; - context = createContext(query, { isPrivate: false }); - await check_results({ - context, - matches: [ - makeVisitResult(context, { - uri: `http://${query}/`, - title: `http://${query}/`, - iconUri: "", - heuristic: true, - }), - ], - }); - - info("numerical operations that look like urls should search"); - query = "123/12"; - context = createContext(query, { isPrivate: false }); - await check_results({ - context, - matches: [ - makeSearchResult(context, { - engineName: ENGINE_NAME, - heuristic: true, - }), - ], - }); - - info("numerical operations that look like urls should search"); - query = "123.12/12.1"; - context = createContext(query, { isPrivate: false }); - await check_results({ - context, - matches: [ - makeSearchResult(context, { - engineName: ENGINE_NAME, - heuristic: true, - }), - ], - }); - - query = "resource:///modules"; - context = createContext(query, { isPrivate: false }); - await check_results({ - context, - matches: [ - makeVisitResult(context, { - uri: query, - title: query, - iconUri: "", - heuristic: true, - }), - ], - }); - - info("access resource://app/modules"); - query = "resource://app/modules"; - context = createContext(query, { isPrivate: false }); - await check_results({ - context, - matches: [ - makeVisitResult(context, { - uri: query, - title: query, - iconUri: "", - heuristic: true, - }), - ], - }); - - info("protocol with an extra slash"); - query = "http:///"; - context = createContext(query, { isPrivate: false }); - await check_results({ - context, - matches: [ - makeSearchResult(context, { - engineName: ENGINE_NAME, - heuristic: true, - }), - ], - }); - - info("change default engine"); - let originalTestEngine = Services.search.getEngineByName(ENGINE_NAME); - let engine2 = await Services.search.addEngineWithDetails("AliasEngine", { - alias: "alias", - method: "GET", - template: "http://example.com/?q={searchTerms}", - }); - Assert.notEqual( - Services.search.defaultEngine, - engine2, - "New engine shouldn't be the current engine yet" - ); - await Services.search.setDefault(engine2); - query = "toronto"; - context = createContext(query, { isPrivate: false }); - await check_results({ - context, - matches: [ - makeSearchResult(context, { - engineName: "AliasEngine", - heuristic: true, - }), - ], - }); - await Services.search.setDefault(originalTestEngine); - - info( - "Leading restriction tokens are not removed from the search result, apart from the search token." - ); - // Note that we use the alias from AliasEngine in the query. Since we're using - // a restriction token, we expect that the default engine be used. - for (let token of Object.values(UrlbarTokenizer.RESTRICT)) { - for (query of [`${token} alias query`, `query ${token}`]) { - let expectedQuery = - token == UrlbarTokenizer.RESTRICT.SEARCH && - query.startsWith(UrlbarTokenizer.RESTRICT.SEARCH) - ? query.substring(2) - : query; - context = createContext(query, { isPrivate: false }); - info(`Searching for "${query}", expecting "${expectedQuery}"`); - await check_results({ - context, - matches: [ - makeSearchResult(context, { - engineName: ENGINE_NAME, - query: expectedQuery, - heuristic: true, - }), - ], - }); - } - } - await Services.search.removeEngine(engine2); -}); diff --git a/browser/components/urlbar/tests/unit/test_providersManager_filtering.js b/browser/components/urlbar/tests/unit/test_providersManager_filtering.js index cd4ccf72e16cf..cfe4a9cb674ea 100644 --- a/browser/components/urlbar/tests/unit/test_providersManager_filtering.js +++ b/browser/components/urlbar/tests/unit/test_providersManager_filtering.js @@ -359,16 +359,30 @@ add_task(async function test_filter_priority() { /** * A test provider. */ - class TestProvider extends UrlbarTestUtils.TestProvider { + class TestProvider extends UrlbarProvider { constructor(priority, shouldBeInvoked, namePart = "") { super(); this._priority = priority; - this._name = `${priority}` + namePart; + this._name = `Provider-${priority}` + namePart; this._shouldBeInvoked = shouldBeInvoked; } + get name() { + return this._name; + } + get type() { + return UrlbarUtils.PROVIDER_TYPE.PROFILE; + } + isActive(context) { + return true; + } + getPriority(context) { + return this._priority; + } async startQuery(context, add) { Assert.ok(this._shouldBeInvoked, `${this.name} was invoked`); } + cancelQuery(context) {} + pickResult(result) {} } // Test all possible orderings of the providers to make sure the logic that diff --git a/browser/components/urlbar/tests/unit/xpcshell.ini b/browser/components/urlbar/tests/unit/xpcshell.ini index d9b6a3f349664..2f006f89b0f55 100644 --- a/browser/components/urlbar/tests/unit/xpcshell.ini +++ b/browser/components/urlbar/tests/unit/xpcshell.ini @@ -6,7 +6,6 @@ support-files = data/engine-tail-suggestions.xml [test_muxer.js] -[test_providerHeuristicFallback.js] [test_providerOmnibox.js] [test_providerOpenTabs.js] [test_providersManager.js] diff --git a/browser/modules/test/browser/browser_UsageTelemetry_urlbar_tip.js b/browser/modules/test/browser/browser_UsageTelemetry_urlbar_tip.js index 4aef2c892abb6..331bb540449b1 100644 --- a/browser/modules/test/browser/browser_UsageTelemetry_urlbar_tip.js +++ b/browser/modules/test/browser/browser_UsageTelemetry_urlbar_tip.js @@ -122,7 +122,7 @@ class TipProvider extends UrlbarProvider { this._results = results; } get name() { - return "TestProviderTip"; + return "TestTipProvider"; } get type() { return UrlbarUtils.PROVIDER_TYPE.PROFILE; diff --git a/toolkit/components/places/UnifiedComplete.jsm b/toolkit/components/places/UnifiedComplete.jsm index 410b0d89fe877..7151c61a38d18 100644 --- a/toolkit/components/places/UnifiedComplete.jsm +++ b/toolkit/components/places/UnifiedComplete.jsm @@ -340,6 +340,7 @@ XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]); XPCOMUtils.defineLazyModuleGetters(this, { AboutPagesUtils: "resource://gre/modules/AboutPagesUtils.jsm", BrowserUtils: "resource://gre/modules/BrowserUtils.jsm", + ExtensionSearchHandler: "resource://gre/modules/ExtensionSearchHandler.jsm", ObjectUtils: "resource://gre/modules/ObjectUtils.jsm", PlacesRemoteTabsAutocompleteProvider: "resource://gre/modules/PlacesRemoteTabsAutocompleteProvider.jsm", @@ -759,9 +760,9 @@ function Search( // actually the first thing in the search string. If a prefix or restriction // character occurs first, then the heurstic token is null. We use the // heuristic token to help determine the heuristic result. It may be a Places - // keyword, a search engine alias, or simply a URL or part of the search - // string the user has typed. We won't know until we create the heuristic - // result. + // keyword, a search engine alias, an extension keyword, or simply a URL or + // part of the search string the user has typed. We won't know until we + // create the heuristic result. let firstToken = !!this._searchTokens.length && this._searchTokens[0].value; this._heuristicToken = firstToken && this._trimmedOriginalSearchString.startsWith(firstToken) @@ -1271,6 +1272,16 @@ Search.prototype = { // We always try to make the first result a special "heuristic" result. The // heuristics below determine what type of result it will be, if any. + if (this._heuristicToken) { + // It may be a keyword registered by an extension. + let matched = await this._matchExtensionHeuristicResult( + this._heuristicToken + ); + if (matched) { + return true; + } + } + if (this.pending && this._enableActions && this._heuristicToken) { // It may be a search engine with an alias - which works like a keyword. let matched = await this._matchSearchEngineAlias(this._heuristicToken); @@ -1327,7 +1338,50 @@ Search.prototype = { } } - // Fall back to UrlbarProviderHeuristicFallback. + if (this.pending && this._searchTokens.length && this._enableActions) { + // If we don't have a result that matches what we know about, then + // we use a fallback for things we don't know about. + + // We may not have auto-filled, but this may still look like a URL. + // However, even if the input is a valid URL, we may not want to use + // it as such. This can happen if the host isn't using a known TLD + // and hasn't been added to a user-controlled list of known domains, + // and/or if it looks like an email address. + let matched = await this._matchUnknownUrl(); + if (matched) { + // Since we can't tell if this is a real URL and whether the user wants + // to visit or search for it, we provide an alternative searchengine + // match if the string looks like an alphanumeric origin or an e-mail. + let str = this._originalSearchString; + try { + new URL(str); + } catch (ex) { + if ( + UrlbarPrefs.get("keyword.enabled") && + (UrlbarTokenizer.looksLikeOrigin(str, { + noIp: true, + noPort: true, + }) || + UrlbarTokenizer.REGEXP_COMMON_EMAIL.test(str)) + ) { + this._addingHeuristicResult = false; + await this._matchCurrentSearchEngine(); + this._addingHeuristicResult = true; + } + } + return true; + } + } + + if (this.pending && this._enableActions && this._originalSearchString) { + // When all else fails, and the search string is non-empty, we search + // using the current search engine. + let matched = await this._matchCurrentSearchEngine(); + if (matched) { + return true; + } + } + return false; }, @@ -1358,6 +1412,18 @@ Search.prototype = { return gotResult; }, + _matchExtensionHeuristicResult(keyword) { + if ( + ExtensionSearchHandler.isKeywordRegistered(keyword) && + substringAfter(this._originalSearchString, keyword) + ) { + let description = ExtensionSearchHandler.getDescription(keyword); + this._addExtensionMatch(this._originalSearchString, description); + return true; + } + return false; + }, + async _matchPlacesKeyword(keyword) { let entry = await PlacesUtils.keywords.fetch(keyword); if (!entry) { @@ -1498,6 +1564,46 @@ Search.prototype = { return true; }, + async _matchCurrentSearchEngine() { + let engine; + if (this._engineName) { + engine = Services.search.getEngineByName(this._engineName); + } else if (this._inPrivateWindow) { + engine = Services.search.defaultPrivateEngine; + } else { + engine = Services.search.defaultEngine; + } + + if (!engine || !this.pending) { + return false; + } + + // Strip a leading search restriction char, because we prepend it to text + // when the search shortcut is used and it's not user typed. Don't strip + // other restriction chars, so that it's possible to search for things + // including one of those (e.g. "c#"). + let query = this._trimmedOriginalSearchString; + if (this._leadingRestrictionToken === UrlbarTokenizer.RESTRICT.SEARCH) { + query = substringAfter(query, this._leadingRestrictionToken).trim(); + } + this._addSearchEngineMatch({ engine, query }); + return true; + }, + + _addExtensionMatch(content, comment) { + this._addMatch({ + value: makeActionUrl("extension", { + content, + keyword: this._heuristicToken, + }), + comment, + icon: "chrome://browser/content/extension.svg", + style: "action extension", + frecency: Infinity, + type: UrlbarUtils.RESULT_GROUP.EXTENSION, + }); + }, + /** * Adds a search engine match. * @@ -1597,6 +1703,106 @@ Search.prototype = { } }, + // TODO (bug 1054814): Use visited URLs to inform which scheme to use, if the + // scheme isn't specificed. + _matchUnknownUrl() { + if (!this._searchString && this._strippedPrefix) { + // The user just typed a stripped protocol, don't build a non-sense url + // like http://http/ for it. + return false; + } + // The user may have typed something like "word?" to run a search, we should + // not convert that to a url. + if (this.hasBehavior("search") && this.hasBehavior("restrict")) { + return false; + } + let flags = + Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS | + Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP; + if (this._inPrivateWindow) { + flags |= Ci.nsIURIFixup.FIXUP_FLAG_PRIVATE_CONTEXT; + } + let fixupInfo = null; + let searchUrl = this._trimmedOriginalSearchString; + try { + fixupInfo = Services.uriFixup.getFixupURIInfo(searchUrl, flags); + } catch (e) { + if ( + e.result == Cr.NS_ERROR_MALFORMED_URI && + !UrlbarPrefs.get("keyword.enabled") + ) { + let value = makeActionUrl("visiturl", { + url: searchUrl, + input: searchUrl, + }); + this._addMatch({ + value, + comment: searchUrl, + style: "action visiturl", + frecency: Infinity, + }); + + return true; + } + return false; + } + + // If the URI cannot be fixed or the preferred URI would do a keyword search, + // that basically means this isn't useful to us. Note that + // fixupInfo.keywordAsSent will never be true if the keyword.enabled pref + // is false or there are no engines, so in that case we will always return + // a "visit". + if (!fixupInfo.fixedURI || fixupInfo.keywordAsSent) { + return false; + } + + let uri = fixupInfo.fixedURI; + // Check the host, as "http:///" is a valid nsIURI, but not useful to us. + // But, some schemes are expected to have no host. So we check just against + // schemes we know should have a host. This allows new schemes to be + // implemented without us accidentally blocking access to them. + let hostExpected = ["http", "https", "ftp", "chrome"].includes(uri.scheme); + if (hostExpected && !uri.host) { + return false; + } + + // getFixupURIInfo() escaped the URI, so it may not be pretty. Embed the + // escaped URL in the action URI since that URL should be "canonical". But + // pass the pretty, unescaped URL as the match comment, since it's likely + // to be displayed to the user, and in any case the front-end should not + // rely on it being canonical. + let escapedURL = uri.displaySpec; + let displayURL = Services.textToSubURI.unEscapeURIForUI(escapedURL); + + let value = makeActionUrl("visiturl", { + url: escapedURL, + input: searchUrl, + }); + + let match = { + value, + comment: displayURL, + style: "action visiturl", + frecency: Infinity, + }; + + // We don't know if this url is in Places or not, and checking that would + // be expensive. Thus we also don't know if we may have an icon. + // If we'd just try to fetch the icon for the typed string, we'd cause icon + // flicker, since the url keeps changing while the user types. + // By default we won't provide an icon, but for the subset of urls with a + // host we'll check for a typed slash and set favicon for the host part. + if ( + hostExpected && + (searchUrl.endsWith("/") || uri.pathQueryRef.length > 1) + ) { + match.icon = `page-icon:${uri.prePath}/`; + } + + this._addMatch(match); + return true; + }, + _onResultRow(row, cancel) { let queryType = row.getResultByIndex(QUERYINDEX_QUERYTYPE); switch (queryType) { diff --git a/toolkit/components/places/tests/unifiedcomplete/autofill_tasks.js b/toolkit/components/places/tests/unifiedcomplete/autofill_tasks.js index 088ccb6c8ce02..439ce5b26b04d 100644 --- a/toolkit/components/places/tests/unifiedcomplete/autofill_tasks.js +++ b/toolkit/components/places/tests/unifiedcomplete/autofill_tasks.js @@ -591,14 +591,15 @@ function addAutofillTasks(origins) { } // Enable actions. In the `origins` case, the failure to make an autofill - // match means UnifiedComplete should not create a heuristic result. In the - // `!origins` case, autofill should still happen since there's no threshold - // comparison. + // match should not interrupt creating another type of heuristic match, in + // this case a search (for "ex"). In the `!origins` case, autofill should + // still happen since there's no threshold comparison. if (origins) { await check_autocomplete({ search, searchParam: "enable-actions", matches: [ + makeSearchMatch(search, { style: ["heuristic"] }), { value: "https://not-" + url, comment: "test visit for https://not-" + url, diff --git a/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js b/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js index 56536a7102ddc..b65acfe1575a7 100644 --- a/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js +++ b/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js @@ -298,24 +298,20 @@ async function check_autocomplete(test) { if (matches.length) { let firstIndexToCheck = 0; if (test.searchParam && test.searchParam.includes("enable-actions")) { + firstIndexToCheck = 1; + info("Checking first match is first autocomplete entry"); let result = { value: controller.getValueAt(0), comment: controller.getCommentAt(0), style: controller.getStyleAt(0), image: controller.getImageAt(0), }; - // We only care about the positioning of the first result if it is - // heuristic. - if (result.style.includes("heuristic")) { - info("Checking first match is first autocomplete entry"); - info(`First match is "${result.value}", "${result.comment}"`); - Assert.ok( - await _check_autocomplete_matches(matches[0], result), - "first item is correct" - ); - info("Checking rest of the matches"); - firstIndexToCheck = 1; - } + info(`First match is "${result.value}", "${result.comment}"`); + Assert.ok( + await _check_autocomplete_matches(matches[0], result), + "first item is correct" + ); + info("Checking rest of the matches"); } for (let i = firstIndexToCheck; i < controller.matchCount; i++) { diff --git a/toolkit/components/places/tests/unifiedcomplete/test_empty_search.js b/toolkit/components/places/tests/unifiedcomplete/test_empty_search.js index 733a12dd3bcf7..9880f649e91dc 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_empty_search.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_empty_search.js @@ -40,6 +40,7 @@ add_task(async function test_javascript_match() { search: "foo", searchParam: "enable-actions", matches: [ + makeSearchMatch("foo", { heuristic: true }), { uri: uri1, title: "title" }, { uri: uri2, title: "title", style: ["bookmark"] }, { uri: uri3, title: "title" }, diff --git a/toolkit/components/places/tests/unifiedcomplete/test_keyword_search_actions.js b/toolkit/components/places/tests/unifiedcomplete/test_keyword_search_actions.js index 35be972a8a318..65382cc094632 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_keyword_search_actions.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_keyword_search_actions.js @@ -251,6 +251,18 @@ add_task(async function test_keyword_search() { ], }); + info("Bug 420328: no-param keyword with a param"); + await check_autocomplete({ + search: "noparam foo", + searchParam: "enable-actions", + matches: [makeSearchMatch("noparam foo", { heuristic: true })], + }); + await check_autocomplete({ + search: "post_noparam foo", + searchParam: "enable-actions", + matches: [makeSearchMatch("post_noparam foo", { heuristic: true })], + }); + info("escaping with default UTF-8 charset"); await check_autocomplete({ search: "encoded foƩ", diff --git a/toolkit/components/places/tests/unifiedcomplete/test_remote_tab_matches.js b/toolkit/components/places/tests/unifiedcomplete/test_remote_tab_matches.js index c1575065383b3..59a061199f752 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_remote_tab_matches.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_remote_tab_matches.js @@ -66,6 +66,28 @@ function makeRemoteTabMatch(url, deviceName, extra = {}) { }; } +// The tests. +add_task(async function test_nomatch() { + // Nothing matches. + configureEngine({ + guid_desktop: { + id: "desktop", + tabs: [ + { + urlHistory: ["http://foo.com/"], + }, + ], + }, + }); + + // No remote tabs match here, so we only expect search results. + await check_autocomplete({ + search: "ex", + searchParam: "enable-actions", + matches: [makeSearchMatch("ex", { heuristic: true })], + }); +}); + add_task(async function test_minimal() { // The minimal client and tabs info we can get away with. configureEngine({ @@ -82,7 +104,10 @@ add_task(async function test_minimal() { await check_autocomplete({ search: "ex", searchParam: "enable-actions", - matches: [makeRemoteTabMatch("http://example.com/", "My Desktop")], + matches: [ + makeSearchMatch("ex", { heuristic: true }), + makeRemoteTabMatch("http://example.com/", "My Desktop"), + ], }); }); @@ -105,6 +130,7 @@ add_task(async function test_maximal() { search: "ex", searchParam: "enable-actions", matches: [ + makeSearchMatch("ex", { heuristic: true }), makeRemoteTabMatch("http://example.com/", "My Phone", { title: "An Example", icon: "moz-anno:favicon:http://favicon/", @@ -132,6 +158,7 @@ add_task(async function test_noShowIcons() { search: "ex", searchParam: "enable-actions", matches: [ + makeSearchMatch("ex", { heuristic: true }), makeRemoteTabMatch("http://example.com/", "My Phone", { title: "An Example", // expecting the default favicon due to that pref. @@ -160,7 +187,7 @@ add_task(async function test_dontMatchSyncedTabs() { await check_autocomplete({ search: "ex", searchParam: "enable-actions", - matches: [], + matches: [makeSearchMatch("ex", { heuristic: true })], }); Services.prefs.clearUserPref("services.sync.syncedTabs.showRemoteTabs"); }); @@ -183,6 +210,7 @@ add_task(async function test_matches_title() { search: "ex", searchParam: "enable-actions", matches: [ + makeSearchMatch("ex", { heuristic: true }), makeRemoteTabMatch("http://foo.com/", "My Phone", { title: "An Example", }), @@ -215,7 +243,10 @@ add_task(async function test_localtab_matches_override() { await check_autocomplete({ search: "ex", searchParam: "enable-actions", - matches: [makeSwitchToTabMatch("http://foo.com/", { title: "An Example" })], + matches: [ + makeSearchMatch("ex", { heuristic: true }), + makeSwitchToTabMatch("http://foo.com/", { title: "An Example" }), + ], }); await removeOpenPages(uri, 1); }); @@ -244,6 +275,7 @@ add_task(async function test_remotetab_matches_override() { search: "rem", searchParam: "enable-actions", matches: [ + makeSearchMatch("rem", { heuristic: true }), makeRemoteTabMatch("http://foo.remote.com/", "My Phone", { title: "An Example", }), @@ -280,6 +312,7 @@ add_task(async function test_many_remotetab_matches() { searchParam: "enable-actions", checkSorting: true, matches: [ + makeSearchMatch("rem", { heuristic: true }), makeRemoteTabMatch("http://foo.remote.com/0", "My Phone", { title: "A title", }), @@ -328,12 +361,15 @@ add_task(async function test_maxResults() { }, }); - // Set maxResults to 4 in our search. + // Set maxResults to 5 in our search. 5 results total should be returned: the + // heuristic followed by ceil(maxResults / 2) remote tabs, then two more + // remote tabs to round out the number of results to 5. await check_autocomplete({ search: "rem", - searchParam: "enable-actions max-results:4", + searchParam: "enable-actions max-results:5", checkSorting: true, matches: [ + makeSearchMatch("rem", { heuristic: true }), makeRemoteTabMatch("http://foo.remote.com/0", "My Phone", { title: "A title", }), @@ -373,14 +409,15 @@ add_task(async function test_restrictionCharacter() { await PlacesTestUtils.addVisits([{ uri, title: "An Example" }]); await addOpenPages(uri, 1); - // Set maxResults to 7 in our search. 7 results should be returned: - // ceil(maxResults / 2) remote tabs, then the open tab, then 2 more remote tab - // results to get to 7 total. + // Set maxResults to 8 in our search. 8 results should be returned: the + // heuristic followed by (maxResults / 2) remote tabs, then the open tab, then + // 2 more remote tab results to get to 8 total. await check_autocomplete({ search: UrlbarTokenizer.RESTRICT.OPENPAGE, - searchParam: "enable-actions max-results:7", + searchParam: "enable-actions max-results:8", checkSorting: true, matches: [ + makeSearchMatch(UrlbarTokenizer.RESTRICT.OPENPAGE, { heuristic: true }), makeRemoteTabMatch("http://foo.remote.com/0", "My Phone", { title: "A title", }), diff --git a/toolkit/components/places/tests/unifiedcomplete/test_restrict_searchstring.js b/toolkit/components/places/tests/unifiedcomplete/test_restrict_searchstring.js new file mode 100644 index 0000000000000..ea97021b62259 --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_restrict_searchstring.js @@ -0,0 +1,32 @@ +/* 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/. */ + +/** + * Test that restriction tokens are not removed from the search string, apart + * from a leading search restriction token. + */ + +add_task(async function test_searchstring() { + for (let token of Object.values(UrlbarTokenizer.RESTRICT)) { + for (let search of [`${token} query`, `query ${token}`]) { + let searchQuery = + token == UrlbarTokenizer.RESTRICT.SEARCH && + search.startsWith(UrlbarTokenizer.RESTRICT.SEARCH) + ? search.substring(2) + : search; + info(`Searching for "${search}", expecting "${searchQuery}"`); + await check_autocomplete({ + search, + searchParam: "enable-actions", + matches: [ + makeSearchMatch(search, { + engineName: "MozSearch", + searchQuery, + heuristic: true, + }), + ], + }); + } + } +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_search_engine_alias.js b/toolkit/components/places/tests/unifiedcomplete/test_search_engine_alias.js index 4dc5ec6da56b1..9887c4676eea4 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_search_engine_alias.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_search_engine_alias.js @@ -111,6 +111,30 @@ add_task(async function basicGetAndPost() { }), ], }); + + // When a restriction token is used before the alias, the alias should *not* + // be recognized. It should be treated as part of the search string. Try + // all the restriction tokens to test that. We should get a single "search + // with" heuristic result without an alias. + for (let token of Object.values(UrlbarTokenizer.RESTRICT)) { + let search = `${token} ${alias} query string`; + let searchQuery = + token == UrlbarTokenizer.RESTRICT.SEARCH && + search.startsWith(UrlbarTokenizer.RESTRICT.SEARCH) + ? search.substring(2) + : search; + await check_autocomplete({ + search, + searchParam: "enable-actions", + matches: [ + makeSearchMatch(search, { + engineName: "MozSearch", + searchQuery, + heuristic: true, + }), + ], + }); + } } await cleanup(); }); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_search_engine_default.js b/toolkit/components/places/tests/unifiedcomplete/test_search_engine_default.js new file mode 100644 index 0000000000000..81d1b6e7accb3 --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_search_engine_default.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function() { + // Note that head_autocomplete.js has already added a MozSearch engine. + // Here we add another engine with a search alias. + await Services.search.addEngineWithDetails("AliasedMozSearch", { + alias: "doit", + method: "GET", + template: "http://s.example.com/search", + }); + + info("search engine"); + await check_autocomplete({ + search: "mozilla", + searchParam: "enable-actions", + matches: [makeSearchMatch("mozilla", { heuristic: true })], + }); + + info("search engine, uri-like input"); + await check_autocomplete({ + search: "http:///", + searchParam: "enable-actions", + matches: [makeSearchMatch("http:///", { heuristic: true })], + }); + + info("search engine, multiple words"); + await check_autocomplete({ + search: "mozzarella cheese", + searchParam: "enable-actions", + matches: [makeSearchMatch("mozzarella cheese", { heuristic: true })], + }); + + info("search engine, after current engine has changed"); + await Services.search.addEngineWithDetails("MozSearch2", { + method: "GET", + template: "http://s.example.com/search2", + }); + let engine = Services.search.getEngineByName("MozSearch2"); + notEqual( + Services.search.defaultEngine, + engine, + "New engine shouldn't be the current engine yet" + ); + await Services.search.setDefault(engine); + await check_autocomplete({ + search: "mozilla", + searchParam: "enable-actions", + matches: [ + makeSearchMatch("mozilla", { engineName: "MozSearch2", heuristic: true }), + ], + }); + + await cleanup(); +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_tab_matches.js b/toolkit/components/places/tests/unifiedcomplete/test_tab_matches.js index 5cb4d790b1e13..42c3b566a8665 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_tab_matches.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_tab_matches.js @@ -25,11 +25,15 @@ add_task(async function test_tab_matches() { await addOpenPages(uri3, 1); await addOpenPages(uri4, 1); - info("basic tab match"); + info("two results, normal result is a tab match"); await check_autocomplete({ search: "abc.com", searchParam: "enable-actions", - matches: [makeSwitchToTabMatch("http://abc.com/", { title: "ABC rocks" })], + matches: [ + makeVisitMatch("abc.com", "http://abc.com/", { heuristic: true }), + makeSwitchToTabMatch("http://abc.com/", { title: "ABC rocks" }), + makeSearchMatch("abc.com", { heuristic: false }), + ], }); info("three results, one tab match"); @@ -37,6 +41,7 @@ add_task(async function test_tab_matches() { search: "abc", searchParam: "enable-actions", matches: [ + makeSearchMatch("abc", { heuristic: true }), makeSwitchToTabMatch("http://abc.com/", { title: "ABC rocks" }), { uri: uri2, @@ -57,6 +62,7 @@ add_task(async function test_tab_matches() { search: "abc", searchParam: "enable-actions", matches: [ + makeSearchMatch("abc", { heuristic: true }), makeSwitchToTabMatch("http://abc.com/", { title: "ABC rocks" }), makeSwitchToTabMatch("http://xyz.net/", { title: "xyz.net - we're better than ABC", @@ -75,6 +81,7 @@ add_task(async function test_tab_matches() { search: "abc", searchParam: "enable-actions", matches: [ + makeSearchMatch("abc", { heuristic: true }), makeSwitchToTabMatch("http://abc.com/", { title: "ABC rocks" }), makeSwitchToTabMatch("http://xyz.net/", { title: "xyz.net - we're better than ABC", @@ -94,6 +101,7 @@ add_task(async function test_tab_matches() { search: "abc", searchParam: "enable-actions user-context-id:3", matches: [ + makeSearchMatch("abc", { heuristic: true }), makeSwitchToTabMatch("http://foobar.org/", { title: "foobar.org - much better than ABC, definitely better than XYZ", }), @@ -111,6 +119,7 @@ add_task(async function test_tab_matches() { search: "abc", searchParam: "enable-actions user-context-id:2", matches: [ + makeSearchMatch("abc", { heuristic: true }), { uri: uri1, title: "ABC rocks", style: ["favicon"] }, { uri: uri2, @@ -133,6 +142,7 @@ add_task(async function test_tab_matches() { search: "abc", searchParam: "enable-actions", matches: [ + makeSearchMatch("abc", { heuristic: true }), makeSwitchToTabMatch("http://abc.com/", { title: "ABC rocks" }), makeSwitchToTabMatch("http://xyz.net/", { title: "xyz.net - we're better than ABC", @@ -150,6 +160,7 @@ add_task(async function test_tab_matches() { search: "abc", searchParam: "enable-actions disable-private-actions", matches: [ + makeSearchMatch("abc", { heuristic: true }), { uri: uri1, title: "ABC rocks", style: ["favicon"] }, { uri: uri2, @@ -184,12 +195,13 @@ add_task(async function test_tab_matches() { }); info("three results, no tab matches"); - await removeOpenPages(uri1, 1); - await removeOpenPages(uri2, 6); + removeOpenPages(uri1, 1); + removeOpenPages(uri2, 6); await check_autocomplete({ search: "abc", searchParam: "enable-actions", matches: [ + makeSearchMatch("abc", { heuristic: true }), { uri: uri1, title: "ABC rocks", style: ["favicon"] }, { uri: uri2, @@ -209,28 +221,46 @@ add_task(async function test_tab_matches() { await check_autocomplete({ search: UrlbarTokenizer.RESTRICT.OPENPAGE + " abc", searchParam: "enable-actions", - matches: [makeSwitchToTabMatch("http://abc.com/", { title: "ABC rocks" })], + matches: [ + makeSearchMatch(UrlbarTokenizer.RESTRICT.OPENPAGE + " abc", { + heuristic: true, + searchQuery: UrlbarTokenizer.RESTRICT.OPENPAGE + " abc", + }), + makeSwitchToTabMatch("http://abc.com/", { title: "ABC rocks" }), + ], }); info("tab match with not-addable pages"); await check_autocomplete({ search: "mozilla", searchParam: "enable-actions", - matches: [makeSwitchToTabMatch("about:mozilla")], + matches: [ + makeSearchMatch("mozilla", { heuristic: true }), + makeSwitchToTabMatch("about:mozilla"), + ], }); info("tab match with not-addable pages, no boundary search"); await check_autocomplete({ search: "ut:mo", searchParam: "enable-actions", - matches: [makeSwitchToTabMatch("about:mozilla")], + matches: [ + makeSearchMatch("ut:mo", { heuristic: true }), + makeSwitchToTabMatch("about:mozilla"), + ], }); info("tab match with not-addable pages and restriction character"); await check_autocomplete({ search: UrlbarTokenizer.RESTRICT.OPENPAGE + " mozilla", searchParam: "enable-actions", - matches: [makeSwitchToTabMatch("about:mozilla")], + matches: [ + makeSearchMatch(UrlbarTokenizer.RESTRICT.OPENPAGE + " mozilla", { + heuristic: true, + searchQuery: UrlbarTokenizer.RESTRICT.OPENPAGE + " mozilla", + }), + makeSwitchToTabMatch("about:mozilla"), + ], }); info("tab match with not-addable pages and only restriction character"); @@ -238,6 +268,7 @@ add_task(async function test_tab_matches() { search: UrlbarTokenizer.RESTRICT.OPENPAGE, searchParam: "enable-actions", matches: [ + makeSearchMatch(UrlbarTokenizer.RESTRICT.OPENPAGE, { heuristic: true }), makeSwitchToTabMatch("http://abc.com/", { title: "ABC rocks" }), makeSwitchToTabMatch("about:mozilla"), makeSwitchToTabMatch("data:text/html,test"), @@ -255,7 +286,11 @@ add_task(async function test_tab_matches() { await check_autocomplete({ search: "abc.com", searchParam: "enable-actions", - matches: [makeSwitchToTabMatch("http://abc.com/", { title: "ABC rocks" })], + matches: [ + makeVisitMatch("abc.com", "http://abc.com/", { heuristic: true }), + makeSwitchToTabMatch("http://abc.com/", { title: "ABC rocks" }), + makeSearchMatch("abc.com", { heuristic: false }), + ], }); await PlacesUtils.bookmarks.remove(bm); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_visit_url.js b/toolkit/components/places/tests/unifiedcomplete/test_visit_url.js new file mode 100644 index 0000000000000..05ba806f8610f --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_visit_url.js @@ -0,0 +1,350 @@ +add_task(async function() { + info("visit url, no protocol"); + await check_autocomplete({ + search: "mozilla.org", + searchParam: "enable-actions", + matches: [ + { + uri: makeActionURI("visiturl", { + url: "http://mozilla.org/", + input: "mozilla.org", + }), + title: "http://mozilla.org/", + style: ["action", "visiturl", "heuristic"], + }, + { + uri: makeActionURI("searchengine", { + engineName: "MozSearch", + input: "mozilla.org", + searchQuery: "mozilla.org", + }), + title: "MozSearch", + style: ["action", "searchengine"], + }, + ], + }); + + info("visit url, no protocol but with 2 dots"); + await check_autocomplete({ + search: "www.mozilla.org", + searchParam: "enable-actions", + matches: [ + { + uri: makeActionURI("visiturl", { + url: "http://www.mozilla.org/", + input: "www.mozilla.org", + }), + title: "http://www.mozilla.org/", + style: ["action", "visiturl", "heuristic"], + }, + { + uri: makeActionURI("searchengine", { + engineName: "MozSearch", + input: "www.mozilla.org", + searchQuery: "www.mozilla.org", + }), + title: "MozSearch", + style: ["action", "searchengine"], + }, + ], + }); + + info("visit url, no protocol, e-mail like"); + await check_autocomplete({ + search: "a@b.com", + searchParam: "enable-actions", + matches: [ + { + uri: makeActionURI("visiturl", { + url: "http://a@b.com/", + input: "a@b.com", + }), + title: "http://a@b.com/", + style: ["action", "visiturl", "heuristic"], + }, + { + uri: makeActionURI("searchengine", { + engineName: "MozSearch", + input: "a@b.com", + searchQuery: "a@b.com", + }), + title: "MozSearch", + style: ["action", "searchengine"], + }, + ], + }); + + info("visit url, with protocol but with 2 dots"); + await check_autocomplete({ + search: "https://www.mozilla.org", + searchParam: "enable-actions", + matches: [ + { + uri: makeActionURI("visiturl", { + url: "https://www.mozilla.org/", + input: "https://www.mozilla.org", + }), + title: "https://www.mozilla.org/", + style: ["action", "visiturl", "heuristic"], + }, + ], + }); + + info("visit url, with protocol but with 3 dots"); + await check_autocomplete({ + search: "https://www.mozilla.org.tw", + searchParam: "enable-actions", + matches: [ + { + uri: makeActionURI("visiturl", { + url: "https://www.mozilla.org.tw/", + input: "https://www.mozilla.org.tw", + }), + title: "https://www.mozilla.org.tw/", + style: ["action", "visiturl", "heuristic"], + }, + ], + }); + + info("visit url, with protocol"); + await check_autocomplete({ + search: "https://mozilla.org", + searchParam: "enable-actions", + matches: [ + { + uri: makeActionURI("visiturl", { + url: "https://mozilla.org/", + input: "https://mozilla.org", + }), + title: "https://mozilla.org/", + style: ["action", "visiturl", "heuristic"], + }, + ], + }); + + info("visit url, about: protocol (no host)"); + await check_autocomplete({ + search: "about:nonexistent", + searchParam: "enable-actions", + matches: [ + { + uri: makeActionURI("visiturl", { + url: "about:nonexistent", + input: "about:nonexistent", + }), + title: "about:nonexistent", + style: ["action", "visiturl", "heuristic"], + }, + ], + }); + + info("visit url, with non-standard whitespace"); + await check_autocomplete({ + search: "https://www.mozilla.org\u2028", + searchParam: "enable-actions", + matches: [ + { + uri: makeActionURI("visiturl", { + url: "https://www.mozilla.org/", + input: "https://www.mozilla.org", + }), + title: "https://www.mozilla.org/", + style: ["action", "visiturl", "heuristic"], + }, + ], + }); + + // This is distinct because of how we predict being able to url autofill via + // host lookups. + info("visit url, host matching visited host but not visited url"); + await PlacesTestUtils.addVisits([ + { + uri: NetUtil.newURI("http://mozilla.org/wine/"), + title: "Mozilla Wine", + transition: TRANSITION_TYPED, + }, + ]); + await check_autocomplete({ + search: "mozilla.org/rum", + searchParam: "enable-actions", + matches: [ + makeVisitMatch("mozilla.org/rum", "http://mozilla.org/rum", { + heuristic: true, + }), + ], + }); + + // And hosts with no dot in them are special, due to requiring whitelisting. + info("non-whitelisted host"); + await check_autocomplete({ + search: "firefox", + searchParam: "enable-actions", + matches: [makeSearchMatch("firefox", { heuristic: true })], + }); + + info("string with non-whitelisted host"); + if (Services.prefs.getBoolPref("browser.fixup.defaultToSearch", true)) { + await check_autocomplete({ + search: "firefox/get", + searchParam: "enable-actions", + matches: [makeSearchMatch("firefox/get", { heuristic: true })], + }); + } else { + await check_autocomplete({ + search: "firefox/get", + searchParam: "enable-actions", + matches: [ + makeVisitMatch("firefox/get", "http://firefox/get", { + heuristic: true, + }), + ], + }); + } + + Services.prefs.setBoolPref("browser.fixup.domainwhitelist.firefox", true); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.fixup.domainwhitelist.firefox"); + }); + + info("whitelisted host"); + await check_autocomplete({ + search: "firefox", + searchParam: "enable-actions", + matches: [ + makeVisitMatch("firefox", "http://firefox/", { heuristic: true }), + makeSearchMatch("firefox", { heuristic: false }), + ], + }); + + info("url with whitelisted host"); + await check_autocomplete({ + search: "firefox/get", + searchParam: "enable-actions", + matches: [ + makeVisitMatch("firefox/get", "http://firefox/get", { heuristic: true }), + ], + }); + + info( + "visit url, host matching visited host but not visited url, whitelisted host" + ); + Services.prefs.setBoolPref("browser.fixup.domainwhitelist.mozilla", true); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.fixup.domainwhitelist.mozilla"); + }); + await check_autocomplete({ + search: "mozilla/rum", + searchParam: "enable-actions", + matches: [ + makeVisitMatch("mozilla/rum", "http://mozilla/rum", { heuristic: true }), + ], + }); + + // ipv4 and ipv6 literal addresses should offer to visit. + info("visit url, ipv4 literal"); + await check_autocomplete({ + search: "127.0.0.1", + searchParam: "enable-actions", + matches: [ + makeVisitMatch("127.0.0.1", "http://127.0.0.1/", { heuristic: true }), + ], + }); + + info("visit url, ipv6 literal"); + await check_autocomplete({ + search: "[2001:db8::1]", + searchParam: "enable-actions", + matches: [ + makeVisitMatch("[2001:db8::1]", "http://[2001:db8::1]/", { + heuristic: true, + }), + ], + }); + + // Setting keyword.enabled to false should always try to visit. + let keywordEnabled = Services.prefs.getBoolPref("keyword.enabled"); + Services.prefs.setBoolPref("keyword.enabled", false); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("keyword.enabled"); + }); + info("visit url, keyword.enabled = false"); + await check_autocomplete({ + search: "bacon", + searchParam: "enable-actions", + matches: [makeVisitMatch("bacon", "http://bacon/", { heuristic: true })], + }); + info("visit two word query, keyword.enabled = false"); + await check_autocomplete({ + search: "bacon lovers", + searchParam: "enable-actions", + matches: [ + makeVisitMatch("bacon lovers", "bacon lovers", { heuristic: true }), + ], + }); + Services.prefs.setBoolPref("keyword.enabled", keywordEnabled); + + info("visit url, scheme+host"); + await check_autocomplete({ + search: "http://example", + searchParam: "enable-actions", + matches: [ + makeVisitMatch("http://example", "http://example/", { heuristic: true }), + ], + }); + + info("visit url, scheme+host"); + await check_autocomplete({ + search: "ftp://example", + searchParam: "enable-actions", + matches: [ + makeVisitMatch("ftp://example", "ftp://example/", { heuristic: true }), + ], + }); + + info("visit url, host+port"); + await check_autocomplete({ + search: "example:8080", + searchParam: "enable-actions", + matches: [ + makeVisitMatch("example:8080", "http://example:8080/", { + heuristic: true, + }), + ], + }); + + info("numerical operations that look like urls should search"); + await check_autocomplete({ + search: "123/12", + searchParam: "enable-actions", + matches: [makeSearchMatch("123/12", { heuristic: true })], + }); + + info("numerical operations that look like urls should search"); + await check_autocomplete({ + search: "123.12/12.1", + searchParam: "enable-actions", + matches: [makeSearchMatch("123.12/12.1", { heuristic: true })], + }); + + info("access resource:///modules"); + await check_autocomplete({ + search: "resource:///modules", + searchParam: "enable-actions", + matches: [ + makeVisitMatch("resource:///modules", "resource:///modules", { + heuristic: true, + }), + ], + }); + + info("access resource://app/modules"); + await check_autocomplete({ + search: "resource://app/modules", + searchParam: "enable-actions", + matches: [ + makeVisitMatch("resource://app/modules", "resource://app/modules", { + heuristic: true, + }), + ], + }); +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/xpcshell.ini b/toolkit/components/places/tests/unifiedcomplete/xpcshell.ini index 992dccb38a7c9..72de101233e1e 100644 --- a/toolkit/components/places/tests/unifiedcomplete/xpcshell.ini +++ b/toolkit/components/places/tests/unifiedcomplete/xpcshell.ini @@ -40,11 +40,14 @@ support-files = [test_query_url.js] [test_remote_tab_matches.js] skip-if = !sync +[test_restrict_searchstring.js] [test_search_engine_alias.js] +[test_search_engine_default.js] [test_search_engine_host.js] [test_search_engine_restyle.js] [test_special_search.js] [test_swap_protocol.js] [test_tab_matches.js] [test_trimming.js] +[test_visit_url.js] [test_word_boundary_search.js] diff --git a/tools/lint/rejected-words.yml b/tools/lint/rejected-words.yml index e45ff25f9e534..fcdba559c3748 100644 --- a/tools/lint/rejected-words.yml +++ b/tools/lint/rejected-words.yml @@ -69,7 +69,6 @@ avoid-blacklist-and-whitelist: - browser/components/uitour/UITourChild.jsm - browser/components/urlbar/tests/browser/browser_searchSingleWordNotification.js - browser/components/urlbar/tests/browser/browser_UrlbarInput_trimURLs.js - - browser/components/urlbar/tests/unit/test_providerHeuristicFallback.js - browser/components/urlbar/tests/unit/test_search_suggestions.js - browser/components/urlbar/tests/unit/test_tokenizer.js - browser/extensions/formautofill/FormAutofillSync.jsm @@ -358,6 +357,7 @@ avoid-blacklist-and-whitelist: - toolkit/components/crashes/tests/xpcshell/test_crash_manager.js - toolkit/components/extensions/Extension.jsm - toolkit/components/extensions/test/xpcshell/test_WebExtensionPolicy.js + - toolkit/components/places/tests/unifiedcomplete/test_visit_url.js - toolkit/components/remotepagemanager/RemotePageManagerParent.jsm - toolkit/components/reputationservice/ApplicationReputation.cpp - toolkit/components/reputationservice/chromium/chrome/common/safe_browsing/csd.pb.h