Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
94 changes: 94 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,41 @@ 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)]
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,
}

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

Expand Down Expand Up @@ -794,6 +829,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 +898,7 @@ impl Default for WebViewAttributes<'_> {
}),
background_throttling: None,
javascript_disabled: false,
permission_handler: None,
}
}
}
Expand Down Expand Up @@ -1260,6 +1323,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
57 changes: 53 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,52 @@ 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_display_device() {
PermissionKind::DisplayCapture
} else if media_request.is_for_audio_device() {
PermissionKind::Microphone
} else if media_request.is_for_video_device() {
PermissionKind::Camera
} else {
PermissionKind::Other
}
} 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
}
}
});
}

// Download handler
if attributes.download_started_handler.is_some()
|| attributes.download_completed_handler.is_some()
Expand Down
48 changes: 46 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,50 @@ 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
}
}

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

// Navigation
if let Some(mut url) = attributes.url {
if let Some((protocol, _)) = url.split_once("://") {
Expand Down
36 changes: 29 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,30 @@ 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
}
} else {
// No handler set, default to grant (backwards compatible behavior)
WKPermissionDecision::Grant
};
Comment on lines 148 to 158
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should align the default action with the OS, giving WKPermissionDecision::Prompt. Granting by default looks dangerous to me.

Copy link
Contributor

Choose a reason for hiding this comment

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

It seems to match the old behavior here? I'm not really aware of the historical context here though

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, you're right. Let's leave it granted then. But I think we might need a discussion for this, maybe in a new issue.

Copy link
Author

Choose a reason for hiding this comment

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

I've added a PermissionResponse::Prompt variant. This keeps the default behavior as Grant (for backward compatibility) but allows apps to explicitly opt-in to the system prompt if they want. I think this covers both use cases.


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

#[cfg(target_os = "macos")]
Expand Down Expand Up @@ -270,6 +290,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 +302,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