Skip to main content

rustical_caldav/calendar_object/
methods.rs

1use crate::Error;
2use crate::calendar_object::{CalendarObjectPathComponents, CalendarObjectResourceService};
3use crate::error::Precondition;
4use axum::body::Body;
5use axum::extract::{Path, State};
6use axum::response::{IntoResponse, Response};
7use axum_extra::TypedHeader;
8use caldata::parser::ParserOptions;
9use headers::{ContentType, ETag, HeaderMapExt, IfMatch, IfNoneMatch};
10use http::{HeaderMap, HeaderValue, Method, StatusCode};
11use rustical_ical::CalendarObject;
12use rustical_store::CalendarStore;
13use rustical_store::auth::Principal;
14use std::str::FromStr;
15use tracing::{instrument, warn};
16
17#[instrument(skip(cal_store))]
18pub async fn get_event<C: CalendarStore>(
19    Path(CalendarObjectPathComponents {
20        principal,
21        calendar_id,
22        object_id,
23    }): Path<CalendarObjectPathComponents>,
24    State(CalendarObjectResourceService {
25        cal_store,
26        config: _,
27    }): State<CalendarObjectResourceService<C>>,
28    user: Principal,
29    method: Method,
30) -> Result<Response, Error> {
31    if !user.is_principal(&principal) {
32        return Err(crate::Error::Unauthorized);
33    }
34
35    let calendar = cal_store
36        .get_calendar(&principal, &calendar_id, false)
37        .await?;
38    if !user.is_principal(&calendar.principal) {
39        return Err(crate::Error::Unauthorized);
40    }
41
42    let event = cal_store
43        .get_object(&principal, &calendar_id, &object_id, false)
44        .await?;
45
46    let mut resp = Response::builder().status(StatusCode::OK);
47    let hdrs = resp.headers_mut().unwrap();
48    hdrs.typed_insert(ETag::from_str(&event.get_etag()).unwrap());
49    hdrs.typed_insert(ContentType::from_str("text/calendar; charset=utf-8").unwrap());
50    if matches!(method, Method::HEAD) {
51        Ok(resp.body(Body::empty()).unwrap())
52    } else {
53        Ok(resp.body(Body::new(event.get_ics().to_owned())).unwrap())
54    }
55}
56
57#[instrument(skip(cal_store))]
58pub async fn put_event<C: CalendarStore>(
59    Path(CalendarObjectPathComponents {
60        principal,
61        calendar_id,
62        object_id,
63    }): Path<CalendarObjectPathComponents>,
64    State(CalendarObjectResourceService { cal_store, config }): State<
65        CalendarObjectResourceService<C>,
66    >,
67    user: Principal,
68    mut if_none_match: Option<TypedHeader<IfNoneMatch>>,
69    mut if_match: Option<TypedHeader<IfMatch>>,
70    header_map: HeaderMap,
71    body: String,
72) -> Result<Response, Error> {
73    if !user.is_principal(&principal) {
74        return Err(crate::Error::Unauthorized);
75    }
76
77    // https://github.com/hyperium/headers/issues/204
78    if !header_map.contains_key("If-None-Match") {
79        if_none_match = None;
80    }
81    if !header_map.contains_key("If-Match") {
82        if_match = None;
83    }
84
85    if if_match.is_some() || if_none_match.is_some() {
86        // TODO: Put into transaction?
87        let existing = match cal_store
88            .get_object(&principal, &calendar_id, &object_id, false)
89            .await
90        {
91            Ok(existing) => Some(existing),
92            Err(rustical_store::Error::NotFound) => None,
93            Err(err) => Err(err)?,
94        };
95
96        // There's an already existing object
97        if let Some(existing) = existing {
98            let etag: Option<ETag> = existing.get_etag().parse().ok();
99
100            if let Some(if_match) = if_match.as_ref()
101                && etag
102                    .as_ref()
103                    // If ETag is None If-Match will also fail
104                    .is_none_or(|etag| !if_match.precondition_passes(etag))
105            {
106                return Err(Error::DavError(rustical_dav::Error::PreconditionFailed));
107            }
108
109            if let Some(if_none_match) = if_none_match.as_ref()
110                && etag
111                    .as_ref()
112                    // If ETag is None If-None-Match will succeed as it will not match
113                    .is_some_and(|etag| !if_none_match.precondition_passes(etag))
114            {
115                return Err(Error::DavError(rustical_dav::Error::PreconditionFailed));
116            }
117        }
118        // No existing object but we still expect a match
119        // From https://datatracker.ietf.org/doc/html/rfc2616#section-14.24
120        // ```
121        // If none of the entity tags match, or if "*" is given and no current
122        // entity exists, the server MUST NOT perform the requested method, and
123        // MUST return a 412 (Precondition Failed) response. This behavior is
124        // most useful when the client wants to prevent an updating method, such
125        // as PUT, from modifying a resource that has changed since the client
126        // last retrieved it.
127        // ```
128        else if if_match.is_some() {
129            return Err(Error::DavError(rustical_dav::Error::PreconditionFailed));
130        }
131    }
132
133    let object = match CalendarObject::import(
134        &body,
135        Some(ParserOptions {
136            rfc7809: config.rfc7809,
137        }),
138    ) {
139        Ok(object) => object,
140        Err(err) => {
141            warn!("invalid calendar data:\n{body}");
142            warn!("{err}");
143            return Err(Error::PreconditionFailed(Precondition::ValidCalendarData));
144        }
145    };
146    let etag = object.get_etag();
147    cal_store
148        .put_object(&principal, &calendar_id, &object_id, object, true)
149        .await?;
150
151    let mut headers = HeaderMap::new();
152    headers.insert(
153        "ETag",
154        HeaderValue::from_str(&etag).expect("Contains no invalid characters"),
155    );
156    Ok((StatusCode::CREATED, headers).into_response())
157}