Skip to main content

rustical_caldav/calendar/
resource.rs

1use super::prop::{SupportedCalendarComponentSet, SupportedCalendarData};
2use crate::Error;
3use crate::calendar::prop::{ReportMethod, SupportedCollationSet};
4use caldata::IcalParser;
5use caldata::types::CalDateTime;
6use chrono::{DateTime, Utc};
7use derive_more::derive::{From, Into};
8use http::Uri;
9use rustical_dav::extensions::{
10    CommonPropertiesExtension, CommonPropertiesProp, SyncTokenExtension, SyncTokenExtensionProp,
11};
12use rustical_dav::privileges::UserPrivilegeSet;
13use rustical_dav::resource::{PrincipalUri, Resource, ResourceName};
14use rustical_dav::xml::{HrefElement, Resourcetype, ResourcetypeInner, SupportedReportSet};
15use rustical_dav_push::{DavPushExtension, DavPushExtensionProp};
16use rustical_store::Calendar;
17use rustical_store::auth::Principal;
18use rustical_xml::{EnumVariants, PropName};
19use rustical_xml::{XmlDeserialize, XmlSerialize};
20use serde::Deserialize;
21use std::borrow::Cow;
22use std::str::FromStr;
23
24#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, EnumVariants, PropName)]
25#[xml(unit_variants_ident = "CalendarPropName")]
26pub enum CalendarProp {
27    // CalDAV (RFC 4791)
28    #[xml(ns = "rustical_dav::namespace::NS_ICAL")]
29    CalendarColor(Option<String>),
30    #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
31    CalendarDescription(Option<String>),
32    #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
33    CalendarTimezone(Option<String>),
34    // https://datatracker.ietf.org/doc/html/rfc7809
35    #[xml(ns = "rustical_dav::namespace::NS_CALDAV", skip_deserializing)]
36    TimezoneServiceSet(HrefElement),
37    #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
38    CalendarTimezoneId(Option<String>),
39    #[xml(ns = "rustical_dav::namespace::NS_ICAL")]
40    CalendarOrder(Option<i64>),
41    #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
42    SupportedCalendarComponentSet(SupportedCalendarComponentSet),
43    #[xml(ns = "rustical_dav::namespace::NS_CALDAV", skip_deserializing)]
44    SupportedCalendarData(SupportedCalendarData),
45    #[xml(ns = "rustical_dav::namespace::NS_CALDAV", skip_deserializing)]
46    SupportedCollationSet(SupportedCollationSet),
47    #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
48    MaxResourceSize(i64),
49    #[xml(skip_deserializing)]
50    #[xml(ns = "rustical_dav::namespace::NS_DAV")]
51    SupportedReportSet(SupportedReportSet<ReportMethod>),
52    #[xml(ns = "rustical_dav::namespace::NS_CALENDARSERVER")]
53    Source(Option<HrefElement>),
54    #[xml(skip_deserializing)]
55    #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
56    MinDateTime(String),
57    #[xml(skip_deserializing)]
58    #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
59    MaxDateTime(String),
60}
61
62#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, EnumVariants, PropName)]
63#[xml(unit_variants_ident = "CalendarPropWrapperName", untagged)]
64pub enum CalendarPropWrapper {
65    Calendar(CalendarProp),
66    SyncToken(SyncTokenExtensionProp),
67    DavPush(DavPushExtensionProp),
68    Common(CommonPropertiesProp),
69}
70
71#[derive(Clone, Debug, From, Into, Deserialize)]
72pub struct CalendarResource {
73    pub cal: Calendar,
74    pub read_only: bool,
75}
76
77impl ResourceName for CalendarResource {
78    fn get_name(&self) -> Cow<'_, str> {
79        Cow::from(&self.cal.id)
80    }
81}
82
83impl From<CalendarResource> for Calendar {
84    fn from(value: CalendarResource) -> Self {
85        value.cal
86    }
87}
88
89impl SyncTokenExtension for CalendarResource {
90    fn get_synctoken(&self) -> String {
91        self.cal.format_synctoken()
92    }
93}
94
95impl DavPushExtension for CalendarResource {
96    fn get_topic(&self) -> String {
97        self.cal.push_topic.clone()
98    }
99}
100
101impl Resource for CalendarResource {
102    type Prop = CalendarPropWrapper;
103    type Error = Error;
104    type Principal = Principal;
105
106    fn is_collection(&self) -> bool {
107        true
108    }
109
110    fn get_resourcetype(&self) -> Resourcetype {
111        if self.cal.subscription_url.is_none() {
112            Resourcetype(&[
113                ResourcetypeInner(Some(rustical_dav::namespace::NS_DAV), "collection"),
114                ResourcetypeInner(Some(rustical_dav::namespace::NS_CALDAV), "calendar"),
115            ])
116        } else {
117            Resourcetype(&[
118                ResourcetypeInner(Some(rustical_dav::namespace::NS_DAV), "collection"),
119                ResourcetypeInner(
120                    Some(rustical_dav::namespace::NS_CALENDARSERVER),
121                    "subscribed",
122                ),
123            ])
124        }
125    }
126
127    fn get_prop(
128        &self,
129        puri: &impl PrincipalUri,
130        user: &Principal,
131        prop: &CalendarPropWrapperName,
132    ) -> Result<Self::Prop, Self::Error> {
133        Ok(match prop {
134            CalendarPropWrapperName::Calendar(prop) => CalendarPropWrapper::Calendar(match prop {
135                CalendarPropName::CalendarColor => {
136                    CalendarProp::CalendarColor(self.cal.meta.color.clone())
137                }
138                CalendarPropName::CalendarDescription => {
139                    CalendarProp::CalendarDescription(self.cal.meta.description.clone())
140                }
141                CalendarPropName::CalendarTimezone => {
142                    CalendarProp::CalendarTimezone(self.cal.timezone_id.as_ref().and_then(|tzid| {
143                        vtimezones_rs::VTIMEZONES
144                            .get(tzid)
145                            .map(|tz| (*tz).to_string())
146                    }))
147                }
148                // chrono_tz uses the IANA database
149                CalendarPropName::TimezoneServiceSet => CalendarProp::TimezoneServiceSet(
150                    Uri::from_static("https://www.iana.org/time-zones").into(),
151                ),
152                CalendarPropName::CalendarTimezoneId => {
153                    CalendarProp::CalendarTimezoneId(self.cal.timezone_id.clone())
154                }
155                CalendarPropName::CalendarOrder => {
156                    CalendarProp::CalendarOrder(Some(self.cal.meta.order))
157                }
158                CalendarPropName::SupportedCalendarComponentSet => {
159                    CalendarProp::SupportedCalendarComponentSet(self.cal.components.clone().into())
160                }
161                CalendarPropName::SupportedCalendarData => {
162                    CalendarProp::SupportedCalendarData(SupportedCalendarData::default())
163                }
164                CalendarPropName::SupportedCollationSet => {
165                    CalendarProp::SupportedCollationSet(SupportedCollationSet::default())
166                }
167                CalendarPropName::MaxResourceSize => CalendarProp::MaxResourceSize(10_000_000),
168                CalendarPropName::SupportedReportSet => {
169                    CalendarProp::SupportedReportSet(SupportedReportSet::all())
170                }
171                CalendarPropName::Source => CalendarProp::Source(
172                    self.cal
173                        .subscription_url
174                        .as_deref()
175                        .map(Uri::from_str)
176                        .and_then(|res| {
177                            res.map_or_else(
178                                |_| {
179                                    tracing::error!(
180                                        "Invalid source url for calendar {}/{}",
181                                        self.cal.principal,
182                                        self.cal.id
183                                    );
184                                    None
185                                },
186                                Some,
187                            )
188                        })
189                        .map(HrefElement::from),
190                ),
191                CalendarPropName::MinDateTime => {
192                    CalendarProp::MinDateTime(CalDateTime::from(DateTime::<Utc>::MIN_UTC).format())
193                }
194                CalendarPropName::MaxDateTime => {
195                    CalendarProp::MaxDateTime(CalDateTime::from(DateTime::<Utc>::MAX_UTC).format())
196                }
197            }),
198            CalendarPropWrapperName::SyncToken(prop) => {
199                CalendarPropWrapper::SyncToken(SyncTokenExtension::get_prop(self, prop)?)
200            }
201            CalendarPropWrapperName::DavPush(prop) => {
202                CalendarPropWrapper::DavPush(DavPushExtension::get_prop(self, prop)?)
203            }
204            CalendarPropWrapperName::Common(prop) => CalendarPropWrapper::Common(
205                CommonPropertiesExtension::get_prop(self, puri, user, prop)?,
206            ),
207        })
208    }
209
210    fn set_prop(&mut self, prop: Self::Prop) -> Result<(), rustical_dav::Error> {
211        match prop {
212            CalendarPropWrapper::Calendar(prop) => match prop {
213                CalendarProp::CalendarColor(color) => {
214                    self.cal.meta.color = color;
215                    Ok(())
216                }
217                CalendarProp::CalendarDescription(description) => {
218                    self.cal.meta.description = description;
219                    Ok(())
220                }
221                CalendarProp::CalendarTimezone(timezone) => {
222                    if let Some(tz) = timezone {
223                        // TODO: Proper error (calendar-timezone precondition)
224                        let calendar = IcalParser::from_slice(tz.as_bytes())
225                            .next()
226                            .ok_or_else(|| {
227                                rustical_dav::Error::BadRequest(
228                                    "No timezone data provided".to_owned(),
229                                )
230                            })?
231                            .map_err(|_| {
232                                rustical_dav::Error::BadRequest(
233                                    "No timezone data provided".to_owned(),
234                                )
235                            })?;
236
237                        let timezone = calendar.vtimezones.values().next().ok_or_else(|| {
238                            rustical_dav::Error::BadRequest("No timezone data provided".to_owned())
239                        })?;
240                        let timezone: Option<chrono_tz::Tz> = timezone.into();
241                        let timezone = timezone.ok_or_else(|| {
242                            rustical_dav::Error::BadRequest("No timezone data provided".to_owned())
243                        })?;
244                        self.cal.timezone_id = Some(timezone.name().to_owned());
245                    }
246                    Ok(())
247                }
248                CalendarProp::CalendarTimezoneId(timezone_id) => {
249                    if let Some(tzid) = &timezone_id
250                        && !vtimezones_rs::VTIMEZONES.contains_key(tzid)
251                    {
252                        return Err(rustical_dav::Error::BadRequest(format!(
253                            "Invalid timezone-id: {tzid}"
254                        )));
255                    }
256                    self.cal.timezone_id = timezone_id;
257                    Ok(())
258                }
259                CalendarProp::CalendarOrder(order) => {
260                    self.cal.meta.order = order.unwrap_or_default();
261                    Ok(())
262                }
263                CalendarProp::SupportedCalendarComponentSet(comp_set) => {
264                    self.cal.components = comp_set.into();
265                    Ok(())
266                }
267                CalendarProp::TimezoneServiceSet(_)
268                | CalendarProp::SupportedCalendarData(_)
269                | CalendarProp::SupportedCollationSet(_)
270                | CalendarProp::MaxResourceSize(_)
271                | CalendarProp::SupportedReportSet(_)
272                | CalendarProp::Source(_)
273                | CalendarProp::MinDateTime(_)
274                | CalendarProp::MaxDateTime(_) => Err(rustical_dav::Error::PropReadOnly),
275            },
276            CalendarPropWrapper::SyncToken(prop) => SyncTokenExtension::set_prop(self, prop),
277            CalendarPropWrapper::DavPush(prop) => DavPushExtension::set_prop(self, prop),
278            CalendarPropWrapper::Common(prop) => CommonPropertiesExtension::set_prop(self, prop),
279        }
280    }
281
282    fn remove_prop(&mut self, prop: &CalendarPropWrapperName) -> Result<(), rustical_dav::Error> {
283        match prop {
284            CalendarPropWrapperName::Calendar(prop) => match prop {
285                CalendarPropName::CalendarColor => {
286                    self.cal.meta.color = None;
287                    Ok(())
288                }
289                CalendarPropName::CalendarDescription => {
290                    self.cal.meta.description = None;
291                    Ok(())
292                }
293                CalendarPropName::CalendarTimezone | CalendarPropName::CalendarTimezoneId => {
294                    self.cal.timezone_id = None;
295                    Ok(())
296                }
297                CalendarPropName::CalendarOrder => {
298                    self.cal.meta.order = 0;
299                    Ok(())
300                }
301                CalendarPropName::SupportedCalendarComponentSet => {
302                    Err(rustical_dav::Error::PropReadOnly)
303                }
304                CalendarPropName::TimezoneServiceSet
305                | CalendarPropName::SupportedCalendarData
306                | CalendarPropName::SupportedCollationSet
307                | CalendarPropName::MaxResourceSize
308                | CalendarPropName::SupportedReportSet
309                | CalendarPropName::Source
310                | CalendarPropName::MinDateTime
311                | CalendarPropName::MaxDateTime => Err(rustical_dav::Error::PropReadOnly),
312            },
313            CalendarPropWrapperName::SyncToken(prop) => SyncTokenExtension::remove_prop(self, prop),
314            CalendarPropWrapperName::DavPush(prop) => DavPushExtension::remove_prop(self, prop),
315            CalendarPropWrapperName::Common(prop) => {
316                CommonPropertiesExtension::remove_prop(self, prop)
317            }
318        }
319    }
320
321    fn get_displayname(&self) -> Option<&str> {
322        self.cal.meta.displayname.as_deref()
323    }
324    fn set_displayname(&mut self, name: Option<String>) -> Result<(), rustical_dav::Error> {
325        self.cal.meta.displayname = name;
326        Ok(())
327    }
328
329    fn get_owner(&self) -> Option<&str> {
330        Some(&self.cal.principal)
331    }
332
333    fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> {
334        if self.cal.subscription_url.is_some() || self.read_only {
335            return Ok(UserPrivilegeSet::owner_write_properties(
336                user.is_principal(&self.cal.principal),
337            ));
338        }
339
340        Ok(UserPrivilegeSet::owner_only(
341            user.is_principal(&self.cal.principal),
342        ))
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    #[test]
349    fn test_tzdb_version() {
350        // Ensure that both chrono_tz and vzic_rs use the same tzdb version
351        assert_eq!(
352            chrono_tz::IANA_TZDB_VERSION,
353            vtimezones_rs::IANA_TZDB_VERSION
354        );
355    }
356}