There's two main changes: - Change `getSearchTermIfDefaultSerpUri` to enable it work for any app-provided engine. - Show the correct Search Chicklet / Unified Search Mode Chicklet when the user loads a non-default SERP. Recall that Persisted Search is enabled in Nightly independent of Scotch Bonnet. **Issue 1:** `this.searchMode` is not always known or consistent with the non-default search page when `setURI` is called. Some scenarios: - Clear search mode, switch tab, return to the original. The absence of search mode seems acceptable because the user was potentially in the middle of modifying their search or navigating to a URL. - Clear search mode and reload a non-default SERP. - Switch search mode, but reload non-default SERP. - Go back/forward in tabhistory to return to a non-default SERP. In the latter three cases, if the search terms were to persist again, I'd expect the most accurate search mode to be present. **Issue 2:** `userTypedValue` has been used as an indicator for whether or not search terms should persist. However, entering search mode causes userTypedValue to have a value, so that it can be restored when switching back to the current tab and across sessions. Thus, on blur, I have a slightly more complicated check to ensure the revert button is consistent regardless of whether the engine is default or non-default. **Issue 3:** Lastly, it's possible for the revert button to show up when switching from tab to tab while the address bar is focused. I added an additional check in this patch since it's a small addition, but I can move it out if it overcomplicates things. I'm not 100% confident my solution is ideal since I ended up having to add hacky awaits in `browser_UrlbarInput_searchTerms_searchMode.js` for the search mode to appear... Differential Revision: https://phabricator.services.mozilla.com/D222846
327 lines
10 KiB
JavaScript
327 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 URIs represent an application provided 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.
|
|
*/
|
|
getSearchTerm(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, we have specific rules for dealing and can
|
|
// understand changes to params.
|
|
let provider = this.#getProviderInfoForURL(currentURI.spec);
|
|
if (provider) {
|
|
let result = Services.search.parseSubmissionURL(currentURI.spec);
|
|
if (
|
|
!result.engine?.isAppProvided ||
|
|
!this.#shouldPersist(currentURI, provider)
|
|
) {
|
|
return "";
|
|
}
|
|
searchTerm = result.terms;
|
|
} else {
|
|
// For all other providers, we use originalURI, because it doesn't change
|
|
// and if it matches what the Engine would've generated, it means it's
|
|
// a SERP.
|
|
let result = Services.search.parseSubmissionURL(originalURI.spec);
|
|
if (!result.engine?.isAppProvided) {
|
|
return "";
|
|
}
|
|
searchTerm = result.engine.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();
|