From 9a3862d55304e3a4e685e85c325f59dc1491c1e0 Mon Sep 17 00:00:00 2001 From: stefan Date: Fri, 28 Aug 2026 17:36:00 +0700 Subject: [PATCH 1/2] fix(darwin): honour Permissions for media capture requests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cross-platform Permissions option has no effect on macOS. It is honoured on Linux since #5552 and on Windows through WebView2, but WebviewWindowDelegate — which is set as the webview's UI delegate — does not implement webView:requestMediaCapturePermissionForOrigin:initiatedByFrame:type:decisionHandler:, so nothing on that platform ever consults it. Left unimplemented, WebKit takes the request's default action, which on Cocoa is promptForGetUserMedia. Capture therefore works, but macOS behaves as though the option were permanently PermissionDefault: PermissionAllow and PermissionDeny are silently ignored. Implementing the delegate against the same option gives them their meaning there. PermissionDefault maps to WKPermissionDecisionPrompt, which is the behaviour that was already in place, so nothing changes for a window that does not configure the option. A request for the camera and the microphone together gets one answer, no more permissive than either half on its own. Permission and WKPermissionDecision happen to agree case for case. The mapping is still written out rather than cast: an agreement between two unrelated ABIs is not something to build on. The delegate method is macOS 12+, so it carries API_AVAILABLE and is never called below that. --- v3/pkg/application/permissions_darwin.go | 85 ++++++++++++++++++++++ v3/pkg/application/webview_window_darwin.m | 16 ++++ 2 files changed, 101 insertions(+) create mode 100644 v3/pkg/application/permissions_darwin.go 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; From 910cb5a0174d42159285214a2a4fea0a4a238bb9 Mon Sep 17 00:00:00 2001 From: stefan Date: Fri, 28 Aug 2026 17:36:35 +0700 Subject: [PATCH 2/2] docs: changelog entry --- v3/UNRELEASED_CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) 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