CalDAV: Allow organized properties to be present in PUT requests if they are equal to the existing ones
This commit is contained in:
@@ -5,15 +5,15 @@
|
||||
*/
|
||||
|
||||
use crate::scheduling::{
|
||||
Email, InstanceId, ItipEntryValue, ItipError, ItipMessage, ItipSnapshot, ItipSnapshots,
|
||||
ItipSummary,
|
||||
Email, InstanceId, ItipEntry, ItipEntryValue, ItipError, ItipMessage, ItipSnapshot,
|
||||
ItipSnapshots, ItipSummary,
|
||||
itip::{
|
||||
ItipExportAs, can_attendee_modify_property, itip_add_tz, itip_build_envelope,
|
||||
itip_export_component,
|
||||
},
|
||||
organizer::organizer_request_full,
|
||||
};
|
||||
use ahash::AHashSet;
|
||||
use ahash::{AHashMap, AHashSet};
|
||||
use calcard::{
|
||||
common::PartialDateTime,
|
||||
icalendar::{
|
||||
@@ -46,6 +46,11 @@ pub(crate) fn attendee_handle_update(
|
||||
(Some(local_attendee), Some(old_local_attendee))
|
||||
if local_attendee.email == old_local_attendee.email =>
|
||||
{
|
||||
// Distinguish a genuine add/remove of a restricted property from a value-only drift
|
||||
// caused by a client re-encoding the same property
|
||||
let old_name_counts = count_entry_names(&old_instance.entries);
|
||||
let new_name_counts = count_entry_names(&instance.entries);
|
||||
|
||||
// Check added fields
|
||||
let mut send_update = false;
|
||||
for new_entry in instance.entries.difference(&old_instance.entries) {
|
||||
@@ -80,9 +85,13 @@ pub(crate) fn attendee_handle_update(
|
||||
&instance.comp.component_type,
|
||||
new_entry.name,
|
||||
) {
|
||||
return Err(ItipError::CannotModifyProperty(
|
||||
new_entry.name.clone(),
|
||||
));
|
||||
if name_count(&new_name_counts, new_entry.name)
|
||||
> name_count(&old_name_counts, new_entry.name)
|
||||
{
|
||||
return Err(ItipError::CannotModifyProperty(
|
||||
new_entry.name.clone(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
send_update = send_update
|
||||
|| (instance.comp.component_type
|
||||
@@ -149,7 +158,9 @@ pub(crate) fn attendee_handle_update(
|
||||
if !can_attendee_modify_property(
|
||||
&instance.comp.component_type,
|
||||
removed_entry.name,
|
||||
) {
|
||||
) && name_count(&old_name_counts, removed_entry.name)
|
||||
> name_count(&new_name_counts, removed_entry.name)
|
||||
{
|
||||
// Removing these properties is not allowed
|
||||
return Err(ItipError::CannotModifyProperty(
|
||||
removed_entry.name.clone(),
|
||||
@@ -351,3 +362,18 @@ pub(crate) fn attendee_decline<'x>(
|
||||
(cancel_comp, &local_attendee.email)
|
||||
})
|
||||
}
|
||||
|
||||
fn count_entry_names<'a>(
|
||||
entries: &'a AHashSet<ItipEntry<'a>>,
|
||||
) -> AHashMap<&'a ICalendarProperty, usize> {
|
||||
let mut counts = AHashMap::with_capacity(entries.len());
|
||||
for entry in entries {
|
||||
*counts.entry(entry.name).or_insert(0) += 1;
|
||||
}
|
||||
counts
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn name_count(counts: &AHashMap<&ICalendarProperty, usize>, name: &ICalendarProperty) -> usize {
|
||||
counts.get(name).copied().unwrap_or(0)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ use registry::{
|
||||
types::{EnumImpl, datetime::UTCDateTime},
|
||||
};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fs::{self, File},
|
||||
io::{self, BufRead, BufReader, Read, Seek, SeekFrom},
|
||||
path::Path,
|
||||
@@ -267,6 +268,7 @@ fn read_log_entries(
|
||||
}
|
||||
|
||||
fn log_from_line(line: &str) -> Option<Log> {
|
||||
let line = strip_ansi(line);
|
||||
let (timestamp, rest) = line.split_once(' ')?;
|
||||
let timestamp = DateTime::parse_from_rfc3339(timestamp).ok()?;
|
||||
let (level, rest) = rest.trim().split_once(' ')?;
|
||||
@@ -281,6 +283,43 @@ fn log_from_line(line: &str) -> Option<Log> {
|
||||
})
|
||||
}
|
||||
|
||||
fn strip_ansi(line: &str) -> Cow<'_, str> {
|
||||
if !line.contains('\x1b') {
|
||||
return Cow::Borrowed(line);
|
||||
}
|
||||
|
||||
let mut out = String::with_capacity(line.len());
|
||||
let mut chars = line.chars();
|
||||
while let Some(c) = chars.next() {
|
||||
if c != '\x1b' {
|
||||
out.push(c);
|
||||
continue;
|
||||
}
|
||||
match chars.next() {
|
||||
Some('[') => {
|
||||
for c in chars.by_ref() {
|
||||
if matches!(c as u32, 0x40..=0x7e) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(']') => {
|
||||
while let Some(c) = chars.next() {
|
||||
if c == '\x07' {
|
||||
break;
|
||||
}
|
||||
if c == '\x1b' {
|
||||
chars.next();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Cow::Owned(out)
|
||||
}
|
||||
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2017 Michael Coyne <mjc@hey.com>
|
||||
*
|
||||
|
||||
@@ -62,17 +62,7 @@ pub trait SieveScriptSet: Sync + Send {
|
||||
update: Option<(u32, Archive<&'x ArchivedSieveScript>)>,
|
||||
ctx: &SetContext,
|
||||
session_id: u64,
|
||||
) -> impl Future<
|
||||
Output = trc::Result<
|
||||
Result<
|
||||
(
|
||||
ObjectIndexBuilder<&'x ArchivedSieveScript, SieveScript>,
|
||||
Option<Vec<u8>>,
|
||||
),
|
||||
SetError<SieveProperty>,
|
||||
>,
|
||||
>,
|
||||
> + Send;
|
||||
) -> impl Future<Output = trc::Result<Result<SetItemResponse<'x>, SetError<SieveProperty>>>> + Send;
|
||||
}
|
||||
|
||||
impl SieveScriptSet for Server {
|
||||
@@ -112,6 +102,7 @@ impl SieveScriptSet for Server {
|
||||
|
||||
// Process creates
|
||||
let mut batch = BatchBuilder::new();
|
||||
let mut activations = Vec::new();
|
||||
for (id, object) in request.unwrap_create() {
|
||||
if sieve_ids.len()
|
||||
< self.object_quota(account.object_quotas(), StorageQuota::MaxSieveScripts) as u64
|
||||
@@ -120,11 +111,16 @@ impl SieveScriptSet for Server {
|
||||
.sieve_set_item(object, None, &ctx, session.session_id)
|
||||
.await?
|
||||
{
|
||||
Ok((mut builder, Some(blob))) => {
|
||||
Ok(mut result) => {
|
||||
// Store blob
|
||||
let sieve = &mut builder.changes_mut().unwrap();
|
||||
let (blob_hash, blob_hold) =
|
||||
self.put_temporary_blob(account_id, &blob, 60).await?;
|
||||
let sieve = &mut result.builder.changes_mut().unwrap();
|
||||
let (blob_hash, blob_hold) = self
|
||||
.put_temporary_blob(
|
||||
account_id,
|
||||
result.blob_update.as_ref().unwrap(),
|
||||
60,
|
||||
)
|
||||
.await?;
|
||||
sieve.blob_hash = blob_hash;
|
||||
let blob_size = sieve.size as usize;
|
||||
let blob_hash = sieve.blob_hash.clone();
|
||||
@@ -139,11 +135,20 @@ impl SieveScriptSet for Server {
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::SieveScript)
|
||||
.with_document(document_id)
|
||||
.custom(builder.with_changed_by(ctx.access_token.account_tenant_ids()))
|
||||
.custom(
|
||||
result
|
||||
.builder
|
||||
.with_changed_by(ctx.access_token.account_tenant_ids()),
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.clear(blob_hold)
|
||||
.commit_point();
|
||||
|
||||
// Set isActive if needed
|
||||
if let Some(set_item) = result.set_item {
|
||||
activations.push((document_id, set_item));
|
||||
}
|
||||
|
||||
let mut result = Map::with_capacity(1)
|
||||
.with_key_value(SieveProperty::Id, SieveValue::Id(document_id.into()))
|
||||
.with_key_value(
|
||||
@@ -179,7 +184,6 @@ impl SieveScriptSet for Server {
|
||||
Err(err) => {
|
||||
ctx.response.not_created.append(id, err);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
} else {
|
||||
ctx.response.not_created.append(
|
||||
@@ -226,16 +230,16 @@ impl SieveScriptSet for Server {
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Ok((mut builder, blob)) => {
|
||||
Ok(mut result) => {
|
||||
// Prepare write batch
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::SieveScript)
|
||||
.with_document(document_id);
|
||||
|
||||
let blob_id = if let Some(blob) = blob {
|
||||
let blob_id = if let Some(blob) = result.blob_update.take() {
|
||||
// Store blob
|
||||
let sieve = &mut builder.changes_mut().unwrap();
|
||||
let sieve = &mut result.builder.changes_mut().unwrap();
|
||||
let (blob_hash, blob_hold) =
|
||||
self.put_temporary_blob(account_id, &blob, 60).await?;
|
||||
sieve.blob_hash = blob_hash;
|
||||
@@ -259,9 +263,18 @@ impl SieveScriptSet for Server {
|
||||
None
|
||||
};
|
||||
|
||||
// Set isActive if needed
|
||||
if let Some(set_item) = result.set_item {
|
||||
activations.push((document_id, set_item));
|
||||
}
|
||||
|
||||
// Write record
|
||||
batch
|
||||
.custom(builder.with_changed_by(ctx.access_token.account_tenant_ids()))
|
||||
.custom(
|
||||
result
|
||||
.builder
|
||||
.with_changed_by(ctx.access_token.account_tenant_ids()),
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.commit_point();
|
||||
|
||||
@@ -328,11 +341,25 @@ impl SieveScriptSet for Server {
|
||||
}
|
||||
}
|
||||
|
||||
// Activate / deactivate scripts
|
||||
let on_success_deactivate_script = request
|
||||
// Non-standard script activation handling
|
||||
let mut on_success_deactivate_script = request
|
||||
.arguments
|
||||
.on_success_deactivate_script
|
||||
.unwrap_or(false);
|
||||
if activations.len() == 1 {
|
||||
let (document_id, set_item) = activations[0];
|
||||
let is_active = active_script_id.is_some_and(|active_id| active_id == document_id);
|
||||
if set_item {
|
||||
if request.arguments.on_success_activate_script.is_none() && !is_active {
|
||||
request.arguments.on_success_activate_script =
|
||||
Some(MaybeIdReference::Id(document_id.into()));
|
||||
}
|
||||
} else if !on_success_deactivate_script && is_active {
|
||||
on_success_deactivate_script = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Activate / deactivate scripts
|
||||
if ctx.response.not_created.is_empty()
|
||||
&& ctx.response.not_updated.is_empty()
|
||||
&& ctx.response.not_destroyed.is_empty()
|
||||
@@ -375,15 +402,7 @@ impl SieveScriptSet for Server {
|
||||
update: Option<(u32, Archive<&'x ArchivedSieveScript>)>,
|
||||
ctx: &SetContext<'_>,
|
||||
session_id: u64,
|
||||
) -> trc::Result<
|
||||
Result<
|
||||
(
|
||||
ObjectIndexBuilder<&'x ArchivedSieveScript, SieveScript>,
|
||||
Option<Vec<u8>>,
|
||||
),
|
||||
SetError<SieveProperty>,
|
||||
>,
|
||||
> {
|
||||
) -> trc::Result<Result<SetItemResponse<'x>, SetError<SieveProperty>>> {
|
||||
// Vacation script cannot be modified
|
||||
if update
|
||||
.as_ref()
|
||||
@@ -396,6 +415,7 @@ impl SieveScriptSet for Server {
|
||||
}
|
||||
|
||||
// Parse properties
|
||||
let mut set_item = None;
|
||||
let mut changes = update
|
||||
.as_ref()
|
||||
.map(|(_, obj)| obj.deserialize().unwrap_or_default())
|
||||
@@ -450,6 +470,10 @@ impl SieveScriptSet for Server {
|
||||
(Key::Property(SieveProperty::Name), Value::Null) => {
|
||||
continue;
|
||||
}
|
||||
(Key::Property(SieveProperty::IsActive), Value::Bool(value)) => {
|
||||
set_item = Some(value);
|
||||
continue;
|
||||
}
|
||||
_ => {
|
||||
return Ok(Err(SetError::invalid_properties()
|
||||
.with_property(property.into_owned())
|
||||
@@ -528,11 +552,18 @@ impl SieveScriptSet for Server {
|
||||
};
|
||||
|
||||
// Validate
|
||||
Ok(Ok((
|
||||
ObjectIndexBuilder::new()
|
||||
Ok(Ok(SetItemResponse {
|
||||
builder: ObjectIndexBuilder::new()
|
||||
.with_changes(changes)
|
||||
.with_current_opt(update.map(|(_, current)| current)),
|
||||
blob_update,
|
||||
)))
|
||||
set_item,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SetItemResponse<'x> {
|
||||
builder: ObjectIndexBuilder<&'x ArchivedSieveScript, SieveScript>,
|
||||
blob_update: Option<Vec<u8>>,
|
||||
set_item: Option<bool>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user