/* 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/. */ /** * Various utilities for search related UI. */ /** * @import { SearchUtils } from "moz-src:///toolkit/components/search/SearchUtils.sys.mjs" * @import { UrlbarInput } from "chrome://browser/content/urlbar/UrlbarInput.mjs" * @import { SearchEngine } from "moz-src:///toolkit/components/search/SearchEngine.sys.mjs" */ import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; import { AboutNewTabComponentRegistry, BaseAboutNewTabComponentRegistrant, } from "moz-src:///browser/components/newtab/AboutNewTabComponents.sys.mjs"; const lazy = XPCOMUtils.declareLazy({ BrowserSearchTelemetry: "moz-src:///browser/components/search/BrowserSearchTelemetry.sys.mjs", BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", CustomizableUI: "moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs", PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", SearchEngineInstallError: "moz-src:///toolkit/components/search/SearchUtils.sys.mjs", SearchService: "moz-src:///toolkit/components/search/SearchService.sys.mjs", SearchUtils: "moz-src:///toolkit/components/search/SearchUtils.sys.mjs", SearchUIUtilsL10n: () => { return new Localization(["browser/search.ftl", "branding/brand.ftl"]); }, }); export var SearchUIUtils = { initialized: false, init() { if (!this.initialized) { Services.obs.addObserver(this, "browser-search-engine-modified"); this.initialized = true; } }, /** * @param {{wrappedJSObject: SearchEngine}} subject * @param {"browser-search-engine-modified"} topic * @param {string} data */ observe(subject, topic, data) { switch (data) { case "engine-default": this.updatePlaceholderNamePreference(subject.wrappedJSObject, false); break; case "engine-default-private": this.updatePlaceholderNamePreference(subject.wrappedJSObject, true); break; } }, /** * This function is called by the category manager for the * `search-service-notification` category. * * It allows the SearchService (in toolkit) to display * notifications in the browser for certain events. * * @param {string} notificationType * Determines the function displaying the notification. * @param {...any} args * The arguments for that function. */ showSearchServiceNotification(notificationType, ...args) { switch (notificationType) { case "search-engine-removal": { let [oldEngine, newEngine] = args; this.removalOfSearchEngineNotificationBox(oldEngine, newEngine); break; } case "search-settings-reset": { let [newEngine] = args; this.searchSettingsResetNotificationBox(newEngine); break; } } }, /** * Infobar to notify the user's search engine has been removed * and replaced with an application default search engine. * * @param {string} oldEngine * name of the engine to be moved and replaced. * @param {string} newEngine * name of the application default engine to replaced the removed engine. */ async removalOfSearchEngineNotificationBox(oldEngine, newEngine) { let win = lazy.BrowserWindowTracker.getTopWindow({ allowFromInactiveWorkspace: true, }); let buttons = [ { "l10n-id": "remove-search-engine-button", primary: true, callback() { const notificationBox = win.gNotificationBox.getNotificationWithValue( "search-engine-removal" ); win.gNotificationBox.removeNotification(notificationBox); }, }, { supportPage: "search-engine-removal", }, ]; await win.gNotificationBox.appendNotification( "search-engine-removal", { label: { "l10n-id": "removed-search-engine-message2", "l10n-args": { oldEngine, newEngine }, }, priority: win.gNotificationBox.PRIORITY_SYSTEM, }, buttons ); // _updatePlaceholderFromDefaultEngine only updates the pref if the search service // hasn't finished initializing, so we explicitly update it here to be sure. SearchUIUtils.updatePlaceholderNamePreference( await lazy.SearchService.getDefault(), false ); SearchUIUtils.updatePlaceholderNamePreference( await lazy.SearchService.getDefaultPrivate(), true ); for (let openWin of lazy.BrowserWindowTracker.orderedWindows) { openWin.gURLBar ?._updatePlaceholderFromDefaultEngine() .catch(console.error); } }, /** * Infobar informing the user that the search settings had to be reset * and what their new default engine is. * * @param {string} newEngine * Name of the new default engine. */ async searchSettingsResetNotificationBox(newEngine) { let win = lazy.BrowserWindowTracker.getTopWindow({ allowFromInactiveWorkspace: true, }); let buttons = [ { "l10n-id": "reset-search-settings-button", primary: true, callback() { const notificationBox = win.gNotificationBox.getNotificationWithValue( "search-settings-reset" ); win.gNotificationBox.removeNotification(notificationBox); }, }, { supportPage: "prefs-search", }, ]; await win.gNotificationBox.appendNotification( "search-settings-reset", { label: { "l10n-id": "reset-search-settings-message", "l10n-args": { newEngine }, }, priority: win.gNotificationBox.PRIORITY_SYSTEM, }, buttons ); }, /** * Adds an open search engine and handles error UI. * * @param {string} locationURL * The URL where the OpenSearch definition is located. * @param {string} image * A URL string to an icon file to be used as the search engine's * icon. This value may be overridden by an icon specified in the * engine description file. * @param {object} browsingContext * The browsing context any error prompt should be opened for. * @returns {Promise} * Returns true if the engine was added. */ async addOpenSearchEngine(locationURL, image, browsingContext) { try { await lazy.SearchService.addOpenSearchEngine( locationURL, image, browsingContext?.embedderElement?.contentPrincipal?.originAttributes ); } catch (ex) { // Use a general download error message, unless we have something more // specific. let titleMsgName = "opensearch-error-download-title"; let descMsgName = "opensearch-error-download-desc"; if (ex instanceof lazy.SearchEngineInstallError) { switch (ex.type) { case "duplicate-title": titleMsgName = "opensearch-error-duplicate-title"; descMsgName = "opensearch-error-duplicate-desc"; break; case "corrupted": titleMsgName = "opensearch-error-format-title"; descMsgName = "opensearch-error-format-desc"; break; default: // e.g. download failure, use the more general message. } } let [title, text] = await lazy.SearchUIUtilsL10n.formatValues([ { id: titleMsgName, }, { id: descMsgName, args: { "location-url": locationURL, }, }, ]); Services.prompt.alertBC( browsingContext, Ci.nsIPrompt.MODAL_TYPE_CONTENT, title, text ); return false; } return true; }, /** * Returns the URL to use for where to get more search engines. * * @returns {string} */ get searchEnginesURL() { return Services.urlFormatter.formatURLPref( "browser.search.searchEnginesURL" ); }, /** * Update the placeholderName preference for the default search engine. * * @param {SearchEngine} engine The new default search engine. * @param {boolean} isPrivate Whether this change applies to private windows. */ updatePlaceholderNamePreference(engine, isPrivate) { const prefName = "browser.urlbar.placeholderName" + (isPrivate ? ".private" : ""); if (engine.isConfigEngine) { Services.prefs.setStringPref(prefName, engine.name); } else { Services.prefs.clearUserPref(prefName); } }, /** * Focuses the search bar if present on the toolbar, or the address bar, * putting it in search mode. Will do so in an existing non-popup browser * window or open a new one if necessary. * * @param {WindowProxy} window * The window where the seach was triggered. */ webSearch(window) { if ( window.location.href != AppConstants.BROWSER_CHROME_URL || window.gURLBar.readOnly ) { let topWindow = lazy.BrowserWindowTracker.getTopWindow(); if (topWindow && !topWindow.gURLBar.readOnly) { // If there's an open browser window, it should handle this command. topWindow.focus(); SearchUIUtils.webSearch(topWindow); } else { // If there are no open browser windows, open a new one. let newWindow = window.openDialog( AppConstants.BROWSER_CHROME_URL, "_blank", "chrome,all,dialog=no", "about:blank" ); let observer = subject => { if (subject == newWindow) { SearchUIUtils.webSearch(newWindow); Services.obs.removeObserver( observer, "browser-delayed-startup-finished" ); } }; Services.obs.addObserver(observer, "browser-delayed-startup-finished"); } return; } /** @type {(searchBar: MozSearchbar | UrlbarInput) => void} */ let focusUrlBarIfSearchFieldIsNotActive = function (searchBar) { if (!searchBar || window.document.activeElement != searchBar.inputField) { // Limit the results to search suggestions, like the search bar. window.gURLBar.searchModeShortcut(); } }; let searchBar = /** @type {MozSearchbar | UrlbarInput} */ ( window.document.getElementById( Services.prefs.getBoolPref("browser.search.widget.new") ? "searchbar-new" : "searchbar" ) ); let placement = lazy.CustomizableUI.getPlacementOfWidget("search-container"); let focusSearchBar = () => { searchBar = /** @type {MozSearchbar | UrlbarInput} */ ( window.document.getElementById( Services.prefs.getBoolPref("browser.search.widget.new") ? "searchbar-new" : "searchbar" ) ); searchBar.select(); focusUrlBarIfSearchFieldIsNotActive(searchBar); }; if ( placement && searchBar && ((searchBar.parentElement.getAttribute("overflowedItem") == "true" && placement.area == lazy.CustomizableUI.AREA_NAVBAR) || placement.area == lazy.CustomizableUI.AREA_FIXED_OVERFLOW_PANEL) ) { let navBar = window.document.getElementById( lazy.CustomizableUI.AREA_NAVBAR ); // @ts-expect-error - Navbar receives the overflowable property upon registration. navBar.overflowable.show().then(focusSearchBar); return; } if (searchBar) { if (window.fullScreen) { window.FullScreen.showNavToolbox(); } searchBar.select(); } focusUrlBarIfSearchFieldIsNotActive(searchBar); }, /** * Opens a search results page, given a set of search terms. * * @param {object} options * Options objects. * @param {WindowProxy} options.window * The window where the search was triggered. * @param {string} options.searchText * The search terms to use for the search. * @param {?string} [options.where] * String indicating where the search should load. Most commonly used * are ``tab`` or ``window``, defaults to ``current``. * @param {boolean} [options.usePrivateWindow] * Whether to open the window in private browsing mode (if opening a window). * Defaults to the type of window that ``options.window` is. * @param {nsIPrincipal} options.triggeringPrincipal * The principal to use for a new window or tab. * @param {nsIPolicyContainer} [options.policyContainer] * The policyContainer to use for a new window or tab. * @param {boolean} [options.inBackground] * Set to true for the tab to be loaded in the background. * @param {?SearchEngine} [options.engine] * The search engine to use for the search. If not supplied, this will default * to the default search engine for normal or private mode, depending on * ``options.usePrivateWindow``. * @param {?MozTabbrowserTab} [options.tab] * The tab to show the search result. * @param {?Values} [options.searchUrlType] * A `SearchUtils.URL_TYPE` value indicating the type of search that should * be performed. A falsey value is equivalent to * `SearchUtils.URL_TYPE.SEARCH`, which will perform a usual web search. * @param {keyof typeof lazy.BrowserSearchTelemetry.KNOWN_SEARCH_SOURCES} options.sapSource * The search access point source. */ async loadSearch({ window, searchText, where, usePrivateWindow = lazy.PrivateBrowsingUtils.isWindowPrivate(window), triggeringPrincipal, policyContainer, inBackground = false, engine, tab, searchUrlType, sapSource, }) { if (!triggeringPrincipal) { throw new Error( "Required argument triggeringPrincipal missing within loadSearch" ); } if (!engine) { engine = usePrivateWindow ? await lazy.SearchService.getDefaultPrivate() : await lazy.SearchService.getDefault(); } let submission = engine.getSubmission(searchText, searchUrlType); // getSubmission can return null if the engine doesn't have a URL // for the given response type. This is an error if it occurs, since // we should only get here if the engine supports the URL type begin // passed. if (!submission) { throw new Error(`No submission URL found for ${searchUrlType}`); } window.openLinkIn(submission.uri.spec, where || "current", { private: usePrivateWindow, postData: submission.postData, inBackground, relatedToCurrent: true, triggeringPrincipal, policyContainer, targetBrowser: tab?.linkedBrowser, globalHistoryOptions: { triggeringSearchEngine: engine.name, }, }); lazy.BrowserSearchTelemetry.recordSearch( window.gBrowser.selectedBrowser, engine, sapSource, { searchUrlType } ); }, /** * Perform a search initiated from the context menu. * Note: This should only be called from the context menu. * * @param {object} options * Options object. * @param {SearchEngine} options.engine * The engine to search with. * @param {WindowProxy} options.window * The window where the search was triggered. * @param {string} options.searchText * The search terms to use for the search. * @param {boolean} [options.usePrivateWindow] * Whether to open the window in private browsing mode (if opening a window). * Defaults to the type of window that ``options.window` is. * @param {nsIPrincipal} options.triggeringPrincipal * The principal of the document whose context menu was clicked. * @param {nsIPolicyContainer} options.policyContainer * The policyContainer to use for a new window or tab. * @param {XULCommandEvent|PointerEvent} options.event * The event triggering the search. * @param {?Values} [options.searchUrlType] * A `SearchUtils.URL_TYPE` value indicating the type of search that should * be performed. A falsey value is equivalent to * `SearchUtils.URL_TYPE.SEARCH` and will perform a usual web search. */ async loadSearchFromContext({ window, engine, searchText, usePrivateWindow, triggeringPrincipal, policyContainer, event, searchUrlType = null, }) { event = lazy.BrowserUtils.getRootEvent(event); let where = lazy.BrowserUtils.whereToOpenLink(event); if (where == "current") { // override: historically search opens in new tab where = "tab"; } if ( usePrivateWindow && !lazy.PrivateBrowsingUtils.isWindowPrivate(window) ) { where = "window"; } let inBackground = Services.prefs.getBoolPref( "browser.search.context.loadInBackground" ); if (event.button == 1 || event.ctrlKey) { inBackground = !inBackground; } return this.loadSearch({ window, engine, searchText, searchUrlType, where, usePrivateWindow, triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal( triggeringPrincipal.originAttributes ), policyContainer, inBackground, sapSource: searchUrlType == lazy.SearchUtils.URL_TYPE.VISUAL_SEARCH ? "contextmenu_visual" : "contextmenu", }); }, }; /** * A registrant that adds the handoff search bar to about:newtab / about:home. */ export class SearchNewTabComponentsRegistrant extends BaseAboutNewTabComponentRegistrant { constructor() { super(); // Wire up a lazy preference getter, primarily so that we have an easy way // of updating our external component registration if the pref changes. this.lazy = XPCOMUtils.declareLazy({ prefHandoffToAwesomebar: { pref: "browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar", default: true, onUpdate: () => { this.updated(); }, }, }); } getComponents() { const { caretBlinkCount, caretBlinkTime } = Services.appinfo; return [ { type: AboutNewTabComponentRegistry.TYPES.SEARCH, l10nURLs: [], componentURL: "chrome://browser/content/contentSearchHandoffUI.mjs", tagName: "content-search-handoff-ui", cssVariables: { "--caret-blink-count": caretBlinkCount > -1 ? caretBlinkCount : "infinite", "--caret-blink-time": caretBlinkTime > 0 ? `${caretBlinkTime * 2}ms` : `${1134}ms`, }, attributes: { nonhandoff: !this.lazy.prefHandoffToAwesomebar, }, }, ]; } }