Import upstream v0.16.22, stripped
Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f Enterprise-only files removed or emptied: 63 Enterprise-only snippets removed: 117 in 50 files Dangling module declarations removed: 5 Cargo edits turning enterprise off: 14 Verification: clean Enterprise feature gates left for rebuilt features: 19 in 18 files Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
DavError, DavMethod,
|
||||
common::{
|
||||
ETag,
|
||||
lock::{LockRequestHandler, ResourceState},
|
||||
uri::DavUriResource,
|
||||
},
|
||||
};
|
||||
use common::{Server, auth::AccessToken, sharing::EffectiveAcl};
|
||||
use dav_proto::RequestHeaders;
|
||||
use groupware::{
|
||||
DestroyArchive,
|
||||
cache::GroupwareCache,
|
||||
contact::{AddressBook, ContactCard},
|
||||
};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use store::write::{BatchBuilder, ValueClass};
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{AlignedBytes, Archive},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::Acl,
|
||||
collection::{Collection, SyncCollection},
|
||||
field::PrincipalField,
|
||||
};
|
||||
|
||||
pub(crate) trait CardDeleteRequestHandler: Sync + Send {
|
||||
fn handle_card_delete_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
}
|
||||
|
||||
impl CardDeleteRequestHandler for Server {
|
||||
async fn handle_card_delete_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
) -> 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 delete_path = resource
|
||||
.resource
|
||||
.filter(|r| !r.is_empty())
|
||||
.ok_or(DavError::Code(StatusCode::FORBIDDEN))?;
|
||||
let resources = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::AddressBook,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Check resource type
|
||||
let delete_resource = resources
|
||||
.by_path(delete_path)
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
let document_id = delete_resource.document_id();
|
||||
|
||||
// Fetch entry
|
||||
let mut batch = BatchBuilder::new();
|
||||
if delete_resource.is_container() {
|
||||
let book_ = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::AddressBook,
|
||||
document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
|
||||
let book = book_
|
||||
.to_unarchived::<AddressBook>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Validate ACL
|
||||
if !access_token.is_member(account_id)
|
||||
&& !book
|
||||
.inner
|
||||
.acls
|
||||
.effective_acl(access_token)
|
||||
.contains_all([Acl::Delete, Acl::RemoveItems].into_iter())
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
// Validate headers
|
||||
self.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![ResourceState {
|
||||
account_id,
|
||||
collection: Collection::AddressBook,
|
||||
document_id: document_id.into(),
|
||||
etag: book.etag().into(),
|
||||
path: delete_path,
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::DELETE,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Delete addressbook and cards
|
||||
DestroyArchive(book)
|
||||
.delete_with_cards(
|
||||
self,
|
||||
access_token.account_tenant_ids(),
|
||||
account_id,
|
||||
document_id,
|
||||
resources
|
||||
.subtree(delete_path)
|
||||
.filter(|r| !r.is_container())
|
||||
.map(|r| r.document_id())
|
||||
.collect::<Vec<_>>(),
|
||||
resources.format_resource(delete_resource).into(),
|
||||
&mut batch,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Reset default address book id
|
||||
let default_book_id = self
|
||||
.store()
|
||||
.get_value::<u32>(ValueKey {
|
||||
account_id,
|
||||
collection: Collection::Principal.into(),
|
||||
document_id: 0,
|
||||
class: ValueClass::Property(PrincipalField::DefaultAddressBookId.into()),
|
||||
})
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
if default_book_id.is_some_and(|id| id == document_id) {
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::Principal)
|
||||
.with_document(0)
|
||||
.clear(PrincipalField::DefaultAddressBookId);
|
||||
}
|
||||
} else {
|
||||
// Validate ACL
|
||||
let addressbook_id = delete_resource.parent_id().unwrap();
|
||||
if !access_token.is_member(account_id)
|
||||
&& !resources.has_access_to_container(
|
||||
access_token,
|
||||
addressbook_id,
|
||||
Acl::RemoveItems,
|
||||
)
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
let card_ = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::ContactCard,
|
||||
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: Collection::ContactCard,
|
||||
document_id: document_id.into(),
|
||||
etag: card_.etag().into(),
|
||||
path: delete_path,
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::DELETE,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Delete card
|
||||
DestroyArchive(
|
||||
card_
|
||||
.to_unarchived::<ContactCard>()
|
||||
.caused_by(trc::location!())?,
|
||||
)
|
||||
.delete(
|
||||
access_token.account_tenant_ids(),
|
||||
account_id,
|
||||
document_id,
|
||||
addressbook_id,
|
||||
resources.format_resource(delete_resource).into(),
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
|
||||
self.commit_batch(batch).await.caused_by(trc::location!())?;
|
||||
self.notify_task_queue();
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::NO_CONTENT))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
DavError, DavMethod,
|
||||
common::{
|
||||
ETag,
|
||||
lock::{LockRequestHandler, ResourceState},
|
||||
uri::DavUriResource,
|
||||
},
|
||||
};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use dav_proto::{RequestHeaders, schema::property::Rfc1123DateTime};
|
||||
use groupware::{cache::GroupwareCache, contact::ContactCard};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{AlignedBytes, Archive},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::Acl,
|
||||
collection::{Collection, SyncCollection},
|
||||
};
|
||||
|
||||
pub(crate) trait CardGetRequestHandler: Sync + Send {
|
||||
fn handle_card_get_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
is_head: bool,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
}
|
||||
|
||||
impl CardGetRequestHandler for Server {
|
||||
async fn handle_card_get_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
is_head: bool,
|
||||
) -> 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 resources = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::AddressBook,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let resource = resources
|
||||
.by_path(
|
||||
resource_
|
||||
.resource
|
||||
.ok_or(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))?,
|
||||
)
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
if resource.is_container() {
|
||||
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
|
||||
}
|
||||
|
||||
// Validate ACL
|
||||
if !access_token.is_member(account_id)
|
||||
&& !resources.has_access_to_container(
|
||||
access_token,
|
||||
resource.parent_id().unwrap(),
|
||||
Acl::ReadItems,
|
||||
)
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
// Fetch card
|
||||
let card_ = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::ContactCard,
|
||||
resource.document_id(),
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
let card = card_
|
||||
.unarchive::<ContactCard>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Validate headers
|
||||
let etag = card_.etag();
|
||||
self.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![ResourceState {
|
||||
account_id,
|
||||
collection: Collection::ContactCard,
|
||||
document_id: resource.document_id().into(),
|
||||
etag: etag.clone().into(),
|
||||
path: resource_.resource.unwrap(),
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::GET,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let response = HttpResponse::new(StatusCode::OK)
|
||||
.with_content_type("text/vcard; charset=utf-8")
|
||||
.with_etag(etag)
|
||||
.with_last_modified(Rfc1123DateTime::new(i64::from(card.modified)).to_string());
|
||||
|
||||
let mut vcard = String::with_capacity(128);
|
||||
let _ = card.card.write_to(
|
||||
&mut vcard,
|
||||
headers
|
||||
.vcard_version
|
||||
.unwrap_or(self.core.groupware.vcard_version),
|
||||
);
|
||||
|
||||
if !is_head {
|
||||
Ok(response.with_binary_body(vcard))
|
||||
} else {
|
||||
Ok(response.with_content_length(vcard.len()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::proppatch::CardPropPatchRequestHandler;
|
||||
use crate::{
|
||||
DavError, DavMethod, PropStatBuilder,
|
||||
common::{
|
||||
ExtractETag,
|
||||
lock::{LockRequestHandler, ResourceState},
|
||||
uri::DavUriResource,
|
||||
},
|
||||
};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use dav_proto::{
|
||||
RequestHeaders, Return,
|
||||
schema::{Namespace, request::MkCol, response::MkColResponse},
|
||||
};
|
||||
use groupware::{
|
||||
cache::GroupwareCache,
|
||||
contact::{AddressBook, AddressBookPreferences},
|
||||
};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use store::write::BatchBuilder;
|
||||
use trc::AddContext;
|
||||
use types::collection::{Collection, SyncCollection};
|
||||
|
||||
pub(crate) trait CardMkColRequestHandler: Sync + Send {
|
||||
fn handle_card_mkcol_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
request: Option<MkCol>,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
}
|
||||
|
||||
impl CardMkColRequestHandler for Server {
|
||||
async fn handle_card_mkcol_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
request: Option<MkCol>,
|
||||
) -> 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 name = resource
|
||||
.resource
|
||||
.ok_or(DavError::Code(StatusCode::FORBIDDEN))?;
|
||||
if !access_token.is_member(account_id) {
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
} else if name.contains('/')
|
||||
|| self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::AddressBook,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.by_path(name)
|
||||
.is_some()
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
|
||||
}
|
||||
|
||||
// Validate headers
|
||||
self.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![ResourceState {
|
||||
account_id,
|
||||
collection: resource.collection,
|
||||
document_id: Some(u32::MAX),
|
||||
path: name,
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::MKCOL,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Build file container
|
||||
let mut book = AddressBook {
|
||||
name: name.to_string(),
|
||||
preferences: vec![AddressBookPreferences {
|
||||
account_id,
|
||||
name: "Address Book".to_string(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Apply MKCOL properties
|
||||
let mut return_prop_stat = None;
|
||||
if let Some(mkcol) = request {
|
||||
let mut prop_stat = PropStatBuilder::default();
|
||||
if !self.apply_addressbook_properties(
|
||||
access_token.personal_id(account_id, Collection::AddressBook),
|
||||
&mut book,
|
||||
false,
|
||||
mkcol.props,
|
||||
&mut prop_stat,
|
||||
) {
|
||||
return Ok(HttpResponse::new(StatusCode::FORBIDDEN).with_xml_body(
|
||||
MkColResponse::new(prop_stat.build())
|
||||
.with_namespace(Namespace::CardDav)
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
if headers.ret != Return::Minimal {
|
||||
return_prop_stat = Some(prop_stat);
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare write batch
|
||||
let mut batch = BatchBuilder::new();
|
||||
let document_id = self
|
||||
.store()
|
||||
.assign_document_ids(account_id, Collection::AddressBook, 1)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
book.insert(
|
||||
access_token.account_tenant_ids(),
|
||||
account_id,
|
||||
document_id,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?;
|
||||
let etag = batch.etag();
|
||||
self.commit_batch(batch).await.caused_by(trc::location!())?;
|
||||
|
||||
if let Some(prop_stat) = return_prop_stat {
|
||||
Ok(HttpResponse::new(StatusCode::CREATED)
|
||||
.with_xml_body(
|
||||
MkColResponse::new(prop_stat.build())
|
||||
.with_namespace(Namespace::CardDav)
|
||||
.to_string(),
|
||||
)
|
||||
.with_etag_opt(etag))
|
||||
} else {
|
||||
Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{DavError, DavErrorCondition};
|
||||
use common::{DavResources, Server};
|
||||
use dav_proto::schema::{
|
||||
property::{CardDavProperty, DavProperty, WebDavProperty},
|
||||
response::CardCondition,
|
||||
};
|
||||
use hyper::StatusCode;
|
||||
use trc::AddContext;
|
||||
use types::{collection::Collection, field::ContactField};
|
||||
|
||||
pub mod copy_move;
|
||||
pub mod delete;
|
||||
pub mod get;
|
||||
pub mod mkcol;
|
||||
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 {
|
||||
properties: Vec::new(),
|
||||
version: None,
|
||||
}),
|
||||
];
|
||||
|
||||
pub(crate) async fn assert_is_unique_uid(
|
||||
server: &Server,
|
||||
resources: &DavResources,
|
||||
account_id: u32,
|
||||
addressbook_id: u32,
|
||||
uid: Option<&str>,
|
||||
) -> crate::Result<()> {
|
||||
if let Some(uid) = uid {
|
||||
let hits = server
|
||||
.document_ids_matching(
|
||||
account_id,
|
||||
Collection::ContactCard,
|
||||
ContactField::Uid,
|
||||
uid.as_bytes(),
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
if !hits.is_empty() {
|
||||
for path in resources.children(addressbook_id) {
|
||||
if hits.contains(path.document_id()) {
|
||||
return Err(DavError::Condition(DavErrorCondition::new(
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
CardCondition::NoUidConflict(resources.format_resource(path).into()),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,488 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
DavError, DavMethod, PropStatBuilder,
|
||||
common::{
|
||||
ETag, ExtractETag,
|
||||
lock::{LockRequestHandler, ResourceState},
|
||||
uri::DavUriResource,
|
||||
},
|
||||
};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use dav_proto::{
|
||||
RequestHeaders, Return,
|
||||
schema::{
|
||||
Namespace,
|
||||
property::{CardDavProperty, DavProperty, DavValue, ResourceType, WebDavProperty},
|
||||
request::{DavPropertyValue, PropertyUpdate},
|
||||
response::{BaseCondition, MultiStatus, Response},
|
||||
},
|
||||
};
|
||||
use groupware::{
|
||||
cache::GroupwareCache,
|
||||
contact::{AddressBook, ContactCard},
|
||||
};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use store::write::BatchBuilder;
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{AlignedBytes, Archive},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::Acl,
|
||||
collection::{Collection, SyncCollection},
|
||||
};
|
||||
|
||||
pub(crate) trait CardPropPatchRequestHandler: Sync + Send {
|
||||
fn handle_card_proppatch_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
request: PropertyUpdate,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
|
||||
fn apply_addressbook_properties(
|
||||
&self,
|
||||
personal_id: u32,
|
||||
address_book: &mut AddressBook,
|
||||
is_update: bool,
|
||||
properties: Vec<DavPropertyValue>,
|
||||
items: &mut PropStatBuilder,
|
||||
) -> bool;
|
||||
|
||||
fn apply_card_properties(
|
||||
&self,
|
||||
card: &mut ContactCard,
|
||||
is_update: bool,
|
||||
properties: Vec<DavPropertyValue>,
|
||||
items: &mut PropStatBuilder,
|
||||
) -> bool;
|
||||
}
|
||||
|
||||
impl CardPropPatchRequestHandler for Server {
|
||||
async fn handle_card_proppatch_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
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(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::AddressBook,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let resource = resource_
|
||||
.resource
|
||||
.and_then(|r| resources.by_path(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
|
||||
};
|
||||
|
||||
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::Modify, resource.document_id())
|
||||
} else {
|
||||
(Acl::ModifyItems, resource.parent_id().unwrap())
|
||||
};
|
||||
|
||||
if !resources.has_access_to_container(access_token, document_id, acl) {
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch archive
|
||||
let archive = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::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 = PropStatBuilder::default();
|
||||
|
||||
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!())?;
|
||||
let personal_id = access_token.personal_id(account_id, Collection::AddressBook);
|
||||
|
||||
// Remove properties
|
||||
if !request.set_first && !request.remove.is_empty() {
|
||||
remove_addressbook_properties(
|
||||
personal_id,
|
||||
&mut new_book,
|
||||
std::mem::take(&mut request.remove),
|
||||
&mut items,
|
||||
);
|
||||
}
|
||||
|
||||
// Set properties
|
||||
is_success = self.apply_addressbook_properties(
|
||||
personal_id,
|
||||
&mut new_book,
|
||||
true,
|
||||
request.set,
|
||||
&mut items,
|
||||
);
|
||||
|
||||
// Remove properties
|
||||
if is_success && !request.remove.is_empty() {
|
||||
remove_addressbook_properties(
|
||||
personal_id,
|
||||
&mut new_book,
|
||||
request.remove,
|
||||
&mut items,
|
||||
);
|
||||
}
|
||||
|
||||
if is_success {
|
||||
new_book
|
||||
.update(
|
||||
access_token.account_tenant_ids(),
|
||||
book,
|
||||
account_id,
|
||||
document_id,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.etag()
|
||||
} 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 {
|
||||
new_card
|
||||
.update(
|
||||
access_token.account_tenant_ids(),
|
||||
card,
|
||||
account_id,
|
||||
document_id,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.etag()
|
||||
} 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.build())])
|
||||
.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(
|
||||
&self,
|
||||
personal_id: u32,
|
||||
address_book: &mut AddressBook,
|
||||
is_update: bool,
|
||||
properties: Vec<DavPropertyValue>,
|
||||
items: &mut PropStatBuilder,
|
||||
) -> 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.groupware.live_property_size {
|
||||
address_book.preferences_mut(personal_id).name = name;
|
||||
items.insert_ok(property.property);
|
||||
} else {
|
||||
items.insert_error_with_description(
|
||||
property.property,
|
||||
StatusCode::INSUFFICIENT_STORAGE,
|
||||
"Property value is too long",
|
||||
);
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
(
|
||||
DavProperty::CardDav(CardDavProperty::AddressbookDescription),
|
||||
DavValue::String(name),
|
||||
) => {
|
||||
if name.len() <= self.core.groupware.live_property_size {
|
||||
address_book.preferences_mut(personal_id).description = Some(name);
|
||||
items.insert_ok(property.property);
|
||||
} else {
|
||||
items.insert_error_with_description(
|
||||
property.property,
|
||||
StatusCode::INSUFFICIENT_STORAGE,
|
||||
"Property value is too long",
|
||||
);
|
||||
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
(DavProperty::WebDav(WebDavProperty::CreationDate), DavValue::Timestamp(dt)) => {
|
||||
address_book.created = dt;
|
||||
items.insert_ok(property.property);
|
||||
}
|
||||
(
|
||||
DavProperty::WebDav(WebDavProperty::ResourceType),
|
||||
DavValue::ResourceTypes(types),
|
||||
) => {
|
||||
if !types.0.iter().all(|rt| {
|
||||
matches!(rt, ResourceType::Collection | ResourceType::AddressBook)
|
||||
}) {
|
||||
items.insert_precondition_failed(
|
||||
property.property,
|
||||
StatusCode::FORBIDDEN,
|
||||
BaseCondition::ValidResourceType,
|
||||
);
|
||||
has_errors = true;
|
||||
} else {
|
||||
items.insert_ok(property.property);
|
||||
}
|
||||
}
|
||||
(DavProperty::DeadProperty(dead), DavValue::DeadProperty(values))
|
||||
if self.core.groupware.dead_property_size.is_some() =>
|
||||
{
|
||||
if is_update {
|
||||
address_book.dead_properties.remove_element(dead);
|
||||
}
|
||||
|
||||
if address_book.dead_properties.size() + values.size() + dead.size()
|
||||
< self.core.groupware.dead_property_size.unwrap()
|
||||
{
|
||||
address_book
|
||||
.dead_properties
|
||||
.add_element(dead.clone(), values.0);
|
||||
items.insert_ok(property.property);
|
||||
} else {
|
||||
items.insert_error_with_description(
|
||||
property.property,
|
||||
StatusCode::INSUFFICIENT_STORAGE,
|
||||
"Property value is too long",
|
||||
);
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
(_, DavValue::Null) => {
|
||||
items.insert_ok(property.property);
|
||||
}
|
||||
_ => {
|
||||
items.insert_error_with_description(
|
||||
property.property,
|
||||
StatusCode::CONFLICT,
|
||||
"Property cannot be modified",
|
||||
);
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
!has_errors
|
||||
}
|
||||
|
||||
fn apply_card_properties(
|
||||
&self,
|
||||
card: &mut ContactCard,
|
||||
is_update: bool,
|
||||
properties: Vec<DavPropertyValue>,
|
||||
items: &mut PropStatBuilder,
|
||||
) -> 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.groupware.live_property_size {
|
||||
card.display_name = Some(name);
|
||||
items.insert_ok(property.property);
|
||||
} else {
|
||||
items.insert_error_with_description(
|
||||
property.property,
|
||||
StatusCode::INSUFFICIENT_STORAGE,
|
||||
"Property value is too long",
|
||||
);
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
(DavProperty::WebDav(WebDavProperty::CreationDate), DavValue::Timestamp(dt)) => {
|
||||
card.created = dt;
|
||||
items.insert_ok(property.property);
|
||||
}
|
||||
(DavProperty::DeadProperty(dead), DavValue::DeadProperty(values))
|
||||
if self.core.groupware.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.groupware.dead_property_size.unwrap()
|
||||
{
|
||||
card.dead_properties.add_element(dead.clone(), values.0);
|
||||
items.insert_ok(property.property);
|
||||
} else {
|
||||
items.insert_error_with_description(
|
||||
property.property,
|
||||
StatusCode::INSUFFICIENT_STORAGE,
|
||||
"Property value is too long",
|
||||
);
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
(_, DavValue::Null) => {
|
||||
items.insert_ok(property.property);
|
||||
}
|
||||
_ => {
|
||||
items.insert_error_with_description(
|
||||
property.property,
|
||||
StatusCode::CONFLICT,
|
||||
"Property cannot be modified",
|
||||
);
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
!has_errors
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_card_properties(
|
||||
card: &mut ContactCard,
|
||||
properties: Vec<DavProperty>,
|
||||
items: &mut PropStatBuilder,
|
||||
) {
|
||||
for property in properties {
|
||||
match &property {
|
||||
DavProperty::WebDav(WebDavProperty::DisplayName) => {
|
||||
card.display_name = None;
|
||||
items.insert_with_status(property, StatusCode::NO_CONTENT);
|
||||
}
|
||||
DavProperty::DeadProperty(dead) => {
|
||||
card.dead_properties.remove_element(dead);
|
||||
items.insert_with_status(property, StatusCode::NO_CONTENT);
|
||||
}
|
||||
_ => {
|
||||
items.insert_error_with_description(
|
||||
property,
|
||||
StatusCode::CONFLICT,
|
||||
"Property cannot be deleted",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_addressbook_properties(
|
||||
personal_id: u32,
|
||||
book: &mut AddressBook,
|
||||
properties: Vec<DavProperty>,
|
||||
items: &mut PropStatBuilder,
|
||||
) {
|
||||
for property in properties {
|
||||
match &property {
|
||||
DavProperty::CardDav(CardDavProperty::AddressbookDescription) => {
|
||||
book.preferences_mut(personal_id).description = None;
|
||||
items.insert_with_status(property, StatusCode::NO_CONTENT);
|
||||
}
|
||||
DavProperty::WebDav(WebDavProperty::DisplayName) => {
|
||||
book.preferences_mut(personal_id).name.clear();
|
||||
items.insert_with_status(property, StatusCode::NO_CONTENT);
|
||||
}
|
||||
DavProperty::DeadProperty(dead) => {
|
||||
book.dead_properties.remove_element(dead);
|
||||
items.insert_with_status(property, StatusCode::NO_CONTENT);
|
||||
}
|
||||
_ => {
|
||||
items.insert_error_with_description(
|
||||
property,
|
||||
StatusCode::CONFLICT,
|
||||
"Property cannot be deleted",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
DavError,
|
||||
common::{
|
||||
AddressbookFilter, DavQuery,
|
||||
propfind::{PropFindItem, PropFindRequestHandler},
|
||||
uri::DavUriResource,
|
||||
},
|
||||
};
|
||||
use calcard::vcard::{
|
||||
ArchivedVCard, ArchivedVCardEntry, ArchivedVCardParameter, VCardParameterName, VCardProperty,
|
||||
VCardVersion,
|
||||
};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use dav_proto::{
|
||||
RequestHeaders,
|
||||
schema::{
|
||||
property::CardDavPropertyName,
|
||||
request::{AddressbookQuery, Filter, FilterOp, VCardPropertyWithGroup},
|
||||
response::MultiStatus,
|
||||
},
|
||||
};
|
||||
use groupware::cache::GroupwareCache;
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use std::fmt::Write;
|
||||
use trc::AddContext;
|
||||
use types::{acl::Acl, collection::SyncCollection};
|
||||
|
||||
pub(crate) trait CardQueryRequestHandler: Sync + Send {
|
||||
fn handle_card_query_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
request: AddressbookQuery,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
}
|
||||
|
||||
impl CardQueryRequestHandler for Server {
|
||||
async fn handle_card_query_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
request: AddressbookQuery,
|
||||
) -> 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 resources = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::AddressBook,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let Some(resource) = resources.by_path(
|
||||
resource_
|
||||
.resource
|
||||
.ok_or(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))?,
|
||||
) else {
|
||||
return Ok(HttpResponse::new(StatusCode::MULTI_STATUS)
|
||||
.with_xml_body(MultiStatus::not_found(headers.uri).to_string()));
|
||||
};
|
||||
if !resource.is_container() {
|
||||
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
|
||||
}
|
||||
|
||||
// Obtain shared ids
|
||||
let shared_ids = if !access_token.is_member(account_id) {
|
||||
resources
|
||||
.shared_items(access_token, [Acl::ReadItems], false)
|
||||
.into()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Obtain document ids in folder
|
||||
let mut items = Vec::with_capacity(16);
|
||||
for resource in resources.children(resource.document_id()) {
|
||||
if shared_ids
|
||||
.as_ref()
|
||||
.is_none_or(|ids| ids.contains(resource.document_id()))
|
||||
{
|
||||
items.push(PropFindItem::new(
|
||||
resources.format_resource(resource),
|
||||
account_id,
|
||||
resource,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
self.handle_dav_query(
|
||||
access_token,
|
||||
DavQuery::addressbook_query(request, items, headers),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn vcard_query(card: &ArchivedVCard, filters: &AddressbookFilter) -> bool {
|
||||
let mut is_all = true;
|
||||
let mut matches_one = false;
|
||||
|
||||
for filter in filters {
|
||||
match filter {
|
||||
Filter::AnyOf => {
|
||||
is_all = false;
|
||||
}
|
||||
Filter::AllOf => {
|
||||
is_all = true;
|
||||
}
|
||||
Filter::Property { prop, op, .. } => {
|
||||
let mut properties = find_properties(card, prop).peekable();
|
||||
let result = if properties.peek().is_some() {
|
||||
properties.any(|entry| match op {
|
||||
FilterOp::Exists => true,
|
||||
FilterOp::Undefined => false,
|
||||
FilterOp::TextMatch(text_match) => {
|
||||
let mut matched_any = false;
|
||||
|
||||
for value in entry.values.iter() {
|
||||
if let Some(text) = value.as_text()
|
||||
&& text_match.matches(text)
|
||||
{
|
||||
matched_any = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
matched_any
|
||||
}
|
||||
FilterOp::TimeRange(_) => false,
|
||||
})
|
||||
} else {
|
||||
matches!(op, FilterOp::Undefined)
|
||||
};
|
||||
|
||||
if result {
|
||||
matches_one = true;
|
||||
} else if is_all {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Filter::Parameter {
|
||||
prop, param, op, ..
|
||||
} => {
|
||||
let mut properties = find_properties(card, prop)
|
||||
.filter_map(|entry| find_parameter(entry, param))
|
||||
.peekable();
|
||||
let result = if properties.peek().is_some() {
|
||||
properties.any(|entry| match op {
|
||||
FilterOp::Exists => true,
|
||||
FilterOp::Undefined => false,
|
||||
FilterOp::TextMatch(text_match) => {
|
||||
if let Some(text) = entry.value.as_text() {
|
||||
text_match.matches(text)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
FilterOp::TimeRange(_) => false,
|
||||
})
|
||||
} else {
|
||||
matches!(op, FilterOp::Undefined)
|
||||
};
|
||||
|
||||
if result {
|
||||
matches_one = true;
|
||||
} else if is_all {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Filter::Component { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
is_all || matches_one
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn find_properties<'x>(
|
||||
card: &'x ArchivedVCard,
|
||||
prop: &VCardPropertyWithGroup,
|
||||
) -> impl Iterator<Item = &'x ArchivedVCardEntry> {
|
||||
card.entries
|
||||
.iter()
|
||||
.filter(move |entry| entry.name == prop.name && entry.group == prop.group)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn find_parameter<'x>(
|
||||
entry: &'x ArchivedVCardEntry,
|
||||
name: &VCardParameterName,
|
||||
) -> Option<&'x ArchivedVCardParameter> {
|
||||
entry.params.iter().find(|param| param.name == *name)
|
||||
}
|
||||
|
||||
pub(crate) fn serialize_vcard_with_props(
|
||||
card: &ArchivedVCard,
|
||||
props: &[CardDavPropertyName],
|
||||
version: VCardVersion,
|
||||
) -> String {
|
||||
let mut vcard = String::with_capacity(128);
|
||||
if !props.is_empty() {
|
||||
let _ = write!(&mut vcard, "BEGIN:VCARD\r\n");
|
||||
let is_v4 = matches!(version, VCardVersion::V4_0);
|
||||
|
||||
for entry in card.entries.iter() {
|
||||
for item in props {
|
||||
if entry.name == item.name && entry.group == item.group {
|
||||
if item.name != VCardProperty::Version {
|
||||
let _ = entry.write_to(&mut vcard, !item.no_value, is_v4);
|
||||
} else {
|
||||
let _ = write!(&mut vcard, "VERSION:{version}\r\n");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = write!(&mut vcard, "END:VCARD\r\n");
|
||||
} else {
|
||||
let _ = card.write_to(&mut vcard, version);
|
||||
}
|
||||
|
||||
vcard
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::assert_is_unique_uid;
|
||||
use crate::{
|
||||
DavError, DavErrorCondition, DavMethod,
|
||||
common::{
|
||||
ETag, ExtractETag,
|
||||
lock::{LockRequestHandler, ResourceState},
|
||||
uri::DavUriResource,
|
||||
},
|
||||
file::DavFileResource,
|
||||
fix_percent_encoding,
|
||||
};
|
||||
use calcard::{Entry, Parser};
|
||||
use common::{DavName, Server, auth::AccessToken};
|
||||
use dav_proto::{
|
||||
RequestHeaders, Return,
|
||||
schema::{property::Rfc1123DateTime, response::CardCondition},
|
||||
};
|
||||
use groupware::{cache::GroupwareCache, contact::ContactCard};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use store::write::BatchBuilder;
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{AlignedBytes, Archive},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::Acl,
|
||||
collection::{Collection, SyncCollection},
|
||||
};
|
||||
|
||||
pub(crate) trait CardUpdateRequestHandler: Sync + Send {
|
||||
fn handle_card_update_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
bytes: Vec<u8>,
|
||||
is_patch: bool,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
}
|
||||
|
||||
impl CardUpdateRequestHandler for Server {
|
||||
async fn handle_card_update_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
bytes: Vec<u8>,
|
||||
_is_patch: bool,
|
||||
) -> 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 resources = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::AddressBook,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let resource_name = fix_percent_encoding(
|
||||
resource
|
||||
.resource
|
||||
.ok_or(DavError::Code(StatusCode::CONFLICT))?,
|
||||
);
|
||||
|
||||
if bytes.len() > self.core.groupware.max_vcard_size {
|
||||
return Err(DavError::Condition(DavErrorCondition::new(
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
CardCondition::MaxResourceSize(self.core.groupware.max_vcard_size as u32),
|
||||
)));
|
||||
}
|
||||
let vcard_raw = std::str::from_utf8(&bytes).map_err(|_| {
|
||||
DavError::Condition(
|
||||
DavErrorCondition::new(
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
CardCondition::SupportedAddressData,
|
||||
)
|
||||
.with_details("The request body is not valid UTF-8."),
|
||||
)
|
||||
})?;
|
||||
|
||||
let vcard = match Parser::new(vcard_raw).strict().entry() {
|
||||
Entry::VCard(vcard) => vcard,
|
||||
_ => {
|
||||
return Err(DavError::Condition(
|
||||
DavErrorCondition::new(
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
CardCondition::SupportedAddressData,
|
||||
)
|
||||
.with_details("Failed to parse vCard data."),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(resource) = resources.by_path(resource_name.as_ref()) {
|
||||
if resource.is_container() {
|
||||
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
|
||||
}
|
||||
|
||||
// Validate ACL
|
||||
let parent_id = resource.parent_id().unwrap();
|
||||
let document_id = resource.document_id();
|
||||
if !access_token.is_member(account_id)
|
||||
&& !resources.has_access_to_container(access_token, parent_id, Acl::ModifyItems)
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
// Update
|
||||
let card_ = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::ContactCard,
|
||||
document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
let card = card_
|
||||
.to_unarchived::<ContactCard>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Validate headers
|
||||
match self
|
||||
.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![ResourceState {
|
||||
account_id,
|
||||
collection: Collection::ContactCard,
|
||||
document_id: Some(document_id),
|
||||
etag: card.etag().into(),
|
||||
path: resource_name.as_ref(),
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::PUT,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(DavError::Code(StatusCode::PRECONDITION_FAILED))
|
||||
if headers.ret == Return::Representation =>
|
||||
{
|
||||
let mut vcard = String::with_capacity(128);
|
||||
let _ = card.inner.card.write_to(
|
||||
&mut vcard,
|
||||
headers
|
||||
.vcard_version
|
||||
.unwrap_or(self.core.groupware.vcard_version),
|
||||
);
|
||||
|
||||
return Ok(HttpResponse::new(StatusCode::PRECONDITION_FAILED)
|
||||
.with_content_type("text/vcard; charset=utf-8")
|
||||
.with_etag(card.etag())
|
||||
.with_last_modified(
|
||||
Rfc1123DateTime::new(i64::from(card.inner.modified)).to_string(),
|
||||
)
|
||||
.with_header("Preference-Applied", "return=representation")
|
||||
.with_binary_body(vcard));
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
|
||||
// Validate UID
|
||||
match (card.inner.card.uid(), vcard.uid()) {
|
||||
(Some(old_uid), Some(new_uid)) if old_uid == new_uid => {}
|
||||
(None, None) | (None, Some(_)) => {}
|
||||
_ => {
|
||||
return Err(DavError::Condition(DavErrorCondition::new(
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
CardCondition::NoUidConflict(resources.format_resource(resource).into()),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
// Validate quota
|
||||
let extra_bytes =
|
||||
(bytes.len() as u64).saturating_sub(u32::from(card.inner.size) as u64);
|
||||
if extra_bytes > 0 {
|
||||
self.has_available_quota(self.account(account_id).await?.as_ref(), extra_bytes)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Build node
|
||||
let mut new_card = card
|
||||
.deserialize::<ContactCard>()
|
||||
.caused_by(trc::location!())?;
|
||||
new_card.size = bytes.len() as u32;
|
||||
new_card.card = vcard;
|
||||
|
||||
// Prepare write batch
|
||||
let mut batch = BatchBuilder::new();
|
||||
let etag = new_card
|
||||
.update(
|
||||
access_token.account_tenant_ids(),
|
||||
card,
|
||||
account_id,
|
||||
document_id,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.etag();
|
||||
self.commit_batch(batch).await.caused_by(trc::location!())?;
|
||||
self.notify_task_queue();
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::NO_CONTENT).with_etag_opt(etag))
|
||||
} else if let Some((Some(parent), name)) = resources.map_parent(resource_name.as_ref()) {
|
||||
if !parent.is_container() {
|
||||
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
|
||||
}
|
||||
|
||||
// Validate ACL
|
||||
if !access_token.is_member(account_id)
|
||||
&& !resources.has_access_to_container(
|
||||
access_token,
|
||||
parent.document_id(),
|
||||
Acl::AddItems,
|
||||
)
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
// Validate headers
|
||||
self.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![ResourceState {
|
||||
account_id,
|
||||
collection: resource.collection,
|
||||
document_id: Some(u32::MAX),
|
||||
path: resource_name.as_ref(),
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::PUT,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Validate UID
|
||||
assert_is_unique_uid(
|
||||
self,
|
||||
&resources,
|
||||
account_id,
|
||||
parent.document_id(),
|
||||
vcard.uid(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Validate quota
|
||||
if !bytes.is_empty() {
|
||||
self.has_available_quota(
|
||||
self.account(account_id).await?.as_ref(),
|
||||
bytes.len() as u64,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Build node
|
||||
let card = ContactCard {
|
||||
names: vec![DavName {
|
||||
name: name.to_string(),
|
||||
parent_id: parent.document_id(),
|
||||
}],
|
||||
card: vcard,
|
||||
size: bytes.len() as u32,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Prepare write batch
|
||||
let mut batch = BatchBuilder::new();
|
||||
let document_id = self
|
||||
.store()
|
||||
.assign_document_ids(account_id, Collection::ContactCard, 1)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let etag = card
|
||||
.insert(
|
||||
access_token.account_tenant_ids(),
|
||||
account_id,
|
||||
document_id,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.etag();
|
||||
self.commit_batch(batch).await.caused_by(trc::location!())?;
|
||||
self.notify_task_queue();
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag))
|
||||
} else {
|
||||
Err(DavError::Code(StatusCode::CONFLICT))?
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user