From a130efd232331adc9c91366b9cefa1777d7a0263 Mon Sep 17 00:00:00 2001 From: "Adam Roach [:abr]" Date: Thu, 10 Jul 2014 21:14:57 +0100 Subject: [PATCH] Bug 1015486 Bypass the video and audio permission prompts for Loop, as Loop will provide its own mechanisms. Patch by abr, tests by Standard8. r=jesup,r=florian --- browser/base/content/test/general/browser.ini | 4 +- ...owser_devices_get_user_media_about_urls.js | 260 ++++++++++++++++++ browser/modules/moz.build | 2 +- browser/modules/webrtcUI.jsm | 20 -- dom/media/MediaManager.cpp | 18 +- 5 files changed, 281 insertions(+), 23 deletions(-) create mode 100644 browser/base/content/test/general/browser_devices_get_user_media_about_urls.js diff --git a/browser/base/content/test/general/browser.ini b/browser/base/content/test/general/browser.ini index 135574c6436..95792380313 100644 --- a/browser/base/content/test/general/browser.ini +++ b/browser/base/content/test/general/browser.ini @@ -282,7 +282,9 @@ skip-if = e10s # Bug ????? - thumbnail captures need e10s love (tabPreviews_capt [browser_datareporting_notification.js] run-if = datareporting [browser_devices_get_user_media.js] -skip-if = (os == "linux" && debug) || e10s # linux: bug 976544; e10s: Bug ?????? - appears user media notifications only happen in the child and don't make their way to the parent? +skip-if = (os == "linux" && debug) || e10s # linux: bug 976544; e10s: Bug 973001 - appears user media notifications only happen in the child and don't make their way to the parent? +[browser_devices_get_user_media_about_urls.js] +skip-if = e10s # Bug 973001 - appears user media notifications only happen in the child and don't make their way to the parent? [browser_discovery.js] skip-if = e10s # Bug 918663 - DOMLinkAdded events don't make their way to chrome [browser_duplicateIDs.js] diff --git a/browser/base/content/test/general/browser_devices_get_user_media_about_urls.js b/browser/base/content/test/general/browser_devices_get_user_media_about_urls.js new file mode 100644 index 00000000000..02c183579bc --- /dev/null +++ b/browser/base/content/test/general/browser_devices_get_user_media_about_urls.js @@ -0,0 +1,260 @@ +/* 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/. */ + +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 gTab; + +var gObservedTopics = {}; +function observer(aSubject, aTopic, aData) { + if (!(aTopic in gObservedTopics)) + gObservedTopics[aTopic] = 1; + else + ++gObservedTopics[aTopic]; +} + +function promiseObserverCalled(aTopic, aAction) { + let deferred = Promise.defer(); + info("Waiting for " + aTopic); + + Services.obs.addObserver(function observer(aSubject, topic, aData) { + ok(true, "got " + aTopic + " notification"); + info("Message: " + aData); + 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 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(); + info("Waiting for " + aName + " to be removed"); + + waitForCondition(() => !PopupNotifications.getNotification(aName), + () => { + ok(!PopupNotifications.getNotification(aName), + aName + " notification removed"); + deferred.resolve(); + }, "timeout waiting for popup notification " + aName + " to disappear"); + + return deferred.promise; +} + +function expectObserverCalled(aTopic) { + is(gObservedTopics[aTopic], 1, "expected notification " + aTopic); + if (aTopic in gObservedTopics) + --gObservedTopics[aTopic]; +} + +function expectNoObserverCalled() { + for (let topic in gObservedTopics) { + if (gObservedTopics[topic]) + is(gObservedTopics[topic], 0, topic + " notification unexpected"); + } + gObservedTopics = {}; +} + +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) { + expectNoObserverCalled(); + + info("closing the stream"); + content.wrappedJSObject.closeStream(); + + if (!aAlreadyClosed) + yield promiseObserverCalled("recording-device-events"); + + yield promiseNoPopupNotification("webRTC-sharingDevices"); + if (!aAlreadyClosed) + expectObserverCalled("recording-window-ended"); + + let statusButton = document.getElementById("webrtc-status-button"); + ok(statusButton.hidden, "WebRTC status button hidden"); +} + +function loadPage(aUrl) { + let deferred = Promise.defer(); + + gTab.linkedBrowser.addEventListener("load", function onload() { + gTab.linkedBrowser.removeEventListener("load", onload, true); + + is(PopupNotifications._currentNotifications.length, 0, + "should start the test without any prior popup notification"); + + deferred.resolve(); + }, true); + content.location = aUrl; + return deferred.promise; +} + +// A fake about module to map get_user_media.html to different about urls. +function fakeLoopAboutModule() { +} + +fakeLoopAboutModule.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule]), + newChannel: function (aURI) { + let rootDir = getRootDirectory(gTestPath); + let chan = Services.io.newChannel(rootDir + "get_user_media.html", null, null); + chan.owner = Services.scriptSecurityManager.getSystemPrincipal(); + return chan; + }, + getURIFlags: function (aURI) { + return Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT | + Ci.nsIAboutModule.ALLOW_SCRIPT | + Ci.nsIAboutModule.HIDE_FROM_ABOUTABOUT; + } +}; + +let factory = XPCOMUtils._getFactory(fakeLoopAboutModule); +let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + +registerCleanupFunction(function() { + gBrowser.removeCurrentTab(); + kObservedTopics.forEach(topic => { + Services.obs.removeObserver(observer, topic); + }); + Services.prefs.clearUserPref(PREF_PERMISSION_FAKE); +}); + + +let gTests = [ + +{ + desc: "getUserMedia about:loopconversation shouldn't prompt", + run: function checkAudioVideoLoop() { + let classID = Cc["@mozilla.org/uuid-generator;1"] + .getService(Ci.nsIUUIDGenerator).generateUUID(); + registrar.registerFactory(classID, "", + "@mozilla.org/network/protocol/about;1?what=loopconversation", + factory); + + yield loadPage("about:loopconversation"); + + yield promiseObserverCalled("recording-device-events", () => { + info("requesting devices"); + content.wrappedJSObject.requestDevice(true, true); + }); + // Wait for the devices to actually be captured and running before + // proceeding. + yield promisePopupNotification("webRTC-sharingDevices"); + + is(getMediaCaptureState(), "CameraAndMicrophone", + "expected camera and microphone to be shared"); + + yield closeStream(); + + registrar.unregisterFactory(classID, factory); + } +}, + +{ + desc: "getUserMedia about:evil should prompt", + run: function checkAudioVideoNonLoop() { + let classID = Cc["@mozilla.org/uuid-generator;1"] + .getService(Ci.nsIUUIDGenerator).generateUUID(); + registrar.registerFactory(classID, "", + "@mozilla.org/network/protocol/about;1?what=evil", + factory); + + yield loadPage("about:evil"); + + yield promiseObserverCalled("getUserMedia:request", () => { + info("requesting devices"); + content.wrappedJSObject.requestDevice(true, true); + }); + + isnot(getMediaCaptureState(), "CameraAndMicrophone", + "expected camera and microphone not to be shared"); + + registrar.unregisterFactory(classID, factory); + } +}, + +]; + +function test() { + waitForExplicitFinish(); + + Services.prefs.setBoolPref(PREF_PERMISSION_FAKE, true); + + gTab = gBrowser.addTab(); + gBrowser.selectedTab = gTab; + + kObservedTopics.forEach(topic => { + Services.obs.addObserver(observer, topic, false); + }); + + Task.spawn(function () { + for (let test of gTests) { + info(test.desc); + yield test.run(); + + // Cleanup before the next test + expectNoObserverCalled(); + } + }).then(finish, ex => { + ok(false, "Unexpected Exception: " + ex); + finish(); + }); +} + +function wait(time) { + let deferred = Promise.defer(); + setTimeout(deferred.resolve, time); + return deferred.promise; +} diff --git a/browser/modules/moz.build b/browser/modules/moz.build index 165febe5118..aad5d40d75e 100644 --- a/browser/modules/moz.build +++ b/browser/modules/moz.build @@ -24,6 +24,7 @@ EXTRA_JS_MODULES += [ 'Social.jsm', 'TabCrashReporter.jsm', 'WebappManager.jsm', + 'webrtcUI.jsm', ] if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows': @@ -42,7 +43,6 @@ EXTRA_PP_JS_MODULES += [ 'AboutHome.jsm', 'RecentWindow.jsm', 'UITour.jsm', - 'webrtcUI.jsm', ] if CONFIG['MOZILLA_OFFICIAL']: diff --git a/browser/modules/webrtcUI.jsm b/browser/modules/webrtcUI.jsm index 43dd0decd30..8ebe773f4aa 100644 --- a/browser/modules/webrtcUI.jsm +++ b/browser/modules/webrtcUI.jsm @@ -128,28 +128,8 @@ function prompt(aContentWindow, aCallID, aAudioRequested, aVideoRequested, aDevi let chromeDoc = browser.ownerDocument; let chromeWin = chromeDoc.defaultView; let stringBundle = chromeWin.gNavigatorBundle; -#ifdef MOZ_LOOP - let host; - // For Loop protocols that start with about:, use brandShortName instead of the host for now. - // Bug 990678 will implement improvements/replacements for the permissions dialog, so this - // should become unnecessary. - if (uri.spec.startsWith("about:loop")) { - let brandBundle = Services.strings.createBundle("chrome://branding/locale/brand.properties"); - - host = brandBundle.GetStringFromName("brandShortName"); - } - else { - // uri.host throws for about: protocols, so we have to do this once we know - // it isn't about:loop. - host = uri.host; - } - - let message = stringBundle.getFormattedString("getUserMedia.share" + requestType + ".message", - [ host ]); -#else let message = stringBundle.getFormattedString("getUserMedia.share" + requestType + ".message", [ uri.host ]); -#endif let mainAction = { label: PluralForm.get(requestType == "CameraAndMicrophone" ? 2 : 1, diff --git a/dom/media/MediaManager.cpp b/dom/media/MediaManager.cpp index 4c1d689601c..dfd018f3cf3 100644 --- a/dom/media/MediaManager.cpp +++ b/dom/media/MediaManager.cpp @@ -23,6 +23,7 @@ #include "nsIDocument.h" #include "nsISupportsPrimitives.h" #include "nsIInterfaceRequestorUtils.h" +#include "nsNetUtil.h" #include "mozilla/Types.h" #include "mozilla/PeerIdentity.h" #include "mozilla/dom/ContentChild.h" @@ -1477,13 +1478,28 @@ MediaManager::GetUserMedia(bool aPrivileged, return NS_OK; } #endif + nsIURI* docURI = aWindow->GetDocumentURI(); +#ifdef MOZ_LOOP + { + bool isLoop = false; + nsCOMPtr loopURI; + nsresult rv = NS_NewURI(getter_AddRefs(loopURI), "about:loopconversation"); + NS_ENSURE_SUCCESS(rv, rv); + rv = docURI->EqualsExceptRef(loopURI, &isLoop); + NS_ENSURE_SUCCESS(rv, rv); + + if (isLoop) { + aPrivileged = true; + } + } +#endif + // XXX No full support for picture in Desktop yet (needs proper UI) if (aPrivileged || (c.mFake && !Preferences::GetBool("media.navigator.permission.fake"))) { mMediaThread->Dispatch(runnable, NS_DISPATCH_NORMAL); } else { bool isHTTPS = false; - nsIURI* docURI = aWindow->GetDocumentURI(); if (docURI) { docURI->SchemeIs("https", &isHTTPS); }