Skip to content

Commit 05a46d2

Browse files
ndo84bwclaude
andcommitted
fix(dav): keep cancelled occurrence in iTip REQUEST so attendees keep it cancelled
When an organizer cancels a single occurrence of a recurring event, the broker emits a per-instance CANCEL plus a REQUEST for the attendee's remaining instances. The REQUEST omitted the cancelled instance, but processMessageRequest replaces all components of the attendee's stored object, so it dropped the CANCELLED override the CANCEL had just added and the occurrence reappeared as a normal event on the attendee's calendar. Keep the cancelled instance in the REQUEST so the override survives the component replace and the occurrence stays cancelled for attendees. Refs: nextcloud/calendar#6655 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Nico Donath <ndo84bw@gmx.de>
1 parent cbc8033 commit 05a46d2

2 files changed

Lines changed: 11 additions & 7 deletions

File tree

apps/dav/lib/CalDAV/TipBroker.php

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -319,13 +319,11 @@ protected function parseEventForOrganizer(VCalendar $calendar, array $eventInfo,
319319
}
320320
return $messages;
321321
}
322-
// detect if a new cancelled instance was created
323-
$cancelledNewInstances = [];
322+
// detect if a new cancelled instance was created and send a CANCEL for it
324323
if (isset($oldEventInfo['instances'])) {
325324
$instancesDelta = array_diff_key($eventInfo['instances'], $oldEventInfo['instances']);
326325
foreach ($instancesDelta as $id => $instance) {
327326
if ($instance->STATUS?->getValue() === 'CANCELLED') {
328-
$cancelledNewInstances[] = $id;
329327
foreach ($eventInfo['attendees'] as $attendee) {
330328
$messages[] = $this->generateMessage(
331329
[$id => $instance], $organizerHref, $organizerName, $attendee, $objectId, $objectType, $objectSequence, 'CANCEL', $template
@@ -366,10 +364,10 @@ protected function parseEventForOrganizer(VCalendar $calendar, array $eventInfo,
366364
// otherwise any created or modified instances will be sent as REQUEST
367365
$instances = array_intersect_key($eventInfo['instances'], array_flip(array_keys($eventInfo['attendees'][$attendee]['instances'])));
368366

369-
// Remove already-cancelled new instances from REQUEST
370-
if (!empty($cancelledNewInstances)) {
371-
$instances = array_diff_key($instances, array_flip($cancelledNewInstances));
372-
}
367+
// Keep newly cancelled instances IN the REQUEST. processMessageRequest replaces all
368+
// components of the attendee's stored object with the ones from the message, so a
369+
// REQUEST that omitted the cancelled instance would drop the CANCELLED override that
370+
// the accompanying CANCEL added and the occurrence would reappear as a normal event.
373371

374372
// Skip if no instances left to send
375373
if (empty($instances)) {

apps/dav/tests/unit/CalDAV/TipBrokerTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,12 @@ public function testParseEventForOrganizerCreatedInstanceCancelled(): void {
303303
$this->assertEquals($mutatedCalendar->VEVENT[1]->ATTENDEE[0]->getValue(), $messages[0]->recipient);
304304
$this->assertCount(1, $messages[0]->message->VEVENT);
305305
$this->assertEquals('20240715T080000', $messages[0]->message->VEVENT->{'RECURRENCE-ID'}->getValue());
306+
// the REQUEST keeps the cancelled instance, otherwise processMessageRequest (which replaces
307+
// all components) would drop the CANCELLED override on the attendee's copy (issue #6655)
308+
$this->assertEquals('REQUEST', $messages[1]->method);
309+
$this->assertCount(2, $messages[1]->message->VEVENT);
310+
$this->assertEquals('20240715T080000', $messages[1]->message->VEVENT[1]->{'RECURRENCE-ID'}->getValue());
311+
$this->assertEquals('CANCELLED', $messages[1]->message->VEVENT[1]->STATUS->getValue());
306312

307313
}
308314

0 commit comments

Comments
 (0)