diff --git a/browser/actors/WebRTCChild.jsm b/browser/actors/WebRTCChild.jsm index dafffb28a303..db4977f955f6 100644 --- a/browser/actors/WebRTCChild.jsm +++ b/browser/actors/WebRTCChild.jsm @@ -10,9 +10,6 @@ const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); const { XPCOMUtils } = ChromeUtils.import( "resource://gre/modules/XPCOMUtils.jsm" ); -const { ActorChild } = ChromeUtils.import( - "resource://gre/modules/ActorChild.jsm" -); const { AppConstants } = ChromeUtils.import( "resource://gre/modules/AppConstants.jsm" ); @@ -25,16 +22,18 @@ XPCOMUtils.defineLazyServiceGetter( const kBrowserURL = AppConstants.BROWSER_CHROME_URL; -class WebRTCChild extends ActorChild { +class WebRTCChild extends JSWindowActorChild { // Called only for 'unload' to remove pending gUM prompts in reloaded frames. static handleEvent(aEvent) { let contentWindow = aEvent.target.defaultView; - let mm = getMessageManagerForWindow(contentWindow); - for (let key of contentWindow.pendingGetUserMediaRequests.keys()) { - mm.sendAsyncMessage("webrtc:CancelRequest", key); - } - for (let key of contentWindow.pendingPeerConnectionRequests.keys()) { - mm.sendAsyncMessage("rtcpeer:CancelRequest", key); + let actor = getActorForWindow(contentWindow); + if (actor) { + for (let key of contentWindow.pendingGetUserMediaRequests.keys()) { + actor.sendAsyncMessage("webrtc:CancelRequest", key); + } + for (let key of contentWindow.pendingPeerConnectionRequests.keys()) { + actor.sendAsyncMessage("rtcpeer:CancelRequest", key); + } } } @@ -112,6 +111,19 @@ class WebRTCChild extends ActorChild { } } +function getActorForWindow(window) { + let windowGlobal = window.getWindowGlobalChild(); + try { + if (windowGlobal) { + return windowGlobal.getActor("WebRTC"); + } + } catch (ex) { + // There might not be an actor for a parent process chrome URL. + } + + return null; +} + function handlePCRequest(aSubject, aTopic, aData) { let { windowID, innerWindowID, callID, isSecure } = aSubject; let contentWindow = Services.wm.getOuterWindowWithId(windowID); @@ -140,7 +152,11 @@ function handlePCRequest(aSubject, aTopic, aData) { documentURI: contentWindow.document.documentURI, secure: isSecure, }; - mm.sendAsyncMessage("rtcpeer:Request", request); + + let actor = getActorForWindow(contentWindow); + if (actor) { + actor.sendAsyncMessage("rtcpeer:Request", request); + } } function handleGUMStop(aSubject, aTopic, aData) { @@ -152,9 +168,9 @@ function handleGUMStop(aSubject, aTopic, aData) { mediaSource: aSubject.mediaSource, }; - let mm = getMessageManagerForWindow(contentWindow); - if (mm) { - mm.sendAsyncMessage("webrtc:StopRecording", request); + let actor = getActorForWindow(contentWindow); + if (actor) { + actor.sendAsyncMessage("webrtc:StopRecording", request); } } @@ -270,11 +286,6 @@ function prompt( } aContentWindow.pendingGetUserMediaRequests.set(aCallID, devices); - // Record third party origins for telemetry. - let isThirdPartyOrigin = - aContentWindow.document.location.origin != - aContentWindow.top.document.location.origin; - // WebRTC prompts have a bunch of special requirements, such as being able to // grant two permissions (microphone and camera), selecting devices and showing // a screen sharing preview. All this could have probably been baked into @@ -295,10 +306,6 @@ function prompt( const shouldDelegatePermission = permDelegateHandler.permissionDelegateFPEnabled; - const origin = shouldDelegatePermission - ? aContentWindow.top.document.nodePrincipal.origin - : aContentWindow.document.nodePrincipal.origin; - let secondOrigin = undefined; if ( shouldDelegatePermission && @@ -312,12 +319,10 @@ function prompt( let request = { callID: aCallID, windowID: aWindowID, - origin, secondOrigin, documentURI: aContentWindow.document.documentURI, secure: aSecure, isHandlingUserInput: aIsHandlingUserInput, - isThirdPartyOrigin, shouldDelegatePermission, requestTypes, sharingScreen, @@ -326,8 +331,10 @@ function prompt( videoDevices, }; - let mm = getMessageManagerForWindow(aContentWindow); - mm.sendAsyncMessage("webrtc:Request", request); + let actor = getActorForWindow(aContentWindow); + if (actor) { + actor.sendAsyncMessage("webrtc:Request", request); + } } function denyGUMRequest(aData) { @@ -388,91 +395,35 @@ function updateIndicators(aSubject, aTopic, aData) { return; } - let contentWindowArray = MediaManagerService.activeMediaCaptureWindows; - let count = contentWindowArray.length; + let contentWindow = aSubject.getProperty("window"); - let state = { - showGlobalIndicator: count > 0, - showCameraIndicator: false, - showMicrophoneIndicator: false, - showScreenSharingIndicator: "", - }; + let actor = contentWindow ? getActorForWindow(contentWindow) : null; + if (actor) { + let tabState = getTabStateForContentWindow(contentWindow, false); + tabState.windowId = getInnerWindowIDForWindow(contentWindow); - Services.cpmm.sendAsyncMessage("webrtc:UpdatingIndicators"); - - // If several iframes in the same page use media streams, it's possible to - // have the same top level window several times. We use a Set to avoid - // sending duplicate notifications. - let contentWindows = new Set(); - for (let i = 0; i < count; ++i) { - contentWindows.add( - contentWindowArray.queryElementAt(i, Ci.nsISupports).top - ); + actor.sendAsyncMessage("webrtc:UpdateIndicators", tabState); } - - for (let contentWindow of contentWindows) { - if (contentWindow.document.documentURI == kBrowserURL) { - // There may be a preview shown at the same time as other streams. - continue; - } - - let tabState = getTabStateForContentWindow(contentWindow); - if ( - tabState.camera == MediaManagerService.STATE_CAPTURE_ENABLED || - tabState.camera == MediaManagerService.STATE_CAPTURE_DISABLED - ) { - state.showCameraIndicator = true; - } - if ( - tabState.microphone == MediaManagerService.STATE_CAPTURE_ENABLED || - tabState.microphone == MediaManagerService.STATE_CAPTURE_DISABLED - ) { - state.showMicrophoneIndicator = true; - } - if (tabState.screen) { - if (tabState.screen.startsWith("Screen")) { - state.showScreenSharingIndicator = "Screen"; - } else if (tabState.screen.startsWith("Window")) { - if (state.showScreenSharingIndicator != "Screen") { - state.showScreenSharingIndicator = "Window"; - } - } else if (tabState.screen.startsWith("Browser")) { - if (!state.showScreenSharingIndicator) { - state.showScreenSharingIndicator = "Browser"; - } - } - } - - let mm = getMessageManagerForWindow(contentWindow); - mm.sendAsyncMessage("webrtc:UpdateBrowserIndicators", tabState); - } - - Services.cpmm.sendAsyncMessage("webrtc:UpdateGlobalIndicators", state); } function removeBrowserSpecificIndicator(aSubject, aTopic, aData) { - let contentWindow = Services.wm.getOuterWindowWithId(aData).top; + let contentWindow = Services.wm.getOuterWindowWithId(aData); if (contentWindow.document.documentURI == kBrowserURL) { // Ignore notifications caused by the browser UI showing previews. return; } - let tabState = getTabStateForContentWindow(contentWindow); - if ( - tabState.camera == MediaManagerService.STATE_NOCAPTURE && - tabState.microphone == MediaManagerService.STATE_NOCAPTURE && - !tabState.screen - ) { - tabState = { windowId: tabState.windowId }; - } + let tabState = getTabStateForContentWindow(contentWindow, true); - let mm = getMessageManagerForWindow(contentWindow); - if (mm) { - mm.sendAsyncMessage("webrtc:UpdateBrowserIndicators", tabState); + tabState.windowId = aData; + + let actor = getActorForWindow(contentWindow); + if (actor) { + actor.sendAsyncMessage("webrtc:UpdateIndicators", tabState); } } -function getTabStateForContentWindow(aContentWindow) { +function getTabStateForContentWindow(aContentWindow, aForRemove = false) { let camera = {}, microphone = {}, screen = {}, @@ -485,55 +436,31 @@ function getTabStateForContentWindow(aContentWindow) { screen, window, browser, - true + false ); - let tabState = { camera: camera.value, microphone: microphone.value }; - if (screen.value == MediaManagerService.STATE_CAPTURE_ENABLED) { - tabState.screen = "Screen"; - } else if (window.value == MediaManagerService.STATE_CAPTURE_ENABLED) { - tabState.screen = "Window"; - } else if (browser.value == MediaManagerService.STATE_CAPTURE_ENABLED) { - tabState.screen = "Browser"; - } else if (screen.value == MediaManagerService.STATE_CAPTURE_DISABLED) { - tabState.screen = "ScreenPaused"; - } else if (window.value == MediaManagerService.STATE_CAPTURE_DISABLED) { - tabState.screen = "WindowPaused"; - } else if (browser.value == MediaManagerService.STATE_CAPTURE_DISABLED) { - tabState.screen = "BrowserPaused"; + + if ( + camera.value == MediaManagerService.STATE_NOCAPTURE && + microphone.value == MediaManagerService.STATE_NOCAPTURE && + screen.value == MediaManagerService.STATE_NOCAPTURE && + window.value == MediaManagerService.STATE_NOCAPTURE && + browser.value == MediaManagerService.STATE_NOCAPTURE + ) { + return {}; } - let screenEnabled = tabState.screen && !tabState.screen.includes("Paused"); - let cameraEnabled = - tabState.camera == MediaManagerService.STATE_CAPTURE_ENABLED; - let microphoneEnabled = - tabState.microphone == MediaManagerService.STATE_CAPTURE_ENABLED; - - // tabState.sharing controls which global indicator should be shown - // for the tab. It should always be set to the _enabled_ device which - // we consider most intrusive (screen > camera > microphone). - if (screenEnabled) { - tabState.sharing = "screen"; - } else if (cameraEnabled) { - tabState.sharing = "camera"; - } else if (microphoneEnabled) { - tabState.sharing = "microphone"; - } else if (tabState.screen) { - tabState.sharing = "screen"; - } else if (tabState.camera) { - tabState.sharing = "camera"; - } else if (tabState.microphone) { - tabState.sharing = "microphone"; + if (aForRemove) { + return {}; } - // The stream is considered paused when we're sharing something - // but all devices are off or set to disabled. - tabState.paused = - tabState.sharing && !screenEnabled && !cameraEnabled && !microphoneEnabled; - - tabState.windowId = getInnerWindowIDForWindow(aContentWindow); - tabState.documentURI = aContentWindow.document.documentURI; - - return tabState; + return { + camera: camera.value, + microphone: microphone.value, + screen: screen.value, + window: window.value, + browser: browser.value, + documentURI: aContentWindow.document.documentURI, + }; } function getInnerWindowIDForWindow(aContentWindow) { diff --git a/browser/actors/WebRTCParent.jsm b/browser/actors/WebRTCParent.jsm new file mode 100644 index 000000000000..1b3741ac2461 --- /dev/null +++ b/browser/actors/WebRTCParent.jsm @@ -0,0 +1,1134 @@ +/* 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 { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +ChromeUtils.defineModuleGetter( + this, + "PluralForm", + "resource://gre/modules/PluralForm.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "SitePermissions", + "resource:///modules/SitePermissions.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "webrtcUI", + "resource:///modules/webrtcUI.jsm" +); + +XPCOMUtils.defineLazyGetter(this, "gBrandBundle", function() { + return Services.strings.createBundle( + "chrome://branding/locale/brand.properties" + ); +}); + +XPCOMUtils.defineLazyServiceGetter( + this, + "OSPermissions", + "@mozilla.org/ospermissionrequest;1", + "nsIOSPermissionRequest" +); + +class WebRTCParent extends JSWindowActorParent { + didDestroy() { + webrtcUI.forgetStreamsFromBrowserContext(this.browsingContext); + webrtcUI.activePerms.delete(this.manager.outerWindowId); + } + + getBrowser() { + let browser = this.browsingContext.top.embedderElement; + if (browser && browser.outerBrowser) { + // Responsive design mode check + browser = browser.outerBrowser; + } + return browser; + } + + 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(webrtcUI.peerConnectionBlockers); + + (async function() { + for (let blocker of blockers) { + try { + let result = await blocker(params); + if (result == "deny") { + return false; + } + } catch (err) { + Cu.reportError(`error in PeerConnection blocker: ${err.message}`); + } + } + return true; + })().then(decision => { + let message; + if (decision) { + webrtcUI.emitter.emit("peer-request-allowed", params); + message = "rtcpeer:Allow"; + } else { + 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, + }); + 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.browsingContext.top.currentWindowGlobal.documentPrincipal.origin; + data.isThirdPartyOrigin = isThirdPartyOrigin; + + data.origin = data.shouldDelegatePermission + ? this.browsingContext.top.currentWindowGlobal.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); + 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": + if (aMessage.data.windowId) { + webrtcUI.streamAddedOrRemoved(this.browsingContext, aMessage.data); + } + + this.updateIndicators(aMessage.data); + break; + } + } + + updateIndicators(aData) { + let browsingContext = this.browsingContext; + let state = 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) { + OSPermissions.getMediaCapturePermissionState(camStatus, micStatus); + } + if (camNeeded) { + let camPermission = camStatus.value; + let camAccessible = await this.checkAndGetOSPermission( + camPermission, + OSPermissions.requestVideoCapturePermission + ); + if (!camAccessible) { + return false; + } + } + if (micNeeded) { + let micPermission = micStatus.value; + let micAccessible = await this.checkAndGetOSPermission( + micPermission, + OSPermissions.requestAudioCapturePermission + ); + if (!micAccessible) { + return false; + } + } + let scrStatus = {}; + if (scrNeeded) { + OSPermissions.getScreenCapturePermissionState(scrStatus); + if (scrStatus.value == OSPermissions.PERMISSION_STATE_DENIED) { + 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 == OSPermissions.PERMISSION_STATE_DENIED || + devicePermission == OSPermissions.PERMISSION_STATE_RESTRICTED + ) { + return false; + } + if (devicePermission == OSPermissions.PERMISSION_STATE_NOTDETERMINED) { + let deviceAllowed = await requestPermissionFunc(); + if (!deviceAllowed) { + return false; + } + } + return true; + } + + stopRecording(aRequest) { + let outerWindowID = this.manager.outerWindowId; + + if (!webrtcUI.activePerms.has(outerWindowID)) { + return; + } + + if (!aRequest.rawID) { + webrtcUI.activePerms.delete(outerWindowID); + } else { + let set = webrtcUI.activePerms.get(outerWindowID); + set.delete(aRequest.windowID + aRequest.mediaSource + aRequest.rawID); + } + } + + /** + * 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; + } + + let { audioDevices, videoDevices, sharingScreen } = aRequest; + + let micAllowed = + SitePermissions.getForPrincipal(aPrincipal, "microphone").state == + SitePermissions.ALLOW; + let camAllowed = + SitePermissions.getForPrincipal(aPrincipal, "camera").state == + SitePermissions.ALLOW; + + let perms = Services.perms; + let mediaManagerPerm = perms.testExactPermissionFromPrincipal( + aPrincipal, + "MediaManagerVideo" + ); + if (mediaManagerPerm) { + perms.removeFromPrincipal(aPrincipal, "MediaManagerVideo"); + } + + // Screen sharing shouldn't follow the camera permissions. + if (videoDevices.length && sharingScreen) { + camAllowed = false; + } + if (aRequest.isThirdPartyOrigin && !aRequest.shouldDelegatePermission) { + camAllowed = false; + micAllowed = false; + } + + let activeCamera; + let activeMic; + + // Always prompt for screen sharing + if (!sharingScreen) { + for (let device of videoDevices) { + let set = webrtcUI.activePerms.get(this.manager.outerWindowId); + if ( + set && + set.has(aRequest.windowID + device.mediaSource + device.id) + ) { + activeCamera = device; + break; + } + } + + for (let device of audioDevices) { + let set = webrtcUI.activePerms.get(this.manager.outerWindowId); + if ( + set && + set.has(aRequest.windowID + device.mediaSource + device.id) + ) { + activeMic = device; + break; + } + } + } + if ( + (!audioDevices.length || micAllowed || activeMic) && + (!videoDevices.length || camAllowed || activeCamera) + ) { + let allowedDevices = []; + if (videoDevices.length) { + allowedDevices.push((activeCamera || videoDevices[0]).deviceIndex); + Services.perms.addFromPrincipal( + aPrincipal, + "MediaManagerVideo", + Services.perms.ALLOW_ACTION, + Services.perms.EXPIRE_SESSION + ); + } + if (audioDevices.length) { + allowedDevices.push((activeMic || audioDevices[0]).deviceIndex); + } + + // Remember on which URIs we found persistent permissions so that we + // can remove them if the user clicks 'Stop Sharing'. There's no + // other way for the stop sharing code to know the hostnames of frames + // using devices until bug 1066082 is fixed. + let browser = this.getBrowser(); + browser.getDevicePermissionOrigins("webrtc").add(aPrincipal.origin); + + // If sharingScreen, we're requesting screen-sharing, otherwise camera + let camNeeded = !!videoDevices.length && !sharingScreen; + let scrNeeded = !!videoDevices.length && sharingScreen; + let micNeeded = !!audioDevices.length; + this.checkOSPermission(camNeeded, micNeeded, scrNeeded).then( + havePermission => { + if (havePermission) { + this.sendAsyncMessage("webrtc:Allow", { + callID: aRequest.callID, + windowID: aRequest.windowID, + devices: allowedDevices, + }); + } else { + this.denyRequestNoPermission(aRequest); + } + } + ); + + return true; + } + + return false; + } +} + +function prompt(aActor, aBrowser, aRequest) { + let { + audioDevices, + videoDevices, + 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 ( + aRequest.secondOrigin || + !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. + if ( + (audioDevices.length && + SitePermissions.getForPrincipal(principal, "microphone", aBrowser) + .state == SitePermissions.BLOCK) || + (videoDevices.length && + SitePermissions.getForPrincipal( + principal, + sharingScreen ? "screen" : "camera", + aBrowser + ).state == SitePermissions.BLOCK) + ) { + aActor.denyRequest(aRequest); + return; + } + + // Tell the browser to refresh the identity block display in case there + // are expired permission states. + aBrowser.dispatchEvent( + new aBrowser.ownerGlobal.CustomEvent("PermissionStateChange") + ); + + let chromeDoc = aBrowser.ownerDocument; + let stringBundle = chromeDoc.defaultView.gNavigatorBundle; + + // Mind the order, because for simplicity we're iterating over the list using + // "includes()". This allows the rotation of string identifiers. We list the + // full identifiers here so they can be cross-referenced more easily. + let joinedRequestTypes = requestTypes.join("And"); + let requestMessages; + if (aRequest.secondOrigin) { + requestMessages = [ + // Individual request types first. + "getUserMedia.shareCameraUnsafeDelegation.message", + "getUserMedia.shareMicrophoneUnsafeDelegation.message", + "getUserMedia.shareScreenUnsafeDelegation.message", + "getUserMedia.shareAudioCaptureUnsafeDelegation.message", + // Combinations of the above request types last. + "getUserMedia.shareCameraAndMicrophoneUnsafeDelegation.message", + "getUserMedia.shareCameraAndAudioCaptureUnsafeDelegation.message", + "getUserMedia.shareScreenAndMicrophoneUnsafeDelegation.message", + "getUserMedia.shareScreenAndAudioCaptureUnsafeDelegation.message", + ]; + } else { + requestMessages = [ + // Individual request types first. + "getUserMedia.shareCamera2.message", + "getUserMedia.shareMicrophone2.message", + "getUserMedia.shareScreen3.message", + "getUserMedia.shareAudioCapture2.message", + // Combinations of the above request types last. + "getUserMedia.shareCameraAndMicrophone2.message", + "getUserMedia.shareCameraAndAudioCapture2.me ssage", + "getUserMedia.shareScreenAndMicrophone3.message", + "getUserMedia.shareScreenAndAudioCapture3.message", + ]; + } + + let stringId = requestMessages.find(id => id.includes(joinedRequestTypes)); + let message = aRequest.secondOrigin + ? stringBundle.getFormattedString(stringId, ["<>", "{}"]) + : stringBundle.getFormattedString(stringId, ["<>"]); + + let notification; // Used by action callbacks. + let mainAction = { + label: stringBundle.getString("getUserMedia.allow.label"), + accessKey: stringBundle.getString("getUserMedia.allow.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() {}, + }; + + let secondaryActions = [ + { + label: stringBundle.getString("getUserMedia.dontAllow.label"), + accessKey: stringBundle.getString("getUserMedia.dontAllow.accesskey"), + callback(aState) { + aActor.denyRequest(aRequest); + let scope = SitePermissions.SCOPE_TEMPORARY; + if (aState && aState.checkboxChecked) { + scope = SitePermissions.SCOPE_PERSISTENT; + } + if (audioDevices.length) { + SitePermissions.setForPrincipal( + principal, + "microphone", + SitePermissions.BLOCK, + scope, + notification.browser + ); + } + if (videoDevices.length) { + SitePermissions.setForPrincipal( + principal, + sharingScreen ? "screen" : "camera", + SitePermissions.BLOCK, + scope, + notification.browser + ); + } + }, + }, + ]; + + let productName = gBrandBundle.GetStringFromName("brandShortName"); + + let options = { + name: 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 ( + ((aTopic == "dismissed" || aTopic == "removed") && + requestTypes.includes("Screen")) || + !requestTypes.includes("Screen") + ) { + let video = doc.getElementById("webRTC-previewVideo"); + 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; + } + + // BLOCK is handled immediately by MediaManager if it has been set + // persistently in the permission manager. If it has been set on the tab, + // 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 we have a secondOrigin, it means this request is lacking explicit + // trust, and we should always prompt even in with persistent permission. + if ( + !aRequest.secondOrigin && + aActor.checkRequestAllowed(aRequest, principal, aBrowser) + ) { + this.remove(); + return true; + } + + function listDevices(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; + + for (let device of devices) { + addDeviceToList(menupopup, device.name, device.deviceIndex); + } + } + + function checkDisabledWindowMenuItem() { + let list = doc.getElementById("webRTC-selectWindow-menulist"); + let item = list.selectedItem; + let notificationElement = doc.getElementById( + "webRTC-shareDevices-notification" + ); + 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; + + let label = doc.getElementById("webRTC-selectWindow-label"); + const gumStringId = "getUserMedia.selectWindowOrScreen"; + label.setAttribute( + "value", + stringBundle.getString(gumStringId + ".label") + ); + label.setAttribute( + "accesskey", + stringBundle.getString(gumStringId + ".accesskey") + ); + + // "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, + stringBundle.getString("getUserMedia.pickWindowOrScreen.label"), + "-1" + ); + menupopup.appendChild(doc.createXULElement("menuseparator")); + + // 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; + // Building screen list from available screens. + if (type == "screen") { + if (device.name == "Primary Monitor") { + name = stringBundle.getString( + "getUserMedia.shareEntireScreen.label" + ); + } else { + name = stringBundle.getFormattedString( + "getUserMedia.shareMonitor.label", + [monitorIndex] + ); + ++monitorIndex; + } + } else { + name = device.name; + if (type == "application") { + // The application names returned by the platform are of the form: + // \x1e + let sepIndex = name.indexOf("\x1e"); + let count = name.slice(0, sepIndex); + let sawcStringId = + "getUserMedia.shareApplicationWindowCount.label"; + name = PluralForm.get( + parseInt(count), + stringBundle.getString(sawcStringId) + ) + .replace("#1", name.slice(sepIndex + 1)) + .replace("#2", count); + } + } + let item = addDeviceToList(menupopup, name, i, type); + item.deviceId = device.id; + item.mediaSource = type; + if (device.scary) { + item.scary = true; + } + } + + // Always re-select the "No " 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; + } + + let type = event.target.mediaSource; + let deviceId = event.target.deviceId; + if (deviceId == undefined) { + doc.getElementById("webRTC-preview").hidden = true; + video.src = null; + return; + } + + let scary = event.target.scary; + let warning = doc.getElementById("webRTC-previewWarning"); + warning.hidden = !scary; + let chromeWin = doc.defaultView; + if (scary) { + warning.hidden = false; + let string; + let bundle = chromeWin.gNavigatorBundle; + + let learnMoreText = bundle.getString( + "getUserMedia.shareScreen.learnMoreLabel" + ); + let baseURL = Services.urlFormatter.formatURLPref( + "app.support.baseURL" + ); + + let learnMore = chromeWin.document.createXULElement("label", { + is: "text-link", + }); + learnMore.setAttribute("href", baseURL + "screenshare-safety"); + learnMore.textContent = learnMoreText; + + if (type == "screen") { + string = bundle.getFormattedString( + "getUserMedia.shareScreenWarning.message", + ["<>"] + ); + } else { + let brand = doc + .getElementById("bundle_brand") + .getString("brandShortName"); + string = bundle.getFormattedString( + "getUserMedia.shareFirefoxWarning.message", + [brand, "<>"] + ); + } + + let [pre, post] = string.split("<>"); + warning.textContent = pre; + warning.appendChild(learnMore); + warning.appendChild(chromeWin.document.createTextNode(post)); + + // 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 = {}; + OSPermissions.getScreenCapturePermissionState(scrStatus); + if (scrStatus.value == OSPermissions.PERMISSION_STATE_DENIED) { + OSPermissions.maybeRequestScreenCapturePermission(); + } + } + + let perms = Services.perms; + let chromePrincipal = Services.scriptSecurityManager.getSystemPrincipal(); + perms.addFromPrincipal( + chromePrincipal, + "MediaManagerVideo", + perms.ALLOW_ACTION, + perms.EXPIRE_SESSION + ); + + video.deviceId = deviceId; + let constraints = { + video: { mediaSource: type, 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; + } + Cu.reportError( + `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 = + !videoDevices.length || sharingScreen; + doc.getElementById("webRTC-selectWindowOrScreen").hidden = + !sharingScreen || !videoDevices.length; + doc.getElementById("webRTC-selectMicrophone").hidden = + !audioDevices.length || sharingAudio; + + let camMenupopup = doc.getElementById("webRTC-selectCamera-menupopup"); + let windowMenupopup = doc.getElementById("webRTC-selectWindow-menupopup"); + let micMenupopup = doc.getElementById( + "webRTC-selectMicrophone-menupopup" + ); + if (sharingScreen) { + listScreenShareDevices(windowMenupopup, videoDevices); + checkDisabledWindowMenuItem(); + } else { + listDevices(camMenupopup, videoDevices); + doc + .getElementById("webRTC-shareDevices-notification") + .removeAttribute("invalidselection"); + } + + if (!sharingAudio) { + listDevices(micMenupopup, audioDevices); + } + + this.mainAction.callback = async function(aState) { + let remember = aState && aState.checkboxChecked; + let allowedDevices = []; + let perms = Services.perms; + if (videoDevices.length) { + let listId = + "webRTC-select" + + (sharingScreen ? "Window" : "Camera") + + "-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 + ); + if (!webrtcUI.activePerms.has(aActor.manager.outerWindowId)) { + webrtcUI.activePerms.set(aActor.manager.outerWindowId, new Set()); + } + + for (let device of videoDevices) { + if (device.deviceIndex == videoDeviceIndex) { + webrtcUI.activePerms + .get(aActor.manager.outerWindowId) + .add(aRequest.windowID + device.mediaSource + device.id); + break; + } + } + if (remember) { + SitePermissions.setForPrincipal( + principal, + "camera", + SitePermissions.ALLOW + ); + } + } + } + if (audioDevices.length) { + if (!sharingAudio) { + let audioDeviceIndex = doc.getElementById( + "webRTC-selectMicrophone-menulist" + ).value; + let allowMic = audioDeviceIndex != "-1"; + if (allowMic) { + allowedDevices.push(audioDeviceIndex); + if (!webrtcUI.activePerms.has(aActor.manager.outerWindowId)) { + webrtcUI.activePerms.set( + aActor.manager.outerWindowId, + new Set() + ); + } + + for (let device of audioDevices) { + if (device.deviceIndex == audioDeviceIndex) { + webrtcUI.activePerms + .get(aActor.manager.outerWindowId) + .add(aRequest.windowID + device.mediaSource + device.id); + break; + } + } + if (remember) { + SitePermissions.setForPrincipal( + principal, + "microphone", + SitePermissions.ALLOW + ); + } + } + } else { + // Only one device possible for audio capture. + allowedDevices.push(0); + } + } + + if (!allowedDevices.length) { + aActor.denyRequest(aRequest); + return; + } + + if (remember) { + // Remember on which URIs we set persistent permissions so that we + // can remove them if the user clicks 'Stop Sharing'. + aBrowser.getDevicePermissionOrigins("webrtc").add(principal.origin); + } + + let camNeeded = !!videoDevices.length && !sharingScreen; + let scrNeeded = !!videoDevices.length && sharingScreen; + let micNeeded = !!audioDevices.length; + let 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, + }); + }; + + // 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 (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; + } + + 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). + let reasonForNoPermanentAllow = ""; + if (sharingScreen) { + reasonForNoPermanentAllow = + "getUserMedia.reasonForNoPermanentAllow.screen3"; + } else if (sharingAudio) { + reasonForNoPermanentAllow = + "getUserMedia.reasonForNoPermanentAllow.audio"; + } else if (!aRequest.secure) { + reasonForNoPermanentAllow = + "getUserMedia.reasonForNoPermanentAllow.insecure"; + } + + options.checkbox = { + label: stringBundle.getString("getUserMedia.remember"), + checked: principal.isAddonOrExpandedAddonPrincipal, + checkedState: reasonForNoPermanentAllow + ? { + disableMainAction: true, + warningLabel: stringBundle.getFormattedString( + reasonForNoPermanentAllow, + [productName] + ), + } + : undefined, + }; + } + + let iconType = "Devices"; + if ( + requestTypes.length == 1 && + (requestTypes[0] == "Microphone" || requestTypes[0] == "AudioCapture") + ) { + iconType = "Microphone"; + } + if (requestTypes.includes("Screen")) { + iconType = "Screen"; + } + let anchorId = "webRTC-share" + iconType + "-notification-icon"; + + let iconClass = iconType.toLowerCase(); + if (iconClass == "devices") { + iconClass = "camera"; + } + options.popupIconClass = iconClass + "-icon"; + + if (aRequest.secondOrigin) { + options.secondName = 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); + } +} + +function removePrompt(aBrowser, aCallId) { + let chromeWin = aBrowser.ownerGlobal; + let notification = chromeWin.PopupNotifications.getNotification( + "webRTC-shareDevices", + aBrowser + ); + if (notification && notification.callID == aCallId) { + notification.remove(); + } +} diff --git a/browser/actors/moz.build b/browser/actors/moz.build index e64615356193..1c664d888c54 100644 --- a/browser/actors/moz.build +++ b/browser/actors/moz.build @@ -57,4 +57,5 @@ FINAL_TARGET_FILES.actors += [ 'SwitchDocumentDirectionChild.jsm', 'URIFixupChild.jsm', 'WebRTCChild.jsm', + 'WebRTCParent.jsm', ] diff --git a/browser/base/content/browser-siteIdentity.js b/browser/base/content/browser-siteIdentity.js index 843fb5081fed..226e9226da8d 100644 --- a/browser/base/content/browser-siteIdentity.js +++ b/browser/base/content/browser-siteIdentity.js @@ -1670,10 +1670,11 @@ var gIdentityHandler = { } } } - browser.messageManager.sendAsyncMessage( - "webrtc:StopSharing", - windowId - ); + + let bc = this._sharingState.webRTC.browsingContext; + bc.currentWindowGlobal + .getActor("WebRTC") + .sendAsyncMessage("webrtc:StopSharing", windowId); webrtcUI.forgetActivePermissionsFromBrowser(gBrowser.selectedBrowser); } } diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index a2b33e18d93d..46f09a106aa0 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -6121,7 +6121,6 @@ var TabsProgressListener = { if (tab && tab._sharingState) { gBrowser.resetBrowserSharing(aBrowser); } - webrtcUI.forgetStreamsFromBrowser(aBrowser); gBrowser.getNotificationBox(aBrowser).removeTransientNotifications(); diff --git a/browser/base/content/tabbrowser.js b/browser/base/content/tabbrowser.js index c3501fcb0e6b..bc3f861f90a1 100644 --- a/browser/base/content/tabbrowser.js +++ b/browser/base/content/tabbrowser.js @@ -2457,7 +2457,7 @@ if (aTab._sharingState) { this.resetBrowserSharing(browser); } - webrtcUI.forgetStreamsFromBrowser(browser); + webrtcUI.forgetStreamsFromBrowserContext(browser.browsingContext); // Set browser parameters for when browser is restored. Also remove // listeners and set up lazy restore data in SessionStore. This must diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_screen.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_screen.js index 70cb789fc830..ace27fa2196d 100644 --- a/browser/base/content/test/webrtc/browser_devices_get_user_media_screen.js +++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_screen.js @@ -760,6 +760,7 @@ var gTests = [ // Request devices and expect a prompt despite the saved 'Allow' permission. observerPromise = expectObserverCalled("getUserMedia:request"); + promise = promisePopupNotificationShown("webRTC-shareDevices"); await promiseRequestDevice(false, true, null, "screen"); await promise; await observerPromise; diff --git a/browser/base/content/test/webrtc/head.js b/browser/base/content/test/webrtc/head.js index 74f768fd602d..f8794f9e18fc 100644 --- a/browser/base/content/test/webrtc/head.js +++ b/browser/base/content/test/webrtc/head.js @@ -1,4 +1,4 @@ -browser/base/content/test/webrtc/head.jsconst { AppConstants } = ChromeUtils.import( +const { AppConstants } = ChromeUtils.import( "resource://gre/modules/AppConstants.jsm" ); @@ -367,45 +367,97 @@ function activateSecondaryAction(aAction) { } } -function getMediaCaptureState() { - return SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function() { - let mediaManagerService = Cc[ - "@mozilla.org/mediaManagerService;1" - ].getService(Ci.nsIMediaManagerService); +async function getMediaCaptureState() { + function gatherBrowsingContexts(aBrowsingContext) { + let list = [aBrowsingContext]; - let hasCamera = {}; - let hasMicrophone = {}; - let hasScreenShare = {}; - let hasWindowShare = {}; - let hasBrowserShare = {}; - mediaManagerService.mediaCaptureWindowState( - content, - hasCamera, - hasMicrophone, - hasScreenShare, - hasWindowShare, - hasBrowserShare, - true - ); - let result = {}; - - if (hasCamera.value != mediaManagerService.STATE_NOCAPTURE) { - result.video = true; - } - if (hasMicrophone.value != mediaManagerService.STATE_NOCAPTURE) { - result.audio = true; + let children = aBrowsingContext.getChildren(); + for (let child of children) { + list.push(...gatherBrowsingContexts(child)); } - if (hasScreenShare.value != mediaManagerService.STATE_NOCAPTURE) { - result.screen = "Screen"; - } else if (hasWindowShare.value != mediaManagerService.STATE_NOCAPTURE) { - result.screen = "Window"; - } else if (hasBrowserShare.value != mediaManagerService.STATE_NOCAPTURE) { - result.screen = "Browser"; - } + return list; + } - return result; - }); + function combine(x, y) { + if ( + x == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED || + y == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED + ) { + return Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED; + } + if ( + x == Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED || + y == Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED + ) { + return Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED; + } + return Ci.nsIMediaManagerService.STATE_NOCAPTURE; + } + + let video = Ci.nsIMediaManagerService.STATE_NOCAPTURE; + let audio = Ci.nsIMediaManagerService.STATE_NOCAPTURE; + let screen = Ci.nsIMediaManagerService.STATE_NOCAPTURE; + let window = Ci.nsIMediaManagerService.STATE_NOCAPTURE; + let browser = Ci.nsIMediaManagerService.STATE_NOCAPTURE; + + for (let bc of gatherBrowsingContexts( + gBrowser.selectedBrowser.browsingContext + )) { + let state = await SpecialPowers.spawn(bc, [], async function() { + let mediaManagerService = Cc[ + "@mozilla.org/mediaManagerService;1" + ].getService(Ci.nsIMediaManagerService); + + let hasCamera = {}; + let hasMicrophone = {}; + let hasScreenShare = {}; + let hasWindowShare = {}; + let hasBrowserShare = {}; + mediaManagerService.mediaCaptureWindowState( + content, + hasCamera, + hasMicrophone, + hasScreenShare, + hasWindowShare, + hasBrowserShare, + false + ); + + return { + video: hasCamera.value, + audio: hasMicrophone.value, + screen: hasScreenShare.value, + window: hasWindowShare.value, + browser: hasBrowserShare.value, + }; + }); + + video = combine(state.video, video); + audio = combine(state.audio, audio); + screen = combine(state.screen, screen); + window = combine(state.window, window); + browser = combine(state.browser, browser); + } + + let result = {}; + + if (video != Ci.nsIMediaManagerService.STATE_NOCAPTURE) { + result.video = true; + } + if (audio != Ci.nsIMediaManagerService.STATE_NOCAPTURE) { + result.audio = true; + } + + if (screen != Ci.nsIMediaManagerService.STATE_NOCAPTURE) { + result.screen = "Screen"; + } else if (window != Ci.nsIMediaManagerService.STATE_NOCAPTURE) { + result.screen = "Window"; + } else if (browser != Ci.nsIMediaManagerService.STATE_NOCAPTURE) { + result.screen = "Browser"; + } + + return result; } async function stopSharing(aType = "camera", aShouldKeepSharing = false) { diff --git a/browser/components/BrowserGlue.jsm b/browser/components/BrowserGlue.jsm index fab867ddf497..899c4dc1a53d 100644 --- a/browser/components/BrowserGlue.jsm +++ b/browser/components/BrowserGlue.jsm @@ -330,6 +330,17 @@ let ACTORS = { }, }, }, + + WebRTC: { + parent: { + moduleURI: "resource:///actors/WebRTCParent.jsm", + }, + child: { + moduleURI: "resource:///actors/WebRTCChild.jsm", + }, + + allFrames: true, + }, }; let LEGACY_ACTORS = { @@ -392,19 +403,6 @@ let LEGACY_ACTORS = { observers: ["keyword-uri-fixup"], }, }, - - WebRTC: { - child: { - module: "resource:///actors/WebRTCChild.jsm", - messages: [ - "rtcpeer:Allow", - "rtcpeer:Deny", - "webrtc:Allow", - "webrtc:Deny", - "webrtc:StopSharing", - ], - }, - }, }; (function earlyBlankFirstPaint() { @@ -583,7 +581,6 @@ let initializedModules = {}; ], ["ContentSearch", "resource:///modules/ContentSearch.jsm", "init"], ["UpdateListener", "resource://gre/modules/UpdateListener.jsm", "init"], - ["webrtcUI", "resource:///modules/webrtcUI.jsm", "init"], ].forEach(([name, resource, init]) => { XPCOMUtils.defineLazyGetter(this, name, () => { ChromeUtils.import(resource, initializedModules); @@ -639,9 +636,6 @@ const listeners = { "AsyncPrefs:SetPref": ["AsyncPrefs"], "AsyncPrefs:ResetPref": ["AsyncPrefs"], // PLEASE KEEP THIS LIST IN SYNC WITH THE LISTENERS ADDED IN AsyncPrefs.init - - "webrtc:UpdateGlobalIndicators": ["webrtcUI"], - "webrtc:UpdatingIndicators": ["webrtcUI"], }, mm: { @@ -665,12 +659,6 @@ const listeners = { ContentSearch: ["ContentSearch"], "Reader:FaviconRequest": ["ReaderParent"], "Reader:UpdateReaderButton": ["ReaderParent"], - "rtcpeer:CancelRequest": ["webrtcUI"], - "rtcpeer:Request": ["webrtcUI"], - "webrtc:CancelRequest": ["webrtcUI"], - "webrtc:Request": ["webrtcUI"], - "webrtc:StopRecording": ["webrtcUI"], - "webrtc:UpdateBrowserIndicators": ["webrtcUI"], }, observe(subject, topic, data) { diff --git a/browser/fxr/content/permissions.js b/browser/fxr/content/permissions.js index 92493b71dd76..ee8d8fbd4271 100644 --- a/browser/fxr/content/permissions.js +++ b/browser/fxr/content/permissions.js @@ -87,18 +87,28 @@ class FxrWebRTCPrompt extends FxrPermissionPromptPrototype { allowedDevices.push(videoDevices[0].deviceIndex); } - this.targetBrowser.messageManager.sendAsyncMessage("webrtc:Allow", { - callID: this.request.callID, - windowID: this.request.windowID, - devices: allowedDevices, - }); + // WebRTCChild doesn't currently care which actor + // this is sent to and just uses the windowID. + this.targetBrowser.sendMessageToActor( + "webrtc:Allow", + { + callID: this.request.callID, + windowID: this.request.windowID, + devices: allowedDevices, + }, + "WebRTC" + ); } deny() { - this.targetBrowser.messageManager.sendAsyncMessage("webrtc:Deny", { - callID: this.request.callID, - windowID: this.request.windowID, - }); + this.targetBrowser.sendMessageToActor( + "webrtc:Deny", + { + callID: this.request.callID, + windowID: this.request.windowID, + }, + "WebRTC" + ); } } diff --git a/browser/modules/ContentObservers.js b/browser/modules/ContentObservers.js index ebc2158db5c8..0b39b88e7fd9 100644 --- a/browser/modules/ContentObservers.js +++ b/browser/modules/ContentObservers.js @@ -45,7 +45,8 @@ function getMessageManagerForWindow(aContentWindow) { Services.obs.addObserver(gEMEUIObserver, "mediakeys-request"); Services.obs.addObserver(gDecoderDoctorObserver, "decoder-doctor-notification"); -// WebRTCChild observer registration. +// WebRTCChild observer registration. Actor observers require the subject +// to be a window, so they are registered here instead. const kWebRTCObserverTopics = [ "getUserMedia:request", "recording-device-stopped", diff --git a/browser/modules/webrtcUI.jsm b/browser/modules/webrtcUI.jsm index f92c94a61085..4895c47f3a7d 100644 --- a/browser/modules/webrtcUI.jsm +++ b/browser/modules/webrtcUI.jsm @@ -10,9 +10,6 @@ const { EventEmitter } = ChromeUtils.import( "resource:///modules/syncedtabs/EventEmitter.jsm" ); const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); -const { XPCOMUtils } = ChromeUtils.import( - "resource://gre/modules/XPCOMUtils.jsm" -); ChromeUtils.defineModuleGetter( this, @@ -24,59 +21,41 @@ ChromeUtils.defineModuleGetter( "PluralForm", "resource://gre/modules/PluralForm.jsm" ); -ChromeUtils.defineModuleGetter( - this, - "PrivateBrowsingUtils", - "resource://gre/modules/PrivateBrowsingUtils.jsm" -); -ChromeUtils.defineModuleGetter( - this, - "SitePermissions", - "resource:///modules/SitePermissions.jsm" -); - -XPCOMUtils.defineLazyGetter(this, "gBrandBundle", function() { - return Services.strings.createBundle( - "chrome://branding/locale/brand.properties" - ); -}); - -XPCOMUtils.defineLazyServiceGetter( - this, - "OSPermissions", - "@mozilla.org/ospermissionrequest;1", - "nsIOSPermissionRequest" -); var webrtcUI = { + initialized: false, + peerConnectionBlockers: new Set(), emitter: new EventEmitter(), init() { - Services.obs.addObserver( - maybeAddMenuIndicator, - "browser-delayed-startup-finished" - ); - Services.ppmm.addMessageListener("child-process-shutdown", this); - }, - - uninit() { - Services.obs.removeObserver( - maybeAddMenuIndicator, - "browser-delayed-startup-finished" - ); - - if (gIndicatorWindow) { - gIndicatorWindow.close(); - gIndicatorWindow = null; + if (!this.initialized) { + Services.obs.addObserver(this, "browser-delayed-startup-finished"); + this.initialized = true; } }, - processIndicators: new Map(), + uninit() { + if (this.initialized) { + Services.obs.removeObserver(this, "browser-delayed-startup-finished"); + this.initialized = false; + } + }, + + observe(subject, topic, data) { + if (topic == "browser-delayed-startup-finished") { + if (webrtcUI.showGlobalIndicator) { + showOrCreateMenuForWindow(subject); + } + } + }, + + // Map of browser elements to indicator data. + perTabIndicators: new Map(), activePerms: new Map(), get showGlobalIndicator() { - for (let [, indicators] of this.processIndicators) { + for (let [, indicators] of this.perTabIndicators) { if (indicators.showGlobalIndicator) { return true; } @@ -85,7 +64,7 @@ var webrtcUI = { }, get showCameraIndicator() { - for (let [, indicators] of this.processIndicators) { + for (let [, indicators] of this.perTabIndicators) { if (indicators.showCameraIndicator) { return true; } @@ -94,7 +73,7 @@ var webrtcUI = { }, get showMicrophoneIndicator() { - for (let [, indicators] of this.processIndicators) { + for (let [, indicators] of this.perTabIndicators) { if (indicators.showMicrophoneIndicator) { return true; } @@ -104,7 +83,7 @@ var webrtcUI = { get showScreenSharingIndicator() { let list = [""]; - for (let [, indicators] of this.processIndicators) { + for (let [, indicators] of this.perTabIndicators) { if (indicators.showScreenSharingIndicator) { list.push(indicators.showScreenSharingIndicator); } @@ -138,7 +117,7 @@ var webrtcUI = { microphone: state.microphone, screen: state.screen, }; - let browser = aStream.browser; + let browser = aStream.topBrowsingContext.embedderElement; let browserWindow = browser.ownerGlobal; let tab = browserWindow.gBrowser && @@ -147,6 +126,187 @@ var webrtcUI = { }); }, + /** + * Determine the combined state of all the active streams associated with + * the specified top-level browsing context. + */ + getCombinedStateForBrowser(aTopBrowsingContext) { + function combine(x, y) { + if ( + x == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED || + y == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED + ) { + return Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED; + } + if ( + x == Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED || + y == Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED + ) { + return Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED; + } + return Ci.nsIMediaManagerService.STATE_NOCAPTURE; + } + + let camera, microphone, screen, window, browser; + for (let stream of this._streams) { + if (stream.topBrowsingContext == aTopBrowsingContext) { + camera = combine(stream.state.camera, camera); + microphone = combine(stream.state.microphone, microphone); + screen = combine(stream.state.screen, screen); + window = combine(stream.state.window, window); + browser = combine(stream.state.browser, browser); + } + } + + let tabState = { camera, microphone }; + if (screen == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED) { + tabState.screen = "Screen"; + } else if (window == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED) { + tabState.screen = "Window"; + } else if (browser == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED) { + tabState.screen = "Browser"; + } else if (screen == Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED) { + tabState.screen = "ScreenPaused"; + } else if (window == Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED) { + tabState.screen = "WindowPaused"; + } else if (browser == Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED) { + tabState.screen = "BrowserPaused"; + } + + let screenEnabled = tabState.screen && !tabState.screen.includes("Paused"); + let cameraEnabled = + tabState.camera == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED; + let microphoneEnabled = + tabState.microphone == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED; + + // tabState.sharing controls which global indicator should be shown + // for the tab. It should always be set to the _enabled_ device which + // we consider most intrusive (screen > camera > microphone). + if (screenEnabled) { + tabState.sharing = "screen"; + } else if (cameraEnabled) { + tabState.sharing = "camera"; + } else if (microphoneEnabled) { + tabState.sharing = "microphone"; + } else if (tabState.screen) { + tabState.sharing = "screen"; + } else if (tabState.camera) { + tabState.sharing = "camera"; + } else if (tabState.microphone) { + tabState.sharing = "microphone"; + } + + // The stream is considered paused when we're sharing something + // but all devices are off or set to disabled. + tabState.paused = + tabState.sharing && + !screenEnabled && + !cameraEnabled && + !microphoneEnabled; + + if ( + tabState.camera == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED || + tabState.camera == Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED + ) { + tabState.showCameraIndicator = true; + } + if ( + tabState.microphone == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED || + tabState.microphone == Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED + ) { + tabState.showMicrophoneIndicator = true; + } + + tabState.showScreenSharingIndicator = ""; + if (tabState.screen) { + if (tabState.screen.startsWith("Screen")) { + tabState.showScreenSharingIndicator = "Screen"; + } else if (tabState.screen.startsWith("Window")) { + if (tabState.showScreenSharingIndicator != "Screen") { + tabState.showScreenSharingIndicator = "Window"; + } + } else if (tabState.screen.startsWith("Browser")) { + if (!tabState.showScreenSharingIndicator) { + tabState.showScreenSharingIndicator = "Browser"; + } + } + } + + return tabState; + }, + + /* + * Indicate that a stream has been added or removed from the given + * browsing context. If it has been added, aData specifies the + * specific indicator types it uses. If aData is null or has no + * documentURI assigned, then the stream has been removed. + */ + streamAddedOrRemoved(aBrowsingContext, aData) { + this.init(); + + let index; + for (index = 0; index < webrtcUI._streams.length; ++index) { + let stream = this._streams[index]; + if (stream.browsingContext == aBrowsingContext) { + break; + } + } + // If there's no documentURI, the update is actually a removal of the + // stream, triggered by the recording-window-ended notification. + if (!aData || !aData.documentURI) { + if (index < this._streams.length) { + this._streams.splice(index, 1); + } + } else if (aData) { + this._streams[index] = { + browsingContext: aBrowsingContext, + topBrowsingContext: aBrowsingContext.top, + state: aData, + }; + } + }, + + /** + * Remove all the streams associated with a given + * browsing context. + */ + forgetStreamsFromBrowserContext(aBrowsingContext) { + for (let index = 0; index < webrtcUI._streams.length; ) { + let stream = this._streams[index]; + if (stream.browsingContext == aBrowsingContext) { + this._streams.splice(index, 1); + } else { + index++; + } + } + + if (aBrowsingContext == aBrowsingContext.top) { + this.perTabIndicators.delete(aBrowsingContext); + } + + this.updateGlobalIndicator(); + }, + + updateIndicators(aTopBrowsingContext) { + let tabState = this.getCombinedStateForBrowser(aTopBrowsingContext); + + let indicators; + if (this.perTabIndicators.has(aTopBrowsingContext)) { + indicators = this.perTabIndicators.get(aTopBrowsingContext); + } else { + indicators = {}; + this.perTabIndicators.set(aTopBrowsingContext, indicators); + } + + indicators.showGlobalIndicator = !!webrtcUI._streams.length; + indicators.showCameraIndicator = tabState.showCameraIndicator; + indicators.showMicrophoneIndicator = tabState.showMicrophoneIndicator; + indicators.showScreenSharingIndicator = tabState.showScreenSharingIndicator; + this.updateGlobalIndicator(); + + return tabState; + }, + swapBrowserForNotification(aOldBrowser, aNewBrowser) { for (let stream of this._streams) { if (stream.browser == aOldBrowser) { @@ -156,19 +316,7 @@ var webrtcUI = { }, forgetActivePermissionsFromBrowser(aBrowser) { - webrtcUI.activePerms.delete(aBrowser.outerWindowID); - }, - - forgetStreamsFromBrowser(aBrowser) { - this._streams = this._streams.filter(stream => stream.browser != aBrowser); - webrtcUI.forgetActivePermissionsFromBrowser(aBrowser); - }, - - forgetStreamsFromProcess(aProcessMM) { - // stream.processMM is null when e10s is disabled. - this._streams = this._streams.filter( - stream => stream.processMM && stream.processMM != aProcessMM - ); + this.activePerms.delete(aBrowser.outerWindowID); }, showSharingDoorhanger(aActiveStream) { @@ -245,1091 +393,69 @@ var webrtcUI = { return this.emitter.off(...args); }, - // Listeners and observers are registered in BrowserGlue.jsm - receiveMessage(aMessage) { - switch (aMessage.name) { - case "rtcpeer:Request": { - let params = Object.freeze( - Object.assign( - { - origin: aMessage.target.contentPrincipal.origin, - }, - aMessage.data - ) - ); - - let blockers = Array.from(this.peerConnectionBlockers); - - (async function() { - for (let blocker of blockers) { - try { - let result = await blocker(params); - if (result == "deny") { - return false; - } - } catch (err) { - Cu.reportError(`error in PeerConnection blocker: ${err.message}`); - } - } - return true; - })().then(decision => { - let message; - if (decision) { - this.emitter.emit("peer-request-allowed", params); - message = "rtcpeer:Allow"; - } else { - this.emitter.emit("peer-request-blocked", params); - message = "rtcpeer:Deny"; - } - - aMessage.target.messageManager.sendAsyncMessage(message, { - callID: params.callID, - windowID: params.windowID, - }); - }); - break; + getHostOrExtensionName(uri, href) { + let host; + try { + if (!uri) { + uri = Services.io.newURI(href); } - case "rtcpeer:CancelRequest": { - let params = Object.freeze({ - origin: aMessage.target.contentPrincipal.origin, - callID: aMessage.data, - }); - this.emitter.emit("peer-request-cancel", params); - break; + + let addonPolicy = WebExtensionPolicy.getByURI(uri); + host = addonPolicy ? addonPolicy.name : uri.host; + } catch (ex) {} + + if (!host) { + if (uri && uri.scheme.toLowerCase() == "about") { + // For about URIs, just use the full spec, without any #hash parts. + host = uri.specIgnoringRef; + } else { + // This is unfortunate, but we should display *something*... + const kBundleURI = "chrome://browser/locale/browser.properties"; + let bundle = Services.strings.createBundle(kBundleURI); + host = bundle.GetStringFromName("getUserMedia.sharingMenuUnknownHost"); } - case "webrtc:Request": - if (aMessage.target.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 availabe. - aMessage.target.fxrPermissionPrompt(aMessage.data); - } else { - prompt(aMessage.target, aMessage.data); + } + return host; + }, + + updateGlobalIndicator() { + for (let chromeWin of Services.wm.getEnumerator("navigator:browser")) { + if (this.showGlobalIndicator) { + showOrCreateMenuForWindow(chromeWin); + } else { + let doc = chromeWin.document; + let existingMenu = doc.getElementById("tabSharingMenu"); + if (existingMenu) { + existingMenu.hidden = true; } - break; - case "webrtc:StopRecording": - stopRecording(aMessage.target, aMessage.data); - break; - case "webrtc:CancelRequest": - removePrompt(aMessage.target, aMessage.data); - break; - case "webrtc:UpdatingIndicators": - webrtcUI.forgetStreamsFromProcess(aMessage.target); - break; - case "webrtc:UpdateGlobalIndicators": - updateIndicators(aMessage.data, aMessage.target); - break; - case "webrtc:UpdateBrowserIndicators": - let id = aMessage.data.windowId; - let processMM = - aMessage.targetFrameLoader.messageManager.processMessageManager; - let index; - for (index = 0; index < webrtcUI._streams.length; ++index) { - let stream = webrtcUI._streams[index]; - if (stream.state.windowId == id && stream.processMM == processMM) { - break; + if (AppConstants.platform == "macosx") { + let separator = doc.getElementById("tabSharingSeparator"); + if (separator) { + separator.hidden = true; } } - // If there's no documentURI, the update is actually a removal of the - // stream, triggered by the recording-window-ended notification. - if (!aMessage.data.documentURI && index < webrtcUI._streams.length) { - webrtcUI._streams.splice(index, 1); - } else { - webrtcUI._streams[index] = { - browser: aMessage.target, - processMM, - state: aMessage.data, - }; + } + } + + if (this.showGlobalIndicator) { + if (!gIndicatorWindow) { + gIndicatorWindow = getGlobalIndicator(); + } else { + try { + gIndicatorWindow.updateIndicatorState(); + } catch (err) { + Cu.reportError( + `error in gIndicatorWindow.updateIndicatorState(): ${err.message}` + ); } - let tabbrowser = aMessage.target.ownerGlobal.gBrowser; - if (tabbrowser) { - tabbrowser.updateBrowserSharing(aMessage.target, { - webRTC: aMessage.data, - }); - } - break; - case "child-process-shutdown": - webrtcUI.processIndicators.delete(aMessage.target); - webrtcUI.forgetStreamsFromProcess(aMessage.target); - updateIndicators(null, null); - break; + } + } else if (gIndicatorWindow) { + gIndicatorWindow.close(); + gIndicatorWindow = null; } }, }; -function denyRequest(aBrowser, aRequest) { - aBrowser.messageManager.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. -// -function denyRequestNoPermission(aBrowser, aRequest) { - aBrowser.messageManager.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 function 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) { - OSPermissions.getMediaCapturePermissionState(camStatus, micStatus); - } - if (camNeeded) { - let camPermission = camStatus.value; - let camAccessible = await checkAndGetOSPermission( - camPermission, - OSPermissions.requestVideoCapturePermission - ); - if (!camAccessible) { - return false; - } - } - if (micNeeded) { - let micPermission = micStatus.value; - let micAccessible = await checkAndGetOSPermission( - micPermission, - OSPermissions.requestAudioCapturePermission - ); - if (!micAccessible) { - return false; - } - } - let scrStatus = {}; - if (scrNeeded) { - OSPermissions.getScreenCapturePermissionState(scrStatus); - if (scrStatus.value == OSPermissions.PERMISSION_STATE_DENIED) { - 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 function checkAndGetOSPermission( - devicePermission, - requestPermissionFunc -) { - if ( - devicePermission == OSPermissions.PERMISSION_STATE_DENIED || - devicePermission == OSPermissions.PERMISSION_STATE_RESTRICTED - ) { - return false; - } - if (devicePermission == OSPermissions.PERMISSION_STATE_NOTDETERMINED) { - let deviceAllowed = await requestPermissionFunc(); - if (!deviceAllowed) { - return false; - } - } - return true; -} - -function getHostOrExtensionName(uri, href) { - let host; - try { - if (!uri) { - uri = Services.io.newURI(href); - } - - let addonPolicy = WebExtensionPolicy.getByURI(uri); - host = addonPolicy ? addonPolicy.name : uri.host; - } catch (ex) {} - - if (!host) { - if (uri && uri.scheme.toLowerCase() == "about") { - // For about URIs, just use the full spec, without any #hash parts. - host = uri.specIgnoringRef; - } else { - // This is unfortunate, but we should display *something*... - const kBundleURI = "chrome://browser/locale/browser.properties"; - let bundle = Services.strings.createBundle(kBundleURI); - host = bundle.GetStringFromName("getUserMedia.sharingMenuUnknownHost"); - } - } - return host; -} - -function stopRecording(aBrowser, aRequest) { - let outerWindowID = aBrowser.outerWindowID; - - if (!webrtcUI.activePerms.has(outerWindowID)) { - return; - } - - if (!aRequest.rawID) { - webrtcUI.activePerms.delete(outerWindowID); - } else { - let set = webrtcUI.activePerms.get(outerWindowID); - set.delete(aRequest.windowID + aRequest.mediaSource + aRequest.rawID); - } -} - -/** - * 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. - */ -function checkRequestAllowed(aRequest, aPrincipal, aBrowser) { - if (!aRequest.secure) { - return false; - } - - let { audioDevices, videoDevices, sharingScreen } = aRequest; - - let micAllowed = - SitePermissions.getForPrincipal(aPrincipal, "microphone").state == - SitePermissions.ALLOW; - let camAllowed = - SitePermissions.getForPrincipal(aPrincipal, "camera").state == - SitePermissions.ALLOW; - - let perms = Services.perms; - let mediaManagerPerm = perms.testExactPermissionFromPrincipal( - aPrincipal, - "MediaManagerVideo" - ); - if (mediaManagerPerm) { - perms.removeFromPrincipal(aPrincipal, "MediaManagerVideo"); - } - - // Screen sharing shouldn't follow the camera permissions. - if (videoDevices.length && sharingScreen) { - camAllowed = false; - } - if (aRequest.isThirdPartyOrigin && !aRequest.shouldDelegatePermission) { - camAllowed = false; - micAllowed = false; - } - - let activeCamera; - let activeMic; - - // Always prompt for screen sharing - if (!sharingScreen) { - for (let device of videoDevices) { - let set = webrtcUI.activePerms.get(aBrowser.outerWindowID); - if (set && set.has(aRequest.windowID + device.mediaSource + device.id)) { - activeCamera = device; - break; - } - } - - for (let device of audioDevices) { - let set = webrtcUI.activePerms.get(aBrowser.outerWindowID); - if (set && set.has(aRequest.windowID + device.mediaSource + device.id)) { - activeMic = device; - break; - } - } - } - - if ( - (!audioDevices.length || micAllowed || activeMic) && - (!videoDevices.length || camAllowed || activeCamera) - ) { - let allowedDevices = []; - if (videoDevices.length) { - allowedDevices.push((activeCamera || videoDevices[0]).deviceIndex); - Services.perms.addFromPrincipal( - aPrincipal, - "MediaManagerVideo", - Services.perms.ALLOW_ACTION, - Services.perms.EXPIRE_SESSION - ); - } - if (audioDevices.length) { - allowedDevices.push((activeMic || audioDevices[0]).deviceIndex); - } - - // Remember on which URIs we found persistent permissions so that we - // can remove them if the user clicks 'Stop Sharing'. There's no - // other way for the stop sharing code to know the hostnames of frames - // using devices until bug 1066082 is fixed. - let browser = aBrowser; - browser.getDevicePermissionOrigins("webrtc").add(aPrincipal.origin); - - // If sharingScreen, we're requesting screen-sharing, otherwise camera - let camNeeded = !!videoDevices.length && !sharingScreen; - let scrNeeded = !!videoDevices.length && sharingScreen; - let micNeeded = !!audioDevices.length; - checkOSPermission(camNeeded, micNeeded, scrNeeded).then(havePermission => { - if (havePermission) { - let mm = browser.messageManager; - mm.sendAsyncMessage("webrtc:Allow", { - callID: aRequest.callID, - windowID: aRequest.windowID, - devices: allowedDevices, - }); - } else { - denyRequestNoPermission(browser, aRequest); - } - }); - - return true; - } - - return false; -} - -function prompt(aBrowser, aRequest) { - let { - audioDevices, - videoDevices, - 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) { - denyRequest(aBrowser, 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 ( - aRequest.secondOrigin || - !checkRequestAllowed(aRequest, principal, aBrowser) - ) { - denyRequest(aBrowser, aRequest); - } - return; - } - } - - // If the user has already denied access once in this tab, - // deny again without even showing the notification icon. - if ( - (audioDevices.length && - SitePermissions.getForPrincipal(principal, "microphone", aBrowser) - .state == SitePermissions.BLOCK) || - (videoDevices.length && - SitePermissions.getForPrincipal( - principal, - sharingScreen ? "screen" : "camera", - aBrowser - ).state == SitePermissions.BLOCK) - ) { - denyRequest(aBrowser, aRequest); - return; - } - - // Tell the browser to refresh the identity block display in case there - // are expired permission states. - aBrowser.dispatchEvent( - new aBrowser.ownerGlobal.CustomEvent("PermissionStateChange") - ); - - let chromeDoc = aBrowser.ownerDocument; - let stringBundle = chromeDoc.defaultView.gNavigatorBundle; - - // Mind the order, because for simplicity we're iterating over the list using - // "includes()". This allows the rotation of string identifiers. We list the - // full identifiers here so they can be cross-referenced more easily. - let joinedRequestTypes = requestTypes.join("And"); - let requestMessages; - if (aRequest.secondOrigin) { - requestMessages = [ - // Individual request types first. - "getUserMedia.shareCameraUnsafeDelegation.message", - "getUserMedia.shareMicrophoneUnsafeDelegation.message", - "getUserMedia.shareScreenUnsafeDelegation.message", - "getUserMedia.shareAudioCaptureUnsafeDelegation.message", - // Combinations of the above request types last. - "getUserMedia.shareCameraAndMicrophoneUnsafeDelegation.message", - "getUserMedia.shareCameraAndAudioCaptureUnsafeDelegation.message", - "getUserMedia.shareScreenAndMicrophoneUnsafeDelegation.message", - "getUserMedia.shareScreenAndAudioCaptureUnsafeDelegation.message", - ]; - } else { - requestMessages = [ - // Individual request types first. - "getUserMedia.shareCamera2.message", - "getUserMedia.shareMicrophone2.message", - "getUserMedia.shareScreen3.message", - "getUserMedia.shareAudioCapture2.message", - // Combinations of the above request types last. - "getUserMedia.shareCameraAndMicrophone2.message", - "getUserMedia.shareCameraAndAudioCapture2.me ssage", - "getUserMedia.shareScreenAndMicrophone3.message", - "getUserMedia.shareScreenAndAudioCapture3.message", - ]; - } - - let stringId = requestMessages.find(id => id.includes(joinedRequestTypes)); - let message = aRequest.secondOrigin - ? stringBundle.getFormattedString(stringId, ["<>", "{}"]) - : stringBundle.getFormattedString(stringId, ["<>"]); - - let notification; // Used by action callbacks. - let mainAction = { - label: stringBundle.getString("getUserMedia.allow.label"), - accessKey: stringBundle.getString("getUserMedia.allow.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() {}, - }; - - let secondaryActions = [ - { - label: stringBundle.getString("getUserMedia.dontAllow.label"), - accessKey: stringBundle.getString("getUserMedia.dontAllow.accesskey"), - callback(aState) { - denyRequest(notification.browser, aRequest); - let scope = SitePermissions.SCOPE_TEMPORARY; - if (aState && aState.checkboxChecked) { - scope = SitePermissions.SCOPE_PERSISTENT; - } - if (audioDevices.length) { - SitePermissions.setForPrincipal( - principal, - "microphone", - SitePermissions.BLOCK, - scope, - notification.browser - ); - } - if (videoDevices.length) { - SitePermissions.setForPrincipal( - principal, - sharingScreen ? "screen" : "camera", - SitePermissions.BLOCK, - scope, - notification.browser - ); - } - }, - }, - ]; - - let productName = gBrandBundle.GetStringFromName("brandShortName"); - - let options = { - name: 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 ( - ((aTopic == "dismissed" || aTopic == "removed") && - requestTypes.includes("Screen")) || - !requestTypes.includes("Screen") - ) { - let video = doc.getElementById("webRTC-previewVideo"); - 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) { - denyRequest(notification.browser, aRequest); - } - - if (aTopic != "showing") { - return false; - } - - // BLOCK is handled immediately by MediaManager if it has been set - // persistently in the permission manager. If it has been set on the tab, - // 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 we have a secondOrigin, it means this request is lacking explicit - // trust, and we should always prompt even in with persistent permission. - if ( - !aRequest.secondOrigin && - checkRequestAllowed(aRequest, principal, aBrowser) - ) { - this.remove(); - return true; - } - - function listDevices(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; - - for (let device of devices) { - addDeviceToList(menupopup, device.name, device.deviceIndex); - } - } - - function checkDisabledWindowMenuItem() { - let list = doc.getElementById("webRTC-selectWindow-menulist"); - let item = list.selectedItem; - let notificationElement = doc.getElementById( - "webRTC-shareDevices-notification" - ); - 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; - - let label = doc.getElementById("webRTC-selectWindow-label"); - const gumStringId = "getUserMedia.selectWindowOrScreen"; - label.setAttribute( - "value", - stringBundle.getString(gumStringId + ".label") - ); - label.setAttribute( - "accesskey", - stringBundle.getString(gumStringId + ".accesskey") - ); - - // "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, - stringBundle.getString("getUserMedia.pickWindowOrScreen.label"), - "-1" - ); - menupopup.appendChild(doc.createXULElement("menuseparator")); - - // 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; - // Building screen list from available screens. - if (type == "screen") { - if (device.name == "Primary Monitor") { - name = stringBundle.getString( - "getUserMedia.shareEntireScreen.label" - ); - } else { - name = stringBundle.getFormattedString( - "getUserMedia.shareMonitor.label", - [monitorIndex] - ); - ++monitorIndex; - } - } else { - name = device.name; - if (type == "application") { - // The application names returned by the platform are of the form: - // \x1e - let sepIndex = name.indexOf("\x1e"); - let count = name.slice(0, sepIndex); - let sawcStringId = - "getUserMedia.shareApplicationWindowCount.label"; - name = PluralForm.get( - parseInt(count), - stringBundle.getString(sawcStringId) - ) - .replace("#1", name.slice(sepIndex + 1)) - .replace("#2", count); - } - } - let item = addDeviceToList(menupopup, name, i, type); - item.deviceId = device.id; - item.mediaSource = type; - if (device.scary) { - item.scary = true; - } - } - - // Always re-select the "No " 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; - } - - let type = event.target.mediaSource; - let deviceId = event.target.deviceId; - if (deviceId == undefined) { - doc.getElementById("webRTC-preview").hidden = true; - video.src = null; - return; - } - - let scary = event.target.scary; - let warning = doc.getElementById("webRTC-previewWarning"); - warning.hidden = !scary; - let chromeWin = doc.defaultView; - if (scary) { - warning.hidden = false; - let string; - let bundle = chromeWin.gNavigatorBundle; - - let learnMoreText = bundle.getString( - "getUserMedia.shareScreen.learnMoreLabel" - ); - let baseURL = Services.urlFormatter.formatURLPref( - "app.support.baseURL" - ); - - let learnMore = chromeWin.document.createXULElement("label", { - is: "text-link", - }); - learnMore.setAttribute("href", baseURL + "screenshare-safety"); - learnMore.textContent = learnMoreText; - - if (type == "screen") { - string = bundle.getFormattedString( - "getUserMedia.shareScreenWarning.message", - ["<>"] - ); - } else { - let brand = doc - .getElementById("bundle_brand") - .getString("brandShortName"); - string = bundle.getFormattedString( - "getUserMedia.shareFirefoxWarning.message", - [brand, "<>"] - ); - } - - let [pre, post] = string.split("<>"); - warning.textContent = pre; - warning.appendChild(learnMore); - warning.appendChild(chromeWin.document.createTextNode(post)); - - // 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 = {}; - OSPermissions.getScreenCapturePermissionState(scrStatus); - if (scrStatus.value == OSPermissions.PERMISSION_STATE_DENIED) { - OSPermissions.maybeRequestScreenCapturePermission(); - } - } - - let perms = Services.perms; - let chromePrincipal = Services.scriptSecurityManager.getSystemPrincipal(); - perms.addFromPrincipal( - chromePrincipal, - "MediaManagerVideo", - perms.ALLOW_ACTION, - perms.EXPIRE_SESSION - ); - - video.deviceId = deviceId; - let constraints = { - video: { mediaSource: type, 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; - } - Cu.reportError( - `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 = - !videoDevices.length || sharingScreen; - doc.getElementById("webRTC-selectWindowOrScreen").hidden = - !sharingScreen || !videoDevices.length; - doc.getElementById("webRTC-selectMicrophone").hidden = - !audioDevices.length || sharingAudio; - - let camMenupopup = doc.getElementById("webRTC-selectCamera-menupopup"); - let windowMenupopup = doc.getElementById("webRTC-selectWindow-menupopup"); - let micMenupopup = doc.getElementById( - "webRTC-selectMicrophone-menupopup" - ); - if (sharingScreen) { - listScreenShareDevices(windowMenupopup, videoDevices); - checkDisabledWindowMenuItem(); - } else { - listDevices(camMenupopup, videoDevices); - doc - .getElementById("webRTC-shareDevices-notification") - .removeAttribute("invalidselection"); - } - - if (!sharingAudio) { - listDevices(micMenupopup, audioDevices); - } - - this.mainAction.callback = async function(aState) { - let remember = aState && aState.checkboxChecked; - let allowedDevices = []; - let perms = Services.perms; - if (videoDevices.length) { - let listId = - "webRTC-select" + - (sharingScreen ? "Window" : "Camera") + - "-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 - ); - if (!webrtcUI.activePerms.has(aBrowser.outerWindowID)) { - webrtcUI.activePerms.set(aBrowser.outerWindowID, new Set()); - } - - for (let device of videoDevices) { - if (device.deviceIndex == videoDeviceIndex) { - webrtcUI.activePerms - .get(aBrowser.outerWindowID) - .add(aRequest.windowID + device.mediaSource + device.id); - break; - } - } - if (remember) { - SitePermissions.setForPrincipal( - principal, - "camera", - SitePermissions.ALLOW - ); - } - } - } - if (audioDevices.length) { - if (!sharingAudio) { - let audioDeviceIndex = doc.getElementById( - "webRTC-selectMicrophone-menulist" - ).value; - let allowMic = audioDeviceIndex != "-1"; - if (allowMic) { - allowedDevices.push(audioDeviceIndex); - if (!webrtcUI.activePerms.has(aBrowser.outerWindowID)) { - webrtcUI.activePerms.set(aBrowser.outerWindowID, new Set()); - } - - for (let device of audioDevices) { - if (device.deviceIndex == audioDeviceIndex) { - webrtcUI.activePerms - .get(aBrowser.outerWindowID) - .add(aRequest.windowID + device.mediaSource + device.id); - break; - } - } - if (remember) { - SitePermissions.setForPrincipal( - principal, - "microphone", - SitePermissions.ALLOW - ); - } - } - } else { - // Only one device possible for audio capture. - allowedDevices.push(0); - } - } - - if (!allowedDevices.length) { - denyRequest(notification.browser, aRequest); - return; - } - - if (remember) { - // Remember on which URIs we set persistent permissions so that we - // can remove them if the user clicks 'Stop Sharing'. - aBrowser.getDevicePermissionOrigins("webrtc").add(principal.origin); - } - - let camNeeded = !!videoDevices.length && !sharingScreen; - let scrNeeded = !!videoDevices.length && sharingScreen; - let micNeeded = !!audioDevices.length; - let havePermission = await checkOSPermission( - camNeeded, - micNeeded, - scrNeeded - ); - if (!havePermission) { - denyRequestNoPermission(notification.browser, aRequest); - return; - } - - let mm = notification.browser.messageManager; - mm.sendAsyncMessage("webrtc:Allow", { - callID: aRequest.callID, - windowID: aRequest.windowID, - devices: allowedDevices, - }); - }; - - // 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 (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; - } - - 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). - let reasonForNoPermanentAllow = ""; - if (sharingScreen) { - reasonForNoPermanentAllow = - "getUserMedia.reasonForNoPermanentAllow.screen3"; - } else if (sharingAudio) { - reasonForNoPermanentAllow = - "getUserMedia.reasonForNoPermanentAllow.audio"; - } else if (!aRequest.secure) { - reasonForNoPermanentAllow = - "getUserMedia.reasonForNoPermanentAllow.insecure"; - } - - options.checkbox = { - label: stringBundle.getString("getUserMedia.remember"), - checked: principal.isAddonOrExpandedAddonPrincipal, - checkedState: reasonForNoPermanentAllow - ? { - disableMainAction: true, - warningLabel: stringBundle.getFormattedString( - reasonForNoPermanentAllow, - [productName] - ), - } - : undefined, - }; - } - - let iconType = "Devices"; - if ( - requestTypes.length == 1 && - (requestTypes[0] == "Microphone" || requestTypes[0] == "AudioCapture") - ) { - iconType = "Microphone"; - } - if (requestTypes.includes("Screen")) { - iconType = "Screen"; - } - let anchorId = "webRTC-share" + iconType + "-notification-icon"; - - let iconClass = iconType.toLowerCase(); - if (iconClass == "devices") { - iconClass = "camera"; - } - options.popupIconClass = iconClass + "-icon"; - - if (aRequest.secondOrigin) { - options.secondName = 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); - } -} - -function removePrompt(aBrowser, aCallId) { - let chromeWin = aBrowser.ownerGlobal; - let notification = chromeWin.PopupNotifications.getNotification( - "webRTC-shareDevices", - aBrowser - ); - if (notification && notification.callID == aCallId) { - notification.remove(); - } -} - function getGlobalIndicator() { if (AppConstants.platform != "macosx") { const INDICATOR_CHROME_URI = @@ -1493,7 +619,7 @@ function onTabSharingMenuPopupShowing(e) { let doc = e.target.ownerDocument; let bundle = doc.defaultView.gNavigatorBundle; - let origin = getHostOrExtensionName(null, streamInfo.uri); + let origin = webrtcUI.getHostOrExtensionName(null, streamInfo.uri); let menuitem = doc.createXULElement("menuitem"); menuitem.setAttribute( "label", @@ -1552,63 +678,4 @@ function showOrCreateMenuForWindow(aWindow) { } } -function maybeAddMenuIndicator(window) { - if (webrtcUI.showGlobalIndicator) { - showOrCreateMenuForWindow(window); - } -} - var gIndicatorWindow = null; - -function updateIndicators(data, target) { - if (data) { - // the global indicators specific to this process - let indicators; - if (webrtcUI.processIndicators.has(target)) { - indicators = webrtcUI.processIndicators.get(target); - } else { - indicators = {}; - webrtcUI.processIndicators.set(target, indicators); - } - - indicators.showGlobalIndicator = data.showGlobalIndicator; - indicators.showCameraIndicator = data.showCameraIndicator; - indicators.showMicrophoneIndicator = data.showMicrophoneIndicator; - indicators.showScreenSharingIndicator = data.showScreenSharingIndicator; - } - - for (let chromeWin of Services.wm.getEnumerator("navigator:browser")) { - if (webrtcUI.showGlobalIndicator) { - showOrCreateMenuForWindow(chromeWin); - } else { - let doc = chromeWin.document; - let existingMenu = doc.getElementById("tabSharingMenu"); - if (existingMenu) { - existingMenu.hidden = true; - } - if (AppConstants.platform == "macosx") { - let separator = doc.getElementById("tabSharingSeparator"); - if (separator) { - separator.hidden = true; - } - } - } - } - - if (webrtcUI.showGlobalIndicator) { - if (!gIndicatorWindow) { - gIndicatorWindow = getGlobalIndicator(); - } else { - try { - gIndicatorWindow.updateIndicatorState(); - } catch (err) { - Cu.reportError( - `error in gIndicatorWindow.updateIndicatorState(): ${err.message}` - ); - } - } - } else if (gIndicatorWindow) { - gIndicatorWindow.close(); - gIndicatorWindow = null; - } -}