Bug 1270572 - allow un-prompted gUM access if the page has a live track connected to the same device; r=florian, gcp draft
authorMunro Mengjue Chiang <mchiang@mozilla.com>
Thu, 01 Dec 2016 15:12:38 +0800
changeset 450836 af0367bfbe920f4aa6c23212117214d5907e90dd
parent 446255 cd4cdcc9ad6c45dad8b8d8c0d40e459db2bca8a1
child 450837 26e8ea5ddf5999f3bf6cc0742cb4fda5b0fd3eb2
push id38960
push usermchiang@mozilla.com
push dateMon, 19 Dec 2016 03:19:27 +0000
reviewersflorian, gcp
bugs1270572
milestone53.0a1
Bug 1270572 - allow un-prompted gUM access if the page has a live track connected to the same device; r=florian, gcp MozReview-Commit-ID: AcLD2TZ3t6S
browser/base/content/browser.js
browser/modules/ContentWebRTC.jsm
browser/modules/webrtcUI.jsm
dom/media/GetUserMediaRequest.cpp
dom/media/GetUserMediaRequest.h
dom/media/MediaManager.cpp
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -4832,16 +4832,17 @@ var TabsProgressListener = {
     // or history.push/pop/replaceState.
     if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
       // Reader mode actually cares about these:
       let mm = gBrowser.selectedBrowser.messageManager;
       mm.sendAsyncMessage("Reader:PushState", {isArticle: gBrowser.selectedBrowser.isArticle});
       return;
     }
 
+    webrtcUI.stopSharing(aBrowser);
     // Filter out location changes in sub documents.
     if (!aWebProgress.isTopLevel)
       return;
 
     // Only need to call locationChange if the PopupNotifications object
     // for this window has already been initialized (i.e. its getter no
     // longer exists)
     if (!Object.getOwnPropertyDescriptor(window, "PopupNotifications").get)
@@ -7429,16 +7430,17 @@ var gIdentityHandler = {
               if (this._sharingState[id] &&
                   SitePermissions.get(uri, id) == SitePermissions.ALLOW)
                 SitePermissions.remove(uri, id);
             }
           }
         }
         let mm = gBrowser.selectedBrowser.messageManager;
         mm.sendAsyncMessage("webrtc:StopSharing", windowId);
+        webrtcUI.stopSharing(gBrowser.selectedBrowser);
       }
       SitePermissions.remove(gBrowser.currentURI, aPermission.id);
       this._permissionJustRemoved = true;
       this.updatePermissionHint();
 
       // Set telemetry values for clearing a permission
       let histogram = Services.telemetry.getKeyedHistogramById("WEB_PERMISSION_CLEARED");
 
--- a/browser/modules/ContentWebRTC.jsm
+++ b/browser/modules/ContentWebRTC.jsm
@@ -20,26 +20,28 @@ this.ContentWebRTC = {
   _initialized: false,
 
   init: function() {
     if (this._initialized)
       return;
 
     this._initialized = true;
     Services.obs.addObserver(handleGUMRequest, "getUserMedia:request", false);
+    Services.obs.addObserver(handleGUMStop, "getUserMedia:stopSharing", false);
     Services.obs.addObserver(handlePCRequest, "PeerConnection:request", false);
     Services.obs.addObserver(updateIndicators, "recording-device-events", false);
     Services.obs.addObserver(removeBrowserSpecificIndicator, "recording-window-ended", false);
 
     if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT)
       Services.obs.addObserver(processShutdown, "content-child-shutdown", false);
   },
 
   uninit: function() {
     Services.obs.removeObserver(handleGUMRequest, "getUserMedia:request");
+    Services.obs.removeObserver(handleGUMStop, "getUserMedia:stopSharing");
     Services.obs.removeObserver(handlePCRequest, "PeerConnection:request");
     Services.obs.removeObserver(updateIndicators, "recording-device-events");
     Services.obs.removeObserver(removeBrowserSpecificIndicator, "recording-window-ended");
 
     if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT)
       Services.obs.removeObserver(processShutdown, "content-child-shutdown");
 
     this._initialized = false;
@@ -119,16 +121,29 @@ function handlePCRequest(aSubject, aTopi
     innerWindowID: innerWindowID,
     callID: callID,
     documentURI: contentWindow.document.documentURI,
     secure: isSecure,
   };
   mm.sendAsyncMessage("rtcpeer:Request", request);
 }
 
+function handleGUMStop(aSubject, aTopic, aData) {
+  let contentWindow = Services.wm.getOuterWindowWithId(aSubject.windowID);
+
+  let request = {
+    windowID: aSubject.windowID,
+    rawID: aSubject.callID,
+    documentURI: contentWindow.document.documentURI
+  };
+
+  let mm = getMessageManagerForWindow(contentWindow);
+  mm.sendAsyncMessage("webrtc:StopSharing", request);
+}
+
 function handleGUMRequest(aSubject, aTopic, aData) {
   let constraints = aSubject.getConstraints();
   let secure = aSubject.isSecure;
   let contentWindow = Services.wm.getOuterWindowWithId(aSubject.windowID);
 
   contentWindow.navigator.mozGetUserMediaDevices(
     constraints,
     function(devices) {
--- a/browser/modules/webrtcUI.jsm
+++ b/browser/modules/webrtcUI.jsm
@@ -32,16 +32,17 @@ this.webrtcUI = {
     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:StopSharing", this);
     mm.addMessageListener("webrtc:CancelRequest", this);
     mm.addMessageListener("webrtc:UpdateBrowserIndicators", this);
   },
 
   uninit: function() {
     Services.obs.removeObserver(maybeAddMenuIndicator, "browser-delayed-startup-finished");
 
     let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
@@ -49,26 +50,28 @@ this.webrtcUI = {
     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:StopSharing");
     mm.removeMessageListener("webrtc:CancelRequest", this);
     mm.removeMessageListener("webrtc:UpdateBrowserIndicators", this);
 
     if (gIndicatorWindow) {
       gIndicatorWindow.close();
       gIndicatorWindow = null;
     }
   },
 
   processIndicators: new Map(),
+  _activeDevicePermissions: new WeakMap(),
 
   get showGlobalIndicator() {
     for (let [, indicators] of this.processIndicators) {
       if (indicators.showGlobalIndicator)
         return true;
     }
     return false;
   },
@@ -131,16 +134,22 @@ this.webrtcUI = {
         stream.browser = aNewBrowser;
     }
   },
 
   forgetStreamsFromBrowser: function(aBrowser) {
     this._streams = this._streams.filter(stream => stream.browser != aBrowser);
   },
 
+  stopSharing: function(aBrowser) {
+    if (webrtcUI._activeDevicePermissions.has(aBrowser)) {
+      webrtcUI._activeDevicePermissions.delete(aBrowser);
+    }
+  },
+
   showSharingDoorhanger: function(aActiveStream) {
     let browserWindow = aActiveStream.browser.ownerGlobal;
     if (aActiveStream.tab) {
       browserWindow.gBrowser.selectedTab = aActiveStream.tab;
     } else {
       aActiveStream.browser.focus();
     }
     browserWindow.focus();
@@ -206,16 +215,19 @@ this.webrtcUI = {
         break;
       }
       case "rtcpeer:CancelRequest":
         // No data to release. This code-point exists for add-ons to override.
         break;
       case "webrtc:Request":
         prompt(aMessage.target, aMessage.data);
         break;
+      case "webrtc:StopSharing":
+        stopSharing(aMessage.target, aMessage.data);
+        break;
       case "webrtc:CancelRequest":
         removePrompt(aMessage.target, aMessage.data);
         break;
       case "webrtc:UpdatingIndicators":
         webrtcUI._streams = [];
         break;
       case "webrtc:UpdateGlobalIndicators":
         updateIndicators(aMessage.data, aMessage.target);
@@ -275,16 +287,27 @@ function getHost(uri, href) {
       const kBundleURI = "chrome://browser/locale/browser.properties";
       let bundle = Services.strings.createBundle(kBundleURI);
       host = bundle.GetStringFromName("getUserMedia.sharingMenuUnknownHost");
     }
   }
   return host;
 }
 
+function stopSharing(aBrowser, aRequest) {
+  if (webrtcUI._activeDevicePermissions.has(aBrowser)) {
+    let s = webrtcUI._activeDevicePermissions.get(aBrowser);
+    if (!aRequest.rawID) {
+      webrtcUI._activeDevicePermissions.delete(aBrowser);
+    } else if (s.has(aRequest.windowID + aRequest.rawID)) {
+      s.delete(aRequest.windowID + aRequest.rawID);
+    }
+  }
+}
+
 function prompt(aBrowser, aRequest) {
   let {audioDevices: audioDevices, videoDevices: videoDevices,
        sharingScreen: sharingScreen, sharingAudio: sharingAudio,
        requestTypes: requestTypes} = aRequest;
   let uri = Services.io.newURI(aRequest.documentURI, null, null);
   let host = getHost(uri);
   let chromeDoc = aBrowser.ownerDocument;
   let stringBundle = chromeDoc.defaultView.gNavigatorBundle;
@@ -390,31 +413,65 @@ function prompt(aBrowser, aRequest) {
 
         if (camPerm == perms.PROMPT_ACTION)
           camPerm = perms.UNKNOWN_ACTION;
 
         // Screen sharing shouldn't follow the camera permissions.
         if (videoDevices.length && sharingScreen)
           camPerm = perms.UNKNOWN_ACTION;
 
+        let tmpCamPerm = perms.UNKNOWN_ACTION;
+        let tmpMicPerm = perms.UNKNOWN_ACTION;
+        let allowedTmpDevices = [];
+
+        for (let device of videoDevices) {
+          if (webrtcUI._activeDevicePermissions.has(this.browser)) {
+            let s = webrtcUI._activeDevicePermissions.get(this.browser);
+            if (s.has(aRequest.windowID + device.id)) {
+              tmpCamPerm = perms.ALLOW_ACTION;
+              allowedTmpDevices.push(device.deviceIndex);
+              break;
+            }
+          }
+        }
+
+        for (let device of audioDevices) {
+          if (webrtcUI._activeDevicePermissions.has(this.browser)) {
+            let s = webrtcUI._activeDevicePermissions.get(this.browser);
+            if (s.has(aRequest.windowID + device.id)) {
+              tmpMicPerm = perms.ALLOW_ACTION;
+              allowedTmpDevices.push(device.deviceIndex);
+              break;
+            }
+          }
+        }
+
         // We don't check that permissions are set to ALLOW_ACTION in this
         // test; only that they are set. This is because if audio is allowed
         // and video is denied persistently, we don't want to show the prompt,
         // and will grant audio access immediately.
-        if ((!audioDevices.length || micPerm) && (!videoDevices.length || camPerm)) {
-          // All permissions we were about to request are already persistently set.
+        if (((!audioDevices.length || micPerm) && (!videoDevices.length || camPerm)) ||
+            ((!audioDevices.length || tmpMicPerm) && (!videoDevices.length || tmpCamPerm))) {
           let allowedDevices = [];
-          if (videoDevices.length && camPerm == perms.ALLOW_ACTION) {
-            allowedDevices.push(videoDevices[0].deviceIndex);
+          if ((!audioDevices.length || micPerm) && (!videoDevices.length || camPerm)) {
+            // All permissions we were about to request are already persistently set.
+            if (videoDevices.length && camPerm == perms.ALLOW_ACTION) {
+              allowedDevices.push(videoDevices[0].deviceIndex);
+              Services.perms.add(uri, "MediaManagerVideo",
+                                 Services.perms.ALLOW_ACTION,
+                                 Services.perms.EXPIRE_SESSION);
+            }
+            if (audioDevices.length && micPerm == perms.ALLOW_ACTION)
+              allowedDevices.push(audioDevices[0].deviceIndex);
+          } else {
+            allowedDevices = allowedTmpDevices;
             Services.perms.add(uri, "MediaManagerVideo",
                                Services.perms.ALLOW_ACTION,
                                Services.perms.EXPIRE_SESSION);
           }
-          if (audioDevices.length && micPerm == perms.ALLOW_ACTION)
-            allowedDevices.push(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);
@@ -599,28 +656,41 @@ function prompt(aBrowser, aRequest) {
           let videoDeviceIndex = doc.getElementById(listId).value;
           let allowCamera = videoDeviceIndex != "-1";
           if (allowCamera) {
             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._activeDevicePermissions.has(aBrowser)) {
+              let s = new Set();
+              webrtcUI._activeDevicePermissions.set(aBrowser, s);
+            }
+            let s = webrtcUI._activeDevicePermissions.get(aBrowser);
+            s.add(aRequest.windowID + videoDevices[videoDeviceIndex].id);
           }
           if (remember) {
             perms.add(uri, "camera",
                       allowCamera ? perms.ALLOW_ACTION : perms.DENY_ACTION);
           }
         }
         if (audioDevices.length) {
           if (!sharingAudio) {
             let audioDeviceIndex = doc.getElementById("webRTC-selectMicrophone-menulist").value;
             let allowMic = audioDeviceIndex != "-1";
-            if (allowMic)
+            if (allowMic) {
               allowedDevices.push(audioDeviceIndex);
+              if (!webrtcUI._activeDevicePermissions.has(aBrowser)) {
+                let s = new Set();
+                webrtcUI._activeDevicePermissions.set(aBrowser, s);
+              }
+              let s = webrtcUI._activeDevicePermissions.get(aBrowser);
+              s.add(aRequest.windowID + audioDevices[audioDeviceIndex - videoDevices.length].id);
+            }
             if (remember) {
               perms.add(uri, "microphone",
                         allowMic ? perms.ALLOW_ACTION : perms.DENY_ACTION);
             }
           } else {
             // Only one device possible for audio capture.
             allowedDevices.push(0);
           }
--- a/dom/media/GetUserMediaRequest.cpp
+++ b/dom/media/GetUserMediaRequest.cpp
@@ -20,16 +20,25 @@ GetUserMediaRequest::GetUserMediaRequest
   : mInnerWindowID(aInnerWindow->WindowID())
   , mOuterWindowID(aInnerWindow->GetOuterWindow()->WindowID())
   , mCallID(aCallID)
   , mConstraints(new MediaStreamConstraints(aConstraints))
   , mIsSecure(aIsSecure)
 {
 }
 
+GetUserMediaRequest::GetUserMediaRequest(
+    nsPIDOMWindowInner* aInnerWindow,
+    const nsAString& aRawId)
+  : mInnerWindowID(aInnerWindow->WindowID())
+  , mOuterWindowID(aInnerWindow->GetOuterWindow()->WindowID())
+  , mCallID(aRawId)
+{
+}
+
 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_0(GetUserMediaRequest)
 NS_IMPL_CYCLE_COLLECTING_ADDREF(GetUserMediaRequest)
 NS_IMPL_CYCLE_COLLECTING_RELEASE(GetUserMediaRequest)
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(GetUserMediaRequest)
   NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
   NS_INTERFACE_MAP_ENTRY(nsISupports)
 NS_INTERFACE_MAP_END
 
--- a/dom/media/GetUserMediaRequest.h
+++ b/dom/media/GetUserMediaRequest.h
@@ -19,16 +19,18 @@ struct MediaStreamConstraints;
 
 class GetUserMediaRequest : public nsISupports, public nsWrapperCache
 {
 public:
   GetUserMediaRequest(nsPIDOMWindowInner* aInnerWindow,
                       const nsAString& aCallID,
                       const MediaStreamConstraints& aConstraints,
                       bool aIsSecure);
+  GetUserMediaRequest(nsPIDOMWindowInner* aInnerWindow,
+                      const nsAString& aRawId);
 
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(GetUserMediaRequest)
 
   JSObject* WrapObject(JSContext* cx, JS::Handle<JSObject*> aGivenProto) override;
   nsISupports* GetParentObject();
 
   uint64_t WindowID();
--- a/dom/media/MediaManager.cpp
+++ b/dom/media/MediaManager.cpp
@@ -170,16 +170,17 @@ HostIsHttps(nsIURI &docURI)
 
 /**
  * This class is an implementation of MediaStreamListener. This is used
  * to Start() and Stop() the underlying MediaEngineSource when MediaStreams
  * are assigned and deassigned in content.
  */
 class GetUserMediaCallbackMediaStreamListener : public MediaStreamListener
 {
+  friend MediaManager;
 public:
   // Create in an inactive state
   GetUserMediaCallbackMediaStreamListener(base::Thread *aThread,
     uint64_t aWindowID,
     const PrincipalHandle& aPrincipalHandle)
     : mMediaThread(aThread)
     , mMainThreadCheck(nullptr)
     , mWindowID(aWindowID)
@@ -2761,25 +2762,101 @@ MediaManager::RemoveWindowID(uint64_t aW
 }
 
 void
 MediaManager::RemoveFromWindowList(uint64_t aWindowID,
   GetUserMediaCallbackMediaStreamListener *aListener)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
+  nsString videoRawId;
+  nsString audioRawId;
+  bool hasVideoDevice = aListener->mVideoDevice;
+  bool hasAudioDevice = aListener->mAudioDevice;
+
+  if (hasVideoDevice) {
+    aListener->mVideoDevice->GetRawId(videoRawId);
+  }
+  if (hasAudioDevice) {
+    aListener->mAudioDevice->GetRawId(audioRawId);
+  }
+
   // This is defined as safe on an inactive GUMCMSListener
   aListener->Remove(); // really queues the remove
 
   StreamListeners* listeners = GetWindowListeners(aWindowID);
   if (!listeners) {
+    nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+    auto* globalWindow = nsGlobalWindow::GetInnerWindowWithId(aWindowID);
+    RefPtr<nsPIDOMWindowInner> window = globalWindow ? globalWindow->AsInner()
+                                                     : nullptr;
+    RefPtr<GetUserMediaRequest> req =
+      new GetUserMediaRequest(window, NullString());
+    obs->NotifyObservers(req, "getUserMedia:stopSharing", nullptr);
     return;
   }
   listeners->RemoveElement(aListener);
-  if (listeners->Length() == 0) {
+
+  uint32_t length = listeners->Length();
+
+  if (hasVideoDevice) {
+    bool revokeVideoPermission = true;
+
+    for (uint32_t i = 0; i < length; ++i) {
+      RefPtr<GetUserMediaCallbackMediaStreamListener> listener =
+        listeners->ElementAt(i);
+      if (hasVideoDevice && listener->mVideoDevice) {
+        nsString rawId;
+        listener->mVideoDevice->GetRawId(rawId);
+        if (videoRawId.Equals(rawId)) {
+          revokeVideoPermission = false;
+          break;
+        }
+      }
+    }
+
+    if (revokeVideoPermission) {
+      nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+      auto* globalWindow = nsGlobalWindow::GetInnerWindowWithId(aWindowID);
+      RefPtr<nsPIDOMWindowInner> window = globalWindow ? globalWindow->AsInner()
+                                                       : nullptr;
+      RefPtr<GetUserMediaRequest> req =
+        new GetUserMediaRequest(window, videoRawId);
+      obs->NotifyObservers(req, "getUserMedia:stopSharing", nullptr);
+    }
+  }
+
+  if (hasAudioDevice) {
+    bool revokeAudioPermission = true;
+
+    for (uint32_t i = 0; i < length; ++i) {
+      RefPtr<GetUserMediaCallbackMediaStreamListener> listener =
+        listeners->ElementAt(i);
+      if (hasAudioDevice && listener->mAudioDevice) {
+        nsString rawId;
+        listener->mAudioDevice->GetRawId(rawId);
+        if (audioRawId.Equals(rawId)) {
+          revokeAudioPermission = false;
+          break;
+        }
+      }
+    }
+
+    if (revokeAudioPermission) {
+      nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+      auto* globalWindow = nsGlobalWindow::GetInnerWindowWithId(aWindowID);
+      RefPtr<nsPIDOMWindowInner> window = globalWindow ? globalWindow->AsInner()
+                                                       : nullptr;
+      RefPtr<GetUserMediaRequest> req =
+        new GetUserMediaRequest(window, audioRawId);
+      obs->NotifyObservers(req, "getUserMedia:stopSharing", nullptr);
+    }
+  }
+
+  if (length == 0) {
     RemoveWindowID(aWindowID);
     // listeners has been deleted here
   }
 }
 
 void
 MediaManager::GetPref(nsIPrefBranch *aBranch, const char *aPref,
                       const char *aData, int32_t *aVal)