diff --git a/.changes/cookies-api.md b/.changes/cookies-api.md new file mode 100644 index 000000000..de2225bbe --- /dev/null +++ b/.changes/cookies-api.md @@ -0,0 +1,5 @@ +--- +"wry": "patch" +--- + +Add `WebView::cookies` and `WebView::cookies_for_url` APIs. diff --git a/Cargo.toml b/Cargo.toml index 33fca77c8..e413dea31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,7 @@ thiserror = "1.0" http = "1.1" raw-window-handle = { version = "0.6", features = ["std"] } dpi = "0.1" +cookie = "0.18" [target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies] javascriptcore-rs = { version = "=1.1.2", features = [ @@ -93,6 +94,7 @@ features = [ ] [target."cfg(any(target_os = \"ios\", target_os = \"macos\"))".dependencies] +url = "2.5" block2 = "0.5" objc2 = { version = "0.5", features = ["exception"] } objc2-web-kit = { version = "0.2.0", features = [ @@ -119,6 +121,7 @@ objc2-web-kit = { version = "0.2.0", features = [ "WKWebpagePreferences", "WKNavigationResponse", "WKUserScript", + "WKHTTPCookieStore", ] } objc2-foundation = { version = "0.2.0", features = [ "NSURLRequest", @@ -137,6 +140,7 @@ objc2-foundation = { version = "0.2.0", features = [ "NSProcessInfo", "NSValue", "NSRange", + "NSRunLoop", ] } [target."cfg(target_os = \"ios\")".dependencies] diff --git a/examples/custom_protocol.rs b/examples/custom_protocol.rs index e19a9a759..b31fbcf41 100644 --- a/examples/custom_protocol.rs +++ b/examples/custom_protocol.rs @@ -19,46 +19,6 @@ fn main() -> wry::Result<()> { let window = WindowBuilder::new().build(&event_loop).unwrap(); let builder = WebViewBuilder::new() - .with_id("id2") - .with_custom_protocol( - "wry".into(), - move |_webview_id, request| match get_wry_response(request) { - Ok(r) => r.map(Into::into), - Err(e) => http::Response::builder() - .header(CONTENT_TYPE, "text/plain") - .status(500) - .body(e.to_string().as_bytes().to_vec()) - .unwrap() - .map(Into::into), - }, - ) - // tell the webview to load the custom protocol - .with_url("wry://localhost"); - - #[cfg(any( - target_os = "windows", - target_os = "macos", - target_os = "ios", - target_os = "android" - ))] - let _webview = builder.build(&window)?; - #[cfg(not(any( - target_os = "windows", - target_os = "macos", - target_os = "ios", - target_os = "android" - )))] - let _webview = { - use tao::platform::unix::WindowExtUnix; - use wry::WebViewBuilderExtUnix; - let vbox = window.default_vbox().unwrap(); - builder.build_gtk(vbox)? - }; - - let window = WindowBuilder::new().build(&event_loop).unwrap(); - - let builder = WebViewBuilder::new() - .with_id("id1") .with_custom_protocol( "wry".into(), move |_webview_id, request| match get_wry_response(request) { diff --git a/examples/simple.rs b/examples/simple.rs index d5d632b24..2cf47f6d9 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -59,7 +59,7 @@ fn main() -> wry::Result<()> { .. } = event { - *control_flow = ControlFlow::Exit + *control_flow = ControlFlow::Exit; } }); } diff --git a/src/android/kotlin/RustWebView.kt b/src/android/kotlin/RustWebView.kt index 1486791a6..cb7b0014e 100644 --- a/src/android/kotlin/RustWebView.kt +++ b/src/android/kotlin/RustWebView.kt @@ -97,6 +97,11 @@ class RustWebView(context: Context, val initScripts: Array, val id: Stri settings.userAgentString = ua } + fun getCookies(url: String): String { + val cookieManager = CookieManager.getInstance() + return cookieManager.getCookie(url) + } + private external fun shouldOverride(url: String): Boolean private external fun onEval(id: Int, result: String) diff --git a/src/android/main_pipe.rs b/src/android/main_pipe.rs index e5da92a51..c82232e36 100644 --- a/src/android/main_pipe.rs +++ b/src/android/main_pipe.rs @@ -10,7 +10,7 @@ use jni::{ JNIEnv, }; use once_cell::sync::Lazy; -use std::{os::unix::prelude::*, sync::atomic::Ordering}; +use std::{os::unix::prelude::*, str::FromStr, sync::atomic::Ordering}; use super::{find_class, EvalCallback, EVAL_CALLBACKS, EVAL_ID_GENERATOR, PACKAGE}; @@ -267,24 +267,6 @@ impl<'a> MainPipe<'a> { Err(e) => tx.send(Err(e.into())).unwrap(), } } - WebViewMessage::GetId(tx) => { - if let Some(webview) = &self.webview { - let url = self - .env - .call_method(webview.as_obj(), "getUrl", "()Ljava/lang/String;", &[]) - .and_then(|v| v.l()) - .and_then(|s| { - let s = JString::from(s); - self - .env - .get_string(&s) - .map(|v| v.to_string_lossy().to_string()) - }) - .unwrap_or_default(); - - tx.send(url).unwrap() - } - } WebViewMessage::GetUrl(tx) => { if let Some(webview) = &self.webview { let url = self @@ -329,6 +311,36 @@ impl<'a> MainPipe<'a> { load_html(&mut self.env, webview.as_obj(), &html)?; } } + WebViewMessage::GetCookies(tx, url) => { + if let Some(webview) = &self.webview { + let url = self.env.new_string(url)?; + let cookies = self + .env + .call_method( + webview, + "getCookies", + "(Ljava/lang/String;)Ljava/lang/String;", + &[(&url).into()], + ) + .and_then(|v| v.l()) + .and_then(|s| { + let s = JString::from(s); + self + .env + .get_string(&s) + .map(|v| v.to_string_lossy().to_string()) + }) + .unwrap_or_default(); + + tx.send( + cookies + .split("; ") + .flat_map(|c| cookie::Cookie::parse(c.to_string())) + .collect(), + ) + .unwrap(); + } + } } } Ok(()) @@ -395,8 +407,8 @@ pub(crate) enum WebViewMessage { Eval(String, Option), SetBackgroundColor(RGBA), GetWebViewVersion(Sender>), - GetId(Sender), GetUrl(Sender), + GetCookies(Sender>>, String), Jni(Box), LoadUrl(String, Option), LoadHtml(String), diff --git a/src/android/mod.rs b/src/android/mod.rs index ae96095ee..32aecd856 100644 --- a/src/android/mod.rs +++ b/src/android/mod.rs @@ -375,6 +375,16 @@ impl InnerWebView { Ok(()) } + pub fn cookies_for_url(&self, url: &str) -> Result>> { + let (tx, rx) = bounded(1); + MainPipe::send(WebViewMessage::GetCookies(tx, url.to_string())); + rx.recv().map_err(Into::into) + } + + pub fn cookies(&self) -> Result>> { + Ok(Vec::new()) + } + pub fn bounds(&self) -> Result { Ok(crate::Rect::default()) } diff --git a/src/error.rs b/src/error.rs index e3b026117..a7fad37e7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -65,4 +65,7 @@ pub enum Error { DuplicateCustomProtocol(String), #[error("Duplicate custom protocol registered on the same web context on Linux: {0}")] ContextDuplicateCustomProtocol(String), + #[error(transparent)] + #[cfg(any(target_os = "macos", target_os = "ios"))] + UrlPrase(#[from] url::ParseError), } diff --git a/src/lib.rs b/src/lib.rs index bf257c662..3c6344825 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -248,6 +248,7 @@ use std::{borrow::Cow, collections::HashMap, path::PathBuf, rc::Rc}; use http::{Request, Response}; +pub use cookie; pub use dpi; pub use error::*; pub use http; @@ -1517,6 +1518,20 @@ impl WebView { self.webview.print() } + /// Get a list of cookies for specific url. + pub fn cookies_for_url(&self, url: &str) -> Result>> { + self.webview.cookies_for_url(url) + } + + /// Get the list of cookies. + /// + /// ## Platform-specific + /// + /// - **Android**: Unsupported, always returns an empty [`Vec`]. + pub fn cookies(&self) -> Result>> { + self.webview.cookies() + } + /// Open the web inspector which is usually called dev tool. /// /// ## Platform-specific diff --git a/src/webkitgtk/mod.rs b/src/webkitgtk/mod.rs index 9cbf75b88..38b99dfc3 100644 --- a/src/webkitgtk/mod.rs +++ b/src/webkitgtk/mod.rs @@ -3,6 +3,7 @@ // SPDX-License-Identifier: MIT use dpi::{LogicalPosition, LogicalSize}; +use ffi::CookieManageExt; use gdkx11::{ ffi::{gdk_x11_window_foreign_new_for_display, GdkX11Display}, X11Display, @@ -25,7 +26,7 @@ use std::{ #[cfg(any(debug_assertions, feature = "devtools"))] use webkit2gtk::WebInspectorExt; use webkit2gtk::{ - AutoplayPolicy, InputMethodContextExt, LoadEvent, NavigationPolicyDecision, + AutoplayPolicy, CookieManagerExt, InputMethodContextExt, LoadEvent, NavigationPolicyDecision, NavigationPolicyDecisionExt, NetworkProxyMode, NetworkProxySettings, PolicyDecisionType, PrintOperationExt, SettingsExt, URIRequest, URIRequestExt, UserContentInjectedFrames, UserContentManager, UserContentManagerExt, UserScript, UserScriptInjectionTime, @@ -804,6 +805,103 @@ impl InnerWebView { Ok(()) } + fn cookie_from_soup_cookie(mut cookie: soup::Cookie) -> cookie::Cookie<'static> { + let name = cookie.name().map(|n| n.to_string()).unwrap_or_default(); + let value = cookie.value().map(|n| n.to_string()).unwrap_or_default(); + + let mut cookie_builder = cookie::CookieBuilder::new(name, value); + + if let Some(domain) = cookie.domain().map(|n| n.to_string()) { + cookie_builder = cookie_builder.domain(domain); + } + + if let Some(path) = cookie.path().map(|n| n.to_string()) { + cookie_builder = cookie_builder.path(path); + } + + let http_only = cookie.is_http_only(); + cookie_builder = cookie_builder.http_only(http_only); + + let secure = cookie.is_secure(); + cookie_builder = cookie_builder.secure(secure); + + let same_site = cookie.same_site_policy(); + let same_site = match same_site { + soup::SameSitePolicy::Lax => cookie::SameSite::Lax, + soup::SameSitePolicy::Strict => cookie::SameSite::Strict, + soup::SameSitePolicy::None => cookie::SameSite::None, + _ => cookie::SameSite::None, + }; + cookie_builder = cookie_builder.same_site(same_site); + + let expires = cookie.expires(); + let expires = match expires { + Some(datetime) => cookie::time::OffsetDateTime::from_unix_timestamp(datetime.to_unix()) + .ok() + .map(cookie::Expiration::DateTime), + None => Some(cookie::Expiration::Session), + }; + if let Some(expires) = expires { + cookie_builder = cookie_builder.expires(expires); + } + + cookie_builder.build() + } + + pub fn cookies_for_url(&self, url: &str) -> Result>> { + let (tx, rx) = std::sync::mpsc::channel(); + self + .webview + .website_data_manager() + .and_then(|manager| manager.cookie_manager()) + .map(|cookies_manager| { + cookies_manager.cookies(url, None::<&Cancellable>, move |cookies| { + let cookies = cookies.map(|cookies| { + cookies + .into_iter() + .map(Self::cookie_from_soup_cookie) + .collect() + }); + let _ = tx.send(cookies); + }) + }); + + loop { + gtk::main_iteration(); + + if let Ok(response) = rx.try_recv() { + return response.map_err(Into::into); + } + } + } + + pub fn cookies(&self) -> Result>> { + let (tx, rx) = std::sync::mpsc::channel(); + self + .webview + .website_data_manager() + .and_then(|manager| manager.cookie_manager()) + .map(|cookies_manager| { + cookies_manager.all_cookies(None::<&Cancellable>, move |cookies| { + let cookies = cookies.map(|cookies| { + cookies + .into_iter() + .map(Self::cookie_from_soup_cookie) + .collect() + }); + let _ = tx.send(cookies); + }) + }); + + loop { + gtk::main_iteration(); + + if let Ok(response) = rx.try_recv() { + return response.map_err(Into::into); + } + } + } + pub fn reparent(&self, container: &W) -> Result<()> where W: gtk::prelude::IsA, @@ -860,3 +958,88 @@ fn scale_factor_from_x11(xlib: &Xlib, display: *mut _XDisplay, parent: c_ulong) let scale_factor = unsafe { (*attrs.screen).width as f64 * 25.4 / (*attrs.screen).mwidth as f64 }; scale_factor / BASE_DPI } + +mod ffi { + use gtk::{ + gdk, + gio::{ + self, + ffi::{GAsyncReadyCallback, GCancellable}, + prelude::*, + Cancellable, + }, + glib::{ + self, + translate::{FromGlibPtrContainer, ToGlibPtr}, + }, + }; + use webkit2gtk::CookieManager; + use webkit2gtk_sys::WebKitCookieManager; + + pub trait CookieManageExt: IsA + 'static { + fn all_cookies, glib::Error>) + 'static>( + &self, + cancellable: Option<&impl IsA>, + callback: P, + ) { + let main_context = glib::MainContext::ref_thread_default(); + let is_main_context_owner = main_context.is_owner(); + let has_acquired_main_context = (!is_main_context_owner) + .then(|| main_context.acquire().ok()) + .flatten(); + assert!( + is_main_context_owner || has_acquired_main_context.is_some(), + "Async operations only allowed if the thread is owning the MainContext" + ); + + let user_data: Box> = + Box::new(glib::thread_guard::ThreadGuard::new(callback)); + unsafe extern "C" fn cookies_trampoline< + P: FnOnce(std::result::Result, glib::Error>) + 'static, + >( + _source_object: *mut glib::gobject_ffi::GObject, + res: *mut gdk::gio::ffi::GAsyncResult, + user_data: glib::ffi::gpointer, + ) { + let mut error = std::ptr::null_mut(); + let ret = + webkit_cookie_manager_get_all_cookies_finish(_source_object as *mut _, res, &mut error); + let result = if error.is_null() { + Ok(FromGlibPtrContainer::from_glib_full(ret)) + } else { + Err(glib::translate::from_glib_full(error)) + }; + let callback: Box> = Box::from_raw(user_data as *mut _); + let callback: P = callback.into_inner(); + callback(result); + } + let callback = cookies_trampoline::

; + + unsafe { + webkit_cookie_manager_get_all_cookies( + self.as_ref().to_glib_none().0, + cancellable.map(|p| p.as_ref()).to_glib_none().0, + Some(callback), + Box::into_raw(user_data) as *mut _, + ); + } + } + } + + impl CookieManageExt for CookieManager {} + + extern "C" { + pub fn webkit_cookie_manager_get_all_cookies( + cookie_manager: *mut webkit2gtk_sys::WebKitCookieManager, + cancellable: *mut GCancellable, + callback: GAsyncReadyCallback, + user_data: glib::ffi::gpointer, + ); + + pub fn webkit_cookie_manager_get_all_cookies_finish( + cookie_manager: *mut WebKitCookieManager, + result: *mut gio::ffi::GAsyncResult, + error: *mut *mut glib::ffi::GError, + ) -> *mut glib::ffi::GList; + } +} diff --git a/src/webview2/mod.rs b/src/webview2/mod.rs index dcdc46a77..d4e294360 100644 --- a/src/webview2/mod.rs +++ b/src/webview2/mod.rs @@ -293,42 +293,42 @@ impl InnerWebView { }); let (tx, rx) = mpsc::channel(); - CreateCoreWebView2EnvironmentCompletedHandler::wait_for_async_operation( - Box::new(move |environmentcreatedhandler| unsafe { - let options = CoreWebView2EnvironmentOptions::default(); - - options.set_additional_browser_arguments(additional_browser_args); - options.set_are_browser_extensions_enabled(pl_attrs.browser_extensions_enabled); - - // Get user's system language - let lcid = GetUserDefaultUILanguage(); - let mut lang = [0; MAX_LOCALE_NAME as usize]; - LCIDToLocaleName(lcid as u32, Some(&mut lang), LOCALE_ALLOW_NEUTRAL_NAMES); - options.set_language(String::from_utf16_lossy(&lang)); - - let scroll_bar_style = match pl_attrs.scroll_bar_style { - ScrollBarStyle::Default => COREWEBVIEW2_SCROLLBAR_STYLE_DEFAULT, - ScrollBarStyle::FluentOverlay => COREWEBVIEW2_SCROLLBAR_STYLE_FLUENT_OVERLAY, - }; + let options = CoreWebView2EnvironmentOptions::default(); + unsafe { + options.set_additional_browser_arguments(additional_browser_args); + options.set_are_browser_extensions_enabled(pl_attrs.browser_extensions_enabled); + + // Get user's system language + let lcid = GetUserDefaultUILanguage(); + let mut lang = [0; MAX_LOCALE_NAME as usize]; + LCIDToLocaleName(lcid as u32, Some(&mut lang), LOCALE_ALLOW_NEUTRAL_NAMES); + options.set_language(String::from_utf16_lossy(&lang)); + + let scroll_bar_style = match pl_attrs.scroll_bar_style { + ScrollBarStyle::Default => COREWEBVIEW2_SCROLLBAR_STYLE_DEFAULT, + ScrollBarStyle::FluentOverlay => COREWEBVIEW2_SCROLLBAR_STYLE_FLUENT_OVERLAY, + }; - options.set_scroll_bar_style(scroll_bar_style); + options.set_scroll_bar_style(scroll_bar_style); - CreateCoreWebView2EnvironmentWithOptions( - PCWSTR::null(), - &data_directory.unwrap_or_default(), - &ICoreWebView2EnvironmentOptions::from(options), - &environmentcreatedhandler, - ) - .map_err(Into::into) - }), - Box::new(move |error_code, environment| { - error_code?; - tx.send(environment.ok_or_else(|| windows::core::Error::from(E_POINTER))) - .map_err(|_| windows::core::Error::from(E_UNEXPECTED)) - }), - )?; + CreateCoreWebView2EnvironmentWithOptions( + PCWSTR::null(), + &data_directory.unwrap_or_default(), + &ICoreWebView2EnvironmentOptions::from(options), + // we don't use CreateCoreWebView2EnvironmentCompletedHandler::wait_for_async + // as it uses an mspc::channel under the hood, so we can avoid using two channels + // by manually creating the callback handler and use webview2_com::with_with_bump + &CreateCoreWebView2EnvironmentCompletedHandler::create(Box::new( + move |error_code, environment| { + error_code?; + tx.send(environment.ok_or_else(|| windows::core::Error::from(E_POINTER))) + .map_err(|_| windows::core::Error::from(E_UNEXPECTED)) + }, + )), + )?; + } - rx.recv()?.map_err(Into::into) + webview2_com::wait_with_pump(rx)?.map_err(Into::into) } #[inline] @@ -341,34 +341,28 @@ impl InnerWebView { let env = env.clone(); let env10 = env.cast::(); - CreateCoreWebView2ControllerCompletedHandler::wait_for_async_operation( - if let Ok(env10) = env10 { - let controller_opts = unsafe { env10.CreateCoreWebView2ControllerOptions()? }; - unsafe { controller_opts.SetIsInPrivateModeEnabled(incognito)? } - Box::new( - move |handler: ICoreWebView2CreateCoreWebView2ControllerCompletedHandler| unsafe { - env10 - .CreateCoreWebView2ControllerWithOptions(hwnd, &controller_opts, &handler) - .map_err(Into::into) - }, - ) - } else { - Box::new( - move |handler: ICoreWebView2CreateCoreWebView2ControllerCompletedHandler| unsafe { - env - .CreateCoreWebView2Controller(hwnd, &handler) - .map_err(Into::into) - }, - ) - }, - Box::new(move |error_code, controller| { + // we don't use CreateCoreWebView2ControllerCompletedHandler::wait_for_async + // as it uses an mspc::channel under the hood, so we can avoid using two channels + // by manually creating the callback handler and use webview2_com::with_with_bump + let handler = CreateCoreWebView2ControllerCompletedHandler::create(Box::new( + move |error_code, controller| { error_code?; tx.send(controller.ok_or_else(|| windows::core::Error::from(E_POINTER))) .map_err(|_| windows::core::Error::from(E_UNEXPECTED)) - }), - )?; + }, + )); + + unsafe { + if let Ok(env10) = env10 { + let controller_opts = env10.CreateCoreWebView2ControllerOptions()?; + controller_opts.SetIsInPrivateModeEnabled(incognito)?; + env10.CreateCoreWebView2ControllerWithOptions(hwnd, &controller_opts, &handler)?; + } else { + env.CreateCoreWebView2Controller(hwnd, &handler)? + } + } - rx.recv()?.map_err(Into::into) + webview2_com::wait_with_pump(rx)?.map_err(Into::into) } #[inline] @@ -1326,6 +1320,112 @@ impl InnerWebView { Ok(()) } + unsafe fn cookie_from_win32(cookie: ICoreWebView2Cookie) -> Result> { + let mut name = PWSTR::null(); + cookie.Name(&mut name)?; + let name = take_pwstr(name); + + let mut value = PWSTR::null(); + cookie.Value(&mut value)?; + let value = take_pwstr(value); + + let mut cookie_builder = cookie::CookieBuilder::new(name, value); + + let mut domain = PWSTR::null(); + cookie.Domain(&mut domain)?; + cookie_builder = cookie_builder.domain(take_pwstr(domain)); + + let mut path = PWSTR::null(); + cookie.Path(&mut path)?; + cookie_builder = cookie_builder.path(take_pwstr(path)); + + let mut http_only: BOOL = false.into(); + cookie.IsHttpOnly(&mut http_only)?; + cookie_builder = cookie_builder.http_only(http_only.as_bool()); + + let mut secure: BOOL = false.into(); + cookie.IsSecure(&mut secure)?; + cookie_builder = cookie_builder.secure(secure.as_bool()); + + let mut same_site = COREWEBVIEW2_COOKIE_SAME_SITE_KIND_LAX; + cookie.SameSite(&mut same_site)?; + let same_site = match same_site { + COREWEBVIEW2_COOKIE_SAME_SITE_KIND_LAX => cookie::SameSite::Lax, + COREWEBVIEW2_COOKIE_SAME_SITE_KIND_STRICT => cookie::SameSite::Strict, + COREWEBVIEW2_COOKIE_SAME_SITE_KIND_NONE => cookie::SameSite::None, + _ => cookie::SameSite::None, + }; + cookie_builder = cookie_builder.same_site(same_site); + + let mut is_session: BOOL = false.into(); + cookie.IsSession(&mut is_session)?; + + let mut expires = 0.0; + cookie.Expires(&mut expires)?; + + let expires = match expires { + -1.0 | _ if is_session.as_bool() => Some(cookie::Expiration::Session), + datetime => cookie::time::OffsetDateTime::from_unix_timestamp(datetime as _) + .ok() + .map(cookie::Expiration::DateTime), + }; + if let Some(expires) = expires { + cookie_builder = cookie_builder.expires(expires); + } + + Ok(cookie_builder.build()) + } + + pub fn cookies_for_url(&self, url: &str) -> Result>> { + let uri = HSTRING::from(url); + self.cookies_inner(PCWSTR::from_raw(uri.as_ptr())) + } + + pub fn cookies(&self) -> Result>> { + self.cookies_inner(PCWSTR::null()) + } + + fn cookies_inner(&self, uri: PCWSTR) -> Result>> { + let (tx, rx) = mpsc::channel(); + + let webview = self.webview.cast::()?; + unsafe { + webview.CookieManager()?.GetCookies( + uri, + // we don't use GetCookiesCompletedHandler::wait_for_async + // as it uses an mspc::channel under the hood, so we can avoid using two channels + // by manually creating the callback handler and use webview2_com::with_with_bump + &GetCookiesCompletedHandler::create(Box::new(move |error_code, cookies| { + error_code?; + + let cookies = if let Some(cookies) = cookies { + let mut count = 0; + cookies.Count(&mut count)?; + + let mut out = Vec::with_capacity(count as _); + + for idx in 0..count { + let cookie = cookies.GetValueAtIndex(idx)?; + + if let Ok(cookie) = Self::cookie_from_win32(cookie) { + out.push(cookie) + } + } + + out + } else { + Vec::new() + }; + + tx.send(cookies) + .map_err(|_| windows::core::Error::from(E_UNEXPECTED)) + })), + )?; + } + + webview2_com::wait_with_pump(rx).map_err(Into::into) + } + pub fn reparent(&self, parent: isize) -> Result<()> { let parent = HWND(parent as _); diff --git a/src/wkwebview/mod.rs b/src/wkwebview/mod.rs index 8fd3b96fc..61c8baf64 100644 --- a/src/wkwebview/mod.rs +++ b/src/wkwebview/mod.rs @@ -42,9 +42,10 @@ use objc2_app_kit::{NSApplication, NSAutoresizingMaskOptions, NSTitlebarSeparato #[cfg(target_os = "macos")] use objc2_foundation::CGSize; use objc2_foundation::{ - ns_string, CGPoint, CGRect, MainThreadMarker, NSBundle, NSDate, NSError, NSJSONSerialization, - NSMutableURLRequest, NSNumber, NSObjectNSKeyValueCoding, NSObjectProtocol, NSString, - NSUTF8StringEncoding, NSURL, NSUUID, + ns_string, CGPoint, CGRect, MainThreadMarker, NSArray, NSBundle, NSDate, NSError, NSHTTPCookie, + NSHTTPCookieSameSiteLax, NSHTTPCookieSameSiteStrict, NSJSONSerialization, NSMutableURLRequest, + NSNumber, NSObjectNSKeyValueCoding, NSObjectProtocol, NSString, NSUTF8StringEncoding, NSURL, + NSUUID, }; #[cfg(target_os = "ios")] use objc2_ui_kit::{UIScrollView, UIViewAutoresizing}; @@ -71,10 +72,11 @@ use raw_window_handle::{HasWindowHandle, RawWindowHandle}; use std::{ collections::{HashMap, HashSet}, ffi::{c_void, CString}, + net::Ipv4Addr, os::raw::c_char, panic::AssertUnwindSafe, - ptr::null_mut, - str, + ptr::{null_mut, NonNull}, + str::{self, FromStr}, sync::{Arc, Mutex}, }; @@ -112,6 +114,7 @@ pub(crate) struct InnerWebView { id: String, pub webview: Retained, pub manager: Retained, + data_store: Retained, ns_view: Retained, #[allow(dead_code)] is_child: bool, @@ -268,8 +271,7 @@ impl InnerWebView { } }; - let proxies: Retained> = - objc2_foundation::NSArray::arrayWithObject(&*proxy_config); + let proxies: Retained> = NSArray::arrayWithObject(&*proxy_config); data_store.setValue_forKey(Some(&proxies), ns_string!("proxyConfigurations")); } @@ -462,6 +464,7 @@ impl InnerWebView { webview: webview.clone(), manager: manager.clone(), ns_view: ns_view.retain(), + data_store, pending_scripts, ipc_handler_delegate, document_title_changed_observer, @@ -825,6 +828,95 @@ r#"Object.defineProperty(window, 'ipc', { Ok(()) } + unsafe fn cookie_from_wkwebview(cookie: &NSHTTPCookie) -> cookie::Cookie<'static> { + let name = cookie.name().to_string(); + let value = cookie.value().to_string(); + + let mut cookie_builder = cookie::CookieBuilder::new(name, value); + + let domain = cookie.domain().to_string(); + cookie_builder = cookie_builder.domain(domain); + + let path = cookie.path().to_string(); + cookie_builder = cookie_builder.path(path); + + let http_only = cookie.isHTTPOnly(); + cookie_builder = cookie_builder.http_only(http_only); + + let secure = cookie.isSecure(); + cookie_builder = cookie_builder.secure(secure); + + let same_site = cookie.sameSitePolicy(); + let same_site = match same_site { + Some(policy) if policy.as_ref() == NSHTTPCookieSameSiteLax => cookie::SameSite::Lax, + Some(policy) if policy.as_ref() == NSHTTPCookieSameSiteStrict => cookie::SameSite::Strict, + _ => cookie::SameSite::None, + }; + cookie_builder = cookie_builder.same_site(same_site); + + let expires = cookie.expiresDate(); + let expires = match expires { + Some(datetime) => { + cookie::time::OffsetDateTime::from_unix_timestamp(datetime.timeIntervalSince1970() as i64) + .ok() + .map(cookie::Expiration::DateTime) + } + None => Some(cookie::Expiration::Session), + }; + if let Some(expires) = expires { + cookie_builder = cookie_builder.expires(expires); + } + + cookie_builder.build() + } + + pub fn cookies_for_url(&self, url: &str) -> Result>> { + let url = url::Url::parse(url)?; + + self.cookies().map(|cookies| { + cookies.into_iter().filter(|cookie: &cookie::Cookie| { + let secure = cookie.secure().unwrap_or_default(); + // domain is the same + cookie.domain() == url.domain() + // and one of + && ( + // cookie is secure and url is https + (secure && url.scheme() == "https") || + // or cookie is secure and is localhost + ( + secure && url.scheme() == "http" && + (url.domain() == Some("localhost") || url.domain().and_then(|d| Ipv4Addr::from_str(d).ok()).map(|ip| ip.is_loopback()).unwrap_or(false)) + ) || + // or cookie is not secure + (!secure) + ) + }).collect() + }) + } + + pub fn cookies(&self) -> Result>> { + let (tx, rx) = std::sync::mpsc::channel(); + + unsafe { + self + .data_store + .httpCookieStore() + .getAllCookies(&block2::RcBlock::new( + move |cookies: NonNull>| { + let cookies = cookies.as_ref(); + let cookies = cookies + .to_vec() + .into_iter() + .map(|cookie| Self::cookie_from_wkwebview(cookie)) + .collect(); + let _ = tx.send(cookies); + }, + )); + + wait_for_blocking_operation(rx) + } + } + #[cfg(target_os = "macos")] pub(crate) fn reparent(&self, window: *mut NSWindow) -> crate::Result<()> { unsafe { @@ -905,3 +997,25 @@ 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) } + +unsafe fn wait_for_blocking_operation(rx: std::sync::mpsc::Receiver) -> Result { + let interval = 0.0002; + let limit = 1.; + let mut elapsed = 0.; + // run event loop until we get the response back, blocking for at most 3 seconds + loop { + let rl = objc2_foundation::NSRunLoop::mainRunLoop(); + let d = NSDate::dateWithTimeIntervalSinceNow(interval); + rl.runUntilDate(&d); + if let Ok(response) = rx.try_recv() { + return Ok(response); + } + elapsed += interval; + if elapsed >= limit { + return Err(Error::Io(std::io::Error::new( + std::io::ErrorKind::TimedOut, + "timed out waiting for cookies response", + ))); + } + } +}