1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 import { UrlbarUtils } from "resource:///modules/UrlbarUtils.sys.mjs";
9 ChromeUtils.defineESModuleGetters(lazy, {
10 RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
13 ChromeUtils.defineLazyGetter(lazy, "logger", () =>
14 UrlbarUtils.getLogger({ prefix: "UrlbarSearchTermsPersistence" })
17 const URLBAR_PERSISTENCE_SETTINGS_KEY = "urlbar-persisted-search-terms";
20 * Provides utilities to manage and validate search terms persistence in the URL
21 * bar. This class is designed to handle the identification of default search
22 * engine results pages (SERPs), retrieval of search terms, and validation of
23 * conditions for persisting search terms based on predefined provider
26 class _UrlbarSearchTermsPersistence {
27 // Whether or not this class is initialised.
30 // The original provider information, mainly used for tests.
31 #originalProviderInfo = [];
33 // The current search provider info.
34 #searchProviderInfo = [];
36 // An instance of remote settings that is used to access the provider info.
37 #urlbarSearchTermsPersistenceSettings;
39 // Callback used when syncing Urlbar Search Terms Persistence config settings.
40 #urlbarSearchTermsPersistenceSettingsSync;
43 if (this.#initialized) {
47 this.#urlbarSearchTermsPersistenceSettings = lazy.RemoteSettings(
48 URLBAR_PERSISTENCE_SETTINGS_KEY
50 let rawProviderInfo = [];
52 rawProviderInfo = await this.#urlbarSearchTermsPersistenceSettings.get();
54 lazy.logger.error("Could not get settings:", ex);
57 this.#urlbarSearchTermsPersistenceSettingsSync = event =>
58 this.#onSettingsSync(event);
59 this.#urlbarSearchTermsPersistenceSettings.on(
61 this.#urlbarSearchTermsPersistenceSettingsSync
64 this.#originalProviderInfo = rawProviderInfo;
65 this.#setSearchProviderInfo(rawProviderInfo);
67 this.#initialized = true;
71 if (!this.#initialized) {
76 this.#urlbarSearchTermsPersistenceSettings.off(
78 this.#urlbarSearchTermsPersistenceSettingsSync
82 "Failed to shutdown UrlbarSearchTermsPersistence Remote Settings.",
86 this.#urlbarSearchTermsPersistenceSettings = null;
87 this.#urlbarSearchTermsPersistenceSettingsSync = null;
89 this.#initialized = false;
92 getSearchProviderInfo() {
93 return this.#searchProviderInfo;
97 * Test-only function, used to override the provider information, so that
98 * unit tests can set it to easy to test values.
100 * @param {Array} providerInfo
101 * An array of provider information to set.
103 overrideSearchTermsPersistenceForTests(providerInfo) {
104 let info = providerInfo ? providerInfo : this.#originalProviderInfo;
105 this.#setSearchProviderInfo(info);
109 * Determines if the URIs represent an application provided search
110 * engine results page (SERP) and retrieves the search terms used.
112 * @param {nsIURI} uri
113 * The primary URI that is checked to determine if it matches the expected
114 * structure of a default SERP.
116 * The search terms used.
117 * Will return an empty string if it's not a default SERP, the search term
118 * looks too similar to a URL, the string exceeds the maximum characters,
119 * or the default engine hasn't been initialized.
122 if (!Services.search.hasSuccessfullyInitialized || !uri?.spec) {
126 // Avoid inspecting URIs if they are non-http(s).
127 if (!/^https?:\/\//.test(uri.spec)) {
133 // If we have a provider, we have specific rules for dealing and can
134 // understand changes to params.
135 let provider = this.#getProviderInfoForURL(uri.spec);
137 let result = Services.search.parseSubmissionURL(uri.spec);
138 if (!result.engine?.isAppProvided || !this.isDefaultPage(uri, provider)) {
141 searchTerm = result.terms;
143 let result = Services.search.parseSubmissionURL(uri.spec);
144 if (!result.engine?.isAppProvided) {
147 searchTerm = result.engine.searchTermFromResult(uri);
150 if (!searchTerm || searchTerm.length > UrlbarUtils.MAX_TEXT_LENGTH) {
154 let searchTermWithSpacesRemoved = searchTerm.replaceAll(/\s/g, "");
156 // Check if the search string uses a commonly used URL protocol. This
157 // avoids doing a fixup if we already know it matches a URL. Additionally,
158 // it ensures neither http:// nor https:// will appear by themselves in
159 // UrlbarInput. This is important because http:// can be trimmed, which in
160 // the Persisted Search Terms case, will cause the UrlbarInput to appear
163 searchTermWithSpacesRemoved.startsWith("https://") ||
164 searchTermWithSpacesRemoved.startsWith("http://")
169 // We pass the search term to URIFixup to determine if it could be
170 // interpreted as a URL, including typos in the scheme and/or the domain
171 // suffix. This is to prevent search terms from persisting in the Urlbar if
172 // they look too similar to a URL, but still allow phrases with periods
173 // that are unlikely to be a URL.
175 let info = Services.uriFixup.getFixupURIInfo(
176 searchTermWithSpacesRemoved,
177 Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
178 Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP
180 if (info.keywordAsSent) {
188 shouldPersist(state, { uri, isSameDocument, userTypedValue, firstView }) {
189 let persist = state.persist;
194 // Don't persist if there are no search terms to show.
195 if (!persist.searchTerms) {
199 // If there is a userTypedValue and it differs from the search terms, the
200 // user must've modified the text.
201 if (userTypedValue && userTypedValue !== persist.searchTerms) {
205 // For some search engines, particularly single page applications, check
206 // if the URL matches a default search results page as page changes will
207 // occur within the same document.
210 state.persist.provider &&
211 !this.isDefaultPage(uri, state.persist.provider)
216 // The first page view will set the search mode but after that, the search
217 // mode could differ. Since persisting the search guarantees the correct
218 // search mode is shown, we don't want to undo changes the user could've
219 // done, like removing/adding the search mode.
222 !this.searchModeMatchesState(state.searchModes?.confirmed, state)
230 // Resets and assigns initial values for Search Terms Persistence state.
231 setPersistenceState(state, uri) {
233 // Whether the engine that loaded the URI is the default search engine.
234 isDefaultEngine: null,
236 // The name of the engine that was used to load the URI.
237 originalEngineName: null,
239 // The search provider associated with the URI. If one exists, it means
240 // we have custom rules for this search provider to determine whether or
241 // not the URI corresponds to a default search engine results page.
244 // The search string within the URI.
245 searchTerms: this.getSearchTerm(uri),
247 // Whether the search terms should persist.
251 if (!state.persist.searchTerms) {
255 let provider = this.#getProviderInfoForURL(uri?.spec);
256 // If we have specific Remote Settings defined providers for the URL,
257 // it's because changing the page won't clear the search terms unless we
258 // observe changes of the params in the URL.
260 state.persist.provider = provider;
263 let result = this.#searchModeForUrl(uri.spec);
264 state.persist.originalEngineName = result.engineName;
265 state.persist.isDefaultEngine = result.isDefaultEngine;
269 * Determines if search mode is in alignment with the persisted
270 * search state. Returns true in either of these cases:
272 * - The search mode engine is the same as the persisted engine.
273 * - There's no search mode, but the persisted engine is a default engine.
275 * @param {object} searchMode
276 * The search mode for the address bar.
277 * @param {object} state
278 * The address bar state associated with the browser.
281 searchModeMatchesState(searchMode, state) {
282 if (searchMode?.engineName === state.persist?.originalEngineName) {
285 if (!searchMode && state.persist?.isDefaultEngine) {
291 onSearchModeChanged(window) {
292 let urlbar = window.gURLBar;
296 let state = urlbar.getBrowserState(window.gBrowser.selectedBrowser);
297 if (!state?.persist) {
301 // Exit search terms persistence when search mode changes and it's not
302 // consistent with the persisted engine.
304 state.persist.shouldPersist &&
305 !this.searchModeMatchesState(state.searchModes?.confirmed, state)
307 state.persist.shouldPersist = false;
308 urlbar.removeAttribute("persistsearchterms");
312 async #onSettingsSync(event) {
313 let current = event.data?.current;
315 lazy.logger.debug("Update provider info due to Remote Settings sync.");
316 this.#originalProviderInfo = current;
317 this.#setSearchProviderInfo(current);
320 "Ignoring Remote Settings sync data due to missing records."
323 Services.obs.notifyObservers(null, "urlbar-persisted-search-terms-synced");
326 #searchModeForUrl(url) {
327 // If there's no default engine, no engines are available.
328 if (!Services.search.defaultEngine) {
331 let result = Services.search.parseSubmissionURL(url);
332 if (!result.engine?.isAppProvided) {
336 engineName: result.engine.name,
337 isDefaultEngine: result.engine === Services.search.defaultEngine,
342 * Used to set the local version of the search provider information.
343 * This automatically maps the regexps to RegExp objects so that
344 * we don't have to create a new instance each time.
346 * @param {Array} providerInfo
347 * A raw array of provider information to set.
349 #setSearchProviderInfo(providerInfo) {
350 this.#searchProviderInfo = providerInfo.map(provider => {
353 searchPageRegexp: new RegExp(provider.searchPageRegexp),
360 * Searches for provider information for a given url.
362 * @param {string} url The url to match for a provider.
363 * @returns {Array | null} Returns an array of provider name and the provider
366 #getProviderInfoForURL(url) {
367 return this.#searchProviderInfo.find(info =>
368 info.searchPageRegexp.test(url)
373 * Determines whether the search terms in the provided URL should be persisted
374 * based on whether we find it's a default web SERP.
376 * @param {nsIURI} currentURI
378 * @param {Array} provider
379 * An array of provider information
380 * @returns {string | null} Returns null if there is no provider match, an
381 * empty string if search terms should not be persisted, or the value of the
382 * first matched query parameter to be persisted.
384 isDefaultPage(currentURI, provider) {
387 searchParams = new URL(currentURI.spec).searchParams;
391 if (provider.includeParams) {
392 let foundMatch = false;
393 for (let param of provider.includeParams) {
394 // The param might not be present on page load.
395 if (param.canBeMissing && !searchParams.has(param.key)) {
400 // If we didn't provide a specific param value,
401 // the presence of the name is sufficient.
402 if (searchParams.has(param.key) && !param.values?.length) {
407 let value = searchParams.get(param.key);
408 // The param name and value must be present.
409 if (value && param?.values.includes(value)) {
419 if (provider.excludeParams) {
420 for (let param of provider.excludeParams) {
421 let value = searchParams.get(param.key);
422 // If we found a value for a key but didn't
423 // provide a specific value to match.
424 if (!param.values?.length && value) {
427 // If we provided a value and it was present.
428 if (param.values?.includes(value)) {
437 export var UrlbarSearchTermsPersistence = new _UrlbarSearchTermsPersistence();