Enforce user gesture for getUserMedia in case a previous getUserMedia call was denied
authoryouenn@apple.com <youenn@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 5 Nov 2019 09:14:33 +0000 (09:14 +0000)
committeryouenn@apple.com <youenn@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 5 Nov 2019 09:14:33 +0000 (09:14 +0000)
https://bugs.webkit.org/show_bug.cgi?id=203362
Source/WebCore:

Reviewed by Eric Carlson.

Compute whether a media request is user priviledged or not.
It is priviledged if it is created as part of a user gesture and no request of the same type
has been previously created for the same user gesture.
If getDisplayMedia is called twice as part of a single user gesture, getDisplaMedia will reject for the second call.

Remove the internal ability to disable user gesture check.
Instead use internals API to simulate a user gesture.

Test: fast/mediastream/getUserMedia-deny-persistency5.html and updated test.

* Modules/mediastream/MediaDevices.cpp:
(WebCore::MediaDevices::computeUserGesturePriviledge):
(WebCore::MediaDevices::getUserMedia):
(WebCore::MediaDevices::getDisplayMedia):
(WebCore::MediaDevices::getUserMedia const): Deleted.
(WebCore::MediaDevices::getDisplayMedia const): Deleted.
* Modules/mediastream/MediaDevices.h:
* platform/mediastream/MediaStreamRequest.h:
(WebCore::MediaStreamRequest::encode const):
(WebCore::MediaStreamRequest::decode):
* testing/Internals.cpp:
(WebCore::Internals::setMediaStreamSourceInterrupted):
(WebCore::Internals::setDisableGetDisplayMediaUserGestureConstraint): Deleted.
* testing/Internals.h:
* testing/Internals.idl:

Source/WebKit:

Reviewed by Eric Carlson.

In case the request has user gesture priviledge, do not look at denied request history.

* UIProcess/UserMediaPermissionRequestManagerProxy.cpp:
(WebKit::UserMediaPermissionRequestManagerProxy::getRequestAction):
* UIProcess/UserMediaPermissionRequestProxy.h:
(WebKit::UserMediaPermissionRequestProxy::isUserGesturePriviledged const):

Tools:

Reviewed by Eric Carlson.

* TestWebKitAPI/Tests/WebKitCocoa/GetDisplayMedia.mm:
(TestWebKitAPI::TEST_F):
Update test to take into account the ability to ask again for permission.
* TestWebKitAPI/Tests/WebKit/getDisplayMedia.html:
Update to make sure we notify test if internals is not available.

LayoutTests:

<rdar://problem/56648232>

Reviewed by Eric Carlson.

* fast/mediastream/constraint-intrinsic-size.html:
* fast/mediastream/get-display-media-muted.html:
* fast/mediastream/getUserMedia-deny-persistency5-expected.txt:
* fast/mediastream/getUserMedia-deny-persistency5.html:
* fast/mediastream/media-stream-page-muted.html:
Use user gesture simulation instead of disabling user gesture check.
* fast/mediastream/screencapture-user-gesture.html:
* fast/mediastream/screencapture-user-gesture-expected.txt:
* http/tests/media/media-stream/get-display-media-iframe-allow-attribute-expected.txt:
* http/tests/media/media-stream/get-display-media-prompt.html:
* http/tests/media/media-stream/resources/get-display-media-devices-iframe.html:
* resources/testharnessreport.js:

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@252046 268f45cc-cd09-0410-ab3c-d52691b4dbfc

25 files changed:
LayoutTests/ChangeLog
LayoutTests/fast/mediastream/constraint-intrinsic-size.html
LayoutTests/fast/mediastream/get-display-media-muted.html
LayoutTests/fast/mediastream/getUserMedia-deny-persistency5-expected.txt [new file with mode: 0644]
LayoutTests/fast/mediastream/getUserMedia-deny-persistency5.html [new file with mode: 0644]
LayoutTests/fast/mediastream/media-stream-page-muted.html
LayoutTests/fast/mediastream/screencapture-user-gesture-expected.txt
LayoutTests/fast/mediastream/screencapture-user-gesture.html
LayoutTests/http/tests/media/media-stream/get-display-media-iframe-allow-attribute-expected.txt
LayoutTests/http/tests/media/media-stream/get-display-media-prompt.html
LayoutTests/http/tests/media/media-stream/resources/get-display-media-devices-iframe.html
LayoutTests/resources/testharnessreport.js
Source/WebCore/ChangeLog
Source/WebCore/Modules/mediastream/MediaDevices.cpp
Source/WebCore/Modules/mediastream/MediaDevices.h
Source/WebCore/platform/mediastream/MediaStreamRequest.h
Source/WebCore/testing/Internals.cpp
Source/WebCore/testing/Internals.h
Source/WebCore/testing/Internals.idl
Source/WebKit/ChangeLog
Source/WebKit/UIProcess/UserMediaPermissionRequestManagerProxy.cpp
Source/WebKit/UIProcess/UserMediaPermissionRequestProxy.h
Tools/ChangeLog
Tools/TestWebKitAPI/Tests/WebKit/getDisplayMedia.html
Tools/TestWebKitAPI/Tests/WebKitCocoa/GetDisplayMedia.mm

index 241812e..4cd9be8 100644 (file)
@@ -1,5 +1,26 @@
 2019-11-05  youenn fablet  <youenn@apple.com>
 
+        Enforce user gesture for getUserMedia in case a previous getUserMedia call was denied
+        https://bugs.webkit.org/show_bug.cgi?id=203362
+        <rdar://problem/56648232>
+
+        Reviewed by Eric Carlson.
+
+        * fast/mediastream/constraint-intrinsic-size.html:
+        * fast/mediastream/get-display-media-muted.html:
+        * fast/mediastream/getUserMedia-deny-persistency5-expected.txt:
+        * fast/mediastream/getUserMedia-deny-persistency5.html:
+        * fast/mediastream/media-stream-page-muted.html:
+        Use user gesture simulation instead of disabling user gesture check.
+        * fast/mediastream/screencapture-user-gesture.html:
+        * fast/mediastream/screencapture-user-gesture-expected.txt:
+        * http/tests/media/media-stream/get-display-media-iframe-allow-attribute-expected.txt:
+        * http/tests/media/media-stream/get-display-media-prompt.html:
+        * http/tests/media/media-stream/resources/get-display-media-devices-iframe.html:
+        * resources/testharnessreport.js:
+
+2019-11-05  youenn fablet  <youenn@apple.com>
+
         LayoutTest webrtc/captureCanvas-webrtc.html is flaky
         https://bugs.webkit.org/show_bug.cgi?id=181835
 
index b8b6075..161d64c 100644 (file)
                 window.internals.settings.setScreenCaptureEnabled(true);
             }
 
+            function callGetDisplayMedia(options)
+            {
+                let promise;
+                window.internals.withUserGesture(() => {
+                    promise = navigator.mediaDevices.getDisplayMedia(options);
+                });
+                return promise;
+            }
+
             let defaultWidth;
             let defaultHeight;
             promise_test(async () => {
-                stream = await navigator.mediaDevices.getDisplayMedia({ video: true });
+                stream = await callGetDisplayMedia({ video: true });
                 let settings = stream.getVideoTracks()[0].getSettings();
                 defaultWidth = settings.width;
                 defaultHeight = settings.height;
             }, "setup");
             
             promise_test((test) => {
-                return navigator.mediaDevices.getDisplayMedia({ video: { width: {ideal: 640} } })
+                return callGetDisplayMedia({ video: { width: {ideal: 640} } })
                     .then((stream) => {
                         let settings = stream.getVideoTracks()[0].getSettings()
                         assert_equals(settings.height, Math.floor(640 * (defaultHeight / defaultWidth)));
@@ -35,7 +44,7 @@
 
             promise_test((test) => {
 
-                return navigator.mediaDevices.getDisplayMedia({ video: { height: {ideal: 240} } })
+                return callGetDisplayMedia({ video: { height: {ideal: 240} } })
                     .then((stream) => {
                         let settings = stream.getVideoTracks()[0].getSettings()
                         assert_equals(settings.width, Math.floor(240 * (defaultWidth / defaultHeight)));
@@ -44,4 +53,4 @@
 
         </script>
     </body>
-</html>
\ No newline at end of file
+</html>
index bcf3e67..a01b29e 100644 (file)
     if (window.testRunner)
         testRunner.setUserMediaPermission(true);
 
+    function callGetDisplayMedia(options)
+    {
+        let promise;
+        window.internals.withUserGesture(() => {
+            promise = navigator.mediaDevices.getDisplayMedia(options);
+        });
+        return promise;
+    }
+
     async function waitForPageStateChange(numberOfTries, originalState)
     {
         return new Promise(async (resolve) => {
@@ -29,7 +38,7 @@
 
     promise_test(async (test) => {
         await new Promise(async (resolve, reject) => {
-            let stream = await navigator.mediaDevices.getDisplayMedia({ video: true });
+            let stream = await callGetDisplayMedia({ video: true });
             let pageMediaState = internals.pageMediaState();
 
             assert_false(pageMediaState.includes('HasMutedDisplayCaptureDevice'), 'page state does not include HasMutedDisplayCaptureDevice');
diff --git a/LayoutTests/fast/mediastream/getUserMedia-deny-persistency5-expected.txt b/LayoutTests/fast/mediastream/getUserMedia-deny-persistency5-expected.txt
new file mode 100644 (file)
index 0000000..32b0afe
--- /dev/null
@@ -0,0 +1,3 @@
+
+PASS Testing getUserMedia with and without user gesture after user denied access 
+
diff --git a/LayoutTests/fast/mediastream/getUserMedia-deny-persistency5.html b/LayoutTests/fast/mediastream/getUserMedia-deny-persistency5.html
new file mode 100644 (file)
index 0000000..359f2bf
--- /dev/null
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+    <head>
+        <script src="../../resources/testharness.js"></script>
+        <script src="../../resources/testharnessreport.js"></script>
+    </head>
+    <body>
+        <script>
+promise_test(async (test) => {
+    if (window.testRunner)
+        testRunner.setUserMediaPermission(false);
+
+    await navigator.mediaDevices.getUserMedia({audio:false, video:true}).then(assert_unreached, (e) => { });
+    await navigator.mediaDevices.getUserMedia({audio:true, video:false}).then(assert_unreached, (e) => { });
+    await navigator.mediaDevices.getUserMedia({audio:true, video:true}).then(assert_unreached, (e) => { });
+
+    if (window.testRunner)
+        testRunner.setUserMediaPermission(true);
+
+    await navigator.mediaDevices.getUserMedia({audio:false, video:true}).then(assert_unreached, (e) => {
+        assert_equals(e.name, "NotAllowedError");
+    });
+
+    let promise;
+    internals.withUserGesture(() => {
+        promise = navigator.mediaDevices.getUserMedia({audio:false, video:true});
+    });
+    await promise;
+    internals.withUserGesture(() => {
+        promise = navigator.mediaDevices.getUserMedia({audio:true, video:false});
+    });
+    await promise;
+
+    internals.withUserGesture(() => {
+        promise = navigator.mediaDevices.getUserMedia({audio:true, video:true});
+    });
+    await promise;
+
+    await navigator.mediaDevices.getUserMedia({audio:false, video:true}).then(assert_unreached, (e) => { });
+    await navigator.mediaDevices.getUserMedia({audio:true, video:false}).then(assert_unreached, (e) => { });
+    await navigator.mediaDevices.getUserMedia({audio:true, video:true}).then(assert_unreached, (e) => { });
+}, "Testing getUserMedia with and without user gesture after user denied access");
+        </script>
+    </body>
+</html>
index 9deb9fd..8b1438b 100644 (file)
@@ -4,9 +4,6 @@
     <title>mediastream page muted</title>
     <script src="../../resources/js-test-pre.js"></script>
     <script>
-        if (window.internals)
-            internals.setDisableGetDisplayMediaUserGestureConstraint(true);
-
         async function checkPageState(activeState, inactiveState) {
             await new Promise((resolve, reject) => {
                 let retryCount = 0;
             });
         }
 
-        async function createScreenCaptureStream() {
+        function createScreenCaptureStream() {
+            let resolveCallback, rejectCallback;
+            let promise = new Promise((resolve, reject) => {
+                resolveCallback = resolve;
+                rejectCallback = reject;
+            });
             debug("<br>*** Creating screen capture stream");
-                       displayStream = await navigator.mediaDevices.getDisplayMedia({ video: true });
-            shouldBeType("displayStream", "Object");
-            shouldBe("displayStream.getTracks().length", "1");
-            shouldBe("displayStream.getVideoTracks().length", "1");
-            screenCaptureTrack = displayStream.getVideoTracks()[0];
-            shouldBeFalse("screenCaptureTrack.muted");
+            window.internals.withUserGesture(async () => {
+                try {
+                    displayStream = await navigator.mediaDevices.getDisplayMedia({ video: true });
+                    shouldBeType("displayStream", "Object");
+                    shouldBe("displayStream.getTracks().length", "1");
+                    shouldBe("displayStream.getVideoTracks().length", "1");
+                    screenCaptureTrack = displayStream.getVideoTracks()[0];
+                    shouldBeFalse("screenCaptureTrack.muted");
+                    resolveCallback();
+                } catch(e) {
+                    rejectCallback(e);
+                }
+            });
+            return promise;
         }
 
         async function createCameraMicrophoneStream() {
index e38d80e..af812d4 100644 (file)
@@ -1,4 +1,5 @@
 
 PASS Allow getDisplayMedia call in case of user gesture 
+PASS Disallow getDisplayMedia calls in case of user gesture if not the first call 
 PASS Deny getDisplayMedia call if no user gesture 
 
index 4a45c6b..952d6e4 100644 (file)
@@ -1,9 +1,6 @@
 <script src="../../resources/testharness.js"></script>
 <script src="../../resources/testharnessreport.js"></script>
 <script>
-if (window.internals)
-    internals.setDisableGetDisplayMediaUserGestureConstraint(false);
-
 promise_test(() => {
     let promise;
     internals.withUserGesture(() => {
@@ -12,6 +9,21 @@ promise_test(() => {
     return promise;
 }, "Allow getDisplayMedia call in case of user gesture");
 
+promise_test(() => {
+    let promise;
+    internals.withUserGesture(() => {
+        const promise1 = navigator.mediaDevices.getDisplayMedia({video : true});
+        const promise2 = navigator.mediaDevices.getDisplayMedia({video : true}).then(() => {
+            return Promise.reject("Second promise should reject");
+        }, () => {
+            return "Second promise rejected";
+        });
+        promise = Promise.all([promise1, promise2]);
+    });
+    return promise;
+}, "Disallow getDisplayMedia calls in case of user gesture if not the first call");
+
+
 promise_test((test) => {
     return promise_rejects(test, "InvalidAccessError", navigator.mediaDevices.getDisplayMedia({video : true}));
 }, "Deny getDisplayMedia call if no user gesture");
index f720ad7..9eeb0a7 100644 (file)
@@ -1,6 +1,6 @@
-CONSOLE MESSAGE: line 8: Trying to call getDisplayMedia from a frame without correct 'allow' attribute.
-CONSOLE MESSAGE: line 8: Trying to call getDisplayMedia from a frame without correct 'allow' attribute.
-CONSOLE MESSAGE: line 8: Trying to call getDisplayMedia from a frame without correct 'allow' attribute.
+CONSOLE MESSAGE: line 6: Trying to call getDisplayMedia from a frame without correct 'allow' attribute.
+CONSOLE MESSAGE: line 6: Trying to call getDisplayMedia from a frame without correct 'allow' attribute.
+CONSOLE MESSAGE: line 6: Trying to call getDisplayMedia from a frame without correct 'allow' attribute.
      
 
 PASS: <iframe allow=''> got "deny"
index 4fdd059..22348e3 100644 (file)
@@ -9,19 +9,25 @@
     <div id="console"></div>
 
 <script>
-    if (window.internals)
-        internals.setDisableGetDisplayMediaUserGestureConstraint(true);
-
     let stream;
     let err;
     
+    function callGetDisplayMedia(options)
+    {
+        let promise;
+        window.internals.withUserGesture(() => {
+            promise = navigator.mediaDevices.getDisplayMedia(options);
+        });
+        return promise;
+    }
+
     function numberOfTimesGetUserMediaPromptHasBeenCalled() {
         return testRunner.userMediaPermissionRequestCountForOrigin(document.location.href, document.location.href);
     }
     
     async function promptForAudioOnly() {
         debug("<br>** Request an audio-only stream, the user should not be prompted **");
-        stream = await navigator.mediaDevices.getDisplayMedia({ audio: true })
+        stream = await callGetDisplayMedia({ audio: true })
             .catch((e) => { err = e; });
         shouldBe("numberOfTimesGetUserMediaPromptHasBeenCalled()", "0");
         shouldBeUndefined("stream");
@@ -31,7 +37,7 @@
 
     async function promptForVideoOnly() {
         debug("<br>** Request an video-only stream, the user should be prompted **");
-        stream = await navigator.mediaDevices.getDisplayMedia({ video: true });
+        stream = await callGetDisplayMedia({ video: true });
         shouldBe("numberOfTimesGetUserMediaPromptHasBeenCalled()", "1");
         shouldBe("stream.getAudioTracks().length", "0");
         shouldBe("stream.getVideoTracks().length", "1");
@@ -39,7 +45,7 @@
 
     async function promptForAudioAndVideo() {
         debug("<br>** Request a stream with audio and video, the user should be prompted but no audio track should be created **");
-        stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true });
+        stream = await callGetDisplayMedia({ video: true, audio: true });
         shouldBe("numberOfTimesGetUserMediaPromptHasBeenCalled()", "2");
         shouldBe("stream.getAudioTracks().length", "0");
         shouldBe("stream.getVideoTracks().length", "1");
@@ -49,7 +55,7 @@
         debug("<br>** Request a stream with 'max' constraints, the user should not be prompted **");
 
         stream = null;
-        stream = await navigator.mediaDevices.getDisplayMedia({ video: {width: {exact: 640}, height: {exact: 480}} })
+        stream = await callGetDisplayMedia({ video: {width: {exact: 640}, height: {exact: 480}} })
             .catch((e) => { err = e; });
         
         shouldBe("numberOfTimesGetUserMediaPromptHasBeenCalled()", "2");
@@ -62,7 +68,7 @@
         debug("<br>** Request a stream with 'min' constraints, the user should not be prompted **");
 
         stream = null;
-        stream = await navigator.mediaDevices.getDisplayMedia({ video: {width: {min: 640}, height: {min: 480}} })
+        stream = await callGetDisplayMedia({ video: {width: {min: 640}, height: {min: 480}} })
             .catch((e) => { err = e; });
         shouldBe("numberOfTimesGetUserMediaPromptHasBeenCalled()", "2");
         shouldBeUndefined("stream");
@@ -74,7 +80,7 @@
         debug("<br>** Request a stream with 'advanced' constraints, the user should not be prompted **");
 
         stream = null;
-        stream = await navigator.mediaDevices.getDisplayMedia({ video: { width: 640, height: 480, advanced: [ { width: 1920, height: 1280 } ] } })
+        stream = await callGetDisplayMedia({ video: { width: 640, height: 480, advanced: [ { width: 1920, height: 1280 } ] } })
             .catch((e) => { err = e; });
         shouldBe("numberOfTimesGetUserMediaPromptHasBeenCalled()", "2");
         shouldBeUndefined("stream");
@@ -86,7 +92,7 @@
         debug("<br>** Request a stream with valid constraints, the user should be prompted **");
 
         stream = null;
-        stream = await navigator.mediaDevices.getDisplayMedia({ video: {width: 640, height: 480} })
+        stream = await callGetDisplayMedia({ video: {width: 640, height: 480} })
             .catch((e) => { err = e; });
         
         shouldBe("numberOfTimesGetUserMediaPromptHasBeenCalled()", "3");
         debug("<br>** Request a stream with an exact audio constraint, it should be ignored **");
 
         stream = null;
-        stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: { volume: { exact: 0.5 } } });
+        stream = await callGetDisplayMedia({ video: true, audio: { volume: { exact: 0.5 } } });
         shouldBe("numberOfTimesGetUserMediaPromptHasBeenCalled()", "4");
         shouldBe("stream.getAudioTracks().length", "0");
         shouldBe("stream.getVideoTracks().length", "1");
index 9e33b66..c8d4cba 100644 (file)
@@ -1,11 +1,17 @@
 <script>
-    if (window.internals)
-        internals.setDisableGetDisplayMediaUserGestureConstraint(true);
+    function callGetDisplayMedia(options)
+    {
+        let promise;
+        window.internals.withUserGesture(() => {
+            promise = navigator.mediaDevices.getDisplayMedia(options);
+        });
+        return promise;
+    }
 
     async function enumerate(event)
     {
         let result;
-        await navigator.mediaDevices.getDisplayMedia({video: true})
+        await callGetDisplayMedia({video: true})
             .then((s) => result = "allow")
             .catch((e) => result = "deny");
         parent.postMessage(`${event.data}:${result}`, '*');
index f8aa8cb..6683428 100644 (file)
@@ -32,9 +32,6 @@ if (self.testRunner) {
        testRunner.setStatisticsShouldDowngradeReferrer(false, function() { });
 }
 
-if (self.internals && internals.setDisableGetDisplayMediaUserGestureConstraint)
-    internals.setDisableGetDisplayMediaUserGestureConstraint(true);
-
 if (self.internals && internals.setICECandidateFiltering)
     internals.setICECandidateFiltering(false);
 
index c821f23..71134ca 100644 (file)
@@ -1,3 +1,36 @@
+2019-11-05  youenn fablet  <youenn@apple.com>
+
+        Enforce user gesture for getUserMedia in case a previous getUserMedia call was denied
+        https://bugs.webkit.org/show_bug.cgi?id=203362
+
+        Reviewed by Eric Carlson.
+
+        Compute whether a media request is user priviledged or not.
+        It is priviledged if it is created as part of a user gesture and no request of the same type
+        has been previously created for the same user gesture.
+        If getDisplayMedia is called twice as part of a single user gesture, getDisplaMedia will reject for the second call.
+
+        Remove the internal ability to disable user gesture check.
+        Instead use internals API to simulate a user gesture.       
+
+        Test: fast/mediastream/getUserMedia-deny-persistency5.html and updated test.
+
+        * Modules/mediastream/MediaDevices.cpp:
+        (WebCore::MediaDevices::computeUserGesturePriviledge):
+        (WebCore::MediaDevices::getUserMedia):
+        (WebCore::MediaDevices::getDisplayMedia):
+        (WebCore::MediaDevices::getUserMedia const): Deleted.
+        (WebCore::MediaDevices::getDisplayMedia const): Deleted.
+        * Modules/mediastream/MediaDevices.h:
+        * platform/mediastream/MediaStreamRequest.h:
+        (WebCore::MediaStreamRequest::encode const):
+        (WebCore::MediaStreamRequest::decode):
+        * testing/Internals.cpp:
+        (WebCore::Internals::setMediaStreamSourceInterrupted):
+        (WebCore::Internals::setDisableGetDisplayMediaUserGestureConstraint): Deleted.
+        * testing/Internals.h:
+        * testing/Internals.idl:
+
 2019-11-05  Carlos Garcia Campos  <cgarcia@igalia.com>
 
         [FreeType] Too slow running encoding/legacy-mb-korean/euc-kr WPT tests
index 65621d9..d85ed0c 100644 (file)
@@ -41,6 +41,7 @@
 #include "JSMediaDeviceInfo.h"
 #include "MediaTrackSupportedConstraints.h"
 #include "RealtimeMediaSourceSettings.h"
+#include "UserGestureIndicator.h"
 #include "UserMediaController.h"
 #include "UserMediaRequest.h"
 #include <wtf/IsoMallocInlines.h>
@@ -104,7 +105,20 @@ static MediaConstraints createMediaConstraints(const Variant<bool, MediaTrackCon
     );
 }
 
-void MediaDevices::getUserMedia(const StreamConstraints& constraints, Promise&& promise) const
+bool MediaDevices::computeUserGesturePriviledge(GestureAllowedRequest requestType)
+{
+    auto* currentGestureToken = UserGestureIndicator::currentUserGesture().get();
+    if (m_currentGestureToken != currentGestureToken) {
+        m_currentGestureToken = currentGestureToken;
+        m_requestTypesForCurrentGesture = { };
+    }
+
+    bool isUserGesturePriviledged = m_currentGestureToken && !m_requestTypesForCurrentGesture.contains(requestType);
+    m_requestTypesForCurrentGesture.add(requestType);
+    return isUserGesturePriviledged;
+}
+
+void MediaDevices::getUserMedia(const StreamConstraints& constraints, Promise&& promise)
 {
     auto* document = this->document();
     if (!document || !document->isFullyActive()) {
@@ -114,25 +128,34 @@ void MediaDevices::getUserMedia(const StreamConstraints& constraints, Promise&&
 
     auto audioConstraints = createMediaConstraints(constraints.audio);
     auto videoConstraints = createMediaConstraints(constraints.video);
-    if (videoConstraints.isValid)
+
+    bool isUserGesturePriviledged = false;
+
+    if (audioConstraints.isValid)
+        isUserGesturePriviledged |= computeUserGesturePriviledge(GestureAllowedRequest::Microphone);
+
+    if (videoConstraints.isValid) {
+        isUserGesturePriviledged |= computeUserGesturePriviledge(GestureAllowedRequest::Camera);
         videoConstraints.setDefaultVideoConstraints();
+    }
 
-    auto request = UserMediaRequest::create(*document, { MediaStreamRequest::Type::UserMedia, WTFMove(audioConstraints), WTFMove(videoConstraints) }, WTFMove(promise));
+    auto request = UserMediaRequest::create(*document, { MediaStreamRequest::Type::UserMedia, WTFMove(audioConstraints), WTFMove(videoConstraints), isUserGesturePriviledged }, WTFMove(promise));
     request->start();
 }
 
-void MediaDevices::getDisplayMedia(const StreamConstraints& constraints, Promise&& promise) const
+void MediaDevices::getDisplayMedia(const StreamConstraints& constraints, Promise&& promise)
 {
     auto* document = this->document();
     if (!document)
         return;
 
-    if (!m_disableGetDisplayMediaUserGestureConstraint && !UserGestureIndicator::processingUserGesture()) {
+    bool isUserGesturePriviledged = computeUserGesturePriviledge(GestureAllowedRequest::Display);
+    if (!isUserGesturePriviledged) {
         promise.reject(Exception { InvalidAccessError, "getDisplayMedia must be called from a user gesture handler."_s });
         return;
     }
 
-    auto request = UserMediaRequest::create(*document, { MediaStreamRequest::Type::DisplayMedia, { }, createMediaConstraints(constraints.video) }, WTFMove(promise));
+    auto request = UserMediaRequest::create(*document, { MediaStreamRequest::Type::DisplayMedia, { }, createMediaConstraints(constraints.video), isUserGesturePriviledged }, WTFMove(promise));
     request->start();
 }
 
index 5395eb8..9b6787c 100644 (file)
@@ -49,6 +49,7 @@ namespace WebCore {
 class Document;
 class MediaDeviceInfo;
 class MediaStream;
+class UserGestureToken;
 
 struct MediaTrackSupportedConstraints;
 
@@ -77,16 +78,14 @@ public:
         Variant<bool, MediaTrackConstraints> video;
         Variant<bool, MediaTrackConstraints> audio;
     };
-    void getUserMedia(const StreamConstraints&, Promise&&) const;
-    void getDisplayMedia(const StreamConstraints&, Promise&&) const;
+    void getUserMedia(const StreamConstraints&, Promise&&);
+    void getDisplayMedia(const StreamConstraints&, Promise&&);
     void enumerateDevices(EnumerateDevicesPromise&&);
     MediaTrackSupportedConstraints getSupportedConstraints();
 
     using RefCounted<MediaDevices>::ref;
     using RefCounted<MediaDevices>::deref;
 
-    void setDisableGetDisplayMediaUserGestureConstraint(bool value) { m_disableGetDisplayMediaUserGestureConstraint = value; }
-
 private:
     explicit MediaDevices(Document&);
 
@@ -109,15 +108,24 @@ private:
     void refEventTarget() final { ref(); }
     void derefEventTarget() final { deref(); }
 
+    enum class GestureAllowedRequest {
+        Microphone = 1 << 0,
+        Camera = 1 << 1,
+        Display = 1 << 2,
+    };
+    bool computeUserGesturePriviledge(GestureAllowedRequest);
+
     Timer m_scheduledEventTimer;
     UserMediaClient::DeviceChangeObserverToken m_deviceChangeToken;
     const EventNames& m_eventNames; // Need to cache this so we can use it from GC threads.
     bool m_listeningForDeviceChanges { false };
-    bool m_disableGetDisplayMediaUserGestureConstraint { false };
 
     Vector<Ref<MediaDeviceInfo>> m_devices;
     bool m_canAccessCamera { false };
     bool m_canAccessMicrophone { false };
+
+    OptionSet<GestureAllowedRequest> m_requestTypesForCurrentGesture;
+    UserGestureToken* m_currentGestureToken { nullptr };
 };
 
 } // namespace WebCore
index 9a72701..004582f 100644 (file)
@@ -33,9 +33,10 @@ namespace WebCore {
 
 struct MediaStreamRequest {
     enum class Type { UserMedia, DisplayMedia };
-    Type type;
+    Type type { Type::UserMedia };
     MediaConstraints audioConstraints;
     MediaConstraints videoConstraints;
+    bool isUserGesturePriviledged { false };
 
     template<class Encoder>
     void encode(Encoder& encoder) const
@@ -43,12 +44,13 @@ struct MediaStreamRequest {
         encoder.encodeEnum(type);
         encoder << audioConstraints;
         encoder << videoConstraints;
+        encoder << isUserGesturePriviledged;
     }
 
     template <class Decoder> static Optional<MediaStreamRequest> decode(Decoder& decoder)
     {
         MediaStreamRequest request;
-        if (decoder.decodeEnum(request.type) && decoder.decode(request.audioConstraints) && decoder.decode(request.videoConstraints))
+        if (decoder.decodeEnum(request.type) && decoder.decode(request.audioConstraints) && decoder.decode(request.videoConstraints) && decoder.decode(request.isUserGesturePriviledged))
             return request;
 
         return WTF::nullopt;
index 1bafc66..a74b4fc 100644 (file)
@@ -4871,16 +4871,6 @@ void Internals::setMediaStreamSourceInterrupted(MediaStreamTrack& track, bool in
 {
     track.source().setInterruptedForTesting(interrupted);
 }
-
-void Internals::setDisableGetDisplayMediaUserGestureConstraint(bool value)
-{
-    Document* document = contextDocument();
-    if (!document || !document->domWindow())
-        return;
-
-    if (auto* mediaDevices = NavigatorMediaDevices::mediaDevices(document->domWindow()->navigator()))
-        mediaDevices->setDisableGetDisplayMediaUserGestureConstraint(value);
-}
 #endif
 
 bool Internals::supportsAudioSession() const
index 0ba066b..78627d6 100644 (file)
@@ -744,7 +744,6 @@ public:
     void simulateMediaStreamTrackCaptureSourceFailure(MediaStreamTrack&);
     void setMediaStreamTrackIdentifier(MediaStreamTrack&, String&& id);
     void setMediaStreamSourceInterrupted(MediaStreamTrack&, bool);
-    void setDisableGetDisplayMediaUserGestureConstraint(bool);
 #endif
 
     bool supportsAudioSession() const;
index d2f9b0b..33f4865 100644 (file)
@@ -726,7 +726,6 @@ enum CompositingPolicy {
     [Conditional=MEDIA_STREAM] void simulateMediaStreamTrackCaptureSourceFailure(MediaStreamTrack track);
     [Conditional=MEDIA_STREAM] void setMediaStreamTrackIdentifier(MediaStreamTrack track, DOMString identifier);
     [Conditional=MEDIA_STREAM] void setMediaStreamSourceInterrupted(MediaStreamTrack track, boolean interrupted);
-    [Conditional=MEDIA_STREAM] void setDisableGetDisplayMediaUserGestureConstraint(boolean value);
 
     unsigned long long documentIdentifier(Document document);
     boolean isDocumentAlive(unsigned long long documentIdentifier);
index 0a0aa08..f14c596 100644 (file)
@@ -1,3 +1,17 @@
+2019-11-05  youenn fablet  <youenn@apple.com>
+
+        Enforce user gesture for getUserMedia in case a previous getUserMedia call was denied
+        https://bugs.webkit.org/show_bug.cgi?id=203362
+
+        Reviewed by Eric Carlson.
+
+        In case the request has user gesture priviledge, do not look at denied request history.
+
+        * UIProcess/UserMediaPermissionRequestManagerProxy.cpp:
+        (WebKit::UserMediaPermissionRequestManagerProxy::getRequestAction):
+        * UIProcess/UserMediaPermissionRequestProxy.h:
+        (WebKit::UserMediaPermissionRequestProxy::isUserGesturePriviledged const):
+
 2019-11-04  Jiewen Tan  <jiewen_tan@apple.com>
 
         [WebAuthn] Guard against unexpected -[_WKWebAuthenticationPanel cancel]
index 13b520a..5a8947a 100644 (file)
@@ -345,7 +345,7 @@ UserMediaPermissionRequestManagerProxy::RequestAction UserMediaPermissionRequest
     ASSERT(!(requestingScreenCapture && !request.hasVideoDevice()));
     ASSERT(!(requestingScreenCapture && requestingMicrophone));
 
-    if (wasRequestDenied(request.frameID(), request.userMediaDocumentSecurityOrigin(), request.topLevelDocumentSecurityOrigin(), requestingMicrophone, requestingCamera, requestingScreenCapture))
+    if (!request.isUserGesturePriviledged() && wasRequestDenied(request.frameID(), request.userMediaDocumentSecurityOrigin(), request.topLevelDocumentSecurityOrigin(), requestingMicrophone, requestingCamera, requestingScreenCapture))
         return RequestAction::Deny;
 
     if (request.requestType() == MediaStreamRequest::Type::DisplayMedia)
index ba4e0a3..7d82497 100644 (file)
@@ -84,6 +84,8 @@ public:
     WebCore::CaptureDevice audioDevice() const { return m_eligibleAudioDevices.isEmpty() ? WebCore::CaptureDevice { } : m_eligibleAudioDevices[0]; }
     WebCore::CaptureDevice videoDevice() const { return m_eligibleVideoDevices.isEmpty() ? WebCore::CaptureDevice { } : m_eligibleVideoDevices[0]; }
 
+    bool isUserGesturePriviledged() const { return m_request.isUserGesturePriviledged; }
+
 private:
     UserMediaPermissionRequestProxy(UserMediaPermissionRequestManagerProxy&, uint64_t userMediaID, WebCore::FrameIdentifier mainFrameID, WebCore::FrameIdentifier, Ref<WebCore::SecurityOrigin>&& userMediaDocumentOrigin, Ref<WebCore::SecurityOrigin>&& topLevelDocumentOrigin, Vector<WebCore::CaptureDevice>&& audioDevices, Vector<WebCore::CaptureDevice>&& videoDevices, WebCore::MediaStreamRequest&&);
 
index 48b99af..e361b38 100644 (file)
@@ -1,3 +1,16 @@
+2019-11-05  youenn fablet  <youenn@apple.com>
+
+        Enforce user gesture for getUserMedia in case a previous getUserMedia call was denied
+        https://bugs.webkit.org/show_bug.cgi?id=203362
+
+        Reviewed by Eric Carlson.
+
+        * TestWebKitAPI/Tests/WebKitCocoa/GetDisplayMedia.mm:
+        (TestWebKitAPI::TEST_F):
+        Update test to take into account the ability to ask again for permission.
+        * TestWebKitAPI/Tests/WebKit/getDisplayMedia.html:
+        Update to make sure we notify test if internals is not available.
+
 2019-11-04  Aakash Jain  <aakash_jain@apple.com>
 
         [ews] Perform validation of patch before retrying API and layout tests
index c4468c4..00f9a34 100644 (file)
@@ -2,12 +2,16 @@
 <html>
     <head>
         <script>
-
             let stream = null;
 
             function promptForCapture(constraints)
             {
-                window.internals.withUserGesture(async () => {
+                if (!window.internals) {
+                    window.webkit.messageHandlers.testHandler.postMessage('test requires internals');
+                    return;
+                }
+
+                window.internals.withUserGesture(() => {
                     navigator.mediaDevices.getDisplayMedia(constraints)
                     .then((s) => {
                         stream = s;
index 41a5e6f..95adca9 100644 (file)
@@ -180,8 +180,7 @@ TEST_F(GetDisplayMediaTest, PromptOnceAfterDenial)
     shouldDeny = true;
     promptForCapture(@"{ video: true }", false);
     shouldDeny = false;
-    promptForCapture(@"{ video: true }", false);
-    promptForCapture(@"{ video: true }", false);
+    promptForCapture(@"{ video: true }", true);
 }
 
 } // namespace TestWebKitAPI