Import upstream v0.16.22, stripped
Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f Enterprise-only files removed or emptied: 63 Enterprise-only snippets removed: 117 in 50 files Dangling module declarations removed: 5 Cargo edits turning enterprise off: 14 Verification: clean Enterprise feature gates left for rebuilt features: 19 in 18 files Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
[package]
|
||||
name = "dav"
|
||||
version = "0.16.22"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
dav-proto = { path = "../dav-proto" }
|
||||
common = { path = "../common" }
|
||||
store = { path = "../store" }
|
||||
utils = { path = "../utils" }
|
||||
groupware = { path = "../groupware" }
|
||||
directory = { path = "../directory" }
|
||||
registry = { path = "../registry" }
|
||||
http_proto = { path = "../http-proto" }
|
||||
types = { path = "../types" }
|
||||
trc = { path = "../trc" }
|
||||
calcard = { version = "0.3", features = ["rkyv"] }
|
||||
hashify = { version = "0.2" }
|
||||
hyper = { version = "1.11.1", features = ["server", "http1", "http2"] }
|
||||
percent-encoding = "2.3.2"
|
||||
rkyv = { version = "0.8.18", features = ["little_endian"] }
|
||||
compact_str = "0.10.0"
|
||||
chrono = "0.4.45"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
[features]
|
||||
test_mode = []
|
||||
dev_mode = []
|
||||
enterprise = []
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
DavError, DavMethod,
|
||||
common::{
|
||||
ETag,
|
||||
lock::{LockRequestHandler, ResourceState},
|
||||
uri::DavUriResource,
|
||||
},
|
||||
};
|
||||
use common::{Server, auth::AccessToken, sharing::EffectiveAcl};
|
||||
use dav_proto::RequestHeaders;
|
||||
use groupware::{
|
||||
DestroyArchive,
|
||||
cache::GroupwareCache,
|
||||
calendar::{Calendar, CalendarEvent},
|
||||
};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use registry::schema::enums::Permission;
|
||||
use store::write::{BatchBuilder, ValueClass};
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{AlignedBytes, Archive},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::Acl,
|
||||
collection::{Collection, SyncCollection},
|
||||
field::PrincipalField,
|
||||
};
|
||||
|
||||
pub(crate) trait CalendarDeleteRequestHandler: Sync + Send {
|
||||
fn handle_calendar_delete_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
}
|
||||
|
||||
impl CalendarDeleteRequestHandler for Server {
|
||||
async fn handle_calendar_delete_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
// Validate URI
|
||||
let resource = self
|
||||
.validate_uri(access_token, headers.uri)
|
||||
.await?
|
||||
.into_owned_uri()?;
|
||||
let account_id = resource.account_id;
|
||||
let delete_path = resource
|
||||
.resource
|
||||
.filter(|r| !r.is_empty())
|
||||
.ok_or(DavError::Code(StatusCode::FORBIDDEN))?;
|
||||
let resources = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::Calendar,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Check resource type
|
||||
let delete_resource = resources
|
||||
.by_path(delete_path)
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
let document_id = delete_resource.document_id();
|
||||
let account_info = self
|
||||
.scheduling_account_info(access_token.account_id(), account_id)
|
||||
.await?;
|
||||
let send_itip = self.core.groupware.itip_enabled
|
||||
&& !headers.no_schedule_reply
|
||||
&& !account_info.addresses().is_empty()
|
||||
&& access_token.has_permission(Permission::CalendarSchedulingSend);
|
||||
|
||||
// Fetch entry
|
||||
let mut batch = BatchBuilder::new();
|
||||
if delete_resource.is_container() {
|
||||
// Deleting the default calendar is not allowed
|
||||
#[cfg(not(any(feature = "dev_mode", feature = "test_mode")))]
|
||||
if self
|
||||
.core
|
||||
.groupware
|
||||
.default_calendar_name
|
||||
.as_ref()
|
||||
.is_some_and(|name| name == delete_path)
|
||||
{
|
||||
return Err(DavError::Condition(crate::DavErrorCondition::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
dav_proto::schema::response::CalCondition::DefaultCalendarNeeded,
|
||||
)));
|
||||
}
|
||||
|
||||
let calendar_ = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::Calendar,
|
||||
document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
|
||||
let calendar = calendar_
|
||||
.to_unarchived::<Calendar>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Validate ACL
|
||||
if !access_token.is_member(account_id)
|
||||
&& !calendar
|
||||
.inner
|
||||
.acls
|
||||
.effective_acl(access_token)
|
||||
.contains_all([Acl::Delete, Acl::RemoveItems].into_iter())
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
// Validate headers
|
||||
self.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![ResourceState {
|
||||
account_id,
|
||||
collection: Collection::Calendar,
|
||||
document_id: document_id.into(),
|
||||
etag: calendar.etag().into(),
|
||||
path: delete_path,
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::DELETE,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Delete calendar and events
|
||||
DestroyArchive(calendar)
|
||||
.delete_with_events(
|
||||
self,
|
||||
&account_info,
|
||||
account_id,
|
||||
document_id,
|
||||
resources
|
||||
.subtree(delete_path)
|
||||
.filter(|r| !r.is_container())
|
||||
.map(|r| r.document_id())
|
||||
.collect::<Vec<_>>(),
|
||||
resources.format_resource(delete_resource).into(),
|
||||
send_itip,
|
||||
&mut batch,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Reset default calendar id
|
||||
let default_calendar_id = self
|
||||
.store()
|
||||
.get_value::<u32>(ValueKey {
|
||||
account_id,
|
||||
collection: Collection::Principal.into(),
|
||||
document_id: 0,
|
||||
class: ValueClass::Property(PrincipalField::DefaultCalendarId.into()),
|
||||
})
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
if default_calendar_id.is_some_and(|id| id == document_id) {
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::Principal)
|
||||
.with_document(0)
|
||||
.clear(PrincipalField::DefaultCalendarId);
|
||||
}
|
||||
} else {
|
||||
// Validate ACL
|
||||
let calendar_id = delete_resource.parent_id().unwrap();
|
||||
if !access_token.is_member(account_id)
|
||||
&& !resources.has_access_to_container(access_token, calendar_id, Acl::RemoveItems)
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
let event_ = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::CalendarEvent,
|
||||
document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
|
||||
// Validate headers
|
||||
self.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![ResourceState {
|
||||
account_id,
|
||||
collection: Collection::CalendarEvent,
|
||||
document_id: document_id.into(),
|
||||
etag: event_.etag().into(),
|
||||
path: delete_path,
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::DELETE,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Validate schedule tag
|
||||
let event = event_
|
||||
.to_unarchived::<CalendarEvent>()
|
||||
.caused_by(trc::location!())?;
|
||||
if headers.if_schedule_tag.is_some()
|
||||
&& event.inner.schedule_tag.as_ref().map(|t| t.to_native())
|
||||
!= headers.if_schedule_tag
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::PRECONDITION_FAILED));
|
||||
}
|
||||
|
||||
// Delete event
|
||||
DestroyArchive(event)
|
||||
.delete(
|
||||
&account_info,
|
||||
account_id,
|
||||
document_id,
|
||||
calendar_id,
|
||||
resources.format_resource(delete_resource).into(),
|
||||
send_itip,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
|
||||
self.commit_batch(batch).await.caused_by(trc::location!())?;
|
||||
self.notify_task_queue();
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::NO_CONTENT))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::query::CalendarQueryHandler;
|
||||
use crate::{DavError, calendar::query::is_resource_in_time_range, common::uri::DavUriResource};
|
||||
use calcard::{
|
||||
common::{PartialDateTime, timezone::Tz},
|
||||
icalendar::{
|
||||
ArchivedICalendarComponentType, ArchivedICalendarEntry, ArchivedICalendarParameterName,
|
||||
ArchivedICalendarParameterValue, ArchivedICalendarProperty, ArchivedICalendarStatus,
|
||||
ArchivedICalendarValue, ICalendar, ICalendarComponent, ICalendarComponentType,
|
||||
ICalendarEntry, ICalendarFreeBusyType, ICalendarParameter, ICalendarPeriod,
|
||||
ICalendarProperty, ICalendarTransparency, ICalendarValue,
|
||||
},
|
||||
};
|
||||
use common::{DavResourcePath, DavResources, PROD_ID, Server, auth::AccessToken};
|
||||
use dav_proto::{RequestHeaders, schema::request::FreeBusyQuery};
|
||||
use groupware::{cache::GroupwareCache, calendar::CalendarEvent};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use std::str::FromStr;
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{AlignedBytes, Archive},
|
||||
};
|
||||
use store::{
|
||||
ahash::AHashMap,
|
||||
write::{now, serialize::rkyv_deserialize},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
TimeRange,
|
||||
acl::Acl,
|
||||
collection::{Collection, SyncCollection},
|
||||
};
|
||||
|
||||
pub(crate) trait CalendarFreebusyRequestHandler: Sync + Send {
|
||||
fn handle_calendar_freebusy_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
request: FreeBusyQuery,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
|
||||
fn build_freebusy_object(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
request: FreeBusyQuery,
|
||||
resources: &DavResources,
|
||||
account_id: u32,
|
||||
resource: DavResourcePath<'_>,
|
||||
) -> impl Future<Output = crate::Result<ICalendar>> + Send;
|
||||
}
|
||||
|
||||
impl CalendarFreebusyRequestHandler for Server {
|
||||
async fn handle_calendar_freebusy_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
request: FreeBusyQuery,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
// Validate URI
|
||||
let resource_ = self
|
||||
.validate_uri(access_token, headers.uri)
|
||||
.await?
|
||||
.into_owned_uri()?;
|
||||
let account_id = resource_.account_id;
|
||||
let resources = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::Calendar,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let resource = resources
|
||||
.by_path(
|
||||
resource_
|
||||
.resource
|
||||
.ok_or(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))?,
|
||||
)
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
if !resource.is_container() {
|
||||
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
|
||||
}
|
||||
|
||||
self.build_freebusy_object(access_token, request, &resources, account_id, resource)
|
||||
.await
|
||||
.map(|ical| {
|
||||
HttpResponse::new(StatusCode::OK)
|
||||
.with_content_type("text/calendar; charset=utf-8")
|
||||
.with_text_body(ical.to_string())
|
||||
})
|
||||
}
|
||||
|
||||
async fn build_freebusy_object(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
request: FreeBusyQuery,
|
||||
resources: &DavResources,
|
||||
account_id: u32,
|
||||
resource: DavResourcePath<'_>,
|
||||
) -> crate::Result<ICalendar> {
|
||||
// Obtain shared ids
|
||||
let shared_ids = if !access_token.is_member(account_id) {
|
||||
resources
|
||||
.shared_items(
|
||||
access_token,
|
||||
[Acl::ReadItems, Acl::SchedulingReadFreeBusy],
|
||||
false,
|
||||
)
|
||||
.into()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Build FreeBusy component
|
||||
let default_tz = resource
|
||||
.resource
|
||||
.calendar_preferences(account_id)
|
||||
.map(|p| p.tz)
|
||||
.unwrap_or(Tz::UTC);
|
||||
let mut entries = Vec::with_capacity(6);
|
||||
if let Some(range) = request.range {
|
||||
entries.push(ICalendarEntry {
|
||||
name: ICalendarProperty::Dtstart,
|
||||
params: vec![],
|
||||
values: vec![ICalendarValue::PartialDateTime(Box::new(
|
||||
PartialDateTime::from_utc_timestamp(range.start),
|
||||
))],
|
||||
});
|
||||
entries.push(ICalendarEntry {
|
||||
name: ICalendarProperty::Dtend,
|
||||
params: vec![],
|
||||
values: vec![ICalendarValue::PartialDateTime(Box::new(
|
||||
PartialDateTime::from_utc_timestamp(range.end),
|
||||
))],
|
||||
});
|
||||
entries.push(ICalendarEntry {
|
||||
name: ICalendarProperty::Dtstamp,
|
||||
params: vec![],
|
||||
values: vec![ICalendarValue::PartialDateTime(Box::new(
|
||||
PartialDateTime::from_utc_timestamp(now() as i64),
|
||||
))],
|
||||
});
|
||||
|
||||
let document_ids = resources
|
||||
.children(resource.document_id())
|
||||
.filter(|resource| {
|
||||
shared_ids
|
||||
.as_ref()
|
||||
.is_none_or(|ids| ids.contains(resource.document_id()))
|
||||
&& is_resource_in_time_range(resource.resource, &range)
|
||||
})
|
||||
.map(|resource| resource.document_id())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut fb_entries: AHashMap<ICalendarFreeBusyType, Vec<(i64, i64)>> =
|
||||
AHashMap::with_capacity(document_ids.len());
|
||||
let max_instances = self.core.groupware.max_ical_instances;
|
||||
let mut total_instances: usize = 0;
|
||||
|
||||
for document_id in document_ids {
|
||||
let Some(archive) = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::CalendarEvent,
|
||||
document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let event = archive
|
||||
.unarchive::<CalendarEvent>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
/*
|
||||
Only VEVENT components without a TRANSP property or with the TRANSP
|
||||
property set to OPAQUE, and VFREEBUSY components SHOULD be considered
|
||||
in generating the free busy time information.
|
||||
*/
|
||||
let mut components = event
|
||||
.data
|
||||
.event
|
||||
.components
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, comp)| {
|
||||
(matches!(comp.component_type, ArchivedICalendarComponentType::VEvent)
|
||||
&& comp
|
||||
.transparency()
|
||||
.is_none_or(|t| t == &ICalendarTransparency::Opaque))
|
||||
|| matches!(
|
||||
comp.component_type,
|
||||
ArchivedICalendarComponentType::VFreebusy
|
||||
)
|
||||
})
|
||||
.peekable();
|
||||
|
||||
if components.peek().is_none() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let events =
|
||||
CalendarQueryHandler::new(event, Some(range), default_tz).into_expanded_times();
|
||||
|
||||
if events.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
total_instances = total_instances.saturating_add(events.len());
|
||||
if total_instances > max_instances {
|
||||
return Err(DavError::Code(StatusCode::PAYLOAD_TOO_LARGE));
|
||||
}
|
||||
|
||||
for (component_id, component) in components {
|
||||
let component_id = component_id as u32;
|
||||
match component.component_type {
|
||||
ArchivedICalendarComponentType::VEvent => {
|
||||
let fbtype = match component.status() {
|
||||
Some(ArchivedICalendarStatus::Cancelled) => continue,
|
||||
Some(ArchivedICalendarStatus::Tentative) => {
|
||||
ICalendarFreeBusyType::BusyTentative
|
||||
}
|
||||
_ => ICalendarFreeBusyType::Busy,
|
||||
};
|
||||
|
||||
let mut events_in_range = Vec::new();
|
||||
for event in &events {
|
||||
if event.comp_id == component_id
|
||||
&& range.is_in_range(false, event.start, event.end)
|
||||
{
|
||||
events_in_range.push((event.start, event.end));
|
||||
}
|
||||
}
|
||||
|
||||
if !events_in_range.is_empty() {
|
||||
fb_entries
|
||||
.entry(fbtype)
|
||||
.or_default()
|
||||
.extend(events_in_range);
|
||||
}
|
||||
}
|
||||
ArchivedICalendarComponentType::VFreebusy => {
|
||||
for entry in component.entries.iter() {
|
||||
if matches!(entry.name, ArchivedICalendarProperty::Freebusy) {
|
||||
let mut fb_in_range =
|
||||
freebusy_in_range_utc(entry, &range, default_tz).peekable();
|
||||
if fb_in_range.peek().is_some() {
|
||||
let fb_type = entry
|
||||
.params
|
||||
.iter()
|
||||
.find_map(|param| {
|
||||
if let (
|
||||
ArchivedICalendarParameterName::Fbtype,
|
||||
ArchivedICalendarParameterValue::Fbtype(param),
|
||||
) = (¶m.name, ¶m.value)
|
||||
{
|
||||
rkyv_deserialize(param).ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or(ICalendarFreeBusyType::Busy);
|
||||
|
||||
fb_entries.entry(fb_type).or_default().extend(fb_in_range);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (fbtype, events_in_range) in fb_entries {
|
||||
entries.push(ICalendarEntry {
|
||||
name: ICalendarProperty::Freebusy,
|
||||
params: vec![ICalendarParameter::fbtype(fbtype)],
|
||||
values: merge_intervals(events_in_range),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Build ICalendar
|
||||
Ok(ICalendar {
|
||||
components: vec![
|
||||
ICalendarComponent {
|
||||
component_type: ICalendarComponentType::VCalendar,
|
||||
entries: vec![
|
||||
ICalendarEntry {
|
||||
name: ICalendarProperty::Version,
|
||||
params: vec![],
|
||||
values: vec![ICalendarValue::Text("2.0".to_string())],
|
||||
},
|
||||
ICalendarEntry {
|
||||
name: ICalendarProperty::Prodid,
|
||||
params: vec![],
|
||||
values: vec![ICalendarValue::Text(PROD_ID.to_string())],
|
||||
},
|
||||
],
|
||||
component_ids: vec![1],
|
||||
},
|
||||
ICalendarComponent {
|
||||
component_type: ICalendarComponentType::VFreebusy,
|
||||
entries,
|
||||
component_ids: vec![],
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_intervals(mut intervals: Vec<(i64, i64)>) -> Vec<ICalendarValue> {
|
||||
if intervals.len() > 1 {
|
||||
intervals.sort_unstable_by_key(|a| a.0);
|
||||
|
||||
let mut unique_intervals = Vec::new();
|
||||
let mut start_time = intervals[0].0;
|
||||
let mut end_time = intervals[0].1;
|
||||
|
||||
for &(curr_start, curr_end) in intervals.iter().skip(1) {
|
||||
if curr_start <= end_time {
|
||||
end_time = end_time.max(curr_end);
|
||||
} else {
|
||||
unique_intervals.push(build_ical_value(start_time, end_time));
|
||||
start_time = curr_start;
|
||||
end_time = curr_end;
|
||||
}
|
||||
}
|
||||
|
||||
unique_intervals.push(build_ical_value(start_time, end_time));
|
||||
unique_intervals
|
||||
} else {
|
||||
intervals
|
||||
.into_iter()
|
||||
.map(|(start, end)| build_ical_value(start, end))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn build_ical_value(from: i64, to: i64) -> ICalendarValue {
|
||||
ICalendarValue::Period(ICalendarPeriod::Range {
|
||||
start: PartialDateTime::from_utc_timestamp(from),
|
||||
end: PartialDateTime::from_utc_timestamp(to),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn freebusy_in_range(
|
||||
entry: &ArchivedICalendarEntry,
|
||||
range: &TimeRange,
|
||||
default_tz: Tz,
|
||||
) -> impl Iterator<Item = ICalendarValue> {
|
||||
let tz = entry
|
||||
.tz_id()
|
||||
.and_then(|tz_id| Tz::from_str(tz_id).ok())
|
||||
.unwrap_or(default_tz);
|
||||
|
||||
entry.values.iter().filter_map(move |value| {
|
||||
if let ArchivedICalendarValue::Period(period) = &value {
|
||||
period.time_range(tz).and_then(|(start, end)| {
|
||||
let start = start.timestamp();
|
||||
let end = end.timestamp();
|
||||
if range.is_in_range(false, start, end) {
|
||||
rkyv_deserialize(value).ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn freebusy_in_range_utc(
|
||||
entry: &ArchivedICalendarEntry,
|
||||
range: &TimeRange,
|
||||
default_tz: Tz,
|
||||
) -> impl Iterator<Item = (i64, i64)> {
|
||||
let tz = entry
|
||||
.tz_id()
|
||||
.and_then(|tz_id| Tz::from_str(tz_id).ok())
|
||||
.unwrap_or(default_tz);
|
||||
|
||||
entry.values.iter().filter_map(move |value| {
|
||||
if let ArchivedICalendarValue::Period(period) = &value {
|
||||
period.time_range(tz).and_then(|(start, end)| {
|
||||
let start = start.timestamp();
|
||||
let end = end.timestamp();
|
||||
if range.is_in_range(false, start, end) {
|
||||
Some((start, end))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
DavError, DavMethod,
|
||||
common::{
|
||||
ETag,
|
||||
lock::{LockRequestHandler, ResourceState},
|
||||
uri::DavUriResource,
|
||||
},
|
||||
};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use dav_proto::{RequestHeaders, schema::property::Rfc1123DateTime};
|
||||
use groupware::{cache::GroupwareCache, calendar::CalendarEvent};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{AlignedBytes, Archive},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::Acl,
|
||||
collection::{Collection, SyncCollection},
|
||||
};
|
||||
|
||||
pub(crate) trait CalendarGetRequestHandler: Sync + Send {
|
||||
fn handle_calendar_get_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
is_head: bool,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
}
|
||||
|
||||
impl CalendarGetRequestHandler for Server {
|
||||
async fn handle_calendar_get_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
is_head: bool,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
// Validate URI
|
||||
let resource_ = self
|
||||
.validate_uri(access_token, headers.uri)
|
||||
.await?
|
||||
.into_owned_uri()?;
|
||||
let account_id = resource_.account_id;
|
||||
let resources = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::Calendar,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let resource = resources
|
||||
.by_path(
|
||||
resource_
|
||||
.resource
|
||||
.ok_or(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))?,
|
||||
)
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
if resource.is_container() {
|
||||
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
|
||||
}
|
||||
|
||||
// Validate ACL
|
||||
if !access_token.is_member(account_id)
|
||||
&& !resources.has_access_to_container(
|
||||
access_token,
|
||||
resource.parent_id().unwrap(),
|
||||
Acl::ReadItems,
|
||||
)
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
// Fetch event
|
||||
let event_ = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::CalendarEvent,
|
||||
resource.document_id(),
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
let event = event_
|
||||
.unarchive::<CalendarEvent>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Validate headers
|
||||
let etag = event_.etag();
|
||||
let schedule_tag = event.schedule_tag.as_ref().map(|tag| tag.to_native());
|
||||
self.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![ResourceState {
|
||||
account_id,
|
||||
collection: Collection::CalendarEvent,
|
||||
document_id: resource.document_id().into(),
|
||||
etag: etag.clone().into(),
|
||||
path: resource_.resource.unwrap(),
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::GET,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let response = HttpResponse::new(StatusCode::OK)
|
||||
.with_content_type("text/calendar; charset=utf-8")
|
||||
.with_etag(etag)
|
||||
.with_schedule_tag_opt(schedule_tag)
|
||||
.with_last_modified(Rfc1123DateTime::new(i64::from(event.modified)).to_string());
|
||||
|
||||
let ical = event.data.event.to_string();
|
||||
|
||||
if !is_head {
|
||||
Ok(response.with_binary_body(ical))
|
||||
} else {
|
||||
Ok(response.with_content_length(ical.len()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::proppatch::CalendarPropPatchRequestHandler;
|
||||
use crate::{
|
||||
DavError, DavMethod, PropStatBuilder,
|
||||
common::{
|
||||
ExtractETag,
|
||||
lock::{LockRequestHandler, ResourceState},
|
||||
uri::DavUriResource,
|
||||
},
|
||||
};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use dav_proto::{
|
||||
RequestHeaders, Return,
|
||||
schema::{Namespace, request::MkCol, response::MkColResponse},
|
||||
};
|
||||
use groupware::{
|
||||
cache::GroupwareCache,
|
||||
calendar::{Calendar, CalendarPreferences},
|
||||
};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use store::write::BatchBuilder;
|
||||
use trc::AddContext;
|
||||
use types::collection::{Collection, SyncCollection};
|
||||
|
||||
pub(crate) trait CalendarMkColRequestHandler: Sync + Send {
|
||||
fn handle_calendar_mkcol_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
request: Option<MkCol>,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
}
|
||||
|
||||
impl CalendarMkColRequestHandler for Server {
|
||||
async fn handle_calendar_mkcol_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
request: Option<MkCol>,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
// Validate URI
|
||||
let resource = self
|
||||
.validate_uri(access_token, headers.uri)
|
||||
.await?
|
||||
.into_owned_uri()?;
|
||||
let account_id = resource.account_id;
|
||||
let name = resource
|
||||
.resource
|
||||
.ok_or(DavError::Code(StatusCode::FORBIDDEN))?;
|
||||
if !access_token.is_member(account_id) {
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
} else if name.contains('/')
|
||||
|| self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::Calendar,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.by_path(name)
|
||||
.is_some()
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
|
||||
}
|
||||
|
||||
// Validate headers
|
||||
self.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![ResourceState {
|
||||
account_id,
|
||||
collection: resource.collection,
|
||||
document_id: Some(u32::MAX),
|
||||
path: name,
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::MKCOL,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Build file container
|
||||
let mut calendar = Calendar {
|
||||
name: name.to_string(),
|
||||
preferences: vec![CalendarPreferences {
|
||||
account_id,
|
||||
name: name.to_string(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Apply MKCOL properties
|
||||
let mut return_prop_stat = None;
|
||||
let mut is_mkcalendar = false;
|
||||
if let Some(mkcol) = request {
|
||||
let mut prop_stat = PropStatBuilder::default();
|
||||
is_mkcalendar = mkcol.is_mkcalendar;
|
||||
if !self.apply_calendar_properties(
|
||||
access_token.personal_id(account_id, Collection::Calendar),
|
||||
&mut calendar,
|
||||
false,
|
||||
mkcol.props,
|
||||
&mut prop_stat,
|
||||
) {
|
||||
return Ok(HttpResponse::new(StatusCode::FORBIDDEN).with_xml_body(
|
||||
MkColResponse::new(prop_stat.build())
|
||||
.with_namespace(Namespace::CalDav)
|
||||
.with_mkcalendar(is_mkcalendar)
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
if headers.ret != Return::Minimal {
|
||||
return_prop_stat = Some(prop_stat);
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare write batch
|
||||
let mut batch = BatchBuilder::new();
|
||||
let document_id = self
|
||||
.store()
|
||||
.assign_document_ids(account_id, Collection::Calendar, 1)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
calendar
|
||||
.insert(
|
||||
access_token.account_tenant_ids(),
|
||||
account_id,
|
||||
document_id,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?;
|
||||
let etag = batch.etag();
|
||||
self.commit_batch(batch).await.caused_by(trc::location!())?;
|
||||
|
||||
if let Some(prop_stat) = return_prop_stat {
|
||||
Ok(HttpResponse::new(StatusCode::CREATED)
|
||||
.with_xml_body(
|
||||
MkColResponse::new(prop_stat.build())
|
||||
.with_namespace(Namespace::CalDav)
|
||||
.with_mkcalendar(is_mkcalendar)
|
||||
.to_string(),
|
||||
)
|
||||
.with_etag_opt(etag))
|
||||
} else {
|
||||
Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod copy_move;
|
||||
pub mod delete;
|
||||
pub mod freebusy;
|
||||
pub mod get;
|
||||
pub mod mkcol;
|
||||
pub mod proppatch;
|
||||
pub mod query;
|
||||
pub mod scheduling;
|
||||
pub mod update;
|
||||
|
||||
use crate::{DavError, DavErrorCondition};
|
||||
use common::{DavResources, Server};
|
||||
use dav_proto::schema::{
|
||||
property::{CalDavProperty, CalendarData, DavProperty, WebDavProperty},
|
||||
response::CalCondition,
|
||||
};
|
||||
use groupware::scheduling::ItipError;
|
||||
use hyper::StatusCode;
|
||||
use trc::AddContext;
|
||||
use types::{collection::Collection, field::CalendarEventField};
|
||||
|
||||
pub(crate) static CALENDAR_CONTAINER_PROPS: [DavProperty; 31] = [
|
||||
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::CalDav(CalDavProperty::CalendarDescription),
|
||||
DavProperty::CalDav(CalDavProperty::SupportedCalendarData),
|
||||
DavProperty::CalDav(CalDavProperty::SupportedCollationSet),
|
||||
DavProperty::CalDav(CalDavProperty::SupportedCalendarComponentSet),
|
||||
DavProperty::CalDav(CalDavProperty::CalendarTimezone),
|
||||
DavProperty::CalDav(CalDavProperty::MaxResourceSize),
|
||||
DavProperty::CalDav(CalDavProperty::MinDateTime),
|
||||
DavProperty::CalDav(CalDavProperty::MaxDateTime),
|
||||
DavProperty::CalDav(CalDavProperty::MaxInstances),
|
||||
DavProperty::CalDav(CalDavProperty::MaxAttendeesPerInstance),
|
||||
DavProperty::CalDav(CalDavProperty::TimezoneServiceSet),
|
||||
DavProperty::CalDav(CalDavProperty::TimezoneId),
|
||||
];
|
||||
|
||||
pub(crate) static CALENDAR_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::CalDav(CalDavProperty::CalendarData(CalendarData {
|
||||
properties: vec![],
|
||||
expand: None,
|
||||
limit_recurrence: None,
|
||||
limit_freebusy: None,
|
||||
})),
|
||||
];
|
||||
|
||||
pub(crate) async fn assert_is_unique_uid(
|
||||
server: &Server,
|
||||
resources: &DavResources,
|
||||
account_id: u32,
|
||||
calendar_id: u32,
|
||||
uid: Option<&str>,
|
||||
) -> crate::Result<()> {
|
||||
if let Some(uid) = uid {
|
||||
let hits = server
|
||||
.document_ids_matching(
|
||||
account_id,
|
||||
Collection::CalendarEvent,
|
||||
CalendarEventField::Uid,
|
||||
uid.as_bytes(),
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
if !hits.is_empty() {
|
||||
for path in resources.children(calendar_id) {
|
||||
if hits.contains(path.document_id()) {
|
||||
return Err(DavError::Condition(DavErrorCondition::new(
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
CalCondition::NoUidConflict(resources.format_resource(path).into()),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) trait ItipPrecondition {
|
||||
fn failed_precondition(&self) -> Option<CalCondition>;
|
||||
}
|
||||
|
||||
impl ItipPrecondition for ItipError {
|
||||
fn failed_precondition(&self) -> Option<CalCondition> {
|
||||
match self {
|
||||
ItipError::MultipleOrganizer => Some(CalCondition::SameOrganizerInAllComponents),
|
||||
ItipError::OrganizerIsLocalAddress
|
||||
| ItipError::SenderIsNotParticipant(_)
|
||||
| ItipError::OrganizerMismatch => Some(CalCondition::ValidOrganizer),
|
||||
ItipError::CannotModifyProperty(_)
|
||||
| ItipError::CannotModifyInstance
|
||||
| ItipError::CannotModifyAddress => Some(CalCondition::AllowedAttendeeObjectChange),
|
||||
ItipError::MissingUid
|
||||
| ItipError::MultipleUid
|
||||
| ItipError::MultipleObjectTypes
|
||||
| ItipError::MultipleObjectInstances
|
||||
| ItipError::MissingMethod
|
||||
| ItipError::InvalidComponentType
|
||||
| ItipError::OutOfSequence
|
||||
| ItipError::UnknownParticipant(_)
|
||||
| ItipError::UnsupportedMethod(_) => Some(CalCondition::ValidSchedulingMessage),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,565 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
DavError, DavMethod, PropStatBuilder,
|
||||
common::{
|
||||
ETag, ExtractETag,
|
||||
lock::{LockRequestHandler, ResourceState},
|
||||
uri::DavUriResource,
|
||||
},
|
||||
};
|
||||
use calcard::common::timezone::Tz;
|
||||
use common::{Server, auth::AccessToken};
|
||||
use dav_proto::{
|
||||
RequestHeaders, Return,
|
||||
schema::{
|
||||
Namespace,
|
||||
property::{CalDavProperty, DavProperty, DavValue, ResourceType, WebDavProperty},
|
||||
request::{DavPropertyValue, PropertyUpdate},
|
||||
response::{BaseCondition, CalCondition, MultiStatus, Response},
|
||||
},
|
||||
};
|
||||
use groupware::{
|
||||
cache::GroupwareCache,
|
||||
calendar::{Calendar, CalendarEvent, SupportedComponent, Timezone},
|
||||
};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use std::str::FromStr;
|
||||
use store::write::BatchBuilder;
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{AlignedBytes, Archive},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::Acl,
|
||||
collection::{Collection, SyncCollection},
|
||||
};
|
||||
use utils::map::bitmap::Bitmap;
|
||||
|
||||
pub(crate) trait CalendarPropPatchRequestHandler: Sync + Send {
|
||||
fn handle_calendar_proppatch_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
request: PropertyUpdate,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
|
||||
fn apply_calendar_properties(
|
||||
&self,
|
||||
personal_id: u32,
|
||||
calendar: &mut Calendar,
|
||||
is_update: bool,
|
||||
properties: Vec<DavPropertyValue>,
|
||||
items: &mut PropStatBuilder,
|
||||
) -> bool;
|
||||
|
||||
fn apply_event_properties(
|
||||
&self,
|
||||
event: &mut CalendarEvent,
|
||||
is_update: bool,
|
||||
properties: Vec<DavPropertyValue>,
|
||||
items: &mut PropStatBuilder,
|
||||
) -> bool;
|
||||
}
|
||||
|
||||
impl CalendarPropPatchRequestHandler for Server {
|
||||
async fn handle_calendar_proppatch_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
mut request: PropertyUpdate,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
// Validate URI
|
||||
let resource_ = self
|
||||
.validate_uri(access_token, headers.uri)
|
||||
.await?
|
||||
.into_owned_uri()?;
|
||||
let uri = headers.uri;
|
||||
let account_id = resource_.account_id;
|
||||
let resources = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::Calendar,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let resource = resource_
|
||||
.resource
|
||||
.and_then(|r| resources.by_path(r))
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
let document_id = resource.document_id();
|
||||
let collection = if resource.is_container() {
|
||||
Collection::Calendar
|
||||
} else {
|
||||
Collection::CalendarEvent
|
||||
};
|
||||
|
||||
if !request.has_changes() {
|
||||
return Ok(HttpResponse::new(StatusCode::NO_CONTENT));
|
||||
}
|
||||
|
||||
// Verify ACL
|
||||
if !access_token.is_member(account_id) {
|
||||
let (acl, document_id) = if resource.is_container() {
|
||||
(Acl::Modify, resource.document_id())
|
||||
} else {
|
||||
(Acl::ModifyItems, resource.parent_id().unwrap())
|
||||
};
|
||||
|
||||
if !resources.has_access_to_container(access_token, document_id, acl) {
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch archive
|
||||
let archive = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
collection,
|
||||
document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
|
||||
// Validate headers
|
||||
self.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![ResourceState {
|
||||
account_id,
|
||||
collection,
|
||||
document_id: document_id.into(),
|
||||
etag: archive.etag().into(),
|
||||
path: resource_.resource.unwrap(),
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::PROPPATCH,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let is_success;
|
||||
let mut batch = BatchBuilder::new();
|
||||
let mut items = PropStatBuilder::default();
|
||||
|
||||
let etag = if resource.is_container() {
|
||||
// Deserialize
|
||||
let calendar = archive
|
||||
.to_unarchived::<Calendar>()
|
||||
.caused_by(trc::location!())?;
|
||||
let mut new_calendar = archive
|
||||
.deserialize::<Calendar>()
|
||||
.caused_by(trc::location!())?;
|
||||
let personal_id = access_token.personal_id(account_id, Collection::Calendar);
|
||||
|
||||
// Remove properties
|
||||
if !request.set_first && !request.remove.is_empty() {
|
||||
remove_calendar_properties(
|
||||
personal_id,
|
||||
&mut new_calendar,
|
||||
std::mem::take(&mut request.remove),
|
||||
&mut items,
|
||||
);
|
||||
}
|
||||
|
||||
// Set properties
|
||||
is_success = self.apply_calendar_properties(
|
||||
personal_id,
|
||||
&mut new_calendar,
|
||||
true,
|
||||
request.set,
|
||||
&mut items,
|
||||
);
|
||||
|
||||
// Remove properties
|
||||
if is_success && !request.remove.is_empty() {
|
||||
remove_calendar_properties(
|
||||
personal_id,
|
||||
&mut new_calendar,
|
||||
request.remove,
|
||||
&mut items,
|
||||
);
|
||||
}
|
||||
|
||||
if is_success {
|
||||
new_calendar
|
||||
.update(
|
||||
access_token.account_tenant_ids(),
|
||||
calendar,
|
||||
account_id,
|
||||
document_id,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.etag()
|
||||
} else {
|
||||
calendar.etag().into()
|
||||
}
|
||||
} else {
|
||||
// Deserialize
|
||||
let event = archive
|
||||
.to_unarchived::<CalendarEvent>()
|
||||
.caused_by(trc::location!())?;
|
||||
let mut new_event = archive
|
||||
.deserialize::<CalendarEvent>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Remove properties
|
||||
if !request.set_first && !request.remove.is_empty() {
|
||||
remove_event_properties(
|
||||
&mut new_event,
|
||||
std::mem::take(&mut request.remove),
|
||||
&mut items,
|
||||
);
|
||||
}
|
||||
|
||||
// Set properties
|
||||
is_success = self.apply_event_properties(&mut new_event, true, request.set, &mut items);
|
||||
|
||||
// Remove properties
|
||||
if is_success && !request.remove.is_empty() {
|
||||
remove_event_properties(&mut new_event, request.remove, &mut items);
|
||||
}
|
||||
|
||||
if is_success {
|
||||
new_event
|
||||
.update(
|
||||
access_token.account_tenant_ids(),
|
||||
event,
|
||||
account_id,
|
||||
document_id,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.etag()
|
||||
} else {
|
||||
event.etag().into()
|
||||
}
|
||||
};
|
||||
|
||||
if is_success {
|
||||
self.commit_batch(batch).await.caused_by(trc::location!())?;
|
||||
}
|
||||
|
||||
if headers.ret != Return::Minimal || !is_success {
|
||||
Ok(HttpResponse::new(StatusCode::MULTI_STATUS)
|
||||
.with_xml_body(
|
||||
MultiStatus::new(vec![Response::new_propstat(uri, items.build())])
|
||||
.with_namespace(Namespace::CalDav)
|
||||
.to_string(),
|
||||
)
|
||||
.with_etag_opt(etag))
|
||||
} else {
|
||||
Ok(HttpResponse::new(StatusCode::NO_CONTENT).with_etag_opt(etag))
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_calendar_properties(
|
||||
&self,
|
||||
personal_id: u32,
|
||||
calendar: &mut Calendar,
|
||||
is_update: bool,
|
||||
properties: Vec<DavPropertyValue>,
|
||||
items: &mut PropStatBuilder,
|
||||
) -> bool {
|
||||
let mut has_errors = false;
|
||||
|
||||
for property in properties {
|
||||
match (&property.property, property.value) {
|
||||
(DavProperty::WebDav(WebDavProperty::DisplayName), DavValue::String(name)) => {
|
||||
if name.len() <= self.core.groupware.live_property_size {
|
||||
calendar.preferences_mut(personal_id).name = name;
|
||||
items.insert_ok(property.property);
|
||||
} else {
|
||||
items.insert_error_with_description(
|
||||
property.property,
|
||||
StatusCode::INSUFFICIENT_STORAGE,
|
||||
"Property value is too long",
|
||||
);
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
(
|
||||
DavProperty::CalDav(CalDavProperty::CalendarDescription),
|
||||
DavValue::String(name),
|
||||
) => {
|
||||
if name.len() <= self.core.groupware.live_property_size {
|
||||
calendar.preferences_mut(personal_id).description = Some(name);
|
||||
items.insert_ok(property.property);
|
||||
} else {
|
||||
items.insert_error_with_description(
|
||||
property.property,
|
||||
StatusCode::INSUFFICIENT_STORAGE,
|
||||
"Property value is too long",
|
||||
);
|
||||
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
(
|
||||
DavProperty::CalDav(CalDavProperty::CalendarTimezone),
|
||||
DavValue::ICalendar(ical),
|
||||
) => {
|
||||
if ical.size() > self.core.groupware.max_ical_size {
|
||||
items.insert_error_with_description(
|
||||
property.property,
|
||||
StatusCode::INSUFFICIENT_STORAGE,
|
||||
"Property value is too long",
|
||||
);
|
||||
has_errors = true;
|
||||
} else if !ical.is_timezone() {
|
||||
items.insert_precondition_failed_with_description(
|
||||
property.property,
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
CalCondition::ValidCalendarData,
|
||||
"Invalid calendar timezone",
|
||||
);
|
||||
has_errors = true;
|
||||
} else {
|
||||
calendar.preferences_mut(personal_id).time_zone = Timezone::Custom(ical);
|
||||
items.insert_ok(property.property);
|
||||
}
|
||||
}
|
||||
(DavProperty::CalDav(CalDavProperty::TimezoneId), DavValue::String(tz_id)) => {
|
||||
if let Ok(tz) = Tz::from_str(&tz_id) {
|
||||
calendar.preferences_mut(personal_id).time_zone =
|
||||
Timezone::IANA(tz.as_id());
|
||||
items.insert_ok(property.property);
|
||||
} else {
|
||||
items.insert_precondition_failed_with_description(
|
||||
property.property,
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
CalCondition::ValidTimezone,
|
||||
"Invalid timezone ID",
|
||||
);
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
(DavProperty::WebDav(WebDavProperty::CreationDate), DavValue::Timestamp(dt)) => {
|
||||
calendar.created = dt;
|
||||
items.insert_ok(property.property);
|
||||
}
|
||||
(
|
||||
DavProperty::WebDav(WebDavProperty::ResourceType),
|
||||
DavValue::ResourceTypes(types),
|
||||
) => {
|
||||
if !types
|
||||
.0
|
||||
.iter()
|
||||
.all(|rt| matches!(rt, ResourceType::Collection | ResourceType::Calendar))
|
||||
{
|
||||
items.insert_precondition_failed(
|
||||
property.property,
|
||||
StatusCode::FORBIDDEN,
|
||||
BaseCondition::ValidResourceType,
|
||||
);
|
||||
has_errors = true;
|
||||
} else {
|
||||
items.insert_ok(property.property);
|
||||
}
|
||||
}
|
||||
(
|
||||
DavProperty::CalDav(CalDavProperty::SupportedCalendarComponentSet),
|
||||
DavValue::Components(components),
|
||||
) => {
|
||||
if !is_update {
|
||||
calendar.supported_components = Bitmap::<SupportedComponent>::from_iter(
|
||||
components
|
||||
.0
|
||||
.into_iter()
|
||||
.map(|v| SupportedComponent::from(v.0)),
|
||||
)
|
||||
.into_inner();
|
||||
if calendar.supported_components != 0 {
|
||||
items.insert_ok(property.property);
|
||||
} else {
|
||||
items.insert_precondition_failed_with_description(
|
||||
property.property,
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
CalCondition::SupportedCalendarComponent,
|
||||
"At least one supported component must be specified",
|
||||
);
|
||||
has_errors = true;
|
||||
}
|
||||
} else {
|
||||
items.insert_precondition_failed_with_description(
|
||||
property.property,
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
CalCondition::SupportedCalendarComponent,
|
||||
"Property cannot be modified",
|
||||
);
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
(DavProperty::DeadProperty(dead), DavValue::DeadProperty(values))
|
||||
if self.core.groupware.dead_property_size.is_some() =>
|
||||
{
|
||||
if is_update {
|
||||
calendar.dead_properties.remove_element(dead);
|
||||
}
|
||||
|
||||
if calendar.dead_properties.size() + values.size() + dead.size()
|
||||
< self.core.groupware.dead_property_size.unwrap()
|
||||
{
|
||||
calendar.dead_properties.add_element(dead.clone(), values.0);
|
||||
items.insert_ok(property.property);
|
||||
} else {
|
||||
items.insert_error_with_description(
|
||||
property.property,
|
||||
StatusCode::INSUFFICIENT_STORAGE,
|
||||
"Property value is too long",
|
||||
);
|
||||
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
(_, DavValue::Null) => {
|
||||
items.insert_ok(property.property);
|
||||
}
|
||||
_ => {
|
||||
items.insert_error_with_description(
|
||||
property.property,
|
||||
StatusCode::CONFLICT,
|
||||
"Property cannot be modified",
|
||||
);
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
!has_errors
|
||||
}
|
||||
|
||||
fn apply_event_properties(
|
||||
&self,
|
||||
event: &mut CalendarEvent,
|
||||
is_update: bool,
|
||||
properties: Vec<DavPropertyValue>,
|
||||
items: &mut PropStatBuilder,
|
||||
) -> bool {
|
||||
let mut has_errors = false;
|
||||
|
||||
for property in properties {
|
||||
match (&property.property, property.value) {
|
||||
(DavProperty::WebDav(WebDavProperty::DisplayName), DavValue::String(name)) => {
|
||||
if name.len() <= self.core.groupware.live_property_size {
|
||||
event.display_name = Some(name);
|
||||
items.insert_ok(property.property);
|
||||
} else {
|
||||
items.insert_error_with_description(
|
||||
property.property,
|
||||
StatusCode::INSUFFICIENT_STORAGE,
|
||||
"Property value is too long",
|
||||
);
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
(DavProperty::WebDav(WebDavProperty::CreationDate), DavValue::Timestamp(dt)) => {
|
||||
event.created = dt;
|
||||
items.insert_ok(property.property);
|
||||
}
|
||||
(DavProperty::DeadProperty(dead), DavValue::DeadProperty(values))
|
||||
if self.core.groupware.dead_property_size.is_some() =>
|
||||
{
|
||||
if is_update {
|
||||
event.dead_properties.remove_element(dead);
|
||||
}
|
||||
|
||||
if event.dead_properties.size() + values.size() + dead.size()
|
||||
< self.core.groupware.dead_property_size.unwrap()
|
||||
{
|
||||
event.dead_properties.add_element(dead.clone(), values.0);
|
||||
items.insert_ok(property.property);
|
||||
} else {
|
||||
items.insert_error_with_description(
|
||||
property.property,
|
||||
StatusCode::INSUFFICIENT_STORAGE,
|
||||
"Property value is too long",
|
||||
);
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
(_, DavValue::Null) => {
|
||||
items.insert_ok(property.property);
|
||||
}
|
||||
_ => {
|
||||
items.insert_error_with_description(
|
||||
property.property,
|
||||
StatusCode::CONFLICT,
|
||||
"Property cannot be modified",
|
||||
);
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
!has_errors
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_event_properties(
|
||||
event: &mut CalendarEvent,
|
||||
properties: Vec<DavProperty>,
|
||||
items: &mut PropStatBuilder,
|
||||
) {
|
||||
for property in properties {
|
||||
match &property {
|
||||
DavProperty::WebDav(WebDavProperty::DisplayName) => {
|
||||
event.display_name = None;
|
||||
items.insert_with_status(property, StatusCode::NO_CONTENT);
|
||||
}
|
||||
DavProperty::DeadProperty(dead) => {
|
||||
event.dead_properties.remove_element(dead);
|
||||
items.insert_with_status(property, StatusCode::NO_CONTENT);
|
||||
}
|
||||
_ => {
|
||||
items.insert_error_with_description(
|
||||
property,
|
||||
StatusCode::CONFLICT,
|
||||
"Property cannot be deleted",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_calendar_properties(
|
||||
personal_id: u32,
|
||||
calendar: &mut Calendar,
|
||||
properties: Vec<DavProperty>,
|
||||
items: &mut PropStatBuilder,
|
||||
) {
|
||||
for property in properties {
|
||||
match &property {
|
||||
DavProperty::CalDav(CalDavProperty::CalendarDescription) => {
|
||||
calendar.preferences_mut(personal_id).description = None;
|
||||
items.insert_with_status(property, StatusCode::NO_CONTENT);
|
||||
}
|
||||
DavProperty::CalDav(CalDavProperty::CalendarTimezone)
|
||||
| DavProperty::CalDav(CalDavProperty::TimezoneId) => {
|
||||
calendar.preferences_mut(personal_id).time_zone = Timezone::Default;
|
||||
items.insert_with_status(property, StatusCode::NO_CONTENT);
|
||||
}
|
||||
DavProperty::DeadProperty(dead) => {
|
||||
calendar.dead_properties.remove_element(dead);
|
||||
items.insert_with_status(property, StatusCode::NO_CONTENT);
|
||||
}
|
||||
_ => {
|
||||
items.insert_error_with_description(
|
||||
property,
|
||||
StatusCode::CONFLICT,
|
||||
"Property cannot be deleted",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,657 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::freebusy::freebusy_in_range;
|
||||
use crate::{
|
||||
DavError,
|
||||
common::{
|
||||
CalendarFilter, DavQuery,
|
||||
propfind::{PropFindItem, PropFindRequestHandler},
|
||||
uri::DavUriResource,
|
||||
},
|
||||
};
|
||||
use calcard::{
|
||||
common::{PartialDateTime, timezone::Tz},
|
||||
icalendar::{
|
||||
ArchivedICalendar, ArchivedICalendarComponent, ArchivedICalendarEntry,
|
||||
ArchivedICalendarParameter, ArchivedICalendarProperty, ArchivedICalendarValue,
|
||||
ICalendarComponentType, ICalendarEntry, ICalendarParameterName, ICalendarProperty,
|
||||
ICalendarValue,
|
||||
},
|
||||
};
|
||||
use common::{DavResource, Server, auth::AccessToken};
|
||||
use dav_proto::{
|
||||
RequestHeaders,
|
||||
schema::{
|
||||
property::{CalDavProperty, CalendarData, DavProperty},
|
||||
request::{CalendarQuery, Filter, FilterOp, PropFind, Timezone},
|
||||
response::MultiStatus,
|
||||
},
|
||||
};
|
||||
use groupware::{
|
||||
cache::GroupwareCache,
|
||||
calendar::{ArchivedCalendarEvent, expand::CalendarEventExpansion},
|
||||
};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use std::{fmt::Write, slice::Iter, str::FromStr};
|
||||
use store::{
|
||||
ahash::{AHashMap, AHashSet},
|
||||
write::serialize::rkyv_deserialize,
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{TimeRange, acl::Acl, collection::SyncCollection};
|
||||
|
||||
pub(crate) trait CalendarQueryRequestHandler: Sync + Send {
|
||||
fn handle_calendar_query_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
request: CalendarQuery,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
}
|
||||
|
||||
impl CalendarQueryRequestHandler for Server {
|
||||
async fn handle_calendar_query_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
request: CalendarQuery,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
// Validate URI
|
||||
let resource_ = self
|
||||
.validate_uri(access_token, headers.uri)
|
||||
.await?
|
||||
.into_owned_uri()?;
|
||||
let account_id = resource_.account_id;
|
||||
let resources = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::Calendar,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let Some(resource) = resources.by_path(
|
||||
resource_
|
||||
.resource
|
||||
.ok_or(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))?,
|
||||
) else {
|
||||
return Ok(HttpResponse::new(StatusCode::MULTI_STATUS)
|
||||
.with_xml_body(MultiStatus::not_found(headers.uri).to_string()));
|
||||
};
|
||||
if !resource.is_container() {
|
||||
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
|
||||
}
|
||||
|
||||
// Obtain shared ids
|
||||
let shared_ids = if !access_token.is_member(account_id) {
|
||||
resources
|
||||
.shared_items(access_token, [Acl::ReadItems], false)
|
||||
.into()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Pre-filter by date range
|
||||
let filter_range = extract_filter_range(&request);
|
||||
|
||||
// Obtain document ids in folder
|
||||
let mut items = Vec::with_capacity(16);
|
||||
for resource in resources.children(resource.document_id()) {
|
||||
if shared_ids
|
||||
.as_ref()
|
||||
.is_none_or(|ids| ids.contains(resource.document_id()))
|
||||
&& filter_range
|
||||
.as_ref()
|
||||
.is_none_or(|range| is_resource_in_time_range(resource.resource, range))
|
||||
{
|
||||
items.push(PropFindItem::new(
|
||||
resources.format_resource(resource),
|
||||
account_id,
|
||||
resource,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Extract the time range from the request
|
||||
let max_time_range = extract_data_range(&request.properties, filter_range);
|
||||
|
||||
self.handle_dav_query(
|
||||
access_token,
|
||||
DavQuery::calendar_query(request, max_time_range, items, headers),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_resource_in_time_range(resource: &DavResource, filter: &TimeRange) -> bool {
|
||||
// Check whether the resource has a time range and if it overlaps with the filter
|
||||
if let Some((start, end)) = resource.event_time_range() {
|
||||
((filter.start < end) || (filter.start <= start))
|
||||
&& (filter.end > start || filter.end >= end)
|
||||
} else {
|
||||
// If the resource does not have a time range, it is not in the range
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_filter_range(query: &CalendarQuery) -> Option<TimeRange> {
|
||||
let mut range = TimeRange {
|
||||
start: i64::MAX,
|
||||
end: i64::MIN,
|
||||
};
|
||||
|
||||
for filter in &query.filters {
|
||||
let op = match filter {
|
||||
Filter::Component { op, .. } => op,
|
||||
Filter::Property { op, .. } => op,
|
||||
Filter::Parameter { op, .. } => op,
|
||||
_ => continue,
|
||||
};
|
||||
if let FilterOp::TimeRange(date_range) = op {
|
||||
if date_range.start < range.start {
|
||||
range.start = date_range.start;
|
||||
}
|
||||
if date_range.end > range.end {
|
||||
range.end = date_range.end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if range.start != i64::MAX {
|
||||
Some(range)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_data_range(propfind: &PropFind, filter_range: Option<TimeRange>) -> Option<TimeRange> {
|
||||
let props = match propfind {
|
||||
PropFind::AllProp(props) | PropFind::Prop(props) => props,
|
||||
PropFind::PropName => &[][..],
|
||||
};
|
||||
|
||||
for prop in props {
|
||||
if let DavProperty::CalDav(CalDavProperty::CalendarData(data)) = prop {
|
||||
let mut range = filter_range.unwrap_or(TimeRange {
|
||||
start: i64::MAX,
|
||||
end: i64::MIN,
|
||||
});
|
||||
|
||||
for data_range in [&data.expand, &data.limit_recurrence, &data.limit_freebusy]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
if data_range.start < range.start {
|
||||
range.start = data_range.start;
|
||||
}
|
||||
if data_range.end > range.end {
|
||||
range.end = data_range.end;
|
||||
}
|
||||
}
|
||||
|
||||
return if range.start != i64::MAX {
|
||||
Some(range)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
filter_range
|
||||
}
|
||||
|
||||
pub fn try_parse_tz(tz: &Timezone) -> Option<Tz> {
|
||||
match tz {
|
||||
Timezone::Name(value) | Timezone::Id(value) => Tz::from_str(value).ok(),
|
||||
Timezone::None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct CalendarQueryHandler {
|
||||
default_tz: Tz,
|
||||
expanded_times: Vec<CalendarEventExpansion>,
|
||||
}
|
||||
|
||||
impl CalendarQueryHandler {
|
||||
pub fn new(
|
||||
event: &ArchivedCalendarEvent,
|
||||
max_time_range: Option<TimeRange>,
|
||||
default_tz: Tz,
|
||||
) -> Self {
|
||||
Self {
|
||||
default_tz,
|
||||
expanded_times: max_time_range
|
||||
.map(|max_time_range| {
|
||||
event
|
||||
.data
|
||||
.expand(default_tz, max_time_range)
|
||||
.unwrap_or_else(|| {
|
||||
trc::event!(
|
||||
Calendar(trc::CalendarEvent::RuleExpansionError),
|
||||
Reason = "chrono error",
|
||||
Details = event.data.event.to_string(),
|
||||
);
|
||||
vec![]
|
||||
})
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn filter(&mut self, event: &ArchivedCalendarEvent, filters: &CalendarFilter) -> bool {
|
||||
let ical = &event.data.event;
|
||||
let mut is_all = true;
|
||||
let mut matches_one = false;
|
||||
|
||||
for filter in filters {
|
||||
match filter {
|
||||
Filter::AnyOf => {
|
||||
is_all = false;
|
||||
}
|
||||
Filter::AllOf => {
|
||||
is_all = true;
|
||||
}
|
||||
Filter::Property { prop, op, comp } => {
|
||||
let mut properties = find_components(ical, comp)
|
||||
.flat_map(|(_, comp)| find_properties(comp, prop))
|
||||
.peekable();
|
||||
|
||||
let result = if properties.peek().is_some() {
|
||||
properties.any(|entry| {
|
||||
match op {
|
||||
FilterOp::Exists => true,
|
||||
FilterOp::Undefined => false,
|
||||
FilterOp::TextMatch(text_match) => {
|
||||
let mut matched_any = false;
|
||||
|
||||
for value in entry.values.iter() {
|
||||
if let Some(text) = value.as_text()
|
||||
&& text_match.matches(text)
|
||||
{
|
||||
matched_any = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
matched_any
|
||||
}
|
||||
FilterOp::TimeRange(range) => {
|
||||
if let Some(ArchivedICalendarValue::PartialDateTime(date)) =
|
||||
entry.values.first()
|
||||
{
|
||||
let tz = entry
|
||||
.tz_id()
|
||||
.and_then(|tz_id| Tz::from_str(tz_id).ok())
|
||||
.unwrap_or(self.default_tz);
|
||||
|
||||
if let Some(date) = date
|
||||
.to_date_time()
|
||||
.and_then(|date| date.to_date_time_with_tz(tz))
|
||||
{
|
||||
let timestamp = date.timestamp();
|
||||
// RFC4791#9.9: start <= DTSTART AND end > DTSTART
|
||||
range.start <= timestamp && range.end > timestamp
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
matches!(op, FilterOp::Undefined)
|
||||
};
|
||||
|
||||
if result {
|
||||
matches_one = true;
|
||||
} else if is_all {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Filter::Parameter {
|
||||
prop,
|
||||
param,
|
||||
op,
|
||||
comp,
|
||||
} => {
|
||||
let mut parameters = find_components(ical, comp)
|
||||
.flat_map(|(_, comp)| {
|
||||
find_properties(comp, prop)
|
||||
.filter_map(|entry| find_parameter(entry, param))
|
||||
})
|
||||
.peekable();
|
||||
|
||||
let result = if parameters.peek().is_some() {
|
||||
parameters.any(|entry| match op {
|
||||
FilterOp::Exists => true,
|
||||
FilterOp::Undefined => false,
|
||||
FilterOp::TextMatch(text_match) => {
|
||||
if let Some(text) = entry.value.as_text() {
|
||||
text_match.matches(text)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
FilterOp::TimeRange(_) => false,
|
||||
})
|
||||
} else {
|
||||
matches!(op, FilterOp::Undefined)
|
||||
};
|
||||
|
||||
if result {
|
||||
matches_one = true;
|
||||
} else if is_all {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Filter::Component { comp, op } => {
|
||||
let result = match op {
|
||||
FilterOp::Exists => find_components(ical, comp).next().is_some(),
|
||||
FilterOp::Undefined => find_components(ical, comp).next().is_none(),
|
||||
FilterOp::TimeRange(range) => {
|
||||
if !matches!(comp.last(), Some(ICalendarComponentType::VAlarm)) {
|
||||
let matching_comp_ids = find_components(ical, comp)
|
||||
.map(|(id, comp)| (id as u32, &comp.component_type))
|
||||
.collect::<AHashMap<_, _>>();
|
||||
|
||||
!matching_comp_ids.is_empty()
|
||||
&& self.expanded_times.iter().any(|event| {
|
||||
matching_comp_ids.get(&event.comp_id).is_some_and(|ct| {
|
||||
range.is_in_range(
|
||||
ct == &&ICalendarComponentType::VTodo,
|
||||
event.start,
|
||||
event.end,
|
||||
)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
let matching_comp_ids = event
|
||||
.data
|
||||
.alarms
|
||||
.iter()
|
||||
.map(|alarm| alarm.parent_id.to_native() as u32)
|
||||
.collect::<AHashSet<_>>();
|
||||
|
||||
!matching_comp_ids.is_empty()
|
||||
&& self.expanded_times.iter().any(|time| {
|
||||
matching_comp_ids.contains(&time.comp_id)
|
||||
&& event.data.alarms.iter().any(|alarm| {
|
||||
alarm.parent_id.to_native() as u32 == time.comp_id
|
||||
&& alarm
|
||||
.delta
|
||||
.to_timestamp(
|
||||
time.start,
|
||||
time.end,
|
||||
self.default_tz,
|
||||
)
|
||||
.is_some_and(|timestamp| {
|
||||
range.is_in_range(
|
||||
false, timestamp, timestamp,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
FilterOp::TextMatch(_) => false,
|
||||
};
|
||||
|
||||
if result {
|
||||
matches_one = true;
|
||||
} else if is_all {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is_all || matches_one
|
||||
}
|
||||
|
||||
pub fn serialize_ical(
|
||||
&mut self,
|
||||
event: &ArchivedCalendarEvent,
|
||||
data: &CalendarData,
|
||||
instances_limit: &mut usize,
|
||||
) -> Option<String> {
|
||||
let mut out = String::with_capacity(event.size.to_native() as usize);
|
||||
let _v = [0.into()];
|
||||
let mut component_iter: Iter<'_, rkyv::rend::u32_le> = _v.iter();
|
||||
let mut component_stack: Vec<(&ArchivedICalendarComponent, Iter<'_, rkyv::rend::u32_le>)> =
|
||||
Vec::with_capacity(4);
|
||||
|
||||
if data.expand.is_some() {
|
||||
self.expanded_times.sort_unstable_by_key(|a| a.start);
|
||||
}
|
||||
|
||||
loop {
|
||||
if let Some(component_id) = component_iter.next() {
|
||||
let component_id = component_id.to_native();
|
||||
let component = event
|
||||
.data
|
||||
.event
|
||||
.components
|
||||
.get(component_id as usize)
|
||||
.unwrap();
|
||||
|
||||
// Limit recurrence override
|
||||
if let Some(limit_recurrence) = &data.limit_recurrence
|
||||
&& component.is_recurrence_override()
|
||||
&& !self.expanded_times.iter().any(|event| {
|
||||
event.comp_id == component_id
|
||||
&& limit_recurrence.is_in_range(
|
||||
component.component_type == ICalendarComponentType::VTodo,
|
||||
event.start,
|
||||
event.end,
|
||||
)
|
||||
})
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Limit freebusy
|
||||
if let Some(limit_recurrence) = &data.limit_freebusy
|
||||
&& component.component_type == ICalendarComponentType::VFreebusy
|
||||
&& !self.expanded_times.iter().any(|event| {
|
||||
event.comp_id == component_id
|
||||
&& limit_recurrence.is_in_range(false, event.start, event.end)
|
||||
})
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Filter entries
|
||||
let mut entries = component
|
||||
.entries
|
||||
.iter()
|
||||
.filter_map(|entry| {
|
||||
if data.properties.is_empty()
|
||||
|| component.component_type == ICalendarComponentType::VCalendar
|
||||
{
|
||||
Some((entry, true))
|
||||
} else {
|
||||
data.properties
|
||||
.iter()
|
||||
.find(|prop| {
|
||||
prop.component.as_ref().is_none_or(|comp| {
|
||||
comp == &component.component_type
|
||||
|| component_stack.iter().any(|(parent_comp, _)| {
|
||||
comp == &parent_comp.component_type
|
||||
})
|
||||
}) && prop.name.as_ref().is_none_or(|name| name == &entry.name)
|
||||
})
|
||||
.map(|prop| (entry, !prop.no_value))
|
||||
}
|
||||
})
|
||||
.peekable();
|
||||
|
||||
// Expand recurrences
|
||||
let component_name = component.component_type.as_str();
|
||||
if let Some(expand) = &data
|
||||
.expand
|
||||
.filter(|_| component.component_type.has_time_ranges())
|
||||
{
|
||||
let is_recurrent = component.is_recurrent();
|
||||
let is_recurrent_or_override =
|
||||
is_recurrent || component.is_recurrence_override();
|
||||
let is_todo = component.component_type == ICalendarComponentType::VTodo;
|
||||
let mut has_duration = false;
|
||||
let entries = entries
|
||||
.filter(|(entry, _)| match &entry.name {
|
||||
ArchivedICalendarProperty::Dtstart
|
||||
| ArchivedICalendarProperty::Dtend
|
||||
| ArchivedICalendarProperty::Exdate
|
||||
| ArchivedICalendarProperty::Exrule
|
||||
| ArchivedICalendarProperty::Rdate
|
||||
| ArchivedICalendarProperty::Rrule
|
||||
| ArchivedICalendarProperty::RecurrenceId => false,
|
||||
ArchivedICalendarProperty::Due
|
||||
| ArchivedICalendarProperty::Completed
|
||||
| ArchivedICalendarProperty::Created => is_recurrent,
|
||||
ArchivedICalendarProperty::Duration => {
|
||||
has_duration = true;
|
||||
true
|
||||
}
|
||||
_ => true,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for event in &self.expanded_times {
|
||||
if event.comp_id == component_id
|
||||
&& (!is_recurrent_or_override
|
||||
|| expand.is_in_range(is_todo, event.start, event.end))
|
||||
{
|
||||
if *instances_limit > 0 {
|
||||
*instances_limit -= 1;
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
let _ = write!(&mut out, "BEGIN:{component_name}\r\n");
|
||||
|
||||
// Write DTSTART, DTEND and RECURRENCE-ID
|
||||
let mut entry = ICalendarEntry {
|
||||
name: ICalendarProperty::Dtstart,
|
||||
params: vec![],
|
||||
values: vec![ICalendarValue::PartialDateTime(Box::new(
|
||||
PartialDateTime::from_utc_timestamp(event.start),
|
||||
))],
|
||||
};
|
||||
let _ = entry.write_to(&mut out);
|
||||
if is_recurrent_or_override {
|
||||
entry.name = ICalendarProperty::RecurrenceId;
|
||||
let _ = entry.write_to(&mut out);
|
||||
}
|
||||
if !has_duration {
|
||||
entry.name = ICalendarProperty::Dtend;
|
||||
entry.values = vec![ICalendarValue::PartialDateTime(Box::new(
|
||||
PartialDateTime::from_utc_timestamp(event.end),
|
||||
))];
|
||||
let _ = entry.write_to(&mut out);
|
||||
}
|
||||
|
||||
// Write other component entries
|
||||
for (entry, with_value) in &entries {
|
||||
let _ = entry.write_to(&mut out, *with_value);
|
||||
}
|
||||
let _ = write!(&mut out, "END:{component_name}\r\n");
|
||||
}
|
||||
}
|
||||
} else if entries.peek().is_some()
|
||||
|| (component.component_type == ICalendarComponentType::VCalendar
|
||||
&& !component.component_ids.is_empty())
|
||||
{
|
||||
let _ = write!(&mut out, "BEGIN:{component_name}\r\n");
|
||||
|
||||
match data.limit_freebusy {
|
||||
Some(range)
|
||||
if component.component_type == ICalendarComponentType::VFreebusy =>
|
||||
{
|
||||
// Filter freebusy
|
||||
for (entry, with_value) in entries {
|
||||
if matches!(entry.name, ArchivedICalendarProperty::Freebusy) {
|
||||
let mut fb_in_range =
|
||||
freebusy_in_range(entry, &range, self.default_tz)
|
||||
.peekable();
|
||||
if fb_in_range.peek().is_none() {
|
||||
continue;
|
||||
} else {
|
||||
let _ = ICalendarEntry {
|
||||
name: ICalendarProperty::Freebusy,
|
||||
params: rkyv_deserialize(&entry.params)
|
||||
.ok()
|
||||
.unwrap_or_default(),
|
||||
values: fb_in_range.collect(),
|
||||
}
|
||||
.write_to(&mut out);
|
||||
}
|
||||
} else {
|
||||
let _ = entry.write_to(&mut out, with_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
for (entry, with_value) in entries {
|
||||
let _ = entry.write_to(&mut out, with_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !component.component_ids.is_empty() {
|
||||
component_stack.push((component, component_iter));
|
||||
component_iter = component.component_ids.iter();
|
||||
} else if component.component_ids.is_empty() {
|
||||
let _ = write!(&mut out, "END:{component_name}\r\n");
|
||||
}
|
||||
}
|
||||
} else if let Some((component, iter)) = component_stack.pop() {
|
||||
let _ = write!(&mut out, "END:{}\r\n", component.component_type.as_str());
|
||||
component_iter = iter;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Some(out)
|
||||
}
|
||||
|
||||
pub fn into_expanded_times(self) -> Vec<CalendarEventExpansion> {
|
||||
self.expanded_times
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn find_components<'x>(
|
||||
ical: &'x ArchivedICalendar,
|
||||
comp: &[ICalendarComponentType],
|
||||
) -> impl Iterator<Item = (usize, &'x ArchivedICalendarComponent)> {
|
||||
// TODO: Properly expand the component type path
|
||||
let comp = comp.last().unwrap_or(&ICalendarComponentType::VCalendar);
|
||||
ical.components
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(move |(_, entry)| {
|
||||
comp == &ICalendarComponentType::VCalendar || &entry.component_type == comp
|
||||
})
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn find_properties<'x>(
|
||||
comp: &'x ArchivedICalendarComponent,
|
||||
prop: &ICalendarProperty,
|
||||
) -> impl Iterator<Item = &'x ArchivedICalendarEntry> {
|
||||
comp.entries.iter().filter(move |entry| &entry.name == prop)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn find_parameter<'x>(
|
||||
entry: &'x ArchivedICalendarEntry,
|
||||
name: &ICalendarParameterName,
|
||||
) -> Option<&'x ArchivedICalendarParameter> {
|
||||
entry.params.iter().find(|param| param.name == *name)
|
||||
}
|
||||
@@ -0,0 +1,438 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
DavError, DavErrorCondition, DavMethod,
|
||||
calendar::freebusy::CalendarFreebusyRequestHandler,
|
||||
common::{
|
||||
ETag,
|
||||
lock::{LockRequestHandler, ResourceState},
|
||||
uri::DavUriResource,
|
||||
},
|
||||
};
|
||||
use calcard::{
|
||||
Entry, Parser,
|
||||
icalendar::{
|
||||
ICalendarComponentType, ICalendarEntry, ICalendarMethod, ICalendarProperty, ICalendarValue,
|
||||
Uri,
|
||||
},
|
||||
};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use dav_proto::{
|
||||
RequestHeaders,
|
||||
schema::{
|
||||
property::Rfc1123DateTime,
|
||||
request::FreeBusyQuery,
|
||||
response::{CalCondition, Href, ScheduleResponse, ScheduleResponseItem},
|
||||
},
|
||||
};
|
||||
use groupware::{
|
||||
DestroyArchive, cache::GroupwareCache, calendar::CalendarEventNotification, strip_mailto_scheme,
|
||||
};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{AlignedBytes, Archive},
|
||||
};
|
||||
use store::{ahash::AHashMap, write::BatchBuilder};
|
||||
use trc::AddContext;
|
||||
use types::collection::{Collection, SyncCollection};
|
||||
use utils::sanitize_email;
|
||||
|
||||
pub(crate) trait CalendarEventNotificationHandler: Sync + Send {
|
||||
fn handle_scheduling_get_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
is_head: bool,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
|
||||
fn handle_scheduling_delete_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
|
||||
fn handle_scheduling_post_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
bytes: Vec<u8>,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
}
|
||||
|
||||
impl CalendarEventNotificationHandler for Server {
|
||||
async fn handle_scheduling_get_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
is_head: bool,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
// Validate URI
|
||||
let resource_ = self
|
||||
.validate_uri(access_token, headers.uri)
|
||||
.await?
|
||||
.into_owned_uri()?;
|
||||
let account_id = resource_.account_id;
|
||||
let resources = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::CalendarEventNotification,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let resource = resources
|
||||
.by_path(
|
||||
resource_
|
||||
.resource
|
||||
.ok_or(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))?,
|
||||
)
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
if resource.is_container() {
|
||||
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
|
||||
}
|
||||
|
||||
// Validate ACL
|
||||
if !access_token.is_member(account_id) {
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
// Fetch event
|
||||
let event_ = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::CalendarEventNotification,
|
||||
resource.document_id(),
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
let event = event_
|
||||
.unarchive::<CalendarEventNotification>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Validate headers
|
||||
let etag = event_.etag();
|
||||
self.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![ResourceState {
|
||||
account_id,
|
||||
collection: Collection::CalendarEventNotification,
|
||||
document_id: resource.document_id().into(),
|
||||
etag: etag.clone().into(),
|
||||
path: resource_.resource.unwrap(),
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::GET,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let response = HttpResponse::new(StatusCode::OK)
|
||||
.with_content_type("text/calendar; charset=utf-8")
|
||||
.with_etag(etag)
|
||||
.with_last_modified(Rfc1123DateTime::new(i64::from(event.modified)).to_string());
|
||||
|
||||
let ical = event.event.to_string();
|
||||
|
||||
if !is_head {
|
||||
Ok(response.with_binary_body(ical))
|
||||
} else {
|
||||
Ok(response.with_content_length(ical.len()))
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_scheduling_delete_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
// Validate URI
|
||||
let resource = self
|
||||
.validate_uri(access_token, headers.uri)
|
||||
.await?
|
||||
.into_owned_uri()?;
|
||||
let account_id = resource.account_id;
|
||||
let delete_path = resource
|
||||
.resource
|
||||
.filter(|r| !r.is_empty())
|
||||
.ok_or(DavError::Code(StatusCode::FORBIDDEN))?;
|
||||
let resources = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::CalendarEventNotification,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Check resource type
|
||||
let resource = resources
|
||||
.by_path(delete_path)
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
if resource.is_container() {
|
||||
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
|
||||
}
|
||||
|
||||
// Validate ACL
|
||||
if !access_token.is_member(account_id) {
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
let document_id = resource.document_id();
|
||||
let event_ = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::CalendarEventNotification,
|
||||
document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
|
||||
// Validate headers
|
||||
self.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![ResourceState {
|
||||
account_id,
|
||||
collection: Collection::CalendarEventNotification,
|
||||
document_id: document_id.into(),
|
||||
etag: event_.etag().into(),
|
||||
path: delete_path,
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::DELETE,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let event = event_
|
||||
.to_unarchived::<CalendarEventNotification>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Delete event
|
||||
let mut batch = BatchBuilder::new();
|
||||
DestroyArchive(event)
|
||||
.delete(
|
||||
access_token.account_tenant_ids(),
|
||||
account_id,
|
||||
document_id,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
self.commit_batch(batch).await.caused_by(trc::location!())?;
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::NO_CONTENT))
|
||||
}
|
||||
|
||||
async fn handle_scheduling_post_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
bytes: Vec<u8>,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
// Validate URI
|
||||
let resource = self
|
||||
.validate_uri(access_token, headers.uri)
|
||||
.await?
|
||||
.into_owned_uri()?;
|
||||
if resource.resource.is_none_or(|r| r != "outbox") {
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
// Parse iTIP message
|
||||
if bytes.len() > self.core.groupware.max_ical_size {
|
||||
return Err(DavError::Condition(DavErrorCondition::new(
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
CalCondition::MaxResourceSize(self.core.groupware.max_ical_size as u32),
|
||||
)));
|
||||
}
|
||||
let itip_raw = std::str::from_utf8(&bytes).map_err(|_| {
|
||||
DavError::Condition(
|
||||
DavErrorCondition::new(
|
||||
StatusCode::BAD_REQUEST,
|
||||
CalCondition::ValidSchedulingMessage,
|
||||
)
|
||||
.with_details("Invalid UTF-8 in iCalendar data"),
|
||||
)
|
||||
})?;
|
||||
let itip = match Parser::new(itip_raw).entry() {
|
||||
Entry::ICalendar(ical) if ical.components.len() > 1 => ical,
|
||||
_ => {
|
||||
return Err(DavError::Condition(
|
||||
DavErrorCondition::new(
|
||||
StatusCode::BAD_REQUEST,
|
||||
CalCondition::ValidSchedulingMessage,
|
||||
)
|
||||
.with_details("Failed to parse iCalendar data"),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Parse request
|
||||
let mut from_date = None;
|
||||
let mut to_date = None;
|
||||
let mut organizer = None;
|
||||
let mut attendees = AHashMap::new();
|
||||
let mut uid = None;
|
||||
let tz_resolver = itip.build_tz_resolver();
|
||||
let mut found_freebusy = false;
|
||||
|
||||
for component in &itip.components {
|
||||
if component.component_type != ICalendarComponentType::VFreebusy {
|
||||
continue;
|
||||
} else if !found_freebusy {
|
||||
found_freebusy = true;
|
||||
} else {
|
||||
return Err(DavError::Condition(
|
||||
DavErrorCondition::new(
|
||||
StatusCode::BAD_REQUEST,
|
||||
CalCondition::ValidSchedulingMessage,
|
||||
)
|
||||
.with_details("Multiple VFREEBUSY components found"),
|
||||
));
|
||||
}
|
||||
|
||||
for entry in &component.entries {
|
||||
let tz_id = entry.tz_id();
|
||||
match (&entry.name, entry.values.first()) {
|
||||
(ICalendarProperty::Dtstart, Some(ICalendarValue::PartialDateTime(dt))) => {
|
||||
from_date = dt.to_date_time_with_tz(tz_resolver.resolve_or_default(tz_id));
|
||||
}
|
||||
(ICalendarProperty::Dtend, Some(ICalendarValue::PartialDateTime(dt))) => {
|
||||
to_date = dt.to_date_time_with_tz(tz_resolver.resolve_or_default(tz_id));
|
||||
}
|
||||
(ICalendarProperty::Uid, Some(ICalendarValue::Text(_))) => {
|
||||
uid = Some(entry);
|
||||
}
|
||||
(
|
||||
ICalendarProperty::Organizer,
|
||||
Some(ICalendarValue::Text(_) | ICalendarValue::Uri(Uri::Location(_))),
|
||||
) => {
|
||||
organizer = Some(entry);
|
||||
}
|
||||
(
|
||||
ICalendarProperty::Attendee,
|
||||
Some(
|
||||
ICalendarValue::Text(value) | ICalendarValue::Uri(Uri::Location(value)),
|
||||
),
|
||||
) => {
|
||||
if let Some(email) = sanitize_email(strip_mailto_scheme(value.as_str())) {
|
||||
attendees.insert(email, entry);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (Some(from_date), Some(to_date)) = (from_date, to_date) else {
|
||||
return Err(DavError::Condition(
|
||||
DavErrorCondition::new(
|
||||
StatusCode::BAD_REQUEST,
|
||||
CalCondition::ValidSchedulingMessage,
|
||||
)
|
||||
.with_details("Missing DTSTART or DTEND in VFREEBUSY component"),
|
||||
));
|
||||
};
|
||||
let Some(organizer) = organizer else {
|
||||
return Err(DavError::Condition(
|
||||
DavErrorCondition::new(
|
||||
StatusCode::BAD_REQUEST,
|
||||
CalCondition::ValidSchedulingMessage,
|
||||
)
|
||||
.with_details("Missing ORGANIZER in VFREEBUSY component"),
|
||||
));
|
||||
};
|
||||
if attendees.is_empty() {
|
||||
return Err(DavError::Condition(
|
||||
DavErrorCondition::new(
|
||||
StatusCode::BAD_REQUEST,
|
||||
CalCondition::ValidSchedulingMessage,
|
||||
)
|
||||
.with_details("Missing ATTENDEE in VFREEBUSY component"),
|
||||
));
|
||||
}
|
||||
|
||||
let mut response = ScheduleResponse::default();
|
||||
|
||||
for (email, attendee) in attendees {
|
||||
if let Some(account_id) = self
|
||||
.account_id_from_email(&email, true)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
let resources = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::Calendar,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
if let Some(resource) = self
|
||||
.core
|
||||
.groupware
|
||||
.default_calendar_name
|
||||
.as_ref()
|
||||
.and_then(|name| resources.by_path(name))
|
||||
{
|
||||
let mut free_busy = self
|
||||
.build_freebusy_object(
|
||||
access_token,
|
||||
FreeBusyQuery::new(from_date.timestamp(), to_date.timestamp()),
|
||||
&resources,
|
||||
account_id,
|
||||
resource,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Add iTIP method
|
||||
free_busy.components[0].entries.push(ICalendarEntry {
|
||||
name: ICalendarProperty::Method,
|
||||
params: vec![],
|
||||
values: vec![ICalendarValue::Method(ICalendarMethod::Reply)],
|
||||
});
|
||||
|
||||
// Add properties
|
||||
let component = &mut free_busy.components[1];
|
||||
component.entries.push(organizer.clone());
|
||||
component.entries.push(attendee.clone());
|
||||
if let Some(uid) = uid {
|
||||
component.entries.push(uid.clone());
|
||||
}
|
||||
|
||||
response.items.0.push(ScheduleResponseItem {
|
||||
recipient: Href(format!("mailto:{email}")),
|
||||
request_status: "2.0;Success".into(),
|
||||
calendar_data: Some(free_busy.to_string()),
|
||||
});
|
||||
} else {
|
||||
response.items.0.push(ScheduleResponseItem {
|
||||
recipient: Href(format!("mailto:{email}")),
|
||||
request_status: "3.7;Default calendar not found".into(),
|
||||
calendar_data: None,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
response.items.0.push(ScheduleResponseItem {
|
||||
recipient: Href(format!("mailto:{email}")),
|
||||
request_status: "3.7;Invalid calendar user or insufficient permissions".into(),
|
||||
calendar_data: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::OK).with_xml_body(response.to_string()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,536 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::assert_is_unique_uid;
|
||||
use crate::{
|
||||
DavError, DavErrorCondition, DavMethod,
|
||||
calendar::ItipPrecondition,
|
||||
common::{
|
||||
ETag, ExtractETag,
|
||||
lock::{LockRequestHandler, ResourceState},
|
||||
uri::DavUriResource,
|
||||
},
|
||||
file::DavFileResource,
|
||||
fix_percent_encoding,
|
||||
};
|
||||
use calcard::{
|
||||
Entry, Parser,
|
||||
common::timezone::Tz,
|
||||
icalendar::{ICalendar, ICalendarComponentType},
|
||||
};
|
||||
use common::{DavName, Server, auth::AccessToken};
|
||||
use dav_proto::{
|
||||
RequestHeaders, Return,
|
||||
schema::{property::Rfc1123DateTime, response::CalCondition},
|
||||
};
|
||||
use groupware::{
|
||||
cache::GroupwareCache,
|
||||
calendar::{CalendarEvent, CalendarEventData, itip::ItipSendStatus},
|
||||
scheduling::{
|
||||
ItipMessages, event_create::itip_create, event_update::itip_update,
|
||||
itip::itip_set_unreachable_status,
|
||||
},
|
||||
};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use std::collections::HashSet;
|
||||
use store::write::{BatchBuilder, now};
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{AlignedBytes, Archive},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::Acl,
|
||||
collection::{Collection, SyncCollection},
|
||||
};
|
||||
|
||||
pub(crate) trait CalendarUpdateRequestHandler: Sync + Send {
|
||||
fn handle_calendar_update_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
bytes: Vec<u8>,
|
||||
is_patch: bool,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
}
|
||||
|
||||
impl CalendarUpdateRequestHandler for Server {
|
||||
async fn handle_calendar_update_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
bytes: Vec<u8>,
|
||||
_is_patch: bool,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
// Validate URI
|
||||
let resource = self
|
||||
.validate_uri(access_token, headers.uri)
|
||||
.await?
|
||||
.into_owned_uri()?;
|
||||
let account_id = resource.account_id;
|
||||
let resources = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::Calendar,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let resource_name = fix_percent_encoding(
|
||||
resource
|
||||
.resource
|
||||
.ok_or(DavError::Code(StatusCode::CONFLICT))?,
|
||||
);
|
||||
|
||||
if bytes.len() > self.core.groupware.max_ical_size {
|
||||
return Err(DavError::Condition(DavErrorCondition::new(
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
CalCondition::MaxResourceSize(self.core.groupware.max_ical_size as u32),
|
||||
)));
|
||||
}
|
||||
let ical_raw = std::str::from_utf8(&bytes).map_err(|_| {
|
||||
DavError::Condition(
|
||||
DavErrorCondition::new(
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
CalCondition::SupportedCalendarData,
|
||||
)
|
||||
.with_details("Invalid UTF-8 in iCalendar data"),
|
||||
)
|
||||
})?;
|
||||
|
||||
let ical = match Parser::new(ical_raw).entry() {
|
||||
Entry::ICalendar(ical) => ical,
|
||||
_ => {
|
||||
return Err(DavError::Condition(
|
||||
DavErrorCondition::new(
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
CalCondition::SupportedCalendarData,
|
||||
)
|
||||
.with_details("Failed to parse iCalendar data"),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let account_info = self
|
||||
.scheduling_account_info(access_token.account_id(), account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
if let Some(resource) = resources.by_path(resource_name.as_ref()) {
|
||||
if resource.is_container() {
|
||||
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
|
||||
}
|
||||
|
||||
// Validate ACL
|
||||
let parent_id = resource.parent_id().unwrap();
|
||||
let document_id = resource.document_id();
|
||||
if !access_token.is_member(account_id)
|
||||
&& !resources.has_access_to_container(access_token, parent_id, Acl::ModifyItems)
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
// Update
|
||||
let event_ = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::CalendarEvent,
|
||||
document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
let event = event_
|
||||
.to_unarchived::<CalendarEvent>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Validate headers
|
||||
match self
|
||||
.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![ResourceState {
|
||||
account_id,
|
||||
collection: Collection::CalendarEvent,
|
||||
document_id: Some(document_id),
|
||||
etag: event.etag().into(),
|
||||
path: resource_name.as_ref(),
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::PUT,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(DavError::Code(StatusCode::PRECONDITION_FAILED))
|
||||
if headers.ret == Return::Representation =>
|
||||
{
|
||||
return Ok(HttpResponse::new(StatusCode::PRECONDITION_FAILED)
|
||||
.with_content_type("text/calendar; charset=utf-8")
|
||||
.with_etag(event.etag())
|
||||
.with_last_modified(
|
||||
Rfc1123DateTime::new(i64::from(event.inner.modified)).to_string(),
|
||||
)
|
||||
.with_header("Preference-Applied", "return=representation")
|
||||
.with_binary_body(event.inner.data.event.to_string()));
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
|
||||
if ical == event.inner.data.event {
|
||||
// No changes, return existing event
|
||||
return Ok(HttpResponse::new(StatusCode::NO_CONTENT));
|
||||
}
|
||||
|
||||
// Validate iCal
|
||||
if event.inner.data.event.uids().next().unwrap_or_default() != validate_ical(&ical)? {
|
||||
return Err(DavError::Condition(DavErrorCondition::new(
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
CalCondition::NoUidConflict(resources.format_resource(resource).into()),
|
||||
)));
|
||||
}
|
||||
|
||||
// Validate schedule tag
|
||||
if headers.if_schedule_tag.is_some()
|
||||
&& event.inner.schedule_tag.as_ref().map(|t| t.to_native())
|
||||
!= headers.if_schedule_tag
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::PRECONDITION_FAILED));
|
||||
}
|
||||
|
||||
// Obtain previous alarm
|
||||
let now = now() as i64;
|
||||
let prev_email_alarm = event.inner.data.next_alarm(now, Tz::Floating);
|
||||
|
||||
// Build event
|
||||
let mut next_email_alarm = None;
|
||||
let mut new_event = event
|
||||
.deserialize::<CalendarEvent>()
|
||||
.caused_by(trc::location!())?;
|
||||
let old_ical = new_event.data.event;
|
||||
new_event.size = bytes.len() as u32;
|
||||
new_event.data = CalendarEventData::new(
|
||||
ical,
|
||||
Tz::Floating,
|
||||
self.core.groupware.max_ical_instances,
|
||||
&mut next_email_alarm,
|
||||
);
|
||||
|
||||
// Scheduling
|
||||
let mut itip_messages = None;
|
||||
let itip_status = ItipSendStatus::resolve(
|
||||
self,
|
||||
access_token,
|
||||
&account_info,
|
||||
new_event.data.event_range_end(),
|
||||
);
|
||||
if itip_status.is_send() {
|
||||
let result = if new_event.schedule_tag.is_some() {
|
||||
itip_update(
|
||||
&mut new_event.data.event,
|
||||
&old_ical,
|
||||
account_info.addresses(),
|
||||
)
|
||||
} else {
|
||||
itip_create(&mut new_event.data.event, account_info.addresses())
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(messages) => {
|
||||
let mut is_organizer = false;
|
||||
if messages
|
||||
.iter()
|
||||
.map(|r| {
|
||||
is_organizer = r.from_organizer;
|
||||
r.to.len()
|
||||
})
|
||||
.sum::<usize>()
|
||||
< self.core.groupware.itip_outbound_max_recipients
|
||||
{
|
||||
// Only update schedule tag if the user is the organizer
|
||||
if is_organizer {
|
||||
if let Some(schedule_tag) = &mut new_event.schedule_tag {
|
||||
*schedule_tag += 1;
|
||||
} else {
|
||||
new_event.schedule_tag = Some(1);
|
||||
}
|
||||
}
|
||||
|
||||
itip_messages = Some(ItipMessages::new(messages));
|
||||
} else {
|
||||
return Err(DavError::Condition(DavErrorCondition::new(
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
CalCondition::MaxAttendeesPerInstance,
|
||||
)));
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
if let Some(failed_precondition) = err.failed_precondition() {
|
||||
return Err(DavError::Condition(
|
||||
DavErrorCondition::new(
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
failed_precondition,
|
||||
)
|
||||
.with_details(err.to_string()),
|
||||
));
|
||||
}
|
||||
|
||||
trc::event!(
|
||||
Calendar(trc::CalendarEvent::ItipMessageError),
|
||||
AccountId = account_id,
|
||||
DocumentId = document_id,
|
||||
Reason = err.to_string(),
|
||||
);
|
||||
|
||||
// Event changed, but there are no iTIP messages to send
|
||||
if let Some(schedule_tag) = &mut new_event.schedule_tag {
|
||||
*schedule_tag += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
itip_set_unreachable_status(&mut new_event.data.event, account_info.addresses());
|
||||
} else if let Some(reason) = itip_status.reason() {
|
||||
trc::event!(
|
||||
Calendar(trc::CalendarEvent::ItipMessageError),
|
||||
AccountId = account_id,
|
||||
DocumentId = document_id,
|
||||
Reason = reason,
|
||||
);
|
||||
}
|
||||
|
||||
// Validate quota
|
||||
let extra_bytes =
|
||||
(bytes.len() as u64).saturating_sub(u32::from(event.inner.size) as u64);
|
||||
if extra_bytes > 0 {
|
||||
self.has_available_quota(self.account(account_id).await?.as_ref(), extra_bytes)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Prepare write batch
|
||||
let mut batch = BatchBuilder::new();
|
||||
let schedule_tag = new_event.schedule_tag;
|
||||
let etag = new_event
|
||||
.update(
|
||||
access_token.account_tenant_ids(),
|
||||
event,
|
||||
account_id,
|
||||
document_id,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.etag();
|
||||
if prev_email_alarm != next_email_alarm {
|
||||
if let Some(prev_alarm) = prev_email_alarm {
|
||||
prev_alarm.delete_task(&mut batch);
|
||||
}
|
||||
if let Some(next_alarm) = next_email_alarm {
|
||||
next_alarm.write_task(&mut batch);
|
||||
}
|
||||
}
|
||||
if let Some(itip_messages) = itip_messages {
|
||||
itip_messages
|
||||
.queue(&mut batch)
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
self.commit_batch(batch).await.caused_by(trc::location!())?;
|
||||
self.notify_task_queue();
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::NO_CONTENT)
|
||||
.with_etag_opt(etag)
|
||||
.with_schedule_tag_opt(schedule_tag))
|
||||
} else if let Some((Some(parent), name)) = resources.map_parent(resource_name.as_ref()) {
|
||||
if !parent.is_container() {
|
||||
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
|
||||
}
|
||||
|
||||
// Validate ACL
|
||||
if !access_token.is_member(account_id)
|
||||
&& !resources.has_access_to_container(
|
||||
access_token,
|
||||
parent.document_id(),
|
||||
Acl::AddItems,
|
||||
)
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
// Validate headers
|
||||
self.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![ResourceState {
|
||||
account_id,
|
||||
collection: resource.collection,
|
||||
document_id: Some(u32::MAX),
|
||||
path: resource_name.as_ref(),
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::PUT,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Validate ical object
|
||||
assert_is_unique_uid(
|
||||
self,
|
||||
&resources,
|
||||
account_id,
|
||||
parent.document_id(),
|
||||
validate_ical(&ical)?.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Build event
|
||||
let mut next_email_alarm = None;
|
||||
let mut event = CalendarEvent {
|
||||
names: vec![DavName {
|
||||
name: name.to_string(),
|
||||
parent_id: parent.document_id(),
|
||||
}],
|
||||
data: CalendarEventData::new(
|
||||
ical,
|
||||
Tz::Floating,
|
||||
self.core.groupware.max_ical_instances,
|
||||
&mut next_email_alarm,
|
||||
),
|
||||
size: bytes.len() as u32,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Scheduling
|
||||
let mut itip_messages = None;
|
||||
let itip_status = ItipSendStatus::resolve(
|
||||
self,
|
||||
access_token,
|
||||
&account_info,
|
||||
event.data.event_range_end(),
|
||||
);
|
||||
if itip_status.is_send() {
|
||||
match itip_create(&mut event.data.event, account_info.addresses()) {
|
||||
Ok(messages) => {
|
||||
if messages.iter().map(|r| r.to.len()).sum::<usize>()
|
||||
< self.core.groupware.itip_outbound_max_recipients
|
||||
{
|
||||
event.schedule_tag = Some(1);
|
||||
itip_messages = Some(ItipMessages::new(messages));
|
||||
} else {
|
||||
return Err(DavError::Condition(DavErrorCondition::new(
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
CalCondition::MaxAttendeesPerInstance,
|
||||
)));
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
if let Some(failed_precondition) = err.failed_precondition() {
|
||||
return Err(DavError::Condition(
|
||||
DavErrorCondition::new(
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
failed_precondition,
|
||||
)
|
||||
.with_details(err.to_string()),
|
||||
));
|
||||
}
|
||||
|
||||
trc::event!(
|
||||
Calendar(trc::CalendarEvent::ItipMessageError),
|
||||
AccountId = account_id,
|
||||
Reason = err.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
itip_set_unreachable_status(&mut event.data.event, account_info.addresses());
|
||||
} else if let Some(reason) = itip_status.reason() {
|
||||
trc::event!(
|
||||
Calendar(trc::CalendarEvent::ItipMessageError),
|
||||
AccountId = account_id,
|
||||
Reason = reason,
|
||||
);
|
||||
}
|
||||
|
||||
// Validate quota
|
||||
if !bytes.is_empty() {
|
||||
self.has_available_quota(
|
||||
self.account(account_id).await?.as_ref(),
|
||||
bytes.len() as u64,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Prepare write batch
|
||||
let mut batch = BatchBuilder::new();
|
||||
let document_id = self
|
||||
.store()
|
||||
.assign_document_ids(account_id, Collection::CalendarEvent, 1)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let schedule_tag = event.schedule_tag;
|
||||
let etag = event
|
||||
.insert(
|
||||
access_token.account_tenant_ids(),
|
||||
account_id,
|
||||
document_id,
|
||||
next_email_alarm,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.etag();
|
||||
if let Some(itip_messages) = itip_messages {
|
||||
itip_messages
|
||||
.queue(&mut batch)
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
self.commit_batch(batch).await.caused_by(trc::location!())?;
|
||||
self.notify_task_queue();
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::CREATED)
|
||||
.with_etag_opt(etag)
|
||||
.with_schedule_tag_opt(schedule_tag))
|
||||
} else {
|
||||
Err(DavError::Code(StatusCode::CONFLICT))?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_ical(ical: &ICalendar) -> crate::Result<&str> {
|
||||
// Validate UIDs
|
||||
let mut uids = HashSet::with_capacity(1);
|
||||
|
||||
// Validate component types
|
||||
let mut types: [u8; 5] = [0; 5];
|
||||
for comp in &ical.components {
|
||||
*(match comp.component_type {
|
||||
ICalendarComponentType::VEvent => &mut types[0],
|
||||
ICalendarComponentType::VTodo => &mut types[1],
|
||||
ICalendarComponentType::VJournal => &mut types[2],
|
||||
ICalendarComponentType::VFreebusy => &mut types[3],
|
||||
ICalendarComponentType::VAvailability => &mut types[4],
|
||||
_ => {
|
||||
continue;
|
||||
}
|
||||
}) += 1;
|
||||
|
||||
if let Some(uid) = comp.uid() {
|
||||
uids.insert(uid);
|
||||
}
|
||||
}
|
||||
|
||||
if uids.len() == 1 && types.iter().filter(|&&v| v == 0).count() == 4 {
|
||||
Ok(uids.iter().next().unwrap())
|
||||
} else {
|
||||
Err(DavError::Condition(
|
||||
DavErrorCondition::new(
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
CalCondition::ValidCalendarObjectResource,
|
||||
)
|
||||
.with_details("iCalendar must contain exactly one UID and same component types"),
|
||||
))
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
DavError, DavMethod,
|
||||
common::{
|
||||
ETag,
|
||||
lock::{LockRequestHandler, ResourceState},
|
||||
uri::DavUriResource,
|
||||
},
|
||||
};
|
||||
use common::{Server, auth::AccessToken, sharing::EffectiveAcl};
|
||||
use dav_proto::RequestHeaders;
|
||||
use groupware::{
|
||||
DestroyArchive,
|
||||
cache::GroupwareCache,
|
||||
contact::{AddressBook, ContactCard},
|
||||
};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use store::write::{BatchBuilder, ValueClass};
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{AlignedBytes, Archive},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::Acl,
|
||||
collection::{Collection, SyncCollection},
|
||||
field::PrincipalField,
|
||||
};
|
||||
|
||||
pub(crate) trait CardDeleteRequestHandler: Sync + Send {
|
||||
fn handle_card_delete_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
}
|
||||
|
||||
impl CardDeleteRequestHandler for Server {
|
||||
async fn handle_card_delete_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
// Validate URI
|
||||
let resource = self
|
||||
.validate_uri(access_token, headers.uri)
|
||||
.await?
|
||||
.into_owned_uri()?;
|
||||
let account_id = resource.account_id;
|
||||
let delete_path = resource
|
||||
.resource
|
||||
.filter(|r| !r.is_empty())
|
||||
.ok_or(DavError::Code(StatusCode::FORBIDDEN))?;
|
||||
let resources = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::AddressBook,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Check resource type
|
||||
let delete_resource = resources
|
||||
.by_path(delete_path)
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
let document_id = delete_resource.document_id();
|
||||
|
||||
// Fetch entry
|
||||
let mut batch = BatchBuilder::new();
|
||||
if delete_resource.is_container() {
|
||||
let book_ = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::AddressBook,
|
||||
document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
|
||||
let book = book_
|
||||
.to_unarchived::<AddressBook>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Validate ACL
|
||||
if !access_token.is_member(account_id)
|
||||
&& !book
|
||||
.inner
|
||||
.acls
|
||||
.effective_acl(access_token)
|
||||
.contains_all([Acl::Delete, Acl::RemoveItems].into_iter())
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
// Validate headers
|
||||
self.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![ResourceState {
|
||||
account_id,
|
||||
collection: Collection::AddressBook,
|
||||
document_id: document_id.into(),
|
||||
etag: book.etag().into(),
|
||||
path: delete_path,
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::DELETE,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Delete addressbook and cards
|
||||
DestroyArchive(book)
|
||||
.delete_with_cards(
|
||||
self,
|
||||
access_token.account_tenant_ids(),
|
||||
account_id,
|
||||
document_id,
|
||||
resources
|
||||
.subtree(delete_path)
|
||||
.filter(|r| !r.is_container())
|
||||
.map(|r| r.document_id())
|
||||
.collect::<Vec<_>>(),
|
||||
resources.format_resource(delete_resource).into(),
|
||||
&mut batch,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Reset default address book id
|
||||
let default_book_id = self
|
||||
.store()
|
||||
.get_value::<u32>(ValueKey {
|
||||
account_id,
|
||||
collection: Collection::Principal.into(),
|
||||
document_id: 0,
|
||||
class: ValueClass::Property(PrincipalField::DefaultAddressBookId.into()),
|
||||
})
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
if default_book_id.is_some_and(|id| id == document_id) {
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::Principal)
|
||||
.with_document(0)
|
||||
.clear(PrincipalField::DefaultAddressBookId);
|
||||
}
|
||||
} else {
|
||||
// Validate ACL
|
||||
let addressbook_id = delete_resource.parent_id().unwrap();
|
||||
if !access_token.is_member(account_id)
|
||||
&& !resources.has_access_to_container(
|
||||
access_token,
|
||||
addressbook_id,
|
||||
Acl::RemoveItems,
|
||||
)
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
let card_ = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::ContactCard,
|
||||
document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
|
||||
// Validate headers
|
||||
self.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![ResourceState {
|
||||
account_id,
|
||||
collection: Collection::ContactCard,
|
||||
document_id: document_id.into(),
|
||||
etag: card_.etag().into(),
|
||||
path: delete_path,
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::DELETE,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Delete card
|
||||
DestroyArchive(
|
||||
card_
|
||||
.to_unarchived::<ContactCard>()
|
||||
.caused_by(trc::location!())?,
|
||||
)
|
||||
.delete(
|
||||
access_token.account_tenant_ids(),
|
||||
account_id,
|
||||
document_id,
|
||||
addressbook_id,
|
||||
resources.format_resource(delete_resource).into(),
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
|
||||
self.commit_batch(batch).await.caused_by(trc::location!())?;
|
||||
self.notify_task_queue();
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::NO_CONTENT))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
DavError, DavMethod,
|
||||
common::{
|
||||
ETag,
|
||||
lock::{LockRequestHandler, ResourceState},
|
||||
uri::DavUriResource,
|
||||
},
|
||||
};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use dav_proto::{RequestHeaders, schema::property::Rfc1123DateTime};
|
||||
use groupware::{cache::GroupwareCache, contact::ContactCard};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{AlignedBytes, Archive},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::Acl,
|
||||
collection::{Collection, SyncCollection},
|
||||
};
|
||||
|
||||
pub(crate) trait CardGetRequestHandler: Sync + Send {
|
||||
fn handle_card_get_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
is_head: bool,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
}
|
||||
|
||||
impl CardGetRequestHandler for Server {
|
||||
async fn handle_card_get_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
is_head: bool,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
// Validate URI
|
||||
let resource_ = self
|
||||
.validate_uri(access_token, headers.uri)
|
||||
.await?
|
||||
.into_owned_uri()?;
|
||||
let account_id = resource_.account_id;
|
||||
let resources = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::AddressBook,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let resource = resources
|
||||
.by_path(
|
||||
resource_
|
||||
.resource
|
||||
.ok_or(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))?,
|
||||
)
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
if resource.is_container() {
|
||||
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
|
||||
}
|
||||
|
||||
// Validate ACL
|
||||
if !access_token.is_member(account_id)
|
||||
&& !resources.has_access_to_container(
|
||||
access_token,
|
||||
resource.parent_id().unwrap(),
|
||||
Acl::ReadItems,
|
||||
)
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
// Fetch card
|
||||
let card_ = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::ContactCard,
|
||||
resource.document_id(),
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
let card = card_
|
||||
.unarchive::<ContactCard>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Validate headers
|
||||
let etag = card_.etag();
|
||||
self.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![ResourceState {
|
||||
account_id,
|
||||
collection: Collection::ContactCard,
|
||||
document_id: resource.document_id().into(),
|
||||
etag: etag.clone().into(),
|
||||
path: resource_.resource.unwrap(),
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::GET,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let response = HttpResponse::new(StatusCode::OK)
|
||||
.with_content_type("text/vcard; charset=utf-8")
|
||||
.with_etag(etag)
|
||||
.with_last_modified(Rfc1123DateTime::new(i64::from(card.modified)).to_string());
|
||||
|
||||
let mut vcard = String::with_capacity(128);
|
||||
let _ = card.card.write_to(
|
||||
&mut vcard,
|
||||
headers
|
||||
.vcard_version
|
||||
.unwrap_or(self.core.groupware.vcard_version),
|
||||
);
|
||||
|
||||
if !is_head {
|
||||
Ok(response.with_binary_body(vcard))
|
||||
} else {
|
||||
Ok(response.with_content_length(vcard.len()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::proppatch::CardPropPatchRequestHandler;
|
||||
use crate::{
|
||||
DavError, DavMethod, PropStatBuilder,
|
||||
common::{
|
||||
ExtractETag,
|
||||
lock::{LockRequestHandler, ResourceState},
|
||||
uri::DavUriResource,
|
||||
},
|
||||
};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use dav_proto::{
|
||||
RequestHeaders, Return,
|
||||
schema::{Namespace, request::MkCol, response::MkColResponse},
|
||||
};
|
||||
use groupware::{
|
||||
cache::GroupwareCache,
|
||||
contact::{AddressBook, AddressBookPreferences},
|
||||
};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use store::write::BatchBuilder;
|
||||
use trc::AddContext;
|
||||
use types::collection::{Collection, SyncCollection};
|
||||
|
||||
pub(crate) trait CardMkColRequestHandler: Sync + Send {
|
||||
fn handle_card_mkcol_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
request: Option<MkCol>,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
}
|
||||
|
||||
impl CardMkColRequestHandler for Server {
|
||||
async fn handle_card_mkcol_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
request: Option<MkCol>,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
// Validate URI
|
||||
let resource = self
|
||||
.validate_uri(access_token, headers.uri)
|
||||
.await?
|
||||
.into_owned_uri()?;
|
||||
let account_id = resource.account_id;
|
||||
let name = resource
|
||||
.resource
|
||||
.ok_or(DavError::Code(StatusCode::FORBIDDEN))?;
|
||||
if !access_token.is_member(account_id) {
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
} else if name.contains('/')
|
||||
|| self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::AddressBook,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.by_path(name)
|
||||
.is_some()
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
|
||||
}
|
||||
|
||||
// Validate headers
|
||||
self.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![ResourceState {
|
||||
account_id,
|
||||
collection: resource.collection,
|
||||
document_id: Some(u32::MAX),
|
||||
path: name,
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::MKCOL,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Build file container
|
||||
let mut book = AddressBook {
|
||||
name: name.to_string(),
|
||||
preferences: vec![AddressBookPreferences {
|
||||
account_id,
|
||||
name: "Address Book".to_string(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Apply MKCOL properties
|
||||
let mut return_prop_stat = None;
|
||||
if let Some(mkcol) = request {
|
||||
let mut prop_stat = PropStatBuilder::default();
|
||||
if !self.apply_addressbook_properties(
|
||||
access_token.personal_id(account_id, Collection::AddressBook),
|
||||
&mut book,
|
||||
false,
|
||||
mkcol.props,
|
||||
&mut prop_stat,
|
||||
) {
|
||||
return Ok(HttpResponse::new(StatusCode::FORBIDDEN).with_xml_body(
|
||||
MkColResponse::new(prop_stat.build())
|
||||
.with_namespace(Namespace::CardDav)
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
if headers.ret != Return::Minimal {
|
||||
return_prop_stat = Some(prop_stat);
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare write batch
|
||||
let mut batch = BatchBuilder::new();
|
||||
let document_id = self
|
||||
.store()
|
||||
.assign_document_ids(account_id, Collection::AddressBook, 1)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
book.insert(
|
||||
access_token.account_tenant_ids(),
|
||||
account_id,
|
||||
document_id,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?;
|
||||
let etag = batch.etag();
|
||||
self.commit_batch(batch).await.caused_by(trc::location!())?;
|
||||
|
||||
if let Some(prop_stat) = return_prop_stat {
|
||||
Ok(HttpResponse::new(StatusCode::CREATED)
|
||||
.with_xml_body(
|
||||
MkColResponse::new(prop_stat.build())
|
||||
.with_namespace(Namespace::CardDav)
|
||||
.to_string(),
|
||||
)
|
||||
.with_etag_opt(etag))
|
||||
} else {
|
||||
Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{DavError, DavErrorCondition};
|
||||
use common::{DavResources, Server};
|
||||
use dav_proto::schema::{
|
||||
property::{CardDavProperty, DavProperty, WebDavProperty},
|
||||
response::CardCondition,
|
||||
};
|
||||
use hyper::StatusCode;
|
||||
use trc::AddContext;
|
||||
use types::{collection::Collection, field::ContactField};
|
||||
|
||||
pub mod copy_move;
|
||||
pub mod delete;
|
||||
pub mod get;
|
||||
pub mod mkcol;
|
||||
pub mod proppatch;
|
||||
pub mod query;
|
||||
pub mod update;
|
||||
|
||||
pub(crate) static CARD_CONTAINER_PROPS: [DavProperty; 23] = [
|
||||
DavProperty::WebDav(WebDavProperty::CreationDate),
|
||||
DavProperty::WebDav(WebDavProperty::DisplayName),
|
||||
DavProperty::WebDav(WebDavProperty::GetETag),
|
||||
DavProperty::WebDav(WebDavProperty::GetLastModified),
|
||||
DavProperty::WebDav(WebDavProperty::ResourceType),
|
||||
DavProperty::WebDav(WebDavProperty::LockDiscovery),
|
||||
DavProperty::WebDav(WebDavProperty::SupportedLock),
|
||||
DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal),
|
||||
DavProperty::WebDav(WebDavProperty::SyncToken),
|
||||
DavProperty::WebDav(WebDavProperty::Owner),
|
||||
DavProperty::WebDav(WebDavProperty::SupportedPrivilegeSet),
|
||||
DavProperty::WebDav(WebDavProperty::CurrentUserPrivilegeSet),
|
||||
DavProperty::WebDav(WebDavProperty::Acl),
|
||||
DavProperty::WebDav(WebDavProperty::AclRestrictions),
|
||||
DavProperty::WebDav(WebDavProperty::InheritedAclSet),
|
||||
DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet),
|
||||
DavProperty::WebDav(WebDavProperty::SupportedReportSet),
|
||||
DavProperty::WebDav(WebDavProperty::QuotaAvailableBytes),
|
||||
DavProperty::WebDav(WebDavProperty::QuotaUsedBytes),
|
||||
DavProperty::CardDav(CardDavProperty::AddressbookDescription),
|
||||
DavProperty::CardDav(CardDavProperty::SupportedAddressData),
|
||||
DavProperty::CardDav(CardDavProperty::SupportedCollationSet),
|
||||
DavProperty::CardDav(CardDavProperty::MaxResourceSize),
|
||||
];
|
||||
|
||||
pub(crate) static CARD_ITEM_PROPS: [DavProperty; 20] = [
|
||||
DavProperty::WebDav(WebDavProperty::CreationDate),
|
||||
DavProperty::WebDav(WebDavProperty::DisplayName),
|
||||
DavProperty::WebDav(WebDavProperty::GetETag),
|
||||
DavProperty::WebDav(WebDavProperty::GetLastModified),
|
||||
DavProperty::WebDav(WebDavProperty::ResourceType),
|
||||
DavProperty::WebDav(WebDavProperty::LockDiscovery),
|
||||
DavProperty::WebDav(WebDavProperty::SupportedLock),
|
||||
DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal),
|
||||
DavProperty::WebDav(WebDavProperty::SyncToken),
|
||||
DavProperty::WebDav(WebDavProperty::Owner),
|
||||
DavProperty::WebDav(WebDavProperty::SupportedPrivilegeSet),
|
||||
DavProperty::WebDav(WebDavProperty::CurrentUserPrivilegeSet),
|
||||
DavProperty::WebDav(WebDavProperty::Acl),
|
||||
DavProperty::WebDav(WebDavProperty::AclRestrictions),
|
||||
DavProperty::WebDav(WebDavProperty::InheritedAclSet),
|
||||
DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet),
|
||||
DavProperty::WebDav(WebDavProperty::GetContentLanguage),
|
||||
DavProperty::WebDav(WebDavProperty::GetContentLength),
|
||||
DavProperty::WebDav(WebDavProperty::GetContentType),
|
||||
DavProperty::CardDav(CardDavProperty::AddressData {
|
||||
properties: Vec::new(),
|
||||
version: None,
|
||||
}),
|
||||
];
|
||||
|
||||
pub(crate) async fn assert_is_unique_uid(
|
||||
server: &Server,
|
||||
resources: &DavResources,
|
||||
account_id: u32,
|
||||
addressbook_id: u32,
|
||||
uid: Option<&str>,
|
||||
) -> crate::Result<()> {
|
||||
if let Some(uid) = uid {
|
||||
let hits = server
|
||||
.document_ids_matching(
|
||||
account_id,
|
||||
Collection::ContactCard,
|
||||
ContactField::Uid,
|
||||
uid.as_bytes(),
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
if !hits.is_empty() {
|
||||
for path in resources.children(addressbook_id) {
|
||||
if hits.contains(path.document_id()) {
|
||||
return Err(DavError::Condition(DavErrorCondition::new(
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
CardCondition::NoUidConflict(resources.format_resource(path).into()),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,488 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
DavError, DavMethod, PropStatBuilder,
|
||||
common::{
|
||||
ETag, ExtractETag,
|
||||
lock::{LockRequestHandler, ResourceState},
|
||||
uri::DavUriResource,
|
||||
},
|
||||
};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use dav_proto::{
|
||||
RequestHeaders, Return,
|
||||
schema::{
|
||||
Namespace,
|
||||
property::{CardDavProperty, DavProperty, DavValue, ResourceType, WebDavProperty},
|
||||
request::{DavPropertyValue, PropertyUpdate},
|
||||
response::{BaseCondition, MultiStatus, Response},
|
||||
},
|
||||
};
|
||||
use groupware::{
|
||||
cache::GroupwareCache,
|
||||
contact::{AddressBook, ContactCard},
|
||||
};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use store::write::BatchBuilder;
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{AlignedBytes, Archive},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::Acl,
|
||||
collection::{Collection, SyncCollection},
|
||||
};
|
||||
|
||||
pub(crate) trait CardPropPatchRequestHandler: Sync + Send {
|
||||
fn handle_card_proppatch_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
request: PropertyUpdate,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
|
||||
fn apply_addressbook_properties(
|
||||
&self,
|
||||
personal_id: u32,
|
||||
address_book: &mut AddressBook,
|
||||
is_update: bool,
|
||||
properties: Vec<DavPropertyValue>,
|
||||
items: &mut PropStatBuilder,
|
||||
) -> bool;
|
||||
|
||||
fn apply_card_properties(
|
||||
&self,
|
||||
card: &mut ContactCard,
|
||||
is_update: bool,
|
||||
properties: Vec<DavPropertyValue>,
|
||||
items: &mut PropStatBuilder,
|
||||
) -> bool;
|
||||
}
|
||||
|
||||
impl CardPropPatchRequestHandler for Server {
|
||||
async fn handle_card_proppatch_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
mut request: PropertyUpdate,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
// Validate URI
|
||||
let resource_ = self
|
||||
.validate_uri(access_token, headers.uri)
|
||||
.await?
|
||||
.into_owned_uri()?;
|
||||
let uri = headers.uri;
|
||||
let account_id = resource_.account_id;
|
||||
let resources = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::AddressBook,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let resource = resource_
|
||||
.resource
|
||||
.and_then(|r| resources.by_path(r))
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
let document_id = resource.document_id();
|
||||
let collection = if resource.is_container() {
|
||||
Collection::AddressBook
|
||||
} else {
|
||||
Collection::ContactCard
|
||||
};
|
||||
|
||||
if !request.has_changes() {
|
||||
return Ok(HttpResponse::new(StatusCode::NO_CONTENT));
|
||||
}
|
||||
|
||||
// Verify ACL
|
||||
if !access_token.is_member(account_id) {
|
||||
let (acl, document_id) = if resource.is_container() {
|
||||
(Acl::Modify, resource.document_id())
|
||||
} else {
|
||||
(Acl::ModifyItems, resource.parent_id().unwrap())
|
||||
};
|
||||
|
||||
if !resources.has_access_to_container(access_token, document_id, acl) {
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch archive
|
||||
let archive = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
collection,
|
||||
document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
|
||||
// Validate headers
|
||||
self.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![ResourceState {
|
||||
account_id,
|
||||
collection,
|
||||
document_id: document_id.into(),
|
||||
etag: archive.etag().into(),
|
||||
path: resource_.resource.unwrap(),
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::PROPPATCH,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let is_success;
|
||||
let mut batch = BatchBuilder::new();
|
||||
let mut items = PropStatBuilder::default();
|
||||
|
||||
let etag = if resource.is_container() {
|
||||
// Deserialize
|
||||
let book = archive
|
||||
.to_unarchived::<AddressBook>()
|
||||
.caused_by(trc::location!())?;
|
||||
let mut new_book = archive
|
||||
.deserialize::<AddressBook>()
|
||||
.caused_by(trc::location!())?;
|
||||
let personal_id = access_token.personal_id(account_id, Collection::AddressBook);
|
||||
|
||||
// Remove properties
|
||||
if !request.set_first && !request.remove.is_empty() {
|
||||
remove_addressbook_properties(
|
||||
personal_id,
|
||||
&mut new_book,
|
||||
std::mem::take(&mut request.remove),
|
||||
&mut items,
|
||||
);
|
||||
}
|
||||
|
||||
// Set properties
|
||||
is_success = self.apply_addressbook_properties(
|
||||
personal_id,
|
||||
&mut new_book,
|
||||
true,
|
||||
request.set,
|
||||
&mut items,
|
||||
);
|
||||
|
||||
// Remove properties
|
||||
if is_success && !request.remove.is_empty() {
|
||||
remove_addressbook_properties(
|
||||
personal_id,
|
||||
&mut new_book,
|
||||
request.remove,
|
||||
&mut items,
|
||||
);
|
||||
}
|
||||
|
||||
if is_success {
|
||||
new_book
|
||||
.update(
|
||||
access_token.account_tenant_ids(),
|
||||
book,
|
||||
account_id,
|
||||
document_id,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.etag()
|
||||
} else {
|
||||
book.etag().into()
|
||||
}
|
||||
} else {
|
||||
// Deserialize
|
||||
let card = archive
|
||||
.to_unarchived::<ContactCard>()
|
||||
.caused_by(trc::location!())?;
|
||||
let mut new_card = archive
|
||||
.deserialize::<ContactCard>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Remove properties
|
||||
if !request.set_first && !request.remove.is_empty() {
|
||||
remove_card_properties(
|
||||
&mut new_card,
|
||||
std::mem::take(&mut request.remove),
|
||||
&mut items,
|
||||
);
|
||||
}
|
||||
|
||||
// Set properties
|
||||
is_success = self.apply_card_properties(&mut new_card, true, request.set, &mut items);
|
||||
|
||||
// Remove properties
|
||||
if is_success && !request.remove.is_empty() {
|
||||
remove_card_properties(&mut new_card, request.remove, &mut items);
|
||||
}
|
||||
|
||||
if is_success {
|
||||
new_card
|
||||
.update(
|
||||
access_token.account_tenant_ids(),
|
||||
card,
|
||||
account_id,
|
||||
document_id,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.etag()
|
||||
} else {
|
||||
card.etag().into()
|
||||
}
|
||||
};
|
||||
|
||||
if is_success {
|
||||
self.commit_batch(batch).await.caused_by(trc::location!())?;
|
||||
}
|
||||
|
||||
if headers.ret != Return::Minimal || !is_success {
|
||||
Ok(HttpResponse::new(StatusCode::MULTI_STATUS)
|
||||
.with_xml_body(
|
||||
MultiStatus::new(vec![Response::new_propstat(uri, items.build())])
|
||||
.with_namespace(Namespace::CardDav)
|
||||
.to_string(),
|
||||
)
|
||||
.with_etag_opt(etag))
|
||||
} else {
|
||||
Ok(HttpResponse::new(StatusCode::NO_CONTENT).with_etag_opt(etag))
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_addressbook_properties(
|
||||
&self,
|
||||
personal_id: u32,
|
||||
address_book: &mut AddressBook,
|
||||
is_update: bool,
|
||||
properties: Vec<DavPropertyValue>,
|
||||
items: &mut PropStatBuilder,
|
||||
) -> bool {
|
||||
let mut has_errors = false;
|
||||
|
||||
for property in properties {
|
||||
match (&property.property, property.value) {
|
||||
(DavProperty::WebDav(WebDavProperty::DisplayName), DavValue::String(name)) => {
|
||||
if name.len() <= self.core.groupware.live_property_size {
|
||||
address_book.preferences_mut(personal_id).name = name;
|
||||
items.insert_ok(property.property);
|
||||
} else {
|
||||
items.insert_error_with_description(
|
||||
property.property,
|
||||
StatusCode::INSUFFICIENT_STORAGE,
|
||||
"Property value is too long",
|
||||
);
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
(
|
||||
DavProperty::CardDav(CardDavProperty::AddressbookDescription),
|
||||
DavValue::String(name),
|
||||
) => {
|
||||
if name.len() <= self.core.groupware.live_property_size {
|
||||
address_book.preferences_mut(personal_id).description = Some(name);
|
||||
items.insert_ok(property.property);
|
||||
} else {
|
||||
items.insert_error_with_description(
|
||||
property.property,
|
||||
StatusCode::INSUFFICIENT_STORAGE,
|
||||
"Property value is too long",
|
||||
);
|
||||
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
(DavProperty::WebDav(WebDavProperty::CreationDate), DavValue::Timestamp(dt)) => {
|
||||
address_book.created = dt;
|
||||
items.insert_ok(property.property);
|
||||
}
|
||||
(
|
||||
DavProperty::WebDav(WebDavProperty::ResourceType),
|
||||
DavValue::ResourceTypes(types),
|
||||
) => {
|
||||
if !types.0.iter().all(|rt| {
|
||||
matches!(rt, ResourceType::Collection | ResourceType::AddressBook)
|
||||
}) {
|
||||
items.insert_precondition_failed(
|
||||
property.property,
|
||||
StatusCode::FORBIDDEN,
|
||||
BaseCondition::ValidResourceType,
|
||||
);
|
||||
has_errors = true;
|
||||
} else {
|
||||
items.insert_ok(property.property);
|
||||
}
|
||||
}
|
||||
(DavProperty::DeadProperty(dead), DavValue::DeadProperty(values))
|
||||
if self.core.groupware.dead_property_size.is_some() =>
|
||||
{
|
||||
if is_update {
|
||||
address_book.dead_properties.remove_element(dead);
|
||||
}
|
||||
|
||||
if address_book.dead_properties.size() + values.size() + dead.size()
|
||||
< self.core.groupware.dead_property_size.unwrap()
|
||||
{
|
||||
address_book
|
||||
.dead_properties
|
||||
.add_element(dead.clone(), values.0);
|
||||
items.insert_ok(property.property);
|
||||
} else {
|
||||
items.insert_error_with_description(
|
||||
property.property,
|
||||
StatusCode::INSUFFICIENT_STORAGE,
|
||||
"Property value is too long",
|
||||
);
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
(_, DavValue::Null) => {
|
||||
items.insert_ok(property.property);
|
||||
}
|
||||
_ => {
|
||||
items.insert_error_with_description(
|
||||
property.property,
|
||||
StatusCode::CONFLICT,
|
||||
"Property cannot be modified",
|
||||
);
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
!has_errors
|
||||
}
|
||||
|
||||
fn apply_card_properties(
|
||||
&self,
|
||||
card: &mut ContactCard,
|
||||
is_update: bool,
|
||||
properties: Vec<DavPropertyValue>,
|
||||
items: &mut PropStatBuilder,
|
||||
) -> bool {
|
||||
let mut has_errors = false;
|
||||
|
||||
for property in properties {
|
||||
match (&property.property, property.value) {
|
||||
(DavProperty::WebDav(WebDavProperty::DisplayName), DavValue::String(name)) => {
|
||||
if name.len() <= self.core.groupware.live_property_size {
|
||||
card.display_name = Some(name);
|
||||
items.insert_ok(property.property);
|
||||
} else {
|
||||
items.insert_error_with_description(
|
||||
property.property,
|
||||
StatusCode::INSUFFICIENT_STORAGE,
|
||||
"Property value is too long",
|
||||
);
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
(DavProperty::WebDav(WebDavProperty::CreationDate), DavValue::Timestamp(dt)) => {
|
||||
card.created = dt;
|
||||
items.insert_ok(property.property);
|
||||
}
|
||||
(DavProperty::DeadProperty(dead), DavValue::DeadProperty(values))
|
||||
if self.core.groupware.dead_property_size.is_some() =>
|
||||
{
|
||||
if is_update {
|
||||
card.dead_properties.remove_element(dead);
|
||||
}
|
||||
|
||||
if card.dead_properties.size() + values.size() + dead.size()
|
||||
< self.core.groupware.dead_property_size.unwrap()
|
||||
{
|
||||
card.dead_properties.add_element(dead.clone(), values.0);
|
||||
items.insert_ok(property.property);
|
||||
} else {
|
||||
items.insert_error_with_description(
|
||||
property.property,
|
||||
StatusCode::INSUFFICIENT_STORAGE,
|
||||
"Property value is too long",
|
||||
);
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
(_, DavValue::Null) => {
|
||||
items.insert_ok(property.property);
|
||||
}
|
||||
_ => {
|
||||
items.insert_error_with_description(
|
||||
property.property,
|
||||
StatusCode::CONFLICT,
|
||||
"Property cannot be modified",
|
||||
);
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
!has_errors
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_card_properties(
|
||||
card: &mut ContactCard,
|
||||
properties: Vec<DavProperty>,
|
||||
items: &mut PropStatBuilder,
|
||||
) {
|
||||
for property in properties {
|
||||
match &property {
|
||||
DavProperty::WebDav(WebDavProperty::DisplayName) => {
|
||||
card.display_name = None;
|
||||
items.insert_with_status(property, StatusCode::NO_CONTENT);
|
||||
}
|
||||
DavProperty::DeadProperty(dead) => {
|
||||
card.dead_properties.remove_element(dead);
|
||||
items.insert_with_status(property, StatusCode::NO_CONTENT);
|
||||
}
|
||||
_ => {
|
||||
items.insert_error_with_description(
|
||||
property,
|
||||
StatusCode::CONFLICT,
|
||||
"Property cannot be deleted",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_addressbook_properties(
|
||||
personal_id: u32,
|
||||
book: &mut AddressBook,
|
||||
properties: Vec<DavProperty>,
|
||||
items: &mut PropStatBuilder,
|
||||
) {
|
||||
for property in properties {
|
||||
match &property {
|
||||
DavProperty::CardDav(CardDavProperty::AddressbookDescription) => {
|
||||
book.preferences_mut(personal_id).description = None;
|
||||
items.insert_with_status(property, StatusCode::NO_CONTENT);
|
||||
}
|
||||
DavProperty::WebDav(WebDavProperty::DisplayName) => {
|
||||
book.preferences_mut(personal_id).name.clear();
|
||||
items.insert_with_status(property, StatusCode::NO_CONTENT);
|
||||
}
|
||||
DavProperty::DeadProperty(dead) => {
|
||||
book.dead_properties.remove_element(dead);
|
||||
items.insert_with_status(property, StatusCode::NO_CONTENT);
|
||||
}
|
||||
_ => {
|
||||
items.insert_error_with_description(
|
||||
property,
|
||||
StatusCode::CONFLICT,
|
||||
"Property cannot be deleted",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
DavError,
|
||||
common::{
|
||||
AddressbookFilter, DavQuery,
|
||||
propfind::{PropFindItem, PropFindRequestHandler},
|
||||
uri::DavUriResource,
|
||||
},
|
||||
};
|
||||
use calcard::vcard::{
|
||||
ArchivedVCard, ArchivedVCardEntry, ArchivedVCardParameter, VCardParameterName, VCardProperty,
|
||||
VCardVersion,
|
||||
};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use dav_proto::{
|
||||
RequestHeaders,
|
||||
schema::{
|
||||
property::CardDavPropertyName,
|
||||
request::{AddressbookQuery, Filter, FilterOp, VCardPropertyWithGroup},
|
||||
response::MultiStatus,
|
||||
},
|
||||
};
|
||||
use groupware::cache::GroupwareCache;
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use std::fmt::Write;
|
||||
use trc::AddContext;
|
||||
use types::{acl::Acl, collection::SyncCollection};
|
||||
|
||||
pub(crate) trait CardQueryRequestHandler: Sync + Send {
|
||||
fn handle_card_query_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
request: AddressbookQuery,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
}
|
||||
|
||||
impl CardQueryRequestHandler for Server {
|
||||
async fn handle_card_query_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
request: AddressbookQuery,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
// Validate URI
|
||||
let resource_ = self
|
||||
.validate_uri(access_token, headers.uri)
|
||||
.await?
|
||||
.into_owned_uri()?;
|
||||
let account_id = resource_.account_id;
|
||||
let resources = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::AddressBook,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let Some(resource) = resources.by_path(
|
||||
resource_
|
||||
.resource
|
||||
.ok_or(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))?,
|
||||
) else {
|
||||
return Ok(HttpResponse::new(StatusCode::MULTI_STATUS)
|
||||
.with_xml_body(MultiStatus::not_found(headers.uri).to_string()));
|
||||
};
|
||||
if !resource.is_container() {
|
||||
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
|
||||
}
|
||||
|
||||
// Obtain shared ids
|
||||
let shared_ids = if !access_token.is_member(account_id) {
|
||||
resources
|
||||
.shared_items(access_token, [Acl::ReadItems], false)
|
||||
.into()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Obtain document ids in folder
|
||||
let mut items = Vec::with_capacity(16);
|
||||
for resource in resources.children(resource.document_id()) {
|
||||
if shared_ids
|
||||
.as_ref()
|
||||
.is_none_or(|ids| ids.contains(resource.document_id()))
|
||||
{
|
||||
items.push(PropFindItem::new(
|
||||
resources.format_resource(resource),
|
||||
account_id,
|
||||
resource,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
self.handle_dav_query(
|
||||
access_token,
|
||||
DavQuery::addressbook_query(request, items, headers),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn vcard_query(card: &ArchivedVCard, filters: &AddressbookFilter) -> bool {
|
||||
let mut is_all = true;
|
||||
let mut matches_one = false;
|
||||
|
||||
for filter in filters {
|
||||
match filter {
|
||||
Filter::AnyOf => {
|
||||
is_all = false;
|
||||
}
|
||||
Filter::AllOf => {
|
||||
is_all = true;
|
||||
}
|
||||
Filter::Property { prop, op, .. } => {
|
||||
let mut properties = find_properties(card, prop).peekable();
|
||||
let result = if properties.peek().is_some() {
|
||||
properties.any(|entry| match op {
|
||||
FilterOp::Exists => true,
|
||||
FilterOp::Undefined => false,
|
||||
FilterOp::TextMatch(text_match) => {
|
||||
let mut matched_any = false;
|
||||
|
||||
for value in entry.values.iter() {
|
||||
if let Some(text) = value.as_text()
|
||||
&& text_match.matches(text)
|
||||
{
|
||||
matched_any = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
matched_any
|
||||
}
|
||||
FilterOp::TimeRange(_) => false,
|
||||
})
|
||||
} else {
|
||||
matches!(op, FilterOp::Undefined)
|
||||
};
|
||||
|
||||
if result {
|
||||
matches_one = true;
|
||||
} else if is_all {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Filter::Parameter {
|
||||
prop, param, op, ..
|
||||
} => {
|
||||
let mut properties = find_properties(card, prop)
|
||||
.filter_map(|entry| find_parameter(entry, param))
|
||||
.peekable();
|
||||
let result = if properties.peek().is_some() {
|
||||
properties.any(|entry| match op {
|
||||
FilterOp::Exists => true,
|
||||
FilterOp::Undefined => false,
|
||||
FilterOp::TextMatch(text_match) => {
|
||||
if let Some(text) = entry.value.as_text() {
|
||||
text_match.matches(text)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
FilterOp::TimeRange(_) => false,
|
||||
})
|
||||
} else {
|
||||
matches!(op, FilterOp::Undefined)
|
||||
};
|
||||
|
||||
if result {
|
||||
matches_one = true;
|
||||
} else if is_all {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Filter::Component { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
is_all || matches_one
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn find_properties<'x>(
|
||||
card: &'x ArchivedVCard,
|
||||
prop: &VCardPropertyWithGroup,
|
||||
) -> impl Iterator<Item = &'x ArchivedVCardEntry> {
|
||||
card.entries
|
||||
.iter()
|
||||
.filter(move |entry| entry.name == prop.name && entry.group == prop.group)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn find_parameter<'x>(
|
||||
entry: &'x ArchivedVCardEntry,
|
||||
name: &VCardParameterName,
|
||||
) -> Option<&'x ArchivedVCardParameter> {
|
||||
entry.params.iter().find(|param| param.name == *name)
|
||||
}
|
||||
|
||||
pub(crate) fn serialize_vcard_with_props(
|
||||
card: &ArchivedVCard,
|
||||
props: &[CardDavPropertyName],
|
||||
version: VCardVersion,
|
||||
) -> String {
|
||||
let mut vcard = String::with_capacity(128);
|
||||
if !props.is_empty() {
|
||||
let _ = write!(&mut vcard, "BEGIN:VCARD\r\n");
|
||||
let is_v4 = matches!(version, VCardVersion::V4_0);
|
||||
|
||||
for entry in card.entries.iter() {
|
||||
for item in props {
|
||||
if entry.name == item.name && entry.group == item.group {
|
||||
if item.name != VCardProperty::Version {
|
||||
let _ = entry.write_to(&mut vcard, !item.no_value, is_v4);
|
||||
} else {
|
||||
let _ = write!(&mut vcard, "VERSION:{version}\r\n");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = write!(&mut vcard, "END:VCARD\r\n");
|
||||
} else {
|
||||
let _ = card.write_to(&mut vcard, version);
|
||||
}
|
||||
|
||||
vcard
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::assert_is_unique_uid;
|
||||
use crate::{
|
||||
DavError, DavErrorCondition, DavMethod,
|
||||
common::{
|
||||
ETag, ExtractETag,
|
||||
lock::{LockRequestHandler, ResourceState},
|
||||
uri::DavUriResource,
|
||||
},
|
||||
file::DavFileResource,
|
||||
fix_percent_encoding,
|
||||
};
|
||||
use calcard::{Entry, Parser};
|
||||
use common::{DavName, Server, auth::AccessToken};
|
||||
use dav_proto::{
|
||||
RequestHeaders, Return,
|
||||
schema::{property::Rfc1123DateTime, response::CardCondition},
|
||||
};
|
||||
use groupware::{cache::GroupwareCache, contact::ContactCard};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use store::write::BatchBuilder;
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{AlignedBytes, Archive},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::Acl,
|
||||
collection::{Collection, SyncCollection},
|
||||
};
|
||||
|
||||
pub(crate) trait CardUpdateRequestHandler: Sync + Send {
|
||||
fn handle_card_update_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
bytes: Vec<u8>,
|
||||
is_patch: bool,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
}
|
||||
|
||||
impl CardUpdateRequestHandler for Server {
|
||||
async fn handle_card_update_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
bytes: Vec<u8>,
|
||||
_is_patch: bool,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
// Validate URI
|
||||
let resource = self
|
||||
.validate_uri(access_token, headers.uri)
|
||||
.await?
|
||||
.into_owned_uri()?;
|
||||
let account_id = resource.account_id;
|
||||
let resources = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::AddressBook,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let resource_name = fix_percent_encoding(
|
||||
resource
|
||||
.resource
|
||||
.ok_or(DavError::Code(StatusCode::CONFLICT))?,
|
||||
);
|
||||
|
||||
if bytes.len() > self.core.groupware.max_vcard_size {
|
||||
return Err(DavError::Condition(DavErrorCondition::new(
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
CardCondition::MaxResourceSize(self.core.groupware.max_vcard_size as u32),
|
||||
)));
|
||||
}
|
||||
let vcard_raw = std::str::from_utf8(&bytes).map_err(|_| {
|
||||
DavError::Condition(
|
||||
DavErrorCondition::new(
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
CardCondition::SupportedAddressData,
|
||||
)
|
||||
.with_details("The request body is not valid UTF-8."),
|
||||
)
|
||||
})?;
|
||||
|
||||
let vcard = match Parser::new(vcard_raw).strict().entry() {
|
||||
Entry::VCard(vcard) => vcard,
|
||||
_ => {
|
||||
return Err(DavError::Condition(
|
||||
DavErrorCondition::new(
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
CardCondition::SupportedAddressData,
|
||||
)
|
||||
.with_details("Failed to parse vCard data."),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(resource) = resources.by_path(resource_name.as_ref()) {
|
||||
if resource.is_container() {
|
||||
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
|
||||
}
|
||||
|
||||
// Validate ACL
|
||||
let parent_id = resource.parent_id().unwrap();
|
||||
let document_id = resource.document_id();
|
||||
if !access_token.is_member(account_id)
|
||||
&& !resources.has_access_to_container(access_token, parent_id, Acl::ModifyItems)
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
// Update
|
||||
let card_ = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::ContactCard,
|
||||
document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
let card = card_
|
||||
.to_unarchived::<ContactCard>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Validate headers
|
||||
match self
|
||||
.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![ResourceState {
|
||||
account_id,
|
||||
collection: Collection::ContactCard,
|
||||
document_id: Some(document_id),
|
||||
etag: card.etag().into(),
|
||||
path: resource_name.as_ref(),
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::PUT,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(DavError::Code(StatusCode::PRECONDITION_FAILED))
|
||||
if headers.ret == Return::Representation =>
|
||||
{
|
||||
let mut vcard = String::with_capacity(128);
|
||||
let _ = card.inner.card.write_to(
|
||||
&mut vcard,
|
||||
headers
|
||||
.vcard_version
|
||||
.unwrap_or(self.core.groupware.vcard_version),
|
||||
);
|
||||
|
||||
return Ok(HttpResponse::new(StatusCode::PRECONDITION_FAILED)
|
||||
.with_content_type("text/vcard; charset=utf-8")
|
||||
.with_etag(card.etag())
|
||||
.with_last_modified(
|
||||
Rfc1123DateTime::new(i64::from(card.inner.modified)).to_string(),
|
||||
)
|
||||
.with_header("Preference-Applied", "return=representation")
|
||||
.with_binary_body(vcard));
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
|
||||
// Validate UID
|
||||
match (card.inner.card.uid(), vcard.uid()) {
|
||||
(Some(old_uid), Some(new_uid)) if old_uid == new_uid => {}
|
||||
(None, None) | (None, Some(_)) => {}
|
||||
_ => {
|
||||
return Err(DavError::Condition(DavErrorCondition::new(
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
CardCondition::NoUidConflict(resources.format_resource(resource).into()),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
// Validate quota
|
||||
let extra_bytes =
|
||||
(bytes.len() as u64).saturating_sub(u32::from(card.inner.size) as u64);
|
||||
if extra_bytes > 0 {
|
||||
self.has_available_quota(self.account(account_id).await?.as_ref(), extra_bytes)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Build node
|
||||
let mut new_card = card
|
||||
.deserialize::<ContactCard>()
|
||||
.caused_by(trc::location!())?;
|
||||
new_card.size = bytes.len() as u32;
|
||||
new_card.card = vcard;
|
||||
|
||||
// Prepare write batch
|
||||
let mut batch = BatchBuilder::new();
|
||||
let etag = new_card
|
||||
.update(
|
||||
access_token.account_tenant_ids(),
|
||||
card,
|
||||
account_id,
|
||||
document_id,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.etag();
|
||||
self.commit_batch(batch).await.caused_by(trc::location!())?;
|
||||
self.notify_task_queue();
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::NO_CONTENT).with_etag_opt(etag))
|
||||
} else if let Some((Some(parent), name)) = resources.map_parent(resource_name.as_ref()) {
|
||||
if !parent.is_container() {
|
||||
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
|
||||
}
|
||||
|
||||
// Validate ACL
|
||||
if !access_token.is_member(account_id)
|
||||
&& !resources.has_access_to_container(
|
||||
access_token,
|
||||
parent.document_id(),
|
||||
Acl::AddItems,
|
||||
)
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
// Validate headers
|
||||
self.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![ResourceState {
|
||||
account_id,
|
||||
collection: resource.collection,
|
||||
document_id: Some(u32::MAX),
|
||||
path: resource_name.as_ref(),
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::PUT,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Validate UID
|
||||
assert_is_unique_uid(
|
||||
self,
|
||||
&resources,
|
||||
account_id,
|
||||
parent.document_id(),
|
||||
vcard.uid(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Validate quota
|
||||
if !bytes.is_empty() {
|
||||
self.has_available_quota(
|
||||
self.account(account_id).await?.as_ref(),
|
||||
bytes.len() as u64,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Build node
|
||||
let card = ContactCard {
|
||||
names: vec![DavName {
|
||||
name: name.to_string(),
|
||||
parent_id: parent.document_id(),
|
||||
}],
|
||||
card: vcard,
|
||||
size: bytes.len() as u32,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Prepare write batch
|
||||
let mut batch = BatchBuilder::new();
|
||||
let document_id = self
|
||||
.store()
|
||||
.assign_document_ids(account_id, Collection::ContactCard, 1)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let etag = card
|
||||
.insert(
|
||||
access_token.account_tenant_ids(),
|
||||
account_id,
|
||||
document_id,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.etag();
|
||||
self.commit_batch(batch).await.caused_by(trc::location!())?;
|
||||
self.notify_task_queue();
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag))
|
||||
} else {
|
||||
Err(DavError::Code(StatusCode::CONFLICT))?
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,582 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::ArchivedResource;
|
||||
use crate::{
|
||||
DavError, DavErrorCondition, DavResourceName, common::uri::DavUriResource,
|
||||
principal::propfind::PrincipalPropFind,
|
||||
};
|
||||
use common::{DavResources, Server, auth::AccessToken, sharing::EffectiveAcl};
|
||||
use dav_proto::{
|
||||
RequestHeaders,
|
||||
schema::{
|
||||
property::{DavProperty, Privilege, WebDavProperty},
|
||||
request::{AclPrincipalPropSet, PropFind},
|
||||
response::{Ace, BaseCondition, GrantDeny, Href, MultiStatus, Principal},
|
||||
},
|
||||
};
|
||||
use groupware::RFC_3986;
|
||||
use groupware::{cache::GroupwareCache, calendar::Calendar, contact::AddressBook, file::FileNode};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use rkyv::vec::ArchivedVec;
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{AlignedBytes, Archive},
|
||||
};
|
||||
use store::{ahash::AHashSet, roaring::RoaringBitmap, write::BatchBuilder};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::{Acl, AclGrant, ArchivedAclGrant},
|
||||
collection::Collection,
|
||||
};
|
||||
use utils::map::bitmap::Bitmap;
|
||||
|
||||
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,
|
||||
headers: &RequestHeaders<'_>,
|
||||
request: AclPrincipalPropSet,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
|
||||
fn validate_and_map_aces(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
acl: dav_proto::schema::request::Acl,
|
||||
collection: Collection,
|
||||
) -> impl Future<Output = crate::Result<Vec<AclGrant>>> + Send;
|
||||
|
||||
fn resolve_ace(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
account_id: u32,
|
||||
grants: &ArchivedVec<ArchivedAclGrant>,
|
||||
expand: Option<&PropFind>,
|
||||
) -> impl Future<Output = crate::Result<Vec<Ace>>> + Send;
|
||||
}
|
||||
|
||||
pub(crate) trait ResourceAcl {
|
||||
fn validate_and_map_parent_acl(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
is_member: bool,
|
||||
parent_id: Option<u32>,
|
||||
check_acls: impl Into<Bitmap<Acl>> + Send,
|
||||
) -> crate::Result<u32>;
|
||||
}
|
||||
|
||||
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(access_token.account_id(), account_id, collection.into())
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let resource = resource_
|
||||
.resource
|
||||
.and_then(|r| resources.by_path(r))
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
if !resource.resource.is_container() && !matches!(collection, Collection::FileNode) {
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
// Fetch node
|
||||
let archive = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::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::Share)
|
||||
{
|
||||
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) {
|
||||
// Refresh ACLs
|
||||
self.refresh_archived_acls(&grants, acls)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
let mut batch = BatchBuilder::new();
|
||||
match container {
|
||||
ArchivedResource::Calendar(calendar) => {
|
||||
let mut new_calendar = calendar
|
||||
.deserialize::<Calendar>()
|
||||
.caused_by(trc::location!())?;
|
||||
new_calendar.acls = grants;
|
||||
new_calendar
|
||||
.update(
|
||||
access_token.account_tenant_ids(),
|
||||
calendar,
|
||||
account_id,
|
||||
resource.document_id(),
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
ArchivedResource::AddressBook(book) => {
|
||||
let mut new_book = book
|
||||
.deserialize::<AddressBook>()
|
||||
.caused_by(trc::location!())?;
|
||||
new_book.acls = grants;
|
||||
new_book
|
||||
.update(
|
||||
access_token.account_tenant_ids(),
|
||||
book,
|
||||
account_id,
|
||||
resource.document_id(),
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
ArchivedResource::FileNode(node) => {
|
||||
let mut new_node =
|
||||
node.deserialize::<FileNode>().caused_by(trc::location!())?;
|
||||
new_node.acls = grants;
|
||||
new_node
|
||||
.update(
|
||||
access_token.account_tenant_ids(),
|
||||
node,
|
||||
account_id,
|
||||
resource.document_id(),
|
||||
true,
|
||||
&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,
|
||||
headers: &RequestHeaders<'_>,
|
||||
mut request: AclPrincipalPropSet,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
let uri = self
|
||||
.validate_uri(access_token, headers.uri)
|
||||
.await
|
||||
.and_then(|uri| uri.into_owned_uri())?;
|
||||
let uri = self
|
||||
.map_uri_resource(access_token, uri)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
|
||||
if !matches!(
|
||||
uri.collection,
|
||||
Collection::Calendar | Collection::AddressBook | Collection::FileNode
|
||||
) {
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
let archive = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
uri.account_id,
|
||||
uri.collection,
|
||||
uri.resource,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
|
||||
let acls = match uri.collection {
|
||||
Collection::FileNode => {
|
||||
&archive
|
||||
.unarchive::<FileNode>()
|
||||
.caused_by(trc::location!())?
|
||||
.acls
|
||||
}
|
||||
Collection::AddressBook => {
|
||||
&archive
|
||||
.unarchive::<AddressBook>()
|
||||
.caused_by(trc::location!())?
|
||||
.acls
|
||||
}
|
||||
Collection::Calendar => {
|
||||
&archive
|
||||
.unarchive::<Calendar>()
|
||||
.caused_by(trc::location!())?
|
||||
.acls
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
// Validate ACLs
|
||||
if !access_token.is_member(uri.account_id)
|
||||
&& !acls.effective_acl(access_token).contains(Acl::Share)
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
// Validate
|
||||
let account_ids = RoaringBitmap::from_iter(acls.iter().map(|a| u32::from(a.account_id)));
|
||||
let mut response = MultiStatus::new(Vec::with_capacity(16));
|
||||
|
||||
if !account_ids.is_empty() {
|
||||
if request.properties.is_empty() {
|
||||
request
|
||||
.properties
|
||||
.push(DavProperty::WebDav(WebDavProperty::DisplayName));
|
||||
}
|
||||
let request = PropFind::Prop(request.properties);
|
||||
self.prepare_principal_propfind_response(
|
||||
access_token,
|
||||
Collection::Principal,
|
||||
account_ids.into_iter(),
|
||||
&request,
|
||||
&mut response,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::MULTI_STATUS).with_xml_body(response.to_string()))
|
||||
}
|
||||
|
||||
async fn validate_and_map_aces(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
acl: dav_proto::schema::request::Acl,
|
||||
collection: Collection,
|
||||
) -> crate::Result<Vec<AclGrant>> {
|
||||
let mut grants = Vec::with_capacity(acl.aces.len());
|
||||
for ace in acl.aces {
|
||||
if ace.invert {
|
||||
return Err(DavError::Condition(DavErrorCondition::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
BaseCondition::NoInvert,
|
||||
)));
|
||||
}
|
||||
let privileges = match ace.grant_deny {
|
||||
GrantDeny::Grant(list) => list.0,
|
||||
GrantDeny::Deny(_) => {
|
||||
return Err(DavError::Condition(DavErrorCondition::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
BaseCondition::GrantOnly,
|
||||
)));
|
||||
}
|
||||
};
|
||||
let principal_uri = match ace.principal {
|
||||
Principal::Href(href) => href.0,
|
||||
_ => {
|
||||
return Err(DavError::Condition(DavErrorCondition::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
BaseCondition::AllowedPrincipal,
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
let mut acls = Bitmap::<Acl>::default();
|
||||
for privilege in privileges {
|
||||
match privilege {
|
||||
Privilege::Read => {
|
||||
acls.insert(Acl::Read);
|
||||
acls.insert(Acl::ReadItems);
|
||||
}
|
||||
Privilege::Write => {
|
||||
acls.insert(Acl::Modify);
|
||||
acls.insert(Acl::Delete);
|
||||
acls.insert(Acl::AddItems);
|
||||
acls.insert(Acl::ModifyItems);
|
||||
acls.insert(Acl::RemoveItems);
|
||||
}
|
||||
Privilege::WriteContent => {
|
||||
acls.insert(Acl::AddItems);
|
||||
acls.insert(Acl::Modify);
|
||||
acls.insert(Acl::ModifyItems);
|
||||
}
|
||||
Privilege::WriteProperties => {
|
||||
acls.insert(Acl::Modify);
|
||||
}
|
||||
Privilege::ReadCurrentUserPrivilegeSet
|
||||
| Privilege::Unlock
|
||||
| Privilege::Bind
|
||||
| Privilege::Unbind => {}
|
||||
Privilege::All => {
|
||||
return Err(DavError::Condition(DavErrorCondition::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
BaseCondition::NoAbstract,
|
||||
)));
|
||||
}
|
||||
Privilege::ReadAcl => {}
|
||||
Privilege::WriteAcl => {
|
||||
acls.insert(Acl::Share);
|
||||
}
|
||||
Privilege::ReadFreeBusy
|
||||
| Privilege::ScheduleQueryFreeBusy
|
||||
| Privilege::ScheduleSendFreeBusy => {
|
||||
if collection == Collection::Calendar {
|
||||
acls.insert(Acl::SchedulingReadFreeBusy);
|
||||
} else {
|
||||
return Err(DavError::Condition(DavErrorCondition::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
BaseCondition::NotSupportedPrivilege,
|
||||
)));
|
||||
}
|
||||
}
|
||||
Privilege::ScheduleDeliver | Privilege::ScheduleSend => {
|
||||
if collection == Collection::Calendar {
|
||||
acls.insert(Acl::SchedulingReadFreeBusy);
|
||||
acls.insert(Acl::SchedulingInvite);
|
||||
acls.insert(Acl::SchedulingReply);
|
||||
} else {
|
||||
return Err(DavError::Condition(DavErrorCondition::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
BaseCondition::NotSupportedPrivilege,
|
||||
)));
|
||||
}
|
||||
}
|
||||
Privilege::ScheduleDeliverInvite | Privilege::ScheduleSendInvite => {
|
||||
if collection == Collection::Calendar {
|
||||
acls.insert(Acl::SchedulingInvite);
|
||||
} else {
|
||||
return Err(DavError::Condition(DavErrorCondition::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
BaseCondition::NotSupportedPrivilege,
|
||||
)));
|
||||
}
|
||||
}
|
||||
Privilege::ScheduleDeliverReply | Privilege::ScheduleSendReply => {
|
||||
if collection == Collection::Calendar {
|
||||
acls.insert(Acl::SchedulingReply);
|
||||
} else {
|
||||
return Err(DavError::Condition(DavErrorCondition::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
BaseCondition::NotSupportedPrivilege,
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if acls.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let principal_id = self
|
||||
.validate_uri(access_token, &principal_uri)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
DavError::Condition(DavErrorCondition::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
BaseCondition::AllowedPrincipal,
|
||||
))
|
||||
})?
|
||||
.account_id
|
||||
.ok_or_else(|| {
|
||||
DavError::Condition(DavErrorCondition::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
BaseCondition::AllowedPrincipal,
|
||||
))
|
||||
})?;
|
||||
|
||||
// Verify that the principal is a valid principal
|
||||
/*let principal = self
|
||||
.directory()
|
||||
.query(QueryParams::id(principal_id).with_return_member_of(false))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or_else(|| {
|
||||
DavError::Condition(DavErrorCondition::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
BaseCondition::AllowedPrincipal,
|
||||
))
|
||||
})?;
|
||||
if !matches!(principal.typ(), Type::Individual | Type::Group) {
|
||||
return Err(DavError::Condition(DavErrorCondition::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
BaseCondition::AllowedPrincipal,
|
||||
)));
|
||||
}*/
|
||||
|
||||
grants.push(AclGrant {
|
||||
account_id: principal_id,
|
||||
grants: acls,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(grants)
|
||||
}
|
||||
|
||||
async fn resolve_ace(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
account_id: u32,
|
||||
grants: &ArchivedVec<ArchivedAclGrant>,
|
||||
expand: Option<&PropFind>,
|
||||
) -> crate::Result<Vec<Ace>> {
|
||||
let mut aces = Vec::with_capacity(grants.len());
|
||||
if access_token.is_member(account_id)
|
||||
|| grants.effective_acl(access_token).contains(Acl::Share)
|
||||
{
|
||||
for grant in grants.iter() {
|
||||
let grant_account_id = u32::from(grant.account_id);
|
||||
let principal = if let Some(expand) = expand {
|
||||
self.expand_principal(access_token, grant_account_id, expand)
|
||||
.await?
|
||||
.map(Principal::Response)
|
||||
.unwrap_or_else(|| {
|
||||
Principal::Href(Href(format!(
|
||||
"{}/_{grant_account_id}/",
|
||||
DavResourceName::Principal.base_path(),
|
||||
)))
|
||||
})
|
||||
} else {
|
||||
let grant_account = self
|
||||
.account(grant_account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
Principal::Href(Href(format!(
|
||||
"{}/{}/",
|
||||
DavResourceName::Principal.base_path(),
|
||||
percent_encoding::utf8_percent_encode(grant_account.name(), RFC_3986),
|
||||
)))
|
||||
};
|
||||
|
||||
aces.push(Ace::new(
|
||||
principal,
|
||||
GrantDeny::grant(current_user_privilege_set(Bitmap::<Acl>::from(
|
||||
&grant.grants,
|
||||
))),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(aces)
|
||||
}
|
||||
}
|
||||
|
||||
impl ResourceAcl for DavResources {
|
||||
fn validate_and_map_parent_acl(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
is_member: bool,
|
||||
parent_id: Option<u32>,
|
||||
check_acls: impl Into<Bitmap<Acl>> + Send,
|
||||
) -> crate::Result<u32> {
|
||||
match parent_id {
|
||||
Some(parent_id) => {
|
||||
if is_member || self.has_access_to_container(access_token, parent_id, check_acls) {
|
||||
Ok(parent_id + 1)
|
||||
} else {
|
||||
Err(DavError::Code(StatusCode::FORBIDDEN))
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if is_member {
|
||||
Ok(0)
|
||||
} else {
|
||||
Err(DavError::Code(StatusCode::FORBIDDEN))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait Privileges {
|
||||
fn current_privilege_set(
|
||||
&self,
|
||||
account_id: u32,
|
||||
grants: &ArchivedVec<ArchivedAclGrant>,
|
||||
is_calendar: bool,
|
||||
) -> Vec<Privilege>;
|
||||
}
|
||||
|
||||
impl Privileges for AccessToken {
|
||||
fn current_privilege_set(
|
||||
&self,
|
||||
account_id: u32,
|
||||
grants: &ArchivedVec<ArchivedAclGrant>,
|
||||
is_calendar: bool,
|
||||
) -> Vec<Privilege> {
|
||||
if self.is_member(account_id) {
|
||||
Privilege::all(is_calendar)
|
||||
} else {
|
||||
current_user_privilege_set(grants.effective_acl(self))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn current_user_privilege_set(acl_bitmap: Bitmap<Acl>) -> Vec<Privilege> {
|
||||
let mut acls = AHashSet::with_capacity(16);
|
||||
for grant in acl_bitmap {
|
||||
match grant {
|
||||
Acl::Read | Acl::ReadItems => {
|
||||
acls.insert(Privilege::Read);
|
||||
acls.insert(Privilege::ReadCurrentUserPrivilegeSet);
|
||||
}
|
||||
Acl::Modify => {
|
||||
acls.insert(Privilege::WriteProperties);
|
||||
}
|
||||
Acl::ModifyItems => {
|
||||
acls.insert(Privilege::WriteContent);
|
||||
}
|
||||
Acl::Delete | Acl::RemoveItems => {
|
||||
acls.insert(Privilege::Write);
|
||||
}
|
||||
Acl::Share => {
|
||||
acls.insert(Privilege::ReadAcl);
|
||||
acls.insert(Privilege::WriteAcl);
|
||||
}
|
||||
Acl::SchedulingReadFreeBusy => {
|
||||
acls.insert(Privilege::ReadFreeBusy);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
acls.into_iter().collect()
|
||||
}
|
||||
@@ -0,0 +1,892 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::ETag;
|
||||
use super::uri::{DavUriResource, OwnedUri, UriResource, Urn};
|
||||
use crate::{DavError, DavErrorCondition, DavMethod};
|
||||
use common::KV_LOCK_DAV;
|
||||
use common::{Server, auth::AccessToken};
|
||||
use dav_proto::schema::property::{ActiveLock, LockScope, WebDavProperty};
|
||||
use dav_proto::schema::request::DavPropertyValue;
|
||||
use dav_proto::schema::response::{BaseCondition, List, PropResponse};
|
||||
use dav_proto::{Condition, Depth, Timeout};
|
||||
use dav_proto::{RequestHeaders, schema::request::LockInfo};
|
||||
use groupware::cache::GroupwareCache;
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use std::collections::HashMap;
|
||||
use store::ValueKey;
|
||||
use store::dispatch::lookup::KeyValue;
|
||||
use store::write::serialize::rkyv_deserialize;
|
||||
use store::write::{AlignedBytes, Archive, Archiver, now};
|
||||
use store::{Serialize, U32_LEN};
|
||||
use trc::AddContext;
|
||||
use types::collection::Collection;
|
||||
use types::dead_property::DeadProperty;
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct ResourceState<'x> {
|
||||
pub account_id: u32,
|
||||
pub collection: Collection,
|
||||
pub document_id: Option<u32>,
|
||||
pub etag: Option<String>,
|
||||
pub lock_tokens: Vec<String>,
|
||||
pub sync_token: Option<String>,
|
||||
pub path: &'x str,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
|
||||
pub(crate) struct LockData {
|
||||
locks: HashMap<String, LockItems>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
|
||||
#[repr(transparent)]
|
||||
pub(crate) struct LockItems(Vec<LockItem>);
|
||||
|
||||
#[derive(Debug, Default, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
|
||||
pub(crate) struct LockItem {
|
||||
lock_id: u64,
|
||||
owner: u32,
|
||||
expires: u64,
|
||||
depth_infinity: bool,
|
||||
exclusive: bool,
|
||||
owner_dav: Option<DeadProperty>,
|
||||
}
|
||||
|
||||
struct LockCache<'x> {
|
||||
account_id: u32,
|
||||
collection: Collection,
|
||||
lock_archive: LockArchive<'x>,
|
||||
}
|
||||
|
||||
enum LockArchive<'x> {
|
||||
Unarchived(&'x ArchivedLockData),
|
||||
Archived(Archive<AlignedBytes>),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct LockCaches<'x> {
|
||||
caches: Vec<LockCache<'x>>,
|
||||
}
|
||||
|
||||
pub(crate) trait LockRequestHandler: Sync + Send {
|
||||
fn handle_lock_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
lock_info: LockRequest,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
|
||||
fn validate_headers(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
resources: Vec<ResourceState<'_>>,
|
||||
locks: LockCaches<'_>,
|
||||
method: DavMethod,
|
||||
) -> impl Future<Output = crate::Result<()>> + Send;
|
||||
}
|
||||
|
||||
pub(crate) enum LockRequest {
|
||||
Lock(LockInfo),
|
||||
Unlock,
|
||||
Refresh,
|
||||
}
|
||||
|
||||
impl LockRequestHandler for Server {
|
||||
async fn handle_lock_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
lock_info: LockRequest,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
let resource = self
|
||||
.validate_uri(access_token, headers.uri)
|
||||
.await?
|
||||
.into_owned_uri()?;
|
||||
let resource_hash = resource.lock_key();
|
||||
let resource_path = resource
|
||||
.resource
|
||||
.ok_or(DavError::Code(StatusCode::CONFLICT))?;
|
||||
let account_id = resource.account_id;
|
||||
if !access_token.is_member(account_id) {
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
let resources = vec![ResourceState {
|
||||
account_id,
|
||||
collection: resource.collection,
|
||||
path: resource_path,
|
||||
..Default::default()
|
||||
}];
|
||||
|
||||
let mut base_path = None;
|
||||
let is_lock_request = !matches!(lock_info, LockRequest::Unlock);
|
||||
let if_lock_token = headers
|
||||
.if_
|
||||
.iter()
|
||||
.flat_map(|if_| if_.list.iter())
|
||||
.find_map(|cond| {
|
||||
if let Condition::StateToken { token, .. } = cond {
|
||||
Urn::parse(token).and_then(|u| u.try_unwrap_lock())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let mut lock_data = if let Some(lock_data) = self
|
||||
.in_memory_store()
|
||||
.key_get::<Archive<AlignedBytes>>(resource_hash.as_slice())
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
let lock_data = lock_data
|
||||
.unarchive::<LockData>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
self.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
resources,
|
||||
LockCaches::new_shared(account_id, resource.collection, lock_data),
|
||||
if is_lock_request {
|
||||
DavMethod::LOCK
|
||||
} else {
|
||||
DavMethod::UNLOCK
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let LockRequest::Lock(lock_info) = &lock_info {
|
||||
let mut failed_locks = Vec::new();
|
||||
let is_exclusive = matches!(lock_info.lock_scope, LockScope::Exclusive);
|
||||
let is_infinity = matches!(headers.depth, Depth::Infinity);
|
||||
|
||||
for (lock_path, lock_item) in lock_data.find_locks(resource_path, true) {
|
||||
if if_lock_token != lock_item.lock_id
|
||||
&& (lock_item.exclusive || is_exclusive)
|
||||
&& (lock_path.len() == resource_path.len()
|
||||
|| lock_item.depth_infinity && resource_path.len() > lock_path.len()
|
||||
|| is_infinity && lock_path.len() > resource_path.len())
|
||||
{
|
||||
let base_path =
|
||||
base_path.get_or_insert_with(|| headers.base_uri().unwrap_or_default());
|
||||
failed_locks.push(format!("{base_path}/{lock_path}").into());
|
||||
}
|
||||
}
|
||||
|
||||
if !failed_locks.is_empty() {
|
||||
return Err(DavErrorCondition::new(
|
||||
StatusCode::LOCKED,
|
||||
BaseCondition::LockTokenSubmitted(List(failed_locks)),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
// Validate lock_info
|
||||
if lock_info.owner.as_ref().is_some_and(|o| {
|
||||
o.size() > self.core.groupware.dead_property_size.unwrap_or(512)
|
||||
}) {
|
||||
return Err(DavError::Code(StatusCode::PAYLOAD_TOO_LARGE));
|
||||
}
|
||||
|
||||
if self.core.groupware.max_locks_per_user > 0
|
||||
&& lock_data
|
||||
.locks
|
||||
.values()
|
||||
.flat_map(|locks| {
|
||||
locks
|
||||
.0
|
||||
.iter()
|
||||
.filter(|lock| lock.owner == access_token.account_id())
|
||||
})
|
||||
.count()
|
||||
>= self.core.groupware.max_locks_per_user
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::TOO_MANY_REQUESTS));
|
||||
}
|
||||
}
|
||||
|
||||
rkyv_deserialize(lock_data).caused_by(trc::location!())?
|
||||
} else if is_lock_request {
|
||||
self.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
resources,
|
||||
Default::default(),
|
||||
DavMethod::LOCK,
|
||||
)
|
||||
.await?;
|
||||
|
||||
LockData::default()
|
||||
} else {
|
||||
return Err(DavErrorCondition::new(
|
||||
StatusCode::CONFLICT,
|
||||
BaseCondition::LockTokenMatchesRequestUri,
|
||||
)
|
||||
.into());
|
||||
};
|
||||
|
||||
let now = now();
|
||||
let response = if is_lock_request {
|
||||
let timeout = if let Timeout::Second(seconds) = headers.timeout {
|
||||
std::cmp::min(seconds, self.core.groupware.max_lock_timeout)
|
||||
} else {
|
||||
self.core.groupware.max_lock_timeout
|
||||
};
|
||||
let expires = now + timeout;
|
||||
|
||||
let lock_item = if if_lock_token > 0 {
|
||||
if let Some(lock_item) = lock_data
|
||||
.locks
|
||||
.values_mut()
|
||||
.flat_map(|locks| locks.0.iter_mut())
|
||||
.find(|lock| lock.lock_id == if_lock_token)
|
||||
{
|
||||
lock_item
|
||||
} else {
|
||||
return Err(DavError::Code(StatusCode::PRECONDITION_FAILED));
|
||||
}
|
||||
} else {
|
||||
let locks = lock_data
|
||||
.locks
|
||||
.entry(resource_path.to_string())
|
||||
.or_insert_with(Default::default);
|
||||
locks.0.push(LockItem::default());
|
||||
locks.0.last_mut().unwrap()
|
||||
};
|
||||
|
||||
lock_item.expires = expires;
|
||||
if let LockRequest::Lock(lock_info) = lock_info {
|
||||
// Validate lock_info
|
||||
if lock_info.owner.as_ref().is_some_and(|o| {
|
||||
o.size() > self.core.groupware.dead_property_size.unwrap_or(512)
|
||||
}) {
|
||||
return Err(DavError::Code(StatusCode::PAYLOAD_TOO_LARGE));
|
||||
}
|
||||
|
||||
lock_item.lock_id = store::rand::random::<u64>() ^ expires;
|
||||
lock_item.owner = access_token.account_id();
|
||||
lock_item.depth_infinity = matches!(headers.depth, Depth::Infinity);
|
||||
lock_item.owner_dav = lock_info.owner;
|
||||
lock_item.exclusive = matches!(lock_info.lock_scope, LockScope::Exclusive);
|
||||
}
|
||||
|
||||
let base_path = base_path.get_or_insert_with(|| headers.base_uri().unwrap_or_default());
|
||||
let active_lock = lock_item.to_active_lock(format!("{base_path}/{resource_path}"));
|
||||
|
||||
HttpResponse::new(if if_lock_token == 0 {
|
||||
StatusCode::CREATED
|
||||
} else {
|
||||
StatusCode::OK
|
||||
})
|
||||
.with_lock_token(&active_lock.lock_token.as_ref().unwrap().0)
|
||||
.with_xml_body(
|
||||
PropResponse::new(vec![DavPropertyValue::new(
|
||||
WebDavProperty::LockDiscovery,
|
||||
vec![active_lock],
|
||||
)])
|
||||
.to_string(),
|
||||
)
|
||||
} else {
|
||||
let lock_id = headers
|
||||
.lock_token
|
||||
.and_then(Urn::parse)
|
||||
.and_then(|urn| urn.try_unwrap_lock())
|
||||
.ok_or(DavError::Code(StatusCode::BAD_REQUEST))?;
|
||||
|
||||
if lock_data.remove_lock(lock_id) {
|
||||
HttpResponse::new(StatusCode::NO_CONTENT)
|
||||
} else {
|
||||
return Err(DavErrorCondition::new(
|
||||
StatusCode::CONFLICT,
|
||||
BaseCondition::LockTokenMatchesRequestUri,
|
||||
)
|
||||
.into());
|
||||
}
|
||||
};
|
||||
|
||||
// Remove expired locks
|
||||
let max_expire = lock_data.remove_expired();
|
||||
if max_expire > 0 {
|
||||
self.in_memory_store()
|
||||
.key_set(
|
||||
KeyValue::new(
|
||||
resource_hash,
|
||||
Archiver::new(lock_data)
|
||||
.untrusted()
|
||||
.serialize()
|
||||
.caused_by(trc::location!())?,
|
||||
)
|
||||
.expires(max_expire),
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
} else {
|
||||
self.in_memory_store()
|
||||
.key_delete(resource_hash)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn validate_headers(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
mut resources: Vec<ResourceState<'_>>,
|
||||
mut locks_: LockCaches<'_>,
|
||||
method: DavMethod,
|
||||
) -> crate::Result<()> {
|
||||
let no_if_headers = headers.if_.is_empty();
|
||||
match method {
|
||||
DavMethod::GET | DavMethod::HEAD if no_if_headers => {
|
||||
// Return early for GET/HEAD requests without If headers
|
||||
return Ok(());
|
||||
}
|
||||
DavMethod::COPY
|
||||
| DavMethod::MOVE
|
||||
| DavMethod::POST
|
||||
| DavMethod::PUT
|
||||
| DavMethod::PATCH
|
||||
if headers.overwrite_fail
|
||||
&& resources.last().is_some_and(|r| {
|
||||
r.etag.is_some() || r.document_id.is_some_and(|id| id != u32::MAX)
|
||||
}) =>
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::PRECONDITION_FAILED));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Add lock data to the cache
|
||||
for resource in &resources {
|
||||
if locks_.is_cached(resource).is_none() {
|
||||
locks_.insert_lock_data(self, resource).await?;
|
||||
}
|
||||
}
|
||||
|
||||
// Unarchive lock data
|
||||
let mut locks = locks_.to_unarchived().caused_by(trc::location!())?;
|
||||
|
||||
// Validate locks for write operations
|
||||
let mut lock_response = Ok(());
|
||||
if !matches!(
|
||||
method,
|
||||
DavMethod::GET | DavMethod::HEAD | DavMethod::LOCK | DavMethod::UNLOCK
|
||||
) {
|
||||
let mut base_path = None;
|
||||
|
||||
'outer: for (pos, resource) in resources.iter().enumerate() {
|
||||
if pos == 0 && matches!(method, DavMethod::COPY) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(idx) = locks.find_cache_pos(self, resource).await? {
|
||||
let mut failed_locks = Vec::new();
|
||||
|
||||
for (lock_path, lock_item) in locks.find_locks_by_pos(idx, resource, true)? {
|
||||
let lock_token = lock_item.urn().to_string();
|
||||
if headers.if_.iter().any(|if_| {
|
||||
if_.resource
|
||||
.is_none_or(|r| {
|
||||
r.trim_end_matches('/').ends_with(lock_path)})
|
||||
&& if_.list.iter().any(|cond| matches!(cond, Condition::StateToken { token, .. } if token == &lock_token))
|
||||
}) {
|
||||
break 'outer;
|
||||
} else {
|
||||
let base_path = base_path.get_or_insert_with(|| {
|
||||
headers.base_uri()
|
||||
.unwrap_or_default()
|
||||
});
|
||||
failed_locks.push(format!("{base_path}/{lock_path}").into());
|
||||
}
|
||||
}
|
||||
|
||||
if !failed_locks.is_empty() {
|
||||
lock_response = Err(DavErrorCondition::new(
|
||||
StatusCode::LOCKED,
|
||||
BaseCondition::LockTokenSubmitted(List(failed_locks)),
|
||||
)
|
||||
.into());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// There are no If headers, so we can return early
|
||||
if no_if_headers {
|
||||
return lock_response;
|
||||
}
|
||||
|
||||
let mut resource_not_found = ResourceState {
|
||||
account_id: u32::MAX,
|
||||
collection: Collection::None,
|
||||
path: "",
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
'outer: for if_ in &headers.if_ {
|
||||
if if_.list.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut resource_state = &mut resource_not_found;
|
||||
|
||||
if let Some(resource) = if_.resource {
|
||||
if let Some(resource) = self
|
||||
.validate_uri(access_token, resource)
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|r| {
|
||||
let path = r.resource?;
|
||||
|
||||
Some(ResourceState {
|
||||
account_id: r.account_id?,
|
||||
collection: if !matches!(r.collection, Collection::FileNode)
|
||||
&& path.contains('/')
|
||||
{
|
||||
r.collection.child_collection().unwrap_or(r.collection)
|
||||
} else {
|
||||
r.collection
|
||||
},
|
||||
path,
|
||||
..Default::default()
|
||||
})
|
||||
})
|
||||
{
|
||||
if let Some(known_resource) = resources.iter_mut().find(|r| {
|
||||
r.account_id == resource.account_id
|
||||
&& r.collection == resource.collection
|
||||
&& r.path == resource.path
|
||||
}) {
|
||||
resource_state = known_resource;
|
||||
} else if access_token.has_access(resource.account_id, resource.collection) {
|
||||
resources.push(resource);
|
||||
resource_state = resources.last_mut().unwrap();
|
||||
}
|
||||
}
|
||||
} else if let Some(resource) = resources.first_mut() {
|
||||
resource_state = resource;
|
||||
};
|
||||
|
||||
// Fill missing data for resource
|
||||
if resource_state.collection != Collection::None
|
||||
&& (resource_state.etag.is_none()
|
||||
|| resource_state.lock_tokens.is_empty()
|
||||
|| resource_state.sync_token.is_none())
|
||||
{
|
||||
let mut needs_lock_token = false;
|
||||
let mut needs_sync_token = false;
|
||||
let mut needs_etag = false;
|
||||
|
||||
for cond in &if_.list {
|
||||
match cond {
|
||||
Condition::StateToken { token, .. } => {
|
||||
if token.starts_with("urn:stalwart:davsync:") {
|
||||
needs_sync_token = true;
|
||||
} else {
|
||||
needs_lock_token = true;
|
||||
}
|
||||
}
|
||||
Condition::ETag { .. } | Condition::Exists { .. } => {
|
||||
needs_etag = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch eTag
|
||||
if needs_etag && resource_state.etag.is_none() {
|
||||
if resource_state.document_id.is_none() {
|
||||
resource_state.document_id = self
|
||||
.map_uri_resource(
|
||||
access_token,
|
||||
UriResource {
|
||||
collection: resource_state.collection,
|
||||
account_id: resource_state.account_id,
|
||||
resource: resource_state.path.into(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.map(|uri| uri.resource)
|
||||
.unwrap_or(u32::MAX)
|
||||
.into();
|
||||
}
|
||||
|
||||
if let Some(document_id) =
|
||||
resource_state.document_id.filter(|&id| id != u32::MAX)
|
||||
&& let Some(archive) = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
resource_state.account_id,
|
||||
resource_state.collection,
|
||||
document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
resource_state.etag = archive.etag().into();
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch lock token
|
||||
if needs_lock_token
|
||||
&& resource_state.lock_tokens.is_empty()
|
||||
&& let Some(idx) = locks.find_cache_pos(self, resource_state).await?
|
||||
{
|
||||
let found_locks = locks
|
||||
.find_locks_by_pos(idx, resource_state, false)?
|
||||
.iter()
|
||||
.map(|(_, lock)| lock.urn().to_string())
|
||||
.collect::<Vec<_>>();
|
||||
resource_state.lock_tokens = found_locks;
|
||||
}
|
||||
|
||||
// Fetch sync token
|
||||
if needs_sync_token && resource_state.sync_token.is_none() {
|
||||
let id = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
resource_state.account_id,
|
||||
resource_state.collection.into(),
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.highest_change_id;
|
||||
resource_state.sync_token = Some(Urn::Sync { id, seq: 0 }.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
for cond in &if_.list {
|
||||
match cond {
|
||||
Condition::StateToken { is_not, token } => {
|
||||
if let Some(token) = Urn::try_extract_sync_id(token) {
|
||||
if !((resource_state
|
||||
.sync_token
|
||||
.as_deref()
|
||||
.and_then(Urn::try_extract_sync_id)
|
||||
.is_some_and(|sync_token| sync_token == token))
|
||||
^ is_not)
|
||||
{
|
||||
continue 'outer;
|
||||
}
|
||||
} else if !((resource_state.lock_tokens.iter().any(|t| t == token))
|
||||
^ is_not)
|
||||
{
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
Condition::ETag { is_not, tag } => {
|
||||
if !((resource_state.etag.as_ref().is_some_and(|etag| etag == tag))
|
||||
^ is_not)
|
||||
{
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
Condition::Exists { is_not } => {
|
||||
if !((resource_state.etag.is_some()) ^ is_not) {
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lock_response;
|
||||
}
|
||||
|
||||
Err(DavError::Code(
|
||||
if matches!(method, DavMethod::GET | DavMethod::HEAD)
|
||||
&& headers
|
||||
.if_
|
||||
.iter()
|
||||
.any(|if_| if_.list.iter().any(|cond| cond.is_none_match()))
|
||||
{
|
||||
StatusCode::NOT_MODIFIED
|
||||
} else {
|
||||
StatusCode::PRECONDITION_FAILED
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl LockData {
|
||||
pub fn remove_lock(&mut self, lock_id: u64) -> bool {
|
||||
for (lock_path, lock_items) in self.locks.iter_mut() {
|
||||
for (idx, lock_item) in lock_items.0.iter().enumerate() {
|
||||
if lock_item.lock_id == lock_id {
|
||||
lock_items.0.swap_remove(idx);
|
||||
if lock_items.0.is_empty() {
|
||||
let lock_path = lock_path.clone();
|
||||
self.locks.remove(&lock_path);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn remove_expired(&mut self) -> u64 {
|
||||
let mut max_expire = 0;
|
||||
let now = now();
|
||||
|
||||
self.locks.retain(|_, locks| {
|
||||
locks.0.retain(|lock| {
|
||||
if lock.expires > now {
|
||||
max_expire = std::cmp::max(max_expire, lock.expires);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
!locks.0.is_empty()
|
||||
});
|
||||
|
||||
max_expire
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> LockArchive<'x> {
|
||||
fn unarchive(&'x self) -> trc::Result<&'x ArchivedLockData> {
|
||||
match self {
|
||||
LockArchive::Unarchived(archived_lock_data) => Ok(archived_lock_data),
|
||||
LockArchive::Archived(archive) => {
|
||||
archive.unarchive::<LockData>().caused_by(trc::location!())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> LockCaches<'x> {
|
||||
pub(self) fn new_shared(
|
||||
account_id: u32,
|
||||
collection: Collection,
|
||||
lock_data: &'x ArchivedLockData,
|
||||
) -> Self {
|
||||
Self {
|
||||
caches: vec![LockCache {
|
||||
account_id,
|
||||
collection,
|
||||
lock_archive: LockArchive::Unarchived(lock_data),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_unarchived(&'x self) -> trc::Result<LockCaches<'x>> {
|
||||
let caches = self
|
||||
.caches
|
||||
.iter()
|
||||
.map(|cache| {
|
||||
Ok(LockCache {
|
||||
account_id: cache.account_id,
|
||||
collection: cache.collection,
|
||||
lock_archive: LockArchive::Unarchived(
|
||||
cache.lock_archive.unarchive().caused_by(trc::location!())?,
|
||||
),
|
||||
})
|
||||
})
|
||||
.collect::<trc::Result<Vec<_>>>()?;
|
||||
|
||||
Ok(LockCaches { caches })
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_cached(&self, resource_state: &ResourceState<'_>) -> Option<usize> {
|
||||
self.caches.iter().position(|cache| {
|
||||
resource_state.account_id == cache.account_id
|
||||
&& resource_state.collection.main_collection() == cache.collection.main_collection()
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn find_cache_pos(
|
||||
&mut self,
|
||||
server: &Server,
|
||||
resource_state: &ResourceState<'_>,
|
||||
) -> trc::Result<Option<usize>> {
|
||||
if let Some(idx) = self.is_cached(resource_state) {
|
||||
Ok(Some(idx))
|
||||
} else if resource_state.collection != Collection::None {
|
||||
if self.insert_lock_data(server, resource_state).await? {
|
||||
Ok(Some(self.caches.len() - 1))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn find_locks_by_pos(
|
||||
&'x self,
|
||||
pos: usize,
|
||||
resource_state: &'x ResourceState<'_>,
|
||||
include_children: bool,
|
||||
) -> trc::Result<Vec<(&'x str, &'x ArchivedLockItem)>> {
|
||||
self.caches[pos]
|
||||
.lock_archive
|
||||
.unarchive()
|
||||
.map(|l| l.find_locks(resource_state.path, include_children))
|
||||
}
|
||||
|
||||
async fn insert_lock_data(
|
||||
&mut self,
|
||||
server: &Server,
|
||||
resource_state: &ResourceState<'_>,
|
||||
) -> trc::Result<bool> {
|
||||
if let Some(lock_archive) = server
|
||||
.in_memory_store()
|
||||
.key_get::<Archive<AlignedBytes>>(resource_state.lock_key().as_slice())
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
self.caches.push(LockCache {
|
||||
account_id: resource_state.account_id,
|
||||
collection: resource_state.collection,
|
||||
lock_archive: LockArchive::Archived(lock_archive),
|
||||
});
|
||||
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LockItem {
|
||||
pub fn to_active_lock(&self, href: String) -> ActiveLock {
|
||||
ActiveLock::new(
|
||||
href,
|
||||
if self.exclusive {
|
||||
LockScope::Exclusive
|
||||
} else {
|
||||
LockScope::Shared
|
||||
},
|
||||
)
|
||||
.with_depth(if self.depth_infinity {
|
||||
Depth::Infinity
|
||||
} else {
|
||||
Depth::Zero
|
||||
})
|
||||
.with_owner_opt(self.owner_dav.clone())
|
||||
.with_timeout(self.expires.saturating_sub(now()))
|
||||
.with_lock_token(self.urn().to_string())
|
||||
}
|
||||
|
||||
pub fn urn(&self) -> Urn {
|
||||
Urn::Lock(self.lock_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl ArchivedLockData {
|
||||
pub fn find_locks<'x: 'y, 'y>(
|
||||
&'x self,
|
||||
resource: &'y str,
|
||||
include_children: bool,
|
||||
) -> Vec<(&'y str, &'x ArchivedLockItem)> {
|
||||
let now = now();
|
||||
let mut resource_part = resource;
|
||||
let mut found_locks = Vec::new();
|
||||
|
||||
loop {
|
||||
if let Some(locks) = self.locks.get(resource_part) {
|
||||
found_locks.extend(
|
||||
locks
|
||||
.0
|
||||
.iter()
|
||||
.filter(|lock| {
|
||||
lock.expires > now && (resource == resource_part || lock.depth_infinity)
|
||||
})
|
||||
.map(|lock| (resource_part, lock)),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some((resource_part_, _)) = resource_part.rsplit_once('/') {
|
||||
resource_part = resource_part_;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if include_children {
|
||||
let prefix = format!("{}/", resource);
|
||||
for (resource_part, locks) in self.locks.iter() {
|
||||
if resource_part.starts_with(&prefix) {
|
||||
found_locks.extend(
|
||||
locks
|
||||
.0
|
||||
.iter()
|
||||
.filter(|lock| lock.expires > now)
|
||||
.map(|lock| (resource_part.as_str(), lock)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
found_locks
|
||||
}
|
||||
}
|
||||
|
||||
impl ArchivedLockItem {
|
||||
pub fn to_active_lock(&self, href: String) -> ActiveLock {
|
||||
ActiveLock::new(
|
||||
href,
|
||||
if self.exclusive {
|
||||
LockScope::Exclusive
|
||||
} else {
|
||||
LockScope::Shared
|
||||
},
|
||||
)
|
||||
.with_depth(if self.depth_infinity {
|
||||
Depth::Infinity
|
||||
} else {
|
||||
Depth::Zero
|
||||
})
|
||||
.with_owner_opt(self.owner_dav.as_ref().map(Into::into))
|
||||
.with_timeout(u64::from(self.expires).saturating_sub(now()))
|
||||
.with_lock_token(self.urn().to_string())
|
||||
}
|
||||
|
||||
pub fn urn(&self) -> Urn {
|
||||
Urn::Lock(self.lock_id.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl OwnedUri<'_> {
|
||||
pub fn lock_key(&self) -> Vec<u8> {
|
||||
build_lock_key(self.account_id, self.collection.main_collection())
|
||||
}
|
||||
}
|
||||
|
||||
impl ResourceState<'_> {
|
||||
pub fn lock_key(&self) -> Vec<u8> {
|
||||
build_lock_key(self.account_id, self.collection.main_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
|
||||
&& self.collection == other.collection
|
||||
&& self.document_id == other.document_id
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for ResourceState<'_> {}
|
||||
@@ -0,0 +1,520 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use calcard::{
|
||||
icalendar::{ICalendarComponentType, ICalendarParameterName, ICalendarProperty},
|
||||
vcard::{VCardParameterName, VCardVersion},
|
||||
};
|
||||
use dav_proto::{
|
||||
Depth, RequestHeaders, Return,
|
||||
schema::{
|
||||
Namespace,
|
||||
property::{DavProperty, ReportSet, ResourceType},
|
||||
request::{
|
||||
AddressbookQuery, CalendarQuery, ExpandProperty, Filter, MultiGet, PropFind,
|
||||
SyncCollection, Timezone, VCardPropertyWithGroup,
|
||||
},
|
||||
},
|
||||
};
|
||||
use groupware::{
|
||||
calendar::{
|
||||
ArchivedCalendar, ArchivedCalendarEvent, ArchivedCalendarEventNotification, Calendar,
|
||||
CalendarEvent, CalendarEventNotification,
|
||||
},
|
||||
contact::{AddressBook, ArchivedAddressBook, ArchivedContactCard, ContactCard},
|
||||
file::{ArchivedFileNode, FileNode},
|
||||
};
|
||||
use propfind::PropFindItem;
|
||||
use rkyv::vec::ArchivedVec;
|
||||
use store::write::{AlignedBytes, Archive, BatchBuilder, Operation, ValueClass, ValueOp};
|
||||
use types::{
|
||||
TimeRange, acl::ArchivedAclGrant, collection::Collection, dead_property::ArchivedDeadProperty,
|
||||
field::Field,
|
||||
};
|
||||
use uri::{OwnedUri, Urn};
|
||||
|
||||
pub mod acl;
|
||||
pub mod lock;
|
||||
pub mod propfind;
|
||||
pub mod uri;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct DavQuery<'x> {
|
||||
pub uri: &'x str,
|
||||
pub resource: DavQueryResource<'x>,
|
||||
pub propfind: PropFind,
|
||||
pub sync_type: SyncType,
|
||||
pub depth: usize,
|
||||
pub limit: Option<u32>,
|
||||
pub vcard_version: Option<VCardVersion>,
|
||||
pub ret: Return,
|
||||
pub depth_no_root: bool,
|
||||
pub expand: bool,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub(crate) enum SyncType {
|
||||
#[default]
|
||||
None,
|
||||
Initial,
|
||||
From {
|
||||
id: u64,
|
||||
seq: u32,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub(crate) enum DavQueryResource<'x> {
|
||||
Uri(OwnedUri<'x>),
|
||||
Multiget {
|
||||
parent_collection: Collection,
|
||||
hrefs: Vec<String>,
|
||||
},
|
||||
Query {
|
||||
filter: DavQueryFilter,
|
||||
parent_collection: Collection,
|
||||
items: Vec<PropFindItem>,
|
||||
},
|
||||
#[default]
|
||||
None,
|
||||
}
|
||||
|
||||
pub(crate) type AddressbookFilter = Vec<Filter<(), VCardPropertyWithGroup, VCardParameterName>>;
|
||||
pub(crate) type CalendarFilter =
|
||||
Vec<Filter<Vec<ICalendarComponentType>, ICalendarProperty, ICalendarParameterName>>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum DavQueryFilter {
|
||||
Addressbook(AddressbookFilter),
|
||||
Calendar {
|
||||
filter: CalendarFilter,
|
||||
max_time_range: Option<TimeRange>,
|
||||
timezone: Timezone,
|
||||
},
|
||||
}
|
||||
|
||||
pub(crate) trait ETag {
|
||||
fn etag(&self) -> String;
|
||||
}
|
||||
|
||||
pub(crate) trait ExtractETag {
|
||||
fn etag(&self) -> Option<String>;
|
||||
}
|
||||
|
||||
impl<T> ETag for Archive<T> {
|
||||
fn etag(&self) -> String {
|
||||
format!("\"{}\"", self.version.hash().unwrap_or_default())
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtractETag for BatchBuilder {
|
||||
fn etag(&self) -> Option<String> {
|
||||
let p_value = u8::from(Field::ARCHIVE);
|
||||
for op in self.ops().iter().rev() {
|
||||
match op {
|
||||
Operation::Value {
|
||||
class: ValueClass::Property(p_id),
|
||||
op: ValueOp::Set(value),
|
||||
} if *p_id == p_value => {
|
||||
return Archive::<AlignedBytes>::extract_hash(value)
|
||||
.map(|hash| format!("\"{}\"", hash));
|
||||
}
|
||||
Operation::Value {
|
||||
class: ValueClass::Property(p_id),
|
||||
op: ValueOp::SetFnc(set_fnc),
|
||||
} if *p_id == p_value => {
|
||||
return Archive::<AlignedBytes>::extract_hash(set_fnc.params().bytes(0))
|
||||
.map(|hash| format!("\"{}\"", hash));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait DavCollection {
|
||||
fn namespace(&self) -> Namespace;
|
||||
}
|
||||
|
||||
impl DavCollection for Collection {
|
||||
fn namespace(&self) -> Namespace {
|
||||
match self {
|
||||
Collection::Calendar
|
||||
| Collection::CalendarEvent
|
||||
| Collection::CalendarEventNotification => Namespace::CalDav,
|
||||
Collection::AddressBook | Collection::ContactCard => Namespace::CardDav,
|
||||
_ => Namespace::Dav,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> DavQuery<'x> {
|
||||
pub fn propfind(
|
||||
resource: OwnedUri<'x>,
|
||||
propfind: PropFind,
|
||||
headers: &RequestHeaders<'x>,
|
||||
) -> Self {
|
||||
Self {
|
||||
resource: DavQueryResource::Uri(resource),
|
||||
propfind,
|
||||
depth: match headers.depth {
|
||||
Depth::Zero => 0,
|
||||
_ => 1,
|
||||
},
|
||||
ret: headers.ret,
|
||||
depth_no_root: headers.depth_no_root,
|
||||
uri: headers.uri,
|
||||
vcard_version: headers.vcard_version,
|
||||
sync_type: Default::default(),
|
||||
limit: Default::default(),
|
||||
expand: 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,
|
||||
ret: headers.ret,
|
||||
depth_no_root: headers.depth_no_root,
|
||||
uri: headers.uri,
|
||||
vcard_version: headers.vcard_version,
|
||||
sync_type: Default::default(),
|
||||
depth: Default::default(),
|
||||
limit: Default::default(),
|
||||
expand: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn addressbook_query(
|
||||
query: AddressbookQuery,
|
||||
items: Vec<PropFindItem>,
|
||||
headers: &RequestHeaders<'x>,
|
||||
) -> Self {
|
||||
Self {
|
||||
resource: DavQueryResource::Query {
|
||||
filter: DavQueryFilter::Addressbook(query.filters),
|
||||
parent_collection: Collection::AddressBook,
|
||||
items,
|
||||
},
|
||||
propfind: query.properties,
|
||||
limit: query.limit,
|
||||
ret: headers.ret,
|
||||
depth_no_root: headers.depth_no_root,
|
||||
uri: headers.uri,
|
||||
vcard_version: headers.vcard_version,
|
||||
sync_type: Default::default(),
|
||||
depth: Default::default(),
|
||||
expand: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn calendar_query(
|
||||
query: CalendarQuery,
|
||||
max_time_range: Option<TimeRange>,
|
||||
items: Vec<PropFindItem>,
|
||||
headers: &RequestHeaders<'x>,
|
||||
) -> Self {
|
||||
Self {
|
||||
resource: DavQueryResource::Query {
|
||||
filter: DavQueryFilter::Calendar {
|
||||
filter: query.filters,
|
||||
timezone: query.timezone,
|
||||
max_time_range,
|
||||
},
|
||||
parent_collection: Collection::Calendar,
|
||||
items,
|
||||
},
|
||||
propfind: query.properties,
|
||||
ret: headers.ret,
|
||||
depth_no_root: headers.depth_no_root,
|
||||
uri: headers.uri,
|
||||
sync_type: Default::default(),
|
||||
depth: Default::default(),
|
||||
limit: Default::default(),
|
||||
vcard_version: Default::default(),
|
||||
expand: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn changes(
|
||||
resource: OwnedUri<'x>,
|
||||
changes: SyncCollection,
|
||||
headers: &RequestHeaders<'x>,
|
||||
) -> Self {
|
||||
Self {
|
||||
resource: DavQueryResource::Uri(resource),
|
||||
propfind: changes.properties,
|
||||
sync_type: changes
|
||||
.sync_token
|
||||
.as_deref()
|
||||
.and_then(Urn::parse)
|
||||
.and_then(|urn| urn.try_unwrap_sync())
|
||||
.map(|(id, seq)| SyncType::From { id, seq })
|
||||
.unwrap_or(SyncType::Initial),
|
||||
depth: match changes.depth {
|
||||
Depth::One => 1,
|
||||
Depth::Infinity => usize::MAX,
|
||||
_ => 0,
|
||||
},
|
||||
limit: changes.limit,
|
||||
ret: headers.ret,
|
||||
depth_no_root: headers.depth_no_root,
|
||||
expand: false,
|
||||
uri: headers.uri,
|
||||
vcard_version: headers.vcard_version,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expand(
|
||||
resource: OwnedUri<'x>,
|
||||
expand: ExpandProperty,
|
||||
headers: &RequestHeaders<'x>,
|
||||
) -> Self {
|
||||
let mut props = Vec::with_capacity(expand.properties.len());
|
||||
for item in expand.properties {
|
||||
if !matches!(item.property, DavProperty::DeadProperty(_))
|
||||
&& !props.contains(&item.property)
|
||||
{
|
||||
props.push(item.property);
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
resource: DavQueryResource::Uri(resource),
|
||||
propfind: PropFind::Prop(props),
|
||||
depth: match headers.depth {
|
||||
Depth::Zero => 0,
|
||||
_ => 1,
|
||||
},
|
||||
ret: headers.ret,
|
||||
depth_no_root: headers.depth_no_root,
|
||||
expand: true,
|
||||
uri: headers.uri,
|
||||
sync_type: Default::default(),
|
||||
limit: Default::default(),
|
||||
vcard_version: headers.vcard_version,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_minimal(&self) -> bool {
|
||||
self.ret == Return::Minimal
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum ArchivedResource<'x> {
|
||||
Calendar(Archive<&'x ArchivedCalendar>),
|
||||
CalendarEvent(Archive<&'x ArchivedCalendarEvent>),
|
||||
CalendarEventNotification(Archive<&'x ArchivedCalendarEventNotification>),
|
||||
CalendarEventNotificationCollection(bool),
|
||||
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::CalendarEventNotification => archive
|
||||
.to_unarchived::<CalendarEventNotification>()
|
||||
.map(ArchivedResource::CalendarEventNotification),
|
||||
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.to_native(),
|
||||
ArchivedResource::CalendarEvent(archive) => archive.inner.created.to_native(),
|
||||
ArchivedResource::AddressBook(archive) => archive.inner.created.to_native(),
|
||||
ArchivedResource::ContactCard(archive) => archive.inner.created.to_native(),
|
||||
ArchivedResource::FileNode(archive) => archive.inner.created.to_native(),
|
||||
ArchivedResource::CalendarEventNotification(archive) => {
|
||||
archive.inner.created.to_native()
|
||||
}
|
||||
ArchivedResource::CalendarEventNotificationCollection(_) => 1634515200,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn modified(&self) -> i64 {
|
||||
match self {
|
||||
ArchivedResource::Calendar(archive) => archive.inner.modified.to_native(),
|
||||
ArchivedResource::CalendarEvent(archive) => archive.inner.modified.to_native(),
|
||||
ArchivedResource::AddressBook(archive) => archive.inner.modified.to_native(),
|
||||
ArchivedResource::ContactCard(archive) => archive.inner.modified.to_native(),
|
||||
ArchivedResource::FileNode(archive) => archive.inner.modified.to_native(),
|
||||
ArchivedResource::CalendarEventNotification(archive) => {
|
||||
archive.inner.modified.to_native()
|
||||
}
|
||||
ArchivedResource::CalendarEventNotificationCollection(_) => 1634515200,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dead_properties(&self) -> Option<&ArchivedDeadProperty> {
|
||||
match self {
|
||||
ArchivedResource::Calendar(archive) => Some(&archive.inner.dead_properties),
|
||||
ArchivedResource::CalendarEvent(archive) => Some(&archive.inner.dead_properties),
|
||||
ArchivedResource::AddressBook(archive) => Some(&archive.inner.dead_properties),
|
||||
ArchivedResource::ContactCard(archive) => Some(&archive.inner.dead_properties),
|
||||
ArchivedResource::FileNode(archive) => Some(&archive.inner.dead_properties),
|
||||
ArchivedResource::CalendarEventNotification(_)
|
||||
| ArchivedResource::CalendarEventNotificationCollection(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
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::CalendarEventNotification(archive) => {
|
||||
archive.inner.size.to_native().into()
|
||||
}
|
||||
ArchivedResource::ContactCard(archive) => archive.inner.size.to_native().into(),
|
||||
ArchivedResource::AddressBook(_)
|
||||
| ArchivedResource::Calendar(_)
|
||||
| ArchivedResource::CalendarEventNotificationCollection(_) => 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(_) | ArchivedResource::CalendarEventNotification(_) => {
|
||||
"text/calendar".into()
|
||||
}
|
||||
ArchivedResource::ContactCard(_) => "text/vcard".into(),
|
||||
ArchivedResource::AddressBook(_)
|
||||
| ArchivedResource::Calendar(_)
|
||||
| ArchivedResource::CalendarEventNotificationCollection(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn display_name(&self, account_id: u32) -> Option<&str> {
|
||||
match self {
|
||||
ArchivedResource::Calendar(archive) => {
|
||||
Some(archive.inner.preferences(account_id).name.as_str())
|
||||
}
|
||||
ArchivedResource::CalendarEvent(archive) => archive.inner.display_name.as_deref(),
|
||||
ArchivedResource::AddressBook(archive) => {
|
||||
Some(archive.inner.preferences(account_id).name.as_str())
|
||||
}
|
||||
ArchivedResource::ContactCard(archive) => archive.inner.display_name.as_deref(),
|
||||
ArchivedResource::FileNode(archive) => archive.inner.display_name.as_deref(),
|
||||
ArchivedResource::CalendarEventNotification(_)
|
||||
| ArchivedResource::CalendarEventNotificationCollection(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
ArchivedResource::CalendarEventNotificationCollection(_) => vec![
|
||||
ReportSet::SyncCollection,
|
||||
ReportSet::CalendarQuery,
|
||||
ReportSet::CalendarMultiGet,
|
||||
]
|
||||
.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()
|
||||
}
|
||||
ArchivedResource::CalendarEventNotificationCollection(true) => {
|
||||
vec![ResourceType::Collection, ResourceType::ScheduleInbox].into()
|
||||
}
|
||||
ArchivedResource::CalendarEventNotificationCollection(false) => {
|
||||
vec![ResourceType::Collection, ResourceType::ScheduleOutbox].into()
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SyncType {
|
||||
pub fn is_none(&self) -> bool {
|
||||
matches!(self, SyncType::None)
|
||||
}
|
||||
|
||||
pub fn is_none_or_initial(&self) -> bool {
|
||||
matches!(self, SyncType::None | SyncType::Initial)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,236 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{DavError, DavResourceName};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use groupware::cache::GroupwareCache;
|
||||
use http_proto::request::decode_path_element;
|
||||
use hyper::StatusCode;
|
||||
use std::fmt::Display;
|
||||
use trc::AddContext;
|
||||
use types::collection::Collection;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct UriResource<A, R> {
|
||||
pub collection: Collection,
|
||||
pub account_id: A,
|
||||
pub resource: R,
|
||||
}
|
||||
|
||||
pub(crate) enum Urn {
|
||||
Lock(u64),
|
||||
Sync { id: u64, seq: u32 },
|
||||
}
|
||||
|
||||
pub(crate) type UnresolvedUri<'x> = UriResource<Option<u32>, Option<&'x str>>;
|
||||
pub(crate) type OwnedUri<'x> = UriResource<u32, Option<&'x str>>;
|
||||
pub(crate) type DocumentUri = UriResource<u32, u32>;
|
||||
|
||||
pub(crate) trait DavUriResource: Sync + Send {
|
||||
fn validate_uri_with_status<'x>(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
uri: &'x str,
|
||||
error_status: StatusCode,
|
||||
) -> impl Future<Output = crate::Result<UnresolvedUri<'x>>> + Send;
|
||||
|
||||
fn validate_uri<'x>(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
uri: &'x str,
|
||||
) -> impl Future<Output = crate::Result<UnresolvedUri<'x>>> + Send;
|
||||
|
||||
fn map_uri_resource(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
uri: OwnedUri<'_>,
|
||||
) -> impl Future<Output = trc::Result<Option<DocumentUri>>> + Send;
|
||||
}
|
||||
|
||||
impl DavUriResource for Server {
|
||||
async fn validate_uri<'x>(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
uri: &'x str,
|
||||
) -> crate::Result<UnresolvedUri<'x>> {
|
||||
self.validate_uri_with_status(access_token, uri, StatusCode::NOT_FOUND)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn validate_uri_with_status<'x>(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
uri: &'x str,
|
||||
error_status: StatusCode,
|
||||
) -> crate::Result<UnresolvedUri<'x>> {
|
||||
let (_, uri_parts) = uri
|
||||
.split_once("/dav/")
|
||||
.ok_or(DavError::Code(error_status))?;
|
||||
|
||||
let mut uri_parts = uri_parts
|
||||
.trim_end_matches('/')
|
||||
.splitn(3, '/')
|
||||
.filter(|x| !x.is_empty());
|
||||
let mut resource = UriResource {
|
||||
collection: uri_parts
|
||||
.next()
|
||||
.and_then(DavResourceName::parse)
|
||||
.ok_or(DavError::Code(error_status))?
|
||||
.into(),
|
||||
account_id: None,
|
||||
resource: None,
|
||||
};
|
||||
if let Some(account) = uri_parts.next() {
|
||||
// Parse account id
|
||||
let account_id = if let Some(account_id) = account.strip_prefix('_') {
|
||||
account_id
|
||||
.parse::<u32>()
|
||||
.map_err(|_| DavError::Code(error_status))?
|
||||
} else {
|
||||
let account = decode_path_element(account);
|
||||
self.account_id_from_email(&account, false)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(error_status))?
|
||||
};
|
||||
|
||||
// Validate access
|
||||
if resource.collection != Collection::Principal
|
||||
&& !access_token.has_access(account_id, resource.collection)
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
// Obtain remaining path
|
||||
resource.account_id = Some(account_id);
|
||||
resource.resource = uri_parts.next();
|
||||
}
|
||||
|
||||
Ok(resource)
|
||||
}
|
||||
|
||||
async fn map_uri_resource(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
uri: OwnedUri<'_>,
|
||||
) -> trc::Result<Option<DocumentUri>> {
|
||||
if let Some(resource) = uri.resource {
|
||||
if let Some(resource) = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
uri.account_id,
|
||||
uri.collection.into(),
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.by_path(resource)
|
||||
{
|
||||
Ok(Some(DocumentUri {
|
||||
collection: if resource.is_container() {
|
||||
uri.collection
|
||||
} else {
|
||||
uri.collection.child_collection().unwrap_or(uri.collection)
|
||||
},
|
||||
account_id: uri.account_id,
|
||||
resource: resource.document_id(),
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> UnresolvedUri<'x> {
|
||||
pub fn into_owned_uri(self) -> crate::Result<OwnedUri<'x>> {
|
||||
Ok(OwnedUri {
|
||||
collection: self.collection,
|
||||
account_id: self
|
||||
.account_id
|
||||
.ok_or(DavError::Code(StatusCode::FORBIDDEN))?,
|
||||
resource: self.resource,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl OwnedUri<'_> {
|
||||
pub fn new_owned(
|
||||
collection: Collection,
|
||||
account_id: u32,
|
||||
resource: Option<&str>,
|
||||
) -> OwnedUri<'_> {
|
||||
OwnedUri {
|
||||
collection,
|
||||
account_id,
|
||||
resource,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*impl<A, R> UriResource<A, R> {
|
||||
pub fn collection_path(&self) -> &'static str {
|
||||
DavResourceName::from(self.collection).collection_path()
|
||||
}
|
||||
}*/
|
||||
|
||||
impl Urn {
|
||||
pub fn try_extract_sync_id(token: &str) -> Option<&str> {
|
||||
token
|
||||
.strip_prefix("urn:stalwart:davsync:")
|
||||
.map(|x| x.split_once(':').map(|(x, _)| x).unwrap_or(x))
|
||||
}
|
||||
|
||||
pub fn parse(input: &str) -> Option<Self> {
|
||||
let inbox = input.strip_prefix("urn:stalwart:")?;
|
||||
let (kind, id) = inbox.split_once(':')?;
|
||||
match kind {
|
||||
"davlock" => u64::from_str_radix(id, 16).ok().map(Urn::Lock),
|
||||
"davsync" => {
|
||||
if let Some((id, seq)) = id.split_once(':') {
|
||||
let id = u64::from_str_radix(id, 16).ok()?;
|
||||
let seq = u32::from_str_radix(seq, 16).ok()?;
|
||||
Some(Urn::Sync { id, seq })
|
||||
} else {
|
||||
u64::from_str_radix(id, 16)
|
||||
.ok()
|
||||
.map(|id| Urn::Sync { id, seq: 0 })
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_unwrap_lock(&self) -> Option<u64> {
|
||||
match self {
|
||||
Urn::Lock(id) => Some(*id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_unwrap_sync(&self) -> Option<(u64, u32)> {
|
||||
match self {
|
||||
Urn::Sync { id, seq } => Some((*id, *seq)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Urn {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Urn::Lock(id) => write!(f, "urn:stalwart:davlock:{id:x}",),
|
||||
Urn::Sync { id, seq } => {
|
||||
if *seq == 0 {
|
||||
write!(f, "urn:stalwart:davsync:{id:x}")
|
||||
} else {
|
||||
write!(f, "urn:stalwart:davsync:{id:x}:{seq:x}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,907 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::FromDavResource;
|
||||
use crate::{
|
||||
DavError, DavMethod,
|
||||
common::{
|
||||
ExtractETag,
|
||||
lock::{LockRequestHandler, ResourceState},
|
||||
uri::{DavUriResource, UriResource},
|
||||
},
|
||||
file::{DavFileResource, FileItemId},
|
||||
};
|
||||
use common::{
|
||||
DavResourcePath, DavResources, Server, auth::AccessToken, storage::index::ObjectIndexBuilder,
|
||||
};
|
||||
use dav_proto::{Depth, RequestHeaders};
|
||||
use groupware::{DestroyArchive, cache::GroupwareCache, file::FileNode};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use std::sync::Arc;
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{AlignedBytes, Archive},
|
||||
};
|
||||
use store::{
|
||||
ahash::AHashMap,
|
||||
write::{BatchBuilder, now},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::Acl,
|
||||
collection::{Collection, SyncCollection, VanishedCollection},
|
||||
};
|
||||
|
||||
pub(crate) trait FileCopyMoveRequestHandler: Sync + Send {
|
||||
fn handle_file_copy_move_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
is_move: bool,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
}
|
||||
|
||||
impl FileCopyMoveRequestHandler for Server {
|
||||
async fn handle_file_copy_move_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
is_move: bool,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
// Validate source
|
||||
let from_resource_ = self
|
||||
.validate_uri(access_token, headers.uri)
|
||||
.await?
|
||||
.into_owned_uri()?;
|
||||
let from_account_id = from_resource_.account_id;
|
||||
let from_resources = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
from_account_id,
|
||||
SyncCollection::FileNode,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let from_resource = from_resources.map_resource::<FileItemId>(&from_resource_)?;
|
||||
let from_resource_name = from_resource_.resource.unwrap();
|
||||
|
||||
// Validate source ACLs
|
||||
if !access_token.is_member(from_account_id) {
|
||||
let shared = from_resources.shared_containers(
|
||||
access_token,
|
||||
if is_move {
|
||||
[Acl::Read, Acl::Delete].as_slice().iter().copied()
|
||||
} else {
|
||||
[Acl::Read].as_slice().iter().copied()
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
for resource in from_resources.subtree(from_resource_.resource.unwrap()) {
|
||||
if !shared.contains(resource.document_id()) {
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate destination
|
||||
let destination = self
|
||||
.validate_uri_with_status(
|
||||
access_token,
|
||||
headers
|
||||
.destination
|
||||
.ok_or(DavError::Code(StatusCode::BAD_GATEWAY))?,
|
||||
StatusCode::BAD_GATEWAY,
|
||||
)
|
||||
.await?;
|
||||
if destination.collection != Collection::FileNode {
|
||||
return Err(DavError::Code(StatusCode::BAD_GATEWAY));
|
||||
}
|
||||
let to_account_id = destination
|
||||
.account_id
|
||||
.ok_or(DavError::Code(StatusCode::BAD_GATEWAY))?;
|
||||
let to_resources = if to_account_id == from_account_id {
|
||||
from_resources.clone()
|
||||
} else {
|
||||
self.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
to_account_id,
|
||||
SyncCollection::FileNode,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
};
|
||||
|
||||
// Map file item
|
||||
let destination_resource_name = destination
|
||||
.resource
|
||||
.ok_or(DavError::Code(StatusCode::BAD_GATEWAY))?;
|
||||
if from_account_id == to_account_id
|
||||
&& (from_resource_name == destination_resource_name
|
||||
|| from_resource_name
|
||||
.strip_prefix(destination_resource_name)
|
||||
.is_some_and(|v| v.is_empty() || v.starts_with('/')))
|
||||
{
|
||||
return Ok(HttpResponse::new(StatusCode::BAD_GATEWAY));
|
||||
}
|
||||
|
||||
// Check if the resource exists
|
||||
let mut delete_destination = None;
|
||||
let mut destination = if let Some((destination, new_name)) =
|
||||
to_resources.map_parent(destination_resource_name)
|
||||
{
|
||||
if let Some(mut existing_destination) = to_resources
|
||||
.by_path(destination_resource_name)
|
||||
.map(Destination::from_dav_resource)
|
||||
{
|
||||
if !headers.overwrite_fail {
|
||||
existing_destination.account_id = to_account_id;
|
||||
delete_destination = Some(existing_destination);
|
||||
} else {
|
||||
return Ok(HttpResponse::new(StatusCode::PRECONDITION_FAILED));
|
||||
}
|
||||
}
|
||||
|
||||
let mut destination = destination
|
||||
.map(Destination::from_dav_resource)
|
||||
.unwrap_or_default();
|
||||
destination.new_name = Some(new_name.to_string());
|
||||
destination
|
||||
} else {
|
||||
return Err(DavError::Code(StatusCode::CONFLICT));
|
||||
};
|
||||
destination.account_id = to_account_id;
|
||||
|
||||
// Validate destination ACLs
|
||||
if let Some(document_id) = destination.document_id {
|
||||
if let Some(delete_destination) = &delete_destination
|
||||
&& !access_token.is_member(to_account_id)
|
||||
&& !to_resources.has_access_to_container(
|
||||
access_token,
|
||||
delete_destination.document_id.unwrap(),
|
||||
Acl::Delete,
|
||||
)
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
if !access_token.is_member(to_account_id)
|
||||
&& !to_resources.has_access_to_container(access_token, document_id, Acl::Modify)
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
} else if !access_token.is_member(to_account_id) {
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
// Validate headers
|
||||
self.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![
|
||||
ResourceState {
|
||||
account_id: from_account_id,
|
||||
collection: Collection::FileNode,
|
||||
document_id: Some(from_resource.resource.document_id),
|
||||
path: from_resource_name,
|
||||
..Default::default()
|
||||
},
|
||||
ResourceState {
|
||||
account_id: to_account_id,
|
||||
collection: Collection::FileNode,
|
||||
document_id: Some(
|
||||
delete_destination
|
||||
.as_ref()
|
||||
.and_then(|d| d.document_id)
|
||||
.unwrap_or(u32::MAX),
|
||||
),
|
||||
path: destination_resource_name,
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
Default::default(),
|
||||
if is_move {
|
||||
DavMethod::MOVE
|
||||
} else {
|
||||
DavMethod::COPY
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
if delete_destination.is_none()
|
||||
&& from_account_id == destination.account_id
|
||||
&& from_resource.resource.parent_id == destination.document_id
|
||||
&& destination.new_name.is_some()
|
||||
&& is_move
|
||||
{
|
||||
// Rename
|
||||
let from_resource_path = if from_resource.resource.is_container {
|
||||
from_resources.format_collection(from_resource_name)
|
||||
} else {
|
||||
from_resources.format_item(from_resource_name)
|
||||
};
|
||||
return rename_item(
|
||||
self,
|
||||
access_token,
|
||||
from_resource,
|
||||
from_resource_path,
|
||||
destination,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// Validate quota
|
||||
if !is_move || from_account_id != to_account_id {
|
||||
let space_needed = from_resources
|
||||
.subtree(from_resource_name)
|
||||
.map(|a| a.size() as u64)
|
||||
.sum::<u64>();
|
||||
self.has_available_quota(self.account(to_account_id).await?.as_ref(), space_needed)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Delete collection
|
||||
let is_overwrite = delete_destination
|
||||
.as_ref()
|
||||
.is_some_and(|d| d.is_container || from_resource.resource.is_container);
|
||||
if is_overwrite {
|
||||
delete_destination = None;
|
||||
// Find ids to delete
|
||||
let mut ids = to_resources
|
||||
.subtree(destination_resource_name)
|
||||
.collect::<Vec<_>>();
|
||||
if !ids.is_empty() {
|
||||
ids.sort_unstable_by_key(|b| std::cmp::Reverse(b.hierarchy_seq()));
|
||||
let mut sorted_ids = Vec::with_capacity(ids.len());
|
||||
sorted_ids.extend(ids.into_iter().map(|a| a.document_id()));
|
||||
DestroyArchive(sorted_ids)
|
||||
.delete(
|
||||
self,
|
||||
access_token.account_tenant_ids(),
|
||||
destination.account_id,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
}
|
||||
|
||||
match (from_resource.resource.is_container, is_move) {
|
||||
(true, true) => {
|
||||
move_container(
|
||||
self,
|
||||
access_token,
|
||||
from_resources,
|
||||
from_resource,
|
||||
from_resource_name,
|
||||
destination,
|
||||
headers.depth,
|
||||
)
|
||||
.await
|
||||
}
|
||||
(true, false) => {
|
||||
copy_container(
|
||||
self,
|
||||
access_token,
|
||||
from_resources,
|
||||
from_resource,
|
||||
from_resource_name,
|
||||
destination,
|
||||
headers.depth,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
}
|
||||
(false, true) => {
|
||||
if let Some(delete_destination) = delete_destination {
|
||||
overwrite_and_delete_item(
|
||||
self,
|
||||
access_token,
|
||||
from_resource,
|
||||
from_resources.format_item(from_resource_name),
|
||||
delete_destination,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
move_item(
|
||||
self,
|
||||
access_token,
|
||||
from_resource,
|
||||
from_resources.format_item(from_resource_name),
|
||||
destination,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
(false, false) => {
|
||||
if let Some(delete_destination) = delete_destination {
|
||||
overwrite_item(self, access_token, from_resource, delete_destination).await
|
||||
} else {
|
||||
copy_item(self, access_token, from_resource, destination).await
|
||||
}
|
||||
}
|
||||
}
|
||||
.map(|r| {
|
||||
if is_overwrite && r.status() == StatusCode::CREATED {
|
||||
r.with_status_code(StatusCode::NO_CONTENT)
|
||||
} else {
|
||||
r
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Destination {
|
||||
pub account_id: u32,
|
||||
pub new_name: Option<String>,
|
||||
pub document_id: Option<u32>,
|
||||
pub is_container: bool,
|
||||
}
|
||||
|
||||
impl Default for Destination {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
account_id: Default::default(),
|
||||
document_id: Default::default(),
|
||||
new_name: Default::default(),
|
||||
is_container: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Moves a container under an existing container
|
||||
async fn move_container(
|
||||
server: &Server,
|
||||
access_token: &AccessToken,
|
||||
from_resources: Arc<DavResources>,
|
||||
from_resource: UriResource<u32, FileItemId>,
|
||||
from_resource_name: &str,
|
||||
destination: Destination,
|
||||
depth: Depth,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
let from_account_id = from_resource.account_id;
|
||||
let to_account_id = destination.account_id;
|
||||
let from_document_id = from_resource.resource.document_id;
|
||||
let parent_id = destination.document_id.map(|id| id + 1).unwrap_or(0);
|
||||
|
||||
if from_account_id == to_account_id {
|
||||
let node_ = server
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
from_account_id,
|
||||
Collection::FileNode,
|
||||
from_document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
let node = node_
|
||||
.to_unarchived::<FileNode>()
|
||||
.caused_by(trc::location!())?;
|
||||
let mut new_node = node.deserialize::<FileNode>().caused_by(trc::location!())?;
|
||||
new_node.parent_id = parent_id;
|
||||
if let Some(new_name) = destination.new_name {
|
||||
new_node.name = new_name;
|
||||
}
|
||||
let mut batch = BatchBuilder::new();
|
||||
let etag = new_node
|
||||
.update(
|
||||
access_token.account_tenant_ids(),
|
||||
node,
|
||||
from_account_id,
|
||||
from_document_id,
|
||||
true,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.etag();
|
||||
batch.with_account_id(from_account_id).log_vanished_item(
|
||||
VanishedCollection::FileNode,
|
||||
from_resources.format_collection(from_resource_name),
|
||||
);
|
||||
server
|
||||
.commit_batch(batch)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag))
|
||||
} else {
|
||||
copy_container(
|
||||
server,
|
||||
access_token,
|
||||
from_resources,
|
||||
from_resource,
|
||||
from_resource_name,
|
||||
destination,
|
||||
depth,
|
||||
true,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn copy_container(
|
||||
server: &Server,
|
||||
access_token: &AccessToken,
|
||||
from_resources: Arc<DavResources>,
|
||||
from_resource: UriResource<u32, FileItemId>,
|
||||
from_resource_name: &str,
|
||||
mut destination: Destination,
|
||||
depth: Depth,
|
||||
delete_source: bool,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
let infinity_copy = match depth {
|
||||
Depth::Zero if !delete_source => {
|
||||
return copy_item(server, access_token, from_resource, destination).await;
|
||||
}
|
||||
Depth::One if !delete_source => false,
|
||||
_ => true,
|
||||
};
|
||||
|
||||
let from_account_id = from_resource.account_id;
|
||||
let to_account_id = destination.account_id;
|
||||
let parent_id = destination.document_id.map(|id| id + 1).unwrap_or(0);
|
||||
|
||||
// Obtain files to copy
|
||||
let mut copy_files = if infinity_copy {
|
||||
from_resources
|
||||
.subtree(from_resource_name)
|
||||
.map(|r| (r.document_id(), r.hierarchy_seq()))
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
from_resources
|
||||
.subtree_with_depth(from_resource_name, 1)
|
||||
.map(|r| (r.document_id(), r.hierarchy_seq()))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
// Top-down copy
|
||||
let mut batch = BatchBuilder::new();
|
||||
let mut id_map = AHashMap::with_capacity(copy_files.len());
|
||||
let mut delete_files = if delete_source {
|
||||
Vec::with_capacity(copy_files.len())
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
copy_files.sort_unstable_by_key(|a| a.1);
|
||||
let now = now() as i64;
|
||||
let mut next_document_id = server
|
||||
.store()
|
||||
.assign_document_ids(to_account_id, Collection::FileNode, copy_files.len() as u64)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
for (document_id, _) in copy_files.into_iter() {
|
||||
let node_ = server
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
from_account_id,
|
||||
Collection::FileNode,
|
||||
document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?
|
||||
.into_deserialized::<FileNode>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Build node
|
||||
let mut node = if !delete_source {
|
||||
node_.inner
|
||||
} else {
|
||||
let node = node_.inner.clone();
|
||||
delete_files.push((document_id, node_));
|
||||
node
|
||||
};
|
||||
node.modified = now;
|
||||
node.created = now;
|
||||
if let Some(new_name) = destination.new_name.take() {
|
||||
node.name = new_name;
|
||||
}
|
||||
node.parent_id = if let Some(&prev_document_id) = id_map.get(&node.parent_id) {
|
||||
prev_document_id
|
||||
} else {
|
||||
parent_id
|
||||
};
|
||||
|
||||
// Prepare write batch
|
||||
let new_document_id = next_document_id;
|
||||
next_document_id -= 1;
|
||||
batch
|
||||
.with_account_id(to_account_id)
|
||||
.with_collection(Collection::FileNode)
|
||||
.with_document(new_document_id)
|
||||
.custom(
|
||||
ObjectIndexBuilder::<(), _>::new()
|
||||
.with_changes(node)
|
||||
.with_changed_by(access_token.account_tenant_ids()),
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.commit_point();
|
||||
id_map.insert(document_id + 1, new_document_id + 1);
|
||||
}
|
||||
|
||||
// Delete nodes
|
||||
if !delete_files.is_empty() {
|
||||
for (document_id, node) in delete_files.into_iter().rev() {
|
||||
// Delete record
|
||||
batch
|
||||
.with_account_id(from_account_id)
|
||||
.with_collection(Collection::FileNode)
|
||||
.with_document(document_id)
|
||||
.custom(
|
||||
ObjectIndexBuilder::<_, ()>::new()
|
||||
.with_changed_by(access_token.account_tenant_ids())
|
||||
.with_current(node),
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.commit_point();
|
||||
}
|
||||
batch.with_account_id(from_account_id).log_vanished_item(
|
||||
VanishedCollection::FileNode,
|
||||
from_resources.format_collection(from_resource_name),
|
||||
);
|
||||
}
|
||||
|
||||
// Write changes
|
||||
if !batch.is_empty() {
|
||||
server
|
||||
.commit_batch(batch)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::CREATED))
|
||||
}
|
||||
|
||||
// Overwrites the contents of one file with another, then deletes the original
|
||||
async fn overwrite_and_delete_item(
|
||||
server: &Server,
|
||||
access_token: &AccessToken,
|
||||
from_resource: UriResource<u32, FileItemId>,
|
||||
from_resource_path: String,
|
||||
destination: Destination,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
let from_account_id = from_resource.account_id;
|
||||
let to_account_id = destination.account_id;
|
||||
let from_document_id = from_resource.resource.document_id;
|
||||
let to_document_id = destination.document_id.unwrap();
|
||||
|
||||
// dest_node is the current file at the destination
|
||||
let dest_node_ = server
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
to_account_id,
|
||||
Collection::FileNode,
|
||||
to_document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
|
||||
let dest_node = dest_node_
|
||||
.to_unarchived::<FileNode>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// source_node is the file to be copied
|
||||
let source_node__ = server
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
from_account_id,
|
||||
Collection::FileNode,
|
||||
from_document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
let source_node_ = source_node__
|
||||
.to_unarchived::<FileNode>()
|
||||
.caused_by(trc::location!())?;
|
||||
let mut source_node = source_node_
|
||||
.deserialize::<FileNode>()
|
||||
.caused_by(trc::location!())?;
|
||||
source_node.name = if let Some(new_name) = destination.new_name {
|
||||
new_name
|
||||
} else {
|
||||
dest_node.inner.name.to_string()
|
||||
};
|
||||
source_node.parent_id = dest_node.inner.parent_id.into();
|
||||
|
||||
let mut batch = BatchBuilder::new();
|
||||
let etag = source_node
|
||||
.update(
|
||||
access_token.account_tenant_ids(),
|
||||
dest_node,
|
||||
to_account_id,
|
||||
to_document_id,
|
||||
true,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.etag();
|
||||
DestroyArchive(source_node_)
|
||||
.delete(
|
||||
access_token.account_tenant_ids(),
|
||||
from_account_id,
|
||||
from_document_id,
|
||||
&mut batch,
|
||||
from_resource_path,
|
||||
)
|
||||
.caused_by(trc::location!())?;
|
||||
server
|
||||
.commit_batch(batch)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::NO_CONTENT).with_etag_opt(etag))
|
||||
}
|
||||
|
||||
// Overwrites the contents of one file with another
|
||||
async fn overwrite_item(
|
||||
server: &Server,
|
||||
access_token: &AccessToken,
|
||||
from_resource: UriResource<u32, FileItemId>,
|
||||
destination: Destination,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
let from_account_id = from_resource.account_id;
|
||||
let to_account_id = destination.account_id;
|
||||
let from_document_id = from_resource.resource.document_id;
|
||||
let to_document_id = destination.document_id.unwrap();
|
||||
|
||||
// dest_node is the current file at the destination
|
||||
let dest_node_ = server
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
to_account_id,
|
||||
Collection::FileNode,
|
||||
to_document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
|
||||
let dest_node = dest_node_
|
||||
.to_unarchived::<FileNode>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// source_node is the file to be copied
|
||||
let mut source_node = server
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
from_account_id,
|
||||
Collection::FileNode,
|
||||
from_document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?
|
||||
.deserialize::<FileNode>()
|
||||
.caused_by(trc::location!())?;
|
||||
source_node.name = if let Some(new_name) = destination.new_name {
|
||||
new_name
|
||||
} else {
|
||||
dest_node.inner.name.to_string()
|
||||
};
|
||||
source_node.parent_id = dest_node.inner.parent_id.into();
|
||||
let mut batch = BatchBuilder::new();
|
||||
let etag = source_node
|
||||
.update(
|
||||
access_token.account_tenant_ids(),
|
||||
dest_node,
|
||||
to_account_id,
|
||||
to_document_id,
|
||||
true,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.etag();
|
||||
server
|
||||
.commit_batch(batch)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::NO_CONTENT).with_etag_opt(etag))
|
||||
}
|
||||
|
||||
// Moves an item under an existing container
|
||||
async fn move_item(
|
||||
server: &Server,
|
||||
access_token: &AccessToken,
|
||||
from_resource: UriResource<u32, FileItemId>,
|
||||
from_resource_path: String,
|
||||
destination: Destination,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
let from_account_id = from_resource.account_id;
|
||||
let to_account_id = destination.account_id;
|
||||
let from_document_id = from_resource.resource.document_id;
|
||||
let parent_id = destination.document_id.map(|id| id + 1).unwrap_or(0);
|
||||
|
||||
let node_ = server
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
from_account_id,
|
||||
Collection::FileNode,
|
||||
from_document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
let node = node_
|
||||
.to_unarchived::<FileNode>()
|
||||
.caused_by(trc::location!())?;
|
||||
let mut new_node = node.deserialize::<FileNode>().caused_by(trc::location!())?;
|
||||
new_node.parent_id = parent_id;
|
||||
if let Some(new_name) = destination.new_name {
|
||||
new_node.name = new_name;
|
||||
}
|
||||
|
||||
let mut batch = BatchBuilder::new();
|
||||
let etag = if from_account_id == to_account_id {
|
||||
// Destination is in the same account: just update the parent id
|
||||
batch.log_vanished_item(VanishedCollection::FileNode, from_resource_path);
|
||||
new_node
|
||||
.update(
|
||||
access_token.account_tenant_ids(),
|
||||
node,
|
||||
from_account_id,
|
||||
from_document_id,
|
||||
true,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.etag()
|
||||
} else {
|
||||
// Destination is in a different account: insert a new node, then delete the old one
|
||||
let to_document_id = server
|
||||
.store()
|
||||
.assign_document_ids(to_account_id, Collection::FileNode, 1)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let etag = new_node
|
||||
.insert(
|
||||
access_token.account_tenant_ids(),
|
||||
to_account_id,
|
||||
to_document_id,
|
||||
true,
|
||||
true,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.etag();
|
||||
DestroyArchive(node)
|
||||
.delete(
|
||||
access_token.account_tenant_ids(),
|
||||
from_account_id,
|
||||
from_document_id,
|
||||
&mut batch,
|
||||
from_resource_path,
|
||||
)
|
||||
.caused_by(trc::location!())?;
|
||||
etag
|
||||
};
|
||||
server
|
||||
.commit_batch(batch)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag))
|
||||
}
|
||||
|
||||
// Copies an item under an existing container
|
||||
async fn copy_item(
|
||||
server: &Server,
|
||||
access_token: &AccessToken,
|
||||
from_resource: UriResource<u32, FileItemId>,
|
||||
destination: Destination,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
let from_account_id = from_resource.account_id;
|
||||
let to_account_id = destination.account_id;
|
||||
let from_document_id = from_resource.resource.document_id;
|
||||
let parent_id = destination.document_id.map(|id| id + 1).unwrap_or(0);
|
||||
|
||||
let mut node = server
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
from_account_id,
|
||||
Collection::FileNode,
|
||||
from_document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?
|
||||
.deserialize::<FileNode>()
|
||||
.caused_by(trc::location!())?;
|
||||
node.parent_id = parent_id;
|
||||
if let Some(new_name) = destination.new_name {
|
||||
node.name = new_name;
|
||||
}
|
||||
let mut batch = BatchBuilder::new();
|
||||
let to_document_id = server
|
||||
.store()
|
||||
.assign_document_ids(to_account_id, Collection::FileNode, 1)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let etag = node
|
||||
.insert(
|
||||
access_token.account_tenant_ids(),
|
||||
to_account_id,
|
||||
to_document_id,
|
||||
true,
|
||||
true,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.etag();
|
||||
server
|
||||
.commit_batch(batch)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag))
|
||||
}
|
||||
|
||||
// Renames an item
|
||||
async fn rename_item(
|
||||
server: &Server,
|
||||
access_token: &AccessToken,
|
||||
from_resource: UriResource<u32, FileItemId>,
|
||||
from_resource_path: String,
|
||||
destination: Destination,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
let from_account_id = from_resource.account_id;
|
||||
let from_document_id = from_resource.resource.document_id;
|
||||
|
||||
let node_ = server
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
from_account_id,
|
||||
Collection::FileNode,
|
||||
from_document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
let node = node_
|
||||
.to_unarchived::<FileNode>()
|
||||
.caused_by(trc::location!())?;
|
||||
let mut new_node = node.deserialize::<FileNode>().caused_by(trc::location!())?;
|
||||
if let Some(new_name) = destination.new_name {
|
||||
new_node.name = new_name;
|
||||
}
|
||||
let mut batch = BatchBuilder::new();
|
||||
let etag = new_node
|
||||
.update(
|
||||
access_token.account_tenant_ids(),
|
||||
node,
|
||||
from_account_id,
|
||||
from_document_id,
|
||||
true,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.etag();
|
||||
batch.log_vanished_item(VanishedCollection::FileNode, from_resource_path);
|
||||
server
|
||||
.commit_batch(batch)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag))
|
||||
}
|
||||
|
||||
impl FromDavResource for Destination {
|
||||
fn from_dav_resource(item: DavResourcePath<'_>) -> Self {
|
||||
Destination {
|
||||
account_id: u32::MAX,
|
||||
document_id: Some(item.document_id()),
|
||||
is_container: item.is_container(),
|
||||
new_name: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
DavError, DavMethod,
|
||||
common::{
|
||||
lock::{LockRequestHandler, ResourceState},
|
||||
uri::DavUriResource,
|
||||
},
|
||||
};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use dav_proto::RequestHeaders;
|
||||
use groupware::{DestroyArchive, cache::GroupwareCache};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use trc::AddContext;
|
||||
use types::{acl::Acl, collection::SyncCollection};
|
||||
|
||||
pub(crate) trait FileDeleteRequestHandler: Sync + Send {
|
||||
fn handle_file_delete_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
}
|
||||
|
||||
impl FileDeleteRequestHandler for Server {
|
||||
async fn handle_file_delete_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
// Validate URI
|
||||
let resource = self
|
||||
.validate_uri(access_token, headers.uri)
|
||||
.await?
|
||||
.into_owned_uri()?;
|
||||
let account_id = resource.account_id;
|
||||
let delete_path = resource
|
||||
.resource
|
||||
.filter(|r| !r.is_empty())
|
||||
.ok_or(DavError::Code(StatusCode::FORBIDDEN))?;
|
||||
let resources = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::FileNode,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Find ids to delete
|
||||
let mut ids = resources.subtree(delete_path).collect::<Vec<_>>();
|
||||
if ids.is_empty() {
|
||||
return Err(DavError::Code(StatusCode::NOT_FOUND));
|
||||
}
|
||||
|
||||
// Sort ids descending from the deepest to the root
|
||||
ids.sort_unstable_by_key(|b| std::cmp::Reverse(b.hierarchy_seq()));
|
||||
let (document_id, full_delete_path) = ids
|
||||
.last()
|
||||
.map(|a| (a.document_id(), resources.format_resource(*a)))
|
||||
.unwrap();
|
||||
let mut sorted_ids = Vec::with_capacity(ids.len());
|
||||
sorted_ids.extend(ids.into_iter().map(|a| a.document_id()));
|
||||
|
||||
// Validate ACLs
|
||||
if !access_token.is_member(account_id) {
|
||||
let permissions = resources.shared_containers(access_token, [Acl::Delete], false);
|
||||
if permissions.len() < sorted_ids.len() as u64
|
||||
|| !sorted_ids.iter().all(|id| permissions.contains(*id))
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
}
|
||||
|
||||
// Validate headers
|
||||
self.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![ResourceState {
|
||||
account_id,
|
||||
collection: resource.collection,
|
||||
document_id: document_id.into(),
|
||||
path: delete_path,
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::DELETE,
|
||||
)
|
||||
.await?;
|
||||
|
||||
DestroyArchive(sorted_ids)
|
||||
.delete(
|
||||
self,
|
||||
access_token.account_tenant_ids(),
|
||||
account_id,
|
||||
full_delete_path.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::NO_CONTENT))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
DavError, DavMethod,
|
||||
common::{
|
||||
ETag,
|
||||
lock::{LockRequestHandler, ResourceState},
|
||||
uri::DavUriResource,
|
||||
},
|
||||
file::DavFileResource,
|
||||
};
|
||||
use common::{Server, auth::AccessToken, sharing::EffectiveAcl};
|
||||
use dav_proto::{RequestHeaders, schema::property::Rfc1123DateTime};
|
||||
use groupware::{cache::GroupwareCache, file::FileNode};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{AlignedBytes, Archive, now},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::Acl,
|
||||
collection::{Collection, SyncCollection},
|
||||
};
|
||||
|
||||
pub(crate) trait FileGetRequestHandler: Sync + Send {
|
||||
fn handle_file_get_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
is_head: bool,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
}
|
||||
|
||||
impl FileGetRequestHandler for Server {
|
||||
async fn handle_file_get_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
is_head: bool,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
// Validate URI
|
||||
let resource_ = self
|
||||
.validate_uri(access_token, headers.uri)
|
||||
.await?
|
||||
.into_owned_uri()?;
|
||||
let account_id = resource_.account_id;
|
||||
let files = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::FileNode,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let resource = files.map_resource(&resource_)?;
|
||||
|
||||
// Fetch node
|
||||
let node_ = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::FileNode,
|
||||
resource.resource,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
let node = node_.unarchive::<FileNode>().caused_by(trc::location!())?;
|
||||
|
||||
// Validate ACL
|
||||
if !access_token.is_member(account_id)
|
||||
&& !node.acls.effective_acl(access_token).contains(Acl::Read)
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
let (hash, size, content_type) = if let Some(file) = node.file.as_ref() {
|
||||
(
|
||||
file.blob_hash.0.as_ref(),
|
||||
u32::from(file.size) as usize,
|
||||
file.media_type.as_ref().map(|s| s.as_str()),
|
||||
)
|
||||
} else {
|
||||
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
|
||||
};
|
||||
|
||||
// Validate headers
|
||||
let etag = node_.etag();
|
||||
self.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![ResourceState {
|
||||
account_id,
|
||||
collection: resource.collection,
|
||||
document_id: resource.resource.into(),
|
||||
etag: etag.clone().into(),
|
||||
path: resource_.resource.unwrap(),
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::GET,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let modified = i64::from(node.modified);
|
||||
let last_modified = Rfc1123DateTime::new(modified).to_string();
|
||||
let byte_range = if !is_head && size > 0 {
|
||||
headers
|
||||
.range
|
||||
.filter(|_| {
|
||||
headers.eval_if_range(
|
||||
&etag,
|
||||
((modified as u64) < now()).then_some(last_modified.as_str()),
|
||||
)
|
||||
})
|
||||
.map(|range| range.resolve(size as u64))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let byte_range = match byte_range {
|
||||
Some(Some(range)) => Some(range.start as usize..range.end as usize),
|
||||
Some(None) => {
|
||||
return Ok(HttpResponse::new(StatusCode::RANGE_NOT_SATISFIABLE)
|
||||
.with_accept_ranges()
|
||||
.with_etag(etag)
|
||||
.with_content_range(format!("bytes */{size}")));
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
let response = HttpResponse::new(StatusCode::OK)
|
||||
.with_content_type(content_type.unwrap_or("application/octet-stream"))
|
||||
.with_etag(etag)
|
||||
.with_last_modified(last_modified)
|
||||
.with_accept_ranges();
|
||||
|
||||
if is_head {
|
||||
return Ok(response.with_content_length(size));
|
||||
}
|
||||
|
||||
let contents = self
|
||||
.blob_store()
|
||||
.get_blob(hash, byte_range.clone().unwrap_or(0..usize::MAX))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
|
||||
Ok(match byte_range {
|
||||
Some(byte_range) if !contents.is_empty() => response
|
||||
.with_status_code(StatusCode::PARTIAL_CONTENT)
|
||||
.with_content_range(format!(
|
||||
"bytes {}-{}/{}",
|
||||
byte_range.start,
|
||||
byte_range.start + contents.len() - 1,
|
||||
size
|
||||
)),
|
||||
Some(_) => return Err(DavError::Code(StatusCode::NOT_FOUND)),
|
||||
None => response,
|
||||
}
|
||||
.with_binary_body(contents))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::proppatch::FilePropPatchRequestHandler;
|
||||
use crate::{
|
||||
DavMethod, PropStatBuilder,
|
||||
common::{
|
||||
ExtractETag,
|
||||
acl::ResourceAcl,
|
||||
lock::{LockRequestHandler, ResourceState},
|
||||
uri::DavUriResource,
|
||||
},
|
||||
file::DavFileResource,
|
||||
};
|
||||
use common::{Server, auth::AccessToken, storage::index::ObjectIndexBuilder};
|
||||
use dav_proto::{
|
||||
RequestHeaders, Return,
|
||||
schema::{Namespace, request::MkCol, response::MkColResponse},
|
||||
};
|
||||
use groupware::{cache::GroupwareCache, file::FileNode};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use store::write::{BatchBuilder, now};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::Acl,
|
||||
collection::{Collection, SyncCollection},
|
||||
};
|
||||
|
||||
pub(crate) trait FileMkColRequestHandler: Sync + Send {
|
||||
fn handle_file_mkcol_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
request: Option<MkCol>,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
}
|
||||
|
||||
impl FileMkColRequestHandler for Server {
|
||||
async fn handle_file_mkcol_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
request: Option<MkCol>,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
// Validate URI
|
||||
let resource_ = self
|
||||
.validate_uri(access_token, headers.uri)
|
||||
.await?
|
||||
.into_owned_uri()?;
|
||||
let account_id = resource_.account_id;
|
||||
let resources = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::FileNode,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let resource = resources.map_parent_resource(&resource_)?;
|
||||
|
||||
// Validate and map parent ACL
|
||||
let parent_id = resources.validate_and_map_parent_acl(
|
||||
access_token,
|
||||
access_token.is_member(account_id),
|
||||
resource.resource.0,
|
||||
Acl::AddItems,
|
||||
)?;
|
||||
|
||||
// Validate headers
|
||||
self.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![ResourceState {
|
||||
account_id,
|
||||
collection: resource.collection,
|
||||
document_id: Some(u32::MAX),
|
||||
path: resource_.resource.unwrap(),
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::MKCOL,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Build file container
|
||||
let now = now();
|
||||
let mut node = FileNode {
|
||||
parent_id,
|
||||
name: resource.resource.1.to_string(),
|
||||
display_name: None,
|
||||
file: None,
|
||||
created: now as i64,
|
||||
modified: now as i64,
|
||||
dead_properties: Default::default(),
|
||||
acls: Default::default(),
|
||||
};
|
||||
|
||||
// Apply MKCOL properties
|
||||
let mut return_prop_stat = None;
|
||||
if let Some(mkcol) = request {
|
||||
let mut prop_stat = PropStatBuilder::default();
|
||||
if !self.apply_file_properties(&mut node, false, mkcol.props, &mut prop_stat) {
|
||||
return Ok(HttpResponse::new(StatusCode::FORBIDDEN).with_xml_body(
|
||||
MkColResponse::new(prop_stat.build())
|
||||
.with_namespace(Namespace::Dav)
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
if headers.ret != Return::Minimal {
|
||||
return_prop_stat = Some(prop_stat);
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare write batch
|
||||
let document_id = self
|
||||
.store()
|
||||
.assign_document_ids(account_id, Collection::FileNode, 1)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let mut batch = BatchBuilder::new();
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::FileNode)
|
||||
.with_document(document_id)
|
||||
.custom(ObjectIndexBuilder::<(), _>::new().with_changes(node))
|
||||
.caused_by(trc::location!())?;
|
||||
let etag = batch.etag();
|
||||
self.commit_batch(batch).await.caused_by(trc::location!())?;
|
||||
|
||||
if let Some(prop_stat) = return_prop_stat {
|
||||
Ok(HttpResponse::new(StatusCode::CREATED)
|
||||
.with_xml_body(
|
||||
MkColResponse::new(prop_stat.build())
|
||||
.with_namespace(Namespace::Dav)
|
||||
.to_string(),
|
||||
)
|
||||
.with_etag_opt(etag))
|
||||
} else {
|
||||
Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
DavError,
|
||||
common::uri::{OwnedUri, UriResource},
|
||||
};
|
||||
use common::{DavResourcePath, DavResources};
|
||||
use dav_proto::schema::property::{DavProperty, WebDavProperty};
|
||||
use hyper::StatusCode;
|
||||
|
||||
pub mod copy_move;
|
||||
pub mod delete;
|
||||
pub mod get;
|
||||
pub mod mkcol;
|
||||
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) trait FromDavResource {
|
||||
fn from_dav_resource(item: DavResourcePath<'_>) -> Self;
|
||||
}
|
||||
|
||||
pub(crate) struct FileItemId {
|
||||
pub document_id: u32,
|
||||
pub parent_id: Option<u32>,
|
||||
pub is_container: bool,
|
||||
}
|
||||
|
||||
pub(crate) trait DavFileResource {
|
||||
fn map_resource<T: FromDavResource>(
|
||||
&self,
|
||||
resource: &OwnedUri<'_>,
|
||||
) -> crate::Result<UriResource<u32, T>>;
|
||||
|
||||
fn map_parent<'x>(&self, resource: &'x str) -> Option<(Option<DavResourcePath<'_>>, &'x str)>;
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn map_parent_resource<'x, T: FromDavResource>(
|
||||
&self,
|
||||
resource: &OwnedUri<'x>,
|
||||
) -> crate::Result<UriResource<u32, (Option<T>, &'x str)>>;
|
||||
}
|
||||
|
||||
impl DavFileResource for DavResources {
|
||||
fn map_resource<T: FromDavResource>(
|
||||
&self,
|
||||
resource: &OwnedUri<'_>,
|
||||
) -> crate::Result<UriResource<u32, T>> {
|
||||
resource
|
||||
.resource
|
||||
.and_then(|r| self.by_path(r))
|
||||
.map(|r| UriResource {
|
||||
collection: resource.collection,
|
||||
account_id: resource.account_id,
|
||||
resource: T::from_dav_resource(r),
|
||||
})
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))
|
||||
}
|
||||
|
||||
fn map_parent<'x>(&self, resource: &'x str) -> Option<(Option<DavResourcePath<'_>>, &'x str)> {
|
||||
let (parent, child) = if let Some((parent, child)) = resource.rsplit_once('/') {
|
||||
(Some(self.by_path(parent)?), child)
|
||||
} else {
|
||||
(None, resource)
|
||||
};
|
||||
|
||||
Some((parent, child))
|
||||
}
|
||||
|
||||
fn map_parent_resource<'x, T: FromDavResource>(
|
||||
&self,
|
||||
resource: &OwnedUri<'x>,
|
||||
) -> crate::Result<UriResource<u32, (Option<T>, &'x str)>> {
|
||||
if let Some(r) = resource.resource {
|
||||
if self.by_path(r).is_none() {
|
||||
self.map_parent(r)
|
||||
.map(|(parent, child)| UriResource {
|
||||
collection: resource.collection,
|
||||
account_id: resource.account_id,
|
||||
resource: (parent.map(T::from_dav_resource), child),
|
||||
})
|
||||
.ok_or(DavError::Code(StatusCode::CONFLICT))
|
||||
} else {
|
||||
Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))
|
||||
}
|
||||
} else {
|
||||
Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromDavResource for u32 {
|
||||
fn from_dav_resource(item: DavResourcePath) -> Self {
|
||||
item.document_id()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromDavResource for FileItemId {
|
||||
fn from_dav_resource(item: DavResourcePath) -> Self {
|
||||
FileItemId {
|
||||
document_id: item.document_id(),
|
||||
parent_id: item.parent_id(),
|
||||
is_container: item.is_container(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
DavError, DavMethod, PropStatBuilder,
|
||||
common::{
|
||||
ETag, ExtractETag,
|
||||
lock::{LockRequestHandler, ResourceState},
|
||||
uri::DavUriResource,
|
||||
},
|
||||
file::DavFileResource,
|
||||
};
|
||||
use common::{Server, auth::AccessToken, sharing::EffectiveAcl};
|
||||
use dav_proto::{
|
||||
RequestHeaders, Return,
|
||||
schema::{
|
||||
property::{DavProperty, DavValue, ResourceType, WebDavProperty},
|
||||
request::{DavPropertyValue, PropertyUpdate},
|
||||
response::{BaseCondition, MultiStatus, Response},
|
||||
},
|
||||
};
|
||||
use groupware::{cache::GroupwareCache, file::FileNode};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use store::write::BatchBuilder;
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{AlignedBytes, Archive},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::Acl,
|
||||
collection::{Collection, SyncCollection},
|
||||
};
|
||||
|
||||
pub(crate) trait FilePropPatchRequestHandler: Sync + Send {
|
||||
fn handle_file_proppatch_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
request: PropertyUpdate,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
|
||||
fn apply_file_properties(
|
||||
&self,
|
||||
file: &mut FileNode,
|
||||
is_update: bool,
|
||||
properties: Vec<DavPropertyValue>,
|
||||
items: &mut PropStatBuilder,
|
||||
) -> bool;
|
||||
}
|
||||
|
||||
impl FilePropPatchRequestHandler for Server {
|
||||
async fn handle_file_proppatch_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
mut request: PropertyUpdate,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
// Validate URI
|
||||
let resource_ = self
|
||||
.validate_uri(access_token, headers.uri)
|
||||
.await?
|
||||
.into_owned_uri()?;
|
||||
let uri = headers.uri;
|
||||
let account_id = resource_.account_id;
|
||||
let files = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::FileNode,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let resource = files.map_resource(&resource_)?;
|
||||
|
||||
if !request.has_changes() {
|
||||
return Ok(HttpResponse::new(StatusCode::NO_CONTENT));
|
||||
}
|
||||
|
||||
// Fetch node
|
||||
let node_ = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::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::Modify)
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
// Validate headers
|
||||
self.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![ResourceState {
|
||||
account_id,
|
||||
collection: resource.collection,
|
||||
document_id: resource.resource.into(),
|
||||
etag: node_.etag().into(),
|
||||
path: resource_.resource.unwrap(),
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::PROPPATCH,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Deserialize
|
||||
let mut new_node = node.deserialize::<FileNode>().caused_by(trc::location!())?;
|
||||
|
||||
// Remove properties
|
||||
let mut items = PropStatBuilder::default();
|
||||
if !request.set_first && !request.remove.is_empty() {
|
||||
remove_file_properties(
|
||||
&mut new_node,
|
||||
std::mem::take(&mut request.remove),
|
||||
&mut items,
|
||||
);
|
||||
}
|
||||
|
||||
// Set properties
|
||||
let is_success = self.apply_file_properties(&mut new_node, true, request.set, &mut items);
|
||||
|
||||
// Remove properties
|
||||
if is_success && !request.remove.is_empty() {
|
||||
remove_file_properties(&mut new_node, request.remove, &mut items);
|
||||
}
|
||||
|
||||
let etag = if is_success {
|
||||
let mut batch = BatchBuilder::new();
|
||||
let etag = new_node
|
||||
.update(
|
||||
access_token.account_tenant_ids(),
|
||||
node,
|
||||
account_id,
|
||||
resource.resource,
|
||||
true,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.etag();
|
||||
self.commit_batch(batch).await.caused_by(trc::location!())?;
|
||||
etag
|
||||
} else {
|
||||
node_.etag().into()
|
||||
};
|
||||
|
||||
if headers.ret != Return::Minimal || !is_success {
|
||||
Ok(HttpResponse::new(StatusCode::MULTI_STATUS)
|
||||
.with_xml_body(
|
||||
MultiStatus::new(vec![Response::new_propstat(uri, items.build())]).to_string(),
|
||||
)
|
||||
.with_etag_opt(etag))
|
||||
} else {
|
||||
Ok(HttpResponse::new(StatusCode::NO_CONTENT).with_etag_opt(etag))
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_file_properties(
|
||||
&self,
|
||||
file: &mut FileNode,
|
||||
is_update: bool,
|
||||
properties: Vec<DavPropertyValue>,
|
||||
items: &mut PropStatBuilder,
|
||||
) -> bool {
|
||||
let mut has_errors = false;
|
||||
|
||||
for property in properties {
|
||||
match (&property.property, property.value) {
|
||||
(DavProperty::WebDav(WebDavProperty::DisplayName), DavValue::String(name)) => {
|
||||
if name.len() <= self.core.groupware.live_property_size {
|
||||
file.display_name = Some(name);
|
||||
items.insert_ok(property.property);
|
||||
} else {
|
||||
items.insert_error_with_description(
|
||||
property.property,
|
||||
StatusCode::INSUFFICIENT_STORAGE,
|
||||
"Property value is too long",
|
||||
);
|
||||
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
(DavProperty::WebDav(WebDavProperty::CreationDate), DavValue::Timestamp(dt)) => {
|
||||
file.created = dt;
|
||||
items.insert_ok(property.property);
|
||||
}
|
||||
(DavProperty::WebDav(WebDavProperty::GetContentType), DavValue::String(name))
|
||||
if file.file.is_some() =>
|
||||
{
|
||||
if name.len() <= self.core.groupware.live_property_size {
|
||||
file.file.as_mut().unwrap().media_type = Some(name);
|
||||
items.insert_ok(property.property);
|
||||
} else {
|
||||
items.insert_error_with_description(
|
||||
property.property,
|
||||
StatusCode::INSUFFICIENT_STORAGE,
|
||||
"Property value is too long",
|
||||
);
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
(
|
||||
DavProperty::WebDav(WebDavProperty::ResourceType),
|
||||
DavValue::ResourceTypes(types),
|
||||
) if file.file.is_none() => {
|
||||
if types.0.len() != 1 || types.0.first() != Some(&ResourceType::Collection) {
|
||||
items.insert_precondition_failed(
|
||||
property.property,
|
||||
StatusCode::FORBIDDEN,
|
||||
BaseCondition::ValidResourceType,
|
||||
);
|
||||
has_errors = true;
|
||||
} else {
|
||||
items.insert_ok(property.property);
|
||||
}
|
||||
}
|
||||
(DavProperty::DeadProperty(dead), DavValue::DeadProperty(values))
|
||||
if self.core.groupware.dead_property_size.is_some() =>
|
||||
{
|
||||
if is_update {
|
||||
file.dead_properties.remove_element(dead);
|
||||
}
|
||||
|
||||
if file.dead_properties.size() + values.size() + dead.size()
|
||||
< self.core.groupware.dead_property_size.unwrap()
|
||||
{
|
||||
file.dead_properties.add_element(dead.clone(), values.0);
|
||||
items.insert_ok(property.property);
|
||||
} else {
|
||||
items.insert_error_with_description(
|
||||
property.property,
|
||||
StatusCode::INSUFFICIENT_STORAGE,
|
||||
"Property value is too long",
|
||||
);
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
(_, DavValue::Null) => {
|
||||
items.insert_ok(property.property);
|
||||
}
|
||||
_ => {
|
||||
items.insert_error_with_description(
|
||||
property.property,
|
||||
StatusCode::CONFLICT,
|
||||
"Property cannot be modified",
|
||||
);
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
!has_errors
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_file_properties(
|
||||
node: &mut FileNode,
|
||||
properties: Vec<DavProperty>,
|
||||
items: &mut PropStatBuilder,
|
||||
) {
|
||||
for property in properties {
|
||||
match &property {
|
||||
DavProperty::WebDav(WebDavProperty::DisplayName) => {
|
||||
node.display_name = None;
|
||||
items.insert_with_status(property, StatusCode::NO_CONTENT);
|
||||
}
|
||||
DavProperty::WebDav(WebDavProperty::GetContentType) if node.file.is_some() => {
|
||||
node.file.as_mut().unwrap().media_type = None;
|
||||
items.insert_with_status(property, StatusCode::NO_CONTENT);
|
||||
}
|
||||
DavProperty::DeadProperty(dead) => {
|
||||
node.dead_properties.remove_element(dead);
|
||||
items.insert_with_status(property, StatusCode::NO_CONTENT);
|
||||
}
|
||||
_ => {
|
||||
items.insert_error_with_description(
|
||||
property,
|
||||
StatusCode::CONFLICT,
|
||||
"Property cannot be deleted",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
DavError, DavMethod,
|
||||
common::{
|
||||
ETag, ExtractETag,
|
||||
acl::ResourceAcl,
|
||||
lock::{LockRequestHandler, ResourceState},
|
||||
uri::DavUriResource,
|
||||
},
|
||||
file::DavFileResource,
|
||||
};
|
||||
use common::{
|
||||
Server, auth::AccessToken, sharing::EffectiveAcl, storage::index::ObjectIndexBuilder,
|
||||
};
|
||||
use dav_proto::{RequestHeaders, Return, schema::property::Rfc1123DateTime};
|
||||
use groupware::{
|
||||
cache::GroupwareCache,
|
||||
file::{FileNode, FileProperties},
|
||||
};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use store::write::{BatchBuilder, now};
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{AlignedBytes, Archive},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::Acl,
|
||||
blob_hash::BlobHash,
|
||||
collection::{Collection, SyncCollection},
|
||||
};
|
||||
|
||||
pub(crate) trait FileUpdateRequestHandler: Sync + Send {
|
||||
fn handle_file_update_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
bytes: Vec<u8>,
|
||||
is_patch: bool,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
}
|
||||
|
||||
impl FileUpdateRequestHandler for Server {
|
||||
async fn handle_file_update_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
bytes: Vec<u8>,
|
||||
_is_patch: bool,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
// Validate URI
|
||||
let resource = self
|
||||
.validate_uri(access_token, headers.uri)
|
||||
.await?
|
||||
.into_owned_uri()?;
|
||||
let account_id = resource.account_id;
|
||||
let resources = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::FileNode,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let resource_name = resource
|
||||
.resource
|
||||
.ok_or(DavError::Code(StatusCode::CONFLICT))?;
|
||||
|
||||
if bytes.len() > self.core.groupware.max_file_size {
|
||||
return Err(DavError::Code(StatusCode::PAYLOAD_TOO_LARGE));
|
||||
}
|
||||
|
||||
if let Some(document_id) = resources
|
||||
.by_path(resource_name.as_ref())
|
||||
.map(|r| r.document_id())
|
||||
{
|
||||
// Update
|
||||
let node_ = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::FileNode,
|
||||
document_id,
|
||||
))
|
||||
.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::Modify)
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
// Validate headers
|
||||
match self
|
||||
.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![ResourceState {
|
||||
account_id,
|
||||
collection: resource.collection,
|
||||
document_id: Some(document_id),
|
||||
etag: node.etag().into(),
|
||||
path: resource_name,
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::PUT,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(DavError::Code(StatusCode::PRECONDITION_FAILED))
|
||||
if headers.ret == Return::Representation =>
|
||||
{
|
||||
let file = node.inner.file.as_ref().unwrap();
|
||||
let contents = self
|
||||
.blob_store()
|
||||
.get_blob(file.blob_hash.0.as_slice(), 0..usize::MAX)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::PRECONDITION_FAILED))?;
|
||||
|
||||
return Ok(HttpResponse::new(StatusCode::PRECONDITION_FAILED)
|
||||
.with_content_type(
|
||||
file.media_type
|
||||
.as_ref()
|
||||
.map(|v| v.as_str())
|
||||
.unwrap_or("application/octet-stream"),
|
||||
)
|
||||
.with_etag(node.etag())
|
||||
.with_last_modified(
|
||||
Rfc1123DateTime::new(i64::from(node.inner.modified)).to_string(),
|
||||
)
|
||||
.with_header("Preference-Applied", "return=representation")
|
||||
.with_binary_body(contents));
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
|
||||
// Verify that the node is a file
|
||||
if let Some(file) = node.inner.file.as_ref() {
|
||||
if BlobHash::generate(&bytes).as_slice() == file.blob_hash.0.as_slice() {
|
||||
return Ok(HttpResponse::new(StatusCode::NO_CONTENT));
|
||||
}
|
||||
} else {
|
||||
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
|
||||
}
|
||||
|
||||
// Validate quota
|
||||
let extra_bytes = (bytes.len() as u64)
|
||||
.saturating_sub(u32::from(node.inner.file.as_ref().unwrap().size) as u64);
|
||||
if extra_bytes > 0 {
|
||||
self.has_available_quota(self.account(account_id).await?.as_ref(), extra_bytes)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Write blob
|
||||
let (blob_hash, blob_hold) = self
|
||||
.put_temporary_blob(account_id, &bytes, 60)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Build node
|
||||
let mut new_node = node.deserialize::<FileNode>().caused_by(trc::location!())?;
|
||||
let new_file = new_node.file.as_mut().unwrap();
|
||||
new_file.blob_hash = blob_hash;
|
||||
new_file.media_type = headers
|
||||
.content_type
|
||||
.filter(|ct| !ct.is_empty() && *ct != "application/octet-stream")
|
||||
.map(|v| v.to_string());
|
||||
new_file.size = bytes.len() as u32;
|
||||
new_node.modified = now() as i64;
|
||||
|
||||
// Prepare write batch
|
||||
let mut batch = BatchBuilder::new();
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::FileNode)
|
||||
.with_document(document_id)
|
||||
.clear(blob_hold)
|
||||
.custom(
|
||||
ObjectIndexBuilder::new()
|
||||
.with_current(node)
|
||||
.with_changes(new_node)
|
||||
.with_changed_by(access_token.account_tenant_ids()),
|
||||
)
|
||||
.caused_by(trc::location!())?;
|
||||
let etag = batch.etag();
|
||||
self.commit_batch(batch).await.caused_by(trc::location!())?;
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::NO_CONTENT).with_etag_opt(etag))
|
||||
} else {
|
||||
// Insert
|
||||
let orig_resource_name = resource_name;
|
||||
let (parent, resource_name) = resources
|
||||
.map_parent(orig_resource_name.as_ref())
|
||||
.ok_or(DavError::Code(StatusCode::CONFLICT))?;
|
||||
|
||||
// Validate ACL
|
||||
let parent_id = resources.validate_and_map_parent_acl(
|
||||
access_token,
|
||||
access_token.is_member(account_id),
|
||||
parent.map(|r| r.document_id()),
|
||||
Acl::AddItems,
|
||||
)?;
|
||||
|
||||
// Verify that parent is a collection
|
||||
if parent.as_ref().is_some_and(|r| !r.is_container()) {
|
||||
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
|
||||
}
|
||||
|
||||
// Validate headers
|
||||
self.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![ResourceState {
|
||||
account_id,
|
||||
collection: resource.collection,
|
||||
document_id: Some(u32::MAX),
|
||||
path: orig_resource_name,
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::PUT,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Validate quota
|
||||
if !bytes.is_empty() {
|
||||
self.has_available_quota(
|
||||
self.account(account_id).await?.as_ref(),
|
||||
bytes.len() as u64,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Write blob
|
||||
let (blob_hash, blob_hold) = self
|
||||
.put_temporary_blob(account_id, &bytes, 60)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Build node
|
||||
let now = now();
|
||||
let node = FileNode {
|
||||
parent_id,
|
||||
name: resource_name.to_string(),
|
||||
display_name: None,
|
||||
file: Some(FileProperties {
|
||||
blob_hash,
|
||||
size: bytes.len() as u32,
|
||||
media_type: headers.content_type.map(|v| v.to_string()),
|
||||
executable: false,
|
||||
}),
|
||||
created: now as i64,
|
||||
modified: now as i64,
|
||||
dead_properties: Default::default(),
|
||||
acls: parent
|
||||
.as_ref()
|
||||
.and_then(|p| p.resource.acls())
|
||||
.map(|acls| acls.to_vec())
|
||||
.unwrap_or_default(),
|
||||
};
|
||||
|
||||
// Prepare write batch
|
||||
let mut batch = BatchBuilder::new();
|
||||
let document_id = self
|
||||
.store()
|
||||
.assign_document_ids(account_id, Collection::FileNode, 1)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::FileNode)
|
||||
.with_document(document_id)
|
||||
.clear(blob_hold)
|
||||
.custom(
|
||||
ObjectIndexBuilder::<(), _>::new()
|
||||
.with_changes(node)
|
||||
.with_changed_by(access_token.account_tenant_ids()),
|
||||
)
|
||||
.caused_by(trc::location!())?;
|
||||
let etag = batch.etag();
|
||||
self.commit_batch(batch).await.caused_by(trc::location!())?;
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
#![warn(clippy::large_futures)]
|
||||
|
||||
pub mod calendar;
|
||||
pub mod card;
|
||||
pub mod common;
|
||||
pub mod file;
|
||||
pub mod principal;
|
||||
pub mod request;
|
||||
|
||||
use dav_proto::schema::{
|
||||
request::DavPropertyValue,
|
||||
response::{Condition, List, Prop, PropStat, ResponseDescription, Status},
|
||||
};
|
||||
use groupware::{DavResourceName, RFC_3986, is_uri_segment};
|
||||
use hyper::{Method, StatusCode};
|
||||
use std::borrow::Cow;
|
||||
use store::ahash::AHashMap;
|
||||
pub(crate) type Result<T> = std::result::Result<T, DavError>;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum DavMethod {
|
||||
GET,
|
||||
PUT,
|
||||
POST,
|
||||
DELETE,
|
||||
HEAD,
|
||||
PATCH,
|
||||
PROPFIND,
|
||||
PROPPATCH,
|
||||
REPORT,
|
||||
MKCOL,
|
||||
MKCALENDAR,
|
||||
COPY,
|
||||
MOVE,
|
||||
LOCK,
|
||||
UNLOCK,
|
||||
OPTIONS,
|
||||
ACL,
|
||||
}
|
||||
|
||||
impl From<DavMethod> for trc::WebDavEvent {
|
||||
fn from(value: DavMethod) -> Self {
|
||||
match value {
|
||||
DavMethod::GET => trc::WebDavEvent::Get,
|
||||
DavMethod::PUT => trc::WebDavEvent::Put,
|
||||
DavMethod::POST => trc::WebDavEvent::Post,
|
||||
DavMethod::DELETE => trc::WebDavEvent::Delete,
|
||||
DavMethod::HEAD => trc::WebDavEvent::Head,
|
||||
DavMethod::PATCH => trc::WebDavEvent::Patch,
|
||||
DavMethod::PROPFIND => trc::WebDavEvent::Propfind,
|
||||
DavMethod::PROPPATCH => trc::WebDavEvent::Proppatch,
|
||||
DavMethod::REPORT => trc::WebDavEvent::Report,
|
||||
DavMethod::MKCOL => trc::WebDavEvent::Mkcol,
|
||||
DavMethod::MKCALENDAR => trc::WebDavEvent::Mkcalendar,
|
||||
DavMethod::COPY => trc::WebDavEvent::Copy,
|
||||
DavMethod::MOVE => trc::WebDavEvent::Move,
|
||||
DavMethod::LOCK => trc::WebDavEvent::Lock,
|
||||
DavMethod::UNLOCK => trc::WebDavEvent::Unlock,
|
||||
DavMethod::OPTIONS => trc::WebDavEvent::Options,
|
||||
DavMethod::ACL => trc::WebDavEvent::Acl,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum DavError {
|
||||
Parse(dav_proto::parser::Error),
|
||||
Internal(trc::Error),
|
||||
Condition(DavErrorCondition),
|
||||
Code(StatusCode),
|
||||
}
|
||||
|
||||
struct DavErrorCondition {
|
||||
pub code: StatusCode,
|
||||
pub condition: Condition,
|
||||
pub details: Option<String>,
|
||||
}
|
||||
|
||||
impl From<DavErrorCondition> for DavError {
|
||||
fn from(value: DavErrorCondition) -> Self {
|
||||
DavError::Condition(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Condition> for DavErrorCondition {
|
||||
fn from(value: Condition) -> Self {
|
||||
DavErrorCondition {
|
||||
code: StatusCode::CONFLICT,
|
||||
condition: value,
|
||||
details: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DavErrorCondition {
|
||||
pub fn new(code: StatusCode, condition: impl Into<Condition>) -> Self {
|
||||
DavErrorCondition {
|
||||
code,
|
||||
condition: condition.into(),
|
||||
details: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_details(mut self, details: impl Into<String>) -> Self {
|
||||
self.details = Some(details.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl DavMethod {
|
||||
pub fn parse(method: &Method) -> Option<Self> {
|
||||
match *method {
|
||||
Method::GET => Some(DavMethod::GET),
|
||||
Method::PUT => Some(DavMethod::PUT),
|
||||
Method::DELETE => Some(DavMethod::DELETE),
|
||||
Method::OPTIONS => Some(DavMethod::OPTIONS),
|
||||
Method::POST => Some(DavMethod::POST),
|
||||
Method::PATCH => Some(DavMethod::PATCH),
|
||||
Method::HEAD => Some(DavMethod::HEAD),
|
||||
_ => {
|
||||
hashify::tiny_map!(method.as_str().as_bytes(),
|
||||
"PROPFIND" => DavMethod::PROPFIND,
|
||||
"PROPPATCH" => DavMethod::PROPPATCH,
|
||||
"REPORT" => DavMethod::REPORT,
|
||||
"MKCOL" => DavMethod::MKCOL,
|
||||
"MKCALENDAR" => DavMethod::MKCALENDAR,
|
||||
"COPY" => DavMethod::COPY,
|
||||
"MOVE" => DavMethod::MOVE,
|
||||
"LOCK" => DavMethod::LOCK,
|
||||
"UNLOCK" => DavMethod::UNLOCK,
|
||||
"ACL" => DavMethod::ACL
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn has_body(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
DavMethod::PUT
|
||||
| DavMethod::POST
|
||||
| DavMethod::PATCH
|
||||
| DavMethod::PROPPATCH
|
||||
| DavMethod::PROPFIND
|
||||
| DavMethod::REPORT
|
||||
| DavMethod::LOCK
|
||||
| DavMethod::ACL
|
||||
| DavMethod::MKCALENDAR
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct PropStatBuilder {
|
||||
propstats: AHashMap<(StatusCode, Option<Condition>, Option<String>), Vec<DavPropertyValue>>,
|
||||
}
|
||||
|
||||
impl PropStatBuilder {
|
||||
pub fn insert_ok(&mut self, prop: impl Into<DavPropertyValue>) -> &mut Self {
|
||||
self.propstats
|
||||
.entry((StatusCode::OK, None, None))
|
||||
.or_default()
|
||||
.push(prop.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn insert_with_status(
|
||||
&mut self,
|
||||
prop: impl Into<DavPropertyValue>,
|
||||
status: StatusCode,
|
||||
) -> &mut Self {
|
||||
self.propstats
|
||||
.entry((status, None, None))
|
||||
.or_default()
|
||||
.push(prop.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn insert_error_with_description(
|
||||
&mut self,
|
||||
prop: impl Into<DavPropertyValue>,
|
||||
status: StatusCode,
|
||||
description: impl Into<String>,
|
||||
) -> &mut Self {
|
||||
self.propstats
|
||||
.entry((status, None, Some(description.into())))
|
||||
.or_default()
|
||||
.push(prop.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn insert_precondition_failed(
|
||||
&mut self,
|
||||
prop: impl Into<DavPropertyValue>,
|
||||
status: StatusCode,
|
||||
condition: impl Into<Condition>,
|
||||
) -> &mut Self {
|
||||
self.propstats
|
||||
.entry((status, Some(condition.into()), None))
|
||||
.or_default()
|
||||
.push(prop.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn insert_precondition_failed_with_description(
|
||||
&mut self,
|
||||
prop: impl Into<DavPropertyValue>,
|
||||
status: StatusCode,
|
||||
condition: impl Into<Condition>,
|
||||
description: impl Into<String>,
|
||||
) -> &mut Self {
|
||||
self.propstats
|
||||
.entry((status, Some(condition.into()), Some(description.into())))
|
||||
.or_default()
|
||||
.push(prop.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Vec<PropStat> {
|
||||
self.propstats
|
||||
.into_iter()
|
||||
.map(|((status, condition, description), props)| PropStat {
|
||||
prop: Prop(List(props)),
|
||||
status: Status(status),
|
||||
error: condition,
|
||||
response_description: description.map(ResponseDescription),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
// Workaround for Apple bug with missing percent encoding in paths
|
||||
pub(crate) fn fix_percent_encoding(path: &'_ str) -> Cow<'_, str> {
|
||||
let (parent, name) = if let Some((parent, name)) = path.rsplit_once('/') {
|
||||
(Some(parent), name)
|
||||
} else {
|
||||
(None, path)
|
||||
};
|
||||
|
||||
if is_uri_segment(name) {
|
||||
path.into()
|
||||
} else {
|
||||
let name = percent_encoding::utf8_percent_encode(name, RFC_3986);
|
||||
|
||||
if let Some(parent) = parent {
|
||||
Cow::Owned(format!("{parent}/{name}"))
|
||||
} else {
|
||||
Cow::Owned(name.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::propfind::PrincipalPropFind;
|
||||
use crate::{
|
||||
DavError,
|
||||
common::{
|
||||
DavQuery, DavQueryResource,
|
||||
propfind::PropFindRequestHandler,
|
||||
uri::{DavUriResource, UriResource},
|
||||
},
|
||||
};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use dav_proto::{
|
||||
RequestHeaders,
|
||||
schema::{
|
||||
property::{DavProperty, WebDavProperty},
|
||||
request::{PrincipalMatch, PropFind},
|
||||
response::MultiStatus,
|
||||
},
|
||||
};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use store::roaring::RoaringBitmap;
|
||||
use types::collection::Collection;
|
||||
|
||||
pub(crate) trait PrincipalMatching: Sync + Send {
|
||||
fn handle_principal_match(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
request: PrincipalMatch,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
}
|
||||
|
||||
impl PrincipalMatching for Server {
|
||||
async fn handle_principal_match(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
mut request: PrincipalMatch,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
let resource = self.validate_uri(access_token, headers.uri).await?;
|
||||
|
||||
match resource.collection {
|
||||
Collection::AddressBook | Collection::Calendar | Collection::FileNode => {
|
||||
if request.properties.is_empty() {
|
||||
request
|
||||
.properties
|
||||
.push(DavProperty::WebDav(WebDavProperty::Owner));
|
||||
}
|
||||
if let Some(account_id) = resource.account_id {
|
||||
return self
|
||||
.handle_dav_query(
|
||||
access_token,
|
||||
DavQuery {
|
||||
resource: DavQueryResource::Uri(UriResource {
|
||||
collection: resource.collection,
|
||||
account_id,
|
||||
resource: resource.resource,
|
||||
}),
|
||||
propfind: PropFind::Prop(request.properties),
|
||||
depth: usize::MAX,
|
||||
ret: headers.ret,
|
||||
depth_no_root: headers.depth_no_root,
|
||||
uri: headers.uri,
|
||||
sync_type: Default::default(),
|
||||
limit: Default::default(),
|
||||
vcard_version: Default::default(),
|
||||
expand: Default::default(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Collection::Principal => {}
|
||||
_ => return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
|
||||
}
|
||||
|
||||
let mut response = MultiStatus::new(Vec::with_capacity(16));
|
||||
if request.properties.is_empty() {
|
||||
request
|
||||
.properties
|
||||
.push(DavProperty::WebDav(WebDavProperty::DisplayName));
|
||||
}
|
||||
let request = PropFind::Prop(request.properties);
|
||||
self.prepare_principal_propfind_response(
|
||||
access_token,
|
||||
resource.collection,
|
||||
RoaringBitmap::from_iter(access_token.all_ids()).into_iter(),
|
||||
&request,
|
||||
&mut response,
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::new(StatusCode::MULTI_STATUS).with_xml_body(response.to_string()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use common::auth::AccountCache;
|
||||
use dav_proto::schema::response::Href;
|
||||
use groupware::RFC_3986;
|
||||
|
||||
use crate::DavResourceName;
|
||||
|
||||
pub mod matching;
|
||||
pub mod propfind;
|
||||
pub mod propsearch;
|
||||
|
||||
pub trait CurrentUserPrincipal {
|
||||
fn current_user_principal(&self) -> Href;
|
||||
}
|
||||
|
||||
impl CurrentUserPrincipal for AccountCache {
|
||||
fn current_user_principal(&self) -> Href {
|
||||
Href(format!(
|
||||
"{}/{}/",
|
||||
DavResourceName::Principal.base_path(),
|
||||
percent_encoding::utf8_percent_encode(self.name(), RFC_3986)
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,464 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::CurrentUserPrincipal;
|
||||
use crate::{
|
||||
DavResourceName,
|
||||
common::propfind::{PropFindRequestHandler, SyncTokenUrn},
|
||||
};
|
||||
use common::{
|
||||
Server,
|
||||
auth::{AccessToken, AccountCache},
|
||||
};
|
||||
use dav_proto::schema::{
|
||||
Namespace,
|
||||
property::{
|
||||
DavProperty, DavValue, PrincipalProperty, Privilege, ReportSet, ResourceType,
|
||||
WebDavProperty,
|
||||
},
|
||||
request::{DavPropertyValue, PropFind},
|
||||
response::{Href, MultiStatus, PropStat, Response},
|
||||
};
|
||||
use groupware::RFC_3986;
|
||||
use groupware::cache::GroupwareCache;
|
||||
use hyper::StatusCode;
|
||||
use std::borrow::Cow;
|
||||
use trc::AddContext;
|
||||
use types::collection::Collection;
|
||||
|
||||
pub(crate) trait PrincipalPropFind: Sync + Send {
|
||||
fn prepare_principal_propfind_response(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
collection: Collection,
|
||||
documents: impl Iterator<Item = u32> + Sync + Send,
|
||||
request: &PropFind,
|
||||
response: &mut MultiStatus,
|
||||
) -> impl Future<Output = crate::Result<()>> + Send;
|
||||
|
||||
fn expand_principal(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
account_id: u32,
|
||||
propfind: &PropFind,
|
||||
) -> impl Future<Output = crate::Result<Option<Response>>> + Send;
|
||||
|
||||
fn owner_href(
|
||||
&self,
|
||||
account_info: &AccountCache,
|
||||
account_id: u32,
|
||||
) -> impl Future<Output = trc::Result<Href>> + Send;
|
||||
}
|
||||
|
||||
impl PrincipalPropFind for Server {
|
||||
async fn prepare_principal_propfind_response(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
collection: Collection,
|
||||
account_ids: impl Iterator<Item = u32> + Sync + Send,
|
||||
request: &PropFind,
|
||||
response: &mut MultiStatus,
|
||||
) -> crate::Result<()> {
|
||||
let access_account_info = self
|
||||
.account(access_token.account_id())
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let properties = match request {
|
||||
PropFind::PropName => {
|
||||
let props = all_props(collection, None);
|
||||
for property in &props {
|
||||
response.set_namespace(property.namespace());
|
||||
}
|
||||
for account_id in account_ids {
|
||||
response.add_response(Response::new_propstat(
|
||||
self.owner_href(&access_account_info, account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?,
|
||||
vec![PropStat::new_list(
|
||||
props.iter().cloned().map(DavPropertyValue::empty).collect(),
|
||||
)],
|
||||
));
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
PropFind::AllProp(items) => Cow::Owned(all_props(collection, items.as_slice().into())),
|
||||
PropFind::Prop(items) => Cow::Borrowed(items),
|
||||
};
|
||||
for property in properties.as_slice() {
|
||||
response.set_namespace(property.namespace());
|
||||
}
|
||||
let is_principal = match collection {
|
||||
Collection::AddressBook | Collection::ContactCard => {
|
||||
response.set_namespace(Namespace::CardDav);
|
||||
false
|
||||
}
|
||||
Collection::Calendar
|
||||
| Collection::CalendarEvent
|
||||
| Collection::CalendarEventNotification => {
|
||||
response.set_namespace(Namespace::CalDav);
|
||||
false
|
||||
}
|
||||
Collection::Principal => true,
|
||||
_ => false,
|
||||
};
|
||||
let base_path = DavResourceName::from(collection).base_path();
|
||||
let needs_quota = properties.iter().any(|property| {
|
||||
matches!(
|
||||
property,
|
||||
DavProperty::WebDav(
|
||||
WebDavProperty::QuotaAvailableBytes | WebDavProperty::QuotaUsedBytes
|
||||
)
|
||||
)
|
||||
});
|
||||
|
||||
for account_id in account_ids {
|
||||
let mut fields = Vec::with_capacity(properties.len());
|
||||
let mut fields_not_found = Vec::new();
|
||||
|
||||
let account = self.account(account_id).await.caused_by(trc::location!())?;
|
||||
|
||||
// Fetch quota
|
||||
let quota = if needs_quota {
|
||||
self.dav_quota(account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
for property in properties.as_slice() {
|
||||
match property {
|
||||
DavProperty::WebDav(dav_property) => match dav_property {
|
||||
WebDavProperty::DisplayName => {
|
||||
fields.push(DavPropertyValue::new(
|
||||
property.clone(),
|
||||
account.description().unwrap_or(account.name()).to_string(),
|
||||
));
|
||||
}
|
||||
WebDavProperty::ResourceType => {
|
||||
let resource_type = if !is_principal {
|
||||
vec![ResourceType::Collection]
|
||||
} else {
|
||||
vec![ResourceType::Principal, ResourceType::Collection]
|
||||
};
|
||||
|
||||
fields.push(DavPropertyValue::new(property.clone(), resource_type));
|
||||
}
|
||||
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(property.clone(), reports));
|
||||
}
|
||||
WebDavProperty::CurrentUserPrincipal => {
|
||||
fields.push(DavPropertyValue::new(
|
||||
property.clone(),
|
||||
vec![access_account_info.current_user_principal()],
|
||||
));
|
||||
}
|
||||
WebDavProperty::QuotaAvailableBytes if !is_principal => {
|
||||
if let Some(available) = quota.available {
|
||||
fields.push(DavPropertyValue::new(property.clone(), available));
|
||||
} else {
|
||||
fields_not_found.push(DavPropertyValue::empty(property.clone()));
|
||||
}
|
||||
}
|
||||
WebDavProperty::QuotaUsedBytes if !is_principal => {
|
||||
fields.push(DavPropertyValue::new(property.clone(), quota.used));
|
||||
}
|
||||
WebDavProperty::SyncToken if !is_principal => {
|
||||
let sync_token = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
collection.into(),
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.sync_token();
|
||||
|
||||
fields.push(DavPropertyValue::new(property.clone(), sync_token));
|
||||
}
|
||||
WebDavProperty::GetCTag if !is_principal => {
|
||||
let ctag = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
collection.into(),
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.highest_change_id;
|
||||
|
||||
fields.push(DavPropertyValue::new(
|
||||
property.clone(),
|
||||
DavValue::String(format!("\"{ctag}\"")),
|
||||
));
|
||||
}
|
||||
WebDavProperty::Owner => {
|
||||
fields.push(DavPropertyValue::new(
|
||||
property.clone(),
|
||||
vec![Href(format!(
|
||||
"{}/{}/",
|
||||
DavResourceName::Principal.base_path(),
|
||||
percent_encoding::utf8_percent_encode(account.name(), RFC_3986),
|
||||
))],
|
||||
));
|
||||
}
|
||||
WebDavProperty::Group if !is_principal => {
|
||||
fields.push(DavPropertyValue::empty(property.clone()));
|
||||
}
|
||||
WebDavProperty::CurrentUserPrivilegeSet if !is_principal => {
|
||||
fields.push(DavPropertyValue::new(
|
||||
property.clone(),
|
||||
if access_token.is_member(account_id) {
|
||||
Privilege::all(matches!(
|
||||
collection,
|
||||
Collection::Calendar | Collection::CalendarEvent
|
||||
))
|
||||
} else {
|
||||
vec![Privilege::Read]
|
||||
},
|
||||
));
|
||||
}
|
||||
WebDavProperty::PrincipalCollectionSet => {
|
||||
fields.push(DavPropertyValue::new(
|
||||
property.clone(),
|
||||
vec![Href(
|
||||
DavResourceName::Principal.collection_path().to_string(),
|
||||
)],
|
||||
));
|
||||
}
|
||||
_ => {
|
||||
fields_not_found.push(DavPropertyValue::empty(property.clone()));
|
||||
}
|
||||
},
|
||||
DavProperty::Principal(principal_property) => match principal_property {
|
||||
PrincipalProperty::AlternateURISet
|
||||
| PrincipalProperty::GroupMemberSet
|
||||
| PrincipalProperty::GroupMembership => {
|
||||
fields.push(DavPropertyValue::empty(property.clone()));
|
||||
}
|
||||
PrincipalProperty::PrincipalURL => {
|
||||
fields.push(DavPropertyValue::new(
|
||||
property.clone(),
|
||||
vec![Href(format!(
|
||||
"{}/{}/",
|
||||
DavResourceName::Principal.base_path(),
|
||||
percent_encoding::utf8_percent_encode(account.name(), RFC_3986),
|
||||
))],
|
||||
));
|
||||
}
|
||||
PrincipalProperty::CalendarHomeSet => {
|
||||
let hrefs = build_home_set(
|
||||
self,
|
||||
access_token,
|
||||
account.name(),
|
||||
account_id,
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
fields.push(DavPropertyValue::new(property.clone(), hrefs));
|
||||
}
|
||||
PrincipalProperty::AddressbookHomeSet => {
|
||||
let hrefs = build_home_set(
|
||||
self,
|
||||
access_token,
|
||||
account.name(),
|
||||
account_id,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
fields.push(DavPropertyValue::new(property.clone(), hrefs));
|
||||
}
|
||||
|
||||
PrincipalProperty::PrincipalAddress => {
|
||||
fields_not_found.push(DavPropertyValue::empty(property.clone()));
|
||||
}
|
||||
PrincipalProperty::CalendarUserAddressSet => {
|
||||
fields.push(DavPropertyValue::new(
|
||||
property.clone(),
|
||||
vec![Href(format!("mailto:{}", account.name()))],
|
||||
));
|
||||
}
|
||||
PrincipalProperty::CalendarUserType => {
|
||||
fields.push(DavPropertyValue::new(
|
||||
property.clone(),
|
||||
if account.is_user_account() {
|
||||
DavValue::String("INDIVIDUAL".to_string())
|
||||
} else {
|
||||
DavValue::String("GROUP".to_string())
|
||||
},
|
||||
));
|
||||
}
|
||||
PrincipalProperty::ScheduleInboxURL => {
|
||||
fields.push(DavPropertyValue::new(
|
||||
property.clone(),
|
||||
vec![Href(format!(
|
||||
"{}/{}/inbox/",
|
||||
DavResourceName::Scheduling.base_path(),
|
||||
percent_encoding::utf8_percent_encode(account.name(), RFC_3986),
|
||||
))],
|
||||
));
|
||||
}
|
||||
PrincipalProperty::ScheduleOutboxURL => {
|
||||
fields.push(DavPropertyValue::new(
|
||||
property.clone(),
|
||||
vec![Href(format!(
|
||||
"{}/{}/outbox/",
|
||||
DavResourceName::Scheduling.base_path(),
|
||||
percent_encoding::utf8_percent_encode(account.name(), RFC_3986),
|
||||
))],
|
||||
));
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
fields_not_found.push(DavPropertyValue::empty(property.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut prop_stats = Vec::with_capacity(2);
|
||||
|
||||
if !fields_not_found.is_empty() {
|
||||
prop_stats
|
||||
.push(PropStat::new_list(fields_not_found).with_status(StatusCode::NOT_FOUND));
|
||||
}
|
||||
|
||||
if !fields.is_empty() || prop_stats.is_empty() {
|
||||
prop_stats.push(PropStat::new_list(fields));
|
||||
}
|
||||
|
||||
response.add_response(Response::new_propstat(
|
||||
Href(format!(
|
||||
"{}/{}/",
|
||||
base_path,
|
||||
percent_encoding::utf8_percent_encode(account.name(), RFC_3986),
|
||||
)),
|
||||
prop_stats,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn expand_principal(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
account_id: u32,
|
||||
propfind: &PropFind,
|
||||
) -> crate::Result<Option<Response>> {
|
||||
let mut status = MultiStatus::new(vec![]);
|
||||
self.prepare_principal_propfind_response(
|
||||
access_token,
|
||||
Collection::Principal,
|
||||
[account_id].into_iter(),
|
||||
propfind,
|
||||
&mut status,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(status.response.0.into_iter().next())
|
||||
}
|
||||
|
||||
async fn owner_href(&self, account_info: &AccountCache, account_id: u32) -> trc::Result<Href> {
|
||||
if account_info.account_id() == account_id {
|
||||
Ok(account_info.current_user_principal())
|
||||
} else {
|
||||
let account_info = self.account(account_id).await.caused_by(trc::location!())?;
|
||||
Ok(Href(format!(
|
||||
"{}/{}/",
|
||||
DavResourceName::Principal.base_path(),
|
||||
percent_encoding::utf8_percent_encode(account_info.name(), RFC_3986),
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn build_home_set(
|
||||
server: &Server,
|
||||
access_token: &AccessToken,
|
||||
name: &str,
|
||||
account_id: u32,
|
||||
is_calendar: bool,
|
||||
) -> trc::Result<Vec<Href>> {
|
||||
let (collection, resource_name) = if is_calendar {
|
||||
(Collection::Calendar, DavResourceName::Cal)
|
||||
} else {
|
||||
(Collection::AddressBook, DavResourceName::Card)
|
||||
};
|
||||
|
||||
let mut hrefs = Vec::new();
|
||||
hrefs.push(Href(format!(
|
||||
"{}/{}/",
|
||||
resource_name.base_path(),
|
||||
percent_encoding::utf8_percent_encode(name, RFC_3986),
|
||||
)));
|
||||
|
||||
if !server.core.groupware.assisted_discovery && account_id == access_token.account_id() {
|
||||
for account_id in access_token.all_ids_by_collection(collection) {
|
||||
if account_id != access_token.account_id() {
|
||||
let other = server
|
||||
.account(account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
hrefs.push(Href(format!(
|
||||
"{}/{}/",
|
||||
resource_name.base_path(),
|
||||
percent_encoding::utf8_percent_encode(other.name(), RFC_3986),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(hrefs)
|
||||
}
|
||||
|
||||
fn all_props(collection: Collection, all_props: Option<&[DavProperty]>) -> Vec<DavProperty> {
|
||||
if collection == Collection::Principal {
|
||||
vec![
|
||||
DavProperty::WebDav(WebDavProperty::DisplayName),
|
||||
DavProperty::WebDav(WebDavProperty::ResourceType),
|
||||
DavProperty::WebDav(WebDavProperty::SupportedReportSet),
|
||||
DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal),
|
||||
DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet),
|
||||
DavProperty::Principal(PrincipalProperty::AlternateURISet),
|
||||
DavProperty::Principal(PrincipalProperty::PrincipalURL),
|
||||
DavProperty::Principal(PrincipalProperty::GroupMemberSet),
|
||||
DavProperty::Principal(PrincipalProperty::GroupMembership),
|
||||
]
|
||||
} else {
|
||||
let mut props = vec![
|
||||
DavProperty::WebDav(WebDavProperty::DisplayName),
|
||||
DavProperty::WebDav(WebDavProperty::ResourceType),
|
||||
DavProperty::WebDav(WebDavProperty::SupportedReportSet),
|
||||
DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal),
|
||||
DavProperty::WebDav(WebDavProperty::SyncToken),
|
||||
DavProperty::WebDav(WebDavProperty::Owner),
|
||||
DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet),
|
||||
];
|
||||
|
||||
if let Some(all_props) = all_props {
|
||||
props.extend(all_props.iter().filter(|p| !p.is_all_prop()).cloned());
|
||||
props
|
||||
} else {
|
||||
props
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::propfind::PrincipalPropFind;
|
||||
use common::{Server, auth::AccessToken};
|
||||
use dav_proto::schema::{
|
||||
property::{DavProperty, WebDavProperty},
|
||||
request::{PrincipalPropertySearch, PropFind},
|
||||
response::MultiStatus,
|
||||
};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use registry::schema::prelude::{ObjectType, Property};
|
||||
use store::{registry::RegistryQuery, roaring::RoaringBitmap};
|
||||
use trc::AddContext;
|
||||
use types::collection::Collection;
|
||||
|
||||
pub(crate) trait PrincipalPropSearch: Sync + Send {
|
||||
fn handle_principal_property_search(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
request: PrincipalPropertySearch,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
}
|
||||
|
||||
impl PrincipalPropSearch for Server {
|
||||
async fn handle_principal_property_search(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
mut request: PrincipalPropertySearch,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
let mut search_for = None;
|
||||
|
||||
for prop_search in request.property_search {
|
||||
if matches!(
|
||||
prop_search.property,
|
||||
DavProperty::WebDav(WebDavProperty::DisplayName)
|
||||
) && !prop_search.match_.is_empty()
|
||||
{
|
||||
search_for = Some(prop_search.match_);
|
||||
}
|
||||
}
|
||||
|
||||
let mut response = MultiStatus::new(Vec::with_capacity(16));
|
||||
if let Some(search_for) = search_for {
|
||||
let ids = self
|
||||
.registry()
|
||||
.query::<RoaringBitmap>(
|
||||
RegistryQuery::new(ObjectType::Account)
|
||||
.with_tenant(access_token.tenant_id())
|
||||
.text(Property::Text, search_for),
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
if !ids.is_empty() {
|
||||
if request.properties.is_empty() {
|
||||
request
|
||||
.properties
|
||||
.push(DavProperty::WebDav(WebDavProperty::DisplayName));
|
||||
}
|
||||
let request = PropFind::Prop(request.properties);
|
||||
self.prepare_principal_propfind_response(
|
||||
access_token,
|
||||
Collection::Principal,
|
||||
ids.into_iter(),
|
||||
&request,
|
||||
&mut response,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::MULTI_STATUS).with_xml_body(response.to_string()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,772 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
DavError, DavErrorCondition, DavMethod, DavResourceName,
|
||||
calendar::{
|
||||
copy_move::CalendarCopyMoveRequestHandler, delete::CalendarDeleteRequestHandler,
|
||||
freebusy::CalendarFreebusyRequestHandler, get::CalendarGetRequestHandler,
|
||||
mkcol::CalendarMkColRequestHandler, proppatch::CalendarPropPatchRequestHandler,
|
||||
query::CalendarQueryRequestHandler, scheduling::CalendarEventNotificationHandler,
|
||||
update::CalendarUpdateRequestHandler,
|
||||
},
|
||||
card::{
|
||||
copy_move::CardCopyMoveRequestHandler, delete::CardDeleteRequestHandler,
|
||||
get::CardGetRequestHandler, mkcol::CardMkColRequestHandler,
|
||||
proppatch::CardPropPatchRequestHandler, query::CardQueryRequestHandler,
|
||||
update::CardUpdateRequestHandler,
|
||||
},
|
||||
common::{
|
||||
DavQuery,
|
||||
acl::DavAclHandler,
|
||||
lock::{LockRequest, LockRequestHandler},
|
||||
propfind::PropFindRequestHandler,
|
||||
uri::DavUriResource,
|
||||
},
|
||||
file::{
|
||||
copy_move::FileCopyMoveRequestHandler, delete::FileDeleteRequestHandler,
|
||||
get::FileGetRequestHandler, mkcol::FileMkColRequestHandler,
|
||||
proppatch::FilePropPatchRequestHandler, update::FileUpdateRequestHandler,
|
||||
},
|
||||
principal::{matching::PrincipalMatching, propsearch::PrincipalPropSearch},
|
||||
};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use compact_str::{CompactString, ToCompactString};
|
||||
use dav_proto::{
|
||||
RequestHeaders,
|
||||
parser::{DavParser, tokenizer::Tokenizer},
|
||||
schema::{
|
||||
Namespace,
|
||||
property::WebDavProperty,
|
||||
request::{Acl, LockInfo, MkCol, PropFind, PropertyUpdate, Report},
|
||||
response::{
|
||||
BaseCondition, ErrorResponse, List, PrincipalSearchProperty, PrincipalSearchPropertySet,
|
||||
},
|
||||
},
|
||||
};
|
||||
use http_proto::{HttpRequest, HttpResponse, HttpSessionData, request::fetch_body};
|
||||
use hyper::{StatusCode, header};
|
||||
use registry::schema::enums::Permission;
|
||||
use std::time::Instant;
|
||||
use trc::{EventType, LimitEvent, StoreEvent, WebDavEvent};
|
||||
use types::collection::Collection;
|
||||
|
||||
pub trait DavRequestHandler: Sync + Send {
|
||||
fn handle_dav_request(
|
||||
&self,
|
||||
request: HttpRequest,
|
||||
access_token: AccessToken,
|
||||
session: &HttpSessionData,
|
||||
resource: DavResourceName,
|
||||
method: DavMethod,
|
||||
) -> impl Future<Output = HttpResponse> + Send;
|
||||
}
|
||||
|
||||
pub(crate) trait DavRequestDispatcher: Sync + Send {
|
||||
fn dispatch_dav_request(
|
||||
&self,
|
||||
headers: &RequestHeaders<'_>,
|
||||
access_token: AccessToken,
|
||||
resource: DavResourceName,
|
||||
method: DavMethod,
|
||||
body: Vec<u8>,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
}
|
||||
|
||||
impl DavRequestDispatcher for Server {
|
||||
async fn dispatch_dav_request(
|
||||
&self,
|
||||
headers: &RequestHeaders<'_>,
|
||||
access_token: AccessToken,
|
||||
resource: DavResourceName,
|
||||
method: DavMethod,
|
||||
body: Vec<u8>,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
// Dispatch
|
||||
match method {
|
||||
DavMethod::PROPFIND => {
|
||||
let request = PropFind::parse(&mut Tokenizer::new(&body))?;
|
||||
|
||||
self.handle_propfind_request(&access_token, headers, request)
|
||||
.await
|
||||
}
|
||||
DavMethod::GET | DavMethod::HEAD => match resource {
|
||||
DavResourceName::Card => {
|
||||
// Validate permissions
|
||||
let access_token =
|
||||
access_token.assert_has_permission(Permission::DavCardGet)?;
|
||||
|
||||
self.handle_card_get_request(
|
||||
&access_token,
|
||||
headers,
|
||||
matches!(method, DavMethod::HEAD),
|
||||
)
|
||||
.await
|
||||
}
|
||||
DavResourceName::Cal => {
|
||||
// Validate permissions
|
||||
let access_token = access_token.assert_has_permission(Permission::DavCalGet)?;
|
||||
|
||||
self.handle_calendar_get_request(
|
||||
&access_token,
|
||||
headers,
|
||||
matches!(method, DavMethod::HEAD),
|
||||
)
|
||||
.await
|
||||
}
|
||||
DavResourceName::File => {
|
||||
// Validate permissions
|
||||
let access_token =
|
||||
access_token.assert_has_permission(Permission::DavFileGet)?;
|
||||
|
||||
// Deal with Litmus bug
|
||||
/*self.handle_file_get_request(
|
||||
&access_token,
|
||||
headers,
|
||||
matches!(method, DavMethod::HEAD)
|
||||
&& !request.headers().contains_key("x-litmus"),
|
||||
)
|
||||
.await*/
|
||||
self.handle_file_get_request(
|
||||
&access_token,
|
||||
headers,
|
||||
matches!(method, DavMethod::HEAD),
|
||||
)
|
||||
.await
|
||||
}
|
||||
DavResourceName::Scheduling => {
|
||||
// Validate permissions
|
||||
let access_token = access_token.assert_has_permission(Permission::DavCalGet)?;
|
||||
|
||||
self.handle_scheduling_get_request(
|
||||
&access_token,
|
||||
headers,
|
||||
matches!(method, DavMethod::HEAD),
|
||||
)
|
||||
.await
|
||||
}
|
||||
DavResourceName::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
|
||||
},
|
||||
DavMethod::REPORT => match Report::parse(&mut Tokenizer::new(&body))? {
|
||||
Report::SyncCollection(sync_collection) => {
|
||||
// Validate permissions
|
||||
let access_token =
|
||||
access_token.assert_has_permission(Permission::DavSyncCollection)?;
|
||||
|
||||
let uri = self
|
||||
.validate_uri(&access_token, headers.uri)
|
||||
.await
|
||||
.and_then(|d| d.into_owned_uri())?;
|
||||
match resource {
|
||||
DavResourceName::Card
|
||||
| DavResourceName::Cal
|
||||
| DavResourceName::File
|
||||
| DavResourceName::Scheduling => {
|
||||
self.handle_dav_query(
|
||||
&access_token,
|
||||
DavQuery::changes(uri, sync_collection, headers),
|
||||
)
|
||||
.await
|
||||
}
|
||||
DavResourceName::Principal => {
|
||||
Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))
|
||||
}
|
||||
}
|
||||
}
|
||||
Report::AclPrincipalPropSet(report) => {
|
||||
// Validate permissions
|
||||
if !self.core.groupware.allow_directory_query
|
||||
&& !access_token.has_permission(Permission::DavPrincipalAcl)
|
||||
{
|
||||
return Err(DavError::Condition(
|
||||
DavErrorCondition::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
BaseCondition::NeedPrivileges(List(Default::default())),
|
||||
)
|
||||
.with_details("The administrator has disabled directory queries."),
|
||||
));
|
||||
}
|
||||
let access_token =
|
||||
access_token.assert_has_permission(Permission::DavPrincipalAcl)?;
|
||||
|
||||
self.handle_acl_prop_set(&access_token, headers, report)
|
||||
.await
|
||||
}
|
||||
Report::PrincipalMatch(report) => {
|
||||
// Validate permissions
|
||||
if !self.core.groupware.allow_directory_query
|
||||
&& !access_token.has_permission(Permission::DavPrincipalMatch)
|
||||
{
|
||||
return Err(DavError::Condition(
|
||||
DavErrorCondition::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
BaseCondition::NeedPrivileges(List(Default::default())),
|
||||
)
|
||||
.with_details("The administrator has disabled directory queries."),
|
||||
));
|
||||
}
|
||||
let access_token =
|
||||
access_token.assert_has_permission(Permission::DavPrincipalMatch)?;
|
||||
|
||||
self.handle_principal_match(&access_token, headers, report)
|
||||
.await
|
||||
}
|
||||
Report::PrincipalPropertySearch(report) => {
|
||||
if resource == DavResourceName::Principal {
|
||||
// Validate permissions
|
||||
if !self.core.groupware.allow_directory_query
|
||||
&& !access_token.has_permission(Permission::DavPrincipalSearch)
|
||||
{
|
||||
return Err(DavError::Condition(
|
||||
DavErrorCondition::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
BaseCondition::NeedPrivileges(List(Default::default())),
|
||||
)
|
||||
.with_details("The administrator has disabled directory queries."),
|
||||
));
|
||||
}
|
||||
|
||||
let access_token =
|
||||
access_token.assert_has_permission(Permission::DavPrincipalSearch)?;
|
||||
|
||||
self.handle_principal_property_search(&access_token, report)
|
||||
.await
|
||||
} else {
|
||||
Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))
|
||||
}
|
||||
}
|
||||
Report::PrincipalSearchPropertySet => {
|
||||
if resource == DavResourceName::Principal {
|
||||
// Validate permissions
|
||||
access_token
|
||||
.assert_has_permission(Permission::DavPrincipalSearchPropSet)?;
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::OK).with_xml_body(
|
||||
PrincipalSearchPropertySet::new(vec![PrincipalSearchProperty::new(
|
||||
WebDavProperty::DisplayName,
|
||||
"Account or Group name",
|
||||
)])
|
||||
.to_string(),
|
||||
))
|
||||
} else {
|
||||
Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))
|
||||
}
|
||||
}
|
||||
Report::AddressbookQuery(report) => {
|
||||
// Validate permissions
|
||||
let access_token =
|
||||
access_token.assert_has_permission(Permission::DavCardQuery)?;
|
||||
|
||||
self.handle_card_query_request(&access_token, headers, report)
|
||||
.await
|
||||
}
|
||||
Report::AddressbookMultiGet(report) => {
|
||||
// Validate permissions
|
||||
let access_token =
|
||||
access_token.assert_has_permission(Permission::DavCardMultiGet)?;
|
||||
|
||||
self.handle_dav_query(
|
||||
&access_token,
|
||||
DavQuery::multiget(report, Collection::AddressBook, headers),
|
||||
)
|
||||
.await
|
||||
}
|
||||
Report::CalendarQuery(report) => {
|
||||
// Validate permissions
|
||||
let access_token =
|
||||
access_token.assert_has_permission(Permission::DavCalQuery)?;
|
||||
|
||||
self.handle_calendar_query_request(&access_token, headers, report)
|
||||
.await
|
||||
}
|
||||
Report::CalendarMultiGet(report) => {
|
||||
// Validate permissions
|
||||
let access_token =
|
||||
access_token.assert_has_permission(Permission::DavCalMultiGet)?;
|
||||
|
||||
self.handle_dav_query(
|
||||
&access_token,
|
||||
DavQuery::multiget(report, Collection::Calendar, headers),
|
||||
)
|
||||
.await
|
||||
}
|
||||
Report::FreeBusyQuery(report) => {
|
||||
// Validate permissions
|
||||
let access_token =
|
||||
access_token.assert_has_permission(Permission::DavCalFreeBusyQuery)?;
|
||||
|
||||
self.handle_calendar_freebusy_request(&access_token, headers, report)
|
||||
.await
|
||||
}
|
||||
Report::ExpandProperty(report) => {
|
||||
let uri = self
|
||||
.validate_uri(&access_token, headers.uri)
|
||||
.await
|
||||
.and_then(|d| d.into_owned_uri())?;
|
||||
|
||||
// Validate permissions
|
||||
let access_token =
|
||||
access_token.assert_has_permission(Permission::DavExpandProperty)?;
|
||||
|
||||
match resource {
|
||||
DavResourceName::Card | DavResourceName::Cal | DavResourceName::File => {
|
||||
self.handle_dav_query(
|
||||
&access_token,
|
||||
DavQuery::expand(uri, report, headers),
|
||||
)
|
||||
.await
|
||||
}
|
||||
DavResourceName::Principal | DavResourceName::Scheduling => {
|
||||
Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
DavMethod::PROPPATCH => {
|
||||
let request = PropertyUpdate::parse(&mut Tokenizer::new(&body))?;
|
||||
match resource {
|
||||
DavResourceName::Card => {
|
||||
// Validate permissions
|
||||
let access_token =
|
||||
access_token.assert_has_permission(Permission::DavCardPropPatch)?;
|
||||
|
||||
self.handle_card_proppatch_request(&access_token, headers, request)
|
||||
.await
|
||||
}
|
||||
DavResourceName::Cal => {
|
||||
// Validate permissions
|
||||
let access_token =
|
||||
access_token.assert_has_permission(Permission::DavCalPropPatch)?;
|
||||
|
||||
self.handle_calendar_proppatch_request(&access_token, headers, request)
|
||||
.await
|
||||
}
|
||||
DavResourceName::File => {
|
||||
// Validate permissions
|
||||
let access_token =
|
||||
access_token.assert_has_permission(Permission::DavFilePropPatch)?;
|
||||
|
||||
self.handle_file_proppatch_request(&access_token, headers, request)
|
||||
.await
|
||||
}
|
||||
DavResourceName::Principal | DavResourceName::Scheduling => {
|
||||
Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))
|
||||
}
|
||||
}
|
||||
}
|
||||
DavMethod::MKCOL => {
|
||||
let request = if !body.is_empty() {
|
||||
Some(MkCol::parse(&mut Tokenizer::new(&body))?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
match resource {
|
||||
DavResourceName::Card => {
|
||||
// Validate permissions
|
||||
let access_token =
|
||||
access_token.assert_has_permission(Permission::DavCardMkCol)?;
|
||||
|
||||
self.handle_card_mkcol_request(&access_token, headers, request)
|
||||
.await
|
||||
}
|
||||
DavResourceName::Cal => {
|
||||
// Validate permissions
|
||||
let access_token =
|
||||
access_token.assert_has_permission(Permission::DavCalMkCol)?;
|
||||
|
||||
self.handle_calendar_mkcol_request(&access_token, headers, request)
|
||||
.await
|
||||
}
|
||||
DavResourceName::File => {
|
||||
// Validate permissions
|
||||
let access_token =
|
||||
access_token.assert_has_permission(Permission::DavFileMkCol)?;
|
||||
|
||||
self.handle_file_mkcol_request(&access_token, headers, request)
|
||||
.await
|
||||
}
|
||||
DavResourceName::Principal | DavResourceName::Scheduling => {
|
||||
Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))
|
||||
}
|
||||
}
|
||||
}
|
||||
DavMethod::DELETE => match resource {
|
||||
DavResourceName::Card => {
|
||||
// Validate permissions
|
||||
let access_token =
|
||||
access_token.assert_has_permission(Permission::DavCardDelete)?;
|
||||
|
||||
self.handle_card_delete_request(&access_token, headers)
|
||||
.await
|
||||
}
|
||||
DavResourceName::Cal => {
|
||||
// Validate permissions
|
||||
let access_token =
|
||||
access_token.assert_has_permission(Permission::DavCalDelete)?;
|
||||
|
||||
self.handle_calendar_delete_request(&access_token, headers)
|
||||
.await
|
||||
}
|
||||
DavResourceName::File => {
|
||||
// Validate permissions
|
||||
let access_token =
|
||||
access_token.assert_has_permission(Permission::DavFileDelete)?;
|
||||
|
||||
self.handle_file_delete_request(&access_token, headers)
|
||||
.await
|
||||
}
|
||||
DavResourceName::Scheduling => {
|
||||
// Validate permissions
|
||||
let access_token =
|
||||
access_token.assert_has_permission(Permission::DavCalDelete)?;
|
||||
|
||||
self.handle_scheduling_delete_request(&access_token, headers)
|
||||
.await
|
||||
}
|
||||
DavResourceName::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
|
||||
},
|
||||
DavMethod::PUT | DavMethod::POST | DavMethod::PATCH => match resource {
|
||||
DavResourceName::Card => {
|
||||
// Validate permissions
|
||||
let access_token =
|
||||
access_token.assert_has_permission(Permission::DavCardPut)?;
|
||||
|
||||
self.handle_card_update_request(
|
||||
&access_token,
|
||||
headers,
|
||||
body,
|
||||
matches!(method, DavMethod::PATCH),
|
||||
)
|
||||
.await
|
||||
}
|
||||
DavResourceName::Cal => {
|
||||
// Validate permissions
|
||||
let access_token = access_token.assert_has_permission(Permission::DavCalPut)?;
|
||||
|
||||
self.handle_calendar_update_request(
|
||||
&access_token,
|
||||
headers,
|
||||
body,
|
||||
matches!(method, DavMethod::PATCH),
|
||||
)
|
||||
.await
|
||||
}
|
||||
DavResourceName::File => {
|
||||
// Validate permissions
|
||||
let access_token =
|
||||
access_token.assert_has_permission(Permission::DavFilePut)?;
|
||||
|
||||
self.handle_file_update_request(
|
||||
&access_token,
|
||||
headers,
|
||||
body,
|
||||
matches!(method, DavMethod::PATCH),
|
||||
)
|
||||
.await
|
||||
}
|
||||
DavResourceName::Scheduling => {
|
||||
// Validate permissions
|
||||
let access_token =
|
||||
access_token.assert_has_permission(Permission::DavCalFreeBusyQuery)?;
|
||||
|
||||
self.handle_scheduling_post_request(&access_token, headers, body)
|
||||
.await
|
||||
}
|
||||
DavResourceName::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
|
||||
},
|
||||
DavMethod::COPY | DavMethod::MOVE => {
|
||||
let is_move = matches!(method, DavMethod::MOVE);
|
||||
match resource {
|
||||
DavResourceName::Card => {
|
||||
// Validate permissions
|
||||
let access_token = access_token.assert_has_permission(if is_move {
|
||||
Permission::DavCardMove
|
||||
} else {
|
||||
Permission::DavCardCopy
|
||||
})?;
|
||||
|
||||
self.handle_card_copy_move_request(&access_token, headers, is_move)
|
||||
.await
|
||||
}
|
||||
DavResourceName::Cal => {
|
||||
// Validate permissions
|
||||
let access_token = access_token.assert_has_permission(if is_move {
|
||||
Permission::DavCalMove
|
||||
} else {
|
||||
Permission::DavCalCopy
|
||||
})?;
|
||||
self.handle_calendar_copy_move_request(&access_token, headers, is_move)
|
||||
.await
|
||||
}
|
||||
DavResourceName::File => {
|
||||
// Validate permissions
|
||||
let access_token = access_token.assert_has_permission(if is_move {
|
||||
Permission::DavFileMove
|
||||
} else {
|
||||
Permission::DavFileCopy
|
||||
})?;
|
||||
|
||||
self.handle_file_copy_move_request(&access_token, headers, is_move)
|
||||
.await
|
||||
}
|
||||
DavResourceName::Principal | DavResourceName::Scheduling => {
|
||||
Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))
|
||||
}
|
||||
}
|
||||
}
|
||||
DavMethod::MKCALENDAR => match resource {
|
||||
DavResourceName::Cal => {
|
||||
// Validate permissions
|
||||
let access_token =
|
||||
access_token.assert_has_permission(Permission::DavCalMkCol)?;
|
||||
|
||||
self.handle_calendar_mkcol_request(
|
||||
&access_token,
|
||||
headers,
|
||||
Some(MkCol::parse(&mut Tokenizer::new(&body))?),
|
||||
)
|
||||
.await
|
||||
}
|
||||
_ => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
|
||||
},
|
||||
DavMethod::LOCK => {
|
||||
// Validate permissions
|
||||
let access_token = access_token.assert_has_permission(match resource {
|
||||
DavResourceName::File => Permission::DavFileLock,
|
||||
DavResourceName::Cal => Permission::DavCalLock,
|
||||
DavResourceName::Card => Permission::DavCardLock,
|
||||
_ => return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
|
||||
})?;
|
||||
|
||||
self.handle_lock_request(
|
||||
&access_token,
|
||||
headers,
|
||||
if !body.is_empty() {
|
||||
LockRequest::Lock(LockInfo::parse(&mut Tokenizer::new(&body))?)
|
||||
} else {
|
||||
LockRequest::Refresh
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
DavMethod::UNLOCK => {
|
||||
// Validate permissions
|
||||
let access_token = access_token.assert_has_permission(match resource {
|
||||
DavResourceName::File => Permission::DavFileLock,
|
||||
DavResourceName::Cal => Permission::DavCalLock,
|
||||
DavResourceName::Card => Permission::DavCardLock,
|
||||
_ => return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
|
||||
})?;
|
||||
|
||||
self.handle_lock_request(&access_token, headers, LockRequest::Unlock)
|
||||
.await
|
||||
}
|
||||
DavMethod::ACL => {
|
||||
// Validate permissions
|
||||
let access_token = access_token.assert_has_permission(match resource {
|
||||
DavResourceName::File => Permission::DavFileAcl,
|
||||
DavResourceName::Cal => Permission::DavCalAcl,
|
||||
DavResourceName::Card => Permission::DavCardAcl,
|
||||
_ => return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
|
||||
})?;
|
||||
|
||||
self.handle_acl_request(
|
||||
&access_token,
|
||||
headers,
|
||||
Acl::parse(&mut Tokenizer::new(&body))?,
|
||||
)
|
||||
.await
|
||||
}
|
||||
DavMethod::OPTIONS => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DavRequestHandler for Server {
|
||||
async fn handle_dav_request(
|
||||
&self,
|
||||
mut request: HttpRequest,
|
||||
access_token: AccessToken,
|
||||
session: &HttpSessionData,
|
||||
resource: DavResourceName,
|
||||
method: DavMethod,
|
||||
) -> HttpResponse {
|
||||
let body = if method.has_body()
|
||||
|| request
|
||||
.headers()
|
||||
.get(header::CONTENT_LENGTH)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
.is_some_and(|len| len > 0)
|
||||
{
|
||||
if let Some(body) = fetch_body(
|
||||
&mut request,
|
||||
if !access_token.has_permission(Permission::UnlimitedUploads) {
|
||||
self.core.groupware.max_request_size
|
||||
} else {
|
||||
0
|
||||
},
|
||||
session.session_id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
body
|
||||
} else {
|
||||
trc::event!(
|
||||
Limit(trc::LimitEvent::SizeRequest),
|
||||
SpanId = session.session_id,
|
||||
Contents = "Request body too large",
|
||||
);
|
||||
|
||||
return HttpResponse::new(StatusCode::PAYLOAD_TOO_LARGE);
|
||||
}
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
// Parse headers
|
||||
let mut headers = RequestHeaders::new(request.uri().path());
|
||||
for (key, value) in request.headers() {
|
||||
headers.parse(key.as_str(), value.to_str().unwrap_or_default());
|
||||
}
|
||||
|
||||
let start_time = Instant::now();
|
||||
match self
|
||||
.dispatch_dav_request(&headers, access_token, resource, method, body)
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
let event = WebDavEvent::from(method);
|
||||
|
||||
trc::event!(
|
||||
WebDav(event),
|
||||
SpanId = session.session_id,
|
||||
Url = headers.uri.to_compact_string(),
|
||||
Type = resource.name(),
|
||||
Details = &headers,
|
||||
Result = response.status().as_u16(),
|
||||
Elapsed = start_time.elapsed(),
|
||||
);
|
||||
|
||||
response
|
||||
}
|
||||
Err(DavError::Internal(err)) => {
|
||||
let err_type = err.event_type();
|
||||
|
||||
trc::error!(
|
||||
err.span_id(session.session_id)
|
||||
.ctx(trc::Key::Url, headers.uri.to_compact_string())
|
||||
.ctx(trc::Key::Type, resource.name())
|
||||
.ctx(trc::Key::Elapsed, start_time.elapsed())
|
||||
);
|
||||
|
||||
match err_type {
|
||||
EventType::Limit(LimitEvent::Quota | LimitEvent::TenantQuota) => {
|
||||
HttpResponse::new(StatusCode::PRECONDITION_FAILED)
|
||||
.with_xml_body(
|
||||
ErrorResponse::new(BaseCondition::QuotaNotExceeded)
|
||||
.with_namespace(match resource {
|
||||
DavResourceName::Card => Namespace::CardDav,
|
||||
DavResourceName::Cal | DavResourceName::Scheduling => {
|
||||
Namespace::CalDav
|
||||
}
|
||||
DavResourceName::File | DavResourceName::Principal => {
|
||||
Namespace::Dav
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.with_no_cache()
|
||||
}
|
||||
EventType::Store(StoreEvent::AssertValueFailed) => {
|
||||
HttpResponse::new(StatusCode::CONFLICT)
|
||||
}
|
||||
EventType::Security(_) => HttpResponse::new(StatusCode::FORBIDDEN),
|
||||
_ => HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
}
|
||||
}
|
||||
Err(DavError::Parse(err)) => {
|
||||
let result = if headers.content_type.is_some_and(|h| h.contains("/xml")) {
|
||||
StatusCode::BAD_REQUEST
|
||||
} else {
|
||||
StatusCode::UNSUPPORTED_MEDIA_TYPE
|
||||
};
|
||||
|
||||
trc::event!(
|
||||
WebDav(WebDavEvent::Error),
|
||||
SpanId = session.session_id,
|
||||
Url = headers.uri.to_compact_string(),
|
||||
Type = resource.name(),
|
||||
Details = &headers,
|
||||
Result = result.as_u16(),
|
||||
Reason = err.to_compact_string(),
|
||||
Elapsed = start_time.elapsed(),
|
||||
);
|
||||
|
||||
HttpResponse::new(result)
|
||||
}
|
||||
Err(DavError::Condition(condition)) => {
|
||||
let event = WebDavEvent::from(method);
|
||||
|
||||
trc::event!(
|
||||
WebDav(event),
|
||||
SpanId = session.session_id,
|
||||
Url = headers.uri.to_compact_string(),
|
||||
Type = resource.name(),
|
||||
Details = &headers,
|
||||
Code = condition.code.as_u16(),
|
||||
Result = CompactString::const_new(condition.condition.display_name()),
|
||||
Reason = condition.details,
|
||||
Elapsed = start_time.elapsed(),
|
||||
);
|
||||
|
||||
HttpResponse::new(condition.code)
|
||||
.with_xml_body(
|
||||
ErrorResponse::new(condition.condition)
|
||||
.with_namespace(match resource {
|
||||
DavResourceName::Card => Namespace::CardDav,
|
||||
DavResourceName::Cal | DavResourceName::Scheduling => {
|
||||
Namespace::CalDav
|
||||
}
|
||||
DavResourceName::File | DavResourceName::Principal => {
|
||||
Namespace::Dav
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.with_no_cache()
|
||||
}
|
||||
Err(DavError::Code(code)) => {
|
||||
let event = WebDavEvent::from(method);
|
||||
|
||||
trc::event!(
|
||||
WebDav(event),
|
||||
SpanId = session.session_id,
|
||||
Url = headers.uri.to_compact_string(),
|
||||
Type = resource.name(),
|
||||
Details = &headers,
|
||||
Result = code.as_u16(),
|
||||
Elapsed = start_time.elapsed(),
|
||||
);
|
||||
|
||||
HttpResponse::new(code)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<dav_proto::parser::Error> for DavError {
|
||||
fn from(err: dav_proto::parser::Error) -> Self {
|
||||
DavError::Parse(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<trc::Error> for DavError {
|
||||
fn from(err: trc::Error) -> Self {
|
||||
DavError::Internal(err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user