forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			1122 lines
		
	
	
	
		
			36 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1122 lines
		
	
	
	
		
			36 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* 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 };
 | |
| }
 | 
