CardDAV PROPFIND + multiget REPORT

This commit is contained in:
mdecimus 2025-04-04 17:27:58 +02:00
parent fac2975a5a
commit a06c94d45d
36 changed files with 2697 additions and 1338 deletions

View file

@ -529,17 +529,15 @@ impl Server {
pub async fn commit_batch(&self, mut builder: BatchBuilder) -> trc::Result<AssignedIds> {
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)

View file

@ -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()),
)),

View file

@ -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<SupportedPrivilege>,
) -> Self {
if let Some(supported_privilege) = supported_privilege {
self.supported_privilege.0.push(supported_privilege);
}
self
}
}
impl Display for Ace {

View file

@ -135,7 +135,7 @@ impl Display for CardCondition {
CardCondition::MaxResourceSize(l) => {
write!(f, "<C:max-resource-size>{l}</C:max-resource-size>")
}
CardCondition::AddressBoolCollectionLocationOk => {
CardCondition::AddressBookCollectionLocationOk => {
write!(f, "<C:addressbook-collection-location-ok/>")
}
}

View file

@ -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<String>) -> Self {
self.sync_token = Some(SyncToken(sync_token.into()));
self
}
pub fn set_sync_token(&mut self, sync_token: impl Into<String>) {
self.sync_token = Some(SyncToken(sync_token.into()));
}
}
impl Response {

View file

@ -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!(
"<C:supported-address-data>",
"<C:address-data-type content-type=\"text/vcard\" version=\"4.0\"/>",
"<C:address-data-type content-type=\"text/vcard\" version=\"3.0\"/>",
"<C:address-data-type content-type=\"text/vcard\" version=\"2.0\"/>",
"</C:supported-address-data>"
)
)
}
DavValue::SupportedCalendarData => {
write!(
f,
concat!(
"<C:supported-calendar-data>",
"<C:calendar-data-type content-type=\"text/calendar\" version=\"2.0\"/>",
"<C:calendar-data-type content-type=\"text/calendar\" version=\"1.0\"/>",
"</C:supported-calendar-data>"
)
)
}
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())
}

View file

@ -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<Ace>),
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)

View file

@ -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<DavProperty>),
Prop(Vec<DavProperty>),

View file

@ -223,7 +223,7 @@ pub enum CardCondition {
ValidAddressData,
NoUidConflict(Href),
MaxResourceSize(u32),
AddressBoolCollectionLocationOk,
AddressBookCollectionLocationOk,
}
impl BaseCondition {

View file

@ -1,37 +0,0 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* 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<Output = crate::Result<HttpResponse>> + Send;
}
impl CardAclRequestHandler for Server {
async fn handle_card_acl_request(
&self,
access_token: &AccessToken,
headers: RequestHeaders<'_>,
request: Acl,
) -> crate::Result<HttpResponse> {
// Validate URI
let resource_ = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
todo!()
}
}

View file

@ -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::<Vec<_>>(),
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::<Vec<_>>(),
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::<Vec<_>>(),
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::<Vec<_>>(),
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::<Vec<_>>();
let to_document_ids = to_resources
.subtree(destination_resource_name)
.filter(|r| !r.is_container)
.map(|r| r.document_id)
.collect::<Vec<_>>();
// 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::<Vec<_>>();
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::<ContactCard>()
.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::<ContactCard>()
.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<u32>,
to_addressbook_id: u32,
new_name: Option<&str>,
to_base_path: String,
new_name: &str,
) -> crate::Result<HttpResponse> {
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::<ContactCard>()
.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::<ContactCard>()
.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::<ContactCard>()
.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::<ContactCard>()
.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<u32>,
to_children_ids: Vec<u32>,
new_name: Option<&str>,
new_name: &str,
remove_source: bool,
) -> crate::Result<HttpResponse> {
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::<AddressBook>()
.caused_by(trc::location!())?;
let mut book = old_book
.deserialize::<AddressBook>()
.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<u32>,
to_account_id: u32,
to_document_id: Option<u32>,
to_children_ids: Vec<u32>,
new_name: Option<&str>,
) -> crate::Result<HttpResponse> {
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::<AddressBook>()
.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::<ContactCard>()
.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::<ContactCard>()
.caused_by(trc::location!())?;
let mut new_card = card
.deserialize::<ContactCard>()
.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)]

View file

@ -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::<ContactCard>()
.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,

View file

@ -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 {

View file

@ -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<Option<String>> {
// 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(())
}

View file

@ -1,53 +0,0 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* 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<Output = crate::Result<HttpResponse>> + Send;
fn handle_card_multiget_request(
&self,
access_token: &AccessToken,
headers: RequestHeaders<'_>,
request: MultiGet,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
}
impl CardPropFindRequestHandler for Server {
async fn handle_card_propfind_request(
&self,
access_token: &AccessToken,
query: DavQuery<'_>,
) -> crate::Result<HttpResponse> {
// Validate URI
todo!()
}
async fn handle_card_multiget_request(
&self,
access_token: &AccessToken,
headers: RequestHeaders<'_>,
request: MultiGet,
) -> crate::Result<HttpResponse> {
// Validate URI
let resource_ = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
todo!()
}
}

View file

@ -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<DavPropertyValue>,
items: &mut Vec<PropStat>,
) -> bool;
fn apply_card_properties(
&self,
card: &mut ContactCard,
is_update: bool,
properties: Vec<DavPropertyValue>,
items: &mut Vec<PropStat>,
) -> 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<HttpResponse> {
// 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::<AddressBook>()
.caused_by(trc::location!())?;
let mut new_book = archive
.deserialize::<AddressBook>()
.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::<ContactCard>()
.caused_by(trc::location!())?;
let mut new_card = archive
.deserialize::<ContactCard>()
.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<DavPropertyValue>,
items: &mut Vec<PropStat>,
) -> 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<DavProperty>,
items: &mut Vec<PropStat>,
) {
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<DavProperty>,
items: &mut Vec<PropStat>,
) {
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"),
);
}
}
}
}

View file

@ -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::<ContactCard>()
.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))

View file

@ -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<Output = crate::Result<HttpResponse>> + 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<UnresolvedAce>,
access_token: &AccessToken,
account_id: u32,
grants: &ArchivedVec<ArchivedAclGrant>,
) -> impl Future<Output = trc::Result<Vec<Ace>>> + Send;
}
impl DavAclHandler for Server {
async fn handle_acl_request(
&self,
access_token: &AccessToken,
headers: RequestHeaders<'_>,
request: dav_proto::schema::request::Acl,
) -> crate::Result<HttpResponse> {
// 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::<AddressBook>()
.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::<FileNode>().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<UnresolvedAce>) -> trc::Result<Vec<Ace>> {
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<Privilege>,
}
pub(crate) trait Privileges {
fn ace(&self, account_id: u32, grants: &ArchivedVec<ArchivedAclGrant>) -> Vec<UnresolvedAce>;
fn current_privilege_set(
async fn resolve_ace(
&self,
access_token: &AccessToken,
account_id: u32,
grants: &ArchivedVec<ArchivedAclGrant>,
) -> Vec<Privilege>;
}
impl Privileges for AccessToken {
fn ace(&self, account_id: u32, grants: &ArchivedVec<ArchivedAclGrant>) -> Vec<UnresolvedAce> {
) -> trc::Result<Vec<Ace>> {
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<ArchivedAclGrant>,
) -> Vec<Privilege>;
}
impl Privileges for AccessToken {
fn current_privilege_set(
&self,
account_id: u32,

View file

@ -838,24 +838,24 @@ impl ArchivedLockItem {
impl OwnedUri<'_> {
pub fn lock_key(&self) -> Vec<u8> {
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<u8> {
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<u8> {
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

View file

@ -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<u64>,
@ -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<String>,
},
}
pub(crate) trait ETag {
fn etag(&self) -> String;
}
pub trait ExtractETag {
pub(crate) trait ExtractETag {
fn etag(&self) -> Option<String>;
}
@ -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<AlignedBytes>,
collection: Collection,
) -> trc::Result<Self> {
match collection {
Collection::Calendar => archive
.to_unarchived::<Calendar>()
.map(ArchivedResource::Calendar),
Collection::CalendarEvent => archive
.to_unarchived::<CalendarEvent>()
.map(ArchivedResource::CalendarEvent),
Collection::AddressBook => archive
.to_unarchived::<AddressBook>()
.map(ArchivedResource::AddressBook),
Collection::FileNode => archive
.to_unarchived::<FileNode>()
.map(ArchivedResource::FileNode),
Collection::ContactCard => archive
.to_unarchived::<ContactCard>()
.map(ArchivedResource::ContactCard),
_ => unreachable!(),
}
}
pub fn acls(&self) -> Option<&ArchivedVec<ArchivedAclGrant>> {
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<u32> {
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<Vec<ReportSet>> {
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<Vec<ResourceType>> {
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,
}
}
}

View file

@ -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<Output = crate::Result<HttpResponse>> + Send;
fn handle_dav_query(
&self,
access_token: &AccessToken,
query: DavQuery<'_>,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
fn dav_quota(
&self,
access_token: &AccessToken,
account_id: u32,
) -> impl Future<Output = trc::Result<(u64, u64)>> + Send;
) -> impl Future<Output = trc::Result<PropFindAccountQuota>> + Send;
}
pub(crate) struct PropFindData {
pub accounts: AHashMap<u32, PropFindAccountData>,
}
#[derive(Default)]
pub(crate) struct PropFindAccountData {
pub sync_token: Option<String>,
pub quota: Option<PropFindAccountQuota>,
pub owner: Option<Href>,
pub locks: Option<Archive<AlignedBytes>>,
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<HttpResponse> {
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::<Vec<_>>()
} 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::<Vec<_>>()
};
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<DavResources>, Arc<Option<RoaringBitmap>>),
> = 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::<Vec<_>>()
} else {
children_props
.iter()
.cloned()
.map(DavPropertyValue::empty)
.collect::<Vec<_>>()
};
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<PropFindAccountQuota> {
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<PropFindAccountQuota> {
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<Href> {
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<String> {
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<Option<Vec<ActiveLock>>> {
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::<Archive<AlignedBytes>>(
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::<LockData>().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::<Vec<_>>()
.into()
})
} else {
Ok(None)
}
}
}

View file

@ -17,6 +17,7 @@ use trc::AddContext;
use crate::{DavError, DavResource};
#[derive(Debug)]
pub(crate) struct UriResource<A, R> {
pub collection: Collection,
pub account_id: A,

View file

@ -1,101 +0,0 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* 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<Output = crate::Result<HttpResponse>> + 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<HttpResponse> {
// 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::<FileNode>()
.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))
}
}

View file

@ -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;
}

View file

@ -1,709 +0,0 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* 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<Output = crate::Result<HttpResponse>> + Send;
}
impl HandleFilePropFindRequest for Server {
async fn handle_file_propfind_request(
&self,
access_token: &AccessToken,
query: DavQuery<'_>,
) -> crate::Result<HttpResponse> {
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::<Acl>::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::<Vec<_>>()
} else {
FILE_PROPS
.iter()
.cloned()
.map(DavPropertyValue::empty)
.collect::<Vec<_>>()
};
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::<Archive<AlignedBytes>>(query.resource.lock_key().as_slice())
.await
.caused_by(trc::location!())?
{
locks_ = Some(lock_archive);
locks = Some(
locks_
.as_ref()
.unwrap()
.unarchive::<LockData>()
.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::<FileNode>().caused_by(trc::location!())?;
let item = paths.items.get(&document_id).unwrap();
let properties: Box<dyn Iterator<Item = &DavProperty>> = 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::<Vec<_>>(),
));
} 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<u32, &'x common::DavResource>,
}
impl<'x> Paths<'x> {
pub fn new(iter: impl Iterator<Item = &'x common::DavResource>) -> 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<Item = u32> {
self.items.keys().copied()
}
}

View file

@ -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

View file

@ -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<D
DavProperty::WebDav(WebDavProperty::ResourceType),
DavProperty::WebDav(WebDavProperty::SupportedReportSet),
DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal),
DavProperty::WebDav(WebDavProperty::AlternateURISet),
DavProperty::WebDav(WebDavProperty::PrincipalURL),
DavProperty::WebDav(WebDavProperty::GroupMemberSet),
DavProperty::WebDav(WebDavProperty::GroupMembership),
DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet),
DavProperty::Principal(PrincipalProperty::AlternateURISet),
DavProperty::Principal(PrincipalProperty::PrincipalURL),
DavProperty::Principal(PrincipalProperty::GroupMemberSet),
DavProperty::Principal(PrincipalProperty::GroupMembership),
]
} else if let Some(all_props) = all_props {
let mut props = vec![

View file

@ -22,13 +22,13 @@ use dav_proto::{
use directory::Permission;
use http_proto::{HttpRequest, HttpResponse, HttpSessionData, request::fetch_body};
use hyper::{StatusCode, header};
use jmap_proto::types::collection::Collection;
use crate::{
DavError, DavMethod, DavResource,
card::{
acl::CardAclRequestHandler, copy_move::CardCopyMoveRequestHandler,
delete::CardDeleteRequestHandler, get::CardGetRequestHandler,
mkcol::CardMkColRequestHandler, propfind::CardPropFindRequestHandler,
copy_move::CardCopyMoveRequestHandler, delete::CardDeleteRequestHandler,
get::CardGetRequestHandler, mkcol::CardMkColRequestHandler,
proppatch::CardPropPatchRequestHandler, query::CardQueryRequestHandler,
update::CardUpdateRequestHandler,
},
@ -40,9 +40,8 @@ use crate::{
uri::DavUriResource,
},
file::{
acl::FileAclRequestHandler, copy_move::FileCopyMoveRequestHandler,
delete::FileDeleteRequestHandler, get::FileGetRequestHandler,
mkcol::FileMkColRequestHandler, propfind::HandleFilePropFindRequest,
copy_move::FileCopyMoveRequestHandler, delete::FileDeleteRequestHandler,
get::FileGetRequestHandler, mkcol::FileMkColRequestHandler,
proppatch::FilePropPatchRequestHandler, update::FileUpdateRequestHandler,
},
principal::{matching::PrincipalMatching, propsearch::PrincipalPropSearch},
@ -258,13 +257,8 @@ impl DavRequestDispatcher for Server {
DavMethod::ACL => {
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!(),

View file

@ -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<u32, CalendarPreferences>,
pub name: String,
pub preferences: HashMap<u32, CalendarPreferences, ahash::RandomState>,
pub acls: Vec<AclGrant>,
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<String, ICalendar>,
pub default_alerts_without_time: VecMap<String, ICalendar>,
pub default_alerts_with_time: HashMap<String, ICalendar, ahash::RandomState>,
pub default_alerts_without_time: HashMap<String, ICalendar, ahash::RandomState>,
pub time_zone: Timezone,
}
@ -41,14 +48,17 @@ pub struct CalendarPreferences {
)]
pub struct CalendarEvent {
pub names: Vec<DavName>,
pub display_name: Option<String>,
pub event: ICalendar,
pub user_properties: VecMap<u32, ICalendar>,
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())
}
}
}

View file

@ -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::<u32>()
+ self.size,
},
IndexValue::LogChild { prefix: None },
IndexValue::LogParent {
collection: Collection::AddressBook,
ids: self.names.iter().map(|v| v.parent_id).collect::<Vec<_>>(),
},
]
.into_iter()
}
@ -112,6 +119,15 @@ impl IndexableObject for &ArchivedContactCard {
+ self.names.iter().map(|n| n.name.len() as u32).sum::<u32>()
+ self.size,
},
IndexValue::LogChild { prefix: None },
IndexValue::LogParent {
collection: Collection::AddressBook,
ids: self
.names
.iter()
.map(|v| v.parent_id.to_native())
.collect::<Vec<_>>(),
},
]
.into_iter()
}

View file

@ -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::<Vec<_>>()
.into(),
},
IndexValue::LogChild { prefix: None },
]);
let size = self.size();

View file

@ -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)
}
}

View file

@ -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;

View file

@ -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<u32>) -> &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<u32>) -> &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<u32>) -> &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<Item = (&u32, &Bitmap<ShortId>)> {
self.changed_collections.iter()
pub fn changes(self) -> Option<VecMap<u32, (u64, Bitmap<ShortId>)>> {
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()
}

View file

@ -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)]

View file

@ -94,8 +94,8 @@ pub struct BatchBuilder {
current_account_id: Option<u32>,
current_collection: Option<u8>,
current_document_id: Option<u32>,
changed_collections: VecMap<u32, Bitmap<ShortId>>,
changelog: ChangeLogBuilder,
changes: VecMap<u32, ChangeLogBuilder>,
changed_collections: VecMap<u32, (u64, Bitmap<ShortId>)>,
has_assertions: bool,
batch_size: usize,
batch_ops: usize,