From 3e1e28935077df113b78855190fc0571f41de803 Mon Sep 17 00:00:00 2001 From: Lennart <18233294+lennart-k@users.noreply.github.com> Date: Wed, 29 May 2024 16:07:38 +0200 Subject: [PATCH] caldav: Very basic implementation of the REPORT method --- crates/caldav/src/calendar/methods/mod.rs | 90 +------- crates/caldav/src/calendar/methods/report.rs | 205 ++++++++++++++++++ crates/caldav/src/lib.rs | 2 +- .../caldav/tests/calendar_methods_report.rs | 22 ++ 4 files changed, 229 insertions(+), 90 deletions(-) create mode 100644 crates/caldav/src/calendar/methods/report.rs create mode 100644 crates/caldav/tests/calendar_methods_report.rs diff --git a/crates/caldav/src/calendar/methods/mod.rs b/crates/caldav/src/calendar/methods/mod.rs index a6e2adf..35d5a85 100644 --- a/crates/caldav/src/calendar/methods/mod.rs +++ b/crates/caldav/src/calendar/methods/mod.rs @@ -1,91 +1,3 @@ -use crate::event::resource::EventFile; -use crate::CalDavContext; -use crate::Error; -use actix_web::http::header::ContentType; -use actix_web::web::{Data, Path}; -use actix_web::HttpResponse; -use anyhow::Result; -use roxmltree::{Node, NodeType}; -use rustical_auth::{AuthInfoExtractor, CheckAuthentication}; -use rustical_dav::namespace::Namespace; -use rustical_dav::propfind::ServicePrefix; -use rustical_dav::resource::HandlePropfind; -use rustical_dav::xml_snippets::generate_multistatus; -use rustical_store::calendar::CalendarStore; -use rustical_store::event::Event; - pub mod delete; pub mod mkcalendar; - -async fn handle_report_calendar_query( - query_node: Node<'_, '_>, - events: Vec, - prefix: &str, -) -> Result { - let prop_node = query_node - .children() - .find(|n| n.node_type() == NodeType::Element && n.tag_name().name() == "prop") - .ok_or(Error::BadRequest)?; - - let props: Vec<&str> = prop_node - .children() - .map(|node| node.tag_name().name()) - .collect(); - - let event_files: Vec<_> = events - .into_iter() - .map(|event| { - // TODO: fix - // let path = format!("{}/{}", request.path(), event.get_uid()); - EventFile { - event, // cal_store: cal_store.clone(), - } - }) - .collect(); - let mut event_responses = Vec::new(); - for event_file in event_files { - event_responses.push(event_file.propfind(prefix, props.clone()).await?); - } - // let event_results: Result, _> = event_files - // .iter() - // .map(|ev| ev.propfind(props.clone())) - // .collect(); - // let event_responses = event_results?; - - let output = generate_multistatus(vec![Namespace::Dav, Namespace::CalDAV], |writer| { - for result in event_responses { - writer - .write_serializable("response", &result) - .map_err(|_e| quick_xml::Error::TextNotFound)?; - } - Ok(()) - })?; - - Ok(HttpResponse::MultiStatus() - .content_type(ContentType::xml()) - .body(output)) -} - -pub async fn route_report_calendar( - context: Data>, - body: String, - path: Path<(String, String)>, - _auth: AuthInfoExtractor, - prefix: Data, -) -> Result { - // TODO: Check authorization - let (_principal, cid) = path.into_inner(); - let prefix = &prefix.0; - - let doc = roxmltree::Document::parse(&body).map_err(|_e| Error::BadRequest)?; - let query_node = doc.root_element(); - let events = context.store.read().await.get_events(&cid).await.unwrap(); - - // TODO: implement filtering - match query_node.tag_name().name() { - "calendar-query" => {} - "calendar-multiget" => {} - _ => return Err(Error::BadRequest), - }; - handle_report_calendar_query(query_node, events, prefix).await -} +pub mod report; diff --git a/crates/caldav/src/calendar/methods/report.rs b/crates/caldav/src/calendar/methods/report.rs new file mode 100644 index 0000000..5631421 --- /dev/null +++ b/crates/caldav/src/calendar/methods/report.rs @@ -0,0 +1,205 @@ +use actix_web::{ + http::header::ContentType, + web::{Data, Path}, + HttpRequest, HttpResponse, +}; +use rustical_auth::{AuthInfoExtractor, CheckAuthentication}; +use rustical_dav::{ + namespace::Namespace, + propfind::{MultistatusElement, PropElement, PropfindType, ServicePrefix}, + resource::HandlePropfind, +}; +use rustical_store::{calendar::CalendarStore, event::Event}; +use serde::{Deserialize, Serialize}; +use tokio::sync::RwLock; + +use crate::{event::resource::EventFile, Error}; + +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "kebab-case")] +pub enum PropQuery { + Allprop, + Prop, + Propname, +} + +#[derive(Deserialize, Clone, Debug)] +#[serde(rename_all = "kebab-case")] +// +pub struct CalendarMultigetRequest { + #[serde(flatten)] + prop: PropfindType, + href: Vec, +} + +#[derive(Deserialize, Clone, Debug)] +#[serde(rename_all = "kebab-case")] +struct TimeRangeElement { + #[serde(rename = "@start")] + start: Option, + #[serde(rename = "@end")] + end: Option, +} + +#[derive(Deserialize, Clone, Debug)] +#[serde(rename_all = "kebab-case")] +struct ParamFilterElement { + is_not_defined: Option<()>, + text_match: Option, + + #[serde(rename = "@name")] + name: String, +} + +#[derive(Deserialize, Clone, Debug)] +#[serde(rename_all = "kebab-case")] +struct TextMatchElement { + #[serde(rename = "@collation")] + collation: String, + #[serde(rename = "@negate-collation")] + negate_collation: String, +} + +#[derive(Deserialize, Clone, Debug)] +#[serde(rename_all = "kebab-case")] +struct PropFilterElement { + is_not_defined: Option<()>, + time_range: Option, + text_match: Option, + #[serde(default)] + param_filter: Vec, +} + +#[derive(Deserialize, Clone, Debug)] +#[serde(rename_all = "kebab-case")] +struct CompFilterElement { + is_not_defined: Option<()>, + time_range: Option, + #[serde(default)] + prop_filter: Vec, + #[serde(default)] + comp_filter: Vec, + + #[serde(rename = "@name")] + name: String, +} + +#[derive(Deserialize, Clone, Debug)] +#[serde(rename_all = "kebab-case")] +struct FilterElement { + comp_filter: CompFilterElement, +} + +#[derive(Deserialize, Clone, Debug)] +#[serde(rename_all = "kebab-case")] +// #[serde(rename = "calendar-query")] +// +pub struct CalendarQueryRequest { + #[serde(flatten)] + prop: PropfindType, + filter: Option, + timezone: Option, +} + +#[derive(Deserialize, Clone, Debug)] +#[serde(rename_all = "kebab-case")] +pub enum ReportRequest { + CalendarMultiget(CalendarMultigetRequest), + CalendarQuery(CalendarQueryRequest), +} + +async fn get_events_calendar_query( + cal_query: CalendarQueryRequest, + cid: &str, + store: &RwLock, +) -> Result, Error> { + // TODO: Implement filtering + Ok(store.read().await.get_events(cid).await?) +} + +async fn get_events_calendar_multiget( + cal_query: CalendarMultigetRequest, + cid: &str, + store: &RwLock, +) -> Result, Error> { + let mut events = Vec::new(); + for href in cal_query.href { + dbg!(href); + // let uid = + // events.push(store.read().await.get_event(cid, &uid)) + } + Ok(events) +} + +pub async fn route_report_calendar( + path: Path<(String, String)>, + body: String, + auth: AuthInfoExtractor, + req: HttpRequest, + cal_store: Data>, + prefix: Data, +) -> Result { + let (principal, cid) = path.into_inner(); + if principal != auth.inner.user_id { + return Err(Error::Unauthorized); + } + + let request: ReportRequest = quick_xml::de::from_str(&body).map_err(|err| { + dbg!(err.to_string()); + Error::InternalError + })?; + let events = match request.clone() { + ReportRequest::CalendarQuery(cal_query) => { + get_events_calendar_query(cal_query, &cid, &cal_store).await? + } + ReportRequest::CalendarMultiget(cal_multiget) => { + get_events_calendar_multiget(cal_multiget, &cid, &cal_store).await? + } + }; + + // TODO: Change this + let proptag = match request { + ReportRequest::CalendarQuery(CalendarQueryRequest { prop, .. }) => prop.clone(), + ReportRequest::CalendarMultiget(CalendarMultigetRequest { prop, .. }) => prop.clone(), + }; + let props = match proptag { + PropfindType::Allprop => { + vec!["allprop".to_owned()] + } + PropfindType::Propname => { + // TODO: Implement + return Err(Error::InternalError); + } + PropfindType::Prop(PropElement { prop: prop_tags }) => prop_tags.into(), + }; + let props: Vec<&str> = props.iter().map(String::as_str).collect(); + + let mut responses = Vec::new(); + for event in events { + responses.push( + EventFile { + path: format!("{}/{}", req.path(), event.get_uid()), + event, + } + .propfind(&prefix.0, props.clone()) + .await?, + ); + } + + let mut output = String::new(); + let mut ser = quick_xml::se::Serializer::new(&mut output); + ser.indent(' ', 4); + MultistatusElement { + responses, + member_responses: Vec::::new(), + ns_dav: Namespace::Dav.as_str(), + ns_caldav: Namespace::CalDAV.as_str(), + ns_ical: Namespace::ICal.as_str(), + } + .serialize(ser) + .unwrap(); + + Ok(HttpResponse::MultiStatus() + .content_type(ContentType::xml()) + .body(output)) +} diff --git a/crates/caldav/src/lib.rs b/crates/caldav/src/lib.rs index 7a627fc..0df8175 100644 --- a/crates/caldav/src/lib.rs +++ b/crates/caldav/src/lib.rs @@ -55,7 +55,7 @@ pub fn configure_dav( ) .service( web::resource("/{principal}/{calendar}") - .route(report_method().to(calendar::methods::route_report_calendar::)) + .route(report_method().to(calendar::methods::report::route_report_calendar::)) .route(propfind_method().to(route_propfind::>)) .route( mkcalendar_method().to(calendar::methods::mkcalendar::route_mkcol_calendar::), diff --git a/crates/caldav/tests/calendar_methods_report.rs b/crates/caldav/tests/calendar_methods_report.rs new file mode 100644 index 0000000..2fe77ed --- /dev/null +++ b/crates/caldav/tests/calendar_methods_report.rs @@ -0,0 +1,22 @@ +use rustical_caldav::calendar::methods::report::CalendarQueryRequest; + +const CALENDAR_QUERY: &str = r#" + + + + + + + + + + + + +"#; + +#[test] +fn test_parse_calendar_query() { + let query: CalendarQueryRequest = quick_xml::de::from_str(CALENDAR_QUERY).unwrap(); + dbg!(query); +}