fune/browser/modules/webrtcUI.jsm
Johann Hofmann 141c4f866b Bug 1348257 - Hide the "always remember" checkbox for webrtc prompts in PBM. r=Nihanth
It's important to not remember any user choices made in private mode
(except the ones we specifically mention in the private browsing landing
page). The original idea was to change the checkbox behavior to store
the permission for session duration if in PBM, but this would actually be
inconsistent with the other permission prompts which hide it completely.

MozReview-Commit-ID: 8ioVmE4XefE

--HG--
extra : rebase_source : bddb6fcd9644ba4275b1da8428518285da387c2b
2017-03-20 19:49:59 +01:00

1103 lines
41 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["webrtcUI"];
const Cu = Components.utils;
const Cc = Components.classes;
const Ci = Components.interfaces;
Cu.import("resource:///modules/syncedtabs/EventEmitter.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
"resource://gre/modules/PluralForm.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "SitePermissions",
"resource:///modules/SitePermissions.jsm");
XPCOMUtils.defineLazyGetter(this, "gBrandBundle", function() {
return Services.strings.createBundle("chrome://branding/locale/brand.properties");
});
this.webrtcUI = {
peerConnectionBlockers: new Set(),
emitter: new EventEmitter(),
init() {
Services.obs.addObserver(maybeAddMenuIndicator, "browser-delayed-startup-finished", false);
let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
.getService(Ci.nsIMessageBroadcaster);
ppmm.addMessageListener("webrtc:UpdatingIndicators", this);
ppmm.addMessageListener("webrtc:UpdateGlobalIndicators", this);
ppmm.addMessageListener("child-process-shutdown", this);
let mm = Cc["@mozilla.org/globalmessagemanager;1"]
.getService(Ci.nsIMessageListenerManager);
mm.addMessageListener("rtcpeer:Request", this);
mm.addMessageListener("rtcpeer:CancelRequest", this);
mm.addMessageListener("webrtc:Request", this);
mm.addMessageListener("webrtc:StopRecording", this);
mm.addMessageListener("webrtc:CancelRequest", this);
mm.addMessageListener("webrtc:UpdateBrowserIndicators", this);
},
uninit() {
Services.obs.removeObserver(maybeAddMenuIndicator, "browser-delayed-startup-finished");
let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
.getService(Ci.nsIMessageBroadcaster);
ppmm.removeMessageListener("webrtc:UpdatingIndicators", this);
ppmm.removeMessageListener("webrtc:UpdateGlobalIndicators", this);
let mm = Cc["@mozilla.org/globalmessagemanager;1"]
.getService(Ci.nsIMessageListenerManager);
mm.removeMessageListener("rtcpeer:Request", this);
mm.removeMessageListener("rtcpeer:CancelRequest", this);
mm.removeMessageListener("webrtc:Request", this);
mm.removeMessageListener("webrtc:StopRecording", this);
mm.removeMessageListener("webrtc:CancelRequest", this);
mm.removeMessageListener("webrtc:UpdateBrowserIndicators", this);
if (gIndicatorWindow) {
gIndicatorWindow.close();
gIndicatorWindow = null;
}
},
processIndicators: new Map(),
activePerms: new Map(),
get showGlobalIndicator() {
for (let [, indicators] of this.processIndicators) {
if (indicators.showGlobalIndicator)
return true;
}
return false;
},
get showCameraIndicator() {
for (let [, indicators] of this.processIndicators) {
if (indicators.showCameraIndicator)
return true;
}
return false;
},
get showMicrophoneIndicator() {
for (let [, indicators] of this.processIndicators) {
if (indicators.showMicrophoneIndicator)
return true;
}
return false;
},
get showScreenSharingIndicator() {
let list = [""];
for (let [, indicators] of this.processIndicators) {
if (indicators.showScreenSharingIndicator)
list.push(indicators.showScreenSharingIndicator);
}
let precedence =
["Screen", "Window", "Application", "Browser", ""];
list.sort((a, b) => {
return precedence.indexOf(a) -
precedence.indexOf(b);
});
return list[0];
},
_streams: [],
// The boolean parameters indicate which streams should be included in the result.
getActiveStreams(aCamera, aMicrophone, aScreen) {
return webrtcUI._streams.filter(aStream => {
let state = aStream.state;
return aCamera && state.camera ||
aMicrophone && state.microphone ||
aScreen && state.screen;
}).map(aStream => {
let state = aStream.state;
let types = {camera: state.camera, microphone: state.microphone,
screen: state.screen};
let browser = aStream.browser;
let browserWindow = browser.ownerGlobal;
let tab = browserWindow.gBrowser &&
browserWindow.gBrowser.getTabForBrowser(browser);
return {uri: state.documentURI, tab, browser, types};
});
},
swapBrowserForNotification(aOldBrowser, aNewBrowser) {
for (let stream of this._streams) {
if (stream.browser == aOldBrowser)
stream.browser = aNewBrowser;
}
},
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);
},
showSharingDoorhanger(aActiveStream) {
let browserWindow = aActiveStream.browser.ownerGlobal;
if (aActiveStream.tab) {
browserWindow.gBrowser.selectedTab = aActiveStream.tab;
} else {
aActiveStream.browser.focus();
}
browserWindow.focus();
let identityBox = browserWindow.document.getElementById("identity-box");
if (AppConstants.platform == "macosx" && !Services.focus.activeWindow) {
browserWindow.addEventListener("activate", function() {
Services.tm.mainThread.dispatch(function() {
identityBox.click();
}, Ci.nsIThread.DISPATCH_NORMAL);
}, {once: true});
Cc["@mozilla.org/widget/macdocksupport;1"].getService(Ci.nsIMacDockSupport)
.activateApplication(true);
return;
}
identityBox.click();
},
updateWarningLabel(aMenuList) {
let type = aMenuList.selectedItem.getAttribute("devicetype");
let document = aMenuList.ownerDocument;
document.getElementById("webRTC-all-windows-shared").hidden = type != "Screen";
},
// Add-ons can override stock permission behavior by doing:
//
// webrtcUI.addPeerConnectionBlocker(function(aParams) {
// // new permission checking logic
// }));
//
// The blocking function receives an object with origin, callID, and windowID
// parameters. If it returns the string "deny" or a Promise that resolves
// to "deny", the connection is immediately blocked. With any other return
// value (though the string "allow" is suggested for consistency), control
// is passed to other registered blockers. If no registered blockers block
// the connection (or of course if there are no registered blockers), then
// the connection is allowed.
//
// Add-ons may also use webrtcUI.on/off to listen to events without
// blocking anything:
// peer-request-allowed is emitted when a new peer connection is
// established (and not blocked).
// peer-request-blocked is emitted when a peer connection request is
// blocked by some blocking connection handler.
// peer-request-cancel is emitted when a peer-request connection request
// is canceled. (This would typically be used in
// conjunction with a blocking handler to cancel
// a user prompt or other work done by the handler)
addPeerConnectionBlocker(aCallback) {
this.peerConnectionBlockers.add(aCallback);
},
removePeerConnectionBlocker(aCallback) {
this.peerConnectionBlockers.delete(aCallback);
},
on(...args) {
return this.emitter.on(...args);
},
off(...args) {
return this.emitter.off(...args);
},
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);
Task.spawn(function*() {
for (let blocker of blockers) {
try {
let result = yield 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;
}
case "rtcpeer:CancelRequest": {
let params = Object.freeze({
origin: aMessage.target.contentPrincipal.origin,
callID: aMessage.data
});
this.emitter.emit("peer-request-cancel", params);
break;
}
case "webrtc:Request":
prompt(aMessage.target, aMessage.data);
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;
aMessage.targetFrameLoader.QueryInterface(Ci.nsIFrameLoader);
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 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
};
}
let tabbrowser = aMessage.target.ownerGlobal.gBrowser;
if (tabbrowser)
tabbrowser.setBrowserSharing(aMessage.target, aMessage.data);
break;
case "child-process-shutdown":
webrtcUI.processIndicators.delete(aMessage.target);
webrtcUI.forgetStreamsFromProcess(aMessage.target);
updateIndicators(null, null);
break;
}
}
};
function getBrowserForWindow(aContentWindow) {
return aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell)
.chromeEventHandler;
}
function denyRequest(aBrowser, aRequest) {
aBrowser.messageManager.sendAsyncMessage("webrtc:Deny",
{callID: aRequest.callID,
windowID: aRequest.windowID});
}
function getHost(uri, href) {
let host;
try {
if (!uri) {
uri = Services.io.newURI(href);
}
host = 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);
}
}
function prompt(aBrowser, aRequest) {
let { audioDevices, videoDevices, sharingScreen, sharingAudio,
requestTypes } = aRequest;
// If the user has already denied access once in this tab,
// deny again without even showing the notification icon.
if ((audioDevices.length && SitePermissions
.get(null, "microphone", aBrowser).state == SitePermissions.BLOCK) ||
(videoDevices.length && SitePermissions
.get(null, 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 uri = Services.io.newURI(aRequest.documentURI);
let host = getHost(uri);
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 stringId = [
// 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.message",
"getUserMedia.shareScreenAndMicrophone3.message",
"getUserMedia.shareScreenAndAudioCapture3.message",
].find(id => id.includes(joinedRequestTypes));
let message = stringBundle.getFormattedString(stringId, [host]);
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.set(uri, "microphone",
SitePermissions.BLOCK, scope, notification.browser);
if (videoDevices.length)
SitePermissions.set(uri, sharingScreen ? "screen" : "camera",
SitePermissions.BLOCK, scope, notification.browser);
}
}
];
let productName = gBrandBundle.GetStringFromName("brandShortName");
let options = {
persistent: true,
hideClose: !Services.prefs.getBoolPref("privacy.permissionPrompts.showCloseButton"),
eventCallback(aTopic, aNewBrowser) {
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")) {
let video = doc.getElementById("webRTC-previewVideo");
video.deviceId = undefined;
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 (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 (aRequest.secure) {
let micAllowed =
SitePermissions.get(uri, "microphone").state == SitePermissions.ALLOW;
let camAllowed =
SitePermissions.get(uri, "camera").state == SitePermissions.ALLOW;
let perms = Services.perms;
let mediaManagerPerm =
perms.testExactPermission(uri, "MediaManagerVideo");
if (mediaManagerPerm) {
perms.remove(uri, "MediaManagerVideo");
}
// Screen sharing shouldn't follow the camera permissions.
if (videoDevices.length && sharingScreen)
camAllowed = false;
let activeCamera;
let activeMic;
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.add(uri, "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.browser;
browser._devicePermissionURIs = browser._devicePermissionURIs || [];
browser._devicePermissionURIs.push(uri);
let mm = browser.messageManager;
mm.sendAsyncMessage("webrtc:Allow", {callID: aRequest.callID,
windowID: aRequest.windowID,
devices: allowedDevices});
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 explicitly.
menupopup.parentNode.removeAttribute("value");
for (let device of devices)
addDeviceToList(menupopup, device.name, device.deviceIndex);
}
function listScreenShareDevices(menupopup, devices) {
while (menupopup.lastChild)
menupopup.removeChild(menupopup.lastChild);
let type = devices[0].mediaSource;
let typeName = type.charAt(0).toUpperCase() + type.substr(1);
let label = doc.getElementById("webRTC-selectWindow-label");
let gumStringId = "getUserMedia.select" + typeName;
label.setAttribute("value",
stringBundle.getString(gumStringId + ".label"));
label.setAttribute("accesskey",
stringBundle.getString(gumStringId + ".accesskey"));
// "No <type>" is the default because we can't pick a
// 'default' window to share.
addDeviceToList(menupopup,
stringBundle.getString("getUserMedia.no" + typeName + ".label"),
"-1");
menupopup.appendChild(doc.createElement("menuseparator"));
// Build the list of 'devices'.
let monitorIndex = 1;
for (let i = 0; i < devices.length; ++i) {
let device = devices[i];
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:
// <window count>\x1e<application name>
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, typeName);
item.deviceId = device.id;
if (device.scary)
item.scary = true;
}
// Always re-select the "No <type>" item.
doc.getElementById("webRTC-selectWindow-menulist").removeAttribute("value");
doc.getElementById("webRTC-all-windows-shared").hidden = true;
menupopup._commandEventListener = event => {
let video = doc.getElementById("webRTC-previewVideo");
if (video.stream) {
video.stream.getTracks().forEach(t => t.stop());
video.stream = null;
}
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 =
"<label class='text-link' href='" + baseURL + "screenshare-safety'>" +
learnMoreText + "</label>";
if (type == "screen") {
string = bundle.getFormattedString("getUserMedia.shareScreenWarning.message",
[learnMore]);
} else {
let brand =
doc.getElementById("bundle_brand").getString("brandShortName");
string = bundle.getFormattedString("getUserMedia.shareFirefoxWarning.message",
[brand, learnMore]);
}
warning.innerHTML = string;
}
let perms = Services.perms;
let chromeUri = Services.io.newURI(doc.documentURI);
perms.add(chromeUri, "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.src = chromeWin.URL.createObjectURL(stream);
video.stream = stream;
doc.getElementById("webRTC-preview").hidden = false;
video.onloadedmetadata = function(e) {
video.play();
};
});
};
menupopup.addEventListener("command", menupopup._commandEventListener);
}
function addDeviceToList(menupopup, deviceName, deviceIndex, type) {
let menuitem = doc.createElement("menuitem");
menuitem.setAttribute("value", deviceIndex);
menuitem.setAttribute("label", deviceName);
menuitem.setAttribute("tooltiptext", deviceName);
if (type)
menuitem.setAttribute("devicetype", type);
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);
else
listDevices(camMenupopup, videoDevices);
if (!sharingAudio)
listDevices(micMenupopup, audioDevices);
this.mainAction.callback = 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.add(uri, "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.set(uri, "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.set(uri, "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._devicePermissionURIs = aBrowser._devicePermissionURIs || [];
aBrowser._devicePermissionURIs.push(uri);
}
let mm = notification.browser.messageManager;
mm.sendAsyncMessage("webrtc:Allow", {callID: aRequest.callID,
windowID: aRequest.windowID,
devices: allowedDevices});
};
return false;
}
};
// Don't offer "always remember" action in PB mode.
if (!PrivateBrowsingUtils.isBrowserPrivate(aBrowser)) {
// 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"),
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";
notification =
chromeDoc.defaultView
.PopupNotifications
.show(aBrowser, "webRTC-shareDevices", message,
anchorId, mainAction, secondaryActions,
options);
notification.callID = aRequest.callID;
}
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 = "chrome://browser/content/webrtcIndicator.xul";
const features = "chrome,dialog=yes,titlebar=no,popup=yes";
return Services.ww.openWindow(null, INDICATOR_CHROME_URI, "_blank", features, []);
}
let indicator = {
_camera: null,
_microphone: null,
_screen: null,
_hiddenDoc: Cc["@mozilla.org/appshell/appShellService;1"]
.getService(Ci.nsIAppShellService)
.hiddenDOMWindow.document,
_statusBar: Cc["@mozilla.org/widget/macsystemstatusbar;1"]
.getService(Ci.nsISystemStatusBar),
_command(aEvent) {
webrtcUI.showSharingDoorhanger(aEvent.target.stream);
},
_popupShowing(aEvent) {
let type = this.getAttribute("type");
let activeStreams;
if (type == "Camera") {
activeStreams = webrtcUI.getActiveStreams(true, false, false);
} else if (type == "Microphone") {
activeStreams = webrtcUI.getActiveStreams(false, true, false);
} else if (type == "Screen") {
activeStreams = webrtcUI.getActiveStreams(false, false, true);
type = webrtcUI.showScreenSharingIndicator;
}
let bundle =
Services.strings.createBundle("chrome://browser/locale/webrtcIndicator.properties");
if (activeStreams.length == 1) {
let stream = activeStreams[0];
let menuitem = this.ownerDocument.createElement("menuitem");
let labelId = "webrtcIndicator.sharing" + type + "With.menuitem";
let label = stream.browser.contentTitle || stream.uri;
menuitem.setAttribute("label", bundle.formatStringFromName(labelId, [label], 1));
menuitem.setAttribute("disabled", "true");
this.appendChild(menuitem);
menuitem = this.ownerDocument.createElement("menuitem");
menuitem.setAttribute("label",
bundle.GetStringFromName("webrtcIndicator.controlSharing.menuitem"));
menuitem.stream = stream;
menuitem.addEventListener("command", indicator._command);
this.appendChild(menuitem);
return true;
}
// We show a different menu when there are several active streams.
let menuitem = this.ownerDocument.createElement("menuitem");
let labelId = "webrtcIndicator.sharing" + type + "WithNTabs.menuitem";
let count = activeStreams.length;
let label = PluralForm.get(count, bundle.GetStringFromName(labelId)).replace("#1", count);
menuitem.setAttribute("label", label);
menuitem.setAttribute("disabled", "true");
this.appendChild(menuitem);
for (let stream of activeStreams) {
let item = this.ownerDocument.createElement("menuitem");
labelId = "webrtcIndicator.controlSharingOn.menuitem";
label = stream.browser.contentTitle || stream.uri;
item.setAttribute("label", bundle.formatStringFromName(labelId, [label], 1));
item.stream = stream;
item.addEventListener("command", indicator._command);
this.appendChild(item);
}
return true;
},
_popupHiding(aEvent) {
while (this.firstChild)
this.firstChild.remove();
},
_setIndicatorState(aName, aState) {
let field = "_" + aName.toLowerCase();
if (aState && !this[field]) {
let menu = this._hiddenDoc.createElement("menu");
menu.setAttribute("id", "webRTC-sharing" + aName + "-menu");
// The CSS will only be applied if the menu is actually inserted in the DOM.
this._hiddenDoc.documentElement.appendChild(menu);
this._statusBar.addItem(menu);
let menupopup = this._hiddenDoc.createElement("menupopup");
menupopup.setAttribute("type", aName);
menupopup.addEventListener("popupshowing", this._popupShowing);
menupopup.addEventListener("popuphiding", this._popupHiding);
menupopup.addEventListener("command", this._command);
menu.appendChild(menupopup);
this[field] = menu;
} else if (this[field] && !aState) {
this._statusBar.removeItem(this[field]);
this[field].remove();
this[field] = null
}
},
updateIndicatorState() {
this._setIndicatorState("Camera", webrtcUI.showCameraIndicator);
this._setIndicatorState("Microphone", webrtcUI.showMicrophoneIndicator);
this._setIndicatorState("Screen", webrtcUI.showScreenSharingIndicator);
},
close() {
this._setIndicatorState("Camera", false);
this._setIndicatorState("Microphone", false);
this._setIndicatorState("Screen", false);
}
};
indicator.updateIndicatorState();
return indicator;
}
function onTabSharingMenuPopupShowing(e) {
let streams = webrtcUI.getActiveStreams(true, true, true);
for (let streamInfo of streams) {
let stringName = "getUserMedia.sharingMenu";
let types = streamInfo.types;
if (types.camera)
stringName += "Camera";
if (types.microphone)
stringName += "Microphone";
if (types.screen)
stringName += types.screen;
let doc = e.target.ownerDocument;
let bundle = doc.defaultView.gNavigatorBundle;
let origin = getHost(null, streamInfo.uri);
let menuitem = doc.createElement("menuitem");
menuitem.setAttribute("label", bundle.getFormattedString(stringName, [origin]));
menuitem.stream = streamInfo;
menuitem.addEventListener("command", onTabSharingMenuPopupCommand);
e.target.appendChild(menuitem);
}
}
function onTabSharingMenuPopupHiding(e) {
while (this.lastChild)
this.lastChild.remove();
}
function onTabSharingMenuPopupCommand(e) {
webrtcUI.showSharingDoorhanger(e.target.stream);
}
function showOrCreateMenuForWindow(aWindow) {
let document = aWindow.document;
let menu = document.getElementById("tabSharingMenu");
if (!menu) {
let stringBundle = aWindow.gNavigatorBundle;
menu = document.createElement("menu");
menu.id = "tabSharingMenu";
let labelStringId = "getUserMedia.sharingMenu.label";
menu.setAttribute("label", stringBundle.getString(labelStringId));
let container, insertionPoint;
if (AppConstants.platform == "macosx") {
container = document.getElementById("windowPopup");
insertionPoint = document.getElementById("sep-window-list");
let separator = document.createElement("menuseparator");
separator.id = "tabSharingSeparator";
container.insertBefore(separator, insertionPoint);
} else {
let accesskeyStringId = "getUserMedia.sharingMenu.accesskey";
menu.setAttribute("accesskey", stringBundle.getString(accesskeyStringId));
container = document.getElementById("main-menubar");
insertionPoint = document.getElementById("helpMenu");
}
let popup = document.createElement("menupopup");
popup.id = "tabSharingMenuPopup";
popup.addEventListener("popupshowing", onTabSharingMenuPopupShowing);
popup.addEventListener("popuphiding", onTabSharingMenuPopupHiding);
menu.appendChild(popup);
container.insertBefore(menu, insertionPoint);
} else {
menu.hidden = false;
if (AppConstants.platform == "macosx") {
document.getElementById("tabSharingSeparator").hidden = false;
}
}
}
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;
}
let browserWindowEnum = Services.wm.getEnumerator("navigator:browser");
while (browserWindowEnum.hasMoreElements()) {
let chromeWin = browserWindowEnum.getNext();
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
gIndicatorWindow.updateIndicatorState();
} else if (gIndicatorWindow) {
gIndicatorWindow.close();
gIndicatorWindow = null;
}
}