leptos-use/src/use_cookie.rs

891 lines
31 KiB
Rust
Raw Normal View History

2024-01-31 16:54:25 +00:00
#![allow(clippy::too_many_arguments)]
use crate::core::now;
2024-07-08 17:10:29 +01:00
use codee::{CodecError, Decoder, Encoder};
2024-01-31 16:54:25 +00:00
use cookie::time::{Duration, OffsetDateTime};
2024-06-05 11:53:59 +02:00
pub use cookie::SameSite;
use cookie::{Cookie, CookieJar};
use default_struct_builder::DefaultBuilder;
2024-01-31 16:54:25 +00:00
use leptos::*;
use std::rc::Rc;
2024-01-21 17:33:53 +05:30
2024-01-31 16:54:25 +00:00
/// SSR-friendly and reactive cookie access.
///
/// You can use this function multiple times for the same cookie and their signals will synchronize
2024-01-31 19:55:30 +00:00
/// (even across windows/tabs). But there is no way to listen to changes to `document.cookie` directly so in case
/// something outside of this function changes the cookie, the signal will **not** be updated.
///
/// When the options `max_age` or `expire` is given then the returned signal will
/// automatically turn to `None` after that time.
///
/// ## Demo
///
/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_cookie)
///
/// ## Usage
///
2024-01-31 16:54:25 +00:00
/// The example below creates a cookie called `counter`. If the cookie doesn't exist, it is initially set to a random value.
/// Whenever we update the `counter` variable, the cookie will be updated accordingly.
///
/// ```
/// # use leptos::*;
/// # use leptos_use::use_cookie;
2024-07-08 17:10:29 +01:00
/// # use codee::string::FromToStringCodec;
2024-01-31 16:54:25 +00:00
/// # use rand::prelude::*;
///
/// #
/// # #[component]
/// # fn Demo() -> impl IntoView {
2024-01-31 16:54:25 +00:00
/// let (counter, set_counter) = use_cookie::<u32, FromToStringCodec>("counter");
///
/// let reset = move || set_counter.set(Some(random()));
///
/// if counter.get().is_none() {
/// reset();
/// }
2024-01-31 16:54:25 +00:00
///
/// let increase = move || {
/// set_counter.set(counter.get().map(|c| c + 1));
/// };
///
/// view! {
/// <p>Counter: {move || counter.get().map(|c| c.to_string()).unwrap_or("—".to_string())}</p>
/// <button on:click=move |_| reset()>Reset</button>
/// <button on:click=move |_| increase()>+</button>
/// }
/// # }
/// ```
///
/// Values are (en)decoded via the given codec. You can use any of the string codecs or a
/// binary codec wrapped in [`Base64`].
///
/// > Please check [the codec chapter](https://leptos-use.rs/codecs.html) to see what codecs are
/// available and what feature flags they require.
2024-01-31 16:54:25 +00:00
///
/// ## Cookie attributes
///
/// As part of the options when you use `use_cookie_with_options` you can specify cookie attributes.
///
/// ```
/// # use cookie::SameSite;
/// # use leptos::*;
/// # use leptos_use::{use_cookie_with_options, UseCookieOptions};
2024-07-08 17:10:29 +01:00
/// # use codee::string::FromToStringCodec;
2024-01-31 16:54:25 +00:00
/// #
/// # #[component]
/// # fn Demo() -> impl IntoView {
/// let (cookie, set_cookie) = use_cookie_with_options::<bool, FromToStringCodec>(
/// "user_info",
/// UseCookieOptions::default()
/// .max_age(3600_000) // one hour
/// .same_site(SameSite::Lax)
/// );
/// #
/// # view! {}
/// # }
/// ```
///
2024-01-23 15:44:33 +00:00
/// ## Server-Side Rendering
///
/// This works equally well on the server or the client.
2024-01-31 16:54:25 +00:00
/// On the server this function reads the cookie from the HTTP request header and writes it back into
/// the HTTP response header according to options (if provided).
/// The returned `WriteSignal` may not affect the cookie headers on the server! It will try and write
/// the headers buy if this happens after the headers have already been streamed to the client then
/// this will have no effect.
///
2024-01-23 16:06:16 +00:00
/// > If you're using `axum` you have to enable the `"axum"` feature in your Cargo.toml.
2024-04-19 14:54:37 +01:00
/// > In case it's `actix-web` enable the feature `"actix"`, for `spin` enable `"spin"`.
///
/// ### Bring your own header
///
/// In case you're neither using Axum nor Actix, or the default implementation is not to your liking,
2024-01-31 16:54:25 +00:00
/// you can provide your own way of reading and writing the cookie header value.
///
/// ```
2024-01-31 16:54:25 +00:00
/// # use cookie::Cookie;
/// # use leptos::*;
2024-01-31 16:54:25 +00:00
/// # use serde::{Deserialize, Serialize};
/// # use leptos_use::{use_cookie_with_options, UseCookieOptions};
2024-07-08 17:10:29 +01:00
/// # use codee::string::JsonSerdeCodec;
2024-01-31 16:54:25 +00:00
/// #
/// # #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
/// # pub struct Auth {
/// # pub username: String,
/// # pub token: String,
/// # }
/// #
/// # #[component]
/// # fn Demo() -> impl IntoView {
/// use_cookie_with_options::<Auth, JsonSerdeCodec>(
2024-01-31 16:54:25 +00:00
/// "auth",
/// UseCookieOptions::default()
/// .ssr_cookies_header_getter(|| {
/// #[cfg(feature = "ssr")]
/// {
2024-01-31 19:55:30 +00:00
/// Some("Somehow get the value of the cookie header as a string".to_owned())
2024-01-31 16:54:25 +00:00
/// }
/// })
/// .ssr_set_cookie(|cookie: &Cookie| {
/// #[cfg(feature = "ssr")]
/// {
/// // somehow insert the Set-Cookie header for this cookie
/// }
/// }),
/// );
/// # view! {}
/// # }
/// ```
2024-01-31 16:54:25 +00:00
///
/// ## Create Your Own Custom Codec
///
/// All you need to do is to implement the [`StringCodec`] trait together with `Default` and `Clone`.
pub fn use_cookie<T, C>(cookie_name: &str) -> (Signal<Option<T>>, WriteSignal<Option<T>>)
where
C: Encoder<T, Encoded = String> + Decoder<T, Encoded = str>,
2024-01-31 16:54:25 +00:00
T: Clone,
{
use_cookie_with_options::<T, C>(cookie_name, UseCookieOptions::default())
}
/// Version of [`use_cookie`] that takes [`UseCookieOptions`].
2024-01-31 16:54:25 +00:00
pub fn use_cookie_with_options<T, C>(
cookie_name: &str,
options: UseCookieOptions<T, <C as Encoder<T>>::Error, <C as Decoder<T>>::Error>,
2024-01-31 16:54:25 +00:00
) -> (Signal<Option<T>>, WriteSignal<Option<T>>)
where
C: Encoder<T, Encoded = String> + Decoder<T, Encoded = str>,
2024-01-31 16:54:25 +00:00
T: Clone,
{
2024-01-29 19:15:55 +00:00
let UseCookieOptions {
2024-01-31 16:54:25 +00:00
max_age,
expires,
http_only,
secure,
domain,
path,
same_site,
2024-01-29 19:15:55 +00:00
ssr_cookies_header_getter,
2024-01-31 16:54:25 +00:00
ssr_set_cookie,
default_value,
readonly,
on_error,
2024-01-29 19:15:55 +00:00
} = options;
2024-01-31 16:54:25 +00:00
let delay = if let Some(max_age) = max_age {
2024-01-31 19:55:30 +00:00
Some(max_age)
2024-01-31 16:54:25 +00:00
} else {
expires.map(|expires| expires * 1000 - now() as i64)
};
let has_expired = if let Some(delay) = delay {
delay <= 0
} else {
false
};
let (cookie, set_cookie) = create_signal(None::<T>);
let jar = store_value(CookieJar::new());
if !has_expired {
let ssr_cookies_header_getter = Rc::clone(&ssr_cookies_header_getter);
jar.update_value(|jar| {
2024-01-31 19:55:30 +00:00
if let Some(new_jar) = load_and_parse_cookie_jar(ssr_cookies_header_getter) {
*jar = new_jar;
set_cookie.set(
jar.get(cookie_name)
.and_then(|c| {
C::decode(c.value())
.map_err(|err| on_error(CodecError::Decode(err)))
2024-01-31 19:55:30 +00:00
.ok()
})
.or(default_value),
);
}
2024-01-31 16:54:25 +00:00
});
handle_expiration(delay, set_cookie);
} else {
logging::debug_warn!(
"not setting cookie '{}' because it has already expired",
cookie_name
);
}
#[cfg(not(feature = "ssr"))]
{
use crate::{
use_broadcast_channel, watch_pausable, UseBroadcastChannelReturn, WatchPausableReturn,
};
2024-07-08 17:10:29 +01:00
use codee::string::{FromToStringCodec, OptionCodec};
2024-01-31 16:54:25 +00:00
let UseBroadcastChannelReturn { message, post, .. } =
use_broadcast_channel::<Option<String>, OptionCodec<FromToStringCodec>>(&format!(
2024-01-31 16:54:25 +00:00
"leptos-use:cookies:{cookie_name}"
));
let on_cookie_change = {
let cookie_name = cookie_name.to_owned();
let ssr_cookies_header_getter = Rc::clone(&ssr_cookies_header_getter);
let on_error = Rc::clone(&on_error);
let domain = domain.clone();
let path = path.clone();
move || {
if readonly {
return;
}
let value = cookie.with_untracked(|cookie| {
2024-07-01 02:29:27 +01:00
cookie.as_ref().and_then(|cookie| {
C::encode(cookie)
.map_err(|err| on_error(CodecError::Encode(err)))
.ok()
})
2024-01-31 16:54:25 +00:00
});
if value
== jar.with_value(|jar| jar.get(&cookie_name).map(|c| c.value().to_owned()))
{
return;
}
jar.update_value(|jar| {
write_client_cookie(
&cookie_name,
&value,
jar,
max_age,
expires,
&domain,
&path,
same_site,
secure,
http_only,
Rc::clone(&ssr_cookies_header_getter),
);
});
post(&value);
}
};
let WatchPausableReturn {
pause,
resume,
stop,
..
} = watch_pausable(move || cookie.get(), {
let on_cookie_change = on_cookie_change.clone();
move |_, _, _| {
on_cookie_change();
}
});
// listen to cookie changes from the broadcast channel
create_effect({
let ssr_cookies_header_getter = Rc::clone(&ssr_cookies_header_getter);
let cookie_name = cookie_name.to_owned();
move |_| {
if let Some(message) = message.get() {
pause();
if let Some(message) = message {
match C::decode(&message) {
2024-01-31 16:54:25 +00:00
Ok(value) => {
let ssr_cookies_header_getter =
Rc::clone(&ssr_cookies_header_getter);
jar.update_value(|jar| {
update_client_cookie_jar(
&cookie_name,
&Some(message),
jar,
max_age,
expires,
&domain,
&path,
same_site,
secure,
http_only,
ssr_cookies_header_getter,
);
});
set_cookie.set(Some(value));
}
Err(err) => {
2024-07-01 02:29:27 +01:00
on_error(CodecError::Decode(err));
2024-01-31 16:54:25 +00:00
}
}
} else {
let cookie_name = cookie_name.clone();
2024-03-22 23:23:38 -06:00
let ssr_cookies_header_getter = Rc::clone(&ssr_cookies_header_getter);
2024-01-21 17:33:53 +05:30
2024-01-31 16:54:25 +00:00
jar.update_value(|jar| {
2024-03-22 23:23:38 -06:00
update_client_cookie_jar(
&cookie_name,
&None,
jar,
max_age,
expires,
&domain,
&path,
same_site,
secure,
http_only,
ssr_cookies_header_getter,
);
2024-01-31 16:54:25 +00:00
jar.force_remove(cookie_name);
});
set_cookie.set(None);
}
resume();
}
}
});
on_cleanup(move || {
stop();
on_cookie_change();
});
let _ = ssr_set_cookie;
}
#[cfg(feature = "ssr")]
{
if !readonly {
create_isomorphic_effect(move |_| {
let value = cookie
.with(|cookie| {
cookie.as_ref().map(|cookie| {
C::encode(cookie)
.map_err(|err| on_error(CodecError::Encode(err)))
.ok()
})
})
.flatten();
jar.update_value(|jar| {
write_server_cookie(
cookie_name,
value,
jar,
max_age,
expires,
domain,
path,
same_site,
secure,
http_only,
ssr_set_cookie,
)
});
2024-01-31 16:54:25 +00:00
});
}
}
(cookie.into(), set_cookie)
2024-01-21 17:33:53 +05:30
}
/// Options for [`use_cookie_with_options`].
2024-01-29 21:29:39 +00:00
#[derive(DefaultBuilder)]
pub struct UseCookieOptions<T, E, D> {
/// [`Max-Age` of the cookie](https://tools.ietf.org/html/rfc6265#section-5.2.2) in milliseconds. The returned signal will turn to `None` after the max age is reached.
2024-01-31 16:54:25 +00:00
/// Default: `None`
///
/// > The [cookie storage model specification](https://tools.ietf.org/html/rfc6265#section-5.3) states
/// > that if both `expires` and `max_age` is set, then `max_age` takes precedence,
/// > but not all clients may obey this, so if both are set, they should point to the same date and time!
///
/// > If neither of `expires` and `max_age` is set, the cookie will be session-only and removed when the user closes their browser.
#[builder(into)]
max_age: Option<i64>,
/// [Expiration date-time of the cookie](https://tools.ietf.org/html/rfc6265#section-5.2.1) as UNIX timestamp in seconds.
/// The signal will turn to `None` after the expiration date-time is reached.
/// Default: `None`
///
/// > The [cookie storage model specification](https://tools.ietf.org/html/rfc6265#section-5.3) states
/// > that if both `expires` and `max_age` is set, then `max_age` takes precedence,
/// > but not all clients may obey this, so if both are set, they should point to the same date and time!
///
/// > If neither of `expires` and `max_age` is set, the cookie will be session-only and removed when the user closes their browser.
#[builder(into)]
expires: Option<i64>,
/// Specifies the [`HttpOnly` cookie attribute](https://tools.ietf.org/html/rfc6265#section-5.2.6).
/// When `true`, the `HttpOnly` attribute is set; otherwise it is not.
/// By default, the `HttpOnly` attribute is not set.
///
/// > Be careful when setting this to `true`, as compliant clients will not allow client-side JavaScript to see the cookie in `document.cookie`.
http_only: bool,
/// Specifies the value for the [`Secure` cookie attribute](https://tools.ietf.org/html/rfc6265#section-5.2.5).
/// When `true`, the `Secure` attribute is set; otherwise it is not.
/// By default, the `Secure` attribute is not set.
///
/// > Be careful when setting this to `true`, as compliant clients will not send the cookie back to the
/// > server in the future if the browser does not have an HTTPS connection. This can lead to hydration errors.
secure: bool,
/// Specifies the value for the [`Domain` cookie attribute](https://tools.ietf.org/html/rfc6265#section-5.2.3).
/// By default, no domain is set, and most clients will consider applying the cookie only to the current domain.
#[builder(into)]
domain: Option<String>,
/// Specifies the value for the [`Path` cookie attribute](https://tools.ietf.org/html/rfc6265#section-5.2.4).
/// By default, the path is considered the ["default path"](https://tools.ietf.org/html/rfc6265#section-5.1.4).
#[builder(into)]
path: Option<String>,
/// Specifies the value for the [`SameSite` cookie attribute](https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7).
///
/// - `'Some(SameSite::Lax)'` will set the `SameSite` attribute to `Lax` for lax same-site enforcement.
/// - `'Some(SameSite::None)'` will set the `SameSite` attribute to `None` for an explicit cross-site cookie.
/// - `'Some(SameSite::Strict)'` will set the `SameSite` attribute to `Strict` for strict same-site enforcement.
/// - `None` will not set the `SameSite` attribute (default).
///
/// More information about the different enforcement levels can be found in [the specification](https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7).
#[builder(into)]
same_site: Option<SameSite>,
/// The default cookie value in case the cookie is not set.
/// Defaults to `None`.
default_value: Option<T>,
/// If `true` the returned `WriteSignal` will not affect the actual cookie.
/// Default: `false`
readonly: bool,
/// Getter function to return the string value of the cookie header.
2024-04-19 14:54:37 +01:00
/// When you use one of the features `"axum"`, `"actix"` or `"spin"` there's a valid default implementation provided.
2024-01-31 19:55:30 +00:00
ssr_cookies_header_getter: Rc<dyn Fn() -> Option<String>>,
2024-01-31 16:54:25 +00:00
/// Function to add a set cookie header to the response on the server.
2024-04-19 14:54:37 +01:00
/// When you use one of the features `"axum"`, `"actix"` or `"spin"` there's a valid default implementation provided.
2024-01-31 16:54:25 +00:00
ssr_set_cookie: Rc<dyn Fn(&Cookie)>,
/// Callback for encoding/decoding errors. Defaults to logging the error to the console.
on_error: Rc<dyn Fn(CodecError<E, D>)>,
}
impl<T, E, D> Default for UseCookieOptions<T, E, D> {
#[allow(dead_code)]
fn default() -> Self {
Self {
2024-01-31 16:54:25 +00:00
max_age: None,
expires: None,
http_only: false,
default_value: None,
readonly: false,
secure: false,
domain: None,
path: None,
same_site: None,
ssr_cookies_header_getter: Rc::new(move || {
#[cfg(feature = "ssr")]
{
#[cfg(all(feature = "actix", feature = "axum"))]
2024-06-05 20:52:13 -04:00
compile_error!("You can only enable one of features \"actix\" and \"axum\" at the same time");
2024-04-18 18:09:09 -03:00
#[cfg(all(feature = "actix", feature = "spin"))]
2024-06-05 20:52:13 -04:00
compile_error!("You can only enable one of features \"actix\" and \"spin\" at the same time");
2024-04-18 18:09:09 -03:00
#[cfg(all(feature = "axum", feature = "spin"))]
2024-06-05 20:52:13 -04:00
compile_error!("You can only enable one of features \"axum\" and \"spin\" at the same time");
2024-04-18 18:09:09 -03:00
#[cfg(feature = "actix")]
const COOKIE: http0_2::HeaderName = http0_2::header::COOKIE;
2024-04-18 18:09:09 -03:00
#[cfg(any(feature = "axum", feature = "spin"))]
const COOKIE: http1::HeaderName = http1::header::COOKIE;
#[cfg(feature = "actix")]
type HeaderValue = http0_2::HeaderValue;
#[cfg(feature = "axum")]
type HeaderValue = http1::HeaderValue;
2024-04-18 18:09:09 -03:00
#[cfg(any(feature = "axum", feature = "actix", feature = "spin"))]
let headers;
#[cfg(feature = "actix")]
{
2024-01-31 19:55:30 +00:00
headers = use_context::<actix_web::HttpRequest>()
.map(|req| req.headers().clone());
}
#[cfg(feature = "axum")]
{
2024-01-31 19:55:30 +00:00
headers = use_context::<http1::request::Parts>().map(|parts| parts.headers);
}
2024-04-18 18:09:09 -03:00
#[cfg(feature = "spin")]
{
headers = use_context::<leptos_spin::RequestParts>()
.map(|parts| parts.headers().clone());
}
2024-04-18 18:09:09 -03:00
#[cfg(all(
not(feature = "axum"),
not(feature = "actix"),
not(feature = "spin")
))]
{
2024-04-18 18:09:09 -03:00
leptos::logging::warn!("If you're using use_cookie without the feature `axum`, `actix` or `spin` enabled, you should provide the option `ssr_cookies_header_getter`");
2024-01-31 19:55:30 +00:00
None
}
#[cfg(any(feature = "axum", feature = "actix"))]
2024-04-18 18:09:09 -03:00
{
headers.map(|headers| {
headers
.get(COOKIE)
.cloned()
.unwrap_or_else(|| HeaderValue::from_static(""))
.to_str()
.unwrap_or_default()
.to_owned()
})
}
#[cfg(feature = "spin")]
{
headers.and_then(|headers| {
headers
.iter()
.find(|(key, _)| **key == COOKIE)
.and_then(|(_, value)| String::from_utf8(value.to_vec()).ok())
})
}
}
2024-01-29 20:27:40 +00:00
#[cfg(not(feature = "ssr"))]
2024-01-31 19:55:30 +00:00
None
}),
2024-01-31 16:54:25 +00:00
ssr_set_cookie: Rc::new(|cookie: &Cookie| {
#[cfg(feature = "ssr")]
{
#[cfg(feature = "actix")]
use leptos_actix::ResponseOptions;
#[cfg(feature = "axum")]
use leptos_axum::ResponseOptions;
2024-04-18 18:09:09 -03:00
#[cfg(feature = "spin")]
use leptos_spin::ResponseOptions;
2024-01-31 16:54:25 +00:00
#[cfg(feature = "actix")]
const SET_COOKIE: http0_2::HeaderName = http0_2::header::SET_COOKIE;
2024-04-18 18:09:09 -03:00
#[cfg(any(feature = "axum", feature = "spin"))]
2024-01-31 16:54:25 +00:00
const SET_COOKIE: http1::HeaderName = http1::header::SET_COOKIE;
#[cfg(feature = "actix")]
type HeaderValue = http0_2::HeaderValue;
2024-04-18 18:09:09 -03:00
#[cfg(any(feature = "axum", feature = "spin"))]
2024-01-31 16:54:25 +00:00
type HeaderValue = http1::HeaderValue;
2024-04-18 18:09:09 -03:00
#[cfg(all(
not(feature = "axum"),
not(feature = "actix"),
not(feature = "spin")
))]
2024-01-31 16:54:25 +00:00
{
let _ = cookie;
2024-04-18 18:09:09 -03:00
leptos::logging::warn!("If you're using use_cookie without the feature `axum`, `actix` or `spin` enabled, you should provide the option `ssr_set_cookie`");
2024-01-31 16:54:25 +00:00
}
#[cfg(any(feature = "axum", feature = "actix"))]
{
2024-01-31 19:55:30 +00:00
if let Some(response_options) = use_context::<ResponseOptions>() {
if let Ok(header_value) =
HeaderValue::from_str(&cookie.encoded().to_string())
{
response_options.insert_header(SET_COOKIE, header_value);
}
2024-01-31 16:54:25 +00:00
}
}
2024-04-18 18:09:09 -03:00
#[cfg(feature = "spin")]
{
if let Some(response_options) = use_context::<ResponseOptions>() {
let header_value = cookie.encoded().to_string().as_bytes().to_vec();
response_options.insert_header(SET_COOKIE.as_str(), header_value);
}
}
2024-01-31 16:54:25 +00:00
}
let _ = cookie;
}),
on_error: Rc::new(|_| {
logging::error!("cookie (de-/)serialization error");
}),
}
}
}
2024-01-29 19:15:55 +00:00
2024-01-31 19:55:30 +00:00
fn read_cookies_string(
ssr_cookies_header_getter: Rc<dyn Fn() -> Option<String>>,
) -> Option<String> {
2024-01-29 21:29:39 +00:00
let cookies;
2024-01-29 19:15:55 +00:00
#[cfg(feature = "ssr")]
2024-01-29 21:29:39 +00:00
{
cookies = ssr_cookies_header_getter();
}
2024-01-29 19:15:55 +00:00
#[cfg(not(feature = "ssr"))]
{
use wasm_bindgen::JsCast;
2024-01-29 21:29:39 +00:00
let _ = ssr_cookies_header_getter;
2024-01-29 19:15:55 +00:00
let js_value: wasm_bindgen::JsValue = leptos::document().into();
let document: web_sys::HtmlDocument = js_value.unchecked_into();
2024-01-31 19:55:30 +00:00
cookies = Some(document.cookie().unwrap_or_default());
2024-01-29 19:15:55 +00:00
}
2024-01-29 21:29:39 +00:00
cookies
2024-01-29 19:15:55 +00:00
}
2024-01-31 16:54:25 +00:00
fn handle_expiration<T>(delay: Option<i64>, set_cookie: WriteSignal<Option<T>>) {
if let Some(delay) = delay {
#[cfg(not(feature = "ssr"))]
{
use leptos::leptos_dom::helpers::TimeoutHandle;
use std::cell::Cell;
use std::cell::RefCell;
// The maximum value allowed on a timeout delay.
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#maximum_delay_value
const MAX_TIMEOUT_DELAY: i64 = 2_147_483_647;
let timeout = Rc::new(Cell::new(None::<TimeoutHandle>));
let elapsed = Rc::new(Cell::new(0));
on_cleanup({
let timeout = Rc::clone(&timeout);
move || {
if let Some(timeout) = timeout.take() {
timeout.clear();
}
}
});
let create_expiration_timeout = Rc::new(RefCell::new(None::<Box<dyn Fn()>>));
create_expiration_timeout.replace(Some(Box::new({
let timeout = Rc::clone(&timeout);
let elapsed = Rc::clone(&elapsed);
let create_expiration_timeout = Rc::clone(&create_expiration_timeout);
move || {
if let Some(timeout) = timeout.take() {
timeout.clear();
}
let time_remaining = delay - elapsed.get();
let timeout_length = time_remaining.min(MAX_TIMEOUT_DELAY);
let elapsed = Rc::clone(&elapsed);
let create_expiration_timeout = Rc::clone(&create_expiration_timeout);
timeout.set(
set_timeout_with_handle(
move || {
elapsed.set(elapsed.get() + timeout_length);
if elapsed.get() < delay {
if let Some(create_expiration_timeout) =
&*create_expiration_timeout.borrow()
{
create_expiration_timeout();
}
return;
}
set_cookie.set(None);
},
std::time::Duration::from_millis(timeout_length as u64),
)
.ok(),
);
}
})));
if let Some(create_expiration_timeout) = &*create_expiration_timeout.borrow() {
create_expiration_timeout();
};
}
#[cfg(feature = "ssr")]
{
let _ = set_cookie;
let _ = delay;
}
}
}
#[cfg(not(feature = "ssr"))]
fn write_client_cookie(
name: &str,
value: &Option<String>,
jar: &mut CookieJar,
max_age: Option<i64>,
expires: Option<i64>,
domain: &Option<String>,
path: &Option<String>,
same_site: Option<SameSite>,
secure: bool,
http_only: bool,
2024-01-31 19:55:30 +00:00
ssr_cookies_header_getter: Rc<dyn Fn() -> Option<String>>,
2024-01-31 16:54:25 +00:00
) {
use wasm_bindgen::JsCast;
update_client_cookie_jar(
name,
value,
jar,
max_age,
expires,
domain,
path,
same_site,
secure,
http_only,
ssr_cookies_header_getter,
);
let document = document();
let document: &web_sys::HtmlDocument = document.unchecked_ref();
2024-03-22 23:23:38 -06:00
document.set_cookie(&cookie_jar_to_string(jar, name)).ok();
2024-01-31 16:54:25 +00:00
}
#[cfg(not(feature = "ssr"))]
fn update_client_cookie_jar(
name: &str,
value: &Option<String>,
jar: &mut CookieJar,
max_age: Option<i64>,
expires: Option<i64>,
domain: &Option<String>,
path: &Option<String>,
same_site: Option<SameSite>,
secure: bool,
http_only: bool,
2024-01-31 19:55:30 +00:00
ssr_cookies_header_getter: Rc<dyn Fn() -> Option<String>>,
2024-01-31 16:54:25 +00:00
) {
2024-01-31 19:55:30 +00:00
if let Some(new_jar) = load_and_parse_cookie_jar(ssr_cookies_header_getter) {
*jar = new_jar;
if let Some(value) = value {
let cookie = build_cookie_from_options(
name, max_age, expires, http_only, secure, path, same_site, domain, value,
);
2024-01-31 16:54:25 +00:00
2024-01-31 19:55:30 +00:00
jar.add_original(cookie);
} else {
2024-03-22 23:23:38 -06:00
let max_age = Some(0);
let expires = Some(0);
let value = "";
let cookie = build_cookie_from_options(
name, max_age, expires, http_only, secure, path, same_site, domain, value,
);
jar.add(cookie);
2024-01-31 19:55:30 +00:00
}
2024-01-31 16:54:25 +00:00
}
}
#[cfg(not(feature = "ssr"))]
2024-03-22 23:23:38 -06:00
fn cookie_jar_to_string(jar: &CookieJar, name: &str) -> String {
match jar.get(name) {
Some(c) => c.encoded().to_string(),
None => "".to_string(),
}
2024-01-31 16:54:25 +00:00
}
fn build_cookie_from_options(
name: &str,
max_age: Option<i64>,
expires: Option<i64>,
http_only: bool,
secure: bool,
path: &Option<String>,
same_site: Option<SameSite>,
domain: &Option<String>,
value: &str,
) -> Cookie<'static> {
let mut cookie = Cookie::build((name, value));
if let Some(max_age) = max_age {
cookie = cookie.max_age(Duration::milliseconds(max_age));
}
if let Some(expires) = expires {
match OffsetDateTime::from_unix_timestamp(expires) {
Ok(expires) => {
cookie = cookie.expires(expires);
}
Err(err) => {
logging::debug_warn!("failed to set cookie expiration: {:?}", err);
}
}
}
if http_only {
cookie = cookie.http_only(true);
}
if secure {
cookie = cookie.secure(true);
}
if let Some(domain) = domain {
cookie = cookie.domain(domain);
}
if let Some(path) = path {
cookie = cookie.path(path);
}
if let Some(same_site) = same_site {
cookie = cookie.same_site(same_site);
}
let cookie: Cookie = cookie.into();
cookie.into_owned()
}
#[cfg(feature = "ssr")]
fn write_server_cookie(
name: &str,
value: Option<String>,
jar: &mut CookieJar,
max_age: Option<i64>,
expires: Option<i64>,
domain: Option<String>,
path: Option<String>,
same_site: Option<SameSite>,
secure: bool,
http_only: bool,
ssr_set_cookie: Rc<dyn Fn(&Cookie)>,
) {
if let Some(value) = value {
let cookie: Cookie = build_cookie_from_options(
name, max_age, expires, http_only, secure, &path, same_site, &domain, &value,
2024-07-01 02:29:27 +01:00
);
2024-01-31 16:54:25 +00:00
jar.add(cookie.into_owned());
} else {
jar.remove(name.to_owned());
}
for cookie in jar.delta() {
ssr_set_cookie(cookie);
}
}
2024-01-31 19:55:30 +00:00
fn load_and_parse_cookie_jar(
ssr_cookies_header_getter: Rc<dyn Fn() -> Option<String>>,
) -> Option<CookieJar> {
read_cookies_string(ssr_cookies_header_getter).map(|cookies| {
let mut jar = CookieJar::new();
for cookie in Cookie::split_parse_encoded(cookies).flatten() {
jar.add_original(cookie);
}
2024-01-31 16:54:25 +00:00
2024-01-31 19:55:30 +00:00
jar
})
2024-01-31 16:54:25 +00:00
}