use http::Uri;
use crate::{
Depth, PropertyName,
dav::{Propfind, WebDavError},
encoding::normalise_percent_encoded,
names,
requests::{DavRequest, ParseResponseError, PreparedRequest},
xmlutils::{check_multistatus, validate_xml_response},
};
pub struct FindPropertyHrefs<'a> {
resource: Uri,
property: &'a PropertyName<'a, 'a>,
}
impl<'a> FindPropertyHrefs<'a> {
#[must_use]
pub fn new(resource: &Uri, property: &'a PropertyName<'a, 'a>) -> Self {
Self {
resource: resource.clone(),
property,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FindPropertyHrefsResponse {
pub hrefs: Vec<Uri>,
}
impl DavRequest for FindPropertyHrefs<'_> {
type Response = FindPropertyHrefsResponse;
type ParseError = ParseResponseError;
type Error<E> = WebDavError<E>;
fn prepare_request(&self) -> Result<PreparedRequest, http::Error> {
let path = self
.resource
.path_and_query()
.map_or("/", http::uri::PathAndQuery::as_str);
Propfind::new(path)
.with_properties(&[self.property])
.with_depth(Depth::Zero)
.prepare_request()
}
fn parse_response(
&self,
parts: &http::response::Parts,
body: &[u8],
) -> Result<Self::Response, ParseResponseError> {
let doc = validate_xml_response(parts, body)?;
let root = doc.root_element();
let props = root
.descendants()
.filter(|node| node.tag_name() == *self.property)
.collect::<Vec<_>>();
if props.len() > 1 {
return Err(ParseResponseError::InvalidResponse(
"expected at most one property in response".into(),
));
}
check_multistatus(root)?;
let Some(prop) = props.first() else {
return Ok(FindPropertyHrefsResponse { hrefs: Vec::new() });
};
let mut hrefs = Vec::new();
let href_nodes = prop
.children()
.filter(|node| node.tag_name() == names::HREF);
for href_node in href_nodes {
if let Some(href_text) = href_node.text() {
let normalized = normalise_percent_encoded(href_text)
.map_err(|e| ParseResponseError::InvalidResponse(e.to_string()))?;
let uri = if let Ok(absolute_uri) = normalized.parse::<Uri>() {
absolute_uri
} else {
let mut parts = self.resource.clone().into_parts();
parts.path_and_query =
Some(normalized.parse().map_err(|_| {
ParseResponseError::InvalidResponse("invalid href".into())
})?);
Uri::from_parts(parts)
.map_err(|e| ParseResponseError::InvalidResponse(e.to_string()))?
};
hrefs.push(uri);
}
}
Ok(FindPropertyHrefsResponse { hrefs })
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::names;
use http::{Method, StatusCode};
#[test]
fn test_prepare_request_caldav_property() {
let resource = Uri::from_static("https://example.com/principals/user/");
let req = FindPropertyHrefs::new(&resource, &names::CALENDAR_HOME_SET);
let prepared = req.prepare_request().unwrap();
assert_eq!(prepared.method, Method::from_bytes(b"PROPFIND").unwrap());
assert_eq!(prepared.path, "/principals/user/");
assert!(prepared.body.contains("<C:calendar-home-set/>"));
assert_eq!(
prepared.headers,
vec![
("Depth".to_string(), "0".to_string()),
(
"Content-Type".to_string(),
"application/xml; charset=utf-8".to_string()
)
]
);
}
#[test]
fn test_prepare_request_carddav_property() {
let resource = Uri::from_static("https://example.com/principals/user/");
let req = FindPropertyHrefs::new(&resource, &names::ADDRESSBOOK_HOME_SET);
let prepared = req.prepare_request().unwrap();
assert_eq!(prepared.method, Method::from_bytes(b"PROPFIND").unwrap());
assert_eq!(prepared.path, "/principals/user/");
assert!(prepared.body.contains(r"<CARD:addressbook-home-set/>"));
assert_eq!(
prepared.headers,
vec![
("Depth".to_string(), "0".to_string()),
(
"Content-Type".to_string(),
"application/xml; charset=utf-8".to_string()
)
]
);
}
#[test]
fn test_prepare_request_dav_property() {
let resource = Uri::from_static("https://example.com/principals/user/");
let req = FindPropertyHrefs::new(&resource, &names::CURRENT_USER_PRINCIPAL);
let prepared = req.prepare_request().unwrap();
assert_eq!(prepared.method, Method::from_bytes(b"PROPFIND").unwrap());
assert!(prepared.body.contains("xmlns:D="));
assert!(prepared.body.contains(r"<D:current-user-principal/>"));
}
#[test]
fn test_parse_response_single_href() {
let resource = Uri::from_static("https://example.com/principals/user/");
let req = FindPropertyHrefs::new(&resource, &names::CALENDAR_HOME_SET);
let response_xml = r#"<?xml version="1.0"?>
<multistatus xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
<response>
<href>/principals/user/</href>
<propstat>
<prop>
<C:calendar-home-set>
<href>/calendars/user/</href>
</C:calendar-home-set>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
</multistatus>"#;
let response = http::Response::builder()
.status(StatusCode::MULTI_STATUS)
.body(())
.unwrap();
let (parts, ()) = response.into_parts();
let result = req.parse_response(&parts, response_xml.as_bytes()).unwrap();
assert_eq!(result.hrefs.len(), 1);
assert_eq!(result.hrefs[0].path(), "/calendars/user/");
}
#[test]
fn test_parse_response_multiple_hrefs() {
let resource = Uri::from_static("https://example.com/principals/user/");
let req = FindPropertyHrefs::new(&resource, &names::CALENDAR_HOME_SET);
let response_xml = r#"<?xml version="1.0"?>
<multistatus xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
<response>
<href>/principals/user/</href>
<propstat>
<prop>
<C:calendar-home-set>
<href>/calendars/user/</href>
<href>/shared-calendars/</href>
</C:calendar-home-set>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
</multistatus>"#;
let response = http::Response::builder()
.status(StatusCode::MULTI_STATUS)
.body(())
.unwrap();
let (parts, ()) = response.into_parts();
let result = req.parse_response(&parts, response_xml.as_bytes()).unwrap();
assert_eq!(result.hrefs.len(), 2);
assert_eq!(result.hrefs[0].path(), "/calendars/user/");
assert_eq!(result.hrefs[1].path(), "/shared-calendars/");
}
#[test]
fn test_parse_response_absolute_hrefs() {
let resource = Uri::from_static("https://example.com/principals/user/");
let req = FindPropertyHrefs::new(&resource, &names::CALENDAR_HOME_SET);
let response_xml = r#"<?xml version="1.0"?>
<multistatus xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
<response>
<href>/principals/user/</href>
<propstat>
<prop>
<C:calendar-home-set>
<href>https://calendar.example.com/user/</href>
</C:calendar-home-set>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
</multistatus>"#;
let response = http::Response::builder()
.status(StatusCode::MULTI_STATUS)
.body(())
.unwrap();
let (parts, ()) = response.into_parts();
let result = req.parse_response(&parts, response_xml.as_bytes()).unwrap();
assert_eq!(result.hrefs.len(), 1);
assert_eq!(
result.hrefs[0],
"https://calendar.example.com/user/".parse::<Uri>().unwrap()
);
}
#[test]
fn test_parse_response_mixed_hrefs() {
let resource = Uri::from_static("https://example.com/principals/user/");
let req = FindPropertyHrefs::new(&resource, &names::CALENDAR_HOME_SET);
let response_xml = r#"<?xml version="1.0"?>
<multistatus xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
<response>
<href>/principals/user/</href>
<propstat>
<prop>
<C:calendar-home-set>
<href>/calendars/user/</href>
<href>https://calendar.example.com/shared/</href>
</C:calendar-home-set>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
</multistatus>"#;
let response = http::Response::builder()
.status(StatusCode::MULTI_STATUS)
.body(())
.unwrap();
let (parts, ()) = response.into_parts();
let result = req.parse_response(&parts, response_xml.as_bytes()).unwrap();
assert_eq!(result.hrefs.len(), 2);
assert_eq!(result.hrefs[0].path(), "/calendars/user/");
assert_eq!(
result.hrefs[1],
"https://calendar.example.com/shared/"
.parse::<Uri>()
.unwrap()
);
}
#[test]
fn test_parse_response_bad_status() {
let resource = Uri::from_static("https://example.com/principals/user/");
let req = FindPropertyHrefs::new(&resource, &names::CALENDAR_HOME_SET);
let response_xml = b"";
let response = http::Response::builder()
.status(StatusCode::NOT_FOUND)
.body(())
.unwrap();
let (parts, ()) = response.into_parts();
let result = req.parse_response(&parts, response_xml);
assert!(result.is_err());
match result {
Err(ParseResponseError::BadStatusCode(status)) => {
assert_eq!(status, StatusCode::NOT_FOUND);
}
_ => panic!("Expected BadStatusCode error"),
}
}
#[test]
fn test_parse_response_missing_property() {
let resource = Uri::from_static("https://example.com/principals/user/");
let req = FindPropertyHrefs::new(&resource, &names::CALENDAR_HOME_SET);
let response_xml = r#"<?xml version="1.0"?>
<multistatus xmlns="DAV:">
<response>
<href>/principals/user/</href>
<propstat>
<prop>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
</multistatus>"#;
let response = http::Response::builder()
.status(StatusCode::MULTI_STATUS)
.body(())
.unwrap();
let (parts, ()) = response.into_parts();
let result = req.parse_response(&parts, response_xml.as_bytes()).unwrap();
assert!(result.hrefs.is_empty());
}
#[test]
fn test_parse_response_property_not_found_404() {
let resource = Uri::from_static("https://example.com/principals/user/");
let req = FindPropertyHrefs::new(&resource, &names::CALENDAR_HOME_SET);
let response_xml = r#"<?xml version="1.0"?>
<multistatus xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
<response>
<href>/principals/user/</href>
<propstat>
<prop>
<C:calendar-home-set/>
</prop>
<status>HTTP/1.1 404 Not Found</status>
</propstat>
</response>
</multistatus>"#;
let response = http::Response::builder()
.status(StatusCode::MULTI_STATUS)
.body(())
.unwrap();
let (parts, ()) = response.into_parts();
let result = req.parse_response(&parts, response_xml.as_bytes());
assert!(result.is_err());
match result {
Err(ParseResponseError::BadStatusCode(status)) => {
assert_eq!(status, StatusCode::NOT_FOUND);
}
_ => panic!("Expected BadStatusCode(404) error"),
}
}
#[test]
fn test_parse_response_multiple_properties() {
let resource = Uri::from_static("https://example.com/principals/user/");
let req = FindPropertyHrefs::new(&resource, &names::CALENDAR_HOME_SET);
let response_xml = r#"<?xml version="1.0"?>
<multistatus xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
<response>
<href>/principals/user/</href>
<propstat>
<prop>
<C:calendar-home-set>
<href>/calendars/user/</href>
</C:calendar-home-set>
<C:calendar-home-set>
<href>/other/</href>
</C:calendar-home-set>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
</multistatus>"#;
let response = http::Response::builder()
.status(StatusCode::MULTI_STATUS)
.body(())
.unwrap();
let (parts, ()) = response.into_parts();
let result = req.parse_response(&parts, response_xml.as_bytes());
assert!(result.is_err());
match result {
Err(ParseResponseError::InvalidResponse(msg)) => {
assert!(msg.contains("expected at most one property"));
}
_ => panic!("Expected InvalidResponse error"),
}
}
}