[MediaStream] enumerateDevices should not expose devices that are not available to...
authoreric.carlson@apple.com <eric.carlson@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sat, 3 Nov 2018 13:30:30 +0000 (13:30 +0000)
committereric.carlson@apple.com <eric.carlson@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sat, 3 Nov 2018 13:30:30 +0000 (13:30 +0000)
https://bugs.webkit.org/show_bug.cgi?id=191177
<rdar://problem/45747873>

Reviewed by Jer Noble.

Source/WebCore:

Test: http/tests/media/media-stream/enumerate-devices-iframe-allow-attribute.html

* Modules/mediastream/MediaDevicesRequest.cpp:
(WebCore::MediaDevicesRequest::start): Only expose devices that are available to gUM.

* Modules/mediastream/UserMediaController.cpp:
(WebCore::isSecure): Moved from UserMediaRequest.cpp.
(WebCore::isAllowedToUse): Ditto.
(WebCore::UserMediaController::canCallGetUserMedia): Modified from UserMediaRequest.cpp.
(WebCore::UserMediaController::logGetUserMediaDenial): Log reason for denial.
* Modules/mediastream/UserMediaController.h:

* Modules/mediastream/UserMediaRequest.cpp:
(WebCore::UserMediaRequest::start): Use UserMediaController::canCallGetUserMedia.
(WebCore::isSecure): Deleted.
(WebCore::isAllowedToUse): Deleted.
(WebCore::canCallGetUserMedia): Deleted.

LayoutTests:

* http/tests/media/media-stream/enumerate-devices-iframe-allow-attribute-expected.txt: Added.
* http/tests/media/media-stream/enumerate-devices-iframe-allow-attribute.html: Added.
* http/tests/media/media-stream/resources/enumerate-devices-iframe.html: Added.
* http/tests/ssl/media-stream/get-user-media-different-host-expected.txt: Rebased for updated logging.
* http/tests/ssl/media-stream/get-user-media-nested-expected.txt: Ditto.

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

12 files changed:
LayoutTests/ChangeLog
LayoutTests/http/tests/media/media-stream/enumerate-devices-iframe-allow-attribute-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/media/media-stream/enumerate-devices-iframe-allow-attribute.html [new file with mode: 0644]
LayoutTests/http/tests/media/media-stream/resources/enumerate-devices-iframe.html [new file with mode: 0644]
LayoutTests/http/tests/ssl/media-stream/get-user-media-different-host-expected.txt
LayoutTests/http/tests/ssl/media-stream/get-user-media-nested-expected.txt
LayoutTests/imported/w3c/web-platform-tests/mediacapture-streams/MediaStream-default-feature-policy.https-expected.txt
Source/WebCore/ChangeLog
Source/WebCore/Modules/mediastream/MediaDevicesRequest.cpp
Source/WebCore/Modules/mediastream/UserMediaController.cpp
Source/WebCore/Modules/mediastream/UserMediaController.h
Source/WebCore/Modules/mediastream/UserMediaRequest.cpp

index a39f5f6..092e98d 100644 (file)
@@ -1,3 +1,17 @@
+2018-11-03  Eric Carlson  <eric.carlson@apple.com>
+
+        [MediaStream] enumerateDevices should not expose devices that are not available to getUserMedia
+        https://bugs.webkit.org/show_bug.cgi?id=191177
+        <rdar://problem/45747873>
+
+        Reviewed by Jer Noble.
+
+        * http/tests/media/media-stream/enumerate-devices-iframe-allow-attribute-expected.txt: Added.
+        * http/tests/media/media-stream/enumerate-devices-iframe-allow-attribute.html: Added.
+        * http/tests/media/media-stream/resources/enumerate-devices-iframe.html: Added.
+        * http/tests/ssl/media-stream/get-user-media-different-host-expected.txt: Rebased for updated logging.
+        * http/tests/ssl/media-stream/get-user-media-nested-expected.txt: Ditto.
+
 2018-11-02  Justin Michaud  <justin_michaud@apple.com>
 
         Add new global object and preliminary Worklets support for CSS painting api
diff --git a/LayoutTests/http/tests/media/media-stream/enumerate-devices-iframe-allow-attribute-expected.txt b/LayoutTests/http/tests/media/media-stream/enumerate-devices-iframe-allow-attribute-expected.txt
new file mode 100644 (file)
index 0000000..48986bb
--- /dev/null
@@ -0,0 +1,3 @@
+CONSOLE MESSAGE: line 4: The top-level frame has prevented a document with a different security origin from calling enumerateDevices.
+      
+PASS: "allow" attribute respected in all iframes
diff --git a/LayoutTests/http/tests/media/media-stream/enumerate-devices-iframe-allow-attribute.html b/LayoutTests/http/tests/media/media-stream/enumerate-devices-iframe-allow-attribute.html
new file mode 100644 (file)
index 0000000..63fd035
--- /dev/null
@@ -0,0 +1,49 @@
+<!doctype html>
+<html>
+<script src="../../../../resources/js-test-pre.js"></script>
+<body>
+    <iframe id=none src="http://localhost:8000/media/media-stream/resources/enumerate-devices-iframe.html"></iframe>
+    <iframe id="microphone" allow="microphone" src="http://localhost:8000/media/media-stream/resources/enumerate-devices-iframe.html"></iframe>
+    <iframe id="camera" allow="camera" src="http://localhost:8000/media/media-stream/resources/enumerate-devices-iframe.html"></iframe>
+    <iframe id="camera+microphone" allow="camera;microphone" src="http://localhost:8000/media/media-stream/resources/enumerate-devices-iframe.html"></iframe>
+    <iframe id="same-origin" src="http://127.0.0.1:8000/media/media-stream/resources/enumerate-devices-iframe.html"></iframe>
+
+    <script>
+
+        if (window.testRunner) {
+            testRunner.dumpAsText();
+            testRunner.waitUntilDone();
+            testRunner.setUserMediaPermission(true);
+        }
+
+        let expected = {
+            'microphone' : 'microphone',
+            'none' : '',
+            'camera+microphone' : 'camera+microphone',
+            'camera' : 'camera',
+            'same-origin' : 'camera+microphone'
+        };
+        let count = 0;
+        let success = true;
+        window.addEventListener("message", (event) => {
+            let visible = event.data.trim().split(':');
+            if (expected[visible[0]] != visible[1]) {
+                result.innerHTML += `FAIL: &LT;iframe id=${visible[0]}> saw "${visible[1]}", should have seen "${expected[visible[0]]}"<br>`;
+                success = false;
+            }
+            if (++count == 5) {
+                if (success)
+                    result.innerHTML = 'PASS: "allow" attribute respected in all iframes';
+                if (window.testRunner)
+                    testRunner.notifyDone();
+            }
+            
+        }, false);
+
+        let enumerate = (evt) => { evt.target.contentWindow.postMessage(evt.target.id, '*'); }
+        Array.from(document.getElementsByTagName('iframe')).forEach(element => element.onload = enumerate);
+    </script>
+
+    <div id="result"></div>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/media/media-stream/resources/enumerate-devices-iframe.html b/LayoutTests/http/tests/media/media-stream/resources/enumerate-devices-iframe.html
new file mode 100644 (file)
index 0000000..580c86f
--- /dev/null
@@ -0,0 +1,16 @@
+<script>
+    async function enumerate(event)
+    {
+        let devices = await navigator.mediaDevices.enumerateDevices();
+        let visible = devices.map(device => device.kind.indexOf('video') == 0 ? 'camera' : 'microphone').sort();
+        visible = visible.join('+');
+        parent.postMessage(`${event.data}:${visible}`, '*');
+        result.innerHTML = visible;
+    }
+
+    window.addEventListener("message", (id) => enumerate(id));
+</script>
+
+<div id='result'></div>
+
+
index 5326d22..356f1d6 100644 (file)
@@ -1,4 +1,4 @@
-CONSOLE MESSAGE: line 52: The top-level frame has prevented a document with a different security origin to call getUserMedia.
+CONSOLE MESSAGE: line 52: The top-level frame has prevented a document with a different security origin from calling getUserMedia.
 Tests that getUserMedia fails when the top level document and iframe do not have the same domain.
 
 On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
index b8811b2..ab8fb7c 100644 (file)
@@ -1,4 +1,4 @@
-CONSOLE MESSAGE: line 52: The top-level frame has prevented a document with a different security origin to call getUserMedia.
+CONSOLE MESSAGE: line 52: The top-level frame has prevented a document with a different security origin from calling getUserMedia.
 Tests that getUserMedia fails when the top level document and iframe do not have the same domain.
 
 On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
index e57491d..b1aaa6c 100644 (file)
@@ -1,10 +1,10 @@
-CONSOLE MESSAGE: line 16: The top-level frame has prevented a document with a different security origin to call getUserMedia.
-CONSOLE MESSAGE: line 16: The top-level frame has prevented a document with a different security origin to call getUserMedia.
-CONSOLE MESSAGE: line 16: The top-level frame has prevented a document with a different security origin to call getUserMedia.
-CONSOLE MESSAGE: line 16: The top-level frame has prevented a document with a different security origin to call getUserMedia.
-CONSOLE MESSAGE: line 16: The top-level frame has prevented a document with a different security origin to call getUserMedia.
-CONSOLE MESSAGE: line 16: The top-level frame has prevented a document with a different security origin to call getUserMedia.
-CONSOLE MESSAGE: line 16: The top-level frame has prevented a document with a different security origin to call getUserMedia.
+CONSOLE MESSAGE: line 16: The top-level frame has prevented a document with a different security origin from calling getUserMedia.
+CONSOLE MESSAGE: line 16: The top-level frame has prevented a document with a different security origin from calling getUserMedia.
+CONSOLE MESSAGE: line 16: The top-level frame has prevented a document with a different security origin from calling getUserMedia.
+CONSOLE MESSAGE: line 16: The top-level frame has prevented a document with a different security origin from calling getUserMedia.
+CONSOLE MESSAGE: line 16: The top-level frame has prevented a document with a different security origin from calling getUserMedia.
+CONSOLE MESSAGE: line 16: The top-level frame has prevented a document with a different security origin from calling getUserMedia.
+CONSOLE MESSAGE: line 16: The top-level frame has prevented a document with a different security origin from calling getUserMedia.
 
 
 PASS Default "microphone" feature policy ["self"] allows the top-level document. 
index a594b29..1d0b5e2 100644 (file)
@@ -1,3 +1,29 @@
+2018-11-03  Eric Carlson  <eric.carlson@apple.com>
+
+        [MediaStream] enumerateDevices should not expose devices that are not available to getUserMedia
+        https://bugs.webkit.org/show_bug.cgi?id=191177
+        <rdar://problem/45747873>
+
+        Reviewed by Jer Noble.
+
+        Test: http/tests/media/media-stream/enumerate-devices-iframe-allow-attribute.html
+
+        * Modules/mediastream/MediaDevicesRequest.cpp:
+        (WebCore::MediaDevicesRequest::start): Only expose devices that are available to gUM.
+
+        * Modules/mediastream/UserMediaController.cpp:
+        (WebCore::isSecure): Moved from UserMediaRequest.cpp.
+        (WebCore::isAllowedToUse): Ditto.
+        (WebCore::UserMediaController::canCallGetUserMedia): Modified from UserMediaRequest.cpp.
+        (WebCore::UserMediaController::logGetUserMediaDenial): Log reason for denial.
+        * Modules/mediastream/UserMediaController.h:
+
+        * Modules/mediastream/UserMediaRequest.cpp:
+        (WebCore::UserMediaRequest::start): Use UserMediaController::canCallGetUserMedia.
+        (WebCore::isSecure): Deleted.
+        (WebCore::isAllowedToUse): Deleted.
+        (WebCore::canCallGetUserMedia): Deleted.
+
 2018-11-02  Justin Michaud  <justin_michaud@apple.com>
 
         Add new global object and preliminary Worklets support for CSS painting api
index 203d240..37b288f 100644 (file)
@@ -103,8 +103,31 @@ void MediaDevicesRequest::filterDeviceList(Vector<Ref<MediaDeviceInfo>>& devices
 
 void MediaDevicesRequest::start()
 {
+    auto& document = downcast<Document>(*scriptExecutionContext());
+    auto* controller = UserMediaController::from(document.page());
+    if (!controller) {
+        callOnMainThread([protectedThis = makeRef(*this)]() {
+            protectedThis->m_promise.resolve({ });
+        });
+
+        return;
+    }
+
+    auto microphoneAccess = controller->canCallGetUserMedia(document, true, false);
+    auto cameraAccess = controller->canCallGetUserMedia(document, false, true);
+    bool canAccessMicrophone = microphoneAccess == UserMediaController::GetUserMediaAccess::CanCall;
+    bool canAccessCamera = cameraAccess == UserMediaController::GetUserMediaAccess::CanCall;
+    if (!canAccessMicrophone && !canAccessCamera) {
+        controller->logGetUserMediaDenial(document, !canAccessMicrophone ? microphoneAccess : cameraAccess, UserMediaController::BlockedCaller::EnumerateDevices);
+        callOnMainThread([protectedThis = makeRef(*this)]() {
+            protectedThis->m_promise.resolve({ });
+        });
+
+        return;
+    }
+
     // This lambda keeps |this| alive until the request completes or is canceled.
-    auto completion = [this, protectedThis = makeRef(*this)] (const Vector<CaptureDevice>& captureDevices, const String& deviceIdentifierHashSalt, bool originHasPersistentAccess) mutable {
+    auto completion = [this, protectedThis = makeRef(*this), canAccessMicrophone, canAccessCamera] (const Vector<CaptureDevice>& captureDevices, const String& deviceIdentifierHashSalt, bool originHasPersistentAccess) mutable {
 
         m_enumerationRequest = nullptr;
 
@@ -117,6 +140,11 @@ void MediaDevicesRequest::start()
         Vector<Ref<MediaDeviceInfo>> devices;
         bool revealIdsAndLabels = originHasPersistentAccess || document.hasHadCaptureMediaStreamTrack();
         for (auto& deviceInfo : captureDevices) {
+            if (!canAccessMicrophone && deviceInfo.type() == CaptureDevice::DeviceType::Microphone)
+                continue;
+            if (!canAccessCamera && deviceInfo.type() == CaptureDevice::DeviceType::Camera)
+                continue;
+
             auto label = emptyString();
             auto id = emptyString();
             auto groupId = emptyString();
@@ -140,7 +168,7 @@ void MediaDevicesRequest::start()
         });
     };
 
-    m_enumerationRequest = MediaDevicesEnumerationRequest::create(*downcast<Document>(scriptExecutionContext()), WTFMove(completion));
+    m_enumerationRequest = MediaDevicesEnumerationRequest::create(document, WTFMove(completion));
     m_enumerationRequest->start();
 }
 
index 8e1aba7..401b34a 100644 (file)
@@ -1,5 +1,6 @@
 /*
  * Copyright (C) 2012 Google Inc. All rights reserved.
+ * Copyright (C) 2013-2018 Apple Inc. All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions
 
 #if ENABLE(MEDIA_STREAM)
 
+#include "DOMWindow.h"
+#include "DeprecatedGlobalSettings.h"
+#include "Document.h"
+#include "DocumentLoader.h"
+#include "Frame.h"
+#include "HTMLIFrameElement.h"
+#include "HTMLParserIdioms.h"
+#include "SchemeRegistry.h"
 #include "UserMediaRequest.h"
 
 namespace WebCore {
@@ -51,6 +60,91 @@ void provideUserMediaTo(Page* page, UserMediaClient* client)
     UserMediaController::provideTo(page, UserMediaController::supplementName(), std::make_unique<UserMediaController>(client));
 }
 
+static bool isSecure(DocumentLoader& documentLoader)
+{
+    auto& response = documentLoader.response();
+    if (SecurityOrigin::isLocalHostOrLoopbackIPAddress(documentLoader.response().url().host()))
+        return true;
+    return SchemeRegistry::shouldTreatURLSchemeAsSecure(response.url().protocol().toStringWithoutCopying())
+        && response.certificateInfo()
+        && !response.certificateInfo()->containsNonRootSHA1SignedCertificate();
+}
+
+static bool isAllowedToUse(Document& document, Document& topDocument, bool requiresAudio, bool requiresVideo)
+{
+    if (&document == &topDocument)
+        return true;
+
+    auto* parentDocument = document.parentDocument();
+    if (!parentDocument)
+        return false;
+
+    if (document.securityOrigin().isSameSchemeHostPort(parentDocument->securityOrigin()))
+        return true;
+
+    auto* element = document.ownerElement();
+    ASSERT(element);
+    if (!element)
+        return false;
+
+    if (!is<HTMLIFrameElement>(*element))
+        return false;
+    auto& allow = downcast<HTMLIFrameElement>(*element).allow();
+
+    bool allowCameraAccess = false;
+    bool allowMicrophoneAccess = false;
+    for (auto allowItem : StringView { allow }.split(';')) {
+        auto item = allowItem.stripLeadingAndTrailingMatchedCharacters(isHTMLSpace<UChar>);
+        if (!allowCameraAccess && item == "camera")
+            allowCameraAccess = true;
+        else if (!allowMicrophoneAccess && item == "microphone")
+            allowMicrophoneAccess = true;
+    }
+    return (allowCameraAccess || !requiresVideo) && (allowMicrophoneAccess || !requiresAudio);
+}
+
+UserMediaController::GetUserMediaAccess UserMediaController::canCallGetUserMedia(Document& document, bool wantsAudio, bool wantsVideo)
+{
+    ASSERT(wantsAudio || wantsVideo);
+
+    bool requiresSecureConnection = DeprecatedGlobalSettings::mediaCaptureRequiresSecureConnection();
+    auto& documentLoader = *document.loader();
+    if (requiresSecureConnection && !isSecure(documentLoader))
+        return GetUserMediaAccess::InsecureDocument;
+
+    auto& topDocument = document.topDocument();
+    if (&document != &topDocument) {
+        for (auto* ancestorDocument = &document; ancestorDocument != &topDocument; ancestorDocument = ancestorDocument->parentDocument()) {
+            if (requiresSecureConnection && !isSecure(*ancestorDocument->loader()))
+                return GetUserMediaAccess::InsecureParent;
+
+            if (!isAllowedToUse(*ancestorDocument, topDocument, wantsAudio, wantsVideo))
+                return GetUserMediaAccess::BlockedByParent;
+        }
+    }
+
+    return GetUserMediaAccess::CanCall;
+}
+
+void UserMediaController::logGetUserMediaDenial(Document& document, GetUserMediaAccess access, BlockedCaller caller)
+{
+    auto& domWindow = *document.domWindow();
+    const char* callerName = caller == BlockedCaller::GetUserMedia ? "getUserMedia" : "enumerateDevices";
+    switch (access) {
+    case UserMediaController::GetUserMediaAccess::InsecureDocument:
+        domWindow.printErrorMessage(makeString("Trying to call ", callerName, " from an insecure document."));
+        break;
+    case UserMediaController::GetUserMediaAccess::InsecureParent:
+        domWindow.printErrorMessage(makeString("Trying to call ", callerName, " from a document with an insecure parent frame."));
+        break;
+    case UserMediaController::GetUserMediaAccess::BlockedByParent:
+        domWindow.printErrorMessage(makeString("The top-level frame has prevented a document with a different security origin from calling ", callerName, "."));
+        break;
+    case UserMediaController::GetUserMediaAccess::CanCall:
+        break;
+    }
+}
+
 } // namespace WebCore
 
 #endif // ENABLE(MEDIA_STREAM)
index cfac599..eac75b9 100644 (file)
@@ -51,6 +51,20 @@ public:
     UserMediaClient::DeviceChangeObserverToken addDeviceChangeObserver(WTF::Function<void()>&&);
     void removeDeviceChangeObserver(UserMediaClient::DeviceChangeObserverToken);
 
+    enum class GetUserMediaAccess {
+        CanCall,
+        InsecureDocument,
+        InsecureParent,
+        BlockedByParent
+    };
+    GetUserMediaAccess canCallGetUserMedia(Document&, bool wantsAudio, bool wantsVideo);
+
+    enum class BlockedCaller {
+        GetUserMedia,
+        EnumerateDevices
+    };
+    void logGetUserMediaDenial(Document&, GetUserMediaAccess, BlockedCaller);
+
     WEBCORE_EXPORT static const char* supplementName();
     static UserMediaController* from(Page* page) { return static_cast<UserMediaController*>(Supplement<Page>::from(page, supplementName())); }
 
index e823f75..3bae11d 100644 (file)
@@ -1,7 +1,7 @@
 /*
  * Copyright (C) 2011 Ericsson AB. All rights reserved.
  * Copyright (C) 2012 Google Inc. All rights reserved.
- * Copyright (C) 2013-2017 Apple Inc. All rights reserved.
+ * Copyright (C) 2013-2018 Apple Inc. All rights reserved.
  * Copyright (C) 2013 Nokia Corporation and/or its subsidiary(-ies).
  *
  * Redistribution and use in source and binary forms, with or without
 
 #if ENABLE(MEDIA_STREAM)
 
-#include "CaptureDeviceManager.h"
-#include "DeprecatedGlobalSettings.h"
 #include "Document.h"
-#include "DocumentLoader.h"
 #include "Frame.h"
-#include "HTMLIFrameElement.h"
-#include "HTMLParserIdioms.h"
 #include "JSMediaStream.h"
 #include "JSOverconstrainedError.h"
 #include "Logging.h"
@@ -84,78 +79,6 @@ SecurityOrigin* UserMediaRequest::topLevelDocumentOrigin() const
     return &m_scriptExecutionContext->topOrigin();
 }
 
-static bool isSecure(DocumentLoader& documentLoader)
-{
-    auto& response = documentLoader.response();
-    if (SecurityOrigin::isLocalHostOrLoopbackIPAddress(documentLoader.response().url().host()))
-        return true;
-    return SchemeRegistry::shouldTreatURLSchemeAsSecure(response.url().protocol().toStringWithoutCopying())
-        && response.certificateInfo()
-        && !response.certificateInfo()->containsNonRootSHA1SignedCertificate();
-}
-
-static bool isAllowedToUse(Document& document, Document& topDocument, bool requiresAudio, bool requiresVideo)
-{
-    if (&document == &topDocument)
-        return true;
-
-    auto* parentDocument = document.parentDocument();
-    if (!parentDocument)
-        return false;
-
-    if (document.securityOrigin().isSameSchemeHostPort(parentDocument->securityOrigin()))
-        return true;
-
-    auto* element = document.ownerElement();
-    ASSERT(element);
-    if (!element)
-        return false;
-
-    if (!is<HTMLIFrameElement>(*element))
-        return false;
-    auto& allow = downcast<HTMLIFrameElement>(*element).allow();
-
-    bool allowCameraAccess = false;
-    bool allowMicrophoneAccess = false;
-    for (auto allowItem : StringView { allow }.split(';')) {
-        auto item = allowItem.stripLeadingAndTrailingMatchedCharacters(isHTMLSpace<UChar>);
-        if (!allowCameraAccess && item == "camera")
-            allowCameraAccess = true;
-        else if (!allowMicrophoneAccess && item == "microphone")
-            allowMicrophoneAccess = true;
-    }
-    return (allowCameraAccess || !requiresVideo) && (allowMicrophoneAccess || !requiresAudio);
-}
-
-static bool canCallGetUserMedia(Document& document, bool wantsAudio, bool wantsVideo, String& errorMessage)
-{
-    ASSERT(wantsAudio || wantsVideo);
-
-    bool requiresSecureConnection = DeprecatedGlobalSettings::mediaCaptureRequiresSecureConnection();
-    auto& documentLoader = *document.loader();
-    if (requiresSecureConnection && !isSecure(documentLoader)) {
-        errorMessage = "Trying to call getUserMedia from an insecure document.";
-        return false;
-    }
-
-    auto& topDocument = document.topDocument();
-    if (&document != &topDocument) {
-        for (auto* ancestorDocument = &document; ancestorDocument != &topDocument; ancestorDocument = ancestorDocument->parentDocument()) {
-            if (requiresSecureConnection && !isSecure(*ancestorDocument->loader())) {
-                errorMessage = "Trying to call getUserMedia from a document with an insecure parent frame.";
-                return false;
-            }
-
-            if (!isAllowedToUse(*ancestorDocument, topDocument, wantsAudio, wantsVideo)) {
-                errorMessage = "The top-level frame has prevented a document with a different security origin to call getUserMedia.";
-                return false;
-            }
-        }
-    }
-    
-    return true;
-}
-
 static bool hasInvalidGetDisplayMediaConstraint(const MediaConstraints& constraints)
 {
     // https://w3c.github.io/mediacapture-screen-share/#navigator-additions
@@ -266,10 +189,10 @@ void UserMediaRequest::start()
     // ...
     // 6.10 Permission Failure: Reject p with a new DOMException object whose name attribute has
     //      the value NotAllowedError.
-    String errorMessage;
-    if (!canCallGetUserMedia(document, m_request.audioConstraints.isValid, m_request.videoConstraints.isValid, errorMessage)) {
+    auto access = controller->canCallGetUserMedia(document, m_request.audioConstraints.isValid, m_request.videoConstraints.isValid);
+    if (access != UserMediaController::GetUserMediaAccess::CanCall) {
         deny(MediaAccessDenialReason::PermissionDenied);
-        document.domWindow()->printErrorMessage(errorMessage);
+        controller->logGetUserMediaDenial(document, access, UserMediaController::BlockedCaller::GetUserMedia);
         return;
     }