diff options
author | Christoph Wurst <christoph@winzerhof-wurst.at> | 2021-02-03 15:01:58 +0100 |
---|---|---|
committer | backportbot[bot] <backportbot[bot]@users.noreply.github.com> | 2021-02-03 18:12:50 +0000 |
commit | f4b32f967602310dd3f4438c938ddfe562d4f11b (patch) | |
tree | 6dd2baefe803e65d708c18559423656be2fd78bf | |
parent | 62f9f4f7fc0db53ab9eb238413a6e623fb59f353 (diff) | |
download | nextcloud-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.php | 70 | ||||
-rw-r--r-- | apps/dav/tests/unit/Listener/CalendarContactInteractionListenerTest.php | 202 |
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); + } +} |