mirror of
https://github.com/nextcloud/server.git
synced 2024-12-29 00:18:42 +00:00
7ebeed45bd
Signed-off-by: SebastianKrupinski <krupinskis05@gmail.com>
468 lines
13 KiB
PHP
468 lines
13 KiB
PHP
<?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;
|
|
}
|
|
}
|
|
}
|