1809 lines
75 KiB
Rust
1809 lines
75 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
|
*/
|
|
|
|
use super::{
|
|
ArchivedResource, DavCollection, DavQuery, DavQueryFilter, ETag, SyncType,
|
|
acl::{DavAclHandler, Privileges},
|
|
lock::{LockData, build_lock_key},
|
|
uri::{UriResource, Urn},
|
|
};
|
|
use crate::{
|
|
DavError, DavErrorCondition,
|
|
calendar::{
|
|
CALENDAR_CONTAINER_PROPS, CALENDAR_ITEM_PROPS,
|
|
query::{CalendarQueryHandler, try_parse_tz},
|
|
},
|
|
card::{
|
|
CARD_CONTAINER_PROPS, CARD_ITEM_PROPS,
|
|
query::{serialize_vcard_with_props, vcard_query},
|
|
},
|
|
common::{DavQueryResource, acl::current_user_privilege_set, uri::DavUriResource},
|
|
file::{FILE_CONTAINER_PROPS, FILE_ITEM_PROPS},
|
|
principal::{
|
|
CurrentUserPrincipal,
|
|
propfind::{PrincipalPropFind, build_home_set},
|
|
},
|
|
};
|
|
use calcard::{common::timezone::Tz, icalendar::ICalendarComponentType};
|
|
use common::{
|
|
DavResourcePath, DavResources, Server,
|
|
auth::{AccessToken, AccountCache},
|
|
};
|
|
use dav_proto::{
|
|
Depth, RequestHeaders,
|
|
parser::header::dav_base_uri,
|
|
requests::NsDeadProperty,
|
|
schema::{
|
|
Collation, Namespace,
|
|
property::{
|
|
ActiveLock, CalDavProperty, CardDavProperty, Comp, DavProperty, DavValue,
|
|
PrincipalProperty, Privilege, ReportSet, ResourceType, Rfc1123DateTime,
|
|
SupportedCollation, SupportedLock, WebDavProperty,
|
|
},
|
|
request::{DavDeadProperty, DavPropertyValue, PropFind},
|
|
response::{
|
|
AclRestrictions, BaseCondition, Href, List, MultiStatus, PropStat, Response,
|
|
SupportedPrivilege,
|
|
},
|
|
},
|
|
};
|
|
use groupware::calendar::{SCHEDULE_INBOX_ID, SupportedComponent};
|
|
use groupware::{
|
|
DavCalendarResource, DavResourceName, cache::GroupwareCache, calendar::ArchivedTimezone,
|
|
};
|
|
use http_proto::HttpResponse;
|
|
use hyper::StatusCode;
|
|
use registry::schema::{enums::Permission, prelude::ObjectType};
|
|
use std::sync::Arc;
|
|
use store::{
|
|
ValueKey,
|
|
registry::RegistryQuery,
|
|
write::{AlignedBytes, Archive},
|
|
};
|
|
use store::{
|
|
ahash::AHashMap,
|
|
query::log::{Change, Query},
|
|
roaring::RoaringBitmap,
|
|
};
|
|
use trc::AddContext;
|
|
use types::{
|
|
acl::Acl,
|
|
collection::{Collection, SyncCollection},
|
|
dead_property::DeadProperty,
|
|
};
|
|
use utils::map::bitmap::Bitmap;
|
|
|
|
pub(crate) trait PropFindRequestHandler: Sync + Send {
|
|
fn handle_propfind_request(
|
|
&self,
|
|
access_token: &AccessToken,
|
|
headers: &RequestHeaders<'_>,
|
|
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,
|
|
account_id: u32,
|
|
) -> impl Future<Output = trc::Result<PropFindAccountQuota>> + Send;
|
|
}
|
|
|
|
pub(crate) struct PropFindData {
|
|
pub accounts: AHashMap<u32, PropFindAccountData>,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub(crate) struct PropFindAccountData {
|
|
pub resources: Option<Arc<DavResources>>,
|
|
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,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) struct PropFindItem {
|
|
pub name: String,
|
|
pub account_id: u32,
|
|
pub document_id: u32,
|
|
pub parent_id: Option<u32>,
|
|
pub is_container: bool,
|
|
}
|
|
|
|
impl PropFindRequestHandler for Server {
|
|
async fn handle_propfind_request(
|
|
&self,
|
|
access_token: &AccessToken,
|
|
headers: &RequestHeaders<'_>,
|
|
request: PropFind,
|
|
) -> crate::Result<HttpResponse> {
|
|
// Validate URI
|
|
let resource = self.validate_uri(access_token, headers.uri).await?;
|
|
|
|
// Reject Infinity depth for certain queries
|
|
let return_children = match headers.depth {
|
|
Depth::One | Depth::None => true,
|
|
Depth::Zero => false,
|
|
Depth::Infinity => match resource.collection {
|
|
Collection::Principal => true,
|
|
Collection::Calendar | Collection::AddressBook
|
|
if resource.account_id.is_some() && resource.resource.is_some() =>
|
|
{
|
|
true
|
|
}
|
|
Collection::CalendarEventNotification if resource.account_id.is_some() => true,
|
|
_ => {
|
|
return Err(DavErrorCondition::new(
|
|
StatusCode::FORBIDDEN,
|
|
BaseCondition::PropFindFiniteDepth,
|
|
)
|
|
.into());
|
|
}
|
|
},
|
|
};
|
|
|
|
// List shared resources
|
|
if let Some(account_id) = resource.account_id {
|
|
match resource.collection {
|
|
Collection::FileNode
|
|
| Collection::Calendar
|
|
| Collection::AddressBook
|
|
| Collection::CalendarEventNotification => {
|
|
// Validate permissions
|
|
access_token.enforce_permission(match resource.collection {
|
|
Collection::FileNode => Permission::DavFilePropFind,
|
|
Collection::Calendar
|
|
| Collection::CalendarEvent
|
|
| Collection::CalendarEventNotification => Permission::DavCalPropFind,
|
|
Collection::AddressBook | Collection::ContactCard => {
|
|
Permission::DavCardPropFind
|
|
}
|
|
_ => unreachable!(),
|
|
})?;
|
|
|
|
self.handle_dav_query(
|
|
access_token,
|
|
DavQuery::propfind(
|
|
UriResource::new_owned(
|
|
resource.collection,
|
|
account_id,
|
|
resource.resource,
|
|
),
|
|
request,
|
|
headers,
|
|
),
|
|
)
|
|
.await
|
|
}
|
|
Collection::Principal => {
|
|
let mut response = MultiStatus::new(Vec::with_capacity(16));
|
|
|
|
if resource.resource.is_some() {
|
|
response.add_response(Response::new_status(
|
|
[headers.uri.to_string()],
|
|
StatusCode::NOT_FOUND,
|
|
));
|
|
} else if access_token.has_account_access(account_id)
|
|
|| (self.core.groupware.allow_directory_query
|
|
&& access_token.has_permission(Permission::DavPrincipalList))
|
|
|| access_token.has_permission(Permission::SysAccountQuery)
|
|
{
|
|
self.prepare_principal_propfind_response(
|
|
access_token,
|
|
Collection::Principal,
|
|
[account_id].into_iter(),
|
|
&request,
|
|
&mut response,
|
|
)
|
|
.await?;
|
|
} else {
|
|
response.add_response(Response::new_status(
|
|
[headers.uri.to_string()],
|
|
StatusCode::FORBIDDEN,
|
|
));
|
|
}
|
|
|
|
Ok(HttpResponse::new(StatusCode::MULTI_STATUS)
|
|
.with_xml_body(response.to_string()))
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
} else {
|
|
let mut response = MultiStatus::new(Vec::with_capacity(16));
|
|
|
|
// Add container info
|
|
if !headers.depth_no_root {
|
|
add_base_collection_response(
|
|
self,
|
|
&request,
|
|
resource.collection,
|
|
access_token,
|
|
&mut response,
|
|
)
|
|
.await?;
|
|
}
|
|
|
|
if return_children {
|
|
let ids = if !matches!(resource.collection, Collection::Principal) {
|
|
// Validate permissions
|
|
access_token.enforce_permission(match resource.collection {
|
|
Collection::FileNode => Permission::DavFilePropFind,
|
|
Collection::Calendar
|
|
| Collection::CalendarEvent
|
|
| Collection::CalendarEventNotification => Permission::DavCalPropFind,
|
|
Collection::AddressBook | Collection::ContactCard => {
|
|
Permission::DavCardPropFind
|
|
}
|
|
_ => unreachable!(),
|
|
})?;
|
|
RoaringBitmap::from_iter(
|
|
access_token.all_ids_by_collection(resource.collection),
|
|
)
|
|
} else if (self.core.groupware.allow_directory_query
|
|
&& access_token.has_permission(Permission::DavPrincipalList))
|
|
|| access_token.has_permission(Permission::SysAccountQuery)
|
|
{
|
|
// Return all principals
|
|
self.registry()
|
|
.query::<RoaringBitmap>(
|
|
RegistryQuery::new(ObjectType::Account)
|
|
.with_tenant(access_token.tenant_id()),
|
|
)
|
|
.await
|
|
.caused_by(trc::location!())?
|
|
} else {
|
|
RoaringBitmap::from_iter(access_token.all_ids())
|
|
};
|
|
|
|
self.prepare_principal_propfind_response(
|
|
access_token,
|
|
resource.collection,
|
|
ids.into_iter(),
|
|
&request,
|
|
&mut response,
|
|
)
|
|
.await?;
|
|
}
|
|
|
|
Ok(HttpResponse::new(StatusCode::MULTI_STATUS).with_xml_body(response.to_string()))
|
|
}
|
|
}
|
|
|
|
async fn handle_dav_query(
|
|
&self,
|
|
access_token: &AccessToken,
|
|
mut 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 sync_collection;
|
|
let mut query_filter = None;
|
|
let mut limit = std::cmp::min(
|
|
query.limit.unwrap_or(u32::MAX) as usize,
|
|
self.core.groupware.max_results,
|
|
);
|
|
let mut is_sync_limited = false;
|
|
let mut is_propfind = false;
|
|
let mut ical_instances_limit = self.core.groupware.max_ical_instances;
|
|
|
|
let paths = match std::mem::take(&mut query.resource) {
|
|
DavQueryResource::Uri(resource) => {
|
|
collection_container = resource.collection;
|
|
collection_children = collection_container.child_collection().unwrap();
|
|
sync_collection = SyncCollection::from(collection_container);
|
|
is_propfind = true;
|
|
|
|
get(
|
|
self,
|
|
access_token,
|
|
collection_container,
|
|
collection_children,
|
|
sync_collection,
|
|
&query,
|
|
&mut data,
|
|
&mut response,
|
|
resource,
|
|
limit,
|
|
&mut is_sync_limited,
|
|
)
|
|
.await?
|
|
}
|
|
DavQueryResource::Multiget {
|
|
hrefs,
|
|
parent_collection,
|
|
} => {
|
|
collection_container = parent_collection;
|
|
collection_children = collection_container.child_collection().unwrap();
|
|
sync_collection = SyncCollection::from(collection_container);
|
|
|
|
multiget(
|
|
self,
|
|
access_token,
|
|
collection_container,
|
|
collection_children,
|
|
sync_collection,
|
|
&mut data,
|
|
&mut response,
|
|
hrefs,
|
|
)
|
|
.await?
|
|
}
|
|
DavQueryResource::Query {
|
|
filter,
|
|
parent_collection,
|
|
items,
|
|
} => {
|
|
query_filter = Some(filter);
|
|
collection_container = parent_collection;
|
|
collection_children = collection_container.child_collection().unwrap();
|
|
sync_collection = SyncCollection::from(collection_container);
|
|
|
|
items
|
|
}
|
|
DavQueryResource::None => unreachable!(),
|
|
};
|
|
response.set_namespace(collection_container.namespace());
|
|
|
|
let mut skip_not_found = query.expand;
|
|
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 | Collection::CalendarEventNotification => (
|
|
CALENDAR_CONTAINER_PROPS.as_slice(),
|
|
CALENDAR_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) => {
|
|
skip_not_found = true;
|
|
let mut result = Vec::with_capacity(items.len() + DavProperty::ALL_PROPS.len());
|
|
result.extend(DavProperty::ALL_PROPS);
|
|
result.extend(items.iter().filter(|field| !field.is_all_prop()).cloned());
|
|
result
|
|
}
|
|
PropFind::Prop(items) => items.clone(),
|
|
};
|
|
|
|
let is_scheduling = collection_container == Collection::CalendarEventNotification;
|
|
let account_info = self
|
|
.account(access_token.account_id())
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
'outer: 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
|
|
};
|
|
|
|
// Unarchive resource
|
|
let archive_;
|
|
let archive = if is_scheduling && item.is_container {
|
|
archive_ = Archive::default();
|
|
ArchivedResource::CalendarEventNotificationCollection(
|
|
item.document_id == SCHEDULE_INBOX_ID,
|
|
)
|
|
} else if let Some(archive) = self
|
|
.store()
|
|
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
|
account_id,
|
|
collection,
|
|
document_id,
|
|
))
|
|
.await
|
|
.caused_by(trc::location!())?
|
|
{
|
|
archive_ = archive;
|
|
ArchivedResource::from_archive(&archive_, collection).caused_by(trc::location!())?
|
|
} else {
|
|
response.add_response(Response::new_status([item.name], StatusCode::NOT_FOUND));
|
|
continue;
|
|
};
|
|
|
|
// Filter
|
|
let mut calendar_filter = None;
|
|
if let Some(query_filter) = &query_filter {
|
|
match (query_filter, &archive) {
|
|
(DavQueryFilter::Addressbook(filter), ArchivedResource::ContactCard(card))
|
|
if !vcard_query(&card.inner.card, filter) =>
|
|
{
|
|
continue;
|
|
}
|
|
(
|
|
DavQueryFilter::Calendar {
|
|
filter,
|
|
timezone,
|
|
max_time_range,
|
|
},
|
|
ArchivedResource::CalendarEvent(event),
|
|
) => {
|
|
let default_tz = if let Some(tz) = try_parse_tz(timezone) {
|
|
tz
|
|
} else if let Some(calendar_id) = item.parent_id {
|
|
data.resources(self, access_token, account_id, SyncCollection::Calendar)
|
|
.await
|
|
.caused_by(trc::location!())?
|
|
.calendar_default_tz(calendar_id, account_id)
|
|
.unwrap_or(Tz::UTC)
|
|
} else {
|
|
Tz::UTC
|
|
};
|
|
let mut query_handler =
|
|
CalendarQueryHandler::new(event.inner, *max_time_range, default_tz);
|
|
if !query_handler.filter(event.inner, filter) {
|
|
continue;
|
|
}
|
|
calendar_filter = Some(query_handler);
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
|
|
// Fill properties
|
|
let dead_properties = archive.dead_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(access_token) {
|
|
fields.push(DavPropertyValue::new(
|
|
property.clone(),
|
|
DavValue::String(name.to_string()),
|
|
));
|
|
} else if !skip_not_found {
|
|
fields_not_found.push(DavPropertyValue::empty(property.clone()));
|
|
}
|
|
}
|
|
WebDavProperty::GetContentLanguage => {
|
|
if !skip_not_found {
|
|
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 !skip_not_found {
|
|
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 !skip_not_found {
|
|
fields_not_found.push(DavPropertyValue::empty(property.clone()));
|
|
}
|
|
}
|
|
WebDavProperty::GetETag => {
|
|
fields.push(DavPropertyValue::new(
|
|
property.clone(),
|
|
DavValue::String(archive_.etag()),
|
|
));
|
|
}
|
|
WebDavProperty::GetCTag => {
|
|
if item.is_container {
|
|
let ctag = data
|
|
.resources(self, access_token, account_id, sync_collection)
|
|
.await
|
|
.caused_by(trc::location!())?
|
|
.highest_change_id;
|
|
|
|
fields.push(DavPropertyValue::new(
|
|
property.clone(),
|
|
DavValue::String(format!("\"{ctag}\"")),
|
|
));
|
|
} else {
|
|
fields_not_found.push(DavPropertyValue::empty(property.clone()));
|
|
}
|
|
response.set_namespace(Namespace::CalendarServer);
|
|
}
|
|
WebDavProperty::GetLastModified => {
|
|
fields.push(DavPropertyValue::new(
|
|
property.clone(),
|
|
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, &item)
|
|
.await
|
|
.caused_by(trc::location!())?
|
|
{
|
|
fields.push(DavPropertyValue::new(property.clone(), locks));
|
|
} else {
|
|
fields.push(DavPropertyValue::empty(property.clone()));
|
|
}
|
|
}
|
|
WebDavProperty::SupportedLock => {
|
|
if !is_scheduling {
|
|
fields.push(DavPropertyValue::new(
|
|
property.clone(),
|
|
SupportedLock::default(),
|
|
));
|
|
} else {
|
|
fields.push(DavPropertyValue::empty(property.clone()));
|
|
}
|
|
}
|
|
WebDavProperty::SupportedReportSet => {
|
|
if let Some(report_set) = archive.supported_report_set() {
|
|
fields.push(DavPropertyValue::new(property.clone(), report_set));
|
|
} else if !skip_not_found {
|
|
fields_not_found.push(DavPropertyValue::empty(property.clone()));
|
|
}
|
|
}
|
|
WebDavProperty::SyncToken => {
|
|
let sync_token = data
|
|
.resources(self, access_token, account_id, sync_collection)
|
|
.await
|
|
.caused_by(trc::location!())?
|
|
.sync_token();
|
|
|
|
fields.push(DavPropertyValue::new(property.clone(), sync_token));
|
|
}
|
|
WebDavProperty::CurrentUserPrincipal => {
|
|
if !query.expand {
|
|
fields.push(DavPropertyValue::new(
|
|
property.clone(),
|
|
vec![account_info.current_user_principal()],
|
|
));
|
|
} else {
|
|
fields.push(DavPropertyValue::new(
|
|
property.clone(),
|
|
self.expand_principal(
|
|
access_token,
|
|
access_token.account_id(),
|
|
&query.propfind,
|
|
)
|
|
.await?
|
|
.map(|r| DavValue::Response(Box::new(r)))
|
|
.unwrap_or(DavValue::Null),
|
|
));
|
|
}
|
|
}
|
|
WebDavProperty::QuotaAvailableBytes => {
|
|
if item.is_container {
|
|
fields.push(DavPropertyValue::new(
|
|
property.clone(),
|
|
data.quota(self, account_id)
|
|
.await
|
|
.caused_by(trc::location!())?
|
|
.available,
|
|
));
|
|
} else if !skip_not_found {
|
|
fields_not_found.push(DavPropertyValue::empty(property.clone()));
|
|
}
|
|
}
|
|
WebDavProperty::QuotaUsedBytes => {
|
|
if item.is_container {
|
|
fields.push(DavPropertyValue::new(
|
|
property.clone(),
|
|
data.quota(self, account_id)
|
|
.await
|
|
.caused_by(trc::location!())?
|
|
.used,
|
|
));
|
|
} else if !skip_not_found {
|
|
fields_not_found.push(DavPropertyValue::empty(property.clone()));
|
|
}
|
|
}
|
|
WebDavProperty::Owner => {
|
|
if !query.expand {
|
|
fields.push(DavPropertyValue::new(
|
|
property.clone(),
|
|
vec![
|
|
data.owner(self, &account_info, account_id)
|
|
.await
|
|
.caused_by(trc::location!())?,
|
|
],
|
|
));
|
|
} else {
|
|
fields.push(DavPropertyValue::new(
|
|
property.clone(),
|
|
self.expand_principal(
|
|
access_token,
|
|
account_id,
|
|
&query.propfind,
|
|
)
|
|
.await?
|
|
.map(|r| DavValue::Response(Box::new(r)))
|
|
.unwrap_or(DavValue::Null),
|
|
));
|
|
}
|
|
}
|
|
WebDavProperty::Group => {
|
|
fields.push(DavPropertyValue::empty(property.clone()));
|
|
}
|
|
WebDavProperty::SupportedPrivilegeSet => {
|
|
if !is_scheduling {
|
|
fields.push(DavPropertyValue::new(
|
|
property.clone(),
|
|
vec![SupportedPrivilege::all_privileges(
|
|
collection_container == Collection::Calendar,
|
|
)],
|
|
));
|
|
} else {
|
|
fields.push(DavPropertyValue::new(
|
|
property.clone(),
|
|
vec![SupportedPrivilege::all_scheduling_privileges(matches!(
|
|
archive,
|
|
ArchivedResource::CalendarEventNotification(_)
|
|
| ArchivedResource::CalendarEventNotificationCollection(
|
|
true
|
|
)
|
|
))],
|
|
));
|
|
}
|
|
}
|
|
WebDavProperty::CurrentUserPrivilegeSet => {
|
|
let privileges = if is_scheduling {
|
|
Privilege::scheduling(
|
|
matches!(
|
|
archive,
|
|
ArchivedResource::CalendarEventNotification(_)
|
|
| ArchivedResource::CalendarEventNotificationCollection(
|
|
true
|
|
)
|
|
),
|
|
access_token.is_member(account_id),
|
|
)
|
|
} else if access_token.is_member(account_id) {
|
|
Privilege::all(matches!(
|
|
collection,
|
|
Collection::Calendar | Collection::CalendarEvent
|
|
))
|
|
} else if let Some(acls) = archive.acls() {
|
|
access_token.current_privilege_set(
|
|
account_id,
|
|
acls,
|
|
collection_container == Collection::Calendar,
|
|
)
|
|
} else if let Some(parent_id) = item.parent_id {
|
|
current_user_privilege_set(
|
|
data.resources(self, access_token, account_id, sync_collection)
|
|
.await
|
|
.caused_by(trc::location!())?
|
|
.container_acl(access_token, parent_id),
|
|
)
|
|
} else {
|
|
vec![]
|
|
};
|
|
|
|
if !privileges.is_empty() {
|
|
fields.push(DavPropertyValue::new(property.clone(), privileges));
|
|
} else if !skip_not_found {
|
|
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,
|
|
query.expand.then_some(&query.propfind),
|
|
)
|
|
.await?;
|
|
|
|
fields.push(DavPropertyValue::new(property.clone(), aces));
|
|
} else if !skip_not_found {
|
|
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(
|
|
DavResourceName::Principal.collection_path().to_string(),
|
|
)],
|
|
));
|
|
}
|
|
},
|
|
DavProperty::DeadProperty(tag) => {
|
|
if let Some(value) =
|
|
dead_properties.and_then(|props| props.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 let Some(desc) =
|
|
book.inner.preferences(access_token).description.as_deref()
|
|
{
|
|
fields.push(DavPropertyValue::new(
|
|
property.clone(),
|
|
desc.to_string(),
|
|
));
|
|
} else {
|
|
fields_not_found.push(DavPropertyValue::empty(property.clone()));
|
|
}
|
|
}
|
|
(
|
|
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: Collation::AsciiCasemap,
|
|
namespace: Namespace::CardDav,
|
|
},
|
|
SupportedCollation {
|
|
collation: Collation::UnicodeCasemap,
|
|
namespace: Namespace::CardDav,
|
|
},
|
|
])),
|
|
));
|
|
}
|
|
(CardDavProperty::MaxResourceSize, ArchivedResource::AddressBook(_)) => {
|
|
fields.push(DavPropertyValue::new(
|
|
property.clone(),
|
|
self.core.groupware.max_vcard_size as u64,
|
|
));
|
|
}
|
|
(
|
|
CardDavProperty::AddressData {
|
|
properties,
|
|
version,
|
|
},
|
|
ArchivedResource::ContactCard(card),
|
|
) => {
|
|
fields.push(DavPropertyValue::new(
|
|
property.clone(),
|
|
DavValue::CData(serialize_vcard_with_props(
|
|
&card.inner.card,
|
|
properties,
|
|
(*version)
|
|
.or(query.max_vcard_version)
|
|
.or_else(|| card.inner.card.version()),
|
|
)),
|
|
));
|
|
}
|
|
_ => {
|
|
if !skip_not_found {
|
|
fields_not_found.push(DavPropertyValue::empty(property.clone()));
|
|
}
|
|
}
|
|
},
|
|
DavProperty::CalDav(cal_property) => match (cal_property, &archive) {
|
|
(
|
|
CalDavProperty::CalendarDescription,
|
|
ArchivedResource::Calendar(calendar),
|
|
) => {
|
|
if let Some(desc) = calendar
|
|
.inner
|
|
.preferences(access_token)
|
|
.description
|
|
.as_deref()
|
|
{
|
|
fields.push(DavPropertyValue::new(
|
|
property.clone(),
|
|
desc.to_string(),
|
|
));
|
|
} else {
|
|
fields_not_found.push(DavPropertyValue::empty(property.clone()));
|
|
}
|
|
}
|
|
(
|
|
CalDavProperty::CalendarTimezone,
|
|
ArchivedResource::Calendar(calendar),
|
|
) => {
|
|
if let ArchivedTimezone::Custom(tz) =
|
|
&calendar.inner.preferences(access_token).time_zone
|
|
{
|
|
fields.push(DavPropertyValue::new(
|
|
property.clone(),
|
|
DavValue::CData(tz.to_string()),
|
|
));
|
|
} else {
|
|
fields_not_found.push(DavPropertyValue::empty(property.clone()));
|
|
}
|
|
}
|
|
(CalDavProperty::TimezoneId, ArchivedResource::Calendar(calendar)) => {
|
|
if let ArchivedTimezone::IANA(tz) =
|
|
&calendar.inner.preferences(access_token).time_zone
|
|
{
|
|
fields.push(DavPropertyValue::new(
|
|
property.clone(),
|
|
Tz::from_id(tz.to_native()).unwrap_or(Tz::UTC).to_string(),
|
|
));
|
|
} else {
|
|
fields_not_found.push(DavPropertyValue::empty(property.clone()));
|
|
}
|
|
}
|
|
(
|
|
CalDavProperty::SupportedCalendarComponentSet,
|
|
ArchivedResource::Calendar(calendar),
|
|
) => {
|
|
let supported_components =
|
|
calendar.inner.supported_components.to_native();
|
|
fields.push(DavPropertyValue::new(
|
|
property.clone(),
|
|
if supported_components != 0 {
|
|
DavValue::Components(List(
|
|
Bitmap::<SupportedComponent>::from(supported_components)
|
|
.into_iter()
|
|
.map(ICalendarComponentType::from)
|
|
.map(Comp)
|
|
.collect(),
|
|
))
|
|
} else {
|
|
DavValue::all_calendar_components()
|
|
},
|
|
));
|
|
}
|
|
(CalDavProperty::SupportedCalendarData, ArchivedResource::Calendar(_)) => {
|
|
fields.push(DavPropertyValue::new(
|
|
property.clone(),
|
|
DavValue::SupportedCalendarData,
|
|
));
|
|
}
|
|
(CalDavProperty::SupportedCollationSet, ArchivedResource::Calendar(_)) => {
|
|
fields.push(DavPropertyValue::new(
|
|
property.clone(),
|
|
DavValue::Collations(List(vec![
|
|
SupportedCollation {
|
|
collation: Collation::AsciiCasemap,
|
|
namespace: Namespace::CalDav,
|
|
},
|
|
SupportedCollation {
|
|
collation: Collation::UnicodeCasemap,
|
|
namespace: Namespace::CalDav,
|
|
},
|
|
])),
|
|
));
|
|
}
|
|
(CalDavProperty::MaxResourceSize, ArchivedResource::Calendar(_)) => {
|
|
fields.push(DavPropertyValue::new(
|
|
property.clone(),
|
|
self.core.groupware.max_ical_size as u64,
|
|
));
|
|
}
|
|
(CalDavProperty::MinDateTime, ArchivedResource::Calendar(_)) => {
|
|
fields.push(DavPropertyValue::new(
|
|
property.clone(),
|
|
DavValue::String("0001-01-01T00:00:00Z".to_string()),
|
|
));
|
|
}
|
|
(CalDavProperty::MaxDateTime, ArchivedResource::Calendar(_)) => {
|
|
fields.push(DavPropertyValue::new(
|
|
property.clone(),
|
|
DavValue::String("9999-12-31T23:59:59Z".to_string()),
|
|
));
|
|
}
|
|
(CalDavProperty::MaxInstances, ArchivedResource::Calendar(_)) => {
|
|
fields.push(DavPropertyValue::new(
|
|
property.clone(),
|
|
self.core.groupware.max_ical_instances as u64,
|
|
));
|
|
}
|
|
(
|
|
CalDavProperty::MaxAttendeesPerInstance,
|
|
ArchivedResource::Calendar(_),
|
|
) => {
|
|
fields.push(DavPropertyValue::new(
|
|
property.clone(),
|
|
self.core.groupware.max_ical_attendees_per_instance as u64,
|
|
));
|
|
}
|
|
(
|
|
CalDavProperty::CalendarData(data),
|
|
ArchivedResource::CalendarEvent(event),
|
|
) => {
|
|
if calendar_filter.is_some() || !data.properties.is_empty() {
|
|
if let Some(ical) = calendar_filter
|
|
.get_or_insert_with(|| {
|
|
CalendarQueryHandler::new(event.inner, None, Tz::UTC)
|
|
})
|
|
.serialize_ical(event.inner, data, &mut ical_instances_limit)
|
|
{
|
|
fields.push(DavPropertyValue::new(
|
|
property.clone(),
|
|
DavValue::CData(ical),
|
|
));
|
|
} else {
|
|
limit = 0;
|
|
break 'outer;
|
|
}
|
|
} else {
|
|
fields.push(DavPropertyValue::new(
|
|
property.clone(),
|
|
DavValue::CData(event.inner.data.event.to_string()),
|
|
));
|
|
}
|
|
}
|
|
(
|
|
CalDavProperty::CalendarData(_),
|
|
ArchivedResource::CalendarEventNotification(event),
|
|
) => {
|
|
fields.push(DavPropertyValue::new(
|
|
property.clone(),
|
|
DavValue::CData(event.inner.event.to_string()),
|
|
));
|
|
}
|
|
(CalDavProperty::ScheduleTag, ArchivedResource::CalendarEvent(event))
|
|
if event.inner.schedule_tag.is_some() =>
|
|
{
|
|
fields.push(DavPropertyValue::new(
|
|
property.clone(),
|
|
DavValue::String(format!(
|
|
"\"{}\"",
|
|
event.inner.schedule_tag.as_ref().unwrap()
|
|
)),
|
|
));
|
|
}
|
|
(CalDavProperty::ScheduleCalendarTransp, ArchivedResource::Calendar(_)) => {
|
|
fields.push(DavPropertyValue::new(
|
|
property.clone(),
|
|
DavValue::DeadProperty(DeadProperty::single_with_ns(
|
|
Namespace::CalDav,
|
|
"opaque",
|
|
)),
|
|
));
|
|
}
|
|
(
|
|
CalDavProperty::ScheduleDefaultCalendarURL,
|
|
ArchivedResource::CalendarEventNotificationCollection(true),
|
|
) => {
|
|
if let Some(default_cal) = &self.core.groupware.default_calendar_name {
|
|
fields.push(DavPropertyValue::new(
|
|
property.clone(),
|
|
vec![Href(format!(
|
|
"{}/{}/{default_cal}/",
|
|
DavResourceName::Cal.base_path(),
|
|
item.name.split('/').nth(3).unwrap_or_default()
|
|
))],
|
|
));
|
|
} else {
|
|
fields_not_found.push(DavPropertyValue::empty(property.clone()));
|
|
}
|
|
}
|
|
|
|
_ => {
|
|
if !skip_not_found {
|
|
response.set_namespace(property.namespace());
|
|
fields_not_found.push(DavPropertyValue::empty(property.clone()));
|
|
}
|
|
}
|
|
},
|
|
|
|
property => {
|
|
if !skip_not_found {
|
|
response.set_namespace(property.namespace());
|
|
fields_not_found.push(DavPropertyValue::empty(property.clone()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add dead properties
|
|
if skip_not_found
|
|
&& let Some(dead_properties) =
|
|
dead_properties.filter(|dead_properties| !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));
|
|
|
|
limit -= 1;
|
|
if limit == 0 {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if limit == 0 || is_sync_limited {
|
|
response.add_response(
|
|
Response::new_status([query.uri], StatusCode::INSUFFICIENT_STORAGE)
|
|
.with_error(BaseCondition::NumberOfMatchesWithinLimit)
|
|
.with_response_description(if ical_instances_limit > 0 {
|
|
format!(
|
|
"The number of matches exceeds the limit of {}",
|
|
query
|
|
.limit
|
|
.unwrap_or(self.core.groupware.max_results as u32)
|
|
)
|
|
} else {
|
|
format!(
|
|
"The number of recurrence instances exceeds the limit of {}",
|
|
query
|
|
.limit
|
|
.unwrap_or(self.core.groupware.max_ical_instances as u32)
|
|
)
|
|
}),
|
|
);
|
|
}
|
|
|
|
if !response.response.0.is_empty() || !query.sync_type.is_none() {
|
|
Ok(HttpResponse::new(StatusCode::MULTI_STATUS).with_xml_body(response.to_string()))
|
|
} else if !is_propfind {
|
|
Ok(HttpResponse::new(StatusCode::MULTI_STATUS)
|
|
.with_xml_body(MultiStatus::not_found(query.uri).to_string()))
|
|
} else {
|
|
Ok(HttpResponse::new(StatusCode::NOT_FOUND))
|
|
}
|
|
}
|
|
|
|
async fn dav_quota(&self, account_id: u32) -> trc::Result<PropFindAccountQuota> {
|
|
let account = self.account(account_id).await.caused_by(trc::location!())?;
|
|
let quota = if account.quota_disk > 0 {
|
|
account.quota_disk
|
|
} else if let Some(tenant_id) = account.id_tenant {
|
|
let tenant = self.tenant(tenant_id).await.caused_by(trc::location!())?;
|
|
if tenant.quota_disk > 0 {
|
|
tenant.quota_disk
|
|
} else {
|
|
u32::MAX as u64
|
|
}
|
|
} else {
|
|
u32::MAX as u64
|
|
};
|
|
let used = self
|
|
.get_used_quota_account(account_id)
|
|
.await
|
|
.caused_by(trc::location!())? as u64;
|
|
|
|
Ok(PropFindAccountQuota {
|
|
used,
|
|
available: quota.saturating_sub(used),
|
|
})
|
|
}
|
|
}
|
|
#[allow(clippy::too_many_arguments)]
|
|
async fn get(
|
|
server: &Server,
|
|
access_token: &AccessToken,
|
|
collection_container: Collection,
|
|
collection_children: Collection,
|
|
sync_collection: SyncCollection,
|
|
query: &DavQuery<'_>,
|
|
data: &mut PropFindData,
|
|
response: &mut MultiStatus,
|
|
resource: UriResource<u32, Option<&str>>,
|
|
limit: usize,
|
|
is_sync_limited: &mut bool,
|
|
) -> crate::Result<Vec<PropFindItem>> {
|
|
let container_has_children = collection_children != collection_container;
|
|
response.set_namespace(collection_container.namespace());
|
|
|
|
let account_id = resource.account_id;
|
|
let resources = data
|
|
.resources(server, access_token, account_id, sync_collection)
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
|
|
// Obtain document ids
|
|
let mut display_containers = if !access_token.is_member(account_id) {
|
|
resources
|
|
.shared_containers(
|
|
access_token,
|
|
[if container_has_children {
|
|
Acl::ReadItems
|
|
} else {
|
|
Acl::Read
|
|
}],
|
|
true,
|
|
)
|
|
.into()
|
|
} else {
|
|
None
|
|
};
|
|
let mut display_children = display_containers
|
|
.as_ref()
|
|
.filter(|_| container_has_children)
|
|
.map(|containers| {
|
|
RoaringBitmap::from_iter(resources.resources.iter().filter_map(|r| {
|
|
if r.child_names()
|
|
.is_some_and(|n| n.iter().any(|n| containers.contains(n.parent_id)))
|
|
{
|
|
Some(r.document_id)
|
|
} else {
|
|
None
|
|
}
|
|
}))
|
|
});
|
|
|
|
// Filter by changelog
|
|
let is_sync = match query.sync_type {
|
|
SyncType::From { id, seq } => {
|
|
let changes = server
|
|
.store()
|
|
.changes(account_id, sync_collection.into(), Query::Since(id))
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
let mut vanished: Vec<String> = Vec::new();
|
|
|
|
// Merge changes
|
|
let mut total_changes = 0;
|
|
let mut maybe_has_vanished = false;
|
|
if container_has_children {
|
|
let mut container_changes = RoaringBitmap::new();
|
|
let mut item_changes = RoaringBitmap::new();
|
|
|
|
for change in changes.changes {
|
|
match change {
|
|
Change::InsertItem(id) => {
|
|
item_changes.insert(id as u32);
|
|
}
|
|
Change::UpdateItem(id) => {
|
|
maybe_has_vanished = true;
|
|
item_changes.insert(id as u32);
|
|
}
|
|
Change::InsertContainer(id) => {
|
|
container_changes.insert(id as u32);
|
|
}
|
|
Change::UpdateContainer(id) => {
|
|
maybe_has_vanished = true;
|
|
container_changes.insert(id as u32);
|
|
}
|
|
Change::DeleteContainer(_) | Change::DeleteItem(_) => {
|
|
maybe_has_vanished = true;
|
|
}
|
|
Change::UpdateContainerProperty(_) => (),
|
|
}
|
|
}
|
|
|
|
for (document_ids, changes) in [
|
|
(&mut display_containers, container_changes),
|
|
(&mut display_children, item_changes),
|
|
] {
|
|
if let Some(document_ids) = document_ids {
|
|
*document_ids &= changes;
|
|
total_changes += document_ids.len() as usize;
|
|
} else {
|
|
total_changes += changes.len() as usize;
|
|
*document_ids = Some(changes);
|
|
}
|
|
}
|
|
} else {
|
|
let changes = RoaringBitmap::from_iter(changes.changes.iter().filter_map(
|
|
|change| match change {
|
|
Change::InsertItem(id) | Change::InsertContainer(id) => Some(*id as u32),
|
|
Change::UpdateItem(id) | Change::UpdateContainer(id) => {
|
|
maybe_has_vanished = true;
|
|
Some(*id as u32)
|
|
}
|
|
Change::DeleteContainer(_) | Change::DeleteItem(_) => {
|
|
maybe_has_vanished = true;
|
|
None
|
|
}
|
|
_ => None,
|
|
},
|
|
));
|
|
if let Some(document_ids) = &mut display_containers {
|
|
*document_ids &= changes;
|
|
total_changes += document_ids.len() as usize;
|
|
} else {
|
|
total_changes += changes.len() as usize;
|
|
display_containers = Some(changes);
|
|
}
|
|
}
|
|
|
|
if maybe_has_vanished
|
|
&& let Some(vanished_collection) = sync_collection.vanished_collection()
|
|
{
|
|
vanished = server
|
|
.store()
|
|
.vanished(account_id, vanished_collection.into(), Query::Since(id))
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
total_changes += vanished.len();
|
|
}
|
|
|
|
// Truncate changes
|
|
if total_changes > limit {
|
|
let mut offset = limit * seq as usize;
|
|
let mut total_changes = 0;
|
|
|
|
// Add vanished items to response
|
|
for item in vanished {
|
|
if offset > 0 {
|
|
offset -= 1;
|
|
} else if total_changes < limit {
|
|
response.add_response(Response::new_status([item], StatusCode::NOT_FOUND));
|
|
total_changes += 1;
|
|
} else {
|
|
*is_sync_limited = true;
|
|
}
|
|
}
|
|
|
|
// Add items to document set
|
|
for document_ids in [&mut display_containers, &mut display_children]
|
|
.into_iter()
|
|
.flatten()
|
|
{
|
|
let mut new_document_ids = RoaringBitmap::new();
|
|
for id in document_ids.iter() {
|
|
if offset > 0 {
|
|
offset -= 1;
|
|
} else if total_changes < limit {
|
|
new_document_ids.insert(id);
|
|
total_changes += 1;
|
|
} else {
|
|
*is_sync_limited = true;
|
|
}
|
|
}
|
|
*document_ids = new_document_ids;
|
|
}
|
|
|
|
if *is_sync_limited {
|
|
response.set_sync_token(Urn::Sync { id, seq: seq + 1 }.to_string());
|
|
}
|
|
} else {
|
|
// Add vanished items to response
|
|
for item in vanished {
|
|
response.add_response(Response::new_status([item], StatusCode::NOT_FOUND));
|
|
}
|
|
}
|
|
|
|
if !*is_sync_limited {
|
|
response.set_sync_token(resources.sync_token());
|
|
}
|
|
|
|
true
|
|
}
|
|
SyncType::Initial => {
|
|
response.set_sync_token(resources.sync_token());
|
|
false
|
|
}
|
|
SyncType::None => false,
|
|
};
|
|
|
|
let mut results = Vec::new();
|
|
if let Some(resource) = resource.resource {
|
|
results = resources
|
|
.subtree_with_depth(resource, query.depth)
|
|
.filter(|item| {
|
|
display_containers.as_ref().is_none_or(|containers| {
|
|
if container_has_children {
|
|
if item.is_container() {
|
|
containers.contains(item.document_id())
|
|
} else {
|
|
display_children
|
|
.as_ref()
|
|
.is_some_and(|children| children.contains(item.document_id()))
|
|
}
|
|
} else {
|
|
containers.contains(item.document_id())
|
|
}
|
|
}) && (!query.depth_no_root || item.path() != resource)
|
|
})
|
|
.map(|item| PropFindItem::new(resources.format_resource(item), account_id, item))
|
|
.collect::<Vec<_>>();
|
|
} else {
|
|
if !query.depth_no_root && query.sync_type.is_none_or_initial() {
|
|
server
|
|
.prepare_principal_propfind_response(
|
|
access_token,
|
|
collection_container,
|
|
[account_id].into_iter(),
|
|
&query.propfind,
|
|
response,
|
|
)
|
|
.await?;
|
|
}
|
|
|
|
if query.depth != 0 {
|
|
results = resources
|
|
.tree_with_depth(query.depth - 1)
|
|
.filter(|item| {
|
|
display_containers.as_ref().is_none_or(|containers| {
|
|
if container_has_children {
|
|
if item.is_container() {
|
|
containers.contains(item.document_id())
|
|
} else {
|
|
display_children
|
|
.as_ref()
|
|
.is_some_and(|children| children.contains(item.document_id()))
|
|
}
|
|
} else {
|
|
containers.contains(item.document_id())
|
|
}
|
|
})
|
|
})
|
|
.map(|item| PropFindItem::new(resources.format_resource(item), account_id, item))
|
|
.collect::<Vec<_>>();
|
|
|
|
// Assisted discovery:
|
|
// If 'bob' has access to 'jane' and `bill` calendars, a query to '/dav/cal/bob' will return:
|
|
// - /dav/cal/bob/default
|
|
// - /dav/cal/jane/default
|
|
// - /dav/cal/bill/default
|
|
// This is invalid but it's the only workaround for clients which do not support multiple home-sets
|
|
if server.core.groupware.assisted_discovery
|
|
&& !is_sync
|
|
&& account_id == access_token.account_id()
|
|
&& matches!(
|
|
sync_collection,
|
|
SyncCollection::Calendar | SyncCollection::AddressBook
|
|
)
|
|
{
|
|
for shared_account_id in access_token.all_ids_by_collection(collection_container) {
|
|
if shared_account_id == access_token.account_id() {
|
|
continue;
|
|
}
|
|
let shared_resources = data
|
|
.resources(server, access_token, shared_account_id, sync_collection)
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
let shared_containers =
|
|
(!access_token.is_member(shared_account_id)).then(|| {
|
|
shared_resources.shared_containers(
|
|
access_token,
|
|
[if container_has_children {
|
|
Acl::ReadItems
|
|
} else {
|
|
Acl::Read
|
|
}],
|
|
true,
|
|
)
|
|
});
|
|
if shared_containers
|
|
.as_ref()
|
|
.is_none_or(|containers| !containers.is_empty())
|
|
{
|
|
results.extend(
|
|
shared_resources
|
|
.tree_with_depth(query.depth - 1)
|
|
.filter(|item| {
|
|
item.is_container()
|
|
&& shared_containers.as_ref().is_none_or(|containers| {
|
|
containers.contains(item.document_id())
|
|
})
|
|
})
|
|
.map(|item| {
|
|
PropFindItem::new(
|
|
shared_resources.format_resource(item),
|
|
shared_account_id,
|
|
item,
|
|
)
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(results)
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
async fn multiget(
|
|
server: &Server,
|
|
access_token: &AccessToken,
|
|
collection_container: Collection,
|
|
collection_children: Collection,
|
|
sync_collection: SyncCollection,
|
|
data: &mut PropFindData,
|
|
response: &mut MultiStatus,
|
|
hrefs: Vec<String>,
|
|
) -> crate::Result<Vec<PropFindItem>> {
|
|
let mut paths = Vec::with_capacity(hrefs.len() * 2);
|
|
let mut shared_folders_by_account: AHashMap<u32, Arc<RoaringBitmap>> =
|
|
AHashMap::with_capacity(3);
|
|
|
|
for item in hrefs {
|
|
let resource = match server
|
|
.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 = data
|
|
.resources(server, access_token, account_id, sync_collection)
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
|
|
let document_ids = if !access_token.is_member(account_id) {
|
|
if let Some(document_ids) = shared_folders_by_account.get(&account_id) {
|
|
document_ids.clone().into()
|
|
} else {
|
|
let document_ids = Arc::new(resources.shared_containers(
|
|
access_token,
|
|
[if collection_children == collection_container {
|
|
Acl::ReadItems
|
|
} else {
|
|
Acl::Read
|
|
}],
|
|
true,
|
|
));
|
|
shared_folders_by_account.insert(account_id, document_ids.clone());
|
|
document_ids.into()
|
|
}
|
|
} else {
|
|
None
|
|
};
|
|
|
|
if let Some(resource) = resource.resource.and_then(|name| resources.by_path(name)) {
|
|
if !resource.is_container() {
|
|
if document_ids
|
|
.as_ref()
|
|
.is_none_or(|docs| docs.contains(resource.parent_id().unwrap()))
|
|
{
|
|
paths.push(PropFindItem::new(
|
|
resources.format_resource(resource),
|
|
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));
|
|
}
|
|
}
|
|
|
|
Ok(paths)
|
|
}
|
|
|
|
impl PropFindItem {
|
|
pub fn new(name: String, account_id: u32, resource: DavResourcePath<'_>) -> Self {
|
|
Self {
|
|
name,
|
|
account_id,
|
|
document_id: resource.document_id(),
|
|
parent_id: resource.parent_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,
|
|
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(account_id).await?.into();
|
|
}
|
|
|
|
Ok(data.quota.clone().unwrap())
|
|
}
|
|
|
|
pub async fn owner(
|
|
&mut self,
|
|
server: &Server,
|
|
account_info: &AccountCache,
|
|
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(account_info, account_id)
|
|
.await
|
|
.caused_by(trc::location!())?
|
|
.into();
|
|
}
|
|
|
|
Ok(data.owner.clone().unwrap())
|
|
}
|
|
|
|
pub async fn resources(
|
|
&mut self,
|
|
server: &Server,
|
|
access_token: &AccessToken,
|
|
account_id: u32,
|
|
sync_collection: SyncCollection,
|
|
) -> trc::Result<Arc<DavResources>> {
|
|
let data = self.accounts.entry(account_id).or_default();
|
|
|
|
if data.resources.is_none() {
|
|
let resources = server
|
|
.fetch_dav_resources(access_token.account_id(), account_id, sync_collection)
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
data.resources = resources.into();
|
|
}
|
|
|
|
Ok(data.resources.clone().unwrap())
|
|
}
|
|
|
|
pub async fn locks(
|
|
&mut self,
|
|
server: &Server,
|
|
account_id: u32,
|
|
collection_container: Collection,
|
|
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 {
|
|
let base_uri = dav_base_uri(&item.name).unwrap_or_default();
|
|
lock_data.unarchive::<LockData>().map(|locks| {
|
|
locks
|
|
.find_locks(&item.name.strip_prefix(base_uri).unwrap()[1..], false)
|
|
.iter()
|
|
.map(|(path, lock)| lock.to_active_lock(format!("{base_uri}/{path}")))
|
|
.collect::<Vec<_>>()
|
|
.into()
|
|
})
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) trait SyncTokenUrn {
|
|
fn sync_token(&self) -> String;
|
|
}
|
|
|
|
impl SyncTokenUrn for DavResources {
|
|
fn sync_token(&self) -> String {
|
|
Urn::Sync {
|
|
id: self.highest_change_id,
|
|
seq: 0,
|
|
}
|
|
.to_string()
|
|
}
|
|
}
|
|
|
|
async fn add_base_collection_response(
|
|
server: &Server,
|
|
request: &PropFind,
|
|
collection: Collection,
|
|
access_token: &AccessToken,
|
|
response: &mut MultiStatus,
|
|
) -> trc::Result<()> {
|
|
let properties = match request {
|
|
PropFind::PropName => {
|
|
response.add_response(Response::new_propstat(
|
|
DavResourceName::from(collection).collection_path(),
|
|
vec![PropStat::new_list(vec![
|
|
DavPropertyValue::empty(DavProperty::WebDav(WebDavProperty::ResourceType)),
|
|
DavPropertyValue::empty(DavProperty::WebDav(
|
|
WebDavProperty::CurrentUserPrincipal,
|
|
)),
|
|
DavPropertyValue::empty(DavProperty::WebDav(
|
|
WebDavProperty::SupportedReportSet,
|
|
)),
|
|
])],
|
|
));
|
|
return Ok(());
|
|
}
|
|
PropFind::AllProp(_) => [
|
|
DavProperty::WebDav(WebDavProperty::ResourceType),
|
|
DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal),
|
|
DavProperty::WebDav(WebDavProperty::SupportedReportSet),
|
|
]
|
|
.as_slice(),
|
|
PropFind::Prop(items) => items,
|
|
};
|
|
|
|
let mut fields = Vec::with_capacity(properties.len());
|
|
let mut fields_not_found = Vec::new();
|
|
let account_info = server
|
|
.account(access_token.account_id())
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
|
|
for prop in properties {
|
|
match &prop {
|
|
DavProperty::WebDav(WebDavProperty::ResourceType) => {
|
|
fields.push(DavPropertyValue::new(
|
|
prop.clone(),
|
|
vec![ResourceType::Collection],
|
|
));
|
|
}
|
|
DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal) => {
|
|
fields.push(DavPropertyValue::new(
|
|
prop.clone(),
|
|
vec![account_info.current_user_principal()],
|
|
));
|
|
}
|
|
DavProperty::Principal(PrincipalProperty::CalendarHomeSet) => {
|
|
let hrefs = build_home_set(
|
|
server,
|
|
access_token,
|
|
account_info.name(),
|
|
access_token.account_id(),
|
|
true,
|
|
)
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
|
|
fields.push(DavPropertyValue::new(prop.clone(), hrefs));
|
|
response.set_namespace(Namespace::CalDav);
|
|
}
|
|
DavProperty::Principal(PrincipalProperty::AddressbookHomeSet) => {
|
|
let hrefs = build_home_set(
|
|
server,
|
|
access_token,
|
|
account_info.name(),
|
|
access_token.account_id(),
|
|
false,
|
|
)
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
|
|
fields.push(DavPropertyValue::new(prop.clone(), hrefs));
|
|
response.set_namespace(Namespace::CardDav);
|
|
}
|
|
DavProperty::WebDav(WebDavProperty::SupportedReportSet) => {
|
|
let reports = match collection {
|
|
Collection::Principal => ReportSet::principal(),
|
|
Collection::Calendar | Collection::CalendarEvent => ReportSet::calendar(),
|
|
Collection::AddressBook | Collection::ContactCard => ReportSet::addressbook(),
|
|
_ => ReportSet::file(),
|
|
};
|
|
|
|
fields.push(DavPropertyValue::new(prop.clone(), reports));
|
|
}
|
|
_ => {
|
|
response.set_namespace(prop.namespace());
|
|
fields_not_found.push(DavPropertyValue::empty(prop.clone()));
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut prop_stat = Vec::with_capacity(2);
|
|
|
|
if !fields.is_empty() {
|
|
prop_stat.push(PropStat::new_list(fields));
|
|
}
|
|
|
|
if !fields_not_found.is_empty() {
|
|
prop_stat.push(PropStat::new_list(fields_not_found).with_status(StatusCode::NOT_FOUND));
|
|
}
|
|
|
|
response.add_response(Response::new_propstat(
|
|
DavResourceName::from(collection).collection_path(),
|
|
prop_stat,
|
|
));
|
|
|
|
Ok(())
|
|
}
|