rustical_caldav/calendar/methods/report/
mod.rs

1use 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}