lots of changes

This commit is contained in:
Lennart
2024-03-15 20:53:41 +01:00
parent b89c954752
commit ebf826f5b0
8 changed files with 97 additions and 80 deletions

View File

@@ -37,14 +37,16 @@ pub struct SupportedCalendarComponentSet {
#[derive(Serialize)] #[derive(Serialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct CalendarData { pub struct CalendarData {
#[serde(rename = "@content-type")]
content_type: &'static str, content_type: &'static str,
#[serde(rename = "@version")]
version: &'static str, version: &'static str,
} }
impl Default for CalendarData { impl Default for CalendarData {
fn default() -> Self { fn default() -> Self {
Self { Self {
content_type: "text/calendar;charset=utf-8", content_type: "text/calendar",
version: "2.0", version: "2.0",
} }
} }
@@ -121,13 +123,13 @@ pub enum CalendarProp {
CurrentUserPrincipal, CurrentUserPrincipal,
Owner, Owner,
Displayname, Displayname,
#[strum(props(tagname = "IC:calendar-color"))] // #[strum(props(tagname = "IC:calendar-color"))]
CalendarColor, CalendarColor,
#[strum(props(tagname = "C:calendar-description"))] // #[strum(props(tagname = "C:calendar-description"))]
CalendarDescription, CalendarDescription,
#[strum(props(tagname = "C:supported-calendar-component-set"))] // #[strum(props(tagname = "C:supported-calendar-component-set"))]
SupportedCalendarComponentSet, SupportedCalendarComponentSet,
#[strum(props(tagname = "C:supported-calendar-data"))] // #[strum(props(tagname = "C:supported-calendar-data"))]
SupportedCalendarData, SupportedCalendarData,
Getcontenttype, Getcontenttype,
CurrentUserPrivilegeSet, CurrentUserPrivilegeSet,
@@ -141,9 +143,19 @@ pub enum CalendarPropResponse {
CurrentUserPrincipal(HrefElement), CurrentUserPrincipal(HrefElement),
Owner(HrefElement), Owner(HrefElement),
Displayname(TextNode), Displayname(TextNode),
#[serde(rename = "IC:calendar-color", alias = "calendar-color")]
CalendarColor(TextNode), CalendarColor(TextNode),
#[serde(rename = "C:calendar-description", alias = "calendar-description")]
CalendarDescription(TextNode), CalendarDescription(TextNode),
#[serde(
rename = "C:supported-calendar-component-set",
alias = "supported-calendar-component-set"
)]
SupportedCalendarComponentSet(SupportedCalendarComponentSet), SupportedCalendarComponentSet(SupportedCalendarComponentSet),
#[serde(
rename = "C:supported-calendar-data",
alias = "supported-calendar-data"
)]
SupportedCalendarData(SupportedCalendarData), SupportedCalendarData(SupportedCalendarData),
Getcontenttype(TextNode), Getcontenttype(TextNode),
MaxResourceSize(TextNode), MaxResourceSize(TextNode),

View File

@@ -29,7 +29,7 @@ pub struct Resourcetype {
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub enum RootPropResponse { pub enum RootPropResponse {
Resourcetype(Resourcetype), Resourcetype(Resourcetype),
CurrentUser(HrefElement), CurrentUserPrincipal(HrefElement),
} }
#[async_trait(?Send)] #[async_trait(?Send)]
@@ -63,9 +63,9 @@ impl Resource for RootResource {
fn get_prop(&self, prop: Self::PropType) -> Result<Self::PropResponse> { fn get_prop(&self, prop: Self::PropType) -> Result<Self::PropResponse> {
match prop { match prop {
RootProp::Resourcetype => Ok(RootPropResponse::Resourcetype(Resourcetype::default())), RootProp::Resourcetype => Ok(RootPropResponse::Resourcetype(Resourcetype::default())),
RootProp::CurrentUserPrincipal => Ok(RootPropResponse::CurrentUser(HrefElement::new( RootProp::CurrentUserPrincipal => Ok(RootPropResponse::CurrentUserPrincipal(
format!("{}/{}/", self.prefix, self.principal), HrefElement::new(format!("{}/{}/", self.prefix, self.principal)),
))), )),
} }
} }
} }

View File

@@ -5,7 +5,6 @@ use actix_web::http::header::ContentType;
use actix_web::web::{Data, Path}; use actix_web::web::{Data, Path};
use actix_web::{HttpRequest, HttpResponse}; use actix_web::{HttpRequest, HttpResponse};
use anyhow::Result; use anyhow::Result;
use quick_xml::events::BytesText;
use roxmltree::{Node, NodeType}; use roxmltree::{Node, NodeType};
use rustical_auth::{AuthInfoExtractor, CheckAuthentication}; use rustical_auth::{AuthInfoExtractor, CheckAuthentication};
use rustical_dav::namespace::Namespace; use rustical_dav::namespace::Namespace;
@@ -51,22 +50,28 @@ async fn handle_report_calendar_query<C: CalendarStore + ?Sized>(
.map(|node| node.tag_name().name()) .map(|node| node.tag_name().name())
.collect(); .collect();
let mut event_results = Vec::new(); let event_resources: Vec<_> = events
for event in events { .iter()
let path = format!("{}/{}", request.path(), event.get_uid()); .map(|event| {
let event_resource = EventResource { let path = format!("{}/{}", request.path(), event.get_uid());
cal_store: cal_store.clone(), EventResource {
path: path.clone(), cal_store: cal_store.clone(),
event, path: path.clone(),
}; event: event.clone(),
event_results.push(event_resource.propfind(props.clone())?); }
} })
.collect();
let event_results: Result<Vec<_>, _> = 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| { let output = generate_multistatus(vec![Namespace::Dav, Namespace::CalDAV], |writer| {
for result in event_results { for result in event_responses {
writer.write_event(quick_xml::events::Event::Text(BytesText::from_escaped( writer
result, .write_serializable("response", &result)
)))?; .map_err(|_e| quick_xml::Error::TextNotFound)?;
} }
Ok(()) Ok(())
})?; })?;
@@ -90,8 +95,6 @@ pub async fn route_report_calendar<A: CheckAuthentication, C: CalendarStore + ?S
let query_node = doc.root_element(); let query_node = doc.root_element();
let events = context.store.read().await.get_events(&cid).await.unwrap(); let events = context.store.read().await.get_events(&cid).await.unwrap();
dbg!(&body);
// TODO: implement filtering // TODO: implement filtering
match query_node.tag_name().name() { match query_node.tag_name().name() {
"calendar-query" => {} "calendar-query" => {}

View File

@@ -4,13 +4,13 @@ use actix_web::http::StatusCode;
use actix_web::web::{Data, Path}; use actix_web::web::{Data, Path};
use actix_web::{HttpRequest, HttpResponse}; use actix_web::{HttpRequest, HttpResponse};
use anyhow::Result; use anyhow::Result;
use quick_xml::events::BytesText;
use rustical_auth::{AuthInfoExtractor, CheckAuthentication}; use rustical_auth::{AuthInfoExtractor, CheckAuthentication};
use rustical_dav::depth_extractor::Depth; use rustical_dav::depth_extractor::Depth;
use rustical_dav::namespace::Namespace; use rustical_dav::namespace::Namespace;
use rustical_dav::resource::{HandlePropfind, Resource}; use rustical_dav::resource::{HandlePropfind, Resource};
use rustical_dav::xml_snippets::generate_multistatus; use rustical_dav::xml_snippets::generate_multistatus;
use rustical_store::calendar::CalendarStore; use rustical_store::calendar::CalendarStore;
use serde::Serialize;
use thiserror::Error; use thiserror::Error;
#[derive(Debug, Error)] #[derive(Debug, Error)]
@@ -76,7 +76,6 @@ pub async fn route_propfind<A: CheckAuthentication, R: Resource, C: CalendarStor
depth: Depth, depth: Depth,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let props = parse_propfind(&body)?; let props = parse_propfind(&body)?;
// let req_path = req.path().to_string();
let auth_info = auth.inner; let auth_info = auth.inner;
let resource = R::acquire_from_request( let resource = R::acquire_from_request(
@@ -87,21 +86,26 @@ pub async fn route_propfind<A: CheckAuthentication, R: Resource, C: CalendarStor
) )
.await?; .await?;
let mut responses = vec![resource.propfind(props.clone())?]; let response = resource.propfind(props.clone())?;
let members = resource.get_members().await?;
let mut member_responses = Vec::new();
if depth != Depth::Zero { if depth != Depth::Zero {
for member in resource.get_members().await? { for member in &members {
responses.push(member.propfind(props.clone())?); member_responses.push(member.propfind(props.clone())?);
} }
} }
let output = generate_multistatus( let output = generate_multistatus(
vec![Namespace::Dav, Namespace::CalDAV, Namespace::ICal], vec![Namespace::Dav, Namespace::CalDAV, Namespace::ICal],
|writer| { |writer| {
for response in responses { writer
writer.write_event(quick_xml::events::Event::Text(BytesText::from_escaped( .write_serializable("response", &response)
response, .map_err(|_e| quick_xml::Error::TextNotFound)?;
)))?; for response in member_responses {
writer
.write_serializable("response", &response)
.map_err(|_e| quick_xml::Error::TextNotFound)?;
} }
Ok(()) Ok(())
}, },
@@ -111,3 +115,17 @@ pub async fn route_propfind<A: CheckAuthentication, R: Resource, C: CalendarStor
.content_type(ContentType::xml()) .content_type(ContentType::xml())
.body(output)) .body(output))
} }
#[derive(Serialize)]
struct MultistatusElement<T1: Serialize, T2: Serialize> {
#[serde(rename = "$value")]
responses: Vec<T1>,
#[serde(rename = "$value")]
member_responses: Vec<T2>,
#[serde(rename = "@xmlns")]
ns_dav: &'static str,
#[serde(rename = "@xmlns:C")]
ns_caldav: &'static str,
#[serde(rename = "@xmlns:IC")]
ns_ical: &'static str,
}

View File

@@ -12,4 +12,5 @@ futures-util = "0.3"
quick-xml = "0.31" quick-xml = "0.31"
rustical_auth = { path = "../auth/" } rustical_auth = { path = "../auth/" }
serde = { version = "1.0.197", features = ["derive"] } serde = { version = "1.0.197", features = ["derive"] }
strum = "0.26.2" strum = "0.26"
itertools = "0.12"

View File

@@ -1,5 +1,4 @@
pub mod depth_extractor; pub mod depth_extractor;
pub mod namespace; pub mod namespace;
pub mod resource; pub mod resource;
pub mod tagname;
pub mod xml_snippets; pub mod xml_snippets;

View File

@@ -1,7 +1,7 @@
use actix_web::{http::StatusCode, HttpRequest}; use actix_web::{http::StatusCode, HttpRequest};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use async_trait::async_trait; use async_trait::async_trait;
use quick_xml::Writer; use itertools::Itertools;
use rustical_auth::AuthInfo; use rustical_auth::AuthInfo;
use serde::Serialize; use serde::Serialize;
use std::str::FromStr; use std::str::FromStr;
@@ -51,18 +51,25 @@ struct PropstatElement<T: Serialize> {
#[derive(Serialize)] #[derive(Serialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
struct PropstatResponseElement<T: Serialize> { struct PropstatResponseElement<T1: Serialize, T2: Serialize> {
href: String, href: String,
propstat: PropstatElement<T>, propstat: Vec<PropstatType<T1, T2>>,
}
#[derive(Serialize)]
#[serde(untagged)]
enum PropstatType<T1: Serialize, T2: Serialize> {
Normal(PropstatElement<T1>),
NotFound(PropstatElement<T2>),
} }
pub trait HandlePropfind { pub trait HandlePropfind {
fn propfind(&self, props: Vec<&str>) -> Result<String>; fn propfind(&self, props: Vec<&str>) -> Result<impl Serialize>;
} }
impl<R: Resource> HandlePropfind for R { impl<R: Resource> HandlePropfind for R {
fn propfind(&self, props: Vec<&str>) -> Result<String> { fn propfind(&self, props: Vec<&str>) -> Result<impl Serialize> {
let mut props = props; let mut props = props.into_iter().unique().collect_vec();
if props.contains(&"allprops") { if props.contains(&"allprops") {
if props.len() != 1 { if props.len() != 1 {
// allprops MUST be the only queried prop per spec // allprops MUST be the only queried prop per spec
@@ -71,9 +78,6 @@ impl<R: Resource> HandlePropfind for R {
props = R::list_dead_props().into(); 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 invalid_props = Vec::<&str>::new();
let mut prop_responses = Vec::new(); let mut prop_responses = Vec::new();
for prop in props { for prop in props {
@@ -89,31 +93,22 @@ impl<R: Resource> HandlePropfind for R {
} }
} }
writer.write_serializable( let mut propstats = Vec::new();
"response", propstats.push(PropstatType::Normal(PropstatElement {
&PropstatResponseElement { status: format!("HTTP/1.1 {}", StatusCode::OK),
href: self.get_path().to_owned(), prop: PropWrapper {
propstat: PropstatElement { prop: prop_responses,
status: format!("HTTP/1.1 {}", StatusCode::OK),
prop: PropWrapper {
prop: prop_responses,
},
},
}, },
)?; }));
if !invalid_props.is_empty() { if !invalid_props.is_empty() {
// TODO: proper error reporting propstats.push(PropstatType::NotFound(PropstatElement {
writer.write_serializable( status: format!("HTTP/1.1 {}", StatusCode::NOT_FOUND),
"response", prop: TagList(invalid_props.iter().map(|&s| s.to_owned()).collect()),
&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()),
},
},
)?;
} }
Ok(std::str::from_utf8(&output_buffer)?.to_string()) Ok(PropstatResponseElement {
href: self.get_path().to_owned(),
propstat: propstats,
})
} }
} }

View File

@@ -1,11 +0,0 @@
use strum::EnumProperty;
pub trait TagName {
fn tagname(self) -> &'static str;
}
impl<P: EnumProperty + Into<&'static str>> TagName for P {
fn tagname(self) -> &'static str {
self.get_str("tagname").unwrap_or(self.into())
}
}