diff --git a/.changes/cookies-api.md b/.changes/cookies-api.md new file mode 100644 index 000000000..930071629 --- /dev/null +++ b/.changes/cookies-api.md @@ -0,0 +1,5 @@ +--- +"wry": "patch" +--- + +Add `WebView::set_cookie` and `WebView::delete_cookie` APIs. diff --git a/examples/cookies.rs b/examples/cookies.rs new file mode 100644 index 000000000..5b856df59 --- /dev/null +++ b/examples/cookies.rs @@ -0,0 +1,73 @@ +// Copyright 2020-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use tao::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, +}; +use wry::WebViewBuilder; + +fn main() -> wry::Result<()> { + let event_loop = EventLoop::new(); + let window = WindowBuilder::new().build(&event_loop).unwrap(); + + let builder = WebViewBuilder::new().with_url("https://www.httpbin.org/cookies/set?foo=bar"); + + #[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)? + }; + + webview.set_cookie( + cookie::Cookie::build(("foo1", "bar1")) + .domain("www.httpbin.org") + .path("/") + .secure(true) + .http_only(true) + .max_age(cookie::time::Duration::seconds(10)) + .inner(), + )?; + + let cookie_deleted = cookie::Cookie::build(("will_be_deleted", "will_be_deleted")); + + webview.set_cookie(cookie_deleted.inner())?; + println!("Setting Cookies:"); + for cookie in webview.cookies()? { + println!("\t{cookie}"); + } + + println!("After Deleting:"); + webview.delete_cookie(cookie_deleted.inner())?; + for cookie in webview.cookies()? { + println!("\t{cookie}"); + } + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + if let Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } = event + { + *control_flow = ControlFlow::Exit; + } + }); +} diff --git a/src/android/mod.rs b/src/android/mod.rs index 03390a376..6f6805e0e 100644 --- a/src/android/mod.rs +++ b/src/android/mod.rs @@ -416,6 +416,16 @@ impl InnerWebView { rx.recv_timeout(MAIN_PIPE_TIMEOUT).map_err(Into::into) } + pub fn set_cookie(&self, cookie: &cookie::Cookie<'_>) -> Result<()> { + // Unsupported + Ok(()) + } + + pub fn delete_cookie(&self, cookie: &cookie::Cookie<'_>) -> Result<()> { + // Unsupported + Ok(()) + } + pub fn cookies(&self) -> Result>> { Ok(Vec::new()) } diff --git a/src/lib.rs b/src/lib.rs index 37434801e..cd5ec6564 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1835,6 +1835,24 @@ impl WebView { self.webview.cookies() } + /// Set a cookie for the webview. + /// + /// ## Platform-specific + /// + /// - **Android**: Not supported. + pub fn set_cookie(&self, cookie: &cookie::Cookie<'_>) -> Result<()> { + self.webview.set_cookie(cookie) + } + + /// Delete a cookie for the webview. + /// + /// ## Platform-specific + /// + /// - **Android**: Not supported. + pub fn delete_cookie(&self, cookie: &cookie::Cookie<'_>) -> Result<()> { + self.webview.delete_cookie(cookie) + } + /// 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 8232fadec..4f2110968 100644 --- a/src/webkitgtk/mod.rs +++ b/src/webkitgtk/mod.rs @@ -899,6 +899,41 @@ impl InnerWebView { cookie_builder.build() } + fn cookie_into_soup_cookie(cookie: &cookie::Cookie<'_>) -> soup::Cookie { + let mut soup_cookie = soup::Cookie::new( + cookie.name(), + cookie.value(), + cookie.domain().unwrap_or(""), + cookie.path().unwrap_or(""), + cookie + .max_age() + .map(|d| d.whole_seconds() as i32) + .unwrap_or(-1), + ); + + if let Some(dt) = cookie.expires_datetime() { + soup_cookie.set_expires(&glib::DateTime::from_unix_utc(dt.unix_timestamp()).unwrap()); + } + + if let Some(http_only) = cookie.http_only() { + soup_cookie.set_http_only(http_only); + } + + if let Some(same_site) = cookie.same_site() { + soup_cookie.set_same_site_policy(match same_site { + cookie::SameSite::Lax => soup::SameSitePolicy::Lax, + cookie::SameSite::Strict => soup::SameSitePolicy::Strict, + cookie::SameSite::None => soup::SameSitePolicy::None, + }); + } + + if let Some(secure) = cookie.secure() { + soup_cookie.set_secure(secure); + } + + soup_cookie + } + pub fn cookies_for_url(&self, url: &str) -> Result>> { let (tx, rx) = std::sync::mpsc::channel(); self @@ -953,6 +988,50 @@ impl InnerWebView { } } + pub fn set_cookie(&self, cookie: &cookie::Cookie<'_>) -> Result<()> { + let (tx, rx) = std::sync::mpsc::channel(); + self + .webview + .website_data_manager() + .and_then(|manager| manager.cookie_manager()) + .map(|cookies_manager| { + let mut soup_cookie = Self::cookie_into_soup_cookie(cookie); + cookies_manager.add_cookie(&mut soup_cookie, None::<&Cancellable>, move |ret| { + let _ = tx.send(ret); + }); + }); + + loop { + gtk::main_iteration(); + + if let Ok(response) = rx.try_recv() { + return response.map_err(Into::into); + } + } + } + + pub fn delete_cookie(&self, cookie: &cookie::Cookie<'_>) -> Result<()> { + let (tx, rx) = std::sync::mpsc::channel(); + self + .webview + .website_data_manager() + .and_then(|manager| manager.cookie_manager()) + .map(|cookies_manager| { + let mut soup_cookie = Self::cookie_into_soup_cookie(cookie); + cookies_manager.delete_cookie(&mut soup_cookie, None::<&Cancellable>, move |ret| { + let _ = tx.send(ret); + }); + }); + + 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, diff --git a/src/webview2/mod.rs b/src/webview2/mod.rs index 8141e208e..87a214b58 100644 --- a/src/webview2/mod.rs +++ b/src/webview2/mod.rs @@ -1469,6 +1469,57 @@ impl InnerWebView { Ok(cookie_builder.build()) } + unsafe fn cookie_into_win32( + cookie_manager: &ICoreWebView2CookieManager, + cookie: &cookie::Cookie<'_>, + ) -> windows::core::Result { + let name = HSTRING::from(cookie.name()); + let value = HSTRING::from(cookie.value()); + let domain = match cookie.domain() { + Some(domain) => HSTRING::from(domain), + None => HSTRING::new(), + }; + let path = match cookie.path() { + Some(path) => HSTRING::from(path), + None => HSTRING::new(), + }; + + let win32_cookie = cookie_manager.CreateCookie(&name, &value, &domain, &path)?; + + let expires = if let Some(max_age) = cookie.max_age() { + let expires_ = cookie::time::OffsetDateTime::now_utc() + .saturating_add(max_age) + .unix_timestamp(); + Some(expires_) + } else if let Some(dt) = cookie.expires_datetime() { + Some(dt.unix_timestamp()) + } else { + None + }; + if let Some(expires) = expires { + win32_cookie.SetExpires(expires as f64)?; + } + + if let Some(http_only) = cookie.http_only() { + win32_cookie.SetIsHttpOnly(http_only)?; + } + + if let Some(same_site) = cookie.same_site() { + let same_site = match same_site { + cookie::SameSite::Lax => COREWEBVIEW2_COOKIE_SAME_SITE_KIND_LAX, + cookie::SameSite::Strict => COREWEBVIEW2_COOKIE_SAME_SITE_KIND_STRICT, + cookie::SameSite::None => COREWEBVIEW2_COOKIE_SAME_SITE_KIND_NONE, + }; + win32_cookie.SetSameSite(same_site)?; + } + + if let Some(secure) = cookie.secure() { + win32_cookie.SetIsSecure(secure)?; + } + + Ok(win32_cookie) + } + pub fn cookies_for_url(&self, url: &str) -> Result>> { let uri = HSTRING::from(url); self.cookies_inner(PCWSTR::from_raw(uri.as_ptr())) @@ -1519,6 +1570,26 @@ impl InnerWebView { webview2_com::wait_with_pump(rx).map_err(Into::into) } + pub fn set_cookie(&self, cookie: &cookie::Cookie<'_>) -> Result<()> { + let webview = self.webview.cast::()?; + unsafe { + let cookie_manager = webview.CookieManager()?; + let cookie = Self::cookie_into_win32(&cookie_manager, cookie)?; + cookie_manager.AddOrUpdateCookie(&cookie)?; + } + Ok(()) + } + + pub fn delete_cookie(&self, cookie: &cookie::Cookie<'_>) -> Result<()> { + let webview = self.webview.cast::()?; + unsafe { + let cookie_manager = webview.CookieManager()?; + let cookie = Self::cookie_into_win32(&cookie_manager, cookie)?; + cookie_manager.DeleteCookie(&cookie)?; + } + Ok(()) + } + 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 66c7f1be2..c5ef62878 100644 --- a/src/wkwebview/mod.rs +++ b/src/wkwebview/mod.rs @@ -44,9 +44,11 @@ use objc2_core_foundation::CGSize; use objc2_core_foundation::{CGPoint, CGRect}; use objc2_foundation::{ ns_string, MainThreadMarker, NSArray, NSBundle, NSDate, NSError, NSHTTPCookie, - NSHTTPCookieSameSiteLax, NSHTTPCookieSameSiteStrict, NSJSONSerialization, NSMutableURLRequest, - NSNumber, NSObjectNSKeyValueCoding, NSObjectProtocol, NSString, NSUTF8StringEncoding, NSURL, - NSUUID, + NSHTTPCookieDomain, NSHTTPCookieExpires, NSHTTPCookieMaximumAge, NSHTTPCookieName, + NSHTTPCookiePath, NSHTTPCookiePropertyKey, NSHTTPCookieSameSiteLax, NSHTTPCookieSameSitePolicy, + NSHTTPCookieSameSiteStrict, NSHTTPCookieSecure, NSHTTPCookieValue, NSHTTPCookieVersion, + NSJSONSerialization, NSMutableDictionary, NSMutableURLRequest, NSNumber, + NSObjectNSKeyValueCoding, NSObjectProtocol, NSString, NSUTF8StringEncoding, NSURL, NSUUID, }; #[cfg(target_os = "ios")] use objc2_ui_kit::{UIScrollView, UIViewAutoresizing}; @@ -979,6 +981,73 @@ r#"Object.defineProperty(window, 'ipc', { cookie_builder.build() } + unsafe fn cookie_into_wkwebview(cookie: &cookie::Cookie<'_>) -> Retained { + let nstring_true: &'static NSString = ns_string!("TRUE"); + let nstring_false: &'static NSString = ns_string!("FALSE"); + let nstring_0: &'static NSString = ns_string!("0"); + let nstring_1: &'static NSString = ns_string!("1"); + + let name = NSString::from_str(cookie.name()); + let value = NSString::from_str(cookie.value()); + let path = cookie.path().map_or_else(NSString::new, NSString::from_str); + let domain = cookie + .domain() + .map_or_else(NSString::new, NSString::from_str); + + let properties: Retained> = + NSMutableDictionary::from_slices( + &[ + NSHTTPCookieName, + NSHTTPCookieValue, + NSHTTPCookiePath, + NSHTTPCookieDomain, + ], + &[&name, &value, &path, &domain], + ); + + if let Some(max_age_) = cookie.max_age() { + let max_age = NSString::from_str(&max_age_.whole_seconds().to_string()); + properties.insert(NSHTTPCookieMaximumAge, &*max_age); + properties.insert(NSHTTPCookieVersion, nstring_1); + } else if let Some(dt) = cookie.expires_datetime() { + let expires = NSDate::dateWithTimeIntervalSince1970(dt.unix_timestamp() as f64); + properties.insert(NSHTTPCookieExpires, &*expires); + properties.insert(NSHTTPCookieVersion, nstring_0); + } + + if let Some(secure) = cookie.secure() { + let secure = if secure { nstring_true } else { nstring_false }; + properties.insert(NSHTTPCookieSecure, secure); + } + + if let Some(http_only) = cookie.http_only() { + let http_only = if http_only { + nstring_true + } else { + nstring_false + }; + // ref: + // - + // - + properties.insert(ns_string!("HttpOnly"), http_only); + } + + if let Some(same_site) = cookie.same_site() { + match same_site { + cookie::SameSite::Lax => { + properties.insert(NSHTTPCookieSameSitePolicy, NSHTTPCookieSameSiteLax); + } + cookie::SameSite::Strict => { + properties.insert(NSHTTPCookieSameSitePolicy, NSHTTPCookieSameSiteStrict); + } + cookie::SameSite::None => {} + }; + } + + NSHTTPCookie::cookieWithProperties(&properties) + .expect("failed to create wkwebview cookie, report this as bug to `wry`") + } + pub fn cookies_for_url(&self, url: &str) -> Result>> { let url = url::Url::parse(url)?; @@ -1026,6 +1095,42 @@ r#"Object.defineProperty(window, 'ipc', { } } + pub fn set_cookie(&self, cookie: &cookie::Cookie<'_>) -> Result<()> { + let (tx, rx) = std::sync::mpsc::channel(); + + unsafe { + let wkwebview_cookie = Self::cookie_into_wkwebview(cookie); + self + .data_store + .httpCookieStore() + .setCookie_completionHandler( + &wkwebview_cookie, + Some(&block2::RcBlock::new(move || { + let _ = tx.send(()); + })), + ); + wait_for_blocking_operation(rx) + } + } + + pub fn delete_cookie(&self, cookie: &cookie::Cookie<'_>) -> Result<()> { + let (tx, rx) = std::sync::mpsc::channel(); + + unsafe { + let wkwebview_cookie = Self::cookie_into_wkwebview(cookie); + self + .data_store + .httpCookieStore() + .deleteCookie_completionHandler( + &wkwebview_cookie, + Some(&block2::RcBlock::new(move || { + let _ = tx.send(()); + })), + ); + wait_for_blocking_operation(rx) + } + } + #[cfg(target_os = "macos")] pub(crate) fn reparent(&self, window: *mut NSWindow) -> crate::Result<()> { unsafe {