rustical_caldav/calendar/methods/report/
mod.rs1use crate::{
2 CalDavPrincipalUri, Error,
3 calendar::CalendarResourceService,
4 calendar_object::{
5 CalendarObjectPropWrapper, CalendarObjectPropWrapperName, resource::CalendarObjectResource,
6 },
7};
8use axum::{
9 Extension,
10 extract::{OriginalUri, Path, State},
11 response::IntoResponse,
12};
13use calendar_multiget::{CalendarMultigetRequest, get_objects_calendar_multiget};
14use calendar_query::{CalendarQueryRequest, get_objects_calendar_query};
15use http::StatusCode;
16use rustical_dav::{
17 resource::{PrincipalUri, Resource},
18 xml::{
19 MultistatusElement, PropfindType, multistatus::ResponseElement,
20 sync_collection::SyncCollectionRequest,
21 },
22};
23use rustical_ical::CalendarObject;
24use rustical_store::{CalendarStore, SubscriptionStore, auth::Principal};
25use rustical_xml::{XmlDeserialize, XmlDocument};
26use sync_collection::handle_sync_collection;
27use tracing::instrument;
28
29mod calendar_multiget;
30pub mod calendar_query;
31mod sync_collection;
32
33#[derive(XmlDeserialize, XmlDocument, Clone, Debug, PartialEq)]
34pub(crate) enum ReportRequest {
35 #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
36 CalendarMultiget(CalendarMultigetRequest),
37 #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
38 CalendarQuery(CalendarQueryRequest),
39 #[xml(ns = "rustical_dav::namespace::NS_DAV")]
40 SyncCollection(SyncCollectionRequest<CalendarObjectPropWrapperName>),
41}
42
43impl ReportRequest {
44 const fn props(&self) -> &PropfindType<CalendarObjectPropWrapperName> {
45 match &self {
46 Self::CalendarMultiget(CalendarMultigetRequest { prop, .. })
47 | Self::CalendarQuery(CalendarQueryRequest { prop, .. })
48 | Self::SyncCollection(SyncCollectionRequest { prop, .. }) => prop,
49 }
50 }
51}
52
53fn objects_response(
54 objects: Vec<(String, CalendarObject)>,
55 not_found: Vec<String>,
56 path: &str,
57 principal: &str,
58 puri: &impl PrincipalUri,
59 user: &Principal,
60 prop: &PropfindType<CalendarObjectPropWrapperName>,
61) -> Result<MultistatusElement<CalendarObjectPropWrapper, String>, Error> {
62 let mut responses = Vec::new();
63 for (object_id, object) in objects {
64 let path = format!("{path}/{object_id}.ics");
65 responses.push(
66 CalendarObjectResource {
67 object,
68 object_id,
69 principal: principal.to_owned(),
70 }
71 .propfind(&path, prop, None, puri, user)?,
72 );
73 }
74
75 let not_found_responses = not_found
76 .into_iter()
77 .map(|path| ResponseElement {
78 href: path,
79 status: Some(StatusCode::NOT_FOUND),
80 ..Default::default()
81 })
82 .collect();
83
84 Ok(MultistatusElement {
85 responses,
86 member_responses: not_found_responses,
87 ..Default::default()
88 })
89}
90
91#[instrument(skip(cal_store))]
92pub async fn route_report_calendar<C: CalendarStore, S: SubscriptionStore>(
93 Path((principal, cal_id)): Path<(String, String)>,
94 user: Principal,
95 Extension(puri): Extension<CalDavPrincipalUri>,
96 State(CalendarResourceService { cal_store, .. }): State<CalendarResourceService<C, S>>,
97 OriginalUri(uri): OriginalUri,
98 body: String,
99) -> Result<impl IntoResponse, Error> {
100 if !user.is_principal(&principal) {
101 return Err(Error::Unauthorized);
102 }
103
104 let request = ReportRequest::parse_str(&body)?;
105 let props = request.props();
106
107 Ok(match &request {
108 ReportRequest::CalendarQuery(cal_query) => {
109 let objects =
110 get_objects_calendar_query(cal_query, &principal, &cal_id, cal_store.as_ref())
111 .await?;
112 objects_response(objects, vec![], uri.path(), &principal, &puri, &user, props)?
113 }
114 ReportRequest::CalendarMultiget(cal_multiget) => {
115 let (objects, not_found) = get_objects_calendar_multiget(
116 cal_multiget,
117 uri.path(),
118 &principal,
119 &cal_id,
120 cal_store.as_ref(),
121 )
122 .await?;
123 objects_response(
124 objects,
125 not_found,
126 uri.path(),
127 &principal,
128 &puri,
129 &user,
130 props,
131 )?
132 }
133 ReportRequest::SyncCollection(sync_collection) => {
134 handle_sync_collection(
135 sync_collection,
136 uri.path(),
137 &puri,
138 &user,
139 &principal,
140 &cal_id,
141 cal_store.as_ref(),
142 )
143 .await?
144 }
145 })
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use crate::calendar_object::{CalendarData, CalendarObjectPropName, ExpandElement};
152 use calendar_query::{CompFilterElement, FilterElement, TimeRangeElement};
153 use rustical_dav::{extensions::CommonPropertiesPropName, xml::PropElement};
154 use rustical_ical::UtcDateTime;
155 use rustical_xml::{NamespaceOwned, ValueDeserialize};
156
157 #[test]
158 fn test_xml_calendar_data() {
159 let report_request = ReportRequest::parse_str(r#"
160 <?xml version="1.0" encoding="UTF-8"?>
161 <calendar-multiget xmlns="urn:ietf:params:xml:ns:caldav" xmlns:D="DAV:">
162 <D:prop>
163 <D:getetag/>
164 <calendar-data>
165 <expand start="20250426T220000Z" end="20250503T220000Z"/>
166 </calendar-data>
167 </D:prop>
168 <D:href>/caldav/user/user/6f787542-5256-401a-8db97003260da/ae7a998fdfd1d84a20391168962c62b</D:href>
169 </calendar-multiget>
170 "#).unwrap();
171
172 assert_eq!(
173 report_request,
174 ReportRequest::CalendarMultiget(CalendarMultigetRequest {
175 prop: rustical_dav::xml::PropfindType::Prop(PropElement(vec![
176 CalendarObjectPropWrapperName::CalendarObject(CalendarObjectPropName::Getetag),
177 CalendarObjectPropWrapperName::CalendarObject(CalendarObjectPropName::CalendarData(
178 CalendarData { comp: None, prop: None, expand: Some(ExpandElement {
179 start: <UtcDateTime as ValueDeserialize>::deserialize("20250426T220000Z").unwrap(),
180 end: <UtcDateTime as ValueDeserialize>::deserialize("20250503T220000Z").unwrap(),
181 }), limit_recurrence_set: None, limit_freebusy_set: None }
182 )),
183 ], vec![])),
184 href: vec![
185 "/caldav/user/user/6f787542-5256-401a-8db97003260da/ae7a998fdfd1d84a20391168962c62b".to_owned()
186 ]
187 })
188 );
189 }
190
191 #[test]
192 fn test_xml_calendar_query() {
193 let report_request = ReportRequest::parse_str(
194 r#"
195 <?xml version='1.0' encoding='UTF-8' ?>
196 <CAL:calendar-query xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav">
197 <prop>
198 <getetag />
199 </prop>
200 <CAL:filter>
201 <CAL:comp-filter name="VCALENDAR">
202 <CAL:comp-filter name="VEVENT">
203 <CAL:time-range start="20240924T143437Z" />
204 </CAL:comp-filter>
205 </CAL:comp-filter>
206 </CAL:filter>
207 </CAL:calendar-query>"#,
208 )
209 .unwrap();
210 assert_eq!(
211 report_request,
212 ReportRequest::CalendarQuery(CalendarQueryRequest {
213 prop: rustical_dav::xml::PropfindType::Prop(PropElement(
214 vec![CalendarObjectPropWrapperName::CalendarObject(
215 CalendarObjectPropName::Getetag
216 ),],
217 vec![]
218 )),
219 filter: Some(FilterElement {
220 comp_filter: CompFilterElement {
221 is_not_defined: None,
222 time_range: None,
223 prop_filter: vec![],
224 comp_filter: vec![CompFilterElement {
225 is_not_defined: None,
226 time_range: Some(TimeRangeElement {
227 start: Some(
228 <UtcDateTime as ValueDeserialize>::deserialize(
229 "20240924T143437Z"
230 )
231 .unwrap()
232 ),
233 end: None
234 }),
235 prop_filter: vec![],
236 comp_filter: vec![],
237 name: "VEVENT".to_owned()
238 }],
239 name: "VCALENDAR".to_owned()
240 }
241 }),
242 timezone: None,
243 timezone_id: None,
244 })
245 );
246 }
247
248 #[test]
249 fn test_xml_calendar_multiget() {
250 let report_request = ReportRequest::parse_str(r#"
251 <?xml version="1.0" encoding="UTF-8"?>
252 <calendar-multiget xmlns="urn:ietf:params:xml:ns:caldav" xmlns:D="DAV:">
253 <D:prop>
254 <D:getetag/>
255 <D:displayname/>
256 <D:invalid-prop/>
257 </D:prop>
258 <D:href>/caldav/user/user/6f787542-5256-401a-8db97003260da/ae7a998fdfd1d84a20391168962c62b</D:href>
259 </calendar-multiget>
260 "#).unwrap();
261
262 assert_eq!(
263 report_request,
264 ReportRequest::CalendarMultiget(CalendarMultigetRequest {
265 prop: rustical_dav::xml::PropfindType::Prop(PropElement(vec![
266 CalendarObjectPropWrapperName::CalendarObject(CalendarObjectPropName::Getetag),
267 CalendarObjectPropWrapperName::Common(CommonPropertiesPropName::Displayname),
268 ], vec![(Some(NamespaceOwned(Vec::from("DAV:"))), "invalid-prop".to_string())])),
269 href: vec![
270 "/caldav/user/user/6f787542-5256-401a-8db97003260da/ae7a998fdfd1d84a20391168962c62b".to_owned()
271 ]
272 })
273 );
274 }
275}