Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions v3/UNRELEASED_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ After processing, the content will be moved to the main changelog and this file

## Fixed
<!-- Bug fixes -->
- 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
<!-- Soon-to-be removed features -->
Expand Down
85 changes: 85 additions & 0 deletions v3/pkg/application/permissions_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//go:build darwin && !ios && !server

package application

/*
#include <stdbool.h>
*/
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]
}
16 changes: 16 additions & 0 deletions v3/pkg/application/webview_window_darwin.m
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand Down
Loading