diff --git a/.changes/permission-handler.md b/.changes/permission-handler.md new file mode 100644 index 000000000..086a6f794 --- /dev/null +++ b/.changes/permission-handler.md @@ -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. diff --git a/examples/permission_handler.rs b/examples/permission_handler.rs new file mode 100644 index 000000000..45026629a --- /dev/null +++ b/examples/permission_handler.rs @@ -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; + } + }); +} diff --git a/src/android/binding.rs b/src/android/binding.rs index 031cd09cc..59c1af26c 100644 --- a/src/android/binding.rs +++ b/src/android/binding.rs @@ -19,11 +19,11 @@ use std::os::fd::{AsFd, AsRawFd}; use super::{ main_pipe::{MainPipe, MAIN_PIPE}, - 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 { @@ -87,6 +87,22 @@ macro_rules! android_binding { handleReceivedTitle, [JString, JString], ); + android_fn!( + $domain, + $package, + RustWebChromeClient, + onPermissionRequestNative, + [JString, jni::objects::JObjectArray], + jint + ); + android_fn!( + $domain, + $package, + RustWebChromeClient, + onGeolocationPermissionRequestNative, + [JString, JString], + jint + ); }}; } @@ -481,3 +497,83 @@ pub unsafe fn onPageLoaded(mut env: JNIEnv, _: JClass, webview_id: JString, url: } } } + +#[allow(non_snake_case)] +pub unsafe fn onPermissionRequestNative( + mut env: JNIEnv, + _: JClass, + webview_id: JString, + resources: jni::objects::JObjectArray, +) -> jint { + let mut allowed = false; + let mut denied = false; + let mut prompt = false; + let Ok(webview_id) = env.get_string(&webview_id) else { + return 2; + }; + let webview_id = webview_id.to_str().ok().unwrap_or_default(); + let permission_handlers = PERMISSION_HANDLER.lock().unwrap(); + let Some(handler) = permission_handlers.get(webview_id) else { + return 2; + }; + + 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, + }; + + 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 + } +} + +#[allow(non_snake_case)] +pub unsafe fn onGeolocationPermissionRequestNative( + mut env: JNIEnv, + _: JClass, + webview_id: JString, + _origin: JString, +) -> jint { + let Ok(webview_id) = env.get_string(&webview_id) else { + return 2; + }; + let webview_id = webview_id.to_str().ok().unwrap_or_default(); + let permission_handlers = PERMISSION_HANDLER.lock().unwrap(); + let Some(handler) = permission_handlers.get(webview_id) else { + return 2; + }; + + match (handler.handler)(PermissionKind::Geolocation) { + PermissionResponse::Allow => 0, + PermissionResponse::Deny => 1, + PermissionResponse::Default => 2, + PermissionResponse::Prompt => 3, + } +} diff --git a/src/android/kotlin/RustWebChromeClient.kt b/src/android/kotlin/RustWebChromeClient.kt index bba26dbe2..416349060 100644 --- a/src/android/kotlin/RustWebChromeClient.kt +++ b/src/android/kotlin/RustWebChromeClient.kt @@ -21,53 +21,13 @@ import android.provider.MediaStore import android.view.View import android.webkit.* import android.widget.EditText -import androidx.activity.result.ActivityResult -import androidx.activity.result.ActivityResultCallback -import androidx.activity.result.ActivityResultLauncher -import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.FileProvider import java.io.File import java.io.IOException import java.text.SimpleDateFormat import java.util.* -class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() { - private interface PermissionListener { - fun onPermissionSelect(isGranted: Boolean?) - } - - private interface ActivityResultListener { - fun onActivityResult(result: ActivityResult?) - } - - private val activity: WryActivity - private var permissionLauncher: ActivityResultLauncher> - private var activityLauncher: ActivityResultLauncher - private var permissionListener: PermissionListener? = null - private var activityListener: ActivityResultListener? = null - - init { - activity = appActivity - val permissionCallback = - ActivityResultCallback { isGranted: Map -> - if (permissionListener != null) { - var granted = true - for ((_, value) in isGranted) { - if (!value) granted = false - } - permissionListener!!.onPermissionSelect(granted) - } - } - permissionLauncher = - activity.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions(), permissionCallback) - activityLauncher = activity.registerForActivityResult( - ActivityResultContracts.StartActivityForResult() - ) { result -> - if (activityListener != null) { - activityListener!!.onActivityResult(result) - } - } - } +class RustWebChromeClient(private val activity: WryActivity, private val webViewId: String) : WebChromeClient() { /** * Render web content in `view`. @@ -92,32 +52,74 @@ class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() { } override fun onPermissionRequest(request: PermissionRequest) { - val isRequestPermissionRequired = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M - val permissionList: MutableList = ArrayList() - if (listOf(*request.resources).contains("android.webkit.resource.VIDEO_CAPTURE")) { - permissionList.add(Manifest.permission.CAMERA) + val requestedResources = safePermissionRequestResources(request.resources) + if (requestedResources.isEmpty()) { + request.deny() + return } - if (listOf(*request.resources).contains("android.webkit.resource.AUDIO_CAPTURE")) { - permissionList.add(Manifest.permission.MODIFY_AUDIO_SETTINGS) - permissionList.add(Manifest.permission.RECORD_AUDIO) + + val grantedResources = ArrayList() + val defaultResources = ArrayList() + + for (resource in requestedResources) { + when (onPermissionRequestNative(webViewId, arrayOf(resource))) { + 0 -> grantedResources.add(resource) + 1 -> { + request.deny() + return + } + 2, 3 -> defaultResources.add(resource) + } + } + + if (grantedResources.isNotEmpty()) { + // Android PermissionRequest can only be completed once. When the handler + // explicitly allows a subset and leaves the rest as default/prompt, grant + // only the handled subset and let the remaining resources be denied. + grantPermissionRequest(request, grantedResources.toTypedArray()) + return } + + grantPermissionRequest(request, defaultResources.toTypedArray()) + } + + private fun grantPermissionRequest(request: PermissionRequest, resources: Array) { + if (resources.isEmpty()) { + request.deny() + return + } + + val isRequestPermissionRequired = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + val permissionList = androidPermissionsForResources(resources) if (permissionList.isNotEmpty() && isRequestPermissionRequired) { val permissions = permissionList.toTypedArray() - permissionListener = object : PermissionListener { - override fun onPermissionSelect(isGranted: Boolean?) { - if (isGranted == true) { - request.grant(request.resources) - } else { - request.deny() - } + activity.requestPermissions(permissions) { isGranted -> + if (isGranted == true) { + request.grant(resources) + } else { + request.deny() } } - permissionLauncher.launch(permissions) } else { - request.grant(request.resources) + request.grant(resources) } } + private fun androidPermissionsForResources(resources: Array): MutableList { + val permissionList: MutableList = ArrayList() + if (resources.contains(PermissionRequest.RESOURCE_VIDEO_CAPTURE)) { + permissionList.add(Manifest.permission.CAMERA) + } + if (resources.contains(PermissionRequest.RESOURCE_AUDIO_CAPTURE)) { + permissionList.add(Manifest.permission.MODIFY_AUDIO_SETTINGS) + permissionList.add(Manifest.permission.RECORD_AUDIO) + } + return permissionList + } + + private external fun onPermissionRequestNative(webviewId: String, resources: Array): Int + private external fun onGeolocationPermissionRequestNative(webviewId: String, origin: String): Int + /** * Show the browser alert modal * @param view @@ -240,28 +242,36 @@ class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() { callback: GeolocationPermissions.Callback ) { super.onGeolocationPermissionsShowPrompt(origin, callback) + when (onGeolocationPermissionRequestNative(webViewId, origin)) { + 1 -> { + callback.invoke(origin, false, false) + return + } + } + Logger.debug("onGeolocationPermissionsShowPrompt: DOING IT HERE FOR ORIGIN: $origin") - val geoPermissions = - arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION) + val geoPermissions = definedGeolocationPermissions() + if (geoPermissions.isEmpty()) { + callback.invoke(origin, false, false) + return + } + if (!PermissionHelper.hasPermissions(activity, geoPermissions)) { - permissionListener = object : PermissionListener { - override fun onPermissionSelect(isGranted: Boolean?) { - if (isGranted == true) { + activity.requestPermissions(geoPermissions) { isGranted -> + if (isGranted == true) { + callback.invoke(origin, true, false) + } else { + val coarsePermission = + arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && + PermissionHelper.hasPermissions(activity, coarsePermission) + ) { callback.invoke(origin, true, false) } else { - val coarsePermission = - arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && - PermissionHelper.hasPermissions(activity, coarsePermission) - ) { - callback.invoke(origin, true, false) - } else { - callback.invoke(origin, false, false) - } + callback.invoke(origin, false, false) } } } - permissionLauncher.launch(geoPermissions) } else { // permission is already granted callback.invoke(origin, true, false) @@ -269,6 +279,26 @@ class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() { } } + private fun safePermissionRequestResources(resources: Array): Array { + return resources.filter { + it == PermissionRequest.RESOURCE_AUDIO_CAPTURE || + it == PermissionRequest.RESOURCE_VIDEO_CAPTURE || + it == PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID || + it == PermissionRequest.RESOURCE_MIDI_SYSEX + }.toTypedArray() + } + + private fun definedGeolocationPermissions(): Array { + val permissions = ArrayList() + if (PermissionHelper.hasDefinedPermission(activity, Manifest.permission.ACCESS_COARSE_LOCATION)) { + permissions.add(Manifest.permission.ACCESS_COARSE_LOCATION) + } + if (PermissionHelper.hasDefinedPermission(activity, Manifest.permission.ACCESS_FINE_LOCATION)) { + permissions.add(Manifest.permission.ACCESS_FINE_LOCATION) + } + return permissions.toTypedArray() + } + override fun onShowFileChooser( webView: WebView, filePathCallback: ValueCallback?>, @@ -282,18 +312,15 @@ class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() { if (isMediaCaptureSupported) { showMediaCaptureOrFilePicker(filePathCallback, fileChooserParams, captureVideo) } else { - permissionListener = object : PermissionListener { - override fun onPermissionSelect(isGranted: Boolean?) { - if (isGranted == true) { - showMediaCaptureOrFilePicker(filePathCallback, fileChooserParams, captureVideo) - } else { - Logger.warn(Logger.tags("FileChooser"), "Camera permission not granted") - filePathCallback.onReceiveValue(null) - } + val camPermission = arrayOf(Manifest.permission.CAMERA) + activity.requestPermissions(camPermission) { isGranted -> + if (isGranted == true) { + showMediaCaptureOrFilePicker(filePathCallback, fileChooserParams, captureVideo) + } else { + Logger.warn(Logger.tags("FileChooser"), "Camera permission not granted") + filePathCallback.onReceiveValue(null) } } - val camPermission = arrayOf(Manifest.permission.CAMERA) - permissionLauncher.launch(camPermission) } } else { showFilePicker(filePathCallback, fileChooserParams) @@ -340,16 +367,13 @@ class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() { return false } takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageFileUri) - activityListener = object : ActivityResultListener { - override fun onActivityResult(result: ActivityResult?) { - var res: Array? = null - if (result?.resultCode == Activity.RESULT_OK) { - res = arrayOf(imageFileUri) - } - filePathCallback.onReceiveValue(res) + activity.launchActivityForResult(takePictureIntent) { result -> + var res: Array? = null + if (result?.resultCode == Activity.RESULT_OK) { + res = arrayOf(imageFileUri) } + filePathCallback.onReceiveValue(res) } - activityLauncher.launch(takePictureIntent) return true } @@ -358,16 +382,13 @@ class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() { if (takeVideoIntent.resolveActivity(activity.packageManager) == null) { return false } - activityListener = object : ActivityResultListener { - override fun onActivityResult(result: ActivityResult?) { - var res: Array? = null - if (result?.resultCode == Activity.RESULT_OK) { - res = arrayOf(result.data!!.data) - } - filePathCallback.onReceiveValue(res) + activity.launchActivityForResult(takeVideoIntent) { result -> + var res: Array? = null + if (result?.resultCode == Activity.RESULT_OK) { + res = arrayOf(result.data!!.data) } + filePathCallback.onReceiveValue(res) } - activityLauncher.launch(takeVideoIntent) return true } @@ -387,26 +408,23 @@ class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() { } } try { - activityListener = object : ActivityResultListener { - override fun onActivityResult(result: ActivityResult?) { - val res: Array? - val resultIntent = result?.data - if (result?.resultCode == Activity.RESULT_OK && resultIntent!!.clipData != null) { - val numFiles = resultIntent.clipData!!.itemCount - res = arrayOfNulls(numFiles) - for (i in 0 until numFiles) { - res[i] = resultIntent.clipData!!.getItemAt(i).uri - } - } else { - res = FileChooserParams.parseResult( - result?.resultCode ?: 0, - resultIntent - ) + activity.launchActivityForResult(intent) { result -> + val res: Array? + val resultIntent = result?.data + if (result?.resultCode == Activity.RESULT_OK && resultIntent!!.clipData != null) { + val numFiles = resultIntent.clipData!!.itemCount + res = arrayOfNulls(numFiles) + for (i in 0 until numFiles) { + res[i] = resultIntent.clipData!!.getItemAt(i).uri } - filePathCallback.onReceiveValue(res) + } else { + res = FileChooserParams.parseResult( + result?.resultCode ?: 0, + resultIntent + ) } + filePathCallback.onReceiveValue(res) } - activityLauncher.launch(intent) } catch (e: ActivityNotFoundException) { filePathCallback.onReceiveValue(null) } diff --git a/src/android/kotlin/WryActivity.kt b/src/android/kotlin/WryActivity.kt index 7152e352b..9fe255468 100644 --- a/src/android/kotlin/WryActivity.kt +++ b/src/android/kotlin/WryActivity.kt @@ -11,6 +11,10 @@ import android.os.Bundle import android.webkit.WebView import android.view.KeyEvent import androidx.activity.OnBackPressedCallback +import androidx.activity.result.ActivityResult +import androidx.activity.result.ActivityResultCallback +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner @@ -48,11 +52,25 @@ object WryLifecycleObserver : DefaultLifecycleObserver { abstract class WryActivity : AppCompatActivity() { private lateinit var mWebView: RustWebView + private lateinit var permissionLauncher: ActivityResultLauncher> + private lateinit var activityLauncher: ActivityResultLauncher + private var permissionListener: ((Boolean?) -> Unit)? = null + private var activityListener: ((ActivityResult?) -> Unit)? = null var id: Int = 0 open val handleBackNavigation: Boolean = true open fun onWebViewCreate(webView: WebView) { } + fun requestPermissions(permissions: Array, listener: (Boolean?) -> Unit) { + permissionListener = listener + permissionLauncher.launch(permissions) + } + + fun launchActivityForResult(intent: Intent, listener: (ActivityResult?) -> Unit) { + activityListener = listener + activityLauncher.launch(intent) + } + fun setWebView(webView: RustWebView) { mWebView = webView @@ -112,6 +130,23 @@ abstract class WryActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) id = savedInstanceState?.getInt(ACTIVITY_ID_KEY) ?: intent.extras?.getInt(ACTIVITY_ID_KEY) ?: hashCode() + val permissionCallback = + ActivityResultCallback { isGranted: Map -> + permissionListener?.let { + var granted = true + for ((_, value) in isGranted) { + if (!value) granted = false + } + it(granted) + } + } + permissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions(), permissionCallback) + activityLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + activityListener?.invoke(result) + } ProcessLifecycleOwner.get().lifecycle.addObserver(WryLifecycleObserver) Rust.onActivityCreate(this) } diff --git a/src/android/main_pipe.rs b/src/android/main_pipe.rs index 52308fd86..de232a325 100644 --- a/src/android/main_pipe.rs +++ b/src/android/main_pipe.rs @@ -36,24 +36,17 @@ pub struct ActivityProxy { pub activity: GlobalRef, pub window_manager: GlobalRef, pub webview: Option, - pub webchrome_client: GlobalRef, pub java_vm: *mut c_void, } unsafe impl Send for ActivityProxy {} impl ActivityProxy { - pub fn new( - vm: JavaVM, - activity: GlobalRef, - window_manager: GlobalRef, - webchrome_client: GlobalRef, - ) -> Self { + pub fn new(vm: JavaVM, activity: GlobalRef, window_manager: GlobalRef) -> Self { Self { activity, window_manager, webview: None, - webchrome_client, java_vm: vm.get_java_vm_pointer() as *mut _, } } @@ -75,16 +68,14 @@ pub fn register_activity_proxy( id: ActivityId, activity: GlobalRef, window_manager: GlobalRef, - webchrome_client: GlobalRef, ) { let mut activity_proxy = ACTIVITY_PROXY.lock().unwrap(); if let Some(proxy) = activity_proxy.get_mut(&id) { proxy.activity = activity; proxy.window_manager = window_manager; - proxy.webchrome_client = webchrome_client; proxy.java_vm = vm.get_java_vm_pointer() as *mut _; } else { - let proxy = ActivityProxy::new(vm, activity, window_manager, webchrome_client); + let proxy = ActivityProxy::new(vm, activity, window_manager); activity_proxy.insert(id, proxy.clone()); } } @@ -146,9 +137,7 @@ impl<'a> MainPipe<'a> { if let Ok((activity_id, message)) = CHANNEL.1.recv() { match message { WebViewMessage::CreateWebView(attrs) => { - let Some((activity, web_chrome_client)) = - activity_proxy(activity_id).map(|p| (p.activity.clone(), p.webchrome_client.clone())) - else { + let Some(activity) = activity_proxy(activity_id).map(|p| p.activity.clone()) else { #[cfg(debug_assertions)] eprintln!("no activity found for activity id: {}", activity_id); return Ok(()); @@ -282,12 +271,25 @@ impl<'a> MainPipe<'a> { "(Landroid/webkit/WebViewClient;)V", &[(&webview_client).into()], )?; - // set webchrome client + // Create and set webchrome client + let rust_webchrome_client_class = find_class( + &mut self.env, + &activity, + format!("{}/RustWebChromeClient", PACKAGE.get().unwrap()), + )?; + let web_chrome_client = self.env.new_object( + &rust_webchrome_client_class, + format!( + "(L{}/WryActivity;Ljava/lang/String;)V", + PACKAGE.get().unwrap() + ), + &[(&activity).into(), (&id).into()], + )?; self.env.call_method( &webview, "setWebChromeClient", "(Landroid/webkit/WebChromeClient;)V", - &[web_chrome_client.as_obj().into()], + &[(&web_chrome_client).into()], )?; // Add javascript interface (IPC) diff --git a/src/android/mod.rs b/src/android/mod.rs index 8f44c7040..85837d71e 100644 --- a/src/android/mod.rs +++ b/src/android/mod.rs @@ -5,7 +5,7 @@ use super::{PageLoadEvent, WebViewAttributes, RGBA}; use crate::{ custom_protocol_workaround, inject_initialization_scripts::inject_scripts_into_html, Error, - RequestAsyncResponder, Result, + PermissionKind, PermissionResponse, RequestAsyncResponder, Result, }; use crossbeam_channel::*; @@ -74,6 +74,7 @@ define_static_handlers! { TITLE_CHANGE_HANDLER = UnsafeTitleHandler { handler: Box }; URL_LOADING_OVERRIDE = UnsafeUrlLoadingOverride { handler: Box bool> }; ON_LOAD_HANDLER = UnsafeOnPageLoadHandler { handler: Box }; + PERMISSION_HANDLER = UnsafePermissionHandler { handler: Box PermissionResponse> }; } define_static_handlers! { WebviewId, WITH_ASSET_LOADER = bool; @@ -95,6 +96,7 @@ pub fn destroy_webview(activity_id: ActivityId, webview_id: &WebviewId) { TITLE_CHANGE_HANDLER.lock().unwrap().remove(webview_id); URL_LOADING_OVERRIDE.lock().unwrap().remove(webview_id); ON_LOAD_HANDLER.lock().unwrap().remove(webview_id); + PERMISSION_HANDLER.lock().unwrap().remove(webview_id); WITH_ASSET_LOADER.lock().unwrap().remove(webview_id); ASSET_LOADER_DOMAIN.lock().unwrap().remove(webview_id); } @@ -131,25 +133,7 @@ pub unsafe fn android_setup( .unwrap(); let window_manager = env.new_global_ref(window_manager).unwrap(); - // we must create the WebChromeClient here because it calls `registerForActivityResult`, - // which gives an `LifecycleOwners must call register before they are STARTED.` error when called outside the onCreate hook - let rust_webchrome_client_class = find_class( - &mut env, - activity.as_obj(), - format!("{}/RustWebChromeClient", PACKAGE.get().unwrap()), - ) - .unwrap(); - let webchrome_client = env - .new_object( - &rust_webchrome_client_class, - format!("(L{}/WryActivity;)V", PACKAGE.get().unwrap()), - &[activity.as_obj().into()], - ) - .unwrap(); - - let webchrome_client = env.new_global_ref(webchrome_client).unwrap(); - - register_activity_proxy(vm, activity_id, activity, window_manager, webchrome_client); + register_activity_proxy(vm, activity_id, activity, window_manager); if let Some(webview_attributes) = WEBVIEW_ATTRIBUTES.lock().unwrap().get(&activity_id) { MainPipe::send( @@ -315,6 +299,15 @@ impl InnerWebView { .insert(id.clone(), UnsafeOnPageLoadHandler::new(h)); } + if let Some(permission_handler) = attributes.permission_handler { + let permission_handler: Box PermissionResponse> = + permission_handler; + PERMISSION_HANDLER + .lock() + .unwrap() + .insert(id.clone(), UnsafePermissionHandler::new(permission_handler)); + } + let attributes = CreateWebViewAttributes { id: id.clone(), url, diff --git a/src/lib.rs b/src/lib.rs index 38fbc9e2a..2a4238368 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -352,6 +352,7 @@ mod custom_protocol_workaround; mod error; #[cfg(any(target_os = "android", test))] mod inject_initialization_scripts; +mod permissions; mod proxy; #[cfg(any(target_os = "macos", target_os = "android", target_os = "ios"))] mod util; @@ -411,6 +412,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; @@ -791,6 +793,39 @@ 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 for geolocation, microphone, camera, + /// protected media, and MIDI requests. Android runtime permissions may still + /// trigger native OS prompts before access is granted. + /// + /// ## 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 PermissionResponse + Send + Sync>>, /// Controls the WebView's browser-level general autofill behavior. /// /// **This option does not disable password or credit card autofill.** @@ -850,6 +885,7 @@ impl Default for WebViewAttributes<'_> { }), background_throttling: None, javascript_disabled: false, + permission_handler: None, general_autofill_enabled: true, } } @@ -1264,6 +1300,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(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 diff --git a/src/permissions.rs b/src/permissions.rs new file mode 100644 index 000000000..84363a3c7 --- /dev/null +++ b/src/permissions.rs @@ -0,0 +1,161 @@ +/// Permission types that can be requested by the webview. +/// +/// See [`crate::WebViewBuilder::with_permission_handler`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum PermissionKind { + /// Microphone access permission. + Microphone, + /// Camera access permission. + Camera, + /// Geolocation access permission. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_GEOLOCATION`. + /// - **Linux**: Supported via `GeolocationPermissionRequest`. + /// - **Android**: Supported via `WebChromeClient.onGeolocationPermissionsShowPrompt`. + /// - **macOS / iOS**: Not yet supported by platform backends. + Geolocation, + /// Notifications permission. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_NOTIFICATIONS`. + /// - **Linux**: Supported via `NotificationPermissionRequest`. + /// - **macOS / Android / iOS**: Not yet supported by platform backends. + Notifications, + /// Clipboard read permission. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_CLIPBOARD_READ`. + /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. + ClipboardRead, + /// Display capture permission (for getDisplayMedia). + DisplayCapture, + /// Midi access permission. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_MIDI_SYSTEM_EXCLUSIVE_MESSAGES`. + /// - **Android**: Supported via `android.webkit.resource.MIDI_SYSEX`. + /// - **macOS / Linux / iOS**: Not yet supported by platform backends. + Midi, + /// Sensors (accelerometer, gyroscope, etc.) access permission. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_OTHER_SENSORS`. + /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. + Sensors, + /// Media key system access permission. + /// + /// ## Platform-specific + /// + /// - **Android**: Supported via `android.webkit.resource.PROTECTED_MEDIA_ID`. + /// - **Windows / macOS / Linux / iOS**: Not yet supported by platform backends. + MediaKeySystemAccess, + /// Local fonts access permission. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_LOCAL_FONTS`. + /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. + LocalFonts, + /// Window management permission. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_WINDOW_MANAGEMENT`. + /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. + WindowManagement, + /// Pointer lock permission. + /// + /// ## Platform-specific + /// + /// - **Linux**: Supported via `PointerLockPermissionRequest`. + /// - **Windows / macOS / Android / iOS**: Not yet supported by platform backends. + PointerLock, + /// Automatic downloads permission (multiple downloads without user interaction). + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_MULTIPLE_AUTOMATIC_DOWNLOADS`. + /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. + AutomaticDownloads, + /// File system access permission (read/write via File System Access API). + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_FILE_READ_WRITE`. + /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. + FileSystemAccess, + /// Media autoplay permission. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_AUTOPLAY`. + /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. + Autoplay, + /// Other unrecognized permission type. + Other, +} + +impl std::fmt::Display for PermissionKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Microphone => write!(f, "microphone"), + Self::Camera => write!(f, "camera"), + Self::Geolocation => write!(f, "geolocation"), + Self::Notifications => write!(f, "notifications"), + Self::ClipboardRead => write!(f, "clipboard-read"), + Self::DisplayCapture => write!(f, "display-capture"), + Self::Midi => write!(f, "midi"), + Self::Sensors => write!(f, "sensors"), + Self::MediaKeySystemAccess => write!(f, "media-key-system-access"), + Self::LocalFonts => write!(f, "local-fonts"), + Self::WindowManagement => write!(f, "window-management"), + Self::PointerLock => write!(f, "pointer-lock"), + Self::AutomaticDownloads => write!(f, "automatic-downloads"), + Self::FileSystemAccess => write!(f, "file-system-access"), + Self::Autoplay => write!(f, "autoplay"), + Self::Other => write!(f, "other"), + } + } +} + +/// Response for permission requests. +/// +/// See [`crate::WebViewBuilder::with_permission_handler`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub enum PermissionResponse { + /// Grant the permission. + Allow, + /// Deny the permission. + Deny, + /// Use default behavior (show system prompt). + /// + /// ## Platform-specific + /// + /// - **Linux**: The default behavior is [`Self::Deny`] + #[default] + Default, + /// Explicitly prompt the user (system dialog). + /// + /// ## Platform-specific + /// + /// - **Linux**: Not supported, same as [`Self::Deny`] + Prompt, +} + +impl std::fmt::Display for PermissionResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Allow => write!(f, "allow"), + Self::Deny => write!(f, "deny"), + Self::Default => write!(f, "default"), + Self::Prompt => write!(f, "prompt"), + } + } +} diff --git a/src/webkitgtk/mod.rs b/src/webkitgtk/mod.rs index 761e27229..f916be771 100644 --- a/src/webkitgtk/mod.rs +++ b/src/webkitgtk/mod.rs @@ -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, @@ -35,10 +36,12 @@ use std::{ #[cfg(any(debug_assertions, feature = "devtools"))] use webkit2gtk::WebInspectorExt; use webkit2gtk::{ - AutoplayPolicy, CookieManagerExt, InputMethodContextExt, LoadEvent, NavigationPolicyDecision, - NavigationPolicyDecisionExt, NetworkProxyMode, NetworkProxySettings, PolicyDecisionType, - PrintOperationExt, SettingsExt, URIRequest, URIRequestExt, UserContentInjectedFrames, - UserContentManager, UserContentManagerExt, UserScript, UserScriptInjectionTime, + AutoplayPolicy, CookieManagerExt, GeolocationPermissionRequest, InputMethodContextExt, LoadEvent, + NavigationPolicyDecision, NavigationPolicyDecisionExt, NetworkProxyMode, NetworkProxySettings, + NotificationPermissionRequest, PermissionRequestExt, PointerLockPermissionRequest, + PolicyDecisionType, PrintOperationExt, SettingsExt, URIRequest, URIRequestExt, + UserContentInjectedFrames, UserContentManager, UserContentManagerExt, UserMediaPermissionRequest, + UserMediaPermissionRequestExt, UserScript, UserScriptInjectionTime, WebContextExt as Webkit2gtkWeContextExt, WebView, WebViewExt, WebsiteDataManagerExt, WebsiteDataManagerExtManual, WebsitePolicies, }; @@ -53,7 +56,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; @@ -576,6 +580,93 @@ impl InnerWebView { }); } + // Permission handler + if let Some(permission_handler) = attributes.permission_handler.take() { + webview.connect_permission_request(move |_webview, request| { + if let Some(media_request) = request.downcast_ref::() { + let is_audio = media_request.is_for_audio_device(); + let is_video = media_request.is_for_video_device(); + + #[cfg(feature = "v2_42")] + let is_display = media_request.is_for_display_device(); + #[cfg(not(feature = "v2_42"))] + let is_display = !is_audio && !is_video; + + if is_display { + // Screen sharing request + let response = permission_handler(PermissionKind::DisplayCapture); + return match response { + PermissionResponse::Allow => { + request.allow(); + true + } + PermissionResponse::Deny => { + request.deny(); + true + } + PermissionResponse::Default | PermissionResponse::Prompt => false, + }; + } + + // For combined audio+video requests, check each individually. + // Deny wins: if either is denied, deny the whole request. + let mut allow = true; + let mut handled = false; + + if is_audio { + handled = true; + match permission_handler(PermissionKind::Microphone) { + PermissionResponse::Allow => {} + PermissionResponse::Deny => allow = false, + PermissionResponse::Default | PermissionResponse::Prompt => handled = false, + } + } + + if is_video && allow { + handled = true; + match permission_handler(PermissionKind::Camera) { + PermissionResponse::Allow => {} + PermissionResponse::Deny => allow = false, + PermissionResponse::Default | PermissionResponse::Prompt => handled = false, + } + } + + if handled { + if allow { + request.allow(); + } else { + request.deny(); + } + true + } else { + false // let WebKitGTK show default prompt + } + } else { + let permission_kind = if request.is::() { + PermissionKind::Geolocation + } else if request.is::() { + PermissionKind::Notifications + } else if request.is::() { + PermissionKind::PointerLock + } else { + PermissionKind::Other + }; + + match permission_handler(permission_kind) { + PermissionResponse::Allow => { + request.allow(); + true + } + PermissionResponse::Deny => { + request.deny(); + true + } + PermissionResponse::Default | PermissionResponse::Prompt => false, + } + } + }); + } + // Download handler if attributes.download_started_handler.is_some() || attributes.download_completed_handler.is_some() diff --git a/src/webview2/mod.rs b/src/webview2/mod.rs index ffa3ece63..f4f46ba67 100644 --- a/src/webview2/mod.rs +++ b/src/webview2/mod.rs @@ -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; @@ -514,6 +514,58 @@ impl InnerWebView { } } + // Permission handler + if let Some(permission_handler) = attributes.permission_handler.take() { + 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, + COREWEBVIEW2_PERMISSION_KIND_LOCAL_FONTS => PermissionKind::LocalFonts, + COREWEBVIEW2_PERMISSION_KIND_OTHER_SENSORS => PermissionKind::Sensors, + COREWEBVIEW2_PERMISSION_KIND_MIDI_SYSTEM_EXCLUSIVE_MESSAGES => PermissionKind::Midi, + COREWEBVIEW2_PERMISSION_KIND_MULTIPLE_AUTOMATIC_DOWNLOADS => { + PermissionKind::AutomaticDownloads + } + COREWEBVIEW2_PERMISSION_KIND_FILE_READ_WRITE => PermissionKind::FileSystemAccess, + COREWEBVIEW2_PERMISSION_KIND_AUTOPLAY => PermissionKind::Autoplay, + COREWEBVIEW2_PERMISSION_KIND_WINDOW_MANAGEMENT => PermissionKind::WindowManagement, + _ => 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 | 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("://") { diff --git a/src/wkwebview/class/wry_web_view_ui_delegate.rs b/src/wkwebview/class/wry_web_view_ui_delegate.rs index 9b21cc0d7..1643d51d4 100644 --- a/src/wkwebview/class/wry_web_view_ui_delegate.rs +++ b/src/wkwebview/class/wry_web_view_ui_delegate.rs @@ -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}; @@ -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 { @@ -84,6 +84,7 @@ pub struct WryWebViewUIDelegateIvars { new_window_req_handler: Option NewWindowResponse>>, #[cfg(target_os = "macos")] new_windows: Rc>>, + permission_handler: Option PermissionResponse + Send + Sync>>, } define_class!( @@ -129,11 +130,40 @@ define_class!( _webview: &WryWebView, _origin: &WKSecurityOrigin, _frame: &WKFrameInfo, - _capture_type: WKMediaCaptureType, + capture_type: WKMediaCaptureType, decision_handler: &Block, ) { - //https://developer.apple.com/documentation/webkit/wkpermissiondecision?language=objc - (*decision_handler).call((WKPermissionDecision::Grant,)); + // Call user's permission handler if set + let decision = if let Some(handler) = &self.ivars().permission_handler { + let translate_response = |res: PermissionResponse| match res { + PermissionResponse::Allow => WKPermissionDecision::Grant, + PermissionResponse::Deny => WKPermissionDecision::Deny, + PermissionResponse::Default => WKPermissionDecision::Prompt, + PermissionResponse::Prompt => WKPermissionDecision::Prompt, + }; + + match capture_type { + WKMediaCaptureType::Camera => translate_response(handler(PermissionKind::Camera)), + WKMediaCaptureType::Microphone => translate_response(handler(PermissionKind::Microphone)), + WKMediaCaptureType::CameraAndMicrophone => { + let mic_res = handler(PermissionKind::Microphone); + let cam_res = handler(PermissionKind::Camera); + + match (mic_res, cam_res) { + (PermissionResponse::Allow, PermissionResponse::Allow) => WKPermissionDecision::Grant, + (PermissionResponse::Deny, _) | (_, PermissionResponse::Deny) => { + WKPermissionDecision::Deny + } + _ => WKPermissionDecision::Prompt, + } + } + _ => translate_response(handler(PermissionKind::Other)), + } + } else { + WKPermissionDecision::Grant + }; + + (*decision_handler).call((decision,)); } #[cfg(target_os = "macos")] @@ -265,6 +295,7 @@ impl WryWebViewUIDelegate { pub fn new( mtm: MainThreadMarker, new_window_req_handler: Option NewWindowResponse>>, + permission_handler: Option PermissionResponse + Send + Sync>>, ) -> Retained { #[cfg(target_os = "ios")] let _new_window_req_handler = new_window_req_handler; @@ -276,6 +307,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] } } diff --git a/src/wkwebview/mod.rs b/src/wkwebview/mod.rs index 685634980..6bc832937 100644 --- a/src/wkwebview/mod.rs +++ b/src/wkwebview/mod.rs @@ -596,8 +596,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::new(mtm, attributes.new_window_req_handler); + let ui_delegate: Retained = 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));