mirror of
https://github.com/lennart-k/rustical.git
synced 2026-01-30 22:28:22 +00:00
carddav: Implement addressbook-query
This commit is contained in:
@@ -22,7 +22,7 @@ base64.workspace = true
|
|||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
rustical_dav.workspace = true
|
rustical_dav = { workspace = true, features = ["ical"] }
|
||||||
rustical_store.workspace = true
|
rustical_store.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
rustical_xml.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::{
|
use crate::{
|
||||||
CardDavPrincipalUri, Error, address_object::AddressObjectPropWrapperName,
|
CardDavPrincipalUri, Error,
|
||||||
addressbook::AddressbookResourceService,
|
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 addressbook_multiget::{AddressbookMultigetRequest, handle_addressbook_multiget};
|
||||||
use axum::{
|
use axum::{
|
||||||
@@ -8,19 +16,30 @@ use axum::{
|
|||||||
extract::{OriginalUri, Path, State},
|
extract::{OriginalUri, Path, State},
|
||||||
response::IntoResponse,
|
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_store::{AddressbookStore, SubscriptionStore, auth::Principal};
|
||||||
use rustical_xml::{XmlDeserialize, XmlDocument};
|
use rustical_xml::{XmlDeserialize, XmlDocument};
|
||||||
use sync_collection::handle_sync_collection;
|
use sync_collection::handle_sync_collection;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
mod addressbook_multiget;
|
mod addressbook_multiget;
|
||||||
|
mod addressbook_query;
|
||||||
mod sync_collection;
|
mod sync_collection;
|
||||||
|
|
||||||
#[derive(XmlDeserialize, XmlDocument, Clone, Debug, PartialEq)]
|
#[derive(XmlDeserialize, XmlDocument, Clone, Debug, PartialEq)]
|
||||||
pub(crate) enum ReportRequest {
|
pub(crate) enum ReportRequest {
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||||
AddressbookMultiget(AddressbookMultigetRequest),
|
AddressbookMultiget(AddressbookMultigetRequest),
|
||||||
|
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||||
|
AddressbookQuery(AddressbookQueryRequest),
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||||
SyncCollection(SyncCollectionRequest<AddressObjectPropWrapperName>),
|
SyncCollection(SyncCollectionRequest<AddressObjectPropWrapperName>),
|
||||||
}
|
}
|
||||||
@@ -29,11 +48,49 @@ impl ReportRequest {
|
|||||||
const fn props(&self) -> &PropfindType<AddressObjectPropWrapperName> {
|
const fn props(&self) -> &PropfindType<AddressObjectPropWrapperName> {
|
||||||
match self {
|
match self {
|
||||||
Self::AddressbookMultiget(AddressbookMultigetRequest { prop, .. })
|
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))]
|
#[instrument(skip(addr_store))]
|
||||||
pub async fn route_report_addressbook<AS: AddressbookStore, S: SubscriptionStore>(
|
pub async fn route_report_addressbook<AS: AddressbookStore, S: SubscriptionStore>(
|
||||||
Path((principal, addressbook_id)): Path<(String, String)>,
|
Path((principal, addressbook_id)): Path<(String, String)>,
|
||||||
@@ -75,13 +132,34 @@ pub async fn route_report_addressbook<AS: AddressbookStore, S: SubscriptionStore
|
|||||||
)
|
)
|
||||||
.await?
|
.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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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};
|
use rustical_dav::xml::{PropElement, sync_collection::SyncLevel};
|
||||||
|
|
||||||
#[test]
|
#[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)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn get_vcard(&self) -> &VcardContact {
|
||||||
|
&self.vcard
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user