forked from mirrors/gecko-dev
Do not specify that browser/tab sharing will be using OS prompt because xdg-desktop-portal has no information about the browser and its tabs. Also do not make the xdg-desktop-portal placeholder as the only item on the list in the picker, because we want to see tabs there and these can also show previews, which were completely disabled if PipeWire is used. Lastly avoid creating wrong capturer when tabs are being shared. Differential Revision: https://phabricator.services.mozilla.com/D170765
1479 lines
50 KiB
JavaScript
1479 lines
50 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/. */
|
|
|
|
"use strict";
|
|
|
|
var EXPORTED_SYMBOLS = ["WebRTCParent"];
|
|
|
|
const { XPCOMUtils } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/XPCOMUtils.sys.mjs"
|
|
);
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
|
|
});
|
|
ChromeUtils.defineModuleGetter(
|
|
lazy,
|
|
"SitePermissions",
|
|
"resource:///modules/SitePermissions.jsm"
|
|
);
|
|
ChromeUtils.defineModuleGetter(
|
|
lazy,
|
|
"webrtcUI",
|
|
"resource:///modules/webrtcUI.jsm"
|
|
);
|
|
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
lazy,
|
|
"OSPermissions",
|
|
"@mozilla.org/ospermissionrequest;1",
|
|
"nsIOSPermissionRequest"
|
|
);
|
|
|
|
class WebRTCParent extends JSWindowActorParent {
|
|
didDestroy() {
|
|
// Media stream tracks end on unload, so call stopRecording() on them early
|
|
// *before* we go away, to ensure we're working with the right principal.
|
|
this.stopRecording(this.manager.outerWindowId);
|
|
lazy.webrtcUI.forgetStreamsFromBrowserContext(this.browsingContext);
|
|
// Must clear activePerms here to prevent them from being read by laggard
|
|
// stopRecording() calls, which due to IPC, may come in *after* navigation.
|
|
// This is to prevent granting temporary grace periods to the wrong page.
|
|
lazy.webrtcUI.activePerms.delete(this.manager.outerWindowId);
|
|
}
|
|
|
|
getBrowser() {
|
|
return this.browsingContext.top.embedderElement;
|
|
}
|
|
|
|
receiveMessage(aMessage) {
|
|
switch (aMessage.name) {
|
|
case "rtcpeer:Request": {
|
|
let params = Object.freeze(
|
|
Object.assign(
|
|
{
|
|
origin: this.manager.documentPrincipal.origin,
|
|
},
|
|
aMessage.data
|
|
)
|
|
);
|
|
|
|
let blockers = Array.from(lazy.webrtcUI.peerConnectionBlockers);
|
|
|
|
(async function() {
|
|
for (let blocker of blockers) {
|
|
try {
|
|
let result = await blocker(params);
|
|
if (result == "deny") {
|
|
return false;
|
|
}
|
|
} catch (err) {
|
|
console.error(`error in PeerConnection blocker: ${err.message}`);
|
|
}
|
|
}
|
|
return true;
|
|
})().then(decision => {
|
|
let message;
|
|
if (decision) {
|
|
lazy.webrtcUI.emitter.emit("peer-request-allowed", params);
|
|
message = "rtcpeer:Allow";
|
|
} else {
|
|
lazy.webrtcUI.emitter.emit("peer-request-blocked", params);
|
|
message = "rtcpeer:Deny";
|
|
}
|
|
|
|
this.sendAsyncMessage(message, {
|
|
callID: params.callID,
|
|
windowID: params.windowID,
|
|
});
|
|
});
|
|
break;
|
|
}
|
|
case "rtcpeer:CancelRequest": {
|
|
let params = Object.freeze({
|
|
origin: this.manager.documentPrincipal.origin,
|
|
callID: aMessage.data,
|
|
});
|
|
lazy.webrtcUI.emitter.emit("peer-request-cancel", params);
|
|
break;
|
|
}
|
|
case "webrtc:Request": {
|
|
let data = aMessage.data;
|
|
|
|
// Record third party origins for telemetry.
|
|
let isThirdPartyOrigin =
|
|
this.manager.documentPrincipal.origin !=
|
|
this.manager.topWindowContext.documentPrincipal.origin;
|
|
data.isThirdPartyOrigin = isThirdPartyOrigin;
|
|
|
|
data.origin = data.shouldDelegatePermission
|
|
? this.manager.topWindowContext.documentPrincipal.origin
|
|
: this.manager.documentPrincipal.origin;
|
|
|
|
let browser = this.getBrowser();
|
|
if (browser.fxrPermissionPrompt) {
|
|
// For Firefox Reality on Desktop, switch to a different mechanism to
|
|
// prompt the user since fewer permissions are available and since many
|
|
// UI dependencies are not available.
|
|
browser.fxrPermissionPrompt(data);
|
|
} else {
|
|
prompt(this, this.getBrowser(), data);
|
|
}
|
|
break;
|
|
}
|
|
case "webrtc:StopRecording":
|
|
this.stopRecording(
|
|
aMessage.data.windowID,
|
|
aMessage.data.mediaSource,
|
|
aMessage.data.rawID
|
|
);
|
|
break;
|
|
case "webrtc:CancelRequest": {
|
|
let browser = this.getBrowser();
|
|
// browser can be null when closing the window
|
|
if (browser) {
|
|
removePrompt(browser, aMessage.data);
|
|
}
|
|
break;
|
|
}
|
|
case "webrtc:UpdateIndicators": {
|
|
let { data } = aMessage;
|
|
data.documentURI = this.manager.documentURI?.spec;
|
|
if (data.windowId) {
|
|
if (!data.remove) {
|
|
data.principal = this.manager.topWindowContext.documentPrincipal;
|
|
}
|
|
lazy.webrtcUI.streamAddedOrRemoved(this.browsingContext, data);
|
|
}
|
|
this.updateIndicators(data);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
updateIndicators(aData) {
|
|
let browsingContext = this.browsingContext;
|
|
let state = lazy.webrtcUI.updateIndicators(browsingContext.top);
|
|
|
|
let browser = this.getBrowser();
|
|
if (!browser) {
|
|
return;
|
|
}
|
|
|
|
state.browsingContext = browsingContext;
|
|
state.windowId = aData.windowId;
|
|
|
|
let tabbrowser = browser.ownerGlobal.gBrowser;
|
|
if (tabbrowser) {
|
|
tabbrowser.updateBrowserSharing(browser, {
|
|
webRTC: state,
|
|
});
|
|
}
|
|
}
|
|
|
|
denyRequest(aRequest) {
|
|
this.sendAsyncMessage("webrtc:Deny", {
|
|
callID: aRequest.callID,
|
|
windowID: aRequest.windowID,
|
|
});
|
|
}
|
|
|
|
//
|
|
// Deny the request because the browser does not have access to the
|
|
// camera or microphone due to OS security restrictions. The user may
|
|
// have granted camera/microphone access to the site, but not have
|
|
// allowed the browser access in OS settings.
|
|
//
|
|
denyRequestNoPermission(aRequest) {
|
|
this.sendAsyncMessage("webrtc:Deny", {
|
|
callID: aRequest.callID,
|
|
windowID: aRequest.windowID,
|
|
noOSPermission: true,
|
|
});
|
|
}
|
|
|
|
//
|
|
// Check if we have permission to access the camera or screen-sharing and/or
|
|
// microphone at the OS level. Triggers a request to access the device if access
|
|
// is needed and the permission state has not yet been determined.
|
|
//
|
|
async checkOSPermission(camNeeded, micNeeded, scrNeeded) {
|
|
// Don't trigger OS permission requests for fake devices. Fake devices don't
|
|
// require OS permission and the dialogs are problematic in automated testing
|
|
// (where fake devices are used) because they require user interaction.
|
|
if (
|
|
!scrNeeded &&
|
|
Services.prefs.getBoolPref("media.navigator.streams.fake", false)
|
|
) {
|
|
return true;
|
|
}
|
|
let camStatus = {},
|
|
micStatus = {};
|
|
if (camNeeded || micNeeded) {
|
|
lazy.OSPermissions.getMediaCapturePermissionState(camStatus, micStatus);
|
|
}
|
|
if (camNeeded) {
|
|
let camPermission = camStatus.value;
|
|
let camAccessible = await this.checkAndGetOSPermission(
|
|
camPermission,
|
|
lazy.OSPermissions.requestVideoCapturePermission
|
|
);
|
|
if (!camAccessible) {
|
|
return false;
|
|
}
|
|
}
|
|
if (micNeeded) {
|
|
let micPermission = micStatus.value;
|
|
let micAccessible = await this.checkAndGetOSPermission(
|
|
micPermission,
|
|
lazy.OSPermissions.requestAudioCapturePermission
|
|
);
|
|
if (!micAccessible) {
|
|
return false;
|
|
}
|
|
}
|
|
let scrStatus = {};
|
|
if (scrNeeded) {
|
|
lazy.OSPermissions.getScreenCapturePermissionState(scrStatus);
|
|
if (scrStatus.value == lazy.OSPermissions.PERMISSION_STATE_DENIED) {
|
|
lazy.OSPermissions.maybeRequestScreenCapturePermission();
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//
|
|
// Given a device's permission, return true if the device is accessible. If
|
|
// the device's permission is not yet determined, request access to the device.
|
|
// |requestPermissionFunc| must return a promise that resolves with true
|
|
// if the device is accessible and false otherwise.
|
|
//
|
|
async checkAndGetOSPermission(devicePermission, requestPermissionFunc) {
|
|
if (
|
|
devicePermission == lazy.OSPermissions.PERMISSION_STATE_DENIED ||
|
|
devicePermission == lazy.OSPermissions.PERMISSION_STATE_RESTRICTED
|
|
) {
|
|
return false;
|
|
}
|
|
if (devicePermission == lazy.OSPermissions.PERMISSION_STATE_NOTDETERMINED) {
|
|
let deviceAllowed = await requestPermissionFunc();
|
|
if (!deviceAllowed) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
stopRecording(aOuterWindowId, aMediaSource, aRawId) {
|
|
for (let { browsingContext, state } of lazy.webrtcUI._streams) {
|
|
if (browsingContext == this.browsingContext) {
|
|
let { principal } = state;
|
|
for (let { mediaSource, rawId } of state.devices) {
|
|
if (aRawId && (aRawId != rawId || aMediaSource != mediaSource)) {
|
|
continue;
|
|
}
|
|
// Deactivate this device (no aRawId means all devices).
|
|
this.deactivateDevicePerm(
|
|
aOuterWindowId,
|
|
mediaSource,
|
|
rawId,
|
|
principal
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a device record to webrtcUI.activePerms, denoting a device as in use.
|
|
* Important to call for permission grace periods to work correctly.
|
|
*/
|
|
activateDevicePerm(aOuterWindowId, aMediaSource, aId) {
|
|
if (!lazy.webrtcUI.activePerms.has(this.manager.outerWindowId)) {
|
|
lazy.webrtcUI.activePerms.set(this.manager.outerWindowId, new Map());
|
|
}
|
|
lazy.webrtcUI.activePerms
|
|
.get(this.manager.outerWindowId)
|
|
.set(aOuterWindowId + aMediaSource + aId, aMediaSource);
|
|
}
|
|
|
|
/**
|
|
* Remove a device record from webrtcUI.activePerms, denoting a device as
|
|
* no longer in use by the site. Meaning: gUM requests for this device will
|
|
* no longer be implicitly granted through the webrtcUI.activePerms mechanism.
|
|
*
|
|
* However, if webrtcUI.deviceGracePeriodTimeoutMs is defined, the implicit
|
|
* grant is extended for an additional period of time through SitePermissions.
|
|
*/
|
|
deactivateDevicePerm(
|
|
aOuterWindowId,
|
|
aMediaSource,
|
|
aId,
|
|
aPermissionPrincipal
|
|
) {
|
|
// If we don't have active permissions for the given window anymore don't
|
|
// set a grace period. This happens if there has been a user revoke and
|
|
// webrtcUI clears the permissions.
|
|
if (!lazy.webrtcUI.activePerms.has(this.manager.outerWindowId)) {
|
|
return;
|
|
}
|
|
let map = lazy.webrtcUI.activePerms.get(this.manager.outerWindowId);
|
|
map.delete(aOuterWindowId + aMediaSource + aId);
|
|
|
|
// Add a permission grace period for camera and microphone only
|
|
if (
|
|
(aMediaSource != "camera" && aMediaSource != "microphone") ||
|
|
!this.browsingContext.top.embedderElement
|
|
) {
|
|
return;
|
|
}
|
|
let gracePeriodMs = lazy.webrtcUI.deviceGracePeriodTimeoutMs;
|
|
if (gracePeriodMs > 0) {
|
|
// A grace period is extended (even past navigation) to this outer window
|
|
// + origin + deviceId only. This avoids re-prompting without the user
|
|
// having to persist permission to the site, in a common case of a web
|
|
// conference asking them for the camera in a lobby page, before
|
|
// navigating to the actual meeting room page. Does not survive tab close.
|
|
//
|
|
// Caution: since navigation causes deactivation, we may be in the middle
|
|
// of one. We must pass in a principal & URI for SitePermissions to use
|
|
// instead of browser.currentURI, because the latter may point to a new
|
|
// page already, and we must not leak permission to unrelated pages.
|
|
//
|
|
let permissionName = [aMediaSource, aId].join("^");
|
|
lazy.SitePermissions.setForPrincipal(
|
|
aPermissionPrincipal,
|
|
permissionName,
|
|
lazy.SitePermissions.ALLOW,
|
|
lazy.SitePermissions.SCOPE_TEMPORARY,
|
|
this.browsingContext.top.embedderElement,
|
|
gracePeriodMs
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if the principal has sufficient permissions
|
|
* to fulfill the given request. If the request can be
|
|
* fulfilled, a message is sent to the child
|
|
* signaling that WebRTC permissions were given and
|
|
* this function will return true.
|
|
*/
|
|
checkRequestAllowed(aRequest, aPrincipal) {
|
|
if (!aRequest.secure) {
|
|
return false;
|
|
}
|
|
// Always prompt for screen sharing
|
|
if (aRequest.sharingScreen) {
|
|
return false;
|
|
}
|
|
let {
|
|
callID,
|
|
windowID,
|
|
audioInputDevices,
|
|
videoInputDevices,
|
|
audioOutputDevices,
|
|
hasInherentAudioConstraints,
|
|
hasInherentVideoConstraints,
|
|
audioOutputId,
|
|
} = aRequest;
|
|
|
|
if (audioOutputDevices?.length) {
|
|
// Prompt if a specific device is not requested, available and allowed.
|
|
let device = audioOutputDevices.find(({ id }) => id == audioOutputId);
|
|
if (
|
|
!device ||
|
|
!lazy.SitePermissions.getForPrincipal(
|
|
aPrincipal,
|
|
["speaker", device.id].join("^"),
|
|
this.getBrowser()
|
|
).state == lazy.SitePermissions.ALLOW
|
|
) {
|
|
return false;
|
|
}
|
|
this.sendAsyncMessage("webrtc:Allow", {
|
|
callID,
|
|
windowID,
|
|
devices: [device.deviceIndex],
|
|
});
|
|
return true;
|
|
}
|
|
|
|
let { perms } = Services;
|
|
if (
|
|
perms.testExactPermissionFromPrincipal(aPrincipal, "MediaManagerVideo")
|
|
) {
|
|
perms.removeFromPrincipal(aPrincipal, "MediaManagerVideo");
|
|
}
|
|
|
|
// Don't use persistent permissions from the top-level principal
|
|
// if we're in a cross-origin iframe and permission delegation is not
|
|
// allowed, or when we're handling a potentially insecure third party
|
|
// through a wildcard ("*") allow attribute.
|
|
let limited =
|
|
(aRequest.isThirdPartyOrigin && !aRequest.shouldDelegatePermission) ||
|
|
aRequest.secondOrigin;
|
|
|
|
let map = lazy.webrtcUI.activePerms.get(this.manager.outerWindowId);
|
|
// We consider a camera or mic active if it is active or was active within a
|
|
// grace period of milliseconds ago.
|
|
const isAllowed = ({ mediaSource, rawId }, permissionID) =>
|
|
map?.get(windowID + mediaSource + rawId) ||
|
|
(!limited &&
|
|
(lazy.SitePermissions.getForPrincipal(aPrincipal, permissionID).state ==
|
|
lazy.SitePermissions.ALLOW ||
|
|
lazy.SitePermissions.getForPrincipal(
|
|
aPrincipal,
|
|
[mediaSource, rawId].join("^"),
|
|
this.getBrowser()
|
|
).state == lazy.SitePermissions.ALLOW));
|
|
|
|
let microphone;
|
|
if (audioInputDevices.length) {
|
|
for (let device of audioInputDevices) {
|
|
if (isAllowed(device, "microphone")) {
|
|
microphone = device;
|
|
break;
|
|
}
|
|
if (hasInherentAudioConstraints) {
|
|
// Inherent constraints suggest site is looking for a specific mic
|
|
break;
|
|
}
|
|
// Some sites don't look too hard at what they get, and spam gUM without
|
|
// adjusting what they ask for to match what they got last time. To keep
|
|
// users in charge and reduce prompts, ignore other constraints by
|
|
// returning the most-fit microphone a site already has access to.
|
|
}
|
|
if (!microphone) {
|
|
return false;
|
|
}
|
|
}
|
|
let camera;
|
|
if (videoInputDevices.length) {
|
|
for (let device of videoInputDevices) {
|
|
if (isAllowed(device, "camera")) {
|
|
camera = device;
|
|
break;
|
|
}
|
|
if (hasInherentVideoConstraints) {
|
|
// Inherent constraints suggest site is looking for a specific camera
|
|
break;
|
|
}
|
|
// Some sites don't look too hard at what they get, and spam gUM without
|
|
// adjusting what they ask for to match what they got last time. To keep
|
|
// users in charge and reduce prompts, ignore other constraints by
|
|
// returning the most-fit camera a site already has access to.
|
|
}
|
|
if (!camera) {
|
|
return false;
|
|
}
|
|
}
|
|
let devices = [];
|
|
if (camera) {
|
|
perms.addFromPrincipal(
|
|
aPrincipal,
|
|
"MediaManagerVideo",
|
|
perms.ALLOW_ACTION,
|
|
perms.EXPIRE_SESSION
|
|
);
|
|
devices.push(camera.deviceIndex);
|
|
this.activateDevicePerm(windowID, camera.mediaSource, camera.rawId);
|
|
}
|
|
if (microphone) {
|
|
devices.push(microphone.deviceIndex);
|
|
this.activateDevicePerm(
|
|
windowID,
|
|
microphone.mediaSource,
|
|
microphone.rawId
|
|
);
|
|
}
|
|
this.checkOSPermission(!!camera, !!microphone, false).then(
|
|
havePermission => {
|
|
if (havePermission) {
|
|
this.sendAsyncMessage("webrtc:Allow", { callID, windowID, devices });
|
|
} else {
|
|
this.denyRequestNoPermission(aRequest);
|
|
}
|
|
}
|
|
);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
function prompt(aActor, aBrowser, aRequest) {
|
|
let {
|
|
audioInputDevices,
|
|
videoInputDevices,
|
|
audioOutputDevices,
|
|
sharingScreen,
|
|
sharingAudio,
|
|
requestTypes,
|
|
} = aRequest;
|
|
|
|
let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
|
|
aRequest.origin
|
|
);
|
|
|
|
// For add-on principals, we immediately check for permission instead
|
|
// of waiting for the notification to focus. This allows for supporting
|
|
// cases such as browserAction popups where no prompt is shown.
|
|
if (principal.addonPolicy) {
|
|
let isPopup = false;
|
|
let isBackground = false;
|
|
|
|
for (let view of principal.addonPolicy.extension.views) {
|
|
if (view.viewType == "popup" && view.xulBrowser == aBrowser) {
|
|
isPopup = true;
|
|
}
|
|
if (view.viewType == "background" && view.xulBrowser == aBrowser) {
|
|
isBackground = true;
|
|
}
|
|
}
|
|
|
|
// Recording from background pages is considered too sensitive and will
|
|
// always be denied.
|
|
if (isBackground) {
|
|
aActor.denyRequest(aRequest);
|
|
return;
|
|
}
|
|
|
|
// If the request comes from a popup, we don't want to show the prompt,
|
|
// but we do want to allow the request if the user previously gave permission.
|
|
if (isPopup) {
|
|
if (!aActor.checkRequestAllowed(aRequest, principal, aBrowser)) {
|
|
aActor.denyRequest(aRequest);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
// If the user has already denied access once in this tab,
|
|
// deny again without even showing the notification icon.
|
|
for (const type of requestTypes) {
|
|
const permissionID =
|
|
type == "AudioCapture" ? "microphone" : type.toLowerCase();
|
|
if (
|
|
lazy.SitePermissions.getForPrincipal(principal, permissionID, aBrowser)
|
|
.state == lazy.SitePermissions.BLOCK
|
|
) {
|
|
aActor.denyRequest(aRequest);
|
|
return;
|
|
}
|
|
}
|
|
|
|
let chromeDoc = aBrowser.ownerDocument;
|
|
const localization = new Localization(
|
|
["browser/webrtcIndicator.ftl", "branding/brand.ftl"],
|
|
true
|
|
);
|
|
|
|
/** @type {"Screen" | "Camera" | null} */
|
|
let reqVideoInput = null;
|
|
if (videoInputDevices.length) {
|
|
reqVideoInput = sharingScreen ? "Screen" : "Camera";
|
|
}
|
|
/** @type {"AudioCapture" | "Microphone" | null} */
|
|
let reqAudioInput = null;
|
|
if (audioInputDevices.length) {
|
|
reqAudioInput = sharingAudio ? "AudioCapture" : "Microphone";
|
|
}
|
|
const reqAudioOutput = !!audioOutputDevices.length;
|
|
|
|
const stringId = getPromptMessageId(
|
|
reqVideoInput,
|
|
reqAudioInput,
|
|
reqAudioOutput,
|
|
!!aRequest.secondOrigin
|
|
);
|
|
const message = localization.formatValueSync(stringId, {
|
|
origin: "<>",
|
|
thirdParty: "{}",
|
|
});
|
|
|
|
let notification; // Used by action callbacks.
|
|
const actionL10nIds = [{ id: "webrtc-action-allow" }];
|
|
|
|
let notificationSilencingEnabled = Services.prefs.getBoolPref(
|
|
"privacy.webrtc.allowSilencingNotifications"
|
|
);
|
|
|
|
const isNotNowLabelEnabled =
|
|
reqAudioOutput || allowedOrActiveCameraOrMicrophone(aBrowser);
|
|
let secondaryActions = [];
|
|
if (reqAudioOutput || (notificationSilencingEnabled && sharingScreen)) {
|
|
// We want to free up the checkbox at the bottom of the permission
|
|
// panel for the notification silencing option, so we use a
|
|
// different configuration for the permissions panel when
|
|
// notification silencing is enabled.
|
|
|
|
let permissionName = reqAudioOutput ? "speaker" : "screen";
|
|
// When selecting speakers, we always offer 'Not now' instead of 'Block'.
|
|
// When selecting screens, we offer 'Not now' if and only if we have a
|
|
// (temporary) allow permission for some mic/cam device.
|
|
const id = isNotNowLabelEnabled
|
|
? "webrtc-action-not-now"
|
|
: "webrtc-action-block";
|
|
actionL10nIds.push({ id }, { id: "webrtc-action-always-block" });
|
|
secondaryActions = [
|
|
{
|
|
callback(aState) {
|
|
aActor.denyRequest(aRequest);
|
|
if (!isNotNowLabelEnabled) {
|
|
lazy.SitePermissions.setForPrincipal(
|
|
principal,
|
|
permissionName,
|
|
lazy.SitePermissions.BLOCK,
|
|
lazy.SitePermissions.SCOPE_TEMPORARY,
|
|
notification.browser
|
|
);
|
|
}
|
|
},
|
|
},
|
|
{
|
|
callback(aState) {
|
|
aActor.denyRequest(aRequest);
|
|
lazy.SitePermissions.setForPrincipal(
|
|
principal,
|
|
permissionName,
|
|
lazy.SitePermissions.BLOCK,
|
|
lazy.SitePermissions.SCOPE_PERSISTENT,
|
|
notification.browser
|
|
);
|
|
},
|
|
},
|
|
];
|
|
} else {
|
|
// We have a (temporary) allow permission for some device
|
|
// hence we offer a 'Not now' label instead of 'Block'.
|
|
const id = isNotNowLabelEnabled
|
|
? "webrtc-action-not-now"
|
|
: "webrtc-action-block";
|
|
actionL10nIds.push({ id });
|
|
secondaryActions = [
|
|
{
|
|
callback(aState) {
|
|
aActor.denyRequest(aRequest);
|
|
|
|
const isPersistent = aState?.checkboxChecked;
|
|
|
|
// Choosing 'Not now' will not set a block permission
|
|
// we just deny the request. This enables certain use cases
|
|
// where sites want to switch devices, but users back out of the permission request
|
|
// (See Bug 1609578).
|
|
// Selecting 'Remember this decision' and clicking 'Not now' will set a persistent block
|
|
if (!isPersistent && isNotNowLabelEnabled) {
|
|
return;
|
|
}
|
|
|
|
// Denying a camera / microphone prompt means we set a temporary or
|
|
// persistent permission block. There may still be active grace period
|
|
// permissions at this point. We need to remove them.
|
|
clearTemporaryGrants(
|
|
notification.browser,
|
|
reqVideoInput === "Camera",
|
|
!!reqAudioInput
|
|
);
|
|
|
|
const scope = isPersistent
|
|
? lazy.SitePermissions.SCOPE_PERSISTENT
|
|
: lazy.SitePermissions.SCOPE_TEMPORARY;
|
|
if (reqAudioInput) {
|
|
lazy.SitePermissions.setForPrincipal(
|
|
principal,
|
|
"microphone",
|
|
lazy.SitePermissions.BLOCK,
|
|
scope,
|
|
notification.browser
|
|
);
|
|
}
|
|
if (reqVideoInput) {
|
|
lazy.SitePermissions.setForPrincipal(
|
|
principal,
|
|
sharingScreen ? "screen" : "camera",
|
|
lazy.SitePermissions.BLOCK,
|
|
scope,
|
|
notification.browser
|
|
);
|
|
}
|
|
},
|
|
},
|
|
];
|
|
}
|
|
|
|
// The formatMessagesSync method returns an array of results
|
|
// for each message that was requested, and for the ones with
|
|
// attributes, returns an attributes array with objects like:
|
|
// { name: "label", value: "somevalue" }
|
|
const [mainMessage, ...secondaryMessages] = localization
|
|
.formatMessagesSync(actionL10nIds)
|
|
.map(msg =>
|
|
msg.attributes.reduce(
|
|
(acc, { name, value }) => ({ ...acc, [name]: value }),
|
|
{}
|
|
)
|
|
);
|
|
|
|
const mainAction = {
|
|
label: mainMessage.label,
|
|
accessKey: mainMessage.accesskey,
|
|
// The real callback will be set during the "showing" event. The
|
|
// empty function here is so that PopupNotifications.show doesn't
|
|
// reject the action.
|
|
callback() {},
|
|
};
|
|
|
|
for (let i = 0; i < secondaryActions.length; ++i) {
|
|
secondaryActions[i].label = secondaryMessages[i].label;
|
|
secondaryActions[i].accessKey = secondaryMessages[i].accesskey;
|
|
}
|
|
|
|
let options = {
|
|
name: lazy.webrtcUI.getHostOrExtensionName(principal.URI),
|
|
persistent: true,
|
|
hideClose: true,
|
|
eventCallback(aTopic, aNewBrowser, isCancel) {
|
|
if (aTopic == "swapping") {
|
|
return true;
|
|
}
|
|
|
|
let doc = this.browser.ownerDocument;
|
|
|
|
// Clean-up video streams of screensharing previews.
|
|
if (
|
|
reqVideoInput !== "Screen" ||
|
|
aTopic == "dismissed" ||
|
|
aTopic == "removed"
|
|
) {
|
|
let video = doc.getElementById("webRTC-previewVideo");
|
|
video.deviceId = null; // Abort previews still being started.
|
|
if (video.stream) {
|
|
video.stream.getTracks().forEach(t => t.stop());
|
|
video.stream = null;
|
|
video.src = null;
|
|
doc.getElementById("webRTC-preview").hidden = true;
|
|
}
|
|
let menupopup = doc.getElementById("webRTC-selectWindow-menupopup");
|
|
if (menupopup._commandEventListener) {
|
|
menupopup.removeEventListener(
|
|
"command",
|
|
menupopup._commandEventListener
|
|
);
|
|
menupopup._commandEventListener = null;
|
|
}
|
|
}
|
|
|
|
// If the notification has been cancelled (e.g. due to entering full-screen), also cancel the webRTC request
|
|
if (aTopic == "removed" && notification && isCancel) {
|
|
aActor.denyRequest(aRequest);
|
|
}
|
|
|
|
if (aTopic != "showing") {
|
|
return false;
|
|
}
|
|
|
|
// If BLOCK has been set persistently in the permission manager or has
|
|
// been set on the tab, then it is handled synchronously before we add
|
|
// the notification.
|
|
// Handling of ALLOW is delayed until the popupshowing event,
|
|
// to avoid granting permissions automatically to background tabs.
|
|
if (aActor.checkRequestAllowed(aRequest, principal, aBrowser)) {
|
|
this.remove();
|
|
return true;
|
|
}
|
|
|
|
function listDevices(menupopup, devices, labelID) {
|
|
while (menupopup.lastChild) {
|
|
menupopup.removeChild(menupopup.lastChild);
|
|
}
|
|
let menulist = menupopup.parentNode;
|
|
// Removing the child nodes of the menupopup doesn't clear the value
|
|
// attribute of the menulist. This can have unfortunate side effects
|
|
// when the list is rebuilt with a different content, so we remove
|
|
// the value attribute and unset the selectedItem explicitly.
|
|
menulist.removeAttribute("value");
|
|
menulist.selectedItem = null;
|
|
|
|
for (let device of devices) {
|
|
let item = addDeviceToList(
|
|
menupopup,
|
|
device.name,
|
|
device.deviceIndex
|
|
);
|
|
if (device.id == aRequest.audioOutputId) {
|
|
menulist.selectedItem = item;
|
|
}
|
|
}
|
|
|
|
let label = doc.getElementById(labelID);
|
|
if (devices.length == 1) {
|
|
label.value = devices[0].name;
|
|
label.hidden = false;
|
|
menulist.hidden = true;
|
|
} else {
|
|
label.hidden = true;
|
|
menulist.hidden = false;
|
|
}
|
|
}
|
|
|
|
let notificationElement = doc.getElementById(
|
|
"webRTC-shareDevices-notification"
|
|
);
|
|
|
|
function checkDisabledWindowMenuItem() {
|
|
let list = doc.getElementById("webRTC-selectWindow-menulist");
|
|
let item = list.selectedItem;
|
|
if (!item || item.hasAttribute("disabled")) {
|
|
notificationElement.setAttribute("invalidselection", "true");
|
|
} else {
|
|
notificationElement.removeAttribute("invalidselection");
|
|
}
|
|
}
|
|
|
|
function listScreenShareDevices(menupopup, devices) {
|
|
while (menupopup.lastChild) {
|
|
menupopup.removeChild(menupopup.lastChild);
|
|
}
|
|
|
|
// Removing the child nodes of the menupopup doesn't clear the value
|
|
// attribute of the menulist. This can have unfortunate side effects
|
|
// when the list is rebuilt with a different content, so we remove
|
|
// the value attribute and unset the selectedItem explicitly.
|
|
menupopup.parentNode.removeAttribute("value");
|
|
menupopup.parentNode.selectedItem = null;
|
|
|
|
// "Select a Window or Screen" is the default because we can't and don't
|
|
// want to pick a 'default' window to share (Full screen is "scary").
|
|
addDeviceToList(
|
|
menupopup,
|
|
localization.formatValueSync("webrtc-pick-window-or-screen"),
|
|
"-1"
|
|
);
|
|
menupopup.appendChild(doc.createXULElement("menuseparator"));
|
|
|
|
let isPipeWireDetected = false;
|
|
|
|
// Build the list of 'devices'.
|
|
let monitorIndex = 1;
|
|
for (let i = 0; i < devices.length; ++i) {
|
|
let device = devices[i];
|
|
let type = device.mediaSource;
|
|
let name;
|
|
if (device.canRequestOsLevelPrompt) {
|
|
// When we share content by PipeWire add only one item to the device
|
|
// list. When it's selected PipeWire portal dialog is opened and
|
|
// user confirms actual window/screen sharing there.
|
|
// Don't mark it as scary as there's an extra confirmation step by
|
|
// PipeWire portal dialog.
|
|
|
|
isPipeWireDetected = true;
|
|
let item = addDeviceToList(
|
|
menupopup,
|
|
localization.formatValueSync("webrtc-share-pipe-wire-portal"),
|
|
i,
|
|
type
|
|
);
|
|
item.deviceId = device.rawId;
|
|
item.mediaSource = type;
|
|
|
|
// In this case the OS sharing dialog will be the only option and
|
|
// can be safely pre-selected.
|
|
menupopup.parentNode.selectedItem = item;
|
|
continue;
|
|
} else if (type == "screen") {
|
|
// Building screen list from available screens.
|
|
if (device.name == "Primary Monitor") {
|
|
name = localization.formatValueSync("webrtc-share-entire-screen");
|
|
} else {
|
|
name = localization.formatValueSync("webrtc-share-monitor", {
|
|
monitorIndex,
|
|
});
|
|
++monitorIndex;
|
|
}
|
|
} else {
|
|
name = device.name;
|
|
|
|
if (type == "application") {
|
|
// The application names returned by the platform are of the form:
|
|
// <window count>\x1e<application name>
|
|
const [count, appName] = name.split("\x1e");
|
|
name = localization.formatValueSync("webrtc-share-application", {
|
|
appName,
|
|
windowCount: parseInt(count),
|
|
});
|
|
}
|
|
}
|
|
let item = addDeviceToList(menupopup, name, i, type);
|
|
item.deviceId = device.rawId;
|
|
item.mediaSource = type;
|
|
if (device.scary) {
|
|
item.scary = true;
|
|
}
|
|
}
|
|
|
|
// Always re-select the "No <type>" item.
|
|
doc
|
|
.getElementById("webRTC-selectWindow-menulist")
|
|
.removeAttribute("value");
|
|
doc.getElementById("webRTC-all-windows-shared").hidden = true;
|
|
|
|
menupopup._commandEventListener = event => {
|
|
checkDisabledWindowMenuItem();
|
|
let video = doc.getElementById("webRTC-previewVideo");
|
|
if (video.stream) {
|
|
video.stream.getTracks().forEach(t => t.stop());
|
|
video.stream = null;
|
|
}
|
|
|
|
const { deviceId, mediaSource, scary } = event.target;
|
|
if (deviceId == undefined) {
|
|
doc.getElementById("webRTC-preview").hidden = true;
|
|
video.src = null;
|
|
return;
|
|
}
|
|
|
|
let warning = doc.getElementById("webRTC-previewWarning");
|
|
let warningBox = doc.getElementById("webRTC-previewWarningBox");
|
|
warningBox.hidden = !scary;
|
|
let chromeWin = doc.defaultView;
|
|
if (scary) {
|
|
const warnId =
|
|
mediaSource == "screen"
|
|
? "webrtc-share-screen-warning"
|
|
: "webrtc-share-browser-warning";
|
|
doc.l10n.setAttributes(warning, warnId);
|
|
|
|
const learnMore = doc.getElementById(
|
|
"webRTC-previewWarning-learnMore"
|
|
);
|
|
const baseURL = Services.urlFormatter.formatURLPref(
|
|
"app.support.baseURL"
|
|
);
|
|
learnMore.setAttribute("href", baseURL + "screenshare-safety");
|
|
doc.l10n.setAttributes(learnMore, "webrtc-share-screen-learn-more");
|
|
|
|
// On Catalina, we don't want to blow our chance to show the
|
|
// OS-level helper prompt to enable screen recording if the user
|
|
// intends to reject anyway. OTOH showing it when they click Allow
|
|
// is too late. A happy middle is to show it when the user makes a
|
|
// choice in the picker. This already happens implicitly if the
|
|
// user chooses "Entire desktop", as a side-effect of our preview,
|
|
// we just need to also do it if they choose "Firefox". These are
|
|
// the lone two options when permission is absent on Catalina.
|
|
// Ironically, these are the two sources marked "scary" from a
|
|
// web-sharing perspective, which is why this code resides here.
|
|
// A restart doesn't appear to be necessary in spite of OS wording.
|
|
let scrStatus = {};
|
|
lazy.OSPermissions.getScreenCapturePermissionState(scrStatus);
|
|
if (scrStatus.value == lazy.OSPermissions.PERMISSION_STATE_DENIED) {
|
|
lazy.OSPermissions.maybeRequestScreenCapturePermission();
|
|
}
|
|
}
|
|
|
|
let perms = Services.perms;
|
|
let chromePrincipal = Services.scriptSecurityManager.getSystemPrincipal();
|
|
perms.addFromPrincipal(
|
|
chromePrincipal,
|
|
"MediaManagerVideo",
|
|
perms.ALLOW_ACTION,
|
|
perms.EXPIRE_SESSION
|
|
);
|
|
|
|
// We don't have access to any screen content besides our browser tabs
|
|
// on Wayland, therefore there are no previews we can show.
|
|
if (!isPipeWireDetected || mediaSource == "browser") {
|
|
video.deviceId = deviceId;
|
|
let constraints = {
|
|
video: { mediaSource, deviceId: { exact: deviceId } },
|
|
};
|
|
chromeWin.navigator.mediaDevices.getUserMedia(constraints).then(
|
|
stream => {
|
|
if (video.deviceId != deviceId) {
|
|
// The user has selected a different device or closed the panel
|
|
// before getUserMedia finished.
|
|
stream.getTracks().forEach(t => t.stop());
|
|
return;
|
|
}
|
|
video.srcObject = stream;
|
|
video.stream = stream;
|
|
doc.getElementById("webRTC-preview").hidden = false;
|
|
video.onloadedmetadata = function(e) {
|
|
video.play();
|
|
};
|
|
},
|
|
err => {
|
|
if (
|
|
err.name == "OverconstrainedError" &&
|
|
err.constraint == "deviceId"
|
|
) {
|
|
// Window has disappeared since enumeration, which can happen.
|
|
// No preview for you.
|
|
return;
|
|
}
|
|
console.error(
|
|
`error in preview: ${err.message} ${err.constraint}`
|
|
);
|
|
}
|
|
);
|
|
}
|
|
};
|
|
menupopup.addEventListener("command", menupopup._commandEventListener);
|
|
}
|
|
|
|
function addDeviceToList(menupopup, deviceName, deviceIndex, type) {
|
|
let menuitem = doc.createXULElement("menuitem");
|
|
menuitem.setAttribute("value", deviceIndex);
|
|
menuitem.setAttribute("label", deviceName);
|
|
menuitem.setAttribute("tooltiptext", deviceName);
|
|
if (type) {
|
|
menuitem.setAttribute("devicetype", type);
|
|
}
|
|
|
|
if (deviceIndex == "-1") {
|
|
menuitem.setAttribute("disabled", true);
|
|
}
|
|
|
|
menupopup.appendChild(menuitem);
|
|
return menuitem;
|
|
}
|
|
|
|
doc.getElementById("webRTC-selectCamera").hidden =
|
|
reqVideoInput !== "Camera";
|
|
doc.getElementById("webRTC-selectWindowOrScreen").hidden =
|
|
reqVideoInput !== "Screen";
|
|
doc.getElementById("webRTC-selectMicrophone").hidden =
|
|
reqAudioInput !== "Microphone";
|
|
doc.getElementById("webRTC-selectSpeaker").hidden = !reqAudioOutput;
|
|
|
|
let camMenupopup = doc.getElementById("webRTC-selectCamera-menupopup");
|
|
let windowMenupopup = doc.getElementById("webRTC-selectWindow-menupopup");
|
|
let micMenupopup = doc.getElementById(
|
|
"webRTC-selectMicrophone-menupopup"
|
|
);
|
|
let speakerMenupopup = doc.getElementById(
|
|
"webRTC-selectSpeaker-menupopup"
|
|
);
|
|
let describedByIDs = ["webRTC-shareDevices-notification-description"];
|
|
|
|
if (sharingScreen) {
|
|
listScreenShareDevices(windowMenupopup, videoInputDevices);
|
|
checkDisabledWindowMenuItem();
|
|
} else {
|
|
let labelID = "webRTC-selectCamera-single-device-label";
|
|
listDevices(camMenupopup, videoInputDevices, labelID);
|
|
notificationElement.removeAttribute("invalidselection");
|
|
if (videoInputDevices.length == 1) {
|
|
describedByIDs.push("webRTC-selectCamera-icon", labelID);
|
|
}
|
|
}
|
|
|
|
if (!sharingAudio) {
|
|
let labelID = "webRTC-selectMicrophone-single-device-label";
|
|
listDevices(micMenupopup, audioInputDevices, labelID);
|
|
if (audioInputDevices.length == 1) {
|
|
describedByIDs.push("webRTC-selectMicrophone-icon", labelID);
|
|
}
|
|
}
|
|
|
|
let labelID = "webRTC-selectSpeaker-single-device-label";
|
|
listDevices(speakerMenupopup, audioOutputDevices, labelID);
|
|
if (audioOutputDevices.length == 1) {
|
|
describedByIDs.push("webRTC-selectSpeaker-icon", labelID);
|
|
}
|
|
|
|
// PopupNotifications knows to clear the aria-describedby attribute
|
|
// when hiding, so we don't have to worry about cleaning it up ourselves.
|
|
chromeDoc.defaultView.PopupNotifications.panel.setAttribute(
|
|
"aria-describedby",
|
|
describedByIDs.join(" ")
|
|
);
|
|
|
|
this.mainAction.callback = async function(aState) {
|
|
let remember = false;
|
|
let silenceNotifications = false;
|
|
|
|
if (notificationSilencingEnabled && sharingScreen) {
|
|
silenceNotifications = aState && aState.checkboxChecked;
|
|
} else {
|
|
remember = aState && aState.checkboxChecked;
|
|
}
|
|
|
|
let allowedDevices = [];
|
|
let perms = Services.perms;
|
|
if (reqVideoInput) {
|
|
let listId = sharingScreen
|
|
? "webRTC-selectWindow-menulist"
|
|
: "webRTC-selectCamera-menulist";
|
|
let videoDeviceIndex = doc.getElementById(listId).value;
|
|
let allowVideoDevice = videoDeviceIndex != "-1";
|
|
if (allowVideoDevice) {
|
|
allowedDevices.push(videoDeviceIndex);
|
|
// Session permission will be removed after use
|
|
// (it's really one-shot, not for the entire session)
|
|
perms.addFromPrincipal(
|
|
principal,
|
|
"MediaManagerVideo",
|
|
perms.ALLOW_ACTION,
|
|
perms.EXPIRE_SESSION
|
|
);
|
|
let { mediaSource, rawId } = videoInputDevices.find(
|
|
({ deviceIndex }) => deviceIndex == videoDeviceIndex
|
|
);
|
|
aActor.activateDevicePerm(aRequest.windowID, mediaSource, rawId);
|
|
if (remember) {
|
|
lazy.SitePermissions.setForPrincipal(
|
|
principal,
|
|
"camera",
|
|
lazy.SitePermissions.ALLOW
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (reqAudioInput === "Microphone") {
|
|
let audioDeviceIndex = doc.getElementById(
|
|
"webRTC-selectMicrophone-menulist"
|
|
).value;
|
|
let allowMic = audioDeviceIndex != "-1";
|
|
if (allowMic) {
|
|
allowedDevices.push(audioDeviceIndex);
|
|
let { mediaSource, rawId } = audioInputDevices.find(
|
|
({ deviceIndex }) => deviceIndex == audioDeviceIndex
|
|
);
|
|
aActor.activateDevicePerm(aRequest.windowID, mediaSource, rawId);
|
|
if (remember) {
|
|
lazy.SitePermissions.setForPrincipal(
|
|
principal,
|
|
"microphone",
|
|
lazy.SitePermissions.ALLOW
|
|
);
|
|
}
|
|
}
|
|
} else if (reqAudioInput === "AudioCapture") {
|
|
// Only one device possible for audio capture.
|
|
allowedDevices.push(0);
|
|
}
|
|
|
|
if (reqAudioOutput) {
|
|
let audioDeviceIndex = doc.getElementById(
|
|
"webRTC-selectSpeaker-menulist"
|
|
).value;
|
|
let allowSpeaker = audioDeviceIndex != "-1";
|
|
if (allowSpeaker) {
|
|
allowedDevices.push(audioDeviceIndex);
|
|
let { id } = audioOutputDevices.find(
|
|
({ deviceIndex }) => deviceIndex == audioDeviceIndex
|
|
);
|
|
lazy.SitePermissions.setForPrincipal(
|
|
principal,
|
|
["speaker", id].join("^"),
|
|
lazy.SitePermissions.ALLOW
|
|
);
|
|
}
|
|
}
|
|
|
|
if (!allowedDevices.length) {
|
|
aActor.denyRequest(aRequest);
|
|
return;
|
|
}
|
|
|
|
const camNeeded = reqVideoInput === "Camera";
|
|
const micNeeded = !!reqAudioInput;
|
|
const scrNeeded = reqVideoInput === "Screen";
|
|
const havePermission = await aActor.checkOSPermission(
|
|
camNeeded,
|
|
micNeeded,
|
|
scrNeeded
|
|
);
|
|
if (!havePermission) {
|
|
aActor.denyRequestNoPermission(aRequest);
|
|
return;
|
|
}
|
|
|
|
aActor.sendAsyncMessage("webrtc:Allow", {
|
|
callID: aRequest.callID,
|
|
windowID: aRequest.windowID,
|
|
devices: allowedDevices,
|
|
suppressNotifications: silenceNotifications,
|
|
});
|
|
};
|
|
|
|
// If we haven't handled the permission yet, we want to show the doorhanger.
|
|
return false;
|
|
},
|
|
};
|
|
|
|
function shouldShowAlwaysRemember() {
|
|
// Don't offer "always remember" action in PB mode
|
|
if (lazy.PrivateBrowsingUtils.isBrowserPrivate(aBrowser)) {
|
|
return false;
|
|
}
|
|
|
|
// Don't offer "always remember" action in third party with no permission
|
|
// delegation
|
|
if (aRequest.isThirdPartyOrigin && !aRequest.shouldDelegatePermission) {
|
|
return false;
|
|
}
|
|
|
|
// Don't offer "always remember" action in maybe unsafe permission
|
|
// delegation
|
|
if (aRequest.shouldDelegatePermission && aRequest.secondOrigin) {
|
|
return false;
|
|
}
|
|
|
|
// Speaker grants are always remembered, so no checkbox is required.
|
|
if (reqAudioOutput) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
if (shouldShowAlwaysRemember()) {
|
|
// Disable the permanent 'Allow' action if the connection isn't secure, or for
|
|
// screen/audio sharing (because we can't guess which window the user wants to
|
|
// share without prompting). Note that we never enter this block for private
|
|
// browsing windows.
|
|
let reason = "";
|
|
if (sharingScreen) {
|
|
reason = "webrtc-reason-for-no-permanent-allow-screen";
|
|
} else if (sharingAudio) {
|
|
reason = "webrtc-reason-for-no-permanent-allow-audio";
|
|
} else if (!aRequest.secure) {
|
|
reason = "webrtc-reason-for-no-permanent-allow-insecure";
|
|
}
|
|
|
|
options.checkbox = {
|
|
label: localization.formatValueSync("webrtc-remember-allow-checkbox"),
|
|
checked: principal.isAddonOrExpandedAddonPrincipal,
|
|
checkedState: reason
|
|
? {
|
|
disableMainAction: true,
|
|
warningLabel: localization.formatValueSync(reason),
|
|
}
|
|
: undefined,
|
|
};
|
|
}
|
|
|
|
// If the notification silencing feature is enabled and we're sharing a
|
|
// screen, then the checkbox for the permission panel is what controls
|
|
// notification silencing.
|
|
if (notificationSilencingEnabled && sharingScreen) {
|
|
options.checkbox = {
|
|
label: localization.formatValueSync("webrtc-mute-notifications-checkbox"),
|
|
checked: false,
|
|
checkedState: {
|
|
disableMainAction: false,
|
|
},
|
|
};
|
|
}
|
|
|
|
let anchorId = "webRTC-shareDevices-notification-icon";
|
|
if (reqVideoInput === "Screen") {
|
|
anchorId = "webRTC-shareScreen-notification-icon";
|
|
} else if (!reqVideoInput) {
|
|
if (reqAudioInput && !reqAudioOutput) {
|
|
anchorId = "webRTC-shareMicrophone-notification-icon";
|
|
} else if (!reqAudioInput && reqAudioOutput) {
|
|
anchorId = "webRTC-shareSpeaker-notification-icon";
|
|
}
|
|
}
|
|
|
|
if (aRequest.secondOrigin) {
|
|
options.secondName = lazy.webrtcUI.getHostOrExtensionName(
|
|
null,
|
|
aRequest.secondOrigin
|
|
);
|
|
}
|
|
|
|
notification = chromeDoc.defaultView.PopupNotifications.show(
|
|
aBrowser,
|
|
"webRTC-shareDevices",
|
|
message,
|
|
anchorId,
|
|
mainAction,
|
|
secondaryActions,
|
|
options
|
|
);
|
|
notification.callID = aRequest.callID;
|
|
|
|
let schemeHistogram = Services.telemetry.getKeyedHistogramById(
|
|
"PERMISSION_REQUEST_ORIGIN_SCHEME"
|
|
);
|
|
let userInputHistogram = Services.telemetry.getKeyedHistogramById(
|
|
"PERMISSION_REQUEST_HANDLING_USER_INPUT"
|
|
);
|
|
|
|
let docURI = aRequest.documentURI;
|
|
let scheme = 0;
|
|
if (docURI.startsWith("https")) {
|
|
scheme = 2;
|
|
} else if (docURI.startsWith("http")) {
|
|
scheme = 1;
|
|
}
|
|
|
|
for (let requestType of requestTypes) {
|
|
if (requestType == "AudioCapture") {
|
|
requestType = "Microphone";
|
|
}
|
|
requestType = requestType.toLowerCase();
|
|
|
|
schemeHistogram.add(requestType, scheme);
|
|
userInputHistogram.add(requestType, aRequest.isHandlingUserInput);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {"Screen" | "Camera" | null} reqVideoInput
|
|
* @param {"AudioCapture" | "Microphone" | null} reqAudioInput
|
|
* @param {boolean} reqAudioOutput
|
|
* @param {boolean} delegation - Is the access delegated to a third party?
|
|
* @returns {string} Localization message identifier
|
|
*/
|
|
function getPromptMessageId(
|
|
reqVideoInput,
|
|
reqAudioInput,
|
|
reqAudioOutput,
|
|
delegation
|
|
) {
|
|
switch (reqVideoInput) {
|
|
case "Camera":
|
|
switch (reqAudioInput) {
|
|
case "Microphone":
|
|
return delegation
|
|
? "webrtc-allow-share-camera-and-microphone-unsafe-delegation"
|
|
: "webrtc-allow-share-camera-and-microphone";
|
|
case "AudioCapture":
|
|
return delegation
|
|
? "webrtc-allow-share-camera-and-audio-capture-unsafe-delegation"
|
|
: "webrtc-allow-share-camera-and-audio-capture";
|
|
default:
|
|
return delegation
|
|
? "webrtc-allow-share-camera-unsafe-delegation"
|
|
: "webrtc-allow-share-camera";
|
|
}
|
|
|
|
case "Screen":
|
|
switch (reqAudioInput) {
|
|
case "Microphone":
|
|
return delegation
|
|
? "webrtc-allow-share-screen-and-microphone-unsafe-delegation"
|
|
: "webrtc-allow-share-screen-and-microphone";
|
|
case "AudioCapture":
|
|
return delegation
|
|
? "webrtc-allow-share-screen-and-audio-capture-unsafe-delegation"
|
|
: "webrtc-allow-share-screen-and-audio-capture";
|
|
default:
|
|
return delegation
|
|
? "webrtc-allow-share-screen-unsafe-delegation"
|
|
: "webrtc-allow-share-screen";
|
|
}
|
|
|
|
default:
|
|
switch (reqAudioInput) {
|
|
case "Microphone":
|
|
return delegation
|
|
? "webrtc-allow-share-microphone-unsafe-delegation"
|
|
: "webrtc-allow-share-microphone";
|
|
case "AudioCapture":
|
|
return delegation
|
|
? "webrtc-allow-share-audio-capture-unsafe-delegation"
|
|
: "webrtc-allow-share-audio-capture";
|
|
default:
|
|
// This should be always true, if we've reached this far.
|
|
if (reqAudioOutput) {
|
|
return delegation
|
|
? "webrtc-allow-share-speaker-unsafe-delegation"
|
|
: "webrtc-allow-share-speaker";
|
|
}
|
|
return undefined;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks whether we have a microphone/camera in use by checking the activePerms map
|
|
* or if we have an allow permission for a microphone/camera in sitePermissions
|
|
* @param {Browser} browser - Browser to find all active and allowed microphone and camera devices for
|
|
* @return true if one of the above conditions is met
|
|
*/
|
|
function allowedOrActiveCameraOrMicrophone(browser) {
|
|
// Do we have an allow permission for cam/mic in the permissions manager?
|
|
if (
|
|
lazy.SitePermissions.getAllForBrowser(browser).some(perm => {
|
|
return (
|
|
perm.state == lazy.SitePermissions.ALLOW &&
|
|
(perm.id.startsWith("camera") || perm.id.startsWith("microphone"))
|
|
);
|
|
})
|
|
) {
|
|
// Return early, no need to check for active devices
|
|
return true;
|
|
}
|
|
|
|
// Do we have an active device?
|
|
return (
|
|
// Find all windowIDs that belong to our browsing contexts
|
|
browser.browsingContext
|
|
.getAllBrowsingContextsInSubtree()
|
|
// Only keep the outerWindowIds
|
|
.map(bc => bc.currentWindowGlobal?.outerWindowId)
|
|
.filter(id => id != null)
|
|
// We have an active device if one of our windowIds has a non empty map in the activePerms map
|
|
// that includes one device of type "camera" or "microphone"
|
|
.some(id => {
|
|
let map = lazy.webrtcUI.activePerms.get(id);
|
|
if (!map) {
|
|
// This windowId has no active device
|
|
return false;
|
|
}
|
|
// Let's see if one of the devices is a camera or a microphone
|
|
let types = [...map.values()];
|
|
return types.includes("microphone") || types.includes("camera");
|
|
})
|
|
);
|
|
}
|
|
|
|
function removePrompt(aBrowser, aCallId) {
|
|
let chromeWin = aBrowser.ownerGlobal;
|
|
let notification = chromeWin.PopupNotifications.getNotification(
|
|
"webRTC-shareDevices",
|
|
aBrowser
|
|
);
|
|
if (notification && notification.callID == aCallId) {
|
|
notification.remove();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clears temporary permission grants used for WebRTC device grace periods.
|
|
* @param browser - Browser element to clear permissions for.
|
|
* @param {boolean} clearCamera - Clear camera grants.
|
|
* @param {boolean} clearMicrophone - Clear microphone grants.
|
|
*/
|
|
function clearTemporaryGrants(browser, clearCamera, clearMicrophone) {
|
|
if (!clearCamera && !clearMicrophone) {
|
|
// Nothing to clear.
|
|
return;
|
|
}
|
|
let perms = lazy.SitePermissions.getAllForBrowser(browser);
|
|
perms
|
|
.filter(perm => {
|
|
let [id, key] = perm.id.split(lazy.SitePermissions.PERM_KEY_DELIMITER);
|
|
// We only want to clear WebRTC grace periods. These are temporary, device
|
|
// specifc (double-keyed) microphone or camera permissions.
|
|
return (
|
|
key &&
|
|
perm.state == lazy.SitePermissions.ALLOW &&
|
|
perm.scope == lazy.SitePermissions.SCOPE_TEMPORARY &&
|
|
((clearCamera && id == "camera") ||
|
|
(clearMicrophone && id == "microphone"))
|
|
);
|
|
})
|
|
.forEach(perm =>
|
|
lazy.SitePermissions.removeFromPrincipal(null, perm.id, browser)
|
|
);
|
|
}
|