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