mirror of
https://github.com/nextcloud/server.git
synced 2026-06-09 08:44:07 -04:00
fix: aliases and capitalization of emails
Signed-off-by: SebastianKrupinski <krupinskis05@gmail.com>
This commit is contained in:
parent
1cb6dc0e58
commit
7e92b157e3
6 changed files with 429 additions and 1599 deletions
|
|
@ -24,9 +24,9 @@ use OCP\Constants;
|
|||
use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp;
|
||||
use Sabre\DAV\Exception\Conflict;
|
||||
use Sabre\VObject\Component\VCalendar;
|
||||
use Sabre\VObject\Component\VEvent;
|
||||
use Sabre\VObject\Component\VTimeZone;
|
||||
use Sabre\VObject\ITip\Message;
|
||||
use Sabre\VObject\ParseException;
|
||||
use Sabre\VObject\Property;
|
||||
use Sabre\VObject\Reader;
|
||||
|
||||
|
|
@ -41,6 +41,9 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage, ICalendarIs
|
|||
) {
|
||||
}
|
||||
|
||||
private const DAV_PROPERTY_USER_ADDRESS = '{http://sabredav.org/ns}email-address';
|
||||
private const DAV_PROPERTY_USER_ADDRESSES = '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set';
|
||||
|
||||
/**
|
||||
* @return string defining the technical unique key
|
||||
* @since 13.0.0
|
||||
|
|
@ -219,58 +222,93 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage, ICalendarIs
|
|||
* @throws CalendarException
|
||||
*/
|
||||
public function handleIMipMessage(string $name, string $calendarData): void {
|
||||
$server = $this->getInvitationResponseServer();
|
||||
|
||||
/** @var CustomPrincipalPlugin $plugin */
|
||||
$plugin = $server->getServer()->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());
|
||||
|
||||
try {
|
||||
/** @var VCalendar $vObject|null */
|
||||
$vObject = Reader::read($calendarData);
|
||||
} catch (ParseException $e) {
|
||||
throw new CalendarException('iMip message could not be processed because an error occurred while parsing the iMip message', 0, $e);
|
||||
}
|
||||
// validate the iMip message
|
||||
if (!isset($vObject->METHOD)) {
|
||||
throw new CalendarException('iMip message contains no valid method');
|
||||
}
|
||||
if (!isset($vObject->VEVENT)) {
|
||||
throw new CalendarException('iMip message contains no event');
|
||||
}
|
||||
if (!isset($vObject->VEVENT->UID)) {
|
||||
throw new CalendarException('iMip message event dose not contain a UID');
|
||||
}
|
||||
if (!isset($vObject->VEVENT->ORGANIZER)) {
|
||||
throw new CalendarException('iMip message event dose not contain an organizer');
|
||||
}
|
||||
if (!isset($vObject->VEVENT->ATTENDEE)) {
|
||||
throw new CalendarException('iMip message event dose not contain an attendee');
|
||||
}
|
||||
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->getServer()->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');
|
||||
}
|
||||
$organizer = $vEvent->{'ORGANIZER'}->getValue();
|
||||
$attendee = $vEvent->{'ATTENDEE'}->getValue();
|
||||
|
||||
$iTipMessage->method = $vObject->{'METHOD'}->getValue();
|
||||
if ($iTipMessage->method === 'REQUEST') {
|
||||
$iTipMessage->sender = $organizer;
|
||||
$iTipMessage->recipient = $attendee;
|
||||
} elseif ($iTipMessage->method === 'REPLY') {
|
||||
if ($server->isExternalAttendee($vEvent->{'ATTENDEE'}->getValue())) {
|
||||
$iTipMessage->recipient = $organizer;
|
||||
} else {
|
||||
$iTipMessage->recipient = $attendee;
|
||||
// construct dav server
|
||||
$server = $this->getInvitationResponseServer();
|
||||
/** @var CustomPrincipalPlugin $authPlugin */
|
||||
$authPlugin = $server->getServer()->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
|
||||
$authPlugin->setCurrentPrincipal($this->calendar->getPrincipalURI());
|
||||
// retrieve all users addresses
|
||||
$userProperties = $server->getServer()->getProperties($this->calendar->getPrincipalURI(), [ self::DAV_PROPERTY_USER_ADDRESS, self::DAV_PROPERTY_USER_ADDRESSES ]);
|
||||
$userAddress = 'mailto:' . ($userProperties[self::DAV_PROPERTY_USER_ADDRESS] ?? null);
|
||||
$userAddresses = $userProperties[self::DAV_PROPERTY_USER_ADDRESSES]->getHrefs() ?? [];
|
||||
$userAddresses = array_map('strtolower', array_map('urldecode', $userAddresses));
|
||||
// validate the method, recipient and sender
|
||||
$imipMethod = strtoupper($vObject->METHOD->getValue());
|
||||
if (in_array($imipMethod, ['REPLY', 'REFRESH'], true)) {
|
||||
// extract sender (REPLY and REFRESH method should only have one attendee)
|
||||
$sender = strtolower($vObject->VEVENT->ATTENDEE->getValue());
|
||||
// extract and verify the recipient
|
||||
$recipient = strtolower($vObject->VEVENT->ORGANIZER->getValue());
|
||||
if (!in_array($recipient, $userAddresses, true)) {
|
||||
throw new CalendarException('iMip message dose not contain an organizer that matches the user');
|
||||
}
|
||||
$iTipMessage->sender = $attendee;
|
||||
} elseif ($iTipMessage->method === 'CANCEL') {
|
||||
$iTipMessage->recipient = $attendee;
|
||||
$iTipMessage->sender = $organizer;
|
||||
// if the recipient address is not the same as the user address this means an alias was used
|
||||
// the iTip broker uses the users primary email address during processing
|
||||
if ($userAddress !== $recipient) {
|
||||
$recipient = $userAddress;
|
||||
}
|
||||
} elseif (in_array($imipMethod, ['PUBLISH', 'REQUEST', 'ADD', 'CANCEL'], true)) {
|
||||
// extract sender
|
||||
$sender = strtolower($vObject->VEVENT->ORGANIZER->getValue());
|
||||
// extract and verify the recipient
|
||||
foreach ($vObject->VEVENT->ATTENDEE as $attendee) {
|
||||
$recipient = strtolower($attendee->getValue());
|
||||
if (in_array($recipient, $userAddresses, true)) {
|
||||
break;
|
||||
}
|
||||
$recipient = null;
|
||||
}
|
||||
if ($recipient === null) {
|
||||
throw new CalendarException('iMip message dose not contain an attendee that matches the user');
|
||||
}
|
||||
// if the recipient address is not the same as the user address this means an alias was used
|
||||
// the iTip broker uses the users primary email address during processing
|
||||
if ($userAddress !== $recipient) {
|
||||
$recipient = $userAddress;
|
||||
}
|
||||
} else {
|
||||
throw new CalendarException('iMip message contains a method that is not supported: ' . $imipMethod);
|
||||
}
|
||||
$iTipMessage->uid = isset($vEvent->{'UID'}) ? $vEvent->{'UID'}->getValue() : '';
|
||||
$iTipMessage->component = 'VEVENT';
|
||||
$iTipMessage->sequence = isset($vEvent->{'SEQUENCE'}) ? (int)$vEvent->{'SEQUENCE'}->getValue() : 0;
|
||||
$iTipMessage->message = $vObject;
|
||||
$server->server->emit('schedule', [$iTipMessage]);
|
||||
// generate the iTip message
|
||||
$iTip = new Message();
|
||||
$iTip->method = $imipMethod;
|
||||
$iTip->sender = $sender;
|
||||
$iTip->recipient = $recipient;
|
||||
$iTip->component = 'VEVENT';
|
||||
$iTip->uid = $vObject->VEVENT->UID->getValue();
|
||||
$iTip->sequence = isset($vObject->VEVENT->SEQUENCE) ? (int)$vObject->VEVENT->SEQUENCE->getValue() : 1;
|
||||
$iTip->message = $vObject;
|
||||
|
||||
$server->server->emit('schedule', [$iTip]);
|
||||
}
|
||||
|
||||
public function getInvitationResponseServer(): InvitationResponseServer {
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
|
|||
* @param string $principal
|
||||
* @return array
|
||||
*/
|
||||
protected function getAddressesForPrincipal($principal) {
|
||||
public function getAddressesForPrincipal($principal) {
|
||||
$result = parent::getAddressesForPrincipal($principal);
|
||||
|
||||
if ($result === null) {
|
||||
|
|
|
|||
|
|
@ -7,51 +7,62 @@ declare(strict_types=1);
|
|||
*/
|
||||
namespace OCA\DAV\Tests\unit\CalDAV;
|
||||
|
||||
use Generator;
|
||||
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 OCA\DAV\Connector\Sabre\Server;
|
||||
use OCP\Calendar\Exceptions\CalendarException;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Sabre\VObject\Component\VCalendar;
|
||||
use Sabre\VObject\Component\VEvent;
|
||||
use Sabre\VObject\ITip\Message;
|
||||
use Sabre\VObject\Reader;
|
||||
|
||||
class CalendarImplTest extends \Test\TestCase {
|
||||
private Calendar&MockObject $calendar;
|
||||
private CalDavBackend|MockObject $backend;
|
||||
private Calendar|MockObject $calendar;
|
||||
private CalendarImpl|MockObject $calendarImpl;
|
||||
private array $calendarInfo;
|
||||
private CalDavBackend&MockObject $backend;
|
||||
private CalendarImpl $calendarImpl;
|
||||
private VCalendar $vCalendar1a;
|
||||
private array $mockExportCollection;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->backend = $this->createMock(CalDavBackend::class);
|
||||
$this->calendar = $this->createMock(Calendar::class);
|
||||
$this->calendarInfo = [
|
||||
'id' => 1,
|
||||
'id' => 'fancy_id_123',
|
||||
'{DAV:}displayname' => 'user readable name 123',
|
||||
'{http://apple.com/ns/ical/}calendar-color' => '#AABBCC',
|
||||
'uri' => '/this/is/a/uri',
|
||||
'principaluri' => 'principal/users/foobar'
|
||||
];
|
||||
$this->backend = $this->createMock(CalDavBackend::class);
|
||||
$this->calendarImpl = new CalendarImpl($this->calendar, $this->calendarInfo, $this->backend);
|
||||
|
||||
$this->calendarImpl = new CalendarImpl(
|
||||
$this->calendar,
|
||||
$this->calendarInfo,
|
||||
$this->backend
|
||||
);
|
||||
// construct calendar with a 1 hour event and same start/end time zones
|
||||
$this->vCalendar1a = new VCalendar();
|
||||
/** @var VEvent $vEvent */
|
||||
$vEvent = $this->vCalendar1a->add('VEVENT', []);
|
||||
$vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
|
||||
$vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
|
||||
$vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
|
||||
$vEvent->add('SEQUENCE', 1);
|
||||
$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'
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public function testGetKey(): void {
|
||||
$this->assertEquals($this->calendarImpl->getKey(), 1);
|
||||
$this->assertEquals($this->calendarImpl->getKey(), 'fancy_id_123');
|
||||
}
|
||||
|
||||
public function testGetDisplayname(): void {
|
||||
|
|
@ -123,56 +134,128 @@ class CalendarImplTest extends \Test\TestCase {
|
|||
$this->assertEquals(31, $this->calendarImpl->getPermissions());
|
||||
}
|
||||
|
||||
public function testHandleImipMessage(): void {
|
||||
$message = <<<EOF
|
||||
BEGIN:VCALENDAR
|
||||
PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN
|
||||
METHOD:REPLY
|
||||
VERSION:2.0
|
||||
BEGIN:VEVENT
|
||||
ATTENDEE;PARTSTAT=ACCEPTED:mailto:lewis@stardew-tent-living.com
|
||||
ORGANIZER:mailto:pierre@generalstore.com
|
||||
UID:aUniqueUid
|
||||
SEQUENCE:2
|
||||
REQUEST-STATUS:2.0;Success
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
EOF;
|
||||
public function testHandleImipNoMethod(): void {
|
||||
// Arrange
|
||||
$vObject = $this->vCalendar1a;
|
||||
|
||||
$this->expectException(CalendarException::class);
|
||||
$this->expectExceptionMessage('iMip message contains no valid method');
|
||||
|
||||
// Act
|
||||
$this->calendarImpl->handleIMipMessage('fakeUser', $vObject->serialize());
|
||||
}
|
||||
|
||||
public function testHandleImipNoEvent(): void {
|
||||
// Arrange
|
||||
$vObject = $this->vCalendar1a;
|
||||
$vObject->add('METHOD', 'REQUEST');
|
||||
$vObject->remove('VEVENT');
|
||||
|
||||
$this->expectException(CalendarException::class);
|
||||
$this->expectExceptionMessage('iMip message contains no event');
|
||||
|
||||
// Act
|
||||
$this->calendarImpl->handleIMipMessage('fakeUser', $vObject->serialize());
|
||||
}
|
||||
|
||||
public function testHandleImipNoUid(): void {
|
||||
// Arrange
|
||||
$vObject = $this->vCalendar1a;
|
||||
$vObject->add('METHOD', 'REQUEST');
|
||||
$vObject->VEVENT->remove('UID');
|
||||
|
||||
$this->expectException(CalendarException::class);
|
||||
$this->expectExceptionMessage('iMip message event dose not contain a UID');
|
||||
|
||||
// Act
|
||||
$this->calendarImpl->handleIMipMessage('fakeUser', $vObject->serialize());
|
||||
}
|
||||
|
||||
public function testHandleImipNoOrganizer(): void {
|
||||
// Arrange
|
||||
$vObject = $this->vCalendar1a;
|
||||
$vObject->add('METHOD', 'REQUEST');
|
||||
$vObject->VEVENT->remove('ORGANIZER');
|
||||
|
||||
$this->expectException(CalendarException::class);
|
||||
$this->expectExceptionMessage('iMip message event dose not contain an organizer');
|
||||
|
||||
// Act
|
||||
$this->calendarImpl->handleIMipMessage('fakeUser', $vObject->serialize());
|
||||
}
|
||||
|
||||
public function testHandleImipNoAttendee(): void {
|
||||
// Arrange
|
||||
$vObject = $this->vCalendar1a;
|
||||
$vObject->add('METHOD', 'REQUEST');
|
||||
$vObject->VEVENT->remove('ATTENDEE');
|
||||
|
||||
$this->expectException(CalendarException::class);
|
||||
$this->expectExceptionMessage('iMip message event dose not contain an attendee');
|
||||
|
||||
// Act
|
||||
$this->calendarImpl->handleIMipMessage('fakeUser', $vObject->serialize());
|
||||
}
|
||||
|
||||
public function testHandleImipRequest(): void {
|
||||
$userAddressSet = new class([ 'mailto:attendee1@testing.com', '/remote.php/dav/principals/users/attendee1/', ]) {
|
||||
public function __construct(
|
||||
private array $hrefs,
|
||||
) {
|
||||
}
|
||||
public function getHrefs(): array {
|
||||
return $this->hrefs;
|
||||
}
|
||||
};
|
||||
|
||||
$vObject = $this->vCalendar1a;
|
||||
$vObject->add('METHOD', 'REQUEST');
|
||||
|
||||
$iTip = new Message();
|
||||
$iTip->method = 'REQUEST';
|
||||
$iTip->sender = $vObject->VEVENT->ORGANIZER->getValue();
|
||||
$iTip->recipient = $vObject->VEVENT->ATTENDEE->getValue();
|
||||
$iTip->component = 'VEVENT';
|
||||
$iTip->uid = $vObject->VEVENT->UID->getValue();
|
||||
$iTip->sequence = (int)$vObject->VEVENT->SEQUENCE->getValue() ?? 0;
|
||||
$iTip->message = $vObject;
|
||||
|
||||
/** @var CustomPrincipalPlugin|MockObject $authPlugin */
|
||||
$authPlugin = $this->createMock(CustomPrincipalPlugin::class);
|
||||
$authPlugin->expects(self::once())
|
||||
->method('setCurrentPrincipal')
|
||||
->with($this->calendar->getPrincipalURI());
|
||||
|
||||
/** @var \Sabre\DAVACL\Plugin|MockObject $aclPlugin */
|
||||
$aclPlugin = $this->createMock(\Sabre\DAVACL\Plugin::class);
|
||||
|
||||
/** @var Plugin|MockObject $schedulingPlugin */
|
||||
$schedulingPlugin = $this->createMock(Plugin::class);
|
||||
$iTipMessage = $this->getITipMessage($message);
|
||||
$iTipMessage->recipient = 'mailto:lewis@stardew-tent-living.com';
|
||||
|
||||
$server = $this->createMock(Server::class);
|
||||
$server->expects($this->any())
|
||||
->method('getPlugin')
|
||||
->willReturnMap([
|
||||
['auth', $authPlugin],
|
||||
['acl', $aclPlugin],
|
||||
['caldav-schedule', $schedulingPlugin]
|
||||
]);
|
||||
|
||||
$server->expects(self::once())
|
||||
->method('getProperties')
|
||||
->with(
|
||||
$this->calendar->getPrincipalURI(),
|
||||
[
|
||||
'{http://sabredav.org/ns}email-address',
|
||||
'{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'
|
||||
]
|
||||
)
|
||||
->willReturn([
|
||||
'{http://sabredav.org/ns}email-address' => 'attendee1@testing.com',
|
||||
'{urn:ietf:params:xml:ns:caldav}calendar-user-address-set' => $userAddressSet,
|
||||
]);
|
||||
$server->expects(self::once())
|
||||
->method('emit');
|
||||
|
||||
$invitationResponseServer = $this->createPartialMock(InvitationResponseServer::class, ['getServer', 'isExternalAttendee']);
|
||||
$invitationResponseServer = $this->createMock(InvitationResponseServer::class, ['getServer']);
|
||||
$invitationResponseServer->server = $server;
|
||||
$invitationResponseServer->expects($this->any())
|
||||
->method('getServer')
|
||||
->willReturn($server);
|
||||
$invitationResponseServer->expects(self::once())
|
||||
->method('isExternalAttendee')
|
||||
->willReturn(false);
|
||||
|
||||
$calendarImpl = $this->getMockBuilder(CalendarImpl::class)
|
||||
->setConstructorArgs([$this->calendar, $this->calendarInfo, $this->backend])
|
||||
->onlyMethods(['getInvitationResponseServer'])
|
||||
|
|
@ -181,128 +264,7 @@ EOF;
|
|||
->method('getInvitationResponseServer')
|
||||
->willReturn($invitationResponseServer);
|
||||
|
||||
$calendarImpl->handleIMipMessage('filename.ics', $message);
|
||||
}
|
||||
|
||||
public function testHandleImipMessageNoCalendarUri(): void {
|
||||
/** @var CustomPrincipalPlugin|MockObject $authPlugin */
|
||||
$authPlugin = $this->createMock(CustomPrincipalPlugin::class);
|
||||
$authPlugin->expects(self::once())
|
||||
->method('setCurrentPrincipal')
|
||||
->with($this->calendar->getPrincipalURI());
|
||||
unset($this->calendarInfo['uri']);
|
||||
|
||||
/** @var Plugin|MockObject $schedulingPlugin */
|
||||
$schedulingPlugin = $this->createMock(Plugin::class);
|
||||
|
||||
/** @var \Sabre\DAVACL\Plugin|MockObject $schedulingPlugin */
|
||||
$aclPlugin = $this->createMock(\Sabre\DAVACL\Plugin::class);
|
||||
|
||||
$server
|
||||
= $this->createMock(Server::class);
|
||||
$server->expects($this->any())
|
||||
->method('getPlugin')
|
||||
->willReturnMap([
|
||||
['auth', $authPlugin],
|
||||
['acl', $aclPlugin],
|
||||
['caldav-schedule', $schedulingPlugin]
|
||||
]);
|
||||
$server->expects(self::never())
|
||||
->method('emit');
|
||||
|
||||
$invitationResponseServer = $this->createPartialMock(InvitationResponseServer::class, ['getServer']);
|
||||
$invitationResponseServer->server = $server;
|
||||
$invitationResponseServer->expects($this->any())
|
||||
->method('getServer')
|
||||
->willReturn($server);
|
||||
|
||||
$calendarImpl = $this->getMockBuilder(CalendarImpl::class)
|
||||
->setConstructorArgs([$this->calendar, $this->calendarInfo, $this->backend])
|
||||
->onlyMethods(['getInvitationResponseServer'])
|
||||
->getMock();
|
||||
$calendarImpl->expects($this->once())
|
||||
->method('getInvitationResponseServer')
|
||||
->willReturn($invitationResponseServer);
|
||||
|
||||
$message = <<<EOF
|
||||
BEGIN:VCALENDAR
|
||||
PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN
|
||||
METHOD:REPLY
|
||||
VERSION:2.0
|
||||
BEGIN:VEVENT
|
||||
ATTENDEE;PARTSTAT=ACCEPTED:mailto:lewis@stardew-tent-living.com
|
||||
ORGANIZER:mailto:pierre@generalstore.com
|
||||
UID:aUniqueUid
|
||||
SEQUENCE:2
|
||||
REQUEST-STATUS:2.0;Success
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
EOF;
|
||||
|
||||
$this->expectException(CalendarException::class);
|
||||
$calendarImpl->handleIMipMessage('filename.ics', $message);
|
||||
}
|
||||
|
||||
private function getITipMessage($calendarData): Message {
|
||||
$iTipMessage = new Message();
|
||||
/** @var VCalendar $vObject */
|
||||
$vObject = Reader::read($calendarData);
|
||||
/** @var VEvent $vEvent */
|
||||
$vEvent = $vObject->{'VEVENT'};
|
||||
$orgaizer = $vEvent->{'ORGANIZER'}->getValue();
|
||||
$attendee = $vEvent->{'ATTENDEE'}->getValue();
|
||||
|
||||
$iTipMessage->method = $vObject->{'METHOD'}->getValue();
|
||||
$iTipMessage->recipient = $orgaizer;
|
||||
$iTipMessage->sender = $attendee;
|
||||
$iTipMessage->uid = isset($vEvent->{'UID'}) ? $vEvent->{'UID'}->getValue() : '';
|
||||
$iTipMessage->component = 'VEVENT';
|
||||
$iTipMessage->sequence = isset($vEvent->{'SEQUENCE'}) ? (int)$vEvent->{'SEQUENCE'}->getValue() : 0;
|
||||
$iTipMessage->message = $vObject;
|
||||
return $iTipMessage;
|
||||
}
|
||||
|
||||
protected function mockExportGenerator(): Generator {
|
||||
foreach ($this->mockExportCollection as $entry) {
|
||||
yield $entry;
|
||||
}
|
||||
}
|
||||
|
||||
public function testExport(): void {
|
||||
// Arrange
|
||||
// construct calendar with a 1 hour event and same start/end time zones
|
||||
$vCalendar = new VCalendar();
|
||||
/** @var VEvent $vEvent */
|
||||
$vEvent = $vCalendar->add('VEVENT', []);
|
||||
$vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
|
||||
$vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
|
||||
$vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
|
||||
$vEvent->add('SUMMARY', 'Test Recurrence 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'
|
||||
]);
|
||||
// construct data store return
|
||||
$this->mockExportCollection[] = [
|
||||
'id' => 1,
|
||||
'calendardata' => $vCalendar->serialize()
|
||||
];
|
||||
$this->backend->expects($this->once())
|
||||
->method('exportCalendar')
|
||||
->with(1, $this->backend::CALENDAR_TYPE_CALENDAR, null)
|
||||
->willReturn($this->mockExportGenerator());
|
||||
|
||||
// Act
|
||||
foreach ($this->calendarImpl->export(null) as $entry) {
|
||||
$exported[] = $entry;
|
||||
}
|
||||
|
||||
// Assert
|
||||
$this->assertCount(1, $exported, 'Invalid exported items count');
|
||||
$calendarImpl->handleIMipMessage('fakeUser', $vObject->serialize());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ use OCP\AppFramework\Utility\ITimeFactory;
|
|||
use OCP\Calendar\Exceptions\CalendarException;
|
||||
use OCP\Calendar\ICalendar;
|
||||
use OCP\Calendar\ICalendarEventBuilder;
|
||||
use OCP\Calendar\ICalendarIsShared;
|
||||
use OCP\Calendar\ICalendarIsWritable;
|
||||
use OCP\Calendar\ICalendarProvider;
|
||||
use OCP\Calendar\ICalendarQuery;
|
||||
|
|
@ -221,17 +220,18 @@ class Manager implements IManager {
|
|||
}
|
||||
|
||||
/**
|
||||
* @since 31.0.0
|
||||
* @since 32.0.0
|
||||
*
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function handleIMipRequest(
|
||||
string $principalUri,
|
||||
string $sender,
|
||||
string $recipient,
|
||||
string $calendarData,
|
||||
public function handleIMip(
|
||||
string $userId,
|
||||
string $message,
|
||||
): bool {
|
||||
|
||||
$userCalendars = $this->getCalendarsForPrincipal($principalUri);
|
||||
$userUri = 'principals/users/' . $userId;
|
||||
|
||||
$userCalendars = $this->getCalendarsForPrincipal($userUri);
|
||||
if (empty($userCalendars)) {
|
||||
$this->logger->warning('iMip message could not be processed because user has no calendars');
|
||||
return false;
|
||||
|
|
@ -239,119 +239,17 @@ class Manager implements IManager {
|
|||
|
||||
try {
|
||||
/** @var VCalendar $vObject|null */
|
||||
$calendarObject = Reader::read($calendarData);
|
||||
$vObject = Reader::read($message);
|
||||
} catch (ParseException $e) {
|
||||
$this->logger->error('iMip message could not be processed because an error occurred while parsing the iMip message', ['exception' => $e]);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($calendarObject->METHOD) || $calendarObject->METHOD->getValue() !== 'REQUEST') {
|
||||
$this->logger->warning('iMip message contains an incorrect or invalid method');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($calendarObject->VEVENT)) {
|
||||
$this->logger->warning('iMip message contains no event');
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var VEvent|null $vEvent */
|
||||
$eventObject = $calendarObject->VEVENT;
|
||||
|
||||
if (!isset($eventObject->UID)) {
|
||||
$this->logger->warning('iMip message event dose not contains a UID');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($eventObject->ORGANIZER)) {
|
||||
$this->logger->warning('iMip message event dose not contains an organizer');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($eventObject->ATTENDEE)) {
|
||||
$this->logger->warning('iMip message event dose not contains any attendees');
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($eventObject->ATTENDEE as $entry) {
|
||||
$address = trim(str_replace('mailto:', '', $entry->getValue()));
|
||||
if ($address === $recipient) {
|
||||
$attendee = $address;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!isset($attendee)) {
|
||||
$this->logger->warning('iMip message event does not contain a attendee that matches the recipient');
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($userCalendars as $calendar) {
|
||||
|
||||
if (!$calendar instanceof ICalendarIsWritable && !$calendar instanceof ICalendarIsShared) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($calendar->isDeleted() || !$calendar->isWritable() || $calendar->isShared()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!empty($calendar->search($recipient, ['ATTENDEE'], ['uid' => $eventObject->UID->getValue()]))) {
|
||||
try {
|
||||
if ($calendar instanceof IHandleImipMessage) {
|
||||
$calendar->handleIMipMessage('', $calendarData);
|
||||
}
|
||||
return true;
|
||||
} catch (CalendarException $e) {
|
||||
$this->logger->error('An error occurred while processing the iMip message event', ['exception' => $e]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->logger->warning('iMip message event could not be processed because no corresponding event was found in any calendar');
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function handleIMipReply(
|
||||
string $principalUri,
|
||||
string $sender,
|
||||
string $recipient,
|
||||
string $calendarData,
|
||||
): bool {
|
||||
|
||||
$calendars = $this->getCalendarsForPrincipal($principalUri);
|
||||
if (empty($calendars)) {
|
||||
$this->logger->warning('iMip message could not be processed because user has no calendars');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
/** @var VCalendar $vObject|null */
|
||||
$vObject = Reader::read($calendarData);
|
||||
} catch (ParseException $e) {
|
||||
$this->logger->error('iMip message could not be processed because an error occurred while parsing the iMip message', ['exception' => $e]);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($vObject === null) {
|
||||
$this->logger->warning('iMip message contains an invalid calendar object');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($vObject->METHOD) || $vObject->METHOD->getValue() !== 'REPLY') {
|
||||
$this->logger->warning('iMip message contains an incorrect or invalid method');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($vObject->VEVENT)) {
|
||||
$this->logger->warning('iMip message contains no event');
|
||||
$this->logger->warning('iMip message does not contain any event(s)');
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var VEvent|null $vEvent */
|
||||
/** @var VEvent $vEvent */
|
||||
$vEvent = $vObject->VEVENT;
|
||||
|
||||
if (!isset($vEvent->UID)) {
|
||||
|
|
@ -369,58 +267,72 @@ class Manager implements IManager {
|
|||
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('iMip message event could not be processed because 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('iMip message event could not be processed because the event is in the past');
|
||||
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 IHandleImipMessage) {
|
||||
$o = $calendar->search($sender, ['ATTENDEE'], ['uid' => $vEvent->{'UID'}->getValue()]);
|
||||
if (!empty($o)) {
|
||||
$found = $calendar;
|
||||
$name = $o[0]['uri'];
|
||||
break;
|
||||
foreach ($userCalendars as $calendar) {
|
||||
if (!$calendar instanceof ICalendarIsWritable) {
|
||||
continue;
|
||||
}
|
||||
if ($calendar->isDeleted() || !$calendar->isWritable()) {
|
||||
continue;
|
||||
}
|
||||
if (!empty($calendar->search('', [], ['uid' => $vEvent->UID->getValue()]))) {
|
||||
try {
|
||||
if ($calendar instanceof IHandleImipMessage) {
|
||||
$calendar->handleIMipMessage($userId, $vObject->serialize());
|
||||
}
|
||||
return true;
|
||||
} catch (CalendarException $e) {
|
||||
$this->logger->error('iMip message could not be processed because an error occurred', ['exception' => $e]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($found)) {
|
||||
$this->logger->warning('iMip message event could not be processed because no corresponding event was found in any calendar', [
|
||||
'principalUri' => $principalUri,
|
||||
'eventUid' => $vEvent->{'UID'}->getValue(),
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
$this->logger->warning('iMip message could not be processed because no corresponding event was found in any calendar');
|
||||
|
||||
try {
|
||||
$found->handleIMipMessage($name, $calendarData); // sabre will handle the scheduling behind the scenes
|
||||
} catch (CalendarException $e) {
|
||||
$this->logger->error('An error occurred while processing the iMip message event', ['exception' => $e]);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 31.0.0
|
||||
*
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function handleIMipRequest(
|
||||
string $principalUri,
|
||||
string $sender,
|
||||
string $recipient,
|
||||
string $calendarData,
|
||||
): bool {
|
||||
if (empty($principalUri) || !str_starts_with($principalUri, 'principals/users/')) {
|
||||
$this->logger->error('Invalid principal URI provided for iMip request');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
$userId = substr($principalUri, 17);
|
||||
return $this->handleIMip($userId, $calendarData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 25.0.0
|
||||
*
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function handleIMipReply(
|
||||
string $principalUri,
|
||||
string $sender,
|
||||
string $recipient,
|
||||
string $calendarData,
|
||||
): bool {
|
||||
if (empty($principalUri) || !str_starts_with($principalUri, 'principals/users/')) {
|
||||
$this->logger->error('Invalid principal URI provided for iMip reply');
|
||||
return false;
|
||||
}
|
||||
$userId = substr($principalUri, 17);
|
||||
return $this->handleIMip($userId, $calendarData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 25.0.0
|
||||
*
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function handleIMipCancel(
|
||||
|
|
@ -430,111 +342,12 @@ class Manager implements IManager {
|
|||
string $recipient,
|
||||
string $calendarData,
|
||||
): bool {
|
||||
|
||||
$calendars = $this->getCalendarsForPrincipal($principalUri);
|
||||
if (empty($calendars)) {
|
||||
$this->logger->warning('iMip message could not be processed because user has no calendars');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
/** @var VCalendar $vObject|null */
|
||||
$vObject = Reader::read($calendarData);
|
||||
} catch (ParseException $e) {
|
||||
$this->logger->error('iMip message could not be processed because an error occurred while parsing the iMip message', ['exception' => $e]);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($vObject === null) {
|
||||
$this->logger->warning('iMip message contains an invalid calendar object');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($vObject->METHOD) || $vObject->METHOD->getValue() !== 'CANCEL') {
|
||||
$this->logger->warning('iMip message contains an incorrect or invalid method');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($vObject->VEVENT)) {
|
||||
$this->logger->warning('iMip message contains no event');
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var VEvent|null $vEvent */
|
||||
$vEvent = $vObject->{'VEVENT'};
|
||||
|
||||
if (!isset($vEvent->UID)) {
|
||||
$this->logger->warning('iMip message event dose not contains a UID');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($vEvent->ORGANIZER)) {
|
||||
$this->logger->warning('iMip message event dose not contains an organizer');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($vEvent->ATTENDEE)) {
|
||||
$this->logger->warning('iMip message event dose not contains any attendees');
|
||||
return false;
|
||||
}
|
||||
|
||||
$attendee = substr($vEvent->{'ATTENDEE'}->getValue(), 7);
|
||||
if (strcasecmp($recipient, $attendee) !== 0) {
|
||||
$this->logger->warning('iMip message event could not be processed because 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);
|
||||
$isNotOrganizer = ($replyTo !== null) ? (strcasecmp($sender, $organizer) !== 0 && strcasecmp($replyTo, $organizer) !== 0) : (strcasecmp($sender, $organizer) !== 0);
|
||||
if ($isNotOrganizer) {
|
||||
$this->logger->warning('iMip message event could not be processed because sender must be the ORGANIZER of this event');
|
||||
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('iMip message event could not be processed because the event is in the past');
|
||||
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 IHandleImipMessage) {
|
||||
$o = $calendar->search($recipient, ['ATTENDEE'], ['uid' => $vEvent->{'UID'}->getValue()]);
|
||||
if (!empty($o)) {
|
||||
$found = $calendar;
|
||||
$name = $o[0]['uri'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($found)) {
|
||||
$this->logger->warning('iMip message event could not be processed because no corresponding event was found in any calendar', [
|
||||
'principalUri' => $principalUri,
|
||||
'eventUid' => $vEvent->{'UID'}->getValue(),
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$found->handleIMipMessage($name, $calendarData); // sabre will handle the scheduling behind the scenes
|
||||
return true;
|
||||
} catch (CalendarException $e) {
|
||||
$this->logger->error('An error occurred while processing the iMip message event', ['exception' => $e]);
|
||||
if (empty($principalUri) || !str_starts_with($principalUri, 'principals/users/')) {
|
||||
$this->logger->error('Invalid principal URI provided for iMip cancel');
|
||||
return false;
|
||||
}
|
||||
$userId = substr($principalUri, 17);
|
||||
return $this->handleIMip($userId, $calendarData);
|
||||
}
|
||||
|
||||
public function createEventBuilder(): ICalendarEventBuilder {
|
||||
|
|
|
|||
|
|
@ -140,6 +140,15 @@ interface IManager {
|
|||
*/
|
||||
public function newQuery(string $principalUri) : ICalendarQuery;
|
||||
|
||||
/**
|
||||
* Handles a iMip message
|
||||
*
|
||||
* @since 32.0.0
|
||||
*
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function handleIMip(string $userId, string $message): bool;
|
||||
|
||||
/**
|
||||
* Handle a iMip REQUEST message
|
||||
*
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue