#![allow(clippy::too_many_arguments)]
use crate::core::now;
use crate::utils::get_header;
use codee::{CodecError, Decoder, Encoder};
use cookie::time::{Duration, OffsetDateTime};
pub use cookie::SameSite;
use cookie::{Cookie, CookieJar};
use default_struct_builder::DefaultBuilder;
use leptos::{
logging::{debug_warn, error},
prelude::*,
};
use std::sync::Arc;
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>,
T: Clone + Send + Sync + 'static,
{
use_cookie_with_options::<T, C>(cookie_name, UseCookieOptions::default())
}
pub fn use_cookie_with_options<T, C>(
cookie_name: &str,
options: UseCookieOptions<T, <C as Encoder<T>>::Error, <C as Decoder<T>>::Error>,
) -> (Signal<Option<T>>, WriteSignal<Option<T>>)
where
C: Encoder<T, Encoded = String> + Decoder<T, Encoded = str>,
T: Clone + Send + Sync + 'static,
{
let UseCookieOptions {
max_age,
expires,
http_only,
secure,
domain,
path,
same_site,
ssr_cookies_header_getter,
ssr_set_cookie,
default_value,
readonly,
on_error,
} = options;
let delay = if let Some(max_age) = max_age {
Some(max_age)
} 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) = signal(None::<T>);
let jar = StoredValue::new(CookieJar::new());
if !has_expired {
let ssr_cookies_header_getter = Arc::clone(&ssr_cookies_header_getter);
let new_cookie = jar.try_update_value(|jar| {
*jar = load_and_parse_cookie_jar(ssr_cookies_header_getter)?;
jar.get(cookie_name)
.and_then(|c| {
C::decode(c.value())
.map_err(|err| on_error(CodecError::Decode(err)))
.ok()
})
.or(default_value)
});
set_cookie.set(new_cookie.flatten());
handle_expiration(delay, set_cookie);
} else {
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,
};
use codee::string::{FromToStringCodec, OptionCodec};
let UseBroadcastChannelReturn { message, post, .. } =
use_broadcast_channel::<Option<String>, OptionCodec<FromToStringCodec>>(&format!(
"leptos-use:cookies:{cookie_name}"
));
let on_cookie_change = {
let cookie_name = cookie_name.to_owned();
let ssr_cookies_header_getter = Arc::clone(&ssr_cookies_header_getter);
let on_error = Arc::clone(&on_error);
let domain = domain.clone();
let path = path.clone();
move || {
if readonly {
return;
}
let value = cookie.try_with_untracked(|cookie| {
cookie.as_ref().and_then(|cookie| {
C::encode(cookie)
.map_err(|err| on_error(CodecError::Encode(err)))
.ok()
})
});
if let Some(value) = value {
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,
Arc::clone(&ssr_cookies_header_getter),
);
});
post(&value);
}
}
};
let WatchPausableReturn {
pause,
resume,
stop,
..
} = watch_pausable(move || cookie.track(), {
let on_cookie_change = on_cookie_change.clone();
move |_, _, _| {
on_cookie_change();
}
});
Effect::new({
let ssr_cookies_header_getter = Arc::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) {
Ok(value) => {
let ssr_cookies_header_getter =
Arc::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) => {
on_error(CodecError::Decode(err));
}
}
} else {
let cookie_name = cookie_name.clone();
let ssr_cookies_header_getter = Arc::clone(&ssr_cookies_header_getter);
jar.update_value(|jar| {
update_client_cookie_jar(
&cookie_name,
&None,
jar,
max_age,
expires,
&domain,
&path,
same_site,
secure,
http_only,
ssr_cookies_header_getter,
);
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 {
Effect::new_isomorphic({
let cookie_name = cookie_name.to_owned();
let ssr_set_cookie = Arc::clone(&ssr_set_cookie);
move |previous_effect_value: Option<()>| {
let domain = domain.clone();
let path = path.clone();
if let Some(value) = cookie.try_with(|cookie| {
cookie.as_ref().map(|cookie| {
C::encode(cookie)
.map_err(|err| on_error(CodecError::Encode(err)))
.ok()
})
}) {
if previous_effect_value.is_some() {
jar.update_value({
let domain = domain.clone();
let path = path.clone();
let ssr_set_cookie = Arc::clone(&ssr_set_cookie);
|jar| {
write_server_cookie(
&cookie_name,
value.flatten(),
jar,
max_age,
expires,
domain,
path,
same_site,
secure,
http_only,
ssr_set_cookie,
)
}
});
}
}
()
}
});
}
}
(cookie.into(), set_cookie)
}
#[derive(DefaultBuilder)]
pub struct UseCookieOptions<T, E, D> {
#[builder(into)]
max_age: Option<i64>,
#[builder(into)]
expires: Option<i64>,
http_only: bool,
secure: bool,
#[builder(into)]
domain: Option<String>,
#[builder(into)]
path: Option<String>,
#[builder(into)]
same_site: Option<SameSite>,
default_value: Option<T>,
readonly: bool,
ssr_cookies_header_getter: Arc<dyn Fn() -> Option<String> + Send + Sync>,
ssr_set_cookie: Arc<dyn Fn(&Cookie) + Send + Sync>,
on_error: Arc<dyn Fn(CodecError<E, D>) + Send + Sync>,
}
impl<T, E, D> Default for UseCookieOptions<T, E, D> {
#[allow(dead_code)]
fn default() -> Self {
Self {
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: Arc::new(move || {
get_header!(COOKIE, use_cookie, ssr_cookies_header_getter)
}),
ssr_set_cookie: Arc::new(|cookie: &Cookie| {
#[cfg(feature = "ssr")]
{
#[cfg(feature = "actix")]
use leptos_actix::ResponseOptions;
#[cfg(feature = "axum")]
use leptos_axum::ResponseOptions;
#[cfg(feature = "spin")]
use leptos_spin::ResponseOptions;
#[cfg(feature = "actix")]
const SET_COOKIE: http0_2::HeaderName = http0_2::header::SET_COOKIE;
#[cfg(any(feature = "axum", feature = "spin"))]
const SET_COOKIE: http1::HeaderName = http1::header::SET_COOKIE;
#[cfg(feature = "actix")]
type HeaderValue = http0_2::HeaderValue;
#[cfg(any(feature = "axum", feature = "spin"))]
type HeaderValue = http1::HeaderValue;
#[cfg(all(
not(feature = "axum"),
not(feature = "actix"),
not(feature = "spin")
))]
{
use leptos::logging::warn;
let _ = cookie;
warn!("If you're using use_cookie without the feature `axum`, `actix` or `spin` enabled, you should provide the option `ssr_set_cookie`");
}
#[cfg(any(feature = "axum", feature = "actix"))]
{
if let Some(response_options) = use_context::<ResponseOptions>() {
if let Ok(header_value) =
HeaderValue::from_str(&cookie.encoded().to_string())
{
response_options.append_header(SET_COOKIE, header_value);
}
}
}
#[cfg(feature = "spin")]
{
if let Some(response_options) = use_context::<ResponseOptions>() {
let header_value = cookie.encoded().to_string().as_bytes().to_vec();
response_options.append_header(SET_COOKIE.as_str(), &header_value);
}
}
}
let _ = cookie;
}),
on_error: Arc::new(|_| {
error!("cookie (de-/)serialization error");
}),
}
}
}
fn read_cookies_string(
ssr_cookies_header_getter: Arc<dyn Fn() -> Option<String> + Send + Sync>,
) -> Option<String> {
let cookies;
#[cfg(feature = "ssr")]
{
cookies = ssr_cookies_header_getter();
}
#[cfg(not(feature = "ssr"))]
{
use wasm_bindgen::JsCast;
let _ = ssr_cookies_header_getter;
let js_value: wasm_bindgen::JsValue = document().into();
let document: web_sys::HtmlDocument = js_value.unchecked_into();
cookies = Some(document.cookie().unwrap_or_default());
}
cookies
}
fn handle_expiration<T>(delay: Option<i64>, set_cookie: WriteSignal<Option<T>>)
where
T: Send + Sync + 'static,
{
if let Some(delay) = delay {
#[cfg(not(feature = "ssr"))]
{
use leptos::leptos_dom::helpers::TimeoutHandle;
use std::sync::{atomic::AtomicI32, Mutex};
const MAX_TIMEOUT_DELAY: i64 = 2_147_483_647;
let timeout = Arc::new(Mutex::new(None::<TimeoutHandle>));
let elapsed = Arc::new(AtomicI32::new(0));
on_cleanup({
let timeout = Arc::clone(&timeout);
move || {
if let Some(timeout) = timeout.lock().unwrap().take() {
timeout.clear();
}
}
});
let create_expiration_timeout =
Arc::new(Mutex::new(None::<Box<dyn Fn() + Send + Sync>>));
*create_expiration_timeout.lock().unwrap() = Some(Box::new({
let timeout = Arc::clone(&timeout);
let elapsed = Arc::clone(&elapsed);
let create_expiration_timeout = Arc::clone(&create_expiration_timeout);
move || {
if let Some(timeout) = timeout.lock().unwrap().take() {
timeout.clear();
}
let time_remaining =
delay - elapsed.load(std::sync::atomic::Ordering::Relaxed) as i64;
let timeout_length = time_remaining.min(MAX_TIMEOUT_DELAY);
let elapsed = Arc::clone(&elapsed);
let create_expiration_timeout = Arc::clone(&create_expiration_timeout);
*timeout.lock().unwrap() = set_timeout_with_handle(
move || {
let elapsed = elapsed.fetch_add(
timeout_length as i32,
std::sync::atomic::Ordering::Relaxed,
) as i64
+ timeout_length;
if elapsed < delay {
if let Some(create_expiration_timeout) =
create_expiration_timeout.lock().unwrap().as_ref()
{
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.lock().unwrap().as_ref()
{
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,
ssr_cookies_header_getter: Arc<dyn Fn() -> Option<String> + Send + Sync>,
) {
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();
document.set_cookie(&cookie_jar_to_string(jar, name)).ok();
}
#[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,
ssr_cookies_header_getter: Arc<dyn Fn() -> Option<String> + Send + Sync>,
) {
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,
);
jar.add_original(cookie);
} else {
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);
}
}
}
#[cfg(not(feature = "ssr"))]
fn cookie_jar_to_string(jar: &CookieJar, name: &str) -> String {
match jar.get(name) {
Some(c) => c.encoded().to_string(),
None => "".to_string(),
}
}
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) => {
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: Arc<dyn Fn(&Cookie) + Send + Sync>,
) {
if let Some(value) = value {
let cookie: Cookie = build_cookie_from_options(
name, max_age, expires, http_only, secure, &path, same_site, &domain, &value,
);
jar.add(cookie.into_owned());
} else {
jar.remove(name.to_owned());
}
for cookie in jar.delta() {
ssr_set_cookie(cookie);
}
}
fn load_and_parse_cookie_jar(
ssr_cookies_header_getter: Arc<dyn Fn() -> Option<String> + Send + Sync>,
) -> 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);
}
jar
})
}