/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/**
 * Utility object to handle manipulations of the identity permission indicators
 * in the UI.
 */
var gPermissionPanel = {
  _popupInitialized: false,
  _initializePopup() {
    if (!this._popupInitialized) {
      let wrapper = document.getElementById("template-permission-popup");
      wrapper.replaceWith(wrapper.content);
      this._popupInitialized = true;
      this._permissionPopup.addEventListener("popupshown", this);
      this._permissionPopup.addEventListener("popuphidden", this);
    }
  },

  hidePopup() {
    if (this._popupInitialized) {
      PanelMultiView.hidePopup(this._permissionPopup);
    }
  },

  /**
   * _popupAnchorNode will be set by setAnchor if an outside consumer
   * of this object wants to override the default anchor for the panel.
   * If there is no override, this remains null, and the _identityPermissionBox
   * will be used as the anchor.
   */
  _popupAnchorNode: null,
  _popupPosition: "bottomleft topleft",
  setAnchor(anchorNode, popupPosition) {
    this._popupAnchorNode = anchorNode;
    this._popupPosition = popupPosition;
  },

  // smart getters
  get _popupAnchor() {
    if (this._popupAnchorNode) {
      return this._popupAnchorNode;
    }
    return this._identityPermissionBox;
  },
  get _identityPermissionBox() {
    delete this._identityPermissionBox;
    return (this._identityPermissionBox = document.getElementById(
      "identity-permission-box"
    ));
  },
  get _permissionGrantedIcon() {
    delete this._permissionGrantedIcon;
    return (this._permissionGrantedIcon = document.getElementById(
      "permissions-granted-icon"
    ));
  },
  get _permissionPopup() {
    if (!this._popupInitialized) {
      return null;
    }
    delete this._permissionPopup;
    return (this._permissionPopup =
      document.getElementById("permission-popup"));
  },
  get _permissionPopupMainView() {
    delete this._permissionPopupPopupMainView;
    return (this._permissionPopupPopupMainView = document.getElementById(
      "permission-popup-mainView"
    ));
  },
  get _permissionPopupMainViewHeaderLabel() {
    delete this._permissionPopupMainViewHeaderLabel;
    return (this._permissionPopupMainViewHeaderLabel = document.getElementById(
      "permission-popup-mainView-panel-header-span"
    ));
  },
  get _permissionList() {
    delete this._permissionList;
    return (this._permissionList = document.getElementById(
      "permission-popup-permission-list"
    ));
  },
  get _defaultPermissionAnchor() {
    delete this._defaultPermissionAnchor;
    return (this._defaultPermissionAnchor = document.getElementById(
      "permission-popup-permission-list-default-anchor"
    ));
  },
  get _permissionReloadHint() {
    delete this._permissionReloadHint;
    return (this._permissionReloadHint = document.getElementById(
      "permission-popup-permission-reload-hint"
    ));
  },
  get _permissionAnchors() {
    delete this._permissionAnchors;
    let permissionAnchors = {};
    for (let anchor of document.getElementById("blocked-permissions-container")
      .children) {
      permissionAnchors[anchor.getAttribute("data-permission-id")] = anchor;
    }
    return (this._permissionAnchors = permissionAnchors);
  },

  get _geoSharingIcon() {
    delete this._geoSharingIcon;
    return (this._geoSharingIcon = document.getElementById("geo-sharing-icon"));
  },

  get _xrSharingIcon() {
    delete this._xrSharingIcon;
    return (this._xrSharingIcon = document.getElementById("xr-sharing-icon"));
  },

  get _webRTCSharingIcon() {
    delete this._webRTCSharingIcon;
    return (this._webRTCSharingIcon = document.getElementById(
      "webrtc-sharing-icon"
    ));
  },

  /**
   * Refresh the contents of the permission popup. This includes the headline
   * and the list of permissions.
   */
  _refreshPermissionPopup() {
    let host = gIdentityHandler.getHostForDisplay();

    // Update header label
    this._permissionPopupMainViewHeaderLabel.textContent =
      gNavigatorBundle.getFormattedString("permissions.header", [host]);

    // Refresh the permission list
    this.updateSitePermissions();
  },

  /**
   * Called by gIdentityHandler to hide permission icons for invalid proxy
   * state.
   */
  hidePermissionIcons() {
    this._identityPermissionBox.removeAttribute("hasPermissions");
  },

  /**
   * Updates the permissions icons in the identity block.
   * We show icons for blocked permissions / popups.
   */
  refreshPermissionIcons() {
    let permissionAnchors = this._permissionAnchors;

    // hide all permission icons
    for (let icon of Object.values(permissionAnchors)) {
      icon.removeAttribute("showing");
    }

    // keeps track if we should show an indicator that there are active permissions
    let hasPermissions = false;

    // show permission icons
    let permissions = SitePermissions.getAllForBrowser(
      gBrowser.selectedBrowser
    );
    for (let permission of permissions) {
      // Don't show persisted PROMPT permissions (unless a pref says to).
      // These would appear as "Always Ask ✖" which have utility, but might confuse
      if (
        permission.state == SitePermissions.UNKNOWN ||
        (permission.state == SitePermissions.PROMPT && !this._gumShowAlwaysAsk)
      ) {
        continue;
      }
      hasPermissions = true;

      if (
        permission.state == SitePermissions.BLOCK ||
        permission.state == SitePermissions.AUTOPLAY_BLOCKED_ALL
      ) {
        let icon = permissionAnchors[permission.id];
        if (icon) {
          icon.setAttribute("showing", "true");
        }
      }
    }

    // Show blocked popup icon in the identity-box if popups or a
    // third-party redirect is blocked irrespective of popup permission
    // capability value.
    if (
      gBrowser.selectedBrowser.popupAndRedirectBlocker.getBlockedPopupCount() ||
      gBrowser.selectedBrowser.popupAndRedirectBlocker.isRedirectBlocked()
    ) {
      let icon = permissionAnchors.popup;
      icon.setAttribute("showing", "true");
      hasPermissions = true;
    }

    this._identityPermissionBox.toggleAttribute(
      "hasPermissions",
      hasPermissions
    );
  },

  /**
   * Shows the permission popup.
   *
   * @param {Event} event - Event which caused the popup to show.
   */
  openPopup(event) {
    // If we are in DOM fullscreen, exit it before showing the permission popup
    // (see bug 1557041)
    if (document.fullscreen) {
      // Open the identity popup after DOM fullscreen exit
      // We need to wait for the exit event and after that wait for the fullscreen exit transition to complete
      // If we call openPopup before the fullscreen transition ends it can get cancelled
      // Only waiting for painted is not sufficient because we could still be in the fullscreen enter transition.
      this._exitedEventReceived = false;
      this._event = event;
      Services.obs.addObserver(this, "fullscreen-painted");
      window.addEventListener(
        "MozDOMFullscreen:Exited",
        () => {
          this._exitedEventReceived = true;
        },
        { once: true }
      );
      document.exitFullscreen();
      return;
    }

    // Make the popup available.
    this._initializePopup();

    // Remove the reload hint that we show after a user has cleared a permission.
    this._permissionReloadHint.hidden = true;

    // Update the popup strings
    this._refreshPermissionPopup();

    // Check the panel state of other panels. Hide them if needed.
    let openPanels = Array.from(document.querySelectorAll("panel[openpanel]"));
    for (let panel of openPanels) {
      PanelMultiView.hidePopup(panel);
    }

    // Now open the popup, anchored off the primary chrome element
    PanelMultiView.openPopup(this._permissionPopup, this._popupAnchor, {
      position: this._popupPosition,
      triggerEvent: event,
    }).catch(console.error);
  },

  /**
   * Update identity permission indicators based on sharing state of the
   * selected tab. This should be called externally whenever the sharing state
   * of the selected tab changes.
   */
  updateSharingIndicator() {
    let tab = gBrowser.selectedTab;
    this._sharingState = tab._sharingState;

    this._webRTCSharingIcon.removeAttribute("paused");
    this._webRTCSharingIcon.removeAttribute("sharing");
    this._geoSharingIcon.removeAttribute("sharing");
    this._xrSharingIcon.removeAttribute("sharing");

    let hasSharingIcon = false;

    if (this._sharingState) {
      if (this._sharingState.webRTC) {
        if (this._sharingState.webRTC.sharing) {
          this._webRTCSharingIcon.setAttribute(
            "sharing",
            this._sharingState.webRTC.sharing
          );
          hasSharingIcon = true;

          if (this._sharingState.webRTC.paused) {
            this._webRTCSharingIcon.setAttribute("paused", "true");
          }
        } else {
          // Reflect any active permission grace periods
          let { micGrace, camGrace } = hasMicCamGracePeriodsSolely(
            gBrowser.selectedBrowser
          );
          if (micGrace || camGrace) {
            // Reuse the "paused sharing" indicator to warn about grace periods
            this._webRTCSharingIcon.setAttribute(
              "sharing",
              camGrace ? "camera" : "microphone"
            );
            hasSharingIcon = true;
            this._webRTCSharingIcon.setAttribute("paused", "true");
          }
        }
      }

      if (this._sharingState.geo) {
        this._geoSharingIcon.setAttribute("sharing", this._sharingState.geo);
        hasSharingIcon = true;
      }

      if (this._sharingState.xr) {
        this._xrSharingIcon.setAttribute("sharing", this._sharingState.xr);
        hasSharingIcon = true;
      }
    }

    this._identityPermissionBox.toggleAttribute(
      "hasSharingIcon",
      hasSharingIcon
    );

    if (this._popupInitialized && this._permissionPopup.state != "closed") {
      this.updateSitePermissions();
    }
  },

  /**
   * Click handler for the permission-box element in primary chrome.
   */
  handleIdentityButtonEvent(event) {
    event.stopPropagation();

    if (
      (event.type == "click" && event.button != 0) ||
      (event.type == "keypress" &&
        event.charCode != KeyEvent.DOM_VK_SPACE &&
        event.keyCode != KeyEvent.DOM_VK_RETURN)
    ) {
      return; // Left click, space or enter only
    }

    // Don't allow left click, space or enter if the location has been modified,
    // so long as we're not sharing any devices.
    // If we are sharing a device, the identity block is prevented by CSS from
    // being focused (and therefore, interacted with) by the user. However, we
    // want to allow opening the identity popup from the device control menu,
    // which calls click() on the identity button, so we don't return early.
    if (
      !this._sharingState &&
      gURLBar.getAttribute("pageproxystate") != "valid"
    ) {
      return;
    }

    this.openPopup(event);
  },

  handleEvent(event) {
    switch (event.type) {
      case "popupshown":
        if (event.target == this._permissionPopup) {
          window.addEventListener("focus", this, true);
        }
        break;
      case "popuphidden":
        if (event.target == this._permissionPopup) {
          window.removeEventListener("focus", this, true);
        }
        break;
      case "focus":
        {
          let elem = document.activeElement;
          let position = elem.compareDocumentPosition(this._permissionPopup);

          if (
            !(
              position &
              (Node.DOCUMENT_POSITION_CONTAINS |
                Node.DOCUMENT_POSITION_CONTAINED_BY)
            ) &&
            !this._permissionPopup.hasAttribute("noautohide")
          ) {
            // Hide the panel when focusing an element that is
            // neither an ancestor nor descendant unless the panel has
            // @noautohide (e.g. for a tour).
            PanelMultiView.hidePopup(this._permissionPopup);
          }
        }
        break;
    }
  },

  observe(subject, topic) {
    switch (topic) {
      case "fullscreen-painted": {
        if (subject != window || !this._exitedEventReceived) {
          return;
        }
        Services.obs.removeObserver(this, "fullscreen-painted");
        this.openPopup(this._event);
        delete this._event;
        break;
      }
    }
  },

  onLocationChange() {
    if (this._popupInitialized && this._permissionPopup.state != "closed") {
      this._permissionReloadHint.hidden = true;
    }
  },

  /**
   * Updates the permission list in the permissions popup.
   */
  updateSitePermissions() {
    let permissionItemSelector = [
      ".permission-popup-permission-item, .permission-popup-permission-item-container",
    ];
    this._permissionList
      .querySelectorAll(permissionItemSelector)
      .forEach(e => e.remove());
    // Used by _createPermissionItem to build unique IDs.
    this._permissionLabelIndex = 0;

    let permissions = SitePermissions.getAllPermissionDetailsForBrowser(
      gBrowser.selectedBrowser
    );

    // Don't display origin-keyed 3rdPartyStorage permissions that are covered by
    // site-keyed 3rdPartyFrameStorage permissions.
    let thirdPartyStorageSites = new Set(
      permissions
        .map(function (permission) {
          let [id, key] = permission.id.split(
            SitePermissions.PERM_KEY_DELIMITER
          );
          if (id == "3rdPartyFrameStorage") {
            return key;
          }
          return null;
        })
        .filter(function (key) {
          return key != null;
        })
    );
    permissions = permissions.filter(function (permission) {
      let [id, key] = permission.id.split(SitePermissions.PERM_KEY_DELIMITER);
      if (id != "3rdPartyStorage") {
        return true;
      }
      try {
        let origin = Services.io.newURI(key);
        let site = Services.eTLD.getSite(origin);
        return !thirdPartyStorageSites.has(site);
      } catch {
        return false;
      }
    });

    this._sharingState = gBrowser.selectedTab._sharingState;

    if (this._sharingState?.geo) {
      let geoPermission = permissions.find(perm => perm.id === "geo");
      if (geoPermission) {
        geoPermission.sharingState = true;
      } else {
        permissions.push({
          id: "geo",
          state: SitePermissions.ALLOW,
          scope: SitePermissions.SCOPE_REQUEST,
          sharingState: true,
        });
      }
    }

    if (this._sharingState?.xr) {
      let xrPermission = permissions.find(perm => perm.id === "xr");
      if (xrPermission) {
        xrPermission.sharingState = true;
      } else {
        permissions.push({
          id: "xr",
          state: SitePermissions.ALLOW,
          scope: SitePermissions.SCOPE_REQUEST,
          sharingState: true,
        });
      }
    }

    if (this._sharingState?.webRTC) {
      let webrtcState = this._sharingState.webRTC;
      // If WebRTC device or screen are in use, we need to find
      // the associated ALLOW permission item to set the sharingState field.
      for (let id of ["camera", "microphone", "screen"]) {
        if (webrtcState[id]) {
          let found = false;
          for (let permission of permissions) {
            let [permId] = permission.id.split(
              SitePermissions.PERM_KEY_DELIMITER
            );
            if (permId != id || permission.state != SitePermissions.ALLOW) {
              continue;
            }
            found = true;
            permission.sharingState = webrtcState[id];
          }
          if (!found) {
            // If the ALLOW permission item we were looking for doesn't exist,
            // the user has temporarily allowed sharing and we need to add
            // an item in the permissions array to reflect this.
            permissions.push({
              id,
              state: SitePermissions.ALLOW,
              scope: SitePermissions.SCOPE_REQUEST,
              sharingState: webrtcState[id],
            });
          }
        }
      }
    }

    let totalBlockedPopups =
      gBrowser.selectedBrowser.popupAndRedirectBlocker.getBlockedPopupCount();
    let isRedirectBlocked =
      gBrowser.selectedBrowser.popupAndRedirectBlocker.isRedirectBlocked();
    let showBlockedIndicator = totalBlockedPopups || isRedirectBlocked;

    let hasBlockedIndicator = false;
    for (let permission of permissions) {
      let [id, key] = permission.id.split(SitePermissions.PERM_KEY_DELIMITER);

      if (id == "storage-access") {
        // Ignore storage access permissions here, they are made visible inside
        // the Content Blocking UI.
        continue;
      }

      let item;
      let anchor =
        this._permissionList.querySelector(`[anchorfor="${id}"]`) ||
        this._defaultPermissionAnchor;

      if (id == "open-protocol-handler") {
        let permContainer = this._createProtocolHandlerPermissionItem(
          permission,
          key
        );
        if (permContainer) {
          anchor.appendChild(permContainer);
        }
      } else if (["camera", "screen", "microphone", "speaker"].includes(id)) {
        if (
          permission.state == SitePermissions.PROMPT &&
          !this._gumShowAlwaysAsk
        ) {
          continue;
        }
        item = this._createWebRTCPermissionItem(permission, id, key);
        if (!item) {
          continue;
        }
        anchor.appendChild(item);
      } else {
        item = this._createPermissionItem({
          permission,
          idNoSuffix: id,
          isContainer: id == "geo" || id == "xr",
          nowrapLabel: id == "3rdPartyStorage" || id == "3rdPartyFrameStorage",
        });

        // We want permission items for the 3rdPartyFrameStorage to use the same
        // anchor as 3rdPartyStorage permission items. They will be bundled together
        // to a single display to the user.
        if (id == "3rdPartyFrameStorage") {
          anchor = this._permissionList.querySelector(
            `[anchorfor="3rdPartyStorage"]`
          );
        }

        if (!item) {
          continue;
        }
        anchor.appendChild(item);
      }

      // Note: The `id` of the permission is "popup", but this may also
      //       include blocked third-party redirects.
      if (id == "popup" && showBlockedIndicator) {
        this._createBlockedPopupIndicator(
          totalBlockedPopups,
          isRedirectBlocked
        );
        hasBlockedIndicator = true;
      } else if (id == "geo" && permission.state === SitePermissions.ALLOW) {
        this._createGeoLocationLastAccessIndicator();
      }
    }

    if (showBlockedIndicator && !hasBlockedIndicator) {
      let permission = {
        id: "popup",
        state: SitePermissions.getDefault("popup"),
        scope: SitePermissions.SCOPE_PERSISTENT,
      };
      let item = this._createPermissionItem({ permission });
      this._defaultPermissionAnchor.appendChild(item);
      this._createBlockedPopupIndicator(totalBlockedPopups, isRedirectBlocked);
    }
  },

  /**
   * Creates a permission item based on the supplied options and returns it.
   * It is up to the caller to actually insert the element somewhere.
   *
   * @param permission - An object containing information representing the
   *                     permission, typically obtained via SitePermissions.sys.mjs
   * @param isContainer - If true, the permission item will be added to a vbox
   *                      and the vbox will be returned.
   * @param permClearButton - Whether to show an "x" button to clear the permission
   * @param showStateLabel - Whether to show a label indicating the current status
   *                         of the permission e.g. "Temporary Allowed"
   * @param idNoSuffix - Some permission types have additional information suffixed
   *                     to the ID - callers can pass the unsuffixed ID via this
   *                     parameter to indicate the permission type manually.
   * @param nowrapLabel - Whether to prevent the permission item's label from
   *                      wrapping its text content. This allows styling text-overflow
   *                      and is useful for e.g. 3rdPartyStorage permissions whose
   *                      labels are origins - which could be of any length.
   */
  _createPermissionItem({
    permission,
    isContainer = false,
    permClearButton = true,
    showStateLabel = true,
    idNoSuffix = permission.id,
    nowrapLabel = false,
    clearCallback = () => {},
  }) {
    let container = document.createXULElement("hbox");
    container.classList.add(
      "permission-popup-permission-item",
      `permission-popup-permission-item-${idNoSuffix}`
    );
    container.setAttribute("align", "center");
    container.setAttribute("role", "group");

    let img = document.createXULElement("image");
    img.classList.add("permission-popup-permission-icon", idNoSuffix + "-icon");
    if (
      permission.state == SitePermissions.BLOCK ||
      permission.state == SitePermissions.AUTOPLAY_BLOCKED_ALL
    ) {
      img.classList.add("blocked-permission-icon");
    }

    if (
      permission.sharingState ==
        Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED ||
      (idNoSuffix == "screen" &&
        permission.sharingState &&
        !permission.sharingState.includes("Paused"))
    ) {
      img.classList.add("in-use");
    }

    let nameLabel = document.createXULElement("label");
    nameLabel.setAttribute("flex", "1");
    nameLabel.setAttribute("class", "permission-popup-permission-label");
    let label = SitePermissions.getPermissionLabel(permission.id);
    if (label === null) {
      return null;
    }
    if (nowrapLabel) {
      nameLabel.setAttribute("value", label);
      nameLabel.setAttribute("tooltiptext", label);
      nameLabel.setAttribute("crop", "end");
    } else {
      nameLabel.textContent = label;
    }
    // idNoSuffix is not unique for double-keyed permissions. Adding an index to
    // ensure IDs are unique.
    // permission.id is unique but may not be a valid HTML ID.
    let nameLabelId = `permission-popup-permission-label-${idNoSuffix}-${this
      ._permissionLabelIndex++}`;
    nameLabel.setAttribute("id", nameLabelId);

    let isPolicyPermission = [
      SitePermissions.SCOPE_POLICY,
      SitePermissions.SCOPE_GLOBAL,
    ].includes(permission.scope);

    if (
      (idNoSuffix == "popup" && !isPolicyPermission) ||
      idNoSuffix == "autoplay-media"
    ) {
      let menulist = document.createXULElement("menulist");
      let menupopup = document.createXULElement("menupopup");
      let block = document.createXULElement("vbox");
      block.setAttribute("id", "permission-popup-container");
      block.setAttribute("class", "permission-popup-permission-item-container");
      menulist.setAttribute("sizetopopup", "none");
      menulist.setAttribute("id", "permission-popup-menulist");

      for (let state of SitePermissions.getAvailableStates(idNoSuffix)) {
        let menuitem = document.createXULElement("menuitem");
        // We need to correctly display the default/unknown state, which has its
        // own integer value (0) but represents one of the other states.
        if (state == SitePermissions.getDefault(idNoSuffix)) {
          menuitem.setAttribute("value", "0");
        } else {
          menuitem.setAttribute("value", state);
        }

        menuitem.setAttribute(
          "label",
          SitePermissions.getMultichoiceStateLabel(idNoSuffix, state)
        );
        menupopup.appendChild(menuitem);
      }

      menulist.appendChild(menupopup);

      if (permission.state == SitePermissions.getDefault(idNoSuffix)) {
        menulist.value = "0";
      } else {
        menulist.value = permission.state;
      }

      // Avoiding listening to the "select" event on purpose. See Bug 1404262.
      menulist.addEventListener("command", () => {
        SitePermissions.setForPrincipal(
          gBrowser.contentPrincipal,
          permission.id,
          menulist.selectedItem.value
        );
      });

      container.appendChild(img);
      container.appendChild(nameLabel);
      container.appendChild(menulist);
      container.setAttribute("aria-labelledby", nameLabelId);
      block.appendChild(container);

      return block;
    }

    container.appendChild(img);
    container.appendChild(nameLabel);
    let labelledBy = nameLabelId;

    let stateLabel;
    if (showStateLabel) {
      stateLabel = this._createStateLabel(permission, idNoSuffix);
      labelledBy += " " + stateLabel.id;
    }

    container.setAttribute("aria-labelledby", labelledBy);

    /* We return the permission item here without a remove button if the permission is a
       SCOPE_POLICY or SCOPE_GLOBAL permission. Policy permissions cannot be
       removed/changed for the duration of the browser session. */
    if (isPolicyPermission) {
      if (stateLabel) {
        container.appendChild(stateLabel);
      }
      return container;
    }

    if (isContainer) {
      let block = document.createXULElement("vbox");
      block.setAttribute("id", "permission-popup-" + idNoSuffix + "-container");
      block.setAttribute("class", "permission-popup-permission-item-container");

      if (permClearButton) {
        let button = this._createPermissionClearButton({
          permission,
          container: block,
          idNoSuffix,
          clearCallback,
        });
        if (stateLabel) {
          button.appendChild(stateLabel);
        }
        container.appendChild(button);
      }

      block.appendChild(container);
      return block;
    }

    if (permClearButton) {
      let button = this._createPermissionClearButton({
        permission,
        container,
        idNoSuffix,
        clearCallback,
      });
      if (stateLabel) {
        button.appendChild(stateLabel);
      }
      container.appendChild(button);
    }

    return container;
  },

  _createStateLabel(aPermission, idNoSuffix) {
    let label = document.createXULElement("label");
    label.setAttribute("class", "permission-popup-permission-state-label");
    let labelId = `permission-popup-permission-state-label-${idNoSuffix}-${this
      ._permissionLabelIndex++}`;
    label.setAttribute("id", labelId);
    let { state, scope } = aPermission;
    // If the user did not permanently allow this device but it is currently
    // used, set the variables to display a "temporarily allowed" info.
    if (state != SitePermissions.ALLOW && aPermission.sharingState) {
      state = SitePermissions.ALLOW;
      scope = SitePermissions.SCOPE_REQUEST;
    }
    label.textContent = SitePermissions.getCurrentStateLabel(
      state,
      idNoSuffix,
      scope
    );
    return label;
  },

  _removePermPersistentAllow(principal, id) {
    let perm = SitePermissions.getForPrincipal(principal, id);
    if (
      perm.state == SitePermissions.ALLOW &&
      perm.scope == SitePermissions.SCOPE_PERSISTENT
    ) {
      SitePermissions.removeFromPrincipal(principal, id);
    }
  },

  _createPermissionClearButton({
    permission,
    container,
    idNoSuffix = permission.id,
    clearCallback = () => {},
  }) {
    let button = document.createXULElement("button");
    button.setAttribute("class", "permission-popup-permission-remove-button");
    let tooltiptext = gNavigatorBundle.getString("permissions.remove.tooltip");
    button.setAttribute("tooltiptext", tooltiptext);
    button.addEventListener("command", () => {
      let browser = gBrowser.selectedBrowser;
      container.remove();
      // For XR permissions we need to keep track of all origins which may have
      // started XR sharing. This is necessary, because XR does not use
      // permission delegation and permissions can be granted for sub-frames. We
      // need to keep track of which origins we need to revoke the permission
      // for.
      if (permission.sharingState && idNoSuffix === "xr") {
        let origins = browser.getDevicePermissionOrigins(idNoSuffix);
        for (let origin of origins) {
          let principal =
            Services.scriptSecurityManager.createContentPrincipalFromOrigin(
              origin
            );
          this._removePermPersistentAllow(principal, permission.id);
        }
        origins.clear();
      }

      // For 3rdPartyFrameStorage permissions, we also need to remove
      // any 3rdPartyStorage permissions for origins covered by
      // the site of this permission. These permissions have the same
      // dialog, but slightly different scopes, so we only show one in
      // the list if they both exist and use it to stand in for both.
      if (idNoSuffix == "3rdPartyFrameStorage") {
        let [, matchSite] = permission.id.split(
          SitePermissions.PERM_KEY_DELIMITER
        );
        let permissions = SitePermissions.getAllForBrowser(browser);
        let removePermissions = permissions.filter(function (removePermission) {
          let [id, key] = removePermission.id.split(
            SitePermissions.PERM_KEY_DELIMITER
          );
          if (id != "3rdPartyStorage") {
            return false;
          }
          try {
            let origin = Services.io.newURI(key);
            let site = Services.eTLD.getSite(origin);
            return site == matchSite;
          } catch {
            return false;
          }
        });
        for (let removePermission of removePermissions) {
          SitePermissions.removeFromPrincipal(
            gBrowser.contentPrincipal,
            removePermission.id,
            browser
          );
        }
      }

      SitePermissions.removeFromPrincipal(
        gBrowser.contentPrincipal,
        permission.id,
        browser
      );

      this._permissionReloadHint.hidden = false;

      if (idNoSuffix === "geo") {
        gBrowser.updateBrowserSharing(browser, { geo: false });
      } else if (idNoSuffix === "xr") {
        gBrowser.updateBrowserSharing(browser, { xr: false });
      }

      clearCallback();
    });

    return button;
  },

  _getGeoLocationLastAccess() {
    return new Promise(resolve => {
      let lastAccess = null;
      ContentPrefService2.getByDomainAndName(
        gBrowser.currentURI.spec,
        "permissions.geoLocation.lastAccess",
        gBrowser.selectedBrowser.loadContext,
        {
          handleResult(pref) {
            lastAccess = pref.value;
          },
          handleCompletion() {
            resolve(lastAccess);
          },
        }
      );
    });
  },

  async _createGeoLocationLastAccessIndicator() {
    let lastAccessStr = await this._getGeoLocationLastAccess();
    let geoContainer = document.getElementById(
      "permission-popup-geo-container"
    );

    // Check whether geoContainer still exists.
    // We are async, the identity popup could have been closed already.
    // Also check if it is already populated with a time label.
    // This can happen if we update the permission panel multiple times in a
    // short timeframe.
    if (
      lastAccessStr == null ||
      !geoContainer ||
      document.getElementById("geo-access-indicator-item")
    ) {
      return;
    }
    let lastAccess = new Date(lastAccessStr);
    if (isNaN(lastAccess)) {
      console.error("Invalid timestamp for last geolocation access");
      return;
    }

    let indicator = document.createXULElement("hbox");
    indicator.setAttribute("class", "permission-popup-permission-item");
    indicator.setAttribute("align", "center");
    indicator.setAttribute("id", "geo-access-indicator-item");

    let timeFormat = new Services.intl.RelativeTimeFormat(undefined, {});

    let text = document.createXULElement("label");
    text.setAttribute("flex", "1");
    text.setAttribute("class", "permission-popup-permission-label");

    text.textContent = gNavigatorBundle.getFormattedString(
      "geolocationLastAccessIndicatorText",
      [timeFormat.formatBestUnit(lastAccess)]
    );

    indicator.appendChild(text);

    geoContainer.appendChild(indicator);
  },

  /**
   * Create a permission item for a WebRTC permission. May return null if there
   * already is a suitable permission item for this device type.
   *
   * @param {object} permission - Permission object.
   * @param {string} id - Permission ID without suffix.
   * @param {string} [key] - Secondary permission key.
   * @returns {xul:hbox|null} - Element for permission or null if permission
   * should be skipped.
   */
  _createWebRTCPermissionItem(permission, id, key) {
    if (!["camera", "screen", "microphone", "speaker"].includes(id)) {
      throw new Error("Invalid permission id for WebRTC permission item.");
    }
    // Only show WebRTC device-specific ALLOW permissions. Since we only show
    // one permission item per device type, we don't support showing mixed
    // states where one devices is allowed and another one blocked.
    if (key && permission.state != SitePermissions.ALLOW) {
      return null;
    }
    // Check if there is already an item for this permission. Multiple
    // permissions with the same id can be set, but with different keys.
    let item = document.querySelector(
      `.permission-popup-permission-item-${id}`
    );

    if (key) {
      // We have a double keyed permission. If there is already an item it will
      // have ownership of all permissions with this WebRTC permission id.
      if (item) {
        return null;
      }
    } else if (item) {
      if (permission.state == SitePermissions.PROMPT) {
        return null;
      }
      // If we have a single-key (not device specific) webRTC permission
      // other than PROMPT, it overrides any existing (device specific)
      // permission items.
      item.remove();
    }

    return this._createPermissionItem({
      permission,
      idNoSuffix: id,
      clearCallback: () => {
        webrtcUI.clearPermissionsAndStopSharing([id], gBrowser.selectedTab);
      },
    });
  },

  _createProtocolHandlerPermissionItem(permission, key) {
    let container = document.getElementById(
      "permission-popup-open-protocol-handler-container"
    );
    let initialCall;

    if (!container) {
      // First open-protocol-handler permission, create container.
      container = this._createPermissionItem({
        permission,
        isContainer: true,
        permClearButton: false,
        showStateLabel: false,
        idNoSuffix: "open-protocol-handler",
      });
      initialCall = true;
    }

    let item = document.createXULElement("hbox");
    item.setAttribute("class", "permission-popup-permission-item");
    item.setAttribute("align", "center");

    let text = document.createXULElement("label");
    text.setAttribute("flex", "1");
    text.setAttribute("class", "permission-popup-permission-label-subitem");

    text.textContent = gNavigatorBundle.getFormattedString(
      "openProtocolHandlerPermissionEntryLabel",
      [key]
    );

    let stateLabel = this._createStateLabel(
      permission,
      "open-protocol-handler"
    );

    item.appendChild(text);

    let button = this._createPermissionClearButton({
      permission,
      container: item,
      clearCallback: () => {
        // When we're clearing the last open-protocol-handler permission, clean up
        // the empty container.
        // (<= 1 because the heading item is also a child of the container)
        if (container.childElementCount <= 1) {
          container.remove();
        }
      },
    });
    button.appendChild(stateLabel);
    item.appendChild(button);

    container.appendChild(item);

    // If container already exists in permission list, don't return it again.
    return initialCall && container;
  },

  _createBlockedRedirectText() {
    let text = document.createXULElement("label", { is: "text-link" });
    text.setAttribute("class", "permission-popup-permission-label");
    text.addEventListener("click", () => {
      gBrowser.selectedBrowser.popupAndRedirectBlocker.unblockFirstRedirect();
    });

    document.l10n.setAttributes(text, "site-permissions-unblock-redirect");

    return text;
  },

  _createBlockedPopupText(aTotalBlockedPopups) {
    let text = document.createXULElement("label", { is: "text-link" });
    text.setAttribute("class", "permission-popup-permission-label");
    text.addEventListener("click", () => {
      gBrowser.selectedBrowser.popupAndRedirectBlocker.unblockAllPopups();
    });

    document.l10n.setAttributes(text, "site-permissions-open-blocked-popups", {
      count: aTotalBlockedPopups,
    });

    return text;
  },

  _createBlockedPopupIndicator(aTotalBlockedPopups, aIsRedirectBlocked) {
    let indicator = document.createXULElement("hbox");
    indicator.setAttribute("class", "permission-popup-permission-item");
    indicator.setAttribute("align", "center");
    indicator.setAttribute("id", "blocked-popup-indicator-item");

    MozXULElement.insertFTLIfNeeded("browser/sitePermissions.ftl");

    if (aIsRedirectBlocked) {
      indicator.appendChild(this._createBlockedRedirectText());
    }

    if (aTotalBlockedPopups) {
      indicator.appendChild(this._createBlockedPopupText(aTotalBlockedPopups));
    }

    document
      .getElementById("permission-popup-container")
      .appendChild(indicator);
  },
};

/**
 * Returns an object containing two booleans: {camGrace, micGrace},
 * whether permission grace periods are found for camera/microphone AND
 * persistent permissions do not exist for said permissions.
 *
 * @param browser - Browser element to get permissions for.
 */
function hasMicCamGracePeriodsSolely(browser) {
  let perms = SitePermissions.getAllForBrowser(browser);
  let micGrace = false;
  let micGrant = false;
  let camGrace = false;
  let camGrant = false;
  for (const perm of perms) {
    if (perm.state != SitePermissions.ALLOW) {
      continue;
    }
    let [id, key] = perm.id.split(SitePermissions.PERM_KEY_DELIMITER);
    let temporary = !!key && perm.scope == SitePermissions.SCOPE_TEMPORARY;
    let persistent = !key && perm.scope == SitePermissions.SCOPE_PERSISTENT;

    if (id == "microphone") {
      if (temporary) {
        micGrace = true;
      }
      if (persistent) {
        micGrant = true;
      }
      continue;
    }
    if (id == "camera") {
      if (temporary) {
        camGrace = true;
      }
      if (persistent) {
        camGrant = true;
      }
    }
  }
  return { micGrace: micGrace && !micGrant, camGrace: camGrace && !camGrant };
}

XPCOMUtils.defineLazyPreferenceGetter(
  gPermissionPanel,
  "_gumShowAlwaysAsk",
  "permissions.media.show_always_ask.enabled",
  false
);
