mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 22:52:22 +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 mkcalendar;
|
||||
|
||||
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
|
||||
}
|
||||
pub mod report;
|
||||
|
||||
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(
|
||||
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(
|
||||
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