From 99ac6544489a983674c24fb71d42929b89c92afb Mon Sep 17 00:00:00 2001 From: Lennart <18233294+lennart-k@users.noreply.github.com> Date: Sun, 28 Jul 2024 20:54:31 +0200 Subject: [PATCH] Refactoring that will hopefully make life easier --- .../methods/report/calendar_multiget.rs | 75 ++++++++++++++ .../{report.rs => report/calendar_query.rs} | 99 +++++-------------- .../caldav/src/calendar/methods/report/mod.rs | 72 ++++++++++++++ .../methods/report/sync_collection.rs | 89 +++++++++++++++++ crates/caldav/src/calendar/prop.rs | 21 ++-- crates/dav/src/methods/proppatch.rs | 15 ++- crates/dav/src/resource.rs | 56 +++-------- crates/dav/src/xml/multistatus.rs | 50 +++++++++- 8 files changed, 338 insertions(+), 139 deletions(-) create mode 100644 crates/caldav/src/calendar/methods/report/calendar_multiget.rs rename crates/caldav/src/calendar/methods/{report.rs => report/calendar_query.rs} (53%) create mode 100644 crates/caldav/src/calendar/methods/report/mod.rs create mode 100644 crates/caldav/src/calendar/methods/report/sync_collection.rs diff --git a/crates/caldav/src/calendar/methods/report/calendar_multiget.rs b/crates/caldav/src/calendar/methods/report/calendar_multiget.rs new file mode 100644 index 0000000..df5f9f5 --- /dev/null +++ b/crates/caldav/src/calendar/methods/report/calendar_multiget.rs @@ -0,0 +1,75 @@ +use crate::{ + event::resource::{EventFile, EventProp}, + Error, +}; +use actix_web::HttpRequest; +use rustical_dav::{ + methods::propfind::{PropElement, PropfindType}, + namespace::Namespace, + resource::HandlePropfind, + xml::{multistatus::PropstatWrapper, MultistatusElement}, +}; +use rustical_store::{event::Event, CalendarStore}; +use serde::Deserialize; +use tokio::sync::RwLock; + +#[derive(Deserialize, Clone, Debug)] +#[serde(rename_all = "kebab-case")] +#[allow(dead_code)] +// +pub struct CalendarMultigetRequest { + #[serde(flatten)] + prop: PropfindType, + href: Vec, +} + +pub async fn get_events_calendar_multiget( + _cal_query: &CalendarMultigetRequest, + principal: &str, + cid: &str, + store: &RwLock, +) -> Result, Error> { + // TODO: proper implementation + Ok(store.read().await.get_events(principal, cid).await?) +} + +pub async fn handle_calendar_multiget( + cal_multiget: CalendarMultigetRequest, + req: HttpRequest, + prefix: &str, + principal: &str, + cid: &str, + cal_store: &RwLock, +) -> Result, String>, Error> { + let events = get_events_calendar_multiget(&cal_multiget, principal, cid, cal_store).await?; + + let props = match cal_multiget.prop { + PropfindType::Allprop => { + vec!["allprop".to_owned()] + } + PropfindType::Propname => { + // TODO: Implement + return Err(Error::NotImplemented); + } + 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 { + let path = format!("{}/{}", req.path(), event.get_uid()); + responses.push( + EventFile { event } + .propfind(prefix, path, props.clone()) + .await?, + ); + } + + Ok(MultistatusElement { + responses, + member_responses: vec![], + ns_dav: Namespace::Dav.as_str(), + ns_caldav: Namespace::CalDAV.as_str(), + ns_ical: Namespace::ICal.as_str(), + }) +} diff --git a/crates/caldav/src/calendar/methods/report.rs b/crates/caldav/src/calendar/methods/report/calendar_query.rs similarity index 53% rename from crates/caldav/src/calendar/methods/report.rs rename to crates/caldav/src/calendar/methods/report/calendar_query.rs index 92cac32..a9f498e 100644 --- a/crates/caldav/src/calendar/methods/report.rs +++ b/crates/caldav/src/calendar/methods/report/calendar_query.rs @@ -1,40 +1,21 @@ -use crate::{event::resource::EventFile, Error}; -use actix_web::{ - web::{Data, Path}, - HttpRequest, Responder, -}; -use rustical_auth::{AuthInfoExtractor, CheckAuthentication}; +use actix_web::HttpRequest; use rustical_dav::{ - methods::propfind::{PropElement, PropfindType, ServicePrefix}, + methods::propfind::{PropElement, PropfindType}, namespace::Namespace, resource::HandlePropfind, - xml::MultistatusElement, + xml::{multistatus::PropstatWrapper, MultistatusElement}, }; -use rustical_store::event::Event; -use rustical_store::CalendarStore; -use serde::{Deserialize, Serialize}; +use rustical_store::{event::Event, CalendarStore}; +use serde::Deserialize; use tokio::sync::RwLock; -#[derive(Deserialize, Serialize, Clone, Debug)] -#[serde(rename_all = "kebab-case")] -pub enum PropQuery { - Allprop, - Prop, - Propname, -} +use crate::{ + event::resource::{EventFile, EventProp}, + Error, +}; // TODO: Implement all the other filters -#[derive(Deserialize, Clone, Debug)] -#[serde(rename_all = "kebab-case")] -#[allow(dead_code)] -// -pub struct CalendarMultigetRequest { - #[serde(flatten)] - prop: PropfindType, - href: Vec, -} - #[derive(Deserialize, Clone, Debug)] #[serde(rename_all = "kebab-case")] #[allow(dead_code)] @@ -102,24 +83,16 @@ struct FilterElement { #[derive(Deserialize, Clone, Debug)] #[serde(rename_all = "kebab-case")] #[allow(dead_code)] -// #[serde(rename = "calendar-query")] // pub struct CalendarQueryRequest { #[serde(flatten)] - prop: PropfindType, + pub 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, +pub async fn get_events_calendar_query( + _cal_query: &CalendarQueryRequest, principal: &str, cid: &str, store: &RwLock, @@ -128,45 +101,17 @@ async fn get_events_calendar_query( Ok(store.read().await.get_events(principal, cid).await?) } -async fn get_events_calendar_multiget( - _cal_query: CalendarMultigetRequest, +pub async fn handle_calendar_query( + cal_query: CalendarQueryRequest, + req: HttpRequest, + prefix: &str, principal: &str, cid: &str, - store: &RwLock, -) -> Result, Error> { - // TODO: proper implementation - Ok(store.read().await.get_events(principal, cid).await?) -} + cal_store: &RwLock, +) -> Result, String>, Error> { + let events = get_events_calendar_query(&cal_query, principal, cid, cal_store).await?; -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)?; - let events = match request.clone() { - ReportRequest::CalendarQuery(cal_query) => { - get_events_calendar_query(cal_query, &principal, &cid, &cal_store).await? - } - ReportRequest::CalendarMultiget(cal_multiget) => { - get_events_calendar_multiget(cal_multiget, &principal, &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 { + let props = match cal_query.prop { PropfindType::Allprop => { vec!["allprop".to_owned()] } @@ -183,14 +128,14 @@ pub async fn route_report_calendar::new(), + member_responses: vec![], ns_dav: Namespace::Dav.as_str(), ns_caldav: Namespace::CalDAV.as_str(), ns_ical: Namespace::ICal.as_str(), diff --git a/crates/caldav/src/calendar/methods/report/mod.rs b/crates/caldav/src/calendar/methods/report/mod.rs new file mode 100644 index 0000000..c05d1eb --- /dev/null +++ b/crates/caldav/src/calendar/methods/report/mod.rs @@ -0,0 +1,72 @@ +use crate::Error; +use actix_web::{ + web::{Data, Path}, + HttpRequest, Responder, +}; +use calendar_multiget::{handle_calendar_multiget, CalendarMultigetRequest}; +use calendar_query::{handle_calendar_query, CalendarQueryRequest}; +use rustical_auth::{AuthInfoExtractor, CheckAuthentication}; +use rustical_dav::methods::propfind::ServicePrefix; +use rustical_store::CalendarStore; +use serde::{Deserialize, Serialize}; +use sync_collection::{handle_sync_collection, SyncCollectionRequest}; +use tokio::sync::RwLock; + +mod calendar_multiget; +mod calendar_query; +mod sync_collection; + +#[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 enum ReportRequest { + CalendarMultiget(CalendarMultigetRequest), + CalendarQuery(CalendarQueryRequest), + SyncCollection(SyncCollectionRequest), +} + +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); + } + + dbg!("REPORT request:", &body); + dbg!(req.headers().get("If")); + let request: ReportRequest = quick_xml::de::from_str(&body)?; + + Ok(match request.clone() { + ReportRequest::CalendarQuery(cal_query) => { + handle_calendar_query(cal_query, req, &prefix.0, &principal, &cid, &cal_store).await? + } + ReportRequest::CalendarMultiget(cal_multiget) => { + handle_calendar_multiget(cal_multiget, req, &prefix.0, &principal, &cid, &cal_store) + .await? + } + ReportRequest::SyncCollection(sync_collection) => { + handle_sync_collection( + sync_collection, + req, + &prefix.0, + &principal, + &cid, + &cal_store, + ) + .await? + } + }) +} diff --git a/crates/caldav/src/calendar/methods/report/sync_collection.rs b/crates/caldav/src/calendar/methods/report/sync_collection.rs new file mode 100644 index 0000000..1f14719 --- /dev/null +++ b/crates/caldav/src/calendar/methods/report/sync_collection.rs @@ -0,0 +1,89 @@ +use actix_web::HttpRequest; +use rustical_dav::{ + methods::propfind::{PropElement, PropfindType}, + namespace::Namespace, + resource::HandlePropfind, + xml::{multistatus::PropstatWrapper, MultistatusElement}, +}; +use rustical_store::{event::Event, CalendarStore}; +use serde::Deserialize; +use tokio::sync::RwLock; + +use crate::{ + event::resource::{EventFile, EventProp}, + Error, +}; + +#[derive(Deserialize, Clone, Debug)] +#[serde(rename_all = "kebab-case")] +enum SyncLevel { + #[serde(rename = "1")] + One, + Infinity, +} + +#[derive(Deserialize, Clone, Debug)] +#[serde(rename_all = "kebab-case")] +#[allow(dead_code)] +// +// +// +pub struct SyncCollectionRequest { + sync_token: String, + sync_level: SyncLevel, + timezone: Option, + #[serde(flatten)] + pub prop: PropfindType, + limit: Option, +} + +pub async fn get_events_sync_collection( + _sync_collection: &SyncCollectionRequest, + principal: &str, + cid: &str, + store: &RwLock, +) -> Result, Error> { + // TODO: proper implementation + Ok(store.read().await.get_events(principal, cid).await?) +} + +pub async fn handle_sync_collection( + sync_collection: SyncCollectionRequest, + req: HttpRequest, + prefix: &str, + principal: &str, + cid: &str, + cal_store: &RwLock, +) -> Result, String>, Error> { + let events = get_events_sync_collection(&sync_collection, principal, cid, cal_store).await?; + + let props = match sync_collection.prop { + PropfindType::Allprop => { + vec!["allprop".to_owned()] + } + PropfindType::Propname => { + // TODO: Implement + return Err(Error::NotImplemented); + } + 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 { + let path = format!("{}/{}", req.path(), event.get_uid()); + responses.push( + EventFile { event } + .propfind(prefix, path, props.clone()) + .await?, + ); + } + + Ok(MultistatusElement { + responses, + member_responses: vec![], + ns_dav: Namespace::Dav.as_str(), + ns_caldav: Namespace::CalDAV.as_str(), + ns_ical: Namespace::ICal.as_str(), + }) +} diff --git a/crates/caldav/src/calendar/prop.rs b/crates/caldav/src/calendar/prop.rs index 67ab422..4a6c3e7 100644 --- a/crates/caldav/src/calendar/prop.rs +++ b/crates/caldav/src/calendar/prop.rs @@ -101,25 +101,34 @@ impl Default for UserPrivilegeSet { pub enum ReportMethod { CalendarQuery, CalendarMultiget, - // SyncCollection, + SyncCollection, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] -pub struct SupportedReport { +pub struct ReportWrapper { + #[serde(rename = "$value")] report: ReportMethod, } -impl From for SupportedReport { +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct SupportedReportWrapper { + report: ReportWrapper, +} + +impl From for SupportedReportWrapper { fn from(value: ReportMethod) -> Self { - Self { report: value } + Self { + report: ReportWrapper { report: value }, + } } } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] pub struct SupportedReportSet { - supported_report: Vec, + supported_report: Vec, } impl Default for SupportedReportSet { @@ -128,7 +137,7 @@ impl Default for SupportedReportSet { supported_report: vec![ ReportMethod::CalendarQuery.into(), ReportMethod::CalendarMultiget.into(), - // ReportMethod::SyncCollection.into(), + ReportMethod::SyncCollection.into(), ], } } diff --git a/crates/dav/src/methods/proppatch.rs b/crates/dav/src/methods/proppatch.rs index 30c6fa2..8d763fc 100644 --- a/crates/dav/src/methods/proppatch.rs +++ b/crates/dav/src/methods/proppatch.rs @@ -4,13 +4,12 @@ use crate::namespace::Namespace; use crate::resource::InvalidProperty; use crate::resource::Resource; use crate::resource::ResourceService; -use crate::resource::{PropstatElement, PropstatResponseElement, PropstatType}; +use crate::xml::multistatus::{PropstatElement, PropstatWrapper, ResponseElement}; use crate::xml::MultistatusElement; use crate::xml::TagList; use crate::xml::TagName; use crate::Error; use actix_web::http::StatusCode; -use actix_web::Responder; use actix_web::{web::Path, HttpRequest}; use log::debug; use rustical_auth::{AuthInfoExtractor, CheckAuthentication}; @@ -55,7 +54,7 @@ pub async fn route_proppatch, -) -> Result { +) -> Result, PropstatWrapper>, R::Error> { let auth_info = auth.inner; let path_components = path.into_inner(); let href = req.path().to_owned(); @@ -138,25 +137,25 @@ pub async fn route_proppatch::new(), + member_responses: vec![], ns_dav: Namespace::Dav.as_str(), ns_caldav: Namespace::CalDAV.as_str(), ns_ical: Namespace::ICal.as_str(), diff --git a/crates/dav/src/resource.rs b/crates/dav/src/resource.rs index da9c525..e218485 100644 --- a/crates/dav/src/resource.rs +++ b/crates/dav/src/resource.rs @@ -1,4 +1,5 @@ -use crate::xml::TagList; +use crate::xml::multistatus::{PropTagWrapper, PropstatElement, PropstatWrapper}; +use crate::xml::{multistatus::ResponseElement, TagList}; use crate::Error; use actix_web::{http::StatusCode, HttpRequest, ResponseError}; use async_trait::async_trait; @@ -65,41 +66,6 @@ pub trait ResourceService: Sized { } } -#[derive(Serialize)] -pub struct PropTagWrapper { - #[serde(rename = "$value")] - prop: T, -} - -#[derive(Serialize)] -#[serde(untagged)] -pub enum PropWrapper { - Prop(PropTagWrapper), - TagList(TagList), -} - -#[derive(Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct PropstatElement { - pub prop: T, - pub status: String, -} - -#[derive(Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct PropstatResponseElement { - pub href: String, - pub propstat: Vec>, -} - -#[derive(Serialize)] -#[serde(untagged)] -pub enum PropstatType { - Normal(PropstatElement), - NotFound(PropstatElement), - Conflict(PropstatElement), -} - #[async_trait(?Send)] pub trait HandlePropfind { type Error: ResponseError + From + From; @@ -121,7 +87,7 @@ impl HandlePropfind for R { prefix: &str, path: String, props: Vec<&str>, - ) -> Result>, TagList>, R::Error> { + ) -> Result>, R::Error> { let mut props = props; if props.contains(&"propname") { if props.len() != 1 { @@ -134,10 +100,10 @@ impl HandlePropfind for R { .iter() .map(|&prop| prop.to_string()) .collect(); - return Ok(PropstatResponseElement { + return Ok(ResponseElement { href: path, - propstat: vec![PropstatType::Normal(PropstatElement { - prop: PropWrapper::TagList(TagList::from(props)), + propstat: vec![PropstatWrapper::TagList(PropstatElement { + prop: TagList::from(props), status: format!("HTTP/1.1 {}", StatusCode::OK), })], }); @@ -170,14 +136,14 @@ impl HandlePropfind for R { .map(|prop| self.get_prop(prefix, prop)) .collect::, R::Error>>()?; - let mut propstats = vec![PropstatType::Normal(PropstatElement { + let mut propstats = vec![PropstatWrapper::Normal(PropstatElement { status: format!("HTTP/1.1 {}", StatusCode::OK), - prop: PropWrapper::Prop(PropTagWrapper { + prop: PropTagWrapper { prop: prop_responses, - }), + }, })]; if !invalid_props.is_empty() { - propstats.push(PropstatType::NotFound(PropstatElement { + propstats.push(PropstatWrapper::TagList(PropstatElement { status: format!("HTTP/1.1 {}", StatusCode::NOT_FOUND), prop: invalid_props .into_iter() @@ -186,7 +152,7 @@ impl HandlePropfind for R { .into(), })); } - Ok(PropstatResponseElement { + Ok(ResponseElement { href: path, propstat: propstats, }) diff --git a/crates/dav/src/xml/multistatus.rs b/crates/dav/src/xml/multistatus.rs index 2c465e6..01be91c 100644 --- a/crates/dav/src/xml/multistatus.rs +++ b/crates/dav/src/xml/multistatus.rs @@ -1,16 +1,60 @@ +use crate::xml::TagList; use actix_web::{ body::BoxBody, http::header::ContentType, HttpRequest, HttpResponse, Responder, ResponseError, }; use log::debug; use serde::Serialize; +// Intermediate struct because of a serde limitation, see following article: +// https://stackoverflow.com/questions/78444158/unsupportedcannot-serialize-enum-newtype-variant-exampledata #[derive(Serialize)] -#[serde(rename = "multistatus")] +pub struct PropTagWrapper { + #[serde(rename = "$value")] + pub prop: Vec, +} + +// #[derive(Serialize)] +// #[serde(untagged)] +// pub enum PropWrapper { +// Prop(Vec), +// TagList(TagList), +// } + +// RFC 2518 +// +#[derive(Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct PropstatElement { + pub prop: PropType, + pub status: String, +} + +#[derive(Serialize)] +#[serde(untagged)] +pub enum PropstatWrapper { + Normal(PropstatElement>), + TagList(PropstatElement), +} + +// RFC 2518 +// +#[derive(Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct ResponseElement { + pub href: String, + pub propstat: Vec, +} + +// RFC 2518 +// +#[derive(Serialize)] +#[serde(rename = "multistatus", rename_all = "kebab-case")] pub struct MultistatusElement { #[serde(rename = "response")] - pub responses: Vec, + pub responses: Vec>, #[serde(rename = "response")] - pub member_responses: Vec, + pub member_responses: Vec>, #[serde(rename = "@xmlns")] pub ns_dav: &'static str, #[serde(rename = "@xmlns:C")]