From f78f3e81942c89b6cede827284cbb3d70932005f Mon Sep 17 00:00:00 2001 From: Lennart <18233294+lennart-k@users.noreply.github.com> Date: Thu, 20 Jun 2024 19:40:01 +0200 Subject: [PATCH] Add basic framework for PROPPATCH implementation --- crates/caldav/src/calendar/resource.rs | 131 ++++++++++++-------- crates/caldav/src/event/resource.rs | 45 ++++--- crates/caldav/src/lib.rs | 13 +- crates/caldav/src/principal/mod.rs | 61 +++++---- crates/caldav/src/root/mod.rs | 43 ++++--- crates/dav/src/lib.rs | 1 + crates/dav/src/proppatch.rs | 165 +++++++++++++++++++++++++ crates/dav/src/resource.rs | 42 ++++--- 8 files changed, 372 insertions(+), 129 deletions(-) create mode 100644 crates/dav/src/proppatch.rs diff --git a/crates/caldav/src/calendar/resource.rs b/crates/caldav/src/calendar/resource.rs index f04acab..110eb5a 100644 --- a/crates/caldav/src/calendar/resource.rs +++ b/crates/caldav/src/calendar/resource.rs @@ -3,11 +3,11 @@ use actix_web::{web::Data, HttpRequest}; use anyhow::anyhow; use async_trait::async_trait; use rustical_auth::AuthInfo; -use rustical_dav::resource::{Resource, ResourceService}; +use rustical_dav::resource::{InvalidProperty, Resource, ResourceService}; use rustical_dav::xml_snippets::{HrefElement, TextNode}; use rustical_store::calendar::Calendar; use rustical_store::CalendarStore; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use std::sync::Arc; use strum::{EnumString, IntoStaticStr, VariantNames}; use tokio::sync::RwLock; @@ -19,54 +19,54 @@ pub struct CalendarResource { pub calendar_id: String, } -#[derive(Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] pub struct SupportedCalendarComponent { #[serde(rename = "@name")] - pub name: &'static str, + pub name: String, } -#[derive(Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] pub struct SupportedCalendarComponentSet { #[serde(rename = "C:comp")] pub comp: Vec, } -#[derive(Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] pub struct CalendarData { #[serde(rename = "@content-type")] - content_type: &'static str, + content_type: String, #[serde(rename = "@version")] - version: &'static str, + version: String, } impl Default for CalendarData { fn default() -> Self { Self { - content_type: "text/calendar", - version: "2.0", + content_type: "text/calendar".to_owned(), + version: "2.0".to_owned(), } } } -#[derive(Serialize, Default)] +#[derive(Debug, Clone, Deserialize, Serialize, Default)] #[serde(rename_all = "kebab-case")] pub struct SupportedCalendarData { - #[serde(rename = "C:calendar-data")] + #[serde(rename = "C:calendar-data", alias = "calendar-data")] calendar_data: CalendarData, } -#[derive(Serialize, Default)] +#[derive(Debug, Clone, Deserialize, Serialize, Default)] #[serde(rename_all = "kebab-case")] pub struct Resourcetype { - #[serde(rename = "C:calendar")] + #[serde(rename = "C:calendar", alias = "calendar")] calendar: (), collection: (), } -#[derive(Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] pub enum UserPrivilege { Read, @@ -79,7 +79,7 @@ pub enum UserPrivilege { Unbind, } -#[derive(Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] pub struct UserPrivilegeWrapper { #[serde(rename = "$value")] @@ -92,7 +92,7 @@ impl From for UserPrivilegeWrapper { } } -#[derive(Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] pub struct UserPrivilegeSet { privilege: Vec, @@ -117,7 +117,7 @@ impl Default for UserPrivilegeSet { #[derive(EnumString, Debug, VariantNames, IntoStaticStr, Clone)] #[strum(serialize_all = "kebab-case")] -pub enum CalendarProp { +pub enum CalendarPropName { Resourcetype, CurrentUserPrincipal, Owner, @@ -132,9 +132,9 @@ pub enum CalendarProp { MaxResourceSize, } -#[derive(Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] -pub enum CalendarPropResponse { +pub enum CalendarProp { Resourcetype(Resourcetype), CurrentUserPrincipal(HrefElement), Owner(HrefElement), @@ -158,8 +158,17 @@ pub enum CalendarPropResponse { Getcontenttype(TextNode), MaxResourceSize(TextNode), CurrentUserPrivilegeSet(UserPrivilegeSet), + #[serde(other)] + Invalid, } +impl InvalidProperty for CalendarProp { + fn invalid_property(&self) -> bool { + matches!(self, Self::Invalid) + } +} + +#[derive(Clone, Debug)] pub struct CalendarFile { pub calendar: Calendar, pub principal: String, @@ -167,60 +176,69 @@ pub struct CalendarFile { } impl Resource for CalendarFile { - type PropType = CalendarProp; - type PropResponse = CalendarPropResponse; + type PropName = CalendarPropName; + type Prop = CalendarProp; type Error = Error; - fn get_prop( - &self, - prefix: &str, - prop: Self::PropType, - ) -> Result { + fn get_prop(&self, prefix: &str, prop: Self::PropName) -> Result { match prop { - CalendarProp::Resourcetype => { - Ok(CalendarPropResponse::Resourcetype(Resourcetype::default())) + CalendarPropName::Resourcetype => { + Ok(CalendarProp::Resourcetype(Resourcetype::default())) } - CalendarProp::CurrentUserPrincipal => Ok(CalendarPropResponse::CurrentUserPrincipal( + CalendarPropName::CurrentUserPrincipal => Ok(CalendarProp::CurrentUserPrincipal( HrefElement::new(format!("{}/{}/", prefix, self.principal)), )), - CalendarProp::Owner => Ok(CalendarPropResponse::Owner(HrefElement::new(format!( + CalendarPropName::Owner => Ok(CalendarProp::Owner(HrefElement::new(format!( "{}/{}/", prefix, self.principal )))), - CalendarProp::Displayname => Ok(CalendarPropResponse::Displayname(TextNode( + CalendarPropName::Displayname => Ok(CalendarProp::Displayname(TextNode( self.calendar.name.clone(), ))), - CalendarProp::CalendarColor => Ok(CalendarPropResponse::CalendarColor(TextNode( + CalendarPropName::CalendarColor => Ok(CalendarProp::CalendarColor(TextNode( self.calendar.color.clone(), ))), - CalendarProp::CalendarDescription => Ok(CalendarPropResponse::CalendarDescription( + CalendarPropName::CalendarDescription => Ok(CalendarProp::CalendarDescription( TextNode(self.calendar.description.clone()), )), - CalendarProp::CalendarOrder => Ok(CalendarPropResponse::CalendarOrder(TextNode( + CalendarPropName::CalendarOrder => Ok(CalendarProp::CalendarOrder(TextNode( format!("{}", self.calendar.order).into(), ))), - CalendarProp::SupportedCalendarComponentSet => { - Ok(CalendarPropResponse::SupportedCalendarComponentSet( - SupportedCalendarComponentSet { - comp: vec![SupportedCalendarComponent { name: "VEVENT" }], - }, - )) - } - CalendarProp::SupportedCalendarData => Ok(CalendarPropResponse::SupportedCalendarData( + CalendarPropName::SupportedCalendarComponentSet => Ok( + CalendarProp::SupportedCalendarComponentSet(SupportedCalendarComponentSet { + comp: vec![SupportedCalendarComponent { + name: "VEVENT".to_owned(), + }], + }), + ), + CalendarPropName::SupportedCalendarData => Ok(CalendarProp::SupportedCalendarData( SupportedCalendarData::default(), )), - CalendarProp::Getcontenttype => Ok(CalendarPropResponse::Getcontenttype(TextNode( - Some("text/calendar;charset=utf-8".to_owned()), - ))), - CalendarProp::MaxResourceSize => Ok(CalendarPropResponse::MaxResourceSize(TextNode( - Some("10000000".to_owned()), - ))), - CalendarProp::CurrentUserPrivilegeSet => Ok( - CalendarPropResponse::CurrentUserPrivilegeSet(UserPrivilegeSet::default()), - ), + CalendarPropName::Getcontenttype => Ok(CalendarProp::Getcontenttype(TextNode(Some( + "text/calendar;charset=utf-8".to_owned(), + )))), + CalendarPropName::MaxResourceSize => Ok(CalendarProp::MaxResourceSize(TextNode(Some( + "10000000".to_owned(), + )))), + CalendarPropName::CurrentUserPrivilegeSet => Ok(CalendarProp::CurrentUserPrivilegeSet( + UserPrivilegeSet::default(), + )), } } + fn set_prop(&mut self, prop: Self::Prop) -> Result<(), rustical_dav::Error> { + match prop { + CalendarProp::CalendarColor(color) => { + self.calendar.color = color.0; + } + CalendarProp::Displayname(TextNode(name)) => { + self.calendar.name = name; + } + _ => return Err(rustical_dav::Error::PropReadOnly), + } + Ok(()) + } + fn get_path(&self) -> &str { &self.path } @@ -274,4 +292,13 @@ impl ResourceService for CalendarResource { cal_store, }) } + + async fn save_file(&self, file: Self::File) -> Result<(), Self::Error> { + self.cal_store + .write() + .await + .update_calendar(self.calendar_id.to_owned(), file.calendar) + .await?; + Ok(()) + } } diff --git a/crates/caldav/src/event/resource.rs b/crates/caldav/src/event/resource.rs index f7881cf..a430e0b 100644 --- a/crates/caldav/src/event/resource.rs +++ b/crates/caldav/src/event/resource.rs @@ -3,11 +3,11 @@ use actix_web::{web::Data, HttpRequest}; use anyhow::anyhow; use async_trait::async_trait; use rustical_auth::AuthInfo; -use rustical_dav::resource::{Resource, ResourceService}; +use rustical_dav::resource::{InvalidProperty, Resource, ResourceService}; use rustical_dav::xml_snippets::TextNode; use rustical_store::event::Event; use rustical_store::CalendarStore; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use std::sync::Arc; use strum::{EnumString, IntoStaticStr, VariantNames}; use tokio::sync::RwLock; @@ -21,52 +21,59 @@ pub struct EventResource { #[derive(EnumString, Debug, VariantNames, IntoStaticStr, Clone)] #[strum(serialize_all = "kebab-case")] -pub enum EventProp { +pub enum EventPropName { Getetag, CalendarData, Getcontenttype, } -#[derive(Serialize)] +#[derive(Deserialize, Serialize, Debug, Clone, IntoStaticStr)] #[serde(rename_all = "kebab-case")] -pub enum EventPropResponse { +pub enum EventProp { Getetag(TextNode), #[serde(rename = "C:calendar-data")] CalendarData(TextNode), Getcontenttype(TextNode), + #[serde(other)] + Invalid, } +impl InvalidProperty for EventProp { + fn invalid_property(&self) -> bool { + matches!(self, Self::Invalid) + } +} + +#[derive(Clone)] pub struct EventFile { pub event: Event, pub path: String, } impl Resource for EventFile { - type PropType = EventProp; - type PropResponse = EventPropResponse; + type PropName = EventPropName; + type Prop = EventProp; type Error = Error; fn get_path(&self) -> &str { &self.path } - fn get_prop( - &self, - _prefix: &str, - prop: Self::PropType, - ) -> Result { + fn get_prop(&self, _prefix: &str, prop: Self::PropName) -> Result { match prop { - EventProp::Getetag => Ok(EventPropResponse::Getetag(TextNode(Some( - self.event.get_etag(), - )))), - EventProp::CalendarData => Ok(EventPropResponse::CalendarData(TextNode(Some( + EventPropName::Getetag => Ok(EventProp::Getetag(TextNode(Some(self.event.get_etag())))), + EventPropName::CalendarData => Ok(EventProp::CalendarData(TextNode(Some( self.event.get_ics().to_owned(), )))), - EventProp::Getcontenttype => Ok(EventPropResponse::Getcontenttype(TextNode(Some( + EventPropName::Getcontenttype => Ok(EventProp::Getcontenttype(TextNode(Some( "text/calendar;charset=utf-8".to_owned(), )))), } } + + fn set_prop(&mut self, _prop: Self::Prop) -> Result<(), rustical_dav::Error> { + Err(rustical_dav::Error::PropReadOnly) + } } #[async_trait(?Send)] @@ -116,4 +123,8 @@ impl ResourceService for EventResource { path: self.path.to_owned(), }) } + + async fn save_file(&self, _file: Self::File) -> Result<(), Self::Error> { + Err(Error::NotImplemented) + } } diff --git a/crates/caldav/src/lib.rs b/crates/caldav/src/lib.rs index 91fe970..ea0e3f5 100644 --- a/crates/caldav/src/lib.rs +++ b/crates/caldav/src/lib.rs @@ -7,6 +7,7 @@ use principal::PrincipalResource; use root::RootResource; use rustical_auth::CheckAuthentication; use rustical_dav::propfind::{route_propfind, ServicePrefix}; +use rustical_dav::proppatch::route_proppatch; use rustical_store::CalendarStore; use std::str::FromStr; use std::sync::Arc; @@ -35,6 +36,7 @@ pub fn configure_dav( store: Arc>, ) { let propfind_method = || web::method(Method::from_str("PROPFIND").unwrap()); + let proppatch_method = || web::method(Method::from_str("PROPPATCH").unwrap()); let report_method = || web::method(Method::from_str("REPORT").unwrap()); let mkcalendar_method = || web::method(Method::from_str("MKCALENDAR").unwrap()); @@ -50,15 +52,21 @@ pub fn configure_dav( .guard(guard::Method(Method::OPTIONS)) .to(options_handler), ) - .service(web::resource("").route(propfind_method().to(route_propfind::))) + .service( + web::resource("") + .route(propfind_method().to(route_propfind::)) + .route(proppatch_method().to(route_proppatch::)), + ) .service( web::resource("/{principal}") - .route(propfind_method().to(route_propfind::>)), + .route(propfind_method().to(route_propfind::>)) + .route(proppatch_method().to(route_proppatch::>)), ) .service( web::resource("/{principal}/{calendar}") .route(report_method().to(calendar::methods::report::route_report_calendar::)) .route(propfind_method().to(route_propfind::>)) + .route(proppatch_method().to(route_proppatch::>)) .route( mkcalendar_method().to(calendar::methods::mkcalendar::route_mkcol_calendar::), ) @@ -70,6 +78,7 @@ pub fn configure_dav( .service( web::resource("/{principal}/{calendar}/{event}") .route(propfind_method().to(route_propfind::>)) + .route(proppatch_method().to(route_proppatch::>)) .route(web::method(Method::DELETE).to(event::methods::delete_event::)) .route(web::method(Method::GET).to(event::methods::get_event::)) .route(web::method(Method::PUT).to(event::methods::put_event::)), diff --git a/crates/caldav/src/principal/mod.rs b/crates/caldav/src/principal/mod.rs index ceeec4b..a74ec0b 100644 --- a/crates/caldav/src/principal/mod.rs +++ b/crates/caldav/src/principal/mod.rs @@ -4,12 +4,12 @@ use actix_web::HttpRequest; use anyhow::anyhow; use async_trait::async_trait; use rustical_auth::AuthInfo; -use rustical_dav::resource::{Resource, ResourceService}; +use rustical_dav::resource::{InvalidProperty, Resource, ResourceService}; use rustical_dav::xml_snippets::HrefElement; use rustical_store::CalendarStore; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use std::sync::Arc; -use strum::{EnumString, IntoStaticStr, VariantNames}; +use strum::{AsRefStr, EnumString, VariantNames}; use tokio::sync::RwLock; use crate::calendar::resource::CalendarFile; @@ -20,21 +20,22 @@ pub struct PrincipalResource { cal_store: Arc>, } +#[derive(Clone)] pub struct PrincipalFile { principal: String, path: String, } -#[derive(Serialize, Default)] +#[derive(Deserialize, Serialize, Default, Debug)] #[serde(rename_all = "kebab-case")] pub struct Resourcetype { principal: (), collection: (), } -#[derive(Serialize)] +#[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "kebab-case")] -pub enum PrincipalPropResponse { +pub enum PrincipalProp { Resourcetype(Resourcetype), CurrentUserPrincipal(HrefElement), #[serde(rename = "principal-URL")] @@ -43,11 +44,19 @@ pub enum PrincipalPropResponse { CalendarHomeSet(HrefElement), #[serde(rename = "C:calendar-user-address-set")] CalendarUserAddressSet(HrefElement), + #[serde(other)] + Invalid, } -#[derive(EnumString, Debug, VariantNames, IntoStaticStr, Clone)] +impl InvalidProperty for PrincipalProp { + fn invalid_property(&self) -> bool { + matches!(self, Self::Invalid) + } +} + +#[derive(EnumString, Debug, VariantNames, AsRefStr, Clone)] #[strum(serialize_all = "kebab-case")] -pub enum PrincipalProp { +pub enum PrincipalPropName { Resourcetype, CurrentUserPrincipal, #[strum(serialize = "principal-URL")] @@ -58,36 +67,34 @@ pub enum PrincipalProp { #[async_trait(?Send)] impl Resource for PrincipalFile { - type PropType = PrincipalProp; - type PropResponse = PrincipalPropResponse; + type PropName = PrincipalPropName; + type Prop = PrincipalProp; type Error = Error; - fn get_prop( - &self, - prefix: &str, - prop: Self::PropType, - ) -> Result { + fn get_prop(&self, prefix: &str, prop: Self::PropName) -> Result { match prop { - PrincipalProp::Resourcetype => { - Ok(PrincipalPropResponse::Resourcetype(Resourcetype::default())) + PrincipalPropName::Resourcetype => { + Ok(PrincipalProp::Resourcetype(Resourcetype::default())) } - PrincipalProp::CurrentUserPrincipal => Ok(PrincipalPropResponse::CurrentUserPrincipal( + PrincipalPropName::CurrentUserPrincipal => Ok(PrincipalProp::CurrentUserPrincipal( HrefElement::new(format!("{}/{}/", prefix, self.principal)), )), - PrincipalProp::PrincipalUrl => Ok(PrincipalPropResponse::PrincipalUrl( + PrincipalPropName::PrincipalUrl => Ok(PrincipalProp::PrincipalUrl(HrefElement::new( + format!("{}/{}/", prefix, self.principal), + ))), + PrincipalPropName::CalendarHomeSet => Ok(PrincipalProp::CalendarHomeSet( HrefElement::new(format!("{}/{}/", prefix, self.principal)), )), - PrincipalProp::CalendarHomeSet => Ok(PrincipalPropResponse::CalendarHomeSet( + PrincipalPropName::CalendarUserAddressSet => Ok(PrincipalProp::CalendarUserAddressSet( HrefElement::new(format!("{}/{}/", prefix, self.principal)), )), - PrincipalProp::CalendarUserAddressSet => { - Ok(PrincipalPropResponse::CalendarUserAddressSet( - HrefElement::new(format!("{}/{}/", prefix, self.principal)), - )) - } } } + fn set_prop(&mut self, _prop: Self::Prop) -> Result<(), rustical_dav::Error> { + Err(rustical_dav::Error::PropReadOnly) + } + fn get_path(&self) -> &str { &self.path } @@ -147,4 +154,8 @@ impl ResourceService for PrincipalResource { }) .collect()) } + + async fn save_file(&self, _file: Self::File) -> Result<(), Self::Error> { + Err(Error::NotImplemented) + } } diff --git a/crates/caldav/src/root/mod.rs b/crates/caldav/src/root/mod.rs index 819c5da..2bdd3e6 100644 --- a/crates/caldav/src/root/mod.rs +++ b/crates/caldav/src/root/mod.rs @@ -2,9 +2,9 @@ use crate::Error; use actix_web::HttpRequest; use async_trait::async_trait; use rustical_auth::AuthInfo; -use rustical_dav::resource::{Resource, ResourceService}; +use rustical_dav::resource::{InvalidProperty, Resource, ResourceService}; use rustical_dav::xml_snippets::HrefElement; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use strum::{EnumString, IntoStaticStr, VariantNames}; pub struct RootResource { @@ -14,47 +14,56 @@ pub struct RootResource { #[derive(EnumString, Debug, VariantNames, IntoStaticStr, Clone)] #[strum(serialize_all = "kebab-case")] -pub enum RootProp { +pub enum RootPropName { Resourcetype, CurrentUserPrincipal, } -#[derive(Serialize, Default)] +#[derive(Deserialize, Serialize, Default, Debug)] #[serde(rename_all = "kebab-case")] pub struct Resourcetype { collection: (), } -#[derive(Serialize)] +#[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "kebab-case")] -pub enum RootPropResponse { +pub enum RootProp { Resourcetype(Resourcetype), CurrentUserPrincipal(HrefElement), + #[serde(other)] + Invalid, } +impl InvalidProperty for RootProp { + fn invalid_property(&self) -> bool { + matches!(self, Self::Invalid) + } +} + +#[derive(Clone)] pub struct RootFile { pub principal: String, pub path: String, } impl Resource for RootFile { - type PropType = RootProp; - type PropResponse = RootPropResponse; + type PropName = RootPropName; + type Prop = RootProp; type Error = Error; - fn get_prop( - &self, - prefix: &str, - prop: Self::PropType, - ) -> Result { + fn get_prop(&self, prefix: &str, prop: Self::PropName) -> Result { match prop { - RootProp::Resourcetype => Ok(RootPropResponse::Resourcetype(Resourcetype::default())), - RootProp::CurrentUserPrincipal => Ok(RootPropResponse::CurrentUserPrincipal( + RootPropName::Resourcetype => Ok(RootProp::Resourcetype(Resourcetype::default())), + RootPropName::CurrentUserPrincipal => Ok(RootProp::CurrentUserPrincipal( HrefElement::new(format!("{}/{}/", prefix, self.principal)), )), } } + fn set_prop(&mut self, _prop: Self::Prop) -> Result<(), rustical_dav::Error> { + Err(rustical_dav::Error::PropReadOnly) + } + fn get_path(&self) -> &str { &self.path } @@ -91,4 +100,8 @@ impl ResourceService for RootResource { principal: self.principal.to_owned(), }) } + + async fn save_file(&self, _file: Self::File) -> Result<(), Self::Error> { + Err(Error::NotImplemented) + } } diff --git a/crates/dav/src/lib.rs b/crates/dav/src/lib.rs index f0d862c..b7311ee 100644 --- a/crates/dav/src/lib.rs +++ b/crates/dav/src/lib.rs @@ -2,6 +2,7 @@ pub mod depth_extractor; pub mod error; pub mod namespace; pub mod propfind; +pub mod proppatch; pub mod resource; pub mod xml; pub mod xml_snippets; diff --git a/crates/dav/src/proppatch.rs b/crates/dav/src/proppatch.rs new file mode 100644 index 0000000..746f97a --- /dev/null +++ b/crates/dav/src/proppatch.rs @@ -0,0 +1,165 @@ +use crate::namespace::Namespace; +use crate::propfind::MultistatusElement; +use crate::resource::InvalidProperty; +use crate::resource::Resource; +use crate::resource::ResourceService; +use crate::resource::{PropstatElement, PropstatResponseElement, PropstatType}; +use crate::xml::tag_list::TagList; +use crate::xml::tag_name::TagName; +use crate::Error; +use actix_web::http::header::ContentType; +use actix_web::http::StatusCode; +use actix_web::{web::Path, HttpRequest, HttpResponse}; +use rustical_auth::{AuthInfoExtractor, CheckAuthentication}; +use serde::{Deserialize, Serialize}; + +// https://docs.rs/quick-xml/latest/quick_xml/de/index.html#normal-enum-variant +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "kebab-case")] +struct PropertyElement { + #[serde(rename = "$value")] + prop: T, +} + +#[derive(Deserialize, Clone, Debug)] +#[serde(rename_all = "kebab-case")] +struct SetPropertyElement { + prop: PropertyElement, +} + +#[derive(Deserialize, Clone, Debug)] +#[serde(rename_all = "kebab-case")] +struct RemovePropertyElement { + #[serde(rename = "$value")] + prop: TagName, +} + +#[derive(Deserialize, Clone, Debug)] +#[serde(rename_all = "kebab-case")] +struct PropertyupdateElement { + #[serde(default = "Vec::new")] + set: Vec>, + #[serde(default = "Vec::new")] + remove: Vec, +} + +pub async fn route_proppatch( + path: Path, + body: String, + req: HttpRequest, + auth: AuthInfoExtractor, +) -> Result { + let auth_info = auth.inner; + let path_components = path.into_inner(); + let href = req.path().to_owned(); + let resource_service = R::new(req, auth_info.clone(), path_components.clone()).await?; + + // TODO: Implement remove! + let PropertyupdateElement::<::Prop> { + set: set_els, + remove: remove_els, + } = quick_xml::de::from_str(&body).map_err(Error::XmlDecodeError)?; + + // Extract all property names without verification + // Weird workaround because quick_xml doesn't allow untagged enums + let propnames: Vec = quick_xml::de::from_str::>(&body) + .map_err(Error::XmlDecodeError)? + .set + .into_iter() + .map(|set_el| set_el.prop.prop.into()) + .collect(); + + // Invalid properties + let props_not_found: Vec = propnames + .iter() + .zip(&set_els) + .filter_map( + |( + name, + SetPropertyElement { + prop: PropertyElement { prop }, + }, + )| { + if prop.invalid_property() { + Some(name.to_string()) + } else { + None + } + }, + ) + .collect(); + + // Filter out invalid props + let set_props: Vec<::Prop> = set_els + .into_iter() + .filter_map( + |SetPropertyElement { + prop: PropertyElement { prop }, + }| { + if prop.invalid_property() { + None + } else { + Some(prop) + } + }, + ) + .collect(); + + let mut resource = resource_service.get_file().await?; + + let mut props_ok = Vec::new(); + let mut props_conflict = Vec::new(); + + for (prop, propname) in set_props.into_iter().zip(propnames) { + match resource.set_prop(prop) { + Ok(()) => { + props_ok.push(propname); + } + Err(Error::PropReadOnly) => { + props_conflict.push(propname); + } + Err(err) => { + return Err(err.into()); + } + }; + } + + if props_not_found.is_empty() && props_conflict.is_empty() { + // Only save if no errors occured + resource_service.save_file(resource).await?; + } + + let mut output = "\n".to_owned(); + let mut ser = quick_xml::se::Serializer::new(&mut output); + ser.indent(' ', 4); + MultistatusElement { + responses: vec![PropstatResponseElement { + href, + propstat: vec![ + PropstatType::Normal(PropstatElement { + prop: TagList::from(props_ok), + status: format!("HTTP/1.1 {}", StatusCode::OK), + }), + PropstatType::NotFound(PropstatElement { + prop: TagList::from(props_not_found), + status: format!("HTTP/1.1 {}", StatusCode::NOT_FOUND), + }), + PropstatType::Conflict(PropstatElement { + prop: TagList::from(props_conflict), + status: format!("HTTP/1.1 {}", StatusCode::CONFLICT), + }), + ], + }], + // Dummy just for typing + 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/dav/src/resource.rs b/crates/dav/src/resource.rs index 6d06c32..b4b7e00 100644 --- a/crates/dav/src/resource.rs +++ b/crates/dav/src/resource.rs @@ -2,29 +2,32 @@ use crate::xml::tag_list::TagList; use crate::Error; use actix_web::{http::StatusCode, HttpRequest, ResponseError}; use async_trait::async_trait; +use core::fmt; use itertools::Itertools; use rustical_auth::AuthInfo; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use std::str::FromStr; use strum::VariantNames; #[async_trait(?Send)] -pub trait Resource { - type PropType: FromStr + VariantNames + Clone; - type PropResponse: Serialize; +pub trait Resource: Clone { + type PropName: FromStr + VariantNames + Clone; + type Prop: Serialize + for<'de> Deserialize<'de> + fmt::Debug + InvalidProperty; type Error: ResponseError + From + From; fn list_dead_props() -> &'static [&'static str] { - Self::PropType::VARIANTS + Self::PropName::VARIANTS } - fn get_prop( - &self, - prefix: &str, - prop: Self::PropType, - ) -> Result; + fn get_prop(&self, prefix: &str, prop: Self::PropName) -> Result; fn get_path(&self) -> &str; + + fn set_prop(&mut self, prop: Self::Prop) -> Result<(), crate::Error>; +} + +pub trait InvalidProperty { + fn invalid_property(&self) -> bool; } // A resource is identified by a URI and has properties @@ -47,6 +50,8 @@ pub trait ResourceService: Sized { async fn get_file(&self) -> Result; async fn get_members(&self, auth_info: AuthInfo) -> Result, Self::Error>; + + async fn save_file(&self, file: Self::File) -> Result<(), Self::Error>; } #[derive(Serialize)] @@ -57,23 +62,24 @@ pub struct PropWrapper { #[derive(Serialize)] #[serde(rename_all = "kebab-case")] -struct PropstatElement { - prop: T, - status: String, +pub struct PropstatElement { + pub prop: T, + pub status: String, } #[derive(Serialize)] #[serde(rename_all = "kebab-case")] pub struct PropstatResponseElement { - href: String, - propstat: Vec>, + pub href: String, + pub propstat: Vec>, } #[derive(Serialize)] #[serde(untagged)] -enum PropstatType { +pub enum PropstatType { Normal(PropstatElement), NotFound(PropstatElement), + Conflict(PropstatElement), } #[async_trait(?Send)] @@ -92,7 +98,7 @@ impl HandlePropfind for R { &self, prefix: &str, props: Vec<&str>, - ) -> Result>, TagList>, R::Error> { + ) -> Result>, TagList>, R::Error> { let mut props = props; if props.contains(&"propname") { if props.len() != 1 { @@ -117,7 +123,7 @@ impl HandlePropfind for R { let mut invalid_props = Vec::new(); let mut prop_responses = Vec::new(); for prop in props { - if let Ok(valid_prop) = R::PropType::from_str(prop) { + if let Ok(valid_prop) = R::PropName::from_str(prop) { let response = self.get_prop(prefix, valid_prop.clone())?; prop_responses.push(response); } else {