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
authorAdam Roach [:abr] <adam@nostrum.com>
Thu, 10 Jul 2014 21:14:57 +0100
changeset 193419 28cffea656488425f68f68c281d338827fe3a8bc
parent 193418 5d91329df900fa0092387b14d2d3b01aaa529fa3
child 193420 e1a037c085d13e5ac044e5385a23f701460ed27f
push id27117
push userryanvm@gmail.com
push dateThu, 10 Jul 2014 22:23:14 +0000
treeherdermozilla-central@e1a037c085d1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjesup, florian
bugs1015486
milestone33.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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
browser/base/content/test/general/browser_devices_get_user_media_about_urls.js
browser/modules/moz.build
browser/modules/webrtcUI.jsm
dom/media/MediaManager.cpp
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -277,17 +277,19 @@ skip-if = e10s # Bug ?????? - [JavaScrip
 [browser_contextSearchTabPosition.js]
 skip-if = os == "mac" # bug 967013, bug 926729
 [browser_ctrlTab.js]
 skip-if = e10s # Bug ????? - thumbnail captures need e10s love (tabPreviews_capture fails with Argument 1 of CanvasRenderingContext2D.drawWindow does not implement interface Window.)
 [browser_customize_popupNotification.js]
 [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]
 [browser_drag.js]
 skip-if = true # browser_drag.js is disabled, as it needs to be updated for the new behavior from bug 320638.
 [browser_findbarClose.js]
 skip-if = e10s # Bug ?????? - test directly manipulates content (tries to grab an iframe directly from content)
 [browser_fullscreen-window-open.js]
new file mode 100644
--- /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;
+}
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -19,16 +19,17 @@ EXTRA_JS_MODULES += [
     'offlineAppCache.jsm',
     'PanelFrame.jsm',
     'RemotePrompt.jsm',
     'SharedFrame.jsm',
     'SitePermissions.jsm',
     'Social.jsm',
     'TabCrashReporter.jsm',
     'WebappManager.jsm',
+    'webrtcUI.jsm',
 ]
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows':
     EXTRA_JS_MODULES += [
         'Windows8WindowFrameColor.jsm',
         'WindowsJumpLists.jsm',
         'WindowsPreviewPerTab.jsm',
     ]
@@ -37,13 +38,12 @@ if CONFIG['NIGHTLY_BUILD']:
     EXTRA_JS_MODULES += [
         'SignInToWebsite.jsm',
     ]
 
 EXTRA_PP_JS_MODULES += [
     'AboutHome.jsm',
     'RecentWindow.jsm',
     'UITour.jsm',
-    'webrtcUI.jsm',
 ]
 
 if CONFIG['MOZILLA_OFFICIAL']:
     DEFINES['MOZILLA_OFFICIAL'] = 1
--- a/browser/modules/webrtcUI.jsm
+++ b/browser/modules/webrtcUI.jsm
@@ -123,38 +123,18 @@ function prompt(aContentWindow, aCallID,
     return;
   }
 
   let uri = aContentWindow.document.documentURIObject;
   let browser = getBrowserForWindow(aContentWindow);
   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,
                           stringBundle.getString("getUserMedia.shareSelectedDevices.label")),
     accessKey: stringBundle.getString("getUserMedia.shareSelectedDevices.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.
--- a/dom/media/MediaManager.cpp
+++ b/dom/media/MediaManager.cpp
@@ -18,16 +18,17 @@
 #include "nsIScriptGlobalObject.h"
 #include "nsIPermissionManager.h"
 #include "nsIPopupWindowManager.h"
 #include "nsISupportsArray.h"
 #include "nsIDocShell.h"
 #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"
 #include "mozilla/dom/MediaStreamBinding.h"
 #include "mozilla/dom/MediaStreamTrackBinding.h"
 #include "mozilla/dom/GetUserMediaRequestBinding.h"
 #include "mozilla/Preferences.h"
 #include "MediaTrackConstraints.h"
@@ -1472,23 +1473,38 @@ MediaManager::GetUserMedia(bool aPrivile
 
 #if defined(ANDROID) && !defined(MOZ_WIDGET_GONK)
   if (c.mPicture) {
     // ShowFilePickerForMimeType() must run on the Main Thread! (on Android)
     NS_DispatchToMainThread(runnable);
     return NS_OK;
   }
 #endif
+  nsIURI* docURI = aWindow->GetDocumentURI();
+#ifdef MOZ_LOOP
+  {
+    bool isLoop = false;
+    nsCOMPtr<nsIURI> 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);
     }
 
     // Check if this site has persistent permissions.
     nsresult rv;
     nsCOMPtr<nsIPermissionManager> permManager =
       do_GetService(NS_PERMISSIONMANAGER_CONTRACTID, &rv);