aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAnna Larch <anna@nextcloud.com>2022-06-23 22:17:53 +0200
committerAnna Larch <anna@nextcloud.com>2022-08-22 22:10:12 +0200
commit4ca4b0279372cd6fe11f717ed697f905ecb1e4a2 (patch)
tree6a6c1fbddc1dd6d06e653cd59475a7d4e8be2374
parent2576609aac3555da0926c27b879df610a8b0f43c (diff)
downloadnextcloud-server-4ca4b0279372cd6fe11f717ed697f905ecb1e4a2.tar.gz
nextcloud-server-4ca4b0279372cd6fe11f717ed697f905ecb1e4a2.zip
Support iMIP invitations from Mail
Signed-off-by: Anna Larch <anna@nextcloud.com>
-rw-r--r--apps/dav/lib/CalDAV/CalDavBackend.php4
-rw-r--r--apps/dav/lib/CalDAV/CalendarHome.php1
-rw-r--r--apps/dav/lib/CalDAV/CalendarImpl.php71
-rw-r--r--apps/dav/lib/CalDAV/CalendarProvider.php1
-rw-r--r--apps/dav/tests/unit/CalDAV/CalendarImplTest.php79
-rw-r--r--lib/private/Calendar/Manager.php153
-rw-r--r--lib/public/Calendar/ICreateFromString.php9
-rw-r--r--lib/public/Calendar/IManager.php14
-rw-r--r--tests/lib/Calendar/ManagerTest.php377
9 files changed, 698 insertions, 11 deletions
diff --git a/apps/dav/lib/CalDAV/CalDavBackend.php b/apps/dav/lib/CalDAV/CalDavBackend.php
index 2bbdc51f42e..42df838523d 100644
--- a/apps/dav/lib/CalDAV/CalDavBackend.php
+++ b/apps/dav/lib/CalDAV/CalDavBackend.php
@@ -1864,6 +1864,10 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
}
+ if(isset($options['uid'])) {
+ $outerQuery->andWhere($outerQuery->expr()->eq('uid', $outerQuery->createNamedParameter($options['uid'])));
+ }
+
if (!empty($options['types'])) {
$or = $outerQuery->expr()->orX();
foreach ($options['types'] as $type) {
diff --git a/apps/dav/lib/CalDAV/CalendarHome.php b/apps/dav/lib/CalDAV/CalendarHome.php
index ceeba31800e..cd6ae1c2f7f 100644
--- a/apps/dav/lib/CalDAV/CalendarHome.php
+++ b/apps/dav/lib/CalDAV/CalendarHome.php
@@ -206,7 +206,6 @@ class CalendarHome extends \Sabre\CalDAV\CalendarHome {
return $this->caldavBackend->calendarSearch($principalUri, $filters, $limit, $offset);
}
-
public function enableCachedSubscriptionsForThisRequest() {
$this->returnCachedSubscriptions = true;
}
diff --git a/apps/dav/lib/CalDAV/CalendarImpl.php b/apps/dav/lib/CalDAV/CalendarImpl.php
index 27a428a4075..a04e520c6bc 100644
--- a/apps/dav/lib/CalDAV/CalendarImpl.php
+++ b/apps/dav/lib/CalDAV/CalendarImpl.php
@@ -29,10 +29,19 @@ namespace OCA\DAV\CalDAV;
use OCA\DAV\CalDAV\Auth\CustomPrincipalPlugin;
use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer;
+use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Calendar\Exceptions\CalendarException;
use OCP\Calendar\ICreateFromString;
use OCP\Constants;
+use OCP\Security\ISecureRandom;
+use Psr\Log\LoggerInterface;
use Sabre\DAV\Exception\Conflict;
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\Component\VEvent;
+use Sabre\VObject\Document;
+use Sabre\VObject\ITip\Message;
+use Sabre\VObject\Property\VCard\DateTime;
+use Sabre\VObject\Reader;
use function Sabre\Uri\split as uriSplit;
class CalendarImpl implements ICreateFromString {
@@ -46,13 +55,6 @@ class CalendarImpl implements ICreateFromString {
/** @var array */
private $calendarInfo;
- /**
- * CalendarImpl constructor.
- *
- * @param Calendar $calendar
- * @param array $calendarInfo
- * @param CalDavBackend $backend
- */
public function __construct(Calendar $calendar,
array $calendarInfo,
CalDavBackend $backend) {
@@ -177,4 +179,59 @@ class CalendarImpl implements ICreateFromString {
fclose($stream);
}
}
+
+ /**
+ * @throws CalendarException
+ */
+ public function handleIMipMessage(string $name, string $calendarData): void {
+ $server = new InvitationResponseServer(false);
+
+ /** @var CustomPrincipalPlugin $plugin */
+ $plugin = $server->server->getPlugin('auth');
+ // we're working around the previous implementation
+ // that only allowed the public system principal to be used
+ // so set the custom principal here
+ $plugin->setCurrentPrincipal($this->calendar->getPrincipalURI());
+
+ if (empty($this->calendarInfo['uri'])) {
+ throw new CalendarException('Could not write to calendar as URI parameter is missing');
+ }
+ // Force calendar change URI
+ /** @var Schedule\Plugin $schedulingPlugin */
+ $schedulingPlugin = $server->server->getPlugin('caldav-schedule');
+ // Let sabre handle the rest
+ $iTipMessage = new Message();
+ /** @var VCalendar $vObject */
+ $vObject = Reader::read($calendarData);
+ /** @var VEvent $vEvent */
+ $vEvent = $vObject->{'VEVENT'};
+
+ if($vObject->{'METHOD'} === null) {
+ throw new CalendarException('No Method provided for scheduling data. Could not process message');
+ }
+
+ if(!isset($vEvent->{'ORGANIZER'}) || !isset($vEvent->{'ATTENDEE'})) {
+ throw new CalendarException('Could not process scheduling data, neccessary data missing from ICAL');
+ }
+ $orgaizer = $vEvent->{'ORGANIZER'}->getValue();
+ $attendee = $vEvent->{'ATTENDEE'}->getValue();
+
+ $iTipMessage->method = $vObject->{'METHOD'}->getValue();
+ if($iTipMessage->method === 'REPLY') {
+ if ($server->isExternalAttendee($vEvent->{'ATTENDEE'}->getValue())) {
+ $iTipMessage->recipient = $orgaizer;
+ } else {
+ $iTipMessage->recipient = $attendee;
+ }
+ $iTipMessage->sender = $attendee;
+ } else if($iTipMessage->method === 'CANCEL') {
+ $iTipMessage->recipient = $attendee;
+ $iTipMessage->sender = $orgaizer;
+ }
+ $iTipMessage->uid = isset($vEvent->{'UID'}) ? $vEvent->{'UID'}->getValue() : '';
+ $iTipMessage->component = 'VEVENT';
+ $iTipMessage->sequence = isset($vEvent->{'SEQUENCE'}) ? (int)$vEvent->{'SEQUENCE'}->getValue() : 0;
+ $iTipMessage->message = $vObject;
+ $schedulingPlugin->scheduleLocalDelivery($iTipMessage);
+ }
}
diff --git a/apps/dav/lib/CalDAV/CalendarProvider.php b/apps/dav/lib/CalDAV/CalendarProvider.php
index f29c601db2d..5779111add3 100644
--- a/apps/dav/lib/CalDAV/CalendarProvider.php
+++ b/apps/dav/lib/CalDAV/CalendarProvider.php
@@ -26,6 +26,7 @@ declare(strict_types=1);
namespace OCA\DAV\CalDAV;
use OCP\Calendar\ICalendarProvider;
+use OCP\Calendar\ICreateFromString;
use OCP\IConfig;
use OCP\IL10N;
use Psr\Log\LoggerInterface;
diff --git a/apps/dav/tests/unit/CalDAV/CalendarImplTest.php b/apps/dav/tests/unit/CalDAV/CalendarImplTest.php
index af8c056cac7..6842bdadb53 100644
--- a/apps/dav/tests/unit/CalDAV/CalendarImplTest.php
+++ b/apps/dav/tests/unit/CalDAV/CalendarImplTest.php
@@ -25,9 +25,13 @@
*/
namespace OCA\DAV\Tests\unit\CalDAV;
+use OCA\DAV\CalDAV\Auth\CustomPrincipalPlugin;
use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\CalDAV\Calendar;
use OCA\DAV\CalDAV\CalendarImpl;
+use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer;
+use OCA\DAV\CalDAV\Schedule\Plugin;
+use PHPUnit\Framework\MockObject\MockObject;
class CalendarImplTest extends \Test\TestCase {
@@ -51,6 +55,7 @@ class CalendarImplTest extends \Test\TestCase {
'id' => 'fancy_id_123',
'{DAV:}displayname' => 'user readable name 123',
'{http://apple.com/ns/ical/}calendar-color' => '#AABBCC',
+ 'uri' => '/this/is/a/uri'
];
$this->backend = $this->createMock(CalDavBackend::class);
@@ -125,4 +130,78 @@ class CalendarImplTest extends \Test\TestCase {
$this->assertEquals(31, $this->calendarImpl->getPermissions());
}
+
+ public function testHandleImipMessage(): void {
+ $invitationResponseServer = $this->createConfiguredMock(InvitationResponseServer::class, [
+ 'server' => $this->createConfiguredMock(CalDavBackend::class, [
+ 'getPlugin' => [
+ 'auth' => $this->createMock(CustomPrincipalPlugin::class),
+ 'schedule' => $this->createMock(Plugin::class)
+ ]
+ ])
+ ]);
+
+ $message = <<<EOF
+BEGIN:VCALENDAR
+PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN
+METHOD:REPLY
+VERSION:2.0
+BEGIN:VEVENT
+ATTENDEE;PARTSTAT=mailto:lewis@stardew-tent-living.com:ACCEPTED
+ORGANIZER:mailto:pierre@generalstore.com
+UID:aUniqueUid
+SEQUENCE:2
+REQUEST-STATUS:2.0;Success
+%sEND:VEVENT
+END:VCALENDAR
+EOF;
+
+ /** @var CustomPrincipalPlugin|MockObject $authPlugin */
+ $authPlugin = $invitationResponseServer->server->getPlugin('auth');
+ $authPlugin->expects(self::once())
+ ->method('setPrincipalUri')
+ ->with($this->calendar->getPrincipalURI());
+
+ /** @var Plugin|MockObject $schedulingPlugin */
+ $schedulingPlugin = $invitationResponseServer->server->getPlugin('caldav-schedule');
+ $schedulingPlugin->expects(self::once())
+ ->method('setPathOfCalendarObjectChange')
+ ->with('fullcalendarname');
+ }
+
+ public function testHandleImipMessageNoCalendarUri(): void {
+ $invitationResponseServer = $this->createConfiguredMock(InvitationResponseServer::class, [
+ 'server' => $this->createConfiguredMock(CalDavBackend::class, [
+ 'getPlugin' => [
+ 'auth' => $this->createMock(CustomPrincipalPlugin::class),
+ 'schedule' => $this->createMock(Plugin::class)
+ ]
+ ])
+ ]);
+
+ $message = <<<EOF
+BEGIN:VCALENDAR
+PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN
+METHOD:REPLY
+VERSION:2.0
+BEGIN:VEVENT
+ATTENDEE;PARTSTAT=mailto:lewis@stardew-tent-living.com:ACCEPTED
+ORGANIZER:mailto:pierre@generalstore.com
+UID:aUniqueUid
+SEQUENCE:2
+REQUEST-STATUS:2.0;Success
+%sEND:VEVENT
+END:VCALENDAR
+EOF;
+
+ /** @var CustomPrincipalPlugin|MockObject $authPlugin */
+ $authPlugin = $invitationResponseServer->server->getPlugin('auth');
+ $authPlugin->expects(self::once())
+ ->method('setPrincipalUri')
+ ->with($this->calendar->getPrincipalURI());
+
+ unset($this->calendarInfo['uri']);
+ $this->expectException('CalendarException');
+ $this->calendarImpl->handleIMipMessage('filename.ics', $message);
+ }
}
diff --git a/lib/private/Calendar/Manager.php b/lib/private/Calendar/Manager.php
index 16e142264aa..f0b8e9fd50d 100644
--- a/lib/private/Calendar/Manager.php
+++ b/lib/private/Calendar/Manager.php
@@ -27,12 +27,19 @@ declare(strict_types=1);
namespace OC\Calendar;
use OC\AppFramework\Bootstrap\Coordinator;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\Calendar\Exceptions\CalendarException;
use OCP\Calendar\ICalendar;
use OCP\Calendar\ICalendarProvider;
use OCP\Calendar\ICalendarQuery;
+use OCP\Calendar\ICreateFromString;
use OCP\Calendar\IManager;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\Component\VEvent;
+use Sabre\VObject\Property\VCard\DateTime;
+use Sabre\VObject\Reader;
use Throwable;
use function array_map;
use function array_merge;
@@ -58,12 +65,17 @@ class Manager implements IManager {
/** @var LoggerInterface */
private $logger;
+ private ITimeFactory $timeFactory;
+
+
public function __construct(Coordinator $coordinator,
ContainerInterface $container,
- LoggerInterface $logger) {
+ LoggerInterface $logger,
+ ITimeFactory $timeFactory) {
$this->coordinator = $coordinator;
$this->container = $container;
$this->logger = $logger;
+ $this->timeFactory = $timeFactory;
}
/**
@@ -167,6 +179,11 @@ class Manager implements IManager {
$this->calendarLoaders = [];
}
+ /**
+ * @param string $principalUri
+ * @param array $calendarUris
+ * @return ICreateFromString[]
+ */
public function getCalendarsForPrincipal(string $principalUri, array $calendarUris = []): array {
$context = $this->coordinator->getRegistrationContext();
if ($context === null) {
@@ -198,7 +215,6 @@ class Manager implements IManager {
);
$results = [];
- /** @var ICalendar $calendar */
foreach ($calendars as $calendar) {
$r = $calendar->search(
$query->getSearchPattern() ?? '',
@@ -219,4 +235,137 @@ class Manager implements IManager {
public function newQuery(string $principalUri): ICalendarQuery {
return new CalendarQuery($principalUri);
}
+
+ /**
+ * @throws \OCP\DB\Exception
+ */
+ public function handleIMipReply(string $principalUri, string $sender, string $recipient, string $calendarData): bool {
+ /** @var VCalendar $vObject */
+ $vObject = Reader::read($calendarData);
+ /** @var VEvent $vEvent */
+ $vEvent = $vObject->{'VEVENT'};
+
+ // First, we check if the correct method is passed to us
+ if (strcasecmp('REPLY', $vObject->{'METHOD'}->getValue()) !== 0) {
+ $this->logger->warning('Wrong method provided for processing');
+ return false;
+ }
+
+ // check if mail recipient and organizer are one and the same
+ $organizer = substr($vEvent->{'ORGANIZER'}->getValue(), 7);
+
+ if (strcasecmp($recipient, $organizer) !== 0) {
+ $this->logger->warning('Recipient and ORGANIZER must be identical');
+ return false;
+ }
+
+ //check if the event is in the future
+ /** @var DateTime $eventTime */
+ $eventTime = $vEvent->{'DTSTART'};
+ if ($eventTime->getDateTime()->getTimeStamp() < $this->timeFactory->getTime()) { // this might cause issues with recurrences
+ $this->logger->warning('Only events in the future are processed');
+ return false;
+ }
+
+ $calendars = $this->getCalendarsForPrincipal($principalUri);
+ if (empty($calendars)) {
+ $this->logger->warning('Could not find any calendars for principal ' . $principalUri);
+ return false;
+ }
+
+ $found = null;
+ // if the attendee has been found in at least one calendar event with the UID of the iMIP event
+ // we process it.
+ // Benefit: no attendee lost
+ // Drawback: attendees that have been deleted will still be able to update their partstat
+ foreach ($calendars as $calendar) {
+ // We should not search in writable calendars
+ if ($calendar instanceof ICreateFromString) {
+ $o = $calendar->search($sender, ['ATTENDEE'], ['uid' => $vEvent->{'UID'}->getValue()]);
+ if (!empty($o)) {
+ $found = $calendar;
+ $name = $o[0]['uri'];
+ break;
+ }
+ }
+ }
+
+ if (empty($found)) {
+ $this->logger->info('Event not found in any calendar for principal ' . $principalUri . 'and UID' . $vEvent->{'UID'}->getValue());
+ return false;
+ }
+
+ try {
+ $found->handleIMipMessage($name, $calendarData); // sabre will handle the scheduling behind the scenes
+ } catch (CalendarException $e) {
+ $this->logger->error('Could not update calendar for iMIP processing', ['exception' => $e]);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * @since 25.0.0
+ * @throws \OCP\DB\Exception
+ */
+ public function handleIMipCancel(string $principalUri, string $sender, ?string $replyTo, string $recipient, string $calendarData): bool {
+ $vObject = Reader::read($calendarData);
+ /** @var VEvent $vEvent */
+ $vEvent = $vObject->{'VEVENT'};
+
+ // First, we check if the correct method is passed to us
+ if (strcasecmp('CANCEL', $vObject->{'METHOD'}->getValue()) !== 0) {
+ $this->logger->warning('Wrong method provided for processing');
+ return false;
+ }
+
+ $attendee = substr($vEvent->{'ATTENDEE'}->getValue(), 7);
+ if (strcasecmp($recipient, $attendee) !== 0) {
+ $this->logger->warning('Recipient must be an ATTENDEE of this event');
+ return false;
+ }
+
+ // Thirdly, we need to compare the email address the CANCEL is coming from (in Mail)
+ // or the Reply- To Address submitted with the CANCEL email
+ // to the email address in the ORGANIZER.
+ // We don't want to accept a CANCEL request from just anyone
+ $organizer = substr($vEvent->{'ORGANIZER'}->getValue(), 7);
+ if (strcasecmp($sender, $organizer) !== 0 && strcasecmp($replyTo, $organizer) !== 0) {
+ $this->logger->warning('Sender must be the ORGANIZER of this event');
+ return false;
+ }
+
+ $calendars = $this->getCalendarsForPrincipal($principalUri);
+ $found = null;
+ // if the attendee has been found in at least one calendar event with the UID of the iMIP event
+ // we process it.
+ // Benefit: no attendee lost
+ // Drawback: attendees that have been deleted will still be able to update their partstat
+ foreach ($calendars as $calendar) {
+ // We should not search in writable calendars
+ if ($calendar instanceof ICreateFromString) {
+ $o = $calendar->search($recipient, ['ATTENDEE'], ['uid' => $vEvent->{'UID'}->getValue()]);
+ if (!empty($o)) {
+ $found = $calendar;
+ $name = $o[0]['uri'];
+ break;
+ }
+ }
+ }
+
+ if (empty($found)) {
+ $this->logger->info('Event not found in any calendar for principal ' . $principalUri . 'and UID' . $vEvent->{'UID'}->getValue());
+ // this is a safe operation
+ // we can ignore events that have been cancelled but were not in the calendar anyway
+ return true;
+ }
+
+ try {
+ $found->handleIMipMessage($name, $calendarData); // sabre will handle the scheduling behind the scenes
+ return true;
+ } catch (CalendarException $e) {
+ $this->logger->error('Could not update calendar for iMIP processing', ['exception' => $e]);
+ return false;
+ }
+ }
}
diff --git a/lib/public/Calendar/ICreateFromString.php b/lib/public/Calendar/ICreateFromString.php
index 343405e8ab6..8c4bdd44041 100644
--- a/lib/public/Calendar/ICreateFromString.php
+++ b/lib/public/Calendar/ICreateFromString.php
@@ -1,4 +1,6 @@
<?php
+
+declare(strict_types=1);
/**
* @copyright 2021 Anna Larch <anna.larch@gmx.net>
*
@@ -38,4 +40,11 @@ interface ICreateFromString extends ICalendar {
* @throws CalendarException
*/
public function createFromString(string $name, string $calendarData): void;
+
+ /**
+ * @since 25.0.0
+ *
+ * @throws CalendarException
+ */
+ public function handleIMipMessage(string $name, string $calendarData): void;
}
diff --git a/lib/public/Calendar/IManager.php b/lib/public/Calendar/IManager.php
index 7f0eec80910..dd65917d12b 100644
--- a/lib/public/Calendar/IManager.php
+++ b/lib/public/Calendar/IManager.php
@@ -156,4 +156,18 @@ interface IManager {
* @since 23.0.0
*/
public function newQuery(string $principalUri) : ICalendarQuery;
+
+ /**
+ * Handle a iMip REPLY message
+ *
+ * @since 25.0.0
+ */
+ public function handleIMipReply(string $principalUri, string $sender, string $recipient, string $calendarData): bool;
+
+ /**
+ * Handle a iMip CANCEL message
+ *
+ * @since 25.0.0
+ */
+ public function handleIMipCancel(string $principalUri, string $sender, ?string $replyTo, string $recipient, string $calendarData): bool;
}
diff --git a/tests/lib/Calendar/ManagerTest.php b/tests/lib/Calendar/ManagerTest.php
index a4d9d45fdb2..8b99c21ae41 100644
--- a/tests/lib/Calendar/ManagerTest.php
+++ b/tests/lib/Calendar/ManagerTest.php
@@ -25,10 +25,14 @@ namespace Test\Calendar;
use OC\AppFramework\Bootstrap\Coordinator;
use OC\Calendar\Manager;
+use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Calendar\ICalendar;
+use OCP\Calendar\ICreateFromString;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
+use Sabre\VObject\Document;
+use Sabre\VObject\Reader;
use Test\TestCase;
class ManagerTest extends TestCase {
@@ -45,17 +49,22 @@ class ManagerTest extends TestCase {
/** @var Manager */
private $manager;
+ /** @var ITimeFactory|ITimeFactory&MockObject|MockObject */
+ private $time;
+
protected function setUp(): void {
parent::setUp();
$this->coordinator = $this->createMock(Coordinator::class);
$this->container = $this->createMock(ContainerInterface::class);
$this->logger = $this->createMock(LoggerInterface::class);
+ $this->time = $this->createMock(ITimeFactory::class);
$this->manager = new Manager(
$this->coordinator,
$this->container,
- $this->logger
+ $this->logger,
+ $this->time,
);
}
@@ -231,4 +240,370 @@ class ManagerTest extends TestCase {
$isEnabled = $this->manager->isEnabled();
$this->assertTrue($isEnabled);
}
+
+ public function testHandleImipReplyWrongMethod(): void {
+ $principalUri = 'principals/user/linus';
+ $sender = 'pierre@general-store.com';
+ $recipient = 'linus@stardew-tent-living.com';
+ $calendarData = $this->getVCalendarReply();
+ $calendarData->METHOD = 'REQUEST';
+
+ $this->logger->expects(self::once())
+ ->method('warning');
+ $this->time->expects(self::never())
+ ->method('getTimestamp');
+
+ $result = $this->manager->handleIMipReply($principalUri, $sender, $recipient, $calendarData->serialize());
+ $this->assertFalse($result);
+ }
+
+ public function testHandleImipReplyOrganizerNotRecipient(): void {
+ $principalUri = 'principals/user/linus';
+ $recipient = 'pierre@general-store.com';
+ $sender = 'linus@stardew-tent-living.com';
+ $calendarData = $this->getVCalendarReply();
+
+ $this->logger->expects(self::once())
+ ->method('warning');
+ $this->time->expects(self::never())
+ ->method('getTimestamp');
+
+ $result = $this->manager->handleIMipReply($principalUri, $sender, $recipient, $calendarData->serialize());
+ $this->assertFalse($result);
+ }
+
+ public function testHandleImipReplyDateInThePast(): void {
+ $principalUri = 'principals/user/linus';
+ $sender = 'pierre@general-store.com';
+ $recipient = 'linus@stardew-tent-living.com';
+ $calendarData = $this->getVCalendarReply();
+ $calendarData->VEVENT->DTSTART = new \DateTime('2013-04-07'); // set to in the past
+
+ $this->time->expects(self::once())
+ ->method('getTimestamp')
+ ->willReturn(time());
+
+ $this->logger->expects(self::once())
+ ->method('warning');
+
+ $result = $this->manager->handleIMipReply($principalUri, $sender, $recipient, $calendarData->serialize());
+ $this->assertFalse($result);
+ }
+
+ public function testHandleImipReplyNoCalendars(): void {
+ /** @var Manager | \PHPUnit\Framework\MockObject\MockObject $manager */
+ $manager = $this->getMockBuilder(Manager::class)
+ ->setConstructorArgs([
+ $this->coordinator,
+ $this->container,
+ $this->logger,
+ $this->time
+ ])
+ ->setMethods([
+ 'getCalendarsForPrincipal'
+ ]);
+ $principalUri = 'principals/user/linus';
+ $sender = 'pierre@general-store.com';
+ $recipient = 'linus@stardew-tent-living.com';
+ $calendarData = $this->getVCalendarReply();
+
+ $this->time->expects(self::once())
+ ->method('getTimestamp')
+ ->willReturn(202208219);
+ $manager->expects(self::once())
+ ->method('getCalendarsForPrincipal')
+ ->willReturn([]);
+ $this->logger->expects(self::once())
+ ->method('warning');
+
+ $result = $manager->handleIMipReply($principalUri, $sender, $recipient, $calendarData->serialize());
+ $this->assertFalse($result);
+ }
+
+ public function testHandleImipReplyEventNotFound(): void {
+ /** @var Manager | \PHPUnit\Framework\MockObject\MockObject $manager */
+ $manager = $this->getMockBuilder(Manager::class)
+ ->setConstructorArgs([
+ $this->coordinator,
+ $this->container,
+ $this->logger,
+ $this->time
+ ])
+ ->setMethods([
+ 'getCalendarsForPrincipal'
+ ]);
+ $calendar = $this->createMock(ICreateFromString::class);
+ $principalUri = 'principals/user/linus';
+ $sender = 'pierre@general-store.com';
+ $recipient = 'linus@stardew-tent-living.com';
+ $calendarData = $this->getVCalendarReply();
+
+ $this->time->expects(self::once())
+ ->method('getTimestamp')
+ ->willReturn(202208219);
+ $manager->expects(self::once())
+ ->method('getCalendarsForPrincipal')
+ ->willReturn([$calendar]);
+ $calendar->expects(self::once())
+ ->method('search')
+ ->willReturn([]);
+ $this->logger->expects(self::once())
+ ->method('info');
+ $calendar->expects(self::never())
+ ->method('handleIMipMessage');
+
+ $result = $manager->handleIMipReply($principalUri, $sender, $recipient, $calendarData->serialize());
+ $this->assertFalse($result);
+ }
+
+ public function testHandleImipReply(): void {
+ /** @var Manager | \PHPUnit\Framework\MockObject\MockObject $manager */
+ $manager = $this->getMockBuilder(Manager::class)
+ ->setConstructorArgs([
+ $this->coordinator,
+ $this->container,
+ $this->logger,
+ $this->time
+ ])
+ ->setMethods([
+ 'getCalendarsForPrincipal'
+ ]);
+ $calendar = $this->createMock(ICreateFromString::class);
+ $principalUri = 'principals/user/linus';
+ $sender = 'pierre@general-store.com';
+ $recipient = 'linus@stardew-tent-living.com';
+ $calendarData = $this->getVCalendarReply();
+
+ $this->time->expects(self::once())
+ ->method('getTimestamp')
+ ->willReturn(202208219);
+ $manager->expects(self::once())
+ ->method('getCalendarsForPrincipal')
+ ->willReturn([$calendar]);
+ $calendar->expects(self::once())
+ ->method('search')
+ ->willReturn([['uri' => 'testname.ics']]);
+ $calendar->expects(self::once())
+ ->method('handleIMipMessage')
+ ->with('testname.ics', $calendarData->serialize());
+
+ $result = $manager->handleIMipReply($principalUri, $sender, $recipient, $calendarData->serialize());
+ $this->assertTrue($result);
+ }
+
+ public function testHandleImipCancelWrongMethod(): void {
+ $principalUri = 'principals/user/pierre';
+ $sender = 'linus@stardew-tent-living.com';
+ $recipient = 'pierre@general-store.com';
+ $replyTo = null;
+ $calendarData = $this->getVCalendarCancel();
+ $calendarData->VEVENT->METHOD = 'REQUEST';
+
+ $this->logger->expects(self::once())
+ ->method('warning');
+ $this->time->expects(self::never())
+ ->method('getTimestamp');
+
+ $result = $this->manager->handleIMipCancel($principalUri, $sender, $replyTo, $recipient, $calendarData->serialize());
+ $this->assertFalse($result);
+ }
+
+ public function testHandleImipCancelAttendeeNotRecipient(): void {
+ $principalUri = '/user/admin';
+ $sender = 'linus@stardew-tent-living.com';
+ $recipient = 'leah@general-store.com';
+ $replyTo = null;
+ $calendarData = $this->getVCalendarCancel();
+ $calendarData->VEVENT->METHOD = 'CANCEL';
+
+ $this->logger->expects(self::once())
+ ->method('warning');
+ $this->time->expects(self::never())
+ ->method('getTimestamp');
+
+ $result = $this->manager->handleIMipCancel($principalUri, $sender, $replyTo, $recipient, $calendarData->serialize());
+ $this->assertFalse($result);
+ }
+
+ public function testHandleImipCancelDateInThePast(): void {
+ $principalUri = 'principals/user/pierre';
+ $sender = 'linus@stardew-tent-living.com';
+ $recipient = 'pierre@general-store.com';
+ $replyTo = null;
+ $calendarData = $this->getVCalendarCancel();
+ $calendarData->VEVENT->DTSTART = new \DateTime('2013-04-07'); // set to in the past
+
+ $this->time->expects(self::once())
+ ->method('getTimestamp')
+ ->willReturn(time());
+ $this->logger->expects(self::once())
+ ->method('warning');
+
+ $result = $this->manager->handleIMipCancel($principalUri, $sender, $replyTo, $recipient, $calendarData->serialize());
+ $this->assertFalse($result);
+ }
+
+ public function testHandleImipCancelNoCalendars(): void {
+ /** @var Manager | \PHPUnit\Framework\MockObject\MockObject $manager */
+ $manager = $this->getMockBuilder(Manager::class)
+ ->setConstructorArgs([
+ $this->coordinator,
+ $this->container,
+ $this->logger,
+ $this->time
+ ])
+ ->setMethods([
+ 'getCalendarsForPrincipal'
+ ]);
+ $principalUri = 'principals/user/pierre';
+ $sender = 'linus@stardew-tent-living.com';
+ $recipient = 'pierre@general-store.com';
+ $replyTo = null;
+ $calendarData = $this->getVCalendarCancel();
+
+ $this->time->expects(self::once())
+ ->method('getTimestamp')
+ ->willReturn(202208219);
+ $manager->expects(self::once())
+ ->method('getCalendarsForPrincipal')
+ ->with($principalUri)
+ ->willReturn([]);
+ $this->logger->expects(self::once())
+ ->method('warning');
+
+ $result = $this->manager->handleIMipCancel($principalUri, $sender, $replyTo, $recipient, $calendarData->serialize());
+ $this->assertTrue($result);
+ }
+
+ public function testHandleImipCancelOrganiserInReplyTo(): void {
+ /** @var Manager | \PHPUnit\Framework\MockObject\MockObject $manager */
+ $manager = $this->getMockBuilder(Manager::class)
+ ->setConstructorArgs([
+ $this->coordinator,
+ $this->container,
+ $this->logger,
+ $this->time
+ ])
+ ->setMethods([
+ 'getCalendarsForPrincipal'
+ ]);
+ $principalUri = 'principals/user/pierre';
+ $sender = 'clint@stardew-blacksmiths.com';
+ $recipient = 'pierre@general-store.com';
+ $replyTo = 'linus@stardew-tent-living.com';
+ $calendar = $this->createMock(ICreateFromString::class);
+ $calendarData = $this->getVCalendarCancel();
+ $calendarData->VEVENT->METHOD = 'CANCEL';
+
+ $this->time->expects(self::once())
+ ->method('getTimestamp')
+ ->willReturn(202208219);
+ $manager->expects(self::once())
+ ->method('getCalendarsForPrincipal')
+ ->with($principalUri)
+ ->willReturn([$calendar]);
+ $calendar->expects(self::once())
+ ->method('search')
+ ->willReturn([['uri' => 'testname.ics']]);
+ $calendar->expects(self::once())
+ ->method('handleIMipMessage')
+ ->with('testname.ics', $calendarData->serialize());
+ $result = $this->manager->handleIMipCancel($principalUri, $sender, $replyTo, $recipient, $calendarData->serialize());
+ $this->assertFalse($result);
+ }
+
+ public function testHandleImipCancel(): void {
+ /** @var Manager | \PHPUnit\Framework\MockObject\MockObject $manager */
+ $manager = $this->getMockBuilder(Manager::class)
+ ->setConstructorArgs([
+ $this->coordinator,
+ $this->container,
+ $this->logger,
+ $this->time
+ ])
+ ->setMethods([
+ 'getCalendarsForPrincipal'
+ ]);
+ $principalUri = 'principals/user/pierre';
+ $sender = 'linus@stardew-tent-living.com';
+ $recipient = 'pierre@general-store.com';
+ $replyTo = null;
+ $calendar = $this->createMock(ICreateFromString::class);
+ $calendarData = $this->getVCalendarCancel();
+ $calendarData->VEVENT->METHOD = 'CANCEL';
+
+ $this->time->expects(self::once())
+ ->method('getTimestamp')
+ ->willReturn(202208219);
+ $manager->expects(self::once())
+ ->method('getCalendarsForPrincipal')
+ ->with($principalUri)
+ ->willReturn([$calendar]);
+ $calendar->expects(self::once())
+ ->method('search')
+ ->willReturn([['uri' => 'testname.ics']]);
+ $calendar->expects(self::once())
+ ->method('handleIMipMessage')
+ ->with('testname.ics', $calendarData->serialize());
+ $result = $this->manager->handleIMipCancel($principalUri, $sender, $replyTo, $recipient, $calendarData->serialize());
+ $this->assertFalse($result);
+ }
+
+ private function getVCalendarReply(): Document {
+ $data = <<<EOF
+BEGIN:VCALENDAR
+PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN
+VERSION:2.0
+CALSCALE:GREGORIAN
+METHOD:REPLY
+BEGIN:VEVENT
+DTSTART;VALUE=DATE:20210820
+DTEND;VALUE=DATE:20220821
+DTSTAMP:20210812T100040Z
+ORGANIZER;CN=admin:mailto:linus@stardew-tent-living.com
+UID:dcc733bf-b2b2-41f2-a8cf-550ae4b67aff
+ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=pierr
+ e@general-store.com;X-NUM-GUESTS=0:mailto:pierre@general-store.com
+CREATED:20220812T100021Z
+DESCRIPTION:
+LAST-MODIFIED:20220812T100040Z
+LOCATION:
+SEQUENCE:3
+STATUS:CONFIRMED
+SUMMARY:berry basket
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+EOF;
+ return Reader::read($data);
+ }
+
+ private function getVCalendarCancel(): Document {
+ $data = <<<EOF
+BEGIN:VCALENDAR
+PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN
+VERSION:2.0
+CALSCALE:GREGORIAN
+METHOD:CANCEL
+BEGIN:VEVENT
+DTSTART;VALUE=DATE:20210820
+DTEND;VALUE=DATE:20220821
+DTSTAMP:20210812T100040Z
+ORGANIZER;CN=admin:mailto:linus@stardew-tent-living.com
+UID:dcc733bf-b2b2-41f2-a8cf-550ae4b67aff
+ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=pierr
+ e@general-store.com;X-NUM-GUESTS=0:mailto:pierre@general-store.com
+CREATED:20220812T100021Z
+DESCRIPTION:
+LAST-MODIFIED:20220812T100040Z
+LOCATION:
+SEQUENCE:3
+STATUS:CANCELLED
+SUMMARY:berry basket
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+EOF;
+ return Reader::read($data);
+ }
}