Bug 804611 - Add a way to grant/deny getUserMedia permissions persistently, r=jesup,dolske, ui-r=Boriss.

This commit is contained in:
Florian Quèze 2014-02-25 12:50:42 +01:00
parent 83fe437fc4
commit 4f48223aa5
16 changed files with 987 additions and 33 deletions

View File

@ -57,6 +57,7 @@ support-files =
file_bug970276_favicon2.ico
file_dom_notifications.html
file_fullscreen-window-open.html
get_user_media.html
head.js
healthreport_testRemoteCommands.html
moz.png
@ -262,6 +263,7 @@ skip-if = true # browser_drag.js is disabled, as it needs to be updated for the
[browser_findbarClose.js]
[browser_fullscreen-window-open.js]
[browser_gestureSupport.js]
[browser_get_user_media.js]
[browser_getshortcutoruri.js]
[browser_hide_removing.js]
[browser_homeDrop.js]

View File

@ -0,0 +1,742 @@
const kObservedTopics = [
"getUserMedia:response:allow",
"getUserMedia:revoke",
"getUserMedia:response:deny",
"getUserMedia:request",
"recording-device-events",
"recording-window-ended"
];
const PREF_PERMISSION_FAKE = "media.navigator.permission.fake";
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService",
"@mozilla.org/mediaManagerService;1",
"nsIMediaManagerService");
var gObservedTopics = {};
function observer(aSubject, aTopic, aData) {
if (!(aTopic in gObservedTopics))
gObservedTopics[aTopic] = 1;
else
++gObservedTopics[aTopic];
}
function promiseNotification(aTopic, aAction) {
let deferred = Promise.defer();
Services.obs.addObserver(function observer() {
ok(true, "got " + aTopic + " notification");
Services.obs.removeObserver(observer, aTopic);
if (kObservedTopics.indexOf(aTopic) != -1) {
if (!(aTopic in gObservedTopics))
gObservedTopics[aTopic] = -1;
else
--gObservedTopics[aTopic];
}
deferred.resolve();
}, aTopic, false);
if (aAction)
aAction();
return deferred.promise;
}
function expectNotification(aTopic) {
is(gObservedTopics[aTopic], 1, "expected notification " + aTopic);
if (aTopic in gObservedTopics)
--gObservedTopics[aTopic];
}
function expectNoNotifications() {
for (let topic in gObservedTopics) {
if (gObservedTopics[topic])
is(gObservedTopics[topic], 0, topic + " notification unexpected");
}
gObservedTopics = {}
}
function promiseMessage(aMessage, aAction) {
let deferred = Promise.defer();
content.addEventListener("message", function messageListener(event) {
content.removeEventListener("message", messageListener);
is(event.data, aMessage, "received " + aMessage);
if (event.data == aMessage)
deferred.resolve();
else
deferred.reject();
});
if (aAction)
aAction();
return deferred.promise;
}
function promisePopupNotification(aName) {
let deferred = Promise.defer();
waitForCondition(() => PopupNotifications.getNotification(aName),
() => {
ok(!!PopupNotifications.getNotification(aName),
aName + " notification appeared");
deferred.resolve();
}, "timeout waiting for popup notification " + aName);
return deferred.promise;
}
function promiseNoPopupNotification(aName) {
let deferred = Promise.defer();
waitForCondition(() => !PopupNotifications.getNotification(aName),
() => {
ok(!PopupNotifications.getNotification(aName),
aName + " notification removed");
deferred.resolve();
}, "timeout waiting for popup notification " + aName + " to disappear");
return deferred.promise;
}
const kActionAlways = 1;
const kActionDeny = 2;
const kActionNever = 3;
function activateSecondaryAction(aAction) {
let notification = PopupNotifications.panel.firstChild;
notification.button.focus();
let popup = notification.menupopup;
popup.addEventListener("popupshown", function () {
popup.removeEventListener("popupshown", arguments.callee, false);
// Press 'down' as many time as needed to select the requested action.
while (aAction--)
EventUtils.synthesizeKey("VK_DOWN", {});
// Activate
EventUtils.synthesizeKey("VK_RETURN", {});
}, false);
// One down event to open the popup
EventUtils.synthesizeKey("VK_DOWN",
{ altKey: !navigator.platform.contains("Mac") });
}
registerCleanupFunction(function() {
gBrowser.removeCurrentTab();
kObservedTopics.forEach(topic => {
Services.obs.removeObserver(observer, topic);
});
Services.prefs.clearUserPref(PREF_PERMISSION_FAKE);
});
function getMediaCaptureState() {
let hasVideo = {};
let hasAudio = {};
MediaManagerService.mediaCaptureWindowState(content, hasVideo, hasAudio);
if (hasVideo.value && hasAudio.value)
return "CameraAndMicrophone";
if (hasVideo.value)
return "Camera";
if (hasAudio.value)
return "Microphone";
return "none";
}
function closeStream(aAlreadyClosed) {
expectNoNotifications();
info("closing the stream");
content.wrappedJSObject.closeStream();
if (!aAlreadyClosed)
yield promiseNotification("recording-device-events");
yield promiseNoPopupNotification("webRTC-sharingDevices");
if (!aAlreadyClosed)
expectNotification("recording-window-ended");
let statusButton = document.getElementById("webrtc-status-button");
ok(statusButton.hidden, "WebRTC status button hidden");
}
function checkDeviceSelectors(aAudio, aVideo) {
let micSelector = document.getElementById("webRTC-selectMicrophone");
if (aAudio)
ok(!micSelector.hidden, "microphone selector visible");
else
ok(micSelector.hidden, "microphone selector hidden");
let cameraSelector = document.getElementById("webRTC-selectCamera");
if (aVideo)
ok(!cameraSelector.hidden, "camera selector visible");
else
ok(cameraSelector.hidden, "camera selector hidden");
}
function checkSharingUI() {
yield promisePopupNotification("webRTC-sharingDevices");
let statusButton = document.getElementById("webrtc-status-button");
ok(!statusButton.hidden, "WebRTC status button visible");
}
function checkNotSharing() {
is(getMediaCaptureState(), "none", "expected nothing to be shared");
ok(!PopupNotifications.getNotification("webRTC-sharingDevices"),
"no webRTC-sharingDevices popup notification");
let statusButton = document.getElementById("webrtc-status-button");
ok(statusButton.hidden, "WebRTC status button hidden");
}
let gTests = [
{
desc: "getUserMedia audio+video",
run: function checkAudioVideo() {
yield promiseNotification("getUserMedia:request", () => {
info("requesting devices");
content.wrappedJSObject.requestDevice(true, true);
});
yield promisePopupNotification("webRTC-shareDevices");
checkDeviceSelectors(true, true);
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
expectNotification("getUserMedia:response:allow");
expectNotification("recording-device-events");
is(getMediaCaptureState(), "CameraAndMicrophone",
"expected camera and microphone to be shared");
yield checkSharingUI();
yield closeStream();
}
},
{
desc: "getUserMedia audio only",
run: function checkAudioOnly() {
yield promiseNotification("getUserMedia:request", () => {
info("requesting devices");
content.wrappedJSObject.requestDevice(true);
});
yield promisePopupNotification("webRTC-shareDevices");
checkDeviceSelectors(true);
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
expectNotification("getUserMedia:response:allow");
expectNotification("recording-device-events");
is(getMediaCaptureState(), "Microphone", "expected microphone to be shared");
yield checkSharingUI();
yield closeStream();
}
},
{
desc: "getUserMedia video only",
run: function checkVideoOnly() {
yield promiseNotification("getUserMedia:request", () => {
info("requesting devices");
content.wrappedJSObject.requestDevice(false, true);
});
yield promisePopupNotification("webRTC-shareDevices");
checkDeviceSelectors(false, true);
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
expectNotification("getUserMedia:response:allow");
expectNotification("recording-device-events");
is(getMediaCaptureState(), "Camera", "expected camera to be shared");
yield checkSharingUI();
yield closeStream();
}
},
{
desc: "getUserMedia audio+video, user disables video",
run: function checkDisableVideo() {
yield promiseNotification("getUserMedia:request", () => {
info("requesting devices");
content.wrappedJSObject.requestDevice(true, true);
});
yield promisePopupNotification("webRTC-shareDevices");
checkDeviceSelectors(true, true);
// disable the camera
document.getElementById("webRTC-selectCamera-menulist").value = -1;
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
// reset the menuitem to have no impact on the following tests.
document.getElementById("webRTC-selectCamera-menulist").value = 0;
expectNotification("getUserMedia:response:allow");
expectNotification("recording-device-events");
is(getMediaCaptureState(), "Microphone",
"expected microphone to be shared");
yield checkSharingUI();
yield closeStream();
}
},
{
desc: "getUserMedia audio+video, user disables audio",
run: function checkDisableAudio() {
yield promiseNotification("getUserMedia:request", () => {
info("requesting devices");
content.wrappedJSObject.requestDevice(true, true);
});
yield promisePopupNotification("webRTC-shareDevices");
checkDeviceSelectors(true, true);
// disable the microphone
document.getElementById("webRTC-selectMicrophone-menulist").value = -1;
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
// reset the menuitem to have no impact on the following tests.
document.getElementById("webRTC-selectMicrophone-menulist").value = 0;
expectNotification("getUserMedia:response:allow");
expectNotification("recording-device-events");
is(getMediaCaptureState(), "Camera",
"expected microphone to be shared");
yield checkSharingUI();
yield closeStream();
}
},
{
desc: "getUserMedia audio+video, user disables both audio and video",
run: function checkDisableAudioVideo() {
yield promiseNotification("getUserMedia:request", () => {
info("requesting devices");
content.wrappedJSObject.requestDevice(true, true);
});
yield promisePopupNotification("webRTC-shareDevices");
checkDeviceSelectors(true, true);
// disable the camera and microphone
document.getElementById("webRTC-selectCamera-menulist").value = -1;
document.getElementById("webRTC-selectMicrophone-menulist").value = -1;
yield promiseMessage("error: PERMISSION_DENIED", () => {
PopupNotifications.panel.firstChild.button.click();
});
// reset the menuitems to have no impact on the following tests.
document.getElementById("webRTC-selectCamera-menulist").value = 0;
document.getElementById("webRTC-selectMicrophone-menulist").value = 0;
expectNotification("getUserMedia:response:deny");
expectNotification("recording-window-ended");
checkNotSharing();
}
},
{
desc: "getUserMedia audio+video, user clicks \"Don't Share\"",
run: function checkDontShare() {
yield promiseNotification("getUserMedia:request", () => {
info("requesting devices");
content.wrappedJSObject.requestDevice(true, true);
});
yield promisePopupNotification("webRTC-shareDevices");
checkDeviceSelectors(true, true);
yield promiseMessage("error: PERMISSION_DENIED", () => {
activateSecondaryAction(kActionDeny);
});
expectNotification("getUserMedia:response:deny");
expectNotification("recording-window-ended");
checkNotSharing();
}
},
{
desc: "getUserMedia audio+video: stop sharing",
run: function checkStopSharing() {
yield promiseNotification("getUserMedia:request", () => {
info("requesting devices");
content.wrappedJSObject.requestDevice(true, true);
});
yield promisePopupNotification("webRTC-shareDevices");
checkDeviceSelectors(true, true);
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
expectNotification("getUserMedia:response:allow");
expectNotification("recording-device-events");
is(getMediaCaptureState(), "CameraAndMicrophone",
"expected camera and microphone to be shared");
yield checkSharingUI();
PopupNotifications.getNotification("webRTC-sharingDevices").reshow();
activateSecondaryAction(kActionDeny);
yield promiseNotification("recording-device-events");
expectNotification("getUserMedia:revoke");
yield promiseNoPopupNotification("webRTC-sharingDevices");
if (gObservedTopics["recording-device-events"] == 1) {
todo(false, "Got the 'recording-device-events' notification twice, likely because of bug 962719");
gObservedTopics["recording-device-events"] = 0;
}
expectNoNotifications();
checkNotSharing();
// the stream is already closed, but this will do some cleanup anyway
yield closeStream(true);
}
},
{
desc: "getUserMedia prompt: Always/Never Share",
run: function checkRememberCheckbox() {
function checkPerm(aRequestAudio, aRequestVideo, aAllowAudio, aAllowVideo,
aExpectedAudioPerm, aExpectedVideoPerm, aNever) {
yield promiseNotification("getUserMedia:request", () => {
content.wrappedJSObject.requestDevice(aRequestAudio, aRequestVideo);
});
yield promisePopupNotification("webRTC-shareDevices");
let elt = id => document.getElementById(id);
let noAudio = aAllowAudio === undefined;
is(elt("webRTC-selectMicrophone").hidden, noAudio,
"microphone selector expected to be " + (noAudio ? "hidden" : "visible"));
if (!noAudio)
elt("webRTC-selectMicrophone-menulist").value = (aAllowAudio || aNever) ? 0 : -1;
let noVideo = aAllowVideo === undefined;
is(elt("webRTC-selectCamera").hidden, noVideo,
"camera selector expected to be " + (noVideo ? "hidden" : "visible"));
if (!noVideo)
elt("webRTC-selectCamera-menulist").value = (aAllowVideo || aNever) ? 0 : -1;
let expectedMessage =
(aAllowVideo || aAllowAudio) ? "ok" : "error: PERMISSION_DENIED";
yield promiseMessage(expectedMessage, () => {
activateSecondaryAction(aNever ? kActionNever : kActionAlways);
});
let expected = [];
if (expectedMessage == "ok") {
expectNotification("getUserMedia:response:allow");
expectNotification("recording-device-events");
if (aAllowVideo)
expected.push("Camera");
if (aAllowAudio)
expected.push("Microphone");
expected = expected.join("And");
}
else {
expectNotification("getUserMedia:response:deny");
expectNotification("recording-window-ended");
expected = "none";
}
is(getMediaCaptureState(), expected,
"expected " + expected + " to be shared");
function checkDevicePermissions(aDevice, aExpected) {
let Perms = Services.perms;
let uri = content.document.documentURIObject;
let devicePerms = Perms.testExactPermission(uri, aDevice);
if (aExpected === undefined)
is(devicePerms, Perms.UNKNOWN_ACTION, "no " + aDevice + " persistent permissions");
else {
is(devicePerms, aExpected ? Perms.ALLOW_ACTION : Perms.DENY_ACTION,
aDevice + " persistently " + (aExpected ? "allowed" : "denied"));
}
Perms.remove(uri.host, aDevice);
}
checkDevicePermissions("microphone", aExpectedAudioPerm);
checkDevicePermissions("camera", aExpectedVideoPerm);
if (expectedMessage == "ok")
yield closeStream();
}
// 3 cases where the user accepts the device prompt.
info("audio+video, user grants, expect both perms set to allow");
yield checkPerm(true, true, true, true, true, true);
info("audio only, user grants, check audio perm set to allow, video perm not set");
yield checkPerm(true, false, true, undefined, true, undefined);
info("video only, user grants, check video perm set to allow, audio perm not set");
yield checkPerm(false, true, undefined, true, undefined, true);
// 3 cases where the user rejects the device request.
// First test these cases by setting the device to 'No Audio'/'No Video'
info("audio+video, user denies, expect both perms set to deny");
yield checkPerm(true, true, false, false, false, false);
info("audio only, user denies, expect audio perm set to deny, video not set");
yield checkPerm(true, false, false, undefined, false, undefined);
info("video only, user denies, expect video perm set to deny, audio perm not set");
yield checkPerm(false, true, undefined, false, undefined, false);
// Now test these 3 cases again by using the 'Never Share' action.
info("audio+video, user denies, expect both perms set to deny");
yield checkPerm(true, true, false, false, false, false, true);
info("audio only, user denies, expect audio perm set to deny, video not set");
yield checkPerm(true, false, false, undefined, false, undefined, true);
info("video only, user denies, expect video perm set to deny, audio perm not set");
yield checkPerm(false, true, undefined, false, undefined, false, true);
// 2 cases where the user allows half of what's requested.
info("audio+video, user denies video, grants audio, " +
"expect video perm set to deny, audio perm set to allow.");
yield checkPerm(true, true, true, false, true, false);
info("audio+video, user denies audio, grants video, " +
"expect video perm set to allow, audio perm set to deny.");
yield checkPerm(true, true, false, true, false, true);
}
},
{
desc: "getUserMedia without prompt: use persistent permissions",
run: function checkUsePersistentPermissions() {
function usePerm(aAllowAudio, aAllowVideo, aRequestAudio, aRequestVideo,
aExpectStream) {
let Perms = Services.perms;
let uri = content.document.documentURIObject;
if (aAllowAudio !== undefined) {
Perms.add(uri, "microphone", aAllowAudio ? Perms.ALLOW_ACTION
: Perms.DENY_ACTION);
}
if (aAllowVideo !== undefined) {
Perms.add(uri, "camera", aAllowVideo ? Perms.ALLOW_ACTION
: Perms.DENY_ACTION);
}
let gum = function() {
content.wrappedJSObject.requestDevice(aRequestAudio, aRequestVideo);
};
if (aExpectStream === undefined) {
// Check that we get a prompt.
yield promiseNotification("getUserMedia:request", gum);
yield promisePopupNotification("webRTC-shareDevices");
// Deny the request to cleanup...
yield promiseMessage("error: PERMISSION_DENIED", () => {
activateSecondaryAction(kActionDeny);
});
expectNotification("getUserMedia:response:deny");
expectNotification("recording-window-ended");
}
else {
let allow = (aAllowVideo && aRequestVideo) || (aAllowAudio && aRequestAudio);
let expectedMessage = allow ? "ok" : "error: PERMISSION_DENIED";
yield promiseMessage(expectedMessage, gum);
if (expectedMessage == "ok") {
expectNotification("recording-device-events");
// Check what's actually shared.
let expected = [];
if (aAllowVideo && aRequestVideo)
expected.push("Camera");
if (aAllowAudio && aRequestAudio)
expected.push("Microphone");
expected = expected.join("And");
is(getMediaCaptureState(), expected,
"expected " + expected + " to be shared");
yield closeStream();
}
else {
expectNotification("recording-window-ended");
}
}
Perms.remove(uri.host, "camera");
Perms.remove(uri.host, "microphone");
}
// Set both permissions identically
info("allow audio+video, request audio+video, expect ok (audio+video)");
yield usePerm(true, true, true, true, true);
info("deny audio+video, request audio+video, expect denied");
yield usePerm(false, false, true, true, false);
// Allow audio, deny video.
info("allow audio, deny video, request audio+video, expect ok (audio)");
yield usePerm(true, false, true, true, true);
info("allow audio, deny video, request audio, expect ok (audio)");
yield usePerm(true, false, true, false, true);
info("allow audio, deny video, request video, expect denied");
yield usePerm(true, false, false, true, false);
// Deny audio, allow video.
info("deny audio, allow video, request audio+video, expect ok (video)");
yield usePerm(false, true, true, true, true);
info("deny audio, allow video, request audio, expect denied");
yield usePerm(false, true, true, false, true);
info("deny audio, allow video, request video, expect ok (video)");
yield usePerm(false, true, false, true, false);
// Allow audio, video not set.
info("allow audio, request audio+video, expect prompt");
yield usePerm(true, undefined, true, true, undefined);
info("allow audio, request audio, expect ok (audio)");
yield usePerm(true, undefined, true, false, true);
info("allow audio, request video, expect prompt");
yield usePerm(true, undefined, false, true, undefined);
// Deny audio, video not set.
info("deny audio, request audio+video, expect prompt");
yield usePerm(false, undefined, true, true, undefined);
info("deny audio, request audio, expect denied");
yield usePerm(false, undefined, true, false, false);
info("deny audio, request video, expect prompt");
yield usePerm(false, undefined, false, true, undefined);
// Allow video, video not set.
info("allow video, request audio+video, expect prompt");
yield usePerm(undefined, true, true, true, undefined);
info("allow video, request audio, expect prompt");
yield usePerm(undefined, true, true, false, undefined);
info("allow video, request video, expect ok (video)");
yield usePerm(undefined, true, false, true, true);
// Deny video, video not set.
info("deny video, request audio+video, expect prompt");
yield usePerm(undefined, false, true, true, undefined);
info("deny video, request audio, expect prompt");
yield usePerm(undefined, false, true, false, undefined);
info("deny video, request video, expect denied");
yield usePerm(undefined, false, false, true, false);
}
},
{
desc: "Stop Sharing removes persistent permissions",
run: function checkStopSharingRemovesPersistentPermissions() {
function stopAndCheckPerm(aRequestAudio, aRequestVideo) {
let Perms = Services.perms;
let uri = content.document.documentURIObject;
// Initially set both permissions to 'allow'.
Perms.add(uri, "microphone", Perms.ALLOW_ACTION);
Perms.add(uri, "camera", Perms.ALLOW_ACTION);
// Start sharing what's been requested.
yield promiseMessage("ok", () => {
content.wrappedJSObject.requestDevice(aRequestAudio, aRequestVideo);
});
expectNotification("recording-device-events");
yield checkSharingUI();
// Stop sharing.
PopupNotifications.getNotification("webRTC-sharingDevices").reshow();
activateSecondaryAction(kActionDeny);
yield promiseNotification("recording-device-events");
expectNotification("getUserMedia:revoke");
yield promiseNoPopupNotification("webRTC-sharingDevices");
if (gObservedTopics["recording-device-events"] == 1) {
todo(false, "Got the 'recording-device-events' notification twice, likely because of bug 962719");
gObservedTopics["recording-device-events"] = 0;
}
// Check that permissions have been removed as expected.
let audioPerm = Perms.testExactPermission(uri, "microphone");
if (aRequestAudio)
is(audioPerm, Perms.UNKNOWN_ACTION, "microphone permissions removed");
else
is(audioPerm, Perms.ALLOW_ACTION, "microphone permissions untouched");
let videoPerm = Perms.testExactPermission(uri, "camera");
if (aRequestVideo)
is(videoPerm, Perms.UNKNOWN_ACTION, "camera permissions removed");
else
is(videoPerm, Perms.ALLOW_ACTION, "camera permissions untouched");
// Cleanup.
yield closeStream(true);
Perms.remove(uri.host, "camera");
Perms.remove(uri.host, "microphone");
}
info("request audio+video, stop sharing resets both");
yield stopAndCheckPerm(true, true);
info("request audio, stop sharing resets audio only");
yield stopAndCheckPerm(true, false);
info("request video, stop sharing resets video only");
yield stopAndCheckPerm(false, true);
}
}
];
function test() {
waitForExplicitFinish();
let tab = gBrowser.addTab();
gBrowser.selectedTab = tab;
tab.linkedBrowser.addEventListener("load", function onload() {
tab.linkedBrowser.removeEventListener("load", onload, true);
kObservedTopics.forEach(topic => {
Services.obs.addObserver(observer, topic, false);
});
Services.prefs.setBoolPref(PREF_PERMISSION_FAKE, true);
Task.spawn(function () {
for (let test of gTests) {
info(test.desc);
yield test.run();
// Cleanup before the next test
expectNoNotifications();
}
}).then(finish, ex => {
ok(false, "Unexpected Exception: " + ex);
finish();
});
}, true);
let rootDir = getRootDirectory(gTestPath)
rootDir = rootDir.replace("chrome://mochitests/content/",
"http://127.0.0.1:8888/");
content.location = rootDir + "get_user_media.html";
}
function wait(time) {
let deferred = Promise.defer();
setTimeout(deferred.resolve, time);
return deferred.promise;
}

View File

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"></head>
<body>
<div id="message"></div>
<script>
function message(m) {
document.getElementById("message").innerHTML = m;
window.parent.postMessage(m, "*");
}
var gStream;
function requestDevice(aAudio, aVideo) {
window.navigator.mozGetUserMedia({video: aVideo, audio: aAudio, fake: true},
function(stream) {
gStream = stream;
message("ok");
}, function(err) { message("error: " + err); });
}
message("pending");
function closeStream() {
if (!gStream)
return;
gStream.stop();
gStream = null;
message("closed");
}
</script>
</body>
</html>

View File

@ -38,7 +38,7 @@ let gVisitStmt = gPlacesDatabase.createAsyncStatement(
* Permission types that should be tested with testExactPermission, as opposed
* to testPermission. This is based on what consumers use to test these permissions.
*/
let TEST_EXACT_PERM_TYPES = ["geo"];
let TEST_EXACT_PERM_TYPES = ["geo", "camera", "microphone"];
/**
* Site object represents a single site, uniquely identified by a host.
@ -330,8 +330,11 @@ let PermissionDefaults = {
set fullscreen(aValue) {
let value = (aValue != this.DENY);
Services.prefs.setBoolPref("full-screen-api.enabled", value);
}
}
},
get camera() this.UNKNOWN,
get microphone() this.UNKNOWN
};
/**
* AboutPermissions manages the about:permissions page.
@ -339,7 +342,7 @@ let PermissionDefaults = {
let AboutPermissions = {
/**
* Number of sites to return from the places database.
*/
*/
PLACES_SITES_LIMIT: 50,
/**
@ -369,17 +372,18 @@ let AboutPermissions = {
*
* Potential future additions: "sts/use", "sts/subd"
*/
_supportedPermissions: ["password", "cookie", "geo", "indexedDB", "popup", "fullscreen"],
_supportedPermissions: ["password", "cookie", "geo", "indexedDB", "popup",
"fullscreen", "camera", "microphone"],
/**
* Permissions that don't have a global "Allow" option.
*/
_noGlobalAllow: ["geo", "indexedDB", "fullscreen"],
_noGlobalAllow: ["geo", "indexedDB", "fullscreen", "camera", "microphone"],
/**
* Permissions that don't have a global "Deny" option.
*/
_noGlobalDeny: [],
_noGlobalDeny: ["camera", "microphone"],
_stringBundle: Services.strings.
createBundle("chrome://browser/locale/preferences/aboutPermissions.properties"),
@ -407,7 +411,7 @@ let AboutPermissions = {
Services.obs.addObserver(this, "passwordmgr-storage-changed", false);
Services.obs.addObserver(this, "cookie-changed", false);
Services.obs.addObserver(this, "browser:purge-domain-data", false);
this._observersInitialized = true;
Services.obs.notifyObservers(null, "browser-permissions-preinit", null);
},
@ -542,7 +546,7 @@ let AboutPermissions = {
let uri = NetUtil.newURI(aLogin.hostname);
this.addHost(uri.host);
} catch (e) {
// newURI will throw for add-ons logins stored in chrome:// URIs
// newURI will throw for add-ons logins stored in chrome:// URIs
}
itemCnt++;
}, this);
@ -557,7 +561,7 @@ let AboutPermissions = {
let uri = NetUtil.newURI(aHostname);
this.addHost(uri.host);
} catch (e) {
// newURI will throw for add-ons logins stored in chrome:// URIs
// newURI will throw for add-ons logins stored in chrome:// URIs
}
itemCnt++;
}, this);
@ -778,7 +782,7 @@ let AboutPermissions = {
let visitLabel = PluralForm.get(aCount, visitForm)
.replace("#1", aCount);
document.getElementById("site-visit-count").value = visitLabel;
});
});
},
updatePasswordsCount: function() {

View File

@ -113,6 +113,48 @@
</vbox>
</hbox>
<!-- Camera -->
<hbox id="camera-pref-item"
class="pref-item" align="top">
<image class="pref-icon" type="camera"/>
<vbox>
<label class="pref-title" value="&camera.label;"/>
<hbox align="center">
<menulist id="camera-menulist"
class="pref-menulist"
type="camera"
oncommand="AboutPermissions.onPermissionCommand(event);">
<menupopup>
<menuitem id="camera-0" value="0" label="&permission.alwaysAsk;"/>
<menuitem id="camera-1" value="1" label="&permission.allow;"/>
<menuitem id="camera-2" value="2" label="&permission.block;"/>
</menupopup>
</menulist>
</hbox>
</vbox>
</hbox>
<!-- Microphone -->
<hbox id="microphone-pref-item"
class="pref-item" align="top">
<image class="pref-icon" type="microphone"/>
<vbox>
<label class="pref-title" value="&microphone.label;"/>
<hbox align="center">
<menulist id="microphone-menulist"
class="pref-menulist"
type="microphone"
oncommand="AboutPermissions.onPermissionCommand(event);">
<menupopup>
<menuitem id="microphone-0" value="0" label="&permission.alwaysAsk;"/>
<menuitem id="microphone-1" value="1" label="&permission.allow;"/>
<menuitem id="microphone-2" value="2" label="&permission.block;"/>
</menupopup>
</menulist>
</hbox>
</vbox>
</hbox>
<!-- Cookies -->
<hbox id="cookie-pref-item"
class="pref-item" align="top">

View File

@ -27,6 +27,8 @@ const TEST_PERMS = {
"indexedDB": PERM_UNKNOWN,
"popup": PERM_DENY,
"fullscreen" : PERM_UNKNOWN,
"camera": PERM_UNKNOWN,
"microphone": PERM_UNKNOWN
};
const NO_GLOBAL_ALLOW = [
@ -36,7 +38,7 @@ const NO_GLOBAL_ALLOW = [
];
// number of managed permissions in the interface
const TEST_PERMS_COUNT = 6;
const TEST_PERMS_COUNT = 8;
function test() {
waitForExplicitFinish();
@ -164,7 +166,7 @@ var tests = [
function test_all_sites_permission() {
// apply the old default of allowing all cookies
Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
// there should be no user-set pref for cookie behavior
is(Services.prefs.getIntPref("network.cookie.cookieBehavior"), PERM_UNKNOWN,
"network.cookie.cookieBehavior is expected default");
@ -189,12 +191,12 @@ var tests = [
// make sure "Manage All Passwords..." button opens the correct dialog
addWindowListener("chrome://passwordmgr/content/passwordManager.xul", runNextTest);
gBrowser.contentDocument.getElementById("passwords-manage-all-button").doCommand();
},
function test_manage_all_cookies() {
// make sure "Manage All Cookies..." button opens the correct dialog
addWindowListener("chrome://browser/content/preferences/cookies.xul", runNextTest);
addWindowListener("chrome://browser/content/preferences/cookies.xul", runNextTest);
gBrowser.contentDocument.getElementById("cookies-manage-all-button").doCommand();
},

View File

@ -497,8 +497,12 @@ getUserMedia.noVideo.label = No Video
getUserMedia.noAudio.label = No Audio
getUserMedia.shareSelectedDevices.label = Share Selected Device;Share Selected Devices
getUserMedia.shareSelectedDevices.accesskey = S
getUserMedia.always.label = Always Share
getUserMedia.always.accesskey = A
getUserMedia.denyRequest.label = Don't Share
getUserMedia.denyRequest.accesskey = D
getUserMedia.never.label = Never Share
getUserMedia.never.accesskey = N
getUserMedia.sharingCamera.message2 = You are currently sharing your camera with this page.
getUserMedia.sharingMicrophone.message2 = You are currently sharing your microphone with this page.
getUserMedia.sharingCameraAndMicrophone.message2 = You are currently sharing your camera and microphone with this page.

View File

@ -42,3 +42,5 @@
<!ENTITY popup.label "Open Pop-up Windows">
<!ENTITY fullscreen.label "Fullscreen">
<!ENTITY camera.label "Use the Camera">
<!ENTITY microphone.label "Use the Microphone">

View File

@ -10,6 +10,8 @@ alwaysAsk = Always Ask
permission.cookie.label = Set Cookies
permission.desktop-notification.label = Show Notifications
permission.image.label = Load Images
permission.camera.label = Use the Camera
permission.microphone.label = Use the Microphone
permission.install.label = Install Add-ons
permission.popup.label = Open Pop-up Windows
permission.geo.label = Access Your Location

View File

@ -186,6 +186,9 @@ let gPermissionObject = {
"desktop-notification": {},
"camera": {},
"microphone": {},
"popup": {
getDefault: function () {
return Services.prefs.getBoolPref("dom.disable_open_during_load") ?

View File

@ -120,13 +120,13 @@ function prompt(aContentWindow, aCallID, aAudioRequested, aVideoRequested, aDevi
return;
}
let host = aContentWindow.document.documentURIObject.host;
let uri = aContentWindow.document.documentURIObject;
let browser = getBrowserForWindow(aContentWindow);
let chromeDoc = browser.ownerDocument;
let chromeWin = chromeDoc.defaultView;
let stringBundle = chromeWin.gNavigatorBundle;
let message = stringBundle.getFormattedString("getUserMedia.share" + requestType + ".message",
[ host ]);
[ uri.host ]);
let mainAction = {
label: PluralForm.get(requestType == "CameraAndMicrophone" ? 2 : 1,
@ -138,13 +138,34 @@ function prompt(aContentWindow, aCallID, aAudioRequested, aVideoRequested, aDevi
callback: function() {}
};
let secondaryActions = [{
label: stringBundle.getString("getUserMedia.denyRequest.label"),
accessKey: stringBundle.getString("getUserMedia.denyRequest.accesskey"),
callback: function () {
denyRequest(aCallID);
let secondaryActions = [
{
label: stringBundle.getString("getUserMedia.always.label"),
accessKey: stringBundle.getString("getUserMedia.always.accesskey"),
callback: function () {
mainAction.callback(true);
}
},
{
label: stringBundle.getString("getUserMedia.denyRequest.label"),
accessKey: stringBundle.getString("getUserMedia.denyRequest.accesskey"),
callback: function () {
denyRequest(aCallID);
}
},
{
label: stringBundle.getString("getUserMedia.never.label"),
accessKey: stringBundle.getString("getUserMedia.never.accesskey"),
callback: function () {
denyRequest(aCallID);
let perms = Services.perms;
if (audioDevices.length)
perms.add(uri, "microphone", perms.DENY_ACTION);
if (videoDevices.length)
perms.add(uri, "camera", perms.DENY_ACTION);
}
}
}];
];
let options = {
eventCallback: function(aTopic, aNewBrowser) {
@ -188,18 +209,29 @@ function prompt(aContentWindow, aCallID, aAudioRequested, aVideoRequested, aDevi
addDeviceToList(micMenupopup, stringBundle.getString("getUserMedia.noAudio.label"), "-1");
}
this.mainAction.callback = function() {
this.mainAction.callback = function(aRemember) {
let allowedDevices = Cc["@mozilla.org/supports-array;1"]
.createInstance(Ci.nsISupportsArray);
let perms = Services.perms;
if (videoDevices.length) {
let videoDeviceIndex = chromeDoc.getElementById("webRTC-selectCamera-menulist").value;
if (videoDeviceIndex != "-1")
let allowCamera = videoDeviceIndex != "-1";
if (allowCamera)
allowedDevices.AppendElement(videoDevices[videoDeviceIndex]);
if (aRemember) {
perms.add(uri, "camera",
allowCamera ? perms.ALLOW_ACTION : perms.DENY_ACTION);
}
}
if (audioDevices.length) {
let audioDeviceIndex = chromeDoc.getElementById("webRTC-selectMicrophone-menulist").value;
if (audioDeviceIndex != "-1")
let allowMic = audioDeviceIndex != "-1";
if (allowMic)
allowedDevices.AppendElement(audioDevices[audioDeviceIndex]);
if (aRemember) {
perms.add(uri, "microphone",
allowMic ? perms.ALLOW_ACTION : perms.DENY_ACTION);
}
}
if (allowedDevices.Count() == 0) {
@ -252,6 +284,7 @@ function showBrowserSpecificIndicator(aBrowser) {
let message = stringBundle.getString("getUserMedia.sharing" + captureState + ".message2");
let uri = aBrowser.contentWindow.document.documentURIObject;
let windowId = aBrowser.contentWindow
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
@ -266,6 +299,14 @@ function showBrowserSpecificIndicator(aBrowser) {
label: stringBundle.getString("getUserMedia.stopSharing.label"),
accessKey: stringBundle.getString("getUserMedia.stopSharing.accesskey"),
callback: function () {
let perms = Services.perms;
if (hasVideo.value &&
perms.testExactPermission(uri, "camera") == perms.ALLOW_ACTION)
perms.remove(uri.host, "camera");
if (hasAudio.value &&
perms.testExactPermission(uri, "microphone") == perms.ALLOW_ACTION)
perms.remove(uri.host, "microphone");
Services.obs.notifyObservers(null, "getUserMedia:revoke", windowId);
}
}];

View File

@ -97,6 +97,12 @@
.pref-icon[type="fullscreen"] {
list-style-image: url(chrome://global/skin/icons/question-64.png);
}
.pref-icon[type="camera"] {
list-style-image: url(chrome://global/skin/icons/question-64.png);
}
.pref-icon[type="microphone"] {
list-style-image: url(chrome://global/skin/icons/question-64.png);
}
.pref-title {
font-size: 125%;

View File

@ -107,6 +107,12 @@
.pref-icon[type="fullscreen"] {
list-style-image: url(chrome://global/skin/icons/question-64.png);
}
.pref-icon[type="camera"] {
list-style-image: url(chrome://global/skin/icons/question-64.png);
}
.pref-icon[type="microphone"] {
list-style-image: url(chrome://global/skin/icons/question-64.png);
}
@media (min-resolution: 2dppx) {
.pref-icon[type="geo"] {

View File

@ -100,6 +100,12 @@
.pref-icon[type="fullscreen"] {
list-style-image: url(chrome://global/skin/icons/question-64.png);
}
.pref-icon[type="camera"] {
list-style-image: url(chrome://global/skin/icons/question-64.png);
}
.pref-icon[type="microphone"] {
list-style-image: url(chrome://global/skin/icons/question-64.png);
}
.pref-title {
font-size: 125%;

View File

@ -14,6 +14,7 @@
#include "nsIEventTarget.h"
#include "nsIUUIDGenerator.h"
#include "nsIScriptGlobalObject.h"
#include "nsIPermissionManager.h"
#include "nsIPopupWindowManager.h"
#include "nsISupportsArray.h"
#include "nsIDocShell.h"
@ -33,8 +34,6 @@
#include "nsDOMFile.h"
#include "nsGlobalWindow.h"
#include "mozilla/Preferences.h"
/* Using WebRTC backend on Desktops (Mac, Windows, Linux), otherwise default */
#include "MediaEngineDefault.h"
#if defined(MOZ_WEBRTC)
@ -907,6 +906,13 @@ public:
return NS_OK;
}
nsresult
SetContraints(const MediaStreamConstraintsInternal& aConstraints)
{
mConstraints = aConstraints;
return NS_OK;
}
nsresult
SetAudioDevice(MediaDevice* aAudioDevice)
{
@ -1075,7 +1081,11 @@ public:
{
NS_ASSERTION(!NS_IsMainThread(), "Don't call on main thread");
MediaEngine *backend = mManager->GetBackend(mWindowId);
nsRefPtr<MediaEngine> backend;
if (mConstraints.mFake)
backend = new MediaEngineDefault();
else
backend = mManager->GetBackend(mWindowId);
ScopedDeletePtr<SourceSet> final (GetSources(backend, mConstraints.mVideom,
&MediaEngine::EnumerateVideoDevices,
@ -1415,14 +1425,59 @@ MediaManager::GetUserMedia(JSContext* aCx, bool aPrivileged,
}
#endif
// XXX No full support for picture in Desktop yet (needs proper UI)
if (aPrivileged || c.mFake) {
if (aPrivileged ||
(c.mFake && !Preferences::GetBool("media.navigator.permission.fake"))) {
runnable->Arm();
mMediaThread->Dispatch(runnable, NS_DISPATCH_NORMAL);
} else {
// Check if this site has persistent permissions.
nsresult rv;
nsCOMPtr<nsIPermissionManager> permManager =
do_GetService(NS_PERMISSIONMANAGER_CONTRACTID, &rv);
NS_ENSURE_SUCCESS(rv, rv);
uint32_t audioPerm = nsIPermissionManager::UNKNOWN_ACTION;
if (c.mAudio) {
rv = permManager->TestExactPermissionFromPrincipal(
aWindow->GetExtantDoc()->NodePrincipal(), "microphone", &audioPerm);
NS_ENSURE_SUCCESS(rv, rv);
if (audioPerm == nsIPermissionManager::PROMPT_ACTION) {
audioPerm = nsIPermissionManager::UNKNOWN_ACTION;
}
}
uint32_t videoPerm = nsIPermissionManager::UNKNOWN_ACTION;
if (c.mVideo) {
rv = permManager->TestExactPermissionFromPrincipal(
aWindow->GetExtantDoc()->NodePrincipal(), "camera", &videoPerm);
NS_ENSURE_SUCCESS(rv, rv);
if (videoPerm == nsIPermissionManager::PROMPT_ACTION) {
videoPerm = nsIPermissionManager::UNKNOWN_ACTION;
}
}
if ((!c.mAudio || audioPerm) && (!c.mVideo || videoPerm)) {
// All permissions we were about to request already have a saved value.
if (c.mAudio && audioPerm == nsIPermissionManager::DENY_ACTION) {
c.mAudio = false;
runnable->SetContraints(c);
}
if (c.mVideo && videoPerm == nsIPermissionManager::DENY_ACTION) {
c.mVideo = false;
runnable->SetContraints(c);
}
runnable->Arm();
if (!c.mAudio && !c.mVideo) {
return runnable->Denied(NS_LITERAL_STRING("PERMISSION_DENIED"));
}
return mMediaThread->Dispatch(runnable, NS_DISPATCH_NORMAL);
}
// Ask for user permission, and dispatch runnable (or not) when a response
// is received via an observer notification. Each call is paired with its
// runnable by a GUID.
nsresult rv;
nsCOMPtr<nsIUUIDGenerator> uuidgen =
do_GetService("@mozilla.org/uuid-generator;1", &rv);
NS_ENSURE_SUCCESS(rv, rv);

View File

@ -19,6 +19,7 @@
#include "nsIDOMNavigatorUserMedia.h"
#include "nsXULAppAPI.h"
#include "mozilla/Attributes.h"
#include "mozilla/Preferences.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/dom/MediaStreamTrackBinding.h"
#include "prlog.h"
@ -102,12 +103,16 @@ public:
bool CapturingVideo()
{
NS_ASSERTION(NS_IsMainThread(), "Only call on main thread");
return mVideoSource && !mVideoSource->IsFake() && !mStopped;
return mVideoSource && !mStopped &&
(!mVideoSource->IsFake() ||
Preferences::GetBool("media.navigator.permission.fake"));
}
bool CapturingAudio()
{
NS_ASSERTION(NS_IsMainThread(), "Only call on main thread");
return mAudioSource && !mAudioSource->IsFake() && !mStopped;
return mAudioSource && !mStopped &&
(!mAudioSource->IsFake() ||
Preferences::GetBool("media.navigator.permission.fake"));
}
void SetStopped()