UrlbarProviderAutofill.sys.mjs (35267B)
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 an autofill result. 7 */ 8 9 import { 10 UrlbarProvider, 11 UrlbarUtils, 12 } from "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs"; 13 14 /** 15 * @typedef {import("UrlbarProvidersManager.sys.mjs").Query} Query 16 */ 17 18 const lazy = {}; 19 20 ChromeUtils.defineESModuleGetters(lazy, { 21 AboutPagesUtils: "resource://gre/modules/AboutPagesUtils.sys.mjs", 22 PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", 23 UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs", 24 UrlbarResult: "moz-src:///browser/components/urlbar/UrlbarResult.sys.mjs", 25 UrlbarTokenizer: 26 "moz-src:///browser/components/urlbar/UrlbarTokenizer.sys.mjs", 27 UrlUtils: "resource://gre/modules/UrlUtils.sys.mjs", 28 }); 29 30 ChromeUtils.defineLazyGetter(lazy, "pageFrecencyThreshold", () => { 31 return lazy.PlacesUtils.history.pageFrecencyThreshold(90, 0, true); 32 }); 33 34 // AutoComplete query type constants. 35 // Describes the various types of queries that we can process rows for. 36 const QUERYTYPE = { 37 AUTOFILL_ORIGIN: 1, 38 AUTOFILL_URL: 2, 39 AUTOFILL_ADAPTIVE: 3, 40 }; 41 42 // Constants to support an alternative frecency algorithm. 43 const ORIGIN_USE_ALT_FRECENCY = Services.prefs.getBoolPref( 44 "places.frecency.origins.alternative.featureGate", 45 false 46 ); 47 const ORIGIN_FRECENCY_FIELD = ORIGIN_USE_ALT_FRECENCY 48 ? "alt_frecency" 49 : "frecency"; 50 51 // `WITH` clause for the autofill queries. 52 // A NULL frecency is normalized to 1.0, because the table doesn't support NULL. 53 // Because of that, here we must set a minimum threshold of 2.0, otherwise when 54 // all the visits are older than the cutoff, we'd check 55 // 0.0 (frecency) >= 0.0 (threshold) and autofill everything instead of nothing. 56 const SQL_AUTOFILL_WITH = ORIGIN_USE_ALT_FRECENCY 57 ? ` 58 WITH 59 autofill_frecency_threshold(value) AS ( 60 SELECT IFNULL( 61 (SELECT value FROM moz_meta WHERE key = 'origin_alt_frecency_threshold'), 62 2.0 63 ) 64 ) 65 ` 66 : ` 67 WITH 68 autofill_frecency_threshold(value) AS ( 69 SELECT IFNULL( 70 (SELECT value FROM moz_meta WHERE key = 'origin_frecency_threshold'), 71 2.0 72 ) 73 ) 74 `; 75 76 const SQL_AUTOFILL_FRECENCY_THRESHOLD = `host_frecency >= ( 77 SELECT value FROM autofill_frecency_threshold 78 )`; 79 80 function originQuery(where) { 81 // `frecency`, `n_bookmarks` and `visited` are partitioned by the fixed host, 82 // without `www.`. `host_prefix` instead is partitioned by full host, because 83 // we assume a prefix may not work regardless of `www.`. 84 let selectVisited = where.includes("visited") 85 ? `MAX(EXISTS( 86 SELECT 1 FROM moz_places WHERE origin_id = o.id AND visit_count > 0 87 )) OVER (PARTITION BY fixup_url(host)) > 0` 88 : "0"; 89 let selectTitle; 90 let joinBookmarks; 91 if (where.includes("n_bookmarks")) { 92 selectTitle = "ifnull(b.title, iif(h.frecency <> 0, h.title, NULL))"; 93 joinBookmarks = "LEFT JOIN moz_bookmarks b ON b.fk = h.id"; 94 } else { 95 selectTitle = "iif(h.frecency <> 0, h.title, NULL)"; 96 joinBookmarks = ""; 97 } 98 return `/* do not warn (bug no): cannot use an index to sort */ 99 ${SQL_AUTOFILL_WITH}, 100 origins(id, prefix, host_prefix, host, fixed, host_frecency, frecency, n_bookmarks, visited) AS ( 101 SELECT 102 id, 103 prefix, 104 first_value(prefix) OVER ( 105 PARTITION BY host ORDER BY ${ORIGIN_FRECENCY_FIELD} DESC, prefix = "https://" DESC, id DESC 106 ), 107 host, 108 fixup_url(host), 109 total(${ORIGIN_FRECENCY_FIELD}) OVER (PARTITION BY fixup_url(host)), 110 ${ORIGIN_FRECENCY_FIELD}, 111 total( 112 (SELECT total(foreign_count) FROM moz_places WHERE origin_id = o.id) 113 ) OVER (PARTITION BY fixup_url(host)), 114 ${selectVisited} 115 FROM moz_origins o 116 WHERE prefix NOT IN ('about:', 'place:') 117 AND ((host BETWEEN :searchString AND :searchString || X'FFFF') 118 OR (host BETWEEN 'www.' || :searchString AND 'www.' || :searchString || X'FFFF')) 119 ), 120 matched_origin(host_fixed, url) AS ( 121 SELECT iif(instr(host, :searchString) = 1, host, fixed) || '/', 122 ifnull(:prefix, host_prefix) || host || '/' 123 FROM origins 124 ${where} 125 ORDER BY frecency DESC, n_bookmarks DESC, prefix = "https://" DESC, id DESC 126 LIMIT 1 127 ), 128 matched_place(host_fixed, url, id, title, frecency) AS ( 129 SELECT o.host_fixed, o.url, h.id, h.title, h.frecency 130 FROM matched_origin o 131 LEFT JOIN moz_places h ON h.url_hash IN ( 132 hash('https://' || o.host_fixed), 133 hash('https://www.' || o.host_fixed), 134 hash('http://' || o.host_fixed), 135 hash('http://www.' || o.host_fixed) 136 ) 137 ORDER BY 138 h.title IS NOT NULL DESC, 139 h.title || '/' <> o.host_fixed DESC, 140 h.url = o.url DESC, 141 h.frecency DESC, 142 h.id DESC 143 LIMIT 1 144 ) 145 SELECT :query_type AS query_type, 146 :searchString AS search_string, 147 h.host_fixed AS host_fixed, 148 h.url AS url, 149 ${selectTitle} AS title 150 FROM matched_place h 151 ${joinBookmarks} 152 `; 153 } 154 155 function urlQuery(where1, where2, isBookmarkContained) { 156 // We limit the search to places that are either bookmarked or have a frecency 157 // over some small, arbitrary threshold in order to avoid scanning as few 158 // rows as possible. Keep in mind that we run this query every time the user 159 // types a key when the urlbar value looks like a URL with a path. 160 let selectTitle; 161 let joinBookmarks; 162 if (isBookmarkContained) { 163 selectTitle = "ifnull(b.title, matched_url.title)"; 164 joinBookmarks = "LEFT JOIN moz_bookmarks b ON b.fk = matched_url.id"; 165 } else { 166 selectTitle = "matched_url.title"; 167 joinBookmarks = ""; 168 } 169 return `/* do not warn (bug no): cannot use an index to sort */ 170 WITH matched_url(url, title, frecency, n_bookmarks, visited, stripped_url, is_exact_match, id) AS ( 171 SELECT url, 172 title, 173 frecency, 174 foreign_count AS n_bookmarks, 175 visit_count > 0 AS visited, 176 strip_prefix_and_userinfo(url) AS stripped_url, 177 strip_prefix_and_userinfo(url) = strip_prefix_and_userinfo(:strippedURL) AS is_exact_match, 178 id 179 FROM moz_places 180 WHERE rev_host = :revHost 181 ${where1} 182 UNION ALL 183 SELECT url, 184 title, 185 frecency, 186 foreign_count AS n_bookmarks, 187 visit_count > 0 AS visited, 188 strip_prefix_and_userinfo(url) AS stripped_url, 189 strip_prefix_and_userinfo(url) = 'www.' || strip_prefix_and_userinfo(:strippedURL) AS is_exact_match, 190 id 191 FROM moz_places 192 WHERE rev_host = :revHost || 'www.' 193 ${where2} 194 ORDER BY is_exact_match DESC, frecency DESC, id DESC 195 LIMIT 1 196 ) 197 SELECT :query_type AS query_type, 198 :searchString AS search_string, 199 :strippedURL AS stripped_url, 200 matched_url.url AS url, 201 ${selectTitle} AS title 202 FROM matched_url 203 ${joinBookmarks} 204 `; 205 } 206 207 // Queries 208 const QUERY_ORIGIN_HISTORY_BOOKMARK = originQuery( 209 `WHERE n_bookmarks > 0 OR ${SQL_AUTOFILL_FRECENCY_THRESHOLD}` 210 ); 211 212 const QUERY_ORIGIN_PREFIX_HISTORY_BOOKMARK = originQuery( 213 `WHERE prefix BETWEEN :prefix AND :prefix || X'FFFF' 214 AND (n_bookmarks > 0 OR ${SQL_AUTOFILL_FRECENCY_THRESHOLD})` 215 ); 216 217 const QUERY_ORIGIN_HISTORY = originQuery( 218 `WHERE visited AND ${SQL_AUTOFILL_FRECENCY_THRESHOLD}` 219 ); 220 221 const QUERY_ORIGIN_PREFIX_HISTORY = originQuery( 222 `WHERE prefix BETWEEN :prefix AND :prefix || X'FFFF' 223 AND visited AND ${SQL_AUTOFILL_FRECENCY_THRESHOLD}` 224 ); 225 226 const QUERY_ORIGIN_BOOKMARK = originQuery(`WHERE n_bookmarks > 0`); 227 228 const QUERY_ORIGIN_PREFIX_BOOKMARK = originQuery( 229 `WHERE prefix BETWEEN :prefix AND :prefix || X'FFFF' AND n_bookmarks > 0` 230 ); 231 232 const QUERY_URL_HISTORY_BOOKMARK = urlQuery( 233 `AND (n_bookmarks > 0 OR frecency > :pageFrecencyThreshold) 234 AND stripped_url COLLATE NOCASE 235 BETWEEN :strippedURL AND :strippedURL || X'FFFF'`, 236 `AND (n_bookmarks > 0 OR frecency > :pageFrecencyThreshold) 237 AND stripped_url COLLATE NOCASE 238 BETWEEN 'www.' || :strippedURL AND 'www.' || :strippedURL || X'FFFF'`, 239 true 240 ); 241 242 const QUERY_URL_PREFIX_HISTORY_BOOKMARK = urlQuery( 243 `AND (n_bookmarks > 0 OR frecency > :pageFrecencyThreshold) 244 AND url COLLATE NOCASE 245 BETWEEN :prefix || :strippedURL AND :prefix || :strippedURL || X'FFFF'`, 246 `AND (n_bookmarks > 0 OR frecency > :pageFrecencyThreshold) 247 AND url COLLATE NOCASE 248 BETWEEN :prefix || 'www.' || :strippedURL AND :prefix || 'www.' || :strippedURL || X'FFFF'`, 249 true 250 ); 251 252 const QUERY_URL_HISTORY = urlQuery( 253 `AND (visited OR n_bookmarks = 0) 254 AND frecency > :pageFrecencyThreshold 255 AND stripped_url COLLATE NOCASE 256 BETWEEN :strippedURL AND :strippedURL || X'FFFF'`, 257 `AND (visited OR n_bookmarks = 0) 258 AND frecency > :pageFrecencyThreshold 259 AND stripped_url COLLATE NOCASE 260 BETWEEN 'www.' || :strippedURL AND 'www.' || :strippedURL || X'FFFF'`, 261 false 262 ); 263 264 const QUERY_URL_PREFIX_HISTORY = urlQuery( 265 `AND (visited OR n_bookmarks = 0) 266 AND frecency > :pageFrecencyThreshold 267 AND url COLLATE NOCASE 268 BETWEEN :prefix || :strippedURL AND :prefix || :strippedURL || X'FFFF'`, 269 `AND (visited OR n_bookmarks = 0) 270 AND frecency > :pageFrecencyThreshold 271 AND url COLLATE NOCASE 272 BETWEEN :prefix || 'www.' || :strippedURL AND :prefix || 'www.' || :strippedURL || X'FFFF'`, 273 false 274 ); 275 276 const QUERY_URL_BOOKMARK = urlQuery( 277 `AND n_bookmarks > 0 278 AND stripped_url COLLATE NOCASE 279 BETWEEN :strippedURL AND :strippedURL || X'FFFF'`, 280 `AND n_bookmarks > 0 281 AND stripped_url COLLATE NOCASE 282 BETWEEN 'www.' || :strippedURL AND 'www.' || :strippedURL || X'FFFF'`, 283 true 284 ); 285 286 const QUERY_URL_PREFIX_BOOKMARK = urlQuery( 287 `AND n_bookmarks > 0 288 AND url COLLATE NOCASE 289 BETWEEN :prefix || :strippedURL AND :prefix || :strippedURL || X'FFFF'`, 290 `AND n_bookmarks > 0 291 AND url COLLATE NOCASE 292 BETWEEN :prefix || 'www.' || :strippedURL AND :prefix || 'www.' || :strippedURL || X'FFFF'`, 293 true 294 ); 295 296 /** 297 * @typedef AutofillData 298 * 299 * @property {UrlbarResult} result 300 * The result entry. 301 * @property {Query} instance 302 * The query instance. 303 */ 304 305 /** 306 * Class used to create the provider. 307 */ 308 export class UrlbarProviderAutofill extends UrlbarProvider { 309 /** 310 * This is usually reset on canceling or completing the query, but since we 311 * query in isActive, it may not have been canceled by the previous call. 312 * 313 * @type {?AutofillData} 314 */ 315 _autofillData = null; 316 constructor() { 317 super(); 318 } 319 320 /** 321 * @returns {Values<typeof UrlbarUtils.PROVIDER_TYPE>} 322 */ 323 get type() { 324 return UrlbarUtils.PROVIDER_TYPE.HEURISTIC; 325 } 326 327 /** 328 * Whether this provider should be invoked for the given context. 329 * If this method returns false, the providers manager won't start a query 330 * with this provider, to save on resources. 331 * 332 * @param {UrlbarQueryContext} queryContext The query context object 333 */ 334 async isActive(queryContext) { 335 let instance = this.queryInstance; 336 337 // This is usually reset on canceling or completing the query, but since we 338 // query in isActive, it may not have been canceled by the previous call. 339 this._autofillData = null; 340 341 // First of all, check for the autoFill pref. 342 if (!lazy.UrlbarPrefs.get("autoFill")) { 343 return false; 344 } 345 346 if (!queryContext.allowAutofill) { 347 return false; 348 } 349 350 if (queryContext.tokens.length != 1) { 351 return false; 352 } 353 354 // Trying to autofill an extremely long string would be expensive, and 355 // not particularly useful since the filled part falls out of screen anyway. 356 if (queryContext.searchString.length > UrlbarUtils.MAX_TEXT_LENGTH) { 357 return false; 358 } 359 360 // autoFill can only cope with history, bookmarks, and about: entries. 361 if ( 362 !queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY) && 363 !queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS) 364 ) { 365 return false; 366 } 367 368 // Autofill doesn't search tags or titles 369 if ( 370 queryContext.tokens.some( 371 t => 372 t.type == lazy.UrlbarTokenizer.TYPE.RESTRICT_TAG || 373 t.type == lazy.UrlbarTokenizer.TYPE.RESTRICT_TITLE 374 ) 375 ) { 376 return false; 377 } 378 379 [this._strippedPrefix, this._searchString] = UrlbarUtils.stripURLPrefix( 380 queryContext.searchString 381 ); 382 this._strippedPrefix = this._strippedPrefix.toLowerCase(); 383 384 // Don't try to autofill if the search term includes any whitespace. 385 // This may confuse completeDefaultIndex cause the AUTOCOMPLETE_MATCH 386 // tokenizer ends up trimming the search string and returning a value 387 // that doesn't match it, or is even shorter. 388 if (lazy.UrlUtils.REGEXP_SPACES.test(queryContext.searchString)) { 389 return false; 390 } 391 392 // Fetch autofill result now, rather than in startQuery. We do this so the 393 // muxer doesn't have to wait on autofill for every query, since startQuery 394 // will be guaranteed to return a result very quickly using this approach. 395 // Bug 1651101 is filed to improve this behaviour. 396 let result = await this._getAutofillResult(queryContext); 397 if (!result || instance != this.queryInstance) { 398 return false; 399 } 400 this._autofillData = { result, instance }; 401 return true; 402 } 403 404 /** 405 * Gets the provider's priority. 406 * 407 * @returns {number} The provider's priority for the given query. 408 */ 409 getPriority() { 410 return 0; 411 } 412 413 /** 414 * Starts querying. 415 * 416 * @param {UrlbarQueryContext} queryContext 417 * @param {(provider: UrlbarProvider, result: UrlbarResult) => void} addCallback 418 * Callback invoked by the provider to add a new result. 419 */ 420 async startQuery(queryContext, addCallback) { 421 // Check if the query was cancelled while the autofill result was being 422 // fetched. We don't expect this to be true since we also check the instance 423 // in isActive and clear _autofillData in cancelQuery, but we sanity check it. 424 if ( 425 !this._autofillData || 426 this._autofillData.instance != this.queryInstance 427 ) { 428 this.logger.error("startQuery invoked with an invalid _autofillData"); 429 return; 430 } 431 432 addCallback(this, this._autofillData.result); 433 this._autofillData = null; 434 } 435 436 /** 437 * Cancels a running query. 438 */ 439 cancelQuery() { 440 if (this._autofillData?.instance == this.queryInstance) { 441 this._autofillData = null; 442 } 443 } 444 445 /** 446 * Filters hosts by retaining only the ones over the autofill threshold, then 447 * sorts them by their frecency, and extracts the one with the highest value. 448 * 449 * @param {UrlbarQueryContext} queryContext The current queryContext. 450 * @param {Array} hosts Array of host names to examine. 451 * @returns {Promise<string?>} 452 * Resolved when the filtering is complete. Resolves with the top matching 453 * host, or null if not found. 454 */ 455 static async getTopHostOverThreshold(queryContext, hosts) { 456 let db = await lazy.PlacesUtils.promiseLargeCacheDBConnection(); 457 let conditions = []; 458 // Pay attention to the order of params, since they are not named. 459 let params = [...hosts]; 460 let sources = queryContext.sources; 461 if ( 462 sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY) && 463 sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS) 464 ) { 465 conditions.push( 466 `(n_bookmarks > 0 OR ${SQL_AUTOFILL_FRECENCY_THRESHOLD})` 467 ); 468 } else if (sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY)) { 469 conditions.push(`visited AND ${SQL_AUTOFILL_FRECENCY_THRESHOLD}`); 470 } else if (sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)) { 471 conditions.push("n_bookmarks > 0"); 472 } 473 474 let rows = await db.executeCached( 475 ` 476 ${SQL_AUTOFILL_WITH}, 477 origins(id, prefix, host_prefix, host, fixed, host_frecency, frecency, n_bookmarks, visited) AS ( 478 SELECT 479 id, 480 prefix, 481 first_value(prefix) OVER ( 482 PARTITION BY host ORDER BY ${ORIGIN_FRECENCY_FIELD} DESC, prefix = "https://" DESC, id DESC 483 ), 484 host, 485 fixup_url(host), 486 total(${ORIGIN_FRECENCY_FIELD}) OVER (PARTITION BY fixup_url(host)), 487 ${ORIGIN_FRECENCY_FIELD}, 488 total( 489 (SELECT total(foreign_count) FROM moz_places WHERE origin_id = o.id) 490 ) OVER (PARTITION BY fixup_url(host)), 491 MAX(EXISTS( 492 SELECT 1 FROM moz_places WHERE origin_id = o.id AND visit_count > 0 493 )) OVER (PARTITION BY fixup_url(host)) 494 FROM moz_origins o 495 WHERE o.host IN (${new Array(hosts.length).fill("?").join(",")}) 496 ) 497 SELECT host 498 FROM origins 499 ${conditions.length ? "WHERE " + conditions.join(" AND ") : ""} 500 ORDER BY frecency DESC, prefix = "https://" DESC, id DESC 501 LIMIT 1 502 `, 503 params 504 ); 505 if (!rows.length) { 506 return null; 507 } 508 return rows[0].getResultByName("host"); 509 } 510 511 /** 512 * @type {string} 513 * The search string with the prefix stripped. 514 */ 515 _searchString; 516 517 /** 518 * Obtains the query to search for autofill origin results. 519 * 520 * @param {UrlbarQueryContext} queryContext 521 * The current queryContext. 522 * @returns {Array} consisting of the correctly optimized query to search the 523 * database with and an object containing the params to bound. 524 */ 525 _getOriginQuery(queryContext) { 526 // At this point, searchString is not a URL with a path; it does not 527 // contain a slash, except for possibly at the very end. If there is 528 // trailing slash, remove it when searching here to match the rest of the 529 // string because it may be an origin. 530 let searchStr = this._searchString.endsWith("/") 531 ? this._searchString.slice(0, -1) 532 : this._searchString; 533 534 let opts = { 535 query_type: QUERYTYPE.AUTOFILL_ORIGIN, 536 searchString: searchStr.toLowerCase(), 537 }; 538 if (this._strippedPrefix) { 539 opts.prefix = this._strippedPrefix; 540 } 541 542 if ( 543 queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY) && 544 queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS) 545 ) { 546 return [ 547 this._strippedPrefix 548 ? QUERY_ORIGIN_PREFIX_HISTORY_BOOKMARK 549 : QUERY_ORIGIN_HISTORY_BOOKMARK, 550 opts, 551 ]; 552 } 553 if (queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY)) { 554 return [ 555 this._strippedPrefix 556 ? QUERY_ORIGIN_PREFIX_HISTORY 557 : QUERY_ORIGIN_HISTORY, 558 opts, 559 ]; 560 } 561 if (queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)) { 562 return [ 563 this._strippedPrefix 564 ? QUERY_ORIGIN_PREFIX_BOOKMARK 565 : QUERY_ORIGIN_BOOKMARK, 566 opts, 567 ]; 568 } 569 throw new Error("Either history or bookmark behavior expected"); 570 } 571 572 /** 573 * Obtains the query to search for autoFill url results. 574 * 575 * @param {UrlbarQueryContext} queryContext 576 * The current queryContext. 577 * @returns {Array} consisting of the correctly optimized query to search the 578 * database with and an object containing the params to bound. 579 */ 580 _getUrlQuery(queryContext) { 581 // Try to get the host from the search string. The host is the part of the 582 // URL up to either the path slash, port colon, or query "?". If the search 583 // string doesn't look like it begins with a host, then return; it doesn't 584 // make sense to do a URL query with it. 585 const urlQueryHostRegexp = /^[^/:?]+/; 586 let hostMatch = urlQueryHostRegexp.exec(this._searchString); 587 if (!hostMatch) { 588 return [null, null]; 589 } 590 591 let host = hostMatch[0].toLowerCase(); 592 let revHost = host.split("").reverse().join("") + "."; 593 594 // Build a string that's the URL stripped of its prefix, i.e., the host plus 595 // everything after. Use queryContext.trimmedSearchString instead of 596 // this._searchString because this._searchString has had unEscapeURIForUI() 597 // called on it. It's therefore not necessarily the literal URL. 598 let strippedURL = queryContext.trimmedSearchString; 599 if (this._strippedPrefix) { 600 strippedURL = strippedURL.substr(this._strippedPrefix.length); 601 } 602 strippedURL = host + strippedURL.substr(host.length); 603 604 let opts = { 605 query_type: QUERYTYPE.AUTOFILL_URL, 606 searchString: this._searchString, 607 revHost, 608 strippedURL, 609 }; 610 if (this._strippedPrefix) { 611 opts.prefix = this._strippedPrefix; 612 } 613 614 if ( 615 queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY) && 616 queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS) 617 ) { 618 opts.pageFrecencyThreshold = lazy.pageFrecencyThreshold; 619 return [ 620 this._strippedPrefix 621 ? QUERY_URL_PREFIX_HISTORY_BOOKMARK 622 : QUERY_URL_HISTORY_BOOKMARK, 623 opts, 624 ]; 625 } 626 if (queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY)) { 627 opts.pageFrecencyThreshold = lazy.pageFrecencyThreshold; 628 return [ 629 this._strippedPrefix ? QUERY_URL_PREFIX_HISTORY : QUERY_URL_HISTORY, 630 opts, 631 ]; 632 } 633 if (queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)) { 634 return [ 635 this._strippedPrefix ? QUERY_URL_PREFIX_BOOKMARK : QUERY_URL_BOOKMARK, 636 opts, 637 ]; 638 } 639 throw new Error("Either history or bookmark behavior expected"); 640 } 641 642 _getAdaptiveHistoryQuery(queryContext) { 643 let sourceCondition; 644 let params = {}; 645 if ( 646 queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY) && 647 queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS) 648 ) { 649 sourceCondition = 650 "(h.foreign_count > 0 OR h.frecency > :pageFrecencyThreshold)"; 651 params.pageFrecencyThreshold = lazy.pageFrecencyThreshold; 652 } else if ( 653 queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY) 654 ) { 655 sourceCondition = 656 "((h.visit_count > 0 OR h.foreign_count = 0) AND h.frecency > :pageFrecencyThreshold)"; 657 params.pageFrecencyThreshold = lazy.pageFrecencyThreshold; 658 } else if ( 659 queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS) 660 ) { 661 sourceCondition = "h.foreign_count > 0"; 662 } else { 663 return []; 664 } 665 666 let selectTitle; 667 let joinBookmarks; 668 if (UrlbarUtils.RESULT_SOURCE.BOOKMARKS) { 669 selectTitle = "ifnull(b.title, matched.title)"; 670 joinBookmarks = "LEFT JOIN moz_bookmarks b ON b.fk = matched.id"; 671 } else { 672 selectTitle = "matched.title"; 673 joinBookmarks = ""; 674 } 675 676 params = Object.assign(params, { 677 queryType: QUERYTYPE.AUTOFILL_ADAPTIVE, 678 // `fullSearchString` is the value the user typed including a prefix if 679 // they typed one. `searchString` has been stripped of the prefix. 680 fullSearchString: queryContext.lowerCaseSearchString, 681 searchString: this._searchString, 682 strippedPrefix: this._strippedPrefix, 683 useCountThreshold: lazy.UrlbarPrefs.get( 684 "autoFillAdaptiveHistoryUseCountThreshold" 685 ), 686 }); 687 688 const query = ` 689 WITH matched(input, url, title, stripped_url, is_exact_match, starts_with, id) AS ( 690 SELECT 691 i.input AS input, 692 h.url AS url, 693 h.title AS title, 694 strip_prefix_and_userinfo(h.url) AS stripped_url, 695 strip_prefix_and_userinfo(h.url) = :searchString AS is_exact_match, 696 (strip_prefix_and_userinfo(h.url) COLLATE NOCASE BETWEEN :searchString AND :searchString || X'FFFF') AS starts_with, 697 h.id AS id 698 FROM moz_places h 699 JOIN moz_inputhistory i ON i.place_id = h.id 700 WHERE LENGTH(i.input) != 0 701 AND :fullSearchString BETWEEN i.input AND i.input || X'FFFF' 702 AND ${sourceCondition} 703 AND i.use_count >= :useCountThreshold 704 AND (:strippedPrefix = '' OR get_prefix(h.url) = :strippedPrefix) 705 AND ( 706 starts_with OR 707 (stripped_url COLLATE NOCASE BETWEEN 'www.' || :searchString AND 'www.' || :searchString || X'FFFF') 708 ) 709 ORDER BY is_exact_match DESC, i.use_count DESC, h.frecency DESC, h.id DESC 710 LIMIT 1 711 ) 712 SELECT 713 :queryType AS query_type, 714 :searchString AS search_string, 715 input, 716 url, 717 iif(starts_with, stripped_url, fixup_url(stripped_url)) AS url_fixed, 718 ${selectTitle} AS title, 719 stripped_url 720 FROM matched 721 ${joinBookmarks} 722 `; 723 724 return [query, params]; 725 } 726 727 /** 728 * Processes a matched row in the Places database. 729 * 730 * @param {object} row 731 * The matched row. 732 * @param {UrlbarQueryContext} queryContext 733 * The query context. 734 * @returns {UrlbarResult} a result generated from the matches row. 735 */ 736 _processRow(row, queryContext) { 737 let queryType = row.getResultByName("query_type"); 738 let title = row.getResultByName("title"); 739 740 // `searchString` is `this._searchString` or derived from it. It is 741 // stripped, meaning the prefix (the URL protocol) has been removed. 742 let searchString = row.getResultByName("search_string"); 743 744 // `fixedURL` is the part of the matching stripped URL that starts with the 745 // stripped search string. The important point here is "www" handling. If a 746 // stripped URL starts with "www", we allow the user to omit the "www" and 747 // still match it. So if the matching stripped URL starts with "www" but the 748 // stripped search string does not, `fixedURL` will also omit the "www". 749 // Otherwise `fixedURL` will be equivalent to the matching stripped URL. 750 // 751 // Example 1: 752 // stripped URL: www.example.com/ 753 // searchString: exam 754 // fixedURL: example.com/ 755 // Example 2: 756 // stripped URL: www.example.com/ 757 // searchString: www.exam 758 // fixedURL: www.example.com/ 759 // Example 3: 760 // stripped URL: example.com/ 761 // searchString: exam 762 // fixedURL: example.com/ 763 let fixedURL; 764 765 // `finalCompleteValue` will be the UrlbarResult's URL. If the matching 766 // stripped URL starts with "www" but the user omitted it, 767 // `finalCompleteValue` will include it to properly reflect the real URL. 768 let finalCompleteValue; 769 770 let autofilledType; 771 let adaptiveHistoryInput; 772 773 switch (queryType) { 774 case QUERYTYPE.AUTOFILL_ORIGIN: { 775 fixedURL = row.getResultByName("host_fixed"); 776 finalCompleteValue = row.getResultByName("url"); 777 autofilledType = "origin"; 778 break; 779 } 780 case QUERYTYPE.AUTOFILL_URL: { 781 let url = row.getResultByName("url"); 782 let strippedURL = row.getResultByName("stripped_url"); 783 784 if (!UrlbarUtils.canAutofillURL(url, strippedURL, true)) { 785 return null; 786 } 787 788 // We autofill urls to-the-next-slash. 789 // http://mozilla.org/foo/bar/baz will be autofilled to: 790 // - http://mozilla.org/f[oo/] 791 // - http://mozilla.org/foo/b[ar/] 792 // - http://mozilla.org/foo/bar/b[az] 793 // And, toLowerCase() is preferred over toLocaleLowerCase() here 794 // because "COLLATE NOCASE" in the SQL only handles ASCII characters. 795 let strippedURLIndex = url 796 .toLowerCase() 797 .indexOf(strippedURL.toLowerCase()); 798 let strippedPrefix = url.substr(0, strippedURLIndex); 799 let nextSlashIndex = url.indexOf( 800 "/", 801 strippedURLIndex + strippedURL.length - 1 802 ); 803 fixedURL = 804 nextSlashIndex < 0 805 ? url.substr(strippedURLIndex) 806 : url.substring(strippedURLIndex, nextSlashIndex + 1); 807 finalCompleteValue = strippedPrefix + fixedURL; 808 if (finalCompleteValue !== url) { 809 title = null; 810 } 811 autofilledType = "url"; 812 break; 813 } 814 case QUERYTYPE.AUTOFILL_ADAPTIVE: { 815 adaptiveHistoryInput = row.getResultByName("input"); 816 fixedURL = row.getResultByName("url_fixed"); 817 finalCompleteValue = row.getResultByName("url"); 818 autofilledType = "adaptive"; 819 break; 820 } 821 } 822 823 // Compute `autofilledValue`, the full value that will be placed in the 824 // input. It includes two parts: the part the user already typed in the 825 // character case they typed it (`queryContext.searchString`), and the 826 // autofilled part, which is the portion of the fixed URL starting after the 827 // stripped search string. 828 let autofilledValue = 829 queryContext.searchString + fixedURL.substring(searchString.length); 830 831 // If more than an origin was autofilled and the user typed the full 832 // autofilled value, override the final URL by using the exact value the 833 // user typed. This allows the user to visit a URL that differs from the 834 // autofilled URL only in character case (for example "wikipedia.org/RAID" 835 // vs. "wikipedia.org/Raid") by typing the full desired URL. 836 if ( 837 queryType != QUERYTYPE.AUTOFILL_ORIGIN && 838 queryContext.searchString.length == autofilledValue.length 839 ) { 840 // Use `new URL().href` to lowercase the domain in the final completed 841 // URL. This isn't necessary since domains are case insensitive, but it 842 // looks nicer because it means the domain will remain lowercased in the 843 // input, and it also reflects the fact that Firefox will visit the 844 // lowercased name. 845 const originalCompleteValue = new URL(finalCompleteValue).href; 846 let strippedAutofilledValue = autofilledValue.substring( 847 this._strippedPrefix.length 848 ); 849 finalCompleteValue = new URL( 850 finalCompleteValue.substring( 851 0, 852 finalCompleteValue.length - strippedAutofilledValue.length 853 ) + strippedAutofilledValue 854 ).href; 855 856 // If the character case of except origin part of the original 857 // finalCompleteValue differs from finalCompleteValue that includes user's 858 // input, we set title null because it expresses different web page. 859 if (finalCompleteValue !== originalCompleteValue) { 860 title = null; 861 } 862 } 863 864 let payload = { 865 url: finalCompleteValue, 866 icon: UrlbarUtils.getIconForUrl(finalCompleteValue), 867 }; 868 869 let noVisitAction = !!title; 870 if (title) { 871 payload.title = title; 872 } else { 873 let trimHttps = lazy.UrlbarPrefs.getScotchBonnetPref("trimHttps"); 874 let displaySpec = UrlbarUtils.prepareUrlForDisplay(finalCompleteValue, { 875 trimURL: false, 876 }); 877 let [fallbackTitle] = UrlbarUtils.stripPrefixAndTrim(displaySpec, { 878 stripHttp: !trimHttps, 879 stripHttps: trimHttps, 880 trimEmptyQuery: true, 881 trimSlash: !this._searchString.includes("/"), 882 }); 883 payload.title = fallbackTitle; 884 } 885 886 return new lazy.UrlbarResult({ 887 type: UrlbarUtils.RESULT_TYPE.URL, 888 source: UrlbarUtils.RESULT_SOURCE.HISTORY, 889 heuristic: true, 890 autofill: { 891 adaptiveHistoryInput, 892 value: autofilledValue, 893 selectionStart: queryContext.searchString.length, 894 selectionEnd: autofilledValue.length, 895 type: autofilledType, 896 noVisitAction, 897 }, 898 payload, 899 highlights: { 900 url: UrlbarUtils.HIGHLIGHT.TYPED, 901 title: UrlbarUtils.HIGHLIGHT.TYPED, 902 fallbackTitle: UrlbarUtils.HIGHLIGHT.TYPED, 903 }, 904 }); 905 } 906 907 async _getAutofillResult(queryContext) { 908 // We may be autofilling an about: link. 909 let result = this._matchAboutPageForAutofill(queryContext); 910 if (result) { 911 return result; 912 } 913 914 // It may also look like a URL we know from the database. 915 result = await this._matchKnownUrl(queryContext); 916 if (result) { 917 return result; 918 } 919 920 return null; 921 } 922 923 _matchAboutPageForAutofill(queryContext) { 924 // Check that the typed query is at least one character longer than the 925 // about: prefix. 926 if (this._strippedPrefix != "about:" || !this._searchString) { 927 return null; 928 } 929 930 for (const aboutUrl of lazy.AboutPagesUtils.visibleAboutUrls) { 931 if (aboutUrl.startsWith(`about:${this._searchString.toLowerCase()}`)) { 932 let [trimmedUrl] = UrlbarUtils.stripPrefixAndTrim(aboutUrl, { 933 stripHttp: true, 934 trimEmptyQuery: true, 935 trimSlash: !this._searchString.includes("/"), 936 }); 937 let autofilledValue = 938 queryContext.searchString + 939 aboutUrl.substring(queryContext.searchString.length); 940 return new lazy.UrlbarResult({ 941 type: UrlbarUtils.RESULT_TYPE.URL, 942 source: UrlbarUtils.RESULT_SOURCE.HISTORY, 943 heuristic: true, 944 autofill: { 945 type: "about", 946 value: autofilledValue, 947 selectionStart: queryContext.searchString.length, 948 selectionEnd: autofilledValue.length, 949 }, 950 payload: { 951 title: trimmedUrl, 952 url: aboutUrl, 953 icon: UrlbarUtils.getIconForUrl(aboutUrl), 954 }, 955 highlights: { 956 title: UrlbarUtils.HIGHLIGHT.TYPED, 957 url: UrlbarUtils.HIGHLIGHT.TYPED, 958 }, 959 }); 960 } 961 } 962 return null; 963 } 964 965 async _matchKnownUrl(queryContext) { 966 let conn = await lazy.PlacesUtils.promiseLargeCacheDBConnection(); 967 if (!conn) { 968 return null; 969 } 970 971 // We try to autofill with adaptive history first. 972 if ( 973 lazy.UrlbarPrefs.get("autoFillAdaptiveHistoryEnabled") && 974 lazy.UrlbarPrefs.get("autoFillAdaptiveHistoryMinCharsThreshold") <= 975 queryContext.searchString.length 976 ) { 977 const [query, params] = this._getAdaptiveHistoryQuery(queryContext); 978 if (query) { 979 const resultSet = await conn.executeCached(query, params); 980 if (resultSet.length) { 981 return this._processRow(resultSet[0], queryContext); 982 } 983 } 984 } 985 986 // The adaptive history query is passed queryContext.searchString (the full 987 // search string), but the origin and URL queries are passed the prefix 988 // (this._strippedPrefix) and the rest of the search string 989 // (this._searchString) separately. The user must specify a non-prefix part 990 // to trigger origin and URL autofill. 991 if (!this._searchString.length) { 992 return null; 993 } 994 995 // If search string looks like an origin, try to autofill against origins. 996 // Otherwise treat it as a possible URL. When the string has only one slash 997 // at the end, we still treat it as an URL. 998 let query, params; 999 if ( 1000 lazy.UrlUtils.looksLikeOrigin(this._searchString, { 1001 ignoreKnownDomains: true, 1002 allowPartialNumericalTLDs: true, 1003 }) 1004 ) { 1005 [query, params] = this._getOriginQuery(queryContext); 1006 } else { 1007 [query, params] = this._getUrlQuery(queryContext); 1008 } 1009 1010 // _getUrlQuery doesn't always return a query. 1011 if (query) { 1012 let rows = await conn.executeCached(query, params); 1013 if (rows.length) { 1014 return this._processRow(rows[0], queryContext); 1015 } 1016 } 1017 return null; 1018 } 1019 }