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