diff --git a/crates/caldav/src/resources/calendar.rs b/crates/caldav/src/resources/calendar.rs index 9b0823a..1d1a417 100644 --- a/crates/caldav/src/resources/calendar.rs +++ b/crates/caldav/src/resources/calendar.rs @@ -37,14 +37,16 @@ pub struct SupportedCalendarComponentSet { #[derive(Serialize)] #[serde(rename_all = "kebab-case")] pub struct CalendarData { + #[serde(rename = "@content-type")] content_type: &'static str, + #[serde(rename = "@version")] version: &'static str, } impl Default for CalendarData { fn default() -> Self { Self { - content_type: "text/calendar;charset=utf-8", + content_type: "text/calendar", version: "2.0", } } @@ -121,13 +123,13 @@ pub enum CalendarProp { CurrentUserPrincipal, Owner, Displayname, - #[strum(props(tagname = "IC:calendar-color"))] + // #[strum(props(tagname = "IC:calendar-color"))] CalendarColor, - #[strum(props(tagname = "C:calendar-description"))] + // #[strum(props(tagname = "C:calendar-description"))] CalendarDescription, - #[strum(props(tagname = "C:supported-calendar-component-set"))] + // #[strum(props(tagname = "C:supported-calendar-component-set"))] SupportedCalendarComponentSet, - #[strum(props(tagname = "C:supported-calendar-data"))] + // #[strum(props(tagname = "C:supported-calendar-data"))] SupportedCalendarData, Getcontenttype, CurrentUserPrivilegeSet, @@ -141,9 +143,19 @@ pub enum CalendarPropResponse { CurrentUserPrincipal(HrefElement), Owner(HrefElement), Displayname(TextNode), + #[serde(rename = "IC:calendar-color", alias = "calendar-color")] CalendarColor(TextNode), + #[serde(rename = "C:calendar-description", alias = "calendar-description")] CalendarDescription(TextNode), + #[serde( + rename = "C:supported-calendar-component-set", + alias = "supported-calendar-component-set" + )] SupportedCalendarComponentSet(SupportedCalendarComponentSet), + #[serde( + rename = "C:supported-calendar-data", + alias = "supported-calendar-data" + )] SupportedCalendarData(SupportedCalendarData), Getcontenttype(TextNode), MaxResourceSize(TextNode), diff --git a/crates/caldav/src/resources/root.rs b/crates/caldav/src/resources/root.rs index 600ad6f..e1c20f6 100644 --- a/crates/caldav/src/resources/root.rs +++ b/crates/caldav/src/resources/root.rs @@ -29,7 +29,7 @@ pub struct Resourcetype { #[serde(rename_all = "kebab-case")] pub enum RootPropResponse { Resourcetype(Resourcetype), - CurrentUser(HrefElement), + CurrentUserPrincipal(HrefElement), } #[async_trait(?Send)] @@ -63,9 +63,9 @@ impl Resource for RootResource { fn get_prop(&self, prop: Self::PropType) -> Result { match prop { RootProp::Resourcetype => Ok(RootPropResponse::Resourcetype(Resourcetype::default())), - RootProp::CurrentUserPrincipal => Ok(RootPropResponse::CurrentUser(HrefElement::new( - format!("{}/{}/", self.prefix, self.principal), - ))), + RootProp::CurrentUserPrincipal => Ok(RootPropResponse::CurrentUserPrincipal( + HrefElement::new(format!("{}/{}/", self.prefix, self.principal)), + )), } } } diff --git a/crates/caldav/src/routes/calendar.rs b/crates/caldav/src/routes/calendar.rs index bfca5d8..b6ba473 100644 --- a/crates/caldav/src/routes/calendar.rs +++ b/crates/caldav/src/routes/calendar.rs @@ -5,7 +5,6 @@ use actix_web::http::header::ContentType; use actix_web::web::{Data, Path}; use actix_web::{HttpRequest, HttpResponse}; use anyhow::Result; -use quick_xml::events::BytesText; use roxmltree::{Node, NodeType}; use rustical_auth::{AuthInfoExtractor, CheckAuthentication}; use rustical_dav::namespace::Namespace; @@ -51,22 +50,28 @@ async fn handle_report_calendar_query( .map(|node| node.tag_name().name()) .collect(); - let mut event_results = Vec::new(); - for event in events { - let path = format!("{}/{}", request.path(), event.get_uid()); - let event_resource = EventResource { - cal_store: cal_store.clone(), - path: path.clone(), - event, - }; - event_results.push(event_resource.propfind(props.clone())?); - } + let event_resources: Vec<_> = events + .iter() + .map(|event| { + let path = format!("{}/{}", request.path(), event.get_uid()); + EventResource { + cal_store: cal_store.clone(), + path: path.clone(), + event: event.clone(), + } + }) + .collect(); + let event_results: Result, _> = event_resources + .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_results { - writer.write_event(quick_xml::events::Event::Text(BytesText::from_escaped( - result, - )))?; + for result in event_responses { + writer + .write_serializable("response", &result) + .map_err(|_e| quick_xml::Error::TextNotFound)?; } Ok(()) })?; @@ -90,8 +95,6 @@ pub async fn route_report_calendar {} diff --git a/crates/caldav/src/routes/propfind.rs b/crates/caldav/src/routes/propfind.rs index 0db3c24..371d97e 100644 --- a/crates/caldav/src/routes/propfind.rs +++ b/crates/caldav/src/routes/propfind.rs @@ -4,13 +4,13 @@ use actix_web::http::StatusCode; use actix_web::web::{Data, Path}; use actix_web::{HttpRequest, HttpResponse}; use anyhow::Result; -use quick_xml::events::BytesText; use rustical_auth::{AuthInfoExtractor, CheckAuthentication}; use rustical_dav::depth_extractor::Depth; use rustical_dav::namespace::Namespace; use rustical_dav::resource::{HandlePropfind, Resource}; use rustical_dav::xml_snippets::generate_multistatus; use rustical_store::calendar::CalendarStore; +use serde::Serialize; use thiserror::Error; #[derive(Debug, Error)] @@ -76,7 +76,6 @@ pub async fn route_propfind Result { let props = parse_propfind(&body)?; - // let req_path = req.path().to_string(); let auth_info = auth.inner; let resource = R::acquire_from_request( @@ -87,21 +86,26 @@ pub async fn route_propfind { + #[serde(rename = "$value")] + responses: Vec, + #[serde(rename = "$value")] + member_responses: Vec, + #[serde(rename = "@xmlns")] + ns_dav: &'static str, + #[serde(rename = "@xmlns:C")] + ns_caldav: &'static str, + #[serde(rename = "@xmlns:IC")] + ns_ical: &'static str, +} diff --git a/crates/dav/Cargo.toml b/crates/dav/Cargo.toml index e6355be..8c7825a 100644 --- a/crates/dav/Cargo.toml +++ b/crates/dav/Cargo.toml @@ -12,4 +12,5 @@ futures-util = "0.3" quick-xml = "0.31" rustical_auth = { path = "../auth/" } serde = { version = "1.0.197", features = ["derive"] } -strum = "0.26.2" +strum = "0.26" +itertools = "0.12" diff --git a/crates/dav/src/lib.rs b/crates/dav/src/lib.rs index ddad68e..374d1d1 100644 --- a/crates/dav/src/lib.rs +++ b/crates/dav/src/lib.rs @@ -1,5 +1,4 @@ pub mod depth_extractor; pub mod namespace; pub mod resource; -pub mod tagname; pub mod xml_snippets; diff --git a/crates/dav/src/resource.rs b/crates/dav/src/resource.rs index 072c8e4..e6117d3 100644 --- a/crates/dav/src/resource.rs +++ b/crates/dav/src/resource.rs @@ -1,7 +1,7 @@ use actix_web::{http::StatusCode, HttpRequest}; use anyhow::{anyhow, Result}; use async_trait::async_trait; -use quick_xml::Writer; +use itertools::Itertools; use rustical_auth::AuthInfo; use serde::Serialize; use std::str::FromStr; @@ -51,18 +51,25 @@ struct PropstatElement { #[derive(Serialize)] #[serde(rename_all = "kebab-case")] -struct PropstatResponseElement { +struct PropstatResponseElement { href: String, - propstat: PropstatElement, + propstat: Vec>, +} + +#[derive(Serialize)] +#[serde(untagged)] +enum PropstatType { + Normal(PropstatElement), + NotFound(PropstatElement), } pub trait HandlePropfind { - fn propfind(&self, props: Vec<&str>) -> Result; + fn propfind(&self, props: Vec<&str>) -> Result; } impl HandlePropfind for R { - fn propfind(&self, props: Vec<&str>) -> Result { - let mut props = props; + fn propfind(&self, props: Vec<&str>) -> Result { + let mut props = props.into_iter().unique().collect_vec(); if props.contains(&"allprops") { if props.len() != 1 { // allprops MUST be the only queried prop per spec @@ -71,9 +78,6 @@ impl HandlePropfind for R { props = R::list_dead_props().into(); } - let mut output_buffer = Vec::new(); - let mut writer = Writer::new_with_indent(&mut output_buffer, b' ', 2); - let mut invalid_props = Vec::<&str>::new(); let mut prop_responses = Vec::new(); for prop in props { @@ -89,31 +93,22 @@ impl HandlePropfind for R { } } - writer.write_serializable( - "response", - &PropstatResponseElement { - href: self.get_path().to_owned(), - propstat: PropstatElement { - status: format!("HTTP/1.1 {}", StatusCode::OK), - prop: PropWrapper { - prop: prop_responses, - }, - }, + let mut propstats = Vec::new(); + propstats.push(PropstatType::Normal(PropstatElement { + status: format!("HTTP/1.1 {}", StatusCode::OK), + prop: PropWrapper { + prop: prop_responses, }, - )?; + })); if !invalid_props.is_empty() { - // TODO: proper error reporting - writer.write_serializable( - "response", - &PropstatResponseElement { - href: self.get_path().to_owned(), - propstat: PropstatElement { - status: format!("HTTP/1.1 {}", StatusCode::NOT_FOUND), - prop: TagList(invalid_props.iter().map(|&s| s.to_owned()).collect()), - }, - }, - )?; + propstats.push(PropstatType::NotFound(PropstatElement { + status: format!("HTTP/1.1 {}", StatusCode::NOT_FOUND), + prop: TagList(invalid_props.iter().map(|&s| s.to_owned()).collect()), + })); } - Ok(std::str::from_utf8(&output_buffer)?.to_string()) + Ok(PropstatResponseElement { + href: self.get_path().to_owned(), + propstat: propstats, + }) } } diff --git a/crates/dav/src/tagname.rs b/crates/dav/src/tagname.rs deleted file mode 100644 index 94240ab..0000000 --- a/crates/dav/src/tagname.rs +++ /dev/null @@ -1,11 +0,0 @@ -use strum::EnumProperty; - -pub trait TagName { - fn tagname(self) -> &'static str; -} - -impl> TagName for P { - fn tagname(self) -> &'static str { - self.get_str("tagname").unwrap_or(self.into()) - } -}