rustical_caldav/calendar/
resource.rs1use 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 #[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 #[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 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 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 assert_eq!(
352 chrono_tz::IANA_TZDB_VERSION,
353 vtimezones_rs::IANA_TZDB_VERSION
354 );
355 }
356}