tor-browser

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

browser-addons.js (116026B)


      1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
      2 * This Source Code Form is subject to the terms of the Mozilla Public
      3 * License, v. 2.0. If a copy of the MPL was not distributed with this
      4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      5 
      6 var { XPCOMUtils } = ChromeUtils.importESModule(
      7  "resource://gre/modules/XPCOMUtils.sys.mjs"
      8 );
      9 
     10 const lazy = {};
     11 
     12 ChromeUtils.defineESModuleGetters(lazy, {
     13  AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
     14  AMBrowserExtensionsImport: "resource://gre/modules/AddonManager.sys.mjs",
     15  AbuseReporter: "resource://gre/modules/AbuseReporter.sys.mjs",
     16  ExtensionCommon: "resource://gre/modules/ExtensionCommon.sys.mjs",
     17  ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs",
     18  ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.sys.mjs",
     19  OriginControls: "resource://gre/modules/ExtensionPermissions.sys.mjs",
     20  PERMISSION_L10N: "resource://gre/modules/ExtensionPermissionMessages.sys.mjs",
     21  SITEPERMS_ADDON_TYPE:
     22    "resource://gre/modules/addons/siteperms-addon-utils.sys.mjs",
     23 });
     24 ChromeUtils.defineLazyGetter(lazy, "l10n", function () {
     25  return new Localization(
     26    ["browser/addonNotifications.ftl", "branding/brand.ftl"],
     27    true
     28  );
     29 });
     30 
     31 const HIDE_NO_SCRIPT_PREF = "extensions.hideNoScript";
     32 const HIDE_UNIFIED_WHEN_EMPTY_PREF = "extensions.hideUnifiedWhenEmpty";
     33 
     34 /**
     35 * Mapping of error code -> [
     36 *   error-id,
     37 *   local-error-id,
     38 *   (optional) error-id-when-addon-name-is-missing,
     39 *   (optional) local-error-id-when-addon-name-is-missing,
     40 * ]
     41 *
     42 * error-id is used for errors in DownloadedAddonInstall,
     43 * local-error-id for errors in LocalAddonInstall.
     44 *
     45 * The error codes are defined in AddonManager's _errors Map.
     46 * Not all error codes listed there are translated,
     47 * since errors that are only triggered during updates
     48 * will never reach this code.
     49 */
     50 const ERROR_L10N_IDS = new Map([
     51  [
     52    -1,
     53    [
     54      "addon-install-error-network-failure",
     55      "addon-local-install-error-network-failure",
     56    ],
     57  ],
     58  [
     59    -2,
     60    [
     61      "addon-install-error-incorrect-hash",
     62      "addon-local-install-error-incorrect-hash",
     63    ],
     64  ],
     65  [
     66    -3,
     67    [
     68      "addon-install-error-corrupt-file",
     69      "addon-local-install-error-corrupt-file",
     70    ],
     71  ],
     72  [
     73    -4,
     74    [
     75      "addon-install-error-file-access",
     76      "addon-local-install-error-file-access",
     77      "addon-install-error-no-addon-name-file-access",
     78      "addon-local-install-error-no-addon-name-file-access",
     79    ],
     80  ],
     81  [
     82    -5,
     83    ["addon-install-error-not-signed", "addon-local-install-error-not-signed"],
     84  ],
     85  [-8, ["addon-install-error-invalid-domain"]],
     86  [
     87    -10,
     88    ["addon-install-error-hard-blocked", "addon-install-error-hard-blocked"],
     89  ],
     90  [
     91    -11,
     92    ["addon-install-error-incompatible", "addon-install-error-incompatible"],
     93  ],
     94  [
     95    -13,
     96    [
     97      "addon-install-error-admin-install-only",
     98      "addon-install-error-admin-install-only",
     99    ],
    100  ],
    101  [
    102    -14,
    103    ["addon-install-error-soft-blocked2", "addon-install-error-soft-blocked2"],
    104  ],
    105 ]);
    106 
    107 customElements.define(
    108  "addon-notification-blocklist-url",
    109  class MozAddonNotificationBlocklistURL extends HTMLAnchorElement {
    110    connectedCallback() {
    111      this.addEventListener("click", this);
    112    }
    113 
    114    disconnectedCallback() {
    115      this.removeEventListener("click", this);
    116    }
    117 
    118    handleEvent(e) {
    119      if (e.type == "click") {
    120        e.preventDefault();
    121        window.openTrustedLinkIn(this.href, "tab", {
    122          // Make sure the newly open tab is going to be focused, independently
    123          // from general user prefs.
    124          forceForeground: true,
    125        });
    126      }
    127    }
    128  },
    129  { extends: "a" }
    130 );
    131 
    132 customElements.define(
    133  "addon-webext-permissions-notification",
    134  class MozAddonPermissionsNotification extends customElements.get(
    135    "popupnotification"
    136  ) {
    137    show() {
    138      super.show();
    139 
    140      if (!this.notification) {
    141        return;
    142      }
    143 
    144      if (!this.notification.options?.customElementOptions) {
    145        throw new Error(
    146          "Mandatory customElementOptions property missing from notification options"
    147        );
    148      }
    149 
    150      this.textEl = this.querySelector("#addon-webext-perm-text");
    151      this.introEl = this.querySelector("#addon-webext-perm-intro");
    152      this.permsTitleEl = this.querySelector(
    153        "#addon-webext-perm-title-required"
    154      );
    155      this.permsListEl = this.querySelector("#addon-webext-perm-list-required");
    156      this.permsTitleDataCollectionEl = this.querySelector(
    157        "#addon-webext-perm-title-data-collection"
    158      );
    159      this.permsListDataCollectionEl = this.querySelector(
    160        "#addon-webext-perm-list-data-collection"
    161      );
    162      this.permsTitleOptionalEl = this.querySelector(
    163        "#addon-webext-perm-title-optional"
    164      );
    165      this.permsListOptionalEl = this.querySelector(
    166        "#addon-webext-perm-list-optional"
    167      );
    168 
    169      this.render();
    170    }
    171 
    172    get hasNoPermissions() {
    173      const {
    174        strings,
    175        showIncognitoCheckbox,
    176        showTechnicalAndInteractionCheckbox,
    177      } = this.notification.options.customElementOptions;
    178 
    179      return !(
    180        strings.msgs.length ||
    181        this.#dataCollectionPermissions?.msg ||
    182        showIncognitoCheckbox ||
    183        showTechnicalAndInteractionCheckbox
    184      );
    185    }
    186 
    187    get domainsSet() {
    188      if (!this.notification?.options?.customElementOptions) {
    189        return undefined;
    190      }
    191      const { strings } = this.notification.options.customElementOptions;
    192      return strings.fullDomainsList?.domainsSet;
    193    }
    194 
    195    get hasFullDomainsList() {
    196      return this.domainsSet?.size;
    197    }
    198 
    199    #isFullDomainsListEntryIndex(idx) {
    200      if (!this.hasFullDomainsList) {
    201        return false;
    202      }
    203      const { strings } = this.notification.options.customElementOptions;
    204      return strings.fullDomainsList.msgIdIndex === idx;
    205    }
    206 
    207    /**
    208     * @returns {{idx: number, collectsTechnicalAndInteractionData: boolean}}
    209     * An object with information about data collection permissions for the UI.
    210     */
    211    get #dataCollectionPermissions() {
    212      if (!this.notification?.options?.customElementOptions) {
    213        return undefined;
    214      }
    215      const { strings } = this.notification.options.customElementOptions;
    216      return strings.dataCollectionPermissions;
    217    }
    218 
    219    render() {
    220      const {
    221        strings,
    222        showIncognitoCheckbox,
    223        showTechnicalAndInteractionCheckbox,
    224        isUserScriptsRequest,
    225      } = this.notification.options.customElementOptions;
    226 
    227      const {
    228        textEl,
    229        introEl,
    230        permsTitleEl,
    231        permsListEl,
    232        permsTitleDataCollectionEl,
    233        permsListDataCollectionEl,
    234        permsTitleOptionalEl,
    235        permsListOptionalEl,
    236      } = this;
    237 
    238      const HTML_NS = "http://www.w3.org/1999/xhtml";
    239      const doc = this.ownerDocument;
    240 
    241      this.#clearChildElements();
    242      // Re-enable "Allow" button if it was disabled by a previous request with
    243      // isUserScriptsRequest=true.
    244      this.#setAllowButtonEnabled(true);
    245 
    246      if (strings.text) {
    247        textEl.textContent = strings.text;
    248        // By default, multiline strings don't get formatted properly. These
    249        // are presently only used in site permission add-ons, so we treat it
    250        // as a special case to avoid unintended effects on other things.
    251        if (strings.text.includes("\n\n")) {
    252          textEl.classList.add("addon-webext-perm-text-multiline");
    253        }
    254        textEl.hidden = false;
    255      }
    256 
    257      if (strings.listIntro) {
    258        introEl.textContent = strings.listIntro;
    259        introEl.hidden = false;
    260      }
    261 
    262      // "sitepermission" add-ons don't have section headers.
    263      if (strings.sectionHeaders) {
    264        const { required, dataCollection, optional } = strings.sectionHeaders;
    265 
    266        permsTitleEl.textContent = required;
    267        permsTitleDataCollectionEl.textContent = dataCollection;
    268        permsTitleOptionalEl.textContent = optional;
    269      }
    270 
    271      // Return earlier if there are no permissions to list.
    272      if (this.hasNoPermissions) {
    273        return;
    274      }
    275 
    276      // We only expect a single permission for a userScripts request per
    277      // https://searchfox.org/mozilla-central/rev/5fb48bf50516ed2529d533e5dfe49b4752efb8b8/browser/modules/ExtensionsUI.sys.mjs#308-313.
    278      if (isUserScriptsRequest) {
    279        // The "userScripts" permission cannot be granted until the user has
    280        // confirmed again in the notification's content, as described at
    281        // https://bugzilla.mozilla.org/show_bug.cgi?id=1917000#c1
    282 
    283        let { checkboxEl, warningEl } = this.#createUserScriptsPermissionItems(
    284          // "userScripts" can only be requested with "permissions.request()",
    285          // which enforces that it is the only permission in the request.
    286          strings.msgs[0]
    287        );
    288 
    289        this.#setAllowButtonEnabled(false);
    290 
    291        let item = doc.createElementNS(HTML_NS, "li");
    292        item.append(checkboxEl, warningEl);
    293        item.classList.add("webext-perm-optional");
    294        permsListEl.append(item);
    295 
    296        permsTitleEl.hidden = false;
    297        permsListEl.hidden = false;
    298      } else {
    299        if (strings.msgs.length) {
    300          for (let [idx, msg] of strings.msgs.entries()) {
    301            let item = doc.createElementNS(HTML_NS, "li");
    302            item.classList.add("webext-perm-granted");
    303            if (
    304              this.hasFullDomainsList &&
    305              this.#isFullDomainsListEntryIndex(idx)
    306            ) {
    307              item.append(this.#createFullDomainsListFragment(msg));
    308            } else {
    309              item.textContent = msg;
    310            }
    311            permsListEl.appendChild(item);
    312          }
    313 
    314          permsTitleEl.hidden = false;
    315          permsListEl.hidden = false;
    316        }
    317 
    318        if (this.#dataCollectionPermissions?.msg) {
    319          let item = doc.createElementNS(HTML_NS, "li");
    320          item.classList.add(
    321            "webext-perm-granted",
    322            "webext-data-collection-perm-granted"
    323          );
    324          item.textContent = this.#dataCollectionPermissions.msg;
    325          permsListDataCollectionEl.appendChild(item);
    326          permsTitleDataCollectionEl.hidden = false;
    327          permsListDataCollectionEl.hidden = false;
    328        }
    329 
    330        // Add a checkbox for the "technicalAndInteraction" optional data
    331        // collection permission.
    332        if (showTechnicalAndInteractionCheckbox) {
    333          let item = doc.createElementNS(HTML_NS, "li");
    334          item.classList.add(
    335            "webext-perm-optional",
    336            "webext-data-collection-perm-optional"
    337          );
    338          item.appendChild(this.#createTechnicalAndInteractionDataCheckbox());
    339          permsListOptionalEl.appendChild(item);
    340          permsTitleOptionalEl.hidden = false;
    341          permsListOptionalEl.hidden = false;
    342        }
    343 
    344        if (showIncognitoCheckbox) {
    345          let item = doc.createElementNS(HTML_NS, "li");
    346          item.classList.add(
    347            "webext-perm-optional",
    348            "webext-perm-privatebrowsing"
    349          );
    350          item.appendChild(this.#createPrivateBrowsingCheckbox());
    351          permsListOptionalEl.appendChild(item);
    352          permsTitleOptionalEl.hidden = false;
    353          permsListOptionalEl.hidden = false;
    354        }
    355      }
    356    }
    357 
    358    #createFullDomainsListFragment(msg) {
    359      const HTML_NS = "http://www.w3.org/1999/xhtml";
    360      const doc = this.ownerDocument;
    361      const label = doc.createXULElement("label");
    362      label.value = msg;
    363      const domainsList = doc.createElementNS(HTML_NS, "ul");
    364      domainsList.classList.add("webext-perm-domains-list");
    365 
    366      // Enforce max-height and ensure the domains list is
    367      // scrollable when there are more than 5 domains.
    368      if (this.domainsSet.size > 5) {
    369        domainsList.classList.add("scrollable-domains-list");
    370      }
    371 
    372      for (const domain of this.domainsSet) {
    373        let domainItem = doc.createElementNS(HTML_NS, "li");
    374        domainItem.textContent = domain;
    375        domainsList.appendChild(domainItem);
    376      }
    377      const { DocumentFragment } = this.ownerGlobal;
    378      const fragment = new DocumentFragment();
    379      fragment.append(label);
    380      fragment.append(domainsList);
    381      return fragment;
    382    }
    383 
    384    #clearChildElements() {
    385      const {
    386        textEl,
    387        introEl,
    388        permsTitleEl,
    389        permsListEl,
    390        permsTitleDataCollectionEl,
    391        permsListDataCollectionEl,
    392        permsTitleOptionalEl,
    393        permsListOptionalEl,
    394      } = this;
    395 
    396      // Clear all changes to the child elements that may have been changed
    397      // by a previous call of the render method.
    398      textEl.textContent = "";
    399      textEl.hidden = true;
    400      textEl.classList.remove("addon-webext-perm-text-multiline");
    401 
    402      introEl.textContent = "";
    403      introEl.hidden = true;
    404 
    405      for (const title of [
    406        permsTitleEl,
    407        permsTitleOptionalEl,
    408        permsTitleDataCollectionEl,
    409      ]) {
    410        title.hidden = true;
    411      }
    412 
    413      for (const list of [
    414        permsListEl,
    415        permsListDataCollectionEl,
    416        permsListOptionalEl,
    417      ]) {
    418        list.textContent = "";
    419        list.hidden = true;
    420      }
    421    }
    422 
    423    #createUserScriptsPermissionItems(userScriptsPermissionMessage) {
    424      let checkboxEl = this.ownerDocument.createElement("moz-checkbox");
    425      checkboxEl.label = userScriptsPermissionMessage;
    426      checkboxEl.checked = false;
    427      checkboxEl.addEventListener("change", () => {
    428        // The main "Allow" button is disabled until the checkbox is checked.
    429        this.#setAllowButtonEnabled(checkboxEl.checked);
    430      });
    431 
    432      let warningEl = this.ownerDocument.createElement("moz-message-bar");
    433      warningEl.setAttribute("type", "warning");
    434      warningEl.setAttribute(
    435        "message",
    436        lazy.PERMISSION_L10N.formatValueSync(
    437          "webext-perms-extra-warning-userScripts-short"
    438        )
    439      );
    440 
    441      return { checkboxEl, warningEl };
    442    }
    443 
    444    #setAllowButtonEnabled(allowed) {
    445      let disabled = !allowed;
    446      // "mainactiondisabled" mirrors the "disabled" boolean attribute of the
    447      // "Allow" button.
    448      this.toggleAttribute("mainactiondisabled", disabled);
    449 
    450      // The "mainactiondisabled" attribute may also be toggled by the
    451      // PopupNotifications._setNotificationUIState() method, which can be
    452      // called as a side effect of toggling a checkbox within the notification
    453      // (via PopupNotifications._onCommand).
    454      //
    455      // To prevent PopupNotifications._setNotificationUIState() from setting
    456      // the "mainactiondisabled" attribute to a different state, also set the
    457      // "invalidselection" attribute, since _setNotificationUIState() mirrors
    458      // its value to "mainactiondisabled".
    459      //
    460      // TODO bug 1938623: Remove this when a better alternative exists.
    461      this.toggleAttribute("invalidselection", disabled);
    462    }
    463 
    464    #createPrivateBrowsingCheckbox() {
    465      const { grantPrivateBrowsingAllowed } =
    466        this.notification.options.customElementOptions;
    467 
    468      let checkboxEl = this.ownerDocument.createElement("moz-checkbox");
    469      checkboxEl.checked = grantPrivateBrowsingAllowed;
    470      checkboxEl.addEventListener("change", () => {
    471        // NOTE: the popupnotification instances will be reused
    472        // and so the callback function is destructured here to
    473        // avoid this custom element to prevent it from being
    474        // garbage collected.
    475        const { onPrivateBrowsingAllowedChanged } =
    476          this.notification.options.customElementOptions;
    477        onPrivateBrowsingAllowedChanged?.(checkboxEl.checked);
    478      });
    479      this.ownerDocument.l10n.setAttributes(
    480        checkboxEl,
    481        "popup-notification-addon-privatebrowsing-checkbox2"
    482      );
    483      return checkboxEl;
    484    }
    485 
    486    #createTechnicalAndInteractionDataCheckbox() {
    487      const { grantTechnicalAndInteractionDataCollection } =
    488        this.notification.options.customElementOptions;
    489 
    490      const checkboxEl = this.ownerDocument.createElement("moz-checkbox");
    491      this.ownerDocument.l10n.setAttributes(
    492        checkboxEl,
    493        "popup-notification-addon-technical-and-interaction-checkbox"
    494      );
    495      checkboxEl.checked = grantTechnicalAndInteractionDataCollection;
    496      checkboxEl.addEventListener("change", () => {
    497        // NOTE: the popupnotification instances will be reused
    498        // and so the callback function is destructured here to
    499        // avoid this custom element to prevent it from being
    500        // garbage collected.
    501        const { onTechnicalAndInteractionDataChanged } =
    502          this.notification.options.customElementOptions;
    503        onTechnicalAndInteractionDataChanged?.(checkboxEl.checked);
    504      });
    505 
    506      return checkboxEl;
    507    }
    508  },
    509  { extends: "popupnotification" }
    510 );
    511 
    512 customElements.define(
    513  "addon-progress-notification",
    514  class MozAddonProgressNotification extends customElements.get(
    515    "popupnotification"
    516  ) {
    517    show() {
    518      super.show();
    519      this.progressmeter = document.getElementById(
    520        "addon-progress-notification-progressmeter"
    521      );
    522 
    523      this.progresstext = document.getElementById(
    524        "addon-progress-notification-progresstext"
    525      );
    526 
    527      if (!this.notification) {
    528        return;
    529      }
    530 
    531      this.notification.options.installs.forEach(function (aInstall) {
    532        aInstall.addListener(this);
    533      }, this);
    534 
    535      // Calling updateProgress can sometimes cause this notification to be
    536      // removed in the middle of refreshing the notification panel which
    537      // makes the panel get refreshed again. Just initialise to the
    538      // undetermined state and then schedule a proper check at the next
    539      // opportunity
    540      this.setProgress(0, -1);
    541      this._updateProgressTimeout = setTimeout(
    542        this.updateProgress.bind(this),
    543        0
    544      );
    545    }
    546 
    547    disconnectedCallback() {
    548      this.destroy();
    549    }
    550 
    551    destroy() {
    552      if (!this.notification) {
    553        return;
    554      }
    555      this.notification.options.installs.forEach(function (aInstall) {
    556        aInstall.removeListener(this);
    557      }, this);
    558 
    559      clearTimeout(this._updateProgressTimeout);
    560    }
    561 
    562    setProgress(aProgress, aMaxProgress) {
    563      if (aMaxProgress == -1) {
    564        this.progressmeter.removeAttribute("value");
    565      } else {
    566        this.progressmeter.setAttribute(
    567          "value",
    568          (aProgress * 100) / aMaxProgress
    569        );
    570      }
    571 
    572      let now = Date.now();
    573 
    574      if (!this.notification.lastUpdate) {
    575        this.notification.lastUpdate = now;
    576        this.notification.lastProgress = aProgress;
    577        return;
    578      }
    579 
    580      let delta = now - this.notification.lastUpdate;
    581      if (delta < 400 && aProgress < aMaxProgress) {
    582        return;
    583      }
    584 
    585      // Set min. time delta to avoid division by zero in the upcoming speed calculation
    586      delta = Math.max(delta, 400);
    587      delta /= 1000;
    588 
    589      // This algorithm is the same used by the downloads code.
    590      let speed = (aProgress - this.notification.lastProgress) / delta;
    591      if (this.notification.speed) {
    592        speed = speed * 0.9 + this.notification.speed * 0.1;
    593      }
    594 
    595      this.notification.lastUpdate = now;
    596      this.notification.lastProgress = aProgress;
    597      this.notification.speed = speed;
    598 
    599      let status = null;
    600      [status, this.notification.last] = DownloadUtils.getDownloadStatus(
    601        aProgress,
    602        aMaxProgress,
    603        speed,
    604        this.notification.last
    605      );
    606      this.progresstext.setAttribute("value", status);
    607      this.progresstext.setAttribute("tooltiptext", status);
    608    }
    609 
    610    cancel() {
    611      let installs = this.notification.options.installs;
    612      installs.forEach(function (aInstall) {
    613        try {
    614          aInstall.cancel();
    615        } catch (e) {
    616          // Cancel will throw if the download has already failed
    617        }
    618      }, this);
    619 
    620      PopupNotifications.remove(this.notification);
    621    }
    622 
    623    updateProgress() {
    624      if (!this.notification) {
    625        return;
    626      }
    627 
    628      let downloadingCount = 0;
    629      let progress = 0;
    630      let maxProgress = 0;
    631 
    632      this.notification.options.installs.forEach(function (aInstall) {
    633        if (aInstall.maxProgress == -1) {
    634          maxProgress = -1;
    635        }
    636        progress += aInstall.progress;
    637        if (maxProgress >= 0) {
    638          maxProgress += aInstall.maxProgress;
    639        }
    640        if (aInstall.state < AddonManager.STATE_DOWNLOADED) {
    641          downloadingCount++;
    642        }
    643      });
    644 
    645      if (downloadingCount == 0) {
    646        this.destroy();
    647        this.progressmeter.removeAttribute("value");
    648        const status = lazy.l10n.formatValueSync("addon-download-verifying");
    649        this.progresstext.setAttribute("value", status);
    650        this.progresstext.setAttribute("tooltiptext", status);
    651      } else {
    652        this.setProgress(progress, maxProgress);
    653      }
    654    }
    655 
    656    onDownloadProgress() {
    657      this.updateProgress();
    658    }
    659 
    660    onDownloadFailed() {
    661      this.updateProgress();
    662    }
    663 
    664    onDownloadCancelled() {
    665      this.updateProgress();
    666    }
    667 
    668    onDownloadEnded() {
    669      this.updateProgress();
    670    }
    671  },
    672  { extends: "popupnotification" }
    673 );
    674 
    675 // This custom element wraps the messagebar shown in the extensions panel
    676 // and used in both ext-browserAction.js and browser-unified-extensions.js
    677 customElements.define(
    678  "unified-extensions-item-messagebar-wrapper",
    679  class extends HTMLElement {
    680    get extensionPolicy() {
    681      return WebExtensionPolicy.getByID(this.extensionId);
    682    }
    683 
    684    get extensionName() {
    685      return this.extensionPolicy?.name;
    686    }
    687 
    688    get isSoftBlocked() {
    689      return this.extensionPolicy?.extension?.isSoftBlocked;
    690    }
    691 
    692    connectedCallback() {
    693      this.messagebar = document.createElement("moz-message-bar");
    694      this.messagebar.classList.add("unified-extensions-item-messagebar");
    695      this.append(this.messagebar);
    696      this.refresh();
    697    }
    698 
    699    disconnectedCallback() {
    700      this.messagebar?.remove();
    701    }
    702 
    703    async refresh() {
    704      if (!this.messagebar) {
    705        // Nothing to refresh, the custom element has not been
    706        // connected to the DOM yet.
    707        return;
    708      }
    709      if (!customElements.get("moz-message-bar")) {
    710        document.createElement("moz-message-bar");
    711        await customElements.whenDefined("moz-message-bar");
    712      }
    713      const { messagebar } = this;
    714      if (this.isSoftBlocked) {
    715        const SOFTBLOCK_FLUENTID =
    716          "unified-extensions-item-messagebar-softblocked2";
    717        if (
    718          messagebar.messageL10nId === SOFTBLOCK_FLUENTID &&
    719          messagebar.messageL10nArgs?.extensionName === this.extensionName
    720        ) {
    721          // nothing to refresh.
    722          return;
    723        }
    724        messagebar.removeAttribute("hidden");
    725        messagebar.setAttribute("type", "warning");
    726        messagebar.messageL10nId = SOFTBLOCK_FLUENTID;
    727        messagebar.messageL10nArgs = {
    728          extensionName: this.extensionName,
    729        };
    730      } else {
    731        if (messagebar.hasAttribute("hidden")) {
    732          // nothing to refresh.
    733          return;
    734        }
    735        messagebar.setAttribute("hidden", "true");
    736        messagebar.messageL10nId = null;
    737        messagebar.messageL10nArgs = null;
    738      }
    739      messagebar.requestUpdate();
    740    }
    741  }
    742 );
    743 
    744 class BrowserActionWidgetObserver {
    745  #connected = false;
    746  /**
    747   * @param {string} addonId The ID of the extension
    748   * @param {function()} onButtonAreaChanged Callback that is called whenever
    749   *   the observer detects the presence, absence or relocation of the browser
    750   *   action button for the given extension.
    751   */
    752  constructor(addonId, onButtonAreaChanged) {
    753    this.addonId = addonId;
    754    // The expected ID of the browserAction widget. Keep in sync with
    755    // actionWidgetId logic in ext-browserAction.js.
    756    this.widgetId = `${lazy.ExtensionCommon.makeWidgetId(addonId)}-browser-action`;
    757    this.onButtonAreaChanged = onButtonAreaChanged;
    758  }
    759 
    760  startObserving() {
    761    if (this.#connected) {
    762      return;
    763    }
    764    this.#connected = true;
    765    CustomizableUI.addListener(this);
    766    window.addEventListener("unload", this);
    767  }
    768 
    769  stopObserving() {
    770    if (!this.#connected) {
    771      return;
    772    }
    773    this.#connected = false;
    774    CustomizableUI.removeListener(this);
    775    window.removeEventListener("unload", this);
    776  }
    777 
    778  hasBrowserActionUI() {
    779    const policy = WebExtensionPolicy.getByID(this.addonId);
    780    if (!policy?.canAccessWindow(window)) {
    781      // Add-on is not an extension, or extension has not started yet. Or it
    782      // was uninstalled/disabled. Or disabled in current (private) window.
    783      return false;
    784    }
    785    if (!gUnifiedExtensions.browserActionFor(policy)) {
    786      // Does not have a browser action button.
    787      return false;
    788    }
    789    return true;
    790  }
    791 
    792  onWidgetCreated(aWidgetId) {
    793    // This is triggered as soon as ext-browserAction registers the button,
    794    // shortly after hasBrowserActionUI() above can return true for the first
    795    // time since add-on installation.
    796    if (aWidgetId === this.widgetId) {
    797      this.onButtonAreaChanged();
    798    }
    799  }
    800 
    801  onWidgetAdded(aWidgetId) {
    802    if (aWidgetId === this.widgetId) {
    803      this.onButtonAreaChanged();
    804    }
    805  }
    806 
    807  onWidgetMoved(aWidgetId) {
    808    if (aWidgetId === this.widgetId) {
    809      this.onButtonAreaChanged();
    810    }
    811  }
    812 
    813  handleEvent(event) {
    814    if (event.type === "unload") {
    815      this.stopObserving();
    816    }
    817  }
    818 }
    819 
    820 customElements.define(
    821  "addon-installed-notification",
    822  class MozAddonInstalledNotification extends customElements.get(
    823    "popupnotification"
    824  ) {
    825    #shouldIgnoreCheckboxStateChangeEvent = false;
    826    #browserActionWidgetObserver;
    827    connectedCallback() {
    828      this.descriptionEl = this.querySelector("#addon-install-description");
    829      this.pinExtensionEl = this.querySelector(
    830        "#addon-pin-toolbarbutton-checkbox"
    831      );
    832 
    833      this.addEventListener("click", this);
    834      this.pinExtensionEl.addEventListener("CheckboxStateChange", this);
    835      this.#browserActionWidgetObserver?.startObserving();
    836    }
    837 
    838    disconnectedCallback() {
    839      this.removeEventListener("click", this);
    840      this.pinExtensionEl.removeEventListener("CheckboxStateChange", this);
    841      this.#browserActionWidgetObserver?.stopObserving();
    842    }
    843 
    844    get #settingsLinkId() {
    845      return "addon-install-settings-link";
    846    }
    847 
    848    handleEvent(event) {
    849      const { target } = event;
    850 
    851      switch (event.type) {
    852        case "click": {
    853          if (target.id === this.#settingsLinkId) {
    854            const { addonId } = this.notification.options.customElementOptions;
    855            BrowserAddonUI.openAddonsMgr(
    856              "addons://detail/" + encodeURIComponent(addonId)
    857            );
    858            // The settings link element has its href set to "#" to be
    859            // accessible with keyboard navigation, and so we call
    860            // preventDefault to avoid the "#" href to be implicitly
    861            // added to the browser chrome window url (See Bug 1983869
    862            // for more details of the regression that the implicit
    863            // change to the chrome window urls triggers).
    864            event.preventDefault();
    865          }
    866          break;
    867        }
    868        case "CheckboxStateChange":
    869          // CheckboxStateChange fires whenever the checked value changes.
    870          // Ignore the event if triggered by us instead of the user.
    871          if (!this.#shouldIgnoreCheckboxStateChangeEvent) {
    872            this.#handlePinnedCheckboxStateChange();
    873          }
    874          break;
    875      }
    876    }
    877 
    878    show() {
    879      super.show();
    880 
    881      if (!this.notification) {
    882        return;
    883      }
    884 
    885      if (!this.notification.options?.customElementOptions) {
    886        throw new Error(
    887          "Mandatory customElementOptions property missing from notification options"
    888        );
    889      }
    890 
    891      this.#browserActionWidgetObserver?.stopObserving();
    892      this.#browserActionWidgetObserver = new BrowserActionWidgetObserver(
    893        this.notification.options.customElementOptions.addonId,
    894        () => this.#renderPinToolbarButtonCheckbox()
    895      );
    896 
    897      this.render();
    898      if (this.isConnected) {
    899        this.#browserActionWidgetObserver.startObserving();
    900      }
    901    }
    902 
    903    render() {
    904      let fluentId = "appmenu-addon-post-install-message3";
    905 
    906      this.ownerDocument.l10n.setAttributes(this.descriptionEl, null);
    907      this.querySelector(`#${this.#settingsLinkId}`)?.remove();
    908 
    909      if (this.#dataCollectionPermissionsEnabled) {
    910        const HTML_NS = "http://www.w3.org/1999/xhtml";
    911        const link = document.createElementNS(HTML_NS, "a");
    912        link.setAttribute("id", this.#settingsLinkId);
    913        link.setAttribute("data-l10n-name", "settings-link");
    914        // Make the link both accessible and keyboard-friendly.
    915        link.href = "#";
    916        this.descriptionEl.append(link);
    917 
    918        fluentId = "appmenu-addon-post-install-message-with-data-collection";
    919      }
    920 
    921      this.ownerDocument.l10n.setAttributes(this.descriptionEl, fluentId);
    922      this.#renderPinToolbarButtonCheckbox();
    923    }
    924 
    925    get #dataCollectionPermissionsEnabled() {
    926      return Services.prefs.getBoolPref(
    927        "extensions.dataCollectionPermissions.enabled",
    928        false
    929      );
    930    }
    931 
    932    #renderPinToolbarButtonCheckbox() {
    933      // If the extension has a browser action, show the checkbox to allow the
    934      // user to customize its location. Hide by default until we know for
    935      // certain that the conditions have been met.
    936      this.pinExtensionEl.hidden = true;
    937 
    938      if (!this.#browserActionWidgetObserver.hasBrowserActionUI()) {
    939        return;
    940      }
    941      const widgetId = this.#browserActionWidgetObserver.widgetId;
    942 
    943      // Extension buttons appear in AREA_ADDONS by default. There are several
    944      // ways for the default to differ for a specific add-on, including the
    945      // extension specifying default_area in its manifest.json file, an
    946      // enterprise policy having been configured, or the user having moved the
    947      // button someplace else. We only show the checkbox if it is either in
    948      // AREA_ADDONS or in the toolbar. This covers almost all common cases.
    949      const area = CustomizableUI.getPlacementOfWidget(widgetId)?.area;
    950      let shouldPinToToolbar = area !== CustomizableUI.AREA_ADDONS;
    951      if (shouldPinToToolbar && area !== CustomizableUI.AREA_NAVBAR) {
    952        // We only support AREA_ADDONS and AREA_NAVBAR for now.
    953        return;
    954      }
    955      this.#shouldIgnoreCheckboxStateChangeEvent = true;
    956      this.pinExtensionEl.checked = shouldPinToToolbar;
    957      this.#shouldIgnoreCheckboxStateChangeEvent = false;
    958      this.pinExtensionEl.hidden = false;
    959    }
    960 
    961    #handlePinnedCheckboxStateChange() {
    962      if (!this.#browserActionWidgetObserver.hasBrowserActionUI()) {
    963        // Unexpected. #renderPinToolbarButtonCheckbox() should have hidden
    964        // the checkbox if there is no widget.
    965        const { addonId } = this.notification.options.customElementOptions;
    966        throw new Error(`No browser action widget found for ${addonId}!`);
    967      }
    968      const widgetId = this.#browserActionWidgetObserver.widgetId;
    969      const shouldPinToToolbar = this.pinExtensionEl.checked;
    970      if (shouldPinToToolbar) {
    971        gUnifiedExtensions._maybeMoveWidgetNodeBack(widgetId);
    972      }
    973      gUnifiedExtensions.pinToToolbar(widgetId, shouldPinToToolbar);
    974    }
    975  },
    976  { extends: "popupnotification" }
    977 );
    978 
    979 // Removes a doorhanger notification if all of the installs it was notifying
    980 // about have ended in some way.
    981 function removeNotificationOnEnd(notification, installs) {
    982  let count = installs.length;
    983 
    984  function maybeRemove(install) {
    985    install.removeListener(this);
    986 
    987    if (--count == 0) {
    988      // Check that the notification is still showing
    989      let current = PopupNotifications.getNotification(
    990        notification.id,
    991        notification.browser
    992      );
    993      if (current === notification) {
    994        notification.remove();
    995      }
    996    }
    997  }
    998 
    999  for (let install of installs) {
   1000    install.addListener({
   1001      onDownloadCancelled: maybeRemove,
   1002      onDownloadFailed: maybeRemove,
   1003      onInstallFailed: maybeRemove,
   1004      onInstallEnded: maybeRemove,
   1005    });
   1006  }
   1007 }
   1008 
   1009 function buildNotificationAction(msg, callback) {
   1010  let label = "";
   1011  let accessKey = "";
   1012  for (let { name, value } of msg.attributes) {
   1013    switch (name) {
   1014      case "label":
   1015        label = value;
   1016        break;
   1017      case "accesskey":
   1018        accessKey = value;
   1019        break;
   1020    }
   1021  }
   1022  return { label, accessKey, callback };
   1023 }
   1024 
   1025 var gXPInstallObserver = {
   1026  pendingInstalls: new WeakMap(),
   1027 
   1028  showInstallConfirmation(browser, installInfo, height = undefined) {
   1029    // If the confirmation notification is already open cache the installInfo
   1030    // and the new confirmation will be shown later
   1031    if (
   1032      PopupNotifications.getNotification("addon-install-confirmation", browser)
   1033    ) {
   1034      let pending = this.pendingInstalls.get(browser);
   1035      if (pending) {
   1036        pending.push(installInfo);
   1037      } else {
   1038        this.pendingInstalls.set(browser, [installInfo]);
   1039      }
   1040      return;
   1041    }
   1042 
   1043    let showNextConfirmation = () => {
   1044      // Make sure the browser is still alive.
   1045      if (!gBrowser.browsers.includes(browser)) {
   1046        return;
   1047      }
   1048 
   1049      let pending = this.pendingInstalls.get(browser);
   1050      if (pending && pending.length) {
   1051        this.showInstallConfirmation(browser, pending.shift());
   1052      }
   1053    };
   1054 
   1055    // If all installs have already been cancelled in some way then just show
   1056    // the next confirmation
   1057    if (
   1058      installInfo.installs.every(i => i.state != AddonManager.STATE_DOWNLOADED)
   1059    ) {
   1060      showNextConfirmation();
   1061      return;
   1062    }
   1063 
   1064    // Make notifications persistent
   1065    var options = {
   1066      displayURI: installInfo.originatingURI,
   1067      persistent: true,
   1068      hideClose: true,
   1069      popupOptions: {
   1070        position: "bottomright topright",
   1071      },
   1072    };
   1073 
   1074    let acceptInstallation = () => {
   1075      for (let install of installInfo.installs) {
   1076        install.install();
   1077      }
   1078      installInfo = null;
   1079 
   1080      Glean.securityUi.events.accumulateSingleSample(
   1081        Ci.nsISecurityUITelemetry.WARNING_CONFIRM_ADDON_INSTALL_CLICK_THROUGH
   1082      );
   1083    };
   1084 
   1085    let cancelInstallation = () => {
   1086      if (installInfo) {
   1087        for (let install of installInfo.installs) {
   1088          // The notification may have been closed because the add-ons got
   1089          // cancelled elsewhere, only try to cancel those that are still
   1090          // pending install.
   1091          if (install.state != AddonManager.STATE_CANCELLED) {
   1092            install.cancel();
   1093          }
   1094        }
   1095      }
   1096 
   1097      showNextConfirmation();
   1098    };
   1099 
   1100    let unsigned = installInfo.installs.filter(
   1101      i => i.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING
   1102    );
   1103    let someUnsigned =
   1104      !!unsigned.length && unsigned.length < installInfo.installs.length;
   1105 
   1106    options.eventCallback = aEvent => {
   1107      switch (aEvent) {
   1108        case "removed":
   1109          cancelInstallation();
   1110          break;
   1111        case "shown": {
   1112          let addonList = document.getElementById(
   1113            "addon-install-confirmation-content"
   1114          );
   1115          while (addonList.firstChild) {
   1116            addonList.firstChild.remove();
   1117          }
   1118 
   1119          for (let install of installInfo.installs) {
   1120            let container = document.createXULElement("hbox");
   1121 
   1122            let name = document.createXULElement("label");
   1123            name.setAttribute("value", install.addon.name);
   1124            name.setAttribute("class", "addon-install-confirmation-name");
   1125            container.appendChild(name);
   1126 
   1127            if (
   1128              someUnsigned &&
   1129              install.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING
   1130            ) {
   1131              let unsignedLabel = document.createXULElement("label");
   1132              document.l10n.setAttributes(
   1133                unsignedLabel,
   1134                "popup-notification-addon-install-unsigned"
   1135              );
   1136              unsignedLabel.setAttribute(
   1137                "class",
   1138                "addon-install-confirmation-unsigned"
   1139              );
   1140              container.appendChild(unsignedLabel);
   1141            }
   1142 
   1143            addonList.appendChild(container);
   1144          }
   1145          break;
   1146        }
   1147      }
   1148    };
   1149 
   1150    options.learnMoreURL = Services.urlFormatter.formatURLPref(
   1151      "app.support.baseURL"
   1152    );
   1153 
   1154    let msgId;
   1155    let notification = document.getElementById(
   1156      "addon-install-confirmation-notification"
   1157    );
   1158    if (unsigned.length == installInfo.installs.length) {
   1159      // None of the add-ons are verified
   1160      msgId = "addon-confirm-install-unsigned-message";
   1161      notification.setAttribute("warning", "true");
   1162      options.learnMoreURL += "unsigned-addons";
   1163    } else if (!unsigned.length) {
   1164      // All add-ons are verified or don't need to be verified
   1165      msgId = "addon-confirm-install-message";
   1166      notification.removeAttribute("warning");
   1167      options.learnMoreURL += "find-and-install-add-ons";
   1168    } else {
   1169      // Some of the add-ons are unverified, the list of names will indicate
   1170      // which
   1171      msgId = "addon-confirm-install-some-unsigned-message";
   1172      notification.setAttribute("warning", "true");
   1173      options.learnMoreURL += "unsigned-addons";
   1174    }
   1175    const addonCount = installInfo.installs.length;
   1176    const messageString = lazy.l10n.formatValueSync(msgId, { addonCount });
   1177 
   1178    const [acceptMsg, cancelMsg] = lazy.l10n.formatMessagesSync([
   1179      "addon-install-accept-button",
   1180      "addon-install-cancel-button",
   1181    ]);
   1182    const action = buildNotificationAction(acceptMsg, acceptInstallation);
   1183    const secondaryAction = buildNotificationAction(cancelMsg, () => {});
   1184 
   1185    if (height) {
   1186      notification.style.minHeight = height + "px";
   1187    }
   1188 
   1189    let tab = gBrowser.getTabForBrowser(browser);
   1190    if (tab) {
   1191      gBrowser.selectedTab = tab;
   1192    }
   1193 
   1194    let popup = PopupNotifications.show(
   1195      browser,
   1196      "addon-install-confirmation",
   1197      messageString,
   1198      gUnifiedExtensions.getPopupAnchorID(browser, window),
   1199      action,
   1200      [secondaryAction],
   1201      options
   1202    );
   1203 
   1204    removeNotificationOnEnd(popup, installInfo.installs);
   1205 
   1206    Glean.securityUi.events.accumulateSingleSample(
   1207      Ci.nsISecurityUITelemetry.WARNING_CONFIRM_ADDON_INSTALL
   1208    );
   1209  },
   1210 
   1211  // IDs of addon install related notifications, passed by this file
   1212  // (browser-addons.js) to PopupNotifications.show(). The only exception is
   1213  // "addon-webext-permissions" (from browser/modules/ExtensionsUI.sys.mjs),
   1214  // which can not only be triggered during add-on installation, but also
   1215  // later, when the extension uses the browser.permissions.request() API.
   1216  NOTIFICATION_IDS: [
   1217    "addon-install-blocked",
   1218    "addon-install-confirmation",
   1219    "addon-install-failed",
   1220    "addon-install-origin-blocked",
   1221    "addon-install-webapi-blocked",
   1222    "addon-install-policy-blocked",
   1223    "addon-progress",
   1224    "addon-webext-permissions",
   1225    "xpinstall-disabled",
   1226  ],
   1227 
   1228  /**
   1229   * Remove all opened addon installation notifications
   1230   *
   1231   * @param {*} browser - Browser to remove notifications for
   1232   * @returns {boolean} - true if notifications have been removed.
   1233   */
   1234  removeAllNotifications(browser) {
   1235    let notifications = this.NOTIFICATION_IDS.map(id =>
   1236      PopupNotifications.getNotification(id, browser)
   1237    ).filter(notification => notification != null);
   1238 
   1239    PopupNotifications.remove(notifications, true);
   1240 
   1241    return !!notifications.length;
   1242  },
   1243 
   1244  logWarningFullScreenInstallBlocked() {
   1245    // If notifications have been removed, log a warning to the website console
   1246    let consoleMsg = Cc["@mozilla.org/scripterror;1"].createInstance(
   1247      Ci.nsIScriptError
   1248    );
   1249    const message = lazy.l10n.formatValueSync(
   1250      "addon-install-full-screen-blocked"
   1251    );
   1252    consoleMsg.initWithWindowID(
   1253      message,
   1254      gBrowser.currentURI.spec,
   1255      0,
   1256      0,
   1257      Ci.nsIScriptError.warningFlag,
   1258      "FullScreen",
   1259      gBrowser.selectedBrowser.innerWindowID
   1260    );
   1261    Services.console.logMessage(consoleMsg);
   1262  },
   1263 
   1264  async observe(aSubject, aTopic) {
   1265    var installInfo = aSubject.wrappedJSObject;
   1266    var browser = installInfo.browser;
   1267 
   1268    // Make sure the browser is still alive.
   1269    if (!browser || !gBrowser.browsers.includes(browser)) {
   1270      return;
   1271    }
   1272 
   1273    // Make notifications persistent
   1274    var options = {
   1275      displayURI: installInfo.originatingURI,
   1276      persistent: true,
   1277      hideClose: true,
   1278      timeout: Date.now() + 30000,
   1279      popupOptions: {
   1280        position: "bottomright topright",
   1281      },
   1282    };
   1283 
   1284    switch (aTopic) {
   1285      case "addon-install-disabled": {
   1286        let msgId, action, secondaryActions;
   1287        if (Services.prefs.prefIsLocked("xpinstall.enabled")) {
   1288          msgId = "xpinstall-disabled-by-policy";
   1289          action = null;
   1290          secondaryActions = null;
   1291        } else {
   1292          msgId = "xpinstall-disabled";
   1293          const [disabledMsg, cancelMsg] = await lazy.l10n.formatMessages([
   1294            "xpinstall-disabled-button",
   1295            "addon-install-cancel-button",
   1296          ]);
   1297          action = buildNotificationAction(disabledMsg, () => {
   1298            Services.prefs.setBoolPref("xpinstall.enabled", true);
   1299          });
   1300          secondaryActions = [buildNotificationAction(cancelMsg, () => {})];
   1301        }
   1302 
   1303        PopupNotifications.show(
   1304          browser,
   1305          "xpinstall-disabled",
   1306          await lazy.l10n.formatValue(msgId),
   1307          gUnifiedExtensions.getPopupAnchorID(browser, window),
   1308          action,
   1309          secondaryActions,
   1310          options
   1311        );
   1312        break;
   1313      }
   1314      case "addon-install-fullscreen-blocked": {
   1315        // AddonManager denied installation because we are in DOM fullscreen
   1316        this.logWarningFullScreenInstallBlocked();
   1317        break;
   1318      }
   1319      case "addon-install-webapi-blocked":
   1320      case "addon-install-policy-blocked":
   1321      case "addon-install-origin-blocked": {
   1322        const msgId =
   1323          aTopic == "addon-install-policy-blocked"
   1324            ? "addon-install-domain-blocked-by-policy"
   1325            : "xpinstall-prompt";
   1326        let messageString = await lazy.l10n.formatValue(msgId);
   1327        if (Services.policies) {
   1328          let extensionSettings = Services.policies.getExtensionSettings("*");
   1329          if (
   1330            extensionSettings &&
   1331            "blocked_install_message" in extensionSettings
   1332          ) {
   1333            messageString += " " + extensionSettings.blocked_install_message;
   1334          }
   1335        }
   1336 
   1337        options.removeOnDismissal = true;
   1338        options.persistent = false;
   1339        Glean.securityUi.events.accumulateSingleSample(
   1340          Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED
   1341        );
   1342        let popup = PopupNotifications.show(
   1343          browser,
   1344          aTopic,
   1345          messageString,
   1346          gUnifiedExtensions.getPopupAnchorID(browser, window),
   1347          null,
   1348          null,
   1349          options
   1350        );
   1351        removeNotificationOnEnd(popup, installInfo.installs);
   1352        break;
   1353      }
   1354      case "addon-install-blocked": {
   1355        // Dismiss the progress notification.  Note that this is bad if
   1356        // there are multiple simultaneous installs happening, see
   1357        // bug 1329884 for a longer explanation.
   1358        let progressNotification = PopupNotifications.getNotification(
   1359          "addon-progress",
   1360          browser
   1361        );
   1362        if (progressNotification) {
   1363          progressNotification.remove();
   1364        }
   1365 
   1366        // The informational content differs somewhat for site permission
   1367        // add-ons. AOM no longer supports installing multiple addons,
   1368        // so the array handling here is vestigial.
   1369        let isSitePermissionAddon = installInfo.installs.every(
   1370          ({ addon }) => addon?.type === lazy.SITEPERMS_ADDON_TYPE
   1371        );
   1372        let hasHost = false;
   1373        let headerId, msgId;
   1374        if (isSitePermissionAddon) {
   1375          // At present, WebMIDI is the only consumer of the site permission
   1376          // add-on infrastructure, and so we can hard-code a midi string here.
   1377          // If and when we use it for other things, we'll need to plumb that
   1378          // information through. See bug 1826747.
   1379          headerId = "site-permission-install-first-prompt-midi-header";
   1380          msgId = "site-permission-install-first-prompt-midi-message";
   1381        } else if (options.displayURI) {
   1382          // PopupNotifications.show replaces <> with options.name.
   1383          headerId = { id: "xpinstall-prompt-header", args: { host: "<>" } };
   1384          // BrowserUIUtils.getLocalizedFragment replaces %1$S with options.name.
   1385          msgId = { id: "xpinstall-prompt-message", args: { host: "%1$S" } };
   1386          options.name = options.displayURI.displayHost;
   1387          hasHost = true;
   1388        } else {
   1389          headerId = "xpinstall-prompt-header-unknown";
   1390          msgId = "xpinstall-prompt-message-unknown";
   1391        }
   1392        const [headerString, msgString] = await lazy.l10n.formatValues([
   1393          headerId,
   1394          msgId,
   1395        ]);
   1396 
   1397        // displayURI becomes it's own label, so we unset it for this panel. It will become part of the
   1398        // messageString above.
   1399        let displayURI = options.displayURI;
   1400        options.displayURI = undefined;
   1401 
   1402        options.eventCallback = topic => {
   1403          if (topic !== "showing") {
   1404            return;
   1405          }
   1406          let doc = browser.ownerDocument;
   1407          let message = doc.getElementById("addon-install-blocked-message");
   1408          // We must remove any prior use of this panel message in this window.
   1409          while (message.firstChild) {
   1410            message.firstChild.remove();
   1411          }
   1412 
   1413          if (!hasHost) {
   1414            message.textContent = msgString;
   1415          } else {
   1416            let b = doc.createElementNS("http://www.w3.org/1999/xhtml", "b");
   1417            b.textContent = options.name;
   1418            let fragment = BrowserUIUtils.getLocalizedFragment(
   1419              doc,
   1420              msgString,
   1421              b
   1422            );
   1423            message.appendChild(fragment);
   1424          }
   1425 
   1426          let article = isSitePermissionAddon
   1427            ? "site-permission-addons"
   1428            : "unlisted-extensions-risks";
   1429          let learnMore = doc.getElementById("addon-install-blocked-info");
   1430          learnMore.setAttribute("support-page", article);
   1431        };
   1432        Glean.securityUi.events.accumulateSingleSample(
   1433          Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED
   1434        );
   1435 
   1436        const [
   1437          installMsg,
   1438          dontAllowMsg,
   1439          neverAllowMsg,
   1440          neverAllowAndReportMsg,
   1441        ] = await lazy.l10n.formatMessages([
   1442          "xpinstall-prompt-install",
   1443          "xpinstall-prompt-dont-allow",
   1444          "xpinstall-prompt-never-allow",
   1445          "xpinstall-prompt-never-allow-and-report",
   1446        ]);
   1447 
   1448        const action = buildNotificationAction(installMsg, () => {
   1449          Glean.securityUi.events.accumulateSingleSample(
   1450            Ci.nsISecurityUITelemetry
   1451              .WARNING_ADDON_ASKING_PREVENTED_CLICK_THROUGH
   1452          );
   1453          installInfo.install();
   1454        });
   1455 
   1456        const neverAllowCallback = () => {
   1457          SitePermissions.setForPrincipal(
   1458            browser.contentPrincipal,
   1459            "install",
   1460            SitePermissions.BLOCK
   1461          );
   1462          for (let install of installInfo.installs) {
   1463            if (install.state != AddonManager.STATE_CANCELLED) {
   1464              install.cancel();
   1465            }
   1466          }
   1467          if (installInfo.cancel) {
   1468            installInfo.cancel();
   1469          }
   1470        };
   1471 
   1472        const declineActions = [
   1473          buildNotificationAction(dontAllowMsg, () => {
   1474            for (let install of installInfo.installs) {
   1475              if (install.state != AddonManager.STATE_CANCELLED) {
   1476                install.cancel();
   1477              }
   1478            }
   1479            if (installInfo.cancel) {
   1480              installInfo.cancel();
   1481            }
   1482          }),
   1483          buildNotificationAction(neverAllowMsg, neverAllowCallback),
   1484        ];
   1485 
   1486        if (isSitePermissionAddon) {
   1487          // Restrict this to site permission add-ons for now pending a decision
   1488          // from product about how to approach this for extensions.
   1489          declineActions.push(
   1490            buildNotificationAction(neverAllowAndReportMsg, () => {
   1491              AMTelemetry.recordSuspiciousSiteEvent({ displayURI });
   1492              neverAllowCallback();
   1493            })
   1494          );
   1495        }
   1496 
   1497        let popup = PopupNotifications.show(
   1498          browser,
   1499          aTopic,
   1500          headerString,
   1501          gUnifiedExtensions.getPopupAnchorID(browser, window),
   1502          action,
   1503          declineActions,
   1504          options
   1505        );
   1506        removeNotificationOnEnd(popup, installInfo.installs);
   1507        break;
   1508      }
   1509      case "addon-install-started": {
   1510        // If all installs have already been downloaded then there is no need to
   1511        // show the download progress
   1512        if (
   1513          installInfo.installs.every(
   1514            aInstall => aInstall.state == AddonManager.STATE_DOWNLOADED
   1515          )
   1516        ) {
   1517          return;
   1518        }
   1519 
   1520        const messageString = lazy.l10n.formatValueSync(
   1521          "addon-downloading-and-verifying",
   1522          { addonCount: installInfo.installs.length }
   1523        );
   1524        options.installs = installInfo.installs;
   1525        options.contentWindow = browser.contentWindow;
   1526        options.sourceURI = browser.currentURI;
   1527        options.eventCallback = function (aEvent) {
   1528          switch (aEvent) {
   1529            case "removed":
   1530              options.contentWindow = null;
   1531              options.sourceURI = null;
   1532              break;
   1533          }
   1534        };
   1535 
   1536        const [acceptMsg, cancelMsg] = lazy.l10n.formatMessagesSync([
   1537          "addon-install-accept-button",
   1538          "addon-install-cancel-button",
   1539        ]);
   1540 
   1541        const action = buildNotificationAction(acceptMsg, () => {});
   1542        action.disabled = true;
   1543 
   1544        const secondaryAction = buildNotificationAction(cancelMsg, () => {
   1545          for (let install of installInfo.installs) {
   1546            if (install.state != AddonManager.STATE_CANCELLED) {
   1547              install.cancel();
   1548            }
   1549          }
   1550        });
   1551 
   1552        let notification = PopupNotifications.show(
   1553          browser,
   1554          "addon-progress",
   1555          messageString,
   1556          gUnifiedExtensions.getPopupAnchorID(browser, window),
   1557          action,
   1558          [secondaryAction],
   1559          options
   1560        );
   1561        notification._startTime = Date.now();
   1562 
   1563        break;
   1564      }
   1565      case "addon-install-failed": {
   1566        options.removeOnDismissal = true;
   1567        options.persistent = false;
   1568 
   1569        // TODO This isn't terribly ideal for the multiple failure case
   1570        for (let install of installInfo.installs) {
   1571          let host;
   1572          try {
   1573            host = options.displayURI.host;
   1574          } catch (e) {
   1575            // displayURI might be missing or 'host' might throw for non-nsStandardURL nsIURIs.
   1576          }
   1577 
   1578          if (!host) {
   1579            host =
   1580              install.sourceURI instanceof Ci.nsIStandardURL &&
   1581              install.sourceURI.host;
   1582          }
   1583 
   1584          let messageString;
   1585          if (
   1586            install.addon &&
   1587            !Services.policies.mayInstallAddon(install.addon)
   1588          ) {
   1589            messageString = lazy.l10n.formatValueSync(
   1590              "addon-installation-blocked-by-policy",
   1591              { addonName: install.name, addonId: install.addon.id }
   1592            );
   1593            let extensionSettings = Services.policies.getExtensionSettings(
   1594              install.addon.id
   1595            );
   1596            if (
   1597              extensionSettings &&
   1598              "blocked_install_message" in extensionSettings
   1599            ) {
   1600              messageString += " " + extensionSettings.blocked_install_message;
   1601            }
   1602          } else {
   1603            // TODO bug 1834484: simplify computation of isLocal.
   1604            const isLocal = !host;
   1605            const fluentIds = ERROR_L10N_IDS.get(install.error);
   1606            // We need to find the group of fluent IDs to use (error-id, local-error-id),
   1607            // depending on whether we have the add-on name or not.
   1608            const offset = fluentIds?.length === 4 && !install.name ? 2 : 0;
   1609            let errorId = fluentIds?.[offset + isLocal ? 1 : 0];
   1610            const args = {
   1611              addonName: install.name,
   1612              appVersion: Services.appinfo.version,
   1613            };
   1614            // TODO: Bug 1846725 - when there is no error ID (which shouldn't
   1615            // happen but... we never know) we use the "incompatible" error
   1616            // message for now but we should have a better error message
   1617            // instead.
   1618            if (!errorId) {
   1619              errorId = "addon-install-error-incompatible";
   1620            }
   1621            messageString = lazy.l10n.formatValueSync(errorId, args);
   1622          }
   1623 
   1624          // Add Learn More link when refusing to install an unsigned add-on
   1625          if (install.error == AddonManager.ERROR_SIGNEDSTATE_REQUIRED) {
   1626            options.learnMoreURL =
   1627              Services.urlFormatter.formatURLPref("app.support.baseURL") +
   1628              "unsigned-addons";
   1629          }
   1630 
   1631          let notificationId = aTopic;
   1632 
   1633          const isBlocklistError = [
   1634            AddonManager.ERROR_BLOCKLISTED,
   1635            AddonManager.ERROR_SOFT_BLOCKED,
   1636          ].includes(install.error);
   1637 
   1638          // On blocklist-related install failures:
   1639          // - use "addon-install-failed-blocklist" as the notificationId
   1640          //   (which will use the popupnotification with id
   1641          //   "addon-install-failed-blocklist-notification" defined
   1642          //   in popup-notification.inc)
   1643          // - add an eventCallback that will take care of filling in the
   1644          //   blocklistURL into the href attribute of the link element
   1645          //   with id "addon-install-failed-blocklist-info"
   1646          if (isBlocklistError) {
   1647            const blocklistURL = await install.addon?.getBlocklistURL();
   1648            notificationId = `${aTopic}-blocklist`;
   1649            options.eventCallback = topic => {
   1650              if (topic !== "showing") {
   1651                return;
   1652              }
   1653              let doc = browser.ownerDocument;
   1654              let blocklistURLEl = doc.getElementById(
   1655                "addon-install-failed-blocklist-info"
   1656              );
   1657              if (blocklistURL) {
   1658                blocklistURLEl.setAttribute("href", blocklistURL);
   1659              } else {
   1660                blocklistURLEl.removeAttribute("href");
   1661              }
   1662            };
   1663          }
   1664 
   1665          PopupNotifications.show(
   1666            browser,
   1667            notificationId,
   1668            messageString,
   1669            gUnifiedExtensions.getPopupAnchorID(browser, window),
   1670            null,
   1671            null,
   1672            options
   1673          );
   1674 
   1675          // Can't have multiple notifications with the same ID, so stop here.
   1676          break;
   1677        }
   1678        this._removeProgressNotification(browser);
   1679        break;
   1680      }
   1681      case "addon-install-confirmation": {
   1682        let showNotification = () => {
   1683          let height = undefined;
   1684 
   1685          if (PopupNotifications.isPanelOpen) {
   1686            let rect = window.windowUtils.getBoundsWithoutFlushing(
   1687              document.getElementById("addon-progress-notification")
   1688            );
   1689            height = rect.height;
   1690          }
   1691 
   1692          this._removeProgressNotification(browser);
   1693          this.showInstallConfirmation(browser, installInfo, height);
   1694        };
   1695 
   1696        let progressNotification = PopupNotifications.getNotification(
   1697          "addon-progress",
   1698          browser
   1699        );
   1700        if (progressNotification) {
   1701          let downloadDuration = Date.now() - progressNotification._startTime;
   1702          let securityDelay =
   1703            Services.prefs.getIntPref("security.dialog_enable_delay") -
   1704            downloadDuration;
   1705          if (securityDelay > 0) {
   1706            setTimeout(() => {
   1707              // The download may have been cancelled during the security delay
   1708              if (
   1709                PopupNotifications.getNotification("addon-progress", browser)
   1710              ) {
   1711                showNotification();
   1712              }
   1713            }, securityDelay);
   1714            break;
   1715          }
   1716        }
   1717        showNotification();
   1718        break;
   1719      }
   1720    }
   1721  },
   1722  _removeProgressNotification(aBrowser) {
   1723    let notification = PopupNotifications.getNotification(
   1724      "addon-progress",
   1725      aBrowser
   1726    );
   1727    if (notification) {
   1728      notification.remove();
   1729    }
   1730  },
   1731 };
   1732 
   1733 var gExtensionsNotifications = {
   1734  initialized: false,
   1735  init() {
   1736    this.updateAlerts();
   1737    this.boundUpdate = this.updateAlerts.bind(this);
   1738    ExtensionsUI.on("change", this.boundUpdate);
   1739    this.initialized = true;
   1740  },
   1741 
   1742  uninit() {
   1743    // uninit() can race ahead of init() in some cases, if that happens,
   1744    // we have no handler to remove.
   1745    if (!this.initialized) {
   1746      return;
   1747    }
   1748    ExtensionsUI.off("change", this.boundUpdate);
   1749  },
   1750 
   1751  _createAddonButton(l10nId, addon, callback) {
   1752    let text = addon
   1753      ? lazy.l10n.formatValueSync(l10nId, { addonName: addon.name })
   1754      : lazy.l10n.formatValueSync(l10nId);
   1755    let button = document.createXULElement("toolbarbutton");
   1756    button.setAttribute("id", l10nId);
   1757    button.setAttribute("wrap", "true");
   1758    button.setAttribute("label", text);
   1759    button.setAttribute("tooltiptext", text);
   1760    const DEFAULT_EXTENSION_ICON =
   1761      "chrome://mozapps/skin/extensions/extensionGeneric.svg";
   1762    button.setAttribute("image", addon?.iconURL || DEFAULT_EXTENSION_ICON);
   1763    button.className = "addon-banner-item subviewbutton";
   1764 
   1765    button.addEventListener("command", callback);
   1766    PanelUI.addonNotificationContainer.appendChild(button);
   1767  },
   1768 
   1769  updateAlerts() {
   1770    let sideloaded = ExtensionsUI.sideloaded;
   1771    let updates = ExtensionsUI.updates;
   1772 
   1773    let container = PanelUI.addonNotificationContainer;
   1774 
   1775    while (container.firstChild) {
   1776      container.firstChild.remove();
   1777    }
   1778 
   1779    let items = 0;
   1780    if (lazy.AMBrowserExtensionsImport.canCompleteOrCancelInstalls) {
   1781      this._createAddonButton("webext-imported-addons", null, () => {
   1782        lazy.AMBrowserExtensionsImport.completeInstalls();
   1783      });
   1784      items++;
   1785    }
   1786 
   1787    for (let update of updates) {
   1788      if (++items > 4) {
   1789        break;
   1790      }
   1791      this._createAddonButton(
   1792        "webext-perms-update-menu-item",
   1793        update.addon,
   1794        () => {
   1795          ExtensionsUI.showUpdate(gBrowser, update);
   1796        }
   1797      );
   1798    }
   1799 
   1800    for (let addon of sideloaded) {
   1801      if (++items > 4) {
   1802        break;
   1803      }
   1804      this._createAddonButton("webext-perms-sideload-menu-item", addon, () => {
   1805        // We need to hide the main menu manually because the toolbarbutton is
   1806        // removed immediately while processing this event, and PanelUI is
   1807        // unable to identify which panel should be closed automatically.
   1808        PanelUI.hide();
   1809        ExtensionsUI.showSideloaded(gBrowser, addon);
   1810      });
   1811    }
   1812  },
   1813 };
   1814 
   1815 var BrowserAddonUI = {
   1816  async promptRemoveExtension(addon) {
   1817    let { name } = addon;
   1818    let [title, btnTitle] = await lazy.l10n.formatValues([
   1819      { id: "addon-removal-title", args: { name } },
   1820      { id: "addon-removal-button" },
   1821    ]);
   1822 
   1823    let {
   1824      BUTTON_TITLE_IS_STRING: titleString,
   1825      BUTTON_TITLE_CANCEL: titleCancel,
   1826      BUTTON_POS_0,
   1827      BUTTON_POS_1,
   1828      confirmEx,
   1829    } = Services.prompt;
   1830    let btnFlags = BUTTON_POS_0 * titleString + BUTTON_POS_1 * titleCancel;
   1831 
   1832    // Enable abuse report checkbox in the remove extension dialog,
   1833    // if enabled by the about:config prefs and the addon type
   1834    // is currently supported.
   1835    let checkboxMessage = null;
   1836    if (
   1837      gAddonAbuseReportEnabled &&
   1838      ["extension", "theme"].includes(addon.type)
   1839    ) {
   1840      checkboxMessage = await lazy.l10n.formatValue(
   1841        "addon-removal-abuse-report-checkbox"
   1842      );
   1843    }
   1844 
   1845    // If the prompt is being used for ML model removal, use a body message
   1846    let body = null;
   1847    if (addon.type === "mlmodel") {
   1848      body = await lazy.l10n.formatValue("addon-mlmodel-removal-body");
   1849    }
   1850 
   1851    let checkboxState = { value: false };
   1852    let result = confirmEx(
   1853      window,
   1854      title,
   1855      body,
   1856      btnFlags,
   1857      btnTitle,
   1858      /* button1 */ null,
   1859      /* button2 */ null,
   1860      checkboxMessage,
   1861      checkboxState
   1862    );
   1863 
   1864    return { remove: result === 0, report: checkboxState.value };
   1865  },
   1866 
   1867  async reportAddon(addonId, _reportEntryPoint) {
   1868    let addon = addonId && (await AddonManager.getAddonByID(addonId));
   1869    if (!addon) {
   1870      return;
   1871    }
   1872 
   1873    const amoUrl = lazy.AbuseReporter.getAMOFormURL({ addonId });
   1874    window.openTrustedLinkIn(amoUrl, "tab", {
   1875      // Make sure the newly open tab is going to be focused, independently
   1876      // from general user prefs.
   1877      forceForeground: true,
   1878    });
   1879  },
   1880 
   1881  async removeAddon(addonId) {
   1882    let addon = addonId && (await AddonManager.getAddonByID(addonId));
   1883    if (!addon || !(addon.permissions & AddonManager.PERM_CAN_UNINSTALL)) {
   1884      return;
   1885    }
   1886 
   1887    let { remove, report } = await this.promptRemoveExtension(addon);
   1888 
   1889    if (remove) {
   1890      // Leave the extension in pending uninstall if we are also reporting the
   1891      // add-on.
   1892      await addon.uninstall(report);
   1893 
   1894      if (report) {
   1895        await this.reportAddon(addon.id, "uninstall");
   1896      }
   1897    }
   1898  },
   1899 
   1900  async manageAddon(addonId) {
   1901    let addon = addonId && (await AddonManager.getAddonByID(addonId));
   1902    if (!addon) {
   1903      return;
   1904    }
   1905 
   1906    this.openAddonsMgr("addons://detail/" + encodeURIComponent(addon.id));
   1907  },
   1908 
   1909  /**
   1910   * Open about:addons page by given view id.
   1911   *
   1912   * @param {string} aView
   1913   *                 View id of page that will open.
   1914   *                 e.g. "addons://discover/"
   1915   * @param {object} options
   1916   *        {
   1917   *          selectTabByViewId: If true, if there is the tab opening page having
   1918   *                             same view id, select the tab. Else if the current
   1919   *                             page is blank, load on it. Otherwise, open a new
   1920   *                             tab, then load on it.
   1921   *                             If false, if there is the tab opening
   1922   *                             about:addoons page, select the tab and load page
   1923   *                             for view id on it. Otherwise, leave the loading
   1924   *                             behavior to switchToTabHavingURI().
   1925   *                             If no options, handles as false.
   1926   *        }
   1927   * @returns {Promise} When the Promise resolves, returns window object loaded the
   1928   *                    view id.
   1929   */
   1930  openAddonsMgr(aView, { selectTabByViewId = false } = {}) {
   1931    return new Promise(resolve => {
   1932      let emWindow;
   1933      let browserWindow;
   1934 
   1935      const receivePong = function (aSubject) {
   1936        const browserWin = aSubject.browsingContext.topChromeWindow;
   1937        if (!emWindow || browserWin == window /* favor the current window */) {
   1938          if (
   1939            selectTabByViewId &&
   1940            aSubject.gViewController.currentViewId !== aView
   1941          ) {
   1942            return;
   1943          }
   1944 
   1945          emWindow = aSubject;
   1946          browserWindow = browserWin;
   1947        }
   1948      };
   1949      Services.obs.addObserver(receivePong, "EM-pong");
   1950      Services.obs.notifyObservers(null, "EM-ping");
   1951      Services.obs.removeObserver(receivePong, "EM-pong");
   1952 
   1953      if (emWindow) {
   1954        if (aView && !selectTabByViewId) {
   1955          emWindow.loadView(aView);
   1956        }
   1957        let tab = browserWindow.gBrowser.getTabForBrowser(
   1958          emWindow.docShell.chromeEventHandler
   1959        );
   1960        browserWindow.gBrowser.selectedTab = tab;
   1961        emWindow.focus();
   1962        resolve(emWindow);
   1963        return;
   1964      }
   1965 
   1966      if (selectTabByViewId) {
   1967        const target = isBlankPageURL(gBrowser.currentURI.spec)
   1968          ? "current"
   1969          : "tab";
   1970        openTrustedLinkIn("about:addons", target);
   1971      } else {
   1972        // This must be a new load, else the ping/pong would have
   1973        // found the window above.
   1974        switchToTabHavingURI("about:addons", true);
   1975      }
   1976 
   1977      Services.obs.addObserver(function observer(aSubject, aTopic) {
   1978        Services.obs.removeObserver(observer, aTopic);
   1979        if (aView) {
   1980          aSubject.loadView(aView);
   1981        }
   1982        aSubject.focus();
   1983        resolve(aSubject);
   1984      }, "EM-loaded");
   1985    });
   1986  },
   1987 };
   1988 
   1989 // We must declare `gUnifiedExtensions` using `var` below to avoid a
   1990 // "redeclaration" syntax error.
   1991 var gUnifiedExtensions = {
   1992  _initialized: false,
   1993  // buttonAlwaysVisible: true, -- based on pref, declared later.
   1994  _buttonShownBeforeButtonOpen: null,
   1995  _buttonBarHasMouse: false,
   1996 
   1997  // We use a `<deck>` in the extension items to show/hide messages below each
   1998  // extension name. We have a default message for origin controls, and
   1999  // optionally a second message shown on hover, which describes the action
   2000  // (when clicking on the action button). We have another message shown when
   2001  // the menu button is hovered/focused. The constants below define the indexes
   2002  // of each message in the `<deck>`.
   2003  MESSAGE_DECK_INDEX_DEFAULT: 0,
   2004  MESSAGE_DECK_INDEX_HOVER: 1,
   2005  MESSAGE_DECK_INDEX_MENU_HOVER: 2,
   2006 
   2007  init() {
   2008    if (this._initialized) {
   2009      return;
   2010    }
   2011 
   2012    // Button is hidden by default, declared in navigator-toolbox.inc.xhtml.
   2013    this._button = document.getElementById("unified-extensions-button");
   2014    this._navbar = document.getElementById("nav-bar");
   2015    this.updateButtonVisibility();
   2016    this._buttonAttrObs = new MutationObserver(() => this.onButtonOpenChange());
   2017    this._buttonAttrObs.observe(this._button, { attributeFilter: ["open"] });
   2018    this._button.addEventListener("PopupNotificationsBeforeAnchor", this);
   2019    this._updateButtonBarListeners();
   2020 
   2021    gBrowser.addTabsProgressListener(this);
   2022    window.addEventListener("TabSelect", () => this.updateAttention());
   2023    window.addEventListener("toolbarvisibilitychange", this);
   2024 
   2025    this.permListener = () => this.updateAttention();
   2026    lazy.ExtensionPermissions.addListener(this.permListener);
   2027 
   2028    this.onAppMenuShowing = this.onAppMenuShowing.bind(this);
   2029    PanelUI.mainView.addEventListener("ViewShowing", this.onAppMenuShowing);
   2030    gNavToolbox.addEventListener("customizationstarting", this);
   2031    gNavToolbox.addEventListener("aftercustomization", this);
   2032    CustomizableUI.addListener(this);
   2033    AddonManager.addManagerListener(this);
   2034 
   2035    Glean.extensionsButton.prefersHiddenButton.set(!this.buttonAlwaysVisible);
   2036 
   2037    // Listen out for changes in extensions.hideNoScript and
   2038    // extension.hideUnifiedWhenEmpty, which can effect the visibility of the
   2039    // unified-extensions-button.
   2040    // See tor-browser#41581.
   2041    this._hideNoScriptObserver = () => this._updateHideEmpty();
   2042    Services.prefs.addObserver(HIDE_NO_SCRIPT_PREF, this._hideNoScriptObserver);
   2043    Services.prefs.addObserver(
   2044      HIDE_UNIFIED_WHEN_EMPTY_PREF,
   2045      this._hideNoScriptObserver
   2046    );
   2047    this._updateHideEmpty(); // Will trigger updateButtonVisibility;
   2048 
   2049    this._initialized = true;
   2050  },
   2051 
   2052  uninit() {
   2053    if (!this._initialized) {
   2054      return;
   2055    }
   2056 
   2057    this._buttonAttrObs.disconnect();
   2058    this._button.removeEventListener("PopupNotificationsBeforeAnchor", this);
   2059 
   2060    window.removeEventListener("toolbarvisibilitychange", this);
   2061 
   2062    lazy.ExtensionPermissions.removeListener(this.permListener);
   2063    this.permListener = null;
   2064 
   2065    PanelUI.mainView.removeEventListener("ViewShowing", this.onAppMenuShowing);
   2066    gNavToolbox.removeEventListener("customizationstarting", this);
   2067    gNavToolbox.removeEventListener("aftercustomization", this);
   2068    CustomizableUI.removeListener(this);
   2069    AddonManager.removeManagerListener(this);
   2070 
   2071    Services.prefs.removeObserver(
   2072      HIDE_NO_SCRIPT_PREF,
   2073      this._hideNoScriptObserver
   2074    );
   2075    Services.prefs.removeObserver(
   2076      HIDE_UNIFIED_WHEN_EMPTY_PREF,
   2077      this._hideNoScriptObserver
   2078    );
   2079  },
   2080 
   2081  _updateButtonBarListeners() {
   2082    // Called from init() and when the buttonAlwaysVisible flag changes.
   2083    //
   2084    // We don't expect the user to be interacting with the Extensions Button or
   2085    // the navbar when the buttonAlwaysVisible flag changes. Still, we reset
   2086    // the _buttonBarHasMouse flag to false to make sure that the button can be
   2087    // hidden eventually if there are no other triggers:
   2088    // - on registration, we don't know whether the mouse is on the navbar.
   2089    // - after unregistration, the flag is no longer maintained, and false is a
   2090    //   safe default value.
   2091    this._buttonBarHasMouse = false;
   2092    // We need mouse listeners on _navbar to maintain _buttonBarHasMouse,
   2093    // but only if the button is conditionally visible/hidden.
   2094    if (this.buttonAlwaysVisible) {
   2095      this._navbar.removeEventListener("mouseover", this);
   2096      this._navbar.removeEventListener("mouseout", this);
   2097    } else {
   2098      this._navbar.addEventListener("mouseover", this);
   2099      this._navbar.addEventListener("mouseout", this);
   2100    }
   2101  },
   2102 
   2103  onBlocklistAttentionUpdated() {
   2104    this.updateAttention();
   2105  },
   2106 
   2107  onAppMenuShowing() {
   2108    // Only show the extension menu item if the extension button is not pinned
   2109    // and the extension popup is not empty.
   2110    // NOTE: This condition is different than _shouldShowButton.
   2111    const hideExtensionItem = this.buttonAlwaysVisible || this._hideEmpty;
   2112    document.getElementById("appMenu-extensions-themes-button").hidden =
   2113      !hideExtensionItem;
   2114    document.getElementById("appMenu-unified-extensions-button").hidden =
   2115      hideExtensionItem;
   2116  },
   2117 
   2118  onLocationChange(browser, webProgress, _request, _uri, flags) {
   2119    // Only update on top-level cross-document navigations in the selected tab.
   2120    if (
   2121      webProgress.isTopLevel &&
   2122      browser === gBrowser.selectedBrowser &&
   2123      !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)
   2124    ) {
   2125      this.updateAttention();
   2126    }
   2127  },
   2128 
   2129  updateButtonVisibility() {
   2130    if (this._hideEmpty === null) {
   2131      return;
   2132    }
   2133    // TODO: Bug 1778684 - Auto-hide button when there is no active extension.
   2134    // Hide the extension button when it is empty. See tor-browser#41581.
   2135    // Likely will conflict with mozilla's Bug 1778684. See tor-browser#42635.
   2136    let shouldShowButton =
   2137      this._shouldShowButton ||
   2138      // If anything is anchored to the button, keep it visible.
   2139      this._button.open ||
   2140      // Button will be open soon - see ensureButtonShownBeforeAttachingPanel.
   2141      this._buttonShownBeforeButtonOpen ||
   2142      // Items in the toolbar shift when the button hides. To prevent the user
   2143      // from clicking on something different than they intended, never hide an
   2144      // already-visible button while the mouse is still in the toolbar.
   2145      (!this.button.hidden && this._buttonBarHasMouse) ||
   2146      // Attention dot - see comment at buttonIgnoresAttention.
   2147      (!this.buttonIgnoresAttention && this.button.hasAttribute("attention")) ||
   2148      // Always show when customizing, because even if the button should mostly
   2149      // be hidden, the user should be able to specify the desired location for
   2150      // cases where the button is forcibly shown.
   2151      CustomizationHandler.isCustomizing();
   2152 
   2153    if (shouldShowButton) {
   2154      this._button.hidden = false;
   2155      this._navbar.setAttribute("unifiedextensionsbuttonshown", true);
   2156    } else {
   2157      this._button.hidden = true;
   2158      this._navbar.removeAttribute("unifiedextensionsbuttonshown");
   2159    }
   2160  },
   2161 
   2162  ensureButtonShownBeforeAttachingPanel(panel) {
   2163    if (!this._shouldShowButton && !this._button.open) {
   2164      // When the panel is anchored to the button, its "open" attribute will be
   2165      // set, which visually renders as a "button pressed". Until we get there,
   2166      // we need to make sure that the button is visible so that it can serve
   2167      // as anchor.
   2168      this._buttonShownBeforeButtonOpen = panel;
   2169      this.updateButtonVisibility();
   2170    }
   2171  },
   2172 
   2173  onButtonOpenChange() {
   2174    if (this._button.open) {
   2175      this._buttonShownBeforeButtonOpen = false;
   2176    }
   2177    if (!this._shouldShowButton && !this._button.open) {
   2178      this.updateButtonVisibility();
   2179    }
   2180  },
   2181 
   2182  // Update the attention indicator for the whole unified extensions button.
   2183  updateAttention() {
   2184    let permissionsAttention = false;
   2185    let quarantinedAttention = false;
   2186    let blocklistAttention = AddonManager.shouldShowBlocklistAttention();
   2187 
   2188    // Computing the OriginControls state for all active extensions is potentially
   2189    // more expensive, and so we don't compute it if we have already determined that
   2190    // there is a blocklist attention to be shown.
   2191    if (!blocklistAttention) {
   2192      for (let policy of this.getActivePolicies()) {
   2193        let widget = this.browserActionFor(policy)?.widget;
   2194 
   2195        // Only show for extensions which are not already visible in the toolbar.
   2196        if (!widget || widget.areaType !== CustomizableUI.TYPE_TOOLBAR) {
   2197          if (lazy.OriginControls.getAttentionState(policy, window).attention) {
   2198            permissionsAttention = true;
   2199            break;
   2200          }
   2201        }
   2202      }
   2203 
   2204      // If the domain is quarantined and we have extensions not allowed, we'll
   2205      // show a notification in the panel so we want to let the user know about
   2206      // it.
   2207      quarantinedAttention = this._shouldShowQuarantinedNotification();
   2208    }
   2209 
   2210    this.button.toggleAttribute(
   2211      "attention",
   2212      quarantinedAttention || permissionsAttention || blocklistAttention
   2213    );
   2214    let msgId = permissionsAttention
   2215      ? "unified-extensions-button-permissions-needed"
   2216      : "unified-extensions-button";
   2217    // Quarantined state takes precedence over anything else.
   2218    if (quarantinedAttention) {
   2219      msgId = "unified-extensions-button-quarantined";
   2220    }
   2221    // blocklistAttention state takes precedence over the other ones
   2222    // because it is dismissible and, once dismissed, the tooltip will
   2223    // show one of the other messages if appropriate.
   2224    if (blocklistAttention) {
   2225      msgId = "unified-extensions-button-blocklisted";
   2226    }
   2227    this.button.ownerDocument.l10n.setAttributes(this.button, msgId);
   2228    if (!this.buttonAlwaysVisible && !this.buttonIgnoresAttention) {
   2229      if (blocklistAttention) {
   2230        this.recordButtonTelemetry("attention_blocklist");
   2231      } else if (permissionsAttention || quarantinedAttention) {
   2232        this.recordButtonTelemetry("attention_permission_denied");
   2233      }
   2234      this.updateButtonVisibility();
   2235    }
   2236  },
   2237 
   2238  // Get the anchor to use with PopupNotifications.show(). If you add a new use
   2239  // of this method, make sure to update gXPInstallObserver.NOTIFICATION_IDS!
   2240  // If the new ID is not added in NOTIFICATION_IDS, consider handling the case
   2241  // in the "PopupNotificationsBeforeAnchor" handler elsewhere in this file.
   2242  getPopupAnchorID(aBrowser, aWindow) {
   2243    const anchorID = "unified-extensions-button";
   2244    const attr = anchorID + "popupnotificationanchor";
   2245 
   2246    if (!aBrowser[attr]) {
   2247      // A hacky way of setting the popup anchor outside the usual url bar
   2248      // icon box, similar to how it was done for CFR.
   2249      // See: https://searchfox.org/mozilla-central/rev/c5c002f81f08a73e04868e0c2bf0eb113f200b03/toolkit/modules/PopupNotifications.sys.mjs#40
   2250      aBrowser[attr] = aWindow.document.getElementById(
   2251        anchorID
   2252        // Anchor on the toolbar icon to position the popup right below the
   2253        // button.
   2254      ).firstElementChild;
   2255    }
   2256 
   2257    return anchorID;
   2258  },
   2259 
   2260  get button() {
   2261    return this._button;
   2262  },
   2263 
   2264  /**
   2265   * Gets a list of active WebExtensionPolicy instances of type "extension",
   2266   * excluding hidden extensions, available to this window.
   2267   *
   2268   * @param {boolean} skipPBMCheck When false (the default), the result
   2269   *                  excludes extensions that cannot access the current window
   2270   *                  due to the window being a private browsing window that
   2271   *                  the extension is not allowed to access.
   2272   * @returns {Array<WebExtensionPolicy>} An array of active policies.
   2273   */
   2274  getActivePolicies(skipPBMCheck = false) {
   2275    let policies = WebExtensionPolicy.getActiveExtensions();
   2276    policies = policies.filter(policy => {
   2277      let { extension } = policy;
   2278      if (extension?.type !== "extension") {
   2279        // extension can only be null due to bugs (bug 1642012).
   2280        // Exclude non-extension types such as themes, dictionaries, etc.
   2281        return false;
   2282      }
   2283 
   2284      // When an extensions is about to be removed, it may still appear in
   2285      // getActiveExtensions.
   2286      // This is needed for hasExtensionsInPanel, when called through
   2287      // onWidgetDestroy when an extension is being removed.
   2288      // See tor-browser#41581.
   2289      if (extension.hasShutdown) {
   2290        return false;
   2291      }
   2292 
   2293      // Ignore hidden and extensions that cannot access the current window
   2294      // (because of PB mode when we are in a private window), since users
   2295      // cannot do anything with those extensions anyway.
   2296      if (
   2297        extension.isHidden ||
   2298        // NOTE: policy.canAccessWindow() sounds generic, but it really only
   2299        // enforces private browsing access.
   2300        (!skipPBMCheck && !policy.canAccessWindow(window))
   2301      ) {
   2302        return false;
   2303      }
   2304 
   2305      return true;
   2306    });
   2307 
   2308    return policies;
   2309  },
   2310 
   2311  /**
   2312   * Whether the extension button should be hidden because it is empty. Or
   2313   * `null` when uninitialised.
   2314   *
   2315   * @type {?boolean}
   2316   */
   2317  _hideEmpty: null,
   2318 
   2319  /**
   2320   * Update the _hideEmpty attribute when the preference or hasExtensionsInPanel
   2321   * value may have changed.
   2322   */
   2323  _updateHideEmpty() {
   2324    const prevHideEmpty = this._hideEmpty;
   2325    this._hideEmpty =
   2326      Services.prefs.getBoolPref(HIDE_UNIFIED_WHEN_EMPTY_PREF, true) &&
   2327      !this.hasExtensionsInPanel();
   2328    if (this._hideEmpty !== prevHideEmpty) {
   2329      this.updateButtonVisibility();
   2330    }
   2331  },
   2332 
   2333  /**
   2334   * Whether we should show the extension button, regardless of whether it is
   2335   * needed as a popup anchor, etc.
   2336   *
   2337   * @type {boolean}
   2338   */
   2339  get _shouldShowButton() {
   2340    return this.buttonAlwaysVisible && !this._hideEmpty;
   2341  },
   2342 
   2343  /**
   2344   * Returns true when there are active extensions listed/shown in the unified
   2345   * extensions panel, and false otherwise (e.g. when extensions are pinned in
   2346   * the toolbar OR there are 0 active extensions).
   2347   *
   2348   * @param {Array<WebExtensionPolicy> [policies] The list of extensions to
   2349   *   evaluate. Defaults to the active extensions with access to this window
   2350   *   (see getActivePolicies).
   2351   * @returns {boolean} Whether there are extensions listed in the panel.
   2352   */
   2353  hasExtensionsInPanel(policies = this.getActivePolicies()) {
   2354    const hideNoScript = Services.prefs.getBoolPref(HIDE_NO_SCRIPT_PREF, true);
   2355    return policies.some(policy => {
   2356      if (hideNoScript && policy.extension?.isNoScript) {
   2357        return false;
   2358      }
   2359      let widget = this.browserActionFor(policy)?.widget;
   2360      return (
   2361        !widget ||
   2362        widget.areaType !== CustomizableUI.TYPE_TOOLBAR ||
   2363        widget.forWindow(window).overflowed
   2364      );
   2365    });
   2366  },
   2367 
   2368  isPrivateWindowMissingExtensionsWithoutPBMAccess() {
   2369    if (!PrivateBrowsingUtils.isWindowPrivate(window)) {
   2370      return false;
   2371    }
   2372    const policies = this.getActivePolicies(/* skipPBMCheck */ true);
   2373    return policies.some(p => !p.privateBrowsingAllowed);
   2374  },
   2375 
   2376  /**
   2377   * Returns whether there is any active extension without private browsing
   2378   * access, for which the user can toggle the "Run in Private Windows" option.
   2379   * This complements the isPrivateWindowMissingExtensionsWithoutPBMAccess()
   2380   * method, by distinguishing cases where the user can enable any extension
   2381   * in the private window, vs cases where the user cannot.
   2382   *
   2383   * @returns {Promise<boolean>} Whether there is any "Run in Private Windows"
   2384   *                             option that is Off and can be set to On.
   2385   */
   2386  async isAtLeastOneExtensionWithPBMOptIn() {
   2387    const addons = await AddonManager.getAddonsByTypes(["extension"]);
   2388    return addons.some(addon => {
   2389      if (
   2390        // We only care about extensions shown in the panel and about:addons.
   2391        addon.hidden ||
   2392        // We only care about extensions whose PBM access can be toggled.
   2393        !(
   2394          addon.permissions &
   2395          lazy.AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS
   2396        )
   2397      ) {
   2398        return false;
   2399      }
   2400      const policy = WebExtensionPolicy.getByID(addon.id);
   2401      // policy can be null if the extension is not active.
   2402      return policy && !policy.privateBrowsingAllowed;
   2403    });
   2404  },
   2405 
   2406  async getDisabledExtensionsInfo() {
   2407    let addons = await AddonManager.getAddonsByTypes(["extension"]);
   2408    addons = addons.filter(a => !a.hidden && !a.isActive);
   2409    const isAnyDisabled = !!addons.length;
   2410    const isAnyEnableable = addons.some(
   2411      a => a.permissions & lazy.AddonManager.PERM_CAN_ENABLE
   2412    );
   2413    return { isAnyDisabled, isAnyEnableable };
   2414  },
   2415 
   2416  handleEvent(event) {
   2417    switch (event.type) {
   2418      case "ViewShowing":
   2419        this.onPanelViewShowing(event.target);
   2420        break;
   2421 
   2422      case "ViewHiding":
   2423        this.onPanelViewHiding(event.target);
   2424        break;
   2425 
   2426      case "PopupNotificationsBeforeAnchor":
   2427        {
   2428          const popupnotification = PopupNotifications.panel.firstElementChild;
   2429          const popupid = popupnotification?.getAttribute("popupid");
   2430          if (popupid === "addon-webext-permissions") {
   2431            // "addon-webext-permissions" is also in NOTIFICATION_IDS, but to
   2432            // distinguish it from other cases, give it a separate reason.
   2433            this.recordButtonTelemetry("extension_permission_prompt");
   2434          } else if (gXPInstallObserver.NOTIFICATION_IDS.includes(popupid)) {
   2435            this.recordButtonTelemetry("addon_install_doorhanger");
   2436          } else {
   2437            console.error(`Unrecognized notification ID: ${popupid}`);
   2438          }
   2439          this.ensureButtonShownBeforeAttachingPanel(PopupNotifications.panel);
   2440        }
   2441        break;
   2442 
   2443      case "mouseover":
   2444        this._buttonBarHasMouse = true;
   2445        break;
   2446 
   2447      case "mouseout":
   2448        if (
   2449          this._buttonBarHasMouse &&
   2450          !this._navbar.contains(event.relatedTarget)
   2451        ) {
   2452          this._buttonBarHasMouse = false;
   2453          this.updateButtonVisibility();
   2454        }
   2455        break;
   2456 
   2457      case "customizationstarting":
   2458        this.panel.hidePopup();
   2459        this.recordButtonTelemetry("customize");
   2460        this.updateButtonVisibility();
   2461        break;
   2462 
   2463      case "aftercustomization":
   2464        this.updateButtonVisibility();
   2465        break;
   2466 
   2467      case "toolbarvisibilitychange":
   2468        this.onToolbarVisibilityChange(event.target.id, event.detail.visible);
   2469        break;
   2470    }
   2471  },
   2472 
   2473  onPanelViewShowing(panelview) {
   2474    const policies = this.getActivePolicies();
   2475 
   2476    // Only add extensions that do not have a browser action in this list since
   2477    // the extensions with browser action have CUI widgets and will appear in
   2478    // the panel (or toolbar) via the CUI mechanism.
   2479    const policiesForList = policies.filter(
   2480      p => !p.extension.hasBrowserActionUI
   2481    );
   2482    policiesForList.sort((a, b) => a.name.localeCompare(b.name));
   2483 
   2484    const list = panelview.querySelector(".unified-extensions-list");
   2485    for (const policy of policiesForList) {
   2486      const item = document.createElement("unified-extensions-item");
   2487      item.setExtension(policy.extension);
   2488      list.appendChild(item);
   2489    }
   2490 
   2491    const emptyStateBox = panelview.querySelector(
   2492      "#unified-extensions-empty-state"
   2493    );
   2494    if (this.hasExtensionsInPanel(policies)) {
   2495      // Any of the extension lists are non-empty.
   2496      emptyStateBox.hidden = true;
   2497    } else if (this.isPrivateWindowMissingExtensionsWithoutPBMAccess()) {
   2498      document.l10n.setAttributes(
   2499        emptyStateBox.querySelector("h2"),
   2500        "unified-extensions-empty-reason-private-browsing-not-allowed"
   2501      );
   2502      document.l10n.setAttributes(
   2503        emptyStateBox.querySelector("description"),
   2504        "unified-extensions-empty-content-explain-enable2"
   2505      );
   2506      emptyStateBox.hidden = false;
   2507      this.isAtLeastOneExtensionWithPBMOptIn().then(result => {
   2508        // The "enable" message is somewhat misleading when the user cannot
   2509        // enable the extension, show a generic message instead (bug 1992179).
   2510        if (!result) {
   2511          document.l10n.setAttributes(
   2512            emptyStateBox.querySelector("description"),
   2513            "unified-extensions-empty-content-explain-manage2"
   2514          );
   2515        }
   2516      });
   2517    } else {
   2518      emptyStateBox.hidden = true;
   2519      this.getDisabledExtensionsInfo().then(disabledExtensionsInfo => {
   2520        if (disabledExtensionsInfo.isAnyDisabled) {
   2521          document.l10n.setAttributes(
   2522            emptyStateBox.querySelector("h2"),
   2523            "unified-extensions-empty-reason-extension-not-enabled"
   2524          );
   2525          document.l10n.setAttributes(
   2526            emptyStateBox.querySelector("description"),
   2527            disabledExtensionsInfo.isAnyEnableable
   2528              ? "unified-extensions-empty-content-explain-enable2"
   2529              : "unified-extensions-empty-content-explain-manage2"
   2530          );
   2531          emptyStateBox.hidden = false;
   2532        } else if (!policies.length) {
   2533          document.l10n.setAttributes(
   2534            emptyStateBox.querySelector("h2"),
   2535            "unified-extensions-empty-reason-zero-extensions-onboarding"
   2536          );
   2537          document.l10n.setAttributes(
   2538            emptyStateBox.querySelector("description"),
   2539            "unified-extensions-empty-content-explain-extensions-onboarding"
   2540          );
   2541          emptyStateBox.hidden = false;
   2542 
   2543          // Replace the "Manage Extensions" button with "Discover Extensions".
   2544          // We add the "Discover Extensions" button, and "Manage Extensions"
   2545          // button (#unified-extensions-manage-extensions) is hidden by CSS.
   2546          const discoverButton = this._createDiscoverButton(panelview);
   2547 
   2548          const manageExtensionsButton = panelview.querySelector(
   2549            "#unified-extensions-manage-extensions"
   2550          );
   2551          // Insert before toolbarseparator, to make it easier to hide the
   2552          // toolbarseparator and manageExtensionsButton with CSS.
   2553          manageExtensionsButton.previousElementSibling.before(discoverButton);
   2554        }
   2555      });
   2556    }
   2557 
   2558    const container = panelview.querySelector(
   2559      "#unified-extensions-messages-container"
   2560    );
   2561 
   2562    if (Services.appinfo.inSafeMode) {
   2563      this._messageBarSafemode ??= this._makeMessageBar({
   2564        messageBarFluentId: "unified-extensions-notice-safe-mode",
   2565        supportPage: "diagnose-firefox-issues-using-troubleshoot-mode",
   2566        type: "info",
   2567      });
   2568      container.prepend(this._messageBarSafemode);
   2569    } // No "else" case; inSafeMode flag is fixed at browser startup.
   2570 
   2571    if (this.blocklistAttentionInfo?.shouldShow) {
   2572      this._messageBarBlocklist = this._createBlocklistMessageBar(container);
   2573    } else {
   2574      this._messageBarBlocklist?.remove();
   2575      this._messageBarBlocklist = null;
   2576    }
   2577 
   2578    const shouldShowQuarantinedNotification =
   2579      this._shouldShowQuarantinedNotification();
   2580    if (shouldShowQuarantinedNotification) {
   2581      if (!this._messageBarQuarantinedDomain) {
   2582        this._messageBarQuarantinedDomain = this._makeMessageBar({
   2583          messageBarFluentId:
   2584            "unified-extensions-mb-quarantined-domain-message-3",
   2585          supportPage: "quarantined-domains",
   2586          supportPageFluentId:
   2587            "unified-extensions-mb-quarantined-domain-learn-more",
   2588          dismissible: false,
   2589        });
   2590        this._messageBarQuarantinedDomain
   2591          .querySelector("a")
   2592          .addEventListener("click", () => {
   2593            this.togglePanel();
   2594          });
   2595      }
   2596 
   2597      container.appendChild(this._messageBarQuarantinedDomain);
   2598    } else if (
   2599      !shouldShowQuarantinedNotification &&
   2600      this._messageBarQuarantinedDomain &&
   2601      container.contains(this._messageBarQuarantinedDomain)
   2602    ) {
   2603      container.removeChild(this._messageBarQuarantinedDomain);
   2604      this._messageBarQuarantinedDomain = null;
   2605    }
   2606  },
   2607 
   2608  onPanelViewHiding(panelview) {
   2609    if (window.closed) {
   2610      return;
   2611    }
   2612    const list = panelview.querySelector(".unified-extensions-list");
   2613    while (list.lastChild) {
   2614      list.lastChild.remove();
   2615    }
   2616    panelview
   2617      .querySelector("#unified-extensions-discover-extensions")
   2618      ?.remove();
   2619 
   2620    // If temporary access was granted, (maybe) clear attention indicator.
   2621    requestAnimationFrame(() => this.updateAttention());
   2622  },
   2623 
   2624  onToolbarVisibilityChange(toolbarId, isVisible) {
   2625    // A list of extension widget IDs (possibly empty).
   2626    let widgetIDs;
   2627 
   2628    try {
   2629      widgetIDs = CustomizableUI.getWidgetIdsInArea(toolbarId).filter(
   2630        CustomizableUI.isWebExtensionWidget
   2631      );
   2632    } catch {
   2633      // Do nothing if the area does not exist for some reason.
   2634      return;
   2635    }
   2636 
   2637    // The list of overflowed extensions in the extensions panel.
   2638    const overflowedExtensionsList = this.panel.querySelector(
   2639      "#overflowed-extensions-list"
   2640    );
   2641 
   2642    // We are going to move all the extension widgets via DOM manipulation
   2643    // *only* so that it looks like these widgets have moved (and users will
   2644    // see that) but CUI still thinks the widgets haven't been moved.
   2645    //
   2646    // We can move the extension widgets either from the toolbar to the
   2647    // extensions panel OR the other way around (when the toolbar becomes
   2648    // visible again).
   2649    for (const widgetID of widgetIDs) {
   2650      const widget = CustomizableUI.getWidget(widgetID);
   2651      if (!widget) {
   2652        continue;
   2653      }
   2654 
   2655      if (isVisible) {
   2656        this._maybeMoveWidgetNodeBack(widget.id);
   2657      } else {
   2658        const { node } = widget.forWindow(window);
   2659        // Artificially overflow the extension widget in the extensions panel
   2660        // when the toolbar is hidden.
   2661        node.setAttribute("overflowedItem", true);
   2662        node.setAttribute("artificallyOverflowed", true);
   2663        // This attribute forces browser action popups to be anchored to the
   2664        // extensions button.
   2665        node.setAttribute("cui-anchorid", "unified-extensions-button");
   2666        overflowedExtensionsList.appendChild(node);
   2667 
   2668        this._updateWidgetClassName(widgetID, /* inPanel */ true);
   2669      }
   2670    }
   2671  },
   2672 
   2673  _maybeMoveWidgetNodeBack(widgetID) {
   2674    const widget = CustomizableUI.getWidget(widgetID);
   2675    if (!widget) {
   2676      return;
   2677    }
   2678 
   2679    // We only want to move back widget nodes that have been manually moved
   2680    // previously via `onToolbarVisibilityChange()`.
   2681    const { node } = widget.forWindow(window);
   2682    if (!node.hasAttribute("artificallyOverflowed")) {
   2683      return;
   2684    }
   2685 
   2686    const { area, position } = CustomizableUI.getPlacementOfWidget(widgetID);
   2687 
   2688    // This is where we are going to re-insert the extension widgets (DOM
   2689    // nodes) but we need to account for some hidden DOM nodes already present
   2690    // in this container when determining where to put the nodes back.
   2691    const container = CustomizableUI.getCustomizationTarget(
   2692      document.getElementById(area)
   2693    );
   2694 
   2695    let moved = false;
   2696    let currentPosition = 0;
   2697 
   2698    for (const child of container.childNodes) {
   2699      const isSkipToolbarset = child.getAttribute("skipintoolbarset") == "true";
   2700      if (isSkipToolbarset && child !== container.lastChild) {
   2701        continue;
   2702      }
   2703 
   2704      if (currentPosition === position) {
   2705        child.before(node);
   2706        moved = true;
   2707        break;
   2708      }
   2709 
   2710      if (child === container.lastChild) {
   2711        child.after(node);
   2712        moved = true;
   2713        break;
   2714      }
   2715 
   2716      currentPosition++;
   2717    }
   2718 
   2719    if (moved) {
   2720      // Remove the attribute set when we artificially overflow the widget.
   2721      node.removeAttribute("overflowedItem");
   2722      node.removeAttribute("artificallyOverflowed");
   2723      node.removeAttribute("cui-anchorid");
   2724 
   2725      this._updateWidgetClassName(widgetID, /* inPanel */ false);
   2726    }
   2727  },
   2728 
   2729  _panel: null,
   2730  get panel() {
   2731    // Lazy load the unified-extensions-panel panel the first time we need to
   2732    // display it.
   2733    if (!this._panel) {
   2734      let template = document.getElementById(
   2735        "unified-extensions-panel-template"
   2736      );
   2737      template.replaceWith(template.content);
   2738      this._panel = document.getElementById("unified-extensions-panel");
   2739      let customizationArea = this._panel.querySelector(
   2740        "#unified-extensions-area"
   2741      );
   2742      CustomizableUI.registerPanelNode(
   2743        customizationArea,
   2744        CustomizableUI.AREA_ADDONS
   2745      );
   2746      CustomizableUI.addPanelCloseListeners(this._panel);
   2747 
   2748      this._panel
   2749        .querySelector("#unified-extensions-manage-extensions")
   2750        .addEventListener("command", () => {
   2751          BrowserAddonUI.openAddonsMgr("addons://list/extension");
   2752        });
   2753 
   2754      // Lazy-load the l10n strings. Those strings are used for the CUI and
   2755      // non-CUI extensions in the unified extensions panel.
   2756      document
   2757        .getElementById("unified-extensions-context-menu")
   2758        .querySelectorAll("[data-lazy-l10n-id]")
   2759        .forEach(el => {
   2760          el.setAttribute("data-l10n-id", el.getAttribute("data-lazy-l10n-id"));
   2761          el.removeAttribute("data-lazy-l10n-id");
   2762        });
   2763    }
   2764    return this._panel;
   2765  },
   2766 
   2767  // `aEvent` and `reason` are optional. If `reason` is specified, it should be
   2768  // a valid argument to gUnifiedExtensions.recordButtonTelemetry().
   2769  async togglePanel(aEvent, reason) {
   2770    if (!CustomizationHandler.isCustomizing()) {
   2771      if (aEvent) {
   2772        if (
   2773          // On MacOS, ctrl-click will send a context menu event from the
   2774          // widget, so we don't want to bring up the panel when ctrl key is
   2775          // pressed.
   2776          (aEvent.type == "mousedown" &&
   2777            (aEvent.button !== 0 ||
   2778              (AppConstants.platform === "macosx" && aEvent.ctrlKey))) ||
   2779          (aEvent.type === "keypress" &&
   2780            aEvent.charCode !== KeyEvent.DOM_VK_SPACE &&
   2781            aEvent.keyCode !== KeyEvent.DOM_VK_RETURN)
   2782        ) {
   2783          return;
   2784        }
   2785 
   2786        // The button should directly open `about:addons` when the user does not
   2787        // have any active extensions listed in the unified extensions panel,
   2788        // and no alternative content is available for display in the panel.
   2789        const policies = this.getActivePolicies();
   2790        if (
   2791          policies.length &&
   2792          !this.hasExtensionsInPanel(policies) &&
   2793          !this.isPrivateWindowMissingExtensionsWithoutPBMAccess() &&
   2794          !(await this.getDisabledExtensionsInfo()).isAnyDisabled
   2795        ) {
   2796          // This may happen if the user has pinned all of their extensions.
   2797          // In that case, the extensions panel is empty.
   2798          await BrowserAddonUI.openAddonsMgr("addons://list/extension");
   2799          return;
   2800        }
   2801      }
   2802 
   2803      this.blocklistAttentionInfo =
   2804        await AddonManager.getBlocklistAttentionInfo();
   2805 
   2806      let panel = this.panel;
   2807 
   2808      if (!this._listView) {
   2809        this._listView = PanelMultiView.getViewNode(
   2810          document,
   2811          "unified-extensions-view"
   2812        );
   2813        this._listView.addEventListener("ViewShowing", this);
   2814        this._listView.addEventListener("ViewHiding", this);
   2815      }
   2816 
   2817      if (this._button.open) {
   2818        PanelMultiView.hidePopup(panel);
   2819        this._button.open = false;
   2820      } else {
   2821        // Overflow extensions placed in collapsed toolbars, if any.
   2822        for (const toolbarId of CustomizableUI.getCollapsedToolbarIds(window)) {
   2823          // We pass `false` because all these toolbars are collapsed.
   2824          this.onToolbarVisibilityChange(toolbarId, /* isVisible */ false);
   2825        }
   2826 
   2827        panel.hidden = false;
   2828        this.recordButtonTelemetry(reason || "extensions_panel_showing");
   2829        this.ensureButtonShownBeforeAttachingPanel(panel);
   2830        PanelMultiView.openPopup(panel, this._button, {
   2831          position: "bottomright topright",
   2832          triggerEvent: aEvent,
   2833        });
   2834      }
   2835    }
   2836 
   2837    // We always dispatch an event (useful for testing purposes).
   2838    window.dispatchEvent(new CustomEvent("UnifiedExtensionsTogglePanel"));
   2839  },
   2840 
   2841  async openPanel(event, reason) {
   2842    if (this._button.open) {
   2843      throw new Error("Tried to open panel whilst a panel was already open!");
   2844    }
   2845    if (CustomizationHandler.isCustomizing()) {
   2846      throw new Error("Cannot open panel while in Customize mode!");
   2847    }
   2848 
   2849    if (event?.sourceEvent?.target.id === "appMenu-unified-extensions-button") {
   2850      Glean.extensionsButton.openViaAppMenu.record({
   2851        is_extensions_panel_empty: !this.hasExtensionsInPanel(),
   2852        is_extensions_button_visible: !this._button.hidden,
   2853      });
   2854    }
   2855 
   2856    await this.togglePanel(event, reason);
   2857  },
   2858 
   2859  updateContextMenu(menu, event) {
   2860    // When the context menu is open, `onpopupshowing` is called when menu
   2861    // items open sub-menus. We don't want to update the context menu in this
   2862    // case.
   2863    if (event.target.id !== "unified-extensions-context-menu") {
   2864      return;
   2865    }
   2866 
   2867    const id = this._getExtensionId(menu);
   2868    const widgetId = this._getWidgetId(menu);
   2869    const forBrowserAction = !!widgetId;
   2870 
   2871    const pinButton = menu.querySelector(
   2872      ".unified-extensions-context-menu-pin-to-toolbar"
   2873    );
   2874    const removeButton = menu.querySelector(
   2875      ".unified-extensions-context-menu-remove-extension"
   2876    );
   2877    const reportButton = menu.querySelector(
   2878      ".unified-extensions-context-menu-report-extension"
   2879    );
   2880    const menuSeparator = menu.querySelector(
   2881      ".unified-extensions-context-menu-management-separator"
   2882    );
   2883    const moveUp = menu.querySelector(
   2884      ".unified-extensions-context-menu-move-widget-up"
   2885    );
   2886    const moveDown = menu.querySelector(
   2887      ".unified-extensions-context-menu-move-widget-down"
   2888    );
   2889 
   2890    for (const element of [menuSeparator, pinButton, moveUp, moveDown]) {
   2891      element.hidden = !forBrowserAction;
   2892    }
   2893 
   2894    reportButton.hidden = !gAddonAbuseReportEnabled;
   2895    // We use this syntax instead of async/await to not block this method that
   2896    // updates the context menu. This avoids the context menu to be out of sync
   2897    // on macOS.
   2898    AddonManager.getAddonByID(id).then(addon => {
   2899      removeButton.disabled = !(
   2900        addon.permissions & AddonManager.PERM_CAN_UNINSTALL
   2901      );
   2902    });
   2903 
   2904    if (forBrowserAction) {
   2905      let area = CustomizableUI.getPlacementOfWidget(widgetId).area;
   2906      let inToolbar = area != CustomizableUI.AREA_ADDONS;
   2907      pinButton.toggleAttribute("checked", inToolbar);
   2908 
   2909      const placement = CustomizableUI.getPlacementOfWidget(widgetId);
   2910      const notInPanel = placement?.area !== CustomizableUI.AREA_ADDONS;
   2911      // We rely on the DOM nodes because CUI widgets will always exist but
   2912      // not necessarily with DOM nodes created depending on the window. For
   2913      // example, in PB mode, not all extensions will be listed in the panel
   2914      // but the CUI widgets may be all created.
   2915      if (
   2916        notInPanel ||
   2917        document.querySelector("#unified-extensions-area > :first-child")
   2918          ?.id === widgetId
   2919      ) {
   2920        moveUp.hidden = true;
   2921      }
   2922 
   2923      if (
   2924        notInPanel ||
   2925        document.querySelector("#unified-extensions-area > :last-child")?.id ===
   2926          widgetId
   2927      ) {
   2928        moveDown.hidden = true;
   2929      }
   2930    }
   2931 
   2932    ExtensionsUI.originControlsMenu(menu, id);
   2933 
   2934    const browserAction = this.browserActionFor(WebExtensionPolicy.getByID(id));
   2935    if (browserAction) {
   2936      browserAction.updateContextMenu(menu);
   2937    }
   2938  },
   2939 
   2940  // This is registered on the top-level unified extensions context menu.
   2941  onContextMenuCommand(menu, event) {
   2942    // Do not close the extensions panel automatically when we move extension
   2943    // widgets.
   2944    const { classList } = event.target;
   2945    if (
   2946      classList.contains("unified-extensions-context-menu-move-widget-up") ||
   2947      classList.contains("unified-extensions-context-menu-move-widget-down")
   2948    ) {
   2949      return;
   2950    }
   2951 
   2952    this.togglePanel();
   2953  },
   2954 
   2955  browserActionFor(policy) {
   2956    // Ideally, we wouldn't do that because `browserActionFor()` will only be
   2957    // defined in `global` when at least one extension has required loading the
   2958    // `ext-browserAction` code.
   2959    let method = lazy.ExtensionParent.apiManager.global.browserActionFor;
   2960    return method?.(policy?.extension);
   2961  },
   2962 
   2963  async manageExtension(menu) {
   2964    const id = this._getExtensionId(menu);
   2965 
   2966    await BrowserAddonUI.manageAddon(id, "unifiedExtensions");
   2967  },
   2968 
   2969  async removeExtension(menu) {
   2970    const id = this._getExtensionId(menu);
   2971 
   2972    await BrowserAddonUI.removeAddon(id, "unifiedExtensions");
   2973  },
   2974 
   2975  async reportExtension(menu) {
   2976    const id = this._getExtensionId(menu);
   2977 
   2978    await BrowserAddonUI.reportAddon(id, "unified_context_menu");
   2979  },
   2980 
   2981  _getExtensionId(menu) {
   2982    const { triggerNode } = menu;
   2983    return triggerNode
   2984      .closest(".unified-extensions-item")
   2985      ?.querySelector("toolbarbutton")?.dataset.extensionid;
   2986  },
   2987 
   2988  _getWidgetId(menu) {
   2989    const { triggerNode } = menu;
   2990    return triggerNode.closest(".unified-extensions-item")?.id;
   2991  },
   2992 
   2993  async onPinToToolbarChange(menu, event) {
   2994    let shouldPinToToolbar = event.target.hasAttribute("checked");
   2995    // Revert the checkbox back to its original state. This is because the
   2996    // addon context menu handlers are asynchronous, and there seems to be
   2997    // a race where the checkbox state won't get set in time to show the
   2998    // right state. So we err on the side of caution, and presume that future
   2999    // attempts to open this context menu on an extension button will show
   3000    // the same checked state that we started in.
   3001    event.target.toggleAttribute("checked", !shouldPinToToolbar);
   3002 
   3003    let widgetId = this._getWidgetId(menu);
   3004    if (!widgetId) {
   3005      return;
   3006    }
   3007 
   3008    // We artificially overflow extension widgets that are placed in collapsed
   3009    // toolbars and CUI does not know about it. For end users, these widgets
   3010    // appear in the list of overflowed extensions in the panel. When we unpin
   3011    // and then pin one of these extensions to the toolbar, we need to first
   3012    // move the DOM node back to where it was (i.e.  in the collapsed toolbar)
   3013    // so that CUI can retrieve the DOM node and do the pinning correctly.
   3014    if (shouldPinToToolbar) {
   3015      this._maybeMoveWidgetNodeBack(widgetId);
   3016    }
   3017 
   3018    this.pinToToolbar(widgetId, shouldPinToToolbar);
   3019  },
   3020 
   3021  pinToToolbar(widgetId, shouldPinToToolbar) {
   3022    let newArea = shouldPinToToolbar
   3023      ? CustomizableUI.AREA_NAVBAR
   3024      : CustomizableUI.AREA_ADDONS;
   3025    let newPosition = shouldPinToToolbar ? undefined : 0;
   3026 
   3027    CustomizableUI.addWidgetToArea(widgetId, newArea, newPosition);
   3028    // addWidgetToArea() will trigger onWidgetAdded or onWidgetMoved as needed,
   3029    // and our handlers will call updateAttention() as needed.
   3030  },
   3031 
   3032  async moveWidget(menu, direction) {
   3033    // We'll move the widgets based on the DOM node positions. This is because
   3034    // in PB mode (for example), we might not have the same extensions listed
   3035    // in the panel but CUI does not know that. As far as CUI is concerned, all
   3036    // extensions will likely have widgets.
   3037    const node = menu.triggerNode.closest(".unified-extensions-item");
   3038 
   3039    // Find the element that is before or after the current widget/node to
   3040    // move. `element` might be `null`, e.g. if the current node is the first
   3041    // one listed in the panel (though it shouldn't be possible to call this
   3042    // method in this case).
   3043    let element;
   3044    if (direction === "up" && node.previousElementSibling) {
   3045      element = node.previousElementSibling;
   3046    } else if (direction === "down" && node.nextElementSibling) {
   3047      element = node.nextElementSibling;
   3048    }
   3049 
   3050    // Now we need to retrieve the position of the CUI placement.
   3051    const placement = CustomizableUI.getPlacementOfWidget(element?.id);
   3052    if (placement) {
   3053      let newPosition = placement.position;
   3054      // That, I am not sure why this is required but it looks like we need to
   3055      // always add one to the current position if we want to move a widget
   3056      // down in the list.
   3057      if (direction === "down") {
   3058        newPosition += 1;
   3059      }
   3060 
   3061      CustomizableUI.moveWidgetWithinArea(node.id, newPosition);
   3062    }
   3063  },
   3064 
   3065  onWidgetRemoved() {
   3066    // hasExtensionsInPanel may have changed.
   3067    this._updateHideEmpty();
   3068  },
   3069 
   3070  onWidgetDestroyed() {
   3071    // hasExtensionsInPanel may have changed.
   3072    this._updateHideEmpty();
   3073  },
   3074 
   3075  onWidgetAdded(aWidgetId, aArea) {
   3076    // hasExtensionsInPanel may have changed.
   3077    this._updateHideEmpty();
   3078 
   3079    if (CustomizableUI.isWebExtensionWidget(aWidgetId)) {
   3080      this.updateAttention();
   3081    }
   3082 
   3083    // When we pin a widget to the toolbar from a narrow window, the widget
   3084    // will be overflowed directly. In this case, we do not want to change the
   3085    // class name since it is going to be changed by `onWidgetOverflow()`
   3086    // below.
   3087    if (CustomizableUI.getWidget(aWidgetId)?.forWindow(window)?.overflowed) {
   3088      return;
   3089    }
   3090 
   3091    const inPanel =
   3092      CustomizableUI.getAreaType(aArea) !== CustomizableUI.TYPE_TOOLBAR;
   3093 
   3094    this._updateWidgetClassName(aWidgetId, inPanel);
   3095  },
   3096 
   3097  onWidgetMoved(aWidgetId) {
   3098    if (CustomizableUI.isWebExtensionWidget(aWidgetId)) {
   3099      this.updateAttention();
   3100    }
   3101  },
   3102 
   3103  onWidgetOverflow(aNode) {
   3104    // hasExtensionsInPanel may have changed.
   3105    this._updateHideEmpty();
   3106 
   3107    // We register a CUI listener for each window so we make sure that we
   3108    // handle the event for the right window here.
   3109    if (window !== aNode.ownerGlobal) {
   3110      return;
   3111    }
   3112 
   3113    this._updateWidgetClassName(aNode.getAttribute("widget-id"), true);
   3114  },
   3115 
   3116  onWidgetUnderflow(aNode) {
   3117    // hasExtensionsInPanel may have changed.
   3118    this._updateHideEmpty();
   3119 
   3120    // We register a CUI listener for each window so we make sure that we
   3121    // handle the event for the right window here.
   3122    if (window !== aNode.ownerGlobal) {
   3123      return;
   3124    }
   3125 
   3126    this._updateWidgetClassName(aNode.getAttribute("widget-id"), false);
   3127  },
   3128 
   3129  onAreaNodeRegistered(aArea, aContainer) {
   3130    // We register a CUI listener for each window so we make sure that we
   3131    // handle the event for the right window here.
   3132    if (window !== aContainer.ownerGlobal) {
   3133      return;
   3134    }
   3135 
   3136    const inPanel =
   3137      CustomizableUI.getAreaType(aArea) !== CustomizableUI.TYPE_TOOLBAR;
   3138 
   3139    for (const widgetId of CustomizableUI.getWidgetIdsInArea(aArea)) {
   3140      this._updateWidgetClassName(widgetId, inPanel);
   3141    }
   3142  },
   3143 
   3144  // This internal method is used to change some CSS classnames on the action
   3145  // and menu buttons of an extension (CUI) widget. When the widget is placed
   3146  // in the panel, the action and menu buttons should have the `.subviewbutton`
   3147  // class and not the `.toolbarbutton-1` one. When NOT placed in the panel,
   3148  // it is the other way around.
   3149  _updateWidgetClassName(aWidgetId, inPanel) {
   3150    if (!CustomizableUI.isWebExtensionWidget(aWidgetId)) {
   3151      return;
   3152    }
   3153 
   3154    const node = CustomizableUI.getWidget(aWidgetId)?.forWindow(window)?.node;
   3155    const actionButton = node?.querySelector(
   3156      ".unified-extensions-item-action-button"
   3157    );
   3158    if (actionButton) {
   3159      actionButton.classList.toggle("subviewbutton", inPanel);
   3160      actionButton.classList.toggle("subviewbutton-iconic", inPanel);
   3161      actionButton.classList.toggle("toolbarbutton-1", !inPanel);
   3162    }
   3163    const menuButton = node?.querySelector(
   3164      ".unified-extensions-item-menu-button"
   3165    );
   3166    if (menuButton) {
   3167      menuButton.classList.toggle("subviewbutton", inPanel);
   3168      menuButton.classList.toggle("subviewbutton-iconic", inPanel);
   3169      menuButton.classList.toggle("toolbarbutton-1", !inPanel);
   3170    }
   3171  },
   3172 
   3173  _createBlocklistMessageBar(container) {
   3174    if (!this.blocklistAttentionInfo) {
   3175      return null;
   3176    }
   3177 
   3178    const { addons, extensionsCount, hasHardBlocked } =
   3179      this.blocklistAttentionInfo;
   3180    const type = hasHardBlocked ? "error" : "warning";
   3181 
   3182    let messageBarFluentId;
   3183    let extensionName;
   3184    if (extensionsCount === 1) {
   3185      extensionName = addons[0].name;
   3186      messageBarFluentId = hasHardBlocked
   3187        ? "unified-extensions-mb-blocklist-error-single"
   3188        : "unified-extensions-mb-blocklist-warning-single2";
   3189    } else {
   3190      messageBarFluentId = hasHardBlocked
   3191        ? "unified-extensions-mb-blocklist-error-multiple"
   3192        : "unified-extensions-mb-blocklist-warning-multiple2";
   3193    }
   3194 
   3195    const messageBarBlocklist = this._makeMessageBar({
   3196      dismissible: true,
   3197      linkToAboutAddons: true,
   3198      messageBarFluentId,
   3199      messageBarFluentArgs: {
   3200        extensionsCount,
   3201        extensionName,
   3202      },
   3203      type,
   3204    });
   3205 
   3206    messageBarBlocklist.addEventListener(
   3207      "message-bar:user-dismissed",
   3208      () => {
   3209        if (messageBarBlocklist === this._messageBarBlocklist) {
   3210          this._messageBarBlocklist = null;
   3211        }
   3212        this.blocklistAttentionInfo?.dismiss();
   3213      },
   3214      { once: true }
   3215    );
   3216 
   3217    if (
   3218      this._messageBarBlocklist &&
   3219      container.contains(this._messageBarBlocklist)
   3220    ) {
   3221      container.replaceChild(messageBarBlocklist, this._messageBarBlocklist);
   3222    } else if (container.contains(this._messageBarQuarantinedDomain)) {
   3223      container.insertBefore(
   3224        messageBarBlocklist,
   3225        this._messageBarQuarantinedDomain
   3226      );
   3227    } else {
   3228      container.appendChild(messageBarBlocklist);
   3229    }
   3230 
   3231    return messageBarBlocklist;
   3232  },
   3233 
   3234  _makeMessageBar({
   3235    dismissible = false,
   3236    messageBarFluentId,
   3237    messageBarFluentArgs,
   3238    supportPage = null,
   3239    supportPageFluentId,
   3240    linkToAboutAddons = false,
   3241    type = "warning",
   3242  }) {
   3243    const messageBar = document.createElement("moz-message-bar");
   3244    messageBar.setAttribute("type", type);
   3245    messageBar.classList.add("unified-extensions-message-bar");
   3246 
   3247    if (dismissible) {
   3248      // NOTE: the moz-message-bar is currently expected to be called `dismissable`.
   3249      messageBar.setAttribute("dismissable", dismissible);
   3250    }
   3251 
   3252    if (linkToAboutAddons) {
   3253      const linkToAboutAddonsEl = document.createElement("a");
   3254      linkToAboutAddonsEl.setAttribute(
   3255        "class",
   3256        "unified-extensions-link-to-aboutaddons"
   3257      );
   3258      linkToAboutAddonsEl.setAttribute("slot", "support-link");
   3259      linkToAboutAddonsEl.addEventListener("click", () => {
   3260        BrowserAddonUI.openAddonsMgr("addons://list/extension");
   3261        this.togglePanel();
   3262      });
   3263      document.l10n.setAttributes(
   3264        linkToAboutAddonsEl,
   3265        "unified-extensions-mb-about-addons-link"
   3266      );
   3267      messageBar.append(linkToAboutAddonsEl);
   3268    }
   3269 
   3270    document.l10n.setAttributes(
   3271      messageBar,
   3272      messageBarFluentId,
   3273      messageBarFluentArgs
   3274    );
   3275 
   3276    if (supportPage) {
   3277      const supportUrl = document.createElement("a", {
   3278        is: "moz-support-link",
   3279      });
   3280      supportUrl.setAttribute("support-page", supportPage);
   3281      if (supportPageFluentId) {
   3282        document.l10n.setAttributes(supportUrl, supportPageFluentId);
   3283      }
   3284      supportUrl.setAttribute("slot", "support-link");
   3285 
   3286      messageBar.append(supportUrl);
   3287    }
   3288 
   3289    return messageBar;
   3290  },
   3291 
   3292  _createDiscoverButton() {
   3293    const discoverButton = document.createElement("moz-button");
   3294    discoverButton.id = "unified-extensions-discover-extensions";
   3295    discoverButton.type = "primary";
   3296    discoverButton.className = "subviewbutton panel-subview-footer-button";
   3297    document.l10n.setAttributes(
   3298      discoverButton,
   3299      "unified-extensions-discover-extensions"
   3300    );
   3301 
   3302    discoverButton.addEventListener("click", () => {
   3303      if (
   3304        // The "Discover Extensions" button is only shown if the user has not
   3305        // installed any extension. In that case, we direct to the discopane
   3306        // in about:addons. If the discopane is disabled, open the default
   3307        // view (Extensions list) instead. This view shows a link to AMO when
   3308        // the user does not have any extensions installed.
   3309        Services.prefs.getBoolPref("extensions.getAddons.showPane", true)
   3310      ) {
   3311        BrowserAddonUI.openAddonsMgr("addons://list/discover");
   3312      } else {
   3313        BrowserAddonUI.openAddonsMgr("addons://list/extension");
   3314      }
   3315      // Close panel.
   3316      this.togglePanel();
   3317    });
   3318 
   3319    return discoverButton;
   3320  },
   3321 
   3322  _shouldShowQuarantinedNotification() {
   3323    const { currentURI, selectedTab } = window.gBrowser;
   3324    // We should show the quarantined notification when the domain is in the
   3325    // list of quarantined domains and we have at least one extension
   3326    // quarantined. In addition, we check that we have extensions in the panel
   3327    // until Bug 1778684 is resolved.
   3328    return (
   3329      WebExtensionPolicy.isQuarantinedURI(currentURI) &&
   3330      this.hasExtensionsInPanel() &&
   3331      this.getActivePolicies().some(
   3332        policy => lazy.OriginControls.getState(policy, selectedTab).quarantined
   3333      )
   3334    );
   3335  },
   3336 
   3337  // Records telemetry when the button is about to temporarily be shown,
   3338  // provided that the button is hidden at the time of invocation.
   3339  //
   3340  // `reason` is one of the labels in extensions_button.temporarily_unhidden
   3341  // in browser/components/extensions/metrics.yaml.
   3342  //
   3343  // This is usually immediately before a updateButtonVisibility() call,
   3344  // sometimes a bit earlier (if the updateButtonVisibility() call is indirect).
   3345  recordButtonTelemetry(reason) {
   3346    if (!this.buttonAlwaysVisible && this._button.hidden) {
   3347      Glean.extensionsButton.temporarilyUnhidden[reason].add();
   3348    }
   3349  },
   3350 
   3351  hideExtensionsButtonFromToolbar() {
   3352    // All browser windows will observe this and call updateButtonVisibility().
   3353    Services.prefs.setBoolPref(
   3354      "extensions.unifiedExtensions.button.always_visible",
   3355      false
   3356    );
   3357    ConfirmationHint.show(
   3358      document.getElementById("PanelUI-menu-button"),
   3359      "confirmation-hint-extensions-button-hidden"
   3360    );
   3361    Glean.extensionsButton.toggleVisibility.record({
   3362      is_customizing: CustomizationHandler.isCustomizing(),
   3363      is_extensions_panel_empty: !this.hasExtensionsInPanel(),
   3364      // After setting the above pref to false, the button should hide
   3365      // immediately. If this was not the case, then something caused the
   3366      // button to be shown temporarily.
   3367      is_temporarily_shown: !this._button.hidden,
   3368      should_hide: true,
   3369    });
   3370  },
   3371 
   3372  showExtensionsButtonInToolbar() {
   3373    let wasShownBefore = !this.buttonAlwaysVisible && !this._button.hidden;
   3374    // All browser windows will observe this and call updateButtonVisibility().
   3375    Services.prefs.setBoolPref(
   3376      "extensions.unifiedExtensions.button.always_visible",
   3377      true
   3378    );
   3379    Glean.extensionsButton.toggleVisibility.record({
   3380      is_customizing: CustomizationHandler.isCustomizing(),
   3381      is_extensions_panel_empty: !this.hasExtensionsInPanel(),
   3382      is_temporarily_shown: wasShownBefore,
   3383      should_hide: false,
   3384    });
   3385  },
   3386 };
   3387 XPCOMUtils.defineLazyPreferenceGetter(
   3388  gUnifiedExtensions,
   3389  "buttonAlwaysVisible",
   3390  "extensions.unifiedExtensions.button.always_visible",
   3391  true,
   3392  (prefName, oldValue, newValue) => {
   3393    if (gUnifiedExtensions._initialized) {
   3394      gUnifiedExtensions._updateButtonBarListeners();
   3395      gUnifiedExtensions.updateButtonVisibility();
   3396      Glean.extensionsButton.prefersHiddenButton.set(!newValue);
   3397    }
   3398  }
   3399 );
   3400 // With button.always_visible is false, we still show the button in specific
   3401 // cases when needed. The user is always empowered to dismiss the specific
   3402 // trigger that causes the button to be shown. The attention dot is the
   3403 // exception, where the button cannot easily be hidden. Users who willingly
   3404 // want to ignore the attention dot can set this preference to keep the button
   3405 // hidden even if attention is requested.
   3406 XPCOMUtils.defineLazyPreferenceGetter(
   3407  gUnifiedExtensions,
   3408  "buttonIgnoresAttention",
   3409  "extensions.unifiedExtensions.button.ignore_attention",
   3410  false,
   3411  () => {
   3412    if (
   3413      gUnifiedExtensions._initialized &&
   3414      !gUnifiedExtensions.buttonAlwaysVisible
   3415    ) {
   3416      gUnifiedExtensions.updateButtonVisibility();
   3417    }
   3418  }
   3419 );