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