CardDAV addressbook-query

This commit is contained in:
mdecimus 2025-04-05 16:46:47 +02:00
parent a06c94d45d
commit b9d93730c7
17 changed files with 883 additions and 600 deletions

View file

@ -534,6 +534,12 @@ impl DavResources {
})
}
pub fn children(&self, parent_id: u32) -> impl Iterator<Item = &DavResource> {
self.paths
.iter()
.filter(move |item| item.parent_id.is_some_and(|id| id == parent_id))
}
pub fn is_ancestor_of(&self, ancestor: u32, descendant: u32) -> bool {
let ancestor = &self.paths.by_id(ancestor).unwrap().name;
let descendant = &self.paths.by_id(descendant).unwrap().name;

View file

@ -45,34 +45,39 @@
},
"value": {
"ICalendar": {
"component_type": "VCalendar",
"entries": [
"components": [
{
"name": {
"type": "Prodid"
},
"params": [],
"values": [
"component_type": "VCalendar",
"entries": [
{
"type": "Text",
"data": "-//Example Corp.//CalDAV Client//EN"
"name": {
"type": "Prodid"
},
"params": [],
"values": [
{
"type": "Text",
"data": "-//Example Corp.//CalDAV Client//EN"
}
]
},
{
"name": {
"type": "Version"
},
"params": [],
"values": [
{
"type": "Text",
"data": "2.0"
}
]
}
],
"component_ids": [
1
]
},
{
"name": {
"type": "Version"
},
"params": [],
"values": [
{
"type": "Text",
"data": "2.0"
}
]
}
],
"components": [
{
"component_type": "VTimezone",
"entries": [
@ -111,244 +116,246 @@
]
}
],
"components": [
"component_ids": [
2,
3
]
},
{
"component_type": "Standard",
"entries": [
{
"component_type": "Standard",
"entries": [
"name": {
"type": "Dtstart"
},
"params": [],
"values": [
{
"name": {
"type": "Dtstart"
},
"params": [],
"values": [
{
"type": "PartialDateTime",
"data": {
"year": 1967,
"month": 10,
"day": 29,
"hour": 2,
"minute": 0,
"second": 0,
"tz_hour": null,
"tz_minute": null,
"tz_minus": false
}
}
]
},
{
"name": {
"type": "Rrule"
},
"params": [],
"values": [
{
"type": "RecurrenceRule",
"data": {
"freq": "Yearly",
"until": null,
"count": null,
"interval": null,
"bysecond": [],
"byminute": [],
"byhour": [],
"byday": [
{
"ordwk": -1,
"weekday": "Sunday"
}
],
"bymonthday": [],
"byyearday": [],
"byweekno": [],
"bymonth": [
10
],
"bysetpos": [],
"wkst": null
}
}
]
},
{
"name": {
"type": "Tzoffsetfrom"
},
"params": [],
"values": [
{
"type": "PartialDateTime",
"data": {
"year": null,
"month": null,
"day": null,
"hour": null,
"minute": null,
"second": null,
"tz_hour": 4,
"tz_minute": 0,
"tz_minus": true
}
}
]
},
{
"name": {
"type": "Tzoffsetto"
},
"params": [],
"values": [
{
"type": "PartialDateTime",
"data": {
"year": null,
"month": null,
"day": null,
"hour": null,
"minute": null,
"second": null,
"tz_hour": 5,
"tz_minute": 0,
"tz_minus": true
}
}
]
},
{
"name": {
"type": "Tzname"
},
"params": [],
"values": [
{
"type": "Text",
"data": "Eastern Standard Time (US & Canada)"
}
]
"type": "PartialDateTime",
"data": {
"year": 1967,
"month": 10,
"day": 29,
"hour": 2,
"minute": 0,
"second": 0,
"tz_hour": null,
"tz_minute": null,
"tz_minus": false
}
}
],
"components": []
]
},
{
"component_type": "Daylight",
"entries": [
"name": {
"type": "Rrule"
},
"params": [],
"values": [
{
"name": {
"type": "Dtstart"
},
"params": [],
"values": [
{
"type": "PartialDateTime",
"data": {
"year": 1987,
"month": 4,
"day": 5,
"hour": 2,
"minute": 0,
"second": 0,
"tz_hour": null,
"tz_minute": null,
"tz_minus": false
"type": "RecurrenceRule",
"data": {
"freq": "Yearly",
"until": null,
"count": null,
"interval": null,
"bysecond": [],
"byminute": [],
"byhour": [],
"byday": [
{
"ordwk": -1,
"weekday": "Sunday"
}
}
]
},
{
"name": {
"type": "Rrule"
},
"params": [],
"values": [
{
"type": "RecurrenceRule",
"data": {
"freq": "Yearly",
"until": null,
"count": null,
"interval": null,
"bysecond": [],
"byminute": [],
"byhour": [],
"byday": [
{
"ordwk": 1,
"weekday": "Sunday"
}
],
"bymonthday": [],
"byyearday": [],
"byweekno": [],
"bymonth": [
4
],
"bysetpos": [],
"wkst": null
}
}
]
},
{
"name": {
"type": "Tzoffsetfrom"
},
"params": [],
"values": [
{
"type": "PartialDateTime",
"data": {
"year": null,
"month": null,
"day": null,
"hour": null,
"minute": null,
"second": null,
"tz_hour": 5,
"tz_minute": 0,
"tz_minus": true
}
}
]
},
{
"name": {
"type": "Tzoffsetto"
},
"params": [],
"values": [
{
"type": "PartialDateTime",
"data": {
"year": null,
"month": null,
"day": null,
"hour": null,
"minute": null,
"second": null,
"tz_hour": 4,
"tz_minute": 0,
"tz_minus": true
}
}
]
},
{
"name": {
"type": "Tzname"
},
"params": [],
"values": [
{
"type": "Text",
"data": "Eastern Daylight Time (US & Canada)"
}
]
],
"bymonthday": [],
"byyearday": [],
"byweekno": [],
"bymonth": [
10
],
"bysetpos": [],
"wkst": null
}
}
],
"components": []
]
},
{
"name": {
"type": "Tzoffsetfrom"
},
"params": [],
"values": [
{
"type": "PartialDateTime",
"data": {
"year": null,
"month": null,
"day": null,
"hour": null,
"minute": null,
"second": null,
"tz_hour": 4,
"tz_minute": 0,
"tz_minus": true
}
}
]
},
{
"name": {
"type": "Tzoffsetto"
},
"params": [],
"values": [
{
"type": "PartialDateTime",
"data": {
"year": null,
"month": null,
"day": null,
"hour": null,
"minute": null,
"second": null,
"tz_hour": 5,
"tz_minute": 0,
"tz_minus": true
}
}
]
},
{
"name": {
"type": "Tzname"
},
"params": [],
"values": [
{
"type": "Text",
"data": "Eastern Standard Time (US & Canada)"
}
]
}
]
],
"component_ids": []
},
{
"component_type": "Daylight",
"entries": [
{
"name": {
"type": "Dtstart"
},
"params": [],
"values": [
{
"type": "PartialDateTime",
"data": {
"year": 1987,
"month": 4,
"day": 5,
"hour": 2,
"minute": 0,
"second": 0,
"tz_hour": null,
"tz_minute": null,
"tz_minus": false
}
}
]
},
{
"name": {
"type": "Rrule"
},
"params": [],
"values": [
{
"type": "RecurrenceRule",
"data": {
"freq": "Yearly",
"until": null,
"count": null,
"interval": null,
"bysecond": [],
"byminute": [],
"byhour": [],
"byday": [
{
"ordwk": 1,
"weekday": "Sunday"
}
],
"bymonthday": [],
"byyearday": [],
"byweekno": [],
"bymonth": [
4
],
"bysetpos": [],
"wkst": null
}
}
]
},
{
"name": {
"type": "Tzoffsetfrom"
},
"params": [],
"values": [
{
"type": "PartialDateTime",
"data": {
"year": null,
"month": null,
"day": null,
"hour": null,
"minute": null,
"second": null,
"tz_hour": 5,
"tz_minute": 0,
"tz_minus": true
}
}
]
},
{
"name": {
"type": "Tzoffsetto"
},
"params": [],
"values": [
{
"type": "PartialDateTime",
"data": {
"year": null,
"month": null,
"day": null,
"hour": null,
"minute": null,
"second": null,
"tz_hour": 4,
"tz_minute": 0,
"tz_minus": true
}
}
]
},
{
"name": {
"type": "Tzname"
},
"params": [],
"values": [
{
"type": "Text",
"data": "Eastern Daylight Time (US & Canada)"
}
]
}
],
"component_ids": []
}
]
}

View file

@ -45,34 +45,39 @@
},
"value": {
"ICalendar": {
"component_type": "VCalendar",
"entries": [
"components": [
{
"name": {
"type": "Prodid"
},
"params": [],
"values": [
"component_type": "VCalendar",
"entries": [
{
"type": "Text",
"data": "-//Example Corp.//CalDAV Client//EN"
"name": {
"type": "Prodid"
},
"params": [],
"values": [
{
"type": "Text",
"data": "-//Example Corp.//CalDAV Client//EN"
}
]
},
{
"name": {
"type": "Version"
},
"params": [],
"values": [
{
"type": "Text",
"data": "2.0"
}
]
}
],
"component_ids": [
1
]
},
{
"name": {
"type": "Version"
},
"params": [],
"values": [
{
"type": "Text",
"data": "2.0"
}
]
}
],
"components": [
{
"component_type": "VTimezone",
"entries": [
@ -111,244 +116,246 @@
]
}
],
"components": [
"component_ids": [
2,
3
]
},
{
"component_type": "Standard",
"entries": [
{
"component_type": "Standard",
"entries": [
"name": {
"type": "Dtstart"
},
"params": [],
"values": [
{
"name": {
"type": "Dtstart"
},
"params": [],
"values": [
{
"type": "PartialDateTime",
"data": {
"year": 1967,
"month": 10,
"day": 29,
"hour": 2,
"minute": 0,
"second": 0,
"tz_hour": null,
"tz_minute": null,
"tz_minus": false
}
}
]
},
{
"name": {
"type": "Rrule"
},
"params": [],
"values": [
{
"type": "RecurrenceRule",
"data": {
"freq": "Yearly",
"until": null,
"count": null,
"interval": null,
"bysecond": [],
"byminute": [],
"byhour": [],
"byday": [
{
"ordwk": -1,
"weekday": "Sunday"
}
],
"bymonthday": [],
"byyearday": [],
"byweekno": [],
"bymonth": [
10
],
"bysetpos": [],
"wkst": null
}
}
]
},
{
"name": {
"type": "Tzoffsetfrom"
},
"params": [],
"values": [
{
"type": "PartialDateTime",
"data": {
"year": null,
"month": null,
"day": null,
"hour": null,
"minute": null,
"second": null,
"tz_hour": 4,
"tz_minute": 0,
"tz_minus": true
}
}
]
},
{
"name": {
"type": "Tzoffsetto"
},
"params": [],
"values": [
{
"type": "PartialDateTime",
"data": {
"year": null,
"month": null,
"day": null,
"hour": null,
"minute": null,
"second": null,
"tz_hour": 5,
"tz_minute": 0,
"tz_minus": true
}
}
]
},
{
"name": {
"type": "Tzname"
},
"params": [],
"values": [
{
"type": "Text",
"data": "Eastern Standard Time (US & Canada)"
}
]
"type": "PartialDateTime",
"data": {
"year": 1967,
"month": 10,
"day": 29,
"hour": 2,
"minute": 0,
"second": 0,
"tz_hour": null,
"tz_minute": null,
"tz_minus": false
}
}
],
"components": []
]
},
{
"component_type": "Daylight",
"entries": [
"name": {
"type": "Rrule"
},
"params": [],
"values": [
{
"name": {
"type": "Dtstart"
},
"params": [],
"values": [
{
"type": "PartialDateTime",
"data": {
"year": 1987,
"month": 4,
"day": 5,
"hour": 2,
"minute": 0,
"second": 0,
"tz_hour": null,
"tz_minute": null,
"tz_minus": false
"type": "RecurrenceRule",
"data": {
"freq": "Yearly",
"until": null,
"count": null,
"interval": null,
"bysecond": [],
"byminute": [],
"byhour": [],
"byday": [
{
"ordwk": -1,
"weekday": "Sunday"
}
}
]
},
{
"name": {
"type": "Rrule"
},
"params": [],
"values": [
{
"type": "RecurrenceRule",
"data": {
"freq": "Yearly",
"until": null,
"count": null,
"interval": null,
"bysecond": [],
"byminute": [],
"byhour": [],
"byday": [
{
"ordwk": 1,
"weekday": "Sunday"
}
],
"bymonthday": [],
"byyearday": [],
"byweekno": [],
"bymonth": [
4
],
"bysetpos": [],
"wkst": null
}
}
]
},
{
"name": {
"type": "Tzoffsetfrom"
},
"params": [],
"values": [
{
"type": "PartialDateTime",
"data": {
"year": null,
"month": null,
"day": null,
"hour": null,
"minute": null,
"second": null,
"tz_hour": 5,
"tz_minute": 0,
"tz_minus": true
}
}
]
},
{
"name": {
"type": "Tzoffsetto"
},
"params": [],
"values": [
{
"type": "PartialDateTime",
"data": {
"year": null,
"month": null,
"day": null,
"hour": null,
"minute": null,
"second": null,
"tz_hour": 4,
"tz_minute": 0,
"tz_minus": true
}
}
]
},
{
"name": {
"type": "Tzname"
},
"params": [],
"values": [
{
"type": "Text",
"data": "Eastern Daylight Time (US & Canada)"
}
]
],
"bymonthday": [],
"byyearday": [],
"byweekno": [],
"bymonth": [
10
],
"bysetpos": [],
"wkst": null
}
}
],
"components": []
]
},
{
"name": {
"type": "Tzoffsetfrom"
},
"params": [],
"values": [
{
"type": "PartialDateTime",
"data": {
"year": null,
"month": null,
"day": null,
"hour": null,
"minute": null,
"second": null,
"tz_hour": 4,
"tz_minute": 0,
"tz_minus": true
}
}
]
},
{
"name": {
"type": "Tzoffsetto"
},
"params": [],
"values": [
{
"type": "PartialDateTime",
"data": {
"year": null,
"month": null,
"day": null,
"hour": null,
"minute": null,
"second": null,
"tz_hour": 5,
"tz_minute": 0,
"tz_minus": true
}
}
]
},
{
"name": {
"type": "Tzname"
},
"params": [],
"values": [
{
"type": "Text",
"data": "Eastern Standard Time (US & Canada)"
}
]
}
]
],
"component_ids": []
},
{
"component_type": "Daylight",
"entries": [
{
"name": {
"type": "Dtstart"
},
"params": [],
"values": [
{
"type": "PartialDateTime",
"data": {
"year": 1987,
"month": 4,
"day": 5,
"hour": 2,
"minute": 0,
"second": 0,
"tz_hour": null,
"tz_minute": null,
"tz_minus": false
}
}
]
},
{
"name": {
"type": "Rrule"
},
"params": [],
"values": [
{
"type": "RecurrenceRule",
"data": {
"freq": "Yearly",
"until": null,
"count": null,
"interval": null,
"bysecond": [],
"byminute": [],
"byhour": [],
"byday": [
{
"ordwk": 1,
"weekday": "Sunday"
}
],
"bymonthday": [],
"byyearday": [],
"byweekno": [],
"bymonth": [
4
],
"bysetpos": [],
"wkst": null
}
}
]
},
{
"name": {
"type": "Tzoffsetfrom"
},
"params": [],
"values": [
{
"type": "PartialDateTime",
"data": {
"year": null,
"month": null,
"day": null,
"hour": null,
"minute": null,
"second": null,
"tz_hour": 5,
"tz_minute": 0,
"tz_minus": true
}
}
]
},
{
"name": {
"type": "Tzoffsetto"
},
"params": [],
"values": [
{
"type": "PartialDateTime",
"data": {
"year": null,
"month": null,
"day": null,
"hour": null,
"minute": null,
"second": null,
"tz_hour": 4,
"tz_minute": 0,
"tz_minus": true
}
}
]
},
{
"name": {
"type": "Tzname"
},
"params": [],
"values": [
{
"type": "Text",
"data": "Eastern Daylight Time (US & Canada)"
}
]
}
],
"component_ids": []
}
]
}

View file

@ -8,7 +8,7 @@
}
},
{
"type": "WebDav",
"type": "Principal",
"data": {
"type": "GroupMembership"
}

View file

@ -1,5 +1,5 @@
{
"type": "Addressbook",
"type": "AddressbookQuery",
"properties": {
"type": "Prop",
"data": [
@ -55,12 +55,6 @@
]
},
"filters": [
{
"type": "AnyOf"
},
{
"type": "AnyOf"
},
{
"type": "Property",
"comp": null,

View file

@ -1,5 +1,5 @@
{
"type": "Addressbook",
"type": "AddressbookQuery",
"properties": {
"type": "Prop",
"data": [
@ -55,9 +55,6 @@
]
},
"filters": [
{
"type": "AnyOf"
},
{
"type": "AnyOf"
},
@ -81,9 +78,6 @@
}
}
},
{
"type": "AnyOf"
},
{
"type": "Property",
"comp": null,

View file

@ -1,5 +1,5 @@
{
"type": "Addressbook",
"type": "AddressbookQuery",
"properties": {
"type": "Prop",
"data": [
@ -12,9 +12,6 @@
]
},
"filters": [
{
"type": "AnyOf"
},
{
"type": "AnyOf"
},

View file

@ -12,6 +12,7 @@ pub mod schema;
#[derive(Debug, Default, PartialEq, Eq)]
pub struct RequestHeaders<'x> {
pub uri: &'x str,
pub base_uri: Option<&'x str>,
pub depth: Depth,
pub timeout: Timeout,
pub content_type: Option<&'x str>,

View file

@ -10,6 +10,7 @@ impl<'x> RequestHeaders<'x> {
pub fn new(uri: &'x str) -> Self {
RequestHeaders {
uri,
base_uri: base_uri(uri),
..Default::default()
}
}
@ -88,39 +89,8 @@ impl<'x> RequestHeaders<'x> {
false
}
pub fn base_uri(&self) -> Option<&'x str> {
// From a path ../dav/collection/account/..
// returns ../dav/collection/account without the trailing slash
let uri = self.uri.as_bytes();
let mut found_dav = false;
let mut last_idx = 0;
let mut sep_count = 0;
for (idx, ch) in uri.iter().enumerate() {
if *ch == b'/' {
if !found_dav {
found_dav = uri.get(idx + 1..idx + 5).is_some_and(|s| s == b"dav/");
} else if found_dav {
if sep_count == 2 {
break;
}
sep_count += 1;
}
}
last_idx = idx;
}
if sep_count == 2 {
uri.get(..last_idx + 1)
.map(|uri| std::str::from_utf8(uri).unwrap())
} else {
None
}
}
pub fn format_to_base_uri(&self, path: &str) -> String {
let base_uri = self.base_uri().unwrap_or_default();
let base_uri = self.base_uri.unwrap_or_default();
format!("{base_uri}/{path}")
}
@ -292,6 +262,37 @@ impl<'x> RequestHeaders<'x> {
}
}
fn base_uri(uri: &str) -> Option<&str> {
// From a path ../dav/collection/account/..
// returns ../dav/collection/account without the trailing slash
let uri = uri.as_bytes();
let mut found_dav = false;
let mut last_idx = 0;
let mut sep_count = 0;
for (idx, ch) in uri.iter().enumerate() {
if *ch == b'/' {
if !found_dav {
found_dav = uri.get(idx + 1..idx + 5).is_some_and(|s| s == b"dav/");
} else if found_dav {
if sep_count == 2 {
break;
}
sep_count += 1;
}
}
last_idx = idx;
}
if sep_count == 2 {
uri.get(..last_idx + 1)
.map(|uri| std::str::from_utf8(uri).unwrap())
} else {
None
}
}
impl Depth {
pub fn parse(value: &str) -> Option<Self> {
hashify::tiny_map!(value.as_bytes(),
@ -358,7 +359,7 @@ mod tests {
("/dav/collection/account/", Some("/dav/collection/account")),
("/dav/collection/account", Some("/dav/collection/account")),
] {
assert_eq!(RequestHeaders::new(uri).base_uri(), expected_base);
assert_eq!(RequestHeaders::new(uri).base_uri, expected_base);
}
}

View file

@ -275,7 +275,9 @@ impl DavParser for AddressbookQuery {
ns: Namespace::CardDav,
element: Element::Filter,
} if depth == 1 => {
aq.filters.push(Filter::parse(raw)?);
if let Some(filter) = Filter::parse(raw)? {
aq.filters.push(filter);
}
depth += 1;
}
NamedElement {
@ -292,19 +294,22 @@ impl DavParser for AddressbookQuery {
ns: Namespace::CardDav,
element: Element::PropFilter,
} if depth == 2 => {
let mut filter = Filter::AnyOf;
let mut filter = None;
for attribute in raw.attributes::<VCardPropertyWithGroup>() {
match attribute? {
Attribute::Name(name) => {
property = Some(name);
}
Attribute::TestAllOf(all_of) => {
filter = if all_of { Filter::AllOf } else { Filter::AnyOf };
filter =
(if all_of { Filter::AllOf } else { Filter::AnyOf }).into();
}
_ => {}
}
}
aq.filters.push(filter);
if let Some(filter) = filter {
aq.filters.push(filter);
}
depth += 1;
}
NamedElement {
@ -542,14 +547,14 @@ impl<A, B, C> Filter<A, B, C> {
}
}
fn parse(raw: RawElement<'_>) -> crate::parser::Result<Self> {
fn parse(raw: RawElement<'_>) -> crate::parser::Result<Option<Self>> {
for attribute in raw.attributes::<String>() {
if let Attribute::TestAllOf(all_of) = attribute? {
return Ok(if all_of { Filter::AllOf } else { Filter::AnyOf });
return Ok(Some(if all_of { Filter::AllOf } else { Filter::AnyOf }));
}
}
Ok(Filter::AnyOf)
Ok(None)
}
}

View file

@ -5,6 +5,8 @@
*/
use std::borrow::Cow;
use request::TextMatch;
pub mod property;
pub mod request;
pub mod response;
@ -1413,3 +1415,14 @@ impl YesNo {
)
}
}
impl TextMatch {
pub fn matches(&self, text: &str) -> bool {
(match self.match_type {
MatchType::Equals => text == self.value,
MatchType::Contains => text.contains(&self.value),
MatchType::StartsWith => text.starts_with(&self.value),
MatchType::EndsWith => text.ends_with(&self.value),
}) ^ self.negate
}
}

View file

@ -295,6 +295,11 @@ impl DavProperty {
| DavProperty::WebDav(WebDavProperty::GetContentType)
| DavProperty::WebDav(WebDavProperty::SupportedReportSet)
| DavProperty::DeadProperty(_)
| DavProperty::CardDav(CardDavProperty::AddressData(_))
| DavProperty::CardDav(CardDavProperty::AddressbookDescription)
| DavProperty::CardDav(CardDavProperty::SupportedAddressData)
| DavProperty::CardDav(CardDavProperty::SupportedCollationSet)
| DavProperty::CardDav(CardDavProperty::MaxResourceSize),
)
}
}

View file

@ -4,9 +4,28 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use calcard::vcard::{
ArchivedVCard, ArchivedVCardEntry, ArchivedVCardParameter, VCardParameterName,
};
use common::{Server, auth::AccessToken};
use dav_proto::{RequestHeaders, schema::request::AddressbookQuery};
use dav_proto::{
RequestHeaders,
schema::request::{AddressbookQuery, Filter, FilterOp, VCardPropertyWithGroup},
};
use groupware::hierarchy::DavHierarchy;
use http_proto::HttpResponse;
use hyper::StatusCode;
use jmap_proto::types::{acl::Acl, collection::Collection};
use trc::AddContext;
use crate::{
DavError,
common::{
AddressbookFilter, DavQuery,
propfind::{PropFindItem, PropFindRequestHandler},
uri::DavUriResource,
},
};
pub(crate) trait CardQueryRequestHandler: Sync + Send {
fn handle_card_query_request(
@ -24,6 +43,155 @@ impl CardQueryRequestHandler for Server {
headers: RequestHeaders<'_>,
request: AddressbookQuery,
) -> crate::Result<HttpResponse> {
todo!()
// Validate URI
let resource_ = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
let account_id = resource_.account_id;
let resources = self
.fetch_dav_resources(account_id, Collection::AddressBook)
.await
.caused_by(trc::location!())?;
let resource = resources
.paths
.by_name(
resource_
.resource
.ok_or(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))?,
)
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
if !resource.is_container {
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
}
// Obtain shared ids
let shared_ids = if !access_token.is_member(account_id) {
self.shared_containers(
access_token,
account_id,
Collection::AddressBook,
Acl::ReadItems,
)
.await
.caused_by(trc::location!())?
.into()
} else {
None
};
// Obtain document ids in folder
let mut items = Vec::with_capacity(16);
let base_uri = headers.base_uri.unwrap_or_default();
for resource in resources.children(resource.document_id) {
if shared_ids
.as_ref()
.is_none_or(|ids| ids.contains(resource.document_id))
{
items.push(PropFindItem::new(base_uri, account_id, resource));
}
}
self.handle_dav_query(
access_token,
DavQuery::addressbook_query(request, items, headers),
)
.await
}
}
pub(crate) fn vcard_query(card: &ArchivedVCard, filters: &AddressbookFilter) -> bool {
let mut is_all = true;
let mut matches_one = false;
for filter in filters {
match filter {
Filter::AnyOf => {
is_all = false;
}
Filter::AllOf => {
is_all = true;
}
Filter::Property { prop, op, .. } => {
let result = if let Some(entry) = find_property(card, prop) {
match op {
FilterOp::Exists => true,
FilterOp::Undefined => false,
FilterOp::TextMatch(text_match) => {
let mut matched_any = false;
for value in entry.values.iter() {
if let Some(text) = value.as_text() {
if text_match.matches(&text.to_lowercase()) {
matched_any = true;
break;
}
}
}
matched_any
}
FilterOp::TimeRange(_) => false,
}
} else {
matches!(op, FilterOp::Undefined)
};
if result {
matches_one = true;
} else if is_all {
return false;
}
}
Filter::Parameter {
prop, param, op, ..
} => {
let result = if let Some(entry) =
find_property(card, prop).and_then(|entry| find_parameter(entry, param))
{
match op {
FilterOp::Exists => true,
FilterOp::Undefined => false,
FilterOp::TextMatch(text_match) => {
if let Some(text) = entry.as_text() {
text_match.matches(&text.to_lowercase())
} else {
false
}
}
FilterOp::TimeRange(_) => false,
}
} else {
matches!(op, FilterOp::Undefined)
};
if result {
matches_one = true;
} else if is_all {
return false;
}
}
Filter::Component { .. } => {}
}
}
is_all || matches_one
}
#[inline(always)]
fn find_property<'x>(
card: &'x ArchivedVCard,
prop: &VCardPropertyWithGroup,
) -> Option<&'x ArchivedVCardEntry> {
card.entries
.iter()
.find(|entry| entry.name == prop.name && entry.group == prop.group)
}
#[inline(always)]
fn find_parameter<'x>(
entry: &'x ArchivedVCardEntry,
name: &VCardParameterName,
) -> Option<&'x ArchivedVCardParameter> {
entry.params.iter().find(|param| param.matches_name(name))
}

View file

@ -251,7 +251,7 @@ impl CardUpdateRequestHandler for Server {
account_id,
parent.document_id,
vcard.uid(),
headers.base_uri().unwrap_or_default(),
headers.base_uri.unwrap_or_default(),
)
.await?;

View file

@ -4,12 +4,19 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use calcard::{
icalendar::{ICalendarComponentType, ICalendarParameterName, ICalendarProperty},
vcard::VCardParameterName,
};
use dav_proto::{
Depth, RequestHeaders, Return,
schema::{
Namespace,
property::{ReportSet, ResourceType},
request::{ArchivedDeadProperty, MultiGet, PropFind, SyncCollection},
request::{
AddressbookQuery, ArchivedDeadProperty, CalendarQuery, Filter, MultiGet, PropFind,
SyncCollection, Timezone, VCardPropertyWithGroup,
},
},
};
use groupware::{
@ -18,6 +25,7 @@ use groupware::{
file::{ArchivedFileNode, FileNode},
};
use jmap_proto::types::{collection::Collection, property::Property, value::ArchivedAclGrant};
use propfind::PropFindItem;
use rkyv::vec::ArchivedVec;
use store::{
U32_LEN,
@ -42,12 +50,32 @@ pub(crate) struct DavQuery<'x> {
pub depth_no_root: bool,
}
#[derive(Default)]
pub(crate) enum DavQueryResource<'x> {
Uri(OwnedUri<'x>),
Multiget {
parent_collection: Collection,
hrefs: Vec<String>,
},
Query {
filter: DavQueryFilter,
parent_collection: Collection,
items: Vec<PropFindItem>,
},
#[default]
None,
}
pub(crate) type AddressbookFilter = Vec<Filter<(), VCardPropertyWithGroup, VCardParameterName>>;
pub(crate) type CalendarFilter =
Vec<Filter<Vec<ICalendarComponentType>, ICalendarProperty, ICalendarParameterName>>;
pub(crate) enum DavQueryFilter {
Addressbook(AddressbookFilter),
Calendar {
filter: CalendarFilter,
timezone: Timezone,
},
}
pub(crate) trait ETag {
@ -108,7 +136,7 @@ impl<'x> DavQuery<'x> {
Self {
resource: DavQueryResource::Uri(resource),
propfind,
base_uri: headers.base_uri().unwrap_or_default(),
base_uri: headers.base_uri.unwrap_or_default(),
depth: match headers.depth {
Depth::Zero => 0,
_ => 1,
@ -130,11 +158,49 @@ impl<'x> DavQuery<'x> {
parent_collection: collection,
},
propfind: multiget.properties,
base_uri: headers.base_uri().unwrap_or_default(),
depth: match headers.depth {
Depth::Zero => 0,
_ => 1,
base_uri: headers.base_uri.unwrap_or_default(),
ret: headers.ret,
depth_no_root: headers.depth_no_root,
..Default::default()
}
}
pub fn addressbook_query(
query: AddressbookQuery,
items: Vec<PropFindItem>,
headers: RequestHeaders<'x>,
) -> Self {
Self {
resource: DavQueryResource::Query {
filter: DavQueryFilter::Addressbook(query.filters),
parent_collection: Collection::AddressBook,
items,
},
propfind: query.properties,
base_uri: headers.base_uri.unwrap_or_default(),
limit: query.limit,
ret: headers.ret,
depth_no_root: headers.depth_no_root,
..Default::default()
}
}
pub fn calendar_query(
query: CalendarQuery,
items: Vec<PropFindItem>,
headers: RequestHeaders<'x>,
) -> Self {
Self {
resource: DavQueryResource::Query {
filter: DavQueryFilter::Calendar {
filter: query.filters,
timezone: query.timezone,
},
parent_collection: Collection::Calendar,
items,
},
propfind: query.properties,
base_uri: headers.base_uri.unwrap_or_default(),
ret: headers.ret,
depth_no_root: headers.depth_no_root,
..Default::default()
@ -149,7 +215,7 @@ impl<'x> DavQuery<'x> {
Self {
resource: DavQueryResource::Uri(resource),
propfind: changes.properties,
base_uri: headers.base_uri().unwrap_or_default(),
base_uri: headers.base_uri.unwrap_or_default(),
from_change_id: changes
.sync_token
.as_deref()
@ -161,6 +227,7 @@ impl<'x> DavQuery<'x> {
limit: changes.limit,
ret: headers.ret,
depth_no_root: headers.depth_no_root,
..Default::default()
}
}
@ -173,15 +240,6 @@ impl<'x> DavQuery<'x> {
}
}
impl Default for DavQueryResource<'_> {
fn default() -> Self {
Self::Multiget {
parent_collection: Collection::None,
hrefs: Vec::new(),
}
}
}
pub(crate) enum ArchivedResource<'x> {
Calendar(Archive<&'x ArchivedCalendar>),
CalendarEvent(Archive<&'x ArchivedCalendarEvent>),

View file

@ -44,14 +44,14 @@ use trc::AddContext;
use crate::{
DavError, DavErrorCondition,
card::{CARD_ALL_PROPS, CARD_CONTAINER_PROPS, CARD_ITEM_PROPS},
card::{CARD_ALL_PROPS, CARD_CONTAINER_PROPS, CARD_ITEM_PROPS, query::vcard_query},
common::{DavQueryResource, uri::DavUriResource},
file::{FILE_ALL_PROPS, FILE_CONTAINER_PROPS, FILE_ITEM_PROPS},
principal::{CurrentUserPrincipal, propfind::PrincipalPropFind},
};
use super::{
ArchivedResource, DavCollection, DavQuery, ETag,
ArchivedResource, DavCollection, DavQuery, DavQueryFilter, ETag,
acl::{DavAclHandler, Privileges},
lock::{LockData, build_lock_key},
uri::{UriResource, Urn},
@ -253,15 +253,16 @@ impl PropFindRequestHandler for Server {
async fn handle_dav_query(
&self,
access_token: &AccessToken,
query: DavQuery<'_>,
mut query: DavQuery<'_>,
) -> crate::Result<HttpResponse> {
let mut response = MultiStatus::new(Vec::with_capacity(16));
let mut data = PropFindData::new();
let collection_container;
let collection_children;
let mut paths;
let mut query_filter = None;
match &query.resource {
match std::mem::take(&mut query.resource) {
DavQueryResource::Uri(resource) => {
let account_id = resource.account_id;
collection_container = resource.collection;
@ -306,13 +307,9 @@ impl PropFindRequestHandler for Server {
sync_token.clone().into();
sync_token
} else {
let id = self
.store()
.get_last_change_id(account_id, collection_children)
data.sync_token(self, account_id, collection_children)
.await
.caused_by(trc::location!())?
.unwrap_or_default();
Urn::Sync(id).to_string()
};
response.set_sync_token(sync_token);
@ -338,7 +335,7 @@ impl PropFindRequestHandler for Server {
.as_ref()
.is_none_or(|d| d.contains(item.document_id))
})
.map(|item| PropFindItem::new(&query, account_id, item))
.map(|item| PropFindItem::new(query.base_uri, account_id, item))
.collect::<Vec<_>>()
} else {
if !query.depth_no_root || query.from_change_id.is_none() {
@ -363,7 +360,7 @@ impl PropFindRequestHandler for Server {
.as_ref()
.is_none_or(|d| d.contains(item.document_id))
})
.map(|item| PropFindItem::new(&query, account_id, item))
.map(|item| PropFindItem::new(query.base_uri, account_id, item))
.collect::<Vec<_>>()
};
@ -386,13 +383,13 @@ impl PropFindRequestHandler for Server {
u32,
(Arc<DavResources>, Arc<Option<RoaringBitmap>>),
> = AHashMap::with_capacity(3);
collection_container = *parent_collection;
collection_container = parent_collection;
collection_children = collection_container.child_collection().unwrap();
response.set_namespace(collection_container.namespace());
for item in hrefs {
let resource = match self
.validate_uri(access_token, item)
.validate_uri(access_token, &item)
.await
.and_then(|r| r.into_owned_uri())
{
@ -443,7 +440,7 @@ impl PropFindRequestHandler for Server {
.as_ref()
.is_none_or(|docs| docs.contains(resource.document_id))
{
paths.push(PropFindItem::new(&query, account_id, resource));
paths.push(PropFindItem::new(query.base_uri, account_id, resource));
} else {
response.add_response(
Response::new_status([item], StatusCode::FORBIDDEN)
@ -465,6 +462,18 @@ impl PropFindRequestHandler for Server {
}
}
}
DavQueryResource::Query {
filter,
parent_collection,
items,
} => {
paths = items;
query_filter = Some(filter);
collection_container = parent_collection;
collection_children = collection_container.child_collection().unwrap();
response.set_namespace(collection_container.namespace());
}
DavQueryResource::None => unreachable!(),
}
if query.depth == usize::MAX && paths.len() > self.core.dav.max_match_results {
@ -554,9 +563,27 @@ impl PropFindRequestHandler for Server {
};
let archive = ArchivedResource::from_archive(&archive_, collection)
.caused_by(trc::location!())?;
let dead_properties = archive.dead_properties();
// Filter
if let Some(query_filter) = &query_filter {
match (query_filter, &archive) {
(DavQueryFilter::Addressbook(filters), ArchivedResource::ContactCard(card)) => {
if !vcard_query(&card.inner.card, filters) {
continue;
}
}
(
DavQueryFilter::Calendar { filter, timezone },
ArchivedResource::CalendarEvent(event),
) => {
todo!()
}
_ => (),
}
}
// Fill properties
let dead_properties = archive.dead_properties();
let mut fields = Vec::with_capacity(properties.len());
let mut fields_not_found = Vec::new();
for property in &properties {
@ -958,9 +985,9 @@ impl PropFindRequestHandler for Server {
}
impl PropFindItem {
pub fn new(query: &DavQuery<'_>, account_id: u32, resource: &DavResource) -> Self {
pub fn new(base_uri: &str, account_id: u32, resource: &DavResource) -> Self {
Self {
name: query.format_to_base_uri(&resource.name),
name: format!("{}{}", base_uri, resource.name),
account_id,
document_id: resource.document_id,
is_container: resource.is_container,

View file

@ -52,7 +52,7 @@ impl PrincipalMatching for Server {
access_token,
DavQuery {
resource: DavQueryResource::Uri(resource),
base_uri: headers.base_uri().unwrap_or_default(),
base_uri: headers.base_uri.unwrap_or_default(),
propfind: PropFind::Prop(request.properties),
depth: usize::MAX,
ret: headers.ret,