CalDAV: Allow organized properties to be present in PUT requests if they are equal to the existing ones

This commit is contained in:
Maurus Decimus
2026-04-18 16:56:35 +02:00
parent 11c58120cd
commit 7209c5fc06
4 changed files with 233 additions and 42 deletions

View File

@@ -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)
}

View File

@@ -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>
*

View File

@@ -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>,
}