UrlbarProviderHeuristicFallback.sys.mjs (11408B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 /** 6 * This module exports a provider that provides a heuristic result. The result 7 * either vists a URL or does a search with the current engine. This result is 8 * always the ultimate fallback for any query, so this provider is always active. 9 */ 10 11 import { 12 UrlbarProvider, 13 UrlbarUtils, 14 } from "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs"; 15 16 const lazy = {}; 17 18 ChromeUtils.defineESModuleGetters(lazy, { 19 UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs", 20 UrlbarResult: "moz-src:///browser/components/urlbar/UrlbarResult.sys.mjs", 21 UrlbarSearchUtils: 22 "moz-src:///browser/components/urlbar/UrlbarSearchUtils.sys.mjs", 23 UrlbarTokenizer: 24 "moz-src:///browser/components/urlbar/UrlbarTokenizer.sys.mjs", 25 UrlUtils: "resource://gre/modules/UrlUtils.sys.mjs", 26 }); 27 28 /** 29 * Class used to create the provider. 30 */ 31 export class UrlbarProviderHeuristicFallback extends UrlbarProvider { 32 constructor() { 33 super(); 34 } 35 36 /** 37 * @returns {Values<typeof UrlbarUtils.PROVIDER_TYPE>} 38 */ 39 get type() { 40 return UrlbarUtils.PROVIDER_TYPE.HEURISTIC; 41 } 42 43 /** 44 * Whether this provider should be invoked for the given context. 45 * If this method returns false, the providers manager won't start a query 46 * with this provider, to save on resources. 47 * 48 * @param {UrlbarQueryContext} queryContext 49 */ 50 async isActive(queryContext) { 51 return !!queryContext.searchString.length; 52 } 53 54 /** 55 * Gets the provider's priority. 56 * 57 * @returns {number} The provider's priority for the given query. 58 */ 59 getPriority() { 60 return 0; 61 } 62 63 /** 64 * Starts querying. 65 * 66 * @param {UrlbarQueryContext} queryContext 67 * @param {(provider: UrlbarProvider, result: UrlbarResult) => void} addCallback 68 * Callback invoked by the provider to add a new result. 69 */ 70 async startQuery(queryContext, addCallback) { 71 let instance = this.queryInstance; 72 73 if (queryContext.sapName != "searchbar") { 74 let result = this._matchUnknownUrl(queryContext); 75 if (result) { 76 addCallback(this, result); 77 // Since we can't tell if this is a real URL and whether the user wants 78 // to visit or search for it, we provide an alternative searchengine 79 // match if the string looks like an alphanumeric origin or an e-mail. 80 let str = queryContext.searchString; 81 if (!URL.canParse(str)) { 82 if ( 83 lazy.UrlbarPrefs.get("keyword.enabled") && 84 (lazy.UrlUtils.looksLikeOrigin(str, { 85 noIp: true, 86 noPort: true, 87 }) || 88 lazy.UrlUtils.REGEXP_COMMON_EMAIL.test(str)) 89 ) { 90 let searchResult = await this._engineSearchResult({ queryContext }); 91 if (instance != this.queryInstance) { 92 return; 93 } 94 addCallback(this, searchResult); 95 } 96 } 97 return; 98 } 99 } 100 101 let result = await this._searchModeKeywordResult(queryContext); 102 if (instance != this.queryInstance) { 103 return; 104 } 105 if (result) { 106 addCallback(this, result); 107 return; 108 } 109 110 if ( 111 queryContext.sapName == "searchbar" || 112 lazy.UrlbarPrefs.get("keyword.enabled") || 113 queryContext.restrictSource == UrlbarUtils.RESULT_SOURCE.SEARCH || 114 queryContext.searchMode 115 ) { 116 result = await this._engineSearchResult({ 117 queryContext, 118 heuristic: true, 119 }); 120 if (instance != this.queryInstance) { 121 return; 122 } 123 if (result) { 124 addCallback(this, result); 125 } 126 } 127 } 128 129 // TODO (bug 1054814): Use visited URLs to inform which scheme to use, if the 130 // scheme isn't specificed. 131 _matchUnknownUrl(queryContext) { 132 // The user may have typed something like "word?" to run a search. We 133 // should not convert that to a URL. We should also never convert actual 134 // URLs into URL results when search mode is active or a search mode 135 // restriction token was typed. 136 if ( 137 queryContext.restrictSource == UrlbarUtils.RESULT_SOURCE.SEARCH || 138 lazy.UrlbarTokenizer.SEARCH_MODE_RESTRICT.has( 139 queryContext.restrictToken?.value 140 ) || 141 queryContext.searchMode 142 ) { 143 return null; 144 } 145 146 let unescapedSearchString = UrlbarUtils.unEscapeURIForUI( 147 queryContext.searchString 148 ); 149 let [prefix, suffix] = UrlbarUtils.stripURLPrefix(unescapedSearchString); 150 if (!suffix && prefix) { 151 // The user just typed a stripped protocol, don't build a non-sense url 152 // like http://http/ for it. 153 return null; 154 } 155 156 let searchUrl = queryContext.trimmedSearchString; 157 158 if (queryContext.fixupError) { 159 if ( 160 queryContext.fixupError == Cr.NS_ERROR_MALFORMED_URI && 161 !lazy.UrlbarPrefs.get("keyword.enabled") 162 ) { 163 return new lazy.UrlbarResult({ 164 type: UrlbarUtils.RESULT_TYPE.URL, 165 source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, 166 heuristic: true, 167 payload: { 168 title: searchUrl, 169 url: searchUrl, 170 }, 171 }); 172 } 173 174 return null; 175 } 176 177 // If the URI cannot be fixed or the preferred URI would do a keyword search, 178 // that basically means this isn't useful to us. Note that 179 // fixupInfo.keywordAsSent will never be true if the keyword.enabled pref 180 // is false or there are no engines, so in that case we will always return 181 // a "visit". 182 if (!queryContext.fixupInfo?.href || queryContext.fixupInfo?.isSearch) { 183 return null; 184 } 185 186 let uri = new URL(queryContext.fixupInfo.href); 187 // Check the host, as "http:///" is a valid nsIURI, but not useful to us. 188 // But, some schemes are expected to have no host. So we check just against 189 // schemes we know should have a host. This allows new schemes to be 190 // implemented without us accidentally blocking access to them. 191 let hostExpected = ["http:", "https:", "ftp:", "chrome:"].includes( 192 uri.protocol 193 ); 194 if (hostExpected && !uri.host) { 195 return null; 196 } 197 198 // getFixupURIInfo() escaped the URI, so it may not be pretty. Embed the 199 // escaped URL in the result since that URL should be "canonical". But 200 // pass the pretty, unescaped URL as the result's title, since it is 201 // displayed to the user. 202 let escapedURL = uri.toString(); 203 let displayURL = UrlbarUtils.prepareUrlForDisplay(uri, { 204 trimURL: false, 205 // If the user didn't type a protocol, and we added one, don't show it, 206 // as https-first may upgrade it, potentially breaking expectations. 207 schemeless: !prefix, 208 }); 209 210 // We don't know if this url is in Places or not, and checking that would 211 // be expensive. Thus we also don't know if we may have an icon. 212 // If we'd just try to fetch the icon for the typed string, we'd cause icon 213 // flicker, since the url keeps changing while the user types. 214 // By default we won't provide an icon, but for the subset of urls with a 215 // host we'll check for a typed slash and set favicon for the host part. 216 let iconUri; 217 if (hostExpected && (searchUrl.endsWith("/") || uri.pathname.length > 1)) { 218 // Look for an icon with the entire URL except for the pathname, including 219 // scheme, usernames, passwords, hostname, and port. 220 let pathIndex = uri.toString().lastIndexOf(uri.pathname); 221 let prePath = uri.toString().slice(0, pathIndex); 222 iconUri = `page-icon:${prePath}/`; 223 } 224 225 return new lazy.UrlbarResult({ 226 type: UrlbarUtils.RESULT_TYPE.URL, 227 source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, 228 heuristic: true, 229 payload: { 230 title: displayURL, 231 url: escapedURL, 232 icon: iconUri, 233 }, 234 }); 235 } 236 237 async _searchModeKeywordResult(queryContext) { 238 if (!queryContext.tokens.length || queryContext.sapName != "urlbar") { 239 return null; 240 } 241 242 let firstToken = queryContext.tokens[0].value; 243 if (!lazy.UrlbarTokenizer.SEARCH_MODE_RESTRICT.has(firstToken)) { 244 return null; 245 } 246 247 // At this point, the search string starts with a token that can be 248 // converted into search mode. 249 // Now we need to determine what to do based on the remainder of the search 250 // string. If the remainder starts with a space, then we should enter 251 // search mode, so we should continue below and create the result. 252 // Otherwise, we should not enter search mode, and in that case, the search 253 // string will look like one of the following: 254 // 255 // * The search string ends with the restriction token (e.g., the user 256 // has typed only the token by itself, with no trailing spaces). 257 // * More tokens exist, but there's no space between the restriction 258 // token and the following token. This is possible because the tokenizer 259 // does not require spaces between a restriction token and the remainder 260 // of the search string. In this case, we should not enter search mode. 261 // 262 // If we return null here and thereby do not enter search mode, then we'll 263 // continue on to _engineSearchResult, and the heuristic will be a 264 // default engine search result. 265 let query = UrlbarUtils.substringAfter( 266 queryContext.searchString, 267 firstToken 268 ); 269 if (!lazy.UrlUtils.REGEXP_SPACES_START.test(query)) { 270 return null; 271 } 272 273 if (queryContext.restrictSource == UrlbarUtils.RESULT_SOURCE.SEARCH) { 274 return await this._engineSearchResult({ 275 queryContext, 276 keyword: firstToken, 277 heuristic: true, 278 }); 279 } 280 281 query = query.trimStart(); 282 return new lazy.UrlbarResult({ 283 type: UrlbarUtils.RESULT_TYPE.SEARCH, 284 source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, 285 heuristic: true, 286 payload: { 287 query, 288 title: query, 289 keyword: firstToken, 290 }, 291 }); 292 } 293 294 async _engineSearchResult({ 295 queryContext, 296 keyword = null, 297 heuristic = false, 298 }) { 299 let engine; 300 if (queryContext.searchMode?.engineName) { 301 engine = lazy.UrlbarSearchUtils.getEngineByName( 302 queryContext.searchMode.engineName 303 ); 304 } else { 305 engine = lazy.UrlbarSearchUtils.getDefaultEngine(queryContext.isPrivate); 306 } 307 308 if (!engine) { 309 return null; 310 } 311 312 // Strip a leading search restriction char, because we prepend it to text 313 // when the search shortcut is used and it's not user typed. Don't strip 314 // other restriction chars, so that it's possible to search for things 315 // including one of those (e.g. "c#"). 316 let query = queryContext.searchString; 317 if ( 318 queryContext.tokens[0] && 319 queryContext.tokens[0].value === lazy.UrlbarTokenizer.RESTRICT.SEARCH 320 ) { 321 query = UrlbarUtils.substringAfter( 322 query, 323 queryContext.tokens[0].value 324 ).trim(); 325 } 326 327 return new lazy.UrlbarResult({ 328 type: UrlbarUtils.RESULT_TYPE.SEARCH, 329 source: UrlbarUtils.RESULT_SOURCE.SEARCH, 330 heuristic, 331 payload: { 332 engine: engine.name, 333 icon: UrlbarUtils.ICON.SEARCH_GLASS, 334 query, 335 title: query, 336 keyword, 337 }, 338 highlights: { 339 engine: UrlbarUtils.HIGHLIGHT.TYPED, 340 }, 341 }); 342 } 343 }