mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-27 23:09:12 +00:00
carddav: Implement addressbook-query
This commit is contained in:
@@ -22,7 +22,7 @@ base64.workspace = true
|
||||
serde.workspace = true
|
||||
tokio.workspace = true
|
||||
url.workspace = true
|
||||
rustical_dav.workspace = true
|
||||
rustical_dav = { workspace = true, features = ["ical"] }
|
||||
rustical_store.workspace = true
|
||||
chrono.workspace = true
|
||||
rustical_xml.workspace = true
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
use crate::{
|
||||
address_object::AddressObjectPropWrapperName,
|
||||
addressbook::methods::report::addressbook_query::PropFilterElement,
|
||||
};
|
||||
use rustical_dav::xml::{PropfindType, TextMatchElement};
|
||||
use rustical_ical::{AddressObject, UtcDateTime};
|
||||
use rustical_xml::XmlDeserialize;
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
||||
#[allow(dead_code)]
|
||||
pub struct TimeRangeElement {
|
||||
#[xml(ty = "attr")]
|
||||
pub(crate) start: Option<UtcDateTime>,
|
||||
#[xml(ty = "attr")]
|
||||
pub(crate) end: Option<UtcDateTime>,
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
||||
#[allow(dead_code)]
|
||||
// https://www.rfc-editor.org/rfc/rfc4791#section-9.7.3
|
||||
pub struct ParamFilterElement {
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||
pub(crate) is_not_defined: Option<()>,
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||
pub(crate) text_match: Option<TextMatchElement>,
|
||||
|
||||
#[xml(ty = "attr")]
|
||||
pub(crate) name: String,
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
||||
#[allow(dead_code)]
|
||||
// <!ELEMENT filter (prop-filter*)>
|
||||
// <!ATTLIST filter test (anyof | allof) "anyof">
|
||||
// <!-- test value:
|
||||
// anyof logical OR for prop-filter matches
|
||||
// allof logical AND for prop-filter matches -->
|
||||
pub struct FilterElement {
|
||||
#[xml(ty = "attr")]
|
||||
pub anyof: Option<String>,
|
||||
#[xml(ty = "attr")]
|
||||
pub allof: Option<String>,
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV", flatten)]
|
||||
pub(crate) prop_filter: Vec<PropFilterElement>,
|
||||
}
|
||||
|
||||
impl FilterElement {
|
||||
#[must_use]
|
||||
pub fn matches(&self, addr_object: &AddressObject) -> bool {
|
||||
let allof = match (self.allof.is_some(), self.anyof.is_some()) {
|
||||
(true, false) => true,
|
||||
(false, _) => false,
|
||||
(true, true) => panic!("wat"),
|
||||
};
|
||||
let mut results = self
|
||||
.prop_filter
|
||||
.iter()
|
||||
.map(|prop_filter| prop_filter.match_component(addr_object));
|
||||
if allof {
|
||||
results.all(|x| x)
|
||||
} else {
|
||||
results.any(|x| x)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
||||
#[allow(dead_code)]
|
||||
// <!ELEMENT addressbook-query ((DAV:allprop |
|
||||
// DAV:propname |
|
||||
// DAV:prop)?, filter, limit?)>
|
||||
pub struct AddressbookQueryRequest {
|
||||
#[xml(ty = "untagged")]
|
||||
pub prop: PropfindType<AddressObjectPropWrapperName>,
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||
pub(crate) filter: FilterElement,
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
use crate::Error;
|
||||
mod elements;
|
||||
mod prop_filter;
|
||||
pub use elements::*;
|
||||
#[allow(unused_imports)]
|
||||
pub use prop_filter::{PropFilterElement, PropFilterable};
|
||||
use rustical_ical::AddressObject;
|
||||
use rustical_store::AddressbookStore;
|
||||
|
||||
pub async fn get_objects_addressbook_query<AS: AddressbookStore>(
|
||||
addr_query: &AddressbookQueryRequest,
|
||||
principal: &str,
|
||||
addressbook_id: &str,
|
||||
store: &AS,
|
||||
) -> Result<Vec<AddressObject>, Error> {
|
||||
let mut objects = store.get_objects(principal, addressbook_id).await?;
|
||||
objects.retain(|object| addr_query.filter.matches(object));
|
||||
Ok(objects)
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
use super::ParamFilterElement;
|
||||
use ical::{parser::Component, property::Property};
|
||||
use rustical_dav::xml::TextMatchElement;
|
||||
use rustical_ical::AddressObject;
|
||||
use rustical_xml::XmlDeserialize;
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
||||
#[allow(dead_code)]
|
||||
// <!ELEMENT prop-filter (is-not-defined |
|
||||
// (text-match*, param-filter*))>
|
||||
//
|
||||
// <!ATTLIST prop-filter name CDATA #REQUIRED
|
||||
// test (anyof | allof) "anyof">
|
||||
// <!-- name value: a vCard property name (e.g., "NICKNAME")
|
||||
// test value:
|
||||
// anyof logical OR for text-match/param-filter matches
|
||||
// allof logical AND for text-match/param-filter matches -->
|
||||
pub struct PropFilterElement {
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||
pub(crate) is_not_defined: Option<()>,
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV", flatten)]
|
||||
pub(crate) text_match: Vec<TextMatchElement>,
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV", flatten)]
|
||||
pub(crate) param_filter: Vec<ParamFilterElement>,
|
||||
|
||||
#[xml(ty = "attr")]
|
||||
pub(crate) name: String,
|
||||
|
||||
#[xml(ty = "attr")]
|
||||
pub anyof: Option<String>,
|
||||
#[xml(ty = "attr")]
|
||||
pub allof: Option<String>,
|
||||
}
|
||||
|
||||
impl PropFilterElement {
|
||||
pub fn match_component(&self, comp: &impl PropFilterable) -> bool {
|
||||
let property = comp.get_property(&self.name);
|
||||
let _property = match (self.is_not_defined.is_some(), property) {
|
||||
// We are the component that's not supposed to be defined
|
||||
(true, Some(_))
|
||||
// We don't match
|
||||
| (false, None) => return false,
|
||||
// We shall not be and indeed we aren't
|
||||
(true, None) => return true,
|
||||
(false, Some(property)) => property
|
||||
};
|
||||
|
||||
let _allof = match (self.allof.is_some(), self.anyof.is_some()) {
|
||||
(true, false) => true,
|
||||
(false, _) => false,
|
||||
(true, true) => panic!("wat"),
|
||||
};
|
||||
|
||||
// TODO: IMPLEMENT
|
||||
// if let Some(text_match) = &self.text_match
|
||||
// && !text_match.match_property(property)
|
||||
// {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// TODO: param-filter
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PropFilterable {
|
||||
fn get_property(&self, name: &str) -> Option<&Property>;
|
||||
}
|
||||
|
||||
impl PropFilterable for AddressObject {
|
||||
fn get_property(&self, name: &str) -> Option<&Property> {
|
||||
self.get_vcard().get_property(name)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,14 @@
|
||||
use crate::{
|
||||
CardDavPrincipalUri, Error, address_object::AddressObjectPropWrapperName,
|
||||
addressbook::AddressbookResourceService,
|
||||
CardDavPrincipalUri, Error,
|
||||
address_object::{
|
||||
AddressObjectPropWrapper, AddressObjectPropWrapperName, resource::AddressObjectResource,
|
||||
},
|
||||
addressbook::{
|
||||
AddressbookResourceService,
|
||||
methods::report::addressbook_query::{
|
||||
AddressbookQueryRequest, get_objects_addressbook_query,
|
||||
},
|
||||
},
|
||||
};
|
||||
use addressbook_multiget::{AddressbookMultigetRequest, handle_addressbook_multiget};
|
||||
use axum::{
|
||||
@@ -8,19 +16,30 @@ use axum::{
|
||||
extract::{OriginalUri, Path, State},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use rustical_dav::xml::{PropfindType, sync_collection::SyncCollectionRequest};
|
||||
use http::StatusCode;
|
||||
use rustical_dav::{
|
||||
resource::{PrincipalUri, Resource},
|
||||
xml::{
|
||||
MultistatusElement, PropfindType, multistatus::ResponseElement,
|
||||
sync_collection::SyncCollectionRequest,
|
||||
},
|
||||
};
|
||||
use rustical_ical::AddressObject;
|
||||
use rustical_store::{AddressbookStore, SubscriptionStore, auth::Principal};
|
||||
use rustical_xml::{XmlDeserialize, XmlDocument};
|
||||
use sync_collection::handle_sync_collection;
|
||||
use tracing::instrument;
|
||||
|
||||
mod addressbook_multiget;
|
||||
mod addressbook_query;
|
||||
mod sync_collection;
|
||||
|
||||
#[derive(XmlDeserialize, XmlDocument, Clone, Debug, PartialEq)]
|
||||
pub(crate) enum ReportRequest {
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||
AddressbookMultiget(AddressbookMultigetRequest),
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||
AddressbookQuery(AddressbookQueryRequest),
|
||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||
SyncCollection(SyncCollectionRequest<AddressObjectPropWrapperName>),
|
||||
}
|
||||
@@ -29,11 +48,49 @@ impl ReportRequest {
|
||||
const fn props(&self) -> &PropfindType<AddressObjectPropWrapperName> {
|
||||
match self {
|
||||
Self::AddressbookMultiget(AddressbookMultigetRequest { prop, .. })
|
||||
| Self::SyncCollection(SyncCollectionRequest { prop, .. }) => prop,
|
||||
| Self::SyncCollection(SyncCollectionRequest { prop, .. })
|
||||
| Self::AddressbookQuery(AddressbookQueryRequest { prop, .. }) => prop,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn objects_response(
|
||||
objects: Vec<AddressObject>,
|
||||
not_found: Vec<String>,
|
||||
path: &str,
|
||||
principal: &str,
|
||||
puri: &impl PrincipalUri,
|
||||
user: &Principal,
|
||||
prop: &PropfindType<AddressObjectPropWrapperName>,
|
||||
) -> Result<MultistatusElement<AddressObjectPropWrapper, String>, Error> {
|
||||
let mut responses = Vec::new();
|
||||
for object in objects {
|
||||
let path = format!("{}/{}.vcf", path, object.get_id());
|
||||
responses.push(
|
||||
AddressObjectResource {
|
||||
object,
|
||||
principal: principal.to_owned(),
|
||||
}
|
||||
.propfind(&path, prop, None, puri, user)?,
|
||||
);
|
||||
}
|
||||
|
||||
let not_found_responses = not_found
|
||||
.into_iter()
|
||||
.map(|path| ResponseElement {
|
||||
href: path,
|
||||
status: Some(StatusCode::NOT_FOUND),
|
||||
..Default::default()
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(MultistatusElement {
|
||||
responses,
|
||||
member_responses: not_found_responses,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(addr_store))]
|
||||
pub async fn route_report_addressbook<AS: AddressbookStore, S: SubscriptionStore>(
|
||||
Path((principal, addressbook_id)): Path<(String, String)>,
|
||||
@@ -75,13 +132,34 @@ pub async fn route_report_addressbook<AS: AddressbookStore, S: SubscriptionStore
|
||||
)
|
||||
.await?
|
||||
}
|
||||
ReportRequest::AddressbookQuery(addr_query) => {
|
||||
let objects = get_objects_addressbook_query(
|
||||
addr_query,
|
||||
&principal,
|
||||
&addressbook_id,
|
||||
addr_store.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
objects_response(
|
||||
objects,
|
||||
vec![],
|
||||
uri.path(),
|
||||
&principal,
|
||||
&puri,
|
||||
&user,
|
||||
&addr_query.prop,
|
||||
)?
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::address_object::AddressObjectPropName;
|
||||
use crate::{
|
||||
address_object::AddressObjectPropName,
|
||||
addressbook::methods::report::addressbook_query::{FilterElement, PropFilterElement},
|
||||
};
|
||||
use rustical_dav::xml::{PropElement, sync_collection::SyncLevel};
|
||||
|
||||
#[test]
|
||||
@@ -144,4 +222,46 @@ mod tests {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_xml_addressbook_query() {
|
||||
let report_request = ReportRequest::parse_str(
|
||||
r#"
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<card:addressbook-query xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:d="DAV:">
|
||||
<d:prop>
|
||||
<d:getetag/>
|
||||
</d:prop>
|
||||
<card:filter>
|
||||
<card:prop-filter name="FN"/>
|
||||
</card:filter>
|
||||
</card:addressbook-query>
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
report_request,
|
||||
ReportRequest::AddressbookQuery(AddressbookQueryRequest {
|
||||
prop: rustical_dav::xml::PropfindType::Prop(PropElement(
|
||||
vec![AddressObjectPropWrapperName::AddressObject(
|
||||
AddressObjectPropName::Getetag
|
||||
),],
|
||||
vec![]
|
||||
)),
|
||||
filter: FilterElement {
|
||||
anyof: None,
|
||||
allof: None,
|
||||
prop_filter: vec![PropFilterElement {
|
||||
name: "FN".to_owned(),
|
||||
is_not_defined: None,
|
||||
text_match: vec![],
|
||||
param_filter: vec![],
|
||||
allof: None,
|
||||
anyof: None
|
||||
}]
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,4 +179,9 @@ END:VCALENDAR",
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_vcard(&self) -> &VcardContact {
|
||||
&self.vcard
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user