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::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 #[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 #[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 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 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 assert_eq!(
345 chrono_tz::IANA_TZDB_VERSION,
346 vtimezones_rs::IANA_TZDB_VERSION
347 );
348 }
349}