tor-browser

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

browser-sitePermissionPanel.js (38873B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 /**
      6 * Utility object to handle manipulations of the identity permission indicators
      7 * in the UI.
      8 */
      9 var gPermissionPanel = {
     10  _popupInitialized: false,
     11  _initializePopup() {
     12    if (!this._popupInitialized) {
     13      let wrapper = document.getElementById("template-permission-popup");
     14      wrapper.replaceWith(wrapper.content);
     15      this._popupInitialized = true;
     16      this._permissionPopup.addEventListener("popupshown", this);
     17      this._permissionPopup.addEventListener("popuphidden", this);
     18    }
     19  },
     20 
     21  hidePopup() {
     22    if (this._popupInitialized) {
     23      PanelMultiView.hidePopup(this._permissionPopup);
     24    }
     25  },
     26 
     27  /**
     28   * _popupAnchorNode will be set by setAnchor if an outside consumer
     29   * of this object wants to override the default anchor for the panel.
     30   * If there is no override, this remains null, and the _identityPermissionBox
     31   * will be used as the anchor.
     32   */
     33  _popupAnchorNode: null,
     34  _popupPosition: "bottomleft topleft",
     35  setAnchor(anchorNode, popupPosition) {
     36    this._popupAnchorNode = anchorNode;
     37    this._popupPosition = popupPosition;
     38  },
     39 
     40  // smart getters
     41  get _popupAnchor() {
     42    if (this._popupAnchorNode) {
     43      return this._popupAnchorNode;
     44    }
     45    return this._identityPermissionBox;
     46  },
     47  get _identityPermissionBox() {
     48    delete this._identityPermissionBox;
     49    return (this._identityPermissionBox = document.getElementById(
     50      "identity-permission-box"
     51    ));
     52  },
     53  get _permissionGrantedIcon() {
     54    delete this._permissionGrantedIcon;
     55    return (this._permissionGrantedIcon = document.getElementById(
     56      "permissions-granted-icon"
     57    ));
     58  },
     59  get _permissionPopup() {
     60    if (!this._popupInitialized) {
     61      return null;
     62    }
     63    delete this._permissionPopup;
     64    return (this._permissionPopup =
     65      document.getElementById("permission-popup"));
     66  },
     67  get _permissionPopupMainView() {
     68    delete this._permissionPopupPopupMainView;
     69    return (this._permissionPopupPopupMainView = document.getElementById(
     70      "permission-popup-mainView"
     71    ));
     72  },
     73  get _permissionPopupMainViewHeaderLabel() {
     74    delete this._permissionPopupMainViewHeaderLabel;
     75    return (this._permissionPopupMainViewHeaderLabel = document.getElementById(
     76      "permission-popup-mainView-panel-header-span"
     77    ));
     78  },
     79  get _permissionList() {
     80    delete this._permissionList;
     81    return (this._permissionList = document.getElementById(
     82      "permission-popup-permission-list"
     83    ));
     84  },
     85  get _defaultPermissionAnchor() {
     86    delete this._defaultPermissionAnchor;
     87    return (this._defaultPermissionAnchor = document.getElementById(
     88      "permission-popup-permission-list-default-anchor"
     89    ));
     90  },
     91  get _permissionReloadHint() {
     92    delete this._permissionReloadHint;
     93    return (this._permissionReloadHint = document.getElementById(
     94      "permission-popup-permission-reload-hint"
     95    ));
     96  },
     97  get _permissionAnchors() {
     98    delete this._permissionAnchors;
     99    let permissionAnchors = {};
    100    for (let anchor of document.getElementById("blocked-permissions-container")
    101      .children) {
    102      permissionAnchors[anchor.getAttribute("data-permission-id")] = anchor;
    103    }
    104    return (this._permissionAnchors = permissionAnchors);
    105  },
    106 
    107  get _geoSharingIcon() {
    108    delete this._geoSharingIcon;
    109    return (this._geoSharingIcon = document.getElementById("geo-sharing-icon"));
    110  },
    111 
    112  get _xrSharingIcon() {
    113    delete this._xrSharingIcon;
    114    return (this._xrSharingIcon = document.getElementById("xr-sharing-icon"));
    115  },
    116 
    117  get _webRTCSharingIcon() {
    118    delete this._webRTCSharingIcon;
    119    return (this._webRTCSharingIcon = document.getElementById(
    120      "webrtc-sharing-icon"
    121    ));
    122  },
    123 
    124  /**
    125   * Refresh the contents of the permission popup. This includes the headline
    126   * and the list of permissions.
    127   */
    128  _refreshPermissionPopup() {
    129    let host = gIdentityHandler.getHostForDisplay();
    130 
    131    // Update header label
    132    this._permissionPopupMainViewHeaderLabel.textContent =
    133      gNavigatorBundle.getFormattedString("permissions.header", [host]);
    134 
    135    // Refresh the permission list
    136    this.updateSitePermissions();
    137  },
    138 
    139  /**
    140   * Called by gIdentityHandler to hide permission icons for invalid proxy
    141   * state.
    142   */
    143  hidePermissionIcons() {
    144    this._identityPermissionBox.removeAttribute("hasPermissions");
    145  },
    146 
    147  /**
    148   * Updates the permissions icons in the identity block.
    149   * We show icons for blocked permissions / popups.
    150   */
    151  refreshPermissionIcons() {
    152    let permissionAnchors = this._permissionAnchors;
    153 
    154    // hide all permission icons
    155    for (let icon of Object.values(permissionAnchors)) {
    156      icon.removeAttribute("showing");
    157    }
    158 
    159    // keeps track if we should show an indicator that there are active permissions
    160    let hasPermissions = false;
    161 
    162    // show permission icons
    163    let permissions = SitePermissions.getAllForBrowser(
    164      gBrowser.selectedBrowser
    165    );
    166    for (let permission of permissions) {
    167      // Don't show persisted PROMPT permissions (unless a pref says to).
    168      // These would appear as "Always Ask ✖" which have utility, but might confuse
    169      if (
    170        permission.state == SitePermissions.UNKNOWN ||
    171        (permission.state == SitePermissions.PROMPT && !this._gumShowAlwaysAsk)
    172      ) {
    173        continue;
    174      }
    175      hasPermissions = true;
    176 
    177      if (
    178        permission.state == SitePermissions.BLOCK ||
    179        permission.state == SitePermissions.AUTOPLAY_BLOCKED_ALL
    180      ) {
    181        let icon = permissionAnchors[permission.id];
    182        if (icon) {
    183          icon.setAttribute("showing", "true");
    184        }
    185      }
    186    }
    187 
    188    // Show blocked popup icon in the identity-box if popups or a
    189    // third-party redirect is blocked irrespective of popup permission
    190    // capability value.
    191    if (
    192      gBrowser.selectedBrowser.popupAndRedirectBlocker.getBlockedPopupCount() ||
    193      gBrowser.selectedBrowser.popupAndRedirectBlocker.isRedirectBlocked()
    194    ) {
    195      let icon = permissionAnchors.popup;
    196      icon.setAttribute("showing", "true");
    197      hasPermissions = true;
    198    }
    199 
    200    this._identityPermissionBox.toggleAttribute(
    201      "hasPermissions",
    202      hasPermissions
    203    );
    204  },
    205 
    206  /**
    207   * Shows the permission popup.
    208   *
    209   * @param {Event} event - Event which caused the popup to show.
    210   */
    211  openPopup(event) {
    212    // If we are in DOM fullscreen, exit it before showing the permission popup
    213    // (see bug 1557041)
    214    if (document.fullscreen) {
    215      // Open the identity popup after DOM fullscreen exit
    216      // We need to wait for the exit event and after that wait for the fullscreen exit transition to complete
    217      // If we call openPopup before the fullscreen transition ends it can get cancelled
    218      // Only waiting for painted is not sufficient because we could still be in the fullscreen enter transition.
    219      this._exitedEventReceived = false;
    220      this._event = event;
    221      Services.obs.addObserver(this, "fullscreen-painted");
    222      window.addEventListener(
    223        "MozDOMFullscreen:Exited",
    224        () => {
    225          this._exitedEventReceived = true;
    226        },
    227        { once: true }
    228      );
    229      document.exitFullscreen();
    230      return;
    231    }
    232 
    233    // Make the popup available.
    234    this._initializePopup();
    235 
    236    // Remove the reload hint that we show after a user has cleared a permission.
    237    this._permissionReloadHint.hidden = true;
    238 
    239    // Update the popup strings
    240    this._refreshPermissionPopup();
    241 
    242    // Check the panel state of other panels. Hide them if needed.
    243    let openPanels = Array.from(document.querySelectorAll("panel[openpanel]"));
    244    for (let panel of openPanels) {
    245      PanelMultiView.hidePopup(panel);
    246    }
    247 
    248    // Now open the popup, anchored off the primary chrome element
    249    PanelMultiView.openPopup(this._permissionPopup, this._popupAnchor, {
    250      position: this._popupPosition,
    251      triggerEvent: event,
    252    }).catch(console.error);
    253  },
    254 
    255  /**
    256   * Update identity permission indicators based on sharing state of the
    257   * selected tab. This should be called externally whenever the sharing state
    258   * of the selected tab changes.
    259   */
    260  updateSharingIndicator() {
    261    let tab = gBrowser.selectedTab;
    262    this._sharingState = tab._sharingState;
    263 
    264    this._webRTCSharingIcon.removeAttribute("paused");
    265    this._webRTCSharingIcon.removeAttribute("sharing");
    266    this._geoSharingIcon.removeAttribute("sharing");
    267    this._xrSharingIcon.removeAttribute("sharing");
    268 
    269    let hasSharingIcon = false;
    270 
    271    if (this._sharingState) {
    272      if (this._sharingState.webRTC) {
    273        if (this._sharingState.webRTC.sharing) {
    274          this._webRTCSharingIcon.setAttribute(
    275            "sharing",
    276            this._sharingState.webRTC.sharing
    277          );
    278          hasSharingIcon = true;
    279 
    280          if (this._sharingState.webRTC.paused) {
    281            this._webRTCSharingIcon.setAttribute("paused", "true");
    282          }
    283        } else {
    284          // Reflect any active permission grace periods
    285          let { micGrace, camGrace } = hasMicCamGracePeriodsSolely(
    286            gBrowser.selectedBrowser
    287          );
    288          if (micGrace || camGrace) {
    289            // Reuse the "paused sharing" indicator to warn about grace periods
    290            this._webRTCSharingIcon.setAttribute(
    291              "sharing",
    292              camGrace ? "camera" : "microphone"
    293            );
    294            hasSharingIcon = true;
    295            this._webRTCSharingIcon.setAttribute("paused", "true");
    296          }
    297        }
    298      }
    299 
    300      if (this._sharingState.geo) {
    301        this._geoSharingIcon.setAttribute("sharing", this._sharingState.geo);
    302        hasSharingIcon = true;
    303      }
    304 
    305      if (this._sharingState.xr) {
    306        this._xrSharingIcon.setAttribute("sharing", this._sharingState.xr);
    307        hasSharingIcon = true;
    308      }
    309    }
    310 
    311    this._identityPermissionBox.toggleAttribute(
    312      "hasSharingIcon",
    313      hasSharingIcon
    314    );
    315 
    316    if (this._popupInitialized && this._permissionPopup.state != "closed") {
    317      this.updateSitePermissions();
    318    }
    319  },
    320 
    321  /**
    322   * Click handler for the permission-box element in primary chrome.
    323   */
    324  handleIdentityButtonEvent(event) {
    325    event.stopPropagation();
    326 
    327    if (
    328      (event.type == "click" && event.button != 0) ||
    329      (event.type == "keypress" &&
    330        event.charCode != KeyEvent.DOM_VK_SPACE &&
    331        event.keyCode != KeyEvent.DOM_VK_RETURN)
    332    ) {
    333      return; // Left click, space or enter only
    334    }
    335 
    336    // Don't allow left click, space or enter if the location has been modified,
    337    // so long as we're not sharing any devices.
    338    // If we are sharing a device, the identity block is prevented by CSS from
    339    // being focused (and therefore, interacted with) by the user. However, we
    340    // want to allow opening the identity popup from the device control menu,
    341    // which calls click() on the identity button, so we don't return early.
    342    if (
    343      !this._sharingState &&
    344      gURLBar.getAttribute("pageproxystate") != "valid"
    345    ) {
    346      return;
    347    }
    348 
    349    this.openPopup(event);
    350  },
    351 
    352  handleEvent(event) {
    353    switch (event.type) {
    354      case "popupshown":
    355        if (event.target == this._permissionPopup) {
    356          window.addEventListener("focus", this, true);
    357        }
    358        break;
    359      case "popuphidden":
    360        if (event.target == this._permissionPopup) {
    361          window.removeEventListener("focus", this, true);
    362        }
    363        break;
    364      case "focus":
    365        {
    366          let elem = document.activeElement;
    367          let position = elem.compareDocumentPosition(this._permissionPopup);
    368 
    369          if (
    370            !(
    371              position &
    372              (Node.DOCUMENT_POSITION_CONTAINS |
    373                Node.DOCUMENT_POSITION_CONTAINED_BY)
    374            ) &&
    375            !this._permissionPopup.hasAttribute("noautohide")
    376          ) {
    377            // Hide the panel when focusing an element that is
    378            // neither an ancestor nor descendant unless the panel has
    379            // @noautohide (e.g. for a tour).
    380            PanelMultiView.hidePopup(this._permissionPopup);
    381          }
    382        }
    383        break;
    384    }
    385  },
    386 
    387  observe(subject, topic) {
    388    switch (topic) {
    389      case "fullscreen-painted": {
    390        if (subject != window || !this._exitedEventReceived) {
    391          return;
    392        }
    393        Services.obs.removeObserver(this, "fullscreen-painted");
    394        this.openPopup(this._event);
    395        delete this._event;
    396        break;
    397      }
    398    }
    399  },
    400 
    401  onLocationChange() {
    402    if (this._popupInitialized && this._permissionPopup.state != "closed") {
    403      this._permissionReloadHint.hidden = true;
    404    }
    405  },
    406 
    407  /**
    408   * Updates the permission list in the permissions popup.
    409   */
    410  updateSitePermissions() {
    411    let permissionItemSelector = [
    412      ".permission-popup-permission-item, .permission-popup-permission-item-container",
    413    ];
    414    this._permissionList
    415      .querySelectorAll(permissionItemSelector)
    416      .forEach(e => e.remove());
    417    // Used by _createPermissionItem to build unique IDs.
    418    this._permissionLabelIndex = 0;
    419 
    420    let permissions = SitePermissions.getAllPermissionDetailsForBrowser(
    421      gBrowser.selectedBrowser
    422    );
    423 
    424    // Don't display origin-keyed 3rdPartyStorage permissions that are covered by
    425    // site-keyed 3rdPartyFrameStorage permissions.
    426    let thirdPartyStorageSites = new Set(
    427      permissions
    428        .map(function (permission) {
    429          let [id, key] = permission.id.split(
    430            SitePermissions.PERM_KEY_DELIMITER
    431          );
    432          if (id == "3rdPartyFrameStorage") {
    433            return key;
    434          }
    435          return null;
    436        })
    437        .filter(function (key) {
    438          return key != null;
    439        })
    440    );
    441    permissions = permissions.filter(function (permission) {
    442      let [id, key] = permission.id.split(SitePermissions.PERM_KEY_DELIMITER);
    443      if (id != "3rdPartyStorage") {
    444        return true;
    445      }
    446      try {
    447        let origin = Services.io.newURI(key);
    448        let site = Services.eTLD.getSite(origin);
    449        return !thirdPartyStorageSites.has(site);
    450      } catch {
    451        return false;
    452      }
    453    });
    454 
    455    this._sharingState = gBrowser.selectedTab._sharingState;
    456 
    457    if (this._sharingState?.geo) {
    458      let geoPermission = permissions.find(perm => perm.id === "geo");
    459      if (geoPermission) {
    460        geoPermission.sharingState = true;
    461      } else {
    462        permissions.push({
    463          id: "geo",
    464          state: SitePermissions.ALLOW,
    465          scope: SitePermissions.SCOPE_REQUEST,
    466          sharingState: true,
    467        });
    468      }
    469    }
    470 
    471    if (this._sharingState?.xr) {
    472      let xrPermission = permissions.find(perm => perm.id === "xr");
    473      if (xrPermission) {
    474        xrPermission.sharingState = true;
    475      } else {
    476        permissions.push({
    477          id: "xr",
    478          state: SitePermissions.ALLOW,
    479          scope: SitePermissions.SCOPE_REQUEST,
    480          sharingState: true,
    481        });
    482      }
    483    }
    484 
    485    if (this._sharingState?.webRTC) {
    486      let webrtcState = this._sharingState.webRTC;
    487      // If WebRTC device or screen are in use, we need to find
    488      // the associated ALLOW permission item to set the sharingState field.
    489      for (let id of ["camera", "microphone", "screen"]) {
    490        if (webrtcState[id]) {
    491          let found = false;
    492          for (let permission of permissions) {
    493            let [permId] = permission.id.split(
    494              SitePermissions.PERM_KEY_DELIMITER
    495            );
    496            if (permId != id || permission.state != SitePermissions.ALLOW) {
    497              continue;
    498            }
    499            found = true;
    500            permission.sharingState = webrtcState[id];
    501          }
    502          if (!found) {
    503            // If the ALLOW permission item we were looking for doesn't exist,
    504            // the user has temporarily allowed sharing and we need to add
    505            // an item in the permissions array to reflect this.
    506            permissions.push({
    507              id,
    508              state: SitePermissions.ALLOW,
    509              scope: SitePermissions.SCOPE_REQUEST,
    510              sharingState: webrtcState[id],
    511            });
    512          }
    513        }
    514      }
    515    }
    516 
    517    let totalBlockedPopups =
    518      gBrowser.selectedBrowser.popupAndRedirectBlocker.getBlockedPopupCount();
    519    let isRedirectBlocked =
    520      gBrowser.selectedBrowser.popupAndRedirectBlocker.isRedirectBlocked();
    521    let showBlockedIndicator = totalBlockedPopups || isRedirectBlocked;
    522 
    523    let hasBlockedIndicator = false;
    524    for (let permission of permissions) {
    525      let [id, key] = permission.id.split(SitePermissions.PERM_KEY_DELIMITER);
    526 
    527      if (id == "storage-access") {
    528        // Ignore storage access permissions here, they are made visible inside
    529        // the Content Blocking UI.
    530        continue;
    531      }
    532 
    533      let item;
    534      let anchor =
    535        this._permissionList.querySelector(`[anchorfor="${id}"]`) ||
    536        this._defaultPermissionAnchor;
    537 
    538      if (id == "open-protocol-handler") {
    539        let permContainer = this._createProtocolHandlerPermissionItem(
    540          permission,
    541          key
    542        );
    543        if (permContainer) {
    544          anchor.appendChild(permContainer);
    545        }
    546      } else if (["camera", "screen", "microphone", "speaker"].includes(id)) {
    547        if (
    548          permission.state == SitePermissions.PROMPT &&
    549          !this._gumShowAlwaysAsk
    550        ) {
    551          continue;
    552        }
    553        item = this._createWebRTCPermissionItem(permission, id, key);
    554        if (!item) {
    555          continue;
    556        }
    557        anchor.appendChild(item);
    558      } else {
    559        item = this._createPermissionItem({
    560          permission,
    561          idNoSuffix: id,
    562          isContainer: id == "geo" || id == "xr",
    563          nowrapLabel: id == "3rdPartyStorage" || id == "3rdPartyFrameStorage",
    564        });
    565 
    566        // We want permission items for the 3rdPartyFrameStorage to use the same
    567        // anchor as 3rdPartyStorage permission items. They will be bundled together
    568        // to a single display to the user.
    569        if (id == "3rdPartyFrameStorage") {
    570          anchor = this._permissionList.querySelector(
    571            `[anchorfor="3rdPartyStorage"]`
    572          );
    573        }
    574 
    575        if (!item) {
    576          continue;
    577        }
    578        anchor.appendChild(item);
    579      }
    580 
    581      // Note: The `id` of the permission is "popup", but this may also
    582      //       include blocked third-party redirects.
    583      if (id == "popup" && showBlockedIndicator) {
    584        this._createBlockedPopupIndicator(
    585          totalBlockedPopups,
    586          isRedirectBlocked
    587        );
    588        hasBlockedIndicator = true;
    589      } else if (id == "geo" && permission.state === SitePermissions.ALLOW) {
    590        this._createGeoLocationLastAccessIndicator();
    591      }
    592    }
    593 
    594    if (showBlockedIndicator && !hasBlockedIndicator) {
    595      let permission = {
    596        id: "popup",
    597        state: SitePermissions.getDefault("popup"),
    598        scope: SitePermissions.SCOPE_PERSISTENT,
    599      };
    600      let item = this._createPermissionItem({ permission });
    601      this._defaultPermissionAnchor.appendChild(item);
    602      this._createBlockedPopupIndicator(totalBlockedPopups, isRedirectBlocked);
    603    }
    604  },
    605 
    606  /**
    607   * Creates a permission item based on the supplied options and returns it.
    608   * It is up to the caller to actually insert the element somewhere.
    609   *
    610   * @param permission - An object containing information representing the
    611   *                     permission, typically obtained via SitePermissions.sys.mjs
    612   * @param isContainer - If true, the permission item will be added to a vbox
    613   *                      and the vbox will be returned.
    614   * @param permClearButton - Whether to show an "x" button to clear the permission
    615   * @param showStateLabel - Whether to show a label indicating the current status
    616   *                         of the permission e.g. "Temporary Allowed"
    617   * @param idNoSuffix - Some permission types have additional information suffixed
    618   *                     to the ID - callers can pass the unsuffixed ID via this
    619   *                     parameter to indicate the permission type manually.
    620   * @param nowrapLabel - Whether to prevent the permission item's label from
    621   *                      wrapping its text content. This allows styling text-overflow
    622   *                      and is useful for e.g. 3rdPartyStorage permissions whose
    623   *                      labels are origins - which could be of any length.
    624   */
    625  _createPermissionItem({
    626    permission,
    627    isContainer = false,
    628    permClearButton = true,
    629    showStateLabel = true,
    630    idNoSuffix = permission.id,
    631    nowrapLabel = false,
    632    clearCallback = () => {},
    633  }) {
    634    let container = document.createXULElement("hbox");
    635    container.classList.add(
    636      "permission-popup-permission-item",
    637      `permission-popup-permission-item-${idNoSuffix}`
    638    );
    639    container.setAttribute("align", "center");
    640    container.setAttribute("role", "group");
    641 
    642    let img = document.createXULElement("image");
    643    img.classList.add("permission-popup-permission-icon", idNoSuffix + "-icon");
    644    if (
    645      permission.state == SitePermissions.BLOCK ||
    646      permission.state == SitePermissions.AUTOPLAY_BLOCKED_ALL
    647    ) {
    648      img.classList.add("blocked-permission-icon");
    649    }
    650 
    651    if (
    652      permission.sharingState ==
    653        Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED ||
    654      (idNoSuffix == "screen" &&
    655        permission.sharingState &&
    656        !permission.sharingState.includes("Paused"))
    657    ) {
    658      img.classList.add("in-use");
    659    }
    660 
    661    let nameLabel = document.createXULElement("label");
    662    nameLabel.setAttribute("flex", "1");
    663    nameLabel.setAttribute("class", "permission-popup-permission-label");
    664    let label = SitePermissions.getPermissionLabel(permission.id);
    665    if (label === null) {
    666      return null;
    667    }
    668    if (nowrapLabel) {
    669      nameLabel.setAttribute("value", label);
    670      nameLabel.setAttribute("tooltiptext", label);
    671      nameLabel.setAttribute("crop", "end");
    672    } else {
    673      nameLabel.textContent = label;
    674    }
    675    // idNoSuffix is not unique for double-keyed permissions. Adding an index to
    676    // ensure IDs are unique.
    677    // permission.id is unique but may not be a valid HTML ID.
    678    let nameLabelId = `permission-popup-permission-label-${idNoSuffix}-${this
    679      ._permissionLabelIndex++}`;
    680    nameLabel.setAttribute("id", nameLabelId);
    681 
    682    let isPolicyPermission = [
    683      SitePermissions.SCOPE_POLICY,
    684      SitePermissions.SCOPE_GLOBAL,
    685    ].includes(permission.scope);
    686 
    687    if (
    688      (idNoSuffix == "popup" && !isPolicyPermission) ||
    689      idNoSuffix == "autoplay-media"
    690    ) {
    691      let menulist = document.createXULElement("menulist");
    692      let menupopup = document.createXULElement("menupopup");
    693      let block = document.createXULElement("vbox");
    694      block.setAttribute("id", "permission-popup-container");
    695      block.setAttribute("class", "permission-popup-permission-item-container");
    696      menulist.setAttribute("sizetopopup", "none");
    697      menulist.setAttribute("id", "permission-popup-menulist");
    698 
    699      for (let state of SitePermissions.getAvailableStates(idNoSuffix)) {
    700        let menuitem = document.createXULElement("menuitem");
    701        // We need to correctly display the default/unknown state, which has its
    702        // own integer value (0) but represents one of the other states.
    703        if (state == SitePermissions.getDefault(idNoSuffix)) {
    704          menuitem.setAttribute("value", "0");
    705        } else {
    706          menuitem.setAttribute("value", state);
    707        }
    708 
    709        menuitem.setAttribute(
    710          "label",
    711          SitePermissions.getMultichoiceStateLabel(idNoSuffix, state)
    712        );
    713        menupopup.appendChild(menuitem);
    714      }
    715 
    716      menulist.appendChild(menupopup);
    717 
    718      if (permission.state == SitePermissions.getDefault(idNoSuffix)) {
    719        menulist.value = "0";
    720      } else {
    721        menulist.value = permission.state;
    722      }
    723 
    724      // Avoiding listening to the "select" event on purpose. See Bug 1404262.
    725      menulist.addEventListener("command", () => {
    726        SitePermissions.setForPrincipal(
    727          gBrowser.contentPrincipal,
    728          permission.id,
    729          menulist.selectedItem.value
    730        );
    731      });
    732 
    733      container.appendChild(img);
    734      container.appendChild(nameLabel);
    735      container.appendChild(menulist);
    736      container.setAttribute("aria-labelledby", nameLabelId);
    737      block.appendChild(container);
    738 
    739      return block;
    740    }
    741 
    742    container.appendChild(img);
    743    container.appendChild(nameLabel);
    744    let labelledBy = nameLabelId;
    745 
    746    let stateLabel;
    747    if (showStateLabel) {
    748      stateLabel = this._createStateLabel(permission, idNoSuffix);
    749      labelledBy += " " + stateLabel.id;
    750    }
    751 
    752    container.setAttribute("aria-labelledby", labelledBy);
    753 
    754    /* We return the permission item here without a remove button if the permission is a
    755       SCOPE_POLICY or SCOPE_GLOBAL permission. Policy permissions cannot be
    756       removed/changed for the duration of the browser session. */
    757    if (isPolicyPermission) {
    758      if (stateLabel) {
    759        container.appendChild(stateLabel);
    760      }
    761      return container;
    762    }
    763 
    764    if (isContainer) {
    765      let block = document.createXULElement("vbox");
    766      block.setAttribute("id", "permission-popup-" + idNoSuffix + "-container");
    767      block.setAttribute("class", "permission-popup-permission-item-container");
    768 
    769      if (permClearButton) {
    770        let button = this._createPermissionClearButton({
    771          permission,
    772          container: block,
    773          idNoSuffix,
    774          clearCallback,
    775        });
    776        if (stateLabel) {
    777          button.appendChild(stateLabel);
    778        }
    779        container.appendChild(button);
    780      }
    781 
    782      block.appendChild(container);
    783      return block;
    784    }
    785 
    786    if (permClearButton) {
    787      let button = this._createPermissionClearButton({
    788        permission,
    789        container,
    790        idNoSuffix,
    791        clearCallback,
    792      });
    793      if (stateLabel) {
    794        button.appendChild(stateLabel);
    795      }
    796      container.appendChild(button);
    797    }
    798 
    799    return container;
    800  },
    801 
    802  _createStateLabel(aPermission, idNoSuffix) {
    803    let label = document.createXULElement("label");
    804    label.setAttribute("class", "permission-popup-permission-state-label");
    805    let labelId = `permission-popup-permission-state-label-${idNoSuffix}-${this
    806      ._permissionLabelIndex++}`;
    807    label.setAttribute("id", labelId);
    808    let { state, scope } = aPermission;
    809    // If the user did not permanently allow this device but it is currently
    810    // used, set the variables to display a "temporarily allowed" info.
    811    if (state != SitePermissions.ALLOW && aPermission.sharingState) {
    812      state = SitePermissions.ALLOW;
    813      scope = SitePermissions.SCOPE_REQUEST;
    814    }
    815    label.textContent = SitePermissions.getCurrentStateLabel(
    816      state,
    817      idNoSuffix,
    818      scope
    819    );
    820    return label;
    821  },
    822 
    823  _removePermPersistentAllow(principal, id) {
    824    let perm = SitePermissions.getForPrincipal(principal, id);
    825    if (
    826      perm.state == SitePermissions.ALLOW &&
    827      perm.scope == SitePermissions.SCOPE_PERSISTENT
    828    ) {
    829      SitePermissions.removeFromPrincipal(principal, id);
    830    }
    831  },
    832 
    833  _createPermissionClearButton({
    834    permission,
    835    container,
    836    idNoSuffix = permission.id,
    837    clearCallback = () => {},
    838  }) {
    839    let button = document.createXULElement("button");
    840    button.setAttribute("class", "permission-popup-permission-remove-button");
    841    let tooltiptext = gNavigatorBundle.getString("permissions.remove.tooltip");
    842    button.setAttribute("tooltiptext", tooltiptext);
    843    button.addEventListener("command", () => {
    844      let browser = gBrowser.selectedBrowser;
    845      container.remove();
    846      // For XR permissions we need to keep track of all origins which may have
    847      // started XR sharing. This is necessary, because XR does not use
    848      // permission delegation and permissions can be granted for sub-frames. We
    849      // need to keep track of which origins we need to revoke the permission
    850      // for.
    851      if (permission.sharingState && idNoSuffix === "xr") {
    852        let origins = browser.getDevicePermissionOrigins(idNoSuffix);
    853        for (let origin of origins) {
    854          let principal =
    855            Services.scriptSecurityManager.createContentPrincipalFromOrigin(
    856              origin
    857            );
    858          this._removePermPersistentAllow(principal, permission.id);
    859        }
    860        origins.clear();
    861      }
    862 
    863      // For 3rdPartyFrameStorage permissions, we also need to remove
    864      // any 3rdPartyStorage permissions for origins covered by
    865      // the site of this permission. These permissions have the same
    866      // dialog, but slightly different scopes, so we only show one in
    867      // the list if they both exist and use it to stand in for both.
    868      if (idNoSuffix == "3rdPartyFrameStorage") {
    869        let [, matchSite] = permission.id.split(
    870          SitePermissions.PERM_KEY_DELIMITER
    871        );
    872        let permissions = SitePermissions.getAllForBrowser(browser);
    873        let removePermissions = permissions.filter(function (removePermission) {
    874          let [id, key] = removePermission.id.split(
    875            SitePermissions.PERM_KEY_DELIMITER
    876          );
    877          if (id != "3rdPartyStorage") {
    878            return false;
    879          }
    880          try {
    881            let origin = Services.io.newURI(key);
    882            let site = Services.eTLD.getSite(origin);
    883            return site == matchSite;
    884          } catch {
    885            return false;
    886          }
    887        });
    888        for (let removePermission of removePermissions) {
    889          SitePermissions.removeFromPrincipal(
    890            gBrowser.contentPrincipal,
    891            removePermission.id,
    892            browser
    893          );
    894        }
    895      }
    896 
    897      SitePermissions.removeFromPrincipal(
    898        gBrowser.contentPrincipal,
    899        permission.id,
    900        browser
    901      );
    902 
    903      this._permissionReloadHint.hidden = false;
    904 
    905      if (idNoSuffix === "geo") {
    906        gBrowser.updateBrowserSharing(browser, { geo: false });
    907      } else if (idNoSuffix === "xr") {
    908        gBrowser.updateBrowserSharing(browser, { xr: false });
    909      }
    910 
    911      clearCallback();
    912    });
    913 
    914    return button;
    915  },
    916 
    917  _getGeoLocationLastAccess() {
    918    return new Promise(resolve => {
    919      let lastAccess = null;
    920      ContentPrefService2.getByDomainAndName(
    921        gBrowser.currentURI.spec,
    922        "permissions.geoLocation.lastAccess",
    923        gBrowser.selectedBrowser.loadContext,
    924        {
    925          handleResult(pref) {
    926            lastAccess = pref.value;
    927          },
    928          handleCompletion() {
    929            resolve(lastAccess);
    930          },
    931        }
    932      );
    933    });
    934  },
    935 
    936  async _createGeoLocationLastAccessIndicator() {
    937    let lastAccessStr = await this._getGeoLocationLastAccess();
    938    let geoContainer = document.getElementById(
    939      "permission-popup-geo-container"
    940    );
    941 
    942    // Check whether geoContainer still exists.
    943    // We are async, the identity popup could have been closed already.
    944    // Also check if it is already populated with a time label.
    945    // This can happen if we update the permission panel multiple times in a
    946    // short timeframe.
    947    if (
    948      lastAccessStr == null ||
    949      !geoContainer ||
    950      document.getElementById("geo-access-indicator-item")
    951    ) {
    952      return;
    953    }
    954    let lastAccess = new Date(lastAccessStr);
    955    if (isNaN(lastAccess)) {
    956      console.error("Invalid timestamp for last geolocation access");
    957      return;
    958    }
    959 
    960    let indicator = document.createXULElement("hbox");
    961    indicator.setAttribute("class", "permission-popup-permission-item");
    962    indicator.setAttribute("align", "center");
    963    indicator.setAttribute("id", "geo-access-indicator-item");
    964 
    965    let timeFormat = new Services.intl.RelativeTimeFormat(undefined, {});
    966 
    967    let text = document.createXULElement("label");
    968    text.setAttribute("flex", "1");
    969    text.setAttribute("class", "permission-popup-permission-label");
    970 
    971    text.textContent = gNavigatorBundle.getFormattedString(
    972      "geolocationLastAccessIndicatorText",
    973      [timeFormat.formatBestUnit(lastAccess)]
    974    );
    975 
    976    indicator.appendChild(text);
    977 
    978    geoContainer.appendChild(indicator);
    979  },
    980 
    981  /**
    982   * Create a permission item for a WebRTC permission. May return null if there
    983   * already is a suitable permission item for this device type.
    984   *
    985   * @param {object} permission - Permission object.
    986   * @param {string} id - Permission ID without suffix.
    987   * @param {string} [key] - Secondary permission key.
    988   * @returns {xul:hbox|null} - Element for permission or null if permission
    989   * should be skipped.
    990   */
    991  _createWebRTCPermissionItem(permission, id, key) {
    992    if (!["camera", "screen", "microphone", "speaker"].includes(id)) {
    993      throw new Error("Invalid permission id for WebRTC permission item.");
    994    }
    995    // Only show WebRTC device-specific ALLOW permissions. Since we only show
    996    // one permission item per device type, we don't support showing mixed
    997    // states where one devices is allowed and another one blocked.
    998    if (key && permission.state != SitePermissions.ALLOW) {
    999      return null;
   1000    }
   1001    // Check if there is already an item for this permission. Multiple
   1002    // permissions with the same id can be set, but with different keys.
   1003    let item = document.querySelector(
   1004      `.permission-popup-permission-item-${id}`
   1005    );
   1006 
   1007    if (key) {
   1008      // We have a double keyed permission. If there is already an item it will
   1009      // have ownership of all permissions with this WebRTC permission id.
   1010      if (item) {
   1011        return null;
   1012      }
   1013    } else if (item) {
   1014      if (permission.state == SitePermissions.PROMPT) {
   1015        return null;
   1016      }
   1017      // If we have a single-key (not device specific) webRTC permission
   1018      // other than PROMPT, it overrides any existing (device specific)
   1019      // permission items.
   1020      item.remove();
   1021    }
   1022 
   1023    return this._createPermissionItem({
   1024      permission,
   1025      idNoSuffix: id,
   1026      clearCallback: () => {
   1027        webrtcUI.clearPermissionsAndStopSharing([id], gBrowser.selectedTab);
   1028      },
   1029    });
   1030  },
   1031 
   1032  _createProtocolHandlerPermissionItem(permission, key) {
   1033    let container = document.getElementById(
   1034      "permission-popup-open-protocol-handler-container"
   1035    );
   1036    let initialCall;
   1037 
   1038    if (!container) {
   1039      // First open-protocol-handler permission, create container.
   1040      container = this._createPermissionItem({
   1041        permission,
   1042        isContainer: true,
   1043        permClearButton: false,
   1044        showStateLabel: false,
   1045        idNoSuffix: "open-protocol-handler",
   1046      });
   1047      initialCall = true;
   1048    }
   1049 
   1050    let item = document.createXULElement("hbox");
   1051    item.setAttribute("class", "permission-popup-permission-item");
   1052    item.setAttribute("align", "center");
   1053 
   1054    let text = document.createXULElement("label");
   1055    text.setAttribute("flex", "1");
   1056    text.setAttribute("class", "permission-popup-permission-label-subitem");
   1057 
   1058    text.textContent = gNavigatorBundle.getFormattedString(
   1059      "openProtocolHandlerPermissionEntryLabel",
   1060      [key]
   1061    );
   1062 
   1063    let stateLabel = this._createStateLabel(
   1064      permission,
   1065      "open-protocol-handler"
   1066    );
   1067 
   1068    item.appendChild(text);
   1069 
   1070    let button = this._createPermissionClearButton({
   1071      permission,
   1072      container: item,
   1073      clearCallback: () => {
   1074        // When we're clearing the last open-protocol-handler permission, clean up
   1075        // the empty container.
   1076        // (<= 1 because the heading item is also a child of the container)
   1077        if (container.childElementCount <= 1) {
   1078          container.remove();
   1079        }
   1080      },
   1081    });
   1082    button.appendChild(stateLabel);
   1083    item.appendChild(button);
   1084 
   1085    container.appendChild(item);
   1086 
   1087    // If container already exists in permission list, don't return it again.
   1088    return initialCall && container;
   1089  },
   1090 
   1091  _createBlockedRedirectText() {
   1092    let text = document.createXULElement("label", { is: "text-link" });
   1093    text.setAttribute("class", "permission-popup-permission-label");
   1094    text.addEventListener("click", () => {
   1095      gBrowser.selectedBrowser.popupAndRedirectBlocker.unblockFirstRedirect();
   1096    });
   1097 
   1098    document.l10n.setAttributes(text, "site-permissions-unblock-redirect");
   1099 
   1100    return text;
   1101  },
   1102 
   1103  _createBlockedPopupText(aTotalBlockedPopups) {
   1104    let text = document.createXULElement("label", { is: "text-link" });
   1105    text.setAttribute("class", "permission-popup-permission-label");
   1106    text.addEventListener("click", () => {
   1107      gBrowser.selectedBrowser.popupAndRedirectBlocker.unblockAllPopups();
   1108    });
   1109 
   1110    document.l10n.setAttributes(text, "site-permissions-open-blocked-popups", {
   1111      count: aTotalBlockedPopups,
   1112    });
   1113 
   1114    return text;
   1115  },
   1116 
   1117  _createBlockedPopupIndicator(aTotalBlockedPopups, aIsRedirectBlocked) {
   1118    let indicator = document.createXULElement("hbox");
   1119    indicator.setAttribute("class", "permission-popup-permission-item");
   1120    indicator.setAttribute("align", "center");
   1121    indicator.setAttribute("id", "blocked-popup-indicator-item");
   1122 
   1123    MozXULElement.insertFTLIfNeeded("browser/sitePermissions.ftl");
   1124 
   1125    if (aIsRedirectBlocked) {
   1126      indicator.appendChild(this._createBlockedRedirectText());
   1127    }
   1128 
   1129    if (aTotalBlockedPopups) {
   1130      indicator.appendChild(this._createBlockedPopupText(aTotalBlockedPopups));
   1131    }
   1132 
   1133    document
   1134      .getElementById("permission-popup-container")
   1135      .appendChild(indicator);
   1136  },
   1137 };
   1138 
   1139 /**
   1140 * Returns an object containing two booleans: {camGrace, micGrace},
   1141 * whether permission grace periods are found for camera/microphone AND
   1142 * persistent permissions do not exist for said permissions.
   1143 *
   1144 * @param browser - Browser element to get permissions for.
   1145 */
   1146 function hasMicCamGracePeriodsSolely(browser) {
   1147  let perms = SitePermissions.getAllForBrowser(browser);
   1148  let micGrace = false;
   1149  let micGrant = false;
   1150  let camGrace = false;
   1151  let camGrant = false;
   1152  for (const perm of perms) {
   1153    if (perm.state != SitePermissions.ALLOW) {
   1154      continue;
   1155    }
   1156    let [id, key] = perm.id.split(SitePermissions.PERM_KEY_DELIMITER);
   1157    let temporary = !!key && perm.scope == SitePermissions.SCOPE_TEMPORARY;
   1158    let persistent = !key && perm.scope == SitePermissions.SCOPE_PERSISTENT;
   1159 
   1160    if (id == "microphone") {
   1161      if (temporary) {
   1162        micGrace = true;
   1163      }
   1164      if (persistent) {
   1165        micGrant = true;
   1166      }
   1167      continue;
   1168    }
   1169    if (id == "camera") {
   1170      if (temporary) {
   1171        camGrace = true;
   1172      }
   1173      if (persistent) {
   1174        camGrant = true;
   1175      }
   1176    }
   1177  }
   1178  return { micGrace: micGrace && !micGrant, camGrace: camGrace && !camGrant };
   1179 }
   1180 
   1181 XPCOMUtils.defineLazyPreferenceGetter(
   1182  gPermissionPanel,
   1183  "_gumShowAlwaysAsk",
   1184  "permissions.media.show_always_ask.enabled",
   1185  false
   1186 );