summaryrefslogtreecommitdiffstats
path: root/browser/components/urlbar/UrlbarSearchUtils.jsm
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /browser/components/urlbar/UrlbarSearchUtils.jsm
parentInitial commit. (diff)
downloadfirefox-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.jsm293
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();