fix(CalDAV): iTipBroker message generation and testing
Summary
Resolves: https://github.com/nextcloud/calendar/issues/7057 Refactored iTip broker message generation to improve logic and RFC compatibility
Testing
How to test, open calendar app, and test the following
🔵 Single Event Operations
Basic Lifecycle
- [ ] Create new event with attendees
- [ ] Update event
- [ ] Delete event
- [ ] Cancel event (STATUS=CANCELLED)
Attendee Management
- [ ] Add new attendee to existing event
- [ ] Remove attendee from event
- [ ] Remove all attendees (convert to non-scheduling)
🟣 Recurring Event Operations
Master Instance
- [ ] Create recurring event (daily/weekly/monthly)
- [ ] Update master event
- [ ] Cancel master instance (entire series)
- [ ] Delete master instance
- [ ] Add EXDATE to master
- [ ] Remove EXDATE from master
- [ ] Convert recurring event to non-scheduling
Exception Instances
- [ ] Create exception (modify single occurrence time)
- [ ] Create exception (modify single occurrence summary)
- [ ] Update existing exception
- [ ] Cancel single occurrence (create cancelled exception)
- [ ] Create new exception that is already cancelled
🟠 Partial Attendee Lists (EXDATE Logic)
Critical EXDATE Scenarios
- [ ] Remove attendee from single occurrence (verify EXDATE in their REQUEST)
- [ ] Add attendee to single occurrence only (verify they get only exception)
- [ ] Different attendees on different exceptions:
- [ ] Master: A + B + C
- [ ] Exception 1: A + B only
- [ ] Exception 2: A + C only
- [ ] Verify A gets master + both exceptions
- [ ] Verify B gets master + exception 1 + EXDATE for exception 2
- [ ] Verify C gets master + exception 2 + EXDATE for exception 1
🔴 Edge Cases
Organizer & Empty States
- [ ] Organizer is also an attendee (verify organizer doesn't receive message)
- [ ] Create event with no attendees
- [ ] Remove all attendees from event
✅ Verification Per Test
For each test above, verify:
Message Method
- [ ] REQUEST for updates/additions
- [ ] CANCEL for removals/cancellations
Recipients
- [ ] All appropriate attendees receive messages
- [ ] No duplicate messages sent
- [ ] Organizer doesn't receive self-invites
- [ ] SCHEDULE-AGENT=CLIENT attendees skipped
Message Content
- [ ] All properties correctly reflected
- [ ] EXDATE properly added for partial attendee lists
Instance Selection
- [ ] Master included when appropriate
- [ ] Exceptions included when appropriate
- [ ] Cancelled instances not in REQUEST (only in CANCEL)
TODO
- [x] Add phpdoc to function
- [x] Add phpdoc parameter definitions for arrays
Checklist
- Code is properly formatted
- Sign-off message is added to all commits
- [x] Tests (unit, integration, api and/or acceptance) are included
- [ ] Screenshots before/after for front-end changes
- [ ] Documentation (manuals or wiki) has been updated or is not required
- [ ] Backports requested where applicable (ex: critical bugfixes)
Is this intended?
I don't think this has anything to do with this PR.
- Organizer invites attendee (recurring, daily, 5 times).
This should work just like any other scenario, I tested this on my end and the iTip message are being generated properly.
Example,
- Attendee declines the second to last instance.
This PR does not touch the Attendee portion of the iTipBroker, that is done in the parseEventForAttendee function and this only overloads the parseEventForOrganizer function which generates messages for organizer changes.
In general, no messages seem to get sent if an attendee accepts or declines a single instance.
I had the same problem, this is most likely a issue with your DEV instance, when our server tests run they empty out the appdata_* folder, this is the place where the contents of a sent message is saved and if the "appdata_/mail/mail_user" folder is missing the send process fails.
/backport to stable31
/backport to stable30
Does not fix cancelling a single instance.
Consider two accounts: organizer and user.
1. Organizer: Create recurring event, daily, 3 times and invite another user. 2. User: Accept whole series. 3. Organizer: Cancels an instance, e.g. the second one.Expected: User will only see the second instance being cancelled. Actual: The whole series is cancelled from user's POV.
I exported the events and was able to confirm that the organizer's event has a new instance with a cancelled state. The user's event however, does not contain a separate instance. Instead, the base event was cancelled.
Okay, having a look. Disregard the last message, I tested the wrong thing
Does not fix cancelling a single instance.
Consider two accounts: organizer and user.
1. Organizer: Create recurring event, daily, 3 times and invite another user. 2. User: Accept whole series. 3. Organizer: Cancels an instance, e.g. the second one.Expected: User will only see the second instance being cancelled. Actual: The whole series is cancelled from user's POV.
I exported the events and was able to confirm that the organizer's event has a new instance with a cancelled state. The user's event however, does not contain a separate instance. Instead, the base event was cancelled.
LMAO. So I found the issue... You've found another bug that I didn't know existed...
So the iTipBroker is now generating proper messages for instances, but its not processing Cancellation instances properly...
The method at fault... Sabre\VObject\ITip\Broker::processMessageCancel()
protected function processMessageCancel(Message $itipMessage, ?VCalendar $existingObject = null)
{
if (!$existingObject) {
// The event didn't exist in the first place, so we're just
// ignoring this message.
} else {
foreach ($existingObject->VEVENT as $vevent) {
$vevent->STATUS = 'CANCELLED';
$vevent->SEQUENCE = $itipMessage->sequence;
}
}
return $existingObject;
}
As you can see it does not check the iTipMessage for a RECURRANCE-ID, it just applies the cancellation to every instance...
Remove EXDATE from master
How do I do this from the Calendar UI?
Remove EXDATE from master
How do I do this from the Calendar UI?
That one you actually can't do through the UI, we don't support that, it can be skipped, or you can do a manual PUT request to trigger it.