1149 lines
37 KiB
JavaScript
1149 lines
37 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);
|
|
this._popupInitialized = true;
|
|
this._permissionPopup.addEventListener("popupshown", this);
|
|
this._permissionPopup.addEventListener("popuphidden", this);
|
|
}
|
|
},
|
|
|
|
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) {
|
|
// Don't show persisted PROMPT permissions (unless a pref says to).
|
|
// These would appear as "Always Ask ✖" which have utility, but might confuse
|
|
if (
|
|
permission.state == SitePermissions.UNKNOWN ||
|
|
(permission.state == SitePermissions.PROMPT && !this._gumShowAlwaysAsk)
|
|
) {
|
|
continue;
|
|
}
|
|
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);
|
|
},
|
|
|
|
handleEvent(event) {
|
|
switch (event.type) {
|
|
case "popupshown":
|
|
if (event.target == this._permissionPopup) {
|
|
window.addEventListener("focus", this, true);
|
|
}
|
|
break;
|
|
case "popuphidden":
|
|
if (event.target == this._permissionPopup) {
|
|
window.removeEventListener("focus", this, true);
|
|
}
|
|
break;
|
|
case "focus":
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
},
|
|
|
|
observe(subject, topic) {
|
|
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 are in use, we need to find
|
|
// the associated ALLOW 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 || permission.state != SitePermissions.ALLOW) {
|
|
continue;
|
|
}
|
|
found = true;
|
|
permission.sharingState = webrtcState[id];
|
|
}
|
|
if (!found) {
|
|
// If the ALLOW 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)) {
|
|
if (
|
|
permission.state == SitePermissions.PROMPT &&
|
|
!this._gumShowAlwaysAsk
|
|
) {
|
|
continue;
|
|
}
|
|
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 (permission.state == SitePermissions.PROMPT) {
|
|
return null;
|
|
}
|
|
// If we have a single-key (not device specific) webRTC permission
|
|
// other than PROMPT, 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 };
|
|
}
|
|
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
gPermissionPanel,
|
|
"_gumShowAlwaysAsk",
|
|
"permissions.media.show_always_ask.enabled",
|
|
false
|
|
);
|