Compare commits

..

4 Commits

Author SHA1 Message Date
Lennart
455b4c405f version 0.4.13 2025-07-10 21:39:28 +02:00
Lennart
2774d092ac propfind: Implement <include/>
Implements #95
2025-07-10 15:45:54 +02:00
Lennart
32b616fd75 xml serialize_to_string: Enable indentation 2025-07-10 15:45:07 +02:00
Lennart K
b02f7c427a minor refactoring 2025-07-10 10:51:59 +02:00
18 changed files with 96 additions and 67 deletions

22
Cargo.lock generated
View File

@@ -2999,7 +2999,7 @@ dependencies = [
[[package]]
name = "rustical"
version = "0.4.12"
version = "0.4.13"
dependencies = [
"anyhow",
"argon2",
@@ -3042,7 +3042,7 @@ dependencies = [
[[package]]
name = "rustical_caldav"
version = "0.4.12"
version = "0.4.13"
dependencies = [
"async-std",
"async-trait",
@@ -3080,7 +3080,7 @@ dependencies = [
[[package]]
name = "rustical_carddav"
version = "0.4.12"
version = "0.4.13"
dependencies = [
"async-trait",
"axum",
@@ -3112,7 +3112,7 @@ dependencies = [
[[package]]
name = "rustical_dav"
version = "0.4.12"
version = "0.4.13"
dependencies = [
"async-trait",
"axum",
@@ -3137,7 +3137,7 @@ dependencies = [
[[package]]
name = "rustical_dav_push"
version = "0.4.12"
version = "0.4.13"
dependencies = [
"async-trait",
"axum",
@@ -3163,7 +3163,7 @@ dependencies = [
[[package]]
name = "rustical_frontend"
version = "0.4.12"
version = "0.4.13"
dependencies = [
"askama",
"askama_web",
@@ -3196,7 +3196,7 @@ dependencies = [
[[package]]
name = "rustical_ical"
version = "0.4.12"
version = "0.4.13"
dependencies = [
"axum",
"chrono",
@@ -3214,7 +3214,7 @@ dependencies = [
[[package]]
name = "rustical_oidc"
version = "0.4.12"
version = "0.4.13"
dependencies = [
"async-trait",
"axum",
@@ -3229,7 +3229,7 @@ dependencies = [
[[package]]
name = "rustical_store"
version = "0.4.12"
version = "0.4.13"
dependencies = [
"anyhow",
"async-trait",
@@ -3263,7 +3263,7 @@ dependencies = [
[[package]]
name = "rustical_store_sqlite"
version = "0.4.12"
version = "0.4.13"
dependencies = [
"async-trait",
"chrono",
@@ -3284,7 +3284,7 @@ dependencies = [
[[package]]
name = "rustical_xml"
version = "0.4.12"
version = "0.4.13"
dependencies = [
"quick-xml",
"thiserror 2.0.12",

View File

@@ -2,7 +2,7 @@
members = ["crates/*"]
[workspace.package]
version = "0.4.12"
version = "0.4.13"
edition = "2024"
description = "A CalDAV server"
repository = "https://github.com/lennart-k/rustical"

View File

@@ -67,7 +67,7 @@ fn objects_response(
object,
principal: principal.to_owned(),
}
.propfind(&path, prop, puri, user)?,
.propfind(&path, prop, None, puri, user)?,
);
}

View File

@@ -39,7 +39,7 @@ pub async fn handle_sync_collection<C: CalendarStore>(
object,
principal: principal.to_owned(),
}
.propfind(&path, &sync_collection.prop, puri, user)?,
.propfind(&path, &sync_collection.prop, None, puri, user)?,
);
}

View File

@@ -69,7 +69,6 @@ impl Resource for CalendarObjectResource {
}
fn get_displayname(&self) -> Option<&str> {
// TODO: Extract summary from object
None
}

View File

@@ -47,7 +47,7 @@ pub async fn route_get<AS: AddressbookStore, S: SubscriptionStore>(
let mut resp = Response::builder().status(StatusCode::OK);
let hdrs = resp.headers_mut().unwrap();
hdrs.typed_insert(ContentType::from_str("text/vcard").unwrap());
let filename = format!("{}_{}.vcf", principal, addressbook_id);
let filename = format!("{principal}_{addressbook_id}.vcf");
let filename = utf8_percent_encode(&filename, CONTROLS);
hdrs.insert(
header::CONTENT_DISPOSITION,

View File

@@ -88,15 +88,8 @@ pub async fn route_mkcol<AS: AddressbookStore, S: SubscriptionStore>(
}
}
match addr_store.insert_addressbook(addressbook).await {
// TODO: The spec says we should return a mkcol-response.
// However, it works without one but breaks on iPadOS when using an empty one :)
Ok(()) => Ok(StatusCode::CREATED.into_response()),
Err(err) => {
dbg!(err.to_string());
Err(err.into())
}
}
addr_store.insert_addressbook(addressbook).await?;
Ok(StatusCode::CREATED.into_response())
}
#[cfg(test)]

View File

@@ -81,7 +81,7 @@ pub async fn handle_addressbook_multiget<AS: AddressbookStore>(
object,
principal: principal.to_owned(),
}
.propfind(&path, prop, puri, user)?,
.propfind(&path, prop, None, puri, user)?,
);
}

View File

@@ -39,7 +39,7 @@ pub async fn handle_sync_collection<AS: AddressbookStore>(
object,
principal: principal.to_owned(),
}
.propfind(&path, &sync_collection.prop, puri, user)?,
.propfind(&path, &sync_collection.prop, None, puri, user)?,
);
}

View File

@@ -64,6 +64,7 @@ pub(crate) async fn route_propfind<R: ResourceService>(
} else {
PropfindElement {
prop: PropfindType::Allprop,
include: None,
}
};
let propfind_member: PropfindElement<<<R::MemberType as Resource>::Prop as PropName>::Names> =
@@ -72,6 +73,7 @@ pub(crate) async fn route_propfind<R: ResourceService>(
} else {
PropfindElement {
prop: PropfindType::Allprop,
include: None,
}
};
@@ -82,13 +84,20 @@ pub(crate) async fn route_propfind<R: ResourceService>(
member_responses.push(member.propfind(
&format!("{}/{}", path.trim_end_matches('/'), member.get_name()),
&propfind_member.prop,
propfind_member.include.as_ref(),
puri,
principal,
)?);
}
}
let response = resource.propfind(path, &propfind_self.prop, puri, principal)?;
let response = resource.propfind(
path,
&propfind_self.prop,
propfind_self.include.as_ref(),
puri,
principal,
)?;
Ok(MultistatusElement {
responses: vec![response],

View File

@@ -106,6 +106,7 @@ pub trait Resource: Clone + Send + 'static {
&self,
path: &str,
prop: &PropfindType<<Self::Prop as PropName>::Names>,
include: Option<&PropElement<<Self::Prop as PropName>::Names>>,
principal_uri: &impl PrincipalUri,
principal: &Self::Principal,
) -> Result<ResponseElement<Self::Prop>, Self::Error> {
@@ -115,36 +116,40 @@ pub trait Resource: Clone + Send + 'static {
path.push('/');
}
// TODO: Support include element
let (props, invalid_props): (HashSet<<Self::Prop as PropName>::Names>, Vec<_>) = match prop
{
PropfindType::Propname => {
let props = Self::list_props()
.into_iter()
.map(|(ns, tag)| (ns.map(NamespaceOwned::from), tag.to_string()))
.collect_vec();
let (mut props, mut invalid_props): (HashSet<<Self::Prop as PropName>::Names>, Vec<_>) =
match prop {
PropfindType::Propname => {
let props = Self::list_props()
.into_iter()
.map(|(ns, tag)| (ns.map(NamespaceOwned::from), tag.to_string()))
.collect_vec();
return Ok(ResponseElement {
href: path.to_owned(),
propstat: vec![PropstatWrapper::TagList(PropstatElement {
prop: TagList::from(props),
status: StatusCode::OK,
})],
..Default::default()
});
}
PropfindType::Allprop => (
Self::list_props()
.iter()
.map(|(_ns, name)| <Self::Prop as PropName>::Names::from_str(name).unwrap())
.collect(),
vec![],
),
PropfindType::Prop(PropElement(valid_tags, invalid_tags)) => (
valid_tags.iter().cloned().collect(),
invalid_tags.to_owned(),
),
};
return Ok(ResponseElement {
href: path.to_owned(),
propstat: vec![PropstatWrapper::TagList(PropstatElement {
prop: TagList::from(props),
status: StatusCode::OK,
})],
..Default::default()
});
}
PropfindType::Allprop => (
Self::list_props()
.iter()
.map(|(_ns, name)| <Self::Prop as PropName>::Names::from_str(name).unwrap())
.collect(),
vec![],
),
PropfindType::Prop(PropElement(valid_tags, invalid_tags)) => (
valid_tags.iter().cloned().collect(),
invalid_tags.to_owned(),
),
};
if let Some(PropElement(valid_tags, invalid_tags)) = include {
props.extend(valid_tags.clone());
invalid_props.extend(invalid_tags.to_owned());
}
let prop_responses = props
.into_iter()

View File

@@ -11,10 +11,11 @@ use rustical_xml::XmlRootTag;
pub struct PropfindElement<PN: XmlDeserialize> {
#[xml(ty = "untagged")]
pub prop: PropfindType<PN>,
#[xml(ns = "crate::namespace::NS_DAV")]
pub include: Option<PropElement<PN>>,
}
#[derive(Debug, Clone, PartialEq)]
// pub struct PropElement<PN: XmlDeserialize = Propname>(#[xml(ty = "untagged", flatten)] pub Vec<PN>);
pub struct PropElement<PN: XmlDeserialize>(
// valid
pub Vec<PN>,

View File

@@ -33,7 +33,13 @@ mod tests {
.unwrap();
assert_eq!(
out,
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<document><resourcetype><displayname xmlns=\"DAV:\"/><calendar-color xmlns=\"http://calendarserver.org/ns/\"/></resourcetype></document>"
r#"<?xml version="1.0" encoding="utf-8"?>
<document>
<resourcetype>
<displayname xmlns="DAV:"/>
<calendar-color xmlns="http://calendarserver.org/ns/"/>
</resourcetype>
</document>"#
)
}
}

View File

@@ -80,7 +80,6 @@ export class CreateAddressbookForm extends LitElement {
alert("Empty displayname")
return
}
// TODO: Escape user input: There's not really a security risk here but would be nicer
await this.client.createDirectory(`/principal/${this.principal || this.user}/${this.addr_id}`, {
data: `
<mkcol xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">

View File

@@ -13,7 +13,7 @@ pub enum PrincipalType {
Resource,
Room,
Unknown,
// TODO: X-Name, IANA-token
// X-Name, IANA-token
}
impl TryFrom<&str> for PrincipalType {

View File

@@ -43,7 +43,7 @@ pub trait XmlSerializeRoot {
fn serialize_to_string(&self) -> std::io::Result<String> {
let mut buf: Vec<_> = b"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n".into();
let mut writer = quick_xml::Writer::new(&mut buf);
let mut writer = quick_xml::Writer::new_with_indent(&mut buf, b' ', 4);
self.serialize_root(&mut writer)?;
Ok(String::from_utf8_lossy(&buf).to_string())
}

View File

@@ -22,6 +22,11 @@ fn test_struct_value_tagged() {
.unwrap();
assert_eq!(
out,
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<propfind><prop><test>asd</test></prop></propfind>"
r#"<?xml version="1.0" encoding="utf-8"?>
<propfind>
<prop>
<test>asd</test>
</prop>
</propfind>"#
);
}

View File

@@ -71,7 +71,11 @@ fn test_struct_value_tagged() {
.unwrap();
assert_eq!(
out,
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<document><href>okay</href><num>123</num></document>"
r#"<?xml version="1.0" encoding="utf-8"?>
<document>
<href>okay</href>
<num>123</num>
</document>"#
);
}
@@ -91,7 +95,8 @@ fn test_struct_value_untagged() {
.unwrap();
assert_eq!(
out,
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<document>okays</document>"
r#"<?xml version="1.0" encoding="utf-8"?>
<document>okays</document>"#
);
}
@@ -111,7 +116,11 @@ fn test_struct_vec() {
.unwrap();
assert_eq!(
out,
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<document><href>okay</href><href>wow</href></document>"
r#"<?xml version="1.0" encoding="utf-8"?>
<document>
<href>okay</href>
<href>wow</href>
</document>"#
);
}
@@ -141,7 +150,10 @@ fn test_struct_serialize_with() {
.unwrap();
assert_eq!(
out,
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<document><href>OKAY</href></document>"
r#"<?xml version="1.0" encoding="utf-8"?>
<document>
<href>OKAY</href>
</document>"#
);
}