tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

main.js (233051B)


      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 file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 /* import-globals-from extensionControlled.js */
      6 /* import-globals-from letterboxing.js */
      7 /* import-globals-from preferences.js */
      8 /* import-globals-from /toolkit/mozapps/preferences/fontbuilder.js */
      9 /* import-globals-from /browser/base/content/aboutDialog-appUpdater.js */
     10 /* global MozXULElement */
     11 
     12 ChromeUtils.defineESModuleGetters(this, {
     13  BackgroundUpdate: "resource://gre/modules/BackgroundUpdate.sys.mjs",
     14  UpdateListener: "resource://gre/modules/UpdateListener.sys.mjs",
     15  // LinkPreview.sys.mjs is missing. tor-browser#44045.
     16  MigrationUtils: "resource:///modules/MigrationUtils.sys.mjs",
     17  SelectableProfileService:
     18    "resource:///modules/profiles/SelectableProfileService.sys.mjs",
     19  TranslationsParent: "resource://gre/actors/TranslationsParent.sys.mjs",
     20  WindowsLaunchOnLogin: "resource://gre/modules/WindowsLaunchOnLogin.sys.mjs",
     21  NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
     22  FormAutofillPreferences:
     23    "resource://autofill/FormAutofillPreferences.sys.mjs",
     24  getMozRemoteImageURL: "moz-src:///browser/modules/FaviconUtils.sys.mjs",
     25 });
     26 
     27 // Constants & Enumeration Values
     28 const TYPE_PDF = "application/pdf";
     29 
     30 const PREF_PDFJS_DISABLED = "pdfjs.disabled";
     31 
     32 // Pref for when containers is being controlled
     33 const PREF_CONTAINERS_EXTENSION = "privacy.userContext.extension";
     34 
     35 // Strings to identify ExtensionSettingsStore overrides
     36 const CONTAINERS_KEY = "privacy.containers";
     37 
     38 const FORCED_COLORS_QUERY = matchMedia("(forced-colors)");
     39 
     40 const AUTO_UPDATE_CHANGED_TOPIC =
     41  UpdateUtils.PER_INSTALLATION_PREFS["app.update.auto"].observerTopic;
     42 const BACKGROUND_UPDATE_CHANGED_TOPIC =
     43  UpdateUtils.PER_INSTALLATION_PREFS["app.update.background.enabled"]
     44    .observerTopic;
     45 
     46 const ICON_URL_APP =
     47  AppConstants.platform == "linux"
     48    ? "moz-icon://dummy.exe?size=16"
     49    : "chrome://browser/skin/preferences/application.png";
     50 
     51 // For CSS. Can be one of "ask", "save" or "handleInternally". If absent, the icon URL
     52 // was set by us to a custom handler icon and CSS should not try to override it.
     53 const APP_ICON_ATTR_NAME = "appHandlerIcon";
     54 
     55 const OPEN_EXTERNAL_LINK_NEXT_TO_ACTIVE_TAB_VALUE =
     56  Ci.nsIBrowserDOMWindow.OPEN_NEWTAB_AFTER_CURRENT;
     57 
     58 Preferences.addAll([
     59  // Startup
     60  { id: "browser.startup.page", type: "int" },
     61  { id: "browser.startup.windowsLaunchOnLogin.enabled", type: "bool" },
     62  { id: "browser.privatebrowsing.autostart", type: "bool" },
     63 
     64  // Downloads
     65  { id: "browser.download.useDownloadDir", type: "bool", inverted: true },
     66  { id: "browser.download.enableDeletePrivate", type: "bool" },
     67  { id: "browser.download.deletePrivate", type: "bool" },
     68  { id: "browser.download.always_ask_before_handling_new_types", type: "bool" },
     69  { id: "browser.download.folderList", type: "int" },
     70  { id: "browser.download.dir", type: "file" },
     71 
     72  /* Tab preferences
     73  Preferences:
     74 
     75  browser.link.open_newwindow
     76      1 opens such links in the most recent window or tab,
     77      2 opens such links in a new window,
     78      3 opens such links in a new tab
     79  browser.link.open_newwindow.override.external
     80    - this setting overrides `browser.link.open_newwindow` for externally
     81      opened links.
     82    - see `nsIBrowserDOMWindow` constants for the meaning of each value.
     83  browser.tabs.loadInBackground
     84  - true if display should switch to a new tab which has been opened from a
     85    link, false if display shouldn't switch
     86  browser.tabs.warnOnClose
     87  - true if when closing a window with multiple tabs the user is warned and
     88    allowed to cancel the action, false to just close the window
     89  browser.tabs.warnOnOpen
     90  - true if the user should be warned if he attempts to open a lot of tabs at
     91    once (e.g. a large folder of bookmarks), false otherwise
     92  browser.warnOnQuitShortcut
     93  - true if the user should be warned if they quit using the keyboard shortcut
     94  browser.taskbar.previews.enable
     95  - true if tabs are to be shown in the Windows 7 taskbar
     96  */
     97 
     98  { id: "browser.link.open_newwindow", type: "int" },
     99  { id: "browser.link.open_newwindow.override.external", type: "int" },
    100  { id: "browser.tabs.loadInBackground", type: "bool", inverted: true },
    101  { id: "browser.tabs.warnOnClose", type: "bool" },
    102  { id: "browser.warnOnQuitShortcut", type: "bool" },
    103  { id: "browser.tabs.warnOnOpen", type: "bool" },
    104  { id: "browser.ctrlTab.sortByRecentlyUsed", type: "bool" },
    105  { id: "browser.tabs.hoverPreview.enabled", type: "bool" },
    106  { id: "browser.tabs.hoverPreview.showThumbnails", type: "bool" },
    107  { id: "browser.tabs.groups.smart.userEnabled", type: "bool" },
    108  { id: "browser.tabs.groups.smart.enabled", type: "bool" },
    109  { id: "privacy.userContext.ui.enabled", type: "bool" },
    110 
    111  { id: "sidebar.verticalTabs", type: "bool" },
    112  { id: "sidebar.revamp", type: "bool" },
    113 
    114  // CFR
    115  {
    116    id: "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons",
    117    type: "bool",
    118  },
    119  {
    120    id: "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features",
    121    type: "bool",
    122  },
    123 
    124  // High Contrast
    125  { id: "browser.display.document_color_use", type: "int" },
    126 
    127  // Fonts
    128  { id: "font.language.group", type: "string" },
    129 
    130  // Languages
    131  { id: "intl.regional_prefs.use_os_locales", type: "bool" },
    132 
    133  // General tab
    134 
    135  /* Accessibility
    136   * accessibility.browsewithcaret
    137     - true enables keyboard navigation and selection within web pages using a
    138       visible caret, false uses normal keyboard navigation with no caret
    139   * accessibility.typeaheadfind
    140     - when set to true, typing outside text areas and input boxes will
    141       automatically start searching for what's typed within the current
    142       document; when set to false, no search action happens */
    143  { id: "accessibility.browsewithcaret", type: "bool" },
    144  { id: "accessibility.typeaheadfind", type: "bool" },
    145  { id: "accessibility.blockautorefresh", type: "bool" },
    146 
    147  /* Zoom */
    148  { id: "browser.zoom.full", type: "bool" },
    149 
    150  /* Browsing
    151   * general.autoScroll
    152     - when set to true, clicking the scroll wheel on the mouse activates a
    153       mouse mode where moving the mouse down scrolls the document downward with
    154       speed correlated with the distance of the cursor from the original
    155       position at which the click occurred (and likewise with movement upward);
    156       if false, this behavior is disabled
    157   * general.smoothScroll
    158     - set to true to enable finer page scrolling than line-by-line on page-up,
    159       page-down, and other such page movements */
    160  { id: "general.autoScroll", type: "bool" },
    161  { id: "general.smoothScroll", type: "bool" },
    162  { id: "widget.gtk.overlay-scrollbars.enabled", type: "bool", inverted: true },
    163  { id: "layout.css.always_underline_links", type: "bool" },
    164  { id: "layout.spellcheckDefault", type: "int" },
    165  { id: "accessibility.tabfocus", type: "int" },
    166  { id: "browser.ml.linkPreview.enabled", type: "bool" },
    167  { id: "browser.ml.linkPreview.optin", type: "bool" },
    168  { id: "browser.ml.linkPreview.longPress", type: "bool" },
    169 
    170  {
    171    id: "browser.preferences.defaultPerformanceSettings.enabled",
    172    type: "bool",
    173  },
    174  { id: "dom.ipc.processCount", type: "int" },
    175  { id: "dom.ipc.processCount.web", type: "int" },
    176  { id: "layers.acceleration.disabled", type: "bool", inverted: true },
    177 
    178  // Files and Applications
    179  { id: "pref.downloads.disable_button.edit_actions", type: "bool" },
    180 
    181  // DRM content
    182  { id: "media.eme.enabled", type: "bool" },
    183 
    184  // Update
    185  { id: "browser.preferences.advanced.selectedTabIndex", type: "int" },
    186  { id: "browser.search.update", type: "bool" },
    187 
    188  { id: "privacy.userContext.enabled", type: "bool" },
    189  {
    190    id: "privacy.userContext.newTabContainerOnLeftClick.enabled",
    191    type: "bool",
    192  },
    193 
    194  // Picture-in-Picture
    195  {
    196    id: "media.videocontrols.picture-in-picture.video-toggle.enabled",
    197    type: "bool",
    198  },
    199  {
    200    id: "media.videocontrols.picture-in-picture.enable-when-switching-tabs.enabled",
    201    type: "bool",
    202  },
    203 
    204  // Media
    205  { id: "media.hardwaremediakeys.enabled", type: "bool" },
    206 
    207  // Appearance
    208  { id: "layout.css.prefers-color-scheme.content-override", type: "int" },
    209 
    210  // Translations
    211  { id: "browser.translations.automaticallyPopup", type: "bool" },
    212 ]);
    213 
    214 if (AppConstants.HAVE_SHELL_SERVICE) {
    215  Preferences.addAll([
    216    { id: "browser.shell.checkDefaultBrowser", type: "bool" },
    217    { id: "pref.general.disable_button.default_browser", type: "bool" },
    218  ]);
    219 }
    220 
    221 if (AppConstants.platform === "win") {
    222  Preferences.addAll([
    223    { id: "browser.taskbar.previews.enable", type: "bool" },
    224    { id: "ui.osk.enabled", type: "bool" },
    225  ]);
    226 }
    227 
    228 if (AppConstants.MOZ_UPDATER) {
    229  Preferences.addAll([
    230    { id: "app.update.disable_button.showUpdateHistory", type: "bool" },
    231  ]);
    232 
    233  if (AppConstants.NIGHTLY_BUILD) {
    234    Preferences.addAll([{ id: "app.update.suppressPrompts", type: "bool" }]);
    235  }
    236 }
    237 
    238 Preferences.addSetting({
    239  id: "privateBrowsingAutoStart",
    240  pref: "browser.privatebrowsing.autostart",
    241 });
    242 
    243 Preferences.addSetting(
    244  /** @type {{ _getLaunchOnLoginApprovedCachedValue: boolean } & SettingConfig} */ ({
    245    id: "launchOnLoginApproved",
    246    _getLaunchOnLoginApprovedCachedValue: true,
    247    get() {
    248      return this._getLaunchOnLoginApprovedCachedValue;
    249    },
    250    // Check for a launch on login registry key
    251    // This accounts for if a user manually changes it in the registry
    252    // Disabling in Task Manager works outside of just deleting the registry key
    253    // in HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run
    254    // but it is not possible to change it back to enabled as the disabled value is just a random
    255    // hexadecimal number
    256    setup() {
    257      if (AppConstants.platform !== "win") {
    258        /**
    259         * WindowsLaunchOnLogin isnt available if not on windows
    260         * but this setup function still fires, so must prevent
    261         * WindowsLaunchOnLogin.getLaunchOnLoginApproved
    262         * below from executing unnecessarily.
    263         */
    264        return;
    265      }
    266      // @ts-ignore bug 1996860
    267      WindowsLaunchOnLogin.getLaunchOnLoginApproved().then(val => {
    268        this._getLaunchOnLoginApprovedCachedValue = val;
    269      });
    270    },
    271  })
    272 );
    273 
    274 Preferences.addSetting({
    275  id: "windowsLaunchOnLoginEnabled",
    276  pref: "browser.startup.windowsLaunchOnLogin.enabled",
    277 });
    278 
    279 Preferences.addSetting(
    280  /** @type {{_getLaunchOnLoginEnabledValue: boolean, startWithLastProfile: boolean} & SettingConfig} */ ({
    281    id: "windowsLaunchOnLogin",
    282    deps: ["launchOnLoginApproved", "windowsLaunchOnLoginEnabled"],
    283    _getLaunchOnLoginEnabledValue: false,
    284    get startWithLastProfile() {
    285      return Cc["@mozilla.org/toolkit/profile-service;1"].getService(
    286        Ci.nsIToolkitProfileService
    287      ).startWithLastProfile;
    288    },
    289    get() {
    290      return this._getLaunchOnLoginEnabledValue;
    291    },
    292    setup(emitChange) {
    293      if (AppConstants.platform !== "win") {
    294        /**
    295         * WindowsLaunchOnLogin isnt available if not on windows
    296         * but this setup function still fires, so must prevent
    297         * WindowsLaunchOnLogin.getLaunchOnLoginEnabled
    298         * below from executing unnecessarily.
    299         */
    300        return;
    301      }
    302 
    303      /** @type {boolean} */
    304      let getLaunchOnLoginEnabledValue;
    305      let maybeEmitChange = () => {
    306        if (
    307          getLaunchOnLoginEnabledValue !== this._getLaunchOnLoginEnabledValue
    308        ) {
    309          this._getLaunchOnLoginEnabledValue = getLaunchOnLoginEnabledValue;
    310          emitChange();
    311        }
    312      };
    313      if (!this.startWithLastProfile) {
    314        getLaunchOnLoginEnabledValue = false;
    315        maybeEmitChange();
    316      } else {
    317        // @ts-ignore bug 1996860
    318        WindowsLaunchOnLogin.getLaunchOnLoginEnabled().then(val => {
    319          getLaunchOnLoginEnabledValue = val;
    320          maybeEmitChange();
    321        });
    322      }
    323    },
    324    visible: ({ windowsLaunchOnLoginEnabled }) => {
    325      let isVisible =
    326        AppConstants.platform === "win" && windowsLaunchOnLoginEnabled.value;
    327      if (isVisible) {
    328        // @ts-ignore bug 1996860
    329        NimbusFeatures.windowsLaunchOnLogin.recordExposureEvent({
    330          once: true,
    331        });
    332      }
    333      return isVisible;
    334    },
    335    disabled({ launchOnLoginApproved }) {
    336      return !this.startWithLastProfile || !launchOnLoginApproved.value;
    337    },
    338    onUserChange(checked) {
    339      if (checked) {
    340        // windowsLaunchOnLogin has been checked: create registry key or shortcut
    341        // The shortcut is created with the same AUMID as Firefox itself. However,
    342        // this is not set during browser tests and the fallback of checking the
    343        // registry fails. As such we pass an arbitrary AUMID for the purpose
    344        // of testing.
    345        // @ts-ignore bug 1996860
    346        WindowsLaunchOnLogin.createLaunchOnLogin();
    347        Services.prefs.setBoolPref(
    348          "browser.startup.windowsLaunchOnLogin.disableLaunchOnLoginPrompt",
    349          true
    350        );
    351      } else {
    352        // windowsLaunchOnLogin has been unchecked: delete registry key and shortcut
    353        // @ts-ignore bug 1996860
    354        WindowsLaunchOnLogin.removeLaunchOnLogin();
    355      }
    356    },
    357  })
    358 );
    359 
    360 Preferences.addSetting({
    361  id: "windowsLaunchOnLoginDisabledProfileBox",
    362  deps: ["windowsLaunchOnLoginEnabled"],
    363  visible: ({ windowsLaunchOnLoginEnabled }) => {
    364    if (AppConstants.platform !== "win") {
    365      return false;
    366    }
    367    let startWithLastProfile = Cc[
    368      "@mozilla.org/toolkit/profile-service;1"
    369    ].getService(Ci.nsIToolkitProfileService).startWithLastProfile;
    370 
    371    return !startWithLastProfile && windowsLaunchOnLoginEnabled.value;
    372  },
    373 });
    374 
    375 Preferences.addSetting({
    376  id: "windowsLaunchOnLoginDisabledBox",
    377  deps: ["launchOnLoginApproved", "windowsLaunchOnLoginEnabled"],
    378  visible: ({ launchOnLoginApproved, windowsLaunchOnLoginEnabled }) => {
    379    if (AppConstants.platform !== "win") {
    380      return false;
    381    }
    382    let startWithLastProfile = Cc[
    383      "@mozilla.org/toolkit/profile-service;1"
    384    ].getService(Ci.nsIToolkitProfileService).startWithLastProfile;
    385 
    386    return (
    387      startWithLastProfile &&
    388      !launchOnLoginApproved.value &&
    389      windowsLaunchOnLoginEnabled.value
    390    );
    391  },
    392 });
    393 
    394 Preferences.addSetting({
    395  /**
    396   * The "Open previous windows and tabs" option on about:preferences page.
    397   */
    398  id: "browserRestoreSession",
    399  pref: "browser.startup.page",
    400  deps: ["privateBrowsingAutoStart"],
    401  get:
    402    /**
    403     * Returns the value of the "Open previous windows and tabs" option based
    404     * on the value of the browser.privatebrowsing.autostart pref.
    405     *
    406     * @param {number | undefined} value
    407     * @returns {boolean}
    408     */
    409    value => {
    410      const pbAutoStartPref = Preferences.get(
    411        "browser.privatebrowsing.autostart"
    412      );
    413      let newValue = pbAutoStartPref.value
    414        ? false
    415        : value === gMainPane.STARTUP_PREF_RESTORE_SESSION;
    416 
    417      return newValue;
    418    },
    419  set: checked => {
    420    const startupPref = Preferences.get("browser.startup.page");
    421    let newValue;
    422 
    423    if (checked) {
    424      // We need to restore the blank homepage setting in our other pref
    425      if (startupPref.value === gMainPane.STARTUP_PREF_BLANK) {
    426        // @ts-ignore bug 1996860
    427        HomePage.safeSet("about:blank");
    428      }
    429      newValue = gMainPane.STARTUP_PREF_RESTORE_SESSION;
    430    } else {
    431      newValue = gMainPane.STARTUP_PREF_HOMEPAGE;
    432    }
    433    return newValue;
    434  },
    435  disabled: deps => {
    436    return deps.privateBrowsingAutoStart.value;
    437  },
    438 });
    439 
    440 Preferences.addSetting({
    441  id: "useAutoScroll",
    442  pref: "general.autoScroll",
    443 });
    444 Preferences.addSetting({
    445  id: "useSmoothScrolling",
    446  pref: "general.smoothScroll",
    447 });
    448 
    449 Preferences.addSetting({
    450  id: "useOverlayScrollbars",
    451  pref: "widget.gtk.overlay-scrollbars.enabled",
    452  visible: () => AppConstants.MOZ_WIDGET_GTK,
    453 });
    454 Preferences.addSetting({
    455  id: "useOnScreenKeyboard",
    456  // Bug 1993053: Restore the pref to `ui.osk.enabled` after changing
    457  // the PrefereceNotFoundError throwing behavior.
    458  pref: AppConstants.platform == "win" ? "ui.osk.enabled" : undefined,
    459  visible: () => AppConstants.platform == "win",
    460 });
    461 Preferences.addSetting({
    462  id: "useCursorNavigation",
    463  pref: "accessibility.browsewithcaret",
    464 });
    465 Preferences.addSetting(
    466  /** @type {{ _storedFullKeyboardNavigation: number } & SettingConfig} */ ({
    467    _storedFullKeyboardNavigation: -1,
    468    id: "useFullKeyboardNavigation",
    469    pref: "accessibility.tabfocus",
    470    visible: () => AppConstants.platform == "macosx",
    471    /**
    472     * Returns true if any full keyboard nav is enabled and false otherwise, caching
    473     * the current value to enable proper pref restoration if the checkbox is
    474     * never changed.
    475     *
    476     * accessibility.tabfocus
    477     * - an integer controlling the focusability of:
    478     *     1  text controls
    479     *     2  form elements
    480     *     4  links
    481     *     7  all of the above
    482     */
    483    get(prefVal) {
    484      this._storedFullKeyboardNavigation = prefVal;
    485      return prefVal == 7;
    486    },
    487    /**
    488     * Returns the value of the full keyboard nav preference represented by UI,
    489     * preserving the preference's "hidden" value if the preference is
    490     * unchanged and represents a value not strictly allowed in UI.
    491     */
    492    set(checked) {
    493      if (checked) {
    494        return 7;
    495      }
    496      if (this._storedFullKeyboardNavigation != 7) {
    497        // 1/2/4 values set via about:config should persist
    498        return this._storedFullKeyboardNavigation;
    499      }
    500      // When the checkbox is unchecked, default to just text controls.
    501      return 1;
    502    },
    503  })
    504 );
    505 Preferences.addSetting({
    506  id: "linkPreviewEnabled",
    507  pref: "browser.ml.linkPreview.enabled",
    508  // @ts-ignore bug 1996860
    509  visible: () => false, // LinkPreview is missing. tor-browser#44045.
    510 });
    511 Preferences.addSetting({
    512  id: "linkPreviewKeyPoints",
    513  pref: "browser.ml.linkPreview.optin",
    514  // @ts-ignore bug 1996860
    515  visible: () => false, // LinkPreview is missing. tor-browser#44045.
    516 });
    517 Preferences.addSetting({
    518  id: "linkPreviewLongPress",
    519  pref: "browser.ml.linkPreview.longPress",
    520 });
    521 Preferences.addSetting({
    522  id: "alwaysUnderlineLinks",
    523  pref: "layout.css.always_underline_links",
    524 });
    525 Preferences.addSetting({
    526  id: "searchStartTyping",
    527  pref: "accessibility.typeaheadfind",
    528 });
    529 Preferences.addSetting({
    530  id: "pictureInPictureToggleEnabled",
    531  pref: "media.videocontrols.picture-in-picture.video-toggle.enabled",
    532  visible: () =>
    533    Services.prefs.getBoolPref(
    534      "media.videocontrols.picture-in-picture.enabled"
    535    ),
    536  onUserChange(checked) {
    537    if (!checked) {
    538      Glean.pictureinpictureSettings.disableSettings.record();
    539    }
    540  },
    541 });
    542 Preferences.addSetting({
    543  id: "pictureInPictureEnableWhenSwitchingTabs",
    544  pref: "media.videocontrols.picture-in-picture.enable-when-switching-tabs.enabled",
    545  deps: ["pictureInPictureToggleEnabled"],
    546  onUserChange(checked) {
    547    if (checked) {
    548      Glean.pictureinpictureSettings.enableAutotriggerSettings.record();
    549    }
    550  },
    551 });
    552 Preferences.addSetting({
    553  id: "mediaControlToggleEnabled",
    554  pref: "media.hardwaremediakeys.enabled",
    555  // For media control toggle button, we support it on Windows, macOS and
    556  // gtk-based Linux.
    557  visible: () =>
    558    AppConstants.platform == "win" ||
    559    AppConstants.platform == "macosx" ||
    560    AppConstants.MOZ_WIDGET_GTK,
    561 });
    562 Preferences.addSetting({
    563  id: "playDRMContent",
    564  pref: "media.eme.enabled",
    565  visible: () => {
    566    if (!Services.prefs.getBoolPref("browser.eme.ui.enabled", false)) {
    567      return false;
    568    }
    569    if (AppConstants.platform == "win") {
    570      try {
    571        return parseFloat(Services.sysinfo.get("version")) >= 6;
    572      } catch (ex) {
    573        return false;
    574      }
    575    }
    576    return true;
    577  },
    578 });
    579 Preferences.addSetting({
    580  id: "cfrRecommendations",
    581  pref: "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons",
    582 });
    583 Preferences.addSetting({
    584  id: "cfrRecommendations-features",
    585  pref: "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features",
    586 });
    587 Preferences.addSetting({
    588  id: "web-appearance-override-warning",
    589  setup: emitChange => {
    590    FORCED_COLORS_QUERY.addEventListener("change", emitChange);
    591    return () => FORCED_COLORS_QUERY.removeEventListener("change", emitChange);
    592  },
    593  visible: () => {
    594    return FORCED_COLORS_QUERY.matches;
    595  },
    596 });
    597 
    598 Preferences.addSetting(
    599  /** @type {{ themeNames: string[] } & SettingConfig}} */ ({
    600    id: "web-appearance-chooser",
    601    themeNames: ["dark", "light", "auto"],
    602    pref: "layout.css.prefers-color-scheme.content-override",
    603    setup(emitChange) {
    604      Services.obs.addObserver(emitChange, "look-and-feel-changed");
    605      return () =>
    606        Services.obs.removeObserver(emitChange, "look-and-feel-changed");
    607    },
    608    get(val, _, setting) {
    609      return (
    610        this.themeNames[val] ||
    611        this.themeNames[/** @type {number} */ (setting.pref.defaultValue)]
    612      );
    613    },
    614    /** @param {string} val */
    615    set(val) {
    616      return this.themeNames.indexOf(val);
    617    },
    618    getControlConfig(config) {
    619      // Set the auto theme image to the light/dark that matches.
    620      let systemThemeIndex = Services.appinfo
    621        .contentThemeDerivedColorSchemeIsDark
    622        ? 2
    623        : 1;
    624      config.options[0].controlAttrs = {
    625        ...config.options[0].controlAttrs,
    626        imagesrc: config.options[systemThemeIndex].controlAttrs.imagesrc,
    627      };
    628      return config;
    629    },
    630  })
    631 );
    632 
    633 Preferences.addSetting({
    634  id: "web-appearance-manage-themes-link",
    635  onUserClick: e => {
    636    e.preventDefault();
    637    // @ts-ignore topChromeWindow global
    638    window.browsingContext.topChromeWindow.BrowserAddonUI.openAddonsMgr(
    639      "addons://list/theme"
    640    );
    641  },
    642 });
    643 
    644 Preferences.addSetting({
    645  id: "containersPane",
    646  onUserClick(e) {
    647    e.preventDefault();
    648    gotoPref("paneContainers2");
    649  },
    650 });
    651 Preferences.addSetting({ id: "containersPlaceholder" });
    652 
    653 Preferences.addSetting({
    654  id: "offerTranslations",
    655  pref: "browser.translations.automaticallyPopup",
    656 });
    657 
    658 function createNeverTranslateSitesDescription() {
    659  const description = document.createElement("span");
    660  description.dataset.l10nId =
    661    "settings-translations-subpage-never-translate-sites-description";
    662 
    663  for (const [name, src] of [
    664    ["translations-icon", "chrome://browser/skin/translations.svg"],
    665    ["settings-icon", "chrome://global/skin/icons/settings.svg"],
    666  ]) {
    667    const icon = document.createElement("img");
    668    icon.src = src;
    669 
    670    icon.dataset.l10nName = name;
    671    icon.style.verticalAlign = "middle";
    672 
    673    icon.setAttribute("role", "presentation");
    674    icon.setAttribute("width", "16");
    675    icon.setAttribute("height", "16");
    676 
    677    description.appendChild(icon);
    678  }
    679 
    680  return description;
    681 }
    682 
    683 Preferences.addSetting({
    684  id: "translationsDownloadLanguagesGroup",
    685 });
    686 
    687 Preferences.addSetting({
    688  id: "translationsDownloadLanguagesRow",
    689 });
    690 
    691 Preferences.addSetting({
    692  id: "translationsDownloadLanguagesSelect",
    693 });
    694 
    695 Preferences.addSetting({
    696  id: "translationsDownloadLanguagesButton",
    697 });
    698 
    699 Preferences.addSetting({
    700  id: "translationsDownloadLanguagesNoneRow",
    701 });
    702 
    703 Preferences.addSetting({
    704  id: "translationsAlwaysTranslateLanguagesGroup",
    705 });
    706 
    707 Preferences.addSetting({
    708  id: "translationsAlwaysTranslateLanguagesRow",
    709 });
    710 
    711 Preferences.addSetting({
    712  id: "translationsAlwaysTranslateLanguagesSelect",
    713 });
    714 
    715 Preferences.addSetting({
    716  id: "translationsAlwaysTranslateLanguagesNoneRow",
    717 });
    718 
    719 Preferences.addSetting({
    720  id: "translationsAlwaysTranslateLanguagesButton",
    721 });
    722 
    723 Preferences.addSetting({
    724  id: "translationsNeverTranslateLanguagesNoneRow",
    725 });
    726 
    727 Preferences.addSetting({
    728  id: "translationsNeverTranslateLanguagesButton",
    729 });
    730 
    731 Preferences.addSetting({
    732  id: "translationsNeverTranslateLanguagesGroup",
    733 });
    734 
    735 Preferences.addSetting({
    736  id: "translationsNeverTranslateLanguagesRow",
    737 });
    738 
    739 Preferences.addSetting({
    740  id: "translationsNeverTranslateLanguagesSelect",
    741 });
    742 
    743 Preferences.addSetting({
    744  id: "translationsNeverTranslateSitesGroup",
    745 });
    746 
    747 Preferences.addSetting({
    748  id: "translationsNeverTranslateSitesRow",
    749 });
    750 
    751 Preferences.addSetting({
    752  id: "translationsNeverTranslateSitesNoneRow",
    753 });
    754 
    755 Preferences.addSetting({
    756  id: "translationsManageButton",
    757  onUserClick(e) {
    758    e.preventDefault();
    759    gotoPref("paneTranslations");
    760  },
    761 });
    762 
    763 Preferences.addSetting({
    764  id: "data-migration",
    765  visible: () =>
    766    !Services.policies || Services.policies.isAllowed("profileImport"),
    767  onUserClick() {
    768    const browserWindow = window.browsingContext.topChromeWindow;
    769    MigrationUtils.showMigrationWizard(browserWindow, {
    770      entrypoint: MigrationUtils.MIGRATION_ENTRYPOINTS.PREFERENCES,
    771    });
    772  },
    773 });
    774 
    775 Preferences.addSetting({
    776  id: "connectionSettings",
    777  onUserClick: () => gMainPane.showConnections(),
    778 });
    779 
    780 Preferences.addSetting({
    781  id: "profilesPane",
    782  onUserClick(e) {
    783    e.preventDefault();
    784    gotoPref("paneProfiles");
    785  },
    786 });
    787 Preferences.addSetting({
    788  id: "profilesSettings",
    789  visible() {
    790    return SelectableProfileService.isEnabled;
    791  },
    792  onUserClick: e => {
    793    e.preventDefault();
    794    gotoPref("profiles");
    795  },
    796 });
    797 Preferences.addSetting({
    798  id: "manageProfiles",
    799  onUserClick: e => {
    800    e.preventDefault();
    801    // Using the existing function for now, since privacy.js also calls it
    802    gMainPane.manageProfiles();
    803  },
    804 });
    805 Preferences.addSetting({
    806  id: "copyProfile",
    807  deps: ["copyProfileSelect"],
    808  disabled: ({ copyProfileSelect }) => !copyProfileSelect.value,
    809  onUserClick: (e, { copyProfileSelect }) => {
    810    e.preventDefault();
    811    SelectableProfileService.getProfile(copyProfileSelect.value).then(
    812      profile => {
    813        profile?.copyProfile();
    814        copyProfileSelect.config.set("");
    815      }
    816    );
    817  },
    818 });
    819 Preferences.addSetting({
    820  id: "copyProfileBox",
    821  visible: () => SelectableProfileService.initialized,
    822 });
    823 Preferences.addSetting({
    824  id: "copyProfileError",
    825  _hasError: false,
    826  setup(emitChange) {
    827    this.emitChange = emitChange;
    828  },
    829  visible() {
    830    return this._hasError;
    831  },
    832  setError(value) {
    833    this._hasError = !!value;
    834    this.emitChange();
    835  },
    836 });
    837 Preferences.addSetting(
    838  class ProfileList extends Preferences.AsyncSetting {
    839    static id = "profileList";
    840    static PROFILE_UPDATED_OBS = "sps-profiles-updated";
    841    setup() {
    842      Services.obs.addObserver(
    843        this.emitChange,
    844        ProfileList.PROFILE_UPDATED_OBS
    845      );
    846      return () => {
    847        Services.obs.removeObserver(
    848          this.emitChange,
    849          ProfileList.PROFILE_UPDATED_OBS
    850        );
    851      };
    852    }
    853 
    854    async get() {
    855      let profiles = await SelectableProfileService.getAllProfiles();
    856      return profiles;
    857    }
    858  }
    859 );
    860 Preferences.addSetting({
    861  id: "copyProfileSelect",
    862  deps: ["profileList"],
    863  _selectedProfile: null,
    864  setup(emitChange) {
    865    this.emitChange = emitChange;
    866    document.l10n
    867      .formatValue("preferences-copy-profile-select")
    868      .then(result => (this.placeholderString = result));
    869  },
    870  get() {
    871    return this._selectedProfile;
    872  },
    873  set(inputVal) {
    874    this._selectedProfile = inputVal;
    875    this.emitChange();
    876  },
    877  getControlConfig(config, { profileList }) {
    878    config.options = profileList.value.map(profile => {
    879      return { controlAttrs: { label: profile.name }, value: profile.id };
    880    });
    881 
    882    // Put the placeholder at the front of the list.
    883    config.options.unshift({
    884      controlAttrs: { label: this.placeholderString },
    885      value: "",
    886    });
    887 
    888    return config;
    889  },
    890 });
    891 Preferences.addSetting({
    892  id: "copyProfileHeader",
    893  visible: () => SelectableProfileService.initialized,
    894 });
    895 
    896 // Downloads
    897 /*
    898 * Preferences:
    899 *
    900 * browser.download.useDownloadDir - bool
    901 *   True - Save files directly to the folder configured via the
    902 *   browser.download.folderList preference.
    903 *   False - Always ask the user where to save a file and default to
    904 *  browser.download.lastDir when displaying a folder picker dialog.
    905 *  browser.download.deletePrivate - bool
    906 *   True - Delete files that were downloaded in a private browsing session
    907 *   on close of the session
    908 *   False - Keep files that were downloaded in a private browsing
    909 *   session
    910 * browser.download.always_ask_before_handling_new_types - bool
    911 *   Defines the default behavior for new file handlers.
    912 *   True - When downloading a file that doesn't match any existing
    913 *   handlers, ask the user whether to save or open the file.
    914 *   False - Save the file. The user can change the default action in
    915 *   the Applications section in the preferences UI.
    916 * browser.download.dir - local file handle
    917 *   A local folder the user may have selected for downloaded files to be
    918 *   saved. Migration of other browser settings may also set this path.
    919 *   This folder is enabled when folderList equals 2.
    920 * browser.download.lastDir - local file handle
    921 *   May contain the last folder path accessed when the user browsed
    922 *   via the file save-as dialog. (see contentAreaUtils.js)
    923 * browser.download.folderList - int
    924 *   Indicates the location users wish to save downloaded files too.
    925 *   It is also used to display special file labels when the default
    926 *   download location is either the Desktop or the Downloads folder.
    927 *   Values:
    928 *     0 - The desktop is the default download location.
    929 *     1 - The system's downloads folder is the default download location.
    930 *     2 - The default download location is elsewhere as specified in
    931 *         browser.download.dir.
    932 * browser.download.downloadDir
    933 *   deprecated.
    934 * browser.download.defaultFolder
    935 *   deprecated.
    936 */
    937 
    938 /**
    939 * Helper object for managing the various downloads related settings.
    940 */
    941 const DownloadsHelpers = new (class DownloadsHelpers {
    942  folder;
    943  folderPath;
    944  folderHostPath;
    945  displayName;
    946  downloadsDir;
    947  desktopDir;
    948  downloadsFolderLocalizedName;
    949  desktopFolderLocalizedName;
    950 
    951  setupDownloadsHelpersFields = async () => {
    952    this.downloadsDir = await this._getDownloadsFolder("Downloads");
    953    this.desktopDir = await this._getDownloadsFolder("Desktop");
    954    [this.downloadsFolderLocalizedName, this.desktopFolderLocalizedName] =
    955      await document.l10n.formatValues([
    956        { id: "downloads-folder-name" },
    957        { id: "desktop-folder-name" },
    958      ]);
    959  };
    960 
    961  /**
    962   * Returns the Downloads folder.  If aFolder is "Desktop", then the Downloads
    963   * folder returned is the desktop folder; otherwise, it is a folder whose name
    964   * indicates that it is a download folder and whose path is as determined by
    965   * the XPCOM directory service via the download manager's attribute
    966   * defaultDownloadsDirectory.
    967   *
    968   * @throws if aFolder is not "Desktop" or "Downloads"
    969   */
    970  async _getDownloadsFolder(aFolder) {
    971    switch (aFolder) {
    972      case "Desktop":
    973        return Services.dirsvc.get("Desk", Ci.nsIFile);
    974      case "Downloads": {
    975        let downloadsDir = await Downloads.getSystemDownloadsDirectory();
    976        return new FileUtils.File(downloadsDir);
    977      }
    978    }
    979    throw new Error(
    980      "ASSERTION FAILED: folder type should be 'Desktop' or 'Downloads'"
    981    );
    982  }
    983 
    984  _getSystemDownloadFolderDetails(folderIndex) {
    985    let currentDirPref = Preferences.get("browser.download.dir");
    986 
    987    let file;
    988    let firefoxLocalizedName;
    989    if (folderIndex == 2 && currentDirPref.value) {
    990      file = currentDirPref.value;
    991      if (file.equals(this.downloadsDir)) {
    992        folderIndex = 1;
    993      } else if (file.equals(this.desktopDir)) {
    994        folderIndex = 0;
    995      }
    996    }
    997    switch (folderIndex) {
    998      case 2: // custom path, handled above.
    999        break;
   1000 
   1001      case 1: {
   1002        // downloads
   1003        file = this.downloadsDir;
   1004        firefoxLocalizedName = this.downloadsFolderLocalizedName;
   1005        break;
   1006      }
   1007 
   1008      case 0:
   1009      // fall through
   1010      default: {
   1011        file = this.desktopDir;
   1012        firefoxLocalizedName = this.desktopFolderLocalizedName;
   1013      }
   1014    }
   1015 
   1016    if (file) {
   1017      let displayName = file.path;
   1018 
   1019      // Attempt to translate path to the path as exists on the host
   1020      // in case the provided path comes from the document portal
   1021      if (AppConstants.platform == "linux") {
   1022        if (this.folderHostPath && displayName == this.folderPath) {
   1023          displayName = this.folderHostPath;
   1024          if (displayName == this.downloadsDir.path) {
   1025            firefoxLocalizedName = this.downloadsFolderLocalizedName;
   1026          } else if (displayName == this.desktopDir.path) {
   1027            firefoxLocalizedName = this.desktopFolderLocalizedName;
   1028          }
   1029        } else if (displayName != this.folderPath) {
   1030          this.folderHostPath = null;
   1031          try {
   1032            file.hostPath().then(folderHostPath => {
   1033              this.folderHostPath = folderHostPath;
   1034              Preferences.getSetting("downloadFolder")?.onChange();
   1035            });
   1036          } catch (error) {
   1037            /* ignored */
   1038          }
   1039        }
   1040      }
   1041 
   1042      if (firefoxLocalizedName) {
   1043        let folderDisplayName, leafName;
   1044        // Either/both of these can throw, so check for failures in both cases
   1045        // so we don't just break display of the download pref:
   1046        try {
   1047          folderDisplayName = file.displayName;
   1048        } catch (ex) {
   1049          /* ignored */
   1050        }
   1051        try {
   1052          leafName = file.leafName;
   1053        } catch (ex) {
   1054          /* ignored */
   1055        }
   1056 
   1057        // If we found a localized name that's different from the leaf name,
   1058        // use that:
   1059        if (folderDisplayName && folderDisplayName != leafName) {
   1060          return { file, folderDisplayName };
   1061        }
   1062 
   1063        // Otherwise, check if we've got a localized name ourselves.
   1064        // You can't move the system download or desktop dir on macOS,
   1065        // so if those are in use just display them. On other platforms
   1066        // only do so if the folder matches the localized name.
   1067        if (
   1068          AppConstants.platform == "macosx" ||
   1069          leafName == firefoxLocalizedName
   1070        ) {
   1071          return { file, folderDisplayName: firefoxLocalizedName };
   1072        }
   1073      }
   1074 
   1075      // If we get here, attempts to use a "pretty" name failed. Just display
   1076      // the full path:
   1077      // Force the left-to-right direction when displaying a custom path.
   1078      return { file, folderDisplayName: `\u2066${displayName}\u2069` };
   1079    }
   1080 
   1081    // Don't even have a file - fall back to desktop directory for the
   1082    // use of the icon, and an empty label:
   1083    file = this.desktopDir;
   1084    return { file, folderDisplayName: "" };
   1085  }
   1086 
   1087  /**
   1088   * Determines the type of the given folder.
   1089   *
   1090   * @param   aFolder
   1091   *          the folder whose type is to be determined
   1092   * @returns integer
   1093   *          0 if aFolder is the Desktop or is unspecified,
   1094   *          1 if aFolder is the Downloads folder,
   1095   *          2 otherwise
   1096   */
   1097  _folderToIndex(aFolder) {
   1098    if (!aFolder || aFolder.equals(this.desktopDir)) {
   1099      return 0;
   1100    } else if (aFolder.equals(this.downloadsDir)) {
   1101      return 1;
   1102    }
   1103    return 2;
   1104  }
   1105 
   1106  getFolderDetails() {
   1107    let folderIndex = Preferences.get("browser.download.folderList").value;
   1108    let { folderDisplayName, file } =
   1109      this._getSystemDownloadFolderDetails(folderIndex);
   1110 
   1111    this.folderPath = file?.path ?? "";
   1112    this.displayName = folderDisplayName;
   1113  }
   1114 
   1115  setFolder(folder) {
   1116    this.folder = folder;
   1117 
   1118    let folderListPref = Preferences.get("browser.download.folderList");
   1119    folderListPref.value = this._folderToIndex(this.folder);
   1120  }
   1121 })();
   1122 
   1123 Preferences.addSetting({
   1124  id: "browserDownloadFolderList",
   1125  pref: "browser.download.folderList",
   1126 });
   1127 Preferences.addSetting({
   1128  id: "downloadFolder",
   1129  pref: "browser.download.dir",
   1130  deps: ["browserDownloadFolderList"],
   1131  get() {
   1132    DownloadsHelpers.getFolderDetails();
   1133    return DownloadsHelpers.folderPath;
   1134  },
   1135  set(folder) {
   1136    DownloadsHelpers.setFolder(folder);
   1137    return DownloadsHelpers.folder;
   1138  },
   1139  getControlConfig(config) {
   1140    if (DownloadsHelpers.displayName) {
   1141      return {
   1142        ...config,
   1143        controlAttrs: {
   1144          ...config.controlAttrs,
   1145          ".displayValue": DownloadsHelpers.displayName,
   1146        },
   1147      };
   1148    }
   1149    return {
   1150      ...config,
   1151    };
   1152  },
   1153  setup(emitChange) {
   1154    DownloadsHelpers.setupDownloadsHelpersFields().then(emitChange);
   1155  },
   1156  disabled: ({ browserDownloadFolderList }) => {
   1157    return browserDownloadFolderList.locked;
   1158  },
   1159 });
   1160 Preferences.addSetting({
   1161  id: "alwaysAsk",
   1162  pref: "browser.download.useDownloadDir",
   1163 });
   1164 Preferences.addSetting({
   1165  id: "enableDeletePrivate",
   1166  pref: "browser.download.enableDeletePrivate",
   1167 });
   1168 Preferences.addSetting({
   1169  id: "deletePrivate",
   1170  pref: "browser.download.deletePrivate",
   1171  deps: ["enableDeletePrivate"],
   1172  visible: ({ enableDeletePrivate }) => enableDeletePrivate.value,
   1173  onUserChange() {
   1174    Services.prefs.setBoolPref("browser.download.deletePrivate.chosen", true);
   1175  },
   1176 });
   1177 /**
   1178 * A helper object containing all logic related to
   1179 * setting the browser as the user's default.
   1180 */
   1181 const DefaultBrowserHelper = {
   1182  /**
   1183   * @type {number}
   1184   */
   1185  _backoffIndex: 0,
   1186 
   1187  /**
   1188   * @type {number | undefined}
   1189   */
   1190  _pollingTimer: undefined,
   1191 
   1192  /**
   1193   * Keeps track of the last known browser
   1194   * default value set to compare while polling.
   1195   *
   1196   * @type {boolean | undefined}
   1197   */
   1198  _lastPolledIsDefault: undefined,
   1199 
   1200  /**
   1201   * @type {typeof import('../shell/ShellService.sys.mjs').ShellService | undefined}
   1202   */
   1203  get shellSvc() {
   1204    return (
   1205      AppConstants.HAVE_SHELL_SERVICE &&
   1206      // @ts-ignore from utilityOverlay.js
   1207      getShellService()
   1208    );
   1209  },
   1210 
   1211  /**
   1212   * Sets up polling of whether the browser is set to default,
   1213   * and calls provided hasChanged function when the state changes.
   1214   *
   1215   * @param {Function} hasChanged
   1216   */
   1217  pollForDefaultChanges(hasChanged) {
   1218    if (this._pollingTimer) {
   1219      return;
   1220    }
   1221    this._lastPolledIsDefault = this.isBrowserDefault;
   1222 
   1223    // Exponential backoff mechanism will delay the polling times if user doesn't
   1224    // trigger SetDefaultBrowser for a long time.
   1225    const backoffTimes = [
   1226      1000, 1000, 1000, 1000, 2000, 2000, 2000, 5000, 5000, 10000,
   1227    ];
   1228 
   1229    const pollForDefaultBrowser = () => {
   1230      if (
   1231        (location.hash == "" || location.hash == "#general") &&
   1232        document.visibilityState == "visible"
   1233      ) {
   1234        const { isBrowserDefault } = this;
   1235        if (isBrowserDefault !== this._lastPolledIsDefault) {
   1236          this._lastPolledIsDefault = isBrowserDefault;
   1237          hasChanged();
   1238        }
   1239      }
   1240 
   1241      if (!this._pollingTimer) {
   1242        return;
   1243      }
   1244 
   1245      // approximately a "requestIdleInterval"
   1246      this._pollingTimer = window.setTimeout(
   1247        () => {
   1248          window.requestIdleCallback(pollForDefaultBrowser);
   1249        },
   1250        backoffTimes[
   1251          this._backoffIndex + 1 < backoffTimes.length
   1252            ? this._backoffIndex++
   1253            : backoffTimes.length - 1
   1254        ]
   1255      );
   1256    };
   1257 
   1258    this._pollingTimer = window.setTimeout(() => {
   1259      window.requestIdleCallback(pollForDefaultBrowser);
   1260    }, backoffTimes[this._backoffIndex]);
   1261  },
   1262 
   1263  /**
   1264   * Stops timer for polling changes.
   1265   */
   1266  clearPollingForDefaultChanges() {
   1267    if (this._pollingTimer) {
   1268      clearTimeout(this._pollingTimer);
   1269      this._pollingTimer = undefined;
   1270    }
   1271  },
   1272 
   1273  /**
   1274   *  Checks if the browser is default through the shell service.
   1275   */
   1276  get isBrowserDefault() {
   1277    if (!this.canCheck) {
   1278      return false;
   1279    }
   1280    return this.shellSvc?.isDefaultBrowser(false, true);
   1281  },
   1282 
   1283  /**
   1284   * Attempts to set the browser as the user's
   1285   * default through the shell service.
   1286   *
   1287   * @returns {Promise<void>}
   1288   */
   1289  async setDefaultBrowser() {
   1290    // Reset exponential backoff delay time in order to do visual update in pollForDefaultBrowser.
   1291    this._backoffIndex = 0;
   1292 
   1293    try {
   1294      await this.shellSvc?.setDefaultBrowser(false);
   1295    } catch (e) {
   1296      console.error(e);
   1297    }
   1298  },
   1299 
   1300  /**
   1301   * Checks whether the browser is capable of being made default.
   1302   *
   1303   * @type {boolean}
   1304   */
   1305  get canCheck() {
   1306    if (AppConstants.BASE_BROWSER_VERSION) {
   1307      // Disabled for Tor Browser. tor-browser#44343 and tor-browser#41822.
   1308      return false;
   1309    }
   1310    return (
   1311      this.shellSvc &&
   1312      /**
   1313       * Flatpak does not support setting nor detection of default browser
   1314       */
   1315      !gGIOService?.isRunningUnderFlatpak
   1316    );
   1317  },
   1318 };
   1319 
   1320 Preferences.addSetting({
   1321  id: "alwaysCheckDefault",
   1322  pref: "browser.shell.checkDefaultBrowser",
   1323  setup: emitChange => {
   1324    if (!DefaultBrowserHelper.canCheck) {
   1325      return;
   1326    }
   1327    DefaultBrowserHelper.pollForDefaultChanges(emitChange);
   1328    // eslint-disable-next-line consistent-return
   1329    return () => DefaultBrowserHelper.clearPollingForDefaultChanges();
   1330  },
   1331  /**
   1332   * Show button for setting browser as default browser or information that
   1333   * browser is already the default browser.
   1334   */
   1335  visible: () => DefaultBrowserHelper.canCheck,
   1336  disabled: (_, setting) =>
   1337    !DefaultBrowserHelper.canCheck ||
   1338    setting.locked ||
   1339    DefaultBrowserHelper.isBrowserDefault,
   1340 });
   1341 
   1342 Preferences.addSetting({
   1343  id: "isDefaultPane",
   1344  deps: ["alwaysCheckDefault"],
   1345  visible: () =>
   1346    DefaultBrowserHelper.canCheck && DefaultBrowserHelper.isBrowserDefault,
   1347 });
   1348 
   1349 Preferences.addSetting({
   1350  id: "isNotDefaultPane",
   1351  deps: ["alwaysCheckDefault"],
   1352  visible: () =>
   1353    DefaultBrowserHelper.canCheck && !DefaultBrowserHelper.isBrowserDefault,
   1354  onUserClick: (e, { alwaysCheckDefault }) => {
   1355    if (!DefaultBrowserHelper.canCheck) {
   1356      return;
   1357    }
   1358    const setDefaultButton = /** @type {MozButton} */ (e.target);
   1359 
   1360    if (!setDefaultButton) {
   1361      return;
   1362    }
   1363    if (setDefaultButton.disabled) {
   1364      return;
   1365    }
   1366 
   1367    /**
   1368     * Disable the set default button, so that the user
   1369     * doesn't try to hit it again while browser is being set to default.
   1370     */
   1371    setDefaultButton.disabled = true;
   1372    alwaysCheckDefault.value = true;
   1373    DefaultBrowserHelper.setDefaultBrowser().finally(() => {
   1374      setDefaultButton.disabled = false;
   1375    });
   1376  },
   1377 });
   1378 
   1379 // Firefox support settings
   1380 Preferences.addSetting({
   1381  id: "supportLinksGroup",
   1382 });
   1383 Preferences.addSetting({
   1384  id: "supportGetHelp",
   1385 });
   1386 Preferences.addSetting({
   1387  id: "supportShareIdeas",
   1388 });
   1389 
   1390 // Performance settings
   1391 Preferences.addSetting({
   1392  id: "contentProcessCount",
   1393  pref: "dom.ipc.processCount",
   1394 });
   1395 Preferences.addSetting({
   1396  id: "allowHWAccel",
   1397  pref: "layers.acceleration.disabled",
   1398  deps: ["useRecommendedPerformanceSettings"],
   1399  visible({ useRecommendedPerformanceSettings }) {
   1400    return !useRecommendedPerformanceSettings.value;
   1401  },
   1402 });
   1403 Preferences.addSetting({
   1404  id: "useRecommendedPerformanceSettings",
   1405  pref: "browser.preferences.defaultPerformanceSettings.enabled",
   1406  deps: ["contentProcessCount", "allowHWAccel"],
   1407  get(val, { allowHWAccel, contentProcessCount }) {
   1408    if (
   1409      allowHWAccel.value != allowHWAccel.pref.defaultValue ||
   1410      contentProcessCount.value != contentProcessCount.pref.defaultValue
   1411    ) {
   1412      return false;
   1413    }
   1414    return val;
   1415  },
   1416  set(val, { allowHWAccel, contentProcessCount }) {
   1417    if (val) {
   1418      contentProcessCount.value = contentProcessCount.pref.defaultValue;
   1419      allowHWAccel.value = allowHWAccel.pref.defaultValue;
   1420    }
   1421    return val;
   1422  },
   1423 });
   1424 
   1425 Preferences.addSetting({
   1426  id: "payment-item",
   1427  async onUserClick(e) {
   1428    const action = e.target.getAttribute("action");
   1429    const guid = e.target.getAttribute("guid");
   1430    if (action === "remove") {
   1431      let [title, confirm, cancel] = await document.l10n.formatValues([
   1432        { id: "payments-delete-payment-prompt-title" },
   1433        { id: "payments-delete-payment-prompt-confirm-button" },
   1434        { id: "payments-delete-payment-prompt-cancel-button" },
   1435      ]);
   1436      FormAutofillPreferences.prototype.openRemovePaymentDialog(
   1437        guid,
   1438        window.browsingContext.topChromeWindow.browsingContext,
   1439        title,
   1440        confirm,
   1441        cancel
   1442      );
   1443    } else if (action === "edit") {
   1444      FormAutofillPreferences.prototype.openEditCreditCardDialog(guid, window);
   1445    }
   1446  },
   1447 });
   1448 
   1449 Preferences.addSetting({
   1450  id: "add-payment-button",
   1451  deps: ["saveAndFillPayments"],
   1452  setup: (emitChange, _, setting) => {
   1453    function updateDepsAndChange() {
   1454      setting._deps = null;
   1455      emitChange();
   1456    }
   1457    Services.obs.addObserver(
   1458      updateDepsAndChange,
   1459      "formautofill-preferences-initialized"
   1460    );
   1461    return () =>
   1462      Services.obs.removeObserver(
   1463        updateDepsAndChange,
   1464        "formautofill-preferences-initialized"
   1465      );
   1466  },
   1467  onUserClick: ({ target }) => {
   1468    target.ownerGlobal.gSubDialog.open(
   1469      "chrome://formautofill/content/editCreditCard.xhtml"
   1470    );
   1471  },
   1472  disabled: ({ saveAndFillPayments }) => !saveAndFillPayments?.value,
   1473 });
   1474 
   1475 Preferences.addSetting({
   1476  id: "payments-list-header",
   1477 });
   1478 
   1479 Preferences.addSetting({
   1480  id: "no-payments-stored",
   1481 });
   1482 
   1483 Preferences.addSetting(
   1484  class extends Preferences.AsyncSetting {
   1485    static id = "payments-list";
   1486 
   1487    /** @type {Promise<any[]>} */
   1488    paymentMethods;
   1489 
   1490    beforeRefresh() {
   1491      this.paymentMethods = this.getPaymentMethods();
   1492    }
   1493 
   1494    async getPaymentMethods() {
   1495      await FormAutofillPreferences.prototype.initializePaymentsStorage();
   1496      return FormAutofillPreferences.prototype.makePaymentsListItems();
   1497    }
   1498 
   1499    async getControlConfig() {
   1500      return {
   1501        items: await this.paymentMethods,
   1502      };
   1503    }
   1504 
   1505    async visible() {
   1506      return Boolean((await this.paymentMethods).length);
   1507    }
   1508 
   1509    setup() {
   1510      Services.obs.addObserver(this.emitChange, "formautofill-storage-changed");
   1511      return () =>
   1512        Services.obs.removeObserver(
   1513          this.emitChange,
   1514          "formautofill-storage-changed"
   1515        );
   1516    }
   1517  }
   1518 );
   1519 
   1520 // Tabs settings
   1521 
   1522 // "Opening" tabs settings
   1523 Preferences.addSetting({
   1524  id: "tabsOpening",
   1525 });
   1526 /**
   1527 * browser.link.open_newwindow - int
   1528 *   Determines where links targeting new windows should open.
   1529 *   Values:
   1530 *     1 - Open in the current window or tab.
   1531 *     2 - Open in a new window.
   1532 *     3 - Open in a new tab in the most recent window.
   1533 */
   1534 Preferences.addSetting({
   1535  id: "linkTargeting",
   1536  pref: "browser.link.open_newwindow",
   1537  /**
   1538   * Determines where a link which opens a new window will open.
   1539   *
   1540   * @returns |true| if such links should be opened in new tabs
   1541   */
   1542  get: prefVal => {
   1543    return prefVal != 2;
   1544  },
   1545  /**
   1546   * Determines where a link which opens a new window will open.
   1547   *
   1548   * @returns 2 if such links should be opened in new windows,
   1549   *          3 if such links should be opened in new tabs
   1550   */
   1551  set: checked => {
   1552    return checked ? 3 : 2;
   1553  },
   1554 });
   1555 /**
   1556 * browser.tabs.loadInBackground - bool
   1557 *  True - Whether browser should switch to a new tab opened from a link.
   1558 */
   1559 Preferences.addSetting({
   1560  id: "switchToNewTabs",
   1561  pref: "browser.tabs.loadInBackground",
   1562 });
   1563 Preferences.addSetting({
   1564  id: "openAppLinksNextToActiveTab",
   1565  pref: "browser.link.open_newwindow.override.external",
   1566  /**
   1567   * @returns {boolean}
   1568   *   Whether the "Open links in tabs instead of new windows" settings
   1569   *   checkbox should be checked. Should only be checked if the
   1570   *   `browser.link.open_newwindow.override.external` pref is set to the
   1571   *   value of 7 (nsIBrowserDOMWindow.OPEN_NEWTAB_AFTER_CURRENT).
   1572   */
   1573  get: prefVal => {
   1574    return prefVal == Ci.nsIBrowserDOMWindow.OPEN_NEWTAB_AFTER_CURRENT;
   1575  },
   1576  /**
   1577   * This pref has at least 8 valid values but we are offering a checkbox
   1578   * to set one specific value (`7`).
   1579   *
   1580   * @param {boolean} checked
   1581   * @returns {number}
   1582   *   - `7` (`nsIBrowserDOMWindow.OPEN_NEWTAB_AFTER_CURRENT`) if checked
   1583   *   - the default value of
   1584   *     `browser.link.open_newwindow.override.external` if not checked
   1585   */
   1586  set: (checked, _, setting) => {
   1587    return checked
   1588      ? Ci.nsIBrowserDOMWindow.OPEN_NEWTAB_AFTER_CURRENT
   1589      : setting.pref.defaultValue;
   1590  },
   1591  onUserChange: checked => {
   1592    Glean.linkHandling.openNextToActiveTabSettingsEnabled.set(checked);
   1593    Glean.linkHandling.openNextToActiveTabSettingsChange.record({
   1594      checked,
   1595    });
   1596  },
   1597 });
   1598 /**
   1599 * browser.tabs.warnOnOpen - bool
   1600 *   True - Whether the user should be warned when trying to open a lot of
   1601 *          tabs at once (e.g. a large folder of bookmarks), allowing to
   1602 *          cancel the action.
   1603 */
   1604 Preferences.addSetting({
   1605  id: "warnOpenMany",
   1606  pref: "browser.tabs.warnOnOpen",
   1607  // The "opening multiple tabs might slow down Firefox" warning provides
   1608  // an option for not showing this warning again. When the user disables it,
   1609  // we provide checkboxes to re-enable the warning.
   1610  visible: () => TransientPrefs.prefShouldBeVisible("browser.tabs.warnOnOpen"),
   1611 });
   1612 
   1613 // "Interaction" tabs settings
   1614 Preferences.addSetting({
   1615  id: "tabsInteraction",
   1616 });
   1617 Preferences.addSetting({
   1618  id: "ctrlTabRecentlyUsedOrder",
   1619  pref: "browser.ctrlTab.sortByRecentlyUsed",
   1620  onUserClick: () => {
   1621    Services.prefs.clearUserPref("browser.ctrlTab.migrated");
   1622  },
   1623 });
   1624 Preferences.addSetting({
   1625  id: "tabHoverPreview",
   1626  pref: "browser.tabs.hoverPreview.enabled",
   1627 });
   1628 Preferences.addSetting({
   1629  id: "tabPreviewShowThumbnails",
   1630  pref: "browser.tabs.hoverPreview.showThumbnails",
   1631  deps: ["tabHoverPreview"],
   1632  visible: ({ tabHoverPreview }) => !!tabHoverPreview.value,
   1633 });
   1634 Preferences.addSetting({
   1635  id: "smartTabGroups",
   1636  pref: "browser.tabs.groups.smart.enabled",
   1637 });
   1638 Preferences.addSetting({
   1639  id: "tabGroupSuggestions",
   1640  pref: "browser.tabs.groups.smart.userEnabled",
   1641  deps: ["smartTabGroups"],
   1642  visible: ({ smartTabGroups }) =>
   1643    !!smartTabGroups.value && Services.locale.appLocaleAsBCP47.startsWith("en"),
   1644 });
   1645 if (AppConstants.platform === "win") {
   1646  /**
   1647   * browser.taskbar.previews.enable - bool
   1648   *   True - Tabs are to be shown in Windows 7 taskbar.
   1649   *   False - Only the window is to be shown in Windows 7 taskbar.
   1650   */
   1651  Preferences.addSetting({
   1652    id: "showTabsInTaskbar",
   1653    pref: "browser.taskbar.previews.enable",
   1654    // Functionality for "Show tabs in taskbar" on Windows 7 and up.
   1655    visible: () => {
   1656      if (AppConstants.platform !== "win") {
   1657        return false;
   1658      }
   1659 
   1660      try {
   1661        let ver = parseFloat(Services.sysinfo.getProperty("version"));
   1662        return ver >= 6.1;
   1663      } catch (ex) {
   1664        return false;
   1665      }
   1666    },
   1667  });
   1668 } else {
   1669  // Not supported unless we're on Windows
   1670  Preferences.addSetting({ id: "showTabsInTaskbar", visible: () => false });
   1671 }
   1672 
   1673 // "Containers" tabs settings
   1674 Preferences.addSetting({
   1675  id: "privacyUserContextUI",
   1676  pref: "privacy.userContext.ui.enabled",
   1677 });
   1678 Preferences.addSetting({
   1679  id: "browserContainersbox",
   1680  deps: ["privacyUserContextUI"],
   1681  visible: ({ privacyUserContextUI }) => !!privacyUserContextUI.value,
   1682 });
   1683 Preferences.addSetting({
   1684  id: "browserContainersCheckbox",
   1685  pref: "privacy.userContext.enabled",
   1686  controllingExtensionInfo: {
   1687    storeId: "privacy.containers",
   1688    l10nId: "extension-controlling-privacy-containers",
   1689  },
   1690  async promptToCloseTabsAndDisable(count, setting) {
   1691    let [title, message, okButton, cancelButton] =
   1692      await document.l10n.formatValues([
   1693        { id: "containers-disable-alert-title" },
   1694        { id: "containers-disable-alert-desc", args: { tabCount: count } },
   1695        { id: "containers-disable-alert-ok-button", args: { tabCount: count } },
   1696        { id: "containers-disable-alert-cancel-button" },
   1697      ]);
   1698 
   1699    let buttonFlags =
   1700      Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0 +
   1701      Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1;
   1702 
   1703    let rv = Services.prompt.confirmEx(
   1704      window,
   1705      title,
   1706      message,
   1707      buttonFlags,
   1708      okButton,
   1709      cancelButton,
   1710      null,
   1711      null,
   1712      {}
   1713    );
   1714 
   1715    // User confirmed - disable containers and close container tabs.
   1716    if (rv == 0) {
   1717      await ContextualIdentityService.closeContainerTabs();
   1718      setting.pref.value = false;
   1719    }
   1720 
   1721    // Keep the checkbox checked when the user opts not to close tabs.
   1722    return true;
   1723  },
   1724  set(val, _, setting) {
   1725    // When enabling container tabs, just set the pref value.
   1726    if (val) {
   1727      return val;
   1728    }
   1729 
   1730    // When disabling container tabs, check if there are container tabs currently
   1731    // open. If there aren't, then proceed with disabling.
   1732    let count = ContextualIdentityService.countContainerTabs();
   1733    if (count == 0) {
   1734      return false;
   1735    }
   1736 
   1737    // When disabling container tabs with container tabs currently open show a
   1738    // dialog to determine whether or not the tabs should be closed.
   1739    return this.promptToCloseTabsAndDisable(count, setting);
   1740  },
   1741 });
   1742 Preferences.addSetting({
   1743  id: "browserContainersSettings",
   1744  deps: ["browserContainersCheckbox"],
   1745  /**
   1746   * Displays container panel for customising and adding containers.
   1747   */
   1748  onUserClick: () => {
   1749    gotoPref("containers");
   1750  },
   1751  getControlConfig: config => {
   1752    let searchKeywords = [
   1753      "user-context-personal",
   1754      "user-context-work",
   1755      "user-context-banking",
   1756      "user-context-shopping",
   1757    ]
   1758      .map(ContextualIdentityService.formatContextLabel)
   1759      .join(" ");
   1760    config.controlAttrs.searchkeywords = searchKeywords;
   1761    return config;
   1762  },
   1763  disabled: ({ browserContainersCheckbox }) => !browserContainersCheckbox.value,
   1764 });
   1765 
   1766 // "Closing" tabs settings
   1767 Preferences.addSetting({
   1768  id: "tabsClosing",
   1769 });
   1770 /**
   1771 * browser.tabs.warnOnClose - bool
   1772 *   True - If when closing a window with multiple tabs the user is warned and
   1773 *          allowed to cancel the action, false to just close the window.
   1774 */
   1775 Preferences.addSetting({
   1776  id: "warnCloseMultiple",
   1777  pref: "browser.tabs.warnOnClose",
   1778 });
   1779 /**
   1780 * browser.warnOnQuitShortcut - bool
   1781 *   True - If the keyboard shortcut (Ctrl/Cmd+Q) is pressed, the user should
   1782 *          be warned, false to just quit without prompting.
   1783 */
   1784 Preferences.addSetting({
   1785  id: "warnOnQuitKey",
   1786  pref: "browser.warnOnQuitShortcut",
   1787  setup() {
   1788    let quitKeyElement =
   1789      window.browsingContext.topChromeWindow.document.getElementById(
   1790        "key_quitApplication"
   1791      );
   1792    if (quitKeyElement) {
   1793      this.quitKey = ShortcutUtils.prettifyShortcut(quitKeyElement);
   1794    }
   1795  },
   1796  visible() {
   1797    return AppConstants.platform !== "win" && this.quitKey;
   1798  },
   1799  getControlConfig(config) {
   1800    return {
   1801      ...config,
   1802      l10nArgs: { quitKey: this.quitKey },
   1803    };
   1804  },
   1805 });
   1806 
   1807 /**
   1808 * Helper object for managing the various zoom related settings.
   1809 */
   1810 const ZoomHelpers = {
   1811  win: window.browsingContext.topChromeWindow,
   1812  get FullZoom() {
   1813    return this.win.FullZoom;
   1814  },
   1815  get ZoomManager() {
   1816    return this.win.ZoomManager;
   1817  },
   1818 
   1819  /**
   1820   * Set the global default zoom value.
   1821   *
   1822   * @param {number} newZoom - The new zoom
   1823   * @returns {Promise<void>}
   1824   */
   1825  async setDefaultZoom(newZoom) {
   1826    let cps2 = Cc["@mozilla.org/content-pref/service;1"].getService(
   1827      Ci.nsIContentPrefService2
   1828    );
   1829    let nonPrivateLoadContext = Cu.createLoadContext();
   1830    let resolvers = Promise.withResolvers();
   1831    /* Because our setGlobal function takes in a browsing context, and
   1832     * because we want to keep this property consistent across both private
   1833     * and non-private contexts, we create a non-private context and use that
   1834     * to set the property, regardless of our actual context.
   1835     */
   1836    cps2.setGlobal(this.FullZoom.name, newZoom, nonPrivateLoadContext, {
   1837      handleCompletion: resolvers.resolve,
   1838      handleError: resolvers.reject,
   1839    });
   1840    return resolvers.promise;
   1841  },
   1842 
   1843  async getDefaultZoom() {
   1844    /** @import { ZoomUI as GlobalZoomUI } from "resource:///modules/ZoomUI.sys.mjs" */
   1845    /** @type {GlobalZoomUI} */
   1846    let ZoomUI = this.win.ZoomUI;
   1847    return await ZoomUI.getGlobalValue();
   1848  },
   1849 
   1850  /**
   1851   * The possible zoom values.
   1852   *
   1853   * @returns {number[]}
   1854   */
   1855  get zoomValues() {
   1856    return this.ZoomManager.zoomValues;
   1857  },
   1858 
   1859  toggleFullZoom() {
   1860    this.ZoomManager.toggleZoom();
   1861  },
   1862 };
   1863 Preferences.addSetting(
   1864  class extends Preferences.AsyncSetting {
   1865    static id = "defaultZoom";
   1866    /** @type {Record<"options", object[]>} */
   1867    optionsConfig;
   1868 
   1869    /**
   1870     * @param {string} val - zoom value as a string
   1871     */
   1872    async set(val) {
   1873      ZoomHelpers.setDefaultZoom(
   1874        parseFloat((parseInt(val, 10) / 100).toFixed(2))
   1875      );
   1876    }
   1877    async get() {
   1878      return Math.round((await ZoomHelpers.getDefaultZoom()) * 100);
   1879    }
   1880    async getControlConfig() {
   1881      if (!this.optionsConfig) {
   1882        this.optionsConfig = {
   1883          options: ZoomHelpers.zoomValues.map(a => {
   1884            let value = Math.round(a * 100);
   1885            return {
   1886              value,
   1887              l10nId: "preferences-default-zoom-value",
   1888              l10nArgs: { percentage: value },
   1889            };
   1890          }),
   1891        };
   1892      }
   1893      return this.optionsConfig;
   1894    }
   1895  }
   1896 );
   1897 Preferences.addSetting({
   1898  id: "zoomTextPref",
   1899  pref: "browser.zoom.full",
   1900 });
   1901 Preferences.addSetting({
   1902  id: "zoomText",
   1903  deps: ["zoomTextPref"],
   1904  // Use the Setting since the ZoomManager getter may not have updated yet.
   1905  get: (_, { zoomTextPref }) => !zoomTextPref.value,
   1906  set: () => ZoomHelpers.toggleFullZoom(),
   1907  disabled: ({ zoomTextPref }) => zoomTextPref.locked,
   1908 });
   1909 Preferences.addSetting({
   1910  id: "zoomWarning",
   1911  deps: ["zoomText"],
   1912  visible: ({ zoomText }) => Boolean(zoomText.value),
   1913 });
   1914 Preferences.addSetting({
   1915  id: "contrastControlSettings",
   1916  pref: "browser.display.document_color_use",
   1917 });
   1918 Preferences.addSetting({
   1919  id: "colors",
   1920  onUserClick() {
   1921    gSubDialog.open(
   1922      "chrome://browser/content/preferences/dialogs/colors.xhtml",
   1923      { features: "resizable=no" }
   1924    );
   1925  },
   1926 });
   1927 
   1928 Preferences.addSetting({
   1929  /** @type {{ _removeAddressDialogStrings: string[] } & SettingConfig} */
   1930  id: "address-item",
   1931  _removeAddressDialogStrings: [],
   1932  onUserClick(e) {
   1933    const action = e.target.getAttribute("action");
   1934    const guid = e.target.getAttribute("guid");
   1935    if (action === "remove") {
   1936      let [title, confirm, cancel] = this._removeAddressDialogStrings;
   1937      FormAutofillPreferences.prototype.openRemoveAddressDialog(
   1938        guid,
   1939        window.browsingContext.topChromeWindow.browsingContext,
   1940        title,
   1941        confirm,
   1942        cancel
   1943      );
   1944    } else if (action === "edit") {
   1945      FormAutofillPreferences.prototype.openEditAddressDialog(guid, window);
   1946    }
   1947  },
   1948  setup(emitChange) {
   1949    document.l10n
   1950      .formatValues([
   1951        { id: "addresses-delete-address-prompt-title" },
   1952        { id: "addresses-delete-address-prompt-confirm-button" },
   1953        { id: "addresses-delete-address-prompt-cancel-button" },
   1954      ])
   1955      .then(val => (this._removeAddressDialogStrings = val))
   1956      .then(emitChange);
   1957  },
   1958  disabled() {
   1959    return !!this._removeAddressDialogStrings.length;
   1960  },
   1961 });
   1962 
   1963 Preferences.addSetting({
   1964  id: "add-address-button",
   1965  deps: ["saveAndFillAddresses"],
   1966  setup: (emitChange, _, setting) => {
   1967    function updateDepsAndChange() {
   1968      setting._deps = null;
   1969      emitChange();
   1970    }
   1971    Services.obs.addObserver(
   1972      updateDepsAndChange,
   1973      "formautofill-preferences-initialized"
   1974    );
   1975    return () =>
   1976      Services.obs.removeObserver(
   1977        updateDepsAndChange,
   1978        "formautofill-preferences-initialized"
   1979      );
   1980  },
   1981  onUserClick: () => {
   1982    FormAutofillPreferences.prototype.openEditAddressDialog(undefined, window);
   1983  },
   1984  disabled: ({ saveAndFillAddresses }) => !saveAndFillAddresses?.value,
   1985 });
   1986 
   1987 Preferences.addSetting({
   1988  id: "addresses-list-header",
   1989 });
   1990 
   1991 Preferences.addSetting({
   1992  id: "no-addresses-stored",
   1993 });
   1994 
   1995 Preferences.addSetting(
   1996  class extends Preferences.AsyncSetting {
   1997    static id = "addresses-list";
   1998 
   1999    async getAddresses() {
   2000      await FormAutofillPreferences.prototype.initializeAddressesStorage();
   2001      return FormAutofillPreferences.prototype.makeAddressesListItems();
   2002    }
   2003 
   2004    async getControlConfig() {
   2005      return {
   2006        items: await this.getAddresses(),
   2007      };
   2008    }
   2009 
   2010    setup() {
   2011      Services.obs.addObserver(this.emitChange, "formautofill-storage-changed");
   2012      return () =>
   2013        Services.obs.removeObserver(
   2014          this.emitChange,
   2015          "formautofill-storage-changed"
   2016        );
   2017    }
   2018 
   2019    async visible() {
   2020      const items = await this.getAddresses();
   2021      return !!items.length;
   2022    }
   2023  }
   2024 );
   2025 
   2026 SettingGroupManager.registerGroups({
   2027  containers: {
   2028    // This section is marked as in progress for testing purposes
   2029    inProgress: true,
   2030    items: [
   2031      {
   2032        id: "containersPlaceholder",
   2033        control: "moz-message-bar",
   2034        controlAttrs: {
   2035          message: "Placeholder for updated containers",
   2036        },
   2037      },
   2038    ],
   2039  },
   2040  profilePane: {
   2041    headingLevel: 2,
   2042    id: "browserProfilesGroupPane",
   2043    l10nId: "preferences-profiles-subpane-description",
   2044    supportPage: "profile-management",
   2045    items: [
   2046      {
   2047        id: "manageProfiles",
   2048        control: "moz-box-button",
   2049        l10nId: "preferences-manage-profiles-button",
   2050      },
   2051      {
   2052        id: "copyProfileHeader",
   2053        l10nId: "preferences-copy-profile-header",
   2054        headingLevel: 2,
   2055        supportPage: "profile-management",
   2056        control: "moz-fieldset",
   2057        items: [
   2058          {
   2059            id: "copyProfileBox",
   2060            l10nId: "preferences-profile-to-copy",
   2061            control: "moz-box-item",
   2062            items: [
   2063              {
   2064                id: "copyProfileSelect",
   2065                control: "moz-select",
   2066                slot: "actions",
   2067              },
   2068              {
   2069                id: "copyProfile",
   2070                l10nId: "preferences-copy-profile-button",
   2071                control: "moz-button",
   2072                slot: "actions",
   2073                controlAttrs: {
   2074                  type: "primary",
   2075                },
   2076              },
   2077            ],
   2078          },
   2079        ],
   2080      },
   2081    ],
   2082  },
   2083  profiles: {
   2084    id: "profilesGroup",
   2085    l10nId: "preferences-profiles-section-header",
   2086    headingLevel: 2,
   2087    supportPage: "profile-management",
   2088    items: [
   2089      {
   2090        id: "profilesSettings",
   2091        control: "moz-box-button",
   2092        l10nId: "preferences-profiles-settings-button",
   2093      },
   2094    ],
   2095  },
   2096  startup: {
   2097    items: [
   2098      {
   2099        id: "browserRestoreSession",
   2100        l10nId: "startup-restore-windows-and-tabs",
   2101      },
   2102      {
   2103        id: "windowsLaunchOnLogin",
   2104        l10nId: "windows-launch-on-login",
   2105      },
   2106      {
   2107        id: "windowsLaunchOnLoginDisabledBox",
   2108        control: "moz-message-bar",
   2109        options: [
   2110          {
   2111            control: "span",
   2112            l10nId: "windows-launch-on-login-disabled",
   2113            slot: "message",
   2114            options: [
   2115              {
   2116                control: "a",
   2117                controlAttrs: {
   2118                  "data-l10n-name": "startup-link",
   2119                  href: "ms-settings:startupapps",
   2120                  target: "_self",
   2121                },
   2122              },
   2123            ],
   2124          },
   2125        ],
   2126      },
   2127      {
   2128        id: "windowsLaunchOnLoginDisabledProfileBox",
   2129        control: "moz-message-bar",
   2130        l10nId: "startup-windows-launch-on-login-profile-disabled",
   2131      },
   2132      {
   2133        id: "alwaysCheckDefault",
   2134        l10nId: "always-check-default",
   2135      },
   2136      {
   2137        id: "isDefaultPane",
   2138        l10nId: "is-default-browser",
   2139        control: "moz-promo",
   2140      },
   2141      {
   2142        id: "isNotDefaultPane",
   2143        l10nId: "is-not-default-browser",
   2144        control: "moz-promo",
   2145        options: [
   2146          {
   2147            control: "moz-button",
   2148            l10nId: "set-as-my-default-browser",
   2149            id: "setDefaultButton",
   2150            slot: "actions",
   2151            controlAttrs: {
   2152              type: "primary",
   2153            },
   2154          },
   2155        ],
   2156      },
   2157    ],
   2158  },
   2159  importBrowserData: {
   2160    l10nId: "preferences-data-migration-group",
   2161    headingLevel: 2,
   2162    items: [
   2163      {
   2164        id: "data-migration",
   2165        l10nId: "preferences-data-migration-button",
   2166        control: "moz-box-button",
   2167      },
   2168    ],
   2169  },
   2170  homepage: {
   2171    inProgress: true,
   2172    headingLevel: 2,
   2173    l10nId: "home-homepage-title",
   2174    items: [
   2175      {
   2176        id: "homepageNewWindows",
   2177        control: "moz-select",
   2178        l10nId: "home-homepage-new-windows",
   2179        options: [
   2180          {
   2181            value: "home",
   2182            l10nId: "home-mode-choice-default-fx",
   2183          },
   2184          { value: "blank", l10nId: "home-mode-choice-blank" },
   2185          { value: "custom", l10nId: "home-mode-choice-custom" },
   2186        ],
   2187      },
   2188      {
   2189        id: "homepageGoToCustomHomepageUrlPanel",
   2190        control: "moz-box-button",
   2191        l10nId: "home-homepage-custom-homepage-button",
   2192      },
   2193      {
   2194        id: "homepageNewTabs",
   2195        control: "moz-select",
   2196        l10nId: "home-homepage-new-tabs",
   2197        options: [
   2198          {
   2199            value: "true",
   2200            l10nId: "home-mode-choice-default-fx",
   2201          },
   2202          { value: "false", l10nId: "home-mode-choice-blank" },
   2203        ],
   2204      },
   2205      {
   2206        id: "homepageRestoreDefaults",
   2207        control: "moz-button",
   2208        l10nId: "home-restore-defaults",
   2209        controlAttrs: { id: "restoreDefaultHomePageBtn" },
   2210      },
   2211    ],
   2212  },
   2213  home: {
   2214    inProgress: true,
   2215    headingLevel: 2,
   2216    l10nId: "home-prefs-content-header",
   2217    // Icons are not ready to be used yet.
   2218    // iconSrc: "chrome://browser/skin/home.svg",
   2219    items: [
   2220      {
   2221        id: "webSearch",
   2222        l10nId: "home-prefs-search-header2",
   2223        control: "moz-toggle",
   2224      },
   2225      {
   2226        id: "weather",
   2227        l10nId: "home-prefs-weather-header",
   2228        control: "moz-toggle",
   2229      },
   2230      {
   2231        id: "widgets",
   2232        l10nId: "home-prefs-widgets-header",
   2233        control: "moz-toggle",
   2234        items: [
   2235          {
   2236            id: "lists",
   2237            l10nId: "home-prefs-lists-header",
   2238          },
   2239          {
   2240            id: "timer",
   2241            l10nId: "home-prefs-timer-header",
   2242          },
   2243        ],
   2244      },
   2245      {
   2246        id: "shortcuts",
   2247        l10nId: "home-prefs-shortcuts-header",
   2248        control: "moz-toggle",
   2249        items: [
   2250          {
   2251            id: "shortcutsRows",
   2252            control: "moz-select",
   2253            options: [
   2254              {
   2255                value: 1,
   2256                l10nId: "home-prefs-sections-rows-option",
   2257                l10nArgs: { num: 1 },
   2258              },
   2259              {
   2260                value: 2,
   2261                l10nId: "home-prefs-sections-rows-option",
   2262                l10nArgs: { num: 2 },
   2263              },
   2264              {
   2265                value: 3,
   2266                l10nId: "home-prefs-sections-rows-option",
   2267                l10nArgs: { num: 3 },
   2268              },
   2269              {
   2270                value: 4,
   2271                l10nId: "home-prefs-sections-rows-option",
   2272                l10nArgs: { num: 4 },
   2273              },
   2274            ],
   2275          },
   2276        ],
   2277      },
   2278      {
   2279        id: "stories",
   2280        l10nId: "home-prefs-stories-header2",
   2281        control: "moz-toggle",
   2282        items: [
   2283          {
   2284            id: "manageTopics",
   2285            l10nId: "home-prefs-manage-topics-link2",
   2286            control: "moz-box-link",
   2287            controlAttrs: {
   2288              href: "about:newtab#customize-topics",
   2289            },
   2290          },
   2291        ],
   2292      },
   2293      {
   2294        id: "supportFirefox",
   2295        l10nId: "home-prefs-support-firefox-header",
   2296        control: "moz-toggle",
   2297        items: [
   2298          {
   2299            id: "sponsoredShortcuts",
   2300            l10nId: "home-prefs-shortcuts-by-option-sponsored",
   2301          },
   2302          {
   2303            id: "sponsoredStories",
   2304            l10nId: "home-prefs-recommended-by-option-sponsored-stories",
   2305          },
   2306          {
   2307            id: "supportFirefoxPromo",
   2308            l10nId: "home-prefs-mission-message2",
   2309            control: "moz-promo",
   2310            options: [
   2311              {
   2312                control: "a",
   2313                l10nId: "home-prefs-mission-message-learn-more-link",
   2314                slot: "support-link",
   2315                controlAttrs: {
   2316                  is: "moz-support-link",
   2317                  "support-page": "sponsor-privacy",
   2318                  "utm-content": "inproduct",
   2319                },
   2320              },
   2321            ],
   2322          },
   2323        ],
   2324      },
   2325      {
   2326        id: "recentActivity",
   2327        l10nId: "home-prefs-recent-activity-header",
   2328        control: "moz-toggle",
   2329        items: [
   2330          {
   2331            id: "recentActivityRows",
   2332            control: "moz-select",
   2333            controlAttrs: {
   2334              class: "newtab-rows-select",
   2335            },
   2336            options: [
   2337              {
   2338                value: 1,
   2339                l10nId: "home-prefs-sections-rows-option",
   2340                l10nArgs: { num: 1 },
   2341              },
   2342              {
   2343                value: 2,
   2344                l10nId: "home-prefs-sections-rows-option",
   2345                l10nArgs: { num: 2 },
   2346              },
   2347              {
   2348                value: 3,
   2349                l10nId: "home-prefs-sections-rows-option",
   2350                l10nArgs: { num: 3 },
   2351              },
   2352              {
   2353                value: 4,
   2354                l10nId: "home-prefs-sections-rows-option",
   2355                l10nArgs: { num: 4 },
   2356              },
   2357            ],
   2358          },
   2359          {
   2360            id: "recentActivityVisited",
   2361            l10nId: "home-prefs-highlights-option-visited-pages",
   2362          },
   2363          {
   2364            id: "recentActivityBookmarks",
   2365            l10nId: "home-prefs-highlights-options-bookmarks",
   2366          },
   2367          {
   2368            id: "recentActivityDownloads",
   2369            l10nId: "home-prefs-highlights-option-most-recent-download",
   2370          },
   2371        ],
   2372      },
   2373      {
   2374        id: "chooseWallpaper",
   2375        l10nId: "home-prefs-choose-wallpaper-link2",
   2376        control: "moz-box-link",
   2377        controlAttrs: {
   2378          href: "about:newtab#customize",
   2379        },
   2380        iconSrc: "chrome://browser/skin/customize.svg",
   2381      },
   2382    ],
   2383  },
   2384  zoom: {
   2385    l10nId: "preferences-zoom-header2",
   2386    headingLevel: 2,
   2387    items: [
   2388      {
   2389        id: "defaultZoom",
   2390        l10nId: "preferences-default-zoom-label",
   2391        control: "moz-select",
   2392      },
   2393      {
   2394        id: "zoomText",
   2395        l10nId: "preferences-zoom-text-only",
   2396      },
   2397      {
   2398        id: "zoomWarning",
   2399        l10nId: "preferences-text-zoom-override-warning",
   2400        control: "moz-message-bar",
   2401        controlAttrs: {
   2402          type: "warning",
   2403        },
   2404      },
   2405    ],
   2406  },
   2407  translations: {
   2408    inProgress: true,
   2409    l10nId: "settings-translations-header",
   2410    iconSrc: "chrome://browser/skin/translations.svg",
   2411    supportPage: "website-translation",
   2412    headingLevel: 2,
   2413    items: [
   2414      {
   2415        id: "offerTranslations",
   2416        l10nId: "settings-translations-offer-to-translate-label",
   2417      },
   2418      {
   2419        id: "translationsManageButton",
   2420        l10nId: "settings-translations-more-settings-button",
   2421        control: "moz-box-button",
   2422      },
   2423    ],
   2424  },
   2425  appearance: {
   2426    l10nId: "web-appearance-group",
   2427    items: [
   2428      {
   2429        id: "web-appearance-override-warning",
   2430        l10nId: "preferences-web-appearance-override-warning3",
   2431        control: "moz-message-bar",
   2432      },
   2433      {
   2434        id: "web-appearance-chooser",
   2435        control: "moz-visual-picker",
   2436        options: [
   2437          {
   2438            value: "auto",
   2439            l10nId: "preferences-web-appearance-choice-auto2",
   2440            controlAttrs: {
   2441              id: "preferences-web-appearance-choice-auto",
   2442              class: "appearance-chooser-item",
   2443              imagesrc:
   2444                "chrome://browser/content/preferences/web-appearance-light.svg",
   2445            },
   2446          },
   2447          {
   2448            value: "light",
   2449            l10nId: "preferences-web-appearance-choice-light2",
   2450            controlAttrs: {
   2451              id: "preferences-web-appearance-choice-light",
   2452              class: "appearance-chooser-item",
   2453              imagesrc:
   2454                "chrome://browser/content/preferences/web-appearance-light.svg",
   2455            },
   2456          },
   2457          {
   2458            value: "dark",
   2459            l10nId: "preferences-web-appearance-choice-dark2",
   2460            controlAttrs: {
   2461              id: "preferences-web-appearance-choice-dark",
   2462              class: "appearance-chooser-item",
   2463              imagesrc:
   2464                "chrome://browser/content/preferences/web-appearance-dark.svg",
   2465            },
   2466          },
   2467        ],
   2468      },
   2469      {
   2470        id: "web-appearance-manage-themes-link",
   2471        l10nId: "preferences-web-appearance-link",
   2472        control: "moz-box-link",
   2473        controlAttrs: {
   2474          href: "about:addons",
   2475        },
   2476      },
   2477    ],
   2478  },
   2479  downloads: {
   2480    l10nId: "downloads-header-2",
   2481    headingLevel: 2,
   2482    items: [
   2483      {
   2484        id: "downloadFolder",
   2485        l10nId: "download-save-where-2",
   2486        control: "moz-input-folder",
   2487        controlAttrs: {
   2488          id: "chooseFolder",
   2489        },
   2490      },
   2491      {
   2492        id: "alwaysAsk",
   2493        l10nId: "download-always-ask-where",
   2494      },
   2495      {
   2496        id: "deletePrivate",
   2497        l10nId: "download-private-browsing-delete",
   2498      },
   2499    ],
   2500  },
   2501  drm: {
   2502    subcategory: "drm",
   2503    items: [
   2504      {
   2505        id: "playDRMContent",
   2506        l10nId: "play-drm-content",
   2507        supportPage: "drm-content",
   2508      },
   2509    ],
   2510  },
   2511  contrast: {
   2512    l10nId: "preferences-contrast-control-group",
   2513    headingLevel: 2,
   2514    items: [
   2515      {
   2516        id: "contrastControlSettings",
   2517        control: "moz-radio-group",
   2518        l10nId: "preferences-contrast-control-radio-group",
   2519        options: [
   2520          {
   2521            id: "contrastSettingsAuto",
   2522            value: 0,
   2523            l10nId: "preferences-contrast-control-use-platform-settings",
   2524          },
   2525          {
   2526            id: "contrastSettingsOff",
   2527            value: 1,
   2528            l10nId: "preferences-contrast-control-off",
   2529          },
   2530          {
   2531            id: "contrastSettingsOn",
   2532            value: 2,
   2533            l10nId: "preferences-contrast-control-custom",
   2534            items: [
   2535              {
   2536                id: "colors",
   2537                l10nId: "preferences-colors-manage-button",
   2538                control: "moz-box-button",
   2539                controlAttrs: {
   2540                  "search-l10n-ids":
   2541                    "colors-text-and-background, colors-text.label, colors-text-background.label, colors-links-header, colors-links-unvisited.label, colors-links-visited.label",
   2542                },
   2543              },
   2544            ],
   2545          },
   2546        ],
   2547      },
   2548    ],
   2549  },
   2550  browsing: {
   2551    l10nId: "browsing-group-label",
   2552    items: [
   2553      {
   2554        id: "useAutoScroll",
   2555        l10nId: "browsing-use-autoscroll",
   2556      },
   2557      // {
   2558      //   id: "useSmoothScrolling",
   2559      //   l10nId: "browsing-use-smooth-scrolling",
   2560      // },
   2561      {
   2562        id: "useOverlayScrollbars",
   2563        l10nId: "browsing-gtk-use-non-overlay-scrollbars",
   2564      },
   2565      {
   2566        id: "useOnScreenKeyboard",
   2567        l10nId: "browsing-use-onscreen-keyboard",
   2568      },
   2569      {
   2570        id: "useCursorNavigation",
   2571        l10nId: "browsing-use-cursor-navigation",
   2572      },
   2573      {
   2574        id: "useFullKeyboardNavigation",
   2575        l10nId: "browsing-use-full-keyboard-navigation",
   2576      },
   2577      // {
   2578      //   id: "alwaysUnderlineLinks",
   2579      //   l10nId: "browsing-always-underline-links",
   2580      // },
   2581      {
   2582        id: "searchStartTyping",
   2583        l10nId: "browsing-search-on-start-typing",
   2584      },
   2585      {
   2586        id: "pictureInPictureToggleEnabled",
   2587        l10nId: "browsing-picture-in-picture-toggle-enabled",
   2588        supportPage: "picture-in-picture",
   2589        items: [
   2590          {
   2591            id: "pictureInPictureEnableWhenSwitchingTabs",
   2592            l10nId: "browsing-picture-in-picture-enable-when-switching-tabs",
   2593          },
   2594        ],
   2595      },
   2596      {
   2597        id: "mediaControlToggleEnabled",
   2598        l10nId: "browsing-media-control",
   2599        supportPage: "media-keyboard-control",
   2600      },
   2601      // {
   2602      //   id: "cfrRecommendations",
   2603      //   l10nId: "browsing-cfr-recommendations",
   2604      //   supportPage: "extensionrecommendations",
   2605      //   subcategory: "cfraddons",
   2606      // },
   2607      // {
   2608      //   id: "cfrRecommendations-features",
   2609      //   l10nId: "browsing-cfr-features",
   2610      //   supportPage: "extensionrecommendations",
   2611      //   subcategory: "cfrfeatures",
   2612      // },
   2613      // {
   2614      //   id: "linkPreviewEnabled",
   2615      //   l10nId: "link-preview-settings-enable",
   2616      //   subcategory: "link-preview",
   2617      //   items: [
   2618      //     {
   2619      //       id: "linkPreviewKeyPoints",
   2620      //       l10nId: "link-preview-settings-key-points",
   2621      //     },
   2622      //     {
   2623      //       id: "linkPreviewLongPress",
   2624      //       l10nId: "link-preview-settings-long-press",
   2625      //     },
   2626      //   ],
   2627      // },
   2628    ],
   2629  },
   2630  httpsOnly: {
   2631    items: [
   2632      {
   2633        id: "httpsOnlyRadioGroup",
   2634        control: "moz-radio-group",
   2635        l10nId: "httpsonly-label",
   2636        supportPage: "https-only-prefs",
   2637        options: [
   2638          {
   2639            id: "httpsOnlyRadioEnabled",
   2640            value: "enabled",
   2641            l10nId: "httpsonly-radio-enabled",
   2642          },
   2643          {
   2644            id: "httpsOnlyRadioEnabledPBM",
   2645            value: "privateOnly",
   2646            l10nId: "httpsonly-radio-enabled-pbm",
   2647          },
   2648          {
   2649            id: "httpsOnlyRadioDisabled",
   2650            value: "disabled",
   2651            l10nId: "httpsonly-radio-disabled3",
   2652            supportPage: "connection-upgrades",
   2653          },
   2654        ],
   2655      },
   2656      {
   2657        id: "httpsOnlyExceptionButton",
   2658        l10nId: "sitedata-cookies-exceptions",
   2659        control: "moz-box-button",
   2660        controlAttrs: {
   2661          "search-l10n-ids":
   2662            "permissions-address,permissions-allow.label,permissions-remove.label,permissions-remove-all.label,permissions-exceptions-https-only-desc2",
   2663        },
   2664      },
   2665    ],
   2666  },
   2667  certificates: {
   2668    l10nId: "certs-description2",
   2669    supportPage: "secure-website-certificate",
   2670    headingLevel: 2,
   2671    items: [
   2672      {
   2673        id: "certEnableThirdPartyToggle",
   2674        l10nId: "certs-thirdparty-toggle",
   2675        supportPage: "automatically-trust-third-party-certificates",
   2676      },
   2677      {
   2678        id: "certificateButtonGroup",
   2679        control: "moz-box-group",
   2680        items: [
   2681          {
   2682            id: "viewCertificatesButton",
   2683            l10nId: "certs-view",
   2684            control: "moz-box-button",
   2685            controlAttrs: {
   2686              "search-l10n-ids":
   2687                "certmgr-tab-mine.label,certmgr-tab-people.label,certmgr-tab-servers.label,certmgr-tab-ca.label,certmgr-mine,certmgr-people,certmgr-server,certmgr-ca,certmgr-cert-name.label,certmgr-token-name.label,certmgr-view.label,certmgr-export.label,certmgr-delete.label",
   2688            },
   2689          },
   2690          {
   2691            id: "viewSecurityDevicesButton",
   2692            l10nId: "certs-devices",
   2693            control: "moz-box-button",
   2694            controlAttrs: {
   2695              "search-l10n-ids":
   2696                "devmgr-window.title,devmgr-devlist.label,devmgr-header-details.label,devmgr-header-value.label,devmgr-button-login.label,devmgr-button-logout.label,devmgr-button-changepw.label,devmgr-button-load.label,devmgr-button-unload.label,certs-devices-enable-fips",
   2697            },
   2698          },
   2699        ],
   2700      },
   2701    ],
   2702  },
   2703  browsingProtection: {
   2704    items: [
   2705      {
   2706        id: "enableSafeBrowsing",
   2707        l10nId: "security-enable-safe-browsing",
   2708        supportPage: "phishing-malware",
   2709        control: "moz-checkbox",
   2710        items: [
   2711          {
   2712            id: "blockDownloads",
   2713            l10nId: "security-block-downloads",
   2714          },
   2715          {
   2716            id: "blockUncommonUnwanted",
   2717            l10nId: "security-block-uncommon-software",
   2718          },
   2719        ],
   2720      },
   2721    ],
   2722  },
   2723  nonTechnicalPrivacy: {
   2724    l10nId: "non-technical-privacy-label",
   2725    items: [
   2726      {
   2727        id: "gpcEnabled",
   2728        l10nId: "global-privacy-control-description",
   2729        supportPage: "global-privacy-control",
   2730        controlAttrs: {
   2731          "search-l10n-ids": "global-privacy-control-search",
   2732        },
   2733      },
   2734      {
   2735        id: "dntRemoval",
   2736        l10nId: "do-not-track-removal2",
   2737        control: "moz-box-link",
   2738        supportPage: "how-do-i-turn-do-not-track-feature",
   2739      },
   2740    ],
   2741  },
   2742  nonTechnicalPrivacy2: {
   2743    inProgress: true,
   2744    l10nId: "non-technical-privacy-heading",
   2745    headingLevel: 2,
   2746    items: [
   2747      {
   2748        id: "gpcEnabled",
   2749        l10nId: "global-privacy-control-description",
   2750        supportPage: "global-privacy-control",
   2751        controlAttrs: {
   2752          "search-l10n-ids": "global-privacy-control-search",
   2753        },
   2754      },
   2755      {
   2756        id: "relayIntegration",
   2757        l10nId: "preferences-privacy-relay-available",
   2758        supportPage: "firefox-relay-integration",
   2759      },
   2760      {
   2761        id: "dntRemoval",
   2762        l10nId: "do-not-track-removal3",
   2763        control: "moz-message-bar",
   2764        supportPage: "how-do-i-turn-do-not-track-feature",
   2765        controlAttrs: {
   2766          dismissable: true,
   2767        },
   2768      },
   2769    ],
   2770  },
   2771  securityPrivacyStatus: {
   2772    inProgress: true,
   2773    items: [
   2774      {
   2775        id: "privacyCard",
   2776        control: "security-privacy-card",
   2777      },
   2778    ],
   2779  },
   2780  securityPrivacyWarnings: {
   2781    inProgress: true,
   2782    items: [
   2783      {
   2784        id: "warningCard",
   2785        l10nId: "security-privacy-issue-card",
   2786        control: "moz-card",
   2787        controlAttrs: {
   2788          type: "accordion",
   2789        },
   2790        items: [
   2791          {
   2792            id: "securityWarningsGroup",
   2793            control: "moz-box-group",
   2794            controlAttrs: {
   2795              type: "list",
   2796            },
   2797          },
   2798        ],
   2799      },
   2800    ],
   2801  },
   2802  support: {
   2803    inProgress: true,
   2804    l10nId: "support-application-heading",
   2805    headingLevel: 2,
   2806    items: [
   2807      {
   2808        id: "supportLinksGroup",
   2809        control: "moz-box-group",
   2810        items: [
   2811          {
   2812            id: "supportGetHelp",
   2813            l10nId: "support-get-help",
   2814            control: "moz-box-link",
   2815            supportPage: "preferences",
   2816          },
   2817          {
   2818            id: "supportShareIdeas",
   2819            l10nId: "support-share-ideas",
   2820            control: "moz-box-link",
   2821            controlAttrs: {
   2822              href: "https://connect.mozilla.org/",
   2823            },
   2824          },
   2825        ],
   2826      },
   2827    ],
   2828  },
   2829  performance: {
   2830    items: [
   2831      {
   2832        id: "useRecommendedPerformanceSettings",
   2833        l10nId: "performance-use-recommended-settings-checkbox",
   2834        supportPage: "performance",
   2835      },
   2836      {
   2837        id: "allowHWAccel",
   2838        l10nId: "performance-allow-hw-accel",
   2839      },
   2840    ],
   2841  },
   2842  ipprotection: {
   2843    l10nId: "ip-protection-description",
   2844    headingLevel: 2,
   2845    // TODO: Replace support url with finalized link (Bug 1993266)
   2846    supportPage: "ip-protection",
   2847    items: [
   2848      {
   2849        id: "ipProtectionExceptions",
   2850        l10nId: "ip-protection-site-exceptions",
   2851        control: "moz-fieldset",
   2852        controlAttrs: {
   2853          ".headingLevel": 3,
   2854        },
   2855        items: [
   2856          {
   2857            id: "ipProtectionExceptionAllListButton",
   2858            control: "moz-box-button",
   2859          },
   2860        ],
   2861      },
   2862      {
   2863        id: "ipProtectionAutoStart",
   2864        l10nId: "ip-protection-autostart",
   2865        control: "moz-fieldset",
   2866        items: [
   2867          {
   2868            id: "ipProtectionAutoStartCheckbox",
   2869            l10nId: "ip-protection-autostart-checkbox",
   2870            control: "moz-checkbox",
   2871          },
   2872          {
   2873            id: "ipProtectionAutoStartPrivateCheckbox",
   2874            l10nId: "ip-protection-autostart-private-checkbox",
   2875            control: "moz-checkbox",
   2876          },
   2877        ],
   2878      },
   2879      {
   2880        id: "ipProtectionAdditionalLinks",
   2881        control: "moz-box-group",
   2882        options: [
   2883          {
   2884            id: "ipProtectionSupportLink",
   2885            l10nId: "ip-protection-contact-support-link",
   2886            control: "moz-box-link",
   2887            controlAttrs: {
   2888              href: "https://support.mozilla.org/questions/new/mozilla-vpn/form",
   2889            },
   2890          },
   2891          {
   2892            id: "ipProtectionUpgradeLink",
   2893            l10nId: "ip-protection-upgrade-link",
   2894            control: "moz-box-link",
   2895            controlAttrs: {
   2896              href: "https://www.mozilla.org/products/vpn/",
   2897            },
   2898          },
   2899        ],
   2900      },
   2901    ],
   2902  },
   2903  cookiesAndSiteData: {
   2904    l10nId: "sitedata-label",
   2905    items: [
   2906      {
   2907        id: "clearSiteDataButton",
   2908        l10nId: "sitedata-clear2",
   2909        control: "moz-box-button",
   2910        iconSrc: "chrome://browser/skin/flame.svg",
   2911        controlAttrs: {
   2912          "search-l10n-ids": `
   2913            clear-site-data-cookies-empty.label,
   2914            clear-site-data-cache-empty.label
   2915          `,
   2916        },
   2917      },
   2918      {
   2919        id: "deleteOnCloseInfo",
   2920        l10nId: "sitedata-delete-on-close-private-browsing3",
   2921        control: "moz-message-bar",
   2922      },
   2923      {
   2924        id: "manageDataSettingsGroup",
   2925        control: "moz-box-group",
   2926        controlAttrs: {
   2927          type: "default",
   2928        },
   2929        items: [
   2930          {
   2931            id: "siteDataSize",
   2932            l10nId: "sitedata-total-size-calculating",
   2933            control: "moz-box-item",
   2934            supportPage: "sitedata-learn-more",
   2935          },
   2936          {
   2937            id: "siteDataSettings",
   2938            l10nId: "sitedata-settings2",
   2939            control: "moz-box-button",
   2940            controlAttrs: {
   2941              "search-l10n-ids": `
   2942                site-data-settings-window.title,
   2943                site-data-column-host.label,
   2944                site-data-column-cookies.label,
   2945                site-data-column-storage.label,
   2946                site-data-settings-description,
   2947                site-data-remove-all.label,
   2948              `,
   2949            },
   2950          },
   2951          {
   2952            id: "cookieExceptions",
   2953            l10nId: "sitedata-cookies-exceptions2",
   2954            control: "moz-box-button",
   2955            controlAttrs: {
   2956              "search-l10n-ids": `
   2957                permissions-address,
   2958                permissions-block.label,
   2959                permissions-allow.label,
   2960                permissions-remove.label,
   2961                permissions-remove-all.label,
   2962                permissions-exceptions-cookie-desc
   2963              `,
   2964            },
   2965          },
   2966        ],
   2967      },
   2968      {
   2969        id: "deleteOnClose",
   2970        l10nId: "sitedata-delete-on-close",
   2971      },
   2972    ],
   2973  },
   2974  cookiesAndSiteData2: {
   2975    inProgress: true,
   2976    l10nId: "sitedata-heading",
   2977    headingLevel: 2,
   2978    items: [
   2979      {
   2980        id: "siteDataSize",
   2981        l10nId: "sitedata-total-size-calculating",
   2982        control: "moz-box-item",
   2983        supportPage: "sitedata-learn-more",
   2984      },
   2985      {
   2986        id: "manageDataSettingsGroup",
   2987        control: "moz-box-group",
   2988        controlAttrs: {
   2989          type: "default",
   2990        },
   2991        items: [
   2992          {
   2993            id: "clearSiteDataButton",
   2994            l10nId: "sitedata-clear2",
   2995            control: "moz-box-button",
   2996            iconSrc: "chrome://browser/skin/flame.svg",
   2997            controlAttrs: {
   2998              "search-l10n-ids": `
   2999                clear-site-data-cookies-empty.label,
   3000                clear-site-data-cache-empty.label
   3001              `,
   3002            },
   3003          },
   3004          {
   3005            id: "siteDataSettings",
   3006            l10nId: "sitedata-settings3",
   3007            control: "moz-box-button",
   3008            controlAttrs: {
   3009              "search-l10n-ids": `
   3010                site-data-settings-window.title,
   3011                site-data-column-host.label,
   3012                site-data-column-cookies.label,
   3013                site-data-column-storage.label,
   3014                site-data-settings-description,
   3015                site-data-remove-all.label,
   3016              `,
   3017            },
   3018          },
   3019          {
   3020            id: "cookieExceptions",
   3021            l10nId: "sitedata-cookies-exceptions3",
   3022            control: "moz-box-button",
   3023            controlAttrs: {
   3024              "search-l10n-ids": `
   3025                permissions-address,
   3026                permissions-block.label,
   3027                permissions-allow.label,
   3028                permissions-remove.label,
   3029                permissions-remove-all.label,
   3030                permissions-exceptions-cookie-desc
   3031              `,
   3032            },
   3033          },
   3034        ],
   3035      },
   3036      {
   3037        id: "deleteOnClose",
   3038        l10nId: "sitedata-delete-on-close",
   3039      },
   3040    ],
   3041  },
   3042  networkProxy: {
   3043    items: [
   3044      {
   3045        id: "connectionSettings",
   3046        l10nId: "network-proxy-connection-settings",
   3047        control: "moz-box-button",
   3048        controlAttrs: {
   3049          "search-l10n-ids":
   3050            "connection-window2.title,connection-proxy-option-no.label,connection-proxy-option-auto.label,connection-proxy-option-system.label,connection-proxy-option-wpad.label,connection-proxy-option-manual.label,connection-proxy-http,connection-proxy-https,connection-proxy-http-port,connection-proxy-socks,connection-proxy-socks4,connection-proxy-socks5,connection-proxy-noproxy,connection-proxy-noproxy-desc,connection-proxy-https-sharing.label,connection-proxy-autotype.label,connection-proxy-reload.label,connection-proxy-autologin-checkbox.label,connection-proxy-socks-remote-dns.label",
   3051        },
   3052        // Bug 1990552: due to how this lays out in the legacy page, we do not include a
   3053        // controllingExtensionInfo attribute here. We will want one in the redesigned page,
   3054        // using storeId: "proxy.settings".
   3055        controllingExtensionInfo: undefined,
   3056      },
   3057    ],
   3058  },
   3059  passwords: {
   3060    inProgress: true,
   3061    id: "passwordsGroup",
   3062    l10nId: "forms-passwords-header",
   3063    headingLevel: 2,
   3064    items: [
   3065      {
   3066        id: "savePasswords",
   3067        l10nId: "forms-ask-to-save-passwords",
   3068        items: [
   3069          {
   3070            id: "managePasswordExceptions",
   3071            l10nId: "forms-manage-password-exceptions",
   3072            control: "moz-box-button",
   3073            controlAttrs: {
   3074              "search-l10n-ids":
   3075                "permissions-address,permissions-exceptions-saved-passwords-window.title,permissions-exceptions-saved-passwords-desc,",
   3076            },
   3077          },
   3078          {
   3079            id: "fillUsernameAndPasswords",
   3080            l10nId: "forms-fill-usernames-and-passwords-2",
   3081            controlAttrs: {
   3082              "search-l10n-ids": "forms-saved-passwords-searchkeywords",
   3083            },
   3084          },
   3085          {
   3086            id: "suggestStrongPasswords",
   3087            l10nId: "forms-suggest-passwords",
   3088            supportPage: "how-generate-secure-password-firefox",
   3089          },
   3090        ],
   3091      },
   3092      {
   3093        id: "requireOSAuthForPasswords",
   3094        l10nId: "forms-os-reauth-2",
   3095      },
   3096      {
   3097        id: "allowWindowSSO",
   3098        l10nId: "forms-windows-sso",
   3099        supportPage: "windows-sso",
   3100      },
   3101      {
   3102        id: "manageSavedPasswords",
   3103        l10nId: "forms-saved-passwords-2",
   3104        control: "moz-box-link",
   3105      },
   3106      {
   3107        id: "additionalProtectionsGroup",
   3108        l10nId: "forms-additional-protections-header",
   3109        control: "moz-fieldset",
   3110        controlAttrs: {
   3111          headingLevel: 2,
   3112        },
   3113        items: [
   3114          {
   3115            id: "primaryPasswordNotSet",
   3116            control: "moz-box-group",
   3117            items: [
   3118              {
   3119                id: "usePrimaryPassword",
   3120                l10nId: "forms-primary-pw-use-2",
   3121                control: "moz-box-item",
   3122                supportPage: "primary-password-stored-logins",
   3123              },
   3124              {
   3125                id: "addPrimaryPassword",
   3126                l10nId: "forms-primary-pw-set",
   3127                control: "moz-box-button",
   3128              },
   3129            ],
   3130          },
   3131          {
   3132            id: "primaryPasswordSet",
   3133            control: "moz-box-group",
   3134            items: [
   3135              {
   3136                id: "statusPrimaryPassword",
   3137                l10nId: "forms-primary-pw-on",
   3138                control: "moz-box-item",
   3139                controlAttrs: {
   3140                  iconsrc: "chrome://global/skin/icons/check-filled.svg",
   3141                },
   3142                options: [
   3143                  {
   3144                    id: "turnOffPrimaryPassword",
   3145                    l10nId: "forms-primary-pw-turn-off",
   3146                    control: "moz-button",
   3147                    slot: "actions",
   3148                  },
   3149                ],
   3150              },
   3151              {
   3152                id: "changePrimaryPassword",
   3153                l10nId: "forms-primary-pw-change-2",
   3154                control: "moz-box-button",
   3155              },
   3156            ],
   3157          },
   3158          {
   3159            id: "breachAlerts",
   3160            l10nId: "forms-breach-alerts",
   3161            supportPage: "lockwise-alerts",
   3162          },
   3163        ],
   3164      },
   3165    ],
   3166  },
   3167  history: {
   3168    items: [
   3169      {
   3170        id: "historyMode",
   3171        control: "moz-select",
   3172        options: [
   3173          {
   3174            value: "remember",
   3175            l10nId: "history-remember-option-all",
   3176          },
   3177          { value: "dontremember", l10nId: "history-remember-option-never" },
   3178          { value: "custom", l10nId: "history-remember-option-custom" },
   3179        ],
   3180        controlAttrs: {
   3181          "search-l10n-ids": `
   3182            history-remember-description3,
   3183            history-dontremember-description3,
   3184            history-private-browsing-permanent.label,
   3185            history-remember-browser-option.label,
   3186            history-remember-search-option.label,
   3187            history-clear-on-close-option.label,
   3188            history-clear-on-close-settings.label
   3189          `,
   3190        },
   3191      },
   3192      {
   3193        id: "privateBrowsingAutoStart",
   3194        l10nId: "history-private-browsing-permanent",
   3195      },
   3196      {
   3197        id: "rememberHistory",
   3198        l10nId: "history-remember-browser-option",
   3199      },
   3200      {
   3201        id: "rememberForms",
   3202        l10nId: "history-remember-search-option",
   3203      },
   3204      {
   3205        id: "alwaysClear",
   3206        l10nId: "history-clear-on-close-option",
   3207      },
   3208      {
   3209        id: "clearDataSettings",
   3210        l10nId: "history-clear-on-close-settings",
   3211        control: "moz-box-button",
   3212        controlAttrs: {
   3213          "search-l10n-ids": `
   3214            clear-data-settings-label,
   3215            history-section-label,
   3216            item-history-and-downloads.label,
   3217            item-cookies.label,
   3218            item-active-logins.label,
   3219            item-cache.label,
   3220            item-form-search-history.label,
   3221            data-section-label,
   3222            item-site-settings.label,
   3223            item-offline-apps.label
   3224          `,
   3225        },
   3226      },
   3227      {
   3228        id: "clearHistoryButton",
   3229        l10nId: "history-clear-button",
   3230        control: "moz-box-button",
   3231      },
   3232    ],
   3233  },
   3234  history2: {
   3235    inProgress: true,
   3236    l10nId: "history-section-header",
   3237    items: [
   3238      {
   3239        id: "deleteOnCloseInfo",
   3240        l10nId: "sitedata-delete-on-close-private-browsing3",
   3241        control: "moz-message-bar",
   3242      },
   3243      {
   3244        id: "historyMode",
   3245        control: "moz-radio-group",
   3246        options: [
   3247          {
   3248            value: "remember",
   3249            l10nId: "history-remember-option-all",
   3250          },
   3251          { value: "dontremember", l10nId: "history-remember-option-never" },
   3252          {
   3253            value: "custom",
   3254            l10nId: "history-remember-option-custom",
   3255            items: [
   3256              {
   3257                id: "customHistoryButton",
   3258                control: "moz-box-button",
   3259                l10nId: "history-custom-button",
   3260              },
   3261            ],
   3262          },
   3263        ],
   3264        controlAttrs: {
   3265          "search-l10n-ids": `
   3266            history-remember-description3,
   3267            history-dontremember-description3,
   3268            history-private-browsing-permanent.label,
   3269            history-remember-browser-option.label,
   3270            history-remember-search-option.label,
   3271            history-clear-on-close-option.label,
   3272            history-clear-on-close-settings.label
   3273          `,
   3274        },
   3275      },
   3276    ],
   3277  },
   3278  historyAdvanced: {
   3279    l10nId: "history-custom-section-header",
   3280    headingLevel: 2,
   3281    items: [
   3282      {
   3283        id: "privateBrowsingAutoStart",
   3284        l10nId: "history-private-browsing-permanent",
   3285      },
   3286      {
   3287        id: "rememberHistory",
   3288        l10nId: "history-remember-browser-option",
   3289      },
   3290      {
   3291        id: "rememberForms",
   3292        l10nId: "history-remember-search-option",
   3293      },
   3294      {
   3295        id: "alwaysClear",
   3296        l10nId: "history-clear-on-close-option",
   3297        items: [
   3298          {
   3299            id: "clearDataSettings",
   3300            l10nId: "history-clear-on-close-settings",
   3301            control: "moz-box-button",
   3302            controlAttrs: {
   3303              "search-l10n-ids": `
   3304                    clear-data-settings-label,
   3305                    history-section-label,
   3306                    item-history-and-downloads.label,
   3307                    item-cookies.label,
   3308                    item-active-logins.label,
   3309                    item-cache.label,
   3310                    item-form-search-history.label,
   3311                    data-section-label,
   3312                    item-site-settings.label,
   3313                    item-offline-apps.label
   3314                  `,
   3315            },
   3316          },
   3317        ],
   3318      },
   3319    ],
   3320  },
   3321  permissions: {
   3322    id: "permissions",
   3323    l10nId: "permissions-header2",
   3324    headingLevel: 2,
   3325    items: [
   3326      {
   3327        id: "permissionBox",
   3328        control: "moz-box-group",
   3329        controlAttrs: {
   3330          type: "list",
   3331        },
   3332        items: [
   3333          {
   3334            id: "locationSettingsButton",
   3335            control: "moz-box-button",
   3336            l10nId: "permissions-location2",
   3337            controlAttrs: {
   3338              ".iconSrc": "chrome://browser/skin/notification-icons/geo.svg",
   3339              "search-l10n-ids":
   3340                "permissions-remove.label,permissions-remove-all.label,permissions-site-location-window2.title,permissions-site-location-desc,permissions-site-location-disable-label,permissions-site-location-disable-desc",
   3341            },
   3342          },
   3343          {
   3344            id: "cameraSettingsButton",
   3345            control: "moz-box-button",
   3346            l10nId: "permissions-camera2",
   3347            controlAttrs: {
   3348              ".iconSrc": "chrome://browser/skin/notification-icons/camera.svg",
   3349              "search-l10n-ids":
   3350                "permissions-remove.label,permissions-remove-all.label,permissions-site-camera-window2.title,permissions-site-camera-desc,permissions-site-camera-disable-label,permissions-site-camera-disable-desc,",
   3351            },
   3352          },
   3353          {
   3354            id: "localHostSettingsButton",
   3355            control: "moz-box-button",
   3356            l10nId: "permissions-localhost2",
   3357            controlAttrs: {
   3358              ".iconSrc":
   3359                "chrome://browser/skin/notification-icons/local-host.svg",
   3360              "search-l10n-ids":
   3361                "permissions-remove.label,permissions-remove-all.label,permissions-site-localhost-window.title,permissions-site-localhost-desc,permissions-site-localhost-disable-label,permissions-site-localhost-disable-desc,",
   3362            },
   3363          },
   3364          {
   3365            id: "localNetworkSettingsButton",
   3366            control: "moz-box-button",
   3367            l10nId: "permissions-local-network2",
   3368            controlAttrs: {
   3369              ".iconSrc":
   3370                "chrome://browser/skin/notification-icons/local-network.svg",
   3371              "search-l10n-ids":
   3372                "permissions-remove.label,permissions-remove-all.label,permissions-site-local-network-window.title,permissions-site-local-network-desc,permissions-site-local-network-disable-label,permissions-site-local-network-disable-desc,",
   3373            },
   3374          },
   3375          {
   3376            id: "microphoneSettingsButton",
   3377            control: "moz-box-button",
   3378            l10nId: "permissions-microphone2",
   3379            controlAttrs: {
   3380              ".iconSrc":
   3381                "chrome://browser/skin/notification-icons/microphone.svg",
   3382              "search-l10n-ids":
   3383                "permissions-remove.label,permissions-remove-all.label,permissions-site-microphone-window2.title,permissions-site-microphone-desc,permissions-site-microphone-disable-label,permissions-site-microphone-disable-desc,",
   3384            },
   3385          },
   3386          {
   3387            id: "speakerSettingsButton",
   3388            control: "moz-box-button",
   3389            l10nId: "permissions-speaker2",
   3390            controlAttrs: {
   3391              ".iconSrc":
   3392                "chrome://browser/skin/notification-icons/speaker.svg",
   3393              "search-l10n-ids":
   3394                "permissions-remove.label,permissions-remove-all.label,permissions-site-speaker-window.title,permissions-site-speaker-desc,",
   3395            },
   3396          },
   3397          {
   3398            id: "notificationSettingsButton",
   3399            control: "moz-box-button",
   3400            l10nId: "permissions-notification2",
   3401            controlAttrs: {
   3402              ".iconSrc":
   3403                "chrome://browser/skin/notification-icons/desktop-notification.svg",
   3404              "search-l10n-ids":
   3405                "permissions-remove.label,permissions-remove-all.label,permissions-site-notification-window2.title,permissions-site-notification-desc,permissions-site-notification-disable-label,permissions-site-notification-disable-desc,",
   3406            },
   3407          },
   3408          {
   3409            id: "autoplaySettingsButton",
   3410            control: "moz-box-button",
   3411            l10nId: "permissions-autoplay2",
   3412            controlAttrs: {
   3413              ".iconSrc":
   3414                "chrome://browser/skin/notification-icons/autoplay-media.svg",
   3415              "search-l10n-ids":
   3416                "permissions-remove.label,permissions-remove-all.label,permissions-site-autoplay-window2.title,permissions-site-autoplay-desc,",
   3417            },
   3418          },
   3419          {
   3420            id: "xrSettingsButton",
   3421            control: "moz-box-button",
   3422            l10nId: "permissions-xr2",
   3423            controlAttrs: {
   3424              ".iconSrc": "chrome://browser/skin/notification-icons/xr.svg",
   3425              "search-l10n-ids":
   3426                "permissions-remove.label,permissions-remove-all.label,permissions-site-xr-window2.title,permissions-site-xr-desc,permissions-site-xr-disable-label,permissions-site-xr-disable-desc,",
   3427            },
   3428          },
   3429        ],
   3430      },
   3431      {
   3432        id: "popupPolicy",
   3433        l10nId: "permissions-block-popups2",
   3434        items: [
   3435          {
   3436            id: "popupPolicyButton",
   3437            l10nId: "permissions-block-popups-exceptions-button2",
   3438            control: "moz-box-button",
   3439            controlAttrs: {
   3440              "search-l10n-ids":
   3441                "permissions-address,permissions-exceptions-popup-window3.title,permissions-exceptions-popup-desc2",
   3442            },
   3443          },
   3444        ],
   3445      },
   3446      {
   3447        id: "warnAddonInstall",
   3448        l10nId: "permissions-addon-install-warning2",
   3449        items: [
   3450          {
   3451            id: "addonExceptions",
   3452            l10nId: "permissions-addon-exceptions2",
   3453            control: "moz-box-button",
   3454            controlAttrs: {
   3455              "search-l10n-ids":
   3456                "permissions-address,permissions-allow.label,permissions-remove.label,permissions-remove-all.label,permissions-exceptions-addons-window2.title,permissions-exceptions-addons-desc",
   3457            },
   3458          },
   3459        ],
   3460      },
   3461      {
   3462        id: "notificationsDoNotDisturb",
   3463        l10nId: "permissions-notification-pause",
   3464      },
   3465    ],
   3466  },
   3467  dnsOverHttps: {
   3468    inProgress: true,
   3469    items: [
   3470      {
   3471        id: "dohBox",
   3472        control: "moz-box-group",
   3473        items: [
   3474          {
   3475            id: "dohModeBoxItem",
   3476            control: "moz-box-item",
   3477          },
   3478          {
   3479            id: "dohAdvancedButton",
   3480            l10nId: "preferences-doh-advanced-button",
   3481            control: "moz-box-button",
   3482          },
   3483        ],
   3484      },
   3485    ],
   3486  },
   3487  defaultEngine: {
   3488    l10nId: "search-engine-group",
   3489    headingLevel: 2,
   3490    items: [
   3491      {
   3492        id: "defaultEngineNormal",
   3493        l10nId: "search-default-engine",
   3494        control: "moz-select",
   3495      },
   3496      {
   3497        id: "searchShowSearchTermCheckbox",
   3498        l10nId: "search-show-search-term-option-2",
   3499      },
   3500      {
   3501        id: "browserSeparateDefaultEngine",
   3502        l10nId: "search-separate-default-engine-2",
   3503        items: [
   3504          {
   3505            id: "defaultPrivateEngine",
   3506            l10nId: "search-separate-default-engine-dropdown",
   3507            control: "moz-select",
   3508          },
   3509        ],
   3510      },
   3511    ],
   3512  },
   3513  searchSuggestions: {
   3514    l10nId: "search-suggestions-header-2",
   3515    headingLevel: 2,
   3516    items: [
   3517      {
   3518        id: "suggestionsInSearchFieldsCheckbox",
   3519        l10nId: "search-show-suggestions-option",
   3520        items: [
   3521          {
   3522            id: "urlBarSuggestionCheckbox",
   3523            l10nId: "search-show-suggestions-url-bar-option",
   3524          },
   3525          {
   3526            id: "showSearchSuggestionsFirstCheckbox",
   3527            l10nId: "search-show-suggestions-above-history-option-2",
   3528          },
   3529          {
   3530            id: "showSearchSuggestionsPrivateWindowsCheckbox",
   3531            l10nId: "search-show-suggestions-private-windows-2",
   3532          },
   3533          {
   3534            id: "showTrendingSuggestionsCheckbox",
   3535            l10nId: "addressbar-locbar-showtrendingsuggestions-option-2",
   3536            supportPage: "use-google-trending-search-firefox-address-bar",
   3537          },
   3538          {
   3539            id: "urlBarSuggestionPermanentPBMessage",
   3540            l10nId: "search-suggestions-cant-show-2",
   3541            control: "moz-message-bar",
   3542          },
   3543        ],
   3544      },
   3545    ],
   3546  },
   3547  dnsOverHttpsAdvanced: {
   3548    inProgress: true,
   3549    l10nId: "preferences-doh-advanced-section",
   3550    supportPage: "dns-over-https",
   3551    headingLevel: 2,
   3552    items: [
   3553      {
   3554        id: "dohStatusBox",
   3555        control: "moz-message-bar",
   3556      },
   3557      {
   3558        id: "dohRadioGroup",
   3559        control: "moz-radio-group",
   3560        options: [
   3561          {
   3562            id: "dohRadioDefault",
   3563            value: "default",
   3564            l10nId: "preferences-doh-radio-default",
   3565          },
   3566          {
   3567            id: "dohRadioCustom",
   3568            value: "custom",
   3569            l10nId: "preferences-doh-radio-custom",
   3570            items: [
   3571              {
   3572                id: "dohFallbackIfCustom",
   3573                l10nId: "preferences-doh-fallback-label",
   3574              },
   3575              {
   3576                id: "dohProviderSelect",
   3577                l10nId: "preferences-doh-select-resolver-label",
   3578                control: "moz-select",
   3579              },
   3580              {
   3581                id: "dohCustomProvider",
   3582                control: "moz-input-text",
   3583                l10nId: "preferences-doh-custom-provider-label",
   3584              },
   3585            ],
   3586          },
   3587          {
   3588            id: "dohRadioOff",
   3589            value: "off",
   3590            l10nId: "preferences-doh-radio-off",
   3591          },
   3592        ],
   3593      },
   3594      {
   3595        id: "dohExceptionsButton",
   3596        l10nId: "preferences-doh-manage-exceptions2",
   3597        control: "moz-box-button",
   3598        controlAttrs: {
   3599          "search-l10n-ids":
   3600            "permissions-doh-entry-field,permissions-doh-add-exception.label,permissions-doh-remove.label,permissions-doh-remove-all.label,permissions-exceptions-doh-window.title,permissions-exceptions-manage-doh-desc,",
   3601        },
   3602      },
   3603    ],
   3604  },
   3605  managePayments: {
   3606    items: [
   3607      {
   3608        id: "add-payment-button",
   3609        control: "moz-button",
   3610        l10nId: "autofill-payment-methods-add-button",
   3611      },
   3612      {
   3613        id: "payments-list",
   3614        control: "moz-box-group",
   3615        controlAttrs: {
   3616          type: "list",
   3617        },
   3618      },
   3619    ],
   3620  },
   3621  tabs: {
   3622    l10nId: "tabs-group-header2",
   3623    headingLevel: 2,
   3624    items: [
   3625      {
   3626        id: "tabsOpening",
   3627        control: "moz-fieldset",
   3628        l10nId: "tabs-opening-heading",
   3629        headingLevel: 3,
   3630        items: [
   3631          {
   3632            id: "linkTargeting",
   3633            l10nId: "open-new-link-as-tabs",
   3634          },
   3635          {
   3636            id: "switchToNewTabs",
   3637            l10nId: "switch-to-new-tabs",
   3638          },
   3639          {
   3640            id: "openAppLinksNextToActiveTab",
   3641            l10nId: "open-external-link-next-to-active-tab",
   3642          },
   3643          {
   3644            id: "warnOpenMany",
   3645            l10nId: "warn-on-open-many-tabs",
   3646          },
   3647        ],
   3648      },
   3649      {
   3650        id: "tabsInteraction",
   3651        control: "moz-fieldset",
   3652        l10nId: "tabs-interaction-heading",
   3653        headingLevel: 3,
   3654        items: [
   3655          {
   3656            id: "ctrlTabRecentlyUsedOrder",
   3657            l10nId: "ctrl-tab-recently-used-order",
   3658          },
   3659          {
   3660            id: "tabPreviewShowThumbnails",
   3661            l10nId: "settings-tabs-show-image-in-preview",
   3662          },
   3663          {
   3664            id: "tabGroupSuggestions",
   3665            l10nId: "settings-tabs-show-group-and-tab-suggestions",
   3666          },
   3667          {
   3668            id: "showTabsInTaskbar",
   3669            l10nId: "show-tabs-in-taskbar",
   3670          },
   3671        ],
   3672      },
   3673      {
   3674        id: "browserContainersbox",
   3675        control: "moz-fieldset",
   3676        l10nId: "tabs-containers-heading",
   3677        headingLevel: 3,
   3678        items: [
   3679          {
   3680            id: "browserContainersCheckbox",
   3681            l10nId: "browser-containers-enabled",
   3682            supportPage: "containers",
   3683          },
   3684          {
   3685            id: "browserContainersSettings",
   3686            l10nId: "browser-containers-settings",
   3687            control: "moz-box-button",
   3688            controlAttrs: {
   3689              "search-l10n-ids":
   3690                "containers-add-button.label, containers-settings-button.label, containers-remove-button.label, containers-new-tab-check.label",
   3691            },
   3692          },
   3693        ],
   3694      },
   3695      {
   3696        id: "tabsClosing",
   3697        control: "moz-fieldset",
   3698        l10nId: "tabs-closing-heading",
   3699        headingLevel: 3,
   3700        items: [
   3701          {
   3702            id: "warnCloseMultiple",
   3703            l10nId: "ask-on-close-multiple-tabs",
   3704          },
   3705          {
   3706            id: "warnOnQuitKey",
   3707            l10nId: "ask-on-quit-with-key",
   3708          },
   3709        ],
   3710      },
   3711    ],
   3712  },
   3713  etpStatus: {
   3714    inProgress: true,
   3715    headingLevel: 2,
   3716    l10nId: "preferences-etp-status-header",
   3717    supportPage: "enhanced-tracking-protection",
   3718    iconSrc: "chrome://browser/skin/controlcenter/tracking-protection.svg",
   3719    items: [
   3720      {
   3721        id: "etpStatusBoxGroup",
   3722        control: "moz-box-group",
   3723        items: [
   3724          {
   3725            id: "etpStatusItem",
   3726            l10nId: "preferences-etp-level-standard",
   3727            control: "moz-box-item",
   3728          },
   3729          {
   3730            id: "etpStatusAdvancedButton",
   3731            l10nId: "preferences-etp-status-advanced-button",
   3732            control: "moz-box-button",
   3733          },
   3734        ],
   3735      },
   3736      {
   3737        id: "protectionsDashboardLink",
   3738        l10nId: "preferences-etp-status-protections-dashboard-link",
   3739        control: "moz-box-link",
   3740        controlAttrs: {
   3741          href: "about:protections",
   3742        },
   3743      },
   3744    ],
   3745  },
   3746  etpBanner: {
   3747    inProgress: true,
   3748    items: [
   3749      {
   3750        id: "etpBannerEl",
   3751        control: "moz-card",
   3752      },
   3753    ],
   3754  },
   3755  etpAdvanced: {
   3756    inProgress: true,
   3757    headingLevel: 2,
   3758    l10nId: "preferences-etp-advanced-settings-group",
   3759    supportPage: "enhanced-tracking-protection",
   3760    items: [
   3761      {
   3762        id: "contentBlockingCategoryRadioGroup",
   3763        control: "moz-radio-group",
   3764        options: [
   3765          {
   3766            id: "etpLevelStandard",
   3767            value: "standard",
   3768            l10nId: "preferences-etp-level-standard",
   3769          },
   3770          {
   3771            id: "etpLevelStrict",
   3772            value: "strict",
   3773            l10nId: "preferences-etp-level-strict",
   3774            items: [
   3775              {
   3776                id: "etpAllowListBaselineEnabled",
   3777                l10nId: "content-blocking-baseline-exceptions-3",
   3778                supportPage: "manage-enhanced-tracking-protection-exceptions",
   3779                control: "moz-checkbox",
   3780                items: [
   3781                  {
   3782                    id: "etpAllowListConvenienceEnabled",
   3783                    l10nId: "content-blocking-convenience-exceptions-3",
   3784                    control: "moz-checkbox",
   3785                  },
   3786                ],
   3787              },
   3788            ],
   3789          },
   3790          {
   3791            id: "etpLevelCustom",
   3792            value: "custom",
   3793            l10nId: "preferences-etp-level-custom",
   3794            items: [
   3795              {
   3796                id: "etpCustomizeButton",
   3797                l10nId: "preferences-etp-customize-button",
   3798                control: "moz-box-button",
   3799              },
   3800            ],
   3801          },
   3802        ],
   3803      },
   3804      {
   3805        id: "reloadTabsHint",
   3806        control: "moz-message-bar",
   3807        l10nId: "preferences-etp-reload-tabs-hint",
   3808        options: [
   3809          {
   3810            control: "moz-button",
   3811            l10nId: "preferences-etp-reload-tabs-hint-button",
   3812            slot: "actions",
   3813          },
   3814        ],
   3815      },
   3816      {
   3817        id: "rfpWarning",
   3818        control: "moz-message-bar",
   3819        l10nId: "preferences-etp-rfp-warning-message",
   3820        supportPage: "resist-fingerprinting",
   3821      },
   3822      {
   3823        id: "etpLevelWarning",
   3824        control: "moz-promo",
   3825        l10nId: "preferences-etp-level-warning-message",
   3826        controlAttrs: {
   3827          ".imageAlignment": "end",
   3828          ".imageSrc":
   3829            "chrome://browser/content/preferences/etp-toggle-promo.svg",
   3830        },
   3831      },
   3832      {
   3833        id: "etpManageExceptionsButton",
   3834        l10nId: "preferences-etp-manage-exceptions-button",
   3835        control: "moz-box-button",
   3836      },
   3837    ],
   3838  },
   3839  etpReset: {
   3840    inProgress: true,
   3841    headingLevel: 2,
   3842    l10nId: "preferences-etp-reset",
   3843    items: [
   3844      {
   3845        id: "etpResetButtonGroup",
   3846        control: "div",
   3847        items: [
   3848          {
   3849            id: "etpResetStandardButton",
   3850            control: "moz-button",
   3851            l10nId: "preferences-etp-reset-standard-button",
   3852          },
   3853          {
   3854            id: "etpResetStrictButton",
   3855            control: "moz-button",
   3856            l10nId: "preferences-etp-reset-strict-button",
   3857          },
   3858        ],
   3859      },
   3860    ],
   3861  },
   3862  etpCustomize: {
   3863    inProgress: true,
   3864    headingLevel: 2,
   3865    l10nId: "preferences-etp-custom-control-group",
   3866    items: [
   3867      {
   3868        id: "etpAllowListBaselineEnabledCustom",
   3869        l10nId: "content-blocking-baseline-exceptions-3",
   3870        supportPage: "manage-enhanced-tracking-protection-exceptions",
   3871        control: "moz-checkbox",
   3872        items: [
   3873          {
   3874            id: "etpAllowListConvenienceEnabledCustom",
   3875            l10nId: "content-blocking-convenience-exceptions-3",
   3876            control: "moz-checkbox",
   3877          },
   3878        ],
   3879      },
   3880      {
   3881        id: "etpCustomCookiesEnabled",
   3882        l10nId: "preferences-etp-custom-cookies-enabled",
   3883        control: "moz-toggle",
   3884        items: [
   3885          {
   3886            id: "cookieBehavior",
   3887            l10nId: "preferences-etp-custom-cookie-behavior",
   3888            control: "moz-select",
   3889            options: [
   3890              {
   3891                value: Ci.nsICookieService.BEHAVIOR_ACCEPT.toString(),
   3892                l10nId: "preferences-etpc-custom-cookie-behavior-accept-all",
   3893              },
   3894              {
   3895                value: Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER.toString(),
   3896                l10nId: "sitedata-option-block-cross-site-trackers",
   3897              },
   3898              {
   3899                value:
   3900                  Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN.toString(),
   3901                l10nId: "sitedata-option-block-cross-site-cookies2",
   3902              },
   3903              {
   3904                value: Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN.toString(),
   3905                l10nId: "sitedata-option-block-unvisited",
   3906              },
   3907              {
   3908                value: Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN.toString(),
   3909                l10nId: "sitedata-option-block-all-cross-site-cookies",
   3910              },
   3911              {
   3912                value: Ci.nsICookieService.BEHAVIOR_REJECT.toString(),
   3913                l10nId: "sitedata-option-block-all",
   3914              },
   3915            ],
   3916          },
   3917        ],
   3918      },
   3919      {
   3920        id: "etpCustomTrackingProtectionEnabled",
   3921        l10nId: "preferences-etp-custom-tracking-protection-enabled",
   3922        control: "moz-toggle",
   3923        items: [
   3924          {
   3925            id: "etpCustomTrackingProtectionEnabledContext",
   3926            l10nId:
   3927              "preferences-etp-custom-tracking-protection-enabled-context",
   3928            control: "moz-select",
   3929            options: [
   3930              {
   3931                value: "all",
   3932                l10nId:
   3933                  "content-blocking-tracking-protection-option-all-windows",
   3934              },
   3935              {
   3936                value: "pbmOnly",
   3937                l10nId: "content-blocking-option-private",
   3938              },
   3939            ],
   3940          },
   3941        ],
   3942      },
   3943      {
   3944        id: "etpCustomCryptominingProtectionEnabled",
   3945        l10nId: "preferences-etp-custom-crypto-mining-protection-enabled",
   3946        control: "moz-toggle",
   3947      },
   3948      {
   3949        id: "etpCustomKnownFingerprintingProtectionEnabled",
   3950        l10nId:
   3951          "preferences-etp-custom-known-fingerprinting-protection-enabled",
   3952        control: "moz-toggle",
   3953      },
   3954      {
   3955        id: "etpCustomSuspectFingerprintingProtectionEnabled",
   3956        l10nId:
   3957          "preferences-etp-custom-suspect-fingerprinting-protection-enabled",
   3958        control: "moz-toggle",
   3959        items: [
   3960          {
   3961            id: "etpCustomSuspectFingerprintingProtectionEnabledContext",
   3962            l10nId:
   3963              "preferences-etp-custom-suspect-fingerprinting-protection-enabled-context",
   3964            control: "moz-select",
   3965            options: [
   3966              {
   3967                value: "all",
   3968                l10nId:
   3969                  "content-blocking-tracking-protection-option-all-windows",
   3970              },
   3971              {
   3972                value: "pbmOnly",
   3973                l10nId: "content-blocking-option-private",
   3974              },
   3975            ],
   3976          },
   3977        ],
   3978      },
   3979    ],
   3980  },
   3981  manageAddresses: {
   3982    items: [
   3983      {
   3984        id: "add-address-button",
   3985        control: "moz-button",
   3986        l10nId: "autofill-addresses-add-button",
   3987      },
   3988      {
   3989        id: "addresses-list",
   3990        control: "moz-box-group",
   3991        controlAttrs: {
   3992          type: "list",
   3993        },
   3994      },
   3995    ],
   3996  },
   3997  sync: {
   3998    inProgress: true,
   3999    l10nId: "sync-group-label",
   4000    headingLevel: 2,
   4001    items: [
   4002      {
   4003        id: "syncNoFxaSignIn",
   4004        l10nId: "sync-signedout-account-signin-4",
   4005        control: "moz-box-link",
   4006        iconSrc: "chrome://global/skin/icons/warning.svg",
   4007        controlAttrs: {
   4008          id: "noFxaSignIn",
   4009        },
   4010      },
   4011      {
   4012        id: "syncConfigured",
   4013        control: "moz-box-group",
   4014        items: [
   4015          {
   4016            id: "syncStatus",
   4017            l10nId: "prefs-syncing-on-2",
   4018            control: "moz-box-item",
   4019            iconSrc: "chrome://global/skin/icons/check-filled.svg",
   4020            items: [
   4021              {
   4022                id: "syncNow",
   4023                control: "moz-button",
   4024                l10nId: "prefs-sync-now-button-2",
   4025                slot: "actions",
   4026              },
   4027              {
   4028                id: "syncing",
   4029                control: "moz-button",
   4030                l10nId: "prefs-syncing-button-2",
   4031                slot: "actions",
   4032              },
   4033            ],
   4034          },
   4035          {
   4036            id: "syncEnginesList",
   4037            control: "sync-engines-list",
   4038          },
   4039          {
   4040            id: "syncChangeOptions",
   4041            control: "moz-box-button",
   4042            l10nId: "sync-manage-options-2",
   4043          },
   4044        ],
   4045      },
   4046      {
   4047        id: "syncNotConfigured",
   4048        l10nId: "prefs-syncing-off-2",
   4049        control: "moz-box-item",
   4050        iconSrc: "chrome://global/skin/icons/warning.svg",
   4051        items: [
   4052          {
   4053            id: "syncSetup",
   4054            control: "moz-button",
   4055            l10nId: "prefs-sync-turn-on-syncing-2",
   4056            slot: "actions",
   4057          },
   4058        ],
   4059      },
   4060      {
   4061        id: "fxaDeviceNameSection",
   4062        l10nId: "sync-device-name-header-2",
   4063        control: "moz-fieldset",
   4064        controlAttrs: {
   4065          ".headingLevel": 3,
   4066        },
   4067        items: [
   4068          {
   4069            id: "fxaDeviceNameGroup",
   4070            control: "moz-box-group",
   4071            items: [
   4072              {
   4073                id: "fxaDeviceName",
   4074                control: "sync-device-name",
   4075              },
   4076              {
   4077                id: "fxaConnectAnotherDevice",
   4078                l10nId: "sync-connect-another-device-2",
   4079                control: "moz-box-link",
   4080                iconSrc: "chrome://browser/skin/device-phone.svg",
   4081                controlAttrs: {
   4082                  id: "connect-another-device",
   4083                  href: "https://accounts.firefox.com/pair",
   4084                },
   4085              },
   4086            ],
   4087          },
   4088        ],
   4089      },
   4090    ],
   4091  },
   4092  account: {
   4093    inProgress: true,
   4094    l10nId: "account-group-label",
   4095    headingLevel: 2,
   4096    items: [
   4097      {
   4098        id: "noFxaAccountGroup",
   4099        control: "moz-box-group",
   4100        items: [
   4101          {
   4102            id: "noFxaAccount",
   4103            control: "placeholder-message",
   4104            l10nId: "account-placeholder",
   4105            controlAttrs: {
   4106              imagesrc: "chrome://global/skin/illustrations/security-error.svg",
   4107            },
   4108          },
   4109          {
   4110            id: "noFxaSignIn",
   4111            control: "moz-box-link",
   4112            l10nId: "sync-signedout-account-short",
   4113          },
   4114        ],
   4115      },
   4116      {
   4117        id: "fxaSignedInGroup",
   4118        control: "moz-box-group",
   4119        items: [
   4120          {
   4121            id: "fxaLoginVerified",
   4122            control: "moz-box-item",
   4123            l10nId: "sync-account-signed-in",
   4124            l10nArgs: { email: "" },
   4125            iconSrc: "chrome://browser/skin/fxa/avatar-color.svg",
   4126            controlAttrs: {
   4127              layout: "large-icon",
   4128            },
   4129          },
   4130          {
   4131            id: "verifiedManage",
   4132            control: "moz-box-link",
   4133            l10nId: "sync-manage-account2",
   4134            controlAttrs: {
   4135              href: "https://accounts.firefox.com/settings",
   4136            },
   4137          },
   4138          {
   4139            id: "fxaUnlinkButton",
   4140            control: "moz-box-button",
   4141            l10nId: "sync-sign-out2",
   4142          },
   4143        ],
   4144      },
   4145      {
   4146        id: "fxaUnverifiedGroup",
   4147        control: "moz-box-group",
   4148        items: [
   4149          {
   4150            id: "fxaLoginUnverified",
   4151            control: "placeholder-message",
   4152            l10nId: "sync-signedin-unverified2",
   4153            l10nArgs: { email: "" },
   4154            controlAttrs: {
   4155              imagesrc: "chrome://global/skin/illustrations/security-error.svg",
   4156            },
   4157          },
   4158          {
   4159            id: "verifyFxaAccount",
   4160            control: "moz-box-link",
   4161            l10nId: "sync-verify-account",
   4162          },
   4163          {
   4164            id: "unverifiedUnlinkFxaAccount",
   4165            control: "moz-box-button",
   4166            l10nId: "sync-remove-account",
   4167          },
   4168        ],
   4169      },
   4170      {
   4171        id: "fxaLoginRejectedGroup",
   4172        control: "moz-box-group",
   4173        items: [
   4174          {
   4175            id: "fxaLoginRejected",
   4176            control: "placeholder-message",
   4177            l10nId: "sync-signedin-login-failure2",
   4178            l10nArgs: { email: "" },
   4179            controlAttrs: {
   4180              imagesrc: "chrome://global/skin/illustrations/security-error.svg",
   4181            },
   4182          },
   4183          {
   4184            id: "rejectReSignIn",
   4185            control: "moz-box-link",
   4186            l10nId: "sync-sign-in",
   4187          },
   4188          {
   4189            id: "rejectUnlinkFxaAccount",
   4190            control: "moz-box-button",
   4191            l10nId: "sync-remove-account",
   4192          },
   4193        ],
   4194      },
   4195    ],
   4196  },
   4197  translationsAutomaticTranslation: {
   4198    inProgress: true,
   4199    headingLevel: 2,
   4200    l10nId: "settings-translations-subpage-automatic-translation-header",
   4201    items: [
   4202      {
   4203        id: "translationsAlwaysTranslateLanguagesGroup",
   4204        control: "moz-box-group",
   4205        controlAttrs: {
   4206          type: "list",
   4207        },
   4208        items: [
   4209          {
   4210            id: "translationsAlwaysTranslateLanguagesRow",
   4211            l10nId: "settings-translations-subpage-always-translate-header",
   4212            control: "moz-box-item",
   4213            slot: "header",
   4214            controlAttrs: {
   4215              class: "box-header-bold",
   4216            },
   4217            items: [
   4218              {
   4219                id: "translationsAlwaysTranslateLanguagesSelect",
   4220                slot: "actions",
   4221                control: "moz-select",
   4222                options: [
   4223                  {
   4224                    value: "",
   4225                    l10nId:
   4226                      "settings-translations-subpage-language-select-option",
   4227                  },
   4228                ],
   4229              },
   4230              {
   4231                id: "translationsAlwaysTranslateLanguagesButton",
   4232                l10nId: "settings-translations-subpage-language-add-button",
   4233                control: "moz-button",
   4234                slot: "actions",
   4235                controlAttrs: {
   4236                  type: "icon",
   4237                  iconsrc: "chrome://global/skin/icons/plus.svg",
   4238                },
   4239              },
   4240            ],
   4241          },
   4242          {
   4243            id: "translationsAlwaysTranslateLanguagesNoneRow",
   4244            l10nId: "settings-translations-subpage-no-languages-added",
   4245            control: "moz-box-item",
   4246            controlAttrs: {
   4247              class: "description-deemphasized",
   4248            },
   4249          },
   4250        ],
   4251      },
   4252      {
   4253        id: "translationsNeverTranslateLanguagesGroup",
   4254        control: "moz-box-group",
   4255        controlAttrs: {
   4256          type: "list",
   4257        },
   4258        items: [
   4259          {
   4260            id: "translationsNeverTranslateLanguagesRow",
   4261            l10nId: "settings-translations-subpage-never-translate-header",
   4262            control: "moz-box-item",
   4263            slot: "header",
   4264            controlAttrs: {
   4265              class: "box-header-bold",
   4266            },
   4267            items: [
   4268              {
   4269                id: "translationsNeverTranslateLanguagesSelect",
   4270                slot: "actions",
   4271                control: "moz-select",
   4272                options: [
   4273                  {
   4274                    value: "",
   4275                    l10nId:
   4276                      "settings-translations-subpage-language-select-option",
   4277                  },
   4278                ],
   4279              },
   4280              {
   4281                id: "translationsNeverTranslateLanguagesButton",
   4282                l10nId: "settings-translations-subpage-language-add-button",
   4283                control: "moz-button",
   4284                slot: "actions",
   4285                controlAttrs: {
   4286                  type: "icon",
   4287                  iconsrc: "chrome://global/skin/icons/plus.svg",
   4288                },
   4289              },
   4290            ],
   4291          },
   4292          {
   4293            id: "translationsNeverTranslateLanguagesNoneRow",
   4294            l10nId: "settings-translations-subpage-no-languages-added",
   4295            control: "moz-box-item",
   4296            controlAttrs: {
   4297              class: "description-deemphasized",
   4298            },
   4299          },
   4300        ],
   4301      },
   4302      {
   4303        id: "translationsNeverTranslateSitesGroup",
   4304        control: "moz-box-group",
   4305        controlAttrs: {
   4306          type: "list",
   4307        },
   4308        items: [
   4309          {
   4310            id: "translationsNeverTranslateSitesRow",
   4311            l10nId:
   4312              "settings-translations-subpage-never-translate-sites-header",
   4313            control: "moz-box-item",
   4314            controlAttrs: {
   4315              class: "box-header-bold",
   4316              ".description": createNeverTranslateSitesDescription(),
   4317            },
   4318          },
   4319          {
   4320            id: "translationsNeverTranslateSitesNoneRow",
   4321            l10nId: "settings-translations-subpage-no-sites-added",
   4322            control: "moz-box-item",
   4323            controlAttrs: {
   4324              class: "description-deemphasized",
   4325            },
   4326          },
   4327        ],
   4328      },
   4329    ],
   4330  },
   4331  translationsDownloadLanguages: {
   4332    inProgress: true,
   4333    headingLevel: 2,
   4334    l10nId: "settings-translations-subpage-speed-up-translation-header",
   4335    items: [
   4336      {
   4337        id: "translationsDownloadLanguagesGroup",
   4338        control: "moz-box-group",
   4339        controlAttrs: {
   4340          type: "list",
   4341        },
   4342        items: [
   4343          {
   4344            id: "translationsDownloadLanguagesRow",
   4345            l10nId: "settings-translations-subpage-download-languages-header",
   4346            control: "moz-box-item",
   4347            slot: "header",
   4348            controlAttrs: {
   4349              class: "box-header-bold",
   4350            },
   4351            items: [
   4352              {
   4353                id: "translationsDownloadLanguagesSelect",
   4354                slot: "actions",
   4355                control: "moz-select",
   4356                options: [
   4357                  {
   4358                    value: "",
   4359                    l10nId:
   4360                      "settings-translations-subpage-download-languages-select-option",
   4361                  },
   4362                ],
   4363              },
   4364              {
   4365                id: "translationsDownloadLanguagesButton",
   4366                l10nId:
   4367                  "settings-translations-subpage-download-languages-button",
   4368                control: "moz-button",
   4369                slot: "actions",
   4370                controlAttrs: {
   4371                  type: "icon",
   4372                  iconsrc: "chrome://browser/skin/downloads/downloads.svg",
   4373                },
   4374              },
   4375            ],
   4376          },
   4377          {
   4378            id: "translationsDownloadLanguagesNoneRow",
   4379            l10nId: "settings-translations-subpage-no-languages-downloaded",
   4380            control: "moz-box-item",
   4381            controlAttrs: {
   4382              class: "description-deemphasized",
   4383            },
   4384          },
   4385        ],
   4386      },
   4387    ],
   4388  },
   4389  firefoxSuggest: {
   4390    id: "locationBarGroup",
   4391    items: [
   4392      {
   4393        id: "locationBarGroupHeader",
   4394        l10nId: "addressbar-header-1",
   4395        supportPage: "firefox-suggest",
   4396        control: "moz-fieldset",
   4397        controlAttrs: {
   4398          headinglevel: 2,
   4399        },
   4400        items: [
   4401          {
   4402            id: "historySuggestion",
   4403            l10nId: "addressbar-locbar-history-option",
   4404          },
   4405          {
   4406            id: "bookmarkSuggestion",
   4407            l10nId: "addressbar-locbar-bookmarks-option",
   4408          },
   4409          {
   4410            id: "clipboardSuggestion",
   4411            l10nId: "addressbar-locbar-clipboard-option",
   4412          },
   4413          {
   4414            id: "openpageSuggestion",
   4415            l10nId: "addressbar-locbar-openpage-option",
   4416          },
   4417          {
   4418            id: "topSitesSuggestion",
   4419            l10nId: "addressbar-locbar-shortcuts-option",
   4420          },
   4421          {
   4422            id: "enableRecentSearches",
   4423            l10nId: "addressbar-locbar-showrecentsearches-option-2",
   4424          },
   4425          {
   4426            id: "enginesSuggestion",
   4427            l10nId: "addressbar-locbar-engines-option-1",
   4428          },
   4429          {
   4430            id: "enableQuickActions",
   4431            l10nId: "addressbar-locbar-quickactions-option",
   4432            supportPage: "quick-actions-firefox-search-bar",
   4433          },
   4434          {
   4435            id: "firefoxSuggestAll",
   4436            l10nId: "addressbar-locbar-suggest-all-option-2",
   4437            items: [
   4438              {
   4439                id: "firefoxSuggestSponsored",
   4440                l10nId: "addressbar-locbar-suggest-sponsored-option-2",
   4441              },
   4442              {
   4443                id: "firefoxSuggestOnlineEnabledToggle",
   4444                l10nId: "addressbar-firefox-suggest-online",
   4445                supportPage: "firefox-suggest",
   4446                subcategory: "w_what-is-firefox-suggest",
   4447              },
   4448            ],
   4449          },
   4450          {
   4451            id: "dismissedSuggestionsDescription",
   4452            l10nId: "addressbar-dismissed-suggestions-label-2",
   4453            control: "moz-fieldset",
   4454            controlAttrs: {
   4455              headinglevel: 3,
   4456            },
   4457            items: [
   4458              {
   4459                id: "restoreDismissedSuggestions",
   4460                l10nId: "addressbar-restore-dismissed-suggestions-button-2",
   4461                control: "moz-button",
   4462                iconSrc:
   4463                  "chrome://global/skin/icons/arrow-counterclockwise-16.svg",
   4464              },
   4465            ],
   4466          },
   4467        ],
   4468      },
   4469    ],
   4470  },
   4471 });
   4472 
   4473 /**
   4474 * @param {string} id - ID of {@link SettingGroup} custom element.
   4475 */
   4476 function initSettingGroup(id) {
   4477  /** @type {SettingGroup} */
   4478  let group = document.querySelector(`setting-group[groupid=${id}]`);
   4479  const config = SettingGroupManager.get(id);
   4480  if (group && config) {
   4481    if (config.inProgress && !srdSectionEnabled(id)) {
   4482      group.remove();
   4483      return;
   4484    }
   4485 
   4486    let legacySections = document.querySelectorAll(`[data-srd-groupid=${id}]`);
   4487    for (let section of legacySections) {
   4488      section.hidden = true;
   4489      section.removeAttribute("data-category");
   4490      section.setAttribute("data-hidden-from-search", "true");
   4491    }
   4492    group.config = config;
   4493    group.getSetting = Preferences.getSetting.bind(Preferences);
   4494  }
   4495 }
   4496 
   4497 ChromeUtils.defineLazyGetter(this, "gIsPackagedApp", () => {
   4498  return Services.sysinfo.getProperty("isPackagedApp");
   4499 });
   4500 
   4501 // A promise that resolves when the list of application handlers is loaded.
   4502 // We store this in a global so tests can await it.
   4503 var promiseLoadHandlersList;
   4504 
   4505 // Load the preferences string bundle for other locales with fallbacks.
   4506 function getBundleForLocales(newLocales) {
   4507  let locales = Array.from(
   4508    new Set([
   4509      ...newLocales,
   4510      ...Services.locale.requestedLocales,
   4511      Services.locale.lastFallbackLocale,
   4512    ])
   4513  );
   4514  return new Localization(
   4515    ["browser/preferences/preferences.ftl", "branding/brand.ftl"],
   4516    false,
   4517    undefined,
   4518    locales
   4519  );
   4520 }
   4521 
   4522 var gNodeToObjectMap = new WeakMap();
   4523 
   4524 var gMainPane = {
   4525  // The set of types the app knows how to handle.  A hash of HandlerInfoWrapper
   4526  // objects, indexed by type.
   4527  _handledTypes: {},
   4528 
   4529  // The list of types we can show, sorted by the sort column/direction.
   4530  // An array of HandlerInfoWrapper objects.  We build this list when we first
   4531  // load the data and then rebuild it when users change a pref that affects
   4532  // what types we can show or change the sort column/direction.
   4533  // Note: this isn't necessarily the list of types we *will* show; if the user
   4534  // provides a filter string, we'll only show the subset of types in this list
   4535  // that match that string.
   4536  _visibleTypes: [],
   4537 
   4538  // browser.startup.page values
   4539  STARTUP_PREF_BLANK: 0,
   4540  STARTUP_PREF_HOMEPAGE: 1,
   4541  STARTUP_PREF_RESTORE_SESSION: 3,
   4542 
   4543  // Convenience & Performance Shortcuts
   4544 
   4545  get _list() {
   4546    delete this._list;
   4547    return (this._list = document.getElementById("handlersView"));
   4548  },
   4549 
   4550  get _filter() {
   4551    delete this._filter;
   4552    return (this._filter = document.getElementById("filter"));
   4553  },
   4554 
   4555  /**
   4556   * Initialization of gMainPane.
   4557   */
   4558  init() {
   4559    /**
   4560     * @param {string} aId
   4561     * @param {string} aEventType
   4562     * @param {(ev: Event) => void} aCallback
   4563     */
   4564    function setEventListener(aId, aEventType, aCallback) {
   4565      document
   4566        .getElementById(aId)
   4567        .addEventListener(aEventType, aCallback.bind(gMainPane));
   4568    }
   4569 
   4570    this.displayUseSystemLocale();
   4571    this.updateProxySettingsUI();
   4572    initializeProxyUI(gMainPane);
   4573 
   4574    if (Services.prefs.getBoolPref("intl.multilingual.enabled")) {
   4575      gMainPane.initPrimaryBrowserLanguageUI();
   4576    }
   4577 
   4578    gMainPane.initTranslations();
   4579 
   4580    // Initialize settings groups from the config object.
   4581    initSettingGroup("appearance");
   4582    initSettingGroup("downloads");
   4583    initSettingGroup("drm");
   4584    initSettingGroup("contrast");
   4585    initSettingGroup("browsing");
   4586    initSettingGroup("zoom");
   4587    initSettingGroup("support");
   4588    initSettingGroup("translations");
   4589    initSettingGroup("performance");
   4590    initSettingGroup("startup");
   4591    initSettingGroup("importBrowserData");
   4592    initSettingGroup("networkProxy");
   4593    initSettingGroup("tabs");
   4594    initSettingGroup("profiles");
   4595    initSettingGroup("profilePane");
   4596 
   4597    setEventListener("manageBrowserLanguagesButton", "command", function () {
   4598      gMainPane.showBrowserLanguagesSubDialog({ search: false });
   4599    });
   4600    if (AppConstants.MOZ_UPDATER) {
   4601      // These elements are only compiled in when the updater is enabled
   4602      setEventListener("checkForUpdatesButton", "command", function () {
   4603        gAppUpdater.checkForUpdates();
   4604      });
   4605      setEventListener("downloadAndInstallButton", "command", function () {
   4606        gAppUpdater.startDownload();
   4607      });
   4608      setEventListener("updateButton", "command", function () {
   4609        gAppUpdater.buttonRestartAfterDownload();
   4610      });
   4611      setEventListener("checkForUpdatesButton2", "command", function () {
   4612        gAppUpdater.checkForUpdates();
   4613      });
   4614      setEventListener("checkForUpdatesButton3", "command", function () {
   4615        gAppUpdater.checkForUpdates();
   4616      });
   4617      setEventListener("checkForUpdatesButton4", "command", function () {
   4618        gAppUpdater.checkForUpdates();
   4619      });
   4620    }
   4621 
   4622    // setEventListener("chooseLanguage", "command", gMainPane.showLanguages);
   4623    {
   4624      const spoofEnglish = document.getElementById("spoofEnglish");
   4625      const kPrefSpoofEnglish = "privacy.spoof_english";
   4626      const preference = Preferences.add({
   4627        id: kPrefSpoofEnglish,
   4628        type: "int",
   4629      });
   4630      const spoofEnglishChanged = () => {
   4631        spoofEnglish.checked = preference.value == 2;
   4632      };
   4633      spoofEnglishChanged();
   4634      preference.on("change", spoofEnglishChanged);
   4635      setEventListener("spoofEnglish", "command", () => {
   4636        preference.value = spoofEnglish.checked ? 2 : 1;
   4637      });
   4638    }
   4639    // TODO (Bug 1817084) Remove this code when we disable the extension
   4640    setEventListener(
   4641      "fxtranslateButton",
   4642      "command",
   4643      gMainPane.showTranslationExceptions
   4644    );
   4645    Preferences.get("font.language.group").on(
   4646      "change",
   4647      gMainPane._rebuildFonts.bind(gMainPane)
   4648    );
   4649    setEventListener("advancedFonts", "command", gMainPane.configureFonts);
   4650 
   4651    document
   4652      .getElementById("browserLayoutShowSidebar")
   4653      .addEventListener(
   4654        "command",
   4655        gMainPane.onShowSidebarCommand.bind(gMainPane),
   4656        { capture: true }
   4657      );
   4658 
   4659    document
   4660      .getElementById("migrationWizardDialog")
   4661      .addEventListener("MigrationWizard:Close", function (e) {
   4662        e.currentTarget.close();
   4663      });
   4664 
   4665    // Initializes the fonts dropdowns displayed in this pane.
   4666    this._rebuildFonts();
   4667 
   4668    // Firefox Translations settings panel
   4669    // TODO (Bug 1817084) Remove this code when we disable the extension
   4670    const fxtranslationsDisabledPrefName = "extensions.translations.disabled";
   4671    if (!Services.prefs.getBoolPref(fxtranslationsDisabledPrefName, true)) {
   4672      let fxtranslationRow = document.getElementById("fxtranslationsBox");
   4673      fxtranslationRow.hidden = false;
   4674    }
   4675 
   4676    // Initialize the Firefox Updates section.
   4677    let version = AppConstants.BASE_BROWSER_VERSION;
   4678 
   4679    // Base Browser and derivatives: do not include the build ID in our alphas,
   4680    // since they are not actually related to the build date.
   4681 
   4682    // Append "(32-bit)" or "(64-bit)" build architecture to the version number:
   4683    let bundle = Services.strings.createBundle(
   4684      "chrome://browser/locale/browser.properties"
   4685    );
   4686    let archResource = Services.appinfo.is64Bit
   4687      ? "aboutDialog.architecture.sixtyFourBit"
   4688      : "aboutDialog.architecture.thirtyTwoBit";
   4689    let arch = bundle.GetStringFromName(archResource);
   4690    version += ` (${arch})`;
   4691 
   4692    document.l10n.setAttributes(
   4693      document.getElementById("updateAppInfo"),
   4694      "update-application-version",
   4695      { version }
   4696    );
   4697 
   4698    // Show a release notes link if we have a URL.
   4699    let relNotesLink = document.getElementById("releasenotes");
   4700    let relNotesPrefType = Services.prefs.getPrefType("app.releaseNotesURL");
   4701    if (relNotesPrefType != Services.prefs.PREF_INVALID) {
   4702      let relNotesURL = Services.urlFormatter.formatURLPref(
   4703        "app.releaseNotesURL"
   4704      );
   4705      if (relNotesURL != "about:blank") {
   4706        relNotesLink.href = relNotesURL;
   4707        relNotesLink.hidden = false;
   4708      }
   4709    }
   4710 
   4711    let defaults = Services.prefs.getDefaultBranch(null);
   4712    let distroId = defaults.getCharPref("distribution.id", "");
   4713    if (distroId) {
   4714      let distroString = distroId;
   4715 
   4716      let distroVersion = defaults.getCharPref("distribution.version", "");
   4717      if (distroVersion) {
   4718        distroString += " - " + distroVersion;
   4719      }
   4720 
   4721      let distroIdField = document.getElementById("distributionId");
   4722      distroIdField.value = distroString;
   4723      distroIdField.hidden = false;
   4724 
   4725      let distroAbout = defaults.getStringPref("distribution.about", "");
   4726      if (distroAbout) {
   4727        let distroField = document.getElementById("distribution");
   4728        distroField.value = distroAbout;
   4729        distroField.hidden = false;
   4730      }
   4731    }
   4732 
   4733    if (AppConstants.MOZ_UPDATER) {
   4734      gAppUpdater = new appUpdater();
   4735      setEventListener("showUpdateHistory", "command", gMainPane.showUpdates);
   4736 
   4737      let updateDisabled =
   4738        Services.policies && !Services.policies.isAllowed("appUpdate");
   4739 
   4740      if (gIsPackagedApp) {
   4741        // When we're running inside an app package, there's no point in
   4742        // displaying any update content here, and it would get confusing if we
   4743        // did, because our updater is not enabled.
   4744        // We can't rely on the hidden attribute for the toplevel elements,
   4745        // because of the pane hiding/showing code interfering.
   4746        document
   4747          .getElementById("updatesCategory")
   4748          .setAttribute("style", "display: none !important");
   4749        document
   4750          .getElementById("updateApp")
   4751          .setAttribute("style", "display: none !important");
   4752      } else if (
   4753        updateDisabled ||
   4754        UpdateUtils.appUpdateAutoSettingIsLocked() ||
   4755        gApplicationUpdateService.manualUpdateOnly
   4756      ) {
   4757        document.getElementById("updateAllowDescription").hidden = true;
   4758        document.getElementById("updateSettingsContainer").hidden = true;
   4759      } else {
   4760        // Start with no option selected since we are still reading the value
   4761        document.getElementById("autoDesktop").removeAttribute("selected");
   4762        document.getElementById("manualDesktop").removeAttribute("selected");
   4763        // Start reading the correct value from the disk
   4764        this.readUpdateAutoPref();
   4765        setEventListener("updateRadioGroup", "command", event => {
   4766          if (event.target.id == "backgroundUpdate") {
   4767            this.writeBackgroundUpdatePref();
   4768          } else {
   4769            this.writeUpdateAutoPref();
   4770          }
   4771        });
   4772        if (this.isBackgroundUpdateUIAvailable()) {
   4773          document.getElementById("backgroundUpdate").hidden = false;
   4774          // Start reading the background update pref's value from the disk.
   4775          this.readBackgroundUpdatePref();
   4776        }
   4777      }
   4778 
   4779      if (AppConstants.platform == "win") {
   4780        // On Windows, the Application Update setting is an installation-
   4781        // specific preference, not a profile-specific one. Show a warning to
   4782        // inform users of this.
   4783        let updateContainer = document.getElementById(
   4784          "updateSettingsContainer"
   4785        );
   4786        updateContainer.classList.add("updateSettingCrossUserWarningContainer");
   4787        document.getElementById("updateSettingCrossUserWarningDesc").hidden =
   4788          false;
   4789      }
   4790    }
   4791 
   4792    // Initilize Application section.
   4793 
   4794    // Observe preferences that influence what we display so we can rebuild
   4795    // the view when they change.
   4796    Services.obs.addObserver(this, AUTO_UPDATE_CHANGED_TOPIC);
   4797    Services.obs.addObserver(this, BACKGROUND_UPDATE_CHANGED_TOPIC);
   4798 
   4799    setEventListener("filter", "MozInputSearch:search", gMainPane.filter);
   4800    setEventListener("typeColumn", "click", gMainPane.sort);
   4801    setEventListener("actionColumn", "click", gMainPane.sort);
   4802 
   4803    // Listen for window unload so we can remove our preference observers.
   4804    window.addEventListener("unload", this);
   4805 
   4806    // Figure out how we should be sorting the list.  We persist sort settings
   4807    // across sessions, so we can't assume the default sort column/direction.
   4808    // XXX should we be using the XUL sort service instead?
   4809    if (document.getElementById("actionColumn").hasAttribute("sortDirection")) {
   4810      this._sortColumn = document.getElementById("actionColumn");
   4811      // The typeColumn element always has a sortDirection attribute,
   4812      // either because it was persisted or because the default value
   4813      // from the xul file was used.  If we are sorting on the other
   4814      // column, we should remove it.
   4815      document.getElementById("typeColumn").removeAttribute("sortDirection");
   4816    } else {
   4817      this._sortColumn = document.getElementById("typeColumn");
   4818    }
   4819 
   4820    gLetterboxingPrefs.init();
   4821 
   4822    // Notify observers that the UI is now ready
   4823    Services.obs.notifyObservers(window, "main-pane-loaded");
   4824 
   4825    Preferences.addSyncFromPrefListener(
   4826      document.getElementById("defaultFont"),
   4827      element => FontBuilder.readFontSelection(element)
   4828    );
   4829    Preferences.addSyncFromPrefListener(
   4830      document.getElementById("checkSpelling"),
   4831      () => this.readCheckSpelling()
   4832    );
   4833    Preferences.addSyncToPrefListener(
   4834      document.getElementById("checkSpelling"),
   4835      () => this.writeCheckSpelling()
   4836    );
   4837    this.setInitialized();
   4838  },
   4839 
   4840  preInit() {
   4841    promiseLoadHandlersList = new Promise((resolve, reject) => {
   4842      // Load the data and build the list of handlers for applications pane.
   4843      // By doing this after pageshow, we ensure it doesn't delay painting
   4844      // of the preferences page.
   4845      window.addEventListener(
   4846        "pageshow",
   4847        async () => {
   4848          await this.initialized;
   4849          try {
   4850            this._initListEventHandlers();
   4851            this._loadData();
   4852            await this._rebuildVisibleTypes();
   4853            await this._rebuildView();
   4854            await this._sortListView();
   4855            resolve();
   4856          } catch (ex) {
   4857            reject(ex);
   4858          }
   4859        },
   4860        { once: true }
   4861      );
   4862    });
   4863  },
   4864 
   4865  handleSubcategory(subcategory) {
   4866    if (Services.policies && !Services.policies.isAllowed("profileImport")) {
   4867      return false;
   4868    }
   4869    if (subcategory == "migrate") {
   4870      this.showMigrationWizardDialog();
   4871      return true;
   4872    }
   4873 
   4874    if (subcategory == "migrate-autoclose") {
   4875      this.showMigrationWizardDialog({ closeTabWhenDone: true });
   4876    }
   4877 
   4878    return false;
   4879  },
   4880 
   4881  /**
   4882   * Handle toggling the "Show sidebar" checkbox to allow SidebarController to know the
   4883   * origin of this change.
   4884   */
   4885  onShowSidebarCommand(event) {
   4886    // Note: We useCapture so while the checkbox' checked property is already updated,
   4887    // the pref value has not yet been changed
   4888    const willEnable = event.target.checked;
   4889    if (willEnable) {
   4890      window.browsingContext.topChromeWindow.SidebarController?.enabledViaSettings(
   4891        true
   4892      );
   4893    }
   4894  },
   4895 
   4896  // CONTAINERS
   4897 
   4898  /*
   4899   * preferences:
   4900   *
   4901   * privacy.userContext.enabled
   4902   * - true if containers is enabled
   4903   */
   4904 
   4905  async onGetStarted() {
   4906    if (!AppConstants.MOZ_DEV_EDITION) {
   4907      return;
   4908    }
   4909    const win = Services.wm.getMostRecentWindow("navigator:browser");
   4910    if (!win) {
   4911      return;
   4912    }
   4913    const user = await fxAccounts.getSignedInUser();
   4914    if (user) {
   4915      // We have a user, open Sync preferences in the same tab
   4916      win.openTrustedLinkIn("about:preferences#sync", "current");
   4917      return;
   4918    }
   4919    if (!(await FxAccounts.canConnectAccount())) {
   4920      return;
   4921    }
   4922    let url =
   4923      await FxAccounts.config.promiseConnectAccountURI("dev-edition-setup");
   4924    let accountsTab = win.gBrowser.addWebTab(url);
   4925    win.gBrowser.selectedTab = accountsTab;
   4926  },
   4927 
   4928  // HOME PAGE
   4929  /*
   4930   * Preferences:
   4931   *
   4932   * browser.startup.page
   4933   * - what page(s) to show when the user starts the application, as an integer:
   4934   *
   4935   *     0: a blank page (DEPRECATED - this can be set via browser.startup.homepage)
   4936   *     1: the home page (as set by the browser.startup.homepage pref)
   4937   *     2: the last page the user visited (DEPRECATED)
   4938   *     3: windows and tabs from the last session (a.k.a. session restore)
   4939   *
   4940   *   The deprecated option is not exposed in UI; however, if the user has it
   4941   *   selected and doesn't change the UI for this preference, the deprecated
   4942   *   option is preserved.
   4943   */
   4944 
   4945  /**
   4946   * Utility function to enable/disable the button specified by aButtonID based
   4947   * on the value of the Boolean preference specified by aPreferenceID.
   4948   */
   4949  updateButtons(aButtonID, aPreferenceID) {
   4950    var button = document.getElementById(aButtonID);
   4951    var preference = Preferences.get(aPreferenceID);
   4952    button.disabled = !preference.value;
   4953    return undefined;
   4954  },
   4955 
   4956  /**
   4957   * Initialize the translations view.
   4958   */
   4959  async initTranslations() {
   4960    if (!Services.prefs.getBoolPref("browser.translations.enable")) {
   4961      return;
   4962    }
   4963 
   4964    /**
   4965     * Which phase a language download is in.
   4966     *
   4967     * @typedef {"downloaded" | "loading" | "uninstalled"} DownloadPhase
   4968     */
   4969 
   4970    // Immediately show the group so that the async load of the component does
   4971    // not cause the layout to jump. The group will be empty initially.
   4972    document.getElementById("translationsGroup").hidden = false;
   4973 
   4974    class TranslationsState {
   4975      /**
   4976       * The fully initialized state.
   4977       *
   4978       * @param {object} supportedLanguages
   4979       * @param {Array<{ langTag: string, displayName: string}>} languageList
   4980       * @param {Map<string, DownloadPhase>} downloadPhases
   4981       */
   4982      constructor(supportedLanguages, languageList, downloadPhases) {
   4983        this.supportedLanguages = supportedLanguages;
   4984        this.languageList = languageList;
   4985        this.downloadPhases = downloadPhases;
   4986      }
   4987 
   4988      /**
   4989       * Handles all of the async initialization logic.
   4990       */
   4991      static async create() {
   4992        const supportedLanguages =
   4993          await TranslationsParent.getSupportedLanguages();
   4994        const languageList =
   4995          TranslationsParent.getLanguageList(supportedLanguages);
   4996        const downloadPhases =
   4997          await TranslationsState.createDownloadPhases(languageList);
   4998 
   4999        if (supportedLanguages.languagePairs.length === 0) {
   5000          throw new Error(
   5001            "The supported languages list was empty. RemoteSettings may not be available at the moment."
   5002          );
   5003        }
   5004 
   5005        return new TranslationsState(
   5006          supportedLanguages,
   5007          languageList,
   5008          downloadPhases
   5009        );
   5010      }
   5011 
   5012      /**
   5013       * Determine the download phase of each language file.
   5014       *
   5015       * @param {Array<{ langTag: string, displayName: string}>} languageList
   5016       * @returns {Promise<Map<string, DownloadPhase>>} Map the language tag to whether it is downloaded.
   5017       */
   5018      static async createDownloadPhases(languageList) {
   5019        const downloadPhases = new Map();
   5020        for (const { langTag } of languageList) {
   5021          downloadPhases.set(
   5022            langTag,
   5023            (await TranslationsParent.hasAllFilesForLanguage(langTag))
   5024              ? "downloaded"
   5025              : "uninstalled"
   5026          );
   5027        }
   5028        return downloadPhases;
   5029      }
   5030    }
   5031 
   5032    class TranslationsView {
   5033      /** @type {Map<string, XULButton>} */
   5034      deleteButtons = new Map();
   5035      /** @type {Map<string, XULButton>} */
   5036      downloadButtons = new Map();
   5037 
   5038      /**
   5039       * @param {TranslationsState} state
   5040       */
   5041      constructor(state) {
   5042        this.state = state;
   5043        this.elements = {
   5044          settingsButton: document.getElementById(
   5045            "translations-manage-settings-button"
   5046          ),
   5047          installList: document.getElementById(
   5048            "translations-manage-install-list"
   5049          ),
   5050          installAll: document.getElementById(
   5051            "translations-manage-install-all"
   5052          ),
   5053          deleteAll: document.getElementById("translations-manage-delete-all"),
   5054          error: document.getElementById("translations-manage-error"),
   5055        };
   5056        this.setup();
   5057      }
   5058 
   5059      setup() {
   5060        this.buildLanguageList();
   5061 
   5062        this.elements.settingsButton.addEventListener(
   5063          "command",
   5064          gMainPane.showTranslationsSettings
   5065        );
   5066        this.elements.installAll.addEventListener(
   5067          "command",
   5068          this.handleInstallAll
   5069        );
   5070        this.elements.deleteAll.addEventListener(
   5071          "command",
   5072          this.handleDeleteAll
   5073        );
   5074 
   5075        Services.obs.addObserver(this, "intl:app-locales-changed");
   5076      }
   5077 
   5078      destroy() {
   5079        Services.obs.removeObserver(this, "intl:app-locales-changed");
   5080      }
   5081 
   5082      handleInstallAll = async () => {
   5083        this.hideError();
   5084        this.disableButtons(true);
   5085        try {
   5086          await TranslationsParent.downloadAllFiles();
   5087          this.markAllDownloadPhases("downloaded");
   5088        } catch (error) {
   5089          TranslationsView.showError(
   5090            "translations-manage-error-download",
   5091            error
   5092          );
   5093          await this.reloadDownloadPhases();
   5094          this.updateAllButtons();
   5095        }
   5096        this.disableButtons(false);
   5097      };
   5098 
   5099      handleDeleteAll = async () => {
   5100        this.hideError();
   5101        this.disableButtons(true);
   5102        try {
   5103          await TranslationsParent.deleteAllLanguageFiles();
   5104          this.markAllDownloadPhases("uninstalled");
   5105        } catch (error) {
   5106          TranslationsView.showError("translations-manage-error-remove", error);
   5107          // The download phases are invalidated with the error and must be reloaded.
   5108          await this.reloadDownloadPhases();
   5109          console.error(error);
   5110        }
   5111        this.disableButtons(false);
   5112      };
   5113 
   5114      /**
   5115       * @param {string} langTag
   5116       * @returns {Function}
   5117       */
   5118      getDownloadButtonHandler(langTag) {
   5119        return async () => {
   5120          this.hideError();
   5121          this.updateDownloadPhase(langTag, "loading");
   5122          try {
   5123            await TranslationsParent.downloadLanguageFiles(langTag);
   5124            this.updateDownloadPhase(langTag, "downloaded");
   5125          } catch (error) {
   5126            TranslationsView.showError(
   5127              "translations-manage-error-download",
   5128              error
   5129            );
   5130            this.updateDownloadPhase(langTag, "uninstalled");
   5131          }
   5132        };
   5133      }
   5134 
   5135      /**
   5136       * @param {string} langTag
   5137       * @returns {Function}
   5138       */
   5139      getDeleteButtonHandler(langTag) {
   5140        return async () => {
   5141          this.hideError();
   5142          this.updateDownloadPhase(langTag, "loading");
   5143          try {
   5144            await TranslationsParent.deleteLanguageFiles(langTag);
   5145            this.updateDownloadPhase(langTag, "uninstalled");
   5146          } catch (error) {
   5147            TranslationsView.showError(
   5148              "translations-manage-error-remove",
   5149              error
   5150            );
   5151            // The download phases are invalidated with the error and must be reloaded.
   5152            await this.reloadDownloadPhases();
   5153          }
   5154        };
   5155      }
   5156 
   5157      buildLanguageList() {
   5158        const listFragment = document.createDocumentFragment();
   5159 
   5160        for (const { langTag, displayName } of this.state.languageList) {
   5161          const hboxRow = document.createXULElement("hbox");
   5162          hboxRow.classList.add("translations-manage-language");
   5163          hboxRow.setAttribute("data-lang-tag", langTag);
   5164 
   5165          const languageLabel = document.createXULElement("label");
   5166          languageLabel.textContent = displayName; // The display name is already localized.
   5167 
   5168          const downloadButton = document.createXULElement("button");
   5169          const deleteButton = document.createXULElement("button");
   5170 
   5171          downloadButton.addEventListener(
   5172            "command",
   5173            this.getDownloadButtonHandler(langTag)
   5174          );
   5175          deleteButton.addEventListener(
   5176            "command",
   5177            this.getDeleteButtonHandler(langTag)
   5178          );
   5179 
   5180          document.l10n.setAttributes(
   5181            downloadButton,
   5182            "translations-manage-language-download-button"
   5183          );
   5184          document.l10n.setAttributes(
   5185            deleteButton,
   5186            "translations-manage-language-remove-button"
   5187          );
   5188 
   5189          downloadButton.hidden = true;
   5190          deleteButton.hidden = true;
   5191 
   5192          this.deleteButtons.set(langTag, deleteButton);
   5193          this.downloadButtons.set(langTag, downloadButton);
   5194 
   5195          hboxRow.appendChild(languageLabel);
   5196          hboxRow.appendChild(downloadButton);
   5197          hboxRow.appendChild(deleteButton);
   5198          listFragment.appendChild(hboxRow);
   5199        }
   5200        this.updateAllButtons();
   5201        this.elements.installList.appendChild(listFragment);
   5202      }
   5203 
   5204      /**
   5205       * Update the DownloadPhase for a single langTag.
   5206       *
   5207       * @param {string} langTag
   5208       * @param {DownloadPhase} downloadPhase
   5209       */
   5210      updateDownloadPhase(langTag, downloadPhase) {
   5211        this.state.downloadPhases.set(langTag, downloadPhase);
   5212        this.updateButton(langTag, downloadPhase);
   5213        this.updateHeaderButtons();
   5214      }
   5215 
   5216      /**
   5217       * Recreates the download map when the state is invalidated.
   5218       */
   5219      async reloadDownloadPhases() {
   5220        this.state.downloadPhases =
   5221          await TranslationsState.createDownloadPhases(this.state.languageList);
   5222        this.updateAllButtons();
   5223      }
   5224 
   5225      /**
   5226       * Set all the downloads.
   5227       *
   5228       * @param {DownloadPhase} downloadPhase
   5229       */
   5230      markAllDownloadPhases(downloadPhase) {
   5231        const { downloadPhases } = this.state;
   5232        for (const key of downloadPhases.keys()) {
   5233          downloadPhases.set(key, downloadPhase);
   5234        }
   5235        this.updateAllButtons();
   5236      }
   5237 
   5238      /**
   5239       * If all languages are downloaded, or no languages are downloaded then
   5240       * the visibility of the buttons need to change.
   5241       */
   5242      updateHeaderButtons() {
   5243        let allDownloaded = true;
   5244        let allUninstalled = true;
   5245        for (const downloadPhase of this.state.downloadPhases.values()) {
   5246          if (downloadPhase === "loading") {
   5247            // Don't count loading towards this calculation.
   5248            continue;
   5249          }
   5250          allDownloaded &&= downloadPhase === "downloaded";
   5251          allUninstalled &&= downloadPhase === "uninstalled";
   5252        }
   5253 
   5254        this.elements.installAll.hidden = allDownloaded;
   5255        this.elements.deleteAll.hidden = allUninstalled;
   5256      }
   5257 
   5258      /**
   5259       * Update the buttons according to their download state.
   5260       */
   5261      updateAllButtons() {
   5262        this.updateHeaderButtons();
   5263        for (const [langTag, downloadPhase] of this.state.downloadPhases) {
   5264          this.updateButton(langTag, downloadPhase);
   5265        }
   5266      }
   5267 
   5268      /**
   5269       * @param {string} langTag
   5270       * @param {DownloadPhase} downloadPhase
   5271       */
   5272      updateButton(langTag, downloadPhase) {
   5273        const downloadButton = this.downloadButtons.get(langTag);
   5274        const deleteButton = this.deleteButtons.get(langTag);
   5275        switch (downloadPhase) {
   5276          case "downloaded":
   5277            downloadButton.hidden = true;
   5278            deleteButton.hidden = false;
   5279            downloadButton.removeAttribute("disabled");
   5280            break;
   5281          case "uninstalled":
   5282            downloadButton.hidden = false;
   5283            deleteButton.hidden = true;
   5284            downloadButton.removeAttribute("disabled");
   5285            break;
   5286          case "loading":
   5287            downloadButton.hidden = false;
   5288            deleteButton.hidden = true;
   5289            downloadButton.setAttribute("disabled", "true");
   5290            break;
   5291        }
   5292      }
   5293 
   5294      /**
   5295       * @param {boolean} isDisabled
   5296       */
   5297      disableButtons(isDisabled) {
   5298        this.elements.installAll.disabled = isDisabled;
   5299        this.elements.deleteAll.disabled = isDisabled;
   5300        for (const button of this.downloadButtons.values()) {
   5301          button.disabled = isDisabled;
   5302        }
   5303        for (const button of this.deleteButtons.values()) {
   5304          button.disabled = isDisabled;
   5305        }
   5306      }
   5307 
   5308      /**
   5309       * This method is static in case an error happens during the creation of the
   5310       * TranslationsState.
   5311       *
   5312       * @param {string} l10nId
   5313       * @param {Error} error
   5314       */
   5315      static showError(l10nId, error) {
   5316        console.error(error);
   5317        const errorMessage = document.getElementById(
   5318          "translations-manage-error"
   5319        );
   5320        errorMessage.hidden = false;
   5321        document.l10n.setAttributes(errorMessage, l10nId);
   5322      }
   5323 
   5324      hideError() {
   5325        this.elements.error.hidden = true;
   5326      }
   5327 
   5328      observe(_subject, topic, _data) {
   5329        if (topic === "intl:app-locales-changed") {
   5330          this.refreshLanguageListDisplay();
   5331        }
   5332      }
   5333 
   5334      refreshLanguageListDisplay() {
   5335        try {
   5336          const languageDisplayNames =
   5337            TranslationsParent.createLanguageDisplayNames();
   5338 
   5339          for (const row of this.elements.installList.children) {
   5340            const rowLangTag = row.getAttribute("data-lang-tag");
   5341            if (!rowLangTag) {
   5342              continue;
   5343            }
   5344 
   5345            const label = row.querySelector("label");
   5346            if (label) {
   5347              const newDisplayName = languageDisplayNames.of(rowLangTag);
   5348              if (label.textContent !== newDisplayName) {
   5349                label.textContent = newDisplayName;
   5350              }
   5351            }
   5352          }
   5353        } catch (error) {
   5354          console.error(error);
   5355        }
   5356      }
   5357    }
   5358 
   5359    TranslationsState.create().then(
   5360      state => {
   5361        this._translationsView = new TranslationsView(state);
   5362      },
   5363      error => {
   5364        // This error can happen when a user is not connected to the internet, or
   5365        // RemoteSettings is down for some reason.
   5366        TranslationsView.showError("translations-manage-error-list", error);
   5367      }
   5368    );
   5369  },
   5370 
   5371  initPrimaryBrowserLanguageUI() {
   5372    // This will register the "command" listener.
   5373    let menulist = document.getElementById("primaryBrowserLocale");
   5374    new SelectionChangedMenulist(menulist, event => {
   5375      gMainPane.onPrimaryBrowserLanguageMenuChange(event);
   5376    });
   5377 
   5378    gMainPane.updatePrimaryBrowserLanguageUI(Services.locale.appLocaleAsBCP47);
   5379  },
   5380 
   5381  /**
   5382   * Update the available list of locales and select the locale that the user
   5383   * is "selecting". This could be the currently requested locale or a locale
   5384   * that the user would like to switch to after confirmation.
   5385   *
   5386   * @param {string} selected - The selected BCP 47 locale.
   5387   */
   5388  async updatePrimaryBrowserLanguageUI(selected) {
   5389    let available = await LangPackMatcher.getAvailableLocales();
   5390    let localeNames = Services.intl.getLocaleDisplayNames(
   5391      undefined,
   5392      available,
   5393      { preferNative: true }
   5394    );
   5395    let locales = available.map((code, i) => {
   5396      let name = localeNames[i].replace(/\s*\(.+\)$/g, "");
   5397      if (code === "ja-JP-macos") {
   5398        // Mozilla codebases handle Japanese in macOS in different ways,
   5399        // sometimes they call it ja-JP-mac and sometimes they call it
   5400        // ja-JP-macos. The former is translated to Japanese when specifying
   5401        // preferNative to true, the latter is not. Since seeing ja-JP-macos
   5402        // would be confusing anyway, we treat it as a special case.
   5403        // See tor-browser#41372 and Bug 1726586.
   5404        name =
   5405          Services.intl.getLocaleDisplayNames(undefined, ["ja"], {
   5406            preferNative: true,
   5407          })[0] + " (ja)";
   5408      } else {
   5409        name += ` (${code})`;
   5410      }
   5411      return {
   5412        code,
   5413        name,
   5414      };
   5415    });
   5416    // tor-browser#42335: Sort language codes independently from the locale,
   5417    // so do not use localeCompare.
   5418    locales.sort((a, b) => a.code > b.code);
   5419 
   5420    let fragment = document.createDocumentFragment();
   5421    for (let { code, name } of locales) {
   5422      let menuitem = document.createXULElement("menuitem");
   5423      menuitem.setAttribute("value", code);
   5424      menuitem.setAttribute("label", name);
   5425      fragment.appendChild(menuitem);
   5426    }
   5427 
   5428    // Add an option to search for more languages if downloading is supported.
   5429    if (Services.prefs.getBoolPref("intl.multilingual.downloadEnabled")) {
   5430      let menuitem = document.createXULElement("menuitem");
   5431      menuitem.id = "primaryBrowserLocaleSearch";
   5432      menuitem.setAttribute(
   5433        "label",
   5434        await document.l10n.formatValue("browser-languages-search")
   5435      );
   5436      menuitem.setAttribute("value", "search");
   5437      fragment.appendChild(menuitem);
   5438    }
   5439 
   5440    let menulist = document.getElementById("primaryBrowserLocale");
   5441    let menupopup = menulist.querySelector("menupopup");
   5442    menupopup.textContent = "";
   5443    menupopup.appendChild(fragment);
   5444    menulist.value = selected;
   5445 
   5446    document.getElementById("browserLanguagesBox").hidden = false;
   5447  },
   5448 
   5449  /* Show the confirmation message bar to allow a restart into the new locales. */
   5450  async showConfirmLanguageChangeMessageBar(locales) {
   5451    let messageBar = document.getElementById("confirmBrowserLanguage");
   5452 
   5453    // Get the bundle for the new locale.
   5454    let newBundle = getBundleForLocales(locales);
   5455 
   5456    // Find the messages and labels.
   5457    let messages = await Promise.all(
   5458      [newBundle, document.l10n].map(async bundle =>
   5459        bundle.formatValue("confirm-browser-language-change-description")
   5460      )
   5461    );
   5462    let buttonLabels = await Promise.all(
   5463      [newBundle, document.l10n].map(async bundle =>
   5464        bundle.formatValue("confirm-browser-language-change-button")
   5465      )
   5466    );
   5467 
   5468    // If both the message and label are the same, just include one row.
   5469    if (messages[0] == messages[1] && buttonLabels[0] == buttonLabels[1]) {
   5470      messages.pop();
   5471      buttonLabels.pop();
   5472    }
   5473 
   5474    let contentContainer = messageBar.querySelector(
   5475      ".message-bar-content-container"
   5476    );
   5477    contentContainer.textContent = "";
   5478 
   5479    for (let i = 0; i < messages.length; i++) {
   5480      let messageContainer = document.createXULElement("hbox");
   5481      messageContainer.classList.add("message-bar-content");
   5482      messageContainer.style.flex = "1 50%";
   5483      messageContainer.setAttribute("align", "center");
   5484 
   5485      let description = document.createXULElement("description");
   5486      description.classList.add("message-bar-description");
   5487 
   5488      if (i == 0 && Services.intl.getScriptDirection(locales[0]) === "rtl") {
   5489        description.classList.add("rtl-locale");
   5490      }
   5491      description.setAttribute("flex", "1");
   5492      description.textContent = messages[i];
   5493      messageContainer.appendChild(description);
   5494 
   5495      let button = document.createXULElement("button");
   5496      button.addEventListener(
   5497        "command",
   5498        gMainPane.confirmBrowserLanguageChange
   5499      );
   5500      button.classList.add("message-bar-button");
   5501      button.setAttribute("locales", locales.join(","));
   5502      button.setAttribute("label", buttonLabels[i]);
   5503      messageContainer.appendChild(button);
   5504 
   5505      contentContainer.appendChild(messageContainer);
   5506    }
   5507 
   5508    messageBar.hidden = false;
   5509    gMainPane.selectedLocalesForRestart = locales;
   5510  },
   5511 
   5512  hideConfirmLanguageChangeMessageBar() {
   5513    let messageBar = document.getElementById("confirmBrowserLanguage");
   5514    messageBar.hidden = true;
   5515    let contentContainer = messageBar.querySelector(
   5516      ".message-bar-content-container"
   5517    );
   5518    contentContainer.textContent = "";
   5519    gMainPane.requestingLocales = null;
   5520  },
   5521 
   5522  /* Confirm the locale change and restart the browser in the new locale. */
   5523  confirmBrowserLanguageChange(event) {
   5524    let localesString = (event.target.getAttribute("locales") || "").trim();
   5525    if (!localesString || !localesString.length) {
   5526      return;
   5527    }
   5528    let locales = localesString.split(",");
   5529    Services.locale.requestedLocales = locales;
   5530 
   5531    // Record the change in telemetry before we restart.
   5532    gMainPane.recordBrowserLanguagesTelemetry("apply");
   5533 
   5534    // Restart with the new locale.
   5535    let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(
   5536      Ci.nsISupportsPRBool
   5537    );
   5538    Services.obs.notifyObservers(
   5539      cancelQuit,
   5540      "quit-application-requested",
   5541      "restart"
   5542    );
   5543    if (!cancelQuit.data) {
   5544      Services.startup.quit(
   5545        Services.startup.eAttemptQuit | Services.startup.eRestart
   5546      );
   5547    }
   5548  },
   5549 
   5550  /* Show or hide the confirm change message bar based on the new locale. */
   5551  onPrimaryBrowserLanguageMenuChange(event) {
   5552    let locale = event.target.value;
   5553 
   5554    if (locale == "search") {
   5555      gMainPane.showBrowserLanguagesSubDialog({ search: true });
   5556      return;
   5557    } else if (locale == Services.locale.appLocaleAsBCP47) {
   5558      this.hideConfirmLanguageChangeMessageBar();
   5559      return;
   5560    }
   5561 
   5562    let newLocales = Array.from(
   5563      new Set([locale, ...Services.locale.requestedLocales]).values()
   5564    );
   5565 
   5566    gMainPane.recordBrowserLanguagesTelemetry("reorder");
   5567 
   5568    switch (gMainPane.getLanguageSwitchTransitionType(newLocales)) {
   5569      case "requires-restart":
   5570        // Prepare to change the locales, as they were different.
   5571        gMainPane.showConfirmLanguageChangeMessageBar(newLocales);
   5572        gMainPane.updatePrimaryBrowserLanguageUI(newLocales[0]);
   5573        break;
   5574      case "live-reload":
   5575        Services.locale.requestedLocales = newLocales;
   5576        gMainPane.updatePrimaryBrowserLanguageUI(
   5577          Services.locale.appLocaleAsBCP47
   5578        );
   5579        gMainPane.hideConfirmLanguageChangeMessageBar();
   5580        break;
   5581      case "locales-match":
   5582        // They matched, so we can reset the UI.
   5583        gMainPane.updatePrimaryBrowserLanguageUI(
   5584          Services.locale.appLocaleAsBCP47
   5585        );
   5586        gMainPane.hideConfirmLanguageChangeMessageBar();
   5587        break;
   5588      default:
   5589        throw new Error("Unhandled transition type.");
   5590    }
   5591  },
   5592 
   5593  /**
   5594   *  Shows a subdialog containing the profile selector page.
   5595   */
   5596  manageProfiles() {
   5597    SelectableProfileService.maybeSetupDataStore().then(() => {
   5598      gSubDialog.open("about:profilemanager");
   5599    });
   5600  },
   5601 
   5602  /**
   5603   * Shows a dialog in which the preferred language for web content may be set.
   5604   */
   5605  showLanguages() {
   5606    gSubDialog.open(
   5607      "chrome://browser/content/preferences/dialogs/languages.xhtml"
   5608    );
   5609  },
   5610 
   5611  recordBrowserLanguagesTelemetry(method, value = null) {
   5612    Glean.intlUiBrowserLanguage[method + "Main"].record(
   5613      value ? { value } : undefined
   5614    );
   5615  },
   5616 
   5617  /**
   5618   * Open the browser languages sub dialog in either the normal mode, or search mode.
   5619   * The search mode is only available from the menu to change the primary browser
   5620   * language.
   5621   *
   5622   * @param {{ search: boolean }} search
   5623   */
   5624  showBrowserLanguagesSubDialog({ search }) {
   5625    // Record the telemetry event with an id to associate related actions.
   5626    let telemetryId = parseInt(
   5627      Services.telemetry.msSinceProcessStart(),
   5628      10
   5629    ).toString();
   5630    let method = search ? "search" : "manage";
   5631    gMainPane.recordBrowserLanguagesTelemetry(method, telemetryId);
   5632 
   5633    let opts = {
   5634      selectedLocalesForRestart: gMainPane.selectedLocalesForRestart,
   5635      search,
   5636      telemetryId,
   5637    };
   5638    gSubDialog.open(
   5639      "chrome://browser/content/preferences/dialogs/browserLanguages.xhtml",
   5640      { closingCallback: this.browserLanguagesClosed },
   5641      opts
   5642    );
   5643  },
   5644 
   5645  /**
   5646   * Determine the transition strategy for switching the locale based on prefs
   5647   * and the switched locales.
   5648   *
   5649   * @param {Array<string>} newLocales - List of BCP 47 locale identifiers.
   5650   * @returns {"locales-match" | "requires-restart" | "live-reload"}
   5651   */
   5652  getLanguageSwitchTransitionType(newLocales) {
   5653    const { appLocalesAsBCP47 } = Services.locale;
   5654    if (appLocalesAsBCP47.join(",") === newLocales.join(",")) {
   5655      // The selected locales match, the order matters.
   5656      return "locales-match";
   5657    }
   5658 
   5659    if (Services.prefs.getBoolPref("intl.multilingual.liveReload")) {
   5660      if (
   5661        Services.intl.getScriptDirection(newLocales[0]) !==
   5662          Services.intl.getScriptDirection(appLocalesAsBCP47[0]) &&
   5663        !Services.prefs.getBoolPref("intl.multilingual.liveReloadBidirectional")
   5664      ) {
   5665        // Bug 1750852: The directionality of the text changed, which requires a restart
   5666        // until the quality of the switch can be improved.
   5667        return "requires-restart";
   5668      }
   5669 
   5670      return "live-reload";
   5671    }
   5672 
   5673    return "requires-restart";
   5674  },
   5675 
   5676  /* Show or hide the confirm change message bar based on the updated ordering. */
   5677  browserLanguagesClosed() {
   5678    // When the subdialog is closed, settings are stored on gBrowserLanguagesDialog.
   5679    // The next time the dialog is opened, a new gBrowserLanguagesDialog is created.
   5680    let { selected } = this.gBrowserLanguagesDialog;
   5681 
   5682    this.gBrowserLanguagesDialog.recordTelemetry(
   5683      selected ? "accept" : "cancel"
   5684    );
   5685 
   5686    if (!selected) {
   5687      // No locales were selected. Cancel the operation.
   5688      return;
   5689    }
   5690 
   5691    // Track how often locale fallback order is changed.
   5692    // Drop the first locale and filter to only include the overlapping set
   5693    const prevLocales = Services.locale.requestedLocales.filter(
   5694      lc => selected.indexOf(lc) > 0
   5695    );
   5696    const newLocales = selected.filter(
   5697      (lc, i) => i > 0 && prevLocales.includes(lc)
   5698    );
   5699    if (prevLocales.some((lc, i) => newLocales[i] != lc)) {
   5700      this.gBrowserLanguagesDialog.recordTelemetry("setFallback");
   5701    }
   5702 
   5703    switch (gMainPane.getLanguageSwitchTransitionType(selected)) {
   5704      case "requires-restart":
   5705        gMainPane.showConfirmLanguageChangeMessageBar(selected);
   5706        gMainPane.updatePrimaryBrowserLanguageUI(selected[0]);
   5707        break;
   5708      case "live-reload":
   5709        Services.locale.requestedLocales = selected;
   5710 
   5711        gMainPane.updatePrimaryBrowserLanguageUI(
   5712          Services.locale.appLocaleAsBCP47
   5713        );
   5714        gMainPane.hideConfirmLanguageChangeMessageBar();
   5715        break;
   5716      case "locales-match":
   5717        // They matched, so we can reset the UI.
   5718        gMainPane.updatePrimaryBrowserLanguageUI(
   5719          Services.locale.appLocaleAsBCP47
   5720        );
   5721        gMainPane.hideConfirmLanguageChangeMessageBar();
   5722        break;
   5723      default:
   5724        throw new Error("Unhandled transition type.");
   5725    }
   5726  },
   5727 
   5728  displayUseSystemLocale() {
   5729    let appLocale = Services.locale.appLocaleAsBCP47;
   5730    let regionalPrefsLocales = Services.locale.regionalPrefsLocales;
   5731    if (!regionalPrefsLocales.length) {
   5732      return;
   5733    }
   5734    let systemLocale = regionalPrefsLocales[0];
   5735    let localeDisplayname = Services.intl.getLocaleDisplayNames(
   5736      undefined,
   5737      [systemLocale],
   5738      { preferNative: true }
   5739    );
   5740    if (!localeDisplayname.length) {
   5741      return;
   5742    }
   5743    let localeName = localeDisplayname[0];
   5744    if (appLocale.split("-u-")[0] != systemLocale.split("-u-")[0]) {
   5745      let checkbox = document.getElementById("useSystemLocale");
   5746      document.l10n.setAttributes(checkbox, "use-system-locale", {
   5747        localeName,
   5748      });
   5749      checkbox.hidden = false;
   5750    }
   5751  },
   5752 
   5753  /**
   5754   * Displays the translation exceptions dialog where specific site and language
   5755   * translation preferences can be set.
   5756   */
   5757  // TODO (Bug 1817084) Remove this code when we disable the extension
   5758  showTranslationExceptions() {
   5759    gSubDialog.open(
   5760      "chrome://browser/content/preferences/dialogs/translationExceptions.xhtml"
   5761    );
   5762  },
   5763 
   5764  showTranslationsSettings() {
   5765    gSubDialog.open(
   5766      "chrome://browser/content/preferences/dialogs/translations.xhtml"
   5767    );
   5768  },
   5769 
   5770  /**
   5771   * Displays the fonts dialog, where web page font names and sizes can be
   5772   * configured.
   5773   */
   5774  configureFonts() {
   5775    gSubDialog.open(
   5776      "chrome://browser/content/preferences/dialogs/fonts.xhtml",
   5777      { features: "resizable=no" }
   5778    );
   5779  },
   5780 
   5781  // NETWORK
   5782  /**
   5783   * Displays a dialog in which proxy settings may be changed.
   5784   */
   5785  showConnections() {
   5786    gSubDialog.open(
   5787      "chrome://browser/content/preferences/dialogs/connection.xhtml",
   5788      { closingCallback: this.updateProxySettingsUI.bind(this) }
   5789    );
   5790  },
   5791 
   5792  // Update the UI to show the proper description depending on whether an
   5793  // extension is in control or not.
   5794  async updateProxySettingsUI() {
   5795    let controllingExtension = await getControllingExtension(
   5796      PREF_SETTING_TYPE,
   5797      PROXY_KEY
   5798    );
   5799    let description = document.getElementById("connectionSettingsDescription");
   5800 
   5801    if (controllingExtension) {
   5802      setControllingExtensionDescription(
   5803        description,
   5804        controllingExtension,
   5805        "proxy.settings"
   5806      );
   5807    } else {
   5808      setControllingExtensionDescription(
   5809        description,
   5810        null,
   5811        "network-proxy-connection-description"
   5812      );
   5813    }
   5814  },
   5815 
   5816  // FONTS
   5817 
   5818  /**
   5819   * Populates the default font list in UI.
   5820   */
   5821  _rebuildFonts() {
   5822    var langGroup = Services.locale.fontLanguageGroup;
   5823    var isSerif = this._readDefaultFontTypeForLanguage(langGroup) == "serif";
   5824    this._selectDefaultLanguageGroup(langGroup, isSerif);
   5825  },
   5826 
   5827  /**
   5828   * Returns the type of the current default font for the language denoted by
   5829   * aLanguageGroup.
   5830   */
   5831  _readDefaultFontTypeForLanguage(aLanguageGroup) {
   5832    const kDefaultFontType = "font.default.%LANG%";
   5833    var defaultFontTypePref = kDefaultFontType.replace(
   5834      /%LANG%/,
   5835      aLanguageGroup
   5836    );
   5837    var preference = Preferences.get(defaultFontTypePref);
   5838    if (!preference) {
   5839      preference = Preferences.add({ id: defaultFontTypePref, type: "string" });
   5840      preference.on("change", gMainPane._rebuildFonts.bind(gMainPane));
   5841    }
   5842    return preference.value;
   5843  },
   5844 
   5845  _selectDefaultLanguageGroupPromise: Promise.resolve(),
   5846 
   5847  _selectDefaultLanguageGroup(aLanguageGroup, aIsSerif) {
   5848    this._selectDefaultLanguageGroupPromise = (async () => {
   5849      // Avoid overlapping language group selections by awaiting the resolution
   5850      // of the previous one.  We do this because this function is re-entrant,
   5851      // as inserting <preference> elements into the DOM sometimes triggers a call
   5852      // back into this function.  And since this function is also asynchronous,
   5853      // that call can enter this function before the previous run has completed,
   5854      // which would corrupt the font menulists.  Awaiting the previous call's
   5855      // resolution avoids that fate.
   5856      await this._selectDefaultLanguageGroupPromise;
   5857 
   5858      const kFontNameFmtSerif = "font.name.serif.%LANG%";
   5859      const kFontNameFmtSansSerif = "font.name.sans-serif.%LANG%";
   5860      const kFontNameListFmtSerif = "font.name-list.serif.%LANG%";
   5861      const kFontNameListFmtSansSerif = "font.name-list.sans-serif.%LANG%";
   5862      const kFontSizeFmtVariable = "font.size.variable.%LANG%";
   5863 
   5864      var prefs = [
   5865        {
   5866          format: aIsSerif ? kFontNameFmtSerif : kFontNameFmtSansSerif,
   5867          type: "fontname",
   5868          element: "defaultFont",
   5869          fonttype: aIsSerif ? "serif" : "sans-serif",
   5870        },
   5871        {
   5872          format: aIsSerif ? kFontNameListFmtSerif : kFontNameListFmtSansSerif,
   5873          type: "unichar",
   5874          element: null,
   5875          fonttype: aIsSerif ? "serif" : "sans-serif",
   5876        },
   5877        {
   5878          format: kFontSizeFmtVariable,
   5879          type: "int",
   5880          element: "defaultFontSize",
   5881          fonttype: null,
   5882        },
   5883      ];
   5884      for (var i = 0; i < prefs.length; ++i) {
   5885        var preference = Preferences.get(
   5886          prefs[i].format.replace(/%LANG%/, aLanguageGroup)
   5887        );
   5888        if (!preference) {
   5889          var name = prefs[i].format.replace(/%LANG%/, aLanguageGroup);
   5890          preference = Preferences.add({ id: name, type: prefs[i].type });
   5891        }
   5892 
   5893        if (!prefs[i].element) {
   5894          continue;
   5895        }
   5896 
   5897        var element = document.getElementById(prefs[i].element);
   5898        if (element) {
   5899          element.setAttribute("preference", preference.id);
   5900 
   5901          if (prefs[i].fonttype) {
   5902            await FontBuilder.buildFontList(
   5903              aLanguageGroup,
   5904              prefs[i].fonttype,
   5905              element
   5906            );
   5907          }
   5908 
   5909          preference.setElementValue(element);
   5910        }
   5911      }
   5912    })().catch(console.error);
   5913  },
   5914 
   5915  /**
   5916   * Displays the migration wizard dialog in an HTML dialog.
   5917   */
   5918  async showMigrationWizardDialog({ closeTabWhenDone = false } = {}) {
   5919    let migrationWizardDialog = document.getElementById(
   5920      "migrationWizardDialog"
   5921    );
   5922 
   5923    if (migrationWizardDialog.open) {
   5924      return;
   5925    }
   5926 
   5927    await customElements.whenDefined("migration-wizard");
   5928 
   5929    // If we've been opened before, remove the old wizard and insert a
   5930    // new one to put it back into its starting state.
   5931    if (!migrationWizardDialog.firstElementChild) {
   5932      let wizard = document.createElement("migration-wizard");
   5933      wizard.toggleAttribute("dialog-mode", true);
   5934      migrationWizardDialog.appendChild(wizard);
   5935    }
   5936    migrationWizardDialog.firstElementChild.requestState();
   5937 
   5938    migrationWizardDialog.addEventListener(
   5939      "close",
   5940      () => {
   5941        // Let others know that the wizard is closed -- potentially because of a
   5942        // user action within the dialog that dispatches "MigrationWizard:Close"
   5943        // but this also covers cases like hitting Escape.
   5944        Services.obs.notifyObservers(
   5945          migrationWizardDialog,
   5946          "MigrationWizard:Closed"
   5947        );
   5948        if (closeTabWhenDone) {
   5949          window.close();
   5950        }
   5951      },
   5952      { once: true }
   5953    );
   5954 
   5955    migrationWizardDialog.showModal();
   5956  },
   5957 
   5958  /**
   5959   * Stores the original value of the spellchecking preference to enable proper
   5960   * restoration if unchanged (since we're mapping a tristate onto a checkbox).
   5961   */
   5962  _storedSpellCheck: 0,
   5963 
   5964  /**
   5965   * Returns true if any spellchecking is enabled and false otherwise, caching
   5966   * the current value to enable proper pref restoration if the checkbox is
   5967   * never changed.
   5968   *
   5969   * layout.spellcheckDefault
   5970   * - an integer:
   5971   *     0  disables spellchecking
   5972   *     1  enables spellchecking, but only for multiline text fields
   5973   *     2  enables spellchecking for all text fields
   5974   */
   5975  readCheckSpelling() {
   5976    var pref = Preferences.get("layout.spellcheckDefault");
   5977    this._storedSpellCheck = pref.value;
   5978 
   5979    return pref.value != 0;
   5980  },
   5981 
   5982  /**
   5983   * Returns the value of the spellchecking preference represented by UI,
   5984   * preserving the preference's "hidden" value if the preference is
   5985   * unchanged and represents a value not strictly allowed in UI.
   5986   */
   5987  writeCheckSpelling() {
   5988    var checkbox = document.getElementById("checkSpelling");
   5989    if (checkbox.checked) {
   5990      if (this._storedSpellCheck == 2) {
   5991        return 2;
   5992      }
   5993      return 1;
   5994    }
   5995    return 0;
   5996  },
   5997 
   5998  _minUpdatePrefDisableTime: 1000,
   5999  /**
   6000   * Selects the correct item in the update radio group
   6001   */
   6002  async readUpdateAutoPref() {
   6003    if (
   6004      AppConstants.MOZ_UPDATER &&
   6005      (!Services.policies || Services.policies.isAllowed("appUpdate")) &&
   6006      !gIsPackagedApp
   6007    ) {
   6008      let radiogroup = document.getElementById("updateRadioGroup");
   6009 
   6010      radiogroup.disabled = true;
   6011      let enabled = await UpdateUtils.getAppUpdateAutoEnabled();
   6012      radiogroup.value = enabled;
   6013      radiogroup.disabled = false;
   6014 
   6015      this.maybeDisableBackgroundUpdateControls();
   6016    }
   6017  },
   6018 
   6019  /**
   6020   * Writes the value of the automatic update radio group to the disk
   6021   */
   6022  async writeUpdateAutoPref() {
   6023    if (
   6024      AppConstants.MOZ_UPDATER &&
   6025      (!Services.policies || Services.policies.isAllowed("appUpdate")) &&
   6026      !gIsPackagedApp
   6027    ) {
   6028      let radiogroup = document.getElementById("updateRadioGroup");
   6029      let updateAutoValue = radiogroup.value == "true";
   6030      let _disableTimeOverPromise = new Promise(r =>
   6031        setTimeout(r, this._minUpdatePrefDisableTime)
   6032      );
   6033      radiogroup.disabled = true;
   6034      try {
   6035        await UpdateUtils.setAppUpdateAutoEnabled(updateAutoValue);
   6036        await _disableTimeOverPromise;
   6037        radiogroup.disabled = false;
   6038      } catch (error) {
   6039        console.error(error);
   6040        await Promise.all([
   6041          this.readUpdateAutoPref(),
   6042          this.reportUpdatePrefWriteError(),
   6043        ]);
   6044        return;
   6045      }
   6046 
   6047      this.maybeDisableBackgroundUpdateControls();
   6048 
   6049      // If the value was changed to false the user should be given the option
   6050      // to discard an update if there is one.
   6051      if (!updateAutoValue) {
   6052        await this.checkUpdateInProgress();
   6053      }
   6054      // For tests:
   6055      radiogroup.dispatchEvent(new CustomEvent("ProcessedUpdatePrefChange"));
   6056    }
   6057  },
   6058 
   6059  isBackgroundUpdateUIAvailable() {
   6060    return (
   6061      AppConstants.MOZ_UPDATE_AGENT &&
   6062      // This UI controls a per-installation pref. It won't necessarily work
   6063      // properly if per-installation prefs aren't supported.
   6064      UpdateUtils.PER_INSTALLATION_PREFS_SUPPORTED &&
   6065      (!Services.policies || Services.policies.isAllowed("appUpdate")) &&
   6066      !gIsPackagedApp &&
   6067      !UpdateUtils.appUpdateSettingIsLocked("app.update.background.enabled")
   6068    );
   6069  },
   6070 
   6071  maybeDisableBackgroundUpdateControls() {
   6072    if (this.isBackgroundUpdateUIAvailable()) {
   6073      let radiogroup = document.getElementById("updateRadioGroup");
   6074      let updateAutoEnabled = radiogroup.value == "true";
   6075 
   6076      // This control is only active if auto update is enabled.
   6077      document.getElementById("backgroundUpdate").disabled = !updateAutoEnabled;
   6078    }
   6079  },
   6080 
   6081  async readBackgroundUpdatePref() {
   6082    const prefName = "app.update.background.enabled";
   6083    if (this.isBackgroundUpdateUIAvailable()) {
   6084      let backgroundCheckbox = document.getElementById("backgroundUpdate");
   6085 
   6086      // When the page first loads, the checkbox is unchecked until we finish
   6087      // reading the config file from the disk. But, ideally, we don't want to
   6088      // give the user the impression that this setting has somehow gotten
   6089      // turned off and they need to turn it back on. We also don't want the
   6090      // user interacting with the control, expecting a particular behavior, and
   6091      // then have the read complete and change the control in an unexpected
   6092      // way. So we disable the control while we are reading.
   6093      // The only entry points for this function are page load and user
   6094      // interaction with the control. By disabling the control to prevent
   6095      // further user interaction, we prevent the possibility of entering this
   6096      // function a second time while we are still reading.
   6097      backgroundCheckbox.disabled = true;
   6098 
   6099      // If we haven't already done this, it might result in the effective value
   6100      // of the Background Update pref changing. Thus, we should do it before
   6101      // we tell the user what value this pref has.
   6102      await BackgroundUpdate.ensureExperimentToRolloutTransitionPerformed();
   6103 
   6104      let enabled = await UpdateUtils.readUpdateConfigSetting(prefName);
   6105      backgroundCheckbox.checked = enabled;
   6106      this.maybeDisableBackgroundUpdateControls();
   6107    }
   6108  },
   6109 
   6110  async writeBackgroundUpdatePref() {
   6111    const prefName = "app.update.background.enabled";
   6112    if (this.isBackgroundUpdateUIAvailable()) {
   6113      let backgroundCheckbox = document.getElementById("backgroundUpdate");
   6114      backgroundCheckbox.disabled = true;
   6115      let backgroundUpdateEnabled = backgroundCheckbox.checked;
   6116      try {
   6117        await UpdateUtils.writeUpdateConfigSetting(
   6118          prefName,
   6119          backgroundUpdateEnabled
   6120        );
   6121      } catch (error) {
   6122        console.error(error);
   6123        await this.readBackgroundUpdatePref();
   6124        await this.reportUpdatePrefWriteError();
   6125        return;
   6126      }
   6127 
   6128      this.maybeDisableBackgroundUpdateControls();
   6129    }
   6130  },
   6131 
   6132  async reportUpdatePrefWriteError() {
   6133    let [title, message] = await document.l10n.formatValues([
   6134      { id: "update-setting-write-failure-title2" },
   6135      {
   6136        id: "update-setting-write-failure-message2",
   6137        args: { path: UpdateUtils.configFilePath },
   6138      },
   6139    ]);
   6140 
   6141    // Set up the Ok Button
   6142    let buttonFlags =
   6143      Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_OK;
   6144    Services.prompt.confirmEx(
   6145      window,
   6146      title,
   6147      message,
   6148      buttonFlags,
   6149      null,
   6150      null,
   6151      null,
   6152      null,
   6153      {}
   6154    );
   6155  },
   6156 
   6157  async checkUpdateInProgress() {
   6158    const aus = Cc["@mozilla.org/updates/update-service;1"].getService(
   6159      Ci.nsIApplicationUpdateService
   6160    );
   6161    let um = Cc["@mozilla.org/updates/update-manager;1"].getService(
   6162      Ci.nsIUpdateManager
   6163    );
   6164    // We don't want to see an idle state just because the updater hasn't
   6165    // initialized yet.
   6166    await aus.init();
   6167    if (aus.currentState == Ci.nsIApplicationUpdateService.STATE_IDLE) {
   6168      return;
   6169    }
   6170 
   6171    let [title, message, okButton, cancelButton] =
   6172      await document.l10n.formatValues([
   6173        { id: "update-in-progress-title" },
   6174        { id: "update-in-progress-message" },
   6175        { id: "update-in-progress-ok-button" },
   6176        { id: "update-in-progress-cancel-button" },
   6177      ]);
   6178 
   6179    // Continue is the cancel button which is BUTTON_POS_1 and is set as the
   6180    // default so pressing escape or using a platform standard method of closing
   6181    // the UI will not discard the update.
   6182    let buttonFlags =
   6183      Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0 +
   6184      Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1 +
   6185      Ci.nsIPrompt.BUTTON_POS_1_DEFAULT;
   6186 
   6187    let rv = Services.prompt.confirmEx(
   6188      window,
   6189      title,
   6190      message,
   6191      buttonFlags,
   6192      okButton,
   6193      cancelButton,
   6194      null,
   6195      null,
   6196      {}
   6197    );
   6198    if (rv != 1) {
   6199      await aus.stopDownload();
   6200      await um.cleanupActiveUpdates();
   6201      UpdateListener.clearPendingAndActiveNotifications();
   6202    }
   6203  },
   6204 
   6205  /**
   6206   * Displays the history of installed updates.
   6207   */
   6208  showUpdates() {
   6209    gSubDialog.open("chrome://mozapps/content/update/history.xhtml");
   6210  },
   6211 
   6212  destroy() {
   6213    window.removeEventListener("unload", this);
   6214    Services.obs.removeObserver(this, AUTO_UPDATE_CHANGED_TOPIC);
   6215    Services.obs.removeObserver(this, BACKGROUND_UPDATE_CHANGED_TOPIC);
   6216 
   6217    // Clean up the TranslationsView instance if it exists
   6218    if (this._translationsView) {
   6219      this._translationsView.destroy();
   6220      this._translationsView = null;
   6221    }
   6222  },
   6223 
   6224  // nsISupports
   6225 
   6226  QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
   6227 
   6228  // nsIObserver
   6229 
   6230  async observe(aSubject, aTopic, aData) {
   6231    if (aTopic == "nsPref:changed") {
   6232      if (aData == PREF_CONTAINERS_EXTENSION) {
   6233        return;
   6234      }
   6235      // Rebuild the list when there are changes to preferences that influence
   6236      // whether or not to show certain entries in the list.
   6237      if (!this._storingAction) {
   6238        await this._rebuildView();
   6239      }
   6240    } else if (aTopic == AUTO_UPDATE_CHANGED_TOPIC) {
   6241      if (!AppConstants.MOZ_UPDATER) {
   6242        return;
   6243      }
   6244      if (aData != "true" && aData != "false") {
   6245        throw new Error("Invalid preference value for app.update.auto");
   6246      }
   6247      document.getElementById("updateRadioGroup").value = aData;
   6248      this.maybeDisableBackgroundUpdateControls();
   6249    } else if (aTopic == BACKGROUND_UPDATE_CHANGED_TOPIC) {
   6250      if (!AppConstants.MOZ_UPDATE_AGENT) {
   6251        return;
   6252      }
   6253      if (aData != "true" && aData != "false") {
   6254        throw new Error(
   6255          "Invalid preference value for app.update.background.enabled"
   6256        );
   6257      }
   6258      document.getElementById("backgroundUpdate").checked = aData == "true";
   6259    }
   6260  },
   6261 
   6262  // EventListener
   6263 
   6264  handleEvent(aEvent) {
   6265    if (aEvent.type == "unload") {
   6266      this.destroy();
   6267      if (AppConstants.MOZ_UPDATER) {
   6268        onUnload();
   6269      }
   6270    }
   6271  },
   6272 
   6273  // Composed Model Construction
   6274 
   6275  _loadData() {
   6276    this._loadInternalHandlers();
   6277    this._loadApplicationHandlers();
   6278  },
   6279 
   6280  /**
   6281   * Load higher level internal handlers so they can be turned on/off in the
   6282   * applications menu.
   6283   */
   6284  _loadInternalHandlers() {
   6285    let internalHandlers = [new PDFHandlerInfoWrapper()];
   6286 
   6287    let enabledHandlers = Services.prefs
   6288      .getCharPref("browser.download.viewableInternally.enabledTypes", "")
   6289      .trim();
   6290    if (enabledHandlers) {
   6291      for (let ext of enabledHandlers.split(",")) {
   6292        internalHandlers.push(
   6293          new ViewableInternallyHandlerInfoWrapper(null, ext.trim())
   6294        );
   6295      }
   6296    }
   6297    for (let internalHandler of internalHandlers) {
   6298      if (internalHandler.enabled) {
   6299        this._handledTypes[internalHandler.type] = internalHandler;
   6300      }
   6301    }
   6302  },
   6303 
   6304  /**
   6305   * Load the set of handlers defined by the application datastore.
   6306   */
   6307  _loadApplicationHandlers() {
   6308    for (let wrappedHandlerInfo of gHandlerService.enumerate()) {
   6309      let type = wrappedHandlerInfo.type;
   6310 
   6311      let handlerInfoWrapper;
   6312      if (type in this._handledTypes) {
   6313        handlerInfoWrapper = this._handledTypes[type];
   6314      } else {
   6315        if (DownloadIntegration.shouldViewDownloadInternally(type)) {
   6316          handlerInfoWrapper = new ViewableInternallyHandlerInfoWrapper(type);
   6317        } else {
   6318          handlerInfoWrapper = new HandlerInfoWrapper(type, wrappedHandlerInfo);
   6319        }
   6320        this._handledTypes[type] = handlerInfoWrapper;
   6321      }
   6322    }
   6323  },
   6324 
   6325  // View Construction
   6326 
   6327  selectedHandlerListItem: null,
   6328 
   6329  _initListEventHandlers() {
   6330    this._list.addEventListener("select", event => {
   6331      if (event.target != this._list) {
   6332        return;
   6333      }
   6334 
   6335      let handlerListItem =
   6336        this._list.selectedItem &&
   6337        HandlerListItem.forNode(this._list.selectedItem);
   6338      if (this.selectedHandlerListItem == handlerListItem) {
   6339        return;
   6340      }
   6341 
   6342      if (this.selectedHandlerListItem) {
   6343        this.selectedHandlerListItem.showActionsMenu = false;
   6344      }
   6345      this.selectedHandlerListItem = handlerListItem;
   6346      if (handlerListItem) {
   6347        this.rebuildActionsMenu();
   6348        handlerListItem.showActionsMenu = true;
   6349      }
   6350    });
   6351  },
   6352 
   6353  async _rebuildVisibleTypes() {
   6354    this._visibleTypes = [];
   6355 
   6356    // Map whose keys are string descriptions and values are references to the
   6357    // first visible HandlerInfoWrapper that has this description. We use this
   6358    // to determine whether or not to annotate descriptions with their types to
   6359    // distinguish duplicate descriptions from each other.
   6360    let visibleDescriptions = new Map();
   6361    for (let type in this._handledTypes) {
   6362      // Yield before processing each handler info object to avoid monopolizing
   6363      // the main thread, as the objects are retrieved lazily, and retrieval
   6364      // can be expensive on Windows.
   6365      await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
   6366 
   6367      let handlerInfo = this._handledTypes[type];
   6368 
   6369      // We couldn't find any reason to exclude the type, so include it.
   6370      this._visibleTypes.push(handlerInfo);
   6371 
   6372      let key = JSON.stringify(handlerInfo.description);
   6373      let otherHandlerInfo = visibleDescriptions.get(key);
   6374      if (!otherHandlerInfo) {
   6375        // This is the first type with this description that we encountered
   6376        // while rebuilding the _visibleTypes array this time. Make sure the
   6377        // flag is reset so we won't add the type to the description.
   6378        handlerInfo.disambiguateDescription = false;
   6379        visibleDescriptions.set(key, handlerInfo);
   6380      } else {
   6381        // There is at least another type with this description. Make sure we
   6382        // add the type to the description on both HandlerInfoWrapper objects.
   6383        handlerInfo.disambiguateDescription = true;
   6384        otherHandlerInfo.disambiguateDescription = true;
   6385      }
   6386    }
   6387  },
   6388 
   6389  async _rebuildView() {
   6390    let lastSelectedType =
   6391      this.selectedHandlerListItem &&
   6392      this.selectedHandlerListItem.handlerInfoWrapper.type;
   6393    this.selectedHandlerListItem = null;
   6394 
   6395    // Clear the list of entries.
   6396    this._list.textContent = "";
   6397 
   6398    var visibleTypes = this._visibleTypes;
   6399 
   6400    let items = visibleTypes.map(
   6401      visibleType => new HandlerListItem(visibleType)
   6402    );
   6403    let itemsFragment = document.createDocumentFragment();
   6404    let lastSelectedItem;
   6405    for (let item of items) {
   6406      item.createNode(itemsFragment);
   6407      if (item.handlerInfoWrapper.type == lastSelectedType) {
   6408        lastSelectedItem = item;
   6409      }
   6410    }
   6411 
   6412    for (let item of items) {
   6413      item.setupNode();
   6414      this.rebuildActionsMenu(item.node, item.handlerInfoWrapper);
   6415      item.refreshAction();
   6416    }
   6417 
   6418    // If the user is filtering the list, then only show matching types.
   6419    // If we filter, we need to first localize the fragment, to
   6420    // be able to filter by localized values.
   6421    if (this._filter.value) {
   6422      await document.l10n.translateFragment(itemsFragment);
   6423 
   6424      this._filterView(itemsFragment);
   6425 
   6426      document.l10n.pauseObserving();
   6427      this._list.appendChild(itemsFragment);
   6428      document.l10n.resumeObserving();
   6429    } else {
   6430      // Otherwise we can just append the fragment and it'll
   6431      // get localized via the Mutation Observer.
   6432      this._list.appendChild(itemsFragment);
   6433    }
   6434 
   6435    if (lastSelectedItem) {
   6436      this._list.selectedItem = lastSelectedItem.node;
   6437    }
   6438  },
   6439 
   6440  /**
   6441   * Whether or not the given handler app is valid.
   6442   *
   6443   * @param aHandlerApp {nsIHandlerApp} the handler app in question
   6444   *
   6445   * @returns {boolean} whether or not it's valid
   6446   */
   6447  isValidHandlerApp(aHandlerApp) {
   6448    if (!aHandlerApp) {
   6449      return false;
   6450    }
   6451 
   6452    if (aHandlerApp instanceof Ci.nsILocalHandlerApp) {
   6453      return this._isValidHandlerExecutable(aHandlerApp.executable);
   6454    }
   6455 
   6456    if (aHandlerApp instanceof Ci.nsIWebHandlerApp) {
   6457      return aHandlerApp.uriTemplate;
   6458    }
   6459 
   6460    if (aHandlerApp instanceof Ci.nsIGIOMimeApp) {
   6461      return aHandlerApp.command;
   6462    }
   6463    if (aHandlerApp instanceof Ci.nsIGIOHandlerApp) {
   6464      return aHandlerApp.id;
   6465    }
   6466 
   6467    return false;
   6468  },
   6469 
   6470  _isValidHandlerExecutable(aExecutable) {
   6471    let leafName;
   6472    if (AppConstants.platform == "win") {
   6473      leafName = `${AppConstants.MOZ_APP_NAME}.exe`;
   6474    } else if (AppConstants.platform == "macosx") {
   6475      leafName = AppConstants.MOZ_MACBUNDLE_NAME;
   6476    } else {
   6477      leafName = `${AppConstants.MOZ_APP_NAME}-bin`;
   6478    }
   6479    return (
   6480      aExecutable &&
   6481      aExecutable.exists() &&
   6482      aExecutable.isExecutable() &&
   6483      // XXXben - we need to compare this with the running instance executable
   6484      //          just don't know how to do that via script...
   6485      // XXXmano TBD: can probably add this to nsIShellService
   6486      aExecutable.leafName != leafName
   6487    );
   6488  },
   6489 
   6490  /**
   6491   * Rebuild the actions menu for the selected entry.  Gets called by
   6492   * the richlistitem constructor when an entry in the list gets selected.
   6493   */
   6494  rebuildActionsMenu(
   6495    typeItem = this._list.selectedItem,
   6496    handlerInfo = this.selectedHandlerListItem.handlerInfoWrapper
   6497  ) {
   6498    var menu = typeItem.querySelector(".actionsMenu");
   6499    var menuPopup = menu.menupopup;
   6500 
   6501    // Clear out existing items.
   6502    while (menuPopup.hasChildNodes()) {
   6503      menuPopup.removeChild(menuPopup.lastChild);
   6504    }
   6505 
   6506    let internalMenuItem;
   6507    // Add the "Open in Firefox" option for optional internal handlers.
   6508    if (
   6509      handlerInfo instanceof InternalHandlerInfoWrapper &&
   6510      !handlerInfo.preventInternalViewing
   6511    ) {
   6512      internalMenuItem = document.createXULElement("menuitem");
   6513      internalMenuItem.setAttribute(
   6514        "action",
   6515        Ci.nsIHandlerInfo.handleInternally
   6516      );
   6517      internalMenuItem.className = "menuitem-iconic";
   6518      document.l10n.setAttributes(internalMenuItem, "applications-open-inapp");
   6519      internalMenuItem.setAttribute(APP_ICON_ATTR_NAME, "handleInternally");
   6520      menuPopup.appendChild(internalMenuItem);
   6521    }
   6522 
   6523    var askMenuItem = document.createXULElement("menuitem");
   6524    askMenuItem.setAttribute("action", Ci.nsIHandlerInfo.alwaysAsk);
   6525    askMenuItem.className = "menuitem-iconic";
   6526    document.l10n.setAttributes(askMenuItem, "applications-always-ask");
   6527    askMenuItem.setAttribute(APP_ICON_ATTR_NAME, "ask");
   6528    menuPopup.appendChild(askMenuItem);
   6529 
   6530    // Create a menu item for saving to disk.
   6531    // Note: this option isn't available to protocol types, since we don't know
   6532    // what it means to save a URL having a certain scheme to disk.
   6533    if (handlerInfo.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) {
   6534      var saveMenuItem = document.createXULElement("menuitem");
   6535      saveMenuItem.setAttribute("action", Ci.nsIHandlerInfo.saveToDisk);
   6536      document.l10n.setAttributes(saveMenuItem, "applications-action-save");
   6537      saveMenuItem.setAttribute(APP_ICON_ATTR_NAME, "save");
   6538      saveMenuItem.className = "menuitem-iconic";
   6539      menuPopup.appendChild(saveMenuItem);
   6540    }
   6541 
   6542    // Add a separator to distinguish these items from the helper app items
   6543    // that follow them.
   6544    let menuseparator = document.createXULElement("menuseparator");
   6545    menuPopup.appendChild(menuseparator);
   6546 
   6547    // Create a menu item for the OS default application, if any.
   6548    if (handlerInfo.hasDefaultHandler) {
   6549      var defaultMenuItem = document.createXULElement("menuitem");
   6550      defaultMenuItem.setAttribute(
   6551        "action",
   6552        Ci.nsIHandlerInfo.useSystemDefault
   6553      );
   6554      // If an internal option is available, don't show the application
   6555      // name for the OS default to prevent two options from appearing
   6556      // that may both say "Firefox".
   6557      if (internalMenuItem) {
   6558        document.l10n.setAttributes(
   6559          defaultMenuItem,
   6560          "applications-use-os-default"
   6561        );
   6562        defaultMenuItem.setAttribute("image", ICON_URL_APP);
   6563      } else {
   6564        document.l10n.setAttributes(
   6565          defaultMenuItem,
   6566          "applications-use-app-default",
   6567          {
   6568            "app-name": handlerInfo.defaultDescription,
   6569          }
   6570        );
   6571        let image = handlerInfo.iconURLForSystemDefault;
   6572        if (image) {
   6573          defaultMenuItem.setAttribute("image", image);
   6574        }
   6575      }
   6576 
   6577      menuPopup.appendChild(defaultMenuItem);
   6578    }
   6579 
   6580    // Create menu items for possible handlers.
   6581    let preferredApp = handlerInfo.preferredApplicationHandler;
   6582    var possibleAppMenuItems = [];
   6583    for (let possibleApp of handlerInfo.possibleApplicationHandlers.enumerate()) {
   6584      if (!this.isValidHandlerApp(possibleApp)) {
   6585        continue;
   6586      }
   6587 
   6588      let menuItem = document.createXULElement("menuitem");
   6589      menuItem.setAttribute("action", Ci.nsIHandlerInfo.useHelperApp);
   6590      let label;
   6591      if (possibleApp instanceof Ci.nsILocalHandlerApp) {
   6592        label = getFileDisplayName(possibleApp.executable);
   6593      } else {
   6594        label = possibleApp.name;
   6595      }
   6596      document.l10n.setAttributes(menuItem, "applications-use-app", {
   6597        "app-name": label,
   6598      });
   6599      let image = this._getIconURLForHandlerApp(possibleApp);
   6600      if (image) {
   6601        menuItem.setAttribute("image", image);
   6602      }
   6603 
   6604      // Attach the handler app object to the menu item so we can use it
   6605      // to make changes to the datastore when the user selects the item.
   6606      menuItem.handlerApp = possibleApp;
   6607 
   6608      menuPopup.appendChild(menuItem);
   6609      possibleAppMenuItems.push(menuItem);
   6610    }
   6611    // Add gio handlers
   6612    if (gGIOService) {
   6613      var gioApps = gGIOService.getAppsForURIScheme(handlerInfo.type);
   6614      let possibleHandlers = handlerInfo.possibleApplicationHandlers;
   6615      for (let handler of gioApps.enumerate(Ci.nsIHandlerApp)) {
   6616        // OS handler share the same name, it's most likely the same app, skipping...
   6617        if (handler.name == handlerInfo.defaultDescription) {
   6618          continue;
   6619        }
   6620        // Check if the handler is already in possibleHandlers
   6621        let appAlreadyInHandlers = false;
   6622        for (let i = possibleHandlers.length - 1; i >= 0; --i) {
   6623          let app = possibleHandlers.queryElementAt(i, Ci.nsIHandlerApp);
   6624          // nsGIOMimeApp::Equals is able to compare with nsILocalHandlerApp
   6625          if (handler.equals(app)) {
   6626            appAlreadyInHandlers = true;
   6627            break;
   6628          }
   6629        }
   6630        if (!appAlreadyInHandlers) {
   6631          let menuItem = document.createXULElement("menuitem");
   6632          menuItem.setAttribute("action", Ci.nsIHandlerInfo.useHelperApp);
   6633          document.l10n.setAttributes(menuItem, "applications-use-app", {
   6634            "app-name": handler.name,
   6635          });
   6636 
   6637          let image = this._getIconURLForHandlerApp(handler);
   6638          if (image) {
   6639            menuItem.setAttribute("image", image);
   6640          }
   6641 
   6642          // Attach the handler app object to the menu item so we can use it
   6643          // to make changes to the datastore when the user selects the item.
   6644          menuItem.handlerApp = handler;
   6645 
   6646          menuPopup.appendChild(menuItem);
   6647          possibleAppMenuItems.push(menuItem);
   6648        }
   6649      }
   6650    }
   6651 
   6652    // Create a menu item for selecting a local application.
   6653    let canOpenWithOtherApp = true;
   6654    if (AppConstants.platform == "win") {
   6655      // On Windows, selecting an application to open another application
   6656      // would be meaningless so we special case executables.
   6657      let executableType = Cc["@mozilla.org/mime;1"]
   6658        .getService(Ci.nsIMIMEService)
   6659        .getTypeFromExtension("exe");
   6660      canOpenWithOtherApp = handlerInfo.type != executableType;
   6661    }
   6662    if (canOpenWithOtherApp) {
   6663      let menuItem = document.createXULElement("menuitem");
   6664      menuItem.className = "choose-app-item";
   6665      menuItem.addEventListener("command", function (e) {
   6666        gMainPane.chooseApp(e);
   6667      });
   6668      document.l10n.setAttributes(menuItem, "applications-use-other");
   6669      menuPopup.appendChild(menuItem);
   6670    }
   6671 
   6672    // Create a menu item for managing applications.
   6673    if (possibleAppMenuItems.length) {
   6674      let menuItem = document.createXULElement("menuseparator");
   6675      menuPopup.appendChild(menuItem);
   6676      menuItem = document.createXULElement("menuitem");
   6677      menuItem.className = "manage-app-item";
   6678      menuItem.addEventListener("command", function (e) {
   6679        gMainPane.manageApp(e);
   6680      });
   6681      document.l10n.setAttributes(menuItem, "applications-manage-app");
   6682      menuPopup.appendChild(menuItem);
   6683    }
   6684 
   6685    // Select the item corresponding to the preferred action.  If the always
   6686    // ask flag is set, it overrides the preferred action.  Otherwise we pick
   6687    // the item identified by the preferred action (when the preferred action
   6688    // is to use a helper app, we have to pick the specific helper app item).
   6689    if (handlerInfo.alwaysAskBeforeHandling) {
   6690      menu.selectedItem = askMenuItem;
   6691    } else {
   6692      // The nsHandlerInfoAction enumeration values in nsIHandlerInfo identify
   6693      // the actions the application can take with content of various types.
   6694      // But since we've stopped support for plugins, there's no value
   6695      // identifying the "use plugin" action, so we use this constant instead.
   6696      const kActionUsePlugin = 5;
   6697 
   6698      switch (handlerInfo.preferredAction) {
   6699        case Ci.nsIHandlerInfo.handleInternally:
   6700          if (internalMenuItem) {
   6701            menu.selectedItem = internalMenuItem;
   6702          } else {
   6703            console.error("No menu item defined to set!");
   6704          }
   6705          break;
   6706        case Ci.nsIHandlerInfo.useSystemDefault:
   6707          // We might not have a default item if we're not aware of an
   6708          // OS-default handler for this type:
   6709          menu.selectedItem = defaultMenuItem || askMenuItem;
   6710          break;
   6711        case Ci.nsIHandlerInfo.useHelperApp:
   6712          if (preferredApp) {
   6713            let preferredItem = possibleAppMenuItems.find(v =>
   6714              v.handlerApp.equals(preferredApp)
   6715            );
   6716            if (preferredItem) {
   6717              menu.selectedItem = preferredItem;
   6718            } else {
   6719              // This shouldn't happen, but let's make sure we end up with a
   6720              // selected item:
   6721              let possible = possibleAppMenuItems
   6722                .map(v => v.handlerApp && v.handlerApp.name)
   6723                .join(", ");
   6724              console.error(
   6725                new Error(
   6726                  `Preferred handler for ${handlerInfo.type} not in list of possible handlers!? (List: ${possible})`
   6727                )
   6728              );
   6729              menu.selectedItem = askMenuItem;
   6730            }
   6731          }
   6732          break;
   6733        case kActionUsePlugin:
   6734          // We no longer support plugins, select "ask" instead:
   6735          menu.selectedItem = askMenuItem;
   6736          break;
   6737        case Ci.nsIHandlerInfo.saveToDisk:
   6738          menu.selectedItem = saveMenuItem;
   6739          break;
   6740      }
   6741    }
   6742  },
   6743 
   6744  // Sorting & Filtering
   6745 
   6746  _sortColumn: null,
   6747 
   6748  /**
   6749   * Sort the list when the user clicks on a column header.
   6750   */
   6751  sort(event) {
   6752    if (event.button != 0) {
   6753      return;
   6754    }
   6755    var column = event.target;
   6756 
   6757    // If the user clicked on a new sort column, remove the direction indicator
   6758    // from the old column.
   6759    if (this._sortColumn && this._sortColumn != column) {
   6760      this._sortColumn.removeAttribute("sortDirection");
   6761    }
   6762 
   6763    this._sortColumn = column;
   6764 
   6765    // Set (or switch) the sort direction indicator.
   6766    if (column.getAttribute("sortDirection") == "ascending") {
   6767      column.setAttribute("sortDirection", "descending");
   6768    } else {
   6769      column.setAttribute("sortDirection", "ascending");
   6770    }
   6771 
   6772    this._sortListView();
   6773  },
   6774 
   6775  async _sortListView() {
   6776    if (!this._sortColumn) {
   6777      return;
   6778    }
   6779    let comp = new Services.intl.Collator(undefined, {
   6780      usage: "sort",
   6781    });
   6782 
   6783    await document.l10n.translateFragment(this._list);
   6784    let items = Array.from(this._list.children);
   6785 
   6786    let textForNode;
   6787    if (this._sortColumn.getAttribute("value") === "type") {
   6788      textForNode = n => n.querySelector(".typeDescription").textContent;
   6789    } else {
   6790      textForNode = n => n.querySelector(".actionsMenu").getAttribute("label");
   6791    }
   6792 
   6793    let sortDir = this._sortColumn.getAttribute("sortDirection");
   6794    let multiplier = sortDir == "descending" ? -1 : 1;
   6795    items.sort(
   6796      (a, b) => multiplier * comp.compare(textForNode(a), textForNode(b))
   6797    );
   6798 
   6799    // Re-append items in the correct order:
   6800    items.forEach(item => this._list.appendChild(item));
   6801  },
   6802 
   6803  _filterView(frag = this._list) {
   6804    const filterValue = this._filter.value.toLowerCase();
   6805    for (let elem of frag.children) {
   6806      const typeDescription =
   6807        elem.querySelector(".typeDescription").textContent;
   6808      const actionDescription = elem
   6809        .querySelector(".actionDescription")
   6810        .getAttribute("value");
   6811      elem.hidden =
   6812        !typeDescription.toLowerCase().includes(filterValue) &&
   6813        !actionDescription.toLowerCase().includes(filterValue);
   6814    }
   6815  },
   6816 
   6817  /**
   6818   * Filter the list when the user enters a filter term into the filter field.
   6819   */
   6820  filter() {
   6821    this._rebuildView(); // FIXME: Should this be await since bug 1508156?
   6822  },
   6823 
   6824  focusFilterBox() {
   6825    this._filter.focus();
   6826    this._filter.select();
   6827  },
   6828 
   6829  // Changes
   6830 
   6831  // Whether or not we are currently storing the action selected by the user.
   6832  // We use this to suppress notification-triggered updates to the list when
   6833  // we make changes that may spawn such updates.
   6834  // XXXgijs: this was definitely necessary when we changed feed preferences
   6835  // from within _storeAction and its calltree. Now, it may still be
   6836  // necessary, to avoid calling _rebuildView. bug 1499350 has more details.
   6837  _storingAction: false,
   6838 
   6839  onSelectAction(aActionItem) {
   6840    this._storingAction = true;
   6841 
   6842    try {
   6843      this._storeAction(aActionItem);
   6844    } finally {
   6845      this._storingAction = false;
   6846    }
   6847  },
   6848 
   6849  _storeAction(aActionItem) {
   6850    var handlerInfo = this.selectedHandlerListItem.handlerInfoWrapper;
   6851 
   6852    let action = parseInt(aActionItem.getAttribute("action"));
   6853 
   6854    // Set the preferred application handler.
   6855    // We leave the existing preferred app in the list when we set
   6856    // the preferred action to something other than useHelperApp so that
   6857    // legacy datastores that don't have the preferred app in the list
   6858    // of possible apps still include the preferred app in the list of apps
   6859    // the user can choose to handle the type.
   6860    if (action == Ci.nsIHandlerInfo.useHelperApp) {
   6861      handlerInfo.preferredApplicationHandler = aActionItem.handlerApp;
   6862    }
   6863 
   6864    // Set the "always ask" flag.
   6865    if (action == Ci.nsIHandlerInfo.alwaysAsk) {
   6866      handlerInfo.alwaysAskBeforeHandling = true;
   6867    } else {
   6868      handlerInfo.alwaysAskBeforeHandling = false;
   6869    }
   6870 
   6871    // Set the preferred action.
   6872    handlerInfo.preferredAction = action;
   6873 
   6874    handlerInfo.store();
   6875 
   6876    // Update the action label and image to reflect the new preferred action.
   6877    this.selectedHandlerListItem.refreshAction();
   6878  },
   6879 
   6880  manageApp(aEvent) {
   6881    // Don't let the normal "on select action" handler get this event,
   6882    // as we handle it specially ourselves.
   6883    aEvent.stopPropagation();
   6884 
   6885    var handlerInfo = this.selectedHandlerListItem.handlerInfoWrapper;
   6886 
   6887    let onComplete = () => {
   6888      // Rebuild the actions menu so that we revert to the previous selection,
   6889      // or "Always ask" if the previous default application has been removed
   6890      this.rebuildActionsMenu();
   6891 
   6892      // update the richlistitem too. Will be visible when selecting another row
   6893      this.selectedHandlerListItem.refreshAction();
   6894    };
   6895 
   6896    gSubDialog.open(
   6897      "chrome://browser/content/preferences/dialogs/applicationManager.xhtml",
   6898      { features: "resizable=no", closingCallback: onComplete },
   6899      handlerInfo
   6900    );
   6901  },
   6902 
   6903  async chooseApp(aEvent) {
   6904    // Don't let the normal "on select action" handler get this event,
   6905    // as we handle it specially ourselves.
   6906    aEvent.stopPropagation();
   6907 
   6908    var handlerApp;
   6909    let chooseAppCallback = aHandlerApp => {
   6910      // Rebuild the actions menu whether the user picked an app or canceled.
   6911      // If they picked an app, we want to add the app to the menu and select it.
   6912      // If they canceled, we want to go back to their previous selection.
   6913      this.rebuildActionsMenu();
   6914 
   6915      // If the user picked a new app from the menu, select it.
   6916      if (aHandlerApp) {
   6917        let typeItem = this._list.selectedItem;
   6918        let actionsMenu = typeItem.querySelector(".actionsMenu");
   6919        let menuItems = actionsMenu.menupopup.childNodes;
   6920        for (let i = 0; i < menuItems.length; i++) {
   6921          let menuItem = menuItems[i];
   6922          if (menuItem.handlerApp && menuItem.handlerApp.equals(aHandlerApp)) {
   6923            actionsMenu.selectedIndex = i;
   6924            this.onSelectAction(menuItem);
   6925            break;
   6926          }
   6927        }
   6928      }
   6929    };
   6930 
   6931    if (AppConstants.platform == "win") {
   6932      var params = {};
   6933      var handlerInfo = this.selectedHandlerListItem.handlerInfoWrapper;
   6934 
   6935      params.mimeInfo = handlerInfo.wrappedHandlerInfo;
   6936      params.title = await document.l10n.formatValue(
   6937        "applications-select-helper"
   6938      );
   6939      if ("id" in handlerInfo.description) {
   6940        params.description = await document.l10n.formatValue(
   6941          handlerInfo.description.id,
   6942          handlerInfo.description.args
   6943        );
   6944      } else {
   6945        params.description = handlerInfo.typeDescription.raw;
   6946      }
   6947      params.filename = null;
   6948      params.handlerApp = null;
   6949 
   6950      let onAppSelected = () => {
   6951        if (this.isValidHandlerApp(params.handlerApp)) {
   6952          handlerApp = params.handlerApp;
   6953 
   6954          // Add the app to the type's list of possible handlers.
   6955          handlerInfo.addPossibleApplicationHandler(handlerApp);
   6956        }
   6957 
   6958        chooseAppCallback(handlerApp);
   6959      };
   6960 
   6961      gSubDialog.open(
   6962        "chrome://global/content/appPicker.xhtml",
   6963        { closingCallback: onAppSelected },
   6964        params
   6965      );
   6966    } else {
   6967      let winTitle = await document.l10n.formatValue(
   6968        "applications-select-helper"
   6969      );
   6970      let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
   6971      let fpCallback = aResult => {
   6972        if (
   6973          aResult == Ci.nsIFilePicker.returnOK &&
   6974          fp.file &&
   6975          this._isValidHandlerExecutable(fp.file)
   6976        ) {
   6977          handlerApp = Cc[
   6978            "@mozilla.org/uriloader/local-handler-app;1"
   6979          ].createInstance(Ci.nsILocalHandlerApp);
   6980          handlerApp.name = getFileDisplayName(fp.file);
   6981          handlerApp.executable = fp.file;
   6982 
   6983          // Add the app to the type's list of possible handlers.
   6984          let handler = this.selectedHandlerListItem.handlerInfoWrapper;
   6985          handler.addPossibleApplicationHandler(handlerApp);
   6986 
   6987          chooseAppCallback(handlerApp);
   6988        }
   6989      };
   6990 
   6991      // Prompt the user to pick an app.  If they pick one, and it's a valid
   6992      // selection, then add it to the list of possible handlers.
   6993      fp.init(window.browsingContext, winTitle, Ci.nsIFilePicker.modeOpen);
   6994      fp.appendFilters(Ci.nsIFilePicker.filterApps);
   6995      fp.open(fpCallback);
   6996    }
   6997  },
   6998 
   6999  _getIconURLForHandlerApp(aHandlerApp) {
   7000    if (aHandlerApp instanceof Ci.nsILocalHandlerApp) {
   7001      return this._getIconURLForFile(aHandlerApp.executable);
   7002    }
   7003 
   7004    if (aHandlerApp instanceof Ci.nsIWebHandlerApp) {
   7005      return this._getIconURLForWebApp(aHandlerApp.uriTemplate);
   7006    }
   7007 
   7008    if (aHandlerApp instanceof Ci.nsIGIOHandlerApp) {
   7009      return this._getIconURLForAppId(aHandlerApp.id);
   7010    }
   7011 
   7012    // We know nothing about other kinds of handler apps.
   7013    return "";
   7014  },
   7015 
   7016  _getIconURLForAppId(aAppId) {
   7017    return "moz-icon://" + aAppId + "?size=16";
   7018  },
   7019 
   7020  _getIconURLForFile(aFile) {
   7021    var fph = Services.io
   7022      .getProtocolHandler("file")
   7023      .QueryInterface(Ci.nsIFileProtocolHandler);
   7024    var urlSpec = fph.getURLSpecFromActualFile(aFile);
   7025 
   7026    return "moz-icon://" + urlSpec + "?size=16";
   7027  },
   7028 
   7029  _getIconURLForWebApp(aWebAppURITemplate) {
   7030    var uri = Services.io.newURI(aWebAppURITemplate);
   7031 
   7032    // Unfortunately we can't use the favicon service to get the favicon,
   7033    // because the service looks in the annotations table for a record with
   7034    // the exact URL we give it, and users won't have such records for URLs
   7035    // they don't visit, and users won't visit the web app's URL template,
   7036    // they'll only visit URLs derived from that template (i.e. with %s
   7037    // in the template replaced by the URL of the content being handled).
   7038 
   7039    if (
   7040      /^https?$/.test(uri.scheme) &&
   7041      Services.prefs.getBoolPref("browser.chrome.site_icons")
   7042    ) {
   7043      // As the favicon originates from web content and is displayed in the parent process,
   7044      // use the moz-remote-image: protocol to safely re-encode it.
   7045      return getMozRemoteImageURL(uri.prePath + "/favicon.ico", 16);
   7046    }
   7047 
   7048    return "";
   7049  },
   7050 };
   7051 
   7052 gMainPane.initialized = new Promise(res => {
   7053  gMainPane.setInitialized = res;
   7054 });
   7055 
   7056 // Utilities
   7057 
   7058 function getFileDisplayName(file) {
   7059  if (AppConstants.platform == "win") {
   7060    if (file instanceof Ci.nsILocalFileWin) {
   7061      try {
   7062        return file.getVersionInfoField("FileDescription");
   7063      } catch (e) {}
   7064    }
   7065  }
   7066  if (AppConstants.platform == "macosx") {
   7067    if (file instanceof Ci.nsILocalFileMac) {
   7068      try {
   7069        return file.bundleDisplayName;
   7070      } catch (e) {}
   7071    }
   7072  }
   7073  return file.leafName;
   7074 }
   7075 
   7076 function getLocalHandlerApp(aFile) {
   7077  var localHandlerApp = Cc[
   7078    "@mozilla.org/uriloader/local-handler-app;1"
   7079  ].createInstance(Ci.nsILocalHandlerApp);
   7080  localHandlerApp.name = getFileDisplayName(aFile);
   7081  localHandlerApp.executable = aFile;
   7082 
   7083  return localHandlerApp;
   7084 }
   7085 
   7086 // eslint-disable-next-line no-undef
   7087 let gHandlerListItemFragment = MozXULElement.parseXULToFragment(`
   7088  <richlistitem>
   7089    <hbox class="typeContainer" flex="1" align="center">
   7090      <html:img class="typeIcon" width="16" height="16" />
   7091      <label class="typeDescription" flex="1" crop="end"/>
   7092    </hbox>
   7093    <hbox class="actionContainer" flex="1" align="center">
   7094      <html:img class="actionIcon" width="16" height="16"/>
   7095      <label class="actionDescription" flex="1" crop="end"/>
   7096    </hbox>
   7097    <hbox class="actionsMenuContainer" flex="1">
   7098      <menulist class="actionsMenu" flex="1" crop="end" selectedIndex="1" aria-labelledby="actionColumn">
   7099        <menupopup/>
   7100      </menulist>
   7101    </hbox>
   7102  </richlistitem>
   7103 `);
   7104 
   7105 /**
   7106 * This is associated to <richlistitem> elements in the handlers view.
   7107 */
   7108 class HandlerListItem {
   7109  static forNode(node) {
   7110    return gNodeToObjectMap.get(node);
   7111  }
   7112 
   7113  constructor(handlerInfoWrapper) {
   7114    this.handlerInfoWrapper = handlerInfoWrapper;
   7115  }
   7116 
   7117  setOrRemoveAttributes(iterable) {
   7118    for (let [selector, name, value] of iterable) {
   7119      let node = selector ? this.node.querySelector(selector) : this.node;
   7120      if (value) {
   7121        node.setAttribute(name, value);
   7122      } else {
   7123        node.removeAttribute(name);
   7124      }
   7125    }
   7126  }
   7127 
   7128  createNode(list) {
   7129    list.appendChild(document.importNode(gHandlerListItemFragment, true));
   7130    this.node = list.lastChild;
   7131    gNodeToObjectMap.set(this.node, this);
   7132  }
   7133 
   7134  setupNode() {
   7135    this.node
   7136      .querySelector(".actionsMenu")
   7137      .addEventListener("command", event =>
   7138        gMainPane.onSelectAction(event.originalTarget)
   7139      );
   7140 
   7141    let typeDescription = this.handlerInfoWrapper.typeDescription;
   7142    this.setOrRemoveAttributes([
   7143      [null, "type", this.handlerInfoWrapper.type],
   7144      [".typeIcon", "srcset", this.handlerInfoWrapper.iconSrcSet],
   7145    ]);
   7146    localizeElement(
   7147      this.node.querySelector(".typeDescription"),
   7148      typeDescription
   7149    );
   7150    this.showActionsMenu = false;
   7151  }
   7152 
   7153  refreshAction() {
   7154    let { actionIconClass } = this.handlerInfoWrapper;
   7155    this.setOrRemoveAttributes([
   7156      [null, APP_ICON_ATTR_NAME, actionIconClass],
   7157      [
   7158        ".actionIcon",
   7159        "srcset",
   7160        actionIconClass ? null : this.handlerInfoWrapper.actionIconSrcset,
   7161      ],
   7162    ]);
   7163    const selectedItem = this.node.querySelector("[selected=true]");
   7164    if (!selectedItem) {
   7165      console.error("No selected item for " + this.handlerInfoWrapper.type);
   7166      return;
   7167    }
   7168    const { id, args } = document.l10n.getAttributes(selectedItem);
   7169    const messageIDs = {
   7170      "applications-action-save": "applications-action-save-label",
   7171      "applications-always-ask": "applications-always-ask-label",
   7172      "applications-open-inapp": "applications-open-inapp-label",
   7173      "applications-use-app-default": "applications-use-app-default-label",
   7174      "applications-use-app": "applications-use-app-label",
   7175      "applications-use-os-default": "applications-use-os-default-label",
   7176      "applications-use-other": "applications-use-other-label",
   7177    };
   7178    localizeElement(this.node.querySelector(".actionDescription"), {
   7179      id: messageIDs[id],
   7180      args,
   7181    });
   7182    localizeElement(this.node.querySelector(".actionsMenu"), { id, args });
   7183  }
   7184 
   7185  set showActionsMenu(value) {
   7186    this.setOrRemoveAttributes([
   7187      [".actionContainer", "hidden", value],
   7188      [".actionsMenuContainer", "hidden", !value],
   7189    ]);
   7190  }
   7191 }
   7192 
   7193 /**
   7194 * This API facilitates dual-model of some localization APIs which
   7195 * may operate on raw strings of l10n id/args pairs.
   7196 *
   7197 * The l10n can be:
   7198 *
   7199 * {raw: string} - raw strings to be used as text value of the element
   7200 * {id: string} - l10n-id
   7201 * {id: string, args: object} - l10n-id + l10n-args
   7202 */
   7203 function localizeElement(node, l10n) {
   7204  if (l10n.hasOwnProperty("raw")) {
   7205    node.removeAttribute("data-l10n-id");
   7206    node.textContent = l10n.raw;
   7207  } else {
   7208    document.l10n.setAttributes(node, l10n.id, l10n.args);
   7209  }
   7210 }
   7211 
   7212 /**
   7213 * This object wraps nsIHandlerInfo with some additional functionality
   7214 * the Applications prefpane needs to display and allow modification of
   7215 * the list of handled types.
   7216 *
   7217 * We create an instance of this wrapper for each entry we might display
   7218 * in the prefpane, and we compose the instances from various sources,
   7219 * including the handler service.
   7220 *
   7221 * We don't implement all the original nsIHandlerInfo functionality,
   7222 * just the stuff that the prefpane needs.
   7223 */
   7224 class HandlerInfoWrapper {
   7225  constructor(type, handlerInfo) {
   7226    this.type = type;
   7227    this.wrappedHandlerInfo = handlerInfo;
   7228    this.disambiguateDescription = false;
   7229  }
   7230 
   7231  get description() {
   7232    if (this.wrappedHandlerInfo.description) {
   7233      return { raw: this.wrappedHandlerInfo.description };
   7234    }
   7235 
   7236    if (this.primaryExtension) {
   7237      var extension = this.primaryExtension.toUpperCase();
   7238      return { id: "applications-file-ending", args: { extension } };
   7239    }
   7240 
   7241    return { raw: this.type };
   7242  }
   7243 
   7244  /**
   7245   * Describe, in a human-readable fashion, the type represented by the given
   7246   * handler info object.  Normally this is just the description, but if more
   7247   * than one object presents the same description, "disambiguateDescription"
   7248   * is set and we annotate the duplicate descriptions with the type itself
   7249   * to help users distinguish between those types.
   7250   */
   7251  get typeDescription() {
   7252    if (this.disambiguateDescription) {
   7253      const description = this.description;
   7254      if (description.id) {
   7255        // Pass through the arguments:
   7256        let { args = {} } = description;
   7257        args.type = this.type;
   7258        return {
   7259          id: description.id + "-with-type",
   7260          args,
   7261        };
   7262      }
   7263 
   7264      return {
   7265        id: "applications-type-description-with-type",
   7266        args: {
   7267          "type-description": description.raw,
   7268          type: this.type,
   7269        },
   7270      };
   7271    }
   7272 
   7273    return this.description;
   7274  }
   7275 
   7276  get actionIconClass() {
   7277    if (this.alwaysAskBeforeHandling) {
   7278      return "ask";
   7279    }
   7280 
   7281    switch (this.preferredAction) {
   7282      case Ci.nsIHandlerInfo.saveToDisk:
   7283        return "save";
   7284 
   7285      case Ci.nsIHandlerInfo.handleInternally:
   7286        if (this instanceof InternalHandlerInfoWrapper) {
   7287          return "handleInternally";
   7288        }
   7289        break;
   7290    }
   7291 
   7292    return "";
   7293  }
   7294 
   7295  get actionIconSrcset() {
   7296    let icon = this.actionIcon;
   7297    if (!icon || !icon.startsWith("moz-icon:")) {
   7298      return icon;
   7299    }
   7300    // We rely on the icon already having the ?size= parameter.
   7301    let srcset = [];
   7302    for (let scale of [1, 2, 3]) {
   7303      let scaledIcon = icon + "&scale=" + scale;
   7304      srcset.push(`${scaledIcon} ${scale}x`);
   7305    }
   7306    return srcset.join(", ");
   7307  }
   7308 
   7309  get actionIcon() {
   7310    switch (this.preferredAction) {
   7311      case Ci.nsIHandlerInfo.useSystemDefault:
   7312        return this.iconURLForSystemDefault;
   7313 
   7314      case Ci.nsIHandlerInfo.useHelperApp: {
   7315        let preferredApp = this.preferredApplicationHandler;
   7316        if (gMainPane.isValidHandlerApp(preferredApp)) {
   7317          return gMainPane._getIconURLForHandlerApp(preferredApp);
   7318        }
   7319      }
   7320      // This should never happen, but if preferredAction is set to some weird
   7321      // value, then fall back to the generic application icon.
   7322      // Explicit fall-through
   7323      default:
   7324        return ICON_URL_APP;
   7325    }
   7326  }
   7327 
   7328  get iconURLForSystemDefault() {
   7329    // Handler info objects for MIME types on some OSes implement a property bag
   7330    // interface from which we can get an icon for the default app, so if we're
   7331    // dealing with a MIME type on one of those OSes, then try to get the icon.
   7332    if (
   7333      this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo &&
   7334      this.wrappedHandlerInfo instanceof Ci.nsIPropertyBag
   7335    ) {
   7336      try {
   7337        let url = this.wrappedHandlerInfo.getProperty(
   7338          "defaultApplicationIconURL"
   7339        );
   7340        if (url) {
   7341          return url + "?size=16";
   7342        }
   7343      } catch (ex) {}
   7344    }
   7345 
   7346    // If this isn't a MIME type object on an OS that supports retrieving
   7347    // the icon, or if we couldn't retrieve the icon for some other reason,
   7348    // then use a generic icon.
   7349    return ICON_URL_APP;
   7350  }
   7351 
   7352  get preferredApplicationHandler() {
   7353    return this.wrappedHandlerInfo.preferredApplicationHandler;
   7354  }
   7355 
   7356  set preferredApplicationHandler(aNewValue) {
   7357    this.wrappedHandlerInfo.preferredApplicationHandler = aNewValue;
   7358 
   7359    // Make sure the preferred handler is in the set of possible handlers.
   7360    if (aNewValue) {
   7361      this.addPossibleApplicationHandler(aNewValue);
   7362    }
   7363  }
   7364 
   7365  get possibleApplicationHandlers() {
   7366    return this.wrappedHandlerInfo.possibleApplicationHandlers;
   7367  }
   7368 
   7369  addPossibleApplicationHandler(aNewHandler) {
   7370    for (let app of this.possibleApplicationHandlers.enumerate()) {
   7371      if (app.equals(aNewHandler)) {
   7372        return;
   7373      }
   7374    }
   7375    this.possibleApplicationHandlers.appendElement(aNewHandler);
   7376  }
   7377 
   7378  removePossibleApplicationHandler(aHandler) {
   7379    var defaultApp = this.preferredApplicationHandler;
   7380    if (defaultApp && aHandler.equals(defaultApp)) {
   7381      // If the app we remove was the default app, we must make sure
   7382      // it won't be used anymore
   7383      this.alwaysAskBeforeHandling = true;
   7384      this.preferredApplicationHandler = null;
   7385    }
   7386 
   7387    var handlers = this.possibleApplicationHandlers;
   7388    for (var i = 0; i < handlers.length; ++i) {
   7389      var handler = handlers.queryElementAt(i, Ci.nsIHandlerApp);
   7390      if (handler.equals(aHandler)) {
   7391        handlers.removeElementAt(i);
   7392        break;
   7393      }
   7394    }
   7395  }
   7396 
   7397  get hasDefaultHandler() {
   7398    return this.wrappedHandlerInfo.hasDefaultHandler;
   7399  }
   7400 
   7401  get defaultDescription() {
   7402    return this.wrappedHandlerInfo.defaultDescription;
   7403  }
   7404 
   7405  // What to do with content of this type.
   7406  get preferredAction() {
   7407    // If the action is to use a helper app, but we don't have a preferred
   7408    // handler app, then switch to using the system default, if any; otherwise
   7409    // fall back to saving to disk, which is the default action in nsMIMEInfo.
   7410    // Note: "save to disk" is an invalid value for protocol info objects,
   7411    // but the alwaysAskBeforeHandling getter will detect that situation
   7412    // and always return true in that case to override this invalid value.
   7413    if (
   7414      this.wrappedHandlerInfo.preferredAction ==
   7415        Ci.nsIHandlerInfo.useHelperApp &&
   7416      !gMainPane.isValidHandlerApp(this.preferredApplicationHandler)
   7417    ) {
   7418      if (this.wrappedHandlerInfo.hasDefaultHandler) {
   7419        return Ci.nsIHandlerInfo.useSystemDefault;
   7420      }
   7421      return Ci.nsIHandlerInfo.saveToDisk;
   7422    }
   7423 
   7424    return this.wrappedHandlerInfo.preferredAction;
   7425  }
   7426 
   7427  set preferredAction(aNewValue) {
   7428    this.wrappedHandlerInfo.preferredAction = aNewValue;
   7429  }
   7430 
   7431  get alwaysAskBeforeHandling() {
   7432    // If this is a protocol type and the preferred action is "save to disk",
   7433    // which is invalid for such types, then return true here to override that
   7434    // action.  This could happen when the preferred action is to use a helper
   7435    // app, but the preferredApplicationHandler is invalid, and there isn't
   7436    // a default handler, so the preferredAction getter returns save to disk
   7437    // instead.
   7438    if (
   7439      !(this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) &&
   7440      this.preferredAction == Ci.nsIHandlerInfo.saveToDisk
   7441    ) {
   7442      return true;
   7443    }
   7444 
   7445    return this.wrappedHandlerInfo.alwaysAskBeforeHandling;
   7446  }
   7447 
   7448  set alwaysAskBeforeHandling(aNewValue) {
   7449    this.wrappedHandlerInfo.alwaysAskBeforeHandling = aNewValue;
   7450  }
   7451 
   7452  // The primary file extension associated with this type, if any.
   7453  get primaryExtension() {
   7454    try {
   7455      if (
   7456        this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo &&
   7457        this.wrappedHandlerInfo.primaryExtension
   7458      ) {
   7459        return this.wrappedHandlerInfo.primaryExtension;
   7460      }
   7461    } catch (ex) {}
   7462 
   7463    return null;
   7464  }
   7465 
   7466  store() {
   7467    gHandlerService.store(this.wrappedHandlerInfo);
   7468  }
   7469 
   7470  get iconSrcSet() {
   7471    let srcset = [];
   7472    for (let scale of [1, 2]) {
   7473      let icon = this._getIcon(16, scale);
   7474      if (!icon) {
   7475        return null;
   7476      }
   7477      srcset.push(`${icon} ${scale}x`);
   7478    }
   7479    return srcset.join(", ");
   7480  }
   7481 
   7482  _getIcon(aSize, aScale = 1) {
   7483    if (this.primaryExtension) {
   7484      return `moz-icon://goat.${this.primaryExtension}?size=${aSize}&scale=${aScale}`;
   7485    }
   7486 
   7487    if (this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) {
   7488      return `moz-icon://goat?size=${aSize}&scale=${aScale}&contentType=${this.type}`;
   7489    }
   7490 
   7491    // FIXME: consider returning some generic icon when we can't get a URL for
   7492    // one (for example in the case of protocol schemes).  Filed as bug 395141.
   7493    return null;
   7494  }
   7495 }
   7496 
   7497 /**
   7498 * InternalHandlerInfoWrapper provides a basic mechanism to create an internal
   7499 * mime type handler that can be enabled/disabled in the applications preference
   7500 * menu.
   7501 */
   7502 class InternalHandlerInfoWrapper extends HandlerInfoWrapper {
   7503  constructor(mimeType, extension) {
   7504    let type = gMIMEService.getFromTypeAndExtension(mimeType, extension);
   7505    super(mimeType || type.type, type);
   7506  }
   7507 
   7508  // Override store so we so we can notify any code listening for registration
   7509  // or unregistration of this handler.
   7510  store() {
   7511    super.store();
   7512  }
   7513 
   7514  get preventInternalViewing() {
   7515    return false;
   7516  }
   7517 
   7518  get enabled() {
   7519    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
   7520  }
   7521 }
   7522 
   7523 class PDFHandlerInfoWrapper extends InternalHandlerInfoWrapper {
   7524  constructor() {
   7525    super(TYPE_PDF, null);
   7526  }
   7527 
   7528  get preventInternalViewing() {
   7529    return Services.prefs.getBoolPref(PREF_PDFJS_DISABLED);
   7530  }
   7531 
   7532  // PDF is always shown in the list, but the 'show internally' option is
   7533  // hidden when the internal PDF viewer is disabled.
   7534  get enabled() {
   7535    return true;
   7536  }
   7537 }
   7538 
   7539 class ViewableInternallyHandlerInfoWrapper extends InternalHandlerInfoWrapper {
   7540  get enabled() {
   7541    return DownloadIntegration.shouldViewDownloadInternally(this.type);
   7542  }
   7543 }