Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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
12 changes: 12 additions & 0 deletions .changes/permission-handler.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"wry": minor
---

Add an expanded permission handling API for WebView2, WKWebView, WebKitGTK, and Android.
This includes:
- `PermissionKind` expansion: `DisplayCapture`, `Midi`, `Sensors`, `MediaKeySystemAccess`, `LocalFonts`, `WindowManagement`, `PointerLock`, `AutomaticDownloads`, `FileSystemAccess`, `Autoplay`.
- Support for `PermissionResponse::Prompt` to trigger native system dialogs.
- Android support via JNI bridge (`onPermissionRequest` in `RustWebChromeClient`).
- macOS: Split camera/microphone requests; `CameraAndMicrophone` resolved from individual responses.
- Linux: `DisplayCapture` detection for WebKitGTK < 2.42 (getDisplayMedia fix).
- Windows: Full coverage of all 12 `COREWEBVIEW2_PERMISSION_KIND` values.
66 changes: 66 additions & 0 deletions examples/permission_handler.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

//! Example demonstrating the permission handler API.
//!
//! Run: cargo run --example permission_handler
//! Then click the buttons and watch the terminal output.

fn main() -> wry::Result<()> {
use tao::{
event::{Event, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::WindowBuilder,
};
use wry::{PermissionKind, PermissionResponse, WebViewBuilder};

let event_loop = EventLoop::new();
let window = WindowBuilder::new()
.with_title("Permission Handler Example")
.with_inner_size(tao::dpi::LogicalSize::new(800, 600))
.build(&event_loop)
.unwrap();

let builder = WebViewBuilder::new()
.with_url("https://permission.site/")
.with_permission_handler(|kind| {
let response = match kind {
PermissionKind::Geolocation => PermissionResponse::Prompt,
_ => PermissionResponse::Allow,
};
println!("[permission] {kind} → {response}");
response
});

#[cfg(any(
target_os = "windows",
target_os = "macos",
target_os = "ios",
target_os = "android"
))]
let _webview = builder.build(&window)?;
#[cfg(not(any(
target_os = "windows",
target_os = "macos",
target_os = "ios",
target_os = "android"
)))]
let _webview = {
use tao::platform::unix::WindowExtUnix;
use wry::WebViewBuilderExtUnix;
let vbox = window.default_vbox().unwrap();
builder.build_gtk(vbox)?
};

event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
if let Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} = event
{
*control_flow = ControlFlow::Exit;
}
});
}
62 changes: 59 additions & 3 deletions src/android/binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ pub use jni::{
pub use ndk;

use super::{
ASSET_LOADER_DOMAIN, EVAL_CALLBACKS, IPC, ON_LOAD_HANDLER, REQUEST_HANDLER, TITLE_CHANGE_HANDLER,
URL_LOADING_OVERRIDE, WITH_ASSET_LOADER,
ASSET_LOADER_DOMAIN, EVAL_CALLBACKS, IPC, ON_LOAD_HANDLER, PERMISSION_HANDLER, REQUEST_HANDLER,
TITLE_CHANGE_HANDLER, URL_LOADING_OVERRIDE, WITH_ASSET_LOADER,
};

use crate::PageLoadEvent;
use crate::{PageLoadEvent, PermissionKind, PermissionResponse};

#[macro_export]
macro_rules! android_binding {
Expand Down Expand Up @@ -97,6 +97,14 @@ macro_rules! android_binding {
handleReceivedTitle,
[JObject, JString],
);
android_fn!(
$domain,
$package,
RustWebChromeClient,
onPermissionRequestNative,
[jni::objects::JObjectArray],
jint
);
}};
}

Expand Down Expand Up @@ -413,3 +421,51 @@ pub unsafe fn onPageLoaded(mut env: JNIEnv, _: JClass, url: JString) {
}
}
}

pub unsafe fn onPermissionRequestNative(
mut env: JNIEnv,
_: JClass,
resources: jni::objects::JObjectArray,
) -> jint {
let mut allowed = false;
let mut denied = false;
let mut prompt = false;

if let Ok(size) = env.get_array_length(&resources) {
for i in 0..size {
if let Ok(resource) = env.get_object_array_element(&resources, i) {
if let Ok(resource_str) = env.get_string(&resource.into()) {
let resource_str = resource_str.to_string_lossy();

let kind = match resource_str.as_ref() {
"android.webkit.resource.AUDIO_CAPTURE" => PermissionKind::Microphone,
"android.webkit.resource.VIDEO_CAPTURE" => PermissionKind::Camera,
"android.webkit.resource.PROTECTED_MEDIA_ID" => PermissionKind::MediaKeySystemAccess,
"android.webkit.resource.MIDI_SYSEX" => PermissionKind::Midi,
_ => PermissionKind::Other,
};

if let Some(handler) = &*PERMISSION_HANDLER.lock().unwrap() {
match (handler.handler)(kind) {
PermissionResponse::Allow => allowed = true,
PermissionResponse::Deny => denied = true,
PermissionResponse::Prompt => prompt = true,
PermissionResponse::Default => {}
}
}
}
}
}
}

// Consolidated decision logic
if denied {
1 // Deny
} else if allowed {
0 // Allow
} else if prompt {
3 // Prompt
} else {
2 // Default
}
}
33 changes: 28 additions & 5 deletions src/android/kotlin/RustWebChromeClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,24 @@ class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() {
}

override fun onPermissionRequest(request: PermissionRequest) {
val response = onPermissionRequestNative(request.resources)
when (response) {
0 -> { // Allow
request.grant(request.resources)
return
}
1 -> { // Deny
request.deny()
return
}
2 -> { // Default
// Continue with default logic
}
3 -> { // Prompt
// Continue with default logic (which prompts)
}
}

val isRequestPermissionRequired = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
val permissionList: MutableList<String> = ArrayList()
if (listOf(*request.resources).contains("android.webkit.resource.VIDEO_CAPTURE")) {
Expand All @@ -118,6 +136,8 @@ class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() {
}
}

private external fun onPermissionRequestNative(resources: Array<String>): Int

/**
* Show the browser alert modal
* @param view
Expand Down Expand Up @@ -482,12 +502,15 @@ class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() {
return File.createTempFile(imageFileName, ".jpg", storageDir)
}

override fun onReceivedTitle(
view: WebView,
title: String
) {
handleReceivedTitle(view, title)
override fun onPermissionRequest(request: PermissionRequest) {
val result = onPermissionRequestNative(request.resources)
when (result) {
0 -> request.grant(request.resources)
1 -> request.deny()
else -> super.onPermissionRequest(request)
}
}

private external fun onPermissionRequestNative(resources: Array<String>): Int
private external fun handleReceivedTitle(webview: WebView, title: String)
}
5 changes: 4 additions & 1 deletion src/android/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
// SPDX-License-Identifier: MIT

use super::{PageLoadEvent, WebViewAttributes, RGBA};
use crate::{custom_protocol_workaround, RequestAsyncResponder, Result};
use crate::{
custom_protocol_workaround, PermissionKind, PermissionResponse, RequestAsyncResponder, Result,
};
use base64::{engine::general_purpose, Engine};
use crossbeam_channel::*;
use html5ever::{interface::QualName, namespace_url, ns, tendril::TendrilSink, LocalName};
Expand Down Expand Up @@ -81,6 +83,7 @@ define_static_handlers! {
TITLE_CHANGE_HANDLER = UnsafeTitleHandler { handler: Box<dyn Fn(String)> };
URL_LOADING_OVERRIDE = UnsafeUrlLoadingOverride { handler: Box<dyn Fn(String) -> bool> };
ON_LOAD_HANDLER = UnsafeOnPageLoadHandler { handler: Box<dyn Fn(PageLoadEvent, String)> };
PERMISSION_HANDLER = UnsafePermissionHandler { handler: Box<dyn Fn(PermissionKind) -> PermissionResponse> };
}

pub static WITH_ASSET_LOADER: StaticValue<Option<bool>> = StaticValue(Mutex::new(None));
Expand Down
72 changes: 72 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@
#[cfg(any(target_os = "windows", target_os = "android"))]
mod custom_protocol_workaround;
mod error;
mod permissions;
mod proxy;
#[cfg(any(target_os = "macos", target_os = "android", target_os = "ios"))]
mod util;
Expand Down Expand Up @@ -409,6 +410,7 @@ pub use cookie;
pub use dpi;
pub use error::*;
pub use http;
pub use permissions::{PermissionKind, PermissionResponse};
pub use proxy::{ProxyConfig, ProxyEndpoint};
pub use web_context::WebContext;

Expand Down Expand Up @@ -796,6 +798,37 @@ 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`].
///
/// > [!NOTE]
/// > This handler only triggers for new permission requests. If the user has already
/// > allowed or denied a permission persistently within the webview, the browser
/// > will use the saved preference instead of calling this handler.
///
/// ## 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**: Supported via JNI bridge with some limitations (WIP).
///
/// ## 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>>,
/// Controls the WebView's browser-level general autofill behavior.
///
/// **This option does not disable password or credit card autofill.**
Expand Down Expand Up @@ -855,6 +888,7 @@ impl Default for WebViewAttributes<'_> {
}),
background_throttling: None,
javascript_disabled: false,
permission_handler: None,
general_autofill_enabled: true,
}
}
Expand Down Expand Up @@ -1281,6 +1315,44 @@ 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`].
///
/// > [!NOTE]
/// > This handler only triggers for new permission requests. If the user has already
/// > allowed or denied a permission persistently within the webview, the browser
/// > will use the saved preference instead of calling this handler.
///
/// ## 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**: Supported via JNI bridge with some limitations (WIP).
///
/// ## 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
Loading