diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 4b2d3330..2a2c7540 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -534,6 +534,12 @@ impl DavResources { }) } + pub fn children(&self, parent_id: u32) -> impl Iterator { + 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; diff --git a/crates/dav-proto/resources/requests/mkcol-002.json b/crates/dav-proto/resources/requests/mkcol-002.json index 130de28e..05e212a0 100644 --- a/crates/dav-proto/resources/requests/mkcol-002.json +++ b/crates/dav-proto/resources/requests/mkcol-002.json @@ -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": [] } ] } diff --git a/crates/dav-proto/resources/requests/propertyupdate-001.json b/crates/dav-proto/resources/requests/propertyupdate-001.json index f3005bed..7d6b32a2 100644 --- a/crates/dav-proto/resources/requests/propertyupdate-001.json +++ b/crates/dav-proto/resources/requests/propertyupdate-001.json @@ -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": [] } ] } diff --git a/crates/dav-proto/resources/requests/propfind-006.json b/crates/dav-proto/resources/requests/propfind-006.json index 566e803f..92fbb577 100644 --- a/crates/dav-proto/resources/requests/propfind-006.json +++ b/crates/dav-proto/resources/requests/propfind-006.json @@ -8,7 +8,7 @@ } }, { - "type": "WebDav", + "type": "Principal", "data": { "type": "GroupMembership" } diff --git a/crates/dav-proto/resources/requests/report-014.json b/crates/dav-proto/resources/requests/report-014.json index e2e2962b..4733001e 100644 --- a/crates/dav-proto/resources/requests/report-014.json +++ b/crates/dav-proto/resources/requests/report-014.json @@ -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, diff --git a/crates/dav-proto/resources/requests/report-015.json b/crates/dav-proto/resources/requests/report-015.json index 1a6336ea..48356178 100644 --- a/crates/dav-proto/resources/requests/report-015.json +++ b/crates/dav-proto/resources/requests/report-015.json @@ -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, diff --git a/crates/dav-proto/resources/requests/report-016.json b/crates/dav-proto/resources/requests/report-016.json index 87bac76d..6d1bc453 100644 --- a/crates/dav-proto/resources/requests/report-016.json +++ b/crates/dav-proto/resources/requests/report-016.json @@ -1,5 +1,5 @@ { - "type": "Addressbook", + "type": "AddressbookQuery", "properties": { "type": "Prop", "data": [ @@ -12,9 +12,6 @@ ] }, "filters": [ - { - "type": "AnyOf" - }, { "type": "AnyOf" }, diff --git a/crates/dav-proto/src/lib.rs b/crates/dav-proto/src/lib.rs index 572d2c6a..8b359a63 100644 --- a/crates/dav-proto/src/lib.rs +++ b/crates/dav-proto/src/lib.rs @@ -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>, diff --git a/crates/dav-proto/src/parser/header.rs b/crates/dav-proto/src/parser/header.rs index 183dfbf1..a17d5a97 100644 --- a/crates/dav-proto/src/parser/header.rs +++ b/crates/dav-proto/src/parser/header.rs @@ -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 { 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); } } diff --git a/crates/dav-proto/src/requests/report.rs b/crates/dav-proto/src/requests/report.rs index 4b8756c9..e565fad2 100644 --- a/crates/dav-proto/src/requests/report.rs +++ b/crates/dav-proto/src/requests/report.rs @@ -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::() { 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 Filter { } } - fn parse(raw: RawElement<'_>) -> crate::parser::Result { + fn parse(raw: RawElement<'_>) -> crate::parser::Result> { for attribute in raw.attributes::() { 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) } } diff --git a/crates/dav-proto/src/schema/mod.rs b/crates/dav-proto/src/schema/mod.rs index e47b8aca..ffaff80c 100644 --- a/crates/dav-proto/src/schema/mod.rs +++ b/crates/dav-proto/src/schema/mod.rs @@ -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 + } +} diff --git a/crates/dav-proto/src/schema/property.rs b/crates/dav-proto/src/schema/property.rs index fe8f26b1..b5006363 100644 --- a/crates/dav-proto/src/schema/property.rs +++ b/crates/dav-proto/src/schema/property.rs @@ -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), ) } } diff --git a/crates/dav/src/card/query.rs b/crates/dav/src/card/query.rs index 651e4192..3ccfcfd3 100644 --- a/crates/dav/src/card/query.rs +++ b/crates/dav/src/card/query.rs @@ -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 { - 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)) +} diff --git a/crates/dav/src/card/update.rs b/crates/dav/src/card/update.rs index e22c11ad..a50fbb97 100644 --- a/crates/dav/src/card/update.rs +++ b/crates/dav/src/card/update.rs @@ -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?; diff --git a/crates/dav/src/common/mod.rs b/crates/dav/src/common/mod.rs index 93091dd4..e1e7c523 100644 --- a/crates/dav/src/common/mod.rs +++ b/crates/dav/src/common/mod.rs @@ -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, }, + Query { + filter: DavQueryFilter, + parent_collection: Collection, + items: Vec, + }, + #[default] + None, +} + +pub(crate) type AddressbookFilter = Vec>; +pub(crate) type CalendarFilter = + Vec, 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, + 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, + 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>), diff --git a/crates/dav/src/common/propfind.rs b/crates/dav/src/common/propfind.rs index 5cb48922..0b96cb1c 100644 --- a/crates/dav/src/common/propfind.rs +++ b/crates/dav/src/common/propfind.rs @@ -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 { 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::>() } 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::>() }; @@ -386,13 +383,13 @@ impl PropFindRequestHandler for Server { u32, (Arc, Arc>), > = 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, diff --git a/crates/dav/src/principal/matching.rs b/crates/dav/src/principal/matching.rs index 1d847630..741ce1c7 100644 --- a/crates/dav/src/principal/matching.rs +++ b/crates/dav/src/principal/matching.rs @@ -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,