Files
tubestation/browser/components/urlbar/UrlbarSearchTermsPersistence.sys.mjs

330 lines
10 KiB
JavaScript

/* 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/. */
const lazy = {};
import { UrlbarUtils } from "resource:///modules/UrlbarUtils.sys.mjs";
ChromeUtils.defineESModuleGetters(lazy, {
RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "logger", () =>
UrlbarUtils.getLogger({ prefix: "UrlbarSearchTermsPersistence" })
);
const URLBAR_PERSISTENCE_SETTINGS_KEY = "urlbar-persisted-search-terms";
/**
* Provides utilities to manage and validate search terms persistence in the URL
* bar. This class is designed to handle the identification of default search
* engine results pages (SERPs), retrieval of search terms, and validation of
* conditions for persisting search terms based on predefined provider
* information.
*/
class _UrlbarSearchTermsPersistence {
// Whether or not this class is initialised.
#initialized = false;
// The original provider information, mainly used for tests.
#originalProviderInfo = [];
// The current search provider info.
#searchProviderInfo = [];
// An instance of remote settings that is used to access the provider info.
#urlbarSearchTermsPersistenceSettings;
// Callback used when syncing Urlbar Search Terms Persistence config settings.
#urlbarSearchTermsPersistenceSettingsSync;
async init() {
if (this.#initialized) {
return;
}
this.#urlbarSearchTermsPersistenceSettings = lazy.RemoteSettings(
URLBAR_PERSISTENCE_SETTINGS_KEY
);
let rawProviderInfo = [];
try {
rawProviderInfo = await this.#urlbarSearchTermsPersistenceSettings.get();
} catch (ex) {
lazy.logger.error("Could not get settings:", ex);
}
this.#urlbarSearchTermsPersistenceSettingsSync = event =>
this.#onSettingsSync(event);
this.#urlbarSearchTermsPersistenceSettings.on(
"sync",
this.#urlbarSearchTermsPersistenceSettingsSync
);
this.#originalProviderInfo = rawProviderInfo;
this.#setSearchProviderInfo(rawProviderInfo);
this.#initialized = true;
}
uninit() {
if (!this.#initialized) {
return;
}
try {
this.#urlbarSearchTermsPersistenceSettings.off(
"sync",
this.#urlbarSearchTermsPersistenceSettingsSync
);
} catch (ex) {
lazy.logger.error(
"Failed to shutdown UrlbarSearchTermsPersistence Remote Settings.",
ex
);
}
this.#urlbarSearchTermsPersistenceSettings = null;
this.#urlbarSearchTermsPersistenceSettingsSync = null;
this.#initialized = false;
}
getSearchProviderInfo() {
return this.#searchProviderInfo;
}
/**
* Test-only function, used to override the provider information, so that
* unit tests can set it to easy to test values.
*
* @param {Array} providerInfo
* An array of provider information to set.
*/
overrideSearchTermsPersistenceForTests(providerInfo) {
let info = providerInfo ? providerInfo : this.#originalProviderInfo;
this.#setSearchProviderInfo(info);
}
/**
* Determines if the current URI represents a default search engine results
* page (SERP) and retrieves the search terms used.
*
* @param {nsIURI} originalURI
* The fallback URI to check. Used if `currentURI` is not provided or if
* conditions require fallback.
* @param {nsIURI} currentURI
* The primary URI that is checked to determine if it matches the expected
* structure of a default SERP.
* @returns {string}
* The search terms used.
* Will return an empty string if it's not a default SERP, the search term
* looks too similar to a URL, the string exceeds the maximum characters,
* or the default engine hasn't been initialized.
*/
getSearchTermIfDefaultSerpUri(originalURI, currentURI) {
if (
!Services.search.hasSuccessfullyInitialized ||
(!originalURI && !currentURI)
) {
return "";
}
if (!originalURI) {
originalURI = currentURI;
}
if (!currentURI) {
currentURI = originalURI;
}
// Avoid inspecting URIs if they are non-http(s).
if (!/^https?:\/\//.test(originalURI.spec)) {
return "";
}
// Since we may have to use both URIs ensure they are similar.
if (
originalURI.prePath !== currentURI.prePath ||
originalURI.filePath !== currentURI.filePath
) {
return "";
}
let searchTerm = "";
// If we have a provider, use the current URI because we have special
// treatment for the case.
let provider = this.#getProviderInfoForURL(currentURI.spec);
if (provider) {
// Pass the URI but don't do strict comparisons of params as we expect
// the params to be different from the url generated by the Engine.
searchTerm = Services.search.defaultEngine.searchTermFromResult(
currentURI,
true
);
// If we don't get a search term, it's because we're not on the default
// search engine results page or no search term could be recovered.
// If we do get a search term, we need to also inspect the parameters of
// of the URI to ensure we're on an all tab.
if (!searchTerm || !this.#shouldPersist(currentURI, provider)) {
return "";
}
} else {
// If we don't have a provider, the URI must precisely match what the
// default engine would've constructed.
searchTerm =
Services.search.defaultEngine.searchTermFromResult(originalURI);
}
if (!searchTerm || searchTerm.length > UrlbarUtils.MAX_TEXT_LENGTH) {
return "";
}
let searchTermWithSpacesRemoved = searchTerm.replaceAll(/\s/g, "");
// Check if the search string uses a commonly used URL protocol. This
// avoids doing a fixup if we already know it matches a URL. Additionally,
// it ensures neither http:// nor https:// will appear by themselves in
// UrlbarInput. This is important because http:// can be trimmed, which in
// the Persisted Search Terms case, will cause the UrlbarInput to appear
// blank.
if (
searchTermWithSpacesRemoved.startsWith("https://") ||
searchTermWithSpacesRemoved.startsWith("http://")
) {
return "";
}
// We pass the search term to URIFixup to determine if it could be
// interpreted as a URL, including typos in the scheme and/or the domain
// suffix. This is to prevent search terms from persisting in the Urlbar if
// they look too similar to a URL, but still allow phrases with periods
// that are unlikely to be a URL.
try {
let info = Services.uriFixup.getFixupURIInfo(
searchTermWithSpacesRemoved,
Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP
);
if (info.keywordAsSent) {
return searchTerm;
}
} catch (e) {}
return "";
}
async #onSettingsSync(event) {
let current = event.data?.current;
if (current) {
lazy.logger.debug("Update provider info due to Remote Settings sync.");
this.#originalProviderInfo = current;
this.#setSearchProviderInfo(current);
} else {
lazy.logger.debug(
"Ignoring Remote Settings sync data due to missing records."
);
}
Services.obs.notifyObservers(null, "urlbar-persisted-search-terms-synced");
}
/**
* Used to set the local version of the search provider information.
* This automatically maps the regexps to RegExp objects so that
* we don't have to create a new instance each time.
*
* @param {Array} providerInfo
* A raw array of provider information to set.
*/
#setSearchProviderInfo(providerInfo) {
this.#searchProviderInfo = providerInfo.map(provider => {
let newProvider = {
...provider,
searchPageRegexp: new RegExp(provider.searchPageRegexp),
};
return newProvider;
});
}
/**
* Searches for provider information for a given url.
*
* @param {string} url The url to match for a provider.
* @returns {Array | null} Returns an array of provider name and the provider
* information.
*/
#getProviderInfoForURL(url) {
return this.#searchProviderInfo.find(info =>
info.searchPageRegexp.test(url)
);
}
/**
* Determines whether the search terms in the provided URL should be persisted
* based on predefined criteria.
*
* @param {nsIURI} currentURI
* The current URI
* @param {Array} provider
* An array of provider information
* @returns {string | null} Returns null if there is no provider match, an
* empty string if search terms should not be persisted, or the value of the
* first matched query parameter to be persisted.
*/
#shouldPersist(currentURI, provider) {
let searchParams;
try {
searchParams = new URL(currentURI.spec).searchParams;
} catch (ex) {
return false;
}
if (provider.includeParams) {
let foundMatch = false;
for (let param of provider.includeParams) {
// The param might not be present on page load.
if (param.canBeMissing && !searchParams.has(param.key)) {
foundMatch = true;
break;
}
// If we didn't provide a specific param value,
// the presence of the name is sufficient.
if (searchParams.has(param.key) && !param.values?.length) {
foundMatch = true;
break;
}
let value = searchParams.get(param.key);
// The param name and value must be present.
if (value && param?.values.includes(value)) {
foundMatch = true;
break;
}
}
if (!foundMatch) {
return false;
}
}
if (provider.excludeParams) {
for (let param of provider.excludeParams) {
let value = searchParams.get(param.key);
// If we found a value for a key but didn't
// provide a specific value to match.
if (!param.values?.length && value) {
return false;
}
// If we provided a value and it was present.
if (param.values?.includes(value)) {
return false;
}
}
}
return true;
}
}
export var UrlbarSearchTermsPersistence = new _UrlbarSearchTermsPersistence();