435 lines
13 KiB
JavaScript
435 lines
13 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 URIs represent an application provided search
|
|
* engine results page (SERP) and retrieves the search terms used.
|
|
*
|
|
* @param {nsIURI} uri
|
|
* 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.
|
|
*/
|
|
getSearchTerm(uri) {
|
|
if (!Services.search.hasSuccessfullyInitialized || !uri?.spec) {
|
|
return "";
|
|
}
|
|
|
|
// Avoid inspecting URIs if they are non-http(s).
|
|
if (!/^https?:\/\//.test(uri.spec)) {
|
|
return "";
|
|
}
|
|
|
|
let searchTerm = "";
|
|
|
|
// If we have a provider, we have specific rules for dealing and can
|
|
// understand changes to params.
|
|
let provider = this.#getProviderInfoForURL(uri.spec);
|
|
if (provider) {
|
|
let result = Services.search.parseSubmissionURL(uri.spec);
|
|
if (!result.engine?.isAppProvided || !this.isDefaultPage(uri, provider)) {
|
|
return "";
|
|
}
|
|
searchTerm = result.terms;
|
|
} else {
|
|
let result = Services.search.parseSubmissionURL(uri.spec);
|
|
if (!result.engine?.isAppProvided) {
|
|
return "";
|
|
}
|
|
searchTerm = result.engine.searchTermFromResult(uri);
|
|
}
|
|
|
|
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 "";
|
|
}
|
|
|
|
shouldPersist(state, { uri, isSameDocument, userTypedValue, firstView }) {
|
|
let persist = state.persist;
|
|
if (!persist) {
|
|
return false;
|
|
}
|
|
|
|
// Don't persist if there are no search terms to show.
|
|
if (!persist.searchTerms) {
|
|
return false;
|
|
}
|
|
|
|
// If there is a userTypedValue and it differs from the search terms, the
|
|
// user must've modified the text.
|
|
if (userTypedValue && userTypedValue !== persist.searchTerms) {
|
|
return false;
|
|
}
|
|
|
|
// For some search engines, particularly single page applications, check
|
|
// if the URL matches a default search results page as page changes will
|
|
// occur within the same document.
|
|
if (
|
|
isSameDocument &&
|
|
state.persist.provider &&
|
|
!this.isDefaultPage(uri, state.persist.provider)
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
// The first page view will set the search mode but after that, the search
|
|
// mode could differ. Since persisting the search guarantees the correct
|
|
// search mode is shown, we don't want to undo changes the user could've
|
|
// done, like removing/adding the search mode.
|
|
if (
|
|
!firstView &&
|
|
!this.searchModeMatchesState(state.searchModes?.confirmed, state)
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Resets and assigns initial values for Search Terms Persistence state.
|
|
setPersistenceState(state, uri) {
|
|
state.persist = {
|
|
// Whether the engine that loaded the URI is the default search engine.
|
|
isDefaultEngine: null,
|
|
|
|
// The name of the engine that was used to load the URI.
|
|
originalEngineName: null,
|
|
|
|
// The search provider associated with the URI. If one exists, it means
|
|
// we have custom rules for this search provider to determine whether or
|
|
// not the URI corresponds to a default search engine results page.
|
|
provider: null,
|
|
|
|
// The search string within the URI.
|
|
searchTerms: this.getSearchTerm(uri),
|
|
|
|
// Whether the search terms should persist.
|
|
shouldPersist: null,
|
|
};
|
|
|
|
if (!state.persist.searchTerms) {
|
|
return;
|
|
}
|
|
|
|
let provider = this.#getProviderInfoForURL(uri?.spec);
|
|
// If we have specific Remote Settings defined providers for the URL,
|
|
// it's because changing the page won't clear the search terms unless we
|
|
// observe changes of the params in the URL.
|
|
if (provider) {
|
|
state.persist.provider = provider;
|
|
}
|
|
|
|
let result = this.#searchModeForUrl(uri.spec);
|
|
state.persist.originalEngineName = result.engineName;
|
|
state.persist.isDefaultEngine = result.isDefaultEngine;
|
|
}
|
|
|
|
/**
|
|
* Determines if search mode is in alignment with the persisted
|
|
* search state. Returns true in either of these cases:
|
|
*
|
|
* - The search mode engine is the same as the persisted engine.
|
|
* - There's no search mode, but the persisted engine is a default engine.
|
|
*
|
|
* @param {object} searchMode
|
|
* The search mode for the address bar.
|
|
* @param {object} state
|
|
* The address bar state associated with the browser.
|
|
* @returns {boolean}
|
|
*/
|
|
searchModeMatchesState(searchMode, state) {
|
|
if (searchMode?.engineName === state.persist?.originalEngineName) {
|
|
return true;
|
|
}
|
|
if (!searchMode && state.persist?.isDefaultEngine) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
onSearchModeChanged(window) {
|
|
let urlbar = window.gURLBar;
|
|
if (!urlbar) {
|
|
return;
|
|
}
|
|
let state = urlbar.getBrowserState(window.gBrowser.selectedBrowser);
|
|
if (!state?.persist) {
|
|
return;
|
|
}
|
|
|
|
// Exit search terms persistence when search mode changes and it's not
|
|
// consistent with the persisted engine.
|
|
if (
|
|
state.persist.shouldPersist &&
|
|
!this.searchModeMatchesState(state.searchModes?.confirmed, state)
|
|
) {
|
|
state.persist.shouldPersist = false;
|
|
urlbar.removeAttribute("persistsearchterms");
|
|
}
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
#searchModeForUrl(url) {
|
|
// If there's no default engine, no engines are available.
|
|
if (!Services.search.defaultEngine) {
|
|
return null;
|
|
}
|
|
let result = Services.search.parseSubmissionURL(url);
|
|
if (!result.engine?.isAppProvided) {
|
|
return null;
|
|
}
|
|
return {
|
|
engineName: result.engine.name,
|
|
isDefaultEngine: result.engine === Services.search.defaultEngine,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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 whether we find it's a default web SERP.
|
|
*
|
|
* @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.
|
|
*/
|
|
isDefaultPage(currentURI, provider) {
|
|
let { searchParams } = URL.fromURI(currentURI);
|
|
if (!searchParams.size) {
|
|
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();
|