Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions apps/dav/lib/CalDAV/Schedule/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@
$partStat = 'ACCEPTED';
} else {
$partStat = 'DECLINED';
$this->declineDeliveredObject($principalUri, $iTipMessage->recipient, $uid, $recurrenceId);
}

$vObject = Reader::read(vsprintf($message, [
Expand Down Expand Up @@ -506,6 +507,51 @@
return $properties[$calendarUserType];
}

/**
* Marks the delivered copy as declined, so it doesn't block the slot
*
* @param string $principalUri
* @param string $recipient
* @param string $uid
* @param string $recurrenceId
* @return void
*/
private function declineDeliveredObject(string $principalUri, string $recipient, string $uid, string $recurrenceId):void {
/** @var \OCA\DAV\CalDAV\Plugin $caldavPlugin */
$caldavPlugin = $this->server->getPlugin('caldav');
$homePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUri);
if (!$homePath) {
return;
}

/** @var CalendarHome $calendarHome */
$calendarHome = $this->server->tree->getNodeForPath($homePath);
$objectPath = $calendarHome->getCalendarObjectByUID($uid);
if (!$objectPath) {
return;
}

/** @var CalendarObject $calendarObject */
$calendarObject = $this->server->tree->getNodeForPath($homePath . '/' . $objectPath);
/** @var VCalendar $vCalendar */
$vCalendar = Reader::read($calendarObject->get());
foreach ($vCalendar->VEVENT as $vevent) {

Check failure on line 538 in apps/dav/lib/CalDAV/Schedule/Plugin.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

UndefinedMagicPropertyFetch

apps/dav/lib/CalDAV/Schedule/Plugin.php:538:12: UndefinedMagicPropertyFetch: Magic instance property Sabre\VObject\Component\VCalendar::$VEVENT is not defined (see https://psalm.dev/218)
$eventRecurrenceId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->serialize() : '';
if ($eventRecurrenceId !== $recurrenceId) {
continue;
}

foreach ($vevent->select('ATTENDEE') as $attendee) {
if (strcasecmp($attendee->getValue(), $recipient) === 0) {
$attendee['PARTSTAT'] = 'DECLINED';
}
}
$vevent->TRANSP = 'TRANSPARENT';
}

$calendarObject->put($vCalendar->serialize());
}

/**
* @param ITip\Message $iTipMessage
* @return null|Property
Expand Down
114 changes: 114 additions & 0 deletions apps/dav/tests/unit/CalDAV/Schedule/PluginTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\CalDAV\Calendar;
use OCA\DAV\CalDAV\CalendarHome;
use OCA\DAV\CalDAV\CalendarObject;
use OCA\DAV\CalDAV\DefaultCalendarValidator;
use OCA\DAV\CalDAV\Plugin as CalDAVPlugin;
use OCA\DAV\CalDAV\Schedule\Plugin;
Expand All @@ -20,19 +21,22 @@
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
use Sabre\CalDAV\Backend\BackendInterface;
use Sabre\CalDAV\Schedule\Inbox;
use Sabre\DAV\PropFind;
use Sabre\DAV\Server;
use Sabre\DAV\Tree;
use Sabre\DAV\Xml\Property\Href;
use Sabre\DAV\Xml\Property\LocalHref;
use Sabre\DAVACL\IPrincipal;
use Sabre\DAVACL\Plugin as ACLPlugin;
use Sabre\HTTP\Request;
use Sabre\HTTP\Response;
use Sabre\HTTP\ResponseInterface;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\ITip\Message;
use Sabre\VObject\Parameter;
use Sabre\VObject\Property\ICalendar\CalAddress;
use Sabre\VObject\Reader;
use Sabre\Xml\Service;
use Test\TestCase;

Expand Down Expand Up @@ -768,4 +772,114 @@ public function testCalendarObjectChangeWithSchedulingDisabled(): void {
$newFlag
);
}

public function testScheduleLocalDeliveryDeclinesBookedRoom(): void {
$caldavNS = '{urn:ietf:params:xml:ns:caldav}';
// construct the event that already books the room
$bookedCalendar = new VCalendar();
$bookedEvent = $bookedCalendar->add('VEVENT', []);
$bookedEvent->UID->setValue('booked-event');
$bookedEvent->add('DTSTART', '20261201T100000Z');
$bookedEvent->add('DTEND', '20261201T110000Z');
// construct the request for the same time slot
$vCalendar = new VCalendar();
$vCalendar->add('METHOD', 'REQUEST');
$vEvent = $vCalendar->add('VEVENT', []);
$vEvent->UID->setValue('new-event');
$vEvent->add('DTSTART', '20261201T100000Z');
$vEvent->add('DTEND', '20261201T110000Z');
$vEvent->add('ORGANIZER', 'mailto:user1@testing.local');
$vEvent->add('ATTENDEE', 'mailto:room1@testing.local', [
'CUTYPE' => 'ROOM',
'PARTSTAT' => 'NEEDS-ACTION',
'RSVP' => 'TRUE'
]);
$iTipMessage = new Message();
$iTipMessage->uid = 'new-event';
$iTipMessage->component = 'VEVENT';
$iTipMessage->method = 'REQUEST';
$iTipMessage->sender = 'mailto:user1@testing.local';
$iTipMessage->recipient = 'mailto:room1@testing.local';
$iTipMessage->message = $vCalendar;
// construct server plugins
$aclPlugin = $this->createMock(ACLPlugin::class);
$aclPlugin->method('getPrincipalByUri')
->willReturn('principals/calendar-rooms/room1');
$aclPlugin->method('checkPrivileges')
->willReturn(true);
$aclPlugin->method('principalSearch')
->willReturn([[200 => [
$caldavNS . 'calendar-home-set' => new Href('calendars/room1'),
$caldavNS . 'schedule-inbox-URL' => new Href('calendars/room1/inbox'),
]]]);
$calDAVPlugin = $this->createMock(CalDAVPlugin::class);
$calDAVPlugin->method('getCalendarHomeForPrincipal')
->willReturn('calendars/room1');
$this->server->method('getPlugin')
->willReturnMap([
['acl', $aclPlugin],
['caldav', $calDAVPlugin],
]);
$this->server->method('getProperties')
->willReturnCallback(function (string $path, array $propertyNames) use ($caldavNS) {
if ($propertyNames === [Plugin::CALENDAR_USER_TYPE]) {
return [Plugin::CALENDAR_USER_TYPE => 'ROOM'];
}
if ($path === 'principals/calendar-rooms/room1') {
return [
$caldavNS . 'calendar-home-set' => new Href('calendars/room1'),
$caldavNS . 'schedule-inbox-URL' => new Href('calendars/room1/inbox'),
$caldavNS . 'schedule-default-calendar-URL' => new Href('calendars/room1/calendar'),
];
}
return [];
});
// construct server tree
$storedData = null;
$bookedObject = $this->createMock(CalendarObject::class);
$bookedObject->method('get')
->willReturn($bookedCalendar->serialize());
$calendarNode = $this->createMock(Calendar::class);
$calendarNode->method('calendarQuery')
->willReturn(['booked-event.ics']);
$calendarNode->method('getChild')
->with('booked-event.ics')
->willReturn($bookedObject);
$calendarNode->expects($this->once())
->method('createFile')
->willReturnCallback(function (string $name, string $data) use (&$storedData): void {
$storedData = $data;
});
$calendarHome = $this->createMock(CalendarHome::class);
$calendarHome->method('getCalendarObjectByUID')
->willReturnOnConsecutiveCalls(null, 'calendar/new-event.ics');
$calendarHome->method('getChildren')
->willReturn([$calendarNode]);
$deliveredObject = $this->createMock(CalendarObject::class);
$deliveredObject->method('get')
->willReturnCallback(function () use (&$storedData) {
return $storedData;
});
$deliveredObject->expects($this->once())
->method('put')
->willReturnCallback(function (string $data) use (&$storedData): void {
$storedData = $data;
});
$tree = $this->createMock(Tree::class);
$tree->method('getNodeForPath')
->willReturnMap([
['calendars/room1', $calendarHome],
['calendars/room1/inbox', $this->createMock(Inbox::class)],
['calendars/room1/calendar', $calendarNode],
['calendars/room1/calendar/new-event.ics', $deliveredObject],
]);
$this->server->tree = $tree;
// execute method
$this->plugin->scheduleLocalDelivery($iTipMessage);
// test that the copy in the room calendar no longer blocks the slot
$this->assertEquals('1.2;Message delivered locally', $iTipMessage->scheduleStatus);
$storedEvent = Reader::read($storedData)->VEVENT;
$this->assertEquals('DECLINED', $storedEvent->ATTENDEE['PARTSTAT']->getValue());
$this->assertEquals('TRANSPARENT', $storedEvent->TRANSP->getValue());
}
}
Loading