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
5 changes: 5 additions & 0 deletions .changes/permission-handler.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"wry": minor
---

feat: add permission handler API for WebView2
97 changes: 97 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,44 @@ pub struct NewWindowFeatures {
pub opener: NewWindowOpener,
}

/// Permission types that can be requested by the webview.
///
/// See [`WebViewBuilder::with_permission_handler`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum PermissionKind {
/// Microphone access permission.
Microphone,
/// Camera access permission.
Camera,
/// Geolocation access permission.
Geolocation,
/// Notifications permission.
Notifications,
/// Clipboard read permission.
ClipboardRead,
/// Display capture permission (for getDisplayMedia).
DisplayCapture,
/// Other unrecognized permission type.
Other,
}

/// Response for permission requests.
///
/// See [`WebViewBuilder::with_permission_handler`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PermissionResponse {
/// Grant the permission.
Allow,
/// Deny the permission.
Deny,
/// Use default behavior (show system prompt).
#[default]
Default,
/// Explicitly prompt the user (system dialog).
Prompt,
}

/// An id for a webview
pub type WebViewId<'a> = &'a str;

Expand Down Expand Up @@ -794,6 +832,33 @@ pub struct WebViewAttributes<'a> {

/// Whether JavaScript should be disabled.
pub javascript_disabled: bool,

/// A handler to intercept permission requests from the webview.
///
/// The handler receives the [`PermissionKind`] and should return
/// the desired [`PermissionResponse`].
///
/// ## Platform-specific:
///
/// - **Windows**: Fully supported via WebView2's PermissionRequested event.
/// - **macOS / iOS**: Fully supported via WKUIDelegate's requestMediaCapturePermission.
/// - **Linux**: Fully supported via WebKitGTK's permission-request signal.
/// - **Android**: Not yet implemented, handler is ignored.
///
/// ## Example
///
/// ```no_run
/// # use wry::{WebViewBuilder, PermissionKind, PermissionResponse};
/// let webview = WebViewBuilder::new()
/// .with_permission_handler(|kind| {
/// match kind {
/// PermissionKind::Microphone => PermissionResponse::Allow,
/// PermissionKind::Camera => PermissionResponse::Allow,
/// _ => PermissionResponse::Default,
/// }
/// });
/// ```
pub permission_handler: Option<Box<dyn Fn(PermissionKind) -> PermissionResponse + Send + Sync>>,
}

impl Default for WebViewAttributes<'_> {
Expand Down Expand Up @@ -836,6 +901,7 @@ impl Default for WebViewAttributes<'_> {
}),
background_throttling: None,
javascript_disabled: false,
permission_handler: None,
}
}
}
Expand Down Expand Up @@ -1260,6 +1326,37 @@ impl<'a> WebViewBuilder<'a> {
self
}

/// Set a handler to intercept permission requests from the webview.
///
/// The handler receives the [`PermissionKind`] and should return
/// the desired [`PermissionResponse`].
///
/// ## Platform-specific:
///
/// - **Windows**: Fully supported via WebView2's PermissionRequested event.
/// - **macOS / iOS / Linux / Android**: Not yet implemented, handler is ignored.
Comment on lines +1336 to +1337
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like you have implemented most of them!

///
/// ## Example
///
/// ```no_run
/// # use wry::{WebViewBuilder, PermissionKind, PermissionResponse};
/// let webview = WebViewBuilder::new()
/// .with_permission_handler(|kind| {
/// match kind {
/// PermissionKind::Microphone => PermissionResponse::Allow,
/// PermissionKind::Camera => PermissionResponse::Allow,
/// _ => PermissionResponse::Default,
/// }
/// });
/// ```
pub fn with_permission_handler<F>(mut self, handler: F) -> Self
where
F: Fn(PermissionKind) -> PermissionResponse + Send + Sync + 'static,
{
self.attrs.permission_handler = Some(Box::new(handler));
self
}

/// Set a download started handler to manage incoming downloads.
///
/// The closure takes two parameters, the first is a `String` representing the url being downloaded from and and the
Expand Down
59 changes: 55 additions & 4 deletions src/webkitgtk/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use gdkx11::{
};
#[cfg(feature = "x11")]
use gtk::glib::{self, translate::FromGlibPtrFull};
use gtk::glib::{Cast, IsA};
use gtk::{
gdk::{self},
gio::Cancellable,
Expand All @@ -36,9 +37,10 @@ use std::{
use webkit2gtk::WebInspectorExt;
use webkit2gtk::{
AutoplayPolicy, CookieManagerExt, InputMethodContextExt, LoadEvent, NavigationPolicyDecision,
NavigationPolicyDecisionExt, NetworkProxyMode, NetworkProxySettings, PolicyDecisionType,
PrintOperationExt, SettingsExt, URIRequest, URIRequestExt, UserContentInjectedFrames,
UserContentManager, UserContentManagerExt, UserScript, UserScriptInjectionTime,
NavigationPolicyDecisionExt, NetworkProxyMode, NetworkProxySettings, PermissionRequestExt,
PolicyDecisionType, PrintOperationExt, SettingsExt, URIRequest, URIRequestExt,
UserContentInjectedFrames, UserContentManager, UserContentManagerExt, UserMediaPermissionRequest,
UserMediaPermissionRequestExt, UserScript, UserScriptInjectionTime,
WebContextExt as Webkit2gtkWeContextExt, WebView, WebViewExt, WebsiteDataManagerExt,
WebsiteDataManagerExtManual, WebsitePolicies,
};
Expand All @@ -53,7 +55,8 @@ pub use web_context::WebContextImpl;

use crate::{
proxy::ProxyConfig, web_context::WebContext, Error, NewWindowFeatures, NewWindowOpener,
NewWindowResponse, PageLoadEvent, Rect, Result, WebViewAttributes, RGBA,
NewWindowResponse, PageLoadEvent, PermissionKind, PermissionResponse, Rect, Result,
WebViewAttributes, RGBA,
};

use self::web_context::WebContextExt;
Expand Down Expand Up @@ -573,6 +576,54 @@ impl InnerWebView {
});
}

// Permission handler
if let Some(permission_handler) = attributes.permission_handler.take() {
let permission_handler = Rc::new(permission_handler);
webview.connect_permission_request(move |_webview, request| {
// Determine permission kind
let permission_kind = if request
.downcast_ref::<UserMediaPermissionRequest>()
.is_some()
{
let media_request = request
.downcast_ref::<UserMediaPermissionRequest>()
.unwrap();
if media_request.is_for_audio_device() {
PermissionKind::Microphone
} else if media_request.is_for_video_device() {
PermissionKind::Camera
} else {
// TODO: Use is_for_display_device() when we bump to WebKitGTK 2.42
PermissionKind::DisplayCapture
}
} else {
// Could be GeolocationPermissionRequest, NotificationPermissionRequest, etc.
PermissionKind::Other
};

// Call user's permission handler
let response = permission_handler(permission_kind);

// Apply the response
match response {
PermissionResponse::Allow => {
request.allow();
true // handled
}
PermissionResponse::Deny => {
request.deny();
true // handled
}
PermissionResponse::Default => {
false // not handled, let WebKitGTK show default prompt
}
PermissionResponse::Prompt => {
false // not handled, let WebKitGTK show default prompt
}
}
});
}

// Download handler
if attributes.download_started_handler.is_some()
|| attributes.download_completed_handler.is_some()
Expand Down
51 changes: 49 additions & 2 deletions src/webview2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ use self::drag_drop::DragDropController;
use super::Theme;
use crate::{
custom_protocol_workaround, proxy::ProxyConfig, Error, MemoryUsageLevel, NewWindowFeatures,
NewWindowOpener, NewWindowResponse, PageLoadEvent, Rect, RequestAsyncResponder, Result,
WebViewAttributes, RGBA,
NewWindowOpener, NewWindowResponse, PageLoadEvent, PermissionKind, PermissionResponse, Rect,
RequestAsyncResponder, Result, WebViewAttributes, RGBA,
};

type EventRegistrationToken = i64;
Expand Down Expand Up @@ -511,6 +511,53 @@ impl InnerWebView {
}
}

// Permission handler
if let Some(permission_handler) = attributes.permission_handler.take() {
let permission_handler = Rc::new(permission_handler);
unsafe {
webview.add_PermissionRequested(
&PermissionRequestedEventHandler::create(Box::new(move |_, args| {
let Some(args) = args else { return Ok(()) };

let mut kind = COREWEBVIEW2_PERMISSION_KIND::default();
args.PermissionKind(&mut kind)?;

// Convert WebView2 permission kind to our PermissionKind
let permission_kind = match kind {
COREWEBVIEW2_PERMISSION_KIND_MICROPHONE => PermissionKind::Microphone,
COREWEBVIEW2_PERMISSION_KIND_CAMERA => PermissionKind::Camera,
COREWEBVIEW2_PERMISSION_KIND_GEOLOCATION => PermissionKind::Geolocation,
COREWEBVIEW2_PERMISSION_KIND_NOTIFICATIONS => PermissionKind::Notifications,
COREWEBVIEW2_PERMISSION_KIND_CLIPBOARD_READ => PermissionKind::ClipboardRead,
_ => PermissionKind::Other,
};

// Call user's permission handler
let response = permission_handler(permission_kind);

// Apply the response
match response {
PermissionResponse::Allow => {
args.SetState(COREWEBVIEW2_PERMISSION_STATE_ALLOW)?;
}
PermissionResponse::Deny => {
args.SetState(COREWEBVIEW2_PERMISSION_STATE_DENY)?;
}
PermissionResponse::Default => {
// Do nothing, let WebView2 show default prompt
}
PermissionResponse::Prompt => {
// Do nothing, let WebView2 show default prompt
}
}

Ok(())
})),
&mut token,
)?;
}
}

// Navigation
if let Some(mut url) = attributes.url {
if let Some((protocol, _)) = url.split_once("://") {
Expand Down
37 changes: 30 additions & 7 deletions src/wkwebview/class/wry_web_view_ui_delegate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
use std::{cell::RefCell, ptr::null_mut, rc::Rc};

use block2::Block;
#[cfg(target_os = "macos")]
use objc2::DefinedClass;
use objc2::{define_class, msg_send, rc::Retained, runtime::NSObject, MainThreadOnly};
use objc2::{
define_class, msg_send, rc::Retained, runtime::NSObject, DefinedClass, MainThreadOnly,
};
#[cfg(target_os = "macos")]
use objc2_app_kit::{NSModalResponse, NSModalResponseOK, NSOpenPanel, NSWindowDelegate};
use objc2_foundation::{MainThreadMarker, NSObjectProtocol};
Expand All @@ -21,7 +21,7 @@ use objc2_web_kit::{
WKFrameInfo, WKMediaCaptureType, WKPermissionDecision, WKSecurityOrigin, WKUIDelegate,
};

use crate::{NewWindowFeatures, NewWindowResponse, WryWebView};
use crate::{NewWindowFeatures, NewWindowResponse, PermissionKind, PermissionResponse, WryWebView};

#[cfg(target_os = "macos")]
struct NewWindow {
Expand Down Expand Up @@ -86,6 +86,7 @@ pub struct WryWebViewUIDelegateIvars {
Option<Box<dyn Fn(String, NewWindowFeatures) -> NewWindowResponse + Send + Sync>>,
#[cfg(target_os = "macos")]
new_windows: Rc<RefCell<Vec<NewWindow>>>,
permission_handler: Option<Box<dyn Fn(PermissionKind) -> PermissionResponse + Send + Sync>>,
}

define_class!(
Expand Down Expand Up @@ -132,11 +133,31 @@ define_class!(
_webview: &WryWebView,
_origin: &WKSecurityOrigin,
_frame: &WKFrameInfo,
_capture_type: WKMediaCaptureType,
capture_type: WKMediaCaptureType,
decision_handler: &Block<dyn Fn(WKPermissionDecision)>,
) {
//https://developer.apple.com/documentation/webkit/wkpermissiondecision?language=objc
(*decision_handler).call((WKPermissionDecision::Grant,));
// Determine permission kind based on capture type
let permission_kind = match capture_type {
WKMediaCaptureType::Camera => PermissionKind::Camera,
WKMediaCaptureType::Microphone => PermissionKind::Microphone,
WKMediaCaptureType::CameraAndMicrophone => PermissionKind::Microphone, // Treat as microphone for now
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Collapsing CameraAndMicrophone to Microphone also looks dangerous to me.

Maybe we can add some platform-specific type and document it well, or split it into something like vec![Microphone, Camera] and granted if and only if Microphone and Camera are both granted.

_ => PermissionKind::Other,
};

// Call user's permission handler if set
let decision = if let Some(handler) = &self.ivars().permission_handler {
match handler(permission_kind) {
PermissionResponse::Allow => WKPermissionDecision::Grant,
PermissionResponse::Deny => WKPermissionDecision::Deny,
PermissionResponse::Default => WKPermissionDecision::Grant, // Default to grant for backwards compatibility
PermissionResponse::Prompt => WKPermissionDecision::Prompt,
}
} else {
// No handler set, default to grant (backwards compatible behavior)
WKPermissionDecision::Grant
};

(*decision_handler).call((decision,));
}

#[cfg(target_os = "macos")]
Expand Down Expand Up @@ -270,6 +291,7 @@ impl WryWebViewUIDelegate {
new_window_req_handler: Option<
Box<dyn Fn(String, NewWindowFeatures) -> NewWindowResponse + Send + Sync>,
>,
permission_handler: Option<Box<dyn Fn(PermissionKind) -> PermissionResponse + Send + Sync>>,
) -> Retained<Self> {
#[cfg(target_os = "ios")]
let _new_window_req_handler = new_window_req_handler;
Expand All @@ -281,6 +303,7 @@ impl WryWebViewUIDelegate {
new_window_req_handler,
#[cfg(target_os = "macos")]
new_windows: Rc::new(RefCell::new(vec![])),
permission_handler,
});
unsafe { msg_send![super(delegate), init] }
}
Expand Down
7 changes: 5 additions & 2 deletions src/wkwebview/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -549,8 +549,11 @@ impl InnerWebView {
let proto_navigation_policy_delegate = ProtocolObject::from_ref(&*navigation_policy_delegate);
webview.setNavigationDelegate(Some(proto_navigation_policy_delegate));

let ui_delegate: Retained<WryWebViewUIDelegate> =
WryWebViewUIDelegate::new(mtm, attributes.new_window_req_handler);
let ui_delegate: Retained<WryWebViewUIDelegate> = WryWebViewUIDelegate::new(
mtm,
attributes.new_window_req_handler,
attributes.permission_handler,
);
let proto_ui_delegate = ProtocolObject::from_ref(&*ui_delegate);
webview.setUIDelegate(Some(proto_ui_delegate));

Expand Down
Loading