summaryrefslogtreecommitdiffstats
path: root/browser/base/content/browser-sitePermissionPanel.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/base/content/browser-sitePermissionPanel.js')
-rw-r--r--browser/base/content/browser-sitePermissionPanel.js1122
1 files changed, 1122 insertions, 0 deletions
diff --git a/browser/base/content/browser-sitePermissionPanel.js b/browser/base/content/browser-sitePermissionPanel.js
new file mode 100644
index 0000000000..d81b636668
--- /dev/null
+++ b/browser/base/content/browser-sitePermissionPanel.js
@@ -0,0 +1,1122 @@
+/* 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/. */
+
+/* eslint-env mozilla/browser-window */
+
+/**
+ * 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);
+
+ window.ensureCustomElements("moz-support-link");
+
+ this._popupInitialized = true;
+ }
+ },
+
+ 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) {
+ if (permission.state != SitePermissions.UNKNOWN) {
+ 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 are blocked
+ // irrespective of popup permission capability value.
+ if (gBrowser.selectedBrowser.popupBlocker.getBlockedPopupCount()) {
+ 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);
+ },
+
+ onPopupShown(event) {
+ if (event.target == this._permissionPopup) {
+ window.addEventListener("focus", this, true);
+ }
+ },
+
+ onPopupHidden(event) {
+ if (event.target == this._permissionPopup) {
+ window.removeEventListener("focus", this, true);
+ }
+ },
+
+ handleEvent(event) {
+ 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);
+ }
+ },
+
+ observe(subject, topic, data) {
+ 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 permissions are in use, we need to find
+ // the associated 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) {
+ continue;
+ }
+ found = true;
+ permission.sharingState = webrtcState[id];
+ }
+ if (!found) {
+ // If the 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.popupBlocker.getBlockedPopupCount();
+ let hasBlockedPopupIndicator = 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)) {
+ 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);
+ }
+
+ if (id == "popup" && totalBlockedPopups) {
+ this._createBlockedPopupIndicator(totalBlockedPopups);
+ hasBlockedPopupIndicator = true;
+ } else if (id == "geo" && permission.state === SitePermissions.ALLOW) {
+ this._createGeoLocationLastAccessIndicator();
+ }
+ }
+
+ if (totalBlockedPopups && !hasBlockedPopupIndicator) {
+ let permission = {
+ id: "popup",
+ state: SitePermissions.getDefault("popup"),
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ };
+ let item = this._createPermissionItem({ permission });
+ this._defaultPermissionAnchor.appendChild(item);
+ this._createBlockedPopupIndicator(totalBlockedPopups);
+ }
+ },
+
+ /**
+ * 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 we have a single-key (not device specific) webRTC permission 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;
+ },
+
+ _createBlockedPopupIndicator(aTotalBlockedPopups) {
+ 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");
+ let text = document.createXULElement("label", { is: "text-link" });
+ text.setAttribute("class", "permission-popup-permission-label");
+ document.l10n.setAttributes(text, "site-permissions-open-blocked-popups", {
+ count: aTotalBlockedPopups,
+ });
+
+ text.addEventListener("click", () => {
+ gBrowser.selectedBrowser.popupBlocker.unblockAllPopups();
+ });
+
+ indicator.appendChild(text);
+
+ 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 };
+}