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,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"),
|
||||
))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user