diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /browser/components/urlbar/UrlbarSearchUtils.jsm | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/urlbar/UrlbarSearchUtils.jsm')
-rw-r--r-- | browser/components/urlbar/UrlbarSearchUtils.jsm | 293 |
1 files changed, 293 insertions, 0 deletions
diff --git a/browser/components/urlbar/UrlbarSearchUtils.jsm b/browser/components/urlbar/UrlbarSearchUtils.jsm new file mode 100644 index 0000000000..0a2a1d30e2 --- /dev/null +++ b/browser/components/urlbar/UrlbarSearchUtils.jsm @@ -0,0 +1,293 @@ +/* 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/. */ + +/* + * Search service utilities for urlbar. The only reason these functions aren't + * a part of UrlbarUtils is that we want O(1) case-insensitive lookup for search + * aliases, and to do that we need to observe the search service, persistent + * state, and an init method. A separate object is easier. + */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["UrlbarSearchUtils"]; + +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +const SEARCH_ENGINE_TOPIC = "browser-search-engine-modified"; + +/** + * Search service utilities for urlbar. + */ +class SearchUtils { + constructor() { + this._refreshEnginesByAliasPromise = Promise.resolve(); + this.QueryInterface = ChromeUtils.generateQI([ + "nsIObserver", + "nsISupportsWeakReference", + ]); + XPCOMUtils.defineLazyPreferenceGetter( + this, + "separatePrivateDefaultUIEnabled", + "browser.search.separatePrivateDefault.ui.enabled", + false + ); + XPCOMUtils.defineLazyPreferenceGetter( + this, + "separatePrivateDefault", + "browser.search.separatePrivateDefault", + false + ); + } + + /** + * Initializes the instance and also Services.search. + */ + async init() { + if (!this._initPromise) { + this._initPromise = this._initInternal(); + } + await this._initPromise; + } + + /** + * Gets the engines whose domains match a given prefix. + * + * @param {string} prefix + * String containing the first part of the matching domain name(s). + * @param {object} [options] + * @param {boolean} [options.matchAllDomainLevels] + * Match at each sub domain, for example "a.b.c.com" will be matched at + * "a.b.c.com", "b.c.com", and "c.com". Partial matches are always returned + * after perfect matches. + * @param {boolean} [options.onlyEnabled] + * Match only engines that have not been disabled on the Search Preferences + * list. + * @returns {Array<nsISearchEngine>} + * An array of all matching engines. An empty array if there are none. + */ + async enginesForDomainPrefix( + prefix, + { matchAllDomainLevels = false, onlyEnabled = false } = {} + ) { + await this.init(); + prefix = prefix.toLowerCase(); + + let disabledEngines = onlyEnabled + ? Services.prefs + .getStringPref("browser.search.hiddenOneOffs", "") + .split(",") + .filter(e => !!e) + : []; + + // Array of partially matched engines, added through matchPrefix(). + let partialMatchEngines = []; + function matchPrefix(engine, engineHost) { + let parts = engineHost.split("."); + for (let i = 1; i < parts.length - 1; ++i) { + if ( + parts + .slice(i) + .join(".") + .startsWith(prefix) + ) { + partialMatchEngines.push(engine); + } + } + } + + // Array of fully matched engines. + let engines = []; + for (let engine of await Services.search.getVisibleEngines()) { + if (disabledEngines.includes(engine.name)) { + continue; + } + let domain = engine.getResultDomain(); + if (domain.startsWith(prefix) || domain.startsWith("www." + prefix)) { + engines.push(engine); + } + + if (matchAllDomainLevels) { + // The prefix may or may not contain part of the public suffix. If + // it contains a dot, we must match with and without the public suffix, + // otherwise it's sufficient to just match without it. + if (prefix.includes(".")) { + matchPrefix(engine, domain); + } + matchPrefix( + engine, + domain.substr(0, domain.length - engine.searchUrlPublicSuffix.length) + ); + } + } + + // Partial matches come after perfect matches. + return [...engines, ...partialMatchEngines]; + } + + /** + * Gets the engine with a given alias. + * + * @param {string} alias + * A search engine alias. The alias string comparison is case insensitive. + * @returns {nsISearchEngine} + * The matching engine or null if there isn't one. + */ + async engineForAlias(alias) { + await Promise.all([this.init(), this._refreshEnginesByAliasPromise]); + return this._enginesByAlias.get(alias.toLocaleLowerCase()) || null; + } + + /** + * The list of engines with token ("@") aliases. + * + * @returns {array} + * Array of objects { engine, tokenAliases } for token alias engines. + */ + async tokenAliasEngines() { + await this.init(); + let tokenAliasEngines = []; + for (let engine of await Services.search.getVisibleEngines()) { + let tokenAliases = this._aliasesForEngine(engine).filter(a => + a.startsWith("@") + ); + if (tokenAliases.length) { + tokenAliasEngines.push({ engine, tokenAliases }); + } + } + return tokenAliasEngines; + } + + /** + * @param {nsISearchEngine} engine + * @returns {string} + * The root domain of a search engine. e.g. If `engine` has the domain + * www.subdomain.rootdomain.com, `rootdomain` is returned. Returns the + * engine's domain if the engine's URL does not have a valid TLD. + */ + getRootDomainFromEngine(engine) { + let domain = engine.getResultDomain(); + let suffix = engine.searchUrlPublicSuffix; + if (!suffix) { + if (domain.endsWith(".test")) { + suffix = "test"; + } else { + return domain; + } + } + domain = domain.substr( + 0, + // -1 to remove the trailing dot. + domain.length - suffix.length - 1 + ); + let domainParts = domain.split("."); + return domainParts.pop(); + } + + getDefaultEngine(isPrivate = false) { + return this.separatePrivateDefaultUIEnabled && + this.separatePrivateDefault && + isPrivate + ? Services.search.defaultPrivateEngine + : Services.search.defaultEngine; + } + + async _initInternal() { + await Services.search.init(); + await this._refreshEnginesByAlias(); + Services.obs.addObserver(this, SEARCH_ENGINE_TOPIC, true); + } + + async _refreshEnginesByAlias() { + // See the comment at the top of this file. The only reason we need this + // class is for O(1) case-insensitive lookup for search aliases, which is + // facilitated by _enginesByAlias. + this._enginesByAlias = new Map(); + for (let engine of await Services.search.getVisibleEngines()) { + if (!engine.hidden) { + for (let alias of this._aliasesForEngine(engine)) { + this._enginesByAlias.set(alias, engine); + } + } + } + } + + /** + * Compares the query parameters of two SERPs to see if one is equivalent to + * the other. URL `x` is equivalent to URL `y` if + * (a) `y` contains at least all the query parameters contained in `x`, and + * (b) The values of the query parameters contained in both `x` and `y `are + * the same. + * + * @param {string} historySerp + * The SERP from history whose params should be contained in + * `generatedSerp`. + * @param {string} generatedSerp + * The search URL we would generate for a search result with the same search + * string used in `historySerp`. + * @param {array} [ignoreParams] + * A list of params to ignore in the matching, i.e. params that can be + * contained in `historySerp` but not be in `generatedSerp`. + * @returns {boolean} True if `historySerp` can be deduped by `generatedSerp`. + * + * @note This function does not compare the SERPs' origins or pathnames. + * `historySerp` can have a different origin and/or pathname than + * `generatedSerp` and still be considered equivalent. + */ + serpsAreEquivalent(historySerp, generatedSerp, ignoreParams = []) { + let historyParams = new URL(historySerp).searchParams; + let generatedParams = new URL(generatedSerp).searchParams; + if ( + !Array.from(historyParams.entries()).every( + ([key, value]) => + ignoreParams.includes(key) || value === generatedParams.get(key) + ) + ) { + return false; + } + + return true; + } + + /** + * Gets the aliases of an engine. For the user's convenience, we recognize + * token versions of all non-token aliases. For example, if the user has an + * alias of "foo", then we recognize both "foo" and "@foo" as aliases for + * foo's engine. The returned list is therefore a superset of + * `engine.aliases`. Additionally, the returned aliases will be lower-cased + * to make lookups and comparisons easier. + * + * @param {nsISearchEngine} engine + * The aliases of this search engine will be returned. + * @returns {array} + * An array of lower-cased string aliases as described above. + */ + _aliasesForEngine(engine) { + return engine.aliases.reduce((aliases, aliasWithCase) => { + // We store lower-cased aliases to make lookups and comparisons easier. + let alias = aliasWithCase.toLocaleLowerCase(); + aliases.push(alias); + if (!alias.startsWith("@")) { + aliases.push("@" + alias); + } + return aliases; + }, []); + } + + observe(subject, topic, data) { + switch (data) { + case "engine-added": + case "engine-changed": + case "engine-removed": + case "engine-default": + this._refreshEnginesByAliasPromise = this._refreshEnginesByAlias(); + break; + } + } +} + +var UrlbarSearchUtils = new SearchUtils(); |