diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /browser/base/content/browser-sitePermissionPanel.js | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | browser/base/content/browser-sitePermissionPanel.js | 1122 |
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 }; +} |