From 3691c4f6c88fe43e92597caf3003c8d57b447a7b Mon Sep 17 00:00:00 2001 From: Iain Laird Date: Wed, 19 Oct 2022 10:28:53 +0100 Subject: [PATCH] feat: Add download started and download completed callbacks (#530) * Add download handler to webview attributes * Potentially implement download event for windows * Add download event example * Implement download event for webkitgtk * Attempt to write example writing to tempdir * Add download compete handler, fix example * Update doc * Fix webkitgtk implementation for download handlers * Attempt to implement download events on macOS * Use more reliable URLs * Improve gtk implementation * Add more details to example * Attempt to write tempfile to documents * Fix download delegate funcs to implement on navdelegate on macOS Also adds the complete and failed callbacks. * dummy commit * Update webkit2gtk * Match changes on dev * Split download handlers Now has handler for download start/deny and download completed callback * Propagate split of download handlers to win impl * Switch to mutable ref PathBuf instead of string * Wrap download_completed callback in Rc This avoids the indirection of the closure builder pattern whilst still solving the lifetime issues. * Windows formatting * Fix merge in linux implementation Downloads still don't actually complete though * Fix macOS implementation + refactor * Rework example Now holds temp_dir as long as necessary to prevent premature drop (and thus deleting our example file). Requires the use of the Rc pattern for interior mutability across a shared reference - this (should) be safe, as the entire example is single threaded, and any mutable borrows should be dropped by the time a borrow ocurrs. * Ignore unused parameter * Improve download example handling of empty path * Formatting * Attempt to improve linux behaviour * Attempt to fix Windows compile errors * Separate download complete handler from download started handler * Formatting * Take closure by mutable reference (windows) * Workaround mutable borrow issues on windows * Separate download started handler from finished on linux * Potentially improve setting output path on linux * Add original download's url as parameter to download completed handler * Formatting * Standardise terminology (replace `callback` with `handler`) * Use dunce to attempt to remove UNC prefixes on windows * Fix incorrect function signature on macOS * Improve docs * Enable devtools in example * Include blob download example macOS implementation works, but may rely on an incorrect assumption - clicking the download link seems to return a random blob URL, so we just grab the first saved blob URL * Formatting * Address comment regarding passing pathbuf to completion handler * Separate download completed from started handler on macOS * Formatting * Move download ffi function to download module * Add change file * Correct the name of download start method Co-authored-by: Iain Laird Co-authored-by: Wu Wayne --- .changes/download.md | 5 + Cargo.toml | 4 + examples/blob_download.rs | 198 +++++++++++++++++++++++++++ examples/download_event.rs | 132 ++++++++++++++++++ src/webview/mod.rs | 60 ++++++++ src/webview/webkitgtk/mod.rs | 9 ++ src/webview/webkitgtk/web_context.rs | 69 ++++++++++ src/webview/webview2/mod.rs | 75 +++++++++- src/webview/wkwebview/download.rs | 120 ++++++++++++++++ src/webview/wkwebview/mod.rs | 189 ++++++++++++++++++++----- 10 files changed, 822 insertions(+), 39 deletions(-) create mode 100644 .changes/download.md create mode 100644 examples/blob_download.rs create mode 100644 examples/download_event.rs create mode 100644 src/webview/wkwebview/download.rs diff --git a/.changes/download.md b/.changes/download.md new file mode 100644 index 000000000..feba38bce --- /dev/null +++ b/.changes/download.md @@ -0,0 +1,5 @@ +--- +"wry": patch +--- + +On Desktop, add `download_started_handler` and `download_completed_handler`. See `blob_download` and `download_event` example for their usages. diff --git a/Cargo.toml b/Cargo.toml index e2cd959b4..034d00415 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,9 @@ http = "0.2.8" anyhow = "1.0" tempfile = "3.3.0" http-range = "0.1.5" +normpath = "0.3.2" +dirs = "4.0.0" +base64 = "0.13.0" [target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies] webkit2gtk = { version = "0.18.2", features = [ "v2_36" ] } @@ -61,6 +64,7 @@ soup2 = "0.2" [target."cfg(target_os = \"windows\")".dependencies] webview2-com = "0.19.1" windows-implement = "0.39.0" +dunce = "1.0.2" [target."cfg(target_os = \"windows\")".dependencies.windows] version = "0.39.0" diff --git a/examples/blob_download.rs b/examples/blob_download.rs new file mode 100644 index 000000000..2646d61d3 --- /dev/null +++ b/examples/blob_download.rs @@ -0,0 +1,198 @@ +// Copyright 2019-2021 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + fs::File, + io::{Read, Write}, + path::PathBuf, +}; + +use base64::decode; +use tempfile::tempdir; + +fn main() -> wry::Result<()> { + use wry::{ + application::{ + event::{Event, StartCause, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, + }, + webview::WebViewBuilder, + }; + + let html = r#" + +
+ Download + +
+ + "#; + + enum UserEvent { + BlobReceived(String), + BlobChunk(Option), + } + + let init_script = r" + // Adds an URL.getFromObjectURL( ) method + // returns the original object ( or ) the URI points to or null + (() => { + // overrides URL methods to be able to retrieve the original blobs later on + const old_create = URL.createObjectURL; + const old_revoke = URL.revokeObjectURL; + Object.defineProperty(URL, 'createObjectURL', { + get: () => storeAndCreate + }); + Object.defineProperty(URL, 'revokeObjectURL', { + get: () => forgetAndRevoke + }); + Object.defineProperty(URL, 'getFromObjectURL', { + get: () => getBlob + }); + Object.defineProperty(URL, 'getObjectURLDict', { + get: () => getDict + }); + Object.defineProperty(URL, 'clearURLDict', { + get: () => clearDict + }); + const dict = {}; + + function storeAndCreate(blob) { + const url = old_create(blob); // let it throw if it has to + dict[url] = blob; + console.log(url) + console.log(blob) + return url + } + + function forgetAndRevoke(url) { + console.log(`revoke ${url}`) + old_revoke(url); + } + + function getBlob(url) { + return dict[url] || null; + } + + function getDict() { + return dict; + } + + function clearDict() { + dict = {}; + } + })(); + "; + + let event_loop: EventLoop = EventLoop::with_user_event(); + let proxy = event_loop.create_proxy(); + let window = WindowBuilder::new() + .with_title("Hello World") + .build(&event_loop)?; + let webview = WebViewBuilder::new(window)? + .with_html(html)? + .with_initialization_script(init_script) + .with_download_started_handler({ + let proxy = proxy.clone(); + move |uri: String, _: &mut PathBuf| { + if uri.starts_with("blob:") { + let _ = proxy.send_event(UserEvent::BlobReceived(dbg!(uri))); + } + + false + } + }) + .with_ipc_handler({ + let proxy = proxy.clone(); + move |_, string| match string.as_str() { + _ if string.starts_with("data:") => { + let _ = proxy.send_event(UserEvent::BlobChunk(Some(string))); + } + "#EOF" => { + let _ = proxy.send_event(UserEvent::BlobChunk(None)); + } + _ => {} + } + }) + .with_devtools(true) + .build()?; + + #[cfg(debug_assertions)] + webview.open_devtools(); + + let mut blob_file = None; + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::NewEvents(StartCause::Init) => println!("Wry has started!"), + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => *control_flow = ControlFlow::Exit, + Event::UserEvent(UserEvent::BlobReceived(uri)) => { + let temp_dir = tempdir().expect("Create temp dir"); + blob_file = Some((File::create(&temp_dir.path().join("blob.txt")).expect("Create file"), temp_dir)); + webview.evaluate_script(&format!(r#" + (() => {{ + /** + * @type Blob + */ + let blob = URL.getObjectURLDict()['{}'] + || Object.values(URL.getObjectURLDict())[0] // For some reason macOS returns a completely random blob URL? Just grab the first one + + var increment = 1024; + var index = 0; + var reader = new FileReader(); + let func = function() {{ + let res = reader.result; + window.ipc.postMessage(`${{res}}`); + index += increment; + if (index < blob.size) {{ + let slice = blob.slice(index, index + increment); + reader = new FileReader(); + reader.onloadend = func; + reader.readAsDataURL(slice); + }} else {{ + window.ipc.postMessage('#EOF'); + }} + }}; + reader.onloadend = func; + reader.readAsDataURL(blob.slice(index, increment)) + }})(); + "#, uri)).expect("Eval script"); + }, + Event::UserEvent(UserEvent::BlobChunk(chunk)) => { + if let Some((file, path)) = blob_file.as_mut() { + match chunk { + Some(chunk) => { + let split = chunk.split(',').nth(1); + println!("{:?}", chunk.split(',').next()); + if let Some(split) = split { + if let Ok(decoded) = decode(split) { + if file.write(&decoded).is_err() { + eprintln!("Failed to write bytes to temp file") + } + } + } + }, + None => { + let mut file = File::open(&path.path().join("blob.txt")).expect("Open temp file"); + let mut content = String::new(); + file.read_to_string(&mut content).expect("Read contents of file"); + println!("Contents of file:"); + println!("{}", content); + blob_file = None; + } + } + } + }, + _ => (), + } + }); +} diff --git a/examples/download_event.rs b/examples/download_event.rs new file mode 100644 index 000000000..5388625ae --- /dev/null +++ b/examples/download_event.rs @@ -0,0 +1,132 @@ +// Copyright 2019-2021 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{cell::RefCell, path::PathBuf, rc::Rc}; + +use normpath::PathExt; +use tempfile::tempdir; + +fn main() -> wry::Result<()> { + use wry::{ + application::{ + event::{Event, StartCause, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, + }, + webview::WebViewBuilder, + }; + + let html = r#" + +
+

WRYYYYYYYYYYYYYYYYYYYYYY!

+ Allowed Download + Denied Download +
+ + "#; + + enum UserEvent { + DownloadStarted(String, String), + DownloadComplete(Option, bool), + Rejected(String), + } + + let temp_dir = Rc::new(RefCell::new(None)); + let event_loop: EventLoop = EventLoop::with_user_event(); + let proxy = event_loop.create_proxy(); + let window = WindowBuilder::new() + .with_title("Hello World") + .build(&event_loop)?; + let webview = WebViewBuilder::new(window)? + .with_html(html)? + .with_download_started_handler({ + let proxy = proxy.clone(); + let tempdir_cell = temp_dir.clone(); + move |uri: String, default_path: &mut PathBuf| { + if uri.contains("wry-v0.13.3") { + if let Ok(tempdir) = tempdir() { + if let Ok(path) = tempdir.path().normalize() { + tempdir_cell.borrow_mut().replace(tempdir); + + let path = path.join("example.zip").as_path().to_path_buf(); + + *default_path = path.clone(); + + let submitted = proxy + .send_event(UserEvent::DownloadStarted( + uri.clone(), + path.display().to_string(), + )) + .is_ok(); + + return submitted; + } + } + } + + let _ = proxy.send_event(UserEvent::Rejected(uri.clone())); + + false + } + }) + .with_download_completed_handler({ + let proxy = proxy.clone(); + move |_uri, path, success| { + let _ = proxy.send_event(UserEvent::DownloadComplete(path, success)); + } + }) + .with_devtools(true) + .build()?; + + #[cfg(debug_assertions)] + webview.open_devtools(); + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::NewEvents(StartCause::Init) => println!("Wry has started!"), + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => *control_flow = ControlFlow::Exit, + Event::UserEvent(UserEvent::DownloadStarted(uri, temp_dir)) => { + println!("Download: {}", uri); + println!("Will write to: {:?}", temp_dir); + } + Event::UserEvent(UserEvent::DownloadComplete(mut path, success)) => { + let _temp_dir_guard = if path.is_none() && success { + let temp_dir = temp_dir.borrow_mut().take(); + path = Some( + temp_dir + .as_ref() + .expect("Stored temp dir") + .path() + .join("example.zip"), + ); + temp_dir + } else { + None + }; + println!("Succeeded: {}", success); + if let Some(path) = path { + let metadata = path.metadata(); + println!("Path: {}", path.to_string_lossy()); + if let Ok(metadata) = metadata { + println!("Size of {}Mb", (metadata.len() / 1024) / 1024) + } else { + println!("Failed to retrieve file metadata - does it exist?") + } + } else { + println!("No output path") + } + } + Event::UserEvent(UserEvent::Rejected(uri)) => { + println!("Rejected download from: {}", uri) + } + _ => (), + } + }); +} diff --git a/src/webview/mod.rs b/src/webview/mod.rs index b033cecd3..2ce67ba64 100644 --- a/src/webview/mod.rs +++ b/src/webview/mod.rs @@ -162,6 +162,29 @@ pub struct WebViewAttributes { /// allow to navigate and false is not. pub navigation_handler: Option bool>>, + /// 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 + /// second is a mutable `PathBuf` reference that (possibly) represents where the file will be downloaded to. The latter + /// parameter can be used to set the download location by assigning a new path to it - the assigned path _must_ be + /// absolute. The closure returns a `bool` to allow or deny the download. + pub download_started_handler: Option bool>>, + + /// Sets a download completion handler to manage downloads that have finished. + /// + /// The closure is fired when the download completes, whether it was successful or not. + /// The closure takes a `String` representing the URL of the original download request, an `Option` + /// potentially representing the filesystem path the file was downloaded to, and a `bool` indicating if the download + /// succeeded. A value of `None` being passed instead of a `PathBuf` does not necessarily indicate that the download + /// did not succeed, and may instead indicate some other failure - always check the third parameter if you need to + /// know if the download succeeded. + /// + /// ## Platform-specific: + /// + /// - **macOS**: The second parameter indicating the path the file was saved to is always empty, due to API + /// limitations. + pub download_completed_handler: Option, bool) + 'static>>, + /// Set a new window handler to decide if incoming url is allowed to open in a new window. /// /// The closure take a `String` parameter as url and return `bool` to determine the url. True is @@ -208,6 +231,8 @@ impl Default for WebViewAttributes { ipc_handler: None, file_drop_handler: None, navigation_handler: None, + download_started_handler: None, + download_completed_handler: None, new_window_req_handler: None, clipboard: false, devtools: false, @@ -445,6 +470,41 @@ impl<'a> WebViewBuilder<'a> { 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 + /// second is a mutable `PathBuf` reference that (possibly) represents where the file will be downloaded to. The latter + /// parameter can be used to set the download location by assigning a new path to it - the assigned path _must_ be + /// absolute. The closure returns a `bool` to allow or deny the download. + pub fn with_download_started_handler( + mut self, + started_handler: impl FnMut(String, &mut PathBuf) -> bool + 'static, + ) -> Self { + self.webview.download_started_handler = Some(Box::new(started_handler)); + self + } + + /// Sets a download completion handler to manage downloads that have finished. + /// + /// The closure is fired when the download completes, whether it was successful or not. + /// The closure takes a `String` representing the URL of the original download request, an `Option` + /// potentially representing the filesystem path the file was downloaded to, and a `bool` indicating if the download + /// succeeded. A value of `None` being passed instead of a `PathBuf` does not necessarily indicate that the download + /// did not succeed, and may instead indicate some other failure - always check the third parameter if you need to + /// know if the download succeeded. + /// + /// ## Platform-specific: + /// + /// - **macOS**: The second parameter indicating the path the file was saved to is always empty, due to API + /// limitations. + pub fn with_download_completed_handler( + mut self, + download_completed_handler: impl Fn(String, Option, bool) + 'static, + ) -> Self { + self.webview.download_completed_handler = Some(Rc::new(download_completed_handler)); + self + } + /// Enables clipboard access for the page rendered on **Linux** and **Windows**. /// /// macOS doesn't provide such method and is always enabled by default. But you still need to add menu diff --git a/src/webview/webkitgtk/mod.rs b/src/webview/webkitgtk/mod.rs index 579189d26..dcbe0721b 100644 --- a/src/webview/webkitgtk/mod.rs +++ b/src/webview/webkitgtk/mod.rs @@ -238,6 +238,15 @@ impl InnerWebView { }); } + if attributes.download_started_handler.is_some() + || attributes.download_completed_handler.is_some() + { + web_context.register_download_handler( + attributes.download_started_handler, + attributes.download_completed_handler, + ) + } + // Gtk application window can only contain one widget at a time. // In tao, we add a GtkBox to pack menu bar. So we check if // there's a box widget here. diff --git a/src/webview/webkitgtk/web_context.rs b/src/webview/webkitgtk/web_context.rs index f9d5cd49f..431956b17 100644 --- a/src/webview/webkitgtk/web_context.rs +++ b/src/webview/webkitgtk/web_context.rs @@ -12,8 +12,11 @@ use http::{ }; use soup::{MessageHeaders, MessageHeadersType}; use std::{ + cell::RefCell, collections::{HashSet, VecDeque}, + path::PathBuf, rc::Rc, + str::FromStr, sync::{ atomic::{AtomicBool, Ordering::SeqCst}, Mutex, @@ -145,6 +148,12 @@ pub trait WebContextExt { fn allows_automation(&self) -> bool; fn register_automation(&mut self, webview: WebView); + + fn register_download_handler( + &mut self, + download_started_callback: Option bool>>, + download_completed_callback: Option, bool) + 'static>>, + ); } impl WebContextExt for super::WebContext { @@ -210,6 +219,66 @@ impl WebContextExt for super::WebContext { }); } } + + fn register_download_handler( + &mut self, + download_started_handler: Option bool>>, + download_completed_handler: Option, bool) + 'static>>, + ) { + use webkit2gtk::traits::*; + let context = &self.os.context; + + let download_started_handler = RefCell::new(download_started_handler); + let failed = Rc::new(RefCell::new(false)); + + context.connect_download_started(move |_context, download| { + if let Some(uri) = download.request().and_then(|req| req.uri()) { + let uri = uri.to_string(); + let mut download_location = download + .destination() + .and_then(|p| PathBuf::from_str(&p).ok()) + .unwrap_or_default(); + + if let Some(download_started_handler) = download_started_handler.borrow_mut().as_mut() { + if download_started_handler(uri, &mut download_location) { + download.connect_response_notify(move |download| { + download.set_destination(&download_location.to_string_lossy()); + }); + } else { + download.cancel(); + } + } + } + download.connect_failed({ + let failed = failed.clone(); + move |_, _error| { + *failed.borrow_mut() = true; + } + }); + if let Some(download_completed_handler) = download_completed_handler.clone() { + download.connect_finished({ + let failed = failed.clone(); + move |download| { + if let Some(uri) = download.request().and_then(|req| req.uri()) { + let failed = failed.borrow(); + let uri = uri.to_string(); + download_completed_handler( + uri, + (!*failed) + .then(|| { + download + .destination() + .map_or_else(|| None, |p| Some(PathBuf::from(p.as_str()))) + }) + .flatten(), + !*failed, + ) + } + } + }); + } + }); + } } fn actually_register_uri_scheme( diff --git a/src/webview/webview2/mod.rs b/src/webview/webview2/mod.rs index 2cfb208ad..bd8e0f884 100644 --- a/src/webview/webview2/mod.rs +++ b/src/webview/webview2/mod.rs @@ -13,7 +13,7 @@ use file_drop::FileDropController; use std::{ collections::HashSet, fmt::Write, iter::once, mem::MaybeUninit, os::windows::prelude::OsStrExt, - rc::Rc, sync::mpsc, + path::PathBuf, rc::Rc, sync::mpsc, }; use once_cell::unsync::OnceCell; @@ -353,6 +353,79 @@ window.addEventListener('mousemove', (e) => window.chrome.webview.postMessage('_ } } + if attributes.download_started_handler.is_some() + || attributes.download_completed_handler.is_some() + { + unsafe { + let webview4: ICoreWebView2_4 = + webview.cast().map_err(webview2_com::Error::WindowsError)?; + + let mut download_started_handler = attributes.download_started_handler.take(); + let download_completed_handler = attributes.download_completed_handler.take(); + + webview4 + .add_DownloadStarting( + &DownloadStartingEventHandler::create(Box::new(move |_, args| { + if let Some(args) = args { + let mut uri = PWSTR::null(); + args.DownloadOperation()?.Uri(&mut uri)?; + let uri = take_pwstr(uri); + + if let Some(download_completed_handler) = download_completed_handler.clone() { + args.DownloadOperation()?.add_StateChanged( + &StateChangedEventHandler::create(Box::new(move |download_operation, _| { + if let Some(download_operation) = download_operation { + let mut state: COREWEBVIEW2_DOWNLOAD_STATE = + COREWEBVIEW2_DOWNLOAD_STATE::default(); + download_operation.State(&mut state)?; + if state != COREWEBVIEW2_DOWNLOAD_STATE_IN_PROGRESS { + let mut path = PWSTR::null(); + download_operation.ResultFilePath(&mut path)?; + let path = take_pwstr(path); + let mut uri = PWSTR::null(); + download_operation.Uri(&mut uri)?; + let uri = take_pwstr(uri); + + let success = state == COREWEBVIEW2_DOWNLOAD_STATE_COMPLETED; + download_completed_handler( + uri, + success.then(|| PathBuf::from(path)), + success, + ); + } + } + + Ok(()) + })), + &mut token, + )?; + } + if let Some(download_started_handler) = download_started_handler.as_mut() { + let mut path = PWSTR::null(); + args.ResultFilePath(&mut path)?; + let path = take_pwstr(path); + let mut path = PathBuf::from(&path); + + if download_started_handler(uri, &mut path) { + let simplified = dunce::simplified(&path); + let result_file_path = + PCWSTR::from_raw(encode_wide(simplified.as_os_str()).as_ptr()); + args.SetResultFilePath(result_file_path)?; + args.SetHandled(true)?; + } else { + args.SetCancel(true)?; + } + } + } + + Ok(()) + })), + &mut token, + ) + .map_err(webview2_com::Error::WindowsError)?; + } + } + if let Some(new_window_req_handler) = attributes.new_window_req_handler { unsafe { webview diff --git a/src/webview/wkwebview/download.rs b/src/webview/wkwebview/download.rs new file mode 100644 index 000000000..b7ea497b8 --- /dev/null +++ b/src/webview/wkwebview/download.rs @@ -0,0 +1,120 @@ +use std::{path::PathBuf, ptr::null_mut, rc::Rc}; + +use cocoa::base::id; +use libc::c_void; +use objc::{ + declare::ClassDecl, + runtime::{Object, Sel}, +}; + +use super::NSString; + +pub(crate) unsafe fn set_download_delegate(webview: *mut Object, download_delegate: *mut Object) { + (*webview).set_ivar( + "DownloadDelegate", + download_delegate as *mut _ as *mut c_void, + ); +} + +unsafe fn get_download_delegate(this: &mut Object) -> *mut objc::runtime::Object { + let delegate: *mut c_void = *this.get_ivar("DownloadDelegate"); + delegate as *mut Object +} + +// Download action handler +extern "C" fn navigation_download_action(this: &mut Object, _: Sel, _: id, _: id, download: id) { + unsafe { + let delegate = get_download_delegate(this); + let _: () = msg_send![download, setDelegate: delegate]; + } +} + +// Download response handler +extern "C" fn navigation_download_response(this: &mut Object, _: Sel, _: id, _: id, download: id) { + unsafe { + let delegate = get_download_delegate(this); + let _: () = msg_send![download, setDelegate: delegate]; + } +} + +pub(crate) unsafe fn add_download_methods(decl: &mut ClassDecl) { + decl.add_ivar::<*mut c_void>("DownloadDelegate"); + + decl.add_method( + sel!(webView:navigationAction:didBecomeDownload:), + navigation_download_action as extern "C" fn(&mut Object, Sel, id, id, id), + ); + + decl.add_method( + sel!(webView:navigationResponse:didBecomeDownload:), + navigation_download_response as extern "C" fn(&mut Object, Sel, id, id, id), + ); +} + +pub extern "C" fn download_policy( + this: &Object, + _: Sel, + download: id, + _: id, + suggested_path: id, + handler: id, +) { + unsafe { + let request: id = msg_send![download, originalRequest]; + let url: id = msg_send![request, URL]; + let url: id = msg_send![url, absoluteString]; + let url = NSString(url); + let path = NSString(suggested_path); + let mut path = PathBuf::from(path.to_str()); + let handler = handler as *mut block::Block<(id,), c_void>; + + let function = this.get_ivar::<*mut c_void>("started"); + if !function.is_null() { + let function = &mut *(*function as *mut Box FnMut(String, &mut PathBuf) -> bool>); + match (function)(url.to_str().to_string(), &mut path) { + true => { + let nsurl: id = msg_send![class!(NSURL), fileURLWithPath: NSString::new(&path.display().to_string()) isDirectory: false]; + (*handler).call((nsurl,)) + } + false => (*handler).call((null_mut(),)), + }; + } else { + log::warn!("WebView instance is dropped! This navigation handler shouldn't be called."); + (*handler).call((null_mut(),)); + } + } +} + +pub extern "C" fn download_did_finish(this: &Object, _: Sel, download: id) { + unsafe { + let function = this.get_ivar::<*mut c_void>("completed"); + let original_request: id = msg_send![download, originalRequest]; + let url: id = msg_send![original_request, URL]; + let url: id = msg_send![url, absoluteString]; + let url = NSString(url).to_str().to_string(); + if !function.is_null() { + let function = &mut *(*function as *mut Rc Fn(String, Option, bool)>); + function(url, None, true); + } + } +} + +pub extern "C" fn download_did_fail(this: &Object, _: Sel, download: id, error: id, _: id) { + unsafe { + let description: id = msg_send![error, localizedDescription]; + let description = NSString(description).to_str().to_string(); + let original_request: id = msg_send![download, originalRequest]; + let url: id = msg_send![original_request, URL]; + let url: id = msg_send![url, absoluteString]; + let url = NSString(url).to_str().to_string(); + + #[cfg(debug_assertions)] + eprintln!("Download failed with error: {}", description); + + let function = this.get_ivar::<*mut c_void>("completed"); + if !function.is_null() { + let function = &mut *(*function as *mut Rc Fn(String, Option, bool)>); + function(url, None, false); + } + } +} diff --git a/src/webview/wkwebview/mod.rs b/src/webview/wkwebview/mod.rs index 8986d2aaf..1b7c4438d 100644 --- a/src/webview/wkwebview/mod.rs +++ b/src/webview/wkwebview/mod.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +mod download; #[cfg(target_os = "macos")] mod file_drop; mod web_context; @@ -43,7 +44,13 @@ use crate::{ dpi::{LogicalSize, PhysicalSize}, window::Window, }, - webview::{FileDropEvent, WebContext, WebViewAttributes, RGBA}, + webview::{ + wkwebview::download::{ + add_download_methods, download_did_fail, download_did_finish, download_policy, + set_download_delegate, + }, + FileDropEvent, WebContext, WebViewAttributes, RGBA, + }, Result, }; @@ -67,6 +74,7 @@ pub(crate) struct InnerWebView { navigation_decide_policy_ptr: *mut Box bool>, #[cfg(target_os = "macos")] file_drop_ptr: *mut (Box bool>, Rc), + download_delegate: id, protocol_ptrs: Vec<*mut Box>) -> Result>>>>, } @@ -356,66 +364,166 @@ impl InnerWebView { let url: id = msg_send![request, URL]; let url: id = msg_send![url, absoluteString]; let url = NSString(url); - + let should_download: bool = msg_send![action, shouldPerformDownload]; let target_frame: id = msg_send![action, targetFrame]; let is_main_frame: bool = msg_send![target_frame, isMainFrame]; let handler = handler as *mut block::Block<(NSInteger,), c_void>; - let function = this.get_ivar::<*mut c_void>("function"); - if !function.is_null() { - let function = &mut *(*function as *mut Box Fn(String, bool) -> bool>); - match (function)(url.to_str().to_string(), is_main_frame) { - true => (*handler).call((1,)), - false => (*handler).call((0,)), - }; + if should_download { + let has_download_handler = this.get_ivar::<*mut c_void>("HasDownloadHandler"); + if !has_download_handler.is_null() { + let has_download_handler = &mut *(*has_download_handler as *mut Box); + if **has_download_handler { + (*handler).call((2,)); + } + } } else { - log::warn!("WebView instance is dropped! This navigation handler shouldn't be called."); - (*handler).call((1,)); + let function = this.get_ivar::<*mut c_void>("function"); + if !function.is_null() { + let function = &mut *(*function as *mut Box Fn(String, bool) -> bool>); + match (function)(url.to_str().to_string(), is_main_frame) { + true => (*handler).call((1,)), + false => (*handler).call((0,)), + }; + } else { + log::warn!( + "WebView instance is dropped! This navigation handler shouldn't be called." + ); + (*handler).call((1,)); + } + } + } + } + + // Navigation handler + extern "C" fn navigation_policy_response( + this: &Object, + _: Sel, + _: id, + response: id, + handler: id, + ) { + unsafe { + let handler = handler as *mut block::Block<(NSInteger,), c_void>; + let can_show_mime_type: bool = msg_send![response, canShowMIMEType]; + + if !can_show_mime_type { + let has_download_handler = this.get_ivar::<*mut c_void>("HasDownloadHandler"); + if !has_download_handler.is_null() { + let has_download_handler = &mut *(*has_download_handler as *mut Box); + if **has_download_handler { + (*handler).call((2,)); + return; + } + } } + + (*handler).call((1,)); } } - let navigation_decide_policy_ptr = - if attributes.navigation_handler.is_some() || attributes.new_window_req_handler.is_some() { - let cls = match ClassDecl::new("UIViewController", class!(NSObject)) { + let (navigation_decide_policy_ptr, download_delegate) = if attributes + .navigation_handler + .is_some() + || attributes.new_window_req_handler.is_some() + || attributes.download_started_handler.is_some() + { + let cls = match ClassDecl::new("UIViewController", class!(NSObject)) { + Some(mut cls) => { + cls.add_ivar::<*mut c_void>("function"); + cls.add_ivar::<*mut c_void>("HasDownloadHandler"); + cls.add_method( + sel!(webView:decidePolicyForNavigationAction:decisionHandler:), + navigation_policy as extern "C" fn(&Object, Sel, id, id, id), + ); + cls.add_method( + sel!(webView:decidePolicyForNavigationResponse:decisionHandler:), + navigation_policy_response as extern "C" fn(&Object, Sel, id, id, id), + ); + add_download_methods(&mut cls); + cls.register() + } + None => class!(UIViewController), + }; + + let handler: id = msg_send![cls, new]; + let function_ptr = { + let navigation_handler = attributes.navigation_handler; + let new_window_req_handler = attributes.new_window_req_handler; + Box::into_raw(Box::new( + 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)) + } + }) as Box bool>, + )) + }; + (*handler).set_ivar("function", function_ptr as *mut _ as *mut c_void); + + let has_download_handler = Box::into_raw(Box::new(Box::new( + attributes.download_started_handler.is_some(), + ))); + (*handler).set_ivar( + "HasDownloadHandler", + has_download_handler as *mut _ as *mut c_void, + ); + let _: () = msg_send![webview, setNavigationDelegate: handler]; + + // Download handler + let download_delegate = if attributes.download_started_handler.is_some() + || attributes.download_completed_handler.is_some() + { + let cls = match ClassDecl::new("DownloadDelegate", class!(NSObject)) { Some(mut cls) => { - cls.add_ivar::<*mut c_void>("function"); + cls.add_ivar::<*mut c_void>("started"); + cls.add_ivar::<*mut c_void>("completed"); + cls.add_method( + sel!(download:decideDestinationUsingResponse:suggestedFilename:completionHandler:), + download_policy as extern "C" fn(&Object, Sel, id, id, id, id), + ); cls.add_method( - sel!(webView:decidePolicyForNavigationAction:decisionHandler:), - navigation_policy as extern "C" fn(&Object, Sel, id, id, id), + sel!(downloadDidFinish:), + download_did_finish as extern "C" fn(&Object, Sel, id), + ); + cls.add_method( + sel!(download:didFailWithError:resumeData:), + download_did_fail as extern "C" fn(&Object, Sel, id, id, id), ); cls.register() } None => class!(UIViewController), }; - let handler: id = msg_send![cls, new]; - let function_ptr = { - let navigation_handler = attributes.navigation_handler; - let new_window_req_handler = attributes.new_window_req_handler; - Box::into_raw(Box::new( - 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)) - } - }) as Box bool>, - )) - }; - (*handler).set_ivar("function", function_ptr as *mut _ as *mut c_void); + let download_delegate: id = msg_send![cls, new]; + if let Some(download_started_handler) = attributes.download_started_handler { + let download_started_ptr = Box::into_raw(Box::new(download_started_handler)); + (*download_delegate).set_ivar("started", download_started_ptr as *mut _ as *mut c_void); + } + if let Some(download_completed_handler) = attributes.download_completed_handler { + let download_completed_ptr = Box::into_raw(Box::new(download_completed_handler)); + (*download_delegate) + .set_ivar("completed", download_completed_ptr as *mut _ as *mut c_void); + } - let _: () = msg_send![webview, setNavigationDelegate: handler]; - function_ptr + set_download_delegate(handler, download_delegate); + + handler } else { null_mut() }; + (function_ptr, download_delegate) + } else { + (null_mut(), null_mut()) + }; + // File upload panel handler extern "C" fn run_file_upload_panel( _this: &Object, @@ -517,6 +625,7 @@ impl InnerWebView { navigation_decide_policy_ptr, #[cfg(target_os = "macos")] file_drop_ptr, + download_delegate, protocol_ptrs, }; @@ -740,6 +849,10 @@ impl Drop for InnerWebView { let _ = Box::from_raw(self.file_drop_ptr); } + if !self.download_delegate.is_null() { + let _ = self.download_delegate.drop_in_place(); + } + for ptr in self.protocol_ptrs.iter() { if !ptr.is_null() { let _ = Box::from_raw(*ptr);