mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-14 02:22:21 +00:00
caldav: Very basic implementation of the REPORT method
This commit is contained in:
@@ -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 delete;
|
||||||
pub mod mkcalendar;
|
pub mod mkcalendar;
|
||||||
|
pub mod report;
|
||||||
async fn handle_report_calendar_query(
|
|
||||||
query_node: Node<'_, '_>,
|
|
||||||
events: Vec<Event>,
|
|
||||||
prefix: &str,
|
|
||||||
) -> Result<HttpResponse, Error> {
|
|
||||||
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<Vec<_>, _> = 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<A: CheckAuthentication, C: CalendarStore + ?Sized>(
|
|
||||||
context: Data<CalDavContext<C>>,
|
|
||||||
body: String,
|
|
||||||
path: Path<(String, String)>,
|
|
||||||
_auth: AuthInfoExtractor<A>,
|
|
||||||
prefix: Data<ServicePrefix>,
|
|
||||||
) -> Result<HttpResponse, Error> {
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|||||||
205
crates/caldav/src/calendar/methods/report.rs
Normal file
205
crates/caldav/src/calendar/methods/report.rs
Normal file
@@ -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")]
|
||||||
|
// <!ELEMENT calendar-query ((DAV:allprop | DAV:propname | DAV:prop)?, href+)>
|
||||||
|
pub struct CalendarMultigetRequest {
|
||||||
|
#[serde(flatten)]
|
||||||
|
prop: PropfindType,
|
||||||
|
href: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone, Debug)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
struct TimeRangeElement {
|
||||||
|
#[serde(rename = "@start")]
|
||||||
|
start: Option<String>,
|
||||||
|
#[serde(rename = "@end")]
|
||||||
|
end: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone, Debug)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
struct ParamFilterElement {
|
||||||
|
is_not_defined: Option<()>,
|
||||||
|
text_match: Option<TextMatchElement>,
|
||||||
|
|
||||||
|
#[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<TimeRangeElement>,
|
||||||
|
text_match: Option<TextMatchElement>,
|
||||||
|
#[serde(default)]
|
||||||
|
param_filter: Vec<ParamFilterElement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone, Debug)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
struct CompFilterElement {
|
||||||
|
is_not_defined: Option<()>,
|
||||||
|
time_range: Option<TimeRangeElement>,
|
||||||
|
#[serde(default)]
|
||||||
|
prop_filter: Vec<PropFilterElement>,
|
||||||
|
#[serde(default)]
|
||||||
|
comp_filter: Vec<CompFilterElement>,
|
||||||
|
|
||||||
|
#[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")]
|
||||||
|
// <!ELEMENT calendar-query ((DAV:allprop | DAV:propname | DAV:prop)?, filter, timezone?)>
|
||||||
|
pub struct CalendarQueryRequest {
|
||||||
|
#[serde(flatten)]
|
||||||
|
prop: PropfindType,
|
||||||
|
filter: Option<FilterElement>,
|
||||||
|
timezone: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone, Debug)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub enum ReportRequest {
|
||||||
|
CalendarMultiget(CalendarMultigetRequest),
|
||||||
|
CalendarQuery(CalendarQueryRequest),
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_events_calendar_query<C: CalendarStore + ?Sized>(
|
||||||
|
cal_query: CalendarQueryRequest,
|
||||||
|
cid: &str,
|
||||||
|
store: &RwLock<C>,
|
||||||
|
) -> Result<Vec<Event>, Error> {
|
||||||
|
// TODO: Implement filtering
|
||||||
|
Ok(store.read().await.get_events(cid).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_events_calendar_multiget<C: CalendarStore + ?Sized>(
|
||||||
|
cal_query: CalendarMultigetRequest,
|
||||||
|
cid: &str,
|
||||||
|
store: &RwLock<C>,
|
||||||
|
) -> Result<Vec<Event>, 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<A: CheckAuthentication, C: CalendarStore + ?Sized>(
|
||||||
|
path: Path<(String, String)>,
|
||||||
|
body: String,
|
||||||
|
auth: AuthInfoExtractor<A>,
|
||||||
|
req: HttpRequest,
|
||||||
|
cal_store: Data<RwLock<C>>,
|
||||||
|
prefix: Data<ServicePrefix>,
|
||||||
|
) -> Result<HttpResponse, Error> {
|
||||||
|
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::<String>::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))
|
||||||
|
}
|
||||||
@@ -55,7 +55,7 @@ pub fn configure_dav<A: CheckAuthentication, C: CalendarStore + ?Sized>(
|
|||||||
)
|
)
|
||||||
.service(
|
.service(
|
||||||
web::resource("/{principal}/{calendar}")
|
web::resource("/{principal}/{calendar}")
|
||||||
.route(report_method().to(calendar::methods::route_report_calendar::<A, C>))
|
.route(report_method().to(calendar::methods::report::route_report_calendar::<A, C>))
|
||||||
.route(propfind_method().to(route_propfind::<A, CalendarResource<C>>))
|
.route(propfind_method().to(route_propfind::<A, CalendarResource<C>>))
|
||||||
.route(
|
.route(
|
||||||
mkcalendar_method().to(calendar::methods::mkcalendar::route_mkcol_calendar::<A, C>),
|
mkcalendar_method().to(calendar::methods::mkcalendar::route_mkcol_calendar::<A, C>),
|
||||||
|
|||||||
22
crates/caldav/tests/calendar_methods_report.rs
Normal file
22
crates/caldav/tests/calendar_methods_report.rs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
use rustical_caldav::calendar::methods::report::CalendarQueryRequest;
|
||||||
|
|
||||||
|
const CALENDAR_QUERY: &str = r#"
|
||||||
|
<calendar-query xmlns="urn:ietf:params:xml:ns:caldav" xmlns:D="DAV:">
|
||||||
|
<D:prop>
|
||||||
|
<D:getetag />
|
||||||
|
</D:prop>
|
||||||
|
<filter>
|
||||||
|
<comp-filter name="VCALENDAR">
|
||||||
|
<comp-filter name="VEVENT">
|
||||||
|
<time-range start="20240423T105630Z" end="20240702T105630Z" />
|
||||||
|
</comp-filter>
|
||||||
|
</comp-filter>
|
||||||
|
</filter>
|
||||||
|
</calendar-query>
|
||||||
|
"#;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_calendar_query() {
|
||||||
|
let query: CalendarQueryRequest = quick_xml::de::from_str(CALENDAR_QUERY).unwrap();
|
||||||
|
dbg!(query);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user