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