From 10ae19f2ebda122e96d4c6118d8ebd70c3ca103c Mon Sep 17 00:00:00 2001 From: mdecimus Date: Fri, 18 Apr 2025 13:55:55 +0200 Subject: [PATCH] CardDAV working with Thunderbird and Apple Contacts --- Cargo.lock | 1 + crates/common/src/config/dav.rs | 22 ++ crates/common/src/lib.rs | 19 +- crates/dav-proto/src/lib.rs | 21 +- crates/dav-proto/src/parser/header.rs | 14 +- crates/dav-proto/src/parser/mod.rs | 7 + crates/dav-proto/src/parser/property.rs | 3 + crates/dav-proto/src/responses/acl.rs | 10 +- crates/dav-proto/src/responses/error.rs | 52 ++-- crates/dav-proto/src/responses/mkcol.rs | 8 +- crates/dav-proto/src/responses/mod.rs | 37 +-- crates/dav-proto/src/responses/multistatus.rs | 10 +- crates/dav-proto/src/responses/property.rs | 87 +++--- crates/dav-proto/src/schema/mod.rs | 38 ++- crates/dav-proto/src/schema/property.rs | 85 +++++- crates/dav-proto/src/schema/response.rs | 12 +- crates/dav/src/card/copy_move.rs | 274 +++++++----------- crates/dav/src/card/delete.rs | 159 ++-------- crates/dav/src/card/get.rs | 2 +- crates/dav/src/card/mkcol.rs | 15 +- crates/dav/src/card/mod.rs | 129 +-------- crates/dav/src/card/proppatch.rs | 34 +-- crates/dav/src/card/query.rs | 9 +- crates/dav/src/card/update.rs | 38 +-- crates/dav/src/common/acl.rs | 73 ++--- crates/dav/src/common/lock.rs | 29 +- crates/dav/src/common/mod.rs | 16 +- crates/dav/src/common/propfind.rs | 229 +++++++++++---- crates/dav/src/common/uri.rs | 17 +- crates/dav/src/file/copy_move.rs | 173 +++++------ crates/dav/src/file/delete.rs | 54 +--- crates/dav/src/file/get.rs | 2 +- crates/dav/src/file/mkcol.rs | 2 +- crates/dav/src/file/mod.rs | 85 +----- crates/dav/src/file/proppatch.rs | 28 +- crates/dav/src/file/update.rs | 2 +- crates/dav/src/lib.rs | 91 +----- crates/dav/src/principal/matching.rs | 1 - crates/dav/src/principal/mod.rs | 6 +- crates/dav/src/principal/propfind.rs | 161 +++++----- crates/dav/src/principal/propsearch.rs | 5 +- crates/dav/src/request.rs | 139 +++++---- crates/directory/src/core/principal.rs | 2 +- crates/groupware/src/contact/index.rs | 10 +- crates/groupware/src/contact/mod.rs | 1 + crates/groupware/src/contact/storage.rs | 231 +++++++++++++++ crates/groupware/src/file/mod.rs | 2 +- crates/groupware/src/file/storage.rs | 133 +++++++++ crates/groupware/src/hierarchy.rs | 101 ++++++- crates/groupware/src/lib.rs | 63 ++++ crates/http/Cargo.toml | 1 + crates/http/src/request.rs | 30 +- crates/utils/src/bimap.rs | 4 + 53 files changed, 1544 insertions(+), 1233 deletions(-) create mode 100644 crates/groupware/src/contact/storage.rs create mode 100644 crates/groupware/src/file/storage.rs diff --git a/Cargo.lock b/Cargo.lock index 09c9f2f4..9d5ae61a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3073,6 +3073,7 @@ dependencies = [ "directory", "email", "form-data", + "groupware", "http-body-util", "http_proto", "hyper 1.6.0", diff --git a/crates/common/src/config/dav.rs b/crates/common/src/config/dav.rs index f69a338c..5f4720e1 100644 --- a/crates/common/src/config/dav.rs +++ b/crates/common/src/config/dav.rs @@ -16,6 +16,10 @@ pub struct DavConfig { pub max_changes: usize, pub max_match_results: usize, pub max_vcard_size: usize, + pub default_calendar_name: Option, + pub default_addressbook_name: Option, + pub default_calendar_display_name: Option, + pub default_addressbook_display_name: Option, } impl DavConfig { @@ -41,6 +45,24 @@ impl DavConfig { max_vcard_size: config .property("dav.limits.size.vcard") .unwrap_or(512 * 1024), + default_calendar_name: config + .property_or_default::>("dav.default.calendar.name", "default") + .unwrap_or_default(), + default_addressbook_name: config + .property_or_default::>("dav.default.addressbook.name", "default") + .unwrap_or_default(), + default_calendar_display_name: config + .property_or_default::>( + "dav.default.calendar.display-name", + "Default Calendar", + ) + .unwrap_or_default(), + default_addressbook_display_name: config + .property_or_default::>( + "dav.default.addressbook.display-name", + "Default Address Book", + ) + .unwrap_or_default(), } } } diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 34b19e9b..ba729acb 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -242,6 +242,7 @@ pub struct DavResourceId { #[derive(Debug, Default)] pub struct DavResources { + pub base_path: String, pub paths: IdBimap, pub size: u64, pub modseq: Option, @@ -479,7 +480,7 @@ impl DavResources { self.paths.iter().filter(move |item| { item.name .strip_prefix(&prefix) - .is_some_and(|name| name.as_bytes().iter().filter(|&&c| c == b'/').count() <= depth) + .is_some_and(|name| name.as_bytes().iter().filter(|&&c| c == b'/').count() < depth) || item.name == search_path }) } @@ -503,6 +504,22 @@ impl DavResources { let prefix = format!("{ancestor}/"); descendant.starts_with(&prefix) || descendant == ancestor } + + pub fn format_resource(&self, resource: &DavResource) -> String { + if resource.is_container { + format!("{}{}/", self.base_path, resource.name) + } else { + format!("{}{}", self.base_path, resource.name) + } + } + + pub fn format_collection(&self, name: &str) -> String { + format!("{}{name}/", self.base_path) + } + + pub fn format_item(&self, name: &str) -> String { + format!("{}{}", self.base_path, name) + } } impl IdBimapItem for DavResource { diff --git a/crates/dav-proto/src/lib.rs b/crates/dav-proto/src/lib.rs index 8b359a63..6367d73b 100644 --- a/crates/dav-proto/src/lib.rs +++ b/crates/dav-proto/src/lib.rs @@ -9,10 +9,29 @@ pub mod requests; pub mod responses; pub mod schema; +pub fn xml_pretty_print(xml_string: &str) -> String { + // Create a reader + let mut reader = quick_xml::Reader::from_str(xml_string); + let mut writer = quick_xml::Writer::new_with_indent(std::io::Cursor::new(Vec::new()), b' ', 2); + let mut buf = Vec::new(); + loop { + match reader.read_event_into(&mut buf) { + Ok(quick_xml::events::Event::Eof) => break, + Ok(event) => { + writer.write_event(event).unwrap(); + } + Err(e) => panic!("Error at position {}: {:?}", reader.buffer_position(), e), + } + buf.clear(); + } + + let result = writer.into_inner().into_inner(); + String::from_utf8(result).unwrap() +} + #[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 a17d5a97..d07e257e 100644 --- a/crates/dav-proto/src/parser/header.rs +++ b/crates/dav-proto/src/parser/header.rs @@ -10,7 +10,6 @@ impl<'x> RequestHeaders<'x> { pub fn new(uri: &'x str) -> Self { RequestHeaders { uri, - base_uri: base_uri(uri), ..Default::default() } } @@ -89,11 +88,6 @@ impl<'x> RequestHeaders<'x> { false } - pub fn format_to_base_uri(&self, path: &str) -> String { - let base_uri = self.base_uri.unwrap_or_default(); - format!("{base_uri}/{path}") - } - pub fn has_if(&self) -> bool { !self.if_.is_empty() } @@ -260,9 +254,13 @@ impl<'x> RequestHeaders<'x> { }); } } + + pub fn base_uri(&self) -> Option<&str> { + dav_base_uri(self.uri) + } } -fn base_uri(uri: &str) -> Option<&str> { +pub fn dav_base_uri(uri: &str) -> Option<&str> { // From a path ../dav/collection/account/.. // returns ../dav/collection/account without the trailing slash @@ -359,7 +357,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/parser/mod.rs b/crates/dav-proto/src/parser/mod.rs index 4ea82068..ca10f3fc 100644 --- a/crates/dav-proto/src/parser/mod.rs +++ b/crates/dav-proto/src/parser/mod.rs @@ -72,6 +72,13 @@ impl NamedElement { element, } } + + pub fn calendarserver(element: Element) -> NamedElement { + NamedElement { + ns: Namespace::CalendarServer, + element, + } + } } impl Token<'_> { diff --git a/crates/dav-proto/src/parser/property.rs b/crates/dav-proto/src/parser/property.rs index 684af151..b3521fe0 100644 --- a/crates/dav-proto/src/parser/property.rs +++ b/crates/dav-proto/src/parser/property.rs @@ -542,6 +542,9 @@ impl DavProperty { (Namespace::CalDav, Element::CalendarTimezoneId) => { Some(DavProperty::CalDav(CalDavProperty::TimezoneId)) } + (Namespace::CalendarServer, Element::Getctag) => { + Some(DavProperty::WebDav(WebDavProperty::GetCTag)) + } _ => None, } } diff --git a/crates/dav-proto/src/responses/acl.rs b/crates/dav-proto/src/responses/acl.rs index 0ed535a9..4f35837c 100644 --- a/crates/dav-proto/src/responses/acl.rs +++ b/crates/dav-proto/src/responses/acl.rs @@ -14,7 +14,7 @@ use crate::{ Ace, AclRestrictions, GrantDeny, Href, List, Principal, PrincipalSearchProperty, PrincipalSearchPropertySet, RequiredPrincipal, Resource, SupportedPrivilege, }, - Namespace, + Namespace, Namespaces, }, }; @@ -175,7 +175,7 @@ impl Display for Privilege { Privilege::Bind => "".fmt(f), Privilege::Unbind => "".fmt(f), Privilege::All => "".fmt(f), - Privilege::ReadFreeBusy => "".fmt(f), + Privilege::ReadFreeBusy => "".fmt(f), } } } @@ -195,7 +195,7 @@ impl Display for PrincipalSearchPropertySet { write!( f, "{}", - self.namespace, self.properties + self.namespaces, self.properties ) } } @@ -227,13 +227,13 @@ impl Resource { impl PrincipalSearchPropertySet { pub fn new(properties: Vec) -> Self { PrincipalSearchPropertySet { - namespace: Namespace::Dav, + namespaces: Namespaces::default(), properties: List(properties), } } pub fn with_namespace(mut self, namespace: Namespace) -> Self { - self.namespace = namespace; + self.namespaces.set(namespace); self } } diff --git a/crates/dav-proto/src/responses/error.rs b/crates/dav-proto/src/responses/error.rs index 656d1666..c5a3477a 100644 --- a/crates/dav-proto/src/responses/error.rs +++ b/crates/dav-proto/src/responses/error.rs @@ -8,12 +8,12 @@ use std::fmt::Display; use crate::schema::{ response::{BaseCondition, CalCondition, CardCondition, Condition, ErrorResponse}, - Namespace, + Namespace, Namespaces, }; impl Display for ErrorResponse { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "", self.namespace)?; + write!(f, "", self.namespaces)?; match &self.error { Condition::Base(e) => e.fmt(f)?, @@ -88,31 +88,31 @@ impl Display for CalCondition { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { CalCondition::CalendarCollectionLocationOk => { - write!(f, "") + write!(f, "") } - CalCondition::ValidCalendarData => write!(f, ""), - CalCondition::ValidFilter => write!(f, ""), + CalCondition::ValidCalendarData => write!(f, ""), + CalCondition::ValidFilter => write!(f, ""), CalCondition::ValidCalendarObjectResource => { - write!(f, "") + write!(f, "") } CalCondition::NoUidConflict(uid) => { - write!(f, "{uid}") + write!(f, "{uid}") } CalCondition::InitializeCalendarCollection => { - write!(f, "") + write!(f, "") } - CalCondition::SupportedCalendarData => write!(f, ""), - CalCondition::SupportedFilter(_) => write!(f, ""), + CalCondition::SupportedCalendarData => write!(f, ""), + CalCondition::SupportedFilter(_) => write!(f, ""), CalCondition::SupportedCollation(c) => { - write!(f, "{c}") + write!(f, "{c}") } - CalCondition::MinDateTime => write!(f, ""), - CalCondition::MaxDateTime => write!(f, ""), + CalCondition::MinDateTime => write!(f, ""), + CalCondition::MaxDateTime => write!(f, ""), CalCondition::MaxResourceSize(l) => { - write!(f, "{l}") + write!(f, "{l}") } - CalCondition::MaxInstances => write!(f, ""), - CalCondition::MaxAttendeesPerInstance => write!(f, ""), + CalCondition::MaxInstances => write!(f, ""), + CalCondition::MaxAttendeesPerInstance => write!(f, ""), } } } @@ -120,23 +120,23 @@ impl Display for CalCondition { impl Display for CardCondition { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - CardCondition::SupportedAddressData => write!(f, ""), + CardCondition::SupportedAddressData => write!(f, ""), CardCondition::SupportedAddressDataConversion => { - write!(f, "") + write!(f, "") } - CardCondition::SupportedFilter(_) => write!(f, ""), + CardCondition::SupportedFilter(_) => write!(f, ""), CardCondition::SupportedCollation(c) => { - write!(f, "{c}") + write!(f, "{c}") } - CardCondition::ValidAddressData => write!(f, ""), + CardCondition::ValidAddressData => write!(f, ""), CardCondition::NoUidConflict(uid) => { - write!(f, "{uid}") + write!(f, "{uid}") } CardCondition::MaxResourceSize(l) => { - write!(f, "{l}") + write!(f, "{l}") } CardCondition::AddressBookCollectionLocationOk => { - write!(f, "") + write!(f, "") } } } @@ -163,13 +163,13 @@ impl From for Condition { impl ErrorResponse { pub fn new(error: impl Into) -> Self { ErrorResponse { - namespace: Namespace::Dav, + namespaces: Namespaces::default(), error: error.into(), } } pub fn with_namespace(mut self, namespace: impl Into) -> Self { - self.namespace = namespace.into(); + self.namespaces.set(namespace.into()); self } } diff --git a/crates/dav-proto/src/responses/mkcol.rs b/crates/dav-proto/src/responses/mkcol.rs index e2c670d4..f59fa787 100644 --- a/crates/dav-proto/src/responses/mkcol.rs +++ b/crates/dav-proto/src/responses/mkcol.rs @@ -8,7 +8,7 @@ use std::fmt::Display; use crate::schema::{ response::{List, MkColResponse, PropStat}, - Namespace, + Namespace, Namespaces, }; impl Display for MkColResponse { @@ -16,7 +16,7 @@ impl Display for MkColResponse { write!( f, "{}", - self.namespace, self.propstat + self.namespaces, self.propstat ) } } @@ -24,13 +24,13 @@ impl Display for MkColResponse { impl MkColResponse { pub fn new(propstat: Vec) -> Self { Self { - namespace: Namespace::Dav, + namespaces: Namespaces::default(), propstat: List(propstat), } } pub fn with_namespace(mut self, namespace: Namespace) -> Self { - self.namespace = namespace; + self.namespaces.set(namespace); self } } diff --git a/crates/dav-proto/src/responses/mod.rs b/crates/dav-proto/src/responses/mod.rs index abbd23dd..a6d80935 100644 --- a/crates/dav-proto/src/responses/mod.rs +++ b/crates/dav-proto/src/responses/mod.rs @@ -18,7 +18,7 @@ use crate::schema::{ property::{Comp, ResourceType, SupportedCollation}, request::{DeadProperty, DeadPropertyTag}, response::{Href, List, Location, ResponseDescription, Status, SyncToken}, - Namespace, + Namespaces, }; trait XmlEscape { @@ -44,19 +44,19 @@ impl> XmlEscape for T { } } -impl Namespace { - pub(crate) fn write_to(&self, out: &mut impl Write) -> std::fmt::Result { - out.write_str(match self { - Namespace::Dav => "xmlns:D=\"DAV:\"", - Namespace::CalDav => "xmlns:D=\"DAV:\" xmlns:C=\"urn:ietf:params:xml:ns:caldav\"", - Namespace::CardDav => "xmlns:D=\"DAV:\" xmlns:C=\"urn:ietf:params:xml:ns:carddav\"", - }) - } -} - -impl Display for Namespace { +impl Display for Namespaces { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.write_to(f) + f.write_str("xmlns:D=\"DAV:\"")?; + if self.cal { + f.write_str(" xmlns:A=\"urn:ietf:params:xml:ns:caldav\"")?; + } + if self.card { + f.write_str(" xmlns:B=\"urn:ietf:params:xml:ns:carddav\"")?; + } + if self.cs { + f.write_str(" xmlns:C=\"http://calendarserver.org/ns/\"")?; + } + Ok(()) } } @@ -112,7 +112,7 @@ impl Display for SyncToken { impl Display for Comp { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "", self.0.as_str()) + write!(f, "", self.0.as_str()) } } @@ -121,18 +121,19 @@ impl Display for ResourceType { match self { ResourceType::Collection => write!(f, ""), ResourceType::Principal => write!(f, ""), - ResourceType::AddressBook => write!(f, ""), - ResourceType::Calendar => write!(f, ""), + ResourceType::AddressBook => write!(f, ""), + ResourceType::Calendar => write!(f, ""), } } } impl Display for SupportedCollation { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let ns = self.namespace.prefix(); write!( f, - "{}", - self.0.as_str() + "<{ns}:supported-collation>{}", + self.collation.as_str() ) } } diff --git a/crates/dav-proto/src/responses/multistatus.rs b/crates/dav-proto/src/responses/multistatus.rs index de3b76b9..d7fe3270 100644 --- a/crates/dav-proto/src/responses/multistatus.rs +++ b/crates/dav-proto/src/responses/multistatus.rs @@ -13,12 +13,12 @@ use crate::schema::{ Condition, Href, List, Location, MultiStatus, PropStat, Response, ResponseDescription, ResponseType, Status, SyncToken, }, - Namespace, + Namespace, Namespaces, }; impl Display for MultiStatus { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.namespace, self.response)?; + write!(f, "{}", self.namespaces, self.response)?; if let Some(response_description) = &self.response_description { write!(f, "{response_description}")?; } @@ -64,7 +64,7 @@ impl Display for ResponseType { impl MultiStatus { pub fn new(response: Vec) -> Self { MultiStatus { - namespace: Namespace::Dav, + namespaces: Namespaces::default(), response: List(response), response_description: None, sync_token: None, @@ -86,12 +86,12 @@ impl MultiStatus { } pub fn with_namespace(mut self, namespace: Namespace) -> Self { - self.namespace = namespace; + self.namespaces.set(namespace); self } pub fn set_namespace(&mut self, namespace: Namespace) { - self.namespace = namespace; + self.namespaces.set(namespace); } pub fn with_sync_token(mut self, sync_token: impl Into) -> Self { diff --git a/crates/dav-proto/src/responses/property.rs b/crates/dav-proto/src/responses/property.rs index 5cd95d76..911e82d4 100644 --- a/crates/dav-proto/src/responses/property.rs +++ b/crates/dav-proto/src/responses/property.rs @@ -20,14 +20,18 @@ use crate::schema::{ }, request::{DavPropertyValue, DeadProperty}, response::{Ace, AclRestrictions, Href, List, PropResponse, SupportedPrivilege}, - Namespace, + Namespace, Namespaces, }; use super::XmlEscape; impl Display for PropResponse { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.namespace, self.properties) + write!( + f, + "{}", + self.namespaces, self.properties + ) } } @@ -98,11 +102,11 @@ impl Display for DavValue { write!( f, concat!( - "", - "", - "", - "", - "" + "", + "", + "", + "", + "" ) ) } @@ -110,10 +114,10 @@ impl Display for DavValue { write!( f, concat!( - "", - "", - "", - "" + "", + "", + "", + "" ) ) } @@ -150,39 +154,40 @@ impl DavProperty { WebDavProperty::AclRestrictions => "D:acl-restrictions", WebDavProperty::InheritedAclSet => "D:inherited-acl-set", WebDavProperty::PrincipalCollectionSet => "D:principal-collection-set", + WebDavProperty::GetCTag => "C:getctag", }, DavProperty::CardDav(prop) => match prop { - CardDavProperty::AddressbookDescription => "C:addressbook-description", - CardDavProperty::SupportedAddressData => "C:supported-address-data", - CardDavProperty::SupportedCollationSet => "C:supported-collation-set", - CardDavProperty::MaxResourceSize => "C:max-resource-size", - CardDavProperty::AddressData(_) => "C:address-data", + CardDavProperty::AddressbookDescription => "B:addressbook-description", + CardDavProperty::SupportedAddressData => "B:supported-address-data", + CardDavProperty::SupportedCollationSet => "B:supported-collation-set", + CardDavProperty::MaxResourceSize => "B:max-resource-size", + CardDavProperty::AddressData(_) => "B:address-data", }, DavProperty::CalDav(prop) => match prop { - CalDavProperty::CalendarDescription => "C:calendar-description", - CalDavProperty::CalendarTimezone => "C:calendar-timezone", + CalDavProperty::CalendarDescription => "A:calendar-description", + CalDavProperty::CalendarTimezone => "A:calendar-timezone", CalDavProperty::SupportedCalendarComponentSet => { - "C:supported-calendar-component-set" + "A:supported-calendar-component-set" } - CalDavProperty::SupportedCalendarData => "C:supported-calendar-data", - CalDavProperty::SupportedCollationSet => "C:supported-collation-set", - CalDavProperty::MaxResourceSize => "C:max-resource-size", - CalDavProperty::MinDateTime => "C:min-date-time", - CalDavProperty::MaxDateTime => "C:max-date-time", - CalDavProperty::MaxInstances => "C:max-instances", - CalDavProperty::MaxAttendeesPerInstance => "C:max-attendees-per-instance", - CalDavProperty::CalendarHomeSet => "C:calendar-home-set", - CalDavProperty::CalendarData(_) => "C:calendar-data", - CalDavProperty::TimezoneServiceSet => "C:timezone-service-set", - CalDavProperty::TimezoneId => "C:calendar-timezone-id", + CalDavProperty::SupportedCalendarData => "A:supported-calendar-data", + CalDavProperty::SupportedCollationSet => "A:supported-collation-set", + CalDavProperty::MaxResourceSize => "A:max-resource-size", + CalDavProperty::MinDateTime => "A:min-date-time", + CalDavProperty::MaxDateTime => "A:max-date-time", + CalDavProperty::MaxInstances => "A:max-instances", + CalDavProperty::MaxAttendeesPerInstance => "A:max-attendees-per-instance", + CalDavProperty::CalendarHomeSet => "A:calendar-home-set", + CalDavProperty::CalendarData(_) => "A:calendar-data", + CalDavProperty::TimezoneServiceSet => "A:timezone-service-set", + CalDavProperty::TimezoneId => "A:calendar-timezone-id", }, DavProperty::Principal(prop) => match prop { PrincipalProperty::AlternateURISet => "D:alternate-URI-set", PrincipalProperty::PrincipalURL => "D:principal-URL", PrincipalProperty::GroupMemberSet => "D:group-member-set", PrincipalProperty::GroupMembership => "D:group-membership", - PrincipalProperty::AddressbookHomeSet => "C:addressbook-home-set", - PrincipalProperty::PrincipalAddress => "C:principal-address", + PrincipalProperty::AddressbookHomeSet => "B:addressbook-home-set", + PrincipalProperty::PrincipalAddress => "B:principal-address", }, DavProperty::DeadProperty(dead) => { return (dead.name.as_str(), dead.attrs.as_deref()) @@ -195,21 +200,23 @@ impl DavProperty { impl Display for ReportSet { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("")?; match self { ReportSet::SyncCollection => write!(f, ""), ReportSet::ExpandProperty => write!(f, ""), - ReportSet::AddressbookQuery => write!(f, ""), - ReportSet::AddressbookMultiGet => write!(f, ""), - ReportSet::CalendarQuery => write!(f, ""), - ReportSet::CalendarMultiGet => write!(f, ""), - ReportSet::FreeBusyQuery => write!(f, ""), + ReportSet::AddressbookQuery => write!(f, ""), + ReportSet::AddressbookMultiGet => write!(f, ""), + ReportSet::CalendarQuery => write!(f, ""), + ReportSet::CalendarMultiGet => write!(f, ""), + ReportSet::FreeBusyQuery => write!(f, ""), ReportSet::AclPrincipalPropSet => write!(f, ""), ReportSet::PrincipalMatch => write!(f, ""), ReportSet::PrincipalPropertySearch => write!(f, ""), ReportSet::PrincipalSearchPropertySet => { write!(f, "") } - } + }?; + f.write_str("") } } @@ -227,13 +234,13 @@ impl Display for DavProperty { impl PropResponse { pub fn new(properties: Vec) -> Self { PropResponse { - namespace: Namespace::Dav, + namespaces: Namespaces::default(), properties: List(properties), } } pub fn with_namespace(mut self, namespace: Namespace) -> Self { - self.namespace = namespace; + self.namespaces.set(namespace); self } } diff --git a/crates/dav-proto/src/schema/mod.rs b/crates/dav-proto/src/schema/mod.rs index ffaff80c..9e326df3 100644 --- a/crates/dav-proto/src/schema/mod.rs +++ b/crates/dav-proto/src/schema/mod.rs @@ -20,10 +20,31 @@ pub struct NamedElement { #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +#[repr(u8)] pub enum Namespace { Dav, CalDav, CardDav, + CalendarServer, +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub struct Namespaces { + pub(crate) cal: bool, + pub(crate) card: bool, + pub(crate) cs: bool, +} + +impl Namespaces { + pub fn set(&mut self, ns: Namespace) { + match ns { + Namespace::CalDav => self.cal = true, + Namespace::CardDav => self.card = true, + Namespace::CalendarServer => self.cs = true, + Namespace::Dav => {} + } + } } impl Namespace { @@ -31,9 +52,20 @@ impl Namespace { hashify::tiny_map!(value, "DAV:" => Namespace::Dav, "urn:ietf:params:xml:ns:caldav" => Namespace::CalDav, - "urn:ietf:params:xml:ns:carddav" => Namespace::CardDav + "urn:ietf:params:xml:ns:carddav" => Namespace::CardDav, + "http://calendarserver.org/ns/" => Namespace::CalendarServer, + "http://calendarserver.org/ns" => Namespace::CalendarServer ) } + + pub fn prefix(&self) -> &str { + match self { + Namespace::Dav => "D", + Namespace::CalDav => "A", + Namespace::CardDav => "B", + Namespace::CalendarServer => "C", + } + } } impl AsRef for Namespace { @@ -42,6 +74,7 @@ impl AsRef for Namespace { Namespace::Dav => "DAV:", Namespace::CalDav => "urn:ietf:params:xml:ns:caldav", Namespace::CardDav => "urn:ietf:params:xml:ns:carddav", + Namespace::CalendarServer => "http://calendarserver.org/ns/", } } } @@ -163,6 +196,7 @@ pub enum Element { Getcontentlanguage, Getcontentlength, Getcontenttype, + Getctag, Getetag, Getlastmodified, Grammar, @@ -543,6 +577,7 @@ impl Element { "getcontentlength" => Element::Getcontentlength, "getcontenttype" => Element::Getcontenttype, "getetag" => Element::Getetag, + "getctag" => Element::Getctag, "getlastmodified" => Element::Getlastmodified, "grammar" => Element::Grammar, "grant" => Element::Grant, @@ -927,6 +962,7 @@ impl AsRef for Element { Element::Getcontentlength => "getcontentlength", Element::Getcontenttype => "getcontenttype", Element::Getetag => "getetag", + Element::Getctag => "getctag", Element::Getlastmodified => "getlastmodified", Element::Grammar => "grammar", Element::Grant => "grant", diff --git a/crates/dav-proto/src/schema/property.rs b/crates/dav-proto/src/schema/property.rs index b5006363..28daf89c 100644 --- a/crates/dav-proto/src/schema/property.rs +++ b/crates/dav-proto/src/schema/property.rs @@ -14,7 +14,7 @@ use crate::{Depth, Timeout}; use super::{ request::{DavPropertyValue, DeadElementTag, DeadProperty}, response::{Ace, AclRestrictions, Href, List, SupportedPrivilege}, - Collation, + Collation, Namespace, }; #[derive(Debug, Clone, PartialEq, Eq)] @@ -58,6 +58,8 @@ pub enum WebDavProperty { AclRestrictions, InheritedAclSet, PrincipalCollectionSet, + // Apple proprietary properties + GetCTag, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -188,7 +190,10 @@ pub struct Comp(pub ICalendarComponentType); #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] -pub struct SupportedCollation(pub Collation); +pub struct SupportedCollation { + pub collation: Collation, + pub namespace: Namespace, +} #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] @@ -257,6 +262,41 @@ pub enum Privilege { ReadFreeBusy, } +impl Privilege { + pub fn all(is_calendar: bool) -> Vec { + if is_calendar { + vec![ + Privilege::All, + Privilege::Read, + Privilege::Write, + Privilege::WriteProperties, + Privilege::WriteContent, + Privilege::Unlock, + Privilege::ReadAcl, + Privilege::ReadCurrentUserPrivilegeSet, + Privilege::WriteAcl, + Privilege::Bind, + Privilege::Unbind, + Privilege::ReadFreeBusy, + ] + } else { + vec![ + Privilege::All, + Privilege::Read, + Privilege::Write, + Privilege::WriteProperties, + Privilege::WriteContent, + Privilege::Unlock, + Privilege::ReadAcl, + Privilege::ReadCurrentUserPrivilegeSet, + Privilege::WriteAcl, + Privilege::Bind, + Privilege::Unbind, + ] + } + } +} + impl From for DavPropertyValue { fn from(value: DavProperty) -> Self { DavPropertyValue { @@ -303,3 +343,44 @@ impl DavProperty { ) } } + +impl ReportSet { + pub fn calendar() -> Vec { + vec![ + ReportSet::SyncCollection, + ReportSet::AclPrincipalPropSet, + ReportSet::PrincipalMatch, + ReportSet::ExpandProperty, + ReportSet::CalendarQuery, + ReportSet::CalendarMultiGet, + ReportSet::FreeBusyQuery, + ] + } + + pub fn addressbook() -> Vec { + vec![ + ReportSet::SyncCollection, + ReportSet::AclPrincipalPropSet, + ReportSet::PrincipalMatch, + ReportSet::ExpandProperty, + ReportSet::AddressbookQuery, + ReportSet::AddressbookMultiGet, + ] + } + + pub fn file() -> Vec { + vec![ + ReportSet::SyncCollection, + ReportSet::AclPrincipalPropSet, + ReportSet::PrincipalMatch, + ] + } + + pub fn principal() -> Vec { + vec![ + ReportSet::PrincipalPropertySearch, + ReportSet::PrincipalSearchPropertySet, + ReportSet::PrincipalMatch, + ] + } +} diff --git a/crates/dav-proto/src/schema/response.rs b/crates/dav-proto/src/schema/response.rs index 96d3e8c6..0ca82947 100644 --- a/crates/dav-proto/src/schema/response.rs +++ b/crates/dav-proto/src/schema/response.rs @@ -15,11 +15,11 @@ use hyper::StatusCode; use super::{ property::{DavProperty, Privilege}, request::{DavPropertyValue, Filter}, - Namespace, + Namespaces, }; pub struct MultiStatus { - pub namespace: Namespace, + pub namespaces: Namespaces, pub response: List, pub response_description: Option, pub sync_token: Option, @@ -61,7 +61,7 @@ pub struct Href(pub String); pub struct List(pub Vec); pub struct MkColResponse { - pub namespace: Namespace, + pub namespaces: Namespaces, pub propstat: List, } @@ -76,7 +76,7 @@ pub struct PropStat { pub struct Prop(pub List); pub struct PropResponse { - pub namespace: Namespace, + pub namespaces: Namespaces, pub properties: List, } @@ -141,7 +141,7 @@ pub enum RequiredPrincipal { #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] pub struct PrincipalSearchPropertySet { - pub namespace: Namespace, + pub namespaces: Namespaces, pub properties: List, } @@ -153,7 +153,7 @@ pub struct PrincipalSearchProperty { } pub struct ErrorResponse { - pub namespace: Namespace, + pub namespaces: Namespaces, pub error: Condition, } diff --git a/crates/dav/src/card/copy_move.rs b/crates/dav/src/card/copy_move.rs index 1fb70813..d153361f 100644 --- a/crates/dav/src/card/copy_move.rs +++ b/crates/dav/src/card/copy_move.rs @@ -7,7 +7,7 @@ use common::{Server, auth::AccessToken}; use dav_proto::{Depth, RequestHeaders, schema::response::CardCondition}; use groupware::{ - DavName, + DavName, DestroyArchive, contact::{AddressBook, ContactCard}, hierarchy::DavHierarchy, }; @@ -17,18 +17,9 @@ use jmap_proto::types::{acl::Acl, collection::Collection}; use store::write::BatchBuilder; use trc::AddContext; -use crate::{ - DavError, DavErrorCondition, - card::{delete::delete_address_book, insert_addressbook, insert_card, update_card}, - common::uri::DavUriResource, - file::DavFileResource, -}; +use crate::{DavError, DavErrorCondition, common::uri::DavUriResource, file::DavFileResource}; -use super::{ - assert_is_unique_uid, - delete::{delete_address_book_and_cards, delete_card}, - update_addressbook, -}; +use super::assert_is_unique_uid; pub(crate) trait CardCopyMoveRequestHandler: Sync + Send { fn handle_card_copy_move_request( @@ -53,7 +44,7 @@ impl CardCopyMoveRequestHandler for Server { .into_owned_uri()?; let from_account_id = from_resource_.account_id; let from_resources = self - .fetch_dav_resources(from_account_id, Collection::AddressBook) + .fetch_dav_resources(access_token, from_account_id, Collection::AddressBook) .await .caused_by(trc::location!())?; let from_resource_name = from_resource_ @@ -102,7 +93,7 @@ impl CardCopyMoveRequestHandler for Server { let to_resources = if to_account_id == from_account_id { from_resources.clone() } else { - self.fetch_dav_resources(to_account_id, Collection::AddressBook) + self.fetch_dav_resources(access_token, to_account_id, Collection::AddressBook) .await .caused_by(trc::location!())? }; @@ -174,12 +165,6 @@ impl CardCopyMoveRequestHandler for Server { // Overwrite card let from_addressbook_id = from_resource.parent_id.unwrap(); let to_addressbook_id = to_resource.parent_id.unwrap(); - let to_base_path = headers.format_to_base_uri( - destination_resource_name - .rsplit_once('/') - .map(|(base, _)| base) - .unwrap_or(destination_resource_name), - ); // Validate ACL if (!access_token.is_member(from_account_id) @@ -212,6 +197,12 @@ impl CardCopyMoveRequestHandler for Server { return Err(DavError::Code(StatusCode::FORBIDDEN)); } + let to_base_path = to_resources + .format_resource(to_resource) + .rsplit_once('/') + .unwrap() + .0 + .to_string(); if is_move { move_card( self, @@ -300,7 +291,7 @@ impl CardCopyMoveRequestHandler for Server { to_account_id, None, to_addressbook_id, - headers.format_to_base_uri(&parent_resource.name), + to_resources.format_resource(parent_resource), new_name, ) .await @@ -324,7 +315,7 @@ impl CardCopyMoveRequestHandler for Server { from_addressbook_id, None, to_addressbook_id, - headers.format_to_base_uri(&parent_resource.name), + to_resources.format_resource(parent_resource), new_name, ) .await @@ -453,7 +444,7 @@ async fn copy_card( { return Err(DavError::Condition(DavErrorCondition::new( StatusCode::PRECONDITION_FAILED, - CardCondition::NoUidConflict(format!("{}/{}", to_base_path, name.name).into()), + CardCondition::NoUidConflict(format!("{}{}", to_base_path, name.name).into()), ))); } let mut new_card = card @@ -463,29 +454,27 @@ async fn copy_card( name: new_name.to_string(), parent_id: to_addressbook_id, }); - update_card( - access_token, - card, - new_card, - from_account_id, - from_document_id, - false, - &mut batch, - ) - .caused_by(trc::location!())?; + new_card + .update( + access_token, + card, + from_account_id, + from_document_id, + &mut batch, + ) + .caused_by(trc::location!())?; } else { // Validate UID assert_is_unique_uid( server, server - .fetch_dav_resources(to_account_id, Collection::AddressBook) + .fetch_dav_resources(access_token, to_account_id, Collection::AddressBook) .await .caused_by(trc::location!())? .as_ref(), to_account_id, to_addressbook_id, card.inner.card.uid(), - &to_base_path, ) .await?; @@ -501,15 +490,9 @@ async fn copy_card( .assign_document_ids(to_account_id, Collection::ContactCard, 1) .await .caused_by(trc::location!())?; - insert_card( - access_token, - new_card, - to_account_id, - to_document_id, - false, - &mut batch, - ) - .caused_by(trc::location!())?; + new_card + .insert(access_token, to_account_id, to_document_id, &mut batch) + .caused_by(trc::location!())?; } let response = if let Some(to_document_id) = to_document_id { @@ -523,15 +506,15 @@ async fn copy_card( .to_unarchived::() .caused_by(trc::location!())?; - delete_card( - access_token, - to_account_id, - to_document_id, - to_addressbook_id, - card, - &mut batch, - ) - .caused_by(trc::location!())?; + DestroyArchive(card) + .delete( + access_token, + to_account_id, + to_document_id, + to_addressbook_id, + &mut batch, + ) + .caused_by(trc::location!())?; } Ok(HttpResponse::new(StatusCode::NO_CONTENT)) @@ -577,7 +560,7 @@ async fn move_card( if name.parent_id == to_addressbook_id { return Err(DavError::Condition(DavErrorCondition::new( StatusCode::PRECONDITION_FAILED, - CardCondition::NoUidConflict(format!("{}/{}", to_base_path, name.name).into()), + CardCondition::NoUidConflict(format!("{}{}", to_base_path, name.name).into()), ))); } else if name.parent_id == from_addressbook_id { name_idx = Some(idx); @@ -599,29 +582,27 @@ async fn move_card( name: new_name.to_string(), parent_id: to_addressbook_id, }); - update_card( - access_token, - card.clone(), - new_card, - from_account_id, - from_document_id, - false, - &mut batch, - ) - .caused_by(trc::location!())?; + new_card + .update( + access_token, + card.clone(), + from_account_id, + from_document_id, + &mut batch, + ) + .caused_by(trc::location!())?; } else { // Validate UID assert_is_unique_uid( server, server - .fetch_dav_resources(to_account_id, Collection::AddressBook) + .fetch_dav_resources(access_token, to_account_id, Collection::AddressBook) .await .caused_by(trc::location!())? .as_ref(), to_account_id, to_addressbook_id, card.inner.card.uid(), - &to_base_path, ) .await?; @@ -633,30 +614,24 @@ async fn move_card( parent_id: to_addressbook_id, }]; - delete_card( - access_token, - from_account_id, - from_document_id, - from_addressbook_id, - card, - &mut batch, - ) - .caused_by(trc::location!())?; + DestroyArchive(card) + .delete( + access_token, + from_account_id, + from_document_id, + from_addressbook_id, + &mut batch, + ) + .caused_by(trc::location!())?; let to_document_id = server .store() .assign_document_ids(to_account_id, Collection::ContactCard, 1) .await .caused_by(trc::location!())?; - insert_card( - access_token, - new_card, - to_account_id, - to_document_id, - false, - &mut batch, - ) - .caused_by(trc::location!())?; + new_card + .insert(access_token, to_account_id, to_document_id, &mut batch) + .caused_by(trc::location!())?; } let response = if let Some(to_document_id) = to_document_id { @@ -670,15 +645,15 @@ async fn move_card( .to_unarchived::() .caused_by(trc::location!())?; - delete_card( - access_token, - to_account_id, - to_document_id, - to_addressbook_id, - card, - &mut batch, - ) - .caused_by(trc::location!())?; + DestroyArchive(card) + .delete( + access_token, + to_account_id, + to_document_id, + to_addressbook_id, + &mut batch, + ) + .caused_by(trc::location!())?; } Ok(HttpResponse::new(StatusCode::NO_CONTENT)) @@ -725,16 +700,9 @@ async fn rename_card( new_card.names[name_idx].name = new_name.to_string(); let mut batch = BatchBuilder::new(); - update_card( - access_token, - card, - new_card, - account_id, - document_id, - false, - &mut batch, - ) - .caused_by(trc::location!())?; + new_card + .update(access_token, card, account_id, document_id, &mut batch) + .caused_by(trc::location!())?; server .commit_batch(batch) .await @@ -773,14 +741,9 @@ async fn copy_container( let mut batch = BatchBuilder::new(); if remove_source { - delete_address_book( - access_token, - from_account_id, - from_document_id, - old_book, - &mut batch, - ) - .caused_by(trc::location!())?; + DestroyArchive(old_book) + .delete(access_token, from_account_id, from_document_id, &mut batch) + .caused_by(trc::location!())?; } book.name = new_name.to_string(); @@ -800,17 +763,17 @@ async fn copy_container( .to_unarchived::() .caused_by(trc::location!())?; - delete_address_book_and_cards( - server, - access_token, - to_account_id, - to_document_id, - to_children_ids, - book, - &mut batch, - ) - .await - .caused_by(trc::location!())?; + DestroyArchive(book) + .delete_with_cards( + server, + access_token, + to_account_id, + to_document_id, + to_children_ids, + &mut batch, + ) + .await + .caused_by(trc::location!())?; } to_document_id @@ -821,15 +784,8 @@ async fn copy_container( .await .caused_by(trc::location!())? }; - insert_addressbook( - access_token, - book, - to_account_id, - to_document_id, - false, - &mut batch, - ) - .caused_by(trc::location!())?; + book.insert(access_token, to_account_id, to_document_id, &mut batch) + .caused_by(trc::location!())?; // Copy children let mut required_space = 0; @@ -877,27 +833,26 @@ async fn copy_container( } new_card.names.push(new_name); - update_card( - access_token, - card, - new_card, - from_account_id, - from_document_id, - false, - &mut batch, - ) - .caused_by(trc::location!())?; - } else { - if remove_source { - delete_card( + new_card + .update( access_token, - from_account_id, - from_child_document_id, - from_document_id, card, + from_account_id, + from_document_id, &mut batch, ) .caused_by(trc::location!())?; + } else { + if remove_source { + DestroyArchive(card) + .delete( + access_token, + from_account_id, + from_child_document_id, + from_document_id, + &mut batch, + ) + .caused_by(trc::location!())?; } let to_document_id = server @@ -907,15 +862,9 @@ async fn copy_container( .caused_by(trc::location!())?; new_card.names = vec![new_name]; required_space += new_card.size as u64; - insert_card( - access_token, - new_card, - to_account_id, - to_document_id, - false, - &mut batch, - ) - .caused_by(trc::location!())?; + new_card + .insert(access_token, to_account_id, to_document_id, &mut batch) + .caused_by(trc::location!())?; } } } @@ -966,16 +915,9 @@ async fn rename_container( new_book.name = new_name.to_string(); let mut batch = BatchBuilder::new(); - update_addressbook( - access_token, - book, - new_book, - account_id, - document_id, - false, - &mut batch, - ) - .caused_by(trc::location!())?; + new_book + .update(access_token, book, account_id, document_id, &mut batch) + .caused_by(trc::location!())?; server .commit_batch(batch) .await diff --git a/crates/dav/src/card/delete.rs b/crates/dav/src/card/delete.rs index 1811f08c..688b687c 100644 --- a/crates/dav/src/card/delete.rs +++ b/crates/dav/src/card/delete.rs @@ -4,18 +4,17 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{ - Server, auth::AccessToken, sharing::EffectiveAcl, storage::index::ObjectIndexBuilder, -}; +use common::{Server, auth::AccessToken, sharing::EffectiveAcl}; use dav_proto::RequestHeaders; use groupware::{ - contact::{AddressBook, ArchivedAddressBook, ArchivedContactCard, ContactCard}, + DestroyArchive, + contact::{AddressBook, ContactCard}, hierarchy::DavHierarchy, }; use http_proto::HttpResponse; use hyper::StatusCode; use jmap_proto::types::{acl::Acl, collection::Collection}; -use store::write::{Archive, BatchBuilder}; +use store::write::BatchBuilder; use trc::AddContext; use crate::{ @@ -52,7 +51,7 @@ impl CardDeleteRequestHandler for Server { .filter(|r| !r.is_empty()) .ok_or(DavError::Code(StatusCode::FORBIDDEN))?; let resources = self - .fetch_dav_resources(account_id, Collection::AddressBook) + .fetch_dav_resources(access_token, account_id, Collection::AddressBook) .await .caused_by(trc::location!())?; @@ -105,21 +104,21 @@ impl CardDeleteRequestHandler for Server { .await?; // Delete addressbook and cards - delete_address_book_and_cards( - self, - access_token, - account_id, - document_id, - resources - .subtree(delete_path) - .filter(|r| !r.is_container) - .map(|r| r.document_id) - .collect::>(), - book, - &mut batch, - ) - .await - .caused_by(trc::location!())?; + DestroyArchive(book) + .delete_with_cards( + self, + access_token, + account_id, + document_id, + resources + .subtree(delete_path) + .filter(|r| !r.is_container) + .map(|r| r.document_id) + .collect::>(), + &mut batch, + ) + .await + .caused_by(trc::location!())?; } else { // Validate ACL let addressbook_id = delete_resource.parent_id.unwrap(); @@ -162,14 +161,16 @@ impl CardDeleteRequestHandler for Server { .await?; // Delete card - delete_card( + DestroyArchive( + card_ + .to_unarchived::() + .caused_by(trc::location!())?, + ) + .delete( access_token, account_id, document_id, addressbook_id, - card_ - .to_unarchived::() - .caused_by(trc::location!())?, &mut batch, ) .caused_by(trc::location!())?; @@ -180,111 +181,3 @@ impl CardDeleteRequestHandler for Server { Ok(HttpResponse::new(StatusCode::NO_CONTENT)) } } - -pub(crate) async fn delete_address_book_and_cards( - server: &Server, - access_token: &AccessToken, - account_id: u32, - document_id: u32, - children_ids: Vec, - book: Archive<&ArchivedAddressBook>, - batch: &mut BatchBuilder, -) -> trc::Result<()> { - // Process deletions - let addressbook_id = document_id; - for document_id in children_ids { - if let Some(card_) = server - .get_archive(account_id, Collection::ContactCard, document_id) - .await? - { - delete_card( - access_token, - account_id, - document_id, - addressbook_id, - card_ - .to_unarchived::() - .caused_by(trc::location!())?, - batch, - )?; - } - } - - delete_address_book(access_token, account_id, document_id, book, batch)?; - - Ok(()) -} - -pub(crate) fn delete_address_book( - access_token: &AccessToken, - account_id: u32, - document_id: u32, - book: Archive<&ArchivedAddressBook>, - batch: &mut BatchBuilder, -) -> trc::Result<()> { - // Delete addressbook - batch - .with_account_id(account_id) - .with_collection(Collection::AddressBook) - .delete_document(document_id) - .custom( - ObjectIndexBuilder::<_, ()>::new() - .with_tenant_id(access_token) - .with_current(book), - ) - .caused_by(trc::location!())? - .commit_point(); - - Ok(()) -} - -pub(crate) fn delete_card( - access_token: &AccessToken, - account_id: u32, - document_id: u32, - addressbook_id: u32, - card: Archive<&ArchivedContactCard>, - batch: &mut BatchBuilder, -) -> trc::Result<()> { - if let Some(delete_idx) = card - .inner - .names - .iter() - .position(|name| name.parent_id == addressbook_id) - { - batch - .with_account_id(account_id) - .with_collection(Collection::ContactCard); - - if card.inner.names.len() > 1 { - // Unlink addressbook id from card - let mut new_card = card - .deserialize::() - .caused_by(trc::location!())?; - new_card.names.swap_remove(delete_idx); - batch - .update_document(document_id) - .custom( - ObjectIndexBuilder::new() - .with_tenant_id(access_token) - .with_current(card) - .with_changes(new_card), - ) - .caused_by(trc::location!())?; - } else { - // Delete card - batch - .delete_document(document_id) - .custom( - ObjectIndexBuilder::<_, ()>::new() - .with_tenant_id(access_token) - .with_current(card), - ) - .caused_by(trc::location!())?; - } - - batch.commit_point(); - } - - Ok(()) -} diff --git a/crates/dav/src/card/get.rs b/crates/dav/src/card/get.rs index f5f34c75..b5aaa875 100644 --- a/crates/dav/src/card/get.rs +++ b/crates/dav/src/card/get.rs @@ -44,7 +44,7 @@ impl CardGetRequestHandler for Server { .into_owned_uri()?; let account_id = resource_.account_id; let resources = self - .fetch_dav_resources(account_id, Collection::AddressBook) + .fetch_dav_resources(access_token, account_id, Collection::AddressBook) .await .caused_by(trc::location!())?; let resource = resources diff --git a/crates/dav/src/card/mkcol.rs b/crates/dav/src/card/mkcol.rs index fd92ad40..23e77d76 100644 --- a/crates/dav/src/card/mkcol.rs +++ b/crates/dav/src/card/mkcol.rs @@ -24,7 +24,7 @@ use crate::{ }, }; -use super::{insert_addressbook, proppatch::CardPropPatchRequestHandler}; +use super::proppatch::CardPropPatchRequestHandler; pub(crate) trait CardMkColRequestHandler: Sync + Send { fn handle_card_mkcol_request( @@ -54,7 +54,7 @@ impl CardMkColRequestHandler for Server { if name.contains('/') || !access_token.is_member(account_id) { return Err(DavError::Code(StatusCode::FORBIDDEN)); } else if self - .fetch_dav_resources(account_id, Collection::AddressBook) + .fetch_dav_resources(access_token, account_id, Collection::AddressBook) .await .caused_by(trc::location!())? .paths @@ -109,15 +109,8 @@ impl CardMkColRequestHandler for Server { .assign_document_ids(account_id, Collection::AddressBook, 1) .await .caused_by(trc::location!())?; - insert_addressbook( - access_token, - book, - account_id, - document_id, - false, - &mut batch, - ) - .caused_by(trc::location!())?; + book.insert(access_token, account_id, document_id, &mut batch) + .caused_by(trc::location!())?; self.commit_batch(batch).await.caused_by(trc::location!())?; if let Some(prop_stat) = return_prop_stat { diff --git a/crates/dav/src/card/mod.rs b/crates/dav/src/card/mod.rs index b641d1b7..73019354 100644 --- a/crates/dav/src/card/mod.rs +++ b/crates/dav/src/card/mod.rs @@ -4,24 +4,18 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{DavResources, Server, auth::AccessToken, storage::index::ObjectIndexBuilder}; +use common::{DavResources, Server}; use dav_proto::schema::{ property::{CardDavProperty, DavProperty, WebDavProperty}, response::CardCondition, }; -use groupware::{ - IDX_CARD_UID, - contact::{AddressBook, ArchivedAddressBook, ArchivedContactCard, ContactCard}, -}; +use groupware::IDX_CARD_UID; use hyper::StatusCode; use jmap_proto::types::collection::Collection; -use store::{ - query::Filter, - write::{Archive, BatchBuilder, now}, -}; +use store::query::Filter; use trc::AddContext; -use crate::{DavError, DavErrorCondition, common::ExtractETag}; +use crate::{DavError, DavErrorCondition}; pub mod copy_move; pub mod delete; @@ -105,125 +99,12 @@ pub(crate) static CARD_ALL_PROPS: [DavProperty; 22] = [ DavProperty::CardDav(CardDavProperty::MaxResourceSize), ]; -pub(crate) fn update_card( - access_token: &AccessToken, - card: Archive<&ArchivedContactCard>, - mut new_card: ContactCard, - account_id: u32, - document_id: u32, - with_etag: bool, - batch: &mut BatchBuilder, -) -> trc::Result> { - // Build card - new_card.modified = now() as i64; - - // Prepare write batch - batch - .with_account_id(account_id) - .with_collection(Collection::ContactCard) - .update_document(document_id) - .custom( - ObjectIndexBuilder::new() - .with_current(card) - .with_changes(new_card) - .with_tenant_id(access_token), - )? - .commit_point(); - - Ok(if with_etag { batch.etag() } else { None }) -} - -pub(crate) fn insert_card( - access_token: &AccessToken, - mut card: ContactCard, - account_id: u32, - document_id: u32, - with_etag: bool, - batch: &mut BatchBuilder, -) -> trc::Result> { - // Build card - let now = now() as i64; - card.modified = now; - card.created = now; - - // Prepare write batch - batch - .with_account_id(account_id) - .with_collection(Collection::ContactCard) - .create_document(document_id) - .custom( - ObjectIndexBuilder::<(), _>::new() - .with_changes(card) - .with_tenant_id(access_token), - )? - .commit_point(); - - Ok(if with_etag { batch.etag() } else { None }) -} - -pub(crate) fn insert_addressbook( - access_token: &AccessToken, - mut book: AddressBook, - account_id: u32, - document_id: u32, - with_etag: bool, - batch: &mut BatchBuilder, -) -> trc::Result> { - // Build card - let now = now() as i64; - book.modified = now; - book.created = now; - - // Prepare write batch - batch - .with_account_id(account_id) - .with_collection(Collection::AddressBook) - .create_document(document_id) - .custom( - ObjectIndexBuilder::<(), _>::new() - .with_changes(book) - .with_tenant_id(access_token), - )? - .commit_point(); - - Ok(if with_etag { batch.etag() } else { None }) -} - -pub(crate) fn update_addressbook( - access_token: &AccessToken, - book: Archive<&ArchivedAddressBook>, - mut new_book: AddressBook, - account_id: u32, - document_id: u32, - with_etag: bool, - batch: &mut BatchBuilder, -) -> trc::Result> { - // Build card - new_book.modified = now() as i64; - - // Prepare write batch - batch - .with_account_id(account_id) - .with_collection(Collection::AddressBook) - .update_document(document_id) - .custom( - ObjectIndexBuilder::new() - .with_current(book) - .with_changes(new_book) - .with_tenant_id(access_token), - )? - .commit_point(); - - Ok(if with_etag { batch.etag() } else { None }) -} - pub(crate) async fn assert_is_unique_uid( server: &Server, resources: &DavResources, account_id: u32, addressbook_id: u32, uid: Option<&str>, - base_uri: &str, ) -> crate::Result<()> { if let Some(uid) = uid { let hits = server @@ -243,7 +124,7 @@ pub(crate) async fn assert_is_unique_uid( { return Err(DavError::Condition(DavErrorCondition::new( StatusCode::PRECONDITION_FAILED, - CardCondition::NoUidConflict(format!("{}/{}", base_uri, path.name).into()), + CardCondition::NoUidConflict(resources.format_resource(path).into()), ))); } } diff --git a/crates/dav/src/card/proppatch.rs b/crates/dav/src/card/proppatch.rs index ea3f5f35..e7a4bc3c 100644 --- a/crates/dav/src/card/proppatch.rs +++ b/crates/dav/src/card/proppatch.rs @@ -27,14 +27,12 @@ use trc::AddContext; use crate::{ DavError, DavMethod, common::{ - ETag, + ETag, ExtractETag, lock::{LockRequestHandler, ResourceState}, uri::DavUriResource, }, }; -use super::{update_addressbook, update_card}; - pub(crate) trait CardPropPatchRequestHandler: Sync + Send { fn handle_card_proppatch_request( &self, @@ -75,7 +73,7 @@ impl CardPropPatchRequestHandler for Server { let uri = headers.uri; let account_id = resource_.account_id; let resources = self - .fetch_dav_resources(account_id, Collection::AddressBook) + .fetch_dav_resources(access_token, account_id, Collection::AddressBook) .await .caused_by(trc::location!())?; let resource = resource_ @@ -172,16 +170,10 @@ impl CardPropPatchRequestHandler for Server { } if is_success { - update_addressbook( - access_token, - book, - new_book, - account_id, - document_id, - true, - &mut batch, - ) - .caused_by(trc::location!())? + new_book + .update(access_token, book, account_id, document_id, &mut batch) + .caused_by(trc::location!())? + .etag() } else { book.etag().into() } @@ -212,16 +204,10 @@ impl CardPropPatchRequestHandler for Server { } if is_success { - update_card( - access_token, - card, - new_card, - account_id, - document_id, - true, - &mut batch, - ) - .caused_by(trc::location!())? + new_card + .update(access_token, card, account_id, document_id, &mut batch) + .caused_by(trc::location!())? + .etag() } else { card.etag().into() } diff --git a/crates/dav/src/card/query.rs b/crates/dav/src/card/query.rs index 3ccfcfd3..71a0ef41 100644 --- a/crates/dav/src/card/query.rs +++ b/crates/dav/src/card/query.rs @@ -50,7 +50,7 @@ impl CardQueryRequestHandler for Server { .into_owned_uri()?; let account_id = resource_.account_id; let resources = self - .fetch_dav_resources(account_id, Collection::AddressBook) + .fetch_dav_resources(access_token, account_id, Collection::AddressBook) .await .caused_by(trc::location!())?; let resource = resources @@ -82,13 +82,16 @@ impl CardQueryRequestHandler for Server { // 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)); + items.push(PropFindItem::new( + resources.format_resource(resource), + account_id, + resource, + )); } } diff --git a/crates/dav/src/card/update.rs b/crates/dav/src/card/update.rs index a50fbb97..70abb112 100644 --- a/crates/dav/src/card/update.rs +++ b/crates/dav/src/card/update.rs @@ -20,14 +20,14 @@ use trc::AddContext; use crate::{ DavError, DavErrorCondition, DavMethod, common::{ - ETag, + ETag, ExtractETag, lock::{LockRequestHandler, ResourceState}, uri::DavUriResource, }, file::DavFileResource, }; -use super::{assert_is_unique_uid, insert_card, update_card}; +use super::assert_is_unique_uid; pub(crate) trait CardUpdateRequestHandler: Sync + Send { fn handle_card_update_request( @@ -54,7 +54,7 @@ impl CardUpdateRequestHandler for Server { .into_owned_uri()?; let account_id = resource.account_id; let resources = self - .fetch_dav_resources(account_id, Collection::AddressBook) + .fetch_dav_resources(access_token, account_id, Collection::AddressBook) .await .caused_by(trc::location!())?; let resource_name = resource @@ -169,9 +169,7 @@ impl CardUpdateRequestHandler for Server { _ => { return Err(DavError::Condition(DavErrorCondition::new( StatusCode::PRECONDITION_FAILED, - CardCondition::NoUidConflict( - headers.format_to_base_uri(resource_name).into(), - ), + CardCondition::NoUidConflict(resources.format_resource(resource).into()), ))); } } @@ -185,16 +183,10 @@ impl CardUpdateRequestHandler for Server { // Prepare write batch let mut batch = BatchBuilder::new(); - let etag = update_card( - access_token, - card, - new_card, - account_id, - document_id, - true, - &mut batch, - ) - .caused_by(trc::location!())?; + let etag = new_card + .update(access_token, card, account_id, document_id, &mut batch) + .caused_by(trc::location!())? + .etag(); self.commit_batch(batch).await.caused_by(trc::location!())?; Ok(HttpResponse::new(StatusCode::NO_CONTENT).with_etag_opt(etag)) @@ -251,7 +243,6 @@ impl CardUpdateRequestHandler for Server { account_id, parent.document_id, vcard.uid(), - headers.base_uri.unwrap_or_default(), ) .await?; @@ -273,15 +264,10 @@ impl CardUpdateRequestHandler for Server { .assign_document_ids(account_id, Collection::ContactCard, 1) .await .caused_by(trc::location!())?; - let etag = insert_card( - access_token, - card, - account_id, - document_id, - true, - &mut batch, - ) - .caused_by(trc::location!())?; + let etag = card + .insert(access_token, account_id, document_id, &mut batch) + .caused_by(trc::location!())? + .etag(); self.commit_batch(batch).await.caused_by(trc::location!())?; Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag)) diff --git a/crates/dav/src/common/acl.rs b/crates/dav/src/common/acl.rs index f30a3a9f..7b56f845 100644 --- a/crates/dav/src/common/acl.rs +++ b/crates/dav/src/common/acl.rs @@ -5,7 +5,6 @@ */ use common::{Server, auth::AccessToken, sharing::EffectiveAcl}; -use compact_str::format_compact; use dav_proto::{ RequestHeaders, schema::{ @@ -25,14 +24,15 @@ use jmap_proto::types::{ collection::Collection, value::{AclGrant, ArchivedAclGrant}, }; +use percent_encoding::NON_ALPHANUMERIC; use rkyv::vec::ArchivedVec; use store::{ahash::AHashSet, roaring::RoaringBitmap, write::BatchBuilder}; use trc::AddContext; use utils::map::bitmap::Bitmap; use crate::{ - DavError, DavErrorCondition, DavResource, card::update_addressbook, - common::uri::DavUriResource, file::update_file_node, principal::propfind::PrincipalPropFind, + DavError, DavErrorCondition, DavResourceName, common::uri::DavUriResource, + principal::propfind::PrincipalPropFind, }; use super::ArchivedResource; @@ -108,7 +108,7 @@ impl DavAclHandler for Server { return Err(DavError::Code(StatusCode::FORBIDDEN)); } let resources = self - .fetch_dav_resources(account_id, collection) + .fetch_dav_resources(access_token, account_id, collection) .await .caused_by(trc::location!())?; let resource = resource_ @@ -152,31 +152,29 @@ impl DavAclHandler for Server { .deserialize::() .caused_by(trc::location!())?; new_book.acls = grants; - update_addressbook( - access_token, - book, - new_book, - account_id, - resource.document_id, - false, - &mut batch, - ) - .caused_by(trc::location!())?; + new_book + .update( + access_token, + book, + account_id, + resource.document_id, + &mut batch, + ) + .caused_by(trc::location!())?; } ArchivedResource::FileNode(node) => { let mut new_node = node.deserialize::().caused_by(trc::location!())?; new_node.acls = grants; - update_file_node( - access_token, - node, - new_node, - account_id, - resource.document_id, - false, - &mut batch, - ) - .caused_by(trc::location!())?; + new_node + .update( + access_token, + node, + account_id, + resource.document_id, + &mut batch, + ) + .caused_by(trc::location!())?; } _ => unreachable!(), } @@ -198,7 +196,7 @@ impl DavAclHandler for Server { .await .and_then(|uri| uri.into_owned_uri())?; let uri = self - .map_uri_resource(uri) + .map_uri_resource(access_token, uri) .await .caused_by(trc::location!())? .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; @@ -497,9 +495,12 @@ impl DavAclHandler for Server { aces.push(Ace::new( Principal::Href(Href(format!( - "{}/{}", - DavResource::Principal.base_path(), - grant_account_name, + "{}/{}/", + DavResourceName::Principal.base_path(), + percent_encoding::utf8_percent_encode( + &grant_account_name, + NON_ALPHANUMERIC + ), ))), GrantDeny::grant(privileges), )); @@ -515,6 +516,7 @@ pub(crate) trait Privileges { &self, account_id: u32, grants: &ArchivedVec, + is_calendar: bool, ) -> Vec; } @@ -523,21 +525,10 @@ impl Privileges for AccessToken { &self, account_id: u32, grants: &ArchivedVec, + is_calendar: bool, ) -> Vec { if self.is_member(account_id) { - vec![ - Privilege::Read, - Privilege::Write, - Privilege::WriteProperties, - Privilege::WriteContent, - Privilege::Unlock, - Privilege::ReadAcl, - Privilege::ReadCurrentUserPrivilegeSet, - Privilege::WriteAcl, - Privilege::Bind, - Privilege::Unbind, - Privilege::ReadFreeBusy, - ] + Privilege::all(is_calendar) } else { let mut acls = AHashSet::with_capacity(16); for grant in grants.effective_acl(self) { diff --git a/crates/dav/src/common/lock.rs b/crates/dav/src/common/lock.rs index 1c29eb49..007db54d 100644 --- a/crates/dav/src/common/lock.rs +++ b/crates/dav/src/common/lock.rs @@ -122,6 +122,7 @@ impl LockRequestHandler for Server { ..Default::default() }]; + let mut base_path = None; let is_lock_request = !matches!(lock_info, LockRequest::Unlock); let if_lock_token = headers .if_ @@ -170,7 +171,9 @@ impl LockRequestHandler for Server { || lock_item.depth_infinity && resource_path.len() > lock_path.len() || is_infinity && lock_path.len() > resource_path.len()) { - failed_locks.push(headers.format_to_base_uri(lock_path).into()); + let base_path = + base_path.get_or_insert_with(|| headers.base_uri().unwrap_or_default()); + failed_locks.push(format!("{base_path}/{lock_path}").into()); } } @@ -266,7 +269,8 @@ impl LockRequestHandler for Server { lock_item.exclusive = matches!(lock_info.lock_scope, LockScope::Exclusive); } - let active_lock = lock_item.to_active_lock(headers.format_to_base_uri(resource_path)); + let base_path = base_path.get_or_insert_with(|| headers.base_uri().unwrap_or_default()); + let active_lock = lock_item.to_active_lock(format!("{base_path}/{resource_path}")); HttpResponse::new(StatusCode::CREATED) .with_lock_token(&active_lock.lock_token.as_ref().unwrap().0) @@ -364,6 +368,8 @@ impl LockRequestHandler for Server { method, DavMethod::GET | DavMethod::HEAD | DavMethod::LOCK | DavMethod::UNLOCK ) { + let mut base_path = None; + 'outer: for (pos, resource) in resources.iter().enumerate() { if pos == 0 && matches!(method, DavMethod::COPY) { continue; @@ -382,7 +388,11 @@ impl LockRequestHandler for Server { }) { break 'outer; } else { - failed_locks.push(headers.format_to_base_uri(lock_path).into()); + let base_path = base_path.get_or_insert_with(|| { + headers.base_uri() + .unwrap_or_default() + }); + failed_locks.push(format!("{base_path}/{lock_path}").into()); } } @@ -475,11 +485,14 @@ impl LockRequestHandler for Server { if needs_etag && resource_state.etag.is_none() { if resource_state.document_id.is_none() { resource_state.document_id = self - .map_uri_resource(UriResource { - collection: resource_state.collection, - account_id: resource_state.account_id, - resource: resource_state.path.into(), - }) + .map_uri_resource( + access_token, + UriResource { + collection: resource_state.collection, + account_id: resource_state.account_id, + resource: resource_state.path.into(), + }, + ) .await .caused_by(trc::location!())? .map(|uri| uri.resource) diff --git a/crates/dav/src/common/mod.rs b/crates/dav/src/common/mod.rs index e1e7c523..c5f897b3 100644 --- a/crates/dav/src/common/mod.rs +++ b/crates/dav/src/common/mod.rs @@ -38,10 +38,9 @@ pub mod lock; pub mod propfind; pub mod uri; -#[derive(Default)] +#[derive(Default, Debug)] pub(crate) struct DavQuery<'x> { pub resource: DavQueryResource<'x>, - pub base_uri: &'x str, pub propfind: PropFind, pub from_change_id: Option, pub depth: usize, @@ -50,7 +49,7 @@ pub(crate) struct DavQuery<'x> { pub depth_no_root: bool, } -#[derive(Default)] +#[derive(Default, Debug)] pub(crate) enum DavQueryResource<'x> { Uri(OwnedUri<'x>), Multiget { @@ -70,6 +69,7 @@ pub(crate) type AddressbookFilter = Vec, ICalendarProperty, ICalendarParameterName>>; +#[derive(Debug)] pub(crate) enum DavQueryFilter { Addressbook(AddressbookFilter), Calendar { @@ -136,7 +136,6 @@ impl<'x> DavQuery<'x> { Self { resource: DavQueryResource::Uri(resource), propfind, - base_uri: headers.base_uri.unwrap_or_default(), depth: match headers.depth { Depth::Zero => 0, _ => 1, @@ -158,7 +157,6 @@ impl<'x> DavQuery<'x> { parent_collection: collection, }, propfind: multiget.properties, - base_uri: headers.base_uri.unwrap_or_default(), ret: headers.ret, depth_no_root: headers.depth_no_root, ..Default::default() @@ -177,7 +175,6 @@ impl<'x> DavQuery<'x> { 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, @@ -200,7 +197,6 @@ impl<'x> DavQuery<'x> { items, }, propfind: query.properties, - base_uri: headers.base_uri.unwrap_or_default(), ret: headers.ret, depth_no_root: headers.depth_no_root, ..Default::default() @@ -215,7 +211,6 @@ impl<'x> DavQuery<'x> { Self { resource: DavQueryResource::Uri(resource), propfind: changes.properties, - base_uri: headers.base_uri.unwrap_or_default(), from_change_id: changes .sync_token .as_deref() @@ -227,14 +222,9 @@ impl<'x> DavQuery<'x> { limit: changes.limit, ret: headers.ret, depth_no_root: headers.depth_no_root, - ..Default::default() } } - pub fn format_to_base_uri(&self, path: &str) -> String { - format!("{}/{}", self.base_uri, path) - } - pub fn is_minimal(&self) -> bool { self.ret == Return::Minimal } diff --git a/crates/dav/src/common/propfind.rs b/crates/dav/src/common/propfind.rs index b95934e8..9b99502a 100644 --- a/crates/dav/src/common/propfind.rs +++ b/crates/dav/src/common/propfind.rs @@ -13,11 +13,13 @@ use common::{ }; use dav_proto::{ Depth, RequestHeaders, + parser::header::dav_base_uri, schema::{ - Collation, + Collation, Namespace, property::{ - ActiveLock, CardDavProperty, DavProperty, DavValue, Privilege, ResourceType, - Rfc1123DateTime, SupportedCollation, SupportedLock, WebDavProperty, + ActiveLock, CardDavProperty, DavProperty, DavValue, PrincipalProperty, Privilege, + ReportSet, ResourceType, Rfc1123DateTime, SupportedCollation, SupportedLock, + WebDavProperty, }, request::{DavPropertyValue, PropFind}, response::{ @@ -26,14 +28,12 @@ use dav_proto::{ }, }, }; -use directory::{ - Type, - backend::internal::{PrincipalField, manage::ManageDirectory}, -}; -use groupware::hierarchy::DavHierarchy; +use directory::{Type, backend::internal::manage::ManageDirectory}; +use groupware::{DavResourceName, hierarchy::DavHierarchy}; use http_proto::HttpResponse; use hyper::StatusCode; use jmap_proto::types::{acl::Acl, collection::Collection}; +use percent_encoding::NON_ALPHANUMERIC; use store::{ ahash::AHashMap, query::log::Query, @@ -97,6 +97,7 @@ pub(crate) struct PropFindAccountQuota { pub available: u64, } +#[derive(Debug)] pub(crate) struct PropFindItem { pub name: String, pub account_id: u32, @@ -155,7 +156,11 @@ impl PropFindRequestHandler for Server { if let Some(resource) = resource.resource { response.add_response(Response::new_status( - [headers.format_to_base_uri(resource)], + [format!( + "{}/{}", + headers.base_uri().unwrap_or_default(), + resource + )], StatusCode::NOT_FOUND, )); } else { @@ -179,40 +184,102 @@ impl PropFindRequestHandler for Server { // Add container info if !headers.depth_no_root { - let mut prop_stat = match &request { - PropFind::PropName | PropFind::AllProp(_) => { - vec![ - DavPropertyValue::empty(DavProperty::WebDav( - WebDavProperty::ResourceType, - )), - DavPropertyValue::empty(DavProperty::WebDav( - WebDavProperty::CurrentUserPrincipal, - )), - ] - } - PropFind::Prop(items) => { - items.iter().cloned().map(DavPropertyValue::empty).collect() + let properties = match &request { + PropFind::PropName => { + response.add_response(Response::new_propstat( + resource.collection_path(), + vec![PropStat::new_list(vec![ + DavPropertyValue::empty(DavProperty::WebDav( + WebDavProperty::ResourceType, + )), + DavPropertyValue::empty(DavProperty::WebDav( + WebDavProperty::CurrentUserPrincipal, + )), + DavPropertyValue::empty(DavProperty::WebDav( + WebDavProperty::SupportedReportSet, + )), + ])], + )); + &[] } + PropFind::AllProp(_) => [ + DavProperty::WebDav(WebDavProperty::ResourceType), + DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal), + DavProperty::WebDav(WebDavProperty::SupportedReportSet), + ] + .as_slice(), + PropFind::Prop(items) => items, }; if !matches!(request, PropFind::PropName) { - for prop in &mut prop_stat { - match &prop.property { + let mut fields = Vec::with_capacity(properties.len()); + let mut fields_not_found = Vec::new(); + + for prop in properties { + match &prop { DavProperty::WebDav(WebDavProperty::ResourceType) => { - prop.value = vec![ResourceType::Collection].into(); + fields.push(DavPropertyValue::new( + prop.clone(), + vec![ResourceType::Collection], + )); } DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal) => { - prop.value = vec![access_token.current_user_principal()].into(); + fields.push(DavPropertyValue::new( + prop.clone(), + vec![access_token.current_user_principal()], + )); + } + DavProperty::Principal(PrincipalProperty::AddressbookHomeSet) => { + fields.push(DavPropertyValue::new( + prop.clone(), + vec![Href(format!( + "{}/{}/", + DavResourceName::Card.base_path(), + percent_encoding::utf8_percent_encode( + &access_token.name, + NON_ALPHANUMERIC + ), + ))], + )); + response.set_namespace(Namespace::CardDav); + } + DavProperty::WebDav(WebDavProperty::SupportedReportSet) => { + let reports = match resource.collection { + Collection::Principal => ReportSet::principal(), + Collection::Calendar | Collection::CalendarEvent => { + ReportSet::calendar() + } + Collection::AddressBook | Collection::ContactCard => { + ReportSet::addressbook() + } + _ => ReportSet::file(), + }; + + fields.push(DavPropertyValue::new(prop.clone(), reports)); + } + _ => { + fields_not_found.push(DavPropertyValue::empty(prop.clone())); } - _ => (), } } - } - response.add_response(Response::new_propstat( - resource.base_path(), - vec![PropStat::new_list(prop_stat)], - )); + let mut prop_stat = Vec::with_capacity(2); + + if !fields.is_empty() { + prop_stat.push(PropStat::new_list(fields)); + } + + if !fields_not_found.is_empty() { + prop_stat.push( + PropStat::new_list(fields_not_found).with_status(StatusCode::NOT_FOUND), + ); + } + + response.add_response(Response::new_propstat( + resource.collection_path(), + prop_stat, + )); + } } if return_children { @@ -259,19 +326,23 @@ impl PropFindRequestHandler for Server { let mut data = PropFindData::new(); let collection_container; let collection_children; + let mut ctag = None; let mut paths; let mut query_filter = None; + //let c = println!("handling DAV query {query:#?}"); + match std::mem::take(&mut query.resource) { DavQueryResource::Uri(resource) => { let account_id = resource.account_id; collection_container = resource.collection; collection_children = collection_container.child_collection().unwrap(); let resources = self - .fetch_dav_resources(account_id, collection_container) + .fetch_dav_resources(access_token, account_id, collection_container) .await .caused_by(trc::location!())?; response.set_namespace(collection_container.namespace()); + ctag = Some(resources.modseq.unwrap_or_default()); // Obtain document ids let mut document_ids = if !access_token.is_member(account_id) { @@ -335,7 +406,9 @@ impl PropFindRequestHandler for Server { .as_ref() .is_none_or(|d| d.contains(item.document_id)) }) - .map(|item| PropFindItem::new(query.base_uri, account_id, item)) + .map(|item| { + PropFindItem::new(resources.format_resource(item), account_id, item) + }) .collect::>() } else { if !query.depth_no_root || query.from_change_id.is_none() { @@ -353,6 +426,7 @@ impl PropFindRequestHandler for Server { .with_xml_body(response.to_string())); } } + resources .tree_with_depth(query.depth - 1) .filter(|item| { @@ -360,15 +434,19 @@ impl PropFindRequestHandler for Server { .as_ref() .is_none_or(|d| d.contains(item.document_id)) }) - .map(|item| PropFindItem::new(query.base_uri, account_id, item)) + .map(|item| { + PropFindItem::new(resources.format_resource(item), account_id, item) + }) .collect::>() }; if paths.is_empty() && query.from_change_id.is_none() { - response.add_response(Response::new_status( - [query.format_to_base_uri(resource.resource.unwrap_or_default())], - StatusCode::NOT_FOUND, - )); + if let Some(resource) = resource.resource { + response.add_response(Response::new_status( + [resources.format_item(resource)], + StatusCode::NOT_FOUND, + )); + } return Ok(HttpResponse::new(StatusCode::MULTI_STATUS) .with_xml_body(response.to_string())); @@ -409,7 +487,7 @@ impl PropFindRequestHandler for Server { resources.clone() } else { let resources = self - .fetch_dav_resources(account_id, collection_container) + .fetch_dav_resources(access_token, account_id, collection_container) .await .caused_by(trc::location!())?; let document_ids = Arc::new(if !access_token.is_member(account_id) { @@ -430,6 +508,15 @@ impl PropFindRequestHandler for Server { (resources, document_ids) }; + let c = println!( + "resources: {:?} resource: {resource:?}", + resources + .paths + .iter() + .map(|r| r.name.to_string()) + .collect::>() + ); + if let Some(resource) = resource .resource .and_then(|name| resources.paths.by_name(name)) @@ -440,7 +527,11 @@ impl PropFindRequestHandler for Server { .as_ref() .is_none_or(|docs| docs.contains(resource.document_id)) { - paths.push(PropFindItem::new(query.base_uri, account_id, resource)); + paths.push(PropFindItem::new( + resources.format_resource(resource), + account_id, + resource, + )); } else { response.add_response( Response::new_status([item], StatusCode::FORBIDDEN) @@ -484,7 +575,7 @@ impl PropFindRequestHandler for Server { } let mut is_all_prop = false; - let todo = "prop lists"; + let todo = "prop lists for calendar"; let properties = match &query.propfind { PropFind::PropName => { let (container_props, children_props) = match collection_container { @@ -636,6 +727,25 @@ impl PropFindRequestHandler for Server { DavValue::String(archive_.etag()), )); } + WebDavProperty::GetCTag => { + if item.is_container { + let ctag = if let Some(ctag) = ctag { + ctag + } else { + self.store() + .get_last_change_id(account_id, collection) + .await? + .unwrap_or_default() + }; + fields.push(DavPropertyValue::new( + property.clone(), + DavValue::String(format!("\"{ctag}\"")), + )); + } else { + fields_not_found.push(DavPropertyValue::empty(property.clone())); + } + response.set_namespace(Namespace::CalendarServer); + } WebDavProperty::GetLastModified => { fields.push(DavPropertyValue::new( property.clone(), @@ -651,7 +761,7 @@ impl PropFindRequestHandler for Server { } WebDavProperty::LockDiscovery => { if let Some(locks) = data - .locks(self, account_id, collection_container, &query, &item) + .locks(self, account_id, collection_container, &item) .await .caused_by(trc::location!())? { @@ -793,7 +903,11 @@ impl PropFindRequestHandler for Server { if let Some(acls) = archive.acls() { fields.push(DavPropertyValue::new( property.clone(), - access_token.current_privilege_set(account_id, acls), + access_token.current_privilege_set( + account_id, + acls, + collection_container == Collection::Calendar, + ), )); } else if !is_all_prop { fields_not_found.push(DavPropertyValue::empty(property.clone())); @@ -825,7 +939,9 @@ impl PropFindRequestHandler for Server { WebDavProperty::PrincipalCollectionSet => { fields.push(DavPropertyValue::new( property.clone(), - vec![Href(crate::DavResource::Principal.base_path().to_string())], + vec![Href( + DavResourceName::Principal.collection_path().to_string(), + )], )); } }, @@ -862,9 +978,14 @@ impl PropFindRequestHandler for Server { fields.push(DavPropertyValue::new( property.clone(), DavValue::Collations(List(vec![ - SupportedCollation(Collation::AsciiCasemap), - SupportedCollation(Collation::UnicodeCasemap), - SupportedCollation(Collation::Octet), + SupportedCollation { + collation: Collation::AsciiCasemap, + namespace: Namespace::CardDav, + }, + SupportedCollation { + collation: Collation::UnicodeCasemap, + namespace: Namespace::CardDav, + }, ])), )); } @@ -970,7 +1091,7 @@ impl PropFindRequestHandler for Server { } else if let Some(tenant) = resource_token.tenant.filter(|t| t.quota > 0) { tenant.quota } else { - u64::MAX + u32::MAX as u64 }; let used = self .get_used_quota(account_id) @@ -985,9 +1106,9 @@ impl PropFindRequestHandler for Server { } impl PropFindItem { - pub fn new(base_uri: &str, account_id: u32, resource: &DavResource) -> Self { + pub fn new(name: String, account_id: u32, resource: &DavResource) -> Self { Self { - name: format!("{}{}", base_uri, resource.name), + name, account_id, document_id: resource.document_id, is_container: resource.is_container, @@ -1062,7 +1183,6 @@ impl PropFindData { server: &Server, account_id: u32, collection_container: Collection, - query: &DavQuery<'_>, item: &PropFindItem, ) -> trc::Result>> { let data = self.accounts.entry(account_id).or_default(); @@ -1081,11 +1201,12 @@ impl PropFindData { } if let Some(lock_data) = &data.locks { + let base_uri = dav_base_uri(&item.name).unwrap_or_default(); lock_data.unarchive::().map(|locks| { locks - .find_locks(&item.name.strip_prefix(query.base_uri).unwrap()[1..], false) + .find_locks(&item.name.strip_prefix(base_uri).unwrap()[1..], false) .iter() - .map(|(path, lock)| lock.to_active_lock(query.format_to_base_uri(path))) + .map(|(path, lock)| lock.to_active_lock(format!("{base_uri}/{path}"))) .collect::>() .into() }) diff --git a/crates/dav/src/common/uri.rs b/crates/dav/src/common/uri.rs index cde510d8..7c88b8b4 100644 --- a/crates/dav/src/common/uri.rs +++ b/crates/dav/src/common/uri.rs @@ -15,7 +15,7 @@ use hyper::StatusCode; use jmap_proto::types::collection::Collection; use trc::AddContext; -use crate::{DavError, DavResource}; +use crate::{DavError, DavResourceName}; #[derive(Debug)] pub(crate) struct UriResource { @@ -42,6 +42,7 @@ pub(crate) trait DavUriResource: Sync + Send { fn map_uri_resource( &self, + access_token: &AccessToken, uri: OwnedUri<'_>, ) -> impl Future>> + Send; } @@ -63,7 +64,7 @@ impl DavUriResource for Server { let mut resource = UriResource { collection: uri_parts .next() - .and_then(DavResource::parse) + .and_then(DavResourceName::parse) .ok_or(DavError::Code(StatusCode::NOT_FOUND))? .into(), account_id: None, @@ -103,10 +104,14 @@ impl DavUriResource for Server { Ok(resource) } - async fn map_uri_resource(&self, uri: OwnedUri<'_>) -> trc::Result> { + async fn map_uri_resource( + &self, + access_token: &AccessToken, + uri: OwnedUri<'_>, + ) -> trc::Result> { if let Some(resource) = uri.resource { if let Some(resource) = self - .fetch_dav_resources(uri.account_id, uri.collection) + .fetch_dav_resources(access_token, uri.account_id, uri.collection) .await .caused_by(trc::location!())? .paths @@ -159,8 +164,8 @@ impl OwnedUri<'_> { } impl UriResource { - pub fn base_path(&self) -> &'static str { - DavResource::from(self.collection).base_path() + pub fn collection_path(&self) -> &'static str { + DavResourceName::from(self.collection).collection_path() } } diff --git a/crates/dav/src/file/copy_move.rs b/crates/dav/src/file/copy_move.rs index b8835b13..55d0dc2d 100644 --- a/crates/dav/src/file/copy_move.rs +++ b/crates/dav/src/file/copy_move.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use common::{DavResources, Server, auth::AccessToken, storage::index::ObjectIndexBuilder}; use dav_proto::{Depth, RequestHeaders}; -use groupware::{file::FileNode, hierarchy::DavHierarchy}; +use groupware::{DestroyArchive, file::FileNode, hierarchy::DavHierarchy}; use http_proto::HttpResponse; use hyper::StatusCode; use jmap_proto::types::{acl::Acl, collection::Collection}; @@ -22,14 +22,15 @@ use utils::map::bitmap::Bitmap; use crate::{ DavError, DavMethod, common::{ + ExtractETag, acl::DavAclHandler, lock::{LockRequestHandler, ResourceState}, uri::{DavUriResource, UriResource}, }, - file::{DavFileResource, FileItemId, insert_file_node, update_file_node}, + file::{DavFileResource, FileItemId}, }; -use super::{FromDavResource, delete::delete_files, delete_file_node}; +use super::FromDavResource; pub(crate) trait FileCopyMoveRequestHandler: Sync + Send { fn handle_file_copy_move_request( @@ -54,7 +55,7 @@ impl FileCopyMoveRequestHandler for Server { .into_owned_uri()?; let from_account_id = from_resource_.account_id; let from_files = self - .fetch_dav_resources(from_account_id, Collection::FileNode) + .fetch_dav_resources(access_token, from_account_id, Collection::FileNode) .await .caused_by(trc::location!())?; let from_resource = from_files.map_resource::(&from_resource_)?; @@ -100,7 +101,7 @@ impl FileCopyMoveRequestHandler for Server { let to_files = if to_account_id == from_account_id { from_files.clone() } else { - self.fetch_dav_resources(to_account_id, Collection::FileNode) + self.fetch_dav_resources(access_token, to_account_id, Collection::FileNode) .await .caused_by(trc::location!())? }; @@ -234,7 +235,8 @@ impl FileCopyMoveRequestHandler for Server { ids.sort_unstable_by(|a, b| b.hierarchy_sequence.cmp(&a.hierarchy_sequence)); let mut sorted_ids = Vec::with_capacity(ids.len()); sorted_ids.extend(ids.into_iter().map(|a| a.document_id)); - delete_files(self, access_token, destination.account_id, sorted_ids) + DestroyArchive(sorted_ids) + .delete(self, access_token, destination.account_id) .await .caused_by(trc::location!())?; } @@ -340,22 +342,22 @@ async fn move_container( let node = node_ .to_unarchived::() .caused_by(trc::location!())?; - let mut new_node = node.deserialize().caused_by(trc::location!())?; + let mut new_node = node.deserialize::().caused_by(trc::location!())?; new_node.parent_id = parent_id; if let Some(new_name) = destination.new_name { new_node.name = new_name; } let mut batch = BatchBuilder::new(); - let etag = update_file_node( - access_token, - node, - new_node, - from_account_id, - from_document_id, - true, - &mut batch, - ) - .caused_by(trc::location!())?; + let etag = new_node + .update( + access_token, + node, + from_account_id, + from_document_id, + &mut batch, + ) + .caused_by(trc::location!())? + .etag(); server .commit_batch(batch) .await @@ -536,7 +538,9 @@ async fn overwrite_and_delete_item( let source_node_ = source_node__ .to_unarchived::() .caused_by(trc::location!())?; - let mut source_node = source_node_.deserialize().caused_by(trc::location!())?; + let mut source_node = source_node_ + .deserialize::() + .caused_by(trc::location!())?; source_node.name = if let Some(new_name) = destination.new_name { new_name } else { @@ -545,25 +549,19 @@ async fn overwrite_and_delete_item( source_node.parent_id = dest_node.inner.parent_id.into(); let mut batch = BatchBuilder::new(); - let etag = update_file_node( - access_token, - dest_node, - source_node, - to_account_id, - to_document_id, - true, - &mut batch, - ) - .caused_by(trc::location!())?; - - delete_file_node( - access_token, - source_node_, - from_account_id, - from_document_id, - &mut batch, - ) - .caused_by(trc::location!())?; + let etag = source_node + .update( + access_token, + dest_node, + to_account_id, + to_document_id, + &mut batch, + ) + .caused_by(trc::location!())? + .etag(); + DestroyArchive(source_node_) + .delete(access_token, from_account_id, from_document_id, &mut batch) + .caused_by(trc::location!())?; server .commit_batch(batch) .await @@ -610,16 +608,16 @@ async fn overwrite_item( }; source_node.parent_id = dest_node.inner.parent_id.into(); let mut batch = BatchBuilder::new(); - let etag = update_file_node( - access_token, - dest_node, - source_node, - to_account_id, - to_document_id, - true, - &mut batch, - ) - .caused_by(trc::location!())?; + let etag = source_node + .update( + access_token, + dest_node, + to_account_id, + to_document_id, + &mut batch, + ) + .caused_by(trc::location!())? + .etag(); server .commit_batch(batch) .await @@ -648,7 +646,7 @@ async fn move_item( let node = node_ .to_unarchived::() .caused_by(trc::location!())?; - let mut new_node = node.deserialize().caused_by(trc::location!())?; + let mut new_node = node.deserialize::().caused_by(trc::location!())?; new_node.parent_id = parent_id; if let Some(new_name) = destination.new_name { new_node.name = new_name; @@ -657,16 +655,16 @@ async fn move_item( let mut batch = BatchBuilder::new(); let etag = if from_account_id == to_account_id { // Destination is in the same account: just update the parent id - update_file_node( - access_token, - node, - new_node, - from_account_id, - from_document_id, - true, - &mut batch, - ) - .caused_by(trc::location!())? + new_node + .update( + access_token, + node, + from_account_id, + from_document_id, + &mut batch, + ) + .caused_by(trc::location!())? + .etag() } else { // Destination is in a different account: insert a new node, then delete the old one let to_document_id = server @@ -674,23 +672,13 @@ async fn move_item( .assign_document_ids(to_account_id, Collection::FileNode, 1) .await .caused_by(trc::location!())?; - let etag = insert_file_node( - access_token, - new_node, - to_account_id, - to_document_id, - true, - &mut batch, - ) - .caused_by(trc::location!())?; - delete_file_node( - access_token, - node, - from_account_id, - from_document_id, - &mut batch, - ) - .caused_by(trc::location!())?; + let etag = new_node + .insert(access_token, to_account_id, to_document_id, &mut batch) + .caused_by(trc::location!())? + .etag(); + DestroyArchive(node) + .delete(access_token, from_account_id, from_document_id, &mut batch) + .caused_by(trc::location!())?; etag }; server @@ -730,15 +718,10 @@ async fn copy_item( .assign_document_ids(to_account_id, Collection::FileNode, 1) .await .caused_by(trc::location!())?; - let etag = insert_file_node( - access_token, - node, - to_account_id, - to_document_id, - true, - &mut batch, - ) - .caused_by(trc::location!())?; + let etag = node + .insert(access_token, to_account_id, to_document_id, &mut batch) + .caused_by(trc::location!())? + .etag(); server .commit_batch(batch) .await @@ -765,21 +748,21 @@ async fn rename_item( let node = node_ .to_unarchived::() .caused_by(trc::location!())?; - let mut new_node = node.deserialize().caused_by(trc::location!())?; + let mut new_node = node.deserialize::().caused_by(trc::location!())?; if let Some(new_name) = destination.new_name { new_node.name = new_name; } let mut batch = BatchBuilder::new(); - let etag = update_file_node( - access_token, - node, - new_node, - from_account_id, - from_document_id, - true, - &mut batch, - ) - .caused_by(trc::location!())?; + let etag = new_node + .update( + access_token, + node, + from_account_id, + from_document_id, + &mut batch, + ) + .caused_by(trc::location!())? + .etag(); server .commit_batch(batch) .await diff --git a/crates/dav/src/file/delete.rs b/crates/dav/src/file/delete.rs index 849816f7..80cb4aaf 100644 --- a/crates/dav/src/file/delete.rs +++ b/crates/dav/src/file/delete.rs @@ -4,13 +4,12 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{Server, auth::AccessToken, storage::index::ObjectIndexBuilder}; +use common::{Server, auth::AccessToken}; use dav_proto::RequestHeaders; -use groupware::{file::FileNode, hierarchy::DavHierarchy}; +use groupware::{DestroyArchive, hierarchy::DavHierarchy}; use http_proto::HttpResponse; use hyper::StatusCode; use jmap_proto::types::{acl::Acl, collection::Collection}; -use store::write::BatchBuilder; use trc::AddContext; use crate::{ @@ -46,7 +45,7 @@ impl FileDeleteRequestHandler for Server { .filter(|r| !r.is_empty()) .ok_or(DavError::Code(StatusCode::FORBIDDEN))?; let files = self - .fetch_dav_resources(account_id, Collection::FileNode) + .fetch_dav_resources(access_token, account_id, Collection::FileNode) .await .caused_by(trc::location!())?; @@ -91,51 +90,10 @@ impl FileDeleteRequestHandler for Server { ) .await?; - delete_files(self, access_token, account_id, sorted_ids).await?; + DestroyArchive(sorted_ids) + .delete(self, access_token, account_id) + .await?; Ok(HttpResponse::new(StatusCode::NO_CONTENT)) } } - -pub(crate) async fn delete_files( - server: &Server, - access_token: &AccessToken, - account_id: u32, - ids: Vec, -) -> trc::Result<()> { - // Process deletions - let mut batch = BatchBuilder::new(); - batch - .with_account_id(account_id) - .with_collection(Collection::FileNode); - for document_id in ids { - if let Some(node) = server - .get_archive(account_id, Collection::FileNode, document_id) - .await? - { - // Delete record - batch - .delete_document(document_id) - .custom( - ObjectIndexBuilder::<_, ()>::new() - .with_tenant_id(access_token) - .with_current( - node.to_unarchived::() - .caused_by(trc::location!())?, - ), - ) - .caused_by(trc::location!())? - .commit_point(); - } - } - - // Write changes - if !batch.is_empty() { - server - .commit_batch(batch) - .await - .caused_by(trc::location!())?; - } - - Ok(()) -} diff --git a/crates/dav/src/file/get.rs b/crates/dav/src/file/get.rs index b4882dc1..401c33c5 100644 --- a/crates/dav/src/file/get.rs +++ b/crates/dav/src/file/get.rs @@ -45,7 +45,7 @@ impl FileGetRequestHandler for Server { .into_owned_uri()?; let account_id = resource_.account_id; let files = self - .fetch_dav_resources(account_id, Collection::FileNode) + .fetch_dav_resources(access_token, account_id, Collection::FileNode) .await .caused_by(trc::location!())?; let resource = files.map_resource(&resource_)?; diff --git a/crates/dav/src/file/mkcol.rs b/crates/dav/src/file/mkcol.rs index 059cf2df..2d1ff3fd 100644 --- a/crates/dav/src/file/mkcol.rs +++ b/crates/dav/src/file/mkcol.rs @@ -51,7 +51,7 @@ impl FileMkColRequestHandler for Server { .into_owned_uri()?; let account_id = resource_.account_id; let files = self - .fetch_dav_resources(account_id, Collection::FileNode) + .fetch_dav_resources(access_token, account_id, Collection::FileNode) .await .caused_by(trc::location!())?; let resource = files.map_parent_resource(&resource_)?; diff --git a/crates/dav/src/file/mod.rs b/crates/dav/src/file/mod.rs index cb6b335e..6ca7a725 100644 --- a/crates/dav/src/file/mod.rs +++ b/crates/dav/src/file/mod.rs @@ -4,19 +4,13 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{DavResource, DavResources, auth::AccessToken, storage::index::ObjectIndexBuilder}; +use common::{DavResource, DavResources}; use dav_proto::schema::property::{DavProperty, WebDavProperty}; -use groupware::file::{ArchivedFileNode, FileNode}; use hyper::StatusCode; -use jmap_proto::types::collection::Collection; -use store::write::{Archive, BatchBuilder, now}; use crate::{ DavError, - common::{ - ExtractETag, - uri::{OwnedUri, UriResource}, - }, + common::uri::{OwnedUri, UriResource}, }; pub mod copy_move; @@ -178,78 +172,3 @@ impl FromDavResource for FileItemId { } } } - -pub(crate) fn update_file_node( - access_token: &AccessToken, - node: Archive<&ArchivedFileNode>, - mut new_node: FileNode, - account_id: u32, - document_id: u32, - with_etag: bool, - batch: &mut BatchBuilder, -) -> trc::Result> { - // Build node - new_node.modified = now() as i64; - batch - .with_account_id(account_id) - .with_collection(Collection::FileNode) - .update_document(document_id) - .custom( - ObjectIndexBuilder::new() - .with_current(node) - .with_changes(new_node) - .with_tenant_id(access_token), - )? - .commit_point(); - - Ok(if with_etag { batch.etag() } else { None }) -} - -pub(crate) fn insert_file_node( - access_token: &AccessToken, - mut node: FileNode, - account_id: u32, - document_id: u32, - with_etag: bool, - batch: &mut BatchBuilder, -) -> trc::Result> { - // Build node - let now = now() as i64; - node.modified = now; - node.created = now; - - // Prepare write batch - batch - .with_account_id(account_id) - .with_collection(Collection::FileNode) - .create_document(document_id) - .custom( - ObjectIndexBuilder::<(), _>::new() - .with_changes(node) - .with_tenant_id(access_token), - )? - .commit_point(); - - Ok(if with_etag { batch.etag() } else { None }) -} - -pub(crate) fn delete_file_node( - access_token: &AccessToken, - node: Archive<&ArchivedFileNode>, - account_id: u32, - document_id: u32, - batch: &mut BatchBuilder, -) -> trc::Result<()> { - // Prepare write batch - batch - .with_account_id(account_id) - .with_collection(Collection::FileNode) - .delete_document(document_id) - .custom( - ObjectIndexBuilder::<_, ()>::new() - .with_current(node) - .with_tenant_id(access_token), - )? - .commit_point(); - Ok(()) -} diff --git a/crates/dav/src/file/proppatch.rs b/crates/dav/src/file/proppatch.rs index 0f4c5390..0a39dfa9 100644 --- a/crates/dav/src/file/proppatch.rs +++ b/crates/dav/src/file/proppatch.rs @@ -23,15 +23,13 @@ use trc::AddContext; use crate::{ DavError, DavMethod, common::{ - ETag, + ETag, ExtractETag, lock::{LockRequestHandler, ResourceState}, uri::DavUriResource, }, file::DavFileResource, }; -use super::update_file_node; - pub(crate) trait FilePropPatchRequestHandler: Sync + Send { fn handle_file_proppatch_request( &self, @@ -64,7 +62,7 @@ impl FilePropPatchRequestHandler for Server { let uri = headers.uri; let account_id = resource_.account_id; let files = self - .fetch_dav_resources(account_id, Collection::FileNode) + .fetch_dav_resources(access_token, account_id, Collection::FileNode) .await .caused_by(trc::location!())?; let resource = files.map_resource(&resource_)?; @@ -112,7 +110,7 @@ impl FilePropPatchRequestHandler for Server { .await?; // Deserialize - let mut new_node = node.deserialize().caused_by(trc::location!())?; + let mut new_node = node.deserialize::().caused_by(trc::location!())?; // Remove properties let mut items = Vec::with_capacity(request.remove.len() + request.set.len()); @@ -134,16 +132,16 @@ impl FilePropPatchRequestHandler for Server { let etag = if is_success { let mut batch = BatchBuilder::new(); - let etag = update_file_node( - access_token, - node, - new_node, - account_id, - resource.resource, - true, - &mut batch, - ) - .caused_by(trc::location!())?; + let etag = new_node + .update( + access_token, + node, + account_id, + resource.resource, + &mut batch, + ) + .caused_by(trc::location!())? + .etag(); self.commit_batch(batch).await.caused_by(trc::location!())?; etag } else { diff --git a/crates/dav/src/file/update.rs b/crates/dav/src/file/update.rs index fed2d91c..78943b79 100644 --- a/crates/dav/src/file/update.rs +++ b/crates/dav/src/file/update.rs @@ -55,7 +55,7 @@ impl FileUpdateRequestHandler for Server { .into_owned_uri()?; let account_id = resource.account_id; let files = self - .fetch_dav_resources(account_id, Collection::FileNode) + .fetch_dav_resources(access_token, account_id, Collection::FileNode) .await .caused_by(trc::location!())?; let resource_name = resource diff --git a/crates/dav/src/lib.rs b/crates/dav/src/lib.rs index cac5b490..431eb60a 100644 --- a/crates/dav/src/lib.rs +++ b/crates/dav/src/lib.rs @@ -12,20 +12,11 @@ pub mod principal; pub mod request; use dav_proto::schema::response::Condition; -use http_proto::HttpResponse; +use groupware::DavResourceName; use hyper::{Method, StatusCode}; -use jmap_proto::types::collection::Collection; pub(crate) type Result = std::result::Result; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DavResource { - Card, - Cal, - File, - Principal, -} - #[derive(Debug, Clone, Copy)] pub enum DavMethod { GET, @@ -82,86 +73,6 @@ impl DavErrorCondition { } } -impl From for Collection { - fn from(value: DavResource) -> Self { - match value { - DavResource::Card => Collection::AddressBook, - DavResource::Cal => Collection::Calendar, - DavResource::File => Collection::FileNode, - DavResource::Principal => Collection::Principal, - } - } -} - -impl From for DavResource { - fn from(value: Collection) -> Self { - match value { - Collection::AddressBook => DavResource::Card, - Collection::Calendar => DavResource::Cal, - Collection::FileNode => DavResource::File, - Collection::Principal => DavResource::Principal, - _ => unreachable!(), - } - } -} - -impl DavResource { - pub fn parse(service: &str) -> Option { - hashify::tiny_map!(service.as_bytes(), - "card" => DavResource::Card, - "cal" => DavResource::Cal, - "file" => DavResource::File, - "pal" => DavResource::Principal, - ) - } - - pub fn base_path(&self) -> &'static str { - match self { - DavResource::Card => "/dav/card", - DavResource::Cal => "/dav/cal", - DavResource::File => "/dav/file", - DavResource::Principal => "/dav/pal", - } - } - - pub fn into_options_response(self, depth: usize) -> HttpResponse { - /* - Depth: - 0 -> /dav/{resource_type} - 1 -> /dav/{resource_type}/{account_id} - 2 -> /dav/{resource_type}/{account_id}/{resource} - - */ - let dav = match self { - DavResource::Cal => "1, 2, 3, access-control, extended-mkcol, calendar-access", - DavResource::Card => "1, 2, 3, access-control, extended-mkcol, addressbook", - DavResource::File => "1, 2, 3, access-control, extended-mkcol", - DavResource::Principal => "1, 2, 3, access-control", - }; - let allow = match depth { - 0 => "OPTIONS, PROPFIND, REPORT", - 1 => { - if self != DavResource::Principal { - "OPTIONS, PROPFIND, MKCOL, REPORT" - } else { - "OPTIONS, PROPFIND, REPORT" - } - } - _ => { - if self != DavResource::Principal { - "OPTIONS, GET, HEAD, POST, PUT, DELETE, COPY, MOVE, MKCOL, PROPFIND, PROPPATCH, LOCK, UNLOCK, REPORT, ACL" - } else { - "OPTIONS, PROPFIND, REPORT" - } - } - }; - - HttpResponse::new(StatusCode::OK) - .with_header("DAV", dav) - .with_header("Allow", allow) - } -} - impl DavMethod { pub fn parse(method: &Method) -> Option { match *method { diff --git a/crates/dav/src/principal/matching.rs b/crates/dav/src/principal/matching.rs index 741ce1c7..169db2bd 100644 --- a/crates/dav/src/principal/matching.rs +++ b/crates/dav/src/principal/matching.rs @@ -52,7 +52,6 @@ impl PrincipalMatching for Server { access_token, DavQuery { resource: DavQueryResource::Uri(resource), - base_uri: headers.base_uri.unwrap_or_default(), propfind: PropFind::Prop(request.properties), depth: usize::MAX, ret: headers.ret, diff --git a/crates/dav/src/principal/mod.rs b/crates/dav/src/principal/mod.rs index fdd659cb..f7779911 100644 --- a/crates/dav/src/principal/mod.rs +++ b/crates/dav/src/principal/mod.rs @@ -8,7 +8,7 @@ use common::auth::AccessToken; use dav_proto::schema::response::Href; use percent_encoding::NON_ALPHANUMERIC; -use crate::DavResource; +use crate::DavResourceName; pub mod matching; pub mod propfind; @@ -21,8 +21,8 @@ pub trait CurrentUserPrincipal { impl CurrentUserPrincipal for AccessToken { fn current_user_principal(&self) -> Href { Href(format!( - "{}/{}", - DavResource::Principal.base_path(), + "{}/{}/", + DavResourceName::Principal.base_path(), percent_encoding::utf8_percent_encode(&self.name, NON_ALPHANUMERIC) )) } diff --git a/crates/dav/src/principal/propfind.rs b/crates/dav/src/principal/propfind.rs index 8ca22940..baece680 100644 --- a/crates/dav/src/principal/propfind.rs +++ b/crates/dav/src/principal/propfind.rs @@ -7,10 +7,11 @@ use std::borrow::Cow; use common::{Server, auth::AccessToken}; -use compact_str::format_compact; use dav_proto::schema::{ Namespace, - property::{DavProperty, PrincipalProperty, ReportSet, ResourceType, WebDavProperty}, + property::{ + DavProperty, PrincipalProperty, Privilege, ReportSet, ResourceType, WebDavProperty, + }, request::{DavPropertyValue, PropFind}, response::{Href, MultiStatus, PropStat, Response}, }; @@ -21,7 +22,7 @@ use percent_encoding::NON_ALPHANUMERIC; use trc::AddContext; use crate::{ - DavResource, + DavResourceName, common::{propfind::PropFindRequestHandler, uri::Urn}, }; @@ -83,7 +84,7 @@ impl PrincipalPropFind for Server { Collection::Principal => true, _ => false, }; - let base_path = DavResource::from(collection).base_path(); + let base_path = DavResourceName::from(collection).base_path(); let needs_quota = properties.iter().any(|property| { matches!( property, @@ -142,28 +143,25 @@ impl PrincipalPropFind for Server { } WebDavProperty::ResourceType => { let resource_type = if !is_principal { - ResourceType::Collection + vec![ResourceType::Collection] } else { - ResourceType::Principal + vec![ResourceType::Principal, ResourceType::Collection] }; - fields - .push(DavPropertyValue::new(property.clone(), vec![resource_type])); + fields.push(DavPropertyValue::new(property.clone(), resource_type)); } WebDavProperty::SupportedReportSet => { - let reports = if !is_principal { - vec![ - ReportSet::SyncCollection, - ReportSet::AclPrincipalPropSet, - ReportSet::PrincipalMatch, - ] - } else { - vec![ - ReportSet::PrincipalPropertySearch, - ReportSet::PrincipalSearchPropertySet, - ReportSet::PrincipalMatch, - ] + let reports = match collection { + Collection::Principal => ReportSet::principal(), + Collection::Calendar | Collection::CalendarEvent => { + ReportSet::calendar() + } + Collection::AddressBook | Collection::ContactCard => { + ReportSet::addressbook() + } + _ => ReportSet::file(), }; + fields.push(DavPropertyValue::new(property.clone(), reports)); } WebDavProperty::CurrentUserPrincipal => { @@ -194,8 +192,8 @@ impl PrincipalPropFind for Server { fields.push(DavPropertyValue::new( property.clone(), vec![Href(format!( - "{}/{}", - DavResource::Principal.base_path(), + "{}/{}/", + DavResourceName::Principal.base_path(), percent_encoding::utf8_percent_encode(&name, NON_ALPHANUMERIC), ))], )); @@ -203,58 +201,67 @@ impl PrincipalPropFind for Server { WebDavProperty::Group if !is_principal => { fields.push(DavPropertyValue::empty(property.clone())); } + WebDavProperty::CurrentUserPrivilegeSet if !is_principal => { + fields.push(DavPropertyValue::new( + property.clone(), + if access_token.is_member(account_id) { + Privilege::all(matches!( + collection, + Collection::Calendar | Collection::CalendarEvent + )) + } else { + vec![Privilege::Read] + }, + )); + } WebDavProperty::PrincipalCollectionSet => { fields.push(DavPropertyValue::new( property.clone(), - vec![Href(DavResource::Principal.base_path().to_string())], + vec![Href( + DavResourceName::Principal.collection_path().to_string(), + )], )); } _ => { fields_not_found.push(DavPropertyValue::empty(property.clone())); } }, - DavProperty::Principal(principal_property) if is_principal => { - match principal_property { - PrincipalProperty::AlternateURISet => { - fields.push(DavPropertyValue::empty(property.clone())); - } - PrincipalProperty::GroupMemberSet => { - fields.push(DavPropertyValue::empty(property.clone())); - } - PrincipalProperty::GroupMembership => { - fields.push(DavPropertyValue::empty(property.clone())); - } - PrincipalProperty::PrincipalURL => { - fields.push(DavPropertyValue::new( - property.clone(), - vec![Href(format!( - "{}/{}", - DavResource::Principal.base_path(), - percent_encoding::utf8_percent_encode( - &name, - NON_ALPHANUMERIC - ), - ))], - )); - } - PrincipalProperty::AddressbookHomeSet => { - fields.push(DavPropertyValue::new( - property.clone(), - vec![Href(format!( - "{}/{}", - DavResource::Card.base_path(), - percent_encoding::utf8_percent_encode( - &name, - NON_ALPHANUMERIC - ), - ))], - )); - } - PrincipalProperty::PrincipalAddress => { - fields_not_found.push(DavPropertyValue::empty(property.clone())); - } + DavProperty::Principal(principal_property) => match principal_property { + PrincipalProperty::AlternateURISet => { + fields.push(DavPropertyValue::empty(property.clone())); } - } + PrincipalProperty::GroupMemberSet => { + fields.push(DavPropertyValue::empty(property.clone())); + } + PrincipalProperty::GroupMembership => { + fields.push(DavPropertyValue::empty(property.clone())); + } + PrincipalProperty::PrincipalURL => { + fields.push(DavPropertyValue::new( + property.clone(), + vec![Href(format!( + "{}/{}/", + DavResourceName::Principal.base_path(), + percent_encoding::utf8_percent_encode(&name, NON_ALPHANUMERIC), + ))], + )); + } + PrincipalProperty::AddressbookHomeSet => { + fields.push(DavPropertyValue::new( + property.clone(), + vec![Href(format!( + "{}/{}/", + DavResourceName::Card.base_path(), + percent_encoding::utf8_percent_encode(&name, NON_ALPHANUMERIC), + ))], + )); + response.set_namespace(Namespace::CardDav); + } + PrincipalProperty::PrincipalAddress => { + fields_not_found.push(DavPropertyValue::empty(property.clone())); + response.set_namespace(Namespace::CardDav); + } + }, _ => { fields_not_found.push(DavPropertyValue::empty(property.clone())); } @@ -274,7 +281,7 @@ impl PrincipalPropFind for Server { response.add_response(Response::new_propstat( Href(format!( - "{}/{}", + "{}/{}/", base_path, percent_encoding::utf8_percent_encode(&name, NON_ALPHANUMERIC), )), @@ -296,8 +303,8 @@ impl PrincipalPropFind for Server { .caused_by(trc::location!())? .unwrap_or_else(|| format!("_{account_id}")); Ok(Href(format!( - "{}/{}", - DavResource::Principal.base_path(), + "{}/{}/", + DavResourceName::Principal.base_path(), percent_encoding::utf8_percent_encode(&name, NON_ALPHANUMERIC), ))) } @@ -317,7 +324,7 @@ fn all_props(collection: Collection, all_props: Option<&[DavProperty]>) -> Vec) -> Vec, session: &HttpSessionData, - resource: DavResource, + resource: DavResourceName, method: DavMethod, ) -> impl Future + Send; } @@ -63,7 +64,7 @@ pub(crate) trait DavRequestDispatcher: Sync + Send { &self, request: &HttpRequest, access_token: Arc, - resource: DavResource, + resource: DavResourceName, method: DavMethod, body: Vec, ) -> impl Future> + Send; @@ -74,7 +75,7 @@ impl DavRequestDispatcher for Server { &self, request: &HttpRequest, access_token: Arc, - resource: DavResource, + resource: DavResourceName, method: DavMethod, body: Vec, ) -> crate::Result { @@ -97,16 +98,18 @@ impl DavRequestDispatcher for Server { DavMethod::PROPPATCH => { let request = PropertyUpdate::parse(&mut Tokenizer::new(&body))?; match resource { - DavResource::Card => { + DavResourceName::Card => { self.handle_card_proppatch_request(&access_token, headers, request) .await } - DavResource::Cal => todo!(), - DavResource::File => { + DavResourceName::Cal => todo!(), + DavResourceName::File => { self.handle_file_proppatch_request(&access_token, headers, request) .await } - DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), + DavResourceName::Principal => { + Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)) + } } } DavMethod::MKCOL => { @@ -117,37 +120,39 @@ impl DavRequestDispatcher for Server { }; match resource { - DavResource::Card => { + DavResourceName::Card => { self.handle_card_mkcol_request(&access_token, headers, request) .await } - DavResource::Cal => todo!(), - DavResource::File => { + DavResourceName::Cal => todo!(), + DavResourceName::File => { self.handle_file_mkcol_request(&access_token, headers, request) .await } - DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), + DavResourceName::Principal => { + Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)) + } } } DavMethod::GET => match resource { - DavResource::Card => { + DavResourceName::Card => { self.handle_card_get_request(&access_token, headers, false) .await } - DavResource::Cal => todo!(), - DavResource::File => { + DavResourceName::Cal => todo!(), + DavResourceName::File => { self.handle_file_get_request(&access_token, headers, false) .await } - DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), + DavResourceName::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), }, DavMethod::HEAD => match resource { - DavResource::Card => { + DavResourceName::Card => { self.handle_card_get_request(&access_token, headers, true) .await } - DavResource::Cal => todo!(), - DavResource::File => { + DavResourceName::Cal => todo!(), + DavResourceName::File => { #[cfg(debug_assertions)] { // Deal with Litmus bug @@ -165,7 +170,7 @@ impl DavRequestDispatcher for Server { .await } } - DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), + DavResourceName::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), }, DavMethod::DELETE => { // Include any fragments in the URI @@ -175,68 +180,70 @@ impl DavRequestDispatcher for Server { } match resource { - DavResource::Card => { + DavResourceName::Card => { self.handle_card_delete_request(&access_token, headers) .await } - DavResource::Cal => todo!(), - DavResource::File => { + DavResourceName::Cal => todo!(), + DavResourceName::File => { self.handle_file_delete_request(&access_token, headers) .await } - DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), + DavResourceName::Principal => { + Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)) + } } } DavMethod::PUT | DavMethod::POST => match resource { - DavResource::Card => { + DavResourceName::Card => { self.handle_card_update_request(&access_token, headers, body, false) .await } - DavResource::Cal => todo!(), - DavResource::File => { + DavResourceName::Cal => todo!(), + DavResourceName::File => { self.handle_file_update_request(&access_token, headers, body, false) .await } - DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), + DavResourceName::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), }, DavMethod::PATCH => match resource { - DavResource::Card => { + DavResourceName::Card => { self.handle_card_update_request(&access_token, headers, body, true) .await } - DavResource::Cal => todo!(), - DavResource::File => { + DavResourceName::Cal => todo!(), + DavResourceName::File => { self.handle_file_update_request(&access_token, headers, body, true) .await } - DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), + DavResourceName::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), }, DavMethod::COPY => match resource { - DavResource::Card => { + DavResourceName::Card => { self.handle_card_copy_move_request(&access_token, headers, false) .await } - DavResource::Cal => todo!(), - DavResource::File => { + DavResourceName::Cal => todo!(), + DavResourceName::File => { self.handle_file_copy_move_request(&access_token, headers, false) .await } - DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), + DavResourceName::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), }, DavMethod::MOVE => match resource { - DavResource::Card => { + DavResourceName::Card => { self.handle_card_copy_move_request(&access_token, headers, false) .await } - DavResource::Cal => todo!(), - DavResource::File => { + DavResourceName::Cal => todo!(), + DavResourceName::File => { self.handle_file_copy_move_request(&access_token, headers, true) .await } - DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), + DavResourceName::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), }, DavMethod::LOCK => match resource { - DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), + DavResourceName::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), _ => { self.handle_lock_request( &access_token, @@ -257,11 +264,13 @@ impl DavRequestDispatcher for Server { DavMethod::ACL => { let request = Acl::parse(&mut Tokenizer::new(&body))?; match resource { - DavResource::Card | DavResource::Cal | DavResource::File => { + DavResourceName::Card | DavResourceName::Cal | DavResourceName::File => { self.handle_acl_request(&access_token, headers, request) .await } - DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), + DavResourceName::Principal => { + Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)) + } } } DavMethod::REPORT => match Report::parse(&mut Tokenizer::new(&body))? { @@ -271,14 +280,14 @@ impl DavRequestDispatcher for Server { .await .and_then(|d| d.into_owned_uri())?; match resource { - DavResource::Card | DavResource::Cal | DavResource::File => { + DavResourceName::Card | DavResourceName::Cal | DavResourceName::File => { self.handle_dav_query( &access_token, DavQuery::changes(uri, sync_collection, headers), ) .await } - DavResource::Principal => { + DavResourceName::Principal => { Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)) } } @@ -292,7 +301,7 @@ impl DavRequestDispatcher for Server { .await } Report::PrincipalPropertySearch(report) => { - if resource == DavResource::Principal { + if resource == DavResourceName::Principal { self.handle_principal_property_search(&access_token, report) .await } else { @@ -300,7 +309,7 @@ impl DavRequestDispatcher for Server { } } Report::PrincipalSearchPropertySet => { - if resource == DavResource::Principal { + if resource == DavResourceName::Principal { Ok(HttpResponse::new(StatusCode::OK).with_xml_body( PrincipalSearchPropertySet::new(vec![PrincipalSearchProperty::new( WebDavProperty::DisplayName, @@ -339,7 +348,7 @@ impl DavRequestHandler for Server { mut request: HttpRequest, access_token: Arc, session: &HttpSessionData, - resource: DavResource, + resource: DavResourceName, method: DavMethod, ) -> HttpResponse { let body = if method.has_body() @@ -375,6 +384,8 @@ impl DavRequestHandler for Server { Vec::new() }; + let c = println!("------------------------------------------"); + let std_body = std::str::from_utf8(&body).unwrap_or("[binary]").to_string(); let result = match self @@ -393,7 +404,13 @@ impl DavRequestHandler for Server { ) => HttpResponse::new(StatusCode::PRECONDITION_FAILED) .with_xml_body( ErrorResponse::new(BaseCondition::QuotaNotExceeded) - .with_namespace(resource) + .with_namespace(match resource { + DavResourceName::Card => Namespace::CardDav, + DavResourceName::Cal => Namespace::CalDav, + DavResourceName::File | DavResourceName::Principal => { + Namespace::Dav + } + }) .to_string(), ) .with_no_cache(), @@ -417,7 +434,11 @@ impl DavRequestHandler for Server { Err(DavError::Condition(condition)) => HttpResponse::new(condition.code) .with_xml_body( ErrorResponse::new(condition.condition) - .with_namespace(resource) + .with_namespace(match resource { + DavResourceName::Card => Namespace::CardDav, + DavResourceName::Cal => Namespace::CalDav, + DavResourceName::File | DavResourceName::Principal => Namespace::Dav, + }) .to_string(), ) .with_no_cache(), @@ -425,7 +446,7 @@ impl DavRequestHandler for Server { }; let c = println!( - "------------------------------------------\n{:?} {} -> {:?}\nHeaders: {:?}\nBody: {}\nResponse headers: {:?}\nResponse: {}", + "{:?} {} -> {:?}\nHeaders: {:?}\nBody: {}\nResponse headers: {:?}\nResponse: {}", method, request.uri().path(), result.status(), @@ -433,9 +454,9 @@ impl DavRequestHandler for Server { std_body, result.headers().unwrap(), match &result.body() { - http_proto::HttpResponseBody::Text(t) => t, - http_proto::HttpResponseBody::Empty => "[empty]", - _ => "[binary]", + http_proto::HttpResponseBody::Text(t) => xml_pretty_print(t), + http_proto::HttpResponseBody::Empty => "[empty]".to_string(), + _ => "[binary]".to_string(), } ); @@ -454,13 +475,3 @@ impl From for DavError { DavError::Internal(err) } } - -impl From for Namespace { - fn from(value: DavResource) -> Self { - match value { - DavResource::Card => Namespace::CardDav, - DavResource::Cal => Namespace::CalDav, - DavResource::File | DavResource::Principal => Namespace::Dav, - } - } -} diff --git a/crates/directory/src/core/principal.rs b/crates/directory/src/core/principal.rs index 87db3fdb..cc83e55e 100644 --- a/crates/directory/src/core/principal.rs +++ b/crates/directory/src/core/principal.rs @@ -310,7 +310,7 @@ impl Principal { typ: Type::Individual, name: "Fallback Administrator".into(), secrets: vec![fallback_pass.into()], - data: vec![PrincipalData::MemberOf(vec![ROLE_ADMIN])], + data: vec![PrincipalData::Roles(vec![ROLE_ADMIN])], description: Default::default(), emails: Default::default(), quota: Default::default(), diff --git a/crates/groupware/src/contact/index.rs b/crates/groupware/src/contact/index.rs index 1bb6e0df..d97e724f 100644 --- a/crates/groupware/src/contact/index.rs +++ b/crates/groupware/src/contact/index.rs @@ -7,7 +7,7 @@ use common::storage::index::{ IndexItem, IndexValue, IndexableAndSerializableObject, IndexableObject, }; -use jmap_proto::types::value::AclGrant; +use jmap_proto::types::{collection::Collection, value::AclGrant}; use store::SerializeInfallible; use crate::{IDX_CARD_UID, IDX_NAME}; @@ -89,6 +89,10 @@ impl IndexableObject for ContactCard { + self.size, }, IndexValue::LogChild { prefix: None }, + IndexValue::LogParent { + collection: Collection::AddressBook.into(), + ids: self.names.iter().map(|n| n.parent_id).collect(), + }, ] .into_iter() } @@ -116,6 +120,10 @@ impl IndexableObject for &ArchivedContactCard { + self.size, }, IndexValue::LogChild { prefix: None }, + IndexValue::LogParent { + collection: Collection::AddressBook.into(), + ids: self.names.iter().map(|n| n.parent_id.to_native()).collect(), + }, ] .into_iter() } diff --git a/crates/groupware/src/contact/mod.rs b/crates/groupware/src/contact/mod.rs index 5404f6d0..166f8ef1 100644 --- a/crates/groupware/src/contact/mod.rs +++ b/crates/groupware/src/contact/mod.rs @@ -5,6 +5,7 @@ */ pub mod index; +pub mod storage; use calcard::vcard::VCard; diff --git a/crates/groupware/src/contact/storage.rs b/crates/groupware/src/contact/storage.rs new file mode 100644 index 00000000..da1032e2 --- /dev/null +++ b/crates/groupware/src/contact/storage.rs @@ -0,0 +1,231 @@ +use common::{Server, auth::AccessToken, storage::index::ObjectIndexBuilder}; +use jmap_proto::types::collection::Collection; +use store::write::{Archive, BatchBuilder, now}; +use trc::AddContext; + +use crate::DestroyArchive; + +use super::{AddressBook, ArchivedAddressBook, ArchivedContactCard, ContactCard}; + +impl ContactCard { + pub fn update<'x>( + self, + access_token: &AccessToken, + card: Archive<&ArchivedContactCard>, + account_id: u32, + document_id: u32, + batch: &'x mut BatchBuilder, + ) -> trc::Result<&'x mut BatchBuilder> { + let mut new_card = self; + + // Build card + new_card.modified = now() as i64; + + // Prepare write batch + batch + .with_account_id(account_id) + .with_collection(Collection::ContactCard) + .update_document(document_id) + .custom( + ObjectIndexBuilder::new() + .with_current(card) + .with_changes(new_card) + .with_tenant_id(access_token), + ) + .map(|b| b.commit_point()) + } + + pub fn insert<'x>( + self, + access_token: &AccessToken, + account_id: u32, + document_id: u32, + batch: &'x mut BatchBuilder, + ) -> trc::Result<&'x mut BatchBuilder> { + // Build card + let mut card = self; + let now = now() as i64; + card.modified = now; + card.created = now; + + // Prepare write batch + batch + .with_account_id(account_id) + .with_collection(Collection::ContactCard) + .create_document(document_id) + .custom( + ObjectIndexBuilder::<(), _>::new() + .with_changes(card) + .with_tenant_id(access_token), + ) + .map(|b| b.commit_point()) + } +} + +impl AddressBook { + pub fn insert<'x>( + self, + access_token: &AccessToken, + account_id: u32, + document_id: u32, + batch: &'x mut BatchBuilder, + ) -> trc::Result<&'x mut BatchBuilder> { + // Build address book + let mut book = self; + let now = now() as i64; + book.modified = now; + book.created = now; + + // Prepare write batch + batch + .with_account_id(account_id) + .with_collection(Collection::AddressBook) + .create_document(document_id) + .custom( + ObjectIndexBuilder::<(), _>::new() + .with_changes(book) + .with_tenant_id(access_token), + ) + .map(|b| b.commit_point()) + } + + pub fn update<'x>( + self, + access_token: &AccessToken, + book: Archive<&ArchivedAddressBook>, + account_id: u32, + document_id: u32, + batch: &'x mut BatchBuilder, + ) -> trc::Result<&'x mut BatchBuilder> { + // Build address book + let mut new_book = self; + new_book.modified = now() as i64; + + // Prepare write batch + batch + .with_account_id(account_id) + .with_collection(Collection::AddressBook) + .update_document(document_id) + .custom( + ObjectIndexBuilder::new() + .with_current(book) + .with_changes(new_book) + .with_tenant_id(access_token), + ) + .map(|b| b.commit_point()) + } +} + +impl DestroyArchive> { + pub async fn delete_with_cards( + self, + server: &Server, + access_token: &AccessToken, + account_id: u32, + document_id: u32, + children_ids: Vec, + batch: &mut BatchBuilder, + ) -> trc::Result<()> { + // Process deletions + let addressbook_id = document_id; + for document_id in children_ids { + if let Some(card_) = server + .get_archive(account_id, Collection::ContactCard, document_id) + .await? + { + DestroyArchive( + card_ + .to_unarchived::() + .caused_by(trc::location!())?, + ) + .delete( + access_token, + account_id, + document_id, + addressbook_id, + batch, + )?; + } + } + + self.delete(access_token, account_id, document_id, batch) + } + + pub fn delete( + self, + access_token: &AccessToken, + account_id: u32, + document_id: u32, + batch: &mut BatchBuilder, + ) -> trc::Result<()> { + let book = self.0; + // Delete addressbook + batch + .with_account_id(account_id) + .with_collection(Collection::AddressBook) + .delete_document(document_id) + .custom( + ObjectIndexBuilder::<_, ()>::new() + .with_tenant_id(access_token) + .with_current(book), + ) + .caused_by(trc::location!())? + .commit_point(); + + Ok(()) + } +} + +impl DestroyArchive> { + pub fn delete( + self, + access_token: &AccessToken, + account_id: u32, + document_id: u32, + addressbook_id: u32, + batch: &mut BatchBuilder, + ) -> trc::Result<()> { + let card = self.0; + if let Some(delete_idx) = card + .inner + .names + .iter() + .position(|name| name.parent_id == addressbook_id) + { + batch + .with_account_id(account_id) + .with_collection(Collection::ContactCard); + + if card.inner.names.len() > 1 { + // Unlink addressbook id from card + let mut new_card = card + .deserialize::() + .caused_by(trc::location!())?; + new_card.names.swap_remove(delete_idx); + batch + .update_document(document_id) + .custom( + ObjectIndexBuilder::new() + .with_tenant_id(access_token) + .with_current(card) + .with_changes(new_card), + ) + .caused_by(trc::location!())?; + } else { + // Delete card + batch + .delete_document(document_id) + .custom( + ObjectIndexBuilder::<_, ()>::new() + .with_tenant_id(access_token) + .with_current(card), + ) + .caused_by(trc::location!())?; + } + + batch.commit_point(); + } + + Ok(()) + } +} diff --git a/crates/groupware/src/file/mod.rs b/crates/groupware/src/file/mod.rs index d7e3b843..200e849d 100644 --- a/crates/groupware/src/file/mod.rs +++ b/crates/groupware/src/file/mod.rs @@ -5,7 +5,7 @@ */ pub mod index; - +pub mod storage; use dav_proto::schema::request::DeadProperty; use jmap_proto::types::value::AclGrant; diff --git a/crates/groupware/src/file/storage.rs b/crates/groupware/src/file/storage.rs new file mode 100644 index 00000000..1f4e5fbb --- /dev/null +++ b/crates/groupware/src/file/storage.rs @@ -0,0 +1,133 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use common::{Server, auth::AccessToken, storage::index::ObjectIndexBuilder}; +use jmap_proto::types::collection::Collection; +use store::write::{Archive, BatchBuilder, now}; +use trc::AddContext; + +use crate::DestroyArchive; + +use super::{ArchivedFileNode, FileNode}; + +impl FileNode { + pub fn insert<'x>( + self, + access_token: &AccessToken, + account_id: u32, + document_id: u32, + batch: &'x mut BatchBuilder, + ) -> trc::Result<&'x mut BatchBuilder> { + // Build node + let mut node = self; + let now = now() as i64; + node.modified = now; + node.created = now; + + // Prepare write batch + batch + .with_account_id(account_id) + .with_collection(Collection::FileNode) + .create_document(document_id) + .custom( + ObjectIndexBuilder::<(), _>::new() + .with_changes(node) + .with_tenant_id(access_token), + ) + .map(|b| b.commit_point()) + } + pub fn update<'x>( + self, + access_token: &AccessToken, + node: Archive<&ArchivedFileNode>, + account_id: u32, + document_id: u32, + batch: &'x mut BatchBuilder, + ) -> trc::Result<&'x mut BatchBuilder> { + // Build node + let mut new_node = self; + new_node.modified = now() as i64; + batch + .with_account_id(account_id) + .with_collection(Collection::FileNode) + .update_document(document_id) + .custom( + ObjectIndexBuilder::new() + .with_current(node) + .with_changes(new_node) + .with_tenant_id(access_token), + ) + .map(|b| b.commit_point()) + } +} + +impl DestroyArchive> { + pub fn delete( + self, + access_token: &AccessToken, + account_id: u32, + document_id: u32, + batch: &mut BatchBuilder, + ) -> trc::Result<()> { + // Prepare write batch + batch + .with_account_id(account_id) + .with_collection(Collection::FileNode) + .delete_document(document_id) + .custom( + ObjectIndexBuilder::<_, ()>::new() + .with_current(self.0) + .with_tenant_id(access_token), + )? + .commit_point(); + Ok(()) + } +} + +impl DestroyArchive> { + pub async fn delete( + self, + server: &Server, + access_token: &AccessToken, + account_id: u32, + ) -> trc::Result<()> { + // Process deletions + let mut batch = BatchBuilder::new(); + batch + .with_account_id(account_id) + .with_collection(Collection::FileNode); + for document_id in self.0 { + if let Some(node) = server + .get_archive(account_id, Collection::FileNode, document_id) + .await? + { + // Delete record + batch + .delete_document(document_id) + .custom( + ObjectIndexBuilder::<_, ()>::new() + .with_tenant_id(access_token) + .with_current( + node.to_unarchived::() + .caused_by(trc::location!())?, + ), + ) + .caused_by(trc::location!())? + .commit_point(); + } + } + + // Write changes + if !batch.is_empty() { + server + .commit_batch(batch) + .await + .caused_by(trc::location!())?; + } + + Ok(()) + } +} diff --git a/crates/groupware/src/hierarchy.rs b/crates/groupware/src/hierarchy.rs index 782cc3f2..5ef21aef 100644 --- a/crates/groupware/src/hierarchy.rs +++ b/crates/groupware/src/hierarchy.rs @@ -6,28 +6,45 @@ use std::sync::Arc; -use common::{DavResource, DavResourceId, DavResources, Server}; +use common::{DavResource, DavResourceId, DavResources, Server, auth::AccessToken}; +use directory::backend::internal::manage::ManageDirectory; use jmap_proto::types::collection::Collection; +use percent_encoding::NON_ALPHANUMERIC; use store::{ - Deserialize, IndexKey, IterateParams, SerializeInfallible, U32_LEN, ahash::AHashMap, - write::key::DeserializeBigEndian, + Deserialize, IndexKey, IndexKeyPrefix, IterateParams, SerializeInfallible, U32_LEN, + ahash::AHashMap, + write::{BatchBuilder, key::DeserializeBigEndian}, }; use trc::AddContext; use utils::bimap::IdBimap; -use crate::{DavName, IDX_NAME, file::FileNode}; +use crate::{DavName, DavResourceName, IDX_NAME, contact::AddressBook, file::FileNode}; pub trait DavHierarchy: Sync + Send { fn fetch_dav_resources( &self, + access_token: &AccessToken, account_id: u32, collection: Collection, ) -> impl Future>> + Send; + + fn create_default_addressbook( + &self, + access_token: &AccessToken, + account_id: u32, + ) -> impl Future> + Send; + + fn create_default_calendar( + &self, + access_token: &AccessToken, + account_id: u32, + ) -> impl Future> + Send; } impl DavHierarchy for Server { async fn fetch_dav_resources( &self, + access_token: &AccessToken, account_id: u32, collection: Collection, ) -> trc::Result> { @@ -51,7 +68,23 @@ impl DavHierarchy for Server { } else { let mut files = match collection { Collection::Calendar | Collection::AddressBook => { - build_hierarchy(self, account_id, collection).await? + let files = build_hierarchy(self, account_id, collection).await?; + if files.paths.is_empty() { + match collection { + Collection::Calendar => { + self.create_default_calendar(access_token, account_id) + .await? + } + Collection::AddressBook => { + self.create_default_addressbook(access_token, account_id) + .await? + } + _ => unreachable!(), + } + build_hierarchy(self, account_id, collection).await? + } else { + files + } } Collection::FileNode => build_file_hierarchy(self, account_id).await?, _ => unreachable!(), @@ -64,6 +97,38 @@ impl DavHierarchy for Server { Ok(files) } } + + async fn create_default_addressbook( + &self, + access_token: &AccessToken, + account_id: u32, + ) -> trc::Result<()> { + if let Some(name) = &self.core.dav.default_addressbook_name { + let mut batch = BatchBuilder::new(); + let document_id = self + .store() + .assign_document_ids(account_id, Collection::AddressBook, 1) + .await?; + AddressBook { + name: name.clone(), + display_name: self.core.dav.default_addressbook_display_name.clone(), + is_default: true, + ..Default::default() + } + .insert(access_token, account_id, document_id, &mut batch)?; + self.commit_batch(batch).await?; + } + + Ok(()) + } + + async fn create_default_calendar( + &self, + access_token: &AccessToken, + account_id: u32, + ) -> trc::Result<()> { + todo!() + } } async fn build_hierarchy( @@ -71,6 +136,7 @@ async fn build_hierarchy( account_id: u32, collection: Collection, ) -> trc::Result { + let base_path = DavResourceName::from(collection).base_path(); let collection = u8::from(collection); let mut containers: AHashMap = AHashMap::with_capacity(16); let mut resources: AHashMap> = AHashMap::with_capacity(16); @@ -99,7 +165,7 @@ async fn build_hierarchy( |key, _| { let document_id = key.deserialize_be_u32(key.len() - U32_LEN)?; let value = key - .get(key.len() - (U32_LEN * 2)..key.len() - U32_LEN) + .get(IndexKeyPrefix::len()..key.len() - U32_LEN) .ok_or_else(|| trc::Error::corrupted_key(key, None, trc::location!()))?; let key_collection = key .get(U32_LEN) @@ -126,10 +192,22 @@ async fn build_hierarchy( .await .caused_by(trc::location!())?; + let name = server + .store() + .get_principal_name(account_id) + .await + .caused_by(trc::location!())? + .unwrap_or_else(|| format!("_{account_id}")); + let mut files = DavResources { paths: IdBimap::with_capacity(containers.len() + resources.len()), size: std::mem::size_of::() as u64, modseq: None, + base_path: format!( + "{}/{}/", + base_path, + percent_encoding::utf8_percent_encode(&name, NON_ALPHANUMERIC), + ), }; for (document_id, dav_names) in resources { @@ -172,7 +250,18 @@ async fn build_file_hierarchy(server: &Server, account_id: u32) -> trc::Result(account_id, Collection::FileNode) .await .caused_by(trc::location!())?; + let name = server + .store() + .get_principal_name(account_id) + .await + .caused_by(trc::location!())? + .unwrap_or_else(|| format!("_{account_id}")); let mut files = DavResources { + base_path: format!( + "{}/{}/", + DavResourceName::Card.base_path(), + percent_encoding::utf8_percent_encode(&name, NON_ALPHANUMERIC), + ), paths: IdBimap::with_capacity(list.len()), size: std::mem::size_of::() as u64, modseq: None, diff --git a/crates/groupware/src/lib.rs b/crates/groupware/src/lib.rs index ec41564f..7256591f 100644 --- a/crates/groupware/src/lib.rs +++ b/crates/groupware/src/lib.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use jmap_proto::types::collection::Collection; use store::{Deserialize, SerializeInfallible, write::key::KeySerializer}; use utils::codec::leb128::Leb128Reader; @@ -15,6 +16,16 @@ pub mod hierarchy; pub const IDX_NAME: u8 = 0; pub const IDX_CARD_UID: u8 = 1; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DavResourceName { + Card, + Cal, + File, + Principal, +} + +pub struct DestroyArchive(pub T); + #[derive( rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, )] @@ -69,3 +80,55 @@ impl Deserialize for DavName { Ok(DavName { name, parent_id }) } } + +impl DavResourceName { + pub fn parse(service: &str) -> Option { + hashify::tiny_map!(service.as_bytes(), + "card" => DavResourceName::Card, + "cal" => DavResourceName::Cal, + "file" => DavResourceName::File, + "pal" => DavResourceName::Principal, + ) + } + + pub fn base_path(&self) -> &'static str { + match self { + DavResourceName::Card => "/dav/card", + DavResourceName::Cal => "/dav/cal", + DavResourceName::File => "/dav/file", + DavResourceName::Principal => "/dav/pal", + } + } + + pub fn collection_path(&self) -> &'static str { + match self { + DavResourceName::Card => "/dav/card/", + DavResourceName::Cal => "/dav/cal/", + DavResourceName::File => "/dav/file/", + DavResourceName::Principal => "/dav/pal/", + } + } +} + +impl From for Collection { + fn from(value: DavResourceName) -> Self { + match value { + DavResourceName::Card => Collection::AddressBook, + DavResourceName::Cal => Collection::Calendar, + DavResourceName::File => Collection::FileNode, + DavResourceName::Principal => Collection::Principal, + } + } +} + +impl From for DavResourceName { + fn from(value: Collection) -> Self { + match value { + Collection::AddressBook => DavResourceName::Card, + Collection::Calendar => DavResourceName::Cal, + Collection::FileNode => DavResourceName::File, + Collection::Principal => DavResourceName::Principal, + _ => unreachable!(), + } + } +} diff --git a/crates/http/Cargo.toml b/crates/http/Cargo.toml index c3bd7137..66fe83b0 100644 --- a/crates/http/Cargo.toml +++ b/crates/http/Cargo.toml @@ -13,6 +13,7 @@ email = { path = "../email" } smtp = { path = "../smtp" } jmap = { path = "../jmap" } dav = { path = "../dav" } +groupware = { path = "../groupware" } spam-filter = { path = "../spam-filter" } http_proto = { path = "../http-proto" } jmap_proto = { path = "../jmap-proto" } diff --git a/crates/http/src/request.rs b/crates/http/src/request.rs index 2d2291c4..a3d10cda 100644 --- a/crates/http/src/request.rs +++ b/crates/http/src/request.rs @@ -14,8 +14,9 @@ use common::{ listener::{SessionData, SessionManager, SessionStream}, manager::webadmin::Resource, }; -use dav::{DavMethod, DavResource, request::DavRequestHandler}; +use dav::{DavMethod, request::DavRequestHandler}; use directory::Permission; +use groupware::DavResourceName; use http_proto::{ DownloadResponse, HttpContext, HttpRequest, HttpResponse, HttpResponseBody, HttpSessionData, JsonProblemResponse, ToHttpResponse, form_urlencoded, request::fetch_body, @@ -206,12 +207,21 @@ impl ParseHttp for Server { } "dav" => { let response = match ( - path.next().and_then(DavResource::parse), + path.next().and_then(DavResourceName::parse), DavMethod::parse(req.method()), ) { - (Some(resource), Some(DavMethod::OPTIONS)) => { - resource.into_options_response(path.count()) - } + (Some(_), Some(DavMethod::OPTIONS)) => HttpResponse::new(StatusCode::OK) + .with_header( + "DAV", + "1, 2, 3, access-control, extended-mkcol, calendar-access, addressbook", + ) + .with_header( + "Allow", + concat!( + "OPTIONS, GET, HEAD, POST, PUT, DELETE, COPY, MOVE, MKCALENDAR, ", + "MKCOL, PROPFIND, PROPPATCH, LOCK, UNLOCK, REPORT, ACL" + ), + ), (Some(resource), Some(method)) => { // Authenticate request let (_in_flight, access_token) = @@ -237,17 +247,15 @@ impl ParseHttp for Server { .await .map(|s| s.into_http_response()); } - ("caldav", &Method::GET) => { - let base_url = ctx.resolve_response_url(self).await; + ("caldav", _) => { return Ok(HttpResponse::new(StatusCode::TEMPORARY_REDIRECT) .with_no_cache() - .with_location(format!("{base_url}/dav/cal"))); + .with_location(DavResourceName::Cal.base_path())); } - ("carddav", &Method::GET) => { - let base_url = ctx.resolve_response_url(self).await; + ("carddav", _) => { return Ok(HttpResponse::new(StatusCode::TEMPORARY_REDIRECT) .with_no_cache() - .with_location(format!("{base_url}/dav/card"))); + .with_location(DavResourceName::Card.base_path())); } ("oauth-authorization-server", &Method::GET) => { // Limit anonymous requests diff --git a/crates/utils/src/bimap.rs b/crates/utils/src/bimap.rs index f58a017d..daa7c2f0 100644 --- a/crates/utils/src/bimap.rs +++ b/crates/utils/src/bimap.rs @@ -47,6 +47,10 @@ impl IdBimap { pub fn iter(&self) -> impl Iterator { self.name_to_id.values().map(|v| v.as_ref()) } + + pub fn is_empty(&self) -> bool { + self.name_to_id.is_empty() + } } // SAFETY: Safe because Rc<> are never returned from the struct