diff --git a/v3/UNRELEASED_CHANGELOG.md b/v3/UNRELEASED_CHANGELOG.md index 33638e7fc86..8366e34ec99 100644 --- a/v3/UNRELEASED_CHANGELOG.md +++ b/v3/UNRELEASED_CHANGELOG.md @@ -23,6 +23,7 @@ After processing, the content will be moved to the main changelog and this file ## Fixed +- Fix the `Permissions` option being ignored on macOS 12 and later: the `WKUIDelegate` media-capture method is now implemented, so `PermissionAllow` and `PermissionDeny` apply to camera and microphone requests as they do on Linux and Windows. `NSCameraUsageDescription` / `NSMicrophoneUsageDescription` and, where sandboxed, the matching device entitlements are still required ## Deprecated diff --git a/v3/pkg/application/permissions_darwin.go b/v3/pkg/application/permissions_darwin.go new file mode 100644 index 00000000000..275d2647283 --- /dev/null +++ b/v3/pkg/application/permissions_darwin.go @@ -0,0 +1,85 @@ +//go:build darwin && !ios && !server + +package application + +/* +#include +*/ +import "C" + +// captureDecision mirrors WKPermissionDecision, whose cases the delegate in +// webview_window_darwin.m hands straight back to WebKit. Written out rather +// than cast from Permission: the two enums happen to agree today, and a +// coincidence between unrelated ABIs is not something to build on. +type captureDecision int + +const ( + captureDecisionPrompt captureDecision = iota + captureDecisionGrant + captureDecisionDeny +) + +// resolveMediaCapturePermission answers a getUserMedia request for the given +// window, applying its Permissions. +// +// With no delegate method, WebKit takes the default action, which on Cocoa is +// its own prompt — so capture works, but the window's Permissions are never +// consulted and macOS behaves as if the option were permanently +// PermissionDefault. This is what gives PermissionAllow and PermissionDeny +// their meaning there; PermissionDefault keeps that same prompt. +// +//export resolveMediaCapturePermission +func resolveMediaCapturePermission(windowID C.uint, needAudio C.bool, needVideo C.bool) C.int { + if !bool(needAudio) && !bool(needVideo) { + return C.int(captureDecisionPrompt) + } + + decision := captureDecisionGrant + if bool(needAudio) { + decision = strictestCaptureDecision(decision, captureDecisionFor(uint(windowID), PermissionMicrophone)) + } + if bool(needVideo) { + decision = strictestCaptureDecision(decision, captureDecisionFor(uint(windowID), PermissionCamera)) + } + + return C.int(decision) +} + +func captureDecisionFor(windowID uint, kind PermissionType) captureDecision { + switch resolvePermission(windowID, kind) { + case PermissionAllow: + return captureDecisionGrant + case PermissionDeny: + return captureDecisionDeny + default: + return captureDecisionPrompt + } +} + +// A request for the camera and the microphone together gets a single answer, +// and it can be no more permissive than either half on its own: deny beats +// prompt beats grant. +func strictestCaptureDecision(a, b captureDecision) captureDecision { + if a == captureDecisionDeny || b == captureDecisionDeny { + return captureDecisionDeny + } + if a == captureDecisionPrompt || b == captureDecisionPrompt { + return captureDecisionPrompt + } + return captureDecisionGrant +} + +// resolvePermission returns the configured Permission for the given type on the +// window identified by windowID, defaulting to PermissionDefault when the +// window or an entry is not found. +func resolvePermission(windowID uint, kind PermissionType) Permission { + window, ok := globalApplication.Window.GetByID(windowID) + if !ok || window == nil { + return PermissionDefault + } + webviewWindow, ok := window.(*WebviewWindow) + if !ok || webviewWindow.options.Permissions == nil { + return PermissionDefault + } + return webviewWindow.options.Permissions[kind] +} diff --git a/v3/pkg/application/webview_window_darwin.m b/v3/pkg/application/webview_window_darwin.m index 67e4dbbce30..3e5f625847b 100644 --- a/v3/pkg/application/webview_window_darwin.m +++ b/v3/pkg/application/webview_window_darwin.m @@ -14,6 +14,7 @@ unsigned int character, int hasCharacter); extern bool processWindowKeyEquivalent(unsigned int, const char*); extern bool hasListeners(unsigned int); +extern int resolveMediaCapturePermission(unsigned int, bool, bool); extern bool windowShouldUnconditionallyClose(unsigned int); extern bool windowIsHidden(unsigned int); // Define custom glass effect style constants (these match the Go constants) @@ -1006,6 +1007,21 @@ - (void)webView:(WKWebView *)webView runOpenPanelWithParameters:(WKOpenPanelPara completionHandler(nil); }]; } +// WKUIDelegate - Handle a getUserMedia request for the camera or microphone. +// Left unimplemented, WebKit takes its default action instead, which on Cocoa +// is its own prompt — so the window's Permissions never reach the decision and +// PermissionAllow and PermissionDeny do nothing. The answer comes from them +// here; PermissionDefault asks for the same prompt as before. +- (void)webView:(WKWebView *)webView + requestMediaCapturePermissionForOrigin:(WKSecurityOrigin *)origin + initiatedByFrame:(WKFrameInfo *)frame + type:(WKMediaCaptureType)type + decisionHandler:(void (^)(WKPermissionDecision decision))decisionHandler + API_AVAILABLE(macos(12.0)) { + bool needAudio = type == WKMediaCaptureTypeMicrophone || type == WKMediaCaptureTypeCameraAndMicrophone; + bool needVideo = type == WKMediaCaptureTypeCamera || type == WKMediaCaptureTypeCameraAndMicrophone; + decisionHandler((WKPermissionDecision)resolveMediaCapturePermission(self.windowId, needAudio, needVideo)); +} @end void windowSetScreen(void* window, void* screen, int yOffset) { NSWindow* nsWindow = (NSWindow*)window;