diff --git a/crates/common/src/core.rs b/crates/common/src/core.rs index b37fd3f2..e001d369 100644 --- a/crates/common/src/core.rs +++ b/crates/common/src/core.rs @@ -529,17 +529,15 @@ impl Server { pub async fn commit_batch(&self, mut builder: BatchBuilder) -> trc::Result { let mut assigned_ids = AssignedIds::default(); - let change_id = builder.last_change_id(); for batch in builder.build() { assigned_ids = self.store().write(batch).await?; } - if builder.has_logs() { - let change_id = change_id.unwrap(); - for (account_id, changed_collections) in builder.changed_collections() { - let mut state_change = StateChange::new(*account_id); - for changed_collection in *changed_collections { + if let Some(changes) = builder.changes() { + for (account_id, (change_id, changed_collections)) in changes { + let mut state_change = StateChange::new(account_id); + for changed_collection in changed_collections { if let Ok(data_type) = DataType::try_from(changed_collection) { state_change.set_change(data_type, change_id); } @@ -547,9 +545,8 @@ impl Server { if state_change.has_changes() { self.broadcast_state_change(state_change).await; } + assigned_ids.change_id = change_id.into(); } - - assigned_ids.change_id = change_id.into(); } Ok(assigned_ids) diff --git a/crates/dav-proto/src/parser/property.rs b/crates/dav-proto/src/parser/property.rs index 0f873f31..684af151 100644 --- a/crates/dav-proto/src/parser/property.rs +++ b/crates/dav-proto/src/parser/property.rs @@ -15,7 +15,7 @@ use mail_parser::DateTime; use crate::schema::{ property::{ CalDavProperty, CalDavPropertyName, CalendarData, CardDavProperty, CardDavPropertyName, - Comp, DateRange, DavProperty, DavValue, ResourceType, WebDavProperty, + Comp, DateRange, DavProperty, DavValue, PrincipalProperty, ResourceType, WebDavProperty, }, request::{DavPropertyValue, DeadProperty, VCardPropertyWithGroup}, response::List, @@ -450,16 +450,16 @@ impl DavProperty { Some(DavProperty::WebDav(WebDavProperty::SyncToken)) } (Namespace::Dav, Element::AlternateUriSet) => { - Some(DavProperty::WebDav(WebDavProperty::AlternateURISet)) + Some(DavProperty::Principal(PrincipalProperty::AlternateURISet)) } (Namespace::Dav, Element::PrincipalUrl) => { - Some(DavProperty::WebDav(WebDavProperty::PrincipalURL)) + Some(DavProperty::Principal(PrincipalProperty::PrincipalURL)) } (Namespace::Dav, Element::GroupMemberSet) => { - Some(DavProperty::WebDav(WebDavProperty::GroupMemberSet)) + Some(DavProperty::Principal(PrincipalProperty::GroupMemberSet)) } (Namespace::Dav, Element::GroupMembership) => { - Some(DavProperty::WebDav(WebDavProperty::GroupMembership)) + Some(DavProperty::Principal(PrincipalProperty::GroupMembership)) } (Namespace::Dav, Element::Owner) => Some(DavProperty::WebDav(WebDavProperty::Owner)), (Namespace::Dav, Element::Group) => Some(DavProperty::WebDav(WebDavProperty::Group)), @@ -488,6 +488,12 @@ impl DavProperty { (Namespace::CardDav, Element::SupportedCollationSet) => { Some(DavProperty::CardDav(CardDavProperty::SupportedCollationSet)) } + (Namespace::CardDav, Element::AddressbookHomeSet) => Some(DavProperty::Principal( + PrincipalProperty::AddressbookHomeSet, + )), + (Namespace::CardDav, Element::PrincipalAddress) => { + Some(DavProperty::Principal(PrincipalProperty::PrincipalAddress)) + } (Namespace::CardDav, Element::AddressData) => Some(DavProperty::CardDav( CardDavProperty::AddressData(Default::default()), )), diff --git a/crates/dav-proto/src/responses/acl.rs b/crates/dav-proto/src/responses/acl.rs index 132a0e26..0ed535a9 100644 --- a/crates/dav-proto/src/responses/acl.rs +++ b/crates/dav-proto/src/responses/acl.rs @@ -53,6 +53,16 @@ impl SupportedPrivilege { self.supported_privilege.0.push(supported_privilege); self } + + pub fn with_opt_supported_privilege( + mut self, + supported_privilege: Option, + ) -> Self { + if let Some(supported_privilege) = supported_privilege { + self.supported_privilege.0.push(supported_privilege); + } + self + } } impl Display for Ace { diff --git a/crates/dav-proto/src/responses/error.rs b/crates/dav-proto/src/responses/error.rs index aa95bfea..656d1666 100644 --- a/crates/dav-proto/src/responses/error.rs +++ b/crates/dav-proto/src/responses/error.rs @@ -135,7 +135,7 @@ impl Display for CardCondition { CardCondition::MaxResourceSize(l) => { write!(f, "{l}") } - CardCondition::AddressBoolCollectionLocationOk => { + CardCondition::AddressBookCollectionLocationOk => { write!(f, "") } } diff --git a/crates/dav-proto/src/responses/multistatus.rs b/crates/dav-proto/src/responses/multistatus.rs index 25e17d23..de3b76b9 100644 --- a/crates/dav-proto/src/responses/multistatus.rs +++ b/crates/dav-proto/src/responses/multistatus.rs @@ -90,10 +90,18 @@ impl MultiStatus { self } + pub fn set_namespace(&mut self, namespace: Namespace) { + self.namespace = namespace; + } + pub fn with_sync_token(mut self, sync_token: impl Into) -> Self { self.sync_token = Some(SyncToken(sync_token.into())); self } + + pub fn set_sync_token(&mut self, sync_token: impl Into) { + self.sync_token = Some(SyncToken(sync_token.into())); + } } impl Response { diff --git a/crates/dav-proto/src/responses/property.rs b/crates/dav-proto/src/responses/property.rs index e529c469..5cd95d76 100644 --- a/crates/dav-proto/src/responses/property.rs +++ b/crates/dav-proto/src/responses/property.rs @@ -15,8 +15,8 @@ use mail_parser::{ use crate::schema::{ property::{ ActiveLock, CalDavProperty, CardDavProperty, Comp, DavProperty, DavValue, LockDiscovery, - LockEntry, Privilege, ReportSet, ResourceType, Rfc1123DateTime, SupportedCollation, - SupportedLock, WebDavProperty, + LockEntry, PrincipalProperty, Privilege, ReportSet, ResourceType, Rfc1123DateTime, + SupportedCollation, SupportedLock, WebDavProperty, }, request::{DavPropertyValue, DeadProperty}, response::{Ace, AclRestrictions, Href, List, PropResponse, SupportedPrivilege}, @@ -94,6 +94,29 @@ impl Display for DavValue { DavValue::Acl(v) => v.fmt(f), DavValue::AclRestrictions(v) => v.fmt(f), DavValue::DeadProperty(v) => v.fmt(f), + DavValue::SupportedAddressData => { + write!( + f, + concat!( + "", + "", + "", + "", + "" + ) + ) + } + DavValue::SupportedCalendarData => { + write!( + f, + concat!( + "", + "", + "", + "" + ) + ) + } DavValue::Null => Ok(()), } } @@ -119,10 +142,6 @@ impl DavProperty { WebDavProperty::QuotaUsedBytes => "D:quota-used-bytes", WebDavProperty::SupportedReportSet => "D:supported-report-set", WebDavProperty::SyncToken => "D:sync-token", - WebDavProperty::AlternateURISet => "D:alternate-URI-set", - WebDavProperty::PrincipalURL => "D:principal-URL", - WebDavProperty::GroupMemberSet => "D:group-member-set", - WebDavProperty::GroupMembership => "D:group-membership", WebDavProperty::Owner => "D:owner", WebDavProperty::Group => "D:group", WebDavProperty::SupportedPrivilegeSet => "D:supported-privilege-set", @@ -157,6 +176,14 @@ impl DavProperty { CalDavProperty::TimezoneServiceSet => "C:timezone-service-set", CalDavProperty::TimezoneId => "C: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", + }, DavProperty::DeadProperty(dead) => { return (dead.name.as_str(), dead.attrs.as_deref()) } diff --git a/crates/dav-proto/src/schema/property.rs b/crates/dav-proto/src/schema/property.rs index ada74ee5..fe8f26b1 100644 --- a/crates/dav-proto/src/schema/property.rs +++ b/crates/dav-proto/src/schema/property.rs @@ -24,6 +24,7 @@ pub enum DavProperty { WebDav(WebDavProperty), CardDav(CardDavProperty), CalDav(CalDavProperty), + Principal(PrincipalProperty), DeadProperty(DeadElementTag), } @@ -48,11 +49,6 @@ pub enum WebDavProperty { QuotaUsedBytes, // Sync properties SyncToken, - // Principal properties - AlternateURISet, - PrincipalURL, - GroupMemberSet, - GroupMembership, // ACL properties (all protected) Owner, Group, @@ -103,6 +99,18 @@ pub enum CalDavProperty { TimezoneId, } +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(test, serde(tag = "type", content = "data"))] +pub enum PrincipalProperty { + AlternateURISet, + PrincipalURL, + GroupMemberSet, + GroupMembership, + AddressbookHomeSet, + PrincipalAddress, +} + #[derive(Debug, Default, Clone, PartialEq, Eq)] #[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] pub struct CalendarData { @@ -153,6 +161,8 @@ pub enum DavValue { Acl(List), AclRestrictions(AclRestrictions), DeadProperty(DeadProperty), + SupportedAddressData, + SupportedCalendarData, Null, } @@ -264,6 +274,7 @@ impl Rfc1123DateTime { impl DavProperty { pub fn is_all_prop(&self) -> bool { + let todo = "add cal, card"; matches!( self, DavProperty::WebDav(WebDavProperty::CreationDate) diff --git a/crates/dav-proto/src/schema/request.rs b/crates/dav-proto/src/schema/request.rs index 65c3ddb6..3fbe1264 100644 --- a/crates/dav-proto/src/schema/request.rs +++ b/crates/dav-proto/src/schema/request.rs @@ -15,10 +15,11 @@ use super::{ Collation, MatchType, }; -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Default)] #[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(test, serde(tag = "type", content = "data"))] pub enum PropFind { + #[default] PropName, AllProp(Vec), Prop(Vec), diff --git a/crates/dav-proto/src/schema/response.rs b/crates/dav-proto/src/schema/response.rs index 3ebb9ea6..96d3e8c6 100644 --- a/crates/dav-proto/src/schema/response.rs +++ b/crates/dav-proto/src/schema/response.rs @@ -223,7 +223,7 @@ pub enum CardCondition { ValidAddressData, NoUidConflict(Href), MaxResourceSize(u32), - AddressBoolCollectionLocationOk, + AddressBookCollectionLocationOk, } impl BaseCondition { diff --git a/crates/dav/src/card/acl.rs b/crates/dav/src/card/acl.rs deleted file mode 100644 index dc7edea8..00000000 --- a/crates/dav/src/card/acl.rs +++ /dev/null @@ -1,37 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use common::{Server, auth::AccessToken}; -use dav_proto::{RequestHeaders, schema::request::Acl}; -use http_proto::HttpResponse; - -use crate::common::uri::DavUriResource; - -pub(crate) trait CardAclRequestHandler: Sync + Send { - fn handle_card_acl_request( - &self, - access_token: &AccessToken, - headers: RequestHeaders<'_>, - request: Acl, - ) -> impl Future> + Send; -} - -impl CardAclRequestHandler for Server { - async fn handle_card_acl_request( - &self, - access_token: &AccessToken, - headers: RequestHeaders<'_>, - request: Acl, - ) -> crate::Result { - // Validate URI - let resource_ = self - .validate_uri(access_token, headers.uri) - .await? - .into_owned_uri()?; - - todo!() - } -} diff --git a/crates/dav/src/card/copy_move.rs b/crates/dav/src/card/copy_move.rs index 7c20ffd7..1fb70813 100644 --- a/crates/dav/src/card/copy_move.rs +++ b/crates/dav/src/card/copy_move.rs @@ -19,12 +19,16 @@ use trc::AddContext; use crate::{ DavError, DavErrorCondition, - card::{insert_card, update_card}, + card::{delete::delete_address_book, insert_addressbook, insert_card, update_card}, common::uri::DavUriResource, file::DavFileResource, }; -use super::{delete::delete_card, update_addressbook}; +use super::{ + assert_is_unique_uid, + delete::{delete_address_book_and_cards, delete_card}, + update_addressbook, +}; pub(crate) trait CardCopyMoveRequestHandler: Sync + Send { fn handle_card_copy_move_request( @@ -119,55 +123,94 @@ impl CardCopyMoveRequestHandler for Server { match (from_resource.is_container, to_resource.is_container) { (true, true) => { - // Overwrite container - if is_move { - move_container( - self, - access_token, - from_account_id, - from_resource.document_id, - from_resources - .subtree(from_resource_name) - .filter(|r| !r.is_container) - .map(|r| r.document_id) - .collect::>(), - to_account_id, - to_resource.document_id.into(), - to_resources - .subtree(destination_resource_name) - .filter(|r| !r.is_container) - .map(|r| r.document_id) - .collect::>(), - new_name.into(), - ) - .await - } else { - copy_container( - self, - access_token, - from_account_id, - from_resource.document_id, - from_resources - .subtree(from_resource_name) - .filter(|r| !r.is_container) - .map(|r| r.document_id) - .collect::>(), - to_account_id, - to_resource.document_id.into(), - to_resources - .subtree(destination_resource_name) - .filter(|r| !r.is_container) - .map(|r| r.document_id) - .collect::>(), - new_name.into(), - ) - .await + let from_children_ids = from_resources + .subtree(from_resource_name) + .filter(|r| !r.is_container) + .map(|r| r.document_id) + .collect::>(); + let to_document_ids = to_resources + .subtree(destination_resource_name) + .filter(|r| !r.is_container) + .map(|r| r.document_id) + .collect::>(); + + // Validate ACLs + if !access_token.is_member(to_account_id) + || (!access_token.is_member(from_account_id) + && !self + .has_access_to_document( + access_token, + from_account_id, + Collection::AddressBook, + from_resource.document_id, + if is_move { + Acl::RemoveItems + } else { + Acl::ReadItems + }, + ) + .await + .caused_by(trc::location!())?) + { + return Err(DavError::Code(StatusCode::FORBIDDEN)); } + + // Overwrite container + copy_container( + self, + access_token, + from_account_id, + from_resource.document_id, + from_children_ids, + to_account_id, + to_resource.document_id.into(), + to_document_ids, + new_name, + is_move, + ) + .await } (false, false) => { // 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) + && !self + .has_access_to_document( + access_token, + from_account_id, + Collection::AddressBook, + from_addressbook_id, + if is_move { + Acl::RemoveItems + } else { + Acl::ReadItems + }, + ) + .await + .caused_by(trc::location!())?) + || (!access_token.is_member(to_account_id) + && !self + .has_access_to_document( + access_token, + to_account_id, + Collection::AddressBook, + to_addressbook_id, + Acl::RemoveItems, + ) + .await + .caused_by(trc::location!())?) + { + return Err(DavError::Code(StatusCode::FORBIDDEN)); + } if is_move { move_card( @@ -179,7 +222,8 @@ impl CardCopyMoveRequestHandler for Server { to_account_id, to_resource.document_id.into(), to_addressbook_id, - new_name.into(), + to_base_path, + new_name, ) .await } else { @@ -188,16 +232,11 @@ impl CardCopyMoveRequestHandler for Server { access_token, from_account_id, from_resource.document_id, - from_addressbook_id, + to_account_id, to_resource.document_id.into(), to_addressbook_id, - headers.format_to_base_uri( - destination_resource_name - .rsplit_once('/') - .map(|(base, _)| base) - .unwrap_or(destination_resource_name), - ), - new_name.into(), + to_base_path, + new_name, ) .await } @@ -214,11 +253,40 @@ impl CardCopyMoveRequestHandler for Server { return Err(DavError::Code(StatusCode::BAD_GATEWAY)); } - let todo = "check acls"; - - // Copy/move card + // Validate ACL let from_addressbook_id = from_resource.parent_id.unwrap(); let to_addressbook_id = parent_resource.document_id; + if (!access_token.is_member(from_account_id) + && !self + .has_access_to_document( + access_token, + from_account_id, + Collection::AddressBook, + from_addressbook_id, + if is_move { + Acl::RemoveItems + } else { + Acl::ReadItems + }, + ) + .await + .caused_by(trc::location!())?) + || (!access_token.is_member(to_account_id) + && !self + .has_access_to_document( + access_token, + to_account_id, + Collection::AddressBook, + to_addressbook_id, + Acl::AddItems, + ) + .await + .caused_by(trc::location!())?) + { + return Err(DavError::Code(StatusCode::FORBIDDEN)); + } + + // Copy/move card if is_move { if from_account_id != to_account_id || parent_resource.document_id != from_addressbook_id @@ -232,7 +300,8 @@ impl CardCopyMoveRequestHandler for Server { to_account_id, None, to_addressbook_id, - new_name.into(), + headers.format_to_base_uri(&parent_resource.name), + new_name, ) .await } else { @@ -271,6 +340,26 @@ impl CardCopyMoveRequestHandler for Server { return Err(DavError::Code(StatusCode::FORBIDDEN)); } + // Validate ACLs + if !access_token.is_member(from_account_id) + && !self + .has_access_to_document( + access_token, + from_account_id, + Collection::AddressBook, + from_resource.document_id, + if is_move { + Acl::RemoveItems + } else { + Acl::ReadItems + }, + ) + .await + .caused_by(trc::location!())? + { + return Err(DavError::Code(StatusCode::FORBIDDEN)); + } + // Copy/move container let from_children_ids = from_resources .subtree(from_resource_name) @@ -279,7 +368,7 @@ impl CardCopyMoveRequestHandler for Server { .collect::>(); if is_move { if from_account_id != to_account_id { - move_container( + copy_container( self, access_token, from_account_id, @@ -292,7 +381,8 @@ impl CardCopyMoveRequestHandler for Server { to_account_id, None, vec![], - new_name.into(), + new_name, + true, ) .await } else { @@ -319,7 +409,8 @@ impl CardCopyMoveRequestHandler for Server { to_account_id, None, vec![], - new_name.into(), + new_name, + false, ) .await } @@ -374,7 +465,7 @@ async fn copy_card( }); update_card( access_token, - card.clone(), + card, new_card, from_account_id, from_document_id, @@ -383,7 +474,21 @@ async fn copy_card( ) .caused_by(trc::location!())?; } else { - let todo = "check uid"; + // Validate UID + assert_is_unique_uid( + server, + server + .fetch_dav_resources(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?; + let mut new_card = card .deserialize::() .caused_by(trc::location!())?; @@ -391,25 +496,55 @@ async fn copy_card( name: new_name.to_string(), parent_id: to_addressbook_id, }]; - //insert_card(access_token, new_card, to_account_id, false, &mut batch) - // .caused_by(trc::location!())?; - } - - if let Some(to_document_id) = to_document_id { - delete_card( + 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, - to_addressbook_id, - card, + false, &mut batch, ) - .await .caused_by(trc::location!())?; + } + + let response = if let Some(to_document_id) = to_document_id { + // Overwrite card on destination + let card_ = server + .get_archive(to_account_id, Collection::ContactCard, to_document_id) + .await + .caused_by(trc::location!())?; + if let Some(card_) = card_ { + let card = 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!())?; + } + Ok(HttpResponse::new(StatusCode::NO_CONTENT)) } else { Ok(HttpResponse::new(StatusCode::CREATED)) - } + }; + + server + .commit_batch(batch) + .await + .caused_by(trc::location!())?; + + response } #[allow(clippy::too_many_arguments)] @@ -422,9 +557,141 @@ async fn move_card( to_account_id: u32, to_document_id: Option, to_addressbook_id: u32, - new_name: Option<&str>, + to_base_path: String, + new_name: &str, ) -> crate::Result { - todo!() + // Fetch card + let card_ = server + .get_archive(from_account_id, Collection::ContactCard, from_document_id) + .await + .caused_by(trc::location!())? + .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; + let card = card_ + .to_unarchived::() + .caused_by(trc::location!())?; + + let mut batch = BatchBuilder::new(); + if from_account_id == to_account_id { + let mut name_idx = None; + for (idx, name) in card.inner.names.iter().enumerate() { + 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()), + ))); + } else if name.parent_id == from_addressbook_id { + name_idx = Some(idx); + break; + } + } + + let name_idx = if let Some(name_idx) = name_idx { + name_idx + } else { + return Err(DavError::Code(StatusCode::NOT_FOUND)); + }; + + let mut new_card = card + .deserialize::() + .caused_by(trc::location!())?; + new_card.names.swap_remove(name_idx); + new_card.names.push(DavName { + 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!())?; + } else { + // Validate UID + assert_is_unique_uid( + server, + server + .fetch_dav_resources(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?; + + let mut new_card = card + .deserialize::() + .caused_by(trc::location!())?; + new_card.names = vec![DavName { + name: new_name.to_string(), + 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!())?; + + 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!())?; + } + + let response = if let Some(to_document_id) = to_document_id { + // Overwrite card on destination + let card_ = server + .get_archive(to_account_id, Collection::ContactCard, to_document_id) + .await + .caused_by(trc::location!())?; + if let Some(card_) = card_ { + let card = 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!())?; + } + + Ok(HttpResponse::new(StatusCode::NO_CONTENT)) + } else { + Ok(HttpResponse::new(StatusCode::CREATED)) + }; + + server + .commit_batch(batch) + .await + .caused_by(trc::location!())?; + + response } #[allow(clippy::too_many_arguments)] @@ -486,24 +753,194 @@ async fn copy_container( to_account_id: u32, to_document_id: Option, to_children_ids: Vec, - new_name: Option<&str>, + new_name: &str, + remove_source: bool, ) -> crate::Result { - todo!() -} + // Fetch book + let book_ = server + .get_archive(from_account_id, Collection::AddressBook, from_document_id) + .await + .caused_by(trc::location!())? + .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; + let old_book = book_ + .to_unarchived::() + .caused_by(trc::location!())?; + let mut book = old_book + .deserialize::() + .caused_by(trc::location!())?; -#[allow(clippy::too_many_arguments)] -async fn move_container( - server: &Server, - access_token: &AccessToken, - from_account_id: u32, - from_document_id: u32, - from_children_ids: Vec, - to_account_id: u32, - to_document_id: Option, - to_children_ids: Vec, - new_name: Option<&str>, -) -> crate::Result { - todo!() + // Prepare write batch + 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!())?; + } + + book.name = new_name.to_string(); + book.subscribers.clear(); + book.acls.clear(); + book.is_default = false; + + let is_overwrite = to_document_id.is_some(); + let to_document_id = if let Some(to_document_id) = to_document_id { + // Overwrite destination + let book_ = server + .get_archive(to_account_id, Collection::AddressBook, to_document_id) + .await + .caused_by(trc::location!())?; + if let Some(book_) = book_ { + let book = book_ + .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!())?; + } + + to_document_id + } else { + server + .store() + .assign_document_ids(to_account_id, Collection::AddressBook, 1) + .await + .caused_by(trc::location!())? + }; + insert_addressbook( + access_token, + book, + to_account_id, + to_document_id, + false, + &mut batch, + ) + .caused_by(trc::location!())?; + + // Copy children + let mut required_space = 0; + for from_child_document_id in from_children_ids { + if let Some(card_) = server + .get_archive( + from_account_id, + Collection::ContactCard, + from_child_document_id, + ) + .await? + { + let card = card_ + .to_unarchived::() + .caused_by(trc::location!())?; + let mut new_name = None; + + for name in card.inner.names.iter() { + if name.parent_id == to_document_id { + continue; + } else if name.parent_id == from_document_id { + new_name = Some(name.name.to_string()); + } + } + let new_name = if let Some(new_name) = new_name { + DavName { + name: new_name, + parent_id: to_document_id, + } + } else { + continue; + }; + let card = card_ + .to_unarchived::() + .caused_by(trc::location!())?; + let mut new_card = card + .deserialize::() + .caused_by(trc::location!())?; + + if from_account_id == to_account_id { + if remove_source { + new_card + .names + .retain(|name| name.parent_id != from_document_id); + } + + 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( + access_token, + from_account_id, + from_child_document_id, + from_document_id, + card, + &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!())?; + 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!())?; + } + } + } + + if from_account_id != to_account_id && required_space > 0 { + server + .has_available_quota( + &server + .get_resource_token(access_token, to_account_id) + .await?, + required_space, + ) + .await?; + } + + server + .commit_batch(batch) + .await + .caused_by(trc::location!())?; + + if !is_overwrite { + Ok(HttpResponse::new(StatusCode::CREATED)) + } else { + Ok(HttpResponse::new(StatusCode::NO_CONTENT)) + } } #[allow(clippy::too_many_arguments)] diff --git a/crates/dav/src/card/delete.rs b/crates/dav/src/card/delete.rs index c328b82b..1811f08c 100644 --- a/crates/dav/src/card/delete.rs +++ b/crates/dav/src/card/delete.rs @@ -105,7 +105,7 @@ impl CardDeleteRequestHandler for Server { .await?; // Delete addressbook and cards - delete_address_book( + delete_address_book_and_cards( self, access_token, account_id, @@ -172,7 +172,6 @@ impl CardDeleteRequestHandler for Server { .caused_by(trc::location!())?, &mut batch, ) - .await .caused_by(trc::location!())?; } @@ -182,7 +181,7 @@ impl CardDeleteRequestHandler for Server { } } -pub(crate) async fn delete_address_book( +pub(crate) async fn delete_address_book_and_cards( server: &Server, access_token: &AccessToken, account_id: u32, @@ -207,13 +206,23 @@ pub(crate) async fn delete_address_book( .to_unarchived::() .caused_by(trc::location!())?, batch, - ) - .await?; + )?; } } + 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 - let mut batch = BatchBuilder::new(); batch .with_account_id(account_id) .with_collection(Collection::AddressBook) @@ -223,12 +232,13 @@ pub(crate) async fn delete_address_book( .with_tenant_id(access_token) .with_current(book), ) - .caused_by(trc::location!())?; + .caused_by(trc::location!())? + .commit_point(); Ok(()) } -pub(crate) async fn delete_card( +pub(crate) fn delete_card( access_token: &AccessToken, account_id: u32, document_id: u32, diff --git a/crates/dav/src/card/mkcol.rs b/crates/dav/src/card/mkcol.rs index 5b94fcaa..fd92ad40 100644 --- a/crates/dav/src/card/mkcol.rs +++ b/crates/dav/src/card/mkcol.rs @@ -4,7 +4,7 @@ * 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, Return, schema::{Namespace, request::MkCol, response::MkColResponse}, @@ -13,7 +13,7 @@ use groupware::{contact::AddressBook, hierarchy::DavHierarchy}; use http_proto::HttpResponse; use hyper::StatusCode; use jmap_proto::types::collection::Collection; -use store::write::{BatchBuilder, now}; +use store::write::BatchBuilder; use trc::AddContext; use crate::{ @@ -24,7 +24,7 @@ use crate::{ }, }; -use super::proppatch::CardPropPatchRequestHandler; +use super::{insert_addressbook, proppatch::CardPropPatchRequestHandler}; pub(crate) trait CardMkColRequestHandler: Sync + Send { fn handle_card_mkcol_request( @@ -81,11 +81,8 @@ impl CardMkColRequestHandler for Server { .await?; // Build file container - let now = now(); let mut book = AddressBook { name: name.to_string(), - created: now as i64, - modified: now as i64, ..Default::default() }; @@ -112,12 +109,15 @@ impl CardMkColRequestHandler for Server { .assign_document_ids(account_id, Collection::AddressBook, 1) .await .caused_by(trc::location!())?; - batch - .with_account_id(account_id) - .with_collection(Collection::AddressBook) - .create_document(document_id) - .custom(ObjectIndexBuilder::<(), _>::new().with_changes(book)) - .caused_by(trc::location!())?; + insert_addressbook( + access_token, + book, + account_id, + document_id, + false, + &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 5006021b..b641d1b7 100644 --- a/crates/dav/src/card/mod.rs +++ b/crates/dav/src/card/mod.rs @@ -4,23 +4,107 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{auth::AccessToken, storage::index::ObjectIndexBuilder}; -use groupware::contact::{AddressBook, ArchivedAddressBook, ArchivedContactCard, ContactCard}; +use common::{DavResources, Server, auth::AccessToken, storage::index::ObjectIndexBuilder}; +use dav_proto::schema::{ + property::{CardDavProperty, DavProperty, WebDavProperty}, + response::CardCondition, +}; +use groupware::{ + IDX_CARD_UID, + contact::{AddressBook, ArchivedAddressBook, ArchivedContactCard, ContactCard}, +}; +use hyper::StatusCode; use jmap_proto::types::collection::Collection; -use store::write::{Archive, BatchBuilder, now}; +use store::{ + query::Filter, + write::{Archive, BatchBuilder, now}, +}; +use trc::AddContext; -use crate::common::ExtractETag; +use crate::{DavError, DavErrorCondition, common::ExtractETag}; -pub mod acl; pub mod copy_move; pub mod delete; pub mod get; pub mod mkcol; -pub mod propfind; pub mod proppatch; pub mod query; pub mod update; +pub(crate) static CARD_CONTAINER_PROPS: [DavProperty; 23] = [ + DavProperty::WebDav(WebDavProperty::CreationDate), + DavProperty::WebDav(WebDavProperty::DisplayName), + DavProperty::WebDav(WebDavProperty::GetETag), + DavProperty::WebDav(WebDavProperty::GetLastModified), + DavProperty::WebDav(WebDavProperty::ResourceType), + DavProperty::WebDav(WebDavProperty::LockDiscovery), + DavProperty::WebDav(WebDavProperty::SupportedLock), + DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal), + DavProperty::WebDav(WebDavProperty::SyncToken), + DavProperty::WebDav(WebDavProperty::Owner), + DavProperty::WebDav(WebDavProperty::SupportedPrivilegeSet), + DavProperty::WebDav(WebDavProperty::CurrentUserPrivilegeSet), + DavProperty::WebDav(WebDavProperty::Acl), + DavProperty::WebDav(WebDavProperty::AclRestrictions), + DavProperty::WebDav(WebDavProperty::InheritedAclSet), + DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet), + DavProperty::WebDav(WebDavProperty::SupportedReportSet), + DavProperty::WebDav(WebDavProperty::QuotaAvailableBytes), + DavProperty::WebDav(WebDavProperty::QuotaUsedBytes), + DavProperty::CardDav(CardDavProperty::AddressbookDescription), + DavProperty::CardDav(CardDavProperty::SupportedAddressData), + DavProperty::CardDav(CardDavProperty::SupportedCollationSet), + DavProperty::CardDav(CardDavProperty::MaxResourceSize), +]; + +pub(crate) static CARD_ITEM_PROPS: [DavProperty; 20] = [ + DavProperty::WebDav(WebDavProperty::CreationDate), + DavProperty::WebDav(WebDavProperty::DisplayName), + DavProperty::WebDav(WebDavProperty::GetETag), + DavProperty::WebDav(WebDavProperty::GetLastModified), + DavProperty::WebDav(WebDavProperty::ResourceType), + DavProperty::WebDav(WebDavProperty::LockDiscovery), + DavProperty::WebDav(WebDavProperty::SupportedLock), + DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal), + DavProperty::WebDav(WebDavProperty::SyncToken), + DavProperty::WebDav(WebDavProperty::Owner), + DavProperty::WebDav(WebDavProperty::SupportedPrivilegeSet), + DavProperty::WebDav(WebDavProperty::CurrentUserPrivilegeSet), + DavProperty::WebDav(WebDavProperty::Acl), + DavProperty::WebDav(WebDavProperty::AclRestrictions), + DavProperty::WebDav(WebDavProperty::InheritedAclSet), + DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet), + DavProperty::WebDav(WebDavProperty::GetContentLanguage), + DavProperty::WebDav(WebDavProperty::GetContentLength), + DavProperty::WebDav(WebDavProperty::GetContentType), + DavProperty::CardDav(CardDavProperty::AddressData(vec![])), +]; + +pub(crate) static CARD_ALL_PROPS: [DavProperty; 22] = [ + DavProperty::WebDav(WebDavProperty::CreationDate), + DavProperty::WebDav(WebDavProperty::DisplayName), + DavProperty::WebDav(WebDavProperty::GetETag), + DavProperty::WebDav(WebDavProperty::GetLastModified), + DavProperty::WebDav(WebDavProperty::ResourceType), + DavProperty::WebDav(WebDavProperty::LockDiscovery), + DavProperty::WebDav(WebDavProperty::SupportedLock), + DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal), + DavProperty::WebDav(WebDavProperty::SyncToken), + DavProperty::WebDav(WebDavProperty::SupportedPrivilegeSet), + DavProperty::WebDav(WebDavProperty::AclRestrictions), + DavProperty::WebDav(WebDavProperty::CurrentUserPrivilegeSet), + DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet), + DavProperty::WebDav(WebDavProperty::GetContentLanguage), + DavProperty::WebDav(WebDavProperty::GetContentLength), + DavProperty::WebDav(WebDavProperty::GetContentType), + DavProperty::WebDav(WebDavProperty::SupportedReportSet), + DavProperty::CardDav(CardDavProperty::AddressData(vec![])), + DavProperty::CardDav(CardDavProperty::AddressbookDescription), + DavProperty::CardDav(CardDavProperty::SupportedAddressData), + DavProperty::CardDav(CardDavProperty::SupportedCollationSet), + DavProperty::CardDav(CardDavProperty::MaxResourceSize), +]; + pub(crate) fn update_card( access_token: &AccessToken, card: Archive<&ArchivedContactCard>, @@ -77,6 +161,34 @@ pub(crate) fn insert_card( 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>, @@ -90,7 +202,6 @@ pub(crate) fn update_addressbook( new_book.modified = now() as i64; // Prepare write batch - let mut batch = BatchBuilder::new(); batch .with_account_id(account_id) .with_collection(Collection::AddressBook) @@ -105,3 +216,39 @@ pub(crate) fn update_addressbook( 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 + .store() + .filter( + account_id, + Collection::ContactCard, + vec![Filter::eq(IDX_CARD_UID, uid.as_bytes().to_vec())], + ) + .await + .caused_by(trc::location!())?; + if !hits.results.is_empty() { + for path in resources.paths.iter() { + if !path.is_container + && hits.results.contains(path.document_id) + && path.parent_id.unwrap() == addressbook_id + { + return Err(DavError::Condition(DavErrorCondition::new( + StatusCode::PRECONDITION_FAILED, + CardCondition::NoUidConflict(format!("{}/{}", base_uri, path.name).into()), + ))); + } + } + } + } + + Ok(()) +} diff --git a/crates/dav/src/card/propfind.rs b/crates/dav/src/card/propfind.rs deleted file mode 100644 index a5e61523..00000000 --- a/crates/dav/src/card/propfind.rs +++ /dev/null @@ -1,53 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use common::{Server, auth::AccessToken}; -use dav_proto::{RequestHeaders, schema::request::MultiGet}; -use http_proto::HttpResponse; - -use crate::common::{DavQuery, uri::DavUriResource}; - -pub(crate) trait CardPropFindRequestHandler: Sync + Send { - fn handle_card_propfind_request( - &self, - access_token: &AccessToken, - query: DavQuery<'_>, - ) -> impl Future> + Send; - - fn handle_card_multiget_request( - &self, - access_token: &AccessToken, - headers: RequestHeaders<'_>, - request: MultiGet, - ) -> impl Future> + Send; -} - -impl CardPropFindRequestHandler for Server { - async fn handle_card_propfind_request( - &self, - access_token: &AccessToken, - query: DavQuery<'_>, - ) -> crate::Result { - // Validate URI - - todo!() - } - - async fn handle_card_multiget_request( - &self, - access_token: &AccessToken, - headers: RequestHeaders<'_>, - request: MultiGet, - ) -> crate::Result { - // Validate URI - let resource_ = self - .validate_uri(access_token, headers.uri) - .await? - .into_owned_uri()?; - - todo!() - } -} diff --git a/crates/dav/src/card/proppatch.rs b/crates/dav/src/card/proppatch.rs index 028bf536..ea3f5f35 100644 --- a/crates/dav/src/card/proppatch.rs +++ b/crates/dav/src/card/proppatch.rs @@ -6,18 +6,34 @@ use common::{Server, auth::AccessToken}; use dav_proto::{ - RequestHeaders, + RequestHeaders, Return, schema::{ + Namespace, property::{CardDavProperty, DavProperty, DavValue, ResourceType, WebDavProperty}, request::{DavPropertyValue, PropertyUpdate}, - response::{BaseCondition, PropStat}, + response::{BaseCondition, MultiStatus, PropStat, Response}, }, }; -use groupware::contact::AddressBook; +use groupware::{ + contact::{AddressBook, ContactCard}, + 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::common::uri::DavUriResource; +use crate::{ + DavError, DavMethod, + common::{ + ETag, + lock::{LockRequestHandler, ResourceState}, + uri::DavUriResource, + }, +}; + +use super::{update_addressbook, update_card}; pub(crate) trait CardPropPatchRequestHandler: Sync + Send { fn handle_card_proppatch_request( @@ -34,6 +50,14 @@ pub(crate) trait CardPropPatchRequestHandler: Sync + Send { properties: Vec, items: &mut Vec, ) -> bool; + + fn apply_card_properties( + &self, + card: &mut ContactCard, + is_update: bool, + properties: Vec, + items: &mut Vec, + ) -> bool; } impl CardPropPatchRequestHandler for Server { @@ -41,15 +65,183 @@ impl CardPropPatchRequestHandler for Server { &self, access_token: &AccessToken, headers: RequestHeaders<'_>, - request: PropertyUpdate, + mut request: PropertyUpdate, ) -> crate::Result { // Validate URI let resource_ = self .validate_uri(access_token, headers.uri) .await? .into_owned_uri()?; + let uri = headers.uri; + let account_id = resource_.account_id; + let resources = self + .fetch_dav_resources(account_id, Collection::AddressBook) + .await + .caused_by(trc::location!())?; + let resource = resource_ + .resource + .and_then(|r| resources.paths.by_name(r)) + .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; + let document_id = resource.document_id; + let collection = if resource.is_container { + Collection::AddressBook + } else { + Collection::ContactCard + }; - todo!() + if !request.has_changes() { + return Ok(HttpResponse::new(StatusCode::NO_CONTENT)); + } + + // Verify ACL + if !access_token.is_member(account_id) { + let (acl, document_id) = if resource.is_container { + (Acl::Read, resource.document_id) + } else { + (Acl::ReadItems, resource.parent_id.unwrap()) + }; + + if !self + .has_access_to_document( + access_token, + account_id, + Collection::AddressBook, + document_id, + acl, + ) + .await + .caused_by(trc::location!())? + { + return Err(DavError::Code(StatusCode::FORBIDDEN)); + } + } + + // Fetch archive + let archive = self + .get_archive(account_id, collection, document_id) + .await + .caused_by(trc::location!())? + .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; + + // Validate headers + self.validate_headers( + access_token, + &headers, + vec![ResourceState { + account_id, + collection, + document_id: document_id.into(), + etag: archive.etag().into(), + path: resource_.resource.unwrap(), + ..Default::default() + }], + Default::default(), + DavMethod::PROPPATCH, + ) + .await?; + + let is_success; + let mut batch = BatchBuilder::new(); + let mut items = Vec::with_capacity(request.remove.len() + request.set.len()); + + let etag = if resource.is_container { + // Deserialize + let book = archive + .to_unarchived::() + .caused_by(trc::location!())?; + let mut new_book = archive + .deserialize::() + .caused_by(trc::location!())?; + + // Remove properties + if !request.set_first && !request.remove.is_empty() { + remove_addressbook_properties( + &mut new_book, + std::mem::take(&mut request.remove), + &mut items, + ); + } + + // Set properties + is_success = + self.apply_addressbook_properties(&mut new_book, true, request.set, &mut items); + + // Remove properties + if is_success && !request.remove.is_empty() { + remove_addressbook_properties(&mut new_book, request.remove, &mut items); + } + + if is_success { + update_addressbook( + access_token, + book, + new_book, + account_id, + document_id, + true, + &mut batch, + ) + .caused_by(trc::location!())? + } else { + book.etag().into() + } + } else { + // Deserialize + let card = archive + .to_unarchived::() + .caused_by(trc::location!())?; + let mut new_card = archive + .deserialize::() + .caused_by(trc::location!())?; + + // Remove properties + if !request.set_first && !request.remove.is_empty() { + remove_card_properties( + &mut new_card, + std::mem::take(&mut request.remove), + &mut items, + ); + } + + // Set properties + is_success = self.apply_card_properties(&mut new_card, true, request.set, &mut items); + + // Remove properties + if is_success && !request.remove.is_empty() { + remove_card_properties(&mut new_card, request.remove, &mut items); + } + + if is_success { + update_card( + access_token, + card, + new_card, + account_id, + document_id, + true, + &mut batch, + ) + .caused_by(trc::location!())? + } else { + card.etag().into() + } + }; + + if is_success { + self.commit_batch(batch).await.caused_by(trc::location!())?; + } + + if headers.ret != Return::Minimal || !is_success { + Ok(HttpResponse::new(StatusCode::MULTI_STATUS) + .with_xml_body( + MultiStatus::new(vec![Response::new_propstat(uri, items)]) + .with_namespace(Namespace::CardDav) + .to_string(), + ) + .with_etag_opt(etag)) + } else { + Ok(HttpResponse::new(StatusCode::NO_CONTENT).with_etag_opt(etag)) + } } fn apply_addressbook_properties( @@ -164,4 +356,143 @@ impl CardPropPatchRequestHandler for Server { !has_errors } + + fn apply_card_properties( + &self, + card: &mut ContactCard, + is_update: bool, + properties: Vec, + items: &mut Vec, + ) -> bool { + let mut has_errors = false; + + for property in properties { + match (property.property, property.value) { + (DavProperty::WebDav(WebDavProperty::DisplayName), DavValue::String(name)) => { + if name.len() <= self.core.dav.live_property_size { + card.display_name = Some(name); + items.push( + PropStat::new(DavProperty::WebDav(WebDavProperty::DisplayName)) + .with_status(StatusCode::OK), + ); + } else { + items.push( + PropStat::new(DavProperty::WebDav(WebDavProperty::DisplayName)) + .with_status(StatusCode::INSUFFICIENT_STORAGE) + .with_response_description("Display name too long"), + ); + has_errors = true; + } + } + (DavProperty::WebDav(WebDavProperty::CreationDate), DavValue::Timestamp(dt)) => { + card.created = dt; + } + (DavProperty::DeadProperty(dead), DavValue::DeadProperty(values)) + if self.core.dav.dead_property_size.is_some() => + { + if is_update { + card.dead_properties.remove_element(&dead); + } + + if card.dead_properties.size() + values.size() + dead.size() + < self.core.dav.dead_property_size.unwrap() + { + card.dead_properties.add_element(dead.clone(), values.0); + items.push( + PropStat::new(DavProperty::DeadProperty(dead)) + .with_status(StatusCode::OK), + ); + } else { + items.push( + PropStat::new(DavProperty::DeadProperty(dead)) + .with_status(StatusCode::INSUFFICIENT_STORAGE) + .with_response_description("Dead property is too large."), + ); + has_errors = true; + } + } + (property, _) => { + items.push( + PropStat::new(property) + .with_status(StatusCode::CONFLICT) + .with_response_description("Property cannot be modified"), + ); + has_errors = true; + } + } + } + + !has_errors + } +} + +fn remove_card_properties( + card: &mut ContactCard, + properties: Vec, + items: &mut Vec, +) { + for property in properties { + match property { + DavProperty::WebDav(WebDavProperty::DisplayName) => { + card.display_name = None; + items.push( + PropStat::new(DavProperty::WebDav(WebDavProperty::DisplayName)) + .with_status(StatusCode::OK), + ); + } + DavProperty::DeadProperty(dead) => { + card.dead_properties.remove_element(&dead); + items.push( + PropStat::new(DavProperty::DeadProperty(dead)).with_status(StatusCode::OK), + ); + } + property => { + items.push( + PropStat::new(property) + .with_status(StatusCode::CONFLICT) + .with_response_description("Property cannot be modified"), + ); + } + } + } +} + +fn remove_addressbook_properties( + book: &mut AddressBook, + properties: Vec, + items: &mut Vec, +) { + for property in properties { + match property { + DavProperty::CardDav(CardDavProperty::AddressbookDescription) => { + book.description = None; + items.push( + PropStat::new(DavProperty::CardDav( + CardDavProperty::AddressbookDescription, + )) + .with_status(StatusCode::OK), + ); + } + DavProperty::WebDav(WebDavProperty::DisplayName) => { + book.display_name = None; + items.push( + PropStat::new(DavProperty::WebDav(WebDavProperty::DisplayName)) + .with_status(StatusCode::OK), + ); + } + DavProperty::DeadProperty(dead) => { + book.dead_properties.remove_element(&dead); + items.push( + PropStat::new(DavProperty::DeadProperty(dead)).with_status(StatusCode::OK), + ); + } + property => { + items.push( + PropStat::new(property) + .with_status(StatusCode::CONFLICT) + .with_response_description("Property cannot be modified"), + ); + } + } + } } diff --git a/crates/dav/src/card/update.rs b/crates/dav/src/card/update.rs index a4771826..e22c11ad 100644 --- a/crates/dav/src/card/update.rs +++ b/crates/dav/src/card/update.rs @@ -5,31 +5,30 @@ */ use calcard::{Entry, Parser}; -use common::{Server, auth::AccessToken, storage::index::ObjectIndexBuilder}; +use common::{Server, auth::AccessToken}; use dav_proto::{ RequestHeaders, Return, schema::{property::Rfc1123DateTime, response::CardCondition}, }; -use groupware::{DavName, IDX_CARD_UID, contact::ContactCard, hierarchy::DavHierarchy}; +use groupware::{DavName, contact::ContactCard, hierarchy::DavHierarchy}; use http_proto::HttpResponse; use hyper::StatusCode; use jmap_proto::types::{acl::Acl, collection::Collection}; -use store::{ - query::Filter, - write::{BatchBuilder, now}, -}; +use store::write::BatchBuilder; use trc::AddContext; use crate::{ DavError, DavErrorCondition, DavMethod, common::{ - ETag, ExtractETag, + ETag, lock::{LockRequestHandler, ResourceState}, uri::DavUriResource, }, file::DavFileResource, }; +use super::{assert_is_unique_uid, insert_card, update_card}; + pub(crate) trait CardUpdateRequestHandler: Sync + Send { fn handle_card_update_request( &self, @@ -110,7 +109,7 @@ impl CardUpdateRequestHandler for Server { // Update let card_ = self - .get_archive(account_id, Collection::FileNode, document_id) + .get_archive(account_id, Collection::ContactCard, document_id) .await .caused_by(trc::location!())? .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; @@ -182,23 +181,20 @@ impl CardUpdateRequestHandler for Server { .deserialize::() .caused_by(trc::location!())?; new_card.size = bytes.len() as u32; - new_card.modified = now() as i64; new_card.card = vcard; // Prepare write batch let mut batch = BatchBuilder::new(); - 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), - ) - .caused_by(trc::location!())?; - let etag = batch.etag(); + let etag = update_card( + access_token, + card, + new_card, + account_id, + document_id, + true, + &mut batch, + ) + .caused_by(trc::location!())?; self.commit_batch(batch).await.caused_by(trc::location!())?; Ok(HttpResponse::new(StatusCode::NO_CONTENT).with_etag_opt(etag)) @@ -249,43 +245,23 @@ impl CardUpdateRequestHandler for Server { } // Validate UID - if let Some(uid) = vcard.uid() { - let hits = self - .store() - .filter( - account_id, - Collection::ContactCard, - vec![Filter::eq(IDX_CARD_UID, uid.as_bytes().to_vec())], - ) - .await - .caused_by(trc::location!())?; - if !hits.results.is_empty() { - for path in resources.paths.iter() { - if !path.is_container - && hits.results.contains(path.document_id) - && path.parent_id.unwrap() == parent.document_id - { - return Err(DavError::Condition(DavErrorCondition::new( - StatusCode::PRECONDITION_FAILED, - CardCondition::NoUidConflict( - headers.format_to_base_uri(&path.name).into(), - ), - ))); - } - } - } - } + assert_is_unique_uid( + self, + &resources, + account_id, + parent.document_id, + vcard.uid(), + headers.base_uri().unwrap_or_default(), + ) + .await?; // Build node - let now = now(); let card = ContactCard { names: vec![DavName { name: name.to_string(), parent_id: parent.document_id, }], card: vcard, - created: now as i64, - modified: now as i64, size: bytes.len() as u32, ..Default::default() }; @@ -297,17 +273,15 @@ impl CardUpdateRequestHandler for Server { .assign_document_ids(account_id, Collection::ContactCard, 1) .await .caused_by(trc::location!())?; - 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), - ) - .caused_by(trc::location!())?; - let etag = batch.etag(); + let etag = insert_card( + access_token, + card, + account_id, + document_id, + true, + &mut batch, + ) + .caused_by(trc::location!())?; 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 d7883e01..34004639 100644 --- a/crates/dav/src/common/acl.rs +++ b/crates/dav/src/common/acl.rs @@ -14,7 +14,9 @@ use dav_proto::{ }, }; use directory::{QueryBy, Type, backend::internal::PrincipalField}; -use groupware::{calendar::Calendar, contact::AddressBook, file::FileNode}; +use groupware::{ + calendar::Calendar, contact::AddressBook, file::FileNode, hierarchy::DavHierarchy, +}; use http_proto::HttpResponse; use hyper::StatusCode; use jmap_proto::types::{ @@ -23,16 +25,25 @@ use jmap_proto::types::{ value::{AclGrant, ArchivedAclGrant}, }; use rkyv::vec::ArchivedVec; -use store::{ahash::AHashSet, roaring::RoaringBitmap}; +use store::{ahash::AHashSet, roaring::RoaringBitmap, write::BatchBuilder}; use trc::AddContext; use utils::map::bitmap::Bitmap; use crate::{ - DavError, DavErrorCondition, DavResource, common::uri::DavUriResource, - principal::propfind::PrincipalPropFind, + DavError, DavErrorCondition, DavResource, card::update_addressbook, + common::uri::DavUriResource, file::update_file_node, principal::propfind::PrincipalPropFind, }; +use super::ArchivedResource; + pub(crate) trait DavAclHandler: Sync + Send { + fn handle_acl_request( + &self, + access_token: &AccessToken, + headers: RequestHeaders<'_>, + request: dav_proto::schema::request::Acl, + ) -> impl Future> + Send; + fn handle_acl_prop_set( &self, access_token: &AccessToken, @@ -68,11 +79,113 @@ pub(crate) trait DavAclHandler: Sync + Send { fn resolve_ace( &self, - unresolved_aces: Vec, + access_token: &AccessToken, + account_id: u32, + grants: &ArchivedVec, ) -> impl Future>> + Send; } impl DavAclHandler for Server { + async fn handle_acl_request( + &self, + access_token: &AccessToken, + headers: RequestHeaders<'_>, + request: dav_proto::schema::request::Acl, + ) -> crate::Result { + // Validate URI + let resource_ = self + .validate_uri(access_token, headers.uri) + .await? + .into_owned_uri()?; + let account_id = resource_.account_id; + let collection = resource_.collection; + + if !matches!( + collection, + Collection::AddressBook | Collection::Calendar | Collection::FileNode + ) { + return Err(DavError::Code(StatusCode::FORBIDDEN)); + } + let resources = self + .fetch_dav_resources(account_id, collection) + .await + .caused_by(trc::location!())?; + let resource = resource_ + .resource + .and_then(|r| resources.paths.by_name(r)) + .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; + if !resource.is_container && !matches!(collection, Collection::FileNode) { + return Err(DavError::Code(StatusCode::FORBIDDEN)); + } + + // Fetch node + let archive = self + .get_archive(account_id, collection, resource.document_id) + .await + .caused_by(trc::location!())? + .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; + + let container = + ArchivedResource::from_archive(&archive, collection).caused_by(trc::location!())?; + + // Validate ACL + let acls = container.acls().unwrap(); + if !access_token.is_member(account_id) + && !acls.effective_acl(access_token).contains(Acl::Administer) + { + return Err(DavError::Code(StatusCode::FORBIDDEN)); + } + + // Validate ACEs + let grants = self + .validate_and_map_aces(access_token, request, collection) + .await?; + + if grants.len() != acls.len() || acls.iter().zip(grants.iter()).any(|(a, b)| a != b) { + let mut batch = BatchBuilder::new(); + + match container { + ArchivedResource::Calendar(calendar) => todo!(), + ArchivedResource::AddressBook(book) => { + let mut new_book = book + .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!())?; + } + 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!())?; + } + _ => unreachable!(), + } + + self.commit_batch(batch).await.caused_by(trc::location!())?; + } + + Ok(HttpResponse::new(StatusCode::OK)) + } + async fn handle_acl_prop_set( &self, access_token: &AccessToken, @@ -342,51 +455,16 @@ impl DavAclHandler for Server { } } - async fn resolve_ace(&self, unresolved_aces: Vec) -> trc::Result> { - let mut aces = Vec::with_capacity(unresolved_aces.len()); - - for ace in unresolved_aces { - let grant_account_name = self - .directory() - .query(QueryBy::Id(ace.account_id), false) - .await - .caused_by(trc::location!())? - .and_then(|mut p| p.take_str(PrincipalField::Name)) - .unwrap_or_else(|| format!("_{}", ace.account_id)); - - aces.push(Ace::new( - Principal::Href(Href(format!( - "{}/{}", - DavResource::Principal.base_path(), - grant_account_name, - ))), - GrantDeny::grant(ace.privileges), - )); - } - - Ok(aces) - } -} - -pub(crate) struct UnresolvedAce { - account_id: u32, - privileges: Vec, -} - -pub(crate) trait Privileges { - fn ace(&self, account_id: u32, grants: &ArchivedVec) -> Vec; - - fn current_privilege_set( + async fn resolve_ace( &self, + access_token: &AccessToken, account_id: u32, grants: &ArchivedVec, - ) -> Vec; -} - -impl Privileges for AccessToken { - fn ace(&self, account_id: u32, grants: &ArchivedVec) -> Vec { + ) -> trc::Result> { let mut aces = Vec::with_capacity(grants.len()); - if self.is_member(account_id) || grants.effective_acl(self).contains(Acl::Administer) { + if access_token.is_member(account_id) + || grants.effective_acl(access_token).contains(Acl::Administer) + { for grant in grants.iter() { let grant_account_id = u32::from(grant.account_id); let mut privileges = Vec::with_capacity(4); @@ -409,15 +487,38 @@ impl Privileges for AccessToken { privileges.push(Privilege::ReadFreeBusy); } - aces.push(UnresolvedAce { - account_id: grant_account_id, - privileges, - }); + let grant_account_name = self + .directory() + .query(QueryBy::Id(grant_account_id), false) + .await + .caused_by(trc::location!())? + .and_then(|mut p| p.take_str(PrincipalField::Name)) + .unwrap_or_else(|| format!("_{grant_account_id}")); + + aces.push(Ace::new( + Principal::Href(Href(format!( + "{}/{}", + DavResource::Principal.base_path(), + grant_account_name, + ))), + GrantDeny::grant(privileges), + )); } } - aces - } + Ok(aces) + } +} + +pub(crate) trait Privileges { + fn current_privilege_set( + &self, + account_id: u32, + grants: &ArchivedVec, + ) -> Vec; +} + +impl Privileges for AccessToken { fn current_privilege_set( &self, account_id: u32, diff --git a/crates/dav/src/common/lock.rs b/crates/dav/src/common/lock.rs index 8174cf83..945c122e 100644 --- a/crates/dav/src/common/lock.rs +++ b/crates/dav/src/common/lock.rs @@ -838,24 +838,24 @@ impl ArchivedLockItem { impl OwnedUri<'_> { pub fn lock_key(&self) -> Vec { - let mut result = Vec::with_capacity(U32_LEN + 2); - result.push(KV_LOCK_DAV); - result.extend_from_slice(self.account_id.to_be_bytes().as_slice()); - result.push(u8::from(self.collection)); - result + build_lock_key(self.account_id, self.collection) } } impl ResourceState<'_> { pub fn lock_key(&self) -> Vec { - let mut result = Vec::with_capacity(U32_LEN + 2); - result.push(KV_LOCK_DAV); - result.extend_from_slice(self.account_id.to_be_bytes().as_slice()); - result.push(u8::from(self.collection)); - result + build_lock_key(self.account_id, self.collection) } } +pub(crate) fn build_lock_key(account_id: u32, collection: Collection) -> Vec { + let mut result = Vec::with_capacity(U32_LEN + 2); + result.push(KV_LOCK_DAV); + result.extend_from_slice(account_id.to_be_bytes().as_slice()); + result.push(u8::from(collection)); + result +} + impl PartialEq for ResourceState<'_> { fn eq(&self, other: &Self) -> bool { self.account_id == other.account_id diff --git a/crates/dav/src/common/mod.rs b/crates/dav/src/common/mod.rs index b9324c3f..93091dd4 100644 --- a/crates/dav/src/common/mod.rs +++ b/crates/dav/src/common/mod.rs @@ -6,12 +6,22 @@ use dav_proto::{ Depth, RequestHeaders, Return, - schema::request::{PropFind, SyncCollection}, + schema::{ + Namespace, + property::{ReportSet, ResourceType}, + request::{ArchivedDeadProperty, MultiGet, PropFind, SyncCollection}, + }, }; -use jmap_proto::types::property::Property; +use groupware::{ + calendar::{ArchivedCalendar, ArchivedCalendarEvent, Calendar, CalendarEvent}, + contact::{AddressBook, ArchivedAddressBook, ArchivedContactCard, ContactCard}, + file::{ArchivedFileNode, FileNode}, +}; +use jmap_proto::types::{collection::Collection, property::Property, value::ArchivedAclGrant}; +use rkyv::vec::ArchivedVec; use store::{ U32_LEN, - write::{Archive, BatchBuilder, Operation, ValueClass, ValueOp}, + write::{AlignedBytes, Archive, BatchBuilder, Operation, ValueClass, ValueOp}, }; use uri::{OwnedUri, Urn}; @@ -20,8 +30,9 @@ pub mod lock; pub mod propfind; pub mod uri; +#[derive(Default)] pub(crate) struct DavQuery<'x> { - pub resource: OwnedUri<'x>, + pub resource: DavQueryResource<'x>, pub base_uri: &'x str, pub propfind: PropFind, pub from_change_id: Option, @@ -31,11 +42,19 @@ pub(crate) struct DavQuery<'x> { pub depth_no_root: bool, } -pub trait ETag { +pub(crate) enum DavQueryResource<'x> { + Uri(OwnedUri<'x>), + Multiget { + parent_collection: Collection, + hrefs: Vec, + }, +} + +pub(crate) trait ETag { fn etag(&self) -> String; } -pub trait ExtractETag { +pub(crate) trait ExtractETag { fn etag(&self) -> Option; } @@ -66,6 +85,20 @@ impl ExtractETag for BatchBuilder { } } +pub(crate) trait DavCollection { + fn namespace(&self) -> Namespace; +} + +impl DavCollection for Collection { + fn namespace(&self) -> Namespace { + match self { + Collection::Calendar | Collection::CalendarEvent => Namespace::CalDav, + Collection::AddressBook | Collection::ContactCard => Namespace::CardDav, + _ => Namespace::Dav, + } + } +} + impl<'x> DavQuery<'x> { pub fn propfind( resource: OwnedUri<'x>, @@ -73,17 +106,38 @@ impl<'x> DavQuery<'x> { headers: RequestHeaders<'x>, ) -> Self { Self { - resource, + resource: DavQueryResource::Uri(resource), propfind, base_uri: headers.base_uri().unwrap_or_default(), - from_change_id: None, depth: match headers.depth { Depth::Zero => 0, _ => 1, }, - limit: None, ret: headers.ret, depth_no_root: headers.depth_no_root, + ..Default::default() + } + } + + pub fn multiget( + multiget: MultiGet, + collection: Collection, + headers: RequestHeaders<'x>, + ) -> Self { + Self { + resource: DavQueryResource::Multiget { + hrefs: multiget.hrefs, + parent_collection: collection, + }, + propfind: multiget.properties, + base_uri: headers.base_uri().unwrap_or_default(), + depth: match headers.depth { + Depth::Zero => 0, + _ => 1, + }, + ret: headers.ret, + depth_no_root: headers.depth_no_root, + ..Default::default() } } @@ -93,7 +147,7 @@ impl<'x> DavQuery<'x> { headers: RequestHeaders<'x>, ) -> Self { Self { - resource, + resource: DavQueryResource::Uri(resource), propfind: changes.properties, base_uri: headers.base_uri().unwrap_or_default(), from_change_id: changes @@ -118,3 +172,170 @@ impl<'x> DavQuery<'x> { self.ret == Return::Minimal } } + +impl Default for DavQueryResource<'_> { + fn default() -> Self { + Self::Multiget { + parent_collection: Collection::None, + hrefs: Vec::new(), + } + } +} + +pub(crate) enum ArchivedResource<'x> { + Calendar(Archive<&'x ArchivedCalendar>), + CalendarEvent(Archive<&'x ArchivedCalendarEvent>), + AddressBook(Archive<&'x ArchivedAddressBook>), + ContactCard(Archive<&'x ArchivedContactCard>), + FileNode(Archive<&'x ArchivedFileNode>), +} + +impl<'x> ArchivedResource<'x> { + pub fn from_archive( + archive: &'x Archive, + collection: Collection, + ) -> trc::Result { + match collection { + Collection::Calendar => archive + .to_unarchived::() + .map(ArchivedResource::Calendar), + Collection::CalendarEvent => archive + .to_unarchived::() + .map(ArchivedResource::CalendarEvent), + Collection::AddressBook => archive + .to_unarchived::() + .map(ArchivedResource::AddressBook), + Collection::FileNode => archive + .to_unarchived::() + .map(ArchivedResource::FileNode), + Collection::ContactCard => archive + .to_unarchived::() + .map(ArchivedResource::ContactCard), + _ => unreachable!(), + } + } + + pub fn acls(&self) -> Option<&ArchivedVec> { + match self { + Self::Calendar(archive) => Some(&archive.inner.acls), + Self::AddressBook(archive) => Some(&archive.inner.acls), + Self::FileNode(archive) => Some(&archive.inner.acls), + _ => None, + } + } + + pub fn created(&self) -> i64 { + match self { + ArchivedResource::Calendar(archive) => archive.inner.created, + ArchivedResource::CalendarEvent(archive) => archive.inner.created, + ArchivedResource::AddressBook(archive) => archive.inner.created, + ArchivedResource::ContactCard(archive) => archive.inner.created, + ArchivedResource::FileNode(archive) => archive.inner.created, + } + .to_native() + } + + pub fn modified(&self) -> i64 { + match self { + ArchivedResource::Calendar(archive) => archive.inner.modified, + ArchivedResource::CalendarEvent(archive) => archive.inner.modified, + ArchivedResource::AddressBook(archive) => archive.inner.modified, + ArchivedResource::ContactCard(archive) => archive.inner.modified, + ArchivedResource::FileNode(archive) => archive.inner.modified, + } + .to_native() + } + + pub fn dead_properties(&self) -> &ArchivedDeadProperty { + match self { + ArchivedResource::Calendar(archive) => &archive.inner.dead_properties, + ArchivedResource::CalendarEvent(archive) => &archive.inner.dead_properties, + ArchivedResource::AddressBook(archive) => &archive.inner.dead_properties, + ArchivedResource::ContactCard(archive) => &archive.inner.dead_properties, + ArchivedResource::FileNode(archive) => &archive.inner.dead_properties, + } + } + + pub fn content_length(&self) -> Option { + match self { + ArchivedResource::FileNode(archive) => { + archive.inner.file.as_ref().map(|f| f.size.to_native()) + } + ArchivedResource::CalendarEvent(archive) => archive.inner.size.to_native().into(), + ArchivedResource::ContactCard(archive) => archive.inner.size.to_native().into(), + ArchivedResource::AddressBook(_) | ArchivedResource::Calendar(_) => None, + } + } + + pub fn content_type(&self) -> Option<&str> { + match self { + ArchivedResource::FileNode(archive) => archive + .inner + .file + .as_ref() + .and_then(|f| f.media_type.as_deref()), + ArchivedResource::CalendarEvent(_) => "text/calendar".into(), + ArchivedResource::ContactCard(_) => "text/vcard".into(), + ArchivedResource::AddressBook(_) | ArchivedResource::Calendar(_) => None, + } + } + + pub fn display_name(&self, account_id: u32) -> Option<&str> { + match self { + ArchivedResource::Calendar(archive) => archive + .inner + .preferences(account_id) + .map(|p| p.name.as_str()), + ArchivedResource::CalendarEvent(archive) => archive.inner.display_name.as_deref(), + ArchivedResource::AddressBook(archive) => archive.inner.display_name.as_deref(), + ArchivedResource::ContactCard(archive) => archive.inner.display_name.as_deref(), + ArchivedResource::FileNode(archive) => archive.inner.display_name.as_deref(), + } + } + + pub fn supported_report_set(&self) -> Option> { + match self { + ArchivedResource::Calendar(_) => vec![ + ReportSet::SyncCollection, + ReportSet::AclPrincipalPropSet, + ReportSet::PrincipalMatch, + ReportSet::ExpandProperty, + ReportSet::CalendarQuery, + ReportSet::CalendarMultiGet, + ReportSet::FreeBusyQuery, + ] + .into(), + ArchivedResource::AddressBook(_) => vec![ + ReportSet::SyncCollection, + ReportSet::AclPrincipalPropSet, + ReportSet::PrincipalMatch, + ReportSet::ExpandProperty, + ReportSet::AddressbookQuery, + ReportSet::AddressbookMultiGet, + ] + .into(), + ArchivedResource::FileNode(archive) if archive.inner.file.is_none() => vec![ + ReportSet::SyncCollection, + ReportSet::AclPrincipalPropSet, + ReportSet::PrincipalMatch, + ] + .into(), + _ => None, + } + } + + pub fn resource_type(&self) -> Option> { + match self { + ArchivedResource::Calendar(_) => { + vec![ResourceType::Collection, ResourceType::Calendar].into() + } + ArchivedResource::AddressBook(_) => { + vec![ResourceType::Collection, ResourceType::AddressBook].into() + } + ArchivedResource::FileNode(archive) if archive.inner.file.is_none() => { + vec![ResourceType::Collection].into() + } + _ => None, + } + } +} diff --git a/crates/dav/src/common/propfind.rs b/crates/dav/src/common/propfind.rs index b1833c14..5cb48922 100644 --- a/crates/dav/src/common/propfind.rs +++ b/crates/dav/src/common/propfind.rs @@ -4,37 +4,58 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use std::sync::Arc; + +use calcard::vcard::{VCard, VCardEntry}; use common::{ - Server, + DavResource, DavResources, Server, auth::{AccessToken, AsTenantId}, }; use dav_proto::{ Depth, RequestHeaders, schema::{ - property::{DavProperty, ResourceType, WebDavProperty}, + Collation, + property::{ + ActiveLock, CardDavProperty, DavProperty, DavValue, Privilege, ResourceType, + Rfc1123DateTime, SupportedCollation, SupportedLock, WebDavProperty, + }, request::{DavPropertyValue, PropFind}, - response::{BaseCondition, MultiStatus, PropStat, Response}, + response::{ + AclRestrictions, BaseCondition, Href, List, MultiStatus, PropStat, Response, + SupportedPrivilege, + }, }, }; use directory::{ Type, backend::internal::{PrincipalField, manage::ManageDirectory}, }; +use groupware::hierarchy::DavHierarchy; use http_proto::HttpResponse; use hyper::StatusCode; -use jmap_proto::types::collection::Collection; -use store::roaring::RoaringBitmap; +use jmap_proto::types::{acl::Acl, collection::Collection}; +use store::{ + ahash::AHashMap, + query::log::Query, + roaring::RoaringBitmap, + write::{AlignedBytes, Archive, serialize::rkyv_deserialize}, +}; use trc::AddContext; use crate::{ - DavErrorCondition, - card::propfind::CardPropFindRequestHandler, - common::uri::DavUriResource, - file::propfind::HandleFilePropFindRequest, + DavError, DavErrorCondition, + card::{CARD_ALL_PROPS, CARD_CONTAINER_PROPS, CARD_ITEM_PROPS}, + common::{DavQueryResource, uri::DavUriResource}, + file::{FILE_ALL_PROPS, FILE_CONTAINER_PROPS, FILE_ITEM_PROPS}, principal::{CurrentUserPrincipal, propfind::PrincipalPropFind}, }; -use super::{DavQuery, uri::UriResource}; +use super::{ + ArchivedResource, DavCollection, DavQuery, ETag, + acl::{DavAclHandler, Privileges}, + lock::{LockData, build_lock_key}, + uri::{UriResource, Urn}, +}; pub(crate) trait PropFindRequestHandler: Sync + Send { fn handle_propfind_request( @@ -44,11 +65,43 @@ pub(crate) trait PropFindRequestHandler: Sync + Send { request: PropFind, ) -> impl Future> + Send; + fn handle_dav_query( + &self, + access_token: &AccessToken, + query: DavQuery<'_>, + ) -> impl Future> + Send; + fn dav_quota( &self, access_token: &AccessToken, account_id: u32, - ) -> impl Future> + Send; + ) -> impl Future> + Send; +} + +pub(crate) struct PropFindData { + pub accounts: AHashMap, +} + +#[derive(Default)] +pub(crate) struct PropFindAccountData { + pub sync_token: Option, + pub quota: Option, + pub owner: Option, + pub locks: Option>, + pub locks_not_found: bool, +} + +#[derive(Clone, Default)] +pub(crate) struct PropFindAccountQuota { + pub used: u64, + pub available: u64, +} + +pub(crate) struct PropFindItem { + pub name: String, + pub account_id: u32, + pub document_id: u32, + pub is_container: bool, } impl PropFindRequestHandler for Server { @@ -82,24 +135,8 @@ impl PropFindRequestHandler for Server { // List shared resources if let Some(account_id) = resource.account_id { match resource.collection { - Collection::FileNode => { - self.handle_file_propfind_request( - access_token, - DavQuery::propfind( - UriResource::new_owned( - resource.collection, - account_id, - resource.resource, - ), - request, - headers, - ), - ) - .await - } - Collection::Calendar => todo!(), - Collection::AddressBook => { - self.handle_card_propfind_request( + Collection::FileNode | Collection::Calendar | Collection::AddressBook => { + self.handle_dav_query( access_token, DavQuery::propfind( UriResource::new_owned( @@ -213,11 +250,690 @@ impl PropFindRequestHandler for Server { } } + async fn handle_dav_query( + &self, + access_token: &AccessToken, + query: DavQuery<'_>, + ) -> crate::Result { + let mut response = MultiStatus::new(Vec::with_capacity(16)); + let mut data = PropFindData::new(); + let collection_container; + let collection_children; + let mut paths; + + match &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) + .await + .caused_by(trc::location!())?; + response.set_namespace(collection_container.namespace()); + + // Obtain document ids + let mut document_ids = if !access_token.is_member(account_id) { + self.shared_containers( + access_token, + account_id, + collection_container, + Acl::ReadItems, + ) + .await + .caused_by(trc::location!())? + .into() + } else { + None + }; + + // Filter by changelog + if let Some(change_id) = query.from_change_id { + let changelog = self + .store() + .changes(account_id, collection_children, Query::Since(change_id)) + .await + .caused_by(trc::location!())?; + let limit = std::cmp::min( + query.limit.unwrap_or(u32::MAX) as usize, + self.core.dav.max_changes, + ); + + // Set sync token + let sync_token = if changelog.to_change_id != 0 { + let sync_token = Urn::Sync(changelog.to_change_id).to_string(); + data.accounts.entry(account_id).or_default().sync_token = + sync_token.clone().into(); + sync_token + } else { + let id = self + .store() + .get_last_change_id(account_id, collection_children) + .await + .caused_by(trc::location!())? + .unwrap_or_default(); + Urn::Sync(id).to_string() + }; + response.set_sync_token(sync_token); + + let mut changes = RoaringBitmap::from_iter( + changelog.changes.iter().map(|change| change.id() as u32), + ); + if changes.len() as usize > limit { + changes = RoaringBitmap::from_sorted_iter(changes.into_iter().take(limit)) + .unwrap(); + } + if let Some(document_ids) = &mut document_ids { + *document_ids &= changes; + } else { + document_ids = Some(changes); + } + } + + paths = if let Some(resource) = resource.resource { + resources + .subtree_with_depth(resource, query.depth) + .filter(|item| { + document_ids + .as_ref() + .is_none_or(|d| d.contains(item.document_id)) + }) + .map(|item| PropFindItem::new(&query, account_id, item)) + .collect::>() + } else { + if !query.depth_no_root || query.from_change_id.is_none() { + self.prepare_principal_propfind_response( + access_token, + collection_container, + [account_id].into_iter(), + &query.propfind, + &mut response, + ) + .await?; + + if query.depth == 0 { + return Ok(HttpResponse::new(StatusCode::MULTI_STATUS) + .with_xml_body(response.to_string())); + } + } + resources + .tree_with_depth(query.depth - 1) + .filter(|item| { + document_ids + .as_ref() + .is_none_or(|d| d.contains(item.document_id)) + }) + .map(|item| PropFindItem::new(&query, 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, + )); + + return Ok(HttpResponse::new(StatusCode::MULTI_STATUS) + .with_xml_body(response.to_string())); + } + } + DavQueryResource::Multiget { + hrefs, + parent_collection, + } => { + paths = Vec::with_capacity(hrefs.len()); + let mut resources_by_account: AHashMap< + u32, + (Arc, Arc>), + > = AHashMap::with_capacity(3); + collection_container = *parent_collection; + collection_children = collection_container.child_collection().unwrap(); + response.set_namespace(collection_container.namespace()); + + for item in hrefs { + let resource = match self + .validate_uri(access_token, item) + .await + .and_then(|r| r.into_owned_uri()) + { + Ok(resource) => resource, + Err(DavError::Code(code)) => { + response.add_response(Response::new_status([item], code)); + continue; + } + Err(err) => { + return Err(err); + } + }; + + let account_id = resource.account_id; + let (resources, document_ids) = + if let Some(resources) = resources_by_account.get(&account_id) { + resources.clone() + } else { + let resources = self + .fetch_dav_resources(account_id, collection_container) + .await + .caused_by(trc::location!())?; + let document_ids = Arc::new(if !access_token.is_member(account_id) { + self.shared_containers( + access_token, + account_id, + collection_container, + Acl::ReadItems, + ) + .await + .caused_by(trc::location!())? + .into() + } else { + None + }); + resources_by_account + .insert(account_id, (resources.clone(), document_ids.clone())); + (resources, document_ids) + }; + + if let Some(resource) = resource + .resource + .and_then(|name| resources.paths.by_name(name)) + { + if !resource.is_container { + if document_ids + .as_ref() + .as_ref() + .is_none_or(|docs| docs.contains(resource.document_id)) + { + paths.push(PropFindItem::new(&query, account_id, resource)); + } else { + response.add_response( + Response::new_status([item], StatusCode::FORBIDDEN) + .with_response_description( + "Not enough permissions to access this shared resource", + ), + ); + } + } else { + response.add_response( + Response::new_status([item], StatusCode::FORBIDDEN) + .with_response_description( + "Multiget not allowed for collections", + ), + ); + } + } else { + response.add_response(Response::new_status([item], StatusCode::NOT_FOUND)); + } + } + } + } + + if query.depth == usize::MAX && paths.len() > self.core.dav.max_match_results { + return Err(DavError::Condition(DavErrorCondition::new( + StatusCode::PRECONDITION_FAILED, + BaseCondition::NumberOfMatchesWithinLimit, + ))); + } + + let mut is_all_prop = false; + let todo = "prop lists"; + let properties = match &query.propfind { + PropFind::PropName => { + let (container_props, children_props) = match collection_container { + Collection::FileNode => { + (FILE_CONTAINER_PROPS.as_slice(), FILE_ITEM_PROPS.as_slice()) + } + Collection::Calendar => { + (FILE_CONTAINER_PROPS.as_slice(), FILE_ITEM_PROPS.as_slice()) + } + Collection::AddressBook => { + (CARD_CONTAINER_PROPS.as_slice(), CARD_ITEM_PROPS.as_slice()) + } + _ => unreachable!(), + }; + + for item in paths { + let props = if item.is_container { + container_props + .iter() + .cloned() + .map(DavPropertyValue::empty) + .collect::>() + } else { + children_props + .iter() + .cloned() + .map(DavPropertyValue::empty) + .collect::>() + }; + + response.add_response(Response::new_propstat( + item.name, + vec![PropStat::new_list(props)], + )); + } + + return Ok( + HttpResponse::new(StatusCode::MULTI_STATUS).with_xml_body(response.to_string()) + ); + } + PropFind::AllProp(items) => { + is_all_prop = true; + let all_props = match collection_container { + Collection::FileNode => FILE_ALL_PROPS.as_slice(), + Collection::Calendar => FILE_ALL_PROPS.as_slice(), + Collection::AddressBook => CARD_ALL_PROPS.as_slice(), + _ => unreachable!(), + }; + + let mut result = Vec::with_capacity(items.len() + all_props.len()); + result.extend_from_slice(all_props); + result.extend(items.iter().filter(|field| !field.is_all_prop()).cloned()); + result + } + PropFind::Prop(items) => items.clone(), + }; + + let view_as_id = access_token.primary_id(); + for item in paths { + let account_id = item.account_id; + let document_id = item.document_id; + let collection = if item.is_container { + collection_container + } else { + collection_children + }; + let archive_ = if let Some(archive_) = self + .get_archive(account_id, collection, document_id) + .await + .caused_by(trc::location!())? + { + archive_ + } else { + response.add_response(Response::new_status([item.name], StatusCode::NOT_FOUND)); + continue; + }; + let archive = ArchivedResource::from_archive(&archive_, collection) + .caused_by(trc::location!())?; + let dead_properties = archive.dead_properties(); + + // Fill properties + let mut fields = Vec::with_capacity(properties.len()); + let mut fields_not_found = Vec::new(); + for property in &properties { + match property { + DavProperty::WebDav(dav_property) => match dav_property { + WebDavProperty::CreationDate => { + fields.push(DavPropertyValue::new( + property.clone(), + DavValue::Timestamp(archive.created()), + )); + } + WebDavProperty::DisplayName => { + if let Some(name) = archive.display_name(view_as_id) { + fields.push(DavPropertyValue::new( + property.clone(), + DavValue::String(name.to_string()), + )); + } else if !is_all_prop { + fields_not_found.push(DavPropertyValue::empty(property.clone())); + } + } + WebDavProperty::GetContentLanguage => { + if !is_all_prop { + fields_not_found.push(DavPropertyValue::empty(property.clone())); + } + } + WebDavProperty::GetContentLength => { + if let Some(value) = archive.content_length() { + fields.push(DavPropertyValue::new( + property.clone(), + DavValue::Uint64(value as u64), + )); + } else if !is_all_prop { + fields_not_found.push(DavPropertyValue::empty(property.clone())); + } + } + WebDavProperty::GetContentType => { + if let Some(value) = archive.content_type() { + fields.push(DavPropertyValue::new( + property.clone(), + DavValue::String(value.to_string()), + )); + } else if !is_all_prop { + fields_not_found.push(DavPropertyValue::empty(property.clone())); + } + } + WebDavProperty::GetETag => { + fields.push(DavPropertyValue::new( + property.clone(), + DavValue::String(archive_.etag()), + )); + } + WebDavProperty::GetLastModified => { + fields.push(DavPropertyValue::new( + property.clone(), + DavValue::Rfc1123Date(Rfc1123DateTime::new(archive.modified())), + )); + } + WebDavProperty::ResourceType => { + if let Some(resource_type) = archive.resource_type() { + fields.push(DavPropertyValue::new(property.clone(), resource_type)); + } else { + fields.push(DavPropertyValue::empty(property.clone())); + } + } + WebDavProperty::LockDiscovery => { + if let Some(locks) = data + .locks(self, account_id, collection_container, &query, &item) + .await + .caused_by(trc::location!())? + { + fields.push(DavPropertyValue::new(property.clone(), locks)); + } else { + fields.push(DavPropertyValue::empty(property.clone())); + } + } + WebDavProperty::SupportedLock => { + fields.push(DavPropertyValue::new( + property.clone(), + SupportedLock::default(), + )); + } + WebDavProperty::SupportedReportSet => { + if let Some(report_set) = archive.supported_report_set() { + fields.push(DavPropertyValue::new(property.clone(), report_set)); + } else if !is_all_prop { + fields_not_found.push(DavPropertyValue::empty(property.clone())); + } + } + WebDavProperty::SyncToken => { + fields.push(DavPropertyValue::new( + property.clone(), + data.sync_token(self, account_id, collection_children) + .await + .caused_by(trc::location!())?, + )); + } + WebDavProperty::CurrentUserPrincipal => { + fields.push(DavPropertyValue::new( + property.clone(), + vec![access_token.current_user_principal()], + )); + } + WebDavProperty::QuotaAvailableBytes => { + if item.is_container { + fields.push(DavPropertyValue::new( + property.clone(), + data.quota(self, access_token, account_id) + .await + .caused_by(trc::location!())? + .available, + )); + } else if !is_all_prop { + fields_not_found.push(DavPropertyValue::empty(property.clone())); + } + } + WebDavProperty::QuotaUsedBytes => { + if item.is_container { + fields.push(DavPropertyValue::new( + property.clone(), + data.quota(self, access_token, account_id) + .await + .caused_by(trc::location!())? + .used, + )); + } else if !is_all_prop { + fields_not_found.push(DavPropertyValue::empty(property.clone())); + } + } + WebDavProperty::Owner => { + fields.push(DavPropertyValue::new( + property.clone(), + vec![ + data.owner(self, access_token, account_id) + .await + .caused_by(trc::location!())?, + ], + )); + } + WebDavProperty::Group => { + fields.push(DavPropertyValue::empty(property.clone())); + } + WebDavProperty::SupportedPrivilegeSet => { + fields.push(DavPropertyValue::new( + property.clone(), + vec![ + SupportedPrivilege::new(Privilege::All, "Any operation") + .with_abstract() + .with_supported_privilege( + SupportedPrivilege::new( + Privilege::Read, + "Read objects", + ) + .with_supported_privilege(SupportedPrivilege::new( + Privilege::ReadCurrentUserPrivilegeSet, + "Read current user privileges", + )), + ) + .with_supported_privilege( + SupportedPrivilege::new( + Privilege::Write, + "Write objects", + ) + .with_supported_privilege(SupportedPrivilege::new( + Privilege::WriteProperties, + "Write properties", + )) + .with_supported_privilege(SupportedPrivilege::new( + Privilege::WriteContent, + "Write object contents", + )) + .with_supported_privilege(SupportedPrivilege::new( + Privilege::Bind, + "Add resources to a collection", + )) + .with_supported_privilege(SupportedPrivilege::new( + Privilege::Unbind, + "Add resources to a collection", + )) + .with_supported_privilege(SupportedPrivilege::new( + Privilege::Unlock, + "Unlock resources", + )), + ) + .with_supported_privilege(SupportedPrivilege::new( + Privilege::ReadAcl, + "Read ACL", + )) + .with_supported_privilege(SupportedPrivilege::new( + Privilege::WriteAcl, + "Write ACL", + )) + .with_opt_supported_privilege( + (collection_container == Collection::Calendar).then( + || { + SupportedPrivilege::new( + Privilege::ReadFreeBusy, + "Read free/busy information", + ) + }, + ), + ), + ], + )); + } + WebDavProperty::CurrentUserPrivilegeSet => { + if let Some(acls) = archive.acls() { + fields.push(DavPropertyValue::new( + property.clone(), + access_token.current_privilege_set(account_id, acls), + )); + } else if !is_all_prop { + fields_not_found.push(DavPropertyValue::empty(property.clone())); + } + } + WebDavProperty::Acl => { + if let Some(acls) = archive.acls() { + let aces = self + .resolve_ace(access_token, account_id, acls) + .await + .caused_by(trc::location!())?; + + fields.push(DavPropertyValue::new(property.clone(), aces)); + } else if !is_all_prop { + fields_not_found.push(DavPropertyValue::empty(property.clone())); + } + } + WebDavProperty::AclRestrictions => { + fields.push(DavPropertyValue::new( + property.clone(), + AclRestrictions::default() + .with_no_invert() + .with_grant_only(), + )); + } + WebDavProperty::InheritedAclSet => { + fields.push(DavPropertyValue::empty(property.clone())); + } + WebDavProperty::PrincipalCollectionSet => { + fields.push(DavPropertyValue::new( + property.clone(), + vec![Href(crate::DavResource::Principal.base_path().to_string())], + )); + } + }, + DavProperty::DeadProperty(tag) => { + if let Some(value) = dead_properties.find_tag(&tag.name) { + fields.push(DavPropertyValue::new(property.clone(), value)); + } else { + fields_not_found.push(DavPropertyValue::empty(property.clone())); + } + } + DavProperty::CardDav(card_property) => match (card_property, &archive) { + ( + CardDavProperty::AddressbookDescription, + ArchivedResource::AddressBook(book), + ) if book.inner.display_name.is_some() => { + fields.push(DavPropertyValue::new( + property.clone(), + book.inner.display_name.as_ref().unwrap().to_string(), + )); + } + ( + CardDavProperty::SupportedAddressData, + ArchivedResource::AddressBook(_), + ) => { + fields.push(DavPropertyValue::new( + property.clone(), + DavValue::SupportedAddressData, + )); + } + ( + CardDavProperty::SupportedCollationSet, + ArchivedResource::AddressBook(_), + ) => { + fields.push(DavPropertyValue::new( + property.clone(), + DavValue::Collations(List(vec![ + SupportedCollation(Collation::AsciiCasemap), + SupportedCollation(Collation::UnicodeCasemap), + SupportedCollation(Collation::Octet), + ])), + )); + } + (CardDavProperty::MaxResourceSize, ArchivedResource::AddressBook(_)) => { + fields.push(DavPropertyValue::new( + property.clone(), + self.core.dav.max_vcard_size as u64, + )); + } + ( + CardDavProperty::AddressData(items), + ArchivedResource::ContactCard(card), + ) => { + let mut vcard; + if !items.is_empty() { + vcard = VCard { + entries: Vec::with_capacity(items.len()), + }; + for item in items { + for entry in card.inner.card.entries.iter() { + if entry.name == item.name && entry.group == item.group { + if !item.no_value { + vcard.entries.push( + rkyv_deserialize(entry) + .caused_by(trc::location!())?, + ); + } else { + vcard.entries.push(VCardEntry { + group: item.group.clone(), + name: item.name.clone(), + params: vec![], + values: vec![], + }); + } + break; + } + } + } + fields.push(DavPropertyValue::new( + property.clone(), + rkyv_deserialize(&card.inner.card) + .caused_by(trc::location!())?, + )); + } else { + vcard = rkyv_deserialize(&card.inner.card) + .caused_by(trc::location!())? + } + + fields.push(DavPropertyValue::new(property.clone(), vcard)); + } + _ => { + if !is_all_prop { + fields_not_found.push(DavPropertyValue::empty(property.clone())); + } + } + }, + DavProperty::CalDav(cal_property) => { + todo!() + } + + property => { + if !is_all_prop { + fields_not_found.push(DavPropertyValue::empty(property.clone())); + } + } + } + } + + // Add dead properties + if is_all_prop && !dead_properties.0.is_empty() { + dead_properties.to_dav_values(&mut fields); + } + + // Add response + 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() && !query.is_minimal() { + prop_stat + .push(PropStat::new_list(fields_not_found).with_status(StatusCode::NOT_FOUND)); + } + if prop_stat.is_empty() { + prop_stat.push(PropStat::new_list(vec![])); + } + response.add_response(Response::new_propstat(item.name, prop_stat)); + } + + Ok(HttpResponse::new(StatusCode::MULTI_STATUS).with_xml_body(response.to_string())) + } + async fn dav_quota( &self, access_token: &AccessToken, account_id: u32, - ) -> trc::Result<(u64, u64)> { + ) -> trc::Result { let resource_token = self .get_resource_token(access_token, account_id) .await @@ -229,12 +945,125 @@ impl PropFindRequestHandler for Server { } else { u64::MAX }; - let quota_used = self + let used = self .get_used_quota(account_id) .await .caused_by(trc::location!())? as u64; - let quota_available = quota.saturating_sub(quota_used); - Ok((quota_used, quota_available)) + Ok(PropFindAccountQuota { + used, + available: quota.saturating_sub(used), + }) + } +} + +impl PropFindItem { + pub fn new(query: &DavQuery<'_>, account_id: u32, resource: &DavResource) -> Self { + Self { + name: query.format_to_base_uri(&resource.name), + account_id, + document_id: resource.document_id, + is_container: resource.is_container, + } + } +} + +impl PropFindData { + pub fn new() -> Self { + Self { + accounts: AHashMap::with_capacity(2), + } + } + + pub async fn quota( + &mut self, + server: &Server, + access_token: &AccessToken, + account_id: u32, + ) -> trc::Result { + let data = self.accounts.entry(account_id).or_default(); + + if data.quota.is_none() { + data.quota = server.dav_quota(access_token, account_id).await?.into(); + } + + Ok(data.quota.clone().unwrap()) + } + + pub async fn owner( + &mut self, + server: &Server, + access_token: &AccessToken, + account_id: u32, + ) -> trc::Result { + let data = self.accounts.entry(account_id).or_default(); + + if data.owner.is_none() { + data.owner = server + .owner_href(access_token, account_id) + .await + .caused_by(trc::location!())? + .into(); + } + + Ok(data.owner.clone().unwrap()) + } + + pub async fn sync_token( + &mut self, + server: &Server, + account_id: u32, + collection_children: Collection, + ) -> trc::Result { + let data = self.accounts.entry(account_id).or_default(); + + if data.sync_token.is_none() { + let id = server + .store() + .get_last_change_id(account_id, collection_children) + .await + .caused_by(trc::location!())? + .unwrap_or_default(); + data.sync_token = Urn::Sync(id).to_string().into(); + } + + Ok(data.sync_token.clone().unwrap()) + } + + pub async fn locks( + &mut self, + server: &Server, + account_id: u32, + collection_container: Collection, + query: &DavQuery<'_>, + item: &PropFindItem, + ) -> trc::Result>> { + let data = self.accounts.entry(account_id).or_default(); + + if data.locks.is_none() && !data.locks_not_found { + data.locks = server + .in_memory_store() + .key_get::>( + build_lock_key(account_id, collection_container).as_slice(), + ) + .await + .caused_by(trc::location!())?; + if data.locks.is_none() { + data.locks_not_found = true; + } + } + + if let Some(lock_data) = &data.locks { + lock_data.unarchive::().map(|locks| { + locks + .find_locks(&item.name.strip_prefix(query.base_uri).unwrap()[1..], false) + .iter() + .map(|(path, lock)| lock.to_active_lock(query.format_to_base_uri(path))) + .collect::>() + .into() + }) + } else { + Ok(None) + } } } diff --git a/crates/dav/src/common/uri.rs b/crates/dav/src/common/uri.rs index d996f94f..cde510d8 100644 --- a/crates/dav/src/common/uri.rs +++ b/crates/dav/src/common/uri.rs @@ -17,6 +17,7 @@ use trc::AddContext; use crate::{DavError, DavResource}; +#[derive(Debug)] pub(crate) struct UriResource { pub collection: Collection, pub account_id: A, diff --git a/crates/dav/src/file/acl.rs b/crates/dav/src/file/acl.rs deleted file mode 100644 index 3c735294..00000000 --- a/crates/dav/src/file/acl.rs +++ /dev/null @@ -1,101 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use common::{Server, auth::AccessToken, sharing::EffectiveAcl}; -use dav_proto::RequestHeaders; -use groupware::{file::FileNode, 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::{ - DavError, - common::{acl::DavAclHandler, uri::DavUriResource}, - file::{DavFileResource, update_file_node}, -}; - -pub(crate) trait FileAclRequestHandler: Sync + Send { - fn handle_file_acl_request( - &self, - access_token: &AccessToken, - headers: RequestHeaders<'_>, - request: dav_proto::schema::request::Acl, - ) -> impl Future> + Send; -} - -impl FileAclRequestHandler for Server { - async fn handle_file_acl_request( - &self, - access_token: &AccessToken, - headers: RequestHeaders<'_>, - request: dav_proto::schema::request::Acl, - ) -> crate::Result { - // Validate URI - let resource_ = self - .validate_uri(access_token, headers.uri) - .await? - .into_owned_uri()?; - let account_id = resource_.account_id; - let files = self - .fetch_dav_resources(account_id, Collection::FileNode) - .await - .caused_by(trc::location!())?; - let resource = files.map_resource(&resource_)?; - - // Fetch node - let node_ = self - .get_archive(account_id, Collection::FileNode, resource.resource) - .await - .caused_by(trc::location!())? - .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; - let node = node_ - .to_unarchived::() - .caused_by(trc::location!())?; - - // Validate ACL - if !access_token.is_member(account_id) - && !node - .inner - .acls - .effective_acl(access_token) - .contains(Acl::Administer) - { - return Err(DavError::Code(StatusCode::FORBIDDEN)); - } - - let grants = self - .validate_and_map_aces(access_token, request, Collection::FileNode) - .await?; - - if grants.len() != node.inner.acls.len() - || node - .inner - .acls - .iter() - .zip(grants.iter()) - .any(|(a, b)| a != b) - { - let mut new_node = node.deserialize().caused_by(trc::location!())?; - new_node.acls = grants; - let mut batch = BatchBuilder::new(); - update_file_node( - access_token, - node, - new_node, - account_id, - resource.resource, - false, - &mut batch, - ) - .caused_by(trc::location!())?; - self.commit_batch(batch).await.caused_by(trc::location!())?; - } - - Ok(HttpResponse::new(StatusCode::OK)) - } -} diff --git a/crates/dav/src/file/mod.rs b/crates/dav/src/file/mod.rs index 04fc688b..cb6b335e 100644 --- a/crates/dav/src/file/mod.rs +++ b/crates/dav/src/file/mod.rs @@ -5,6 +5,7 @@ */ use common::{DavResource, DavResources, auth::AccessToken, storage::index::ObjectIndexBuilder}; +use dav_proto::schema::property::{DavProperty, WebDavProperty}; use groupware::file::{ArchivedFileNode, FileNode}; use hyper::StatusCode; use jmap_proto::types::collection::Collection; @@ -18,15 +19,77 @@ use crate::{ }, }; -pub mod acl; pub mod copy_move; pub mod delete; pub mod get; pub mod mkcol; -pub mod propfind; pub mod proppatch; pub mod update; +pub(crate) static FILE_CONTAINER_PROPS: [DavProperty; 19] = [ + DavProperty::WebDav(WebDavProperty::CreationDate), + DavProperty::WebDav(WebDavProperty::DisplayName), + DavProperty::WebDav(WebDavProperty::GetETag), + DavProperty::WebDav(WebDavProperty::GetLastModified), + DavProperty::WebDav(WebDavProperty::ResourceType), + DavProperty::WebDav(WebDavProperty::LockDiscovery), + DavProperty::WebDav(WebDavProperty::SupportedLock), + DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal), + DavProperty::WebDav(WebDavProperty::SyncToken), + DavProperty::WebDav(WebDavProperty::Owner), + DavProperty::WebDav(WebDavProperty::SupportedPrivilegeSet), + DavProperty::WebDav(WebDavProperty::CurrentUserPrivilegeSet), + DavProperty::WebDav(WebDavProperty::Acl), + DavProperty::WebDav(WebDavProperty::AclRestrictions), + DavProperty::WebDav(WebDavProperty::InheritedAclSet), + DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet), + DavProperty::WebDav(WebDavProperty::SupportedReportSet), + DavProperty::WebDav(WebDavProperty::QuotaAvailableBytes), + DavProperty::WebDav(WebDavProperty::QuotaUsedBytes), +]; + +pub(crate) static FILE_ITEM_PROPS: [DavProperty; 19] = [ + DavProperty::WebDav(WebDavProperty::CreationDate), + DavProperty::WebDav(WebDavProperty::DisplayName), + DavProperty::WebDav(WebDavProperty::GetETag), + DavProperty::WebDav(WebDavProperty::GetLastModified), + DavProperty::WebDav(WebDavProperty::ResourceType), + DavProperty::WebDav(WebDavProperty::LockDiscovery), + DavProperty::WebDav(WebDavProperty::SupportedLock), + DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal), + DavProperty::WebDav(WebDavProperty::SyncToken), + DavProperty::WebDav(WebDavProperty::Owner), + DavProperty::WebDav(WebDavProperty::SupportedPrivilegeSet), + DavProperty::WebDav(WebDavProperty::CurrentUserPrivilegeSet), + DavProperty::WebDav(WebDavProperty::Acl), + DavProperty::WebDav(WebDavProperty::AclRestrictions), + DavProperty::WebDav(WebDavProperty::InheritedAclSet), + DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet), + DavProperty::WebDav(WebDavProperty::GetContentLanguage), + DavProperty::WebDav(WebDavProperty::GetContentLength), + DavProperty::WebDav(WebDavProperty::GetContentType), +]; + +pub(crate) static FILE_ALL_PROPS: [DavProperty; 17] = [ + DavProperty::WebDav(WebDavProperty::CreationDate), + DavProperty::WebDav(WebDavProperty::DisplayName), + DavProperty::WebDav(WebDavProperty::GetETag), + DavProperty::WebDav(WebDavProperty::GetLastModified), + DavProperty::WebDav(WebDavProperty::ResourceType), + DavProperty::WebDav(WebDavProperty::LockDiscovery), + DavProperty::WebDav(WebDavProperty::SupportedLock), + DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal), + DavProperty::WebDav(WebDavProperty::SyncToken), + DavProperty::WebDav(WebDavProperty::SupportedPrivilegeSet), + DavProperty::WebDav(WebDavProperty::AclRestrictions), + DavProperty::WebDav(WebDavProperty::CurrentUserPrivilegeSet), + DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet), + DavProperty::WebDav(WebDavProperty::GetContentLanguage), + DavProperty::WebDav(WebDavProperty::GetContentLength), + DavProperty::WebDav(WebDavProperty::GetContentType), + DavProperty::WebDav(WebDavProperty::SupportedReportSet), +]; + pub(crate) trait FromDavResource { fn from_dav_resource(item: &DavResource) -> Self; } diff --git a/crates/dav/src/file/propfind.rs b/crates/dav/src/file/propfind.rs deleted file mode 100644 index 905810f1..00000000 --- a/crates/dav/src/file/propfind.rs +++ /dev/null @@ -1,709 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use common::{Server, auth::AccessToken}; -use dav_proto::schema::{ - property::{ - DavProperty, DavValue, Privilege, ReportSet, ResourceType, Rfc1123DateTime, SupportedLock, - WebDavProperty, - }, - request::{DavPropertyValue, PropFind}, - response::{ - AclRestrictions, BaseCondition, Href, MultiStatus, PropStat, Response, ResponseType, - SupportedPrivilege, - }, -}; -use groupware::{file::FileNode, hierarchy::DavHierarchy}; -use http_proto::HttpResponse; -use hyper::StatusCode; -use jmap_proto::types::{acl::Acl, collection::Collection}; -use store::{ - ahash::AHashMap, - dispatch::DocumentSet, - query::log::Query, - roaring::RoaringBitmap, - write::{AlignedBytes, Archive}, -}; -use trc::AddContext; -use utils::map::bitmap::Bitmap; - -use crate::{ - DavError, DavErrorCondition, DavResource, - common::{ - DavQuery, ETag, - acl::{DavAclHandler, Privileges}, - lock::LockData, - propfind::PropFindRequestHandler, - uri::Urn, - }, - principal::{CurrentUserPrincipal, propfind::PrincipalPropFind}, -}; - -pub(crate) trait HandleFilePropFindRequest: Sync + Send { - fn handle_file_propfind_request( - &self, - access_token: &AccessToken, - query: DavQuery<'_>, - ) -> impl Future> + Send; -} - -impl HandleFilePropFindRequest for Server { - async fn handle_file_propfind_request( - &self, - access_token: &AccessToken, - query: DavQuery<'_>, - ) -> crate::Result { - let account_id = query.resource.account_id; - let files = self - .fetch_dav_resources(account_id, Collection::FileNode) - .await - .caused_by(trc::location!())?; - - // Obtain document ids - let mut document_ids = if !access_token.is_member(account_id) { - self.shared_containers( - access_token, - account_id, - Collection::FileNode, - Bitmap::::from_iter([Acl::ReadItems, Acl::Read]), - ) - .await - .caused_by(trc::location!())? - .into() - } else { - None - }; - - // Filter by changelog - let mut sync_token = None; - if let Some(change_id) = query.from_change_id { - let changelog = self - .store() - .changes(account_id, Collection::FileNode, Query::Since(change_id)) - .await - .caused_by(trc::location!())?; - let limit = std::cmp::min( - query.limit.unwrap_or(u32::MAX) as usize, - self.core.dav.max_changes, - ); - if changelog.to_change_id != 0 { - sync_token = Some(Urn::Sync(changelog.to_change_id).to_string()); - } - let mut changes = - RoaringBitmap::from_iter(changelog.changes.iter().map(|change| change.id() as u32)); - if changes.len() as usize > limit { - changes = RoaringBitmap::from_sorted_iter(changes.into_iter().take(limit)).unwrap(); - } - if let Some(document_ids) = &mut document_ids { - *document_ids &= changes; - } else { - document_ids = Some(changes); - } - } - - let mut response = MultiStatus::new(Vec::with_capacity(16)); - let paths = if let Some(resource) = query.resource.resource { - Paths::new( - files - .subtree_with_depth(resource, query.depth) - .filter(|item| { - document_ids - .as_ref() - .is_none_or(|d| d.contains(item.document_id)) - }), - ) - } else { - if !query.depth_no_root || query.from_change_id.is_none() { - self.prepare_principal_propfind_response( - access_token, - Collection::FileNode, - [account_id].into_iter(), - &query.propfind, - &mut response, - ) - .await?; - - if query.depth == 0 { - return Ok(HttpResponse::new(StatusCode::MULTI_STATUS) - .with_xml_body(response.to_string())); - } - } - Paths::new(files.tree_with_depth(query.depth - 1).filter(|item| { - document_ids - .as_ref() - .is_none_or(|d| d.contains(item.document_id)) - })) - }; - - if paths.is_empty() && query.from_change_id.is_none() { - response.add_response(Response::new_status( - [query.format_to_base_uri(query.resource.resource.unwrap_or_default())], - StatusCode::NOT_FOUND, - )); - - return Ok( - HttpResponse::new(StatusCode::MULTI_STATUS).with_xml_body(response.to_string()) - ); - } else if query.depth == usize::MAX && paths.len() > self.core.dav.max_match_results { - return Err(DavError::Condition(DavErrorCondition::new( - StatusCode::PRECONDITION_FAILED, - BaseCondition::NumberOfMatchesWithinLimit, - ))); - } - - // Prepare response - let (fields, is_all_prop) = match &query.propfind { - PropFind::PropName => { - for (_, item) in paths.items { - let props = if item.is_container { - FOLDER_PROPS - .iter() - .cloned() - .map(DavPropertyValue::empty) - .collect::>() - } else { - FILE_PROPS - .iter() - .cloned() - .map(DavPropertyValue::empty) - .collect::>() - }; - - response.add_response(Response::new_propstat( - query.format_to_base_uri(&item.name), - vec![PropStat::new_list(props)], - )); - } - - return Ok( - HttpResponse::new(StatusCode::MULTI_STATUS).with_xml_body(response.to_string()) - ); - } - PropFind::AllProp(items) => (items, true), - PropFind::Prop(items) => (items, false), - }; - - // Fetch sync token - if sync_token.is_none() - && (is_all_prop - || query.from_change_id.is_some() - || fields - .iter() - .any(|field| matches!(field, DavProperty::WebDav(WebDavProperty::SyncToken)))) - { - let id = self - .store() - .get_last_change_id(account_id, Collection::FileNode) - .await - .caused_by(trc::location!())? - .unwrap_or_default(); - sync_token = Some(Urn::Sync(id).to_string()) - } - - // Add sync token - if query.from_change_id.is_some() { - response = response.with_sync_token(sync_token.clone().unwrap()); - } - - // Fetch locks - #[allow(unused_assignments)] - let mut locks_ = None; - let mut locks = None; - if is_all_prop - || fields - .iter() - .any(|field| matches!(field, DavProperty::WebDav(WebDavProperty::LockDiscovery))) - { - if let Some(lock_archive) = self - .in_memory_store() - .key_get::>(query.resource.lock_key().as_slice()) - .await - .caused_by(trc::location!())? - { - locks_ = Some(lock_archive); - locks = Some( - locks_ - .as_ref() - .unwrap() - .unarchive::() - .caused_by(trc::location!())?, - ); - } - } - - // Fetch quota - let (quota_used, quota_available) = if fields.iter().any(|field| { - matches!( - field, - DavProperty::WebDav( - WebDavProperty::QuotaAvailableBytes | WebDavProperty::QuotaUsedBytes - ) - ) - }) { - self.dav_quota(access_token, account_id) - .await - .caused_by(trc::location!())? - } else { - (0, 0) - }; - - // Fetch owner - let mut owner = None; - if fields - .iter() - .any(|field| matches!(field, DavProperty::WebDav(WebDavProperty::Owner))) - { - owner = self - .owner_href(access_token, account_id) - .await - .caused_by(trc::location!())? - .into(); - } - - let mut aces = Vec::new(); - self.get_archives( - account_id, - Collection::FileNode, - &paths, - |document_id, node_| { - let node = node_.unarchive::().caused_by(trc::location!())?; - let item = paths.items.get(&document_id).unwrap(); - let properties: Box> = if is_all_prop { - Box::new( - ALL_PROPS - .iter() - .chain(fields.iter().filter(|field| !field.is_all_prop())), - ) - } else { - Box::new(fields.iter()) - }; - - // Fill properties - let mut fields = Vec::with_capacity(19); - let mut fields_not_found = Vec::new(); - for property in properties { - match property { - DavProperty::WebDav(dav_property) => match dav_property { - WebDavProperty::CreationDate => { - fields.push(DavPropertyValue::new( - property.clone(), - DavValue::Timestamp(node.created.into()), - )); - } - WebDavProperty::DisplayName => { - if let Some(name) = node.display_name.as_ref() { - fields.push(DavPropertyValue::new( - property.clone(), - DavValue::String(name.to_string()), - )); - } else if !is_all_prop { - fields_not_found - .push(DavPropertyValue::empty(property.clone())); - } - } - WebDavProperty::GetContentLanguage => { - if !is_all_prop { - fields_not_found - .push(DavPropertyValue::empty(property.clone())); - } - } - WebDavProperty::GetContentLength => { - if let Some(value) = node.file.as_ref() { - fields.push(DavPropertyValue::new( - property.clone(), - DavValue::Uint64(u32::from(value.size) as u64), - )); - } else if !is_all_prop { - fields_not_found - .push(DavPropertyValue::empty(property.clone())); - } - } - WebDavProperty::GetContentType => { - if let Some(value) = - node.file.as_ref().and_then(|file| file.media_type.as_ref()) - { - fields.push(DavPropertyValue::new( - property.clone(), - DavValue::String(value.to_string()), - )); - } else if !is_all_prop { - fields_not_found - .push(DavPropertyValue::empty(property.clone())); - } - } - WebDavProperty::GetETag => { - fields.push(DavPropertyValue::new( - property.clone(), - DavValue::String(node_.etag()), - )); - } - WebDavProperty::GetLastModified => { - fields.push(DavPropertyValue::new( - property.clone(), - DavValue::Rfc1123Date(Rfc1123DateTime::new( - node.modified.into(), - )), - )); - } - WebDavProperty::ResourceType => { - if node.file.is_none() { - fields.push(DavPropertyValue::new( - property.clone(), - vec![ResourceType::Collection], - )); - } else { - fields.push(DavPropertyValue::empty(property.clone())); - } - } - WebDavProperty::LockDiscovery => { - if let Some(locks) = locks.as_ref() { - fields.push(DavPropertyValue::new( - property.clone(), - locks - .find_locks(&item.name, false) - .iter() - .map(|(path, lock)| { - lock.to_active_lock(query.format_to_base_uri(path)) - }) - .collect::>(), - )); - } else { - fields.push(DavPropertyValue::empty(property.clone())); - } - } - WebDavProperty::SupportedLock => { - fields.push(DavPropertyValue::new( - property.clone(), - SupportedLock::default(), - )); - } - WebDavProperty::SupportedReportSet => { - if node.file.is_none() { - fields.push(DavPropertyValue::new( - property.clone(), - vec![ - ReportSet::SyncCollection, - ReportSet::AclPrincipalPropSet, - ReportSet::PrincipalMatch, - ], - )); - } else if !is_all_prop { - fields_not_found - .push(DavPropertyValue::empty(property.clone())); - } - } - WebDavProperty::SyncToken => { - fields.push(DavPropertyValue::new( - property.clone(), - sync_token.clone().unwrap(), - )); - } - WebDavProperty::CurrentUserPrincipal => { - fields.push(DavPropertyValue::new( - property.clone(), - vec![access_token.current_user_principal()], - )); - } - WebDavProperty::QuotaAvailableBytes => { - if node.file.is_none() { - fields.push(DavPropertyValue::new( - property.clone(), - quota_available, - )); - } else if !is_all_prop { - fields_not_found - .push(DavPropertyValue::empty(property.clone())); - } - } - WebDavProperty::QuotaUsedBytes => { - if node.file.is_none() { - fields - .push(DavPropertyValue::new(property.clone(), quota_used)); - } else if !is_all_prop { - fields_not_found - .push(DavPropertyValue::empty(property.clone())); - } - } - WebDavProperty::Owner => { - if let Some(owner) = owner.take() { - fields - .push(DavPropertyValue::new(property.clone(), vec![owner])); - } - } - WebDavProperty::Group => { - fields.push(DavPropertyValue::empty(property.clone())); - } - WebDavProperty::SupportedPrivilegeSet => { - fields.push(DavPropertyValue::new( - property.clone(), - vec![ - SupportedPrivilege::new(Privilege::All, "Any operation") - .with_abstract() - .with_supported_privilege( - SupportedPrivilege::new( - Privilege::Read, - "Read objects", - ) - .with_supported_privilege(SupportedPrivilege::new( - Privilege::ReadCurrentUserPrivilegeSet, - "Read current user privileges", - )), - ) - .with_supported_privilege( - SupportedPrivilege::new( - Privilege::Write, - "Write objects", - ) - .with_supported_privilege(SupportedPrivilege::new( - Privilege::WriteProperties, - "Write properties", - )) - .with_supported_privilege(SupportedPrivilege::new( - Privilege::WriteContent, - "Write object contents", - )) - .with_supported_privilege(SupportedPrivilege::new( - Privilege::Bind, - "Add resources to a collection", - )) - .with_supported_privilege(SupportedPrivilege::new( - Privilege::Unbind, - "Add resources to a collection", - )) - .with_supported_privilege(SupportedPrivilege::new( - Privilege::Unlock, - "Unlock resources", - )), - ) - .with_supported_privilege(SupportedPrivilege::new( - Privilege::ReadAcl, - "Read ACL", - )) - .with_supported_privilege(SupportedPrivilege::new( - Privilege::WriteAcl, - "Write ACL", - )), - ], - )); - } - WebDavProperty::CurrentUserPrivilegeSet => { - fields.push(DavPropertyValue::new( - property.clone(), - access_token.current_privilege_set(account_id, &node.acls), - )); - } - WebDavProperty::Acl => { - aces.push(access_token.ace(account_id, &node.acls)); - } - WebDavProperty::AclRestrictions => { - fields.push(DavPropertyValue::new( - property.clone(), - AclRestrictions::default() - .with_no_invert() - .with_grant_only(), - )); - } - WebDavProperty::InheritedAclSet => { - fields.push(DavPropertyValue::empty(property.clone())); - } - WebDavProperty::PrincipalCollectionSet => { - fields.push(DavPropertyValue::new( - property.clone(), - vec![Href(DavResource::Principal.base_path().to_string())], - )); - } - WebDavProperty::AlternateURISet - | WebDavProperty::PrincipalURL - | WebDavProperty::GroupMemberSet - | WebDavProperty::GroupMembership => { - fields_not_found.push(DavPropertyValue::empty(property.clone())); - } - }, - DavProperty::DeadProperty(tag) => { - if let Some(value) = node.dead_properties.find_tag(&tag.name) { - fields.push(DavPropertyValue::new(property.clone(), value)); - } else { - fields_not_found.push(DavPropertyValue::empty(property.clone())); - } - } - property => { - if !is_all_prop { - fields_not_found.push(DavPropertyValue::empty(property.clone())); - } - } - } - } - - // Add dead properties - if is_all_prop && !node.dead_properties.0.is_empty() { - node.dead_properties.to_dav_values(&mut fields); - } - - // Add response - let mut prop_stat = Vec::with_capacity(2); - if !fields.is_empty() || !aces.is_empty() { - prop_stat.push(PropStat::new_list(fields)); - } - if !fields_not_found.is_empty() && !query.is_minimal() { - prop_stat.push( - PropStat::new_list(fields_not_found).with_status(StatusCode::NOT_FOUND), - ); - } - if prop_stat.is_empty() { - prop_stat.push(PropStat::new_list(vec![])); - } - response.add_response(Response::new_propstat( - query.format_to_base_uri(&item.name), - prop_stat, - )); - - Ok(true) - }, - ) - .await - .caused_by(trc::location!())?; - - // Resolve ACEs - if !aces.is_empty() { - for (ace, response) in aces.into_iter().zip(response.response.0.iter_mut()) { - let ace = self.resolve_ace(ace).await.caused_by(trc::location!())?; - if let ResponseType::PropStat(list) = &mut response.typ { - list.0 - .first_mut() - .unwrap() - .prop - .0 - .0 - .push(DavPropertyValue::new( - DavProperty::WebDav(WebDavProperty::Acl), - ace, - )); - } - } - } - - Ok(HttpResponse::new(StatusCode::MULTI_STATUS).with_xml_body(response.to_string())) - } -} - -static FOLDER_PROPS: [DavProperty; 19] = [ - DavProperty::WebDav(WebDavProperty::CreationDate), - DavProperty::WebDav(WebDavProperty::DisplayName), - DavProperty::WebDav(WebDavProperty::GetETag), - DavProperty::WebDav(WebDavProperty::GetLastModified), - DavProperty::WebDav(WebDavProperty::ResourceType), - DavProperty::WebDav(WebDavProperty::LockDiscovery), - DavProperty::WebDav(WebDavProperty::SupportedLock), - DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal), - DavProperty::WebDav(WebDavProperty::SyncToken), - DavProperty::WebDav(WebDavProperty::Owner), - DavProperty::WebDav(WebDavProperty::SupportedPrivilegeSet), - DavProperty::WebDav(WebDavProperty::CurrentUserPrivilegeSet), - DavProperty::WebDav(WebDavProperty::Acl), - DavProperty::WebDav(WebDavProperty::AclRestrictions), - DavProperty::WebDav(WebDavProperty::InheritedAclSet), - DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet), - DavProperty::WebDav(WebDavProperty::SupportedReportSet), - DavProperty::WebDav(WebDavProperty::QuotaAvailableBytes), - DavProperty::WebDav(WebDavProperty::QuotaUsedBytes), -]; - -static FILE_PROPS: [DavProperty; 19] = [ - DavProperty::WebDav(WebDavProperty::CreationDate), - DavProperty::WebDav(WebDavProperty::DisplayName), - DavProperty::WebDav(WebDavProperty::GetETag), - DavProperty::WebDav(WebDavProperty::GetLastModified), - DavProperty::WebDav(WebDavProperty::ResourceType), - DavProperty::WebDav(WebDavProperty::LockDiscovery), - DavProperty::WebDav(WebDavProperty::SupportedLock), - DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal), - DavProperty::WebDav(WebDavProperty::SyncToken), - DavProperty::WebDav(WebDavProperty::Owner), - DavProperty::WebDav(WebDavProperty::SupportedPrivilegeSet), - DavProperty::WebDav(WebDavProperty::CurrentUserPrivilegeSet), - DavProperty::WebDav(WebDavProperty::Acl), - DavProperty::WebDav(WebDavProperty::AclRestrictions), - DavProperty::WebDav(WebDavProperty::InheritedAclSet), - DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet), - DavProperty::WebDav(WebDavProperty::GetContentLanguage), - DavProperty::WebDav(WebDavProperty::GetContentLength), - DavProperty::WebDav(WebDavProperty::GetContentType), -]; - -static ALL_PROPS: [DavProperty; 17] = [ - DavProperty::WebDav(WebDavProperty::CreationDate), - DavProperty::WebDav(WebDavProperty::DisplayName), - DavProperty::WebDav(WebDavProperty::GetETag), - DavProperty::WebDav(WebDavProperty::GetLastModified), - DavProperty::WebDav(WebDavProperty::ResourceType), - DavProperty::WebDav(WebDavProperty::LockDiscovery), - DavProperty::WebDav(WebDavProperty::SupportedLock), - DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal), - DavProperty::WebDav(WebDavProperty::SyncToken), - DavProperty::WebDav(WebDavProperty::SupportedPrivilegeSet), - DavProperty::WebDav(WebDavProperty::AclRestrictions), - DavProperty::WebDav(WebDavProperty::CurrentUserPrivilegeSet), - DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet), - DavProperty::WebDav(WebDavProperty::GetContentLanguage), - DavProperty::WebDav(WebDavProperty::GetContentLength), - DavProperty::WebDav(WebDavProperty::GetContentType), - DavProperty::WebDav(WebDavProperty::SupportedReportSet), -]; - -struct Paths<'x> { - min: u32, - max: u32, - items: AHashMap, -} - -impl<'x> Paths<'x> { - pub fn new(iter: impl Iterator) -> Self { - let mut paths = Paths { - min: u32::MAX, - max: 0, - items: AHashMap::with_capacity(16), - }; - - for item in iter { - if item.document_id < paths.min { - paths.min = item.document_id; - } - - if item.document_id > paths.max { - paths.max = item.document_id; - } - - paths.items.insert(item.document_id, item); - } - paths - } - - pub fn is_empty(&self) -> bool { - self.items.is_empty() - } -} - -impl DocumentSet for Paths<'_> { - fn min(&self) -> u32 { - self.min - } - - fn max(&self) -> u32 { - self.max - } - - fn contains(&self, id: u32) -> bool { - self.items.contains_key(&id) - } - - fn len(&self) -> usize { - self.items.len() - } - - fn iterate(&self) -> impl Iterator { - self.items.keys().copied() - } -} diff --git a/crates/dav/src/principal/matching.rs b/crates/dav/src/principal/matching.rs index fdbc80c2..1d847630 100644 --- a/crates/dav/src/principal/matching.rs +++ b/crates/dav/src/principal/matching.rs @@ -6,7 +6,7 @@ use common::{Server, auth::AccessToken}; use dav_proto::{ - RequestHeaders, Return, + RequestHeaders, schema::{ property::{DavProperty, WebDavProperty}, request::{PrincipalMatch, PropFind}, @@ -20,8 +20,7 @@ use store::roaring::RoaringBitmap; use crate::{ DavError, - common::{DavQuery, uri::DavUriResource}, - file::propfind::HandleFilePropFindRequest, + common::{DavQuery, DavQueryResource, propfind::PropFindRequestHandler, uri::DavUriResource}, }; use super::propfind::PrincipalPropFind; @@ -47,23 +46,18 @@ impl PrincipalMatching for Server { .await .and_then(|uri| uri.into_owned_uri())?; - let todo = "implement cal, card"; - match resource.collection { - Collection::Calendar => todo!(), - Collection::AddressBook => todo!(), - Collection::FileNode => { - self.handle_file_propfind_request( + Collection::AddressBook | Collection::Calendar | Collection::FileNode => { + self.handle_dav_query( access_token, DavQuery { - resource, - base_uri: headers.uri, + resource: DavQueryResource::Uri(resource), + base_uri: headers.base_uri().unwrap_or_default(), propfind: PropFind::Prop(request.properties), - from_change_id: None, depth: usize::MAX, - limit: None, ret: headers.ret, depth_no_root: headers.depth_no_root, + ..Default::default() }, ) .await diff --git a/crates/dav/src/principal/propfind.rs b/crates/dav/src/principal/propfind.rs index 40c18583..41a53410 100644 --- a/crates/dav/src/principal/propfind.rs +++ b/crates/dav/src/principal/propfind.rs @@ -8,7 +8,8 @@ use std::borrow::Cow; use common::{Server, auth::AccessToken}; use dav_proto::schema::{ - property::{DavProperty, ReportSet, ResourceType, WebDavProperty}, + Namespace, + property::{DavProperty, PrincipalProperty, ReportSet, ResourceType, WebDavProperty}, request::{DavPropertyValue, PropFind}, response::{Href, MultiStatus, PropStat, Response}, }; @@ -69,7 +70,18 @@ impl PrincipalPropFind for Server { PropFind::AllProp(items) => Cow::Owned(all_props(collection, items.as_slice().into())), PropFind::Prop(items) => Cow::Borrowed(items), }; - let is_principal = collection == Collection::Principal; + let is_principal = match collection { + Collection::AddressBook | Collection::ContactCard => { + response.set_namespace(Namespace::CardDav); + false + } + Collection::Calendar | Collection::CalendarEvent => { + response.set_namespace(Namespace::CalDav); + false + } + Collection::Principal => true, + _ => false, + }; let base_path = DavResource::from(collection).base_path(); let needs_quota = properties.iter().any(|property| { matches!( @@ -116,12 +128,12 @@ impl PrincipalPropFind for Server { }; // Fetch quota - let (quota_used, quota_available) = if needs_quota { + let quota = if needs_quota { self.dav_quota(access_token, account_id) .await .caused_by(trc::location!())? } else { - (0, 0) + Default::default() }; for property in properties.as_slice() { @@ -132,14 +144,14 @@ impl PrincipalPropFind for Server { .push(DavPropertyValue::new(property.clone(), description.clone())); } WebDavProperty::ResourceType => { - if !is_principal { - fields.push(DavPropertyValue::new( - property.clone(), - vec![ResourceType::Collection], - )); + let resource_type = if !is_principal { + ResourceType::Collection } else { - fields.push(DavPropertyValue::empty(property.clone())); - } + ResourceType::Principal + }; + + fields + .push(DavPropertyValue::new(property.clone(), vec![resource_type])); } WebDavProperty::SupportedReportSet => { let reports = if !is_principal { @@ -164,10 +176,10 @@ impl PrincipalPropFind for Server { )); } WebDavProperty::QuotaAvailableBytes if !is_principal => { - fields.push(DavPropertyValue::new(property.clone(), quota_available)); + fields.push(DavPropertyValue::new(property.clone(), quota.available)); } WebDavProperty::QuotaUsedBytes if !is_principal => { - fields.push(DavPropertyValue::new(property.clone(), quota_used)); + fields.push(DavPropertyValue::new(property.clone(), quota.used)); } WebDavProperty::SyncToken if !is_principal => { let id = self @@ -181,16 +193,7 @@ impl PrincipalPropFind for Server { Urn::Sync(id).to_string(), )); } - WebDavProperty::AlternateURISet if is_principal => { - fields.push(DavPropertyValue::empty(property.clone())); - } - WebDavProperty::GroupMemberSet if is_principal => { - fields.push(DavPropertyValue::empty(property.clone())); - } - WebDavProperty::GroupMembership if is_principal => { - fields.push(DavPropertyValue::empty(property.clone())); - } - WebDavProperty::Owner | WebDavProperty::PrincipalURL => { + WebDavProperty::Owner => { fields.push(DavPropertyValue::new( property.clone(), vec![Href(format!( @@ -213,6 +216,48 @@ impl PrincipalPropFind for Server { 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())); + } + } + } _ => { fields_not_found.push(DavPropertyValue::empty(property.clone())); } @@ -270,11 +315,11 @@ fn all_props(collection: Collection, all_props: Option<&[DavProperty]>) -> Vec { let request = Acl::parse(&mut Tokenizer::new(&body))?; match resource { - DavResource::Card => { - self.handle_card_acl_request(&access_token, headers, request) - .await - } - DavResource::Cal => todo!(), - DavResource::File => { - self.handle_file_acl_request(&access_token, headers, request) + DavResource::Card | DavResource::Cal | DavResource::File => { + self.handle_acl_request(&access_token, headers, request) .await } DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), @@ -276,16 +270,13 @@ impl DavRequestDispatcher for Server { .validate_uri(&access_token, headers.uri) .await .and_then(|d| d.into_owned_uri())?; - let request = DavQuery::changes(uri, sync_collection, headers); match resource { - DavResource::Card => { - self.handle_card_propfind_request(&access_token, request) - .await - } - DavResource::Cal => todo!(), - DavResource::File => { - self.handle_file_propfind_request(&access_token, request) - .await + DavResource::Card | DavResource::Cal | DavResource::File => { + self.handle_dav_query( + &access_token, + DavQuery::changes(uri, sync_collection, headers), + ) + .await } DavResource::Principal => { Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)) @@ -326,8 +317,11 @@ impl DavRequestDispatcher for Server { .await } Report::AddressbookMultiGet(report) => { - self.handle_card_multiget_request(&access_token, headers, report) - .await + self.handle_dav_query( + &access_token, + DavQuery::multiget(report, Collection::AddressBook, headers), + ) + .await } Report::CalendarQuery(report) => todo!(), Report::CalendarMultiGet(report) => todo!(), diff --git a/crates/groupware/src/calendar/mod.rs b/crates/groupware/src/calendar/mod.rs index 5920b801..c14fb283 100644 --- a/crates/groupware/src/calendar/mod.rs +++ b/crates/groupware/src/calendar/mod.rs @@ -4,9 +4,12 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use std::collections::HashMap; + use calcard::icalendar::ICalendar; +use dav_proto::schema::request::DeadProperty; use jmap_proto::types::{acl::Acl, value::AclGrant}; -use store::{SERIALIZE_OBJ_14_V1, SerializedVersion}; +use store::{SERIALIZE_OBJ_14_V1, SERIALIZE_OBJ_16_V1, SerializedVersion, ahash}; use utils::map::vec_map::VecMap; use crate::DavName; @@ -15,8 +18,12 @@ use crate::DavName; rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, )] pub struct Calendar { - pub preferences: VecMap, + pub name: String, + pub preferences: HashMap, pub acls: Vec, + pub dead_properties: DeadProperty, + pub created: i64, + pub modified: i64, } #[derive( @@ -31,8 +38,8 @@ pub struct CalendarPreferences { pub is_default: bool, pub is_visible: bool, pub include_in_availability: IncludeInAvailability, - pub default_alerts_with_time: VecMap, - pub default_alerts_without_time: VecMap, + pub default_alerts_with_time: HashMap, + pub default_alerts_without_time: HashMap, pub time_zone: Timezone, } @@ -41,14 +48,17 @@ pub struct CalendarPreferences { )] pub struct CalendarEvent { pub names: Vec, + pub display_name: Option, pub event: ICalendar, pub user_properties: VecMap, - pub created: u64, - pub updated: u64, pub may_invite_self: bool, pub may_invite_others: bool, pub hide_attendees: bool, pub is_draft: bool, + pub dead_properties: DeadProperty, + pub size: u32, + pub created: i64, + pub modified: i64, } #[derive( @@ -121,3 +131,21 @@ impl SerializedVersion for Calendar { SERIALIZE_OBJ_14_V1 } } + +impl SerializedVersion for CalendarEvent { + fn serialize_version() -> u8 { + SERIALIZE_OBJ_16_V1 + } +} + +impl ArchivedCalendar { + pub fn preferences(&self, account_id: u32) -> Option<&ArchivedCalendarPreferences> { + if self.preferences.len() == 1 { + self.preferences.values().next() + } else { + self.preferences + .get(&rkyv::rend::u32_le::from_native(account_id)) + .or_else(|| self.preferences.values().next()) + } + } +} diff --git a/crates/groupware/src/contact/index.rs b/crates/groupware/src/contact/index.rs index 02a05eae..157d575e 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}; @@ -32,6 +32,7 @@ impl IndexableObject for AddressBook { + self.description.as_ref().map_or(0, |n| n.len() as u32) + self.name.len() as u32, }, + IndexValue::LogChild { prefix: None }, ] .into_iter() } @@ -58,6 +59,7 @@ impl IndexableObject for &ArchivedAddressBook { + self.description.as_ref().map_or(0, |n| n.len() as u32) + self.name.len() as u32, }, + IndexValue::LogChild { prefix: None }, ] .into_iter() } @@ -86,6 +88,11 @@ impl IndexableObject for ContactCard { + self.names.iter().map(|n| n.name.len() as u32).sum::() + self.size, }, + IndexValue::LogChild { prefix: None }, + IndexValue::LogParent { + collection: Collection::AddressBook, + ids: self.names.iter().map(|v| v.parent_id).collect::>(), + }, ] .into_iter() } @@ -112,6 +119,15 @@ impl IndexableObject for &ArchivedContactCard { + self.names.iter().map(|n| n.name.len() as u32).sum::() + self.size, }, + IndexValue::LogChild { prefix: None }, + IndexValue::LogParent { + collection: Collection::AddressBook, + ids: self + .names + .iter() + .map(|v| v.parent_id.to_native()) + .collect::>(), + }, ] .into_iter() } diff --git a/crates/groupware/src/file/index.rs b/crates/groupware/src/file/index.rs index add92217..6f9e8687 100644 --- a/crates/groupware/src/file/index.rs +++ b/crates/groupware/src/file/index.rs @@ -36,6 +36,7 @@ impl IndexableObject for FileNode { IndexValue::Acl { value: (&self.acls).into(), }, + IndexValue::LogChild { prefix: None }, ]); if let Some(file) = &self.file { @@ -79,6 +80,7 @@ impl IndexableObject for &ArchivedFileNode { .collect::>() .into(), }, + IndexValue::LogChild { prefix: None }, ]); let size = self.size(); diff --git a/crates/groupware/src/hierarchy.rs b/crates/groupware/src/hierarchy.rs index e2c6ea9c..782cc3f2 100644 --- a/crates/groupware/src/hierarchy.rs +++ b/crates/groupware/src/hierarchy.rs @@ -60,6 +60,7 @@ impl DavHierarchy for Server { files.modseq = change_id; let files = Arc::new(files); self.inner.cache.dav.insert(resource_id, files.clone()); + Ok(files) } } diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs index 6401f94a..57071e76 100644 --- a/crates/store/src/lib.rs +++ b/crates/store/src/lib.rs @@ -86,6 +86,7 @@ pub const SERIALIZE_OBJ_13_V1: u8 = 12; pub const SERIALIZE_OBJ_14_V1: u8 = 13; pub const SERIALIZE_OBJ_15_V1: u8 = 14; pub const SERIALIZE_OBJ_16_V1: u8 = 15; +pub const SERIALIZE_OBJ_17_V1: u8 = 16; pub trait SerializedVersion { fn serialize_version() -> u8; diff --git a/crates/store/src/write/batch.rs b/crates/store/src/write/batch.rs index 0220a577..277ed788 100644 --- a/crates/store/src/write/batch.rs +++ b/crates/store/src/write/batch.rs @@ -10,7 +10,10 @@ use std::sync::{ }; use utils::{ - map::bitmap::{Bitmap, ShortId}, + map::{ + bitmap::{Bitmap, ShortId}, + vec_map::VecMap, + }, snowflake::SnowflakeIdGenerator, }; @@ -32,12 +35,12 @@ impl BatchBuilder { current_account_id: None, current_collection: None, current_document_id: None, + changes: Default::default(), changed_collections: Default::default(), batch_size: 0, batch_ops: 0, has_assertions: false, commit_points: Vec::new(), - changelog: Default::default(), } } @@ -59,9 +62,6 @@ impl BatchBuilder { .current_account_id .is_none_or(|current_account_id| current_account_id != account_id) { - if self.current_account_id.is_some() && self.current_change_id.is_some() { - self.serialize_changes(); - } self.current_account_id = account_id.into(); self.ops.push(Operation::AccountId { account_id }); } @@ -251,15 +251,14 @@ impl BatchBuilder { } pub fn log_insert(&mut self, prefix: Option) -> &mut Self { - if let (Some(account_id), Some(collection)) = - (self.current_account_id, self.current_collection) - { - self.changed_collections + if let (Some(account_id), Some(collection), Some(document_id)) = ( + self.current_account_id, + self.current_collection, + self.current_document_id, + ) { + self.changes .get_mut_or_insert(account_id) - .insert(ShortId(collection)); - if let Some(document_id) = self.current_document_id { - self.changelog.log_insert(collection, prefix, document_id); - } + .log_insert(collection, prefix, document_id); } if self.current_change_id.is_none() { self.generate_change_id(); @@ -269,15 +268,14 @@ impl BatchBuilder { } pub fn log_update(&mut self, prefix: Option) -> &mut Self { - if let (Some(account_id), Some(collection)) = - (self.current_account_id, self.current_collection) - { - self.changed_collections + if let (Some(account_id), Some(collection), Some(document_id)) = ( + self.current_account_id, + self.current_collection, + self.current_document_id, + ) { + self.changes .get_mut_or_insert(account_id) - .insert(ShortId(collection)); - if let Some(document_id) = self.current_document_id { - self.changelog.log_update(collection, prefix, document_id); - } + .log_update(collection, prefix, document_id); } if self.current_change_id.is_none() { self.generate_change_id(); @@ -287,15 +285,14 @@ impl BatchBuilder { } pub fn log_delete(&mut self, prefix: Option) -> &mut Self { - if let (Some(account_id), Some(collection)) = - (self.current_account_id, self.current_collection) - { - self.changed_collections + if let (Some(account_id), Some(collection), Some(document_id)) = ( + self.current_account_id, + self.current_collection, + self.current_document_id, + ) { + self.changes .get_mut_or_insert(account_id) - .insert(ShortId(collection)); - if let Some(document_id) = self.current_document_id { - self.changelog.log_delete(collection, prefix, document_id); - } + .log_delete(collection, prefix, document_id); } if self.current_change_id.is_none() { self.generate_change_id(); @@ -308,11 +305,10 @@ impl BatchBuilder { let collection = collection.into(); if let Some(account_id) = self.current_account_id { - self.changed_collections + self.changes .get_mut_or_insert(account_id) - .insert(ShortId(collection)); + .log_child_update(collection, None, parent_id); } - self.changelog.log_child_update(collection, None, parent_id); if self.current_change_id.is_none() { self.generate_change_id(); self.batch_ops += 1; @@ -322,13 +318,21 @@ impl BatchBuilder { fn serialize_changes(&mut self) { if let Some(change_id) = self.current_change_id.take() { - if !self.changelog.is_empty() { - for (collection, set) in std::mem::take(&mut self.changelog).serialize() { - self.ops.push(Operation::Log { - change_id, - collection, - set, - }); + if !self.changes.is_empty() { + for (account_id, changelog) in std::mem::take(&mut self.changes) { + self.with_account_id(account_id); + + for (collection, set) in changelog.serialize() { + let cc = self.changed_collections.get_mut_or_insert(account_id); + cc.0 = change_id; + cc.1.insert(ShortId(collection)); + + self.ops.push(Operation::Log { + change_id, + collection, + set, + }); + } } } } @@ -401,11 +405,15 @@ impl BatchBuilder { } } - pub fn changed_collections(&mut self) -> impl Iterator)> { - self.changed_collections.iter() + pub fn changes(self) -> Option)>> { + if self.has_changes() { + Some(self.changed_collections) + } else { + None + } } - pub fn has_logs(&self) -> bool { + pub fn has_changes(&self) -> bool { !self.changed_collections.is_empty() } diff --git a/crates/store/src/write/log.rs b/crates/store/src/write/log.rs index 0d6abc36..50f59f4d 100644 --- a/crates/store/src/write/log.rs +++ b/crates/store/src/write/log.rs @@ -61,10 +61,6 @@ impl ChangeLogBuilder { .child_updates .insert(build_id(prefix, document_id)); } - - pub fn is_empty(&self) -> bool { - self.changes.is_empty() - } } #[inline(always)] diff --git a/crates/store/src/write/mod.rs b/crates/store/src/write/mod.rs index fa70d9b1..aadfe2c1 100644 --- a/crates/store/src/write/mod.rs +++ b/crates/store/src/write/mod.rs @@ -94,8 +94,8 @@ pub struct BatchBuilder { current_account_id: Option, current_collection: Option, current_document_id: Option, - changed_collections: VecMap>, - changelog: ChangeLogBuilder, + changes: VecMap, + changed_collections: VecMap)>, has_assertions: bool, batch_size: usize, batch_ops: usize,