mirror of
https://github.com/nextcloud/server.git
synced 2026-02-19 02:38:40 -05:00
fix: iTipBroker message generation and testing
Signed-off-by: SebastianKrupinski <krupinskis05@gmail.com>
This commit is contained in:
parent
4cc2bb26ca
commit
70d051f602
2 changed files with 763 additions and 248 deletions
|
|
@ -9,6 +9,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace OCA\DAV\CalDAV;
|
||||
|
||||
use Sabre\VObject\Component;
|
||||
use Sabre\VObject\Component\VCalendar;
|
||||
use Sabre\VObject\ITip\Broker;
|
||||
use Sabre\VObject\ITip\Message;
|
||||
|
|
@ -27,9 +28,56 @@ class TipBroker extends Broker {
|
|||
'SUMMARY',
|
||||
'DESCRIPTION',
|
||||
'LOCATION',
|
||||
|
||||
];
|
||||
|
||||
/**
|
||||
* Processes incoming CANCEL messages.
|
||||
*
|
||||
* This is a message from an organizer, and means that either an
|
||||
* attendee got removed from an event, or an event got cancelled
|
||||
* altogether.
|
||||
*
|
||||
* @param VCalendar $existingObject
|
||||
*
|
||||
* @return VCalendar|null
|
||||
*/
|
||||
protected function processMessageCancel(Message $itipMessage, ?VCalendar $existingObject = null) {
|
||||
if ($existingObject === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$componentType = $itipMessage->component;
|
||||
$instances = [];
|
||||
|
||||
foreach ($itipMessage->message->$componentType as $component) {
|
||||
$instanceId = isset($component->{'RECURRENCE-ID'}) ? $component->{'RECURRENCE-ID'}->getValue() : 'base';
|
||||
$instances[$instanceId] = $component;
|
||||
}
|
||||
// any existing instances should be marked as cancelled
|
||||
foreach ($existingObject->$componentType as $component) {
|
||||
$instanceId = isset($component->{'RECURRENCE-ID'}) ? $component->{'RECURRENCE-ID'}->getValue() : 'base';
|
||||
if (isset($instances[$instanceId])) {
|
||||
if (isset($component->STATUS)) {
|
||||
$component->STATUS->setValue('CANCELLED');
|
||||
} else {
|
||||
$component->add('STATUS', 'CANCELLED');
|
||||
}
|
||||
if (isset($component->SEQUENCE)) {
|
||||
$component->SEQUENCE->setValue($itipMessage->sequence);
|
||||
} else {
|
||||
$component->add('SEQUENCE', $itipMessage->sequence);
|
||||
}
|
||||
unset($instances[$instanceId]);
|
||||
}
|
||||
}
|
||||
// any remaining instances are new and should be added
|
||||
foreach ($instances as $instance) {
|
||||
$existingObject->add($instance);
|
||||
}
|
||||
|
||||
return $existingObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used in cases where an event got updated, and we
|
||||
* potentially need to send emails to attendees to let them know of updates
|
||||
|
|
@ -38,154 +86,219 @@ class TipBroker extends Broker {
|
|||
* We will detect which attendees got added, which got removed and create
|
||||
* specific messages for these situations.
|
||||
*
|
||||
* @return array
|
||||
* @return array<int,Message>
|
||||
*/
|
||||
protected function parseEventForOrganizer(VCalendar $calendar, array $eventInfo, array $oldEventInfo) {
|
||||
// Merging attendee lists.
|
||||
$attendees = [];
|
||||
foreach ($oldEventInfo['attendees'] as $attendee) {
|
||||
$attendees[$attendee['href']] = [
|
||||
'href' => $attendee['href'],
|
||||
'oldInstances' => $attendee['instances'],
|
||||
'newInstances' => [],
|
||||
'name' => $attendee['name'],
|
||||
'forceSend' => null,
|
||||
];
|
||||
}
|
||||
foreach ($eventInfo['attendees'] as $attendee) {
|
||||
if (isset($attendees[$attendee['href']])) {
|
||||
$attendees[$attendee['href']]['name'] = $attendee['name'];
|
||||
$attendees[$attendee['href']]['newInstances'] = $attendee['instances'];
|
||||
$attendees[$attendee['href']]['forceSend'] = $attendee['forceSend'];
|
||||
} else {
|
||||
$attendees[$attendee['href']] = [
|
||||
'href' => $attendee['href'],
|
||||
'oldInstances' => [],
|
||||
'newInstances' => $attendee['instances'],
|
||||
'name' => $attendee['name'],
|
||||
'forceSend' => $attendee['forceSend'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$messages = [];
|
||||
|
||||
// construct template calendar from original calendar without components
|
||||
$template = new VCalendar();
|
||||
foreach ($template->children() as $property) {
|
||||
$template->remove($property);
|
||||
}
|
||||
foreach ($calendar->children() as $property) {
|
||||
if (in_array($property->name, ['METHOD', 'VEVENT', 'VTODO', 'VJOURNAL', 'VFREEBUSY'], true) === false) {
|
||||
$template->add(clone $property);
|
||||
}
|
||||
}
|
||||
// extract event information
|
||||
$objectId = $eventInfo['uid'];
|
||||
if ($calendar->getBaseComponent() === null) {
|
||||
$objectType = $calendar->getComponents()[0]->name;
|
||||
} else {
|
||||
$objectType = $calendar->getBaseComponent()->name;
|
||||
}
|
||||
$objectSequence = $eventInfo['sequence'] ?? 1;
|
||||
$organizerHref = $eventInfo['organizer'] ?? $oldEventInfo['organizer'];
|
||||
if ($eventInfo['organizerName'] instanceof \Sabre\VObject\Parameter) {
|
||||
$organizerName = $eventInfo['organizerName']->getValue();
|
||||
} else {
|
||||
$organizerName = $eventInfo['organizerName'];
|
||||
}
|
||||
// detect if the singleton or recurring base instance was converted to non-scheduling
|
||||
if (count($eventInfo['instances']) === 0 && count($oldEventInfo['instances']) > 0) {
|
||||
foreach ($oldEventInfo['attendees'] as $attendee) {
|
||||
$messages[] = $this->generateMessage(
|
||||
$oldEventInfo['instances'], $organizerHref, $organizerName, $attendee, $objectId, $objectType, $objectSequence, 'CANCEL', $template
|
||||
);
|
||||
}
|
||||
return $messages;
|
||||
}
|
||||
// detect if the singleton or recurring base instance was cancelled
|
||||
if ($eventInfo['instances']['master']?->STATUS?->getValue() === 'CANCELLED' && $oldEventInfo['instances']['master']?->STATUS?->getValue() !== 'CANCELLED') {
|
||||
foreach ($eventInfo['attendees'] as $attendee) {
|
||||
$messages[] = $this->generateMessage(
|
||||
$eventInfo['instances'], $organizerHref, $organizerName, $attendee, $objectId, $objectType, $objectSequence, 'CANCEL', $template
|
||||
);
|
||||
}
|
||||
return $messages;
|
||||
}
|
||||
// detect if a new cancelled instance was created
|
||||
$cancelledNewInstances = [];
|
||||
if (isset($oldEventInfo['instances'])) {
|
||||
$instancesDelta = array_diff_key($eventInfo['instances'], $oldEventInfo['instances']);
|
||||
foreach ($instancesDelta as $id => $instance) {
|
||||
if ($instance->STATUS?->getValue() === 'CANCELLED') {
|
||||
$cancelledNewInstances[] = $id;
|
||||
foreach ($eventInfo['attendees'] as $attendee) {
|
||||
$messages[] = $this->generateMessage(
|
||||
[$id => $instance], $organizerHref, $organizerName, $attendee, $objectId, $objectType, $objectSequence, 'CANCEL', $template
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// detect attendee mutations
|
||||
$attendees = array_unique(
|
||||
array_merge(
|
||||
array_keys($eventInfo['attendees']),
|
||||
array_keys($oldEventInfo['attendees'])
|
||||
)
|
||||
);
|
||||
foreach ($attendees as $attendee) {
|
||||
// An organizer can also be an attendee. We should not generate any
|
||||
// messages for those.
|
||||
if ($attendee['href'] === $eventInfo['organizer']) {
|
||||
// Skip organizer
|
||||
if ($attendee === $organizerHref) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$message = new Message();
|
||||
$message->uid = $eventInfo['uid'];
|
||||
$message->component = 'VEVENT';
|
||||
$message->sequence = $eventInfo['sequence'];
|
||||
$message->sender = $eventInfo['organizer'];
|
||||
$message->senderName = $eventInfo['organizerName'];
|
||||
$message->recipient = $attendee['href'];
|
||||
$message->recipientName = $attendee['name'];
|
||||
|
||||
// Creating the new iCalendar body.
|
||||
$icalMsg = new VCalendar();
|
||||
|
||||
foreach ($calendar->select('VTIMEZONE') as $timezone) {
|
||||
$icalMsg->add(clone $timezone);
|
||||
// Skip if SCHEDULE-AGENT=CLIENT (respect RFC 6638)
|
||||
if ($this->scheduleAgentServerRules
|
||||
&& isset($eventInfo['attendees'][$attendee]['scheduleAgent'])
|
||||
&& strtoupper($eventInfo['attendees'][$attendee]['scheduleAgent']) === 'CLIENT') {
|
||||
continue;
|
||||
}
|
||||
// If there are no instances the attendee is a part of, it means
|
||||
// the attendee was removed and we need to send them a CANCEL message.
|
||||
// Also If the meeting STATUS property was changed to CANCELLED
|
||||
// we need to send the attendee a CANCEL message.
|
||||
if (!$attendee['newInstances'] || $eventInfo['status'] === 'CANCELLED') {
|
||||
|
||||
$message->method = $icalMsg->METHOD = 'CANCEL';
|
||||
$message->significantChange = true;
|
||||
// clone base event
|
||||
if (isset($eventInfo['instances']['master'])) {
|
||||
$event = clone $eventInfo['instances']['master'];
|
||||
} else {
|
||||
$event = clone $oldEventInfo['instances']['master'];
|
||||
}
|
||||
// alter some properties
|
||||
unset($event->ATTENDEE);
|
||||
$event->add('ATTENDEE', $attendee['href'], ['CN' => $attendee['name'],]);
|
||||
$event->DTSTAMP = gmdate('Ymd\\THis\\Z');
|
||||
$event->SEQUENCE = $message->sequence;
|
||||
$icalMsg->add($event);
|
||||
// detect if attendee was removed and send cancel message
|
||||
if (!isset($eventInfo['attendees'][$attendee]) && isset($oldEventInfo['attendees'][$attendee])) {
|
||||
//get all instances of the attendee was removed from.
|
||||
$instances = array_intersect_key($oldEventInfo['instances'], array_flip(array_keys($oldEventInfo['attendees'][$attendee]['instances'])));
|
||||
$messages[] = $this->generateMessage(
|
||||
$instances, $organizerHref, $organizerName, $oldEventInfo['attendees'][$attendee], $objectId, $objectType, $objectSequence, 'CANCEL', $template
|
||||
);
|
||||
continue;
|
||||
}
|
||||
// otherwise any created or modified instances will be sent as REQUEST
|
||||
$instances = array_intersect_key($eventInfo['instances'], array_flip(array_keys($eventInfo['attendees'][$attendee]['instances'])));
|
||||
|
||||
} else {
|
||||
// The attendee gets the updated event body
|
||||
$message->method = $icalMsg->METHOD = 'REQUEST';
|
||||
// Remove already-cancelled new instances from REQUEST
|
||||
if (!empty($cancelledNewInstances)) {
|
||||
$instances = array_diff_key($instances, array_flip($cancelledNewInstances));
|
||||
}
|
||||
|
||||
// We need to find out that this change is significant. If it's
|
||||
// not, systems may opt to not send messages.
|
||||
//
|
||||
// We do this based on the 'significantChangeHash' which is
|
||||
// some value that changes if there's a certain set of
|
||||
// properties changed in the event, or simply if there's a
|
||||
// difference in instances that the attendee is invited to.
|
||||
// Skip if no instances left to send
|
||||
if (empty($instances)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$oldAttendeeInstances = array_keys($attendee['oldInstances']);
|
||||
$newAttendeeInstances = array_keys($attendee['newInstances']);
|
||||
// Add EXDATE for instances the attendee is NOT part of (only for recurring events with master)
|
||||
if (isset($instances['master']) && count($eventInfo['instances']) > 1) {
|
||||
$masterInstance = clone $instances['master'];
|
||||
$excludedDates = [];
|
||||
|
||||
$message->significantChange
|
||||
= $attendee['forceSend'] === 'REQUEST'
|
||||
|| count($oldAttendeeInstances) !== count($newAttendeeInstances)
|
||||
|| count(array_diff($oldAttendeeInstances, $newAttendeeInstances)) > 0
|
||||
|| $oldEventInfo['significantChangeHash'] !== $eventInfo['significantChangeHash'];
|
||||
|
||||
foreach ($attendee['newInstances'] as $instanceId => $instanceInfo) {
|
||||
$currentEvent = clone $eventInfo['instances'][$instanceId];
|
||||
if ($instanceId === 'master') {
|
||||
// We need to find a list of events that the attendee
|
||||
// is not a part of to add to the list of exceptions.
|
||||
$exceptions = [];
|
||||
foreach ($eventInfo['instances'] as $instanceId => $vevent) {
|
||||
if (!isset($attendee['newInstances'][$instanceId])) {
|
||||
$exceptions[] = $instanceId;
|
||||
}
|
||||
}
|
||||
|
||||
// If there were exceptions, we need to add it to an
|
||||
// existing EXDATE property, if it exists.
|
||||
if ($exceptions) {
|
||||
if (isset($currentEvent->EXDATE)) {
|
||||
$currentEvent->EXDATE->setParts(array_merge(
|
||||
$currentEvent->EXDATE->getParts(),
|
||||
$exceptions
|
||||
));
|
||||
} else {
|
||||
$currentEvent->EXDATE = $exceptions;
|
||||
}
|
||||
}
|
||||
|
||||
// Cleaning up any scheduling information that
|
||||
// shouldn't be sent along.
|
||||
unset($currentEvent->ORGANIZER['SCHEDULE-FORCE-SEND']);
|
||||
unset($currentEvent->ORGANIZER['SCHEDULE-STATUS']);
|
||||
|
||||
foreach ($currentEvent->ATTENDEE as $attendee) {
|
||||
unset($attendee['SCHEDULE-FORCE-SEND']);
|
||||
unset($attendee['SCHEDULE-STATUS']);
|
||||
|
||||
// We're adding PARTSTAT=NEEDS-ACTION to ensure that
|
||||
// iOS shows an "Inbox Item"
|
||||
if (!isset($attendee['PARTSTAT'])) {
|
||||
$attendee['PARTSTAT'] = 'NEEDS-ACTION';
|
||||
}
|
||||
}
|
||||
foreach ($eventInfo['instances'] as $instanceId => $instance) {
|
||||
if ($instanceId !== 'master' && !isset($eventInfo['attendees'][$attendee]['instances'][$instanceId])) {
|
||||
$excludedDates[] = $instance->{'RECURRENCE-ID'}->getValue();
|
||||
}
|
||||
}
|
||||
|
||||
$currentEvent->DTSTAMP = gmdate('Ymd\\THis\\Z');
|
||||
$icalMsg->add($currentEvent);
|
||||
if (!empty($excludedDates)) {
|
||||
if (isset($masterInstance->EXDATE)) {
|
||||
$currentExdates = $masterInstance->EXDATE->getParts();
|
||||
$masterInstance->EXDATE->setParts(array_merge($currentExdates, $excludedDates));
|
||||
} else {
|
||||
$masterInstance->EXDATE = $excludedDates;
|
||||
}
|
||||
$instances['master'] = $masterInstance;
|
||||
}
|
||||
}
|
||||
|
||||
$message->message = $icalMsg;
|
||||
$messages[] = $message;
|
||||
$messages[] = $this->generateMessage(
|
||||
$instances, $organizerHref, $organizerName, $eventInfo['attendees'][$attendee], $objectId, $objectType, $objectSequence, 'REQUEST', $template
|
||||
);
|
||||
}
|
||||
|
||||
return $messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an iTip message for a specific attendee
|
||||
*
|
||||
* @param array<string, Component> $instances Array of event instances to include, keyed by instance ID:
|
||||
* - 'master' => Component: The master/base event
|
||||
* - '{RECURRENCE-ID}' => Component: Exception instances
|
||||
* @param string $organizerHref The organizer's calendar-user address (e.g., 'mailto:user@example.com')
|
||||
* @param string|null $organizerName The organizer's display name
|
||||
* @param array $attendee The attendee information containing:
|
||||
* - 'href' (string): The attendee's calendar-user address
|
||||
* - 'name' (string): The attendee's display name
|
||||
* - 'scheduleAgent' (string|null): SCHEDULE-AGENT parameter
|
||||
* - 'instances' (array): Instances this attendee is part of
|
||||
* @param string $objectId The UID of the event
|
||||
* @param string $objectType The component type ('VEVENT', 'VTODO', etc.)
|
||||
* @param int $objectSequence The sequence number of the event
|
||||
* @param string $method The iTip method ('REQUEST', 'CANCEL', 'REPLY', etc.)
|
||||
* @param VCalendar $template The template calendar object (without event components)
|
||||
* @return Message The generated iTip message ready to be sent
|
||||
*/
|
||||
protected function generateMessage(
|
||||
array $instances,
|
||||
string $organizerHref,
|
||||
?string $organizerName,
|
||||
array $attendee,
|
||||
string $objectId,
|
||||
string $objectType,
|
||||
int $objectSequence,
|
||||
string $method,
|
||||
VCalendar $template,
|
||||
): Message {
|
||||
|
||||
$recipientAddress = $attendee['href'] ?? '';
|
||||
$recipientName = $attendee['name'] ?? '';
|
||||
|
||||
$vObject = clone $template;
|
||||
if ($vObject->METHOD && $vObject->METHOD->getValue() !== $method) {
|
||||
$vObject->METHOD->setValue($method);
|
||||
} else {
|
||||
$vObject->add('METHOD', $method);
|
||||
}
|
||||
foreach ($instances as $instance) {
|
||||
$vObject->add($this->componentSanitizeScheduling(clone $instance));
|
||||
}
|
||||
|
||||
$message = new Message();
|
||||
$message->method = $method;
|
||||
$message->uid = $objectId;
|
||||
$message->component = $objectType;
|
||||
$message->sequence = $objectSequence;
|
||||
$message->sender = $organizerHref;
|
||||
$message->senderName = $organizerName;
|
||||
$message->recipient = $recipientAddress;
|
||||
$message->recipientName = $recipientName;
|
||||
$message->significantChange = true;
|
||||
$message->message = $vObject;
|
||||
|
||||
return $message;
|
||||
|
||||
}
|
||||
|
||||
protected function componentSanitizeScheduling(Component $component): Component {
|
||||
// Cleaning up any scheduling information that should not be sent or is missing
|
||||
unset($component->ORGANIZER['SCHEDULE-FORCE-SEND'], $component->ORGANIZER['SCHEDULE-STATUS']);
|
||||
foreach ($component->ATTENDEE as $attendee) {
|
||||
unset($attendee['SCHEDULE-FORCE-SEND'], $attendee['SCHEDULE-STATUS']);
|
||||
|
||||
if (!isset($attendee['PARTSTAT'])) {
|
||||
$attendee['PARTSTAT'] = 'NEEDS-ACTION';
|
||||
}
|
||||
}
|
||||
// Sequence is a required property, default is 0
|
||||
// https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.7.4
|
||||
if ($component->SEQUENCE === null) {
|
||||
$component->add('SEQUENCE', 0);
|
||||
}
|
||||
|
||||
return $component;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,11 +15,19 @@ class TipBrokerTest extends TestCase {
|
|||
|
||||
private TipBroker $broker;
|
||||
private VCalendar $vCalendar1a;
|
||||
private VCalendar $vCalendar2a;
|
||||
private array $templateEventInfo;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->broker = new TipBroker();
|
||||
|
||||
$this->templateEventInfo = [
|
||||
'organizer' => null,
|
||||
'attendees' => [],
|
||||
'significantChangeHash' => '',
|
||||
];
|
||||
// construct calendar with a 1 hour event and same start/end time zones
|
||||
$this->vCalendar1a = new VCalendar();
|
||||
/** @var VEvent $vEvent */
|
||||
|
|
@ -28,7 +36,7 @@ class TipBrokerTest extends TestCase {
|
|||
$vEvent->add('DTSTAMP', '20240701T000000Z');
|
||||
$vEvent->add('CREATED', '20240701T000000Z');
|
||||
$vEvent->add('LAST-MODIFIED', '20240701T000000Z');
|
||||
$vEvent->add('SEQUENCE', '1');
|
||||
$vEvent->add('SEQUENCE', 1);
|
||||
$vEvent->add('STATUS', 'CONFIRMED');
|
||||
$vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
|
||||
$vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
|
||||
|
|
@ -41,140 +49,534 @@ class TipBrokerTest extends TestCase {
|
|||
'ROLE' => 'REQ-PARTICIPANT',
|
||||
'RSVP' => 'TRUE'
|
||||
]);
|
||||
}
|
||||
|
||||
public function testParseEventForOrganizerOnCreate(): void {
|
||||
|
||||
// construct calendar and generate event info for newly created event with one attendee
|
||||
$calendar = clone $this->vCalendar1a;
|
||||
$previousEventInfo = [
|
||||
'organizer' => null,
|
||||
'significantChangeHash' => '',
|
||||
'attendees' => [],
|
||||
];
|
||||
$currentEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
|
||||
// test iTip generation
|
||||
$messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$calendar, $currentEventInfo, $previousEventInfo]);
|
||||
$this->assertCount(1, $messages);
|
||||
$this->assertEquals('REQUEST', $messages[0]->method);
|
||||
$this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
|
||||
$this->assertEquals($calendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
|
||||
|
||||
}
|
||||
|
||||
public function testParseEventForOrganizerOnModify(): void {
|
||||
|
||||
// construct calendar and generate event info for modified event with one attendee
|
||||
$calendar = clone $this->vCalendar1a;
|
||||
$previousEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
|
||||
$calendar->VEVENT->{'LAST-MODIFIED'}->setValue('20240701T020000Z');
|
||||
$calendar->VEVENT->SEQUENCE->setValue(2);
|
||||
$calendar->VEVENT->SUMMARY->setValue('Test Event Modified');
|
||||
$currentEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
|
||||
// test iTip generation
|
||||
$messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$calendar, $currentEventInfo, $previousEventInfo]);
|
||||
$this->assertCount(1, $messages);
|
||||
$this->assertEquals('REQUEST', $messages[0]->method);
|
||||
$this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
|
||||
$this->assertEquals($calendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
|
||||
|
||||
}
|
||||
|
||||
public function testParseEventForOrganizerOnDelete(): void {
|
||||
|
||||
// construct calendar and generate event info for modified event with one attendee
|
||||
$calendar = clone $this->vCalendar1a;
|
||||
$previousEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
|
||||
$currentEventInfo = $previousEventInfo;
|
||||
$currentEventInfo['attendees'] = [];
|
||||
++$currentEventInfo['sequence'];
|
||||
// test iTip generation
|
||||
$messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$calendar, $currentEventInfo, $previousEventInfo]);
|
||||
$this->assertCount(1, $messages);
|
||||
$this->assertEquals('CANCEL', $messages[0]->method);
|
||||
$this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
|
||||
$this->assertEquals($calendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
|
||||
|
||||
}
|
||||
|
||||
public function testParseEventForOrganizerOnStatusCancelled(): void {
|
||||
|
||||
// construct calendar and generate event info for modified event with one attendee
|
||||
$calendar = clone $this->vCalendar1a;
|
||||
$previousEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
|
||||
$calendar->VEVENT->{'LAST-MODIFIED'}->setValue('20240701T020000Z');
|
||||
$calendar->VEVENT->SEQUENCE->setValue(2);
|
||||
$calendar->VEVENT->STATUS->setValue('CANCELLED');
|
||||
$currentEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
|
||||
// test iTip generation
|
||||
$messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$calendar, $currentEventInfo, $previousEventInfo]);
|
||||
$this->assertCount(1, $messages);
|
||||
$this->assertEquals('CANCEL', $messages[0]->method);
|
||||
$this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
|
||||
$this->assertEquals($calendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
|
||||
|
||||
}
|
||||
|
||||
public function testParseEventForOrganizerOnAddAttendee(): void {
|
||||
|
||||
// construct calendar and generate event info for modified event with two attendees
|
||||
$calendar = clone $this->vCalendar1a;
|
||||
$previousEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
|
||||
$calendar->VEVENT->{'LAST-MODIFIED'}->setValue('20240701T020000Z');
|
||||
$calendar->VEVENT->SEQUENCE->setValue(2);
|
||||
$calendar->VEVENT->add('ATTENDEE', 'mailto:attendee2@testing.com', [
|
||||
'CN' => 'Attendee Two',
|
||||
'CUTYPE' => 'INDIVIDUAL',
|
||||
'PARTSTAT' => 'NEEDS-ACTION',
|
||||
'ROLE' => 'REQ-PARTICIPANT',
|
||||
'RSVP' => 'TRUE'
|
||||
]);
|
||||
$currentEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
|
||||
// test iTip generation
|
||||
$messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$calendar, $currentEventInfo, $previousEventInfo]);
|
||||
$this->assertCount(2, $messages);
|
||||
$this->assertEquals('REQUEST', $messages[0]->method);
|
||||
$this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
|
||||
$this->assertEquals($calendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
|
||||
$this->assertEquals('REQUEST', $messages[1]->method);
|
||||
$this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[1]->sender);
|
||||
$this->assertEquals($calendar->VEVENT->ATTENDEE[1]->getValue(), $messages[1]->recipient);
|
||||
|
||||
}
|
||||
|
||||
public function testParseEventForOrganizerOnRemoveAttendee(): void {
|
||||
|
||||
// construct calendar and generate event info for modified event with two attendees
|
||||
$calendar = clone $this->vCalendar1a;
|
||||
$calendar->VEVENT->add('ATTENDEE', 'mailto:attendee2@testing.com', [
|
||||
'CN' => 'Attendee Two',
|
||||
'CUTYPE' => 'INDIVIDUAL',
|
||||
'PARTSTAT' => 'NEEDS-ACTION',
|
||||
'ROLE' => 'REQ-PARTICIPANT',
|
||||
'RSVP' => 'TRUE'
|
||||
]);
|
||||
$previousEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
|
||||
$calendar->VEVENT->{'LAST-MODIFIED'}->setValue('20240701T020000Z');
|
||||
$calendar->VEVENT->SEQUENCE->setValue(2);
|
||||
$calendar->VEVENT->remove('ATTENDEE');
|
||||
$calendar->VEVENT->add('ATTENDEE', 'mailto:attendee1@testing.com', [
|
||||
// construct calendar with a 1 hour event and same start/end time zones
|
||||
// recurring every week on Monday for 12 weeks
|
||||
$this->vCalendar2a = new VCalendar();
|
||||
/** @var VEvent $vEvent */
|
||||
$vEvent = $this->vCalendar2a->add('VEVENT', []);
|
||||
$vEvent->add('UID', '96a0e6b1-d886-4a55-a60d-152b31401dcc');
|
||||
$vEvent->add('DTSTAMP', '20240701T000000Z');
|
||||
$vEvent->add('CREATED', '20240701T000000Z');
|
||||
$vEvent->add('LAST-MODIFIED', '20240701T000000Z');
|
||||
$vEvent->add('SEQUENCE', 1);
|
||||
$vEvent->add('STATUS', 'CONFIRMED');
|
||||
$vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
|
||||
$vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
|
||||
$vEvent->add('RRULE', 'FREQ=WEEKLY;COUNT=12;BYDAY=MO');
|
||||
$vEvent->add('SUMMARY', 'Test Event');
|
||||
$vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
|
||||
$vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
|
||||
'CN' => 'Attendee One',
|
||||
'CUTYPE' => 'INDIVIDUAL',
|
||||
'PARTSTAT' => 'NEEDS-ACTION',
|
||||
'ROLE' => 'REQ-PARTICIPANT',
|
||||
'RSVP' => 'TRUE'
|
||||
]);
|
||||
$currentEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests user creating a new singleton or recurring event
|
||||
*/
|
||||
public function testParseEventForOrganizerCreated(): void {
|
||||
// construct calendar and generate event info for newly created event with one attendee
|
||||
$mutatedCalendar = clone $this->vCalendar1a;
|
||||
$originalEventInfo = $this->templateEventInfo;
|
||||
$mutatedEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$mutatedCalendar]);
|
||||
// test iTip generation
|
||||
$messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$calendar, $currentEventInfo, $previousEventInfo]);
|
||||
$messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$mutatedCalendar, $mutatedEventInfo, $originalEventInfo]);
|
||||
$this->assertCount(1, $messages);
|
||||
$this->assertEquals('REQUEST', $messages[0]->method);
|
||||
$this->assertEquals($mutatedCalendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
|
||||
$this->assertEquals($mutatedCalendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests user modifying an existing singleton or recurring (base) event
|
||||
*/
|
||||
public function testParseEventForOrganizerModified(): void {
|
||||
// construct calendar and generate event info for modified event with one attendee
|
||||
$originalCalendar = clone $this->vCalendar1a;
|
||||
$originalEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$originalCalendar]);
|
||||
$mutatedCalendar = clone $this->vCalendar1a;
|
||||
$mutatedCalendar->VEVENT->{'LAST-MODIFIED'}->setValue('20240701T020000Z');
|
||||
$mutatedCalendar->VEVENT->SEQUENCE->setValue(2);
|
||||
$mutatedCalendar->VEVENT->SUMMARY->setValue('Test Event Modified');
|
||||
$mutatedEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$mutatedCalendar]);
|
||||
// test iTip generation
|
||||
$messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$mutatedCalendar, $mutatedEventInfo, $originalEventInfo]);
|
||||
$this->assertCount(1, $messages);
|
||||
$this->assertEquals('REQUEST', $messages[0]->method);
|
||||
$this->assertEquals($mutatedCalendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
|
||||
$this->assertEquals($mutatedCalendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests user deleting an existing singleton or recurring (base) event
|
||||
*/
|
||||
public function testParseEventForOrganizerDeleted(): void {
|
||||
// construct calendar and generate event info for modified event with one attendee
|
||||
$originalCalendar = clone $this->vCalendar1a;
|
||||
$originalEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$originalCalendar]);
|
||||
$mutatedEventInfo = $originalEventInfo;
|
||||
$mutatedEventInfo['attendees'] = [];
|
||||
++$mutatedEventInfo['sequence'];
|
||||
// test iTip generation
|
||||
$messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$originalCalendar, $mutatedEventInfo, $originalEventInfo]);
|
||||
$this->assertCount(1, $messages);
|
||||
$this->assertEquals('CANCEL', $messages[0]->method);
|
||||
$this->assertEquals($originalCalendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
|
||||
$this->assertEquals($originalCalendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests user cancelling an existing singleton or recurring (base) event
|
||||
*/
|
||||
public function testParseEventForOrganizerStatusCancelled(): void {
|
||||
// construct calendar and generate event info for modified event with one attendee
|
||||
$originalCalendar = clone $this->vCalendar1a;
|
||||
$originalEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$originalCalendar]);
|
||||
$mutatedCalendar = clone $this->vCalendar1a;
|
||||
$mutatedCalendar->VEVENT->{'LAST-MODIFIED'}->setValue('20240701T020000Z');
|
||||
$mutatedCalendar->VEVENT->SEQUENCE->setValue(2);
|
||||
$mutatedCalendar->VEVENT->STATUS->setValue('CANCELLED');
|
||||
$mutatedEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$mutatedCalendar]);
|
||||
// test iTip generation
|
||||
$messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$mutatedCalendar, $mutatedEventInfo, $originalEventInfo]);
|
||||
$this->assertCount(1, $messages);
|
||||
$this->assertEquals('CANCEL', $messages[0]->method);
|
||||
$this->assertEquals($mutatedCalendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
|
||||
$this->assertEquals($mutatedCalendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests user adding an attendee to an existing singleton or recurring (base) event
|
||||
*/
|
||||
public function testParseEventForOrganizerAddAttendee(): void {
|
||||
// construct calendar and generate event info for modified event with two attendees
|
||||
$originalCalendar = clone $this->vCalendar1a;
|
||||
$originalEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$originalCalendar]);
|
||||
$mutatedCalendar = clone $this->vCalendar1a;
|
||||
$mutatedCalendar->VEVENT->{'LAST-MODIFIED'}->setValue('20240701T020000Z');
|
||||
$mutatedCalendar->VEVENT->SEQUENCE->setValue(2);
|
||||
$mutatedCalendar->VEVENT->add('ATTENDEE', 'mailto:attendee2@testing.com', [
|
||||
'CN' => 'Attendee Two',
|
||||
'CUTYPE' => 'INDIVIDUAL',
|
||||
'PARTSTAT' => 'NEEDS-ACTION',
|
||||
'ROLE' => 'REQ-PARTICIPANT',
|
||||
'RSVP' => 'TRUE'
|
||||
]);
|
||||
$mutatedEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$mutatedCalendar]);
|
||||
// test iTip generation
|
||||
$messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$mutatedCalendar, $mutatedEventInfo, $originalEventInfo]);
|
||||
// attendee modifications get generated in order of Added, Removed, Existing
|
||||
$this->assertCount(2, $messages);
|
||||
$this->assertEquals('REQUEST', $messages[0]->method);
|
||||
$this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
|
||||
$this->assertEquals($calendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
|
||||
$this->assertEquals($mutatedCalendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
|
||||
$this->assertEquals($mutatedCalendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
|
||||
$this->assertEquals('REQUEST', $messages[1]->method);
|
||||
$this->assertEquals($mutatedCalendar->VEVENT->ORGANIZER->getValue(), $messages[1]->sender);
|
||||
$this->assertEquals($mutatedCalendar->VEVENT->ATTENDEE[1]->getValue(), $messages[1]->recipient);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests user removing an attendee from an existing singleton or recurring (base) event
|
||||
*/
|
||||
public function testParseEventForOrganizerRemoveAttendee(): void {
|
||||
// construct calendar and generate event info for modified event with two attendees
|
||||
$originalCalendar = clone $this->vCalendar1a;
|
||||
$originalCalendar->VEVENT->add('ATTENDEE', 'mailto:attendee2@testing.com', [
|
||||
'CN' => 'Attendee Two',
|
||||
'CUTYPE' => 'INDIVIDUAL',
|
||||
'PARTSTAT' => 'NEEDS-ACTION',
|
||||
'ROLE' => 'REQ-PARTICIPANT',
|
||||
'RSVP' => 'TRUE'
|
||||
]);
|
||||
$originalEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$originalCalendar]);
|
||||
$mutatedCalendar = clone $this->vCalendar1a;
|
||||
$mutatedCalendar->VEVENT->{'LAST-MODIFIED'}->setValue('20240701T020000Z');
|
||||
$mutatedCalendar->VEVENT->SEQUENCE->setValue(2);
|
||||
$mutatedCalendar->VEVENT->remove('ATTENDEE');
|
||||
$mutatedCalendar->VEVENT->add('ATTENDEE', 'mailto:attendee1@testing.com', [
|
||||
'CN' => 'Attendee One',
|
||||
'CUTYPE' => 'INDIVIDUAL',
|
||||
'PARTSTAT' => 'NEEDS-ACTION',
|
||||
'ROLE' => 'REQ-PARTICIPANT',
|
||||
'RSVP' => 'TRUE'
|
||||
]);
|
||||
$mutatedEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$mutatedCalendar]);
|
||||
// test iTip generation
|
||||
$messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$mutatedCalendar, $mutatedEventInfo, $originalEventInfo]);
|
||||
// attendee modifications get generated in order of Added, Removed, Existing
|
||||
$this->assertCount(2, $messages);
|
||||
$this->assertEquals('REQUEST', $messages[0]->method);
|
||||
$this->assertEquals($mutatedCalendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
|
||||
$this->assertEquals($mutatedCalendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
|
||||
$this->assertEquals('CANCEL', $messages[1]->method);
|
||||
$this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[1]->sender);
|
||||
$this->assertEquals($mutatedCalendar->VEVENT->ORGANIZER->getValue(), $messages[1]->sender);
|
||||
$this->assertEquals('mailto:attendee2@testing.com', $messages[1]->recipient);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tests user converts existing singleton or recurring (base) event from attended to attendeless
|
||||
*/
|
||||
public function testParseEventForOrganizerRemoveOrganizerAndAttendees(): void {
|
||||
// construct calendar and generate event info for modified event with two attendees
|
||||
$originalCalendar = clone $this->vCalendar1a;
|
||||
$originalEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$originalCalendar]);
|
||||
$mutatedCalendar = clone $this->vCalendar1a;
|
||||
$mutatedCalendar->VEVENT->{'LAST-MODIFIED'}->setValue('20240701T020000Z');
|
||||
$mutatedCalendar->VEVENT->SEQUENCE->setValue(2);
|
||||
$mutatedCalendar->VEVENT->remove('ORGANIZER');
|
||||
$mutatedCalendar->VEVENT->remove('ATTENDEE');
|
||||
$mutatedEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$mutatedCalendar]);
|
||||
// test iTip generation
|
||||
$messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$mutatedCalendar, $mutatedEventInfo, $originalEventInfo]);
|
||||
// attendee modifications get generated in order of Added, Removed, Existing
|
||||
$this->assertCount(1, $messages);
|
||||
$this->assertEquals('CANCEL', $messages[0]->method);
|
||||
$this->assertEquals(2, $messages[0]->sequence);
|
||||
$this->assertEquals($originalCalendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
|
||||
$this->assertEquals($originalCalendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests user modifying recurring (base) event by moving instance to a new date
|
||||
*/
|
||||
public function testParseEventForOrganizerCreatedInstance(): void {
|
||||
// construct calendar and generate event info for modified event with two attendees
|
||||
$originalCalendar = clone $this->vCalendar2a;
|
||||
$originalEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$originalCalendar]);
|
||||
$mutatedInstance = clone $originalCalendar->VEVENT;
|
||||
$mutatedInstance->add('RECURRENCE-ID', '20240715T080000', ['TZID' => 'America/Toronto']);
|
||||
$mutatedInstance->SEQUENCE->setValue(0);
|
||||
$mutatedInstance->DTSTART->setValue('20240717T080000');
|
||||
$mutatedInstance->DTEND->setValue('20240717T090000');
|
||||
$mutatedCalendar = clone $this->vCalendar2a;
|
||||
$mutatedCalendar->add($mutatedInstance);
|
||||
$mutatedEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$mutatedCalendar]);
|
||||
// test iTip generation
|
||||
$messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$mutatedCalendar, $mutatedEventInfo, $originalEventInfo]);
|
||||
// attendee modifications get generated in order of Added, Removed, Existing
|
||||
$this->assertCount(1, $messages);
|
||||
$this->assertEquals('REQUEST', $messages[0]->method);
|
||||
$this->assertEquals(1, $messages[0]->sequence);
|
||||
$this->assertEquals($mutatedCalendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
|
||||
$this->assertEquals($mutatedCalendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
|
||||
$this->assertCount(2, $messages[0]->message->VEVENT);
|
||||
$this->assertEquals('20240715T080000', $messages[0]->message->VEVENT[1]->{'RECURRENCE-ID'}->getValue());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests user modifying recurring (base) event by cancelling a single instance
|
||||
*/
|
||||
public function testParseEventForOrganizerCreatedInstanceCancelled(): void {
|
||||
// construct calendar and generate event info for modified event with two attendees
|
||||
$originalCalendar = clone $this->vCalendar2a;
|
||||
$originalEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$originalCalendar]);
|
||||
$mutatedInstance = clone $originalCalendar->VEVENT;
|
||||
$mutatedInstance->add('RECURRENCE-ID', '20240715T080000', ['TZID' => 'America/Toronto']);
|
||||
$mutatedInstance->SEQUENCE->setValue(0);
|
||||
$mutatedInstance->STATUS->setValue('CANCELLED');
|
||||
$mutatedCalendar = clone $this->vCalendar2a;
|
||||
$mutatedCalendar->add($mutatedInstance);
|
||||
$mutatedEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$mutatedCalendar]);
|
||||
// test iTip generation
|
||||
$messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$mutatedCalendar, $mutatedEventInfo, $originalEventInfo]);
|
||||
// attendee modifications get generated in order of Added, Removed, Existing
|
||||
$this->assertCount(2, $messages);
|
||||
$this->assertEquals('CANCEL', $messages[0]->method);
|
||||
$this->assertEquals(1, $messages[0]->sequence);
|
||||
$this->assertEquals($mutatedCalendar->VEVENT[1]->ORGANIZER->getValue(), $messages[0]->sender);
|
||||
$this->assertEquals($mutatedCalendar->VEVENT[1]->ATTENDEE[0]->getValue(), $messages[0]->recipient);
|
||||
$this->assertCount(1, $messages[0]->message->VEVENT);
|
||||
$this->assertEquals('20240715T080000', $messages[0]->message->VEVENT->{'RECURRENCE-ID'}->getValue());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests user modifying recurring (instance) event with non status or attendee changes
|
||||
*/
|
||||
public function testParseEventForOrganizerModifyInstance(): void {
|
||||
// construct calendar and generate event info for modified event with two attendees
|
||||
$originalCalendar = clone $this->vCalendar2a;
|
||||
$originalInstance = clone $originalCalendar->VEVENT;
|
||||
$originalInstance->add('RECURRENCE-ID', '20240715T080000', ['TZID' => 'America/Toronto']);
|
||||
$originalInstance->SEQUENCE->setValue(1);
|
||||
$originalInstance->DTSTART->setValue('20240717T080000');
|
||||
$originalInstance->DTEND->setValue('20240717T090000');
|
||||
$originalCalendar->add($originalInstance);
|
||||
$originalEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$originalCalendar]);
|
||||
|
||||
$mutatedInstance = clone $originalInstance;
|
||||
$mutatedInstance->SEQUENCE->setValue(2);
|
||||
$mutatedInstance->DTSTART->setValue('20240718T080000');
|
||||
$mutatedInstance->DTEND->setValue('20240718T090000');
|
||||
$mutatedCalendar = clone $this->vCalendar2a;
|
||||
$mutatedCalendar->add($mutatedInstance);
|
||||
$mutatedEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$mutatedCalendar]);
|
||||
// test iTip generation
|
||||
$messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$mutatedCalendar, $mutatedEventInfo, $originalEventInfo]);
|
||||
// attendee modifications get generated in order of Added, Removed, Existing
|
||||
$this->assertCount(1, $messages);
|
||||
$this->assertEquals('REQUEST', $messages[0]->method);
|
||||
$this->assertEquals(1, $messages[0]->sequence);
|
||||
$this->assertEquals($mutatedCalendar->VEVENT[1]->ORGANIZER->getValue(), $messages[0]->sender);
|
||||
$this->assertEquals($mutatedCalendar->VEVENT[1]->ATTENDEE[0]->getValue(), $messages[0]->recipient);
|
||||
$this->assertCount(2, $messages[0]->message->VEVENT);
|
||||
$this->assertEquals('20240715T080000', $messages[0]->message->VEVENT[1]->{'RECURRENCE-ID'}->getValue());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests user modifying recurring (instance) event by setting status to cancelled
|
||||
*/
|
||||
public function testParseEventForOrganizerModifyInstanceStatus(): void {
|
||||
// construct calendar and generate event info for modified event with two attendees
|
||||
$originalCalendar = clone $this->vCalendar2a;
|
||||
$originalInstance = clone $originalCalendar->VEVENT;
|
||||
$originalInstance->add('RECURRENCE-ID', '20240715T080000', ['TZID' => 'America/Toronto']);
|
||||
$originalInstance->SEQUENCE->setValue(1);
|
||||
$originalInstance->DTSTART->setValue('20240717T080000');
|
||||
$originalInstance->DTEND->setValue('20240717T090000');
|
||||
$originalCalendar->add($originalInstance);
|
||||
$originalEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$originalCalendar]);
|
||||
$mutatedInstance = clone $originalInstance;
|
||||
$mutatedInstance->SEQUENCE->setValue(2);
|
||||
$mutatedInstance->STATUS->setValue('CANCELLED');
|
||||
$mutatedCalendar = clone $this->vCalendar2a;
|
||||
$mutatedCalendar->add($mutatedInstance);
|
||||
$mutatedEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$mutatedCalendar]);
|
||||
// test iTip generation
|
||||
$messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$mutatedCalendar, $mutatedEventInfo, $originalEventInfo]);
|
||||
// attendee modifications get generated in order of Added, Removed, Existing
|
||||
$this->assertCount(1, $messages);
|
||||
$this->assertEquals('REQUEST', $messages[0]->method);
|
||||
$this->assertEquals(1, $messages[0]->sequence);
|
||||
$this->assertEquals($mutatedCalendar->VEVENT[1]->ORGANIZER->getValue(), $messages[0]->sender);
|
||||
$this->assertEquals($mutatedCalendar->VEVENT[1]->ATTENDEE[0]->getValue(), $messages[0]->recipient);
|
||||
$this->assertCount(2, $messages[0]->message->VEVENT);
|
||||
$this->assertEquals('20240715T080000', $messages[0]->message->VEVENT[1]->{'RECURRENCE-ID'}->getValue());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests user modifying recurring (instance) event by adding attendee
|
||||
*/
|
||||
public function testParseEventForOrganizerModifyInstanceAddAttendee(): void {
|
||||
// construct calendar and generate event info for modified event with two attendees
|
||||
$originalCalendar = clone $this->vCalendar2a;
|
||||
$originalInstance = clone $originalCalendar->VEVENT;
|
||||
$originalInstance->add('RECURRENCE-ID', '20240715T080000', ['TZID' => 'America/Toronto']);
|
||||
$originalInstance->SEQUENCE->setValue(1);
|
||||
$originalInstance->DTSTART->setValue('20240717T080000');
|
||||
$originalInstance->DTEND->setValue('20240717T090000');
|
||||
$originalCalendar->add($originalInstance);
|
||||
$originalEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$originalCalendar]);
|
||||
$mutatedInstance = clone $originalInstance;
|
||||
$mutatedInstance->SEQUENCE->setValue(2);
|
||||
$mutatedInstance->add('ATTENDEE', 'mailto:attendee2@testing.com', [
|
||||
'CN' => 'Attendee Two',
|
||||
'CUTYPE' => 'INDIVIDUAL',
|
||||
'PARTSTAT' => 'NEEDS-ACTION',
|
||||
'ROLE' => 'REQ-PARTICIPANT',
|
||||
'RSVP' => 'TRUE'
|
||||
]);
|
||||
$mutatedCalendar = clone $this->vCalendar2a;
|
||||
$mutatedCalendar->add($mutatedInstance);
|
||||
$mutatedEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$mutatedCalendar]);
|
||||
// test iTip generation
|
||||
$messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$mutatedCalendar, $mutatedEventInfo, $originalEventInfo]);
|
||||
// attendee modifications get generated in order of Added, Removed, Existing
|
||||
$this->assertCount(2, $messages);
|
||||
$this->assertEquals('REQUEST', $messages[0]->method);
|
||||
$this->assertEquals(1, $messages[0]->sequence);
|
||||
$this->assertEquals($mutatedCalendar->VEVENT[1]->ORGANIZER->getValue(), $messages[0]->sender);
|
||||
$this->assertEquals($mutatedCalendar->VEVENT[1]->ATTENDEE[0]->getValue(), $messages[0]->recipient);
|
||||
$this->assertCount(2, $messages[0]->message->VEVENT);
|
||||
$this->assertEquals('20240715T080000', $messages[0]->message->VEVENT[1]->{'RECURRENCE-ID'}->getValue());
|
||||
$this->assertEquals('REQUEST', $messages[1]->method);
|
||||
$this->assertEquals(1, $messages[1]->sequence);
|
||||
$this->assertEquals($mutatedCalendar->VEVENT[1]->ORGANIZER->getValue(), $messages[1]->sender);
|
||||
$this->assertEquals($mutatedCalendar->VEVENT[1]->ATTENDEE[1]->getValue(), $messages[1]->recipient);
|
||||
$this->assertCount(1, $messages[1]->message->VEVENT);
|
||||
$this->assertEquals('20240715T080000', $messages[1]->message->VEVENT->{'RECURRENCE-ID'}->getValue());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests user modifying recurring (instance) event by removing attendee
|
||||
*/
|
||||
public function testParseEventForOrganizerModifyInstanceRemoveAttendee(): void {
|
||||
// construct calendar and generate event info for modified event with two attendees
|
||||
$originalCalendar = clone $this->vCalendar2a;
|
||||
$originalInstance = clone $originalCalendar->VEVENT;
|
||||
$originalInstance->add('RECURRENCE-ID', '20240715T080000', ['TZID' => 'America/Toronto']);
|
||||
$originalInstance->SEQUENCE->setValue(1);
|
||||
$originalInstance->DTSTART->setValue('20240717T080000');
|
||||
$originalInstance->DTEND->setValue('20240717T090000');
|
||||
$originalInstance->add('ATTENDEE', 'mailto:attendee2@testing.com', [
|
||||
'CN' => 'Attendee Two',
|
||||
'CUTYPE' => 'INDIVIDUAL',
|
||||
'PARTSTAT' => 'NEEDS-ACTION',
|
||||
'ROLE' => 'REQ-PARTICIPANT',
|
||||
'RSVP' => 'TRUE'
|
||||
]);
|
||||
$originalCalendar->add($originalInstance);
|
||||
$originalEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$originalCalendar]);
|
||||
$mutatedInstance = clone $originalInstance;
|
||||
$mutatedInstance->SEQUENCE->setValue(2);
|
||||
$mutatedInstance->remove('ATTENDEE');
|
||||
$mutatedInstance->add('ATTENDEE', 'mailto:attendee1@testing.com', [
|
||||
'CN' => 'Attendee One',
|
||||
'CUTYPE' => 'INDIVIDUAL',
|
||||
'PARTSTAT' => 'NEEDS-ACTION',
|
||||
'ROLE' => 'REQ-PARTICIPANT',
|
||||
'RSVP' => 'TRUE'
|
||||
]);
|
||||
$mutatedCalendar = clone $this->vCalendar2a;
|
||||
$mutatedCalendar->add($mutatedInstance);
|
||||
$mutatedEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$mutatedCalendar]);
|
||||
// test iTip generation
|
||||
$messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$mutatedCalendar, $mutatedEventInfo, $originalEventInfo]);
|
||||
// attendee modifications get generated in order of Added, Removed, Existing
|
||||
$this->assertCount(2, $messages);
|
||||
$this->assertEquals('REQUEST', $messages[0]->method);
|
||||
$this->assertEquals(1, $messages[0]->sequence);
|
||||
$this->assertEquals($originalCalendar->VEVENT[1]->ORGANIZER->getValue(), $messages[0]->sender);
|
||||
$this->assertEquals($originalCalendar->VEVENT[1]->ATTENDEE[0]->getValue(), $messages[0]->recipient);
|
||||
$this->assertCount(2, $messages[0]->message->VEVENT);
|
||||
$this->assertEquals('20240715T080000', $messages[0]->message->VEVENT[1]->{'RECURRENCE-ID'}->getValue());
|
||||
$this->assertEquals('CANCEL', $messages[1]->method);
|
||||
$this->assertEquals(1, $messages[1]->sequence);
|
||||
$this->assertEquals($originalCalendar->VEVENT[1]->ORGANIZER->getValue(), $messages[1]->sender);
|
||||
$this->assertEquals($originalCalendar->VEVENT[1]->ATTENDEE[1]->getValue(), $messages[1]->recipient);
|
||||
$this->assertCount(1, $messages[1]->message->VEVENT);
|
||||
$this->assertEquals('20240715T080000', $messages[1]->message->VEVENT->{'RECURRENCE-ID'}->getValue());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests user deleting master instance of recurring event
|
||||
*/
|
||||
public function testParseEventForOrganizerDeleteMasterInstance(): void {
|
||||
// construct calendar with recurring event
|
||||
$originalCalendar = clone $this->vCalendar2a;
|
||||
$originalEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$originalCalendar]);
|
||||
// delete the master instance (convert to non-scheduling)
|
||||
$mutatedCalendar = clone $this->vCalendar2a;
|
||||
$mutatedCalendar->VEVENT->{'LAST-MODIFIED'}->setValue('20240701T020000Z');
|
||||
$mutatedCalendar->VEVENT->SEQUENCE->setValue(2);
|
||||
$mutatedCalendar->VEVENT->remove('ORGANIZER');
|
||||
$mutatedCalendar->VEVENT->remove('ATTENDEE');
|
||||
$mutatedEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$mutatedCalendar]);
|
||||
// test iTip generation
|
||||
$messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$mutatedCalendar, $mutatedEventInfo, $originalEventInfo]);
|
||||
$this->assertCount(1, $messages);
|
||||
$this->assertEquals('CANCEL', $messages[0]->method);
|
||||
$this->assertEquals(2, $messages[0]->sequence);
|
||||
$this->assertEquals($originalCalendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
|
||||
$this->assertEquals($originalCalendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests user adding EXDATE to master instance
|
||||
*/
|
||||
public function testParseEventForOrganizerAddExdate(): void {
|
||||
// construct calendar with recurring event
|
||||
$originalCalendar = clone $this->vCalendar2a;
|
||||
$originalEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$originalCalendar]);
|
||||
// add EXDATE to exclude specific occurrences
|
||||
$mutatedCalendar = clone $this->vCalendar2a;
|
||||
$mutatedCalendar->VEVENT->{'LAST-MODIFIED'}->setValue('20240701T020000Z');
|
||||
$mutatedCalendar->VEVENT->SEQUENCE->setValue(2);
|
||||
$mutatedCalendar->VEVENT->add('EXDATE', ['20240715T080000', '20240722T080000'], ['TZID' => 'America/Toronto']);
|
||||
$mutatedEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$mutatedCalendar]);
|
||||
// test iTip generation
|
||||
$messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$mutatedCalendar, $mutatedEventInfo, $originalEventInfo]);
|
||||
$this->assertCount(1, $messages);
|
||||
$this->assertEquals('REQUEST', $messages[0]->method);
|
||||
$this->assertEquals(2, $messages[0]->sequence);
|
||||
$this->assertEquals($mutatedCalendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
|
||||
$this->assertEquals($mutatedCalendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
|
||||
// verify EXDATE is present in the message
|
||||
$this->assertTrue(isset($messages[0]->message->VEVENT->EXDATE));
|
||||
$exdates = $messages[0]->message->VEVENT->EXDATE->getParts();
|
||||
$this->assertContains('20240715T080000', $exdates);
|
||||
$this->assertContains('20240722T080000', $exdates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests user removing EXDATE from master instance
|
||||
*/
|
||||
public function testParseEventForOrganizerRemoveExdate(): void {
|
||||
// construct calendar with recurring event that has EXDATE
|
||||
$originalCalendar = clone $this->vCalendar2a;
|
||||
$originalCalendar->VEVENT->add('EXDATE', ['20240715T080000', '20240722T080000'], ['TZID' => 'America/Toronto']);
|
||||
$originalEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$originalCalendar]);
|
||||
// remove EXDATE to restore excluded occurrences
|
||||
$mutatedCalendar = clone $this->vCalendar2a;
|
||||
$mutatedCalendar->VEVENT->{'LAST-MODIFIED'}->setValue('20240701T020000Z');
|
||||
$mutatedCalendar->VEVENT->SEQUENCE->setValue(2);
|
||||
$mutatedEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$mutatedCalendar]);
|
||||
// test iTip generation
|
||||
$messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$mutatedCalendar, $mutatedEventInfo, $originalEventInfo]);
|
||||
$this->assertCount(1, $messages);
|
||||
$this->assertEquals('REQUEST', $messages[0]->method);
|
||||
$this->assertEquals(2, $messages[0]->sequence);
|
||||
$this->assertEquals($mutatedCalendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
|
||||
$this->assertEquals($mutatedCalendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
|
||||
// verify EXDATE is not present in the message
|
||||
$this->assertFalse(isset($messages[0]->message->VEVENT->EXDATE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests user converting recurring event to non-scheduling
|
||||
*/
|
||||
public function testParseEventForOrganizerConvertRecurringToNonScheduling(): void {
|
||||
// construct calendar with recurring event
|
||||
$originalCalendar = clone $this->vCalendar2a;
|
||||
$originalEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$originalCalendar]);
|
||||
// remove ORGANIZER and ATTENDEE properties to convert to non-scheduling
|
||||
$mutatedCalendar = clone $this->vCalendar2a;
|
||||
$mutatedCalendar->VEVENT->{'LAST-MODIFIED'}->setValue('20240701T020000Z');
|
||||
$mutatedCalendar->VEVENT->SEQUENCE->setValue(2);
|
||||
$mutatedCalendar->VEVENT->remove('ORGANIZER');
|
||||
$mutatedCalendar->VEVENT->remove('ATTENDEE');
|
||||
$mutatedEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$mutatedCalendar]);
|
||||
// test iTip generation
|
||||
$messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$mutatedCalendar, $mutatedEventInfo, $originalEventInfo]);
|
||||
$this->assertCount(1, $messages);
|
||||
$this->assertEquals('CANCEL', $messages[0]->method);
|
||||
$this->assertEquals(2, $messages[0]->sequence);
|
||||
$this->assertEquals($originalCalendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
|
||||
$this->assertEquals($originalCalendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests SCHEDULE-FORCE-SEND parameter handling
|
||||
*/
|
||||
public function testParseEventForOrganizerScheduleForceSend(): void {
|
||||
// construct calendar with event
|
||||
$originalCalendar = clone $this->vCalendar1a;
|
||||
$originalEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$originalCalendar]);
|
||||
// add SCHEDULE-FORCE-SEND parameter to ATTENDEE
|
||||
$mutatedCalendar = clone $this->vCalendar1a;
|
||||
$mutatedCalendar->VEVENT->{'LAST-MODIFIED'}->setValue('20240701T020000Z');
|
||||
$mutatedCalendar->VEVENT->SEQUENCE->setValue(2);
|
||||
$mutatedCalendar->VEVENT->ATTENDEE->add('SCHEDULE-FORCE-SEND', 'REQUEST');
|
||||
$mutatedEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$mutatedCalendar]);
|
||||
// test iTip generation
|
||||
$messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$mutatedCalendar, $mutatedEventInfo, $originalEventInfo]);
|
||||
$this->assertCount(1, $messages);
|
||||
$this->assertEquals('REQUEST', $messages[0]->method);
|
||||
$this->assertEquals(2, $messages[0]->sequence);
|
||||
$this->assertEquals($mutatedCalendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
|
||||
$this->assertEquals($mutatedCalendar->VEVENT->ATTENDEE->getValue(), $messages[0]->recipient);
|
||||
// verify SCHEDULE-FORCE-SEND is removed from the message (sanitized)
|
||||
$this->assertFalse(isset($messages[0]->message->VEVENT->ATTENDEE['SCHEDULE-FORCE-SEND']));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue