123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467 |
- <?php
- declare(strict_types=1);
- /**
- * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
- 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\ICalendarIsShared;
- use OCP\Calendar\ICalendarIsWritable;
- use OCP\Calendar\ICalendarProvider;
- use OCP\Calendar\ICalendarQuery;
- use OCP\Calendar\ICreateFromString;
- use OCP\Calendar\IHandleImipMessage;
- 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;
- class Manager implements IManager {
- /**
- * @var ICalendar[] holds all registered calendars
- */
- private array $calendars = [];
- /**
- * @var \Closure[] to call to load/register calendar providers
- */
- private array $calendarLoaders = [];
- public function __construct(
- private Coordinator $coordinator,
- private ContainerInterface $container,
- private LoggerInterface $logger,
- private ITimeFactory $timeFactory,
- ) {
- }
- /**
- * This function is used to search and find objects within the user's calendars.
- * In case $pattern is empty all events/journals/todos will be returned.
- *
- * @param string $pattern which should match within the $searchProperties
- * @param array $searchProperties defines the properties within the query pattern should match
- * @param array $options - optional parameters:
- * ['timerange' => ['start' => new DateTime(...), 'end' => new DateTime(...)]]
- * @param integer|null $limit - limit number of search results
- * @param integer|null $offset - offset for paging of search results
- * @return array an array of events/journals/todos which are arrays of arrays of key-value-pairs
- * @since 13.0.0
- */
- public function search(
- $pattern,
- array $searchProperties = [],
- array $options = [],
- $limit = null,
- $offset = null,
- ): array {
- $this->loadCalendars();
- $result = [];
- foreach ($this->calendars as $calendar) {
- $r = $calendar->search($pattern, $searchProperties, $options, $limit, $offset);
- foreach ($r as $o) {
- $o['calendar-key'] = $calendar->getKey();
- $result[] = $o;
- }
- }
- return $result;
- }
- /**
- * Check if calendars are available
- *
- * @return bool true if enabled, false if not
- * @since 13.0.0
- */
- public function isEnabled(): bool {
- return !empty($this->calendars) || !empty($this->calendarLoaders);
- }
- /**
- * Registers a calendar
- *
- * @since 13.0.0
- */
- public function registerCalendar(ICalendar $calendar): void {
- $this->calendars[$calendar->getKey()] = $calendar;
- }
- /**
- * Unregisters a calendar
- *
- * @since 13.0.0
- */
- public function unregisterCalendar(ICalendar $calendar): void {
- unset($this->calendars[$calendar->getKey()]);
- }
- /**
- * In order to improve lazy loading a closure can be registered which will be called in case
- * calendars are actually requested
- *
- * @since 13.0.0
- */
- public function register(\Closure $callable): void {
- $this->calendarLoaders[] = $callable;
- }
- /**
- * @return ICalendar[]
- *
- * @since 13.0.0
- */
- public function getCalendars(): array {
- $this->loadCalendars();
- return array_values($this->calendars);
- }
- /**
- * removes all registered calendar instances
- *
- * @since 13.0.0
- */
- public function clear(): void {
- $this->calendars = [];
- $this->calendarLoaders = [];
- }
- /**
- * loads all calendars
- */
- private function loadCalendars(): void {
- foreach ($this->calendarLoaders as $callable) {
- $callable($this);
- }
- $this->calendarLoaders = [];
- }
- /**
- * @return ICreateFromString[]
- */
- public function getCalendarsForPrincipal(string $principalUri, array $calendarUris = []): array {
- $context = $this->coordinator->getRegistrationContext();
- if ($context === null) {
- return [];
- }
- return array_merge(
- ...array_map(function ($registration) use ($principalUri, $calendarUris) {
- try {
- /** @var ICalendarProvider $provider */
- $provider = $this->container->get($registration->getService());
- } catch (Throwable $e) {
- $this->logger->error('Could not load calendar provider ' . $registration->getService() . ': ' . $e->getMessage(), [
- 'exception' => $e,
- ]);
- return [];
- }
- return $provider->getCalendars($principalUri, $calendarUris);
- }, $context->getCalendarProviders())
- );
- }
- public function searchForPrincipal(ICalendarQuery $query): array {
- /** @var CalendarQuery $query */
- $calendars = $this->getCalendarsForPrincipal(
- $query->getPrincipalUri(),
- $query->getCalendarUris(),
- );
- $results = [];
- foreach ($calendars as $calendar) {
- $r = $calendar->search(
- $query->getSearchPattern() ?? '',
- $query->getSearchProperties(),
- $query->getOptions(),
- $query->getLimit(),
- $query->getOffset()
- );
- foreach ($r as $o) {
- $o['calendar-key'] = $calendar->getKey();
- $o['calendar-uri'] = $calendar->getUri();
- $results[] = $o;
- }
- }
- return $results;
- }
- public function newQuery(string $principalUri): ICalendarQuery {
- return new CalendarQuery($principalUri);
- }
- /**
- * @since 31.0.0
- * @throws \OCP\DB\Exception
- */
- public function handleIMipRequest(
- string $principalUri,
- string $sender,
- string $recipient,
- string $calendarData,
- ): bool {
-
- $userCalendars = $this->getCalendarsForPrincipal($principalUri);
- if (empty($userCalendars)) {
- $this->logger->warning('iMip message could not be processed because user has no calendars');
- return false;
- }
-
- /** @var VCalendar $vObject|null */
- $calendarObject = Reader::read($calendarData);
-
- 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;
- }
- $eventObject = $calendarObject->VEVENT;
- if (!isset($eventObject->UID)) {
- $this->logger->warning('iMip message event dose not contains a UID');
- 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 the 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 {
- /** @var VCalendar $vObject|null */
- $vObject = Reader::read($calendarData);
- if ($vObject === null) {
- return false;
- }
- /** @var VEvent|null $vEvent */
- $vEvent = $vObject->{'VEVENT'};
- if ($vEvent === null) {
- return false;
- }
- // 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 IHandleImipMessage) {
- $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 {
- /** @var VCalendar $vObject|null */
- $vObject = Reader::read($calendarData);
- if ($vObject === null) {
- return false;
- }
- /** @var VEvent|null $vEvent */
- $vEvent = $vObject->{'VEVENT'};
- if ($vEvent === null) {
- return false;
- }
- // 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);
- $isNotOrganizer = ($replyTo !== null) ? (strcasecmp($sender, $organizer) !== 0 && strcasecmp($replyTo, $organizer) !== 0) : (strcasecmp($sender, $organizer) !== 0);
- if ($isNotOrganizer) {
- $this->logger->warning('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('Only events in the future are processed');
- return false;
- }
- // Check if we have a calendar to work with
- $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 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->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;
- }
- }
- }
|