Source code

Revision control

Copy as Markdown

Other Tools

/* 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() {
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) {
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 };
}