leptos_use/use_cookie.rs
1#![allow(clippy::too_many_arguments)]
2
3use crate::core::now;
4use crate::utils::get_header;
5use codee::{CodecError, Decoder, Encoder};
6use cookie::time::{Duration, OffsetDateTime};
7pub use cookie::SameSite;
8use cookie::{Cookie, CookieJar};
9use default_struct_builder::DefaultBuilder;
10use leptos::{
11 logging::{debug_warn, error},
12 prelude::*,
13};
14use std::sync::Arc;
15
16/// SSR-friendly and reactive cookie access.
17///
18/// You can use this function multiple times for the same cookie and their signals will synchronize
19/// (even across windows/tabs). But there is no way to listen to changes to `document.cookie` directly so in case
20/// something outside of this function changes the cookie, the signal will **not** be updated.
21///
22/// When the options `max_age` or `expire` is given then the returned signal will
23/// automatically turn to `None` after that time.
24///
25/// ## Demo
26///
27/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_cookie)
28///
29/// ## Usage
30///
31/// The example below creates a cookie called `counter`. If the cookie doesn't exist, it is initially set to a random value.
32/// Whenever we update the `counter` variable, the cookie will be updated accordingly.
33///
34/// ```
35/// # use leptos::prelude::*;
36/// # use leptos_use::use_cookie;
37/// # use codee::string::FromToStringCodec;
38/// # use rand::prelude::*;
39///
40/// #
41/// # #[component]
42/// # fn Demo() -> impl IntoView {
43/// let (counter, set_counter) = use_cookie::<u32, FromToStringCodec>("counter");
44///
45/// let reset = move || set_counter.set(Some(random()));
46///
47/// if counter.get().is_none() {
48/// reset();
49/// }
50///
51/// let increase = move || {
52/// set_counter.set(counter.get().map(|c| c + 1));
53/// };
54///
55/// view! {
56/// <p>Counter: {move || counter.get().map(|c| c.to_string()).unwrap_or("—".to_string())}</p>
57/// <button on:click=move |_| reset()>Reset</button>
58/// <button on:click=move |_| increase()>+</button>
59/// }
60/// # }
61/// ```
62///
63/// Values are (en)decoded via the given codec. You can use any of the string codecs or a
64/// binary codec wrapped in `Base64`.
65///
66/// > Please check [the codec chapter](https://leptos-use.rs/codecs.html) to see what codecs are
67/// > available and what feature flags they require.
68///
69/// ## Cookie attributes
70///
71/// As part of the options when you use `use_cookie_with_options` you can specify cookie attributes.
72///
73/// ```
74/// # use cookie::SameSite;
75/// # use leptos::prelude::*;
76/// # use leptos_use::{use_cookie_with_options, UseCookieOptions};
77/// # use codee::string::FromToStringCodec;
78/// #
79/// # #[component]
80/// # fn Demo() -> impl IntoView {
81/// let (cookie, set_cookie) = use_cookie_with_options::<bool, FromToStringCodec>(
82/// "user_info",
83/// UseCookieOptions::default()
84/// .max_age(3600_000) // one hour
85/// .same_site(SameSite::Lax)
86/// );
87/// #
88/// # view! {}
89/// # }
90/// ```
91///
92/// ## Server-Side Rendering
93///
94/// This works equally well on the server or the client.
95/// On the server this function reads the cookie from the HTTP request header and writes it back into
96/// the HTTP response header according to options (if provided).
97/// The returned `WriteSignal` may not affect the cookie headers on the server! It will try and write
98/// the headers buy if this happens after the headers have already been streamed to the client then
99/// this will have no effect.
100///
101/// > If you're using `axum` you have to enable the `"axum"` feature in your Cargo.toml.
102/// > In case it's `actix-web` enable the feature `"actix"`, for `spin` enable `"spin"`.
103///
104/// ### Bring your own header
105///
106/// In case you're neither using Axum, Actix nor Spin, or the default implementation is not to your liking,
107/// you can provide your own way of reading and writing the cookie header value.
108///
109/// ```
110/// # use cookie::Cookie;
111/// # use leptos::prelude::*;
112/// # use serde::{Deserialize, Serialize};
113/// # use leptos_use::{use_cookie_with_options, UseCookieOptions};
114/// # use codee::string::JsonSerdeCodec;
115/// #
116/// # #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
117/// # pub struct Auth {
118/// # pub username: String,
119/// # pub token: String,
120/// # }
121/// #
122/// # #[component]
123/// # fn Demo() -> impl IntoView {
124/// use_cookie_with_options::<Auth, JsonSerdeCodec>(
125/// "auth",
126/// UseCookieOptions::default()
127/// .ssr_cookies_header_getter(|| {
128/// #[cfg(feature = "ssr")]
129/// {
130/// Some("Somehow get the value of the cookie header as a string".to_owned())
131/// }
132/// #[cfg(not(feature = "ssr"))]
133/// None
134/// })
135/// .ssr_set_cookie(|cookie: &Cookie| {
136/// #[cfg(feature = "ssr")]
137/// {
138/// // somehow insert the Set-Cookie header for this cookie
139/// }
140/// }),
141/// );
142/// # view! {}
143/// # }
144/// ```
145pub fn use_cookie<T, C>(cookie_name: &str) -> (Signal<Option<T>>, WriteSignal<Option<T>>)
146where
147 C: Encoder<T, Encoded = String> + Decoder<T, Encoded = str>,
148 T: Clone + Send + Sync + 'static,
149{
150 use_cookie_with_options::<T, C>(cookie_name, UseCookieOptions::default())
151}
152
153/// Version of [`use_cookie`] that takes [`UseCookieOptions`].
154pub fn use_cookie_with_options<T, C>(
155 cookie_name: &str,
156 options: UseCookieOptions<T, <C as Encoder<T>>::Error, <C as Decoder<T>>::Error>,
157) -> (Signal<Option<T>>, WriteSignal<Option<T>>)
158where
159 C: Encoder<T, Encoded = String> + Decoder<T, Encoded = str>,
160 T: Clone + Send + Sync + 'static,
161{
162 let UseCookieOptions {
163 max_age,
164 expires,
165 http_only,
166 secure,
167 domain,
168 path,
169 same_site,
170 ssr_cookies_header_getter,
171 ssr_set_cookie,
172 default_value,
173 readonly,
174 on_error,
175 } = options;
176
177 let delay = if let Some(max_age) = max_age {
178 Some(max_age)
179 } else {
180 expires.map(|expires| expires * 1000 - now() as i64)
181 };
182
183 let has_expired = if let Some(delay) = delay {
184 delay <= 0
185 } else {
186 false
187 };
188
189 let (cookie, set_cookie) = signal(None::<T>);
190
191 let jar = StoredValue::new(CookieJar::new());
192
193 if !has_expired {
194 let ssr_cookies_header_getter = Arc::clone(&ssr_cookies_header_getter);
195
196 let new_cookie = jar.try_update_value(|jar| {
197 *jar = load_and_parse_cookie_jar(ssr_cookies_header_getter)?;
198 jar.get(cookie_name)
199 .and_then(|c| {
200 C::decode(c.value())
201 .map_err(|err| on_error(CodecError::Decode(err)))
202 .ok()
203 })
204 .or(default_value)
205 });
206
207 set_cookie.set(new_cookie.flatten());
208
209 handle_expiration(delay, set_cookie);
210 } else {
211 debug_warn!(
212 "not setting cookie '{}' because it has already expired",
213 cookie_name
214 );
215 }
216
217 #[cfg(not(feature = "ssr"))]
218 {
219 use crate::{
220 use_broadcast_channel, watch_pausable, UseBroadcastChannelReturn, WatchPausableReturn,
221 };
222 use codee::string::{FromToStringCodec, OptionCodec};
223
224 let UseBroadcastChannelReturn { message, post, .. } =
225 use_broadcast_channel::<Option<String>, OptionCodec<FromToStringCodec>>(&format!(
226 "leptos-use:cookies:{cookie_name}"
227 ));
228
229 let on_cookie_change = {
230 let cookie_name = cookie_name.to_owned();
231 let ssr_cookies_header_getter = Arc::clone(&ssr_cookies_header_getter);
232 let on_error = Arc::clone(&on_error);
233 let domain = domain.clone();
234 let path = path.clone();
235
236 move || {
237 if readonly {
238 return;
239 }
240
241 let value = cookie.try_with_untracked(|cookie| {
242 cookie.as_ref().and_then(|cookie| {
243 C::encode(cookie)
244 .map_err(|err| on_error(CodecError::Encode(err)))
245 .ok()
246 })
247 });
248
249 if let Some(value) = value {
250 if value
251 == jar.with_value(|jar| jar.get(&cookie_name).map(|c| c.value().to_owned()))
252 {
253 return;
254 }
255
256 jar.update_value(|jar| {
257 write_client_cookie(
258 &cookie_name,
259 &value,
260 jar,
261 max_age,
262 expires,
263 &domain,
264 &path,
265 same_site,
266 secure,
267 http_only,
268 Arc::clone(&ssr_cookies_header_getter),
269 );
270 });
271
272 post(&value);
273 }
274 }
275 };
276
277 let WatchPausableReturn {
278 pause,
279 resume,
280 stop,
281 ..
282 } = watch_pausable(move || cookie.track(), {
283 let on_cookie_change = on_cookie_change.clone();
284
285 move |_, _, _| {
286 on_cookie_change();
287 }
288 });
289
290 // listen to cookie changes from the broadcast channel
291 Effect::new({
292 let ssr_cookies_header_getter = Arc::clone(&ssr_cookies_header_getter);
293 let cookie_name = cookie_name.to_owned();
294
295 move |_| {
296 if let Some(message) = message.get() {
297 pause();
298
299 if let Some(message) = message {
300 match C::decode(&message) {
301 Ok(value) => {
302 let ssr_cookies_header_getter =
303 Arc::clone(&ssr_cookies_header_getter);
304
305 jar.update_value(|jar| {
306 update_client_cookie_jar(
307 &cookie_name,
308 &Some(message),
309 jar,
310 max_age,
311 expires,
312 &domain,
313 &path,
314 same_site,
315 secure,
316 http_only,
317 ssr_cookies_header_getter,
318 );
319 });
320
321 set_cookie.set(Some(value));
322 }
323 Err(err) => {
324 on_error(CodecError::Decode(err));
325 }
326 }
327 } else {
328 let cookie_name = cookie_name.clone();
329 let ssr_cookies_header_getter = Arc::clone(&ssr_cookies_header_getter);
330
331 jar.update_value(|jar| {
332 update_client_cookie_jar(
333 &cookie_name,
334 &None,
335 jar,
336 max_age,
337 expires,
338 &domain,
339 &path,
340 same_site,
341 secure,
342 http_only,
343 ssr_cookies_header_getter,
344 );
345 jar.force_remove(cookie_name);
346 });
347
348 set_cookie.set(None);
349 }
350
351 resume();
352 }
353 }
354 });
355
356 on_cleanup(move || {
357 stop();
358 on_cookie_change();
359 });
360
361 let _ = ssr_set_cookie;
362 }
363
364 #[cfg(feature = "ssr")]
365 {
366 if !readonly {
367 Effect::new_isomorphic({
368 let cookie_name = cookie_name.to_owned();
369 let ssr_set_cookie = Arc::clone(&ssr_set_cookie);
370
371 move |previous_effect_value: Option<()>| {
372 let domain = domain.clone();
373 let path = path.clone();
374
375 if let Some(value) = cookie.try_with(|cookie| {
376 cookie.as_ref().map(|cookie| {
377 C::encode(cookie)
378 .map_err(|err| on_error(CodecError::Encode(err)))
379 .ok()
380 })
381 }) {
382 if previous_effect_value.is_some() {
383 jar.update_value({
384 let domain = domain.clone();
385 let path = path.clone();
386 let ssr_set_cookie = Arc::clone(&ssr_set_cookie);
387
388 |jar| {
389 write_server_cookie(
390 &cookie_name,
391 value.flatten(),
392 jar,
393 max_age,
394 expires,
395 domain,
396 path,
397 same_site,
398 secure,
399 http_only,
400 ssr_set_cookie,
401 )
402 }
403 });
404 }
405 }
406
407 ()
408 }
409 });
410 }
411 }
412
413 (cookie.into(), set_cookie)
414}
415
416/// Options for [`use_cookie_with_options`].
417#[derive(DefaultBuilder)]
418pub struct UseCookieOptions<T, E, D> {
419 /// [`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.
420 /// Default: `None`
421 ///
422 /// > The [cookie storage model specification](https://tools.ietf.org/html/rfc6265#section-5.3) states
423 /// > that if both `expires` and `max_age` is set, then `max_age` takes precedence,
424 /// > but not all clients may obey this, so if both are set, they should point to the same date and time!
425 ///
426 /// > If neither of `expires` and `max_age` is set, the cookie will be session-only and removed when the user closes their browser.
427 #[builder(into)]
428 max_age: Option<i64>,
429
430 /// [Expiration date-time of the cookie](https://tools.ietf.org/html/rfc6265#section-5.2.1) as UNIX timestamp in seconds.
431 /// The signal will turn to `None` after the expiration date-time is reached.
432 /// Default: `None`
433 ///
434 /// > The [cookie storage model specification](https://tools.ietf.org/html/rfc6265#section-5.3) states
435 /// > that if both `expires` and `max_age` is set, then `max_age` takes precedence,
436 /// > but not all clients may obey this, so if both are set, they should point to the same date and time!
437 ///
438 /// > If neither of `expires` and `max_age` is set, the cookie will be session-only and removed when the user closes their browser.
439 #[builder(into)]
440 expires: Option<i64>,
441
442 /// Specifies the [`HttpOnly` cookie attribute](https://tools.ietf.org/html/rfc6265#section-5.2.6).
443 /// When `true`, the `HttpOnly` attribute is set; otherwise it is not.
444 /// By default, the `HttpOnly` attribute is not set.
445 ///
446 /// > Be careful when setting this to `true`, as compliant clients will not allow client-side JavaScript to see the cookie in `document.cookie`.
447 http_only: bool,
448
449 /// Specifies the value for the [`Secure` cookie attribute](https://tools.ietf.org/html/rfc6265#section-5.2.5).
450 /// When `true`, the `Secure` attribute is set; otherwise it is not.
451 /// By default, the `Secure` attribute is not set.
452 ///
453 /// > Be careful when setting this to `true`, as compliant clients will not send the cookie back to the
454 /// > server in the future if the browser does not have an HTTPS connection. This can lead to hydration errors.
455 secure: bool,
456
457 /// Specifies the value for the [`Domain` cookie attribute](https://tools.ietf.org/html/rfc6265#section-5.2.3).
458 /// By default, no domain is set, and most clients will consider applying the cookie only to the current domain.
459 #[builder(into)]
460 domain: Option<String>,
461
462 /// Specifies the value for the [`Path` cookie attribute](https://tools.ietf.org/html/rfc6265#section-5.2.4).
463 /// By default, the path is considered the ["default path"](https://tools.ietf.org/html/rfc6265#section-5.1.4).
464 #[builder(into)]
465 path: Option<String>,
466
467 /// Specifies the value for the [`SameSite` cookie attribute](https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7).
468 ///
469 /// - `'Some(SameSite::Lax)'` will set the `SameSite` attribute to `Lax` for lax same-site enforcement.
470 /// - `'Some(SameSite::None)'` will set the `SameSite` attribute to `None` for an explicit cross-site cookie.
471 /// - `'Some(SameSite::Strict)'` will set the `SameSite` attribute to `Strict` for strict same-site enforcement.
472 /// - `None` will not set the `SameSite` attribute (default).
473 ///
474 /// 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).
475 #[builder(into)]
476 same_site: Option<SameSite>,
477
478 /// The default cookie value in case the cookie is not set.
479 /// Defaults to `None`.
480 default_value: Option<T>,
481
482 /// If `true` the returned `WriteSignal` will not affect the actual cookie.
483 /// Default: `false`
484 readonly: bool,
485
486 /// Getter function to return the string value of the cookie header.
487 /// When you use one of the features `"axum"`, `"actix"` or `"spin"` there's a valid default implementation provided.
488 ssr_cookies_header_getter: Arc<dyn Fn() -> Option<String> + Send + Sync>,
489
490 /// Function to add a set cookie header to the response on the server.
491 /// When you use one of the features `"axum"`, `"actix"` or `"spin"` there's a valid default implementation provided.
492 ssr_set_cookie: Arc<dyn Fn(&Cookie) + Send + Sync>,
493
494 /// Callback for encoding/decoding errors. Defaults to logging the error to the console.
495 on_error: Arc<dyn Fn(CodecError<E, D>) + Send + Sync>,
496}
497
498impl<T, E, D> Default for UseCookieOptions<T, E, D> {
499 #[allow(dead_code)]
500 fn default() -> Self {
501 Self {
502 max_age: None,
503 expires: None,
504 http_only: false,
505 default_value: None,
506 readonly: false,
507 secure: false,
508 domain: None,
509 path: None,
510 same_site: None,
511 ssr_cookies_header_getter: Arc::new(move || {
512 get_header!(COOKIE, use_cookie, ssr_cookies_header_getter)
513 }),
514 ssr_set_cookie: Arc::new(|cookie: &Cookie| {
515 #[cfg(feature = "ssr")]
516 {
517 #[cfg(feature = "actix")]
518 use leptos_actix::ResponseOptions;
519 #[cfg(feature = "axum")]
520 use leptos_axum::ResponseOptions;
521 #[cfg(feature = "spin")]
522 use leptos_spin::ResponseOptions;
523
524 #[cfg(feature = "actix")]
525 const SET_COOKIE: http0_2::HeaderName = http0_2::header::SET_COOKIE;
526 #[cfg(any(feature = "axum", feature = "spin"))]
527 const SET_COOKIE: http1::HeaderName = http1::header::SET_COOKIE;
528
529 #[cfg(feature = "actix")]
530 type HeaderValue = http0_2::HeaderValue;
531 #[cfg(any(feature = "axum", feature = "spin"))]
532 type HeaderValue = http1::HeaderValue;
533
534 #[cfg(all(
535 not(feature = "axum"),
536 not(feature = "actix"),
537 not(feature = "spin")
538 ))]
539 {
540 use leptos::logging::warn;
541 let _ = cookie;
542 warn!("If you're using use_cookie without the feature `axum`, `actix` or `spin` enabled, you should provide the option `ssr_set_cookie`");
543 }
544
545 #[cfg(any(feature = "axum", feature = "actix"))]
546 {
547 if let Some(response_options) = use_context::<ResponseOptions>() {
548 if let Ok(header_value) =
549 HeaderValue::from_str(&cookie.encoded().to_string())
550 {
551 response_options.append_header(SET_COOKIE, header_value);
552 }
553 }
554 }
555 #[cfg(feature = "spin")]
556 {
557 if let Some(response_options) = use_context::<ResponseOptions>() {
558 let header_value = cookie.encoded().to_string().as_bytes().to_vec();
559 response_options.append_header(SET_COOKIE.as_str(), &header_value);
560 }
561 }
562 }
563
564 let _ = cookie;
565 }),
566 on_error: Arc::new(|_| {
567 error!("cookie (de-/)serialization error");
568 }),
569 }
570 }
571}
572
573fn read_cookies_string(
574 ssr_cookies_header_getter: Arc<dyn Fn() -> Option<String> + Send + Sync>,
575) -> Option<String> {
576 let cookies;
577
578 #[cfg(feature = "ssr")]
579 {
580 cookies = ssr_cookies_header_getter();
581 }
582
583 #[cfg(not(feature = "ssr"))]
584 {
585 use wasm_bindgen::JsCast;
586
587 let _ = ssr_cookies_header_getter;
588
589 let js_value: wasm_bindgen::JsValue = document().into();
590 let document: web_sys::HtmlDocument = js_value.unchecked_into();
591 cookies = Some(document.cookie().unwrap_or_default());
592 }
593
594 cookies
595}
596
597fn handle_expiration<T>(delay: Option<i64>, set_cookie: WriteSignal<Option<T>>)
598where
599 T: Send + Sync + 'static,
600{
601 if let Some(delay) = delay {
602 #[cfg(not(feature = "ssr"))]
603 {
604 use leptos::leptos_dom::helpers::TimeoutHandle;
605 use std::sync::{atomic::AtomicI32, Mutex};
606
607 // The maximum value allowed on a timeout delay.
608 // Reference: https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#maximum_delay_value
609 const MAX_TIMEOUT_DELAY: i64 = 2_147_483_647;
610
611 let timeout = Arc::new(Mutex::new(None::<TimeoutHandle>));
612 let elapsed = Arc::new(AtomicI32::new(0));
613
614 on_cleanup({
615 let timeout = Arc::clone(&timeout);
616 move || {
617 if let Some(timeout) = timeout.lock().unwrap().take() {
618 timeout.clear();
619 }
620 }
621 });
622
623 let create_expiration_timeout =
624 Arc::new(Mutex::new(None::<Box<dyn Fn() + Send + Sync>>));
625
626 *create_expiration_timeout.lock().unwrap() = Some(Box::new({
627 let timeout = Arc::clone(&timeout);
628 let elapsed = Arc::clone(&elapsed);
629 let create_expiration_timeout = Arc::clone(&create_expiration_timeout);
630
631 move || {
632 if let Some(timeout) = timeout.lock().unwrap().take() {
633 timeout.clear();
634 }
635
636 let time_remaining =
637 delay - elapsed.load(std::sync::atomic::Ordering::Relaxed) as i64;
638 let timeout_length = time_remaining.min(MAX_TIMEOUT_DELAY);
639
640 let elapsed = Arc::clone(&elapsed);
641 let create_expiration_timeout = Arc::clone(&create_expiration_timeout);
642
643 *timeout.lock().unwrap() = set_timeout_with_handle(
644 move || {
645 let elapsed = elapsed.fetch_add(
646 timeout_length as i32,
647 std::sync::atomic::Ordering::Relaxed,
648 ) as i64
649 + timeout_length;
650
651 if elapsed < delay {
652 if let Some(create_expiration_timeout) =
653 create_expiration_timeout.lock().unwrap().as_ref()
654 {
655 create_expiration_timeout();
656 }
657 return;
658 }
659
660 set_cookie.set(None);
661 },
662 std::time::Duration::from_millis(timeout_length as u64),
663 )
664 .ok();
665 }
666 }));
667
668 if let Some(create_expiration_timeout) =
669 create_expiration_timeout.lock().unwrap().as_ref()
670 {
671 create_expiration_timeout();
672 };
673 }
674
675 #[cfg(feature = "ssr")]
676 {
677 let _ = set_cookie;
678 let _ = delay;
679 }
680 }
681}
682
683#[cfg(not(feature = "ssr"))]
684fn write_client_cookie(
685 name: &str,
686 value: &Option<String>,
687 jar: &mut CookieJar,
688 max_age: Option<i64>,
689 expires: Option<i64>,
690 domain: &Option<String>,
691 path: &Option<String>,
692 same_site: Option<SameSite>,
693 secure: bool,
694 http_only: bool,
695 ssr_cookies_header_getter: Arc<dyn Fn() -> Option<String> + Send + Sync>,
696) {
697 use wasm_bindgen::JsCast;
698
699 update_client_cookie_jar(
700 name,
701 value,
702 jar,
703 max_age,
704 expires,
705 domain,
706 path,
707 same_site,
708 secure,
709 http_only,
710 ssr_cookies_header_getter,
711 );
712
713 let document = document();
714 let document: &web_sys::HtmlDocument = document.unchecked_ref();
715
716 document.set_cookie(&cookie_jar_to_string(jar, name)).ok();
717}
718
719#[cfg(not(feature = "ssr"))]
720fn update_client_cookie_jar(
721 name: &str,
722 value: &Option<String>,
723 jar: &mut CookieJar,
724 max_age: Option<i64>,
725 expires: Option<i64>,
726 domain: &Option<String>,
727 path: &Option<String>,
728 same_site: Option<SameSite>,
729 secure: bool,
730 http_only: bool,
731 ssr_cookies_header_getter: Arc<dyn Fn() -> Option<String> + Send + Sync>,
732) {
733 if let Some(new_jar) = load_and_parse_cookie_jar(ssr_cookies_header_getter) {
734 *jar = new_jar;
735 if let Some(value) = value {
736 let cookie = build_cookie_from_options(
737 name, max_age, expires, http_only, secure, path, same_site, domain, value,
738 );
739
740 jar.add_original(cookie);
741 } else {
742 let max_age = Some(0);
743 let expires = Some(0);
744 let value = "";
745 let cookie = build_cookie_from_options(
746 name, max_age, expires, http_only, secure, path, same_site, domain, value,
747 );
748
749 jar.add(cookie);
750 }
751 }
752}
753
754#[cfg(not(feature = "ssr"))]
755fn cookie_jar_to_string(jar: &CookieJar, name: &str) -> String {
756 match jar.get(name) {
757 Some(c) => c.encoded().to_string(),
758 None => "".to_string(),
759 }
760}
761
762fn build_cookie_from_options(
763 name: &str,
764 max_age: Option<i64>,
765 expires: Option<i64>,
766 http_only: bool,
767 secure: bool,
768 path: &Option<String>,
769 same_site: Option<SameSite>,
770 domain: &Option<String>,
771 value: &str,
772) -> Cookie<'static> {
773 let mut cookie = Cookie::build((name, value));
774 if let Some(max_age) = max_age {
775 cookie = cookie.max_age(Duration::milliseconds(max_age));
776 }
777 if let Some(expires) = expires {
778 match OffsetDateTime::from_unix_timestamp(expires) {
779 Ok(expires) => {
780 cookie = cookie.expires(expires);
781 }
782 Err(err) => {
783 debug_warn!("failed to set cookie expiration: {:?}", err);
784 }
785 }
786 }
787 if http_only {
788 cookie = cookie.http_only(true);
789 }
790 if secure {
791 cookie = cookie.secure(true);
792 }
793 if let Some(domain) = domain {
794 cookie = cookie.domain(domain);
795 }
796 if let Some(path) = path {
797 cookie = cookie.path(path);
798 }
799 if let Some(same_site) = same_site {
800 cookie = cookie.same_site(same_site);
801 }
802
803 let cookie: Cookie = cookie.into();
804 cookie.into_owned()
805}
806
807#[cfg(feature = "ssr")]
808fn write_server_cookie(
809 name: &str,
810 value: Option<String>,
811 jar: &mut CookieJar,
812 max_age: Option<i64>,
813 expires: Option<i64>,
814 domain: Option<String>,
815 path: Option<String>,
816 same_site: Option<SameSite>,
817 secure: bool,
818 http_only: bool,
819 ssr_set_cookie: Arc<dyn Fn(&Cookie) + Send + Sync>,
820) {
821 if let Some(value) = value {
822 let cookie: Cookie = build_cookie_from_options(
823 name, max_age, expires, http_only, secure, &path, same_site, &domain, &value,
824 );
825
826 jar.add(cookie.into_owned());
827 } else {
828 jar.remove(name.to_owned());
829 }
830
831 for cookie in jar.delta() {
832 ssr_set_cookie(cookie);
833 }
834}
835
836fn load_and_parse_cookie_jar(
837 ssr_cookies_header_getter: Arc<dyn Fn() -> Option<String> + Send + Sync>,
838) -> Option<CookieJar> {
839 read_cookies_string(ssr_cookies_header_getter).map(|cookies| {
840 let mut jar = CookieJar::new();
841 for cookie in Cookie::split_parse_encoded(cookies).flatten() {
842 jar.add_original(cookie);
843 }
844
845 jar
846 })
847}