mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2025-10-06 10:35:46 +08:00
CardDAV PROPFIND + multiget REPORT
This commit is contained in:
parent
fac2975a5a
commit
a06c94d45d
36 changed files with 2697 additions and 1338 deletions
|
@ -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)
|
||||
|
|
|
@ -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()),
|
||||
)),
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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/>")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>),
|
||||
|
|
|
@ -223,7 +223,7 @@ pub enum CardCondition {
|
|||
ValidAddressData,
|
||||
NoUidConflict(Href),
|
||||
MaxResourceSize(u32),
|
||||
AddressBoolCollectionLocationOk,
|
||||
AddressBookCollectionLocationOk,
|
||||
}
|
||||
|
||||
impl BaseCondition {
|
||||
|
|
|
@ -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!()
|
||||
}
|
||||
}
|
|
@ -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)]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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!()
|
||||
}
|
||||
}
|
|
@ -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"),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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![
|
||||
|
|
|
@ -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!(),
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Reference in a new issue