diff options
Diffstat (limited to '')
-rw-r--r-- | browser/components/urlbar/UrlbarPrefs.sys.mjs | 1536 |
1 files changed, 1536 insertions, 0 deletions
diff --git a/browser/components/urlbar/UrlbarPrefs.sys.mjs b/browser/components/urlbar/UrlbarPrefs.sys.mjs new file mode 100644 index 0000000000..17f4d534b4 --- /dev/null +++ b/browser/components/urlbar/UrlbarPrefs.sys.mjs @@ -0,0 +1,1536 @@ +/* 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/. */ + +/** + * This module exports the UrlbarPrefs singleton, which manages preferences for + * the urlbar. It also provides access to urlbar Nimbus variables as if they are + * preferences, but only for variables with fallback prefs. + */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + Region: "resource://gre/modules/Region.sys.mjs", + TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.sys.mjs", + UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + NimbusFeatures: "resource://nimbus/ExperimentAPI.jsm", +}); + +const PREF_URLBAR_BRANCH = "browser.urlbar."; + +// Prefs are defined as [pref name, default value] or [pref name, [default +// value, type]]. In the former case, the getter method name is inferred from +// the typeof the default value. +// +// NOTE: Don't name prefs (relative to the `browser.urlbar` branch) the same as +// Nimbus urlbar features. Doing so would cause a name collision because pref +// names and Nimbus feature names are both kept as keys in UrlbarPref's map. For +// a list of Nimbus features, see: toolkit/components/nimbus/FeatureManifest.js +const PREF_URLBAR_DEFAULTS = new Map([ + // Whether we announce to screen readers when tab-to-search results are + // inserted. + ["accessibility.tabToSearch.announceResults", true], + + // "Autofill" is the name of the feature that automatically completes domains + // and URLs that the user has visited as the user is typing them in the urlbar + // textbox. If false, autofill will be disabled. + ["autoFill", true], + + // Whether enabling adaptive history autofill. This pref is a fallback for the + // Nimbus variable `autoFillAdaptiveHistoryEnabled`. + ["autoFill.adaptiveHistory.enabled", false], + + // Minimum char length of the user's search string to enable adaptive history + // autofill. This pref is a fallback for the Nimbus variable + // `autoFillAdaptiveHistoryMinCharsThreshold`. + ["autoFill.adaptiveHistory.minCharsThreshold", 0], + + // Threshold for use count of input history that we handle as adaptive history + // autofill. If the use count is this value or more, it will be a candidate. + // Set the threshold to not be candidate the input history passed approximately + // 30 days since user input it as the default. + ["autoFill.adaptiveHistory.useCountThreshold", [0.47, "float"]], + + // If true, the domains of the user's installed search engines will be + // autofilled even if the user hasn't actually visited them. + ["autoFill.searchEngines", false], + + // Affects the frecency threshold of the autofill algorithm. The threshold is + // the mean of all origin frecencies plus one standard deviation multiplied by + // this value. See UrlbarProviderPlaces. + ["autoFill.stddevMultiplier", [0.0, "float"]], + + // Whether best match results can be blocked. This pref is a fallback for the + // Nimbus variable `bestMatchBlockingEnabled`. + ["bestMatch.blockingEnabled", false], + + // Whether the best match feature is enabled. + ["bestMatch.enabled", true], + + // Whether to show a link for using the search functionality provided by the + // active view if the the view utilizes OpenSearch. + ["contextualSearch.enabled", false], + + // Whether using `ctrl` when hitting return/enter in the URL bar + // (or clicking 'go') should prefix 'www.' and suffix + // browser.fixup.alternate.suffix to the URL bar value prior to + // navigating. + ["ctrlCanonizesURLs", true], + + // Whether copying the entire URL from the location bar will put a human + // readable (percent-decoded) URL on the clipboard. + ["decodeURLsOnCopy", false], + + // The amount of time (ms) to wait after the user has stopped typing before + // fetching results. However, we ignore this for the very first result (the + // "heuristic" result). We fetch it as fast as possible. + ["delay", 50], + + // Some performance tests disable this because extending the urlbar needs + // layout information that we can't get before the first paint. (Or we could + // but this would mean flushing layout.) + ["disableExtendForTests", false], + + // Ensure we use trailing dots for DNS lookups for single words that could + // be hosts. + ["dnsResolveFullyQualifiedNames", true], + + // Controls when to DNS resolve single word search strings, after they were + // searched for. If the string is resolved as a valid host, show a + // "Did you mean to go to 'host'" prompt. + // 0 - never resolve; 1 - use heuristics (default); 2 - always resolve + ["dnsResolveSingleWordsAfterSearch", 0], + + // Whether telemetry events should be recorded. + ["eventTelemetry.enabled", false], + + // Whether we expand the font size when when the urlbar is + // focused. + ["experimental.expandTextOnFocus", false], + + // Whether the heuristic result is hidden. + ["experimental.hideHeuristic", false], + + // Whether the urlbar displays a permanent search button. + ["experimental.searchButton", false], + + // When we send events to extensions, we wait this amount of time in + // milliseconds for them to respond before timing out. + ["extension.timeout", 400], + + // When true, `javascript:` URLs are not included in search results. + ["filter.javascript", true], + + // Applies URL highlighting and other styling to the text in the urlbar input. + ["formatting.enabled", true], + + // Whether search engagement telemetry should be recorded. This pref is a + // fallback for the Nimbus variable `searchEngagementTelemetryEnabled`. + ["searchEngagementTelemetry.enabled", false], + + // Interval time until taking pause impression telemetry. + ["searchEngagementTelemetry.pauseImpressionIntervalMs", 1000], + + // Whether Firefox Suggest group labels are shown in the urlbar view in en-* + // locales. Labels are not shown in other locales but likely will be in the + // future. + ["groupLabels.enabled", true], + + // Whether the results panel should be kept open during IME composition. + ["keepPanelOpenDuringImeComposition", false], + + // As a user privacy measure, don't fetch results from remote services for + // searches that start by pasting a string longer than this. The pref name + // indicates search suggestions, but this is used for all remote results. + ["maxCharsForSearchSuggestions", 100], + + // The maximum number of form history results to include. + ["maxHistoricalSearchSuggestions", 0], + + // The maximum number of results in the urlbar popup. + ["maxRichResults", 10], + + // Comma-separated list of client variants to send to Merino + ["merino.clientVariants", ""], + + // Whether Merino is enabled as a quick suggest source. + ["merino.enabled", false], + + // The Merino endpoint URL, not including parameters. + ["merino.endpointURL", "https://merino.services.mozilla.com/api/v1/suggest"], + + // Comma-separated list of providers to request from Merino + ["merino.providers", ""], + + // Timeout for Merino fetches (ms). + ["merino.timeoutMs", 200], + + // Whether addresses and search results typed into the address bar + // should be opened in new tabs by default. + ["openintab", false], + + // When true, URLs in the user's history that look like search result pages + // are styled to look like search engine results instead of the usual history + // results. + ["restyleSearches", false], + + // Controls the composition of results. The default value is computed by + // calling: + // makeResultGroups({ + // showSearchSuggestionsFirst: UrlbarPrefs.get( + // "showSearchSuggestionsFirst" + // ), + // }); + // The value of this pref is a JSON string of the root group. See below. + ["resultGroups", ""], + + // If true, we show tail suggestions when available. + ["richSuggestions.tail", true], + + // Hidden pref. Disables checks that prevent search tips being shown, thus + // showing them every time the newtab page or the default search engine + // homepage is opened. + ["searchTips.test.ignoreShowLimits", false], + + // Whether to show each local search shortcut button in the view. + ["shortcuts.bookmarks", true], + ["shortcuts.tabs", true], + ["shortcuts.history", true], + ["shortcuts.quickactions", false], + + // Whether to show search suggestions before general results. + ["showSearchSuggestionsFirst", true], + + // Global toggle for whether the show search terms feature + // can be used at all, and enabled/disabled by the user. + ["showSearchTerms.featureGate", false], + + // If true, show the search term in the Urlbar while on + // a default search engine results page. + ["showSearchTerms.enabled", true], + + // Whether speculative connections should be enabled. + ["speculativeConnect.enabled", true], + + // When `bestMatch.enabled` is true, this controls whether results will + // include best matches. + ["suggest.bestmatch", true], + + // Whether results will include the user's bookmarks. + ["suggest.bookmark", true], + + // Whether results will include a calculator. + ["suggest.calculator", false], + + // Whether results will include search engines (e.g. tab-to-search). + ["suggest.engines", true], + + // Whether results will include the user's history. + ["suggest.history", true], + + // Whether results will include switch-to-tab results. + ["suggest.openpage", true], + + // Whether results will include synced tab results. The syncing of open tabs + // must also be enabled, from Sync preferences. + ["suggest.remotetab", true], + + // Whether results will include QuickActions in the default search mode. + ["suggest.quickactions", false], + + // If disabled, QuickActions will not be included in either the default search + // mode or the QuickActions search mode. + ["quickactions.enabled", false], + + // Whether we show the Actions section in about:preferences. + ["quickactions.showPrefs", false], + + // Whether we will match QuickActions within a phrase and not only a prefix. + ["quickactions.matchInPhrase", true], + + // Whether we show QuickActions when in zero-prefix. + ["quickactions.showInZeroPrefix", false], + + // Show multiple actions in a random order. + ["quickactions.randomOrderActions", false], + + // Whether results will include non-sponsored quick suggest suggestions. + ["suggest.quicksuggest.nonsponsored", false], + + // Whether results will include sponsored quick suggest suggestions. + ["suggest.quicksuggest.sponsored", false], + + // Whether results will include search suggestions. + ["suggest.searches", false], + + // Whether results will include top sites and the view will open on focus. + ["suggest.topsites", true], + + // If `browser.urlbar.weather.featureGate` is true, this controls whether + // weather suggestions are turned on. + ["suggest.weather", true], + + // JSON'ed array of blocked quick suggest URL digests. + ["quicksuggest.blockedDigests", ""], + + // Whether the usual non-best-match quick suggest results can be blocked. This + // pref is a fallback for the Nimbus variable `quickSuggestBlockingEnabled`. + ["quicksuggest.blockingEnabled", false], + + // Global toggle for whether the quick suggest feature is enabled, i.e., + // sponsored and recommended results related to the user's search string. + ["quicksuggest.enabled", false], + + // Whether non-sponsored quick suggest results are subject to impression + // frequency caps. This pref is a fallback for the Nimbus variable + // `quickSuggestImpressionCapsNonSponsoredEnabled`. + ["quicksuggest.impressionCaps.nonSponsoredEnabled", false], + + // Whether sponsored quick suggest results are subject to impression frequency + // caps. This pref is a fallback for the Nimbus variable + // `quickSuggestImpressionCapsSponsoredEnabled`. + ["quicksuggest.impressionCaps.sponsoredEnabled", false], + + // JSON'ed object of quick suggest impression stats. Used for implementing + // impression frequency caps for quick suggest suggestions. + ["quicksuggest.impressionCaps.stats", ""], + + // The user's response to the Firefox Suggest online opt-in dialog. + ["quicksuggest.onboardingDialogChoice", ""], + + // If the user has gone through a quick suggest prefs migration, then this + // pref will have a user-branch value that records the latest prefs version. + // Version changelog: + // + // 0: (Unversioned) When `suggest.quicksuggest` is false, all quick suggest + // results are disabled and `suggest.quicksuggest.sponsored` is ignored. To + // show sponsored suggestions, both prefs must be true. + // + // 1: `suggest.quicksuggest` is removed, `suggest.quicksuggest.nonsponsored` + // is introduced. `suggest.quicksuggest.nonsponsored` and + // `suggest.quicksuggest.sponsored` are independent: + // `suggest.quicksuggest.nonsponsored` controls non-sponsored results and + // `suggest.quicksuggest.sponsored` controls sponsored results. + // `quicksuggest.dataCollection.enabled` is introduced. + // + // 2: For online, the defaults for `suggest.quicksuggest.nonsponsored` and + // `suggest.quicksuggest.sponsored` are true. Previously they were false. + ["quicksuggest.migrationVersion", 0], + + // Whether Remote Settings is enabled as a quick suggest source. + ["quicksuggest.remoteSettings.enabled", true], + + // The Firefox Suggest scenario in which the user is enrolled. This is set + // when the scenario is updated (see `updateFirefoxSuggestScenario`) and is + // not a pref the user should set. Once initialized, its value is one of: + // "history", "offline", "online" + ["quicksuggest.scenario", ""], + + // Whether the user has opted in to data collection for quick suggest. + ["quicksuggest.dataCollection.enabled", false], + + // The version of dialog user saw. + ["quicksuggest.onboardingDialogVersion", ""], + + // Whether to show the quick suggest onboarding dialog. + ["quicksuggest.shouldShowOnboardingDialog", true], + + // Whether the user has seen the onboarding dialog. + ["quicksuggest.showedOnboardingDialog", false], + + // Count the restarts before showing the onboarding dialog. + ["quicksuggest.seenRestarts", 0], + + // Whether quick suggest results can be shown in position specified in the + // suggestions. + ["quicksuggest.allowPositionInSuggestions", true], + + // Enable three-dot options button and menu for eligible results. + ["resultMenu", false], + + // When using switch to tabs, if set to true this will move the tab into the + // active window. + ["switchTabs.adoptIntoActiveWindow", false], + + // The number of remaining times the user can interact with tab-to-search + // onboarding results before we stop showing them. + ["tabToSearch.onboard.interactionsLeft", 3], + + // The number of times the user has been shown the onboarding search tip. + ["tipShownCount.searchTip_onboard", 0], + + // The number of times the user has been shown the urlbar persisted search tip. + ["tipShownCount.searchTip_persist", 0], + + // The number of times the user has been shown the redirect search tip. + ["tipShownCount.searchTip_redirect", 0], + + // Remove redundant portions from URLs. + ["trimURLs", true], + + // If true, top sites may include sponsored ones. + ["sponsoredTopSites", false], + + // Whether unit conversion is enabled. + ["unitConversion.enabled", false], + + // The index where we show unit conversion results. + ["unitConversion.suggestedIndex", 1], + + // Results will include a built-in set of popular domains when this is true. + ["usepreloadedtopurls.enabled", false], + + // After this many days from the profile creation date, the built-in set of + // popular domains will no longer be included in the results. + ["usepreloadedtopurls.expire_days", 14], + + // Controls the empty search behavior in Search Mode: + // 0 - Show nothing + // 1 - Show search history + // 2 - Show search and browsing history + ["update2.emptySearchBehavior", 0], + + // Feature gate pref for weather suggestions in the urlbar. + ["weather.featureGate", false], +]); +const PREF_OTHER_DEFAULTS = new Map([ + ["browser.fixup.dns_first_for_single_words", false], + ["browser.search.suggest.enabled", true], + ["browser.search.suggest.enabled.private", false], + ["browser.search.widget.inNavBar", false], + ["keyword.enabled", true], + ["ui.popup.disable_autohide", false], +]); + +// Default values for Nimbus urlbar variables that do not have fallback prefs. +// Variables with fallback prefs do not need to be defined here because their +// defaults are the values of their fallbacks. +const NIMBUS_DEFAULTS = { + experimentType: "", + isBestMatchExperiment: false, + quickSuggestRemoteSettingsDataType: "data", +}; + +// Maps preferences under browser.urlbar.suggest to behavior names, as defined +// in mozIPlacesAutoComplete. +const SUGGEST_PREF_TO_BEHAVIOR = { + history: "history", + bookmark: "bookmark", + openpage: "openpage", + searches: "search", +}; + +const PREF_TYPES = new Map([ + ["boolean", "Bool"], + ["float", "Float"], + ["number", "Int"], + ["string", "Char"], +]); + +/** + * Builds the standard result groups and returns the root group. Result + * groups determine the composition of results in the muxer, i.e., how they're + * grouped and sorted. Each group is an object that looks like this: + * + * { + * {UrlbarUtils.RESULT_GROUP} [group] + * This is defined only on groups without children, and it determines the + * result group that the group will contain. + * {number} [maxResultCount] + * An optional maximum number of results the group can contain. If it's + * not defined and the parent group does not define `flexChildren: true`, + * then the max is the parent's max. If the parent group defines + * `flexChildren: true`, then `maxResultCount` is ignored. + * {boolean} [flexChildren] + * If true, then child groups are "flexed", similar to flex in HTML. Each + * child group should define the `flex` property (or, if they don't, `flex` + * is assumed to be zero). `flex` is a number that defines the ratio of a + * child's result count to the total result count of all children. More + * specifically, `flex: X` on a child means that the initial maximum result + * count of the child is `parentMaxResultCount * (X / N)`, where `N` is the + * sum of the `flex` values of all children. If there are any child groups + * that cannot be completely filled, then the muxer will attempt to overfill + * the children that were completely filled, while still respecting their + * relative `flex` values. + * {number} [flex] + * The flex value of the group. This should be defined only on groups + * where the parent defines `flexChildren: true`. See `flexChildren` for a + * discussion of flex. + * {array} [children] + * An array of child group objects. + * } + * + * @param {boolean} showSearchSuggestionsFirst + * If true, the suggestions group will come before the general group. + * @returns {object} + * The root group. + */ +function makeResultGroups({ showSearchSuggestionsFirst }) { + let rootGroup = { + children: [ + // heuristic + { + maxResultCount: 1, + children: [ + { group: lazy.UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }, + { group: lazy.UrlbarUtils.RESULT_GROUP.HEURISTIC_EXTENSION }, + { group: lazy.UrlbarUtils.RESULT_GROUP.HEURISTIC_SEARCH_TIP }, + { group: lazy.UrlbarUtils.RESULT_GROUP.HEURISTIC_OMNIBOX }, + { group: lazy.UrlbarUtils.RESULT_GROUP.HEURISTIC_ENGINE_ALIAS }, + { group: lazy.UrlbarUtils.RESULT_GROUP.HEURISTIC_BOOKMARK_KEYWORD }, + { group: lazy.UrlbarUtils.RESULT_GROUP.HEURISTIC_AUTOFILL }, + { group: lazy.UrlbarUtils.RESULT_GROUP.HEURISTIC_PRELOADED }, + { group: lazy.UrlbarUtils.RESULT_GROUP.HEURISTIC_TOKEN_ALIAS_ENGINE }, + { group: lazy.UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK }, + ], + }, + // extensions using the omnibox API + { + group: lazy.UrlbarUtils.RESULT_GROUP.OMNIBOX, + availableSpan: lazy.UrlbarUtils.MAX_OMNIBOX_RESULT_COUNT - 1, + }, + ], + }; + + // Prepare the parent group for suggestions and general. + let mainGroup = { + flexChildren: true, + children: [ + // suggestions + { + children: [ + { + flexChildren: true, + children: [ + { + // If `maxHistoricalSearchSuggestions` == 0, the muxer forces + // `maxResultCount` to be zero and flex is ignored, per query. + flex: 2, + group: lazy.UrlbarUtils.RESULT_GROUP.FORM_HISTORY, + }, + { + flex: 4, + group: lazy.UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + }, + ], + }, + { + group: lazy.UrlbarUtils.RESULT_GROUP.TAIL_SUGGESTION, + }, + ], + }, + // general + { + group: lazy.UrlbarUtils.RESULT_GROUP.GENERAL_PARENT, + children: [ + { + availableSpan: 3, + group: lazy.UrlbarUtils.RESULT_GROUP.INPUT_HISTORY, + }, + { + flexChildren: true, + children: [ + { + flex: 1, + group: lazy.UrlbarUtils.RESULT_GROUP.REMOTE_TAB, + }, + { + flex: 2, + group: lazy.UrlbarUtils.RESULT_GROUP.GENERAL, + }, + { + // We show relatively many about-page results because they're + // only added for queries starting with "about:". + flex: 2, + group: lazy.UrlbarUtils.RESULT_GROUP.ABOUT_PAGES, + }, + { + flex: 1, + group: lazy.UrlbarUtils.RESULT_GROUP.PRELOADED, + }, + ], + }, + { + group: lazy.UrlbarUtils.RESULT_GROUP.INPUT_HISTORY, + }, + ], + }, + ], + }; + if (!showSearchSuggestionsFirst) { + mainGroup.children.reverse(); + } + mainGroup.children[0].flex = 2; + mainGroup.children[1].flex = 1; + rootGroup.children.push(mainGroup); + + return rootGroup; +} + +/** + * Preferences class. The exported object is a singleton instance. + */ +class Preferences { + /** + * Constructor + */ + constructor() { + this._map = new Map(); + this.QueryInterface = ChromeUtils.generateQI([ + "nsIObserver", + "nsISupportsWeakReference", + ]); + + Services.prefs.addObserver(PREF_URLBAR_BRANCH, this, true); + for (let pref of PREF_OTHER_DEFAULTS.keys()) { + Services.prefs.addObserver(pref, this, true); + } + this._observerWeakRefs = []; + this.addObserver(this); + + // These prefs control the value of the shouldHandOffToSearchMode pref. They + // are exposed as a class variable so UrlbarPrefs observers can watch for + // changes in these prefs. + this.shouldHandOffToSearchModePrefs = [ + "keyword.enabled", + "suggest.searches", + ]; + + // This is resolved when the first update to the Firefox Suggest scenario + // (on startup) finishes. + this._firefoxSuggestScenarioStartupPromise = new Promise( + resolve => (this._resolveFirefoxSuggestScenarioStartupPromise = resolve) + ); + + // This is set to true when we update the Firefox Suggest scenario to + // prevent re-entry due to pref observers. + this._updatingFirefoxSuggestScenario = false; + + lazy.NimbusFeatures.urlbar.onUpdate(() => this._onNimbusUpdate()); + } + + /** + * Returns the value for the preference with the given name. + * For preferences in the "browser.urlbar."" branch, the passed-in name + * should be relative to the branch. It's also possible to get prefs from the + * PREF_OTHER_DEFAULTS Map, specifying their full name. + * + * @param {string} pref + * The name of the preference to get. + * @returns {*} The preference value. + */ + get(pref) { + let value = this._map.get(pref); + if (value === undefined) { + value = this._getPrefValue(pref); + this._map.set(pref, value); + } + return value; + } + + /** + * Sets the value for the preference with the given name. + * For preferences in the "browser.urlbar."" branch, the passed-in name + * should be relative to the branch. It's also possible to set prefs from the + * PREF_OTHER_DEFAULTS Map, specifying their full name. + * + * @param {string} pref + * The name of the preference to set. + * @param {*} value The preference value. + */ + set(pref, value) { + let { defaultValue, set } = this._getPrefDescriptor(pref); + if (typeof value != typeof defaultValue) { + throw new Error(`Invalid value type ${typeof value} for pref ${pref}`); + } + set(pref, value); + } + + /** + * Clears the value for the preference with the given name. + * + * @param {string} pref + * The name of the preference to clear. + */ + clear(pref) { + let { clear } = this._getPrefDescriptor(pref); + clear(pref); + } + + /** + * Builds the standard result groups. See makeResultGroups. + * + * @param {object} options + * See makeResultGroups. + * @returns {object} + * The root group. + */ + makeResultGroups(options) { + return makeResultGroups(options); + } + + /** + * Sets the value of the resultGroups pref to the current default groups. + * This should be called from BrowserGlue._migrateUI when the default groups + * are modified. + */ + migrateResultGroups() { + this.set( + "resultGroups", + JSON.stringify( + makeResultGroups({ + showSearchSuggestionsFirst: this.get("showSearchSuggestionsFirst"), + }) + ) + ); + } + + /** + * Sets the appropriate Firefox Suggest scenario based on the current Nimbus + * rollout (if any) and "hardcoded" rollouts (if any). The possible scenarios + * are: + * + * history + * This is the scenario when the user is not in any rollouts. Firefox + * Suggest suggestions are disabled. + * offline + * This is the scenario for the "offline" rollout. Firefox Suggest + * suggestions are enabled by default. Data collection is not enabled by + * default, but the user can opt in in about:preferences. The onboarding + * dialog is not shown. + * online + * This is the scenario for the "online" rollout. Firefox Suggest + * suggestions are enabled by default. Data collection is not enabled by + * default, and the user will be shown an onboarding dialog that prompts + * them to opt in to it. The user can also opt in in about:preferences. + * + * @param {string} [testOverrides] + * This is intended for tests only. Pass to force the following: + * `{ scenario, migrationVersion, defaultPrefs, isStartup }` + */ + async updateFirefoxSuggestScenario(testOverrides = null) { + // Make sure we don't re-enter this method while updating prefs. Updates to + // prefs that are fallbacks for Nimbus variables trigger the pref observer + // in Nimbus, which triggers our Nimbus `onUpdate` callback, which calls + // this method again. + if (this._updatingFirefoxSuggestScenario) { + return; + } + + let isStartup = + !this._updateFirefoxSuggestScenarioCalled || !!testOverrides?.isStartup; + this._updateFirefoxSuggestScenarioCalled = true; + + try { + this._updatingFirefoxSuggestScenario = true; + + // This is called early in startup by BrowserGlue, so make sure the user's + // region and our Nimbus variables are initialized since the scenario may + // depend on them. Also note that pref migrations may depend on the + // scenario, and since each migration is performed only once, at startup, + // prefs can end up wrong if their migrations use the wrong scenario. + await lazy.Region.init(); + await lazy.NimbusFeatures.urlbar.ready(); + this._clearNimbusCache(); + + // This also races TelemetryEnvironment's initialization, so wait for it + // to finish. TelemetryEnvironment is important because it records the + // values of a number of Suggest preferences. If we didn't wait, we could + // end up updating prefs after TelemetryEnvironment does its initial pref + // cache but before it adds its observer to be notified of pref changes. + // It would end up recording the wrong values on startup in that case. + if (!this._testSkipTelemetryEnvironmentInit) { + await lazy.TelemetryEnvironment.onInitialized(); + } + + this._updateFirefoxSuggestScenarioHelper(isStartup, testOverrides); + } finally { + this._updatingFirefoxSuggestScenario = false; + } + + // Null check `_resolveFirefoxSuggestScenarioStartupPromise` since some + // tests force `isStartup` after actual startup and it's been set to null. + if (isStartup && this._resolveFirefoxSuggestScenarioStartupPromise) { + this._resolveFirefoxSuggestScenarioStartupPromise(); + this._resolveFirefoxSuggestScenarioStartupPromise = null; + } + } + + _updateFirefoxSuggestScenarioHelper(isStartup, testOverrides) { + // Updating the scenario is tricky and it's important to preserve the user's + // choices, so we describe the process in detail below. tl;dr: + // + // * Prefs exposed in the UI should be sticky. + // * Prefs that are both exposed in the UI and configurable via Nimbus + // should be added to `uiPrefNamesByVariable` below. + // * Prefs that are both exposed in the UI and configurable via Nimbus don't + // need to be specified as a `fallbackPref` in the feature manifest. + // Access these prefs directly instead of through their Nimbus variables. + // * If you are modifying this method, keep in mind that setting a pref + // that's a `fallbackPref` for a Nimbus variable will trigger the pref + // observer inside Nimbus and call all `NimbusFeatures.urlbar.onUpdate` + // callbacks. Inside this class we guard against that by using + // `updatingFirefoxSuggestScenario`. + // + // The scenario-update process is described next. + // + // 1. Pick a scenario. If the user is in a Nimbus rollout, then Nimbus will + // define it. Otherwise the user may be in a "hardcoded" rollout + // depending on their region and locale. If the user is not in any + // rollouts, then the scenario is "history", which means no Firefox + // Suggest suggestions should appear. + // + // 2. Set prefs on the default branch appropriate for the scenario. We use + // the default branch and not the user branch because conceptually each + // scenario has a default behavior, which we want to distinguish from the + // user's choices. + // + // In particular it's important to consider prefs that are exposed in the + // UI, like whether sponsored suggestions are enabled. Once the user + // makes a choice to change a default, we want to preserve that choice + // indefinitely regardless of the scenario the user is currently enrolled + // in or future scenarios they might be enrolled in. User choices are of + // course recorded on the user branch, so if we set scenario defaults on + // the user branch too, we wouldn't be able to distinguish user choices + // from default values. This is also why prefs that are exposed in the UI + // should be sticky. Unlike non-sticky prefs, sticky prefs retain their + // user-branch values even when those values are the same as the ones on + // the default branch. + // + // It's important to note that the defaults we set here do not persist + // across app restarts. (This is a feature of the pref service; prefs set + // programmatically on the default branch are not stored anywhere + // permanent like firefox.js or user.js.) That's why BrowserGlue calls + // `updateFirefoxSuggestScenario` on every startup. + // + // 3. Some prefs are both exposed in the UI and configurable via Nimbus, + // like whether data collection is enabled. We absolutely want to + // preserve the user's past choices for these prefs. But if the user + // hasn't yet made a choice for a particular pref, then it should be + // configurable. + // + // For any such prefs that have values defined in Nimbus, we set their + // default-branch values to their Nimbus values. (These defaults + // therefore override any set in the previous step.) If a pref has a user + // value, accessing the pref will return the user value; if it does not + // have a user value, accessing it will return the value that was + // specified in Nimbus. + // + // This isn't strictly necessary. Since prefs exposed in the UI are + // sticky, they will always preserve their user-branch values regardless + // of their default-branch values, and as long as a pref is listed as a + // `fallbackPref` for its corresponding Nimbus variable, Nimbus will use + // the user-branch value. So we could instead specify fallback prefs in + // Nimbus and always access values through Nimbus instead of through + // prefs. But that would make preferences UI code a little harder to + // write since the checked state of a checkbox would depend on something + // other than its pref. Since we're already setting default-branch values + // here as part of the previous step, it's not much more work to set + // defaults for these prefs too, and it makes the UI code a little nicer. + // + // 4. Migrate prefs as necessary. This refers to any pref changes that are + // neccesary across app versions: introducing and initializing new prefs, + // removing prefs, or changing the meaning of existing prefs. + + // 1. Pick a scenario + let scenario = + testOverrides?.scenario || this._getIntendedFirefoxSuggestScenario(); + + // 2. Set default-branch values for the scenario + let defaultPrefs = + testOverrides?.defaultPrefs || this.FIREFOX_SUGGEST_DEFAULT_PREFS; + let prefs = { ...defaultPrefs[scenario] }; + + // 3. Set default-branch values for prefs that are both exposed in the UI + // and configurable via Nimbus + for (let [variable, prefName] of Object.entries( + this.FIREFOX_SUGGEST_UI_PREFS_BY_VARIABLE + )) { + if (this._nimbus.hasOwnProperty(variable)) { + prefs[prefName] = this._nimbus[variable]; + } + } + + let defaults = Services.prefs.getDefaultBranch("browser.urlbar."); + for (let [name, value] of Object.entries(prefs)) { + // We assume all prefs are boolean right now. + defaults.setBoolPref(name, value); + } + + // 4. Migrate prefs across app versions + if (isStartup) { + this._ensureFirefoxSuggestPrefsMigrated(scenario, testOverrides); + } + + // Set the scenario pref only after migrating so that migrations can tell + // what the last-seen scenario was. Set it on the user branch so that its + // value persists across app restarts. + this.set("quicksuggest.scenario", scenario); + } + + /** + * Returns the Firefox Suggest scenario the user should be enrolled in. This + * does *not* return the scenario they are currently enrolled in. + * + * @returns {string} + * The scenario the user should be enrolled in. + */ + _getIntendedFirefoxSuggestScenario() { + // If the user is in a Nimbus rollout, then Nimbus will define the scenario. + // Otherwise the user may be in a "hardcoded" rollout depending on their + // region and locale. If the user is not in any rollouts, then the scenario + // is "history", which means no Firefox Suggest suggestions will appear. + let scenario = this._nimbus.quickSuggestScenario; + if (!scenario) { + if ( + lazy.Region.home == "US" && + Services.locale.appLocaleAsBCP47.substring(0, 2) == "en" + ) { + // offline rollout for en locales in the US region + scenario = "offline"; + } else { + // no rollout + scenario = "history"; + } + } + if (!this.FIREFOX_SUGGEST_DEFAULT_PREFS.hasOwnProperty(scenario)) { + scenario = "history"; + Cu.reportError(`Unrecognized Firefox Suggest scenario "${scenario}"`); + } + return scenario; + } + + /** + * Prefs that are exposed in the UI and whose default-branch values are + * configurable via Nimbus variables. This getter returns an object that maps + * from variable names to pref names relative to `browser.urlbar`. See point 3 + * in the comment inside `_updateFirefoxSuggestScenarioHelper()` for more. + * + * @returns {{ quickSuggestNonSponsoredEnabled: string; quickSuggestSponsoredEnabled: string; quickSuggestDataCollectionEnabled: string; }} + */ + get FIREFOX_SUGGEST_UI_PREFS_BY_VARIABLE() { + return { + quickSuggestNonSponsoredEnabled: "suggest.quicksuggest.nonsponsored", + quickSuggestSponsoredEnabled: "suggest.quicksuggest.sponsored", + quickSuggestDataCollectionEnabled: "quicksuggest.dataCollection.enabled", + }; + } + + /** + * Default prefs relative to `browser.urlbar` per Firefox Suggest scenario. + * + * @returns {Record<Record<string, boolean>>} + */ + get FIREFOX_SUGGEST_DEFAULT_PREFS() { + // Important notes when modifying this: + // + // If you add a pref to one scenario, you typically need to add it to all + // scenarios even if the pref is in firefox.js. That's because we need to + // allow for switching from one scenario to another at any time after + // startup. If we set a pref for one scenario on the default branch, we + // switch to a new scenario, and we don't set the pref for the new scenario, + // it will keep its default-branch value from the old scenario. The only + // possible exception is for prefs that make others unnecessary, like how + // when `quicksuggest.enabled` is false, none of the other prefs matter. + // + // Prefs not listed here for any scenario keep their values set in + // firefox.js. + return { + history: { + "quicksuggest.enabled": false, + }, + offline: { + "quicksuggest.enabled": true, + "quicksuggest.dataCollection.enabled": false, + "quicksuggest.shouldShowOnboardingDialog": false, + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": true, + }, + online: { + "quicksuggest.enabled": true, + "quicksuggest.dataCollection.enabled": false, + "quicksuggest.shouldShowOnboardingDialog": true, + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": true, + }, + }; + } + + /** + * The current version of the Firefox Suggest prefs. + * + * @returns {number} + */ + get FIREFOX_SUGGEST_MIGRATION_VERSION() { + return 2; + } + + /** + * Migrates Firefox Suggest prefs to the current version if they haven't been + * migrated already. + * + * @param {string} scenario + * The current Firefox Suggest scenario. + * @param {string} testOverrides + * This is intended for tests only. Pass to force a migration version: + * `{ migrationVersion }` + */ + _ensureFirefoxSuggestPrefsMigrated(scenario, testOverrides) { + let currentVersion = + testOverrides?.migrationVersion !== undefined + ? testOverrides.migrationVersion + : this.FIREFOX_SUGGEST_MIGRATION_VERSION; + let lastSeenVersion = Math.max( + 0, + this.get("quicksuggest.migrationVersion") + ); + if (currentVersion <= lastSeenVersion) { + // Migration up to date. + return; + } + + let version = lastSeenVersion; + + // When the current scenario is online and the last-seen prefs version is + // unversioned, specially handle migration up to version 2. + if (!version && scenario == "online" && 2 <= currentVersion) { + this._migrateFirefoxSuggestPrefsUnversionedTo2Online(); + version = 2; + } + + // Migrate from the last-seen version up to the current version. + for (; version < currentVersion; version++) { + let nextVersion = version + 1; + let methodName = "_migrateFirefoxSuggestPrefsTo_" + nextVersion; + try { + this[methodName](scenario); + } catch (error) { + Cu.reportError( + `Error migrating Firefox Suggest prefs to version ${nextVersion}: ` + + error + ); + break; + } + } + + // Record the new last-seen migration version. + this.set("quicksuggest.migrationVersion", version); + } + + /** + * Migrates unversioned Firefox Suggest prefs to version 2 but only when the + * user's current scenario is online. This case requires special handling that + * isn't covered by the usual migration path from unversioned to 2. + */ + _migrateFirefoxSuggestPrefsUnversionedTo2Online() { + // Copy `suggest.quicksuggest` to `suggest.quicksuggest.nonsponsored` and + // clear the first. + let mainPref = "browser.urlbar.suggest.quicksuggest"; + let mainPrefHasUserValue = Services.prefs.prefHasUserValue(mainPref); + if (mainPrefHasUserValue) { + this.set( + "suggest.quicksuggest.nonsponsored", + Services.prefs.getBoolPref(mainPref) + ); + Services.prefs.clearUserPref(mainPref); + } + + if (!this.get("quicksuggest.showedOnboardingDialog")) { + // The user was enrolled in history or offline, or they were enrolled in + // online and weren't shown the modal yet. + // + // If they were in history, they should now see suggestions by default, + // and we don't need to worry about any current pref values since Firefox + // Suggest is new to them. + // + // If they were in offline, they saw suggestions by default, but if they + // disabled the main suggestions pref, then both non-sponsored and + // sponsored suggestions were disabled and we need to carry that forward. + // + // If they were in online and weren't shown the modal yet, suggestions + // were disabled by default. The modal is shown only on startup, so it's + // possible they used Firefox for quite a while after being enrolled in + // online with suggestions disabled the whole time. If they looked at the + // prefs UI, they would have seen both suggestion checkboxes unchecked. + // For these users, ideally we wouldn't suddenly enable suggestions, but + // unfortunately there's no simple way to distinguish them from history + // and offline users at this point based on the unversioned prefs. We + // could check whether the user is or was enrolled in the initial online + // experiment; if they were, then disable suggestions. However, that's a + // little risky because it assumes future online rollouts will be + // delivered by new experiments and not by increasing the original + // experiment's population. If that assumption does not hold, we would end + // up disabling suggestions for all users who are newly enrolled in online + // even if they were previously in history or offline. Further, based on + // telemetry data at the time of writing, only a small number of users in + // online have not yet seen the modal. Therefore we will enable + // suggestions for these users too. + // + // Note that if the user is in online and hasn't been shown the modal yet, + // we'll show it at some point during startup right after this. However, + // starting with the version-2 prefs, the modal now opts the user in to + // only data collection, not suggestions as it previously did. + + if ( + mainPrefHasUserValue && + !this.get("suggest.quicksuggest.nonsponsored") + ) { + // The user was in offline and disabled the main suggestions pref, so + // sponsored suggestions were automatically disabled too. We know they + // disabled the main pref since it has a false user-branch value. + this.set("suggest.quicksuggest.sponsored", false); + } + return; + } + + // At this point, the user was in online, they were shown the modal, and the + // current scenario is online. In the unversioned prefs for online, the + // suggestion prefs were false on the default branch, but in the version-2 + // prefs, they're true on the default branch. + + if (mainPrefHasUserValue && this.get("suggest.quicksuggest.nonsponsored")) { + // The main pref is true on the user branch. The user opted in either via + // the modal or by checking the checkbox in the prefs UI. In the latter + // case, they were shown some informational text about data collection + // under the checkbox. Either way, they've opted in to data collection. + this.set("quicksuggest.dataCollection.enabled", true); + if ( + !Services.prefs.prefHasUserValue( + "browser.urlbar.suggest.quicksuggest.sponsored" + ) + ) { + // The sponsored pref does not have a user value, so the default-branch + // false value was the effective value and the user did not see + // sponsored suggestions. We need to override the version-2 default- + // branch true value by setting the pref to false. + this.set("suggest.quicksuggest.sponsored", false); + } + } else { + // The main pref is not true on the user branch, so the user either did + // not opt in or they later disabled suggestions in the prefs UI. Set the + // suggestion prefs to false on the user branch to override the version-2 + // default-branch true values. The data collection pref is false on the + // default branch, but since the user was shown the modal, set it on the + // user branch too, where it's sticky, to record the user's choice not to + // opt in. + this.set("suggest.quicksuggest.nonsponsored", false); + this.set("suggest.quicksuggest.sponsored", false); + this.set("quicksuggest.dataCollection.enabled", false); + } + } + + _migrateFirefoxSuggestPrefsTo_1(scenario) { + // Copy `suggest.quicksuggest` to `suggest.quicksuggest.nonsponsored` and + // clear the first. + let suggestQuicksuggest = "browser.urlbar.suggest.quicksuggest"; + if (Services.prefs.prefHasUserValue(suggestQuicksuggest)) { + this.set( + "suggest.quicksuggest.nonsponsored", + Services.prefs.getBoolPref(suggestQuicksuggest) + ); + Services.prefs.clearUserPref(suggestQuicksuggest); + } + + // In the unversioned prefs, sponsored suggestions were shown only if the + // main suggestions pref `suggest.quicksuggest` was true, but now there are + // two independent prefs, so disable sponsored if the main pref was false. + if (!this.get("suggest.quicksuggest.nonsponsored")) { + switch (scenario) { + case "offline": + // Set the pref on the user branch. Suggestions are enabled by default + // for offline; we want to preserve the user's choice of opting out, + // and we want to preserve the default-branch true value. + this.set("suggest.quicksuggest.sponsored", false); + break; + case "online": + // If the user-branch value is true, clear it so the default-branch + // false value becomes the effective value. + if (this.get("suggest.quicksuggest.sponsored")) { + this.clear("suggest.quicksuggest.sponsored"); + } + break; + } + } + + // The data collection pref is new in this version. Enable it iff the + // scenario is online and the user opted in to suggestions. In offline, it + // should always start off false. + if (scenario == "online" && this.get("suggest.quicksuggest.nonsponsored")) { + this.set("quicksuggest.dataCollection.enabled", true); + } + } + + _migrateFirefoxSuggestPrefsTo_2(scenario) { + // In previous versions of the prefs for online, suggestions were disabled + // by default; in version 2, they're enabled by default. For users who were + // already in online and did not enable suggestions (because they did not + // opt in, they did opt in but later disabled suggestions, or they were not + // shown the modal) we don't want to suddenly enable them, so if the prefs + // do not have user-branch values, set them to false. + if (this.get("quicksuggest.scenario") == "online") { + if ( + !Services.prefs.prefHasUserValue( + "browser.urlbar.suggest.quicksuggest.nonsponsored" + ) + ) { + this.set("suggest.quicksuggest.nonsponsored", false); + } + if ( + !Services.prefs.prefHasUserValue( + "browser.urlbar.suggest.quicksuggest.sponsored" + ) + ) { + this.set("suggest.quicksuggest.sponsored", false); + } + } + } + + /** + * @returns {Promise} + * This can be used to wait until the initial Firefox Suggest scenario has + * been set on startup. It's resolved when the first call to + * `updateFirefoxSuggestScenario()` finishes. + */ + get firefoxSuggestScenarioStartupPromise() { + return this._firefoxSuggestScenarioStartupPromise; + } + + /** + * @returns {boolean} + * Whether the Firefox Suggest scenario is being updated. While true, + * changes to related prefs should be ignored, depending on the observer. + * Telemetry intended to capture user changes to the prefs should not be + * recorded, for example. + */ + get updatingFirefoxSuggestScenario() { + return this._updatingFirefoxSuggestScenario; + } + + /** + * Adds a preference observer. Observers are held weakly. + * + * @param {object} observer + * An object that must have a method named `onPrefChanged`, which will + * be called when a urlbar preference changes. It will be passed the + * pref name. For prefs in the `browser.urlbar.` branch, the name will + * be relative to the branch. For other prefs, the name will be the + * full name. + */ + addObserver(observer) { + this._observerWeakRefs.push(Cu.getWeakReference(observer)); + } + + /** + * Observes preference changes. + * + * @param {nsISupports} subject + * The subject of the notification. + * @param {string} topic + * The topic of the notification. + * @param {string} data + * The data attached to the notification. + */ + observe(subject, topic, data) { + let pref = data.replace(PREF_URLBAR_BRANCH, ""); + if (!PREF_URLBAR_DEFAULTS.has(pref) && !PREF_OTHER_DEFAULTS.has(pref)) { + return; + } + for (let i = 0; i < this._observerWeakRefs.length; ) { + let observer = this._observerWeakRefs[i].get(); + if (!observer) { + // The observer has been GC'ed, so remove it from our list. + this._observerWeakRefs.splice(i, 1); + } else { + observer.onPrefChanged(pref); + ++i; + } + } + } + + /** + * Called when a pref tracked by UrlbarPrefs changes. + * + * @param {string} pref + * The name of the pref, relative to `browser.urlbar.` if the pref is + * in that branch. + */ + onPrefChanged(pref) { + this._map.delete(pref); + + // Some prefs may influence others. + switch (pref) { + case "autoFill.adaptiveHistory.useCountThreshold": + this._map.delete("autoFillAdaptiveHistoryUseCountThreshold"); + return; + case "showSearchSuggestionsFirst": + this.set( + "resultGroups", + JSON.stringify( + makeResultGroups({ showSearchSuggestionsFirst: this.get(pref) }) + ) + ); + return; + } + + if (pref.startsWith("suggest.")) { + this._map.delete("defaultBehavior"); + } + + if (this.shouldHandOffToSearchModePrefs.includes(pref)) { + this._map.delete("shouldHandOffToSearchMode"); + } + } + + /** + * Called when the `NimbusFeatures.urlbar` value changes. + */ + _onNimbusUpdate() { + let oldNimbus = this._clearNimbusCache(); + let newNimbus = this._nimbus; + + // If a change occurred to the Firefox Suggest scenario variable or any + // variables that correspond to prefs exposed in the UI, we need to update + // the scenario. + let variables = [ + "quickSuggestScenario", + ...Object.keys(this.FIREFOX_SUGGEST_UI_PREFS_BY_VARIABLE), + ]; + for (let name of variables) { + if (oldNimbus[name] != newNimbus[name]) { + this.updateFirefoxSuggestScenario(); + return; + } + } + + // If the current default-branch value of any pref is incorrect for the + // intended scenario, we need to update the scenario. + let scenario = this._getIntendedFirefoxSuggestScenario(); + let intendedDefaultPrefs = this.FIREFOX_SUGGEST_DEFAULT_PREFS[scenario]; + let defaults = Services.prefs.getDefaultBranch("browser.urlbar."); + for (let [name, value] of Object.entries(intendedDefaultPrefs)) { + // We assume all prefs are boolean right now. + if (defaults.getBoolPref(name) != value) { + this.updateFirefoxSuggestScenario(); + return; + } + } + } + + /** + * Clears cached Nimbus variables. The cache will be repopulated the next time + * `_nimbus` is accessed. + * + * @returns {object} + * The value of the cache before it was cleared. It's an object that maps + * from variable names to values. + */ + _clearNimbusCache() { + let nimbus = this.__nimbus; + if (nimbus) { + for (let key of Object.keys(nimbus)) { + this._map.delete(key); + } + this.__nimbus = null; + } + return nimbus || {}; + } + + get _nimbus() { + if (!this.__nimbus) { + this.__nimbus = lazy.NimbusFeatures.urlbar.getAllVariables({ + defaultValues: NIMBUS_DEFAULTS, + }); + } + return this.__nimbus; + } + + /** + * Returns the raw value of the given preference straight from Services.prefs. + * + * @param {string} pref + * The name of the preference to get. + * @returns {*} The raw preference value. + */ + _readPref(pref) { + let { defaultValue, get } = this._getPrefDescriptor(pref); + return get(pref, defaultValue); + } + + /** + * Returns a validated and/or fixed-up value of the given preference. The + * value may be validated for correctness, or it might be converted into a + * different value that is easier to work with than the actual value stored in + * the preferences branch. Not all preferences require validation or fixup. + * + * The values returned from this method are the values that are made public by + * this module. + * + * @param {string} pref + * The name of the preference to get. + * @returns {*} The validated and/or fixed-up preference value. + */ + _getPrefValue(pref) { + switch (pref) { + case "defaultBehavior": { + let val = 0; + for (let type of Object.keys(SUGGEST_PREF_TO_BEHAVIOR)) { + let behavior = `BEHAVIOR_${SUGGEST_PREF_TO_BEHAVIOR[ + type + ].toUpperCase()}`; + val |= + this.get("suggest." + type) && Ci.mozIPlacesAutoComplete[behavior]; + } + return val; + } + case "resultGroups": + try { + return JSON.parse(this._readPref(pref)); + } catch (ex) {} + return makeResultGroups({ + showSearchSuggestionsFirst: this.get("showSearchSuggestionsFirst"), + }); + case "shouldHandOffToSearchMode": + return this.shouldHandOffToSearchModePrefs.some( + prefName => !this.get(prefName) + ); + case "autoFillAdaptiveHistoryUseCountThreshold": + const nimbusValue = this._nimbus + .autoFillAdaptiveHistoryUseCountThreshold; + return nimbusValue === undefined + ? this.get("autoFill.adaptiveHistory.useCountThreshold") + : parseFloat(nimbusValue); + } + return this._readPref(pref); + } + + /** + * Returns a descriptor of the given preference. + * + * @param {string} pref The preference to examine. + * @returns {object} An object describing the pref with the following shape: + * { defaultValue, get, set, clear } + */ + _getPrefDescriptor(pref) { + let branch = Services.prefs.getBranch(PREF_URLBAR_BRANCH); + let defaultValue = PREF_URLBAR_DEFAULTS.get(pref); + if (defaultValue === undefined) { + branch = Services.prefs; + defaultValue = PREF_OTHER_DEFAULTS.get(pref); + if (defaultValue === undefined) { + let nimbus = this._getNimbusDescriptor(pref); + if (nimbus) { + return nimbus; + } + throw new Error("Trying to access an unknown pref " + pref); + } + } + + let type; + if (!Array.isArray(defaultValue)) { + type = PREF_TYPES.get(typeof defaultValue); + } else { + if (defaultValue.length != 2) { + throw new Error("Malformed pref def: " + pref); + } + [defaultValue, type] = defaultValue; + type = PREF_TYPES.get(type); + } + if (!type) { + throw new Error("Unknown pref type: " + pref); + } + return { + defaultValue, + get: branch[`get${type}Pref`], + // Float prefs are stored as Char. + set: branch[`set${type == "Float" ? "Char" : type}Pref`], + clear: branch.clearUserPref, + }; + } + + /** + * Returns a descriptor for the given Nimbus property, if it exists. + * + * @param {string} name + * The name of the desired property in the object returned from + * NimbusFeatures.urlbar.getAllVariables(). + * @returns {object} + * An object describing the property's value with the following shape (same + * as _getPrefDescriptor()): + * { defaultValue, get, set, clear } + * If the property doesn't exist, null is returned. + */ + _getNimbusDescriptor(name) { + if (!this._nimbus.hasOwnProperty(name)) { + return null; + } + return { + defaultValue: this._nimbus[name], + get: () => this._nimbus[name], + set() { + throw new Error(`'${name}' is a Nimbus value and cannot be set`); + }, + clear() { + throw new Error(`'${name}' is a Nimbus value and cannot be cleared`); + }, + }; + } + + /** + * Initializes the showSearchSuggestionsFirst pref based on the matchGroups + * pref. This function can be removed when the corresponding UI migration in + * BrowserGlue.sys.mjs is no longer needed. + */ + initializeShowSearchSuggestionsFirstPref() { + let matchGroups = []; + let pref = Services.prefs.getCharPref("browser.urlbar.matchGroups", ""); + try { + matchGroups = pref.split(",").map(v => { + let group = v.split(":"); + return [group[0].trim().toLowerCase(), Number(group[1])]; + }); + } catch (ex) {} + let groupNames = matchGroups.map(group => group[0]); + let suggestionIndex = groupNames.indexOf("suggestion"); + let generalIndex = groupNames.indexOf("general"); + let showSearchSuggestionsFirst = + generalIndex < 0 || + (suggestionIndex >= 0 && suggestionIndex < generalIndex); + let oldValue = Services.prefs.getBoolPref( + "browser.urlbar.showSearchSuggestionsFirst" + ); + Services.prefs.setBoolPref( + "browser.urlbar.showSearchSuggestionsFirst", + showSearchSuggestionsFirst + ); + + // Pref observers aren't called when a pref is set to its current value, but + // we always want to set matchGroups to the appropriate default value via + // onPrefChanged, so call it now if necessary. This is really only + // necessary for tests since the only time this function is called outside + // of tests is by a UI migration in BrowserGlue. + if (oldValue == showSearchSuggestionsFirst) { + this.onPrefChanged("showSearchSuggestionsFirst"); + } + } + + /** + * Return whether or not persisted search terms is enabled. + * + * @returns {boolean} true: if enabled. + */ + isPersistedSearchTermsEnabled() { + return ( + this.get("showSearchTermsFeatureGate") && + this.get("showSearchTerms.enabled") && + !this.get("browser.search.widget.inNavBar") + ); + } +} + +export var UrlbarPrefs = new Preferences(); |