mirror of
https://github.com/nextcloud/server.git
synced 2026-05-22 10:06:37 -04:00
Merge pull request #59988 from nextcloud/feat/party-crasher
caldav party crasher
This commit is contained in:
commit
69d0b7e2e1
3 changed files with 568 additions and 11 deletions
|
|
@ -19,6 +19,7 @@ use OCA\DAV\CalDAV\Reminder\NotificationProvider\EmailProvider;
|
|||
use OCA\DAV\CalDAV\Reminder\NotificationProvider\PushProvider;
|
||||
use OCA\DAV\CalDAV\Reminder\NotificationProviderManager;
|
||||
use OCA\DAV\CalDAV\Reminder\Notifier as NotifierCalDAV;
|
||||
use OCA\DAV\CalDAV\TipBroker;
|
||||
use OCA\DAV\Capabilities;
|
||||
use OCA\DAV\CardDAV\ContactsManager;
|
||||
use OCA\DAV\CardDAV\Notification\Notifier as NotifierCardDAV;
|
||||
|
|
@ -108,6 +109,7 @@ use OCP\User\Events\UserIdAssignedEvent;
|
|||
use OCP\User\Events\UserIdUnassignedEvent;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Sabre\VObject;
|
||||
use Throwable;
|
||||
use function is_null;
|
||||
|
||||
|
|
@ -238,6 +240,8 @@ class Application extends App implements IBootstrap {
|
|||
|
||||
#[\Override]
|
||||
public function boot(IBootContext $context): void {
|
||||
VObject\Component\VCalendar::$propertyMap[TipBroker::INVITATION_FORWARDING_PROPERTY] = VObject\Property\Boolean::class;
|
||||
|
||||
// Load all dav apps
|
||||
$context->getServerContainer()->get(IAppManager::class)->loadApps(['dav']);
|
||||
|
||||
|
|
|
|||
|
|
@ -11,10 +11,18 @@ namespace OCA\DAV\CalDAV;
|
|||
|
||||
use Sabre\VObject\Component;
|
||||
use Sabre\VObject\Component\VCalendar;
|
||||
use Sabre\VObject\Component\VEvent;
|
||||
use Sabre\VObject\ITip\Broker;
|
||||
use Sabre\VObject\ITip\Message;
|
||||
use Sabre\VObject\Parameter;
|
||||
use Sabre\VObject\Property;
|
||||
use Sabre\VObject\Property\Boolean;
|
||||
use Sabre\VObject\Property\ICalendar\CalAddress;
|
||||
use Sabre\VObject\Property\ICalendar\DateTime;
|
||||
use Sabre\VObject\Recur\EventIterator;
|
||||
|
||||
class TipBroker extends Broker {
|
||||
public const INVITATION_FORWARDING_PROPERTY = 'X-NC-INVITATION-FORWARDING';
|
||||
|
||||
public $significantChangeProperties = [
|
||||
'DTSTART',
|
||||
|
|
@ -79,6 +87,181 @@ class TipBroker extends Broker {
|
|||
return $existingObject;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function processMessageReply(Message $itipMessage, ?VCalendar $existingObject = null) {
|
||||
// A reply can only be processed based on an existing object.
|
||||
// If the object is not available, the reply is ignored.
|
||||
if ($existingObject === null) {
|
||||
return null;
|
||||
}
|
||||
$instances = [];
|
||||
$requestStatus = '2.0';
|
||||
|
||||
/** @var list<VEvent> $vevents */
|
||||
$vevents = $itipMessage->message->select('VEVENT');
|
||||
|
||||
// Finding all the instances the attendee replied to.
|
||||
foreach ($vevents as $vevent) {
|
||||
// Use the Unix timestamp returned by getTimestamp as a unique identifier for the recurrence.
|
||||
// The Unix timestamp will be the same for an event, even if the reply from the attendee
|
||||
// used a different format/timezone to express the event date-time.
|
||||
$recurId = $this->getRecurrenceKey($vevent);
|
||||
$attendee = $this->getFirstAttendee($vevent);
|
||||
if ($attendee === null) {
|
||||
continue;
|
||||
}
|
||||
$partstat = $attendee->offsetGet('PARTSTAT');
|
||||
if (!$partstat instanceof Parameter) {
|
||||
continue;
|
||||
}
|
||||
$instances[$recurId] = $partstat->getValue();
|
||||
if (isset($vevent->{'REQUEST-STATUS'})) {
|
||||
$requestStatus = $vevent->{'REQUEST-STATUS'}->getValue();
|
||||
[$requestStatus] = explode(';', $requestStatus);
|
||||
}
|
||||
}
|
||||
|
||||
// Now we need to loop through the original organizer event, to find
|
||||
// all the instances where we have a reply for.
|
||||
/** @var VEvent|null $masterObject */
|
||||
$masterObject = $existingObject->getBaseComponent('VEVENT');
|
||||
$masterAllowInvitationForwarding = $masterObject === null || $this->allowInvitationForwarding($masterObject);
|
||||
|
||||
/** @var list<VEvent> $vevents */
|
||||
$vevents = $existingObject->select('VEVENT');
|
||||
|
||||
foreach ($vevents as $vevent) {
|
||||
// Use the Unix timestamp returned by getTimestamp as a unique identifier for the recurrence.
|
||||
$recurId = $this->getRecurrenceKey($vevent);
|
||||
if (isset($instances[$recurId])) {
|
||||
$allowInvitationForwarding = $this->allowInvitationForwarding($vevent);
|
||||
$attendeeFound = false;
|
||||
if (isset($vevent->ATTENDEE)) {
|
||||
foreach ($vevent->ATTENDEE as $attendee) {
|
||||
if ($attendee->getValue() === $itipMessage->sender) {
|
||||
$attendeeFound = true;
|
||||
$attendee['PARTSTAT'] = $instances[$recurId];
|
||||
$attendee['SCHEDULE-STATUS'] = $requestStatus;
|
||||
// Un-setting the RSVP status, because we now know
|
||||
// that the attendee already replied.
|
||||
unset($attendee['RSVP']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$attendeeFound && $allowInvitationForwarding) {
|
||||
// Adding a new attendee. The iTip documentation calls this
|
||||
// a party crasher.
|
||||
$parameters = [
|
||||
'PARTSTAT' => $instances[$recurId],
|
||||
];
|
||||
if ($itipMessage->senderName) {
|
||||
$parameters['CN'] = $itipMessage->senderName;
|
||||
}
|
||||
$vevent->add('ATTENDEE', $itipMessage->sender, $parameters);
|
||||
}
|
||||
unset($instances[$recurId]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($masterObject === null) {
|
||||
// No master object, we can't add new instances.
|
||||
return null;
|
||||
}
|
||||
// If we got replies to instances that did not exist in the
|
||||
// original list, it means that new exceptions must be created.
|
||||
foreach ($instances as $recurId => $partstat) {
|
||||
$recurrenceIterator = new EventIterator($existingObject, $itipMessage->uid);
|
||||
$found = false;
|
||||
$iterations = 1000;
|
||||
do {
|
||||
$newObject = $recurrenceIterator->getEventObject();
|
||||
$recurrenceIterator->next();
|
||||
|
||||
// Compare the Unix timestamp returned by getTimestamp with the previously calculated timestamp.
|
||||
// If they are the same, then this is a matching recurrence, even though its date-time may have
|
||||
// been expressed in a different format/timezone.
|
||||
if (isset($newObject->{'RECURRENCE-ID'}) && $newObject->{'RECURRENCE-ID'}->getDateTime()->getTimestamp() === $recurId) {
|
||||
$found = true;
|
||||
}
|
||||
--$iterations;
|
||||
} while ($recurrenceIterator->valid() && !$found && $iterations);
|
||||
|
||||
// Invalid recurrence id. Skipping this object.
|
||||
if (!$found) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$newObject->remove('RRULE');
|
||||
$newObject->remove('EXDATE');
|
||||
$newObject->remove('RDATE');
|
||||
|
||||
$attendeeFound = false;
|
||||
if (isset($newObject->ATTENDEE)) {
|
||||
foreach ($newObject->ATTENDEE as $attendee) {
|
||||
if ($attendee->getValue() === $itipMessage->sender) {
|
||||
$attendeeFound = true;
|
||||
$attendee['PARTSTAT'] = $partstat;
|
||||
$attendee['SCHEDULE-STATUS'] = $requestStatus;
|
||||
unset($attendee['RSVP']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$attendeeFound && !$masterAllowInvitationForwarding) {
|
||||
continue;
|
||||
}
|
||||
if (!$attendeeFound) {
|
||||
// Adding a new attendee
|
||||
$parameters = [
|
||||
'PARTSTAT' => $partstat,
|
||||
];
|
||||
if ($itipMessage->senderName) {
|
||||
$parameters['CN'] = $itipMessage->senderName;
|
||||
}
|
||||
$newObject->add('ATTENDEE', $itipMessage->sender, $parameters);
|
||||
}
|
||||
$existingObject->add($newObject);
|
||||
}
|
||||
|
||||
return $existingObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|'master'
|
||||
*/
|
||||
protected function getRecurrenceKey(VEvent $vevent): int|string {
|
||||
/** @var list<Property> $recurrenceIds */
|
||||
$recurrenceIds = $vevent->select('RECURRENCE-ID');
|
||||
foreach ($recurrenceIds as $recurrenceId) {
|
||||
if ($recurrenceId instanceof DateTime) {
|
||||
return $recurrenceId->getDateTime()->getTimestamp();
|
||||
}
|
||||
}
|
||||
return 'master';
|
||||
}
|
||||
|
||||
protected function getFirstAttendee(VEvent $vevent): ?CalAddress {
|
||||
/** @var list<Property> $attendees */
|
||||
$attendees = $vevent->select('ATTENDEE');
|
||||
foreach ($attendees as $attendee) {
|
||||
if ($attendee instanceof CalAddress) {
|
||||
return $attendee;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function allowInvitationForwarding(VEvent $vevent): bool {
|
||||
$properties = $vevent->select(self::INVITATION_FORWARDING_PROPERTY);
|
||||
foreach ($properties as $property) {
|
||||
if ($property instanceof Boolean) {
|
||||
return $property->getValue() === 'TRUE';
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ declare(strict_types=1);
|
|||
namespace OCA\DAV\Tests\unit\CalDAV;
|
||||
|
||||
use OCA\DAV\CalDAV\TipBroker;
|
||||
use Sabre\VObject;
|
||||
use Sabre\VObject\Component\VCalendar;
|
||||
use Sabre\VObject\ITip\Message;
|
||||
use Test\TestCase;
|
||||
|
||||
class TipBrokerTest extends TestCase {
|
||||
|
|
@ -21,6 +23,8 @@ class TipBrokerTest extends TestCase {
|
|||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
VCalendar::$propertyMap[TipBroker::INVITATION_FORWARDING_PROPERTY] = VObject\Property\Boolean::class;
|
||||
|
||||
$this->broker = new TipBroker();
|
||||
|
||||
$this->templateEventInfo = [
|
||||
|
|
@ -41,8 +45,8 @@ class TipBrokerTest extends TestCase {
|
|||
$vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
|
||||
$vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
|
||||
$vEvent->add('SUMMARY', 'Test Event');
|
||||
$vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
|
||||
$vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
|
||||
$vEvent->add('ORGANIZER', 'mailto:organizer@example.org', ['CN' => 'Organizer']);
|
||||
$vEvent->add('ATTENDEE', 'mailto:attendee1@example.org', [
|
||||
'CN' => 'Attendee One',
|
||||
'CUTYPE' => 'INDIVIDUAL',
|
||||
'PARTSTAT' => 'NEEDS-ACTION',
|
||||
|
|
@ -65,8 +69,8 @@ class TipBrokerTest extends TestCase {
|
|||
$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', [
|
||||
$vEvent->add('ORGANIZER', 'mailto:organizer@example.org', ['CN' => 'Organizer']);
|
||||
$vEvent->add('ATTENDEE', 'mailto:attendee1@example.org', [
|
||||
'CN' => 'Attendee One',
|
||||
'CUTYPE' => 'INDIVIDUAL',
|
||||
'PARTSTAT' => 'NEEDS-ACTION',
|
||||
|
|
@ -75,6 +79,12 @@ class TipBrokerTest extends TestCase {
|
|||
]);
|
||||
}
|
||||
|
||||
protected function tearDown(): void {
|
||||
unset(VCalendar::$propertyMap[TipBroker::INVITATION_FORWARDING_PROPERTY]);
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests user creating a new singleton or recurring event
|
||||
*/
|
||||
|
|
@ -159,7 +169,7 @@ class TipBrokerTest extends TestCase {
|
|||
$mutatedCalendar = clone $this->vCalendar1a;
|
||||
$mutatedCalendar->VEVENT->{'LAST-MODIFIED'}->setValue('20240701T020000Z');
|
||||
$mutatedCalendar->VEVENT->SEQUENCE->setValue(2);
|
||||
$mutatedCalendar->VEVENT->add('ATTENDEE', 'mailto:attendee2@testing.com', [
|
||||
$mutatedCalendar->VEVENT->add('ATTENDEE', 'mailto:attendee2@example.org', [
|
||||
'CN' => 'Attendee Two',
|
||||
'CUTYPE' => 'INDIVIDUAL',
|
||||
'PARTSTAT' => 'NEEDS-ACTION',
|
||||
|
|
@ -185,7 +195,7 @@ class TipBrokerTest extends TestCase {
|
|||
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', [
|
||||
$originalCalendar->VEVENT->add('ATTENDEE', 'mailto:attendee2@example.org', [
|
||||
'CN' => 'Attendee Two',
|
||||
'CUTYPE' => 'INDIVIDUAL',
|
||||
'PARTSTAT' => 'NEEDS-ACTION',
|
||||
|
|
@ -197,7 +207,7 @@ class TipBrokerTest extends TestCase {
|
|||
$mutatedCalendar->VEVENT->{'LAST-MODIFIED'}->setValue('20240701T020000Z');
|
||||
$mutatedCalendar->VEVENT->SEQUENCE->setValue(2);
|
||||
$mutatedCalendar->VEVENT->remove('ATTENDEE');
|
||||
$mutatedCalendar->VEVENT->add('ATTENDEE', 'mailto:attendee1@testing.com', [
|
||||
$mutatedCalendar->VEVENT->add('ATTENDEE', 'mailto:attendee1@example.org', [
|
||||
'CN' => 'Attendee One',
|
||||
'CUTYPE' => 'INDIVIDUAL',
|
||||
'PARTSTAT' => 'NEEDS-ACTION',
|
||||
|
|
@ -214,7 +224,7 @@ class TipBrokerTest extends TestCase {
|
|||
$this->assertEquals($mutatedCalendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
|
||||
$this->assertEquals('CANCEL', $messages[1]->method);
|
||||
$this->assertEquals($mutatedCalendar->VEVENT->ORGANIZER->getValue(), $messages[1]->sender);
|
||||
$this->assertEquals('mailto:attendee2@testing.com', $messages[1]->recipient);
|
||||
$this->assertEquals('mailto:attendee2@example.org', $messages[1]->recipient);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -377,7 +387,7 @@ class TipBrokerTest extends TestCase {
|
|||
$originalEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$originalCalendar]);
|
||||
$mutatedInstance = clone $originalInstance;
|
||||
$mutatedInstance->SEQUENCE->setValue(2);
|
||||
$mutatedInstance->add('ATTENDEE', 'mailto:attendee2@testing.com', [
|
||||
$mutatedInstance->add('ATTENDEE', 'mailto:attendee2@example.org', [
|
||||
'CN' => 'Attendee Two',
|
||||
'CUTYPE' => 'INDIVIDUAL',
|
||||
'PARTSTAT' => 'NEEDS-ACTION',
|
||||
|
|
@ -417,7 +427,7 @@ class TipBrokerTest extends TestCase {
|
|||
$originalInstance->SEQUENCE->setValue(1);
|
||||
$originalInstance->DTSTART->setValue('20240717T080000');
|
||||
$originalInstance->DTEND->setValue('20240717T090000');
|
||||
$originalInstance->add('ATTENDEE', 'mailto:attendee2@testing.com', [
|
||||
$originalInstance->add('ATTENDEE', 'mailto:attendee2@example.org', [
|
||||
'CN' => 'Attendee Two',
|
||||
'CUTYPE' => 'INDIVIDUAL',
|
||||
'PARTSTAT' => 'NEEDS-ACTION',
|
||||
|
|
@ -429,7 +439,7 @@ class TipBrokerTest extends TestCase {
|
|||
$mutatedInstance = clone $originalInstance;
|
||||
$mutatedInstance->SEQUENCE->setValue(2);
|
||||
$mutatedInstance->remove('ATTENDEE');
|
||||
$mutatedInstance->add('ATTENDEE', 'mailto:attendee1@testing.com', [
|
||||
$mutatedInstance->add('ATTENDEE', 'mailto:attendee1@example.org', [
|
||||
'CN' => 'Attendee One',
|
||||
'CUTYPE' => 'INDIVIDUAL',
|
||||
'PARTSTAT' => 'NEEDS-ACTION',
|
||||
|
|
@ -579,4 +589,364 @@ class TipBrokerTest extends TestCase {
|
|||
$this->assertFalse(isset($messages[0]->message->VEVENT->ATTENDEE['SCHEDULE-FORCE-SEND']));
|
||||
}
|
||||
|
||||
public function testProcessMessageReplyDisallowsInvitationForwarding(): void {
|
||||
$existingCalendar = clone $this->vCalendar1a;
|
||||
$existingCalendar->VEVENT->add(TipBroker::INVITATION_FORWARDING_PROPERTY, 'FALSE');
|
||||
$existingCalendar->VEVENT->ATTENDEE[0]->setValue('mailto:attendee1@example.org');
|
||||
$reply = new Message();
|
||||
$reply->uid = $existingCalendar->VEVENT->UID->getValue();
|
||||
$reply->component = 'VEVENT';
|
||||
$reply->sender = 'mailto:attendee2@example.org';
|
||||
$reply->senderName = 'Attendee Two';
|
||||
$reply->sequence = 1;
|
||||
$reply->message = new VCalendar();
|
||||
/** @var \Sabre\VObject\Component\VEvent $replyEvent */
|
||||
$replyEvent = $reply->message->add('VEVENT', []);
|
||||
$replyEvent->add('UID', $reply->uid);
|
||||
$replyEvent->add('ATTENDEE', $reply->sender, [
|
||||
'PARTSTAT' => 'ACCEPTED',
|
||||
]);
|
||||
|
||||
$result = $this->invokePrivate($this->broker, 'processMessageReply', [$reply, $existingCalendar]);
|
||||
|
||||
$this->assertSame($existingCalendar, $result);
|
||||
$this->assertCount(1, $result->VEVENT->ATTENDEE);
|
||||
$this->assertEquals('mailto:attendee1@example.org', $result->VEVENT->ATTENDEE[0]->getValue());
|
||||
}
|
||||
|
||||
public function testProcessMessageReplyUpdatesExistingAttendeeWhenInvitationForwardingDisabled(): void {
|
||||
$existingCalendar = clone $this->vCalendar1a;
|
||||
$existingCalendar->VEVENT->add(TipBroker::INVITATION_FORWARDING_PROPERTY, 'FALSE');
|
||||
$existingCalendar->VEVENT->ATTENDEE[0]->setValue('mailto:attendee1@example.org');
|
||||
$reply = new Message();
|
||||
$reply->uid = $existingCalendar->VEVENT->UID->getValue();
|
||||
$reply->component = 'VEVENT';
|
||||
$reply->sender = 'mailto:attendee1@example.org';
|
||||
$reply->senderName = 'Attendee One';
|
||||
$reply->sequence = 1;
|
||||
$reply->message = new VCalendar();
|
||||
/** @var \Sabre\VObject\Component\VEvent $replyEvent */
|
||||
$replyEvent = $reply->message->add('VEVENT', []);
|
||||
$replyEvent->add('UID', $reply->uid);
|
||||
$replyEvent->add('ATTENDEE', $reply->sender, [
|
||||
'PARTSTAT' => 'ACCEPTED',
|
||||
]);
|
||||
$replyEvent->add('REQUEST-STATUS', '2.0;Success');
|
||||
|
||||
$result = $this->invokePrivate($this->broker, 'processMessageReply', [$reply, $existingCalendar]);
|
||||
|
||||
$this->assertSame($existingCalendar, $result);
|
||||
$this->assertCount(1, $result->VEVENT->ATTENDEE);
|
||||
$this->assertEquals('mailto:attendee1@example.org', $result->VEVENT->ATTENDEE[0]->getValue());
|
||||
$this->assertEquals('ACCEPTED', $result->VEVENT->ATTENDEE[0]['PARTSTAT']->getValue());
|
||||
$this->assertEquals('2.0', $result->VEVENT->ATTENDEE[0]['SCHEDULE-STATUS']->getValue());
|
||||
$this->assertFalse(isset($result->VEVENT->ATTENDEE[0]['RSVP']));
|
||||
}
|
||||
|
||||
public function testProcessMessageReplyAllowsInvitationForwarding(): void {
|
||||
$existingCalendar = clone $this->vCalendar1a;
|
||||
$existingCalendar->VEVENT->add(TipBroker::INVITATION_FORWARDING_PROPERTY, 'TRUE');
|
||||
$existingCalendar->VEVENT->ATTENDEE[0]->setValue('mailto:attendee1@example.org');
|
||||
$reply = new Message();
|
||||
$reply->uid = $existingCalendar->VEVENT->UID->getValue();
|
||||
$reply->component = 'VEVENT';
|
||||
$reply->sender = 'mailto:attendee2@example.org';
|
||||
$reply->senderName = 'Attendee Two';
|
||||
$reply->sequence = 1;
|
||||
$reply->message = new VCalendar();
|
||||
/** @var \Sabre\VObject\Component\VEvent $replyEvent */
|
||||
$replyEvent = $reply->message->add('VEVENT', []);
|
||||
$replyEvent->add('UID', $reply->uid);
|
||||
$replyEvent->add('ATTENDEE', $reply->sender, [
|
||||
'PARTSTAT' => 'ACCEPTED',
|
||||
]);
|
||||
|
||||
$result = $this->invokePrivate($this->broker, 'processMessageReply', [$reply, $existingCalendar]);
|
||||
|
||||
$this->assertSame($existingCalendar, $result);
|
||||
$this->assertCount(2, $result->VEVENT->ATTENDEE);
|
||||
$this->assertEquals('mailto:attendee2@example.org', $result->VEVENT->ATTENDEE[1]->getValue());
|
||||
$this->assertEquals('ACCEPTED', $result->VEVENT->ATTENDEE[1]['PARTSTAT']->getValue());
|
||||
$this->assertEquals('Attendee Two', $result->VEVENT->ATTENDEE[1]['CN']->getValue());
|
||||
}
|
||||
|
||||
public function testProcessMessageReplyAllowsInvitationForwardingByDefault(): void {
|
||||
$existingCalendar = clone $this->vCalendar1a;
|
||||
$existingCalendar->VEVENT->ATTENDEE[0]->setValue('mailto:attendee1@example.org');
|
||||
$reply = new Message();
|
||||
$reply->uid = $existingCalendar->VEVENT->UID->getValue();
|
||||
$reply->component = 'VEVENT';
|
||||
$reply->sender = 'mailto:attendee2@example.org';
|
||||
$reply->senderName = 'Attendee Two';
|
||||
$reply->sequence = 1;
|
||||
$reply->message = new VCalendar();
|
||||
/** @var \Sabre\VObject\Component\VEvent $replyEvent */
|
||||
$replyEvent = $reply->message->add('VEVENT', []);
|
||||
$replyEvent->add('UID', $reply->uid);
|
||||
$replyEvent->add('ATTENDEE', $reply->sender, [
|
||||
'PARTSTAT' => 'ACCEPTED',
|
||||
]);
|
||||
|
||||
$result = $this->invokePrivate($this->broker, 'processMessageReply', [$reply, $existingCalendar]);
|
||||
|
||||
$this->assertSame($existingCalendar, $result);
|
||||
$this->assertCount(2, $result->VEVENT->ATTENDEE);
|
||||
$this->assertEquals('mailto:attendee2@example.org', $result->VEVENT->ATTENDEE[1]->getValue());
|
||||
$this->assertEquals('ACCEPTED', $result->VEVENT->ATTENDEE[1]['PARTSTAT']->getValue());
|
||||
$this->assertEquals('Attendee Two', $result->VEVENT->ATTENDEE[1]['CN']->getValue());
|
||||
}
|
||||
|
||||
public function testProcessMessageReplyIgnoresReplyWithoutAttendee(): void {
|
||||
$existingCalendar = clone $this->vCalendar1a;
|
||||
$reply = new Message();
|
||||
$reply->uid = $existingCalendar->VEVENT->UID->getValue();
|
||||
$reply->component = 'VEVENT';
|
||||
$reply->sender = 'mailto:attendee1@example.org';
|
||||
$reply->senderName = 'Attendee One';
|
||||
$reply->sequence = 1;
|
||||
$reply->message = new VCalendar();
|
||||
/** @var \Sabre\VObject\Component\VEvent $replyEvent */
|
||||
$replyEvent = $reply->message->add('VEVENT', []);
|
||||
$replyEvent->add('UID', $reply->uid);
|
||||
|
||||
$result = $this->invokePrivate($this->broker, 'processMessageReply', [$reply, $existingCalendar]);
|
||||
|
||||
$this->assertSame($existingCalendar, $result);
|
||||
$this->assertCount(1, $result->VEVENT->ATTENDEE);
|
||||
$this->assertEquals('NEEDS-ACTION', $result->VEVENT->ATTENDEE[0]['PARTSTAT']->getValue());
|
||||
$this->assertTrue(isset($result->VEVENT->ATTENDEE[0]['RSVP']));
|
||||
}
|
||||
|
||||
public function testProcessMessageReplyIgnoresReplyWithoutPartstat(): void {
|
||||
$existingCalendar = clone $this->vCalendar1a;
|
||||
$reply = new Message();
|
||||
$reply->uid = $existingCalendar->VEVENT->UID->getValue();
|
||||
$reply->component = 'VEVENT';
|
||||
$reply->sender = 'mailto:attendee1@example.org';
|
||||
$reply->senderName = 'Attendee One';
|
||||
$reply->sequence = 1;
|
||||
$reply->message = new VCalendar();
|
||||
/** @var \Sabre\VObject\Component\VEvent $replyEvent */
|
||||
$replyEvent = $reply->message->add('VEVENT', []);
|
||||
$replyEvent->add('UID', $reply->uid);
|
||||
$replyEvent->add('ATTENDEE', $reply->sender, []);
|
||||
|
||||
$result = $this->invokePrivate($this->broker, 'processMessageReply', [$reply, $existingCalendar]);
|
||||
|
||||
$this->assertSame($existingCalendar, $result);
|
||||
$this->assertCount(1, $result->VEVENT->ATTENDEE);
|
||||
$this->assertEquals('NEEDS-ACTION', $result->VEVENT->ATTENDEE[0]['PARTSTAT']->getValue());
|
||||
$this->assertTrue(isset($result->VEVENT->ATTENDEE[0]['RSVP']));
|
||||
}
|
||||
|
||||
public function testProcessMessageReplyDisallowsInvitationForwardingForGeneratedRecurringInstance(): void {
|
||||
$existingCalendar = clone $this->vCalendar2a;
|
||||
$existingCalendar->VEVENT->add(TipBroker::INVITATION_FORWARDING_PROPERTY, 'FALSE');
|
||||
$existingCalendar->VEVENT->ATTENDEE[0]->setValue('mailto:attendee1@example.org');
|
||||
$reply = new Message();
|
||||
$reply->uid = $existingCalendar->VEVENT->UID->getValue();
|
||||
$reply->component = 'VEVENT';
|
||||
$reply->sender = 'mailto:attendee2@example.org';
|
||||
$reply->senderName = 'Attendee Two';
|
||||
$reply->sequence = 1;
|
||||
$reply->message = new VCalendar();
|
||||
/** @var \Sabre\VObject\Component\VEvent $replyEvent */
|
||||
$replyEvent = $reply->message->add('VEVENT', []);
|
||||
$replyEvent->add('UID', $reply->uid);
|
||||
$replyEvent->add('RECURRENCE-ID', '20240715T080000', ['TZID' => 'America/Toronto']);
|
||||
$replyEvent->add('ATTENDEE', $reply->sender, [
|
||||
'PARTSTAT' => 'ACCEPTED',
|
||||
]);
|
||||
|
||||
$result = $this->invokePrivate($this->broker, 'processMessageReply', [$reply, $existingCalendar]);
|
||||
|
||||
$this->assertSame($existingCalendar, $result);
|
||||
$this->assertCount(1, $result->VEVENT);
|
||||
$this->assertCount(1, $result->VEVENT->ATTENDEE);
|
||||
$this->assertEquals('mailto:attendee1@example.org', $result->VEVENT->ATTENDEE[0]->getValue());
|
||||
}
|
||||
|
||||
public function testProcessMessageReplyUpdatesExistingAttendeeForGeneratedRecurringInstanceWhenInvitationForwardingDisabled(): void {
|
||||
$existingCalendar = clone $this->vCalendar2a;
|
||||
$existingCalendar->VEVENT->add(TipBroker::INVITATION_FORWARDING_PROPERTY, 'FALSE');
|
||||
$existingCalendar->VEVENT->ATTENDEE[0]->setValue('mailto:attendee1@example.org');
|
||||
$reply = new Message();
|
||||
$reply->uid = $existingCalendar->VEVENT->UID->getValue();
|
||||
$reply->component = 'VEVENT';
|
||||
$reply->sender = 'mailto:attendee1@example.org';
|
||||
$reply->senderName = 'Attendee One';
|
||||
$reply->sequence = 1;
|
||||
$reply->message = new VCalendar();
|
||||
/** @var \Sabre\VObject\Component\VEvent $replyEvent */
|
||||
$replyEvent = $reply->message->add('VEVENT', []);
|
||||
$replyEvent->add('UID', $reply->uid);
|
||||
$replyEvent->add('RECURRENCE-ID', '20240715T080000', ['TZID' => 'America/Toronto']);
|
||||
$replyEvent->add('ATTENDEE', $reply->sender, [
|
||||
'PARTSTAT' => 'ACCEPTED',
|
||||
]);
|
||||
$replyEvent->add('REQUEST-STATUS', '2.0;Success');
|
||||
|
||||
$result = $this->invokePrivate($this->broker, 'processMessageReply', [$reply, $existingCalendar]);
|
||||
|
||||
$this->assertSame($existingCalendar, $result);
|
||||
$this->assertCount(2, $result->VEVENT);
|
||||
$this->assertEquals('20240715T080000', $result->VEVENT[1]->{'RECURRENCE-ID'}->getValue());
|
||||
$this->assertCount(1, $result->VEVENT[1]->ATTENDEE);
|
||||
$this->assertEquals('mailto:attendee1@example.org', $result->VEVENT[1]->ATTENDEE[0]->getValue());
|
||||
$this->assertEquals('ACCEPTED', $result->VEVENT[1]->ATTENDEE[0]['PARTSTAT']->getValue());
|
||||
$this->assertEquals('2.0', $result->VEVENT[1]->ATTENDEE[0]['SCHEDULE-STATUS']->getValue());
|
||||
$this->assertFalse(isset($result->VEVENT[1]->ATTENDEE[0]['RSVP']));
|
||||
}
|
||||
|
||||
public function testProcessMessageReplyAllowsInvitationForwardingForDetachedRecurringExceptionWhenMasterDisallows(): void {
|
||||
$existingCalendar = clone $this->vCalendar2a;
|
||||
$existingCalendar->VEVENT->add(TipBroker::INVITATION_FORWARDING_PROPERTY, 'FALSE');
|
||||
$existingCalendar->VEVENT->ATTENDEE[0]->setValue('mailto:attendee1@example.org');
|
||||
/** @var \Sabre\VObject\Component\VEvent $detachedInstance */
|
||||
$detachedInstance = $existingCalendar->add('VEVENT', []);
|
||||
$detachedInstance->add('UID', $existingCalendar->VEVENT->UID->getValue());
|
||||
$detachedInstance->add('DTSTAMP', '20240701T000000Z');
|
||||
$detachedInstance->add('RECURRENCE-ID', '20240715T080000', ['TZID' => 'America/Toronto']);
|
||||
$detachedInstance->add('DTSTART', '20240715T080000', ['TZID' => 'America/Toronto']);
|
||||
$detachedInstance->add('DTEND', '20240715T090000', ['TZID' => 'America/Toronto']);
|
||||
$detachedInstance->add('SUMMARY', 'Detached Test Event');
|
||||
$detachedInstance->add('ORGANIZER', 'mailto:organizer@example.org', ['CN' => 'Organizer']);
|
||||
$detachedInstance->add(TipBroker::INVITATION_FORWARDING_PROPERTY, 'TRUE');
|
||||
$detachedInstance->add('ATTENDEE', 'mailto:attendee1@example.org', [
|
||||
'CN' => 'Attendee One',
|
||||
'CUTYPE' => 'INDIVIDUAL',
|
||||
'PARTSTAT' => 'NEEDS-ACTION',
|
||||
'ROLE' => 'REQ-PARTICIPANT',
|
||||
'RSVP' => 'TRUE',
|
||||
]);
|
||||
|
||||
$reply = new Message();
|
||||
$reply->uid = $existingCalendar->VEVENT->UID->getValue();
|
||||
$reply->component = 'VEVENT';
|
||||
$reply->sender = 'mailto:attendee2@example.org';
|
||||
$reply->senderName = 'Attendee Two';
|
||||
$reply->sequence = 1;
|
||||
$reply->message = new VCalendar();
|
||||
/** @var \Sabre\VObject\Component\VEvent $replyEvent */
|
||||
$replyEvent = $reply->message->add('VEVENT', []);
|
||||
$replyEvent->add('UID', $reply->uid);
|
||||
$replyEvent->add('RECURRENCE-ID', '20240715T080000', ['TZID' => 'America/Toronto']);
|
||||
$replyEvent->add('ATTENDEE', $reply->sender, [
|
||||
'PARTSTAT' => 'ACCEPTED',
|
||||
]);
|
||||
|
||||
$result = $this->invokePrivate($this->broker, 'processMessageReply', [$reply, $existingCalendar]);
|
||||
|
||||
$this->assertSame($existingCalendar, $result);
|
||||
$this->assertCount(2, $result->VEVENT);
|
||||
$this->assertCount(2, $result->VEVENT[1]->ATTENDEE);
|
||||
$this->assertEquals('mailto:attendee2@example.org', $result->VEVENT[1]->ATTENDEE[1]->getValue());
|
||||
}
|
||||
|
||||
public function testProcessMessageReplyDisallowsInvitationForwardingForDetachedRecurringExceptionWhenMasterAllows(): void {
|
||||
$existingCalendar = clone $this->vCalendar2a;
|
||||
$existingCalendar->VEVENT->add(TipBroker::INVITATION_FORWARDING_PROPERTY, 'TRUE');
|
||||
$existingCalendar->VEVENT->ATTENDEE[0]->setValue('mailto:attendee1@example.org');
|
||||
/** @var \Sabre\VObject\Component\VEvent $detachedInstance */
|
||||
$detachedInstance = $existingCalendar->add('VEVENT', []);
|
||||
$detachedInstance->add('UID', $existingCalendar->VEVENT->UID->getValue());
|
||||
$detachedInstance->add('DTSTAMP', '20240701T000000Z');
|
||||
$detachedInstance->add('RECURRENCE-ID', '20240715T080000', ['TZID' => 'America/Toronto']);
|
||||
$detachedInstance->add('DTSTART', '20240715T080000', ['TZID' => 'America/Toronto']);
|
||||
$detachedInstance->add('DTEND', '20240715T090000', ['TZID' => 'America/Toronto']);
|
||||
$detachedInstance->add('SUMMARY', 'Detached Test Event');
|
||||
$detachedInstance->add('ORGANIZER', 'mailto:organizer@example.org', ['CN' => 'Organizer']);
|
||||
$detachedInstance->add(TipBroker::INVITATION_FORWARDING_PROPERTY, 'FALSE');
|
||||
$detachedInstance->add('ATTENDEE', 'mailto:attendee1@example.org', [
|
||||
'CN' => 'Attendee One',
|
||||
'CUTYPE' => 'INDIVIDUAL',
|
||||
'PARTSTAT' => 'NEEDS-ACTION',
|
||||
'ROLE' => 'REQ-PARTICIPANT',
|
||||
'RSVP' => 'TRUE',
|
||||
]);
|
||||
|
||||
$reply = new Message();
|
||||
$reply->uid = $existingCalendar->VEVENT->UID->getValue();
|
||||
$reply->component = 'VEVENT';
|
||||
$reply->sender = 'mailto:attendee2@example.org';
|
||||
$reply->senderName = 'Attendee Two';
|
||||
$reply->sequence = 1;
|
||||
$reply->message = new VCalendar();
|
||||
/** @var \Sabre\VObject\Component\VEvent $replyEvent */
|
||||
$replyEvent = $reply->message->add('VEVENT', []);
|
||||
$replyEvent->add('UID', $reply->uid);
|
||||
$replyEvent->add('RECURRENCE-ID', '20240715T080000', ['TZID' => 'America/Toronto']);
|
||||
$replyEvent->add('ATTENDEE', $reply->sender, [
|
||||
'PARTSTAT' => 'ACCEPTED',
|
||||
]);
|
||||
|
||||
$result = $this->invokePrivate($this->broker, 'processMessageReply', [$reply, $existingCalendar]);
|
||||
|
||||
$this->assertSame($existingCalendar, $result);
|
||||
$this->assertCount(2, $result->VEVENT);
|
||||
$this->assertCount(1, $result->VEVENT[1]->ATTENDEE);
|
||||
$this->assertEquals('mailto:attendee1@example.org', $result->VEVENT[1]->ATTENDEE[0]->getValue());
|
||||
}
|
||||
|
||||
public function testProcessMessageReplyAllowsInvitationForwardingForGeneratedRecurringInstance(): void {
|
||||
$existingCalendar = clone $this->vCalendar2a;
|
||||
$existingCalendar->VEVENT->add(TipBroker::INVITATION_FORWARDING_PROPERTY, 'TRUE');
|
||||
$existingCalendar->VEVENT->ATTENDEE[0]->setValue('mailto:attendee1@example.org');
|
||||
$reply = new Message();
|
||||
$reply->uid = $existingCalendar->VEVENT->UID->getValue();
|
||||
$reply->component = 'VEVENT';
|
||||
$reply->sender = 'mailto:attendee2@example.org';
|
||||
$reply->senderName = 'Attendee Two';
|
||||
$reply->sequence = 1;
|
||||
$reply->message = new VCalendar();
|
||||
/** @var \Sabre\VObject\Component\VEvent $replyEvent */
|
||||
$replyEvent = $reply->message->add('VEVENT', []);
|
||||
$replyEvent->add('UID', $reply->uid);
|
||||
$replyEvent->add('RECURRENCE-ID', '20240715T080000', ['TZID' => 'America/Toronto']);
|
||||
$replyEvent->add('ATTENDEE', $reply->sender, [
|
||||
'PARTSTAT' => 'ACCEPTED',
|
||||
]);
|
||||
|
||||
$result = $this->invokePrivate($this->broker, 'processMessageReply', [$reply, $existingCalendar]);
|
||||
|
||||
$this->assertSame($existingCalendar, $result);
|
||||
$this->assertCount(2, $result->VEVENT);
|
||||
$this->assertEquals('20240715T080000', $result->VEVENT[1]->{'RECURRENCE-ID'}->getValue());
|
||||
$this->assertFalse(isset($result->VEVENT[1]->RRULE));
|
||||
$this->assertCount(2, $result->VEVENT[1]->ATTENDEE);
|
||||
$this->assertEquals('mailto:attendee2@example.org', $result->VEVENT[1]->ATTENDEE[1]->getValue());
|
||||
$this->assertEquals('ACCEPTED', $result->VEVENT[1]->ATTENDEE[1]['PARTSTAT']->getValue());
|
||||
$this->assertEquals('Attendee Two', $result->VEVENT[1]->ATTENDEE[1]['CN']->getValue());
|
||||
}
|
||||
|
||||
public function testProcessMessageReplyAllowsInvitationForwardingByDefaultForGeneratedRecurringInstance(): void {
|
||||
$existingCalendar = clone $this->vCalendar2a;
|
||||
$existingCalendar->VEVENT->ATTENDEE[0]->setValue('mailto:attendee1@example.org');
|
||||
$reply = new Message();
|
||||
$reply->uid = $existingCalendar->VEVENT->UID->getValue();
|
||||
$reply->component = 'VEVENT';
|
||||
$reply->sender = 'mailto:attendee2@example.org';
|
||||
$reply->senderName = 'Attendee Two';
|
||||
$reply->sequence = 1;
|
||||
$reply->message = new VCalendar();
|
||||
/** @var \Sabre\VObject\Component\VEvent $replyEvent */
|
||||
$replyEvent = $reply->message->add('VEVENT', []);
|
||||
$replyEvent->add('UID', $reply->uid);
|
||||
$replyEvent->add('RECURRENCE-ID', '20240715T080000', ['TZID' => 'America/Toronto']);
|
||||
$replyEvent->add('ATTENDEE', $reply->sender, [
|
||||
'PARTSTAT' => 'ACCEPTED',
|
||||
]);
|
||||
|
||||
$result = $this->invokePrivate($this->broker, 'processMessageReply', [$reply, $existingCalendar]);
|
||||
|
||||
$this->assertSame($existingCalendar, $result);
|
||||
$this->assertCount(2, $result->VEVENT);
|
||||
$this->assertEquals('20240715T080000', $result->VEVENT[1]->{'RECURRENCE-ID'}->getValue());
|
||||
$this->assertFalse(isset($result->VEVENT[1]->RRULE));
|
||||
$this->assertCount(2, $result->VEVENT[1]->ATTENDEE);
|
||||
$this->assertEquals('mailto:attendee2@example.org', $result->VEVENT[1]->ATTENDEE[1]->getValue());
|
||||
$this->assertEquals('ACCEPTED', $result->VEVENT[1]->ATTENDEE[1]['PARTSTAT']->getValue());
|
||||
$this->assertEquals('Attendee Two', $result->VEVENT[1]->ATTENDEE[1]['CN']->getValue());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue