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, VapidPublicKeyB64};
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    pub vapid_pubkey: VapidPublicKeyB64,
78}
79
80impl ResourceName for CalendarResource {
81    fn get_name(&self) -> Cow<'_, str> {
82        Cow::from(&self.cal.id)
83    }
84}
85
86impl From<CalendarResource> for Calendar {
87    fn from(value: CalendarResource) -> Self {
88        value.cal
89    }
90}
91
92impl SyncTokenExtension for CalendarResource {
93    fn get_synctoken(&self) -> String {
94        self.cal.format_synctoken()
95    }
96}
97
98impl DavPushExtension for CalendarResource {
99    fn get_topic(&self) -> String {
100        self.cal.push_topic.clone()
101    }
102
103    fn get_vapid_public_key(&self) -> Option<VapidPublicKeyB64> {
104        Some(self.vapid_pubkey.clone())
105    }
106}
107
108impl Resource for CalendarResource {
109    type Prop = CalendarPropWrapper;
110    type Error = Error;
111    type Principal = Principal;
112
113    fn is_collection(&self) -> bool {
114        true
115    }
116
117    fn get_resourcetype(&self) -> Resourcetype {
118        if self.cal.subscription_url.is_none() {
119            resourcetype!((NS_DAV, "collection"), (NS_CALDAV, "calendar"),)
120        } else {
121            resourcetype!((NS_DAV, "collection"), (NS_CALENDARSERVER, "subscribed"),)
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                    Uri::from_static("https://www.iana.org/time-zones").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 => CalendarProp::Source(
170                    self.cal
171                        .subscription_url
172                        .as_deref()
173                        .map(Uri::from_str)
174                        .and_then(|res| {
175                            res.map_or_else(
176                                |_| {
177                                    tracing::error!(
178                                        "Invalid source url for calendar {}/{}",
179                                        self.cal.principal,
180                                        self.cal.id
181                                    );
182                                    None
183                                },
184                                Some,
185                            )
186                        })
187                        .map(HrefElement::from),
188                ),
189                CalendarPropName::MinDateTime => {
190                    CalendarProp::MinDateTime(CalDateTime::from(DateTime::<Utc>::MIN_UTC).format())
191                }
192                CalendarPropName::MaxDateTime => {
193                    CalendarProp::MaxDateTime(CalDateTime::from(DateTime::<Utc>::MAX_UTC).format())
194                }
195            }),
196            CalendarPropWrapperName::SyncToken(prop) => {
197                CalendarPropWrapper::SyncToken(SyncTokenExtension::get_prop(self, prop)?)
198            }
199            CalendarPropWrapperName::DavPush(prop) => {
200                CalendarPropWrapper::DavPush(DavPushExtension::get_prop(self, prop)?)
201            }
202            CalendarPropWrapperName::Common(prop) => CalendarPropWrapper::Common(
203                CommonPropertiesExtension::get_prop(self, puri, user, prop)?,
204            ),
205        })
206    }
207
208    fn set_prop(&mut self, prop: Self::Prop) -> Result<(), rustical_dav::Error> {
209        match prop {
210            CalendarPropWrapper::Calendar(prop) => match prop {
211                CalendarProp::CalendarColor(color) => {
212                    self.cal.meta.color = color;
213                    Ok(())
214                }
215                CalendarProp::CalendarDescription(description) => {
216                    self.cal.meta.description = description;
217                    Ok(())
218                }
219                CalendarProp::CalendarTimezone(timezone) => {
220                    if let Some(tz) = timezone {
221                        // TODO: Proper error (calendar-timezone precondition)
222                        let calendar = IcalParser::from_slice(tz.as_bytes())
223                            .next()
224                            .ok_or_else(|| {
225                                rustical_dav::Error::BadRequest(
226                                    "No timezone data provided".to_owned(),
227                                )
228                            })?
229                            .map_err(|_| {
230                                rustical_dav::Error::BadRequest(
231                                    "No timezone data provided".to_owned(),
232                                )
233                            })?;
234
235                        let timezone = calendar.vtimezones.values().next().ok_or_else(|| {
236                            rustical_dav::Error::BadRequest("No timezone data provided".to_owned())
237                        })?;
238                        let timezone: Option<chrono_tz::Tz> = timezone.into();
239                        let timezone = timezone.ok_or_else(|| {
240                            rustical_dav::Error::BadRequest("No timezone data provided".to_owned())
241                        })?;
242                        self.cal.timezone_id = Some(timezone.name().to_owned());
243                    }
244                    Ok(())
245                }
246                CalendarProp::CalendarTimezoneId(timezone_id) => {
247                    if let Some(tzid) = &timezone_id
248                        && !vtimezones_rs::VTIMEZONES.contains_key(tzid)
249                    {
250                        return Err(rustical_dav::Error::BadRequest(format!(
251                            "Invalid timezone-id: {tzid}"
252                        )));
253                    }
254                    self.cal.timezone_id = timezone_id;
255                    Ok(())
256                }
257                CalendarProp::CalendarOrder(order) => {
258                    self.cal.meta.order = order.unwrap_or_default();
259                    Ok(())
260                }
261                CalendarProp::SupportedCalendarComponentSet(comp_set) => {
262                    self.cal.components = comp_set.into();
263                    Ok(())
264                }
265                CalendarProp::TimezoneServiceSet(_)
266                | CalendarProp::SupportedCalendarData(_)
267                | CalendarProp::SupportedCollationSet(_)
268                | CalendarProp::MaxResourceSize(_)
269                | CalendarProp::SupportedReportSet(_)
270                | CalendarProp::Source(_)
271                | CalendarProp::MinDateTime(_)
272                | CalendarProp::MaxDateTime(_) => Err(rustical_dav::Error::PropReadOnly),
273            },
274            CalendarPropWrapper::SyncToken(prop) => SyncTokenExtension::set_prop(self, prop),
275            CalendarPropWrapper::DavPush(prop) => DavPushExtension::set_prop(self, prop),
276            CalendarPropWrapper::Common(prop) => CommonPropertiesExtension::set_prop(self, prop),
277        }
278    }
279
280    fn remove_prop(&mut self, prop: &CalendarPropWrapperName) -> Result<(), rustical_dav::Error> {
281        match prop {
282            CalendarPropWrapperName::Calendar(prop) => match prop {
283                CalendarPropName::CalendarColor => {
284                    self.cal.meta.color = None;
285                    Ok(())
286                }
287                CalendarPropName::CalendarDescription => {
288                    self.cal.meta.description = None;
289                    Ok(())
290                }
291                CalendarPropName::CalendarTimezone | CalendarPropName::CalendarTimezoneId => {
292                    self.cal.timezone_id = None;
293                    Ok(())
294                }
295                CalendarPropName::CalendarOrder => {
296                    self.cal.meta.order = 0;
297                    Ok(())
298                }
299                CalendarPropName::SupportedCalendarComponentSet => {
300                    Err(rustical_dav::Error::PropReadOnly)
301                }
302                CalendarPropName::TimezoneServiceSet
303                | CalendarPropName::SupportedCalendarData
304                | CalendarPropName::SupportedCollationSet
305                | CalendarPropName::MaxResourceSize
306                | CalendarPropName::SupportedReportSet
307                | CalendarPropName::Source
308                | CalendarPropName::MinDateTime
309                | CalendarPropName::MaxDateTime => Err(rustical_dav::Error::PropReadOnly),
310            },
311            CalendarPropWrapperName::SyncToken(prop) => SyncTokenExtension::remove_prop(self, prop),
312            CalendarPropWrapperName::DavPush(prop) => DavPushExtension::remove_prop(self, prop),
313            CalendarPropWrapperName::Common(prop) => {
314                CommonPropertiesExtension::remove_prop(self, prop)
315            }
316        }
317    }
318
319    fn get_displayname(&self) -> Option<&str> {
320        self.cal.meta.displayname.as_deref()
321    }
322    fn set_displayname(&mut self, name: Option<String>) -> Result<(), rustical_dav::Error> {
323        self.cal.meta.displayname = name;
324        Ok(())
325    }
326
327    fn get_owner(&self) -> Option<&str> {
328        Some(&self.cal.principal)
329    }
330
331    fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> {
332        if self.cal.subscription_url.is_some() || self.read_only {
333            return Ok(UserPrivilegeSet::owner_write_properties(
334                user.is_principal(&self.cal.principal),
335            ));
336        }
337
338        Ok(UserPrivilegeSet::owner_only(
339            user.is_principal(&self.cal.principal),
340        ))
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    #[test]
347    fn test_tzdb_version() {
348        // Ensure that both chrono_tz and vzic_rs use the same tzdb version
349        assert_eq!(
350            chrono_tz::IANA_TZDB_VERSION,
351            vtimezones_rs::IANA_TZDB_VERSION
352        );
353    }
354}