The main goal is to simplify the logic in setURI, and create a more easier to understand flow of operations. It involves making Persisted Search more contained, offload some responsibilities to UrlbarSearchTermsPersistence, and store fewer state variables than the patch that was first reviewed. I also simplified code in UrlbarSearchTermsPersistence. **How Search Terms Persist:** **Step 1** When the page first appears, set state for search terms persistence. First load doesn't necessarily mean no tab switch or same page load, as a user could load a browser in the background tab. In that latter case, the first "view" may occur on a tab switch and the state won't exist because `setURI` will never have been called with that browser object being selected. Once the state has been set, the only property that could be updated is `shouldPersist`. Otherwise, don't update the state object unless a full new page load occurs. **Step 2** Call `shouldPersist`. This has all the logic for when search terms should persist and gets called anytime `setURI` is called. The conditions in which we shouldn't persist: - There's no reason to show search terms: - The persist state is missing. - There are no search terms extracted from the URL. - We need to temporarily hide the search terms, such as by handleRevert - `hideSearchTerms` is provided to setURI - There is an user typed value and it differs from the search terms - A single page application moved to a secondary page - After the initial page load, there's the search mode (or lack of search mode) differs from the persist engine. **Step 3** Determine if we need to update `this.window.gBrowser.userTypedValue`. If we're going to persist, set `userTypedValue` to the search terms, which will keep the search mode and non-search mode state consistent. This'll also enable the ability to reuse existing logic that determines how to set `value`. On same page loads, when switching from persist to non-persist, and search term equals the userTypedValue, then `null` the userTypedValue so that the URL shows. **Step 4** In the area of setURI which has logic for search mode, if we persist, ensure the search mode (or lack of search mode) is consistent with the engine. If it is, do nothing. If it isn't, either nullify the search mode or enter search mode. Differential Revision: https://phabricator.services.mozilla.com/D224520
438 lines
13 KiB
JavaScript
438 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;
|
|
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();
|