summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristoph Wurst <christoph@winzerhof-wurst.at>2021-02-03 15:01:58 +0100
committerbackportbot[bot] <backportbot[bot]@users.noreply.github.com>2021-02-03 18:12:50 +0000
commitf4b32f967602310dd3f4438c938ddfe562d4f11b (patch)
tree6dd2baefe803e65d708c18559423656be2fd78bf
parent62f9f4f7fc0db53ab9eb238413a6e623fb59f353 (diff)
downloadnextcloud-server-f4b32f967602310dd3f4438c938ddfe562d4f11b.tar.gz
nextcloud-server-f4b32f967602310dd3f4438c938ddfe562d4f11b.zip
Parse calendar object for attendees and emit interaction events
Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
-rw-r--r--apps/dav/lib/Listener/CalendarContactInteractionListener.php70
-rw-r--r--apps/dav/tests/unit/Listener/CalendarContactInteractionListenerTest.php202
2 files changed, 267 insertions, 5 deletions
diff --git a/apps/dav/lib/Listener/CalendarContactInteractionListener.php b/apps/dav/lib/Listener/CalendarContactInteractionListener.php
index 62bddd500e4..1a04d44f6c6 100644
--- a/apps/dav/lib/Listener/CalendarContactInteractionListener.php
+++ b/apps/dav/lib/Listener/CalendarContactInteractionListener.php
@@ -35,7 +35,13 @@ use OCP\EventDispatcher\IEventDispatcher;
use OCP\EventDispatcher\IEventListener;
use OCP\IUser;
use OCP\IUserSession;
+use OCP\Mail\IMailer;
use Psr\Log\LoggerInterface;
+use Sabre\VObject\Component\VEvent;
+use Sabre\VObject\Parameter;
+use Sabre\VObject\Property;
+use Sabre\VObject\Reader;
+use Throwable;
use function strlen;
use function strpos;
use function substr;
@@ -47,39 +53,59 @@ class CalendarContactInteractionListener implements IEventListener {
private $dispatcher;
/** @var IUserSession */
- private $userManager;
+ private $userSession;
/** @var Principal */
private $principalConnector;
+ /** @var IMailer */
+ private $mailer;
+
/** @var LoggerInterface */
private $logger;
public function __construct(IEventDispatcher $dispatcher,
- IUserSession $userManager,
+ IUserSession $userSession,
Principal $principalConnector,
+ IMailer $mailer,
LoggerInterface $logger) {
$this->dispatcher = $dispatcher;
- $this->userManager = $userManager;
+ $this->userSession = $userSession;
$this->principalConnector = $principalConnector;
+ $this->mailer = $mailer;
$this->logger = $logger;
}
public function handle(Event $event): void {
- if (($user = $this->userManager->getUser()) === null) {
+ if (($user = $this->userSession->getUser()) === null) {
// Without user context we can't do anything
return;
}
if ($event instanceof CalendarObjectCreatedEvent || $event instanceof CalendarObjectUpdatedEvent) {
// users: href => principal:principals/users/admin
- // TODO: parse (email) attendees from the VCARD
foreach ($event->getShares() as $share) {
if (!isset($share['href'])) {
continue;
}
$this->emitFromUri($share['href'], $user);
}
+
+ // emit interaction for email attendees as well
+ if (isset($event->getObjectData()['calendardata'])) {
+ try {
+ $calendar = Reader::read($event->getObjectData()['calendardata']);
+ if ($calendar->VEVENT) {
+ foreach ($calendar->VEVENT as $calendarEvent) {
+ $this->emitFromObject($calendarEvent, $user);
+ }
+ }
+ } catch (Throwable $e) {
+ $this->logger->warning('Could not read calendar data for interaction events: ' . $e->getMessage(), [
+ 'exception' => $e,
+ ]);
+ }
+ }
}
if ($event instanceof CalendarShareUpdatedEvent && !empty($event->getAdded())) {
@@ -114,4 +140,38 @@ class CalendarContactInteractionListener implements IEventListener {
(new ContactInteractedWithEvent($user))->setUid($uid)
);
}
+
+ private function emitFromObject(VEvent $vevent, IUser $user): void {
+ if (!$vevent->ATTENDEE) {
+ // Nothing left to do
+ return;
+ }
+
+ foreach ($vevent->ATTENDEE as $attendee) {
+ if (!($attendee instanceof Property)) {
+ continue;
+ }
+
+ $cuType = $attendee->offsetGet('CUTYPE');
+ if ($cuType instanceof Parameter && $cuType->getValue() !== 'INDIVIDUAL') {
+ // Don't care about those
+ continue;
+ }
+
+ $mailTo = $attendee->getValue();
+ if (strpos($mailTo, 'mailto:') !== 0) {
+ // Doesn't look like an email
+ continue;
+ }
+ $email = substr($mailTo, strlen('mailto:'));
+ if (!$this->mailer->validateMailAddress($email)) {
+ // This really isn't a valid email
+ continue;
+ }
+
+ $this->dispatcher->dispatchTyped(
+ (new ContactInteractedWithEvent($user))->setEmail($email)
+ );
+ }
+ }
}
diff --git a/apps/dav/tests/unit/Listener/CalendarContactInteractionListenerTest.php b/apps/dav/tests/unit/Listener/CalendarContactInteractionListenerTest.php
new file mode 100644
index 00000000000..5a90f5440a0
--- /dev/null
+++ b/apps/dav/tests/unit/Listener/CalendarContactInteractionListenerTest.php
@@ -0,0 +1,202 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\DAV\Tests\Unit\Listener;
+
+use OCA\DAV\Connector\Sabre\Principal;
+use OCA\DAV\Events\CalendarObjectCreatedEvent;
+use OCA\DAV\Events\CalendarShareUpdatedEvent;
+use OCA\DAV\Listener\CalendarContactInteractionListener;
+use OCP\Contacts\Events\ContactInteractedWithEvent;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\IUser;
+use OCP\IUserSession;
+use OCP\Mail\IMailer;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Test\TestCase;
+
+class CalendarContactInteractionListenerTest extends TestCase {
+
+ /** @var IEventDispatcher|MockObject */
+ private $eventDispatcher;
+
+ /** @var IUserSession|MockObject */
+ private $userSession;
+
+ /** @var Principal|MockObject */
+ private $principalConnector;
+
+ /** @var LoggerInterface|MockObject */
+ private $logger;
+
+ /** @var IMailer|MockObject */
+ private $mailer;
+
+ /** @var CalendarContactInteractionListener */
+ private $listener;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->eventDispatcher = $this->createMock(IEventDispatcher::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->principalConnector = $this->createMock(Principal::class);
+ $this->mailer = $this->createMock(IMailer::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+
+ $this->listener = new CalendarContactInteractionListener(
+ $this->eventDispatcher,
+ $this->userSession,
+ $this->principalConnector,
+ $this->mailer,
+ $this->logger
+ );
+ }
+
+ public function testParseUnrelated(): void {
+ $event = new Event();
+ $this->eventDispatcher->expects(self::never())->method('dispatchTyped');
+
+ $this->listener->handle($event);
+ }
+
+ public function testHandleWithoutAnythingInteresting(): void {
+ $event = new CalendarShareUpdatedEvent(123, [], [], [], []);
+ $user = $this->createMock(IUser::class);
+ $this->userSession->expects(self::once())->method('getUser')->willReturn($user);
+ $this->eventDispatcher->expects(self::never())->method('dispatchTyped');
+
+ $this->listener->handle($event);
+ }
+
+ public function testParseInvalidData(): void {
+ $event = new CalendarObjectCreatedEvent(123, [], [], ['calendardata' => 'BEGIN:FOO']);
+ $user = $this->createMock(IUser::class);
+ $this->userSession->expects(self::once())->method('getUser')->willReturn($user);
+ $this->eventDispatcher->expects(self::never())->method('dispatchTyped');
+ $this->logger->expects(self::once())->method('warning');
+
+ $this->listener->handle($event);
+ }
+
+ public function testParseCalendarEventWithInvalidEmail(): void {
+ $event = new CalendarObjectCreatedEvent(123, [], [], ['calendardata' => <<<EVENT
+BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//IDN nextcloud.com//Calendar app 2.1.3//EN
+BEGIN:VTIMEZONE
+TZID:Europe/Vienna
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+CREATED:20210202T091151Z
+DTSTAMP:20210203T130231Z
+LAST-MODIFIED:20210203T130231Z
+SEQUENCE:9
+UID:b74a0c8e-93b0-447f-aed5-b679b19e874a
+DTSTART;TZID=Europe/Vienna:20210202T103000
+DTEND;TZID=Europe/Vienna:20210202T133000
+SUMMARY:tes
+ORGANIZER;CN=admin:mailto:christoph.wurst@nextcloud.com
+ATTENDEE;CN=somethingbutnotanemail;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION;
+ ROLE=REQ-PARTICIPANT;RSVP=FALSE:mailto:somethingbutnotanemail
+DESCRIPTION:test
+END:VEVENT
+END:VCALENDAR
+EVENT]);
+ $user = $this->createMock(IUser::class);
+ $this->userSession->expects(self::once())->method('getUser')->willReturn($user);
+ $this->eventDispatcher->expects(self::never())->method('dispatchTyped');
+ $this->logger->expects(self::never())->method('warning');
+
+ $this->listener->handle($event);
+ }
+
+ public function testParseCalendarEvent(): void {
+ $event = new CalendarObjectCreatedEvent(123, [], [], ['calendardata' => <<<EVENT
+BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//IDN nextcloud.com//Calendar app 2.1.3//EN
+BEGIN:VTIMEZONE
+TZID:Europe/Vienna
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+CREATED:20210202T091151Z
+DTSTAMP:20210203T130231Z
+LAST-MODIFIED:20210203T130231Z
+SEQUENCE:9
+UID:b74a0c8e-93b0-447f-aed5-b679b19e874a
+DTSTART;TZID=Europe/Vienna:20210202T103000
+DTEND;TZID=Europe/Vienna:20210202T133000
+SUMMARY:tes
+ORGANIZER;CN=admin:mailto:christoph.wurst@nextcloud.com
+ATTENDEE;CN=user@domain.tld;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION;
+ ROLE=REQ-PARTICIPANT;RSVP=FALSE:mailto:user@domain.tld
+DESCRIPTION:test
+END:VEVENT
+END:VCALENDAR
+EVENT]);
+ $user = $this->createMock(IUser::class);
+ $this->userSession->expects(self::once())->method('getUser')->willReturn($user);
+ $this->mailer->expects(self::once())->method('validateMailAddress')->willReturn(true);
+ $this->eventDispatcher->expects(self::once())
+ ->method('dispatchTyped')
+ ->with(self::equalTo((new ContactInteractedWithEvent($user))->setEmail('user@domain.tld')));
+ $this->logger->expects(self::never())->method('warning');
+
+ $this->listener->handle($event);
+ }
+}