/* 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/. */ /** * This module exports a provider that offers input history (aka adaptive * history) results. These results map typed search strings to Urlbar results. * That way, a user can find a particular result again by typing the same * string. */ import { UrlbarProvider, UrlbarUtils, } from "resource:///modules/UrlbarUtils.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs", UrlbarProviderOpenTabs: "resource:///modules/UrlbarProviderOpenTabs.sys.mjs", UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs", }); // Sqlite result row index constants. const QUERYINDEX = { URL: 0, TITLE: 1, BOOKMARKED: 2, BOOKMARKTITLE: 3, TAGS: 4, SWITCHTAB: 8, }; // Constants to support an alternative frecency algorithm. const PAGES_USE_ALT_FRECENCY = Services.prefs.getBoolPref( "places.frecency.pages.alternative.featureGate", false ); const PAGES_FRECENCY_FIELD = PAGES_USE_ALT_FRECENCY ? "alt_frecency" : "frecency"; // This SQL query fragment provides the following: // - whether the entry is bookmarked (QUERYINDEX_BOOKMARKED) // - the bookmark title, if it is a bookmark (QUERYINDEX_BOOKMARKTITLE) // - the tags associated with a bookmarked entry (QUERYINDEX_TAGS) const SQL_BOOKMARK_TAGS_FRAGMENT = `EXISTS(SELECT 1 FROM moz_bookmarks WHERE fk = h.id) AS bookmarked, ( SELECT title FROM moz_bookmarks WHERE fk = h.id AND title NOTNULL ORDER BY lastModified DESC LIMIT 1 ) AS btitle, ( SELECT GROUP_CONCAT(t.title ORDER BY t.title) FROM moz_bookmarks b JOIN moz_bookmarks t ON t.id = +b.parent AND t.parent = :parent WHERE b.fk = h.id ) AS tags`; const SQL_ADAPTIVE_QUERY = `/* do not warn (bug 487789) */ SELECT h.url, h.title, ${SQL_BOOKMARK_TAGS_FRAGMENT}, h.visit_count, h.typed, h.id, t.open_count, ${PAGES_FRECENCY_FIELD} FROM ( SELECT ROUND(MAX(use_count) * (1 + (input = :search_string)), 1) AS rank, place_id FROM moz_inputhistory WHERE input BETWEEN :search_string AND :search_string || X'FFFF' GROUP BY place_id ) AS i JOIN moz_places h ON h.id = i.place_id LEFT JOIN moz_openpages_temp t ON t.url = h.url AND (t.userContextId = :userContextId OR (t.userContextId <> -1 AND :userContextId IS NULL)) WHERE AUTOCOMPLETE_MATCH(NULL, h.url, IFNULL(btitle, h.title), tags, h.visit_count, h.typed, bookmarked, t.open_count, :matchBehavior, :searchBehavior, NULL) ORDER BY rank DESC, ${PAGES_FRECENCY_FIELD} DESC LIMIT :maxResults`; /** * Class used to create the provider. */ class ProviderInputHistory extends UrlbarProvider { /** * Unique name for the provider, used by the context to filter on providers. * * @returns {string} */ get name() { return "InputHistory"; } /** * The type of the provider, must be one of UrlbarUtils.PROVIDER_TYPE. * * @returns {UrlbarUtils.PROVIDER_TYPE} */ get type() { return UrlbarUtils.PROVIDER_TYPE.PROFILE; } /** * 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 ( (lazy.UrlbarPrefs.get("suggest.history") || lazy.UrlbarPrefs.get("suggest.bookmark") || lazy.UrlbarPrefs.get("suggest.openpage")) && !queryContext.searchMode ); } /** * Starts querying. Extended classes should return a Promise resolved when the * provider is done searching AND returning results. * * @param {UrlbarQueryContext} queryContext The query context object * @param {Function} addCallback Callback invoked by the provider to add a new * result. A UrlbarResult should be passed to it. * @returns {Promise} */ async startQuery(queryContext, addCallback) { let instance = this.queryInstance; let conn = await lazy.PlacesUtils.promiseLargeCacheDBConnection(); if (instance != this.queryInstance) { return; } let [query, params] = this._getAdaptiveQuery(queryContext); let rows = await conn.executeCached(query, params); if (instance != this.queryInstance) { return; } for (let row of rows) { const url = row.getResultByIndex(QUERYINDEX.URL); const openPageCount = row.getResultByIndex(QUERYINDEX.SWITCHTAB) || 0; const historyTitle = row.getResultByIndex(QUERYINDEX.TITLE) || ""; const bookmarked = row.getResultByIndex(QUERYINDEX.BOOKMARKED); const bookmarkTitle = bookmarked ? row.getResultByIndex(QUERYINDEX.BOOKMARKTITLE) : null; const tags = row.getResultByIndex(QUERYINDEX.TAGS) || ""; let resultTitle = historyTitle; if (openPageCount > 0 && lazy.UrlbarPrefs.get("suggest.openpage")) { if (url == queryContext.currentPage) { // Don't suggest switching to the current page. continue; } let result = new lazy.UrlbarResult( UrlbarUtils.RESULT_TYPE.TAB_SWITCH, UrlbarUtils.RESULT_SOURCE.TABS, ...lazy.UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, { url: [url, UrlbarUtils.HIGHLIGHT.TYPED], title: [resultTitle, UrlbarUtils.HIGHLIGHT.TYPED], icon: UrlbarUtils.getIconForUrl(url), userContextId: queryContext.userContextId || 0, }) ); addCallback(this, result); continue; } let resultSource; if (bookmarked && lazy.UrlbarPrefs.get("suggest.bookmark")) { resultSource = UrlbarUtils.RESULT_SOURCE.BOOKMARKS; resultTitle = bookmarkTitle || historyTitle; } else if (lazy.UrlbarPrefs.get("suggest.history")) { resultSource = UrlbarUtils.RESULT_SOURCE.HISTORY; } else { continue; } let resultTags = tags.split(",").filter(tag => { let lowerCaseTag = tag.toLocaleLowerCase(); return queryContext.tokens.some(token => lowerCaseTag.includes(token.lowerCaseValue) ); }); let isBlockable = resultSource == UrlbarUtils.RESULT_SOURCE.HISTORY; let result = new lazy.UrlbarResult( UrlbarUtils.RESULT_TYPE.URL, resultSource, ...lazy.UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, { url: [url, UrlbarUtils.HIGHLIGHT.TYPED], title: [resultTitle, UrlbarUtils.HIGHLIGHT.TYPED], tags: [resultTags, UrlbarUtils.HIGHLIGHT.TYPED], icon: UrlbarUtils.getIconForUrl(url), isBlockable, blockL10n: isBlockable ? { id: "urlbar-result-menu-remove-from-history" } : undefined, helpUrl: isBlockable ? Services.urlFormatter.formatURLPref("app.support.baseURL") + "awesome-bar-result-menu" : undefined, }) ); addCallback(this, result); } } onEngagement(state, queryContext, details, controller) { let { result } = details; if (result?.providerName != this.name) { return; } if ( details.selType == "dismiss" && result.type == UrlbarUtils.RESULT_TYPE.URL ) { // Even if removing history normally also removes input history, that // doesn't happen if the page is bookmarked, so we do remove input history // regardless for this specific search term. UrlbarUtils.removeInputHistory( result.payload.url, queryContext.searchString ).catch(console.error); // Remove browsing history for the page. lazy.PlacesUtils.history.remove(result.payload.url).catch(console.error); controller.removeResult(result); } } /** * Obtains the query to search for adaptive results. * * @param {UrlbarQueryContext} queryContext * The current queryContext. * @returns {Array} Contains the optimized query with which to search the * database and an object containing the params to bound. */ _getAdaptiveQuery(queryContext) { return [ SQL_ADAPTIVE_QUERY, { parent: lazy.PlacesUtils.tagsFolderId, search_string: queryContext.searchString.toLowerCase(), matchBehavior: Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE, searchBehavior: lazy.UrlbarPrefs.get("defaultBehavior"), userContextId: lazy.UrlbarPrefs.get("switchTabs.searchAllContainers") ? lazy.UrlbarProviderOpenTabs.getUserContextIdForOpenPagesTable( null, queryContext.isPrivate ) : queryContext.userContextId, maxResults: queryContext.maxResults, }, ]; } } export var UrlbarProviderInputHistory = new ProviderInputHistory();