Skip to content

Commit 147ef55

Browse files
fix: aliases and capitalization of emails
Signed-off-by: SebastianKrupinski <krupinskis05@gmail.com> [skip ci]
1 parent 5d0c3c1 commit 147ef55

File tree

6 files changed

+380
-621
lines changed

6 files changed

+380
-621
lines changed

apps/dav/lib/CalDAV/CalendarImpl.php

Lines changed: 85 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@
2020
use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp;
2121
use Sabre\DAV\Exception\Conflict;
2222
use Sabre\VObject\Component\VCalendar;
23-
use Sabre\VObject\Component\VEvent;
2423
use Sabre\VObject\Component\VTimeZone;
2524
use Sabre\VObject\ITip\Message;
25+
use Sabre\VObject\ParseException;
2626
use Sabre\VObject\Property;
2727
use Sabre\VObject\Reader;
2828
use function Sabre\Uri\split as uriSplit;
@@ -36,6 +36,9 @@ public function __construct(
3636
) {
3737
}
3838

39+
private const DAV_PROPERTY_USER_ADDRESS = '{http://sabredav.org/ns}email-address';
40+
private const DAV_PROPERTY_USER_ADDRESSES = '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set';
41+
3942
/**
4043
* @return string defining the technical unique key
4144
* @since 13.0.0
@@ -210,58 +213,93 @@ public function createFromString(string $name, string $calendarData): void {
210213
* @throws CalendarException
211214
*/
212215
public function handleIMipMessage(string $name, string $calendarData): void {
213-
$server = $this->getInvitationResponseServer();
214-
215-
/** @var CustomPrincipalPlugin $plugin */
216-
$plugin = $server->getServer()->getPlugin('auth');
217-
// we're working around the previous implementation
218-
// that only allowed the public system principal to be used
219-
// so set the custom principal here
220-
$plugin->setCurrentPrincipal($this->calendar->getPrincipalURI());
221216

222-
if (empty($this->calendarInfo['uri'])) {
223-
throw new CalendarException('Could not write to calendar as URI parameter is missing');
217+
try {
218+
/** @var VCalendar $vObject|null */
219+
$vObject = Reader::read($calendarData);
220+
} catch (ParseException $e) {
221+
throw new CalendarException('iMip message could not be processed because an error occurred while parsing the iMip message', 0, $e);
224222
}
225-
// Force calendar change URI
226-
/** @var Schedule\Plugin $schedulingPlugin */
227-
$schedulingPlugin = $server->getServer()->getPlugin('caldav-schedule');
228-
// Let sabre handle the rest
229-
$iTipMessage = new Message();
230-
/** @var VCalendar $vObject */
231-
$vObject = Reader::read($calendarData);
232-
/** @var VEvent $vEvent */
233-
$vEvent = $vObject->{'VEVENT'};
234-
235-
if ($vObject->{'METHOD'} === null) {
236-
throw new CalendarException('No Method provided for scheduling data. Could not process message');
223+
// validate the iMip message
224+
if (!isset($vObject->METHOD)) {
225+
throw new CalendarException('iMip message contains no valid method');
237226
}
238-
239-
if (!isset($vEvent->{'ORGANIZER'}) || !isset($vEvent->{'ATTENDEE'})) {
240-
throw new CalendarException('Could not process scheduling data, neccessary data missing from ICAL');
227+
if (!isset($vObject->VEVENT)) {
228+
throw new CalendarException('iMip message contains no event');
241229
}
242-
$organizer = $vEvent->{'ORGANIZER'}->getValue();
243-
$attendee = $vEvent->{'ATTENDEE'}->getValue();
244-
245-
$iTipMessage->method = $vObject->{'METHOD'}->getValue();
246-
if ($iTipMessage->method === 'REQUEST') {
247-
$iTipMessage->sender = $organizer;
248-
$iTipMessage->recipient = $attendee;
249-
} elseif ($iTipMessage->method === 'REPLY') {
250-
if ($server->isExternalAttendee($vEvent->{'ATTENDEE'}->getValue())) {
251-
$iTipMessage->recipient = $organizer;
252-
} else {
253-
$iTipMessage->recipient = $attendee;
230+
if (!isset($vObject->VEVENT->UID)) {
231+
throw new CalendarException('iMip message event dose not contain a UID');
232+
}
233+
if (!isset($vObject->VEVENT->ORGANIZER)) {
234+
throw new CalendarException('iMip message event dose not contain an organizer');
235+
}
236+
if (!isset($vObject->VEVENT->ATTENDEE)) {
237+
throw new CalendarException('iMip message event dose not contain an attendee');
238+
}
239+
if (empty($this->calendarInfo['uri'])) {
240+
throw new CalendarException('Could not write to calendar as URI parameter is missing');
241+
}
242+
// construct dav server
243+
$server = $this->getInvitationResponseServer();
244+
/** @var CustomPrincipalPlugin $authPlugin */
245+
$authPlugin = $server->getServer()->getPlugin('auth');
246+
// we're working around the previous implementation
247+
// that only allowed the public system principal to be used
248+
// so set the custom principal here
249+
$authPlugin->setCurrentPrincipal($this->calendar->getPrincipalURI());
250+
// retrieve all users addresses
251+
$userProperties = $server->getServer()->getProperties($this->calendar->getPrincipalURI(), [ self::DAV_PROPERTY_USER_ADDRESS, self::DAV_PROPERTY_USER_ADDRESSES ]);
252+
$userAddress = 'mailto:' . ($userProperties[self::DAV_PROPERTY_USER_ADDRESS] ?? null);
253+
$userAddresses = $userProperties[self::DAV_PROPERTY_USER_ADDRESSES]->getHrefs() ?? [];
254+
$userAddresses = array_map('strtolower', array_map('urldecode', $userAddresses));
255+
// validate the method, recipient and sender
256+
$imipMethod = strtoupper($vObject->METHOD->getValue());
257+
if (in_array($imipMethod, ['REPLY', 'REFRESH'], true)) {
258+
// extract sender (REPLY and REFRESH method should only have one attendee)
259+
$sender = strtolower($vObject->VEVENT->ATTENDEE->getValue());
260+
// extract and verify the recipient
261+
$recipient = strtolower($vObject->VEVENT->ORGANIZER->getValue());
262+
if (!in_array($recipient, $userAddresses, true)) {
263+
throw new CalendarException('iMip message dose not contain an organizer that matches the user');
264+
}
265+
// if the recipient address is not the same as the user address this means an alias was used
266+
// the iTip broker uses the users primary email address during processing
267+
if ($userAddress !== $recipient) {
268+
$recipient = $userAddress;
269+
}
270+
} elseif (in_array($imipMethod, ['PUBLISH', 'REQUEST', 'ADD', 'CANCEL'], true)) {
271+
// extract sender
272+
$sender = strtolower($vObject->VEVENT->ORGANIZER->getValue());
273+
// extract and verify the recipient
274+
foreach ($vObject->VEVENT->ATTENDEE as $attendee) {
275+
$recipient = strtolower($attendee->getValue());
276+
if (in_array($recipient, $userAddresses, true)) {
277+
break;
278+
}
279+
$recipient = null;
280+
}
281+
if ($recipient === null) {
282+
throw new CalendarException('iMip message dose not contain an attendee that matches the user');
254283
}
255-
$iTipMessage->sender = $attendee;
256-
} elseif ($iTipMessage->method === 'CANCEL') {
257-
$iTipMessage->recipient = $attendee;
258-
$iTipMessage->sender = $organizer;
284+
// if the recipient address is not the same as the user address this means an alias was used
285+
// the iTip broker uses the users primary email address during processing
286+
if ($userAddress !== $recipient) {
287+
$recipient = $userAddress;
288+
}
289+
} else {
290+
throw new CalendarException('iMip message contains a method that is not supported: ' . $imipMethod);
259291
}
260-
$iTipMessage->uid = isset($vEvent->{'UID'}) ? $vEvent->{'UID'}->getValue() : '';
261-
$iTipMessage->component = 'VEVENT';
262-
$iTipMessage->sequence = isset($vEvent->{'SEQUENCE'}) ? (int)$vEvent->{'SEQUENCE'}->getValue() : 0;
263-
$iTipMessage->message = $vObject;
264-
$server->server->emit('schedule', [$iTipMessage]);
292+
// generate the iTip message
293+
$iTip = new Message();
294+
$iTip->method = $imipMethod;
295+
$iTip->sender = $sender;
296+
$iTip->recipient = $recipient;
297+
$iTip->component = 'VEVENT';
298+
$iTip->uid = $vObject->VEVENT->UID->getValue();
299+
$iTip->sequence = isset($vObject->VEVENT->SEQUENCE) ? (int)$vObject->VEVENT->SEQUENCE->getValue() : 1;
300+
$iTip->message = $vObject;
301+
302+
$server->server->emit('schedule', [$iTip]);
265303
}
266304

267305
public function getInvitationResponseServer(): InvitationResponseServer {

apps/dav/lib/CalDAV/Schedule/Plugin.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ public function propFind(PropFind $propFind, INode $node) {
132132
* @param string $principal
133133
* @return array
134134
*/
135-
protected function getAddressesForPrincipal($principal) {
135+
public function getAddressesForPrincipal($principal) {
136136
$result = parent::getAddressesForPrincipal($principal);
137137

138138
if ($result === null) {

apps/dav/tests/unit/CalDAV/CalendarImplTest.php

Lines changed: 102 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,12 @@
1010
use OCA\DAV\CalDAV\Calendar;
1111
use OCA\DAV\CalDAV\CalendarImpl;
1212
use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer;
13-
use OCA\DAV\CalDAV\Schedule\Plugin;
1413
use OCA\DAV\Connector\Sabre\Server;
1514
use OCP\Calendar\Exceptions\CalendarException;
1615
use PHPUnit\Framework\MockObject\MockObject;
1716
use Sabre\VObject\Component\VCalendar;
1817
use Sabre\VObject\Component\VEvent;
1918
use Sabre\VObject\ITip\Message;
20-
use Sabre\VObject\Reader;
2119

2220
class CalendarImplTest extends \Test\TestCase {
2321
/** @var CalendarImpl */
@@ -35,6 +33,7 @@ class CalendarImplTest extends \Test\TestCase {
3533
protected function setUp(): void {
3634
parent::setUp();
3735

36+
$this->backend = $this->createMock(CalDavBackend::class);
3837
$this->calendar = $this->createMock(Calendar::class);
3938
$this->calendarInfo = [
4039
'id' => 'fancy_id_123',
@@ -43,7 +42,7 @@ protected function setUp(): void {
4342
'uri' => '/this/is/a/uri',
4443
'principaluri' => 'principal/users/foobar'
4544
];
46-
$this->backend = $this->createMock(CalDavBackend::class);
45+
$this->calendarImpl = new CalendarImpl($this->calendar, $this->calendarInfo, $this->backend);
4746

4847
$this->calendarImpl = new CalendarImpl($this->calendar,
4948
$this->calendarInfo, $this->backend);
@@ -123,56 +122,128 @@ public function testGetPermissionAll(): void {
123122
$this->assertEquals(31, $this->calendarImpl->getPermissions());
124123
}
125124

126-
public function testHandleImipMessage(): void {
127-
$message = <<<EOF
128-
BEGIN:VCALENDAR
129-
PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN
130-
METHOD:REPLY
131-
VERSION:2.0
132-
BEGIN:VEVENT
133-
ATTENDEE;PARTSTAT=ACCEPTED:mailto:lewis@stardew-tent-living.com
134-
ORGANIZER:mailto:pierre@generalstore.com
135-
UID:aUniqueUid
136-
SEQUENCE:2
137-
REQUEST-STATUS:2.0;Success
138-
END:VEVENT
139-
END:VCALENDAR
140-
EOF;
125+
public function testHandleImipNoMethod(): void {
126+
// Arrange
127+
$vObject = $this->vCalendar1a;
128+
129+
$this->expectException(CalendarException::class);
130+
$this->expectExceptionMessage('iMip message contains no valid method');
131+
132+
// Act
133+
$this->calendarImpl->handleIMipMessage('fakeUser', $vObject->serialize());
134+
}
135+
136+
public function testHandleImipNoEvent(): void {
137+
// Arrange
138+
$vObject = $this->vCalendar1a;
139+
$vObject->add('METHOD', 'REQUEST');
140+
$vObject->remove('VEVENT');
141+
142+
$this->expectException(CalendarException::class);
143+
$this->expectExceptionMessage('iMip message contains no event');
144+
145+
// Act
146+
$this->calendarImpl->handleIMipMessage('fakeUser', $vObject->serialize());
147+
}
148+
149+
public function testHandleImipNoUid(): void {
150+
// Arrange
151+
$vObject = $this->vCalendar1a;
152+
$vObject->add('METHOD', 'REQUEST');
153+
$vObject->VEVENT->remove('UID');
154+
155+
$this->expectException(CalendarException::class);
156+
$this->expectExceptionMessage('iMip message event dose not contain a UID');
157+
158+
// Act
159+
$this->calendarImpl->handleIMipMessage('fakeUser', $vObject->serialize());
160+
}
161+
162+
public function testHandleImipNoOrganizer(): void {
163+
// Arrange
164+
$vObject = $this->vCalendar1a;
165+
$vObject->add('METHOD', 'REQUEST');
166+
$vObject->VEVENT->remove('ORGANIZER');
167+
168+
$this->expectException(CalendarException::class);
169+
$this->expectExceptionMessage('iMip message event dose not contain an organizer');
170+
171+
// Act
172+
$this->calendarImpl->handleIMipMessage('fakeUser', $vObject->serialize());
173+
}
174+
175+
public function testHandleImipNoAttendee(): void {
176+
// Arrange
177+
$vObject = $this->vCalendar1a;
178+
$vObject->add('METHOD', 'REQUEST');
179+
$vObject->VEVENT->remove('ATTENDEE');
180+
181+
$this->expectException(CalendarException::class);
182+
$this->expectExceptionMessage('iMip message event dose not contain an attendee');
183+
184+
// Act
185+
$this->calendarImpl->handleIMipMessage('fakeUser', $vObject->serialize());
186+
}
187+
188+
public function testHandleImipRequest(): void {
189+
$userAddressSet = new class([ 'mailto:attendee1@testing.com', '/remote.php/dav/principals/users/attendee1/', ]) {
190+
public function __construct(
191+
private array $hrefs,
192+
) {
193+
}
194+
public function getHrefs(): array {
195+
return $this->hrefs;
196+
}
197+
};
198+
199+
$vObject = $this->vCalendar1a;
200+
$vObject->add('METHOD', 'REQUEST');
201+
202+
$iTip = new Message();
203+
$iTip->method = 'REQUEST';
204+
$iTip->sender = $vObject->VEVENT->ORGANIZER->getValue();
205+
$iTip->recipient = $vObject->VEVENT->ATTENDEE->getValue();
206+
$iTip->component = 'VEVENT';
207+
$iTip->uid = $vObject->VEVENT->UID->getValue();
208+
$iTip->sequence = (int)$vObject->VEVENT->SEQUENCE->getValue() ?? 0;
209+
$iTip->message = $vObject;
141210

142211
/** @var CustomPrincipalPlugin|MockObject $authPlugin */
143212
$authPlugin = $this->createMock(CustomPrincipalPlugin::class);
144213
$authPlugin->expects(self::once())
145214
->method('setCurrentPrincipal')
146215
->with($this->calendar->getPrincipalURI());
147-
148216
/** @var \Sabre\DAVACL\Plugin|MockObject $aclPlugin */
149217
$aclPlugin = $this->createMock(\Sabre\DAVACL\Plugin::class);
150218

151-
/** @var Plugin|MockObject $schedulingPlugin */
152-
$schedulingPlugin = $this->createMock(Plugin::class);
153-
$iTipMessage = $this->getITipMessage($message);
154-
$iTipMessage->recipient = 'mailto:lewis@stardew-tent-living.com';
155-
156219
$server = $this->createMock(Server::class);
157220
$server->expects($this->any())
158221
->method('getPlugin')
159222
->willReturnMap([
160223
['auth', $authPlugin],
161224
['acl', $aclPlugin],
162-
['caldav-schedule', $schedulingPlugin]
225+
]);
226+
227+
$server->expects(self::once())
228+
->method('getProperties')
229+
->with(
230+
$this->calendar->getPrincipalURI(),
231+
[
232+
'{http://sabredav.org/ns}email-address',
233+
'{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'
234+
]
235+
)
236+
->willReturn([
237+
'{http://sabredav.org/ns}email-address' => 'attendee1@testing.com',
238+
'{urn:ietf:params:xml:ns:caldav}calendar-user-address-set' => $userAddressSet,
163239
]);
164240
$server->expects(self::once())
165241
->method('emit');
166-
167-
$invitationResponseServer = $this->createPartialMock(InvitationResponseServer::class, ['getServer', 'isExternalAttendee']);
242+
$invitationResponseServer = $this->createMock(InvitationResponseServer::class, ['getServer']);
168243
$invitationResponseServer->server = $server;
169244
$invitationResponseServer->expects($this->any())
170245
->method('getServer')
171246
->willReturn($server);
172-
$invitationResponseServer->expects(self::once())
173-
->method('isExternalAttendee')
174-
->willReturn(false);
175-
176247
$calendarImpl = $this->getMockBuilder(CalendarImpl::class)
177248
->setConstructorArgs([$this->calendar, $this->calendarInfo, $this->backend])
178249
->onlyMethods(['getInvitationResponseServer'])

0 commit comments

Comments
 (0)