From 4f76fd15fc299574d05ad532f0a041a09ca89682 Mon Sep 17 00:00:00 2001 From: Jason Tsai Date: Thu, 11 Jul 2024 16:35:49 +0800 Subject: [PATCH] refactor(macos): move custom class to individual files --- Cargo.toml | 1 - src/lib.rs | 6 +- .../class/document_title_changed_observer.rs | 98 ++ src/wkwebview/class/mod.rs | 12 + src/wkwebview/class/url_scheme_handler.rs | 279 ++++++ src/wkwebview/class/wry_download_delegate.rs | 102 ++ .../class/wry_navigation_delegate.rs | 158 +++ src/wkwebview/class/wry_web_view.rs | 152 +++ src/wkwebview/class/wry_web_view_delegate.rs | 108 ++ src/wkwebview/class/wry_web_view_parent.rs | 50 + .../class/wry_web_view_ui_delegate.rs | 88 ++ src/wkwebview/download.rs | 4 +- src/wkwebview/mod.rs | 946 +----------------- src/wkwebview/navigation.rs | 4 +- 14 files changed, 1077 insertions(+), 931 deletions(-) create mode 100644 src/wkwebview/class/document_title_changed_observer.rs create mode 100644 src/wkwebview/class/mod.rs create mode 100644 src/wkwebview/class/url_scheme_handler.rs create mode 100644 src/wkwebview/class/wry_download_delegate.rs create mode 100644 src/wkwebview/class/wry_navigation_delegate.rs create mode 100644 src/wkwebview/class/wry_web_view.rs create mode 100644 src/wkwebview/class/wry_web_view_delegate.rs create mode 100644 src/wkwebview/class/wry_web_view_parent.rs create mode 100644 src/wkwebview/class/wry_web_view_ui_delegate.rs diff --git a/Cargo.toml b/Cargo.toml index e62373a57c..2b3eda72df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -144,7 +144,6 @@ objc2-app-kit = { version = "0.2.0", features = [ "NSResponder", "NSOpenPanel", "NSSavePanel", - "NSWindow", "NSMenu", ] } objc2-ui-kit = { version = "0.2.2", features = [ diff --git a/src/lib.rs b/src/lib.rs index 783bb10c6c..dc282babab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -239,7 +239,7 @@ pub(crate) mod wkwebview; #[cfg(any(target_os = "macos", target_os = "ios"))] use wkwebview::*; #[cfg(any(target_os = "macos", target_os = "ios"))] -pub use wkwebview::{PrintMargin, PrintOptions}; +pub use wkwebview::{PrintMargin, PrintOptions, WryWebView}; #[cfg(target_os = "windows")] pub(crate) mod webview2; @@ -1631,10 +1631,6 @@ impl WebViewExtMacOS for WebView { } fn ns_window(&self) -> Retained { - // unsafe { - // let ns_window: cocoa::base::id = msg_send![self.webview.webview, window]; - // ns_window - // } self.webview.webview.window().unwrap().clone() } diff --git a/src/wkwebview/class/document_title_changed_observer.rs b/src/wkwebview/class/document_title_changed_observer.rs new file mode 100644 index 0000000000..1e0e0edff5 --- /dev/null +++ b/src/wkwebview/class/document_title_changed_observer.rs @@ -0,0 +1,98 @@ +// Copyright 2020-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ffi::c_void, ptr::null_mut}; + +use objc2::{ + declare_class, msg_send, msg_send_id, + mutability::InteriorMutable, + rc::Retained, + runtime::{AnyObject, NSObject}, + ClassType, DeclaredClass, +}; +use objc2_foundation::{ + NSDictionary, NSKeyValueChangeKey, NSKeyValueObservingOptions, + NSObjectNSKeyValueObserverRegistration, NSObjectProtocol, NSString, +}; + +use crate::WryWebView; +pub struct DocumentTitleChangedObserverIvars { + pub object: Retained, + pub handler: Box, +} + +declare_class!( + pub struct DocumentTitleChangedObserver; + + unsafe impl ClassType for DocumentTitleChangedObserver { + type Super = NSObject; + type Mutability = InteriorMutable; + const NAME: &'static str = "DocumentTitleChangedObserver"; + } + + impl DeclaredClass for DocumentTitleChangedObserver { + type Ivars = DocumentTitleChangedObserverIvars; + } + + unsafe impl DocumentTitleChangedObserver { + #[method(observeValueForKeyPath:ofObject:change:context:)] + fn observe_value_for_key_path( + &self, + key_path: Option<&NSString>, + of_object: Option<&AnyObject>, + _change: Option<&NSDictionary>, + _context: *mut c_void, + ) { + if let (Some(key_path), Some(object)) = (key_path, of_object) { + if key_path.to_string() == "title" { + unsafe { + let handler = &self.ivars().handler; + // if !handler.is_null() { + let title: *const NSString = msg_send![object, title]; + handler((*title).to_string()); + // } + } + } + } + } + } + + unsafe impl NSObjectProtocol for DocumentTitleChangedObserver {} +); + +impl DocumentTitleChangedObserver { + pub fn new(webview: Retained, handler: Box) -> Retained { + let observer = Self::alloc().set_ivars(DocumentTitleChangedObserverIvars { + object: webview, + handler, + }); + + let observer: Retained = unsafe { msg_send_id![super(observer), init] }; + + unsafe { + observer + .ivars() + .object + .addObserver_forKeyPath_options_context( + &observer, + &NSString::from_str("title"), + NSKeyValueObservingOptions::NSKeyValueObservingOptionNew, + null_mut(), + ); + } + + observer + } +} + +impl Drop for DocumentTitleChangedObserver { + fn drop(&mut self) { + unsafe { + self + .ivars() + .object + .removeObserver_forKeyPath(&self, &NSString::from_str("title")); + } + } +} diff --git a/src/wkwebview/class/mod.rs b/src/wkwebview/class/mod.rs new file mode 100644 index 0000000000..0381ef9909 --- /dev/null +++ b/src/wkwebview/class/mod.rs @@ -0,0 +1,12 @@ +// Copyright 2020-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +pub mod wry_web_view; +pub mod wry_web_view_delegate; +pub mod document_title_changed_observer; +pub mod wry_navigation_delegate; +pub mod wry_download_delegate; +pub mod wry_web_view_ui_delegate; +pub mod wry_web_view_parent; +pub mod url_scheme_handler; \ No newline at end of file diff --git a/src/wkwebview/class/url_scheme_handler.rs b/src/wkwebview/class/url_scheme_handler.rs new file mode 100644 index 0000000000..9d924f1d23 --- /dev/null +++ b/src/wkwebview/class/url_scheme_handler.rs @@ -0,0 +1,279 @@ +// Copyright 2020-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::borrow::Cow; +use std::ffi::c_void; +use std::ptr::NonNull; +use std::slice; + +use http::header::{CONTENT_LENGTH, CONTENT_TYPE}; +use http::{Request, Response as HttpResponse, StatusCode, Version}; +use objc2::rc::Retained; +use objc2::runtime::{AnyClass, AnyObject, ClassBuilder, ProtocolObject}; +use objc2::ClassType; +use objc2_foundation::NSObjectProtocol; +use objc2_foundation::{ + NSData, NSHTTPURLResponse, NSMutableDictionary, NSObject, NSString, NSURL, NSUUID, +}; +use objc2_web_kit::{WKURLSchemeHandler, WKURLSchemeTask}; + +use crate::wkwebview::WEBVIEW_IDS; +use crate::{RequestAsyncResponder, WryWebView}; + +pub fn create(name: &str) -> &AnyClass { + unsafe { + let scheme_name = format!("{}URLSchemeHandler", name); + let cls = ClassBuilder::new(&scheme_name, NSObject::class()); + let cls = match cls { + Some(mut cls) => { + cls.add_ivar::<*mut c_void>("function"); + cls.add_ivar::("webview_id"); + cls.add_method( + objc2::sel!(webView:startURLSchemeTask:), + start_task as extern "C" fn(_, _, _, _), + ); + cls.add_method( + objc2::sel!(webView:stopURLSchemeTask:), + stop_task as extern "C" fn(_, _, _, _), + ); + cls.register() + } + None => AnyClass::get(&scheme_name).expect("Failed to get the class definition"), + }; + cls + } +} + +// Task handler for custom protocol +extern "C" fn start_task<'a>( + this: &AnyObject, + _sel: objc2::runtime::Sel, + webview: *mut WryWebView, + task: *mut ProtocolObject, +) { + unsafe { + #[cfg(feature = "tracing")] + let span = tracing::info_span!(parent: None, "wry::custom_protocol::handle", uri = tracing::field::Empty) + .entered(); + + let task_key = (*task).hash(); // hash by task object address + let task_uuid = (*webview).add_custom_task_key(task_key); + + let ivar = this.class().instance_variable("webview_id").unwrap(); + let webview_id: u32 = ivar.load::(this).clone(); + let ivar = this.class().instance_variable("function").unwrap(); + let function: &*mut c_void = ivar.load(this); + if !function.is_null() { + let function = &mut *(*function as *mut Box>, RequestAsyncResponder)>); + + // Get url request + let request = (*task).request(); + let url = request.URL().unwrap(); + + let uri = url.absoluteString().unwrap().to_string(); + + #[cfg(feature = "tracing")] + span.record("uri", uri.clone()); + + // Get request method (GET, POST, PUT etc...) + let method = request.HTTPMethod().unwrap().to_string(); + + // Prepare our HttpRequest + let mut http_request = Request::builder().uri(uri).method(method.as_str()); + + // Get body + let mut sent_form_body = Vec::new(); + let body = request.HTTPBody(); + let body_stream = request.HTTPBodyStream(); + if let Some(body) = body { + let length = body.length(); + let data_bytes = body.bytes(); + sent_form_body = slice::from_raw_parts(data_bytes.as_ptr(), length).to_vec(); + } else if let Some(body_stream) = body_stream { + body_stream.open(); + + while body_stream.hasBytesAvailable() { + sent_form_body.reserve(128); + let p = sent_form_body.as_mut_ptr().add(sent_form_body.len()); + let read_length = sent_form_body.capacity() - sent_form_body.len(); + let count = body_stream.read_maxLength(NonNull::new(p).unwrap(), read_length); + sent_form_body.set_len(sent_form_body.len() + count as usize); + } + + body_stream.close(); + } + + // Extract all headers fields + let all_headers = request.allHTTPHeaderFields(); + + // get all our headers values and inject them in our request + if let Some(all_headers) = all_headers { + for current_header in all_headers.allKeys().to_vec() { + let header_value = all_headers.valueForKey(current_header).unwrap(); + + // inject the header into the request + http_request = http_request.header(current_header.to_string(), header_value.to_string()); + } + } + + let respond_with_404 = || { + let urlresponse = NSHTTPURLResponse::alloc(); + let response = NSHTTPURLResponse::initWithURL_statusCode_HTTPVersion_headerFields( + urlresponse, + &url, + StatusCode::NOT_FOUND.as_u16().try_into().unwrap(), + Some(&NSString::from_str( + format!("{:#?}", Version::HTTP_11).as_str(), + )), + None, + ) + .unwrap(); + (*task).didReceiveResponse(&response); + // Finish + (*task).didFinish(); + }; + + // send response + match http_request.body(sent_form_body) { + Ok(final_request) => { + let responder: Box>)> = + Box::new(move |sent_response| { + fn check_webview_id_valid(webview_id: u32) -> crate::Result<()> { + if !WEBVIEW_IDS.lock().unwrap().contains(&webview_id) { + return Err(crate::Error::CustomProtocolTaskInvalid); + } + Ok(()) + } + /// Task may not live longer than async custom protocol handler. + /// + /// There are roughly 2 ways to cause segfault: + /// 1. Task has stopped. pointer of the task not valid anymore. + /// 2. Task had stopped, but the pointer of the task has allocated to a new task. + /// Outdated custom handler may call to the new task instance and cause segfault. + fn check_task_is_valid( + webview: &WryWebView, + task_key: usize, + current_uuid: Retained, + ) -> crate::Result<()> { + let latest_task_uuid = webview.get_custom_task_uuid(task_key); + if let Some(latest_uuid) = latest_task_uuid { + if latest_uuid != current_uuid { + return Err(crate::Error::CustomProtocolTaskInvalid); + } + } else { + return Err(crate::Error::CustomProtocolTaskInvalid); + } + Ok(()) + } + + // FIXME: This is 10000% unsafe. `task` and `webview` are not guaranteed to be valid. + // We should consider use sync command only. + unsafe fn response( + task: *mut ProtocolObject, + webview: *mut WryWebView, + task_key: usize, + task_uuid: Retained, + webview_id: u32, + url: Retained, + sent_response: HttpResponse>, + ) -> crate::Result<()> { + check_task_is_valid(&*webview, task_key, task_uuid.clone())?; + + let content = sent_response.body(); + // default: application/octet-stream, but should be provided by the client + let wanted_mime = sent_response.headers().get(CONTENT_TYPE); + // default to 200 + let wanted_status_code = sent_response.status().as_u16() as i32; + // default to HTTP/1.1 + let wanted_version = format!("{:#?}", sent_response.version()); + + let mut headers = NSMutableDictionary::new(); + + if let Some(mime) = wanted_mime { + headers.insert_id( + NSString::from_str(mime.to_str().unwrap()).as_ref(), + NSString::from_str(CONTENT_TYPE.as_str()), + ); + } + headers.insert_id( + NSString::from_str(&content.len().to_string()).as_ref(), + NSString::from_str(CONTENT_LENGTH.as_str()), + ); + + // add headers + for (name, value) in sent_response.headers().iter() { + if let Ok(value) = value.to_str() { + headers.insert_id( + NSString::from_str(name.as_str()).as_ref(), + NSString::from_str(value), + ); + } + } + + let urlresponse = NSHTTPURLResponse::alloc(); + let response = NSHTTPURLResponse::initWithURL_statusCode_HTTPVersion_headerFields( + urlresponse, + &url, + wanted_status_code.try_into().unwrap(), + Some(&NSString::from_str(&wanted_version)), + Some(&headers), + ) + .unwrap(); + + check_webview_id_valid(webview_id)?; + check_task_is_valid(&*webview, task_key, task_uuid.clone())?; + (*task).didReceiveResponse(&response); + + // Send data + let bytes = content.as_ptr() as *mut c_void; + let data = NSData::alloc(); + // MIGRATE NOTE: we copied the content to the NSData because content will be freed + // when out of scope but NSData will also free the content when it's done and cause doube free. + let data = NSData::initWithBytes_length(data, bytes, content.len()); + check_webview_id_valid(webview_id)?; + check_task_is_valid(&*webview, task_key, task_uuid.clone())?; + (*task).didReceiveData(&data); + + // Finish + check_webview_id_valid(webview_id)?; + check_task_is_valid(&*webview, task_key, task_uuid.clone())?; + (*task).didFinish(); + + (*webview).remove_custom_task_key(task_key); + Ok(()) + } + + let _ = response( + task, + webview, + task_key, + task_uuid, + webview_id, + url.clone(), + sent_response, + ); + }); + + #[cfg(feature = "tracing")] + let _span = tracing::info_span!("wry::custom_protocol::call_handler").entered(); + function(final_request, RequestAsyncResponder { responder }); + } + Err(_) => respond_with_404(), + }; + } else { + #[cfg(feature = "tracing")] + tracing::warn!( + "Either WebView or WebContext instance is dropped! This handler shouldn't be called." + ); + } + } +} +extern "C" fn stop_task( + _this: &ProtocolObject, + _sel: objc2::runtime::Sel, + webview: &mut WryWebView, + task: &ProtocolObject, +) { + webview.remove_custom_task_key(task.hash()); +} diff --git a/src/wkwebview/class/wry_download_delegate.rs b/src/wkwebview/class/wry_download_delegate.rs new file mode 100644 index 0000000000..5ac2814ff2 --- /dev/null +++ b/src/wkwebview/class/wry_download_delegate.rs @@ -0,0 +1,102 @@ +// Copyright 2020-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{path::PathBuf, ptr::null_mut, rc::Rc}; + +use objc2::{ + declare_class, msg_send_id, mutability::MainThreadOnly, rc::Retained, runtime::NSObject, + ClassType, DeclaredClass, +}; +use objc2_foundation::{ + MainThreadMarker, NSData, NSError, NSObjectProtocol, NSString, NSURLResponse, NSURL, +}; +use objc2_web_kit::{WKDownload, WKDownloadDelegate}; + +use crate::wkwebview::download::{download_did_fail, download_did_finish, download_policy}; + +pub struct WryDownloadDelegateIvars { + pub started: *mut Box bool>, + pub completed: *mut Rc, bool)>, +} + +declare_class!( + pub struct WryDownloadDelegate; + + unsafe impl ClassType for WryDownloadDelegate { + type Super = NSObject; + type Mutability = MainThreadOnly; + const NAME: &'static str = "WryDownloadDelegate"; + } + + impl DeclaredClass for WryDownloadDelegate { + type Ivars = WryDownloadDelegateIvars; + } + + unsafe impl NSObjectProtocol for WryDownloadDelegate {} + + unsafe impl WKDownloadDelegate for WryDownloadDelegate { + #[method(download:decideDestinationUsingResponse:suggestedFilename:completionHandler:)] + fn download_policy( + &self, + download: &WKDownload, + response: &NSURLResponse, + suggested_path: &NSString, + handler: &block2::Block, + ) { + download_policy(self, download, response, suggested_path, handler); + } + + #[method(downloadDidFinish:)] + fn download_did_finish(&self, download: &WKDownload) { + download_did_finish(self, download); + } + + #[method(download:didFailWithError:resumeData:)] + fn download_did_fail( + &self, + download: &WKDownload, + error: &NSError, + resume_data: &NSData, + ) { + download_did_fail(self, download, error, resume_data); + } + } +); + +impl WryDownloadDelegate { + pub fn new( + download_started_handler: Option bool>>, + download_completed_handler: Option, bool)>>, + mtm: MainThreadMarker, + ) -> Retained { + let started = match download_started_handler { + Some(handler) => Box::into_raw(Box::new(handler)), + None => null_mut(), + }; + let completed = match download_completed_handler { + Some(handler) => Box::into_raw(Box::new(handler)), + None => null_mut(), + }; + let delegate = mtm + .alloc::() + .set_ivars(WryDownloadDelegateIvars { started, completed }); + + unsafe { msg_send_id![super(delegate), init] } + } +} + +impl Drop for WryDownloadDelegate { + fn drop(&mut self) { + if self.ivars().started != null_mut() { + unsafe { + drop(Box::from_raw(self.ivars().started)); + } + } + if self.ivars().completed != null_mut() { + unsafe { + drop(Box::from_raw(self.ivars().completed)); + } + } + } +} diff --git a/src/wkwebview/class/wry_navigation_delegate.rs b/src/wkwebview/class/wry_navigation_delegate.rs new file mode 100644 index 0000000000..682e6aa77e --- /dev/null +++ b/src/wkwebview/class/wry_navigation_delegate.rs @@ -0,0 +1,158 @@ +// Copyright 2020-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::sync::{Arc, Mutex}; + +use objc2::{ + declare_class, msg_send_id, mutability::MainThreadOnly, rc::Retained, runtime::NSObject, + ClassType, DeclaredClass, +}; +use objc2_foundation::{MainThreadMarker, NSObjectProtocol}; +use objc2_web_kit::{ + WKDownload, WKNavigation, WKNavigationAction, WKNavigationActionPolicy, WKNavigationDelegate, + WKNavigationResponse, WKNavigationResponsePolicy, WKWebView, +}; + +use crate::{ + url_from_webview, + wkwebview::{ + download::{navigation_download_action, navigation_download_response}, + navigation::{ + did_commit_navigation, did_finish_navigation, navigation_policy, navigation_policy_response, + }, + }, + PageLoadEvent, WryWebView, +}; + +use super::wry_download_delegate::WryDownloadDelegate; + +pub struct WryNavigationDelegateIvars { + pub pending_scripts: Arc>>>, + pub has_download_handler: bool, + pub navigation_policy_function: Box bool>, + pub download_delegate: Option>, + pub on_page_load_handler: Option>, +} + +declare_class!( + pub struct WryNavigationDelegate; + + unsafe impl ClassType for WryNavigationDelegate { + type Super = NSObject; + type Mutability = MainThreadOnly; + const NAME: &'static str = "WryNavigationDelegate"; + } + + impl DeclaredClass for WryNavigationDelegate { + type Ivars = WryNavigationDelegateIvars; + } + + unsafe impl NSObjectProtocol for WryNavigationDelegate {} + + unsafe impl WKNavigationDelegate for WryNavigationDelegate { + #[method(webView:decidePolicyForNavigationAction:decisionHandler:)] + fn navigation_policy( + &self, + webview: &WKWebView, + action: &WKNavigationAction, + handler: &block2::Block, + ) { + navigation_policy(self, webview, action, handler); + } + + #[method(webView:decidePolicyForNavigationResponse:decisionHandler:)] + fn navigation_policy_response( + &self, + webview: &WKWebView, + response: &WKNavigationResponse, + handler: &block2::Block, + ) { + navigation_policy_response(self, webview, response, handler); + } + + #[method(webView:didFinishNavigation:)] + fn did_finish_navigation( + &self, + webview: &WKWebView, + navigation: &WKNavigation, + ) { + did_finish_navigation(self, webview, navigation); + } + + #[method(webView:didCommitNavigation:)] + fn did_commit_navigation( + &self, + webview: &WKWebView, + navigation: &WKNavigation, + ) { + did_commit_navigation(self, webview, navigation); + } + + #[method(webView:navigationAction:didBecomeDownload:)] + fn navigation_download_action( + &self, + webview: &WKWebView, + action: &WKNavigationAction, + download: &WKDownload, + ) { + navigation_download_action(self, webview, action, download); + } + + #[method(webView:navigationResponse:didBecomeDownload:)] + fn navigation_download_response( + &self, + webview: &WKWebView, + response: &WKNavigationResponse, + download: &WKDownload, + ) { + navigation_download_response(self, webview, response, download); + } + } +); + +impl WryNavigationDelegate { + pub fn new( + webview: Retained, + pending_scripts: Arc>>>, + has_download_handler: bool, + navigation_handler: Option bool>>, + new_window_req_handler: Option bool>>, + download_delegate: Option>, + on_page_load_handler: Option>, + mtm: MainThreadMarker, + ) -> Retained { + let navigation_policy_function = Box::new(move |url: String, is_main_frame: bool| -> bool { + if is_main_frame { + navigation_handler + .as_ref() + .map_or(true, |navigation_handler| (navigation_handler)(url)) + } else { + new_window_req_handler + .as_ref() + .map_or(true, |new_window_req_handler| (new_window_req_handler)(url)) + } + }); + + let on_page_load_handler = if let Some(handler) = on_page_load_handler { + let custom_handler = Box::new(move |event| { + handler(event, url_from_webview(&webview).unwrap_or_default()); + }) as Box; + Some(custom_handler) + } else { + None + }; + + let delegate = mtm + .alloc::() + .set_ivars(WryNavigationDelegateIvars { + pending_scripts, + navigation_policy_function, + has_download_handler, + download_delegate, + on_page_load_handler, + }); + + unsafe { msg_send_id![super(delegate), init] } + } +} diff --git a/src/wkwebview/class/wry_web_view.rs b/src/wkwebview/class/wry_web_view.rs new file mode 100644 index 0000000000..c743a7e181 --- /dev/null +++ b/src/wkwebview/class/wry_web_view.rs @@ -0,0 +1,152 @@ +// Copyright 2020-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::collections::HashMap; + +use objc2::{ + declare_class, + mutability::MainThreadOnly, + rc::Retained, + runtime::{Bool, MessageReceiver, ProtocolObject}, + ClassType, DeclaredClass, +}; +use objc2_app_kit::NSEvent; +use objc2_foundation::NSUUID; +use objc2_web_kit::WKWebView; + +use crate::{ + wkwebview::{drag_drop, synthetic_mouse_events}, + DragDropEvent, +}; + +pub struct WryWebViewIvars { + pub(crate) is_child: bool, + #[cfg(target_os = "macos")] + pub(crate) drag_drop_handler: Box bool>, + #[cfg(target_os = "macos")] + pub(crate) accept_first_mouse: objc2::runtime::Bool, + pub(crate) custom_protocol_task_ids: HashMap>, +} + +declare_class!( + pub struct WryWebView; + + unsafe impl ClassType for WryWebView { + type Super = WKWebView; + type Mutability = MainThreadOnly; + const NAME: &'static str = "WryWebView"; + } + + impl DeclaredClass for WryWebView { + type Ivars = WryWebViewIvars; + } + + unsafe impl WryWebView { + #[method(performKeyEquivalent:)] + fn perform_key_equivalent( + &self, + _event: &NSEvent, + ) -> Bool { + // This is a temporary workaround for https://github.com/tauri-apps/tauri/issues/9426 + // FIXME: When the webview is a child webview, performKeyEquivalent always return YES + // and stop propagating the event to the window, hence the menu shortcut won't be + // triggered. However, overriding this method also means the cmd+key event won't be + // handled in webview, which means the key cannot be listened by JavaScript. + if self.ivars().is_child { + Bool::NO + } else { + unsafe { + let result: Bool = self.send_super_message( + WKWebView::class(), + objc2::sel!(performKeyEquivalent:), + (_event,), + ); + result + } + } + } + + #[method(acceptsFirstMouse:)] + fn accept_first_mouse( + &self, + _event: &NSEvent, + ) -> Bool { + self.ivars().accept_first_mouse + } + } + + // Drag & Drop + #[cfg(target_os = "macos")] + unsafe impl WryWebView { + #[method(draggingEntered:)] + fn dragging_entered( + &self, + drag_info: &ProtocolObject, + ) -> objc2_app_kit::NSDragOperation { + drag_drop::dragging_entered(self, drag_info) + } + + #[method(draggingUpdated:)] + fn dragging_updated( + &self, + drag_info: &ProtocolObject, + ) -> objc2_app_kit::NSDragOperation { + drag_drop::dragging_updated(self, drag_info) + } + + #[method(performDragOperation:)] + fn perform_drag_operation( + &self, + drag_info: &ProtocolObject, + ) -> Bool { + drag_drop::perform_drag_operation(self, drag_info) + } + + #[method(draggingExited:)] + fn dragging_exited( + &self, + drag_info: &ProtocolObject, + ) { + drag_drop::dragging_exited(self, drag_info) + } + } + + // Synthetic mouse events + #[cfg(target_os = "macos")] + unsafe impl WryWebView { + #[method(otherMouseDown:)] + fn other_mouse_down( + &self, + event: &NSEvent, + ) { + synthetic_mouse_events::other_mouse_down(self, event) + } + + #[method(otherMouseUp:)] + fn other_mouse_up( + &self, + event: &NSEvent, + ) { + synthetic_mouse_events::other_mouse_up(self, event) + } + } +); + +// Custom Protocol Task Checker +impl WryWebView { + pub(crate) fn add_custom_task_key(&mut self, task_id: usize) -> Retained { + let task_uuid = NSUUID::new(); + self + .ivars_mut() + .custom_protocol_task_ids + .insert(task_id, task_uuid.clone()); + task_uuid + } + pub(crate) fn remove_custom_task_key(&mut self, task_id: usize) { + self.ivars_mut().custom_protocol_task_ids.remove(&task_id); + } + pub(crate) fn get_custom_task_uuid(&self, task_id: usize) -> Option> { + self.ivars().custom_protocol_task_ids.get(&task_id).cloned() + } +} diff --git a/src/wkwebview/class/wry_web_view_delegate.rs b/src/wkwebview/class/wry_web_view_delegate.rs new file mode 100644 index 0000000000..d49fe00e57 --- /dev/null +++ b/src/wkwebview/class/wry_web_view_delegate.rs @@ -0,0 +1,108 @@ +// Copyright 2020-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::ffi::CStr; + +use http::Request; +use objc2::{ + declare_class, msg_send_id, + mutability::MainThreadOnly, + rc::Retained, + runtime::{NSObject, ProtocolObject}, + ClassType, DeclaredClass, +}; +use objc2_foundation::{MainThreadMarker, NSObjectProtocol, NSString}; +use objc2_web_kit::{WKScriptMessage, WKScriptMessageHandler, WKUserContentController}; + +pub const IPC_MESSAGE_HANDLER_NAME: &str = "ipc"; + +pub struct WryWebViewDelegateIvars { + pub controller: Retained, + pub ipc_handler: Box)>, +} + +declare_class!( + pub struct WryWebViewDelegate; + + unsafe impl ClassType for WryWebViewDelegate { + type Super = NSObject; + type Mutability = MainThreadOnly; + const NAME: &'static str = "WryWebViewDelegate"; + } + + impl DeclaredClass for WryWebViewDelegate { + type Ivars = WryWebViewDelegateIvars; + } + + unsafe impl NSObjectProtocol for WryWebViewDelegate {} + + unsafe impl WKScriptMessageHandler for WryWebViewDelegate { + // Function for ipc handler + #[method(userContentController:didReceiveScriptMessage:)] + fn did_receive( + this: &WryWebViewDelegate, + _controller: &WKUserContentController, + msg: &WKScriptMessage, + ) { + // Safety: objc runtime calls are unsafe + unsafe { + #[cfg(feature = "tracing")] + let _span = tracing::info_span!(parent: None, "wry::ipc::handle").entered(); + + let ipc_handler = &this.ivars().ipc_handler; + let body = msg.body(); + let is_string = Retained::cast::(body.clone()).isKindOfClass(NSString::class()); + if is_string { + let body = Retained::cast::(body); + let js_utf8 = body.UTF8String(); + + let frame_info = msg.frameInfo(); + let request = frame_info.request(); + let url = request.URL().unwrap(); + let absolute_url = url.absoluteString().unwrap(); + let url_utf8 = absolute_url.UTF8String(); + + if let (Ok(url), Ok(js)) = ( + CStr::from_ptr(url_utf8).to_str(), + CStr::from_ptr(js_utf8).to_str(), + ) { + ipc_handler(Request::builder().uri(url).body(js.to_string()).unwrap()); + return; + } + } + + #[cfg(feature = "tracing")] + tracing::warn!("WebView received invalid IPC call."); + } + } + } +); + +impl WryWebViewDelegate { + pub fn new( + controller: Retained, + ipc_handler: Box)>, + mtm: MainThreadMarker, + ) -> Retained { + let delegate = mtm + .alloc::() + .set_ivars(WryWebViewDelegateIvars { + ipc_handler, + controller, + }); + + let delegate: Retained = unsafe { msg_send_id![super(delegate), init] }; + + let proto_delegate = ProtocolObject::from_ref(delegate.as_ref()); + unsafe { + // this will increate the retain count of the delegate + delegate.ivars().controller.addScriptMessageHandler_name( + proto_delegate, + &NSString::from_str(IPC_MESSAGE_HANDLER_NAME), + ); + } + + delegate + } +} diff --git a/src/wkwebview/class/wry_web_view_parent.rs b/src/wkwebview/class/wry_web_view_parent.rs new file mode 100644 index 0000000000..c212967a9e --- /dev/null +++ b/src/wkwebview/class/wry_web_view_parent.rs @@ -0,0 +1,50 @@ +// Copyright 2020-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use objc2::{ + declare_class, msg_send_id, mutability::MainThreadOnly, rc::Retained, ClassType, DeclaredClass, +}; +use objc2_app_kit::{NSApp, NSEvent, NSView}; +use objc2_foundation::MainThreadMarker; + +pub struct WryWebViewParentIvars {} + +declare_class!( + pub struct WryWebViewParent; + + unsafe impl ClassType for WryWebViewParent { + type Super = NSView; + type Mutability = MainThreadOnly; + const NAME: &'static str = "WryWebViewParent"; + } + + impl DeclaredClass for WryWebViewParent { + type Ivars = WryWebViewParentIvars; + } + + unsafe impl WryWebViewParent { + #[method(keyDown:)] + fn key_down( + &self, + event: &NSEvent, + ) { + let mtm = MainThreadMarker::new().unwrap(); + let app = NSApp(mtm); + unsafe { + if let Some(menu) = app.mainMenu() { + menu.performKeyEquivalent(event); + } + } + } + } +); + +impl WryWebViewParent { + pub fn new(mtm: MainThreadMarker) -> Retained { + let delegate = mtm + .alloc::() + .set_ivars(WryWebViewParentIvars {}); + unsafe { msg_send_id![super(delegate), init] } + } +} diff --git a/src/wkwebview/class/wry_web_view_ui_delegate.rs b/src/wkwebview/class/wry_web_view_ui_delegate.rs new file mode 100644 index 0000000000..d7bbed8fc6 --- /dev/null +++ b/src/wkwebview/class/wry_web_view_ui_delegate.rs @@ -0,0 +1,88 @@ +// Copyright 2020-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::ptr::null_mut; + +use block2::Block; +use objc2::{ + declare_class, msg_send_id, mutability::MainThreadOnly, rc::Retained, runtime::NSObject, + ClassType, DeclaredClass, +}; +use objc2_app_kit::{NSModalResponse, NSModalResponseOK, NSOpenPanel}; +use objc2_foundation::{MainThreadMarker, NSArray, NSObjectProtocol, NSURL}; +use objc2_web_kit::{ + WKFrameInfo, WKMediaCaptureType, WKOpenPanelParameters, WKPermissionDecision, WKSecurityOrigin, + WKUIDelegate, +}; + +use crate::WryWebView; + +pub struct WryWebViewUIDelegateIvars {} + +declare_class!( + pub struct WryWebViewUIDelegate; + + unsafe impl ClassType for WryWebViewUIDelegate { + type Super = NSObject; + type Mutability = MainThreadOnly; + const NAME: &'static str = "WryWebViewUIDelegate"; + } + + impl DeclaredClass for WryWebViewUIDelegate { + type Ivars = WryWebViewUIDelegateIvars; + } + + unsafe impl NSObjectProtocol for WryWebViewUIDelegate {} + + unsafe impl WKUIDelegate for WryWebViewUIDelegate { + #[method(webView:runOpenPanelWithParameters:initiatedByFrame:completionHandler:)] + fn run_file_upload_panel( + &self, + _webview: &WryWebView, + open_panel_params: &WKOpenPanelParameters, + _frame: &WKFrameInfo, + handler: &block2::Block)> + ) { + unsafe { + if let Some(mtm) = MainThreadMarker::new() { + let open_panel = NSOpenPanel::openPanel(mtm); + open_panel.setCanChooseFiles(true); + let allow_multi = open_panel_params.allowsMultipleSelection(); + open_panel.setAllowsMultipleSelection(allow_multi); + let allow_dir = open_panel_params.allowsDirectories(); + open_panel.setCanChooseDirectories(allow_dir); + let ok: NSModalResponse = open_panel.runModal(); + if ok == NSModalResponseOK { + let url = open_panel.URLs(); + (*handler).call((Retained::as_ptr(&url),)); + } else { + (*handler).call((null_mut(),)); + } + } + } + } + + #[method(webView:requestMediaCapturePermissionForOrigin:initiatedByFrame:type:decisionHandler:)] + fn request_media_capture_permission( + &self, + _webview: &WryWebView, + _origin: &WKSecurityOrigin, + _frame: &WKFrameInfo, + _capture_type: WKMediaCaptureType, + decision_handler: &Block + ) { + //https://developer.apple.com/documentation/webkit/wkpermissiondecision?language=objc + (*decision_handler).call((WKPermissionDecision::Grant,)); + } + } +); + +impl WryWebViewUIDelegate { + pub fn new(mtm: MainThreadMarker) -> Retained { + let delegate = mtm + .alloc::() + .set_ivars(WryWebViewUIDelegateIvars {}); + unsafe { msg_send_id![super(delegate), init] } + } +} diff --git a/src/wkwebview/download.rs b/src/wkwebview/download.rs index dcec294556..7031882bec 100644 --- a/src/wkwebview/download.rs +++ b/src/wkwebview/download.rs @@ -4,7 +4,9 @@ use objc2::{rc::Retained, runtime::ProtocolObject, DeclaredClass}; use objc2_foundation::{NSData, NSError, NSString, NSURLResponse, NSURL}; use objc2_web_kit::{WKDownload, WKNavigationAction, WKNavigationResponse, WKWebView}; -use crate::{WryDownloadDelegate, WryNavigationDelegate}; +use super::class::{ + wry_download_delegate::WryDownloadDelegate, wry_navigation_delegate::WryNavigationDelegate, +}; // Download action handler pub(crate) fn navigation_download_action( diff --git a/src/wkwebview/mod.rs b/src/wkwebview/mod.rs index 0493fca06a..06c4b0926d 100644 --- a/src/wkwebview/mod.rs +++ b/src/wkwebview/mod.rs @@ -12,55 +12,46 @@ mod proxy; mod synthetic_mouse_events; mod util; -use block2::Block; +mod class; +use class::url_scheme_handler; +use class::wry_download_delegate::WryDownloadDelegate; +pub use class::wry_web_view::WryWebView; +use class::wry_web_view::WryWebViewIvars; +use class::wry_web_view_delegate::{WryWebViewDelegate, IPC_MESSAGE_HANDLER_NAME}; +use class::wry_web_view_parent::WryWebViewParent; +use class::wry_web_view_ui_delegate::WryWebViewUIDelegate; +use class::{document_title_changed_observer::*, wry_navigation_delegate::WryNavigationDelegate}; -use download::{navigation_download_action, navigation_download_response}; use dpi::{LogicalPosition, LogicalSize}; -use navigation::{ - did_commit_navigation, did_finish_navigation, navigation_policy, navigation_policy_response, -}; use objc2::{ - declare::ClassBuilder, - declare_class, msg_send, msg_send_id, - mutability::{InteriorMutable, MainThreadOnly}, rc::Retained, - runtime::{AnyClass, AnyObject, Bool, MessageReceiver, NSObject, ProtocolObject}, + runtime::{AnyObject, Bool, NSObject, ProtocolObject}, ClassType, DeclaredClass, }; use objc2_app_kit::{ - NSApp, NSApplication, NSAutoresizingMaskOptions, NSEvent, NSModalResponse, NSModalResponseOK, - NSOpenPanel, NSTitlebarSeparatorStyle, NSView, NSWindow, + NSApplication, NSAutoresizingMaskOptions, NSTitlebarSeparatorStyle, NSView, NSWindow, }; use objc2_foundation::{ - ns_string, CGPoint, CGRect, CGSize, MainThreadMarker, NSArray, NSBundle, NSData, NSDate, - NSDictionary, NSError, NSHTTPURLResponse, NSJSONSerialization, NSKeyValueChangeKey, - NSKeyValueObservingOptions, NSMutableDictionary, NSMutableURLRequest, NSNumber, - NSObjectNSKeyValueCoding, NSObjectNSKeyValueObserverRegistration, NSObjectProtocol, NSString, - NSURLResponse, NSUTF8StringEncoding, NSURL, NSUUID, + ns_string, CGPoint, CGRect, CGSize, MainThreadMarker, NSBundle, NSDate, NSError, + NSJSONSerialization, NSMutableURLRequest, NSNumber, NSObjectNSKeyValueCoding, NSObjectProtocol, + NSString, NSUTF8StringEncoding, NSURL, NSUUID, }; #[cfg(target_os = "ios")] use objc2_ui_kit::UIScrollView; use objc2_web_kit::{ - WKAudiovisualMediaTypes, WKDownload, WKDownloadDelegate, WKFrameInfo, WKMediaCaptureType, - WKNavigation, WKNavigationAction, WKNavigationActionPolicy, WKNavigationDelegate, - WKNavigationResponse, WKNavigationResponsePolicy, WKOpenPanelParameters, WKPermissionDecision, - WKScriptMessage, WKScriptMessageHandler, WKSecurityOrigin, WKUIDelegate, WKURLSchemeHandler, - WKURLSchemeTask, WKUserContentController, WKUserScript, WKUserScriptInjectionTime, WKWebView, - WKWebViewConfiguration, WKWebsiteDataStore, + WKAudiovisualMediaTypes, WKURLSchemeHandler, WKUserContentController, WKUserScript, + WKUserScriptInjectionTime, WKWebView, WKWebViewConfiguration, WKWebsiteDataStore, }; use once_cell::sync::Lazy; use raw_window_handle::{HasWindowHandle, RawWindowHandle}; use std::{ - borrow::Cow, collections::{HashMap, HashSet}, - ffi::{c_void, CStr}, + ffi::c_void, os::raw::c_char, panic::{catch_unwind, AssertUnwindSafe}, - path::PathBuf, - ptr::{null_mut, NonNull}, - rc::Rc, - slice, str, + ptr::null_mut, + str, sync::{Arc, Mutex}, }; @@ -72,23 +63,12 @@ use crate::{ }, }; -use crate::{ - wkwebview::download::{download_did_fail, download_did_finish, download_policy}, - DragDropEvent, Error, PageLoadEvent, Rect, RequestAsyncResponder, Result, WebContext, - WebViewAttributes, RGBA, -}; +use crate::{Error, Rect, RequestAsyncResponder, Result, WebContext, WebViewAttributes, RGBA}; -use http::{ - header::{CONTENT_LENGTH, CONTENT_TYPE}, - status::StatusCode, - version::Version, - Request, Response as HttpResponse, -}; +use http::Request; use self::util::Counter; -const IPC_MESSAGE_HANDLER_NAME: &str = "ipc"; - static COUNTER: Counter = Counter::new(); static WEBVIEW_IDS: Lazy>> = Lazy::new(Default::default); @@ -189,242 +169,6 @@ impl InnerWebView { ) -> Result { let mtm = MainThreadMarker::new().ok_or(Error::NotMainThread)?; - // Task handler for custom protocol - extern "C" fn start_task<'a>( - this: &AnyObject, - _sel: objc2::runtime::Sel, - webview: *mut WryWebView, - task: *mut ProtocolObject, - ) { - unsafe { - #[cfg(feature = "tracing")] - let span = tracing::info_span!(parent: None, "wry::custom_protocol::handle", uri = tracing::field::Empty) - .entered(); - - let task_key = (*task).hash(); // hash by task object address - let task_uuid = (*webview).add_custom_task_key(task_key); - - let ivar = this.class().instance_variable("webview_id").unwrap(); - let webview_id: u32 = ivar.load::(this).clone(); - let ivar = this.class().instance_variable("function").unwrap(); - let function: &*mut c_void = ivar.load(this); - if !function.is_null() { - let function = - &mut *(*function as *mut Box>, RequestAsyncResponder)>); - - // Get url request - let request = (*task).request(); - let url = request.URL().unwrap(); - - let uri = url.absoluteString().unwrap().to_string(); - - #[cfg(feature = "tracing")] - span.record("uri", uri.clone()); - - // Get request method (GET, POST, PUT etc...) - let method = request.HTTPMethod().unwrap().to_string(); - - // Prepare our HttpRequest - let mut http_request = Request::builder().uri(uri).method(method.as_str()); - - // Get body - let mut sent_form_body = Vec::new(); - let body = request.HTTPBody(); - let body_stream = request.HTTPBodyStream(); - if let Some(body) = body { - let length = body.length(); - let data_bytes = body.bytes(); - sent_form_body = slice::from_raw_parts(data_bytes.as_ptr(), length).to_vec(); - } else if let Some(body_stream) = body_stream { - body_stream.open(); - - while body_stream.hasBytesAvailable() { - sent_form_body.reserve(128); - let p = sent_form_body.as_mut_ptr().add(sent_form_body.len()); - let read_length = sent_form_body.capacity() - sent_form_body.len(); - let count = body_stream.read_maxLength(NonNull::new(p).unwrap(), read_length); - sent_form_body.set_len(sent_form_body.len() + count as usize); - } - - body_stream.close(); - } - - // Extract all headers fields - let all_headers = request.allHTTPHeaderFields(); - - // get all our headers values and inject them in our request - if let Some(all_headers) = all_headers { - for current_header in all_headers.allKeys().to_vec() { - let header_value = all_headers.valueForKey(current_header).unwrap(); - - // inject the header into the request - http_request = - http_request.header(current_header.to_string(), header_value.to_string()); - } - } - - let respond_with_404 = || { - let urlresponse = NSHTTPURLResponse::alloc(); - let response = NSHTTPURLResponse::initWithURL_statusCode_HTTPVersion_headerFields( - urlresponse, - &url, - StatusCode::NOT_FOUND.as_u16().try_into().unwrap(), - Some(&NSString::from_str( - format!("{:#?}", Version::HTTP_11).as_str(), - )), - None, - ) - .unwrap(); - (*task).didReceiveResponse(&response); - // Finish - (*task).didFinish(); - }; - - // send response - match http_request.body(sent_form_body) { - Ok(final_request) => { - let responder: Box>)> = - Box::new(move |sent_response| { - fn check_webview_id_valid(webview_id: u32) -> crate::Result<()> { - if !WEBVIEW_IDS.lock().unwrap().contains(&webview_id) { - return Err(crate::Error::CustomProtocolTaskInvalid); - } - Ok(()) - } - /// Task may not live longer than async custom protocol handler. - /// - /// There are roughly 2 ways to cause segfault: - /// 1. Task has stopped. pointer of the task not valid anymore. - /// 2. Task had stopped, but the pointer of the task has allocated to a new task. - /// Outdated custom handler may call to the new task instance and cause segfault. - fn check_task_is_valid( - webview: &WryWebView, - task_key: usize, - current_uuid: Retained, - ) -> crate::Result<()> { - let latest_task_uuid = webview.get_custom_task_uuid(task_key); - if let Some(latest_uuid) = latest_task_uuid { - if latest_uuid != current_uuid { - return Err(crate::Error::CustomProtocolTaskInvalid); - } - } else { - return Err(crate::Error::CustomProtocolTaskInvalid); - } - Ok(()) - } - - // FIXME: This is 10000% unsafe. `task` and `webview` are not guaranteed to be valid. - // We should consider use sync command only. - unsafe fn response( - task: *mut ProtocolObject, - webview: *mut WryWebView, - task_key: usize, - task_uuid: Retained, - webview_id: u32, - url: Retained, - sent_response: HttpResponse>, - ) -> crate::Result<()> { - check_task_is_valid(&*webview, task_key, task_uuid.clone())?; - - let content = sent_response.body(); - // default: application/octet-stream, but should be provided by the client - let wanted_mime = sent_response.headers().get(CONTENT_TYPE); - // default to 200 - let wanted_status_code = sent_response.status().as_u16() as i32; - // default to HTTP/1.1 - let wanted_version = format!("{:#?}", sent_response.version()); - - let mut headers = NSMutableDictionary::new(); - - if let Some(mime) = wanted_mime { - headers.insert_id( - NSString::from_str(mime.to_str().unwrap()).as_ref(), - NSString::from_str(CONTENT_TYPE.as_str()), - ); - } - headers.insert_id( - NSString::from_str(&content.len().to_string()).as_ref(), - NSString::from_str(CONTENT_LENGTH.as_str()), - ); - - // add headers - for (name, value) in sent_response.headers().iter() { - if let Ok(value) = value.to_str() { - headers.insert_id( - NSString::from_str(name.as_str()).as_ref(), - NSString::from_str(value), - ); - } - } - - let urlresponse = NSHTTPURLResponse::alloc(); - let response = - NSHTTPURLResponse::initWithURL_statusCode_HTTPVersion_headerFields( - urlresponse, - &url, - wanted_status_code.try_into().unwrap(), - Some(&NSString::from_str(&wanted_version)), - Some(&headers), - ) - .unwrap(); - - check_webview_id_valid(webview_id)?; - check_task_is_valid(&*webview, task_key, task_uuid.clone())?; - (*task).didReceiveResponse(&response); - - // Send data - let bytes = content.as_ptr() as *mut c_void; - let data = NSData::alloc(); - // MIGRATE NOTE: we copied the content to the NSData because content will be freed - // when out of scope but NSData will also free the content when it's done and cause doube free. - let data = NSData::initWithBytes_length(data, bytes, content.len()); - check_webview_id_valid(webview_id)?; - check_task_is_valid(&*webview, task_key, task_uuid.clone())?; - (*task).didReceiveData(&data); - - // Finish - check_webview_id_valid(webview_id)?; - check_task_is_valid(&*webview, task_key, task_uuid.clone())?; - (*task).didFinish(); - - (*webview).remove_custom_task_key(task_key); - Ok(()) - } - - let _ = response( - task, - webview, - task_key, - task_uuid, - webview_id, - url.clone(), - sent_response, - ); - }); - - #[cfg(feature = "tracing")] - let _span = tracing::info_span!("wry::custom_protocol::call_handler").entered(); - function(final_request, RequestAsyncResponder { responder }); - } - Err(_) => respond_with_404(), - }; - } else { - #[cfg(feature = "tracing")] - tracing::warn!( - "Either WebView or WebContext instance is dropped! This handler shouldn't be called." - ); - } - } - } - extern "C" fn stop_task( - _this: &ProtocolObject, - _sel: objc2::runtime::Sel, - webview: &mut WryWebView, - task: &ProtocolObject, - ) { - webview.remove_custom_task_key(task.hash()); - } - let mut wv_ids = WEBVIEW_IDS.lock().unwrap(); let webview_id = COUNTER.next(); wv_ids.insert(webview_id); @@ -461,25 +205,8 @@ impl InnerWebView { }; for (name, function) in attributes.custom_protocols { - let scheme_name = format!("{}URLSchemeHandler", name); - let cls = ClassBuilder::new(&scheme_name, NSObject::class()); - let cls = match cls { - Some(mut cls) => { - cls.add_ivar::<*mut c_void>("function"); - cls.add_ivar::("webview_id"); - cls.add_method( - objc2::sel!(webView:startURLSchemeTask:), - start_task as extern "C" fn(_, _, _, _), - ); - cls.add_method( - objc2::sel!(webView:stopURLSchemeTask:), - stop_task as extern "C" fn(_, _, _, _), - ); - cls.register() - } - None => AnyClass::get(&scheme_name).expect("Failed to get the class definition"), - }; - let handler: *mut AnyObject = objc2::msg_send![cls, new]; + let url_scheme_handler_cls = url_scheme_handler::create(&name); + let handler: *mut AnyObject = objc2::msg_send![url_scheme_handler_cls, new]; let function = Box::into_raw(Box::new(function)); protocol_ptrs.push(function); @@ -1134,630 +861,3 @@ unsafe fn window_position(view: &NSView, x: i32, y: i32, height: f64) -> CGPoint let frame: CGRect = view.frame(); CGPoint::new(x as f64, frame.size.height - y as f64 - height) } - -pub struct WryWebViewIvars { - is_child: bool, - #[cfg(target_os = "macos")] - drag_drop_handler: Box bool>, - #[cfg(target_os = "macos")] - accept_first_mouse: objc2::runtime::Bool, - custom_protocol_task_ids: HashMap>, -} - -declare_class!( - pub struct WryWebView; - - unsafe impl ClassType for WryWebView { - type Super = WKWebView; - type Mutability = MainThreadOnly; - const NAME: &'static str = "WryWebView"; - } - - impl DeclaredClass for WryWebView { - type Ivars = WryWebViewIvars; - } - - unsafe impl WryWebView { - #[method(performKeyEquivalent:)] - fn perform_key_equivalent( - &self, - _event: &NSEvent, - ) -> Bool { - // This is a temporary workaround for https://github.com/tauri-apps/tauri/issues/9426 - // FIXME: When the webview is a child webview, performKeyEquivalent always return YES - // and stop propagating the event to the window, hence the menu shortcut won't be - // triggered. However, overriding this method also means the cmd+key event won't be - // handled in webview, which means the key cannot be listened by JavaScript. - if self.ivars().is_child { - Bool::NO - } else { - unsafe { - let result: Bool = self.send_super_message( - WKWebView::class(), - objc2::sel!(performKeyEquivalent:), - (_event,), - ); - result - } - } - } - - #[method(acceptsFirstMouse:)] - fn accept_first_mouse( - &self, - _event: &NSEvent, - ) -> Bool { - self.ivars().accept_first_mouse - } - } - - // Drag & Drop - #[cfg(target_os = "macos")] - unsafe impl WryWebView { - #[method(draggingEntered:)] - fn dragging_entered( - &self, - drag_info: &ProtocolObject, - ) -> objc2_app_kit::NSDragOperation { - drag_drop::dragging_entered(self, drag_info) - } - - #[method(draggingUpdated:)] - fn dragging_updated( - &self, - drag_info: &ProtocolObject, - ) -> objc2_app_kit::NSDragOperation { - drag_drop::dragging_updated(self, drag_info) - } - - #[method(performDragOperation:)] - fn perform_drag_operation( - &self, - drag_info: &ProtocolObject, - ) -> Bool { - drag_drop::perform_drag_operation(self, drag_info) - } - - #[method(draggingExited:)] - fn dragging_exited( - &self, - drag_info: &ProtocolObject, - ) { - drag_drop::dragging_exited(self, drag_info) - } - } - - // Synthetic mouse events - #[cfg(target_os = "macos")] - unsafe impl WryWebView { - #[method(otherMouseDown:)] - fn other_mouse_down( - &self, - event: &NSEvent, - ) { - synthetic_mouse_events::other_mouse_down(self, event) - } - - #[method(otherMouseUp:)] - fn other_mouse_up( - &self, - event: &NSEvent, - ) { - synthetic_mouse_events::other_mouse_up(self, event) - } - } -); - -// Custom Protocol Task Checker -impl WryWebView { - fn add_custom_task_key(&mut self, task_id: usize) -> Retained { - let task_uuid = NSUUID::new(); - self - .ivars_mut() - .custom_protocol_task_ids - .insert(task_id, task_uuid.clone()); - task_uuid - } - fn remove_custom_task_key(&mut self, task_id: usize) { - self.ivars_mut().custom_protocol_task_ids.remove(&task_id); - } - fn get_custom_task_uuid(&self, task_id: usize) -> Option> { - self.ivars().custom_protocol_task_ids.get(&task_id).cloned() - } -} - -struct WryWebViewDelegateIvars { - controller: Retained, - ipc_handler: Box)>, -} - -declare_class!( - struct WryWebViewDelegate; - - unsafe impl ClassType for WryWebViewDelegate { - type Super = NSObject; - type Mutability = MainThreadOnly; - const NAME: &'static str = "WryWebViewDelegate"; - } - - impl DeclaredClass for WryWebViewDelegate { - type Ivars = WryWebViewDelegateIvars; - } - - unsafe impl NSObjectProtocol for WryWebViewDelegate {} - - unsafe impl WKScriptMessageHandler for WryWebViewDelegate { - // Function for ipc handler - #[method(userContentController:didReceiveScriptMessage:)] - fn did_receive( - this: &WryWebViewDelegate, - _controller: &WKUserContentController, - msg: &WKScriptMessage, - ) { - // Safety: objc runtime calls are unsafe - unsafe { - #[cfg(feature = "tracing")] - let _span = tracing::info_span!(parent: None, "wry::ipc::handle").entered(); - - let ipc_handler = &this.ivars().ipc_handler; - let body = msg.body(); - let is_string = Retained::cast::(body.clone()).isKindOfClass(NSString::class()); - if is_string { - let body = Retained::cast::(body); - let js_utf8 = body.UTF8String(); - - let frame_info = msg.frameInfo(); - let request = frame_info.request(); - let url = request.URL().unwrap(); - let absolute_url = url.absoluteString().unwrap(); - let url_utf8 = absolute_url.UTF8String(); - - if let (Ok(url), Ok(js)) = ( - CStr::from_ptr(url_utf8).to_str(), - CStr::from_ptr(js_utf8).to_str(), - ) { - ipc_handler(Request::builder().uri(url).body(js.to_string()).unwrap()); - return; - } - } - - #[cfg(feature = "tracing")] - tracing::warn!("WebView received invalid IPC call."); - } - } - } -); - -impl WryWebViewDelegate { - fn new( - controller: Retained, - ipc_handler: Box)>, - mtm: MainThreadMarker, - ) -> Retained { - let delegate = mtm - .alloc::() - .set_ivars(WryWebViewDelegateIvars { - ipc_handler, - controller, - }); - - let delegate: Retained = unsafe { msg_send_id![super(delegate), init] }; - - let proto_delegate = ProtocolObject::from_ref(delegate.as_ref()); - unsafe { - // this will increate the retain count of the delegate - delegate.ivars().controller.addScriptMessageHandler_name( - proto_delegate, - &NSString::from_str(IPC_MESSAGE_HANDLER_NAME), - ); - } - - delegate - } -} - -struct DocumentTitleChangedObserverIvars { - object: Retained, - handler: Box, -} - -declare_class!( - struct DocumentTitleChangedObserver; - - unsafe impl ClassType for DocumentTitleChangedObserver { - type Super = NSObject; - type Mutability = InteriorMutable; - const NAME: &'static str = "DocumentTitleChangedObserver"; - } - - impl DeclaredClass for DocumentTitleChangedObserver { - type Ivars = DocumentTitleChangedObserverIvars; - } - - unsafe impl DocumentTitleChangedObserver { - #[method(observeValueForKeyPath:ofObject:change:context:)] - fn observe_value_for_key_path( - &self, - key_path: Option<&NSString>, - of_object: Option<&AnyObject>, - _change: Option<&NSDictionary>, - _context: *mut c_void, - ) { - if let (Some(key_path), Some(object)) = (key_path, of_object) { - if key_path.to_string() == "title" { - unsafe { - let handler = &self.ivars().handler; - // if !handler.is_null() { - let title: *const NSString = msg_send![object, title]; - handler((*title).to_string()); - // } - } - } - } - } - } - - unsafe impl NSObjectProtocol for DocumentTitleChangedObserver {} -); - -impl DocumentTitleChangedObserver { - fn new(webview: Retained, handler: Box) -> Retained { - let observer = Self::alloc().set_ivars(DocumentTitleChangedObserverIvars { - object: webview, - handler, - }); - - let observer: Retained = unsafe { msg_send_id![super(observer), init] }; - - unsafe { - observer - .ivars() - .object - .addObserver_forKeyPath_options_context( - &observer, - &NSString::from_str("title"), - NSKeyValueObservingOptions::NSKeyValueObservingOptionNew, - null_mut(), - ); - } - - observer - } -} - -impl Drop for DocumentTitleChangedObserver { - fn drop(&mut self) { - unsafe { - self - .ivars() - .object - .removeObserver_forKeyPath(&self, &NSString::from_str("title")); - } - } -} - -pub struct WryNavigationDelegateIvars { - pending_scripts: Arc>>>, - has_download_handler: bool, - navigation_policy_function: Box bool>, - download_delegate: Option>, - on_page_load_handler: Option>, -} - -declare_class!( - pub struct WryNavigationDelegate; - - unsafe impl ClassType for WryNavigationDelegate { - type Super = NSObject; - type Mutability = MainThreadOnly; - const NAME: &'static str = "WryNavigationDelegate"; - } - - impl DeclaredClass for WryNavigationDelegate { - type Ivars = WryNavigationDelegateIvars; - } - - unsafe impl NSObjectProtocol for WryNavigationDelegate {} - - unsafe impl WKNavigationDelegate for WryNavigationDelegate { - #[method(webView:decidePolicyForNavigationAction:decisionHandler:)] - fn navigation_policy( - &self, - webview: &WKWebView, - action: &WKNavigationAction, - handler: &block2::Block, - ) { - navigation_policy(self, webview, action, handler); - } - - #[method(webView:decidePolicyForNavigationResponse:decisionHandler:)] - fn navigation_policy_response( - &self, - webview: &WKWebView, - response: &WKNavigationResponse, - handler: &block2::Block, - ) { - navigation_policy_response(self, webview, response, handler); - } - - #[method(webView:didFinishNavigation:)] - fn did_finish_navigation( - &self, - webview: &WKWebView, - navigation: &WKNavigation, - ) { - did_finish_navigation(self, webview, navigation); - } - - #[method(webView:didCommitNavigation:)] - fn did_commit_navigation( - &self, - webview: &WKWebView, - navigation: &WKNavigation, - ) { - did_commit_navigation(self, webview, navigation); - } - - #[method(webView:navigationAction:didBecomeDownload:)] - fn navigation_download_action( - &self, - webview: &WKWebView, - action: &WKNavigationAction, - download: &WKDownload, - ) { - navigation_download_action(self, webview, action, download); - } - - #[method(webView:navigationResponse:didBecomeDownload:)] - fn navigation_download_response( - &self, - webview: &WKWebView, - response: &WKNavigationResponse, - download: &WKDownload, - ) { - navigation_download_response(self, webview, response, download); - } - } -); - -impl WryNavigationDelegate { - fn new( - webview: Retained, - pending_scripts: Arc>>>, - has_download_handler: bool, - navigation_handler: Option bool>>, - new_window_req_handler: Option bool>>, - download_delegate: Option>, - on_page_load_handler: Option>, - mtm: MainThreadMarker, - ) -> Retained { - let navigation_policy_function = Box::new(move |url: String, is_main_frame: bool| -> bool { - if is_main_frame { - navigation_handler - .as_ref() - .map_or(true, |navigation_handler| (navigation_handler)(url)) - } else { - new_window_req_handler - .as_ref() - .map_or(true, |new_window_req_handler| (new_window_req_handler)(url)) - } - }); - - let on_page_load_handler = if let Some(handler) = on_page_load_handler { - let custom_handler = Box::new(move |event| { - handler(event, url_from_webview(&webview).unwrap_or_default()); - }) as Box; - Some(custom_handler) - } else { - None - }; - - let delegate = mtm - .alloc::() - .set_ivars(WryNavigationDelegateIvars { - pending_scripts, - navigation_policy_function, - has_download_handler, - download_delegate, - on_page_load_handler, - }); - - unsafe { msg_send_id![super(delegate), init] } - } -} - -pub struct WryDownloadDelegateIvars { - started: *mut Box bool>, - completed: *mut Rc, bool)>, -} - -declare_class!( - pub struct WryDownloadDelegate; - - unsafe impl ClassType for WryDownloadDelegate { - type Super = NSObject; - type Mutability = MainThreadOnly; - const NAME: &'static str = "WryDownloadDelegate"; - } - - impl DeclaredClass for WryDownloadDelegate { - type Ivars = WryDownloadDelegateIvars; - } - - unsafe impl NSObjectProtocol for WryDownloadDelegate {} - - unsafe impl WKDownloadDelegate for WryDownloadDelegate { - #[method(download:decideDestinationUsingResponse:suggestedFilename:completionHandler:)] - fn download_policy( - &self, - download: &WKDownload, - response: &NSURLResponse, - suggested_path: &NSString, - handler: &block2::Block, - ) { - download_policy(self, download, response, suggested_path, handler); - } - - #[method(downloadDidFinish:)] - fn download_did_finish(&self, download: &WKDownload) { - download_did_finish(self, download); - } - - #[method(download:didFailWithError:resumeData:)] - fn download_did_fail( - &self, - download: &WKDownload, - error: &NSError, - resume_data: &NSData, - ) { - download_did_fail(self, download, error, resume_data); - } - } -); - -impl WryDownloadDelegate { - fn new( - download_started_handler: Option bool>>, - download_completed_handler: Option, bool)>>, - mtm: MainThreadMarker, - ) -> Retained { - let started = match download_started_handler { - Some(handler) => Box::into_raw(Box::new(handler)), - None => null_mut(), - }; - let completed = match download_completed_handler { - Some(handler) => Box::into_raw(Box::new(handler)), - None => null_mut(), - }; - let delegate = mtm - .alloc::() - .set_ivars(WryDownloadDelegateIvars { started, completed }); - - unsafe { msg_send_id![super(delegate), init] } - } -} - -impl Drop for WryDownloadDelegate { - fn drop(&mut self) { - if self.ivars().started != null_mut() { - unsafe { - drop(Box::from_raw(self.ivars().started)); - } - } - if self.ivars().completed != null_mut() { - unsafe { - drop(Box::from_raw(self.ivars().completed)); - } - } - } -} - -struct WryWebViewUIDelegateIvars {} - -declare_class!( - struct WryWebViewUIDelegate; - - unsafe impl ClassType for WryWebViewUIDelegate { - type Super = NSObject; - type Mutability = MainThreadOnly; - const NAME: &'static str = "WryWebViewUIDelegate"; - } - - impl DeclaredClass for WryWebViewUIDelegate { - type Ivars = WryWebViewUIDelegateIvars; - } - - unsafe impl NSObjectProtocol for WryWebViewUIDelegate {} - - unsafe impl WKUIDelegate for WryWebViewUIDelegate { - #[method(webView:runOpenPanelWithParameters:initiatedByFrame:completionHandler:)] - fn run_file_upload_panel( - &self, - _webview: &WryWebView, - open_panel_params: &WKOpenPanelParameters, - _frame: &WKFrameInfo, - handler: &block2::Block)> - ) { - unsafe { - if let Some(mtm) = MainThreadMarker::new() { - let open_panel = NSOpenPanel::openPanel(mtm); - open_panel.setCanChooseFiles(true); - let allow_multi = open_panel_params.allowsMultipleSelection(); - open_panel.setAllowsMultipleSelection(allow_multi); - let allow_dir = open_panel_params.allowsDirectories(); - open_panel.setCanChooseDirectories(allow_dir); - let ok: NSModalResponse = open_panel.runModal(); - if ok == NSModalResponseOK { - let url = open_panel.URLs(); - (*handler).call((Retained::as_ptr(&url),)); - } else { - (*handler).call((null_mut(),)); - } - } - } - } - - #[method(webView:requestMediaCapturePermissionForOrigin:initiatedByFrame:type:decisionHandler:)] - fn request_media_capture_permission( - &self, - _webview: &WryWebView, - _origin: &WKSecurityOrigin, - _frame: &WKFrameInfo, - _capture_type: WKMediaCaptureType, - decision_handler: &Block - ) { - //https://developer.apple.com/documentation/webkit/wkpermissiondecision?language=objc - (*decision_handler).call((WKPermissionDecision::Grant,)); - } - } -); - -impl WryWebViewUIDelegate { - fn new(mtm: MainThreadMarker) -> Retained { - let delegate = mtm - .alloc::() - .set_ivars(WryWebViewUIDelegateIvars {}); - unsafe { msg_send_id![super(delegate), init] } - } -} - -struct WryWebViewParentIvars {} - -declare_class!( - struct WryWebViewParent; - - unsafe impl ClassType for WryWebViewParent { - type Super = NSView; - type Mutability = MainThreadOnly; - const NAME: &'static str = "WryWebViewParent"; - } - - impl DeclaredClass for WryWebViewParent { - type Ivars = WryWebViewParentIvars; - } - - unsafe impl WryWebViewParent { - #[method(keyDown:)] - fn key_down( - &self, - event: &NSEvent, - ) { - let mtm = MainThreadMarker::new().unwrap(); - let app = NSApp(mtm); - unsafe { - if let Some(menu) = app.mainMenu() { - menu.performKeyEquivalent(event); - } - } - } - } -); - -impl WryWebViewParent { - fn new(mtm: MainThreadMarker) -> Retained { - let delegate = mtm - .alloc::() - .set_ivars(WryWebViewParentIvars {}); - unsafe { msg_send_id![super(delegate), init] } - } -} diff --git a/src/wkwebview/navigation.rs b/src/wkwebview/navigation.rs index 8b46421333..ef9fad38ff 100644 --- a/src/wkwebview/navigation.rs +++ b/src/wkwebview/navigation.rs @@ -6,7 +6,9 @@ use objc2_web_kit::{ WKNavigationResponsePolicy, WKWebView, }; -use crate::{PageLoadEvent, WryNavigationDelegate}; +use crate::PageLoadEvent; + +use super::class::wry_navigation_delegate::WryNavigationDelegate; pub(crate) fn did_commit_navigation( this: &WryNavigationDelegate,